From dbecee50b599aaf0ca8ed67a426befd827ad2a96 Mon Sep 17 00:00:00 2001 From: Ron Northrip Date: Mon, 20 Jul 2026 21:45:46 -0400 Subject: [PATCH 1/3] Passing edges when the type is custom --- CHANGELOG.md | 6 + dist/alpineflow.bundle.esm.js | 7828 +++++++++++++++-------------- dist/alpineflow.bundle.esm.js.map | 2 +- docs/configuration/edges.md | 11 +- docs/edges/types.md | 6 +- src/core/edge-utils.test.ts | 51 + src/core/edge-utils.ts | 9 +- src/core/types.ts | 7 +- 8 files changed, 4094 insertions(+), 3826 deletions(-) create mode 100644 src/core/edge-utils.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0750add..60ec950 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Added — custom edge generators receive the `edge` + +Custom edge path generators registered via `flowCanvas({ edgeTypes })` are now called with the `edge` as a second argument — `edgeTypes[type](params, edge)` — where before they saw only the endpoint coordinates. That limitation forced any per-edge routing data (waypoints, lane/channel assignments) to be smuggled in through a **separate closure per edge**; because that data lived in a function rather than on the model, it was lost on `toObject()`/`fromObject()`, so a custom-routed edge came back unrouted after a reload or snapshot restore. A generator can now read its route straight off `edge.data`, where it serializes with the edge and survives the round-trip. + +Not a breaking change: the `edge` argument is optional and appended after the existing `params`, so every existing single-parameter generator keeps working unchanged. Thanks to [@ronnorthrip](https://github.com/ronnorthrip) for the report and change. + ### Added — schema render hooks: `x-flow-schema` customization without forking the directive `x-flow-schema` owns the DOM it builds, so consumers previously had to replace the whole directive to customize a node. Four new `flowCanvas({ … })` hooks augment its output per-render instead — read from the canvas config on every render, no-ops when unset, and firing only for `x-flow-schema` nodes: diff --git a/dist/alpineflow.bundle.esm.js b/dist/alpineflow.bundle.esm.js index 2cbe8e7..552551a 100644 --- a/dist/alpineflow.bundle.esm.js +++ b/dist/alpineflow.bundle.esm.js @@ -1,49 +1,49 @@ -let zo = null; -function Oa(t) { - zo = t; +let Bo = null; +function qa(t) { + Bo = t; } -function Ce() { - if (!zo) +function ke() { + if (!Bo) throw new Error("[AlpineFlow] Alpine not initialized. Ensure Alpine.plugin(AlpineFlow) was called."); - return zo; + return Bo; } -var za = { value: () => { +var Xa = { value: () => { } }; -function go() { +function po() { for (var t = 0, e = arguments.length, n = {}, o; t < e; ++t) { if (!(o = arguments[t] + "") || o in n || /[\s.]/.test(o)) throw new Error("illegal type: " + o); n[o] = []; } - return new On(n); + return new zn(n); } -function On(t) { +function zn(t) { this._ = t; } -function Va(t, e) { +function Ya(t, e) { return t.trim().split(/^|\s+/).map(function(n) { var o = "", i = n.indexOf("."); if (i >= 0 && (o = n.slice(i + 1), n = n.slice(0, i)), n && !e.hasOwnProperty(n)) throw new Error("unknown type: " + n); return { type: n, name: o }; }); } -On.prototype = go.prototype = { - constructor: On, +zn.prototype = po.prototype = { + constructor: zn, on: function(t, e) { - var n = this._, o = Va(t + "", n), i, r = -1, s = o.length; + var n = this._, o = Ya(t + "", n), i, r = -1, s = o.length; if (arguments.length < 2) { - for (; ++r < s; ) if ((i = (t = o[r]).type) && (i = Ba(n[i], t.name))) return i; + for (; ++r < s; ) if ((i = (t = o[r]).type) && (i = Wa(n[i], t.name))) return i; return; } if (e != null && typeof e != "function") throw new Error("invalid callback: " + e); for (; ++r < s; ) - if (i = (t = o[r]).type) n[i] = Di(n[i], t.name, e); - else if (e == null) for (i in n) n[i] = Di(n[i], t.name, null); + if (i = (t = o[r]).type) n[i] = Hi(n[i], t.name, e); + else if (e == null) for (i in n) n[i] = Hi(n[i], t.name, null); return this; }, copy: function() { var t = {}, e = this._; for (var n in e) t[n] = e[n].slice(); - return new On(t); + return new zn(t); }, call: function(t, e) { if ((i = arguments.length - 2) > 0) for (var n = new Array(i), o = 0, i, r; o < i; ++o) n[o] = arguments[o + 2]; @@ -55,129 +55,129 @@ On.prototype = go.prototype = { for (var o = this._[t], i = 0, r = o.length; i < r; ++i) o[i].value.apply(e, n); } }; -function Ba(t, e) { +function Wa(t, e) { for (var n = 0, o = t.length, i; n < o; ++n) if ((i = t[n]).name === e) return i.value; } -function Di(t, e, n) { +function Hi(t, e, n) { for (var o = 0, i = t.length; o < i; ++o) if (t[o].name === e) { - t[o] = za, t = t.slice(0, o).concat(t.slice(o + 1)); + t[o] = Xa, t = t.slice(0, o).concat(t.slice(o + 1)); break; } return n != null && t.push({ name: e, value: n }), t; } -var Vo = "http://www.w3.org/1999/xhtml"; -const Hi = { +var qo = "http://www.w3.org/1999/xhtml"; +const Fi = { svg: "http://www.w3.org/2000/svg", - xhtml: Vo, + xhtml: qo, xlink: "http://www.w3.org/1999/xlink", xml: "http://www.w3.org/XML/1998/namespace", xmlns: "http://www.w3.org/2000/xmlns/" }; -function po(t) { +function mo(t) { var e = t += "", n = e.indexOf(":"); - return n >= 0 && (e = t.slice(0, n)) !== "xmlns" && (t = t.slice(n + 1)), Hi.hasOwnProperty(e) ? { space: Hi[e], local: t } : t; + return n >= 0 && (e = t.slice(0, n)) !== "xmlns" && (t = t.slice(n + 1)), Fi.hasOwnProperty(e) ? { space: Fi[e], local: t } : t; } -function qa(t) { +function ja(t) { return function() { var e = this.ownerDocument, n = this.namespaceURI; - return n === Vo && e.documentElement.namespaceURI === Vo ? e.createElement(t) : e.createElementNS(n, t); + return n === qo && e.documentElement.namespaceURI === qo ? e.createElement(t) : e.createElementNS(n, t); }; } -function Ya(t) { +function Ua(t) { return function() { return this.ownerDocument.createElementNS(t.space, t.local); }; } -function rr(t) { - var e = po(t); - return (e.local ? Ya : qa)(e); +function ur(t) { + var e = mo(t); + return (e.local ? Ua : ja)(e); } -function Xa() { +function Ga() { } -function di(t) { - return t == null ? Xa : function() { +function fi(t) { + return t == null ? Ga : function() { return this.querySelector(t); }; } -function Wa(t) { - typeof t != "function" && (t = di(t)); +function Za(t) { + typeof t != "function" && (t = fi(t)); for (var e = this._groups, n = e.length, o = new Array(n), i = 0; i < n; ++i) for (var r = e[i], s = r.length, a = o[i] = new Array(s), l, c, d = 0; d < s; ++d) (l = r[d]) && (c = t.call(l, l.__data__, d, r)) && ("__data__" in l && (c.__data__ = l.__data__), a[d] = c); - return new Fe(o, this._parents); + return new Oe(o, this._parents); } -function ja(t) { +function Ka(t) { return t == null ? [] : Array.isArray(t) ? t : Array.from(t); } -function Ua() { +function Ja() { return []; } -function ar(t) { - return t == null ? Ua : function() { +function fr(t) { + return t == null ? Ja : function() { return this.querySelectorAll(t); }; } -function Ga(t) { +function Qa(t) { return function() { - return ja(t.apply(this, arguments)); + return Ka(t.apply(this, arguments)); }; } -function Za(t) { - typeof t == "function" ? t = Ga(t) : t = ar(t); +function el(t) { + typeof t == "function" ? t = Qa(t) : t = fr(t); for (var e = this._groups, n = e.length, o = [], i = [], r = 0; r < n; ++r) for (var s = e[r], a = s.length, l, c = 0; c < a; ++c) (l = s[c]) && (o.push(t.call(l, l.__data__, c, s)), i.push(l)); - return new Fe(o, i); + return new Oe(o, i); } -function lr(t) { +function hr(t) { return function() { return this.matches(t); }; } -function cr(t) { +function gr(t) { return function(e) { return e.matches(t); }; } -var Ka = Array.prototype.find; -function Ja(t) { +var tl = Array.prototype.find; +function nl(t) { return function() { - return Ka.call(this.children, t); + return tl.call(this.children, t); }; } -function Qa() { +function ol() { return this.firstElementChild; } -function el(t) { - return this.select(t == null ? Qa : Ja(typeof t == "function" ? t : cr(t))); +function il(t) { + return this.select(t == null ? ol : nl(typeof t == "function" ? t : gr(t))); } -var tl = Array.prototype.filter; -function nl() { +var sl = Array.prototype.filter; +function rl() { return Array.from(this.children); } -function ol(t) { +function al(t) { return function() { - return tl.call(this.children, t); + return sl.call(this.children, t); }; } -function il(t) { - return this.selectAll(t == null ? nl : ol(typeof t == "function" ? t : cr(t))); +function ll(t) { + return this.selectAll(t == null ? rl : al(typeof t == "function" ? t : gr(t))); } -function sl(t) { - typeof t != "function" && (t = lr(t)); +function cl(t) { + typeof t != "function" && (t = hr(t)); for (var e = this._groups, n = e.length, o = new Array(n), i = 0; i < n; ++i) for (var r = e[i], s = r.length, a = o[i] = [], l, c = 0; c < s; ++c) (l = r[c]) && t.call(l, l.__data__, c, r) && a.push(l); - return new Fe(o, this._parents); + return new Oe(o, this._parents); } -function dr(t) { +function pr(t) { return new Array(t.length); } -function rl() { - return new Fe(this._enter || this._groups.map(dr), this._parents); +function dl() { + return new Oe(this._enter || this._groups.map(pr), this._parents); } function Yn(t, e) { this.ownerDocument = t.ownerDocument, this.namespaceURI = t.namespaceURI, this._next = null, this._parent = t, this.__data__ = e; @@ -197,18 +197,18 @@ Yn.prototype = { return this._parent.querySelectorAll(t); } }; -function al(t) { +function ul(t) { return function() { return t; }; } -function ll(t, e, n, o, i, r) { +function fl(t, e, n, o, i, r) { for (var s = 0, a, l = e.length, c = r.length; s < c; ++s) (a = e[s]) ? (a.__data__ = r[s], o[s] = a) : n[s] = new Yn(t, r[s]); for (; s < l; ++s) (a = e[s]) && (i[s] = a); } -function cl(t, e, n, o, i, r, s) { +function hl(t, e, n, o, i, r, s) { var a, l, c = /* @__PURE__ */ new Map(), d = e.length, u = r.length, f = new Array(d), h; for (a = 0; a < d; ++a) (l = e[a]) && (f[a] = h = s.call(l, l.__data__, a, e) + "", c.has(h) ? i[a] = l : c.set(h, l)); @@ -217,50 +217,50 @@ function cl(t, e, n, o, i, r, s) { for (a = 0; a < d; ++a) (l = e[a]) && c.get(f[a]) === l && (i[a] = l); } -function dl(t) { +function gl(t) { return t.__data__; } -function ul(t, e) { - if (!arguments.length) return Array.from(this, dl); - var n = e ? cl : ll, o = this._parents, i = this._groups; - typeof t != "function" && (t = al(t)); +function pl(t, e) { + if (!arguments.length) return Array.from(this, gl); + var n = e ? hl : fl, o = this._parents, i = this._groups; + typeof t != "function" && (t = ul(t)); for (var r = i.length, s = new Array(r), a = new Array(r), l = new Array(r), c = 0; c < r; ++c) { - var d = o[c], u = i[c], f = u.length, h = fl(t.call(d, d && d.__data__, c, o)), p = h.length, g = a[c] = new Array(p), m = s[c] = new Array(p), y = l[c] = new Array(f); + var d = o[c], u = i[c], f = u.length, h = ml(t.call(d, d && d.__data__, c, o)), p = h.length, g = a[c] = new Array(p), m = s[c] = new Array(p), y = l[c] = new Array(f); n(d, u, g, m, y, h, e); - for (var _ = 0, S = 0, x, b; _ < p; ++_) + for (var _ = 0, k = 0, x, C; _ < p; ++_) if (x = g[_]) { - for (_ >= S && (S = _ + 1); !(b = m[S]) && ++S < p; ) ; - x._next = b || null; + for (_ >= k && (k = _ + 1); !(C = m[k]) && ++k < p; ) ; + x._next = C || null; } } - return s = new Fe(s, o), s._enter = a, s._exit = l, s; + return s = new Oe(s, o), s._enter = a, s._exit = l, s; } -function fl(t) { +function ml(t) { return typeof t == "object" && "length" in t ? t : Array.from(t); } -function hl() { - return new Fe(this._exit || this._groups.map(dr), this._parents); +function yl() { + return new Oe(this._exit || this._groups.map(pr), this._parents); } -function gl(t, e, n) { +function wl(t, e, n) { var o = this.enter(), i = this, r = this.exit(); return typeof t == "function" ? (o = t(o), o && (o = o.selection())) : o = o.append(t + ""), e != null && (i = e(i), i && (i = i.selection())), n == null ? r.remove() : n(r), o && i ? o.merge(i).order() : i; } -function pl(t) { +function vl(t) { for (var e = t.selection ? t.selection() : t, n = this._groups, o = e._groups, i = n.length, r = o.length, s = Math.min(i, r), a = new Array(i), l = 0; l < s; ++l) for (var c = n[l], d = o[l], u = c.length, f = a[l] = new Array(u), h, p = 0; p < u; ++p) (h = c[p] || d[p]) && (f[p] = h); for (; l < i; ++l) a[l] = n[l]; - return new Fe(a, this._parents); + return new Oe(a, this._parents); } -function ml() { +function _l() { for (var t = this._groups, e = -1, n = t.length; ++e < n; ) for (var o = t[e], i = o.length - 1, r = o[i], s; --i >= 0; ) (s = o[i]) && (r && s.compareDocumentPosition(r) ^ 4 && r.parentNode.insertBefore(s, r), r = s); return this; } -function yl(t) { - t || (t = wl); +function bl(t) { + t || (t = xl); function e(u, f) { return u && f ? t(u.__data__, f.__data__) : !u - !f; } @@ -269,19 +269,19 @@ function yl(t) { (c = s[d]) && (l[d] = c); l.sort(e); } - return new Fe(i, this._parents).order(); + return new Oe(i, this._parents).order(); } -function wl(t, e) { +function xl(t, e) { return t < e ? -1 : t > e ? 1 : t >= e ? 0 : NaN; } -function vl() { +function El() { var t = arguments[0]; return arguments[0] = this, t.apply(null, arguments), this; } -function _l() { +function Cl() { return Array.from(this); } -function bl() { +function Sl() { for (var t = this._groups, e = 0, n = t.length; e < n; ++e) for (var o = t[e], i = 0, r = o.length; i < r; ++i) { var s = o[i]; @@ -289,114 +289,114 @@ function bl() { } return null; } -function xl() { +function kl() { let t = 0; for (const e of this) ++t; return t; } -function El() { +function Ll() { return !this.node(); } -function Cl(t) { +function Pl(t) { for (var e = this._groups, n = 0, o = e.length; n < o; ++n) for (var i = e[n], r = 0, s = i.length, a; r < s; ++r) (a = i[r]) && t.call(a, a.__data__, r, i); return this; } -function Sl(t) { +function Ml(t) { return function() { this.removeAttribute(t); }; } -function kl(t) { +function Tl(t) { return function() { this.removeAttributeNS(t.space, t.local); }; } -function Ll(t, e) { +function Nl(t, e) { return function() { this.setAttribute(t, e); }; } -function Pl(t, e) { +function Al(t, e) { return function() { this.setAttributeNS(t.space, t.local, e); }; } -function Ml(t, e) { +function $l(t, e) { return function() { var n = e.apply(this, arguments); n == null ? this.removeAttribute(t) : this.setAttribute(t, n); }; } -function Tl(t, e) { +function Il(t, e) { return function() { var n = e.apply(this, arguments); n == null ? this.removeAttributeNS(t.space, t.local) : this.setAttributeNS(t.space, t.local, n); }; } -function Al(t, e) { - var n = po(t); +function Dl(t, e) { + var n = mo(t); if (arguments.length < 2) { var o = this.node(); return n.local ? o.getAttributeNS(n.space, n.local) : o.getAttribute(n); } - return this.each((e == null ? n.local ? kl : Sl : typeof e == "function" ? n.local ? Tl : Ml : n.local ? Pl : Ll)(n, e)); + return this.each((e == null ? n.local ? Tl : Ml : typeof e == "function" ? n.local ? Il : $l : n.local ? Al : Nl)(n, e)); } -function ur(t) { +function mr(t) { return t.ownerDocument && t.ownerDocument.defaultView || t.document && t || t.defaultView; } -function Nl(t) { +function Rl(t) { return function() { this.style.removeProperty(t); }; } -function $l(t, e, n) { +function Hl(t, e, n) { return function() { this.style.setProperty(t, e, n); }; } -function Il(t, e, n) { +function Fl(t, e, n) { return function() { var o = e.apply(this, arguments); o == null ? this.style.removeProperty(t) : this.style.setProperty(t, o, n); }; } -function Dl(t, e, n) { - return arguments.length > 1 ? this.each((e == null ? Nl : typeof e == "function" ? Il : $l)(t, e, n ?? "")) : Vt(this.node(), t); +function Ol(t, e, n) { + return arguments.length > 1 ? this.each((e == null ? Rl : typeof e == "function" ? Fl : Hl)(t, e, n ?? "")) : qt(this.node(), t); } -function Vt(t, e) { - return t.style.getPropertyValue(e) || ur(t).getComputedStyle(t, null).getPropertyValue(e); +function qt(t, e) { + return t.style.getPropertyValue(e) || mr(t).getComputedStyle(t, null).getPropertyValue(e); } -function Hl(t) { +function zl(t) { return function() { delete this[t]; }; } -function Rl(t, e) { +function Vl(t, e) { return function() { this[t] = e; }; } -function Fl(t, e) { +function Bl(t, e) { return function() { var n = e.apply(this, arguments); n == null ? delete this[t] : this[t] = n; }; } -function Ol(t, e) { - return arguments.length > 1 ? this.each((e == null ? Hl : typeof e == "function" ? Fl : Rl)(t, e)) : this.node()[t]; +function ql(t, e) { + return arguments.length > 1 ? this.each((e == null ? zl : typeof e == "function" ? Bl : Vl)(t, e)) : this.node()[t]; } -function fr(t) { +function yr(t) { return t.trim().split(/^|\s+/); } -function ui(t) { - return t.classList || new hr(t); +function hi(t) { + return t.classList || new wr(t); } -function hr(t) { - this._node = t, this._names = fr(t.getAttribute("class") || ""); +function wr(t) { + this._node = t, this._names = yr(t.getAttribute("class") || ""); } -hr.prototype = { +wr.prototype = { add: function(t) { var e = this._names.indexOf(t); e < 0 && (this._names.push(t), this._node.setAttribute("class", this._names.join(" "))); @@ -409,129 +409,129 @@ hr.prototype = { return this._names.indexOf(t) >= 0; } }; -function gr(t, e) { - for (var n = ui(t), o = -1, i = e.length; ++o < i; ) n.add(e[o]); +function vr(t, e) { + for (var n = hi(t), o = -1, i = e.length; ++o < i; ) n.add(e[o]); } -function pr(t, e) { - for (var n = ui(t), o = -1, i = e.length; ++o < i; ) n.remove(e[o]); +function _r(t, e) { + for (var n = hi(t), o = -1, i = e.length; ++o < i; ) n.remove(e[o]); } -function zl(t) { +function Xl(t) { return function() { - gr(this, t); + vr(this, t); }; } -function Vl(t) { +function Yl(t) { return function() { - pr(this, t); + _r(this, t); }; } -function Bl(t, e) { +function Wl(t, e) { return function() { - (e.apply(this, arguments) ? gr : pr)(this, t); + (e.apply(this, arguments) ? vr : _r)(this, t); }; } -function ql(t, e) { - var n = fr(t + ""); +function jl(t, e) { + var n = yr(t + ""); if (arguments.length < 2) { - for (var o = ui(this.node()), i = -1, r = n.length; ++i < r; ) if (!o.contains(n[i])) return !1; + for (var o = hi(this.node()), i = -1, r = n.length; ++i < r; ) if (!o.contains(n[i])) return !1; return !0; } - return this.each((typeof e == "function" ? Bl : e ? zl : Vl)(n, e)); + return this.each((typeof e == "function" ? Wl : e ? Xl : Yl)(n, e)); } -function Yl() { +function Ul() { this.textContent = ""; } -function Xl(t) { +function Gl(t) { return function() { this.textContent = t; }; } -function Wl(t) { +function Zl(t) { return function() { var e = t.apply(this, arguments); this.textContent = e ?? ""; }; } -function jl(t) { - return arguments.length ? this.each(t == null ? Yl : (typeof t == "function" ? Wl : Xl)(t)) : this.node().textContent; +function Kl(t) { + return arguments.length ? this.each(t == null ? Ul : (typeof t == "function" ? Zl : Gl)(t)) : this.node().textContent; } -function Ul() { +function Jl() { this.innerHTML = ""; } -function Gl(t) { +function Ql(t) { return function() { this.innerHTML = t; }; } -function Zl(t) { +function ec(t) { return function() { var e = t.apply(this, arguments); this.innerHTML = e ?? ""; }; } -function Kl(t) { - return arguments.length ? this.each(t == null ? Ul : (typeof t == "function" ? Zl : Gl)(t)) : this.node().innerHTML; +function tc(t) { + return arguments.length ? this.each(t == null ? Jl : (typeof t == "function" ? ec : Ql)(t)) : this.node().innerHTML; } -function Jl() { +function nc() { this.nextSibling && this.parentNode.appendChild(this); } -function Ql() { - return this.each(Jl); +function oc() { + return this.each(nc); } -function ec() { +function ic() { this.previousSibling && this.parentNode.insertBefore(this, this.parentNode.firstChild); } -function tc() { - return this.each(ec); +function sc() { + return this.each(ic); } -function nc(t) { - var e = typeof t == "function" ? t : rr(t); +function rc(t) { + var e = typeof t == "function" ? t : ur(t); return this.select(function() { return this.appendChild(e.apply(this, arguments)); }); } -function oc() { +function ac() { return null; } -function ic(t, e) { - var n = typeof t == "function" ? t : rr(t), o = e == null ? oc : typeof e == "function" ? e : di(e); +function lc(t, e) { + var n = typeof t == "function" ? t : ur(t), o = e == null ? ac : typeof e == "function" ? e : fi(e); return this.select(function() { return this.insertBefore(n.apply(this, arguments), o.apply(this, arguments) || null); }); } -function sc() { +function cc() { var t = this.parentNode; t && t.removeChild(this); } -function rc() { - return this.each(sc); +function dc() { + return this.each(cc); } -function ac() { +function uc() { var t = this.cloneNode(!1), e = this.parentNode; return e ? e.insertBefore(t, this.nextSibling) : t; } -function lc() { +function fc() { var t = this.cloneNode(!0), e = this.parentNode; return e ? e.insertBefore(t, this.nextSibling) : t; } -function cc(t) { - return this.select(t ? lc : ac); +function hc(t) { + return this.select(t ? fc : uc); } -function dc(t) { +function gc(t) { return arguments.length ? this.property("__data__", t) : this.node().__data__; } -function uc(t) { +function pc(t) { return function(e) { t.call(this, e, this.__data__); }; } -function fc(t) { +function mc(t) { return t.trim().split(/^|\s+/).map(function(e) { var n = "", o = e.indexOf("."); return o >= 0 && (n = e.slice(o + 1), e = e.slice(0, o)), { type: e, name: n }; }); } -function hc(t) { +function yc(t) { return function() { var e = this.__on; if (e) { @@ -541,9 +541,9 @@ function hc(t) { } }; } -function gc(t, e, n) { +function wc(t, e, n) { return function() { - var o = this.__on, i, r = uc(e); + var o = this.__on, i, r = pc(e); if (o) { for (var s = 0, a = o.length; s < a; ++s) if ((i = o[s]).type === t.type && i.name === t.name) { @@ -554,8 +554,8 @@ function gc(t, e, n) { this.addEventListener(t.type, r, n), i = { type: t.type, name: t.name, value: e, listener: r, options: n }, o ? o.push(i) : this.__on = [i]; }; } -function pc(t, e, n) { - var o = fc(t + ""), i, r = o.length, s; +function vc(t, e, n) { + var o = mc(t + ""), i, r = o.length, s; if (arguments.length < 2) { var a = this.node().__on; if (a) { @@ -566,89 +566,89 @@ function pc(t, e, n) { } return; } - for (a = e ? gc : hc, i = 0; i < r; ++i) this.each(a(o[i], e, n)); + for (a = e ? wc : yc, i = 0; i < r; ++i) this.each(a(o[i], e, n)); return this; } -function mr(t, e, n) { - var o = ur(t), i = o.CustomEvent; +function br(t, e, n) { + var o = mr(t), i = o.CustomEvent; typeof i == "function" ? i = new i(e, n) : (i = o.document.createEvent("Event"), n ? (i.initEvent(e, n.bubbles, n.cancelable), i.detail = n.detail) : i.initEvent(e, !1, !1)), t.dispatchEvent(i); } -function mc(t, e) { +function _c(t, e) { return function() { - return mr(this, t, e); + return br(this, t, e); }; } -function yc(t, e) { +function bc(t, e) { return function() { - return mr(this, t, e.apply(this, arguments)); + return br(this, t, e.apply(this, arguments)); }; } -function wc(t, e) { - return this.each((typeof e == "function" ? yc : mc)(t, e)); +function xc(t, e) { + return this.each((typeof e == "function" ? bc : _c)(t, e)); } -function* vc() { +function* Ec() { for (var t = this._groups, e = 0, n = t.length; e < n; ++e) for (var o = t[e], i = 0, r = o.length, s; i < r; ++i) (s = o[i]) && (yield s); } -var yr = [null]; -function Fe(t, e) { +var xr = [null]; +function Oe(t, e) { this._groups = t, this._parents = e; } -function xn() { - return new Fe([[document.documentElement]], yr); +function Cn() { + return new Oe([[document.documentElement]], xr); } -function _c() { +function Cc() { return this; } -Fe.prototype = xn.prototype = { - constructor: Fe, - select: Wa, - selectAll: Za, - selectChild: el, - selectChildren: il, - filter: sl, - data: ul, - enter: rl, - exit: hl, - join: gl, - merge: pl, - selection: _c, - order: ml, - sort: yl, - call: vl, - nodes: _l, - node: bl, - size: xl, - empty: El, - each: Cl, - attr: Al, - style: Dl, - property: Ol, - classed: ql, - text: jl, - html: Kl, - raise: Ql, - lower: tc, - append: nc, - insert: ic, - remove: rc, - clone: cc, - datum: dc, - on: pc, - dispatch: wc, - [Symbol.iterator]: vc +Oe.prototype = Cn.prototype = { + constructor: Oe, + select: Za, + selectAll: el, + selectChild: il, + selectChildren: ll, + filter: cl, + data: pl, + enter: dl, + exit: yl, + join: wl, + merge: vl, + selection: Cc, + order: _l, + sort: bl, + call: El, + nodes: Cl, + node: Sl, + size: kl, + empty: Ll, + each: Pl, + attr: Dl, + style: Ol, + property: ql, + classed: jl, + text: Kl, + html: tc, + raise: oc, + lower: sc, + append: rc, + insert: lc, + remove: dc, + clone: hc, + datum: gc, + on: vc, + dispatch: xc, + [Symbol.iterator]: Ec }; -function Ye(t) { - return typeof t == "string" ? new Fe([[document.querySelector(t)]], [document.documentElement]) : new Fe([[t]], yr); +function Xe(t) { + return typeof t == "string" ? new Oe([[document.querySelector(t)]], [document.documentElement]) : new Oe([[t]], xr); } -function bc(t) { +function Sc(t) { let e; for (; e = t.sourceEvent; ) t = e; return t; } function Qe(t, e) { - if (t = bc(t), e === void 0 && (e = t.currentTarget), e) { + if (t = Sc(t), e === void 0 && (e = t.currentTarget), e) { var n = e.ownerSVGElement || e; if (n.createSVGPoint) { var o = n.createSVGPoint(); @@ -661,25 +661,25 @@ function Qe(t, e) { } return [t.pageX, t.pageY]; } -const xc = { passive: !1 }, fn = { capture: !0, passive: !1 }; -function Eo(t) { +const kc = { passive: !1 }, gn = { capture: !0, passive: !1 }; +function So(t) { t.stopImmediatePropagation(); } -function Rt(t) { +function Ot(t) { t.preventDefault(), t.stopImmediatePropagation(); } -function wr(t) { - var e = t.document.documentElement, n = Ye(t).on("dragstart.drag", Rt, fn); - "onselectstart" in e ? n.on("selectstart.drag", Rt, fn) : (e.__noselect = e.style.MozUserSelect, e.style.MozUserSelect = "none"); +function Er(t) { + var e = t.document.documentElement, n = Xe(t).on("dragstart.drag", Ot, gn); + "onselectstart" in e ? n.on("selectstart.drag", Ot, gn) : (e.__noselect = e.style.MozUserSelect, e.style.MozUserSelect = "none"); } -function vr(t, e) { - var n = t.document.documentElement, o = Ye(t).on("dragstart.drag", null); - e && (o.on("click.drag", Rt, fn), setTimeout(function() { +function Cr(t, e) { + var n = t.document.documentElement, o = Xe(t).on("dragstart.drag", null); + e && (o.on("click.drag", Ot, gn), setTimeout(function() { o.on("click.drag", null); }, 0)), "onselectstart" in n ? o.on("selectstart.drag", null) : (n.style.MozUserSelect = n.__noselect, delete n.__noselect); } -const Pn = (t) => () => t; -function Bo(t, { +const Mn = (t) => () => t; +function Xo(t, { sourceEvent: e, subject: n, target: o, @@ -705,66 +705,66 @@ function Bo(t, { _: { value: d } }); } -Bo.prototype.on = function() { +Xo.prototype.on = function() { var t = this._.on.apply(this._, arguments); return t === this._ ? this : t; }; -function Ec(t) { +function Lc(t) { return !t.ctrlKey && !t.button; } -function Cc() { +function Pc() { return this.parentNode; } -function Sc(t, e) { +function Mc(t, e) { return e ?? { x: t.x, y: t.y }; } -function kc() { +function Tc() { return navigator.maxTouchPoints || "ontouchstart" in this; } -function Lc() { - var t = Ec, e = Cc, n = Sc, o = kc, i = {}, r = go("start", "drag", "end"), s = 0, a, l, c, d, u = 0; +function Nc() { + var t = Lc, e = Pc, n = Mc, o = Tc, i = {}, r = po("start", "drag", "end"), s = 0, a, l, c, d, u = 0; function f(x) { - x.on("mousedown.drag", h).filter(o).on("touchstart.drag", m).on("touchmove.drag", y, xc).on("touchend.drag touchcancel.drag", _).style("touch-action", "none").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)"); + x.on("mousedown.drag", h).filter(o).on("touchstart.drag", m).on("touchmove.drag", y, kc).on("touchend.drag touchcancel.drag", _).style("touch-action", "none").style("-webkit-tap-highlight-color", "rgba(0,0,0,0)"); } - function h(x, b) { - if (!(d || !t.call(this, x, b))) { - var C = S(this, e.call(this, x, b), x, b, "mouse"); - C && (Ye(x.view).on("mousemove.drag", p, fn).on("mouseup.drag", g, fn), wr(x.view), Eo(x), c = !1, a = x.clientX, l = x.clientY, C("start", x)); + function h(x, C) { + if (!(d || !t.call(this, x, C))) { + var E = k(this, e.call(this, x, C), x, C, "mouse"); + E && (Xe(x.view).on("mousemove.drag", p, gn).on("mouseup.drag", g, gn), Er(x.view), So(x), c = !1, a = x.clientX, l = x.clientY, E("start", x)); } } function p(x) { - if (Rt(x), !c) { - var b = x.clientX - a, C = x.clientY - l; - c = b * b + C * C > u; + if (Ot(x), !c) { + var C = x.clientX - a, E = x.clientY - l; + c = C * C + E * E > u; } i.mouse("drag", x); } function g(x) { - Ye(x.view).on("mousemove.drag mouseup.drag", null), vr(x.view, c), Rt(x), i.mouse("end", x); + Xe(x.view).on("mousemove.drag mouseup.drag", null), Cr(x.view, c), Ot(x), i.mouse("end", x); } - function m(x, b) { - if (t.call(this, x, b)) { - var C = x.changedTouches, E = e.call(this, x, b), T = C.length, I, M; - for (I = 0; I < T; ++I) - (M = S(this, E, x, b, C[I].identifier, C[I])) && (Eo(x), M("start", x, C[I])); + function m(x, C) { + if (t.call(this, x, C)) { + var E = x.changedTouches, b = e.call(this, x, C), T = E.length, D, L; + for (D = 0; D < T; ++D) + (L = k(this, b, x, C, E[D].identifier, E[D])) && (So(x), L("start", x, E[D])); } } function y(x) { - var b = x.changedTouches, C = b.length, E, T; - for (E = 0; E < C; ++E) - (T = i[b[E].identifier]) && (Rt(x), T("drag", x, b[E])); + var C = x.changedTouches, E = C.length, b, T; + for (b = 0; b < E; ++b) + (T = i[C[b].identifier]) && (Ot(x), T("drag", x, C[b])); } function _(x) { - var b = x.changedTouches, C = b.length, E, T; + var C = x.changedTouches, E = C.length, b, T; for (d && clearTimeout(d), d = setTimeout(function() { d = null; - }, 500), E = 0; E < C; ++E) - (T = i[b[E].identifier]) && (Eo(x), T("end", x, b[E])); + }, 500), b = 0; b < E; ++b) + (T = i[C[b].identifier]) && (So(x), T("end", x, C[b])); } - function S(x, b, C, E, T, I) { - var M = r.copy(), A = Qe(I || C, b), P, v, w; - if ((w = n.call(x, new Bo("beforestart", { - sourceEvent: C, + function k(x, C, E, b, T, D) { + var L = r.copy(), A = Qe(D || E, C), P, v, w; + if ((w = n.call(x, new Xo("beforestart", { + sourceEvent: E, target: f, identifier: T, active: s, @@ -772,48 +772,48 @@ function Lc() { y: A[1], dx: 0, dy: 0, - dispatch: M - }), E)) != null) - return P = w.x - A[0] || 0, v = w.y - A[1] || 0, function N(k, R, O) { - var q = A, D; - switch (k) { + dispatch: L + }), b)) != null) + return P = w.x - A[0] || 0, v = w.y - A[1] || 0, function I(M, R, z) { + var V = A, $; + switch (M) { case "start": - i[T] = N, D = s++; + i[T] = I, $ = s++; break; case "end": delete i[T], --s; // falls through case "drag": - A = Qe(O || R, b), D = s; + A = Qe(z || R, C), $ = s; break; } - M.call( - k, + L.call( + M, x, - new Bo(k, { + new Xo(M, { sourceEvent: R, subject: w, target: f, identifier: T, - active: D, + active: $, x: A[0] + P, y: A[1] + v, - dx: A[0] - q[0], - dy: A[1] - q[1], - dispatch: M + dx: A[0] - V[0], + dy: A[1] - V[1], + dispatch: L }), - E + b ); }; } return f.filter = function(x) { - return arguments.length ? (t = typeof x == "function" ? x : Pn(!!x), f) : t; + return arguments.length ? (t = typeof x == "function" ? x : Mn(!!x), f) : t; }, f.container = function(x) { - return arguments.length ? (e = typeof x == "function" ? x : Pn(x), f) : e; + return arguments.length ? (e = typeof x == "function" ? x : Mn(x), f) : e; }, f.subject = function(x) { - return arguments.length ? (n = typeof x == "function" ? x : Pn(x), f) : n; + return arguments.length ? (n = typeof x == "function" ? x : Mn(x), f) : n; }, f.touchable = function(x) { - return arguments.length ? (o = typeof x == "function" ? x : Pn(!!x), f) : o; + return arguments.length ? (o = typeof x == "function" ? x : Mn(!!x), f) : o; }, f.on = function() { var x = r.on.apply(r, arguments); return x === r ? f : x; @@ -821,17 +821,17 @@ function Lc() { return arguments.length ? (u = (x = +x) * x, f) : Math.sqrt(u); }, f; } -function fi(t, e, n) { +function gi(t, e, n) { t.prototype = e.prototype = n, n.constructor = t; } -function _r(t, e) { +function Sr(t, e) { var n = Object.create(t.prototype); for (var o in e) n[o] = e[o]; return n; } -function En() { +function Sn() { } -var hn = 0.7, Xn = 1 / hn, Ft = "\\s*([+-]?\\d+)\\s*", gn = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*", Ze = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*", Pc = /^#([0-9a-f]{3,8})$/, Mc = new RegExp(`^rgb\\(${Ft},${Ft},${Ft}\\)$`), Tc = new RegExp(`^rgb\\(${Ze},${Ze},${Ze}\\)$`), Ac = new RegExp(`^rgba\\(${Ft},${Ft},${Ft},${gn}\\)$`), Nc = new RegExp(`^rgba\\(${Ze},${Ze},${Ze},${gn}\\)$`), $c = new RegExp(`^hsl\\(${gn},${Ze},${Ze}\\)$`), Ic = new RegExp(`^hsla\\(${gn},${Ze},${Ze},${gn}\\)$`), Ri = { +var pn = 0.7, Wn = 1 / pn, zt = "\\s*([+-]?\\d+)\\s*", mn = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*", Ze = "\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*", Ac = /^#([0-9a-f]{3,8})$/, $c = new RegExp(`^rgb\\(${zt},${zt},${zt}\\)$`), Ic = new RegExp(`^rgb\\(${Ze},${Ze},${Ze}\\)$`), Dc = new RegExp(`^rgba\\(${zt},${zt},${zt},${mn}\\)$`), Rc = new RegExp(`^rgba\\(${Ze},${Ze},${Ze},${mn}\\)$`), Hc = new RegExp(`^hsl\\(${mn},${Ze},${Ze}\\)$`), Fc = new RegExp(`^hsla\\(${mn},${Ze},${Ze},${mn}\\)$`), Oi = { aliceblue: 15792383, antiquewhite: 16444375, aqua: 65535, @@ -981,86 +981,86 @@ var hn = 0.7, Xn = 1 / hn, Ft = "\\s*([+-]?\\d+)\\s*", gn = "\\s*([+-]?(?:\\d*\\ yellow: 16776960, yellowgreen: 10145074 }; -fi(En, pn, { +gi(Sn, yn, { copy(t) { return Object.assign(new this.constructor(), this, t); }, displayable() { return this.rgb().displayable(); }, - hex: Fi, + hex: zi, // Deprecated! Use color.formatHex. - formatHex: Fi, - formatHex8: Dc, - formatHsl: Hc, - formatRgb: Oi, - toString: Oi + formatHex: zi, + formatHex8: Oc, + formatHsl: zc, + formatRgb: Vi, + toString: Vi }); -function Fi() { +function zi() { return this.rgb().formatHex(); } -function Dc() { +function Oc() { return this.rgb().formatHex8(); } -function Hc() { - return br(this).formatHsl(); +function zc() { + return kr(this).formatHsl(); } -function Oi() { +function Vi() { return this.rgb().formatRgb(); } -function pn(t) { +function yn(t) { var e, n; - return t = (t + "").trim().toLowerCase(), (e = Pc.exec(t)) ? (n = e[1].length, e = parseInt(e[1], 16), n === 6 ? zi(e) : n === 3 ? new Ne(e >> 8 & 15 | e >> 4 & 240, e >> 4 & 15 | e & 240, (e & 15) << 4 | e & 15, 1) : n === 8 ? Mn(e >> 24 & 255, e >> 16 & 255, e >> 8 & 255, (e & 255) / 255) : n === 4 ? Mn(e >> 12 & 15 | e >> 8 & 240, e >> 8 & 15 | e >> 4 & 240, e >> 4 & 15 | e & 240, ((e & 15) << 4 | e & 15) / 255) : null) : (e = Mc.exec(t)) ? new Ne(e[1], e[2], e[3], 1) : (e = Tc.exec(t)) ? new Ne(e[1] * 255 / 100, e[2] * 255 / 100, e[3] * 255 / 100, 1) : (e = Ac.exec(t)) ? Mn(e[1], e[2], e[3], e[4]) : (e = Nc.exec(t)) ? Mn(e[1] * 255 / 100, e[2] * 255 / 100, e[3] * 255 / 100, e[4]) : (e = $c.exec(t)) ? qi(e[1], e[2] / 100, e[3] / 100, 1) : (e = Ic.exec(t)) ? qi(e[1], e[2] / 100, e[3] / 100, e[4]) : Ri.hasOwnProperty(t) ? zi(Ri[t]) : t === "transparent" ? new Ne(NaN, NaN, NaN, 0) : null; + return t = (t + "").trim().toLowerCase(), (e = Ac.exec(t)) ? (n = e[1].length, e = parseInt(e[1], 16), n === 6 ? Bi(e) : n === 3 ? new Ae(e >> 8 & 15 | e >> 4 & 240, e >> 4 & 15 | e & 240, (e & 15) << 4 | e & 15, 1) : n === 8 ? Tn(e >> 24 & 255, e >> 16 & 255, e >> 8 & 255, (e & 255) / 255) : n === 4 ? Tn(e >> 12 & 15 | e >> 8 & 240, e >> 8 & 15 | e >> 4 & 240, e >> 4 & 15 | e & 240, ((e & 15) << 4 | e & 15) / 255) : null) : (e = $c.exec(t)) ? new Ae(e[1], e[2], e[3], 1) : (e = Ic.exec(t)) ? new Ae(e[1] * 255 / 100, e[2] * 255 / 100, e[3] * 255 / 100, 1) : (e = Dc.exec(t)) ? Tn(e[1], e[2], e[3], e[4]) : (e = Rc.exec(t)) ? Tn(e[1] * 255 / 100, e[2] * 255 / 100, e[3] * 255 / 100, e[4]) : (e = Hc.exec(t)) ? Yi(e[1], e[2] / 100, e[3] / 100, 1) : (e = Fc.exec(t)) ? Yi(e[1], e[2] / 100, e[3] / 100, e[4]) : Oi.hasOwnProperty(t) ? Bi(Oi[t]) : t === "transparent" ? new Ae(NaN, NaN, NaN, 0) : null; } -function zi(t) { - return new Ne(t >> 16 & 255, t >> 8 & 255, t & 255, 1); +function Bi(t) { + return new Ae(t >> 16 & 255, t >> 8 & 255, t & 255, 1); } -function Mn(t, e, n, o) { - return o <= 0 && (t = e = n = NaN), new Ne(t, e, n, o); +function Tn(t, e, n, o) { + return o <= 0 && (t = e = n = NaN), new Ae(t, e, n, o); } -function Rc(t) { - return t instanceof En || (t = pn(t)), t ? (t = t.rgb(), new Ne(t.r, t.g, t.b, t.opacity)) : new Ne(); +function Vc(t) { + return t instanceof Sn || (t = yn(t)), t ? (t = t.rgb(), new Ae(t.r, t.g, t.b, t.opacity)) : new Ae(); } -function qo(t, e, n, o) { - return arguments.length === 1 ? Rc(t) : new Ne(t, e, n, o ?? 1); +function Yo(t, e, n, o) { + return arguments.length === 1 ? Vc(t) : new Ae(t, e, n, o ?? 1); } -function Ne(t, e, n, o) { +function Ae(t, e, n, o) { this.r = +t, this.g = +e, this.b = +n, this.opacity = +o; } -fi(Ne, qo, _r(En, { +gi(Ae, Yo, Sr(Sn, { brighter(t) { - return t = t == null ? Xn : Math.pow(Xn, t), new Ne(this.r * t, this.g * t, this.b * t, this.opacity); + return t = t == null ? Wn : Math.pow(Wn, t), new Ae(this.r * t, this.g * t, this.b * t, this.opacity); }, darker(t) { - return t = t == null ? hn : Math.pow(hn, t), new Ne(this.r * t, this.g * t, this.b * t, this.opacity); + return t = t == null ? pn : Math.pow(pn, t), new Ae(this.r * t, this.g * t, this.b * t, this.opacity); }, rgb() { return this; }, clamp() { - return new Ne(St(this.r), St(this.g), St(this.b), Wn(this.opacity)); + return new Ae(St(this.r), St(this.g), St(this.b), jn(this.opacity)); }, displayable() { return -0.5 <= this.r && this.r < 255.5 && -0.5 <= this.g && this.g < 255.5 && -0.5 <= this.b && this.b < 255.5 && 0 <= this.opacity && this.opacity <= 1; }, - hex: Vi, + hex: qi, // Deprecated! Use color.formatHex. - formatHex: Vi, - formatHex8: Fc, - formatRgb: Bi, - toString: Bi + formatHex: qi, + formatHex8: Bc, + formatRgb: Xi, + toString: Xi })); -function Vi() { +function qi() { return `#${Ct(this.r)}${Ct(this.g)}${Ct(this.b)}`; } -function Fc() { +function Bc() { return `#${Ct(this.r)}${Ct(this.g)}${Ct(this.b)}${Ct((isNaN(this.opacity) ? 1 : this.opacity) * 255)}`; } -function Bi() { - const t = Wn(this.opacity); +function Xi() { + const t = jn(this.opacity); return `${t === 1 ? "rgb(" : "rgba("}${St(this.r)}, ${St(this.g)}, ${St(this.b)}${t === 1 ? ")" : `, ${t})`}`; } -function Wn(t) { +function jn(t) { return isNaN(t) ? 1 : Math.max(0, Math.min(1, t)); } function St(t) { @@ -1069,83 +1069,83 @@ function St(t) { function Ct(t) { return t = St(t), (t < 16 ? "0" : "") + t.toString(16); } -function qi(t, e, n, o) { - return o <= 0 ? t = e = n = NaN : n <= 0 || n >= 1 ? t = e = NaN : e <= 0 && (t = NaN), new Xe(t, e, n, o); +function Yi(t, e, n, o) { + return o <= 0 ? t = e = n = NaN : n <= 0 || n >= 1 ? t = e = NaN : e <= 0 && (t = NaN), new Ye(t, e, n, o); } -function br(t) { - if (t instanceof Xe) return new Xe(t.h, t.s, t.l, t.opacity); - if (t instanceof En || (t = pn(t)), !t) return new Xe(); - if (t instanceof Xe) return t; +function kr(t) { + if (t instanceof Ye) return new Ye(t.h, t.s, t.l, t.opacity); + if (t instanceof Sn || (t = yn(t)), !t) return new Ye(); + if (t instanceof Ye) return t; t = t.rgb(); var e = t.r / 255, n = t.g / 255, o = t.b / 255, i = Math.min(e, n, o), r = Math.max(e, n, o), s = NaN, a = r - i, l = (r + i) / 2; - return a ? (e === r ? s = (n - o) / a + (n < o) * 6 : n === r ? s = (o - e) / a + 2 : s = (e - n) / a + 4, a /= l < 0.5 ? r + i : 2 - r - i, s *= 60) : a = l > 0 && l < 1 ? 0 : s, new Xe(s, a, l, t.opacity); + return a ? (e === r ? s = (n - o) / a + (n < o) * 6 : n === r ? s = (o - e) / a + 2 : s = (e - n) / a + 4, a /= l < 0.5 ? r + i : 2 - r - i, s *= 60) : a = l > 0 && l < 1 ? 0 : s, new Ye(s, a, l, t.opacity); } -function Oc(t, e, n, o) { - return arguments.length === 1 ? br(t) : new Xe(t, e, n, o ?? 1); +function qc(t, e, n, o) { + return arguments.length === 1 ? kr(t) : new Ye(t, e, n, o ?? 1); } -function Xe(t, e, n, o) { +function Ye(t, e, n, o) { this.h = +t, this.s = +e, this.l = +n, this.opacity = +o; } -fi(Xe, Oc, _r(En, { +gi(Ye, qc, Sr(Sn, { brighter(t) { - return t = t == null ? Xn : Math.pow(Xn, t), new Xe(this.h, this.s, this.l * t, this.opacity); + return t = t == null ? Wn : Math.pow(Wn, t), new Ye(this.h, this.s, this.l * t, this.opacity); }, darker(t) { - return t = t == null ? hn : Math.pow(hn, t), new Xe(this.h, this.s, this.l * t, this.opacity); + return t = t == null ? pn : Math.pow(pn, t), new Ye(this.h, this.s, this.l * t, this.opacity); }, rgb() { var t = this.h % 360 + (this.h < 0) * 360, e = isNaN(t) || isNaN(this.s) ? 0 : this.s, n = this.l, o = n + (n < 0.5 ? n : 1 - n) * e, i = 2 * n - o; - return new Ne( - Co(t >= 240 ? t - 240 : t + 120, i, o), - Co(t, i, o), - Co(t < 120 ? t + 240 : t - 120, i, o), + return new Ae( + ko(t >= 240 ? t - 240 : t + 120, i, o), + ko(t, i, o), + ko(t < 120 ? t + 240 : t - 120, i, o), this.opacity ); }, clamp() { - return new Xe(Yi(this.h), Tn(this.s), Tn(this.l), Wn(this.opacity)); + return new Ye(Wi(this.h), Nn(this.s), Nn(this.l), jn(this.opacity)); }, displayable() { return (0 <= this.s && this.s <= 1 || isNaN(this.s)) && 0 <= this.l && this.l <= 1 && 0 <= this.opacity && this.opacity <= 1; }, formatHsl() { - const t = Wn(this.opacity); - return `${t === 1 ? "hsl(" : "hsla("}${Yi(this.h)}, ${Tn(this.s) * 100}%, ${Tn(this.l) * 100}%${t === 1 ? ")" : `, ${t})`}`; + const t = jn(this.opacity); + return `${t === 1 ? "hsl(" : "hsla("}${Wi(this.h)}, ${Nn(this.s) * 100}%, ${Nn(this.l) * 100}%${t === 1 ? ")" : `, ${t})`}`; } })); -function Yi(t) { +function Wi(t) { return t = (t || 0) % 360, t < 0 ? t + 360 : t; } -function Tn(t) { +function Nn(t) { return Math.max(0, Math.min(1, t || 0)); } -function Co(t, e, n) { +function ko(t, e, n) { return (t < 60 ? e + (n - e) * t / 60 : t < 180 ? n : t < 240 ? e + (n - e) * (240 - t) / 60 : e) * 255; } -const xr = (t) => () => t; -function zc(t, e) { +const Lr = (t) => () => t; +function Xc(t, e) { return function(n) { return t + n * e; }; } -function Vc(t, e, n) { +function Yc(t, e, n) { return t = Math.pow(t, n), e = Math.pow(e, n) - t, n = 1 / n, function(o) { return Math.pow(t + o * e, n); }; } -function Bc(t) { - return (t = +t) == 1 ? Er : function(e, n) { - return n - e ? Vc(e, n, t) : xr(isNaN(e) ? n : e); +function Wc(t) { + return (t = +t) == 1 ? Pr : function(e, n) { + return n - e ? Yc(e, n, t) : Lr(isNaN(e) ? n : e); }; } -function Er(t, e) { +function Pr(t, e) { var n = e - t; - return n ? zc(t, n) : xr(isNaN(t) ? e : t); + return n ? Xc(t, n) : Lr(isNaN(t) ? e : t); } -const Yo = (function t(e) { - var n = Bc(e); +const Wo = (function t(e) { + var n = Wc(e); function o(i, r) { - var s = n((i = qo(i)).r, (r = qo(r)).r), a = n(i.g, r.g), l = n(i.b, r.b), c = Er(i.opacity, r.opacity); + var s = n((i = Yo(i)).r, (r = Yo(r)).r), a = n(i.g, r.g), l = n(i.b, r.b), c = Pr(i.opacity, r.opacity); return function(d) { return i.r = s(d), i.g = a(d), i.b = l(d), i.opacity = c(d), i + ""; }; @@ -1157,27 +1157,27 @@ function dt(t, e) { return t * (1 - n) + e * n; }; } -var Xo = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, So = new RegExp(Xo.source, "g"); -function qc(t) { +var jo = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, Lo = new RegExp(jo.source, "g"); +function jc(t) { return function() { return t; }; } -function Yc(t) { +function Uc(t) { return function(e) { return t(e) + ""; }; } -function Xc(t, e) { - var n = Xo.lastIndex = So.lastIndex = 0, o, i, r, s = -1, a = [], l = []; - for (t = t + "", e = e + ""; (o = Xo.exec(t)) && (i = So.exec(e)); ) - (r = i.index) > n && (r = e.slice(n, r), a[s] ? a[s] += r : a[++s] = r), (o = o[0]) === (i = i[0]) ? a[s] ? a[s] += i : a[++s] = i : (a[++s] = null, l.push({ i: s, x: dt(o, i) })), n = So.lastIndex; - return n < e.length && (r = e.slice(n), a[s] ? a[s] += r : a[++s] = r), a.length < 2 ? l[0] ? Yc(l[0].x) : qc(e) : (e = l.length, function(c) { +function Gc(t, e) { + var n = jo.lastIndex = Lo.lastIndex = 0, o, i, r, s = -1, a = [], l = []; + for (t = t + "", e = e + ""; (o = jo.exec(t)) && (i = Lo.exec(e)); ) + (r = i.index) > n && (r = e.slice(n, r), a[s] ? a[s] += r : a[++s] = r), (o = o[0]) === (i = i[0]) ? a[s] ? a[s] += i : a[++s] = i : (a[++s] = null, l.push({ i: s, x: dt(o, i) })), n = Lo.lastIndex; + return n < e.length && (r = e.slice(n), a[s] ? a[s] += r : a[++s] = r), a.length < 2 ? l[0] ? Uc(l[0].x) : jc(e) : (e = l.length, function(c) { for (var d = 0, u; d < e; ++d) a[(u = l[d]).i] = u.x(c); return a.join(""); }); } -var Xi = 180 / Math.PI, Wo = { +var ji = 180 / Math.PI, Uo = { translateX: 0, translateY: 0, rotate: 0, @@ -1185,26 +1185,26 @@ var Xi = 180 / Math.PI, Wo = { scaleX: 1, scaleY: 1 }; -function Cr(t, e, n, o, i, r) { +function Mr(t, e, n, o, i, r) { var s, a, l; return (s = Math.sqrt(t * t + e * e)) && (t /= s, e /= s), (l = t * n + e * o) && (n -= t * l, o -= e * l), (a = Math.sqrt(n * n + o * o)) && (n /= a, o /= a, l /= a), t * o < e * n && (t = -t, e = -e, l = -l, s = -s), { translateX: i, translateY: r, - rotate: Math.atan2(e, t) * Xi, - skewX: Math.atan(l) * Xi, + rotate: Math.atan2(e, t) * ji, + skewX: Math.atan(l) * ji, scaleX: s, scaleY: a }; } var An; -function Wc(t) { +function Zc(t) { const e = new (typeof DOMMatrix == "function" ? DOMMatrix : WebKitCSSMatrix)(t + ""); - return e.isIdentity ? Wo : Cr(e.a, e.b, e.c, e.d, e.e, e.f); + return e.isIdentity ? Uo : Mr(e.a, e.b, e.c, e.d, e.e, e.f); } -function jc(t) { - return t == null || (An || (An = document.createElementNS("http://www.w3.org/2000/svg", "g")), An.setAttribute("transform", t), !(t = An.transform.baseVal.consolidate())) ? Wo : (t = t.matrix, Cr(t.a, t.b, t.c, t.d, t.e, t.f)); +function Kc(t) { + return t == null || (An || (An = document.createElementNS("http://www.w3.org/2000/svg", "g")), An.setAttribute("transform", t), !(t = An.transform.baseVal.consolidate())) ? Uo : (t = t.matrix, Mr(t.a, t.b, t.c, t.d, t.e, t.f)); } -function Sr(t, e, n, o) { +function Tr(t, e, n, o) { function i(c) { return c.length ? c.pop() + " " : ""; } @@ -1234,35 +1234,35 @@ function Sr(t, e, n, o) { }; }; } -var Uc = Sr(Wc, "px, ", "px)", "deg)"), Gc = Sr(jc, ", ", ")", ")"), Zc = 1e-12; -function Wi(t) { +var Jc = Tr(Zc, "px, ", "px)", "deg)"), Qc = Tr(Kc, ", ", ")", ")"), ed = 1e-12; +function Ui(t) { return ((t = Math.exp(t)) + 1 / t) / 2; } -function Kc(t) { +function td(t) { return ((t = Math.exp(t)) - 1 / t) / 2; } -function Jc(t) { +function nd(t) { return ((t = Math.exp(2 * t)) - 1) / (t + 1); } -const Qc = (function t(e, n, o) { +const od = (function t(e, n, o) { function i(r, s) { var a = r[0], l = r[1], c = r[2], d = s[0], u = s[1], f = s[2], h = d - a, p = u - l, g = h * h + p * p, m, y; - if (g < Zc) - y = Math.log(f / c) / e, m = function(E) { + if (g < ed) + y = Math.log(f / c) / e, m = function(b) { return [ - a + E * h, - l + E * p, - c * Math.exp(e * E * y) + a + b * h, + l + b * p, + c * Math.exp(e * b * y) ]; }; else { - var _ = Math.sqrt(g), S = (f * f - c * c + o * g) / (2 * c * n * _), x = (f * f - c * c - o * g) / (2 * f * n * _), b = Math.log(Math.sqrt(S * S + 1) - S), C = Math.log(Math.sqrt(x * x + 1) - x); - y = (C - b) / e, m = function(E) { - var T = E * y, I = Wi(b), M = c / (n * _) * (I * Jc(e * T + b) - Kc(b)); + var _ = Math.sqrt(g), k = (f * f - c * c + o * g) / (2 * c * n * _), x = (f * f - c * c - o * g) / (2 * f * n * _), C = Math.log(Math.sqrt(k * k + 1) - k), E = Math.log(Math.sqrt(x * x + 1) - x); + y = (E - C) / e, m = function(b) { + var T = b * y, D = Ui(C), L = c / (n * _) * (D * nd(e * T + C) - td(C)); return [ - a + M * h, - l + M * p, - c * I / Wi(e * T + b) + a + L * h, + l + L * p, + c * D / Ui(e * T + C) ]; }; } @@ -1273,97 +1273,97 @@ const Qc = (function t(e, n, o) { return t(s, a, l); }, i; })(Math.SQRT2, 2, 4); -var Bt = 0, rn = 0, Qt = 0, kr = 1e3, jn, an, Un = 0, Lt = 0, mo = 0, mn = typeof performance == "object" && performance.now ? performance : Date, Lr = typeof window == "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(t) { +var Xt = 0, ln = 0, tn = 0, Nr = 1e3, Un, cn, Gn = 0, Lt = 0, yo = 0, wn = typeof performance == "object" && performance.now ? performance : Date, Ar = typeof window == "object" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(t) { setTimeout(t, 17); }; -function hi() { - return Lt || (Lr(ed), Lt = mn.now() + mo); +function pi() { + return Lt || (Ar(id), Lt = wn.now() + yo); } -function ed() { +function id() { Lt = 0; } -function Gn() { +function Zn() { this._call = this._time = this._next = null; } -Gn.prototype = Pr.prototype = { - constructor: Gn, +Zn.prototype = $r.prototype = { + constructor: Zn, restart: function(t, e, n) { if (typeof t != "function") throw new TypeError("callback is not a function"); - n = (n == null ? hi() : +n) + (e == null ? 0 : +e), !this._next && an !== this && (an ? an._next = this : jn = this, an = this), this._call = t, this._time = n, jo(); + n = (n == null ? pi() : +n) + (e == null ? 0 : +e), !this._next && cn !== this && (cn ? cn._next = this : Un = this, cn = this), this._call = t, this._time = n, Go(); }, stop: function() { - this._call && (this._call = null, this._time = 1 / 0, jo()); + this._call && (this._call = null, this._time = 1 / 0, Go()); } }; -function Pr(t, e, n) { - var o = new Gn(); +function $r(t, e, n) { + var o = new Zn(); return o.restart(t, e, n), o; } -function td() { - hi(), ++Bt; - for (var t = jn, e; t; ) +function sd() { + pi(), ++Xt; + for (var t = Un, e; t; ) (e = Lt - t._time) >= 0 && t._call.call(void 0, e), t = t._next; - --Bt; + --Xt; } -function ji() { - Lt = (Un = mn.now()) + mo, Bt = rn = 0; +function Gi() { + Lt = (Gn = wn.now()) + yo, Xt = ln = 0; try { - td(); + sd(); } finally { - Bt = 0, od(), Lt = 0; + Xt = 0, ad(), Lt = 0; } } -function nd() { - var t = mn.now(), e = t - Un; - e > kr && (mo -= e, Un = t); +function rd() { + var t = wn.now(), e = t - Gn; + e > Nr && (yo -= e, Gn = t); } -function od() { - for (var t, e = jn, n, o = 1 / 0; e; ) - e._call ? (o > e._time && (o = e._time), t = e, e = e._next) : (n = e._next, e._next = null, e = t ? t._next = n : jn = n); - an = t, jo(o); +function ad() { + for (var t, e = Un, n, o = 1 / 0; e; ) + e._call ? (o > e._time && (o = e._time), t = e, e = e._next) : (n = e._next, e._next = null, e = t ? t._next = n : Un = n); + cn = t, Go(o); } -function jo(t) { - if (!Bt) { - rn && (rn = clearTimeout(rn)); +function Go(t) { + if (!Xt) { + ln && (ln = clearTimeout(ln)); var e = t - Lt; - e > 24 ? (t < 1 / 0 && (rn = setTimeout(ji, t - mn.now() - mo)), Qt && (Qt = clearInterval(Qt))) : (Qt || (Un = mn.now(), Qt = setInterval(nd, kr)), Bt = 1, Lr(ji)); + e > 24 ? (t < 1 / 0 && (ln = setTimeout(Gi, t - wn.now() - yo)), tn && (tn = clearInterval(tn))) : (tn || (Gn = wn.now(), tn = setInterval(rd, Nr)), Xt = 1, Ar(Gi)); } } -function Ui(t, e, n) { - var o = new Gn(); +function Zi(t, e, n) { + var o = new Zn(); return e = e == null ? 0 : +e, o.restart((i) => { o.stop(), t(i + e); }, e, n), o; } -var id = go("start", "end", "cancel", "interrupt"), sd = [], Mr = 0, Gi = 1, Uo = 2, zn = 3, Zi = 4, Go = 5, Vn = 6; -function yo(t, e, n, o, i, r) { +var ld = po("start", "end", "cancel", "interrupt"), cd = [], Ir = 0, Ki = 1, Zo = 2, Vn = 3, Ji = 4, Ko = 5, Bn = 6; +function wo(t, e, n, o, i, r) { var s = t.__transition; if (!s) t.__transition = {}; else if (n in s) return; - rd(t, n, { + dd(t, n, { name: e, index: o, // For context during callback. group: i, // For context during callback. - on: id, - tween: sd, + on: ld, + tween: cd, time: r.time, delay: r.delay, duration: r.duration, ease: r.ease, timer: null, - state: Mr + state: Ir }); } -function gi(t, e) { +function mi(t, e) { var n = je(t, e); - if (n.state > Mr) throw new Error("too late; already scheduled"); + if (n.state > Ir) throw new Error("too late; already scheduled"); return n; } function Ke(t, e) { var n = je(t, e); - if (n.state > zn) throw new Error("too late; already running"); + if (n.state > Vn) throw new Error("too late; already running"); return n; } function je(t, e) { @@ -1371,40 +1371,40 @@ function je(t, e) { if (!n || !(n = n[e])) throw new Error("transition not found"); return n; } -function rd(t, e, n) { +function dd(t, e, n) { var o = t.__transition, i; - o[e] = n, n.timer = Pr(r, 0, n.time); + o[e] = n, n.timer = $r(r, 0, n.time); function r(c) { - n.state = Gi, n.timer.restart(s, n.delay, n.time), n.delay <= c && s(c - n.delay); + n.state = Ki, n.timer.restart(s, n.delay, n.time), n.delay <= c && s(c - n.delay); } function s(c) { var d, u, f, h; - if (n.state !== Gi) return l(); + if (n.state !== Ki) return l(); for (d in o) if (h = o[d], h.name === n.name) { - if (h.state === zn) return Ui(s); - h.state === Zi ? (h.state = Vn, h.timer.stop(), h.on.call("interrupt", t, t.__data__, h.index, h.group), delete o[d]) : +d < e && (h.state = Vn, h.timer.stop(), h.on.call("cancel", t, t.__data__, h.index, h.group), delete o[d]); + if (h.state === Vn) return Zi(s); + h.state === Ji ? (h.state = Bn, h.timer.stop(), h.on.call("interrupt", t, t.__data__, h.index, h.group), delete o[d]) : +d < e && (h.state = Bn, h.timer.stop(), h.on.call("cancel", t, t.__data__, h.index, h.group), delete o[d]); } - if (Ui(function() { - n.state === zn && (n.state = Zi, n.timer.restart(a, n.delay, n.time), a(c)); - }), n.state = Uo, n.on.call("start", t, t.__data__, n.index, n.group), n.state === Uo) { - for (n.state = zn, i = new Array(f = n.tween.length), d = 0, u = -1; d < f; ++d) + if (Zi(function() { + n.state === Vn && (n.state = Ji, n.timer.restart(a, n.delay, n.time), a(c)); + }), n.state = Zo, n.on.call("start", t, t.__data__, n.index, n.group), n.state === Zo) { + for (n.state = Vn, i = new Array(f = n.tween.length), d = 0, u = -1; d < f; ++d) (h = n.tween[d].value.call(t, t.__data__, n.index, n.group)) && (i[++u] = h); i.length = u + 1; } } function a(c) { - for (var d = c < n.duration ? n.ease.call(null, c / n.duration) : (n.timer.restart(l), n.state = Go, 1), u = -1, f = i.length; ++u < f; ) + for (var d = c < n.duration ? n.ease.call(null, c / n.duration) : (n.timer.restart(l), n.state = Ko, 1), u = -1, f = i.length; ++u < f; ) i[u].call(t, d); - n.state === Go && (n.on.call("end", t, t.__data__, n.index, n.group), l()); + n.state === Ko && (n.on.call("end", t, t.__data__, n.index, n.group), l()); } function l() { - n.state = Vn, n.timer.stop(), delete o[e]; + n.state = Bn, n.timer.stop(), delete o[e]; for (var c in o) return; delete t.__transition; } } -function Bn(t, e) { +function qn(t, e) { var n = t.__transition, o, i, r = !0, s; if (n) { e = e == null ? null : e + ""; @@ -1413,17 +1413,17 @@ function Bn(t, e) { r = !1; continue; } - i = o.state > Uo && o.state < Go, o.state = Vn, o.timer.stop(), o.on.call(i ? "interrupt" : "cancel", t, t.__data__, o.index, o.group), delete n[s]; + i = o.state > Zo && o.state < Ko, o.state = Bn, o.timer.stop(), o.on.call(i ? "interrupt" : "cancel", t, t.__data__, o.index, o.group), delete n[s]; } r && delete t.__transition; } } -function ad(t) { +function ud(t) { return this.each(function() { - Bn(this, t); + qn(this, t); }); } -function ld(t, e) { +function fd(t, e) { var n, o; return function() { var i = Ke(this, t), r = i.tween; @@ -1438,7 +1438,7 @@ function ld(t, e) { i.tween = o; }; } -function cd(t, e, n) { +function hd(t, e, n) { var o, i; if (typeof n != "function") throw new Error(); return function() { @@ -1455,7 +1455,7 @@ function cd(t, e, n) { r.tween = i; }; } -function dd(t, e) { +function gd(t, e) { var n = this._id; if (t += "", arguments.length < 2) { for (var o = je(this.node(), n).tween, i = 0, r = o.length, s; i < r; ++i) @@ -1463,9 +1463,9 @@ function dd(t, e) { return s.value; return null; } - return this.each((e == null ? ld : cd)(n, t, e)); + return this.each((e == null ? fd : hd)(n, t, e)); } -function pi(t, e, n) { +function yi(t, e, n) { var o = t._id; return t.each(function() { var i = Ke(this, o); @@ -1474,143 +1474,143 @@ function pi(t, e, n) { return je(i, o).value[e]; }; } -function Tr(t, e) { +function Dr(t, e) { var n; - return (typeof e == "number" ? dt : e instanceof pn ? Yo : (n = pn(e)) ? (e = n, Yo) : Xc)(t, e); + return (typeof e == "number" ? dt : e instanceof yn ? Wo : (n = yn(e)) ? (e = n, Wo) : Gc)(t, e); } -function ud(t) { +function pd(t) { return function() { this.removeAttribute(t); }; } -function fd(t) { +function md(t) { return function() { this.removeAttributeNS(t.space, t.local); }; } -function hd(t, e, n) { +function yd(t, e, n) { var o, i = n + "", r; return function() { var s = this.getAttribute(t); return s === i ? null : s === o ? r : r = e(o = s, n); }; } -function gd(t, e, n) { +function wd(t, e, n) { var o, i = n + "", r; return function() { var s = this.getAttributeNS(t.space, t.local); return s === i ? null : s === o ? r : r = e(o = s, n); }; } -function pd(t, e, n) { +function vd(t, e, n) { var o, i, r; return function() { var s, a = n(this), l; return a == null ? void this.removeAttribute(t) : (s = this.getAttribute(t), l = a + "", s === l ? null : s === o && l === i ? r : (i = l, r = e(o = s, a))); }; } -function md(t, e, n) { +function _d(t, e, n) { var o, i, r; return function() { var s, a = n(this), l; return a == null ? void this.removeAttributeNS(t.space, t.local) : (s = this.getAttributeNS(t.space, t.local), l = a + "", s === l ? null : s === o && l === i ? r : (i = l, r = e(o = s, a))); }; } -function yd(t, e) { - var n = po(t), o = n === "transform" ? Gc : Tr; - return this.attrTween(t, typeof e == "function" ? (n.local ? md : pd)(n, o, pi(this, "attr." + t, e)) : e == null ? (n.local ? fd : ud)(n) : (n.local ? gd : hd)(n, o, e)); +function bd(t, e) { + var n = mo(t), o = n === "transform" ? Qc : Dr; + return this.attrTween(t, typeof e == "function" ? (n.local ? _d : vd)(n, o, yi(this, "attr." + t, e)) : e == null ? (n.local ? md : pd)(n) : (n.local ? wd : yd)(n, o, e)); } -function wd(t, e) { +function xd(t, e) { return function(n) { this.setAttribute(t, e.call(this, n)); }; } -function vd(t, e) { +function Ed(t, e) { return function(n) { this.setAttributeNS(t.space, t.local, e.call(this, n)); }; } -function _d(t, e) { +function Cd(t, e) { var n, o; function i() { var r = e.apply(this, arguments); - return r !== o && (n = (o = r) && vd(t, r)), n; + return r !== o && (n = (o = r) && Ed(t, r)), n; } return i._value = e, i; } -function bd(t, e) { +function Sd(t, e) { var n, o; function i() { var r = e.apply(this, arguments); - return r !== o && (n = (o = r) && wd(t, r)), n; + return r !== o && (n = (o = r) && xd(t, r)), n; } return i._value = e, i; } -function xd(t, e) { +function kd(t, e) { var n = "attr." + t; if (arguments.length < 2) return (n = this.tween(n)) && n._value; if (e == null) return this.tween(n, null); if (typeof e != "function") throw new Error(); - var o = po(t); - return this.tween(n, (o.local ? _d : bd)(o, e)); + var o = mo(t); + return this.tween(n, (o.local ? Cd : Sd)(o, e)); } -function Ed(t, e) { +function Ld(t, e) { return function() { - gi(this, t).delay = +e.apply(this, arguments); + mi(this, t).delay = +e.apply(this, arguments); }; } -function Cd(t, e) { +function Pd(t, e) { return e = +e, function() { - gi(this, t).delay = e; + mi(this, t).delay = e; }; } -function Sd(t) { +function Md(t) { var e = this._id; - return arguments.length ? this.each((typeof t == "function" ? Ed : Cd)(e, t)) : je(this.node(), e).delay; + return arguments.length ? this.each((typeof t == "function" ? Ld : Pd)(e, t)) : je(this.node(), e).delay; } -function kd(t, e) { +function Td(t, e) { return function() { Ke(this, t).duration = +e.apply(this, arguments); }; } -function Ld(t, e) { +function Nd(t, e) { return e = +e, function() { Ke(this, t).duration = e; }; } -function Pd(t) { +function Ad(t) { var e = this._id; - return arguments.length ? this.each((typeof t == "function" ? kd : Ld)(e, t)) : je(this.node(), e).duration; + return arguments.length ? this.each((typeof t == "function" ? Td : Nd)(e, t)) : je(this.node(), e).duration; } -function Md(t, e) { +function $d(t, e) { if (typeof e != "function") throw new Error(); return function() { Ke(this, t).ease = e; }; } -function Td(t) { +function Id(t) { var e = this._id; - return arguments.length ? this.each(Md(e, t)) : je(this.node(), e).ease; + return arguments.length ? this.each($d(e, t)) : je(this.node(), e).ease; } -function Ad(t, e) { +function Dd(t, e) { return function() { var n = e.apply(this, arguments); if (typeof n != "function") throw new Error(); Ke(this, t).ease = n; }; } -function Nd(t) { +function Rd(t) { if (typeof t != "function") throw new Error(); - return this.each(Ad(this._id, t)); + return this.each(Dd(this._id, t)); } -function $d(t) { - typeof t != "function" && (t = lr(t)); +function Hd(t) { + typeof t != "function" && (t = hr(t)); for (var e = this._groups, n = e.length, o = new Array(n), i = 0; i < n; ++i) for (var r = e[i], s = r.length, a = o[i] = [], l, c = 0; c < s; ++c) (l = r[c]) && t.call(l, l.__data__, c, r) && a.push(l); return new rt(o, this._parents, this._name, this._id); } -function Id(t) { +function Fd(t) { if (t._id !== this._id) throw new Error(); for (var e = this._groups, n = t._groups, o = e.length, i = n.length, r = Math.min(o, i), s = new Array(o), a = 0; a < r; ++a) for (var l = e[a], c = n[a], d = l.length, u = s[a] = new Array(d), f, h = 0; h < d; ++h) @@ -1619,154 +1619,154 @@ function Id(t) { s[a] = e[a]; return new rt(s, this._parents, this._name, this._id); } -function Dd(t) { +function Od(t) { return (t + "").trim().split(/^|\s+/).every(function(e) { var n = e.indexOf("."); return n >= 0 && (e = e.slice(0, n)), !e || e === "start"; }); } -function Hd(t, e, n) { - var o, i, r = Dd(e) ? gi : Ke; +function zd(t, e, n) { + var o, i, r = Od(e) ? mi : Ke; return function() { var s = r(this, t), a = s.on; a !== o && (i = (o = a).copy()).on(e, n), s.on = i; }; } -function Rd(t, e) { +function Vd(t, e) { var n = this._id; - return arguments.length < 2 ? je(this.node(), n).on.on(t) : this.each(Hd(n, t, e)); + return arguments.length < 2 ? je(this.node(), n).on.on(t) : this.each(zd(n, t, e)); } -function Fd(t) { +function Bd(t) { return function() { var e = this.parentNode; for (var n in this.__transition) if (+n !== t) return; e && e.removeChild(this); }; } -function Od() { - return this.on("end.remove", Fd(this._id)); +function qd() { + return this.on("end.remove", Bd(this._id)); } -function zd(t) { +function Xd(t) { var e = this._name, n = this._id; - typeof t != "function" && (t = di(t)); + typeof t != "function" && (t = fi(t)); for (var o = this._groups, i = o.length, r = new Array(i), s = 0; s < i; ++s) for (var a = o[s], l = a.length, c = r[s] = new Array(l), d, u, f = 0; f < l; ++f) - (d = a[f]) && (u = t.call(d, d.__data__, f, a)) && ("__data__" in d && (u.__data__ = d.__data__), c[f] = u, yo(c[f], e, n, f, c, je(d, n))); + (d = a[f]) && (u = t.call(d, d.__data__, f, a)) && ("__data__" in d && (u.__data__ = d.__data__), c[f] = u, wo(c[f], e, n, f, c, je(d, n))); return new rt(r, this._parents, e, n); } -function Vd(t) { +function Yd(t) { var e = this._name, n = this._id; - typeof t != "function" && (t = ar(t)); + typeof t != "function" && (t = fr(t)); for (var o = this._groups, i = o.length, r = [], s = [], a = 0; a < i; ++a) for (var l = o[a], c = l.length, d, u = 0; u < c; ++u) if (d = l[u]) { for (var f = t.call(d, d.__data__, u, l), h, p = je(d, n), g = 0, m = f.length; g < m; ++g) - (h = f[g]) && yo(h, e, n, g, f, p); + (h = f[g]) && wo(h, e, n, g, f, p); r.push(f), s.push(d); } return new rt(r, s, e, n); } -var Bd = xn.prototype.constructor; -function qd() { - return new Bd(this._groups, this._parents); +var Wd = Cn.prototype.constructor; +function jd() { + return new Wd(this._groups, this._parents); } -function Yd(t, e) { +function Ud(t, e) { var n, o, i; return function() { - var r = Vt(this, t), s = (this.style.removeProperty(t), Vt(this, t)); + var r = qt(this, t), s = (this.style.removeProperty(t), qt(this, t)); return r === s ? null : r === n && s === o ? i : i = e(n = r, o = s); }; } -function Ar(t) { +function Rr(t) { return function() { this.style.removeProperty(t); }; } -function Xd(t, e, n) { +function Gd(t, e, n) { var o, i = n + "", r; return function() { - var s = Vt(this, t); + var s = qt(this, t); return s === i ? null : s === o ? r : r = e(o = s, n); }; } -function Wd(t, e, n) { +function Zd(t, e, n) { var o, i, r; return function() { - var s = Vt(this, t), a = n(this), l = a + ""; - return a == null && (l = a = (this.style.removeProperty(t), Vt(this, t))), s === l ? null : s === o && l === i ? r : (i = l, r = e(o = s, a)); + var s = qt(this, t), a = n(this), l = a + ""; + return a == null && (l = a = (this.style.removeProperty(t), qt(this, t))), s === l ? null : s === o && l === i ? r : (i = l, r = e(o = s, a)); }; } -function jd(t, e) { +function Kd(t, e) { var n, o, i, r = "style." + e, s = "end." + r, a; return function() { - var l = Ke(this, t), c = l.on, d = l.value[r] == null ? a || (a = Ar(e)) : void 0; + var l = Ke(this, t), c = l.on, d = l.value[r] == null ? a || (a = Rr(e)) : void 0; (c !== n || i !== d) && (o = (n = c).copy()).on(s, i = d), l.on = o; }; } -function Ud(t, e, n) { - var o = (t += "") == "transform" ? Uc : Tr; - return e == null ? this.styleTween(t, Yd(t, o)).on("end.style." + t, Ar(t)) : typeof e == "function" ? this.styleTween(t, Wd(t, o, pi(this, "style." + t, e))).each(jd(this._id, t)) : this.styleTween(t, Xd(t, o, e), n).on("end.style." + t, null); +function Jd(t, e, n) { + var o = (t += "") == "transform" ? Jc : Dr; + return e == null ? this.styleTween(t, Ud(t, o)).on("end.style." + t, Rr(t)) : typeof e == "function" ? this.styleTween(t, Zd(t, o, yi(this, "style." + t, e))).each(Kd(this._id, t)) : this.styleTween(t, Gd(t, o, e), n).on("end.style." + t, null); } -function Gd(t, e, n) { +function Qd(t, e, n) { return function(o) { this.style.setProperty(t, e.call(this, o), n); }; } -function Zd(t, e, n) { +function eu(t, e, n) { var o, i; function r() { var s = e.apply(this, arguments); - return s !== i && (o = (i = s) && Gd(t, s, n)), o; + return s !== i && (o = (i = s) && Qd(t, s, n)), o; } return r._value = e, r; } -function Kd(t, e, n) { +function tu(t, e, n) { var o = "style." + (t += ""); if (arguments.length < 2) return (o = this.tween(o)) && o._value; if (e == null) return this.tween(o, null); if (typeof e != "function") throw new Error(); - return this.tween(o, Zd(t, e, n ?? "")); + return this.tween(o, eu(t, e, n ?? "")); } -function Jd(t) { +function nu(t) { return function() { this.textContent = t; }; } -function Qd(t) { +function ou(t) { return function() { var e = t(this); this.textContent = e ?? ""; }; } -function eu(t) { - return this.tween("text", typeof t == "function" ? Qd(pi(this, "text", t)) : Jd(t == null ? "" : t + "")); +function iu(t) { + return this.tween("text", typeof t == "function" ? ou(yi(this, "text", t)) : nu(t == null ? "" : t + "")); } -function tu(t) { +function su(t) { return function(e) { this.textContent = t.call(this, e); }; } -function nu(t) { +function ru(t) { var e, n; function o() { var i = t.apply(this, arguments); - return i !== n && (e = (n = i) && tu(i)), e; + return i !== n && (e = (n = i) && su(i)), e; } return o._value = t, o; } -function ou(t) { +function au(t) { var e = "text"; if (arguments.length < 1) return (e = this.tween(e)) && e._value; if (t == null) return this.tween(e, null); if (typeof t != "function") throw new Error(); - return this.tween(e, nu(t)); + return this.tween(e, ru(t)); } -function iu() { - for (var t = this._name, e = this._id, n = Nr(), o = this._groups, i = o.length, r = 0; r < i; ++r) +function lu() { + for (var t = this._name, e = this._id, n = Hr(), o = this._groups, i = o.length, r = 0; r < i; ++r) for (var s = o[r], a = s.length, l, c = 0; c < a; ++c) if (l = s[c]) { var d = je(l, e); - yo(l, t, n, c, s, { + wo(l, t, n, c, s, { time: d.time + d.delay + d.duration, delay: 0, duration: d.duration, @@ -1775,7 +1775,7 @@ function iu() { } return new rt(o, this._parents, t, n); } -function su() { +function cu() { var t, e, n = this, o = n._id, i = n.size(); return new Promise(function(r, s) { var a = { value: s }, l = { value: function() { @@ -1787,179 +1787,179 @@ function su() { }), i === 0 && r(); }); } -var ru = 0; +var du = 0; function rt(t, e, n, o) { this._groups = t, this._parents = e, this._name = n, this._id = o; } -function Nr() { - return ++ru; +function Hr() { + return ++du; } -var Je = xn.prototype; +var Je = Cn.prototype; rt.prototype = { constructor: rt, - select: zd, - selectAll: Vd, + select: Xd, + selectAll: Yd, selectChild: Je.selectChild, selectChildren: Je.selectChildren, - filter: $d, - merge: Id, - selection: qd, - transition: iu, + filter: Hd, + merge: Fd, + selection: jd, + transition: lu, call: Je.call, nodes: Je.nodes, node: Je.node, size: Je.size, empty: Je.empty, each: Je.each, - on: Rd, - attr: yd, - attrTween: xd, - style: Ud, - styleTween: Kd, - text: eu, - textTween: ou, - remove: Od, - tween: dd, - delay: Sd, - duration: Pd, - ease: Td, - easeVarying: Nd, - end: su, + on: Vd, + attr: bd, + attrTween: kd, + style: Jd, + styleTween: tu, + text: iu, + textTween: au, + remove: qd, + tween: gd, + delay: Md, + duration: Ad, + ease: Id, + easeVarying: Rd, + end: cu, [Symbol.iterator]: Je[Symbol.iterator] }; -const au = (t) => +t; -function lu(t) { +const uu = (t) => +t; +function fu(t) { return t * t; } -function cu(t) { +function hu(t) { return t * (2 - t); } -function du(t) { +function gu(t) { return ((t *= 2) <= 1 ? t * t : --t * (2 - t) + 1) / 2; } -function uu(t) { +function pu(t) { return t * t * t; } -function fu(t) { +function mu(t) { return --t * t * t + 1; } -function $r(t) { +function Fr(t) { return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2; } -var Ir = Math.PI, Dr = Ir / 2; -function hu(t) { - return +t == 1 ? 1 : 1 - Math.cos(t * Dr); +var Or = Math.PI, zr = Or / 2; +function yu(t) { + return +t == 1 ? 1 : 1 - Math.cos(t * zr); } -function gu(t) { - return Math.sin(t * Dr); +function wu(t) { + return Math.sin(t * zr); } -function pu(t) { - return (1 - Math.cos(Ir * t)) / 2; +function vu(t) { + return (1 - Math.cos(Or * t)) / 2; } function _t(t) { return (Math.pow(2, -10 * t) - 9765625e-10) * 1.0009775171065494; } -function mu(t) { +function _u(t) { return _t(1 - +t); } -function yu(t) { +function bu(t) { return 1 - _t(t); } -function wu(t) { +function xu(t) { return ((t *= 2) <= 1 ? _t(1 - t) : 2 - _t(t - 1)) / 2; } -function vu(t) { +function Eu(t) { return 1 - Math.sqrt(1 - t * t); } -function _u(t) { +function Cu(t) { return Math.sqrt(1 - --t * t); } -function bu(t) { +function Su(t) { return ((t *= 2) <= 1 ? 1 - Math.sqrt(1 - t * t) : Math.sqrt(1 - (t -= 2) * t) + 1) / 2; } -var Zo = 4 / 11, xu = 6 / 11, Eu = 8 / 11, Cu = 3 / 4, Su = 9 / 11, ku = 10 / 11, Lu = 15 / 16, Pu = 21 / 22, Mu = 63 / 64, Nn = 1 / Zo / Zo; -function Tu(t) { - return 1 - Zn(1 - t); +var Jo = 4 / 11, ku = 6 / 11, Lu = 8 / 11, Pu = 3 / 4, Mu = 9 / 11, Tu = 10 / 11, Nu = 15 / 16, Au = 21 / 22, $u = 63 / 64, $n = 1 / Jo / Jo; +function Iu(t) { + return 1 - Kn(1 - t); } -function Zn(t) { - return (t = +t) < Zo ? Nn * t * t : t < Eu ? Nn * (t -= xu) * t + Cu : t < ku ? Nn * (t -= Su) * t + Lu : Nn * (t -= Pu) * t + Mu; +function Kn(t) { + return (t = +t) < Jo ? $n * t * t : t < Lu ? $n * (t -= ku) * t + Pu : t < Tu ? $n * (t -= Mu) * t + Nu : $n * (t -= Au) * t + $u; } -function Au(t) { - return ((t *= 2) <= 1 ? 1 - Zn(1 - t) : Zn(t - 1) + 1) / 2; +function Du(t) { + return ((t *= 2) <= 1 ? 1 - Kn(1 - t) : Kn(t - 1) + 1) / 2; } -var mi = 1.70158, Nu = (function t(e) { +var wi = 1.70158, Ru = (function t(e) { e = +e; function n(o) { return (o = +o) * o * (e * (o - 1) + o); } return n.overshoot = t, n; -})(mi), $u = (function t(e) { +})(wi), Hu = (function t(e) { e = +e; function n(o) { return --o * o * ((o + 1) * e + o) + 1; } return n.overshoot = t, n; -})(mi), Iu = (function t(e) { +})(wi), Fu = (function t(e) { e = +e; function n(o) { return ((o *= 2) < 1 ? o * o * ((e + 1) * o - e) : (o -= 2) * o * ((e + 1) * o + e) + 2) / 2; } return n.overshoot = t, n; -})(mi), qt = 2 * Math.PI, yi = 1, wi = 0.3, Du = (function t(e, n) { - var o = Math.asin(1 / (e = Math.max(1, e))) * (n /= qt); +})(wi), Yt = 2 * Math.PI, vi = 1, _i = 0.3, Ou = (function t(e, n) { + var o = Math.asin(1 / (e = Math.max(1, e))) * (n /= Yt); function i(r) { return e * _t(- --r) * Math.sin((o - r) / n); } return i.amplitude = function(r) { - return t(r, n * qt); + return t(r, n * Yt); }, i.period = function(r) { return t(e, r); }, i; -})(yi, wi), Hu = (function t(e, n) { - var o = Math.asin(1 / (e = Math.max(1, e))) * (n /= qt); +})(vi, _i), zu = (function t(e, n) { + var o = Math.asin(1 / (e = Math.max(1, e))) * (n /= Yt); function i(r) { return 1 - e * _t(r = +r) * Math.sin((r + o) / n); } return i.amplitude = function(r) { - return t(r, n * qt); + return t(r, n * Yt); }, i.period = function(r) { return t(e, r); }, i; -})(yi, wi), Ru = (function t(e, n) { - var o = Math.asin(1 / (e = Math.max(1, e))) * (n /= qt); +})(vi, _i), Vu = (function t(e, n) { + var o = Math.asin(1 / (e = Math.max(1, e))) * (n /= Yt); function i(r) { return ((r = r * 2 - 1) < 0 ? e * _t(-r) * Math.sin((o - r) / n) : 2 - e * _t(r) * Math.sin((o + r) / n)) / 2; } return i.amplitude = function(r) { - return t(r, n * qt); + return t(r, n * Yt); }, i.period = function(r) { return t(e, r); }, i; -})(yi, wi), Fu = { +})(vi, _i), Bu = { time: null, // Set on use. delay: 0, duration: 250, - ease: $r + ease: Fr }; -function Ou(t, e) { +function qu(t, e) { for (var n; !(n = t.__transition) || !(n = n[e]); ) if (!(t = t.parentNode)) throw new Error(`transition ${e} not found`); return n; } -function zu(t) { +function Xu(t) { var e, n; - t instanceof rt ? (e = t._id, t = t._name) : (e = Nr(), (n = Fu).time = hi(), t = t == null ? null : t + ""); + t instanceof rt ? (e = t._id, t = t._name) : (e = Hr(), (n = Bu).time = pi(), t = t == null ? null : t + ""); for (var o = this._groups, i = o.length, r = 0; r < i; ++r) for (var s = o[r], a = s.length, l, c = 0; c < a; ++c) - (l = s[c]) && yo(l, t, e, c, s, n || Ou(l, e)); + (l = s[c]) && wo(l, t, e, c, s, n || qu(l, e)); return new rt(o, this._parents, t, e); } -xn.prototype.interrupt = ad; -xn.prototype.transition = zu; -const $n = (t) => () => t; -function Vu(t, { +Cn.prototype.interrupt = ud; +Cn.prototype.transition = Xu; +const In = (t) => () => t; +function Yu(t, { sourceEvent: e, target: n, transform: o, @@ -2012,214 +2012,214 @@ et.prototype = { return "translate(" + this.x + "," + this.y + ") scale(" + this.k + ")"; } }; -var Kn = new et(1, 0, 0); +var Jn = new et(1, 0, 0); et.prototype; -function ko(t) { +function Po(t) { t.stopImmediatePropagation(); } -function en(t) { +function nn(t) { t.preventDefault(), t.stopImmediatePropagation(); } -function Bu(t) { +function Wu(t) { return (!t.ctrlKey || t.type === "wheel") && !t.button; } -function qu() { +function ju() { var t = this; return t instanceof SVGElement ? (t = t.ownerSVGElement || t, t.hasAttribute("viewBox") ? (t = t.viewBox.baseVal, [[t.x, t.y], [t.x + t.width, t.y + t.height]]) : [[0, 0], [t.width.baseVal.value, t.height.baseVal.value]]) : [[0, 0], [t.clientWidth, t.clientHeight]]; } -function Ki() { - return this.__zoom || Kn; +function Qi() { + return this.__zoom || Jn; } -function Yu(t) { +function Uu(t) { return -t.deltaY * (t.deltaMode === 1 ? 0.05 : t.deltaMode ? 1 : 2e-3) * (t.ctrlKey ? 10 : 1); } -function Xu() { +function Gu() { return navigator.maxTouchPoints || "ontouchstart" in this; } -function Wu(t, e, n) { +function Zu(t, e, n) { var o = t.invertX(e[0][0]) - n[0][0], i = t.invertX(e[1][0]) - n[1][0], r = t.invertY(e[0][1]) - n[0][1], s = t.invertY(e[1][1]) - n[1][1]; return t.translate( i > o ? (o + i) / 2 : Math.min(0, o) || Math.max(0, i), s > r ? (r + s) / 2 : Math.min(0, r) || Math.max(0, s) ); } -function ju() { - var t = Bu, e = qu, n = Wu, o = Yu, i = Xu, r = [0, 1 / 0], s = [[-1 / 0, -1 / 0], [1 / 0, 1 / 0]], a = 250, l = Qc, c = go("start", "zoom", "end"), d, u, f, h = 500, p = 150, g = 0, m = 10; +function Ku() { + var t = Wu, e = ju, n = Zu, o = Uu, i = Gu, r = [0, 1 / 0], s = [[-1 / 0, -1 / 0], [1 / 0, 1 / 0]], a = 250, l = od, c = po("start", "zoom", "end"), d, u, f, h = 500, p = 150, g = 0, m = 10; function y(w) { - w.property("__zoom", Ki).on("wheel.zoom", T, { passive: !1 }).on("mousedown.zoom", I).on("dblclick.zoom", M).filter(i).on("touchstart.zoom", A).on("touchmove.zoom", P).on("touchend.zoom touchcancel.zoom", v).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)"); + w.property("__zoom", Qi).on("wheel.zoom", T, { passive: !1 }).on("mousedown.zoom", D).on("dblclick.zoom", L).filter(i).on("touchstart.zoom", A).on("touchmove.zoom", P).on("touchend.zoom touchcancel.zoom", v).style("-webkit-tap-highlight-color", "rgba(0,0,0,0)"); } - y.transform = function(w, N, k, R) { - var O = w.selection ? w.selection() : w; - O.property("__zoom", Ki), w !== O ? b(w, N, k, R) : O.interrupt().each(function() { - C(this, arguments).event(R).start().zoom(null, typeof N == "function" ? N.apply(this, arguments) : N).end(); + y.transform = function(w, I, M, R) { + var z = w.selection ? w.selection() : w; + z.property("__zoom", Qi), w !== z ? C(w, I, M, R) : z.interrupt().each(function() { + E(this, arguments).event(R).start().zoom(null, typeof I == "function" ? I.apply(this, arguments) : I).end(); }); - }, y.scaleBy = function(w, N, k, R) { + }, y.scaleBy = function(w, I, M, R) { y.scaleTo(w, function() { - var O = this.__zoom.k, q = typeof N == "function" ? N.apply(this, arguments) : N; - return O * q; - }, k, R); - }, y.scaleTo = function(w, N, k, R) { + var z = this.__zoom.k, V = typeof I == "function" ? I.apply(this, arguments) : I; + return z * V; + }, M, R); + }, y.scaleTo = function(w, I, M, R) { y.transform(w, function() { - var O = e.apply(this, arguments), q = this.__zoom, D = k == null ? x(O) : typeof k == "function" ? k.apply(this, arguments) : k, L = q.invert(D), $ = typeof N == "function" ? N.apply(this, arguments) : N; - return n(S(_(q, $), D, L), O, s); - }, k, R); - }, y.translateBy = function(w, N, k, R) { + var z = e.apply(this, arguments), V = this.__zoom, $ = M == null ? x(z) : typeof M == "function" ? M.apply(this, arguments) : M, S = V.invert($), N = typeof I == "function" ? I.apply(this, arguments) : I; + return n(k(_(V, N), $, S), z, s); + }, M, R); + }, y.translateBy = function(w, I, M, R) { y.transform(w, function() { return n(this.__zoom.translate( - typeof N == "function" ? N.apply(this, arguments) : N, - typeof k == "function" ? k.apply(this, arguments) : k + typeof I == "function" ? I.apply(this, arguments) : I, + typeof M == "function" ? M.apply(this, arguments) : M ), e.apply(this, arguments), s); }, null, R); - }, y.translateTo = function(w, N, k, R, O) { + }, y.translateTo = function(w, I, M, R, z) { y.transform(w, function() { - var q = e.apply(this, arguments), D = this.__zoom, L = R == null ? x(q) : typeof R == "function" ? R.apply(this, arguments) : R; - return n(Kn.translate(L[0], L[1]).scale(D.k).translate( - typeof N == "function" ? -N.apply(this, arguments) : -N, - typeof k == "function" ? -k.apply(this, arguments) : -k - ), q, s); - }, R, O); + var V = e.apply(this, arguments), $ = this.__zoom, S = R == null ? x(V) : typeof R == "function" ? R.apply(this, arguments) : R; + return n(Jn.translate(S[0], S[1]).scale($.k).translate( + typeof I == "function" ? -I.apply(this, arguments) : -I, + typeof M == "function" ? -M.apply(this, arguments) : -M + ), V, s); + }, R, z); }; - function _(w, N) { - return N = Math.max(r[0], Math.min(r[1], N)), N === w.k ? w : new et(N, w.x, w.y); + function _(w, I) { + return I = Math.max(r[0], Math.min(r[1], I)), I === w.k ? w : new et(I, w.x, w.y); } - function S(w, N, k) { - var R = N[0] - k[0] * w.k, O = N[1] - k[1] * w.k; - return R === w.x && O === w.y ? w : new et(w.k, R, O); + function k(w, I, M) { + var R = I[0] - M[0] * w.k, z = I[1] - M[1] * w.k; + return R === w.x && z === w.y ? w : new et(w.k, R, z); } function x(w) { return [(+w[0][0] + +w[1][0]) / 2, (+w[0][1] + +w[1][1]) / 2]; } - function b(w, N, k, R) { + function C(w, I, M, R) { w.on("start.zoom", function() { - C(this, arguments).event(R).start(); + E(this, arguments).event(R).start(); }).on("interrupt.zoom end.zoom", function() { - C(this, arguments).event(R).end(); + E(this, arguments).event(R).end(); }).tween("zoom", function() { - var O = this, q = arguments, D = C(O, q).event(R), L = e.apply(O, q), $ = k == null ? x(L) : typeof k == "function" ? k.apply(O, q) : k, F = Math.max(L[1][0] - L[0][0], L[1][1] - L[0][1]), K = O.__zoom, ne = typeof N == "function" ? N.apply(O, q) : N, G = l(K.invert($).concat(F / K.k), ne.invert($).concat(F / ne.k)); - return function(B) { - if (B === 1) B = ne; + var z = this, V = arguments, $ = E(z, V).event(R), S = e.apply(z, V), N = M == null ? x(S) : typeof M == "function" ? M.apply(z, V) : M, F = Math.max(S[1][0] - S[0][0], S[1][1] - S[0][1]), W = z.__zoom, Q = typeof I == "function" ? I.apply(z, V) : I, U = l(W.invert(N).concat(F / W.k), Q.invert(N).concat(F / Q.k)); + return function(G) { + if (G === 1) G = Q; else { - var z = G(B), Y = F / z[2]; - B = new et(Y, $[0] - z[0] * Y, $[1] - z[1] * Y); + var Z = U(G), H = F / Z[2]; + G = new et(H, N[0] - Z[0] * H, N[1] - Z[1] * H); } - D.zoom(null, B); + $.zoom(null, G); }; }); } - function C(w, N, k) { - return !k && w.__zooming || new E(w, N); + function E(w, I, M) { + return !M && w.__zooming || new b(w, I); } - function E(w, N) { - this.that = w, this.args = N, this.active = 0, this.sourceEvent = null, this.extent = e.apply(w, N), this.taps = 0; + function b(w, I) { + this.that = w, this.args = I, this.active = 0, this.sourceEvent = null, this.extent = e.apply(w, I), this.taps = 0; } - E.prototype = { + b.prototype = { event: function(w) { return w && (this.sourceEvent = w), this; }, start: function() { return ++this.active === 1 && (this.that.__zooming = this, this.emit("start")), this; }, - zoom: function(w, N) { - return this.mouse && w !== "mouse" && (this.mouse[1] = N.invert(this.mouse[0])), this.touch0 && w !== "touch" && (this.touch0[1] = N.invert(this.touch0[0])), this.touch1 && w !== "touch" && (this.touch1[1] = N.invert(this.touch1[0])), this.that.__zoom = N, this.emit("zoom"), this; + zoom: function(w, I) { + return this.mouse && w !== "mouse" && (this.mouse[1] = I.invert(this.mouse[0])), this.touch0 && w !== "touch" && (this.touch0[1] = I.invert(this.touch0[0])), this.touch1 && w !== "touch" && (this.touch1[1] = I.invert(this.touch1[0])), this.that.__zoom = I, this.emit("zoom"), this; }, end: function() { return --this.active === 0 && (delete this.that.__zooming, this.emit("end")), this; }, emit: function(w) { - var N = Ye(this.that).datum(); + var I = Xe(this.that).datum(); c.call( w, this.that, - new Vu(w, { + new Yu(w, { sourceEvent: this.sourceEvent, target: y, transform: this.that.__zoom, dispatch: c }), - N + I ); } }; - function T(w, ...N) { + function T(w, ...I) { if (!t.apply(this, arguments)) return; - var k = C(this, N).event(w), R = this.__zoom, O = Math.max(r[0], Math.min(r[1], R.k * Math.pow(2, o.apply(this, arguments)))), q = Qe(w); - if (k.wheel) - (k.mouse[0][0] !== q[0] || k.mouse[0][1] !== q[1]) && (k.mouse[1] = R.invert(k.mouse[0] = q)), clearTimeout(k.wheel); + var M = E(this, I).event(w), R = this.__zoom, z = Math.max(r[0], Math.min(r[1], R.k * Math.pow(2, o.apply(this, arguments)))), V = Qe(w); + if (M.wheel) + (M.mouse[0][0] !== V[0] || M.mouse[0][1] !== V[1]) && (M.mouse[1] = R.invert(M.mouse[0] = V)), clearTimeout(M.wheel); else { - if (R.k === O) return; - k.mouse = [q, R.invert(q)], Bn(this), k.start(); + if (R.k === z) return; + M.mouse = [V, R.invert(V)], qn(this), M.start(); } - en(w), k.wheel = setTimeout(D, p), k.zoom("mouse", n(S(_(R, O), k.mouse[0], k.mouse[1]), k.extent, s)); - function D() { - k.wheel = null, k.end(); + nn(w), M.wheel = setTimeout($, p), M.zoom("mouse", n(k(_(R, z), M.mouse[0], M.mouse[1]), M.extent, s)); + function $() { + M.wheel = null, M.end(); } } - function I(w, ...N) { + function D(w, ...I) { if (f || !t.apply(this, arguments)) return; - var k = w.currentTarget, R = C(this, N, !0).event(w), O = Ye(w.view).on("mousemove.zoom", $, !0).on("mouseup.zoom", F, !0), q = Qe(w, k), D = w.clientX, L = w.clientY; - wr(w.view), ko(w), R.mouse = [q, this.__zoom.invert(q)], Bn(this), R.start(); - function $(K) { - if (en(K), !R.moved) { - var ne = K.clientX - D, G = K.clientY - L; - R.moved = ne * ne + G * G > g; + var M = w.currentTarget, R = E(this, I, !0).event(w), z = Xe(w.view).on("mousemove.zoom", N, !0).on("mouseup.zoom", F, !0), V = Qe(w, M), $ = w.clientX, S = w.clientY; + Er(w.view), Po(w), R.mouse = [V, this.__zoom.invert(V)], qn(this), R.start(); + function N(W) { + if (nn(W), !R.moved) { + var Q = W.clientX - $, U = W.clientY - S; + R.moved = Q * Q + U * U > g; } - R.event(K).zoom("mouse", n(S(R.that.__zoom, R.mouse[0] = Qe(K, k), R.mouse[1]), R.extent, s)); + R.event(W).zoom("mouse", n(k(R.that.__zoom, R.mouse[0] = Qe(W, M), R.mouse[1]), R.extent, s)); } - function F(K) { - O.on("mousemove.zoom mouseup.zoom", null), vr(K.view, R.moved), en(K), R.event(K).end(); + function F(W) { + z.on("mousemove.zoom mouseup.zoom", null), Cr(W.view, R.moved), nn(W), R.event(W).end(); } } - function M(w, ...N) { + function L(w, ...I) { if (t.apply(this, arguments)) { - var k = this.__zoom, R = Qe(w.changedTouches ? w.changedTouches[0] : w, this), O = k.invert(R), q = k.k * (w.shiftKey ? 0.5 : 2), D = n(S(_(k, q), R, O), e.apply(this, N), s); - en(w), a > 0 ? Ye(this).transition().duration(a).call(b, D, R, w) : Ye(this).call(y.transform, D, R, w); + var M = this.__zoom, R = Qe(w.changedTouches ? w.changedTouches[0] : w, this), z = M.invert(R), V = M.k * (w.shiftKey ? 0.5 : 2), $ = n(k(_(M, V), R, z), e.apply(this, I), s); + nn(w), a > 0 ? Xe(this).transition().duration(a).call(C, $, R, w) : Xe(this).call(y.transform, $, R, w); } } - function A(w, ...N) { + function A(w, ...I) { if (t.apply(this, arguments)) { - var k = w.touches, R = k.length, O = C(this, N, w.changedTouches.length === R).event(w), q, D, L, $; - for (ko(w), D = 0; D < R; ++D) - L = k[D], $ = Qe(L, this), $ = [$, this.__zoom.invert($), L.identifier], O.touch0 ? !O.touch1 && O.touch0[2] !== $[2] && (O.touch1 = $, O.taps = 0) : (O.touch0 = $, q = !0, O.taps = 1 + !!d); - d && (d = clearTimeout(d)), q && (O.taps < 2 && (u = $[0], d = setTimeout(function() { + var M = w.touches, R = M.length, z = E(this, I, w.changedTouches.length === R).event(w), V, $, S, N; + for (Po(w), $ = 0; $ < R; ++$) + S = M[$], N = Qe(S, this), N = [N, this.__zoom.invert(N), S.identifier], z.touch0 ? !z.touch1 && z.touch0[2] !== N[2] && (z.touch1 = N, z.taps = 0) : (z.touch0 = N, V = !0, z.taps = 1 + !!d); + d && (d = clearTimeout(d)), V && (z.taps < 2 && (u = N[0], d = setTimeout(function() { d = null; - }, h)), Bn(this), O.start()); + }, h)), qn(this), z.start()); } } - function P(w, ...N) { + function P(w, ...I) { if (this.__zooming) { - var k = C(this, N).event(w), R = w.changedTouches, O = R.length, q, D, L, $; - for (en(w), q = 0; q < O; ++q) - D = R[q], L = Qe(D, this), k.touch0 && k.touch0[2] === D.identifier ? k.touch0[0] = L : k.touch1 && k.touch1[2] === D.identifier && (k.touch1[0] = L); - if (D = k.that.__zoom, k.touch1) { - var F = k.touch0[0], K = k.touch0[1], ne = k.touch1[0], G = k.touch1[1], B = (B = ne[0] - F[0]) * B + (B = ne[1] - F[1]) * B, z = (z = G[0] - K[0]) * z + (z = G[1] - K[1]) * z; - D = _(D, Math.sqrt(B / z)), L = [(F[0] + ne[0]) / 2, (F[1] + ne[1]) / 2], $ = [(K[0] + G[0]) / 2, (K[1] + G[1]) / 2]; - } else if (k.touch0) L = k.touch0[0], $ = k.touch0[1]; + var M = E(this, I).event(w), R = w.changedTouches, z = R.length, V, $, S, N; + for (nn(w), V = 0; V < z; ++V) + $ = R[V], S = Qe($, this), M.touch0 && M.touch0[2] === $.identifier ? M.touch0[0] = S : M.touch1 && M.touch1[2] === $.identifier && (M.touch1[0] = S); + if ($ = M.that.__zoom, M.touch1) { + var F = M.touch0[0], W = M.touch0[1], Q = M.touch1[0], U = M.touch1[1], G = (G = Q[0] - F[0]) * G + (G = Q[1] - F[1]) * G, Z = (Z = U[0] - W[0]) * Z + (Z = U[1] - W[1]) * Z; + $ = _($, Math.sqrt(G / Z)), S = [(F[0] + Q[0]) / 2, (F[1] + Q[1]) / 2], N = [(W[0] + U[0]) / 2, (W[1] + U[1]) / 2]; + } else if (M.touch0) S = M.touch0[0], N = M.touch0[1]; else return; - k.zoom("touch", n(S(D, L, $), k.extent, s)); + M.zoom("touch", n(k($, S, N), M.extent, s)); } } - function v(w, ...N) { + function v(w, ...I) { if (this.__zooming) { - var k = C(this, N).event(w), R = w.changedTouches, O = R.length, q, D; - for (ko(w), f && clearTimeout(f), f = setTimeout(function() { + var M = E(this, I).event(w), R = w.changedTouches, z = R.length, V, $; + for (Po(w), f && clearTimeout(f), f = setTimeout(function() { f = null; - }, h), q = 0; q < O; ++q) - D = R[q], k.touch0 && k.touch0[2] === D.identifier ? delete k.touch0 : k.touch1 && k.touch1[2] === D.identifier && delete k.touch1; - if (k.touch1 && !k.touch0 && (k.touch0 = k.touch1, delete k.touch1), k.touch0) k.touch0[1] = this.__zoom.invert(k.touch0[0]); - else if (k.end(), k.taps === 2 && (D = Qe(D, this), Math.hypot(u[0] - D[0], u[1] - D[1]) < m)) { - var L = Ye(this).on("dblclick.zoom"); - L && L.apply(this, arguments); + }, h), V = 0; V < z; ++V) + $ = R[V], M.touch0 && M.touch0[2] === $.identifier ? delete M.touch0 : M.touch1 && M.touch1[2] === $.identifier && delete M.touch1; + if (M.touch1 && !M.touch0 && (M.touch0 = M.touch1, delete M.touch1), M.touch0) M.touch0[1] = this.__zoom.invert(M.touch0[0]); + else if (M.end(), M.taps === 2 && ($ = Qe($, this), Math.hypot(u[0] - $[0], u[1] - $[1]) < m)) { + var S = Xe(this).on("dblclick.zoom"); + S && S.apply(this, arguments); } } } return y.wheelDelta = function(w) { - return arguments.length ? (o = typeof w == "function" ? w : $n(+w), y) : o; + return arguments.length ? (o = typeof w == "function" ? w : In(+w), y) : o; }, y.filter = function(w) { - return arguments.length ? (t = typeof w == "function" ? w : $n(!!w), y) : t; + return arguments.length ? (t = typeof w == "function" ? w : In(!!w), y) : t; }, y.touchable = function(w) { - return arguments.length ? (i = typeof w == "function" ? w : $n(!!w), y) : i; + return arguments.length ? (i = typeof w == "function" ? w : In(!!w), y) : i; }, y.extent = function(w) { - return arguments.length ? (e = typeof w == "function" ? w : $n([[+w[0][0], +w[0][1]], [+w[1][0], +w[1][1]]]), y) : e; + return arguments.length ? (e = typeof w == "function" ? w : In([[+w[0][0], +w[0][1]], [+w[1][0], +w[1][1]]]), y) : e; }, y.scaleExtent = function(w) { return arguments.length ? (r[0] = +w[0], r[1] = +w[1], y) : [r[0], r[1]]; }, y.translateExtent = function(w) { @@ -2239,7 +2239,7 @@ function ju() { return arguments.length ? (m = +w, y) : m; }, y; } -function Ji(t) { +function es(t) { const { pannable: e, zoomable: n, isLocked: o, noPanClassName: i, noWheelClassName: r, isTouchSelectionMode: s, isPanKeyHeld: a, panOnDrag: l } = t; return (c) => { if (o?.() || i && c.target?.closest?.("." + i) || c.type === "wheel" && r && c.target?.closest?.("." + r) || !n && c.type === "wheel") return !1; @@ -2257,38 +2257,38 @@ function Ji(t) { return !0; }; } -function Uu(t, e) { +function Ju(t, e) { const { onTransformChange: n, minZoom: o = 0.5, maxZoom: i = 2, pannable: r = !0, zoomable: s = !0 - } = e, a = Ye(t); + } = e, a = Xe(t); let l = !1; - const c = e.panActivationKeyCode !== void 0 ? e.panActivationKeyCode : "Space", d = (E) => { - c && E.code === c && (l = !0, t.style.cursor = "grab"); - }, u = (E) => { - c && E.code === c && (l = !1, t.style.cursor = ""); + const c = e.panActivationKeyCode !== void 0 ? e.panActivationKeyCode : "Space", d = (b) => { + c && b.code === c && (l = !0, t.style.cursor = "grab"); + }, u = (b) => { + c && b.code === c && (l = !1, t.style.cursor = ""); }, f = () => { l = !1, t.style.cursor = ""; }; c && (window.addEventListener("keydown", d), window.addEventListener("keyup", u), window.addEventListener("blur", f)); - const h = ju().scaleExtent([o, i]).on("start", (E) => { - if (!E.sourceEvent) return; + const h = Ku().scaleExtent([o, i]).on("start", (b) => { + if (!b.sourceEvent) return; l && (t.style.cursor = "grabbing"); - const { x: T, y: I, k: M } = E.transform; - e.onMoveStart?.({ x: T, y: I, zoom: M }); - }).on("zoom", (E) => { - const { x: T, y: I, k: M } = E.transform; - n({ x: T, y: I, zoom: M }), E.sourceEvent && e.onMove?.({ x: T, y: I, zoom: M }); - }).on("end", (E) => { - if (!E.sourceEvent) return; + const { x: T, y: D, k: L } = b.transform; + e.onMoveStart?.({ x: T, y: D, zoom: L }); + }).on("zoom", (b) => { + const { x: T, y: D, k: L } = b.transform; + n({ x: T, y: D, zoom: L }), b.sourceEvent && e.onMove?.({ x: T, y: D, zoom: L }); + }).on("end", (b) => { + if (!b.sourceEvent) return; l && (t.style.cursor = "grab"); - const { x: T, y: I, k: M } = E.transform; - e.onMoveEnd?.({ x: T, y: I, zoom: M }); + const { x: T, y: D, k: L } = b.transform; + e.onMoveEnd?.({ x: T, y: D, zoom: L }); }); - e.translateExtent && h.translateExtent(e.translateExtent), h.filter(Ji({ + e.translateExtent && h.translateExtent(e.translateExtent), h.filter(es({ pannable: r, zoomable: s, isLocked: e.isLocked, @@ -2299,40 +2299,40 @@ function Uu(t, e) { panOnDrag: e.panOnDrag })), a.call(h), e.zoomOnDoubleClick === !1 && a.on("dblclick.zoom", null); let p = e.panOnScroll ?? !1, g = e.panOnScrollDirection ?? "both", m = e.panOnScrollSpeed ?? 1, y = !1; - const _ = e.zoomActivationKeyCode !== void 0 ? e.zoomActivationKeyCode : null, S = (E) => { - _ && E.code === _ && (y = !0); - }, x = (E) => { - _ && E.code === _ && (y = !1); - }, b = () => { + const _ = e.zoomActivationKeyCode !== void 0 ? e.zoomActivationKeyCode : null, k = (b) => { + _ && b.code === _ && (y = !0); + }, x = (b) => { + _ && b.code === _ && (y = !1); + }, C = () => { y = !1; }; - _ && (window.addEventListener("keydown", S), window.addEventListener("keyup", x), window.addEventListener("blur", b)); - const C = (E) => { + _ && (window.addEventListener("keydown", k), window.addEventListener("keyup", x), window.addEventListener("blur", C)); + const E = (b) => { if (e.isLocked?.()) return; - const T = E.ctrlKey || E.metaKey || y; - if (!(p ? !T : E.shiftKey)) return; - E.preventDefault(), E.stopPropagation(); - const M = m; + const T = b.ctrlKey || b.metaKey || y; + if (!(p ? !T : b.shiftKey)) return; + b.preventDefault(), b.stopPropagation(); + const L = m; let A = 0, P = 0; - g !== "horizontal" && (P = -E.deltaY * M), g !== "vertical" && (A = -E.deltaX * M, E.shiftKey && E.deltaX === 0 && g === "both" && (A = -E.deltaY * M, P = 0)), e.onScrollPan?.(A, P); + g !== "horizontal" && (P = -b.deltaY * L), g !== "vertical" && (A = -b.deltaX * L, b.shiftKey && b.deltaX === 0 && g === "both" && (A = -b.deltaY * L, P = 0)), e.onScrollPan?.(A, P); }; - return t.addEventListener("wheel", C, { passive: !1, capture: !0 }), { - setViewport(E, T) { - const I = T?.duration ?? 0, M = Kn.translate(E.x ?? 0, E.y ?? 0).scale(E.zoom ?? 1); - I > 0 ? a.transition().duration(I).call(h.transform, M) : a.call(h.transform, M); + return t.addEventListener("wheel", E, { passive: !1, capture: !0 }), { + setViewport(b, T) { + const D = T?.duration ?? 0, L = Jn.translate(b.x ?? 0, b.y ?? 0).scale(b.zoom ?? 1); + D > 0 ? a.transition().duration(D).call(h.transform, L) : a.call(h.transform, L); }, getTransform() { - return t.__zoom ?? Kn; + return t.__zoom ?? Jn; }, - update(E) { - if ((E.minZoom !== void 0 || E.maxZoom !== void 0) && h.scaleExtent([ - E.minZoom ?? o, - E.maxZoom ?? i - ]), E.pannable !== void 0 || E.zoomable !== void 0) { - const T = E.pannable ?? r, I = E.zoomable ?? s; - h.filter(Ji({ + update(b) { + if ((b.minZoom !== void 0 || b.maxZoom !== void 0) && h.scaleExtent([ + b.minZoom ?? o, + b.maxZoom ?? i + ]), b.pannable !== void 0 || b.zoomable !== void 0) { + const T = b.pannable ?? r, D = b.zoomable ?? s; + h.filter(es({ pannable: T, - zoomable: I, + zoomable: D, isLocked: e.isLocked, noPanClassName: e.noPanClassName, noWheelClassName: e.noWheelClassName, @@ -2341,37 +2341,37 @@ function Uu(t, e) { panOnDrag: e.panOnDrag })); } - E.panOnScroll !== void 0 && (p = E.panOnScroll), E.panOnScrollDirection !== void 0 && (g = E.panOnScrollDirection), E.panOnScrollSpeed !== void 0 && (m = E.panOnScrollSpeed); + b.panOnScroll !== void 0 && (p = b.panOnScroll), b.panOnScrollDirection !== void 0 && (g = b.panOnScrollDirection), b.panOnScrollSpeed !== void 0 && (m = b.panOnScrollSpeed); }, destroy() { - t.removeEventListener("wheel", C, { capture: !0 }), c && (window.removeEventListener("keydown", d), window.removeEventListener("keyup", u), window.removeEventListener("blur", f)), _ && (window.removeEventListener("keydown", S), window.removeEventListener("keyup", x), window.removeEventListener("blur", b)), a.on(".zoom", null); + t.removeEventListener("wheel", E, { capture: !0 }), c && (window.removeEventListener("keydown", d), window.removeEventListener("keyup", u), window.removeEventListener("blur", f)), _ && (window.removeEventListener("keydown", k), window.removeEventListener("keyup", x), window.removeEventListener("blur", C)), a.on(".zoom", null); } }; } -function Hr(t, e, n, o) { +function Vr(t, e, n, o) { return { x: (t - o.left - n.x) / n.zoom, y: (e - o.top - n.y) / n.zoom }; } -function Gu(t, e, n, o) { +function Qu(t, e, n, o) { return { x: t * n.zoom + n.x + o.left, y: e * n.zoom + n.y + o.top }; } -const we = 150, _e = 50; -function wo(t, e, n, o, i) { +const ve = 150, be = 50; +function vo(t, e, n, o, i) { if (i % 360 === 0) return { x: t, y: e, width: n, height: o }; const r = i * Math.PI / 180, s = Math.abs(Math.cos(r)), a = Math.abs(Math.sin(r)), l = n * s + o * a, c = n * a + o * s, d = t + n / 2, u = e + o / 2; return { x: d - l / 2, y: u - c / 2, width: l, height: c }; } -function Yt(t, e) { +function Wt(t, e) { if (t.length === 0) return { x: 0, y: 0, width: 0, height: 0 }; let n = 1 / 0, o = 1 / 0, i = -1 / 0, r = -1 / 0; for (const s of t) { - const a = s.dimensions?.width ?? we, l = s.dimensions?.height ?? _e, c = Gt(s, e), d = s.rotation ? wo(c.x, c.y, a, l, s.rotation) : { x: c.x, y: c.y, width: a, height: l }; + const a = s.dimensions?.width ?? ve, l = s.dimensions?.height ?? be, c = Kt(s, e), d = s.rotation ? vo(c.x, c.y, a, l, s.rotation) : { x: c.x, y: c.y, width: a, height: l }; n = Math.min(n, d.x), o = Math.min(o, d.y), i = Math.max(i, d.x + d.width), r = Math.max(r, d.y + d.height); } return { @@ -2381,25 +2381,25 @@ function Yt(t, e) { height: r - o }; } -function Zu(t, e, n) { +function ef(t, e, n) { const o = e.x + e.width, i = e.y + e.height; return t.filter((r) => { - const s = r.dimensions?.width ?? we, a = r.dimensions?.height ?? _e, l = Gt(r, n), c = r.rotation ? wo(l.x, l.y, s, a, r.rotation) : { x: l.x, y: l.y, width: s, height: a }, d = c.x + c.width, u = c.y + c.height; + const s = r.dimensions?.width ?? ve, a = r.dimensions?.height ?? be, l = Kt(r, n), c = r.rotation ? vo(l.x, l.y, s, a, r.rotation) : { x: l.x, y: l.y, width: s, height: a }, d = c.x + c.width, u = c.y + c.height; return !(d < e.x || c.x > o || u < e.y || c.y > i); }); } -function Ku(t, e, n) { +function tf(t, e, n) { const o = e.x + e.width, i = e.y + e.height; return t.filter((r) => { - const s = r.dimensions?.width ?? we, a = r.dimensions?.height ?? _e, l = Gt(r, n), c = r.rotation ? wo(l.x, l.y, s, a, r.rotation) : { x: l.x, y: l.y, width: s, height: a }; + const s = r.dimensions?.width ?? ve, a = r.dimensions?.height ?? be, l = Kt(r, n), c = r.rotation ? vo(l.x, l.y, s, a, r.rotation) : { x: l.x, y: l.y, width: s, height: a }; return c.x >= e.x && c.y >= e.y && c.x + c.width <= o && c.y + c.height <= i; }); } -function Jn(t, e, n, o, i, r = 0.1) { +function Qn(t, e, n, o, i, r = 0.1) { const s = Math.max(t.width, 1), a = Math.max(t.height, 1), l = s * (1 + r), c = a * (1 + r), d = e / l, u = n / c, f = Math.min(Math.max(Math.min(d, u), o), i), h = { x: t.x + s / 2, y: t.y + a / 2 }, p = e / 2 - h.x * f, g = n / 2 - h.y * f; return { x: p, y: g, zoom: f }; } -function Ju(t, e, n, o) { +function nf(t, e, n, o) { const i = 1 / t.zoom; return { minX: (0 - t.x) * i - o, @@ -2408,7 +2408,7 @@ function Ju(t, e, n, o) { maxY: (n - t.y) * i + o }; } -class Qu { +class of { constructor(e = 300) { this._cells = /* @__PURE__ */ new Map(), this._nodeCells = /* @__PURE__ */ new Map(), this._cellSize = e; } @@ -2471,24 +2471,24 @@ class Qu { return this._nodeCells.size; } } -function Gt(t, e) { +function Kt(t, e) { if (!t.position) return { x: 0, y: 0 }; - const n = t.nodeOrigin ?? e ?? [0, 0], o = t.dimensions?.width ?? we, i = t.dimensions?.height ?? _e; + const n = t.nodeOrigin ?? e ?? [0, 0], o = t.dimensions?.width ?? ve, i = t.dimensions?.height ?? be; return { x: t.position.x - o * n[0], y: t.position.y - i * n[1] }; } -let Rr = !1; -function Fr(t) { - Rr = t; +let Br = !1; +function qr(t) { + Br = t; } -function V(t, e, n) { - if (!Rr) return; - const o = `%c[AlpineFlow:${t}]`, i = ef(t); +function B(t, e, n) { + if (!Br) return; + const o = `%c[AlpineFlow:${t}]`, i = sf(t); n !== void 0 ? console.log(o, i, e, n) : console.log(o, i, e); } -function ef(t) { +function sf(t) { return `color: ${{ init: "#4ade80", destroy: "#f87171", @@ -2508,8 +2508,8 @@ function ef(t) { clipboard: "#94a3b8" }[t] ?? "#94a3b8"}; font-weight: bold`; } -const yn = "#64748b", vi = "#d4d4d8", Or = "#ef4444", tf = "2", nf = "6 3", Qi = 1.2, Ko = 0.2, Qn = 5, es = 25; -class of { +const vn = "#64748b", bi = "#d4d4d8", Xr = "#ef4444", rf = "2", af = "6 3", ts = 1.2, Qo = 0.2, eo = 5, ns = 25; +class lf { constructor(e = 50) { this.past = [], this.future = [], this._suspendDepth = 0, this.maxSize = e; } @@ -2543,19 +2543,19 @@ class of { return this.future.length > 0; } } -const sf = 16; -function rf() { +const cf = 16; +function df() { return typeof requestAnimationFrame == "function" ? { request: (t) => requestAnimationFrame(t), cancel: (t) => cancelAnimationFrame(t) } : { - request: (t) => setTimeout(() => t(performance.now()), sf), + request: (t) => setTimeout(() => t(performance.now()), cf), cancel: (t) => clearTimeout(t) }; } -class zr { +class Yr { constructor() { - this._scheduler = rf(), this._entries = [], this._postTickCallbacks = [], this._frameId = null, this._running = !1; + this._scheduler = df(), this._entries = [], this._postTickCallbacks = [], this._frameId = null, this._running = !1; } /** True when the rAF loop is running. */ get active() { @@ -2636,47 +2636,47 @@ class zr { this._scheduleFrame(); } } -const eo = new zr(), af = { - linear: au, - easeIn: lu, - easeOut: cu, - easeInOut: du, - easeCubicIn: uu, - easeCubicOut: fu, - easeCubicInOut: $r, - easeCircIn: vu, - easeCircOut: _u, - easeCircInOut: bu, - easeSinIn: hu, - easeSinOut: gu, - easeSinInOut: pu, - easeExpoIn: mu, - easeExpoOut: yu, - easeExpoInOut: wu, - easeBounce: Zn, - easeBounceIn: Tu, - easeBounceInOut: Au, - easeElastic: Hu, - easeElasticIn: Du, - easeElasticInOut: Ru, - easeBack: Iu, - easeBackIn: Nu, - easeBackOut: $u +const to = new Yr(), uf = { + linear: uu, + easeIn: fu, + easeOut: hu, + easeInOut: gu, + easeCubicIn: pu, + easeCubicOut: mu, + easeCubicInOut: Fr, + easeCircIn: Eu, + easeCircOut: Cu, + easeCircInOut: Su, + easeSinIn: yu, + easeSinOut: wu, + easeSinInOut: vu, + easeExpoIn: _u, + easeExpoOut: bu, + easeExpoInOut: xu, + easeBounce: Kn, + easeBounceIn: Iu, + easeBounceInOut: Du, + easeElastic: zu, + easeElasticIn: Ou, + easeElasticInOut: Vu, + easeBack: Fu, + easeBackIn: Ru, + easeBackOut: Hu }; -function Vr(t) { +function Wr(t) { const e = t ?? "auto"; return e === !1 ? !1 : e === !0 ? !0 : typeof globalThis < "u" && globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === !0; } -function to(t) { - return typeof t == "function" ? t : af[t ?? "easeInOut"]; +function no(t) { + return typeof t == "function" ? t : uf[t ?? "easeInOut"]; } function it(t, e, n) { return t + (e - t) * n; } -function _i(t, e, n) { - return Yo(t, e)(n); +function xi(t, e, n) { + return Wo(t, e)(n); } -function wn(t) { +function _n(t) { if (typeof t != "string") return t; if (!t.trim()) @@ -2692,8 +2692,8 @@ function wn(t) { } return e; } -const ts = /^(-?\d+\.?\d*)(px|em|rem|%|vh|vw|pt|cm|mm|in|ex|ch)?$/, ns = /^(#|rgb|hsl)/; -function Br(t, e, n) { +const os = /^(-?\d+\.?\d*)(px|em|rem|%|vh|vw|pt|cm|mm|in|ex|ch)?$/, is = /^(#|rgb|hsl)/; +function jr(t, e, n) { const o = {}, i = /* @__PURE__ */ new Set([...Object.keys(t), ...Object.keys(e)]); for (const r of i) { const s = t[r], a = e[r]; @@ -2705,21 +2705,21 @@ function Br(t, e, n) { o[r] = s; continue; } - const l = ts.exec(s), c = ts.exec(a); + const l = os.exec(s), c = os.exec(a); if (l && c) { const d = parseFloat(l[1]), u = parseFloat(c[1]), f = c[2] ?? "", h = it(d, u, n); o[r] = f ? `${h}${f}` : String(h); continue; } - if (ns.test(s) && ns.test(a)) { - o[r] = _i(s, a, n); + if (is.test(s) && is.test(a)) { + o[r] = xi(s, a, n); continue; } o[r] = n < 0.5 ? s : a; } return o; } -function lf(t, e, n, o) { +function ff(t, e, n, o) { let i = it(t.zoom, e.zoom, n); return o?.minZoom !== void 0 && (i = Math.max(i, o.minZoom)), o?.maxZoom !== void 0 && (i = Math.min(i, o.maxZoom)), { x: it(t.x, e.x, n), @@ -2727,7 +2727,7 @@ function lf(t, e, n, o) { zoom: i }; } -class cf { +class hf { constructor() { this._handles = /* @__PURE__ */ new Set(); } @@ -2763,7 +2763,7 @@ class cf { return this._handles.size; } } -class df { +class gf { constructor() { this._handles = [], this._state = "active", this._propertySnapshots = /* @__PURE__ */ new Map(), this._onAfterRollback = null, this.finished = new Promise((e) => { this._resolveFinished = e; @@ -2813,44 +2813,44 @@ class df { this._state = "rolled-back", this._onAfterRollback?.(e), this._resolveFinished(); } } -const tn = { +const on = { stiffness: 180, damping: 12, mass: 1, restVelocity: 0.01, restDisplacement: 0.01 }; -function qr(t, e, n) { +function Ur(t, e, n) { if (n <= 0) return; - const o = e.stiffness ?? tn.stiffness, i = e.damping ?? tn.damping, r = e.mass ?? tn.mass, s = t.value - t.target, a = (-o * s - i * t.velocity) / r; - t.velocity += a * n, t.value += t.velocity * n, Math.abs(t.velocity) < (e.restVelocity ?? tn.restVelocity) && Math.abs(t.value - t.target) < (e.restDisplacement ?? tn.restDisplacement) && (t.value = t.target, t.velocity = 0, t.settled = !0); + const o = e.stiffness ?? on.stiffness, i = e.damping ?? on.damping, r = e.mass ?? on.mass, s = t.value - t.target, a = (-o * s - i * t.velocity) / r; + t.velocity += a * n, t.value += t.velocity * n, Math.abs(t.velocity) < (e.restVelocity ?? on.restVelocity) && Math.abs(t.value - t.target) < (e.restDisplacement ?? on.restDisplacement) && (t.value = t.target, t.velocity = 0, t.settled = !0); } -const os = { +const ss = { timeConstant: 350, restVelocity: 0.5 }; -function bi(t, e, n) { +function Ei(t, e, n) { if (n <= 0) return; - const o = e.timeConstant ?? os.timeConstant, i = Math.exp(-n * 1e3 / o); - t.velocity *= i, t.value += t.velocity * n, Math.abs(t.velocity) < os.restVelocity && (t.velocity = 0, t.settled = !0, t.target = t.value); + const o = e.timeConstant ?? ss.timeConstant, i = Math.exp(-n * 1e3 / o); + t.velocity *= i, t.value += t.velocity * n, Math.abs(t.velocity) < ss.restVelocity && (t.velocity = 0, t.settled = !0, t.target = t.value); } -function xi(t) { +function Ci(t) { const e = t.lastIndexOf("."), n = t.lastIndexOf(":"), o = Math.max(e, n); if (o < 0) return null; const i = t.slice(o + 1); return i.length === 0 || i.length > 6 ? null : i; } -function Yr(t, e, n, o) { +function Gr(t, e, n, o) { if (n <= 0) return; - bi(t, { + Ei(t, { velocity: t.velocity, power: e.power, timeConstant: e.timeConstant }, n); - const i = o ? xi(o) : null; + const i = o ? Ci(o) : null; if (e.bounds && o) { const r = e.bounds[o] ?? (i ? e.bounds[i] : void 0); if (r) { @@ -2870,8 +2870,8 @@ function Yr(t, e, n, o) { t.value = r; } } -function Xr(t, e, n, o) { - const i = xi(o), r = e.values.map( +function Zr(t, e, n, o) { + const i = Ci(o), r = e.values.map( (p) => p[o] ?? (i ? p[i] : void 0) ?? t.value ); if (r.length < 2) { @@ -2885,20 +2885,20 @@ function Xr(t, e, n, o) { const c = s[l], d = s[l + 1] ?? 1, u = d > c ? (a - c) / (d - c) : 1, f = r[l], h = r[l + 1] ?? r[l]; t.value = f + (h - f) * Math.max(0, Math.min(1, u)), a >= 1 && (t.value = r[r.length - 1], t.settled = !0); } -const is = { +const rs = { gentle: { type: "spring", stiffness: 120, damping: 14 }, wobbly: { type: "spring", stiffness: 180, damping: 12 }, stiff: { type: "spring", stiffness: 300, damping: 30 }, slow: { type: "spring", stiffness: 60, damping: 15 }, molasses: { type: "spring", stiffness: 40, damping: 30 } -}, ss = { +}, as = { smooth: { type: "decay", velocity: 0, power: 0.6, timeConstant: 400 }, snappy: { type: "decay", velocity: 0, power: 1.2, timeConstant: 200 } -}, rs = { +}, ls = { momentum: { type: "inertia", velocity: 0, power: 0.8, timeConstant: 700 }, rails: { type: "inertia", velocity: 0, bounceStiffness: 500, bounceDamping: 40 } }; -function Wr(t) { +function Kr(t) { if (typeof t != "string") return t; const [e, n] = t.split("."); @@ -2906,24 +2906,24 @@ function Wr(t) { return null; switch (e) { case "spring": - return is[n] ? { ...is[n] } : null; + return rs[n] ? { ...rs[n] } : null; case "decay": - return ss[n] ? { ...ss[n] } : null; + return as[n] ? { ...as[n] } : null; case "inertia": - return rs[n] ? { ...rs[n] } : null; + return ls[n] ? { ...ls[n] } : null; default: return null; } } -function as(t) { +function cs(t) { return typeof t != "string" ? !1 : /^(#|rgb|hsl)/.test(t); } -function uf(t, e, n) { - return typeof t == "number" && typeof e == "number" ? it(t, e, n) : as(t) && as(e) ? _i(t, e, n) : n < 0.5 ? t : e; +function pf(t, e, n) { + return typeof t == "number" && typeof e == "number" ? it(t, e, n) : cs(t) && cs(e) ? xi(t, e, n) : n < 0.5 ? t : e; } -class ff { +class mf { constructor(e) { - this._ownership = /* @__PURE__ */ new Map(), this._groups = /* @__PURE__ */ new Set(), this._nextGroupId = 0, this._registry = new cf(), this._activeTransaction = null, this._engine = e; + this._ownership = /* @__PURE__ */ new Map(), this._groups = /* @__PURE__ */ new Set(), this._nextGroupId = 0, this._registry = new hf(), this._activeTransaction = null, this._engine = e; } /** Whether any animations are currently running. */ get active() { @@ -2935,7 +2935,7 @@ class ff { } /** Begin a new transaction — all subsequent `animate()` calls will be tracked until `endTransaction()`. */ beginTransaction() { - const e = new df(); + const e = new gf(); return this._activeTransaction = e, e; } /** End the current transaction context (does NOT commit or rollback — the caller decides). */ @@ -2965,26 +2965,26 @@ class ff { whileStopMode: p = "jump-end", motion: g, maxDuration: m = 5e3 - } = n, y = to(i), _ = g ? Wr(g) : void 0; + } = n, y = no(i), _ = g ? Kr(g) : void 0; for (const w of e) { - const N = this._ownership.get(w.key); - if (N && !N.stopped) { - const k = N.currentValues.get(w.key); - k !== void 0 && (w.from = k), N.entries = N.entries.filter((R) => R.key !== w.key), N.entries.length === 0 && this._stop(N, "superseded"); + const I = this._ownership.get(w.key); + if (I && !I.stopped) { + const M = I.currentValues.get(w.key); + M !== void 0 && (w.from = M), I.entries = I.entries.filter((R) => R.key !== w.key), I.entries.length === 0 && this._stop(I, "superseded"); } } if (this._activeTransaction && this._activeTransaction.state === "active") for (const w of e) this._activeTransaction.captureProperty(w.key, w.from, w.apply); if (o <= 0) { - const w = /* @__PURE__ */ new Map(), N = /* @__PURE__ */ new Map(); - for (const O of e) - w.set(O.key, O.from), N.set(O.key, O.to); + const w = /* @__PURE__ */ new Map(), I = /* @__PURE__ */ new Map(); + for (const z of e) + w.set(z.key, z.from), I.set(z.key, z.to); l?.(); - for (const O of e) - O.apply(O.to); - const k = [...u ? [u] : [], ...f ?? []], R = { - _tags: k.length > 0 ? k : void 0, + for (const z of e) + z.apply(z.to); + const M = [...u ? [u] : [], ...f ?? []], R = { + _tags: M.length > 0 ? M : void 0, pause: () => { }, resume: () => { @@ -3008,24 +3008,24 @@ class ff { return !0; }, get currentValue() { - return N; + return I; }, finished: Promise.resolve(), get _snapshot() { return w; }, get _target() { - return N; + return I; } }; return this._registry.register(R), queueMicrotask(() => this._registry.unregister(R)), this._activeTransaction && this._activeTransaction.state === "active" && this._activeTransaction.trackHandle(R), d?.(), R; } - const S = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(); + const k = /* @__PURE__ */ new Map(), x = /* @__PURE__ */ new Map(); for (const w of e) - S.set(w.key, w.from), x.set(w.key, w.to); - let b; + k.set(w.key, w.from), x.set(w.key, w.to); + let C; if (_) { - b = /* @__PURE__ */ new Map(); + C = /* @__PURE__ */ new Map(); for (const w of e) { if (typeof w.from != "number" || typeof w.to != "number") { console.warn( @@ -3033,42 +3033,42 @@ class ff { ), w.apply(w.to); continue; } - let N = 0; + let I = 0; if (_.type === "decay" || _.type === "inertia") { - const k = _.velocity; - if (typeof k == "number") - N = k; - else if (k && typeof k == "object") { - const O = k, q = xi(w.key); - N = O[w.key] ?? (q ? O[q] ?? 0 : 0); + const M = _.velocity; + if (typeof M == "number") + I = M; + else if (M && typeof M == "object") { + const z = M, V = Ci(w.key); + I = z[w.key] ?? (V ? z[V] ?? 0 : 0); } const R = _.power ?? 0.8; - N *= R; + I *= R; } - b.set(w.key, { + C.set(w.key, { value: w.from, - velocity: N, + velocity: I, target: w.to, settled: !1 }); } - b.size === 0 && (b = void 0); + C.size === 0 && (C = void 0); } - const C = s === "ping-pong" ? "reverse" : s, E = a === "end" ? "backward" : "forward"; + const E = s === "ping-pong" ? "reverse" : s, b = a === "end" ? "backward" : "forward"; let T; - const I = new Promise((w) => { + const D = new Promise((w) => { T = w; - }), M = { + }), L = { _id: this._nextGroupId++, entries: [...e], engineHandle: null, startTime: 0, pausedElapsed: null, _resumeNeeded: !1, - direction: E, + direction: b, duration: o, easingFn: y, - loop: C, + loop: E, onStart: l, startFired: !1, onProgress: c, @@ -3079,58 +3079,58 @@ class ff { currentValues: /* @__PURE__ */ new Map(), _lastElapsed: 0, _lastTickWallTime: 0, - snapshot: S, + snapshot: k, target: x, - _currentFinished: I, + _currentFinished: D, whilePredicate: h, whileStopMode: p, - motionConfig: b ? _ : void 0, - physicsStates: b, + motionConfig: C ? _ : void 0, + physicsStates: C, maxDuration: m, - isPhysics: !!b, + isPhysics: !!C, _prevElapsed: 0 }; if (a === "end") - for (const w of M.entries) - w.apply(w.to), M.currentValues.set(w.key, w.to); + for (const w of L.entries) + w.apply(w.to), L.currentValues.set(w.key, w.to); else - for (const w of M.entries) - M.currentValues.set(w.key, w.from); + for (const w of L.entries) + L.currentValues.set(w.key, w.from); for (const w of e) - this._ownership.set(w.key, M); - this._groups.add(M); - const A = this._engine.register((w) => this._tick(M, w), r); - M.engineHandle = A; + this._ownership.set(w.key, L); + this._groups.add(L); + const A = this._engine.register((w) => this._tick(L, w), r); + L.engineHandle = A; const P = [...u ? [u] : [], ...f ?? []], v = { _tags: P.length > 0 ? P : void 0, - pause: () => this._pause(M), - resume: () => this._resume(M), - stop: (w) => this._stop(M, w?.mode ?? "jump-end"), - reverse: () => this._reverse(M), - play: () => this._play(M), - playForward: () => this._playDirection(M, "forward"), - playBackward: () => this._playDirection(M, "backward"), - restart: (w) => this._restart(M, w), + pause: () => this._pause(L), + resume: () => this._resume(L), + stop: (w) => this._stop(L, w?.mode ?? "jump-end"), + reverse: () => this._reverse(L), + play: () => this._play(L), + playForward: () => this._playDirection(L, "forward"), + playBackward: () => this._playDirection(L, "backward"), + restart: (w) => this._restart(L, w), get direction() { - return M.direction; + return L.direction; }, get isFinished() { - return M.isFinished; + return L.isFinished; }, get currentValue() { - return M.currentValues; + return L.currentValues; }, get finished() { - return M._currentFinished; + return L._currentFinished; }, get _snapshot() { - return M.snapshot; + return L.snapshot; }, get _target() { - return M.target; + return L.target; } }; - return this._registry.register(v), M._handle = v, this._activeTransaction && this._activeTransaction.state === "active" && this._activeTransaction.trackHandle(v), v; + return this._registry.register(v), L._handle = v, this._activeTransaction && this._activeTransaction.state === "active" && this._activeTransaction.trackHandle(v), v; } /** Stop all active animations. */ stopAll(e) { @@ -3164,7 +3164,7 @@ class ff { i = o % e.duration / e.duration; const r = e.direction === "backward" ? 1 - i : i, s = e.easingFn(r); for (const a of e.entries) { - const l = uf(a.from, a.to, s); + const l = pf(a.from, a.to, s); e.currentValues.set(a.key, l), a.apply(l); } if (e.onProgress?.(r), !e.loop && i >= 1) { @@ -3207,17 +3207,17 @@ class ff { if (!d.settled) { switch (e.direction === "backward" ? d.target = e.snapshot.get(c.key) : d.target = e.target.get(c.key), e.motionConfig.type) { case "spring": - qr(d, e.motionConfig, i); + Ur(d, e.motionConfig, i); break; case "decay": - bi(d, e.motionConfig, i); + Ei(d, e.motionConfig, i); break; case "inertia": - Yr(d, e.motionConfig, i, c.key); + Gr(d, e.motionConfig, i, c.key); break; case "keyframes": { const u = n - e.startTime, f = e.motionConfig.duration ?? e.maxDuration, h = Math.min(u / f, 1); - Xr(d, e.motionConfig, h, c.key); + Zr(d, e.motionConfig, h, c.key); break; } } @@ -3375,21 +3375,21 @@ class ff { this._groups.delete(e); } } -const jr = /* @__PURE__ */ new Map(); -function hf(t, e) { - jr.set(t, e); +const Jr = /* @__PURE__ */ new Map(); +function yf(t, e) { + Jr.set(t, e); } -function Lo(t) { +function Mo(t) { return t.replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); } -function Ot(t) { +function Vt(t) { return typeof t == "string" ? { type: t } : t; } -function zt(t, e) { - return `${e}__${t.type}__${(t.color ?? vi).replace(/[^a-zA-Z0-9]/g, "_")}`; +function Bt(t, e) { + return `${e}__${t.type}__${(t.color ?? bi).replace(/[^a-zA-Z0-9]/g, "_")}`; } -function no(t, e) { - const n = Lo(t.color ?? vi), o = Number(t.width ?? 12.5), i = Number(t.height ?? 12.5), r = Number.isFinite(o) && o > 0 ? o : 12.5, s = Number.isFinite(i) && i > 0 ? i : 12.5, a = Lo(t.orient ?? "auto-start-reverse"), l = Lo(e); +function oo(t, e) { + const n = Mo(t.color ?? bi), o = Number(t.width ?? 12.5), i = Number(t.height ?? 12.5), r = Number.isFinite(o) && o > 0 ? o : 12.5, s = Number.isFinite(i) && i > 0 ? i : 12.5, a = Mo(t.orient ?? "auto-start-reverse"), l = Mo(e); if (t.type === "arrow") return ` `; - const c = jr.get(t.type); - return c ? c({ id: l, color: n, width: r, height: s, orient: a }) : no({ ...t, type: "arrowclosed" }, e); + const c = Jr.get(t.type); + return c ? c({ id: l, color: n, width: r, height: s, orient: a }) : oo({ ...t, type: "arrowclosed" }, e); } -const bt = 200, xt = 150, gf = 1.2, nn = "http://www.w3.org/2000/svg"; -function pf(t, e) { +const bt = 200, xt = 150, wf = 1.2, sn = "http://www.w3.org/2000/svg"; +function vf(t, e) { const { getState: n, setViewport: o, config: i } = e, r = i.minimapPosition ?? "bottom-right", s = i.minimapMaskColor, a = i.minimapNodeColor, l = document.createElement("div"); l.className = `flow-minimap flow-minimap-${r}`; - const c = document.createElementNS(nn, "svg"); + const c = document.createElementNS(sn, "svg"); c.setAttribute("width", String(bt)), c.setAttribute("height", String(xt)); - const d = document.createElementNS(nn, "rect"); + const d = document.createElementNS(sn, "rect"); d.classList.add("flow-minimap-bg"), d.setAttribute("width", String(bt)), d.setAttribute("height", String(xt)); - const u = document.createElementNS(nn, "g"); + const u = document.createElementNS(sn, "g"); u.classList.add("flow-minimap-nodes"); - const f = document.createElementNS(nn, "path"); + const f = document.createElementNS(sn, "path"); f.classList.add("flow-minimap-mask"), s && f.setAttribute("fill", s), f.setAttribute("fill-rule", "evenodd"), c.appendChild(d), c.appendChild(u), c.appendChild(f), l.appendChild(c), t.appendChild(l); let h = { x: 0, y: 0, width: 0, height: 0 }, p = 1; function g() { const A = n(); - if (h = Yt(A.nodes.filter((P) => !P.hidden), i.nodeOrigin), h.width === 0 && h.height === 0) { + if (h = Wt(A.nodes.filter((P) => !P.hidden), i.nodeOrigin), h.width === 0 && h.height === 0) { p = 1; return; } p = Math.max( h.width / bt, h.height / xt - ) * gf; + ) * wf; } function m(A) { return typeof a == "function" ? a(A) : a; @@ -3466,10 +3466,10 @@ function pf(t, e) { const P = (bt - h.width / p) / 2, v = (xt - h.height / p) / 2; for (const w of A.nodes) { if (w.hidden) continue; - const N = document.createElementNS(nn, "rect"), k = (w.dimensions?.width ?? we) / p, R = (w.dimensions?.height ?? _e) / p, O = (w.position.x - h.x) / p + P, q = (w.position.y - h.y) / p + v; - N.setAttribute("x", String(O)), N.setAttribute("y", String(q)), N.setAttribute("width", String(k)), N.setAttribute("height", String(R)), N.setAttribute("rx", "2"); - const D = m(w); - D && (N.style.fill = D), u.appendChild(N); + const I = document.createElementNS(sn, "rect"), M = (w.dimensions?.width ?? ve) / p, R = (w.dimensions?.height ?? be) / p, z = (w.position.x - h.x) / p + P, V = (w.position.y - h.y) / p + v; + I.setAttribute("x", String(z)), I.setAttribute("y", String(V)), I.setAttribute("width", String(M)), I.setAttribute("height", String(R)), I.setAttribute("rx", "2"); + const $ = m(w); + $ && (I.style.fill = $), u.appendChild(I); } _(); } @@ -3479,43 +3479,43 @@ function pf(t, e) { f.setAttribute("d", ""); return; } - const P = (bt - h.width / p) / 2, v = (xt - h.height / p) / 2, w = (-A.viewport.x / A.viewport.zoom - h.x) / p + P, N = (-A.viewport.y / A.viewport.zoom - h.y) / p + v, k = A.containerWidth / A.viewport.zoom / p, R = A.containerHeight / A.viewport.zoom / p, O = `M0,0 H${bt} V${xt} H0 Z`, q = `M${w},${N} h${k} v${R} h${-k} Z`; - f.setAttribute("d", `${O} ${q}`); + const P = (bt - h.width / p) / 2, v = (xt - h.height / p) / 2, w = (-A.viewport.x / A.viewport.zoom - h.x) / p + P, I = (-A.viewport.y / A.viewport.zoom - h.y) / p + v, M = A.containerWidth / A.viewport.zoom / p, R = A.containerHeight / A.viewport.zoom / p, z = `M0,0 H${bt} V${xt} H0 Z`, V = `M${w},${I} h${M} v${R} h${-M} Z`; + f.setAttribute("d", `${z} ${V}`); } - let S = !1; + let k = !1; function x(A, P) { - const v = (bt - h.width / p) / 2, w = (xt - h.height / p) / 2, N = (A - v) * p + h.x, k = (P - w) * p + h.y; - return { x: N, y: k }; - } - function b(A) { - const P = c.getBoundingClientRect(), v = A.clientX - P.left, w = A.clientY - P.top, N = n(), k = x(v, w), R = -k.x * N.viewport.zoom + N.containerWidth / 2, O = -k.y * N.viewport.zoom + N.containerHeight / 2; - o({ x: R, y: O, zoom: N.viewport.zoom }); + const v = (bt - h.width / p) / 2, w = (xt - h.height / p) / 2, I = (A - v) * p + h.x, M = (P - w) * p + h.y; + return { x: I, y: M }; } function C(A) { - i.minimapPannable && (S = !0, c.setPointerCapture(A.pointerId), b(A)); + const P = c.getBoundingClientRect(), v = A.clientX - P.left, w = A.clientY - P.top, I = n(), M = x(v, w), R = -M.x * I.viewport.zoom + I.containerWidth / 2, z = -M.y * I.viewport.zoom + I.containerHeight / 2; + o({ x: R, y: z, zoom: I.viewport.zoom }); } function E(A) { - S && b(A); + i.minimapPannable && (k = !0, c.setPointerCapture(A.pointerId), C(A)); + } + function b(A) { + k && C(A); } function T(A) { - S && (S = !1, c.releasePointerCapture(A.pointerId)); + k && (k = !1, c.releasePointerCapture(A.pointerId)); } - c.addEventListener("pointerdown", C), c.addEventListener("pointermove", E), c.addEventListener("pointerup", T); - function I(A) { + c.addEventListener("pointerdown", E), c.addEventListener("pointermove", b), c.addEventListener("pointerup", T); + function D(A) { if (!i.minimapZoomable) return; A.preventDefault(); - const P = n(), v = i.minZoom ?? 0.5, w = i.maxZoom ?? 2, N = A.deltaY > 0 ? 0.9 : 1.1, k = Math.min(Math.max(P.viewport.zoom * N, v), w); - o({ zoom: k }); + const P = n(), v = i.minZoom ?? 0.5, w = i.maxZoom ?? 2, I = A.deltaY > 0 ? 0.9 : 1.1, M = Math.min(Math.max(P.viewport.zoom * I, v), w); + o({ zoom: M }); } - c.addEventListener("wheel", I, { passive: !1 }); - function M() { - c.removeEventListener("pointerdown", C), c.removeEventListener("pointermove", E), c.removeEventListener("pointerup", T), c.removeEventListener("wheel", I), l.remove(); + c.addEventListener("wheel", D, { passive: !1 }); + function L() { + c.removeEventListener("pointerdown", E), c.removeEventListener("pointermove", b), c.removeEventListener("pointerup", T), c.removeEventListener("wheel", D), l.remove(); } - return { render: y, updateViewport: _, destroy: M }; + return { render: y, updateViewport: _, destroy: L }; } -const mf = '', yf = '', wf = '', ls = '', vf = '', _f = '', cs = '', bf = ''; -function xf(t, e) { +const _f = '', bf = '', xf = '', ds = '', Ef = '', Cf = '', us = '', Sf = ''; +function kf(t, e) { const { position: n, orientation: o, @@ -3537,45 +3537,45 @@ function xf(t, e) { l ? m.push("flow-controls-external") : m.push(`flow-controls-${n}`), g.className = m.join(" "), g.setAttribute("role", "toolbar"), g.setAttribute("aria-label", "Flow controls"); let y = null, _ = null; if (i) { - const b = Tt(mf, "Zoom in", c), C = Tt(yf, "Zoom out", d); - g.appendChild(b), g.appendChild(C); + const C = At(_f, "Zoom in", c), E = At(bf, "Zoom out", d); + g.appendChild(C), g.appendChild(E); } if (r) { - const b = Tt(wf, "Fit view", u); - g.appendChild(b); - } - if (s && (y = Tt(ls, "Toggle interactivity", f), g.appendChild(y)), a) { - const b = Tt(_f, "Reset panels", h); - g.appendChild(b); - } - p && (_ = Tt(cs, "Toggle fullscreen", p), _.classList.add("flow-controls-button-fullscreen"), g.appendChild(_)), g.addEventListener("mousedown", (b) => b.stopPropagation()), g.addEventListener("pointerdown", (b) => b.stopPropagation()), g.addEventListener("wheel", (b) => b.stopPropagation(), { passive: !1 }), t.appendChild(g); - function S(b) { - if (y && typeof b.isInteractive == "boolean") { - Jo(y, b.isInteractive ? ls : vf); - const C = b.isInteractive ? "Lock interactivity" : "Unlock interactivity"; - y.title = C, y.setAttribute("aria-label", C); + const C = At(xf, "Fit view", u); + g.appendChild(C); + } + if (s && (y = At(ds, "Toggle interactivity", f), g.appendChild(y)), a) { + const C = At(Cf, "Reset panels", h); + g.appendChild(C); + } + p && (_ = At(us, "Toggle fullscreen", p), _.classList.add("flow-controls-button-fullscreen"), g.appendChild(_)), g.addEventListener("mousedown", (C) => C.stopPropagation()), g.addEventListener("pointerdown", (C) => C.stopPropagation()), g.addEventListener("wheel", (C) => C.stopPropagation(), { passive: !1 }), t.appendChild(g); + function k(C) { + if (y && typeof C.isInteractive == "boolean") { + ei(y, C.isInteractive ? ds : Ef); + const E = C.isInteractive ? "Lock interactivity" : "Unlock interactivity"; + y.title = E, y.setAttribute("aria-label", E); } - if (_ && typeof b.isFullscreen == "boolean") { - Jo(_, b.isFullscreen ? bf : cs); - const C = b.isFullscreen ? "Exit fullscreen" : "Enter fullscreen"; - _.title = C, _.setAttribute("aria-label", C), _.classList.toggle("flow-controls-button-fullscreen--active", b.isFullscreen); + if (_ && typeof C.isFullscreen == "boolean") { + ei(_, C.isFullscreen ? Sf : us); + const E = C.isFullscreen ? "Exit fullscreen" : "Enter fullscreen"; + _.title = E, _.setAttribute("aria-label", E), _.classList.toggle("flow-controls-button-fullscreen--active", C.isFullscreen); } } function x() { g.remove(); } - return { update: S, destroy: x }; + return { update: k, destroy: x }; } -function Tt(t, e, n) { +function At(t, e, n) { const o = document.createElement("button"); - return o.type = "button", Jo(o, t), o.title = e, o.setAttribute("aria-label", e), o.addEventListener("click", n), o; + return o.type = "button", ei(o, t), o.title = e, o.setAttribute("aria-label", e), o.addEventListener("click", n), o; } -function Jo(t, e) { +function ei(t, e) { const o = new DOMParser().parseFromString(e, "image/svg+xml").documentElement; t.replaceChildren(o); } -const ds = 5; -function Ef(t) { +const fs = 5; +function Lf(t) { const e = document.createElement("div"); e.className = "flow-selection-box", t.appendChild(e); let n = !1, o = 0, i = 0, r = 0, s = 0; @@ -3594,10 +3594,10 @@ function Ef(t) { return null; n = !1, e.classList.remove("flow-selection-box-active"), e.classList.remove("flow-selection-partial", "flow-selection-full"); const h = Math.abs(r - o), p = Math.abs(s - i); - if (h < ds && p < ds) + if (h < fs && p < fs) return null; - const g = Math.min(o, r), m = Math.min(i, s), y = (g - f.x) / f.zoom, _ = (m - f.y) / f.zoom, S = h / f.zoom, x = p / f.zoom; - return { x: y, y: _, width: S, height: x }; + const g = Math.min(o, r), m = Math.min(i, s), y = (g - f.x) / f.zoom, _ = (m - f.y) / f.zoom, k = h / f.zoom, x = p / f.zoom; + return { x: y, y: _, width: k, height: x }; } function d() { return n; @@ -3607,8 +3607,8 @@ function Ef(t) { } return { start: a, update: l, end: c, isActive: d, destroy: u }; } -const us = 3; -function Cf(t) { +const hs = 3; +function Pf(t) { const e = document.createElementNS("http://www.w3.org/2000/svg", "svg"); e.classList.add("flow-lasso-svg"), e.setAttribute("width", "100%"), e.setAttribute("height", "100%"), t.appendChild(e); const n = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); @@ -3621,7 +3621,7 @@ function Cf(t) { if (!o) return; const f = i[i.length - 1], h = d - f.x, p = u - f.y; - h * h + p * p < us * us || (i.push({ x: d, y: u }), n.setAttribute("points", i.map((g) => `${g.x},${g.y}`).join(" "))); + h * h + p * p < hs * hs || (i.push({ x: d, y: u }), n.setAttribute("points", i.map((g) => `${g.x},${g.y}`).join(" "))); } function a(d) { if (!o || (o = !1, e.classList.remove("flow-lasso-active", "flow-lasso-partial", "flow-lasso-full"), n.setAttribute("points", ""), i.length < 3)) @@ -3640,7 +3640,7 @@ function Cf(t) { } return { start: r, update: s, end: a, isActive: l, destroy: c }; } -function Ei(t, e, n) { +function Si(t, e, n) { if (n.length < 3) return !1; let o = !1; for (let i = 0, r = n.length - 1; i < n.length; r = i++) { @@ -3649,15 +3649,15 @@ function Ei(t, e, n) { } return o; } -function Sf(t, e, n, o, i, r, s, a) { +function Mf(t, e, n, o, i, r, s, a) { const l = n - t, c = o - e, d = s - i, u = a - r, f = l * u - c * d; if (Math.abs(f) < 1e-10) return !1; const h = i - t, p = r - e, g = (h * u - p * d) / f, m = (h * c - p * l) / f; return g >= 0 && g <= 1 && m >= 0 && m <= 1; } -function kf(t, e) { +function Tf(t, e) { const n = e.x, o = e.y, i = e.x + e.width, r = e.y + e.height, s = n + e.width / 2, a = o + e.height / 2; - if (Ei(s, a, t)) return !0; + if (Si(s, a, t)) return !0; for (const c of t) if (c.x >= n && c.x <= i && c.y >= o && c.y <= r) return !0; const l = [ @@ -3672,49 +3672,49 @@ function kf(t, e) { ]; for (let c = 0, d = t.length - 1; c < t.length; d = c++) for (const [u, f, h, p] of l) - if (Sf(t[d].x, t[d].y, t[c].x, t[c].y, u, f, h, p)) + if (Mf(t[d].x, t[d].y, t[c].x, t[c].y, u, f, h, p)) return !0; return !1; } -function Ur(t) { - const e = t.dimensions?.width ?? we, n = t.dimensions?.height ?? _e; - return t.rotation ? wo(t.position.x, t.position.y, e, n, t.rotation) : { x: t.position.x, y: t.position.y, width: e, height: n }; +function Qr(t) { + const e = t.dimensions?.width ?? ve, n = t.dimensions?.height ?? be; + return t.rotation ? vo(t.position.x, t.position.y, e, n, t.rotation) : { x: t.position.x, y: t.position.y, width: e, height: n }; } -function Lf(t, e) { +function Nf(t, e) { return e.length < 3 ? [] : t.filter((n) => { if (n.hidden || n.selectable === !1) return !1; - const o = Ur(n); - return kf(e, o); + const o = Qr(n); + return Tf(e, o); }); } -function Pf(t, e) { +function Af(t, e) { return e.length < 3 ? [] : t.filter((n) => { if (n.hidden || n.selectable === !1) return !1; - const o = Ur(n); + const o = Qr(n); return [ { x: o.x, y: o.y }, { x: o.x + o.width, y: o.y }, { x: o.x + o.width, y: o.y + o.height }, { x: o.x, y: o.y + o.height } - ].every((r) => Ei(r.x, r.y, e)); + ].every((r) => Si(r.x, r.y, e)); }); } -function Mf(t, e) { +function $f(t, e) { return e.filter((n) => n.source === t || n.target === t); } -function Qo(t, e, n) { +function ti(t, e, n) { const o = new Set( n.filter((i) => i.source === t).map((i) => i.target) ); return e.filter((i) => o.has(i.id)); } -function Tf(t, e, n) { +function If(t, e, n) { const o = new Set( n.filter((i) => i.target === t).map((i) => i.source) ); return e.filter((i) => o.has(i.id)); } -function Af(t, e, n) { +function Df(t, e, n) { if (t === e) return !0; const o = /* @__PURE__ */ new Map(); for (const s of n) { @@ -3734,10 +3734,10 @@ function Af(t, e, n) { } return !1; } -function Nf(t, e, n, o = !1) { +function Rf(t, e, n, o = !1) { return n.some((i) => o ? i.source === t && i.target === e : i.source === t && i.target === e || i.source === e && i.target === t); } -function $f(t, e, n) { +function Hf(t, e, n) { const o = new Map(e.map((l) => [l.id, l])), i = new Set( n.map((l) => `${l.source}|${l.target}|${l.sourceHandle ?? ""}|${l.targetHandle ?? ""}`) ), r = [], s = /* @__PURE__ */ new Set(); @@ -3782,10 +3782,10 @@ function gt(t, e, n) { function pt(t, e, n) { return !(t.source === t.target || e.some( (i) => i.source === t.source && i.target === t.target && i.sourceHandle === t.sourceHandle && i.targetHandle === t.targetHandle - ) || n?.preventCycles && Af(t.source, t.target, e)); + ) || n?.preventCycles && Df(t.source, t.target, e)); } const We = "_flowHandleValidate"; -function If(t) { +function Ff(t) { t.directive( "flow-handle-validate", (e, { expression: n }, { evaluate: o, effect: i, cleanup: r }) => { @@ -3811,7 +3811,7 @@ function If(t) { ); } const ut = "_flowHandleLimit"; -function Df(t) { +function Of(t) { t.directive( "flow-handle-limit", (e, { expression: n }, { evaluate: o, effect: i, cleanup: r }) => { @@ -3825,7 +3825,7 @@ function Df(t) { ); } const Pt = "_flowHandleConnectableStart", st = "_flowHandleConnectableEnd"; -function Hf(t) { +function zf(t) { t.directive( "flow-handle-connectable", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -3839,32 +3839,32 @@ function Hf(t) { } ); } -function Cn(t, e, n = !0) { +function kn(t, e, n = !0) { return e !== void 0 ? e : t.locked ? !1 : n; } -function Gr(t) { - return Cn(t, t.draggable); +function ea(t) { + return kn(t, t.draggable); } -function Rf(t) { - return Cn(t, t.deletable); +function Vf(t) { + return kn(t, t.deletable); } -function Be(t) { - return Cn(t, t.connectable); +function qe(t) { + return kn(t, t.connectable); } -function ei(t) { - return Cn(t, t.selectable); +function ni(t) { + return kn(t, t.selectable); } -function fs(t) { - return Cn(t, t.resizable); +function gs(t) { + return kn(t, t.resizable); } -function Xt(t, e, n, o, i, r, s) { +function jt(t, e, n, o, i, r, s) { const a = n - t, l = o - e, c = i - n, d = r - o; if (a === 0 && c === 0 || l === 0 && d === 0) return `L${n},${o}`; const u = Math.sqrt(a * a + l * l), f = Math.sqrt(c * c + d * d), h = Math.min(s, u / 2, f / 2), p = n - a / u * h, g = o - l / u * h, m = n + c / f * h, y = o + d / f * h; return `L${p},${g} Q${n},${o} ${m},${y}`; } -function Sn({ +function Ln({ sourceX: t, sourceY: e, targetX: n, @@ -3878,10 +3878,10 @@ function Sn({ offsetY: r }; } -function In(t, e) { +function Dn(t, e) { return t >= 0 ? 0.5 * t : e * 25 * Math.sqrt(-t); } -function Ff({ +function Bf({ sourceX: t, sourceY: e, sourcePosition: n = "bottom", @@ -3890,23 +3890,23 @@ function Ff({ targetPosition: r = "top", curvature: s = 0.25 }) { - const a = n === "left" || n === "right", l = r === "left" || r === "right", c = a ? t + (n === "right" ? 1 : -1) * In( + const a = n === "left" || n === "right", l = r === "left" || r === "right", c = a ? t + (n === "right" ? 1 : -1) * Dn( n === "right" ? o - t : t - o, s - ) : t, d = a ? e : e + (n === "bottom" ? 1 : -1) * In( + ) : t, d = a ? e : e + (n === "bottom" ? 1 : -1) * Dn( n === "bottom" ? i - e : e - i, s - ), u = l ? o + (r === "right" ? 1 : -1) * In( + ), u = l ? o + (r === "right" ? 1 : -1) * Dn( r === "right" ? t - o : o - t, s - ) : o, f = l ? i : i + (r === "bottom" ? 1 : -1) * In( + ) : o, f = l ? i : i + (r === "bottom" ? 1 : -1) * Dn( r === "bottom" ? e - i : i - e, s ); return [c, d, u, f]; } -function oo(t) { - const { sourceX: e, sourceY: n, targetX: o, targetY: i } = t, [r, s, a, l] = Ff(t), c = `M${e},${n} C${r},${s} ${a},${l} ${o},${i}`, { x: d, y: u, offsetX: f, offsetY: h } = Sn({ sourceX: e, sourceY: n, targetX: o, targetY: i }); +function io(t) { + const { sourceX: e, sourceY: n, targetX: o, targetY: i } = t, [r, s, a, l] = Bf(t), c = `M${e},${n} C${r},${s} ${a},${l} ${o},${i}`, { x: d, y: u, offsetX: f, offsetY: h } = Ln({ sourceX: e, sourceY: n, targetX: o, targetY: i }); return { path: c, labelPosition: { x: d, y: u }, @@ -3914,13 +3914,13 @@ function oo(t) { labelOffsetY: h }; } -function Dy({ +function Gy({ sourceX: t, sourceY: e, targetX: n, targetY: o }) { - const i = (t + n) / 2, r = `M${t},${e} C${i},${e} ${i},${o} ${n},${o}`, { x: s, y: a, offsetX: l, offsetY: c } = Sn({ sourceX: t, sourceY: e, targetX: n, targetY: o }); + const i = (t + n) / 2, r = `M${t},${e} C${i},${e} ${i},${o} ${n},${o}`, { x: s, y: a, offsetX: l, offsetY: c } = Ln({ sourceX: t, sourceY: e, targetX: n, targetY: o }); return { path: r, labelPosition: { x: s, y: a }, @@ -3928,7 +3928,7 @@ function Dy({ labelOffsetY: c }; } -function hs(t) { +function ps(t) { switch (t) { case "top": case "top-left": @@ -3944,8 +3944,8 @@ function hs(t) { return { x: 1, y: 0 }; } } -function Of(t, e, n, o, i, r, s) { - const a = hs(n), l = hs(r), c = t + a.x * s, d = e + a.y * s, u = o + l.x * s, f = i + l.y * s, h = n === "left" || n === "right"; +function qf(t, e, n, o, i, r, s) { + const a = ps(n), l = ps(r), c = t + a.x * s, d = e + a.y * s, u = o + l.x * s, f = i + l.y * s, h = n === "left" || n === "right"; if (h === (r === "left" || r === "right")) { const g = (c + u) / 2, m = (d + f) / 2; return h ? [ @@ -3970,7 +3970,7 @@ function Of(t, e, n, o, i, r, s) { [u, i] ]; } -function vn({ +function bn({ sourceX: t, sourceY: e, sourcePosition: n = "bottom", @@ -3980,7 +3980,7 @@ function vn({ borderRadius: s = 5, offset: a = 10 }) { - const l = Of( + const l = qf( t, e, n, @@ -3993,13 +3993,13 @@ function vn({ for (let p = 0; p < l.length; p++) { const [g, m] = l[p]; if (s > 0 && p > 0 && p < l.length - 1) { - const [y, _] = p === 1 ? [t, e] : l[p - 1], [S, x] = l[p + 1]; - c += ` ${Xt(y, _, g, m, S, x, s)}`; + const [y, _] = p === 1 ? [t, e] : l[p - 1], [k, x] = l[p + 1]; + c += ` ${jt(y, _, g, m, k, x, s)}`; } else c += ` L${g},${m}`; } c += ` L${o},${i}`; - const { x: d, y: u, offsetX: f, offsetY: h } = Sn({ sourceX: t, sourceY: e, targetX: o, targetY: i }); + const { x: d, y: u, offsetX: f, offsetY: h } = Ln({ sourceX: t, sourceY: e, targetX: o, targetY: i }); return { path: c, labelPosition: { x: d, y: u }, @@ -4007,16 +4007,16 @@ function vn({ labelOffsetY: h }; } -function zf(t) { - return vn({ ...t, borderRadius: 0 }); +function Xf(t) { + return bn({ ...t, borderRadius: 0 }); } -function Zr({ +function ta({ sourceX: t, sourceY: e, targetX: n, targetY: o }) { - const i = `M${t},${e} L${n},${o}`, { x: r, y: s, offsetX: a, offsetY: l } = Sn({ sourceX: t, sourceY: e, targetX: n, targetY: o }); + const i = `M${t},${e} L${n},${o}`, { x: r, y: s, offsetX: a, offsetY: l } = Ln({ sourceX: t, sourceY: e, targetX: n, targetY: o }); return { path: i, labelPosition: { x: r, y: s }, @@ -4025,18 +4025,18 @@ function Zr({ }; } const at = 40; -function Vf(t, e, n, o) { +function Yf(t, e, n, o) { let i = 0, r = 0; const s = t - n.left, a = n.right - t, l = e - n.top, c = n.bottom - e; return s < at && s >= 0 ? i = -o * (1 - s / at) : a < at && a >= 0 && (i = o * (1 - a / at)), l < at && l >= 0 ? r = -o * (1 - l / at) : c < at && c >= 0 && (r = o * (1 - c / at)), { dx: i, dy: r }; } -function Kr(t) { +function na(t) { const { container: e, speed: n, onPan: o } = t; let i = null, r = 0, s = 0, a = !1; function l() { if (!a) return; - const c = e.getBoundingClientRect(), { dx: d, dy: u } = Vf(r, s, c, n); + const c = e.getBoundingClientRect(), { dx: d, dy: u } = Yf(r, s, c, n); if ((d !== 0 || u !== 0) && o(d, u) === !0) { a = !1, i = null; return; @@ -4058,11 +4058,11 @@ function Kr(t) { } }; } -function Wt(t) { +function Ut(t) { const e = t.connectionLineType ?? "straight", o = { - stroke: (t.invalid ? (t.containerEl ? getComputedStyle(t.containerEl).getPropertyValue("--flow-connection-line-invalid").trim() : "") || Or : null) ?? t.connectionLineStyle?.stroke ?? ((t.containerEl ? getComputedStyle(t.containerEl).getPropertyValue("--flow-edge-stroke-selected").trim() : "") || yn), - strokeWidth: t.connectionLineStyle?.strokeWidth ?? Number(tf), - strokeDasharray: t.connectionLineStyle?.strokeDasharray ?? nf + stroke: (t.invalid ? (t.containerEl ? getComputedStyle(t.containerEl).getPropertyValue("--flow-connection-line-invalid").trim() : "") || Xr : null) ?? t.connectionLineStyle?.stroke ?? ((t.containerEl ? getComputedStyle(t.containerEl).getPropertyValue("--flow-edge-stroke-selected").trim() : "") || vn), + strokeWidth: t.connectionLineStyle?.strokeWidth ?? Number(rf), + strokeDasharray: t.connectionLineStyle?.strokeDasharray ?? af }, i = document.createElementNS("http://www.w3.org/2000/svg", "svg"); i.setAttribute("class", "flow-connect-line"), i.style.cssText = "position:absolute;top:0;left:0;width:1px;height:1px;overflow:visible;pointer-events:none;z-index:1000;"; let r = null; @@ -4081,19 +4081,19 @@ function Wt(t) { let p; switch (e) { case "bezier": { - p = oo({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; + p = io({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; break; } case "smoothstep": { - p = vn({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; + p = bn({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; break; } case "step": { - p = zf({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; + p = Xf({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; break; } default: { - p = Zr({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; + p = ta({ sourceX: d, sourceY: u, targetX: f, targetY: h }).path; break; } } @@ -4104,7 +4104,7 @@ function Wt(t) { } return { svg: i, update: s, destroy: a }; } -function _n(t) { +function xn(t) { if (t.connectionSnapRadius <= 0) return { element: null, position: t.cursorFlowPos }; if (t.index) { @@ -4113,7 +4113,7 @@ function _n(t) { for (const d of s) { if (d.nodeId === t.excludeNodeId || t.targetNodeId && d.nodeId !== t.targetNodeId) continue; const u = t.getNode(d.nodeId); - if (u && !Be(u) || (t.handleType === "target" ? !d.connectableEnd : !d.connectableStart)) continue; + if (u && !qe(u) || (t.handleType === "target" ? !d.connectableEnd : !d.connectableStart)) continue; const f = t.cursorFlowPos.x - d.flowX, h = t.cursorFlowPos.y - d.flowY, p = Math.sqrt(f * f + h * h); p < c && (c = p, a = d.el, l = { x: d.flowX, y: d.flowY }); } @@ -4127,7 +4127,7 @@ function _n(t) { const c = l.dataset.flowNodeId; if (c) { const p = t.getNode(c); - if (p && !Be(p)) return; + if (p && !qe(p)) return; } const d = t.handleType === "target" ? st : Pt; if (a[d] === !1) return; @@ -4142,9 +4142,9 @@ function _n(t) { h < r && (r = h, o = a, i = f); }), { element: o, position: i }; } -function vo(t, e, n, o) { +function _o(t, e, n, o) { if (e._config?.autoPanOnConnect === !1) return null; - const i = Kr({ + const i = na({ container: t, speed: e._config?.autoPanSpeed ?? 15, onPan(r, s) { @@ -4160,7 +4160,7 @@ function vo(t, e, n, o) { }); return i.updatePointer(n, o), i.start(), i; } -function Bf(t, e, n, o) { +function Wf(t, e, n, o) { const i = o ? t.edges.filter((c) => c.id !== o) : t.edges, r = /* @__PURE__ */ new Set(), s = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(); for (const c of i) { const d = `${c.source}|${c.sourceHandle ?? "source"}`, u = `${c.target}|${c.targetHandle ?? "target"}`; @@ -4185,7 +4185,7 @@ function Bf(t, e, n, o) { } return { existingTargets: r, cycleForbidden: l, sourceCounts: s, targetCounts: a }; } -function Jr(t, e) { +function oa(t, e) { const n = [], o = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map(), r = t.querySelectorAll("[data-flow-handle-type]"); for (const l of r) { const c = l.closest("[data-flow-node-id]"); @@ -4218,8 +4218,8 @@ function Jr(t, e) { get: (l, c, d) => o.get(`${l}|${c}|${d}`) }; } -let dn = 0; -const Dn = /* @__PURE__ */ new WeakMap(); +let fn = 0; +const Rn = /* @__PURE__ */ new WeakMap(); function tt(t, e) { const n = t.querySelector( `[data-flow-node-id="${CSS.escape(e.source)}"]` @@ -4270,12 +4270,12 @@ function nt(t, e, n) { } return !0; } -function bn(t, e, n, o, i, r) { +function En(t, e, n, o, i, r) { if (!r) { - qf(t, e, n, o, i); + jf(t, e, n, o, i); return; } - const s = Bf(o, e, n, i), a = r.get(e, n, "source"), l = a?.limit != null && (s.sourceCounts.get(`${e}|${n}`) ?? 0) >= a.limit, c = []; + const s = Wf(o, e, n, i), a = r.get(e, n, "source"), l = a?.limit != null && (s.sourceCounts.get(`${e}|${n}`) ?? 0) >= a.limit, c = []; for (const d of r.byType("target")) { if (!d.connectableEnd) { c.push({ el: d.el, valid: !1, limitHit: !1 }); @@ -4297,7 +4297,7 @@ function bn(t, e, n, o, i, r) { for (const d of c) d.el.classList.toggle("flow-handle-valid", d.valid), d.el.classList.toggle("flow-handle-invalid", !d.valid), d.el.classList.toggle("flow-handle-limit-reached", d.limitHit); } -function qf(t, e, n, o, i) { +function jf(t, e, n, o, i) { const r = i ? o.edges.filter((a) => a.id !== i) : o.edges, s = t.querySelectorAll('[data-flow-handle-type="target"]'); for (const a of s) { const c = a.closest("[x-flow-node]")?.dataset.flowNodeId; @@ -4316,7 +4316,7 @@ function qf(t, e, n, o, i) { p && tt(t, u) && (!o._config?.isValidConnection || o._config.isValidConnection(u)) ? (a.classList.add("flow-handle-valid"), a.classList.remove("flow-handle-invalid", "flow-handle-limit-reached")) : (a.classList.add("flow-handle-invalid"), a.classList.remove("flow-handle-valid"), h && !p ? a.classList.add("flow-handle-limit-reached") : a.classList.remove("flow-handle-limit-reached")); } } -function ke(t) { +function Pe(t) { const e = t.querySelectorAll('[data-flow-handle-type="target"]'); for (const n of e) n.classList.remove("flow-handle-valid", "flow-handle-invalid", "flow-handle-limit-reached"); @@ -4324,7 +4324,7 @@ function ke(t) { function kt(t, e) { t && (e ? t.classList.add("flow-connect-line--validating") : t.classList.remove("flow-connect-line--validating")); } -function Ae(t, e) { +function Ne(t, e) { const n = { source: e.source, target: e.target, @@ -4337,7 +4337,7 @@ function Ae(t, e) { bubbles: !0 }))); } -async function _o(t, e, n, o, i, r) { +async function bo(t, e, n, o, i, r) { if (!t) return { allowed: !0 }; n?.classList.add(r), o?.classList.add(r), i.dispatchEvent(new CustomEvent("flow-connect-validating", { detail: { connection: e }, @@ -4347,7 +4347,7 @@ async function _o(t, e, n, o, i, r) { try { s = await t(e); } catch (c) { - V("connection", "connectValidator threw", c), s = !1; + B("connection", "connectValidator threw", c), s = !1; } finally { n?.classList.remove(r), o?.classList.remove(r); } @@ -4357,10 +4357,10 @@ async function _o(t, e, n, o, i, r) { bubbles: !0 })), { allowed: a, reason: l }; } -async function Qr(t) { +async function ia(t) { const { edge: e, newConnection: n, canvas: o, containerEl: i } = t, r = t.endpoint ?? "target", s = o.edges.filter( (c) => c.id !== e.id - ), a = (c) => (Ae(i, { + ), a = (c) => (Ne(i, { source: n.source, target: n.target, sourceHandle: n.sourceHandle, @@ -4371,11 +4371,11 @@ async function Qr(t) { return a(); const l = o._config?.connectValidator; if (l) { - const c = o._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: d, targetEl: u } = bo(i, n); + const c = o._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: d, targetEl: u } = xo(i, n); o._connectValidating = !0; let f; try { - f = await _o( + f = await bo( l, n, d, @@ -4391,23 +4391,23 @@ async function Qr(t) { } return o._captureHistory?.(), r === "source" ? (e.source = n.source, e.sourceHandle = n.sourceHandle) : (e.target = n.target, e.targetHandle = n.targetHandle), { applied: !0 }; } -async function ea(t) { - const { connection: e, canvas: n, containerEl: o } = t, i = n.edges, r = (d) => (Ae(o, { +async function sa(t) { + const { connection: e, canvas: n, containerEl: o } = t, i = n.edges, r = (d) => (Ne(o, { source: e.source, target: e.target, sourceHandle: e.sourceHandle, targetHandle: e.targetHandle, reason: d }), { applied: !1, reason: d }), s = n.getNode?.(e.target); - if (s && !Be(s) || !pt(e, i, { preventCycles: n._config?.preventCycles }) || !gt(e, n._config?.connectionRules, n._nodeMap) || !nt(o, e, i) || !tt(o, e) || n._config?.isValidConnection && !n._config.isValidConnection(e)) + if (s && !qe(s) || !pt(e, i, { preventCycles: n._config?.preventCycles }) || !gt(e, n._config?.connectionRules, n._nodeMap) || !nt(o, e, i) || !tt(o, e) || n._config?.isValidConnection && !n._config.isValidConnection(e)) return r(); const a = n._config?.connectValidator; if (a) { - const d = n._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: u, targetEl: f } = bo(o, e); + const d = n._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: u, targetEl: f } = xo(o, e); n._connectValidating = !0; let h; try { - h = await _o( + h = await bo( a, e, u, @@ -4421,10 +4421,10 @@ async function ea(t) { if (!h.allowed) return r(h.reason); } - const c = { id: `e-${e.source}-${e.target}-${Date.now()}-${dn++}`, ...e }; + const c = { id: `e-${e.source}-${e.target}-${Date.now()}-${fn++}`, ...e }; return n.addEdges(c), n._emit?.("connect", { connection: e }), { applied: !0, edge: c }; } -function bo(t, e) { +function xo(t, e) { const n = t.querySelector( `[data-flow-node-id="${CSS.escape(e.source)}"]` ), o = e.sourceHandle ?? "source", i = n?.querySelector( @@ -4437,14 +4437,14 @@ function bo(t, e) { return { sourceEl: i, targetEl: a }; } const ft = /* @__PURE__ */ new WeakMap(); -function ta(t, e, n) { +function ra(t, e, n) { n.preventDefault(), n.stopPropagation(); const o = t.dataset.flowHandleId ?? "source", i = t.closest("[x-flow-node]"); if (!e || !i || e._animationLocked) return; const r = i.dataset.flowNodeId; if (!r) return; const s = e.getNode(r); - if (s && !Be(s) || t[Pt] === !1) return; + if (s && !qe(s) || t[Pt] === !1) return; const a = n.clientX, l = n.clientY; let c = !1; if (e.pendingConnection && e._config?.connectOnClick !== !1) { @@ -4455,331 +4455,331 @@ function ta(t, e, n) { position: { x: 0, y: 0 } }), e.pendingConnection = null, e._container?.classList.remove("flow-connecting"); const A = t.closest(".flow-container"); - A && ke(A); + A && Pe(A); } let d = null, u = null, f = null, h = null, p = null; const g = e._config?.connectionSnapRadius ?? 20, m = t.closest(".flow-container"); - let y = null, _ = 0, S = 0, x = !1, b = /* @__PURE__ */ new Map(); - const C = () => { - if (c = !0, V("connection", `Connection drag started from node "${r}" handle "${o}"`), e._emit("connect-start", { source: r, sourceHandle: o }), !m) return; - u = Wt({ + let y = null, _ = 0, k = 0, x = !1, C = /* @__PURE__ */ new Map(); + const E = () => { + if (c = !0, B("connection", `Connection drag started from node "${r}" handle "${o}"`), e._emit("connect-start", { source: r, sourceHandle: o }), !m) return; + u = Ut({ connectionLineType: e._config?.connectionLineType, connectionLineStyle: e._config?.connectionLineStyle, connectionLine: e._config?.connectionLine, containerEl: m }), d = u.svg; - const A = t.getBoundingClientRect(), P = m.getBoundingClientRect(), v = e._viewportLive ?? e.viewport, w = v?.zoom || 1, N = v?.x || 0, k = v?.y || 0; - _ = (A.left + A.width / 2 - P.left - N) / w, S = (A.top + A.height / 2 - P.top - k) / w, u.update({ fromX: _, fromY: S, toX: _, toY: S, source: r, sourceHandle: o }); + const A = t.getBoundingClientRect(), P = m.getBoundingClientRect(), v = e._viewportLive ?? e.viewport, w = v?.zoom || 1, I = v?.x || 0, M = v?.y || 0; + _ = (A.left + A.width / 2 - P.left - I) / w, k = (A.top + A.height / 2 - P.top - M) / w, u.update({ fromX: _, fromY: k, toX: _, toY: k, source: r, sourceHandle: o }); const R = m.querySelector(".flow-viewport"); if (R && R.appendChild(d), e.pendingConnection = { source: r, sourceHandle: o, - position: { x: _, y: S } - }, h = vo(m, e, a, l), y = Jr( + position: { x: _, y: k } + }, h = _o(m, e, a, l), y = oa( m, - (O, q) => e.screenToFlowPosition(O, q) - ), bn(m, r, o, e, void 0, y), e._config?.onEdgeDrop) { - const O = e._config.edgeDropPreview, D = O ? O({ source: r, sourceHandle: o }) : "New Node"; - if (D !== null) { + (z, V) => e.screenToFlowPosition(z, V) + ), En(m, r, o, e, void 0, y), e._config?.onEdgeDrop) { + const z = e._config.edgeDropPreview, $ = z ? z({ source: r, sourceHandle: o }) : "New Node"; + if ($ !== null) { p = document.createElement("div"), p.className = "flow-ghost-node"; - const L = document.createElement("div"); - if (L.className = "flow-ghost-handle", p.appendChild(L), typeof D == "string") { + const S = document.createElement("div"); + if (S.className = "flow-ghost-handle", p.appendChild(S), typeof $ == "string") { const F = document.createElement("span"); - F.textContent = D, p.appendChild(F); + F.textContent = $, p.appendChild(F); } else - p.appendChild(D); - p.style.left = `${_}px`, p.style.top = `${S}px`; - const $ = m.querySelector(".flow-viewport"); - $ && $.appendChild(p); + p.appendChild($); + p.style.left = `${_}px`, p.style.top = `${k}px`; + const N = m.querySelector(".flow-viewport"); + N && N.appendChild(p); } } - }, E = () => { - const A = [...e.selectedNodes], P = [], v = m.getBoundingClientRect(), w = e._viewportLive ?? e.viewport, N = w?.zoom || 1, k = w?.x || 0, R = w?.y || 0; - for (const O of A) { - if (O === r) continue; - const D = m?.querySelector(`[data-flow-node-id="${CSS.escape(O)}"]`)?.querySelector('[data-flow-handle-type="source"]'); - if (!D) continue; - const L = D.getBoundingClientRect(); + }, b = () => { + const A = [...e.selectedNodes], P = [], v = m.getBoundingClientRect(), w = e._viewportLive ?? e.viewport, I = w?.zoom || 1, M = w?.x || 0, R = w?.y || 0; + for (const z of A) { + if (z === r) continue; + const $ = m?.querySelector(`[data-flow-node-id="${CSS.escape(z)}"]`)?.querySelector('[data-flow-handle-type="source"]'); + if (!$) continue; + const S = $.getBoundingClientRect(); P.push({ - nodeId: O, - handleId: D.dataset.flowHandleId ?? "source", + nodeId: z, + handleId: $.dataset.flowHandleId ?? "source", pos: { - x: (L.left + L.width / 2 - v.left - k) / N, - y: (L.top + L.height / 2 - v.top - R) / N + x: (S.left + S.width / 2 - v.left - M) / I, + y: (S.top + S.height / 2 - v.top - R) / I } }); } return P; }, T = (A) => { - x = !0, u && (b.set(r, { + x = !0, u && (C.set(r, { line: u, sourceNodeId: r, sourceHandleId: o, - sourcePos: { x: _, y: S }, + sourcePos: { x: _, y: k }, valid: !0 }), u = null); - const P = E(), v = m.querySelector(".flow-viewport"); + const P = b(), v = m.querySelector(".flow-viewport"); for (const w of P) { - const N = Wt({ + const I = Ut({ connectionLineType: e._config?.connectionLineType, connectionLineStyle: e._config?.connectionLineStyle, connectionLine: e._config?.connectionLine, containerEl: m }); - N.update({ + I.update({ fromX: w.pos.x, fromY: w.pos.y, toX: A.x, toY: A.y, source: w.nodeId, sourceHandle: w.handleId - }), v && v.appendChild(N.svg), b.set(w.nodeId, { - line: N, + }), v && v.appendChild(I.svg), C.set(w.nodeId, { + line: I, sourceNodeId: w.nodeId, sourceHandleId: w.handleId, sourcePos: w.pos, valid: !0 }); } - }, I = (A) => { + }, D = (A) => { if (!c) { - const w = A.clientX - a, N = A.clientY - l; - if (Math.abs(w) >= Qn || Math.abs(N) >= Qn) { - if (C(), e._config?.multiConnect && e.selectedNodes.size > 1 && e.selectedNodes.has(r)) { - const k = e.screenToFlowPosition(A.clientX, A.clientY); - T(k); + const w = A.clientX - a, I = A.clientY - l; + if (Math.abs(w) >= eo || Math.abs(I) >= eo) { + if (E(), e._config?.multiConnect && e.selectedNodes.size > 1 && e.selectedNodes.has(r)) { + const M = e.screenToFlowPosition(A.clientX, A.clientY); + T(M); } } else return; } const P = e.screenToFlowPosition(A.clientX, A.clientY); if (x) { - const w = _n({ + const w = xn({ containerEl: m, handleType: "target", excludeNodeId: r, cursorFlowPos: P, connectionSnapRadius: g, - getNode: (q) => e.getNode(q), - toFlowPosition: (q, D) => e.screenToFlowPosition(q, D), + getNode: (V) => e.getNode(V), + toFlowPosition: (V, $) => e.screenToFlowPosition(V, $), connectionMode: e._config?.connectionMode, index: y ?? void 0 }); w.element !== f && (f?.classList.remove("flow-handle-active"), w.element?.classList.add("flow-handle-active"), f = w.element); - const k = w.element?.closest("[x-flow-node]")?.dataset.flowNodeId ?? null, R = w.element?.dataset.flowHandleId ?? "target", O = e._config?.connectionLineStyle?.stroke ?? (getComputedStyle(m).getPropertyValue("--flow-edge-stroke-selected").trim() || yn); - for (const q of b.values()) - if (q.line.update({ - fromX: q.sourcePos.x, - fromY: q.sourcePos.y, + const M = w.element?.closest("[x-flow-node]")?.dataset.flowNodeId ?? null, R = w.element?.dataset.flowHandleId ?? "target", z = e._config?.connectionLineStyle?.stroke ?? (getComputedStyle(m).getPropertyValue("--flow-edge-stroke-selected").trim() || vn); + for (const V of C.values()) + if (V.line.update({ + fromX: V.sourcePos.x, + fromY: V.sourcePos.y, toX: w.position.x, toY: w.position.y, - source: q.sourceNodeId, - sourceHandle: q.sourceHandleId - }), w.element && k) { - const D = { - source: q.sourceNodeId, - sourceHandle: q.sourceHandleId, - target: k, + source: V.sourceNodeId, + sourceHandle: V.sourceHandleId + }), w.element && M) { + const $ = { + source: V.sourceNodeId, + sourceHandle: V.sourceHandleId, + target: M, targetHandle: R - }, G = e.getNode(k)?.connectable !== !1 && q.sourceNodeId !== k && pt(D, e.edges, { preventCycles: e._config?.preventCycles }) && gt(D, e._config?.connectionRules, e._nodeMap) && nt(m, D, e.edges) && tt(m, D) && (!e._config?.isValidConnection || e._config.isValidConnection(D)); - q.valid = G; - const B = q.line.svg.querySelector("path"); - if (B) - if (G) - B.setAttribute("stroke", O); + }, U = e.getNode(M)?.connectable !== !1 && V.sourceNodeId !== M && pt($, e.edges, { preventCycles: e._config?.preventCycles }) && gt($, e._config?.connectionRules, e._nodeMap) && nt(m, $, e.edges) && tt(m, $) && (!e._config?.isValidConnection || e._config.isValidConnection($)); + V.valid = U; + const G = V.line.svg.querySelector("path"); + if (G) + if (U) + G.setAttribute("stroke", z); else { - const z = getComputedStyle(m).getPropertyValue("--flow-connection-line-invalid").trim() || Or; - B.setAttribute("stroke", z); + const Z = getComputedStyle(m).getPropertyValue("--flow-connection-line-invalid").trim() || Xr; + G.setAttribute("stroke", Z); } } else { - q.valid = !0; - const D = q.line.svg.querySelector("path"); - D && D.setAttribute("stroke", O); + V.valid = !0; + const $ = V.line.svg.querySelector("path"); + $ && $.setAttribute("stroke", z); } e.pendingConnection = { ...e.pendingConnection, position: w.position }, h?.updatePointer(A.clientX, A.clientY); return; } - const v = _n({ + const v = xn({ containerEl: m, handleType: "target", excludeNodeId: r, cursorFlowPos: P, connectionSnapRadius: g, getNode: (w) => e.getNode(w), - toFlowPosition: (w, N) => e.screenToFlowPosition(w, N), + toFlowPosition: (w, I) => e.screenToFlowPosition(w, I), index: y ?? void 0 }); - v.element !== f && (f?.classList.remove("flow-handle-active"), v.element?.classList.add("flow-handle-active"), f = v.element), p ? v.element ? (p.style.display = "none", u?.update({ fromX: _, fromY: S, toX: v.position.x, toY: v.position.y, source: r, sourceHandle: o })) : (p.style.display = "", p.style.left = `${P.x}px`, p.style.top = `${P.y}px`, u?.update({ fromX: _, fromY: S, toX: P.x, toY: P.y, source: r, sourceHandle: o })) : u?.update({ fromX: _, fromY: S, toX: v.position.x, toY: v.position.y, source: r, sourceHandle: o }), e.pendingConnection = { ...e.pendingConnection, position: v.position }, h?.updatePointer(A.clientX, A.clientY); - }, M = async (A) => { - if (h?.stop(), h = null, document.removeEventListener("pointermove", I), document.removeEventListener("pointerup", M), document.removeEventListener("pointercancel", M), ft.delete(t), y = null, e._connectValidating) return; + v.element !== f && (f?.classList.remove("flow-handle-active"), v.element?.classList.add("flow-handle-active"), f = v.element), p ? v.element ? (p.style.display = "none", u?.update({ fromX: _, fromY: k, toX: v.position.x, toY: v.position.y, source: r, sourceHandle: o })) : (p.style.display = "", p.style.left = `${P.x}px`, p.style.top = `${P.y}px`, u?.update({ fromX: _, fromY: k, toX: P.x, toY: P.y, source: r, sourceHandle: o })) : u?.update({ fromX: _, fromY: k, toX: v.position.x, toY: v.position.y, source: r, sourceHandle: o }), e.pendingConnection = { ...e.pendingConnection, position: v.position }, h?.updatePointer(A.clientX, A.clientY); + }, L = async (A) => { + if (h?.stop(), h = null, document.removeEventListener("pointermove", D), document.removeEventListener("pointerup", L), document.removeEventListener("pointercancel", L), ft.delete(t), y = null, e._connectValidating) return; if (x) { - const N = e.screenToFlowPosition(A.clientX, A.clientY); - let k = f; - k || (k = document.elementFromPoint(A.clientX, A.clientY)?.closest('[data-flow-handle-type="target"]')); - const O = k?.closest("[x-flow-node]")?.dataset.flowNodeId ?? null, q = k?.dataset.flowHandleId ?? "target", D = [], L = [], $ = [], F = []; - if (k && O) { - const K = e.getNode(O); - for (const ne of b.values()) { - const G = { - source: ne.sourceNodeId, - sourceHandle: ne.sourceHandleId, - target: O, - targetHandle: q + const I = e.screenToFlowPosition(A.clientX, A.clientY); + let M = f; + M || (M = document.elementFromPoint(A.clientX, A.clientY)?.closest('[data-flow-handle-type="target"]')); + const z = M?.closest("[x-flow-node]")?.dataset.flowNodeId ?? null, V = M?.dataset.flowHandleId ?? "target", $ = [], S = [], N = [], F = []; + if (M && z) { + const W = e.getNode(z); + for (const Q of C.values()) { + const U = { + source: Q.sourceNodeId, + sourceHandle: Q.sourceHandleId, + target: z, + targetHandle: V }; - if (K?.connectable !== !1 && ne.sourceNodeId !== O && pt(G, e.edges, { preventCycles: e._config?.preventCycles }) && gt(G, e._config?.connectionRules, e._nodeMap) && nt(m, G, e.edges) && tt(m, G) && (!e._config?.isValidConnection || e._config.isValidConnection(G))) { - const H = `e-${ne.sourceNodeId}-${O}-${Date.now()}-${dn++}`; - D.push({ id: H, ...G }), L.push(G), F.push(ne); + if (W?.connectable !== !1 && Q.sourceNodeId !== z && pt(U, e.edges, { preventCycles: e._config?.preventCycles }) && gt(U, e._config?.connectionRules, e._nodeMap) && nt(m, U, e.edges) && tt(m, U) && (!e._config?.isValidConnection || e._config.isValidConnection(U))) { + const K = `e-${Q.sourceNodeId}-${z}-${Date.now()}-${fn++}`; + $.push({ id: K, ...U }), S.push(U), F.push(Q); } else - $.push(ne); + N.push(Q); } } else - $.push(...b.values()); - for (const K of F) - K.line.destroy(); - if (D.length > 0) { - e.addEdges(D); - for (const K of L) - e._emit("connect", { connection: K }); - e._emit("multi-connect", { connections: L }); + N.push(...C.values()); + for (const W of F) + W.line.destroy(); + if ($.length > 0) { + e.addEdges($); + for (const W of S) + e._emit("connect", { connection: W }); + e._emit("multi-connect", { connections: S }); } - $.length > 0 && setTimeout(() => { - for (const K of $) - K.line.destroy(); + N.length > 0 && setTimeout(() => { + for (const W of N) + W.line.destroy(); }, 100), f?.classList.remove("flow-handle-active"), e._emit("connect-end", { - connection: L.length > 0 ? L[0] : null, + connection: S.length > 0 ? S[0] : null, source: r, sourceHandle: o, - position: N - }), b.clear(), x = !1, ke(m), e.pendingConnection = null, e._container?.classList.remove("flow-connecting"); + position: I + }), C.clear(), x = !1, Pe(m), e.pendingConnection = null, e._container?.classList.remove("flow-connecting"); return; } if (!c) { - e._config?.connectOnClick !== !1 && (V("connection", `Click-to-connect started from node "${r}" handle "${o}"`), e._emit("connect-start", { source: r, sourceHandle: o }), e.pendingConnection = { + e._config?.connectOnClick !== !1 && (B("connection", `Click-to-connect started from node "${r}" handle "${o}"`), e._emit("connect-start", { source: r, sourceHandle: o }), e.pendingConnection = { source: r, sourceHandle: o, position: { x: 0, y: 0 } - }, e._container?.classList.add("flow-connecting"), bn(m, r, o, e, void 0, y ?? void 0)); + }, e._container?.classList.add("flow-connecting"), En(m, r, o, e, void 0, y ?? void 0)); return; } const P = u?.svg ?? null; - p?.remove(), p = null, f?.classList.remove("flow-handle-active"), ke(m); + p?.remove(), p = null, f?.classList.remove("flow-handle-active"), Pe(m); const v = e.screenToFlowPosition(A.clientX, A.clientY), w = { source: r, sourceHandle: o, position: v }; try { - let N = f; - if (N || (N = document.elementFromPoint(A.clientX, A.clientY)?.closest('[data-flow-handle-type="target"]')), N) { - const R = N.closest("[x-flow-node]")?.dataset.flowNodeId, O = N.dataset.flowHandleId ?? "target"; + let I = f; + if (I || (I = document.elementFromPoint(A.clientX, A.clientY)?.closest('[data-flow-handle-type="target"]')), I) { + const R = I.closest("[x-flow-node]")?.dataset.flowNodeId, z = I.dataset.flowHandleId ?? "target"; if (R) { - if (N[st] === !1) { - V("connection", "Connection rejected (handle not connectable end)"), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; + if (I[st] === !1) { + B("connection", "Connection rejected (handle not connectable end)"), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; return; } - const q = e.getNode(R); - if (q && !Be(q)) { - V("connection", `Connection rejected (target "${R}" not connectable)`), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; + const V = e.getNode(R); + if (V && !qe(V)) { + B("connection", `Connection rejected (target "${R}" not connectable)`), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; return; } - const D = { + const $ = { source: r, sourceHandle: o, target: R, - targetHandle: O + targetHandle: z }; - if (pt(D, e.edges, { preventCycles: e._config?.preventCycles })) { - if (!gt(D, e._config?.connectionRules, e._nodeMap)) { - V("connection", "Connection rejected (connection rules)", D), Ae(m, D), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; + if (pt($, e.edges, { preventCycles: e._config?.preventCycles })) { + if (!gt($, e._config?.connectionRules, e._nodeMap)) { + B("connection", "Connection rejected (connection rules)", $), Ne(m, $), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; return; } - if (!nt(m, D, e.edges)) { - V("connection", "Connection rejected (handle limit)", D), Ae(m, D), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; + if (!nt(m, $, e.edges)) { + B("connection", "Connection rejected (handle limit)", $), Ne(m, $), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; return; } - if (!tt(m, D)) { - V("connection", "Connection rejected (per-handle validator)", D), Ae(m, D), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; + if (!tt(m, $)) { + B("connection", "Connection rejected (per-handle validator)", $), Ne(m, $), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; return; } - if (e._config?.isValidConnection && !e._config.isValidConnection(D)) { - V("connection", "Connection rejected (custom validator)", D), Ae(m, D), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; + if (e._config?.isValidConnection && !e._config.isValidConnection($)) { + B("connection", "Connection rejected (custom validator)", $), Ne(m, $), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; return; } - const L = e._config?.connectValidator; - if (L) { - const F = e._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: K, targetEl: ne } = bo(m, D); + const S = e._config?.connectValidator; + if (S) { + const F = e._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: W, targetEl: Q } = xo(m, $); e._connectValidating = !0, kt(P, !0); - let G; + let U; try { - G = await _o( - L, - D, - K, - ne, + U = await bo( + S, + $, + W, + Q, m, F ); } finally { e._connectValidating = !1, kt(P, !1); } - if (!G.allowed) { - V("connection", "Connection rejected (async connectValidator)", { connection: D, reason: G.reason }), Ae(m, { ...D, reason: G.reason }), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; + if (!U.allowed) { + B("connection", "Connection rejected (async connectValidator)", { connection: $, reason: U.reason }), Ne(m, { ...$, reason: U.reason }), e._emit("connect-end", { connection: null, ...w }), e.pendingConnection = null; return; } } - const $ = `e-${r}-${R}-${Date.now()}-${dn++}`; - e.addEdges({ id: $, ...D }), V("connection", `Connection created: ${r} → ${R}`, D), e._emit("connect", { connection: D }), e._emit("connect-end", { connection: D, ...w }); + const N = `e-${r}-${R}-${Date.now()}-${fn++}`; + e.addEdges({ id: N, ...$ }), B("connection", `Connection created: ${r} → ${R}`, $), e._emit("connect", { connection: $ }), e._emit("connect-end", { connection: $, ...w }); } else - V("connection", "Connection rejected (invalid)", D), Ae(m, D), e._emit("connect-end", { connection: null, ...w }); + B("connection", "Connection rejected (invalid)", $), Ne(m, $), e._emit("connect-end", { connection: null, ...w }); } else e._emit("connect-end", { connection: null, ...w }); } else if (e._config?.onEdgeDrop) { - const k = { - x: v.x - we / 2, - y: v.y - _e / 2 + const M = { + x: v.x - ve / 2, + y: v.y - be / 2 }, R = e._config.onEdgeDrop({ source: r, sourceHandle: o, - position: k + position: M }); if (R) { - const O = { + const z = { source: r, sourceHandle: o, target: R.id, targetHandle: "target" }; - if (!nt(m, O, e.edges)) - V("connection", "Edge drop: connection rejected (handle limit)"), e._emit("connect-end", { connection: null, ...w }); - else if (!tt(m, O)) - V("connection", "Edge drop: connection rejected (per-handle validator)"), e._emit("connect-end", { connection: null, ...w }); - else if (!e._config.isValidConnection || e._config.isValidConnection(O)) { + if (!nt(m, z, e.edges)) + B("connection", "Edge drop: connection rejected (handle limit)"), e._emit("connect-end", { connection: null, ...w }); + else if (!tt(m, z)) + B("connection", "Edge drop: connection rejected (per-handle validator)"), e._emit("connect-end", { connection: null, ...w }); + else if (!e._config.isValidConnection || e._config.isValidConnection(z)) { e.addNodes(R); - const q = `e-${r}-${R.id}-${Date.now()}-${dn++}`; - e.addEdges({ id: q, ...O }), V("connection", `Edge drop: created node "${R.id}" and edge`, O), e._emit("connect", { connection: O }), e._emit("connect-end", { connection: O, ...w }); + const V = `e-${r}-${R.id}-${Date.now()}-${fn++}`; + e.addEdges({ id: V, ...z }), B("connection", `Edge drop: created node "${R.id}" and edge`, z), e._emit("connect", { connection: z }), e._emit("connect-end", { connection: z, ...w }); } else - V("connection", "Edge drop: connection rejected by validator"), e._emit("connect-end", { connection: null, ...w }); + B("connection", "Edge drop: connection rejected by validator"), e._emit("connect-end", { connection: null, ...w }); } else - V("connection", "Edge drop: callback returned null"), e._emit("connect-end", { connection: null, ...w }); + B("connection", "Edge drop: callback returned null"), e._emit("connect-end", { connection: null, ...w }); } else - V("connection", "Connection cancelled (no target)"), e._emit("connect-end", { connection: null, ...w }); + B("connection", "Connection cancelled (no target)"), e._emit("connect-end", { connection: null, ...w }); } finally { kt(P, !1), u?.destroy(), u = null; } e.pendingConnection = null; }; - document.addEventListener("pointermove", I), document.addEventListener("pointerup", M), document.addEventListener("pointercancel", M), ft.set(t, () => { - document.removeEventListener("pointermove", I), document.removeEventListener("pointerup", M), document.removeEventListener("pointercancel", M), h?.stop(), u?.destroy(), u = null, p?.remove(), p = null; - for (const A of b.values()) + document.addEventListener("pointermove", D), document.addEventListener("pointerup", L), document.addEventListener("pointercancel", L), ft.set(t, () => { + document.removeEventListener("pointermove", D), document.removeEventListener("pointerup", L), document.removeEventListener("pointercancel", L), h?.stop(), u?.destroy(), u = null, p?.remove(), p = null; + for (const A of C.values()) A.line.destroy(); - b.clear(), x = !1, f?.classList.remove("flow-handle-active"), ke(m), y = null, e.pendingConnection = null, e._container?.classList.remove("flow-connecting"); + C.clear(), x = !1, f?.classList.remove("flow-handle-active"), Pe(m), y = null, e.pendingConnection = null, e._container?.classList.remove("flow-connecting"); }); } -function na(t, e, n) { +function aa(t, e, n) { if (n.button !== 0) return; const o = t.dataset.flowHandleId ?? "target", r = t.closest("[x-flow-node]")?.getAttribute("data-flow-node-id") ?? null; if (!e || !r || e._animationLocked || e._config?.edgesReconnectable === !1 || e._pendingReconnection) return; const s = e.edges.filter( - (D) => D.target === r && (D.targetHandle ?? "target") === o + ($) => $.target === r && ($.targetHandle ?? "target") === o ); if (s.length === 0) return; - const a = s.find((D) => D.selected) ?? (s.length === 1 ? s[0] : null); + const a = s.find(($) => $.selected) ?? (s.length === 1 ? s[0] : null); if (!a) return; const l = a.reconnectable ?? !0; if (l === !1 || l === "source") return; @@ -4790,135 +4790,135 @@ function na(t, e, n) { if (!g) return; const m = g.querySelector( `[data-flow-node-id="${CSS.escape(a.source)}"]` - ), y = a.sourceHandle ? `[data-flow-handle-id="${CSS.escape(a.sourceHandle)}"]` : '[data-flow-handle-type="source"]', _ = m?.querySelector(y), S = g.getBoundingClientRect(), x = e._viewportLive ?? e.viewport, b = x?.zoom || 1, C = x?.x || 0, E = x?.y || 0; - let T, I; + ), y = a.sourceHandle ? `[data-flow-handle-id="${CSS.escape(a.sourceHandle)}"]` : '[data-flow-handle-type="source"]', _ = m?.querySelector(y), k = g.getBoundingClientRect(), x = e._viewportLive ?? e.viewport, C = x?.zoom || 1, E = x?.x || 0, b = x?.y || 0; + let T, D; if (_) { - const D = _.getBoundingClientRect(); - T = (D.left + D.width / 2 - S.left - C) / b, I = (D.top + D.height / 2 - S.top - E) / b; + const $ = _.getBoundingClientRect(); + T = ($.left + $.width / 2 - k.left - E) / C, D = ($.top + $.height / 2 - k.top - b) / C; } else { - const D = e.getNode(a.source); - if (!D) return; - const L = D.dimensions?.width ?? we, $ = D.dimensions?.height ?? _e; - T = D.position.x + L / 2, I = D.position.y + $; + const $ = e.getNode(a.source); + if (!$) return; + const S = $.dimensions?.width ?? ve, N = $.dimensions?.height ?? be; + T = $.position.x + S / 2, D = $.position.y + N; } - let M = null, A = null, P = null, v = c, w = d, N = null; - const k = () => { + let L = null, A = null, P = null, v = c, w = d, I = null; + const M = () => { u = !0; - const D = g.querySelector( + const $ = g.querySelector( `[data-flow-edge-id="${a.id}"]` ); - D && D.classList.add("flow-edge-reconnecting"), e._emit("reconnect-start", { edge: a, handleType: "target" }), V("reconnect", `Reconnection drag started from target handle on edge "${a.id}"`), A = Wt({ + $ && $.classList.add("flow-edge-reconnecting"), e._emit("reconnect-start", { edge: a, handleType: "target" }), B("reconnect", `Reconnection drag started from target handle on edge "${a.id}"`), A = Ut({ connectionLineType: e._config?.connectionLineType, connectionLineStyle: e._config?.connectionLineStyle, connectionLine: e._config?.connectionLine, containerEl: g - }), M = A.svg; - const L = e.screenToFlowPosition(c, d); + }), L = A.svg; + const S = e.screenToFlowPosition(c, d); A.update({ fromX: T, - fromY: I, - toX: L.x, - toY: L.y, + fromY: D, + toX: S.x, + toY: S.y, source: a.source, sourceHandle: a.sourceHandle }); - const $ = g.querySelector(".flow-viewport"); - $ && $.appendChild(M), e.pendingConnection = { + const N = g.querySelector(".flow-viewport"); + N && N.appendChild(L), e.pendingConnection = { source: a.source, sourceHandle: a.sourceHandle, - position: L + position: S }, e._pendingReconnection = { edge: a, draggedEnd: "target", - anchorPosition: { x: T, y: I }, - position: L - }, P = vo(g, e, v, w), N = Jr( + anchorPosition: { x: T, y: D }, + position: S + }, P = _o(g, e, v, w), I = oa( g, - (F, K) => e.screenToFlowPosition(F, K) - ), bn(g, a.source, a.sourceHandle ?? "source", e, a.id, N); - }, R = (D) => { - if (v = D.clientX, w = D.clientY, !u) { + (F, W) => e.screenToFlowPosition(F, W) + ), En(g, a.source, a.sourceHandle ?? "source", e, a.id, I); + }, R = ($) => { + if (v = $.clientX, w = $.clientY, !u) { Math.sqrt( - (D.clientX - c) ** 2 + (D.clientY - d) ** 2 - ) >= Qn && k(); + ($.clientX - c) ** 2 + ($.clientY - d) ** 2 + ) >= eo && M(); return; } - const L = e.screenToFlowPosition(D.clientX, D.clientY), $ = _n({ + const S = e.screenToFlowPosition($.clientX, $.clientY), N = xn({ containerEl: g, handleType: "target", excludeNodeId: a.source, - cursorFlowPos: L, + cursorFlowPos: S, connectionSnapRadius: p, getNode: (F) => e.getNode(F), - toFlowPosition: (F, K) => e.screenToFlowPosition(F, K), - index: N ?? void 0 + toFlowPosition: (F, W) => e.screenToFlowPosition(F, W), + index: I ?? void 0 }); - $.element !== h && (h?.classList.remove("flow-handle-active"), $.element?.classList.add("flow-handle-active"), h = $.element), A?.update({ + N.element !== h && (h?.classList.remove("flow-handle-active"), N.element?.classList.add("flow-handle-active"), h = N.element), A?.update({ fromX: T, - fromY: I, - toX: $.position.x, - toY: $.position.y, + fromY: D, + toX: N.position.x, + toY: N.position.y, source: a.source, sourceHandle: a.sourceHandle }), e.pendingConnection && (e.pendingConnection = { ...e.pendingConnection, - position: $.position + position: N.position }), e._pendingReconnection && (e._pendingReconnection = { ...e._pendingReconnection, - position: $.position - }), P?.updatePointer(D.clientX, D.clientY); - }, O = () => { + position: N.position + }), P?.updatePointer($.clientX, $.clientY); + }, z = () => { if (f) return; - f = !0, document.removeEventListener("pointermove", R), document.removeEventListener("pointerup", q), document.removeEventListener("pointercancel", q), P?.stop(), P = null, A?.destroy(), A = null, M = null, N = null, h?.classList.remove("flow-handle-active"), ft.delete(t); - const D = g.querySelector( + f = !0, document.removeEventListener("pointermove", R), document.removeEventListener("pointerup", V), document.removeEventListener("pointercancel", V), P?.stop(), P = null, A?.destroy(), A = null, L = null, I = null, h?.classList.remove("flow-handle-active"), ft.delete(t); + const $ = g.querySelector( `[data-flow-edge-id="${a.id}"]` ); - D && D.classList.remove("flow-edge-reconnecting"), ke(g), e.pendingConnection = null, e._pendingReconnection = null; - }, q = async (D) => { + $ && $.classList.remove("flow-edge-reconnecting"), Pe(g), e.pendingConnection = null, e._pendingReconnection = null; + }, V = async ($) => { if (!u) { - O(); + z(); return; } if (e._connectValidating) return; - let L = h; - L || (L = document.elementFromPoint(D.clientX, D.clientY)?.closest('[data-flow-handle-type="target"]')); - let $ = !1; - if (L) { - const K = L.closest("[x-flow-node]")?.dataset.flowNodeId, ne = L.dataset.flowHandleId; - if (K && e.getNode(K)?.connectable !== !1) { - const B = { + let S = h; + S || (S = document.elementFromPoint($.clientX, $.clientY)?.closest('[data-flow-handle-type="target"]')); + let N = !1; + if (S) { + const W = S.closest("[x-flow-node]")?.dataset.flowNodeId, Q = S.dataset.flowHandleId; + if (W && e.getNode(W)?.connectable !== !1) { + const G = { source: a.source, sourceHandle: a.sourceHandle, - target: K, - targetHandle: ne - }, z = { ...a }, Y = A?.svg ?? null; - kt(Y, !0); - let W; + target: W, + targetHandle: Q + }, Z = { ...a }, H = A?.svg ?? null; + kt(H, !0); + let q; try { - W = await Qr({ + q = await ia({ edge: a, - newConnection: B, + newConnection: G, canvas: e, containerEl: g, endpoint: "target" }); } finally { - kt(Y, !1); + kt(H, !1); } - W.applied ? ($ = !0, V("reconnect", `Edge "${a.id}" reconnected (target)`, B), e._emit("reconnect", { oldEdge: z, newConnection: B })) : V("reconnect", "Reconnection rejected", { connection: B, reason: W.reason }); + q.applied ? (N = !0, B("reconnect", `Edge "${a.id}" reconnected (target)`, G), e._emit("reconnect", { oldEdge: Z, newConnection: G })) : B("reconnect", "Reconnection rejected", { connection: G, reason: q.reason }); } } - $ || V("reconnect", `Edge "${a.id}" reconnection cancelled — snapping back`), e._emit("reconnect-end", { edge: a, successful: $ }), O(); + N || B("reconnect", `Edge "${a.id}" reconnection cancelled — snapping back`), e._emit("reconnect-end", { edge: a, successful: N }), z(); }; - document.addEventListener("pointermove", R), document.addEventListener("pointerup", q), document.addEventListener("pointercancel", q), ft.set(t, O); + document.addEventListener("pointermove", R), document.addEventListener("pointerup", V), document.addEventListener("pointercancel", V), ft.set(t, z); } -function Yf(t, e, n) { - t.dataset.flowHandleType === "source" ? ta(t, e, n) : na(t, e, n); +function Uf(t, e, n) { + t.dataset.flowHandleType === "source" ? ra(t, e, n) : aa(t, e, n); } -function gs(t) { +function ms(t) { return t?._config?.delegatedHandleEvents === !1; } -function Xf(t) { +function Gf(t) { t.directive( "flow-handle", (e, { value: n, modifiers: o, expression: i }, { evaluate: r, effect: s, cleanup: a }) => { @@ -4927,229 +4927,229 @@ function Xf(t) { c && u ? p = "top-left" : c && f ? p = "top-right" : d && u ? p = "bottom-left" : d && f ? p = "bottom-right" : c ? p = "top" : f ? p = "right" : d ? p = "bottom" : u ? p = "left" : p = e.getAttribute("data-flow-handle-position") ?? (l === "source" ? "bottom" : "top"); let g, m = !1; if (i) { - const b = r(i); - b && typeof b == "object" && !Array.isArray(b) ? (g = b.id || e.getAttribute("data-flow-handle-id") || l, b.position && (p = b.position, m = !0)) : g = b || e.getAttribute("data-flow-handle-id") || l; + const C = r(i); + C && typeof C == "object" && !Array.isArray(C) ? (g = C.id || e.getAttribute("data-flow-handle-id") || l, C.position && (p = C.position, m = !0)) : g = C || e.getAttribute("data-flow-handle-id") || l; } else g = e.getAttribute("data-flow-handle-id") || l; if (o.includes("hidden") && (e.style.display = "none"), e.dataset.flowHandleType = l, e.dataset.flowHandlePosition = p, e.dataset.flowHandleId = g, h && (e.dataset.flowHandleExplicit = "true"), m && i && (e.dataset.flowHandleExplicit = "true", s(() => { - const b = r(i); - b && typeof b == "object" && !Array.isArray(b) && b.position && (e.dataset.flowHandlePosition = b.position); + const C = r(i); + C && typeof C == "object" && !Array.isArray(C) && C.position && (e.dataset.flowHandlePosition = C.position); })), !h && !m) { - const b = () => { - const E = e.closest("[x-flow-node]")?.dataset.flowNodeId; - if (!E) return; + const C = () => { + const b = e.closest("[x-flow-node]")?.dataset.flowNodeId; + if (!b) return; const T = e.closest("[x-data]"); - return T ? t.$data(T)?.getNode?.(E) : void 0; + return T ? t.$data(T)?.getNode?.(b) : void 0; }; s(() => { - const C = b(); - if (!C) return; - const E = l === "source" ? C.sourcePosition : C.targetPosition; - E && (e.dataset.flowHandlePosition = E); + const E = C(); + if (!E) return; + const b = l === "source" ? E.sourcePosition : E.targetPosition; + b && (e.dataset.flowHandlePosition = b); }); } e.classList.add("flow-handle", `flow-handle-${l}`); const y = () => { - const b = e.closest("[x-flow-node]"); - return b ? b.getAttribute("data-flow-node-id") ?? null : null; + const C = e.closest("[x-flow-node]"); + return C ? C.getAttribute("data-flow-node-id") ?? null : null; }, _ = () => { - const b = e.closest("[x-data]"); - return b ? t.$data(b) : null; - }, S = _(); + const C = e.closest("[x-data]"); + return C ? t.$data(C) : null; + }, k = _(); let x = null; - if (S?._config?.keyboardConnect) { + if (k?._config?.keyboardConnect) { e.setAttribute("tabindex", "0"), e.setAttribute("role", "button"), e.setAttribute("aria-label", `${l} handle ${g}`); - const b = (T) => { - const I = T?._pendingKeyboardConnect; - if (!I) return; - const M = e.closest(".flow-container"); - M && M.querySelector( - `[data-flow-node-id="${CSS.escape(I.sourceNodeId)}"] [data-flow-handle-id="${CSS.escape(I.sourceHandleId)}"][data-flow-handle-type="source"]` + const C = (T) => { + const D = T?._pendingKeyboardConnect; + if (!D) return; + const L = e.closest(".flow-container"); + L && L.querySelector( + `[data-flow-node-id="${CSS.escape(D.sourceNodeId)}"] [data-flow-handle-id="${CSS.escape(D.sourceHandleId)}"][data-flow-handle-type="source"]` )?.classList.remove("flow-handle-connect-pending"), T && (T._pendingKeyboardConnect = null); - }, C = (T) => { + }, E = (T) => { if (!(T.key === "Enter" || T.key === " " || T.key === "Spacebar")) return; - const M = _(); - if (!M || M._animationLocked) return; + const L = _(); + if (!L || L._animationLocked) return; const A = y(); if (A) if (l === "source") { - const P = M.getNode?.(A); - if (P && !Be(P) || e[Pt] === !1) return; - T.preventDefault(), T.stopPropagation(), b(M), M._pendingKeyboardConnect = { + const P = L.getNode?.(A); + if (P && !qe(P) || e[Pt] === !1) return; + T.preventDefault(), T.stopPropagation(), C(L), L._pendingKeyboardConnect = { sourceNodeId: A, sourceHandleId: g - }, e.classList.add("flow-handle-connect-pending"), M._announcer?.announce?.(`Connecting from ${l} handle ${g}. Focus a target handle and press Enter to connect.`); + }, e.classList.add("flow-handle-connect-pending"), L._announcer?.announce?.(`Connecting from ${l} handle ${g}. Focus a target handle and press Enter to connect.`); } else { - if (!M._pendingKeyboardConnect) return; - const P = M.getNode?.(A); - if (P && !Be(P) || e[st] === !1) return; + if (!L._pendingKeyboardConnect) return; + const P = L.getNode?.(A); + if (P && !qe(P) || e[st] === !1) return; T.preventDefault(), T.stopPropagation(); - const { sourceNodeId: v, sourceHandleId: w } = M._pendingKeyboardConnect, N = { + const { sourceNodeId: v, sourceHandleId: w } = L._pendingKeyboardConnect, I = { source: v, sourceHandle: w, target: A, targetHandle: g - }, k = e.closest(".flow-container"); - if (b(M), !k) return; - ea({ connection: N, canvas: M, containerEl: k }).then((R) => { - R.applied && M._announcer?.announce?.(`Connected ${v} to ${A}.`); + }, M = e.closest(".flow-container"); + if (C(L), !M) return; + sa({ connection: I, canvas: L, containerEl: M }).then((R) => { + R.applied && L._announcer?.announce?.(`Connected ${v} to ${A}.`); }); } }; - e.addEventListener("keydown", C); - const E = e.closest(".flow-container"); - if (E) { - const T = Dn.get(E); + e.addEventListener("keydown", E); + const b = e.closest(".flow-container"); + if (b) { + const T = Rn.get(b); if (T) T.count += 1; else { - const I = (M) => { - if (M.key !== "Escape") return; - const A = E.matches("[x-data]") ? E : E.closest("[x-data]") ?? E.querySelector("[x-data]"); + const D = (L) => { + if (L.key !== "Escape") return; + const A = b.matches("[x-data]") ? b : b.closest("[x-data]") ?? b.querySelector("[x-data]"); if (!A) return; const P = t.$data(A); - P?._pendingKeyboardConnect && b(P); + P?._pendingKeyboardConnect && C(P); }; - E.addEventListener("keydown", I), Dn.set(E, { count: 1, handler: I }); + b.addEventListener("keydown", D), Rn.set(b, { count: 1, handler: D }); } } x = () => { - if (e.removeEventListener("keydown", C), E) { - const T = Dn.get(E); - T && (T.count -= 1, T.count <= 0 && (E.removeEventListener("keydown", T.handler), Dn.delete(E))); + if (e.removeEventListener("keydown", E), b) { + const T = Rn.get(b); + T && (T.count -= 1, T.count <= 0 && (b.removeEventListener("keydown", T.handler), Rn.delete(b))); } e.removeAttribute("tabindex"), e.removeAttribute("role"), e.removeAttribute("aria-label"), e.classList.remove("flow-handle-connect-pending"); }; } if (l === "source") { - const b = (T) => { - ta(e, _(), T); + const C = (T) => { + ra(e, _(), T); }; - gs(S) && e.addEventListener("pointerdown", b); - const C = () => { + ms(k) && e.addEventListener("pointerdown", C); + const E = () => { const T = _(); if (!T?._pendingReconnection || T._pendingReconnection.draggedEnd !== "source") return; - const I = y(); - if (I) { - const M = T.getNode(I); - if (M && !Be(M)) return; + const D = y(); + if (D) { + const L = T.getNode(D); + if (L && !qe(L)) return; } e[Pt] !== !1 && e.classList.add("flow-handle-active"); - }, E = () => { + }, b = () => { e.classList.remove("flow-handle-active"); }; - e.addEventListener("pointerenter", C), e.addEventListener("pointerleave", E), a(() => { - ft.get(e)?.(), ft.delete(e), x?.(), e.removeEventListener("pointerdown", b), e.removeEventListener("pointerenter", C), e.removeEventListener("pointerleave", E), e.classList.remove("flow-handle", `flow-handle-${l}`); + e.addEventListener("pointerenter", E), e.addEventListener("pointerleave", b), a(() => { + ft.get(e)?.(), ft.delete(e), x?.(), e.removeEventListener("pointerdown", C), e.removeEventListener("pointerenter", E), e.removeEventListener("pointerleave", b), e.classList.remove("flow-handle", `flow-handle-${l}`); }); } else { - const b = () => { - const I = _(); - if (!I?.pendingConnection) return; - const M = y(); - if (M) { - const A = I.getNode(M); - if (A && !Be(A)) return; + const C = () => { + const D = _(); + if (!D?.pendingConnection) return; + const L = y(); + if (L) { + const A = D.getNode(L); + if (A && !qe(A)) return; } e[st] !== !1 && e.classList.add("flow-handle-active"); - }, C = () => { + }, E = () => { e.classList.remove("flow-handle-active"); }; - e.addEventListener("pointerenter", b), e.addEventListener("pointerleave", C); - const E = async (I) => { - const M = _(); - if (!M?.pendingConnection || M._config?.connectOnClick === !1 || M._connectValidating) return; - I.preventDefault(), I.stopPropagation(); + e.addEventListener("pointerenter", C), e.addEventListener("pointerleave", E); + const b = async (D) => { + const L = _(); + if (!L?.pendingConnection || L._config?.connectOnClick === !1 || L._connectValidating) return; + D.preventDefault(), D.stopPropagation(); const A = y(); if (!A) return; if (e[st] === !1) { - V("connection", "Click-to-connect rejected (handle not connectable end)"), M._emit("connect-end", { connection: null, source: M.pendingConnection.source, sourceHandle: M.pendingConnection.sourceHandle, position: { x: 0, y: 0 } }), M.pendingConnection = null, M._container?.classList.remove("flow-connecting"); - const k = e.closest(".flow-container"); - k && ke(k); + B("connection", "Click-to-connect rejected (handle not connectable end)"), L._emit("connect-end", { connection: null, source: L.pendingConnection.source, sourceHandle: L.pendingConnection.sourceHandle, position: { x: 0, y: 0 } }), L.pendingConnection = null, L._container?.classList.remove("flow-connecting"); + const M = e.closest(".flow-container"); + M && Pe(M); return; } - const P = M.getNode(A); - if (P && !Be(P)) { - V("connection", `Click-to-connect rejected (target "${A}" not connectable)`), M._emit("connect-end", { connection: null, source: M.pendingConnection.source, sourceHandle: M.pendingConnection.sourceHandle, position: { x: 0, y: 0 } }), M.pendingConnection = null, M._container?.classList.remove("flow-connecting"); - const k = e.closest(".flow-container"); - k && ke(k); + const P = L.getNode(A); + if (P && !qe(P)) { + B("connection", `Click-to-connect rejected (target "${A}" not connectable)`), L._emit("connect-end", { connection: null, source: L.pendingConnection.source, sourceHandle: L.pendingConnection.sourceHandle, position: { x: 0, y: 0 } }), L.pendingConnection = null, L._container?.classList.remove("flow-connecting"); + const M = e.closest(".flow-container"); + M && Pe(M); return; } const v = { - source: M.pendingConnection.source, - sourceHandle: M.pendingConnection.sourceHandle, + source: L.pendingConnection.source, + sourceHandle: L.pendingConnection.sourceHandle, target: A, targetHandle: g - }, w = { source: M.pendingConnection.source, sourceHandle: M.pendingConnection.sourceHandle, position: { x: 0, y: 0 } }; - if (pt(v, M.edges, { preventCycles: M._config?.preventCycles })) { - const k = e.closest(".flow-container"); - if (!gt(v, M._config?.connectionRules, M._nodeMap)) { - V("connection", "Click-to-connect rejected (connection rules)", v), Ae(k, v), M._emit("connect-end", { connection: null, ...w }), M.pendingConnection = null, M._container?.classList.remove("flow-connecting"), k && ke(k); + }, w = { source: L.pendingConnection.source, sourceHandle: L.pendingConnection.sourceHandle, position: { x: 0, y: 0 } }; + if (pt(v, L.edges, { preventCycles: L._config?.preventCycles })) { + const M = e.closest(".flow-container"); + if (!gt(v, L._config?.connectionRules, L._nodeMap)) { + B("connection", "Click-to-connect rejected (connection rules)", v), Ne(M, v), L._emit("connect-end", { connection: null, ...w }), L.pendingConnection = null, L._container?.classList.remove("flow-connecting"), M && Pe(M); return; } - if (k && !nt(k, v, M.edges)) { - V("connection", "Click-to-connect rejected (handle limit)", v), Ae(k, v), M._emit("connect-end", { connection: null, ...w }), M.pendingConnection = null, M._container?.classList.remove("flow-connecting"), ke(k); + if (M && !nt(M, v, L.edges)) { + B("connection", "Click-to-connect rejected (handle limit)", v), Ne(M, v), L._emit("connect-end", { connection: null, ...w }), L.pendingConnection = null, L._container?.classList.remove("flow-connecting"), Pe(M); return; } - if (k && !tt(k, v)) { - V("connection", "Click-to-connect rejected (per-handle validator)", v), Ae(k, v), M._emit("connect-end", { connection: null, ...w }), M.pendingConnection = null, M._container?.classList.remove("flow-connecting"), k && ke(k); + if (M && !tt(M, v)) { + B("connection", "Click-to-connect rejected (per-handle validator)", v), Ne(M, v), L._emit("connect-end", { connection: null, ...w }), L.pendingConnection = null, L._container?.classList.remove("flow-connecting"), M && Pe(M); return; } - if (M._config?.isValidConnection && !M._config.isValidConnection(v)) { - V("connection", "Click-to-connect rejected (custom validator)", v), Ae(k, v), M._emit("connect-end", { connection: null, ...w }), M.pendingConnection = null, M._container?.classList.remove("flow-connecting"), k && ke(k); + if (L._config?.isValidConnection && !L._config.isValidConnection(v)) { + B("connection", "Click-to-connect rejected (custom validator)", v), Ne(M, v), L._emit("connect-end", { connection: null, ...w }), L.pendingConnection = null, L._container?.classList.remove("flow-connecting"), M && Pe(M); return; } - const R = M._config?.connectValidator; - if (R && k) { - const q = M._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: D, targetEl: L } = bo(k, v); - M._connectValidating = !0; - let $; + const R = L._config?.connectValidator; + if (R && M) { + const V = L._config?.validatingHandleClass ?? "flow-handle-validating", { sourceEl: $, targetEl: S } = xo(M, v); + L._connectValidating = !0; + let N; try { - $ = await _o( + N = await bo( R, v, - D, - L, - k, - q + $, + S, + M, + V ); } finally { - M._connectValidating = !1; + L._connectValidating = !1; } - if (!$.allowed) { - V("connection", "Click-to-connect rejected (async connectValidator)", { connection: v, reason: $.reason }), Ae(k, { ...v, reason: $.reason }), M._emit("connect-end", { connection: null, ...w }), M.pendingConnection = null, M._container?.classList.remove("flow-connecting"), ke(k); + if (!N.allowed) { + B("connection", "Click-to-connect rejected (async connectValidator)", { connection: v, reason: N.reason }), Ne(M, { ...v, reason: N.reason }), L._emit("connect-end", { connection: null, ...w }), L.pendingConnection = null, L._container?.classList.remove("flow-connecting"), Pe(M); return; } } - const O = `e-${v.source}-${v.target}-${Date.now()}-${dn++}`; - M.addEdges({ id: O, ...v }), V("connection", `Click-to-connect: ${v.source} → ${v.target}`, v), M._emit("connect", { connection: v }), M._emit("connect-end", { connection: v, ...w }); + const z = `e-${v.source}-${v.target}-${Date.now()}-${fn++}`; + L.addEdges({ id: z, ...v }), B("connection", `Click-to-connect: ${v.source} → ${v.target}`, v), L._emit("connect", { connection: v }), L._emit("connect-end", { connection: v, ...w }); } else { - V("connection", "Click-to-connect rejected (invalid)", v); - const k = e.closest(".flow-container"); - Ae(k, v), M._emit("connect-end", { connection: null, ...w }); + B("connection", "Click-to-connect rejected (invalid)", v); + const M = e.closest(".flow-container"); + Ne(M, v), L._emit("connect-end", { connection: null, ...w }); } - M.pendingConnection = null, M._container?.classList.remove("flow-connecting"); - const N = e.closest(".flow-container"); - N && ke(N); + L.pendingConnection = null, L._container?.classList.remove("flow-connecting"); + const I = e.closest(".flow-container"); + I && Pe(I); }; - e.addEventListener("click", E); - const T = (I) => { - na(e, _(), I); + e.addEventListener("click", b); + const T = (D) => { + aa(e, _(), D); }; - gs(S) && e.addEventListener("pointerdown", T), a(() => { - ft.get(e)?.(), ft.delete(e), x?.(), e.removeEventListener("pointerdown", T), e.removeEventListener("pointerenter", b), e.removeEventListener("pointerleave", C), e.removeEventListener("click", E), e.classList.remove("flow-handle", `flow-handle-${l}`, "flow-handle-active"); + ms(k) && e.addEventListener("pointerdown", T), a(() => { + ft.get(e)?.(), ft.delete(e), x?.(), e.removeEventListener("pointerdown", T), e.removeEventListener("pointerenter", C), e.removeEventListener("pointerleave", E), e.removeEventListener("click", b), e.classList.remove("flow-handle", `flow-handle-${l}`, "flow-handle-active"); }); } } ); } -function ps(t, e) { +function ys(t, e) { const n = (o) => { const r = o.target?.closest?.("[data-flow-handle-type]"); - r && t.contains(r) && (e?._container && r.closest(".flow-container") !== e._container || Yf(r, e, o)); + r && t.contains(r) && (e?._container && r.closest(".flow-container") !== e._container || Uf(r, e, o)); }; return t.addEventListener("pointerdown", n, !0), () => t.removeEventListener("pointerdown", n, !0); } -const ms = { +const ws = { delete: ["Delete", "Backspace"], selectionBox: "Shift", multiSelect: "Shift", @@ -5166,9 +5166,9 @@ const ms = { selectionModeToggle: "Alt", selectionToolToggle: "l" }; -function Wf(t) { - if (!t) return { ...ms }; - const e = { ...ms }; +function Zf(t) { + if (!t) return { ...ws }; + const e = { ...ws }; for (const n of Object.keys(t)) n in t && (e[n] = t[n]); return e; @@ -5178,7 +5178,7 @@ function Ge(t, e) { const n = t.length === 1 ? t.toLowerCase() : t; return Array.isArray(e) ? e.some((o) => (o.length === 1 ? o.toLowerCase() : o) === n) : (e.length === 1 ? e.toLowerCase() : e) === n; } -function jf(t, e, n, o) { +function Kf(t, e, n, o) { return !t && e > 0 && (n !== 0 || o !== 0); } function mt(t, e) { @@ -5196,7 +5196,7 @@ function mt(t, e) { return !1; } } -function Uf(t, e, n = {}) { +function Jf(t, e, n = {}) { const o = n.duration ?? 500, i = n.moveThreshold ?? 10; let r = null, s = 0, a = 0, l = null; function c() { @@ -5216,8 +5216,8 @@ function Uf(t, e, n = {}) { c(), t.removeEventListener("pointerdown", u); }; } -const ti = 20, Hn = ti + 1, oa = 1, ia = 0.5, Gf = `b${oa}d${ia}`; -function ys(t) { +const oi = 20, Hn = oi + 1, la = 1, ca = 0.5, Qf = `b${la}d${ca}`; +function vs(t) { switch (t) { case "top": return { x: 0, y: -1 }; @@ -5231,7 +5231,7 @@ function ys(t) { return { x: 0, y: 1 }; } } -function ws(t, e) { +function _s(t, e) { return { x: t.x - e, y: t.y - e, @@ -5239,10 +5239,10 @@ function ws(t, e) { height: t.height + e * 2 }; } -function Zf(t, e, n) { +function eh(t, e, n) { return t > n.x && t < n.x + n.width && e > n.y && e < n.y + n.height; } -function sa(t, e, n, o) { +function da(t, e, n, o) { const i = Math.min(t, e), r = Math.max(t, e); for (const s of o) { const a = s.x, l = s.x + s.width, c = s.y, d = s.y + s.height; @@ -5251,7 +5251,7 @@ function sa(t, e, n, o) { } return !1; } -function ra(t, e, n, o) { +function ua(t, e, n, o) { const i = Math.min(e, n), r = Math.max(e, n); for (const s of o) { const a = s.x, l = s.x + s.width, c = s.y, d = s.y + s.height; @@ -5260,7 +5260,7 @@ function ra(t, e, n, o) { } return !1; } -function Kf(t, e, n, o, i) { +function th(t, e, n, o, i) { const r = /* @__PURE__ */ new Set([t, n]), s = /* @__PURE__ */ new Set([e, o]); for (const u of i) r.add(u.x), r.add(u.x + u.width), s.add(u.y), s.add(u.y + u.height); @@ -5270,7 +5270,7 @@ function Kf(t, e, n, o, i) { for (const f of l) { let h = !1; for (const p of i) - if (Zf(u, f, p)) { + if (eh(u, f, p)) { h = !0; break; } @@ -5278,7 +5278,7 @@ function Kf(t, e, n, o, i) { } return c; } -class Jf { +class nh { constructor(e) { this.less = e, this.items = []; } @@ -5311,7 +5311,7 @@ class Jf { return n; } } -function Qf(t, e) { +function oh(t, e) { const n = t.map(() => []), o = /* @__PURE__ */ new Map(), i = /* @__PURE__ */ new Map(); for (const r of t) { let s = o.get(r.x); @@ -5323,55 +5323,55 @@ function Qf(t, e) { r.sort((s, a) => s.y - a.y); for (let s = 1; s < r.length; s++) { const a = r[s - 1], l = r[s]; - ra(a.x, a.y, l.y, e) || (n[a.index].push(l.index), n[l.index].push(a.index)); + ua(a.x, a.y, l.y, e) || (n[a.index].push(l.index), n[l.index].push(a.index)); } } for (const r of i.values()) { r.sort((s, a) => s.x - a.x); for (let s = 1; s < r.length; s++) { const a = r[s - 1], l = r[s]; - sa(a.x, l.x, a.y, e) || (n[a.index].push(l.index), n[l.index].push(a.index)); + da(a.x, l.x, a.y, e) || (n[a.index].push(l.index), n[l.index].push(a.index)); } } return n; } -function eh(t, e, n, o) { - const i = n.length, r = 2 * i, s = new Float64Array(r).fill(1 / 0), a = new Float64Array(r).fill(1 / 0), l = new Int32Array(r).fill(-1), c = new Uint8Array(r), d = Qf(n, o), u = Math.min(t.x, e.x), f = Math.max(t.x, e.x), h = Math.min(t.y, e.y), p = Math.max(t.y, e.y), g = (E) => Math.max(0, u - E.x) + Math.max(0, E.x - f) + Math.max(0, h - E.y) + Math.max(0, E.y - p), m = (E, T) => s[E] < s[T] || s[E] === s[T] && a[E] < a[T], y = new Jf(m); - for (let E = 0; E < 2; E++) { - const T = E * i + t.index; +function ih(t, e, n, o) { + const i = n.length, r = 2 * i, s = new Float64Array(r).fill(1 / 0), a = new Float64Array(r).fill(1 / 0), l = new Int32Array(r).fill(-1), c = new Uint8Array(r), d = oh(n, o), u = Math.min(t.x, e.x), f = Math.max(t.x, e.x), h = Math.min(t.y, e.y), p = Math.max(t.y, e.y), g = (b) => Math.max(0, u - b.x) + Math.max(0, b.x - f) + Math.max(0, h - b.y) + Math.max(0, b.y - p), m = (b, T) => s[b] < s[T] || s[b] === s[T] && a[b] < a[T], y = new nh(m); + for (let b = 0; b < 2; b++) { + const T = b * i + t.index; s[T] = 0, a[T] = 0, y.push(T); } - const _ = (E) => E % i, S = (E) => E < i ? 0 : 1; + const _ = (b) => b % i, k = (b) => b < i ? 0 : 1; let x = -1; for (; y.size > 0; ) { - const E = y.pop(); - if (c[E]) continue; - c[E] = 1; - const T = _(E); + const b = y.pop(); + if (c[b]) continue; + c[b] = 1; + const T = _(b); if (T === e.index) { - x = E; + x = b; break; } - const I = S(E), M = n[T]; + const D = k(b), L = n[T]; for (const A of d[T]) { - const P = n[A], v = M.x === P.x ? 1 : 0, w = v * i + A; + const P = n[A], v = L.x === P.x ? 1 : 0, w = v * i + A; if (c[w]) continue; - const N = Math.abs(P.x - M.x) + Math.abs(P.y - M.y), R = oa * (I === v ? 0 : 1) + ia * g(P), O = s[E] + N, q = a[E] + R; - (O < s[w] || O === s[w] && q < a[w]) && (s[w] = O, a[w] = q, l[w] = E, y.push(w)); + const I = Math.abs(P.x - L.x) + Math.abs(P.y - L.y), R = la * (D === v ? 0 : 1) + ca * g(P), z = s[b] + I, V = a[b] + R; + (z < s[w] || z === s[w] && V < a[w]) && (s[w] = z, a[w] = V, l[w] = b, y.push(w)); } } if (x === -1) { - const E = e.index, T = i + e.index; - if (s[E] === 1 / 0 && s[T] === 1 / 0) return null; - x = m(E, T) ? E : T; + const b = e.index, T = i + e.index; + if (s[b] === 1 / 0 && s[T] === 1 / 0) return null; + x = m(b, T) ? b : T; } - const b = []; - let C = x; - for (; C !== -1; ) - b.unshift(n[_(C)]), C = l[C]; - return b; + const C = []; + let E = x; + for (; E !== -1; ) + C.unshift(n[_(E)]), E = l[E]; + return C; } -function th(t) { +function sh(t) { if (t.length <= 2) return t; const e = [t[0]]; for (let n = 1; n < t.length - 1; n++) { @@ -5380,17 +5380,17 @@ function th(t) { } return e.push(t[t.length - 1]), e; } -function nh(t, e) { +function rh(t, e) { if (t.length < 2) return ""; let n = `M${t[0].x},${t[0].y}`; for (let i = 1; i < t.length - 1; i++) { const r = t[i - 1], s = t[i], a = t[i + 1]; - e > 0 ? n += ` ${Xt(r.x, r.y, s.x, s.y, a.x, a.y, e)}` : n += ` L${s.x},${s.y}`; + e > 0 ? n += ` ${jt(r.x, r.y, s.x, s.y, a.x, a.y, e)}` : n += ` L${s.x},${s.y}`; } const o = t[t.length - 1]; return n += ` L${o.x},${o.y}`, n; } -function oh(t) { +function ah(t) { if (t.length < 2) return { x: t[0]?.x ?? 0, y: t[0]?.y ?? 0, offsetX: 0, offsetY: 0 }; let e = 0; @@ -5416,35 +5416,35 @@ function oh(t) { return { x: i.x, y: i.y, offsetX: 0, offsetY: 0 }; } const ht = 200; -function ih(t, e, n, o, i) { +function lh(t, e, n, o, i) { const r = Math.min(t, n) - ht, s = Math.max(t, n) + ht, a = Math.min(e, o) - ht, l = Math.max(e, o) + ht; return i.filter( (c) => c.x < s && c.x + c.width > r && c.y < l && c.y + c.height > a ); } -function sh(t, e) { +function ch(t, e) { for (let n = 1; n < t.length; n++) { const o = t[n - 1], i = t[n]; if (o.x === i.x) { - if (ra(o.x, o.y, i.y, e)) return !0; - } else if (o.y === i.y && sa(o.x, i.x, o.y, e)) + if (ua(o.x, o.y, i.y, e)) return !0; + } else if (o.y === i.y && da(o.x, i.x, o.y, e)) return !0; } return !1; } -function rh(t, e, n, o, i, r, s) { - const a = ys(n), l = ys(r), c = t + a.x * Hn, d = e + a.y * Hn, u = o + l.x * Hn, f = i + l.y * Hn, h = (_) => { - const S = _.map((I) => ws(I, ti)), x = Kf(c, d, u, f, S); +function dh(t, e, n, o, i, r, s) { + const a = vs(n), l = vs(r), c = t + a.x * Hn, d = e + a.y * Hn, u = o + l.x * Hn, f = i + l.y * Hn, h = (_) => { + const k = _.map((D) => _s(D, oi)), x = th(c, d, u, f, k); x.length; - const b = x.find((I) => I.x === c && I.y === d), C = x.find((I) => I.x === u && I.y === f); - b || x.push({ x: c, y: d, index: x.length }), C || x.push({ x: u, y: f, index: x.length }); - const E = b ?? x[x.length - (C ? 1 : 2)], T = C ?? x[x.length - 1]; - return eh(E, T, x, S); - }, p = ih(t, e, o, i, s), g = p.length < s.length; + const C = x.find((D) => D.x === c && D.y === d), E = x.find((D) => D.x === u && D.y === f); + C || x.push({ x: c, y: d, index: x.length }), E || x.push({ x: u, y: f, index: x.length }); + const b = C ?? x[x.length - (E ? 1 : 2)], T = E ?? x[x.length - 1]; + return ih(b, T, x, k); + }, p = lh(t, e, o, i, s), g = p.length < s.length; let m = h(p); if (g) { - const _ = s.map((x) => ws(x, ti)); - (!(m !== null && m.length >= 2) || sh(m, _)) && (m = h(s)); + const _ = s.map((x) => _s(x, oi)); + (!(m !== null && m.length >= 2) || ch(m, _)) && (m = h(s)); } if (!m || m.length < 2) return null; const y = [ @@ -5452,25 +5452,25 @@ function rh(t, e, n, o, i, r, s) { ...m, { x: o, y: i, index: -2 } ]; - return th(y); + return sh(y); } -const ah = 512, lt = /* @__PURE__ */ new Map(); -function lh(t, e, n, o, i, r, s) { - let a = `${Gf}|${Math.round(t)},${Math.round(e)},${n}|${Math.round(o)},${Math.round(i)},${r}`; +const uh = 512, lt = /* @__PURE__ */ new Map(); +function fh(t, e, n, o, i, r, s) { + let a = `${Qf}|${Math.round(t)},${Math.round(e)},${n}|${Math.round(o)},${Math.round(i)},${r}`; for (const l of s) a += `|${Math.round(l.x)},${Math.round(l.y)},${Math.round(l.width)},${Math.round(l.height)}`; return a; } -function aa(t, e, n, o, i, r, s) { - const a = lh(t, e, n, o, i, r, s); +function fa(t, e, n, o, i, r, s) { + const a = fh(t, e, n, o, i, r, s); if (lt.has(a)) { const c = lt.get(a); return lt.delete(a), lt.set(a, c), c; } - const l = rh(t, e, n, o, i, r, s); - return lt.set(a, l), lt.size > ah && lt.delete(lt.keys().next().value), l; + const l = dh(t, e, n, o, i, r, s); + return lt.set(a, l), lt.size > uh && lt.delete(lt.keys().next().value), l; } -function ch({ +function hh({ sourceX: t, sourceY: e, sourcePosition: n = "bottom", @@ -5481,7 +5481,7 @@ function ch({ borderRadius: a = 5 }) { if (!s || s.length === 0) - return vn({ + return bn({ sourceX: t, sourceY: e, sourcePosition: n, @@ -5490,9 +5490,9 @@ function ch({ targetPosition: r, borderRadius: a }); - const l = aa(t, e, n, o, i, r, s); + const l = fa(t, e, n, o, i, r, s); if (!l) - return vn({ + return bn({ sourceX: t, sourceY: e, sourcePosition: n, @@ -5501,7 +5501,7 @@ function ch({ targetPosition: r, borderRadius: a }); - const c = nh(l, a), { x: d, y: u, offsetX: f, offsetY: h } = oh(l); + const c = rh(l, a), { x: d, y: u, offsetX: f, offsetY: h } = ah(l); return { path: c, labelPosition: { x: d, y: u }, @@ -5509,23 +5509,23 @@ function ch({ labelOffsetY: h }; } -const vs = 5; -function io(t) { - return t ? t === !0 ? vs : t.spacing ?? vs : null; +const bs = 5; +function so(t) { + return t ? t === !0 ? bs : t.spacing ?? bs : null; } -function _s(t, e, n, o) { +function xs(t, e, n, o) { if (e <= 1) return 0; const r = Math.min((e - 1) * o, Math.max(0, n)) / (e - 1); return (t - (e - 1) / 2) * r; } -function bs(t, e, n) { +function Es(t, e, n) { return e === "left" || e === "right" ? { x: t.x, y: t.y + n } : { x: t.x + n, y: t.y }; } -const xs = 20; -function la(t) { +const Cs = 20; +function ha(t) { return new Map(t.map((e) => [e.id, e])); } -function dh(t, e) { +function gh(t, e) { const n = /* @__PURE__ */ new Map(); for (const o of e) if (o.parentId) { @@ -5539,7 +5539,7 @@ function dh(t, e) { (!r || r.length !== i.length || r.some((s, a) => s !== i[a])) && t.set(o, i); } } -function ni(t, e, n) { +function ii(t, e, n) { if (!t.position) return { x: 0, y: 0 }; let o = t.position.x, i = t.position.y; const r = /* @__PURE__ */ new Set(); @@ -5547,7 +5547,7 @@ function ni(t, e, n) { let s = t.parentId ? e.get(t.parentId) : void 0; for (; s && !r.has(s.id); ) { r.add(s.id); - const a = s.nodeOrigin ?? n ?? [0, 0], l = s.dimensions?.width ?? we, c = s.dimensions?.height ?? _e; + const a = s.nodeOrigin ?? n ?? [0, 0], l = s.dimensions?.width ?? ve, c = s.dimensions?.height ?? be; o += s.position.x - l * a[0], i += s.position.y - c * a[1], s = s.parentId ? e.get(s.parentId) : void 0; } return { x: o, y: i }; @@ -5555,10 +5555,10 @@ function ni(t, e, n) { function yt(t, e, n) { if (!t.parentId) return t; - const o = ni(t, e, n); + const o = ii(t, e, n); return { ...t, position: o }; } -function so(t, e, n) { +function ro(t, e, n) { return t.map((o) => yt(o, e, n)); } function wt(t, e) { @@ -5577,7 +5577,7 @@ function wt(t, e) { return n; } function Mt(t) { - const e = la(t), n = [], o = /* @__PURE__ */ new Set(); + const e = ha(t), n = [], o = /* @__PURE__ */ new Set(); function i(r) { if (!o.has(r.id)) { if (r.parentId) { @@ -5591,37 +5591,37 @@ function Mt(t) { i(r); return n; } -function ca(t, e, n = /* @__PURE__ */ new Set()) { +function ga(t, e, n = /* @__PURE__ */ new Set()) { if (n.has(t.id)) return t.zIndex ?? 2; if (n.add(t.id), !t.parentId) return t.zIndex !== void 0 ? t.zIndex : t.type === "group" ? 0 : 2; const o = e.get(t.parentId); - return o ? ca(o, e, n) + 2 + (t.zIndex ?? 0) : (t.zIndex ?? 0) + 2; + return o ? ga(o, e, n) + 2 + (t.zIndex ?? 0) : (t.zIndex ?? 0) + 2; } -function da(t, e, n) { +function pa(t, e, n) { return { x: Math.max(e[0][0], Math.min(t.x, e[1][0] - (n?.width ?? 0))), y: Math.max(e[0][1], Math.min(t.y, e[1][1] - (n?.height ?? 0))) }; } -function Po(t, e, n) { +function To(t, e, n) { return { x: Math.max(0, Math.min(t.x, n.width - e.width)), y: Math.max(0, Math.min(t.y, n.height - e.height)) }; } -function Rn(t, e, n) { +function Fn(t, e, n) { const o = e.extent ?? n; if (!o || o === "parent" || e.parentId) return t; - const i = e.dimensions ?? { width: we, height: _e }; - return da(t, o, i); + const i = e.dimensions ?? { width: ve, height: be }; + return pa(t, o, i); } -function uh(t, e, n) { - const o = t.x + e.width + xs, i = t.y + e.height + xs, r = Math.max(n.width, o), s = Math.max(n.height, i); +function ph(t, e, n) { + const o = t.x + e.width + Cs, i = t.y + e.height + Cs, r = Math.max(n.width, o), s = Math.max(n.height, i); return r === n.width && s === n.height ? null : { width: r, height: s }; } -function Es(t, e, n) { +function Ss(t, e, n) { switch (n) { case "top": return { x: t / 2, y: 0 }; @@ -5641,7 +5641,7 @@ function Es(t, e, n) { return { x: t, y: e }; } } -function fh(t, e, n) { +function mh(t, e, n) { const o = t / 2, i = e / 2, r = t / 2, s = e / 2; switch (n) { case "top": @@ -5670,7 +5670,7 @@ function fh(t, e, n) { } } } -function hh(t, e, n) { +function yh(t, e, n) { switch (n) { case "top": return { x: t / 2, y: 0 }; @@ -5690,7 +5690,7 @@ function hh(t, e, n) { return { x: t * 0.25, y: e * 0.75 }; } } -function gh(t, e, n) { +function wh(t, e, n) { switch (n) { case "top": return { x: t / 2, y: 0 }; @@ -5710,7 +5710,7 @@ function gh(t, e, n) { return { x: t * 0.25, y: e }; } } -function ph(t, e, n) { +function vh(t, e, n) { const o = t * 0.15; switch (n) { case "top": @@ -5731,7 +5731,7 @@ function ph(t, e, n) { return { x: 0, y: e }; } } -function mh(t, e, n) { +function _h(t, e, n) { switch (n) { case "top": return { x: t / 2, y: 0 }; @@ -5751,7 +5751,7 @@ function mh(t, e, n) { return { x: 0, y: e }; } } -function yh(t, e, n) { +function bh(t, e, n) { const o = e * 0.12; switch (n) { case "top": @@ -5772,7 +5772,7 @@ function yh(t, e, n) { return { x: 0, y: e - o }; } } -function wh(t, e, n) { +function xh(t, e, n) { const o = Math.min(t, e) / 2, i = t / 2, r = e / 2; switch (n) { case "top": @@ -5801,16 +5801,16 @@ function wh(t, e, n) { } } } -const ua = { - circle: { perimeterPoint: fh }, - diamond: { perimeterPoint: hh }, - hexagon: { perimeterPoint: gh }, - parallelogram: { perimeterPoint: ph }, - triangle: { perimeterPoint: mh }, - cylinder: { perimeterPoint: yh }, - stadium: { perimeterPoint: wh } +const ma = { + circle: { perimeterPoint: mh }, + diamond: { perimeterPoint: yh }, + hexagon: { perimeterPoint: wh }, + parallelogram: { perimeterPoint: vh }, + triangle: { perimeterPoint: _h }, + cylinder: { perimeterPoint: bh }, + stadium: { perimeterPoint: xh } }; -function fa(t, e = "light") { +function ya(t, e = "light") { let n = e === "dark" ? "dark" : "light", o = null, i = null; function r(a) { n = a ? "dark" : "light", t.classList.toggle("dark", a); @@ -5828,18 +5828,18 @@ function fa(t, e = "light") { } }; } -const Mo = "__alpineflow_collab_store__"; -function vh() { - return typeof globalThis < "u" ? (globalThis[Mo] || (globalThis[Mo] = /* @__PURE__ */ new WeakMap()), globalThis[Mo]) : /* @__PURE__ */ new WeakMap(); +const No = "__alpineflow_collab_store__"; +function Eh() { + return typeof globalThis < "u" ? (globalThis[No] || (globalThis[No] = /* @__PURE__ */ new WeakMap()), globalThis[No]) : /* @__PURE__ */ new WeakMap(); } -const He = vh(), To = "__alpineflow_registry__"; -function ha() { - return typeof globalThis < "u" ? (globalThis[To] || (globalThis[To] = /* @__PURE__ */ new Map()), globalThis[To]) : /* @__PURE__ */ new Map(); +const He = Eh(), Ao = "__alpineflow_registry__"; +function wa() { + return typeof globalThis < "u" ? (globalThis[Ao] || (globalThis[Ao] = /* @__PURE__ */ new Map()), globalThis[Ao]) : /* @__PURE__ */ new Map(); } -function Dt(t) { - return ha().get(t); +function Ht(t) { + return wa().get(t); } -function _h(t, e) { +function Ch(t, e) { switch (t) { case "nodes-change": { const n = e.nodes ?? [], o = n.length === 1 ? n[0].data?.label || n[0].id : null; @@ -5870,17 +5870,17 @@ function _h(t, e) { return null; } } -const bh = 1e3; -class xh { +const Sh = 1e3; +class kh { constructor(e, n) { - this._clearTimer = null, this._formatMessage = n ?? _h, this._el = document.createElement("div"), this._el.setAttribute("aria-live", "polite"), this._el.setAttribute("aria-atomic", "true"), this._el.setAttribute("role", "status"); + this._clearTimer = null, this._formatMessage = n ?? Ch, this._el = document.createElement("div"), this._el.setAttribute("aria-live", "polite"), this._el.setAttribute("aria-atomic", "true"), this._el.setAttribute("role", "status"); const o = this._el.style; o.position = "absolute", o.width = "1px", o.height = "1px", o.padding = "0", o.margin = "-1px", o.overflow = "hidden", o.clip = "rect(0,0,0,0)", o.whiteSpace = "nowrap", o.border = "0", e.appendChild(this._el); } announce(e) { this._clearTimer && clearTimeout(this._clearTimer), this._el.textContent = e, this._clearTimer = setTimeout(() => { this._el.textContent = "", this._clearTimer = null; - }, bh); + }, Sh); } handleEvent(e, n) { const o = this._formatMessage(e, n); @@ -5890,7 +5890,7 @@ class xh { this._clearTimer && clearTimeout(this._clearTimer), this._el.remove(); } } -class Eh { +class Lh { constructor() { this._registry = /* @__PURE__ */ new Map(); } @@ -5980,7 +5980,7 @@ class Eh { return i.delete(e), i; } } -const Ch = { +const Ph = { connect: (t) => [t.connection?.source ?? t.source, t.connection?.target ?? t.target, t.connection?.sourceHandle ?? t.sourceHandle, t.connection?.targetHandle ?? t.targetHandle], "connect-start": (t) => [t.source, t.sourceHandle], "connect-end": (t) => [t.connection, t.source, t.sourceHandle, t.position], @@ -6010,7 +6010,7 @@ const Ch = { "row-select": (t) => [t.rowId, t.nodeId, t.attrId], "row-deselect": (t) => [t.rowId, t.nodeId, t.attrId], "row-selection-change": (t) => [t.selectedRows] -}, Sh = { +}, Mh = { "flow:addNodes": "addNodes", "flow:removeNodes": "removeNodes", "flow:addEdges": "addEdges", @@ -6052,7 +6052,7 @@ const Ch = { // RunState (D2) "flow:setNodeState": "setNodeState", "flow:resetStates": "resetStates" -}, kh = { +}, Th = { "flow:addNodes": (t) => [t.nodes], "flow:removeNodes": (t) => [t.ids], "flow:addEdges": (t) => [t.edges], @@ -6094,13 +6094,13 @@ const Ch = { // RunState (D2) "flow:setNodeState": (t) => [t.ids, t.state], "flow:resetStates": () => [] -}, Cs = { +}, ks = { success: { borderColor: "#22c55e", shadow: "0 0 0 2px rgba(34,197,94,0.3)" }, error: { borderColor: "#ef4444", shadow: "0 0 0 2px rgba(239,68,68,0.3)" }, warning: { borderColor: "#f59e0b", shadow: "0 0 0 2px rgba(245,158,11,0.3)" }, info: { borderColor: "#3b82f6", shadow: "0 0 0 2px rgba(59,130,246,0.3)" } }; -function Lh(t, e) { +function Nh(t, e) { const n = []; return n.push(e.on("flow:moveNode", (o) => { const i = o.duration ?? 0; @@ -6131,7 +6131,7 @@ function Lh(t, e) { })), n.push(e.on("flow:highlightNode", (o) => { const i = t.getNode(o.id); if (!i) return; - const r = Cs[o.style] ?? Cs.info, s = o.duration ?? 1500, a = Math.floor(s * 0.6), l = Math.floor(s * 0.4), c = i.style?.borderColor ?? null, d = i.style?.boxShadow ?? null; + const r = ks[o.style] ?? ks.info, s = o.duration ?? 1500, a = Math.floor(s * 0.6), l = Math.floor(s * 0.4), c = i.style?.borderColor ?? null, d = i.style?.boxShadow ?? null; t.update({ nodes: { [o.id]: { style: `border-color: ${r.borderColor}; box-shadow: ${r.shadow}` } } }, { duration: 100 }), setTimeout(() => { @@ -6191,13 +6191,13 @@ function Lh(t, e) { typeof o == "function" && o(); }; } -function Ph(t) { +function Ah(t) { return "on" + t.split("-").map( (e) => e.charAt(0).toUpperCase() + e.slice(1) ).join(""); } -const Mh = /* @__PURE__ */ new Set(["viewport-change", "viewport-move"]), Th = 150; -function Ah(t, e) { +const $h = /* @__PURE__ */ new Set(["viewport-change", "viewport-move"]), Ih = 150; +function Dh(t, e) { let n = null, o = null; return ((...i) => { o = i, n === null && (n = setTimeout(() => { @@ -6207,24 +6207,24 @@ function Ah(t, e) { }, e)); }); } -function Nh(t, e, n) { +function Rh(t, e, n) { for (const [o, i] of Object.entries(n)) { - const r = Ph(o), s = t[r], a = (l) => { + const r = Ah(o), s = t[r], a = (l) => { let c; typeof s == "function" && (c = s(l)); - const d = Ch[o], u = d ? d(l) : [l], f = e[i]; + const d = Ph[o], u = d ? d(l) : [l], f = e[i]; return typeof f == "function" && f.call(e, ...u), c; }; - t[r] = Mh.has(o) ? Ah(a, Th) : a; + t[r] = $h.has(o) ? Dh(a, Ih) : a; } } -function $h(t, e) { +function Hh(t, e) { const n = []; - for (const [o, i] of Object.entries(Sh)) { + for (const [o, i] of Object.entries(Mh)) { const r = e.on(o, (s) => { const a = t[i]; if (typeof a != "function") return; - const l = kh[o], c = l ? l(s) : Object.values(s); + const l = Th[o], c = l ? l(s) : Object.values(s); a.call(t, ...c); }); n.push(r); @@ -6234,8 +6234,8 @@ function $h(t, e) { typeof o == "function" && o(); }; } -const Ih = 5; -function Dh(t) { +const Fh = 5; +function Oh(t) { const e = /* @__PURE__ */ new Set(), n = /* @__PURE__ */ new Map(), o = /* @__PURE__ */ new Set(); let i = null, r = /* @__PURE__ */ new Set(), s = 0; const a = /* @__PURE__ */ new Set(); @@ -6244,7 +6244,7 @@ function Dh(t) { i = null; for (const c of e) { const u = (r.has(c) ? n.get(c) ?? 0 : 0) + 1; - n.set(c, u), u > Ih && !o.has(c) && (o.add(c), console.warn( + n.set(c, u), u > Fh && !o.has(c) && (o.add(c), console.warn( `[alpineflow] Auto-layout for parent "${c}" has run for ${u} consecutive frames. Suppressing to avoid an infinite loop. This usually indicates a layout that keeps changing child dimensions by more than the 1px threshold. Next user mutation will clear the suppression.` )); } @@ -6281,7 +6281,7 @@ function Dh(t) { } }; } -function Hh(t) { +function zh(t) { return function(n) { t.suspend(); try { @@ -6291,16 +6291,16 @@ function Hh(t) { } }; } -function Rh(t, e, n) { +function Vh(t, e, n) { let { width: o, height: i } = t; return e?.width !== void 0 && (o = Math.max(o, e.width)), e?.height !== void 0 && (i = Math.max(i, e.height)), n?.width !== void 0 && (o = Math.min(o, n.width)), n?.height !== void 0 && (i = Math.min(i, n.height)), { width: o, height: i }; } -function un(t, e) { +function hn(t, e) { const n = t.type ?? "default", o = e[n], i = t.data?.childValidation; if (!(!o && !i)) return o ? i ? { ...o, ...i } : o : i; } -function ga(t, e, n, o) { +function va(t, e, n, o) { if (!o) return { valid: !0 }; if (o.maxChildren !== void 0 && n.length >= o.maxChildren) return { @@ -6339,7 +6339,7 @@ function ga(t, e, n, o) { } return { valid: !0 }; } -function ro(t, e, n, o) { +function ao(t, e, n, o) { if (!o) return { valid: !0 }; if (o.preventChildEscape) return { @@ -6370,7 +6370,7 @@ function ro(t, e, n, o) { } return { valid: !0 }; } -function Ss(t, e, n) { +function Ls(t, e, n) { if (!n) return []; const o = [], i = Math.max( n.minChildren ?? 0, @@ -6385,32 +6385,32 @@ function Ss(t, e, n) { } return o; } -function jt(t, e) { - const n = Gt(t, e); +function Gt(t, e) { + const n = Kt(t, e); return { x: n.x, y: n.y, - width: t.dimensions?.width ?? we, - height: t.dimensions?.height ?? _e + width: t.dimensions?.width ?? ve, + height: t.dimensions?.height ?? be }; } -function pa(t, e) { +function _a(t, e) { return t.x < e.x + e.width && t.x + t.width > e.x && t.y < e.y + e.height && t.y + t.height > e.y; } -function Fh(t, e, n = !0) { - const o = jt(t); +function Bh(t, e, n = !0) { + const o = Gt(t); return e.filter((i) => { if (i.id === t.id) return !1; - const r = jt(i); - return n ? pa(o, r) : o.x <= r.x && o.y <= r.y && o.x + o.width >= r.x + r.width && o.y + o.height >= r.y + r.height; + const r = Gt(i); + return n ? _a(o, r) : o.x <= r.x && o.y <= r.y && o.x + o.width >= r.x + r.width && o.y + o.height >= r.y + r.height; }); } -function Oh(t, e, n = !0) { +function qh(t, e, n = !0) { if (t.id === e.id) return !1; - const o = jt(t), i = jt(e); - return n ? pa(o, i) : o.x <= i.x && o.y <= i.y && o.x + o.width >= i.x + i.width && o.y + o.height >= i.y + i.height; + const o = Gt(t), i = Gt(e); + return n ? _a(o, i) : o.x <= i.x && o.y <= i.y && o.x + o.width >= i.x + i.width && o.y + o.height >= i.y + i.height; } -function zh(t, e, n, o, i = 5) { +function Xh(t, e, n, o, i = 5) { let { x: r, y: s } = t; for (const a of o) { const l = r + e, c = s + n, d = a.x + a.width, u = a.y + a.height; @@ -6421,7 +6421,7 @@ function zh(t, e, n, o, i = 5) { } return { x: r, y: s }; } -function Vh(t) { +function Yh(t) { return { /** * Add one or more nodes to the canvas. @@ -6439,7 +6439,7 @@ function Vh(t) { addNodes(e, n) { t._captureHistory(); let o = Array.isArray(e) ? e : [e]; - V("init", `Adding ${o.length} node(s)`, o.map((d) => d.id)); + B("init", `Adding ${o.length} node(s)`, o.map((d) => d.id)); const i = /* @__PURE__ */ new Map(); if (n?.center) { for (const d of o) @@ -6460,7 +6460,7 @@ function Vh(t) { ...r.filter( (g) => g.parentId === d.parentId ) - ], p = ga(f, d, h, u); + ], p = va(f, d, h, u); if (!p.valid) { t._config.onChildValidationFail && t._config.onChildValidationFail({ parent: f, @@ -6559,7 +6559,7 @@ function Vh(t) { if (!p) continue; const g = t.nodes.filter( (y) => y.parentId === f.parentId - ), m = ro(p, f, g, h); + ), m = ao(p, f, g, h); m.valid || (o.add(u), t._config.onChildValidationFail && t._config.onChildValidationFail({ parent: p, child: f, @@ -6579,12 +6579,12 @@ function Vh(t) { for (const u of [...n]) for (const f of wt(u, t.nodes)) n.add(f); - V("destroy", `Removing ${n.size} node(s)`, [...n]); + B("destroy", `Removing ${n.size} node(s)`, [...n]); const r = t.nodes.filter((u) => n.has(u.id)); let s = []; - t._config.reconnectOnDelete && (s = $f(n, t.nodes, t.edges)); + t._config.reconnectOnDelete && (s = Hf(n, t.nodes, t.edges)); const a = []; - t.edges = t.edges.filter((u) => n.has(u.source) || n.has(u.target) ? (a.push(u.id), !1) : !0), s.length && (t.edges.push(...s), V("destroy", `Created ${s.length} reconnection edge(s)`)), t._rebuildEdgeMap(), t.nodes = t.nodes.filter((u) => !n.has(u.id)), t._rebuildNodeMap(); + t.edges = t.edges.filter((u) => n.has(u.source) || n.has(u.target) ? (a.push(u.id), !1) : !0), s.length && (t.edges.push(...s), B("destroy", `Created ${s.length} reconnection edge(s)`)), t._rebuildEdgeMap(), t.nodes = t.nodes.filter((u) => !n.has(u.id)), t._rebuildNodeMap(); for (const u of n) t.selectedNodes.delete(u), t._initialDimensions.delete(u), t._uninstallChildLayoutWatchers(u), t._draggingNodeIds?.delete(u); for (const u of a) @@ -6627,26 +6627,26 @@ function Vh(t) { * Get all nodes connected via outgoing edges from the given node. */ getOutgoers(e) { - return Qo(e, t.nodes, t.edges); + return ti(e, t.nodes, t.edges); }, /** * Get all nodes connected via incoming edges to the given node. */ getIncomers(e) { - return Tf(e, t.nodes, t.edges); + return If(e, t.nodes, t.edges); }, /** * Get all edges connected to a node (both incoming and outgoing). */ getConnectedEdges(e) { - return Mf(e, t.edges); + return $f(e, t.edges); }, /** * Check if two nodes are connected by an edge. * When `directed` is true, only checks source→target direction. */ areNodesConnected(e, n, o = !1) { - return Nf(e, n, t.edges, o); + return Rf(e, n, t.edges, o); }, /** * Apply a node-level filter predicate. @@ -6658,7 +6658,7 @@ function Vh(t) { const r = !e(i); i.filtered = r, r ? n.push(i) : o.push(i); } - V("filter", `Node filter applied: ${o.length} visible, ${n.length} filtered`), t._emit("node-filter-change", { filtered: n, visible: o }); + B("filter", `Node filter applied: ${o.length} visible, ${n.length} filtered`), t._emit("node-filter-change", { filtered: n, visible: o }); }, /** * Clear node filter — restore all nodes to visible. @@ -6667,7 +6667,7 @@ function Vh(t) { let e = !1; for (const n of t.nodes) n.filtered && (n.filtered = !1, e = !0); - e && (V("filter", "Node filter cleared"), t._emit("node-filter-change", { filtered: [], visible: [...t.nodes] })); + e && (B("filter", "Node filter cleared"), t._emit("node-filter-change", { filtered: [], visible: [...t.nodes] })); }, /** * Get nodes whose bounding rect overlaps the given node. @@ -6675,7 +6675,7 @@ function Vh(t) { */ getIntersectingNodes(e, n) { const o = typeof e == "string" ? t.nodes.find((i) => i.id === e) : e; - return o ? Fh(o, t.nodes, n) : []; + return o ? Bh(o, t.nodes, n) : []; }, /** * Check if two nodes' bounding rects overlap. @@ -6683,7 +6683,7 @@ function Vh(t) { */ isNodeIntersecting(e, n, o) { const i = typeof e == "string" ? t.nodes.find((s) => s.id === e) : e, r = typeof n == "string" ? t.nodes.find((s) => s.id === n) : n; - return !i || !r ? !1 : Oh(i, r, o); + return !i || !r ? !1 : qh(i, r, o); }, /** * Set runState on one or more nodes by ID. @@ -6705,7 +6705,7 @@ function Vh(t) { } }; } -function Bh(t) { +function Wh(t) { return { /** * Add one or more edges to the canvas. @@ -6723,7 +6723,7 @@ function Bh(t) { return gt(l, o, t._nodeMap); }); if (i.length === 0) return; - t._captureHistory(), V("edge", `Adding ${i.length} edge(s)`, i.map((a) => a.id)), t.edges.push(...i), t._rebuildEdgeMap(); + t._captureHistory(), B("edge", `Adding ${i.length} edge(s)`, i.map((a) => a.id)), t.edges.push(...i), t._rebuildEdgeMap(); const r = t._computeEndpointGrouping(); r.size > 0 && t._markEdgesDirtyById(r), t._emit("edges-change", { type: "add", edges: i }); const s = t._container ? He.get(t._container) : void 0; @@ -6744,7 +6744,7 @@ function Bh(t) { removeEdges(e) { t._captureHistory(); const n = new Set(Array.isArray(e) ? e : [e]); - V("edge", `Removing ${n.size} edge(s)`, [...n]); + B("edge", `Removing ${n.size} edge(s)`, [...n]); const o = t.edges.filter((s) => n.has(s.id)); t.edges = t.edges.filter((s) => !n.has(s.id)), t._rebuildEdgeMap(); for (const s of n) @@ -6795,7 +6795,7 @@ function Bh(t) { } }; } -function qh(t) { +function jh(t) { return { // ── Coordinate Transforms ───────────────────────────────────────────── /** @@ -6805,7 +6805,7 @@ function qh(t) { screenToFlowPosition(e, n) { if (!t._container) return { x: e, y: n }; const o = t._container.getBoundingClientRect(); - return Hr(e, n, t._viewportLive ?? t.viewport, o); + return Vr(e, n, t._viewportLive ?? t.viewport, o); }, /** * Convert flow coordinates to screen coordinates, accounting for the @@ -6814,7 +6814,7 @@ function qh(t) { flowToScreenPosition(e, n) { if (!t._container) return { x: e, y: n }; const o = t._container.getBoundingClientRect(); - return Gu(e, n, t._viewportLive ?? t.viewport, o); + return Qu(e, n, t._viewportLive ?? t.viewport, o); }, // ── Fit & Bounds ────────────────────────────────────────────────────── /** @@ -6828,7 +6828,7 @@ function qh(t) { n < 10 && requestAnimationFrame(() => this.fitView(e, n + 1)); return; } - const o = t.nodes.filter((r) => !r.hidden), i = Yt(so(o, t._nodeMap, t._config.nodeOrigin), t._config.nodeOrigin); + const o = t.nodes.filter((r) => !r.hidden), i = Wt(ro(o, t._nodeMap, t._config.nodeOrigin), t._config.nodeOrigin); this.fitBounds(i, e), t._announcer?.handleEvent("fit-view", {}); }, /** @@ -6839,15 +6839,15 @@ function qh(t) { * is set directly via `ctx._panZoom`. */ fitBounds(e, n) { - const o = t._container ? { width: t._container.clientWidth, height: t._container.clientHeight } : { width: 800, height: 600 }, i = Jn( + const o = t._container ? { width: t._container.clientWidth, height: t._container.clientHeight } : { width: 800, height: 600 }, i = Qn( e, o.width, o.height, t._config.minZoom ?? 0.5, t._config.maxZoom ?? 2, - n?.padding ?? Ko + n?.padding ?? Qo ); - V("viewport", "fitBounds", { rect: e, viewport: i }); + B("viewport", "fitBounds", { rect: e, viewport: i }); const r = n?.duration ?? 0; r > 0 ? t.animate?.( { viewport: { pan: { x: i.x, y: i.y }, zoom: i.zoom } }, @@ -6860,7 +6860,7 @@ function qh(t) { */ getNodesBounds(e) { let n; - return e ? n = e.map((o) => t.getNode(o)).filter((o) => !!o) : n = t.nodes.filter((o) => !o.hidden), Yt(so(n, t._nodeMap, t._config.nodeOrigin), t._config.nodeOrigin); + return e ? n = e.map((o) => t.getNode(o)).filter((o) => !!o) : n = t.nodes.filter((o) => !o.hidden), Wt(ro(n, t._nodeMap, t._config.nodeOrigin), t._config.nodeOrigin); }, /** * Compute the viewport (pan + zoom) that frames the given bounds @@ -6868,13 +6868,13 @@ function qh(t) { */ getViewportForBounds(e, n) { const o = t._container; - return o ? Jn( + return o ? Qn( e, o.clientWidth, o.clientHeight, t._config.minZoom ?? 0.5, t._config.maxZoom ?? 2, - n ?? Ko + n ?? Qo ) : { x: 0, y: 0, zoom: 1 }; }, // ── Viewport Mutation ───────────────────────────────────────────────── @@ -6882,21 +6882,21 @@ function qh(t) { * Set the viewport programmatically (pan and/or zoom). */ setViewport(e, n) { - V("viewport", "setViewport", e), t._panZoom?.setViewport(e, n); + B("viewport", "setViewport", e), t._panZoom?.setViewport(e, n); }, /** * Zoom in by `ZOOM_STEP_FACTOR`, clamped to `maxZoom`. */ zoomIn(e) { - const n = t._viewportLive ?? t.viewport, o = t._config.maxZoom ?? 2, i = Math.min(n.zoom * Qi, o); - V("viewport", "zoomIn", { from: n.zoom, to: i }), t._panZoom?.setViewport({ ...n, zoom: i }, e); + const n = t._viewportLive ?? t.viewport, o = t._config.maxZoom ?? 2, i = Math.min(n.zoom * ts, o); + B("viewport", "zoomIn", { from: n.zoom, to: i }), t._panZoom?.setViewport({ ...n, zoom: i }, e); }, /** * Zoom out by `ZOOM_STEP_FACTOR`, clamped to `minZoom`. */ zoomOut(e) { - const n = t._viewportLive ?? t.viewport, o = t._config.minZoom ?? 0.5, i = Math.max(n.zoom / Qi, o); - V("viewport", "zoomOut", { from: n.zoom, to: i }), t._panZoom?.setViewport({ ...n, zoom: i }, e); + const n = t._viewportLive ?? t.viewport, o = t._config.minZoom ?? 0.5, i = Math.max(n.zoom / ts, o); + B("viewport", "zoomOut", { from: n.zoom, to: i }), t._panZoom?.setViewport({ ...n, zoom: i }, e); }, /** * Center the viewport on flow coordinate `(x, y)` at the given zoom @@ -6906,14 +6906,14 @@ function qh(t) { const r = t._container; if (!r) return; const s = o ?? (t._viewportLive ?? t.viewport).zoom, a = r.clientWidth / 2 - e * s, l = r.clientHeight / 2 - n * s; - V("viewport", "setCenter", { x: e, y: n, zoom: s }), t._panZoom?.setViewport({ x: a, y: l, zoom: s }, i); + B("viewport", "setCenter", { x: e, y: n, zoom: s }), t._panZoom?.setViewport({ x: a, y: l, zoom: s }, i); }, /** * Pan the viewport by a delta `(dx, dy)`. */ panBy(e, n, o) { const i = t._viewportLive ?? t.viewport; - V("viewport", "panBy", { dx: e, dy: n }), t._panZoom?.setViewport( + B("viewport", "panBy", { dx: e, dy: n }), t._panZoom?.setViewport( { x: i.x + e, y: i.y + n, zoom: i.zoom }, o ); @@ -6923,7 +6923,7 @@ function qh(t) { * Toggle pan/zoom interactivity on and off. */ toggleInteractive() { - t.isInteractive = !t.isInteractive, V("interactive", "toggleInteractive", { isInteractive: t.isInteractive }), t._panZoom?.update({ + t.isInteractive = !t.isInteractive, B("interactive", "toggleInteractive", { isInteractive: t.isInteractive }), t._panZoom?.update({ pannable: t.isInteractive, zoomable: t.isInteractive }); @@ -6951,54 +6951,54 @@ function qh(t) { * on the container and emitting a `panel-reset` event. */ resetPanels() { - V("panel", "resetPanels"), t._container?.dispatchEvent(new CustomEvent("flow-panel-reset")), t._emit("panel-reset"); + B("panel", "resetPanels"), t._container?.dispatchEvent(new CustomEvent("flow-panel-reset")), t._emit("panel-reset"); } }; } let Et = null; -const Yh = 20; -function oi(t) { +const Uh = 20; +function si(t) { return JSON.parse(JSON.stringify(t)); } -function ks(t) { +function Ps(t) { return `${t}-copy-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; } -function ma(t, e) { +function ba(t, e) { const n = t.filter((r) => r.selected), o = new Set(n.map((r) => r.id)), i = e.filter( (r) => r.selected || o.has(r.source) && o.has(r.target) ); return Et = { - nodes: oi(n), - edges: oi(i), + nodes: si(n), + edges: si(i), pasteCount: 0 }, { nodeCount: n.length, edgeCount: i.length }; } -function Xh() { +function Gh() { if (!Et || Et.nodes.length === 0) return null; Et.pasteCount++; - const t = Et.pasteCount * Yh, e = /* @__PURE__ */ new Map(), n = Et.nodes.map((i) => { - const r = ks(i.id); + const t = Et.pasteCount * Uh, e = /* @__PURE__ */ new Map(), n = Et.nodes.map((i) => { + const r = Ps(i.id); return e.set(i.id, r), { ...i, id: r, - data: oi(i.data), + data: si(i.data), position: { x: i.position.x + t, y: i.position.y + t }, selected: !0 }; }), o = Et.edges.map((i) => ({ ...i, - id: ks(i.id), + id: Ps(i.id), source: e.get(i.source), target: e.get(i.target), selected: !0 })); return { nodes: n, edges: o }; } -function Wh(t, e) { - const n = ma(t, e); +function Zh(t, e) { + const n = ba(t, e); return { nodeIds: t.filter((i) => i.selected).map((i) => i.id), ...n }; } -function jh(t) { +function Kh(t) { return { // ── Deselect ───────────────────────────────────────────────────────── /** @@ -7012,7 +7012,7 @@ function jh(t) { */ deselectAll() { if (!(t.selectedNodes.size === 0 && t.selectedEdges.size === 0 && t.selectedRows.size === 0)) { - V("selection", "Deselecting all"); + B("selection", "Deselecting all"); for (const e of t.selectedNodes) { const n = t.getNode(e); n && (n.selected = !1); @@ -7040,7 +7040,7 @@ function jh(t) { async _deleteSelected() { const e = [...t.selectedNodes].filter((l) => { const c = t.getNode(l); - return c ? Rf(c) : !1; + return c ? Vf(c) : !1; }), n = [...t.selectedEdges].filter((l) => t.getEdge(l)?.deletable !== !1); let o = e.map((l) => t.getNode(l)).filter(Boolean); const i = new Set(e), r = t.edges.filter( @@ -7060,7 +7060,7 @@ function jh(t) { if (!d) return !0; const u = t.nodes.filter( (h) => h.parentId === l.parentId - ), f = ro(d, l, u, c); + ), f = ao(d, l, u, c); return !f.valid && t._config.onChildValidationFail && t._config.onChildValidationFail({ parent: d, child: l, @@ -7075,14 +7075,14 @@ function jh(t) { edges: a }); if (l === !1) { - V("delete", "onBeforeDelete cancelled deletion"); + B("delete", "onBeforeDelete cancelled deletion"); return; } t._captureHistory(), t._suspendHistory(); try { - if (l.nodes.length > 0 && (V("delete", `onBeforeDelete approved ${l.nodes.length} node(s)`), t.removeNodes(l.nodes.map((c) => c.id))), l.edges.length > 0) { + if (l.nodes.length > 0 && (B("delete", `onBeforeDelete approved ${l.nodes.length} node(s)`), t.removeNodes(l.nodes.map((c) => c.id))), l.edges.length > 0) { const c = l.edges.map((d) => d.id).filter((d) => t.edges.some((u) => u.id === d)); - c.length > 0 && (V("delete", `onBeforeDelete approved ${c.length} edge(s)`), t.removeEdges(c)); + c.length > 0 && (B("delete", `onBeforeDelete approved ${c.length} edge(s)`), t.removeEdges(c)); } t._recomputeChildValidation(); for (const c of t.selectedNodes) @@ -7096,11 +7096,11 @@ function jh(t) { } t._captureHistory(), t._suspendHistory(); try { - if (o.length > 0 && (V("delete", `Deleting ${o.length} selected node(s)`), t.removeNodes(o.map((l) => l.id))), n.length > 0) { + if (o.length > 0 && (B("delete", `Deleting ${o.length} selected node(s)`), t.removeNodes(o.map((l) => l.id))), n.length > 0) { const l = n.filter( (c) => t.edges.some((d) => d.id === c) ); - l.length > 0 && (V("delete", `Deleting ${l.length} selected edge(s)`), t.removeEdges(l)); + l.length > 0 && (B("delete", `Deleting ${l.length} selected edge(s)`), t.removeEdges(l)); } t._recomputeChildValidation(); for (const l of t.selectedNodes) @@ -7118,8 +7118,8 @@ function jh(t) { * module-level clipboard. Emits a `copy` event. */ copy() { - const e = ma(t.nodes, t.edges); - e.nodeCount > 0 && (V("clipboard", `Copied ${e.nodeCount} node(s) and ${e.edgeCount} edge(s)`), t._emit("copy", e)); + const e = ba(t.nodes, t.edges); + e.nodeCount > 0 && (B("clipboard", `Copied ${e.nodeCount} node(s) and ${e.edgeCount} edge(s)`), t._emit("copy", e)); }, /** * Paste nodes/edges from the clipboard with new IDs and an @@ -7132,14 +7132,14 @@ function jh(t) { * after Alpine renders the new DOM elements. */ paste() { - const e = Xh(); + const e = Gh(); if (e) { t._captureHistory(), t.deselectAll(), t.nodes.push(...e.nodes), t.nodes = Mt(t.nodes), t._rebuildNodeMap(), t.edges.push(...e.edges), t._rebuildEdgeMap(); for (const n of e.nodes) t.selectedNodes.add(n.id); for (const n of e.edges) t.selectedEdges.add(n.id); - t._emitSelectionChange(), t._emit("nodes-change", { type: "add", nodes: e.nodes }), t._emit("edges-change", { type: "add", edges: e.edges }), t._emit("paste", { nodes: e.nodes, edges: e.edges }), V("clipboard", `Pasted ${e.nodes.length} node(s) and ${e.edges.length} edge(s)`), t.$nextTick(() => { + t._emitSelectionChange(), t._emit("nodes-change", { type: "add", nodes: e.nodes }), t._emit("edges-change", { type: "add", edges: e.edges }), t._emit("paste", { nodes: e.nodes, edges: e.edges }), B("clipboard", `Pasted ${e.nodes.length} node(s) and ${e.edges.length} edge(s)`), t.$nextTick(() => { for (const n of e.nodes) t._container?.querySelector(`[data-flow-node-id="${CSS.escape(n.id)}"]`)?.classList.add("flow-node-selected"); for (const n of e.edges) @@ -7153,15 +7153,15 @@ function jh(t) { */ async cut() { if (t.selectedNodes.size === 0) return; - const e = Wh(t.nodes, t.edges); - e.nodeCount !== 0 && (await t._deleteSelected(), t._emit("cut", { nodeCount: e.nodeCount, edgeCount: e.edgeCount }), V("clipboard", `Cut ${e.nodeCount} node(s)`)); + const e = Zh(t.nodes, t.edges); + e.nodeCount !== 0 && (await t._deleteSelected(), t._emit("cut", { nodeCount: e.nodeCount, edgeCount: e.edgeCount }), B("clipboard", `Cut ${e.nodeCount} node(s)`)); } }; } -function Uh(t, e) { +function Jh(t, e) { return t === e ? !0 : t === void 0 || e === void 0 ? t === e : JSON.stringify(t) === JSON.stringify(e); } -function ao(t, e, n = {}) { +function lo(t, e, n = {}) { const o = n.deleteMissing ?? !0, i = new Map(t.map((s) => [s.id, s])), r = []; for (const s of e) { const a = i.get(s.id); @@ -7173,15 +7173,15 @@ function ao(t, e, n = {}) { for (const l of Object.keys(a)) l !== "id" && !(l in s) && delete a[l]; for (const [l, c] of Object.entries(s)) - l === "id" || l === "__proto__" || l === "constructor" || l === "prototype" || Uh(a[l], c) || (a[l] = c); + l === "id" || l === "__proto__" || l === "constructor" || l === "prototype" || Jh(a[l], c) || (a[l] = c); r.push(a); } return r; } -function Ls(t, e, n) { - const o = ao(t.nodes, Mt(e.nodes)); +function Ms(t, e, n) { + const o = lo(t.nodes, Mt(e.nodes)); t.nodes.splice(0, t.nodes.length, ...o); - const i = ao(t.edges, e.edges); + const i = lo(t.edges, e.edges); t.edges.splice(0, t.edges.length, ...i), t._rebuildNodeMap(), t._rebuildEdgeMap(), t.deselectAll(); for (const r of t.nodes) r.selected && (r.selected = !1); @@ -7189,9 +7189,9 @@ function Ls(t, e, n) { r.selected && (r.selected = !1); t._emit("restore", { nodes: t.nodes, edges: t.edges, source: n }), requestAnimationFrame(() => { t._layoutAnimTick++, t._commitNodeGeometry?.(); - }), V("history", `${n} applied`, { nodes: e.nodes.length, edges: e.edges.length }); + }), B("history", `${n} applied`, { nodes: e.nodes.length, edges: e.edges.length }); } -function Gh(t) { +function Qh(t) { return { // ── Save / Restore ──────────────────────────────────────────── /** @@ -7216,18 +7216,18 @@ function Gh(t) { * - Deselects all, emits `restore`, and schedules auto-layout. */ fromObject(e) { - if (V("store", "fromObject: restoring state", { + if (B("store", "fromObject: restoring state", { nodes: e.nodes?.length ?? 0, edges: e.edges?.length ?? 0, viewport: !!e.viewport }), e.nodes) { const n = Mt( JSON.parse(JSON.stringify(e.nodes)) - ), o = ao(t.nodes, n); + ), o = lo(t.nodes, n); t.nodes.splice(0, t.nodes.length, ...o); } if (e.edges) { - const n = JSON.parse(JSON.stringify(e.edges)), o = ao(t.edges, n); + const n = JSON.parse(JSON.stringify(e.edges)), o = lo(t.edges, n); t.edges.splice(0, t.edges.length, ...o); } if (t._rebuildNodeMap(), t._rebuildEdgeMap(), e.viewport) { @@ -7242,7 +7242,7 @@ function Gh(t) { * Reset the canvas to its initial configuration state. */ $reset() { - V("store", "$reset: restoring initial config"), this.fromObject({ + B("store", "$reset: restoring initial config"), this.fromObject({ nodes: t._config.nodes ?? [], edges: t._config.edges ?? [], viewport: t._config.viewport ?? { x: 0, y: 0, zoom: 1 } @@ -7252,7 +7252,7 @@ function Gh(t) { * Clear all nodes and edges, resetting the viewport to origin. */ $clear() { - V("store", "$clear: emptying canvas"), this.fromObject({ + B("store", "$clear: emptying canvas"), this.fromObject({ nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } @@ -7266,7 +7266,7 @@ function Gh(t) { undo() { if (!t._history) return; const e = t._history.undo({ nodes: t.nodes, edges: t.edges }); - e && Ls(t, e, "undo"); + e && Ms(t, e, "undo"); }, /** * Redo the last undone change by popping a snapshot from the @@ -7275,7 +7275,7 @@ function Gh(t) { redo() { if (!t._history) return; const e = t._history.redo({ nodes: t.nodes, edges: t.edges }); - e && Ls(t, e, "redo"); + e && Ms(t, e, "redo"); }, /** * Whether an undo operation is available. @@ -7291,35 +7291,35 @@ function Gh(t) { } }; } -function Zh(t, e) { +function eg(t, e) { return t * (1 - e); } -function Kh(t, e) { +function tg(t, e) { return t * e; } -function Jh(t, e) { +function ng(t, e) { return e === "in" ? t : 1 - t; } -function Qh(t, e, n) { +function og(t, e, n) { const o = t.getTotalLength(); t.style.strokeDasharray = String(o); - const i = n === "in" ? Zh(o, e) : Kh(o, e); + const i = n === "in" ? eg(o, e) : tg(o, e); t.style.strokeDashoffset = String(i), (n === "in" && e < 1 || n === "out") && (t.style.setProperty("marker-start", "none"), t.style.setProperty("marker-end", "none")); } -function eg(t) { +function ig(t) { t.style.removeProperty("stroke-dasharray"), t.style.removeProperty("stroke-dashoffset"), t.style.removeProperty("marker-start"), t.style.removeProperty("marker-end"); } -function tg(t, e, n) { - t.style.opacity = String(Jh(e, n)); +function sg(t, e, n) { + t.style.opacity = String(ng(e, n)); } -function ng(t) { +function rg(t) { t.style.removeProperty("opacity"); } -const ot = Math.PI * 2, on = /* @__PURE__ */ new Map(), og = 64; -function Ci(t) { +const ot = Math.PI * 2, rn = /* @__PURE__ */ new Map(), ag = 64; +function ki(t) { if (typeof document > "u" || typeof document.createElementNS != "function") return null; - const e = on.get(t); + const e = rn.get(t); if (e) return e; const n = document.createElementNS("http://www.w3.org/2000/svg", "path"); n.setAttribute("d", t); @@ -7327,28 +7327,28 @@ function Ci(t) { const s = n.getPointAtLength(r * o); return { x: s.x, y: s.y }; }; - if (on.size >= og) { - const r = on.keys().next().value; - r !== void 0 && on.delete(r); + if (rn.size >= ag) { + const r = rn.keys().next().value; + r !== void 0 && rn.delete(r); } - return on.set(t, i), i; + return rn.set(t, i), i; } -function Hy(t) { +function Zy(t) { const { cx: e, cy: n, offset: o = 0, clockwise: i = !0 } = t, r = t.rx ?? t.radius ?? 100, s = t.ry ?? t.radius ?? 100, a = i ? 1 : -1; return (l) => ({ x: e + r * Math.cos(ot * l * a + o * ot), y: n + s * Math.sin(ot * l * a + o * ot) }); } -function Ry(t) { +function Ky(t) { const { startX: e, startY: n, endX: o, endY: i, amplitude: r = 30, frequency: s = 1, offset: a = 0 } = t, l = o - e, c = i - n, d = Math.sqrt(l * l + c * c), u = d > 0 ? l / d : 1, h = -(d > 0 ? c / d : 0), p = u; return (g) => { const m = e + l * g, y = n + c * g, _ = r * Math.sin(ot * s * g + a * ot); return { x: m + h * _, y: y + p * _ }; }; } -function Fy(t, e) { - const n = Ci(t); +function Jy(t, e) { + const n = ki(t); if (!n) return null; const { reverse: o = !1, startAt: i = 0, endAt: r = 1 } = e ?? {}, s = r - i; return (a) => { @@ -7356,7 +7356,7 @@ function Fy(t, e) { return o && (l = r - a * s), n(l); }; } -function Oy(t) { +function Qy(t) { const { cx: e, cy: n, radius: o, angle: i = 60, offset: r = 0 } = t, s = i * Math.PI / 180; return (a) => { const l = s * Math.sin(ot * a + r * ot); @@ -7366,39 +7366,39 @@ function Oy(t) { }; }; } -function zy(t) { +function ew(t) { const { originX: e, originY: n, range: o = 20, speed: i = 1, seed: r = 0 } = t, s = 1 + r % 7 * 0.3, a = 1.3 + r % 11 * 0.2, l = 0.7 + r % 13 * 0.25, c = 1.1 + r % 17 * 0.15; return (d) => { const u = d * i * ot, f = (Math.sin(s * u) + Math.sin(a * u * 1.3)) / 2, h = (Math.sin(l * u * 0.9) + Math.sin(c * u * 1.1)) / 2; return { x: e + f * o, y: n + h * o }; }; } -function Vy(t, e) { +function tw(t, e) { const n = e?.from ?? 0; return (o, i) => n + o * t; } -let Ps = !1; -function ve(t) { +let Ts = !1; +function _e(t) { try { return structuredClone(t); } catch { - return Ps || (Ps = !0, typeof console < "u" && console.warn( + return Ts || (Ts = !0, typeof console < "u" && console.warn( "[AlpineFlow] Cloning fell back to JSON roundtrip because structuredClone could not clone the input (likely a reactive proxy or an object with functions). Non-JSON values (functions, Symbols, Dates) will be stripped. This warning fires once per session." )), JSON.parse(JSON.stringify(t)); } } -function ig(t) { +function lg(t) { return { position: { ...t.position }, class: t.class, style: typeof t.style == "string" ? t.style : t.style ? { ...t.style } : void 0, - data: ve(t.data), + data: _e(t.data), dimensions: t.dimensions ? { ...t.dimensions } : void 0, selected: t.selected, zIndex: t.zIndex }; } -function sg(t) { +function cg(t) { return { animated: t.animated, color: t.color, @@ -7407,12 +7407,12 @@ function sg(t) { strokeWidth: t.strokeWidth }; } -function rg(t, e) { - t.position.x = e.position.x, t.position.y = e.position.y, t.class = e.class, t.style = e.style, t.data = ve(e.data), t.dimensions = e.dimensions ? { ...e.dimensions } : t.dimensions, t.selected = e.selected, t.zIndex = e.zIndex; +function dg(t, e) { + t.position.x = e.position.x, t.position.y = e.position.y, t.class = e.class, t.style = e.style, t.data = _e(e.data), t.dimensions = e.dimensions ? { ...e.dimensions } : t.dimensions, t.selected = e.selected, t.zIndex = e.zIndex; } -class Si { +class Li { constructor(e, n) { - this._entries = [], this._state = "idle", this._reversed = !1, this._loopCount = -1, this._lockEnabled = !1, this._locked = !1, this._respectReducedMotion = void 0, this._listeners = /* @__PURE__ */ new Map(), this._context = {}, this._activeHandles = [], this._subTimelines = [], this._initialSnapshot = /* @__PURE__ */ new Map(), this._initialEdgeSnapshot = /* @__PURE__ */ new Map(), this._playResolve = null, this._pauseWaiters = /* @__PURE__ */ new Set(), this._canvas = e, this._engine = n ?? new zr(); + this._entries = [], this._state = "idle", this._reversed = !1, this._loopCount = -1, this._lockEnabled = !1, this._locked = !1, this._respectReducedMotion = void 0, this._listeners = /* @__PURE__ */ new Map(), this._context = {}, this._activeHandles = [], this._subTimelines = [], this._initialSnapshot = /* @__PURE__ */ new Map(), this._initialEdgeSnapshot = /* @__PURE__ */ new Map(), this._playResolve = null, this._pauseWaiters = /* @__PURE__ */ new Set(), this._canvas = e, this._engine = n ?? new Yr(); } // ── Public API ─────────────────────────────────────────────────────── get state() { @@ -7441,7 +7441,7 @@ class Si { * Returns the sub-timeline for individual targeting. */ timeline(e, n) { - const o = new Si(this._canvas, this._engine); + const o = new Li(this._canvas, this._engine); return this._tag && !n?.independent && o.setTag(this._tag), e(o), this._entries.push({ type: "step", config: { timeline: o, independent: n?.independent } @@ -7512,7 +7512,7 @@ class Si { } /** Check if reduced motion is active (OS preference + not opted out). */ _isReducedMotion() { - return Vr(this._respectReducedMotion); + return Wr(this._respectReducedMotion); } // ── Internal: event emission ──────────────────────────────────────── _emit(e, n) { @@ -7545,21 +7545,21 @@ class Si { for (const n of e.nodes) if (!this._initialSnapshot.has(n)) { const o = this._canvas.getNode(n); - o && this._initialSnapshot.set(n, ig(o)); + o && this._initialSnapshot.set(n, lg(o)); } } if (e.edges) { for (const n of e.edges) if (!this._initialEdgeSnapshot.has(n)) { const o = this._canvas.getEdge(n); - o && this._initialEdgeSnapshot.set(n, sg(o)); + o && this._initialEdgeSnapshot.set(n, cg(o)); } } } _restoreInitialSnapshot() { for (const [e, n] of this._initialSnapshot) { const o = this._canvas.getNode(e); - o && rg(o, n); + o && dg(o, n); } } // ── Internal: handle management ───────────────────────────────────── @@ -7644,7 +7644,7 @@ class Si { } // ── Internal: single step execution ───────────────────────────────── async _executeStep(e, n) { - const o = this._isReducedMotion(), i = o ? 0 : e.duration ?? 300, r = o ? 0 : e.delay ?? 0, s = to(e.easing), a = this._makeContext(n, e.id); + const o = this._isReducedMotion(), i = o ? 0 : e.duration ?? 300, r = o ? 0 : e.delay ?? 0, s = no(e.easing), a = this._makeContext(n, e.id); if (e.when && !e.when(a)) { if (e.else) return this._executeStep(e.else, n); @@ -7655,8 +7655,8 @@ class Si { const T = e.timeline; if (this._tag && !e.independent && T.setTag(this._tag), e.independent || this._subTimelines.push(T), this._emit("step", { index: n, id: e.id, timeline: T }), e.onStart?.(a), await T.play(), this._state === "stopped") return; if (e.onComplete?.(a), this._emit("step-complete", { timeline: T }), !e.independent) { - const I = this._subTimelines.indexOf(T); - I >= 0 && this._subTimelines.splice(I, 1); + const D = this._subTimelines.indexOf(T); + D >= 0 && this._subTimelines.splice(D, 1); } return; } @@ -7674,7 +7674,7 @@ class Si { const p = this._resolveFollowPath(e), g = this._createGuidePath(e), m = !!(e.viewport || e.fitView || e.panTo); let y = null, _ = null; m && this._canvas.viewport && (y = { ...this._canvas.viewport }, _ = this._resolveTargetViewport(e)); - const S = e.edgeTransition ?? "none", x = e.addEdges?.map((T) => T.id) ?? [], b = e.removeEdges?.filter((T) => this._canvas.getEdge(T)).slice() ?? [], C = { + const k = e.edgeTransition ?? "none", x = e.addEdges?.map((T) => T.id) ?? [], C = e.removeEdges?.filter((T) => this._canvas.getEdge(T)).slice() ?? [], E = { step: e, ctx: a, duration: i, @@ -7690,14 +7690,14 @@ class Si { edgeFromColor: h, viewportFrom: y, viewportTarget: _, - transition: S, + transition: k, addEdgeIds: x, - removeEdgeIds: b + removeEdgeIds: C }; if (i === 0) - return this._executeInstantStep(C); - const E = this._prepareAnimatedEdges(e, S, x); - return E && await E, p ? this._executeFollowPathStep(C) : this._executeAnimatedStep(C); + return this._executeInstantStep(E); + const b = this._prepareAnimatedEdges(e, k, x); + return b && await b, p ? this._executeFollowPathStep(E) : this._executeAnimatedStep(E); } // ── Step decomposition: target validation ────────────────────────── /** Filter node/edge IDs to only those present on the canvas; warn in debug mode. */ @@ -7746,7 +7746,7 @@ class Si { if (n) for (const r of n) { const s = this._canvas.getNode(r); - s && (s.dimensions && e.dimensions && o.set(r, { ...s.dimensions }), e.style && s.style && i.set(r, wn(s.style))); + s && (s.dimensions && e.dimensions && o.set(r, { ...s.dimensions }), e.style && s.style && i.set(r, _n(s.style))); } } /** Capture initial edge strokeWidth and color for interpolation. */ @@ -7763,7 +7763,7 @@ class Si { if (!e.followPath) return null; if (typeof e.followPath == "function") return e.followPath; - const n = Ci(e.followPath); + const n = ki(e.followPath); return !n && this._canvas.debug && console.warn("[AlpineFlow] SVG path resolution unavailable (no DOM), followPath string ignored"), n; } /** Create a visible SVG guide path overlay for string-based followPath. */ @@ -7830,17 +7830,17 @@ class Si { addEdgeIds: m, removeEdgeIds: y, guidePathEl: _ - } = e, S = e.resolvedPathFn; + } = e, k = e.resolvedPathFn; return new Promise((x) => { - const b = this._engine.register((C) => { + const C = this._engine.register((E) => { if (this._state === "stopped") return x(), !0; - const E = Math.min(C / i, 1), T = s(E); + const b = Math.min(E / i, 1), T = s(b); if (a) { - const I = S(T); - for (const M of a) { - const A = this._canvas.getNode(M); - A && (A.position.x = I.x, A.position.y = I.y); + const D = k(T); + for (const L of a) { + const A = this._canvas.getNode(L); + A && (A.position.x = D.x, A.position.y = D.y); } } return this._interpolateFollowPathTick( @@ -7854,9 +7854,9 @@ class Si { f, h, p - ), this._tickEdgeTransitions(g, m, y, T), n.onProgress?.(E, o), E >= 1 ? (this._cleanupEdgeTransitions(g, m, y), y.length && this._removeEdges(y), this._applyStepInstant(n), _ && n.guidePath?.autoRemove !== !1 && _.remove(), n.onProgress?.(1, o), n.onComplete?.(o), this._emit("step-complete"), x(), !0) : !1; + ), this._tickEdgeTransitions(g, m, y, T), n.onProgress?.(b, o), b >= 1 ? (this._cleanupEdgeTransitions(g, m, y), y.length && this._removeEdges(y), this._applyStepInstant(n), _ && n.guidePath?.autoRemove !== !1 && _.remove(), n.onProgress?.(1, o), n.onComplete?.(o), this._emit("step-complete"), x(), !0) : !1; }, r); - this._activeHandles.push(b); + this._activeHandles.push(C); }); } /** Per-tick interpolation for properties during followPath animation. */ @@ -7867,10 +7867,10 @@ class Si { !f || !h || !f.dimensions || (e.dimensions.width !== void 0 && (f.dimensions.width = it(h.width, e.dimensions.width, n)), e.dimensions.height !== void 0 && (f.fixedDimensions = !0, f.dimensions.height = it(h.height, e.dimensions.height, n))); } if (o && e.style) { - const u = wn(e.style); + const u = _n(e.style); for (const f of o) { const h = this._canvas.getNode(f), p = s.get(f); - h && p && (h.style = Br(p, u, n)); + h && p && (h.style = jr(p, u, n)); } } if (i && e.edgeStrokeWidth !== void 0) @@ -7881,10 +7881,10 @@ class Si { if (i && e.edgeColor !== void 0) for (const u of i) { const f = this._canvas.getEdge(u), h = l.get(u); - f && (h !== void 0 && typeof h == "string" ? f.color = _i(h, e.edgeColor, n) : f.color = e.edgeColor); + f && (h !== void 0 && typeof h == "string" ? f.color = xi(h, e.edgeColor, n) : f.color = e.edgeColor); } if (c && d && this._canvas.viewport) { - const u = lf(c, d, n, { + const u = ff(c, d, n, { minZoom: this._canvas.minZoom, maxZoom: this._canvas.maxZoom }); @@ -8017,28 +8017,28 @@ class Si { _applyEdgeDrawTransition(e, n, o) { for (const i of e) { const r = this._canvas.getEdgePathElement?.(i); - r && Qh(r, n, o); + r && og(r, n, o); } } /** Clean up draw transition styles. */ _cleanupEdgeDrawTransition(e) { for (const n of e) { const o = this._canvas.getEdgePathElement?.(n); - o && eg(o); + o && ig(o); } } /** Apply fade transition on each tick for added/removed edges. */ _applyEdgeFadeTransition(e, n, o) { for (const i of e) { const r = this._canvas.getEdgeElement?.(i); - r && tg(r, n, o); + r && sg(r, n, o); } } /** Clean up fade transition styles. */ _cleanupEdgeFadeTransition(e) { for (const n of e) { const o = this._canvas.getEdgeElement?.(n); - o && ng(o); + o && rg(o); } } // ── Internal: viewport helpers ────────────────────────────────── @@ -8057,8 +8057,8 @@ class Si { if (!n) return null; const o = e.nodes ? e.nodes.map((s) => this._canvas.getNode(s)).filter((s) => !!s) : this._canvas.nodes; if (o.length === 0) return null; - const i = Yt(o), r = e.fitViewPadding ?? 0.1; - return Jn( + const i = Wt(o), r = e.fitViewPadding ?? 0.1; + return Qn( i, n.width, n.height, @@ -8075,7 +8075,7 @@ class Si { if (!o) return null; const i = this._canvas.getContainerDimensions?.(); if (!i) return null; - const r = n.dimensions?.width ?? we, s = n.dimensions?.height ?? _e, a = n.position.x + r / 2, l = n.position.y + s / 2; + const r = n.dimensions?.width ?? ve, s = n.dimensions?.height ?? be, a = n.position.x + r / 2, l = n.position.y + s / 2; return { x: i.width / 2 - a * o.zoom, y: i.height / 2 - l * o.zoom, @@ -8088,16 +8088,16 @@ class Si { !n || !this._canvas.viewport || (this._canvas.viewport.x = n.x, this._canvas.viewport.y = n.y, this._canvas.viewport.zoom = n.zoom); } } -const ya = /* @__PURE__ */ new Map(); -function Zt(t, e) { - ya.set(t, e); +const xa = /* @__PURE__ */ new Map(); +function Jt(t, e) { + xa.set(t, e); } -function ag(t) { - return ya.get(t); +function ug(t) { + return xa.get(t); } -const Re = "http://www.w3.org/2000/svg", lg = { +const Fe = "http://www.w3.org/2000/svg", fg = { create(t, e) { - const n = document.createElementNS(Re, "circle"); + const n = document.createElementNS(Fe, "circle"); if (n.setAttribute("r", String(e.size ?? 4)), n.setAttribute("fill", e.color ?? "#8B5CF6"), n.classList.add("flow-edge-particle"), e.class) for (const o of e.class.split(" ")) o && n.classList.add(o); @@ -8109,11 +8109,11 @@ const Re = "http://www.w3.org/2000/svg", lg = { destroy(t) { t.remove(); } -}, cg = { +}, hg = { create(t, e) { - const n = document.createElementNS(Re, "g"), o = e.size ?? 6, i = e.color ?? "#8B5CF6", r = document.createElementNS(Re, "circle"); + const n = document.createElementNS(Fe, "g"), o = e.size ?? 6, i = e.color ?? "#8B5CF6", r = document.createElementNS(Fe, "circle"); r.setAttribute("r", String(o * 1.5)), r.setAttribute("fill", i), r.setAttribute("opacity", "0.3"), n.appendChild(r); - const s = document.createElementNS(Re, "circle"); + const s = document.createElementNS(Fe, "circle"); if (s.setAttribute("r", String(o)), s.setAttribute("fill", i), n.appendChild(s), e.class) for (const a of e.class.split(" ")) a && n.classList.add(a); @@ -8127,11 +8127,11 @@ const Re = "http://www.w3.org/2000/svg", lg = { t.remove(); } }; -let dg = 0; -const ug = { +let gg = 0; +const pg = { create(t, e) { - const n = document.createElementNS(Re, "g"); - if (n.__beamLength = e.length ?? 30, n.__beamWidth = e.width ?? 4, n.__beamColor = e.color ?? "#8B5CF6", n.__beamGradient = e.gradient, n.__beamFollowThrough = e.followThrough ?? !0, n.__beamUid = `afbeam-${++dg}`, e.class) + const n = document.createElementNS(Fe, "g"); + if (n.__beamLength = e.length ?? 30, n.__beamWidth = e.width ?? 4, n.__beamColor = e.color ?? "#8B5CF6", n.__beamGradient = e.gradient, n.__beamFollowThrough = e.followThrough ?? !0, n.__beamUid = `afbeam-${++gg}`, e.class) for (const o of e.class.split(" ")) o && n.classList.add(o); return t.appendChild(n), n; @@ -8143,15 +8143,15 @@ const ug = { if (!d) { let g = r; if (s && s.length > 0) { - const m = document.createElementNS(Re, "defs"); - u = document.createElementNS(Re, "linearGradient"), u.setAttribute("id", a), u.setAttribute("gradientUnits", "userSpaceOnUse"); + const m = document.createElementNS(Fe, "defs"); + u = document.createElementNS(Fe, "linearGradient"), u.setAttribute("id", a), u.setAttribute("gradientUnits", "userSpaceOnUse"); for (const y of s) { - const _ = document.createElementNS(Re, "stop"); + const _ = document.createElementNS(Fe, "stop"); _.setAttribute("offset", String(y.offset)), _.setAttribute("stop-color", y.color), y.opacity !== void 0 && _.setAttribute("stop-opacity", String(y.opacity)), u.appendChild(_); } m.appendChild(u), n.appendChild(m), g = `url(#${a})`, n.__gradient = u; } - d = document.createElementNS(Re, "path"), d.setAttribute("d", e.pathEl.getAttribute("d") ?? ""), d.setAttribute("fill", "none"), d.style.stroke = g, d.style.strokeWidth = String(i), d.style.strokeLinecap = "round", d.style.fill = "none", s || (d.style.opacity = "0.85"), d.setAttribute("stroke-dasharray", `${o} ${e.pathLength}`), n.appendChild(d), n.__pathClone = d; + d = document.createElementNS(Fe, "path"), d.setAttribute("d", e.pathEl.getAttribute("d") ?? ""), d.setAttribute("fill", "none"), d.style.stroke = g, d.style.strokeWidth = String(i), d.style.strokeLinecap = "round", d.style.fill = "none", s || (d.style.opacity = "0.85"), d.setAttribute("stroke-dasharray", `${o} ${e.pathLength}`), n.appendChild(d), n.__pathClone = d; } const h = n.__beamFollowThrough ? e.progress * (e.pathLength + o) : e.progress * e.pathLength, p = o - h; if (d.setAttribute("stroke-dashoffset", String(p)), u) { @@ -8161,7 +8161,7 @@ const ug = { return; } let l = n.__fallbackRect; - l || (l = document.createElementNS(Re, "rect"), l.setAttribute("width", String(o)), l.setAttribute("height", String(i)), l.setAttribute("rx", String(i / 2)), l.setAttribute("fill", r), l.setAttribute("opacity", "0.8"), n.appendChild(l), n.__fallbackRect = l); + l || (l = document.createElementNS(Fe, "rect"), l.setAttribute("width", String(o)), l.setAttribute("height", String(i)), l.setAttribute("rx", String(i / 2)), l.setAttribute("fill", r), l.setAttribute("opacity", "0.8"), n.appendChild(l), n.__fallbackRect = l); const c = Math.atan2(e.velocity.y, e.velocity.x) * (180 / Math.PI); l.setAttribute( "transform", @@ -8171,9 +8171,9 @@ const ug = { destroy(t) { t.remove(); } -}, fg = { +}, mg = { create(t, e) { - const n = document.createElementNS(Re, "circle"); + const n = document.createElementNS(Fe, "circle"); if (n.setAttribute("r", String(e.size ?? 6)), n.setAttribute("fill", "none"), n.setAttribute("stroke", e.color ?? "#8B5CF6"), n.setAttribute("stroke-width", "2"), e.class) for (const o of e.class.split(" ")) o && n.classList.add(o); @@ -8186,11 +8186,11 @@ const ug = { destroy(t) { t.remove(); } -}, hg = { +}, yg = { create(t, e) { const n = e.size ?? 16, o = e.href ?? ""; let i; - if (o.startsWith("#") ? (i = document.createElementNS(Re, "use"), i.setAttribute("href", o), i.setAttribute("width", String(n)), i.setAttribute("height", String(n))) : (i = document.createElementNS(Re, "image"), i.setAttribute("href", o), i.setAttribute("width", String(n)), i.setAttribute("height", String(n))), e.class) + if (o.startsWith("#") ? (i = document.createElementNS(Fe, "use"), i.setAttribute("href", o), i.setAttribute("width", String(n)), i.setAttribute("height", String(n))) : (i = document.createElementNS(Fe, "image"), i.setAttribute("href", o), i.setAttribute("width", String(n)), i.setAttribute("height", String(n))), e.class) for (const r of e.class.split(" ")) r && i.classList.add(r); return t.appendChild(i), i; @@ -8203,35 +8203,35 @@ const ug = { t.remove(); } }; -Zt("circle", lg); -Zt("orb", cg); -Zt("beam", ug); -Zt("pulse", fg); -Zt("image", hg); -let Ms = !1; -function gg(t) { +Jt("circle", fg); +Jt("orb", hg); +Jt("beam", pg); +Jt("pulse", mg); +Jt("image", yg); +let Ns = !1; +function wg(t) { const e = t.match(/^([\d.]+)(ms|s)?$/); if (!e) return 2e3; const n = parseFloat(e[1]); return e[2] === "ms" ? n : n * 1e3; } -function Ts(t, e, n) { +function As(t, e, n) { if (t.speed !== void 0 && t.speed > 0) return t.duration !== void 0 && console.warn("[AlpineFlow] Both speed and duration provided for particle; speed takes precedence."), e / t.speed * 1e3; const o = t.duration ?? n; - return typeof o == "number" ? o : gg(o); + return typeof o == "number" ? o : wg(o); } -function pg(t) { +function vg(t) { function e(o, i, r = {}, s = {}) { - const a = r.renderer ?? "circle", l = ag(a); + const a = r.renderer ?? "circle", l = ug(a); if (!l) { - V("particle", `_fireParticleOnPath: unknown renderer "${a}"`); + B("particle", `_fireParticleOnPath: unknown renderer "${a}"`); return; } - a === "beam" && typeof r.onComplete == "function" && r.followThrough === void 0 && !Ms && (Ms = !0, console.warn( + a === "beam" && typeof r.onComplete == "function" && r.followThrough === void 0 && !Ns && (Ns = !0, console.warn( "[AlpineFlow] beam `onComplete` fires after the tail exits the path (follow-through is on by default). Pass `followThrough: false` if you want `onComplete` to fire when the head reaches the target." )); - const c = t._containerStyles, d = r.size ?? s.size ?? (parseFloat(c?.getPropertyValue("--flow-edge-dot-size").trim() ?? "4") || 4), u = r.color ?? s.color ?? c?.getPropertyValue("--flow-edge-dot-fill").trim() ?? yn, f = s.durationFallback ?? c?.getPropertyValue("--flow-edge-dot-duration").trim() ?? "2s", h = o.getTotalLength(), p = Ts(r, h, f), g = { ...r, size: d, color: u }, m = l.create(i, g), y = o.getPointAtLength(0), _ = { + const c = t._containerStyles, d = r.size ?? s.size ?? (parseFloat(c?.getPropertyValue("--flow-edge-dot-size").trim() ?? "4") || 4), u = r.color ?? s.color ?? c?.getPropertyValue("--flow-edge-dot-fill").trim() ?? vn, f = s.durationFallback ?? c?.getPropertyValue("--flow-edge-dot-duration").trim() ?? "2s", h = o.getTotalLength(), p = As(r, h, f), g = { ...r, size: d, color: u }, m = l.create(i, g), y = o.getPointAtLength(0), _ = { x: y.x, y: y.y, progress: 0, @@ -8241,27 +8241,27 @@ function pg(t) { pathEl: o }; l.update(m, _); - let S; - const x = new Promise((I) => { - S = I; - }), b = () => { - typeof r.onComplete == "function" && r.onComplete(), S(); - }, C = s.wrapOnComplete ? s.wrapOnComplete(b) : b, E = { + let k; + const x = new Promise((D) => { + k = D; + }), C = () => { + typeof r.onComplete == "function" && r.onComplete(), k(); + }, E = s.wrapOnComplete ? s.wrapOnComplete(C) : C, b = { element: m, renderer: l, pathEl: o, startElapsed: -1, // set on first engine tick ms: p, - onComplete: C, + onComplete: E, currentPosition: { x: y.x, y: y.y } }; - return t._activeParticles.add(E), t._particleEngineHandle || (t._particleEngineHandle = eo.register((I) => t._tickParticles(I))), { + return t._activeParticles.add(b), t._particleEngineHandle || (t._particleEngineHandle = to.register((D) => t._tickParticles(D))), { getCurrentPosition() { - return t._activeParticles.has(E) ? { ...E.currentPosition } : null; + return t._activeParticles.has(b) ? { ...b.currentPosition } : null; }, stop() { - t._activeParticles.has(E) && (E.renderer.destroy(E.element), t._activeParticles.delete(E), C()); + t._activeParticles.has(b) && (b.renderer.destroy(b.element), t._activeParticles.delete(b), E()); }, get finished() { return x; @@ -8271,7 +8271,7 @@ function pg(t) { function n(o, i = {}) { const r = t.getEdgeSvgElement?.(); if (!r) { - V("particle", "sendParticleAlongPath: SVG layer unavailable"); + B("particle", "sendParticleAlongPath: SVG layer unavailable"); return; } const s = document.createElementNS("http://www.w3.org/2000/svg", "path"); @@ -8285,7 +8285,7 @@ function pg(t) { s.remove(); return; } - return V("particle", "sendParticleAlongPath", { path: o.slice(0, 40) }), a; + return B("particle", "sendParticleAlongPath", { path: o.slice(0, 40) }), a; } return { // ── Particle tick loop ──────────────────────────────────────────────── @@ -8331,26 +8331,26 @@ function pg(t) { if (r && r.style.display === "none") return; const s = t.getEdge(o); if (!s) { - V("particle", `sendParticle: edge "${o}" not found`); + B("particle", `sendParticle: edge "${o}" not found`); return; } const a = t.getEdgePathElement(o); if (!a) { - V("particle", `sendParticle: no path element for edge "${o}"`); + B("particle", `sendParticle: no path element for edge "${o}"`); return; } if (!a.getAttribute("d")) { - V("particle", `sendParticle: edge "${o}" path has no d attribute`); + B("particle", `sendParticle: edge "${o}" path has no d attribute`); return; } const c = t.getEdgeElement(o); if (!c) return; - const d = t._containerStyles, u = i.size ?? s.particleSize ?? (parseFloat(d?.getPropertyValue("--flow-edge-dot-size").trim() ?? "4") || 4), f = i.color ?? s.particleColor ?? d?.getPropertyValue("--flow-edge-dot-fill").trim() ?? yn, h = s.animationDuration ?? d?.getPropertyValue("--flow-edge-dot-duration").trim() ?? "2s", p = e(a, c, i, { + const d = t._containerStyles, u = i.size ?? s.particleSize ?? (parseFloat(d?.getPropertyValue("--flow-edge-dot-size").trim() ?? "4") || 4), f = i.color ?? s.particleColor ?? d?.getPropertyValue("--flow-edge-dot-fill").trim() ?? vn, h = s.animationDuration ?? d?.getPropertyValue("--flow-edge-dot-duration").trim() ?? "2s", p = e(a, c, i, { size: u, color: f, durationFallback: h }); - return p && V("particle", `sendParticle on edge "${o}"`, { size: u, color: f, duration: i.duration }), p; + return p && B("particle", `sendParticle on edge "${o}"`, { size: u, color: f, duration: i.duration }), p; }, // ── Send particle along arbitrary SVG path ─────────────────────────── /** @@ -8369,16 +8369,16 @@ function pg(t) { sendParticleBetween(o, i, r = {}) { const s = t.getNode(o); if (!s) { - V("particle", `sendParticleBetween: source node "${o}" not found`); + B("particle", `sendParticleBetween: source node "${o}" not found`); return; } const a = t.getNode(i); if (!a) { - V("particle", `sendParticleBetween: target node "${i}" not found`); + B("particle", `sendParticleBetween: target node "${i}" not found`); return; } const l = s.position.x + (s.dimensions?.width ?? 150) / 2, c = s.position.y + (s.dimensions?.height ?? 40) / 2, d = a.position.x + (a.dimensions?.width ?? 150) / 2, u = a.position.y + (a.dimensions?.height ?? 40) / 2, f = `M ${l} ${c} L ${d} ${u}`; - return V("particle", `sendParticleBetween "${o}" -> "${i}"`, { path: f }), n(f, r); + return B("particle", `sendParticleBetween "${o}" -> "${i}"`, { path: f }), n(f, r); }, // ── Burst: sequenced multi-particle emission ───────────────────────── /** @@ -8439,17 +8439,17 @@ function pg(t) { }, finished: g, stopAll() { } }; } - const h = Math.max(...f.map((g) => g.length)), p = Ts(l, h, "2s"); + const h = Math.max(...f.map((g) => g.length)), p = As(l, h, "2s"); for (const { id: g, length: m } of f) { - const y = m / h, _ = p * y, S = p - _; - if (S <= 0) { + const y = m / h, _ = p * y, k = p - _; + if (k <= 0) { const x = this.sendParticle(g, { ...l, duration: _ }); x && c.push(x); } else { const x = setTimeout(() => { - const b = this.sendParticle(g, { ...l, duration: _ }); - b && c.push(b); - }, S); + const C = this.sendParticle(g, { ...l, duration: _ }); + C && c.push(C); + }, k); d.push(x); } } @@ -8491,7 +8491,7 @@ function pg(t) { } }; } -class mg { +class _g { constructor(e, n) { this.name = e, this._host = n; } @@ -8535,10 +8535,10 @@ class mg { return this._host.getHandles({ tag: this.name }); } } -const ii = 1, si = 1 / 60; -class ln { +const ri = 1, ai = 1 / 60; +class dn { constructor(e) { - this._virtualTime = 0, this._inFlight = /* @__PURE__ */ new Map(), this._state = ve(e); + this._virtualTime = 0, this._inFlight = /* @__PURE__ */ new Map(), this._state = _e(e); } /** Current virtual time in milliseconds. */ get virtualTime() { @@ -8550,7 +8550,7 @@ class ln { } /** Return a deep-cloned copy of the current virtual canvas state. */ getState() { - return ve(this._state); + return _e(this._state); } /** Advance virtual clock by `dt` seconds and step all in-flight animations. */ advance(e) { @@ -8571,8 +8571,8 @@ class ln { const n = e.args.nodes; if (Array.isArray(n)) for (const o of n) - o?.id && (this._state.nodes[o.id] = ve(o)); - else n?.id ? this._state.nodes[n.id] = ve(n) : e.args.id && e.args.node && (this._state.nodes[e.args.id] = ve(e.args.node)); + o?.id && (this._state.nodes[o.id] = _e(o)); + else n?.id ? this._state.nodes[n.id] = _e(n) : e.args.id && e.args.node && (this._state.nodes[e.args.id] = _e(e.args.node)); break; } case "node-remove": { @@ -8587,8 +8587,8 @@ class ln { const n = e.args.edges; if (Array.isArray(n)) for (const o of n) - o?.id && (this._state.edges[o.id] = ve(o)); - else n?.id ? this._state.edges[n.id] = ve(n) : e.args.id && e.args.edge && (this._state.edges[e.args.id] = ve(e.args.edge)); + o?.id && (this._state.edges[o.id] = _e(o)); + else n?.id ? this._state.edges[n.id] = _e(n) : e.args.id && e.args.edge && (this._state.edges[e.args.id] = _e(e.args.edge)); break; } case "edge-remove": { @@ -8606,16 +8606,16 @@ class ln { } /** Restore the engine from a Checkpoint. */ restoreCheckpoint(e) { - this._state = ve(e.canvas), this._virtualTime = e.t, this._inFlight.clear(); + this._state = _e(e.canvas), this._virtualTime = e.t, this._inFlight.clear(); for (const n of e.inFlight) { - const o = ve(n); + const o = _e(n); this._rehydrateAnim(o), this._inFlight.set(o.handleId, o); } } /** Capture the current engine state as a serializable Checkpoint payload. */ captureCheckpointData() { return { - canvas: ve(this._state), + canvas: _e(this._state), inFlight: [...this._inFlight.values()].map((e) => this._serializeAnim(e)), tagRegistry: {} }; @@ -8624,10 +8624,10 @@ class ln { _applyAnimate(e) { const n = e.args.handleId ?? `virt-${this._virtualTime.toFixed(3)}-${this._inFlight.size}`; e.args.handleId || console.warn("[AlpineFlow VirtualEngine] animate event missing handleId — determinism not guaranteed for this event"); - const o = e.args.targets ?? {}, i = e.args.options ?? {}, r = i.motion, s = r ? Wr(r) ?? void 0 : void 0, a = { + const o = e.args.targets ?? {}, i = e.args.options ?? {}, r = i.motion, s = r ? Kr(r) ?? void 0 : void 0, a = { handleId: n, type: s ? s.type : "eased", - targets: ve(o), + targets: _e(o), startTime: this._virtualTime, duration: i.duration, easing: i.easing, @@ -8641,7 +8641,7 @@ class ln { _initAnim(e) { const n = {}, o = {}; if (this._collectNumericProperties(e.targets, n, o, this._state), e._from = n, e.type === "eased") - e._easingFn = to(e.easing); + e._easingFn = no(e.easing); else { e._physicsStates = /* @__PURE__ */ new Map(); for (const i of Object.keys(n)) @@ -8666,7 +8666,7 @@ class ln { } _rehydrateAnim(e) { if (e._motion = e.motion, e.type === "eased") { - e._easingFn = to(e.easing), e._from = e.fromValues ? { ...e.fromValues } : { ...e.currentValues ?? {} }; + e._easingFn = no(e.easing), e._from = e.fromValues ? { ...e.fromValues } : { ...e.currentValues ?? {} }; return; } if (e.integratorState) { @@ -8690,7 +8690,7 @@ class ln { target: i.target, settled: i.settled }; - return ve({ + return _e({ handleId: e.handleId, type: e.type, targets: e.targets, @@ -8724,17 +8724,17 @@ class ln { if (!r.settled) { switch (o.type) { case "spring": - qr(r, o, n); + Ur(r, o, n); break; case "decay": - bi(r, o, n); + Ei(r, o, n); break; case "inertia": - Yr(r, o, n, i); + Gr(r, o, n, i); break; case "keyframes": { const s = o, a = s.duration ?? 5e3, l = a > 0 ? Math.min((this._virtualTime - e.startTime) / a, 1) : 1; - Xr(r, s, l, i), l >= 1 && (r.settled = !0); + Zr(r, s, l, i), l >= 1 && (r.settled = !0); break; } } @@ -8782,14 +8782,14 @@ class ln { return !0; } } -const wa = /* @__PURE__ */ new Map(); -function ki(t, e) { - wa.set(t, e); +const Ea = /* @__PURE__ */ new Map(); +function Pi(t, e) { + Ea.set(t, e); } -function yg(t) { - return wa.get(t); +function bg(t) { + return Ea.get(t); } -function Li(t, e = 20) { +function Mi(t, e = 20) { const n = Object.values(t); if (n.length === 0) return null; @@ -8800,7 +8800,7 @@ function Li(t, e = 20) { } return o -= e, i -= e, r += e, s += e, { minX: o, minY: i, vbWidth: r - o, vbHeight: s - i }; } -function va(t) { +function Ca(t) { let e = ""; for (const n of Object.values(t.edges)) { const o = t.nodes[n.source], i = t.nodes[n.target]; @@ -8811,29 +8811,29 @@ function va(t) { } return e; } -const wg = { +const xg = { render(t, { width: e, height: n }) { const o = Object.values(t.nodes); if (o.length === 0) return ``; - const i = Li(t.nodes); + const i = Mi(t.nodes); if (!i) return ``; const { minX: r, minY: s, vbWidth: a, vbHeight: l } = i; let c = ``; - c += va(t); + c += Ca(t); for (const d of o) { const u = d.position?.x ?? 0, f = d.position?.y ?? 0, h = d.dimensions?.width ?? 150, p = d.dimensions?.height ?? 40; c += ``; } return c += "", c; } -}, vg = { +}, Eg = { render(t, { width: e, height: n }) { const o = Object.values(t.nodes); if (o.length === 0) return ``; - const i = Li(t.nodes); + const i = Mi(t.nodes); if (!i) return ``; const { minX: r, minY: s, vbWidth: a, vbHeight: l } = i; @@ -8851,12 +8851,12 @@ const wg = { } return c += "", c; } -}, _g = { +}, Cg = { render(t, { width: e, height: n, inFlight: o }) { const i = Object.values(t.nodes); if (i.length === 0) return ``; - const r = Li(t.nodes); + const r = Mi(t.nodes); if (!r) return ``; const s = /* @__PURE__ */ new Set(); @@ -8868,7 +8868,7 @@ const wg = { } const { minX: a, minY: l, vbWidth: c, vbHeight: d } = r; let u = ``; - u += va(t); + u += Ca(t); for (const f of i) { const h = f.position?.x ?? 0, p = f.position?.y ?? 0, g = f.dimensions?.width ?? 150, m = f.dimensions?.height ?? 40; s.has(f.id ?? "") ? u += `` : u += ``; @@ -8876,10 +8876,10 @@ const wg = { return u += "", u; } }; -ki("faithful", wg); -ki("outline", vg); -ki("activity", _g); -function ri(t, e) { +Pi("faithful", xg); +Pi("outline", Eg); +Pi("activity", Cg); +function li(t, e) { let n = 0, o = t.length; for (; n < o; ) { const i = n + o >>> 1; @@ -8887,7 +8887,7 @@ function ri(t, e) { } return n; } -function ai(t, e) { +function ci(t, e) { let n = 0, o = t.length; for (; n < o; ) { const i = n + o >>> 1; @@ -8895,37 +8895,37 @@ function ai(t, e) { } return n; } -function bg(t, e) { +function Sg(t, e) { return e.split(".").reduce((n, o) => n?.[o], t); } -function _a(t) { +function Sa(t) { if (t !== null && typeof t == "object") { Object.freeze(t); for (const e of Object.keys(t)) - _a(t[e]); + Sa(t[e]); } return t; } -class Pi { +class Ti { constructor(e) { - this.version = e.version, this.duration = e.duration, this.initialState = _a(ve(e.initialState)), this.events = Object.freeze(ve(e.events)), this.checkpoints = Object.freeze(ve(e.checkpoints)), this.metadata = Object.freeze({ ...e.metadata ?? {} }), Object.freeze(this); + this.version = e.version, this.duration = e.duration, this.initialState = Sa(_e(e.initialState)), this.events = Object.freeze(_e(e.events)), this.checkpoints = Object.freeze(_e(e.checkpoints)), this.metadata = Object.freeze({ ...e.metadata ?? {} }), Object.freeze(this); } toJSON() { return { version: this.version, duration: this.duration, - initialState: ve(this.initialState), - events: ve(this.events), - checkpoints: ve(this.checkpoints), + initialState: _e(this.initialState), + events: _e(this.events), + checkpoints: _e(this.checkpoints), metadata: { ...this.metadata } }; } static fromJSON(e) { - if (e.version > ii) + if (e.version > ri) throw new Error( - `[AlpineFlow] Recording version ${e.version} is newer than supported (${ii}). Please update AlpineFlow to replay this recording.` + `[AlpineFlow] Recording version ${e.version} is newer than supported (${ri}). Please update AlpineFlow to replay this recording.` ); - return new Pi(e); + return new Ti(e); } /** * Returns unique subjects (nodes, edges, timelines, particles) that appeared @@ -9040,7 +9040,7 @@ class Pi { getValueTrack(e) { const n = []; for (const o of this.checkpoints) { - const i = bg(o.canvas, e); + const i = Sg(o.canvas, e); i !== void 0 && n.push({ t: o.t, v: i }); } return n; @@ -9050,15 +9050,15 @@ class Pi { * up to that point from the nearest prior checkpoint. */ getStateAt(e) { - const n = new ln(this.initialState); + const n = new dn(this.initialState); let o = null; for (const c of this.checkpoints) c.t <= e && (!o || c.t > o.t) && (o = c); o && n.restoreCheckpoint(o); const i = o?.t ?? 0, r = this.events; let s = i; - const a = si * 1e3; - let l = o ? ri(r, i) : ai(r, i); + const a = ai * 1e3; + let l = o ? li(r, i) : ci(r, i); for (; s < e; ) { const c = Math.min(s + a, e); for (; l < r.length && r[l].t <= c; ) @@ -9072,13 +9072,13 @@ class Pi { * Renders a thumbnail SVG snapshot of the canvas state at virtual time `t`. */ renderThumbnailAt(e, n) { - const o = this.getStateAt(e), i = n.renderer ?? "faithful", r = yg(i); + const o = this.getStateAt(e), i = n.renderer ?? "faithful", r = bg(i); if (!r) throw new Error(`[AlpineFlow] Unknown thumbnail renderer "${i}"`); return r.render(o, { width: n.width, height: n.height }); } } -class xg { +class kg { constructor(e, n = {}) { this._events = [], this._checkpoints = [], this._startTime = 0, this._originalMethods = {}, this._checkpointTimer = null, this._eventCounter = 0, this._activeAnims = /* @__PURE__ */ new Map(), this._canvas = e, this._checkpointInterval = n.checkpointInterval ?? 500, this._maxDuration = n.maxDuration ?? 6e4; } @@ -9095,14 +9095,14 @@ class xg { } this._captureCheckpoint(), this._activeAnims.clear(); const i = { - version: ii, + version: ri, duration: this._virtualNow(), initialState: o, events: this._events, checkpoints: this._checkpoints, metadata: n }; - return new Pi(i); + return new Ti(i); } _virtualNow() { return performance.now() - this._startTime; @@ -9162,10 +9162,10 @@ class xg { _captureSnapshot() { const e = {}; for (const o of this._canvas.nodes ?? []) - o && typeof o == "object" && "id" in o && (e[o.id] = ve(o)); + o && typeof o == "object" && "id" in o && (e[o.id] = _e(o)); const n = {}; for (const o of this._canvas.edges ?? []) - o && typeof o == "object" && "id" in o && (n[o.id] = ve(o)); + o && typeof o == "object" && "id" in o && (n[o.id] = _e(o)); return { nodes: e, edges: n, @@ -9204,11 +9204,11 @@ class xg { }), e.push({ handleId: n.handleId, type: r, - targets: ve(n.targets), + targets: _e(n.targets), startTime: n.eventT, duration: i ? void 0 : o.duration ?? 300, easing: i ? void 0 : o.easing, - motion: i ? ve(o.motion) : void 0, + motion: i ? _e(o.motion) : void 0, direction: n.handle?.direction ?? "forward", currentValues: s, fromValues: { ...n.fromValues }, @@ -9258,10 +9258,10 @@ class xg { this._originalMethods = {}; } } -class Eg { +class Lg { constructor(e, n, o = {}) { this._currentTime = 0, this._state = "idle", this._direction = "forward", this._speed = 1, this._rafHandle = null, this._lastWallTime = 0, this._resolveFinished = () => { - }, this.recording = n, this._canvas = e, this._virtualEngine = new ln(n.initialState), this._speed = o.speed ?? 1, this._direction = this._speed < 0 ? "backward" : "forward", this._from = o.from ?? 0, this._to = o.to ?? n.duration, this._loop = o.loop ?? !1, this._currentTime = this._from, this._from > 0 && this._seekEngineTo(this._from), o.skipInitialState || this._applyStateToCanvas(this._virtualEngine.getState()), this.finished = new Promise((i) => { + }, this.recording = n, this._canvas = e, this._virtualEngine = new dn(n.initialState), this._speed = o.speed ?? 1, this._direction = this._speed < 0 ? "backward" : "forward", this._from = o.from ?? 0, this._to = o.to ?? n.duration, this._loop = o.loop ?? !1, this._currentTime = this._from, this._from > 0 && this._seekEngineTo(this._from), o.skipInitialState || this._applyStateToCanvas(this._virtualEngine.getState()), this.finished = new Promise((i) => { this._resolveFinished = i; }), o.paused ? this._state = "paused" : this._speed !== 0 && this.play(); } @@ -9284,13 +9284,13 @@ class Eg { this._speed = e, this._direction = e < 0 ? "backward" : "forward"; } play() { - this._state !== "playing" && (this._state === "ended" && (this._currentTime = this._from, this._seekEngineTo(this._from), this._applyStateToCanvas(this._virtualEngine.getState())), this._state = "playing", this._lastWallTime = Ao(), this._scheduleTick()); + this._state !== "playing" && (this._state === "ended" && (this._currentTime = this._from, this._seekEngineTo(this._from), this._applyStateToCanvas(this._virtualEngine.getState())), this._state = "playing", this._lastWallTime = $o(), this._scheduleTick()); } pause() { this._state === "playing" && (this._state = "paused", this._cancelTick()); } stop() { - this._cancelTick(), this._currentTime = this._from, this._virtualEngine = new ln(this.recording.initialState), this._applyStateToCanvas(this._virtualEngine.getState()), this._state = "idle"; + this._cancelTick(), this._currentTime = this._from, this._virtualEngine = new dn(this.recording.initialState), this._applyStateToCanvas(this._virtualEngine.getState()), this._state = "idle"; } scrubTo(e) { const n = this._resolveTarget(e); @@ -9303,12 +9303,12 @@ class Eg { return this.recording.events.filter((n) => n.t <= e); } getStateAt(e) { - const n = this._findNearestCheckpoint(e), o = new ln(this.recording.initialState); + const n = this._findNearestCheckpoint(e), o = new dn(this.recording.initialState); n && o.restoreCheckpoint(n); const i = n?.t ?? 0, r = this.recording.events; let s = i; - const a = si * 1e3; - let l = n ? ri(r, i) : ai(r, i); + const a = ai * 1e3; + let l = n ? li(r, i) : ci(r, i); for (; s < e; ) { const c = Math.min(s + a, e); for (; l < r.length && r[l].t <= c; ) @@ -9322,7 +9322,7 @@ class Eg { _tick() { if (this._state !== "playing") return; - const e = Ao(), n = (e - this._lastWallTime) / 1e3; + const e = $o(), n = (e - this._lastWallTime) / 1e3; this._lastWallTime = e; const o = n * this._speed * 1e3, i = this._currentTime + o; if (this._direction === "forward" ? i >= this._to : i <= this._from) { @@ -9347,15 +9347,15 @@ class Eg { */ _seekEngineTo(e) { const n = this._findNearestCheckpoint(e); - n ? this._virtualEngine.restoreCheckpoint(n) : this._virtualEngine = new ln(this.recording.initialState), this._walkTo(n?.t ?? 0, e); + n ? this._virtualEngine.restoreCheckpoint(n) : this._virtualEngine = new dn(this.recording.initialState), this._walkTo(n?.t ?? 0, e); } _walkTo(e, n, o = !1) { if (n <= e) return; const i = this.recording.events; let r = e; - const s = si * 1e3; - let a = e === 0 ? ai(i, 0) : ri(i, e); + const s = ai * 1e3; + let a = e === 0 ? ci(i, 0) : li(i, e); for (; r < n; ) { const l = Math.min(r + s, n); for (; a < i.length && i[a].t <= l; ) { @@ -9418,7 +9418,7 @@ class Eg { if (this._loop) { const e = typeof this._loop == "number" ? this._loop - 1 : 1 / 0; if (e > 0) { - this._loop = typeof this._loop == "number" ? e : !0, this._currentTime = this._from, this._seekEngineTo(this._from), this._applyStateToCanvas(this._virtualEngine.getState()), this._state = "playing", this._lastWallTime = Ao(), this._scheduleTick(); + this._loop = typeof this._loop == "number" ? e : !0, this._currentTime = this._from, this._seekEngineTo(this._from), this._applyStateToCanvas(this._virtualEngine.getState()), this._state = "playing", this._lastWallTime = $o(), this._scheduleTick(); return; } } @@ -9470,11 +9470,11 @@ class Eg { } } } -function Ao() { +function $o() { return typeof performance < "u" && typeof performance.now == "function" ? performance.now() : Date.now(); } -function Cg(t) { - const e = pg(t); +function Pg(t) { + const e = vg(t); return { // ── Internal: Sync animation lock state ─────────────────────────────── /** @@ -9491,7 +9491,7 @@ function Cg(t) { * history suspension are automatically managed via timeline events. */ timeline() { - const n = new Si(t, eo); + const n = new Li(t, to); n.on("play", () => { t._activeTimelines.add(n), t._syncAnimationState(); }), n.on("resume", () => { @@ -9522,7 +9522,7 @@ function Cg(t) { async playAnimation(n) { const o = t._animationRegistry.get(n); if (!o) { - V("animation", `Named animation "${n}" not found`); + B("animation", `Named animation "${n}" not found`); return; } const i = t.timeline(); @@ -9561,7 +9561,7 @@ function Cg(t) { t._layoutAnimTick++, t._commitNodeGeometry?.(g); }); }, d = n.nodes ? Object.keys(n.nodes).length : 0, u = n.edges ? Object.keys(n.edges).length : 0; - if (V("animate", "update() called", { + if (B("animate", "update() called", { nodes: d, edges: u, viewport: !!n.viewport, @@ -9574,59 +9574,59 @@ function Cg(t) { if (!m) continue; const _ = (g._duration ?? i) === 0; if (g.followPath && !_) { - let S = null; - typeof g.followPath == "function" ? S = g.followPath : S = Ci(g.followPath); + let k = null; + typeof g.followPath == "function" ? k = g.followPath : k = ki(g.followPath); let x = null; if (g.guidePath?.visible && typeof g.followPath == "string" && typeof document < "u") { - const b = t.getEdgeSvgElement?.(); - b && (x = document.createElementNS("http://www.w3.org/2000/svg", "path"), x.setAttribute("d", g.followPath), x.classList.add("flow-guide-path"), g.guidePath.class && x.classList.add(g.guidePath.class), b.appendChild(x)); + const C = t.getEdgeSvgElement?.(); + C && (x = document.createElementNS("http://www.w3.org/2000/svg", "path"), x.setAttribute("d", g.followPath), x.classList.add("flow-guide-path"), g.guidePath.class && x.classList.add(g.guidePath.class), C.appendChild(x)); } - if (S) { - const b = S, C = x, E = g.guidePath?.autoRemove !== !1; + if (k) { + const C = k, E = x, b = g.guidePath?.autoRemove !== !1; r.push({ key: `node:${p}:followPath`, from: 0, to: 1, apply: (T) => { - const I = t._nodeMap.get(p); - if (!I) return; - const M = b(T); - Ce().raw(I).position.x = M.x, Ce().raw(I).position.y = M.y, s.add(p), T >= 1 && C && E && C.remove(); + const D = t._nodeMap.get(p); + if (!D) return; + const L = C(T); + ke().raw(D).position.x = L.x, ke().raw(D).position.y = L.y, s.add(p), T >= 1 && E && b && E.remove(); } }); } } else if (g.position) { - const x = Ce().raw(m).position; + const x = ke().raw(m).position; if (g.position.x !== void 0) { - const b = g.position.x; + const C = g.position.x; if (_) - x.x = b; + x.x = C; else { - const C = x.x; + const E = x.x; r.push({ key: `node:${p}:position.x`, - from: C, - to: b, - apply: (E) => { + from: E, + to: C, + apply: (b) => { const T = t._nodeMap.get(p); - T && (Ce().raw(T).position.x = E, s.add(p)); + T && (ke().raw(T).position.x = b, s.add(p)); } }); } } if (g.position.y !== void 0) { - const b = g.position.y; + const C = g.position.y; if (_) - x.y = b; + x.y = C; else { - const C = x.y; + const E = x.y; r.push({ key: `node:${p}:position.y`, - from: C, - to: b, - apply: (E) => { + from: E, + to: C, + apply: (b) => { const T = t._nodeMap.get(p); - T && (Ce().raw(T).position.y = E), s.add(p); + T && (ke().raw(T).position.y = b), s.add(p); } }); } @@ -9637,19 +9637,19 @@ function Cg(t) { if (_) m.style = g.style, a.add(p); else { - const S = wn(m.style || {}), x = wn(g.style), b = t._nodeElements.get(p); - if (b) { - const C = getComputedStyle(b); - for (const E of Object.keys(x)) - S[E] === void 0 && (S[E] = C.getPropertyValue(E)); + const k = _n(m.style || {}), x = _n(g.style), C = t._nodeElements.get(p); + if (C) { + const E = getComputedStyle(C); + for (const b of Object.keys(x)) + k[b] === void 0 && (k[b] = E.getPropertyValue(b)); } r.push({ key: `node:${p}:style`, from: 0, to: 1, - apply: (C) => { - const E = t._nodeMap.get(p); - E && (Ce().raw(E).style = Br(S, x, C), a.add(p)); + apply: (E) => { + const b = t._nodeMap.get(p); + b && (ke().raw(b).style = jr(k, x, E), a.add(p)); } }); } @@ -9657,15 +9657,15 @@ function Cg(t) { key: `node:${p}:dimensions.width`, from: m.dimensions.width, to: g.dimensions.width, - apply: (S) => { - m.dimensions.width = S; + apply: (k) => { + m.dimensions.width = k; } })), g.dimensions.height !== void 0 && (m.fixedDimensions = !0, _ ? m.dimensions.height = g.dimensions.height : r.push({ key: `node:${p}:dimensions.height`, from: m.dimensions.height, to: g.dimensions.height, - apply: (S) => { - m.dimensions.height = S; + apply: (k) => { + m.dimensions.height = k; } }))); } @@ -9680,14 +9680,14 @@ function Cg(t) { else if (_) m.color = g.color, l.add(p); else { - const S = typeof m.color == "string" && m.color || getComputedStyle(t._container).getPropertyValue("--flow-edge-stroke").trim() || vi; + const k = typeof m.color == "string" && m.color || getComputedStyle(t._container).getPropertyValue("--flow-edge-stroke").trim() || bi; r.push({ key: `edge:${p}:color`, - from: S, + from: k, to: g.color, apply: (x) => { - const b = t._edgeMap.get(p); - b && (Ce().raw(b).color = x, l.add(p)); + const C = t._edgeMap.get(p); + C && (ke().raw(C).color = x, l.add(p)); } }); } @@ -9695,14 +9695,14 @@ function Cg(t) { if (_) m.strokeWidth = g.strokeWidth, l.add(p); else { - const S = m.strokeWidth ?? (parseFloat(getComputedStyle(t._container).getPropertyValue("--flow-edge-stroke-width").trim() || "1") || 1); + const k = m.strokeWidth ?? (parseFloat(getComputedStyle(t._container).getPropertyValue("--flow-edge-stroke-width").trim() || "1") || 1); r.push({ key: `edge:${p}:strokeWidth`, - from: S, + from: k, to: g.strokeWidth, apply: (x) => { - const b = t._edgeMap.get(p); - b && (Ce().raw(b).strokeWidth = x, l.add(p)); + const C = t._edgeMap.get(p); + C && (ke().raw(C).strokeWidth = x, l.add(p)); } }); } @@ -9766,7 +9766,7 @@ function Cg(t) { }; return o.onComplete?.(), p; } - const h = Ce().raw(t._animator).animate(r, { + const h = ke().raw(t._animator).animate(r, { duration: i, easing: o.easing, delay: o.delay, @@ -9786,14 +9786,14 @@ function Cg(t) { for (const [p, g] of Object.entries(n.nodes)) { const m = t._nodeMap.get(p); if (!m) continue; - const y = Ce().raw(m); + const y = ke().raw(m); (g.followPath || g.position?.x !== void 0) && (m.position.x = y.position.x), (g.followPath || g.position?.y !== void 0) && (m.position.y = y.position.y), g.style !== void 0 && (m.style = y.style); } if (n.edges) for (const [p, g] of Object.entries(n.edges)) { const m = t._edgeMap.get(p); if (!m) continue; - const y = Ce().raw(m); + const y = ke().raw(m); g.color !== void 0 && typeof g.color == "string" && (m.color = y.color), g.strokeWidth !== void 0 && (m.strokeWidth = y.strokeWidth); } s.size > 0 && (t._flushNodePositions(s), t._refreshEdgePaths(s), c(s), s.clear()), a.size > 0 && (t._flushNodeStyles(a), a.clear()), l.size > 0 && (t._flushEdgeStyles(l), l.clear()), o.onComplete?.(); @@ -9811,7 +9811,7 @@ function Cg(t) { * the effective duration is collapsed to 0 for an instant snap. */ animate(n, o = {}) { - const i = Vr(t._config?.respectReducedMotion) ? 0 : o.duration ?? 300; + const i = Wr(t._config?.respectReducedMotion) ? 0 : o.duration ?? 300; return this.update(n, { ...o, duration: i }); }, // ── Follow (viewport tracking) ──────────────────────────────────────── @@ -9827,7 +9827,7 @@ function Cg(t) { i = d; }); let s = !1; - const a = o.zoom, l = eo.register(() => { + const a = o.zoom, l = to.register(() => { if (s) return !0; let d = null; if (typeof n == "string") { @@ -9894,32 +9894,32 @@ function Cg(t) { * Get all tracked animation handles, optionally filtered by tag. */ getHandles(n) { - return Ce().raw(t._animator).registry.getHandles(n); + return ke().raw(t._animator).registry.getHandles(n); }, /** * Cancel all animations matching a tag filter. */ cancelAll(n, o) { - Ce().raw(t._animator).registry.cancelAll(n, o); + ke().raw(t._animator).registry.cancelAll(n, o); }, /** * Pause all animations matching a tag filter. */ pauseAll(n) { - Ce().raw(t._animator).registry.pauseAll(n); + ke().raw(t._animator).registry.pauseAll(n); }, /** * Resume all animations matching a tag filter. */ resumeAll(n) { - Ce().raw(t._animator).registry.resumeAll(n); + ke().raw(t._animator).registry.resumeAll(n); }, /** * Create a named group that auto-tags all animations made through it. */ group(n) { const o = this; - return new mg(n, { + return new _g(n, { animate: (i, r) => o.animate(i, r), update: (i, r) => o.update(i, r), sendParticle: (i, r) => o.sendParticle(i, r), @@ -9938,7 +9938,7 @@ function Cg(t) { * Create a transaction for grouped rollback of multiple animations. */ transaction(n) { - const o = Ce().raw(t._animator), i = o.beginTransaction(); + const o = ke().raw(t._animator), i = o.beginTransaction(); i.onAfterRollback?.((r) => { const s = /* @__PURE__ */ new Set(); for (const a of r) @@ -9962,7 +9962,7 @@ function Cg(t) { * Capture current canvas state. Call restore() to revert. */ snapshot() { - const n = structuredClone(Ce().raw(t.nodes)), o = structuredClone(Ce().raw(t.edges)), i = { ...t.viewport }; + const n = structuredClone(ke().raw(t.nodes)), o = structuredClone(ke().raw(t.edges)), i = { ...t.viewport }; return { restore: () => { t.nodes.splice(0, t.nodes.length, ...structuredClone(n)), t.edges.splice(0, t.edges.length, ...structuredClone(o)), Object.assign(t.viewport, i); @@ -10004,7 +10004,7 @@ function Cg(t) { removeNodes: (g) => t.removeNodes(g), addEdges: (g) => t.addEdges(g), removeEdges: (g) => t.removeEdges(g) - }, h = new xg(f, o), p = async () => { + }, h = new kg(f, o), p = async () => { i.animate = (...g) => f.animate(...g), i.update = (...g) => f.update(...g), i.sendParticle = (...g) => f.sendParticle(...g), i.sendParticleAlongPath = (...g) => f.sendParticleAlongPath(...g), i.sendParticleBetween = (...g) => f.sendParticleBetween(...g), i.sendParticleBurst = (...g) => f.sendParticleBurst(...g), i.sendConverging = (...g) => f.sendConverging(...g); try { const g = n(); @@ -10040,7 +10040,7 @@ function Cg(t) { sendParticleBurst: (s, a) => i.sendParticleBurst(s, a), sendConverging: (s, a) => i.sendConverging(s, a) }; - return new Eg(r, n, o); + return new Lg(r, n, o); }, // ── Cleanup lifecycle ───────────────────────────────────────────────── /** @@ -10065,7 +10065,7 @@ function Cg(t) { * `renderer: 'your-name'` in any `sendParticle*` options to use it. */ registerParticleRenderer(n, o) { - Zt(n, o); + Jt(n, o); }, // ── Particle system (delegated to canvas-particles sub-mixin) ──────── _tickParticles: e._tickParticles, @@ -10077,25 +10077,25 @@ function Cg(t) { destroyParticles: e.destroyParticles }; } -function As(t, e, n, o) { +function $s(t, e, n, o) { const i = e.find((a) => a.id === t); if (!i) return /* @__PURE__ */ new Set(); if (i.type === "group") return wt(t, e); - const r = /* @__PURE__ */ new Set(), s = Qo(t, e, n); + const r = /* @__PURE__ */ new Set(), s = ti(t, e, n); for (const a of s) r.add(a.id); if (o?.recursive) { const a = s.map((l) => l.id); for (; a.length > 0; ) { - const l = a.shift(), c = Qo(l, e, n); + const l = a.shift(), c = ti(l, e, n); for (const d of c) !r.has(d.id) && d.id !== t && (r.add(d.id), a.push(d.id)); } } return r; } -function Sg(t, e, n) { +function Mg(t, e, n) { const o = /* @__PURE__ */ new Map(); for (const i of e) n.has(i.id) && o.set(i.id, { ...i.position }); @@ -10105,12 +10105,12 @@ function Sg(t, e, n) { reroutedEdges: /* @__PURE__ */ new Map() }; } -function No(t, e, n, o) { +function Io(t, e, n, o) { t.collapsed = !0, o && (t.dimensions = { ...o }); for (const i of e) n.targetPositions.has(i.id) && (i.hidden = !0); } -function Ns(t, e, n, o = !0) { +function Is(t, e, n, o = !0) { t.collapsed = !1, o && n.originalDimensions && (t.dimensions = { ...n.originalDimensions }); const i = /* @__PURE__ */ new Set(); if (t.type === "group") { @@ -10127,7 +10127,7 @@ function Ns(t, e, n, o = !0) { r.position = { ...s }, i.has(r.id) || (r.hidden = !1); } } -function $o(t, e, n) { +function Do(t, e, n) { const o = /* @__PURE__ */ new Map(); for (const i of e) { const r = n.has(i.source), s = n.has(i.target), a = i.source === t, l = i.target === t; @@ -10135,14 +10135,14 @@ function $o(t, e, n) { } return o; } -function kg(t, e) { +function Tg(t, e) { for (const n of t) { const o = e.get(n.id); o && (n.source = o.source, n.target = o.target, o.hidden !== void 0 ? n.hidden = o.hidden : delete n.hidden); } } -const Fn = { width: 150, height: 50 }; -function Lg(t) { +const On = { width: 150, height: 50 }; +function Ng(t) { return { /** * Collapse a node — hide its descendants/outgoers and optionally animate. @@ -10150,22 +10150,22 @@ function Lg(t) { collapseNode(e, n) { const o = t._nodeMap.get(e); if (!o || o.collapsed) return; - const i = As(e, t.nodes, t.edges, { recursive: n?.recursive }); + const i = $s(e, t.nodes, t.edges, { recursive: n?.recursive }); if (i.size === 0) return; - V("collapse", `Collapsing node "${e}"`, { + B("collapse", `Collapsing node "${e}"`, { type: o.type ?? "default", descendants: [...i], animate: n?.animate !== !1, recursive: n?.recursive ?? !1 }), t._captureHistory(); - const r = o.type === "group", s = r ? o.collapsedDimensions ?? { width: 150, height: 60 } : void 0, a = n?.animate !== !1, l = Sg(o, t.nodes, i); + const r = o.type === "group", s = r ? o.collapsedDimensions ?? { width: 150, height: 60 } : void 0, a = n?.animate !== !1, l = Mg(o, t.nodes, i); if (a) { t._suspendHistory(); - const c = o.dimensions ?? Fn, d = r && s ? s : c, u = {}; + const c = o.dimensions ?? On, d = r && s ? s : c, u = {}; for (const [h] of l.targetPositions) { const p = t._nodeMap.get(h); if (!p) continue; - const g = p.dimensions ?? Fn; + const g = p.dimensions ?? On; let m, y; p.parentId === e ? (m = (d.width - g.width) / 2, y = (d.height - g.height) / 2) : (m = o.position.x + (d.width - g.width) / 2, y = o.position.y + (d.height - g.height) / 2), u[h] = { position: { x: m, y }, @@ -10188,11 +10188,11 @@ function Lg(t) { }, onComplete: () => { for (const h of f) h.style.opacity = ""; - No(o, t.nodes, l, s), l.reroutedEdges = $o(e, t.edges, i), t._collapseState.set(e, l), t._resumeHistory(), t._emit("node-collapse", { node: o, descendants: [...i] }); + Io(o, t.nodes, l, s), l.reroutedEdges = Do(e, t.edges, i), t._collapseState.set(e, l), t._resumeHistory(), t._emit("node-collapse", { node: o, descendants: [...i] }); } - }) : (No(o, t.nodes, l, s), l.reroutedEdges = $o(e, t.edges, i), t._collapseState.set(e, l), t._resumeHistory(), t._emit("node-collapse", { node: o, descendants: [...i] })); + }) : (Io(o, t.nodes, l, s), l.reroutedEdges = Do(e, t.edges, i), t._collapseState.set(e, l), t._resumeHistory(), t._emit("node-collapse", { node: o, descendants: [...i] })); } else - No(o, t.nodes, l, s), l.reroutedEdges = $o(e, t.edges, i), t._collapseState.set(e, l), t._emit("node-collapse", { node: o, descendants: [...i] }); + Io(o, t.nodes, l, s), l.reroutedEdges = Do(e, t.edges, i), t._collapseState.set(e, l), t._emit("node-collapse", { node: o, descendants: [...i] }); }, /** * Expand a previously collapsed node — restore descendants/outgoers. @@ -10202,22 +10202,22 @@ function Lg(t) { if (!o || !o.collapsed) return; const i = t._collapseState.get(e); if (!i) return; - V("collapse", `Expanding node "${e}"`, { + B("collapse", `Expanding node "${e}"`, { type: o.type ?? "default", descendants: [...i.targetPositions.keys()], animate: n?.animate !== !1, reroutedEdges: i.reroutedEdges.size }), t._captureHistory(); const r = o.type === "group", s = n?.animate !== !1; - if (i.reroutedEdges.size > 0 && kg(t.edges, i.reroutedEdges), s) { + if (i.reroutedEdges.size > 0 && Tg(t.edges, i.reroutedEdges), s) { t._suspendHistory(), r && i.originalDimensions && (o.dimensions = { ...i.originalDimensions }); - const a = o.dimensions ?? Fn; - Ns(o, t.nodes, i, r); + const a = o.dimensions ?? On; + Is(o, t.nodes, i, r); const l = {}; for (const [u, f] of i.targetPositions) { const h = t._nodeMap.get(u); if (h && !h.hidden) { - const p = h.dimensions ?? Fn; + const p = h.dimensions ?? On; let g, m; h.parentId === e ? (g = (a.width - p.width) / 2, m = (a.height - p.height) / 2) : (g = o.position.x + (a.width - p.width) / 2, m = o.position.y + (a.height - p.height) / 2), h.position = { x: g, y: m }, h.style = { ...h.style || {}, opacity: "0" }, l[u] = { position: f, @@ -10250,14 +10250,14 @@ function Lg(t) { } }) : t._resumeHistory(), t._collapseState.delete(e), t._emit("node-expand", { node: o, descendants: [...i.targetPositions.keys()] }); } else - Ns(o, t.nodes, i, r), t._collapseState.delete(e), t._emit("node-expand", { node: o, descendants: [...i.targetPositions.keys()] }); + Is(o, t.nodes, i, r), t._collapseState.delete(e), t._emit("node-expand", { node: o, descendants: [...i.targetPositions.keys()] }); }, /** * Toggle collapse/expand state of a node. */ toggleNode(e, n) { const o = t._nodeMap.get(e); - o && (V("collapse", `Toggle node "${e}" → ${o.collapsed ? "expand" : "collapse"}`), o.collapsed ? this.expandNode(e, n) : this.collapseNode(e, n)); + o && (B("collapse", `Toggle node "${e}" → ${o.collapsed ? "expand" : "collapse"}`), o.collapsed ? this.expandNode(e, n) : this.collapseNode(e, n)); }, /** * Check if a node is collapsed. @@ -10269,7 +10269,7 @@ function Lg(t) { * Get the number of nodes that would be hidden when collapsing this node. */ getCollapseTargetCount(e) { - return As(e, t.nodes, t.edges).size; + return $s(e, t.nodes, t.edges).size; }, /** * Get the number of descendants (via parentId hierarchy) of a node. @@ -10279,21 +10279,21 @@ function Lg(t) { } }; } -function Pg(t) { +function Ag(t) { return { /** * Condense a node — switch to summary view hiding internal rows. */ condenseNode(e) { const n = t._nodeMap.get(e); - !n || n.condensed || (t._captureHistory(), n.condensed = !0, V("condense", `Node "${e}" condensed`), t._emit("node-condense", { node: n })); + !n || n.condensed || (t._captureHistory(), n.condensed = !0, B("condense", `Node "${e}" condensed`), t._emit("node-condense", { node: n })); }, /** * Uncondense a node — restore full row view. */ uncondenseNode(e) { const n = t._nodeMap.get(e); - !n || !n.condensed || (t._captureHistory(), n.condensed = !1, V("condense", `Node "${e}" uncondensed`), t._emit("node-uncondense", { node: n })); + !n || !n.condensed || (t._captureHistory(), n.condensed = !1, B("condense", `Node "${e}" uncondensed`), t._emit("node-uncondense", { node: n })); }, /** * Toggle condensed state of a node. @@ -10310,20 +10310,20 @@ function Pg(t) { } }; } -function Mg(t) { +function $g(t) { return { // ── Row Selection ──────────────────────────────────────────────────── selectRow(e) { if (t.selectedRows.has(e)) return; t.selectedRows.add(e); const n = e.indexOf("."), o = n === -1 ? e : e.slice(0, n), i = n === -1 ? "" : e.slice(n + 1); - V("selection", `Row "${e}" selected`), t._emit("row-select", { rowId: e, nodeId: o, attrId: i }), t._emit("row-selection-change", { selectedRows: [...t.selectedRows] }); + B("selection", `Row "${e}" selected`), t._emit("row-select", { rowId: e, nodeId: o, attrId: i }), t._emit("row-selection-change", { selectedRows: [...t.selectedRows] }); }, deselectRow(e) { if (!t.selectedRows.has(e)) return; t.selectedRows.delete(e); const n = e.indexOf("."), o = n === -1 ? e : e.slice(0, n), i = n === -1 ? "" : e.slice(n + 1); - V("selection", `Row "${e}" deselected`), t._emit("row-deselect", { rowId: e, nodeId: o, attrId: i }), t._emit("row-selection-change", { selectedRows: [...t.selectedRows] }); + B("selection", `Row "${e}" deselected`), t._emit("row-deselect", { rowId: e, nodeId: o, attrId: i }), t._emit("row-selection-change", { selectedRows: [...t.selectedRows] }); }, toggleRowSelect(e) { t.selectedRows.has(e) ? this.deselectRow(e) : this.selectRow(e); @@ -10335,14 +10335,14 @@ function Mg(t) { return t.selectedRows.has(e); }, deselectAllRows() { - t.selectedRows.size !== 0 && (V("selection", "Deselecting all rows"), t.selectedRows.clear(), t._container?.querySelectorAll(".flow-row-selected").forEach((e) => { + t.selectedRows.size !== 0 && (B("selection", "Deselecting all rows"), t.selectedRows.clear(), t._container?.querySelectorAll(".flow-row-selected").forEach((e) => { e.classList.remove("flow-row-selected"); }), t._emit("row-selection-change", { selectedRows: [] })); }, // ── Row Filtering ──────────────────────────────────────────────────── setRowFilter(e, n) { const o = t._nodeMap.get(e); - o && (o.rowFilter = n, V("filter", `Node "${e}" row filter set to "${typeof n == "function" ? "predicate" : n}"`)); + o && (o.rowFilter = n, B("filter", `Node "${e}" row filter set to "${typeof n == "function" ? "predicate" : n}"`)); }, getRowFilter(e) { return t._nodeMap.get(e)?.rowFilter ?? "all"; @@ -10369,14 +10369,14 @@ function Mg(t) { } }; } -const Tg = 8, Ag = 12, Ng = 2; -function Mi(t) { +const Ig = 8, Dg = 12, Rg = 2; +function Ni(t) { return { - width: t.dimensions?.width ?? we, - height: t.dimensions?.height ?? _e + width: t.dimensions?.width ?? ve, + height: t.dimensions?.height ?? be }; } -function $g(t) { +function Hg(t) { if (t.stretch) return t.stretch; switch (t.direction) { case "vertical": @@ -10387,14 +10387,14 @@ function $g(t) { return "both"; } } -function Ig(t) { +function Fg(t) { return [...t].sort((e, n) => { const o = e.order ?? 1 / 0, i = n.order ?? 1 / 0; return o !== i ? o - i : 0; }); } -function $s(t, e, n) { - const o = e.gap ?? Tg, i = e.padding ?? Ag, r = e.headerHeight ?? 0, s = $g(e), a = Ig(t), l = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map(); +function Ds(t, e, n) { + const o = e.gap ?? Ig, i = e.padding ?? Dg, r = e.headerHeight ?? 0, s = Hg(e), a = Fg(t), l = /* @__PURE__ */ new Map(), c = /* @__PURE__ */ new Map(); if (a.length === 0) return { positions: l, @@ -10402,11 +10402,11 @@ function $s(t, e, n) { parentDimensions: n ? { width: n.width, height: n.height } : { width: i * 2, height: i * 2 + r } }; const d = n ? n.width - i * 2 : 0, u = n ? n.height - i * 2 - r : 0; - return e.direction === "vertical" ? Dg(a, o, i, r, s, d, l, c) : e.direction === "horizontal" ? Hg(a, o, i, r, s, u, l, c) : Rg(a, o, i, r, s, e.columns ?? Ng, d, u, l, c); + return e.direction === "vertical" ? Og(a, o, i, r, s, d, l, c) : e.direction === "horizontal" ? zg(a, o, i, r, s, u, l, c) : Vg(a, o, i, r, s, e.columns ?? Rg, d, u, l, c); } -function Dg(t, e, n, o, i, r, s, a) { +function Og(t, e, n, o, i, r, s, a) { let l = 0; - const c = t.map((f) => Mi(f)); + const c = t.map((f) => Ni(f)); for (const f of c) l = Math.max(l, f.width); const d = r > 0 ? r : l; let u = n + o; @@ -10420,9 +10420,9 @@ function Dg(t, e, n, o, i, r, s, a) { parentDimensions: { width: d + n * 2, height: u } }; } -function Hg(t, e, n, o, i, r, s, a) { +function zg(t, e, n, o, i, r, s, a) { let l = 0; - const c = t.map((f) => Mi(f)); + const c = t.map((f) => Ni(f)); for (const f of c) l = Math.max(l, f.height); const d = r > 0 ? r : l; let u = n; @@ -10436,8 +10436,8 @@ function Hg(t, e, n, o, i, r, s, a) { parentDimensions: { width: u, height: d + n * 2 + o } }; } -function Rg(t, e, n, o, i, r, s, a, l, c) { - const d = Math.min(r, t.length), u = t.map((y) => Mi(y)); +function Vg(t, e, n, o, i, r, s, a, l, c) { + const d = Math.min(r, t.length), u = t.map((y) => Ni(y)); let f = 0, h = 0; for (const y of u) f = Math.max(f, y.width), h = Math.max(h, y.height); @@ -10446,8 +10446,8 @@ function Rg(t, e, n, o, i, r, s, a, l, c) { const g = Math.ceil(t.length / d), m = a > 0 ? (a - (g - 1) * e) / g : 0; m > 0 && (h = m); for (let y = 0; y < t.length; y++) { - const _ = y % d, S = Math.floor(y / d), x = n + _ * (f + e), b = n + o + S * (h + e); - l.set(t[y].id, { x, y: b }), i === "both" ? c.set(t[y].id, { width: f, height: h }) : i === "width" ? c.set(t[y].id, { width: f, height: u[y].height }) : i === "height" && c.set(t[y].id, { width: u[y].width, height: h }); + const _ = y % d, k = Math.floor(y / d), x = n + _ * (f + e), C = n + o + k * (h + e); + l.set(t[y].id, { x, y: C }), i === "both" ? c.set(t[y].id, { width: f, height: h }) : i === "width" ? c.set(t[y].id, { width: f, height: u[y].height }) : i === "height" && c.set(t[y].id, { width: u[y].width, height: h }); } return { positions: l, @@ -10458,7 +10458,7 @@ function Rg(t, e, n, o, i, r, s, a, l, c) { } }; } -function Fg(t) { +function Bg(t) { return { // ── Auto-layout scheduling ───────────────────────────────────────────── /** @@ -10540,7 +10540,7 @@ function Fg(t) { */ _applyLayout(e, n) { const o = n?.duration ?? 300; - if (V("layout", `_applyLayout: repositioning ${e.size} node(s)`, { + if (B("layout", `_applyLayout: repositioning ${e.size} node(s)`, { duration: o, adjustHandles: n?.adjustHandles ?? !1, fitView: n?.fitView !== !1 @@ -10626,34 +10626,34 @@ function Fg(t) { if (u.dimensions = void 0, !d && u.maxDimensions && u.maxDimensions.width !== void 0 && u.maxDimensions.height !== void 0 && (d = { width: u.maxDimensions.width, height: u.maxDimensions.height }), !c) for (const x of f) x.childLayout && this.layoutChildren(x.id, { excludeId: s, omitFromComputation: a, shallow: !1 }); - const p = u.childLayout, g = p.headerHeight !== void 0 ? p : u.data?.label ? { ...p, headerHeight: 30 } : p, m = $s(f, g, d); - for (const [x, b] of m.positions) { + const p = u.childLayout, g = p.headerHeight !== void 0 ? p : u.data?.label ? { ...p, headerHeight: 30 } : p, m = Ds(f, g, d); + for (const [x, C] of m.positions) { if (x === s || l && x === l.id && !t._nodeMap.has(x)) continue; - const C = h.get(x); - C && (C.position ? (C.position.x = b.x, C.position.y = b.y) : C.position = { x: b.x, y: b.y }); + const E = h.get(x); + E && (E.position ? (E.position.x = C.x, E.position.y = C.y) : E.position = { x: C.x, y: C.y }); } - for (const [x, b] of m.dimensions) { + for (const [x, C] of m.dimensions) { if (x === s || l && x === l.id && !t._nodeMap.has(x)) continue; - const C = h.get(x); - if (C) { - let E = b.width, T = b.height; - C.minDimensions && (C.minDimensions.width != null && (E = Math.max(E, C.minDimensions.width)), C.minDimensions.height != null && (T = Math.max(T, C.minDimensions.height))), C.maxDimensions && (C.maxDimensions.width != null && (E = Math.min(E, C.maxDimensions.width)), C.maxDimensions.height != null && (T = Math.min(T, C.maxDimensions.height))), C.dimensions ? (C.dimensions.width = E, C.dimensions.height = T) : C.dimensions = { width: E, height: T }, C.childLayout && !c && this.layoutChildren(x, { excludeId: s, omitFromComputation: a, shallow: !1, stretchedSize: C.dimensions }); + const E = h.get(x); + if (E) { + let b = C.width, T = C.height; + E.minDimensions && (E.minDimensions.width != null && (b = Math.max(b, E.minDimensions.width)), E.minDimensions.height != null && (T = Math.max(T, E.minDimensions.height))), E.maxDimensions && (E.maxDimensions.width != null && (b = Math.min(b, E.maxDimensions.width)), E.maxDimensions.height != null && (T = Math.min(T, E.maxDimensions.height))), E.dimensions ? (E.dimensions.width = b, E.dimensions.height = T) : E.dimensions = { width: b, height: T }, E.childLayout && !c && this.layoutChildren(x, { excludeId: s, omitFromComputation: a, shallow: !1, stretchedSize: E.dimensions }); } } let y = m.parentDimensions.width, _ = m.parentDimensions.height; if (u.minDimensions && (u.minDimensions.width != null && (y = Math.max(y, u.minDimensions.width)), u.minDimensions.height != null && (_ = Math.max(_, u.minDimensions.height))), u.maxDimensions && (u.maxDimensions.width != null && (y = Math.min(y, u.maxDimensions.width)), u.maxDimensions.height != null && (_ = Math.min(_, u.maxDimensions.height))), u.dimensions || (u.dimensions = { width: 0, height: 0 }), u.dimensions.width = y, u.dimensions.height = _, y !== m.parentDimensions.width || _ !== m.parentDimensions.height) { - const b = $s(f, g, { width: y, height: _ }); - for (const [C, E] of b.positions) { - if (C === s || l && C === l.id && !t._nodeMap.has(C)) continue; - const T = h.get(C); - T && (T.position ? (T.position.x = E.x, T.position.y = E.y) : T.position = { x: E.x, y: E.y }); + const C = Ds(f, g, { width: y, height: _ }); + for (const [E, b] of C.positions) { + if (E === s || l && E === l.id && !t._nodeMap.has(E)) continue; + const T = h.get(E); + T && (T.position ? (T.position.x = b.x, T.position.y = b.y) : T.position = { x: b.x, y: b.y }); } - for (const [C, E] of b.dimensions) { - if (C === s || l && C === l.id && !t._nodeMap.has(C)) continue; - const T = h.get(C); + for (const [E, b] of C.dimensions) { + if (E === s || l && E === l.id && !t._nodeMap.has(E)) continue; + const T = h.get(E); if (T) { - let I = E.width, M = E.height; - T.minDimensions && (T.minDimensions.width != null && (I = Math.max(I, T.minDimensions.width)), T.minDimensions.height != null && (M = Math.max(M, T.minDimensions.height))), T.maxDimensions && (T.maxDimensions.width != null && (I = Math.min(I, T.maxDimensions.width)), T.maxDimensions.height != null && (M = Math.min(M, T.maxDimensions.height))), T.dimensions ? (T.dimensions.width = I, T.dimensions.height = M) : T.dimensions = { width: I, height: M }, T.childLayout && !c && this.layoutChildren(C, { excludeId: s, omitFromComputation: a, shallow: !1, stretchedSize: T.dimensions }); + let D = b.width, L = b.height; + T.minDimensions && (T.minDimensions.width != null && (D = Math.max(D, T.minDimensions.width)), T.minDimensions.height != null && (L = Math.max(L, T.minDimensions.height))), T.maxDimensions && (T.maxDimensions.width != null && (D = Math.min(D, T.maxDimensions.width)), T.maxDimensions.height != null && (L = Math.min(L, T.maxDimensions.height))), T.dimensions ? (T.dimensions.width = D, T.dimensions.height = L) : T.dimensions = { width: D, height: L }, T.childLayout && !c && this.layoutChildren(E, { excludeId: s, omitFromComputation: a, shallow: !1, stretchedSize: T.dimensions }); } } } @@ -10701,7 +10701,7 @@ function Fg(t) { * to include them. */ layout(e) { - const n = Dt("layout:dagre"); + const n = Ht("layout:dagre"); if (!n) throw new Error("layout() requires the dagre plugin. Register it with: Alpine.plugin(AlpineFlowDagre)"); const o = e?.direction ?? "TB", i = e?.includeChildren ? t.nodes : t.nodes.filter((s) => !s.parentId), r = n(i, t.edges, { @@ -10714,7 +10714,7 @@ function Fg(t) { handleDirection: o, fitView: e?.fitView, duration: e?.duration - }), V("layout", "Applied dagre layout", { direction: o }), t._emit("layout", { type: "dagre", direction: o }); + }), B("layout", "Applied dagre layout", { direction: o }), t._emit("layout", { type: "dagre", direction: o }); }, /** * Apply force-directed layout. @@ -10726,7 +10726,7 @@ function Fg(t) { * to include them. */ forceLayout(e) { - const n = Dt("layout:force"); + const n = Ht("layout:force"); if (!n) throw new Error("forceLayout() requires the force plugin. Register it with: Alpine.plugin(AlpineFlowForce)"); const o = e?.includeChildren ? t.nodes : t.nodes.filter((r) => !r.parentId), i = n(o, t.edges, { @@ -10739,7 +10739,7 @@ function Fg(t) { this._applyLayout(i, { fitView: e?.fitView, duration: e?.duration - }), V("layout", "Applied force layout", { charge: e?.charge ?? -300, distance: e?.distance ?? 150 }), t._emit("layout", { type: "force", charge: e?.charge ?? -300, distance: e?.distance ?? 150 }); + }), B("layout", "Applied force layout", { charge: e?.charge ?? -300, distance: e?.distance ?? 150 }), t._emit("layout", { type: "force", charge: e?.charge ?? -300, distance: e?.distance ?? 150 }); }, /** * Apply hierarchy/tree layout. @@ -10751,7 +10751,7 @@ function Fg(t) { * to include them. */ treeLayout(e) { - const n = Dt("layout:hierarchy"); + const n = Ht("layout:hierarchy"); if (!n) throw new Error("treeLayout() requires the hierarchy plugin. Register it with: Alpine.plugin(AlpineFlowHierarchy)"); const o = e?.direction ?? "TB", i = e?.includeChildren ? t.nodes : t.nodes.filter((s) => !s.parentId), r = n(i, t.edges, { @@ -10765,7 +10765,7 @@ function Fg(t) { handleDirection: o, fitView: e?.fitView, duration: e?.duration - }), V("layout", "Applied tree layout", { layoutType: e?.layoutType ?? "tree", direction: o }), t._emit("layout", { type: "tree", layoutType: e?.layoutType ?? "tree", direction: o }); + }), B("layout", "Applied tree layout", { layoutType: e?.layoutType ?? "tree", direction: o }), t._emit("layout", { type: "tree", layoutType: e?.layoutType ?? "tree", direction: o }); }, /** * Apply ELK (Eclipse Layout Kernel) layout. @@ -10778,7 +10778,7 @@ function Fg(t) { * to include them. */ async elkLayout(e) { - const n = Dt("layout:elk"); + const n = Ht("layout:elk"); if (!n) throw new Error("elkLayout() requires the elk plugin. Register it with: Alpine.plugin(AlpineFlowElk)"); const o = e?.direction ?? "DOWN", i = e?.includeChildren ? t.nodes : t.nodes.filter((s) => !s.parentId), r = await n(i, t.edges, { @@ -10788,7 +10788,7 @@ function Fg(t) { layerSpacing: e?.layerSpacing }); if (r.size === 0) { - V("layout", "ELK layout returned no positions — skipping apply"); + B("layout", "ELK layout returned no positions — skipping apply"); return; } this._applyLayout(r, { @@ -10796,17 +10796,17 @@ function Fg(t) { handleDirection: o, fitView: e?.fitView, duration: e?.duration - }), V("layout", "Applied ELK layout", { algorithm: e?.algorithm ?? "layered", direction: o }), t._emit("layout", { type: "elk", algorithm: e?.algorithm ?? "layered", direction: o }); + }), B("layout", "Applied ELK layout", { algorithm: e?.algorithm ?? "layered", direction: o }), t._emit("layout", { type: "elk", algorithm: e?.algorithm ?? "layered", direction: o }); } }; } -function Og(t) { +function qg(t) { return { // ── Internal helpers ────────────────────────────────────────────────── _getChildValidation(e) { const n = t.getNode(e); if (n) - return un(n, t._config.childValidationRules ?? {}); + return hn(n, t._config.childValidationRules ?? {}); }, _recomputeChildValidation() { const e = /* @__PURE__ */ new Set(), n = t._config.childValidationRules ?? {}; @@ -10820,12 +10820,12 @@ function Og(t) { t._validationErrorCache.delete(o); continue; } - const r = un(i, t._config.childValidationRules ?? {}); + const r = hn(i, t._config.childValidationRules ?? {}); if (!r) { t._validationErrorCache.delete(o); continue; } - const s = t.nodes.filter((l) => l.parentId === o), a = Ss(i, s, r); + const s = t.nodes.filter((l) => l.parentId === o), a = Ls(i, s, r); a.length > 0 ? t._validationErrorCache.set(o, a) : t._validationErrorCache.delete(o), i._validationErrors = a; } }, @@ -10833,9 +10833,9 @@ function Og(t) { validateParent(e) { const n = t.getNode(e); if (!n) return { valid: !0, errors: [] }; - const o = un(n, t._config.childValidationRules ?? {}); + const o = hn(n, t._config.childValidationRules ?? {}); if (!o) return { valid: !0, errors: [] }; - const i = t.nodes.filter((s) => s.parentId === e), r = Ss(n, i, o); + const i = t.nodes.filter((s) => s.parentId === e), r = Ls(n, i, o); return { valid: r.length === 0, errors: r }; }, validateAll() { @@ -10868,7 +10868,7 @@ function Og(t) { if (f) { const h = t.nodes.filter( (g) => g.parentId === i - ), p = ro(f, o, h, u); + ), p = ao(f, o, h, u); if (!p.valid) return t._config.onChildValidationFail && t._config.onChildValidationFail({ parent: f, @@ -10899,7 +10899,7 @@ function Og(t) { if (s) { const d = t.nodes.filter( (f) => f.parentId === n && f.id !== e - ), u = ga(r, o, d, s); + ), u = va(r, o, d, s); if (!u.valid) return t._config.onChildValidationFail && t._config.onChildValidationFail({ parent: r, @@ -10916,7 +10916,7 @@ function Og(t) { if (u) { const f = t.nodes.filter( (p) => p.parentId === i - ), h = ro(u, o, f, d); + ), h = ao(u, o, f, d); if (!h.valid) return t._config.onChildValidationFail && t._config.onChildValidationFail({ parent: u, @@ -10959,7 +10959,7 @@ function Og(t) { } }; } -function zg(t) { +function Xg(t) { return { registerCompute(e, n) { t._computeEngine.registerCompute(e, n); @@ -10983,14 +10983,14 @@ function zg(t) { } }; } -function qn(t, e, n, o, i) { +function Xn(t, e, n, o, i) { const r = i * Math.PI / 180, s = Math.cos(r), a = Math.sin(r), l = t - n, c = e - o; return { x: n + l * s - c * a, y: o + l * a + c * s }; } -function Vg(t) { +function Yg(t) { if (t.length < 2) return ""; if (t.length === 2) return `M${t[0].x},${t[0].y} L${t[1].x},${t[1].y}`; @@ -11001,8 +11001,8 @@ function Vg(t) { } return e; } -const Bg = 40; -function qg(t, e = Bg) { +const Wg = 40; +function jg(t, e = Wg) { if (t.length < 2) return ""; if (t.length === 2) return `M${t[0].x},${t[0].y} L${t[1].x},${t[1].y}`; @@ -11014,7 +11014,7 @@ function qg(t, e = Bg) { const o = t[t.length - 1]; return n += ` L${o.x},${o.y}`, n; } -function Yg(t) { +function Ug(t) { if (t.length < 2) return { x: t[0]?.x ?? 0, y: t[0]?.y ?? 0, offsetX: 0, offsetY: 0 }; let e = 0; @@ -11039,7 +11039,7 @@ function Yg(t) { const i = t[t.length - 1]; return { x: i.x, y: i.y, offsetX: 0, offsetY: 0 }; } -function Xg({ +function Gg({ sourceX: t, sourceY: e, sourcePosition: n = "bottom", @@ -11049,7 +11049,7 @@ function Xg({ obstacles: s }) { if (!s || s.length === 0) - return oo({ + return io({ sourceX: t, sourceY: e, sourcePosition: n, @@ -11057,9 +11057,9 @@ function Xg({ targetY: i, targetPosition: r }); - const a = aa(t, e, n, o, i, r, s); + const a = fa(t, e, n, o, i, r, s); if (!a) - return oo({ + return io({ sourceX: t, sourceY: e, sourcePosition: n, @@ -11067,7 +11067,7 @@ function Xg({ targetY: i, targetPosition: r }); - const l = qg(a), { x: c, y: d, offsetX: u, offsetY: f } = Yg(a); + const l = jg(a), { x: c, y: d, offsetX: u, offsetY: f } = Ug(a); return { path: l, labelPosition: { x: c, y: d }, @@ -11075,7 +11075,7 @@ function Xg({ labelOffsetY: f }; } -function Wg(t) { +function Zg(t) { const { sourceX: e, sourceY: n, @@ -11092,22 +11092,22 @@ function Wg(t) { let c; switch (s) { case "linear": - c = Is(l); + c = Rs(l); break; case "step": - c = jg(l, 0); + c = Kg(l, 0); break; case "smoothstep": - c = Ug(l, a); + c = Jg(l, a); break; case "catmull-rom": case "bezier": - c = Vg(l.map((f, h) => ({ ...f, index: h }))); + c = Yg(l.map((f, h) => ({ ...f, index: h }))); break; default: - c = Is(l); + c = Rs(l); } - const d = Gg(l), u = Sn({ sourceX: e, sourceY: n, targetX: o, targetY: i }); + const d = Qg(l), u = Ln({ sourceX: e, sourceY: n, targetX: o, targetY: i }); return { path: c, labelPosition: d, @@ -11115,33 +11115,33 @@ function Wg(t) { labelOffsetY: u.offsetY }; } -function Is(t) { +function Rs(t) { if (t.length < 2) return ""; let e = `M${t[0].x},${t[0].y}`; for (let n = 1; n < t.length; n++) e += ` L${t[n].x},${t[n].y}`; return e; } -function jg(t, e) { +function Kg(t, e) { if (t.length < 2) return ""; if (t.length === 2) - return ba(t[0], t[1], e); + return ka(t[0], t[1], e); let n = `M${t[0].x},${t[0].y}`; for (let i = 1; i < t.length - 1; i++) { const r = t[i - 1], s = t[i], a = t[i + 1]; - n += Xt(r.x, r.y, s.x, s.y, a.x, a.y, e); + n += jt(r.x, r.y, s.x, s.y, a.x, a.y, e); } const o = t[t.length - 1]; return n += ` L${o.x},${o.y}`, n; } -function ba(t, e, n) { - const o = (t.x + e.x) / 2, i = Xt(t.x, t.y, o, t.y, o, e.y, n), r = Xt(o, t.y, o, e.y, e.x, e.y, n); +function ka(t, e, n) { + const o = (t.x + e.x) / 2, i = jt(t.x, t.y, o, t.y, o, e.y, n), r = jt(o, t.y, o, e.y, e.x, e.y, n); return `M${t.x},${t.y}${i}${r} L${e.x},${e.y}`; } -function Ug(t, e) { +function Jg(t, e) { if (t.length < 2) return ""; if (t.length === 2) - return ba(t[0], t[1], e); + return ka(t[0], t[1], e); const n = [t[0]]; for (let r = 0; r < t.length - 1; r++) { const s = t[r], a = t[r + 1], l = Math.abs(a.x - s.x), c = Math.abs(a.y - s.y); @@ -11155,12 +11155,12 @@ function Ug(t, e) { let o = `M${n[0].x},${n[0].y}`; for (let r = 1; r < n.length - 1; r++) { const s = n[r - 1], a = n[r], l = n[r + 1]; - o += Xt(s.x, s.y, a.x, a.y, l.x, l.y, e); + o += jt(s.x, s.y, a.x, a.y, l.x, l.y, e); } const i = n[n.length - 1]; return o += ` L${i.x},${i.y}`, o; } -function Gg(t) { +function Qg(t) { if (t.length < 2) return t[0] ?? { x: 0, y: 0 }; let e = 0; const n = []; @@ -11182,29 +11182,29 @@ function Gg(t) { } return t[t.length - 1]; } -function Ut(t, e, n, o) { - const i = t.dimensions?.width ?? we, r = t.dimensions?.height ?? _e, s = Gt(t, o); +function Zt(t, e, n, o) { + const i = t.dimensions?.width ?? ve, r = t.dimensions?.height ?? be, s = Kt(t, o); let a; if (t.shape) { - const l = n?.[t.shape] ?? ua[t.shape]; + const l = n?.[t.shape] ?? ma[t.shape]; if (l) { const c = l.perimeterPoint(i, r, e); a = { x: s.x + c.x, y: s.y + c.y }; } else { - const c = Es(i, r, e); + const c = Ss(i, r, e); a = { x: s.x + c.x, y: s.y + c.y }; } } else { - const l = Es(i, r, e); + const l = Ss(i, r, e); a = { x: s.x + l.x, y: s.y + l.y }; } if (t.rotation) { const l = s.x + i / 2, c = s.y + r / 2; - a = qn(a.x, a.y, l, c, t.rotation); + a = Xn(a.x, a.y, l, c, t.rotation); } return a; } -function Ds(t) { +function Hs(t) { switch (t) { case "top-left": case "top-right": @@ -11216,7 +11216,7 @@ function Ds(t) { return t; } } -function li(t) { +function di(t) { const e = Math.SQRT1_2; switch (t) { case "top": @@ -11237,55 +11237,55 @@ function li(t) { return { x: e, y: e }; } } -const Zg = 1.5, Kg = 5 / 20; -function Ht(t, e, n, o) { +const ep = 1.5, tp = 5 / 20; +function Ft(t, e, n, o) { if (!o) return t; const i = typeof o == "string" ? {} : o, r = n ? Math.min(n.handleWidth, n.handleHeight) / 2 : 5; if (i.offset !== void 0) { - const u = li(e); + const u = di(e); return { x: t.x + u.x * i.offset, y: t.y + u.y * i.offset }; } - const l = (i.width ?? 12.5) * Zg * Kg * 0.4, c = r + l, d = li(e); + const l = (i.width ?? 12.5) * ep * tp * 0.4, c = r + l, d = di(e); return { x: t.x + d.x * c, y: t.y + d.y * c }; } -function lo(t, e, n, o = "bottom", i = "top", r, s, a, l, c, d, u) { - const f = r ?? Ut(e, o, c, d), h = s ?? Ut(n, i, c, d), p = { +function co(t, e, n, o = "bottom", i = "top", r, s, a, l, c, d, u) { + const f = r ?? Zt(e, o, c, d), h = s ?? Zt(n, i, c, d), p = { sourceX: f.x, sourceY: f.y, - sourcePosition: Ds(o), + sourcePosition: Hs(o), targetX: h.x, targetY: h.y, - targetPosition: Ds(i) + targetPosition: Hs(i) }, g = t.type ?? u ?? "bezier"; if (a?.[g]) - return a[g](p); + return a[g](p, t); switch (g === "floating" ? t.pathType ?? "bezier" : g) { case "editable": - return Wg({ + return Zg({ ...p, controlPoints: t.controlPoints, pathStyle: t.pathStyle }); case "avoidant": - return Xg({ ...p, obstacles: l }); + return Gg({ ...p, obstacles: l }); case "orthogonal": - return ch({ ...p, obstacles: l }); + return hh({ ...p, obstacles: l }); case "smoothstep": - return vn(p); + return bn(p); case "straight": - return Zr({ sourceX: f.x, sourceY: f.y, targetX: h.x, targetY: h.y }); + return ta({ sourceX: f.x, sourceY: f.y, targetX: h.x, targetY: h.y }); default: - return oo(p); + return io(p); } } -function Hs(t, e) { - const n = t.dimensions?.width ?? we, o = t.dimensions?.height ?? _e, i = { +function Fs(t, e) { + const n = t.dimensions?.width ?? ve, o = t.dimensions?.height ?? be, i = { x: t.position.x + n / 2, y: t.position.y + o / 2 - }, r = t.rotation ? qn(e.x, e.y, i.x, i.y, -t.rotation) : e, s = r.x - i.x, a = r.y - i.y; + }, r = t.rotation ? Xn(e.x, e.y, i.x, i.y, -t.rotation) : e, s = r.x - i.x, a = r.y - i.y; if (s === 0 && a === 0) { const p = { x: i.x, y: i.y - o / 2 }; - return t.rotation ? qn(p.x, p.y, i.x, i.y, t.rotation) : p; + return t.rotation ? Xn(p.x, p.y, i.x, i.y, t.rotation) : p; } const l = n / 2, c = o / 2, d = Math.abs(s), u = Math.abs(a); let f; @@ -11294,10 +11294,10 @@ function Hs(t, e) { x: i.x + s * f, y: i.y + a * f }; - return t.rotation ? qn(h.x, h.y, i.x, i.y, t.rotation) : h; + return t.rotation ? Xn(h.x, h.y, i.x, i.y, t.rotation) : h; } -function Rs(t, e) { - const n = t.dimensions?.width ?? we, o = t.dimensions?.height ?? _e, i = t.position.x + n / 2, r = t.position.y + o / 2; +function Os(t, e) { + const n = t.dimensions?.width ?? ve, o = t.dimensions?.height ?? be, i = t.position.x + n / 2, r = t.position.y + o / 2; if (t.rotation) { const h = e.x - i, p = e.y - r; return Math.abs(h) > Math.abs(p) ? h > 0 ? "right" : "left" : p > 0 ? "bottom" : "top"; @@ -11310,14 +11310,14 @@ function Rs(t, e) { const u = e.x - i, f = e.y - r; return Math.abs(u) > Math.abs(f) ? u > 0 ? "right" : "left" : f > 0 ? "bottom" : "top"; } -function xa(t, e) { - const n = t.dimensions?.width ?? we, o = t.dimensions?.height ?? _e, i = e.dimensions?.width ?? we, r = e.dimensions?.height ?? _e, s = { +function La(t, e) { + const n = t.dimensions?.width ?? ve, o = t.dimensions?.height ?? be, i = e.dimensions?.width ?? ve, r = e.dimensions?.height ?? be, s = { x: t.position.x + n / 2, y: t.position.y + o / 2 }, a = { x: e.position.x + i / 2, y: e.position.y + r / 2 - }, l = Hs(t, a), c = Hs(e, s), d = Rs(t, l), u = Rs(e, c); + }, l = Fs(t, a), c = Fs(e, s), d = Os(t, l), u = Os(e, c); return { sx: l.x, sy: l.y, @@ -11327,18 +11327,18 @@ function xa(t, e) { targetPos: u }; } -function By(t, e) { +function nw(t, e) { const n = e.x - t.x, o = e.y - t.y; let i, r; return Math.abs(n) > Math.abs(o) ? (i = n > 0 ? "right" : "left", r = n > 0 ? "left" : "right") : (i = o > 0 ? "bottom" : "top", r = o > 0 ? "top" : "bottom"), { sourcePos: i, targetPos: r }; } -function Ea(t) { +function Pa(t) { return typeof t == "object" && t !== null && "from" in t && "to" in t; } -function Ca(t, e) { +function Ma(t, e) { return `${t}__grad__${e}`; } -function Sa(t, e, n, o, i, r, s) { +function Ta(t, e, n, o, i, r, s) { let a = t.querySelector(`#${CSS.escape(e)}`); if (!a) { a = document.createElementNS("http://www.w3.org/2000/svg", "linearGradient"), a.id = e, a.setAttribute("gradientUnits", "userSpaceOnUse"), a.classList.add("flow-edge-gradient"); @@ -11351,13 +11351,25 @@ function Sa(t, e, n, o, i, r, s) { const l = a.querySelectorAll("stop"); return l[0]?.setAttribute("stop-color", n.from), l[1]?.setAttribute("stop-color", n.to), a; } -function Io(t, e) { +function Ro(t, e) { t.querySelector(`#${CSS.escape(e)}`)?.remove(); } -function Jg(t, e) { - return Array.isArray(t) ? t.findIndex((n) => n?.name === e) : -1; +function Eo(t) { + return t.endsWith("-l") ? { field: t.slice(0, -2), side: "left" } : t.endsWith("-r") ? { field: t.slice(0, -2), side: "right" } : { field: t, side: null }; +} +function np(t, e) { + if (!Array.isArray(t)) return -1; + const n = t.findIndex((r) => r?.name === e); + if (n >= 0) return n; + const { field: o, side: i } = Eo(e); + return i === null ? -1 : t.findIndex((r) => r?.name === o); +} +function zs(t, e) { + if (!Array.isArray(t) || !e || t.some((i) => i?.name === e)) return null; + const { field: n, side: o } = Eo(e); + return o === null ? null : t.some((i) => i?.name === n) ? o : null; } -function Fs(t, e, n, o, i) { +function Vs(t, e, n, o, i) { const r = t.data?.fields; if (!Array.isArray(r) || !Number.isInteger(n) || n < 0 || n >= r.length) return null; const { width: s, height: a } = t.dimensions ?? {}; @@ -11368,15 +11380,15 @@ function Fs(t, e, n, o, i) { const g = n === r.length - 1 ? u : d, m = e.y + p + l + n * c + g; return { x: o === "left" ? e.x + f : e.x + s - h, y: m, position: o }; } -const Qg = /* @__PURE__ */ new Set(["x-data", "x-init", "x-bind", "href", "src", "action", "formaction", "srcdoc"]); -async function ep(t) { +const op = /* @__PURE__ */ new Set(["x-data", "x-init", "x-bind", "href", "src", "action", "formaction", "srcdoc"]); +async function ip(t) { const { dropNodeId: e, dropHandleId: n, draggedEnd: o, edge: i, canvas: r, containerEl: s } = t; if (!e) return { applied: !1 }; const a = r.getNode(e); - if (a && !Be(a)) + if (a && !qe(a)) return { applied: !1 }; - const l = o === "target" ? { source: i.source, sourceHandle: i.sourceHandle, target: e, targetHandle: n } : { source: e, sourceHandle: n, target: i.target, targetHandle: i.targetHandle }, c = { ...i }, d = await Qr({ + const l = o === "target" ? { source: i.source, sourceHandle: i.sourceHandle, target: e, targetHandle: n } : { source: e, sourceHandle: n, target: i.target, targetHandle: i.targetHandle }, c = { ...i }, d = await ia({ edge: i, newConnection: l, canvas: r, @@ -11385,21 +11397,18 @@ async function ep(t) { }); return d.applied ? (r._emit?.("reconnect", { oldEdge: c, newConnection: l }), { applied: !0, newConnection: l }) : { applied: !1, reason: d.reason, newConnection: l }; } -function tp(t) { +function sp(t) { return t === !0 || t === "dash" ? "dash" : t === "pulse" ? "pulse" : t === "dot" ? "dot" : "none"; } -function ka(t) { - return t.endsWith("-l") ? "left" : t.endsWith("-r") ? "right" : null; -} -function Os(t, e) { +function Bs(t, e) { if (!e) return t; - const n = li(t), o = e * Math.PI / 180, i = Math.cos(o), r = Math.sin(o), s = n.x * i - n.y * r, a = n.x * r + n.y * i; + const n = di(t), o = e * Math.PI / 180, i = Math.cos(o), r = Math.sin(o), s = n.x * i - n.y * r, a = n.x * r + n.y * i; return Math.abs(s) > Math.abs(a) ? s > 0 ? "right" : "left" : a > 0 ? "bottom" : "top"; } -function zs(t) { +function qs(t) { return { x: (t.left + t.right) / 2, y: (t.top + t.bottom) / 2 }; } -function co(t, e) { +function uo(t, e) { const n = Array.from(t); if (n.length === 0) return null; if (n.length === 1 || !e) return n[0]; @@ -11410,27 +11419,29 @@ function co(t, e) { } return o; } -function uo(t, e, n, o, i, r, s) { +function fo(t, e, n, o, i, r, s) { const a = s ?? t.querySelector(`[data-flow-node-id="${CSS.escape(e)}"]`); if (a) { if (n) { const c = a.querySelectorAll( `[data-flow-handle-id="${CSS.escape(n)}"][data-flow-handle-type="${o}"]` ); - let d = co(c, r); + let d = uo(c, r); if (!d) { const u = a.querySelectorAll( `[data-flow-handle-id="${CSS.escape(n)}"]` ); - d = co(u, r); + d = uo(u, r); } if (d) return d.getAttribute("data-flow-handle-position") ?? (o === "source" ? "bottom" : "top"); } if (n) { - const c = ka(n); - if (c && a.querySelector(`[data-flow-handle-position="${c}"]`)) - return c; + const { field: c, side: d } = Eo(n); + if (d && (a.querySelector( + `[data-flow-handle-id="${CSS.escape(c)}"][data-flow-handle-type="${o}"][data-flow-handle-position="${d}"]` + ) || a.querySelector(`[data-flow-handle-position="${d}"]`))) + return d; } const l = a.querySelector(`[data-flow-handle-type="${o}"]`); if (l) @@ -11442,7 +11453,7 @@ function uo(t, e, n, o, i, r, s) { } return o === "source" ? "bottom" : "top"; } -function Vs(t, e, n, o) { +function Xs(t, e, n, o) { if (!t || !e || t.hidden || t.collapsed || t.condensed || t.rotation) return -1; const i = t.nodeOrigin; if (i && (i[0] !== 0 || i[1] !== 0) || !n?.hasAttribute("data-flow-schema-node") || n.style.display === "none") return -1; @@ -11451,31 +11462,31 @@ function Vs(t, e, n, o) { const a = t.data?.fields; if (!Array.isArray(a) || a.length === 0) return -1; const l = o.insetTop + o.headerHeight + (a.length - 1) * o.rowHeight + o.rowHeightLast + o.insetBottom; - return Math.abs(l - s) > 0.5 ? -1 : Jg(a, e); + return Math.abs(l - s) > 0.5 ? -1 : np(a, e); } -function Bs(t, e, n, o, i) { - const r = t.dimensions?.width ?? we, s = e.x + (i.insetLeft + (r - i.insetRight)) / 2; +function Ys(t, e, n, o, i) { + const r = t.dimensions?.width ?? ve, s = e.x + (i.insetLeft + (r - i.insetRight)) / 2; return n === "source" ? o >= s ? "right" : "left" : o > s ? "right" : "left"; } -function qs(t) { - return t.position.x + (t.dimensions?.width ?? we) / 2; +function Ws(t) { + return t.position.x + (t.dimensions?.width ?? ve) / 2; } -function np(t, e, n, o, i, r, s, a) { - const l = Vs(t, i, s?.get(t.id), a); +function rp(t, e, n, o, i, r, s, a) { + const l = Xs(t, i, s?.get(t.id), a); if (l < 0) return null; - const c = Vs(e, r, s?.get(e.id), a); + const c = Xs(e, r, s?.get(e.id), a); if (c < 0) return null; - const d = Bs(t, n.position, "source", qs(o), a), u = Bs(e, o.position, "target", qs(n), a), f = Fs(t, n.position, l, d, a), h = Fs(e, o.position, c, u, a); - if (!f || !h) return null; - const p = { handleWidth: a.handleWidth, handleHeight: a.handleHeight }; + const d = t.data?.fields, u = e.data?.fields, f = zs(d, i) ?? Ys(t, n.position, "source", Ws(o), a), h = zs(u, r) ?? Ys(e, o.position, "target", Ws(n), a), p = Vs(t, n.position, l, f, a), g = Vs(e, o.position, c, h, a); + if (!p || !g) return null; + const m = { handleWidth: a.handleWidth, handleHeight: a.handleHeight }; return { - sourcePos: f.position, - targetPos: h.position, - srcMeasurement: { x: f.x, y: f.y, ...p }, - tgtMeasurement: { x: h.x, y: h.y, ...p } + sourcePos: p.position, + targetPos: g.position, + srcMeasurement: { x: p.x, y: p.y, ...m }, + tgtMeasurement: { x: g.x, y: g.y, ...m } }; } -function Ys(t, e, n, o, i, r, s, a, l) { +function js(t, e, n, o, i, r, s, a, l) { const c = l ?? t.querySelector(`[data-flow-node-id="${CSS.escape(e)}"]`); if (!c) return null; let d = null; @@ -11483,15 +11494,17 @@ function Ys(t, e, n, o, i, r, s, a, l) { const g = c.querySelectorAll( `[data-flow-handle-id="${CSS.escape(o)}"][data-flow-handle-type="${i}"]` ); - if (d = co(g, a), !d) { + if (d = uo(g, a), !d) { const m = c.querySelectorAll( `[data-flow-handle-id="${CSS.escape(o)}"]` ); - d = co(m, a); + d = uo(m, a); } if (!d) { - const m = ka(o); - m && (d = c.querySelector(`[data-flow-handle-position="${m}"]`)); + const { field: m, side: y } = Eo(o); + y && (d = c.querySelector( + `[data-flow-handle-id="${CSS.escape(m)}"][data-flow-handle-type="${i}"][data-flow-handle-position="${y}"]` + ) ?? c.querySelector(`[data-flow-handle-position="${y}"]`)); } } else d = c.querySelector(`[data-flow-handle-type="${i}"]`); @@ -11506,7 +11519,7 @@ function Ys(t, e, n, o, i, r, s, a, l) { handleHeight: u.height / r }; } -function op(t, e, n) { +function ap(t, e, n) { const o = n ?? t.getTotalLength(), i = t.getPointAtLength(o * Math.max(0, Math.min(1, e))); return { x: i.x, y: i.y }; } @@ -11514,7 +11527,7 @@ function ct(t, e, n, o, i) { const r = t - n, s = e - o; return Math.sqrt(r * r + s * s) <= i; } -function ip(t, e, n) { +function lp(t, e, n) { const o = n.x - e.x, i = n.y - e.y, r = o * o + i * i; if (r === 0) return Math.sqrt((t.x - e.x) ** 2 + (t.y - e.y) ** 2); let s = ((t.x - e.x) * o + (t.y - e.y) * i) / r; @@ -11522,7 +11535,7 @@ function ip(t, e, n) { const a = e.x + s * o, l = e.y + s * i; return Math.sqrt((t.x - a) ** 2 + (t.y - l) ** 2); } -function sp(t) { +function cp(t) { t.directive( "flow-edge", (e, { expression: n }, { evaluate: o, effect: i, cleanup: r }) => { @@ -11533,395 +11546,395 @@ function sp(t) { let l = e.querySelector("path:not(:first-child)"); l || (l = document.createElementNS("http://www.w3.org/2000/svg", "path"), l.setAttribute("fill", "none"), l.setAttribute("stroke-width", "1.5"), l.style.pointerEvents = "none", s.appendChild(l)); let c = null, d = null, u = null, f = null, h = 0, p = null, g = "none", m = null, y = null; - function _(L, $, F, K, ne) { - p || (p = document.createElementNS("http://www.w3.org/2000/svg", "circle"), p.classList.add("flow-edge-dot"), p.style.pointerEvents = "none", L.appendChild(p)); - const G = F.closest(".flow-container"), B = G ? getComputedStyle(G) : null, z = K.particleSize ?? (parseFloat(B?.getPropertyValue("--flow-edge-dot-size").trim() ?? "4") || 4), Y = ne || B?.getPropertyValue("--flow-edge-dot-duration").trim() || "2s"; - p.setAttribute("r", String(z)), K.particleColor ? p.style.fill = K.particleColor : p.style.removeProperty("fill"); - const W = p.querySelector("animateMotion"); - W && W.remove(); + function _(S, N, F, W, Q) { + p || (p = document.createElementNS("http://www.w3.org/2000/svg", "circle"), p.classList.add("flow-edge-dot"), p.style.pointerEvents = "none", S.appendChild(p)); + const U = F.closest(".flow-container"), G = U ? getComputedStyle(U) : null, Z = W.particleSize ?? (parseFloat(G?.getPropertyValue("--flow-edge-dot-size").trim() ?? "4") || 4), H = Q || G?.getPropertyValue("--flow-edge-dot-duration").trim() || "2s"; + p.setAttribute("r", String(Z)), W.particleColor ? p.style.fill = W.particleColor : p.style.removeProperty("fill"); + const q = p.querySelector("animateMotion"); + q && q.remove(); const j = document.createElementNS("http://www.w3.org/2000/svg", "animateMotion"); - j.setAttribute("dur", Y), j.setAttribute("repeatCount", "indefinite"), j.setAttribute("path", $), p.appendChild(j); + j.setAttribute("dur", H), j.setAttribute("repeatCount", "indefinite"), j.setAttribute("path", N), p.appendChild(j); } - function S() { + function k() { p?.remove(), p = null; } - let x = null, b = null, C = null, E = null; - const T = (L) => { - L.stopPropagation(); - const $ = o(n); - if (!$) return; + let x = null, C = null, E = null, b = null; + const T = (S) => { + S.stopPropagation(); + const N = o(n); + if (!N) return; const F = t.$data(e.closest("[x-data]")); - F && (F._emit("edge-click", { edge: $, event: L }), mt(L, F._shortcuts?.multiSelect) ? F.selectedEdges.has($.id) ? (F.selectedEdges.delete($.id), $.selected = !1, V("selection", `Edge "${$.id}" deselected (shift)`)) : (F.selectedEdges.add($.id), $.selected = !0, V("selection", `Edge "${$.id}" selected (shift)`)) : (F.deselectAll(), F.selectedEdges.add($.id), $.selected = !0, V("selection", `Edge "${$.id}" selected`)), F._emitSelectionChange()); - }, I = (L) => { - L.preventDefault(), L.stopPropagation(); - const $ = o(n); - if (!$) return; + F && (F._emit("edge-click", { edge: N, event: S }), mt(S, F._shortcuts?.multiSelect) ? F.selectedEdges.has(N.id) ? (F.selectedEdges.delete(N.id), N.selected = !1, B("selection", `Edge "${N.id}" deselected (shift)`)) : (F.selectedEdges.add(N.id), N.selected = !0, B("selection", `Edge "${N.id}" selected (shift)`)) : (F.deselectAll(), F.selectedEdges.add(N.id), N.selected = !0, B("selection", `Edge "${N.id}" selected`)), F._emitSelectionChange()); + }, D = (S) => { + S.preventDefault(), S.stopPropagation(); + const N = o(n); + if (!N) return; const F = t.$data(e.closest("[x-data]")); if (!F) return; - const K = L.target; - if (K.classList.contains("flow-edge-control-point")) { - const ne = parseInt(K.dataset.pointIndex ?? "", 10); - if (!isNaN(ne)) { + const W = S.target; + if (W.classList.contains("flow-edge-control-point")) { + const Q = parseInt(W.dataset.pointIndex ?? "", 10); + if (!isNaN(Q)) { F._emit("edge-control-point-context-menu", { - edge: $, - pointIndex: ne, - position: { x: L.clientX, y: L.clientY }, - event: L + edge: N, + pointIndex: Q, + position: { x: S.clientX, y: S.clientY }, + event: S }); return; } } - F._emit("edge-context-menu", { edge: $, event: L }); - }, M = (L) => { - L.stopPropagation(), L.preventDefault(); - const $ = o(n), F = t.$data(e.closest("[x-data]")); - if (!$ || !F || ($.type ?? F._config?.defaultEdgeType ?? "bezier") !== "editable") return; - const ne = L.target; - if (ne.classList.contains("flow-edge-control-point")) { - const G = parseInt(ne.dataset.pointIndex ?? "", 10); - !isNaN(G) && $.controlPoints && (F._captureHistory?.(), $.controlPoints.splice(G, 1), F._emit("edge-control-point-change", { edge: $, action: "remove", index: G })); + F._emit("edge-context-menu", { edge: N, event: S }); + }, L = (S) => { + S.stopPropagation(), S.preventDefault(); + const N = o(n), F = t.$data(e.closest("[x-data]")); + if (!N || !F || (N.type ?? F._config?.defaultEdgeType ?? "bezier") !== "editable") return; + const Q = S.target; + if (Q.classList.contains("flow-edge-control-point")) { + const U = parseInt(Q.dataset.pointIndex ?? "", 10); + !isNaN(U) && N.controlPoints && (F._captureHistory?.(), N.controlPoints.splice(U, 1), F._emit("edge-control-point-change", { edge: N, action: "remove", index: U })); return; } - if (ne.classList.contains("flow-edge-midpoint")) { - const G = parseInt(ne.dataset.segmentIndex ?? "", 10); - if (!isNaN(G)) { - const B = F.screenToFlowPosition(L.clientX, L.clientY); - $.controlPoints || ($.controlPoints = []), F._captureHistory?.(), $.controlPoints.splice(G, 0, { x: B.x, y: B.y }), F._emit("edge-control-point-change", { edge: $, action: "add", index: G }); + if (Q.classList.contains("flow-edge-midpoint")) { + const U = parseInt(Q.dataset.segmentIndex ?? "", 10); + if (!isNaN(U)) { + const G = F.screenToFlowPosition(S.clientX, S.clientY); + N.controlPoints || (N.controlPoints = []), F._captureHistory?.(), N.controlPoints.splice(U, 0, { x: G.x, y: G.y }), F._emit("edge-control-point-change", { edge: N, action: "add", index: U }); } return; } - if (ne.closest("path")) { - const G = F.screenToFlowPosition(L.clientX, L.clientY); - $.controlPoints || ($.controlPoints = []); - const B = [ + if (Q.closest("path")) { + const U = F.screenToFlowPosition(S.clientX, S.clientY); + N.controlPoints || (N.controlPoints = []); + const G = [ x ?? { x: 0, y: 0 }, - ...$.controlPoints, - b ?? { x: 0, y: 0 } + ...N.controlPoints, + C ?? { x: 0, y: 0 } ]; - let z = 0, Y = 1 / 0; - for (let W = 0; W < B.length - 1; W++) { - const j = ip(G, B[W], B[W + 1]); - j < Y && (Y = j, z = W); + let Z = 0, H = 1 / 0; + for (let q = 0; q < G.length - 1; q++) { + const j = lp(U, G[q], G[q + 1]); + j < H && (H = j, Z = q); } - F._captureHistory?.(), $.controlPoints.splice(z, 0, { x: G.x, y: G.y }), F._emit("edge-control-point-change", { edge: $, action: "add", index: z }); + F._captureHistory?.(), N.controlPoints.splice(Z, 0, { x: U.x, y: U.y }), F._emit("edge-control-point-change", { edge: N, action: "add", index: Z }); } - }, A = (L) => { - const $ = L.target; - if (!$.classList.contains("flow-edge-control-point") || L.button !== 0) return; - L.stopPropagation(), L.preventDefault(); + }, A = (S) => { + const N = S.target; + if (!N.classList.contains("flow-edge-control-point") || S.button !== 0) return; + S.stopPropagation(), S.preventDefault(); const F = o(n); if (!F?.controlPoints) return; - const K = t.$data(e.closest("[x-data]")); - if (!K) return; - const ne = parseInt($.dataset.pointIndex ?? "", 10); - if (isNaN(ne)) return; - $.classList.add("dragging"); - let G = !1; - const B = (Y) => { - G || (K._captureHistory?.(), G = !0); - let W = K.screenToFlowPosition(Y.clientX, Y.clientY); - const j = K._config?.snapToGrid; - j && (W = { - x: Math.round(W.x / j[0]) * j[0], - y: Math.round(W.y / j[1]) * j[1] - }), F.controlPoints[ne] = W; - }, z = () => { - document.removeEventListener("pointermove", B), document.removeEventListener("pointerup", z), $.classList.remove("dragging"), G && K._emit("edge-control-point-change", { edge: F, action: "move", index: ne }); + const W = t.$data(e.closest("[x-data]")); + if (!W) return; + const Q = parseInt(N.dataset.pointIndex ?? "", 10); + if (isNaN(Q)) return; + N.classList.add("dragging"); + let U = !1; + const G = (H) => { + U || (W._captureHistory?.(), U = !0); + let q = W.screenToFlowPosition(H.clientX, H.clientY); + const j = W._config?.snapToGrid; + j && (q = { + x: Math.round(q.x / j[0]) * j[0], + y: Math.round(q.y / j[1]) * j[1] + }), F.controlPoints[Q] = q; + }, Z = () => { + document.removeEventListener("pointermove", G), document.removeEventListener("pointerup", Z), N.classList.remove("dragging"), U && W._emit("edge-control-point-change", { edge: F, action: "move", index: Q }); }; - document.addEventListener("pointermove", B), document.addEventListener("pointerup", z); + document.addEventListener("pointermove", G), document.addEventListener("pointerup", Z); }; - s.addEventListener("contextmenu", I), s.addEventListener("dblclick", M), s.addEventListener("pointerdown", A, !0); + s.addEventListener("contextmenu", D), s.addEventListener("dblclick", L), s.addEventListener("pointerdown", A, !0); let P = null; - const v = (L) => { - if (L.button !== 0) return; - L.stopPropagation(); - const $ = o(n); - if (!$) return; + const v = (S) => { + if (S.button !== 0) return; + S.stopPropagation(); + const N = o(n); + if (!N) return; const F = t.$data(e.closest("[x-data]")); if (!F) return; - const K = F._config?.reconnectSnapRadius ?? es, ne = F._config?.edgesReconnectable !== !1, G = $.reconnectable ?? !0; - let B = null; - if (ne && G !== !1 && x && b) { - const re = F.screenToFlowPosition(L.clientX, L.clientY), me = ct(re.x, re.y, x.x, x.y, K) || C && ct(re.x, re.y, C.x, C.y, K); - (ct(re.x, re.y, b.x, b.y, K) || E && ct(re.x, re.y, E.x, E.y, K)) && (G === !0 || G === "target") ? B = "target" : me && (G === !0 || G === "source") && (B = "source"); + const W = F._config?.reconnectSnapRadius ?? ns, Q = F._config?.edgesReconnectable !== !1, U = N.reconnectable ?? !0; + let G = null; + if (Q && U !== !1 && x && C) { + const ie = F.screenToFlowPosition(S.clientX, S.clientY), fe = ct(ie.x, ie.y, x.x, x.y, W) || E && ct(ie.x, ie.y, E.x, E.y, W); + (ct(ie.x, ie.y, C.x, C.y, W) || b && ct(ie.x, ie.y, b.x, b.y, W)) && (U === !0 || U === "target") ? G = "target" : fe && (U === !0 || U === "source") && (G = "source"); } - if (!B) { - const re = (me) => { - document.removeEventListener("pointerup", re), T(me); + if (!G) { + const ie = (fe) => { + document.removeEventListener("pointerup", ie), T(fe); }; - document.addEventListener("pointerup", re, { once: !0 }); + document.addEventListener("pointerup", ie, { once: !0 }); return; } - const z = L.clientX, Y = L.clientY; - let W = !1, j = !1, H = null; - const te = F._config?.connectionSnapRadius ?? 20; - let Q = null, U = null, J = null, se = z, oe = Y; - const Z = e.closest(".flow-container"); - if (!Z) return; - const ee = B === "target" ? x : b, ge = () => { - W = !0, s.classList.add("flow-edge-reconnecting"), F._emit("reconnect-start", { edge: $, handleType: B }), V("reconnect", `Reconnection drag started on edge "${$.id}" (${B} end)`), U = Wt({ + const Z = S.clientX, H = S.clientY; + let q = !1, j = !1, K = null; + const O = F._config?.connectionSnapRadius ?? 20; + let J = null, ne = null, Y = null, re = Z, se = H; + const oe = e.closest(".flow-container"); + if (!oe) return; + const ee = G === "target" ? x : C, te = () => { + q = !0, s.classList.add("flow-edge-reconnecting"), F._emit("reconnect-start", { edge: N, handleType: G }), B("reconnect", `Reconnection drag started on edge "${N.id}" (${G} end)`), ne = Ut({ connectionLineType: F._config?.connectionLineType, connectionLineStyle: F._config?.connectionLineStyle, connectionLine: F._config?.connectionLine, containerEl: s.closest(".flow-container") ?? void 0 - }), Q = U.svg; - const re = F.screenToFlowPosition(z, Y); - U.update({ + }), J = ne.svg; + const ie = F.screenToFlowPosition(Z, H); + ne.update({ fromX: ee.x, fromY: ee.y, - toX: re.x, - toY: re.y, - source: $.source, - sourceHandle: $.sourceHandle + toX: ie.x, + toY: ie.y, + source: N.source, + sourceHandle: N.sourceHandle }); - const me = Z.querySelector(".flow-viewport"); - me && me.appendChild(Q), B === "target" && (F.pendingConnection = { - source: $.source, - sourceHandle: $.sourceHandle, - position: re + const fe = oe.querySelector(".flow-viewport"); + fe && fe.appendChild(J), G === "target" && (F.pendingConnection = { + source: N.source, + sourceHandle: N.sourceHandle, + position: ie }), F._pendingReconnection = { - edge: $, - draggedEnd: B, + edge: N, + draggedEnd: G, anchorPosition: { ...ee }, - position: re - }, J = vo(Z, F, se, oe), B === "target" && bn(Z, $.source, $.sourceHandle ?? "source", F, $.id); - }, fe = (re) => { - if (se = re.clientX, oe = re.clientY, !W) { + position: ie + }, Y = _o(oe, F, re, se), G === "target" && En(oe, N.source, N.sourceHandle ?? "source", F, N.id); + }, he = (ie) => { + if (re = ie.clientX, se = ie.clientY, !q) { Math.sqrt( - (re.clientX - z) ** 2 + (re.clientY - Y) ** 2 - ) >= Qn && ge(); + (ie.clientX - Z) ** 2 + (ie.clientY - H) ** 2 + ) >= eo && te(); return; } - const me = F.screenToFlowPosition(re.clientX, re.clientY), ye = _n({ - containerEl: Z, - handleType: B === "target" ? "target" : "source", - excludeNodeId: B === "target" ? $.source : $.target, - cursorFlowPos: me, - connectionSnapRadius: te, - getNode: (Pe) => F.getNode(Pe), - toFlowPosition: (Pe, Ee) => F.screenToFlowPosition(Pe, Ee) + const fe = F.screenToFlowPosition(ie.clientX, ie.clientY), ge = xn({ + containerEl: oe, + handleType: G === "target" ? "target" : "source", + excludeNodeId: G === "target" ? N.source : N.target, + cursorFlowPos: fe, + connectionSnapRadius: O, + getNode: (Me) => F.getNode(Me), + toFlowPosition: (Me, Te) => F.screenToFlowPosition(Me, Te) }); - ye.element !== H && (H?.classList.remove("flow-handle-active"), ye.element?.classList.add("flow-handle-active"), H = ye.element), U?.update({ + ge.element !== K && (K?.classList.remove("flow-handle-active"), ge.element?.classList.add("flow-handle-active"), K = ge.element), ne?.update({ fromX: ee.x, fromY: ee.y, - toX: ye.position.x, - toY: ye.position.y, - source: $.source, - sourceHandle: $.sourceHandle + toX: ge.position.x, + toY: ge.position.y, + source: N.source, + sourceHandle: N.sourceHandle }); - const Oe = ye.position; - B === "target" && F.pendingConnection && (F.pendingConnection = { + const Ie = ge.position; + G === "target" && F.pendingConnection && (F.pendingConnection = { ...F.pendingConnection, - position: Oe + position: Ie }), F._pendingReconnection && (F._pendingReconnection = { ...F._pendingReconnection, - position: Oe - }), J?.updatePointer(re.clientX, re.clientY); - }, ie = () => { - j || (j = !0, document.removeEventListener("pointermove", fe), document.removeEventListener("pointerup", pe), J?.stop(), J = null, U?.destroy(), U = null, Q = null, H?.classList.remove("flow-handle-active"), P = null, s.classList.remove("flow-edge-reconnecting"), ke(Z), F.pendingConnection = null, F._pendingReconnection = null); - }, pe = async (re) => { - if (!W) { - ie(), T(re); + position: Ie + }), Y?.updatePointer(ie.clientX, ie.clientY); + }, de = () => { + j || (j = !0, document.removeEventListener("pointermove", he), document.removeEventListener("pointerup", ae), Y?.stop(), Y = null, ne?.destroy(), ne = null, J = null, K?.classList.remove("flow-handle-active"), P = null, s.classList.remove("flow-edge-reconnecting"), Pe(oe), F.pendingConnection = null, F._pendingReconnection = null); + }, ae = async (ie) => { + if (!q) { + de(), T(ie); return; } if (F._connectValidating) return; - let me = H, ye = null; - if (!me) { - ye = document.elementFromPoint(re.clientX, re.clientY); - const Me = B === "target" ? '[data-flow-handle-type="target"]' : '[data-flow-handle-type="source"]'; - me = ye?.closest(Me); + let fe = K, ge = null; + if (!fe) { + ge = document.elementFromPoint(ie.clientX, ie.clientY); + const Ee = G === "target" ? '[data-flow-handle-type="target"]' : '[data-flow-handle-type="source"]'; + fe = ge?.closest(Ee); } - const Pe = (me ? me.closest("[data-flow-node-id]") : ye?.closest("[data-flow-node-id]"))?.dataset.flowNodeId, Ee = me?.dataset.flowHandleId, he = U?.svg ?? null; - kt(he, !0); - let be; + const Me = (fe ? fe.closest("[data-flow-node-id]") : ge?.closest("[data-flow-node-id]"))?.dataset.flowNodeId, Te = fe?.dataset.flowHandleId, Le = ne?.svg ?? null; + kt(Le, !0); + let ue; try { - be = await ep({ - dropNodeId: Pe, - dropHandleId: Ee, - draggedEnd: B, - edge: $, + ue = await ip({ + dropNodeId: Me, + dropHandleId: Te, + draggedEnd: G, + edge: N, canvas: F, - containerEl: Z + containerEl: oe }); } finally { - kt(he, !1); + kt(Le, !1); } - be.applied ? V("reconnect", `Edge "${$.id}" reconnected (${B})`, be.newConnection) : V("reconnect", `Edge "${$.id}" reconnection cancelled — snapping back`, { reason: be.reason }), F._emit("reconnect-end", { edge: $, successful: be.applied }), ie(); + ue.applied ? B("reconnect", `Edge "${N.id}" reconnected (${G})`, ue.newConnection) : B("reconnect", `Edge "${N.id}" reconnection cancelled — snapping back`, { reason: ue.reason }), F._emit("reconnect-end", { edge: N, successful: ue.applied }), de(); }; - document.addEventListener("pointermove", fe), document.addEventListener("pointerup", pe), P = ie; + document.addEventListener("pointermove", he), document.addEventListener("pointerup", ae), P = de; }; s.addEventListener("pointerdown", v); - const w = (L) => { - const $ = o(n); - if (!$) return; + const w = (S) => { + const N = o(n); + if (!N) return; const F = t.$data(e.closest("[x-data]")); if (!F) return; - const K = F._config?.edgesReconnectable !== !1, ne = $.reconnectable ?? !0; - if (!K || ne === !1 || !x || !b) { + const W = F._config?.edgesReconnectable !== !1, Q = N.reconnectable ?? !0; + if (!W || Q === !1 || !x || !C) { s.style.removeProperty("cursor"), a.style.cursor = "pointer"; return; } - const G = F._config?.reconnectSnapRadius ?? es, B = F.screenToFlowPosition(L.clientX, L.clientY), z = (ct(B.x, B.y, x.x, x.y, G) || C && ct(B.x, B.y, C.x, C.y, G)) && (ne === !0 || ne === "source"), Y = (ct(B.x, B.y, b.x, b.y, G) || E && ct(B.x, B.y, E.x, E.y, G)) && (ne === !0 || ne === "target"); - z || Y ? (s.style.cursor = "grab", a.style.cursor = "grab") : (s.style.removeProperty("cursor"), a.style.cursor = "pointer"); + const U = F._config?.reconnectSnapRadius ?? ns, G = F.screenToFlowPosition(S.clientX, S.clientY), Z = (ct(G.x, G.y, x.x, x.y, U) || E && ct(G.x, G.y, E.x, E.y, U)) && (Q === !0 || Q === "source"), H = (ct(G.x, G.y, C.x, C.y, U) || b && ct(G.x, G.y, b.x, b.y, U)) && (Q === !0 || Q === "target"); + Z || H ? (s.style.cursor = "grab", a.style.cursor = "grab") : (s.style.removeProperty("cursor"), a.style.cursor = "pointer"); }; s.addEventListener("pointermove", w); - const N = (L) => { - if (L.key !== "Enter" && L.key !== " ") return; - L.preventDefault(), L.stopPropagation(); - const $ = o(n); - if (!$) return; + const I = (S) => { + if (S.key !== "Enter" && S.key !== " ") return; + S.preventDefault(), S.stopPropagation(); + const N = o(n); + if (!N) return; const F = t.$data(e.closest("[x-data]")); - F && (F._emit("edge-click", { edge: $, event: L }), mt(L, F._shortcuts?.multiSelect) ? F.selectedEdges.has($.id) ? (F.selectedEdges.delete($.id), $.selected = !1) : (F.selectedEdges.add($.id), $.selected = !0) : (F.deselectAll(), F.selectedEdges.add($.id), $.selected = !0), F._emitSelectionChange()); + F && (F._emit("edge-click", { edge: N, event: S }), mt(S, F._shortcuts?.multiSelect) ? F.selectedEdges.has(N.id) ? (F.selectedEdges.delete(N.id), N.selected = !1) : (F.selectedEdges.add(N.id), N.selected = !0) : (F.deselectAll(), F.selectedEdges.add(N.id), N.selected = !0), F._emitSelectionChange()); }; - s.addEventListener("keydown", N); - const k = () => { + s.addEventListener("keydown", I); + const M = () => { s.matches(":focus-visible") && s.classList.add("flow-edge-focused"); }, R = () => s.classList.remove("flow-edge-focused"); - s.addEventListener("focus", k), s.addEventListener("blur", R); - const O = (L) => { - L.stopPropagation(); + s.addEventListener("focus", M), s.addEventListener("blur", R); + const z = (S) => { + S.stopPropagation(); }; - s.addEventListener("mousedown", O); - const q = () => { - for (const L of [c, d, u]) - L && L.classList.add("flow-edge-hovered"); - }, D = () => { - for (const L of [c, d, u]) - L && L.classList.remove("flow-edge-hovered"); + s.addEventListener("mousedown", z); + const V = () => { + for (const S of [c, d, u]) + S && S.classList.add("flow-edge-hovered"); + }, $ = () => { + for (const S of [c, d, u]) + S && S.classList.remove("flow-edge-hovered"); }; - s.addEventListener("mouseenter", q), s.addEventListener("mouseleave", D), i(() => { - const L = o(n); - if (!L || !l) return; - s.setAttribute("data-flow-edge-id", L.id); - const $ = t.$data(e.closest("[x-data]")); - if (!$?.nodes) return; - const F = L.type ?? $._config?.defaultEdgeType ?? "bezier", K = $._config?.edgeLod; - let ne = F; - if (K) { - const X = $._zoomLevel; - (K.simplifyAt === "medium" && X === "medium" || X === "far") && (ne = "straight"); + s.addEventListener("mouseenter", V), s.addEventListener("mouseleave", $), i(() => { + const S = o(n); + if (!S || !l) return; + s.setAttribute("data-flow-edge-id", S.id); + const N = t.$data(e.closest("[x-data]")); + if (!N?.nodes) return; + const F = S.type ?? N._config?.defaultEdgeType ?? "bezier", W = N._config?.edgeLod; + let Q = F; + if (W) { + const X = N._zoomLevel; + (W.simplifyAt === "medium" && X === "medium" || X === "far") && (Q = "straight"); } - $._layoutAnimTick, $._edgeDirtyTicks?.get(L.id); - const G = $.getNode(L.source), B = $.getNode(L.target); - if (!G || !B) return; - G.sourcePosition, B.targetPosition; - const z = yt(G, $._nodeMap, $._config?.nodeOrigin), Y = yt(B, $._nodeMap, $._config?.nodeOrigin), W = e.closest("[x-data]"); - let j, H, te, Q; - const U = $._schemaMetrics, J = $._config?.nodeOrigin, se = F !== "floating" && $._config?.schemaHandleGeometry !== "dom" && U && (!J || J[0] === 0 && J[1] === 0) ? np( + N._layoutAnimTick, N._edgeDirtyTicks?.get(S.id); + const U = N.getNode(S.source), G = N.getNode(S.target); + if (!U || !G) return; + U.sourcePosition, G.targetPosition; + const Z = yt(U, N._nodeMap, N._config?.nodeOrigin), H = yt(G, N._nodeMap, N._config?.nodeOrigin), q = e.closest("[x-data]"); + let j, K, O, J; + const ne = N._schemaMetrics, Y = N._config?.nodeOrigin, re = F !== "floating" && N._config?.schemaHandleGeometry !== "dom" && ne && (!Y || Y[0] === 0 && Y[1] === 0) ? rp( + U, G, - B, - z, - Y, - L.sourceHandle, - L.targetHandle, - $._nodeElements, - U + Z, + H, + S.sourceHandle, + S.targetHandle, + N._nodeElements, + ne ) : null; if (F === "floating") { - const X = xa(z, Y); - j = X.sourcePos, H = X.targetPos, te = { x: X.sx, y: X.sy, handleWidth: 0, handleHeight: 0 }, Q = { x: X.tx, y: X.ty, handleWidth: 0, handleHeight: 0 }, x = { x: X.sx, y: X.sy }, b = { x: X.tx, y: X.ty }; - } else if (se) - j = se.sourcePos, H = se.targetPos, te = se.srcMeasurement, Q = se.tgtMeasurement, x = { x: te.x, y: te.y }, b = { x: Q.x, y: Q.y }; + const X = La(Z, H); + j = X.sourcePos, K = X.targetPos, O = { x: X.sx, y: X.sy, handleWidth: 0, handleHeight: 0 }, J = { x: X.tx, y: X.ty, handleWidth: 0, handleHeight: 0 }, x = { x: X.sx, y: X.sy }, C = { x: X.tx, y: X.ty }; + } else if (re) + j = re.sourcePos, K = re.targetPos, O = re.srcMeasurement, J = re.tgtMeasurement, x = { x: O.x, y: O.y }, C = { x: J.x, y: J.y }; else { - const X = $._nodeElements?.get(L.source) ?? W.querySelector(`[data-flow-node-id="${CSS.escape(L.source)}"]`), ae = $._nodeElements?.get(L.target) ?? W.querySelector(`[data-flow-node-id="${CSS.escape(L.target)}"]`), ue = X ? zs(X.getBoundingClientRect()) : void 0, ce = ae ? zs(ae.getBoundingClientRect()) : void 0; - j = uo(W, L.source, L.sourceHandle, "source", G, ce, X), H = uo(W, L.target, L.targetHandle, "target", B, ue, ae); - const le = t.raw($).viewport ?? { x: 0, y: 0, zoom: 1 }, de = le.zoom || 1, xe = G.rotation, Se = B.rotation; - j = Os(j, xe), H = Os(H, Se), te = Ys(W, L.source, z, L.sourceHandle, "source", de, le, ce, X), Q = Ys(W, L.target, Y, L.targetHandle, "target", de, le, ue, ae); - const Te = Ut(z, j, $._shapeRegistry, $._config?.nodeOrigin), Le = Ut(Y, H, $._shapeRegistry, $._config?.nodeOrigin); - x = te ?? Te, b = Q ?? Le; + const X = N._nodeElements?.get(S.source) ?? q.querySelector(`[data-flow-node-id="${CSS.escape(S.source)}"]`), le = N._nodeElements?.get(S.target) ?? q.querySelector(`[data-flow-node-id="${CSS.escape(S.target)}"]`), pe = X ? qs(X.getBoundingClientRect()) : void 0, we = le ? qs(le.getBoundingClientRect()) : void 0; + j = fo(q, S.source, S.sourceHandle, "source", U, we, X), K = fo(q, S.target, S.targetHandle, "target", G, pe, le); + const me = t.raw(N).viewport ?? { x: 0, y: 0, zoom: 1 }, ce = me.zoom || 1, ye = U.rotation, Se = G.rotation; + j = Bs(j, ye), K = Bs(K, Se), O = js(q, S.source, Z, S.sourceHandle, "source", ce, me, we, X), J = js(q, S.target, H, S.targetHandle, "target", ce, me, pe, le); + const De = Zt(Z, j, N._shapeRegistry, N._config?.nodeOrigin), xe = Zt(H, K, N._shapeRegistry, N._config?.nodeOrigin); + x = O ?? De, C = J ?? xe; } - let oe = Ht(te ?? x, j, te, L.markerStart), Z = Ht(Q ?? b, H, Q, L.markerEnd); + let se = Ft(O ?? x, j, O, S.markerStart), oe = Ft(J ?? C, K, J, S.markerEnd); if (F === "orthogonal" || F === "avoidant") { - const X = t.raw($._endpointSpreadGrouping); + const X = t.raw(N._endpointSpreadGrouping); if (X) { - const ae = io(G.endpointSpread ?? $._config?.avoidantEndpointSpread); - if (ae !== null) { - const ce = X.get(`${L.source}|${L.sourceHandle ?? ""}`), le = ce?.lanes.get(L.id); - if (ce && le !== void 0 && ce.count > 1) { - const de = $._schemaMetrics?.rowHeight ?? te?.handleHeight ?? 0; - oe = bs(oe, j, _s(le, ce.count, de, ae)); + const le = so(U.endpointSpread ?? N._config?.avoidantEndpointSpread); + if (le !== null) { + const we = X.get(`${S.source}|${S.sourceHandle ?? ""}`), me = we?.lanes.get(S.id); + if (we && me !== void 0 && we.count > 1) { + const ce = N._schemaMetrics?.rowHeight ?? O?.handleHeight ?? 0; + se = Es(se, j, xs(me, we.count, ce, le)); } } - const ue = io(B.endpointSpread ?? $._config?.avoidantEndpointSpread); - if (ue !== null) { - const ce = X.get(`${L.target}|${L.targetHandle ?? ""}`), le = ce?.lanes.get(L.id); - if (ce && le !== void 0 && ce.count > 1) { - const de = $._schemaMetrics?.rowHeight ?? Q?.handleHeight ?? 0; - Z = bs(Z, H, _s(le, ce.count, de, ue)); + const pe = so(G.endpointSpread ?? N._config?.avoidantEndpointSpread); + if (pe !== null) { + const we = X.get(`${S.target}|${S.targetHandle ?? ""}`), me = we?.lanes.get(S.id); + if (we && me !== void 0 && we.count > 1) { + const ce = N._schemaMetrics?.rowHeight ?? J?.handleHeight ?? 0; + oe = Es(oe, K, xs(me, we.count, ce, pe)); } } } } - C = oe, E = Z; + E = se, b = oe; let ee; if (F === "orthogonal" || F === "avoidant") - if ($._config?.avoidantSimplifyOnDrag !== !1 && ($._draggingNodeIds?.has(L.source) || $._draggingNodeIds?.has(L.target))) + if (N._config?.avoidantSimplifyOnDrag !== !1 && (N._draggingNodeIds?.has(S.source) || N._draggingNodeIds?.has(S.target))) ee = void 0; else { - const ae = t.raw($._obstacleSnapshot); - if (ae) - ee = ae.filter((ue) => ue.id !== L.source && ue.id !== L.target); + const le = t.raw(N._obstacleSnapshot); + if (le) + ee = le.filter((pe) => pe.id !== S.source && pe.id !== S.target); else { - const ue = t.raw($.nodes), ce = new Map(ue.map((de) => [de.id, de])), le = $._config?.nodeOrigin; - ee = ue.filter((de) => de.id !== L.source && de.id !== L.target).map((de) => { - const xe = yt(de, ce, le); + const pe = t.raw(N.nodes), we = new Map(pe.map((ce) => [ce.id, ce])), me = N._config?.nodeOrigin; + ee = pe.filter((ce) => ce.id !== S.source && ce.id !== S.target).map((ce) => { + const ye = yt(ce, we, me); return { - x: xe.position.x, - y: xe.position.y, - width: xe.dimensions?.width ?? we, - height: xe.dimensions?.height ?? _e + x: ye.position.x, + y: ye.position.y, + width: ye.dimensions?.width ?? ve, + height: ye.dimensions?.height ?? be }; }); } } - const ge = ne === F ? L : { ...L, type: ne }, { path: fe, labelPosition: ie } = lo(ge, z, Y, j, H, oe, Z, $._config?.edgeTypes, ee, $._shapeRegistry, $._config?.nodeOrigin, $._config?.defaultEdgeType); - l.setAttribute("d", fe), a.setAttribute("d", fe), (F === "orthogonal" || F === "avoidant") && t.raw($._edgeCorridors)?.set(L.id, { - minX: Math.min(oe.x, Z.x), - minY: Math.min(oe.y, Z.y), - maxX: Math.max(oe.x, Z.x), - maxY: Math.max(oe.y, Z.y) + const te = Q === F ? S : { ...S, type: Q }, { path: he, labelPosition: de } = co(te, Z, H, j, K, se, oe, N._config?.edgeTypes, ee, N._shapeRegistry, N._config?.nodeOrigin, N._config?.defaultEdgeType); + l.setAttribute("d", he), a.setAttribute("d", he), (F === "orthogonal" || F === "avoidant") && t.raw(N._edgeCorridors)?.set(S.id, { + minX: Math.min(se.x, oe.x), + minY: Math.min(se.y, oe.y), + maxX: Math.max(se.x, oe.x), + maxY: Math.max(se.y, oe.y) }); - const pe = F === "editable", re = pe && (L.showControlPoints || L.selected); - if (s.querySelectorAll(".flow-edge-control-point, .flow-edge-midpoint").forEach((X) => X.remove()), re) { - const X = L.controlPoints ?? [], ae = $.viewport?.zoom ?? 1, ue = 6 / ae, ce = 5 / ae, le = x ?? { x: 0, y: 0 }, de = b ?? { x: 0, y: 0 }, xe = [le, ...X, de], Se = xe.length - 1, Te = l.getTotalLength?.() ?? 0; - if (Te > 0) { - const Le = [0], ze = 200; - let Ie = 1; - for (let qe = 1; qe <= ze && Ie < xe.length; qe++) { - const Ln = qe / ze * Te, Kt = l.getPointAtLength(Ln), Ue = xe[Ie], Jt = Kt.x - Ue.x, Ii = Kt.y - Ue.y; - Jt * Jt + Ii * Ii < 25 && (Le.push(Ln), Ie++); + const ae = F === "editable", ie = ae && (S.showControlPoints || S.selected); + if (s.querySelectorAll(".flow-edge-control-point, .flow-edge-midpoint").forEach((X) => X.remove()), ie) { + const X = S.controlPoints ?? [], le = N.viewport?.zoom ?? 1, pe = 6 / le, we = 5 / le, me = x ?? { x: 0, y: 0 }, ce = C ?? { x: 0, y: 0 }, ye = [me, ...X, ce], Se = ye.length - 1, De = l.getTotalLength?.() ?? 0; + if (De > 0) { + const xe = [0], ze = 200; + let Tt = 1; + for (let Ce = 1; Ce <= ze && Tt < ye.length; Ce++) { + const Nt = Ce / ze * De, Qt = l.getPointAtLength(Nt), Ue = ye[Tt], en = Qt.x - Ue.x, Ri = Qt.y - Ue.y; + en * en + Ri * Ri < 25 && (xe.push(Nt), Tt++); } - for (; Le.length <= Se; ) - Le.push(Te); - for (let qe = 0; qe < Se; qe++) { - const Ln = (Le[qe] + Le[qe + 1]) / 2, Kt = l.getPointAtLength(Ln), Ue = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - Ue.classList.add("flow-edge-midpoint"), Ue.setAttribute("cx", String(Kt.x)), Ue.setAttribute("cy", String(Kt.y)), Ue.setAttribute("r", String(ce)), Ue.dataset.segmentIndex = String(qe); - const Jt = document.createElementNS("http://www.w3.org/2000/svg", "title"); - Jt.textContent = "Double-click to add control point", Ue.appendChild(Jt), s.appendChild(Ue); + for (; xe.length <= Se; ) + xe.push(De); + for (let Ce = 0; Ce < Se; Ce++) { + const Nt = (xe[Ce] + xe[Ce + 1]) / 2, Qt = l.getPointAtLength(Nt), Ue = document.createElementNS("http://www.w3.org/2000/svg", "circle"); + Ue.classList.add("flow-edge-midpoint"), Ue.setAttribute("cx", String(Qt.x)), Ue.setAttribute("cy", String(Qt.y)), Ue.setAttribute("r", String(we)), Ue.dataset.segmentIndex = String(Ce); + const en = document.createElementNS("http://www.w3.org/2000/svg", "title"); + en.textContent = "Double-click to add control point", Ue.appendChild(en), s.appendChild(Ue); } } - for (let Le = 0; Le < X.length; Le++) { + for (let xe = 0; xe < X.length; xe++) { const ze = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - ze.classList.add("flow-edge-control-point"), ze.setAttribute("cx", String(X[Le].x)), ze.setAttribute("cy", String(X[Le].y)), ze.setAttribute("r", String(ue)), ze.dataset.pointIndex = String(Le), s.appendChild(ze); + ze.classList.add("flow-edge-control-point"), ze.setAttribute("cx", String(X[xe].x)), ze.setAttribute("cy", String(X[xe].y)), ze.setAttribute("r", String(pe)), ze.dataset.pointIndex = String(xe), s.appendChild(ze); } } - if (a.style.cursor = pe ? "crosshair" : "pointer", a.style.strokeWidth = String( - L.interactionWidth ?? $._config?.defaultInteractionWidth ?? 20 - ), L.markerStart != null) { - const X = Ot(L.markerStart), ae = zt(X, $._id); - l.setAttribute("marker-start", `url(#${ae})`); - } else if (L._renderDualMarker && L.markerEnd) { - const X = Ot(L.markerEnd), ae = zt(X, $._id); - l.setAttribute("marker-start", `url(#${ae})`); + if (a.style.cursor = ae ? "crosshair" : "pointer", a.style.strokeWidth = String( + S.interactionWidth ?? N._config?.defaultInteractionWidth ?? 20 + ), S.markerStart != null) { + const X = Vt(S.markerStart), le = Bt(X, N._id); + l.setAttribute("marker-start", `url(#${le})`); + } else if (S._renderDualMarker && S.markerEnd) { + const X = Vt(S.markerEnd), le = Bt(X, N._id); + l.setAttribute("marker-start", `url(#${le})`); } else l.removeAttribute("marker-start"); - if (L.markerEnd) { - const X = Ot(L.markerEnd), ae = zt(X, $._id); - l.setAttribute("marker-end", `url(#${ae})`); + if (S.markerEnd) { + const X = Vt(S.markerEnd), le = Bt(X, N._id); + l.setAttribute("marker-end", `url(#${le})`); } else l.removeAttribute("marker-end"); - const me = L.strokeWidth ?? 1.5, ye = tp(L.animated); - switch (ye !== g && (l.classList.remove("flow-edge-animated", "flow-edge-pulse"), g === "dot" && S(), g = ye), ye) { + const fe = S.strokeWidth ?? 1.5, ge = sp(S.animated); + switch (ge !== g && (l.classList.remove("flow-edge-animated", "flow-edge-pulse"), g === "dot" && k(), g = ge), ge) { case "dash": l.classList.add("flow-edge-animated"); break; @@ -11929,98 +11942,98 @@ function sp(t) { l.classList.add("flow-edge-pulse"); break; case "dot": - _(s, fe, W, L, L.animationDuration); + _(s, he, q, S, S.animationDuration); break; } - if (L.animationDuration && ye !== "none" ? (ye === "dash" || ye === "pulse") && (l.style.animationDuration = L.animationDuration) : (ye === "dash" || ye === "pulse") && l.style.removeProperty("animation-duration"), y && y !== L.class && s.classList.remove(...y.split(" ").filter(Boolean)), L.class) { - const X = ye === "dash" ? " flow-edge-animated" : ye === "pulse" ? " flow-edge-pulse" : ""; - l.setAttribute("class", L.class + X), s.classList.add(...L.class.split(" ").filter(Boolean)), y = L.class; + if (S.animationDuration && ge !== "none" ? (ge === "dash" || ge === "pulse") && (l.style.animationDuration = S.animationDuration) : (ge === "dash" || ge === "pulse") && l.style.removeProperty("animation-duration"), y && y !== S.class && s.classList.remove(...y.split(" ").filter(Boolean)), S.class) { + const X = ge === "dash" ? " flow-edge-animated" : ge === "pulse" ? " flow-edge-pulse" : ""; + l.setAttribute("class", S.class + X), s.classList.add(...S.class.split(" ").filter(Boolean)), y = S.class; } else y && (s.classList.remove(...y.split(" ").filter(Boolean)), y = null); - if (s.setAttribute("aria-selected", String(!!L.selected)), L.selected) - s.classList.add("flow-edge-selected"), l.style.strokeWidth = String(Math.max(me + 1, 2.5)), l.style.stroke = "var(--flow-edge-stroke-selected, " + yn + ")"; + if (s.setAttribute("aria-selected", String(!!S.selected)), S.selected) + s.classList.add("flow-edge-selected"), l.style.strokeWidth = String(Math.max(fe + 1, 2.5)), l.style.stroke = "var(--flow-edge-stroke-selected, " + vn + ")"; else { - s.classList.remove("flow-edge-selected"), l.style.strokeWidth = String(me); - const X = $._markerDefsEl?.querySelector("defs") ?? null; - if (Ea(L.color)) { + s.classList.remove("flow-edge-selected"), l.style.strokeWidth = String(fe); + const X = N._markerDefsEl?.querySelector("defs") ?? null; + if (Pa(S.color)) { if (X) { - const ae = Ca($._id, L.id), ue = L.gradientDirection === "target-source", ce = x.x, le = x.y, de = b.x, xe = b.y; - Sa( + const le = Ma(N._id, S.id), pe = S.gradientDirection === "target-source", we = x.x, me = x.y, ce = C.x, ye = C.y; + Ta( X, - ae, - ue ? { from: L.color.to, to: L.color.from } : L.color, - ce, le, - de, - xe - ), l.style.stroke = `url(#${ae})`, m = ae; + pe ? { from: S.color.to, to: S.color.from } : S.color, + we, + me, + ce, + ye + ), l.style.stroke = `url(#${le})`, m = le; } - } else if (L.color) { + } else if (S.color) { if (m) { - const ae = X; - ae && Io(ae, m), m = null; + const le = X; + le && Ro(le, m), m = null; } - l.style.stroke = L.color; + l.style.stroke = S.color; } else { if (m) { - const ae = X; - ae && Io(ae, m), m = null; + const le = X; + le && Ro(le, m), m = null; } l.style.removeProperty("stroke"); } } - if (!L.selected && ((L.sourceHandle ? $.selectedRows?.has(L.sourceHandle.replace(/-[lr]$/, "")) : !1) || (L.targetHandle ? $.selectedRows?.has(L.targetHandle.replace(/-[lr]$/, "")) : !1)) ? (s.classList.add("flow-edge-row-highlighted"), L.selected || (l.style.strokeWidth = String(Math.max(me + 0.5, 2)), l.style.stroke = getComputedStyle(s.closest(".flow-container")).getPropertyValue("--flow-edge-row-highlight-color").trim() || "#3b82f6")) : s.classList.remove("flow-edge-row-highlighted"), L.focusable ?? $._config?.edgesFocusable !== !1 ? (s.setAttribute("tabindex", "0"), s.setAttribute("role", L.ariaRole ?? "group"), s.setAttribute("aria-label", L.ariaLabel ?? (L.label ? `Edge: ${L.label}` : `Edge from ${L.source} to ${L.target}`))) : (s.removeAttribute("tabindex"), s.removeAttribute("role"), s.removeAttribute("aria-label")), L.domAttributes) - for (const [X, ae] of Object.entries(L.domAttributes)) - X.startsWith("on") || Qg.has(X.toLowerCase()) || s.setAttribute(X, ae); - const Ee = (X, ae, ue, ce, le) => { - if (ae) { - if (!X && ce) { - const de = ue.includes("flow-edge-label-start"), xe = ue.includes("flow-edge-label-end"); - let Se = `[data-flow-edge-id="${le}"].flow-edge-label`; - de ? Se += ".flow-edge-label-start" : xe ? Se += ".flow-edge-label-end" : Se += ":not(.flow-edge-label-start):not(.flow-edge-label-end)", X = ce.querySelector(Se); + if (!S.selected && ((S.sourceHandle ? N.selectedRows?.has(S.sourceHandle.replace(/-[lr]$/, "")) : !1) || (S.targetHandle ? N.selectedRows?.has(S.targetHandle.replace(/-[lr]$/, "")) : !1)) ? (s.classList.add("flow-edge-row-highlighted"), S.selected || (l.style.strokeWidth = String(Math.max(fe + 0.5, 2)), l.style.stroke = getComputedStyle(s.closest(".flow-container")).getPropertyValue("--flow-edge-row-highlight-color").trim() || "#3b82f6")) : s.classList.remove("flow-edge-row-highlighted"), S.focusable ?? N._config?.edgesFocusable !== !1 ? (s.setAttribute("tabindex", "0"), s.setAttribute("role", S.ariaRole ?? "group"), s.setAttribute("aria-label", S.ariaLabel ?? (S.label ? `Edge: ${S.label}` : `Edge from ${S.source} to ${S.target}`))) : (s.removeAttribute("tabindex"), s.removeAttribute("role"), s.removeAttribute("aria-label")), S.domAttributes) + for (const [X, le] of Object.entries(S.domAttributes)) + X.startsWith("on") || op.has(X.toLowerCase()) || s.setAttribute(X, le); + const Te = (X, le, pe, we, me) => { + if (le) { + if (!X && we) { + const ce = pe.includes("flow-edge-label-start"), ye = pe.includes("flow-edge-label-end"); + let Se = `[data-flow-edge-id="${me}"].flow-edge-label`; + ce ? Se += ".flow-edge-label-start" : ye ? Se += ".flow-edge-label-end" : Se += ":not(.flow-edge-label-start):not(.flow-edge-label-end)", X = we.querySelector(Se); } - return X || (X = document.createElement("div"), X.className = ue, X.dataset.flowEdgeId = le, ce && ce.appendChild(X)), X.textContent = ae, X; + return X || (X = document.createElement("div"), X.className = pe, X.dataset.flowEdgeId = me, we && we.appendChild(X)), X.textContent = le, X; } return X && X.remove(), null; - }, he = e.closest(".flow-viewport"), be = L.labelVisibility ?? "always", Me = () => { + }, Le = e.closest(".flow-viewport"), ue = S.labelVisibility ?? "always", Ee = () => { const X = l.getAttribute("d") ?? ""; return X !== f && (f = X, h = typeof l.getTotalLength == "function" && l.getTotalLength() || 0), h; }; - if (c = Ee(c, L.label, "flow-edge-label", he, L.id), c) { - const X = Me(); + if (c = Te(c, S.label, "flow-edge-label", Le, S.id), c) { + const X = Ee(); if (X > 0) { - const ae = L.labelPosition ?? 0.5, ue = op(l, ae, X); - c.style.left = `${ue.x}px`, c.style.top = `${ue.y}px`; + const le = S.labelPosition ?? 0.5, pe = ap(l, le, X); + c.style.left = `${pe.x}px`, c.style.top = `${pe.y}px`; } else - c.style.left = `${ie.x}px`, c.style.top = `${ie.y}px`; + c.style.left = `${de.x}px`, c.style.top = `${de.y}px`; } - if (d = Ee(d, L.labelStart, "flow-edge-label flow-edge-label-start", he, L.id), d) { - const X = Me(); + if (d = Te(d, S.labelStart, "flow-edge-label flow-edge-label-start", Le, S.id), d) { + const X = Ee(); if (X > 0) { - const ae = L.labelStartOffset ?? 30, ue = l.getPointAtLength(Math.min(ae, X / 2)); - d.style.left = `${ue.x}px`, d.style.top = `${ue.y}px`; + const le = S.labelStartOffset ?? 30, pe = l.getPointAtLength(Math.min(le, X / 2)); + d.style.left = `${pe.x}px`, d.style.top = `${pe.y}px`; } } - if (u = Ee(u, L.labelEnd, "flow-edge-label flow-edge-label-end", he, L.id), u) { - const X = Me(); + if (u = Te(u, S.labelEnd, "flow-edge-label flow-edge-label-end", Le, S.id), u) { + const X = Ee(); if (X > 0) { - const ae = L.labelEndOffset ?? 30, ue = l.getPointAtLength(Math.max(X - ae, X / 2)); - u.style.left = `${ue.x}px`, u.style.top = `${ue.y}px`; + const le = S.labelEndOffset ?? 30, pe = l.getPointAtLength(Math.max(X - le, X / 2)); + u.style.left = `${pe.x}px`, u.style.top = `${pe.y}px`; } } for (const X of [c, d, u]) - X && (X.classList.toggle("flow-edge-label-hover", be === "hover"), X.classList.toggle("flow-edge-label-on-select", be === "selected"), X.classList.toggle("flow-edge-label-selected", !!L.selected), L.class ? X.classList.add(...L.class.split(" ").filter(Boolean)) : y && X.classList.remove(...y.split(" ").filter(Boolean))); + X && (X.classList.toggle("flow-edge-label-hover", ue === "hover"), X.classList.toggle("flow-edge-label-on-select", ue === "selected"), X.classList.toggle("flow-edge-label-selected", !!S.selected), S.class ? X.classList.add(...S.class.split(" ").filter(Boolean)) : y && X.classList.remove(...y.split(" ").filter(Boolean))); }), r(() => { if (m) { - const $ = t.$data(e.closest("[x-data]"))?._markerDefsEl?.querySelector("defs"); - $ && Io($, m); + const N = t.$data(e.closest("[x-data]"))?._markerDefsEl?.querySelector("defs"); + N && Ro(N, m); } - P?.(), S(), s.removeEventListener("contextmenu", I), s.removeEventListener("dblclick", M), s.removeEventListener("pointerdown", A, !0), s.removeEventListener("pointerdown", v), s.removeEventListener("pointermove", w), s.removeEventListener("keydown", N), s.removeEventListener("focus", k), s.removeEventListener("blur", R), s.removeEventListener("mousedown", O), s.removeEventListener("mouseenter", q), s.removeEventListener("mouseleave", D), c?.remove(), d?.remove(), u?.remove(); + P?.(), k(), s.removeEventListener("contextmenu", D), s.removeEventListener("dblclick", L), s.removeEventListener("pointerdown", A, !0), s.removeEventListener("pointerdown", v), s.removeEventListener("pointermove", w), s.removeEventListener("keydown", I), s.removeEventListener("focus", M), s.removeEventListener("blur", R), s.removeEventListener("mousedown", z), s.removeEventListener("mouseenter", V), s.removeEventListener("mouseleave", $), c?.remove(), d?.remove(), u?.remove(); }); } ); } -function rp(t, e) { +function dp(t, e) { return { /** Write node positions directly to DOM elements (bypassing Alpine effects). */ _flushNodePositions(n) { @@ -12042,7 +12055,7 @@ function rp(t, e) { if (!r) continue; const a = e.raw(i).style; if (!a) continue; - const l = typeof a == "string" ? wn(a) : a; + const l = typeof a == "string" ? _n(a) : a; for (const [c, d] of Object.entries(l)) r.style.setProperty(c, d); } @@ -12073,31 +12086,31 @@ function rp(t, e) { const s = yt(i, t._nodeMap, t._config.nodeOrigin), a = yt(r, t._nodeMap, t._config.nodeOrigin); let l, c, d, u; if (o.type === "floating") { - const h = xa(s, a); + const h = La(s, a); d = { x: h.sx, y: h.sy }, u = { x: h.tx, y: h.ty }; - const p = Ht(d, h.sourcePos, null, o.markerStart), g = Ht(u, h.targetPos, null, o.markerEnd), m = lo(o, s, a, h.sourcePos, h.targetPos, p, g, void 0, void 0, t._shapeRegistry, t._config.nodeOrigin); + const p = Ft(d, h.sourcePos, null, o.markerStart), g = Ft(u, h.targetPos, null, o.markerEnd), m = co(o, s, a, h.sourcePos, h.targetPos, p, g, void 0, void 0, t._shapeRegistry, t._config.nodeOrigin); l = m.path, c = m.labelPosition; } else { const h = t._container; let p, g; if (h) { - const b = h.querySelector( + const C = h.querySelector( `[data-flow-node-id="${CSS.escape(o.source)}"]` - ), C = h.querySelector( + ), E = h.querySelector( `[data-flow-node-id="${CSS.escape(o.target)}"]` ); - if (b) { - const E = b.getBoundingClientRect(); - p = { x: (E.left + E.right) / 2, y: (E.top + E.bottom) / 2 }; - } if (C) { - const E = C.getBoundingClientRect(); - g = { x: (E.left + E.right) / 2, y: (E.top + E.bottom) / 2 }; + const b = C.getBoundingClientRect(); + p = { x: (b.left + b.right) / 2, y: (b.top + b.bottom) / 2 }; + } + if (E) { + const b = E.getBoundingClientRect(); + g = { x: (b.left + b.right) / 2, y: (b.top + b.bottom) / 2 }; } } - const m = h ? uo(h, o.source, o.sourceHandle, "source", i, g) : i?.sourcePosition ?? "bottom", y = h ? uo(h, o.target, o.targetHandle, "target", r, p) : r?.targetPosition ?? "top"; - d = Ut(s, m, t._shapeRegistry, t._config.nodeOrigin), u = Ut(a, y, t._shapeRegistry, t._config.nodeOrigin); - const _ = Ht(d, m, null, o.markerStart), S = Ht(u, y, null, o.markerEnd), x = lo(o, s, a, m, y, _, S, void 0, void 0, t._shapeRegistry, t._config.nodeOrigin); + const m = h ? fo(h, o.source, o.sourceHandle, "source", i, g) : i?.sourcePosition ?? "bottom", y = h ? fo(h, o.target, o.targetHandle, "target", r, p) : r?.targetPosition ?? "top"; + d = Zt(s, m, t._shapeRegistry, t._config.nodeOrigin), u = Zt(a, y, t._shapeRegistry, t._config.nodeOrigin); + const _ = Ft(d, m, null, o.markerStart), k = Ft(u, y, null, o.markerEnd), x = co(o, s, a, m, y, _, k, void 0, void 0, t._shapeRegistry, t._config.nodeOrigin); l = x.path, c = x.labelPosition; } const f = t.getEdgePathElement(o.id); @@ -12106,11 +12119,11 @@ function rp(t, e) { const p = f.parentElement?.querySelector("path:first-child"); p && p !== f && p.setAttribute("d", l); } - if (Ea(o.color)) { + if (Pa(o.color)) { const h = t._markerDefsEl?.querySelector("defs"); if (h) { - const p = Ca(t._id, o.id), g = o.gradientDirection === "target-source"; - Sa( + const p = Ma(t._id, o.id), g = o.gradientDirection === "target-source"; + Ta( h, p, g ? { from: o.color.to, to: o.color.from } : o.color, @@ -12161,7 +12174,7 @@ function rp(t, e) { } }; } -function ap(t) { +function up(t) { return { _applyConfigPatch(e) { const n = t._config; @@ -12187,7 +12200,7 @@ function ap(t) { t._patternColorOverride = i, t._container && t._container.style.setProperty("--flow-bg-pattern-color", i); break; case "debug": - Fr(!!i); + qr(!!i); break; case "preventOverlap": t._config.preventOverlap = i; @@ -12208,7 +12221,7 @@ function ap(t) { t._loadingText = i; break; case "colorMode": - t._config.colorMode = i, i && t._container ? t._colorModeHandle ? t._colorModeHandle.update(i) : t._colorModeHandle = fa(t._container, i) : !i && t._colorModeHandle && (t._colorModeHandle.destroy(), t._colorModeHandle = null); + t._config.colorMode = i, i && t._container ? t._colorModeHandle ? t._colorModeHandle.update(i) : t._colorModeHandle = ya(t._container, i) : !i && t._colorModeHandle && (t._colorModeHandle.destroy(), t._colorModeHandle = null); break; case "autoLayout": n.autoLayout = i || void 0, t._autoLayoutFailed = !1, i ? (t._autoLayoutReady = !0, t._scheduleAutoLayout()) : (t._autoLayoutReady = !1, t._autoLayoutTimer && (clearTimeout(t._autoLayoutTimer), t._autoLayoutTimer = null)); @@ -12217,8 +12230,8 @@ function ap(t) { } }; } -let lp = 0; -function cp(t, e) { +let fp = 0; +function hp(t, e) { switch (t) { case "lines": case "cross": @@ -12227,15 +12240,15 @@ function cp(t, e) { return `radial-gradient(circle, ${e} 1px, transparent 1px)`; } } -function dp(t, e) { +function gp(t, e) { return t ? !(t.maxX < e.minX || t.minX > e.maxX || t.maxY < e.minY || t.minY > e.maxY) : !0; } -function up(t) { +function pp(t) { t.data("flowCanvas", (e = {}) => { const n = { // ── Reactive State ──────────────────────────────────────────────── /** Unique instance ID for SVG marker dedup, etc. */ - _id: `flow-${++lp}`, + _id: `flow-${++fp}`, nodes: e.nodes ?? [], edges: e.edges ?? [], viewport: { @@ -12288,7 +12301,7 @@ function up(t) { event: null }, // ── Shape Registry ───────────────────────────────────────────────── - _shapeRegistry: { ...ua, ...e.shapeTypes }, + _shapeRegistry: { ...ma, ...e.shapeTypes }, // ── Background ──────────────────────────────────────────────────── _background: e.background ?? "dots", _backgroundGap: e.backgroundGap ?? null, @@ -12344,7 +12357,7 @@ function up(t) { const a = this.viewport.zoom, l = this.viewport.x, c = this.viewport.y, d = [], u = [], f = []; for (const h of s) { const p = h.gap * a, g = h.variant === "cross" ? p / 2 : p; - d.push(cp(h.variant, h.color)), h.variant === "lines" || h.variant === "cross" ? (u.push(`${g}px ${g}px, ${g}px ${g}px`), f.push(`${l}px ${c}px, ${l}px ${c}px`)) : (u.push(`${p}px ${p}px`), f.push(`${l}px ${c}px`)); + d.push(hp(h.variant, h.color)), h.variant === "lines" || h.variant === "cross" ? (u.push(`${g}px ${g}px, ${g}px ${g}px`), f.push(`${l}px ${c}px, ${l}px ${c}px`)) : (u.push(`${p}px ${p}px`), f.push(`${l}px ${c}px`)); } return { backgroundImage: d.join(", "), @@ -12360,7 +12373,7 @@ function up(t) { const { collab: s, ...a } = e; return a; })(), - _shortcuts: Wf(e.keyboardShortcuts), + _shortcuts: Zf(e.keyboardShortcuts), _container: null, _panZoom: null, _onKeyDown: null, @@ -12426,7 +12439,7 @@ function up(t) { _vpMoved: !1, _history: null, _announcer: null, - _computeEngine: new Eh(), + _computeEngine: new Lh(), _computeDebounceTimer: null, _animationLocked: !1, _activeTimelines: /* @__PURE__ */ new Set(), @@ -12458,7 +12471,7 @@ function up(t) { _layoutAnimFrame: 0, // ── Shared obstacle cache (Workstream C) ───────────────────────────── /** Spatial index of node id → flow-space rect cells (committed geometry). Non-reactive: operate on it via `Alpine.raw(canvas._spatialGrid)` (raw the NESTED property — `Alpine.raw(canvas)` does NOT unwrap Alpine's merge-scope proxy; see flow-edge.ts). Also maintained for the viewport-culling overhaul (WS E), which will consume it — do not delete as dead code even though only WS C currently reads it. */ - _spatialGrid: new Qu(), + _spatialGrid: new of(), /** Obstacle rects rebuilt once per commit. In the edge effect read it via `Alpine.raw(canvas._obstacleSnapshot)` (nested-raw) so the edge does NOT subscribe to every node's reactive state. */ _obstacleSnapshot: null, /** WS-2 endpoint-spread lanes; null until first computed. Mutated in place (see _obstacleSnapshot). */ @@ -12500,7 +12513,7 @@ function up(t) { * dispatch a DOM CustomEvent (flow-xxx) for Alpine @flow-xxx listeners. */ _emit(s, a) { - s !== "viewport-change" && s !== "viewport-move" && V("event", s, a); + s !== "viewport-change" && s !== "viewport-move" && B("event", s, a); const l = "on" + s.split("-").map( (d) => d.charAt(0).toUpperCase() + d.slice(1) ).join(""), c = e[l]; @@ -12523,7 +12536,7 @@ function up(t) { }); }, _rebuildNodeMap() { - this._nodeMap = la(this.nodes), dh(this._childrenIds, this.nodes); + this._nodeMap = ha(this.nodes), gh(this._childrenIds, this.nodes); }, _rebuildEdgeMap() { this._edgeMap = new Map(this.edges.map((s) => [s.id, s])); @@ -12553,8 +12566,8 @@ function up(t) { id: g.id, x: m.position.x, y: m.position.y, - width: m.dimensions?.width ?? we, - height: m.dimensions?.height ?? _e + width: m.dimensions?.width ?? ve, + height: m.dimensions?.height ?? be }; f.insert(g.id, y.x, y.y, y.width, y.height), !g.hidden && h.push(y); } @@ -12572,41 +12585,41 @@ function up(t) { * edge effect doesn't re-run every edge — mirrors `_obstacleSnapshot`. */ _computeEndpointGrouping() { - const s = /* @__PURE__ */ new Set(), a = io(this._config?.avoidantEndpointSpread), l = t.raw(this.nodes), c = new Map(l.map((_) => [_.id, _])), d = this._config?.nodeOrigin, u = (_) => { - const S = c.get(_); - if (!S) return 0; - const x = yt(S, c, d); + const s = /* @__PURE__ */ new Set(), a = so(this._config?.avoidantEndpointSpread), l = t.raw(this.nodes), c = new Map(l.map((_) => [_.id, _])), d = this._config?.nodeOrigin, u = (_) => { + const k = c.get(_); + if (!k) return 0; + const x = yt(k, c, d); return x.position.y + (x.dimensions?.height ?? 0) / 2; }, f = (_) => { - const S = c.get(_)?.endpointSpread; - return S !== void 0 ? io(S) !== null : a !== null; - }, h = /* @__PURE__ */ new Map(), p = (_, S, x, b) => { + const k = c.get(_)?.endpointSpread; + return k !== void 0 ? so(k) !== null : a !== null; + }, h = /* @__PURE__ */ new Map(), p = (_, k, x, C) => { if (!f(_)) return; - const C = `${_}|${S ?? ""}`; - let E = h.get(C); - E || (E = [], h.set(C, E)), E.push({ edgeId: x, sortKey: u(b) }); + const E = `${_}|${k ?? ""}`; + let b = h.get(E); + b || (b = [], h.set(E, b)), b.push({ edgeId: x, sortKey: u(C) }); }, g = t.raw(this.edges); for (const _ of g) { - const S = _.type ?? this._config?.defaultEdgeType; - S !== "avoidant" && S !== "orthogonal" || (p(_.source, _.sourceHandle, _.id, _.target), p(_.target, _.targetHandle, _.id, _.source)); + const k = _.type ?? this._config?.defaultEdgeType; + k !== "avoidant" && k !== "orthogonal" || (p(_.source, _.sourceHandle, _.id, _.target), p(_.target, _.targetHandle, _.id, _.source)); } const m = t.raw(this._endpointSpreadGrouping), y = /* @__PURE__ */ new Map(); - for (const [_, S] of h) { - S.sort((C, E) => C.sortKey - E.sortKey || (C.edgeId < E.edgeId ? -1 : 1)); + for (const [_, k] of h) { + k.sort((E, b) => E.sortKey - b.sortKey || (E.edgeId < b.edgeId ? -1 : 1)); const x = /* @__PURE__ */ new Map(); - S.forEach((C, E) => x.set(C.edgeId, E)), y.set(_, { count: S.length, lanes: x }); - const b = m?.get(_); - for (const [C, E] of x) - (!b || b.count !== S.length || b.lanes.get(C) !== E) && s.add(C); + k.forEach((E, b) => x.set(E.edgeId, b)), y.set(_, { count: k.length, lanes: x }); + const C = m?.get(_); + for (const [E, b] of x) + (!C || C.count !== k.length || C.lanes.get(E) !== b) && s.add(E); } if (m) { - for (const [_, S] of m) + for (const [_, k] of m) if (!y.has(_)) - for (const x of S.lanes.keys()) s.add(x); + for (const x of k.lanes.keys()) s.add(x); } if (m) { m.clear(); - for (const [_, S] of y) m.set(_, S); + for (const [_, k] of y) m.set(_, k); } else this._endpointSpreadGrouping = y; return s; @@ -12649,9 +12662,9 @@ function up(t) { } const p = new Set(s), g = []; for (const m of p) { - const y = f?.find((S) => S.id === m); + const y = f?.find((k) => k.id === m); y && g.push(y); - const _ = a?.find((S) => S.id === m); + const _ = a?.find((k) => k.id === m); _ && g.push(_); } for (const m of d) { @@ -12659,8 +12672,8 @@ function up(t) { if (!y) { const _ = u.get(m.id); if (_) { - for (const S of g) - if (S.x < _.maxX + ht && S.x + S.width > _.minX - ht && S.y < _.maxY + ht && S.y + S.height > _.minY - ht) { + for (const k of g) + if (k.x < _.maxX + ht && k.x + k.width > _.minX - ht && k.y < _.maxY + ht && k.y + k.height > _.minY - ht) { y = !0; break; } @@ -12767,29 +12780,29 @@ function up(t) { if (this._cullingWasActive = !0, !this._container) return; const l = this._container.clientWidth, c = this._container.clientHeight; if (l === 0 || c === 0) return; - const d = e.cullingBuffer ?? 100, u = Ju(this.viewport, l, c, d), h = t.raw(this._spatialGrid).query(u), p = this._draggingNodeIds, g = /* @__PURE__ */ new Set(), m = (S) => { - const x = this._nodeMap.get(S); + const d = e.cullingBuffer ?? 100, u = nf(this.viewport, l, c, d), h = t.raw(this._spatialGrid).query(u), p = this._draggingNodeIds, g = /* @__PURE__ */ new Set(), m = (k) => { + const x = this._nodeMap.get(k); if (!x || x.hidden) return; - const b = x.dimensions?.width ?? 150, C = x.dimensions?.height ?? 50, E = x.parentId ? ni(x, this._nodeMap, this._config.nodeOrigin) : x.position; - !(E.x + b < u.minX || E.x > u.maxX || E.y + C < u.minY || E.y > u.maxY) && g.add(S); + const C = x.dimensions?.width ?? 150, E = x.dimensions?.height ?? 50, b = x.parentId ? ii(x, this._nodeMap, this._config.nodeOrigin) : x.position; + !(b.x + C < u.minX || b.x > u.maxX || b.y + E < u.minY || b.y > u.maxY) && g.add(k); }; - for (const S of h) m(S); + for (const k of h) m(k); if (p) - for (const S of p) - h.has(S) || m(S); - for (const [S, x] of this._nodeElements) { - const b = g.has(S) ? "" : "none"; - x.style.display !== b && (x.style.display = b); + for (const k of p) + h.has(k) || m(k); + for (const [k, x] of this._nodeElements) { + const C = g.has(k) ? "" : "none"; + x.style.display !== C && (x.style.display = C); } const y = this._culledEdgeIds, _ = /* @__PURE__ */ new Set(); - for (const [S, x] of this._edgeSvgElements) { - const b = this._edgeMap.get(S); - if (!b) continue; - const C = this._nodeMap.get(b.source)?.hidden, E = this._nodeMap.get(b.target)?.hidden; - if (b.hidden || b._hiddenByCollapse || C || E) + for (const [k, x] of this._edgeSvgElements) { + const C = this._edgeMap.get(k); + if (!C) continue; + const E = this._nodeMap.get(C.source)?.hidden, b = this._nodeMap.get(C.target)?.hidden; + if (C.hidden || C._hiddenByCollapse || E || b) continue; - const T = g.has(b.source) || g.has(b.target) || dp(this._edgeCorridors.get(S), u), I = !y.has(S); - T !== I && (x.style.display = T ? "" : "none"), T || _.add(S); + const T = g.has(C.source) || g.has(C.target) || gp(this._edgeCorridors.get(k), u), D = !y.has(k); + T !== D && (x.style.display = T ? "" : "none"), T || _.add(k); } this._visibleNodeIds = g, this._culledEdgeIds = _; }, @@ -12818,16 +12831,16 @@ function up(t) { }, getAbsolutePosition(s) { const a = this._nodeMap.get(s); - return a ? ni(a, this._nodeMap, this._config.nodeOrigin) : { x: 0, y: 0 }; + return a ? ii(a, this._nodeMap, this._config.nodeOrigin) : { x: 0, y: 0 }; }, // ── Init Helpers ───────────────────────────────────────────────── /** Enable debug logging if configured. */ _initDebug() { - e.debug && Fr(!0); + e.debug && qr(!0); }, /** Set up container element, attributes, CSS custom properties, animator. */ _initContainer() { - this._container = this.$el, this._container.setAttribute("data-flow-canvas", ""), e.fitViewOnInit && this._container.setAttribute("data-fit-view", ""), this._container.setAttribute("role", "application"), this._container.setAttribute("aria-label", e.ariaLabel ?? "Flow diagram"), this._containerStyles = getComputedStyle(this._container), this._animator = new ff(eo), e.patternColor && this._container.style.setProperty("--flow-bg-pattern-color", e.patternColor), e.backgroundGap && this._container.style.setProperty("--flow-bg-pattern-gap", String(e.backgroundGap)); + this._container = this.$el, this._container.setAttribute("data-flow-canvas", ""), e.fitViewOnInit && this._container.setAttribute("data-fit-view", ""), this._container.setAttribute("role", "application"), this._container.setAttribute("aria-label", e.ariaLabel ?? "Flow diagram"), this._containerStyles = getComputedStyle(this._container), this._animator = new mf(to), e.patternColor && this._container.style.setProperty("--flow-bg-pattern-color", e.patternColor), e.backgroundGap && this._container.style.setProperty("--flow-bg-pattern-gap", String(e.backgroundGap)); const s = e.containerHeight; if (s !== void 0 && s !== "auto") { let a = null; @@ -12837,7 +12850,7 @@ function up(t) { }, /** Create color mode handle if configured. */ _initColorMode() { - e.colorMode && (this._colorModeHandle = fa(this._container, e.colorMode)); + e.colorMode && (this._colorModeHandle = ya(this._container, e.colorMode)); }, /** Hydrate from static HTML, sort nodes, rebuild maps, capture initial dimensions. */ _initHydration() { @@ -12847,19 +12860,19 @@ function up(t) { }, /** Create FlowHistory if configured. */ _initHistory() { - e.history && (this._history = new of(e.historyMaxSize ?? 50)); + e.history && (this._history = new lf(e.historyMaxSize ?? 50)); }, /** Create screen reader announcer. */ _initAnnouncer() { if (e.announcements !== !1 && this._container) { const s = typeof e.announcements == "object" ? e.announcements.formatMessage : void 0; - this._announcer = new xh(this._container, s); + this._announcer = new kh(this._container, s); } }, /** Set up collaboration bridge via collab addon plugin. */ _initCollab() { if (e.collab && this._container) { - const s = Dt("collab"); + const s = Ht("collab"); if (!s) { console.error("[AlpineFlow] Collaboration requires the collab plugin. Register it with: Alpine.plugin(AlpineFlowCollab)"); return; @@ -12867,31 +12880,31 @@ function up(t) { const a = this._container, { Doc: l, Awareness: c, CollabBridge: d, CollabAwareness: u } = s, f = e.collab, h = new l(), p = new c(h), g = new d(h, this, f.provider), m = new u(p, f.user); if (He.set(a, { bridge: g, awareness: m, doc: h }), f.provider.connect(h, p), f.cursors !== !1) { let y = !1; - const _ = f.throttle ?? 20, S = (C) => { + const _ = f.throttle ?? 20, k = (E) => { if (y) return; y = !0; - const E = a.getBoundingClientRect(), T = this._viewportLive ?? this.viewport, I = (C.clientX - E.left - T.x) / T.zoom, M = (C.clientY - E.top - T.y) / T.zoom; - m.updateCursor({ x: I, y: M }), setTimeout(() => { + const b = a.getBoundingClientRect(), T = this._viewportLive ?? this.viewport, D = (E.clientX - b.left - T.x) / T.zoom, L = (E.clientY - b.top - T.y) / T.zoom; + m.updateCursor({ x: D, y: L }), setTimeout(() => { y = !1; }, _); }, x = () => { m.updateCursor(null); }; - a.addEventListener("mousemove", S), a.addEventListener("mouseleave", x); - const b = He.get(a); - b.cursorCleanup = () => { - a.removeEventListener("mousemove", S), a.removeEventListener("mouseleave", x); + a.addEventListener("mousemove", k), a.addEventListener("mouseleave", x); + const C = He.get(a); + C.cursorCleanup = () => { + a.removeEventListener("mousemove", k), a.removeEventListener("mouseleave", x); }; } } }, /** Create panZoom instance, viewport element fallback, apply background, register with store, setup marker defs. */ _initPanZoom() { - if (V("init", `flowCanvas "${this._id}" initializing`, { + if (B("init", `flowCanvas "${this._id}" initializing`, { nodes: this.nodes.map((s) => ({ id: s.id, type: s.type ?? "default", position: s.position, parentId: s.parentId })), edges: this.edges.map((s) => ({ id: s.id, source: s.source, target: s.target, type: s.type ?? "default" })), config: { minZoom: e.minZoom, maxZoom: e.maxZoom, pannable: e.pannable, zoomable: e.zoomable, debug: e.debug } - }), this._panZoom = Uu(this._container, { + }), this._panZoom = Ju(this._container, { onTransformChange: (s) => { this._onViewportTransform(s); }, @@ -12954,9 +12967,9 @@ function up(t) { this._config?.delegatedHandleEvents !== !1 && this.$nextTick(() => { if (this._handleDelegationTornDown) return; const s = this._viewportEl ?? this._container?.querySelector(".flow-viewport"); - !s && this._container && V("init", `flowCanvas "${this._id}" has no .flow-viewport — delegating handle pointerdown on the container instead; an active whiteboard tool may not suppress handle presses`); + !s && this._container && B("init", `flowCanvas "${this._id}" has no .flow-viewport — delegating handle pointerdown on the container instead; an active whiteboard tool may not suppress handle presses`); const a = s ?? this._container; - a && (this._handleDelegationCleanup = ps(a, this), this._handleDelegationEl = a); + a && (this._handleDelegationCleanup = ys(a, this), this._handleDelegationEl = a); }); }, /** @@ -12982,7 +12995,7 @@ function up(t) { _registerViewportEl(s) { this._viewportEl = s; const a = this._handleDelegationEl; - !a || a === s || (this._handleDelegationCleanup?.(), this._handleDelegationCleanup = null, this._handleDelegationEl = null, !this._handleDelegationTornDown && (this._handleDelegationCleanup = ps(s, this), this._handleDelegationEl = s, V("init", `flowCanvas "${this._id}" re-bound its delegated handle pointerdown listener to a replaced .flow-viewport`))); + !a || a === s || (this._handleDelegationCleanup?.(), this._handleDelegationCleanup = null, this._handleDelegationEl = null, !this._handleDelegationTornDown && (this._handleDelegationCleanup = ys(s, this), this._handleDelegationEl = s, B("init", `flowCanvas "${this._id}" re-bound its delegated handle pointerdown listener to a replaced .flow-viewport`))); }, /** Canvas click handler, context menu handler, long press, touch selection mode, context menu event listeners. */ _initClickHandlers() { @@ -12996,7 +13009,7 @@ function up(t) { source: this.pendingConnection.source, sourceHandle: this.pendingConnection.sourceHandle, position: this.screenToFlowPosition(l.clientX, l.clientY) - }), this.pendingConnection = null, this._container?.classList.remove("flow-connecting"), this._container && ke(this._container)); + }), this.pendingConnection = null, this._container?.classList.remove("flow-connecting"), this._container && Pe(this._container)); const c = l.target; if (c === this._container || c.classList.contains("flow-viewport")) { const d = this.screenToFlowPosition(l.clientX, l.clientY); @@ -13014,7 +13027,7 @@ function up(t) { } }, this._container.addEventListener("contextmenu", this._onCanvasContextMenu); const s = e.longPressAction ?? "context-menu"; - if (s && (this._longPressCleanup = Uf( + if (s && (this._longPressCleanup = Jf( this._container, (l) => { const c = l.target; @@ -13102,7 +13115,7 @@ function up(t) { source: this.pendingConnection.source, sourceHandle: this.pendingConnection.sourceHandle, position: { x: 0, y: 0 } - }), this.pendingConnection = null, this._container?.classList.remove("flow-connecting"), this._container && ke(this._container); + }), this.pendingConnection = null, this._container?.classList.remove("flow-connecting"), this._container && Pe(this._container); return; } if (Ge(s.key, l.delete)) { @@ -13137,10 +13150,10 @@ function up(t) { p === 0 ? u = -c : p === 1 ? u = c : p === 2 ? d = -c : p === 3 && (d = c); } } - jf(s.repeat, this.selectedNodes.size, d, u) && this._captureHistory(); + Kf(s.repeat, this.selectedNodes.size, d, u) && this._captureHistory(); for (const f of this.selectedNodes) { const h = this.getNode(f); - if (h && Gr(h)) { + if (h && ea(h)) { h.position.x += d, h.position.y += u; const p = this._container ? He.get(this._container) : void 0; p?.bridge && p.bridge.pushLocalNodeUpdate(h.id, { position: h.position }); @@ -13163,9 +13176,9 @@ function up(t) { }, /** Create minimap if configured. */ _initMinimap() { - e.minimap && (this._minimap = pf(this._container, { + e.minimap && (this._minimap = vf(this._container, { getState: () => ({ - nodes: so(this.nodes, this._nodeMap, this._config.nodeOrigin), + nodes: ro(this.nodes, this._nodeMap, this._config.nodeOrigin), viewport: this.viewport, containerWidth: this._container?.clientWidth ?? 0, containerHeight: this._container?.clientHeight ?? 0 @@ -13183,7 +13196,7 @@ function up(t) { _initControls() { if (e.controls) { const s = e.controlsContainer ? document.querySelector(e.controlsContainer) ?? this._container : this._container, a = s !== this._container; - this._controls = xf(s, { + this._controls = kf(s, { position: e.controlsPosition ?? "bottom-left", orientation: e.controlsOrientation ?? "vertical", external: a, @@ -13193,7 +13206,7 @@ function up(t) { showResetPanels: e.controlsShowResetPanels ?? !1, onZoomIn: () => this.zoomIn(), onZoomOut: () => this.zoomOut(), - onFitView: () => this.fitView({ padding: Ko }), + onFitView: () => this.fitView({ padding: Qo }), onToggleInteractive: () => this.toggleInteractive(), onResetPanels: () => this.resetPanels(), onToggleFullscreen: () => this.toggleFullscreen() @@ -13269,7 +13282,7 @@ function up(t) { }, /** Selection box/lasso setup (pointerdown/pointermove/pointerup handlers). */ _initSelection() { - this._selectionBox = Ef(this._container), this._lasso = Cf(this._container), this._selectionTool = e.selectionTool ?? "box", this._onSelectionPointerDown = (s) => { + this._selectionBox = Lf(this._container), this._lasso = Pf(this._container), this._selectionTool = e.selectionTool ?? "box", this._onSelectionPointerDown = (s) => { if (!this._config.selectionOnDrag && !this._touchSelectionMode && !mt(s, this._shortcuts.selectionBox)) return; const a = s.target; @@ -13287,12 +13300,12 @@ function up(t) { }, this._onSelectionPointerUp = (s) => { if (!(this._selectionTool === "lasso" ? this._lasso?.isActive() : this._selectionBox?.isActive())) return; s.target.releasePointerCapture(s.pointerId), this._suppressNextCanvasClick = !0; - const l = so(this.nodes, this._nodeMap, this._config.nodeOrigin); + const l = ro(this.nodes, this._nodeMap, this._config.nodeOrigin); let c, d = []; if (this._selectionTool === "lasso") { const u = this._lasso.end(this.viewport); if (!u) return; - const f = this._selectionEffectiveMode === "full" ? Pf(l, u) : Lf(l, u), h = new Set(f.map((p) => p.id)); + const f = this._selectionEffectiveMode === "full" ? Af(l, u) : Nf(l, u), h = new Set(f.map((p) => p.id)); if (c = this.nodes.filter((p) => h.has(p.id)), this._config.lassoSelectsEdges) for (const p of this.edges) { if (p.hidden) continue; @@ -13303,20 +13316,20 @@ function up(t) { const m = g.getTotalLength(), y = Math.max(10, Math.ceil(m / 20)); let _ = 0; for (let x = 0; x <= y; x++) { - const b = g.getPointAtLength(x / y * m); - Ei(b.x, b.y, u) && _++; + const C = g.getPointAtLength(x / y * m); + Si(C.x, C.y, u) && _++; } (this._selectionEffectiveMode === "full" ? _ === y + 1 : _ > 0) && d.push(p.id); } } else { const u = this._selectionBox.end(this.viewport); if (!u) return; - const f = this._selectionEffectiveMode === "full" ? Ku(l, u, this._config.nodeOrigin) : Zu(l, u, this._config.nodeOrigin), h = new Set(f.map((p) => p.id)); + const f = this._selectionEffectiveMode === "full" ? tf(l, u, this._config.nodeOrigin) : ef(l, u, this._config.nodeOrigin), h = new Set(f.map((p) => p.id)); c = this.nodes.filter((p) => h.has(p.id)); } this._selectionShiftHeld || this.deselectAll(); for (const u of c) { - if (!ei(u) || u.hidden) continue; + if (!ni(u) || u.hidden) continue; u.selected = !0, this.selectedNodes.add(u.id); const f = this._container?.querySelector(`[data-flow-node-id="${CSS.escape(u.id)}"]`); f && f.classList.add("flow-node-selected"); @@ -13374,7 +13387,7 @@ function up(t) { } if (!this._container) return; - const f = Hr( + const f = Vr( l.clientX, l.clientY, this.viewport, @@ -13463,7 +13476,7 @@ function up(t) { const g = Math.round(h), m = Math.round(p), y = u.dimensions; if (y && Math.abs((y.width ?? 0) - g) < 1 && Math.abs((y.height ?? 0) - m) < 1) continue; - const _ = Rh( + const _ = Vh( { width: g, height: m }, u.minDimensions, u.maxDimensions @@ -13475,17 +13488,17 @@ function up(t) { }, /** Run initial child layouts for all layout parents. */ _initChildLayout() { - if (this._layoutDedup = Dh((s) => { + if (this._layoutDedup = Oh((s) => { this.layoutChildren(s); }), this._resizeObserverInit(), this.$wire) { const s = this.$wire; - e.wireEvents && Nh(e, s, e.wireEvents); - const a = $h(this, s), l = Lh(this, s); + e.wireEvents && Rh(e, s, e.wireEvents); + const a = Hh(this, s), l = Nh(this, s); this._wireCleanup = () => { a(), l(); - }, V("init", `wire bridge activated for "${this._id}"`); + }, B("init", `wire bridge activated for "${this._id}"`); } - V("init", `flowCanvas "${this._id}" ready`), this._emit("init"), this._recomputeChildValidation(); + B("init", `flowCanvas "${this._id}" ready`), this._emit("init"), this._recomputeChildValidation(); for (const s of this.nodes) s.childLayout && this._installChildLayoutWatchers(s); for (const s of this.nodes) @@ -13498,7 +13511,7 @@ function up(t) { }, /** Call setup(canvas) on any addon that provides it. */ _initAddons() { - for (const [, s] of ha().entries()) + for (const [, s] of wa().entries()) s && typeof s == "object" && typeof s.setup == "function" && s.setup(this); }, /** Validate auto-layout dependency and start initial layout. */ @@ -13515,7 +13528,7 @@ function up(t) { hierarchy: "AlpineFlowHierarchy", elk: "AlpineFlowElk" }, c = a[s]; - c && Dt(c) ? (this._autoLayoutReady = !0, this.$nextTick(() => this._runAutoLayout())) : c && this._warn("AUTO_LAYOUT_MISSING_DEP", `autoLayout requires the ${s} plugin. Register it with: Alpine.plugin(${l[s]})`); + c && Ht(c) ? (this._autoLayoutReady = !0, this.$nextTick(() => this._runAutoLayout())) : c && this._warn("AUTO_LAYOUT_MISSING_DEP", `autoLayout requires the ${s} plugin. Register it with: Alpine.plugin(${l[s]})`); } }, /** requestAnimationFrame ready flip, loading watch, loading overlay injection. */ @@ -13562,8 +13575,8 @@ function up(t) { for (const d of this.edges) for (const u of [d.markerStart, d.markerEnd]) { if (!u) continue; - const f = Ot(u), h = zt(f, this._id); - a.has(h) || a.set(h, no(f, h)); + const f = Vt(u), h = Bt(f, this._id); + a.has(h) || a.set(h, oo(f, h)); } const l = s.querySelectorAll("marker"), c = /* @__PURE__ */ new Set(); l.forEach((d) => { @@ -13588,7 +13601,7 @@ function up(t) { s ); } - if (V("destroy", `flowCanvas "${this._id}" destroying`), this._onCanvasClick && this._container && this._container.removeEventListener("click", this._onCanvasClick), this._onCanvasContextMenu && this._container && this._container.removeEventListener("contextmenu", this._onCanvasContextMenu), this._container) + if (B("destroy", `flowCanvas "${this._id}" destroying`), this._onCanvasClick && this._container && this._container.removeEventListener("click", this._onCanvasClick), this._onCanvasContextMenu && this._container && this._container.removeEventListener("contextmenu", this._onCanvasContextMenu), this._container) for (const s of this._contextMenuListeners) this._container.removeEventListener(s.event, s.handler); if (this._contextMenuListeners = [], this._onKeyDown && document.removeEventListener("keydown", this._onKeyDown), this._onContainerPointerDown && this._container && this._container.removeEventListener("pointerdown", this._onContainerPointerDown), this._markerDefsEl?.remove(), this._markerDefsEl = null, this._minimap?.destroy(), this._minimap = null, this._controls?.destroy(), this._controls = null, this._onFullscreenChange && typeof document < "u" && document.removeEventListener("fullscreenchange", this._onFullscreenChange), this._onFullscreenChange = null, typeof document < "u") { @@ -13632,7 +13645,7 @@ function up(t) { * even if fn throws. */ batch(s) { - return this._layoutDedup ? Hh(this._layoutDedup)(s) : s(); + return this._layoutDedup ? zh(this._layoutDedup)(s) : s(); }, get collab() { return this._container ? He.get(this._container)?.awareness : void 0; @@ -13640,7 +13653,7 @@ function up(t) { async toImage(s) { let a; try { - ({ captureFlowImage: a } = await Promise.resolve().then(() => Fm)); + ({ captureFlowImage: a } = await Promise.resolve().then(() => Jm)); } catch { throw new Error("toImage() requires html-to-image. Install it with: npm install html-to-image"); } @@ -13662,69 +13675,69 @@ function up(t) { return o[a] = l, !0; } }), r = [ - Vh(i), - Bh(i), - qh(i), + Yh(i), + Wh(i), jh(i), - Gh(i), - Cg(i), - Lg(i), + Kh(i), + Qh(i), Pg(i), - Mg(i), - Fg(i), - Og(i), - zg(i), - rp(i, t), - ap(i) + Ng(i), + Ag(i), + $g(i), + Bg(i), + qg(i), + Xg(i), + dp(i, t), + up(i) ]; for (const s of r) Object.defineProperties(n, Object.getOwnPropertyDescriptors(s)); return n.registerMarker = (s, a) => { - hf(s, a); + yf(s, a); }, n; }); } -function Xs(t, e) { +function Us(t, e) { return { x: e[0] * Math.round(t.x / e[0]), y: e[1] * Math.round(t.y / e[1]) }; } -function fp(t, e, n) { +function mp(t, e, n) { const { onDragStart: o, onDrag: i, onDragEnd: r, getViewport: s, getNodePosition: a, snapToGrid: l = !1, filterSelector: c, container: d, isLocked: u, noDragClassName: f, dragThreshold: h = 0 } = n; let p = { x: 0, y: 0 }; function g(_) { - const S = s(); + const k = s(); return { - x: (_.x - S.x) / S.zoom, - y: (_.y - S.y) / S.zoom + x: (_.x - k.x) / k.zoom, + y: (_.y - k.y) / k.zoom }; } - const m = Ye(t), y = Lc().subject(() => { - const _ = s(), S = a(); + const m = Xe(t), y = Nc().subject(() => { + const _ = s(), k = a(); return { - x: S.x * _.zoom + _.x, - y: S.y * _.zoom + _.y + x: k.x * _.zoom + _.x, + y: k.y * _.zoom + _.y }; }).on("start", (_) => { p = g(_), o?.({ nodeId: e, position: p, sourceEvent: _.sourceEvent }); }).on("drag", (_) => { - let S = g(_); - l && (S = Xs(S, l)); + let k = g(_); + l && (k = Us(k, l)); const x = { - x: S.x - p.x, - y: S.y - p.y + x: k.x - p.x, + y: k.y - p.y }; - i?.({ nodeId: e, position: S, delta: x, sourceEvent: _.sourceEvent }); + i?.({ nodeId: e, position: k, delta: x, sourceEvent: _.sourceEvent }); }).on("end", (_) => { - let S = g(_); - l && (S = Xs(S, l)), r?.({ nodeId: e, position: S, sourceEvent: _.sourceEvent }); + let k = g(_); + l && (k = Us(k, l)), r?.({ nodeId: e, position: k, sourceEvent: _.sourceEvent }); }); return d && y.container(() => d), h > 0 && y.clickDistance(h), y.filter((_) => { if (u?.() || f && _.target?.closest?.("." + f)) return !1; if (c) { - const S = t.querySelector(c); - return S ? S.contains(_.target) : !0; + const k = t.querySelector(c); + return k ? k.contains(_.target) : !0; } return !0; }), m.call(y), { @@ -13733,17 +13746,17 @@ function fp(t, e, n) { } }; } -function hp(t, e) { - const n = Gt(t, e); +function yp(t, e) { + const n = Kt(t, e); return { id: t.id, x: n.x, y: n.y, - width: t.dimensions?.width ?? we, - height: t.dimensions?.height ?? _e + width: t.dimensions?.width ?? ve, + height: t.dimensions?.height ?? be }; } -function gp(t, e, n) { +function wp(t, e, n) { const o = /* @__PURE__ */ new Set(), i = /* @__PURE__ */ new Set(); let r = 0, s = 0, a = 1 / 0, l = 1 / 0; const c = t.x + t.width / 2, d = t.y + t.height / 2, u = t.x + t.width, f = t.y + t.height; @@ -13760,11 +13773,11 @@ function gp(t, e, n) { [u, h.x] // right-left ]; - for (const [x, b] of _) { - const C = b - x; - Math.abs(C) <= n && (i.add(b), Math.abs(C) < Math.abs(a) && (a = C, r = C)); + for (const [x, C] of _) { + const E = C - x; + Math.abs(E) <= n && (i.add(C), Math.abs(E) < Math.abs(a) && (a = E, r = E)); } - const S = [ + const k = [ [t.y, h.y], // top-top [f, y], @@ -13776,9 +13789,9 @@ function gp(t, e, n) { [f, h.y] // bottom-top ]; - for (const [x, b] of S) { - const C = b - x; - Math.abs(C) <= n && (o.add(b), Math.abs(C) < Math.abs(l) && (l = C, s = C)); + for (const [x, C] of k) { + const E = C - x; + Math.abs(E) <= n && (o.add(C), Math.abs(E) < Math.abs(l) && (l = E, s = E)); } } return { @@ -13787,10 +13800,10 @@ function gp(t, e, n) { snapOffset: { x: r, y: s } }; } -function pp(t, e, n, o) { +function vp(t, e, n, o) { return Math.abs(t.x - e.x) > 30 ? t.x < e.x ? { source: n, target: o } : { source: o, target: n } : t.y < e.y ? { source: n, target: o } : { source: o, target: n }; } -function mp(t, e, n, o) { +function _p(t, e, n, o) { let i = null, r = o; for (const s of n) { if (s.id === t) continue; @@ -13799,18 +13812,18 @@ function mp(t, e, n, o) { ); if (a < r) { r = a; - const { source: l, target: c } = pp(e, s.center, t, s.id); + const { source: l, target: c } = vp(e, s.center, t, s.id); i = { source: l, target: c, targetId: s.id, distance: a, targetCenter: s.center }; } } return i; } -const yp = /* @__PURE__ */ new Set(["x-data", "x-init", "x-bind", "href", "src", "action", "formaction", "srcdoc"]); -let wp = 0; -function Ws(t, e, n) { +const bp = /* @__PURE__ */ new Set(["x-data", "x-init", "x-bind", "href", "src", "action", "formaction", "srcdoc"]); +let xp = 0; +function Gs(t, e, n) { e && n !== null && t._commitHistory?.(n); } -function Do(t, e, n) { +function Ho(t, e, n) { t._suspendHistory?.(); try { t.reparentNode?.(e, n); @@ -13818,7 +13831,7 @@ function Do(t, e, n) { t._resumeHistory?.(); } } -function vp(t, e) { +function Ep(t, e) { switch (e) { case "alt": return t.altKey; @@ -13828,7 +13841,7 @@ function vp(t, e) { return t.shiftKey; } } -function _p(t, e, n) { +function Cp(t, e, n) { const o = t.querySelectorAll('[data-flow-handle-type="source"]'); if (o.length === 0) return null; let i = null, r = 1 / 0; @@ -13839,7 +13852,7 @@ function _p(t, e, n) { u < r && (r = u, i = a); }), i; } -function bp(t, e, n) { +function Sp(t, e, n) { let o = 1 / 0, i = -1 / 0, r = 1 / 0, s = -1 / 0; for (const c of n) o = Math.min(o, c.x), i = Math.max(i, c.x + c.width), r = Math.min(r, c.y), s = Math.max(s, c.y + c.height); @@ -13855,417 +13868,417 @@ function bp(t, e, n) { } return l; } -function xp(t) { +function kp(t) { t.directive( "flow-node", (e, { expression: n }, { evaluate: o, effect: i, cleanup: r }) => { - let s = null, a = !1, l = null, c = !1, d = null, u = null, f = null, h = null, p = null, g = null, m = !1, y = -1, _ = null, S = !1, x = [], b = "", C = [], E = null; + let s = null, a = !1, l = null, c = !1, d = null, u = null, f = null, h = null, p = null, g = null, m = !1, y = -1, _ = null, k = !1, x = [], C = "", E = [], b = null; i(() => { if (!e.isConnected) return; const P = o(n); if (!P || P.hidden) return; const v = t.$data(e.closest("[x-data]")); if (!v?.viewport) return; - const w = P.parentId ? v.getAbsolutePosition(P.id) : P.position ?? { x: 0, y: 0 }, N = P.nodeOrigin ?? v._config?.nodeOrigin ?? [0, 0], k = P.dimensions?.width ?? 150, R = P.dimensions?.height ?? 40; - e.style.left = w.x - k * N[0] + "px", e.style.top = w.y - R * N[1] + "px"; + const w = P.parentId ? v.getAbsolutePosition(P.id) : P.position ?? { x: 0, y: 0 }, I = P.nodeOrigin ?? v._config?.nodeOrigin ?? [0, 0], M = P.dimensions?.width ?? 150, R = P.dimensions?.height ?? 40; + e.style.left = w.x - M * I[0] + "px", e.style.top = w.y - R * I[1] + "px"; }), i(() => { if (!e.isConnected) return; const P = o(n); if (!P) return; - if (e.dataset.flowNodeId = P.id, P.type && (e.dataset.flowNodeType = P.type), !S) { - const z = e.closest("[x-data]"), Y = z ? t.$data(z) : null; - let W = !1; - if (Y?._config?.nodeTypes) { - const j = P.type ?? "default", H = Y._config.nodeTypes[j] ?? Y._config.nodeTypes.default; - if (typeof H == "string") { - const te = document.querySelector(H); - te?.content && (e.appendChild(te.content.cloneNode(!0)), W = !0); - } else typeof H == "function" && (H(P, e), W = !0); + if (e.dataset.flowNodeId = P.id, P.type && (e.dataset.flowNodeType = P.type), !k) { + const H = e.closest("[x-data]"), q = H ? t.$data(H) : null; + let j = !1; + if (q?._config?.nodeTypes) { + const K = P.type ?? "default", O = q._config.nodeTypes[K] ?? q._config.nodeTypes.default; + if (typeof O == "string") { + const J = document.querySelector(O); + J?.content && (e.appendChild(J.content.cloneNode(!0)), j = !0); + } else typeof O == "function" && (O(P, e), j = !0); } - if (!W && e.children.length === 0) { - const j = document.createElement("div"); - j.setAttribute("x-flow-handle:target", ""); - const H = document.createElement("span"); - H.setAttribute("x-text", "node.data.label"); - const te = document.createElement("div"); - te.setAttribute("x-flow-handle:source", ""), e.appendChild(j), e.appendChild(H), e.appendChild(te), W = !0; + if (!j && e.children.length === 0) { + const K = document.createElement("div"); + K.setAttribute("x-flow-handle:target", ""); + const O = document.createElement("span"); + O.setAttribute("x-text", "node.data.label"); + const J = document.createElement("div"); + J.setAttribute("x-flow-handle:source", ""), e.appendChild(K), e.appendChild(O), e.appendChild(J), j = !0; } - if (W) - for (const j of Array.from(e.children)) - t.addScopeToNode(j, { node: P }), t.initTree(j); - S = !0; + if (j) + for (const K of Array.from(e.children)) + t.addScopeToNode(K, { node: P }), t.initTree(K); + k = !0; } if (P.hidden) { e.classList.add("flow-node-hidden"), e.removeAttribute("tabindex"), e.removeAttribute("role"), e.removeAttribute("aria-label"), s?.destroy(), s = null; return; } - e.classList.remove("flow-node-hidden"), E !== P.id && (s?.destroy(), s = null, E = P.id); + e.classList.remove("flow-node-hidden"), b !== P.id && (s?.destroy(), s = null, b = P.id); const v = t.$data(e.closest("[x-data]")); if (!v?.viewport) return; if (e.classList.add("flow-node", "nopan"), P.type === "group" ? e.classList.add("flow-node-group") : e.classList.remove("flow-node-group"), P.dimensions) { - const z = P.childLayout, Y = P.fixedDimensions, W = (v._childrenIds?.get(P.id)?.length ?? 0) > 0; - e.style.width = P.dimensions.width + "px", z || Y || W ? e.style.height = P.dimensions.height + "px" : e.style.height = ""; + const H = P.childLayout, q = P.fixedDimensions, j = (v._childrenIds?.get(P.id)?.length ?? 0) > 0; + e.style.width = P.dimensions.width + "px", H || q || j ? e.style.height = P.dimensions.height + "px" : e.style.height = ""; } v.selectedNodes.has(P.id) ? e.classList.add("flow-node-selected") : e.classList.remove("flow-node-selected"), e.setAttribute("aria-selected", String(!!P.selected)), P._validationErrors && P._validationErrors.length > 0 ? e.classList.add("flow-node-invalid") : e.classList.remove("flow-node-invalid"); - const w = ["flow-node-running", "flow-node-completed", "flow-node-failed", "flow-node-skipped"], N = P.runState; - for (const z of w) - e.classList.remove(z); - N && N !== "pending" && e.classList.add(`flow-node-${N}`); - for (const z of x) - e.classList.remove(z); - const k = P.class ? P.class.split(/\s+/).filter(Boolean) : []; - for (const z of k) - e.classList.add(z); - x = k; + const w = ["flow-node-running", "flow-node-completed", "flow-node-failed", "flow-node-skipped"], I = P.runState; + for (const H of w) + e.classList.remove(H); + I && I !== "pending" && e.classList.add(`flow-node-${I}`); + for (const H of x) + e.classList.remove(H); + const M = P.class ? P.class.split(/\s+/).filter(Boolean) : []; + for (const H of M) + e.classList.add(H); + x = M; const R = P.shape ? `flow-node-${P.shape}` : ""; - b !== R && (b && e.classList.remove(b), R && e.classList.add(R), b = R); - const O = t.$data(e.closest("[data-flow-canvas]")), q = P.shape && O?._shapeRegistry?.[P.shape]; - if (q?.clipPath ? e.style.clipPath = q.clipPath : e.style.clipPath = "", P.style) { - const z = typeof P.style == "string" ? Object.fromEntries(P.style.split(";").filter(Boolean).map((W) => W.split(":").map((j) => j.trim()))) : P.style, Y = []; - for (const [W, j] of Object.entries(z)) - W && j && (e.style.setProperty(W, j), Y.push(W)); - for (const W of C) - Y.includes(W) || e.style.removeProperty(W); - C = Y; - } else if (C.length > 0) { - for (const z of C) - e.style.removeProperty(z); - C = []; + C !== R && (C && e.classList.remove(C), R && e.classList.add(R), C = R); + const z = e.closest("[data-flow-canvas]"), V = z ? t.$data(z) : null, $ = P.shape && V?._shapeRegistry?.[P.shape]; + if ($?.clipPath ? e.style.clipPath = $.clipPath : e.style.clipPath = "", P.style) { + const H = typeof P.style == "string" ? Object.fromEntries(P.style.split(";").filter(Boolean).map((j) => j.split(":").map((K) => K.trim()))) : P.style, q = []; + for (const [j, K] of Object.entries(H)) + j && K && (e.style.setProperty(j, K), q.push(j)); + for (const j of E) + q.includes(j) || e.style.removeProperty(j); + E = q; + } else if (E.length > 0) { + for (const H of E) + e.style.removeProperty(H); + E = []; } if (P.rotation ? (e.style.transform = `rotate(${P.rotation}deg)`, e.style.transformOrigin = "center") : e.style.transform = "", P.focusable ?? v._config?.nodesFocusable !== !1 ? (e.setAttribute("tabindex", "0"), e.setAttribute("role", P.ariaRole ?? "group"), e.setAttribute("aria-label", P.ariaLabel ?? (P.data?.label ? `Node: ${P.data.label}` : `Node ${P.id}`))) : (e.removeAttribute("tabindex"), e.removeAttribute("role"), e.removeAttribute("aria-label")), P.domAttributes) - for (const [z, Y] of Object.entries(P.domAttributes)) - z.startsWith("on") || yp.has(z.toLowerCase()) || e.setAttribute(z, Y); - Be(P) ? e.classList.remove("flow-node-no-connect") : e.classList.add("flow-node-no-connect"), P.collapsed ? e.classList.add("flow-node-collapsed") : e.classList.remove("flow-node-collapsed"); - const L = e.classList.contains("flow-node-condensed"); - P.condensed ? e.classList.add("flow-node-condensed") : e.classList.remove("flow-node-condensed"), !!P.condensed !== L && requestAnimationFrame(() => { + for (const [H, q] of Object.entries(P.domAttributes)) + H.startsWith("on") || bp.has(H.toLowerCase()) || e.setAttribute(H, q); + qe(P) ? e.classList.remove("flow-node-no-connect") : e.classList.add("flow-node-no-connect"), P.collapsed ? e.classList.add("flow-node-collapsed") : e.classList.remove("flow-node-collapsed"); + const N = e.classList.contains("flow-node-condensed"); + P.condensed ? e.classList.add("flow-node-condensed") : e.classList.remove("flow-node-condensed"), !!P.condensed !== N && requestAnimationFrame(() => { P.dimensions = { width: e.offsetWidth, height: e.offsetHeight - }, V("condense", `Node "${P.id}" re-measured after condense toggle`, P.dimensions); + }, B("condense", `Node "${P.id}" re-measured after condense toggle`, P.dimensions); }), P.filtered ? e.classList.add("flow-node-filtered") : e.classList.remove("flow-node-filtered"); - const $ = P.handles ?? "visible"; - e.classList.remove("flow-handles-hidden", "flow-handles-hover", "flow-handles-select"), $ !== "visible" && e.classList.add(`flow-handles-${$}`); - let F = ca(P, v._nodeMap); - v._config?.elevateNodesOnSelect !== !1 && v.selectedNodes.has(P.id) && (F += P.type === "group" ? Math.max(1 - F, 0) : 1e3), m && (F += 1e3); - const ne = P.type === "group" ? 0 : 2; - if (F !== ne ? e.style.zIndex = String(F) : e.style.removeProperty("z-index"), !Gr(P)) { + const F = P.handles ?? "visible"; + e.classList.remove("flow-handles-hidden", "flow-handles-hover", "flow-handles-select"), F !== "visible" && e.classList.add(`flow-handles-${F}`); + let W = ga(P, v._nodeMap); + v._config?.elevateNodesOnSelect !== !1 && v.selectedNodes.has(P.id) && (W += P.type === "group" ? Math.max(1 - W, 0) : 1e3), m && (W += 1e3); + const U = P.type === "group" ? 0 : 2; + if (W !== U ? e.style.zIndex = String(W) : e.style.removeProperty("z-index"), !ea(P)) { e.classList.add("flow-node-locked"), s?.destroy(), s = null; return; } e.classList.remove("flow-node-locked"), e.querySelector("[data-flow-drag-handle]") ? e.classList.add("flow-node-has-handle") : e.classList.remove("flow-node-has-handle"); - const B = e.closest(".flow-container"); - s || (s = fp(e, P.id, { - container: B ?? void 0, + const Z = e.closest(".flow-container"); + s || (s = mp(e, P.id, { + container: Z ?? void 0, filterSelector: "[data-flow-drag-handle]", isLocked: () => v._animationLocked, noDragClassName: v._config?.noDragClassName ?? "nodrag", dragThreshold: v._config?.nodeDragThreshold ?? 0, getViewport: () => v.viewport, getNodePosition: () => { - const z = v.getNode(P.id); - return z ? z.parentId ? v.getAbsolutePosition(z.id) : { x: z.position.x, y: z.position.y } : { x: 0, y: 0 }; + const H = v.getNode(P.id); + return H ? H.parentId ? v.getAbsolutePosition(H.id) : { x: H.position.x, y: H.position.y } : { x: 0, y: 0 }; }, snapToGrid: v._config?.snapToGrid ?? !1, - onDragStart({ nodeId: z, position: Y, sourceEvent: W }) { + onDragStart({ nodeId: H, position: q, sourceEvent: j }) { e.classList.add("flow-node-dragging"), a = !1, c = !1, d = null; - const j = v._container ? He.get(v._container) : void 0; - j?.bridge && j.bridge.setDragging(z, !0), h?.destroy(), h = null, p = null, g && B && B.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), g = null, l = v._snapshotHistory?.() ?? null, V("drag", `Node "${z}" drag start`, Y); - const H = v.getNode(z); - if (H) { - if (v._config?.selectNodesOnDrag !== !1 && H.selectable !== !1 && !v.selectedNodes.has(z) && (mt(W, v._shortcuts?.multiSelect) || v.deselectAll(), v.selectedNodes.add(z), H.selected = !0, v._emitSelectionChange(), c = !0), v._emit("node-drag-start", { node: H }), v.selectedNodes.has(z) && v.selectedNodes.size > 1) { - const te = wt(z, v.nodes); + const K = v._container ? He.get(v._container) : void 0; + K?.bridge && K.bridge.setDragging(H, !0), h?.destroy(), h = null, p = null, g && Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), g = null, l = v._snapshotHistory?.() ?? null, B("drag", `Node "${H}" drag start`, q); + const O = v.getNode(H); + if (O) { + if (v._config?.selectNodesOnDrag !== !1 && O.selectable !== !1 && !v.selectedNodes.has(H) && (mt(j, v._shortcuts?.multiSelect) || v.deselectAll(), v.selectedNodes.add(H), O.selected = !0, v._emitSelectionChange(), c = !0), v._emit("node-drag-start", { node: O }), v.selectedNodes.has(H) && v.selectedNodes.size > 1) { + const J = wt(H, v.nodes); d = /* @__PURE__ */ new Map(); - for (const Q of v.selectedNodes) { - if (Q === z || te.has(Q)) + for (const ne of v.selectedNodes) { + if (ne === H || J.has(ne)) continue; - const U = v.getNode(Q); - U && U.draggable !== !1 && d.set(Q, { x: U.position.x, y: U.position.y }); + const Y = v.getNode(ne); + Y && Y.draggable !== !1 && d.set(ne, { x: Y.position.x, y: Y.position.y }); } } - if (v._draggingNodeIds.add(z), d) - for (const te of d.keys()) - v._draggingNodeIds.add(te); + if (v._draggingNodeIds.add(H), d) + for (const J of d.keys()) + v._draggingNodeIds.add(J); } - v._config?.autoPanOnNodeDrag !== !1 && B && (u = Kr({ - container: B, + v._config?.autoPanOnNodeDrag !== !1 && Z && (u = na({ + container: Z, speed: v._config?.autoPanSpeed ?? 15, - onPan(te, Q) { - const U = () => v._viewportLive ?? v.viewport, J = U().zoom || 1, se = { x: U().x, y: U().y }; + onPan(J, ne) { + const Y = () => v._viewportLive ?? v.viewport, re = Y().zoom || 1, se = { x: Y().x, y: Y().y }; v._panZoom?.setViewport({ - x: U().x - te, - y: U().y - Q, - zoom: J + x: Y().x - J, + y: Y().y - ne, + zoom: re }); - const oe = se.x - U().x, Z = se.y - U().y, ee = oe === 0 && Z === 0, ge = v.getNode(z); - let fe = !1; - if (ge) { - const ie = ge.position.x, pe = ge.position.y; - ge.position.x += oe / J, ge.position.y += Z / J; - const re = Rn(ge.position, ge, v._config?.nodeExtent); - ge.position.x = re.x, ge.position.y = re.y, fe = ge.position.x === ie && ge.position.y === pe; + const oe = se.x - Y().x, ee = se.y - Y().y, te = oe === 0 && ee === 0, he = v.getNode(H); + let de = !1; + if (he) { + const ae = he.position.x, ie = he.position.y; + he.position.x += oe / re, he.position.y += ee / re; + const fe = Fn(he.position, he, v._config?.nodeExtent); + he.position.x = fe.x, he.position.y = fe.y, de = he.position.x === ae && he.position.y === ie; } if (d) - for (const [ie] of d) { - const pe = v.getNode(ie); - if (pe) { - pe.position.x += oe / J, pe.position.y += Z / J; - const re = Rn(pe.position, pe, v._config?.nodeExtent); - pe.position.x = re.x, pe.position.y = re.y; + for (const [ae] of d) { + const ie = v.getNode(ae); + if (ie) { + ie.position.x += oe / re, ie.position.y += ee / re; + const fe = Fn(ie.position, ie, v._config?.nodeExtent); + ie.position.x = fe.x, ie.position.y = fe.y; } } - return ee && fe; + return te && de; } - }), W instanceof MouseEvent && u.updatePointer(W.clientX, W.clientY), u.start()); + }), j instanceof MouseEvent && u.updatePointer(j.clientX, j.clientY), u.start()); }, - onDrag({ nodeId: z, position: Y, delta: W, sourceEvent: j }) { + onDrag({ nodeId: H, position: q, delta: j, sourceEvent: K }) { a = !0; - const H = v.getNode(z); - if (H) { - if (H.parentId) { - const U = v.getAbsolutePosition(H.parentId); - let J = Y.x - U.x, se = Y.y - U.y; - const oe = H.dimensions ?? { width: 150, height: 50 }, Z = v.getNode(H.parentId); - if (Z?.childLayout) { - m || (e.classList.add("flow-reorder-dragging"), _ = H.parentId), m = !0; - const ee = H.extent !== "parent"; - if (H.position.x = Y.x - U.x, H.position.y = Y.y - U.y, !ee && Z.dimensions) { - const ie = Po({ x: H.position.x, y: H.position.y }, oe, Z.dimensions); - H.position.x = ie.x, H.position.y = ie.y; + const O = v.getNode(H); + if (O) { + if (O.parentId) { + const Y = v.getAbsolutePosition(O.parentId); + let re = q.x - Y.x, se = q.y - Y.y; + const oe = O.dimensions ?? { width: 150, height: 50 }, ee = v.getNode(O.parentId); + if (ee?.childLayout) { + m || (e.classList.add("flow-reorder-dragging"), _ = O.parentId), m = !0; + const te = O.extent !== "parent"; + if (O.position.x = q.x - Y.x, O.position.y = q.y - Y.y, !te && ee.dimensions) { + const ae = To({ x: O.position.x, y: O.position.y }, oe, ee.dimensions); + O.position.x = ae.x, O.position.y = ae.y; } - const ge = H.dimensions?.width ?? 150, fe = H.dimensions?.height ?? 50; - if (ee) { - const ie = Z.dimensions?.width ?? 150, pe = Z.dimensions?.height ?? 50, re = H.position.x + ge / 2, me = H.position.y + fe / 2, ye = 12, Oe = _ === H.parentId ? 0 : ye, Pe = re >= Oe && re <= ie - Oe && me >= Oe && me <= pe - Oe, Ee = /* @__PURE__ */ new Set(); - let he = H.parentId; - for (; he; ) - Ee.add(he), he = v.getNode(he)?.parentId; - const be = Y.x + ge / 2, Me = Y.y + fe / 2, X = wt(H.id, v.nodes); - let ae = null; - const ue = v.nodes.filter( - (le) => le.id !== H.id && (le.droppable || le.childLayout) && !le.hidden && !X.has(le.id) && (Pe ? !Ee.has(le.id) : le.id !== H.parentId) && (!le.acceptsDrop || le.acceptsDrop(H)) + const he = O.dimensions?.width ?? 150, de = O.dimensions?.height ?? 50; + if (te) { + const ae = ee.dimensions?.width ?? 150, ie = ee.dimensions?.height ?? 50, fe = O.position.x + he / 2, ge = O.position.y + de / 2, Ie = 12, Me = _ === O.parentId ? 0 : Ie, Te = fe >= Me && fe <= ae - Me && ge >= Me && ge <= ie - Me, Le = /* @__PURE__ */ new Set(); + let ue = O.parentId; + for (; ue; ) + Le.add(ue), ue = v.getNode(ue)?.parentId; + const Ee = q.x + he / 2, X = q.y + de / 2, le = wt(O.id, v.nodes); + let pe = null; + const we = v.nodes.filter( + (ce) => ce.id !== O.id && (ce.droppable || ce.childLayout) && !ce.hidden && !le.has(ce.id) && (Te ? !Le.has(ce.id) : ce.id !== O.parentId) && (!ce.acceptsDrop || ce.acceptsDrop(O)) ); - for (const le of ue) { - const de = le.parentId ? v.getAbsolutePosition(le.id) : le.position, xe = le.dimensions?.width ?? 150, Se = le.dimensions?.height ?? 50, Te = le.id === g ? 0 : ye; - be >= de.x + Te && be <= de.x + xe - Te && Me >= de.y + Te && Me <= de.y + Se - Te && (ae = le); + for (const ce of we) { + const ye = ce.parentId ? v.getAbsolutePosition(ce.id) : ce.position, Se = ce.dimensions?.width ?? 150, De = ce.dimensions?.height ?? 50, xe = ce.id === g ? 0 : Ie; + Ee >= ye.x + xe && Ee <= ye.x + Se - xe && X >= ye.y + xe && X <= ye.y + De - xe && (pe = ce); } - const ce = ae?.id ?? null; - if (ce !== g) { - g && B && B.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), ce && B && B.querySelector(`[data-flow-node-id="${CSS.escape(ce)}"]`)?.classList.add("flow-node-drop-target"), g = ce; - const le = ce ? v.getNode(ce) : null, de = _; - if (le?.childLayout && ce !== _) { - de && (v.layoutChildren(de, { omitFromComputation: z, shallow: !0 }), v.propagateLayoutUp(de, { omitFromComputation: z })), _ = ce; - const xe = v.nodes.filter((Ie) => Ie.parentId === ce && Ie.id !== z).sort((Ie, qe) => (Ie.order ?? 1 / 0) - (qe.order ?? 1 / 0)), Se = xe.length, Te = [...xe]; - Te.splice(Se, 0, H); - for (let Ie = 0; Ie < Te.length; Ie++) - Te[Ie].order = Ie; - y = Se; - const Le = v._initialDimensions?.get(z), ze = { ...H, dimensions: Le ? { ...Le } : void 0 }; - v.layoutChildren(ce, { excludeId: z, includeNode: ze, shallow: !0 }), v.propagateLayoutUp(ce, { includeNode: ze }); - } else Pe && _ !== H.parentId ? (de && de !== H.parentId && (v.layoutChildren(de, { omitFromComputation: z, shallow: !0 }), v.propagateLayoutUp(de, { omitFromComputation: z })), _ = H.parentId, y = -1) : !ce && !Pe && (de && (v.layoutChildren(de, { omitFromComputation: z, shallow: !0 }), v.propagateLayoutUp(de, { omitFromComputation: z })), _ = null, y = -1); + const me = pe?.id ?? null; + if (me !== g) { + g && Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), me && Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(me)}"]`)?.classList.add("flow-node-drop-target"), g = me; + const ce = me ? v.getNode(me) : null, ye = _; + if (ce?.childLayout && me !== _) { + ye && (v.layoutChildren(ye, { omitFromComputation: H, shallow: !0 }), v.propagateLayoutUp(ye, { omitFromComputation: H })), _ = me; + const Se = v.nodes.filter((Ce) => Ce.parentId === me && Ce.id !== H).sort((Ce, Nt) => (Ce.order ?? 1 / 0) - (Nt.order ?? 1 / 0)), De = Se.length, xe = [...Se]; + xe.splice(De, 0, O); + for (let Ce = 0; Ce < xe.length; Ce++) + xe[Ce].order = Ce; + y = De; + const ze = v._initialDimensions?.get(H), Tt = { ...O, dimensions: ze ? { ...ze } : void 0 }; + v.layoutChildren(me, { excludeId: H, includeNode: Tt, shallow: !0 }), v.propagateLayoutUp(me, { includeNode: Tt }); + } else Te && _ !== O.parentId ? (ye && ye !== O.parentId && (v.layoutChildren(ye, { omitFromComputation: H, shallow: !0 }), v.propagateLayoutUp(ye, { omitFromComputation: H })), _ = O.parentId, y = -1) : !me && !Te && (ye && (v.layoutChildren(ye, { omitFromComputation: H, shallow: !0 }), v.propagateLayoutUp(ye, { omitFromComputation: H })), _ = null, y = -1); } } if (_) { - const ie = v.getNode(_), pe = ie?.childLayout ?? Z.childLayout, re = v.nodes.filter((he) => he.parentId === _ && he.id !== z).sort((he, be) => (he.order ?? 1 / 0) - (be.order ?? 1 / 0)); - let me, ye; - if (_ !== H.parentId) { - const he = ie?.parentId ? v.getAbsolutePosition(_) : ie?.position ?? { x: 0, y: 0 }; - me = Y.x - he.x, ye = Y.y - he.y; + const ae = v.getNode(_), ie = ae?.childLayout ?? ee.childLayout, fe = v.nodes.filter((ue) => ue.parentId === _ && ue.id !== H).sort((ue, Ee) => (ue.order ?? 1 / 0) - (Ee.order ?? 1 / 0)); + let ge, Ie; + if (_ !== O.parentId) { + const ue = ae?.parentId ? v.getAbsolutePosition(_) : ae?.position ?? { x: 0, y: 0 }; + ge = q.x - ue.x, Ie = q.y - ue.y; } else - me = H.position.x, ye = H.position.y; - const Oe = pe.swapThreshold ?? 0.5; + ge = O.position.x, Ie = O.position.y; + const Me = ie.swapThreshold ?? 0.5; if (y === -1) - if (_ === H.parentId) { - const he = H.order ?? 0; - y = re.filter((be) => (be.order ?? 0) < he).length; + if (_ === O.parentId) { + const ue = O.order ?? 0; + y = fe.filter((Ee) => (Ee.order ?? 0) < ue).length; } else - y = re.length; - const Pe = y; - let Ee = re.length; - for (let he = 0; he < re.length; he++) { - const be = re[he], Me = be.dimensions?.width ?? 150, X = be.dimensions?.height ?? 50, ae = he < Pe ? 1 - Oe : Oe, ue = be.position.y + X * ae, ce = be.position.x + Me * ae; - if (pe.direction === "grid") { - const le = { - x: me + ge / 2, - y: ye + fe / 2 - }, de = be.position.y + X / 2; - if (le.y < be.position.y) { - Ee = he; + y = fe.length; + const Te = y; + let Le = fe.length; + for (let ue = 0; ue < fe.length; ue++) { + const Ee = fe[ue], X = Ee.dimensions?.width ?? 150, le = Ee.dimensions?.height ?? 50, pe = ue < Te ? 1 - Me : Me, we = Ee.position.y + le * pe, me = Ee.position.x + X * pe; + if (ie.direction === "grid") { + const ce = { + x: ge + he / 2, + y: Ie + de / 2 + }, ye = Ee.position.y + le / 2; + if (ce.y < Ee.position.y) { + Le = ue; break; } - if (Math.abs(le.y - de) < X / 2 && le.x < ce) { - Ee = he; + if (Math.abs(ce.y - ye) < le / 2 && ce.x < me) { + Le = ue; break; } - } else if (pe.direction === "vertical") { - if ((he < Pe ? ye : ye + fe) < ue) { - Ee = he; + } else if (ie.direction === "vertical") { + if ((ue < Te ? Ie : Ie + de) < we) { + Le = ue; break; } - } else if ((he < Pe ? me : me + ge) < ce) { - Ee = he; + } else if ((ue < Te ? ge : ge + he) < me) { + Le = ue; break; } } - if (Ee !== y) { - y = Ee; - const he = [...re]; - he.splice(Ee, 0, H); - for (let ue = 0; ue < he.length; ue++) - he[ue].order = ue; + if (Le !== y) { + y = Le; + const ue = [...fe]; + ue.splice(Le, 0, O); + for (let we = 0; we < ue.length; we++) + ue[we].order = we; e.closest(".flow-container")?.classList.add("flow-layout-animating"), v._layoutAnimFrame && cancelAnimationFrame(v._layoutAnimFrame); - const Me = H.id, X = _, ae = X !== H.parentId; + const X = O.id, le = _, pe = le !== O.parentId; v._layoutAnimFrame = requestAnimationFrame(() => { - if (ae && X) { - const de = v.getNode(Me); - let xe; - if (de) { - const Se = v._initialDimensions?.get(Me); - xe = { ...de, dimensions: Se ? { ...Se } : void 0 }; + if (pe && le) { + const ye = v.getNode(X); + let Se; + if (ye) { + const De = v._initialDimensions?.get(X); + Se = { ...ye, dimensions: De ? { ...De } : void 0 }; } - v.layoutChildren(X, { - excludeId: Me, - includeNode: xe, + v.layoutChildren(le, { + excludeId: X, + includeNode: Se, shallow: !0 - }), v.propagateLayoutUp(X, { - includeNode: xe + }), v.propagateLayoutUp(le, { + includeNode: Se }); } else - v.layoutChildren(X, Me, !0); - const ue = performance.now(), ce = 300, le = () => { - v._layoutAnimTick++, performance.now() - ue < ce ? v._layoutAnimFrame = requestAnimationFrame(le) : v._layoutAnimFrame = 0; + v.layoutChildren(le, X, !0); + const we = performance.now(), me = 300, ce = () => { + v._layoutAnimTick++, performance.now() - we < me ? v._layoutAnimFrame = requestAnimationFrame(ce) : v._layoutAnimFrame = 0; }; - v._layoutAnimFrame = requestAnimationFrame(le); + v._layoutAnimFrame = requestAnimationFrame(ce); }); } } - u && j instanceof MouseEvent && u.updatePointer(j.clientX, j.clientY); + u && K instanceof MouseEvent && u.updatePointer(K.clientX, K.clientY); return; } - if (H.extent === "parent" && Z?.dimensions) { - const ee = Po( - { x: J, y: se }, + if (O.extent === "parent" && ee?.dimensions) { + const te = To( + { x: re, y: se }, oe, - Z.dimensions + ee.dimensions ); - J = ee.x, se = ee.y; - } else if (Array.isArray(H.extent)) { - const ee = da({ x: J, y: se }, H.extent, oe); - J = ee.x, se = ee.y; + re = te.x, se = te.y; + } else if (Array.isArray(O.extent)) { + const te = pa({ x: re, y: se }, O.extent, oe); + re = te.x, se = te.y; } - if ((!H.extent || H.extent !== "parent") && (un( - Z, + if ((!O.extent || O.extent !== "parent") && (hn( + ee, v._config?.childValidationRules ?? {} - )?.preventChildEscape || !!Z?.childLayout) && Z?.dimensions) { - const fe = Po( - { x: J, y: se }, + )?.preventChildEscape || !!ee?.childLayout) && ee?.dimensions) { + const de = To( + { x: re, y: se }, oe, - Z.dimensions + ee.dimensions ); - J = fe.x, se = fe.y; + re = de.x, se = de.y; } - if (H.expandParent && Z?.dimensions) { - const ee = uh( - { x: J, y: se }, + if (O.expandParent && ee?.dimensions) { + const te = ph( + { x: re, y: se }, oe, - Z.dimensions + ee.dimensions ); - ee && (Z.dimensions.width = ee.width, Z.dimensions.height = ee.height); + te && (ee.dimensions.width = te.width, ee.dimensions.height = te.height); } - H.position.x = J, H.position.y = se; + O.position.x = re, O.position.y = se; } else { - const U = Rn(Y, H, v._config?.nodeExtent); - H.position.x = U.x, H.position.y = U.y; + const Y = Fn(q, O, v._config?.nodeExtent); + O.position.x = Y.x, O.position.y = Y.y; } if (v._config?.snapToGrid) { - const U = H.nodeOrigin ?? v._config?.nodeOrigin ?? [0, 0], J = H.dimensions?.width ?? 150, se = H.dimensions?.height ?? 40, oe = H.parentId ? v.getAbsolutePosition(H.id) : H.position; - e.style.left = oe.x - J * U[0] + "px", e.style.top = oe.y - se * U[1] + "px", v._layoutAnimTick++; + const Y = O.nodeOrigin ?? v._config?.nodeOrigin ?? [0, 0], re = O.dimensions?.width ?? 150, se = O.dimensions?.height ?? 40, oe = O.parentId ? v.getAbsolutePosition(O.id) : O.position; + e.style.left = oe.x - re * Y[0] + "px", e.style.top = oe.y - se * Y[1] + "px", v._layoutAnimTick++; } - if (v._emit("node-drag", { node: H, position: Y }), d) - for (const [U, J] of d) { - const se = v.getNode(U); + if (v._emit("node-drag", { node: O, position: q }), d) + for (const [Y, re] of d) { + const se = v.getNode(Y); if (se) { - let oe = J.x + W.x, Z = J.y + W.y; - const ee = Rn({ x: oe, y: Z }, se, v._config?.nodeExtent); - se.position.x = ee.x, se.position.y = ee.y; + let oe = re.x + j.x, ee = re.y + j.y; + const te = Fn({ x: oe, y: ee }, se, v._config?.nodeExtent); + se.position.x = te.x, se.position.y = te.y; } } - const Q = v._config?.helperLines; - if (Q) { - const U = typeof Q == "object" ? Q.snap ?? !0 : !0, J = typeof Q == "object" ? Q.threshold ?? 5 : 5, se = (ie) => { - const pe = ie.parentId ? v.getAbsolutePosition(ie.id) : ie.position; - return hp({ ...ie, position: pe }, v._config?.nodeOrigin); - }, Z = (v.selectedNodes.size > 1 && v.selectedNodes.has(z) ? v.nodes.filter((ie) => v.selectedNodes.has(ie.id)) : [H]).map(se), ee = { - x: Math.min(...Z.map((ie) => ie.x)), - y: Math.min(...Z.map((ie) => ie.y)), - width: Math.max(...Z.map((ie) => ie.x + ie.width)) - Math.min(...Z.map((ie) => ie.x)), - height: Math.max(...Z.map((ie) => ie.y + ie.height)) - Math.min(...Z.map((ie) => ie.y)) - }, ge = v.nodes.filter( - (ie) => !v.selectedNodes.has(ie.id) && ie.id !== z && ie.hidden !== !0 && ie.filtered !== !0 - ).map(se), fe = gp(ee, ge, J); - if (U && (fe.snapOffset.x !== 0 || fe.snapOffset.y !== 0) && (H.position.x += fe.snapOffset.x, H.position.y += fe.snapOffset.y, d)) - for (const [ie] of d) { - const pe = v.getNode(ie); - pe && (pe.position.x += fe.snapOffset.x, pe.position.y += fe.snapOffset.y); + const ne = v._config?.helperLines; + if (ne) { + const Y = typeof ne == "object" ? ne.snap ?? !0 : !0, re = typeof ne == "object" ? ne.threshold ?? 5 : 5, se = (ae) => { + const ie = ae.parentId ? v.getAbsolutePosition(ae.id) : ae.position; + return yp({ ...ae, position: ie }, v._config?.nodeOrigin); + }, ee = (v.selectedNodes.size > 1 && v.selectedNodes.has(H) ? v.nodes.filter((ae) => v.selectedNodes.has(ae.id)) : [O]).map(se), te = { + x: Math.min(...ee.map((ae) => ae.x)), + y: Math.min(...ee.map((ae) => ae.y)), + width: Math.max(...ee.map((ae) => ae.x + ae.width)) - Math.min(...ee.map((ae) => ae.x)), + height: Math.max(...ee.map((ae) => ae.y + ae.height)) - Math.min(...ee.map((ae) => ae.y)) + }, he = v.nodes.filter( + (ae) => !v.selectedNodes.has(ae.id) && ae.id !== H && ae.hidden !== !0 && ae.filtered !== !0 + ).map(se), de = wp(te, he, re); + if (Y && (de.snapOffset.x !== 0 || de.snapOffset.y !== 0) && (O.position.x += de.snapOffset.x, O.position.y += de.snapOffset.y, d)) + for (const [ae] of d) { + const ie = v.getNode(ae); + ie && (ie.position.x += de.snapOffset.x, ie.position.y += de.snapOffset.y); } - if (f?.remove(), fe.horizontal.length > 0 || fe.vertical.length > 0) { - const ie = B?.querySelector(".flow-viewport"); - if (ie) { - const pe = v.nodes.map(se); - f = bp(fe.horizontal, fe.vertical, pe), ie.appendChild(f); + if (f?.remove(), de.horizontal.length > 0 || de.vertical.length > 0) { + const ae = Z?.querySelector(".flow-viewport"); + if (ae) { + const ie = v.nodes.map(se); + f = Sp(de.horizontal, de.vertical, ie), ae.appendChild(f); } } else f = null; v._emit("helper-lines-change", { - horizontal: fe.horizontal, - vertical: fe.vertical + horizontal: de.horizontal, + vertical: de.vertical }); } } if (v._config?.preventOverlap) { - const Q = typeof v._config.preventOverlap == "number" ? v._config.preventOverlap : 5, U = H.dimensions?.width ?? we, J = H.dimensions?.height ?? _e, se = v.selectedNodes, oe = v.nodes.filter((ee) => ee.id !== H.id && !ee.hidden && !se.has(ee.id)).map((ee) => jt(ee, v._config?.nodeOrigin)), Z = zh(H.position, U, J, oe, Q); - H.position.x = Z.x, H.position.y = Z.y; + const ne = typeof v._config.preventOverlap == "number" ? v._config.preventOverlap : 5, Y = O.dimensions?.width ?? ve, re = O.dimensions?.height ?? be, se = v.selectedNodes, oe = v.nodes.filter((te) => te.id !== O.id && !te.hidden && !se.has(te.id)).map((te) => Gt(te, v._config?.nodeOrigin)), ee = Xh(O.position, Y, re, oe, ne); + O.position.x = ee.x, O.position.y = ee.y; } - if (!H.parentId) { - const Q = wt(H.id, v.nodes), U = v.nodes.filter( - (ee) => ee.id !== H.id && ee.droppable && !ee.hidden && !Q.has(ee.id) && (!ee.acceptsDrop || ee.acceptsDrop(H)) - ), J = jt(H, v._config?.nodeOrigin); + if (!O.parentId) { + const ne = wt(O.id, v.nodes), Y = v.nodes.filter( + (te) => te.id !== O.id && te.droppable && !te.hidden && !ne.has(te.id) && (!te.acceptsDrop || te.acceptsDrop(O)) + ), re = Gt(O, v._config?.nodeOrigin); let se = null; const oe = 12; - for (const ee of U) { - const ge = ee.parentId ? v.getAbsolutePosition(ee.id) : ee.position, fe = ee.dimensions?.width ?? we, ie = ee.dimensions?.height ?? _e, pe = J.x + J.width / 2, re = J.y + J.height / 2, me = ee.id === g ? 0 : oe; - pe >= ge.x + me && pe <= ge.x + fe - me && re >= ge.y + me && re <= ge.y + ie - me && (se = ee); + for (const te of Y) { + const he = te.parentId ? v.getAbsolutePosition(te.id) : te.position, de = te.dimensions?.width ?? ve, ae = te.dimensions?.height ?? be, ie = re.x + re.width / 2, fe = re.y + re.height / 2, ge = te.id === g ? 0 : oe; + ie >= he.x + ge && ie <= he.x + de - ge && fe >= he.y + ge && fe <= he.y + ae - ge && (se = te); } - const Z = se?.id ?? null; - Z !== g && (g && B && B.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), Z && B && B.querySelector(`[data-flow-node-id="${CSS.escape(Z)}"]`)?.classList.add("flow-node-drop-target"), g = Z); + const ee = se?.id ?? null; + ee !== g && (g && Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), ee && Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(ee)}"]`)?.classList.add("flow-node-drop-target"), g = ee); } if (v._config?.proximityConnect) { - const Q = v._config.proximityConnectDistance ?? 150, U = H.dimensions ?? { width: 150, height: 50 }, J = { - x: H.position.x + U.width / 2, - y: H.position.y + U.height / 2 - }, se = v.nodes.filter((Z) => Z.id !== H.id && !Z.hidden).map((Z) => ({ - id: Z.id, + const ne = v._config.proximityConnectDistance ?? 150, Y = O.dimensions ?? { width: 150, height: 50 }, re = { + x: O.position.x + Y.width / 2, + y: O.position.y + Y.height / 2 + }, se = v.nodes.filter((ee) => ee.id !== O.id && !ee.hidden).map((ee) => ({ + id: ee.id, center: { - x: Z.position.x + (Z.dimensions?.width ?? 150) / 2, - y: Z.position.y + (Z.dimensions?.height ?? 50) / 2 + x: ee.position.x + (ee.dimensions?.width ?? 150) / 2, + y: ee.position.y + (ee.dimensions?.height ?? 50) / 2 } - })), oe = mp(H.id, J, se, Q); + })), oe = _p(O.id, re, se, ne); if (oe) if (v.edges.some( - (ee) => ee.source === oe.source && ee.target === oe.target || ee.source === oe.target && ee.target === oe.source + (te) => te.source === oe.source && te.target === oe.target || te.source === oe.target && te.target === oe.source )) h?.destroy(), h = null, p = null; else { if (p = oe, !h) { - h = Wt({ + h = Ut({ connectionLineType: v._config?.connectionLineType, connectionLineStyle: v._config?.connectionLineStyle, connectionLine: v._config?.connectionLine }); - const ee = B?.querySelector(".flow-viewport"); - ee && ee.appendChild(h.svg); + const te = Z?.querySelector(".flow-viewport"); + te && te.appendChild(h.svg); } h.update({ - fromX: J.x, - fromY: J.y, + fromX: re.x, + fromY: re.y, toX: oe.targetCenter.x, toY: oe.targetCenter.y, source: oe.source @@ -14274,151 +14287,151 @@ function xp(t) { else h?.destroy(), h = null, p = null; } - const te = v._container ? He.get(v._container) : void 0; - if (te?.bridge) { - if (te.bridge.pushLocalNodeUpdate(z, { position: H.position }), d) - for (const [Q] of d) { - const U = v.getNode(Q); - U && te.bridge.pushLocalNodeUpdate(Q, { position: U.position }); + const J = v._container ? He.get(v._container) : void 0; + if (J?.bridge) { + if (J.bridge.pushLocalNodeUpdate(H, { position: O.position }), d) + for (const [ne] of d) { + const Y = v.getNode(ne); + Y && J.bridge.pushLocalNodeUpdate(ne, { position: Y.position }); } - if (te.awareness && j instanceof MouseEvent && v._container) { - const Q = v._container.getBoundingClientRect(), U = v._viewportLive ?? v.viewport, J = (j.clientX - Q.left - U.x) / U.zoom, se = (j.clientY - Q.top - U.y) / U.zoom; - te.awareness.updateCursor({ x: J, y: se }); + if (J.awareness && K instanceof MouseEvent && v._container) { + const ne = v._container.getBoundingClientRect(), Y = v._viewportLive ?? v.viewport, re = (K.clientX - ne.left - Y.x) / Y.zoom, se = (K.clientY - ne.top - Y.y) / Y.zoom; + J.awareness.updateCursor({ x: re, y: se }); } } - u && j instanceof MouseEvent && u.updatePointer(j.clientX, j.clientY); + u && K instanceof MouseEvent && u.updatePointer(K.clientX, K.clientY); }, - onDragEnd({ nodeId: z, position: Y }) { - const W = d ? [z, ...d.keys()] : [z]; - v._draggingNodeIds.clear(), e.classList.remove("flow-node-dragging"), V("drag", `Node "${z}" drag end`, Y); - const j = v._container ? He.get(v._container) : void 0; - j?.bridge && j.bridge.setDragging(z, !1), u?.stop(), u = null, f?.remove(), f = null, v._config?.helperLines && v._emit("helper-lines-change", { horizontal: [], vertical: [] }); - const H = v.getNode(z); - if (H && v._emit("node-drag-end", { node: H, position: Y }), m && H?.parentId) { + onDragEnd({ nodeId: H, position: q }) { + const j = d ? [H, ...d.keys()] : [H]; + v._draggingNodeIds.clear(), e.classList.remove("flow-node-dragging"), B("drag", `Node "${H}" drag end`, q); + const K = v._container ? He.get(v._container) : void 0; + K?.bridge && K.bridge.setDragging(H, !1), u?.stop(), u = null, f?.remove(), f = null, v._config?.helperLines && v._emit("helper-lines-change", { horizontal: [], vertical: [] }); + const O = v.getNode(H); + if (O && v._emit("node-drag-end", { node: O, position: q }), m && O?.parentId) { e.classList.remove("flow-reorder-dragging"); - const te = _; - m = !1, y = -1, _ = null, v._layoutAnimFrame && (cancelAnimationFrame(v._layoutAnimFrame), v._layoutAnimFrame = 0), e.closest(".flow-container")?.classList.remove("flow-layout-animating"), g ? (B && B.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), Do(v, z, g), g = null) : te && te !== H.parentId ? (v.layoutChildren(te, { omitFromComputation: z, shallow: !0 }), v.propagateLayoutUp(te, { omitFromComputation: z }), v.layoutChildren(H.parentId), v._emit("child-reorder", { - nodeId: z, - parentId: H.parentId, - order: H.order - })) : (v.layoutChildren(H.parentId), v._emit("child-reorder", { - nodeId: z, - parentId: H.parentId, - order: H.order - })), d = null, v._layoutAnimTick++, v._commitNodeGeometry(W), Ws(v, a, l), l = null, a = !1; + const J = _; + m = !1, y = -1, _ = null, v._layoutAnimFrame && (cancelAnimationFrame(v._layoutAnimFrame), v._layoutAnimFrame = 0), e.closest(".flow-container")?.classList.remove("flow-layout-animating"), g ? (Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), Ho(v, H, g), g = null) : J && J !== O.parentId ? (v.layoutChildren(J, { omitFromComputation: H, shallow: !0 }), v.propagateLayoutUp(J, { omitFromComputation: H }), v.layoutChildren(O.parentId), v._emit("child-reorder", { + nodeId: H, + parentId: O.parentId, + order: O.order + })) : (v.layoutChildren(O.parentId), v._emit("child-reorder", { + nodeId: H, + parentId: O.parentId, + order: O.order + })), d = null, v._layoutAnimTick++, v._commitNodeGeometry(j), Gs(v, a, l), l = null, a = !1; return; } - if (H && g) - B && B.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), Do(v, z, g), g = null; - else if (H && H.parentId && !g) { - const te = un( - v.getNode(H.parentId), + if (O && g) + Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), Ho(v, H, g), g = null; + else if (O && O.parentId && !g) { + const J = hn( + v.getNode(O.parentId), v._config?.childValidationRules ?? {} - ), Q = v.getNode(H.parentId); - if (!te?.preventChildEscape && !Q?.childLayout && Q?.dimensions) { - const U = H.position.x, J = H.position.y, se = H.dimensions?.width ?? 150, oe = H.dimensions?.height ?? 50; - (U + se < 0 || J + oe < 0 || U > Q.dimensions.width || J > Q.dimensions.height) && Do(v, z, null); + ), ne = v.getNode(O.parentId); + if (!J?.preventChildEscape && !ne?.childLayout && ne?.dimensions) { + const Y = O.position.x, re = O.position.y, se = O.dimensions?.width ?? 150, oe = O.dimensions?.height ?? 50; + (Y + se < 0 || re + oe < 0 || Y > ne.dimensions.width || re > ne.dimensions.height) && Ho(v, H, null); } g = null; } else - g && B && B.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), g = null; + g && Z && Z.querySelector(`[data-flow-node-id="${CSS.escape(g)}"]`)?.classList.remove("flow-node-drop-target"), g = null; if (v._config?.proximityConnect && p) { - const te = p; + const J = p; h?.destroy(), h = null, p = null; - let Q = !0; + let ne = !0; if (v._config.onProximityConnect && v._config.onProximityConnect({ - source: te.source, - target: te.target, - distance: te.distance - }) === !1 && (Q = !1), Q) { - const U = { - source: te.source, + source: J.source, + target: J.target, + distance: J.distance + }) === !1 && (ne = !1), ne) { + const Y = { + source: J.source, sourceHandle: "source", - target: te.target, + target: J.target, targetHandle: "target" }; - if (pt(U, v.edges, { preventCycles: v._config?.preventCycles }) && gt(U, v._config?.connectionRules, v._nodeMap) && (B ? nt(B, U, v.edges) : !0) && (B ? tt(B, U) : !0) && (!v._config.isValidConnection || v._config.isValidConnection(U))) { + if (pt(Y, v.edges, { preventCycles: v._config?.preventCycles }) && gt(Y, v._config?.connectionRules, v._nodeMap) && (Z ? nt(Z, Y, v.edges) : !0) && (Z ? tt(Z, Y) : !0) && (!v._config.isValidConnection || v._config.isValidConnection(Y))) { if (v._config.proximityConnectConfirm) { - const ge = B?.querySelector(`[data-flow-node-id="${CSS.escape(te.source)}"]`), fe = B?.querySelector(`[data-flow-node-id="${CSS.escape(te.target)}"]`); - ge?.classList.add("flow-proximity-confirm"), fe?.classList.add("flow-proximity-confirm"), setTimeout(() => { - ge?.classList.remove("flow-proximity-confirm"), fe?.classList.remove("flow-proximity-confirm"); + const he = Z?.querySelector(`[data-flow-node-id="${CSS.escape(J.source)}"]`), de = Z?.querySelector(`[data-flow-node-id="${CSS.escape(J.target)}"]`); + he?.classList.add("flow-proximity-confirm"), de?.classList.add("flow-proximity-confirm"), setTimeout(() => { + he?.classList.remove("flow-proximity-confirm"), de?.classList.remove("flow-proximity-confirm"); }, 400); } - const ee = `e-${te.source}-${te.target}-${Date.now()}-${wp++}`; - v.addEdges({ id: ee, ...U }), v._emit("connect", { connection: U }); + const te = `e-${J.source}-${J.target}-${Date.now()}-${xp++}`; + v.addEdges({ id: te, ...Y }), v._emit("connect", { connection: Y }); } } } else h?.destroy(), h = null, p = null; - d = null, a && (v._layoutAnimTick++, v._commitNodeGeometry(W)), Ws(v, a, l), l = null, a = !1; + d = null, a && (v._layoutAnimTick++, v._commitNodeGeometry(j)), Gs(v, a, l), l = null, a = !1; } })); }); { const P = t.$data(e.closest("[x-data]")); if (P?._config?.easyConnect) { - const v = P._config.easyConnectKey ?? "alt", w = (N) => { - if (!vp(N, v) || N.target.closest("[data-flow-handle-type]")) return; - const k = t.$data(e.closest("[x-data]")); - if (!k || k._animationLocked || k._connectValidating) return; + const v = P._config.easyConnectKey ?? "alt", w = (I) => { + if (!Ep(I, v) || I.target.closest("[data-flow-handle-type]")) return; + const M = t.$data(e.closest("[x-data]")); + if (!M || M._animationLocked || M._connectValidating) return; const R = o(n); if (!R) return; - const O = k.getNode(R.id); - if (!O || O.connectable === !1) return; - N.preventDefault(), N.stopPropagation(), N.stopImmediatePropagation(); - const q = _p(e, N.clientX, N.clientY), D = q?.dataset.flowHandleId ?? "source"; + const z = M.getNode(R.id); + if (!z || z.connectable === !1) return; + I.preventDefault(), I.stopPropagation(), I.stopImmediatePropagation(); + const V = Cp(e, I.clientX, I.clientY), $ = V?.dataset.flowHandleId ?? "source"; e.classList.add("flow-easy-connecting"); - const L = e.closest(".flow-container"); - if (!L) return; - const $ = k._viewportLive ?? k.viewport, F = $?.zoom || 1, K = $?.x || 0, ne = $?.y || 0, G = L.getBoundingClientRect(); - let B, z; - if (q) { - const J = q.getBoundingClientRect(); - B = (J.left + J.width / 2 - G.left - K) / F, z = (J.top + J.height / 2 - G.top - ne) / F; + const S = e.closest(".flow-container"); + if (!S) return; + const N = M._viewportLive ?? M.viewport, F = N?.zoom || 1, W = N?.x || 0, Q = N?.y || 0, U = S.getBoundingClientRect(); + let G, Z; + if (V) { + const Y = V.getBoundingClientRect(); + G = (Y.left + Y.width / 2 - U.left - W) / F, Z = (Y.top + Y.height / 2 - U.top - Q) / F; } else { - const J = e.getBoundingClientRect(); - B = (J.left + J.width / 2 - G.left - K) / F, z = (J.top + J.height / 2 - G.top - ne) / F; + const Y = e.getBoundingClientRect(); + G = (Y.left + Y.width / 2 - U.left - W) / F, Z = (Y.top + Y.height / 2 - U.top - Q) / F; } - k._emit("connect-start", { source: R.id, sourceHandle: D }); - const Y = Wt({ - connectionLineType: k._config?.connectionLineType, - connectionLineStyle: k._config?.connectionLineStyle, - connectionLine: k._config?.connectionLine - }), W = L.querySelector(".flow-viewport"); - W && W.appendChild(Y.svg), Y.update({ fromX: B, fromY: z, toX: B, toY: z, source: R.id, sourceHandle: D }), k.pendingConnection = { source: R.id, sourceHandle: D, position: { x: B, y: z } }, bn(L, R.id, D, k); - let j = vo(L, k, N.clientX, N.clientY), H = null; - const te = k._config?.connectionSnapRadius ?? 20, Q = (J) => { - const se = k.screenToFlowPosition(J.clientX, J.clientY), oe = _n({ - containerEl: L, + M._emit("connect-start", { source: R.id, sourceHandle: $ }); + const H = Ut({ + connectionLineType: M._config?.connectionLineType, + connectionLineStyle: M._config?.connectionLineStyle, + connectionLine: M._config?.connectionLine + }), q = S.querySelector(".flow-viewport"); + q && q.appendChild(H.svg), H.update({ fromX: G, fromY: Z, toX: G, toY: Z, source: R.id, sourceHandle: $ }), M.pendingConnection = { source: R.id, sourceHandle: $, position: { x: G, y: Z } }, En(S, R.id, $, M); + let j = _o(S, M, I.clientX, I.clientY), K = null; + const O = M._config?.connectionSnapRadius ?? 20, J = (Y) => { + const re = M.screenToFlowPosition(Y.clientX, Y.clientY), se = xn({ + containerEl: S, handleType: "target", excludeNodeId: R.id, - cursorFlowPos: se, - connectionSnapRadius: te, - getNode: (Z) => k.getNode(Z), - toFlowPosition: (Z, ee) => k.screenToFlowPosition(Z, ee) + cursorFlowPos: re, + connectionSnapRadius: O, + getNode: (oe) => M.getNode(oe), + toFlowPosition: (oe, ee) => M.screenToFlowPosition(oe, ee) }); - oe.element !== H && (H?.classList.remove("flow-handle-active"), oe.element?.classList.add("flow-handle-active"), H = oe.element), Y.update({ fromX: B, fromY: z, toX: oe.position.x, toY: oe.position.y, source: R.id, sourceHandle: D }), k.pendingConnection = { ...k.pendingConnection, position: oe.position }, j?.updatePointer(J.clientX, J.clientY); - }, U = async (J) => { - j?.stop(), j = null, document.removeEventListener("pointermove", Q), document.removeEventListener("pointerup", U), Y.destroy(), H?.classList.remove("flow-handle-active"), ke(L), e.classList.remove("flow-easy-connecting"); - const se = k.screenToFlowPosition(J.clientX, J.clientY), oe = { source: R.id, sourceHandle: D, position: se }; - k.pendingConnection = null; - let Z = H; - if (Z || (Z = document.elementFromPoint(J.clientX, J.clientY)?.closest('[data-flow-handle-type="target"]')), !Z) { - k._emit("connect-end", { connection: null, ...oe }); + se.element !== K && (K?.classList.remove("flow-handle-active"), se.element?.classList.add("flow-handle-active"), K = se.element), H.update({ fromX: G, fromY: Z, toX: se.position.x, toY: se.position.y, source: R.id, sourceHandle: $ }), M.pendingConnection = { ...M.pendingConnection, position: se.position }, j?.updatePointer(Y.clientX, Y.clientY); + }, ne = async (Y) => { + j?.stop(), j = null, document.removeEventListener("pointermove", J), document.removeEventListener("pointerup", ne), H.destroy(), K?.classList.remove("flow-handle-active"), Pe(S), e.classList.remove("flow-easy-connecting"); + const re = M.screenToFlowPosition(Y.clientX, Y.clientY), se = { source: R.id, sourceHandle: $, position: re }; + M.pendingConnection = null; + let oe = K; + if (oe || (oe = document.elementFromPoint(Y.clientX, Y.clientY)?.closest('[data-flow-handle-type="target"]')), !oe) { + M._emit("connect-end", { connection: null, ...se }); return; } - const ge = Z.closest("[x-flow-node]")?.dataset.flowNodeId, fe = Z.dataset.flowHandleId ?? "target"; - if (!ge) { - k._emit("connect-end", { connection: null, ...oe }); + const te = oe.closest("[x-flow-node]")?.dataset.flowNodeId, he = oe.dataset.flowHandleId ?? "target"; + if (!te) { + M._emit("connect-end", { connection: null, ...se }); return; } - const ie = { source: R.id, sourceHandle: D, target: ge, targetHandle: fe }, pe = await ea({ connection: ie, canvas: k, containerEl: L }); - k._emit("connect-end", { - connection: pe.applied ? ie : null, - ...oe + const de = { source: R.id, sourceHandle: $, target: te, targetHandle: he }, ae = await sa({ connection: de, canvas: M, containerEl: S }); + M._emit("connect-end", { + connection: ae.applied ? de : null, + ...se }); }; - document.addEventListener("pointermove", Q), document.addEventListener("pointerup", U); + document.addEventListener("pointermove", J), document.addEventListener("pointerup", ne); }; e.addEventListener("pointerdown", w, { capture: !0 }), r(() => { e.removeEventListener("pointerdown", w, { capture: !0 }); @@ -14431,10 +14444,10 @@ function xp(t) { const v = o(n); if (!v) return; const w = t.$data(e.closest("[x-data]")); - w && (w._animationLocked || ei(v) && (w._emit("node-click", { node: v, event: P }), P.stopPropagation(), mt(P, w._shortcuts?.multiSelect) ? w.selectedNodes.has(v.id) ? (w.selectedNodes.delete(v.id), v.selected = !1) : (w.selectedNodes.add(v.id), v.selected = !0) : (w.deselectAll(), w.selectedNodes.add(v.id), v.selected = !0), w._emitSelectionChange())); + w && (w._animationLocked || ni(v) && (w._emit("node-click", { node: v, event: P }), P.stopPropagation(), mt(P, w._shortcuts?.multiSelect) ? w.selectedNodes.has(v.id) ? (w.selectedNodes.delete(v.id), v.selected = !1) : (w.selectedNodes.add(v.id), v.selected = !0) : (w.deselectAll(), w.selectedNodes.add(v.id), v.selected = !0), w._emitSelectionChange())); }; e.addEventListener("keydown", T); - const I = () => { + const D = () => { const P = t.$data(e.closest("[x-data]")); if (!P?._config?.autoPanOnNodeFocus) return; const v = o(n); @@ -14445,21 +14458,21 @@ function xp(t) { w.y + (v.dimensions?.height ?? 40) / 2 ); }; - e.addEventListener("focus", I); - const M = (P) => { + e.addEventListener("focus", D); + const L = (P) => { if (a) return; const v = o(n); if (!v) return; const w = t.$data(e.closest("[x-data]")); - if (w && !w._animationLocked && (w._emit("node-click", { node: v, event: P }), !!ei(v))) { + if (w && !w._animationLocked && (w._emit("node-click", { node: v, event: P }), !!ni(v))) { if (P.stopPropagation(), c) { c = !1; return; } - mt(P, w._shortcuts?.multiSelect) ? w.selectedNodes.has(v.id) ? (w.selectedNodes.delete(v.id), v.selected = !1, e.classList.remove("flow-node-selected"), V("selection", `Node "${v.id}" deselected (shift)`)) : (w.selectedNodes.add(v.id), v.selected = !0, e.classList.add("flow-node-selected"), V("selection", `Node "${v.id}" selected (shift)`)) : (w.deselectAll(), w.selectedNodes.add(v.id), v.selected = !0, e.classList.add("flow-node-selected"), V("selection", `Node "${v.id}" selected`)), w._emitSelectionChange(); + mt(P, w._shortcuts?.multiSelect) ? w.selectedNodes.has(v.id) ? (w.selectedNodes.delete(v.id), v.selected = !1, e.classList.remove("flow-node-selected"), B("selection", `Node "${v.id}" deselected (shift)`)) : (w.selectedNodes.add(v.id), v.selected = !0, e.classList.add("flow-node-selected"), B("selection", `Node "${v.id}" selected (shift)`)) : (w.deselectAll(), w.selectedNodes.add(v.id), v.selected = !0, e.classList.add("flow-node-selected"), B("selection", `Node "${v.id}" selected`)), w._emitSelectionChange(); } }; - e.addEventListener("click", M); + e.addEventListener("click", L); const A = (P) => { P.preventDefault(), P.stopPropagation(); const v = o(n); @@ -14467,8 +14480,8 @@ function xp(t) { const w = t.$data(e.closest("[x-data]")); if (w) if (w.selectedNodes.size > 1 && w.selectedNodes.has(v.id)) { - const N = w.nodes.filter((k) => w.selectedNodes.has(k.id)); - w._emit("selection-context-menu", { nodes: N, event: P }); + const I = w.nodes.filter((M) => w.selectedNodes.has(M.id)); + w._emit("selection-context-menu", { nodes: I, event: P }); } else w._emit("node-context-menu", { node: v, event: P }); }; @@ -14480,9 +14493,9 @@ function xp(t) { P.dimensions = { width: e.offsetWidth, height: e.offsetHeight - }, V("init", `Node "${P.id}" measured`, P.dimensions), v?._nodeElements?.set(P.id, e), P.resizeObserver !== !1 && v?._resizeObserver && v._resizeObserver.observe(e); + }, B("init", `Node "${P.id}" measured`, P.dimensions), v?._nodeElements?.set(P.id, e), P.resizeObserver !== !1 && v?._resizeObserver && v._resizeObserver.observe(e); }), r(() => { - s?.destroy(), f?.remove(), f = null, h?.destroy(), h = null, e.removeEventListener("keydown", T), e.removeEventListener("focus", I), e.removeEventListener("click", M), e.removeEventListener("contextmenu", A); + s?.destroy(), f?.remove(), f = null, h?.destroy(), h = null, e.removeEventListener("keydown", T), e.removeEventListener("focus", D), e.removeEventListener("click", L), e.removeEventListener("contextmenu", A); const P = e.dataset.flowNodeId; if (P) { const v = t.$data(e.closest("[x-data]")); @@ -14492,25 +14505,25 @@ function xp(t) { } ); } -const At = { +const $t = { minWidth: 30, minHeight: 30, maxWidth: 1 / 0, maxHeight: 1 / 0 }; -function Ep(t, e, n, o, i, r) { +function Lp(t, e, n, o, i, r) { const { minWidth: s, minHeight: a, maxWidth: l, maxHeight: c } = i, d = t.includes("left"), u = t.includes("right"), f = t.includes("top"), h = t.includes("bottom"); let p = o.width; u ? p = o.width + e.x : d && (p = o.width - e.x); let g = o.height; h ? g = o.height + e.y : f && (g = o.height - e.y), p = Math.max(s, Math.min(l, p)), g = Math.max(a, Math.min(c, g)), r && (p = r[0] * Math.round(p / r[0]), g = r[1] * Math.round(g / r[1]), p = Math.max(s, Math.min(l, p)), g = Math.max(a, Math.min(c, g))); - const m = p - o.width, y = g - o.height, _ = d ? n.x - m : n.x, S = f ? n.y - y : n.y; + const m = p - o.width, y = g - o.height, _ = d ? n.x - m : n.x, k = f ? n.y - y : n.y; return { - position: { x: _, y: S }, + position: { x: _, y: k }, dimensions: { width: p, height: g } }; } -const La = ["top-left", "top-right", "bottom-left", "bottom-right"], Pa = ["top", "right", "bottom", "left"], Cp = [...La, ...Pa], Sp = { +const Na = ["top-left", "top-right", "bottom-left", "bottom-right"], Aa = ["top", "right", "bottom", "left"], Pp = [...Na, ...Aa], Mp = { "top-left": "nwse-resize", "top-right": "nesw-resize", "bottom-left": "nesw-resize", @@ -14520,22 +14533,22 @@ const La = ["top-left", "top-right", "bottom-left", "bottom-right"], Pa = ["top" left: "ew-resize", right: "ew-resize" }; -function kp(t) { +function Tp(t) { t.directive( "flow-resizer", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { - const a = Lp(o); - let l = { ...At }; + const a = Np(o); + let l = { ...$t }; if (n) try { const d = i(n); - l = { ...At, ...d }; + l = { ...$t, ...d }; } catch { } const c = []; for (const d of a) { const u = document.createElement("div"); - u.className = `flow-resizer-handle flow-resizer-handle-${d}`, u.style.cursor = Sp[d], u.dataset.flowResizeDirection = d, e.appendChild(u), c.push(u), u.addEventListener("pointerdown", (f) => { + u.className = `flow-resizer-handle flow-resizer-handle-${d}`, u.style.cursor = Mp[d], u.dataset.flowResizeDirection = d, e.appendChild(u), c.push(u), u.addEventListener("pointerdown", (f) => { f.preventDefault(), f.stopPropagation(); const h = e.closest("[x-flow-node]"); if (!h) return; @@ -14544,23 +14557,23 @@ function kp(t) { const g = t.$data(p), m = h.dataset.flowNodeId; if (!m || !g) return; const y = g.getNode(m); - if (!y || !fs(y)) return; + if (!y || !gs(y)) return; y.fixedDimensions = !0; const _ = { ...l }; - if (y.minDimensions?.width != null && l.minWidth === At.minWidth && (_.minWidth = y.minDimensions.width), y.minDimensions?.height != null && l.minHeight === At.minHeight && (_.minHeight = y.minDimensions.height), y.maxDimensions?.width != null && l.maxWidth === At.maxWidth && (_.maxWidth = y.maxDimensions.width), y.maxDimensions?.height != null && l.maxHeight === At.maxHeight && (_.maxHeight = y.maxDimensions.height), !y.dimensions) { - const M = g.viewport?.zoom || 1, A = h.getBoundingClientRect(); - y.dimensions = { width: A.width / M, height: A.height / M }; + if (y.minDimensions?.width != null && l.minWidth === $t.minWidth && (_.minWidth = y.minDimensions.width), y.minDimensions?.height != null && l.minHeight === $t.minHeight && (_.minHeight = y.minDimensions.height), y.maxDimensions?.width != null && l.maxWidth === $t.maxWidth && (_.maxWidth = y.maxDimensions.width), y.maxDimensions?.height != null && l.maxHeight === $t.maxHeight && (_.maxHeight = y.maxDimensions.height), !y.dimensions) { + const L = g.viewport?.zoom || 1, A = h.getBoundingClientRect(); + y.dimensions = { width: A.width / L, height: A.height / L }; } - const S = { x: y.position.x, y: y.position.y }, x = { width: y.dimensions.width, height: y.dimensions.height }, b = g.viewport?.zoom || 1, C = f.clientX, E = f.clientY; - g._captureHistory?.(), V("resize", `Resize start on "${m}" (${d})`, x), g._emit("node-resize-start", { node: y, dimensions: { ...x } }); - const T = (M) => { + const k = { x: y.position.x, y: y.position.y }, x = { width: y.dimensions.width, height: y.dimensions.height }, C = g.viewport?.zoom || 1, E = f.clientX, b = f.clientY; + g._captureHistory?.(), B("resize", `Resize start on "${m}" (${d})`, x), g._emit("node-resize-start", { node: y, dimensions: { ...x } }); + const T = (L) => { const A = { - x: (M.clientX - C) / b, - y: (M.clientY - E) / b - }, P = Ep( + x: (L.clientX - E) / C, + y: (L.clientY - b) / C + }, P = Lp( d, A, - S, + k, x, _, g._config?.snapToGrid ?? !1 @@ -14571,10 +14584,10 @@ function kp(t) { } else h.style.left = `${P.position.x}px`, h.style.top = `${P.position.y}px`; h.style.width = `${P.dimensions.width}px`, h.style.height = `${P.dimensions.height}px`, g._layoutAnimTick++, g._emit("node-resize", { node: y, dimensions: { ...P.dimensions } }); - }, I = () => { - document.removeEventListener("pointermove", T), document.removeEventListener("pointerup", I), document.removeEventListener("pointercancel", I), V("resize", `Resize end on "${m}"`, y.dimensions), g._emit("node-resize-end", { node: y, dimensions: { ...y.dimensions } }); + }, D = () => { + document.removeEventListener("pointermove", T), document.removeEventListener("pointerup", D), document.removeEventListener("pointercancel", D), B("resize", `Resize end on "${m}"`, y.dimensions), g._emit("node-resize-end", { node: y, dimensions: { ...y.dimensions } }); }; - document.addEventListener("pointermove", T), document.addEventListener("pointerup", I), document.addEventListener("pointercancel", I); + document.addEventListener("pointermove", T), document.addEventListener("pointerup", D), document.addEventListener("pointercancel", D); }); } r(() => { @@ -14586,7 +14599,7 @@ function kp(t) { if (!h || !f) return; const p = f.getNode(h); if (!p) return; - const g = !fs(p); + const g = !gs(p); for (const m of c) m.style.display = g ? "none" : ""; }), s(() => { @@ -14596,11 +14609,11 @@ function kp(t) { } ); } -function Lp(t) { +function Np(t) { if (t.includes("corners")) - return La; + return Na; if (t.includes("edges")) - return Pa; + return Aa; const e = t.includes("top"), n = t.includes("bottom"), o = t.includes("left"), i = t.includes("right"); if (e || n || o || i) { if (e && o) return ["top-left"]; @@ -14612,15 +14625,15 @@ function Lp(t) { if (o) return ["left"]; if (i) return ["right"]; } - return Cp; + return Pp; } -function Pp(t, e, n, o) { +function Ap(t, e, n, o) { return (Math.atan2(t - n, -(e - o)) * 180 / Math.PI % 360 + 360) % 360; } -function Mp(t, e) { +function $p(t, e) { return (Math.round(t / e) * e % 360 + 360) % 360; } -function Tp(t) { +function Ip(t) { t.directive( "flow-rotate", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -14638,18 +14651,18 @@ function Tp(t) { if (!g) return; const m = u.getBoundingClientRect(), y = m.left + m.width / 2, _ = m.top + m.height / 2; h._captureHistory(), e.style.cursor = "grabbing"; - const S = (b) => { - let C = Pp( - b.clientX, - b.clientY, + const k = (C) => { + let E = Ap( + C.clientX, + C.clientY, y, _ ); - a && (C = Mp(C, l)), g.rotation = C; + a && (E = $p(E, l)), g.rotation = E; }, x = () => { - document.removeEventListener("pointermove", S), document.removeEventListener("pointerup", x), e.style.cursor = "grab", h._emit("node-rotate-end", { node: g, rotation: g.rotation }); + document.removeEventListener("pointermove", k), document.removeEventListener("pointerup", x), e.style.cursor = "grab", h._emit("node-rotate-end", { node: g, rotation: g.rotation }); }; - document.addEventListener("pointermove", S), document.addEventListener("pointerup", x); + document.addEventListener("pointermove", k), document.addEventListener("pointerup", x); }; e.addEventListener("pointerdown", c), s(() => { e.removeEventListener("pointerdown", c), e.classList.remove("flow-rotate-handle"); @@ -14657,7 +14670,7 @@ function Tp(t) { } ); } -function Ap(t) { +function Dp(t) { t.directive( "flow-drag-handle", (e) => { @@ -14667,20 +14680,20 @@ function Ap(t) { } ); } -const Np = "application/alpineflow"; -function $p(t) { +const Rp = "application/alpineflow"; +function Hp(t) { t.directive( "flow-draggable", (e, { expression: n }, { evaluate: o }) => { e.setAttribute("draggable", "true"), e.style.cursor = "grab", e.addEventListener("dragstart", (i) => { if (!i.dataTransfer) return; const r = o(n), s = typeof r == "string" ? r : JSON.stringify(r); - i.dataTransfer.setData(Np, s), i.dataTransfer.effectAllowed = "move"; + i.dataTransfer.setData(Rp, s), i.dataTransfer.effectAllowed = "move"; }); } ); } -function Ip(t) { +function Fp(t) { const e = [], n = /* @__PURE__ */ new Set(); for (const o of t) { if (n.has(o.id) || o.source === o.target) @@ -14692,7 +14705,7 @@ function Ip(t) { } return e; } -function Dp(t) { +function Op(t) { t.directive( "flow-viewport", (e, {}, { effect: n, cleanup: o }) => { @@ -14722,7 +14735,7 @@ function Dp(t) { u && u.remove(); const f = !!i._config?.collapseBidirectionalEdges, h = /* @__PURE__ */ new Set(), p = /* @__PURE__ */ new Set(); if (f) { - const g = Ip( + const g = Fp( l ); for (const m of g) @@ -14735,8 +14748,8 @@ function Dp(t) { for (const g of l) { const m = a.get(g.id); if (!m) continue; - const y = i.getNode?.(g.source), _ = i.getNode?.(g.target), S = g.hidden || g._hiddenByCollapse || y?.hidden || _?.hidden; - m.style.display = S ? "none" : ""; + const y = i.getNode?.(g.source), _ = i.getNode?.(g.target), k = g.hidden || g._hiddenByCollapse || y?.hidden || _?.hidden; + m.style.display = k ? "none" : ""; } for (const g of l) { const m = a.get(g.id); @@ -14752,7 +14765,7 @@ function Dp(t) { } ); } -const Hp = [ +const zp = [ "top", "bottom", "left", @@ -14761,28 +14774,28 @@ const Hp = [ "top-right", "bottom-left", "bottom-right" -], Rp = "a, button, input, textarea, select, [contenteditable]", Fp = 100, Op = 60, zp = /* @__PURE__ */ new Set(["top", "top-left", "top-right"]), Vp = /* @__PURE__ */ new Set(["bottom", "bottom-left", "bottom-right"]), Bp = /* @__PURE__ */ new Set(["left", "top-left", "bottom-left"]), qp = /* @__PURE__ */ new Set(["right", "top-right", "bottom-right"]); -function Yp(t, e) { +], Vp = "a, button, input, textarea, select, [contenteditable]", Bp = 100, qp = 60, Xp = /* @__PURE__ */ new Set(["top", "top-left", "top-right"]), Yp = /* @__PURE__ */ new Set(["bottom", "bottom-left", "bottom-right"]), Wp = /* @__PURE__ */ new Set(["left", "top-left", "bottom-left"]), jp = /* @__PURE__ */ new Set(["right", "top-right", "bottom-right"]); +function Up(t, e) { const n = new Set(e), o = n.has("static"), i = n.has("no-resize") || n.has("noresize"), r = n.has("locked"), s = n.has("constrained"); let a = n.has("fill-width") || n.has("fill"), l = n.has("fill-height") || n.has("fill"); - return { position: t && Hp.includes(t) ? t : "top-right", isStatic: o, isFixed: r, noResize: i, constrained: s, fillWidth: a, fillHeight: l }; + return { position: t && zp.includes(t) ? t : "top-right", isStatic: o, isFixed: r, noResize: i, constrained: s, fillWidth: a, fillHeight: l }; } -function Nt(t, e, n) { +function It(t, e, n) { t.dispatchEvent(new CustomEvent(`flow-${e}`, { bubbles: !0, detail: n })); } -function Xp(t, e, n, o, i, r) { +function Gp(t, e, n, o, i, r) { return { left: Math.max(0, Math.min(t, i - n)), top: Math.max(0, Math.min(e, r - o)) }; } -function Wp(t, e, n, o) { - t.style.transform = "none", t.style.borderRadius = "0", n && (t.style.left = "0", t.style.right = "0", t.style.width = "auto"), o && (t.style.top = "0", t.style.bottom = "0", t.style.height = "auto"), n && !o && (zp.has(e) && (t.style.top = "0"), Vp.has(e) && (t.style.bottom = "0")), o && !n && (Bp.has(e) && (t.style.left = "0"), qp.has(e) && (t.style.right = "0")); +function Zp(t, e, n, o) { + t.style.transform = "none", t.style.borderRadius = "0", n && (t.style.left = "0", t.style.right = "0", t.style.width = "auto"), o && (t.style.top = "0", t.style.bottom = "0", t.style.height = "auto"), n && !o && (Xp.has(e) && (t.style.top = "0"), Yp.has(e) && (t.style.bottom = "0")), o && !n && (Wp.has(e) && (t.style.left = "0"), jp.has(e) && (t.style.right = "0")); } -function jp(t) { +function Kp(t) { t.directive( "flow-panel", (e, { value: n, modifiers: o }, { cleanup: i }) => { @@ -14794,9 +14807,9 @@ function jp(t) { constrained: c, fillWidth: d, fillHeight: u - } = Yp(n, o), f = d || u, h = !s && !a && !f, p = !s && !l && !f; - e.classList.add("flow-panel", `flow-panel-${r}`), s && e.classList.add("flow-panel-static"), (a || f) && e.classList.add("flow-panel-locked"), (l || f) && e.classList.add("flow-panel-no-resize"), d && e.classList.add("flow-panel-fill-width"), u && e.classList.add("flow-panel-fill-height"), f && Wp(e, r, d, u); - const g = (b) => b.stopPropagation(); + } = Up(n, o), f = d || u, h = !s && !a && !f, p = !s && !l && !f; + e.classList.add("flow-panel", `flow-panel-${r}`), s && e.classList.add("flow-panel-static"), (a || f) && e.classList.add("flow-panel-locked"), (l || f) && e.classList.add("flow-panel-no-resize"), d && e.classList.add("flow-panel-fill-width"), u && e.classList.add("flow-panel-fill-height"), f && Zp(e, r, d, u); + const g = (C) => C.stopPropagation(); e.addEventListener("mousedown", g), e.addEventListener("pointerdown", g), e.addEventListener("wheel", g); const m = e.parentElement, y = { left: e.style.left, @@ -14807,96 +14820,96 @@ function jp(t) { width: e.style.width, height: e.style.height, borderRadius: e.style.borderRadius - }, _ = `flow-panel-${r}`, S = () => { + }, _ = `flow-panel-${r}`, k = () => { e.style.left = y.left, e.style.top = y.top, e.style.right = y.right, e.style.bottom = y.bottom, e.style.transform = y.transform, e.style.width = y.width, e.style.height = y.height, e.style.borderRadius = y.borderRadius, e.classList.contains(_) || e.classList.add(_); }; - m.addEventListener("flow-panel-reset", S), m.__flowPanels || (m.__flowPanels = /* @__PURE__ */ new Set()), m.__flowPanels.add(e); + m.addEventListener("flow-panel-reset", k), m.__flowPanels || (m.__flowPanels = /* @__PURE__ */ new Set()), m.__flowPanels.add(e); let x = null; if (h) { - let b = !1, C = 0, E = 0, T = 0, I = 0; - const M = () => { - const w = e.getBoundingClientRect(), N = m.getBoundingClientRect(); + let C = !1, E = 0, b = 0, T = 0, D = 0; + const L = () => { + const w = e.getBoundingClientRect(), I = m.getBoundingClientRect(); return { - x: w.left - N.left, - y: w.top - N.top + x: w.left - I.left, + y: w.top - I.top }; }, A = (w) => { - if (!b) return; - let N = T + (w.clientX - C), k = I + (w.clientY - E); + if (!C) return; + let I = T + (w.clientX - E), M = D + (w.clientY - b); if (c) { - const R = Xp( - N, - k, + const R = Gp( + I, + M, e.offsetWidth, e.offsetHeight, m.clientWidth, m.clientHeight ); - N = R.left, k = R.top; + I = R.left, M = R.top; } - e.style.left = `${N}px`, e.style.top = `${k}px`, Nt(m, "panel-drag", { + e.style.left = `${I}px`, e.style.top = `${M}px`, It(m, "panel-drag", { panel: e, - position: { x: N, y: k } + position: { x: I, y: M } }); }, P = () => { - if (!b) return; - b = !1, document.removeEventListener("pointermove", A), document.removeEventListener("pointerup", P), document.removeEventListener("pointercancel", P); - const w = M(); - Nt(m, "panel-drag-end", { + if (!C) return; + C = !1, document.removeEventListener("pointermove", A), document.removeEventListener("pointerup", P), document.removeEventListener("pointercancel", P); + const w = L(); + It(m, "panel-drag-end", { panel: e, position: w }); }, v = (w) => { - const N = w.target; - if (N.closest(Rp) || N.closest(".flow-panel-resize-handle")) + const I = w.target; + if (I.closest(Vp) || I.closest(".flow-panel-resize-handle")) return; - b = !0, C = w.clientX, E = w.clientY; - const k = e.getBoundingClientRect(), R = m.getBoundingClientRect(); - T = k.left - R.left, I = k.top - R.top, e.style.bottom = "auto", e.style.right = "auto", e.style.transform = "none", e.style.left = `${T}px`, e.style.top = `${I}px`, document.addEventListener("pointermove", A), document.addEventListener("pointerup", P), document.addEventListener("pointercancel", P), Nt(m, "panel-drag-start", { + C = !0, E = w.clientX, b = w.clientY; + const M = e.getBoundingClientRect(), R = m.getBoundingClientRect(); + T = M.left - R.left, D = M.top - R.top, e.style.bottom = "auto", e.style.right = "auto", e.style.transform = "none", e.style.left = `${T}px`, e.style.top = `${D}px`, document.addEventListener("pointermove", A), document.addEventListener("pointerup", P), document.addEventListener("pointercancel", P), It(m, "panel-drag-start", { panel: e, - position: { x: T, y: I } + position: { x: T, y: D } }); }; if (e.addEventListener("pointerdown", v), p) { x = document.createElement("div"), x.classList.add("flow-panel-resize-handle"), e.appendChild(x); - let w = !1, N = 0, k = 0, R = 0, O = 0; - const q = ($) => { + let w = !1, I = 0, M = 0, R = 0, z = 0; + const V = (N) => { if (!w) return; - const F = Math.max(Fp, R + ($.clientX - N)), K = Math.max(Op, O + ($.clientY - k)); - e.style.width = `${F}px`, e.style.height = `${K}px`, Nt(m, "panel-resize", { + const F = Math.max(Bp, R + (N.clientX - I)), W = Math.max(qp, z + (N.clientY - M)); + e.style.width = `${F}px`, e.style.height = `${W}px`, It(m, "panel-resize", { panel: e, - dimensions: { width: F, height: K } + dimensions: { width: F, height: W } }); - }, D = () => { - w && (w = !1, document.removeEventListener("pointermove", q), document.removeEventListener("pointerup", D), document.removeEventListener("pointercancel", D), Nt(m, "panel-resize-end", { + }, $ = () => { + w && (w = !1, document.removeEventListener("pointermove", V), document.removeEventListener("pointerup", $), document.removeEventListener("pointercancel", $), It(m, "panel-resize-end", { panel: e, dimensions: { width: e.offsetWidth, height: e.offsetHeight } })); - }, L = ($) => { - $.stopPropagation(), w = !0, N = $.clientX, k = $.clientY, R = e.offsetWidth, O = e.offsetHeight, document.addEventListener("pointermove", q), document.addEventListener("pointerup", D), document.addEventListener("pointercancel", D), Nt(m, "panel-resize-start", { + }, S = (N) => { + N.stopPropagation(), w = !0, I = N.clientX, M = N.clientY, R = e.offsetWidth, z = e.offsetHeight, document.addEventListener("pointermove", V), document.addEventListener("pointerup", $), document.addEventListener("pointercancel", $), It(m, "panel-resize-start", { panel: e, - dimensions: { width: R, height: O } + dimensions: { width: R, height: z } }); }; - x.addEventListener("pointerdown", L), i(() => { - e.removeEventListener("pointerdown", v), x?.removeEventListener("pointerdown", L), document.removeEventListener("pointermove", A), document.removeEventListener("pointerup", P), document.removeEventListener("pointercancel", P), document.removeEventListener("pointermove", q), document.removeEventListener("pointerup", D), document.removeEventListener("pointercancel", D), x?.remove(), e.removeEventListener("mousedown", g), e.removeEventListener("pointerdown", g), e.removeEventListener("wheel", g), m.removeEventListener("flow-panel-reset", S), m.__flowPanels?.delete(e); + x.addEventListener("pointerdown", S), i(() => { + e.removeEventListener("pointerdown", v), x?.removeEventListener("pointerdown", S), document.removeEventListener("pointermove", A), document.removeEventListener("pointerup", P), document.removeEventListener("pointercancel", P), document.removeEventListener("pointermove", V), document.removeEventListener("pointerup", $), document.removeEventListener("pointercancel", $), x?.remove(), e.removeEventListener("mousedown", g), e.removeEventListener("pointerdown", g), e.removeEventListener("wheel", g), m.removeEventListener("flow-panel-reset", k), m.__flowPanels?.delete(e); }); } else i(() => { - e.removeEventListener("pointerdown", v), document.removeEventListener("pointermove", A), document.removeEventListener("pointerup", P), document.removeEventListener("pointercancel", P), e.removeEventListener("mousedown", g), e.removeEventListener("pointerdown", g), e.removeEventListener("wheel", g), m.removeEventListener("flow-panel-reset", S), m.__flowPanels?.delete(e); + e.removeEventListener("pointerdown", v), document.removeEventListener("pointermove", A), document.removeEventListener("pointerup", P), document.removeEventListener("pointercancel", P), e.removeEventListener("mousedown", g), e.removeEventListener("pointerdown", g), e.removeEventListener("wheel", g), m.removeEventListener("flow-panel-reset", k), m.__flowPanels?.delete(e); }); } else i(() => { - e.removeEventListener("mousedown", g), e.removeEventListener("pointerdown", g), e.removeEventListener("wheel", g), m.removeEventListener("flow-panel-reset", S), m.__flowPanels?.delete(e); + e.removeEventListener("mousedown", g), e.removeEventListener("pointerdown", g), e.removeEventListener("wheel", g), m.removeEventListener("flow-panel-reset", k), m.__flowPanels?.delete(e); }); } ); } -function Up(t) { +function Jp(t) { t.directive( "flow-node-toolbar", (e, { value: n, modifiers: o }, { effect: i, cleanup: r }) => { - const s = Gp(n), a = Zp(o); + const s = Qp(n), a = em(o); e.classList.add("flow-node-toolbar"), e.style.position = "absolute"; const l = (d) => { d.stopPropagation(); @@ -14910,22 +14923,22 @@ function Up(t) { if (!u) return; const f = t.$data(u); if (!f?.viewport) return; - const h = f.viewport.zoom || 1, p = parseInt(e.getAttribute("data-flow-offset") ?? "10", 10), g = d.dataset.flowNodeId, m = g ? f.getNode(g) : null, y = m?.dimensions?.width ?? d.offsetWidth, _ = m?.dimensions?.height ?? d.offsetHeight, S = p / h; - let x, b, C, E; - s === "top" || s === "bottom" ? (b = s === "top" ? -S : _ + S, E = s === "top" ? "-100%" : "0%", a === "start" ? (x = 0, C = "0%") : a === "end" ? (x = y, C = "-100%") : (x = y / 2, C = "-50%")) : (x = s === "left" ? -S : y + S, C = s === "left" ? "-100%" : "0%", a === "start" ? (b = 0, E = "0%") : a === "end" ? (b = _, E = "-100%") : (b = _ / 2, E = "-50%")), e.style.left = `${x}px`, e.style.top = `${b}px`, e.style.transformOrigin = "0 0", e.style.transform = `scale(${1 / h}) translate(${C}, ${E})`; + const h = f.viewport.zoom || 1, p = parseInt(e.getAttribute("data-flow-offset") ?? "10", 10), g = d.dataset.flowNodeId, m = g ? f.getNode(g) : null, y = m?.dimensions?.width ?? d.offsetWidth, _ = m?.dimensions?.height ?? d.offsetHeight, k = p / h; + let x, C, E, b; + s === "top" || s === "bottom" ? (C = s === "top" ? -k : _ + k, b = s === "top" ? "-100%" : "0%", a === "start" ? (x = 0, E = "0%") : a === "end" ? (x = y, E = "-100%") : (x = y / 2, E = "-50%")) : (x = s === "left" ? -k : y + k, E = s === "left" ? "-100%" : "0%", a === "start" ? (C = 0, b = "0%") : a === "end" ? (C = _, b = "-100%") : (C = _ / 2, b = "-50%")), e.style.left = `${x}px`, e.style.top = `${C}px`, e.style.transformOrigin = "0 0", e.style.transform = `scale(${1 / h}) translate(${E}, ${b})`; }), r(() => { e.removeEventListener("pointerdown", l), e.removeEventListener("click", c), e.classList.remove("flow-node-toolbar"); }); } ); } -function Gp(t) { +function Qp(t) { return t === "bottom" ? "bottom" : t === "left" ? "left" : t === "right" ? "right" : "top"; } -function Zp(t) { +function em(t) { return t.includes("start") ? "start" : t.includes("end") ? "end" : "center"; } -function Kp(t) { +function tm(t) { t.directive( "flow-context-menu", (e, { modifiers: n, expression: o }, { effect: i, evaluate: r, cleanup: s }) => { @@ -14939,8 +14952,8 @@ function Kp(t) { const d = t.$data(c); let u = 0, f = 0; if (o) { - const C = r(o); - u = C?.offsetX ?? 0, f = C?.offsetY ?? 0; + const E = r(o); + u = E?.offsetX ?? 0, f = E?.offsetY ?? 0; } l.setAttribute("role", "menu"), l.setAttribute("tabindex", "-1"), l.style.display = "none"; const h = document.createElement("div"); @@ -14948,88 +14961,88 @@ function Kp(t) { let p = null; const g = 4, m = () => { p = document.activeElement; - const C = d.contextMenu.x + u, E = d.contextMenu.y + f; - l.style.display = "", l.style.position = "fixed", l.style.left = C + "px", l.style.top = E + "px", l.style.zIndex = "5000", l.querySelectorAll(':scope > button, :scope > [role="menuitem"]').forEach((v) => { + const E = d.contextMenu.x + u, b = d.contextMenu.y + f; + l.style.display = "", l.style.position = "fixed", l.style.left = E + "px", l.style.top = b + "px", l.style.zIndex = "5000", l.querySelectorAll(':scope > button, :scope > [role="menuitem"]').forEach((v) => { v.setAttribute("role", "menuitem"), v.hasAttribute("tabindex") || v.setAttribute("tabindex", "-1"); }); - const T = l.getBoundingClientRect(), I = window.innerWidth, M = window.innerHeight; - let A = C, P = E; - T.right > I - g && (A = I - T.width - g), T.bottom > M - g && (P = M - T.height - g), A < g && (A = g), P < g && (P = g), l.style.left = A + "px", l.style.top = P + "px", h.style.display = "", l.focus({ preventScroll: !0 }); + const T = l.getBoundingClientRect(), D = window.innerWidth, L = window.innerHeight; + let A = E, P = b; + T.right > D - g && (A = D - T.width - g), T.bottom > L - g && (P = L - T.height - g), A < g && (A = g), P < g && (P = g), l.style.left = A + "px", l.style.top = P + "px", h.style.display = "", l.focus({ preventScroll: !0 }); }, y = () => { l.style.display = "none", h.style.display = "none", p && document.contains(p) && (p.focus({ preventScroll: !0 }), p = null); }; i(() => { - const C = d.contextMenu; - C.show && C.type === a ? m() : y(); - }), h.addEventListener("click", () => d.closeContextMenu()), h.addEventListener("contextmenu", (C) => { - C.preventDefault(), d.closeContextMenu(); + const E = d.contextMenu; + E.show && E.type === a ? m() : y(); + }), h.addEventListener("click", () => d.closeContextMenu()), h.addEventListener("contextmenu", (E) => { + E.preventDefault(), d.closeContextMenu(); }); const _ = () => { d.contextMenu.show && d.contextMenu.type === a && d.closeContextMenu(); }; window.addEventListener("scroll", _, !0); - const S = () => Array.from(l.querySelectorAll( + const k = () => Array.from(l.querySelectorAll( ':scope > button:not([disabled]), :scope > [role="menuitem"]:not([disabled])' - )), x = (C) => Array.from(C.querySelectorAll( + )), x = (E) => Array.from(E.querySelectorAll( "button:not([disabled])" - )), b = (C) => { + )), C = (E) => { if (!d.contextMenu.show || d.contextMenu.type !== a || l.style.display === "none") return; - const E = document.activeElement, T = E?.closest(".flow-context-submenu"), I = T ? x(T) : S(); - if (I.length === 0) return; - const M = I.indexOf(E); - switch (C.key) { + const b = document.activeElement, T = b?.closest(".flow-context-submenu"), D = T ? x(T) : k(); + if (D.length === 0) return; + const L = D.indexOf(b); + switch (E.key) { case "ArrowDown": { - C.preventDefault(); - const A = M < I.length - 1 ? M + 1 : 0; - I[A].focus({ preventScroll: !0 }); + E.preventDefault(); + const A = L < D.length - 1 ? L + 1 : 0; + D[A].focus({ preventScroll: !0 }); break; } case "ArrowUp": { - C.preventDefault(); - const A = M > 0 ? M - 1 : I.length - 1; - I[A].focus({ preventScroll: !0 }); + E.preventDefault(); + const A = L > 0 ? L - 1 : D.length - 1; + D[A].focus({ preventScroll: !0 }); break; } case "Tab": { - if (C.preventDefault(), C.shiftKey) { - const A = M > 0 ? M - 1 : I.length - 1; - I[A].focus({ preventScroll: !0 }); + if (E.preventDefault(), E.shiftKey) { + const A = L > 0 ? L - 1 : D.length - 1; + D[A].focus({ preventScroll: !0 }); } else { - const A = M < I.length - 1 ? M + 1 : 0; - I[A].focus({ preventScroll: !0 }); + const A = L < D.length - 1 ? L + 1 : 0; + D[A].focus({ preventScroll: !0 }); } break; } case "Enter": case " ": { - C.preventDefault(), E?.click(); + E.preventDefault(), b?.click(); break; } case "ArrowRight": { if (!T) { - const A = E?.querySelector(".flow-context-submenu"); - A && (C.preventDefault(), A.querySelector("button:not([disabled])")?.focus({ preventScroll: !0 })); + const A = b?.querySelector(".flow-context-submenu"); + A && (E.preventDefault(), A.querySelector("button:not([disabled])")?.focus({ preventScroll: !0 })); } break; } case "ArrowLeft": { - T && (C.preventDefault(), T.closest(".flow-context-submenu-trigger")?.focus({ preventScroll: !0 })); + T && (E.preventDefault(), T.closest(".flow-context-submenu-trigger")?.focus({ preventScroll: !0 })); break; } } }; - l.addEventListener("keydown", b), s(() => { - h.remove(), window.removeEventListener("scroll", _, !0), l.removeEventListener("keydown", b); + l.addEventListener("keydown", C), s(() => { + h.remove(), window.removeEventListener("scroll", _, !0), l.removeEventListener("keydown", C); }); } ); } -const Jp = { +const nm = { mouseenter: "mouseleave", click: "click" // toggle behavior }; -function Qp(t) { +function om(t) { t.directive( "flow-animate", (e, { value: n, modifiers: o, expression: i }, { evaluate: r, effect: s, cleanup: a }) => { @@ -15037,7 +15050,7 @@ function Qp(t) { let h = "click"; l.has("mouseenter") ? h = "mouseenter" : l.has("click") && (h = "click"); let p = null, g = [], m = !1, y = !1, _ = !1; - function S() { + function k() { const A = r(i); return Array.isArray(A) ? A : A && typeof A == "object" ? [A] : []; } @@ -15045,30 +15058,30 @@ function Qp(t) { const A = e.closest("[x-data]"); return A ? t.$data(A) : null; } - function b(A, P = !1) { + function C(A, P = !1) { const v = x(); if (!v?.timeline) return Promise.resolve(); const w = v.timeline(); if (P) { - for (let N = A.length - 1; N >= 0; N--) - w.step(A[N]); + for (let I = A.length - 1; I >= 0; I--) + w.step(A[I]); w.reverse(); } else - for (const N of A) - N.parallel ? w.parallel(N.parallel) : w.step(N); + for (const I of A) + I.parallel ? w.parallel(I.parallel) : w.step(I); return p = w, w.play().then(() => { p === w && (p = null); }); } - function C(A = !1) { + function E(A = !1) { if (c && y) return; y = !0; - const P = S(); + const P = k(); if (P.length === 0) return; - const v = () => b(P, A); - u ? (g.push(v), E()) : (p?.stop(), p = null, g = [], m = !1, v()); + const v = () => C(P, A); + u ? (g.push(v), b()) : (p?.stop(), p = null, g = [], m = !1, v()); } - async function E() { + async function b() { if (!m) { for (m = !0; g.length > 0; ) await g.shift()(); @@ -15077,7 +15090,7 @@ function Qp(t) { } if (f) { s(() => { - const A = S(), P = x(); + const A = k(), P = x(); P?.registerAnimation && P.registerAnimation(f, A); }), a(() => { const A = x(); @@ -15086,31 +15099,31 @@ function Qp(t) { return; } const T = () => { - d && h === "click" ? (C(_), _ = !_) : C(!1); + d && h === "click" ? (E(_), _ = !_) : E(!1); }; e.addEventListener(h, T); - let I = null, M = null; - d && h !== "click" && (M = Jp[h] ?? null, M && (I = () => C(!0), e.addEventListener(M, I))), a(() => { - p?.stop(), e.removeEventListener(h, T), M && I && e.removeEventListener(M, I); + let D = null, L = null; + d && h !== "click" && (L = nm[h] ?? null, L && (D = () => E(!0), e.addEventListener(L, D))), a(() => { + p?.stop(), e.removeEventListener(h, T), L && D && e.removeEventListener(L, D); }); } ); } -function em(t, e, n, o, i) { - const r = e.position?.x ?? t.position.x, s = e.position?.y ?? t.position.y, a = t.dimensions?.width ?? we, l = t.dimensions?.height ?? _e, c = r * n.zoom + n.x, d = s * n.zoom + n.y, u = (r + a) * n.zoom + n.x, f = (s + l) * n.zoom + n.y; +function im(t, e, n, o, i) { + const r = e.position?.x ?? t.position.x, s = e.position?.y ?? t.position.y, a = t.dimensions?.width ?? ve, l = t.dimensions?.height ?? be, c = r * n.zoom + n.x, d = s * n.zoom + n.y, u = (r + a) * n.zoom + n.x, f = (s + l) * n.zoom + n.y; return u > 0 && c < o && f > 0 && d < i; } -function tm(t, e, n, o, i) { +function sm(t, e, n, o, i) { const r = t.nodes; if (!r || r.length === 0) return !1; for (const s of r) { const a = e.getNode?.(s) ?? e.nodes?.find((l) => l.id === s); - if (a && !em(a, t, n, o, i)) + if (a && !im(a, t, n, o, i)) return !0; } return !1; } -function nm(t) { +function rm(t) { t.directive( "flow-timeline", (e, { expression: n }, { evaluate: o, effect: i, cleanup: r }) => { @@ -15122,39 +15135,39 @@ function nm(t) { function h(m, y) { const _ = f(); if (!_?.timeline) return Promise.resolve(); - const S = _.timeline(), x = y.speed ?? 1, b = y.autoFitView === !0, C = y.fitViewPadding ?? 0.1, E = _.viewport, T = _.getContainerDimensions?.(); - for (const I of m) { - const M = x !== 1 ? { - ...I, - duration: I.duration !== void 0 ? I.duration / x : void 0, - delay: I.delay !== void 0 ? I.delay / x : void 0 - } : I; - if (M.parallel) { - const A = M.parallel.map( + const k = _.timeline(), x = y.speed ?? 1, C = y.autoFitView === !0, E = y.fitViewPadding ?? 0.1, b = _.viewport, T = _.getContainerDimensions?.(); + for (const D of m) { + const L = x !== 1 ? { + ...D, + duration: D.duration !== void 0 ? D.duration / x : void 0, + delay: D.delay !== void 0 ? D.delay / x : void 0 + } : D; + if (L.parallel) { + const A = L.parallel.map( (P) => x !== 1 ? { ...P, duration: P.duration !== void 0 ? P.duration / x : void 0, delay: P.delay !== void 0 ? P.delay / x : void 0 } : P ); - S.parallel(A); - } else if (b && E && T && tm(M, _, E, T.width, T.height)) { + k.parallel(A); + } else if (C && b && T && sm(L, _, b, T.width, T.height)) { const A = { fitView: !0, - fitViewPadding: C, - duration: M.duration, - easing: M.easing + fitViewPadding: E, + duration: L.duration, + easing: L.easing }; - S.parallel([M, A]); + k.parallel([L, A]); } else - S.step(M); + k.step(L); } - if (y.lock && S.lock(!0), y.loop !== void 0 && y.loop !== !1) { - const I = y.loop === !0 ? 0 : y.loop; - S.loop(I); + if (y.lock && k.lock(!0), y.loop !== void 0 && y.loop !== !1) { + const D = y.loop === !0 ? 0 : y.loop; + k.loop(D); } - return y.respectReducedMotion !== void 0 && S.respectReducedMotion(y.respectReducedMotion), a = S, d = "playing", c = !0, S.play().then(() => { - a === S && (a = null, d = "idle", c = !1); + return y.respectReducedMotion !== void 0 && k.respectReducedMotion(y.respectReducedMotion), a = k, d = "playing", c = !0, k.play().then(() => { + a === k && (a = null, d = "idle", c = !1); }); } async function p(m) { @@ -15194,8 +15207,8 @@ function nm(t) { if (!m || !m.steps) return; const y = m.steps, _ = m.autoplay !== !1; if (y.length > u) { - const S = y.slice(Math.max(s, u)); - u = y.length, S.length > 0 && _ && (l.push(...S), p(m)); + const k = y.slice(Math.max(s, u)); + u = y.length, k.length > 0 && _ && (l.push(...k), p(m)); } else u = y.length; }), r(() => { @@ -15204,7 +15217,7 @@ function nm(t) { } ); } -function om(t) { +function am(t) { t.directive( "flow-collapse", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -15247,7 +15260,7 @@ function om(t) { } ); } -function im(t) { +function lm(t) { t.directive( "flow-condense", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -15274,10 +15287,28 @@ function im(t) { } ); } -function Ho(t) { +function Fo(t) { for (; t.firstChild; ) t.removeChild(t.firstChild); } -function sm(t) { +function Ve(t, e, n) { + const o = (Array.isArray(n) ? n : n ? [n] : []).flatMap((s) => s.split(/\s+/)).filter(Boolean), i = new Set(o), r = t.dataset[e] ? t.dataset[e].split(" ") : []; + for (const s of r) i.has(s) || t.classList.remove(s); + for (const s of i) t.classList.add(s); + i.size ? t.dataset[e] = [...i].join(" ") : delete t.dataset[e]; +} +function Zs(t) { + return typeof t == "object" && t !== null && !Array.isArray(t); +} +const Ks = [ + ["icon", ".flow-schema-row-icon"], + ["name", ".flow-schema-row-name"], + ["type", ".flow-schema-row-type"], + ["target", ".flow-schema-handle--target:not(.flow-schema-handle--mirror)"], + ["source", ".flow-schema-handle--source:not(.flow-schema-handle--mirror)"], + ["mirrorTarget", ".flow-schema-handle--target.flow-schema-handle--mirror"], + ["mirrorSource", ".flow-schema-handle--source.flow-schema-handle--mirror"] +]; +function cm(t) { t.directive("flow-schema", (e, n, { evaluate: o, effect: i, cleanup: r }) => { const s = e, a = () => { try { @@ -15287,150 +15318,241 @@ function sm(t) { } }, l = () => { try { - const b = s.closest(".flow-container"); - return b ? !!t.$data?.(b)?._config?.rowsReorderable : !1; + const E = s.closest(".flow-container"); + return E ? !!t.$data?.(E)?._config?.rowsReorderable : !1; } catch { return !1; } }, c = () => { try { - const b = s.closest(".flow-container"); - return b ? !!t.$data?.(b)?._config?.keyboardConnect : !1; + const E = s.closest(".flow-container"); + return E ? !!t.$data?.(E)?._config?.keyboardConnect : !1; } catch { return !1; } }, d = () => { try { - const b = s.closest(".flow-container"); - return b ? t.$data?.(b) ?? null : null; + const E = s.closest(".flow-container"); + return E ? t.$data?.(E) ?? null : null; } catch { return null; } }, u = () => { + const E = d()?._config; + return { + nodeDecorator: typeof E?.schemaNodeDecorator == "function" ? E.schemaNodeDecorator : null, + rowDecorator: typeof E?.schemaRowDecorator == "function" ? E.schemaRowDecorator : null, + nodeClass: typeof E?.schemaNodeClass == "function" ? E.schemaNodeClass : null, + rowClass: typeof E?.schemaRowClass == "function" ? E.schemaRowClass : null + }; + }, f = () => { t.nextTick(() => { - const b = d(); - if (!b) return; - const C = t.raw(b); - if (C._schemaMetrics != null) return; - const E = s.querySelector(":scope > .flow-schema-header"), T = s.querySelector(":scope > .flow-schema-body"), I = s.querySelectorAll(".flow-schema-row"); - if (I.length < 2) return; - const M = I[0], A = I[1], P = I[I.length - 1], v = M.querySelector(".flow-schema-handle"), w = P.querySelector(".flow-schema-handle"); - if (!E || !T || !v || !w) return; - const N = s.closest("[data-flow-node-id]") ?? s, k = C.viewport?.zoom || 1, R = N.getBoundingClientRect(), O = E.getBoundingClientRect(), q = T.getBoundingClientRect(), D = M.getBoundingClientRect(), L = A.getBoundingClientRect(), $ = P.getBoundingClientRect(), F = v.getBoundingClientRect(), K = w.getBoundingClientRect(), ne = (L.top - D.top) / k, G = $.height / k; - if (ne <= 0 || G <= 0) return; - const B = { - headerHeight: O.height / k, - rowHeight: ne, + const E = d(); + if (!E) return; + const b = t.raw(E); + if (b._schemaMetrics != null) return; + const T = s.querySelector(":scope > .flow-schema-header"), D = s.querySelector(":scope > .flow-schema-body"), L = s.querySelectorAll(".flow-schema-row"); + if (L.length < 2) return; + const A = L[0], P = L[1], v = L[L.length - 1], w = A.querySelector(".flow-schema-handle"), I = v.querySelector(".flow-schema-handle"); + if (!T || !D || !w || !I) return; + const M = s.closest("[data-flow-node-id]") ?? s, R = b.viewport?.zoom || 1, z = M.getBoundingClientRect(), V = T.getBoundingClientRect(), $ = D.getBoundingClientRect(), S = A.getBoundingClientRect(), N = P.getBoundingClientRect(), F = v.getBoundingClientRect(), W = w.getBoundingClientRect(), Q = I.getBoundingClientRect(), U = (N.top - S.top) / R, G = F.height / R; + if (U <= 0 || G <= 0) return; + const Z = { + headerHeight: V.height / R, + rowHeight: U, // NOT the same as `rowHeight` under the shipped theme — the last row loses // its border-bottom. See SchemaMetrics.rowHeightLast. rowHeightLast: G, // Where the handle actually sits inside its row. MEASURED, not `rowHeight / 2`: // `top: 50%` resolves against the row's PADDING box, which the theme's // border-bottom shrinks. See SchemaMetrics.handleOffsetY. - handleOffsetY: (F.top + F.height / 2 - D.top) / k, - handleOffsetYLast: (K.top + K.height / 2 - $.top) / k, - insetLeft: (D.left - R.left) / k, - insetRight: (R.right - D.right) / k, - insetTop: (O.top - R.top) / k, + handleOffsetY: (W.top + W.height / 2 - S.top) / R, + handleOffsetYLast: (Q.top + Q.height / 2 - F.top) / R, + insetLeft: (S.left - z.left) / R, + insetRight: (z.right - S.right) / R, + insetTop: (V.top - z.top) / R, // Closes the row model: with insetBottom, a consumer can reconstruct the // node's expected border-box height and so DETECT non-uniform rows (a // wrapped field name — nothing in the CSS forces `white-space: nowrap`) // instead of assuming uniformity. See `flow-edge.ts`'s eligibility check. - insetBottom: (R.bottom - q.bottom) / k, - handleWidth: F.width / k, - handleHeight: F.height / k + insetBottom: (z.bottom - $.bottom) / R, + handleWidth: W.width / R, + handleHeight: W.height / R }; - C._schemaMetrics = B; + b._schemaMetrics = Z; }); }; s.classList.add("flow-schema-node"); - let f = s.closest("[data-flow-node-id]"), h = !1; - f ? f.setAttribute("data-flow-schema-node", "") : t.nextTick(() => { - h || !s.isConnected || (f = s.closest("[data-flow-node-id]"), f?.setAttribute("data-flow-schema-node", "")); + let h = s.closest("[data-flow-node-id]"), p = !1; + h ? h.setAttribute("data-flow-schema-node", "") : t.nextTick(() => { + p || !s.isConnected || (h = s.closest("[data-flow-node-id]"), h?.setAttribute("data-flow-schema-node", "")); }); - let p = null, g = null; - const m = /* @__PURE__ */ new Map(), y = () => { - p && g || (Ho(s), m.clear(), p = document.createElement("div"), p.className = "flow-schema-header", s.appendChild(p), g = document.createElement("div"), g.className = "flow-schema-body", s.appendChild(g)); - }, _ = () => { - const b = a(), C = b?.data; - if (!C) { - for (const w of m.values()) - t.destroyTree(w); - m.clear(), Ho(s), p = null, g = null; + let g = null, m = null; + const y = /* @__PURE__ */ new Map(), _ = () => g && m ? !1 : (Fo(s), y.clear(), g = document.createElement("div"), g.className = "flow-schema-header", s.appendChild(g), m = document.createElement("div"), m.className = "flow-schema-body", s.appendChild(m), !0), k = () => { + const E = a(), b = E?.data; + if (!b) { + for (const $ of y.values()) + t.destroyTree($); + y.clear(), Fo(s), Ve(s, "flowSchemaNodeClass", null), delete s.dataset.flowSchemaNodeSub, g = null, m = null; return; } - y(); - const E = typeof C.label == "string" ? C.label : "", T = Array.isArray(C.fields) ? C.fields : [], I = typeof b?.id == "string" ? b.id : ""; - typeof C.kind == "string" && C.kind ? s.setAttribute("data-flow-schema-kind", C.kind) : s.removeAttribute("data-flow-schema-kind"), p.textContent !== E && (p.textContent = E); - const M = l(), A = c(), P = /* @__PURE__ */ new Set(); - for (const w of T) { - P.add(w.name); - const N = m.get(w.name); - if (N) - S(N, w); + const T = _(), D = u(), L = typeof b.label == "string" ? b.label : "", A = Array.isArray(b.fields) ? b.fields : [], P = typeof E?.id == "string" ? E.id : "", v = ($, S, N) => { + const F = $.dataset.flowSchemaRowSub === "1"; + if (!D.rowClass && !$.dataset.flowSchemaRowClass && !F) return; + let W = null; + if (D.rowClass) + try { + W = D.rowClass({ + field: S, + node: E, + nodeId: P, + isNew: N + }); + } catch (Q) { + console.error("[alpineflow] schemaRowClass threw:", Q); + return; + } + if (Zs(W)) { + const Q = W; + Ve($, "flowSchemaRowClass", Q.row); + for (const [U, G] of Ks) { + const Z = $.querySelector(G); + Z && Ve(Z, "flowSchemaRowClass", Q[U]); + } + $.dataset.flowSchemaRowSub = "1"; + } else if (Ve($, "flowSchemaRowClass", W), F) { + for (const [, Q] of Ks) { + const U = $.querySelector(Q); + U && Ve(U, "flowSchemaRowClass", null); + } + delete $.dataset.flowSchemaRowSub; + } + }, w = ($, S, N) => { + if (!D.rowDecorator) return; + const F = { + icon: $.querySelector(".flow-schema-row-icon"), + name: $.querySelector(".flow-schema-row-name"), + type: $.querySelector(".flow-schema-row-type"), + target: $.querySelector( + ".flow-schema-handle--target:not(.flow-schema-handle--mirror)" + ), + source: $.querySelector( + ".flow-schema-handle--source:not(.flow-schema-handle--mirror)" + ), + mirrorTarget: $.querySelector( + ".flow-schema-handle--target.flow-schema-handle--mirror" + ), + mirrorSource: $.querySelector( + ".flow-schema-handle--source.flow-schema-handle--mirror" + ) + }; + try { + D.rowDecorator({ row: $, field: S, nodeId: P, slots: F, isNew: N }); + } catch (W) { + console.error("[alpineflow] schemaRowDecorator threw:", W); + } + }; + typeof b.kind == "string" && b.kind ? s.setAttribute("data-flow-schema-kind", b.kind) : s.removeAttribute("data-flow-schema-kind"), g.textContent !== L && (g.textContent = L); + const I = l(), M = c(), R = /* @__PURE__ */ new Set(); + for (const $ of A) { + R.add($.name); + const S = y.get($.name); + if (S) + x(S, $), v(S, $, !1), w(S, $, !1); else { - const k = x(w, I, M, A); - m.set(w.name, k), g.appendChild(k), t.initTree(k); + const N = C($, P, I, M); + y.set($.name, N), m.appendChild(N), t.initTree(N), v(N, $, !0), w(N, $, !0); } } - for (const [w, N] of m) - P.has(w) || (t.destroyTree(N), N.remove(), m.delete(w)); - let v = g.firstChild; - for (const w of T) { - const N = m.get(w.name); - N && (v === N ? v = v.nextSibling : g.insertBefore(N, v)); + for (const [$, S] of y) + R.has($) || (t.destroyTree(S), S.remove(), y.delete($)); + let z = m.firstChild; + for (const $ of A) { + const S = y.get($.name); + S && (z === S ? z = z.nextSibling : m.insertBefore(S, z)); } - u(); - }, S = (b, C) => { - b.dataset.flowSchemaField !== C.name && (b.dataset.flowSchemaField = C.name), b.classList.toggle("flow-schema-row--pk", C.key === "primary"), b.classList.toggle("flow-schema-row--fk", C.key === "foreign"), b.classList.toggle("flow-schema-row--required", !!C.required); - let E = b.querySelector(".flow-schema-row-icon"); - const T = b.querySelector(".flow-schema-row-name"); - C.icon ? (E || (E = document.createElement("span"), E.className = "flow-schema-row-icon", b.insertBefore(E, T)), E.textContent !== C.icon && (E.textContent = C.icon)) : E && E.remove(), T && T.textContent !== C.name && (T.textContent = C.name); - const I = b.querySelector(".flow-schema-row-type"); - I && I.textContent !== C.type && (I.textContent = C.type); - }, x = (b, C, E, T) => { - const I = document.createElement("div"); - I.className = "flow-schema-row", I.dataset.flowSchemaField = b.name, b.key === "primary" && I.classList.add("flow-schema-row--pk"), b.key === "foreign" && I.classList.add("flow-schema-row--fk"), b.required && I.classList.add("flow-schema-row--required"), C && I.setAttribute( + const V = s.dataset.flowSchemaNodeSub === "1"; + if (D.nodeClass || s.dataset.flowSchemaNodeClass || V) { + let $ = null, S = !1; + if (D.nodeClass) + try { + $ = D.nodeClass({ + node: E, + isNew: T + }); + } catch (N) { + console.error("[alpineflow] schemaNodeClass threw:", N), S = !0; + } + if (!S) + if (Zs($)) { + const N = $; + Ve(s, "flowSchemaNodeClass", N.node), Ve(g, "flowSchemaNodeClass", N.header), Ve(m, "flowSchemaNodeClass", N.body), s.dataset.flowSchemaNodeSub = "1"; + } else + Ve(s, "flowSchemaNodeClass", $), V && (Ve(g, "flowSchemaNodeClass", null), Ve(m, "flowSchemaNodeClass", null), delete s.dataset.flowSchemaNodeSub); + } + if (D.nodeDecorator) + try { + D.nodeDecorator({ + host: s, + header: g, + body: m, + node: E, + isNew: T + }); + } catch ($) { + console.error("[alpineflow] schemaNodeDecorator threw:", $); + } + f(); + }, x = (E, b) => { + E.dataset.flowSchemaField !== b.name && (E.dataset.flowSchemaField = b.name), E.classList.toggle("flow-schema-row--pk", b.key === "primary"), E.classList.toggle("flow-schema-row--fk", b.key === "foreign"), E.classList.toggle("flow-schema-row--required", !!b.required); + let T = E.querySelector(".flow-schema-row-icon"); + const D = E.querySelector(".flow-schema-row-name"); + b.icon ? (T || (T = document.createElement("span"), T.className = "flow-schema-row-icon", E.insertBefore(T, D)), T.textContent !== b.icon && (T.textContent = b.icon)) : T && T.remove(), D && D.textContent !== b.name && (D.textContent = b.name); + const L = E.querySelector(".flow-schema-row-type"); + L && L.textContent !== b.type && (L.textContent = b.type); + }, C = (E, b, T, D) => { + const L = document.createElement("div"); + L.className = "flow-schema-row", L.dataset.flowSchemaField = E.name, E.key === "primary" && L.classList.add("flow-schema-row--pk"), E.key === "foreign" && L.classList.add("flow-schema-row--fk"), E.required && L.classList.add("flow-schema-row--required"), b && L.setAttribute( "x-flow-row-select", - JSON.stringify(`${C}.${b.name}`) - ), E && I.setAttribute("x-schema-reorderable", ""), T && C && I.setAttribute( + JSON.stringify(`${b}.${E.name}`) + ), T && L.setAttribute("x-schema-reorderable", ""), D && b && L.setAttribute( "x-schema-keyboard-nav", - JSON.stringify(`${C}.${b.name}`) + JSON.stringify(`${b}.${E.name}`) ); - const M = document.createElement("div"); - if (M.className = "flow-schema-handle flow-schema-handle--target", M.setAttribute("x-flow-handle:target.left", JSON.stringify(b.name)), I.appendChild(M), b.icon) { - const k = document.createElement("span"); - k.className = "flow-schema-row-icon", k.textContent = b.icon, I.appendChild(k); + const A = document.createElement("div"); + if (A.className = "flow-schema-handle flow-schema-handle--target", A.setAttribute("x-flow-handle:target.left", JSON.stringify(E.name)), L.appendChild(A), E.icon) { + const R = document.createElement("span"); + R.className = "flow-schema-row-icon", R.textContent = E.icon, L.appendChild(R); } - const A = document.createElement("span"); - A.className = "flow-schema-row-name", A.textContent = b.name, I.appendChild(A); const P = document.createElement("span"); - P.className = "flow-schema-row-type", P.textContent = b.type, I.appendChild(P); - const v = document.createElement("div"); - v.className = "flow-schema-handle flow-schema-handle--source", v.setAttribute("x-flow-handle:source.right", JSON.stringify(b.name)), I.appendChild(v); + P.className = "flow-schema-row-name", P.textContent = E.name, L.appendChild(P); + const v = document.createElement("span"); + v.className = "flow-schema-row-type", v.textContent = E.type, L.appendChild(v); const w = document.createElement("div"); - w.className = "flow-schema-handle flow-schema-handle--target flow-schema-handle--mirror", w.setAttribute("x-flow-handle:target.right", JSON.stringify(b.name)), I.appendChild(w); - const N = document.createElement("div"); - return N.className = "flow-schema-handle flow-schema-handle--source flow-schema-handle--mirror", N.setAttribute("x-flow-handle:source.left", JSON.stringify(b.name)), I.appendChild(N), I; + w.className = "flow-schema-handle flow-schema-handle--source", w.setAttribute("x-flow-handle:source.right", JSON.stringify(E.name)), L.appendChild(w); + const I = document.createElement("div"); + I.className = "flow-schema-handle flow-schema-handle--target flow-schema-handle--mirror", I.setAttribute("x-flow-handle:target.right", JSON.stringify(E.name)), L.appendChild(I); + const M = document.createElement("div"); + return M.className = "flow-schema-handle flow-schema-handle--source flow-schema-handle--mirror", M.setAttribute("x-flow-handle:source.left", JSON.stringify(E.name)), L.appendChild(M), L; }; i(() => { if (!s.isConnected) return; - const b = a()?.data; - b?.label, b?.kind; - const C = b?.fields; - if (Array.isArray(C)) - for (const E of C) - E.name, E.type, E.key, E.required, E.icon; - _(); + const E = a()?.data; + E?.label, E?.kind; + const b = E?.fields; + if (Array.isArray(b)) + for (const T of b) + T.name, T.type, T.key, T.required, T.icon, T.description, T.deprecated, T.tags, T.defaultValue; + k(); }), r(() => { - h = !0; - for (const b of m.values()) - t.destroyTree(b); - m.clear(), Ho(s), p = null, g = null, s.classList.remove("flow-schema-node"), f?.removeAttribute("data-flow-schema-node"); + p = !0; + for (const E of y.values()) + t.destroyTree(E); + y.clear(), Fo(s), g = null, m = null, s.classList.remove("flow-schema-node"), Ve(s, "flowSchemaNodeClass", null), delete s.dataset.flowSchemaNodeSub, h?.removeAttribute("data-flow-schema-node"); }); }); } -function rm(t) { +function dm(t) { if (!Number.isFinite(t) || t < 0) return ""; if (t < 1e3) return `${t}ms`; if (t < 6e4) { @@ -15440,10 +15562,10 @@ function rm(t) { const e = Math.floor(t / 6e4), n = Math.floor(t % 6e4 / 1e3); return n === 0 ? `${e}m` : `${e}m ${n}s`; } -function js(t) { +function Js(t) { for (; t.firstChild; ) t.removeChild(t.firstChild); } -function am(t) { +function um(t) { t.directive("flow-wait", (e, n, { evaluate: o, effect: i, cleanup: r }) => { const s = e, a = () => { try { @@ -15454,18 +15576,18 @@ function am(t) { }; s.classList.add("flow-wait-node"), s.setAttribute("data-flow-wait", "true"); const l = () => { - js(s); + Js(s); const d = a()?.data; if (!d) return; const u = typeof d.label == "string" && d.label ? d.label : "Wait", f = typeof d.icon == "string" && d.icon ? d.icon : "", h = typeof d.durationMs == "number" ? d.durationMs : NaN, p = document.createElement("div"); if (p.className = "flow-wait-header", f) { - const S = document.createElement("span"); - S.className = "flow-wait-icon", S.textContent = f, p.appendChild(S); + const k = document.createElement("span"); + k.className = "flow-wait-icon", k.textContent = f, p.appendChild(k); } const g = document.createElement("span"); g.className = "flow-wait-label", g.textContent = u, p.appendChild(g); const m = document.createElement("span"); - m.className = "flow-wait-duration", m.textContent = rm(h), p.appendChild(m), s.appendChild(p); + m.className = "flow-wait-duration", m.textContent = dm(h), p.appendChild(m), s.appendChild(p); const y = document.createElement("div"); y.className = "flow-wait-handle flow-wait-handle--target", y.setAttribute("x-flow-handle:target.top", JSON.stringify("in")), s.appendChild(y); const _ = document.createElement("div"); @@ -15476,11 +15598,11 @@ function am(t) { const c = a()?.data; c?.durationMs, c?.label, c?.icon, l(); }), r(() => { - js(s), s.classList.remove("flow-wait-node"), s.removeAttribute("data-flow-wait"); + Js(s), s.classList.remove("flow-wait-node"), s.removeAttribute("data-flow-wait"); }); }); } -const Us = { +const Qs = { equals: "==", notEquals: "!=", greaterThan: ">", @@ -15488,20 +15610,20 @@ const Us = { greaterThanOrEqual: ">=", lessThanOrEqual: "<=" }; -function cn(t) { - return t === null || t === void 0 ? "null" : typeof t == "string" ? `'${t}'` : Array.isArray(t) ? `[${t.map(cn).join(", ")}]` : String(t); +function un(t) { + return t === null || t === void 0 ? "null" : typeof t == "string" ? `'${t}'` : Array.isArray(t) ? `[${t.map(un).join(", ")}]` : String(t); } -function lm(t) { +function fm(t) { const { field: e, op: n, value: o } = t; - return n in Us ? `${e} ${Us[n]} ${cn(o)}` : n === "in" ? `${e} in ${cn(o)}` : n === "notIn" ? `${e} not in ${cn(o)}` : n === "exists" ? `${e} exists` : n === "matches" ? `${e} ~ /${String(o)}/` : `${e} ${n} ${cn(o)}`; + return n in Qs ? `${e} ${Qs[n]} ${un(o)}` : n === "in" ? `${e} in ${un(o)}` : n === "notIn" ? `${e} not in ${un(o)}` : n === "exists" ? `${e} exists` : n === "matches" ? `${e} ~ /${String(o)}/` : `${e} ${n} ${un(o)}`; } -function Gs(t) { +function er(t) { for (; t.firstChild; ) t.removeChild(t.firstChild); } -function cm(t, e) { +function hm(t, e) { return t === "vertical" || t === "horizontal" ? t : e === "vertical" || e === "horizontal" ? e : "horizontal"; } -function dm(t) { +function gm(t) { t.directive("flow-condition", (e, { expression: n }, { evaluate: o, effect: i, cleanup: r }) => { const s = e, a = () => { try { @@ -15519,31 +15641,31 @@ function dm(t) { }; s.classList.add("flow-condition-node"); const c = () => { - const u = a()?.data ?? {}, f = cm(l(), u.direction); + const u = a()?.data ?? {}, f = hm(l(), u.direction); s.setAttribute("data-flow-condition-direction", f); const h = u._branchTaken; - h === "true" || h === "false" ? s.setAttribute("data-flow-condition-branch-taken", h) : s.removeAttribute("data-flow-condition-branch-taken"), Gs(s); + h === "true" || h === "false" ? s.setAttribute("data-flow-condition-branch-taken", h) : s.removeAttribute("data-flow-condition-branch-taken"), er(s); const p = typeof u.label == "string" && u.label ? u.label : "Condition", g = document.createElement("div"); g.className = "flow-condition-header", g.textContent = p, s.appendChild(g); const m = document.createElement("div"); - m.className = "flow-condition-body", u.condition && typeof u.condition == "object" ? m.textContent = lm(u.condition) : typeof u.evaluate == "function" ? m.textContent = typeof u.evaluateLabel == "string" && u.evaluateLabel ? u.evaluateLabel : "[custom evaluator]" : m.textContent = "", s.appendChild(m); + m.className = "flow-condition-body", u.condition && typeof u.condition == "object" ? m.textContent = fm(u.condition) : typeof u.evaluate == "function" ? m.textContent = typeof u.evaluateLabel == "string" && u.evaluateLabel ? u.evaluateLabel : "[custom evaluator]" : m.textContent = "", s.appendChild(m); const y = document.createElement("div"); y.className = "flow-condition-handle-target", y.setAttribute("data-flow-handle-direction", "target"), y.setAttribute("x-flow-handle:target", JSON.stringify("in")), s.appendChild(y); const _ = document.createElement("div"); _.className = "flow-condition-handle-source flow-condition-handle--true", _.setAttribute("data-flow-handle-direction", "source"), _.setAttribute("data-source-handle", "true"), _.setAttribute("x-flow-handle:source", JSON.stringify("true")), s.appendChild(_); - const S = document.createElement("div"); - S.className = "flow-condition-handle-source flow-condition-handle--false", S.setAttribute("data-flow-handle-direction", "source"), S.setAttribute("data-source-handle", "false"), S.setAttribute("x-flow-handle:source", JSON.stringify("false")), s.appendChild(S), t.initTree(s); + const k = document.createElement("div"); + k.className = "flow-condition-handle-source flow-condition-handle--false", k.setAttribute("data-flow-handle-direction", "source"), k.setAttribute("data-source-handle", "false"), k.setAttribute("x-flow-handle:source", JSON.stringify("false")), s.appendChild(k), t.initTree(s); }; i(() => { if (!s.isConnected) return; const d = a()?.data; d?.label, d?.condition, d?.condition?.field, d?.condition?.op, d?.condition?.value, d?.evaluate, d?.evaluateLabel, d?.direction, d?._branchTaken, c(); }), r(() => { - Gs(s), s.classList.remove("flow-condition-node"), s.removeAttribute("data-flow-condition-direction"), s.removeAttribute("data-flow-condition-branch-taken"); + er(s), s.classList.remove("flow-condition-node"), s.removeAttribute("data-flow-condition-direction"), s.removeAttribute("data-flow-condition-branch-taken"); }); }); } -function um(t) { +function pm(t) { t.directive( "flow-row-select", (e, { expression: n }, { evaluate: o, effect: i, cleanup: r }) => { @@ -15572,7 +15694,7 @@ function um(t) { } ); } -function fm(t) { +function mm(t) { t.directive( "flow-detail", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -15606,11 +15728,11 @@ function fm(t) { } ); } -const hm = ["perf", "events", "viewport", "state", "activity"], Zs = ["fps", "memory", "counts", "visible"], Ks = 30; -function gm(t, e) { +const ym = ["perf", "events", "viewport", "state", "activity"], tr = ["fps", "memory", "counts", "visible"], nr = 30; +function wm(t, e) { if (t && typeof t == "object" && Object.keys(t).length > 0) return t; - const n = e.filter((i) => hm.includes(i)); + const n = e.filter((i) => ym.includes(i)); if (n.length === 0) return { perf: !0, events: !0, viewport: !0, state: !0, activity: !0 }; const o = {}; @@ -15618,13 +15740,13 @@ function gm(t, e) { o[i] = !0; return o; } -function pm(t) { - return t.perf ? t.perf === !0 ? [...Zs] : t.perf.filter((e) => Zs.includes(e)) : []; +function vm(t) { + return t.perf ? t.perf === !0 ? [...tr] : t.perf.filter((e) => tr.includes(e)) : []; } -function mm(t) { - return t.events ? t.events === !0 ? Ks : t.events.max ?? Ks : 0; +function _m(t) { + return t.events ? t.events === !0 ? nr : t.events.max ?? nr : 0; } -function sn(t, e) { +function an(t, e) { const n = document.createElement("div"); n.className = `flow-devtools-section ${e}`; const o = document.createElement("div"); @@ -15632,7 +15754,7 @@ function sn(t, e) { const i = document.createElement("div"); return i.className = "flow-devtools-section-content", n.appendChild(i), { wrapper: n, content: i }; } -function Ve(t, e) { +function Be(t, e) { const n = document.createElement("div"); n.className = `flow-devtools-row ${e}`; const o = document.createElement("span"); @@ -15640,7 +15762,7 @@ function Ve(t, e) { const i = document.createElement("span"); return i.className = "flow-devtools-value", i.textContent = "—", n.appendChild(o), n.appendChild(i), { row: n, valueEl: i }; } -function ym(t) { +function bm(t) { t.directive( "flow-devtools", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -15650,12 +15772,12 @@ function ym(t) { a = i(n); } catch { } - const l = gm(a, o), c = e.closest("[x-data]"); + const l = wm(a, o), c = e.closest("[x-data]"); if (!c) return; const d = e.closest(".flow-container"); if (!d) return; e.classList.add("flow-devtools", "canvas-overlay"), e.setAttribute("data-flow-devtools", ""); - const u = (Y) => Y.stopPropagation(); + const u = (H) => H.stopPropagation(); e.addEventListener("wheel", u); const f = document.createElement("button"); f.className = "flow-devtools-toggle nopan", f.title = "Devtools"; @@ -15667,77 +15789,77 @@ function ym(t) { g.className = "flow-devtools-panel", g.style.display = "none", g.style.userSelect = "none", e.appendChild(g); let m = !1; const y = () => { - m = !m, g.style.display = m ? "" : "none", f.title = m ? "Collapse" : "Devtools", m ? K() : ne(); + m = !m, g.style.display = m ? "" : "none", f.title = m ? "Collapse" : "Devtools", m ? W() : Q(); }; f.addEventListener("click", y); - const _ = pm(l); - let S = null, x = null, b = null, C = null, E = null; + const _ = vm(l); + let k = null, x = null, C = null, E = null, b = null; if (_.length > 0) { - const { wrapper: Y, content: W } = sn("Performance", "flow-devtools-perf"); + const { wrapper: H, content: q } = an("Performance", "flow-devtools-perf"); if (_.includes("fps")) { - const { row: j, valueEl: H } = Ve("FPS", "flow-devtools-fps"); - S = H, W.appendChild(j); + const { row: j, valueEl: K } = Be("FPS", "flow-devtools-fps"); + k = K, q.appendChild(j); } if (_.includes("memory")) { - const { row: j, valueEl: H } = Ve("Memory", "flow-devtools-memory"); - x = H, W.appendChild(j); + const { row: j, valueEl: K } = Be("Memory", "flow-devtools-memory"); + x = K, q.appendChild(j); } if (_.includes("counts")) { - const j = Ve("Nodes", "flow-devtools-counts"); - b = j.valueEl, W.appendChild(j.row); - const H = Ve("Edges", "flow-devtools-counts"); - C = H.valueEl, W.appendChild(H.row); + const j = Be("Nodes", "flow-devtools-counts"); + C = j.valueEl, q.appendChild(j.row); + const K = Be("Edges", "flow-devtools-counts"); + E = K.valueEl, q.appendChild(K.row); } if (_.includes("visible")) { - const { row: j, valueEl: H } = Ve("Visible", "flow-devtools-visible"); - E = H, W.appendChild(j); + const { row: j, valueEl: K } = Be("Visible", "flow-devtools-visible"); + b = K, q.appendChild(j); } - g.appendChild(Y); + g.appendChild(H); } - const T = mm(l); - let I = null; + const T = _m(l); + let D = null; if (T > 0) { - const { wrapper: Y, content: W } = sn("Events", "flow-devtools-events"), j = document.createElement("button"); + const { wrapper: H, content: q } = an("Events", "flow-devtools-events"), j = document.createElement("button"); j.className = "flow-devtools-clear-btn nopan", j.textContent = "Clear", j.addEventListener("click", () => { - I && (I.textContent = ""), G.length = 0; - }), Y.querySelector(".flow-devtools-section-title").appendChild(j), I = document.createElement("div"), I.className = "flow-devtools-event-list", W.appendChild(I), g.appendChild(Y); + D && (D.textContent = ""), U.length = 0; + }), H.querySelector(".flow-devtools-section-title").appendChild(j), D = document.createElement("div"), D.className = "flow-devtools-event-list", q.appendChild(D), g.appendChild(H); } - let M = null, A = null, P = null; + let L = null, A = null, P = null; if (l.viewport) { - const { wrapper: Y, content: W } = sn("Viewport", "flow-devtools-viewport"), j = Ve("X", "flow-devtools-vp-x"); - M = j.valueEl, W.appendChild(j.row); - const H = Ve("Y", "flow-devtools-vp-y"); - A = H.valueEl, W.appendChild(H.row); - const te = Ve("Zoom", "flow-devtools-vp-zoom"); - P = te.valueEl, W.appendChild(te.row), g.appendChild(Y); + const { wrapper: H, content: q } = an("Viewport", "flow-devtools-viewport"), j = Be("X", "flow-devtools-vp-x"); + L = j.valueEl, q.appendChild(j.row); + const K = Be("Y", "flow-devtools-vp-y"); + A = K.valueEl, q.appendChild(K.row); + const O = Be("Zoom", "flow-devtools-vp-zoom"); + P = O.valueEl, q.appendChild(O.row), g.appendChild(H); } let v = null; if (l.state) { - const { wrapper: Y, content: W } = sn("Selection", "flow-devtools-state"); - v = document.createElement("div"), v.className = "flow-devtools-state-content", v.textContent = "No selection", W.appendChild(v), g.appendChild(Y); + const { wrapper: H, content: q } = an("Selection", "flow-devtools-state"); + v = document.createElement("div"), v.className = "flow-devtools-state-content", v.textContent = "No selection", q.appendChild(v), g.appendChild(H); } - let w = null, N = null, k = null, R = null; + let w = null, I = null, M = null, R = null; if (l.activity) { - const { wrapper: Y, content: W } = sn("Activity", "flow-devtools-activity"), j = Ve("Animations", "flow-devtools-anim"); - w = j.valueEl, W.appendChild(j.row); - const H = Ve("Particles", "flow-devtools-particles"); - N = H.valueEl, W.appendChild(H.row); - const te = Ve("Follow", "flow-devtools-follow"); - k = te.valueEl, W.appendChild(te.row); - const Q = Ve("Timelines", "flow-devtools-timelines"); - R = Q.valueEl, W.appendChild(Q.row), g.appendChild(Y); + const { wrapper: H, content: q } = an("Activity", "flow-devtools-activity"), j = Be("Animations", "flow-devtools-anim"); + w = j.valueEl, q.appendChild(j.row); + const K = Be("Particles", "flow-devtools-particles"); + I = K.valueEl, q.appendChild(K.row); + const O = Be("Follow", "flow-devtools-follow"); + M = O.valueEl, q.appendChild(O.row); + const J = Be("Timelines", "flow-devtools-timelines"); + R = J.valueEl, q.appendChild(J.row), g.appendChild(H); } - let O = null, q = !1, D = 0, L = performance.now(); - const $ = !!(S || x), F = () => { - if (!q) return; - D++; - const Y = performance.now(); - Y - L >= 1e3 && (S && (S.textContent = String(Math.round(D * 1e3 / (Y - L)))), D = 0, L = Y, x && performance.memory && (x.textContent = Math.round(performance.memory.usedJSHeapSize / 1048576) + " MB")), O = requestAnimationFrame(F); - }, K = () => { - !$ || q || (q = !0, D = 0, L = performance.now(), O = requestAnimationFrame(F)); - }, ne = () => { - q = !1, O !== null && (cancelAnimationFrame(O), O = null); - }, G = [], B = [ + let z = null, V = !1, $ = 0, S = performance.now(); + const N = !!(k || x), F = () => { + if (!V) return; + $++; + const H = performance.now(); + H - S >= 1e3 && (k && (k.textContent = String(Math.round($ * 1e3 / (H - S)))), $ = 0, S = H, x && performance.memory && (x.textContent = Math.round(performance.memory.usedJSHeapSize / 1048576) + " MB")), z = requestAnimationFrame(F); + }, W = () => { + !N || V || (V = !0, $ = 0, S = performance.now(), z = requestAnimationFrame(F)); + }, Q = () => { + V = !1, z !== null && (cancelAnimationFrame(z), z = null); + }, U = [], G = [ "flow-init", "flow-connect", "flow-disconnect", @@ -15760,74 +15882,74 @@ function ym(t) { "flow-undo", "flow-redo" ]; - let z = null; - if (T > 0 && I) { - z = (Y) => { + let Z = null; + if (T > 0 && D) { + Z = (H) => { if (!m) return; - const W = Y, j = W.type.replace("flow-", ""); - let H = ""; + const q = H, j = q.type.replace("flow-", ""); + let K = ""; try { - H = W.detail ? JSON.stringify(W.detail).slice(0, 80) : ""; + K = q.detail ? JSON.stringify(q.detail).slice(0, 80) : ""; } catch { - H = "[circular]"; + K = "[circular]"; } - G.unshift({ name: j, time: Date.now(), detail: H }); - const te = I, Q = document.createElement("div"); - Q.className = "flow-devtools-event-entry"; - const U = document.createElement("span"); - U.className = "flow-devtools-event-name", U.textContent = j; - const J = document.createElement("span"); - J.className = "flow-devtools-event-age", J.textContent = "now"; - const se = document.createElement("span"); - for (se.className = "flow-devtools-event-detail", se.textContent = H, Q.appendChild(U), Q.appendChild(J), Q.appendChild(se), te.prepend(Q); te.children.length > T; ) - te.removeChild(te.lastChild), G.pop(); + U.unshift({ name: j, time: Date.now(), detail: K }); + const O = D, J = document.createElement("div"); + J.className = "flow-devtools-event-entry"; + const ne = document.createElement("span"); + ne.className = "flow-devtools-event-name", ne.textContent = j; + const Y = document.createElement("span"); + Y.className = "flow-devtools-event-age", Y.textContent = "now"; + const re = document.createElement("span"); + for (re.className = "flow-devtools-event-detail", re.textContent = K, J.appendChild(ne), J.appendChild(Y), J.appendChild(re), O.prepend(J); O.children.length > T; ) + O.removeChild(O.lastChild), U.pop(); }; - for (const Y of B) - d.addEventListener(Y, z); + for (const H of G) + d.addEventListener(H, Z); } r(() => { - const Y = t.$data(c); - !Y || !Y.viewport || (M && (M.textContent = Math.round(Y.viewport.x).toString()), A && (A.textContent = Math.round(Y.viewport.y).toString()), P && (P.textContent = Y.viewport.zoom.toFixed(2))); + const H = t.$data(c); + !H || !H.viewport || (L && (L.textContent = Math.round(H.viewport.x).toString()), A && (A.textContent = Math.round(H.viewport.y).toString()), P && (P.textContent = H.viewport.zoom.toFixed(2))); }), r(() => { - const Y = t.$data(c); - if (Y) { - if (b && (b.textContent = String(Y.nodes?.length ?? 0)), C && (C.textContent = String(Y.edges?.length ?? 0)), E && Y._getVisibleNodeIds && (E.textContent = String(Y._getVisibleNodeIds().size)), v) { - const W = Y.selectedNodes, j = Y.selectedEdges; - if (!((W?.size ?? 0) > 0 || (j?.size ?? 0) > 0)) + const H = t.$data(c); + if (H) { + if (C && (C.textContent = String(H.nodes?.length ?? 0)), E && (E.textContent = String(H.edges?.length ?? 0)), b && H._getVisibleNodeIds && (b.textContent = String(H._getVisibleNodeIds().size)), v) { + const q = H.selectedNodes, j = H.selectedEdges; + if (!((q?.size ?? 0) > 0 || (j?.size ?? 0) > 0)) v.textContent = "No selection"; else { - if (v.textContent = "", W && W.size > 0) - for (const te of W) { - const Q = Y.getNode?.(te); - if (!Q) continue; - const U = document.createElement("pre"); - U.className = "flow-devtools-json", U.textContent = JSON.stringify({ id: Q.id, position: Q.position, data: Q.data }, null, 2), v.appendChild(U); + if (v.textContent = "", q && q.size > 0) + for (const O of q) { + const J = H.getNode?.(O); + if (!J) continue; + const ne = document.createElement("pre"); + ne.className = "flow-devtools-json", ne.textContent = JSON.stringify({ id: J.id, position: J.position, data: J.data }, null, 2), v.appendChild(ne); } if (j && j.size > 0) - for (const te of j) { - const Q = Y.edges?.find((J) => J.id === te); - if (!Q) continue; - const U = document.createElement("pre"); - U.className = "flow-devtools-json", U.textContent = JSON.stringify({ id: Q.id, source: Q.source, target: Q.target, type: Q.type }, null, 2), v.appendChild(U); + for (const O of j) { + const J = H.edges?.find((Y) => Y.id === O); + if (!J) continue; + const ne = document.createElement("pre"); + ne.className = "flow-devtools-json", ne.textContent = JSON.stringify({ id: J.id, source: J.source, target: J.target, type: J.type }, null, 2), v.appendChild(ne); } } } if (w) { - const W = Y._animator?._groups?.size ?? 0; - w.textContent = String(W); + const q = H._animator?._groups?.size ?? 0; + w.textContent = String(q); } - N && (N.textContent = String(Y._activeParticles?.size ?? 0)), k && (k.textContent = Y._followHandle ? "Active" : "Idle"), R && (R.textContent = String(Y._activeTimelines?.size ?? 0)); + I && (I.textContent = String(H._activeParticles?.size ?? 0)), M && (M.textContent = H._followHandle ? "Active" : "Idle"), R && (R.textContent = String(H._activeTimelines?.size ?? 0)); } }), s(() => { - if (ne(), f.removeEventListener("click", y), z) - for (const Y of B) - d.removeEventListener(Y, z); - e.removeEventListener("wheel", u), e.textContent = "", S = null, x = null, b = null, C = null, E = null, I = null, M = null, A = null, P = null, v = null, w = null, N = null, k = null, R = null; + if (Q(), f.removeEventListener("click", y), Z) + for (const H of G) + d.removeEventListener(H, Z); + e.removeEventListener("wheel", u), e.textContent = "", k = null, x = null, C = null, E = null, b = null, D = null, L = null, A = null, P = null, v = null, w = null, I = null, M = null, R = null; }); } ); } -const wm = { +const xm = { undo: { method: "undo", disabledWhen: (t) => !t.canUndo, aria: "disabled" }, redo: { method: "redo", disabledWhen: (t) => !t.canRedo, aria: "disabled" }, "fit-view": { method: "fitView", passExpression: !0 }, @@ -15846,14 +15968,14 @@ const wm = { reset: { method: "$reset" }, export: { method: "toImage", passExpression: !0 } }; -function vm(t) { - return wm[t] ?? null; +function Em(t) { + return xm[t] ?? null; } -function _m(t) { +function Cm(t) { t.directive( "flow-action", (e, { value: n, expression: o }, { evaluate: i, effect: r, cleanup: s }) => { - const l = vm(n); + const l = Em(n); if (!l) return; const c = e.closest("[data-flow-canvas]"); @@ -15878,17 +16000,17 @@ function _m(t) { } ); } -function bm(t, e) { +function Sm(t, e) { if (t !== "node" && t !== "row") return null; const n = e.includes("clear"); return { type: t, isClear: n }; } -const Ro = /* @__PURE__ */ new WeakMap(); -function xm(t) { +const Oo = /* @__PURE__ */ new WeakMap(); +function km(t) { t.directive( "flow-filter", (e, { value: n, expression: o, modifiers: i }, { evaluate: r, effect: s, cleanup: a }) => { - const l = bm(n, i); + const l = Sm(n, i); if (!l) return; const c = e.closest("[data-flow-canvas]"); if (!c) return; @@ -15898,14 +16020,14 @@ function xm(t) { const f = () => { if (l.isClear) { if (l.type === "node") - d.clearNodeFilter(), Ro.set(c, null); + d.clearNodeFilter(), Oo.set(c, null); else for (const h of d.nodes) h.rowFilter && h.rowFilter !== "all" && d.setRowFilter(h.id, "all"); return; } if (l.type === "node" && o) - u = r(`[${o}]`)[0], d.setNodeFilter(u), Ro.set(c, u); + u = r(`[${o}]`)[0], d.setNodeFilter(u), Oo.set(c, u); else if (l.type === "row" && o) { const h = r(o); d.setRowFilter(h.node, h.predicate); @@ -15913,7 +16035,7 @@ function xm(t) { }; e.addEventListener("click", f), e.style.cursor = "pointer", l.type === "node" && !l.isClear && s(() => { d.nodes.length; - const h = Ro.get(c) === u && u !== null; + const h = Oo.get(c) === u && u !== null; e.classList.toggle("flow-filter-active", h), e.setAttribute("aria-pressed", String(h)); }), a(() => { e.removeEventListener("click", f); @@ -15921,7 +16043,7 @@ function xm(t) { } ); } -function Em(t) { +function Lm(t) { if (typeof t == "string") return { target: t }; if (t && typeof t == "object" && "target" in t) { @@ -15934,7 +16056,7 @@ function Em(t) { } return null; } -function Cm(t) { +function Pm(t) { t.directive( "flow-follow", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -15947,7 +16069,7 @@ function Cm(t) { e.classList.toggle("flow-following", h), e.setAttribute("aria-pressed", String(h)); }, f = () => { if (!n) return; - const h = i(n), p = Em(h); + const h = i(n), p = Lm(h); if (!p) return; if (a && d) { d.stop(), d = null, u(!1); @@ -15965,27 +16087,27 @@ function Cm(t) { } ); } -function Sm(t, e) { +function Mm(t, e) { return t !== "save" && t !== "restore" ? null : { action: t, persist: e.includes("persist") }; } -const Ti = /* @__PURE__ */ new Map(); -function km(t, e) { - Ti.set(t, e); +const Ai = /* @__PURE__ */ new Map(); +function Tm(t, e) { + Ai.set(t, e); } -function Lm(t) { - return Ti.get(t) ?? null; +function Nm(t) { + return Ai.get(t) ?? null; } -function Pm(t) { - return Ti.has(t); +function Am(t) { + return Ai.has(t); } -function Fo(t) { +function zo(t) { return `alpineflow-snapshot-${t}`; } -function Mm(t) { +function $m(t) { t.directive( "flow-snapshot", (e, { value: n, expression: o, modifiers: i }, { evaluate: r, effect: s, cleanup: a }) => { - const l = Sm(n, i); + const l = Mm(n, i); if (!l) return; const c = e.closest("[data-flow-canvas]"); if (!c) return; @@ -15997,18 +16119,18 @@ function Mm(t) { if (f) if (l.action === "save") { const h = d.toObject(); - l.persist ? localStorage.setItem(Fo(f), JSON.stringify(h)) : km(f, h); + l.persist ? localStorage.setItem(zo(f), JSON.stringify(h)) : Tm(f, h); } else { let h = null; if (l.persist) { - const p = localStorage.getItem(Fo(f)); + const p = localStorage.getItem(zo(f)); if (p) try { h = JSON.parse(p); } catch { } } else - h = Lm(f); + h = Nm(f); h && d.fromObject(h); } }; @@ -16017,14 +16139,14 @@ function Mm(t) { const f = r(o); if (!f) return; let h; - l.persist ? h = localStorage.getItem(Fo(f)) !== null : (d.nodes.length, h = Pm(f)), e.disabled = !h, e.setAttribute("aria-disabled", String(!h)); + l.persist ? h = localStorage.getItem(zo(f)) !== null : (d.nodes.length, h = Am(f)), e.disabled = !h, e.setAttribute("aria-disabled", String(!h)); }), a(() => { e.removeEventListener("click", u); }); } ); } -function Tm(t) { +function Im(t) { const e = document.createElement("div"); e.className = "flow-loading-indicator"; const n = document.createElement("div"); @@ -16032,7 +16154,7 @@ function Tm(t) { const o = document.createElement("div"); return o.className = "flow-loading-indicator-text", o.textContent = t ?? "Loading…", e.appendChild(n), e.appendChild(o), e; } -function Am(t) { +function Dm(t) { t.directive( "flow-loading", (e, { modifiers: n }, { effect: o, cleanup: i }) => { @@ -16040,7 +16162,7 @@ function Am(t) { if (!r) return; const s = t.$data(r); if (!s) return; - e.classList.add("flow-loading-overlay"), e.childElementCount > 0 || e.textContent.trim().length > 0 || e.appendChild(Tm(s._loadingText)); + e.classList.add("flow-loading-overlay"), e.childElementCount > 0 || e.textContent.trim().length > 0 || e.appendChild(Im(s._loadingText)); const l = n.includes("fade"); l && e.classList.add("flow-loading-fade"), r.setAttribute("data-flow-loading-directive", ""); let c = null; @@ -16061,7 +16183,7 @@ function Am(t) { } ); } -function Nm(t) { +function Rm(t) { t.directive( "flow-edge-toolbar", (e, { expression: n, modifiers: o }, { evaluate: i, effect: r, cleanup: s }) => { @@ -16087,35 +16209,35 @@ function Nm(t) { e.addEventListener("pointerdown", f), e.addEventListener("click", h); const p = o.includes("below"), g = 20; r(() => { - if (!d.edges.some((I) => I.id === l)) { + if (!d.edges.some((D) => D.id === l)) { e.removeEventListener("pointerdown", f), e.removeEventListener("click", h), e.classList.remove("flow-edge-toolbar"), e.remove(); return; } const m = d.viewport?.zoom || 1, y = parseInt(e.getAttribute("data-flow-offset") ?? String(g), 10); let _ = 0.5; if (n) { - const I = i(n); - typeof I == "number" && (_ = I); + const D = i(n); + typeof D == "number" && (_ = D); } - const S = a.querySelectorAll("path"), x = S.length > 1 ? S[1] : S[0]; + const k = a.querySelectorAll("path"), x = k.length > 1 ? k[1] : k[0]; if (!x) return; - const b = x.getTotalLength?.(); - if (!b) return; - const C = x.getPointAtLength(b * Math.max(0, Math.min(1, _))), E = y / m, T = p ? E : -E; - e.style.left = `${C.x}px`, e.style.top = `${C.y + T}px`, e.style.transformOrigin = "0 0", e.style.transform = `scale(${1 / m}) translate(-50%, ${p ? "0%" : "-100%"})`; + const C = x.getTotalLength?.(); + if (!C) return; + const E = x.getPointAtLength(C * Math.max(0, Math.min(1, _))), b = y / m, T = p ? b : -b; + e.style.left = `${E.x}px`, e.style.top = `${E.y + T}px`, e.style.transformOrigin = "0 0", e.style.transform = `scale(${1 / m}) translate(-50%, ${p ? "0%" : "-100%"})`; }), s(() => { e.removeEventListener("pointerdown", f), e.removeEventListener("click", h), e.classList.remove("flow-edge-toolbar"), e.remove(); }); } ); } -function $m(t) { +function Hm(t) { t.magic("flow", (e) => { const n = e.closest("[data-flow-canvas]"); return n ? t.$data(n) : (console.warn("[alpinejs-flow] $flow used outside of a flowCanvas context"), {}); }); } -function Im(t) { +function Fm(t) { t.store("flow", { instances: {}, activeId: null, @@ -16140,8 +16262,8 @@ function Im(t) { } }); } -function qy(t, e, n) { - const o = n?.defaultDimensions?.width ?? we, i = n?.defaultDimensions?.height ?? _e, r = n?.padding ?? 20, s = n?.flowId ?? "ssr", l = t.filter((y) => !y.hidden).map((y) => ({ +function ow(t, e, n) { + const o = n?.defaultDimensions?.width ?? ve, i = n?.defaultDimensions?.height ?? be, r = n?.padding ?? 20, s = n?.flowId ?? "ssr", l = t.filter((y) => !y.hidden).map((y) => ({ ...y, dimensions: { width: y.dimensions?.width ?? o, @@ -16158,55 +16280,55 @@ function qy(t, e, n) { height: y.dimensions.height, ...y.class ? { class: y.class } : {}, ...y.style ? { - style: typeof y.style == "string" ? y.style : Object.entries(y.style).map(([_, S]) => `${_}:${S}`).join(";") + style: typeof y.style == "string" ? y.style : Object.entries(y.style).map(([_, k]) => `${_}:${k}`).join(";") } : {}, data: y.data ?? {} })), u = e.filter((y) => !y.hidden), f = [], h = /* @__PURE__ */ new Map(); for (const y of u) { - const _ = c.get(y.source), S = c.get(y.target); - if (!_ || !S) + const _ = c.get(y.source), k = c.get(y.target); + if (!_ || !k) continue; - let x, b; + let x, C; try { - const M = lo( + const L = co( y, _, - S, + k, _.sourcePosition ?? "bottom", - S.targetPosition ?? "top" + k.targetPosition ?? "top" ); - x = M.path, b = M.labelPosition; + x = L.path, C = L.labelPosition; } catch { continue; } - let C, E; + let E, b; if (y.markerStart) { - const M = Ot(y.markerStart), A = zt(M, s); - h.has(A) || h.set(A, no(M, A)), C = `url(#${A})`; + const L = Vt(y.markerStart), A = Bt(L, s); + h.has(A) || h.set(A, oo(L, A)), E = `url(#${A})`; } if (y.markerEnd) { - const M = Ot(y.markerEnd), A = zt(M, s); - h.has(A) || h.set(A, no(M, A)), E = `url(#${A})`; + const L = Vt(y.markerEnd), A = Bt(L, s); + h.has(A) || h.set(A, oo(L, A)), b = `url(#${A})`; } - let T, I; + let T, D; if (y.label) - if (b) - T = b.x, I = b.y; + if (C) + T = C.x, D = C.y; else { - const M = _.position.x + _.dimensions.width / 2, A = _.position.y + _.dimensions.height / 2, P = S.position.x + S.dimensions.width / 2, v = S.position.y + S.dimensions.height / 2; - T = (M + P) / 2, I = (A + v) / 2; + const L = _.position.x + _.dimensions.width / 2, A = _.position.y + _.dimensions.height / 2, P = k.position.x + k.dimensions.width / 2, v = k.position.y + k.dimensions.height / 2; + T = (L + P) / 2, D = (A + v) / 2; } f.push({ id: y.id, source: y.source, target: y.target, pathD: x, - ...C ? { markerStart: C } : {}, - ...E ? { markerEnd: E } : {}, + ...E ? { markerStart: E } : {}, + ...b ? { markerEnd: b } : {}, ...y.class ? { class: y.class } : {}, ...y.label ? { label: y.label } : {}, ...T !== void 0 ? { labelX: T } : {}, - ...I !== void 0 ? { labelY: I } : {} + ...D !== void 0 ? { labelY: D } : {} }); } const p = Array.from(h.values()).join(` @@ -16215,7 +16337,7 @@ function qy(t, e, n) { if (l.length === 0) g = { x: 0, y: 0, width: 0, height: 0 }, m = { x: 0, y: 0, zoom: 1 }; else { - const y = Yt(l); + const y = Wt(l); g = { x: y.x - r, y: y.y - r, @@ -16235,86 +16357,158 @@ function qy(t, e, n) { viewport: m }; } -const Js = /* @__PURE__ */ new WeakSet(); -function Yy(t) { - Js.has(t) || (Js.add(t), Oa(t), Im(t), up(t), xp(t), Xf(t), If(t), Df(t), Hf(t), sp(t), kp(t), Tp(t), Ap(t), $p(t), Dp(t), jp(t), Up(t), Kp(t), Qp(t), nm(t), om(t), im(t), um(t), fm(t), ym(t), _m(t), xm(t), Cm(t), Mm(t), Am(t), Nm(t), sm(t), am(t), dm(t), $m(t)); +const or = /* @__PURE__ */ new WeakSet(); +function iw(t) { + or.has(t) || (or.add(t), qa(t), Fm(t), pp(t), kp(t), Gf(t), Ff(t), Of(t), zf(t), cp(t), Tp(t), Ip(t), Dp(t), Hp(t), Op(t), Kp(t), Jp(t), tm(t), om(t), rm(t), am(t), lm(t), pm(t), mm(t), bm(t), Cm(t), km(t), Pm(t), $m(t), Dm(t), Rm(t), cm(t), um(t), gm(t), Hm(t)); } -function Dm(t) { +function Om(t) { return t.replace(/\s+(?:@|:|x-)[\w.:-]*="[^"]*"/g, "").replace(/\s+externalResourcesRequired="[^"]*"/g, ""); } -function Hm(t, e, n, o) { - return new Promise((i, r) => { - const s = new Image(); - s.onload = () => { - const a = document.createElement("canvas"); - a.width = e, a.height = n; - const l = a.getContext("2d"); - l.fillStyle = o, l.fillRect(0, 0, e, n), l.drawImage(s, 0, 0), i(a.toDataURL("image/png")); - }, s.onerror = () => { - r(new Error("Failed to render SVG to image")); - }, s.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(t); +const zm = [ + "stroke", + "stroke-width", + "stroke-opacity", + "stroke-dasharray", + "stroke-dashoffset", + "stroke-linecap", + "stroke-linejoin", + "fill", + "fill-opacity", + "fill-rule", + "opacity", + "marker-start", + "marker-mid", + "marker-end" +]; +function Vm(t) { + const e = t.querySelectorAll( + "svg path, svg line, svg polyline, svg polygon, svg circle, svg ellipse, svg rect, svg text" + ), n = []; + for (const o of e) { + const i = getComputedStyle(o), r = []; + for (const s of zm) { + const a = i.getPropertyValue(s); + a && (r.push([s, o.getAttribute(s)]), o.setAttribute(s, a)); + } + r.length > 0 && n.push(() => { + for (const [s, a] of r) + a === null ? o.removeAttribute(s) : o.setAttribute(s, a); + }); + } + return () => { + for (const o of n) o(); + }; +} +const Bm = 16384, qm = 4e7, Xm = 8; +function Ym(t, e, n) { + if (typeof t != "number" || !Number.isFinite(t) || t <= 0) return 1; + const o = Bm / Math.max(e, n), i = Math.sqrt(qm / Math.max(1, e * n)), r = Math.max(1, Math.min(Xm, o, i)); + return Math.min(t, r); +} +const Wm = 0.92; +function jm(t) { + return typeof t != "number" || !Number.isFinite(t) ? Wm : Math.min(1, Math.max(0, t)); +} +function Um(t) { + return t.replace(/&/g, "&").replace(/"/g, """).replace(/"); + if (n === -1) return t; + const o = ``; + return t.slice(0, n + 1) + o + t.slice(n + 1); +} +function Zm(t, e, n, o, i = 1, r = "png", s) { + return new Promise((a, l) => { + const c = new Image(); + c.onload = () => { + const d = document.createElement("canvas"); + d.width = Math.round(e * i), d.height = Math.round(n * i); + const u = d.getContext("2d"); + u.scale(i, i), u.fillStyle = o, u.fillRect(0, 0, e, n), u.drawImage(c, 0, 0, e, n), a( + r === "jpeg" ? d.toDataURL("image/jpeg", jm(s)) : d.toDataURL("image/png") + ); + }, c.onerror = () => { + l(new Error("Failed to render SVG to image")); + }, c.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(t); }); } -async function Rm(t, e, n, o, i = {}) { +async function Km(t, e, n, o, i = {}) { let r; try { - ({ toSvg: r } = await Promise.resolve().then(() => Iy)); + ({ toSvg: r } = await Promise.resolve().then(() => Uy)); } catch { throw new Error("toImage() requires html-to-image. Install it with: npm install html-to-image"); } const s = i.scope ?? "all", a = t.getBoundingClientRect(), l = s === "viewport" ? a.width : i.width ?? 1920, c = s === "viewport" ? a.height : i.height ?? 1080, d = i.background ?? (getComputedStyle(t).getPropertyValue("--flow-bg-color").trim() || "#ffffff"), u = e.style.transform, f = e.style.width, h = e.style.height, p = t.style.width, g = t.style.height, m = t.style.overflow, y = []; + let _ = null; try { if (s === "all") { - const M = t.querySelectorAll("[data-flow-culled]"); - for (const N of M) - N.style.display = "", y.push(N); - const A = n.filter((N) => !N.hidden), P = Yt(A), v = i.padding ?? 0.1, w = Jn( - P, + const P = t.querySelectorAll("[data-flow-culled]"); + for (const R of P) + R.style.display = "", y.push(R); + const v = n.filter((R) => !R.hidden), w = Wt(v), I = i.padding ?? 0.1, M = Qn( + w, l, c, 0.1, // minZoom 2, // maxZoom - v + I ); - e.style.transform = `translate(${w.x}px, ${w.y}px) scale(${w.zoom})`, e.style.width = `${l}px`, e.style.height = `${c}px`; + e.style.transform = `translate(${M.x}px, ${M.y}px) scale(${M.zoom})`, e.style.width = `${l}px`, e.style.height = `${c}px`; } - t.style.width = `${l}px`, t.style.height = `${c}px`, t.style.overflow = "hidden", await new Promise((M) => requestAnimationFrame(M)); - const _ = i.includeOverlays, S = _ === !0, x = typeof _ == "object" ? _ : {}, b = [ - ["canvas-overlay", S || (x.toolbar ?? !1)], - ["flow-minimap", S || (x.minimap ?? !1)], - ["flow-controls", S || (x.controls ?? !1)], - ["flow-panel", S || (x.panels ?? !1)], + t.style.width = `${l}px`, t.style.height = `${c}px`, t.style.overflow = "hidden", await new Promise((P) => requestAnimationFrame(P)), _ = Vm(t); + const k = i.includeOverlays, x = k === !0, C = typeof k == "object" ? k : {}, E = [ + ["canvas-overlay", x || (C.toolbar ?? !1)], + ["flow-minimap", x || (C.minimap ?? !1)], + ["flow-controls", x || (C.controls ?? !1)], + ["flow-panel", x || (C.panels ?? !1)], ["flow-selection-box", !1] - ], C = await r(t, { + ], b = await r(t, { width: l, height: c, skipFonts: !0, - filter: (M) => { - if (M.classList) { - for (const [A, P] of b) - if (M.classList.contains(A) && !P) return !1; + filter: (P) => { + if (P.classList) { + for (const [v, w] of E) + if (P.classList.contains(v) && !w) return !1; } return !0; } - }), T = Dm(decodeURIComponent(C.substring("data:image/svg+xml;charset=utf-8,".length))), I = await Hm(T, l, c, d); + }), T = "data:image/svg+xml;charset=utf-8,", D = Om(decodeURIComponent(b.substring(T.length))), L = i.format ?? "png"; + let A; + if (L === "svg") + A = T + encodeURIComponent(Gm(D, d)); + else { + const P = Ym(i.scale, l, c); + A = await Zm( + D, + l, + c, + d, + P, + L, + i.quality + ); + } if (i.filename) { - const M = document.createElement("a"); - M.download = i.filename, M.href = I, M.click(); + const P = document.createElement("a"); + P.download = i.filename, P.href = A, P.click(); } - return I; + return A; } finally { - e.style.transform = u, e.style.width = f, e.style.height = h, t.style.width = p, t.style.height = g, t.style.overflow = m; - for (const _ of y) - _.style.display = "none"; + _?.(), e.style.transform = u, e.style.width = f, e.style.height = h, t.style.width = p, t.style.height = g, t.style.overflow = m; + for (const k of y) + k.style.display = "none"; } } -const Fm = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const Jm = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, - captureFlowImage: Rm + captureFlowImage: Km }, Symbol.toStringTag, { value: "Module" })); -function Om(t, e) { +function Qm(t, e) { if (t.match(/^[a-z]+:\/\//i)) return t; if (t.match(/^\/\//)) @@ -16324,7 +16518,7 @@ function Om(t, e) { const n = document.implementation.createHTMLDocument(), o = n.createElement("base"), i = n.createElement("a"); return n.head.appendChild(o), n.body.appendChild(i), e && (o.href = e), i.href = t, i.href; } -const zm = /* @__PURE__ */ (() => { +const ey = /* @__PURE__ */ (() => { let t = 0; const e = () => ( // eslint-disable-next-line no-bitwise @@ -16338,27 +16532,27 @@ function vt(t) { e.push(t[n]); return e; } -let $t = null; -function Ma(t = {}) { - return $t || (t.includeStyleProperties ? ($t = t.includeStyleProperties, $t) : ($t = vt(window.getComputedStyle(document.documentElement)), $t)); +let Dt = null; +function $a(t = {}) { + return Dt || (t.includeStyleProperties ? (Dt = t.includeStyleProperties, Dt) : (Dt = vt(window.getComputedStyle(document.documentElement)), Dt)); } -function fo(t, e) { +function ho(t, e) { const o = (t.ownerDocument.defaultView || window).getComputedStyle(t).getPropertyValue(e); return o ? parseFloat(o.replace("px", "")) : 0; } -function Vm(t) { - const e = fo(t, "border-left-width"), n = fo(t, "border-right-width"); +function ty(t) { + const e = ho(t, "border-left-width"), n = ho(t, "border-right-width"); return t.clientWidth + e + n; } -function Bm(t) { - const e = fo(t, "border-top-width"), n = fo(t, "border-bottom-width"); +function ny(t) { + const e = ho(t, "border-top-width"), n = ho(t, "border-bottom-width"); return t.clientHeight + e + n; } -function Ai(t, e = {}) { - const n = e.width || Vm(t), o = e.height || Bm(t); +function $i(t, e = {}) { + const n = e.width || ty(t), o = e.height || ny(t); return { width: n, height: o }; } -function qm() { +function oy() { let t, e; try { e = process; @@ -16367,11 +16561,11 @@ function qm() { const n = e && e.env ? e.env.devicePixelRatio : null; return n && (t = parseInt(n, 10), Number.isNaN(t) && (t = 1)), t || window.devicePixelRatio || 1; } -const De = 16384; -function Ym(t) { - (t.width > De || t.height > De) && (t.width > De && t.height > De ? t.width > t.height ? (t.height *= De / t.width, t.width = De) : (t.width *= De / t.height, t.height = De) : t.width > De ? (t.height *= De / t.width, t.width = De) : (t.width *= De / t.height, t.height = De)); +const Re = 16384; +function iy(t) { + (t.width > Re || t.height > Re) && (t.width > Re && t.height > Re ? t.width > t.height ? (t.height *= Re / t.width, t.width = Re) : (t.width *= Re / t.height, t.height = Re) : t.width > Re ? (t.height *= Re / t.width, t.width = Re) : (t.width *= Re / t.height, t.height = Re)); } -function Xm(t, e = {}) { +function sy(t, e = {}) { return t.toBlob ? new Promise((n) => { t.toBlob(n, e.type ? e.type : "image/png", e.quality ? e.quality : 1); }) : new Promise((n) => { @@ -16383,7 +16577,7 @@ function Xm(t, e = {}) { })); }); } -function ho(t) { +function go(t) { return new Promise((e, n) => { const o = new Image(); o.onload = () => { @@ -16393,12 +16587,12 @@ function ho(t) { }, o.onerror = n, o.crossOrigin = "anonymous", o.decoding = "async", o.src = t; }); } -async function Wm(t) { +async function ry(t) { return Promise.resolve().then(() => new XMLSerializer().serializeToString(t)).then(encodeURIComponent).then((e) => `data:image/svg+xml;charset=utf-8,${e}`); } -async function jm(t, e, n) { +async function ay(t, e, n) { const o = "http://www.w3.org/2000/svg", i = document.createElementNS(o, "svg"), r = document.createElementNS(o, "foreignObject"); - return i.setAttribute("width", `${e}`), i.setAttribute("height", `${n}`), i.setAttribute("viewBox", `0 0 ${e} ${n}`), r.setAttribute("width", "100%"), r.setAttribute("height", "100%"), r.setAttribute("x", "0"), r.setAttribute("y", "0"), r.setAttribute("externalResourcesRequired", "true"), i.appendChild(r), r.appendChild(t), Wm(i); + return i.setAttribute("width", `${e}`), i.setAttribute("height", `${n}`), i.setAttribute("viewBox", `0 0 ${e} ${n}`), r.setAttribute("width", "100%"), r.setAttribute("height", "100%"), r.setAttribute("x", "0"), r.setAttribute("y", "0"), r.setAttribute("externalResourcesRequired", "true"), i.appendChild(r), r.appendChild(t), ry(i); } const $e = (t, e) => { if (t instanceof e) @@ -16406,67 +16600,67 @@ const $e = (t, e) => { const n = Object.getPrototypeOf(t); return n === null ? !1 : n.constructor.name === e.name || $e(n, e); }; -function Um(t) { +function ly(t) { const e = t.getPropertyValue("content"); return `${t.cssText} content: '${e.replace(/'|"/g, "")}';`; } -function Gm(t, e) { - return Ma(e).map((n) => { +function cy(t, e) { + return $a(e).map((n) => { const o = t.getPropertyValue(n), i = t.getPropertyPriority(n); return `${n}: ${o}${i ? " !important" : ""};`; }).join(" "); } -function Zm(t, e, n, o) { - const i = `.${t}:${e}`, r = n.cssText ? Um(n) : Gm(n, o); +function dy(t, e, n, o) { + const i = `.${t}:${e}`, r = n.cssText ? ly(n) : cy(n, o); return document.createTextNode(`${i}{${r}}`); } -function Qs(t, e, n, o) { +function ir(t, e, n, o) { const i = window.getComputedStyle(t, n), r = i.getPropertyValue("content"); if (r === "" || r === "none") return; - const s = zm(); + const s = ey(); try { e.className = `${e.className} ${s}`; } catch { return; } const a = document.createElement("style"); - a.appendChild(Zm(s, n, i, o)), e.appendChild(a); + a.appendChild(dy(s, n, i, o)), e.appendChild(a); } -function Km(t, e, n) { - Qs(t, e, ":before", n), Qs(t, e, ":after", n); +function uy(t, e, n) { + ir(t, e, ":before", n), ir(t, e, ":after", n); } -const er = "application/font-woff", tr = "image/jpeg", Jm = { - woff: er, - woff2: er, +const sr = "application/font-woff", rr = "image/jpeg", fy = { + woff: sr, + woff2: sr, ttf: "application/font-truetype", eot: "application/vnd.ms-fontobject", png: "image/png", - jpg: tr, - jpeg: tr, + jpg: rr, + jpeg: rr, gif: "image/gif", tiff: "image/tiff", svg: "image/svg+xml", webp: "image/webp" }; -function Qm(t) { +function hy(t) { const e = /\.([^./]*?)$/g.exec(t); return e ? e[1] : ""; } -function Ni(t) { - const e = Qm(t).toLowerCase(); - return Jm[e] || ""; +function Ii(t) { + const e = hy(t).toLowerCase(); + return fy[e] || ""; } -function ey(t) { +function gy(t) { return t.split(/,/)[1]; } -function ci(t) { +function ui(t) { return t.search(/^(data:)/) !== -1; } -function ty(t, e) { +function py(t, e) { return `data:${e};base64,${t}`; } -async function Ta(t, e, n) { +async function Ia(t, e, n) { const o = await fetch(t, e); if (o.status === 404) throw new Error(`Resource "${o.url}" not found`); @@ -16482,86 +16676,86 @@ async function Ta(t, e, n) { }, a.readAsDataURL(i); }); } -const Oo = {}; -function ny(t, e, n) { +const Vo = {}; +function my(t, e, n) { let o = t.replace(/\?.*/, ""); return n && (o = t), /ttf|otf|eot|woff2?/i.test(o) && (o = o.replace(/.*\//, "")), e ? `[${e}]${o}` : o; } -async function $i(t, e, n) { - const o = ny(t, e, n.includeQueryParams); - if (Oo[o] != null) - return Oo[o]; +async function Di(t, e, n) { + const o = my(t, e, n.includeQueryParams); + if (Vo[o] != null) + return Vo[o]; n.cacheBust && (t += (/\?/.test(t) ? "&" : "?") + (/* @__PURE__ */ new Date()).getTime()); let i; try { - const r = await Ta(t, n.fetchRequestInit, ({ res: s, result: a }) => (e || (e = s.headers.get("Content-Type") || ""), ey(a))); - i = ty(r, e); + const r = await Ia(t, n.fetchRequestInit, ({ res: s, result: a }) => (e || (e = s.headers.get("Content-Type") || ""), gy(a))); + i = py(r, e); } catch (r) { i = n.imagePlaceholder || ""; let s = `Failed to fetch resource: ${t}`; r && (s = typeof r == "string" ? r : r.message), s && console.warn(s); } - return Oo[o] = i, i; + return Vo[o] = i, i; } -async function oy(t) { +async function yy(t) { const e = t.toDataURL(); - return e === "data:," ? t.cloneNode(!1) : ho(e); + return e === "data:," ? t.cloneNode(!1) : go(e); } -async function iy(t, e) { +async function wy(t, e) { if (t.currentSrc) { const r = document.createElement("canvas"), s = r.getContext("2d"); r.width = t.clientWidth, r.height = t.clientHeight, s?.drawImage(t, 0, 0, r.width, r.height); const a = r.toDataURL(); - return ho(a); + return go(a); } - const n = t.poster, o = Ni(n), i = await $i(n, o, e); - return ho(i); + const n = t.poster, o = Ii(n), i = await Di(n, o, e); + return go(i); } -async function sy(t, e) { +async function vy(t, e) { var n; try { if (!((n = t?.contentDocument) === null || n === void 0) && n.body) - return await xo(t.contentDocument.body, e, !0); + return await Co(t.contentDocument.body, e, !0); } catch { } return t.cloneNode(!1); } -async function ry(t, e) { - return $e(t, HTMLCanvasElement) ? oy(t) : $e(t, HTMLVideoElement) ? iy(t, e) : $e(t, HTMLIFrameElement) ? sy(t, e) : t.cloneNode(Aa(t)); +async function _y(t, e) { + return $e(t, HTMLCanvasElement) ? yy(t) : $e(t, HTMLVideoElement) ? wy(t, e) : $e(t, HTMLIFrameElement) ? vy(t, e) : t.cloneNode(Da(t)); } -const ay = (t) => t.tagName != null && t.tagName.toUpperCase() === "SLOT", Aa = (t) => t.tagName != null && t.tagName.toUpperCase() === "SVG"; -async function ly(t, e, n) { +const by = (t) => t.tagName != null && t.tagName.toUpperCase() === "SLOT", Da = (t) => t.tagName != null && t.tagName.toUpperCase() === "SVG"; +async function xy(t, e, n) { var o, i; - if (Aa(e)) + if (Da(e)) return e; let r = []; - return ay(t) && t.assignedNodes ? r = vt(t.assignedNodes()) : $e(t, HTMLIFrameElement) && (!((o = t.contentDocument) === null || o === void 0) && o.body) ? r = vt(t.contentDocument.body.childNodes) : r = vt(((i = t.shadowRoot) !== null && i !== void 0 ? i : t).childNodes), r.length === 0 || $e(t, HTMLVideoElement) || await r.reduce((s, a) => s.then(() => xo(a, n)).then((l) => { + return by(t) && t.assignedNodes ? r = vt(t.assignedNodes()) : $e(t, HTMLIFrameElement) && (!((o = t.contentDocument) === null || o === void 0) && o.body) ? r = vt(t.contentDocument.body.childNodes) : r = vt(((i = t.shadowRoot) !== null && i !== void 0 ? i : t).childNodes), r.length === 0 || $e(t, HTMLVideoElement) || await r.reduce((s, a) => s.then(() => Co(a, n)).then((l) => { l && e.appendChild(l); }), Promise.resolve()), e; } -function cy(t, e, n) { +function Ey(t, e, n) { const o = e.style; if (!o) return; const i = window.getComputedStyle(t); - i.cssText ? (o.cssText = i.cssText, o.transformOrigin = i.transformOrigin) : Ma(n).forEach((r) => { + i.cssText ? (o.cssText = i.cssText, o.transformOrigin = i.transformOrigin) : $a(n).forEach((r) => { let s = i.getPropertyValue(r); r === "font-size" && s.endsWith("px") && (s = `${Math.floor(parseFloat(s.substring(0, s.length - 2))) - 0.1}px`), $e(t, HTMLIFrameElement) && r === "display" && s === "inline" && (s = "block"), r === "d" && e.getAttribute("d") && (s = `path(${e.getAttribute("d")})`), o.setProperty(r, s, i.getPropertyPriority(r)); }); } -function dy(t, e) { +function Cy(t, e) { $e(t, HTMLTextAreaElement) && (e.innerHTML = t.value), $e(t, HTMLInputElement) && e.setAttribute("value", t.value); } -function uy(t, e) { +function Sy(t, e) { if ($e(t, HTMLSelectElement)) { const o = Array.from(e.children).find((i) => t.value === i.getAttribute("value")); o && o.setAttribute("selected", ""); } } -function fy(t, e, n) { - return $e(e, Element) && (cy(t, e, n), Km(t, e, n), dy(t, e), uy(t, e)), e; +function ky(t, e, n) { + return $e(e, Element) && (Ey(t, e, n), uy(t, e, n), Cy(t, e), Sy(t, e)), e; } -async function hy(t, e) { +async function Ly(t, e) { const n = t.querySelectorAll ? t.querySelectorAll("use") : []; if (n.length === 0) return t; @@ -16570,7 +16764,7 @@ async function hy(t, e) { const a = n[r].getAttribute("xlink:href"); if (a) { const l = t.querySelector(a), c = document.querySelector(a); - !l && c && !o[a] && (o[a] = await xo(c, e, !0)); + !l && c && !o[a] && (o[a] = await Co(c, e, !0)); } } const i = Object.values(o); @@ -16585,31 +16779,31 @@ async function hy(t, e) { } return t; } -async function xo(t, e, n) { - return !n && e.filter && !e.filter(t) ? null : Promise.resolve(t).then((o) => ry(o, e)).then((o) => ly(t, o, e)).then((o) => fy(t, o, e)).then((o) => hy(o, e)); +async function Co(t, e, n) { + return !n && e.filter && !e.filter(t) ? null : Promise.resolve(t).then((o) => _y(o, e)).then((o) => xy(t, o, e)).then((o) => ky(t, o, e)).then((o) => Ly(o, e)); } -const Na = /url\((['"]?)([^'"]+?)\1\)/g, gy = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g, py = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g; -function my(t) { +const Ra = /url\((['"]?)([^'"]+?)\1\)/g, Py = /url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g, My = /src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g; +function Ty(t) { const e = t.replace(/([.*+?^${}()|\[\]\/\\])/g, "\\$1"); return new RegExp(`(url\\(['"]?)(${e})(['"]?\\))`, "g"); } -function yy(t) { +function Ny(t) { const e = []; - return t.replace(Na, (n, o, i) => (e.push(i), n)), e.filter((n) => !ci(n)); + return t.replace(Ra, (n, o, i) => (e.push(i), n)), e.filter((n) => !ui(n)); } -async function wy(t, e, n, o, i) { +async function Ay(t, e, n, o, i) { try { - const r = n ? Om(e, n) : e, s = Ni(e); + const r = n ? Qm(e, n) : e, s = Ii(e); let a; - return i || (a = await $i(r, s, o)), t.replace(my(e), `$1${a}$3`); + return i || (a = await Di(r, s, o)), t.replace(Ty(e), `$1${a}$3`); } catch { } return t; } -function vy(t, { preferredFontFormat: e }) { - return e ? t.replace(py, (n) => { +function $y(t, { preferredFontFormat: e }) { + return e ? t.replace(My, (n) => { for (; ; ) { - const [o, , i] = gy.exec(n) || []; + const [o, , i] = Py.exec(n) || []; if (!i) return ""; if (i === e) @@ -16617,32 +16811,32 @@ function vy(t, { preferredFontFormat: e }) { } }) : t; } -function $a(t) { - return t.search(Na) !== -1; +function Ha(t) { + return t.search(Ra) !== -1; } -async function Ia(t, e, n) { - if (!$a(t)) +async function Fa(t, e, n) { + if (!Ha(t)) return t; - const o = vy(t, n); - return yy(o).reduce((r, s) => r.then((a) => wy(a, s, e, n)), Promise.resolve(o)); + const o = $y(t, n); + return Ny(o).reduce((r, s) => r.then((a) => Ay(a, s, e, n)), Promise.resolve(o)); } -async function It(t, e, n) { +async function Rt(t, e, n) { var o; const i = (o = e.style) === null || o === void 0 ? void 0 : o.getPropertyValue(t); if (i) { - const r = await Ia(i, null, n); + const r = await Fa(i, null, n); return e.style.setProperty(t, r, e.style.getPropertyPriority(t)), !0; } return !1; } -async function _y(t, e) { - await It("background", t, e) || await It("background-image", t, e), await It("mask", t, e) || await It("-webkit-mask", t, e) || await It("mask-image", t, e) || await It("-webkit-mask-image", t, e); +async function Iy(t, e) { + await Rt("background", t, e) || await Rt("background-image", t, e), await Rt("mask", t, e) || await Rt("-webkit-mask", t, e) || await Rt("mask-image", t, e) || await Rt("-webkit-mask-image", t, e); } -async function by(t, e) { +async function Dy(t, e) { const n = $e(t, HTMLImageElement); - if (!(n && !ci(t.src)) && !($e(t, SVGImageElement) && !ci(t.href.baseVal))) + if (!(n && !ui(t.src)) && !($e(t, SVGImageElement) && !ui(t.href.baseVal))) return; - const o = n ? t.src : t.href.baseVal, i = await $i(o, Ni(o), e); + const o = n ? t.src : t.href.baseVal, i = await Di(o, Ii(o), e); await new Promise((r, s) => { t.onload = r, t.onerror = e.onImageErrorHandler ? (...l) => { try { @@ -16655,14 +16849,14 @@ async function by(t, e) { a.decode && (a.decode = r), a.loading === "lazy" && (a.loading = "eager"), n ? (t.srcset = "", t.src = i) : t.href.baseVal = i; }); } -async function xy(t, e) { - const o = vt(t.childNodes).map((i) => Da(i, e)); +async function Ry(t, e) { + const o = vt(t.childNodes).map((i) => Oa(i, e)); await Promise.all(o).then(() => t); } -async function Da(t, e) { - $e(t, Element) && (await _y(t, e), await by(t, e), await xy(t, e)); +async function Oa(t, e) { + $e(t, Element) && (await Iy(t, e), await Dy(t, e), await Ry(t, e)); } -function Ey(t, e) { +function Hy(t, e) { const { style: n } = t; e.backgroundColor && (n.backgroundColor = e.backgroundColor), e.width && (n.width = `${e.width}px`), e.height && (n.height = `${e.height}px`); const o = e.style; @@ -16670,23 +16864,23 @@ function Ey(t, e) { n[i] = o[i]; }), t; } -const nr = {}; -async function or(t) { - let e = nr[t]; +const ar = {}; +async function lr(t) { + let e = ar[t]; if (e != null) return e; const o = await (await fetch(t)).text(); - return e = { url: t, cssText: o }, nr[t] = e, e; + return e = { url: t, cssText: o }, ar[t] = e, e; } -async function ir(t, e) { +async function cr(t, e) { let n = t.cssText; const o = /url\(["']?([^"')]+)["']?\)/g, r = (n.match(/url\([^)]+\)/g) || []).map(async (s) => { let a = s.replace(o, "$1"); - return a.startsWith("https://") || (a = new URL(a, t.url).href), Ta(a, e.fetchRequestInit, ({ result: l }) => (n = n.replace(s, `url(${l})`), [s, l])); + return a.startsWith("https://") || (a = new URL(a, t.url).href), Ia(a, e.fetchRequestInit, ({ result: l }) => (n = n.replace(s, `url(${l})`), [s, l])); }); return Promise.all(r).then(() => n); } -function sr(t) { +function dr(t) { if (t == null) return []; const e = [], n = /(\/\*[\s\S]*?\*\/)/gi; @@ -16712,7 +16906,7 @@ function sr(t) { } return e; } -async function Cy(t, e) { +async function Fy(t, e) { const n = [], o = []; return t.forEach((i) => { if ("cssRules" in i) @@ -16720,7 +16914,7 @@ async function Cy(t, e) { vt(i.cssRules || []).forEach((r, s) => { if (r.type === CSSRule.IMPORT_RULE) { let a = s + 1; - const l = r.href, c = or(l).then((d) => ir(d, e)).then((d) => sr(d).forEach((u) => { + const l = r.href, c = lr(l).then((d) => cr(d, e)).then((d) => dr(d).forEach((u) => { try { i.insertRule(u, u.startsWith("@import") ? a += 1 : i.cssRules.length); } catch (f) { @@ -16737,7 +16931,7 @@ async function Cy(t, e) { }); } catch (r) { const s = t.find((a) => a.href == null) || document.styleSheets[0]; - i.href != null && o.push(or(i.href).then((a) => ir(a, e)).then((a) => sr(a).forEach((l) => { + i.href != null && o.push(lr(i.href).then((a) => cr(a, e)).then((a) => dr(a).forEach((l) => { s.insertRule(l, s.cssRules.length); })).catch((a) => { console.error("Error loading remote stylesheet", a); @@ -16754,135 +16948,135 @@ async function Cy(t, e) { } }), n)); } -function Sy(t) { - return t.filter((e) => e.type === CSSRule.FONT_FACE_RULE).filter((e) => $a(e.style.getPropertyValue("src"))); +function Oy(t) { + return t.filter((e) => e.type === CSSRule.FONT_FACE_RULE).filter((e) => Ha(e.style.getPropertyValue("src"))); } -async function ky(t, e) { +async function zy(t, e) { if (t.ownerDocument == null) throw new Error("Provided element is not within a Document"); - const n = vt(t.ownerDocument.styleSheets), o = await Cy(n, e); - return Sy(o); + const n = vt(t.ownerDocument.styleSheets), o = await Fy(n, e); + return Oy(o); } -function Ha(t) { +function za(t) { return t.trim().replace(/["']/g, ""); } -function Ly(t) { +function Vy(t) { const e = /* @__PURE__ */ new Set(); function n(o) { (o.style.fontFamily || getComputedStyle(o).fontFamily).split(",").forEach((r) => { - e.add(Ha(r)); + e.add(za(r)); }), Array.from(o.children).forEach((r) => { r instanceof HTMLElement && n(r); }); } return n(t), e; } -async function Ra(t, e) { - const n = await ky(t, e), o = Ly(t); - return (await Promise.all(n.filter((r) => o.has(Ha(r.style.fontFamily))).map((r) => { +async function Va(t, e) { + const n = await zy(t, e), o = Vy(t); + return (await Promise.all(n.filter((r) => o.has(za(r.style.fontFamily))).map((r) => { const s = r.parentStyleSheet ? r.parentStyleSheet.href : null; - return Ia(r.cssText, s, e); + return Fa(r.cssText, s, e); }))).join(` `); } -async function Py(t, e) { - const n = e.fontEmbedCSS != null ? e.fontEmbedCSS : e.skipFonts ? null : await Ra(t, e); +async function By(t, e) { + const n = e.fontEmbedCSS != null ? e.fontEmbedCSS : e.skipFonts ? null : await Va(t, e); if (n) { const o = document.createElement("style"), i = document.createTextNode(n); o.appendChild(i), t.firstChild ? t.insertBefore(o, t.firstChild) : t.appendChild(o); } } -async function Fa(t, e = {}) { - const { width: n, height: o } = Ai(t, e), i = await xo(t, e, !0); - return await Py(i, e), await Da(i, e), Ey(i, e), await jm(i, n, o); +async function Ba(t, e = {}) { + const { width: n, height: o } = $i(t, e), i = await Co(t, e, !0); + return await By(i, e), await Oa(i, e), Hy(i, e), await ay(i, n, o); } -async function kn(t, e = {}) { - const { width: n, height: o } = Ai(t, e), i = await Fa(t, e), r = await ho(i), s = document.createElement("canvas"), a = s.getContext("2d"), l = e.pixelRatio || qm(), c = e.canvasWidth || n, d = e.canvasHeight || o; - return s.width = c * l, s.height = d * l, e.skipAutoScale || Ym(s), s.style.width = `${c}`, s.style.height = `${d}`, e.backgroundColor && (a.fillStyle = e.backgroundColor, a.fillRect(0, 0, s.width, s.height)), a.drawImage(r, 0, 0, s.width, s.height), s; +async function Pn(t, e = {}) { + const { width: n, height: o } = $i(t, e), i = await Ba(t, e), r = await go(i), s = document.createElement("canvas"), a = s.getContext("2d"), l = e.pixelRatio || oy(), c = e.canvasWidth || n, d = e.canvasHeight || o; + return s.width = c * l, s.height = d * l, e.skipAutoScale || iy(s), s.style.width = `${c}`, s.style.height = `${d}`, e.backgroundColor && (a.fillStyle = e.backgroundColor, a.fillRect(0, 0, s.width, s.height)), a.drawImage(r, 0, 0, s.width, s.height), s; } -async function My(t, e = {}) { - const { width: n, height: o } = Ai(t, e); - return (await kn(t, e)).getContext("2d").getImageData(0, 0, n, o).data; +async function qy(t, e = {}) { + const { width: n, height: o } = $i(t, e); + return (await Pn(t, e)).getContext("2d").getImageData(0, 0, n, o).data; } -async function Ty(t, e = {}) { - return (await kn(t, e)).toDataURL(); +async function Xy(t, e = {}) { + return (await Pn(t, e)).toDataURL(); } -async function Ay(t, e = {}) { - return (await kn(t, e)).toDataURL("image/jpeg", e.quality || 1); +async function Yy(t, e = {}) { + return (await Pn(t, e)).toDataURL("image/jpeg", e.quality || 1); } -async function Ny(t, e = {}) { - const n = await kn(t, e); - return await Xm(n); +async function Wy(t, e = {}) { + const n = await Pn(t, e); + return await sy(n); } -async function $y(t, e = {}) { - return Ra(t, e); +async function jy(t, e = {}) { + return Va(t, e); } -const Iy = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ +const Uy = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, - getFontEmbedCSS: $y, - toBlob: Ny, - toCanvas: kn, - toJpeg: Ay, - toPixelData: My, - toPng: Ty, - toSvg: Fa + getFontEmbedCSS: jy, + toBlob: Wy, + toCanvas: Pn, + toJpeg: Yy, + toPixelData: qy, + toPng: Xy, + toSvg: Ba }, Symbol.toStringTag, { value: "Module" })); export { - Eh as ComputeEngine, - of as FlowHistory, - ms as SHORTCUT_DEFAULTS, - Fy as along, - Nf as areNodesConnected, - la as buildNodeMap, - da as clampToExtent, - Po as clampToParent, - qy as computeRenderPlan, - Ss as computeValidationErrors, - ca as computeZIndex, - Yy as default, - zy as drift, - uh as expandParentToFitChild, - ni as getAbsolutePosition, - Vf as getAutoPanDelta, - oo as getBezierPath, - Mf as getConnectedEdges, + Lh as ComputeEngine, + lf as FlowHistory, + ws as SHORTCUT_DEFAULTS, + Jy as along, + Rf as areNodesConnected, + ha as buildNodeMap, + pa as clampToExtent, + To as clampToParent, + ow as computeRenderPlan, + Ls as computeValidationErrors, + ga as computeZIndex, + iw as default, + ew as drift, + ph as expandParentToFitChild, + ii as getAbsolutePosition, + Yf as getAutoPanDelta, + io as getBezierPath, + $f as getConnectedEdges, wt as getDescendantIds, - Rs as getEdgePosition, - xa as getFloatingEdgeParams, - Tf as getIncomers, - Hs as getNodeIntersection, - Yt as getNodesBounds, - Pf as getNodesFullyInPolygon, - Ku as getNodesFullyInRect, - Lf as getNodesInPolygon, - Zu as getNodesInRect, - Qo as getOutgoers, - Dy as getSimpleBezierPath, - By as getSimpleFloatingPosition, - vn as getSmoothStepPath, - zf as getStepPath, - Zr as getStraightPath, - Jn as getViewportForBounds, - Be as isConnectable, - Rf as isDeletable, - Gr as isDraggable, - fs as isResizable, - ei as isSelectable, + Os as getEdgePosition, + La as getFloatingEdgeParams, + If as getIncomers, + Fs as getNodeIntersection, + Wt as getNodesBounds, + Af as getNodesFullyInPolygon, + tf as getNodesFullyInRect, + Nf as getNodesInPolygon, + ef as getNodesInRect, + ti as getOutgoers, + Gy as getSimpleBezierPath, + nw as getSimpleFloatingPosition, + bn as getSmoothStepPath, + Xf as getStepPath, + ta as getStraightPath, + Qn as getViewportForBounds, + qe as isConnectable, + Vf as isDeletable, + ea as isDraggable, + gs as isResizable, + ni as isSelectable, Ge as matchesKey, mt as matchesModifier, - Hy as orbit, - Oy as pendulum, - Ei as pointInPolygon, - kf as polygonIntersectsAABB, - hf as registerMarker, - un as resolveChildValidation, - Wf as resolveShortcuts, + Zy as orbit, + Qy as pendulum, + Si as pointInPolygon, + Tf as polygonIntersectsAABB, + yf as registerMarker, + hn as resolveChildValidation, + Zf as resolveShortcuts, Mt as sortNodesTopological, - Vy as stagger, + tw as stagger, yt as toAbsoluteNode, - so as toAbsoluteNodes, - ga as validateChildAdd, - ro as validateChildRemove, - Ry as wave + ro as toAbsoluteNodes, + va as validateChildAdd, + ao as validateChildRemove, + Ky as wave }; //# sourceMappingURL=alpineflow.bundle.esm.js.map diff --git a/dist/alpineflow.bundle.esm.js.map b/dist/alpineflow.bundle.esm.js.map index 64d9dfe..f1241cb 100644 --- a/dist/alpineflow.bundle.esm.js.map +++ b/dist/alpineflow.bundle.esm.js.map @@ -1 +1 @@ -{"version":3,"file":"alpineflow.bundle.esm.js","sources":["../src/plugin/alpine-ref.ts","../node_modules/d3-dispatch/src/dispatch.js","../node_modules/d3-selection/src/namespaces.js","../node_modules/d3-selection/src/namespace.js","../node_modules/d3-selection/src/creator.js","../node_modules/d3-selection/src/selector.js","../node_modules/d3-selection/src/selection/select.js","../node_modules/d3-selection/src/array.js","../node_modules/d3-selection/src/selectorAll.js","../node_modules/d3-selection/src/selection/selectAll.js","../node_modules/d3-selection/src/matcher.js","../node_modules/d3-selection/src/selection/selectChild.js","../node_modules/d3-selection/src/selection/selectChildren.js","../node_modules/d3-selection/src/selection/filter.js","../node_modules/d3-selection/src/selection/sparse.js","../node_modules/d3-selection/src/selection/enter.js","../node_modules/d3-selection/src/constant.js","../node_modules/d3-selection/src/selection/data.js","../node_modules/d3-selection/src/selection/exit.js","../node_modules/d3-selection/src/selection/join.js","../node_modules/d3-selection/src/selection/merge.js","../node_modules/d3-selection/src/selection/order.js","../node_modules/d3-selection/src/selection/sort.js","../node_modules/d3-selection/src/selection/call.js","../node_modules/d3-selection/src/selection/nodes.js","../node_modules/d3-selection/src/selection/node.js","../node_modules/d3-selection/src/selection/size.js","../node_modules/d3-selection/src/selection/empty.js","../node_modules/d3-selection/src/selection/each.js","../node_modules/d3-selection/src/selection/attr.js","../node_modules/d3-selection/src/window.js","../node_modules/d3-selection/src/selection/style.js","../node_modules/d3-selection/src/selection/property.js","../node_modules/d3-selection/src/selection/classed.js","../node_modules/d3-selection/src/selection/text.js","../node_modules/d3-selection/src/selection/html.js","../node_modules/d3-selection/src/selection/raise.js","../node_modules/d3-selection/src/selection/lower.js","../node_modules/d3-selection/src/selection/append.js","../node_modules/d3-selection/src/selection/insert.js","../node_modules/d3-selection/src/selection/remove.js","../node_modules/d3-selection/src/selection/clone.js","../node_modules/d3-selection/src/selection/datum.js","../node_modules/d3-selection/src/selection/on.js","../node_modules/d3-selection/src/selection/dispatch.js","../node_modules/d3-selection/src/selection/iterator.js","../node_modules/d3-selection/src/selection/index.js","../node_modules/d3-selection/src/select.js","../node_modules/d3-selection/src/sourceEvent.js","../node_modules/d3-selection/src/pointer.js","../node_modules/d3-drag/src/noevent.js","../node_modules/d3-drag/src/nodrag.js","../node_modules/d3-drag/src/constant.js","../node_modules/d3-drag/src/event.js","../node_modules/d3-drag/src/drag.js","../node_modules/d3-color/src/define.js","../node_modules/d3-color/src/color.js","../node_modules/d3-interpolate/src/constant.js","../node_modules/d3-interpolate/src/color.js","../node_modules/d3-interpolate/src/rgb.js","../node_modules/d3-interpolate/src/number.js","../node_modules/d3-interpolate/src/string.js","../node_modules/d3-interpolate/src/transform/decompose.js","../node_modules/d3-interpolate/src/transform/parse.js","../node_modules/d3-interpolate/src/transform/index.js","../node_modules/d3-interpolate/src/zoom.js","../node_modules/d3-timer/src/timer.js","../node_modules/d3-timer/src/timeout.js","../node_modules/d3-transition/src/transition/schedule.js","../node_modules/d3-transition/src/interrupt.js","../node_modules/d3-transition/src/selection/interrupt.js","../node_modules/d3-transition/src/transition/tween.js","../node_modules/d3-transition/src/transition/interpolate.js","../node_modules/d3-transition/src/transition/attr.js","../node_modules/d3-transition/src/transition/attrTween.js","../node_modules/d3-transition/src/transition/delay.js","../node_modules/d3-transition/src/transition/duration.js","../node_modules/d3-transition/src/transition/ease.js","../node_modules/d3-transition/src/transition/easeVarying.js","../node_modules/d3-transition/src/transition/filter.js","../node_modules/d3-transition/src/transition/merge.js","../node_modules/d3-transition/src/transition/on.js","../node_modules/d3-transition/src/transition/remove.js","../node_modules/d3-transition/src/transition/select.js","../node_modules/d3-transition/src/transition/selectAll.js","../node_modules/d3-transition/src/transition/selection.js","../node_modules/d3-transition/src/transition/style.js","../node_modules/d3-transition/src/transition/styleTween.js","../node_modules/d3-transition/src/transition/text.js","../node_modules/d3-transition/src/transition/textTween.js","../node_modules/d3-transition/src/transition/transition.js","../node_modules/d3-transition/src/transition/end.js","../node_modules/d3-transition/src/transition/index.js","../node_modules/d3-ease/src/linear.js","../node_modules/d3-ease/src/quad.js","../node_modules/d3-ease/src/cubic.js","../node_modules/d3-ease/src/sin.js","../node_modules/d3-ease/src/math.js","../node_modules/d3-ease/src/exp.js","../node_modules/d3-ease/src/circle.js","../node_modules/d3-ease/src/bounce.js","../node_modules/d3-ease/src/back.js","../node_modules/d3-ease/src/elastic.js","../node_modules/d3-transition/src/selection/transition.js","../node_modules/d3-transition/src/selection/index.js","../node_modules/d3-zoom/src/constant.js","../node_modules/d3-zoom/src/event.js","../node_modules/d3-zoom/src/transform.js","../node_modules/d3-zoom/src/noevent.js","../node_modules/d3-zoom/src/zoom.js","../src/core/pan-zoom.ts","../src/core/geometry.ts","../src/core/debug.ts","../src/core/constants.ts","../src/core/history.ts","../src/animate/engine.ts","../src/animate/easing.ts","../src/animate/interpolators.ts","../src/animate/handle-registry.ts","../src/animate/transaction.ts","../src/animate/motion/spring.ts","../src/animate/motion/decay.ts","../src/animate/motion/axis.ts","../src/animate/motion/inertia.ts","../src/animate/motion/keyframes.ts","../src/animate/motion/presets.ts","../src/animate/animator.ts","../src/core/markers.ts","../src/core/minimap.ts","../src/core/controls-panel.ts","../src/core/selection-box.ts","../src/core/lasso.ts","../src/core/lasso-hit-test.ts","../src/core/graph.ts","../src/core/connections.ts","../src/plugin/directives/flow-handle-validate.ts","../src/plugin/directives/flow-handle-limit.ts","../src/plugin/directives/flow-handle-connectable.ts","../src/core/node-flags.ts","../src/core/edge-paths/utils.ts","../src/core/edge-paths/bezier.ts","../src/core/edge-paths/smoothstep.ts","../src/core/edge-paths/straight.ts","../src/core/auto-pan.ts","../src/plugin/connection-utils.ts","../src/plugin/drag-validation.ts","../src/plugin/handle-index.ts","../src/plugin/directives/flow-handle.ts","../src/plugin/handle-delegation.ts","../src/core/keyboard-shortcuts.ts","../src/core/long-press.ts","../src/core/edge-paths/orthogonal.ts","../src/core/endpoint-spread.ts","../src/core/sub-flow.ts","../src/core/shapes.ts","../src/core/color-mode.ts","../src/collab/store.ts","../src/core/registry.ts","../src/core/announcer.ts","../src/core/compute.ts","../src/core/wire-bridge.ts","../src/plugin/data/canvas-layout-dedup.ts","../src/plugin/data/canvas-batch.ts","../src/animate/clamp-dimensions.ts","../src/core/child-validation.ts","../src/core/intersection.ts","../src/plugin/data/canvas-nodes.ts","../src/plugin/data/canvas-edges.ts","../src/plugin/data/canvas-viewport.ts","../src/core/clipboard.ts","../src/plugin/data/canvas-selection.ts","../src/core/deep-merge.ts","../src/plugin/data/canvas-history.ts","../src/animate/edge-transitions.ts","../src/animate/paths.ts","../src/animate/clone.ts","../src/animate/timeline.ts","../src/animate/particle-renderers.ts","../src/plugin/data/canvas-particles.ts","../src/animate/flow-group.ts","../src/animate/recording/types.ts","../src/animate/recording/virtual-engine.ts","../src/animate/recording/thumbnail.ts","../src/animate/recording/event-cursor.ts","../src/animate/recording/recording.ts","../src/animate/recording/recorder.ts","../src/animate/recording/replay.ts","../src/plugin/data/canvas-animation.ts","../src/core/collapse.ts","../src/plugin/data/canvas-collapse.ts","../src/plugin/data/canvas-condense.ts","../src/plugin/data/canvas-rows.ts","../src/core/child-layout.ts","../src/plugin/data/canvas-layout.ts","../src/plugin/data/canvas-validation.ts","../src/plugin/data/canvas-compute.ts","../src/core/rotation.ts","../src/core/edge-paths/avoidant.ts","../src/core/edge-paths/editable.ts","../src/core/edge-utils.ts","../src/core/floating-edge.ts","../src/core/gradients.ts","../src/core/schema-geometry.ts","../src/plugin/directives/flow-edge.ts","../src/plugin/data/canvas-dom.ts","../src/plugin/data/canvas-config.ts","../src/plugin/data/flow-canvas.ts","../src/core/drag.ts","../src/core/helper-lines.ts","../src/core/proximity-connect.ts","../src/plugin/directives/flow-node.ts","../src/core/resize.ts","../src/plugin/directives/flow-resizer.ts","../src/plugin/directives/flow-rotate.ts","../src/plugin/directives/flow-drag-handle.ts","../src/plugin/directives/flow-draggable.ts","../src/core/bidirectional-edges.ts","../src/plugin/directives/flow-viewport.ts","../src/plugin/directives/flow-panel.ts","../src/plugin/directives/flow-node-toolbar.ts","../src/plugin/directives/flow-context-menu.ts","../src/plugin/directives/flow-animate.ts","../src/plugin/directives/flow-timeline.ts","../src/plugin/directives/flow-collapse.ts","../src/plugin/directives/flow-condense.ts","../src/plugin/directives/flow-schema.ts","../src/plugin/directives/flow-wait.ts","../src/workflow/condition-pretty-print.ts","../src/plugin/directives/flow-condition.ts","../src/plugin/directives/flow-row-select.ts","../src/plugin/directives/flow-detail.ts","../src/plugin/directives/flow-devtools.ts","../src/plugin/directives/flow-action.ts","../src/plugin/directives/flow-filter.ts","../src/plugin/directives/flow-follow.ts","../src/plugin/directives/flow-snapshot.ts","../src/plugin/directives/flow-loading.ts","../src/plugin/directives/flow-edge-toolbar.ts","../src/plugin/magics/flow.ts","../src/plugin/store.ts","../src/core/render-plan.ts","../src/index.ts","../src/core/export.ts","../node_modules/html-to-image/es/util.js","../node_modules/html-to-image/es/clone-pseudos.js","../node_modules/html-to-image/es/mimes.js","../node_modules/html-to-image/es/dataurl.js","../node_modules/html-to-image/es/clone-node.js","../node_modules/html-to-image/es/embed-resources.js","../node_modules/html-to-image/es/embed-images.js","../node_modules/html-to-image/es/apply-style.js","../node_modules/html-to-image/es/embed-webfonts.js","../node_modules/html-to-image/es/index.js"],"sourcesContent":["// ============================================================================\n// Alpine Reference\n//\n// Stores the Alpine instance passed during plugin registration so internal\n// modules can access it without importing 'alpinejs' directly. This allows\n// the bundle to work in environments where Alpine is provided externally\n// (e.g. Livewire).\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nlet _alpine: Alpine | null = null;\n\nexport function setAlpine(alpine: Alpine): void {\n _alpine = alpine;\n}\n\nexport function getAlpine(): Alpine {\n if (!_alpine) {\n throw new Error('[AlpineFlow] Alpine not initialized. Ensure Alpine.plugin(AlpineFlow) was called.');\n }\n return _alpine;\n}\n","var noop = {value: () => {}};\n\nfunction dispatch() {\n for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) {\n if (!(t = arguments[i] + \"\") || (t in _) || /[\\s.]/.test(t)) throw new Error(\"illegal type: \" + t);\n _[t] = [];\n }\n return new Dispatch(_);\n}\n\nfunction Dispatch(_) {\n this._ = _;\n}\n\nfunction parseTypenames(typenames, types) {\n return typenames.trim().split(/^|\\s+/).map(function(t) {\n var name = \"\", i = t.indexOf(\".\");\n if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);\n if (t && !types.hasOwnProperty(t)) throw new Error(\"unknown type: \" + t);\n return {type: t, name: name};\n });\n}\n\nDispatch.prototype = dispatch.prototype = {\n constructor: Dispatch,\n on: function(typename, callback) {\n var _ = this._,\n T = parseTypenames(typename + \"\", _),\n t,\n i = -1,\n n = T.length;\n\n // If no callback was specified, return the callback of the given type and name.\n if (arguments.length < 2) {\n while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t;\n return;\n }\n\n // If a type was specified, set the callback for the given type and name.\n // Otherwise, if a null callback was specified, remove callbacks of the given name.\n if (callback != null && typeof callback !== \"function\") throw new Error(\"invalid callback: \" + callback);\n while (++i < n) {\n if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback);\n else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null);\n }\n\n return this;\n },\n copy: function() {\n var copy = {}, _ = this._;\n for (var t in _) copy[t] = _[t].slice();\n return new Dispatch(copy);\n },\n call: function(type, that) {\n if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2];\n if (!this._.hasOwnProperty(type)) throw new Error(\"unknown type: \" + type);\n for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);\n },\n apply: function(type, that, args) {\n if (!this._.hasOwnProperty(type)) throw new Error(\"unknown type: \" + type);\n for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args);\n }\n};\n\nfunction get(type, name) {\n for (var i = 0, n = type.length, c; i < n; ++i) {\n if ((c = type[i]).name === name) {\n return c.value;\n }\n }\n}\n\nfunction set(type, name, callback) {\n for (var i = 0, n = type.length; i < n; ++i) {\n if (type[i].name === name) {\n type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1));\n break;\n }\n }\n if (callback != null) type.push({name: name, value: callback});\n return type;\n}\n\nexport default dispatch;\n","export var xhtml = \"http://www.w3.org/1999/xhtml\";\n\nexport default {\n svg: \"http://www.w3.org/2000/svg\",\n xhtml: xhtml,\n xlink: \"http://www.w3.org/1999/xlink\",\n xml: \"http://www.w3.org/XML/1998/namespace\",\n xmlns: \"http://www.w3.org/2000/xmlns/\"\n};\n","import namespaces from \"./namespaces.js\";\n\nexport default function(name) {\n var prefix = name += \"\", i = prefix.indexOf(\":\");\n if (i >= 0 && (prefix = name.slice(0, i)) !== \"xmlns\") name = name.slice(i + 1);\n return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name; // eslint-disable-line no-prototype-builtins\n}\n","import namespace from \"./namespace.js\";\nimport {xhtml} from \"./namespaces.js\";\n\nfunction creatorInherit(name) {\n return function() {\n var document = this.ownerDocument,\n uri = this.namespaceURI;\n return uri === xhtml && document.documentElement.namespaceURI === xhtml\n ? document.createElement(name)\n : document.createElementNS(uri, name);\n };\n}\n\nfunction creatorFixed(fullname) {\n return function() {\n return this.ownerDocument.createElementNS(fullname.space, fullname.local);\n };\n}\n\nexport default function(name) {\n var fullname = namespace(name);\n return (fullname.local\n ? creatorFixed\n : creatorInherit)(fullname);\n}\n","function none() {}\n\nexport default function(selector) {\n return selector == null ? none : function() {\n return this.querySelector(selector);\n };\n}\n","import {Selection} from \"./index.js\";\nimport selector from \"../selector.js\";\n\nexport default function(select) {\n if (typeof select !== \"function\") select = selector(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {\n if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {\n if (\"__data__\" in node) subnode.__data__ = node.__data__;\n subgroup[i] = subnode;\n }\n }\n }\n\n return new Selection(subgroups, this._parents);\n}\n","// Given something array like (or null), returns something that is strictly an\n// array. This is used to ensure that array-like objects passed to d3.selectAll\n// or selection.selectAll are converted into proper arrays when creating a\n// selection; we don’t ever want to create a selection backed by a live\n// HTMLCollection or NodeList. However, note that selection.selectAll will use a\n// static NodeList as a group, since it safely derived from querySelectorAll.\nexport default function array(x) {\n return x == null ? [] : Array.isArray(x) ? x : Array.from(x);\n}\n","function empty() {\n return [];\n}\n\nexport default function(selector) {\n return selector == null ? empty : function() {\n return this.querySelectorAll(selector);\n };\n}\n","import {Selection} from \"./index.js\";\nimport array from \"../array.js\";\nimport selectorAll from \"../selectorAll.js\";\n\nfunction arrayAll(select) {\n return function() {\n return array(select.apply(this, arguments));\n };\n}\n\nexport default function(select) {\n if (typeof select === \"function\") select = arrayAll(select);\n else select = selectorAll(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n subgroups.push(select.call(node, node.__data__, i, group));\n parents.push(node);\n }\n }\n }\n\n return new Selection(subgroups, parents);\n}\n","export default function(selector) {\n return function() {\n return this.matches(selector);\n };\n}\n\nexport function childMatcher(selector) {\n return function(node) {\n return node.matches(selector);\n };\n}\n\n","import {childMatcher} from \"../matcher.js\";\n\nvar find = Array.prototype.find;\n\nfunction childFind(match) {\n return function() {\n return find.call(this.children, match);\n };\n}\n\nfunction childFirst() {\n return this.firstElementChild;\n}\n\nexport default function(match) {\n return this.select(match == null ? childFirst\n : childFind(typeof match === \"function\" ? match : childMatcher(match)));\n}\n","import {childMatcher} from \"../matcher.js\";\n\nvar filter = Array.prototype.filter;\n\nfunction children() {\n return Array.from(this.children);\n}\n\nfunction childrenFilter(match) {\n return function() {\n return filter.call(this.children, match);\n };\n}\n\nexport default function(match) {\n return this.selectAll(match == null ? children\n : childrenFilter(typeof match === \"function\" ? match : childMatcher(match)));\n}\n","import {Selection} from \"./index.js\";\nimport matcher from \"../matcher.js\";\n\nexport default function(match) {\n if (typeof match !== \"function\") match = matcher(match);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {\n if ((node = group[i]) && match.call(node, node.__data__, i, group)) {\n subgroup.push(node);\n }\n }\n }\n\n return new Selection(subgroups, this._parents);\n}\n","export default function(update) {\n return new Array(update.length);\n}\n","import sparse from \"./sparse.js\";\nimport {Selection} from \"./index.js\";\n\nexport default function() {\n return new Selection(this._enter || this._groups.map(sparse), this._parents);\n}\n\nexport function EnterNode(parent, datum) {\n this.ownerDocument = parent.ownerDocument;\n this.namespaceURI = parent.namespaceURI;\n this._next = null;\n this._parent = parent;\n this.__data__ = datum;\n}\n\nEnterNode.prototype = {\n constructor: EnterNode,\n appendChild: function(child) { return this._parent.insertBefore(child, this._next); },\n insertBefore: function(child, next) { return this._parent.insertBefore(child, next); },\n querySelector: function(selector) { return this._parent.querySelector(selector); },\n querySelectorAll: function(selector) { return this._parent.querySelectorAll(selector); }\n};\n","export default function(x) {\n return function() {\n return x;\n };\n}\n","import {Selection} from \"./index.js\";\nimport {EnterNode} from \"./enter.js\";\nimport constant from \"../constant.js\";\n\nfunction bindIndex(parent, group, enter, update, exit, data) {\n var i = 0,\n node,\n groupLength = group.length,\n dataLength = data.length;\n\n // Put any non-null nodes that fit into update.\n // Put any null nodes into enter.\n // Put any remaining data into enter.\n for (; i < dataLength; ++i) {\n if (node = group[i]) {\n node.__data__ = data[i];\n update[i] = node;\n } else {\n enter[i] = new EnterNode(parent, data[i]);\n }\n }\n\n // Put any non-null nodes that don’t fit into exit.\n for (; i < groupLength; ++i) {\n if (node = group[i]) {\n exit[i] = node;\n }\n }\n}\n\nfunction bindKey(parent, group, enter, update, exit, data, key) {\n var i,\n node,\n nodeByKeyValue = new Map,\n groupLength = group.length,\n dataLength = data.length,\n keyValues = new Array(groupLength),\n keyValue;\n\n // Compute the key for each node.\n // If multiple nodes have the same key, the duplicates are added to exit.\n for (i = 0; i < groupLength; ++i) {\n if (node = group[i]) {\n keyValues[i] = keyValue = key.call(node, node.__data__, i, group) + \"\";\n if (nodeByKeyValue.has(keyValue)) {\n exit[i] = node;\n } else {\n nodeByKeyValue.set(keyValue, node);\n }\n }\n }\n\n // Compute the key for each datum.\n // If there a node associated with this key, join and add it to update.\n // If there is not (or the key is a duplicate), add it to enter.\n for (i = 0; i < dataLength; ++i) {\n keyValue = key.call(parent, data[i], i, data) + \"\";\n if (node = nodeByKeyValue.get(keyValue)) {\n update[i] = node;\n node.__data__ = data[i];\n nodeByKeyValue.delete(keyValue);\n } else {\n enter[i] = new EnterNode(parent, data[i]);\n }\n }\n\n // Add any remaining nodes that were not bound to data to exit.\n for (i = 0; i < groupLength; ++i) {\n if ((node = group[i]) && (nodeByKeyValue.get(keyValues[i]) === node)) {\n exit[i] = node;\n }\n }\n}\n\nfunction datum(node) {\n return node.__data__;\n}\n\nexport default function(value, key) {\n if (!arguments.length) return Array.from(this, datum);\n\n var bind = key ? bindKey : bindIndex,\n parents = this._parents,\n groups = this._groups;\n\n if (typeof value !== \"function\") value = constant(value);\n\n for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {\n var parent = parents[j],\n group = groups[j],\n groupLength = group.length,\n data = arraylike(value.call(parent, parent && parent.__data__, j, parents)),\n dataLength = data.length,\n enterGroup = enter[j] = new Array(dataLength),\n updateGroup = update[j] = new Array(dataLength),\n exitGroup = exit[j] = new Array(groupLength);\n\n bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);\n\n // Now connect the enter nodes to their following update node, such that\n // appendChild can insert the materialized enter node before this node,\n // rather than at the end of the parent node.\n for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {\n if (previous = enterGroup[i0]) {\n if (i0 >= i1) i1 = i0 + 1;\n while (!(next = updateGroup[i1]) && ++i1 < dataLength);\n previous._next = next || null;\n }\n }\n }\n\n update = new Selection(update, parents);\n update._enter = enter;\n update._exit = exit;\n return update;\n}\n\n// Given some data, this returns an array-like view of it: an object that\n// exposes a length property and allows numeric indexing. Note that unlike\n// selectAll, this isn’t worried about “live” collections because the resulting\n// array will only be used briefly while data is being bound. (It is possible to\n// cause the data to change while iterating by using a key function, but please\n// don’t; we’d rather avoid a gratuitous copy.)\nfunction arraylike(data) {\n return typeof data === \"object\" && \"length\" in data\n ? data // Array, TypedArray, NodeList, array-like\n : Array.from(data); // Map, Set, iterable, string, or anything else\n}\n","import sparse from \"./sparse.js\";\nimport {Selection} from \"./index.js\";\n\nexport default function() {\n return new Selection(this._exit || this._groups.map(sparse), this._parents);\n}\n","export default function(onenter, onupdate, onexit) {\n var enter = this.enter(), update = this, exit = this.exit();\n if (typeof onenter === \"function\") {\n enter = onenter(enter);\n if (enter) enter = enter.selection();\n } else {\n enter = enter.append(onenter + \"\");\n }\n if (onupdate != null) {\n update = onupdate(update);\n if (update) update = update.selection();\n }\n if (onexit == null) exit.remove(); else onexit(exit);\n return enter && update ? enter.merge(update).order() : update;\n}\n","import {Selection} from \"./index.js\";\n\nexport default function(context) {\n var selection = context.selection ? context.selection() : context;\n\n for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {\n for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {\n if (node = group0[i] || group1[i]) {\n merge[i] = node;\n }\n }\n }\n\n for (; j < m0; ++j) {\n merges[j] = groups0[j];\n }\n\n return new Selection(merges, this._parents);\n}\n","export default function() {\n\n for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {\n for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {\n if (node = group[i]) {\n if (next && node.compareDocumentPosition(next) ^ 4) next.parentNode.insertBefore(node, next);\n next = node;\n }\n }\n }\n\n return this;\n}\n","import {Selection} from \"./index.js\";\n\nexport default function(compare) {\n if (!compare) compare = ascending;\n\n function compareNode(a, b) {\n return a && b ? compare(a.__data__, b.__data__) : !a - !b;\n }\n\n for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n sortgroup[i] = node;\n }\n }\n sortgroup.sort(compareNode);\n }\n\n return new Selection(sortgroups, this._parents).order();\n}\n\nfunction ascending(a, b) {\n return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;\n}\n","export default function() {\n var callback = arguments[0];\n arguments[0] = this;\n callback.apply(null, arguments);\n return this;\n}\n","export default function() {\n return Array.from(this);\n}\n","export default function() {\n\n for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {\n for (var group = groups[j], i = 0, n = group.length; i < n; ++i) {\n var node = group[i];\n if (node) return node;\n }\n }\n\n return null;\n}\n","export default function() {\n let size = 0;\n for (const node of this) ++size; // eslint-disable-line no-unused-vars\n return size;\n}\n","export default function() {\n return !this.node();\n}\n","export default function(callback) {\n\n for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {\n for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {\n if (node = group[i]) callback.call(node, node.__data__, i, group);\n }\n }\n\n return this;\n}\n","import namespace from \"../namespace.js\";\n\nfunction attrRemove(name) {\n return function() {\n this.removeAttribute(name);\n };\n}\n\nfunction attrRemoveNS(fullname) {\n return function() {\n this.removeAttributeNS(fullname.space, fullname.local);\n };\n}\n\nfunction attrConstant(name, value) {\n return function() {\n this.setAttribute(name, value);\n };\n}\n\nfunction attrConstantNS(fullname, value) {\n return function() {\n this.setAttributeNS(fullname.space, fullname.local, value);\n };\n}\n\nfunction attrFunction(name, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) this.removeAttribute(name);\n else this.setAttribute(name, v);\n };\n}\n\nfunction attrFunctionNS(fullname, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) this.removeAttributeNS(fullname.space, fullname.local);\n else this.setAttributeNS(fullname.space, fullname.local, v);\n };\n}\n\nexport default function(name, value) {\n var fullname = namespace(name);\n\n if (arguments.length < 2) {\n var node = this.node();\n return fullname.local\n ? node.getAttributeNS(fullname.space, fullname.local)\n : node.getAttribute(fullname);\n }\n\n return this.each((value == null\n ? (fullname.local ? attrRemoveNS : attrRemove) : (typeof value === \"function\"\n ? (fullname.local ? attrFunctionNS : attrFunction)\n : (fullname.local ? attrConstantNS : attrConstant)))(fullname, value));\n}\n","export default function(node) {\n return (node.ownerDocument && node.ownerDocument.defaultView) // node is a Node\n || (node.document && node) // node is a Window\n || node.defaultView; // node is a Document\n}\n","import defaultView from \"../window.js\";\n\nfunction styleRemove(name) {\n return function() {\n this.style.removeProperty(name);\n };\n}\n\nfunction styleConstant(name, value, priority) {\n return function() {\n this.style.setProperty(name, value, priority);\n };\n}\n\nfunction styleFunction(name, value, priority) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) this.style.removeProperty(name);\n else this.style.setProperty(name, v, priority);\n };\n}\n\nexport default function(name, value, priority) {\n return arguments.length > 1\n ? this.each((value == null\n ? styleRemove : typeof value === \"function\"\n ? styleFunction\n : styleConstant)(name, value, priority == null ? \"\" : priority))\n : styleValue(this.node(), name);\n}\n\nexport function styleValue(node, name) {\n return node.style.getPropertyValue(name)\n || defaultView(node).getComputedStyle(node, null).getPropertyValue(name);\n}\n","function propertyRemove(name) {\n return function() {\n delete this[name];\n };\n}\n\nfunction propertyConstant(name, value) {\n return function() {\n this[name] = value;\n };\n}\n\nfunction propertyFunction(name, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (v == null) delete this[name];\n else this[name] = v;\n };\n}\n\nexport default function(name, value) {\n return arguments.length > 1\n ? this.each((value == null\n ? propertyRemove : typeof value === \"function\"\n ? propertyFunction\n : propertyConstant)(name, value))\n : this.node()[name];\n}\n","function classArray(string) {\n return string.trim().split(/^|\\s+/);\n}\n\nfunction classList(node) {\n return node.classList || new ClassList(node);\n}\n\nfunction ClassList(node) {\n this._node = node;\n this._names = classArray(node.getAttribute(\"class\") || \"\");\n}\n\nClassList.prototype = {\n add: function(name) {\n var i = this._names.indexOf(name);\n if (i < 0) {\n this._names.push(name);\n this._node.setAttribute(\"class\", this._names.join(\" \"));\n }\n },\n remove: function(name) {\n var i = this._names.indexOf(name);\n if (i >= 0) {\n this._names.splice(i, 1);\n this._node.setAttribute(\"class\", this._names.join(\" \"));\n }\n },\n contains: function(name) {\n return this._names.indexOf(name) >= 0;\n }\n};\n\nfunction classedAdd(node, names) {\n var list = classList(node), i = -1, n = names.length;\n while (++i < n) list.add(names[i]);\n}\n\nfunction classedRemove(node, names) {\n var list = classList(node), i = -1, n = names.length;\n while (++i < n) list.remove(names[i]);\n}\n\nfunction classedTrue(names) {\n return function() {\n classedAdd(this, names);\n };\n}\n\nfunction classedFalse(names) {\n return function() {\n classedRemove(this, names);\n };\n}\n\nfunction classedFunction(names, value) {\n return function() {\n (value.apply(this, arguments) ? classedAdd : classedRemove)(this, names);\n };\n}\n\nexport default function(name, value) {\n var names = classArray(name + \"\");\n\n if (arguments.length < 2) {\n var list = classList(this.node()), i = -1, n = names.length;\n while (++i < n) if (!list.contains(names[i])) return false;\n return true;\n }\n\n return this.each((typeof value === \"function\"\n ? classedFunction : value\n ? classedTrue\n : classedFalse)(names, value));\n}\n","function textRemove() {\n this.textContent = \"\";\n}\n\nfunction textConstant(value) {\n return function() {\n this.textContent = value;\n };\n}\n\nfunction textFunction(value) {\n return function() {\n var v = value.apply(this, arguments);\n this.textContent = v == null ? \"\" : v;\n };\n}\n\nexport default function(value) {\n return arguments.length\n ? this.each(value == null\n ? textRemove : (typeof value === \"function\"\n ? textFunction\n : textConstant)(value))\n : this.node().textContent;\n}\n","function htmlRemove() {\n this.innerHTML = \"\";\n}\n\nfunction htmlConstant(value) {\n return function() {\n this.innerHTML = value;\n };\n}\n\nfunction htmlFunction(value) {\n return function() {\n var v = value.apply(this, arguments);\n this.innerHTML = v == null ? \"\" : v;\n };\n}\n\nexport default function(value) {\n return arguments.length\n ? this.each(value == null\n ? htmlRemove : (typeof value === \"function\"\n ? htmlFunction\n : htmlConstant)(value))\n : this.node().innerHTML;\n}\n","function raise() {\n if (this.nextSibling) this.parentNode.appendChild(this);\n}\n\nexport default function() {\n return this.each(raise);\n}\n","function lower() {\n if (this.previousSibling) this.parentNode.insertBefore(this, this.parentNode.firstChild);\n}\n\nexport default function() {\n return this.each(lower);\n}\n","import creator from \"../creator.js\";\n\nexport default function(name) {\n var create = typeof name === \"function\" ? name : creator(name);\n return this.select(function() {\n return this.appendChild(create.apply(this, arguments));\n });\n}\n","import creator from \"../creator.js\";\nimport selector from \"../selector.js\";\n\nfunction constantNull() {\n return null;\n}\n\nexport default function(name, before) {\n var create = typeof name === \"function\" ? name : creator(name),\n select = before == null ? constantNull : typeof before === \"function\" ? before : selector(before);\n return this.select(function() {\n return this.insertBefore(create.apply(this, arguments), select.apply(this, arguments) || null);\n });\n}\n","function remove() {\n var parent = this.parentNode;\n if (parent) parent.removeChild(this);\n}\n\nexport default function() {\n return this.each(remove);\n}\n","function selection_cloneShallow() {\n var clone = this.cloneNode(false), parent = this.parentNode;\n return parent ? parent.insertBefore(clone, this.nextSibling) : clone;\n}\n\nfunction selection_cloneDeep() {\n var clone = this.cloneNode(true), parent = this.parentNode;\n return parent ? parent.insertBefore(clone, this.nextSibling) : clone;\n}\n\nexport default function(deep) {\n return this.select(deep ? selection_cloneDeep : selection_cloneShallow);\n}\n","export default function(value) {\n return arguments.length\n ? this.property(\"__data__\", value)\n : this.node().__data__;\n}\n","function contextListener(listener) {\n return function(event) {\n listener.call(this, event, this.__data__);\n };\n}\n\nfunction parseTypenames(typenames) {\n return typenames.trim().split(/^|\\s+/).map(function(t) {\n var name = \"\", i = t.indexOf(\".\");\n if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);\n return {type: t, name: name};\n });\n}\n\nfunction onRemove(typename) {\n return function() {\n var on = this.__on;\n if (!on) return;\n for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {\n if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {\n this.removeEventListener(o.type, o.listener, o.options);\n } else {\n on[++i] = o;\n }\n }\n if (++i) on.length = i;\n else delete this.__on;\n };\n}\n\nfunction onAdd(typename, value, options) {\n return function() {\n var on = this.__on, o, listener = contextListener(value);\n if (on) for (var j = 0, m = on.length; j < m; ++j) {\n if ((o = on[j]).type === typename.type && o.name === typename.name) {\n this.removeEventListener(o.type, o.listener, o.options);\n this.addEventListener(o.type, o.listener = listener, o.options = options);\n o.value = value;\n return;\n }\n }\n this.addEventListener(typename.type, listener, options);\n o = {type: typename.type, name: typename.name, value: value, listener: listener, options: options};\n if (!on) this.__on = [o];\n else on.push(o);\n };\n}\n\nexport default function(typename, value, options) {\n var typenames = parseTypenames(typename + \"\"), i, n = typenames.length, t;\n\n if (arguments.length < 2) {\n var on = this.node().__on;\n if (on) for (var j = 0, m = on.length, o; j < m; ++j) {\n for (i = 0, o = on[j]; i < n; ++i) {\n if ((t = typenames[i]).type === o.type && t.name === o.name) {\n return o.value;\n }\n }\n }\n return;\n }\n\n on = value ? onAdd : onRemove;\n for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options));\n return this;\n}\n","import defaultView from \"../window.js\";\n\nfunction dispatchEvent(node, type, params) {\n var window = defaultView(node),\n event = window.CustomEvent;\n\n if (typeof event === \"function\") {\n event = new event(type, params);\n } else {\n event = window.document.createEvent(\"Event\");\n if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;\n else event.initEvent(type, false, false);\n }\n\n node.dispatchEvent(event);\n}\n\nfunction dispatchConstant(type, params) {\n return function() {\n return dispatchEvent(this, type, params);\n };\n}\n\nfunction dispatchFunction(type, params) {\n return function() {\n return dispatchEvent(this, type, params.apply(this, arguments));\n };\n}\n\nexport default function(type, params) {\n return this.each((typeof params === \"function\"\n ? dispatchFunction\n : dispatchConstant)(type, params));\n}\n","export default function*() {\n for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {\n for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {\n if (node = group[i]) yield node;\n }\n }\n}\n","import selection_select from \"./select.js\";\nimport selection_selectAll from \"./selectAll.js\";\nimport selection_selectChild from \"./selectChild.js\";\nimport selection_selectChildren from \"./selectChildren.js\";\nimport selection_filter from \"./filter.js\";\nimport selection_data from \"./data.js\";\nimport selection_enter from \"./enter.js\";\nimport selection_exit from \"./exit.js\";\nimport selection_join from \"./join.js\";\nimport selection_merge from \"./merge.js\";\nimport selection_order from \"./order.js\";\nimport selection_sort from \"./sort.js\";\nimport selection_call from \"./call.js\";\nimport selection_nodes from \"./nodes.js\";\nimport selection_node from \"./node.js\";\nimport selection_size from \"./size.js\";\nimport selection_empty from \"./empty.js\";\nimport selection_each from \"./each.js\";\nimport selection_attr from \"./attr.js\";\nimport selection_style from \"./style.js\";\nimport selection_property from \"./property.js\";\nimport selection_classed from \"./classed.js\";\nimport selection_text from \"./text.js\";\nimport selection_html from \"./html.js\";\nimport selection_raise from \"./raise.js\";\nimport selection_lower from \"./lower.js\";\nimport selection_append from \"./append.js\";\nimport selection_insert from \"./insert.js\";\nimport selection_remove from \"./remove.js\";\nimport selection_clone from \"./clone.js\";\nimport selection_datum from \"./datum.js\";\nimport selection_on from \"./on.js\";\nimport selection_dispatch from \"./dispatch.js\";\nimport selection_iterator from \"./iterator.js\";\n\nexport var root = [null];\n\nexport function Selection(groups, parents) {\n this._groups = groups;\n this._parents = parents;\n}\n\nfunction selection() {\n return new Selection([[document.documentElement]], root);\n}\n\nfunction selection_selection() {\n return this;\n}\n\nSelection.prototype = selection.prototype = {\n constructor: Selection,\n select: selection_select,\n selectAll: selection_selectAll,\n selectChild: selection_selectChild,\n selectChildren: selection_selectChildren,\n filter: selection_filter,\n data: selection_data,\n enter: selection_enter,\n exit: selection_exit,\n join: selection_join,\n merge: selection_merge,\n selection: selection_selection,\n order: selection_order,\n sort: selection_sort,\n call: selection_call,\n nodes: selection_nodes,\n node: selection_node,\n size: selection_size,\n empty: selection_empty,\n each: selection_each,\n attr: selection_attr,\n style: selection_style,\n property: selection_property,\n classed: selection_classed,\n text: selection_text,\n html: selection_html,\n raise: selection_raise,\n lower: selection_lower,\n append: selection_append,\n insert: selection_insert,\n remove: selection_remove,\n clone: selection_clone,\n datum: selection_datum,\n on: selection_on,\n dispatch: selection_dispatch,\n [Symbol.iterator]: selection_iterator\n};\n\nexport default selection;\n","import {Selection, root} from \"./selection/index.js\";\n\nexport default function(selector) {\n return typeof selector === \"string\"\n ? new Selection([[document.querySelector(selector)]], [document.documentElement])\n : new Selection([[selector]], root);\n}\n","export default function(event) {\n let sourceEvent;\n while (sourceEvent = event.sourceEvent) event = sourceEvent;\n return event;\n}\n","import sourceEvent from \"./sourceEvent.js\";\n\nexport default function(event, node) {\n event = sourceEvent(event);\n if (node === undefined) node = event.currentTarget;\n if (node) {\n var svg = node.ownerSVGElement || node;\n if (svg.createSVGPoint) {\n var point = svg.createSVGPoint();\n point.x = event.clientX, point.y = event.clientY;\n point = point.matrixTransform(node.getScreenCTM().inverse());\n return [point.x, point.y];\n }\n if (node.getBoundingClientRect) {\n var rect = node.getBoundingClientRect();\n return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];\n }\n }\n return [event.pageX, event.pageY];\n}\n","// These are typically used in conjunction with noevent to ensure that we can\n// preventDefault on the event.\nexport const nonpassive = {passive: false};\nexport const nonpassivecapture = {capture: true, passive: false};\n\nexport function nopropagation(event) {\n event.stopImmediatePropagation();\n}\n\nexport default function(event) {\n event.preventDefault();\n event.stopImmediatePropagation();\n}\n","import {select} from \"d3-selection\";\nimport noevent, {nonpassivecapture} from \"./noevent.js\";\n\nexport default function(view) {\n var root = view.document.documentElement,\n selection = select(view).on(\"dragstart.drag\", noevent, nonpassivecapture);\n if (\"onselectstart\" in root) {\n selection.on(\"selectstart.drag\", noevent, nonpassivecapture);\n } else {\n root.__noselect = root.style.MozUserSelect;\n root.style.MozUserSelect = \"none\";\n }\n}\n\nexport function yesdrag(view, noclick) {\n var root = view.document.documentElement,\n selection = select(view).on(\"dragstart.drag\", null);\n if (noclick) {\n selection.on(\"click.drag\", noevent, nonpassivecapture);\n setTimeout(function() { selection.on(\"click.drag\", null); }, 0);\n }\n if (\"onselectstart\" in root) {\n selection.on(\"selectstart.drag\", null);\n } else {\n root.style.MozUserSelect = root.__noselect;\n delete root.__noselect;\n }\n}\n","export default x => () => x;\n","export default function DragEvent(type, {\n sourceEvent,\n subject,\n target,\n identifier,\n active,\n x, y, dx, dy,\n dispatch\n}) {\n Object.defineProperties(this, {\n type: {value: type, enumerable: true, configurable: true},\n sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},\n subject: {value: subject, enumerable: true, configurable: true},\n target: {value: target, enumerable: true, configurable: true},\n identifier: {value: identifier, enumerable: true, configurable: true},\n active: {value: active, enumerable: true, configurable: true},\n x: {value: x, enumerable: true, configurable: true},\n y: {value: y, enumerable: true, configurable: true},\n dx: {value: dx, enumerable: true, configurable: true},\n dy: {value: dy, enumerable: true, configurable: true},\n _: {value: dispatch}\n });\n}\n\nDragEvent.prototype.on = function() {\n var value = this._.on.apply(this._, arguments);\n return value === this._ ? this : value;\n};\n","import {dispatch} from \"d3-dispatch\";\nimport {select, pointer} from \"d3-selection\";\nimport nodrag, {yesdrag} from \"./nodrag.js\";\nimport noevent, {nonpassive, nonpassivecapture, nopropagation} from \"./noevent.js\";\nimport constant from \"./constant.js\";\nimport DragEvent from \"./event.js\";\n\n// Ignore right-click, since that should open the context menu.\nfunction defaultFilter(event) {\n return !event.ctrlKey && !event.button;\n}\n\nfunction defaultContainer() {\n return this.parentNode;\n}\n\nfunction defaultSubject(event, d) {\n return d == null ? {x: event.x, y: event.y} : d;\n}\n\nfunction defaultTouchable() {\n return navigator.maxTouchPoints || (\"ontouchstart\" in this);\n}\n\nexport default function() {\n var filter = defaultFilter,\n container = defaultContainer,\n subject = defaultSubject,\n touchable = defaultTouchable,\n gestures = {},\n listeners = dispatch(\"start\", \"drag\", \"end\"),\n active = 0,\n mousedownx,\n mousedowny,\n mousemoving,\n touchending,\n clickDistance2 = 0;\n\n function drag(selection) {\n selection\n .on(\"mousedown.drag\", mousedowned)\n .filter(touchable)\n .on(\"touchstart.drag\", touchstarted)\n .on(\"touchmove.drag\", touchmoved, nonpassive)\n .on(\"touchend.drag touchcancel.drag\", touchended)\n .style(\"touch-action\", \"none\")\n .style(\"-webkit-tap-highlight-color\", \"rgba(0,0,0,0)\");\n }\n\n function mousedowned(event, d) {\n if (touchending || !filter.call(this, event, d)) return;\n var gesture = beforestart(this, container.call(this, event, d), event, d, \"mouse\");\n if (!gesture) return;\n select(event.view)\n .on(\"mousemove.drag\", mousemoved, nonpassivecapture)\n .on(\"mouseup.drag\", mouseupped, nonpassivecapture);\n nodrag(event.view);\n nopropagation(event);\n mousemoving = false;\n mousedownx = event.clientX;\n mousedowny = event.clientY;\n gesture(\"start\", event);\n }\n\n function mousemoved(event) {\n noevent(event);\n if (!mousemoving) {\n var dx = event.clientX - mousedownx, dy = event.clientY - mousedowny;\n mousemoving = dx * dx + dy * dy > clickDistance2;\n }\n gestures.mouse(\"drag\", event);\n }\n\n function mouseupped(event) {\n select(event.view).on(\"mousemove.drag mouseup.drag\", null);\n yesdrag(event.view, mousemoving);\n noevent(event);\n gestures.mouse(\"end\", event);\n }\n\n function touchstarted(event, d) {\n if (!filter.call(this, event, d)) return;\n var touches = event.changedTouches,\n c = container.call(this, event, d),\n n = touches.length, i, gesture;\n\n for (i = 0; i < n; ++i) {\n if (gesture = beforestart(this, c, event, d, touches[i].identifier, touches[i])) {\n nopropagation(event);\n gesture(\"start\", event, touches[i]);\n }\n }\n }\n\n function touchmoved(event) {\n var touches = event.changedTouches,\n n = touches.length, i, gesture;\n\n for (i = 0; i < n; ++i) {\n if (gesture = gestures[touches[i].identifier]) {\n noevent(event);\n gesture(\"drag\", event, touches[i]);\n }\n }\n }\n\n function touchended(event) {\n var touches = event.changedTouches,\n n = touches.length, i, gesture;\n\n if (touchending) clearTimeout(touchending);\n touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!\n for (i = 0; i < n; ++i) {\n if (gesture = gestures[touches[i].identifier]) {\n nopropagation(event);\n gesture(\"end\", event, touches[i]);\n }\n }\n }\n\n function beforestart(that, container, event, d, identifier, touch) {\n var dispatch = listeners.copy(),\n p = pointer(touch || event, container), dx, dy,\n s;\n\n if ((s = subject.call(that, new DragEvent(\"beforestart\", {\n sourceEvent: event,\n target: drag,\n identifier,\n active,\n x: p[0],\n y: p[1],\n dx: 0,\n dy: 0,\n dispatch\n }), d)) == null) return;\n\n dx = s.x - p[0] || 0;\n dy = s.y - p[1] || 0;\n\n return function gesture(type, event, touch) {\n var p0 = p, n;\n switch (type) {\n case \"start\": gestures[identifier] = gesture, n = active++; break;\n case \"end\": delete gestures[identifier], --active; // falls through\n case \"drag\": p = pointer(touch || event, container), n = active; break;\n }\n dispatch.call(\n type,\n that,\n new DragEvent(type, {\n sourceEvent: event,\n subject: s,\n target: drag,\n identifier,\n active: n,\n x: p[0] + dx,\n y: p[1] + dy,\n dx: p[0] - p0[0],\n dy: p[1] - p0[1],\n dispatch\n }),\n d\n );\n };\n }\n\n drag.filter = function(_) {\n return arguments.length ? (filter = typeof _ === \"function\" ? _ : constant(!!_), drag) : filter;\n };\n\n drag.container = function(_) {\n return arguments.length ? (container = typeof _ === \"function\" ? _ : constant(_), drag) : container;\n };\n\n drag.subject = function(_) {\n return arguments.length ? (subject = typeof _ === \"function\" ? _ : constant(_), drag) : subject;\n };\n\n drag.touchable = function(_) {\n return arguments.length ? (touchable = typeof _ === \"function\" ? _ : constant(!!_), drag) : touchable;\n };\n\n drag.on = function() {\n var value = listeners.on.apply(listeners, arguments);\n return value === listeners ? drag : value;\n };\n\n drag.clickDistance = function(_) {\n return arguments.length ? (clickDistance2 = (_ = +_) * _, drag) : Math.sqrt(clickDistance2);\n };\n\n return drag;\n}\n","export default function(constructor, factory, prototype) {\n constructor.prototype = factory.prototype = prototype;\n prototype.constructor = constructor;\n}\n\nexport function extend(parent, definition) {\n var prototype = Object.create(parent.prototype);\n for (var key in definition) prototype[key] = definition[key];\n return prototype;\n}\n","import define, {extend} from \"./define.js\";\n\nexport function Color() {}\n\nexport var darker = 0.7;\nexport var brighter = 1 / darker;\n\nvar reI = \"\\\\s*([+-]?\\\\d+)\\\\s*\",\n reN = \"\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)\\\\s*\",\n reP = \"\\\\s*([+-]?(?:\\\\d*\\\\.)?\\\\d+(?:[eE][+-]?\\\\d+)?)%\\\\s*\",\n reHex = /^#([0-9a-f]{3,8})$/,\n reRgbInteger = new RegExp(`^rgb\\\\(${reI},${reI},${reI}\\\\)$`),\n reRgbPercent = new RegExp(`^rgb\\\\(${reP},${reP},${reP}\\\\)$`),\n reRgbaInteger = new RegExp(`^rgba\\\\(${reI},${reI},${reI},${reN}\\\\)$`),\n reRgbaPercent = new RegExp(`^rgba\\\\(${reP},${reP},${reP},${reN}\\\\)$`),\n reHslPercent = new RegExp(`^hsl\\\\(${reN},${reP},${reP}\\\\)$`),\n reHslaPercent = new RegExp(`^hsla\\\\(${reN},${reP},${reP},${reN}\\\\)$`);\n\nvar named = {\n aliceblue: 0xf0f8ff,\n antiquewhite: 0xfaebd7,\n aqua: 0x00ffff,\n aquamarine: 0x7fffd4,\n azure: 0xf0ffff,\n beige: 0xf5f5dc,\n bisque: 0xffe4c4,\n black: 0x000000,\n blanchedalmond: 0xffebcd,\n blue: 0x0000ff,\n blueviolet: 0x8a2be2,\n brown: 0xa52a2a,\n burlywood: 0xdeb887,\n cadetblue: 0x5f9ea0,\n chartreuse: 0x7fff00,\n chocolate: 0xd2691e,\n coral: 0xff7f50,\n cornflowerblue: 0x6495ed,\n cornsilk: 0xfff8dc,\n crimson: 0xdc143c,\n cyan: 0x00ffff,\n darkblue: 0x00008b,\n darkcyan: 0x008b8b,\n darkgoldenrod: 0xb8860b,\n darkgray: 0xa9a9a9,\n darkgreen: 0x006400,\n darkgrey: 0xa9a9a9,\n darkkhaki: 0xbdb76b,\n darkmagenta: 0x8b008b,\n darkolivegreen: 0x556b2f,\n darkorange: 0xff8c00,\n darkorchid: 0x9932cc,\n darkred: 0x8b0000,\n darksalmon: 0xe9967a,\n darkseagreen: 0x8fbc8f,\n darkslateblue: 0x483d8b,\n darkslategray: 0x2f4f4f,\n darkslategrey: 0x2f4f4f,\n darkturquoise: 0x00ced1,\n darkviolet: 0x9400d3,\n deeppink: 0xff1493,\n deepskyblue: 0x00bfff,\n dimgray: 0x696969,\n dimgrey: 0x696969,\n dodgerblue: 0x1e90ff,\n firebrick: 0xb22222,\n floralwhite: 0xfffaf0,\n forestgreen: 0x228b22,\n fuchsia: 0xff00ff,\n gainsboro: 0xdcdcdc,\n ghostwhite: 0xf8f8ff,\n gold: 0xffd700,\n goldenrod: 0xdaa520,\n gray: 0x808080,\n green: 0x008000,\n greenyellow: 0xadff2f,\n grey: 0x808080,\n honeydew: 0xf0fff0,\n hotpink: 0xff69b4,\n indianred: 0xcd5c5c,\n indigo: 0x4b0082,\n ivory: 0xfffff0,\n khaki: 0xf0e68c,\n lavender: 0xe6e6fa,\n lavenderblush: 0xfff0f5,\n lawngreen: 0x7cfc00,\n lemonchiffon: 0xfffacd,\n lightblue: 0xadd8e6,\n lightcoral: 0xf08080,\n lightcyan: 0xe0ffff,\n lightgoldenrodyellow: 0xfafad2,\n lightgray: 0xd3d3d3,\n lightgreen: 0x90ee90,\n lightgrey: 0xd3d3d3,\n lightpink: 0xffb6c1,\n lightsalmon: 0xffa07a,\n lightseagreen: 0x20b2aa,\n lightskyblue: 0x87cefa,\n lightslategray: 0x778899,\n lightslategrey: 0x778899,\n lightsteelblue: 0xb0c4de,\n lightyellow: 0xffffe0,\n lime: 0x00ff00,\n limegreen: 0x32cd32,\n linen: 0xfaf0e6,\n magenta: 0xff00ff,\n maroon: 0x800000,\n mediumaquamarine: 0x66cdaa,\n mediumblue: 0x0000cd,\n mediumorchid: 0xba55d3,\n mediumpurple: 0x9370db,\n mediumseagreen: 0x3cb371,\n mediumslateblue: 0x7b68ee,\n mediumspringgreen: 0x00fa9a,\n mediumturquoise: 0x48d1cc,\n mediumvioletred: 0xc71585,\n midnightblue: 0x191970,\n mintcream: 0xf5fffa,\n mistyrose: 0xffe4e1,\n moccasin: 0xffe4b5,\n navajowhite: 0xffdead,\n navy: 0x000080,\n oldlace: 0xfdf5e6,\n olive: 0x808000,\n olivedrab: 0x6b8e23,\n orange: 0xffa500,\n orangered: 0xff4500,\n orchid: 0xda70d6,\n palegoldenrod: 0xeee8aa,\n palegreen: 0x98fb98,\n paleturquoise: 0xafeeee,\n palevioletred: 0xdb7093,\n papayawhip: 0xffefd5,\n peachpuff: 0xffdab9,\n peru: 0xcd853f,\n pink: 0xffc0cb,\n plum: 0xdda0dd,\n powderblue: 0xb0e0e6,\n purple: 0x800080,\n rebeccapurple: 0x663399,\n red: 0xff0000,\n rosybrown: 0xbc8f8f,\n royalblue: 0x4169e1,\n saddlebrown: 0x8b4513,\n salmon: 0xfa8072,\n sandybrown: 0xf4a460,\n seagreen: 0x2e8b57,\n seashell: 0xfff5ee,\n sienna: 0xa0522d,\n silver: 0xc0c0c0,\n skyblue: 0x87ceeb,\n slateblue: 0x6a5acd,\n slategray: 0x708090,\n slategrey: 0x708090,\n snow: 0xfffafa,\n springgreen: 0x00ff7f,\n steelblue: 0x4682b4,\n tan: 0xd2b48c,\n teal: 0x008080,\n thistle: 0xd8bfd8,\n tomato: 0xff6347,\n turquoise: 0x40e0d0,\n violet: 0xee82ee,\n wheat: 0xf5deb3,\n white: 0xffffff,\n whitesmoke: 0xf5f5f5,\n yellow: 0xffff00,\n yellowgreen: 0x9acd32\n};\n\ndefine(Color, color, {\n copy(channels) {\n return Object.assign(new this.constructor, this, channels);\n },\n displayable() {\n return this.rgb().displayable();\n },\n hex: color_formatHex, // Deprecated! Use color.formatHex.\n formatHex: color_formatHex,\n formatHex8: color_formatHex8,\n formatHsl: color_formatHsl,\n formatRgb: color_formatRgb,\n toString: color_formatRgb\n});\n\nfunction color_formatHex() {\n return this.rgb().formatHex();\n}\n\nfunction color_formatHex8() {\n return this.rgb().formatHex8();\n}\n\nfunction color_formatHsl() {\n return hslConvert(this).formatHsl();\n}\n\nfunction color_formatRgb() {\n return this.rgb().formatRgb();\n}\n\nexport default function color(format) {\n var m, l;\n format = (format + \"\").trim().toLowerCase();\n return (m = reHex.exec(format)) ? (l = m[1].length, m = parseInt(m[1], 16), l === 6 ? rgbn(m) // #ff0000\n : l === 3 ? new Rgb((m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1) // #f00\n : l === 8 ? rgba(m >> 24 & 0xff, m >> 16 & 0xff, m >> 8 & 0xff, (m & 0xff) / 0xff) // #ff000000\n : l === 4 ? rgba((m >> 12 & 0xf) | (m >> 8 & 0xf0), (m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), (((m & 0xf) << 4) | (m & 0xf)) / 0xff) // #f000\n : null) // invalid hex\n : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0)\n : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%)\n : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1)\n : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1)\n : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%)\n : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1)\n : named.hasOwnProperty(format) ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins\n : format === \"transparent\" ? new Rgb(NaN, NaN, NaN, 0)\n : null;\n}\n\nfunction rgbn(n) {\n return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1);\n}\n\nfunction rgba(r, g, b, a) {\n if (a <= 0) r = g = b = NaN;\n return new Rgb(r, g, b, a);\n}\n\nexport function rgbConvert(o) {\n if (!(o instanceof Color)) o = color(o);\n if (!o) return new Rgb;\n o = o.rgb();\n return new Rgb(o.r, o.g, o.b, o.opacity);\n}\n\nexport function rgb(r, g, b, opacity) {\n return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity);\n}\n\nexport function Rgb(r, g, b, opacity) {\n this.r = +r;\n this.g = +g;\n this.b = +b;\n this.opacity = +opacity;\n}\n\ndefine(Rgb, rgb, extend(Color, {\n brighter(k) {\n k = k == null ? brighter : Math.pow(brighter, k);\n return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);\n },\n darker(k) {\n k = k == null ? darker : Math.pow(darker, k);\n return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity);\n },\n rgb() {\n return this;\n },\n clamp() {\n return new Rgb(clampi(this.r), clampi(this.g), clampi(this.b), clampa(this.opacity));\n },\n displayable() {\n return (-0.5 <= this.r && this.r < 255.5)\n && (-0.5 <= this.g && this.g < 255.5)\n && (-0.5 <= this.b && this.b < 255.5)\n && (0 <= this.opacity && this.opacity <= 1);\n },\n hex: rgb_formatHex, // Deprecated! Use color.formatHex.\n formatHex: rgb_formatHex,\n formatHex8: rgb_formatHex8,\n formatRgb: rgb_formatRgb,\n toString: rgb_formatRgb\n}));\n\nfunction rgb_formatHex() {\n return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}`;\n}\n\nfunction rgb_formatHex8() {\n return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}${hex((isNaN(this.opacity) ? 1 : this.opacity) * 255)}`;\n}\n\nfunction rgb_formatRgb() {\n const a = clampa(this.opacity);\n return `${a === 1 ? \"rgb(\" : \"rgba(\"}${clampi(this.r)}, ${clampi(this.g)}, ${clampi(this.b)}${a === 1 ? \")\" : `, ${a})`}`;\n}\n\nfunction clampa(opacity) {\n return isNaN(opacity) ? 1 : Math.max(0, Math.min(1, opacity));\n}\n\nfunction clampi(value) {\n return Math.max(0, Math.min(255, Math.round(value) || 0));\n}\n\nfunction hex(value) {\n value = clampi(value);\n return (value < 16 ? \"0\" : \"\") + value.toString(16);\n}\n\nfunction hsla(h, s, l, a) {\n if (a <= 0) h = s = l = NaN;\n else if (l <= 0 || l >= 1) h = s = NaN;\n else if (s <= 0) h = NaN;\n return new Hsl(h, s, l, a);\n}\n\nexport function hslConvert(o) {\n if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity);\n if (!(o instanceof Color)) o = color(o);\n if (!o) return new Hsl;\n if (o instanceof Hsl) return o;\n o = o.rgb();\n var r = o.r / 255,\n g = o.g / 255,\n b = o.b / 255,\n min = Math.min(r, g, b),\n max = Math.max(r, g, b),\n h = NaN,\n s = max - min,\n l = (max + min) / 2;\n if (s) {\n if (r === max) h = (g - b) / s + (g < b) * 6;\n else if (g === max) h = (b - r) / s + 2;\n else h = (r - g) / s + 4;\n s /= l < 0.5 ? max + min : 2 - max - min;\n h *= 60;\n } else {\n s = l > 0 && l < 1 ? 0 : h;\n }\n return new Hsl(h, s, l, o.opacity);\n}\n\nexport function hsl(h, s, l, opacity) {\n return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity);\n}\n\nfunction Hsl(h, s, l, opacity) {\n this.h = +h;\n this.s = +s;\n this.l = +l;\n this.opacity = +opacity;\n}\n\ndefine(Hsl, hsl, extend(Color, {\n brighter(k) {\n k = k == null ? brighter : Math.pow(brighter, k);\n return new Hsl(this.h, this.s, this.l * k, this.opacity);\n },\n darker(k) {\n k = k == null ? darker : Math.pow(darker, k);\n return new Hsl(this.h, this.s, this.l * k, this.opacity);\n },\n rgb() {\n var h = this.h % 360 + (this.h < 0) * 360,\n s = isNaN(h) || isNaN(this.s) ? 0 : this.s,\n l = this.l,\n m2 = l + (l < 0.5 ? l : 1 - l) * s,\n m1 = 2 * l - m2;\n return new Rgb(\n hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2),\n hsl2rgb(h, m1, m2),\n hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2),\n this.opacity\n );\n },\n clamp() {\n return new Hsl(clamph(this.h), clampt(this.s), clampt(this.l), clampa(this.opacity));\n },\n displayable() {\n return (0 <= this.s && this.s <= 1 || isNaN(this.s))\n && (0 <= this.l && this.l <= 1)\n && (0 <= this.opacity && this.opacity <= 1);\n },\n formatHsl() {\n const a = clampa(this.opacity);\n return `${a === 1 ? \"hsl(\" : \"hsla(\"}${clamph(this.h)}, ${clampt(this.s) * 100}%, ${clampt(this.l) * 100}%${a === 1 ? \")\" : `, ${a})`}`;\n }\n}));\n\nfunction clamph(value) {\n value = (value || 0) % 360;\n return value < 0 ? value + 360 : value;\n}\n\nfunction clampt(value) {\n return Math.max(0, Math.min(1, value || 0));\n}\n\n/* From FvD 13.37, CSS Color Module Level 3 */\nfunction hsl2rgb(h, m1, m2) {\n return (h < 60 ? m1 + (m2 - m1) * h / 60\n : h < 180 ? m2\n : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60\n : m1) * 255;\n}\n","export default x => () => x;\n","import constant from \"./constant.js\";\n\nfunction linear(a, d) {\n return function(t) {\n return a + t * d;\n };\n}\n\nfunction exponential(a, b, y) {\n return a = Math.pow(a, y), b = Math.pow(b, y) - a, y = 1 / y, function(t) {\n return Math.pow(a + t * b, y);\n };\n}\n\nexport function hue(a, b) {\n var d = b - a;\n return d ? linear(a, d > 180 || d < -180 ? d - 360 * Math.round(d / 360) : d) : constant(isNaN(a) ? b : a);\n}\n\nexport function gamma(y) {\n return (y = +y) === 1 ? nogamma : function(a, b) {\n return b - a ? exponential(a, b, y) : constant(isNaN(a) ? b : a);\n };\n}\n\nexport default function nogamma(a, b) {\n var d = b - a;\n return d ? linear(a, d) : constant(isNaN(a) ? b : a);\n}\n","import {rgb as colorRgb} from \"d3-color\";\nimport basis from \"./basis.js\";\nimport basisClosed from \"./basisClosed.js\";\nimport nogamma, {gamma} from \"./color.js\";\n\nexport default (function rgbGamma(y) {\n var color = gamma(y);\n\n function rgb(start, end) {\n var r = color((start = colorRgb(start)).r, (end = colorRgb(end)).r),\n g = color(start.g, end.g),\n b = color(start.b, end.b),\n opacity = nogamma(start.opacity, end.opacity);\n return function(t) {\n start.r = r(t);\n start.g = g(t);\n start.b = b(t);\n start.opacity = opacity(t);\n return start + \"\";\n };\n }\n\n rgb.gamma = rgbGamma;\n\n return rgb;\n})(1);\n\nfunction rgbSpline(spline) {\n return function(colors) {\n var n = colors.length,\n r = new Array(n),\n g = new Array(n),\n b = new Array(n),\n i, color;\n for (i = 0; i < n; ++i) {\n color = colorRgb(colors[i]);\n r[i] = color.r || 0;\n g[i] = color.g || 0;\n b[i] = color.b || 0;\n }\n r = spline(r);\n g = spline(g);\n b = spline(b);\n color.opacity = 1;\n return function(t) {\n color.r = r(t);\n color.g = g(t);\n color.b = b(t);\n return color + \"\";\n };\n };\n}\n\nexport var rgbBasis = rgbSpline(basis);\nexport var rgbBasisClosed = rgbSpline(basisClosed);\n","export default function(a, b) {\n return a = +a, b = +b, function(t) {\n return a * (1 - t) + b * t;\n };\n}\n","import number from \"./number.js\";\n\nvar reA = /[-+]?(?:\\d+\\.?\\d*|\\.?\\d+)(?:[eE][-+]?\\d+)?/g,\n reB = new RegExp(reA.source, \"g\");\n\nfunction zero(b) {\n return function() {\n return b;\n };\n}\n\nfunction one(b) {\n return function(t) {\n return b(t) + \"\";\n };\n}\n\nexport default function(a, b) {\n var bi = reA.lastIndex = reB.lastIndex = 0, // scan index for next number in b\n am, // current match in a\n bm, // current match in b\n bs, // string preceding current number in b, if any\n i = -1, // index in s\n s = [], // string constants and placeholders\n q = []; // number interpolators\n\n // Coerce inputs to strings.\n a = a + \"\", b = b + \"\";\n\n // Interpolate pairs of numbers in a & b.\n while ((am = reA.exec(a))\n && (bm = reB.exec(b))) {\n if ((bs = bm.index) > bi) { // a string precedes the next number in b\n bs = b.slice(bi, bs);\n if (s[i]) s[i] += bs; // coalesce with previous string\n else s[++i] = bs;\n }\n if ((am = am[0]) === (bm = bm[0])) { // numbers in a & b match\n if (s[i]) s[i] += bm; // coalesce with previous string\n else s[++i] = bm;\n } else { // interpolate non-matching numbers\n s[++i] = null;\n q.push({i: i, x: number(am, bm)});\n }\n bi = reB.lastIndex;\n }\n\n // Add remains of b.\n if (bi < b.length) {\n bs = b.slice(bi);\n if (s[i]) s[i] += bs; // coalesce with previous string\n else s[++i] = bs;\n }\n\n // Special optimization for only a single match.\n // Otherwise, interpolate each of the numbers and rejoin the string.\n return s.length < 2 ? (q[0]\n ? one(q[0].x)\n : zero(b))\n : (b = q.length, function(t) {\n for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t);\n return s.join(\"\");\n });\n}\n","var degrees = 180 / Math.PI;\n\nexport var identity = {\n translateX: 0,\n translateY: 0,\n rotate: 0,\n skewX: 0,\n scaleX: 1,\n scaleY: 1\n};\n\nexport default function(a, b, c, d, e, f) {\n var scaleX, scaleY, skewX;\n if (scaleX = Math.sqrt(a * a + b * b)) a /= scaleX, b /= scaleX;\n if (skewX = a * c + b * d) c -= a * skewX, d -= b * skewX;\n if (scaleY = Math.sqrt(c * c + d * d)) c /= scaleY, d /= scaleY, skewX /= scaleY;\n if (a * d < b * c) a = -a, b = -b, skewX = -skewX, scaleX = -scaleX;\n return {\n translateX: e,\n translateY: f,\n rotate: Math.atan2(b, a) * degrees,\n skewX: Math.atan(skewX) * degrees,\n scaleX: scaleX,\n scaleY: scaleY\n };\n}\n","import decompose, {identity} from \"./decompose.js\";\n\nvar svgNode;\n\n/* eslint-disable no-undef */\nexport function parseCss(value) {\n const m = new (typeof DOMMatrix === \"function\" ? DOMMatrix : WebKitCSSMatrix)(value + \"\");\n return m.isIdentity ? identity : decompose(m.a, m.b, m.c, m.d, m.e, m.f);\n}\n\nexport function parseSvg(value) {\n if (value == null) return identity;\n if (!svgNode) svgNode = document.createElementNS(\"http://www.w3.org/2000/svg\", \"g\");\n svgNode.setAttribute(\"transform\", value);\n if (!(value = svgNode.transform.baseVal.consolidate())) return identity;\n value = value.matrix;\n return decompose(value.a, value.b, value.c, value.d, value.e, value.f);\n}\n","import number from \"../number.js\";\nimport {parseCss, parseSvg} from \"./parse.js\";\n\nfunction interpolateTransform(parse, pxComma, pxParen, degParen) {\n\n function pop(s) {\n return s.length ? s.pop() + \" \" : \"\";\n }\n\n function translate(xa, ya, xb, yb, s, q) {\n if (xa !== xb || ya !== yb) {\n var i = s.push(\"translate(\", null, pxComma, null, pxParen);\n q.push({i: i - 4, x: number(xa, xb)}, {i: i - 2, x: number(ya, yb)});\n } else if (xb || yb) {\n s.push(\"translate(\" + xb + pxComma + yb + pxParen);\n }\n }\n\n function rotate(a, b, s, q) {\n if (a !== b) {\n if (a - b > 180) b += 360; else if (b - a > 180) a += 360; // shortest path\n q.push({i: s.push(pop(s) + \"rotate(\", null, degParen) - 2, x: number(a, b)});\n } else if (b) {\n s.push(pop(s) + \"rotate(\" + b + degParen);\n }\n }\n\n function skewX(a, b, s, q) {\n if (a !== b) {\n q.push({i: s.push(pop(s) + \"skewX(\", null, degParen) - 2, x: number(a, b)});\n } else if (b) {\n s.push(pop(s) + \"skewX(\" + b + degParen);\n }\n }\n\n function scale(xa, ya, xb, yb, s, q) {\n if (xa !== xb || ya !== yb) {\n var i = s.push(pop(s) + \"scale(\", null, \",\", null, \")\");\n q.push({i: i - 4, x: number(xa, xb)}, {i: i - 2, x: number(ya, yb)});\n } else if (xb !== 1 || yb !== 1) {\n s.push(pop(s) + \"scale(\" + xb + \",\" + yb + \")\");\n }\n }\n\n return function(a, b) {\n var s = [], // string constants and placeholders\n q = []; // number interpolators\n a = parse(a), b = parse(b);\n translate(a.translateX, a.translateY, b.translateX, b.translateY, s, q);\n rotate(a.rotate, b.rotate, s, q);\n skewX(a.skewX, b.skewX, s, q);\n scale(a.scaleX, a.scaleY, b.scaleX, b.scaleY, s, q);\n a = b = null; // gc\n return function(t) {\n var i = -1, n = q.length, o;\n while (++i < n) s[(o = q[i]).i] = o.x(t);\n return s.join(\"\");\n };\n };\n}\n\nexport var interpolateTransformCss = interpolateTransform(parseCss, \"px, \", \"px)\", \"deg)\");\nexport var interpolateTransformSvg = interpolateTransform(parseSvg, \", \", \")\", \")\");\n","var epsilon2 = 1e-12;\n\nfunction cosh(x) {\n return ((x = Math.exp(x)) + 1 / x) / 2;\n}\n\nfunction sinh(x) {\n return ((x = Math.exp(x)) - 1 / x) / 2;\n}\n\nfunction tanh(x) {\n return ((x = Math.exp(2 * x)) - 1) / (x + 1);\n}\n\nexport default (function zoomRho(rho, rho2, rho4) {\n\n // p0 = [ux0, uy0, w0]\n // p1 = [ux1, uy1, w1]\n function zoom(p0, p1) {\n var ux0 = p0[0], uy0 = p0[1], w0 = p0[2],\n ux1 = p1[0], uy1 = p1[1], w1 = p1[2],\n dx = ux1 - ux0,\n dy = uy1 - uy0,\n d2 = dx * dx + dy * dy,\n i,\n S;\n\n // Special case for u0 ≅ u1.\n if (d2 < epsilon2) {\n S = Math.log(w1 / w0) / rho;\n i = function(t) {\n return [\n ux0 + t * dx,\n uy0 + t * dy,\n w0 * Math.exp(rho * t * S)\n ];\n }\n }\n\n // General case.\n else {\n var d1 = Math.sqrt(d2),\n b0 = (w1 * w1 - w0 * w0 + rho4 * d2) / (2 * w0 * rho2 * d1),\n b1 = (w1 * w1 - w0 * w0 - rho4 * d2) / (2 * w1 * rho2 * d1),\n r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0),\n r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1);\n S = (r1 - r0) / rho;\n i = function(t) {\n var s = t * S,\n coshr0 = cosh(r0),\n u = w0 / (rho2 * d1) * (coshr0 * tanh(rho * s + r0) - sinh(r0));\n return [\n ux0 + u * dx,\n uy0 + u * dy,\n w0 * coshr0 / cosh(rho * s + r0)\n ];\n }\n }\n\n i.duration = S * 1000 * rho / Math.SQRT2;\n\n return i;\n }\n\n zoom.rho = function(_) {\n var _1 = Math.max(1e-3, +_), _2 = _1 * _1, _4 = _2 * _2;\n return zoomRho(_1, _2, _4);\n };\n\n return zoom;\n})(Math.SQRT2, 2, 4);\n","var frame = 0, // is an animation frame pending?\n timeout = 0, // is a timeout pending?\n interval = 0, // are any timers active?\n pokeDelay = 1000, // how frequently we check for clock skew\n taskHead,\n taskTail,\n clockLast = 0,\n clockNow = 0,\n clockSkew = 0,\n clock = typeof performance === \"object\" && performance.now ? performance : Date,\n setFrame = typeof window === \"object\" && window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : function(f) { setTimeout(f, 17); };\n\nexport function now() {\n return clockNow || (setFrame(clearNow), clockNow = clock.now() + clockSkew);\n}\n\nfunction clearNow() {\n clockNow = 0;\n}\n\nexport function Timer() {\n this._call =\n this._time =\n this._next = null;\n}\n\nTimer.prototype = timer.prototype = {\n constructor: Timer,\n restart: function(callback, delay, time) {\n if (typeof callback !== \"function\") throw new TypeError(\"callback is not a function\");\n time = (time == null ? now() : +time) + (delay == null ? 0 : +delay);\n if (!this._next && taskTail !== this) {\n if (taskTail) taskTail._next = this;\n else taskHead = this;\n taskTail = this;\n }\n this._call = callback;\n this._time = time;\n sleep();\n },\n stop: function() {\n if (this._call) {\n this._call = null;\n this._time = Infinity;\n sleep();\n }\n }\n};\n\nexport function timer(callback, delay, time) {\n var t = new Timer;\n t.restart(callback, delay, time);\n return t;\n}\n\nexport function timerFlush() {\n now(); // Get the current time, if not already set.\n ++frame; // Pretend we’ve set an alarm, if we haven’t already.\n var t = taskHead, e;\n while (t) {\n if ((e = clockNow - t._time) >= 0) t._call.call(undefined, e);\n t = t._next;\n }\n --frame;\n}\n\nfunction wake() {\n clockNow = (clockLast = clock.now()) + clockSkew;\n frame = timeout = 0;\n try {\n timerFlush();\n } finally {\n frame = 0;\n nap();\n clockNow = 0;\n }\n}\n\nfunction poke() {\n var now = clock.now(), delay = now - clockLast;\n if (delay > pokeDelay) clockSkew -= delay, clockLast = now;\n}\n\nfunction nap() {\n var t0, t1 = taskHead, t2, time = Infinity;\n while (t1) {\n if (t1._call) {\n if (time > t1._time) time = t1._time;\n t0 = t1, t1 = t1._next;\n } else {\n t2 = t1._next, t1._next = null;\n t1 = t0 ? t0._next = t2 : taskHead = t2;\n }\n }\n taskTail = t0;\n sleep(time);\n}\n\nfunction sleep(time) {\n if (frame) return; // Soonest alarm already set, or will be.\n if (timeout) timeout = clearTimeout(timeout);\n var delay = time - clockNow; // Strictly less than if we recomputed clockNow.\n if (delay > 24) {\n if (time < Infinity) timeout = setTimeout(wake, time - clock.now() - clockSkew);\n if (interval) interval = clearInterval(interval);\n } else {\n if (!interval) clockLast = clock.now(), interval = setInterval(poke, pokeDelay);\n frame = 1, setFrame(wake);\n }\n}\n","import {Timer} from \"./timer.js\";\n\nexport default function(callback, delay, time) {\n var t = new Timer;\n delay = delay == null ? 0 : +delay;\n t.restart(elapsed => {\n t.stop();\n callback(elapsed + delay);\n }, delay, time);\n return t;\n}\n","import {dispatch} from \"d3-dispatch\";\nimport {timer, timeout} from \"d3-timer\";\n\nvar emptyOn = dispatch(\"start\", \"end\", \"cancel\", \"interrupt\");\nvar emptyTween = [];\n\nexport var CREATED = 0;\nexport var SCHEDULED = 1;\nexport var STARTING = 2;\nexport var STARTED = 3;\nexport var RUNNING = 4;\nexport var ENDING = 5;\nexport var ENDED = 6;\n\nexport default function(node, name, id, index, group, timing) {\n var schedules = node.__transition;\n if (!schedules) node.__transition = {};\n else if (id in schedules) return;\n create(node, id, {\n name: name,\n index: index, // For context during callback.\n group: group, // For context during callback.\n on: emptyOn,\n tween: emptyTween,\n time: timing.time,\n delay: timing.delay,\n duration: timing.duration,\n ease: timing.ease,\n timer: null,\n state: CREATED\n });\n}\n\nexport function init(node, id) {\n var schedule = get(node, id);\n if (schedule.state > CREATED) throw new Error(\"too late; already scheduled\");\n return schedule;\n}\n\nexport function set(node, id) {\n var schedule = get(node, id);\n if (schedule.state > STARTED) throw new Error(\"too late; already running\");\n return schedule;\n}\n\nexport function get(node, id) {\n var schedule = node.__transition;\n if (!schedule || !(schedule = schedule[id])) throw new Error(\"transition not found\");\n return schedule;\n}\n\nfunction create(node, id, self) {\n var schedules = node.__transition,\n tween;\n\n // Initialize the self timer when the transition is created.\n // Note the actual delay is not known until the first callback!\n schedules[id] = self;\n self.timer = timer(schedule, 0, self.time);\n\n function schedule(elapsed) {\n self.state = SCHEDULED;\n self.timer.restart(start, self.delay, self.time);\n\n // If the elapsed delay is less than our first sleep, start immediately.\n if (self.delay <= elapsed) start(elapsed - self.delay);\n }\n\n function start(elapsed) {\n var i, j, n, o;\n\n // If the state is not SCHEDULED, then we previously errored on start.\n if (self.state !== SCHEDULED) return stop();\n\n for (i in schedules) {\n o = schedules[i];\n if (o.name !== self.name) continue;\n\n // While this element already has a starting transition during this frame,\n // defer starting an interrupting transition until that transition has a\n // chance to tick (and possibly end); see d3/d3-transition#54!\n if (o.state === STARTED) return timeout(start);\n\n // Interrupt the active transition, if any.\n if (o.state === RUNNING) {\n o.state = ENDED;\n o.timer.stop();\n o.on.call(\"interrupt\", node, node.__data__, o.index, o.group);\n delete schedules[i];\n }\n\n // Cancel any pre-empted transitions.\n else if (+i < id) {\n o.state = ENDED;\n o.timer.stop();\n o.on.call(\"cancel\", node, node.__data__, o.index, o.group);\n delete schedules[i];\n }\n }\n\n // Defer the first tick to end of the current frame; see d3/d3#1576.\n // Note the transition may be canceled after start and before the first tick!\n // Note this must be scheduled before the start event; see d3/d3-transition#16!\n // Assuming this is successful, subsequent callbacks go straight to tick.\n timeout(function() {\n if (self.state === STARTED) {\n self.state = RUNNING;\n self.timer.restart(tick, self.delay, self.time);\n tick(elapsed);\n }\n });\n\n // Dispatch the start event.\n // Note this must be done before the tween are initialized.\n self.state = STARTING;\n self.on.call(\"start\", node, node.__data__, self.index, self.group);\n if (self.state !== STARTING) return; // interrupted\n self.state = STARTED;\n\n // Initialize the tween, deleting null tween.\n tween = new Array(n = self.tween.length);\n for (i = 0, j = -1; i < n; ++i) {\n if (o = self.tween[i].value.call(node, node.__data__, self.index, self.group)) {\n tween[++j] = o;\n }\n }\n tween.length = j + 1;\n }\n\n function tick(elapsed) {\n var t = elapsed < self.duration ? self.ease.call(null, elapsed / self.duration) : (self.timer.restart(stop), self.state = ENDING, 1),\n i = -1,\n n = tween.length;\n\n while (++i < n) {\n tween[i].call(node, t);\n }\n\n // Dispatch the end event.\n if (self.state === ENDING) {\n self.on.call(\"end\", node, node.__data__, self.index, self.group);\n stop();\n }\n }\n\n function stop() {\n self.state = ENDED;\n self.timer.stop();\n delete schedules[id];\n for (var i in schedules) return; // eslint-disable-line no-unused-vars\n delete node.__transition;\n }\n}\n","import {STARTING, ENDING, ENDED} from \"./transition/schedule.js\";\n\nexport default function(node, name) {\n var schedules = node.__transition,\n schedule,\n active,\n empty = true,\n i;\n\n if (!schedules) return;\n\n name = name == null ? null : name + \"\";\n\n for (i in schedules) {\n if ((schedule = schedules[i]).name !== name) { empty = false; continue; }\n active = schedule.state > STARTING && schedule.state < ENDING;\n schedule.state = ENDED;\n schedule.timer.stop();\n schedule.on.call(active ? \"interrupt\" : \"cancel\", node, node.__data__, schedule.index, schedule.group);\n delete schedules[i];\n }\n\n if (empty) delete node.__transition;\n}\n","import interrupt from \"../interrupt.js\";\n\nexport default function(name) {\n return this.each(function() {\n interrupt(this, name);\n });\n}\n","import {get, set} from \"./schedule.js\";\n\nfunction tweenRemove(id, name) {\n var tween0, tween1;\n return function() {\n var schedule = set(this, id),\n tween = schedule.tween;\n\n // If this node shared tween with the previous node,\n // just assign the updated shared tween and we’re done!\n // Otherwise, copy-on-write.\n if (tween !== tween0) {\n tween1 = tween0 = tween;\n for (var i = 0, n = tween1.length; i < n; ++i) {\n if (tween1[i].name === name) {\n tween1 = tween1.slice();\n tween1.splice(i, 1);\n break;\n }\n }\n }\n\n schedule.tween = tween1;\n };\n}\n\nfunction tweenFunction(id, name, value) {\n var tween0, tween1;\n if (typeof value !== \"function\") throw new Error;\n return function() {\n var schedule = set(this, id),\n tween = schedule.tween;\n\n // If this node shared tween with the previous node,\n // just assign the updated shared tween and we’re done!\n // Otherwise, copy-on-write.\n if (tween !== tween0) {\n tween1 = (tween0 = tween).slice();\n for (var t = {name: name, value: value}, i = 0, n = tween1.length; i < n; ++i) {\n if (tween1[i].name === name) {\n tween1[i] = t;\n break;\n }\n }\n if (i === n) tween1.push(t);\n }\n\n schedule.tween = tween1;\n };\n}\n\nexport default function(name, value) {\n var id = this._id;\n\n name += \"\";\n\n if (arguments.length < 2) {\n var tween = get(this.node(), id).tween;\n for (var i = 0, n = tween.length, t; i < n; ++i) {\n if ((t = tween[i]).name === name) {\n return t.value;\n }\n }\n return null;\n }\n\n return this.each((value == null ? tweenRemove : tweenFunction)(id, name, value));\n}\n\nexport function tweenValue(transition, name, value) {\n var id = transition._id;\n\n transition.each(function() {\n var schedule = set(this, id);\n (schedule.value || (schedule.value = {}))[name] = value.apply(this, arguments);\n });\n\n return function(node) {\n return get(node, id).value[name];\n };\n}\n","import {color} from \"d3-color\";\nimport {interpolateNumber, interpolateRgb, interpolateString} from \"d3-interpolate\";\n\nexport default function(a, b) {\n var c;\n return (typeof b === \"number\" ? interpolateNumber\n : b instanceof color ? interpolateRgb\n : (c = color(b)) ? (b = c, interpolateRgb)\n : interpolateString)(a, b);\n}\n","import {interpolateTransformSvg as interpolateTransform} from \"d3-interpolate\";\nimport {namespace} from \"d3-selection\";\nimport {tweenValue} from \"./tween.js\";\nimport interpolate from \"./interpolate.js\";\n\nfunction attrRemove(name) {\n return function() {\n this.removeAttribute(name);\n };\n}\n\nfunction attrRemoveNS(fullname) {\n return function() {\n this.removeAttributeNS(fullname.space, fullname.local);\n };\n}\n\nfunction attrConstant(name, interpolate, value1) {\n var string00,\n string1 = value1 + \"\",\n interpolate0;\n return function() {\n var string0 = this.getAttribute(name);\n return string0 === string1 ? null\n : string0 === string00 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, value1);\n };\n}\n\nfunction attrConstantNS(fullname, interpolate, value1) {\n var string00,\n string1 = value1 + \"\",\n interpolate0;\n return function() {\n var string0 = this.getAttributeNS(fullname.space, fullname.local);\n return string0 === string1 ? null\n : string0 === string00 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, value1);\n };\n}\n\nfunction attrFunction(name, interpolate, value) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0, value1 = value(this), string1;\n if (value1 == null) return void this.removeAttribute(name);\n string0 = this.getAttribute(name);\n string1 = value1 + \"\";\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));\n };\n}\n\nfunction attrFunctionNS(fullname, interpolate, value) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0, value1 = value(this), string1;\n if (value1 == null) return void this.removeAttributeNS(fullname.space, fullname.local);\n string0 = this.getAttributeNS(fullname.space, fullname.local);\n string1 = value1 + \"\";\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));\n };\n}\n\nexport default function(name, value) {\n var fullname = namespace(name), i = fullname === \"transform\" ? interpolateTransform : interpolate;\n return this.attrTween(name, typeof value === \"function\"\n ? (fullname.local ? attrFunctionNS : attrFunction)(fullname, i, tweenValue(this, \"attr.\" + name, value))\n : value == null ? (fullname.local ? attrRemoveNS : attrRemove)(fullname)\n : (fullname.local ? attrConstantNS : attrConstant)(fullname, i, value));\n}\n","import {namespace} from \"d3-selection\";\n\nfunction attrInterpolate(name, i) {\n return function(t) {\n this.setAttribute(name, i.call(this, t));\n };\n}\n\nfunction attrInterpolateNS(fullname, i) {\n return function(t) {\n this.setAttributeNS(fullname.space, fullname.local, i.call(this, t));\n };\n}\n\nfunction attrTweenNS(fullname, value) {\n var t0, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t0 = (i0 = i) && attrInterpolateNS(fullname, i);\n return t0;\n }\n tween._value = value;\n return tween;\n}\n\nfunction attrTween(name, value) {\n var t0, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t0 = (i0 = i) && attrInterpolate(name, i);\n return t0;\n }\n tween._value = value;\n return tween;\n}\n\nexport default function(name, value) {\n var key = \"attr.\" + name;\n if (arguments.length < 2) return (key = this.tween(key)) && key._value;\n if (value == null) return this.tween(key, null);\n if (typeof value !== \"function\") throw new Error;\n var fullname = namespace(name);\n return this.tween(key, (fullname.local ? attrTweenNS : attrTween)(fullname, value));\n}\n","import {get, init} from \"./schedule.js\";\n\nfunction delayFunction(id, value) {\n return function() {\n init(this, id).delay = +value.apply(this, arguments);\n };\n}\n\nfunction delayConstant(id, value) {\n return value = +value, function() {\n init(this, id).delay = value;\n };\n}\n\nexport default function(value) {\n var id = this._id;\n\n return arguments.length\n ? this.each((typeof value === \"function\"\n ? delayFunction\n : delayConstant)(id, value))\n : get(this.node(), id).delay;\n}\n","import {get, set} from \"./schedule.js\";\n\nfunction durationFunction(id, value) {\n return function() {\n set(this, id).duration = +value.apply(this, arguments);\n };\n}\n\nfunction durationConstant(id, value) {\n return value = +value, function() {\n set(this, id).duration = value;\n };\n}\n\nexport default function(value) {\n var id = this._id;\n\n return arguments.length\n ? this.each((typeof value === \"function\"\n ? durationFunction\n : durationConstant)(id, value))\n : get(this.node(), id).duration;\n}\n","import {get, set} from \"./schedule.js\";\n\nfunction easeConstant(id, value) {\n if (typeof value !== \"function\") throw new Error;\n return function() {\n set(this, id).ease = value;\n };\n}\n\nexport default function(value) {\n var id = this._id;\n\n return arguments.length\n ? this.each(easeConstant(id, value))\n : get(this.node(), id).ease;\n}\n","import {set} from \"./schedule.js\";\n\nfunction easeVarying(id, value) {\n return function() {\n var v = value.apply(this, arguments);\n if (typeof v !== \"function\") throw new Error;\n set(this, id).ease = v;\n };\n}\n\nexport default function(value) {\n if (typeof value !== \"function\") throw new Error;\n return this.each(easeVarying(this._id, value));\n}\n","import {matcher} from \"d3-selection\";\nimport {Transition} from \"./index.js\";\n\nexport default function(match) {\n if (typeof match !== \"function\") match = matcher(match);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {\n if ((node = group[i]) && match.call(node, node.__data__, i, group)) {\n subgroup.push(node);\n }\n }\n }\n\n return new Transition(subgroups, this._parents, this._name, this._id);\n}\n","import {Transition} from \"./index.js\";\n\nexport default function(transition) {\n if (transition._id !== this._id) throw new Error;\n\n for (var groups0 = this._groups, groups1 = transition._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {\n for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {\n if (node = group0[i] || group1[i]) {\n merge[i] = node;\n }\n }\n }\n\n for (; j < m0; ++j) {\n merges[j] = groups0[j];\n }\n\n return new Transition(merges, this._parents, this._name, this._id);\n}\n","import {get, set, init} from \"./schedule.js\";\n\nfunction start(name) {\n return (name + \"\").trim().split(/^|\\s+/).every(function(t) {\n var i = t.indexOf(\".\");\n if (i >= 0) t = t.slice(0, i);\n return !t || t === \"start\";\n });\n}\n\nfunction onFunction(id, name, listener) {\n var on0, on1, sit = start(name) ? init : set;\n return function() {\n var schedule = sit(this, id),\n on = schedule.on;\n\n // If this node shared a dispatch with the previous node,\n // just assign the updated shared dispatch and we’re done!\n // Otherwise, copy-on-write.\n if (on !== on0) (on1 = (on0 = on).copy()).on(name, listener);\n\n schedule.on = on1;\n };\n}\n\nexport default function(name, listener) {\n var id = this._id;\n\n return arguments.length < 2\n ? get(this.node(), id).on.on(name)\n : this.each(onFunction(id, name, listener));\n}\n","function removeFunction(id) {\n return function() {\n var parent = this.parentNode;\n for (var i in this.__transition) if (+i !== id) return;\n if (parent) parent.removeChild(this);\n };\n}\n\nexport default function() {\n return this.on(\"end.remove\", removeFunction(this._id));\n}\n","import {selector} from \"d3-selection\";\nimport {Transition} from \"./index.js\";\nimport schedule, {get} from \"./schedule.js\";\n\nexport default function(select) {\n var name = this._name,\n id = this._id;\n\n if (typeof select !== \"function\") select = selector(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {\n if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {\n if (\"__data__\" in node) subnode.__data__ = node.__data__;\n subgroup[i] = subnode;\n schedule(subgroup[i], name, id, i, subgroup, get(node, id));\n }\n }\n }\n\n return new Transition(subgroups, this._parents, name, id);\n}\n","import {selectorAll} from \"d3-selection\";\nimport {Transition} from \"./index.js\";\nimport schedule, {get} from \"./schedule.js\";\n\nexport default function(select) {\n var name = this._name,\n id = this._id;\n\n if (typeof select !== \"function\") select = selectorAll(select);\n\n for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n for (var children = select.call(node, node.__data__, i, group), child, inherit = get(node, id), k = 0, l = children.length; k < l; ++k) {\n if (child = children[k]) {\n schedule(child, name, id, k, children, inherit);\n }\n }\n subgroups.push(children);\n parents.push(node);\n }\n }\n }\n\n return new Transition(subgroups, parents, name, id);\n}\n","import {selection} from \"d3-selection\";\n\nvar Selection = selection.prototype.constructor;\n\nexport default function() {\n return new Selection(this._groups, this._parents);\n}\n","import {interpolateTransformCss as interpolateTransform} from \"d3-interpolate\";\nimport {style} from \"d3-selection\";\nimport {set} from \"./schedule.js\";\nimport {tweenValue} from \"./tween.js\";\nimport interpolate from \"./interpolate.js\";\n\nfunction styleNull(name, interpolate) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0 = style(this, name),\n string1 = (this.style.removeProperty(name), style(this, name));\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, string10 = string1);\n };\n}\n\nfunction styleRemove(name) {\n return function() {\n this.style.removeProperty(name);\n };\n}\n\nfunction styleConstant(name, interpolate, value1) {\n var string00,\n string1 = value1 + \"\",\n interpolate0;\n return function() {\n var string0 = style(this, name);\n return string0 === string1 ? null\n : string0 === string00 ? interpolate0\n : interpolate0 = interpolate(string00 = string0, value1);\n };\n}\n\nfunction styleFunction(name, interpolate, value) {\n var string00,\n string10,\n interpolate0;\n return function() {\n var string0 = style(this, name),\n value1 = value(this),\n string1 = value1 + \"\";\n if (value1 == null) string1 = value1 = (this.style.removeProperty(name), style(this, name));\n return string0 === string1 ? null\n : string0 === string00 && string1 === string10 ? interpolate0\n : (string10 = string1, interpolate0 = interpolate(string00 = string0, value1));\n };\n}\n\nfunction styleMaybeRemove(id, name) {\n var on0, on1, listener0, key = \"style.\" + name, event = \"end.\" + key, remove;\n return function() {\n var schedule = set(this, id),\n on = schedule.on,\n listener = schedule.value[key] == null ? remove || (remove = styleRemove(name)) : undefined;\n\n // If this node shared a dispatch with the previous node,\n // just assign the updated shared dispatch and we’re done!\n // Otherwise, copy-on-write.\n if (on !== on0 || listener0 !== listener) (on1 = (on0 = on).copy()).on(event, listener0 = listener);\n\n schedule.on = on1;\n };\n}\n\nexport default function(name, value, priority) {\n var i = (name += \"\") === \"transform\" ? interpolateTransform : interpolate;\n return value == null ? this\n .styleTween(name, styleNull(name, i))\n .on(\"end.style.\" + name, styleRemove(name))\n : typeof value === \"function\" ? this\n .styleTween(name, styleFunction(name, i, tweenValue(this, \"style.\" + name, value)))\n .each(styleMaybeRemove(this._id, name))\n : this\n .styleTween(name, styleConstant(name, i, value), priority)\n .on(\"end.style.\" + name, null);\n}\n","function styleInterpolate(name, i, priority) {\n return function(t) {\n this.style.setProperty(name, i.call(this, t), priority);\n };\n}\n\nfunction styleTween(name, value, priority) {\n var t, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t = (i0 = i) && styleInterpolate(name, i, priority);\n return t;\n }\n tween._value = value;\n return tween;\n}\n\nexport default function(name, value, priority) {\n var key = \"style.\" + (name += \"\");\n if (arguments.length < 2) return (key = this.tween(key)) && key._value;\n if (value == null) return this.tween(key, null);\n if (typeof value !== \"function\") throw new Error;\n return this.tween(key, styleTween(name, value, priority == null ? \"\" : priority));\n}\n","import {tweenValue} from \"./tween.js\";\n\nfunction textConstant(value) {\n return function() {\n this.textContent = value;\n };\n}\n\nfunction textFunction(value) {\n return function() {\n var value1 = value(this);\n this.textContent = value1 == null ? \"\" : value1;\n };\n}\n\nexport default function(value) {\n return this.tween(\"text\", typeof value === \"function\"\n ? textFunction(tweenValue(this, \"text\", value))\n : textConstant(value == null ? \"\" : value + \"\"));\n}\n","function textInterpolate(i) {\n return function(t) {\n this.textContent = i.call(this, t);\n };\n}\n\nfunction textTween(value) {\n var t0, i0;\n function tween() {\n var i = value.apply(this, arguments);\n if (i !== i0) t0 = (i0 = i) && textInterpolate(i);\n return t0;\n }\n tween._value = value;\n return tween;\n}\n\nexport default function(value) {\n var key = \"text\";\n if (arguments.length < 1) return (key = this.tween(key)) && key._value;\n if (value == null) return this.tween(key, null);\n if (typeof value !== \"function\") throw new Error;\n return this.tween(key, textTween(value));\n}\n","import {Transition, newId} from \"./index.js\";\nimport schedule, {get} from \"./schedule.js\";\n\nexport default function() {\n var name = this._name,\n id0 = this._id,\n id1 = newId();\n\n for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n var inherit = get(node, id0);\n schedule(node, name, id1, i, group, {\n time: inherit.time + inherit.delay + inherit.duration,\n delay: 0,\n duration: inherit.duration,\n ease: inherit.ease\n });\n }\n }\n }\n\n return new Transition(groups, this._parents, name, id1);\n}\n","import {set} from \"./schedule.js\";\n\nexport default function() {\n var on0, on1, that = this, id = that._id, size = that.size();\n return new Promise(function(resolve, reject) {\n var cancel = {value: reject},\n end = {value: function() { if (--size === 0) resolve(); }};\n\n that.each(function() {\n var schedule = set(this, id),\n on = schedule.on;\n\n // If this node shared a dispatch with the previous node,\n // just assign the updated shared dispatch and we’re done!\n // Otherwise, copy-on-write.\n if (on !== on0) {\n on1 = (on0 = on).copy();\n on1._.cancel.push(cancel);\n on1._.interrupt.push(cancel);\n on1._.end.push(end);\n }\n\n schedule.on = on1;\n });\n\n // The selection was empty, resolve end immediately\n if (size === 0) resolve();\n });\n}\n","import {selection} from \"d3-selection\";\nimport transition_attr from \"./attr.js\";\nimport transition_attrTween from \"./attrTween.js\";\nimport transition_delay from \"./delay.js\";\nimport transition_duration from \"./duration.js\";\nimport transition_ease from \"./ease.js\";\nimport transition_easeVarying from \"./easeVarying.js\";\nimport transition_filter from \"./filter.js\";\nimport transition_merge from \"./merge.js\";\nimport transition_on from \"./on.js\";\nimport transition_remove from \"./remove.js\";\nimport transition_select from \"./select.js\";\nimport transition_selectAll from \"./selectAll.js\";\nimport transition_selection from \"./selection.js\";\nimport transition_style from \"./style.js\";\nimport transition_styleTween from \"./styleTween.js\";\nimport transition_text from \"./text.js\";\nimport transition_textTween from \"./textTween.js\";\nimport transition_transition from \"./transition.js\";\nimport transition_tween from \"./tween.js\";\nimport transition_end from \"./end.js\";\n\nvar id = 0;\n\nexport function Transition(groups, parents, name, id) {\n this._groups = groups;\n this._parents = parents;\n this._name = name;\n this._id = id;\n}\n\nexport default function transition(name) {\n return selection().transition(name);\n}\n\nexport function newId() {\n return ++id;\n}\n\nvar selection_prototype = selection.prototype;\n\nTransition.prototype = transition.prototype = {\n constructor: Transition,\n select: transition_select,\n selectAll: transition_selectAll,\n selectChild: selection_prototype.selectChild,\n selectChildren: selection_prototype.selectChildren,\n filter: transition_filter,\n merge: transition_merge,\n selection: transition_selection,\n transition: transition_transition,\n call: selection_prototype.call,\n nodes: selection_prototype.nodes,\n node: selection_prototype.node,\n size: selection_prototype.size,\n empty: selection_prototype.empty,\n each: selection_prototype.each,\n on: transition_on,\n attr: transition_attr,\n attrTween: transition_attrTween,\n style: transition_style,\n styleTween: transition_styleTween,\n text: transition_text,\n textTween: transition_textTween,\n remove: transition_remove,\n tween: transition_tween,\n delay: transition_delay,\n duration: transition_duration,\n ease: transition_ease,\n easeVarying: transition_easeVarying,\n end: transition_end,\n [Symbol.iterator]: selection_prototype[Symbol.iterator]\n};\n","export const linear = t => +t;\n","export function quadIn(t) {\n return t * t;\n}\n\nexport function quadOut(t) {\n return t * (2 - t);\n}\n\nexport function quadInOut(t) {\n return ((t *= 2) <= 1 ? t * t : --t * (2 - t) + 1) / 2;\n}\n","export function cubicIn(t) {\n return t * t * t;\n}\n\nexport function cubicOut(t) {\n return --t * t * t + 1;\n}\n\nexport function cubicInOut(t) {\n return ((t *= 2) <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;\n}\n","var pi = Math.PI,\n halfPi = pi / 2;\n\nexport function sinIn(t) {\n return (+t === 1) ? 1 : 1 - Math.cos(t * halfPi);\n}\n\nexport function sinOut(t) {\n return Math.sin(t * halfPi);\n}\n\nexport function sinInOut(t) {\n return (1 - Math.cos(pi * t)) / 2;\n}\n","// tpmt is two power minus ten times t scaled to [0,1]\nexport function tpmt(x) {\n return (Math.pow(2, -10 * x) - 0.0009765625) * 1.0009775171065494;\n}\n","import {tpmt} from \"./math.js\";\n\nexport function expIn(t) {\n return tpmt(1 - +t);\n}\n\nexport function expOut(t) {\n return 1 - tpmt(t);\n}\n\nexport function expInOut(t) {\n return ((t *= 2) <= 1 ? tpmt(1 - t) : 2 - tpmt(t - 1)) / 2;\n}\n","export function circleIn(t) {\n return 1 - Math.sqrt(1 - t * t);\n}\n\nexport function circleOut(t) {\n return Math.sqrt(1 - --t * t);\n}\n\nexport function circleInOut(t) {\n return ((t *= 2) <= 1 ? 1 - Math.sqrt(1 - t * t) : Math.sqrt(1 - (t -= 2) * t) + 1) / 2;\n}\n","var b1 = 4 / 11,\n b2 = 6 / 11,\n b3 = 8 / 11,\n b4 = 3 / 4,\n b5 = 9 / 11,\n b6 = 10 / 11,\n b7 = 15 / 16,\n b8 = 21 / 22,\n b9 = 63 / 64,\n b0 = 1 / b1 / b1;\n\nexport function bounceIn(t) {\n return 1 - bounceOut(1 - t);\n}\n\nexport function bounceOut(t) {\n return (t = +t) < b1 ? b0 * t * t : t < b3 ? b0 * (t -= b2) * t + b4 : t < b6 ? b0 * (t -= b5) * t + b7 : b0 * (t -= b8) * t + b9;\n}\n\nexport function bounceInOut(t) {\n return ((t *= 2) <= 1 ? 1 - bounceOut(1 - t) : bounceOut(t - 1) + 1) / 2;\n}\n","var overshoot = 1.70158;\n\nexport var backIn = (function custom(s) {\n s = +s;\n\n function backIn(t) {\n return (t = +t) * t * (s * (t - 1) + t);\n }\n\n backIn.overshoot = custom;\n\n return backIn;\n})(overshoot);\n\nexport var backOut = (function custom(s) {\n s = +s;\n\n function backOut(t) {\n return --t * t * ((t + 1) * s + t) + 1;\n }\n\n backOut.overshoot = custom;\n\n return backOut;\n})(overshoot);\n\nexport var backInOut = (function custom(s) {\n s = +s;\n\n function backInOut(t) {\n return ((t *= 2) < 1 ? t * t * ((s + 1) * t - s) : (t -= 2) * t * ((s + 1) * t + s) + 2) / 2;\n }\n\n backInOut.overshoot = custom;\n\n return backInOut;\n})(overshoot);\n","import {tpmt} from \"./math.js\";\n\nvar tau = 2 * Math.PI,\n amplitude = 1,\n period = 0.3;\n\nexport var elasticIn = (function custom(a, p) {\n var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);\n\n function elasticIn(t) {\n return a * tpmt(-(--t)) * Math.sin((s - t) / p);\n }\n\n elasticIn.amplitude = function(a) { return custom(a, p * tau); };\n elasticIn.period = function(p) { return custom(a, p); };\n\n return elasticIn;\n})(amplitude, period);\n\nexport var elasticOut = (function custom(a, p) {\n var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);\n\n function elasticOut(t) {\n return 1 - a * tpmt(t = +t) * Math.sin((t + s) / p);\n }\n\n elasticOut.amplitude = function(a) { return custom(a, p * tau); };\n elasticOut.period = function(p) { return custom(a, p); };\n\n return elasticOut;\n})(amplitude, period);\n\nexport var elasticInOut = (function custom(a, p) {\n var s = Math.asin(1 / (a = Math.max(1, a))) * (p /= tau);\n\n function elasticInOut(t) {\n return ((t = t * 2 - 1) < 0\n ? a * tpmt(-t) * Math.sin((s - t) / p)\n : 2 - a * tpmt(t) * Math.sin((s + t) / p)) / 2;\n }\n\n elasticInOut.amplitude = function(a) { return custom(a, p * tau); };\n elasticInOut.period = function(p) { return custom(a, p); };\n\n return elasticInOut;\n})(amplitude, period);\n","import {Transition, newId} from \"../transition/index.js\";\nimport schedule from \"../transition/schedule.js\";\nimport {easeCubicInOut} from \"d3-ease\";\nimport {now} from \"d3-timer\";\n\nvar defaultTiming = {\n time: null, // Set on use.\n delay: 0,\n duration: 250,\n ease: easeCubicInOut\n};\n\nfunction inherit(node, id) {\n var timing;\n while (!(timing = node.__transition) || !(timing = timing[id])) {\n if (!(node = node.parentNode)) {\n throw new Error(`transition ${id} not found`);\n }\n }\n return timing;\n}\n\nexport default function(name) {\n var id,\n timing;\n\n if (name instanceof Transition) {\n id = name._id, name = name._name;\n } else {\n id = newId(), (timing = defaultTiming).time = now(), name = name == null ? null : name + \"\";\n }\n\n for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) {\n for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {\n if (node = group[i]) {\n schedule(node, name, id, i, group, timing || inherit(node, id));\n }\n }\n }\n\n return new Transition(groups, this._parents, name, id);\n}\n","import {selection} from \"d3-selection\";\nimport selection_interrupt from \"./interrupt.js\";\nimport selection_transition from \"./transition.js\";\n\nselection.prototype.interrupt = selection_interrupt;\nselection.prototype.transition = selection_transition;\n","export default x => () => x;\n","export default function ZoomEvent(type, {\n sourceEvent,\n target,\n transform,\n dispatch\n}) {\n Object.defineProperties(this, {\n type: {value: type, enumerable: true, configurable: true},\n sourceEvent: {value: sourceEvent, enumerable: true, configurable: true},\n target: {value: target, enumerable: true, configurable: true},\n transform: {value: transform, enumerable: true, configurable: true},\n _: {value: dispatch}\n });\n}\n","export function Transform(k, x, y) {\n this.k = k;\n this.x = x;\n this.y = y;\n}\n\nTransform.prototype = {\n constructor: Transform,\n scale: function(k) {\n return k === 1 ? this : new Transform(this.k * k, this.x, this.y);\n },\n translate: function(x, y) {\n return x === 0 & y === 0 ? this : new Transform(this.k, this.x + this.k * x, this.y + this.k * y);\n },\n apply: function(point) {\n return [point[0] * this.k + this.x, point[1] * this.k + this.y];\n },\n applyX: function(x) {\n return x * this.k + this.x;\n },\n applyY: function(y) {\n return y * this.k + this.y;\n },\n invert: function(location) {\n return [(location[0] - this.x) / this.k, (location[1] - this.y) / this.k];\n },\n invertX: function(x) {\n return (x - this.x) / this.k;\n },\n invertY: function(y) {\n return (y - this.y) / this.k;\n },\n rescaleX: function(x) {\n return x.copy().domain(x.range().map(this.invertX, this).map(x.invert, x));\n },\n rescaleY: function(y) {\n return y.copy().domain(y.range().map(this.invertY, this).map(y.invert, y));\n },\n toString: function() {\n return \"translate(\" + this.x + \",\" + this.y + \") scale(\" + this.k + \")\";\n }\n};\n\nexport var identity = new Transform(1, 0, 0);\n\ntransform.prototype = Transform.prototype;\n\nexport default function transform(node) {\n while (!node.__zoom) if (!(node = node.parentNode)) return identity;\n return node.__zoom;\n}\n","export function nopropagation(event) {\n event.stopImmediatePropagation();\n}\n\nexport default function(event) {\n event.preventDefault();\n event.stopImmediatePropagation();\n}\n","import {dispatch} from \"d3-dispatch\";\nimport {dragDisable, dragEnable} from \"d3-drag\";\nimport {interpolateZoom} from \"d3-interpolate\";\nimport {select, pointer} from \"d3-selection\";\nimport {interrupt} from \"d3-transition\";\nimport constant from \"./constant.js\";\nimport ZoomEvent from \"./event.js\";\nimport {Transform, identity} from \"./transform.js\";\nimport noevent, {nopropagation} from \"./noevent.js\";\n\n// Ignore right-click, since that should open the context menu.\n// except for pinch-to-zoom, which is sent as a wheel+ctrlKey event\nfunction defaultFilter(event) {\n return (!event.ctrlKey || event.type === 'wheel') && !event.button;\n}\n\nfunction defaultExtent() {\n var e = this;\n if (e instanceof SVGElement) {\n e = e.ownerSVGElement || e;\n if (e.hasAttribute(\"viewBox\")) {\n e = e.viewBox.baseVal;\n return [[e.x, e.y], [e.x + e.width, e.y + e.height]];\n }\n return [[0, 0], [e.width.baseVal.value, e.height.baseVal.value]];\n }\n return [[0, 0], [e.clientWidth, e.clientHeight]];\n}\n\nfunction defaultTransform() {\n return this.__zoom || identity;\n}\n\nfunction defaultWheelDelta(event) {\n return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * (event.ctrlKey ? 10 : 1);\n}\n\nfunction defaultTouchable() {\n return navigator.maxTouchPoints || (\"ontouchstart\" in this);\n}\n\nfunction defaultConstrain(transform, extent, translateExtent) {\n var dx0 = transform.invertX(extent[0][0]) - translateExtent[0][0],\n dx1 = transform.invertX(extent[1][0]) - translateExtent[1][0],\n dy0 = transform.invertY(extent[0][1]) - translateExtent[0][1],\n dy1 = transform.invertY(extent[1][1]) - translateExtent[1][1];\n return transform.translate(\n dx1 > dx0 ? (dx0 + dx1) / 2 : Math.min(0, dx0) || Math.max(0, dx1),\n dy1 > dy0 ? (dy0 + dy1) / 2 : Math.min(0, dy0) || Math.max(0, dy1)\n );\n}\n\nexport default function() {\n var filter = defaultFilter,\n extent = defaultExtent,\n constrain = defaultConstrain,\n wheelDelta = defaultWheelDelta,\n touchable = defaultTouchable,\n scaleExtent = [0, Infinity],\n translateExtent = [[-Infinity, -Infinity], [Infinity, Infinity]],\n duration = 250,\n interpolate = interpolateZoom,\n listeners = dispatch(\"start\", \"zoom\", \"end\"),\n touchstarting,\n touchfirst,\n touchending,\n touchDelay = 500,\n wheelDelay = 150,\n clickDistance2 = 0,\n tapDistance = 10;\n\n function zoom(selection) {\n selection\n .property(\"__zoom\", defaultTransform)\n .on(\"wheel.zoom\", wheeled, {passive: false})\n .on(\"mousedown.zoom\", mousedowned)\n .on(\"dblclick.zoom\", dblclicked)\n .filter(touchable)\n .on(\"touchstart.zoom\", touchstarted)\n .on(\"touchmove.zoom\", touchmoved)\n .on(\"touchend.zoom touchcancel.zoom\", touchended)\n .style(\"-webkit-tap-highlight-color\", \"rgba(0,0,0,0)\");\n }\n\n zoom.transform = function(collection, transform, point, event) {\n var selection = collection.selection ? collection.selection() : collection;\n selection.property(\"__zoom\", defaultTransform);\n if (collection !== selection) {\n schedule(collection, transform, point, event);\n } else {\n selection.interrupt().each(function() {\n gesture(this, arguments)\n .event(event)\n .start()\n .zoom(null, typeof transform === \"function\" ? transform.apply(this, arguments) : transform)\n .end();\n });\n }\n };\n\n zoom.scaleBy = function(selection, k, p, event) {\n zoom.scaleTo(selection, function() {\n var k0 = this.__zoom.k,\n k1 = typeof k === \"function\" ? k.apply(this, arguments) : k;\n return k0 * k1;\n }, p, event);\n };\n\n zoom.scaleTo = function(selection, k, p, event) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t0 = this.__zoom,\n p0 = p == null ? centroid(e) : typeof p === \"function\" ? p.apply(this, arguments) : p,\n p1 = t0.invert(p0),\n k1 = typeof k === \"function\" ? k.apply(this, arguments) : k;\n return constrain(translate(scale(t0, k1), p0, p1), e, translateExtent);\n }, p, event);\n };\n\n zoom.translateBy = function(selection, x, y, event) {\n zoom.transform(selection, function() {\n return constrain(this.__zoom.translate(\n typeof x === \"function\" ? x.apply(this, arguments) : x,\n typeof y === \"function\" ? y.apply(this, arguments) : y\n ), extent.apply(this, arguments), translateExtent);\n }, null, event);\n };\n\n zoom.translateTo = function(selection, x, y, p, event) {\n zoom.transform(selection, function() {\n var e = extent.apply(this, arguments),\n t = this.__zoom,\n p0 = p == null ? centroid(e) : typeof p === \"function\" ? p.apply(this, arguments) : p;\n return constrain(identity.translate(p0[0], p0[1]).scale(t.k).translate(\n typeof x === \"function\" ? -x.apply(this, arguments) : -x,\n typeof y === \"function\" ? -y.apply(this, arguments) : -y\n ), e, translateExtent);\n }, p, event);\n };\n\n function scale(transform, k) {\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], k));\n return k === transform.k ? transform : new Transform(k, transform.x, transform.y);\n }\n\n function translate(transform, p0, p1) {\n var x = p0[0] - p1[0] * transform.k, y = p0[1] - p1[1] * transform.k;\n return x === transform.x && y === transform.y ? transform : new Transform(transform.k, x, y);\n }\n\n function centroid(extent) {\n return [(+extent[0][0] + +extent[1][0]) / 2, (+extent[0][1] + +extent[1][1]) / 2];\n }\n\n function schedule(transition, transform, point, event) {\n transition\n .on(\"start.zoom\", function() { gesture(this, arguments).event(event).start(); })\n .on(\"interrupt.zoom end.zoom\", function() { gesture(this, arguments).event(event).end(); })\n .tween(\"zoom\", function() {\n var that = this,\n args = arguments,\n g = gesture(that, args).event(event),\n e = extent.apply(that, args),\n p = point == null ? centroid(e) : typeof point === \"function\" ? point.apply(that, args) : point,\n w = Math.max(e[1][0] - e[0][0], e[1][1] - e[0][1]),\n a = that.__zoom,\n b = typeof transform === \"function\" ? transform.apply(that, args) : transform,\n i = interpolate(a.invert(p).concat(w / a.k), b.invert(p).concat(w / b.k));\n return function(t) {\n if (t === 1) t = b; // Avoid rounding error on end.\n else { var l = i(t), k = w / l[2]; t = new Transform(k, p[0] - l[0] * k, p[1] - l[1] * k); }\n g.zoom(null, t);\n };\n });\n }\n\n function gesture(that, args, clean) {\n return (!clean && that.__zooming) || new Gesture(that, args);\n }\n\n function Gesture(that, args) {\n this.that = that;\n this.args = args;\n this.active = 0;\n this.sourceEvent = null;\n this.extent = extent.apply(that, args);\n this.taps = 0;\n }\n\n Gesture.prototype = {\n event: function(event) {\n if (event) this.sourceEvent = event;\n return this;\n },\n start: function() {\n if (++this.active === 1) {\n this.that.__zooming = this;\n this.emit(\"start\");\n }\n return this;\n },\n zoom: function(key, transform) {\n if (this.mouse && key !== \"mouse\") this.mouse[1] = transform.invert(this.mouse[0]);\n if (this.touch0 && key !== \"touch\") this.touch0[1] = transform.invert(this.touch0[0]);\n if (this.touch1 && key !== \"touch\") this.touch1[1] = transform.invert(this.touch1[0]);\n this.that.__zoom = transform;\n this.emit(\"zoom\");\n return this;\n },\n end: function() {\n if (--this.active === 0) {\n delete this.that.__zooming;\n this.emit(\"end\");\n }\n return this;\n },\n emit: function(type) {\n var d = select(this.that).datum();\n listeners.call(\n type,\n this.that,\n new ZoomEvent(type, {\n sourceEvent: this.sourceEvent,\n target: zoom,\n type,\n transform: this.that.__zoom,\n dispatch: listeners\n }),\n d\n );\n }\n };\n\n function wheeled(event, ...args) {\n if (!filter.apply(this, arguments)) return;\n var g = gesture(this, args).event(event),\n t = this.__zoom,\n k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], t.k * Math.pow(2, wheelDelta.apply(this, arguments)))),\n p = pointer(event);\n\n // If the mouse is in the same location as before, reuse it.\n // If there were recent wheel events, reset the wheel idle timeout.\n if (g.wheel) {\n if (g.mouse[0][0] !== p[0] || g.mouse[0][1] !== p[1]) {\n g.mouse[1] = t.invert(g.mouse[0] = p);\n }\n clearTimeout(g.wheel);\n }\n\n // If this wheel event won’t trigger a transform change, ignore it.\n else if (t.k === k) return;\n\n // Otherwise, capture the mouse point and location at the start.\n else {\n g.mouse = [p, t.invert(p)];\n interrupt(this);\n g.start();\n }\n\n noevent(event);\n g.wheel = setTimeout(wheelidled, wheelDelay);\n g.zoom(\"mouse\", constrain(translate(scale(t, k), g.mouse[0], g.mouse[1]), g.extent, translateExtent));\n\n function wheelidled() {\n g.wheel = null;\n g.end();\n }\n }\n\n function mousedowned(event, ...args) {\n if (touchending || !filter.apply(this, arguments)) return;\n var currentTarget = event.currentTarget,\n g = gesture(this, args, true).event(event),\n v = select(event.view).on(\"mousemove.zoom\", mousemoved, true).on(\"mouseup.zoom\", mouseupped, true),\n p = pointer(event, currentTarget),\n x0 = event.clientX,\n y0 = event.clientY;\n\n dragDisable(event.view);\n nopropagation(event);\n g.mouse = [p, this.__zoom.invert(p)];\n interrupt(this);\n g.start();\n\n function mousemoved(event) {\n noevent(event);\n if (!g.moved) {\n var dx = event.clientX - x0, dy = event.clientY - y0;\n g.moved = dx * dx + dy * dy > clickDistance2;\n }\n g.event(event)\n .zoom(\"mouse\", constrain(translate(g.that.__zoom, g.mouse[0] = pointer(event, currentTarget), g.mouse[1]), g.extent, translateExtent));\n }\n\n function mouseupped(event) {\n v.on(\"mousemove.zoom mouseup.zoom\", null);\n dragEnable(event.view, g.moved);\n noevent(event);\n g.event(event).end();\n }\n }\n\n function dblclicked(event, ...args) {\n if (!filter.apply(this, arguments)) return;\n var t0 = this.__zoom,\n p0 = pointer(event.changedTouches ? event.changedTouches[0] : event, this),\n p1 = t0.invert(p0),\n k1 = t0.k * (event.shiftKey ? 0.5 : 2),\n t1 = constrain(translate(scale(t0, k1), p0, p1), extent.apply(this, args), translateExtent);\n\n noevent(event);\n if (duration > 0) select(this).transition().duration(duration).call(schedule, t1, p0, event);\n else select(this).call(zoom.transform, t1, p0, event);\n }\n\n function touchstarted(event, ...args) {\n if (!filter.apply(this, arguments)) return;\n var touches = event.touches,\n n = touches.length,\n g = gesture(this, args, event.changedTouches.length === n).event(event),\n started, i, t, p;\n\n nopropagation(event);\n for (i = 0; i < n; ++i) {\n t = touches[i], p = pointer(t, this);\n p = [p, this.__zoom.invert(p), t.identifier];\n if (!g.touch0) g.touch0 = p, started = true, g.taps = 1 + !!touchstarting;\n else if (!g.touch1 && g.touch0[2] !== p[2]) g.touch1 = p, g.taps = 0;\n }\n\n if (touchstarting) touchstarting = clearTimeout(touchstarting);\n\n if (started) {\n if (g.taps < 2) touchfirst = p[0], touchstarting = setTimeout(function() { touchstarting = null; }, touchDelay);\n interrupt(this);\n g.start();\n }\n }\n\n function touchmoved(event, ...args) {\n if (!this.__zooming) return;\n var g = gesture(this, args).event(event),\n touches = event.changedTouches,\n n = touches.length, i, t, p, l;\n\n noevent(event);\n for (i = 0; i < n; ++i) {\n t = touches[i], p = pointer(t, this);\n if (g.touch0 && g.touch0[2] === t.identifier) g.touch0[0] = p;\n else if (g.touch1 && g.touch1[2] === t.identifier) g.touch1[0] = p;\n }\n t = g.that.__zoom;\n if (g.touch1) {\n var p0 = g.touch0[0], l0 = g.touch0[1],\n p1 = g.touch1[0], l1 = g.touch1[1],\n dp = (dp = p1[0] - p0[0]) * dp + (dp = p1[1] - p0[1]) * dp,\n dl = (dl = l1[0] - l0[0]) * dl + (dl = l1[1] - l0[1]) * dl;\n t = scale(t, Math.sqrt(dp / dl));\n p = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2];\n l = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2];\n }\n else if (g.touch0) p = g.touch0[0], l = g.touch0[1];\n else return;\n\n g.zoom(\"touch\", constrain(translate(t, p, l), g.extent, translateExtent));\n }\n\n function touchended(event, ...args) {\n if (!this.__zooming) return;\n var g = gesture(this, args).event(event),\n touches = event.changedTouches,\n n = touches.length, i, t;\n\n nopropagation(event);\n if (touchending) clearTimeout(touchending);\n touchending = setTimeout(function() { touchending = null; }, touchDelay);\n for (i = 0; i < n; ++i) {\n t = touches[i];\n if (g.touch0 && g.touch0[2] === t.identifier) delete g.touch0;\n else if (g.touch1 && g.touch1[2] === t.identifier) delete g.touch1;\n }\n if (g.touch1 && !g.touch0) g.touch0 = g.touch1, delete g.touch1;\n if (g.touch0) g.touch0[1] = this.__zoom.invert(g.touch0[0]);\n else {\n g.end();\n // If this was a dbltap, reroute to the (optional) dblclick.zoom handler.\n if (g.taps === 2) {\n t = pointer(t, this);\n if (Math.hypot(touchfirst[0] - t[0], touchfirst[1] - t[1]) < tapDistance) {\n var p = select(this).on(\"dblclick.zoom\");\n if (p) p.apply(this, arguments);\n }\n }\n }\n }\n\n zoom.wheelDelta = function(_) {\n return arguments.length ? (wheelDelta = typeof _ === \"function\" ? _ : constant(+_), zoom) : wheelDelta;\n };\n\n zoom.filter = function(_) {\n return arguments.length ? (filter = typeof _ === \"function\" ? _ : constant(!!_), zoom) : filter;\n };\n\n zoom.touchable = function(_) {\n return arguments.length ? (touchable = typeof _ === \"function\" ? _ : constant(!!_), zoom) : touchable;\n };\n\n zoom.extent = function(_) {\n return arguments.length ? (extent = typeof _ === \"function\" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), zoom) : extent;\n };\n\n zoom.scaleExtent = function(_) {\n return arguments.length ? (scaleExtent[0] = +_[0], scaleExtent[1] = +_[1], zoom) : [scaleExtent[0], scaleExtent[1]];\n };\n\n zoom.translateExtent = function(_) {\n return arguments.length ? (translateExtent[0][0] = +_[0][0], translateExtent[1][0] = +_[1][0], translateExtent[0][1] = +_[0][1], translateExtent[1][1] = +_[1][1], zoom) : [[translateExtent[0][0], translateExtent[0][1]], [translateExtent[1][0], translateExtent[1][1]]];\n };\n\n zoom.constrain = function(_) {\n return arguments.length ? (constrain = _, zoom) : constrain;\n };\n\n zoom.duration = function(_) {\n return arguments.length ? (duration = +_, zoom) : duration;\n };\n\n zoom.interpolate = function(_) {\n return arguments.length ? (interpolate = _, zoom) : interpolate;\n };\n\n zoom.on = function() {\n var value = listeners.on.apply(listeners, arguments);\n return value === listeners ? zoom : value;\n };\n\n zoom.clickDistance = function(_) {\n return arguments.length ? (clickDistance2 = (_ = +_) * _, zoom) : Math.sqrt(clickDistance2);\n };\n\n zoom.tapDistance = function(_) {\n return arguments.length ? (tapDistance = +_, zoom) : tapDistance;\n };\n\n return zoom;\n}\n","// ============================================================================\n// Pan/Zoom Controller\n//\n// Wraps d3-zoom for viewport panning and zooming. Reworked from @xyflow/system\n// XYPanZoom (MIT License, Copyright 2019-2025 webkid GmbH) to use a callback\n// API instead of store-getters.\n// ============================================================================\n\nimport { zoom, zoomIdentity, type ZoomBehavior, type ZoomTransform } from 'd3-zoom';\nimport { select, type Selection } from 'd3-selection';\nimport 'd3-transition'; // augments Selection with .transition()\nimport type { Viewport } from './types';\n\nexport interface PanZoomOptions {\n /** Called whenever the viewport transform changes */\n onTransformChange: (viewport: Viewport) => void;\n /** Called when a user gesture starts (d3-zoom 'start' event) */\n onMoveStart?: (viewport: Viewport) => void;\n /** Called each frame during a user gesture (d3-zoom 'zoom' event, user-only) */\n onMove?: (viewport: Viewport) => void;\n /** Called when a user gesture ends (d3-zoom 'end' event) */\n onMoveEnd?: (viewport: Viewport) => void;\n /** Minimum zoom level. Default: 0.5 */\n minZoom?: number;\n /** Maximum zoom level. Default: 2 */\n maxZoom?: number;\n /** Is panning enabled? Default: true */\n pannable?: boolean;\n /** Is zooming enabled? Default: true */\n zoomable?: boolean;\n /** Viewport pan boundaries [[minX, minY], [maxX, maxY]]. Limits how far the viewport can pan. */\n translateExtent?: [[number, number], [number, number]];\n /** Return true to block pan/zoom (e.g. during animation lock). */\n isLocked?: () => boolean;\n /** CSS class name that prevents pan/zoom when on or inside event target. */\n noPanClassName?: string;\n /** CSS class name that prevents wheel zoom when on or inside event target. */\n noWheelClassName?: string;\n /** Return true when touch selection mode is active (suppress single-finger pan). */\n isTouchSelectionMode?: () => boolean;\n /** Pan viewport on wheel instead of zooming. Ctrl/Cmd+wheel zooms. Default: false */\n panOnScroll?: boolean;\n /** Which axes scroll-panning applies to. Default: 'both' */\n panOnScrollDirection?: 'both' | 'vertical' | 'horizontal';\n /** Multiplier for scroll pan sensitivity. Default: 1 */\n panOnScrollSpeed?: number;\n /** Called when pan-on-scroll needs to pan the viewport. */\n onScrollPan?: (dx: number, dy: number) => void;\n /** Zoom in on double-click. Default: true */\n zoomOnDoubleClick?: boolean;\n /** Key code that temporarily enables panning when held. Default: 'Space' */\n panActivationKeyCode?: string | null;\n /** Key code that forces zoom (overrides panOnScroll) when held with wheel. Default: 'Control'.\n * Note: Ctrl/Meta already force zoom when panOnScroll is enabled. Set to null to disable. */\n zoomActivationKeyCode?: string | null;\n /** Which mouse buttons trigger panning. true = left (0), array = specific buttons, false = none.\n * Default: true (left button) */\n panOnDrag?: boolean | number[];\n}\n\nexport interface PanZoomInstance {\n /** Set viewport programmatically */\n setViewport(viewport: Partial, options?: { duration?: number }): void;\n /** Get current d3 transform */\n getTransform(): ZoomTransform;\n /** Update options at runtime (e.g. toggle pannable/zoomable) */\n update(options: Partial): void;\n /** Clean up d3 listeners */\n destroy(): void;\n}\n\n/**\n * Attach pan/zoom behavior to a container element.\n *\n * @example\n * ```ts\n * const panZoom = createPanZoom(container, {\n * onTransformChange: (vp) => { state.viewport = vp; },\n * minZoom: 0.25,\n * maxZoom: 3,\n * });\n *\n * // Later: clean up\n * panZoom.destroy();\n * ```\n */\ninterface PanZoomFilterOptions {\n pannable: boolean;\n zoomable: boolean;\n isLocked?: () => boolean;\n noPanClassName?: string;\n noWheelClassName?: string;\n isTouchSelectionMode?: () => boolean;\n isPanKeyHeld?: () => boolean;\n panOnDrag?: boolean | number[];\n}\n\nfunction createPanZoomFilter(opts: PanZoomFilterOptions) {\n const { pannable, zoomable, isLocked, noPanClassName, noWheelClassName, isTouchSelectionMode, isPanKeyHeld, panOnDrag } = opts;\n return (event: any) => {\n if (isLocked?.()) return false;\n if (noPanClassName && (event.target as HTMLElement)?.closest?.('.' + noPanClassName)) return false;\n if (event.type === 'wheel' && noWheelClassName && (event.target as HTMLElement)?.closest?.('.' + noWheelClassName)) return false;\n if (!zoomable && event.type === 'wheel') return false;\n if (event.type === 'touchstart') {\n const isSingleTouch = !event.touches || event.touches.length < 2;\n if (isTouchSelectionMode?.() && isSingleTouch) return false;\n if (!pannable && !isPanKeyHeld?.() && isSingleTouch) return false;\n if (!zoomable && !isSingleTouch) return false;\n }\n if (event.type === 'mousedown') {\n // Pan activation key overrides all mouse button / pannable checks\n if (isPanKeyHeld?.()) return true;\n if (!pannable) return false;\n // Check panOnDrag mouse button filter\n if (Array.isArray(panOnDrag)) {\n return panOnDrag.includes(event.button);\n }\n if (panOnDrag === false) return false;\n }\n return true;\n };\n}\n\nexport function createPanZoom(\n container: HTMLElement,\n options: PanZoomOptions,\n): PanZoomInstance {\n const {\n onTransformChange,\n minZoom = 0.5,\n maxZoom = 2,\n pannable = true,\n zoomable = true,\n } = options;\n\n const sel: Selection = select(container);\n\n // ── Activation key state ──────────────────────────────────────\n let panKeyHeld = false;\n const panActivationKeyCode = options.panActivationKeyCode !== undefined ? options.panActivationKeyCode : 'Space';\n\n const onKeyDown = (e: KeyboardEvent) => {\n if (panActivationKeyCode && e.code === panActivationKeyCode) {\n panKeyHeld = true;\n container.style.cursor = 'grab';\n }\n };\n const onKeyUp = (e: KeyboardEvent) => {\n if (panActivationKeyCode && e.code === panActivationKeyCode) {\n panKeyHeld = false;\n container.style.cursor = '';\n }\n };\n const onBlur = () => {\n panKeyHeld = false;\n container.style.cursor = '';\n };\n\n if (panActivationKeyCode) {\n window.addEventListener('keydown', onKeyDown);\n window.addEventListener('keyup', onKeyUp);\n window.addEventListener('blur', onBlur);\n }\n\n const zoomBehavior: ZoomBehavior = zoom()\n .scaleExtent([minZoom, maxZoom])\n .on('start', (event) => {\n if (!event.sourceEvent) return;\n if (panKeyHeld) container.style.cursor = 'grabbing';\n const { x, y, k } = event.transform;\n options.onMoveStart?.({ x, y, zoom: k });\n })\n .on('zoom', (event) => {\n const { x, y, k } = event.transform;\n onTransformChange({ x, y, zoom: k });\n if (event.sourceEvent) {\n options.onMove?.({ x, y, zoom: k });\n }\n })\n .on('end', (event) => {\n if (!event.sourceEvent) return;\n if (panKeyHeld) container.style.cursor = 'grab';\n const { x, y, k } = event.transform;\n options.onMoveEnd?.({ x, y, zoom: k });\n });\n\n if (options.translateExtent) {\n zoomBehavior.translateExtent(options.translateExtent);\n }\n\n // Always apply filter — noPanClassName must be checked even when pan/zoom are globally enabled\n zoomBehavior.filter(createPanZoomFilter({\n pannable, zoomable,\n isLocked: options.isLocked,\n noPanClassName: options.noPanClassName,\n noWheelClassName: options.noWheelClassName,\n isTouchSelectionMode: options.isTouchSelectionMode,\n isPanKeyHeld: () => panKeyHeld,\n panOnDrag: options.panOnDrag,\n }));\n\n // Apply zoom behavior to the container\n sel.call(zoomBehavior);\n\n // Disable d3-zoom's built-in double-click zoom if configured\n if (options.zoomOnDoubleClick === false) {\n sel.on('dblclick.zoom', null);\n }\n\n // ── Pan-on-scroll wheel interceptor ──────────────────────────\n let panOnScroll = options.panOnScroll ?? false;\n let panOnScrollDirection = options.panOnScrollDirection ?? 'both';\n let panOnScrollSpeed = options.panOnScrollSpeed ?? 1;\n\n // Zoom activation key state (for overriding panOnScroll)\n let zoomKeyHeld = false;\n const zoomActivationKeyCode = options.zoomActivationKeyCode !== undefined ? options.zoomActivationKeyCode : null;\n\n const onZoomKeyDown = (e: KeyboardEvent) => {\n if (zoomActivationKeyCode && e.code === zoomActivationKeyCode) zoomKeyHeld = true;\n };\n const onZoomKeyUp = (e: KeyboardEvent) => {\n if (zoomActivationKeyCode && e.code === zoomActivationKeyCode) zoomKeyHeld = false;\n };\n const onZoomBlur = () => { zoomKeyHeld = false; };\n\n if (zoomActivationKeyCode) {\n window.addEventListener('keydown', onZoomKeyDown);\n window.addEventListener('keyup', onZoomKeyUp);\n window.addEventListener('blur', onZoomBlur);\n }\n\n const wheelHandler = (event: WheelEvent) => {\n if (options.isLocked?.()) return;\n\n const isModifier = event.ctrlKey || event.metaKey || zoomKeyHeld;\n const shouldPan = panOnScroll ? !isModifier : event.shiftKey;\n\n if (!shouldPan) return;\n\n event.preventDefault();\n event.stopPropagation();\n\n const speed = panOnScrollSpeed;\n let dx = 0;\n let dy = 0;\n\n if (panOnScrollDirection !== 'horizontal') {\n dy = -event.deltaY * speed;\n }\n if (panOnScrollDirection !== 'vertical') {\n dx = -event.deltaX * speed;\n // When shift is held and deltaX is 0, browsers put the value in deltaY\n if (event.shiftKey && event.deltaX === 0 && panOnScrollDirection === 'both') {\n dx = -event.deltaY * speed;\n dy = 0;\n }\n }\n\n options.onScrollPan?.(dx, dy);\n };\n\n container.addEventListener('wheel', wheelHandler, { passive: false, capture: true });\n\n return {\n setViewport(viewport, opts) {\n const duration = opts?.duration ?? 0;\n\n const transform = zoomIdentity\n .translate(viewport.x ?? 0, viewport.y ?? 0)\n .scale(viewport.zoom ?? 1);\n\n if (duration > 0) {\n sel.transition().duration(duration).call(zoomBehavior.transform, transform);\n } else {\n sel.call(zoomBehavior.transform, transform);\n }\n },\n\n getTransform() {\n // d3-zoom stores the current transform on the element\n const el = container as any;\n return el.__zoom ?? zoomIdentity;\n },\n\n update(newOptions) {\n if (newOptions.minZoom !== undefined || newOptions.maxZoom !== undefined) {\n zoomBehavior.scaleExtent([\n newOptions.minZoom ?? minZoom,\n newOptions.maxZoom ?? maxZoom,\n ]);\n }\n\n if (newOptions.pannable !== undefined || newOptions.zoomable !== undefined) {\n const pan = newOptions.pannable ?? pannable;\n const zm = newOptions.zoomable ?? zoomable;\n zoomBehavior.filter(createPanZoomFilter({\n pannable: pan, zoomable: zm,\n isLocked: options.isLocked,\n noPanClassName: options.noPanClassName,\n noWheelClassName: options.noWheelClassName,\n isTouchSelectionMode: options.isTouchSelectionMode,\n isPanKeyHeld: () => panKeyHeld,\n panOnDrag: options.panOnDrag,\n }));\n }\n\n if (newOptions.panOnScroll !== undefined) {\n panOnScroll = newOptions.panOnScroll;\n }\n if (newOptions.panOnScrollDirection !== undefined) {\n panOnScrollDirection = newOptions.panOnScrollDirection;\n }\n if (newOptions.panOnScrollSpeed !== undefined) {\n panOnScrollSpeed = newOptions.panOnScrollSpeed;\n }\n },\n\n destroy() {\n container.removeEventListener('wheel', wheelHandler, { capture: true } as EventListenerOptions);\n if (panActivationKeyCode) {\n window.removeEventListener('keydown', onKeyDown);\n window.removeEventListener('keyup', onKeyUp);\n window.removeEventListener('blur', onBlur);\n }\n if (zoomActivationKeyCode) {\n window.removeEventListener('keydown', onZoomKeyDown);\n window.removeEventListener('keyup', onZoomKeyUp);\n window.removeEventListener('blur', onZoomBlur);\n }\n sel.on('.zoom', null);\n },\n };\n}\n","// ============================================================================\n// Geometry Utilities\n//\n// Coordinate transforms between screen space and flow space, bounding box\n// calculations for fitView, and DOM measurement helpers.\n// ============================================================================\n\nimport type { XYPosition, Viewport, Rect, Dimensions, FlowNode } from './types';\n\n/**\n * Convert screen (client) coordinates to flow coordinates.\n */\nexport function screenToFlowPosition(\n screenX: number,\n screenY: number,\n viewport: Viewport,\n containerRect: DOMRect,\n): XYPosition {\n return {\n x: (screenX - containerRect.left - viewport.x) / viewport.zoom,\n y: (screenY - containerRect.top - viewport.y) / viewport.zoom,\n };\n}\n\n/**\n * Convert flow coordinates to screen (client) coordinates.\n */\nexport function flowToScreenPosition(\n flowX: number,\n flowY: number,\n viewport: Viewport,\n containerRect: DOMRect,\n): XYPosition {\n return {\n x: flowX * viewport.zoom + viewport.x + containerRect.left,\n y: flowY * viewport.zoom + viewport.y + containerRect.top,\n };\n}\n\n/**\n * Calculate the bounding box that contains all given nodes.\n */\nexport const DEFAULT_NODE_WIDTH = 150;\nexport const DEFAULT_NODE_HEIGHT = 50;\n\n/**\n * Compute the axis-aligned bounding box of a rectangle rotated around its center.\n * Returns the expanded AABB in the same coordinate space as the input.\n */\nexport function getRotatedBounds(\n x: number,\n y: number,\n w: number,\n h: number,\n rotation: number,\n): Rect {\n if (rotation % 360 === 0) return { x, y, width: w, height: h };\n\n const rad = (rotation * Math.PI) / 180;\n const cos = Math.abs(Math.cos(rad));\n const sin = Math.abs(Math.sin(rad));\n const newW = w * cos + h * sin;\n const newH = w * sin + h * cos;\n const cx = x + w / 2;\n const cy = y + h / 2;\n\n return { x: cx - newW / 2, y: cy - newH / 2, width: newW, height: newH };\n}\n\nexport function getNodesBounds(\n nodes: FlowNode[],\n globalOrigin?: [number, number],\n): Rect {\n if (nodes.length === 0) {\n return { x: 0, y: 0, width: 0, height: 0 };\n }\n\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const node of nodes) {\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n\n const vp = getNodeVisualPosition(node, globalOrigin);\n const bounds = node.rotation\n ? getRotatedBounds(vp.x, vp.y, w, h, node.rotation)\n : { x: vp.x, y: vp.y, width: w, height: h };\n\n minX = Math.min(minX, bounds.x);\n minY = Math.min(minY, bounds.y);\n maxX = Math.max(maxX, bounds.x + bounds.width);\n maxY = Math.max(maxY, bounds.y + bounds.height);\n }\n\n return {\n x: minX,\n y: minY,\n width: maxX - minX,\n height: maxY - minY,\n };\n}\n\n/**\n * Return all nodes whose bounding box intersects the given rectangle.\n * Uses AABB intersection with DEFAULT_NODE_WIDTH/HEIGHT fallback.\n */\nexport function getNodesInRect(\n nodes: FlowNode[],\n rect: Rect,\n globalOrigin?: [number, number],\n): FlowNode[] {\n const rectRight = rect.x + rect.width;\n const rectBottom = rect.y + rect.height;\n\n return nodes.filter((node) => {\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n\n const vp = getNodeVisualPosition(node, globalOrigin);\n const bounds = node.rotation\n ? getRotatedBounds(vp.x, vp.y, w, h, node.rotation)\n : { x: vp.x, y: vp.y, width: w, height: h };\n\n const nodeRight = bounds.x + bounds.width;\n const nodeBottom = bounds.y + bounds.height;\n\n return !(\n nodeRight < rect.x ||\n bounds.x > rectRight ||\n nodeBottom < rect.y ||\n bounds.y > rectBottom\n );\n });\n}\n\n/**\n * Return all nodes whose bounding box is fully contained within the given rectangle.\n * Uses DEFAULT_NODE_WIDTH/HEIGHT fallback for nodes without measured dimensions.\n */\nexport function getNodesFullyInRect(\n nodes: FlowNode[],\n rect: Rect,\n globalOrigin?: [number, number],\n): FlowNode[] {\n const rectRight = rect.x + rect.width;\n const rectBottom = rect.y + rect.height;\n\n return nodes.filter((node) => {\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n\n const vp = getNodeVisualPosition(node, globalOrigin);\n const bounds = node.rotation\n ? getRotatedBounds(vp.x, vp.y, w, h, node.rotation)\n : { x: vp.x, y: vp.y, width: w, height: h };\n\n return (\n bounds.x >= rect.x &&\n bounds.y >= rect.y &&\n bounds.x + bounds.width <= rectRight &&\n bounds.y + bounds.height <= rectBottom\n );\n });\n}\n\n/**\n * Calculate the viewport transform needed to fit a rect into a container.\n */\nexport function getViewportForBounds(\n bounds: Rect,\n containerWidth: number,\n containerHeight: number,\n minZoom: number,\n maxZoom: number,\n padding: number = 0.1,\n): Viewport {\n const effectiveWidth = Math.max(bounds.width, 1);\n const effectiveHeight = Math.max(bounds.height, 1);\n\n const paddedWidth = effectiveWidth * (1 + padding);\n const paddedHeight = effectiveHeight * (1 + padding);\n\n const zoomX = containerWidth / paddedWidth;\n const zoomY = containerHeight / paddedHeight;\n const zoom = Math.min(Math.max(Math.min(zoomX, zoomY), minZoom), maxZoom);\n\n const boundsCenter = { x: bounds.x + effectiveWidth / 2, y: bounds.y + effectiveHeight / 2 };\n const x = containerWidth / 2 - boundsCenter.x * zoom;\n const y = containerHeight / 2 - boundsCenter.y * zoom;\n\n return { x, y, zoom };\n}\n\n// ─── Viewport Culling Utilities ─────────────────────────────────────────────\n\n/** Axis-aligned bounds (min/max form, not x/y/width/height) */\nexport interface Bounds {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n}\n\n/**\n * Compute the visible bounds in flow coordinates given viewport state,\n * container dimensions, and a buffer margin.\n */\nexport function getVisibleBounds(\n viewport: Viewport,\n containerWidth: number,\n containerHeight: number,\n buffer: number,\n): Bounds {\n const invZoom = 1 / viewport.zoom;\n\n return {\n minX: (0 - viewport.x) * invZoom - buffer,\n minY: (0 - viewport.y) * invZoom - buffer,\n maxX: (containerWidth - viewport.x) * invZoom + buffer,\n maxY: (containerHeight - viewport.y) * invZoom + buffer,\n };\n}\n\n/**\n * Grid-based spatial index for fast viewport queries.\n *\n * Flow space is divided into cells of `cellSize` pixels.\n * Each cell holds a Set of node IDs whose bounding boxes overlap that cell.\n * Viewport queries find all cells overlapping the query bounds, then\n * union their node sets — O(cells) instead of O(nodes).\n */\nexport class SpatialGrid {\n private _cellSize: number;\n private _cells = new Map>();\n private _nodeCells = new Map();\n\n constructor(cellSize: number = 300) {\n this._cellSize = cellSize;\n }\n\n private _cellKey(cx: number, cy: number): string {\n return `${cx},${cy}`;\n }\n\n private _getCellRange(\n x: number,\n y: number,\n w: number,\n h: number,\n ): { minCX: number; minCY: number; maxCX: number; maxCY: number } {\n return {\n minCX: Math.floor(x / this._cellSize),\n minCY: Math.floor(y / this._cellSize),\n maxCX: Math.floor((x + w) / this._cellSize),\n maxCY: Math.floor((y + h) / this._cellSize),\n };\n }\n\n insert(id: string, x: number, y: number, w: number, h: number): void {\n this.remove(id);\n const { minCX, minCY, maxCX, maxCY } = this._getCellRange(x, y, w, h);\n const keys: string[] = [];\n for (let cx = minCX; cx <= maxCX; cx++) {\n for (let cy = minCY; cy <= maxCY; cy++) {\n const key = this._cellKey(cx, cy);\n keys.push(key);\n let cell = this._cells.get(key);\n if (!cell) {\n cell = new Set();\n this._cells.set(key, cell);\n }\n cell.add(id);\n }\n }\n this._nodeCells.set(id, keys);\n }\n\n remove(id: string): void {\n const keys = this._nodeCells.get(id);\n if (!keys) {\n return;\n }\n for (const key of keys) {\n const cell = this._cells.get(key);\n if (cell) {\n cell.delete(id);\n if (cell.size === 0) {\n this._cells.delete(key);\n }\n }\n }\n this._nodeCells.delete(id);\n }\n\n update(id: string, x: number, y: number, w: number, h: number): void {\n this.insert(id, x, y, w, h);\n }\n\n query(bounds: Bounds): Set {\n const { minCX, minCY, maxCX, maxCY } = this._getCellRange(\n bounds.minX,\n bounds.minY,\n bounds.maxX - bounds.minX,\n bounds.maxY - bounds.minY,\n );\n const result = new Set();\n for (let cx = minCX; cx <= maxCX; cx++) {\n for (let cy = minCY; cy <= maxCY; cy++) {\n const cell = this._cells.get(this._cellKey(cx, cy));\n if (cell) {\n for (const id of cell) {\n result.add(id);\n }\n }\n }\n }\n return result;\n }\n\n clear(): void {\n this._cells.clear();\n this._nodeCells.clear();\n }\n\n get size(): number {\n return this._nodeCells.size;\n }\n}\n\n// ─── Node Origin Utilities ──────────────────────────────────────────────────\n\n/**\n * Compute the pixel offset from a node's anchor point to its visual top-left corner.\n * Returns {x: 0, y: 0} for the default [0, 0] origin.\n */\nexport function getOriginOffset(\n origin: [number, number] | undefined,\n width: number,\n height: number,\n): XYPosition {\n if (!origin || (origin[0] === 0 && origin[1] === 0)) return { x: 0, y: 0 };\n return { x: -width * origin[0], y: -height * origin[1] };\n}\n\n/**\n * Get the visual top-left position of a node, accounting for nodeOrigin.\n * The node's `position` is its anchor point; this returns where the DOM\n * element's top-left corner should be placed.\n */\nexport function getNodeVisualPosition(\n node: FlowNode,\n globalOrigin?: [number, number],\n): XYPosition {\n if (!node.position) return { x: 0, y: 0 };\n const origin = node.nodeOrigin ?? globalOrigin ?? [0, 0];\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n return {\n x: node.position.x - w * origin[0],\n y: node.position.y - h * origin[1],\n };\n}\n","// ============================================================================\n// Debug Logger\n//\n// Centralized debug logging for AlpineFlow. Enable via the `debug` option\n// on the flowCanvas config. Logs are grouped by category for easy filtering\n// in the browser console.\n//\n// TODO: Add visual debug panel overlay (toggle with debug: true) showing live\n// node/edge state as a JSON tree or table. Also consider a profiling mode that\n// tracks animation frame times, flush counts, and edge path recalculations.\n// ============================================================================\n\nlet enabled = false;\n\nexport function setDebugEnabled(value: boolean): void {\n enabled = value;\n}\n\nexport function isDebugEnabled(): boolean {\n return enabled;\n}\n\n/**\n * Log a debug message to the console when debug mode is enabled.\n *\n * @param category - Log category (e.g. 'drag', 'viewport', 'edge', 'connection')\n * @param message - Human-readable message\n * @param data - Optional data to log alongside the message\n */\nexport function debug(category: string, message: string, data?: any): void {\n if (!enabled) return;\n\n const prefix = `%c[AlpineFlow:${category}]`;\n const style = categoryStyle(category);\n\n if (data !== undefined) {\n console.log(prefix, style, message, data);\n } else {\n console.log(prefix, style, message);\n }\n}\n\nfunction categoryStyle(category: string): string {\n const colors: Record = {\n init: '#4ade80',\n destroy: '#f87171',\n drag: '#60a5fa',\n viewport: '#a78bfa',\n edge: '#fb923c',\n connection: '#f472b6',\n selection: '#facc15',\n event: '#38bdf8',\n store: '#2dd4bf',\n resize: '#c084fc',\n collapse: '#c084fc',\n animate: '#34d399',\n layout: '#818cf8',\n particle: '#f472b6',\n history: '#fbbf24',\n clipboard: '#94a3b8',\n };\n\n const color = colors[category] ?? '#94a3b8';\n return `color: ${color}; font-weight: bold`;\n}\n","// ============================================================================\n// Shared Constants\n//\n// Centralizes magic values used across multiple files so they stay consistent\n// and can be tuned from a single location.\n// ============================================================================\n\n/** Stroke color applied to selected edges and active connection lines. */\nexport const CONNECTION_ACTIVE_COLOR = '#64748b';\n\n/** Default stroke color for edge paths. */\nexport const DEFAULT_STROKE_COLOR = '#d4d4d8';\n\n/** Stroke color applied to invalid connection lines. */\nexport const CONNECTION_INVALID_COLOR = '#ef4444';\n\n/** Stroke width for temporary connection drag lines. */\nexport const TEMP_LINE_STROKE_WIDTH = '2';\n\n/** Dash pattern for temporary connection drag lines. */\nexport const TEMP_LINE_DASH_PATTERN = '6 3';\n\n/** Multiplier for zoom-in / zoom-out steps. */\nexport const ZOOM_STEP_FACTOR = 1.2;\n\n/** Default padding ratio for fitView and layout operations. */\nexport const DEFAULT_FIT_PADDING = 0.2;\n\n/** Minimum pointer movement (in px) before a click is treated as a drag. */\nexport const DRAG_THRESHOLD = 5;\n\n/** Default snap radius (in flow-space px) for edge reconnection hit-detection. */\nexport const DEFAULT_RECONNECT_SNAP_RADIUS = 25;\n","// ============================================================================\n// FlowHistory — Snapshot-based Undo/Redo\n//\n// Maintains past/future stacks of { nodes, edges } snapshots serialized as\n// JSON strings. String storage roughly halves retained memory vs keeping 50\n// live object graphs, makes duplicate-state dedup an O(1) string compare, and\n// costs nothing extra — `JSON.stringify` was already paid on every capture.\n// ============================================================================\n\nimport type { FlowNode, FlowEdge } from './types';\n\nexport interface HistorySnapshot {\n nodes: FlowNode[];\n edges: FlowEdge[];\n}\n\n/**\n * Snapshots are stored as JSON strings. Serialization uses JSON (not\n * structuredClone) because Alpine.js wraps reactive arrays/objects in Proxy,\n * which structuredClone cannot handle (throws DataCloneError).\n *\n * **Warning:** Only JSON-serializable data is preserved. Functions, Symbols,\n * Maps, Sets, and `undefined` values stored in `node.data` or `edge.data`\n * will be silently dropped during undo/redo.\n */\nexport class FlowHistory {\n private past: string[] = [];\n private future: string[] = [];\n private maxSize: number;\n private _suspendDepth = 0;\n\n constructor(maxSize = 50) {\n this.maxSize = maxSize;\n }\n\n suspend(): void {\n this._suspendDepth++;\n }\n\n resume(): void {\n if (this._suspendDepth > 0) this._suspendDepth--;\n }\n\n capture(current: HistorySnapshot): void {\n if (this._suspendDepth > 0) return;\n this.commit(JSON.stringify(current));\n }\n\n /** Serialize without pushing — pair with commit() for deferred capture. */\n snapshot(current: HistorySnapshot): string {\n return JSON.stringify(current);\n }\n\n /** Push a snapshot taken earlier via snapshot(). Dedups against the top of the stack. */\n commit(snapshotJson: string): void {\n if (this._suspendDepth > 0) return;\n if (this.past.length > 0 && this.past[this.past.length - 1] === snapshotJson) return;\n this.past.push(snapshotJson);\n this.future = [];\n if (this.past.length > this.maxSize) this.past.shift();\n }\n\n undo(current: HistorySnapshot): HistorySnapshot | null {\n if (this.past.length === 0) return null;\n this.future.push(JSON.stringify(current));\n return JSON.parse(this.past.pop()!);\n }\n\n redo(current: HistorySnapshot): HistorySnapshot | null {\n if (this.future.length === 0) return null;\n this.past.push(JSON.stringify(current));\n return JSON.parse(this.future.pop()!);\n }\n\n get canUndo(): boolean { return this.past.length > 0; }\n get canRedo(): boolean { return this.future.length > 0; }\n}\n","// ============================================================================\n// AnimationEngine — Shared requestAnimationFrame loop\n//\n// Manages a single rAF loop that ticks all active animation callbacks in the\n// same frame. Replaces per-timeline setTimeout loops for better performance\n// and frame coherence.\n// ============================================================================\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\n/**\n * Callback invoked each animation frame.\n * @param elapsed - Milliseconds since this callback was activated.\n * @returns `true` to signal completion and unregister, or `void`/`false` to continue.\n */\nexport type TickCallback = (elapsed: number) => boolean | void;\n\n/** Handle returned by `register()` — call `stop()` to unregister the callback. */\nexport interface EngineHandle {\n stop(): void;\n}\n\n/** Injectable frame scheduler — allows tests to swap in fake timers. */\nexport interface FrameScheduler {\n request: (cb: FrameRequestCallback) => number;\n cancel: (id: number) => void;\n}\n\n// ── Internal entry type ──────────────────────────────────────────────────────\n\ninterface CallbackEntry {\n callback: TickCallback;\n startTime: number; // performance.now() when activated (after delay)\n delay: number; // delay before first invocation\n registeredAt: number; // performance.now() when registered\n activated: boolean; // true once delay has elapsed\n removed: boolean; // true when stop() called or callback returned true\n}\n\n// ── Default scheduler ────────────────────────────────────────────────────────\n\nconst FRAME_INTERVAL = 16; // ~60fps fallback\n\nfunction createDefaultScheduler(): FrameScheduler {\n if (typeof requestAnimationFrame === 'function') {\n return {\n request: (cb) => requestAnimationFrame(cb),\n cancel: (id) => cancelAnimationFrame(id),\n };\n }\n // Node / non-browser fallback\n return {\n request: (cb) => setTimeout(() => cb(performance.now()), FRAME_INTERVAL) as unknown as number,\n cancel: (id) => clearTimeout(id),\n };\n}\n\n// ── Post-tick entry type ─────────────────────────────────────────────────────\n\ninterface PostTickEntry {\n callback: (frameTime: number) => void;\n removed: boolean;\n keepAlive: boolean;\n}\n\n// ── AnimationEngine ──────────────────────────────────────────────────────────\n\nexport class AnimationEngine {\n private _scheduler: FrameScheduler = createDefaultScheduler();\n private _entries: CallbackEntry[] = [];\n private _postTickCallbacks: PostTickEntry[] = [];\n private _frameId: number | null = null;\n private _running = false;\n\n /** True when the rAF loop is running. */\n get active(): boolean {\n return this._running;\n }\n\n /** Replace the frame scheduler (useful for tests with fake timers). */\n setScheduler(scheduler: FrameScheduler): void {\n this._scheduler = scheduler;\n }\n\n /**\n * Register a tick callback.\n * @param callback - Called each frame with elapsed ms since activation.\n * @param delay - Optional delay (ms) before first invocation, measured from rAF frames.\n * @returns Handle with a `stop()` method to unregister.\n */\n register(callback: TickCallback, delay = 0): EngineHandle {\n const entry: CallbackEntry = {\n callback,\n startTime: 0,\n delay,\n registeredAt: performance.now(),\n activated: delay <= 0,\n removed: false,\n };\n\n if (entry.activated) {\n entry.startTime = performance.now();\n }\n\n this._entries.push(entry);\n\n if (!this._running) {\n this._start();\n }\n\n const handle: EngineHandle = {\n stop: () => {\n entry.removed = true;\n },\n };\n\n return handle;\n }\n\n /**\n * Register a post-tick callback, fired after all regular tick callbacks each frame.\n * @param callback - Called with the frame timestamp (same `now` value passed to `_tick`).\n * @param options - Optional settings. `keepAlive: true` keeps the engine loop running\n * even when no regular callbacks are registered (useful for recorders that need every frame).\n * @returns Handle with a `stop()` method to unregister.\n */\n onPostTick(callback: (frameTime: number) => void, options?: { keepAlive?: boolean }): EngineHandle {\n const entry: PostTickEntry = { callback, removed: false, keepAlive: options?.keepAlive ?? false };\n this._postTickCallbacks.push(entry);\n\n // If keepAlive is set and the engine isn't running, start it now\n if (entry.keepAlive && !this._running) {\n this._start();\n }\n\n return {\n stop: () => {\n entry.removed = true;\n },\n };\n }\n\n // ── Internal: loop management ──────────────────────────────────────\n\n private _start(): void {\n if (this._running) return;\n this._running = true;\n this._scheduleFrame();\n }\n\n private _stop(): void {\n if (!this._running) return;\n this._running = false;\n if (this._frameId !== null) {\n this._scheduler.cancel(this._frameId);\n this._frameId = null;\n }\n }\n\n private _scheduleFrame(): void {\n this._frameId = this._scheduler.request((now) => {\n this._tick(now);\n });\n }\n\n private _tick(now: number): void {\n const entries = this._entries.slice();\n // Process all entries\n for (const entry of entries) {\n if (entry.removed) continue;\n\n // Check delay\n if (!entry.activated) {\n const waitedMs = now - entry.registeredAt;\n if (waitedMs < entry.delay) continue;\n\n // Activate: set startTime to now so elapsed starts from 0\n entry.activated = true;\n entry.startTime = now;\n }\n\n const elapsed = now - entry.startTime;\n const done = entry.callback(elapsed);\n if (done === true) {\n entry.removed = true;\n }\n }\n\n // Prune removed entries\n this._entries = this._entries.filter((e) => !e.removed);\n\n // Fire post-tick callbacks (only when the engine is actively running frames)\n for (const postEntry of this._postTickCallbacks) {\n if (!postEntry.removed) {\n postEntry.callback(now);\n }\n }\n\n // Prune removed post-tick callbacks\n this._postTickCallbacks = this._postTickCallbacks.filter((e) => !e.removed);\n\n // Auto-stop if no callbacks remain and no keepAlive postTick observers\n const hasKeepAlive = this._postTickCallbacks.some((e) => !e.removed && e.keepAlive);\n if (this._entries.length === 0 && !hasKeepAlive) {\n this._stop();\n return;\n }\n\n // Schedule next frame\n this._scheduleFrame();\n }\n}\n\n// ── Module-level singleton ───────────────────────────────────────────────────\n\nexport const engine = new AnimationEngine();\n","// ============================================================================\n// Easing — d3-ease name-to-function mapping\n//\n// Maps friendly preset names to d3-ease functions. Custom easing functions\n// are passed through unchanged. Falls back to easeInOut when unspecified.\n// ============================================================================\n\nimport {\n easeLinear,\n easeQuadIn,\n easeQuadOut,\n easeQuadInOut,\n easeCubicIn,\n easeCubicOut,\n easeCubicInOut,\n easeCircleIn,\n easeCircleOut,\n easeCircleInOut,\n easeSinIn,\n easeSinOut,\n easeSinInOut,\n easeExpIn,\n easeExpOut,\n easeExpInOut,\n easeBounceIn,\n easeBounceOut,\n easeBounceInOut,\n easeElasticIn,\n easeElasticOut,\n easeElasticInOut,\n easeBackIn,\n easeBackOut,\n easeBackInOut,\n} from 'd3-ease';\n\nexport type EasingName =\n | 'linear'\n | 'easeIn' | 'easeOut' | 'easeInOut'\n | 'easeCubicIn' | 'easeCubicOut' | 'easeCubicInOut'\n | 'easeCircIn' | 'easeCircOut' | 'easeCircInOut'\n | 'easeSinIn' | 'easeSinOut' | 'easeSinInOut'\n | 'easeExpoIn' | 'easeExpoOut' | 'easeExpoInOut'\n | 'easeBounce' | 'easeBounceIn' | 'easeBounceInOut'\n | 'easeElastic' | 'easeElasticIn' | 'easeElasticInOut'\n | 'easeBack' | 'easeBackIn' | 'easeBackOut';\n\nexport type EasingFn = (t: number) => number;\n\n// Abbreviated names (easeCirc*, easeExpo*) map to d3-ease's easeCircle*, easeExp*\n// for conciseness. Kept short since these appear in user-facing config.\nconst EASING_MAP: Record = {\n linear: easeLinear,\n easeIn: easeQuadIn,\n easeOut: easeQuadOut,\n easeInOut: easeQuadInOut,\n easeCubicIn: easeCubicIn,\n easeCubicOut: easeCubicOut,\n easeCubicInOut: easeCubicInOut,\n easeCircIn: easeCircleIn,\n easeCircOut: easeCircleOut,\n easeCircInOut: easeCircleInOut,\n easeSinIn: easeSinIn,\n easeSinOut: easeSinOut,\n easeSinInOut: easeSinInOut,\n easeExpoIn: easeExpIn,\n easeExpoOut: easeExpOut,\n easeExpoInOut: easeExpInOut,\n easeBounce: easeBounceOut,\n easeBounceIn: easeBounceIn,\n easeBounceInOut: easeBounceInOut,\n easeElastic: easeElasticOut,\n easeElasticIn: easeElasticIn,\n easeElasticInOut: easeElasticInOut,\n easeBack: easeBackInOut,\n easeBackIn: easeBackIn,\n easeBackOut: easeBackOut,\n};\n\n/**\n * Check whether reduced motion should be applied given a user preference.\n *\n * - `false` — always animate (ignore OS setting)\n * - `true` — always skip animation\n * - `'auto'` / `undefined` — honour the OS `prefers-reduced-motion` media query\n *\n * Shared by FlowTimeline and the canvas-animation mixin so both follow\n * the same logic without diverging.\n */\nexport function checkReducedMotion(preference: boolean | 'auto' | undefined): boolean {\n const pref = preference ?? 'auto';\n if (pref === false) return false;\n if (pref === true) return true;\n return typeof globalThis !== 'undefined'\n && globalThis.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches === true;\n}\n\n/** Resolve an easing name or custom function to a callable (t: number) => number. */\nexport function resolveEasing(easing?: EasingName | EasingFn): EasingFn {\n if (typeof easing === 'function') {\n return easing;\n }\n return EASING_MAP[easing ?? 'easeInOut'];\n}\n","// ============================================================================\n// Interpolators — Property-specific value interpolation\n//\n// Provides numeric lerp, color interpolation (via d3-interpolateRgb), and\n// style string parsing/interpolation for animated property transitions.\n// ============================================================================\n\nimport { interpolateRgb } from 'd3-interpolate';\nimport type { Viewport } from '../core/types';\n\n/** Linear interpolation between two numbers. */\nexport function lerpNumber(a: number, b: number, t: number): number {\n return a + (b - a) * t;\n}\n\n/** Interpolate between two CSS color strings using d3-interpolateRgb. */\nexport function interpolateColor(a: string, b: string, t: number): string {\n return interpolateRgb(a, b)(t);\n}\n\n/** Parse a style string (\"key: value; ...\") into a Record, or pass through a Record. */\nexport function parseStyle(style: string | Record): Record {\n if (typeof style !== 'string') {\n return style;\n }\n if (!style.trim()) {\n return {};\n }\n const result: Record = {};\n for (const part of style.split(';')) {\n const trimmed = part.trim();\n if (!trimmed) continue;\n const colonIndex = trimmed.indexOf(':');\n if (colonIndex === -1) continue;\n const key = trimmed.slice(0, colonIndex).trim();\n const value = trimmed.slice(colonIndex + 1).trim();\n result[key] = value;\n }\n return result;\n}\n\n/** Regex matching a number optionally followed by a CSS unit (px, em, rem, %, etc.) */\nconst NUMERIC_RE = /^(-?\\d+\\.?\\d*)(px|em|rem|%|vh|vw|pt|cm|mm|in|ex|ch)?$/;\n\n/** Regex to detect CSS color values. */\nconst COLOR_RE = /^(#|rgb|hsl)/;\n\n/** Interpolate between two parsed style records, per-property. */\nexport function interpolateStyle(\n from: Record,\n to: Record,\n t: number,\n): Record {\n const result: Record = {};\n const allKeys = new Set([...Object.keys(from), ...Object.keys(to)]);\n\n for (const key of allKeys) {\n const a = from[key];\n const b = to[key];\n\n // Property only in one side — snap to target\n if (a === undefined) {\n result[key] = b;\n continue;\n }\n if (b === undefined) {\n result[key] = a;\n continue;\n }\n\n // Try numeric interpolation\n const matchA = NUMERIC_RE.exec(a);\n const matchB = NUMERIC_RE.exec(b);\n if (matchA && matchB) {\n const numA = parseFloat(matchA[1]);\n const numB = parseFloat(matchB[1]);\n const unit = matchB[2] ?? '';\n const lerped = lerpNumber(numA, numB, t);\n result[key] = unit ? `${lerped}${unit}` : String(lerped);\n continue;\n }\n\n // Try color interpolation\n if (COLOR_RE.test(a) && COLOR_RE.test(b)) {\n result[key] = interpolateColor(a, b, t);\n continue;\n }\n\n // Non-interpolatable: instant swap at t >= 0.5\n result[key] = t < 0.5 ? a : b;\n }\n\n return result;\n}\n\n/** Interpolate between two viewports (x, y, zoom) with optional zoom clamping. */\nexport function lerpViewport(\n from: Viewport,\n to: Viewport,\n t: number,\n clamp?: { minZoom?: number; maxZoom?: number },\n): Viewport {\n let zoom = lerpNumber(from.zoom, to.zoom, t);\n if (clamp?.minZoom !== undefined) zoom = Math.max(zoom, clamp.minZoom);\n if (clamp?.maxZoom !== undefined) zoom = Math.min(zoom, clamp.maxZoom);\n return {\n x: lerpNumber(from.x, to.x, t),\n y: lerpNumber(from.y, to.y, t),\n zoom,\n };\n}\n","import type { StopOptions } from '../core/types';\n\nexport interface Taggable {\n _tags?: string[];\n stop(options?: StopOptions): void;\n pause(): void;\n resume(): void;\n readonly isFinished: boolean;\n}\n\nexport class HandleRegistry {\n private _handles = new Set();\n\n register(handle: Taggable): void {\n this._handles.add(handle);\n }\n\n unregister(handle: Taggable): void {\n this._handles.delete(handle);\n }\n\n getHandles(filter?: { tag?: string; tags?: string[] }): Taggable[] {\n const handles = [...this._handles];\n if (!filter?.tag && !filter?.tags?.length) {\n return handles;\n }\n const tagSet = new Set();\n if (filter.tag) {\n tagSet.add(filter.tag);\n }\n if (filter.tags) {\n filter.tags.forEach((t) => tagSet.add(t));\n }\n return handles.filter((h) => h._tags?.some((t) => tagSet.has(t)) ?? false);\n }\n\n cancelAll(filter: { tag?: string; tags?: string[] }, options?: StopOptions): void {\n for (const handle of this.getHandles(filter)) {\n if (!handle.isFinished) {\n handle.stop(options);\n }\n }\n }\n\n pauseAll(filter: { tag?: string; tags?: string[] }): void {\n for (const handle of this.getHandles(filter)) {\n if (!handle.isFinished) {\n handle.pause();\n }\n }\n }\n\n resumeAll(filter: { tag?: string; tags?: string[] }): void {\n for (const handle of this.getHandles(filter)) {\n handle.resume();\n }\n }\n\n clear(): void {\n this._handles.clear();\n }\n\n get size(): number {\n return this._handles.size;\n }\n}\n","// ============================================================================\n// Transaction — Groups animation handles with rollback capability\n//\n// A Transaction captures property snapshots lazily (first touch per key) and\n// tracks all AnimationHandles created while it is active. On rollback, all\n// tracked handles are frozen and all touched properties revert to their\n// pre-transaction values.\n// ============================================================================\n\nimport type { AnimationHandle } from './animator';\n\nexport type TransactionState = 'active' | 'committed' | 'rolled-back';\n\nexport class Transaction {\n private _handles: AnimationHandle[] = [];\n private _state: TransactionState = 'active';\n private _propertySnapshots = new Map void }>();\n private _resolveFinished!: () => void;\n private _onAfterRollback: ((revertedKeys: string[]) => void) | null = null;\n readonly finished: Promise;\n\n constructor() {\n this.finished = new Promise((resolve) => {\n this._resolveFinished = resolve;\n });\n }\n\n /**\n * @internal\n * Register a callback fired after `rollback()` has reverted all captured\n * properties. Receives the list of keys that were reverted. Used by the\n * canvas layer to flush DOM for the affected nodes — without it, raw-state\n * writes done outside the animation rAF loop never reach the DOM.\n *\n * Not part of the public API — do not call from application code. Canvas\n * wiring is managed internally in `$flow.transaction()`.\n */\n onAfterRollback(cb: (revertedKeys: string[]) => void): void {\n this._onAfterRollback = cb;\n }\n\n get state(): TransactionState { return this._state; }\n get handles(): ReadonlyArray { return this._handles; }\n\n /** Called by the Animator when a new handle is created inside this transaction. */\n trackHandle(handle: AnimationHandle): void {\n if (this._state !== 'active') { return; }\n this._handles.push(handle);\n }\n\n /**\n * Called by the Animator the FIRST time a property key is touched inside this transaction.\n * Captures the pre-transaction value for rollback (lazy snapshot — only first touch per key).\n */\n captureProperty(key: string, value: number | string, apply: (v: number | string) => void): void {\n if (this._state !== 'active') { return; }\n // Only capture the first time — lazy snapshot\n if (!this._propertySnapshots.has(key)) {\n this._propertySnapshots.set(key, { value, apply });\n }\n }\n\n commit(): void {\n if (this._state !== 'active') { return; }\n this._state = 'committed';\n this._resolveFinished();\n }\n\n rollback(): void {\n if (this._state !== 'active') { return; }\n // Stop all tracked handles with freeze (don't jump to end)\n for (const handle of this._handles) {\n handle.stop({ mode: 'freeze' });\n }\n // Revert all captured properties\n const revertedKeys: string[] = [];\n for (const [key, snap] of this._propertySnapshots) {\n snap.apply(snap.value);\n revertedKeys.push(key);\n }\n this._state = 'rolled-back';\n this._onAfterRollback?.(revertedKeys);\n this._resolveFinished();\n }\n}\n","import type { SpringMotion, PhysicsState } from './types';\n\nexport const SPRING_DEFAULTS = {\n stiffness: 180,\n damping: 12,\n mass: 1,\n restVelocity: 0.01,\n restDisplacement: 0.01,\n};\n\nexport function stepSpring(state: PhysicsState, config: SpringMotion, dt: number): void {\n if (dt <= 0) {\n return;\n }\n\n const s = config.stiffness ?? SPRING_DEFAULTS.stiffness;\n const d = config.damping ?? SPRING_DEFAULTS.damping;\n const m = config.mass ?? SPRING_DEFAULTS.mass;\n\n const displacement = state.value - state.target;\n const acceleration = (-s * displacement - d * state.velocity) / m;\n\n state.velocity += acceleration * dt;\n state.value += state.velocity * dt;\n\n if (\n Math.abs(state.velocity) < (config.restVelocity ?? SPRING_DEFAULTS.restVelocity) &&\n Math.abs(state.value - state.target) < (config.restDisplacement ?? SPRING_DEFAULTS.restDisplacement)\n ) {\n state.value = state.target;\n state.velocity = 0;\n state.settled = true;\n }\n}\n","import type { DecayMotion, PhysicsState } from './types';\n\nexport const DECAY_DEFAULTS = {\n power: 0.8,\n timeConstant: 350,\n restVelocity: 0.5,\n};\n\nexport function stepDecay(state: PhysicsState, config: DecayMotion, dt: number): void {\n if (dt <= 0) {\n return;\n }\n\n const timeConstant = config.timeConstant ?? DECAY_DEFAULTS.timeConstant;\n\n // Pure exponential decay: v *= e^(-dt * 1000 / timeConstant)\n // Note: `power` is NOT applied here — it is a one-shot initial velocity\n // multiplier applied at Animator setup time (matches Framer Motion convention).\n // Using `power` per-frame with values > 1 causes velocity to diverge.\n const decayFactor = Math.exp((-dt * 1000) / timeConstant);\n state.velocity *= decayFactor;\n state.value += state.velocity * dt;\n\n if (Math.abs(state.velocity) < DECAY_DEFAULTS.restVelocity) {\n state.velocity = 0;\n state.settled = true;\n state.target = state.value; // target is wherever it landed\n }\n}\n","/**\n * Extract the axis letter (\"x\", \"y\", or \"zoom\") from a PropertyEntry key\n * like \"node:id:position.x\" or \"viewport:pan.x\".\n *\n * This lets physics configs (decay velocity, inertia bounds, snap points)\n * be authored with natural axis keys { x: 400, y: 0 } instead of full\n * property paths.\n *\n * Returns null if the key doesn't end with a recognized axis segment.\n */\nexport function extractAxis(key: string): string | null {\n const dotIdx = key.lastIndexOf('.');\n const colonIdx = key.lastIndexOf(':');\n const sepIdx = Math.max(dotIdx, colonIdx);\n if (sepIdx < 0) return null;\n const tail = key.slice(sepIdx + 1);\n // Only treat short alphanumeric tails as axes — filters out things like\n // \"position\" or \"dimensions\" that shouldn't match.\n if (tail.length === 0 || tail.length > 6) return null;\n return tail;\n}\n","import type { InertiaMotion, PhysicsState } from './types';\nimport { stepDecay } from './decay';\nimport { extractAxis } from './axis';\n\nexport function stepInertia(state: PhysicsState, config: InertiaMotion, dt: number, key?: string): void {\n if (dt <= 0) {\n return;\n }\n\n // Apply base decay\n stepDecay(state, {\n type: 'decay',\n velocity: state.velocity,\n power: config.power,\n timeConstant: config.timeConstant,\n }, dt);\n\n // Resolve axis-based config lookup. Bounds/snap authored as { x: [...], y: [...] }\n // need to be matched against the entry's axis letter, not the full property key.\n const axis = key ? extractAxis(key) : null;\n\n // Bounce off bounds\n if (config.bounds && key) {\n const axisBounds = config.bounds[key] ?? (axis ? config.bounds[axis] : undefined);\n if (axisBounds) {\n const [min, max] = axisBounds;\n // bounceStiffness acts as a bounciness coefficient (higher = stronger\n // rebound). Normalize around 500 so the default (200) matches the\n // prior softer-bounce feel; values >500 produce a stronger rebound.\n const bounciness = (config.bounceStiffness ?? 200) / 500;\n const damping = (config.bounceDamping ?? 40) / 100;\n const bounceFactor = bounciness * (1 - damping);\n\n if (state.value < min) {\n state.value = min;\n state.velocity = Math.abs(state.velocity) * bounceFactor;\n state.settled = false; // un-settle — still bouncing\n } else if (state.value > max) {\n state.value = max;\n state.velocity = -Math.abs(state.velocity) * bounceFactor;\n state.settled = false;\n }\n }\n }\n\n // Snap to nearest point when settled\n if (state.settled && config.snapTo?.length && key) {\n let nearest = state.value;\n let minDist = Infinity;\n for (const point of config.snapTo) {\n const val = point[key] ?? (axis ? point[axis] : undefined);\n if (val !== undefined) {\n const dist = Math.abs(state.value - val);\n if (dist < minDist) {\n minDist = dist;\n nearest = val;\n }\n }\n }\n state.value = nearest;\n }\n}\n","import type { KeyframesMotion, PhysicsState } from './types';\nimport { extractAxis } from './axis';\n\nexport function stepKeyframes(\n state: PhysicsState,\n config: KeyframesMotion,\n progress: number, // 0-1 overall\n key: string,\n): void {\n // Waypoints authored as { x: 10, y: 20 } need to be matched against the\n // entry's axis letter extracted from the key suffix (e.g., 'node:id:position.x'\n // → 'x'). Fall through to full-key match for consumers using explicit keys.\n const axis = extractAxis(key);\n const values = config.values.map(v =>\n v[key] ?? (axis ? v[axis] : undefined) ?? state.value,\n );\n if (values.length < 2) {\n state.value = values[0] ?? state.value;\n state.settled = true;\n return;\n }\n\n const offsets = config.offsets ?? values.map((_, i) => i / (values.length - 1));\n\n // Clamp progress\n const p = Math.max(0, Math.min(1, progress));\n\n // Find segment\n let segIdx = 0;\n for (let i = 0; i < offsets.length - 1; i++) {\n if (p >= offsets[i]) {\n segIdx = i;\n }\n }\n\n const segStart = offsets[segIdx];\n const segEnd = offsets[segIdx + 1] ?? 1;\n const segProgress = segEnd > segStart ? (p - segStart) / (segEnd - segStart) : 1;\n\n const from = values[segIdx];\n const to = values[segIdx + 1] ?? values[segIdx];\n\n state.value = from + (to - from) * Math.max(0, Math.min(1, segProgress));\n\n if (p >= 1) {\n state.value = values[values.length - 1];\n state.settled = true;\n }\n}\n","import type { SpringMotion, DecayMotion, InertiaMotion, MotionConfig } from './types';\n\nconst SPRING_PRESETS: Record = {\n gentle: { type: 'spring', stiffness: 120, damping: 14 },\n wobbly: { type: 'spring', stiffness: 180, damping: 12 },\n stiff: { type: 'spring', stiffness: 300, damping: 30 },\n slow: { type: 'spring', stiffness: 60, damping: 15 },\n molasses: { type: 'spring', stiffness: 40, damping: 30 },\n};\n\nconst DECAY_PRESETS: Record = {\n smooth: { type: 'decay', velocity: 0, power: 0.6, timeConstant: 400 },\n snappy: { type: 'decay', velocity: 0, power: 1.2, timeConstant: 200 },\n};\n\nconst INERTIA_PRESETS: Record = {\n momentum: { type: 'inertia', velocity: 0, power: 0.8, timeConstant: 700 },\n rails: { type: 'inertia', velocity: 0, bounceStiffness: 500, bounceDamping: 40 },\n};\n\nexport function resolveMotion(input: string | MotionConfig): MotionConfig | null {\n if (typeof input !== 'string') {\n return input;\n }\n\n const [category, name] = input.split('.');\n\n if (!name) {\n return null;\n }\n\n switch (category) {\n case 'spring': return SPRING_PRESETS[name] ? { ...SPRING_PRESETS[name] } : null;\n case 'decay': return DECAY_PRESETS[name] ? { ...DECAY_PRESETS[name] } : null;\n case 'inertia': return INERTIA_PRESETS[name] ? { ...INERTIA_PRESETS[name] } : null;\n default: return null;\n }\n}\n","// ============================================================================\n// Animator — Unified property animation with blend/compose support\n//\n// Manages per-property animation entries on a shared AnimationEngine rAF loop.\n// Handles interpolation, easing, looping, pause/resume, reverse, and automatic\n// conflict resolution when a new animation targets an already-in-flight key.\n// ============================================================================\n\nimport { AnimationEngine } from './engine';\nimport type { EngineHandle } from './engine';\nimport { resolveEasing } from './easing';\nimport type { EasingName, EasingFn } from './easing';\nimport { lerpNumber, interpolateColor } from './interpolators';\nimport type { StopOptions } from '../core/types';\nimport { HandleRegistry } from './handle-registry';\nimport type { Taggable } from './handle-registry';\nimport { Transaction } from './transaction';\nimport type { MotionConfig, PhysicsState, SpringMotion, DecayMotion, InertiaMotion, KeyframesMotion } from './motion';\nimport { resolveMotion, stepSpring, stepDecay, stepInertia, stepKeyframes, extractAxis } from './motion';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\n/** A single property to animate. */\nexport interface PropertyEntry {\n key: string;\n from: number | string;\n to: number | string;\n apply: (value: number | string) => void;\n}\n\n/** Internal options for the `animate()` method. */\nexport interface AnimateInternalOptions {\n duration: number;\n easing?: EasingName | EasingFn;\n delay?: number;\n loop?: boolean | 'reverse' | 'ping-pong';\n startAt?: 'start' | 'end';\n onStart?: () => void;\n onProgress?: (progress: number) => void;\n onComplete?: () => void;\n tag?: string;\n tags?: string[];\n /** Predicate evaluated once per frame; stops the animation when it returns false. */\n while?: () => boolean;\n /** Controls how the animation stops when `while` returns false. Default: 'jump-end'. */\n whileStopMode?: 'jump-end' | 'rollback' | 'freeze';\n /** Physics-based motion configuration. When provided, bypasses eased interpolation. */\n motion?: MotionConfig | string;\n /** Maximum duration (ms) for physics animations. Prevents runaway springs. Default: 5000. */\n maxDuration?: number;\n}\n\n/** Handle returned by `animate()` — controls the animation lifecycle. */\nexport interface AnimationHandle {\n pause(): void;\n resume(): void;\n stop(options?: StopOptions): void;\n reverse(): void;\n play(): void;\n playForward(): void;\n playBackward(): void;\n restart(options?: { direction?: 'forward' | 'backward' }): void;\n readonly direction: 'forward' | 'backward';\n readonly isFinished: boolean;\n readonly currentValue: Map;\n readonly finished: Promise;\n readonly _snapshot: Map;\n readonly _target: Map;\n}\n\n// ── Internal types ───────────────────────────────────────────────────────────\n\n/** Tracks a group of properties being animated together. */\ninterface ActiveGroup {\n /** Unique identifier for this group, used for ownership comparisons. */\n _id: number;\n entries: PropertyEntry[];\n engineHandle: EngineHandle;\n startTime: number;\n /**\n * Engine-relative elapsed value at the moment of pause, or null when not paused.\n * Uses engine tick elapsed (not wall-clock) so fake schedulers stay in sync.\n */\n pausedElapsed: number | null;\n /** When true, the next tick adjusts startTime to account for the pause gap. */\n _resumeNeeded: boolean;\n direction: 'forward' | 'backward';\n duration: number;\n easingFn: EasingFn;\n loop: boolean | 'reverse';\n onStart?: () => void;\n startFired: boolean;\n onProgress?: (progress: number) => void;\n onComplete?: () => void;\n resolve: (() => void) | null;\n stopped: boolean;\n /** True when the animation reaches its current-direction endpoint. */\n isFinished: boolean;\n /** Current interpolated values per key, updated every frame. */\n currentValues: Map;\n /** Last elapsed time from engine tick, used for seamless reverse. */\n _lastElapsed: number;\n /** Wall-clock performance.now() at the moment of the last tick. Used to\n * estimate the *current* elapsed time when reverse/restart is called\n * between ticks — without this, _lastElapsed can be up to 16ms stale\n * and a visible sub-frame jump shows up when flipping direction. */\n _lastTickWallTime: number;\n /** Starting values captured at animate() call time. Persists after completion. */\n snapshot: Map;\n /** Target values captured at animate() call time. Persists after completion. */\n target: Map;\n /** Current finished promise — renewed on reverse/restart after completion. */\n _currentFinished: Promise;\n /** Reference to the public handle for registry deregistration. */\n _handle?: Taggable;\n /** Optional predicate evaluated each frame; stops the animation when false. */\n whilePredicate?: () => boolean;\n /** How to stop the animation when whilePredicate returns false. */\n whileStopMode: 'jump-end' | 'rollback' | 'freeze';\n /** Resolved motion config for physics-based animation. */\n motionConfig?: MotionConfig;\n /** Per-property physics state. Only set when motionConfig is present. */\n physicsStates?: Map;\n /** Maximum duration (ms) for physics animations. Default: 5000. */\n maxDuration: number;\n /** True when this group uses physics-based motion instead of eased interpolation. */\n isPhysics: boolean;\n /** Previous elapsed time for computing dt in physics tick. */\n _prevElapsed: number;\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Detect whether a value looks like a color string. */\nfunction isColorString(value: unknown): value is string {\n if (typeof value !== 'string') {\n return false;\n }\n return /^(#|rgb|hsl)/.test(value);\n}\n\n/** Interpolate a single property value based on from/to types. */\nfunction interpolateValue(from: number | string, to: number | string, t: number): number | string {\n if (typeof from === 'number' && typeof to === 'number') {\n return lerpNumber(from, to, t);\n }\n\n if (isColorString(from) && isColorString(to)) {\n return interpolateColor(from, to, t);\n }\n\n // Non-interpolatable strings: snap at midpoint\n return t < 0.5 ? from : to;\n}\n\n// ── Animator ─────────────────────────────────────────────────────────────────\n\nexport class Animator {\n private _engine: AnimationEngine;\n /** Maps property key -> the ActiveGroup that currently owns it. */\n private _ownership = new Map();\n /** All active groups. */\n private _groups = new Set();\n private _nextGroupId = 0;\n private _registry = new HandleRegistry();\n private _activeTransaction: Transaction | null = null;\n\n constructor(engine: AnimationEngine) {\n this._engine = engine;\n }\n\n /** Whether any animations are currently running. */\n get active(): boolean {\n return this._groups.size > 0;\n }\n\n /** The handle registry for tag-based animation control. */\n get registry(): HandleRegistry {\n return this._registry;\n }\n\n /** Begin a new transaction — all subsequent `animate()` calls will be tracked until `endTransaction()`. */\n beginTransaction(): Transaction {\n const tx = new Transaction();\n this._activeTransaction = tx;\n return tx;\n }\n\n /** End the current transaction context (does NOT commit or rollback — the caller decides). */\n endTransaction(): void {\n this._activeTransaction = null;\n }\n\n /**\n * Animate a set of property entries over the given duration.\n *\n * If any entry targets a key already being animated, the current in-flight\n * value is captured as the new \"from\" and the property is removed from the\n * old group (blend/compose).\n */\n animate(entries: PropertyEntry[], options: AnimateInternalOptions): AnimationHandle {\n const {\n duration,\n easing,\n delay = 0,\n loop = false,\n startAt,\n onStart,\n onProgress,\n onComplete,\n tag,\n tags,\n while: whilePredicate,\n whileStopMode = 'jump-end',\n motion,\n maxDuration = 5000,\n } = options;\n\n // Resolve easing once\n const easingFn = resolveEasing(easing);\n\n // Resolve motion config (physics-based animation)\n const resolvedMotion = motion ? resolveMotion(motion) : undefined;\n\n // Blend/compose: capture in-flight values and steal ownership\n for (const entry of entries) {\n const existing = this._ownership.get(entry.key);\n if (existing && !existing.stopped) {\n // Capture the current interpolated value as the new \"from\"\n const currentValue = existing.currentValues.get(entry.key);\n if (currentValue !== undefined) {\n entry.from = currentValue;\n }\n\n // Remove this property from the old group\n existing.entries = existing.entries.filter((e) => e.key !== entry.key);\n\n // If the old group has no properties left, stop it as superseded\n if (existing.entries.length === 0) {\n this._stop(existing, 'superseded');\n }\n }\n }\n\n // Transaction: capture property snapshots (after blend/compose resolves entry.from)\n if (this._activeTransaction && this._activeTransaction.state === 'active') {\n for (const entry of entries) {\n this._activeTransaction.captureProperty(entry.key, entry.from, entry.apply);\n }\n }\n\n // Duration 0 = instant snap\n if (duration <= 0) {\n const snap = new Map();\n const tgt = new Map();\n for (const entry of entries) {\n snap.set(entry.key, entry.from);\n tgt.set(entry.key, entry.to);\n }\n\n onStart?.();\n\n for (const entry of entries) {\n entry.apply(entry.to);\n }\n\n const allTags = [...(tag ? [tag] : []), ...(tags ?? [])];\n const handle: AnimationHandle & { _tags?: string[] } = {\n _tags: allTags.length > 0 ? allTags : undefined,\n pause: () => {},\n resume: () => {},\n stop: () => {},\n reverse: () => {},\n play: () => {},\n playForward: () => {},\n playBackward: () => {},\n restart: () => {},\n get direction(): 'forward' | 'backward' { return 'forward'; },\n get isFinished() { return true; },\n get currentValue() { return tgt; },\n finished: Promise.resolve(),\n get _snapshot() { return snap; },\n get _target() { return tgt; },\n };\n\n this._registry.register(handle as Taggable);\n queueMicrotask(() => this._registry.unregister(handle as Taggable));\n\n // Transaction: track instant handle\n if (this._activeTransaction && this._activeTransaction.state === 'active') {\n this._activeTransaction.trackHandle(handle);\n }\n\n onComplete?.();\n return handle;\n }\n\n // Build snapshot and target maps before group construction\n const snapshot = new Map();\n const target = new Map();\n for (const entry of entries) {\n snapshot.set(entry.key, entry.from);\n target.set(entry.key, entry.to);\n }\n\n // Build physics states for motion-based animation\n let physicsStates: Map | undefined;\n const isPhysics = !!resolvedMotion;\n if (resolvedMotion) {\n physicsStates = new Map();\n for (const entry of entries) {\n if (typeof entry.from !== 'number' || typeof entry.to !== 'number') {\n // Non-numeric property — can't use physics, snap immediately\n console.warn(\n `[AlpineFlow] motion: requires numeric properties. \"${entry.key}\" is non-numeric; snapping to target.`,\n );\n entry.apply(entry.to);\n continue;\n }\n let initialVelocity = 0;\n if (resolvedMotion.type === 'decay' || resolvedMotion.type === 'inertia') {\n const vel = (resolvedMotion as { velocity?: number | Record }).velocity;\n if (typeof vel === 'number') {\n initialVelocity = vel;\n } else if (vel && typeof vel === 'object') {\n // Try full key first, then fall back to the axis letter extracted from\n // the key suffix (e.g., 'node:id:position.x' → 'x') so natural\n // { x, y } velocity configs work with canvas-level PropertyEntry keys.\n const velRecord = vel as Record;\n const axis = extractAxis(entry.key);\n initialVelocity = velRecord[entry.key] ?? (axis ? velRecord[axis] ?? 0 : 0);\n }\n // `power` is applied once at entry as an initial velocity multiplier,\n // matching Framer Motion semantics. The integrator itself performs\n // pure exponential decay, keeping values > 1 stable.\n const power = (resolvedMotion as DecayMotion | InertiaMotion).power ?? 0.8;\n initialVelocity *= power;\n }\n physicsStates.set(entry.key, {\n value: entry.from as number,\n velocity: initialVelocity,\n target: entry.to as number,\n settled: false,\n });\n }\n\n // If no entries could be used for physics, fall back to eased animation\n if (physicsStates.size === 0) {\n physicsStates = undefined;\n }\n }\n\n // Normalize loop: 'ping-pong' is an alias for 'reverse'\n const normalizedLoop = loop === 'ping-pong' ? 'reverse' : loop;\n\n // Determine initial direction\n const initialDirection: 'forward' | 'backward' = startAt === 'end' ? 'backward' : 'forward';\n\n // Set up the active group\n let resolveFinished!: () => void;\n const finished = new Promise((resolve) => {\n resolveFinished = resolve;\n });\n\n const group: ActiveGroup = {\n _id: this._nextGroupId++,\n entries: [...entries],\n engineHandle: null as unknown as EngineHandle,\n startTime: 0,\n pausedElapsed: null,\n _resumeNeeded: false,\n direction: initialDirection,\n duration,\n easingFn,\n loop: normalizedLoop,\n onStart,\n startFired: false,\n onProgress,\n onComplete,\n resolve: resolveFinished,\n stopped: false,\n isFinished: false,\n currentValues: new Map(),\n _lastElapsed: 0,\n _lastTickWallTime: 0,\n snapshot,\n target,\n _currentFinished: finished,\n whilePredicate,\n whileStopMode,\n motionConfig: physicsStates ? resolvedMotion! : undefined,\n physicsStates,\n maxDuration,\n isPhysics: !!physicsStates,\n _prevElapsed: 0,\n };\n\n // Initialize current values\n if (startAt === 'end') {\n // Snap to target values immediately; animation will play backward to snapshot\n for (const entry of group.entries) {\n entry.apply(entry.to);\n group.currentValues.set(entry.key, entry.to);\n }\n } else {\n for (const entry of group.entries) {\n group.currentValues.set(entry.key, entry.from);\n }\n }\n\n // Register ownership\n for (const entry of entries) {\n this._ownership.set(entry.key, group);\n }\n\n this._groups.add(group);\n\n // Register on engine\n const engineHandle = this._engine.register((elapsed) => {\n return this._tick(group, elapsed);\n }, delay);\n\n group.engineHandle = engineHandle;\n\n // Compute tags for this animation\n const allTags = [...(tag ? [tag] : []), ...(tags ?? [])];\n\n // Build the animation handle\n const handle: AnimationHandle & { _tags?: string[] } = {\n _tags: allTags.length > 0 ? allTags : undefined,\n pause: () => this._pause(group),\n resume: () => this._resume(group),\n stop: (options?: StopOptions) => this._stop(group, options?.mode ?? 'jump-end'),\n reverse: () => this._reverse(group),\n play: () => this._play(group),\n playForward: () => this._playDirection(group, 'forward'),\n playBackward: () => this._playDirection(group, 'backward'),\n restart: (opts?: { direction?: 'forward' | 'backward' }) => this._restart(group, opts),\n get direction() { return group.direction; },\n get isFinished() { return group.isFinished; },\n get currentValue() { return group.currentValues; },\n get finished() { return group._currentFinished; },\n get _snapshot() { return group.snapshot; },\n get _target() { return group.target; },\n };\n\n // Register handle for tag-based control\n this._registry.register(handle as Taggable);\n // Store handle reference on group for auto-deregister on completion\n group._handle = handle as Taggable;\n\n // Transaction: track this handle\n if (this._activeTransaction && this._activeTransaction.state === 'active') {\n this._activeTransaction.trackHandle(handle);\n }\n\n return handle;\n }\n\n /** Stop all active animations. */\n stopAll(options?: StopOptions): void {\n const mode = options?.mode ?? 'jump-end';\n for (const group of this._groups) {\n if (!group.stopped) {\n this._stop(group, mode);\n }\n }\n this._groups.clear();\n this._ownership.clear();\n }\n\n // ── Internal: tick ───────────────────────────────────────────────────\n\n /**\n * Per-frame tick for an animation group.\n * @returns `true` when the animation is complete (to unregister from engine).\n */\n private _tick(group: ActiveGroup, elapsed: number): boolean | void {\n if (group.stopped) {\n return true;\n }\n\n // Paused: don't advance\n if (group.pausedElapsed !== null) {\n return;\n }\n\n // Physics-based animation: delegate to _tickPhysics\n if (group.isPhysics) {\n return this._tickPhysics(group, elapsed);\n }\n\n // State-aware cancellation: evaluate the while predicate and stop if false\n if (group.whilePredicate && !group.whilePredicate()) {\n this._stop(group, group.whileStopMode);\n return true;\n }\n\n // Just resumed: adjust startTime so the animation continues from the paused position.\n // The gap between pausedElapsed and now is dead time that should not count toward progress.\n if (group._resumeNeeded) {\n group.startTime += elapsed - group._lastElapsed;\n group._resumeNeeded = false;\n }\n\n // On first tick, record startTime\n if (group.startTime === 0) {\n group.startTime = elapsed;\n }\n\n // Fire onStart once on the first active tick\n if (!group.startFired) {\n group.startFired = true;\n group.onStart?.();\n }\n\n group._lastElapsed = elapsed;\n group._lastTickWallTime = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const timeSinceStart = elapsed - group.startTime;\n let rawProgress = Math.min(timeSinceStart / group.duration, 1);\n\n // Handle looping\n if (group.loop && rawProgress >= 1) {\n if (group.loop === 'reverse') {\n // Ping-pong: cycle through 0->1->0->1...\n const cycles = timeSinceStart / group.duration;\n const cycleIndex = Math.floor(cycles);\n const fractional = cycles - cycleIndex;\n // Even cycles go forward, odd cycles go backward\n rawProgress = cycleIndex % 2 === 0 ? fractional : 1 - fractional;\n } else {\n // loop: true — restart\n const fractional = (timeSinceStart % group.duration) / group.duration;\n rawProgress = fractional;\n }\n }\n\n // Apply reverse\n const directedProgress = group.direction === 'backward' ? 1 - rawProgress : rawProgress;\n\n // Apply easing\n const easedProgress = group.easingFn(directedProgress);\n\n // Interpolate and apply all entries\n for (const entry of group.entries) {\n const value = interpolateValue(entry.from, entry.to, easedProgress);\n group.currentValues.set(entry.key, value);\n entry.apply(value);\n }\n\n // Report progress\n group.onProgress?.(directedProgress);\n\n // Check completion (non-looping only)\n if (!group.loop && rawProgress >= 1) {\n // Ensure final values are exact\n for (const entry of group.entries) {\n const target = group.direction === 'backward' ? entry.from : entry.to;\n entry.apply(target);\n group.currentValues.set(entry.key, target);\n }\n\n this._completeGroup(group);\n return true;\n }\n }\n\n /**\n * Mark a group as complete: set flags, clean up, fire callbacks, resolve promise,\n * and schedule auto-deregistration. Shared by both the eased and physics paths.\n */\n private _completeGroup(group: ActiveGroup): void {\n group.stopped = true;\n group.isFinished = true;\n this._cleanup(group);\n group.onComplete?.();\n group.resolve?.();\n if (group._handle) {\n const handle = group._handle;\n queueMicrotask(() => {\n if (group.isFinished) {\n this._registry.unregister(handle);\n }\n });\n }\n }\n\n /**\n * Per-frame tick for a physics-based animation group.\n * Runs the physics integrator (spring, etc.) each frame instead of eased interpolation.\n * @returns `true` when the animation is complete (to unregister from engine).\n */\n private _tickPhysics(group: ActiveGroup, elapsed: number): boolean | void {\n // State-aware cancellation\n if (group.whilePredicate && !group.whilePredicate()) {\n this._stop(group, group.whileStopMode);\n return true;\n }\n\n // Just resumed: reset timing for physics continuity\n if (group._resumeNeeded) {\n group._resumeNeeded = false;\n // Reset _prevElapsed to current so first post-resume dt is small\n group._prevElapsed = elapsed;\n group.startTime = elapsed - (group._lastElapsed - group.startTime);\n }\n\n // On first tick, record startTime\n if (group.startTime === 0) {\n group.startTime = elapsed;\n }\n\n // Fire onStart once on the first active tick\n if (!group.startFired) {\n group.startFired = true;\n group.onStart?.();\n }\n\n // Compute dt in seconds, capped to prevent explosion on background tabs\n const prevElapsed = group._prevElapsed || elapsed;\n const dt = Math.min((elapsed - prevElapsed) / 1000, 0.064); // cap at 64ms\n group._prevElapsed = elapsed;\n group._lastElapsed = elapsed;\n group._lastTickWallTime = typeof performance !== 'undefined' ? performance.now() : Date.now();\n\n if (dt <= 0) {\n return; // skip zero-delta ticks (first frame)\n }\n\n const physicsStates = group.physicsStates!;\n let allSettled = true;\n\n // Step each physics state\n for (const entry of group.entries) {\n const state = physicsStates.get(entry.key);\n if (!state) {\n continue; // non-physics entry, already snapped\n }\n\n if (!state.settled) {\n // Direction handling: update target based on direction\n if (group.direction === 'backward') {\n state.target = group.snapshot.get(entry.key) as number;\n } else {\n state.target = group.target.get(entry.key) as number;\n }\n\n // Call the appropriate integrator based on motion type\n switch (group.motionConfig!.type) {\n case 'spring':\n stepSpring(state, group.motionConfig as SpringMotion, dt);\n break;\n case 'decay':\n stepDecay(state, group.motionConfig as DecayMotion, dt);\n break;\n case 'inertia':\n stepInertia(state, group.motionConfig as InertiaMotion, dt, entry.key);\n break;\n case 'keyframes': {\n const totalElapsed = elapsed - group.startTime;\n const kfDuration = (group.motionConfig as KeyframesMotion).duration ?? group.maxDuration;\n const kfProgress = Math.min(totalElapsed / kfDuration, 1);\n stepKeyframes(state, group.motionConfig as KeyframesMotion, kfProgress, entry.key);\n break;\n }\n }\n\n group.currentValues.set(entry.key, state.value);\n entry.apply(state.value);\n }\n\n if (!state.settled) {\n allSettled = false;\n }\n }\n\n // Progress approximation for physics (useful for onProgress)\n const totalElapsed = elapsed - group.startTime;\n const approxProgress = Math.min(totalElapsed / group.maxDuration, 1);\n group.onProgress?.(approxProgress);\n\n // Check maxDuration cap\n if (totalElapsed >= group.maxDuration) {\n // Force settle all states\n for (const [key, state] of physicsStates) {\n if (!state.settled) {\n state.value = state.target;\n state.velocity = 0;\n state.settled = true;\n const entry = group.entries.find((e) => e.key === key);\n if (entry) {\n entry.apply(state.value);\n group.currentValues.set(entry.key, state.value);\n }\n }\n }\n allSettled = true;\n }\n\n if (allSettled) {\n this._completeGroup(group);\n return true;\n }\n }\n\n // ── Internal: handle actions ─────────────────────────────────────────\n\n private _pause(group: ActiveGroup): void {\n if (group.stopped || group.pausedElapsed !== null || group.startTime === 0) {\n return;\n }\n // Store the engine-relative elapsed value at pause time so resume can compute\n // the exact gap without relying on wall-clock (performance.now()).\n group.pausedElapsed = group._lastElapsed;\n }\n\n private _resume(group: ActiveGroup): void {\n if (group.stopped || group.pausedElapsed === null) {\n return;\n }\n // Signal the next tick to adjust startTime by the pause gap.\n // _lastElapsed still holds the elapsed value from just before the pause,\n // and _tick will receive the new elapsed on the next frame.\n group._resumeNeeded = true;\n group.pausedElapsed = null;\n }\n\n private _stop(group: ActiveGroup, mode: 'jump-end' | 'rollback' | 'freeze' | 'superseded' = 'jump-end'): void {\n if (group.stopped) {\n return;\n }\n\n group.stopped = true;\n group.engineHandle.stop();\n\n if (mode === 'jump-end') {\n // Apply final target values (current behavior)\n for (const entry of group.entries) {\n const target = group.direction === 'backward' ? entry.from : entry.to;\n entry.apply(target);\n }\n // Force-settle physics states\n if (group.isPhysics && group.physicsStates) {\n for (const [, state] of group.physicsStates) {\n state.value = state.target;\n state.velocity = 0;\n state.settled = true;\n }\n }\n } else if (mode === 'rollback') {\n // Revert to snapshot (start state)\n for (const entry of group.entries) {\n const snapValue = group.snapshot.get(entry.key);\n if (snapValue !== undefined) {\n entry.apply(snapValue);\n }\n }\n } else if (mode === 'freeze') {\n // No property writes — leave at current interpolated value\n } else if (mode === 'superseded') {\n // No property writes — new owner is about to overwrite\n }\n\n this._cleanup(group);\n\n // onComplete fires for consumer-facing modes, not for superseded\n if (mode !== 'superseded') {\n group.onComplete?.();\n }\n group.resolve?.();\n if (group._handle) {\n const handle = group._handle;\n queueMicrotask(() => {\n if (group.isFinished || group.stopped) {\n this._registry.unregister(handle);\n }\n });\n }\n }\n\n private _reverse(group: ActiveGroup): void {\n // Reverse on a finished handle: revive and play in opposite direction\n if (group.isFinished) {\n group.direction = group.direction === 'forward' ? 'backward' : 'forward';\n\n // Reset physics states for the new direction\n if (group.isPhysics && group.physicsStates) {\n for (const [key, state] of group.physicsStates) {\n // value stays at current (where the spring settled)\n // target switches to the other end\n if (group.direction === 'backward') {\n state.target = group.snapshot.get(key) as number;\n } else {\n state.target = group.target.get(key) as number;\n }\n state.velocity = 0;\n state.settled = false;\n }\n }\n\n this._revive(group);\n return;\n }\n\n if (group.stopped) {\n return;\n }\n\n group.direction = group.direction === 'forward' ? 'backward' : 'forward';\n\n if (group.isPhysics && group.physicsStates) {\n // For physics groups: update targets and reset velocity so the spring pulls toward new target\n for (const [key, state] of group.physicsStates) {\n if (group.direction === 'backward') {\n state.target = group.snapshot.get(key) as number;\n } else {\n state.target = group.target.get(key) as number;\n }\n state.velocity = 0;\n state.settled = false;\n }\n return;\n }\n\n // Pre-first-tick reverse: the animation has been created but no tick has\n // run yet, so the node is still at its `from` value. The first tick would\n // otherwise compute `directedProgress = 1 - 0 = 1` for backward direction\n // and snap the node to `to` — a visible teleport. Push startTime back by\n // `duration` so the first tick sees `rawProgress = 1` and the animation\n // completes immediately at the node's current (`from`) position. Net\n // effect: reverse before anything has happened = silent no-op.\n if (group._lastElapsed === 0 && group.startTime === 0) {\n group.startTime = -group.duration;\n return;\n }\n\n // Adjust startTime so that directedProgress is continuous.\n // Before reverse at rawProgress p: directedProgress = p (or 1-p if already reversed).\n // After flipping, we need 1 - newRawProgress = oldRawProgress, so\n // newRawProgress = 1 - oldRawProgress, meaning newStartTime = elapsed - (1-p)*duration.\n //\n // Use the estimated *current* elapsed (last tick + wall time since) rather\n // than the stale _lastElapsed — the gap can be up to 16ms, which shows up\n // as a visible sub-frame jump backward when reverse fires between ticks.\n //\n // Guard is only `_lastElapsed > 0` (the tick has run at least once) —\n // startTime can be negative after prior reverses at low progress, and the\n // adjustment math works correctly for any startTime value.\n if (group._lastElapsed > 0) {\n const elapsed = this._estimatedCurrentElapsed(group);\n const rawProgress = Math.min((elapsed - group.startTime) / group.duration, 1);\n group.startTime = elapsed - (1 - rawProgress) * group.duration;\n }\n }\n\n /**\n * Best estimate of the current elapsed time for a group, accounting for\n * wall-clock time that has passed since the last engine tick. Used by\n * reverse/restart/play-direction paths that run synchronously from user\n * input between ticks — without this correction, _lastElapsed can be up\n * to 16ms stale and causes a visible direction-flip jump.\n */\n private _estimatedCurrentElapsed(group: ActiveGroup): number {\n if (group._lastTickWallTime <= 0) return group._lastElapsed;\n const now = typeof performance !== 'undefined' ? performance.now() : Date.now();\n const wallDelta = Math.max(0, now - group._lastTickWallTime);\n // Cap the correction at one frame's worth — larger values mean the engine\n // is paused/background-throttled and we shouldn't extrapolate blindly.\n return group._lastElapsed + Math.min(wallDelta, 32);\n }\n\n private _play(group: ActiveGroup): void {\n if (group.isFinished) {\n // Restart in current direction from the beginning\n this._revive(group);\n return;\n }\n\n if (group.stopped) {\n return;\n }\n\n // If paused, resume\n if (group.pausedElapsed !== null) {\n this._resume(group);\n }\n }\n\n private _playDirection(group: ActiveGroup, direction: 'forward' | 'backward'): void {\n const directionChanged = group.direction !== direction;\n group.direction = direction;\n\n if (group.isFinished) {\n this._revive(group);\n return;\n }\n\n if (group.stopped) {\n return;\n }\n\n // For physics groups: update targets when direction changes\n if (directionChanged && group.isPhysics && group.physicsStates) {\n for (const [key, state] of group.physicsStates) {\n if (direction === 'backward') {\n state.target = group.snapshot.get(key) as number;\n } else {\n state.target = group.target.get(key) as number;\n }\n state.velocity = 0;\n state.settled = false;\n }\n } else if (directionChanged && group._lastElapsed > 0) {\n // Same adjustment as _reverse() for seamless mid-flight direction change.\n // Uses _estimatedCurrentElapsed to avoid the inter-tick lag jump. Guard\n // is only `_lastElapsed > 0` — startTime can be negative after prior\n // direction flips and the math works regardless.\n const elapsed = this._estimatedCurrentElapsed(group);\n const rawProgress = Math.min((elapsed - group.startTime) / group.duration, 1);\n group.startTime = elapsed - (1 - rawProgress) * group.duration;\n }\n\n // If paused, resume\n if (group.pausedElapsed !== null) {\n this._resume(group);\n }\n }\n\n private _restart(group: ActiveGroup, options?: { direction?: 'forward' | 'backward' }): void {\n const direction = options?.direction ?? 'forward';\n group.direction = direction;\n\n // Apply snapshot or target values based on direction\n if (direction === 'forward') {\n // Reset to snapshot values\n for (const entry of group.entries) {\n const snapValue = group.snapshot.get(entry.key);\n if (snapValue !== undefined) {\n entry.apply(snapValue);\n group.currentValues.set(entry.key, snapValue);\n }\n }\n } else {\n // Reset to target values\n for (const entry of group.entries) {\n const targetValue = group.target.get(entry.key);\n if (targetValue !== undefined) {\n entry.apply(targetValue);\n group.currentValues.set(entry.key, targetValue);\n }\n }\n }\n\n // Reset physics states for physics-based animations\n if (group.isPhysics && group.physicsStates) {\n for (const [key, state] of group.physicsStates) {\n if (direction === 'forward') {\n state.value = group.snapshot.get(key) as number;\n state.target = group.target.get(key) as number;\n } else {\n state.value = group.target.get(key) as number;\n state.target = group.snapshot.get(key) as number;\n }\n state.velocity = 0;\n state.settled = false;\n }\n }\n\n this._revive(group);\n }\n\n /** Revive a finished/stopped group: reset timing, re-register on engine, renew promise. */\n private _revive(group: ActiveGroup): void {\n group.isFinished = false;\n group.stopped = false;\n group.startTime = 0;\n group.startFired = false;\n group.pausedElapsed = null;\n group._resumeNeeded = false;\n group._lastElapsed = 0;\n group._prevElapsed = 0;\n\n // Reset physics states that haven't already been prepared by the caller.\n // For physics groups: ensure unsettled states are ready for integration.\n // Callers like _reverse() and _restart() may have already set up the states;\n // this only resets states that are still marked as settled.\n if (group.isPhysics && group.physicsStates) {\n for (const [key, state] of group.physicsStates) {\n if (state.settled) {\n // Reset to starting position for current direction\n if (group.direction === 'forward') {\n state.value = group.snapshot.get(key) as number;\n state.target = group.target.get(key) as number;\n } else {\n state.value = group.target.get(key) as number;\n state.target = group.snapshot.get(key) as number;\n }\n state.velocity = 0;\n state.settled = false;\n }\n }\n }\n\n // Renew the finished promise\n this._renewFinished(group);\n\n // Re-register ownership\n for (const entry of group.entries) {\n this._ownership.set(entry.key, group);\n }\n\n // Re-add to active groups\n this._groups.add(group);\n\n // Re-register handle in the registry (may have been deregistered on completion)\n if (group._handle) {\n this._registry.register(group._handle);\n }\n\n // Re-register on engine\n const engineHandle = this._engine.register((elapsed) => {\n return this._tick(group, elapsed);\n });\n group.engineHandle = engineHandle;\n }\n\n /** Create a new finished promise for the group (old one stays resolved). */\n private _renewFinished(group: ActiveGroup): void {\n group.resolve = null;\n const promise = new Promise((resolve) => {\n group.resolve = resolve;\n });\n group._currentFinished = promise;\n }\n\n // ── Internal: cleanup ────────────────────────────────────────────────\n\n private _cleanup(group: ActiveGroup): void {\n // Remove ownership for all keys in this group.\n // Compare by _id rather than reference equality to guard against callers\n // that may wrap the Animator in a proxy (e.g. Alpine.js reactive system),\n // which would cause Map.get() to return a proxy-wrapped group that fails\n // strict equality (===) against the raw group object.\n for (const entry of group.entries) {\n const owner = this._ownership.get(entry.key);\n if (owner && owner._id === group._id) {\n this._ownership.delete(entry.key);\n }\n }\n this._groups.delete(group);\n }\n}\n","// ============================================================================\n// SVG Markers\n//\n// Generates SVG definitions for edge arrowheads and other end markers.\n// Supports built-in types (arrow, arrowclosed) and custom marker registration.\n// ============================================================================\n\nimport { DEFAULT_STROKE_COLOR } from './constants';\n\nexport type MarkerType = 'arrow' | 'arrowclosed' | (string & {});\n\nexport interface MarkerConfig {\n type: MarkerType;\n color?: string;\n width?: number;\n height?: number;\n orient?: string;\n offset?: number;\n}\n\n/**\n * Render function for a custom marker. Receives the marker config (with resolved\n * defaults for color, width, height, orient) and the unique SVG id. Must return\n * a complete `...` SVG string.\n */\nexport type CustomMarkerRenderer = (config: {\n id: string;\n color: string;\n width: number;\n height: number;\n orient: string;\n}) => string;\n\n// ── Custom marker registry ─────────────────────────────────────────────────\n\nconst customMarkers = new Map();\n\n/**\n * Register a custom marker type. The renderer receives resolved defaults and\n * must return a complete `` SVG element string.\n *\n * @example\n * registerMarker('diamond', ({ id, color, width, height, orient }) => `\n * \n * \n * \n * `);\n *\n * // Then use on edges:\n * { id: 'e1', source: 'a', target: 'b', markerEnd: 'diamond' }\n */\nexport function registerMarker(type: string, renderer: CustomMarkerRenderer): void {\n customMarkers.set(type, renderer);\n}\n\n/** Escape a string for safe use inside an HTML/SVG attribute value. */\nfunction escapeAttr(value: string): string {\n return value.replace(/&/g, '&').replace(/\"/g, '"').replace(//g, '>');\n}\n\n/**\n * Normalize a marker shorthand (string type) into a full MarkerConfig.\n */\nexport function normalizeMarker(marker: MarkerType | MarkerConfig): MarkerConfig {\n return typeof marker === 'string' ? { type: marker } : marker;\n}\n\n/**\n * Generate a unique marker ID for deduplication in SVG defs.\n */\nexport function getMarkerId(marker: MarkerConfig, flowId: string): string {\n return `${flowId}__${marker.type}__${(marker.color ?? DEFAULT_STROKE_COLOR).replace(/[^a-zA-Z0-9]/g, '_')}`;\n}\n\n/**\n * Generate SVG marker element markup string.\n */\nexport function getMarkerSvg(marker: MarkerConfig, id: string): string {\n const color = escapeAttr(marker.color ?? DEFAULT_STROKE_COLOR);\n const rawW = Number(marker.width ?? 12.5);\n const rawH = Number(marker.height ?? 12.5);\n const w = Number.isFinite(rawW) && rawW > 0 ? rawW : 12.5;\n const h = Number.isFinite(rawH) && rawH > 0 ? rawH : 12.5;\n const orient = escapeAttr(marker.orient ?? 'auto-start-reverse');\n const safeId = escapeAttr(id);\n\n if (marker.type === 'arrow') {\n return `\n \n `;\n }\n\n if (marker.type === 'arrowclosed') {\n return `\n \n `;\n }\n\n // Custom marker — look up in registry\n const renderer = customMarkers.get(marker.type);\n if (renderer) {\n return renderer({ id: safeId, color, width: w, height: h, orient });\n }\n\n // Unknown type — fall back to arrowclosed\n return getMarkerSvg({ ...marker, type: 'arrowclosed' }, id);\n}\n","// ============================================================================\n// MiniMap\n//\n// A bird's-eye view overlay showing all nodes as simplified SVG rectangles\n// with a viewport indicator mask. Optionally supports click-to-pan and\n// scroll-to-zoom interaction.\n// ============================================================================\n\nimport type { FlowNode, Viewport, FlowCanvasConfig } from './types';\nimport { getNodesBounds, DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from './geometry';\n\nconst MINIMAP_WIDTH = 200;\nconst MINIMAP_HEIGHT = 150;\nconst BOUNDS_PADDING = 1.2;\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\nexport interface MiniMapState {\n nodes: FlowNode[];\n viewport: Viewport;\n containerWidth: number;\n containerHeight: number;\n}\n\n/** Subset of state that `updateViewport` needs — no node mapping. */\nexport interface MiniMapViewportState {\n viewport: Viewport;\n containerWidth: number;\n containerHeight: number;\n}\n\nexport interface MiniMapOptions {\n getState: () => MiniMapState;\n /**\n * Lean getter used by `updateViewport` — resolves only the viewport and\n * container size, skipping the full `toAbsoluteNodes` remap `getState` does.\n * Falls back to `getState` when omitted.\n */\n getViewportState?: () => MiniMapViewportState;\n setViewport: (vp: Partial) => void;\n config: FlowCanvasConfig;\n}\n\nexport interface MiniMapInstance {\n render(): void;\n updateViewport(): void;\n destroy(): void;\n}\n\nexport function createMiniMap(\n container: HTMLElement,\n options: MiniMapOptions,\n): MiniMapInstance {\n const { getState, setViewport, config } = options;\n const position = config.minimapPosition ?? 'bottom-right';\n const maskColor = config.minimapMaskColor;\n const nodeColor = config.minimapNodeColor;\n\n // ── Build DOM ──────────────────────────────────────────────────────\n const wrapper = document.createElement('div');\n wrapper.className = `flow-minimap flow-minimap-${position}`;\n\n const svg = document.createElementNS(SVG_NS, 'svg');\n svg.setAttribute('width', String(MINIMAP_WIDTH));\n svg.setAttribute('height', String(MINIMAP_HEIGHT));\n\n const bg = document.createElementNS(SVG_NS, 'rect');\n bg.classList.add('flow-minimap-bg');\n bg.setAttribute('width', String(MINIMAP_WIDTH));\n bg.setAttribute('height', String(MINIMAP_HEIGHT));\n // Fill controlled via CSS --flow-minimap-bg; no inline attribute needed\n\n const nodesGroup = document.createElementNS(SVG_NS, 'g');\n nodesGroup.classList.add('flow-minimap-nodes');\n\n const maskPath = document.createElementNS(SVG_NS, 'path');\n maskPath.classList.add('flow-minimap-mask');\n // Fill controlled via CSS --flow-minimap-mask-color; override only if user configured explicitly\n if (maskColor) {\n maskPath.setAttribute('fill', maskColor);\n }\n maskPath.setAttribute('fill-rule', 'evenodd');\n\n svg.appendChild(bg);\n svg.appendChild(nodesGroup);\n svg.appendChild(maskPath);\n wrapper.appendChild(svg);\n container.appendChild(wrapper);\n\n // ── Cached bounds for viewport-only updates ────────────────────────\n let cachedBounds = { x: 0, y: 0, width: 0, height: 0 };\n let cachedScale = 1;\n\n function computeScale(): void {\n const state = getState();\n cachedBounds = getNodesBounds(state.nodes.filter((n) => !n.hidden), config.nodeOrigin);\n\n if (cachedBounds.width === 0 && cachedBounds.height === 0) {\n cachedScale = 1;\n return;\n }\n\n cachedScale = Math.max(\n cachedBounds.width / MINIMAP_WIDTH,\n cachedBounds.height / MINIMAP_HEIGHT,\n ) * BOUNDS_PADDING;\n }\n\n function getNodeFill(node: FlowNode): string | undefined {\n if (typeof nodeColor === 'function') {\n return nodeColor(node);\n }\n return nodeColor;\n }\n\n // ── Render ─────────────────────────────────────────────────────────\n function render(): void {\n const state = getState();\n\n computeScale();\n\n // Clear existing node rects\n nodesGroup.innerHTML = '';\n\n // Offset to center the bounds in the minimap\n const offsetX = (MINIMAP_WIDTH - cachedBounds.width / cachedScale) / 2;\n const offsetY = (MINIMAP_HEIGHT - cachedBounds.height / cachedScale) / 2;\n\n for (const node of state.nodes) {\n if (node.hidden) continue;\n const rect = document.createElementNS(SVG_NS, 'rect');\n const w = (node.dimensions?.width ?? DEFAULT_NODE_WIDTH) / cachedScale;\n const h = (node.dimensions?.height ?? DEFAULT_NODE_HEIGHT) / cachedScale;\n const x = (node.position.x - cachedBounds.x) / cachedScale + offsetX;\n const y = (node.position.y - cachedBounds.y) / cachedScale + offsetY;\n\n rect.setAttribute('x', String(x));\n rect.setAttribute('y', String(y));\n rect.setAttribute('width', String(w));\n rect.setAttribute('height', String(h));\n rect.setAttribute('rx', '2');\n // Fill controlled via CSS --flow-minimap-node-color; override with inline style\n // when user configured minimapNodeColor (inline style beats CSS property).\n const fill = getNodeFill(node);\n if (fill) {\n rect.style.fill = fill;\n }\n nodesGroup.appendChild(rect);\n }\n\n updateViewport();\n }\n\n // ── Viewport mask ──────────────────────────────────────────────────\n function updateViewport(): void {\n // Lean path: only viewport + container size are read below, so skip the\n // per-frame full-node remap that getState performs.\n const state = options.getViewportState ? options.getViewportState() : getState();\n\n if (cachedBounds.width === 0 && cachedBounds.height === 0) {\n maskPath.setAttribute('d', '');\n return;\n }\n\n const offsetX = (MINIMAP_WIDTH - cachedBounds.width / cachedScale) / 2;\n const offsetY = (MINIMAP_HEIGHT - cachedBounds.height / cachedScale) / 2;\n\n // Viewport rect in flow coordinates → minimap coordinates\n const vpX = (-state.viewport.x / state.viewport.zoom - cachedBounds.x) / cachedScale + offsetX;\n const vpY = (-state.viewport.y / state.viewport.zoom - cachedBounds.y) / cachedScale + offsetY;\n const vpW = (state.containerWidth / state.viewport.zoom) / cachedScale;\n const vpH = (state.containerHeight / state.viewport.zoom) / cachedScale;\n\n // Evenodd path: outer rect minus inner viewport rect\n const outer = `M0,0 H${MINIMAP_WIDTH} V${MINIMAP_HEIGHT} H0 Z`;\n const inner = `M${vpX},${vpY} h${vpW} v${vpH} h${-vpW} Z`;\n maskPath.setAttribute('d', `${outer} ${inner}`);\n }\n\n // ── Pan interaction ────────────────────────────────────────────────\n let isPanning = false;\n\n function minimapToFlowPosition(mmX: number, mmY: number): { x: number; y: number } {\n const offsetX = (MINIMAP_WIDTH - cachedBounds.width / cachedScale) / 2;\n const offsetY = (MINIMAP_HEIGHT - cachedBounds.height / cachedScale) / 2;\n\n const flowX = (mmX - offsetX) * cachedScale + cachedBounds.x;\n const flowY = (mmY - offsetY) * cachedScale + cachedBounds.y;\n\n return { x: flowX, y: flowY };\n }\n\n function handlePan(e: PointerEvent): void {\n const rect = svg.getBoundingClientRect();\n const mmX = e.clientX - rect.left;\n const mmY = e.clientY - rect.top;\n\n const state = getState();\n const flow = minimapToFlowPosition(mmX, mmY);\n\n // Center the viewport on the clicked flow position\n const newX = -flow.x * state.viewport.zoom + state.containerWidth / 2;\n const newY = -flow.y * state.viewport.zoom + state.containerHeight / 2;\n\n setViewport({ x: newX, y: newY, zoom: state.viewport.zoom });\n }\n\n function onPointerDown(e: PointerEvent): void {\n if (!config.minimapPannable) {\n return;\n }\n isPanning = true;\n svg.setPointerCapture(e.pointerId);\n handlePan(e);\n }\n\n function onPointerMove(e: PointerEvent): void {\n if (!isPanning) {\n return;\n }\n handlePan(e);\n }\n\n function onPointerUp(e: PointerEvent): void {\n if (!isPanning) {\n return;\n }\n isPanning = false;\n svg.releasePointerCapture(e.pointerId);\n }\n\n svg.addEventListener('pointerdown', onPointerDown);\n svg.addEventListener('pointermove', onPointerMove);\n svg.addEventListener('pointerup', onPointerUp);\n\n // ── Zoom interaction ───────────────────────────────────────────────\n function onWheel(e: WheelEvent): void {\n if (!config.minimapZoomable) {\n return;\n }\n e.preventDefault();\n\n const state = getState();\n const minZoom = config.minZoom ?? 0.5;\n const maxZoom = config.maxZoom ?? 2;\n const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;\n const newZoom = Math.min(Math.max(state.viewport.zoom * zoomFactor, minZoom), maxZoom);\n\n setViewport({ zoom: newZoom });\n }\n\n svg.addEventListener('wheel', onWheel, { passive: false });\n\n // ── Destroy ────────────────────────────────────────────────────────\n function destroy(): void {\n svg.removeEventListener('pointerdown', onPointerDown);\n svg.removeEventListener('pointermove', onPointerMove);\n svg.removeEventListener('pointerup', onPointerUp);\n svg.removeEventListener('wheel', onWheel);\n wrapper.remove();\n }\n\n return { render, updateViewport, destroy };\n}\n","// ============================================================================\n// Controls Panel\n//\n// A floating toolbar overlay with zoom in/out, fit view, and interactivity\n// lock toggle buttons. Follows the same DOM injection pattern as MiniMap.\n// ============================================================================\n\nexport interface ControlsPanelOptions {\n position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';\n orientation: 'vertical' | 'horizontal';\n showZoom: boolean;\n showFitView: boolean;\n showInteractive: boolean;\n showResetPanels: boolean;\n external?: boolean;\n onZoomIn: () => void;\n onZoomOut: () => void;\n onFitView: () => void;\n onToggleInteractive: () => void;\n onResetPanels: () => void;\n /**\n * Optional fullscreen toggle handler. When provided, a fullscreen\n * button is appended to the panel. When omitted, no button is rendered\n * — keeping the control row count stable for existing consumers.\n */\n onToggleFullscreen?: () => void;\n}\n\nexport interface ControlsPanelInstance {\n /** Update button state (interactive icon + fullscreen active class) */\n update(state: { isInteractive?: boolean; isFullscreen?: boolean }): void;\n /** Remove the panel from the DOM */\n destroy(): void;\n}\n\n// ── SVG Icon Paths ──────────────────────────────────────────────────────\nconst ICON_ZOOM_IN = '';\n\nconst ICON_ZOOM_OUT = '';\n\nconst ICON_FIT_VIEW = '';\n\nconst ICON_UNLOCK = '';\n\nconst ICON_LOCK = '';\n\nconst ICON_RESET_PANELS = '';\n\n// Fullscreen icons: four L-shaped corners. Expand points outward, collapse points inward.\nconst ICON_FULLSCREEN_ENTER = '';\n\nconst ICON_FULLSCREEN_EXIT = '';\n\nexport function createControlsPanel(\n container: HTMLElement,\n options: ControlsPanelOptions,\n): ControlsPanelInstance {\n const {\n position,\n orientation,\n showZoom,\n showFitView,\n showInteractive,\n showResetPanels,\n external,\n onZoomIn,\n onZoomOut,\n onFitView,\n onToggleInteractive,\n onResetPanels,\n onToggleFullscreen,\n } = options;\n\n // ── Build DOM ──────────────────────────────────────────────────────\n const wrapper = document.createElement('div');\n const classes = [\n 'flow-controls',\n `flow-controls-${orientation}`,\n ];\n if (external) {\n classes.push('flow-controls-external');\n } else {\n classes.push(`flow-controls-${position}`);\n }\n wrapper.className = classes.join(' ');\n wrapper.setAttribute('role', 'toolbar');\n wrapper.setAttribute('aria-label', 'Flow controls');\n\n let interactiveBtn: HTMLButtonElement | null = null;\n let fullscreenBtn: HTMLButtonElement | null = null;\n\n if (showZoom) {\n const zoomInBtn = createButton(ICON_ZOOM_IN, 'Zoom in', onZoomIn);\n const zoomOutBtn = createButton(ICON_ZOOM_OUT, 'Zoom out', onZoomOut);\n wrapper.appendChild(zoomInBtn);\n wrapper.appendChild(zoomOutBtn);\n }\n\n if (showFitView) {\n const fitBtn = createButton(ICON_FIT_VIEW, 'Fit view', onFitView);\n wrapper.appendChild(fitBtn);\n }\n\n if (showInteractive) {\n interactiveBtn = createButton(ICON_UNLOCK, 'Toggle interactivity', onToggleInteractive);\n wrapper.appendChild(interactiveBtn);\n }\n\n if (showResetPanels) {\n const resetBtn = createButton(ICON_RESET_PANELS, 'Reset panels', onResetPanels);\n wrapper.appendChild(resetBtn);\n }\n\n if (onToggleFullscreen) {\n fullscreenBtn = createButton(ICON_FULLSCREEN_ENTER, 'Toggle fullscreen', onToggleFullscreen);\n fullscreenBtn.classList.add('flow-controls-button-fullscreen');\n wrapper.appendChild(fullscreenBtn);\n }\n\n // Prevent pan/zoom from triggering when interacting with buttons\n wrapper.addEventListener('mousedown', (e) => e.stopPropagation());\n wrapper.addEventListener('pointerdown', (e) => e.stopPropagation());\n wrapper.addEventListener('wheel', (e) => e.stopPropagation(), { passive: false });\n\n container.appendChild(wrapper);\n\n // ── API ────────────────────────────────────────────────────────────\n function update(state: { isInteractive?: boolean; isFullscreen?: boolean }): void {\n if (interactiveBtn && typeof state.isInteractive === 'boolean') {\n setButtonIcon(interactiveBtn, state.isInteractive ? ICON_UNLOCK : ICON_LOCK);\n const interactiveTitle = state.isInteractive ? 'Lock interactivity' : 'Unlock interactivity';\n interactiveBtn.title = interactiveTitle;\n interactiveBtn.setAttribute('aria-label', interactiveTitle);\n }\n if (fullscreenBtn && typeof state.isFullscreen === 'boolean') {\n setButtonIcon(fullscreenBtn, state.isFullscreen ? ICON_FULLSCREEN_EXIT : ICON_FULLSCREEN_ENTER);\n const fullscreenTitle = state.isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen';\n fullscreenBtn.title = fullscreenTitle;\n fullscreenBtn.setAttribute('aria-label', fullscreenTitle);\n fullscreenBtn.classList.toggle('flow-controls-button-fullscreen--active', state.isFullscreen);\n }\n }\n\n function destroy(): void {\n wrapper.remove();\n }\n\n return { update, destroy };\n}\n\nfunction createButton(icon: string, title: string, onClick: () => void): HTMLButtonElement {\n const btn = document.createElement('button');\n btn.type = 'button';\n setButtonIcon(btn, icon);\n btn.title = title;\n btn.setAttribute('aria-label', title);\n btn.addEventListener('click', onClick);\n return btn;\n}\n\n/**\n * Replace a button's icon with a parsed SVG node. Safer than innerHTML\n * since icons are static constants parsed into DOM rather than rendered\n * as markup each time.\n */\nfunction setButtonIcon(btn: HTMLButtonElement, icon: string): void {\n const doc = new DOMParser().parseFromString(icon, 'image/svg+xml');\n const svg = doc.documentElement;\n btn.replaceChildren(svg);\n}\n","// ============================================================================\n// Selection Box\n//\n// A translucent rectangle drawn over the canvas during shift+drag to select\n// multiple nodes at once. Follows the same DOM injection pattern as MiniMap\n// and Controls Panel.\n// ============================================================================\n\nimport type { Rect, Viewport } from './types';\n\nexport interface SelectionBoxInstance {\n /** Start a new selection at a position relative to the container */\n start(containerX: number, containerY: number, mode?: 'partial' | 'full'): void;\n /** Update the selection box to a new position relative to the container */\n update(containerX: number, containerY: number): void;\n /** Finish selection and return the rect in flow coordinates */\n end(viewport: Viewport): Rect | null;\n /** Whether a selection is in progress */\n isActive(): boolean;\n /** Remove from DOM */\n destroy(): void;\n}\n\nconst MIN_SELECTION_SIZE = 5;\n\nexport function createSelectionBox(container: HTMLElement): SelectionBoxInstance {\n const el = document.createElement('div');\n el.className = 'flow-selection-box';\n container.appendChild(el);\n\n let active = false;\n let originX = 0;\n let originY = 0;\n let currentX = 0;\n let currentY = 0;\n\n function start(containerX: number, containerY: number, mode: 'partial' | 'full' = 'partial'): void {\n originX = containerX;\n originY = containerY;\n currentX = containerX;\n currentY = containerY;\n active = true;\n\n el.style.left = `${containerX}px`;\n el.style.top = `${containerY}px`;\n el.style.width = '0px';\n el.style.height = '0px';\n el.classList.remove('flow-selection-partial', 'flow-selection-full');\n el.classList.add('flow-selection-box-active', `flow-selection-${mode}`);\n }\n\n function update(containerX: number, containerY: number): void {\n if (!active) {\n return;\n }\n\n currentX = containerX;\n currentY = containerY;\n\n const left = Math.min(originX, currentX);\n const top = Math.min(originY, currentY);\n const width = Math.abs(currentX - originX);\n const height = Math.abs(currentY - originY);\n\n el.style.left = `${left}px`;\n el.style.top = `${top}px`;\n el.style.width = `${width}px`;\n el.style.height = `${height}px`;\n }\n\n function end(viewport: Viewport): Rect | null {\n if (!active) {\n return null;\n }\n\n active = false;\n el.classList.remove('flow-selection-box-active');\n el.classList.remove('flow-selection-partial', 'flow-selection-full');\n\n const screenWidth = Math.abs(currentX - originX);\n const screenHeight = Math.abs(currentY - originY);\n\n if (screenWidth < MIN_SELECTION_SIZE && screenHeight < MIN_SELECTION_SIZE) {\n return null;\n }\n\n // Convert screen-space rect (relative to container) to flow coordinates\n const left = Math.min(originX, currentX);\n const top = Math.min(originY, currentY);\n\n const flowX = (left - viewport.x) / viewport.zoom;\n const flowY = (top - viewport.y) / viewport.zoom;\n const flowWidth = screenWidth / viewport.zoom;\n const flowHeight = screenHeight / viewport.zoom;\n\n return { x: flowX, y: flowY, width: flowWidth, height: flowHeight };\n }\n\n function isActive(): boolean {\n return active;\n }\n\n function destroy(): void {\n el.remove();\n }\n\n return { start, update, end, isActive, destroy };\n}\n","// ============================================================================\n// Lasso Selection Overlay\n//\n// Renders an SVG polyline during freeform drag selection. Collects pointer\n// positions, simplifies with a distance threshold, and converts to flow\n// coordinates on completion. Mirrors the SelectionBoxInstance API pattern.\n// ============================================================================\n\nimport type { Viewport } from './types';\nimport type { Point } from './lasso-hit-test';\n\n/** Minimum distance (screen pixels) between consecutive lasso points. */\nconst MIN_POINT_DISTANCE = 3;\n\nexport interface LassoInstance {\n start(containerX: number, containerY: number, mode?: 'partial' | 'full'): void;\n update(containerX: number, containerY: number): void;\n end(viewport: Viewport): Point[] | null;\n isActive(): boolean;\n destroy(): void;\n}\n\nexport function createLasso(container: HTMLElement): LassoInstance {\n // Create SVG overlay\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.classList.add('flow-lasso-svg');\n svg.setAttribute('width', '100%');\n svg.setAttribute('height', '100%');\n container.appendChild(svg);\n\n const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');\n polyline.classList.add('flow-lasso-path');\n svg.appendChild(polyline);\n\n let active = false;\n let points: { x: number; y: number }[] = [];\n\n function start(containerX: number, containerY: number, mode: 'partial' | 'full' = 'partial'): void {\n active = true;\n points = [{ x: containerX, y: containerY }];\n\n svg.classList.remove('flow-lasso-partial', 'flow-lasso-full');\n svg.classList.add('flow-lasso-active', `flow-lasso-${mode}`);\n polyline.setAttribute('points', `${containerX},${containerY}`);\n }\n\n function update(containerX: number, containerY: number): void {\n if (!active) {\n return;\n }\n\n // Deduplicate: skip if too close to last point\n const last = points[points.length - 1];\n const dx = containerX - last.x;\n const dy = containerY - last.y;\n if (dx * dx + dy * dy < MIN_POINT_DISTANCE * MIN_POINT_DISTANCE) {\n return;\n }\n\n points.push({ x: containerX, y: containerY });\n polyline.setAttribute('points', points.map(p => `${p.x},${p.y}`).join(' '));\n }\n\n function end(viewport: Viewport): Point[] | null {\n if (!active) {\n return null;\n }\n\n active = false;\n svg.classList.remove('flow-lasso-active', 'flow-lasso-partial', 'flow-lasso-full');\n polyline.setAttribute('points', '');\n\n // Need at least 3 points for a polygon\n if (points.length < 3) {\n return null;\n }\n\n // Convert container-space points to flow-space\n const result = points.map(p => ({\n x: (p.x - viewport.x) / viewport.zoom,\n y: (p.y - viewport.y) / viewport.zoom,\n }));\n points = [];\n return result;\n }\n\n function isActive(): boolean {\n return active;\n }\n\n function destroy(): void {\n svg.remove();\n }\n\n return { start, update, end, isActive, destroy };\n}\n","// ============================================================================\n// Lasso Hit Testing\n//\n// Point-in-polygon (ray-casting) and polygon-AABB intersection utilities\n// for freeform lasso selection.\n// ============================================================================\n\nimport type { FlowNode, Rect } from './types';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT, getRotatedBounds } from './geometry';\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/**\n * Ray-casting algorithm: cast a horizontal ray from (px, py) to the right\n * and count how many polygon edges it crosses. Odd = inside, even = outside.\n */\nexport function pointInPolygon(px: number, py: number, polygon: Point[]): boolean {\n if (polygon.length < 3) return false;\n\n let inside = false;\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n const xi = polygon[i].x, yi = polygon[i].y;\n const xj = polygon[j].x, yj = polygon[j].y;\n\n if ((yi > py) !== (yj > py) &&\n px < (xj - xi) * (py - yi) / (yj - yi) + xi) {\n inside = !inside;\n }\n }\n return inside;\n}\n\n/**\n * Test whether two line segments (p1->p2) and (p3->p4) intersect.\n */\nfunction segmentsIntersect(\n x1: number, y1: number, x2: number, y2: number,\n x3: number, y3: number, x4: number, y4: number,\n): boolean {\n const d1x = x2 - x1, d1y = y2 - y1;\n const d2x = x4 - x3, d2y = y4 - y3;\n const cross = d1x * d2y - d1y * d2x;\n if (Math.abs(cross) < 1e-10) return false; // parallel\n\n const dx = x3 - x1, dy = y3 - y1;\n const t = (dx * d2y - dy * d2x) / cross;\n const u = (dx * d1y - dy * d1x) / cross;\n\n return t >= 0 && t <= 1 && u >= 0 && u <= 1;\n}\n\n/**\n * Test whether any edge of the polygon intersects the AABB, or if the AABB\n * center lies inside the polygon.\n */\nexport function polygonIntersectsAABB(polygon: Point[], rect: Rect): boolean {\n const rx = rect.x;\n const ry = rect.y;\n const rr = rect.x + rect.width;\n const rb = rect.y + rect.height;\n\n // Quick check: is the AABB center inside the polygon?\n const cx = rx + rect.width / 2;\n const cy = ry + rect.height / 2;\n if (pointInPolygon(cx, cy, polygon)) return true;\n\n // Check if any polygon vertex is inside the AABB\n for (const p of polygon) {\n if (p.x >= rx && p.x <= rr && p.y >= ry && p.y <= rb) return true;\n }\n\n // Check if any polygon edge intersects any AABB edge\n const aabbEdges: [number, number, number, number][] = [\n [rx, ry, rr, ry], // top\n [rr, ry, rr, rb], // right\n [rr, rb, rx, rb], // bottom\n [rx, rb, rx, ry], // left\n ];\n\n for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {\n for (const [ax, ay, bx, by] of aabbEdges) {\n if (segmentsIntersect(polygon[j].x, polygon[j].y, polygon[i].x, polygon[i].y, ax, ay, bx, by)) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/** Get the AABB for a node, accounting for rotation. */\nfunction getNodeAABB(node: FlowNode): Rect {\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n if (node.rotation) {\n return getRotatedBounds(node.position.x, node.position.y, w, h, node.rotation);\n }\n return { x: node.position.x, y: node.position.y, width: w, height: h };\n}\n\n/**\n * Return nodes that partially overlap the lasso polygon (center inside OR\n * AABB edges intersect polygon edges). Skips hidden and non-selectable nodes.\n */\nexport function getNodesInPolygon(nodes: FlowNode[], polygon: Point[]): FlowNode[] {\n if (polygon.length < 3) return [];\n return nodes.filter(node => {\n if (node.hidden || node.selectable === false) return false;\n const aabb = getNodeAABB(node);\n return polygonIntersectsAABB(polygon, aabb);\n });\n}\n\n/**\n * Return nodes fully enclosed by the lasso polygon (all four AABB corners\n * inside). Skips hidden and non-selectable nodes.\n */\nexport function getNodesFullyInPolygon(nodes: FlowNode[], polygon: Point[]): FlowNode[] {\n if (polygon.length < 3) return [];\n return nodes.filter(node => {\n if (node.hidden || node.selectable === false) return false;\n const aabb = getNodeAABB(node);\n const corners = [\n { x: aabb.x, y: aabb.y },\n { x: aabb.x + aabb.width, y: aabb.y },\n { x: aabb.x + aabb.width, y: aabb.y + aabb.height },\n { x: aabb.x, y: aabb.y + aabb.height },\n ];\n return corners.every(c => pointInPolygon(c.x, c.y, polygon));\n });\n}\n","// ============================================================================\n// Graph Utilities\n//\n// Pure graph traversal functions. Forked from @xyflow/system utils/graph.ts\n// (MIT License, Copyright 2019-2025 webkid GmbH). Simplified to work with\n// plain objects instead of Map-based stores.\n// ============================================================================\n\nimport type { FlowNode, FlowEdge } from './types';\n\n/**\n * Get all edges connected to a node (incoming and outgoing).\n */\nexport function getConnectedEdges(nodeId: string, edges: FlowEdge[]): FlowEdge[] {\n return edges.filter((e) => e.source === nodeId || e.target === nodeId);\n}\n\n/**\n * Get all edges connected to any of the given nodes.\n */\nexport function getConnectedEdgesForNodes(nodeIds: string[], edges: FlowEdge[]): FlowEdge[] {\n const idSet = new Set(nodeIds);\n return edges.filter((e) => idSet.has(e.source) || idSet.has(e.target));\n}\n\n/**\n * Get all nodes that a given node connects to (outgoing edges).\n */\nexport function getOutgoers(nodeId: string, nodes: FlowNode[], edges: FlowEdge[]): FlowNode[] {\n const outgoingIds = new Set(\n edges.filter((e) => e.source === nodeId).map((e) => e.target),\n );\n return nodes.filter((n) => outgoingIds.has(n.id));\n}\n\n/**\n * Get all nodes that connect to a given node (incoming edges).\n */\nexport function getIncomers(nodeId: string, nodes: FlowNode[], edges: FlowEdge[]): FlowNode[] {\n const incomingIds = new Set(\n edges.filter((e) => e.target === nodeId).map((e) => e.source),\n );\n return nodes.filter((n) => incomingIds.has(n.id));\n}\n\n/**\n * Check whether adding an edge from `source` to `target` would create a cycle\n * in the directed graph defined by `edges`. Uses iterative DFS from `target`\n * following outgoing edges — if `source` is reachable, the new edge would\n * close a loop.\n */\nexport function wouldCreateCycle(\n source: string,\n target: string,\n edges: { source: string; target: string }[],\n): boolean {\n // Self-loop is a trivial cycle\n if (source === target) return true;\n\n // Build adjacency list (outgoing edges only)\n const adj = new Map();\n for (const e of edges) {\n let list = adj.get(e.source);\n if (!list) {\n list = [];\n adj.set(e.source, list);\n }\n list.push(e.target);\n }\n\n // Iterative DFS from target — can we reach source?\n const stack = [target];\n const visited = new Set();\n\n while (stack.length > 0) {\n const node = stack.pop()!;\n if (node === source) return true;\n if (visited.has(node)) continue;\n visited.add(node);\n\n const neighbors = adj.get(node);\n if (neighbors) {\n for (const n of neighbors) {\n if (!visited.has(n)) stack.push(n);\n }\n }\n }\n\n return false;\n}\n\n/**\n * Check if two nodes are directly connected by an edge.\n */\nexport function areNodesConnected(\n nodeA: string,\n nodeB: string,\n edges: FlowEdge[],\n directed: boolean = false,\n): boolean {\n return edges.some((e) => {\n if (directed) {\n return e.source === nodeA && e.target === nodeB;\n }\n return (\n (e.source === nodeA && e.target === nodeB) ||\n (e.source === nodeB && e.target === nodeA)\n );\n });\n}\n\n/**\n * Compute bridge edges that reconnect predecessors to successors\n * when middle nodes are deleted. For each deleted node that has both\n * incoming and outgoing edges (to/from non-deleted nodes), creates\n * edges from each predecessor to each successor.\n */\nexport function computeReconnectionEdges(\n deletionIds: Set,\n nodes: FlowNode[],\n edges: FlowEdge[],\n): FlowEdge[] {\n const nodeMap = new Map(nodes.map((n) => [n.id, n]));\n // Build set of existing source+target+handles combos for dedup\n const existingKeys = new Set(\n edges.map((e) => `${e.source}|${e.target}|${e.sourceHandle ?? ''}|${e.targetHandle ?? ''}`),\n );\n const result: FlowEdge[] = [];\n const newKeys = new Set();\n let counter = 0;\n\n for (const nodeId of deletionIds) {\n const node = nodeMap.get(nodeId);\n if (node?.reconnectOnDelete === false) continue;\n\n // Find incoming edges from non-deleted sources\n const incoming = edges.filter(\n (e) => e.target === nodeId && !deletionIds.has(e.source),\n );\n // Find outgoing edges to non-deleted targets\n const outgoing = edges.filter(\n (e) => e.source === nodeId && !deletionIds.has(e.target),\n );\n\n if (incoming.length === 0 || outgoing.length === 0) continue;\n\n for (const inc of incoming) {\n for (const out of outgoing) {\n // Skip self-loops\n if (inc.source === out.target) continue;\n\n const key = `${inc.source}|${out.target}|${inc.sourceHandle ?? ''}|${out.targetHandle ?? ''}`;\n // Skip if edge already exists or already created in this batch\n if (existingKeys.has(key) || newKeys.has(key)) continue;\n\n const bridge: FlowEdge = {\n id: `reconnect-${inc.source}-${out.target}-${counter++}`,\n source: inc.source,\n target: out.target,\n sourceHandle: inc.sourceHandle,\n targetHandle: out.targetHandle,\n };\n\n // Inherit visual properties from incoming edge\n if (inc.type) bridge.type = inc.type;\n if (inc.animated !== undefined) bridge.animated = inc.animated;\n if (inc.style) bridge.style = inc.style;\n if (inc.class) bridge.class = inc.class;\n if (inc.markerEnd) bridge.markerEnd = inc.markerEnd;\n if (inc.markerStart) bridge.markerStart = inc.markerStart;\n if (inc.label) bridge.label = inc.label;\n\n newKeys.add(key);\n result.push(bridge);\n }\n }\n }\n\n return result;\n}\n","// ============================================================================\n// Connection Controller\n//\n// Handles drag-to-connect interactions between source and target handles.\n// Reworked from @xyflow/system XYHandle (MIT License, Copyright 2019-2025\n// webkid GmbH). Split into pure utilities + interaction controller.\n//\n// Phase 4 implementation.\n// ============================================================================\n\nimport type { XYPosition, HandlePosition, Connection, ConnectionRules, FlowNode, PendingConnection } from './types';\nimport { wouldCreateCycle } from './graph';\n\nexport interface HandleBounds {\n nodeId: string;\n handleId: string;\n type: 'source' | 'target';\n position: HandlePosition;\n /** Absolute position of the handle center in flow coordinates */\n absolutePosition: XYPosition;\n width: number;\n height: number;\n}\n\n/**\n * Check if a point is within the hit area of a handle.\n */\nexport function isPointInHandle(\n point: XYPosition,\n handle: HandleBounds,\n tolerance: number = 10,\n): boolean {\n const halfW = handle.width / 2 + tolerance;\n const halfH = handle.height / 2 + tolerance;\n\n return (\n Math.abs(point.x - handle.absolutePosition.x) <= halfW &&\n Math.abs(point.y - handle.absolutePosition.y) <= halfH\n );\n}\n\n/**\n * Find the closest target handle to a given point.\n */\nexport function findClosestHandle(\n point: XYPosition,\n handles: HandleBounds[],\n tolerance: number = 20,\n): HandleBounds | null {\n let closest: HandleBounds | null = null;\n let closestDist = Infinity;\n\n for (const handle of handles) {\n const dx = point.x - handle.absolutePosition.x;\n const dy = point.y - handle.absolutePosition.y;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n if (dist < tolerance && dist < closestDist) {\n closest = handle;\n closestDist = dist;\n }\n }\n\n return closest;\n}\n\n/**\n * Check canvas-level `connectionRules` against a proposed connection.\n *\n * Evaluation:\n * 1. `byType` map — if source type has an entry, target type must be listed.\n * Source types with no entry are unrestricted.\n * 2. `validate` function — if present, must return true.\n *\n * Returns true when no rules are configured or all configured rules pass.\n * Returns false as soon as any rule rejects.\n */\nexport function checkConnectionRules(\n connection: Connection,\n rules: ConnectionRules | undefined,\n nodeMap: Map,\n): boolean {\n if (!rules) return true;\n\n const source = nodeMap.get(connection.source);\n const target = nodeMap.get(connection.target);\n\n // If either node is unknown let downstream handle it\n if (!source || !target) return true;\n\n // Map-based type check\n const { byType } = rules;\n if (byType) {\n const sourceType = source.type ?? '';\n if (Object.prototype.hasOwnProperty.call(byType, sourceType)) {\n const allowed = byType[sourceType];\n if (!allowed.includes(target.type ?? '')) return false;\n }\n }\n\n // Function-based check\n if (typeof rules.validate === 'function') {\n if (!rules.validate(connection, source, target)) return false;\n }\n\n return true;\n}\n\n/**\n * Validate whether a connection is allowed.\n * Basic rules: no self-connections, no duplicate edges.\n * Optional: reject connections that would create a directed cycle.\n */\nexport function isValidConnection(\n connection: Connection,\n existingEdges: { source: string; target: string; sourceHandle?: string; targetHandle?: string }[],\n options?: { preventCycles?: boolean },\n): boolean {\n // No self-connections\n if (connection.source === connection.target) {\n return false;\n }\n\n // No duplicate edges (same source+target+handles)\n const isDuplicate = existingEdges.some(\n (e) =>\n e.source === connection.source &&\n e.target === connection.target &&\n e.sourceHandle === connection.sourceHandle &&\n e.targetHandle === connection.targetHandle,\n );\n if (isDuplicate) return false;\n\n // Cycle detection\n if (options?.preventCycles && wouldCreateCycle(connection.source, connection.target, existingEdges)) {\n return false;\n }\n\n return true;\n}\n","// ============================================================================\n// x-flow-handle-validate Directive\n//\n// Stores a per-handle connection validator on the handle element.\n// The validator is called during connection completion with a Connection object\n// and must return a boolean.\n//\n// Usage:\n//
\n//
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type { Connection } from '../../core/types';\n\n/** Expando property name for the validator function on handle elements. */\nexport const HANDLE_VALIDATE_KEY = '_flowHandleValidate';\n\ndeclare global {\n interface HTMLElement {\n [HANDLE_VALIDATE_KEY]?: (connection: Connection) => boolean;\n }\n}\n\nexport function registerFlowHandleValidateDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-handle-validate',\n (\n el,\n { expression },\n { evaluate, effect, cleanup },\n ) => {\n function resolve(): void {\n // First try Alpine evaluate (works for inline expressions)\n let validator: unknown;\n try {\n validator = evaluate(expression);\n } catch {\n // Expression may reference a name on $data — look it up directly\n const data = (Alpine as any).$data(el);\n if (data && typeof data[expression] === 'function') {\n validator = data[expression];\n }\n }\n\n if (typeof validator === 'function') {\n el[HANDLE_VALIDATE_KEY] = validator as (connection: Connection) => boolean;\n } else {\n delete el[HANDLE_VALIDATE_KEY];\n // Retry once after next frame to allow x-init to define the function\n requestAnimationFrame(() => {\n const data = (Alpine as any).$data(el);\n if (data && typeof data[expression] === 'function') {\n el[HANDLE_VALIDATE_KEY] = data[expression];\n }\n });\n }\n }\n\n effect(() => {\n resolve();\n });\n\n cleanup(() => {\n delete el[HANDLE_VALIDATE_KEY];\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-handle-limit Directive\n//\n// Stores a per-handle connection limit on the handle element.\n// The limit is checked during connection validation — if the handle already\n// has >= limit connections, new connections are rejected.\n//\n// Usage:\n//
\n//
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\n/** Expando property name for the connection limit on handle elements. */\nexport const HANDLE_LIMIT_KEY = '_flowHandleLimit';\n\ndeclare global {\n interface HTMLElement {\n [HANDLE_LIMIT_KEY]?: number;\n }\n}\n\nexport function registerFlowHandleLimitDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-handle-limit',\n (\n el,\n { expression },\n { evaluate, effect, cleanup },\n ) => {\n effect(() => {\n const limit = Number(evaluate(expression));\n if (limit > 0) {\n el[HANDLE_LIMIT_KEY] = limit;\n } else {\n delete el[HANDLE_LIMIT_KEY];\n }\n });\n\n cleanup(() => {\n delete el[HANDLE_LIMIT_KEY];\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-handle-connectable Directive\n//\n// Controls whether a handle can initiate connections (start) or receive\n// connections (end). By default both are true. Use this directive to disable\n// one or both directions per handle.\n//\n// Usage:\n//
\n//
\n//
(both)\n//
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\n/** Expando property: can this handle initiate a connection drag? */\nexport const HANDLE_CONNECTABLE_START_KEY = '_flowHandleConnectableStart';\n\n/** Expando property: can this handle receive a connection? */\nexport const HANDLE_CONNECTABLE_END_KEY = '_flowHandleConnectableEnd';\n\ndeclare global {\n interface HTMLElement {\n [HANDLE_CONNECTABLE_START_KEY]?: boolean;\n [HANDLE_CONNECTABLE_END_KEY]?: boolean;\n }\n}\n\nexport function registerFlowHandleConnectableDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-handle-connectable',\n (\n el,\n { expression, modifiers },\n { evaluate, effect, cleanup },\n ) => {\n const hasStart = modifiers.includes('start');\n const hasEnd = modifiers.includes('end');\n // No modifier → applies to both\n const applyStart = hasStart || (!hasStart && !hasEnd);\n const applyEnd = hasEnd || (!hasStart && !hasEnd);\n\n effect(() => {\n const value = expression ? Boolean(evaluate(expression)) : true;\n\n if (applyStart) {\n el[HANDLE_CONNECTABLE_START_KEY] = value;\n }\n if (applyEnd) {\n el[HANDLE_CONNECTABLE_END_KEY] = value;\n }\n });\n\n cleanup(() => {\n delete el[HANDLE_CONNECTABLE_START_KEY];\n delete el[HANDLE_CONNECTABLE_END_KEY];\n });\n },\n );\n}\n","/**\n * Minimal node shape for flag checking — allows partial node objects\n * from connection-utils and other contexts where the full FlowNode\n * type isn't available.\n */\ninterface NodeLike {\n locked?: boolean;\n draggable?: boolean;\n deletable?: boolean;\n connectable?: boolean;\n selectable?: boolean;\n resizable?: boolean;\n}\n\n/**\n * Check if a node allows a specific interaction.\n *\n * When `locked` is true, all interactions are blocked unless the\n * individual flag explicitly overrides. For example:\n * { locked: true, draggable: true } → draggable (explicit override wins)\n * { locked: true } → not draggable (locked blocks)\n * { draggable: false } → not draggable (individual flag blocks)\n * { } → draggable (default is true)\n */\nfunction isAllowed(node: NodeLike, flag: boolean | undefined, defaultValue = true): boolean {\n // Explicit individual flag always wins\n if (flag !== undefined) return flag;\n // If locked, block\n if (node.locked) return false;\n // Default\n return defaultValue;\n}\n\nexport function isDraggable(node: NodeLike): boolean {\n return isAllowed(node, node.draggable);\n}\n\nexport function isDeletable(node: NodeLike): boolean {\n return isAllowed(node, node.deletable);\n}\n\nexport function isConnectable(node: NodeLike): boolean {\n return isAllowed(node, node.connectable);\n}\n\nexport function isSelectable(node: NodeLike): boolean {\n return isAllowed(node, node.selectable);\n}\n\nexport function isResizable(node: NodeLike): boolean {\n return isAllowed(node, node.resizable);\n}\n","// ============================================================================\n// Edge Path Utilities\n//\n// Forked from @xyflow/system (MIT License, Copyright 2019-2025 webkid GmbH)\n// Pure functions: positions in → SVG path data + label position out.\n// ============================================================================\n\nimport type { XYPosition } from '../types';\n\nexport interface EdgePathResult {\n /** SVG path `d` attribute */\n path: string;\n /** Suggested position for a label at the midpoint */\n labelPosition: XYPosition;\n /** X offset for centering a label */\n labelOffsetX: number;\n /** Y offset for centering a label */\n labelOffsetY: number;\n}\n\n/**\n * Build a rounded SVG bend through three consecutive waypoints.\n * Returns an `L…Q…` fragment (or just `L` if the points are collinear).\n */\nexport function getBend(\n ax: number,\n ay: number,\n bx: number,\n by: number,\n cx: number,\n cy: number,\n radius: number,\n): string {\n const dx1 = bx - ax;\n const dy1 = by - ay;\n const dx2 = cx - bx;\n const dy2 = cy - by;\n\n // If points are collinear, just draw a line\n if ((dx1 === 0 && dx2 === 0) || (dy1 === 0 && dy2 === 0)) {\n return `L${bx},${by}`;\n }\n\n // Clamp radius to half the minimum segment length\n const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);\n const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);\n const r = Math.min(radius, len1 / 2, len2 / 2);\n\n // Points on the two segments where the arc begins/ends\n const p1x = bx - (dx1 / len1) * r;\n const p1y = by - (dy1 / len1) * r;\n const p2x = bx + (dx2 / len2) * r;\n const p2y = by + (dy2 / len2) * r;\n\n return `L${p1x},${p1y} Q${bx},${by} ${p2x},${p2y}`;\n}\n\n/**\n * Calculate the center point of an edge for label placement.\n */\nexport function getEdgeCenter({\n sourceX,\n sourceY,\n targetX,\n targetY,\n}: {\n sourceX: number;\n sourceY: number;\n targetX: number;\n targetY: number;\n}): { x: number; y: number; offsetX: number; offsetY: number } {\n const offsetX = Math.abs(targetX - sourceX) / 2;\n const offsetY = Math.abs(targetY - sourceY) / 2;\n\n return {\n x: (sourceX + targetX) / 2,\n y: (sourceY + targetY) / 2,\n offsetX,\n offsetY,\n };\n}\n","// ============================================================================\n// Bezier Edge Paths\n//\n// Forked from @xyflow/system (MIT License, Copyright 2019-2025 webkid GmbH)\n// Generates cubic bezier SVG paths between two points with handle-aware\n// control point calculation.\n// ============================================================================\n\nimport type { HandlePosition } from '../types';\nimport { getEdgeCenter, type EdgePathResult } from './utils';\n\ninterface BezierPathParams {\n sourceX: number;\n sourceY: number;\n sourcePosition?: HandlePosition;\n targetX: number;\n targetY: number;\n targetPosition?: HandlePosition;\n curvature?: number;\n}\n\n/**\n * Calculate control point offsets based on handle positions and curvature.\n */\nfunction calculateControlOffset(distance: number, curvature: number): number {\n if (distance >= 0) {\n return 0.5 * distance;\n }\n return curvature * 25 * Math.sqrt(-distance);\n}\n\nfunction getControlPoints({\n sourceX,\n sourceY,\n sourcePosition = 'bottom',\n targetX,\n targetY,\n targetPosition = 'top',\n curvature = 0.25,\n}: BezierPathParams): [number, number, number, number] {\n const isHorizontalSource = sourcePosition === 'left' || sourcePosition === 'right';\n const isHorizontalTarget = targetPosition === 'left' || targetPosition === 'right';\n\n const sourceControlX = isHorizontalSource\n ? sourceX + (sourcePosition === 'right' ? 1 : -1) * calculateControlOffset(\n sourcePosition === 'right' ? targetX - sourceX : sourceX - targetX,\n curvature,\n )\n : sourceX;\n\n const sourceControlY = isHorizontalSource\n ? sourceY\n : sourceY + (sourcePosition === 'bottom' ? 1 : -1) * calculateControlOffset(\n sourcePosition === 'bottom' ? targetY - sourceY : sourceY - targetY,\n curvature,\n );\n\n const targetControlX = isHorizontalTarget\n ? targetX + (targetPosition === 'right' ? 1 : -1) * calculateControlOffset(\n targetPosition === 'right' ? sourceX - targetX : targetX - sourceX,\n curvature,\n )\n : targetX;\n\n const targetControlY = isHorizontalTarget\n ? targetY\n : targetY + (targetPosition === 'bottom' ? 1 : -1) * calculateControlOffset(\n targetPosition === 'bottom' ? sourceY - targetY : targetY - sourceY,\n curvature,\n );\n\n return [sourceControlX, sourceControlY, targetControlX, targetControlY];\n}\n\n/**\n * Generate a cubic bezier edge path with handle-aware control points.\n */\nexport function getBezierPath(params: BezierPathParams): EdgePathResult {\n const { sourceX, sourceY, targetX, targetY } = params;\n const [sControlX, sControlY, tControlX, tControlY] = getControlPoints(params);\n\n const path = `M${sourceX},${sourceY} C${sControlX},${sControlY} ${tControlX},${tControlY} ${targetX},${targetY}`;\n const { x, y, offsetX, offsetY } = getEdgeCenter({ sourceX, sourceY, targetX, targetY });\n\n return {\n path,\n labelPosition: { x, y },\n labelOffsetX: offsetX,\n labelOffsetY: offsetY,\n };\n}\n\n/**\n * Generate a simple cubic bezier where control points are always at the\n * midpoint between source and target (no handle-position awareness).\n */\nexport function getSimpleBezierPath({\n sourceX,\n sourceY,\n targetX,\n targetY,\n}: {\n sourceX: number;\n sourceY: number;\n targetX: number;\n targetY: number;\n}): EdgePathResult {\n const midX = (sourceX + targetX) / 2;\n const midY = (sourceY + targetY) / 2;\n\n const path = `M${sourceX},${sourceY} C${midX},${sourceY} ${midX},${targetY} ${targetX},${targetY}`;\n const { x, y, offsetX, offsetY } = getEdgeCenter({ sourceX, sourceY, targetX, targetY });\n\n return {\n path,\n labelPosition: { x, y },\n labelOffsetX: offsetX,\n labelOffsetY: offsetY,\n };\n}\n","// ============================================================================\n// Smoothstep Edge Path\n//\n// Forked from @xyflow/system (MIT License, Copyright 2019-2025 webkid GmbH)\n// Generates paths with rounded right-angle bends, routing edges around nodes\n// based on handle positions.\n// ============================================================================\n\nimport type { HandlePosition } from '../types';\nimport { getBend, getEdgeCenter, type EdgePathResult } from './utils';\n\ninterface SmoothStepPathParams {\n sourceX: number;\n sourceY: number;\n sourcePosition?: HandlePosition;\n targetX: number;\n targetY: number;\n targetPosition?: HandlePosition;\n borderRadius?: number;\n /** Minimum offset from node before the first bend */\n offset?: number;\n}\n\nfunction getDirection(position: HandlePosition): { x: number; y: number } {\n switch (position) {\n case 'top':\n case 'top-left':\n case 'top-right':\n return { x: 0, y: -1 };\n case 'bottom':\n case 'bottom-left':\n case 'bottom-right':\n return { x: 0, y: 1 };\n case 'left':\n return { x: -1, y: 0 };\n case 'right':\n return { x: 1, y: 0 };\n }\n}\n\nfunction getPoints(\n sourceX: number,\n sourceY: number,\n sourcePosition: HandlePosition,\n targetX: number,\n targetY: number,\n targetPosition: HandlePosition,\n offset: number,\n): [number, number][] {\n const sourceDir = getDirection(sourcePosition);\n const targetDir = getDirection(targetPosition);\n\n const sourceOffsetX = sourceX + sourceDir.x * offset;\n const sourceOffsetY = sourceY + sourceDir.y * offset;\n const targetOffsetX = targetX + targetDir.x * offset;\n const targetOffsetY = targetY + targetDir.y * offset;\n\n const isSourceHorizontal = sourcePosition === 'left' || sourcePosition === 'right';\n const isTargetHorizontal = targetPosition === 'left' || targetPosition === 'right';\n\n // Both handles on same axis → simple L-bend or straight with one midpoint\n if (isSourceHorizontal === isTargetHorizontal) {\n const midX = (sourceOffsetX + targetOffsetX) / 2;\n const midY = (sourceOffsetY + targetOffsetY) / 2;\n\n if (isSourceHorizontal) {\n return [\n [sourceOffsetX, sourceY],\n [midX, sourceY],\n [midX, targetY],\n [targetOffsetX, targetY],\n ];\n }\n\n return [\n [sourceX, sourceOffsetY],\n [sourceX, midY],\n [targetX, midY],\n [targetX, targetOffsetY],\n ];\n }\n\n // Different axes → L-bend through intersection\n if (isSourceHorizontal) {\n return [\n [sourceOffsetX, sourceY],\n [targetX, sourceY],\n [targetX, targetOffsetY],\n ];\n }\n\n return [\n [sourceX, sourceOffsetY],\n [sourceX, targetY],\n [targetOffsetX, targetY],\n ];\n}\n\n/**\n * Generate a smoothstep (rounded right-angle) edge path.\n */\nexport function getSmoothStepPath({\n sourceX,\n sourceY,\n sourcePosition = 'bottom',\n targetX,\n targetY,\n targetPosition = 'top',\n borderRadius = 5,\n offset = 10,\n}: SmoothStepPathParams): EdgePathResult {\n const points = getPoints(\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n offset,\n );\n\n let path = `M${sourceX},${sourceY}`;\n\n for (let i = 0; i < points.length; i++) {\n const [px, py] = points[i];\n\n if (borderRadius > 0 && i > 0 && i < points.length - 1) {\n const [prevX, prevY] = i === 1 ? [sourceX, sourceY] : points[i - 1];\n const [nextX, nextY] = points[i + 1];\n path += ` ${getBend(prevX, prevY, px, py, nextX, nextY, borderRadius)}`;\n } else {\n path += ` L${px},${py}`;\n }\n }\n\n path += ` L${targetX},${targetY}`;\n\n const { x, y, offsetX, offsetY } = getEdgeCenter({ sourceX, sourceY, targetX, targetY });\n\n return {\n path,\n labelPosition: { x, y },\n labelOffsetX: offsetX,\n labelOffsetY: offsetY,\n };\n}\n\n/**\n * Generate a step (non-rounded right-angle) edge path.\n */\nexport function getStepPath(params: Omit): EdgePathResult {\n return getSmoothStepPath({ ...params, borderRadius: 0 });\n}\n","// ============================================================================\n// Straight Edge Path\n//\n// Forked from @xyflow/system (MIT License, Copyright 2019-2025 webkid GmbH)\n// A simple straight line between source and target.\n// ============================================================================\n\nimport { getEdgeCenter, type EdgePathResult } from './utils';\n\ninterface StraightPathParams {\n sourceX: number;\n sourceY: number;\n targetX: number;\n targetY: number;\n}\n\n/**\n * Generate a straight line edge path.\n */\nexport function getStraightPath({\n sourceX,\n sourceY,\n targetX,\n targetY,\n}: StraightPathParams): EdgePathResult {\n const path = `M${sourceX},${sourceY} L${targetX},${targetY}`;\n const { x, y, offsetX, offsetY } = getEdgeCenter({ sourceX, sourceY, targetX, targetY });\n\n return {\n path,\n labelPosition: { x, y },\n labelOffsetX: offsetX,\n labelOffsetY: offsetY,\n };\n}\n","// ============================================================================\n// Auto-Pan Module\n//\n// Automatically pans the viewport when the pointer is near the edge of the\n// canvas container during a drag operation (node drag or connection drag).\n// Uses requestAnimationFrame for smooth, continuous panning.\n// ============================================================================\n\n/** Distance in pixels from the container edge where auto-pan activates */\nconst EDGE_THRESHOLD = 40;\n\n/**\n * Compute the auto-pan delta for a single frame based on pointer proximity\n * to container edges. Returns { dx, dy } where each value is 0 when the\n * pointer is outside the detection zone, and scales linearly as the pointer\n * moves closer to the edge.\n */\nexport function getAutoPanDelta(\n pointerX: number,\n pointerY: number,\n containerRect: DOMRect,\n speed: number,\n): { dx: number; dy: number } {\n let dx = 0;\n let dy = 0;\n\n const distLeft = pointerX - containerRect.left;\n const distRight = containerRect.right - pointerX;\n const distTop = pointerY - containerRect.top;\n const distBottom = containerRect.bottom - pointerY;\n\n if (distLeft < EDGE_THRESHOLD && distLeft >= 0) {\n dx = -speed * (1 - distLeft / EDGE_THRESHOLD);\n } else if (distRight < EDGE_THRESHOLD && distRight >= 0) {\n dx = speed * (1 - distRight / EDGE_THRESHOLD);\n }\n\n if (distTop < EDGE_THRESHOLD && distTop >= 0) {\n dy = -speed * (1 - distTop / EDGE_THRESHOLD);\n } else if (distBottom < EDGE_THRESHOLD && distBottom >= 0) {\n dy = speed * (1 - distBottom / EDGE_THRESHOLD);\n }\n\n return { dx, dy };\n}\n\nexport interface AutoPanInstance {\n /** Start the rAF pan loop */\n start(): void;\n /** Stop the rAF pan loop */\n stop(): void;\n /** Feed the latest pointer position (client coordinates) */\n updatePointer(clientX: number, clientY: number): void;\n /** Stop and clean up */\n destroy(): void;\n}\n\nexport interface AutoPanOptions {\n /** The container element to read bounds from each frame */\n container: HTMLElement;\n /** Speed multiplier (default 15) */\n speed: number;\n /** Called each frame with the pixel delta to apply.\n * Return `true` to signal that a boundary was hit and auto-pan should stop. */\n onPan: (dx: number, dy: number) => void | boolean;\n /** Return true to prevent auto-pan from activating (e.g. during animation lock). */\n isLocked?: () => boolean;\n}\n\n/**\n * Create an auto-pan instance that drives a rAF loop. Call `updatePointer()`\n * on every pointermove/drag event; the loop reads the latest position each\n * frame and invokes `onPan` when the pointer is inside the edge zone.\n */\nexport function createAutoPan(options: AutoPanOptions): AutoPanInstance {\n const { container, speed, onPan } = options;\n\n let rafId: number | null = null;\n let pointerX = 0;\n let pointerY = 0;\n let running = false;\n\n function loop() {\n if (!running) {\n return;\n }\n\n const rect = container.getBoundingClientRect();\n const { dx, dy } = getAutoPanDelta(pointerX, pointerY, rect, speed);\n\n if (dx !== 0 || dy !== 0) {\n const hitBoundary = onPan(dx, dy);\n if (hitBoundary === true) {\n running = false;\n rafId = null;\n return;\n }\n }\n\n rafId = requestAnimationFrame(loop);\n }\n\n return {\n start() {\n if (running || options.isLocked?.()) {\n return;\n }\n running = true;\n rafId = requestAnimationFrame(loop);\n },\n\n stop() {\n running = false;\n if (rafId !== null) {\n cancelAnimationFrame(rafId);\n rafId = null;\n }\n },\n\n updatePointer(clientX: number, clientY: number) {\n pointerX = clientX;\n pointerY = clientY;\n },\n\n destroy() {\n this.stop();\n },\n };\n}\n","// ============================================================================\n// Connection Drag Utilities\n//\n// Shared helpers for connection and reconnection drag operations. Used by\n// both flow-handle and flow-edge directives to avoid duplicating the\n// temporary SVG line, snap-to-handle, and auto-pan logic.\n// ============================================================================\n\nimport type { XYPosition, Viewport, ConnectionLineProps, FlowCanvasConfig, FlowNode } from '../core/types';\nimport { isConnectable } from '../core/node-flags';\nimport type { HandleIndex } from './handle-index';\nimport {\n CONNECTION_ACTIVE_COLOR,\n CONNECTION_INVALID_COLOR,\n TEMP_LINE_STROKE_WIDTH,\n TEMP_LINE_DASH_PATTERN,\n} from '../core/constants';\nimport { getBezierPath } from '../core/edge-paths/bezier';\nimport { getSmoothStepPath, getStepPath } from '../core/edge-paths/smoothstep';\nimport { getStraightPath } from '../core/edge-paths/straight';\nimport { createAutoPan, type AutoPanInstance } from '../core/auto-pan';\nimport { HANDLE_CONNECTABLE_START_KEY, HANDLE_CONNECTABLE_END_KEY } from './directives/flow-handle-connectable';\n\n/**\n * Updatable connection line instance returned by `createConnectionLine()`.\n * Supports preset path types (straight, bezier, smoothstep, step) and a\n * fully custom SVG renderer callback.\n */\nexport interface ConnectionLineInstance {\n svg: SVGSVGElement;\n update(props: Omit): void;\n destroy(): void;\n}\n\n/**\n * Create a connection line with configurable path type, style, and optional\n * custom renderer. Returns an updatable instance whose `update()` method\n * recomputes the SVG path from new coordinates.\n */\nexport function createConnectionLine(config: {\n connectionLineType?: FlowCanvasConfig['connectionLineType'];\n connectionLineStyle?: FlowCanvasConfig['connectionLineStyle'];\n connectionLine?: FlowCanvasConfig['connectionLine'];\n invalid?: boolean;\n containerEl?: HTMLElement;\n}): ConnectionLineInstance {\n const lineType = config.connectionLineType ?? 'straight';\n\n const invalidColor = config.invalid\n ? (config.containerEl\n ? getComputedStyle(config.containerEl).getPropertyValue('--flow-connection-line-invalid').trim()\n : '')\n || CONNECTION_INVALID_COLOR\n : null;\n\n const style = {\n stroke: invalidColor ?? config.connectionLineStyle?.stroke\n ?? ((config.containerEl ? getComputedStyle(config.containerEl).getPropertyValue('--flow-edge-stroke-selected').trim() : '') || CONNECTION_ACTIVE_COLOR),\n strokeWidth: config.connectionLineStyle?.strokeWidth ?? Number(TEMP_LINE_STROKE_WIDTH),\n strokeDasharray: config.connectionLineStyle?.strokeDasharray ?? TEMP_LINE_DASH_PATTERN,\n };\n\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('class', 'flow-connect-line');\n svg.style.cssText = 'position:absolute;top:0;left:0;width:1px;height:1px;overflow:visible;pointer-events:none;z-index:1000;';\n\n let currentChild: SVGElement | null = null;\n\n function update(props: Omit): void {\n const fullProps: ConnectionLineProps = {\n ...props,\n connectionLineType: lineType,\n connectionLineStyle: style,\n };\n\n // Custom renderer\n if (config.connectionLine) {\n if (currentChild) {\n currentChild.remove();\n }\n currentChild = config.connectionLine(fullProps);\n svg.appendChild(currentChild);\n return;\n }\n\n // Preset path rendering\n if (!currentChild) {\n currentChild = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n currentChild.setAttribute('fill', 'none');\n svg.appendChild(currentChild);\n }\n\n currentChild.setAttribute('stroke', style.stroke);\n currentChild.setAttribute('stroke-width', String(style.strokeWidth));\n currentChild.setAttribute('stroke-dasharray', style.strokeDasharray);\n\n const { fromX, fromY, toX, toY } = props;\n let pathD: string;\n\n switch (lineType) {\n case 'bezier': {\n const result = getBezierPath({ sourceX: fromX, sourceY: fromY, targetX: toX, targetY: toY });\n pathD = result.path;\n break;\n }\n case 'smoothstep': {\n const result = getSmoothStepPath({ sourceX: fromX, sourceY: fromY, targetX: toX, targetY: toY });\n pathD = result.path;\n break;\n }\n case 'step': {\n const result = getStepPath({ sourceX: fromX, sourceY: fromY, targetX: toX, targetY: toY });\n pathD = result.path;\n break;\n }\n default: {\n const result = getStraightPath({ sourceX: fromX, sourceY: fromY, targetX: toX, targetY: toY });\n pathD = result.path;\n break;\n }\n }\n\n currentChild.setAttribute('d', pathD);\n }\n\n function destroy(): void {\n svg.remove();\n }\n\n return { svg, update, destroy };\n}\n\n/**\n * Find the closest connectable handle within a snap radius.\n *\n * Queries the container for all handles of the given type, skips handles on\n * the excluded node and non-connectable nodes, then returns the closest one\n * (if any) along with its flow-space position.\n *\n * When `index` is provided (connect-drag / reconnect gestures build one on\n * pointerdown), this reads the precomputed flow-space handle centers with\n * ZERO further DOM queries or measurements. Without an index (one-shot / other\n * callers) the legacy container-wide querySelectorAll + getBoundingClientRect\n * sweep runs unchanged. The two paths are behaviorally identical — see\n * src/plugin/connection-utils.test.ts's \"indexed parity\" battery.\n */\nexport function findSnapTarget(params: {\n containerEl: HTMLElement;\n handleType: 'source' | 'target';\n excludeNodeId: string;\n cursorFlowPos: XYPosition;\n connectionSnapRadius: number;\n getNode: (id: string) => { connectable?: boolean; locked?: boolean } | undefined;\n toFlowPosition: (screenX: number, screenY: number) => XYPosition;\n targetNodeId?: string;\n connectionMode?: 'strict' | 'loose';\n index?: HandleIndex;\n}): { element: HTMLElement | null; position: XYPosition } {\n if (params.connectionSnapRadius <= 0) {\n return { element: null, position: params.cursorFlowPos };\n }\n\n // Indexed path: flow-space handle centers are invariant under viewport pan\n // (auto-pan included) and nodes cannot move during a connect drag, so the\n // drag-start index needs no refresh — reusing it here does ZERO further DOM\n // reads. If a future feature moves nodes mid-connect-drag, the index must\n // be rebuilt (or this branch must re-measure) to stay correct.\n if (params.index) {\n const candidates = params.connectionMode === 'loose'\n ? params.index.all\n : params.index.byType(params.handleType);\n\n let closestElement: HTMLElement | null = null;\n let closestPos = params.cursorFlowPos;\n let minDist = params.connectionSnapRadius;\n\n for (const rec of candidates) {\n if (rec.nodeId === params.excludeNodeId) continue;\n if (params.targetNodeId && rec.nodeId !== params.targetNodeId) continue;\n\n const node = params.getNode(rec.nodeId);\n if (node && !isConnectable(node)) continue;\n\n // Per-handle connectable guard uses the DRAG's handleType (NOT the\n // candidate's type) — matches the legacy guard below.\n if (params.handleType === 'target' ? !rec.connectableEnd : !rec.connectableStart) continue;\n\n const dx = params.cursorFlowPos.x - rec.flowX;\n const dy = params.cursorFlowPos.y - rec.flowY;\n const dist = Math.sqrt(dx * dx + dy * dy);\n\n if (dist < minDist) {\n minDist = dist;\n closestElement = rec.el;\n closestPos = { x: rec.flowX, y: rec.flowY };\n }\n }\n\n return { element: closestElement, position: closestPos };\n }\n\n // In loose mode, snap to handles of any type (not just the opposite type)\n const selector = params.connectionMode === 'loose'\n ? '[data-flow-handle-type]'\n : `[data-flow-handle-type=\"${params.handleType}\"]`;\n const handles = params.containerEl.querySelectorAll(selector);\n\n let closestElement: HTMLElement | null = null;\n let closestPos = params.cursorFlowPos;\n let minDist = params.connectionSnapRadius;\n\n handles.forEach((th) => {\n const handleEl = th as HTMLElement;\n const nodeEl = handleEl.closest('[x-flow-node]') as HTMLElement | null;\n if (!nodeEl || nodeEl.dataset.flowNodeId === params.excludeNodeId) return;\n\n // Optional per-node filter for multi-connect\n if (params.targetNodeId && nodeEl.dataset.flowNodeId !== params.targetNodeId) return;\n\n const nodeId = nodeEl.dataset.flowNodeId;\n if (nodeId) {\n const node = params.getNode(nodeId);\n if (node && !isConnectable(node)) return;\n }\n\n // Per-handle connectable guard\n const connectableKey = params.handleType === 'target' ? HANDLE_CONNECTABLE_END_KEY : HANDLE_CONNECTABLE_START_KEY;\n if (handleEl[connectableKey] === false) return;\n\n const rect = handleEl.getBoundingClientRect();\n // Hidden handles (display:none) return zero-size rects — skip them\n if (rect.width === 0 && rect.height === 0) return;\n const hPos = params.toFlowPosition(\n rect.left + rect.width / 2,\n rect.top + rect.height / 2,\n );\n const dist = Math.sqrt(\n (params.cursorFlowPos.x - hPos.x) ** 2 + (params.cursorFlowPos.y - hPos.y) ** 2,\n );\n\n if (dist < minDist) {\n minDist = dist;\n closestElement = handleEl;\n closestPos = hPos;\n }\n });\n\n return { element: closestElement, position: closestPos };\n}\n\n/**\n * Start auto-pan behavior for a connection drag. Returns the AutoPanInstance\n * (or null if auto-pan is disabled via config). The instance should be stopped\n * when the drag ends.\n */\nexport function startConnectionAutoPan(\n containerEl: HTMLElement,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n canvas: Record,\n clientX: number,\n clientY: number,\n): AutoPanInstance | null {\n if (canvas._config?.autoPanOnConnect === false) return null;\n\n const instance = createAutoPan({\n container: containerEl,\n speed: canvas._config?.autoPanSpeed ?? 15,\n onPan(dx: number, dy: number) {\n // Reactive `viewport` is frame-coalesced; setViewport updates `_viewportLive`\n // synchronously, so measure the applied delta against that (reading reactive\n // `viewport` here would always yield 0 and kill the auto-pan loop).\n const liveVp = () => canvas._viewportLive ?? canvas.viewport;\n const vpBefore = { x: liveVp().x, y: liveVp().y };\n canvas._panZoom?.setViewport({\n x: liveVp().x - dx,\n y: liveVp().y - dy,\n zoom: liveVp().zoom,\n });\n const actualDx = vpBefore.x - liveVp().x;\n const actualDy = vpBefore.y - liveVp().y;\n return actualDx === 0 && actualDy === 0;\n },\n });\n instance.updatePointer(clientX, clientY);\n instance.start();\n return instance;\n}\n","// ============================================================================\n// Drag validation context\n//\n// Precomputes, ONCE per connect-drag gesture, everything the per-handle\n// validation chain needs so that `applyValidationClasses` can validate every\n// target handle in O(1) each — instead of running container-wide\n// `querySelector` sweeps (`isValidConnection` + `checkHandleLimits` +\n// `runHandleValidators`) per target on every pointer move.\n//\n// The context is derived purely from `canvas.edges` + the fixed drag source, so\n// it stays valid for the whole gesture (edges don't change mid-drag).\n// ============================================================================\n\nimport type { Connection, FlowEdge } from '../core/types';\n\n/**\n * Precomputed lookups for the drag-time validation chain, keyed so the\n * per-handle apply loop can answer each check with a single map/set read.\n */\nexport interface DragValidationContext {\n /**\n * `${target}|${targetHandle}` for every edge already emanating from the exact\n * drag source handle. Built with STRICT raw handle values (no `?? 'target'`\n * defaulting) to mirror `isValidConnection`'s strict `===` duplicate check.\n */\n existingTargets: Set;\n /** Node ids that would close a directed cycle (empty when preventCycles off). */\n cycleForbidden: Set;\n /** `${source}|${sourceHandle ?? 'source'}` → edge count (matches checkHandleLimits). */\n sourceCounts: Map;\n /** `${target}|${targetHandle ?? 'target'}` → edge count (matches checkHandleLimits). */\n targetCounts: Map;\n}\n\n/**\n * Build the {@link DragValidationContext} for a drag originating at\n * `sourceNodeId` / `sourceHandleId`. `excludeEdgeId` drops a single edge from\n * consideration (used by reconnect, which must not treat the edge being\n * reconnected as a pre-existing duplicate or limit contributor).\n */\nexport function buildDragValidationContext(\n canvas: { edges: FlowEdge[]; _config?: { preventCycles?: boolean } },\n sourceNodeId: string,\n sourceHandleId: string,\n excludeEdgeId?: string,\n): DragValidationContext {\n const edges = excludeEdgeId\n ? (canvas.edges as FlowEdge[]).filter((e) => e.id !== excludeEdgeId)\n : (canvas.edges as FlowEdge[]);\n\n const existingTargets = new Set();\n const sourceCounts = new Map();\n const targetCounts = new Map();\n\n for (const e of edges) {\n const sKey = `${e.source}|${e.sourceHandle ?? 'source'}`;\n const tKey = `${e.target}|${e.targetHandle ?? 'target'}`;\n sourceCounts.set(sKey, (sourceCounts.get(sKey) ?? 0) + 1);\n targetCounts.set(tKey, (targetCounts.get(tKey) ?? 0) + 1);\n\n // Duplicate detection must mirror isValidConnection's STRICT `===` on raw\n // stored handles — NO `?? 'source'`/`?? 'target'` defaulting. An edge with\n // `sourceHandle: undefined` is NOT a duplicate of a drag from the literal\n // 'source' handle, and `${e.target}|undefined` can never match a concrete\n // target-handle id in the apply loop. (Counts above intentionally DO\n // normalize, because checkHandleLimits normalizes.)\n if (e.source === sourceNodeId && e.sourceHandle === sourceHandleId) {\n existingTargets.add(`${e.target}|${e.targetHandle}`);\n }\n }\n\n const cycleForbidden = new Set();\n if (canvas._config?.preventCycles) {\n // Walk INCOMING edges from the source: the set of nodes that can reach the\n // source is exactly the set whose target would close a cycle\n // (wouldCreateCycle(source, t) === \"t can reach source\").\n const incoming = new Map();\n for (const e of edges) {\n let list = incoming.get(e.target);\n if (!list) {\n list = [];\n incoming.set(e.target, list);\n }\n list.push(e.source);\n }\n\n const stack = [sourceNodeId];\n while (stack.length > 0) {\n const id = stack.pop()!;\n if (cycleForbidden.has(id)) {\n continue;\n }\n cycleForbidden.add(id);\n for (const parent of incoming.get(id) ?? []) {\n stack.push(parent);\n }\n }\n }\n\n return { existingTargets, cycleForbidden, sourceCounts, targetCounts };\n}\n\n/** Re-exported for callers threading connections through the context. */\nexport type { Connection };\n","// ============================================================================\n// HandleIndex\n//\n// One measured DOM pass over connection handles per drag gesture. Building\n// this index reads `getBoundingClientRect()` for every handle exactly once\n// and converts each handle's screen-space center to a flow-space position.\n// Because nodes don't move and panning doesn't change flow-space positions\n// during a connect-drag, the captured flow-space centers stay valid for the\n// entire gesture — callers build the index once on pointerdown and reuse it\n// on every subsequent pointermove instead of re-querying and re-measuring\n// the DOM per event.\n// ============================================================================\n\nimport type { XYPosition } from '../core/types';\nimport { HANDLE_LIMIT_KEY } from './directives/flow-handle-limit';\nimport { HANDLE_VALIDATE_KEY } from './directives/flow-handle-validate';\nimport {\n HANDLE_CONNECTABLE_START_KEY,\n HANDLE_CONNECTABLE_END_KEY,\n} from './directives/flow-handle-connectable';\n\n/** A single measured connection handle, captured once per drag gesture. */\nexport interface HandleRecord {\n el: HTMLElement;\n nodeId: string;\n handleId: string; // dataset.flowHandleId ?? 'source'/'target'\n type: 'source' | 'target';\n isMirror: boolean; // classList contains 'flow-schema-handle--mirror'\n flowX: number; // flow-space center\n flowY: number;\n connectableStart: boolean; // HANDLE_CONNECTABLE_START_KEY !== false at build time\n connectableEnd: boolean;\n hasValidator: boolean; // HANDLE_VALIDATE_KEY present\n limit: number | null; // HANDLE_LIMIT_KEY value\n}\n\n/** Indexed view over the handles measured by `buildHandleIndex()`. */\nexport interface HandleIndex {\n all: HandleRecord[];\n byType(type: 'source' | 'target'): HandleRecord[];\n /** Real handle preferred over mirror. */\n get(nodeId: string, handleId: string, type: 'source' | 'target'): HandleRecord | undefined;\n}\n\n/**\n * Build a `HandleIndex` by making one measured pass over every connection\n * handle inside `containerEl`. Captures each handle's flow-space center\n * ONCE per drag — nodes don't move and panning doesn't change flow-space\n * positions, so the captured centers stay valid for the whole gesture.\n * Consumers (A2 validation, A3 snap-targeting) should build this once on\n * pointerdown and reuse it across every pointermove, rather than re-querying\n * and re-measuring the DOM per event.\n *\n * This is a read-only pass: it never writes to the DOM, so it never\n * triggers layout thrash.\n */\nexport function buildHandleIndex(\n containerEl: HTMLElement,\n toFlowPosition: (screenX: number, screenY: number) => XYPosition,\n): HandleIndex {\n const all: HandleRecord[] = [];\n const byKey = new Map();\n const nodeIdCache = new Map();\n\n const els = containerEl.querySelectorAll('[data-flow-handle-type]');\n for (const el of els) {\n // Resolve owning node once per node element, not per handle.\n const nodeEl = el.closest('[data-flow-node-id]') as HTMLElement | null;\n if (!nodeEl) continue;\n let nodeId = nodeIdCache.get(nodeEl);\n if (nodeId === undefined) {\n nodeId = nodeEl.dataset.flowNodeId ?? null;\n nodeIdCache.set(nodeEl, nodeId);\n }\n if (!nodeId) continue;\n\n const rect = el.getBoundingClientRect(); // READ phase only — no writes in this loop\n if (rect.width === 0 && rect.height === 0) continue; // display:none handles\n\n const type = el.dataset.flowHandleType as 'source' | 'target';\n const center = toFlowPosition(rect.left + rect.width / 2, rect.top + rect.height / 2);\n const record: HandleRecord = {\n el,\n nodeId,\n handleId: el.dataset.flowHandleId ?? type,\n type,\n isMirror: el.classList.contains('flow-schema-handle--mirror'),\n flowX: center.x,\n flowY: center.y,\n connectableStart: el[HANDLE_CONNECTABLE_START_KEY] !== false,\n connectableEnd: el[HANDLE_CONNECTABLE_END_KEY] !== false,\n hasValidator: el[HANDLE_VALIDATE_KEY] != null,\n limit: el[HANDLE_LIMIT_KEY] ?? null,\n };\n all.push(record);\n const key = `${nodeId}|${record.handleId}|${type}`;\n const existing = byKey.get(key);\n if (!existing || (existing.isMirror && !record.isMirror)) byKey.set(key, record);\n }\n\n const sources = all.filter((r) => r.type === 'source');\n const targets = all.filter((r) => r.type === 'target');\n return {\n all,\n byType: (t) => (t === 'source' ? sources : targets),\n get: (nodeId, handleId, type) => byKey.get(`${nodeId}|${handleId}|${type}`),\n };\n}\n","// ============================================================================\n// x-flow-handle Directive\n//\n// Marks an element as a connection handle (source or target).\n// Source handles support drag-to-connect: drag from a source to a target\n// handle to create a new edge. Respects per-node `connectable` flag.\n//\n// Usage:\n//
— source handle (bottom, default)\n//
— target handle (top, default)\n//
— source on the right side\n//
— target at top-left, id=\"tl\"\n//
\n// — dynamic position via object expression\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type { HandleType, HandlePosition, FlowEdge, FlowNode, Connection, XYPosition, PendingKeyboardConnect } from '../../core/types';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from '../../core/geometry';\nimport { isValidConnection, checkConnectionRules } from '../../core/connections';\nimport { debug } from '../../core/debug';\nimport { HANDLE_VALIDATE_KEY } from './flow-handle-validate';\nimport { HANDLE_LIMIT_KEY } from './flow-handle-limit';\nimport { HANDLE_CONNECTABLE_START_KEY, HANDLE_CONNECTABLE_END_KEY } from './flow-handle-connectable';\nimport { DRAG_THRESHOLD, CONNECTION_ACTIVE_COLOR, CONNECTION_INVALID_COLOR } from '../../core/constants';\nimport { createConnectionLine, findSnapTarget, startConnectionAutoPan, type ConnectionLineInstance } from '../connection-utils';\nimport { isConnectable } from '../../core/node-flags';\nimport { buildDragValidationContext } from '../drag-validation';\nimport { buildHandleIndex, type HandleIndex } from '../handle-index';\n\nlet edgeIdCounter = 0;\n\n/**\n * Per-container Escape listener registry for keyboard drag-to-connect.\n *\n * A canvas may contain N handles (N nodes × 2), and each handle's init used\n * to attach its own keydown listener to the shared `.flow-container`. On a\n * 10-node canvas that's 20 duplicate listeners — plus accumulation each time\n * a handle re-inits (x-if toggles, Livewire morphing, etc.).\n *\n * Instead we share ONE keydown listener per container, refcounted by the\n * number of keyboard-enabled handles currently bound. The first handle to\n * init attaches; subsequent handles bump the count; the last to clean up\n * removes the listener. Kept module-local so it lives alongside the\n * directive it serves and does not leak into the public canvas surface.\n */\ntype ContainerEscEntry = { count: number; handler: (e: KeyboardEvent) => void };\nconst containerEscListeners = new WeakMap();\n\n/**\n * Run per-handle validators for a connection.\n * Looks up source and target handle elements by node ID and handle ID,\n * reads the expando validator, and calls it. Returns false if either rejects.\n */\nexport function runHandleValidators(\n containerEl: HTMLElement,\n connection: Connection,\n): boolean {\n // Source handle validator\n const sourceNodeEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(connection.source)}\"]`,\n );\n if (sourceNodeEl) {\n const sh = connection.sourceHandle ?? 'source';\n const sourceHandleEl = (\n sourceNodeEl.querySelector(\n `[data-flow-handle-id=\"${CSS.escape(sh)}\"][data-flow-handle-type=\"source\"]`,\n ) ?? sourceNodeEl.querySelector(`[data-flow-handle-id=\"${CSS.escape(sh)}\"]`)\n ) as HTMLElement | null;\n if (sourceHandleEl?.[HANDLE_VALIDATE_KEY]) {\n if (!sourceHandleEl[HANDLE_VALIDATE_KEY]!(connection)) return false;\n }\n }\n\n // Target handle validator\n const targetNodeEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(connection.target)}\"]`,\n );\n if (targetNodeEl) {\n const th = connection.targetHandle ?? 'target';\n const targetHandleEl = (\n targetNodeEl.querySelector(\n `[data-flow-handle-id=\"${CSS.escape(th)}\"][data-flow-handle-type=\"target\"]`,\n ) ?? targetNodeEl.querySelector(`[data-flow-handle-id=\"${CSS.escape(th)}\"]`)\n ) as HTMLElement | null;\n if (targetHandleEl?.[HANDLE_VALIDATE_KEY]) {\n if (!targetHandleEl[HANDLE_VALIDATE_KEY]!(connection)) return false;\n }\n }\n\n return true;\n}\n\n/**\n * Check connection limits on source and target handles.\n * Counts existing edges matching each handle and rejects if at/over the limit.\n */\nexport function checkHandleLimits(\n containerEl: HTMLElement,\n connection: Connection,\n edges: { source: string; target: string; sourceHandle?: string; targetHandle?: string }[],\n): boolean {\n // Source handle limit\n const sourceNodeEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(connection.source)}\"]`,\n );\n if (sourceNodeEl) {\n const sh = connection.sourceHandle ?? 'source';\n const sourceHandleEl = (\n sourceNodeEl.querySelector(\n `[data-flow-handle-id=\"${CSS.escape(sh)}\"][data-flow-handle-type=\"source\"]`,\n ) ?? sourceNodeEl.querySelector(`[data-flow-handle-id=\"${CSS.escape(sh)}\"]`)\n ) as HTMLElement | null;\n if (sourceHandleEl?.[HANDLE_LIMIT_KEY]) {\n const count = edges.filter(\n e => e.source === connection.source && (e.sourceHandle ?? 'source') === (connection.sourceHandle ?? 'source'),\n ).length;\n if (count >= sourceHandleEl[HANDLE_LIMIT_KEY]!) return false;\n }\n }\n\n // Target handle limit\n const targetNodeEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(connection.target)}\"]`,\n );\n if (targetNodeEl) {\n const th = connection.targetHandle ?? 'target';\n const targetHandleEl = (\n targetNodeEl.querySelector(\n `[data-flow-handle-id=\"${CSS.escape(th)}\"][data-flow-handle-type=\"target\"]`,\n ) ?? targetNodeEl.querySelector(`[data-flow-handle-id=\"${CSS.escape(th)}\"]`)\n ) as HTMLElement | null;\n if (targetHandleEl?.[HANDLE_LIMIT_KEY]) {\n const count = edges.filter(\n e => e.target === connection.target && (e.targetHandle ?? 'target') === (connection.targetHandle ?? 'target'),\n ).length;\n if (count >= targetHandleEl[HANDLE_LIMIT_KEY]!) return false;\n }\n }\n\n return true;\n}\n\n/**\n * Apply .flow-handle-valid / .flow-handle-invalid classes to all target handles\n * in the container based on the validation chain for a hypothetical connection\n * from the given source.\n *\n * When `index` is provided (connect-drag / reconnect gestures build one on\n * pointerdown), validation runs O(1) per handle off a precomputed context with\n * ZERO further DOM queries or measurements. Without an index (one-shot callers:\n * click-to-connect, easy-connect, edge-body reconnect) the legacy container-wide\n * querySelector sweep runs unchanged. The two paths are behaviorally identical\n * — see src/plugin/drag-validation.test.ts's characterization battery.\n */\nexport function applyValidationClasses(\n containerEl: HTMLElement,\n sourceNodeId: string,\n sourceHandleId: string,\n canvas: any,\n excludeEdgeId?: string,\n index?: HandleIndex,\n): void {\n if (!index) {\n legacyApplyValidationClasses(containerEl, sourceNodeId, sourceHandleId, canvas, excludeEdgeId);\n return;\n }\n\n const vctx = buildDragValidationContext(canvas, sourceNodeId, sourceHandleId, excludeEdgeId);\n\n // Hoist the SOURCE-side limit once: legacy checkHandleLimits checks the source\n // handle first, so a source already at its limit rejects EVERY target.\n const srcRec = index.get(sourceNodeId, sourceHandleId, 'source');\n const sourceLimitHit =\n srcRec?.limit != null &&\n (vctx.sourceCounts.get(`${sourceNodeId}|${sourceHandleId}`) ?? 0) >= srcRec.limit;\n\n // READ every record first, WRITE all classes after — no interleaved DOM\n // read/write so the browser never re-lays-out mid-loop.\n const results: Array<{ el: HTMLElement; valid: boolean; limitHit: boolean }> = [];\n\n for (const rec of index.byType('target')) {\n // Per-handle connectable guard reads the SPECIFIC element (mirror vs real),\n // matching legacy's `targetEl[HANDLE_CONNECTABLE_END_KEY] === false`.\n if (!rec.connectableEnd) {\n results.push({ el: rec.el, valid: false, limitHit: false });\n continue;\n }\n\n const connection: Connection = {\n source: sourceNodeId,\n sourceHandle: sourceHandleId,\n target: rec.nodeId,\n targetHandle: rec.handleId,\n };\n\n const targetNode = canvas.getNode(rec.nodeId);\n const builtInValid =\n targetNode?.connectable !== false &&\n rec.nodeId !== sourceNodeId &&\n !vctx.existingTargets.has(`${rec.nodeId}|${rec.handleId}`) &&\n !vctx.cycleForbidden.has(rec.nodeId);\n\n // Legacy checkHandleLimits / runHandleValidators re-resolve the target\n // handle by (nodeId, handleId) via querySelector, which returns the REAL\n // handle before its mirror. `index.get` applies the same real-preference, so\n // a mirror record inherits the real handle's limit + validator (its own are\n // permissive defaults). The connectable guard above intentionally uses the\n // specific element; only the limit/validator lookups are authoritative.\n const authoritative = index.get(rec.nodeId, rec.handleId, 'target') ?? rec;\n\n let limitValid = builtInValid && !sourceLimitHit;\n if (limitValid && authoritative.limit != null) {\n limitValid = (vctx.targetCounts.get(`${rec.nodeId}|${rec.handleId}`) ?? 0) < authoritative.limit;\n }\n\n let handleValid = limitValid;\n // Source validator: matches legacy runHandleValidators (rejects on any falsy\n // return) and is evaluated per-target since the connection carries `target`.\n if (handleValid && srcRec?.hasValidator) {\n handleValid = !!srcRec.el[HANDLE_VALIDATE_KEY]!(connection);\n }\n if (handleValid && authoritative.hasValidator) {\n handleValid = !!authoritative.el[HANDLE_VALIDATE_KEY]!(connection);\n }\n\n const globalValid =\n handleValid &&\n (!canvas._config?.isValidConnection || canvas._config.isValidConnection(connection));\n\n results.push({ el: rec.el, valid: globalValid, limitHit: builtInValid && !limitValid });\n }\n\n for (const r of results) {\n // classList.toggle(cls, cond) reproduces the legacy add/remove pairs exactly.\n r.el.classList.toggle('flow-handle-valid', r.valid);\n r.el.classList.toggle('flow-handle-invalid', !r.valid);\n r.el.classList.toggle('flow-handle-limit-reached', r.limitHit);\n }\n}\n\n/**\n * Legacy container-wide validation sweep — the behavioral ORACLE. Runs one\n * querySelectorAll over target handles and, per target, a full isValidConnection\n * + checkHandleLimits + runHandleValidators chain (each doing its own\n * querySelectors). Retained verbatim as the fallback for one-shot callers that\n * don't build a HandleIndex.\n */\nexport function legacyApplyValidationClasses(\n containerEl: HTMLElement,\n sourceNodeId: string,\n sourceHandleId: string,\n canvas: any,\n excludeEdgeId?: string,\n): void {\n const edges = excludeEdgeId\n ? (canvas.edges as FlowEdge[]).filter(e => e.id !== excludeEdgeId)\n : canvas.edges;\n const targetHandles = containerEl.querySelectorAll('[data-flow-handle-type=\"target\"]');\n for (const targetEl of targetHandles) {\n const targetNodeEl = targetEl.closest('[x-flow-node]') as HTMLElement | null;\n const targetNodeId = targetNodeEl?.dataset.flowNodeId;\n if (!targetNodeId) continue;\n\n const targetHandleId = (targetEl as HTMLElement).dataset.flowHandleId ?? 'target';\n\n // Per-handle connectable guard\n if ((targetEl as HTMLElement)[HANDLE_CONNECTABLE_END_KEY] === false) {\n (targetEl as HTMLElement).classList.add('flow-handle-invalid');\n (targetEl as HTMLElement).classList.remove('flow-handle-valid', 'flow-handle-limit-reached');\n continue;\n }\n\n const connection = {\n source: sourceNodeId,\n sourceHandle: sourceHandleId,\n target: targetNodeId,\n targetHandle: targetHandleId,\n };\n\n // Run full validation chain\n const targetNode = canvas.getNode(targetNodeId);\n const builtInValid = targetNode?.connectable !== false\n && isValidConnection(connection, edges, { preventCycles: canvas._config?.preventCycles });\n const limitValid = builtInValid && checkHandleLimits(containerEl, connection, edges);\n const handleValid = limitValid && runHandleValidators(containerEl, connection);\n const globalValid = handleValid\n && (!canvas._config?.isValidConnection || canvas._config.isValidConnection(connection));\n\n if (globalValid) {\n (targetEl as HTMLElement).classList.add('flow-handle-valid');\n (targetEl as HTMLElement).classList.remove('flow-handle-invalid', 'flow-handle-limit-reached');\n } else {\n (targetEl as HTMLElement).classList.add('flow-handle-invalid');\n (targetEl as HTMLElement).classList.remove('flow-handle-valid');\n // Add limit-reached modifier when the rejection is due to the connection limit\n if (builtInValid && !limitValid) {\n (targetEl as HTMLElement).classList.add('flow-handle-limit-reached');\n } else {\n (targetEl as HTMLElement).classList.remove('flow-handle-limit-reached');\n }\n }\n }\n}\n\n/**\n * Remove .flow-handle-valid / .flow-handle-invalid from all target handles.\n */\nexport function clearValidationClasses(containerEl: HTMLElement): void {\n const targetHandles = containerEl.querySelectorAll('[data-flow-handle-type=\"target\"]');\n for (const targetEl of targetHandles) {\n (targetEl as HTMLElement).classList.remove('flow-handle-valid', 'flow-handle-invalid', 'flow-handle-limit-reached');\n }\n}\n\n/**\n * Toggle `.flow-connect-line--validating` on the temporary drag-line SVG while\n * an async `connectValidator` is awaiting. Call sites wrap the validator await\n * with `setDragLineValidating(svg, true)` … `finally { setDragLineValidating(svg, false) }`\n * so the pending affordance is guaranteed to clear even if the validator throws.\n *\n * Safe to call with `null`/`undefined` — the drag line may not exist on all\n * paths (e.g. click-to-connect) and the helper is a no-op in that case.\n */\nexport function setDragLineValidating(\n el: Element | null | undefined,\n on: boolean,\n): void {\n if (!el) return;\n if (on) {\n el.classList.add('flow-connect-line--validating');\n } else {\n el.classList.remove('flow-connect-line--validating');\n }\n}\n\n/**\n * Single chokepoint for every \"connection rejected\" path (drag-to-connect,\n * click-to-connect, handle-pip reconnect, edge-body reconnect). Guarantees:\n *\n * 1. A consistent `flow-connect-rejected` CustomEvent on `containerEl` with\n * detail `{reason, source, target, sourceHandle, targetHandle}`. `reason`\n * is always present as a key, even when undefined (sync rejections don't\n * carry a reason).\n * 2. A discoverable `console.warn('[alpineflow] connection rejected: ...')`\n * so devs see the rejection in the console without wiring any listener.\n *\n * Extracted so future rejection paths can't diverge on detail shape or forget\n * to warn. Safe to call with a nullish container (no-op) — the drag-line\n * lifecycle sometimes tears down the container before rejection dispatches.\n */\nexport function dispatchConnectRejected(\n containerEl: Element | null | undefined,\n detail: {\n source: string;\n target: string;\n sourceHandle?: string;\n targetHandle?: string;\n reason?: string;\n },\n): void {\n // Keep `reason` in the detail shape even when undefined so consumers can\n // destructure without existence checks.\n const eventDetail = {\n source: detail.source,\n target: detail.target,\n sourceHandle: detail.sourceHandle,\n targetHandle: detail.targetHandle,\n reason: detail.reason,\n };\n\n if (!containerEl) return;\n\n if (detail.reason !== undefined) {\n console.warn('[alpineflow] connection rejected:', detail.reason);\n } else {\n console.warn('[alpineflow] connection rejected');\n }\n\n containerEl.dispatchEvent(new CustomEvent('flow-connect-rejected', {\n detail: eventDetail,\n bubbles: true,\n }));\n}\n\n/**\n * Run the optional async `connectValidator` gate.\n *\n * Returns { allowed: true } when no validator is configured. Otherwise:\n * - Adds `validatingClass` to the source + target handle elements.\n * - Dispatches a `flow-connect-validating` CustomEvent on `containerEl`.\n * - Awaits the validator (thrown errors become `{ allowed: false }`).\n * - Removes the class and dispatches `flow-connect-validated` with detail\n * `{ connection, allowed, reason }`.\n *\n * Extracted so it can be unit-tested without simulating pointer drags.\n */\nexport async function runConnectValidator(\n validator: ((conn: Connection) => Promise) | undefined,\n connection: Connection,\n sourceEl: Element | null,\n targetEl: Element | null,\n containerEl: Element,\n validatingClass: string,\n): Promise<{ allowed: boolean; reason?: string }> {\n if (!validator) return { allowed: true };\n\n sourceEl?.classList.add(validatingClass);\n targetEl?.classList.add(validatingClass);\n containerEl.dispatchEvent(new CustomEvent('flow-connect-validating', {\n detail: { connection },\n bubbles: true,\n }));\n\n let result: boolean | { allowed: boolean; reason?: string };\n try {\n result = await validator(connection);\n } catch (err) {\n debug('connection', 'connectValidator threw', err);\n result = false;\n } finally {\n sourceEl?.classList.remove(validatingClass);\n targetEl?.classList.remove(validatingClass);\n }\n\n const allowed = typeof result === 'boolean' ? result : !!result?.allowed;\n const reason = typeof result === 'object' && result && 'reason' in result ? result.reason : undefined;\n containerEl.dispatchEvent(new CustomEvent('flow-connect-validated', {\n detail: { connection, allowed, reason },\n bubbles: true,\n }));\n\n return { allowed, reason };\n}\n\n/**\n * Validate + apply an edge-endpoint reconnect.\n *\n * Runs the same validator chain as drag-to-connect (sync `isValidConnection`,\n * `connectionRules`, handle limits, per-handle validators, global\n * `isValidConnection`, then the async `connectValidator`). On success mutates\n * the edge in place (`edge.target`, `edge.targetHandle` — or the source pair\n * when `endpoint === 'source'`) and captures history. On rejection leaves the\n * edge unchanged and dispatches a `flow-connect-rejected` CustomEvent on\n * `containerEl` with the offending `{source, target, sourceHandle, targetHandle,\n * reason}` so consumers can show their own UI.\n *\n * Extracted so it can be unit-tested without simulating pointer drags.\n */\nexport async function applyReconnectValidation(params: {\n edge: FlowEdge;\n newConnection: Connection;\n canvas: any;\n containerEl: HTMLElement;\n endpoint?: HandleType;\n}): Promise<{ applied: boolean; reason?: string }> {\n const { edge, newConnection, canvas, containerEl } = params;\n const endpoint: HandleType = params.endpoint ?? 'target';\n\n const otherEdges = (canvas.edges as FlowEdge[]).filter(\n (e: FlowEdge) => e.id !== edge.id,\n );\n\n const reject = (reason?: string): { applied: false; reason?: string } => {\n dispatchConnectRejected(containerEl, {\n source: newConnection.source,\n target: newConnection.target,\n sourceHandle: newConnection.sourceHandle,\n targetHandle: newConnection.targetHandle,\n reason,\n });\n return { applied: false, reason };\n };\n\n // ── Sync chain ─────────────────────────────────────────────────────────\n if (!isValidConnection(newConnection, otherEdges, { preventCycles: canvas._config?.preventCycles })) {\n return reject();\n }\n if (!checkConnectionRules(newConnection, canvas._config?.connectionRules, canvas._nodeMap)) {\n return reject();\n }\n if (!checkHandleLimits(containerEl, newConnection, otherEdges)) {\n return reject();\n }\n if (!runHandleValidators(containerEl, newConnection)) {\n return reject();\n }\n if (canvas._config?.isValidConnection && !canvas._config.isValidConnection(newConnection)) {\n return reject();\n }\n\n // ── Async validator gate ───────────────────────────────────────────────\n const asyncValidator = canvas._config?.connectValidator;\n if (asyncValidator) {\n const validatingClass = canvas._config?.validatingHandleClass ?? 'flow-handle-validating';\n const { sourceEl, targetEl } = findHandleElements(containerEl, newConnection);\n canvas._connectValidating = true;\n let asyncResult: { allowed: boolean; reason?: string };\n try {\n asyncResult = await runConnectValidator(\n asyncValidator, newConnection, sourceEl, targetEl, containerEl, validatingClass,\n );\n } finally {\n canvas._connectValidating = false;\n }\n if (!asyncResult.allowed) {\n return reject(asyncResult.reason);\n }\n }\n\n // ── Apply — mutate the edge in place so Alpine reactivity picks up ────\n canvas._captureHistory?.();\n if (endpoint === 'source') {\n edge.source = newConnection.source;\n edge.sourceHandle = newConnection.sourceHandle;\n } else {\n edge.target = newConnection.target;\n edge.targetHandle = newConnection.targetHandle;\n }\n\n return { applied: true };\n}\n\n/**\n * Validate + apply a new connection (drag-to-connect / keyboard-connect).\n *\n * Runs the same validator chain used by pointer drag-to-connect (sync\n * `isValidConnection`, `connectionRules`, handle limits, per-handle validators,\n * global `isValidConnection`, then the async `connectValidator`). On success,\n * creates the edge via `canvas.addEdges`, emits `connect`, and returns\n * `{ applied: true, edge }`. On rejection, dispatches `flow-connect-rejected`\n * on `containerEl` (single chokepoint) and returns `{ applied: false, reason }`.\n *\n * Used by the keyboard drag-to-connect path; the pointer drag-to-connect\n * handler keeps its inline chain for now (drag lifecycle + ghost-node + multi-\n * connect logic is tightly coupled to the pointer event loop). Both paths end\n * up firing the same events and dispatching the same rejection shape so\n * consumers see a consistent API.\n */\nexport async function applyConnectValidation(params: {\n connection: Connection;\n canvas: any;\n containerEl: HTMLElement;\n}): Promise<{ applied: boolean; reason?: string; edge?: FlowEdge }> {\n const { connection, canvas, containerEl } = params;\n const edges = canvas.edges as FlowEdge[];\n\n const reject = (reason?: string): { applied: false; reason?: string } => {\n dispatchConnectRejected(containerEl, {\n source: connection.source,\n target: connection.target,\n sourceHandle: connection.sourceHandle,\n targetHandle: connection.targetHandle,\n reason,\n });\n return { applied: false, reason };\n };\n\n // Target connectability guard\n const targetNode = canvas.getNode?.(connection.target);\n if (targetNode && !isConnectable(targetNode)) {\n return reject();\n }\n\n // ── Sync chain ─────────────────────────────────────────────────────────\n if (!isValidConnection(connection, edges, { preventCycles: canvas._config?.preventCycles })) {\n return reject();\n }\n if (!checkConnectionRules(connection, canvas._config?.connectionRules, canvas._nodeMap)) {\n return reject();\n }\n if (!checkHandleLimits(containerEl, connection, edges)) {\n return reject();\n }\n if (!runHandleValidators(containerEl, connection)) {\n return reject();\n }\n if (canvas._config?.isValidConnection && !canvas._config.isValidConnection(connection)) {\n return reject();\n }\n\n // ── Async validator gate ───────────────────────────────────────────────\n const asyncValidator = canvas._config?.connectValidator;\n if (asyncValidator) {\n const validatingClass = canvas._config?.validatingHandleClass ?? 'flow-handle-validating';\n const { sourceEl, targetEl } = findHandleElements(containerEl, connection);\n canvas._connectValidating = true;\n let asyncResult: { allowed: boolean; reason?: string };\n try {\n asyncResult = await runConnectValidator(\n asyncValidator, connection, sourceEl, targetEl, containerEl, validatingClass,\n );\n } finally {\n canvas._connectValidating = false;\n }\n if (!asyncResult.allowed) {\n return reject(asyncResult.reason);\n }\n }\n\n // ── Apply — create the edge ───────────────────────────────────────────\n const edgeId = `e-${connection.source}-${connection.target}-${Date.now()}-${edgeIdCounter++}`;\n const edge = { id: edgeId, ...connection } as FlowEdge;\n canvas.addEdges(edge);\n canvas._emit?.('connect', { connection });\n\n return { applied: true, edge };\n}\n\n/**\n * Resolve the source + target handle DOM elements for a connection within a\n * container. Returns null for any handle it cannot locate. Used by the\n * drag-to-connect + reconnect paths so the async validator can pulse both\n * sides of the pending edge.\n */\nfunction findHandleElements(\n containerEl: HTMLElement,\n connection: Connection,\n): { sourceEl: HTMLElement | null; targetEl: HTMLElement | null } {\n const sourceNodeEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(connection.source)}\"]`,\n );\n const sh = connection.sourceHandle ?? 'source';\n const sourceEl = (\n sourceNodeEl?.querySelector(\n `[data-flow-handle-id=\"${CSS.escape(sh)}\"][data-flow-handle-type=\"source\"]`,\n ) ?? sourceNodeEl?.querySelector(`[data-flow-handle-id=\"${CSS.escape(sh)}\"]`)\n ?? null\n ) as HTMLElement | null;\n\n const targetNodeEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(connection.target)}\"]`,\n );\n const th = connection.targetHandle ?? 'target';\n const targetEl = (\n targetNodeEl?.querySelector(\n `[data-flow-handle-id=\"${CSS.escape(th)}\"][data-flow-handle-type=\"target\"]`,\n ) ?? targetNodeEl?.querySelector(`[data-flow-handle-id=\"${CSS.escape(th)}\"]`)\n ?? null\n ) as HTMLElement | null;\n\n return { sourceEl, targetEl };\n}\n\n/** One connection line inside a multi-connect gesture. */\ntype MultiConnectLine = {\n line: ConnectionLineInstance;\n sourceNodeId: string;\n sourceHandleId: string;\n sourcePos: XYPosition;\n valid: boolean;\n};\n\n/**\n * Teardown for the gesture a handle currently has in flight, keyed by the\n * handle element that started it.\n *\n * Before delegation these were two closure slots inside the directive\n * (`activeConnectionCleanup` / `activeReconnectCleanup`): written by the\n * pointerdown handler when a gesture starts, nulled when it ends, and read by\n * the directive's `cleanup()` so a handle torn down mid-drag (x-if toggle,\n * Livewire morph, schema row re-stamp) aborts its own in-flight gesture instead\n * of leaking document listeners and a drag SVG. The handler bodies now live at\n * module scope, so the slot moves onto a WeakMap keyed by the handle element —\n * preserving the per-handle teardown granularity exactly. Entries are deleted on\n * every end/cancel path, mirroring today's `= null`.\n *\n * A handle is either a source or a target, never both, so one map serves both\n * gesture kinds without collision.\n */\nconst activeHandleGestureCleanups = new WeakMap void>();\n\n/**\n * Start the SOURCE-handle pointer interaction: drag-to-connect, click-to-connect,\n * multi-connect and edge-drop all begin here.\n *\n * Extracted verbatim from the per-handle `pointerdown` listener so the delegated\n * listener (one per canvas — see `../handle-delegation.ts`) can drive any handle\n * without the directive attaching 5,000 listeners. Everything it needs is\n * recoverable from `handleEl`: the handle id from `dataset.flowHandleId`, the\n * node from `closest('[x-flow-node]')`, the connectable flag from the expando.\n *\n * Propagation semantics are load-bearing and preserved exactly: `preventDefault()`\n * + `stopPropagation()` fire UNCONDITIONALLY as the first two statements, before\n * any guard. `preventDefault()` is what suppresses the compatibility mouse events\n * that d3-drag binds on the node, i.e. it is the only thing stopping a handle\n * press from also dragging the node; `stopPropagation()` is what stops a schema\n * row from starting a reorder. Do not move them behind a guard.\n */\nexport function startSourceHandlePointerInteraction(\n handleEl: HTMLElement,\n canvas: any,\n e: PointerEvent,\n): void {\n e.preventDefault();\n e.stopPropagation();\n\n const handleId = handleEl.dataset.flowHandleId ?? 'source';\n const nodeEl = handleEl.closest('[x-flow-node]') as HTMLElement | null;\n if (!canvas || !nodeEl) return;\n if (canvas._animationLocked) return;\n\n const sourceNodeId = nodeEl.dataset.flowNodeId;\n if (!sourceNodeId) return;\n\n // ── Connectable guard (source) ─────────────────────────\n const sourceNode = canvas.getNode(sourceNodeId);\n if (sourceNode && !isConnectable(sourceNode)) return;\n if (handleEl[HANDLE_CONNECTABLE_START_KEY] === false) return;\n\n const startX = e.clientX;\n const startY = e.clientY;\n let dragStarted = false;\n\n // If we already have a pending click-to-connect, cancel it first\n if (canvas.pendingConnection && (canvas._config?.connectOnClick !== false)) {\n canvas._emit('connect-end', {\n connection: null,\n source: canvas.pendingConnection.source,\n sourceHandle: canvas.pendingConnection.sourceHandle,\n position: { x: 0, y: 0 },\n });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n const prevContainer = handleEl.closest('.flow-container') as HTMLElement;\n if (prevContainer) clearValidationClasses(prevContainer);\n }\n\n // Drag setup variables (deferred until threshold)\n let tempSvg: SVGSVGElement | null = null;\n let connectionLineInstance: ConnectionLineInstance | null = null;\n let snappedHandle: HTMLElement | null = null;\n let connectAutoPan: ReturnType = null;\n let ghostEl: HTMLElement | null = null;\n const connectionSnapRadius = canvas._config?.connectionSnapRadius ?? 20;\n const containerEl = handleEl.closest('.flow-container') as HTMLElement;\n\n // Handle index for the whole gesture: built once on drag-start (see\n // initDrag), read O(1) per handle by applyValidationClasses on every\n // pointermove and, in a follow-up, by findSnapTarget. Nulled on every\n // end/cancel path so a stale index can't leak into the next gesture.\n let dragHandleIndex: HandleIndex | null = null;\n\n let sourceX = 0;\n let sourceY = 0;\n let multiConnectMode = false;\n let multiConnectLines: Map = new Map();\n\n const initDrag = () => {\n dragStarted = true;\n debug('connection', `Connection drag started from node \"${sourceNodeId}\" handle \"${handleId}\"`);\n canvas._emit('connect-start', { source: sourceNodeId, sourceHandle: handleId });\n\n if (!containerEl) return;\n\n connectionLineInstance = createConnectionLine({\n connectionLineType: canvas._config?.connectionLineType,\n connectionLineStyle: canvas._config?.connectionLineStyle,\n connectionLine: canvas._config?.connectionLine,\n containerEl: containerEl!,\n });\n tempSvg = connectionLineInstance.svg;\n\n const handleRect = handleEl.getBoundingClientRect();\n const initContainerRect = containerEl.getBoundingClientRect();\n // Live viewport: reactive `viewport` may lag a frame behind a zoom.\n const liveVp = canvas._viewportLive ?? canvas.viewport;\n const initZoom = liveVp?.zoom || 1;\n const initVpX = liveVp?.x || 0;\n const initVpY = liveVp?.y || 0;\n\n sourceX = (handleRect.left + handleRect.width / 2 - initContainerRect.left - initVpX) / initZoom;\n sourceY = (handleRect.top + handleRect.height / 2 - initContainerRect.top - initVpY) / initZoom;\n\n connectionLineInstance.update({ fromX: sourceX, fromY: sourceY, toX: sourceX, toY: sourceY, source: sourceNodeId, sourceHandle: handleId });\n\n const viewportEl = containerEl.querySelector('.flow-viewport');\n if (viewportEl) viewportEl.appendChild(tempSvg);\n\n canvas.pendingConnection = {\n source: sourceNodeId,\n sourceHandle: handleId,\n position: { x: sourceX, y: sourceY },\n };\n\n connectAutoPan = startConnectionAutoPan(containerEl, canvas, startX, startY);\n\n // Measure every handle ONCE for the gesture. Valid for the whole\n // drag: nodes don't move during a connect-drag and viewport panning\n // doesn't change flow-space handle centers. If a future feature moves\n // nodes mid-connect-drag, rebuild this on those moves. The same\n // screen→flow transform findSnapTarget uses keeps centers consistent.\n dragHandleIndex = buildHandleIndex(\n containerEl,\n (sx: number, sy: number) => canvas.screenToFlowPosition(sx, sy),\n );\n\n applyValidationClasses(containerEl, sourceNodeId, handleId, canvas, undefined, dragHandleIndex);\n\n // Create ghost node preview when onEdgeDrop is configured\n if (canvas._config?.onEdgeDrop) {\n const previewFn = canvas._config.edgeDropPreview;\n const detail = { source: sourceNodeId, sourceHandle: handleId };\n const previewResult = previewFn ? previewFn(detail) : 'New Node';\n\n if (previewResult !== null) {\n ghostEl = document.createElement('div');\n ghostEl.className = 'flow-ghost-node';\n\n const ghostHandle = document.createElement('div');\n ghostHandle.className = 'flow-ghost-handle';\n ghostEl.appendChild(ghostHandle);\n\n if (typeof previewResult === 'string') {\n const label = document.createElement('span');\n label.textContent = previewResult;\n ghostEl.appendChild(label);\n } else {\n ghostEl.appendChild(previewResult);\n }\n\n ghostEl.style.left = `${sourceX}px`;\n ghostEl.style.top = `${sourceY}px`;\n\n const viewportEl = containerEl.querySelector('.flow-viewport');\n if (viewportEl) viewportEl.appendChild(ghostEl);\n }\n }\n };\n\n const getMultiConnectSources = (): Array<{ nodeId: string; handleId: string; pos: XYPosition }> => {\n const selected = [...canvas.selectedNodes] as string[];\n const result: Array<{ nodeId: string; handleId: string; pos: XYPosition }> = [];\n const containerRect = containerEl!.getBoundingClientRect();\n // Live viewport: reactive `viewport` may lag a frame behind a zoom.\n const liveVp = canvas._viewportLive ?? canvas.viewport;\n const zoom = liveVp?.zoom || 1;\n const vpX = liveVp?.x || 0;\n const vpY = liveVp?.y || 0;\n\n for (const id of selected) {\n if (id === sourceNodeId) continue;\n const nodeEl = containerEl?.querySelector(`[data-flow-node-id=\"${CSS.escape(id)}\"]`);\n const srcHandle = nodeEl?.querySelector('[data-flow-handle-type=\"source\"]') as HTMLElement | null;\n if (!srcHandle) continue;\n\n const handleRect = srcHandle.getBoundingClientRect();\n result.push({\n nodeId: id,\n handleId: srcHandle.dataset.flowHandleId ?? 'source',\n pos: {\n x: (handleRect.left + handleRect.width / 2 - containerRect.left - vpX) / zoom,\n y: (handleRect.top + handleRect.height / 2 - containerRect.top - vpY) / zoom,\n },\n });\n }\n return result;\n };\n\n const enterMultiConnect = (cursorFlowPos: XYPosition) => {\n multiConnectMode = true;\n\n // Move the primary connection line into the multi-connect set\n if (connectionLineInstance) {\n multiConnectLines.set(sourceNodeId, {\n line: connectionLineInstance,\n sourceNodeId: sourceNodeId,\n sourceHandleId: handleId,\n sourcePos: { x: sourceX, y: sourceY },\n valid: true,\n });\n connectionLineInstance = null;\n }\n\n // Create lines for each other selected node's source handle\n const sources = getMultiConnectSources();\n const viewportEl = containerEl!.querySelector('.flow-viewport');\n\n for (const src of sources) {\n const line = createConnectionLine({\n connectionLineType: canvas._config?.connectionLineType,\n connectionLineStyle: canvas._config?.connectionLineStyle,\n connectionLine: canvas._config?.connectionLine,\n containerEl: containerEl!,\n });\n\n line.update({\n fromX: src.pos.x, fromY: src.pos.y,\n toX: cursorFlowPos.x, toY: cursorFlowPos.y,\n source: src.nodeId, sourceHandle: src.handleId,\n });\n\n if (viewportEl) viewportEl.appendChild(line.svg);\n\n multiConnectLines.set(src.nodeId, {\n line,\n sourceNodeId: src.nodeId,\n sourceHandleId: src.handleId,\n sourcePos: src.pos,\n valid: true,\n });\n }\n };\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n if (!dragStarted) {\n const dx = moveEvent.clientX - startX;\n const dy = moveEvent.clientY - startY;\n if (Math.abs(dx) >= DRAG_THRESHOLD || Math.abs(dy) >= DRAG_THRESHOLD) {\n initDrag();\n // Auto-activate multi-connect when enabled and multiple nodes selected\n if (canvas._config?.multiConnect && canvas.selectedNodes.size > 1 && canvas.selectedNodes.has(sourceNodeId)) {\n const cursorFlowPos = canvas.screenToFlowPosition(moveEvent.clientX, moveEvent.clientY);\n enterMultiConnect(cursorFlowPos);\n }\n } else {\n return;\n }\n }\n\n const cursorFlowPos = canvas.screenToFlowPosition(moveEvent.clientX, moveEvent.clientY);\n\n if (multiConnectMode) {\n // Find nearest target handle for all lines to converge on\n const snap = findSnapTarget({\n containerEl: containerEl!,\n handleType: 'target',\n excludeNodeId: sourceNodeId,\n cursorFlowPos,\n connectionSnapRadius,\n getNode: (id: string) => canvas.getNode(id),\n toFlowPosition: (sx: number, sy: number) => canvas.screenToFlowPosition(sx, sy),\n connectionMode: canvas._config?.connectionMode,\n index: dragHandleIndex ?? undefined,\n });\n\n if (snap.element !== snappedHandle) {\n snappedHandle?.classList.remove('flow-handle-active');\n snap.element?.classList.add('flow-handle-active');\n snappedHandle = snap.element;\n }\n\n // Determine target info for validation\n const targetNodeEl = snap.element?.closest('[x-flow-node]') as HTMLElement | null;\n const targetNodeId = targetNodeEl?.dataset.flowNodeId ?? null;\n const targetHandleId = snap.element?.dataset.flowHandleId ?? 'target';\n const normalColor = canvas._config?.connectionLineStyle?.stroke\n ?? (getComputedStyle(containerEl!).getPropertyValue('--flow-edge-stroke-selected').trim() || CONNECTION_ACTIVE_COLOR);\n\n for (const entry of multiConnectLines.values()) {\n entry.line.update({\n fromX: entry.sourcePos.x, fromY: entry.sourcePos.y,\n toX: snap.position.x, toY: snap.position.y,\n source: entry.sourceNodeId, sourceHandle: entry.sourceHandleId,\n });\n\n // Validate per-source when snapped to a target handle\n if (snap.element && targetNodeId) {\n const connection = {\n source: entry.sourceNodeId,\n sourceHandle: entry.sourceHandleId,\n target: targetNodeId,\n targetHandle: targetHandleId,\n };\n const targetNode = canvas.getNode(targetNodeId);\n const builtInValid = targetNode?.connectable !== false\n && entry.sourceNodeId !== targetNodeId\n && isValidConnection(connection, canvas.edges, { preventCycles: canvas._config?.preventCycles });\n const rulesValid = builtInValid && checkConnectionRules(connection, canvas._config?.connectionRules, canvas._nodeMap);\n const limitValid = rulesValid && checkHandleLimits(containerEl!, connection, canvas.edges);\n const handleValid = limitValid && runHandleValidators(containerEl!, connection);\n const globalValid = handleValid\n && (!canvas._config?.isValidConnection || canvas._config.isValidConnection(connection));\n\n entry.valid = globalValid;\n const path = entry.line.svg.querySelector('path');\n if (path) {\n if (!globalValid) {\n const invalidColor = getComputedStyle(containerEl!).getPropertyValue('--flow-connection-line-invalid').trim() || CONNECTION_INVALID_COLOR;\n path.setAttribute('stroke', invalidColor);\n } else {\n path.setAttribute('stroke', normalColor);\n }\n }\n } else {\n entry.valid = true;\n const path = entry.line.svg.querySelector('path');\n if (path) path.setAttribute('stroke', normalColor);\n }\n }\n\n canvas.pendingConnection = { ...canvas.pendingConnection, position: snap.position };\n connectAutoPan?.updatePointer(moveEvent.clientX, moveEvent.clientY);\n return;\n }\n\n const snap = findSnapTarget({\n containerEl,\n handleType: 'target',\n excludeNodeId: sourceNodeId,\n cursorFlowPos,\n connectionSnapRadius,\n getNode: (id: string) => canvas.getNode(id),\n toFlowPosition: (sx: number, sy: number) => canvas.screenToFlowPosition(sx, sy),\n index: dragHandleIndex ?? undefined,\n });\n\n if (snap.element !== snappedHandle) {\n snappedHandle?.classList.remove('flow-handle-active');\n snap.element?.classList.add('flow-handle-active');\n snappedHandle = snap.element;\n }\n\n // Position ghost node and adjust connection line target\n if (ghostEl) {\n if (snap.element) {\n // Snapped to a real handle — hide ghost, line goes to handle\n ghostEl.style.display = 'none';\n connectionLineInstance?.update({ fromX: sourceX, fromY: sourceY, toX: snap.position.x, toY: snap.position.y, source: sourceNodeId, sourceHandle: handleId });\n } else {\n // No snap — show ghost at cursor, line goes to ghost handle\n ghostEl.style.display = '';\n ghostEl.style.left = `${cursorFlowPos.x}px`;\n ghostEl.style.top = `${cursorFlowPos.y}px`;\n // Ghost handle is at top-center of ghost node; connection line targets that point\n connectionLineInstance?.update({ fromX: sourceX, fromY: sourceY, toX: cursorFlowPos.x, toY: cursorFlowPos.y, source: sourceNodeId, sourceHandle: handleId });\n }\n } else {\n connectionLineInstance?.update({ fromX: sourceX, fromY: sourceY, toX: snap.position.x, toY: snap.position.y, source: sourceNodeId, sourceHandle: handleId });\n }\n\n canvas.pendingConnection = { ...canvas.pendingConnection, position: snap.position };\n connectAutoPan?.updatePointer(moveEvent.clientX, moveEvent.clientY);\n };\n\n const onPointerUp = async (upEvent: PointerEvent) => {\n connectAutoPan?.stop();\n connectAutoPan = null;\n document.removeEventListener('pointermove', onPointerMove);\n document.removeEventListener('pointerup', onPointerUp);\n // Mirror the listener set registered on drag-start (move/up/cancel)\n // and torn down by the gesture cleanup — otherwise each drop\n // leaks an orphan `pointercancel` handler on `document`.\n document.removeEventListener('pointercancel', onPointerUp);\n activeHandleGestureCleanups.delete(handleEl);\n // The gesture is ending; drop the index before any early return so a\n // stale one can never leak into a later drag. (A click that never\n // dragged never built one — it stays null and the click-to-connect\n // apply below falls through to the legacy path.)\n dragHandleIndex = null;\n\n // Guard against overlapping drops while an async connectValidator is pending.\n if (canvas._connectValidating) return;\n\n if (multiConnectMode) {\n const dropPosition = canvas.screenToFlowPosition(upEvent.clientX, upEvent.clientY);\n\n // Find the single drop target handle\n let targetHandle: HTMLElement | null = snappedHandle;\n if (!targetHandle) {\n const dropTarget = document.elementFromPoint(upEvent.clientX, upEvent.clientY);\n targetHandle = dropTarget?.closest('[data-flow-handle-type=\"target\"]') as HTMLElement | null;\n }\n\n const targetNodeEl = targetHandle?.closest('[x-flow-node]') as HTMLElement | null;\n const targetNodeId = targetNodeEl?.dataset.flowNodeId ?? null;\n const targetHandleId = targetHandle?.dataset.flowHandleId ?? 'target';\n\n const validEdges: Array<{ id: string; source: string; sourceHandle: string; target: string; targetHandle: string }> = [];\n const validConnections: Connection[] = [];\n const invalidEntries: MultiConnectLine[] = [];\n const validEntries: MultiConnectLine[] = [];\n\n if (targetHandle && targetNodeId) {\n const targetNode = canvas.getNode(targetNodeId);\n\n for (const entry of multiConnectLines.values()) {\n const connection = {\n source: entry.sourceNodeId,\n sourceHandle: entry.sourceHandleId,\n target: targetNodeId,\n targetHandle: targetHandleId,\n };\n\n const builtInValid = targetNode?.connectable !== false\n && entry.sourceNodeId !== targetNodeId\n && isValidConnection(connection, canvas.edges, { preventCycles: canvas._config?.preventCycles });\n const rulesValid = builtInValid && checkConnectionRules(connection, canvas._config?.connectionRules, canvas._nodeMap);\n const limitValid = rulesValid && checkHandleLimits(containerEl!, connection, canvas.edges);\n const handleValid = limitValid && runHandleValidators(containerEl!, connection);\n const globalValid = handleValid\n && (!canvas._config?.isValidConnection || canvas._config.isValidConnection(connection));\n\n if (globalValid) {\n const edgeId = `e-${entry.sourceNodeId}-${targetNodeId}-${Date.now()}-${edgeIdCounter++}`;\n validEdges.push({ id: edgeId, ...connection });\n validConnections.push(connection);\n validEntries.push(entry);\n } else {\n invalidEntries.push(entry);\n }\n }\n } else {\n invalidEntries.push(...multiConnectLines.values());\n }\n\n for (const entry of validEntries) {\n entry.line.destroy();\n }\n\n if (validEdges.length > 0) {\n canvas.addEdges(validEdges);\n for (const connection of validConnections) {\n canvas._emit('connect', { connection });\n }\n canvas._emit('multi-connect', { connections: validConnections });\n }\n\n if (invalidEntries.length > 0) {\n setTimeout(() => {\n for (const entry of invalidEntries) {\n entry.line.destroy();\n }\n }, 100);\n }\n\n snappedHandle?.classList.remove('flow-handle-active');\n canvas._emit('connect-end', {\n connection: validConnections.length > 0 ? validConnections[0] : null,\n source: sourceNodeId,\n sourceHandle: handleId,\n position: dropPosition,\n });\n\n multiConnectLines.clear();\n multiConnectMode = false;\n clearValidationClasses(containerEl);\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n return;\n }\n\n if (!dragStarted) {\n // Click (no drag): start click-to-connect\n if (canvas._config?.connectOnClick !== false) {\n debug('connection', `Click-to-connect started from node \"${sourceNodeId}\" handle \"${handleId}\"`);\n canvas._emit('connect-start', { source: sourceNodeId, sourceHandle: handleId });\n canvas.pendingConnection = {\n source: sourceNodeId,\n sourceHandle: handleId,\n position: { x: 0, y: 0 },\n };\n canvas._container?.classList.add('flow-connecting');\n // Click-to-connect is a one-shot, not a drag gesture:\n // dragHandleIndex is null here, so this uses the legacy path.\n applyValidationClasses(containerEl, sourceNodeId, handleId, canvas, undefined, dragHandleIndex ?? undefined);\n }\n return;\n }\n\n // Drag completed: existing drag-to-connect logic.\n //\n // NB: the drag-line SVG is intentionally kept alive past this point so\n // the async connectValidator (if any) can pulse it via\n // `.flow-connect-line--validating`. It is destroyed in the `finally`\n // at the bottom of this block so every return path still cleans up.\n const dragLineEl = connectionLineInstance?.svg ?? null;\n ghostEl?.remove();\n ghostEl = null;\n snappedHandle?.classList.remove('flow-handle-active');\n clearValidationClasses(containerEl);\n\n const dropPosition = canvas.screenToFlowPosition(upEvent.clientX, upEvent.clientY);\n const connectEndBase = { source: sourceNodeId, sourceHandle: handleId, position: dropPosition };\n\n try {\n\n let targetHandle: HTMLElement | null = snappedHandle;\n if (!targetHandle) {\n const dropTarget = document.elementFromPoint(upEvent.clientX, upEvent.clientY);\n targetHandle = dropTarget?.closest('[data-flow-handle-type=\"target\"]') as HTMLElement | null;\n }\n\n if (targetHandle) {\n const targetNodeEl = targetHandle.closest('[x-flow-node]') as HTMLElement | null;\n const targetNodeId = targetNodeEl?.dataset.flowNodeId;\n const targetHandleId = targetHandle.dataset.flowHandleId ?? 'target';\n\n if (targetNodeId) {\n if (targetHandle[HANDLE_CONNECTABLE_END_KEY] === false) {\n debug('connection', 'Connection rejected (handle not connectable end)');\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n return;\n }\n\n const targetNode = canvas.getNode(targetNodeId);\n if (targetNode && !isConnectable(targetNode)) {\n debug('connection', `Connection rejected (target \"${targetNodeId}\" not connectable)`);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n return;\n }\n\n const connection = {\n source: sourceNodeId,\n sourceHandle: handleId,\n target: targetNodeId,\n targetHandle: targetHandleId,\n };\n\n if (isValidConnection(connection, canvas.edges, { preventCycles: canvas._config?.preventCycles })) {\n if (!checkConnectionRules(connection, canvas._config?.connectionRules, canvas._nodeMap)) {\n debug('connection', 'Connection rejected (connection rules)', connection);\n dispatchConnectRejected(containerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n return;\n }\n if (!checkHandleLimits(containerEl, connection, canvas.edges)) {\n debug('connection', 'Connection rejected (handle limit)', connection);\n dispatchConnectRejected(containerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n return;\n }\n if (!runHandleValidators(containerEl, connection)) {\n debug('connection', 'Connection rejected (per-handle validator)', connection);\n dispatchConnectRejected(containerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n return;\n }\n if (canvas._config?.isValidConnection && !canvas._config.isValidConnection(connection)) {\n debug('connection', 'Connection rejected (custom validator)', connection);\n dispatchConnectRejected(containerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n return;\n }\n\n // Async validator gate\n const asyncValidator = canvas._config?.connectValidator;\n if (asyncValidator) {\n const validatingClass = canvas._config?.validatingHandleClass ?? 'flow-handle-validating';\n const { sourceEl, targetEl } = findHandleElements(containerEl, connection);\n canvas._connectValidating = true;\n setDragLineValidating(dragLineEl, true);\n let asyncResult: { allowed: boolean; reason?: string };\n try {\n asyncResult = await runConnectValidator(\n asyncValidator, connection, sourceEl, targetEl, containerEl, validatingClass,\n );\n } finally {\n canvas._connectValidating = false;\n setDragLineValidating(dragLineEl, false);\n }\n if (!asyncResult.allowed) {\n debug('connection', 'Connection rejected (async connectValidator)', { connection, reason: asyncResult.reason });\n dispatchConnectRejected(containerEl, { ...connection, reason: asyncResult.reason });\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n return;\n }\n }\n\n const edgeId = `e-${sourceNodeId}-${targetNodeId}-${Date.now()}-${edgeIdCounter++}`;\n canvas.addEdges({ id: edgeId, ...connection });\n debug('connection', `Connection created: ${sourceNodeId} → ${targetNodeId}`, connection);\n canvas._emit('connect', { connection });\n canvas._emit('connect-end', { connection, ...connectEndBase });\n } else {\n debug('connection', 'Connection rejected (invalid)', connection);\n dispatchConnectRejected(containerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n }\n } else {\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n }\n } else {\n if (canvas._config?.onEdgeDrop) {\n const centeredPosition = {\n x: dropPosition.x - DEFAULT_NODE_WIDTH / 2,\n y: dropPosition.y - DEFAULT_NODE_HEIGHT / 2,\n };\n const newNode = canvas._config.onEdgeDrop({\n source: sourceNodeId,\n sourceHandle: handleId,\n position: centeredPosition,\n });\n if (newNode) {\n const connection: Connection = {\n source: sourceNodeId,\n sourceHandle: handleId,\n target: newNode.id,\n targetHandle: 'target',\n };\n // Per-handle: only source validator fires; target node is not yet in DOM\n if (!checkHandleLimits(containerEl, connection, canvas.edges)) {\n debug('connection', 'Edge drop: connection rejected (handle limit)');\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n } else if (!runHandleValidators(containerEl, connection)) {\n debug('connection', 'Edge drop: connection rejected (per-handle validator)');\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n } else if (!canvas._config.isValidConnection || canvas._config.isValidConnection(connection)) {\n canvas.addNodes(newNode);\n const edgeId = `e-${sourceNodeId}-${newNode.id}-${Date.now()}-${edgeIdCounter++}`;\n canvas.addEdges({ id: edgeId, ...connection });\n debug('connection', `Edge drop: created node \"${newNode.id}\" and edge`, connection);\n canvas._emit('connect', { connection });\n canvas._emit('connect-end', { connection, ...connectEndBase });\n } else {\n debug('connection', 'Edge drop: connection rejected by validator');\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n }\n } else {\n debug('connection', 'Edge drop: callback returned null');\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n }\n } else {\n debug('connection', 'Connection cancelled (no target)');\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n }\n }\n } finally {\n // Tear down the drag-line SVG regardless of which path produced the\n // outcome (success, sync rejection, async rejection, or early return).\n setDragLineValidating(dragLineEl, false);\n connectionLineInstance?.destroy();\n connectionLineInstance = null;\n }\n\n canvas.pendingConnection = null;\n };\n\n document.addEventListener('pointermove', onPointerMove);\n document.addEventListener('pointerup', onPointerUp);\n document.addEventListener('pointercancel', onPointerUp);\n\n activeHandleGestureCleanups.set(handleEl, () => {\n document.removeEventListener('pointermove', onPointerMove);\n document.removeEventListener('pointerup', onPointerUp);\n document.removeEventListener('pointercancel', onPointerUp);\n connectAutoPan?.stop();\n connectionLineInstance?.destroy();\n connectionLineInstance = null;\n ghostEl?.remove();\n ghostEl = null;\n for (const entry of multiConnectLines.values()) {\n entry.line.destroy();\n }\n multiConnectLines.clear();\n multiConnectMode = false;\n snappedHandle?.classList.remove('flow-handle-active');\n clearValidationClasses(containerEl);\n dragHandleIndex = null;\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n });\n}\n\n/**\n * Start the TARGET-handle pointer interaction: drag an existing edge's target\n * endpoint off this handle to reconnect it elsewhere.\n *\n * Extracted verbatim from the per-handle `pointerdown` listener, for the same\n * reason as the source variant. The propagation asymmetry with the source handler\n * is DELIBERATE and preserved: this one calls `preventDefault()` +\n * `stopPropagation()` only AFTER all seven guards pass (primary button, canvas +\n * node resolved, not animation-locked, `edgesReconnectable`, no reconnect already\n * in flight, at least one matching edge, that edge is reconnectable at its target\n * end). A target handle with no reconnectable edge therefore FALLS THROUGH to the\n * node drag / schema row reorder underneath it — that is how you grab a node by\n * an unconnected target pip. Do not hoist the propagation calls.\n */\nexport function startTargetHandlePointerInteraction(\n handleEl: HTMLElement,\n canvas: any,\n e: PointerEvent,\n): void {\n if (e.button !== 0) return;\n\n const handleId = handleEl.dataset.flowHandleId ?? 'target';\n const nodeEl = handleEl.closest('[x-flow-node]');\n const nodeId = nodeEl?.getAttribute('data-flow-node-id') ?? null;\n if (!canvas || !nodeId) return;\n if (canvas._animationLocked) return;\n\n // Global reconnectable guard\n if (canvas._config?.edgesReconnectable === false) return;\n\n // Already reconnecting?\n if (canvas._pendingReconnection) return;\n\n // Find edge(s) connected to this target handle\n const matchingEdges = (canvas.edges as FlowEdge[]).filter(\n (edge: FlowEdge) =>\n edge.target === nodeId &&\n (edge.targetHandle ?? 'target') === handleId,\n );\n if (matchingEdges.length === 0) return;\n\n // Prefer the selected edge; if none selected require exactly one match\n const connectedEdge =\n matchingEdges.find((edge: FlowEdge) => edge.selected) ??\n (matchingEdges.length === 1 ? matchingEdges[0] : null);\n if (!connectedEdge) return;\n\n // Per-edge reconnectable guard\n const edgeReconnectable = connectedEdge.reconnectable ?? true;\n if (edgeReconnectable === false || edgeReconnectable === 'source') return;\n\n e.preventDefault();\n e.stopPropagation();\n\n const startX = e.clientX;\n const startY = e.clientY;\n let dragging = false;\n let reconnectCleanedUp = false;\n let snappedHandle: HTMLElement | null = null;\n const connectionSnapRadius = canvas._config?.connectionSnapRadius ?? 20;\n\n const containerEl = handleEl.closest('.flow-container') as HTMLElement;\n if (!containerEl) return;\n\n // Compute source handle (anchor) center in flow coordinates\n const sourceNodeEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(connectedEdge.source)}\"]`,\n ) as HTMLElement | null;\n const sourceHandleSelector = connectedEdge.sourceHandle\n ? `[data-flow-handle-id=\"${CSS.escape(connectedEdge.sourceHandle)}\"]`\n : `[data-flow-handle-type=\"source\"]`;\n const sourceHandleEl = sourceNodeEl?.querySelector(sourceHandleSelector) as HTMLElement | null;\n\n const cRect = containerEl.getBoundingClientRect();\n // Live viewport: reactive `viewport` may lag a frame behind a zoom.\n const liveVp = canvas._viewportLive ?? canvas.viewport;\n const initZoom = liveVp?.zoom || 1;\n const initVpX = liveVp?.x || 0;\n const initVpY = liveVp?.y || 0;\n\n let anchorX: number;\n let anchorY: number;\n if (sourceHandleEl) {\n const shRect = sourceHandleEl.getBoundingClientRect();\n anchorX = (shRect.left + shRect.width / 2 - cRect.left - initVpX) / initZoom;\n anchorY = (shRect.top + shRect.height / 2 - cRect.top - initVpY) / initZoom;\n } else {\n // Fallback: estimate source node bottom-center\n const sourceNode = canvas.getNode(connectedEdge.source);\n if (!sourceNode) return;\n const w = sourceNode.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = sourceNode.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n anchorX = sourceNode.position.x + w / 2;\n anchorY = sourceNode.position.y + h;\n }\n\n let tempSvg: SVGSVGElement | null = null;\n let reconnectLineInstance: ConnectionLineInstance | null = null;\n let connectAutoPan: ReturnType = null;\n let lastMoveX = startX;\n let lastMoveY = startY;\n\n // Separate index for this reconnect gesture (its own pointer scope),\n // built once on drag-start and cleared in cleanupReconnection.\n let reconnectHandleIndex: HandleIndex | null = null;\n\n const startReconnectionDrag = () => {\n dragging = true;\n\n // Dim the edge via its tagged \n const edgeGEl = containerEl.querySelector(\n `[data-flow-edge-id=\"${connectedEdge.id}\"]`,\n );\n if (edgeGEl) {\n edgeGEl.classList.add('flow-edge-reconnecting');\n }\n\n canvas._emit('reconnect-start', { edge: connectedEdge, handleType: 'target' as HandleType });\n debug('reconnect', `Reconnection drag started from target handle on edge \"${connectedEdge.id}\"`);\n\n // Create connection line for visual feedback\n reconnectLineInstance = createConnectionLine({\n connectionLineType: canvas._config?.connectionLineType,\n connectionLineStyle: canvas._config?.connectionLineStyle,\n connectionLine: canvas._config?.connectionLine,\n containerEl: containerEl!,\n });\n tempSvg = reconnectLineInstance.svg;\n\n const flowPos = canvas.screenToFlowPosition(startX, startY);\n reconnectLineInstance.update({\n fromX: anchorX, fromY: anchorY,\n toX: flowPos.x, toY: flowPos.y,\n source: connectedEdge.source, sourceHandle: connectedEdge.sourceHandle,\n });\n\n const viewportEl = containerEl.querySelector('.flow-viewport');\n if (viewportEl) {\n viewportEl.appendChild(tempSvg);\n }\n\n // Set pendingConnection for target handle highlighting\n canvas.pendingConnection = {\n source: connectedEdge.source,\n sourceHandle: connectedEdge.sourceHandle,\n position: flowPos,\n };\n\n // Set _pendingReconnection\n canvas._pendingReconnection = {\n edge: connectedEdge,\n draggedEnd: 'target' as HandleType,\n anchorPosition: { x: anchorX, y: anchorY },\n position: flowPos,\n };\n\n // Auto-pan\n connectAutoPan = startConnectionAutoPan(containerEl, canvas, lastMoveX, lastMoveY);\n\n // Measure handles once for this reconnect gesture (same rationale +\n // transform as the connect-drag path). Excludes the edge being\n // reconnected from duplicate/limit accounting via its id.\n reconnectHandleIndex = buildHandleIndex(\n containerEl,\n (sx: number, sy: number) => canvas.screenToFlowPosition(sx, sy),\n );\n\n applyValidationClasses(containerEl, connectedEdge.source, connectedEdge.sourceHandle ?? 'source', canvas, connectedEdge.id, reconnectHandleIndex);\n };\n\n const onPointerMove = (moveE: PointerEvent) => {\n lastMoveX = moveE.clientX;\n lastMoveY = moveE.clientY;\n\n if (!dragging) {\n const dist = Math.sqrt(\n (moveE.clientX - startX) ** 2 + (moveE.clientY - startY) ** 2,\n );\n if (dist >= DRAG_THRESHOLD) {\n startReconnectionDrag();\n }\n return;\n }\n\n const cursorFlowPos = canvas.screenToFlowPosition(moveE.clientX, moveE.clientY);\n\n const snap = findSnapTarget({\n containerEl,\n handleType: 'target',\n excludeNodeId: connectedEdge.source,\n cursorFlowPos,\n connectionSnapRadius,\n getNode: (id: string) => canvas.getNode(id),\n toFlowPosition: (sx: number, sy: number) => canvas.screenToFlowPosition(sx, sy),\n index: reconnectHandleIndex ?? undefined,\n });\n\n if (snap.element !== snappedHandle) {\n snappedHandle?.classList.remove('flow-handle-active');\n snap.element?.classList.add('flow-handle-active');\n snappedHandle = snap.element;\n }\n\n reconnectLineInstance?.update({\n fromX: anchorX, fromY: anchorY,\n toX: snap.position.x, toY: snap.position.y,\n source: connectedEdge.source, sourceHandle: connectedEdge.sourceHandle,\n });\n\n if (canvas.pendingConnection) {\n canvas.pendingConnection = {\n ...canvas.pendingConnection,\n position: snap.position,\n };\n }\n if (canvas._pendingReconnection) {\n canvas._pendingReconnection = {\n ...canvas._pendingReconnection,\n position: snap.position,\n };\n }\n\n connectAutoPan?.updatePointer(moveE.clientX, moveE.clientY);\n };\n\n const cleanupReconnection = () => {\n if (reconnectCleanedUp) return;\n reconnectCleanedUp = true;\n\n document.removeEventListener('pointermove', onPointerMove);\n document.removeEventListener('pointerup', onPointerUp);\n document.removeEventListener('pointercancel', onPointerUp);\n connectAutoPan?.stop();\n connectAutoPan = null;\n reconnectLineInstance?.destroy();\n reconnectLineInstance = null;\n tempSvg = null;\n reconnectHandleIndex = null;\n snappedHandle?.classList.remove('flow-handle-active');\n activeHandleGestureCleanups.delete(handleEl);\n\n // Undim the edge\n const edgeGEl = containerEl.querySelector(\n `[data-flow-edge-id=\"${connectedEdge.id}\"]`,\n );\n if (edgeGEl) {\n edgeGEl.classList.remove('flow-edge-reconnecting');\n }\n\n clearValidationClasses(containerEl);\n canvas.pendingConnection = null;\n canvas._pendingReconnection = null;\n };\n\n const onPointerUp = async (upE: PointerEvent) => {\n if (!dragging) {\n cleanupReconnection();\n return;\n }\n\n // Guard against overlapping drops while an async connectValidator is pending.\n if (canvas._connectValidating) return;\n\n // Use snapped handle if available, otherwise fall back to elementFromPoint\n let targetHandleEl: HTMLElement | null = snappedHandle;\n if (!targetHandleEl) {\n const dropTarget = document.elementFromPoint(upE.clientX, upE.clientY);\n targetHandleEl = dropTarget?.closest('[data-flow-handle-type=\"target\"]') as HTMLElement | null;\n }\n\n let successful = false;\n\n if (targetHandleEl) {\n const targetNodeEl = targetHandleEl.closest('[x-flow-node]') as HTMLElement | null;\n const dropNodeId = targetNodeEl?.dataset.flowNodeId;\n const dropHandleId = targetHandleEl.dataset.flowHandleId;\n\n if (dropNodeId) {\n const dropNode = canvas.getNode(dropNodeId);\n if (dropNode?.connectable !== false) {\n const newConnection: Connection = {\n source: connectedEdge.source,\n sourceHandle: connectedEdge.sourceHandle,\n target: dropNodeId,\n targetHandle: dropHandleId,\n };\n\n const oldEdge = { ...connectedEdge };\n const reconnectDragLineEl = reconnectLineInstance?.svg ?? null;\n setDragLineValidating(reconnectDragLineEl, true);\n let result: { applied: boolean; reason?: string };\n try {\n result = await applyReconnectValidation({\n edge: connectedEdge,\n newConnection,\n canvas,\n containerEl,\n endpoint: 'target',\n });\n } finally {\n setDragLineValidating(reconnectDragLineEl, false);\n }\n\n if (result.applied) {\n successful = true;\n debug('reconnect', `Edge \"${connectedEdge.id}\" reconnected (target)`, newConnection);\n canvas._emit('reconnect', { oldEdge, newConnection });\n } else {\n debug('reconnect', 'Reconnection rejected', { connection: newConnection, reason: result.reason });\n }\n }\n }\n }\n\n if (!successful) {\n debug('reconnect', `Edge \"${connectedEdge.id}\" reconnection cancelled — snapping back`);\n }\n\n canvas._emit('reconnect-end', { edge: connectedEdge, successful });\n cleanupReconnection();\n };\n\n document.addEventListener('pointermove', onPointerMove);\n document.addEventListener('pointerup', onPointerUp);\n document.addEventListener('pointercancel', onPointerUp);\n activeHandleGestureCleanups.set(handleEl, cleanupReconnection);\n}\n\n/**\n * Start whichever pointer interaction the handle's type calls for.\n *\n * The entry point the delegated canvas-level listener calls (see\n * `../handle-delegation.ts`). The branch that used to be taken at directive init\n * — `if (type === 'source')` — is taken here instead, off the dataset the\n * directive stamped on the element.\n */\nexport function startHandlePointerInteraction(\n handleEl: HTMLElement,\n canvas: any,\n event: PointerEvent,\n): void {\n if (handleEl.dataset.flowHandleType === 'source') {\n startSourceHandlePointerInteraction(handleEl, canvas, event);\n } else {\n startTargetHandlePointerInteraction(handleEl, canvas, event);\n }\n}\n\n/**\n * Should THIS handle attach its own `pointerdown` listener?\n *\n * Only when delegation is switched off. Default is on, so the canvas's single\n * delegated listener drives every handle; `delegatedHandleEvents: false` is the\n * revert path and restores the pre-delegation per-handle listeners byte for byte.\n */\nfunction usesLegacyHandlePointerListeners(canvas: any): boolean {\n return canvas?._config?.delegatedHandleEvents === false;\n}\n\nexport function registerFlowHandleDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-handle',\n (\n el,\n { value, modifiers, expression },\n { evaluate, effect, cleanup },\n ) => {\n const type: HandleType = value === 'source' ? 'source' : 'target';\n\n // Position priority: compound modifier > single modifier > data attribute > node default > default\n const hasTop = modifiers.includes('top');\n const hasBottom = modifiers.includes('bottom');\n const hasLeft = modifiers.includes('left');\n const hasRight = modifiers.includes('right');\n const hasExplicitModifier = hasTop || hasBottom || hasLeft || hasRight;\n\n let position: HandlePosition;\n if (hasTop && hasLeft) position = 'top-left';\n else if (hasTop && hasRight) position = 'top-right';\n else if (hasBottom && hasLeft) position = 'bottom-left';\n else if (hasBottom && hasRight) position = 'bottom-right';\n else if (hasTop) position = 'top';\n else if (hasRight) position = 'right';\n else if (hasBottom) position = 'bottom';\n else if (hasLeft) position = 'left';\n else position = (el.getAttribute('data-flow-handle-position') as HandlePosition)\n ?? (type === 'source' ? 'bottom' : 'top');\n\n // Handle ID priority: expression > pre-existing data attribute > type\n // Expression can be a string (handle ID) or an object { id?, position? }\n let handleId: string;\n let expressionHasPosition = false;\n\n if (expression) {\n const evaluated = evaluate(expression);\n if (evaluated && typeof evaluated === 'object' && !Array.isArray(evaluated)) {\n // Object expression: { id?: string, position?: HandlePosition }\n handleId = evaluated.id || el.getAttribute('data-flow-handle-id') || type;\n if (evaluated.position) {\n position = evaluated.position;\n expressionHasPosition = true;\n }\n } else {\n handleId = evaluated || el.getAttribute('data-flow-handle-id') || type;\n }\n } else {\n handleId = el.getAttribute('data-flow-handle-id') || type;\n }\n\n // Hide individual handle via .hidden modifier\n if (modifiers.includes('hidden')) {\n el.style.display = 'none';\n }\n\n // Store handle metadata on the element for measurement\n el.dataset.flowHandleType = type;\n el.dataset.flowHandlePosition = position;\n el.dataset.flowHandleId = handleId;\n\n // Mark handles with explicit modifiers so layout algorithms skip them\n if (hasExplicitModifier) {\n el.dataset.flowHandleExplicit = 'true';\n }\n\n // Reactively update position from object expression when it has a position property\n if (expressionHasPosition && expression) {\n el.dataset.flowHandleExplicit = 'true';\n effect(() => {\n const evaluated = evaluate(expression);\n if (evaluated && typeof evaluated === 'object' && !Array.isArray(evaluated) && evaluated.position) {\n el.dataset.flowHandlePosition = evaluated.position;\n }\n });\n }\n\n // When no explicit modifier and no expression-based position, reactively inherit from node.sourcePosition/targetPosition\n if (!hasExplicitModifier && !expressionHasPosition) {\n const getNodeData = (): FlowNode | undefined => {\n const nodeEl = el.closest('[x-flow-node]') as HTMLElement | null;\n const nodeId = nodeEl?.dataset.flowNodeId;\n if (!nodeId) return undefined;\n const canvasEl = el.closest('[x-data]') as HTMLElement | null;\n if (!canvasEl) return undefined;\n const canvas = Alpine.$data(canvasEl);\n return canvas?.getNode?.(nodeId);\n };\n\n effect(() => {\n const node = getNodeData();\n if (!node) return;\n const nodeDefault = type === 'source' ? node.sourcePosition : node.targetPosition;\n if (nodeDefault) {\n el.dataset.flowHandlePosition = nodeDefault;\n }\n });\n }\n\n // Add base styling hook\n el.classList.add('flow-handle', `flow-handle-${type}`);\n\n // Find the parent node element to get the node ID\n const getNodeId = (): string | null => {\n const nodeEl = el.closest('[x-flow-node]');\n if (!nodeEl) return null;\n // The node ID is stored by x-flow-node on the element\n return nodeEl.getAttribute('data-flow-node-id') ?? null;\n };\n\n const getCanvas = () => {\n const canvasEl = el.closest('[x-data]') as HTMLElement | null;\n return canvasEl ? Alpine.$data(canvasEl) : null;\n };\n\n /**\n * The canvas, resolved ONCE at init and shared by every init-time consumer\n * below (keyboardConnect, and the two `usesLegacyHandlePointerListeners`\n * checks). `getCanvas()` costs a `closest('[x-data]')` plus an\n * `Alpine.$data()` — which mints a fresh `mergeProxies` Proxy — and a\n * Draftsman-scale schema graph mounts ~5,000 handles, so this is paid 5,000×.\n * All three read config that cannot change between them.\n *\n * The pointer GESTURES still resolve the canvas fresh on every press via\n * `getCanvas()`; only the init-time reads are hoisted.\n */\n const initCanvas = getCanvas();\n\n // ── Keyboard drag-to-connect (a11y, opt-in) ───────────────────────\n // When canvas._config.keyboardConnect is true, make this handle focusable\n // and wire Enter/Space/Escape to drive a connection flow equivalent to\n // pointer drag-to-connect. Source handles arm a pending connection;\n // target handles complete it; Escape cancels. Attributes are only set\n // when the feature is enabled so existing tab order is preserved on apps\n // that haven't opted in.\n let keyboardBindingsCleanup: (() => void) | null = null;\n {\n if (initCanvas?._config?.keyboardConnect) {\n el.setAttribute('tabindex', '0');\n el.setAttribute('role', 'button');\n el.setAttribute('aria-label', `${type} handle ${handleId}`);\n\n const clearPending = (canvas: { _pendingKeyboardConnect?: PendingKeyboardConnect | null } | null | undefined) => {\n const prev = canvas?._pendingKeyboardConnect;\n if (!prev) return;\n const containerEl = el.closest('.flow-container') as HTMLElement | null;\n if (containerEl) {\n const prevEl = containerEl.querySelector(\n `[data-flow-node-id=\"${CSS.escape(prev.sourceNodeId)}\"] [data-flow-handle-id=\"${CSS.escape(prev.sourceHandleId)}\"][data-flow-handle-type=\"source\"]`,\n );\n (prevEl as HTMLElement | null)?.classList.remove('flow-handle-connect-pending');\n }\n if (canvas) canvas._pendingKeyboardConnect = null;\n };\n\n const onKeyDown = (e: KeyboardEvent) => {\n const isActivate = e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar';\n if (!isActivate) return;\n\n const canvas = getCanvas();\n if (!canvas) return;\n if (canvas._animationLocked) return;\n\n const nodeId = getNodeId();\n if (!nodeId) return;\n\n if (type === 'source') {\n // Guard: node / handle must be connectable as start\n const sourceNode = canvas.getNode?.(nodeId);\n if (sourceNode && !isConnectable(sourceNode)) return;\n if (el[HANDLE_CONNECTABLE_START_KEY] === false) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n // Re-arming: clear previous pending first\n clearPending(canvas);\n\n canvas._pendingKeyboardConnect = {\n sourceNodeId: nodeId,\n sourceHandleId: handleId,\n };\n el.classList.add('flow-handle-connect-pending');\n\n // Route to the existing aria-live announcer when present.\n // If no announcer is configured on this canvas, skip silently —\n // we don't invent a new live region here.\n canvas._announcer?.announce?.(`Connecting from ${type} handle ${handleId}. Focus a target handle and press Enter to connect.`);\n } else {\n // target handle: complete pending connection (if any)\n if (!canvas._pendingKeyboardConnect) return;\n\n const targetNode = canvas.getNode?.(nodeId);\n if (targetNode && !isConnectable(targetNode)) return;\n if (el[HANDLE_CONNECTABLE_END_KEY] === false) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n const { sourceNodeId, sourceHandleId } = canvas._pendingKeyboardConnect;\n const connection: Connection = {\n source: sourceNodeId,\n sourceHandle: sourceHandleId,\n target: nodeId,\n targetHandle: handleId,\n };\n\n const containerEl = el.closest('.flow-container') as HTMLElement | null;\n // Clear pending state + class before running the pipeline — the\n // validator is async, and we don't want the pending affordance\n // stuck on a no-longer-armed source if the user re-focuses mid-\n // validation.\n clearPending(canvas);\n\n if (!containerEl) return;\n\n // Fire-and-forget the async validator pipeline. Rejections\n // dispatch flow-connect-rejected via the shared helper; success\n // emits `connect` + adds the edge.\n void applyConnectValidation({ connection, canvas, containerEl }).then((result) => {\n if (result.applied) {\n canvas._announcer?.announce?.(`Connected ${sourceNodeId} to ${nodeId}.`);\n }\n });\n }\n };\n\n el.addEventListener('keydown', onKeyDown);\n\n // Shared Escape listener on the canvas container (not document) so\n // we don't hijack keys on other focusable apps on the page. Only one\n // listener is attached per container regardless of how many handles\n // init — see `containerEscListeners` at the top of this module.\n const containerEl = el.closest('.flow-container') as HTMLElement | null;\n if (containerEl) {\n const existing = containerEscListeners.get(containerEl);\n if (existing) {\n existing.count += 1;\n } else {\n const handler = (e: KeyboardEvent) => {\n if (e.key !== 'Escape') return;\n // Look up the canvas fresh each fire — the container outlives\n // any individual handle, so closing over `getCanvas()` (which\n // closes over `el`) would leak if that handle is torn down.\n const canvasEl =\n containerEl.matches('[x-data]')\n ? containerEl\n : (containerEl.closest('[x-data]') as HTMLElement | null)\n ?? (containerEl.querySelector('[x-data]') as HTMLElement | null);\n if (!canvasEl) return;\n const canvas = Alpine.$data(canvasEl) as\n | { _pendingKeyboardConnect?: PendingKeyboardConnect | null }\n | null;\n if (!canvas?._pendingKeyboardConnect) return;\n clearPending(canvas);\n };\n containerEl.addEventListener('keydown', handler);\n containerEscListeners.set(containerEl, { count: 1, handler });\n }\n }\n\n keyboardBindingsCleanup = () => {\n el.removeEventListener('keydown', onKeyDown);\n if (containerEl) {\n const entry = containerEscListeners.get(containerEl);\n if (entry) {\n entry.count -= 1;\n if (entry.count <= 0) {\n containerEl.removeEventListener('keydown', entry.handler);\n containerEscListeners.delete(containerEl);\n }\n }\n }\n el.removeAttribute('tabindex');\n el.removeAttribute('role');\n el.removeAttribute('aria-label');\n el.classList.remove('flow-handle-connect-pending');\n };\n }\n }\n\n if (type === 'source') {\n // ── Source handle: initiate drag-to-connect ──────────────────\n // The gesture body lives at module scope (startSourceHandlePointerInteraction)\n // so the delegated canvas-level listener can drive any handle. The canvas is\n // still resolved fresh on every press, exactly as before.\n const onPointerDown = (e: PointerEvent) => {\n startSourceHandlePointerInteraction(el, getCanvas(), e);\n };\n\n if (usesLegacyHandlePointerListeners(initCanvas)) {\n el.addEventListener('pointerdown', onPointerDown);\n }\n\n // Highlight source handles during source-end reconnection\n const onReconnectPointerEnter = () => {\n const canvas = getCanvas();\n if (!canvas?._pendingReconnection || canvas._pendingReconnection.draggedEnd !== 'source') return;\n\n const nodeId = getNodeId();\n if (nodeId) {\n const node = canvas.getNode(nodeId);\n if (node && !isConnectable(node)) return;\n }\n if (el[HANDLE_CONNECTABLE_START_KEY] === false) return;\n\n el.classList.add('flow-handle-active');\n };\n\n const onReconnectPointerLeave = () => {\n el.classList.remove('flow-handle-active');\n };\n\n el.addEventListener('pointerenter', onReconnectPointerEnter);\n el.addEventListener('pointerleave', onReconnectPointerLeave);\n\n cleanup(() => {\n // Abort any gesture this handle still has in flight (see\n // `activeHandleGestureCleanups`), then drop its slot.\n activeHandleGestureCleanups.get(el)?.();\n activeHandleGestureCleanups.delete(el);\n keyboardBindingsCleanup?.();\n el.removeEventListener('pointerdown', onPointerDown);\n el.removeEventListener('pointerenter', onReconnectPointerEnter);\n el.removeEventListener('pointerleave', onReconnectPointerLeave);\n el.classList.remove('flow-handle', `flow-handle-${type}`);\n });\n } else {\n // ── Target handle: highlight on hover during connection drag ──\n const onPointerEnter = () => {\n const canvas = getCanvas();\n if (!canvas?.pendingConnection) return;\n\n // ── Connectable guard (target hover) ───────────────────\n const nodeId = getNodeId();\n if (nodeId) {\n const node = canvas.getNode(nodeId);\n if (node && !isConnectable(node)) return;\n }\n if (el[HANDLE_CONNECTABLE_END_KEY] === false) return;\n\n el.classList.add('flow-handle-active');\n };\n\n const onPointerLeave = () => {\n el.classList.remove('flow-handle-active');\n };\n\n el.addEventListener('pointerenter', onPointerEnter);\n el.addEventListener('pointerleave', onPointerLeave);\n\n // ── Target handle: click to complete click-to-connect ──────\n const onTargetClick = async (e: MouseEvent) => {\n const canvas = getCanvas();\n if (!canvas?.pendingConnection) return;\n if (canvas._config?.connectOnClick === false) return;\n if (canvas._connectValidating) return;\n\n e.preventDefault();\n e.stopPropagation();\n\n const targetNodeId = getNodeId();\n if (!targetNodeId) return;\n\n if (el[HANDLE_CONNECTABLE_END_KEY] === false) {\n debug('connection', `Click-to-connect rejected (handle not connectable end)`);\n canvas._emit('connect-end', { connection: null, source: canvas.pendingConnection.source, sourceHandle: canvas.pendingConnection.sourceHandle, position: { x: 0, y: 0 } });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n const ctcContainer = el.closest('.flow-container') as HTMLElement;\n if (ctcContainer) clearValidationClasses(ctcContainer);\n return;\n }\n\n const targetNode = canvas.getNode(targetNodeId);\n if (targetNode && !isConnectable(targetNode)) {\n debug('connection', `Click-to-connect rejected (target \"${targetNodeId}\" not connectable)`);\n canvas._emit('connect-end', { connection: null, source: canvas.pendingConnection.source, sourceHandle: canvas.pendingConnection.sourceHandle, position: { x: 0, y: 0 } });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n const cContainer = el.closest('.flow-container') as HTMLElement;\n if (cContainer) clearValidationClasses(cContainer);\n return;\n }\n\n const connection: Connection = {\n source: canvas.pendingConnection.source,\n sourceHandle: canvas.pendingConnection.sourceHandle,\n target: targetNodeId,\n targetHandle: handleId,\n };\n\n const connectEndBase = { source: canvas.pendingConnection.source, sourceHandle: canvas.pendingConnection.sourceHandle, position: { x: 0, y: 0 } };\n\n if (isValidConnection(connection, canvas.edges, { preventCycles: canvas._config?.preventCycles })) {\n const clickContainerEl = el.closest('.flow-container') as HTMLElement;\n if (!checkConnectionRules(connection, canvas._config?.connectionRules, canvas._nodeMap)) {\n debug('connection', 'Click-to-connect rejected (connection rules)', connection);\n dispatchConnectRejected(clickContainerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n if (clickContainerEl) clearValidationClasses(clickContainerEl);\n return;\n }\n if (clickContainerEl && !checkHandleLimits(clickContainerEl, connection, canvas.edges)) {\n debug('connection', 'Click-to-connect rejected (handle limit)', connection);\n dispatchConnectRejected(clickContainerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n clearValidationClasses(clickContainerEl);\n return;\n }\n if (clickContainerEl && !runHandleValidators(clickContainerEl, connection)) {\n debug('connection', 'Click-to-connect rejected (per-handle validator)', connection);\n dispatchConnectRejected(clickContainerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n if (clickContainerEl) clearValidationClasses(clickContainerEl);\n return;\n }\n if (canvas._config?.isValidConnection && !canvas._config.isValidConnection(connection)) {\n debug('connection', 'Click-to-connect rejected (custom validator)', connection);\n dispatchConnectRejected(clickContainerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n if (clickContainerEl) clearValidationClasses(clickContainerEl);\n return;\n }\n\n // Async validator gate\n const asyncValidator = canvas._config?.connectValidator;\n if (asyncValidator && clickContainerEl) {\n const validatingClass = canvas._config?.validatingHandleClass ?? 'flow-handle-validating';\n const { sourceEl, targetEl } = findHandleElements(clickContainerEl, connection);\n canvas._connectValidating = true;\n let asyncResult: { allowed: boolean; reason?: string };\n try {\n asyncResult = await runConnectValidator(\n asyncValidator, connection, sourceEl, targetEl, clickContainerEl, validatingClass,\n );\n } finally {\n canvas._connectValidating = false;\n }\n if (!asyncResult.allowed) {\n debug('connection', 'Click-to-connect rejected (async connectValidator)', { connection, reason: asyncResult.reason });\n dispatchConnectRejected(clickContainerEl, { ...connection, reason: asyncResult.reason });\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n clearValidationClasses(clickContainerEl);\n return;\n }\n }\n\n const edgeId = `e-${connection.source}-${connection.target}-${Date.now()}-${edgeIdCounter++}`;\n canvas.addEdges({ id: edgeId, ...connection });\n debug('connection', `Click-to-connect: ${connection.source} → ${connection.target}`, connection);\n canvas._emit('connect', { connection });\n canvas._emit('connect-end', { connection, ...connectEndBase });\n } else {\n debug('connection', 'Click-to-connect rejected (invalid)', connection);\n const invalidContainerEl = el.closest('.flow-container') as HTMLElement;\n dispatchConnectRejected(invalidContainerEl, connection);\n canvas._emit('connect-end', { connection: null, ...connectEndBase });\n }\n\n canvas.pendingConnection = null;\n canvas._container?.classList.remove('flow-connecting');\n const endContainer = el.closest('.flow-container') as HTMLElement;\n if (endContainer) clearValidationClasses(endContainer);\n };\n\n el.addEventListener('click', onTargetClick);\n\n // ── Target handle: pointerdown for edge reconnection ──────\n // Body at module scope (startTargetHandlePointerInteraction) so the\n // delegated canvas-level listener can drive any handle.\n const onTargetPointerDown = (e: PointerEvent) => {\n startTargetHandlePointerInteraction(el, getCanvas(), e);\n };\n\n if (usesLegacyHandlePointerListeners(initCanvas)) {\n el.addEventListener('pointerdown', onTargetPointerDown);\n }\n\n cleanup(() => {\n // Abort any gesture this handle still has in flight (see\n // `activeHandleGestureCleanups`), then drop its slot.\n activeHandleGestureCleanups.get(el)?.();\n activeHandleGestureCleanups.delete(el);\n keyboardBindingsCleanup?.();\n el.removeEventListener('pointerdown', onTargetPointerDown);\n el.removeEventListener('pointerenter', onPointerEnter);\n el.removeEventListener('pointerleave', onPointerLeave);\n el.removeEventListener('click', onTargetClick);\n el.classList.remove('flow-handle', `flow-handle-${type}`, 'flow-handle-active');\n });\n }\n },\n );\n}\n","// ============================================================================\n// Handle pointer-event delegation\n//\n// WHY: every handle used to attach its own `pointerdown` listener at directive\n// init. A Draftsman-scale schema graph has ~5,000 handles, so that was ~5,000\n// listeners paid at mount and re-paid on every schema row re-stamp. This module\n// installs ONE `pointerdown` listener per canvas that can start the same gesture\n// for any handle, resolving it with `closest('[data-flow-handle-type]')`.\n//\n// SCOPE: pointerdown ONLY. `pointerenter` / `pointerleave` / `click` / `keydown`\n// stay per-handle — the directive still runs, still stamps datasets, expandos\n// and classes, and still owns keyboard/a11y. It just skips the pointerdown\n// registration. Opt out with `delegatedHandleEvents: false`.\n//\n// ── WHERE THIS LISTENER SITS, AND WHY IT MATTERS ────────────────────────────\n//\n// It goes on `.flow-viewport` in the CAPTURE phase. Both halves of that are\n// load-bearing; every other placement regresses something.\n//\n// CAPTURE, not bubble.\n// Two things sit on ANCESTORS of a handle and listen for pointerdown in the\n// BUBBLE phase: d3-drag's node drag (`core/drag.ts`, bound on the node) and\n// the schema row reorder (`schema/reorderable.ts:249`, bound on the row).\n// Neither excludes handles. The only reason pressing a handle doesn't also\n// drag the node or reorder the row is that the handle's own listener used to\n// sit DEEPER in the tree, fire first, and call `stopPropagation()`. A delegated\n// listener on an ancestor in the bubble phase would fire AFTER both of them and\n// that suppression would be gone. In the capture phase it fires BEFORE them, so\n// `stopPropagation()` lands exactly where it used to.\n//\n// `.flow-viewport`, not `.flow-container`.\n// Seven whiteboard tools (`flow-freehand`, `flow-eraser`, `flow-circle-draw`,\n// `flow-rectangle-draw`, `flow-arrow-draw`, `flow-highlighter`,\n// `flow-text-tool`) each do `containerEl.addEventListener('pointerdown', …,\n// true)` and call `stopImmediatePropagation()`, which is what makes handles\n// inert while a draw tool is active. They attach LAZILY, when the tool\n// activates — i.e. AFTER a listener we register at canvas init. Had we attached\n// on the container in capture too, ordering would degrade to registration order\n// and we would win, starting a connect-drag in draw mode. Attaching one level\n// down means the tools' listener is on a strict ANCESTOR in the same phase, so\n// it still runs first and its `stopImmediatePropagation()` keeps us from ever\n// firing. Same suppression chain as before, through the same DOM mechanism.\n//\n// Ordering against `flow-node.ts`'s easy-connect (node, capture) is harmless:\n// that handler bails immediately on handle presses.\n// ============================================================================\n\nimport { startHandlePointerInteraction } from './directives/flow-handle';\n\n/**\n * Install the single delegated `pointerdown` listener for one canvas.\n *\n * @param rootEl Element to attach to — `.flow-viewport` in production (see the\n * block above). Also acts as the containment boundary: a press on\n * a handle outside it is ignored.\n * @param canvas The canvas that owns `rootEl`. Passed straight through to the\n * gesture, replacing the directive's per-press `Alpine.$data()`\n * lookup with a value we already hold.\n * @returns Teardown that removes the listener.\n */\nexport function installHandleDelegation(\n rootEl: HTMLElement,\n canvas: any,\n): () => void {\n const onPointerDown = (event: Event): void => {\n const target = event.target as HTMLElement | null;\n const handleEl = target?.closest?.('[data-flow-handle-type]') as HTMLElement | null;\n if (!handleEl) return;\n if (!rootEl.contains(handleEl)) return;\n\n // Ownership is discriminated on the CANVAS ROOT, not the nearest [x-data].\n // A nested CANVAS necessarily brings its own `.flow-container`, so its handles\n // resolve to it and we decline them — a parent must never drive a child's\n // handles. A plain Alpine scope inside a node (`
`\n // wrapping a handle) brings no container, so it still resolves to US — which is\n // correct, and is the whole point: Alpine's `$data` merges the entire ancestor\n // scope stack, so such a handle has always found this canvas and always worked.\n // Keying ownership off `closest('[x-data]')` would disown it and leave it inert.\n //\n // This is also exactly the element the gesture itself derives as `containerEl`\n // (`flow-handle.ts`), so the two can never disagree about which canvas is in play.\n if (canvas?._container && handleEl.closest('.flow-container') !== canvas._container) return;\n\n startHandlePointerInteraction(handleEl, canvas, event as PointerEvent);\n };\n\n rootEl.addEventListener('pointerdown', onPointerDown, true);\n return () => rootEl.removeEventListener('pointerdown', onPointerDown, true);\n}\n","// ── Keyboard Shortcuts ──────────────────────────────────────────────\n// Utilities for configurable keyboard shortcut resolution and matching.\n\nexport type KeyCode = string;\n\nexport interface KeyboardShortcuts {\n /** Key(s) for deleting selected elements. Default: ['Delete', 'Backspace'] */\n delete?: KeyCode | KeyCode[] | null;\n\n /** Modifier key to activate selection box (rubber-band). Default: 'Shift' */\n selectionBox?: KeyCode | null;\n\n /** Modifier key for multi-select (click to add). Default: 'Shift' */\n multiSelect?: KeyCode | null;\n\n /** Key(s) for arrow-key node movement. Default: ['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'] */\n moveNodes?: KeyCode | KeyCode[] | null;\n\n /** Base movement step in pixels. Default: 5 */\n moveStep?: number;\n\n /** Modifier key that multiplies movement step. Default: 'Shift' */\n moveStepModifier?: KeyCode | null;\n\n /** Multiplier applied when moveStepModifier is held. Default: 4 */\n moveStepMultiplier?: number;\n\n /** Key for copy (with Ctrl/Cmd). Default: 'c' */\n copy?: KeyCode | null;\n\n /** Key for paste (with Ctrl/Cmd). Default: 'v' */\n paste?: KeyCode | null;\n\n /** Key for cut (with Ctrl/Cmd). Default: 'x' */\n cut?: KeyCode | null;\n\n /** Key for undo (with Ctrl/Cmd). Default: 'z' */\n undo?: KeyCode | null;\n\n /** Key for redo (with Ctrl/Cmd+Shift). Default: 'z' */\n redo?: KeyCode | null;\n\n /** Key for escape/cancel. Default: 'Escape' */\n escape?: KeyCode | null;\n\n /** Modifier key to temporarily toggle selection mode during a drag. Default: 'Alt' */\n selectionModeToggle?: KeyCode | null;\n\n /** Key to toggle between box and lasso selection tools. Default: 'l' */\n selectionToolToggle?: KeyCode | null;\n}\n\nexport const SHORTCUT_DEFAULTS: Readonly> = {\n delete: ['Delete', 'Backspace'],\n selectionBox: 'Shift',\n multiSelect: 'Shift',\n moveNodes: ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'],\n moveStep: 5,\n moveStepModifier: 'Shift',\n moveStepMultiplier: 4,\n copy: 'c',\n paste: 'v',\n cut: 'x',\n undo: 'z',\n redo: 'z',\n escape: 'Escape',\n selectionModeToggle: 'Alt',\n selectionToolToggle: 'l',\n};\n\n/** Merge user-provided shortcuts with defaults. `null` = disabled, `undefined`/missing = use default. */\nexport function resolveShortcuts(user?: Partial): Required {\n if (!user) return { ...SHORTCUT_DEFAULTS };\n const resolved = { ...SHORTCUT_DEFAULTS } as Record;\n for (const key of Object.keys(user) as (keyof KeyboardShortcuts)[]) {\n if (key in user) {\n resolved[key] = user[key];\n }\n }\n return resolved as Required;\n}\n\n/** Check if a keyboard event's key matches a configured shortcut. Case-insensitive for single characters (Shift changes 'w' to 'W'). */\nexport function matchesKey(eventKey: string, configured: KeyCode | KeyCode[] | null | undefined): boolean {\n if (configured === null || configured === undefined) return false;\n const key = eventKey.length === 1 ? eventKey.toLowerCase() : eventKey;\n if (Array.isArray(configured)) {\n return configured.some(c => (c.length === 1 ? c.toLowerCase() : c) === key);\n }\n return (configured.length === 1 ? configured.toLowerCase() : configured) === key;\n}\n\n/**\n * Decide whether an arrow-key nudge should capture a history snapshot. Captures\n * once per physical keypress: skips auto-repeat (`repeat`), skips when nothing is\n * selected, and skips keys that produced no movement (dx === 0 && dy === 0).\n */\nexport function shouldCaptureNudge(repeat: boolean, selectedCount: number, dx: number, dy: number): boolean {\n return !repeat && selectedCount > 0 && (dx !== 0 || dy !== 0);\n}\n\n/** Check if a modifier key is held on an event. Maps 'Shift'→shiftKey, 'Control'→ctrlKey, etc. */\nexport function matchesModifier(\n event: { shiftKey: boolean; ctrlKey: boolean; metaKey: boolean; altKey: boolean },\n configured: KeyCode | null | undefined,\n): boolean {\n if (configured === null || configured === undefined) return false;\n switch (configured) {\n case 'Shift': return event.shiftKey;\n case 'Control': return event.ctrlKey;\n case 'Meta': return event.metaKey;\n case 'Alt': return event.altKey;\n default: return false;\n }\n}\n","// ============================================================================\n// Long-Press Detection Utility\n//\n// Attaches pointer event listeners to detect a long-press gesture.\n// Fires a callback after a configurable delay. Cancels if the finger\n// moves beyond a threshold or lifts early.\n// ============================================================================\n\nexport interface LongPressOptions {\n /** Delay before firing in milliseconds. Default: 500 */\n duration?: number;\n /** Max movement in pixels before cancel. Default: 10 */\n moveThreshold?: number;\n}\n\n/**\n * Attach long-press detection to an element.\n * Returns a cleanup function that removes all listeners.\n */\nexport function attachLongPress(\n el: HTMLElement,\n callback: (event: PointerEvent) => void,\n options: LongPressOptions = {},\n): () => void {\n const duration = options.duration ?? 500;\n const moveThreshold = options.moveThreshold ?? 10;\n\n let timer: ReturnType | null = null;\n let startX = 0;\n let startY = 0;\n let startEvent: PointerEvent | null = null;\n\n function cancel(): void {\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n startEvent = null;\n document.removeEventListener('pointermove', onMove);\n document.removeEventListener('pointerup', cancel);\n document.removeEventListener('pointercancel', cancel);\n }\n\n function onMove(e: PointerEvent): void {\n const dx = e.clientX - startX;\n const dy = e.clientY - startY;\n if (dx * dx + dy * dy > moveThreshold * moveThreshold) {\n cancel();\n }\n }\n\n function onDown(e: PointerEvent): void {\n cancel();\n startX = e.clientX;\n startY = e.clientY;\n startEvent = e;\n\n document.addEventListener('pointermove', onMove);\n document.addEventListener('pointerup', cancel);\n document.addEventListener('pointercancel', cancel);\n\n timer = setTimeout(() => {\n const evt = startEvent;\n cancel();\n if (evt) {\n callback(evt);\n }\n }, duration);\n }\n\n el.addEventListener('pointerdown', onDown);\n\n return () => {\n cancel();\n el.removeEventListener('pointerdown', onDown);\n };\n}\n","// ============================================================================\n// Orthogonal Edge Path\n//\n// Routes edges with right-angle segments around obstacle nodes using a\n// visibility graph + Dijkstra pathfinding algorithm. Falls back to\n// getSmoothStepPath when no obstacles exist or routing fails.\n// ============================================================================\n\nimport type { HandlePosition, Rect } from '../types';\nimport { getBend, type EdgePathResult } from './utils';\nimport { getSmoothStepPath } from './smoothstep';\n\nexport const OBSTACLE_PADDING = 20;\n\nexport interface OrthogonalPathParams {\n sourceX: number;\n sourceY: number;\n sourcePosition?: HandlePosition;\n targetX: number;\n targetY: number;\n targetPosition?: HandlePosition;\n obstacles?: Rect[];\n borderRadius?: number;\n}\n\n/** Offset from the handle before routing begins. Slightly larger than OBSTACLE_PADDING\n * so the offset point is guaranteed to land outside the padded source/target rect. */\nconst HANDLE_OFFSET = OBSTACLE_PADDING + 1;\n\n/** Secondary-cost weights for route quality. These live ONLY in the lexicographic\n * tie-break — they never affect the primary Manhattan length, so a route is never\n * made longer to save a bend or reduce deviation. Tunable (see spec §3.3). */\nconst BEND_WEIGHT = 1;\nconst DEVIATION_WEIGHT = 0.5;\n\n/** Signature of the cost constants, folded into the route cache key so tuning the\n * weights (or a future routing config) can't serve routes computed under the old\n * cost. See {@link routeKey}. */\nexport const ROUTE_COST_VERSION = `b${BEND_WEIGHT}d${DEVIATION_WEIGHT}`;\n\nfunction getDirection(position: HandlePosition): { x: number; y: number } {\n switch (position) {\n case 'top':\n return { x: 0, y: -1 };\n case 'bottom':\n return { x: 0, y: 1 };\n case 'left':\n return { x: -1, y: 0 };\n case 'right':\n return { x: 1, y: 0 };\n default:\n return { x: 0, y: 1 };\n }\n}\n\n// ── Geometry helpers ─────────────────────────────────────────────────────────\n\nfunction padRect(rect: Rect, padding: number): Rect {\n return {\n x: rect.x - padding,\n y: rect.y - padding,\n width: rect.width + padding * 2,\n height: rect.height + padding * 2,\n };\n}\n\nfunction isInsideRect(px: number, py: number, rect: Rect): boolean {\n return (\n px > rect.x &&\n px < rect.x + rect.width &&\n py > rect.y &&\n py < rect.y + rect.height\n );\n}\n\nfunction isHSegmentBlocked(\n x1: number,\n x2: number,\n y: number,\n rects: Rect[],\n): boolean {\n const minX = Math.min(x1, x2);\n const maxX = Math.max(x1, x2);\n\n for (const r of rects) {\n const rLeft = r.x;\n const rRight = r.x + r.width;\n const rTop = r.y;\n const rBottom = r.y + r.height;\n\n // Segment must cross the rect's vertical span and horizontal span\n if (y > rTop && y < rBottom && maxX > rLeft && minX < rRight) {\n return true;\n }\n }\n\n return false;\n}\n\nfunction isVSegmentBlocked(\n x: number,\n y1: number,\n y2: number,\n rects: Rect[],\n): boolean {\n const minY = Math.min(y1, y2);\n const maxY = Math.max(y1, y2);\n\n for (const r of rects) {\n const rLeft = r.x;\n const rRight = r.x + r.width;\n const rTop = r.y;\n const rBottom = r.y + r.height;\n\n if (x > rLeft && x < rRight && maxY > rTop && minY < rBottom) {\n return true;\n }\n }\n\n return false;\n}\n\n// ── Visibility graph ─────────────────────────────────────────────────────────\n\nexport interface RoutePoint {\n x: number;\n y: number;\n index: number;\n}\n\nfunction buildVisibilityGraph(\n sourceX: number,\n sourceY: number,\n targetX: number,\n targetY: number,\n paddedObstacles: Rect[],\n): RoutePoint[] {\n // Collect all candidate X and Y coordinates (scanlines)\n const xSet = new Set([sourceX, targetX]);\n const ySet = new Set([sourceY, targetY]);\n\n for (const r of paddedObstacles) {\n xSet.add(r.x);\n xSet.add(r.x + r.width);\n ySet.add(r.y);\n ySet.add(r.y + r.height);\n }\n\n const xCoords = Array.from(xSet).sort((a, b) => a - b);\n const yCoords = Array.from(ySet).sort((a, b) => a - b);\n\n // Generate all grid intersection points, filtering out those inside obstacles\n const points: RoutePoint[] = [];\n let index = 0;\n\n for (const x of xCoords) {\n for (const y of yCoords) {\n let inside = false;\n for (const r of paddedObstacles) {\n if (isInsideRect(x, y, r)) {\n inside = true;\n break;\n }\n }\n if (!inside) {\n points.push({ x, y, index: index++ });\n }\n }\n }\n\n return points;\n}\n\n// ── Dijkstra ─────────────────────────────────────────────────────────────────\n\n/**\n * Binary min-heap of indices ordered by a caller-supplied `less` comparator.\n * Decrease-key is emulated with lazy insertion — a node may appear multiple\n * times; stale entries are skipped via the caller's `visited` set on pop.\n */\nclass MinHeap {\n private items: number[] = [];\n\n constructor(private less: (a: number, b: number) => boolean) {}\n\n get size(): number {\n return this.items.length;\n }\n\n push(i: number): void {\n this.items.push(i);\n let c = this.items.length - 1;\n while (c > 0) {\n const p = (c - 1) >> 1;\n if (!this.less(this.items[c], this.items[p])) break;\n [this.items[p], this.items[c]] = [this.items[c], this.items[p]];\n c = p;\n }\n }\n\n pop(): number | undefined {\n const n = this.items.length;\n if (n === 0) return undefined;\n const top = this.items[0];\n const last = this.items.pop()!;\n if (n > 1) {\n this.items[0] = last;\n let p = 0;\n for (;;) {\n const l = 2 * p + 1;\n const r = l + 1;\n let m = p;\n if (l < this.items.length && this.less(this.items[l], this.items[m])) m = l;\n if (r < this.items.length && this.less(this.items[r], this.items[m])) m = r;\n if (m === p) break;\n [this.items[p], this.items[m]] = [this.items[m], this.items[p]];\n p = m;\n }\n }\n return top;\n }\n}\n\n/**\n * Build orthogonal adjacency from scanline geometry: along each shared x-line\n * (and y-line) connect only *consecutive* points whose joining segment is\n * unobstructed. Because the weight is additive Manhattan distance, chaining\n * through the intermediate collinear points costs exactly the same as a direct\n * non-consecutive hop, so shortest-path cost is preserved while the neighbour\n * scan drops from O(N) per pop to the handful of segments that actually touch\n * each point. `point.index` equals the array position (see buildVisibilityGraph\n * and findRoute), so it doubles as the adjacency index.\n */\nfunction buildAdjacency(points: RoutePoint[], obstacles: Rect[]): number[][] {\n const adj: number[][] = points.map(() => []);\n const byX = new Map();\n const byY = new Map();\n\n for (const p of points) {\n let xs = byX.get(p.x);\n if (!xs) {\n xs = [];\n byX.set(p.x, xs);\n }\n xs.push(p);\n\n let ys = byY.get(p.y);\n if (!ys) {\n ys = [];\n byY.set(p.y, ys);\n }\n ys.push(p);\n }\n\n for (const line of byX.values()) {\n line.sort((a, b) => a.y - b.y);\n for (let i = 1; i < line.length; i++) {\n const a = line[i - 1];\n const b = line[i];\n if (!isVSegmentBlocked(a.x, a.y, b.y, obstacles)) {\n adj[a.index].push(b.index);\n adj[b.index].push(a.index);\n }\n }\n }\n\n for (const line of byY.values()) {\n line.sort((a, b) => a.x - b.x);\n for (let i = 1; i < line.length; i++) {\n const a = line[i - 1];\n const b = line[i];\n if (!isHSegmentBlocked(a.x, b.x, a.y, obstacles)) {\n adj[a.index].push(b.index);\n adj[b.index].push(a.index);\n }\n }\n }\n\n return adj;\n}\n\nfunction dijkstra(\n source: RoutePoint,\n target: RoutePoint,\n graphPoints: RoutePoint[],\n obstacles: Rect[],\n): RoutePoint[] | null {\n const n = graphPoints.length;\n // Two arrival-axis states per vertex: state s = axis * n + vertexIndex,\n // where axis 0 = arrived on a horizontal segment, 1 = arrived on a vertical one.\n const S = 2 * n;\n const len = new Float64Array(S).fill(Infinity); // primary cost: Manhattan length\n const tie = new Float64Array(S).fill(Infinity); // secondary cost: bends + deviation\n const prev = new Int32Array(S).fill(-1); // predecessor STATE index\n const visited = new Uint8Array(S);\n\n const adj = buildAdjacency(graphPoints, obstacles);\n\n // Endpoint bounding box for the corridor-deviation term.\n const bx0 = Math.min(source.x, target.x);\n const bx1 = Math.max(source.x, target.x);\n const by0 = Math.min(source.y, target.y);\n const by1 = Math.max(source.y, target.y);\n const outOfBox = (p: RoutePoint): number =>\n Math.max(0, bx0 - p.x) + Math.max(0, p.x - bx1) +\n Math.max(0, by0 - p.y) + Math.max(0, p.y - by1);\n\n // Lexicographic order: primary Manhattan length, then secondary tie cost.\n const less = (a: number, b: number): boolean =>\n len[a] < len[b] || (len[a] === len[b] && tie[a] < tie[b]);\n const heap = new MinHeap(less);\n\n // Seed both arrival axes at the source so the first move is bend-free.\n for (let axis = 0; axis < 2; axis++) {\n const s = axis * n + source.index;\n len[s] = 0;\n tie[s] = 0;\n heap.push(s);\n }\n\n const stateNode = (s: number): number => s % n;\n const stateAxis = (s: number): number => (s < n ? 0 : 1);\n\n let bestTarget = -1;\n while (heap.size > 0) {\n const s = heap.pop()!;\n if (visited[s]) continue;\n visited[s] = 1;\n\n const uIdx = stateNode(s);\n if (uIdx === target.index) {\n bestTarget = s; // first target state popped is lexicographically optimal\n break;\n }\n\n const axisIn = stateAxis(s);\n const u = graphPoints[uIdx];\n\n for (const vIdx of adj[uIdx]) {\n const v = graphPoints[vIdx];\n const moveAxis = u.x === v.x ? 1 : 0; // vertical move shares x\n const t = moveAxis * n + vIdx;\n if (visited[t]) continue;\n\n const stepLen = Math.abs(v.x - u.x) + Math.abs(v.y - u.y);\n const bend = axisIn === moveAxis ? 0 : 1;\n const stepTie = BEND_WEIGHT * bend + DEVIATION_WEIGHT * outOfBox(v);\n\n const newLen = len[s] + stepLen;\n const newTie = tie[s] + stepTie;\n\n if (newLen < len[t] || (newLen === len[t] && newTie < tie[t])) {\n len[t] = newLen;\n tie[t] = newTie;\n prev[t] = s;\n heap.push(t);\n }\n }\n }\n\n if (bestTarget === -1) {\n // Target never popped → unreachable in both axis states.\n const s0 = target.index;\n const s1 = n + target.index;\n if (len[s0] === Infinity && len[s1] === Infinity) return null;\n bestTarget = less(s0, s1) ? s0 : s1;\n }\n\n // Reconstruct path (state indices → vertices).\n const path: RoutePoint[] = [];\n let cur = bestTarget;\n while (cur !== -1) {\n path.unshift(graphPoints[stateNode(cur)]);\n cur = prev[cur];\n }\n return path;\n}\n\n// ── Path simplification & SVG ────────────────────────────────────────────────\n\nfunction simplifyPath(points: RoutePoint[]): RoutePoint[] {\n if (points.length <= 2) return points;\n\n const result: RoutePoint[] = [points[0]];\n\n for (let i = 1; i < points.length - 1; i++) {\n const prev = result[result.length - 1];\n const next = points[i + 1];\n const curr = points[i];\n\n // Keep the point if direction changes (not collinear)\n const sameX = prev.x === curr.x && curr.x === next.x;\n const sameY = prev.y === curr.y && curr.y === next.y;\n\n if (!sameX && !sameY) {\n result.push(curr);\n }\n }\n\n result.push(points[points.length - 1]);\n return result;\n}\n\nfunction buildSvgPath(waypoints: RoutePoint[], borderRadius: number): string {\n if (waypoints.length < 2) return '';\n\n let path = `M${waypoints[0].x},${waypoints[0].y}`;\n\n for (let i = 1; i < waypoints.length - 1; i++) {\n const prev = waypoints[i - 1];\n const curr = waypoints[i];\n const next = waypoints[i + 1];\n\n if (borderRadius > 0) {\n path += ` ${getBend(prev.x, prev.y, curr.x, curr.y, next.x, next.y, borderRadius)}`;\n } else {\n path += ` L${curr.x},${curr.y}`;\n }\n }\n\n const last = waypoints[waypoints.length - 1];\n path += ` L${last.x},${last.y}`;\n\n return path;\n}\n\nfunction getPathMidpoint(\n waypoints: RoutePoint[],\n): { x: number; y: number; offsetX: number; offsetY: number } {\n if (waypoints.length < 2) {\n return { x: waypoints[0]?.x ?? 0, y: waypoints[0]?.y ?? 0, offsetX: 0, offsetY: 0 };\n }\n\n // Compute total polyline length\n let totalLength = 0;\n const segLengths: number[] = [];\n\n for (let i = 1; i < waypoints.length; i++) {\n const dx = waypoints[i].x - waypoints[i - 1].x;\n const dy = waypoints[i].y - waypoints[i - 1].y;\n const len = Math.abs(dx) + Math.abs(dy); // Manhattan distance for orthogonal paths\n segLengths.push(len);\n totalLength += len;\n }\n\n // Walk to the midpoint\n let remaining = totalLength / 2;\n\n for (let i = 0; i < segLengths.length; i++) {\n if (remaining <= segLengths[i]) {\n const t = segLengths[i] > 0 ? remaining / segLengths[i] : 0;\n const x = waypoints[i].x + (waypoints[i + 1].x - waypoints[i].x) * t;\n const y = waypoints[i].y + (waypoints[i + 1].y - waypoints[i].y) * t;\n\n return {\n x,\n y,\n offsetX: Math.abs(waypoints[waypoints.length - 1].x - waypoints[0].x) / 2,\n offsetY: Math.abs(waypoints[waypoints.length - 1].y - waypoints[0].y) / 2,\n };\n }\n remaining -= segLengths[i];\n }\n\n // Fallback: last point\n const last = waypoints[waypoints.length - 1];\n return { x: last.x, y: last.y, offsetX: 0, offsetY: 0 };\n}\n\n// ── Shared routing pipeline ──────────────────────────────────────────────────\n\n/** Flow units beyond the endpoint bounding box that the corridor retains. */\nexport const CORRIDOR_MARGIN = 200;\n\n/** Test-only diagnostics for the most recent findRoute call. */\nconst routeDebug = { gridSize: 0, usedFullSet: false };\n\nexport function __routeDebugForTests(): { gridSize: number; usedFullSet: boolean } {\n return { ...routeDebug };\n}\n\n/**\n * Keep only obstacles whose bounding box intersects the endpoint bounding box\n * expanded by CORRIDOR_MARGIN. Most routes interact only with nearby obstacles,\n * so this collapses the scanline grid from every-node to a local neighbourhood.\n */\nfunction corridorObstacles(\n sx: number,\n sy: number,\n tx: number,\n ty: number,\n obstacles: Rect[],\n): Rect[] {\n const minX = Math.min(sx, tx) - CORRIDOR_MARGIN;\n const maxX = Math.max(sx, tx) + CORRIDOR_MARGIN;\n const minY = Math.min(sy, ty) - CORRIDOR_MARGIN;\n const maxY = Math.max(sy, ty) + CORRIDOR_MARGIN;\n return obstacles.filter(\n (r) => r.x < maxX && r.x + r.width > minX && r.y < maxY && r.y + r.height > minY,\n );\n}\n\n/** True if any axis-aligned segment of a graph route crosses a padded obstacle. */\nfunction routeCrossesObstacles(route: RoutePoint[], paddedObstacles: Rect[]): boolean {\n for (let i = 1; i < route.length; i++) {\n const a = route[i - 1];\n const b = route[i];\n if (a.x === b.x) {\n if (isVSegmentBlocked(a.x, a.y, b.y, paddedObstacles)) return true;\n } else if (a.y === b.y) {\n if (isHSegmentBlocked(a.x, b.x, a.y, paddedObstacles)) return true;\n }\n }\n return false;\n}\n\n/**\n * Find an obstacle-free route from source to target using handle-aware offsets,\n * a visibility graph, and Dijkstra pathfinding.\n *\n * Obstacles are first pruned to a corridor around the endpoints; the pruned\n * route is validated against the FULL obstacle set, so pruning that either\n * fails to find a route (coarser grid) or routes through a dropped obstacle\n * transparently retries once with the complete set.\n *\n * Returns simplified waypoints (including actual source/target endpoints) or\n * null if no route can be found.\n *\n * Internal — call the memoized {@link findRoute} wrapper instead.\n */\nfunction computeRoute(\n sourceX: number,\n sourceY: number,\n sourcePosition: HandlePosition,\n targetX: number,\n targetY: number,\n targetPosition: HandlePosition,\n obstacles: Rect[],\n): RoutePoint[] | null {\n // Compute handle-direction offset points so the edge leaves/arrives correctly\n const srcDir = getDirection(sourcePosition);\n const tgtDir = getDirection(targetPosition);\n const srcOffX = sourceX + srcDir.x * HANDLE_OFFSET;\n const srcOffY = sourceY + srcDir.y * HANDLE_OFFSET;\n const tgtOffX = targetX + tgtDir.x * HANDLE_OFFSET;\n const tgtOffY = targetY + tgtDir.y * HANDLE_OFFSET;\n\n // Route against an arbitrary obstacle subset, returning the graph route\n // (offset → … → offset) computed on its padded scanline grid.\n const routeAgainst = (subset: Rect[]): RoutePoint[] | null => {\n const paddedObstacles = subset.map((r) => padRect(r, OBSTACLE_PADDING));\n const graphPoints = buildVisibilityGraph(srcOffX, srcOffY, tgtOffX, tgtOffY, paddedObstacles);\n routeDebug.gridSize = graphPoints.length;\n\n const sourcePoint = graphPoints.find((p) => p.x === srcOffX && p.y === srcOffY);\n const targetPoint = graphPoints.find((p) => p.x === tgtOffX && p.y === tgtOffY);\n if (!sourcePoint) {\n graphPoints.push({ x: srcOffX, y: srcOffY, index: graphPoints.length });\n }\n if (!targetPoint) {\n graphPoints.push({ x: tgtOffX, y: tgtOffY, index: graphPoints.length });\n }\n const finalSource = sourcePoint ?? graphPoints[graphPoints.length - (targetPoint ? 1 : 2)];\n const finalTarget = targetPoint ?? graphPoints[graphPoints.length - 1];\n\n return dijkstra(finalSource, finalTarget, graphPoints, paddedObstacles);\n };\n\n const pruned = corridorObstacles(sourceX, sourceY, targetX, targetY, obstacles);\n const prunedRemovedSome = pruned.length < obstacles.length;\n routeDebug.usedFullSet = !prunedRemovedSome;\n\n let route = routeAgainst(pruned);\n\n // Validate against the full padded set: pruning may fail to route (coarser\n // grid) or route through an obstacle it dropped. Retry once with everything.\n if (prunedRemovedSome) {\n const fullPadded = obstacles.map((r) => padRect(r, OBSTACLE_PADDING));\n const usable = route !== null && route.length >= 2;\n if (!usable || routeCrossesObstacles(route as RoutePoint[], fullPadded)) {\n routeDebug.usedFullSet = true;\n route = routeAgainst(obstacles);\n }\n }\n\n if (!route || route.length < 2) return null;\n\n // Prepend actual source and append actual target\n const fullRoute: RoutePoint[] = [\n { x: sourceX, y: sourceY, index: -1 },\n ...route,\n { x: targetX, y: targetY, index: -2 },\n ];\n\n return simplifyPath(fullRoute);\n}\n\n// ── Route memo cache ─────────────────────────────────────────────────────────\n\nconst ROUTE_CACHE_MAX = 512;\nconst routeCache = new Map();\nconst cacheStats = {\n hits: 0,\n misses: 0,\n clear(): void {\n routeCache.clear();\n this.hits = 0;\n this.misses = 0;\n },\n};\n\n/**\n * Cache key from endpoints + obstacle geometry. Coordinates are rounded to\n * integers so sub-pixel churn (drag jitter, fractional zoom) stays cache-hot.\n */\nfunction routeKey(\n sx: number,\n sy: number,\n sp: HandlePosition,\n tx: number,\n ty: number,\n tp: HandlePosition,\n obstacles: Rect[],\n): string {\n let key = `${ROUTE_COST_VERSION}|${Math.round(sx)},${Math.round(sy)},${sp}|${Math.round(tx)},${Math.round(ty)},${tp}`;\n for (const r of obstacles) {\n key += `|${Math.round(r.x)},${Math.round(r.y)},${Math.round(r.width)},${Math.round(r.height)}`;\n }\n return key;\n}\n\n/**\n * Memoized {@link computeRoute}. Full-graph passes (undo tick, selection flush)\n * re-route every edge even when nothing moved; identical (endpoints, obstacles)\n * inputs return the cached result instead of recomputing.\n *\n * The cached array is returned by reference; callers (getOrthogonalPath,\n * avoidant.ts) only read it. If a future caller mutates the waypoint array,\n * return a shallow copy on hit.\n */\nexport function findRoute(\n sourceX: number,\n sourceY: number,\n sourcePosition: HandlePosition,\n targetX: number,\n targetY: number,\n targetPosition: HandlePosition,\n obstacles: Rect[],\n): RoutePoint[] | null {\n const key = routeKey(sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, obstacles);\n\n if (routeCache.has(key)) {\n cacheStats.hits++;\n // LRU touch: re-insert so this key becomes most-recently used.\n const cached = routeCache.get(key)!;\n routeCache.delete(key);\n routeCache.set(key, cached);\n return cached;\n }\n\n cacheStats.misses++;\n const result = computeRoute(sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, obstacles);\n routeCache.set(key, result);\n if (routeCache.size > ROUTE_CACHE_MAX) {\n // Evict least-recently used (first insertion-ordered key).\n routeCache.delete(routeCache.keys().next().value!);\n }\n return result;\n}\n\nexport function routeCacheStatsForTests(): typeof cacheStats {\n return cacheStats;\n}\n\n// ── Main export ──────────────────────────────────────────────────────────────\n\nexport function getOrthogonalPath({\n sourceX,\n sourceY,\n sourcePosition = 'bottom',\n targetX,\n targetY,\n targetPosition = 'top',\n obstacles,\n borderRadius = 5,\n}: OrthogonalPathParams): EdgePathResult {\n // Fast path: no obstacles → delegate to smoothstep\n if (!obstacles || obstacles.length === 0) {\n return getSmoothStepPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n borderRadius,\n });\n }\n\n const waypoints = findRoute(sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, obstacles);\n\n // No route found → fall back\n if (!waypoints) {\n return getSmoothStepPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n borderRadius,\n });\n }\n\n // Build SVG path\n const path = buildSvgPath(waypoints, borderRadius);\n\n // Compute label midpoint along polyline\n const { x, y, offsetX, offsetY } = getPathMidpoint(waypoints);\n\n return {\n path,\n labelPosition: { x, y },\n labelOffsetX: offsetX,\n labelOffsetY: offsetY,\n };\n}\n","// ============================================================================\n// Endpoint Spread (WS-2)\n//\n// Pure geometry for fanning edges that share an endpoint handle across that\n// handle's row extent. Never grows the extent — a fan wider than `extent`\n// condenses to fit. No DOM, no Alpine: the lane INDEX comes from the canvas\n// grouping pass; this file only turns (index, count, extent, spacing) into a\n// perpendicular offset and applies it on the correct axis.\n// ============================================================================\n\nimport type { HandlePosition } from './types';\n\n/** Default px gap between adjacent lanes when spacing isn't specified. */\nexport const DEFAULT_SPREAD_SPACING = 5;\n\n/** Resolve the desired lane spacing from the config value; null = disabled. */\nexport function resolveSpreadSpacing(\n value: boolean | { spacing?: number } | undefined,\n): number | null {\n if (!value) return null;\n if (value === true) return DEFAULT_SPREAD_SPACING;\n return value.spacing ?? DEFAULT_SPREAD_SPACING;\n}\n\n/**\n * Signed perpendicular offset for lane `index` of a `count`-edge fan, centred on\n * 0 and clamped so the whole fan spans at most `extent`. `count <= 1` → 0, so a\n * handle with a single edge is never moved.\n */\nexport function laneOffset(index: number, count: number, extent: number, spacing: number): number {\n if (count <= 1) return 0;\n const span = Math.min((count - 1) * spacing, Math.max(0, extent));\n const effSpacing = span / (count - 1);\n return (index - (count - 1) / 2) * effSpacing;\n}\n\n/** Apply a lane offset perpendicular to the handle side (L/R fan vertically, T/B horizontally). */\nexport function applyLaneOffset(\n coords: { x: number; y: number },\n side: HandlePosition,\n offset: number,\n): { x: number; y: number } {\n return side === 'left' || side === 'right'\n ? { x: coords.x, y: coords.y + offset }\n : { x: coords.x + offset, y: coords.y };\n}\n","// ============================================================================\n// Sub-Flow Utilities\n//\n// Pure utility functions for parent-child node hierarchies. Handles relative\n// positioning, absolute position resolution, topological sorting, z-index\n// computation, extent clamping, and parent expansion.\n//\n// No DOM or Alpine dependencies — works with plain FlowNode objects.\n// ============================================================================\n\nimport type { FlowNode, XYPosition, Dimensions, CoordinateExtent } from './types';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from './geometry';\n\n/** Padding (in px) added when expanding a parent to fit a child. */\nconst EXPAND_PADDING = 20;\n\n/**\n * Build a Map for O(1) lookups by node ID.\n */\nexport function buildNodeMap(nodes: FlowNode[]): Map {\n return new Map(nodes.map((node) => [node.id, node]));\n}\n\n/**\n * Reconcile a parent→children-ids index in place against the current node list.\n *\n * Only keys whose child list actually changed are written, and keys whose\n * children have all left are deleted. When `target` is an Alpine-reactive Map,\n * appending or moving an UNRELATED node therefore produces zero writes, so node\n * effects that read a single parent's entry (`target.get(id)`) don't re-run.\n * This replaces the previous per-effect `nodes.some(n => n.parentId === id)`\n * scan — each of N node effects subscribed to the whole array and scanned it\n * (O(N) subscriptions per effect, O(N²) proxied reads on any array change).\n */\nexport function reconcileChildrenIndex(\n target: Map,\n nodes: FlowNode[],\n): void {\n const next = new Map();\n for (const node of nodes) {\n if (node.parentId) {\n const list = next.get(node.parentId);\n if (list) {\n list.push(node.id);\n } else {\n next.set(node.parentId, [node.id]);\n }\n }\n }\n // Delete keys that no longer have any children.\n for (const key of target.keys()) {\n if (!next.has(key)) target.delete(key);\n }\n // Upsert only the keys whose child list changed.\n for (const [key, list] of next) {\n const prev = target.get(key);\n if (!prev || prev.length !== list.length || prev.some((id, i) => id !== list[i])) {\n target.set(key, list);\n }\n }\n}\n\n/**\n * Walk the parentId chain and sum positions to get the absolute flow-space\n * position of a node. Iterative with a visited-set cycle guard.\n */\nexport function getAbsolutePosition(\n node: FlowNode,\n nodeMap: Map,\n globalOrigin?: [number, number],\n): XYPosition {\n if (!node.position) return { x: 0, y: 0 };\n let x = node.position.x;\n let y = node.position.y;\n\n const visited = new Set();\n visited.add(node.id);\n\n let current = node.parentId ? nodeMap.get(node.parentId) : undefined;\n while (current) {\n if (visited.has(current.id)) {\n break; // cycle guard\n }\n visited.add(current.id);\n\n // Use parent's visual top-left (accounting for its nodeOrigin)\n const origin = current.nodeOrigin ?? globalOrigin ?? [0, 0];\n const pw = current.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const ph = current.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n x += current.position.x - pw * origin[0];\n y += current.position.y - ph * origin[1];\n\n current = current.parentId ? nodeMap.get(current.parentId) : undefined;\n }\n\n return { x, y };\n}\n\n/**\n * Create a shallow copy of a node with its position set to absolute\n * flow-space coordinates. Shares all other property references.\n */\nexport function toAbsoluteNode(\n node: FlowNode,\n nodeMap: Map,\n globalOrigin?: [number, number],\n): FlowNode {\n if (!node.parentId) {\n return node;\n }\n const abs = getAbsolutePosition(node, nodeMap, globalOrigin);\n return { ...node, position: abs };\n}\n\n/**\n * Map all nodes to absolute-position wrappers.\n */\nexport function toAbsoluteNodes(\n nodes: FlowNode[],\n nodeMap: Map,\n globalOrigin?: [number, number],\n): FlowNode[] {\n return nodes.map((n) => toAbsoluteNode(n, nodeMap, globalOrigin));\n}\n\n/**\n * BFS to collect all descendant IDs of a given node.\n */\nexport function getDescendantIds(\n nodeId: string,\n nodes: FlowNode[],\n): Set {\n const descendants = new Set();\n const queue = [nodeId];\n\n // Build a quick children lookup\n const childrenOf = new Map();\n for (const node of nodes) {\n if (node.parentId) {\n const siblings = childrenOf.get(node.parentId);\n if (siblings) {\n siblings.push(node.id);\n } else {\n childrenOf.set(node.parentId, [node.id]);\n }\n }\n }\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n const children = childrenOf.get(current);\n if (children) {\n for (const childId of children) {\n if (!descendants.has(childId)) {\n descendants.add(childId);\n queue.push(childId);\n }\n }\n }\n }\n\n return descendants;\n}\n\n/**\n * Sort nodes so parents appear before children. Preserves sibling order.\n */\nexport function sortNodesTopological(nodes: FlowNode[]): FlowNode[] {\n const nodeMap = buildNodeMap(nodes);\n const sorted: FlowNode[] = [];\n const visited = new Set();\n\n function visit(node: FlowNode): void {\n if (visited.has(node.id)) {\n return;\n }\n // Ensure parent is visited first\n if (node.parentId) {\n const parent = nodeMap.get(node.parentId);\n if (parent) {\n visit(parent);\n }\n }\n visited.add(node.id);\n sorted.push(node);\n }\n\n for (const node of nodes) {\n visit(node);\n }\n\n return sorted;\n}\n\n/**\n * Compute the effective z-index for a node.\n *\n * Base layering is handled by CSS:\n * .flow-node-group { z-index: 0 } → .flow-edges { z-index: 1 } → .flow-node { z-index: 2 }\n *\n * This function is used by the directive to compute inline overrides for\n * child nodes in sub-flows and nodes with explicit zIndex. Returns null\n * when the CSS default is sufficient (regular root nodes without zIndex).\n *\n * - Group nodes (`type === 'group'`): 0 + (node.zIndex ?? 0) — below edge layer\n * - Regular root nodes: 2 + (node.zIndex ?? 0) — above edge layer\n * - Child nodes: parentComputedZ + 2 + (node.zIndex ?? 0) — above edge layer & parent\n */\nexport function computeZIndex(\n node: FlowNode,\n nodeMap: Map,\n visited: Set = new Set(),\n): number {\n if (visited.has(node.id)) {\n return node.zIndex ?? 2;\n }\n visited.add(node.id);\n\n if (!node.parentId) {\n if (node.zIndex !== undefined) {\n return node.zIndex;\n }\n return node.type === 'group' ? 0 : 2;\n }\n\n const parent = nodeMap.get(node.parentId);\n if (!parent) {\n return (node.zIndex ?? 0) + 2;\n }\n\n const parentZ = computeZIndex(parent, nodeMap, visited);\n return parentZ + 2 + (node.zIndex ?? 0);\n}\n\n/**\n * Clamp a position within a coordinate extent [[minX, minY], [maxX, maxY]].\n * Subtracts node dimensions from max so the entire node stays in bounds.\n */\nexport function clampToExtent(\n position: XYPosition,\n extent: CoordinateExtent,\n dimensions?: Dimensions,\n): XYPosition {\n return {\n x: Math.max(extent[0][0], Math.min(position.x, extent[1][0] - (dimensions?.width ?? 0))),\n y: Math.max(extent[0][1], Math.min(position.y, extent[1][1] - (dimensions?.height ?? 0))),\n };\n}\n\n/**\n * Clamp a relative position so the child stays within parent bounds.\n */\nexport function clampToParent(\n relPos: XYPosition,\n childDims: Dimensions,\n parentDims: Dimensions,\n): XYPosition {\n return {\n x: Math.max(0, Math.min(relPos.x, parentDims.width - childDims.width)),\n y: Math.max(0, Math.min(relPos.y, parentDims.height - childDims.height)),\n };\n}\n\n/**\n * Return expanded parent dimensions if the child exceeds the parent bounds.\n * Grows right/bottom with padding. Returns null if no expansion needed.\n */\n/**\n * Clamp a root-level node's position within its applicable extent.\n * Skips nodes with parentId or 'parent' extent (those use clampToParent).\n */\nexport function clampRootNodePosition(\n position: XYPosition,\n node: FlowNode,\n globalExtent?: CoordinateExtent | 'parent',\n): XYPosition {\n const extent = node.extent ?? globalExtent;\n if (!extent || extent === 'parent' || node.parentId) return position;\n const dims = node.dimensions ?? { width: DEFAULT_NODE_WIDTH, height: DEFAULT_NODE_HEIGHT };\n return clampToExtent(position, extent as CoordinateExtent, dims);\n}\n\nexport function expandParentToFitChild(\n relPos: XYPosition,\n childDims: Dimensions,\n parentDims: Dimensions,\n): Dimensions | null {\n const neededWidth = relPos.x + childDims.width + EXPAND_PADDING;\n const neededHeight = relPos.y + childDims.height + EXPAND_PADDING;\n\n const newWidth = Math.max(parentDims.width, neededWidth);\n const newHeight = Math.max(parentDims.height, neededHeight);\n\n if (newWidth === parentDims.width && newHeight === parentDims.height) {\n return null;\n }\n\n return { width: newWidth, height: newHeight };\n}\n","// ============================================================================\n// Built-in Shape Registry\n//\n// Each shape provides a perimeterPoint(w, h, position) function that returns\n// the { x, y } coordinate (relative to the node's top-left origin) where an\n// edge handle should connect for the given HandlePosition.\n//\n// Custom shapes registered via shapeTypes config use the same interface.\n// ============================================================================\n\nimport type { HandlePosition, ShapeDefinition } from './types';\n\n// ── Helpers ─────────────────────────────────────────────────────────\n\n/** Rectangular fallback — same as the default getHandleCoords logic. */\nfunction rectPerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n switch (position) {\n case 'top': return { x: w / 2, y: 0 };\n case 'right': return { x: w, y: h / 2 };\n case 'bottom': return { x: w / 2, y: h };\n case 'left': return { x: 0, y: h / 2 };\n case 'top-left': return { x: 0, y: 0 };\n case 'top-right': return { x: w, y: 0 };\n case 'bottom-left': return { x: 0, y: h };\n case 'bottom-right': return { x: w, y: h };\n }\n}\n\n// ── Circle / Ellipse ────────────────────────────────────────────────\n\nfunction circlePerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n const cx = w / 2;\n const cy = h / 2;\n const rx = w / 2;\n const ry = h / 2;\n\n switch (position) {\n case 'top': return { x: cx, y: 0 };\n case 'right': return { x: w, y: cy };\n case 'bottom': return { x: cx, y: h };\n case 'left': return { x: 0, y: cy };\n case 'top-right': {\n const a = -Math.PI / 4;\n return { x: cx + rx * Math.cos(a), y: cy + ry * Math.sin(a) };\n }\n case 'top-left': {\n const a = -3 * Math.PI / 4;\n return { x: cx + rx * Math.cos(a), y: cy + ry * Math.sin(a) };\n }\n case 'bottom-right': {\n const a = Math.PI / 4;\n return { x: cx + rx * Math.cos(a), y: cy + ry * Math.sin(a) };\n }\n case 'bottom-left': {\n const a = 3 * Math.PI / 4;\n return { x: cx + rx * Math.cos(a), y: cy + ry * Math.sin(a) };\n }\n }\n}\n\n// ── Diamond ─────────────────────────────────────────────────────────\n\nfunction diamondPerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n switch (position) {\n case 'top': return { x: w / 2, y: 0 };\n case 'right': return { x: w, y: h / 2 };\n case 'bottom': return { x: w / 2, y: h };\n case 'left': return { x: 0, y: h / 2 };\n case 'top-right': return { x: w * 0.75, y: h * 0.25 };\n case 'top-left': return { x: w * 0.25, y: h * 0.25 };\n case 'bottom-right': return { x: w * 0.75, y: h * 0.75 };\n case 'bottom-left': return { x: w * 0.25, y: h * 0.75 };\n }\n}\n\n// ── Hexagon ─────────────────────────────────────────────────────────\n\nfunction hexagonPerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n switch (position) {\n case 'top': return { x: w / 2, y: 0 };\n case 'right': return { x: w, y: h / 2 };\n case 'bottom': return { x: w / 2, y: h };\n case 'left': return { x: 0, y: h / 2 };\n case 'top-right': return { x: w * 0.75, y: 0 };\n case 'top-left': return { x: w * 0.25, y: 0 };\n case 'bottom-right': return { x: w * 0.75, y: h };\n case 'bottom-left': return { x: w * 0.25, y: h };\n }\n}\n\n// ── Parallelogram ───────────────────────────────────────────────────\n\nfunction parallelogramPerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n const skew = w * 0.15;\n switch (position) {\n case 'top': return { x: w * 0.575, y: 0 };\n case 'right': return { x: w * 0.925, y: h / 2 };\n case 'bottom': return { x: w * 0.425, y: h };\n case 'left': return { x: w * 0.075, y: h / 2 };\n case 'top-right': return { x: w, y: 0 };\n case 'top-left': return { x: skew, y: 0 };\n case 'bottom-right': return { x: w - skew, y: h };\n case 'bottom-left': return { x: 0, y: h };\n }\n}\n\n// ── Triangle ────────────────────────────────────────────────────────\n\nfunction trianglePerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n switch (position) {\n case 'top': return { x: w / 2, y: 0 };\n case 'right': return { x: w * 0.75, y: h / 2 };\n case 'bottom': return { x: w / 2, y: h };\n case 'left': return { x: w * 0.25, y: h / 2 };\n case 'top-right': return { x: w * 0.625, y: h * 0.25 };\n case 'top-left': return { x: w * 0.375, y: h * 0.25 };\n case 'bottom-right': return { x: w, y: h };\n case 'bottom-left': return { x: 0, y: h };\n }\n}\n\n// ── Cylinder ────────────────────────────────────────────────────────\n\nfunction cylinderPerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n const capH = h * 0.12;\n switch (position) {\n case 'top': return { x: w / 2, y: capH };\n case 'right': return { x: w, y: h / 2 };\n case 'bottom': return { x: w / 2, y: h - capH };\n case 'left': return { x: 0, y: h / 2 };\n case 'top-right': return { x: w, y: capH };\n case 'top-left': return { x: 0, y: capH };\n case 'bottom-right': return { x: w, y: h - capH };\n case 'bottom-left': return { x: 0, y: h - capH };\n }\n}\n\n// ── Stadium (Pill) ──────────────────────────────────────────────────\n\nfunction stadiumPerimeter(w: number, h: number, position: HandlePosition): { x: number; y: number } {\n const r = Math.min(w, h) / 2;\n const cx = w / 2;\n const cy = h / 2;\n\n switch (position) {\n case 'top': return { x: cx, y: 0 };\n case 'right': return { x: w, y: cy };\n case 'bottom': return { x: cx, y: h };\n case 'left': return { x: 0, y: cy };\n case 'top-right': {\n const capCx = w - r;\n const a = -Math.PI / 4;\n return { x: capCx + r * Math.cos(a), y: cy + r * Math.sin(a) };\n }\n case 'top-left': {\n const capCx = r;\n const a = -3 * Math.PI / 4;\n return { x: capCx + r * Math.cos(a), y: cy + r * Math.sin(a) };\n }\n case 'bottom-right': {\n const capCx = w - r;\n const a = Math.PI / 4;\n return { x: capCx + r * Math.cos(a), y: cy + r * Math.sin(a) };\n }\n case 'bottom-left': {\n const capCx = r;\n const a = 3 * Math.PI / 4;\n return { x: capCx + r * Math.cos(a), y: cy + r * Math.sin(a) };\n }\n }\n}\n\n// ── Registry ────────────────────────────────────────────────────────\n\n/** Built-in shape definitions, keyed by NodeShape name. */\nexport const builtinShapes: Record = {\n circle: { perimeterPoint: circlePerimeter },\n diamond: { perimeterPoint: diamondPerimeter },\n hexagon: { perimeterPoint: hexagonPerimeter },\n parallelogram: { perimeterPoint: parallelogramPerimeter },\n triangle: { perimeterPoint: trianglePerimeter },\n cylinder: { perimeterPoint: cylinderPerimeter },\n stadium: { perimeterPoint: stadiumPerimeter },\n};\n\n/** Rectangular fallback for nodes without a shape or unknown shapes. */\nexport { rectPerimeter };\n","// ============================================================================\n// Color Mode Utility\n//\n// Per-instance light/dark/system color mode management. Adds/removes the\n// `.dark` class on the flow container element, working with the existing\n// dark-mode CSS selectors in theme-default.css.\n// ============================================================================\n\nexport type ColorMode = 'light' | 'dark' | 'system';\n\nexport interface ColorModeHandle {\n /** Current resolved mode ('light' | 'dark'). */\n readonly resolved: 'light' | 'dark';\n /** Update the color mode. */\n update(mode: ColorMode): void;\n /** Clean up matchMedia listener. */\n destroy(): void;\n}\n\n/** Create a color mode controller for a flow container element. */\nexport function createColorMode(\n container: HTMLElement,\n initial: ColorMode = 'light',\n): ColorModeHandle {\n let resolved: 'light' | 'dark' = initial === 'dark' ? 'dark' : 'light';\n let mediaQuery: MediaQueryList | null = null;\n let listener: ((e: MediaQueryListEvent) => void) | null = null;\n\n function apply(isDark: boolean): void {\n resolved = isDark ? 'dark' : 'light';\n container.classList.toggle('dark', isDark);\n }\n\n function update(mode: ColorMode): void {\n // Clean up previous system listener\n if (mediaQuery && listener) {\n mediaQuery.removeEventListener('change', listener);\n mediaQuery = null;\n listener = null;\n }\n\n if (mode === 'system') {\n mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n apply(mediaQuery.matches);\n listener = (e) => apply(e.matches);\n mediaQuery.addEventListener('change', listener);\n } else {\n apply(mode === 'dark');\n }\n }\n\n // Initialize\n update(initial);\n\n return {\n get resolved() {\n return resolved;\n },\n update,\n destroy() {\n if (mediaQuery && listener) {\n mediaQuery.removeEventListener('change', listener);\n }\n container.classList.remove('dark');\n },\n };\n}\n","/**\n * Global store for collaboration instances.\n *\n * Collab objects (Yjs Doc, CollabBridge, CollabAwareness) contain circular\n * references that crash Alpine's deep-reactive proxy walker. Instead of\n * attaching them to the Alpine data object, they live here in a WeakMap\n * keyed by the canvas container element. Both flow-canvas.ts and the\n * x-flow-cursors directive import from this module.\n *\n * Uses globalThis to ensure a single shared instance across bundles —\n * when core loads from one bundle and the collab addon from another,\n * both must read/write the same WeakMap.\n */\nimport type { CollabBridge } from './bridge';\nimport type { CollabAwareness } from './awareness';\n\nexport interface CollabEntry {\n bridge: CollabBridge;\n awareness: CollabAwareness;\n doc: any;\n cursorCleanup?: () => void;\n}\n\nconst COLLAB_STORE_KEY = '__alpineflow_collab_store__';\n\nfunction getCollabStore(): WeakMap {\n if (typeof globalThis !== 'undefined') {\n if (!(globalThis as any)[COLLAB_STORE_KEY]) {\n (globalThis as any)[COLLAB_STORE_KEY] = new WeakMap();\n }\n return (globalThis as any)[COLLAB_STORE_KEY];\n }\n return new WeakMap();\n}\n\nexport const collabStore = getCollabStore();\n","/**\n * Addon registry — singleton via window global.\n *\n * When AlpineFlow core is loaded from one bundle (e.g., WireFlow's\n * alpineflow.bundle.esm.js) and addons are loaded from separate ESM\n * files, each bundle gets its own module scope. A module-scoped Map\n * would create disconnected registries — addons would register on\n * their copy, but the core would check its own empty copy.\n *\n * Using a window global ensures all bundles share one registry,\n * regardless of how they're loaded (CDN, npm, Vite, WireFlow bundle).\n */\n\nconst REGISTRY_KEY = '__alpineflow_registry__';\n\nexport function getRegistry(): Map {\n if (typeof globalThis !== 'undefined') {\n if (!(globalThis as any)[REGISTRY_KEY]) {\n (globalThis as any)[REGISTRY_KEY] = new Map();\n }\n return (globalThis as any)[REGISTRY_KEY];\n }\n // Fallback for environments without globalThis (shouldn't happen in modern runtimes)\n return new Map();\n}\n\nexport function registerAddon(name: string, value: any): void {\n getRegistry().set(name, value);\n}\n\nexport function getAddon(name: string): T | undefined {\n return getRegistry().get(name);\n}\n\n/** Remove a single addon by name. */\nexport function unregisterAddon(name: string): void {\n getRegistry().delete(name);\n}\n\n/** Test-only: clear all registered addons. */\nexport function _resetRegistry(): void {\n getRegistry().clear();\n}\n","/**\n * ARIA live region announcer for screen readers.\n *\n * Creates a visually-hidden div with aria-live=\"polite\" that announces\n * flow diagram changes. Hooks into _emit() via handleEvent().\n */\n\nexport type FormatAnnouncementFn = (event: string, detail: Record) => string | null;\n\n/**\n * Default event-to-announcement formatter.\n * Returns null for events that should not be announced.\n */\nexport function defaultFormatAnnouncement(event: string, detail: Record): string | null {\n switch (event) {\n case 'nodes-change': {\n const nodes = detail.nodes ?? [];\n const label = nodes.length === 1 ? (nodes[0].data?.label || nodes[0].id) : null;\n if (detail.type === 'add') {\n return label ? `Added node: ${label}` : `Added ${nodes.length} nodes`;\n }\n if (detail.type === 'remove') {\n return label ? `Removed node: ${label}` : `Removed ${nodes.length} nodes`;\n }\n return null;\n }\n\n case 'edges-change': {\n const edges = detail.edges ?? [];\n if (detail.type === 'add') {\n if (edges.length === 1) {\n return `Connected ${edges[0].source} to ${edges[0].target}`;\n }\n return `Added ${edges.length} connections`;\n }\n if (detail.type === 'remove') {\n if (edges.length === 1 && edges[0].source && edges[0].target) {\n return `Removed connection from ${edges[0].source} to ${edges[0].target}`;\n }\n return `Removed ${edges.length} connections`;\n }\n return null;\n }\n\n case 'selection-change': {\n const nodeCount = detail.nodes?.length ?? 0;\n const edgeCount = detail.edges?.length ?? 0;\n if (nodeCount === 0 && edgeCount === 0) {\n return 'Selection cleared';\n }\n const parts: string[] = [];\n if (nodeCount > 0) parts.push(`${nodeCount} node${nodeCount === 1 ? '' : 's'}`);\n if (edgeCount > 0) parts.push(`${edgeCount} edge${edgeCount === 1 ? '' : 's'}`);\n return `${parts.join(' and ')} selected`;\n }\n\n case 'viewport-move-end': {\n const zoom = detail.viewport?.zoom ?? 1;\n return `Viewport: zoom ${Math.round(zoom * 100)}%`;\n }\n\n case 'fit-view':\n return 'Fitted view to content';\n\n case 'node-reparent': {\n const name = detail.node?.data?.label || detail.node?.id || 'node';\n if (detail.newParentId) {\n return `Moved ${name} into ${detail.newParentId}`;\n }\n return `Detached ${name} from ${detail.oldParentId}`;\n }\n\n default:\n return null;\n }\n}\n\nconst CLEAR_DELAY = 1000;\n\nexport class FlowAnnouncer {\n private _el: HTMLDivElement;\n private _clearTimer: ReturnType | null = null;\n private _formatMessage: FormatAnnouncementFn;\n\n constructor(container: HTMLElement, formatMessage?: FormatAnnouncementFn) {\n this._formatMessage = formatMessage ?? defaultFormatAnnouncement;\n\n this._el = document.createElement('div');\n this._el.setAttribute('aria-live', 'polite');\n this._el.setAttribute('aria-atomic', 'true');\n this._el.setAttribute('role', 'status');\n\n // Visually hidden but accessible to screen readers\n const s = this._el.style;\n s.position = 'absolute';\n s.width = '1px';\n s.height = '1px';\n s.padding = '0';\n s.margin = '-1px';\n s.overflow = 'hidden';\n s.clip = 'rect(0,0,0,0)';\n s.whiteSpace = 'nowrap';\n s.border = '0';\n\n container.appendChild(this._el);\n }\n\n announce(message: string): void {\n if (this._clearTimer) clearTimeout(this._clearTimer);\n this._el.textContent = message;\n this._clearTimer = setTimeout(() => {\n this._el.textContent = '';\n this._clearTimer = null;\n }, CLEAR_DELAY);\n }\n\n handleEvent(event: string, detail: Record): void {\n const message = this._formatMessage(event, detail);\n if (message) this.announce(message);\n }\n\n destroy(): void {\n if (this._clearTimer) clearTimeout(this._clearTimer);\n this._el.remove();\n }\n}\n","/**\n * Compute engine for node-to-node data propagation.\n *\n * Nodes register compute functions by type. The engine runs in topological\n * order, routing outputs through edges via handle port names.\n */\n\nimport type { FlowNode, FlowEdge } from './types';\n\nexport interface ComputeDefinition {\n /** Compute function. Receives gathered inputs (keyed by target handle name)\n * and the node's own data object. Returns outputs keyed by output port name. */\n compute: (inputs: Record, nodeData: Record) => Record;\n}\n\nexport class ComputeEngine {\n private _registry = new Map();\n\n registerCompute(nodeType: string, definition: ComputeDefinition): void {\n this._registry.set(nodeType, definition);\n }\n\n hasCompute(nodeType: string): boolean {\n return this._registry.has(nodeType);\n }\n\n /**\n * Kahn's algorithm topological sort. Skips back-edges in cycles\n * by appending remaining nodes at the end.\n */\n topologicalSort(nodes: FlowNode[], edges: FlowEdge[]): FlowNode[] {\n const nodeMap = new Map(nodes.map(n => [n.id, n]));\n const inDegree = new Map();\n const adj = new Map();\n\n for (const n of nodes) {\n inDegree.set(n.id, 0);\n adj.set(n.id, []);\n }\n\n for (const e of edges) {\n if (!nodeMap.has(e.source) || !nodeMap.has(e.target)) continue;\n inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);\n adj.get(e.source)!.push(e.target);\n }\n\n const queue: string[] = [];\n for (const [id, deg] of inDegree) {\n if (deg === 0) queue.push(id);\n }\n\n const sorted: FlowNode[] = [];\n while (queue.length > 0) {\n const id = queue.shift()!;\n sorted.push(nodeMap.get(id)!);\n for (const neighbor of adj.get(id) ?? []) {\n const newDeg = (inDegree.get(neighbor) ?? 0) - 1;\n inDegree.set(neighbor, newDeg);\n if (newDeg === 0) queue.push(neighbor);\n }\n }\n\n // Append any remaining nodes (part of cycles)\n if (sorted.length < nodes.length) {\n const sortedIds = new Set(sorted.map(n => n.id));\n for (const n of nodes) {\n if (!sortedIds.has(n.id)) sorted.push(n);\n }\n }\n\n return sorted;\n }\n\n /**\n * Run compute propagation.\n *\n * @param nodes All nodes in the graph\n * @param edges All edges in the graph\n * @param startNodeId If provided, only compute this node and its downstream descendants\n * @returns Map of nodeId → output data for nodes that had a registered compute function\n */\n compute(\n nodes: FlowNode[],\n edges: FlowEdge[],\n startNodeId?: string,\n ): Map> {\n const sorted = this.topologicalSort(nodes, edges);\n\n // Build outputs map — seed with existing $outputs for upstream nodes\n // when doing partial computation\n const outputs = new Map>();\n if (startNodeId) {\n for (const n of nodes) {\n if (n.data.$outputs) {\n outputs.set(n.id, n.data.$outputs);\n }\n }\n }\n\n // Determine which nodes to compute\n let computeSet: Set | null = null;\n if (startNodeId) {\n computeSet = this._getDownstream(startNodeId, edges);\n computeSet.add(startNodeId);\n }\n\n const results = new Map>();\n\n for (const node of sorted) {\n if (computeSet && !computeSet.has(node.id)) continue;\n\n const def = this._registry.get(node.type ?? 'default');\n if (!def) continue;\n\n // Gather inputs from upstream outputs via edges\n const inputs: Record = {};\n const incomingEdges = edges.filter(e => e.target === node.id);\n for (const e of incomingEdges) {\n const sourceOutputs = outputs.get(e.source);\n if (!sourceOutputs) continue;\n const sourcePort = e.sourceHandle ?? 'default';\n const targetPort = e.targetHandle ?? 'default';\n if (sourcePort in sourceOutputs) {\n inputs[targetPort] = sourceOutputs[sourcePort];\n }\n }\n\n const result = def.compute(inputs, node.data);\n outputs.set(node.id, result);\n results.set(node.id, result);\n\n // Write to node data for reactive binding\n node.data.$inputs = inputs;\n node.data.$outputs = result;\n }\n\n return results;\n }\n\n /** Get all downstream node IDs reachable from a start node. */\n private _getDownstream(startId: string, edges: FlowEdge[]): Set {\n const adj = new Map();\n for (const e of edges) {\n let list = adj.get(e.source);\n if (!list) { list = []; adj.set(e.source, list); }\n list.push(e.target);\n }\n\n const visited = new Set();\n const stack = [startId];\n while (stack.length > 0) {\n const id = stack.pop()!;\n if (visited.has(id)) continue;\n visited.add(id);\n for (const neighbor of adj.get(id) ?? []) {\n if (!visited.has(neighbor)) stack.push(neighbor);\n }\n }\n visited.delete(startId);\n return visited;\n }\n}\n","/**\n * Wire Bridge — Livewire <-> AlpineFlow integration.\n *\n * Dynamically imported by flowCanvas when $wire is detected.\n * Maps AlpineFlow events to $wire method calls (client->server)\n * and Livewire dispatches to canvas methods (server->client).\n */\n\n/**\n * Maps event names to functions that extract positional arguments\n * from the event detail object. These args are spread into $wire.method(...args).\n */\nexport const WIRE_PAYLOAD_MAP: Record any[]> = {\n 'connect': (d) => [d.connection?.source ?? d.source, d.connection?.target ?? d.target, d.connection?.sourceHandle ?? d.sourceHandle, d.connection?.targetHandle ?? d.targetHandle],\n 'connect-start': (d) => [d.source, d.sourceHandle],\n 'connect-end': (d) => [d.connection, d.source, d.sourceHandle, d.position],\n 'node-click': (d) => [d.node.id, d.node],\n 'node-drag-start': (d) => [d.node.id],\n 'node-drag-end': (d) => [d.node.id, d.position],\n 'node-resize-start': (d) => [d.node.id, d.dimensions],\n 'node-resize-end': (d) => [d.node.id, d.dimensions],\n 'node-collapse': (d) => [d.node.id, d.descendants],\n 'node-expand': (d) => [d.node.id, d.descendants],\n 'node-reparent': (d) => [d.node.id, d.oldParentId, d.newParentId],\n 'node-context-menu': (d) => [d.node.id, { x: d.event.clientX, y: d.event.clientY }],\n 'nodes-change': (d) => [d],\n 'edge-click': (d) => [d.edge.id],\n 'edge-context-menu': (d) => [d.edge.id, { x: d.event.clientX, y: d.event.clientY }],\n 'edges-change': (d) => [d],\n 'reconnect-start': (d) => [d.edge.id, d.handleType],\n 'reconnect': (d) => [d.oldEdge.id, d.newConnection],\n 'reconnect-end': (d) => [d.edge.id, d.successful],\n 'pane-click': (d) => [d.position],\n 'pane-context-menu': (d) => [d.position],\n 'viewport-change': (d) => [d.viewport],\n 'selection-change': (d) => [d.nodes, d.edges],\n 'selection-context-menu': (d) => [d.nodes, d.edges, { x: d.event.clientX, y: d.event.clientY }],\n 'drop': (d) => [d.data, d.position],\n 'init': () => [],\n 'row-select': (d) => [d.rowId, d.nodeId, d.attrId],\n 'row-deselect': (d) => [d.rowId, d.nodeId, d.attrId],\n 'row-selection-change': (d) => [d.selectedRows],\n};\n\n/**\n * Maps Livewire dispatch event names to flowCanvas method names.\n * All commands are auto-registered when $wire is detected.\n */\nexport const WIRE_COMMAND_MAP: Record = {\n 'flow:addNodes': 'addNodes',\n 'flow:removeNodes': 'removeNodes',\n 'flow:addEdges': 'addEdges',\n 'flow:removeEdges': 'removeEdges',\n 'flow:update': 'update',\n 'flow:animate': 'animate',\n // Particle emission — all five firing methods\n 'flow:sendParticle': 'sendParticle',\n 'flow:sendParticleAlongPath': 'sendParticleAlongPath',\n 'flow:sendParticleBetween': 'sendParticleBetween',\n 'flow:sendParticleBurst': 'sendParticleBurst',\n 'flow:sendConverging': 'sendConverging',\n // Tag-filtered bulk animation control (v0.2.0-alpha)\n 'flow:cancelAll': 'cancelAll',\n 'flow:pauseAll': 'pauseAll',\n 'flow:resumeAll': 'resumeAll',\n // Viewport\n 'flow:fitView': 'fitView',\n 'flow:zoomIn': 'zoomIn',\n 'flow:zoomOut': 'zoomOut',\n 'flow:setCenter': 'setCenter',\n 'flow:setViewport': 'setViewport',\n 'flow:follow': 'follow',\n 'flow:unfollow': 'unfollow',\n 'flow:undo': 'undo',\n 'flow:redo': 'redo',\n 'flow:layout': 'layout',\n 'flow:fromObject': 'fromObject',\n 'flow:setLoading': 'setLoading',\n 'flow:clear': '$clear',\n 'flow:toggleInteractive': 'toggleInteractive',\n 'flow:panBy': 'panBy',\n 'flow:fitBounds': 'fitBounds',\n 'flow:patchConfig': 'patchConfig',\n 'flow:deselectAll': 'deselectAll',\n 'flow:collapseNode': 'collapseNode',\n 'flow:expandNode': 'expandNode',\n 'flow:toggleNode': 'toggleNode',\n // RunState (D2)\n 'flow:setNodeState': 'setNodeState',\n 'flow:resetStates': 'resetStates',\n};\n\n/**\n * Parameter extraction for commands that need special handling.\n * Maps Livewire dispatch params (named args object) to positional args for canvas methods.\n */\nconst WIRE_COMMAND_ARGS: Record any[]> = {\n 'flow:addNodes': (p) => [p.nodes],\n 'flow:removeNodes': (p) => [p.ids],\n 'flow:addEdges': (p) => [p.edges],\n 'flow:removeEdges': (p) => [p.ids],\n 'flow:update': (p) => [p.targets, p.options ?? {}],\n 'flow:animate': (p) => [p.targets, p.options ?? {}],\n // Particle emission — all five firing methods\n 'flow:sendParticle': (p) => [p.edgeId, p.options ?? {}],\n 'flow:sendParticleAlongPath': (p) => [p.path, p.options ?? {}],\n 'flow:sendParticleBetween': (p) => [p.source, p.target, p.options ?? {}],\n 'flow:sendParticleBurst': (p) => [p.edgeId, p.options ?? {}],\n 'flow:sendConverging': (p) => [p.sources, p.options ?? {}],\n // Tag-filtered bulk animation control\n 'flow:cancelAll': (p) => [p.filter ?? {}, p.options ?? {}],\n 'flow:pauseAll': (p) => [p.filter ?? {}],\n 'flow:resumeAll': (p) => [p.filter ?? {}],\n // Viewport\n 'flow:fitView': () => [],\n 'flow:zoomIn': () => [],\n 'flow:zoomOut': () => [],\n 'flow:setCenter': (p) => [p.x, p.y, p.zoom],\n 'flow:setViewport': (p) => [p.viewport],\n 'flow:follow': (p) => [p.nodeId, p.options ?? {}],\n 'flow:unfollow': () => [],\n 'flow:undo': () => [],\n 'flow:redo': () => [],\n 'flow:layout': (p) => [p.options ?? {}],\n 'flow:fromObject': (p) => [p.data],\n 'flow:setLoading': (p) => [p.loading],\n 'flow:clear': () => [],\n 'flow:toggleInteractive': () => [],\n 'flow:panBy': (p) => [p.dx, p.dy],\n 'flow:fitBounds': (p) => [p.rect, p.options],\n 'flow:patchConfig': (p) => [p.changes],\n 'flow:deselectAll': () => [],\n 'flow:collapseNode': (p) => [p.id],\n 'flow:expandNode': (p) => [p.id],\n 'flow:toggleNode': (p) => [p.id],\n // RunState (D2)\n 'flow:setNodeState': (p) => [p.ids, p.state],\n 'flow:resetStates': () => [],\n};\n\n// ── Highlight presets ────────────────────────────────────────────────────────\n\nconst HIGHLIGHT_PRESETS: Record = {\n success: { borderColor: '#22c55e', shadow: '0 0 0 2px rgba(34,197,94,0.3)' },\n error: { borderColor: '#ef4444', shadow: '0 0 0 2px rgba(239,68,68,0.3)' },\n warning: { borderColor: '#f59e0b', shadow: '0 0 0 2px rgba(245,158,11,0.3)' },\n info: { borderColor: '#3b82f6', shadow: '0 0 0 2px rgba(59,130,246,0.3)' },\n};\n\n// ── Custom command handlers (composite operations) ──────────────────────────\n\n/**\n * Register convenience commands that compose multiple canvas operations.\n * Returns a cleanup function.\n */\nexport function registerCustomWireCommands(canvas: any, $wire: any): () => void {\n const cleanups: (Function | undefined)[] = [];\n\n // flow:moveNode — move a single node with optional animation\n cleanups.push($wire.on('flow:moveNode', (p: any) => {\n const duration = p.duration ?? 0;\n canvas.update(\n { nodes: { [p.id]: { position: { x: p.x, y: p.y } } } },\n { duration },\n );\n }));\n\n // flow:updateNode — update arbitrary node properties\n cleanups.push($wire.on('flow:updateNode', (p: any) => {\n const duration = p.duration ?? 0;\n canvas.update(\n { nodes: { [p.id]: p.changes } },\n { duration },\n );\n }));\n\n // flow:focusNode — pan/zoom to center on a single node\n cleanups.push($wire.on('flow:focusNode', (p: any) => {\n const node = canvas.getNode(p.id);\n if (!node) return;\n const w = node.dimensions?.width ?? 150;\n const h = node.dimensions?.height ?? 40;\n const pos = node.parentId ? canvas.getAbsolutePosition(p.id) : node.position;\n canvas.fitBounds(\n { x: pos.x, y: pos.y, width: w, height: h },\n { padding: p.padding ?? 0.5, duration: p.duration ?? 300 },\n );\n }));\n\n // flow:connect — create edge between two nodes with optional draw animation\n cleanups.push($wire.on('flow:connect', (p: any) => {\n const edgeId = p.edgeId ?? `e-${p.source}-${p.target}`;\n const edge = { id: edgeId, source: p.source, target: p.target, ...(p.options ?? {}) };\n if (p.duration && p.duration > 0) {\n canvas.timeline()\n .step({ addEdges: [edge], edgeTransition: 'draw', duration: p.duration })\n .play();\n } else {\n canvas.addEdges(edge);\n }\n }));\n\n // flow:disconnect — remove edge(s) between two nodes with optional fade\n cleanups.push($wire.on('flow:disconnect', (p: any) => {\n const edgeIds = canvas.edges\n .filter((e: any) => e.source === p.source && e.target === p.target)\n .map((e: any) => e.id);\n if (edgeIds.length === 0) return;\n if (p.duration && p.duration > 0) {\n canvas.timeline()\n .step({ removeEdges: edgeIds, edgeTransition: 'fade', duration: p.duration })\n .play();\n } else {\n canvas.removeEdges(edgeIds);\n }\n }));\n\n // flow:highlightNode — flash a preset style then revert\n cleanups.push($wire.on('flow:highlightNode', (p: any) => {\n const node = canvas.getNode(p.id);\n if (!node) return;\n const preset = HIGHLIGHT_PRESETS[p.style] ?? HIGHLIGHT_PRESETS.info;\n const duration = p.duration ?? 1500;\n const holdTime = Math.floor(duration * 0.6);\n const fadeTime = Math.floor(duration * 0.4);\n\n const originalBorder = node.style?.borderColor ?? null;\n const originalShadow = node.style?.boxShadow ?? null;\n\n // Apply highlight\n canvas.update({\n nodes: { [p.id]: { style: `border-color: ${preset.borderColor}; box-shadow: ${preset.shadow}` } },\n }, { duration: 100 });\n\n // Revert after hold\n setTimeout(() => {\n const revertStyle = originalBorder\n ? `border-color: ${originalBorder}; box-shadow: ${originalShadow ?? 'none'}`\n : '';\n canvas.update({\n nodes: { [p.id]: { style: revertStyle } },\n }, { duration: fadeTime });\n }, 100 + holdTime);\n }));\n\n // flow:highlightPath — fire particles along edges connecting a sequence of nodes.\n // All particle options pass through transparently so beam gradients,\n // followThrough, custom renderers, etc. all work from Livewire.\n cleanups.push($wire.on('flow:highlightPath', (p: any) => {\n const nodeIds: string[] = p.nodeIds;\n const options = p.options ?? {};\n const { delay: rawDelay, ...particleOptions } = options;\n const delay = rawDelay ?? 200;\n // Apply sensible defaults without clobbering user-provided values.\n const resolved = {\n color: '#3b82f6',\n size: 5,\n duration: '800ms',\n ...particleOptions,\n };\n\n for (let i = 0; i < nodeIds.length - 1; i++) {\n const source = nodeIds[i];\n const target = nodeIds[i + 1];\n const edge = canvas.edges.find((e: any) => e.source === source && e.target === target);\n if (edge) {\n setTimeout(() => {\n canvas.sendParticle(edge.id, resolved);\n }, i * delay);\n }\n }\n }));\n\n // flow:lockNode / flow:unlockNode — set locked state (freezes all interactions)\n cleanups.push($wire.on('flow:lockNode', (p: any) => {\n const node = canvas.getNode(p.id);\n if (node) node.locked = true;\n }));\n cleanups.push($wire.on('flow:unlockNode', (p: any) => {\n const node = canvas.getNode(p.id);\n if (node) node.locked = false;\n }));\n\n // flow:hideNode / flow:showNode — set hidden state\n cleanups.push($wire.on('flow:hideNode', (p: any) => {\n const node = canvas.getNode(p.id);\n if (node) node.hidden = true;\n }));\n cleanups.push($wire.on('flow:showNode', (p: any) => {\n const node = canvas.getNode(p.id);\n if (node) node.hidden = false;\n }));\n\n // flow:selectNodes — select specific nodes\n cleanups.push($wire.on('flow:selectNodes', (p: any) => {\n canvas.deselectAll();\n for (const id of p.ids) {\n canvas.selectedNodes.add(id);\n const node = canvas.getNode(id);\n if (node) node.selected = true;\n }\n }));\n\n // flow:run — invoke $flow.run() with server-provided startId + options.\n // Handlers (onEnter, pickBranch, etc.) must be pre-registered on the canvas\n // via x-init or the workflowHandlers config key, since JS callbacks can't\n // be serialized from PHP. The server provides startId, options (payload,\n // defaultDurationMs, particleOnEdges, particleOptions, muteUntakenBranches, etc.).\n cleanups.push($wire.on('flow:run', (p: any) => {\n if (typeof canvas.run !== 'function') {\n console.warn('[wire-bridge] flow:run: canvas.run not available — is the workflow addon registered?');\n return;\n }\n const handlers = canvas._workflowHandlers ?? {};\n canvas.run(p.startId, handlers, p.options ?? {});\n }));\n\n // flow:selectEdges — select specific edges\n cleanups.push($wire.on('flow:selectEdges', (p: any) => {\n canvas.deselectAll();\n for (const id of p.ids) {\n canvas.selectedEdges.add(id);\n const edge = canvas.getEdge(id);\n if (edge) edge.selected = true;\n }\n }));\n\n return () => {\n for (const cleanup of cleanups) {\n if (typeof cleanup === 'function') cleanup();\n }\n };\n}\n\n/**\n * Convert kebab-case event name to PascalCase config callback name.\n * 'node-drag-end' -> 'onNodeDragEnd'\n */\nfunction toCallbackName(event: string): string {\n return 'on' + event.split('-').map(\n (s: string) => s.charAt(0).toUpperCase() + s.slice(1),\n ).join('');\n}\n\n/**\n * Viewport events fire at animation-frame rate during zoom/pan; forwarding each\n * to Livewire is a network round-trip per frame. These are trailing-throttled to\n * one call per {@link WIRE_VIEWPORT_THROTTLE_MS} window (the final viewport wins).\n */\nconst THROTTLED_WIRE_EVENTS = new Set(['viewport-change', 'viewport-move']);\nconst WIRE_VIEWPORT_THROTTLE_MS = 150;\n\n/**\n * Trailing-edge throttle: the first call schedules a flush `wait` ms later; calls\n * in between update the pending args so the flush runs once with the latest.\n */\nfunction trailingThrottle void>(fn: T, wait: number): T {\n let timer: ReturnType | null = null;\n let lastArgs: Parameters | null = null;\n return ((...args: Parameters) => {\n lastArgs = args;\n if (timer !== null) return;\n timer = setTimeout(() => {\n timer = null;\n const a = lastArgs!;\n lastArgs = null;\n fn(...a);\n }, wait);\n }) as T;\n}\n\n/**\n * Register config callbacks that bridge AlpineFlow events to $wire method calls.\n * Modifies the config object in-place, adding onXxx callbacks.\n *\n * @param config - The flowCanvas config object (mutated)\n * @param $wire - Livewire's $wire proxy\n * @param wireEvents - Map of event names to Livewire method names\n */\nexport function registerWireEvents(\n config: any,\n $wire: any,\n wireEvents: Record,\n): void {\n for (const [event, method] of Object.entries(wireEvents)) {\n const callbackName = toCallbackName(event);\n const existingCallback = config[callbackName];\n\n const handler = (detail: any) => {\n // Call existing callback first if present, preserve return value\n let result: any;\n if (typeof existingCallback === 'function') {\n result = existingCallback(detail);\n }\n\n // Extract args using payload map, or fall back to full detail\n const extractor = WIRE_PAYLOAD_MAP[event];\n const args = extractor ? extractor(detail) : [detail];\n\n // Call the Livewire method\n const wireFn = $wire[method];\n if (typeof wireFn === 'function') {\n wireFn.call($wire, ...args);\n }\n\n return result;\n };\n\n // Viewport events fire per frame — trailing-throttle the Livewire bridge so\n // it makes at most one round-trip per window. (Throttled handlers run async,\n // so their return value is not observable — irrelevant for viewport events.)\n config[callbackName] = THROTTLED_WIRE_EVENTS.has(event)\n ? trailingThrottle(handler, WIRE_VIEWPORT_THROTTLE_MS)\n : handler;\n }\n}\n\n/**\n * Register $wire.on() listeners for all server->client commands.\n * Returns a cleanup function that deregisters all listeners.\n */\nexport function registerWireCommands(canvas: any, $wire: any): () => void {\n const cleanups: (Function | undefined)[] = [];\n\n for (const [dispatch, method] of Object.entries(WIRE_COMMAND_MAP)) {\n const cleanup = $wire.on(dispatch, (params: any) => {\n const fn = canvas[method];\n if (typeof fn !== 'function') return;\n\n const argsExtractor = WIRE_COMMAND_ARGS[dispatch];\n const args = argsExtractor ? argsExtractor(params) : Object.values(params);\n fn.call(canvas, ...args);\n });\n cleanups.push(cleanup);\n }\n\n return () => {\n for (const cleanup of cleanups) {\n if (typeof cleanup === 'function') cleanup();\n }\n };\n}\n","/**\n * Frame-aligned layout dedup with cross-frame loop safety net.\n *\n * Ensures `layoutFn(parentId)` runs at most once per parent per animation\n * frame. First call within a frame runs synchronously; subsequent calls for\n * the same parent no-op until the next RAF clears the frame marker.\n *\n * Also tracks consecutive-frame layouts per parent; if a parent is laid out\n * in >5 successive frames, emits a console.warn and suppresses further\n * auto-layout until `resetLoopCounter` is called (on user mutations).\n */\nconst CONSECUTIVE_FRAME_CAP = 5;\n\nexport interface LayoutDedup {\n /** Run layoutFn(parentId) unless already done this frame. */\n safeLayoutChildren(parentId: string): void;\n /** Call on user-initiated mutations to reset per-parent loop counters and clear suppression. */\n resetLoopCounter(parentId: string): void;\n /** Teardown — cancels any pending RAF. */\n dispose(): void;\n /** Pause reconciliation. Calls to safeLayoutChildren queue until matching resume(). */\n suspend(): void;\n /** Resume and run all queued parents once (outermost call only). */\n resume(): void;\n}\n\nexport function createLayoutDedup(layoutFn: (parentId: string) => void): LayoutDedup {\n const laidOutThisFrame = new Set();\n const consecutiveCounts = new Map();\n const suppressed = new Set();\n let rafHandle: number | null = null;\n let prevFrameParents = new Set();\n let suspendDepth = 0;\n const queuedWhileSuspended = new Set();\n\n function schedule(): void {\n if (rafHandle !== null) return;\n rafHandle = requestAnimationFrame(() => {\n rafHandle = null;\n\n // Update consecutive-frame counts for this frame's parents.\n for (const parentId of laidOutThisFrame) {\n const prevCount = prevFrameParents.has(parentId) ? (consecutiveCounts.get(parentId) ?? 0) : 0;\n const nextCount = prevCount + 1;\n consecutiveCounts.set(parentId, nextCount);\n if (nextCount > CONSECUTIVE_FRAME_CAP && !suppressed.has(parentId)) {\n suppressed.add(parentId);\n console.warn(\n `[alpineflow] Auto-layout for parent \"${parentId}\" has run for ${nextCount} consecutive frames. ` +\n `Suppressing to avoid an infinite loop. This usually indicates a layout that keeps changing ` +\n `child dimensions by more than the 1px threshold. Next user mutation will clear the suppression.`,\n );\n }\n }\n\n // Reset counts for parents that did NOT lay out this frame.\n for (const id of consecutiveCounts.keys()) {\n if (!laidOutThisFrame.has(id)) consecutiveCounts.set(id, 0);\n }\n\n prevFrameParents = new Set(laidOutThisFrame);\n laidOutThisFrame.clear();\n });\n }\n\n return {\n safeLayoutChildren(parentId) {\n if (suppressed.has(parentId)) return;\n if (suspendDepth > 0) {\n queuedWhileSuspended.add(parentId);\n return;\n }\n if (laidOutThisFrame.has(parentId)) return;\n laidOutThisFrame.add(parentId);\n schedule();\n layoutFn(parentId);\n },\n resetLoopCounter(parentId) {\n consecutiveCounts.delete(parentId);\n suppressed.delete(parentId);\n },\n dispose() {\n if (rafHandle !== null) {\n cancelAnimationFrame(rafHandle);\n rafHandle = null;\n }\n },\n suspend() {\n suspendDepth++;\n },\n resume() {\n if (suspendDepth === 0) return;\n suspendDepth--;\n if (suspendDepth === 0) {\n for (const parentId of queuedWhileSuspended) {\n if (suppressed.has(parentId) || laidOutThisFrame.has(parentId)) continue;\n laidOutThisFrame.add(parentId);\n schedule();\n layoutFn(parentId);\n }\n queuedWhileSuspended.clear();\n }\n },\n };\n}\n","import type { LayoutDedup } from './canvas-layout-dedup';\n\n/**\n * Returns a `batch(fn)` function that suspends layout reconciliation\n * during `fn` and runs it once after. Nested batches join the outermost —\n * inner `batch()` calls increment a depth counter and only the outermost\n * decrements to 0 and reconciles. Reconciliation runs via try/finally so\n * a thrown `fn` still reconciles before the error propagates. `fn`'s\n * return value is forwarded.\n */\nexport function createBatch(dedup: LayoutDedup): (fn: () => T) => T {\n return function batch(fn: () => T): T {\n dedup.suspend();\n try {\n return fn();\n } finally {\n dedup.resume();\n }\n };\n}\n","import type { Dimensions } from '../core/types';\n\n/**\n * Clamp observed dimensions to min/max bounds.\n *\n * Bounds are `Partial` — either field may be undefined, meaning\n * \"no constraint on that axis.\" Either bound object may itself be undefined.\n * Infinity is valid for unbounded upper.\n */\nexport function clampDimensions(\n observed: Dimensions,\n min: Partial | undefined,\n max: Partial | undefined,\n): Dimensions {\n let { width, height } = observed;\n if (min?.width !== undefined) width = Math.max(width, min.width);\n if (min?.height !== undefined) height = Math.max(height, min.height);\n if (max?.width !== undefined) width = Math.min(width, max.width);\n if (max?.height !== undefined) height = Math.min(height, max.height);\n return { width, height };\n}\n","// ============================================================================\n// Child Validation\n//\n// Pure validation functions for parent-child node constraints. No DOM or\n// Alpine dependencies — operates on FlowNode objects and ChildValidation rules.\n//\n// Rule resolution: type-registry defaults (childValidationRules config) are\n// shallow-merged with per-node overrides (node.data.childValidation).\n// ============================================================================\n\nimport type { FlowNode, ChildValidation, ChildValidationResult } from './types';\n\n/**\n * Resolve the effective ChildValidation rules for a parent node.\n * Shallow-merges type-registry defaults with per-node overrides.\n * Returns undefined if no rules apply.\n */\nexport function resolveChildValidation(\n parent: FlowNode,\n typeRules: Record,\n): ChildValidation | undefined {\n const nodeType = parent.type ?? 'default';\n const typeRule = typeRules[nodeType];\n const nodeRule = parent.data?.childValidation as ChildValidation | undefined;\n\n if (!typeRule && !nodeRule) return undefined;\n if (!typeRule) return nodeRule;\n if (!nodeRule) return typeRule;\n\n return { ...typeRule, ...nodeRule };\n}\n\n/**\n * Validate whether a child can be added to a parent.\n * `currentChildren` is the list of existing children (before adding).\n */\nexport function validateChildAdd(\n parent: FlowNode,\n child: FlowNode,\n currentChildren: FlowNode[],\n rules: ChildValidation | undefined,\n): ChildValidationResult {\n if (!rules) return { valid: true };\n\n // maxChildren\n if (rules.maxChildren !== undefined && currentChildren.length >= rules.maxChildren) {\n return {\n valid: false,\n rule: 'maxChildren',\n message: `Maximum ${rules.maxChildren} child node(s) allowed`,\n };\n }\n\n // allowedChildTypes\n if (rules.allowedChildTypes) {\n const childType = child.type ?? 'default';\n if (!rules.allowedChildTypes.includes(childType)) {\n return {\n valid: false,\n rule: 'allowedChildTypes',\n message: `Node type \"${childType}\" is not allowed in this group`,\n };\n }\n }\n\n // childTypeConstraints — per-type max\n if (rules.childTypeConstraints) {\n const childType = child.type ?? 'default';\n const constraint = rules.childTypeConstraints[childType];\n if (constraint?.max !== undefined) {\n const currentCount = currentChildren.filter(\n (n) => (n.type ?? 'default') === childType,\n ).length;\n if (currentCount >= constraint.max) {\n return {\n valid: false,\n rule: 'childTypeConstraints',\n message: `Maximum ${constraint.max} \"${childType}\" node(s) allowed`,\n };\n }\n }\n }\n\n // validateChild callback\n if (rules.validateChild) {\n const result = rules.validateChild(child, currentChildren);\n if (result !== true) {\n return {\n valid: false,\n rule: 'validateChild',\n message: typeof result === 'string' ? result : 'Custom validation rejected',\n };\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Validate whether a child can be removed from a parent.\n * `currentChildren` includes the child being removed.\n */\nexport function validateChildRemove(\n _parent: FlowNode,\n child: FlowNode,\n currentChildren: FlowNode[],\n rules: ChildValidation | undefined,\n): ChildValidationResult {\n if (!rules) return { valid: true };\n\n // preventChildEscape — always blocks removal via drag-out\n if (rules.preventChildEscape) {\n return {\n valid: false,\n rule: 'preventChildEscape',\n message: 'Children cannot be moved out of this group',\n };\n }\n\n const countAfter = currentChildren.length - 1;\n\n // minChildren / requiredChildren\n const effectiveMin = Math.max(\n rules.minChildren ?? 0,\n rules.requiredChildren ? 1 : 0,\n );\n if (effectiveMin > 0 && countAfter < effectiveMin) {\n return {\n valid: false,\n rule: 'minChildren',\n message: `Requires at least ${effectiveMin} child node(s)`,\n };\n }\n\n // childTypeConstraints — per-type min\n if (rules.childTypeConstraints) {\n const childType = child.type ?? 'default';\n const constraint = rules.childTypeConstraints[childType];\n if (constraint?.min !== undefined) {\n const currentTypeCount = currentChildren.filter(\n (n) => (n.type ?? 'default') === childType,\n ).length;\n if (currentTypeCount - 1 < constraint.min) {\n return {\n valid: false,\n rule: 'childTypeConstraints',\n message: `Requires at least ${constraint.min} \"${childType}\" node(s)`,\n };\n }\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Compute the current validation errors for a parent node.\n * Returns an empty array when the parent is valid.\n * Used for reactive state (not for blocking operations).\n */\nexport function computeValidationErrors(\n _parent: FlowNode,\n currentChildren: FlowNode[],\n rules: ChildValidation | undefined,\n): string[] {\n if (!rules) return [];\n\n const errors: string[] = [];\n\n // minChildren / requiredChildren\n const effectiveMin = Math.max(\n rules.minChildren ?? 0,\n rules.requiredChildren ? 1 : 0,\n );\n if (effectiveMin > 0 && currentChildren.length < effectiveMin) {\n errors.push(`Requires at least ${effectiveMin} child node(s)`);\n }\n\n // maxChildren\n if (rules.maxChildren !== undefined && currentChildren.length > rules.maxChildren) {\n errors.push(`Maximum ${rules.maxChildren} child node(s) allowed`);\n }\n\n // childTypeConstraints\n if (rules.childTypeConstraints) {\n for (const [type, constraint] of Object.entries(rules.childTypeConstraints)) {\n const count = currentChildren.filter(\n (n) => (n.type ?? 'default') === type,\n ).length;\n if (constraint.min !== undefined && count < constraint.min) {\n errors.push(`Requires at least ${constraint.min} \"${type}\" node(s)`);\n }\n if (constraint.max !== undefined && count > constraint.max) {\n errors.push(`Maximum ${constraint.max} \"${type}\" node(s) allowed`);\n }\n }\n }\n\n return errors;\n}\n","// ============================================================================\n// Intersection Detection & Collision Avoidance\n//\n// Pure geometry functions for node overlap detection and collision prevention.\n// ============================================================================\n\nimport type { FlowNode, Rect } from './types';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT, getNodeVisualPosition } from './geometry';\n\n/** Get the bounding rect of a node (uses measured or default dimensions). */\nexport function getNodeRect(\n node: FlowNode,\n globalOrigin?: [number, number],\n): Rect {\n const vp = getNodeVisualPosition(node, globalOrigin);\n return {\n x: vp.x,\n y: vp.y,\n width: node.dimensions?.width ?? DEFAULT_NODE_WIDTH,\n height: node.dimensions?.height ?? DEFAULT_NODE_HEIGHT,\n };\n}\n\n/** AABB overlap test between two rectangles. */\nexport function rectsIntersect(a: Rect, b: Rect): boolean {\n return (\n a.x < b.x + b.width &&\n a.x + a.width > b.x &&\n a.y < b.y + b.height &&\n a.y + a.height > b.y\n );\n}\n\n/**\n * Return all nodes whose bounding rect overlaps the given node.\n * `partially` (default true) — any overlap counts. If false, only full containment.\n */\nexport function getIntersectingNodes(\n node: FlowNode,\n candidates: FlowNode[],\n partially: boolean = true,\n): FlowNode[] {\n const rect = getNodeRect(node);\n return candidates.filter((c) => {\n if (c.id === node.id) return false;\n const cr = getNodeRect(c);\n if (partially) return rectsIntersect(rect, cr);\n // Full containment: rect fully contains cr\n return (\n rect.x <= cr.x &&\n rect.y <= cr.y &&\n rect.x + rect.width >= cr.x + cr.width &&\n rect.y + rect.height >= cr.y + cr.height\n );\n });\n}\n\n/**\n * Check if a specific node intersects with the given node.\n * `partially` (default true) — any overlap counts. If false, only full containment.\n */\nexport function isNodeIntersecting(\n node: FlowNode,\n target: FlowNode,\n partially: boolean = true,\n): boolean {\n if (node.id === target.id) return false;\n const rect = getNodeRect(node);\n const tr = getNodeRect(target);\n if (partially) return rectsIntersect(rect, tr);\n return (\n rect.x <= tr.x &&\n rect.y <= tr.y &&\n rect.x + rect.width >= tr.x + tr.width &&\n rect.y + rect.height >= tr.y + tr.height\n );\n}\n\n/**\n * Clamp a position to avoid overlap with other node rects.\n * Returns the nearest non-overlapping position (pushes out along shortest axis).\n *\n * Note: Uses a single pass — chain overlaps (A pushed into B pushed into C)\n * may not be fully resolved. This is intentional: a single pass is fast enough\n * for real-time drag interactions where the user repositions continuously.\n */\nexport function clampToAvoidOverlap(\n position: { x: number; y: number },\n nodeWidth: number,\n nodeHeight: number,\n otherRects: Rect[],\n gap: number = 5,\n): { x: number; y: number } {\n let { x, y } = position;\n\n for (const r of otherRects) {\n const nodeRight = x + nodeWidth;\n const nodeBottom = y + nodeHeight;\n const rRight = r.x + r.width;\n const rBottom = r.y + r.height;\n\n if (x < rRight + gap && nodeRight > r.x - gap && y < rBottom + gap && nodeBottom > r.y - gap) {\n // Overlapping — find shortest escape direction\n const pushLeft = nodeRight - (r.x - gap);\n const pushRight = rRight + gap - x;\n const pushUp = nodeBottom - (r.y - gap);\n const pushDown = rBottom + gap - y;\n const min = Math.min(pushLeft, pushRight, pushUp, pushDown);\n\n if (min === pushLeft) x -= pushLeft;\n else if (min === pushRight) x += pushRight;\n else if (min === pushUp) y -= pushUp;\n else y += pushDown;\n }\n }\n\n return { x, y };\n}\n","// ============================================================================\n// canvas-nodes — Node CRUD, graph queries, filtering & intersection mixin\n//\n// Twelve methods covering the full node lifecycle: add/remove with child\n// validation, graph traversal (outgoers/incomers/connected edges/connectivity),\n// node filtering, and bounding-rect intersection queries.\n//\n// Cross-mixin deps (optional chaining): ctx.layoutChildren\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowNode, FlowEdge, FlowNodeRunState, XYPosition } from '../../core/types';\nimport { debug } from '../../core/debug';\nimport { sortNodesTopological, getDescendantIds } from '../../core/sub-flow';\nimport {\n computeReconnectionEdges,\n getConnectedEdges as graphGetConnectedEdges,\n getOutgoers as graphGetOutgoers,\n getIncomers as graphGetIncomers,\n areNodesConnected as graphAreNodesConnected,\n} from '../../core/graph';\nimport {\n validateChildAdd,\n validateChildRemove,\n} from '../../core/child-validation';\nimport {\n getIntersectingNodes as coreGetIntersectingNodes,\n isNodeIntersecting as coreIsNodeIntersecting,\n} from '../../core/intersection';\nimport { collabStore } from '../../collab/store';\n\nexport function createNodesMixin(ctx: CanvasContext) {\n return {\n /**\n * Add one or more nodes to the canvas.\n *\n * - Normalizes single node or array input.\n * - When `options.center` is set, stashes intended positions off-screen\n * so the directive can measure dimensions without a visible flash,\n * then repositions after measurement via double-rAF.\n * - Validates child constraints before accepting each node.\n * - Captures history, sorts topologically, rebuilds node map.\n * - Pushes collab updates when a collaboration bridge is active.\n * - Runs child layout for any layout parents that received new children.\n * - Schedules auto-layout after the mutation.\n */\n addNodes(newNodes: FlowNode | FlowNode[], options?: { center?: boolean }): void {\n ctx._captureHistory();\n let arr = Array.isArray(newNodes) ? newNodes : [newNodes];\n debug('init', `Adding ${arr.length} node(s)`, arr.map((n) => n.id));\n\n // When center is requested, stash intended positions and place off-screen\n // so the directive can measure dimensions without a visible flash.\n const intendedPositions = new Map();\n if (options?.center) {\n for (const node of arr) {\n intendedPositions.set(node.id, { ...node.position });\n }\n arr = arr.map(n => ({ ...n, position: { x: -9999, y: -9999 } }));\n }\n\n // Child validation: check each batch node before adding\n const acceptedNodes: FlowNode[] = [];\n for (const node of arr) {\n if (node.parentId) {\n const rules = ctx._getChildValidation(node.parentId);\n if (rules) {\n const parent = ctx._nodeMap.get(node.parentId);\n if (parent) {\n // Count existing siblings + already-accepted batch siblings\n const siblings = [\n ...(ctx.nodes as FlowNode[]).filter(\n (n: FlowNode) => n.parentId === node.parentId,\n ),\n ...acceptedNodes.filter(\n (n: FlowNode) => n.parentId === node.parentId,\n ),\n ];\n const result = validateChildAdd(parent, node, siblings, rules);\n if (!result.valid) {\n if (ctx._config.onChildValidationFail) {\n ctx._config.onChildValidationFail({\n parent,\n child: node,\n operation: 'add',\n rule: result.rule!,\n message: result.message!,\n });\n }\n continue;\n }\n }\n }\n }\n acceptedNodes.push(node);\n }\n arr = acceptedNodes;\n ctx.nodes.push(...arr);\n\n // Capture configured dimensions before layout stretch modifies them\n for (const node of arr) {\n if (node.dimensions) {\n ctx._initialDimensions.set(node.id, { ...node.dimensions });\n }\n }\n\n // Re-sort only when the batch introduces a parent→child ordering constraint:\n // either a new node is a child (has parentId), or a new node is the parent of a node\n // already present (a child added earlier with a then-dangling parentId). Flat,\n // unrelated batches keep the array identity (no reallocation) so effects reading\n // `nodes` don't invalidate. The splice re-sorts in place, preserving identity even then.\n let needsTopologicalSort = arr.some((n) => n.parentId);\n if (!needsTopologicalSort) {\n const addedIds = new Set(arr.map((n) => n.id));\n needsTopologicalSort = (ctx.nodes as FlowNode[]).some(\n (n) => n.parentId && addedIds.has(n.parentId),\n );\n }\n if (needsTopologicalSort) {\n const sorted = sortNodesTopological(ctx.nodes);\n ctx.nodes.splice(0, ctx.nodes.length, ...sorted);\n }\n ctx._rebuildNodeMap();\n\n // A3: Install childLayout watchers for any newly added container nodes so\n // that mutations to their layout properties trigger re-layout automatically.\n // Look up each node via _nodeMap to get the Alpine reactive proxy (not the\n // original plain object) — Alpine.watch must receive the reactive version so\n // mutations go through the proxy and trigger watchers correctly.\n for (const node of arr) {\n if (node.childLayout) {\n const reactiveNode = ctx._nodeMap.get(node.id);\n if (reactiveNode) {\n ctx._installChildLayoutWatchers(reactiveNode);\n }\n }\n }\n\n ctx._emit('nodes-change', { type: 'add', nodes: arr });\n\n const collab = ctx._container ? collabStore.get(ctx._container) : undefined;\n if (collab?.bridge) {\n for (const node of arr) {\n collab.bridge.pushLocalNodeAdd(node);\n }\n }\n\n // After measurement, center each node on its intended drop position\n if (options?.center) {\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n for (const [id, pos] of intendedPositions) {\n const node = ctx.nodes.find((n: FlowNode) => n.id === id);\n if (!node) continue;\n const w = node.dimensions?.width ?? 0;\n const h = node.dimensions?.height ?? 0;\n node.position.x = pos.x - w / 2;\n node.position.y = pos.y - h / 2;\n }\n });\n });\n }\n\n ctx._recomputeChildValidation();\n\n // Run child layout for any layout parents that received new children\n const layoutParents = new Set();\n for (const node of arr) {\n if (node.parentId) {\n const parent = ctx._nodeMap.get(node.parentId);\n if (parent?.childLayout) {\n // Auto-assign order if not set\n if (node.order == null) {\n const siblings = ctx.nodes.filter(\n (n: FlowNode) => n.parentId === node.parentId && n.id !== node.id,\n );\n node.order = siblings.length > 0\n ? Math.max(...siblings.map((s: FlowNode) => s.order ?? 0)) + 1\n : 0;\n }\n layoutParents.add(node.parentId);\n }\n }\n }\n // Layout from the topmost layout ancestor so constraints\n // (maxDimensions, stretch) cascade all the way down.\n const addRoots = new Set();\n for (const pid of layoutParents) {\n let topmost = pid;\n let walkId = ctx._nodeMap.get(pid)?.parentId;\n while (walkId) {\n const ancestor = ctx._nodeMap.get(walkId);\n if (ancestor?.childLayout) topmost = walkId;\n walkId = ancestor?.parentId;\n }\n addRoots.add(topmost);\n }\n // A4: route through dedup so multiple addNodes calls within the same\n // batch/frame collapse to a single layout pass per parent (consistent\n // with removeNodes behavior, which also calls layoutChildren per parent).\n for (const rid of addRoots) {\n if (ctx._layoutDedup) {\n ctx._layoutDedup.safeLayoutChildren(rid);\n } else {\n ctx.layoutChildren?.(rid);\n }\n }\n\n ctx._scheduleAutoLayout();\n ctx._commitNodeGeometry?.(arr.map((n: FlowNode) => n.id));\n },\n\n /**\n * Remove one or more nodes by ID.\n *\n * - Normalizes single ID or array input.\n * - Validates child constraints before allowing removal.\n * - Cascades removal to all descendants.\n * - Removes connected edges and optionally creates reconnection bridges.\n * - Cleans up selection state and initial dimensions.\n * - Pushes collab updates when a collaboration bridge is active.\n * - Re-layouts any layout parents that lost children.\n * - Schedules auto-layout after the mutation.\n */\n removeNodes(ids: string | string[]): void {\n ctx._captureHistory();\n const idSet = new Set(Array.isArray(ids) ? ids : [ids]);\n\n // Child validation: check if removing would violate parent constraints\n const blockedIds = new Set();\n for (const id of [...idSet]) {\n const node = ctx._nodeMap.get(id);\n if (!node?.parentId) continue;\n if (idSet.has(node.parentId)) continue;\n const rules = ctx._getChildValidation(node.parentId);\n if (!rules) continue;\n const parent = ctx._nodeMap.get(node.parentId);\n if (!parent) continue;\n const siblings = (ctx.nodes as FlowNode[]).filter(\n (n: FlowNode) => n.parentId === node.parentId,\n );\n const result = validateChildRemove(parent, node, siblings, rules);\n if (!result.valid) {\n blockedIds.add(id);\n if (ctx._config.onChildValidationFail) {\n ctx._config.onChildValidationFail({\n parent,\n child: node,\n operation: 'remove',\n rule: result.rule!,\n message: result.message!,\n });\n }\n }\n }\n for (const id of blockedIds) {\n idSet.delete(id);\n }\n if (idSet.size === 0) return;\n\n // Capture parent IDs before removal so we can re-layout them after\n const removedParentMap = new Map();\n for (const id of idSet) {\n const node = ctx._nodeMap.get(id);\n if (node?.parentId) removedParentMap.set(id, node.parentId);\n }\n\n // Also remove all descendants of the targeted nodes\n for (const id of [...idSet]) {\n for (const descId of getDescendantIds(id, ctx.nodes)) {\n idSet.add(descId);\n }\n }\n debug('destroy', `Removing ${idSet.size} node(s)`, [...idSet]);\n const removed = ctx.nodes.filter((n: FlowNode) => idSet.has(n.id));\n\n // Compute reconnection edges BEFORE removing connected edges\n let reconnected: FlowEdge[] = [];\n if (ctx._config.reconnectOnDelete) {\n reconnected = computeReconnectionEdges(idSet, ctx.nodes as FlowNode[], ctx.edges as FlowEdge[]);\n }\n\n // Remove connected edges — track removed IDs for collab sync\n const removedEdgeIds: string[] = [];\n ctx.edges = ctx.edges.filter((e: FlowEdge) => {\n if (idSet.has(e.source) || idSet.has(e.target)) {\n removedEdgeIds.push(e.id);\n return false;\n }\n return true;\n });\n\n // Add reconnection bridge edges\n if (reconnected.length) {\n ctx.edges.push(...reconnected);\n debug('destroy', `Created ${reconnected.length} reconnection edge(s)`);\n }\n\n ctx._rebuildEdgeMap();\n ctx.nodes = ctx.nodes.filter((n: FlowNode) => !idSet.has(n.id));\n ctx._rebuildNodeMap();\n // Clean up selection and childLayout watchers\n for (const id of idSet) {\n ctx.selectedNodes.delete(id);\n ctx._initialDimensions.delete(id);\n ctx._uninstallChildLayoutWatchers(id);\n ctx._draggingNodeIds?.delete(id);\n }\n // Prune routing state for cascade-removed edges — mirrors removeEdges'\n // cleanup, which this path bypasses since it splices ctx.edges directly.\n for (const edgeId of removedEdgeIds) {\n ctx._edgeDirtyTicks?.delete(edgeId);\n ctx._edgeCorridors?.delete(edgeId);\n }\n if (removed.length) ctx._emit('nodes-change', { type: 'remove', nodes: removed });\n if (reconnected.length) ctx._emit('edges-change', { type: 'add', edges: reconnected });\n\n const collab = ctx._container ? collabStore.get(ctx._container) : undefined;\n if (collab?.bridge) {\n for (const id of idSet) {\n collab.bridge.pushLocalNodeRemove(id);\n }\n for (const edgeId of removedEdgeIds) {\n collab.bridge.pushLocalEdgeRemove(edgeId);\n }\n for (const edge of reconnected) {\n collab.bridge.pushLocalEdgeAdd(edge);\n }\n }\n\n ctx._recomputeChildValidation();\n\n // Re-layout from the topmost layout ancestor so constraints\n // (maxDimensions, stretch) cascade down after the removal.\n const removedLayoutParents = new Set();\n for (const id of idSet) {\n const parentId = removedParentMap.get(id);\n if (parentId) {\n const parent = ctx._nodeMap.get(parentId);\n if (parent?.childLayout) removedLayoutParents.add(parentId);\n }\n }\n const removeRoots = new Set();\n for (const pid of removedLayoutParents) {\n let topmost = pid;\n let walkId = ctx._nodeMap.get(pid)?.parentId;\n while (walkId) {\n const ancestor = ctx._nodeMap.get(walkId);\n if (ancestor?.childLayout) topmost = walkId;\n walkId = ancestor?.parentId;\n }\n removeRoots.add(topmost);\n }\n for (const rid of removeRoots) ctx.layoutChildren?.(rid);\n\n ctx._scheduleAutoLayout();\n ctx._commitNodeGeometry?.([...idSet]);\n },\n\n /**\n * Look up a node by ID.\n */\n getNode(id: string): FlowNode | undefined {\n return ctx._nodeMap.get(id);\n },\n\n /**\n * Get all nodes connected via outgoing edges from the given node.\n */\n getOutgoers(nodeId: string): FlowNode[] {\n return graphGetOutgoers(nodeId, ctx.nodes, ctx.edges);\n },\n\n /**\n * Get all nodes connected via incoming edges to the given node.\n */\n getIncomers(nodeId: string): FlowNode[] {\n return graphGetIncomers(nodeId, ctx.nodes, ctx.edges);\n },\n\n /**\n * Get all edges connected to a node (both incoming and outgoing).\n */\n getConnectedEdges(nodeId: string): FlowEdge[] {\n return graphGetConnectedEdges(nodeId, ctx.edges);\n },\n\n /**\n * Check if two nodes are connected by an edge.\n * When `directed` is true, only checks source→target direction.\n */\n areNodesConnected(nodeA: string, nodeB: string, directed: boolean = false): boolean {\n return graphAreNodesConnected(nodeA, nodeB, ctx.edges, directed);\n },\n\n /**\n * Apply a node-level filter predicate.\n * Nodes that fail the predicate get `filtered = true`.\n */\n setNodeFilter(predicate: (node: FlowNode) => boolean): void {\n const filtered: FlowNode[] = [];\n const visible: FlowNode[] = [];\n for (const node of ctx.nodes as FlowNode[]) {\n const shouldFilter = !predicate(node);\n node.filtered = shouldFilter;\n if (shouldFilter) {\n filtered.push(node);\n } else {\n visible.push(node);\n }\n }\n debug('filter', `Node filter applied: ${visible.length} visible, ${filtered.length} filtered`);\n ctx._emit('node-filter-change', { filtered, visible });\n },\n\n /**\n * Clear node filter — restore all nodes to visible.\n */\n clearNodeFilter(): void {\n let hadFiltered = false;\n for (const node of ctx.nodes as FlowNode[]) {\n if (node.filtered) {\n node.filtered = false;\n hadFiltered = true;\n }\n }\n if (hadFiltered) {\n debug('filter', 'Node filter cleared');\n ctx._emit('node-filter-change', { filtered: [], visible: [...ctx.nodes] });\n }\n },\n\n /**\n * Get nodes whose bounding rect overlaps the given node.\n * Accepts either a FlowNode object or a node ID string.\n */\n getIntersectingNodes(nodeOrId: FlowNode | string, partially?: boolean): FlowNode[] {\n const node = typeof nodeOrId === 'string' ? ctx.nodes.find((n: FlowNode) => n.id === nodeOrId) : nodeOrId;\n if (!node) return [];\n return coreGetIntersectingNodes(node, ctx.nodes, partially);\n },\n\n /**\n * Check if two nodes' bounding rects overlap.\n * Accepts either FlowNode objects or node ID strings.\n */\n isNodeIntersecting(nodeOrId: FlowNode | string, targetOrId: FlowNode | string, partially?: boolean): boolean {\n const node = typeof nodeOrId === 'string' ? ctx.nodes.find((n: FlowNode) => n.id === nodeOrId) : nodeOrId;\n const target = typeof targetOrId === 'string' ? ctx.nodes.find((n: FlowNode) => n.id === targetOrId) : targetOrId;\n if (!node || !target) return false;\n return coreIsNodeIntersecting(node, target, partially);\n },\n\n /**\n * Set runState on one or more nodes by ID.\n * The x-flow-node directive reactively applies .flow-node-{state} CSS classes.\n */\n setNodeState(ids: string | string[], state: FlowNodeRunState): void {\n const idArr = Array.isArray(ids) ? ids : [ids];\n for (const id of idArr) {\n const node = ctx._nodeMap.get(id);\n if (node) (node as any).runState = state;\n }\n },\n\n /**\n * Clear all runState values, removing state CSS classes from all nodes.\n */\n resetStates(): void {\n for (const node of ctx.nodes as FlowNode[]) {\n delete (node as any).runState;\n }\n },\n };\n}\n","// ============================================================================\n// canvas-edges — Edge CRUD & DOM element access mixin for flow-canvas\n//\n// Public API: addEdges, removeEdges, getEdge, getEdgePathElement,\n// getEdgeElement, getEdgeSvgElement.\n//\n// Six methods handling edge lifecycle operations. addEdges merges\n// defaultEdgeOptions from config onto new edges.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowEdge } from '../../core/types';\nimport { debug } from '../../core/debug';\nimport { collabStore } from '../../collab/store';\nimport { checkConnectionRules } from '../../core/connections';\n\nexport function createEdgesMixin(ctx: CanvasContext) {\n return {\n /**\n * Add one or more edges to the canvas.\n *\n * - Normalizes single edge or array input.\n * - Merges `defaultEdgeOptions` from config (edge-specific props override defaults).\n * - Captures history before mutation.\n * - Pushes collab updates when a collaboration bridge is active.\n * - Schedules auto-layout after the mutation.\n */\n addEdges(newEdges: FlowEdge | FlowEdge[]): void {\n const defaults = ctx._config.defaultEdgeOptions;\n const connectionRules = ctx._config.connectionRules;\n const arr = (Array.isArray(newEdges) ? newEdges : [newEdges])\n .map((e) => (defaults ? { ...defaults, ...e } : e))\n .filter((e) => {\n if (!connectionRules) return true;\n const connection = { source: e.source, sourceHandle: e.sourceHandle, target: e.target, targetHandle: e.targetHandle };\n return checkConnectionRules(connection, connectionRules, ctx._nodeMap);\n });\n if (arr.length === 0) return;\n ctx._captureHistory();\n debug('edge', `Adding ${arr.length} edge(s)`, arr.map((e) => e.id));\n ctx.edges.push(...arr);\n ctx._rebuildEdgeMap();\n // WS-2: adding edges changes shared-handle membership → re-lane the\n // affected groups and dirty exactly those edges. No-op when spread is off\n // (grouping stays empty → empty change set).\n const relaned = ctx._computeEndpointGrouping();\n if (relaned.size > 0) ctx._markEdgesDirtyById(relaned);\n ctx._emit('edges-change', { type: 'add', edges: arr });\n\n const collab = ctx._container ? collabStore.get(ctx._container) : undefined;\n if (collab?.bridge) {\n for (const edge of arr) {\n collab.bridge.pushLocalEdgeAdd(edge);\n }\n }\n\n ctx._scheduleAutoLayout();\n },\n\n /**\n * Remove one or more edges by ID.\n *\n * - Normalizes single ID or array input.\n * - Filters edges, rebuilds edge map, deselects removed edges.\n * - Captures history before mutation.\n * - Pushes collab updates when a collaboration bridge is active.\n * - Schedules auto-layout after the mutation.\n */\n removeEdges(ids: string | string[]): void {\n ctx._captureHistory();\n const idSet = new Set(Array.isArray(ids) ? ids : [ids]);\n debug('edge', `Removing ${idSet.size} edge(s)`, [...idSet]);\n const removed = ctx.edges.filter((e: FlowEdge) => idSet.has(e.id));\n ctx.edges = ctx.edges.filter((e: FlowEdge) => !idSet.has(e.id));\n ctx._rebuildEdgeMap();\n for (const id of idSet) {\n ctx.selectedEdges.delete(id);\n ctx._edgeDirtyTicks?.delete(id);\n ctx._edgeCorridors?.delete(id);\n }\n // WS-2: removing edges changes shared-handle membership → re-lane the\n // surviving group members and dirty exactly those. No-op when spread is off.\n const relaned = ctx._computeEndpointGrouping();\n if (relaned.size > 0) ctx._markEdgesDirtyById(relaned);\n if (removed.length) ctx._emit('edges-change', { type: 'remove', edges: removed });\n\n const collab = ctx._container ? collabStore.get(ctx._container) : undefined;\n if (collab?.bridge) {\n for (const id of idSet) {\n collab.bridge.pushLocalEdgeRemove(id);\n }\n }\n\n ctx._scheduleAutoLayout();\n },\n\n /**\n * Look up an edge by ID.\n */\n getEdge(id: string): FlowEdge | undefined {\n return ctx._edgeMap.get(id);\n },\n\n /**\n * Get the visible SVG `` element for an edge.\n * The visible path is the second `` child (the first is the interaction hit area).\n */\n getEdgePathElement(id: string): SVGPathElement | null {\n const g = ctx._container?.querySelector(`[data-flow-edge-id=\"${CSS.escape(id)}\"]`);\n // The visible path is the second child (first is the interaction hit area)\n return g?.querySelector('path:nth-child(2)') as SVGPathElement | null;\n },\n\n /**\n * Get the container element (SVG group) for an edge.\n */\n getEdgeElement(id: string): SVGElement | HTMLElement | null {\n return ctx._container?.querySelector(`[data-flow-edge-id=\"${CSS.escape(id)}\"]`) as SVGElement | null;\n },\n\n /**\n * Get the SVG element that hosts edge paths.\n * Returns the first `.flow-edge-svg` element inside the viewport if any edges\n * exist. When the canvas has zero edges (e.g., a particle-only canvas using\n * sendParticleAlongPath or sendParticleBetween), lazily creates and caches a\n * dedicated `.flow-particle-svg` element inside the edges container so\n * particle emission methods have a place to inject temporary paths.\n */\n getEdgeSvgElement(): SVGSVGElement | null {\n if (!ctx._viewportEl) return null;\n const existing = ctx._viewportEl.querySelector('.flow-edge-svg') as SVGSVGElement | null;\n if (existing) return existing;\n\n const edgesDiv = ctx._viewportEl.querySelector('.flow-edges');\n if (!edgesDiv) return null;\n\n let fallback = edgesDiv.querySelector('.flow-particle-svg') as SVGSVGElement | null;\n if (!fallback) {\n fallback = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;\n // Match the structural.css .flow-edge-svg sizing pattern exactly:\n // 1px × 1px with overflow:visible so children render at their absolute\n // SVG user-space coordinates. 100% width/height would resolve to 0×0\n // because .flow-edges has no explicit dimensions.\n fallback.setAttribute('class', 'flow-particle-svg');\n fallback.style.position = 'absolute';\n fallback.style.top = '0';\n fallback.style.left = '0';\n fallback.style.width = '1px';\n fallback.style.height = '1px';\n fallback.style.overflow = 'visible';\n fallback.style.pointerEvents = 'none';\n edgesDiv.appendChild(fallback);\n }\n return fallback;\n },\n };\n}\n","// ============================================================================\n// canvas-viewport — Viewport coordinate transforms, zoom, pan, fit, and\n// intersection detection mixin for flow-canvas\n//\n// Public API: screenToFlowPosition, flowToScreenPosition, fitView, fitBounds,\n// getNodesBounds, getViewportForBounds, setViewport, zoomIn,\n// zoomOut, setCenter, panBy, toggleInteractive, colorMode,\n// getContainerDimensions, resetPanels.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowNode, Viewport, XYPosition } from '../../core/types';\nimport {\n getNodesBounds,\n getViewportForBounds,\n screenToFlowPosition,\n flowToScreenPosition,\n} from '../../core/geometry';\nimport { toAbsoluteNodes } from '../../core/sub-flow';\nimport { ZOOM_STEP_FACTOR, DEFAULT_FIT_PADDING } from '../../core/constants';\nimport { debug } from '../../core/debug';\n\nexport function createViewportMixin(ctx: CanvasContext) {\n return {\n // ── Coordinate Transforms ─────────────────────────────────────────────\n\n /**\n * Convert screen coordinates (e.g. from a pointer event) to flow\n * coordinates, accounting for the current viewport pan and zoom.\n */\n screenToFlowPosition(x: number, y: number): XYPosition {\n if (!ctx._container) return { x, y };\n const rect = ctx._container.getBoundingClientRect();\n // Live viewport: reactive `viewport` lags a frame behind the transform.\n return screenToFlowPosition(x, y, ctx._viewportLive ?? ctx.viewport, rect);\n },\n\n /**\n * Convert flow coordinates to screen coordinates, accounting for the\n * current viewport pan and zoom.\n */\n flowToScreenPosition(x: number, y: number): XYPosition {\n if (!ctx._container) return { x, y };\n const rect = ctx._container.getBoundingClientRect();\n // Live viewport: reactive `viewport` lags a frame behind the transform.\n return flowToScreenPosition(x, y, ctx._viewportLive ?? ctx.viewport, rect);\n },\n\n // ── Fit & Bounds ──────────────────────────────────────────────────────\n\n /**\n * Fit all visible nodes into the viewport.\n *\n * Defers via `requestAnimationFrame` if any node lacks measured\n * dimensions (up to 10 retries) to give the DOM time to render.\n */\n fitView(options?: { padding?: number; duration?: number }, _retries = 0): void {\n // Defer if any node lacks measured dimensions so the DOM has time to render\n if (ctx.nodes.some((n: FlowNode) => !n.dimensions)) {\n if (_retries < 10) {\n requestAnimationFrame(() => this.fitView(options, _retries + 1));\n }\n return;\n }\n\n const visibleNodes = ctx.nodes.filter((n: FlowNode) => !n.hidden);\n const bounds = getNodesBounds(toAbsoluteNodes(visibleNodes, ctx._nodeMap, ctx._config.nodeOrigin), ctx._config.nodeOrigin);\n this.fitBounds(bounds, options);\n ctx._announcer?.handleEvent('fit-view', {});\n },\n\n /**\n * Fit a specific rectangle into the viewport.\n *\n * If `duration` is specified, the transition is animated via\n * `ctx.animate()` (cross-mixin dependency). Otherwise the viewport\n * is set directly via `ctx._panZoom`.\n */\n fitBounds(rect: { x: number; y: number; width: number; height: number }, options?: { padding?: number; duration?: number }): void {\n const dims = ctx._container\n ? { width: ctx._container.clientWidth, height: ctx._container.clientHeight }\n : { width: 800, height: 600 };\n\n const vp = getViewportForBounds(\n rect,\n dims.width,\n dims.height,\n ctx._config.minZoom ?? 0.5,\n ctx._config.maxZoom ?? 2,\n options?.padding ?? DEFAULT_FIT_PADDING,\n );\n\n debug('viewport', 'fitBounds', { rect, viewport: vp });\n\n const duration = options?.duration ?? 0;\n if (duration > 0) {\n ctx.animate?.(\n { viewport: { pan: { x: vp.x, y: vp.y }, zoom: vp.zoom } },\n { duration },\n );\n } else {\n ctx._panZoom?.setViewport(vp);\n }\n },\n\n /**\n * Get the bounding rectangle of the specified nodes (or all visible\n * nodes if no IDs are provided).\n */\n getNodesBounds(nodeIds?: string[]): { x: number; y: number; width: number; height: number } {\n let nodes: FlowNode[];\n if (nodeIds) {\n nodes = nodeIds\n .map((id: string) => ctx.getNode(id))\n .filter((n: FlowNode | undefined): n is FlowNode => !!n);\n } else {\n nodes = ctx.nodes.filter((n: FlowNode) => !n.hidden);\n }\n return getNodesBounds(toAbsoluteNodes(nodes, ctx._nodeMap, ctx._config.nodeOrigin), ctx._config.nodeOrigin);\n },\n\n /**\n * Compute the viewport (pan + zoom) that frames the given bounds\n * within the container, respecting min/max zoom and padding.\n */\n getViewportForBounds(bounds: { x: number; y: number; width: number; height: number }, padding?: number): Viewport {\n const el = ctx._container;\n if (!el) return { x: 0, y: 0, zoom: 1 };\n return getViewportForBounds(\n bounds,\n el.clientWidth,\n el.clientHeight,\n ctx._config.minZoom ?? 0.5,\n ctx._config.maxZoom ?? 2,\n padding ?? DEFAULT_FIT_PADDING,\n );\n },\n\n // ── Viewport Mutation ─────────────────────────────────────────────────\n\n /**\n * Set the viewport programmatically (pan and/or zoom).\n */\n setViewport(viewport: Partial, options?: { duration?: number }): void {\n debug('viewport', 'setViewport', viewport);\n ctx._panZoom?.setViewport(viewport, options);\n },\n\n /**\n * Zoom in by `ZOOM_STEP_FACTOR`, clamped to `maxZoom`.\n */\n zoomIn(options?: { duration?: number }): void {\n // Base off the live viewport: reactive `viewport` settles a frame late, so\n // two relative ops in one tick would otherwise both read the same stale base.\n const vp = ctx._viewportLive ?? ctx.viewport;\n const maxZoom = ctx._config.maxZoom ?? 2;\n const newZoom = Math.min(vp.zoom * ZOOM_STEP_FACTOR, maxZoom);\n debug('viewport', 'zoomIn', { from: vp.zoom, to: newZoom });\n ctx._panZoom?.setViewport({ ...vp, zoom: newZoom }, options);\n },\n\n /**\n * Zoom out by `ZOOM_STEP_FACTOR`, clamped to `minZoom`.\n */\n zoomOut(options?: { duration?: number }): void {\n const vp = ctx._viewportLive ?? ctx.viewport;\n const minZoom = ctx._config.minZoom ?? 0.5;\n const newZoom = Math.max(vp.zoom / ZOOM_STEP_FACTOR, minZoom);\n debug('viewport', 'zoomOut', { from: vp.zoom, to: newZoom });\n ctx._panZoom?.setViewport({ ...vp, zoom: newZoom }, options);\n },\n\n /**\n * Center the viewport on flow coordinate `(x, y)` at the given zoom\n * level (defaults to the current zoom).\n */\n setCenter(x: number, y: number, zoom?: number, options?: { duration?: number }): void {\n const el = ctx._container;\n if (!el) return;\n const z = zoom ?? (ctx._viewportLive ?? ctx.viewport).zoom;\n const vpX = el.clientWidth / 2 - x * z;\n const vpY = el.clientHeight / 2 - y * z;\n debug('viewport', 'setCenter', { x, y, zoom: z });\n ctx._panZoom?.setViewport({ x: vpX, y: vpY, zoom: z }, options);\n },\n\n /**\n * Pan the viewport by a delta `(dx, dy)`.\n */\n panBy(dx: number, dy: number, options?: { duration?: number }): void {\n const vp = ctx._viewportLive ?? ctx.viewport;\n debug('viewport', 'panBy', { dx, dy });\n ctx._panZoom?.setViewport(\n { x: vp.x + dx, y: vp.y + dy, zoom: vp.zoom },\n options,\n );\n },\n\n // ── Interactivity Toggle ──────────────────────────────────────────────\n\n /**\n * Toggle pan/zoom interactivity on and off.\n */\n toggleInteractive(): void {\n ctx.isInteractive = !ctx.isInteractive;\n debug('interactive', 'toggleInteractive', { isInteractive: ctx.isInteractive });\n ctx._panZoom?.update({\n pannable: ctx.isInteractive,\n zoomable: ctx.isInteractive,\n });\n },\n\n // ── Color Mode ────────────────────────────────────────────────────────\n\n /**\n * The current resolved color mode ('light' | 'dark' | undefined).\n */\n get colorMode(): 'light' | 'dark' | undefined {\n return ctx._colorModeHandle?.resolved;\n },\n\n // ── Container Dimensions ──────────────────────────────────────────────\n\n /**\n * Get the current width and height of the container element.\n */\n getContainerDimensions(): { width: number; height: number } {\n return {\n width: ctx._container?.clientWidth ?? 0,\n height: ctx._container?.clientHeight ?? 0,\n };\n },\n\n // ── Panel Operations ──────────────────────────────────────────────────\n\n /**\n * Reset all panels by dispatching a `flow-panel-reset` CustomEvent\n * on the container and emitting a `panel-reset` event.\n */\n resetPanels(): void {\n debug('panel', 'resetPanels');\n ctx._container?.dispatchEvent(new CustomEvent('flow-panel-reset'));\n ctx._emit('panel-reset');\n },\n };\n}\n","// ============================================================================\n// Clipboard Manager\n//\n// Pure functions for copy/paste/cut of selected nodes and edges.\n// Uses a module-level clipboard variable shared across all canvas instances\n// on the same page, enabling cross-canvas copy/paste.\n// ============================================================================\n\nimport type { FlowNode, FlowEdge } from './types';\n\ninterface ClipboardData {\n nodes: FlowNode[];\n edges: FlowEdge[];\n pasteCount: number;\n}\n\nlet clipboard: ClipboardData | null = null;\n\nconst PASTE_OFFSET = 20;\n\n/**\n * Deep-clone a value. Uses JSON round-trip instead of structuredClone\n * because Alpine.js wraps reactive arrays/objects in Proxy, which\n * structuredClone cannot handle (throws DataCloneError).\n *\n * Note: Only JSON-serializable values in node.data / edge.data are preserved.\n * Map, Set, Date, RegExp, undefined, and function values will be silently dropped.\n */\nfunction clone(value: T): T {\n return JSON.parse(JSON.stringify(value));\n}\n\n/**\n * Generate a unique ID for a pasted node/edge.\n */\nfunction generateCopyId(originalId: string): string {\n return `${originalId}-copy-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;\n}\n\n/**\n * Copy selected nodes and their internal edges to the clipboard.\n * \"Internal\" means both source and target are in the selection.\n */\nexport function copySelection(\n nodes: FlowNode[],\n edges: FlowEdge[],\n): { nodeCount: number; edgeCount: number } {\n const selectedNodes = nodes.filter((n) => n.selected);\n const selectedNodeIds = new Set(selectedNodes.map((n) => n.id));\n\n const selectedEdges = edges.filter(\n (e) => e.selected || (selectedNodeIds.has(e.source) && selectedNodeIds.has(e.target)),\n );\n\n clipboard = {\n nodes: clone(selectedNodes),\n edges: clone(selectedEdges),\n pasteCount: 0,\n };\n\n return { nodeCount: selectedNodes.length, edgeCount: selectedEdges.length };\n}\n\n/**\n * Paste clipboard contents with new IDs and accumulating offset.\n * Returns null if clipboard is empty.\n */\nexport function pasteClipboard(): { nodes: FlowNode[]; edges: FlowEdge[] } | null {\n if (!clipboard || clipboard.nodes.length === 0) return null;\n\n clipboard.pasteCount++;\n const offset = clipboard.pasteCount * PASTE_OFFSET;\n const idMap = new Map();\n\n const newNodes = clipboard.nodes.map((n) => {\n const newId = generateCopyId(n.id);\n idMap.set(n.id, newId);\n return {\n ...n,\n id: newId,\n data: clone(n.data),\n position: { x: n.position.x + offset, y: n.position.y + offset },\n selected: true,\n };\n });\n\n const newEdges = clipboard.edges.map((e) => ({\n ...e,\n id: generateCopyId(e.id),\n source: idMap.get(e.source)!,\n target: idMap.get(e.target)!,\n selected: true,\n }));\n\n return { nodes: newNodes, edges: newEdges };\n}\n\n/**\n * Copy selected nodes to clipboard and return their IDs for removal.\n */\nexport function cutSelection(\n nodes: FlowNode[],\n edges: FlowEdge[],\n): { nodeIds: string[]; edgeCount: number; nodeCount: number } {\n const result = copySelection(nodes, edges);\n const nodeIds = nodes.filter((n) => n.selected).map((n) => n.id);\n return { nodeIds, ...result };\n}\n\n/**\n * Clear the clipboard.\n */\nexport function clearClipboard(): void {\n clipboard = null;\n}\n","// ============================================================================\n// canvas-selection — Selection, clipboard, and deletion mixin for flow-canvas\n//\n// Public API: deselectAll, _deleteSelected, copy, paste, cut.\n//\n// Selection state (selectedNodes, selectedEdges Sets) is declared in the\n// orchestrator. This mixin provides deselection clearing, user-initiated\n// deletion with child validation and onBeforeDelete callback, and clipboard\n// operations delegating to core/clipboard pure functions.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowNode, FlowEdge } from '../../core/types';\nimport { copySelection, pasteClipboard, cutSelection } from '../../core/clipboard';\nimport { validateChildRemove } from '../../core/child-validation';\nimport { isDeletable } from '../../core/node-flags';\nimport { sortNodesTopological } from '../../core/sub-flow';\nimport { debug } from '../../core/debug';\n\nexport function createSelectionMixin(ctx: CanvasContext) {\n return {\n // ── Deselect ─────────────────────────────────────────────────────────\n\n /**\n * Clear all node, edge, and row selections.\n *\n * - Sets `selected = false` on each selected node/edge data object.\n * - Clears `selectedNodes`, `selectedEdges`, and `selectedRows` Sets.\n * - Removes `.flow-node-selected`, `.flow-edge-selected`, and\n * `.flow-row-selected` CSS classes from the DOM.\n * - Emits a `selection-change` event.\n */\n deselectAll(): void {\n if (ctx.selectedNodes.size === 0 && ctx.selectedEdges.size === 0 && ctx.selectedRows.size === 0) return;\n debug('selection', 'Deselecting all');\n for (const id of ctx.selectedNodes) {\n const n = ctx.getNode(id);\n if (n) n.selected = false;\n }\n for (const id of ctx.selectedEdges) {\n const e = ctx.getEdge(id);\n if (e) e.selected = false;\n }\n ctx.selectedNodes.clear();\n ctx.selectedEdges.clear();\n ctx.selectedRows.clear();\n // Remove selection class from all node/edge/row elements\n ctx._container?.querySelectorAll('.flow-node-selected, .flow-edge-selected, .flow-row-selected').forEach((el: Element) => {\n el.classList.remove('flow-node-selected', 'flow-edge-selected', 'flow-row-selected');\n });\n ctx._emitSelectionChange();\n },\n\n // ── Deletion ─────────────────────────────────────────────────────────\n\n /**\n * Delete currently selected nodes and edges.\n *\n * - Filters out non-deletable nodes/edges (where `deletable === false`).\n * - Cascades edge deletion for edges connected to deleted nodes.\n * - Validates child removal constraints before deleting child nodes.\n * - Supports an async `onBeforeDelete` callback that can cancel or\n * modify the set of items to delete.\n * - Wraps the entire operation in a single history step.\n */\n async _deleteSelected(): Promise {\n // Collect deletable selected nodes\n const nodeIds = [...ctx.selectedNodes].filter((id: string) => {\n const n = ctx.getNode(id);\n return n ? isDeletable(n) : false;\n });\n // Collect deletable selected edges\n const edgeIds = [...ctx.selectedEdges].filter((id: string) => {\n const edge = ctx.getEdge(id);\n return edge?.deletable !== false;\n });\n\n let nodesToDelete = nodeIds.map((id: string) => ctx.getNode(id)!).filter(Boolean);\n // Compute cascaded edges (connected to deleted nodes)\n const nodeIdSet = new Set(nodeIds);\n const cascadedEdges = (ctx.edges as FlowEdge[]).filter(\n (e: FlowEdge) => nodeIdSet.has(e.source) || nodeIdSet.has(e.target),\n );\n // Merge explicitly selected edges with cascaded (dedupe)\n const allEdgeMap = new Map();\n for (const e of cascadedEdges) allEdgeMap.set(e.id, e);\n for (const id of edgeIds) {\n const e = ctx.getEdge(id);\n if (e) allEdgeMap.set(e.id, e);\n }\n const edgesToDelete = [...allEdgeMap.values()];\n\n // Child validation: filter out nodes whose deletion would violate parent constraints\n nodesToDelete = nodesToDelete.filter((node: FlowNode) => {\n if (!node.parentId) return true;\n // Parent is also being deleted — no constraint to check\n if (nodesToDelete.some((n: FlowNode) => n.id === node.parentId)) return true;\n const rules = ctx._getChildValidation(node.parentId);\n if (!rules) return true;\n const parent = ctx.getNode(node.parentId);\n if (!parent) return true;\n const siblings = (ctx.nodes as FlowNode[]).filter(\n (n: FlowNode) => n.parentId === node.parentId,\n );\n const result = validateChildRemove(parent, node, siblings, rules);\n if (!result.valid && ctx._config.onChildValidationFail) {\n ctx._config.onChildValidationFail({\n parent,\n child: node,\n operation: 'remove',\n rule: result.rule!,\n message: result.message!,\n });\n }\n return result.valid;\n });\n\n if (nodesToDelete.length === 0 && edgesToDelete.length === 0) return;\n\n if (ctx._config?.onBeforeDelete) {\n const result = await ctx._config.onBeforeDelete({\n nodes: nodesToDelete,\n edges: edgesToDelete,\n });\n if (result === false) {\n debug('delete', 'onBeforeDelete cancelled deletion');\n return;\n }\n ctx._captureHistory();\n ctx._suspendHistory();\n try {\n if (result.nodes.length > 0) {\n debug('delete', `onBeforeDelete approved ${result.nodes.length} node(s)`);\n ctx.removeNodes(result.nodes.map((n: FlowNode) => n.id));\n }\n if (result.edges.length > 0) {\n const remainingEdgeIds = result.edges\n .map((e: FlowEdge) => e.id)\n .filter((id: string) => (ctx.edges as FlowEdge[]).some((e: FlowEdge) => e.id === id));\n if (remainingEdgeIds.length > 0) {\n debug('delete', `onBeforeDelete approved ${remainingEdgeIds.length} edge(s)`);\n ctx.removeEdges(remainingEdgeIds);\n }\n }\n ctx._recomputeChildValidation();\n for (const id of ctx.selectedNodes) {\n if (!(ctx.nodes as FlowNode[]).some((n: FlowNode) => n.id === id)) {\n ctx.selectedNodes.delete(id);\n }\n }\n for (const id of ctx.selectedEdges) {\n if (!(ctx.edges as FlowEdge[]).some((e: FlowEdge) => e.id === id)) {\n ctx.selectedEdges.delete(id);\n }\n }\n } finally {\n ctx._resumeHistory();\n }\n return;\n }\n\n ctx._captureHistory();\n ctx._suspendHistory();\n try {\n if (nodesToDelete.length > 0) {\n debug('delete', `Deleting ${nodesToDelete.length} selected node(s)`);\n ctx.removeNodes(nodesToDelete.map((n: FlowNode) => n.id));\n }\n if (edgeIds.length > 0) {\n const remainingEdgeIds = edgeIds.filter((id: string) =>\n (ctx.edges as FlowEdge[]).some((e: FlowEdge) => e.id === id),\n );\n if (remainingEdgeIds.length > 0) {\n debug('delete', `Deleting ${remainingEdgeIds.length} selected edge(s)`);\n ctx.removeEdges(remainingEdgeIds);\n }\n }\n ctx._recomputeChildValidation();\n for (const id of ctx.selectedNodes) {\n if (!(ctx.nodes as FlowNode[]).some((n: FlowNode) => n.id === id)) {\n ctx.selectedNodes.delete(id);\n }\n }\n for (const id of ctx.selectedEdges) {\n if (!(ctx.edges as FlowEdge[]).some((e: FlowEdge) => e.id === id)) {\n ctx.selectedEdges.delete(id);\n }\n }\n } finally {\n ctx._resumeHistory();\n }\n },\n\n // ── Clipboard Operations ─────────────────────────────────────────────\n\n /**\n * Copy currently selected nodes and their internal edges to the\n * module-level clipboard. Emits a `copy` event.\n */\n copy(): void {\n const result = copySelection(ctx.nodes, ctx.edges);\n if (result.nodeCount > 0) {\n debug('clipboard', `Copied ${result.nodeCount} node(s) and ${result.edgeCount} edge(s)`);\n ctx._emit('copy', result);\n }\n },\n\n /**\n * Paste nodes/edges from the clipboard with new IDs and an\n * accumulating 20 px offset.\n *\n * - Deselects all current selection first.\n * - Pushes new nodes (topologically sorted) and edges directly.\n * - Selects all pasted items.\n * - Applies `.flow-node-selected` / `.flow-edge-selected` CSS classes\n * after Alpine renders the new DOM elements.\n */\n paste(): void {\n const result = pasteClipboard();\n if (!result) return;\n ctx._captureHistory();\n ctx.deselectAll();\n // Add pasted items directly to avoid double history capture from addNodes/addEdges\n ctx.nodes.push(...result.nodes);\n ctx.nodes = sortNodesTopological(ctx.nodes);\n ctx._rebuildNodeMap();\n ctx.edges.push(...result.edges);\n ctx._rebuildEdgeMap();\n // Update selection sets\n for (const n of result.nodes) {\n ctx.selectedNodes.add(n.id);\n }\n for (const e of result.edges) {\n ctx.selectedEdges.add(e.id);\n }\n ctx._emitSelectionChange();\n ctx._emit('nodes-change', { type: 'add', nodes: result.nodes });\n ctx._emit('edges-change', { type: 'add', edges: result.edges });\n ctx._emit('paste', { nodes: result.nodes, edges: result.edges });\n debug('clipboard', `Pasted ${result.nodes.length} node(s) and ${result.edges.length} edge(s)`);\n // Apply selected CSS class after Alpine renders the new DOM elements\n ctx.$nextTick(() => {\n for (const n of result.nodes) {\n const el = ctx._container?.querySelector(`[data-flow-node-id=\"${CSS.escape(n.id)}\"]`);\n el?.classList.add('flow-node-selected');\n }\n for (const e of result.edges) {\n const el = ctx._container?.querySelector(`[data-flow-edge-id=\"${CSS.escape(e.id)}\"]`);\n el?.classList.add('flow-edge-selected');\n }\n });\n },\n\n /**\n * Copy selected nodes to the clipboard, then delete them.\n * Emits a `cut` event.\n */\n async cut(): Promise {\n if (ctx.selectedNodes.size === 0) return;\n const result = cutSelection(ctx.nodes, ctx.edges);\n if (result.nodeCount === 0) return;\n await ctx._deleteSelected();\n ctx._emit('cut', { nodeCount: result.nodeCount, edgeCount: result.edgeCount });\n debug('clipboard', `Cut ${result.nodeCount} node(s)`);\n },\n };\n}\n","/**\n * Deep-merge `source` into `target` in place.\n *\n * Rules:\n * - Objects are recursively merged (keys added/overwritten, unmentioned keys preserved).\n * - Arrays are replaced, not merged.\n * - `null` explicitly removes a key.\n * - `undefined` values are ignored (key is not touched).\n */\nexport function deepMerge>(\n target: T,\n source: Record,\n): T {\n for (const key of Object.keys(source)) {\n // Guard against prototype pollution\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;\n\n const val = source[key];\n\n // undefined → skip\n if (val === undefined) continue;\n\n // null → delete key\n if (val === null) {\n delete (target as any)[key];\n continue;\n }\n\n // Arrays → replace (not merge)\n if (Array.isArray(val)) {\n (target as any)[key] = [...val];\n continue;\n }\n\n // Plain objects → recurse\n if (typeof val === 'object' && val.constructor === Object) {\n if (\n typeof (target as any)[key] !== 'object' ||\n (target as any)[key] === null ||\n Array.isArray((target as any)[key])\n ) {\n (target as any)[key] = {};\n }\n deepMerge((target as any)[key], val);\n continue;\n }\n\n // Primitives → overwrite\n (target as any)[key] = val;\n }\n\n return target;\n}\n\nfunction jsonEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true;\n if (a === undefined || b === undefined) return a === b;\n return JSON.stringify(a) === JSON.stringify(b);\n}\n\n/**\n * Merge `incoming` entities into `existing` by id, PRESERVING the original\n * object references for ids present in both. Property writes are skipped when\n * deep-equal, so Alpine effects watching those objects only re-run for real\n * changes. Returns the merged list in `incoming` order.\n */\nexport function mergeEntitiesById(\n existing: T[],\n incoming: T[],\n opts: { deleteMissing?: boolean } = {},\n): T[] {\n const deleteMissing = opts.deleteMissing ?? true;\n const existingById = new Map(existing.map((e) => [e.id, e]));\n const merged: T[] = [];\n for (const inc of incoming) {\n const cur = existingById.get(inc.id);\n if (!cur) {\n merged.push(inc);\n continue;\n }\n if (deleteMissing) {\n for (const key of Object.keys(cur)) {\n if (key !== 'id' && !(key in inc)) delete (cur as Record)[key];\n }\n }\n for (const [key, value] of Object.entries(inc)) {\n // Guard against prototype pollution (mirrors deepMerge): a bracket\n // assignment to `__proto__` would invoke the prototype setter.\n if (key === 'id' || key === '__proto__' || key === 'constructor' || key === 'prototype') {\n continue;\n }\n if (!jsonEqual((cur as Record)[key], value)) {\n (cur as Record)[key] = value;\n }\n }\n merged.push(cur);\n }\n return merged;\n}\n","// ============================================================================\n// canvas-history — Undo/redo & serialization mixin for flow-canvas\n//\n// Public API: undo, redo, canUndo, canRedo, toObject, fromObject, $reset, $clear.\n//\n// Eight methods handling history operations and state serialization.\n// undo/redo apply snapshots from the FlowHistory stack, then rebuild maps.\n// toObject/fromObject deep-clone state for save/restore workflows.\n// $reset/$clear are convenience wrappers around fromObject.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowNode, FlowEdge, Viewport } from '../../core/types';\nimport { sortNodesTopological } from '../../core/sub-flow';\nimport { mergeEntitiesById } from '../../core/deep-merge';\nimport type { HistorySnapshot } from '../../core/history';\nimport { debug } from '../../core/debug';\n\n/**\n * Apply an undo/redo snapshot by MERGING it into the existing reactive node\n * and edge objects instead of wholesale-replacing them. Surviving ids keep\n * their original object identity, so live Alpine edge/node scopes (and the\n * child-layout watchers bound to node proxies) stay attached — a wholesale\n * `ctx.edges = snapshot.edges` would orphan every edge scope. Array identity is\n * preserved via `splice`, and a `restore` event is emitted so wire-bridge and\n * collaboration observers are notified of the change.\n */\nfunction applyHistorySnapshot(\n ctx: CanvasContext,\n snapshot: HistorySnapshot,\n source: 'undo' | 'redo',\n): void {\n const mergedNodes = mergeEntitiesById(ctx.nodes, sortNodesTopological(snapshot.nodes));\n ctx.nodes.splice(0, ctx.nodes.length, ...mergedNodes);\n const mergedEdges = mergeEntitiesById(ctx.edges, snapshot.edges);\n ctx.edges.splice(0, ctx.edges.length, ...mergedEdges);\n ctx._rebuildNodeMap();\n ctx._rebuildEdgeMap();\n ctx.deselectAll();\n // Snapshots may carry selected: true from capture time; deselectAll only\n // clears ids present in the (already-cleared) selection sets, so clear the\n // restored flags on the surviving objects directly.\n for (const n of ctx.nodes) {\n if (n.selected) n.selected = false;\n }\n for (const e of ctx.edges) {\n if (e.selected) e.selected = false;\n }\n // Carry the restored nodes/edges (parity with fromObject's `restore` payload)\n // plus a `source` tag, so wire-bridge / collaboration observers can react to\n // undo/redo the same way they react to fromObject.\n ctx._emit('restore', { nodes: ctx.nodes, edges: ctx.edges, source });\n // Bump _layoutAnimTick in a rAF so edge effects re-run after node effects\n // have repositioned DOM elements (edges measure handle positions from the\n // DOM via getBoundingClientRect).\n requestAnimationFrame(() => {\n ctx._layoutAnimTick++;\n ctx._commitNodeGeometry?.();\n });\n debug('history', `${source} applied`, { nodes: snapshot.nodes.length, edges: snapshot.edges.length });\n}\n\nexport function createHistoryMixin(ctx: CanvasContext) {\n return {\n // ── Save / Restore ────────────────────────────────────────────\n\n /**\n * Serialize the current canvas state (nodes, edges, viewport) as a\n * deep-cloned plain object. Emits a `save` event with the snapshot.\n */\n toObject(): { nodes: FlowNode[]; edges: FlowEdge[]; viewport: Viewport } {\n const obj = {\n nodes: JSON.parse(JSON.stringify(ctx.nodes)) as FlowNode[],\n edges: JSON.parse(JSON.stringify(ctx.edges)) as FlowEdge[],\n viewport: { ...ctx.viewport },\n };\n ctx._emit('save', obj);\n return obj;\n },\n\n /**\n * Restore canvas state from a saved object.\n *\n * - Deep-clones incoming nodes/edges to avoid shared references.\n * - Sorts nodes topologically for correct parent-before-child ordering.\n * - Rebuilds node and edge lookup maps.\n * - Applies viewport if provided.\n * - Deselects all, emits `restore`, and schedules auto-layout.\n */\n fromObject(obj: { nodes?: FlowNode[]; edges?: FlowEdge[]; viewport?: Partial }): void {\n debug('store', `fromObject: restoring state`, {\n nodes: obj.nodes?.length ?? 0,\n edges: obj.edges?.length ?? 0,\n viewport: !!obj.viewport,\n });\n if (obj.nodes) {\n // Merge into existing reactive node objects (preserving identity for\n // surviving ids) instead of wholesale replacement. Keep array identity\n // via splice so Alpine's x-for and the child-layout watchers stay bound.\n const incomingNodes = sortNodesTopological(\n JSON.parse(JSON.stringify(obj.nodes)) as FlowNode[],\n );\n const mergedNodes = mergeEntitiesById(ctx.nodes, incomingNodes);\n ctx.nodes.splice(0, ctx.nodes.length, ...mergedNodes);\n }\n if (obj.edges) {\n // Merge into existing reactive edge objects so that Alpine scopes\n // bound via addScopeToNode (in flow-viewport) keep working.\n // A wholesale array replacement would orphan the old proxy refs\n // that flow-edge effects are watching.\n const incomingEdges: FlowEdge[] = JSON.parse(JSON.stringify(obj.edges));\n const mergedEdges = mergeEntitiesById(ctx.edges, incomingEdges);\n ctx.edges.splice(0, ctx.edges.length, ...mergedEdges);\n }\n ctx._rebuildNodeMap();\n ctx._rebuildEdgeMap();\n if (obj.viewport) {\n const vp = { ...ctx.viewport, ...obj.viewport };\n ctx._panZoom?.setViewport(vp);\n }\n ctx.deselectAll();\n ctx._emit('restore', obj);\n ctx._scheduleAutoLayout();\n // Ensure edges re-measure DOM handle positions after node effects\n // have repositioned elements (edges use getBoundingClientRect).\n requestAnimationFrame(() => {\n ctx._layoutAnimTick++;\n ctx._commitNodeGeometry?.();\n });\n },\n\n /**\n * Reset the canvas to its initial configuration state.\n */\n $reset(): void {\n debug('store', '$reset: restoring initial config');\n this.fromObject({\n nodes: ctx._config.nodes ?? [],\n edges: ctx._config.edges ?? [],\n viewport: ctx._config.viewport ?? { x: 0, y: 0, zoom: 1 },\n });\n },\n\n /**\n * Clear all nodes and edges, resetting the viewport to origin.\n */\n $clear(): void {\n debug('store', '$clear: emptying canvas');\n this.fromObject({\n nodes: [],\n edges: [],\n viewport: { x: 0, y: 0, zoom: 1 },\n });\n },\n\n // ── Undo / Redo ────────────────────────────────────────────\n\n /**\n * Undo the last structural change by popping a snapshot from the\n * history past stack. Rebuilds maps and deselects all after applying.\n */\n undo(): void {\n if (!ctx._history) return;\n const snapshot = ctx._history.undo({ nodes: ctx.nodes, edges: ctx.edges });\n if (snapshot) applyHistorySnapshot(ctx, snapshot, 'undo');\n },\n\n /**\n * Redo the last undone change by popping a snapshot from the\n * history future stack. Merges into existing objects and rebuilds maps.\n */\n redo(): void {\n if (!ctx._history) return;\n const snapshot = ctx._history.redo({ nodes: ctx.nodes, edges: ctx.edges });\n if (snapshot) applyHistorySnapshot(ctx, snapshot, 'redo');\n },\n\n /**\n * Whether an undo operation is available.\n */\n get canUndo(): boolean {\n return ctx._history?.canUndo ?? false;\n },\n\n /**\n * Whether a redo operation is available.\n */\n get canRedo(): boolean {\n return ctx._history?.canRedo ?? false;\n },\n };\n}\n","// ============================================================================\n// Edge Transitions — Draw (dashoffset) and Fade (opacity)\n//\n// Pure calculation functions for offset/opacity values plus DOM helpers\n// that apply the inline styles to SVG edge elements during animation.\n// ============================================================================\n\n// ── Pure calculations ───────────────────────────────────────────────────────\n\n/** Stroke-dashoffset for draw-in: totalLength → 0 as progress goes 0 → 1. */\nexport function drawInOffset(totalLength: number, progress: number): number {\n return totalLength * (1 - progress);\n}\n\n/** Stroke-dashoffset for draw-out: 0 → totalLength as progress goes 0 → 1. */\nexport function drawOutOffset(totalLength: number, progress: number): number {\n return totalLength * progress;\n}\n\n/** Opacity value for fade transitions. */\nexport function fadeOpacity(progress: number, direction: 'in' | 'out'): number {\n return direction === 'in' ? progress : 1 - progress;\n}\n\n// ── DOM helpers ─────────────────────────────────────────────────────────────\n\n/**\n * Apply draw transition styles to an SVG path element.\n * Re-reads getTotalLength() on each call so dynamic paths stay synchronized.\n *\n * Markers are hidden via inline CSS `marker-start/end: none` during draw-in\n * (until progress reaches 1) and during draw-out. Inline styles override the\n * SVG presentation attributes that Alpine's reactive effect may re-set at\n * any time, so this is race-condition-free.\n */\nexport function applyDrawTransition(\n pathEl: SVGPathElement,\n progress: number,\n direction: 'in' | 'out',\n): void {\n const totalLength = pathEl.getTotalLength();\n pathEl.style.strokeDasharray = String(totalLength);\n const offset = direction === 'in'\n ? drawInOffset(totalLength, progress)\n : drawOutOffset(totalLength, progress);\n pathEl.style.strokeDashoffset = String(offset);\n\n // Hide markers while the stroke is still drawing so arrowheads don't\n // appear before the line reaches the endpoint. Using inline style\n // `marker-*: none` overrides the SVG attribute without removing it.\n if (direction === 'in' && progress < 1) {\n pathEl.style.setProperty('marker-start', 'none');\n pathEl.style.setProperty('marker-end', 'none');\n } else if (direction === 'out') {\n pathEl.style.setProperty('marker-start', 'none');\n pathEl.style.setProperty('marker-end', 'none');\n }\n}\n\n/** Remove draw transition inline styles so the edge renders normally. */\nexport function cleanupDrawTransition(pathEl: SVGPathElement): void {\n pathEl.style.removeProperty('stroke-dasharray');\n pathEl.style.removeProperty('stroke-dashoffset');\n pathEl.style.removeProperty('marker-start');\n pathEl.style.removeProperty('marker-end');\n}\n\n/** Apply fade transition opacity to an element. */\nexport function applyFadeTransition(\n el: SVGElement | HTMLElement,\n progress: number,\n direction: 'in' | 'out',\n): void {\n el.style.opacity = String(fadeOpacity(progress, direction));\n}\n\n/** Remove fade transition inline styles. */\nexport function cleanupFadeTransition(el: SVGElement | HTMLElement): void {\n el.style.removeProperty('opacity');\n}\n","// ============================================================================\n// Path Functions — Factories that produce (t: number) => { x, y } functions\n//\n// Used by FlowTimeline's followPath feature to drive node positions along\n// arbitrary curves instead of linear interpolation.\n// ============================================================================\n\n/** Full circle in radians. */\nexport const TAU = Math.PI * 2;\n\n/** A function that maps normalised progress (0-1) to flow-space coordinates. */\nexport type PathFunction = (t: number) => { x: number; y: number };\n\n// ── SVG Path ────────────────────────────────────────────────────────────────\n\nconst SVG_PATH_CACHE = new Map();\nconst SVG_PATH_CACHE_MAX = 64;\n\n/**\n * Convert an SVG path `d` attribute string into a PathFunction.\n * Creates an off-screen element for geometry calculations.\n * Returns null in environments without SVG DOM support (e.g. Node/vitest).\n * Results are cached with an LRU policy (max 64 entries).\n */\nexport function svgPathToFunction(d: string): PathFunction | null {\n if (typeof document === 'undefined' || typeof document.createElementNS !== 'function') {\n return null;\n }\n\n const cached = SVG_PATH_CACHE.get(d);\n if (cached) return cached;\n\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n path.setAttribute('d', d);\n const totalLength = path.getTotalLength();\n\n const fn: PathFunction = (t: number) => {\n const pt = path.getPointAtLength(t * totalLength);\n return { x: pt.x, y: pt.y };\n };\n\n if (SVG_PATH_CACHE.size >= SVG_PATH_CACHE_MAX) {\n const firstKey = SVG_PATH_CACHE.keys().next().value;\n if (firstKey !== undefined) SVG_PATH_CACHE.delete(firstKey);\n }\n SVG_PATH_CACHE.set(d, fn);\n\n return fn;\n}\n\n/**\n * Clear the SVG path cache. Used for testing.\n */\nexport function _clearPathCache(): void {\n SVG_PATH_CACHE.clear();\n}\n\n// ── Orbit ───────────────────────────────────────────────────────────────────\n\nexport interface OrbitConfig {\n cx: number;\n cy: number;\n radius?: number;\n rx?: number;\n ry?: number;\n offset?: number;\n clockwise?: boolean;\n}\n\n/** Circular/elliptical motion around a center point. */\nexport function orbit(config: OrbitConfig): PathFunction {\n const { cx, cy, offset = 0, clockwise = true } = config;\n const rx = config.rx ?? config.radius ?? 100;\n const ry = config.ry ?? config.radius ?? 100;\n const dir = clockwise ? 1 : -1;\n\n return (t: number) => ({\n x: cx + rx * Math.cos(TAU * t * dir + offset * TAU),\n y: cy + ry * Math.sin(TAU * t * dir + offset * TAU),\n });\n}\n\n// ── Wave ────────────────────────────────────────────────────────────────────\n\nexport interface WaveConfig {\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n amplitude?: number;\n frequency?: number;\n offset?: number;\n}\n\n/** Sinusoidal oscillation along a start-end axis with perpendicular displacement. */\nexport function wave(config: WaveConfig): PathFunction {\n const { startX, startY, endX, endY, amplitude = 30, frequency = 1, offset = 0 } = config;\n const dx = endX - startX;\n const dy = endY - startY;\n const length = Math.sqrt(dx * dx + dy * dy);\n const ux = length > 0 ? dx / length : 1;\n const uy = length > 0 ? dy / length : 0;\n const px = -uy; // perpendicular\n const py = ux;\n\n return (t: number) => {\n const baseX = startX + dx * t;\n const baseY = startY + dy * t;\n const w = amplitude * Math.sin(TAU * frequency * t + offset * TAU);\n return { x: baseX + px * w, y: baseY + py * w };\n };\n}\n\n// ── Along ───────────────────────────────────────────────────────────────────\n\nexport interface AlongConfig {\n reverse?: boolean;\n startAt?: number;\n endAt?: number;\n}\n\n/** Convenience wrapper around svgPathToFunction with reverse/startAt/endAt. */\nexport function along(path: string, config?: AlongConfig): PathFunction | null {\n const baseFn = svgPathToFunction(path);\n if (!baseFn) return null;\n const { reverse = false, startAt = 0, endAt = 1 } = config ?? {};\n const range = endAt - startAt;\n return (t: number) => {\n let mapped = startAt + t * range;\n if (reverse) mapped = endAt - t * range;\n return baseFn(mapped);\n };\n}\n\n// ── Pendulum ────────────────────────────────────────────────────────────────\n\nexport interface PendulumConfig {\n cx: number;\n cy: number;\n radius: number;\n angle?: number;\n offset?: number;\n}\n\n/** Swinging arc around a pivot. Node hangs down (positive y) from the pivot. */\nexport function pendulum(config: PendulumConfig): PathFunction {\n const { cx, cy, radius, angle = 60, offset = 0 } = config;\n const maxRadians = (angle * Math.PI) / 180;\n return (t: number) => {\n const theta = maxRadians * Math.sin(TAU * t + offset * TAU);\n return {\n x: cx + radius * Math.sin(theta),\n y: cy + radius * Math.cos(theta),\n };\n };\n}\n\n// ── Drift ───────────────────────────────────────────────────────────────────\n\nexport interface DriftConfig {\n originX: number;\n originY: number;\n range?: number;\n speed?: number;\n seed?: number;\n}\n\n/** Smooth pseudo-random wandering using sine-sum noise. */\nexport function drift(config: DriftConfig): PathFunction {\n const { originX, originY, range = 20, speed = 1, seed = 0 } = config;\n const a1 = 1.0 + (seed % 7) * 0.3;\n const a2 = 1.3 + (seed % 11) * 0.2;\n const b1 = 0.7 + (seed % 13) * 0.25;\n const b2 = 1.1 + (seed % 17) * 0.15;\n return (t: number) => {\n const s = t * speed * TAU;\n const nx = (Math.sin(a1 * s) + Math.sin(a2 * s * 1.3)) / 2;\n const ny = (Math.sin(b1 * s * 0.9) + Math.sin(b2 * s * 1.1)) / 2;\n return { x: originX + nx * range, y: originY + ny * range };\n };\n}\n\n// ── Stagger ─────────────────────────────────────────────────────────────────\n\n/** Distributes offset values across items. */\nexport function stagger(\n amount: number,\n options?: { from?: number },\n): (index: number, total: number) => number {\n const from = options?.from ?? 0;\n return (index: number, _total: number) => from + index * amount;\n}\n","/**\n * Framework-agnostic deep clone helper.\n *\n * Uses `structuredClone` when possible (preserves Dates, Maps, Sets, RegExps,\n * typed arrays, etc.) and falls back to a JSON roundtrip when the input isn't\n * structured-cloneable — typically when a reactive proxy (Alpine, Vue, etc.)\n * or a plain object with function properties is passed in.\n *\n * The fallback silently drops non-JSON values (functions, Symbols, undefined).\n * A dev-mode warning fires once per session so consumers know when the\n * fallback is active.\n */\nlet warnedOnCloneFallback = false;\n\nexport function safeClone(value: T): T {\n try {\n return structuredClone(value);\n } catch {\n if (!warnedOnCloneFallback) {\n warnedOnCloneFallback = true;\n if (typeof console !== 'undefined') {\n console.warn(\n '[AlpineFlow] Cloning fell back to JSON roundtrip because structuredClone could not clone the input (likely a reactive proxy or an object with functions). Non-JSON values (functions, Symbols, Dates) will be stripped. This warning fires once per session.',\n );\n }\n }\n return JSON.parse(JSON.stringify(value));\n }\n}\n","// ============================================================================\n// FlowTimeline — Step-based animation sequencer\n//\n// Orchestrates animated changes to nodes and edges over time. Supports\n// sequential and parallel steps, pause/resume with context forwarding,\n// looping, reversal, lock mode, and event emission.\n//\n// Uses the shared AnimationEngine for frame scheduling.\n// Operates on plain objects (no Alpine or DOM dependency) — the canvas\n// integration layer is added in Phase 2.\n// ============================================================================\n\nimport { resolveEasing, checkReducedMotion, type EasingName } from './easing';\nimport { lerpNumber, lerpViewport, interpolateColor, parseStyle, interpolateStyle } from './interpolators';\nimport { applyDrawTransition, cleanupDrawTransition, applyFadeTransition, cleanupFadeTransition } from './edge-transitions';\nimport { AnimationEngine, type EngineHandle } from './engine';\nimport { svgPathToFunction, type PathFunction } from './paths';\nimport { safeClone } from './clone';\nimport type { FlowNode, FlowEdge, XYPosition, Dimensions, Viewport, AnimateTargets, AnimateOptions, FlowAnimationHandle, EdgeGradient } from '../core/types';\nimport type { EdgeAnimationMode } from '../core/types';\nimport { getNodesBounds, getViewportForBounds } from '../core/geometry';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from '../core/geometry';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport type StepContext = Record> = T & {\n stepIndex: number;\n stepId?: string;\n};\n\nexport interface TimelineStep = Record> {\n id?: string;\n\n // Node targeting\n nodes?: string[];\n position?: Partial;\n class?: string;\n dimensions?: Partial;\n style?: string | Record;\n data?: Record;\n selected?: boolean;\n zIndex?: number;\n\n /** Path function or SVG path string for curved motion. Overrides `position`. */\n followPath?: PathFunction | string;\n\n /** Optional visible guide path overlay for string-based followPath. */\n guidePath?: {\n visible?: boolean;\n class?: string;\n autoRemove?: boolean;\n };\n\n // Edge targeting\n edges?: string[];\n edgeAnimated?: boolean | EdgeAnimationMode;\n edgeColor?: string;\n edgeClass?: string;\n edgeLabel?: string;\n edgeStrokeWidth?: number;\n\n // Edge lifecycle\n addEdges?: FlowEdge[];\n removeEdges?: string[];\n edgeTransition?: 'none' | 'draw' | 'fade';\n\n // Viewport\n viewport?: Partial;\n fitView?: boolean;\n panTo?: string;\n fitViewPadding?: number;\n\n // Parallel container\n parallel?: TimelineStep[];\n\n // Timing\n duration?: number;\n easing?: EasingName | ((t: number) => number);\n delay?: number;\n lock?: boolean;\n\n // Awaitable\n /** Block this step until a promise resolves. */\n await?: Promise | { finished: Promise } | (() => Promise | { finished: Promise });\n /** If await doesn't resolve within this many ms, emit 'step-timeout' and advance. */\n timeout?: number;\n\n // Conditional execution\n /** Evaluated once at step activation. If false, the step is skipped (or the else branch runs). */\n when?: (ctx: StepContext) => boolean;\n /** Alternative step to execute when `when` returns false. */\n else?: TimelineStep;\n\n // Sub-timeline composition\n /** Nested timeline to play as a single step from this parent's point of view. */\n timeline?: FlowTimeline;\n /** When true, the sub-timeline is NOT paused/stopped when the parent pauses/stops. */\n independent?: boolean;\n\n // Hooks\n onStart?: (ctx: StepContext) => void;\n onProgress?: (progress: number, ctx: StepContext) => void;\n onComplete?: (ctx: StepContext) => void;\n}\n\ntype StepEntry = Record> =\n | { type: 'step'; config: TimelineStep | ((ctx: StepContext) => TimelineStep) }\n | { type: 'parallel'; configs: Array | ((ctx: StepContext) => TimelineStep)> }\n | { type: 'pause'; callback?: (resume: (context?: Record) => void) => void };\n\nexport type TimelineEvent =\n | 'play' | 'step' | 'step-complete' | 'step-skipped' | 'step-timeout'\n | 'pause' | 'resume' | 'interrupt' | 'complete' | 'reverse' | 'loop' | 'stop' | 'reset' | 'restart';\n\nexport type TimelineState = 'idle' | 'playing' | 'paused' | 'stopped';\n\n/** Minimal canvas interface the timeline needs — avoids coupling to the full canvas. */\nexport interface TimelineCanvas {\n nodes: FlowNode[];\n edges: FlowEdge[];\n getNode: (id: string) => FlowNode | undefined;\n getEdge: (id: string) => FlowEdge | undefined;\n /** The unified animation API. */\n animate: (targets: AnimateTargets, options?: AnimateOptions) => FlowAnimationHandle;\n /** Find the SVG path element for an edge (for draw transitions). */\n getEdgePathElement?: (id: string) => SVGPathElement | null;\n /** Find the edge group/wrapper element (for fade transitions). */\n getEdgeElement?: (id: string) => SVGElement | HTMLElement | null;\n /** Reactive viewport state. */\n viewport?: Viewport;\n /** Container dimensions for fitView/panTo calculations. */\n getContainerDimensions?: () => { width: number; height: number };\n /** Zoom constraints. */\n minZoom?: number;\n maxZoom?: number;\n /** The SVG element where guide paths should be inserted. */\n getEdgeSvgElement?: () => SVGSVGElement | null;\n /** When true, log warnings for missing targets. */\n debug?: boolean;\n /** Rebuild the edge lookup map after direct array mutations. */\n _rebuildEdgeMap?: () => void;\n}\n\n// ── Snapshot helpers ─────────────────────────────────────────────────────────\n\ninterface NodeSnapshot {\n position: XYPosition;\n class?: string;\n style?: string | Record;\n data: Record;\n dimensions?: Dimensions;\n selected?: boolean;\n zIndex?: number;\n}\n\ninterface EdgeSnapshot {\n animated?: boolean | EdgeAnimationMode;\n color?: string | EdgeGradient;\n class?: string;\n label?: string;\n strokeWidth?: number;\n}\n\nfunction snapshotNode(node: FlowNode): NodeSnapshot {\n return {\n position: { ...node.position },\n class: node.class,\n style: typeof node.style === 'string' ? node.style : node.style ? { ...node.style } : undefined,\n data: safeClone(node.data),\n dimensions: node.dimensions ? { ...node.dimensions } : undefined,\n selected: node.selected,\n zIndex: node.zIndex,\n };\n}\n\nfunction snapshotEdge(edge: FlowEdge): EdgeSnapshot {\n return {\n animated: edge.animated,\n color: edge.color,\n class: edge.class,\n label: edge.label,\n strokeWidth: edge.strokeWidth,\n };\n}\n\nfunction restoreNode(node: FlowNode, snap: NodeSnapshot): void {\n node.position.x = snap.position.x;\n node.position.y = snap.position.y;\n node.class = snap.class;\n node.style = snap.style;\n node.data = safeClone(snap.data);\n node.dimensions = snap.dimensions ? { ...snap.dimensions } : node.dimensions;\n node.selected = snap.selected;\n node.zIndex = snap.zIndex;\n}\n\n// ── Step execution context ──────────────────────────────────────────────────\n\n/** Bundles the computed values for a single step execution, replacing long parameter lists. */\ninterface StepExecutionContext> {\n step: TimelineStep;\n ctx: StepContext;\n duration: number;\n delay: number;\n easing: (t: number) => number;\n validNodeIds: string[] | undefined;\n validEdgeIds: string[] | undefined;\n resolvedPathFn: PathFunction | null;\n guidePathEl: SVGPathElement | null;\n nodeFromDimensions: Map;\n nodeFromStyles: Map>;\n edgeFromStrokeWidth: Map;\n edgeFromColor: Map;\n viewportFrom: Viewport | null;\n viewportTarget: Viewport | null;\n transition: string;\n addEdgeIds: string[];\n removeEdgeIds: string[];\n}\n\n// ── FlowTimeline ─────────────────────────────────────────────────────────────\n\nexport class FlowTimeline = Record> {\n private _canvas: TimelineCanvas;\n private _engine: AnimationEngine;\n private _entries: StepEntry[] = [];\n private _state: TimelineState = 'idle';\n private _reversed = false;\n private _loopCount = -1; // -1 = no loop\n private _lockEnabled = false;\n private _locked = false;\n private _respectReducedMotion: boolean | undefined = undefined; // undefined = auto-detect\n private _listeners = new Map>();\n private _context: Record = {};\n private _activeHandles: EngineHandle[] = [];\n private _subTimelines: FlowTimeline[] = [];\n private _initialSnapshot: Map = new Map();\n private _initialEdgeSnapshot: Map = new Map();\n private _playResolve: (() => void) | null = null;\n private _pauseWaiters = new Set<() => void>();\n private _tag?: string;\n\n constructor(canvas: TimelineCanvas, engine?: AnimationEngine) {\n this._canvas = canvas;\n this._engine = engine ?? new AnimationEngine();\n }\n\n // ── Public API ───────────────────────────────────────────────────────\n\n get state(): TimelineState {\n return this._state;\n }\n\n get locked(): boolean {\n return this._locked;\n }\n\n get subTimelines(): ReadonlyArray {\n return this._subTimelines;\n }\n\n get tag(): string | undefined {\n return this._tag;\n }\n\n setTag(tag: string): this {\n this._tag = tag;\n return this;\n }\n\n step(config: TimelineStep | ((ctx: StepContext) => TimelineStep)): this {\n this._entries.push({ type: 'step', config });\n return this;\n }\n\n parallel(steps: Array | ((ctx: StepContext) => TimelineStep)>): this {\n this._entries.push({ type: 'parallel', configs: steps });\n return this;\n }\n\n /**\n * Create and insert a sub-timeline via a builder callback.\n * Returns the sub-timeline for individual targeting.\n */\n timeline = TContext>(\n builder: (sub: FlowTimeline) => void,\n options?: { independent?: boolean },\n ): FlowTimeline {\n const sub = new FlowTimeline(this._canvas, this._engine);\n\n // Inherit tag from parent (unless independent)\n if (this._tag && !options?.independent) {\n sub.setTag(this._tag);\n }\n\n builder(sub);\n this._entries.push({\n type: 'step',\n config: { timeline: sub, independent: options?.independent } as TimelineStep,\n });\n return sub;\n }\n\n pause(callback?: (resume: (context?: Record) => void) => void): this {\n this._entries.push({ type: 'pause', callback });\n return this;\n }\n\n play(): Promise {\n return new Promise((resolve) => {\n this._playResolve = resolve;\n this._state = 'playing';\n if (this._lockEnabled) this._locked = true;\n this._captureInitialSnapshot();\n this._emit('play');\n this._context = {};\n this._runEntries(resolve);\n });\n }\n\n stop(): void {\n this._stopAll();\n // Stop all tracked (non-independent) sub-timelines\n for (const sub of this._subTimelines) {\n sub.stop();\n }\n this._subTimelines.length = 0;\n this._state = 'stopped';\n this._locked = false;\n this._emit('stop');\n this._playResolve?.();\n this._playResolve = null;\n }\n\n reset(replay?: boolean): Promise | void {\n if (replay) {\n console.warn('[AlpineFlow] timeline.reset(true) is deprecated. Use timeline.restart() instead.');\n return this.restart();\n }\n\n this._stopAll();\n // Stop all tracked sub-timelines on reset\n for (const sub of this._subTimelines) {\n sub.stop();\n }\n this._subTimelines.length = 0;\n this._restoreInitialSnapshot();\n this._state = 'idle';\n this._locked = false;\n this._emit('reset');\n }\n\n async restart(options?: { direction?: 'forward' | 'backward' }): Promise {\n // Stop everything currently running\n this._stopAll();\n\n // Stop all sub-timelines\n for (const sub of this._subTimelines) {\n sub.stop();\n }\n this._subTimelines.length = 0;\n\n // Revert to initial snapshot\n this._restoreInitialSnapshot();\n\n // Reset state\n this._state = 'idle';\n this._locked = false;\n\n // Apply direction if specified\n if (options?.direction === 'backward') {\n this._reversed = true;\n } else if (options?.direction === 'forward') {\n this._reversed = false;\n }\n\n // Emit restart event\n this._emit('restart');\n\n // Play from the beginning\n return this.play();\n }\n\n reverse(): this {\n this._reversed = !this._reversed;\n this._emit('reverse');\n return this;\n }\n\n loop(count?: number): this {\n this._loopCount = count ?? 0; // 0 = infinite\n return this;\n }\n\n lock(enabled?: boolean): this {\n this._lockEnabled = enabled ?? true;\n return this;\n }\n\n respectReducedMotion(enabled?: boolean): this {\n this._respectReducedMotion = enabled ?? true;\n return this;\n }\n\n on(event: TimelineEvent, handler: Function): this {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event)!.add(handler);\n return this;\n }\n\n /** Externally pause a playing timeline. Non-independent sub-timelines are also paused. */\n pausePlayback(): void {\n if (this._state !== 'playing') return;\n this._state = 'paused';\n if (this._lockEnabled) this._locked = false;\n // Propagate pause to running non-independent sub-timelines\n for (const sub of this._subTimelines) {\n if (sub.state === 'playing') sub.pausePlayback();\n }\n this._emit('pause');\n }\n\n /** Resume a paused timeline. Non-independent sub-timelines are also resumed. */\n resumePlayback(): void {\n if (this._state !== 'paused') return;\n this._state = 'playing';\n if (this._lockEnabled) this._locked = true;\n // Propagate resume to paused non-independent sub-timelines\n for (const sub of this._subTimelines) {\n if (sub.state === 'paused') sub.resumePlayback();\n }\n this._emit('resume');\n // Resolve all pause waiters\n for (const resolve of this._pauseWaiters) resolve();\n this._pauseWaiters.clear();\n }\n\n /** Check if reduced motion is active (OS preference + not opted out). */\n private _isReducedMotion(): boolean {\n return checkReducedMotion(this._respectReducedMotion);\n }\n\n // ── Internal: event emission ────────────────────────────────────────\n\n private _emit(event: TimelineEvent, detail?: any): void {\n const handlers = this._listeners.get(event);\n if (!handlers) return;\n for (const handler of handlers) {\n handler(detail);\n }\n }\n\n // ── Internal: snapshot management ───────────────────────────────────\n\n private _captureInitialSnapshot(): void {\n if (this._initialSnapshot.size > 0) return; // Only capture once\n for (const entry of this._entries) {\n this._captureEntryTargets(entry);\n }\n }\n\n private _captureEntryTargets(entry: StepEntry): void {\n if (entry.type === 'pause') return;\n\n const configs = entry.type === 'parallel' ? entry.configs : [entry.config];\n for (const config of configs) {\n const resolved = typeof config === 'function'\n ? null // Can't resolve dynamic steps at capture time\n : config;\n if (!resolved) continue;\n\n if (resolved.parallel) {\n for (const sub of resolved.parallel) {\n this._captureStepTargets(sub);\n }\n } else {\n this._captureStepTargets(resolved);\n }\n }\n }\n\n private _captureStepTargets(step: TimelineStep): void {\n if (step.nodes) {\n for (const id of step.nodes) {\n if (!this._initialSnapshot.has(id)) {\n const node = this._canvas.getNode(id);\n if (node) this._initialSnapshot.set(id, snapshotNode(node));\n }\n }\n }\n if (step.edges) {\n for (const id of step.edges) {\n if (!this._initialEdgeSnapshot.has(id)) {\n const edge = this._canvas.getEdge(id);\n if (edge) this._initialEdgeSnapshot.set(id, snapshotEdge(edge));\n }\n }\n }\n }\n\n private _restoreInitialSnapshot(): void {\n for (const [id, snap] of this._initialSnapshot) {\n const node = this._canvas.getNode(id);\n if (node) restoreNode(node, snap);\n }\n }\n\n // ── Internal: handle management ─────────────────────────────────────\n\n private _stopAll(): void {\n for (const h of this._activeHandles) {\n h.stop();\n }\n this._activeHandles = [];\n }\n\n // ── Internal: entry execution ───────────────────────────────────────\n\n private async _runEntries(resolve: () => void): Promise {\n const entries = this._reversed ? [...this._entries].reverse() : this._entries;\n let loopsRemaining = this._loopCount;\n\n const runOnce = async (): Promise => {\n for (let i = 0; i < entries.length; i++) {\n if (this._state === 'stopped') return;\n // Wait if externally paused via pausePlayback()\n if (this._state === 'paused') {\n await this._waitForResume();\n if ((this._state as TimelineState) === 'stopped') return;\n }\n\n const entry = entries[i];\n\n if (entry.type === 'pause') {\n await this._executePause(entry);\n continue;\n }\n\n if (entry.type === 'parallel') {\n await this._executeParallel(entry.configs, i);\n continue;\n }\n\n // Single step\n const config = entry.config;\n const resolved = typeof config === 'function'\n ? config(this._makeContext(i))\n : config;\n\n if (resolved.parallel) {\n await this._executeParallelSteps(resolved.parallel, i);\n } else {\n await this._executeStep(resolved, i);\n }\n }\n };\n\n await runOnce();\n\n // Handle looping\n if (this._state !== 'stopped' && loopsRemaining !== -1) {\n let infiniteIteration = 0;\n while ((this._state as TimelineState) !== 'stopped') {\n if (loopsRemaining === 0) {\n this._restoreInitialSnapshot();\n this._emit('loop', { iteration: infiniteIteration++ });\n try {\n await runOnce();\n } catch {\n resolve();\n return;\n }\n } else if (loopsRemaining > 0) {\n loopsRemaining--;\n this._restoreInitialSnapshot();\n this._emit('loop', { iteration: this._loopCount - loopsRemaining });\n await runOnce();\n if (loopsRemaining <= 0) break;\n } else {\n break;\n }\n }\n }\n\n if (this._state !== 'stopped') {\n this._state = 'idle';\n this._locked = false;\n this._emit('complete');\n }\n resolve();\n }\n\n private _makeContext(stepIndex: number, stepId?: string): StepContext {\n return {\n ...this._context,\n stepIndex,\n stepId,\n } as StepContext;\n }\n\n // ── Internal: pause-playback wait ────────────────────────────────────\n\n /** Block until resumePlayback() is called. Used by _runEntries when externally paused. */\n private _waitForResume(): Promise {\n return new Promise((resolve) => {\n this._pauseWaiters.add(resolve);\n });\n }\n\n // ── Internal: pause execution ───────────────────────────────────────\n\n private _executePause(entry: StepEntry & { type: 'pause' }): Promise {\n return new Promise((resolve) => {\n this._state = 'paused';\n if (this._lockEnabled) this._locked = false;\n this._emit('pause');\n\n const resume = (context?: Record): void => {\n if (context) {\n Object.assign(this._context, context);\n }\n this._state = 'playing';\n if (this._lockEnabled) this._locked = true;\n this._emit('resume');\n resolve();\n };\n\n entry.callback?.(resume);\n });\n }\n\n // ── Internal: parallel execution ────────────────────────────────────\n\n private async _executeParallel(\n configs: Array | ((ctx: StepContext) => TimelineStep)>,\n entryIndex: number,\n ): Promise {\n const resolved = configs.map((c) =>\n typeof c === 'function' ? c(this._makeContext(entryIndex)) : c,\n );\n await this._executeParallelSteps(resolved, entryIndex);\n }\n\n private async _executeParallelSteps(steps: TimelineStep[], entryIndex: number): Promise {\n const promises = steps.map((step) => this._executeStep(step, entryIndex));\n await Promise.all(promises);\n }\n\n // ── Internal: single step execution ─────────────────────────────────\n\n private async _executeStep(step: TimelineStep, entryIndex: number): Promise {\n const reducedMotion = this._isReducedMotion();\n const duration = reducedMotion ? 0 : (step.duration ?? 300);\n const delay = reducedMotion ? 0 : (step.delay ?? 0);\n const easing = resolveEasing(step.easing);\n const ctx = this._makeContext(entryIndex, step.id);\n\n // Conditional step evaluation\n if (step.when) {\n const shouldRun = step.when(ctx);\n if (!shouldRun) {\n if (step.else) {\n return this._executeStep(step.else, entryIndex);\n }\n this._emit('step-skipped', { index: entryIndex, id: step.id });\n return;\n }\n }\n\n // Sub-timeline composition — play the nested timeline as one step\n if (step.timeline) {\n const sub = step.timeline;\n\n // Inherit tag from parent (unless independent)\n if (this._tag && !step.independent) {\n sub.setTag(this._tag);\n }\n\n // Track non-independent subs for pause/stop propagation\n if (!step.independent) {\n this._subTimelines.push(sub);\n }\n\n this._emit('step', { index: entryIndex, id: step.id, timeline: sub });\n step.onStart?.(ctx);\n\n await sub.play();\n if (this._state === 'stopped') return;\n\n step.onComplete?.(ctx);\n this._emit('step-complete', { timeline: sub });\n\n // Clean up tracking after sub completes\n if (!step.independent) {\n const idx = this._subTimelines.indexOf(sub);\n if (idx >= 0) this._subTimelines.splice(idx, 1);\n }\n return;\n }\n\n this._emit('step', { index: entryIndex, id: step.id });\n step.onStart?.(ctx);\n\n // Resolve awaitable before proceeding with animation logic\n if (step.await) {\n await this._resolveAwait(step, entryIndex);\n if (this._state === 'stopped') return;\n }\n\n // Pure wait step — has await but no animation targets\n if (step.await && this._isAwaitOnlyStep(step)) {\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n return Promise.resolve();\n }\n\n const { validNodeIds, validEdgeIds } = this._validateStepTargets(step, entryIndex);\n\n if (this._isEmptyStep(step, validNodeIds, validEdgeIds)) {\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n return Promise.resolve();\n }\n\n const nodeFromDimensions = new Map();\n const nodeFromStyles = new Map>();\n this._captureNodeFromValues(step, validNodeIds, nodeFromDimensions, nodeFromStyles);\n\n const edgeFromStrokeWidth = new Map();\n const edgeFromColor = new Map();\n this._captureEdgeFromValues(step, validEdgeIds, edgeFromStrokeWidth, edgeFromColor);\n\n const resolvedPathFn = this._resolveFollowPath(step);\n const guidePathEl = this._createGuidePath(step);\n const hasViewportStep = !!(step.viewport || step.fitView || step.panTo);\n\n let viewportFrom: Viewport | null = null;\n let viewportTarget: Viewport | null = null;\n if (hasViewportStep && this._canvas.viewport) {\n viewportFrom = { ...this._canvas.viewport };\n viewportTarget = this._resolveTargetViewport(step);\n }\n\n const transition = step.edgeTransition ?? 'none';\n const addEdgeIds = step.addEdges?.map((e) => e.id) ?? [];\n const removeEdgeIds = step.removeEdges?.filter((id) => this._canvas.getEdge(id)).slice() ?? [];\n\n const execCtx: StepExecutionContext = {\n step, ctx, duration, delay, easing,\n validNodeIds, validEdgeIds, resolvedPathFn, guidePathEl,\n nodeFromDimensions, nodeFromStyles, edgeFromStrokeWidth, edgeFromColor,\n viewportFrom, viewportTarget, transition, addEdgeIds, removeEdgeIds,\n };\n\n // Instant step (duration: 0)\n if (duration === 0) {\n return this._executeInstantStep(execCtx);\n }\n\n // For animated transitions, add edges up front so the DOM renders paths.\n // Only await when the method actually performs async work (transition pre-hide),\n // so that synchronous callers (e.g. followPath without addEdges) don't get\n // deferred to a later microtask.\n const edgePrepPromise = this._prepareAnimatedEdges(step, transition, addEdgeIds);\n if (edgePrepPromise) {\n await edgePrepPromise;\n }\n\n // followPath steps retain engine-based interpolation — animate() can't handle custom path functions\n if (resolvedPathFn) {\n return this._executeFollowPathStep(execCtx);\n }\n\n // Animated step — delegate to canvas.animate()\n return this._executeAnimatedStep(execCtx);\n }\n\n // ── Step decomposition: target validation ──────────────────────────\n\n /** Filter node/edge IDs to only those present on the canvas; warn in debug mode. */\n private _validateStepTargets(\n step: TimelineStep,\n entryIndex: number,\n ): { validNodeIds: string[] | undefined; validEdgeIds: string[] | undefined } {\n let validNodeIds: string[] | undefined;\n let validEdgeIds: string[] | undefined;\n\n if (step.nodes) {\n validNodeIds = [];\n for (const id of step.nodes) {\n if (this._canvas.getNode(id)) {\n validNodeIds.push(id);\n } else if (this._canvas.debug) {\n console.warn(`[AlpineFlow] Animation step \"${step.id ?? entryIndex}\": node \"${id}\" not found, skipping`);\n }\n }\n }\n\n if (step.edges) {\n validEdgeIds = [];\n for (const id of step.edges) {\n if (this._canvas.getEdge(id)) {\n validEdgeIds.push(id);\n } else if (this._canvas.debug) {\n console.warn(`[AlpineFlow] Animation step \"${step.id ?? entryIndex}\": edge \"${id}\" not found, skipping`);\n }\n }\n }\n\n return { validNodeIds, validEdgeIds };\n }\n\n // ── Step decomposition: empty-step check ───────────────────────────\n\n /** Return true when the step targets nodes/edges but has zero valid targets and nothing else to do. */\n private _isEmptyStep(\n step: TimelineStep,\n validNodeIds: string[] | undefined,\n validEdgeIds: string[] | undefined,\n ): boolean {\n const hasTargetedNodes = step.nodes && step.nodes.length > 0;\n const hasTargetedEdges = step.edges && step.edges.length > 0;\n const hasViewportStep = !!(step.viewport || step.fitView || step.panTo);\n const hasEdgeLifecycle = !!(step.addEdges?.length || step.removeEdges?.length);\n const hasZeroValidNodeTargets = hasTargetedNodes && (!validNodeIds || validNodeIds.length === 0);\n const hasZeroValidEdgeTargets = hasTargetedEdges && (!validEdgeIds || validEdgeIds.length === 0);\n\n if (hasZeroValidNodeTargets && hasZeroValidEdgeTargets && !hasViewportStep && !hasEdgeLifecycle) {\n return true;\n }\n if (hasZeroValidNodeTargets && !hasTargetedEdges && !hasViewportStep && !hasEdgeLifecycle) {\n return true;\n }\n if (hasZeroValidEdgeTargets && !hasTargetedNodes && !hasViewportStep && !hasEdgeLifecycle) {\n return true;\n }\n return false;\n }\n\n /** Check whether a step has only an await and no animation targets. */\n private _isAwaitOnlyStep(step: TimelineStep): boolean {\n const hasNodes = step.nodes && step.nodes.length > 0;\n const hasEdges = step.edges && step.edges.length > 0;\n const hasViewport = !!(step.viewport || step.fitView || step.panTo);\n const hasEdgeLifecycle = !!(step.addEdges?.length || step.removeEdges?.length);\n return !hasNodes && !hasEdges && !hasViewport && !hasEdgeLifecycle;\n }\n\n // ── Step decomposition: resolve awaitable ─────────────────────────\n\n /** Normalize and await the step's `await` field (Promise, handle, or thunk). */\n private async _resolveAwait(step: TimelineStep, entryIndex: number): Promise {\n let target = step.await;\n if (!target) return;\n\n // Thunk form — evaluate lazily at step activation time\n if (typeof target === 'function') {\n target = (target as Function)();\n }\n\n // Handle-like form — unwrap .finished\n if (target && typeof target === 'object' && 'finished' in target && !(target instanceof Promise)) {\n target = (target as { finished: Promise }).finished;\n }\n\n if (!(target instanceof Promise)) return;\n\n // Apply timeout if specified\n if (step.timeout && step.timeout > 0) {\n let timerId: ReturnType | undefined;\n const timeoutPromise = new Promise<'timeout'>((resolve) => {\n timerId = setTimeout(() => resolve('timeout'), step.timeout);\n });\n const result = await Promise.race([target.then(() => 'resolved' as const), timeoutPromise]);\n if (timerId !== undefined) clearTimeout(timerId);\n if (result === 'timeout') {\n this._emit('step-timeout', { index: entryIndex, id: step.id });\n }\n } else {\n await target;\n }\n }\n\n // ── Step decomposition: capture from-values ────────────────────────\n\n /** Capture initial node dimensions and styles for interpolation. */\n private _captureNodeFromValues(\n step: TimelineStep,\n validNodeIds: string[] | undefined,\n nodeFromDimensions: Map,\n nodeFromStyles: Map>,\n ): void {\n if (!validNodeIds) return;\n for (const id of validNodeIds) {\n const node = this._canvas.getNode(id);\n if (!node) continue;\n if (node.dimensions && step.dimensions) {\n nodeFromDimensions.set(id, { ...node.dimensions });\n }\n if (step.style && node.style) {\n nodeFromStyles.set(id, parseStyle(node.style) as Record);\n }\n }\n }\n\n /** Capture initial edge strokeWidth and color for interpolation. */\n private _captureEdgeFromValues(\n step: TimelineStep,\n validEdgeIds: string[] | undefined,\n edgeFromStrokeWidth: Map,\n edgeFromColor: Map,\n ): void {\n if (!validEdgeIds) return;\n for (const id of validEdgeIds) {\n const edge = this._canvas.getEdge(id);\n if (!edge) continue;\n if (step.edgeStrokeWidth !== undefined && edge.strokeWidth !== undefined) {\n edgeFromStrokeWidth.set(id, edge.strokeWidth);\n }\n if (step.edgeColor !== undefined && edge.color !== undefined) {\n edgeFromColor.set(id, edge.color);\n }\n }\n }\n\n // ── Step decomposition: followPath & guide path ────────────────────\n\n /** Resolve a followPath config to a callable PathFunction. */\n private _resolveFollowPath(step: TimelineStep): PathFunction | null {\n if (!step.followPath) return null;\n if (typeof step.followPath === 'function') {\n return step.followPath;\n }\n const fn = svgPathToFunction(step.followPath);\n if (!fn && this._canvas.debug) {\n console.warn('[AlpineFlow] SVG path resolution unavailable (no DOM), followPath string ignored');\n }\n return fn;\n }\n\n /** Create a visible SVG guide path overlay for string-based followPath. */\n private _createGuidePath(step: TimelineStep): SVGPathElement | null {\n if (\n !step.guidePath?.visible ||\n typeof step.followPath !== 'string' ||\n typeof document === 'undefined'\n ) {\n return null;\n }\n\n const svgContainer = this._canvas.getEdgeSvgElement?.();\n if (!svgContainer) return null;\n\n const guidePathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n guidePathEl.setAttribute('d', step.followPath);\n guidePathEl.classList.add('flow-guide-path');\n if (step.guidePath.class) {\n guidePathEl.classList.add(step.guidePath.class);\n }\n svgContainer.appendChild(guidePathEl);\n return guidePathEl;\n }\n\n // ── Step decomposition: instant execution (duration: 0) ────────────\n\n /** Handle an instant step (duration === 0), optionally with a delay. */\n private _executeInstantStep(execCtx: StepExecutionContext): Promise {\n const { step, ctx, delay, resolvedPathFn, validNodeIds, guidePathEl } = execCtx;\n if (delay > 0) {\n return new Promise((resolve) => {\n const timerId = setTimeout(() => {\n this._applyStepFinal(step);\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n resolve();\n }, delay);\n // Track for cleanup\n const handle: EngineHandle = {\n stop() { clearTimeout(timerId); },\n };\n this._activeHandles.push(handle);\n });\n }\n if (resolvedPathFn && validNodeIds) {\n const pos = resolvedPathFn(1);\n for (const id of validNodeIds) {\n const node = this._canvas.getNode(id);\n if (node) {\n node.position.x = pos.x;\n node.position.y = pos.y;\n }\n }\n }\n this._applyStepFinal(step);\n // Guide path cleanup (instant)\n if (guidePathEl && step.guidePath?.autoRemove !== false) {\n guidePathEl.remove();\n }\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n return Promise.resolve();\n }\n\n // ── Step decomposition: pre-animation edge setup ───────────────────\n\n /** Add edges to the DOM and pre-hide them for transition animations. Returns a promise only when async work is needed. */\n private _prepareAnimatedEdges(\n step: TimelineStep,\n transition: string,\n addEdgeIds: string[],\n ): Promise | void {\n // For animated transitions, add edges up front so the DOM renders paths\n if (step.addEdges) {\n this._addEdges(step.addEdges);\n }\n\n // Pre-hide newly added edges so they don't flash for one frame.\n // Uses double queueMicrotask so Alpine's reactive effects (also microtask-\n // scheduled) have created the DOM elements before we apply initial styles.\n // This runs before the browser's next paint, unlike requestAnimationFrame\n // which fires in the next frame and causes a single-frame flash.\n if (transition !== 'none' && addEdgeIds.length && step.addEdges) {\n return new Promise((hideResolve) => {\n queueMicrotask(() => queueMicrotask(() => {\n if (transition === 'draw') {\n this._applyEdgeDrawTransition(addEdgeIds, 0, 'in');\n } else if (transition === 'fade') {\n this._applyEdgeFadeTransition(addEdgeIds, 0, 'in');\n }\n hideResolve();\n }));\n });\n }\n }\n\n // ── Step decomposition: followPath animation ───────────────────────\n\n /** Execute an animated step using engine-based interpolation for followPath. */\n private _executeFollowPathStep(execCtx: StepExecutionContext): Promise {\n const {\n step, ctx, duration, delay, easing,\n validNodeIds, validEdgeIds, nodeFromDimensions, nodeFromStyles,\n edgeFromStrokeWidth, edgeFromColor, viewportFrom, viewportTarget,\n transition, addEdgeIds, removeEdgeIds, guidePathEl,\n } = execCtx;\n // Guaranteed non-null: only called when resolvedPathFn is truthy\n const resolvedPathFn = execCtx.resolvedPathFn!;\n return new Promise((resolve) => {\n const handle = this._engine.register((elapsed) => {\n if (this._state === 'stopped') {\n resolve();\n return true; // stop\n }\n\n const rawProgress = Math.min(elapsed / duration, 1);\n const progress = easing(rawProgress);\n\n // followPath position from path function\n if (validNodeIds) {\n const pos = resolvedPathFn(progress);\n for (const id of validNodeIds) {\n const node = this._canvas.getNode(id);\n if (node) {\n node.position.x = pos.x;\n node.position.y = pos.y;\n }\n }\n }\n\n this._interpolateFollowPathTick(\n step, progress, validNodeIds, validEdgeIds,\n nodeFromDimensions, nodeFromStyles, edgeFromStrokeWidth, edgeFromColor,\n viewportFrom, viewportTarget,\n );\n\n this._tickEdgeTransitions(transition, addEdgeIds, removeEdgeIds, progress);\n\n step.onProgress?.(rawProgress, ctx);\n\n // Step complete\n if (rawProgress >= 1) {\n this._cleanupEdgeTransitions(transition, addEdgeIds, removeEdgeIds);\n\n if (removeEdgeIds.length) {\n this._removeEdges(removeEdgeIds);\n }\n\n this._applyStepInstant(step);\n\n if (guidePathEl && step.guidePath?.autoRemove !== false) {\n guidePathEl.remove();\n }\n\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n resolve();\n return true; // stop scheduling\n }\n\n return false; // continue\n }, delay);\n this._activeHandles.push(handle);\n });\n }\n\n /** Per-tick interpolation for properties during followPath animation. */\n private _interpolateFollowPathTick(\n step: TimelineStep,\n progress: number,\n validNodeIds: string[] | undefined,\n validEdgeIds: string[] | undefined,\n nodeFromDimensions: Map,\n nodeFromStyles: Map>,\n edgeFromStrokeWidth: Map,\n edgeFromColor: Map,\n viewportFrom: Viewport | null,\n viewportTarget: Viewport | null,\n ): void {\n // Interpolate node dimensions\n if (validNodeIds && step.dimensions) {\n for (const id of validNodeIds) {\n const node = this._canvas.getNode(id);\n const from = nodeFromDimensions.get(id);\n if (!node || !from || !node.dimensions) continue;\n\n if (step.dimensions.width !== undefined) {\n node.dimensions.width = lerpNumber(from.width, step.dimensions.width, progress);\n }\n if (step.dimensions.height !== undefined) {\n // A2: height control requires fixedDimensions to prevent the Alpine effect\n // from clearing inline height on leaf nodes. Timeline is authoritative here.\n node.fixedDimensions = true;\n node.dimensions.height = lerpNumber(from.height, step.dimensions.height, progress);\n }\n }\n }\n\n // Interpolate node styles\n if (validNodeIds && step.style) {\n const targetStyle = parseStyle(step.style);\n for (const id of validNodeIds) {\n const node = this._canvas.getNode(id);\n const from = nodeFromStyles.get(id);\n if (!node) continue;\n if (from) {\n node.style = interpolateStyle(from, targetStyle as Record, progress);\n }\n }\n }\n\n // Interpolate edge strokeWidth\n if (validEdgeIds && step.edgeStrokeWidth !== undefined) {\n for (const id of validEdgeIds) {\n const edge = this._canvas.getEdge(id);\n const from = edgeFromStrokeWidth.get(id);\n if (!edge) continue;\n if (from !== undefined) {\n edge.strokeWidth = lerpNumber(from, step.edgeStrokeWidth, progress);\n } else {\n edge.strokeWidth = step.edgeStrokeWidth;\n }\n }\n }\n\n // Interpolate edge color\n if (validEdgeIds && step.edgeColor !== undefined) {\n for (const id of validEdgeIds) {\n const edge = this._canvas.getEdge(id);\n const from = edgeFromColor.get(id);\n if (!edge) continue;\n if (from !== undefined && typeof from === 'string') {\n edge.color = interpolateColor(from, step.edgeColor, progress);\n } else if (from !== undefined) {\n edge.color = step.edgeColor;\n } else {\n edge.color = step.edgeColor;\n }\n }\n }\n\n // Interpolate viewport\n if (viewportFrom && viewportTarget && this._canvas.viewport) {\n const vp = lerpViewport(viewportFrom, viewportTarget, progress, {\n minZoom: this._canvas.minZoom,\n maxZoom: this._canvas.maxZoom,\n });\n this._canvas.viewport.x = vp.x;\n this._canvas.viewport.y = vp.y;\n this._canvas.viewport.zoom = vp.zoom;\n }\n }\n\n // ── Step decomposition: edge transition helpers ────────────────────\n\n /** Apply edge transitions (draw/fade) for a single animation tick. */\n private _tickEdgeTransitions(\n transition: string,\n addEdgeIds: string[],\n removeEdgeIds: string[],\n progress: number,\n ): void {\n if (transition === 'draw') {\n if (addEdgeIds.length) this._applyEdgeDrawTransition(addEdgeIds, progress, 'in');\n if (removeEdgeIds.length) this._applyEdgeDrawTransition(removeEdgeIds, progress, 'out');\n } else if (transition === 'fade') {\n if (addEdgeIds.length) this._applyEdgeFadeTransition(addEdgeIds, progress, 'in');\n if (removeEdgeIds.length) this._applyEdgeFadeTransition(removeEdgeIds, progress, 'out');\n }\n }\n\n /** Clean up edge transition styles at the end of animation. */\n private _cleanupEdgeTransitions(\n transition: string,\n addEdgeIds: string[],\n removeEdgeIds: string[],\n ): void {\n if (transition === 'draw') {\n this._cleanupEdgeDrawTransition(addEdgeIds);\n this._cleanupEdgeDrawTransition(removeEdgeIds);\n } else if (transition === 'fade') {\n this._cleanupEdgeFadeTransition(addEdgeIds);\n this._cleanupEdgeFadeTransition(removeEdgeIds);\n }\n }\n\n // ── Step decomposition: canvas.animate() execution ─────────────────\n\n /** Execute an animated step using canvas.animate() for standard interpolation. */\n private _executeAnimatedStep(execCtx: StepExecutionContext): Promise {\n const {\n step, ctx, duration, delay, validNodeIds, validEdgeIds,\n viewportFrom, viewportTarget, transition, addEdgeIds, removeEdgeIds, guidePathEl,\n } = execCtx;\n return new Promise((resolve) => {\n const animTargets = this._buildAnimateTargets(\n step, validNodeIds, validEdgeIds, viewportFrom, viewportTarget,\n );\n\n const hasAnimatableTargets = Object.keys(animTargets.nodes || {}).length > 0\n || Object.keys(animTargets.edges || {}).length > 0\n || animTargets.viewport;\n\n if (!hasAnimatableTargets && !addEdgeIds.length && !removeEdgeIds.length) {\n // Nothing to animate\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n resolve();\n return;\n }\n\n // If we have animatable targets, use canvas.animate()\n if (hasAnimatableTargets) {\n const animHandle = this._canvas.animate(animTargets, {\n duration,\n easing: step.easing,\n delay,\n onProgress: (progress) => {\n if (this._state === 'stopped') {\n animHandle.stop();\n resolve();\n return;\n }\n\n this._tickEdgeTransitions(transition, addEdgeIds, removeEdgeIds, progress);\n step.onProgress?.(progress, ctx);\n },\n onComplete: () => {\n this._cleanupEdgeTransitions(transition, addEdgeIds, removeEdgeIds);\n\n if (removeEdgeIds.length) {\n this._removeEdges(removeEdgeIds);\n }\n\n // Apply instant edge properties not handled by animate\n this._applyStepInstant(step);\n\n if (guidePathEl && step.guidePath?.autoRemove !== false) {\n guidePathEl.remove();\n }\n\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n resolve();\n },\n });\n\n this._activeHandles.push({ stop: () => animHandle.stop() });\n } else {\n // Only edge lifecycle, no interpolation needed — use engine for timing\n this._executeEdgeLifecycleOnly(execCtx, resolve);\n }\n });\n }\n\n /** Build AnimateTargets from step config for canvas.animate(). */\n private _buildAnimateTargets(\n step: TimelineStep,\n validNodeIds: string[] | undefined,\n validEdgeIds: string[] | undefined,\n viewportFrom: Viewport | null,\n viewportTarget: Viewport | null,\n ): AnimateTargets {\n const animTargets: AnimateTargets = {};\n\n if (validNodeIds) {\n animTargets.nodes = {};\n for (const id of validNodeIds) {\n const target: Record = {};\n if (step.position) target.position = { ...step.position };\n if (step.dimensions) target.dimensions = { ...step.dimensions };\n if (step.style !== undefined) target.style = step.style;\n if (step.class !== undefined) target.class = step.class;\n if (step.data !== undefined) target.data = step.data;\n if (step.selected !== undefined) target.selected = step.selected;\n if (step.zIndex !== undefined) target.zIndex = step.zIndex;\n animTargets.nodes[id] = target;\n }\n }\n\n if (validEdgeIds) {\n animTargets.edges = {};\n for (const id of validEdgeIds) {\n const target: Record = {};\n if (step.edgeColor !== undefined) target.color = step.edgeColor;\n if (step.edgeStrokeWidth !== undefined) target.strokeWidth = step.edgeStrokeWidth;\n if (step.edgeLabel !== undefined) target.label = step.edgeLabel;\n if (step.edgeAnimated !== undefined) target.animated = step.edgeAnimated;\n if (step.edgeClass !== undefined) target.class = step.edgeClass;\n animTargets.edges[id] = target;\n }\n }\n\n if (viewportTarget && viewportFrom) {\n animTargets.viewport = {\n pan: { x: viewportTarget.x, y: viewportTarget.y },\n zoom: viewportTarget.zoom,\n };\n }\n\n return animTargets;\n }\n\n /** Run edge lifecycle transitions (draw/fade) via the engine when there are no other animatable targets. */\n private _executeEdgeLifecycleOnly(\n execCtx: StepExecutionContext,\n resolve: () => void,\n ): void {\n const { step, ctx, duration, delay, transition, addEdgeIds, removeEdgeIds, guidePathEl } = execCtx;\n const handle = this._engine.register((elapsed) => {\n if (this._state === 'stopped') {\n resolve();\n return true;\n }\n\n const rawProgress = Math.min(elapsed / duration, 1);\n\n this._tickEdgeTransitions(transition, addEdgeIds, removeEdgeIds, rawProgress);\n\n step.onProgress?.(rawProgress, ctx);\n\n if (rawProgress >= 1) {\n this._cleanupEdgeTransitions(transition, addEdgeIds, removeEdgeIds);\n\n if (removeEdgeIds.length) {\n this._removeEdges(removeEdgeIds);\n }\n\n if (guidePathEl && step.guidePath?.autoRemove !== false) {\n guidePathEl.remove();\n }\n\n step.onProgress?.(1, ctx);\n step.onComplete?.(ctx);\n this._emit('step-complete');\n resolve();\n return true;\n }\n\n return false;\n }, delay);\n this._activeHandles.push(handle);\n }\n\n // ── Internal: apply step properties ─────────────────────────────────\n\n /** Apply all properties of a step at their final values (for instant steps). */\n private _applyStepFinal(step: TimelineStep): void {\n // Edge lifecycle (instant — no transition animation)\n if (step.addEdges) {\n this._addEdges(step.addEdges);\n }\n if (step.removeEdges) {\n this._removeEdges(step.removeEdges);\n }\n\n if (step.nodes) {\n for (const id of step.nodes) {\n const node = this._canvas.getNode(id);\n if (!node) continue;\n\n if (step.position) {\n if (step.position.x !== undefined) node.position.x = step.position.x;\n if (step.position.y !== undefined) node.position.y = step.position.y;\n }\n if (step.class !== undefined) node.class = step.class;\n if (step.data !== undefined) Object.assign(node.data, step.data);\n if (step.selected !== undefined) node.selected = step.selected;\n if (step.zIndex !== undefined) node.zIndex = step.zIndex;\n if (step.dimensions && node.dimensions) {\n if (step.dimensions.width !== undefined) node.dimensions.width = step.dimensions.width;\n if (step.dimensions.height !== undefined) {\n // A2: height control requires fixedDimensions to prevent the Alpine effect\n // from clearing inline height on leaf nodes. Timeline is authoritative here.\n node.fixedDimensions = true;\n node.dimensions.height = step.dimensions.height;\n }\n }\n if (step.style !== undefined) node.style = step.style;\n }\n }\n\n // Viewport\n this._applyViewportFinal(step);\n\n this._applyStepInstant(step);\n }\n\n /** Apply instant-swap edge properties (not interpolated). */\n private _applyStepInstant(step: TimelineStep): void {\n if (step.edges) {\n for (const id of step.edges) {\n const edge = this._canvas.getEdge(id);\n if (!edge) continue;\n\n if (step.edgeAnimated !== undefined) edge.animated = step.edgeAnimated;\n if (step.edgeClass !== undefined) edge.class = step.edgeClass;\n if (step.edgeLabel !== undefined) edge.label = step.edgeLabel;\n }\n }\n }\n\n // ── Internal: edge lifecycle ───────────────────────────────────────\n\n /** Add edges to the canvas edges array. */\n private _addEdges(edges: FlowEdge[]): void {\n this._canvas.edges.push(...edges);\n this._canvas._rebuildEdgeMap?.();\n }\n\n /** Remove edges from the canvas edges array by ID. */\n private _removeEdges(ids: string[]): void {\n for (const id of ids) {\n const idx = this._canvas.edges.findIndex((e) => e.id === id);\n if (idx !== -1) this._canvas.edges.splice(idx, 1);\n }\n this._canvas._rebuildEdgeMap?.();\n }\n\n /** Apply draw transition on each tick for added/removed edges. */\n private _applyEdgeDrawTransition(\n edgeIds: string[],\n progress: number,\n direction: 'in' | 'out',\n ): void {\n for (const id of edgeIds) {\n const pathEl = this._canvas.getEdgePathElement?.(id);\n if (pathEl) {\n applyDrawTransition(pathEl, progress, direction);\n }\n }\n }\n\n /** Clean up draw transition styles. */\n private _cleanupEdgeDrawTransition(edgeIds: string[]): void {\n for (const id of edgeIds) {\n const pathEl = this._canvas.getEdgePathElement?.(id);\n if (pathEl) {\n cleanupDrawTransition(pathEl);\n }\n }\n }\n\n /** Apply fade transition on each tick for added/removed edges. */\n private _applyEdgeFadeTransition(\n edgeIds: string[],\n progress: number,\n direction: 'in' | 'out',\n ): void {\n for (const id of edgeIds) {\n const el = this._canvas.getEdgeElement?.(id);\n if (el) {\n applyFadeTransition(el, progress, direction);\n }\n }\n }\n\n /** Clean up fade transition styles. */\n private _cleanupEdgeFadeTransition(edgeIds: string[]): void {\n for (const id of edgeIds) {\n const el = this._canvas.getEdgeElement?.(id);\n if (el) {\n cleanupFadeTransition(el);\n }\n }\n }\n\n // ── Internal: viewport helpers ──────────────────────────────────\n\n /** Compute the target viewport for a step (viewport, fitView, or panTo). */\n private _resolveTargetViewport(step: TimelineStep): Viewport | null {\n const vp = this._canvas.viewport;\n if (!vp) return null;\n\n if (step.fitView) {\n return this._computeFitViewViewport(step);\n }\n\n if (step.panTo) {\n return this._computePanToViewport(step.panTo);\n }\n\n if (step.viewport) {\n return {\n x: step.viewport.x ?? vp.x,\n y: step.viewport.y ?? vp.y,\n zoom: step.viewport.zoom ?? vp.zoom,\n };\n }\n\n return null;\n }\n\n /** Compute the viewport that fits all (or specified) nodes with padding. */\n private _computeFitViewViewport(step: TimelineStep): Viewport | null {\n const dims = this._canvas.getContainerDimensions?.();\n if (!dims) return null;\n\n const targetNodes = step.nodes\n ? step.nodes.map((id) => this._canvas.getNode(id)).filter((n): n is FlowNode => !!n)\n : this._canvas.nodes;\n\n if (targetNodes.length === 0) return null;\n\n const bounds = getNodesBounds(targetNodes);\n const padding = step.fitViewPadding ?? 0.1;\n return getViewportForBounds(\n bounds,\n dims.width,\n dims.height,\n this._canvas.minZoom ?? 0.5,\n this._canvas.maxZoom ?? 2,\n padding,\n );\n }\n\n /** Compute the viewport that centers on a given node. */\n private _computePanToViewport(nodeId: string): Viewport | null {\n const node = this._canvas.getNode(nodeId);\n if (!node) return null;\n\n const vp = this._canvas.viewport;\n if (!vp) return null;\n\n const dims = this._canvas.getContainerDimensions?.();\n if (!dims) return null;\n\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n const centerX = node.position.x + w / 2;\n const centerY = node.position.y + h / 2;\n\n return {\n x: dims.width / 2 - centerX * vp.zoom,\n y: dims.height / 2 - centerY * vp.zoom,\n zoom: vp.zoom,\n };\n }\n\n /** Apply viewport at final values (for instant steps). */\n private _applyViewportFinal(step: TimelineStep): void {\n const target = this._resolveTargetViewport(step);\n if (!target || !this._canvas.viewport) return;\n\n this._canvas.viewport.x = target.x;\n this._canvas.viewport.y = target.y;\n this._canvas.viewport.zoom = target.zoom;\n }\n}\n","import type { ParticleRenderer, ParticleOptions, ParticleRenderState } from '../core/types';\n\nconst RENDERER_REGISTRY = new Map();\n\nexport function registerParticleRenderer(name: string, renderer: ParticleRenderer): void {\n RENDERER_REGISTRY.set(name, renderer);\n}\n\nexport function getParticleRenderer(name: string): ParticleRenderer | undefined {\n return RENDERER_REGISTRY.get(name);\n}\n\nexport function hasParticleRenderer(name: string): boolean {\n return RENDERER_REGISTRY.has(name);\n}\n\n// Built-in: circle (default, back-compat)\nconst NS = 'http://www.w3.org/2000/svg';\n\nexport const circleRenderer: ParticleRenderer = {\n create(svgLayer, options) {\n const el = document.createElementNS(NS, 'circle');\n el.setAttribute('r', String(options.size ?? 4));\n el.setAttribute('fill', options.color ?? '#8B5CF6');\n el.classList.add('flow-edge-particle');\n if (options.class) {\n for (const cls of options.class.split(' ')) {\n if (cls) { el.classList.add(cls); }\n }\n }\n svgLayer.appendChild(el);\n return el;\n },\n update(el, { x, y }: ParticleRenderState) {\n el.setAttribute('cx', String(x));\n el.setAttribute('cy', String(y));\n },\n destroy(el) {\n el.remove();\n },\n};\n\n/** Orb renderer — two-circle group with pulsing scale driven by elapsed time. */\nexport const orbRenderer: ParticleRenderer = {\n create(svgLayer, options) {\n const g = document.createElementNS(NS, 'g');\n const size = options.size ?? 6;\n const color = options.color ?? '#8B5CF6';\n\n const outer = document.createElementNS(NS, 'circle');\n outer.setAttribute('r', String(size * 1.5));\n outer.setAttribute('fill', color);\n outer.setAttribute('opacity', '0.3');\n g.appendChild(outer);\n\n const inner = document.createElementNS(NS, 'circle');\n inner.setAttribute('r', String(size));\n inner.setAttribute('fill', color);\n g.appendChild(inner);\n\n if (options.class) {\n for (const cls of options.class.split(' ')) {\n if (cls) { g.classList.add(cls); }\n }\n }\n svgLayer.appendChild(g);\n return g;\n },\n update(el, { x, y, elapsed }) {\n const pulseRate = 2; // Hz — can be made configurable via data attributes later\n const pulseScale = 1 + 0.2 * Math.sin(elapsed * 0.001 * pulseRate * Math.PI * 2);\n el.setAttribute('transform', `translate(${x},${y}) scale(${pulseScale})`);\n },\n destroy(el) {\n el.remove();\n },\n};\n\nlet beamUid = 0;\n\n/**\n * Beam renderer — a traveling segment of the particle's path with an optional\n * multi-stop gradient painted along its length (tail→head). Clones the\n * backing SVG path and drives `stroke-dasharray`/`stroke-dashoffset` so the\n * beam bends with the path's curvature. A per-instance ``\n * whose orientation vector is updated each frame paints the gradient along\n * the chord from the beam's tail point to its head point.\n *\n * Options:\n * - `length`: beam length in SVG user units (default 30)\n * - `width`: beam thickness (stroke-width) in SVG user units (default 4)\n * - `color`: solid stroke color (used when `gradient` is not provided)\n * - `gradient`: array of `{ offset, color, opacity? }` stops (tail→head)\n *\n * Falls back to an oriented rectangle when no `pathEl` is available on the\n * render state (solid color only, gradient is ignored in fallback).\n */\nexport const beamRenderer: ParticleRenderer = {\n create(svgLayer, options) {\n const g = document.createElementNS(NS, 'g');\n (g as any).__beamLength = options.length ?? 30;\n (g as any).__beamWidth = options.width ?? 4;\n (g as any).__beamColor = options.color ?? '#8B5CF6';\n (g as any).__beamGradient = options.gradient;\n (g as any).__beamFollowThrough = options.followThrough ?? true;\n (g as any).__beamUid = `afbeam-${++beamUid}`;\n if (options.class) {\n for (const cls of options.class.split(' ')) {\n if (cls) { g.classList.add(cls); }\n }\n }\n svgLayer.appendChild(g);\n return g;\n },\n update(el, state) {\n const g = el as SVGGElement;\n const length = (g as any).__beamLength as number;\n const width = (g as any).__beamWidth as number;\n const color = (g as any).__beamColor as string;\n const gradient = (g as any).__beamGradient as ParticleOptions['gradient'];\n const uid = (g as any).__beamUid as string;\n\n if (state.pathEl) {\n let clone = (g as any).__pathClone as SVGPathElement | undefined;\n let gradientEl = (g as any).__gradient as SVGLinearGradientElement | undefined;\n\n if (!clone) {\n // Build the optional gradient def first so we can reference it\n // from the path's stroke attribute.\n let stroke = color;\n if (gradient && gradient.length > 0) {\n const defs = document.createElementNS(NS, 'defs');\n gradientEl = document.createElementNS(NS, 'linearGradient');\n gradientEl.setAttribute('id', uid);\n gradientEl.setAttribute('gradientUnits', 'userSpaceOnUse');\n for (const stop of gradient) {\n const stopEl = document.createElementNS(NS, 'stop');\n stopEl.setAttribute('offset', String(stop.offset));\n stopEl.setAttribute('stop-color', stop.color);\n if (stop.opacity !== undefined) {\n stopEl.setAttribute('stop-opacity', String(stop.opacity));\n }\n gradientEl.appendChild(stopEl);\n }\n defs.appendChild(gradientEl);\n g.appendChild(defs);\n stroke = `url(#${uid})`;\n (g as any).__gradient = gradientEl;\n }\n\n clone = document.createElementNS(NS, 'path');\n clone.setAttribute('d', state.pathEl.getAttribute('d') ?? '');\n clone.setAttribute('fill', 'none');\n // Use inline styles (not presentation attributes) so a default\n // `.flow-edges path { stroke: var(--flow-edge-stroke); … }`\n // theme rule cannot override the beam's stroke.\n clone.style.stroke = stroke;\n clone.style.strokeWidth = String(width);\n clone.style.strokeLinecap = 'round';\n clone.style.fill = 'none';\n if (!gradient) {\n clone.style.opacity = '0.85';\n }\n // Gap is the whole path length so the dash pattern period\n // (length + pathLength) never wraps within the visible path.\n // A shorter gap (e.g. pathLength - length) makes a phantom\n // copy of the dash appear at the opposite end at low progress.\n clone.setAttribute('stroke-dasharray', `${length} ${state.pathLength}`);\n g.appendChild(clone);\n (g as any).__pathClone = clone;\n }\n\n // Head travels from 0 to `pathLength + length` across the particle\n // lifetime so the tail can \"follow through\" after the head has\n // reached the target — the beam shrinks from the back as it exits\n // instead of vanishing the instant its head hits the endpoint.\n // When `followThrough: false`, fall back to the pre-v0.1.3 mapping\n // where `progress=1` means the head has just reached the target.\n const followThrough = (g as any).__beamFollowThrough as boolean;\n const headAt = followThrough\n ? state.progress * (state.pathLength + length)\n : state.progress * state.pathLength;\n const dashoffset = length - headAt;\n clone.setAttribute('stroke-dashoffset', String(dashoffset));\n\n // If we have a gradient, reorient it each frame to point from the\n // beam's tail to its head so the stops paint along the chord.\n // getPointAtLength naturally clamps at the path endpoints, which\n // keeps the bright head color at the target during the follow-\n // through phase.\n if (gradientEl) {\n const headLen = Math.max(0, Math.min(state.pathLength, headAt));\n const tailLen = Math.max(0, Math.min(state.pathLength, headAt - length));\n const headPt = state.pathEl.getPointAtLength(headLen);\n const tailPt = state.pathEl.getPointAtLength(tailLen);\n gradientEl.setAttribute('x1', String(tailPt.x));\n gradientEl.setAttribute('y1', String(tailPt.y));\n gradientEl.setAttribute('x2', String(headPt.x));\n gradientEl.setAttribute('y2', String(headPt.y));\n }\n return;\n }\n\n // Fallback: oriented rectangle for callers without a pathEl.\n let rect = (g as any).__fallbackRect as SVGRectElement | undefined;\n if (!rect) {\n rect = document.createElementNS(NS, 'rect');\n rect.setAttribute('width', String(length));\n rect.setAttribute('height', String(width));\n rect.setAttribute('rx', String(width / 2));\n rect.setAttribute('fill', color);\n rect.setAttribute('opacity', '0.8');\n g.appendChild(rect);\n (g as any).__fallbackRect = rect;\n }\n const angle = Math.atan2(state.velocity.y, state.velocity.x) * (180 / Math.PI);\n rect.setAttribute(\n 'transform',\n `translate(${state.x - length / 2},${state.y - width / 2}) rotate(${angle},${length / 2},${width / 2})`,\n );\n },\n destroy(el) {\n el.remove();\n },\n};\n\n/** Pulse renderer — expanding ripple ring that fades out over the particle lifetime. */\nexport const pulseRenderer: ParticleRenderer = {\n create(svgLayer, options) {\n const el = document.createElementNS(NS, 'circle');\n el.setAttribute('r', String(options.size ?? 6));\n el.setAttribute('fill', 'none');\n el.setAttribute('stroke', options.color ?? '#8B5CF6');\n el.setAttribute('stroke-width', '2');\n if (options.class) {\n for (const cls of options.class.split(' ')) {\n if (cls) { el.classList.add(cls); }\n }\n }\n svgLayer.appendChild(el);\n return el;\n },\n update(el, { x, y, progress }) {\n const maxScale = 3;\n const scale = 1 + progress * (maxScale - 1);\n const opacity = Math.max(0, 1 - progress);\n el.setAttribute('cx', '0');\n el.setAttribute('cy', '0');\n el.setAttribute('transform', `translate(${x},${y}) scale(${scale})`);\n el.setAttribute('opacity', String(opacity));\n },\n destroy(el) {\n el.remove();\n },\n};\n\n/** Image renderer — renders a user-provided SVG symbol (`#id`) or external image URL. */\nexport const imageRenderer: ParticleRenderer = {\n create(svgLayer, options) {\n const size = options.size ?? 16;\n const href = options.href ?? '';\n let el: SVGElement;\n\n if (href.startsWith('#')) {\n el = document.createElementNS(NS, 'use');\n el.setAttribute('href', href);\n el.setAttribute('width', String(size));\n el.setAttribute('height', String(size));\n } else {\n el = document.createElementNS(NS, 'image') as SVGElement;\n el.setAttribute('href', href);\n el.setAttribute('width', String(size));\n el.setAttribute('height', String(size));\n }\n\n if (options.class) {\n for (const cls of options.class.split(' ')) {\n if (cls) { el.classList.add(cls); }\n }\n }\n svgLayer.appendChild(el);\n return el;\n },\n update(el, { x, y }) {\n const size = parseFloat(el.getAttribute('width') ?? '16');\n el.setAttribute('x', String(x - size / 2));\n el.setAttribute('y', String(y - size / 2));\n },\n destroy(el) {\n el.remove();\n },\n};\n\nregisterParticleRenderer('circle', circleRenderer);\nregisterParticleRenderer('orb', orbRenderer);\nregisterParticleRenderer('beam', beamRenderer);\nregisterParticleRenderer('pulse', pulseRenderer);\nregisterParticleRenderer('image', imageRenderer);\n","// ============================================================================\n// canvas-particles — Particle system mixin for flow-canvas\n//\n// Public API: sendParticle, sendParticleAlongPath, sendParticleBetween.\n// Internal: _tickParticles, _fireParticleOnPath, destroyParticles.\n//\n// Manages SVG particles that travel along edge paths via\n// getPointAtLength. Particles share a single AnimationEngine registration\n// so multiple concurrent particles use one tick callback.\n//\n// Cross-mixin deps (via ctx): getEdge, getNode, getEdgePathElement,\n// getEdgeElement, getEdgeSvgElement, _edgeSvgElements, _containerStyles,\n// _activeParticles, _particleEngineHandle.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type {\n BurstOptions,\n ConvergingHandle,\n ConvergingOptions,\n ParticleBurstHandle,\n ParticleHandle,\n ParticleOptions,\n ParticleRenderState,\n XYPosition,\n} from '../../core/types';\nimport { engine } from '../../animate/engine';\nimport { debug } from '../../core/debug';\nimport { CONNECTION_ACTIVE_COLOR } from '../../core/constants';\nimport { getParticleRenderer } from '../../animate/particle-renderers';\n\n// ── Local utility ───────────────────────────────────────────────────────────\n\n/**\n * Warn-once latch for the beam `onComplete` + default-followThrough surprise.\n * The timing differs from other renderers; if a caller attaches an onComplete\n * without explicitly setting followThrough, we emit a single dev warning.\n */\nlet beamOnCompleteWarned = false;\n\n/** Parse a CSS duration string (e.g. '2s', '300ms') to milliseconds. */\nfunction parseDurationMs(dur: string): number {\n const match = dur.match(/^([\\d.]+)(ms|s)?$/);\n if (!match) return 2000;\n const val = parseFloat(match[1]);\n return match[2] === 'ms' ? val : val * 1000;\n}\n\n/**\n * Resolve final particle duration in ms from options, path length, and fallback.\n *\n * Priority:\n * 1. `speed` (SVG units/s) → `pathLength / speed * 1000`. Wins over `duration`.\n * 2. `duration` as a number → treated directly as milliseconds.\n * 3. `duration` as a CSS time string → parsed via `parseDurationMs`.\n * 4. `fallback` CSS time string.\n */\nexport function resolveDurationMs(\n options: ParticleOptions,\n pathLength: number,\n fallback: string,\n): number {\n if (options.speed !== undefined && options.speed > 0) {\n if (options.duration !== undefined) {\n console.warn('[AlpineFlow] Both speed and duration provided for particle; speed takes precedence.');\n }\n return (pathLength / options.speed) * 1000;\n }\n\n const raw = options.duration ?? fallback;\n if (typeof raw === 'number') {\n return raw;\n }\n return parseDurationMs(raw);\n}\n\n// ── Mixin factory ───────────────────────────────────────────────────────────\n\nexport function createParticleMixin(ctx: CanvasContext) {\n\n // ── Shared core: fire particle on a path element (closure-scoped) ────\n\n /**\n * Core particle-firing logic shared by sendParticle, sendParticleAlongPath,\n * and sendParticleBetween. Creates a particle element, registers it with\n * the engine, and returns a handle.\n *\n * @param pathEl - SVG path element to travel along\n * @param svgLayer - SVG element to host the particle element\n * @param options - Particle visual/behavioral options\n * @param extra - Optional hooks: onComplete wrapper, size/color/duration overrides\n */\n function _fireParticleOnPath(\n pathEl: SVGPathElement,\n svgLayer: SVGElement,\n options: ParticleOptions = {},\n extra: {\n size?: number;\n color?: string;\n durationFallback?: string;\n wrapOnComplete?: (original: () => void) => () => void;\n } = {},\n ): ParticleHandle | undefined {\n // Resolve renderer\n const rendererName = options.renderer ?? 'circle';\n const renderer = getParticleRenderer(rendererName);\n if (!renderer) {\n debug('particle', `_fireParticleOnPath: unknown renderer \"${rendererName}\"`);\n return undefined;\n }\n\n // Warn once per session if a beam caller passed `onComplete` without\n // explicitly opting out of follow-through — `onComplete` fires when the\n // tail fully exits, not when the head reaches the target, which surprises\n // devs expecting circle/orb/pulse semantics.\n if (\n rendererName === 'beam'\n && typeof options.onComplete === 'function'\n && options.followThrough === undefined\n && !beamOnCompleteWarned\n ) {\n beamOnCompleteWarned = true;\n console.warn(\n '[AlpineFlow] beam `onComplete` fires after the tail exits the path '\n + '(follow-through is on by default). Pass `followThrough: false` if '\n + 'you want `onComplete` to fire when the head reaches the target.',\n );\n }\n\n const styles = ctx._containerStyles;\n\n const size = options.size\n ?? extra.size\n ?? (parseFloat(styles?.getPropertyValue('--flow-edge-dot-size').trim() ?? '4') || 4);\n const color = options.color\n ?? extra.color\n ?? styles?.getPropertyValue('--flow-edge-dot-fill').trim()\n ?? CONNECTION_ACTIVE_COLOR;\n const durationFallback = extra.durationFallback\n ?? styles?.getPropertyValue('--flow-edge-dot-duration').trim()\n ?? '2s';\n\n const pathLength = pathEl.getTotalLength();\n const ms = resolveDurationMs(options, pathLength, durationFallback);\n\n // Delegate element creation to the renderer\n const resolvedOptions: ParticleOptions = { ...options, size, color };\n const el = renderer.create(svgLayer, resolvedOptions);\n\n // Position at path start immediately — no origin flash.\n const startPt = pathEl.getPointAtLength(0);\n const initialState: ParticleRenderState = {\n x: startPt.x,\n y: startPt.y,\n progress: 0,\n velocity: { x: 0, y: 0 },\n pathLength,\n elapsed: 0,\n pathEl,\n };\n renderer.update(el, initialState);\n\n // Promise for handle.finished\n let resolveHandleFinished: () => void;\n const handleFinished = new Promise((r) => { resolveHandleFinished = r; });\n\n const baseOnComplete = () => {\n if (typeof options.onComplete === 'function') {\n options.onComplete();\n }\n resolveHandleFinished!();\n };\n\n const wrappedOnComplete = extra.wrapOnComplete\n ? extra.wrapOnComplete(baseOnComplete)\n : baseOnComplete;\n\n const particle = {\n element: el,\n renderer,\n pathEl,\n startElapsed: -1, // set on first engine tick\n ms,\n onComplete: wrappedOnComplete,\n currentPosition: { x: startPt.x, y: startPt.y },\n };\n ctx._activeParticles.add(particle);\n\n // Register on shared AnimationEngine if not already running\n if (!ctx._particleEngineHandle) {\n ctx._particleEngineHandle = engine.register((elapsed) => ctx._tickParticles(elapsed));\n }\n\n const handle: ParticleHandle = {\n getCurrentPosition(): XYPosition | null {\n if (!ctx._activeParticles.has(particle)) return null;\n return { ...particle.currentPosition };\n },\n stop() {\n if (!ctx._activeParticles.has(particle)) return;\n particle.renderer.destroy(particle.element);\n ctx._activeParticles.delete(particle);\n wrappedOnComplete();\n },\n get finished() { return handleFinished; },\n };\n\n return handle;\n }\n\n /**\n * Fire a particle along an arbitrary SVG path string.\n * Closure-scoped so both sendParticleAlongPath and sendParticleBetween\n * can call it without `this` binding issues.\n */\n function _sendParticleAlongPath(svgPath: string, options: ParticleOptions = {}): ParticleHandle | undefined {\n const svgLayer = ctx.getEdgeSvgElement?.();\n if (!svgLayer) {\n debug('particle', 'sendParticleAlongPath: SVG layer unavailable');\n return undefined;\n }\n\n // Create a temporary hidden element for getPointAtLength\n const tempPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n tempPath.setAttribute('d', svgPath);\n tempPath.style.display = 'none';\n svgLayer.appendChild(tempPath);\n\n const handle = _fireParticleOnPath(tempPath, svgLayer, options, {\n wrapOnComplete: (original) => () => {\n original();\n tempPath.remove();\n },\n });\n\n if (!handle) {\n // Cleanup temp path if _fireParticleOnPath failed\n tempPath.remove();\n return undefined;\n }\n\n debug('particle', 'sendParticleAlongPath', { path: svgPath.slice(0, 40) });\n\n return handle;\n }\n\n return {\n // ── Particle tick loop ────────────────────────────────────────────────\n\n /**\n * Engine tick callback — processes all active particles in one pass.\n * Receives `elapsed` (ms since engine registration) from the engine.\n * Returns true to unregister from engine when all particles are done.\n */\n _tickParticles(elapsed: number): boolean {\n const lengthCache = new Map();\n\n for (const particle of ctx._activeParticles) {\n // Set startElapsed on first tick (deferred from sendParticle)\n if (particle.startElapsed < 0) {\n particle.startElapsed = elapsed;\n }\n\n const progress = (elapsed - particle.startElapsed) / particle.ms;\n\n // Complete particle if progress finished or if the DOM element was detached.\n if (progress >= 1 || !particle.element.parentNode) {\n particle.renderer.destroy(particle.element);\n if (typeof particle.onComplete === 'function') {\n particle.onComplete();\n }\n ctx._activeParticles.delete(particle);\n continue;\n }\n\n let len = lengthCache.get(particle.pathEl);\n if (len === undefined) {\n len = particle.pathEl.getTotalLength();\n lengthCache.set(particle.pathEl, len);\n }\n\n const pt = particle.pathEl.getPointAtLength(progress * len);\n\n // Compute render state for the pluggable renderer\n const renderState: ParticleRenderState = {\n x: pt.x,\n y: pt.y,\n progress,\n velocity: {\n x: pt.x - particle.currentPosition.x,\n y: pt.y - particle.currentPosition.y,\n },\n pathLength: len,\n elapsed: elapsed - particle.startElapsed,\n pathEl: particle.pathEl,\n };\n\n particle.renderer.update(particle.element, renderState);\n\n // Track position internally so getCurrentPosition() doesn't need DOM reads\n particle.currentPosition = { x: pt.x, y: pt.y };\n }\n\n // Return true to unregister from engine when all particles are done\n if (ctx._activeParticles.size === 0) {\n ctx._particleEngineHandle = null;\n return true;\n }\n return false;\n },\n\n // ── Send particle along edge ──────────────────────────────────────────\n\n /**\n * Fire a particle along an edge path. The particle is an SVG element\n * that follows the edge's `` element using `getPointAtLength`.\n */\n sendParticle(edgeId: string, options: ParticleOptions = {}): ParticleHandle | undefined {\n // Skip particles on culled (hidden) edges — zero wasted work off-screen\n const svg = ctx._edgeSvgElements.get(edgeId);\n if (svg && svg.style.display === 'none') return undefined;\n\n const edge = ctx.getEdge(edgeId);\n if (!edge) {\n debug('particle', `sendParticle: edge \"${edgeId}\" not found`);\n return undefined;\n }\n\n const pathEl = ctx.getEdgePathElement(edgeId);\n if (!pathEl) {\n debug('particle', `sendParticle: no path element for edge \"${edgeId}\"`);\n return undefined;\n }\n\n const pathD = pathEl.getAttribute('d');\n if (!pathD) {\n debug('particle', `sendParticle: edge \"${edgeId}\" path has no d attribute`);\n return undefined;\n }\n\n const gEl = ctx.getEdgeElement(edgeId);\n if (!gEl) return undefined;\n\n // Resolve cascade: options -> edge properties -> CSS variables\n const styles = ctx._containerStyles;\n\n const size = options.size\n ?? edge.particleSize\n ?? (parseFloat(styles?.getPropertyValue('--flow-edge-dot-size').trim() ?? '4') || 4);\n const color = options.color\n ?? edge.particleColor\n ?? styles?.getPropertyValue('--flow-edge-dot-fill').trim()\n ?? CONNECTION_ACTIVE_COLOR;\n const durationFallback = edge.animationDuration\n ?? styles?.getPropertyValue('--flow-edge-dot-duration').trim()\n ?? '2s';\n\n const handle = _fireParticleOnPath(pathEl, gEl as SVGElement, options, {\n size,\n color,\n durationFallback,\n });\n\n if (handle) {\n debug('particle', `sendParticle on edge \"${edgeId}\"`, { size, color, duration: options.duration });\n }\n\n return handle;\n },\n\n // ── Send particle along arbitrary SVG path ───────────────────────────\n\n /**\n * Fire a particle along an arbitrary SVG path string, not tied to an\n * existing edge. A temporary invisible `` element is injected\n * into the edge SVG layer and removed when the particle finishes.\n */\n sendParticleAlongPath(svgPath: string, options: ParticleOptions = {}): ParticleHandle | undefined {\n return _sendParticleAlongPath(svgPath, options);\n },\n\n // ── Send particle between two nodes ──────────────────────────────────\n\n /**\n * Fire a particle along a straight line between two node centers.\n * Delegates to sendParticleAlongPath after computing the SVG path.\n */\n sendParticleBetween(sourceNodeId: string, targetNodeId: string, options: ParticleOptions = {}): ParticleHandle | undefined {\n const sourceNode = ctx.getNode(sourceNodeId);\n if (!sourceNode) {\n debug('particle', `sendParticleBetween: source node \"${sourceNodeId}\" not found`);\n return undefined;\n }\n\n const targetNode = ctx.getNode(targetNodeId);\n if (!targetNode) {\n debug('particle', `sendParticleBetween: target node \"${targetNodeId}\" not found`);\n return undefined;\n }\n\n // Compute center positions\n const sx = sourceNode.position.x + (sourceNode.dimensions?.width ?? 150) / 2;\n const sy = sourceNode.position.y + (sourceNode.dimensions?.height ?? 40) / 2;\n const tx = targetNode.position.x + (targetNode.dimensions?.width ?? 150) / 2;\n const ty = targetNode.position.y + (targetNode.dimensions?.height ?? 40) / 2;\n\n const pathD = `M ${sx} ${sy} L ${tx} ${ty}`;\n\n debug('particle', `sendParticleBetween \"${sourceNodeId}\" -> \"${targetNodeId}\"`, { path: pathD });\n\n return _sendParticleAlongPath(pathD, options);\n },\n\n // ── Burst: sequenced multi-particle emission ─────────────────────────\n\n /**\n * Fire multiple particles along a single edge with staggered timing.\n * An optional `variant` function customizes each particle individually.\n */\n sendParticleBurst(edgeId: string, options: BurstOptions): ParticleBurstHandle {\n const { count, stagger = 100, variant, ...baseOptions } = options;\n const handles: (ParticleHandle | undefined)[] = [];\n const pendingTimers: ReturnType[] = [];\n\n for (let i = 0; i < count; i++) {\n const particleOptions: ParticleOptions = variant\n ? { ...baseOptions, ...variant(i, count) }\n : { ...baseOptions };\n\n if (i === 0) {\n handles.push(this.sendParticle(edgeId, particleOptions));\n } else {\n const timer = setTimeout(() => {\n handles.push(this.sendParticle(edgeId, particleOptions));\n }, i * stagger);\n pendingTimers.push(timer);\n }\n }\n\n const validHandles = () => handles.filter((h): h is ParticleHandle => h != null);\n\n return {\n get handles() { return validHandles(); },\n get finished() {\n return new Promise((resolve) => {\n // Wait for all staggered particles to be created, then await all\n setTimeout(() => {\n Promise.all(validHandles().map(h => h.finished)).then(() => resolve());\n }, count * stagger + 50);\n });\n },\n stopAll() {\n for (const timer of pendingTimers) {\n clearTimeout(timer);\n }\n for (const h of validHandles()) {\n h.stop();\n }\n },\n };\n },\n\n // ── Converging: fan-in particle visualization ───────────────────────\n\n /**\n * Fire particles from multiple edges that all arrive at (or depart from)\n * a target node simultaneously. For 'arrival' synchronization, shorter\n * paths get shorter durations and delayed starts so all particles reach\n * the target at the same time.\n */\n sendConverging(sourceEdgeIds: string[], options: ConvergingOptions): ConvergingHandle {\n const { targetNodeId: _targetNodeId, synchronize = 'arrival', onAllArrived, ...particleOptions } = options;\n const handles: ParticleHandle[] = [];\n const pendingTimers: ReturnType[] = [];\n\n if (synchronize === 'arrival') {\n // Compute path lengths for proportional timing\n const edgeData = sourceEdgeIds.map(id => {\n const pathEl = ctx.getEdgePathElement(id);\n const length = pathEl?.getTotalLength() ?? 0;\n return { id, length };\n }).filter(d => d.length > 0);\n\n if (edgeData.length === 0) {\n const emptyFinished = Promise.resolve();\n return { get handles() { return []; }, finished: emptyFinished, stopAll() {} };\n }\n\n const maxLength = Math.max(...edgeData.map(d => d.length));\n const baseDuration = resolveDurationMs(particleOptions, maxLength, '2s');\n\n for (const { id, length } of edgeData) {\n const ratio = length / maxLength;\n const adjustedDuration = baseDuration * ratio;\n const delay = baseDuration - adjustedDuration;\n\n if (delay <= 0) {\n const h = this.sendParticle(id, { ...particleOptions, duration: adjustedDuration });\n if (h) {\n handles.push(h);\n }\n } else {\n const timer = setTimeout(() => {\n const h = this.sendParticle(id, { ...particleOptions, duration: adjustedDuration });\n if (h) {\n handles.push(h);\n }\n }, delay);\n pendingTimers.push(timer);\n }\n }\n } else {\n // departure: all start simultaneously with their default durations\n for (const edgeId of sourceEdgeIds) {\n const h = this.sendParticle(edgeId, particleOptions);\n if (h) {\n handles.push(h);\n }\n }\n }\n\n const finished = new Promise((resolve) => {\n // Small buffer for staggered particles to register\n const waitTime = synchronize === 'arrival' ? 100 : 0;\n setTimeout(() => {\n Promise.all(handles.map(h => h.finished)).then(() => {\n onAllArrived?.();\n resolve();\n });\n }, waitTime);\n });\n\n return {\n get handles() { return handles; },\n finished,\n stopAll() {\n for (const timer of pendingTimers) {\n clearTimeout(timer);\n }\n for (const h of handles) {\n h.stop();\n }\n },\n };\n },\n\n // ── Cleanup ───────────────────────────────────────────────────────────\n\n /**\n * Stop the particle engine and remove all active particles from the DOM.\n * Called during canvas destroy().\n */\n destroyParticles(): void {\n ctx._particleEngineHandle?.stop();\n ctx._particleEngineHandle = null;\n for (const p of ctx._activeParticles) {\n p.renderer.destroy(p.element);\n }\n ctx._activeParticles.clear();\n },\n };\n}\n","import type { AnimateTargets, AnimateOptions, FlowAnimationHandle, StopOptions, ParticleOptions, ParticleHandle, BurstOptions, ParticleBurstHandle, ConvergingOptions, ConvergingHandle } from '../core/types';\n\nexport interface FlowGroupHost {\n animate(targets: AnimateTargets, options?: AnimateOptions): FlowAnimationHandle;\n update(targets: AnimateTargets, options?: AnimateOptions): FlowAnimationHandle;\n sendParticle?(edgeId: string, options?: ParticleOptions): ParticleHandle | undefined;\n sendParticleAlongPath?(svgPath: string, options?: ParticleOptions): ParticleHandle | undefined;\n sendParticleBetween?(sourceNodeId: string, targetNodeId: string, options?: ParticleOptions): ParticleHandle | undefined;\n sendParticleBurst?(edgeId: string, options: BurstOptions): ParticleBurstHandle;\n sendConverging?(sourceEdgeIds: string[], options: ConvergingOptions): ConvergingHandle;\n timeline?(): any;\n getHandles(filter?: { tag?: string; tags?: string[] }): FlowAnimationHandle[];\n cancelAll(filter: { tag?: string; tags?: string[] }, options?: StopOptions): void;\n pauseAll(filter: { tag?: string; tags?: string[] }): void;\n resumeAll(filter: { tag?: string; tags?: string[] }): void;\n}\n\nexport class FlowGroup {\n readonly name: string;\n private _host: FlowGroupHost;\n\n constructor(name: string, host: FlowGroupHost) {\n this.name = name;\n this._host = host;\n }\n\n animate(targets: AnimateTargets, options?: AnimateOptions): FlowAnimationHandle {\n const mergedTags = [...(options?.tags ?? [])];\n if (options?.tag) mergedTags.push(options.tag);\n return this._host.animate(targets, { ...options, tag: this.name, tags: mergedTags });\n }\n\n update(targets: AnimateTargets, options?: AnimateOptions): FlowAnimationHandle {\n const mergedTags = [...(options?.tags ?? [])];\n if (options?.tag) mergedTags.push(options.tag);\n return this._host.update(targets, { ...options, tag: this.name, tags: mergedTags });\n }\n\n sendParticle(edgeId: string, options?: ParticleOptions): ParticleHandle | undefined {\n return this._host.sendParticle?.(edgeId, { ...options, tag: this.name } as ParticleOptions);\n }\n\n sendParticleAlongPath(svgPath: string, options?: ParticleOptions): ParticleHandle | undefined {\n return this._host.sendParticleAlongPath?.(svgPath, { ...options, tag: this.name } as ParticleOptions);\n }\n\n sendParticleBetween(sourceNodeId: string, targetNodeId: string, options?: ParticleOptions): ParticleHandle | undefined {\n return this._host.sendParticleBetween?.(sourceNodeId, targetNodeId, { ...options, tag: this.name } as ParticleOptions);\n }\n\n sendParticleBurst(edgeId: string, options: BurstOptions): ParticleBurstHandle {\n return this._host.sendParticleBurst!(edgeId, { ...options, tag: this.name } as BurstOptions);\n }\n\n sendConverging(sourceEdgeIds: string[], options: ConvergingOptions): ConvergingHandle {\n return this._host.sendConverging!(sourceEdgeIds, { ...options, tag: this.name } as ConvergingOptions);\n }\n\n timeline(): any {\n const tl = this._host.timeline?.();\n if (tl && typeof tl.setTag === 'function') {\n tl.setTag(this.name);\n }\n return tl;\n }\n\n cancelAll(options?: StopOptions): void {\n this._host.cancelAll({ tag: this.name }, options);\n }\n\n pauseAll(): void {\n this._host.pauseAll({ tag: this.name });\n }\n\n resumeAll(): void {\n this._host.resumeAll({ tag: this.name });\n }\n\n get handles(): FlowAnimationHandle[] {\n return this._host.getHandles({ tag: this.name });\n }\n}\n","import type { FlowNode, FlowEdge, Viewport, AnimateTargets } from '../../core/types';\nimport type { EasingName } from '../easing';\nimport type { MotionConfig } from '../motion';\n\nexport interface CanvasSnapshot {\n nodes: Record>;\n edges: Record>;\n viewport: Viewport;\n}\n\nexport type RecordingEventType =\n | 'animate' | 'update'\n | 'particle' | 'particle-along-path' | 'particle-between'\n | 'particle-burst' | 'converging'\n | 'timeline-play' | 'timeline-step' | 'timeline-complete'\n | 'node-add' | 'node-remove'\n | 'edge-add' | 'edge-remove'\n | 'viewport-change';\n\nexport interface RecordingEvent {\n t: number;\n type: RecordingEventType;\n args: Record;\n source?: string;\n}\n\nexport interface InFlightAnimation {\n handleId: string;\n type: 'eased' | 'spring' | 'decay' | 'inertia' | 'keyframes';\n targets: AnimateTargets;\n startTime: number;\n duration?: number;\n easing?: EasingName;\n motion?: MotionConfig;\n direction: 'forward' | 'backward';\n integratorState?: Record;\n currentValues: Record;\n /**\n * Baseline values at animation start — snapshotted when animate() was called.\n * For eased animations, rehydration needs this to correctly compute mid-flight\n * lerp positions. Populated by Recorder; optional for back-compat.\n */\n fromValues?: Record;\n}\n\nexport interface Checkpoint {\n t: number;\n canvas: CanvasSnapshot;\n inFlight: InFlightAnimation[];\n tagRegistry: Record;\n}\n\nexport interface RecordingData {\n version: number;\n duration: number;\n initialState: CanvasSnapshot;\n events: RecordingEvent[];\n checkpoints: Checkpoint[];\n metadata?: Record;\n}\n\nexport const RECORDING_VERSION = 1;\n","// ============================================================================\n// VirtualEngine — headless animation replay\n//\n// A pure, headless animation engine that reproduces canvas state\n// deterministically from a Recording. It mirrors the real Animator/Engine\n// but operates on virtual time, has no DOM bindings, and no rAF loop.\n//\n// The replay driver advances the engine by calling `advance(dt)` per virtual\n// frame and `applyEvent(event)` when recorded events need to be replayed.\n// ============================================================================\n\nimport type { AnimateTargets } from '../../core/types';\nimport type {\n CanvasSnapshot, RecordingEvent, Checkpoint, InFlightAnimation,\n} from './types';\nimport type {\n MotionConfig, SpringMotion, DecayMotion, InertiaMotion,\n KeyframesMotion, PhysicsState,\n} from '../motion';\nimport {\n stepSpring, stepDecay, stepInertia, stepKeyframes, resolveMotion,\n} from '../motion';\nimport { resolveEasing, type EasingFn } from '../easing';\nimport { lerpNumber } from '../interpolators';\nimport { safeClone } from '../clone';\n\n/** Fixed virtual time step for deterministic replay (60 fps). */\nexport const REPLAY_DT = 1 / 60;\n\n/** Runtime-extended in-flight animation with resolved physics/easing state. */\ninterface ActiveVirtualAnim extends InFlightAnimation {\n /** Per-key physics state (only for physics types). */\n _physicsStates?: Map;\n /** Resolved easing function (eased type only). */\n _easingFn?: EasingFn;\n /** Snapshot of from-values at animation start (eased only). */\n _from?: Record;\n /** Resolved motion config (physics only). */\n _motion?: MotionConfig;\n}\n\n/**\n * Headless replay engine. Constructed from an initial `CanvasSnapshot` and\n * driven by the replay layer through `advance()` and `applyEvent()`.\n */\nexport class VirtualEngine {\n private _state: CanvasSnapshot;\n private _virtualTime = 0;\n private _inFlight = new Map();\n\n constructor(initialState: CanvasSnapshot) {\n this._state = safeClone(initialState);\n }\n\n /** Current virtual time in milliseconds. */\n get virtualTime(): number {\n return this._virtualTime;\n }\n\n /** Number of currently in-flight animations. */\n get inFlightCount(): number {\n return this._inFlight.size;\n }\n\n /** Return a deep-cloned copy of the current virtual canvas state. */\n getState(): CanvasSnapshot {\n return safeClone(this._state);\n }\n\n /** Advance virtual clock by `dt` seconds and step all in-flight animations. */\n advance(dt: number): void {\n if (dt <= 0) {\n return;\n }\n this._virtualTime += dt * 1000;\n\n for (const [handleId, anim] of this._inFlight) {\n this._stepAnimation(anim, dt);\n if (this._isSettled(anim)) {\n this._inFlight.delete(handleId);\n }\n }\n }\n\n /** Apply a recorded event to the virtual state. */\n applyEvent(event: RecordingEvent): void {\n switch (event.type) {\n case 'animate':\n case 'update':\n this._applyAnimate(event);\n break;\n case 'node-add': {\n const nodes = event.args.nodes;\n if (Array.isArray(nodes)) {\n for (const n of nodes) {\n if (n?.id) {\n this._state.nodes[n.id] = safeClone(n);\n }\n }\n } else if (nodes?.id) {\n this._state.nodes[nodes.id] = safeClone(nodes);\n } else if (event.args.id && event.args.node) {\n // Legacy shape support\n this._state.nodes[event.args.id] = safeClone(event.args.node);\n }\n break;\n }\n case 'node-remove': {\n const ids = event.args.ids;\n if (Array.isArray(ids)) {\n for (const id of ids) {\n delete this._state.nodes[id];\n }\n } else if (typeof ids === 'string') {\n delete this._state.nodes[ids];\n } else if (event.args.id) {\n // Legacy shape support\n delete this._state.nodes[event.args.id];\n }\n break;\n }\n case 'edge-add': {\n const edges = event.args.edges;\n if (Array.isArray(edges)) {\n for (const e of edges) {\n if (e?.id) {\n this._state.edges[e.id] = safeClone(e);\n }\n }\n } else if (edges?.id) {\n this._state.edges[edges.id] = safeClone(edges);\n } else if (event.args.id && event.args.edge) {\n // Legacy shape support\n this._state.edges[event.args.id] = safeClone(event.args.edge);\n }\n break;\n }\n case 'edge-remove': {\n const ids = event.args.ids;\n if (Array.isArray(ids)) {\n for (const id of ids) {\n delete this._state.edges[id];\n }\n } else if (typeof ids === 'string') {\n delete this._state.edges[ids];\n } else if (event.args.id) {\n // Legacy shape support\n delete this._state.edges[event.args.id];\n }\n break;\n }\n case 'viewport-change':\n Object.assign(this._state.viewport, event.args);\n break;\n // Particle events are visual-only and have no effect on headless state.\n case 'particle':\n case 'particle-along-path':\n case 'particle-between':\n case 'particle-burst':\n case 'converging':\n break;\n // Timeline events are structural markers; no virtual state change here.\n case 'timeline-play':\n case 'timeline-step':\n case 'timeline-complete':\n break;\n }\n }\n\n /** Restore the engine from a Checkpoint. */\n restoreCheckpoint(cp: Checkpoint): void {\n this._state = safeClone(cp.canvas);\n this._virtualTime = cp.t;\n this._inFlight.clear();\n for (const anim of cp.inFlight) {\n const active: ActiveVirtualAnim = safeClone(anim) as ActiveVirtualAnim;\n this._rehydrateAnim(active);\n this._inFlight.set(active.handleId, active);\n }\n }\n\n /** Capture the current engine state as a serializable Checkpoint payload. */\n captureCheckpointData(): Omit {\n return {\n canvas: safeClone(this._state),\n inFlight: [...this._inFlight.values()].map((anim) => this._serializeAnim(anim)),\n tagRegistry: {},\n };\n }\n\n // ── Private helpers ───────────────────────────────────────────────────────\n\n private _applyAnimate(event: RecordingEvent): void {\n const handleId: string = event.args.handleId\n ?? `virt-${this._virtualTime.toFixed(3)}-${this._inFlight.size}`;\n if (!event.args.handleId) {\n console.warn('[AlpineFlow VirtualEngine] animate event missing handleId — determinism not guaranteed for this event');\n }\n const targets: AnimateTargets = event.args.targets ?? {};\n const options = event.args.options ?? {};\n\n const motion = options.motion;\n const resolvedMotion = motion ? resolveMotion(motion) ?? undefined : undefined;\n\n const anim: ActiveVirtualAnim = {\n handleId,\n type: resolvedMotion ? resolvedMotion.type : 'eased',\n targets: safeClone(targets),\n startTime: this._virtualTime,\n duration: options.duration,\n easing: options.easing,\n motion: resolvedMotion,\n direction: 'forward',\n currentValues: {},\n _motion: resolvedMotion,\n };\n\n this._initAnim(anim);\n this._inFlight.set(handleId, anim);\n }\n\n private _initAnim(anim: ActiveVirtualAnim): void {\n const from: Record = {};\n const to: Record = {};\n this._collectNumericProperties(anim.targets, from, to, this._state);\n\n anim._from = from;\n\n if (anim.type === 'eased') {\n anim._easingFn = resolveEasing(anim.easing);\n } else {\n anim._physicsStates = new Map();\n for (const key of Object.keys(from)) {\n anim._physicsStates.set(key, {\n value: from[key],\n velocity: 0,\n target: to[key] ?? from[key],\n settled: false,\n });\n }\n }\n }\n\n private _collectNumericProperties(\n targets: AnimateTargets,\n from: Record,\n to: Record,\n state: CanvasSnapshot,\n ): void {\n for (const [nodeId, target] of Object.entries(targets.nodes ?? {})) {\n const node = state.nodes[nodeId];\n if (!node) {\n continue;\n }\n const position = (target as { position?: { x?: number; y?: number } }).position;\n if (position?.x !== undefined) {\n from[`nodes.${nodeId}.position.x`] = node.position?.x ?? 0;\n to[`nodes.${nodeId}.position.x`] = position.x;\n }\n if (position?.y !== undefined) {\n from[`nodes.${nodeId}.position.y`] = node.position?.y ?? 0;\n to[`nodes.${nodeId}.position.y`] = position.y;\n }\n }\n\n const viewport = targets.viewport;\n if (viewport?.pan?.x !== undefined) {\n from['viewport.x'] = state.viewport.x;\n to['viewport.x'] = viewport.pan.x;\n }\n if (viewport?.pan?.y !== undefined) {\n from['viewport.y'] = state.viewport.y;\n to['viewport.y'] = viewport.pan.y;\n }\n if (viewport?.zoom !== undefined) {\n from['viewport.zoom'] = state.viewport.zoom;\n to['viewport.zoom'] = viewport.zoom;\n }\n }\n\n private _rehydrateAnim(anim: ActiveVirtualAnim): void {\n anim._motion = anim.motion;\n\n if (anim.type === 'eased') {\n anim._easingFn = resolveEasing(anim.easing);\n // Prefer the baseline captured at animate() call time — lets\n // _stepEased compute correct lerp(from, to, eased(elapsed/dur))\n // from the original startTime. Fall back to currentValues for\n // recordings captured before fromValues was added.\n anim._from = anim.fromValues\n ? { ...anim.fromValues }\n : { ...(anim.currentValues ?? {}) };\n return;\n }\n\n if (anim.integratorState) {\n anim._physicsStates = new Map();\n for (const [key, is] of Object.entries(anim.integratorState)) {\n anim._physicsStates.set(key, {\n value: is.value ?? 0,\n velocity: is.velocity ?? 0,\n target: is.target ?? 0,\n settled: is.settled ?? false,\n });\n }\n }\n }\n\n private _serializeAnim(anim: ActiveVirtualAnim): InFlightAnimation {\n const integratorState: InFlightAnimation['integratorState'] = {};\n if (anim._physicsStates) {\n for (const [key, state] of anim._physicsStates) {\n integratorState[key] = {\n velocity: state.velocity,\n value: state.value,\n target: state.target,\n settled: state.settled,\n };\n }\n }\n\n return safeClone({\n handleId: anim.handleId,\n type: anim.type,\n targets: anim.targets,\n startTime: anim.startTime,\n duration: anim.duration,\n easing: anim.easing,\n motion: anim.motion,\n direction: anim.direction,\n integratorState: anim._physicsStates ? integratorState : anim.integratorState,\n currentValues: anim.currentValues,\n fromValues: anim.fromValues,\n });\n }\n\n private _stepAnimation(anim: ActiveVirtualAnim, dt: number): void {\n if (anim.type === 'eased') {\n this._stepEased(anim, dt);\n } else if (anim._physicsStates) {\n this._stepPhysics(anim, dt);\n }\n }\n\n private _stepEased(anim: ActiveVirtualAnim, _dt: number): void {\n if (!anim.duration || !anim._easingFn || !anim._from) {\n return;\n }\n const elapsed = this._virtualTime - anim.startTime;\n const rawProgress = anim.duration > 0\n ? Math.min(elapsed / anim.duration, 1)\n : 1;\n const easedProgress = anim._easingFn(rawProgress);\n\n for (const key of Object.keys(anim._from)) {\n const from = anim._from[key];\n const to = this._getTargetValue(key, anim.targets) ?? from;\n const value = lerpNumber(from, to, easedProgress);\n anim.currentValues[key] = value;\n this._applyValueToState(key, value);\n }\n }\n\n private _stepPhysics(anim: ActiveVirtualAnim, dt: number): void {\n if (!anim._physicsStates || !anim._motion) {\n return;\n }\n\n const motion = anim._motion;\n for (const [key, state] of anim._physicsStates) {\n if (state.settled) {\n continue;\n }\n\n switch (motion.type) {\n case 'spring':\n stepSpring(state, motion as SpringMotion, dt);\n break;\n case 'decay':\n stepDecay(state, motion as DecayMotion, dt);\n break;\n case 'inertia':\n stepInertia(state, motion as InertiaMotion, dt, key);\n break;\n case 'keyframes': {\n const kf = motion as KeyframesMotion;\n const kfDuration = kf.duration ?? 5000;\n const progress = kfDuration > 0\n ? Math.min((this._virtualTime - anim.startTime) / kfDuration, 1)\n : 1;\n stepKeyframes(state, kf, progress, key);\n if (progress >= 1) {\n state.settled = true;\n }\n break;\n }\n }\n\n anim.currentValues[key] = state.value;\n this._applyValueToState(key, state.value);\n }\n }\n\n private _getTargetValue(key: string, targets: AnimateTargets): number | undefined {\n const parts = key.split('.');\n if (parts[0] === 'nodes' && parts.length >= 4) {\n const nodeId = parts[1];\n const nodeTarget = targets.nodes?.[nodeId] as { position?: { x?: number; y?: number } } | undefined;\n if (!nodeTarget) {\n return undefined;\n }\n if (parts[2] === 'position' && parts[3] === 'x') {\n return nodeTarget.position?.x;\n }\n if (parts[2] === 'position' && parts[3] === 'y') {\n return nodeTarget.position?.y;\n }\n }\n if (parts[0] === 'viewport') {\n const vp = targets.viewport;\n if (parts[1] === 'x') return vp?.pan?.x;\n if (parts[1] === 'y') return vp?.pan?.y;\n if (parts[1] === 'zoom') return vp?.zoom;\n }\n return undefined;\n }\n\n private _applyValueToState(key: string, value: number): void {\n const parts = key.split('.');\n if (parts[0] === 'nodes' && parts.length >= 4) {\n const nodeId = parts[1];\n const node = this._state.nodes[nodeId];\n if (!node) {\n return;\n }\n if (!node.position) {\n node.position = { x: 0, y: 0 };\n }\n if (parts[2] === 'position') {\n if (parts[3] === 'x') node.position.x = value;\n if (parts[3] === 'y') node.position.y = value;\n }\n return;\n }\n if (parts[0] === 'viewport') {\n if (parts[1] === 'x') this._state.viewport.x = value;\n if (parts[1] === 'y') this._state.viewport.y = value;\n if (parts[1] === 'zoom') this._state.viewport.zoom = value;\n }\n }\n\n private _isSettled(anim: ActiveVirtualAnim): boolean {\n if (anim.type === 'eased') {\n const elapsed = this._virtualTime - anim.startTime;\n return elapsed >= (anim.duration ?? 0);\n }\n if (anim._physicsStates) {\n for (const state of anim._physicsStates.values()) {\n if (!state.settled) {\n return false;\n }\n }\n return true;\n }\n return true;\n }\n}\n","import type { CanvasSnapshot, InFlightAnimation } from './types';\n\nexport interface ThumbnailRenderOptions {\n width: number;\n height: number;\n inFlight?: InFlightAnimation[];\n}\n\nexport interface ThumbnailRenderer {\n render: (state: CanvasSnapshot, options: ThumbnailRenderOptions) => string;\n}\n\nconst REGISTRY = new Map();\n\nexport function registerThumbnailRenderer(name: string, renderer: ThumbnailRenderer): void {\n REGISTRY.set(name, renderer);\n}\n\nexport function getThumbnailRenderer(name: string): ThumbnailRenderer | undefined {\n return REGISTRY.get(name);\n}\n\nexport function hasThumbnailRenderer(name: string): boolean {\n return REGISTRY.has(name);\n}\n\nfunction computeBoundingBox(\n nodes: CanvasSnapshot['nodes'],\n pad: number = 20,\n): { minX: number; minY: number; vbWidth: number; vbHeight: number } | null {\n const nodeList = Object.values(nodes);\n if (nodeList.length === 0) {\n return null;\n }\n\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const node of nodeList) {\n const nx = node.position?.x ?? 0;\n const ny = node.position?.y ?? 0;\n const nw = node.dimensions?.width ?? 150;\n const nh = node.dimensions?.height ?? 40;\n minX = Math.min(minX, nx);\n minY = Math.min(minY, ny);\n maxX = Math.max(maxX, nx + nw);\n maxY = Math.max(maxY, ny + nh);\n }\n\n minX -= pad;\n minY -= pad;\n maxX += pad;\n maxY += pad;\n\n return { minX, minY, vbWidth: maxX - minX, vbHeight: maxY - minY };\n}\n\nfunction buildEdgeLines(state: CanvasSnapshot): string {\n let out = '';\n for (const edge of Object.values(state.edges)) {\n const source = state.nodes[edge.source!];\n const target = state.nodes[edge.target!];\n if (!source || !target) { continue; }\n const sx = (source.position?.x ?? 0) + (source.dimensions?.width ?? 150) / 2;\n const sy = (source.position?.y ?? 0) + (source.dimensions?.height ?? 40) / 2;\n const tx = (target.position?.x ?? 0) + (target.dimensions?.width ?? 150) / 2;\n const ty = (target.position?.y ?? 0) + (target.dimensions?.height ?? 40) / 2;\n out += ``;\n }\n return out;\n}\n\nexport const faithfulRenderer: ThumbnailRenderer = {\n render(state, { width, height }) {\n const nodes = Object.values(state.nodes);\n if (nodes.length === 0) {\n return ``;\n }\n\n const bbox = computeBoundingBox(state.nodes);\n if (!bbox) {\n return ``;\n }\n\n const { minX, minY, vbWidth, vbHeight } = bbox;\n let svg = ``;\n\n svg += buildEdgeLines(state);\n\n for (const node of nodes) {\n const nx = node.position?.x ?? 0;\n const ny = node.position?.y ?? 0;\n const nw = node.dimensions?.width ?? 150;\n const nh = node.dimensions?.height ?? 40;\n svg += ``;\n }\n\n svg += '';\n return svg;\n },\n};\n\nexport const outlineRenderer: ThumbnailRenderer = {\n render(state, { width, height }) {\n const nodes = Object.values(state.nodes);\n if (nodes.length === 0) {\n return ``;\n }\n\n const bbox = computeBoundingBox(state.nodes);\n if (!bbox) {\n return ``;\n }\n\n const { minX, minY, vbWidth, vbHeight } = bbox;\n let svg = ``;\n\n // Edges slightly stronger than faithful\n for (const edge of Object.values(state.edges)) {\n const source = state.nodes[edge.source!];\n const target = state.nodes[edge.target!];\n if (!source || !target) { continue; }\n const sx = (source.position?.x ?? 0) + (source.dimensions?.width ?? 150) / 2;\n const sy = (source.position?.y ?? 0) + (source.dimensions?.height ?? 40) / 2;\n const tx = (target.position?.x ?? 0) + (target.dimensions?.width ?? 150) / 2;\n const ty = (target.position?.y ?? 0) + (target.dimensions?.height ?? 40) / 2;\n svg += ``;\n }\n\n // Nodes as outlines only (no fill)\n for (const node of nodes) {\n const nx = node.position?.x ?? 0;\n const ny = node.position?.y ?? 0;\n const nw = node.dimensions?.width ?? 150;\n const nh = node.dimensions?.height ?? 40;\n svg += ``;\n }\n\n svg += '';\n return svg;\n },\n};\n\nexport const activityRenderer: ThumbnailRenderer = {\n render(state, { width, height, inFlight }) {\n const nodes = Object.values(state.nodes);\n if (nodes.length === 0) {\n return ``;\n }\n\n const bbox = computeBoundingBox(state.nodes);\n if (!bbox) {\n return ``;\n }\n\n // Extract active node IDs from inFlight animations\n const activeNodeIds = new Set();\n if (inFlight) {\n for (const anim of inFlight) {\n if (anim.targets?.nodes) {\n for (const id of Object.keys(anim.targets.nodes)) {\n activeNodeIds.add(id);\n }\n }\n }\n }\n\n const { minX, minY, vbWidth, vbHeight } = bbox;\n let svg = ``;\n\n svg += buildEdgeLines(state);\n\n for (const node of nodes) {\n const nx = node.position?.x ?? 0;\n const ny = node.position?.y ?? 0;\n const nw = node.dimensions?.width ?? 150;\n const nh = node.dimensions?.height ?? 40;\n const isActive = activeNodeIds.has(node.id ?? '');\n\n if (isActive) {\n // Accent: bright fill, full opacity\n svg += ``;\n } else {\n // Greyed out\n svg += ``;\n }\n }\n\n svg += '';\n return svg;\n },\n};\n\nregisterThumbnailRenderer('faithful', faithfulRenderer);\nregisterThumbnailRenderer('outline', outlineRenderer);\nregisterThumbnailRenderer('activity', activityRenderer);\n","import type { RecordingEvent } from './types';\n\n/**\n * Binary-search the first event index whose `t` is strictly greater than `t`.\n * Events are assumed to be sorted by `t` ascending (guaranteed by the Recorder).\n * Returns `events.length` if all events have `t <= boundary`.\n */\nexport function firstEventIndexAfter(\n events: ReadonlyArray,\n boundary: number,\n): number {\n let lo = 0;\n let hi = events.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if (events[mid].t > boundary) {\n hi = mid;\n } else {\n lo = mid + 1;\n }\n }\n return lo;\n}\n\n/**\n * Binary-search the first event index whose `t` is >= `t`. Used when we need\n * to include events exactly at the boundary (e.g. \"apply events at startT\").\n */\nexport function firstEventIndexAtOrAfter(\n events: ReadonlyArray,\n boundary: number,\n): number {\n let lo = 0;\n let hi = events.length;\n while (lo < hi) {\n const mid = (lo + hi) >>> 1;\n if (events[mid].t >= boundary) {\n hi = mid;\n } else {\n lo = mid + 1;\n }\n }\n return lo;\n}\n","import type { RecordingData, RecordingEvent, Checkpoint, CanvasSnapshot } from './types';\nimport { RECORDING_VERSION } from './types';\nimport { VirtualEngine, REPLAY_DT } from './virtual-engine';\nimport { getThumbnailRenderer } from './thumbnail';\nimport { firstEventIndexAfter, firstEventIndexAtOrAfter } from './event-cursor';\nimport { safeClone } from '../clone';\n\nfunction getNestedProperty(obj: any, path: string): any {\n return path.split('.').reduce((acc: any, key: string) => acc?.[key], obj);\n}\n\nfunction deepFreeze(value: T): T {\n if (value !== null && typeof value === 'object') {\n Object.freeze(value);\n for (const key of Object.keys(value as object)) {\n deepFreeze((value as Record)[key]);\n }\n }\n return value;\n}\n\nexport class Recording {\n readonly version: number;\n readonly duration: number;\n readonly initialState: Readonly;\n readonly events: ReadonlyArray;\n readonly checkpoints: ReadonlyArray;\n readonly metadata: Readonly>;\n\n constructor(data: RecordingData) {\n this.version = data.version;\n this.duration = data.duration;\n this.initialState = deepFreeze(safeClone(data.initialState));\n this.events = Object.freeze(safeClone(data.events));\n this.checkpoints = Object.freeze(safeClone(data.checkpoints));\n this.metadata = Object.freeze({ ...(data.metadata ?? {}) });\n Object.freeze(this);\n }\n\n toJSON(): RecordingData {\n return {\n version: this.version,\n duration: this.duration,\n initialState: safeClone(this.initialState as CanvasSnapshot),\n events: safeClone(this.events as RecordingEvent[]),\n checkpoints: safeClone(this.checkpoints as Checkpoint[]),\n metadata: { ...this.metadata },\n };\n }\n\n static fromJSON(data: RecordingData): Recording {\n if (data.version > RECORDING_VERSION) {\n throw new Error(\n `[AlpineFlow] Recording version ${data.version} is newer than supported (${RECORDING_VERSION}). ` +\n `Please update AlpineFlow to replay this recording.`\n );\n }\n // Future: migrate older versions here\n return new Recording(data);\n }\n\n /**\n * Returns unique subjects (nodes, edges, timelines, particles) that appeared\n * during the recording, with their first-seen and last-seen timestamps.\n */\n getSubjects(): Array<{\n kind: 'node' | 'edge' | 'timeline' | 'particle';\n id: string;\n firstSeenT: number;\n lastSeenT: number;\n }> {\n const map = new Map();\n\n const track = (kind: 'node' | 'edge' | 'timeline' | 'particle', id: string, t: number) => {\n const key = `${kind}:${id}`;\n const existing = map.get(key);\n if (existing) {\n if (t < existing.firstSeenT) { existing.firstSeenT = t; }\n if (t > existing.lastSeenT) { existing.lastSeenT = t; }\n } else {\n map.set(key, { kind, id, firstSeenT: t, lastSeenT: t });\n }\n };\n\n // Seed from initialState (t = 0)\n for (const id of Object.keys(this.initialState.nodes)) {\n track('node', id, 0);\n }\n for (const id of Object.keys(this.initialState.edges)) {\n track('edge', id, 0);\n }\n\n // Walk events\n for (const event of this.events) {\n const { t, type, args } = event;\n switch (type) {\n case 'animate':\n case 'update':\n for (const id of Object.keys(args.targets?.nodes ?? {})) {\n track('node', id, t);\n }\n for (const id of Object.keys(args.targets?.edges ?? {})) {\n track('edge', id, t);\n }\n break;\n case 'particle':\n case 'particle-burst':\n if (args.edgeId) { track('edge', args.edgeId, t); }\n break;\n case 'particle-between':\n if (args.source) { track('node', args.source, t); }\n if (args.target) { track('node', args.target, t); }\n break;\n case 'converging':\n if (Array.isArray(args.sources)) {\n for (const id of args.sources) { track('edge', id, t); }\n }\n if (args.options?.targetNodeId) { track('node', args.options.targetNodeId, t); }\n break;\n case 'node-add':\n case 'node-remove':\n if (args.id) { track('node', args.id, t); }\n if (Array.isArray(args.nodes)) {\n for (const n of args.nodes) {\n if (n.id) { track('node', n.id, t); }\n }\n }\n break;\n case 'edge-add':\n case 'edge-remove':\n if (args.id) { track('edge', args.id, t); }\n if (Array.isArray(args.edges)) {\n for (const e of args.edges) {\n if (e.id) { track('edge', e.id, t); }\n }\n }\n break;\n }\n }\n\n return Array.from(map.values());\n }\n\n /**\n * Returns activity spans for a specific subject identified by `id`.\n */\n getActivityFor(id: string): Array<{ startT: number; endT: number; reason: string }> {\n const spans: Array<{ startT: number; endT: number; reason: string }> = [];\n\n for (const event of this.events) {\n const { t, type, args } = event;\n const involvesSubject = (() => {\n switch (type) {\n case 'animate':\n case 'update':\n return id in (args.targets?.nodes ?? {}) || id in (args.targets?.edges ?? {});\n case 'particle':\n case 'particle-burst':\n return args.edgeId === id;\n case 'particle-between':\n return args.source === id || args.target === id;\n case 'converging':\n return (Array.isArray(args.sources) && args.sources.includes(id))\n || args.options?.targetNodeId === id;\n case 'node-add':\n case 'node-remove':\n if (args.id === id) { return true; }\n if (Array.isArray(args.nodes) && args.nodes.some((n: any) => n.id === id)) { return true; }\n return false;\n case 'edge-add':\n case 'edge-remove':\n if (args.id === id) { return true; }\n if (Array.isArray(args.edges) && args.edges.some((e: any) => e.id === id)) { return true; }\n return false;\n default:\n return false;\n }\n })();\n\n if (!involvesSubject) { continue; }\n\n switch (type) {\n case 'animate': {\n const duration = args.options?.duration ?? 0;\n spans.push({ startT: t, endT: t + duration, reason: 'animate' });\n break;\n }\n case 'particle':\n case 'particle-burst':\n case 'particle-between': {\n const duration = args.options?.duration ?? args.duration ?? 1;\n spans.push({ startT: t, endT: t + duration, reason: type });\n break;\n }\n case 'converging': {\n const duration = args.options?.duration ?? 1;\n spans.push({ startT: t, endT: t + duration, reason: 'converging' });\n break;\n }\n default:\n spans.push({ startT: t, endT: t + 1, reason: type });\n break;\n }\n }\n\n return spans;\n }\n\n /**\n * Returns sample points for a property's value over time, sampled from checkpoints.\n * `path` uses dot notation, e.g. `'nodes.trigger.position.x'`.\n */\n getValueTrack(path: string): Array<{ t: number; v: any }> {\n const result: Array<{ t: number; v: any }> = [];\n for (const cp of this.checkpoints) {\n const v = getNestedProperty(cp.canvas, path);\n if (v !== undefined) {\n result.push({ t: cp.t, v });\n }\n }\n return result;\n }\n\n /**\n * Returns the canvas state at virtual time `t` by running the VirtualEngine\n * up to that point from the nearest prior checkpoint.\n */\n getStateAt(t: number): CanvasSnapshot {\n const engine = new VirtualEngine(this.initialState as CanvasSnapshot);\n\n // Find nearest checkpoint ≤ t\n let nearest: Checkpoint | null = null;\n for (const cp of this.checkpoints) {\n if (cp.t <= t && (!nearest || cp.t > nearest.t)) {\n nearest = cp as Checkpoint;\n }\n }\n\n if (nearest) { engine.restoreCheckpoint(nearest); }\n\n // Walk forward to t\n const startVt = nearest?.t ?? 0;\n const events = this.events;\n let vt = startVt;\n const dtMs = REPLAY_DT * 1000;\n\n // Monotonic cursor. If a checkpoint was restored, the checkpoint\n // already reflects the canvas state AFTER events at startVt ran, so\n // skip them (strictly-after). Without a checkpoint, include events at\n // startVt so t=0 events are picked up by the main loop on its first\n // iteration (where nextVt = min(dtMs, t) covers them via `<= nextVt`).\n // Matches ReplayHandle.getStateAt exactly — no pre-loop needed.\n let cursor = nearest\n ? firstEventIndexAfter(events, startVt)\n : firstEventIndexAtOrAfter(events, startVt);\n\n while (vt < t) {\n const nextVt = Math.min(vt + dtMs, t);\n while (cursor < events.length && events[cursor].t <= nextVt) {\n engine.applyEvent(events[cursor] as RecordingEvent);\n cursor++;\n }\n const stepDt = (nextVt - vt) / 1000;\n engine.advance(stepDt);\n vt = nextVt;\n }\n\n return engine.getState();\n }\n\n /**\n * Renders a thumbnail SVG snapshot of the canvas state at virtual time `t`.\n */\n renderThumbnailAt(t: number, options: { width: number; height: number; renderer?: string }): string {\n const state = this.getStateAt(t);\n const rendererName = options.renderer ?? 'faithful';\n const renderer = getThumbnailRenderer(rendererName);\n\n if (!renderer) {\n throw new Error(`[AlpineFlow] Unknown thumbnail renderer \"${rendererName}\"`);\n }\n\n return renderer.render(state, { width: options.width, height: options.height });\n }\n}\n","import type { RecordingEvent, RecordingEventType, Checkpoint, CanvasSnapshot, RecordingData, InFlightAnimation } from './types';\nimport { Recording } from './recording';\nimport { RECORDING_VERSION } from './types';\nimport { safeClone } from '../clone';\n\n/**\n * Live record for an animation that's still in flight at the current virtual\n * time. The Recorder stores these as each `animate`/`update` hook fires and\n * drops them when the handle's `finished` promise resolves.\n */\ninterface ActiveAnim {\n handleId: string;\n eventT: number; // virtual ms when the animate event was recorded\n targets: any; // AnimateTargets passed to animate()\n options: any; // AnimateOptions passed to animate()\n handle: any; // FlowAnimationHandle — exposes direction, currentValue, isFinished\n fromValues: Record; // baseline values snapshotted at call time\n}\n\nexport interface RecordOptions {\n /** How often (in ms) to snapshot canvas state during recording. Default: 500ms. */\n checkpointInterval?: number;\n /** Optional metadata attached to the resulting Recording. */\n captureMetadata?: Record;\n /** Safety cap — throws if recording exceeds this duration (ms). Default: 60000ms. */\n maxDuration?: number;\n}\n\n/** Minimal canvas interface the recorder hooks into. */\nexport interface RecorderCanvas {\n // State access\n nodes: any[];\n edges: any[];\n viewport: { x: number; y: number; zoom: number };\n\n // Methods to hook (all optional — the recorder only hooks what exists)\n animate?(targets: any, options?: any): any;\n update?(targets: any, options?: any): any;\n sendParticle?(edgeId: string, options?: any): any;\n sendParticleAlongPath?(path: string, options?: any): any;\n sendParticleBetween?(source: string, target: string, options?: any): any;\n sendParticleBurst?(edgeId: string, options: any): any;\n sendConverging?(sources: string[], options: any): any;\n addNodes?(nodes: any | any[]): void;\n removeNodes?(ids: string | string[]): void;\n addEdges?(edges: any | any[]): void;\n removeEdges?(ids: string | string[]): void;\n\n // Engine for checkpoint scheduling (reserved for future use)\n _engine?: any;\n}\n\n/**\n * Records animation events and canvas state during fn() execution.\n * Installs method hooks on a canvas, captures each API call as a RecordingEvent\n * with a virtual timestamp, and periodically snapshots canvas state.\n */\nexport class Recorder {\n private _canvas: RecorderCanvas;\n private _events: RecordingEvent[] = [];\n private _checkpoints: Checkpoint[] = [];\n private _startTime = 0;\n private _originalMethods: Record = {};\n private _checkpointInterval: number;\n private _maxDuration: number;\n private _checkpointTimer: ReturnType | null = null;\n private _eventCounter = 0;\n /** In-flight animations indexed by handleId — populated by animate/update hooks, pruned by handle.finished. */\n private _activeAnims = new Map();\n\n constructor(canvas: RecorderCanvas, options: RecordOptions = {}) {\n this._canvas = canvas;\n this._checkpointInterval = options.checkpointInterval ?? 500;\n this._maxDuration = options.maxDuration ?? 60000;\n }\n\n async record(fn: () => Promise | void, metadata?: Record): Promise {\n this._startTime = performance.now();\n const initialState = this._captureSnapshot();\n\n this._installHooks();\n this._scheduleCheckpoints();\n\n try {\n const result = fn();\n if (result instanceof Promise) {\n await result;\n }\n\n if (this._virtualNow() > this._maxDuration) {\n throw new Error(`[AlpineFlow] Recording exceeded maxDuration (${this._maxDuration}ms)`);\n }\n } finally {\n this._uninstallHooks();\n if (this._checkpointTimer !== null) {\n clearInterval(this._checkpointTimer);\n this._checkpointTimer = null;\n }\n }\n\n // Final checkpoint at recording end — before clearing _activeAnims so\n // any animations still in flight get captured into the final checkpoint.\n this._captureCheckpoint();\n this._activeAnims.clear();\n\n const data: RecordingData = {\n version: RECORDING_VERSION,\n duration: this._virtualNow(),\n initialState,\n events: this._events,\n checkpoints: this._checkpoints,\n metadata,\n };\n\n return new Recording(data);\n }\n\n private _virtualNow(): number {\n return performance.now() - this._startTime;\n }\n\n private _recordEvent(type: RecordingEventType, args: Record): void {\n this._events.push({\n t: this._virtualNow(),\n type,\n args: this._sanitizeArgs(args),\n });\n }\n\n /** Strip non-serializable values (functions, etc.) and log warnings. */\n private _sanitizeArgs(args: Record): Record {\n const result: Record = {};\n for (const [key, value] of Object.entries(args)) {\n if (typeof value === 'function') {\n console.warn(`[AlpineFlow recorder] Stripped non-serializable option \"${key}\" (function)`);\n continue;\n }\n if (value && typeof value === 'object') {\n result[key] = this._sanitizeNested(value);\n } else {\n result[key] = value;\n }\n }\n return result;\n }\n\n private _sanitizeNested(value: any): any {\n if (value === null || typeof value !== 'object') {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map((v) => this._sanitizeNested(v));\n }\n\n const result: Record = {};\n for (const [key, v] of Object.entries(value)) {\n if (typeof v === 'function') {\n console.warn(`[AlpineFlow recorder] Stripped nested function at key \"${key}\"`);\n continue;\n }\n result[key] = this._sanitizeNested(v);\n }\n return result;\n }\n\n /**\n * Capture the live canvas values that an animate()/update() call is about\n * to transition FROM. Keys are the same flat form VirtualEngine uses\n * (e.g. `nodes.n.position.x`) so rehydration can lerp correctly.\n */\n private _snapshotFromValues(targets: any): Record {\n const out: Record = {};\n const nodeTargets = targets?.nodes ?? {};\n const nodesById = new Map();\n for (const n of this._canvas.nodes ?? []) {\n if (n && typeof n === 'object' && 'id' in n) {\n nodesById.set(n.id, n);\n }\n }\n for (const [nodeId, target] of Object.entries(nodeTargets)) {\n const node = nodesById.get(nodeId);\n if (!node) continue;\n const position = (target as { position?: { x?: number; y?: number } }).position;\n if (position?.x !== undefined) {\n out[`nodes.${nodeId}.position.x`] = node.position?.x ?? 0;\n }\n if (position?.y !== undefined) {\n out[`nodes.${nodeId}.position.y`] = node.position?.y ?? 0;\n }\n }\n const viewport = targets?.viewport;\n if (viewport?.pan?.x !== undefined) out['viewport.x'] = this._canvas.viewport.x;\n if (viewport?.pan?.y !== undefined) out['viewport.y'] = this._canvas.viewport.y;\n if (viewport?.zoom !== undefined) out['viewport.zoom'] = this._canvas.viewport.zoom;\n return out;\n }\n\n private _captureSnapshot(): CanvasSnapshot {\n const nodes: Record = {};\n for (const n of this._canvas.nodes ?? []) {\n if (n && typeof n === 'object' && 'id' in n) {\n nodes[n.id] = safeClone(n);\n }\n }\n const edges: Record = {};\n for (const e of this._canvas.edges ?? []) {\n if (e && typeof e === 'object' && 'id' in e) {\n edges[e.id] = safeClone(e);\n }\n }\n return {\n nodes,\n edges,\n viewport: { ...this._canvas.viewport },\n };\n }\n\n private _captureCheckpoint(): void {\n this._checkpoints.push({\n t: this._virtualNow(),\n canvas: this._captureSnapshot(),\n inFlight: this._captureInFlight(),\n tagRegistry: {},\n });\n }\n\n /**\n * Serialize the current in-flight animations tracked by this recorder into\n * the InFlightAnimation shape the VirtualEngine can restore from.\n * Draws data from each ActiveAnim entry's original event args + the handle's\n * live state. Finished handles are skipped (the finished promise cleanup\n * typically runs before this, but we defend anyway).\n */\n private _captureInFlight(): InFlightAnimation[] {\n const out: InFlightAnimation[] = [];\n for (const active of this._activeAnims.values()) {\n if (active.handle?.isFinished) continue;\n\n const options = active.options ?? {};\n const hasMotion = !!options.motion;\n\n // Derive the inflight type from the options.motion if present.\n let type: InFlightAnimation['type'] = 'eased';\n if (hasMotion) {\n const m = options.motion;\n if (typeof m === 'string') {\n // 'spring.wobbly' → 'spring'\n const cat = m.split('.')[0];\n type = (cat as InFlightAnimation['type']);\n } else if (m && typeof m === 'object' && m.type) {\n type = m.type as InFlightAnimation['type'];\n }\n }\n\n // Serialize currentValues (a Map on the live handle) to a plain\n // Record that can be structured-cloned and restored later.\n let currentValues: Record = {};\n const cv = active.handle?.currentValue;\n if (cv && typeof cv.forEach === 'function') {\n cv.forEach((v: any, k: string) => {\n currentValues[k] = v;\n });\n }\n\n out.push({\n handleId: active.handleId,\n type,\n targets: safeClone(active.targets),\n startTime: active.eventT,\n duration: hasMotion ? undefined : (options.duration ?? 300),\n easing: hasMotion ? undefined : options.easing,\n motion: hasMotion ? safeClone(options.motion) : undefined,\n direction: active.handle?.direction ?? 'forward',\n currentValues,\n fromValues: { ...active.fromValues },\n // integratorState populated if/when handles expose physics state.\n // For now, scrubbing into mid-physics relies on rehydration via\n // walk-forward from the nearest event; direct physics state\n // capture is a planned follow-up for perfect fidelity.\n integratorState: undefined,\n });\n }\n return out;\n }\n\n private _scheduleCheckpoints(): void {\n this._checkpointTimer = setInterval(() => {\n this._captureCheckpoint();\n }, this._checkpointInterval);\n }\n\n private _installHooks(): void {\n const hook = (\n methodName: string,\n eventType: RecordingEventType,\n argsMapper?: (...args: any[]) => Record,\n ): void => {\n const original = (this._canvas as any)[methodName];\n if (typeof original !== 'function') {\n return;\n }\n\n this._originalMethods[methodName] = original;\n (this._canvas as any)[methodName] = (...args: any[]) => {\n const mappedArgs = argsMapper ? argsMapper(...args) : { args };\n this._recordEvent(eventType, mappedArgs);\n return original.apply(this._canvas, args);\n };\n };\n\n // animate/update are tracked for in-flight checkpoint capture — the\n // hook wraps the real call, records the event, AND registers the\n // returned handle so _captureCheckpoint can serialize active animations.\n const animHook = (methodName: 'animate' | 'update', eventType: RecordingEventType): void => {\n const original = (this._canvas as any)[methodName];\n if (typeof original !== 'function') return;\n\n this._originalMethods[methodName] = original;\n (this._canvas as any)[methodName] = (targets: any, options: any) => {\n const handleId = `rec-${++this._eventCounter}`;\n const eventT = this._virtualNow();\n // Snapshot baseline values BEFORE the call — any other in-flight\n // animations on the same keys will update the canvas via rAF,\n // so reading now captures the true pre-animation baseline for\n // mid-flight checkpoint rehydration.\n const fromValues = this._snapshotFromValues(targets);\n this._recordEvent(eventType, { targets, options, handleId });\n\n const handle = original.apply(this._canvas, [targets, options]);\n\n // Track the handle only if it has the shape we need to serialize.\n // `update()` with duration 0 returns a noop-like handle whose\n // `finished` resolves immediately — skip those.\n if (handle && typeof handle === 'object' && handle.finished && !handle.isFinished) {\n const active: ActiveAnim = { handleId, eventT, targets, options, handle, fromValues };\n this._activeAnims.set(handleId, active);\n handle.finished.then(() => {\n this._activeAnims.delete(handleId);\n }).catch(() => {\n this._activeAnims.delete(handleId);\n });\n }\n\n return handle;\n };\n };\n\n animHook('animate', 'animate');\n animHook('update', 'update');\n hook('sendParticle', 'particle', (edgeId, options) => ({ edgeId, options }));\n hook('sendParticleAlongPath', 'particle-along-path', (path, options) => ({ path, options }));\n hook('sendParticleBetween', 'particle-between', (source, target, options) => ({ source, target, options }));\n hook('sendParticleBurst', 'particle-burst', (edgeId, options) => ({ edgeId, options }));\n hook('sendConverging', 'converging', (sources, options) => ({ sources, options }));\n hook('addNodes', 'node-add', (nodes) => ({ nodes }));\n hook('removeNodes', 'node-remove', (ids) => ({ ids }));\n hook('addEdges', 'edge-add', (edges) => ({ edges }));\n hook('removeEdges', 'edge-remove', (ids) => ({ ids }));\n }\n\n private _uninstallHooks(): void {\n for (const [methodName, original] of Object.entries(this._originalMethods)) {\n (this._canvas as any)[methodName] = original;\n }\n this._originalMethods = {};\n }\n}\n","// ============================================================================\n// ReplayHandle — drives a VirtualEngine to reproduce canvas state.\n//\n// Returned from `$flow.replay(recording, options)`. Walks virtual time through\n// the recording's events (optionally using checkpoints for efficient scrubbing)\n// and applies the resulting snapshot to the real canvas.\n// ============================================================================\n\nimport type { RecordingEvent, CanvasSnapshot, Checkpoint } from './types';\nimport type { Recording } from './recording';\nimport { VirtualEngine, REPLAY_DT } from './virtual-engine';\nimport { firstEventIndexAfter, firstEventIndexAtOrAfter } from './event-cursor';\n\nexport interface ReplayOptions {\n /** Playback speed multiplier. Negative plays backward. Default 1.0. */\n speed?: number;\n /** Virtual-ms start; default 0. */\n from?: number;\n /** Virtual-ms end; default recording.duration. */\n to?: number;\n /** Repeat forever (true) or N times. Default false. */\n loop?: boolean | number;\n /** Start paused. Default false. */\n paused?: boolean;\n /** Skip restoring `initialState` to the canvas on construction. */\n skipInitialState?: boolean;\n}\n\nexport interface ReplayCanvas {\n nodes: any[];\n edges: any[];\n viewport: { x: number; y: number; zoom: number };\n update?(targets: any, options?: any): any;\n /**\n * Optional structural methods — when present, replay uses these to add or\n * remove nodes/edges during state reconciliation. Without them, a recording\n * that contains node-add/remove events only replays correctly on a canvas\n * that already has the same nodes present.\n */\n addNodes?(nodes: any | any[]): void;\n removeNodes?(ids: string | string[]): void;\n addEdges?(edges: any | any[]): void;\n removeEdges?(ids: string | string[]): void;\n /**\n * Optional particle emission methods — when present, replay dispatches\n * captured particle events back to the live canvas during forward playback\n * so recordings containing `sendParticle*` / `sendConverging` calls reproduce\n * their visual effects. Not called during scrubbing (particles are\n * momentary visuals; re-firing on every scrub frame would be disorienting).\n */\n sendParticle?(edgeId: string, options?: any): any;\n sendParticleAlongPath?(path: string, options?: any): any;\n sendParticleBetween?(source: string, target: string, options?: any): any;\n sendParticleBurst?(edgeId: string, options: any): any;\n sendConverging?(sources: string[], options: any): any;\n}\n\nexport type ReplayState = 'idle' | 'playing' | 'paused' | 'ended';\nexport type ReplayDirection = 'forward' | 'backward';\n\n/**\n * Drives a `VirtualEngine` through a recording, applying the virtual state to\n * a real canvas each tick. Exposes pause/play/scrub controls and a `finished`\n * promise that resolves when the replay reaches the end (and isn't looping).\n */\nexport class ReplayHandle {\n readonly recording: Recording;\n readonly finished: Promise;\n\n private _canvas: ReplayCanvas;\n private _virtualEngine: VirtualEngine;\n private _currentTime = 0;\n private _state: ReplayState = 'idle';\n private _direction: ReplayDirection = 'forward';\n private _speed = 1.0;\n private _from: number;\n private _to: number;\n private _loop: boolean | number;\n private _rafHandle: number | null = null;\n private _lastWallTime = 0;\n private _resolveFinished: () => void = () => {};\n\n constructor(canvas: ReplayCanvas, recording: Recording, options: ReplayOptions = {}) {\n this.recording = recording;\n this._canvas = canvas;\n this._virtualEngine = new VirtualEngine(recording.initialState);\n\n this._speed = options.speed ?? 1.0;\n this._direction = this._speed < 0 ? 'backward' : 'forward';\n this._from = options.from ?? 0;\n this._to = options.to ?? recording.duration;\n this._loop = options.loop ?? false;\n this._currentTime = this._from;\n\n // If starting past t=0, walk the engine forward so it reflects the\n // correct state at _from (accounting for checkpoints). Without this,\n // events in [0, _from] are skipped and the first tick applies events\n // strictly after _from — causing the canvas to jump from initialState\n // to mid-recording state without reflecting anything in between.\n if (this._from > 0) {\n this._seekEngineTo(this._from);\n }\n\n if (!options.skipInitialState) {\n this._applyStateToCanvas(this._virtualEngine.getState());\n }\n\n this.finished = new Promise((resolve) => {\n this._resolveFinished = resolve;\n });\n\n if (options.paused) {\n this._state = 'paused';\n } else if (this._speed !== 0) {\n this.play();\n }\n }\n\n get duration(): number {\n return this.recording.duration;\n }\n\n get currentTime(): number {\n return this._currentTime;\n }\n\n get state(): ReplayState {\n return this._state;\n }\n\n get direction(): ReplayDirection {\n return this._direction;\n }\n\n get speed(): number {\n return this._speed;\n }\n\n set speed(v: number) {\n this._speed = v;\n this._direction = v < 0 ? 'backward' : 'forward';\n }\n\n play(): void {\n if (this._state === 'playing') {\n return;\n }\n if (this._state === 'ended') {\n this._currentTime = this._from;\n this._seekEngineTo(this._from);\n this._applyStateToCanvas(this._virtualEngine.getState());\n }\n this._state = 'playing';\n this._lastWallTime = now();\n this._scheduleTick();\n }\n\n pause(): void {\n if (this._state !== 'playing') {\n return;\n }\n this._state = 'paused';\n this._cancelTick();\n }\n\n stop(): void {\n this._cancelTick();\n this._currentTime = this._from;\n this._virtualEngine = new VirtualEngine(this.recording.initialState);\n this._applyStateToCanvas(this._virtualEngine.getState());\n this._state = 'idle';\n }\n\n scrubTo(target: number | string): void {\n const t = this._resolveTarget(target);\n this._currentTime = t;\n this._seekEngineTo(t);\n this._applyStateToCanvas(this._virtualEngine.getState());\n }\n\n seek(target: number | string): void {\n this.scrubTo(target);\n }\n\n eventsUpTo(t: number): RecordingEvent[] {\n return this.recording.events.filter((e) => e.t <= t);\n }\n\n getStateAt(t: number): CanvasSnapshot {\n const cp = this._findNearestCheckpoint(t);\n const engine = new VirtualEngine(this.recording.initialState);\n if (cp) {\n engine.restoreCheckpoint(cp);\n }\n\n const startT = cp?.t ?? 0;\n const events = this.recording.events;\n let vt = startT;\n const dtMs = REPLAY_DT * 1000;\n\n // Monotonic cursor — binary-search to the first event past startT.\n // When there's no checkpoint, include events exactly at startT so t=0\n // events aren't skipped.\n let cursor = cp\n ? firstEventIndexAfter(events, startT)\n : firstEventIndexAtOrAfter(events, startT);\n\n while (vt < t) {\n const nextVt = Math.min(vt + dtMs, t);\n while (cursor < events.length && events[cursor].t <= nextVt) {\n engine.applyEvent(events[cursor]);\n cursor++;\n }\n const stepDt = (nextVt - vt) / 1000;\n if (stepDt > 0) {\n engine.advance(stepDt);\n }\n vt = nextVt;\n }\n\n return engine.getState();\n }\n\n // ── Private ─────────────────────────────────────────────────────────────\n\n private _tick(): void {\n if (this._state !== 'playing') {\n return;\n }\n\n const current = now();\n const wallDt = (current - this._lastWallTime) / 1000;\n this._lastWallTime = current;\n\n const virtualDtMs = wallDt * this._speed * 1000;\n const nextTime = this._currentTime + virtualDtMs;\n\n const hitEnd = this._direction === 'forward'\n ? nextTime >= this._to\n : nextTime <= this._from;\n\n if (hitEnd) {\n const boundary = this._direction === 'forward' ? this._to : this._from;\n if (this._direction === 'forward') {\n // Fire particles for the remaining forward range so any events\n // sitting at the boundary still emit.\n this._walkTo(this._currentTime, boundary, true);\n } else {\n // Backward boundary: re-seek from nearest checkpoint.\n this._seekEngineTo(boundary);\n }\n this._currentTime = boundary;\n this._applyStateToCanvas(this._virtualEngine.getState());\n this._handleEnd();\n return;\n }\n\n if (virtualDtMs > 0) {\n // Forward playback — dispatch captured particle events live so\n // recordings that include sendParticle* calls visibly replay them.\n this._walkTo(this._currentTime, nextTime, true);\n } else if (virtualDtMs < 0) {\n // Backward step: re-seek via checkpoint. No particle dispatch —\n // particles are momentary; firing them during reverse would be\n // disorienting and doesn't match any real \"undo\" of the emission.\n this._seekEngineTo(nextTime);\n }\n\n this._currentTime = nextTime;\n this._applyStateToCanvas(this._virtualEngine.getState());\n\n this._scheduleTick();\n }\n\n private _scheduleTick(): void {\n if (typeof requestAnimationFrame === 'function') {\n this._rafHandle = requestAnimationFrame(() => this._tick()) as unknown as number;\n } else {\n this._rafHandle = setTimeout(() => this._tick(), 16) as unknown as number;\n }\n }\n\n private _cancelTick(): void {\n if (this._rafHandle === null) {\n return;\n }\n if (typeof cancelAnimationFrame === 'function') {\n cancelAnimationFrame(this._rafHandle);\n } else {\n clearTimeout(this._rafHandle as unknown as ReturnType);\n }\n this._rafHandle = null;\n }\n\n /**\n * Reset the virtual engine to reflect the canvas state at virtual time `t`\n * — either by restoring the nearest checkpoint and walking forward, or by\n * walking from the recording's initial state. Used when seeking discretely\n * (play-after-ended, loop restart, constructor with non-zero `from`);\n * scrubTo has its own inlined copy because it also updates `_currentTime`.\n */\n private _seekEngineTo(t: number): void {\n const cp = this._findNearestCheckpoint(t);\n if (cp) {\n this._virtualEngine.restoreCheckpoint(cp);\n } else {\n this._virtualEngine = new VirtualEngine(this.recording.initialState);\n }\n this._walkTo(cp?.t ?? 0, t);\n }\n\n private _walkTo(startT: number, endT: number, dispatchLiveParticles = false): void {\n if (endT <= startT) {\n return;\n }\n const events = this.recording.events;\n let vt = startT;\n const dtMs = REPLAY_DT * 1000;\n\n // Monotonic cursor — one binary search, then linear advance per step.\n // At t=0 we include events on the boundary (matching pre-refactor\n // behavior); otherwise the scan window is (vt, nextVt].\n let cursor = startT === 0\n ? firstEventIndexAtOrAfter(events, 0)\n : firstEventIndexAfter(events, startT);\n\n while (vt < endT) {\n const nextVt = Math.min(vt + dtMs, endT);\n while (cursor < events.length && events[cursor].t <= nextVt) {\n const event = events[cursor];\n this._virtualEngine.applyEvent(event);\n if (dispatchLiveParticles) {\n this._dispatchLiveParticle(event);\n }\n cursor++;\n }\n const stepDt = (nextVt - vt) / 1000;\n if (stepDt > 0) {\n this._virtualEngine.advance(stepDt);\n }\n vt = nextVt;\n }\n }\n\n /**\n * Forward a captured particle event to the live canvas so its visual\n * effect replays. Non-particle events (animate, update, structural) are\n * already driven by `_applyStateToCanvas` via the virtual engine's state\n * — particles are the one event class that only exists as a visual and\n * therefore must be re-emitted on the real canvas.\n */\n private _dispatchLiveParticle(event: RecordingEvent): void {\n const c = this._canvas;\n switch (event.type) {\n case 'particle':\n c.sendParticle?.(event.args.edgeId, event.args.options);\n break;\n case 'particle-along-path':\n c.sendParticleAlongPath?.(event.args.path, event.args.options);\n break;\n case 'particle-between':\n c.sendParticleBetween?.(event.args.source, event.args.target, event.args.options);\n break;\n case 'particle-burst':\n c.sendParticleBurst?.(event.args.edgeId, event.args.options);\n break;\n case 'converging':\n c.sendConverging?.(event.args.sources, event.args.options);\n break;\n }\n }\n\n private _findNearestCheckpoint(t: number): Checkpoint | null {\n let nearest: Checkpoint | null = null;\n for (const cp of this.recording.checkpoints) {\n if (cp.t <= t && (!nearest || cp.t > nearest.t)) {\n nearest = cp;\n }\n }\n return nearest;\n }\n\n private _resolveTarget(target: number | string): number {\n const lo = Math.min(this._from, this._to);\n const hi = Math.max(this._from, this._to);\n if (typeof target === 'number') {\n return Math.max(lo, Math.min(hi, target));\n }\n if (target === 'start') {\n return this._from;\n }\n if (target === 'end') {\n return this._to;\n }\n if (target.endsWith('%')) {\n const pct = parseFloat(target) / 100;\n return this._from + pct * (this._to - this._from);\n }\n const parsed = parseFloat(target);\n if (!Number.isNaN(parsed)) {\n return Math.max(lo, Math.min(hi, parsed));\n }\n return this._from;\n }\n\n private _handleEnd(): void {\n if (this._loop) {\n const remaining = typeof this._loop === 'number' ? this._loop - 1 : Infinity;\n if (remaining > 0) {\n this._loop = typeof this._loop === 'number' ? remaining : true;\n this._currentTime = this._from;\n this._seekEngineTo(this._from);\n this._applyStateToCanvas(this._virtualEngine.getState());\n this._state = 'playing';\n this._lastWallTime = now();\n this._scheduleTick();\n return;\n }\n }\n\n this._state = 'ended';\n this._rafHandle = null;\n this._resolveFinished();\n }\n\n private _applyStateToCanvas(state: CanvasSnapshot): void {\n this._reconcileNodes(state);\n this._reconcileEdges(state);\n\n for (const [id, node] of Object.entries(state.nodes)) {\n const realNode = this._canvas.nodes.find((n: any) => n.id === id);\n if (!realNode) {\n continue;\n }\n if (node.position) {\n if (!realNode.position) {\n realNode.position = { x: 0, y: 0 };\n }\n if (node.position.x !== undefined) {\n realNode.position.x = node.position.x;\n }\n if (node.position.y !== undefined) {\n realNode.position.y = node.position.y;\n }\n }\n }\n\n if (state.viewport) {\n this._canvas.viewport.x = state.viewport.x;\n this._canvas.viewport.y = state.viewport.y;\n this._canvas.viewport.zoom = state.viewport.zoom;\n }\n }\n\n /**\n * Diff virtual vs. real node sets and apply structural changes. Prefers\n * the canvas's own addNodes/removeNodes when available so reactivity and\n * measurement hooks fire; falls back to direct array mutation otherwise.\n */\n private _reconcileNodes(state: CanvasSnapshot): void {\n const virtualIds = new Set(Object.keys(state.nodes));\n const realIds = new Set(this._canvas.nodes.map((n: any) => n?.id).filter(Boolean));\n\n const toAdd: any[] = [];\n for (const id of virtualIds) {\n if (!realIds.has(id)) {\n toAdd.push(state.nodes[id]);\n }\n }\n const toRemove: string[] = [];\n for (const id of realIds) {\n if (!virtualIds.has(id)) {\n toRemove.push(id);\n }\n }\n\n if (toAdd.length > 0) {\n if (typeof this._canvas.addNodes === 'function') {\n this._canvas.addNodes(toAdd);\n } else {\n this._canvas.nodes.push(...toAdd);\n }\n }\n if (toRemove.length > 0) {\n if (typeof this._canvas.removeNodes === 'function') {\n this._canvas.removeNodes(toRemove);\n } else {\n // Splice-by-index preserves the array reference (callers may\n // hold it). Safe here because reconciliation runs inside the\n // replay rAF tick — before Alpine's reactive re-render — so\n // no external iterator should be traversing `canvas.nodes`.\n for (const id of toRemove) {\n const idx = this._canvas.nodes.findIndex((n: any) => n?.id === id);\n if (idx !== -1) this._canvas.nodes.splice(idx, 1);\n }\n }\n }\n }\n\n private _reconcileEdges(state: CanvasSnapshot): void {\n const virtualIds = new Set(Object.keys(state.edges));\n const realIds = new Set(this._canvas.edges.map((e: any) => e?.id).filter(Boolean));\n\n const toAdd: any[] = [];\n for (const id of virtualIds) {\n if (!realIds.has(id)) {\n toAdd.push(state.edges[id]);\n }\n }\n const toRemove: string[] = [];\n for (const id of realIds) {\n if (!virtualIds.has(id)) {\n toRemove.push(id);\n }\n }\n\n if (toAdd.length > 0) {\n if (typeof this._canvas.addEdges === 'function') {\n this._canvas.addEdges(toAdd);\n } else {\n this._canvas.edges.push(...toAdd);\n }\n }\n if (toRemove.length > 0) {\n if (typeof this._canvas.removeEdges === 'function') {\n this._canvas.removeEdges(toRemove);\n } else {\n // See `_reconcileNodes` — splice-by-index is the least-bad\n // fallback when no structural method is available.\n for (const id of toRemove) {\n const idx = this._canvas.edges.findIndex((e: any) => e?.id === id);\n if (idx !== -1) this._canvas.edges.splice(idx, 1);\n }\n }\n }\n }\n}\n\nfunction now(): number {\n if (typeof performance !== 'undefined' && typeof performance.now === 'function') {\n return performance.now();\n }\n return Date.now();\n}\n","// ============================================================================\n// canvas-animation — Animation, timeline, and follow mixin for flow-canvas\n//\n// Public API: animate, update, timeline, registerAnimation, unregisterAnimation,\n// playAnimation, follow, sendParticle, sendParticleAlongPath,\n// sendParticleBetween, getHandles, cancelAll,\n// pauseAll, resumeAll, group, transaction, snapshot.\n// Internal: _syncAnimationState, _tickParticles.\n//\n// Covers the core animation engine wiring, the timeline factory, named\n// animation registry, and viewport follow tracking. Particle effects are\n// delegated to the canvas-particles sub-mixin.\n//\n// Cross-mixin deps (via ctx): getNode, getEdge, getEdgePathElement,\n// getEdgeElement, getAbsolutePosition, _flushNodePositions,\n// _flushNodeStyles, _flushEdgeStyles, _flushViewport, _refreshEdgePaths,\n// _suspendHistory, _resumeHistory.\n// ============================================================================\n\nimport { getAlpine } from '../alpine-ref';\nimport type { CanvasContext } from './canvas-context';\nimport type {\n FlowNode,\n FlowEdge,\n XYPosition,\n AnimateTargets,\n AnimateOptions,\n FlowAnimationHandle,\n FollowOptions,\n ParticleHandle,\n StopOptions,\n} from '../../core/types';\nimport type { Animator, PropertyEntry } from '../../animate/animator';\nimport { parseStyle, interpolateStyle } from '../../animate/interpolators';\nimport { checkReducedMotion } from '../../animate/easing';\nimport { FlowTimeline, type TimelineStep } from '../../animate/timeline';\nimport { type PathFunction, svgPathToFunction } from '../../animate/paths';\nimport { engine } from '../../animate/engine';\nimport { debug } from '../../core/debug';\nimport { DEFAULT_STROKE_COLOR } from '../../core/constants';\nimport { createParticleMixin } from './canvas-particles';\nimport { registerParticleRenderer as _registerParticleRenderer } from '../../animate/particle-renderers';\nimport { FlowGroup } from '../../animate/flow-group';\nimport type { Transaction } from '../../animate/transaction';\nimport type { ParticleRenderer } from '../../core/types';\nimport { Recorder, ReplayHandle, type RecordOptions, type ReplayOptions } from '../../animate/recording';\nimport type { Recording } from '../../animate/recording';\n\n// ── Mixin factory ───────────────────────────────────────────────────────────\n\nexport function createAnimationMixin(ctx: CanvasContext) {\n // Delegate particle system to the dedicated sub-mixin\n const particles = createParticleMixin(ctx);\n\n return {\n // ── Internal: Sync animation lock state ───────────────────────────────\n\n /**\n * Synchronize the `_animationLocked` flag from active timelines and\n * manage history suspension while any timeline is playing.\n */\n _syncAnimationState(): void {\n const anyLocked = [...ctx._activeTimelines].some((tl) => tl.locked);\n ctx._animationLocked = anyLocked;\n if (ctx._activeTimelines.size === 0) {\n ctx._resumeHistory();\n } else {\n ctx._suspendHistory();\n }\n },\n\n // ── Timeline factory ──────────────────────────────────────────────────\n\n /**\n * Create a new FlowTimeline wired to this canvas. Lock flag and\n * history suspension are automatically managed via timeline events.\n */\n timeline(): FlowTimeline {\n const tl = new FlowTimeline(ctx, engine);\n\n tl.on('play', () => {\n ctx._activeTimelines.add(tl);\n ctx._syncAnimationState();\n });\n\n tl.on('resume', () => {\n ctx._activeTimelines.add(tl);\n ctx._syncAnimationState();\n });\n\n for (const event of ['pause', 'stop', 'complete'] as const) {\n tl.on(event, () => {\n ctx._activeTimelines.delete(tl);\n ctx._syncAnimationState();\n });\n }\n\n return tl;\n },\n\n // ── Named animation registry ──────────────────────────────────────────\n\n /**\n * Register a named animation (used by x-flow-animate directive).\n */\n registerAnimation(name: string, steps: any[]): void {\n ctx._animationRegistry.set(name, steps);\n },\n\n /**\n * Unregister a named animation.\n */\n unregisterAnimation(name: string): void {\n ctx._animationRegistry.delete(name);\n },\n\n /**\n * Play a named animation registered via x-flow-animate directive.\n */\n async playAnimation(name: string): Promise {\n const steps = ctx._animationRegistry.get(name);\n if (!steps) {\n debug('animation', `Named animation \"${name}\" not found`);\n return;\n }\n const tl = ctx.timeline();\n for (const s of steps) {\n if (s.parallel) {\n tl.parallel(s.parallel as TimelineStep[]);\n } else {\n tl.step(s as TimelineStep);\n }\n }\n await tl.play();\n },\n\n // ── Core update/animate API ─────────────────────────────────────────\n\n /**\n * Update nodes, edges, and/or the viewport.\n *\n * The core method for applying property changes. When duration is 0\n * (the default), changes are applied instantly via DOM flushing.\n * When duration > 0, transitions are delegated to the Animator for\n * frame-by-frame interpolation.\n *\n * Use `animate()` for a convenience wrapper that defaults to smooth\n * transitions (duration: 300ms).\n */\n update(\n targets: AnimateTargets,\n options: AnimateOptions = {},\n ): FlowAnimationHandle {\n // Compile boundTo into a while predicate\n if (options?.boundTo) {\n const binding = options.boundTo;\n if ('node' in binding) {\n options = {\n ...options,\n while: () => {\n const node = ctx.getNode(binding.node);\n return node?.[binding.property as keyof typeof node] === binding.equals;\n },\n };\n } else if ('edge' in binding) {\n options = {\n ...options,\n while: () => {\n const edge = ctx.getEdge(binding.edge);\n return edge?.[binding.property as keyof typeof edge] === binding.equals;\n },\n };\n }\n }\n\n const duration = options.duration ?? 0;\n const entries: PropertyEntry[] = [];\n const movedNodeIds = new Set();\n const styledNodeIds = new Set();\n const styledEdgeIds = new Set();\n\n // After a move settles, `_refreshEdgePaths` has drawn a simplified\n // node-centre/no-obstacle edge path (fast enough to run every animation\n // frame, but it ignores schema handle geometry and obstacle routing). Left\n // as-is, schema edges stay detached at the node centre once the animation\n // ends (the reset-grid / scramble regression). Re-run the full reactive\n // edge effect (`_layoutAnimTick`) and rebuild the obstacle snapshot\n // (`_commitNodeGeometry`) so edges snap back to their real handles +\n // avoidant routes — mirrors the history-restore settle. rAF so it runs\n // after the node DOM has been repositioned.\n const settleEdgeGeometry = (ids: Set): void => {\n if (ids.size === 0) {\n return;\n }\n const settledIds = Array.from(ids);\n requestAnimationFrame(() => {\n ctx._layoutAnimTick++;\n ctx._commitNodeGeometry?.(settledIds);\n });\n };\n\n // Debug logging\n const nodeCount = targets.nodes ? Object.keys(targets.nodes).length : 0;\n const edgeCount = targets.edges ? Object.keys(targets.edges).length : 0;\n debug('animate', `update() called`, {\n nodes: nodeCount,\n edges: edgeCount,\n viewport: !!targets.viewport,\n duration,\n easing: options.easing ?? 'default',\n instant: duration === 0,\n });\n\n // ── Node targets ────────────────────────────────────────────\n if (targets.nodes) {\n for (const [nodeId, target] of Object.entries(targets.nodes)) {\n const node = ctx._nodeMap.get(nodeId) as FlowNode | undefined;\n if (!node) continue;\n\n const elemDuration = target._duration ?? duration;\n const isInstant = elemDuration === 0;\n\n // followPath — curved motion overrides position\n if (target.followPath && !isInstant) {\n let pathFn: PathFunction | null = null;\n if (typeof target.followPath === 'function') {\n pathFn = target.followPath;\n } else {\n pathFn = svgPathToFunction(target.followPath);\n }\n\n // Guide path overlay for SVG path strings\n let guidePathEl: SVGPathElement | null = null;\n if (\n target.guidePath?.visible &&\n typeof target.followPath === 'string' &&\n typeof document !== 'undefined'\n ) {\n const svgContainer = ctx.getEdgeSvgElement?.();\n if (svgContainer) {\n guidePathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n guidePathEl.setAttribute('d', target.followPath);\n guidePathEl.classList.add('flow-guide-path');\n if (target.guidePath.class) {\n guidePathEl.classList.add(target.guidePath.class);\n }\n svgContainer.appendChild(guidePathEl);\n }\n }\n\n if (pathFn) {\n const capturedPathFn = pathFn;\n const capturedGuide = guidePathEl;\n const autoRemove = target.guidePath?.autoRemove !== false;\n entries.push({\n key: `node:${nodeId}:followPath`,\n from: 0,\n to: 1,\n apply: (t) => {\n const n = ctx._nodeMap.get(nodeId);\n if (!n) return;\n const pos = capturedPathFn(t as number);\n getAlpine().raw(n).position.x = pos.x;\n getAlpine().raw(n).position.y = pos.y;\n movedNodeIds.add(nodeId);\n // Clean up guide path on completion\n if ((t as number) >= 1 && capturedGuide && autoRemove) {\n capturedGuide.remove();\n }\n },\n });\n }\n } else if (target.position) {\n // Position animation (linear interpolation)\n const rawNode = getAlpine().raw(node);\n const fromPos = rawNode.position;\n if (target.position.x !== undefined) {\n const toX = target.position.x;\n if (isInstant) {\n fromPos.x = toX;\n } else {\n const fromX = fromPos.x;\n entries.push({\n key: `node:${nodeId}:position.x`,\n from: fromX,\n to: toX,\n apply: (v) => {\n const n = ctx._nodeMap.get(nodeId);\n if (!n) return;\n getAlpine().raw(n).position.x = v as number;\n movedNodeIds.add(nodeId);\n },\n });\n }\n }\n if (target.position.y !== undefined) {\n const toY = target.position.y;\n if (isInstant) {\n fromPos.y = toY;\n } else {\n const fromY = fromPos.y;\n entries.push({\n key: `node:${nodeId}:position.y`,\n from: fromY,\n to: toY,\n apply: (v) => {\n const n = ctx._nodeMap.get(nodeId);\n if (n) getAlpine().raw(n).position.y = v as number;\n movedNodeIds.add(nodeId);\n },\n });\n }\n }\n if (isInstant) movedNodeIds.add(nodeId);\n }\n\n // Instant-only properties (data, class, selected, zIndex)\n if (target.data !== undefined) Object.assign(node.data, target.data);\n if (target.class !== undefined) node.class = target.class;\n if (target.selected !== undefined) node.selected = target.selected;\n if (target.zIndex !== undefined) node.zIndex = target.zIndex;\n\n // Style animation\n if (target.style !== undefined) {\n if (isInstant) {\n node.style = target.style;\n styledNodeIds.add(nodeId);\n } else {\n const fromStyle = parseStyle(node.style || {});\n const toStyle = parseStyle(target.style);\n // Fill missing \"from\" values from the DOM computed style\n const nodeEl = ctx._nodeElements.get(nodeId);\n if (nodeEl) {\n const computed = getComputedStyle(nodeEl);\n for (const key of Object.keys(toStyle)) {\n if (fromStyle[key] === undefined) {\n fromStyle[key] = computed.getPropertyValue(key);\n }\n }\n }\n entries.push({\n key: `node:${nodeId}:style`,\n from: 0,\n to: 1,\n apply: (t) => {\n const n = ctx._nodeMap.get(nodeId);\n if (!n) return;\n getAlpine().raw(n).style = interpolateStyle(fromStyle, toStyle as Record, t as number);\n styledNodeIds.add(nodeId);\n },\n });\n }\n }\n\n // Dimensions animation\n if (target.dimensions && node.dimensions) {\n if (target.dimensions.width !== undefined) {\n if (isInstant) {\n node.dimensions.width = target.dimensions.width;\n } else {\n entries.push({\n key: `node:${nodeId}:dimensions.width`,\n from: node.dimensions.width,\n to: target.dimensions.width,\n apply: (v) => { node.dimensions!.width = v as number; },\n });\n }\n }\n if (target.dimensions.height !== undefined) {\n // A2: height control requires fixedDimensions to prevent the Alpine effect\n // from clearing inline height on leaf nodes. Animate is authoritative here.\n node.fixedDimensions = true;\n if (isInstant) {\n node.dimensions.height = target.dimensions.height;\n } else {\n entries.push({\n key: `node:${nodeId}:dimensions.height`,\n from: node.dimensions.height,\n to: target.dimensions.height,\n apply: (v) => { node.dimensions!.height = v as number; },\n });\n }\n }\n }\n }\n }\n\n // ── Edge targets ────────────────────────────────────────────\n if (targets.edges) {\n for (const [edgeId, target] of Object.entries(targets.edges)) {\n const edge = ctx._edgeMap.get(edgeId) as FlowEdge | undefined;\n if (!edge) continue;\n\n const elemDuration = target._duration ?? duration;\n const isInstant = elemDuration === 0;\n\n // Color (string or gradient object)\n if (target.color !== undefined) {\n if (typeof target.color === 'object') {\n // Gradient: apply directly (CSS handles transitions)\n (edge as any).color = target.color;\n } else if (isInstant) {\n edge.color = target.color;\n styledEdgeIds.add(edgeId);\n } else {\n const fromColor = (typeof edge.color === 'string' && edge.color)\n || getComputedStyle(ctx._container!).getPropertyValue('--flow-edge-stroke').trim()\n || DEFAULT_STROKE_COLOR;\n entries.push({\n key: `edge:${edgeId}:color`,\n from: fromColor,\n to: target.color,\n apply: (v) => {\n const e = ctx._edgeMap.get(edgeId);\n if (!e) return;\n getAlpine().raw(e).color = v as string;\n styledEdgeIds.add(edgeId);\n },\n });\n }\n }\n\n // StrokeWidth\n if (target.strokeWidth !== undefined) {\n if (isInstant) {\n edge.strokeWidth = target.strokeWidth;\n styledEdgeIds.add(edgeId);\n } else {\n const fromWidth = edge.strokeWidth\n ?? (parseFloat(getComputedStyle(ctx._container!).getPropertyValue('--flow-edge-stroke-width').trim() || '1')\n || 1);\n entries.push({\n key: `edge:${edgeId}:strokeWidth`,\n from: fromWidth,\n to: target.strokeWidth,\n apply: (v) => {\n const e = ctx._edgeMap.get(edgeId);\n if (!e) return;\n getAlpine().raw(e).strokeWidth = v as number;\n styledEdgeIds.add(edgeId);\n },\n });\n }\n }\n\n // Instant-only edge properties\n if (target.label !== undefined) edge.label = target.label;\n if (target.animated !== undefined) edge.animated = target.animated;\n if (target.class !== undefined) edge.class = target.class;\n }\n }\n\n // ── Viewport target ─────────────────────────────────────────\n if (targets.viewport) {\n const vp = targets.viewport;\n const elemDuration = vp._duration ?? duration;\n const isInstant = elemDuration === 0;\n const currentVp = ctx.viewport;\n\n if (vp.pan?.x !== undefined) {\n if (isInstant) {\n currentVp.x = vp.pan.x;\n } else {\n entries.push({\n key: 'viewport:pan.x',\n from: currentVp.x,\n to: vp.pan.x,\n apply: (v) => { currentVp.x = v as number; },\n });\n }\n }\n if (vp.pan?.y !== undefined) {\n if (isInstant) {\n currentVp.y = vp.pan.y;\n } else {\n entries.push({\n key: 'viewport:pan.y',\n from: currentVp.y,\n to: vp.pan.y,\n apply: (v) => { currentVp.y = v as number; },\n });\n }\n }\n if (vp.zoom !== undefined) {\n if (isInstant) {\n currentVp.zoom = vp.zoom;\n } else {\n entries.push({\n key: 'viewport:zoom',\n from: currentVp.zoom,\n to: vp.zoom,\n apply: (v) => { currentVp.zoom = v as number; },\n });\n }\n }\n }\n\n // ── Handle instant case ─────────────────────────────────────\n if (entries.length === 0) {\n // All changes were instant — flush DOM and notify Alpine\n if (movedNodeIds.size > 0) {\n ctx._flushNodePositions(movedNodeIds);\n ctx._refreshEdgePaths(movedNodeIds);\n settleEdgeGeometry(movedNodeIds);\n }\n if (styledNodeIds.size > 0) {\n ctx._flushNodeStyles(styledNodeIds);\n }\n if (styledEdgeIds.size > 0) {\n ctx._flushEdgeStyles(styledEdgeIds);\n }\n\n const noop: FlowAnimationHandle = {\n pause: () => {},\n resume: () => {},\n stop: () => {},\n reverse: () => {},\n play: () => {},\n playForward: () => {},\n playBackward: () => {},\n restart: () => {},\n get direction(): 'forward' | 'backward' { return 'forward'; },\n get isFinished() { return true; },\n get currentValue() { return new Map(); },\n finished: Promise.resolve(),\n _targetNodeIds: targets.nodes ? Object.keys(targets.nodes) : undefined,\n };\n options.onComplete?.();\n return noop;\n }\n\n // ── Animated case ───────────────────────────────────────────\n\n // Unwrap the Alpine proxy so the Animator's internal Maps/Sets use\n // correct reference equality (Proxy(obj) !== obj breaks _cleanup).\n const rawAnimator = getAlpine().raw(ctx._animator!) as Animator;\n const handle = rawAnimator.animate(entries, {\n duration,\n easing: options.easing,\n delay: options.delay,\n loop: options.loop,\n startAt: options.startAt,\n while: options.while,\n whileStopMode: options.whileStopMode,\n tag: options.tag,\n tags: options.tags,\n motion: options.motion,\n maxDuration: options.maxDuration,\n onProgress(progress) {\n // Flush moved nodes to DOM each frame\n if (movedNodeIds.size > 0) {\n ctx._flushNodePositions(movedNodeIds);\n ctx._refreshEdgePaths(movedNodeIds);\n movedNodeIds.clear();\n }\n // Flush styled nodes to DOM each frame\n if (styledNodeIds.size > 0) {\n ctx._flushNodeStyles(styledNodeIds);\n styledNodeIds.clear();\n }\n // Flush styled edges to DOM each frame\n if (styledEdgeIds.size > 0) {\n ctx._flushEdgeStyles(styledEdgeIds);\n styledEdgeIds.clear();\n }\n // Flush viewport transform when animating pan/zoom\n if (targets.viewport) {\n ctx._flushViewport();\n }\n options.onProgress?.(progress);\n },\n onComplete() {\n // Final frame: sync Alpine reactive state from the raw values\n // that the animator already applied (respects reverse direction).\n if (targets.nodes) {\n for (const [nodeId, target] of Object.entries(targets.nodes)) {\n const node = ctx._nodeMap.get(nodeId);\n if (!node) continue;\n const raw = getAlpine().raw(node);\n if (target.followPath || target.position?.x !== undefined) node.position.x = raw.position.x;\n if (target.followPath || target.position?.y !== undefined) node.position.y = raw.position.y;\n if (target.style !== undefined) node.style = raw.style;\n }\n }\n // Final frame: sync Alpine reactive state for edges\n if (targets.edges) {\n for (const [edgeId, target] of Object.entries(targets.edges)) {\n const edge = ctx._edgeMap.get(edgeId);\n if (!edge) continue;\n const raw = getAlpine().raw(edge);\n if (target.color !== undefined && typeof target.color === 'string') edge.color = raw.color;\n if (target.strokeWidth !== undefined) edge.strokeWidth = raw.strokeWidth;\n }\n }\n // Flush any moved/styled IDs left over from entry.apply() writes that\n // ran after the last onProgress tick — notably stop({mode:'rollback'})\n // and stop({mode:'jump-end'}) both mutate raw state during cleanup,\n // and without this flush the DOM stays at the last-animated position.\n if (movedNodeIds.size > 0) {\n ctx._flushNodePositions(movedNodeIds);\n ctx._refreshEdgePaths(movedNodeIds);\n settleEdgeGeometry(movedNodeIds);\n movedNodeIds.clear();\n }\n if (styledNodeIds.size > 0) {\n ctx._flushNodeStyles(styledNodeIds);\n styledNodeIds.clear();\n }\n if (styledEdgeIds.size > 0) {\n ctx._flushEdgeStyles(styledEdgeIds);\n styledEdgeIds.clear();\n }\n options.onComplete?.();\n },\n });\n\n // Attach target node IDs so follow() can resolve position\n if (targets.nodes) {\n (handle as FlowAnimationHandle)._targetNodeIds = Object.keys(targets.nodes);\n }\n\n return handle;\n },\n\n /**\n * Animate nodes, edges, and/or the viewport with smooth transitions.\n *\n * Convenience wrapper around `update()` that defaults to 300ms duration.\n * Pass `duration: 0` for instant changes, or use `update()` directly.\n *\n * When `respectReducedMotion` is active (via config or OS media query),\n * the effective duration is collapsed to 0 for an instant snap.\n */\n animate(\n targets: AnimateTargets,\n options: AnimateOptions = {},\n ): FlowAnimationHandle {\n const effectiveDuration = checkReducedMotion(ctx._config?.respectReducedMotion) ? 0 : (options.duration ?? 300);\n return this.update(targets, { ...options, duration: effectiveDuration });\n },\n\n // ── Follow (viewport tracking) ────────────────────────────────────────\n\n /**\n * Track a target with the viewport camera. The target can be a node ID,\n * a ParticleHandle, an animation handle, or a static XYPosition.\n * The viewport smoothly follows via engine tick with linear interpolation.\n */\n follow(\n target: string | FlowAnimationHandle | ParticleHandle | XYPosition,\n options: FollowOptions = {},\n ): FlowAnimationHandle {\n // Stop any existing follow\n if (ctx._followHandle) {\n ctx._followHandle.stop();\n }\n\n let resolveFinished: () => void;\n const finished = new Promise((r) => { resolveFinished = r; });\n let stopped = false;\n\n const targetZoom = options.zoom;\n\n const engineHandle = engine.register(() => {\n if (stopped) return true;\n\n let pos: XYPosition | null = null;\n\n if (typeof target === 'string') {\n const node = ctx._nodeMap.get(target);\n if (node) {\n pos = node.parentId ? ctx.getAbsolutePosition(target) : { ...node.position };\n const followOrig = node.nodeOrigin ?? ctx._config.nodeOrigin ?? [0, 0];\n if (node.dimensions) {\n pos.x += node.dimensions.width * (0.5 - followOrig[0]);\n pos.y += node.dimensions.height * (0.5 - followOrig[1]);\n }\n }\n } else if ('_targetNodeIds' in target && (target as FlowAnimationHandle)._targetNodeIds?.length) {\n // FlowAnimationHandle — follow the first animated node's reactive position\n const animNodeId = (target as FlowAnimationHandle)._targetNodeIds![0];\n const node = ctx._nodeMap.get(animNodeId);\n if (node) {\n pos = node.parentId ? ctx.getAbsolutePosition(animNodeId) : { ...node.position };\n const followOrig = node.nodeOrigin ?? ctx._config.nodeOrigin ?? [0, 0];\n if (node.dimensions) {\n pos.x += node.dimensions.width * (0.5 - followOrig[0]);\n pos.y += node.dimensions.height * (0.5 - followOrig[1]);\n }\n }\n } else if ('getCurrentPosition' in target && typeof (target as any).getCurrentPosition === 'function') {\n const particlePos = (target as ParticleHandle).getCurrentPosition();\n if (particlePos) {\n pos = particlePos;\n } else {\n // Particle completed — auto-stop follow\n stopped = true;\n engineHandle.stop();\n ctx._followHandle = null;\n resolveFinished!();\n return true;\n }\n } else if ('x' in target && 'y' in target) {\n pos = target as XYPosition;\n }\n\n if (!pos) return false;\n\n const dims = ctx._container\n ? { width: ctx._container.clientWidth, height: ctx._container.clientHeight }\n : { width: 800, height: 600 };\n const zoom = targetZoom ?? ctx.viewport.zoom;\n const vpX = dims.width / 2 - pos.x * zoom;\n const vpY = dims.height / 2 - pos.y * zoom;\n\n const lerp = 0.08;\n ctx.viewport.x += (vpX - ctx.viewport.x) * lerp;\n ctx.viewport.y += (vpY - ctx.viewport.y) * lerp;\n if (targetZoom) {\n ctx.viewport.zoom += (targetZoom - ctx.viewport.zoom) * lerp;\n }\n\n ctx._flushViewport();\n return false;\n });\n\n ctx._followHandle = engineHandle;\n\n // Auto-stop follow when animation handle finishes\n if (typeof target === 'object' && '_targetNodeIds' in target && (target as FlowAnimationHandle).finished) {\n (target as FlowAnimationHandle).finished.then(() => {\n if (!stopped) {\n stopped = true;\n engineHandle.stop();\n ctx._followHandle = null;\n resolveFinished!();\n }\n });\n }\n\n const handle: FlowAnimationHandle = {\n pause: () => {},\n resume: () => {},\n stop: () => {\n stopped = true;\n engineHandle.stop();\n ctx._followHandle = null;\n resolveFinished!();\n },\n reverse: () => {},\n play: () => {},\n playForward: () => {},\n playBackward: () => {},\n restart: () => {},\n get direction(): 'forward' | 'backward' { return 'forward'; },\n get isFinished() { return stopped; },\n get currentValue() { return new Map(); },\n get finished() { return finished; },\n };\n\n return handle;\n },\n\n // ── Registry & group helpers ─────────────────────────────────────────\n\n /**\n * Get all tracked animation handles, optionally filtered by tag.\n */\n getHandles(filter?: { tag?: string; tags?: string[] }): FlowAnimationHandle[] {\n const rawAnimator = getAlpine().raw(ctx._animator!) as Animator;\n return rawAnimator.registry.getHandles(filter) as FlowAnimationHandle[];\n },\n\n /**\n * Cancel all animations matching a tag filter.\n */\n cancelAll(filter: { tag?: string; tags?: string[] }, options?: StopOptions): void {\n const rawAnimator = getAlpine().raw(ctx._animator!) as Animator;\n rawAnimator.registry.cancelAll(filter, options);\n },\n\n /**\n * Pause all animations matching a tag filter.\n */\n pauseAll(filter: { tag?: string; tags?: string[] }): void {\n const rawAnimator = getAlpine().raw(ctx._animator!) as Animator;\n rawAnimator.registry.pauseAll(filter);\n },\n\n /**\n * Resume all animations matching a tag filter.\n */\n resumeAll(filter: { tag?: string; tags?: string[] }): void {\n const rawAnimator = getAlpine().raw(ctx._animator!) as Animator;\n rawAnimator.registry.resumeAll(filter);\n },\n\n /**\n * Create a named group that auto-tags all animations made through it.\n */\n group(name: string): FlowGroup {\n const self = this;\n return new FlowGroup(name, {\n animate: (t, o) => self.animate(t, o),\n update: (t, o) => self.update(t, o),\n sendParticle: (id, o) => self.sendParticle(id, o),\n sendParticleAlongPath: (p, o) => self.sendParticleAlongPath(p, o),\n sendParticleBetween: (s, t, o) => self.sendParticleBetween(s, t, o),\n sendParticleBurst: (id, o) => self.sendParticleBurst(id, o),\n sendConverging: (ids, o) => self.sendConverging(ids, o),\n timeline: () => self.timeline(),\n getHandles: (f) => self.getHandles(f),\n cancelAll: (f, o) => self.cancelAll(f, o),\n pauseAll: (f) => self.pauseAll(f),\n resumeAll: (f) => self.resumeAll(f),\n });\n },\n\n /**\n * Create a transaction for grouped rollback of multiple animations.\n */\n transaction(fn: () => Promise | void): Transaction {\n const rawAnimator = getAlpine().raw(ctx._animator!) as Animator;\n const tx = rawAnimator.beginTransaction();\n // Rollback writes raw-state values (via entry.apply) outside any\n // active rAF tick, so the DOM never sees them. Flush affected nodes\n // and their edges once properties have been reverted.\n tx.onAfterRollback?.((keys) => {\n const nodeIds = new Set();\n for (const key of keys) {\n // Keys look like `node::position.x` or `node::style.*`\n if (key.startsWith('node:')) {\n const id = key.split(':')[1];\n if (id) nodeIds.add(id);\n }\n }\n if (nodeIds.size > 0) {\n ctx._flushNodePositions(nodeIds);\n ctx._flushNodeStyles(nodeIds);\n ctx._refreshEdgePaths(nodeIds);\n }\n });\n try {\n const result = fn();\n if (result && typeof (result as any).then === 'function') {\n (result as Promise)\n .then(() => rawAnimator.endTransaction())\n .catch(() => {\n tx.rollback();\n rawAnimator.endTransaction();\n });\n } else {\n rawAnimator.endTransaction();\n }\n } catch (err) {\n tx.rollback();\n rawAnimator.endTransaction();\n throw err;\n }\n return tx;\n },\n\n /**\n * Capture current canvas state. Call restore() to revert.\n */\n snapshot(): { restore: () => void } {\n const savedNodes = structuredClone(getAlpine().raw(ctx.nodes));\n const savedEdges = structuredClone(getAlpine().raw(ctx.edges));\n const savedViewport = { ...ctx.viewport };\n return {\n restore: () => {\n ctx.nodes.splice(0, ctx.nodes.length, ...structuredClone(savedNodes));\n ctx.edges.splice(0, ctx.edges.length, ...structuredClone(savedEdges));\n Object.assign(ctx.viewport, savedViewport);\n },\n };\n },\n\n // ── Record & Replay ───────────────────────────────────────────────────\n\n /**\n * Record canvas animation events during `fn()` execution.\n * Returns a `Recording` that can be passed to `replay()`.\n */\n record(fn: () => Promise | void, options?: RecordOptions): Promise {\n // The Recorder hooks methods on a RecorderCanvas facade object. To ensure\n // that calls to $flow.animate / $flow.update etc. during fn() are captured,\n // we temporarily replace the mixin's own method slots with recording-aware\n // wrappers, and restore them once recording completes.\n const self = this;\n\n // Save direct function references to the real mixin methods.\n const realAnimate = self.animate;\n const realUpdate = self.update;\n const realSendParticle = self.sendParticle;\n const realSendParticleAlongPath = self.sendParticleAlongPath;\n const realSendParticleBetween = self.sendParticleBetween;\n const realSendParticleBurst = self.sendParticleBurst;\n const realSendConverging = self.sendConverging;\n\n // Build a facade with real methods as its initial implementations.\n // The Recorder will wrap these with event-capturing hooks.\n // NOTE: facade.animate → realAnimate → this.update. To avoid double-\n // recording when animate internally calls update, we use realUpdate\n // (bypassing the hook) as the update impl inside the animate call chain.\n // We achieve this by temporarily un-patching self.update during animate.\n const facade: any = {\n get nodes() { return ctx.nodes; },\n get edges() { return ctx.edges; },\n get viewport() { return ctx.viewport; },\n animate: (targets: any, opts?: any) => {\n // Temporarily restore real update so animate's internal update call\n // is not double-recorded as an 'update' event.\n const savedUpdate = (self as any).update;\n (self as any).update = realUpdate;\n try {\n return realAnimate.call(self, targets, opts);\n } finally {\n (self as any).update = savedUpdate;\n }\n },\n update: (targets: any, opts?: any) => realUpdate.call(self, targets, opts),\n sendParticle: (edgeId: string, opts?: any) => realSendParticle.call(self, edgeId, opts),\n sendParticleAlongPath: (path: string, opts?: any) => realSendParticleAlongPath.call(self, path, opts),\n sendParticleBetween: (source: string, target: string, opts?: any) => realSendParticleBetween.call(self, source, target, opts),\n sendParticleBurst: (edgeId: string, opts: any) => realSendParticleBurst.call(self, edgeId, opts),\n sendConverging: (sources: string[], opts: any) => realSendConverging.call(self, sources, opts),\n addNodes: (nodes: any) => ctx.addNodes(nodes),\n removeNodes: (ids: any) => ctx.removeNodes(ids),\n addEdges: (edges: any) => ctx.addEdges(edges),\n removeEdges: (ids: any) => ctx.removeEdges(ids),\n };\n\n const recorder = new Recorder(facade, options);\n\n // Wrap fn() so that $flow.animate / $flow.update calls during recording\n // are routed through the hooked facade (which records events). Restore\n // the originals once recording completes.\n const wrappedFn = async () => {\n (self as any).animate = (...args: any[]) => facade.animate(...args);\n (self as any).update = (...args: any[]) => facade.update(...args);\n (self as any).sendParticle = (...args: any[]) => facade.sendParticle(...args);\n (self as any).sendParticleAlongPath = (...args: any[]) => facade.sendParticleAlongPath(...args);\n (self as any).sendParticleBetween = (...args: any[]) => facade.sendParticleBetween(...args);\n (self as any).sendParticleBurst = (...args: any[]) => facade.sendParticleBurst(...args);\n (self as any).sendConverging = (...args: any[]) => facade.sendConverging(...args);\n try {\n const result = fn();\n if (result instanceof Promise) {\n await result;\n }\n } finally {\n (self as any).animate = realAnimate;\n (self as any).update = realUpdate;\n (self as any).sendParticle = realSendParticle;\n (self as any).sendParticleAlongPath = realSendParticleAlongPath;\n (self as any).sendParticleBetween = realSendParticleBetween;\n (self as any).sendParticleBurst = realSendParticleBurst;\n (self as any).sendConverging = realSendConverging;\n }\n };\n\n return recorder.record(wrappedFn, options?.captureMetadata);\n },\n\n /**\n * Replay a previously recorded `Recording` on this canvas.\n * Returns a `ReplayHandle` with play/pause/stop/scrub controls.\n */\n replay(recording: Recording, options?: ReplayOptions): ReplayHandle {\n const self = this;\n // Expose structural + particle methods to the replay handle so that\n // recordings containing node-add/remove, edge-add/remove, or particle\n // emissions replay with the right side effects. Without these the handle\n // falls back to raw array splicing (structural) or silently drops\n // particle events entirely.\n const replayCanvas = {\n get nodes() { return ctx.nodes; },\n get edges() { return ctx.edges; },\n get viewport() { return ctx.viewport; },\n addNodes: (nodes: any) => (self as any).addNodes(nodes),\n removeNodes: (ids: any) => (self as any).removeNodes(ids),\n addEdges: (edges: any) => (self as any).addEdges(edges),\n removeEdges: (ids: any) => (self as any).removeEdges(ids),\n sendParticle: (edgeId: string, opts: any) => (self as any).sendParticle(edgeId, opts),\n sendParticleAlongPath: (path: string, opts: any) => (self as any).sendParticleAlongPath(path, opts),\n sendParticleBetween: (source: string, target: string, opts: any) => (self as any).sendParticleBetween(source, target, opts),\n sendParticleBurst: (edgeId: string, opts: any) => (self as any).sendParticleBurst(edgeId, opts),\n sendConverging: (sources: string[], opts: any) => (self as any).sendConverging(sources, opts),\n };\n return new ReplayHandle(replayCanvas as any, recording, options);\n },\n\n // ── Cleanup lifecycle ─────────────────────────────────────────────────\n\n /**\n * Stop all in-flight animations, particles, and timelines.\n *\n * Called BY the canvas `destroy()` lifecycle hook when the element is removed\n * from the DOM — it must not be named `destroy()`. Mixins are applied onto the\n * canvas with `Object.defineProperties`, so a `destroy` here would OVERWRITE\n * `flowCanvas`'s own `destroy()` and silently take the entire canvas teardown\n * (listener removal, panZoom/minimap disposal, store unregistration) out of the\n * lifecycle. It did exactly that until this rename.\n */\n _destroyAnimations(): void {\n if (ctx._animator) {\n ctx._animator.stopAll();\n }\n particles.destroyParticles();\n for (const tl of ctx._activeTimelines) {\n tl.stop();\n }\n ctx._activeTimelines.clear();\n },\n\n // ── Particle renderer registry ────────────────────────────────────────\n\n /**\n * Register a custom particle renderer by name. Once registered, pass\n * `renderer: 'your-name'` in any `sendParticle*` options to use it.\n */\n registerParticleRenderer(name: string, renderer: ParticleRenderer): void {\n _registerParticleRenderer(name, renderer);\n },\n\n // ── Particle system (delegated to canvas-particles sub-mixin) ────────\n\n _tickParticles: particles._tickParticles,\n sendParticle: particles.sendParticle,\n sendParticleAlongPath: particles.sendParticleAlongPath,\n sendParticleBetween: particles.sendParticleBetween,\n sendParticleBurst: particles.sendParticleBurst,\n sendConverging: particles.sendConverging,\n destroyParticles: particles.destroyParticles,\n };\n}\n","// ============================================================================\n// Collapse/Expand — Core Logic\n//\n// Pure functions for collapsing and expanding nodes. Two behaviors:\n// - Group nodes (type === 'group'): hide children via parentId hierarchy\n// - Regular nodes: hide direct outgoers via edges\n//\n// These handle data mutations (hidden flags, dimension changes, edge rerouting)\n// but not animation — that's handled by the canvas methods that call these.\n// ============================================================================\n\nimport type { FlowNode, FlowEdge, Dimensions, XYPosition } from './types';\nimport { getDescendantIds } from './sub-flow';\nimport { getOutgoers } from './graph';\n\n/** Saved state needed to restore a node after expanding. */\nexport interface CollapseState {\n targetPositions: Map;\n originalDimensions?: Dimensions;\n reroutedEdges: Map;\n}\n\n/**\n * Determine which nodes to hide when collapsing a given node.\n *\n * Group nodes: returns all descendants via parentId hierarchy.\n * Regular nodes: returns direct outgoers via edges, or all downstream\n * outgoers when recursive is true.\n */\nexport function getCollapseTargets(\n nodeId: string,\n nodes: FlowNode[],\n edges: FlowEdge[],\n options?: { recursive?: boolean },\n): Set {\n const node = nodes.find(n => n.id === nodeId);\n if (!node) return new Set();\n\n if (node.type === 'group') {\n return getDescendantIds(nodeId, nodes);\n }\n\n const targets = new Set();\n const directOutgoers = getOutgoers(nodeId, nodes, edges);\n for (const o of directOutgoers) {\n targets.add(o.id);\n }\n\n if (options?.recursive) {\n const queue = directOutgoers.map(o => o.id);\n while (queue.length > 0) {\n const currentId = queue.shift()!;\n const nextOutgoers = getOutgoers(currentId, nodes, edges);\n for (const o of nextOutgoers) {\n if (!targets.has(o.id) && o.id !== nodeId) {\n targets.add(o.id);\n queue.push(o.id);\n }\n }\n }\n }\n\n return targets;\n}\n\n/**\n * Capture the current state of a node before collapsing.\n */\nexport function captureCollapseState(\n node: FlowNode,\n allNodes: FlowNode[],\n targets: Set,\n): CollapseState {\n const targetPositions = new Map();\n\n for (const n of allNodes) {\n if (targets.has(n.id)) {\n targetPositions.set(n.id, { ...n.position });\n }\n }\n\n return {\n targetPositions,\n originalDimensions: node.type === 'group'\n ? { ...(node.dimensions ?? { width: 400, height: 300 }) }\n : undefined,\n reroutedEdges: new Map(),\n };\n}\n\n/**\n * Apply collapse: hide targets, optionally shrink node, set collapsed flag.\n * When collapsedDimensions is provided the node is resized (typical for groups);\n * when omitted the node keeps its current size (typical for regular nodes).\n */\nexport function applyCollapse(\n node: FlowNode,\n allNodes: FlowNode[],\n state: CollapseState,\n collapsedDimensions?: Dimensions,\n): void {\n node.collapsed = true;\n\n if (collapsedDimensions) {\n node.dimensions = { ...collapsedDimensions };\n }\n\n for (const n of allNodes) {\n if (state.targetPositions.has(n.id)) {\n n.hidden = true;\n }\n }\n}\n\n/**\n * Apply expand: unhide targets, optionally restore dimensions, restore positions.\n * For group nodes, respects nested collapsed groups — their descendants stay hidden.\n */\nexport function applyExpand(\n node: FlowNode,\n allNodes: FlowNode[],\n state: CollapseState,\n restoreDimensions: boolean = true,\n): void {\n node.collapsed = false;\n\n if (restoreDimensions && state.originalDimensions) {\n node.dimensions = { ...state.originalDimensions };\n }\n\n // For group nodes: build set of IDs that belong to a nested collapsed group\n const nestedCollapsedIds = new Set();\n if (node.type === 'group') {\n for (const n of allNodes) {\n if (n.collapsed && n.id !== node.id && state.targetPositions.has(n.id)) {\n const innerDescendants = getDescendantIds(n.id, allNodes);\n for (const id of innerDescendants) {\n nestedCollapsedIds.add(id);\n }\n }\n }\n }\n\n for (const n of allNodes) {\n if (state.targetPositions.has(n.id)) {\n const savedPos = state.targetPositions.get(n.id)!;\n n.position = { ...savedPos };\n\n if (!nestedCollapsedIds.has(n.id)) {\n n.hidden = false;\n }\n }\n }\n}\n\n/**\n * Reroute edges for collapse. Trigger-aware: edges between the trigger and\n * hidden nodes are hidden (to prevent self-loops). External edges crossing\n * the boundary are rerouted to the trigger. Returns saved originals for restore.\n */\nexport function rerouteEdgesForCollapse(\n triggerId: string,\n edges: FlowEdge[],\n hiddenSet: Set,\n): Map {\n const saved = new Map();\n\n for (const edge of edges) {\n const srcIsHidden = hiddenSet.has(edge.source);\n const tgtIsHidden = hiddenSet.has(edge.target);\n const srcIsTrigger = edge.source === triggerId;\n const tgtIsTrigger = edge.target === triggerId;\n\n if (!srcIsHidden && !tgtIsHidden) continue;\n\n saved.set(edge.id, { source: edge.source, target: edge.target, hidden: edge.hidden });\n\n if (srcIsHidden && tgtIsHidden) {\n edge.hidden = true;\n } else if (srcIsTrigger && tgtIsHidden) {\n edge.hidden = true;\n } else if (srcIsHidden && tgtIsTrigger) {\n edge.hidden = true;\n } else if (srcIsHidden) {\n edge.source = triggerId;\n } else {\n edge.target = triggerId;\n }\n }\n\n return saved;\n}\n\n/**\n * Restore edges to their original source/target after expanding.\n */\nexport function restoreReroutedEdges(\n edges: FlowEdge[],\n saved: Map,\n): void {\n for (const edge of edges) {\n const original = saved.get(edge.id);\n if (original) {\n edge.source = original.source;\n edge.target = original.target;\n if (original.hidden !== undefined) {\n edge.hidden = original.hidden;\n } else {\n delete edge.hidden;\n }\n }\n }\n}\n","// ============================================================================\n// canvas-collapse — Collapse/expand mixin for flow-canvas\n//\n// Six methods that manage collapsing and expanding nodes, including animated\n// transitions. Group nodes shrink to collapsed dimensions and hide children;\n// regular nodes hide downstream outgoers via edge rerouting.\n//\n// Cross-mixin deps (optional chaining): ctx.animate, ctx.getEdgeElement\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport { debug } from '../../core/debug';\nimport {\n getCollapseTargets,\n captureCollapseState,\n applyCollapse,\n applyExpand,\n rerouteEdgesForCollapse,\n restoreReroutedEdges,\n} from '../../core/collapse';\nimport { getDescendantIds } from '../../core/sub-flow';\n\n/** Fallback dimensions when a node has no measured dimensions yet. */\nconst DEFAULT_NODE_DIMS = { width: 150, height: 50 };\n\nexport function createCollapseMixin(ctx: CanvasContext) {\n return {\n /**\n * Collapse a node — hide its descendants/outgoers and optionally animate.\n */\n collapseNode(id: string, options?: { animate?: boolean; recursive?: boolean }): void {\n const node = ctx._nodeMap.get(id);\n if (!node || node.collapsed) return;\n\n const targets = getCollapseTargets(id, ctx.nodes, ctx.edges, { recursive: options?.recursive });\n if (targets.size === 0) return;\n\n debug('collapse', `Collapsing node \"${id}\"`, {\n type: node.type ?? 'default',\n descendants: [...targets],\n animate: options?.animate !== false,\n recursive: options?.recursive ?? false,\n });\n\n ctx._captureHistory();\n\n const isGroup = node.type === 'group';\n const collapsedDims = isGroup\n ? (node.collapsedDimensions ?? { width: 150, height: 60 })\n : undefined;\n const shouldAnimate = options?.animate !== false;\n const state = captureCollapseState(node, ctx.nodes, targets);\n\n if (shouldAnimate) {\n ctx._suspendHistory();\n\n const nodeDims = node.dimensions ?? DEFAULT_NODE_DIMS;\n const refDims = isGroup && collapsedDims ? collapsedDims : nodeDims;\n\n const nodeTargets: Record = {};\n for (const [targetId] of state.targetPositions) {\n const targetNode = ctx._nodeMap.get(targetId);\n if (!targetNode) continue;\n const targetDims = targetNode.dimensions ?? DEFAULT_NODE_DIMS;\n\n let targetX: number;\n let targetY: number;\n if (targetNode.parentId === id) {\n targetX = (refDims.width - targetDims.width) / 2;\n targetY = (refDims.height - targetDims.height) / 2;\n } else {\n targetX = node.position.x + (refDims.width - targetDims.width) / 2;\n targetY = node.position.y + (refDims.height - targetDims.height) / 2;\n }\n\n nodeTargets[targetId] = {\n position: { x: targetX, y: targetY },\n style: { opacity: '0' },\n };\n }\n\n if (isGroup && collapsedDims) {\n nodeTargets[id] = { dimensions: collapsedDims };\n }\n\n const edgeSvgs: SVGElement[] = [];\n for (const edge of ctx.edges) {\n if (targets.has(edge.source) || targets.has(edge.target)) {\n const svg = ctx.getEdgeElement?.(edge.id)?.closest('svg');\n if (svg) edgeSvgs.push(svg as SVGElement);\n }\n }\n\n if (ctx.animate) {\n ctx.animate({ nodes: nodeTargets }, {\n duration: 300,\n easing: 'easeInOut',\n onProgress: (progress: number) => {\n const opacity = String(1 - progress);\n for (const svg of edgeSvgs) svg.style.opacity = opacity;\n },\n onComplete: () => {\n for (const svg of edgeSvgs) svg.style.opacity = '';\n applyCollapse(node, ctx.nodes, state, collapsedDims);\n state.reroutedEdges = rerouteEdgesForCollapse(id, ctx.edges, targets);\n ctx._collapseState.set(id, state);\n ctx._resumeHistory();\n ctx._emit('node-collapse', { node, descendants: [...targets] });\n },\n });\n } else {\n applyCollapse(node, ctx.nodes, state, collapsedDims);\n state.reroutedEdges = rerouteEdgesForCollapse(id, ctx.edges, targets);\n ctx._collapseState.set(id, state);\n ctx._resumeHistory();\n ctx._emit('node-collapse', { node, descendants: [...targets] });\n }\n } else {\n applyCollapse(node, ctx.nodes, state, collapsedDims);\n state.reroutedEdges = rerouteEdgesForCollapse(id, ctx.edges, targets);\n ctx._collapseState.set(id, state);\n ctx._emit('node-collapse', { node, descendants: [...targets] });\n }\n },\n\n /**\n * Expand a previously collapsed node — restore descendants/outgoers.\n */\n expandNode(id: string, options?: { animate?: boolean }): void {\n const node = ctx._nodeMap.get(id);\n if (!node || !node.collapsed) return;\n\n const state = ctx._collapseState.get(id);\n if (!state) return;\n\n debug('collapse', `Expanding node \"${id}\"`, {\n type: node.type ?? 'default',\n descendants: [...state.targetPositions.keys()],\n animate: options?.animate !== false,\n reroutedEdges: state.reroutedEdges.size,\n });\n\n ctx._captureHistory();\n\n const isGroup = node.type === 'group';\n const shouldAnimate = options?.animate !== false;\n\n if (state.reroutedEdges.size > 0) {\n restoreReroutedEdges(ctx.edges, state.reroutedEdges);\n }\n\n if (shouldAnimate) {\n ctx._suspendHistory();\n\n if (isGroup && state.originalDimensions) {\n node.dimensions = { ...state.originalDimensions };\n }\n\n const nodeDims = node.dimensions ?? DEFAULT_NODE_DIMS;\n\n applyExpand(node, ctx.nodes, state, isGroup);\n\n const nodeTargets: Record = {};\n for (const [targetId, savedPos] of state.targetPositions) {\n const targetNode = ctx._nodeMap.get(targetId);\n if (targetNode && !targetNode.hidden) {\n const targetDims = targetNode.dimensions ?? DEFAULT_NODE_DIMS;\n\n let startX: number;\n let startY: number;\n if (targetNode.parentId === id) {\n startX = (nodeDims.width - targetDims.width) / 2;\n startY = (nodeDims.height - targetDims.height) / 2;\n } else {\n startX = node.position.x + (nodeDims.width - targetDims.width) / 2;\n startY = node.position.y + (nodeDims.height - targetDims.height) / 2;\n }\n\n targetNode.position = { x: startX, y: startY };\n targetNode.style = { ...(targetNode.style as Record || {}), opacity: '0' };\n nodeTargets[targetId] = {\n position: savedPos,\n style: { opacity: '1' },\n };\n }\n }\n\n const expandTargetIds = new Set(state.targetPositions.keys());\n ctx._flushNodeStyles(expandTargetIds);\n\n const edgeSvgs: SVGElement[] = [];\n for (const edge of ctx.edges) {\n if (expandTargetIds.has(edge.source) || expandTargetIds.has(edge.target)) {\n const svg = ctx.getEdgeElement?.(edge.id)?.closest('svg');\n if (svg) {\n (svg as SVGElement).style.opacity = '0';\n edgeSvgs.push(svg as SVGElement);\n }\n }\n }\n\n if (ctx.animate) {\n ctx.animate({ nodes: nodeTargets }, {\n duration: 300,\n easing: 'easeOut',\n onProgress: (progress: number) => {\n const opacity = String(progress);\n for (const svg of edgeSvgs) svg.style.opacity = opacity;\n },\n onComplete: () => {\n for (const svg of edgeSvgs) svg.style.opacity = '';\n for (const targetId of expandTargetIds) {\n const targetNode = ctx._nodeMap.get(targetId);\n if (targetNode && typeof targetNode.style === 'object') {\n delete (targetNode.style as Record).opacity;\n }\n }\n ctx._resumeHistory();\n },\n });\n } else {\n ctx._resumeHistory();\n }\n\n ctx._collapseState.delete(id);\n ctx._emit('node-expand', { node, descendants: [...state.targetPositions.keys()] });\n } else {\n applyExpand(node, ctx.nodes, state, isGroup);\n ctx._collapseState.delete(id);\n ctx._emit('node-expand', { node, descendants: [...state.targetPositions.keys()] });\n }\n },\n\n /**\n * Toggle collapse/expand state of a node.\n */\n toggleNode(id: string, options?: { animate?: boolean; recursive?: boolean }): void {\n const node = ctx._nodeMap.get(id);\n if (!node) return;\n debug('collapse', `Toggle node \"${id}\" → ${node.collapsed ? 'expand' : 'collapse'}`);\n if (node.collapsed) {\n this.expandNode(id, options);\n } else {\n this.collapseNode(id, options);\n }\n },\n\n /**\n * Check if a node is collapsed.\n */\n isCollapsed(id: string): boolean {\n return ctx._nodeMap.get(id)?.collapsed === true;\n },\n\n /**\n * Get the number of nodes that would be hidden when collapsing this node.\n */\n getCollapseTargetCount(id: string): number {\n return getCollapseTargets(id, ctx.nodes, ctx.edges).size;\n },\n\n /**\n * Get the number of descendants (via parentId hierarchy) of a node.\n */\n getDescendantCount(id: string): number {\n return getDescendantIds(id, ctx.nodes).size;\n },\n };\n}\n","// ============================================================================\n// canvas-condense — Condense/uncondense mixin for flow-canvas\n//\n// Toggles a node's condensed flag (summary view vs full row view).\n// Four methods, zero dependencies on other mixins.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport { debug } from '../../core/debug';\n\nexport function createCondenseMixin(ctx: CanvasContext) {\n return {\n /**\n * Condense a node — switch to summary view hiding internal rows.\n */\n condenseNode(id: string): void {\n const node = ctx._nodeMap.get(id);\n if (!node || node.condensed) return;\n\n ctx._captureHistory();\n node.condensed = true;\n debug('condense', `Node \"${id}\" condensed`);\n ctx._emit('node-condense', { node });\n },\n\n /**\n * Uncondense a node — restore full row view.\n */\n uncondenseNode(id: string): void {\n const node = ctx._nodeMap.get(id);\n if (!node || !node.condensed) return;\n\n ctx._captureHistory();\n node.condensed = false;\n debug('condense', `Node \"${id}\" uncondensed`);\n ctx._emit('node-uncondense', { node });\n },\n\n /**\n * Toggle condensed state of a node.\n */\n toggleCondense(id: string): void {\n const node = ctx._nodeMap.get(id);\n if (!node) return;\n if (node.condensed) {\n this.uncondenseNode(id);\n } else {\n this.condenseNode(id);\n }\n },\n\n /**\n * Check if a node is condensed.\n */\n isCondensed(id: string): boolean {\n return ctx._nodeMap.get(id)?.condensed === true;\n },\n };\n}\n","// ============================================================================\n// canvas-rows — Row selection & row filtering mixin for flow-canvas\n//\n// Row selection: select/deselect/toggle rows, query selection state.\n// Row filtering: per-node filter (all | connected | unconnected | predicate)\n// that determines which schema rows are visible based on edge connectivity.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowEdge, RowFilter } from '../../core/types';\nimport { debug } from '../../core/debug';\n\nexport function createRowsMixin(ctx: CanvasContext) {\n return {\n // ── Row Selection ────────────────────────────────────────────────────\n\n selectRow(rowId: string): void {\n if (ctx.selectedRows.has(rowId)) return;\n ctx.selectedRows.add(rowId);\n const dotIdx = rowId.indexOf('.');\n const nodeId = dotIdx === -1 ? rowId : rowId.slice(0, dotIdx);\n const attrId = dotIdx === -1 ? '' : rowId.slice(dotIdx + 1);\n debug('selection', `Row \"${rowId}\" selected`);\n ctx._emit('row-select', { rowId, nodeId, attrId });\n ctx._emit('row-selection-change', { selectedRows: [...ctx.selectedRows] });\n },\n\n deselectRow(rowId: string): void {\n if (!ctx.selectedRows.has(rowId)) return;\n ctx.selectedRows.delete(rowId);\n const dotIdx = rowId.indexOf('.');\n const nodeId = dotIdx === -1 ? rowId : rowId.slice(0, dotIdx);\n const attrId = dotIdx === -1 ? '' : rowId.slice(dotIdx + 1);\n debug('selection', `Row \"${rowId}\" deselected`);\n ctx._emit('row-deselect', { rowId, nodeId, attrId });\n ctx._emit('row-selection-change', { selectedRows: [...ctx.selectedRows] });\n },\n\n toggleRowSelect(rowId: string): void {\n if (ctx.selectedRows.has(rowId)) {\n this.deselectRow(rowId);\n } else {\n this.selectRow(rowId);\n }\n },\n\n getSelectedRows(): string[] {\n return [...ctx.selectedRows];\n },\n\n isRowSelected(rowId: string): boolean {\n return ctx.selectedRows.has(rowId);\n },\n\n deselectAllRows(): void {\n if (ctx.selectedRows.size === 0) return;\n debug('selection', 'Deselecting all rows');\n ctx.selectedRows.clear();\n ctx._container?.querySelectorAll('.flow-row-selected').forEach((el: Element) => {\n el.classList.remove('flow-row-selected');\n });\n ctx._emit('row-selection-change', { selectedRows: [] });\n },\n\n // ── Row Filtering ────────────────────────────────────────────────────\n\n setRowFilter(nodeId: string, filter: RowFilter): void {\n const node = ctx._nodeMap.get(nodeId);\n if (!node) return;\n node.rowFilter = filter;\n debug('filter', `Node \"${nodeId}\" row filter set to \"${typeof filter === 'function' ? 'predicate' : filter}\"`);\n },\n\n getRowFilter(nodeId: string): RowFilter {\n return ctx._nodeMap.get(nodeId)?.rowFilter ?? 'all';\n },\n\n getVisibleRows(nodeId: string, schema: any[]): any[] {\n const node = ctx._nodeMap.get(nodeId);\n if (!node) return schema;\n const filter = node.rowFilter ?? 'all';\n if (filter === 'all') return schema;\n\n // Custom predicate — apply directly\n if (typeof filter === 'function') {\n return schema.filter(filter);\n }\n\n const connectedAttrIds = new Set();\n for (const edge of ctx.edges as FlowEdge[]) {\n if (edge.sourceHandle?.startsWith(nodeId + '.')) {\n const attrId = edge.sourceHandle.slice(nodeId.length + 1).replace(/-[lr]$/, '');\n if (attrId) connectedAttrIds.add(attrId);\n }\n if (edge.targetHandle?.startsWith(nodeId + '.')) {\n const attrId = edge.targetHandle.slice(nodeId.length + 1).replace(/-[lr]$/, '');\n if (attrId) connectedAttrIds.add(attrId);\n }\n }\n\n if (filter === 'connected') {\n return schema.filter((attr: any) => connectedAttrIds.has(attr.id));\n }\n return schema.filter((attr: any) => !connectedAttrIds.has(attr.id));\n },\n };\n}\n","// ============================================================================\n// Child Layout Computation\n//\n// Pure functions for arranging children within a layout parent.\n// Computes positions, optional dimension overrides, and parent auto-size.\n// No DOM or Alpine dependencies — works with plain FlowNode objects.\n// ============================================================================\n\nimport type { FlowNode, ChildLayout, XYPosition, Dimensions } from './types';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from './geometry';\n\n/** Default gap between children in px. */\nconst DEFAULT_GAP = 8;\n\n/** Default padding inside the parent in px. */\nconst DEFAULT_PADDING = 12;\n\n/** Default grid columns. */\nconst DEFAULT_COLUMNS = 2;\n\n/** Result of a child layout computation. */\nexport interface ChildLayoutResult {\n /** New positions for each child (relative to parent). */\n positions: Map;\n /** Dimension overrides for stretched children. Only populated when stretch applies. */\n dimensions: Map;\n /** Computed parent dimensions to fit all children. */\n parentDimensions: Dimensions;\n}\n\n/** Get the effective dimensions of a node, falling back to defaults. */\nfunction getNodeDims(node: FlowNode): Dimensions {\n return {\n width: node.dimensions?.width ?? DEFAULT_NODE_WIDTH,\n height: node.dimensions?.height ?? DEFAULT_NODE_HEIGHT,\n };\n}\n\n/** Resolve the default stretch mode for a direction. */\nfunction resolveStretch(config: ChildLayout): 'none' | 'width' | 'height' | 'both' {\n if (config.stretch) return config.stretch;\n switch (config.direction) {\n case 'vertical': return 'width';\n case 'horizontal': return 'height';\n case 'grid': return 'both';\n }\n}\n\n/** Sort children by order (then array index as tiebreaker). */\nfunction sortByOrder(children: FlowNode[]): FlowNode[] {\n return [...children].sort((a, b) => {\n const oa = a.order ?? Infinity;\n const ob = b.order ?? Infinity;\n if (oa !== ob) return oa - ob;\n return 0; // preserve insertion order (stable sort)\n });\n}\n\n/**\n * Compute child positions, dimension overrides, and parent auto-size\n * for a layout parent. Pure function — does not mutate any nodes.\n *\n * @param availableSize - When the parent has been externally sized (e.g. stretched\n * by its own parent), children stretch to fill this space instead of just maxW/maxH.\n */\nexport function computeChildLayout(\n children: FlowNode[],\n config: ChildLayout,\n availableSize?: Dimensions,\n): ChildLayoutResult {\n const gap = config.gap ?? DEFAULT_GAP;\n const padding = config.padding ?? DEFAULT_PADDING;\n const headerHeight = config.headerHeight ?? 0;\n const stretch = resolveStretch(config);\n const sorted = sortByOrder(children);\n\n const positions = new Map();\n const dimensions = new Map();\n\n if (sorted.length === 0) {\n // When externally sized (stretched by parent), retain that size even\n // when empty. Otherwise fall back to minimal padding-only dimensions.\n return {\n positions,\n dimensions,\n parentDimensions: availableSize\n ? { width: availableSize.width, height: availableSize.height }\n : { width: padding * 2, height: padding * 2 + headerHeight },\n };\n }\n\n // Available inner space when parent has been externally sized\n const innerW = availableSize ? availableSize.width - padding * 2 : 0;\n const innerH = availableSize ? availableSize.height - padding * 2 - headerHeight : 0;\n\n if (config.direction === 'vertical') {\n return layoutVertical(sorted, gap, padding, headerHeight, stretch, innerW, positions, dimensions);\n } else if (config.direction === 'horizontal') {\n return layoutHorizontal(sorted, gap, padding, headerHeight, stretch, innerH, positions, dimensions);\n } else {\n return layoutGrid(sorted, gap, padding, headerHeight, stretch, config.columns ?? DEFAULT_COLUMNS, innerW, innerH, positions, dimensions);\n }\n}\n\nfunction layoutVertical(\n sorted: FlowNode[],\n gap: number,\n padding: number,\n headerHeight: number,\n stretch: string,\n availableInnerW: number,\n positions: Map,\n dimensions: Map,\n): ChildLayoutResult {\n let maxW = 0;\n const dims = sorted.map(n => getNodeDims(n));\n for (const d of dims) maxW = Math.max(maxW, d.width);\n // When parent provides available space, use it as the stretch target\n // (both expanding small children AND constraining oversized ones).\n // When no available space, use the widest child as the stretch target.\n const stretchW = availableInnerW > 0 ? availableInnerW : maxW;\n\n let y = padding + headerHeight;\n for (let i = 0; i < sorted.length; i++) {\n const node = sorted[i];\n const d = dims[i];\n positions.set(node.id, { x: padding, y });\n\n if (stretch === 'width' || stretch === 'both') {\n dimensions.set(node.id, { width: stretchW, height: d.height });\n }\n\n y += d.height + gap;\n }\n y -= gap;\n y += padding;\n\n return {\n positions,\n dimensions,\n parentDimensions: { width: stretchW + padding * 2, height: y },\n };\n}\n\nfunction layoutHorizontal(\n sorted: FlowNode[],\n gap: number,\n padding: number,\n headerHeight: number,\n stretch: string,\n availableInnerH: number,\n positions: Map,\n dimensions: Map,\n): ChildLayoutResult {\n let maxH = 0;\n const dims = sorted.map(n => getNodeDims(n));\n for (const d of dims) maxH = Math.max(maxH, d.height);\n const stretchH = availableInnerH > 0 ? availableInnerH : maxH;\n\n let x = padding;\n for (let i = 0; i < sorted.length; i++) {\n const node = sorted[i];\n const d = dims[i];\n positions.set(node.id, { x, y: padding + headerHeight });\n\n if (stretch === 'height' || stretch === 'both') {\n dimensions.set(node.id, { width: d.width, height: stretchH });\n }\n\n x += d.width + gap;\n }\n x -= gap;\n x += padding;\n\n return {\n positions,\n dimensions,\n parentDimensions: { width: x, height: stretchH + padding * 2 + headerHeight },\n };\n}\n\nfunction layoutGrid(\n sorted: FlowNode[],\n gap: number,\n padding: number,\n headerHeight: number,\n stretch: string,\n configColumns: number,\n availableInnerW: number,\n availableInnerH: number,\n positions: Map,\n dimensions: Map,\n): ChildLayoutResult {\n // Clamp columns to actual child count — more columns than children\n // creates empty cells and wastes space.\n const columns = Math.min(configColumns, sorted.length);\n const dims = sorted.map(n => getNodeDims(n));\n\n let cellW = 0;\n let cellH = 0;\n for (const d of dims) {\n cellW = Math.max(cellW, d.width);\n cellH = Math.max(cellH, d.height);\n }\n\n // Use available space if parent was externally sized (both expand and constrain)\n const availCellW = availableInnerW > 0 ? (availableInnerW - (columns - 1) * gap) / columns : 0;\n if (availCellW > 0) cellW = availCellW;\n const rows = Math.ceil(sorted.length / columns);\n const availCellH = availableInnerH > 0 ? (availableInnerH - (rows - 1) * gap) / rows : 0;\n if (availCellH > 0) cellH = availCellH;\n\n for (let i = 0; i < sorted.length; i++) {\n const col = i % columns;\n const row = Math.floor(i / columns);\n const x = padding + col * (cellW + gap);\n const y = padding + headerHeight + row * (cellH + gap);\n positions.set(sorted[i].id, { x, y });\n\n if (stretch === 'both') {\n dimensions.set(sorted[i].id, { width: cellW, height: cellH });\n } else if (stretch === 'width') {\n dimensions.set(sorted[i].id, { width: cellW, height: dims[i].height });\n } else if (stretch === 'height') {\n dimensions.set(sorted[i].id, { width: dims[i].width, height: cellH });\n }\n }\n\n return {\n positions,\n dimensions,\n parentDimensions: {\n width: columns * cellW + (columns - 1) * gap + padding * 2,\n height: rows * cellH + (rows - 1) * gap + padding * 2 + headerHeight,\n },\n };\n}\n","// ============================================================================\n// canvas-layout — Layout engines, child layout, and auto-layout mixin for\n// flow-canvas\n//\n// Public API: layout, forceLayout, treeLayout, elkLayout, layoutChildren,\n// reorderChild.\n// Internal: _scheduleAutoLayout, _runAutoLayout, _applyLayout,\n// _adjustHandlePositions.\n//\n// Ten methods covering Dagre/Force/Hierarchy/ELK layout engines, child layout\n// computation with recursive stretch propagation, reorder, and auto-layout\n// scheduling with debounce.\n//\n// Cross-mixin deps (via ctx): fitView.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowNode, Dimensions, HandlePosition } from '../../core/types';\nimport type { ElkAlgorithm, ElkDirection } from '../../core/layout/elk';\nimport type { HierarchyLayoutType, HierarchyDirection } from '../../core/layout/hierarchy';\nimport { computeChildLayout } from '../../core/child-layout';\nimport { debug } from '../../core/debug';\nimport { getAddon } from '../../core/registry';\n\nexport function createLayoutMixin(ctx: CanvasContext) {\n return {\n // ── Auto-layout scheduling ─────────────────────────────────────────────\n\n /**\n * Debounced trigger for automatic layout.\n *\n * Skips when no autoLayout config is set, dependencies haven't loaded,\n * or the auto-layout has permanently failed.\n */\n _scheduleAutoLayout(): void {\n const alc = ctx._config.autoLayout;\n if (!alc || !ctx._autoLayoutReady || ctx._autoLayoutFailed) return;\n if (ctx._autoLayoutTimer) clearTimeout(ctx._autoLayoutTimer);\n ctx._autoLayoutTimer = setTimeout(() => {\n ctx._autoLayoutTimer = null;\n this._runAutoLayout();\n }, alc.debounce ?? 50);\n },\n\n /**\n * Execute the configured auto-layout algorithm.\n *\n * Delegates to the appropriate layout engine method based on\n * `config.autoLayout.algorithm`. Catches errors and sets\n * `_autoLayoutFailed` to prevent repeated attempts.\n */\n async _runAutoLayout(): Promise {\n const alc = ctx._config.autoLayout;\n if (!alc) return;\n const opts = {\n fitView: alc.fitView !== false,\n duration: alc.duration ?? 300,\n };\n try {\n switch (alc.algorithm) {\n case 'dagre':\n this.layout({\n direction: alc.direction,\n nodesep: alc.nodesep,\n ranksep: alc.ranksep,\n adjustHandles: alc.adjustHandles,\n ...opts,\n });\n break;\n case 'force':\n this.forceLayout({\n strength: alc.strength,\n distance: alc.distance,\n charge: alc.charge,\n iterations: alc.iterations,\n ...opts,\n });\n break;\n case 'hierarchy':\n this.treeLayout({\n layoutType: alc.layoutType as any,\n nodeWidth: alc.nodeWidth,\n nodeHeight: alc.nodeHeight,\n adjustHandles: alc.adjustHandles,\n ...opts,\n });\n break;\n case 'elk':\n await this.elkLayout({\n algorithm: alc.elkAlgorithm,\n nodeSpacing: alc.nodeSpacing,\n layerSpacing: alc.layerSpacing,\n adjustHandles: alc.adjustHandles,\n ...opts,\n });\n break;\n }\n } catch (err: any) {\n if (!ctx._autoLayoutFailed) {\n ctx._warn('AUTO_LAYOUT_FAILED', `autoLayout failed: ${err.message}`);\n ctx._autoLayoutFailed = true;\n }\n }\n },\n\n // ── Shared layout application ──────────────────────────────────────────\n\n /**\n * Apply computed layout positions to nodes with optional animation.\n *\n * When duration > 0, delegates to ctx.animate() for smooth transitions.\n * When duration === 0, applies positions directly (instant).\n * Calls `_adjustHandlePositions` when requested, and triggers fitView.\n */\n _applyLayout(\n positions: Map,\n options?: {\n adjustHandles?: boolean;\n handleDirection?: string;\n fitView?: boolean;\n duration?: number;\n },\n ): void {\n const duration = options?.duration ?? 300;\n\n debug('layout', `_applyLayout: repositioning ${positions.size} node(s)`, {\n duration,\n adjustHandles: options?.adjustHandles ?? false,\n fitView: options?.fitView !== false,\n });\n\n if (options?.adjustHandles && options.handleDirection) {\n this._adjustHandlePositions(options.handleDirection);\n }\n\n if (duration > 0) {\n // Build animate() targets\n const nodeTargets: Record = {};\n for (const [id, pos] of positions) {\n nodeTargets[id] = { position: pos };\n }\n\n ctx.animate?.({ nodes: nodeTargets }, {\n duration,\n easing: 'easeInOut' as any,\n onComplete: () => {\n if (options?.fitView !== false) {\n ctx.fitView?.({ padding: 0.2, duration });\n }\n },\n });\n } else {\n // Instant apply — no animation\n for (const node of ctx.nodes) {\n const pos = positions.get(node.id);\n if (pos) {\n if (!node.position) node.position = { x: 0, y: 0 };\n node.position.x = pos.x;\n node.position.y = pos.y;\n }\n }\n\n if (options?.fitView !== false) {\n ctx.fitView?.({ padding: 0.2, duration: 0 });\n }\n }\n },\n\n /**\n * Update handle positions on nodes and DOM elements to match a layout\n * direction (TB, LR, BT, RL, DOWN, RIGHT, UP, LEFT).\n *\n * Skips handles that have an explicit position set via\n * `data-flow-handle-explicit`.\n */\n _adjustHandlePositions(direction: string): void {\n const map: Record = {\n TB: { source: 'bottom', target: 'top' },\n DOWN: { source: 'bottom', target: 'top' },\n LR: { source: 'right', target: 'left' },\n RIGHT: { source: 'right', target: 'left' },\n BT: { source: 'top', target: 'bottom' },\n UP: { source: 'top', target: 'bottom' },\n RL: { source: 'left', target: 'right' },\n LEFT: { source: 'left', target: 'right' },\n };\n\n const positions = map[direction] ?? map['TB'];\n\n for (const node of ctx.nodes) {\n node.sourcePosition = positions.source;\n node.targetPosition = positions.target;\n }\n\n ctx._container?.querySelectorAll('[data-flow-handle-type=\"source\"]').forEach((el) => {\n if (!el.dataset.flowHandleExplicit) {\n el.dataset.flowHandlePosition = positions.source;\n }\n });\n ctx._container?.querySelectorAll('[data-flow-handle-type=\"target\"]').forEach((el) => {\n if (!el.dataset.flowHandleExplicit) {\n el.dataset.flowHandlePosition = positions.target;\n }\n });\n },\n\n // ── Child layout ───────────────────────────────────────────────────────\n\n /**\n * Compute and apply child layout for a parent node.\n *\n * Recursively lays out nested layout parents bottom-up (unless `shallow`\n * is true). Applies computed positions, dimension overrides with\n * min/max constraint clamping, and auto-sizes the parent.\n */\n /**\n * Compute and apply child layout for a parent node.\n *\n * Supports both the legacy positional signature and a new options object:\n *\n * layoutChildren(parentId) // full layout\n * layoutChildren(parentId, excludeId, shallow) // legacy (backward compat)\n * layoutChildren(parentId, { ... }) // options object\n *\n * Options:\n * - excludeId: skip applying position/dimensions but still count in computation\n * - omitFromComputation: fully remove node from child list (old parent shrinks)\n * - includeNode: add a virtual child to computation (new parent grows)\n * - shallow: don't recurse into nested layout children\n * - stretchedSize: externally-provided size for stretch propagation\n */\n layoutChildren(\n parentId: string,\n optsOrExcludeId?: string | {\n excludeId?: string;\n omitFromComputation?: string;\n includeNode?: FlowNode;\n shallow?: boolean;\n stretchedSize?: Dimensions;\n },\n shallowArg?: boolean,\n stretchedSizeArg?: Dimensions,\n ): void {\n // Normalize legacy positional args into options object\n let opts: {\n excludeId?: string;\n omitFromComputation?: string;\n includeNode?: FlowNode;\n shallow?: boolean;\n stretchedSize?: Dimensions;\n };\n if (typeof optsOrExcludeId === 'string') {\n opts = { excludeId: optsOrExcludeId, shallow: shallowArg, stretchedSize: stretchedSizeArg };\n } else {\n opts = optsOrExcludeId ?? {};\n }\n\n const { excludeId, omitFromComputation, includeNode, shallow } = opts;\n let { stretchedSize } = opts;\n\n // Use proxied references from ctx.nodes for reactivity\n const parent = (ctx.nodes as FlowNode[]).find((n: FlowNode) => n.id === parentId);\n if (!parent?.childLayout) return;\n\n // Build the child list for computation:\n // - omitFromComputation: fully remove from the list (parent auto-sizes without it)\n // - includeNode: add as virtual child (parent auto-sizes with it)\n let children = ctx.nodes.filter((n: FlowNode) => n.parentId === parentId);\n if (omitFromComputation) {\n children = children.filter((n: FlowNode) => n.id !== omitFromComputation);\n }\n if (includeNode && !children.some((n: FlowNode) => n.id === includeNode.id)) {\n children = [...children, includeNode];\n }\n\n const childMap = new Map(children.map((c: FlowNode) => [c.id, c]));\n\n // Clear stale parent dimensions\n parent.dimensions = undefined;\n\n // Top-down constraint: when this parent has maxDimensions and no\n // externally-provided stretchedSize, use maxDimensions as the\n // initial available space so children are constrained from the start\n // rather than auto-sized first and clamped after.\n if (!stretchedSize && parent.maxDimensions && parent.maxDimensions.width !== undefined && parent.maxDimensions.height !== undefined) {\n stretchedSize = { width: parent.maxDimensions.width, height: parent.maxDimensions.height };\n }\n\n // Recurse bottom-up\n if (!shallow) {\n for (const child of children) {\n if (child.childLayout) {\n this.layoutChildren(child.id, { excludeId, omitFromComputation, shallow: false });\n }\n }\n }\n\n // Auto-default headerHeight when parent has a label\n const config = parent.childLayout;\n const effectiveConfig = config.headerHeight !== undefined ? config\n : parent.data?.label ? { ...config, headerHeight: 30 } : config;\n\n const result = computeChildLayout(children, effectiveConfig, stretchedSize);\n\n // Apply positions (skip excludeId — it participates in computation but\n // isn't repositioned since it's being dragged)\n for (const [id, pos] of result.positions) {\n if (id === excludeId) continue;\n // Don't apply position to virtual includeNode (it's not in ctx.nodes)\n if (includeNode && id === includeNode.id && !ctx._nodeMap.has(id)) continue;\n const node = childMap.get(id);\n if (node) {\n if (!node.position) node.position = { x: pos.x, y: pos.y };\n else { node.position.x = pos.x; node.position.y = pos.y; }\n }\n }\n\n // Apply dimension overrides (stretch) + propagate to nested layout parents\n for (const [id, dims] of result.dimensions) {\n if (id === excludeId) continue;\n if (includeNode && id === includeNode.id && !ctx._nodeMap.has(id)) continue;\n const node = childMap.get(id);\n if (node) {\n let w = dims.width;\n let h = dims.height;\n if (node.minDimensions) {\n if (node.minDimensions.width != null) w = Math.max(w, node.minDimensions.width);\n if (node.minDimensions.height != null) h = Math.max(h, node.minDimensions.height);\n }\n if (node.maxDimensions) {\n if (node.maxDimensions.width != null) w = Math.min(w, node.maxDimensions.width);\n if (node.maxDimensions.height != null) h = Math.min(h, node.maxDimensions.height);\n }\n if (!node.dimensions) {\n node.dimensions = { width: w, height: h };\n } else {\n node.dimensions.width = w;\n node.dimensions.height = h;\n }\n // Stretch propagation\n if (node.childLayout && !shallow) {\n this.layoutChildren(id, { excludeId, omitFromComputation, shallow: false, stretchedSize: node.dimensions! });\n }\n }\n }\n\n // Auto-size parent\n let pw = result.parentDimensions.width;\n let ph = result.parentDimensions.height;\n if (parent.minDimensions) {\n if (parent.minDimensions.width != null) pw = Math.max(pw, parent.minDimensions.width);\n if (parent.minDimensions.height != null) ph = Math.max(ph, parent.minDimensions.height);\n }\n if (parent.maxDimensions) {\n if (parent.maxDimensions.width != null) pw = Math.min(pw, parent.maxDimensions.width);\n if (parent.maxDimensions.height != null) ph = Math.min(ph, parent.maxDimensions.height);\n }\n if (!parent.dimensions) parent.dimensions = { width: 0, height: 0 };\n parent.dimensions.width = pw;\n parent.dimensions.height = ph;\n\n // Re-compute with constrained size when clamping changed the parent.\n // This feeds the clamped dimensions back as stretchedSize so children\n // are properly sized within the constrained space (e.g. maxDimensions\n // on the topmost parent cascades down).\n const wasClamped = pw !== result.parentDimensions.width || ph !== result.parentDimensions.height;\n if (wasClamped) {\n const constrainedSize = { width: pw, height: ph };\n const reResult = computeChildLayout(children, effectiveConfig, constrainedSize);\n\n for (const [id, pos] of reResult.positions) {\n if (id === excludeId) continue;\n if (includeNode && id === includeNode.id && !ctx._nodeMap.has(id)) continue;\n const node = childMap.get(id);\n if (node) {\n if (!node.position) node.position = { x: pos.x, y: pos.y };\n else { node.position.x = pos.x; node.position.y = pos.y; }\n }\n }\n\n for (const [id, dims] of reResult.dimensions) {\n if (id === excludeId) continue;\n if (includeNode && id === includeNode.id && !ctx._nodeMap.has(id)) continue;\n const node = childMap.get(id);\n if (node) {\n let w = dims.width;\n let h = dims.height;\n if (node.minDimensions) {\n if (node.minDimensions.width != null) w = Math.max(w, node.minDimensions.width);\n if (node.minDimensions.height != null) h = Math.max(h, node.minDimensions.height);\n }\n if (node.maxDimensions) {\n if (node.maxDimensions.width != null) w = Math.min(w, node.maxDimensions.width);\n if (node.maxDimensions.height != null) h = Math.min(h, node.maxDimensions.height);\n }\n if (!node.dimensions) {\n node.dimensions = { width: w, height: h };\n } else {\n node.dimensions.width = w;\n node.dimensions.height = h;\n }\n // Stretch propagation with constrained size\n if (node.childLayout && !shallow) {\n this.layoutChildren(id, { excludeId, omitFromComputation, shallow: false, stretchedSize: node.dimensions! });\n }\n }\n }\n }\n },\n\n /**\n * Walk up from a parent through ancestor layout parents, calling\n * layoutChildren(shallow) at each level so parent resizes propagate\n * through the hierarchy (e.g. Column grows -> Row adjusts -> Step adjusts).\n */\n propagateLayoutUp(\n startParentId: string,\n opts?: {\n excludeId?: string;\n omitFromComputation?: string;\n includeNode?: FlowNode;\n },\n ): void {\n // Only forward omitFromComputation to ancestors — it's safe because\n // the node won't be in ancestor children lists (filters by parentId).\n // Do NOT forward includeNode — it only belongs in the direct target\n // parent. At ancestor levels, the updated child dimensions already\n // reflect the included node, so ancestors just need a plain re-compute.\n const ancestorOpts = opts?.omitFromComputation\n ? { omitFromComputation: opts.omitFromComputation }\n : undefined;\n\n let walkId: string | undefined = (ctx.nodes as FlowNode[]).find(\n (n: FlowNode) => n.id === startParentId,\n )?.parentId;\n while (walkId) {\n const ancestor = ctx._nodeMap.get(walkId);\n if (!ancestor?.childLayout) break;\n this.layoutChildren(walkId, { ...ancestorOpts, shallow: true });\n walkId = ancestor.parentId;\n }\n },\n\n /**\n * Reorder a child within its layout parent.\n *\n * Reassigns order values for all siblings, then runs layoutChildren\n * and emits a `child-reorder` event.\n */\n reorderChild(nodeId: string, newOrder: number): void {\n const node = ctx._nodeMap.get(nodeId);\n if (!node?.parentId) return;\n const parent = ctx._nodeMap.get(node.parentId);\n if (!parent?.childLayout) return;\n\n ctx._captureHistory();\n\n const siblings = ctx.nodes\n .filter((n: FlowNode) => n.parentId === node.parentId)\n .sort((a: FlowNode, b: FlowNode) => (a.order ?? Infinity) - (b.order ?? Infinity));\n\n const filtered = siblings.filter((s: FlowNode) => s.id !== nodeId);\n const insertIdx = Math.max(0, Math.min(newOrder, filtered.length));\n filtered.splice(insertIdx, 0, node);\n\n for (let i = 0; i < filtered.length; i++) {\n filtered[i].order = i;\n }\n\n this.layoutChildren(node.parentId);\n ctx._emit('child-reorder', { nodeId, parentId: node.parentId, order: insertIdx });\n },\n\n // ── Layout engines ─────────────────────────────────────────────────────\n\n /**\n * Apply Dagre (directed acyclic graph) layout.\n *\n * Requires the dagre addon to be registered via `Alpine.plugin(AlpineFlowDagre)`.\n *\n * Nodes with `parentId` are excluded by default — their positions are managed\n * by `childLayout`, not top-level auto-layout. Pass `{ includeChildren: true }`\n * to include them.\n */\n layout(options?: {\n direction?: 'TB' | 'LR' | 'BT' | 'RL';\n nodesep?: number;\n ranksep?: number;\n adjustHandles?: boolean;\n fitView?: boolean;\n duration?: number;\n includeChildren?: boolean;\n }): void {\n const computeDagreLayout = getAddon('layout:dagre');\n if (!computeDagreLayout) {\n throw new Error('layout() requires the dagre plugin. Register it with: Alpine.plugin(AlpineFlowDagre)');\n }\n const direction = options?.direction ?? 'TB';\n const layoutNodes = options?.includeChildren\n ? ctx.nodes\n : ctx.nodes.filter((n: FlowNode) => !n.parentId);\n const positions = computeDagreLayout(layoutNodes, ctx.edges, {\n direction,\n nodesep: options?.nodesep,\n ranksep: options?.ranksep,\n });\n\n this._applyLayout(positions, {\n adjustHandles: options?.adjustHandles,\n handleDirection: direction,\n fitView: options?.fitView,\n duration: options?.duration,\n });\n\n debug('layout', 'Applied dagre layout', { direction });\n ctx._emit('layout', { type: 'dagre', direction });\n },\n\n /**\n * Apply force-directed layout.\n *\n * Requires the force addon to be registered via `Alpine.plugin(AlpineFlowForce)`.\n *\n * Nodes with `parentId` are excluded by default — their positions are managed\n * by `childLayout`, not top-level auto-layout. Pass `{ includeChildren: true }`\n * to include them.\n */\n forceLayout(options?: {\n strength?: number;\n distance?: number;\n charge?: number;\n iterations?: number;\n center?: { x: number; y: number };\n fitView?: boolean;\n duration?: number;\n includeChildren?: boolean;\n }): void {\n const computeForceLayout = getAddon('layout:force');\n if (!computeForceLayout) {\n throw new Error('forceLayout() requires the force plugin. Register it with: Alpine.plugin(AlpineFlowForce)');\n }\n const layoutNodes = options?.includeChildren\n ? ctx.nodes\n : ctx.nodes.filter((n: FlowNode) => !n.parentId);\n const positions = computeForceLayout(layoutNodes, ctx.edges, {\n strength: options?.strength,\n distance: options?.distance,\n charge: options?.charge,\n iterations: options?.iterations,\n center: options?.center,\n });\n\n this._applyLayout(positions, {\n fitView: options?.fitView,\n duration: options?.duration,\n });\n\n debug('layout', 'Applied force layout', { charge: options?.charge ?? -300, distance: options?.distance ?? 150 });\n ctx._emit('layout', { type: 'force', charge: options?.charge ?? -300, distance: options?.distance ?? 150 });\n },\n\n /**\n * Apply hierarchy/tree layout.\n *\n * Requires the hierarchy addon to be registered via `Alpine.plugin(AlpineFlowHierarchy)`.\n *\n * Nodes with `parentId` are excluded by default — their positions are managed\n * by `childLayout`, not top-level auto-layout. Pass `{ includeChildren: true }`\n * to include them.\n */\n treeLayout(options?: {\n layoutType?: HierarchyLayoutType;\n direction?: HierarchyDirection;\n nodeWidth?: number;\n nodeHeight?: number;\n adjustHandles?: boolean;\n fitView?: boolean;\n duration?: number;\n includeChildren?: boolean;\n }): void {\n const computeHierarchyLayout = getAddon('layout:hierarchy');\n if (!computeHierarchyLayout) {\n throw new Error('treeLayout() requires the hierarchy plugin. Register it with: Alpine.plugin(AlpineFlowHierarchy)');\n }\n const direction = options?.direction ?? 'TB';\n const layoutNodes = options?.includeChildren\n ? ctx.nodes\n : ctx.nodes.filter((n: FlowNode) => !n.parentId);\n const positions = computeHierarchyLayout(layoutNodes, ctx.edges, {\n layoutType: options?.layoutType,\n direction,\n nodeWidth: options?.nodeWidth,\n nodeHeight: options?.nodeHeight,\n });\n\n this._applyLayout(positions, {\n adjustHandles: options?.adjustHandles,\n handleDirection: direction,\n fitView: options?.fitView,\n duration: options?.duration,\n });\n\n debug('layout', 'Applied tree layout', { layoutType: options?.layoutType ?? 'tree', direction });\n ctx._emit('layout', { type: 'tree', layoutType: options?.layoutType ?? 'tree', direction });\n },\n\n /**\n * Apply ELK (Eclipse Layout Kernel) layout.\n *\n * Requires the elk addon to be registered via `Alpine.plugin(AlpineFlowElk)`.\n * Note: elkLayout is async because ELK's layout() returns a Promise.\n *\n * Nodes with `parentId` are excluded by default — their positions are managed\n * by `childLayout`, not top-level auto-layout. Pass `{ includeChildren: true }`\n * to include them.\n */\n async elkLayout(options?: {\n algorithm?: ElkAlgorithm;\n direction?: ElkDirection;\n nodeSpacing?: number;\n layerSpacing?: number;\n adjustHandles?: boolean;\n fitView?: boolean;\n duration?: number;\n includeChildren?: boolean;\n }): Promise {\n const computeElkLayout = getAddon('layout:elk');\n if (!computeElkLayout) {\n throw new Error('elkLayout() requires the elk plugin. Register it with: Alpine.plugin(AlpineFlowElk)');\n }\n const direction = options?.direction ?? 'DOWN';\n const layoutNodes = options?.includeChildren\n ? ctx.nodes\n : ctx.nodes.filter((n: FlowNode) => !n.parentId);\n const positions = await computeElkLayout(layoutNodes, ctx.edges, {\n algorithm: options?.algorithm,\n direction,\n nodeSpacing: options?.nodeSpacing,\n layerSpacing: options?.layerSpacing,\n });\n\n if (positions.size === 0) {\n debug('layout', 'ELK layout returned no positions — skipping apply');\n return;\n }\n\n this._applyLayout(positions, {\n adjustHandles: options?.adjustHandles,\n handleDirection: direction,\n fitView: options?.fitView,\n duration: options?.duration,\n });\n\n debug('layout', 'Applied ELK layout', { algorithm: options?.algorithm ?? 'layered', direction });\n ctx._emit('layout', { type: 'elk', algorithm: options?.algorithm ?? 'layered', direction });\n },\n };\n}\n","// ============================================================================\n// canvas-validation — Child validation & reparenting mixin for flow-canvas\n//\n// Public API: validateParent, validateAll, getValidationErrors, reparentNode.\n// Internal helpers: _getChildValidation, _recomputeChildValidation.\n//\n// This is the first mixin with cross-mixin dependencies — reparentNode calls\n// layoutChildren (from the layout mixin) via ctx with optional chaining.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowNode, ChildValidation } from '../../core/types';\nimport {\n resolveChildValidation,\n computeValidationErrors,\n validateChildAdd,\n validateChildRemove,\n} from '../../core/child-validation';\nimport { getDescendantIds, sortNodesTopological } from '../../core/sub-flow';\n\nexport function createValidationMixin(ctx: CanvasContext) {\n return {\n // ── Internal helpers ──────────────────────────────────────────────────\n\n _getChildValidation(parentId: string): ChildValidation | undefined {\n const parent = ctx.getNode(parentId);\n if (!parent) return undefined;\n return resolveChildValidation(parent, ctx._config.childValidationRules ?? {});\n },\n\n _recomputeChildValidation(): void {\n const parentIds = new Set();\n const typeRules = ctx._config.childValidationRules ?? {};\n for (const node of ctx.nodes as FlowNode[]) {\n if (node.parentId) parentIds.add(node.parentId);\n // Also include nodes that have validation rules (even with 0 children)\n if (node.data?.childValidation || typeRules[node.type ?? 'default']) {\n parentIds.add(node.id);\n }\n }\n for (const [id] of ctx._validationErrorCache) {\n parentIds.add(id);\n }\n\n for (const parentId of parentIds) {\n const parent = ctx.getNode(parentId);\n if (!parent) {\n ctx._validationErrorCache.delete(parentId);\n continue;\n }\n const rules = resolveChildValidation(parent, ctx._config.childValidationRules ?? {});\n if (!rules) {\n ctx._validationErrorCache.delete(parentId);\n continue;\n }\n const children = (ctx.nodes as FlowNode[]).filter((n: FlowNode) => n.parentId === parentId);\n const errors = computeValidationErrors(parent, children, rules);\n if (errors.length > 0) {\n ctx._validationErrorCache.set(parentId, errors);\n } else {\n ctx._validationErrorCache.delete(parentId);\n }\n parent._validationErrors = errors;\n }\n },\n\n // ── Child Validation API ─────────────────────────────────────────────\n\n validateParent(nodeId: string): { valid: boolean; errors: string[] } {\n const parent = ctx.getNode(nodeId);\n if (!parent) return { valid: true, errors: [] };\n const rules = resolveChildValidation(parent, ctx._config.childValidationRules ?? {});\n if (!rules) return { valid: true, errors: [] };\n const children = (ctx.nodes as FlowNode[]).filter((n: FlowNode) => n.parentId === nodeId);\n const errors = computeValidationErrors(parent, children, rules);\n return { valid: errors.length === 0, errors };\n },\n\n validateAll(): Map {\n const results = new Map();\n const parentIds = new Set();\n for (const node of ctx.nodes as FlowNode[]) {\n if (node.parentId) parentIds.add(node.parentId);\n }\n for (const parentId of parentIds) {\n results.set(parentId, this.validateParent(parentId));\n }\n return results;\n },\n\n getValidationErrors(nodeId: string): string[] {\n return ctx._validationErrorCache.get(nodeId) ?? [];\n },\n\n // ── Reparent ─────────────────────────────────────────────────────────\n\n /**\n * Reparent a node into a new parent (or detach from current parent).\n * Handles position conversion and child validation.\n * Returns true on success, false if validation rejects the operation.\n */\n reparentNode(nodeId: string, newParentId: string | null): boolean {\n const node = ctx.getNode(nodeId);\n if (!node) return false;\n\n const oldParentId = node.parentId ?? null;\n\n // No-op if already in the target parent\n if (oldParentId === newParentId) return true;\n\n // ── Detach: child → root ───────────────────────────────────\n if (newParentId === null) {\n // Check if removal is allowed by child validation\n if (oldParentId) {\n const rules = this._getChildValidation(oldParentId);\n if (rules) {\n const parent = ctx.getNode(oldParentId);\n if (parent) {\n const siblings = (ctx.nodes as FlowNode[]).filter(\n (n: FlowNode) => n.parentId === oldParentId,\n );\n const result = validateChildRemove(parent, node, siblings, rules);\n if (!result.valid) {\n if (ctx._config.onChildValidationFail) {\n ctx._config.onChildValidationFail({\n parent,\n child: node,\n operation: 'remove',\n rule: result.rule!,\n message: result.message!,\n });\n }\n return false;\n }\n }\n }\n }\n\n // Convert relative position to absolute\n ctx._captureHistory();\n const absPos = ctx.getAbsolutePosition(nodeId);\n node.position.x = absPos.x;\n node.position.y = absPos.y;\n node.parentId = undefined;\n node.extent = undefined;\n\n ctx.nodes = sortNodesTopological(ctx.nodes);\n ctx._rebuildNodeMap();\n this._recomputeChildValidation();\n\n // Re-layout from topmost ancestor layout parent so size changes propagate\n if (oldParentId) {\n let topmost: string | undefined;\n let walkId: string | undefined = oldParentId;\n while (walkId) {\n const walkNode = ctx._nodeMap.get(walkId);\n if (!walkNode) break;\n if (walkNode.childLayout) topmost = walkId;\n walkId = walkNode.parentId;\n }\n if (topmost) ctx.layoutChildren?.(topmost);\n }\n\n ctx._emit('node-reparent', { node, oldParentId, newParentId: null });\n return true;\n }\n\n // ── Reparent: root → group or group → different group ──────\n const newParent = ctx.getNode(newParentId);\n if (!newParent) return false;\n\n // Guard against circular reparenting (dropping a node into its own descendant)\n if (getDescendantIds(nodeId, ctx.nodes).has(newParentId)) return false;\n\n // Validate child add on the new parent\n const rules = this._getChildValidation(newParentId);\n if (rules) {\n const siblings = (ctx.nodes as FlowNode[]).filter(\n (n: FlowNode) => n.parentId === newParentId && n.id !== nodeId,\n );\n const result = validateChildAdd(newParent, node, siblings, rules);\n if (!result.valid) {\n if (ctx._config.onChildValidationFail) {\n ctx._config.onChildValidationFail({\n parent: newParent,\n child: node,\n operation: 'add',\n rule: result.rule!,\n message: result.message!,\n });\n }\n return false;\n }\n }\n\n // If currently a child, validate removal from old parent first\n if (oldParentId) {\n const oldRules = this._getChildValidation(oldParentId);\n if (oldRules) {\n const oldParent = ctx.getNode(oldParentId);\n if (oldParent) {\n const oldSiblings = (ctx.nodes as FlowNode[]).filter(\n (n: FlowNode) => n.parentId === oldParentId,\n );\n const removeResult = validateChildRemove(oldParent, node, oldSiblings, oldRules);\n if (!removeResult.valid) {\n if (ctx._config.onChildValidationFail) {\n ctx._config.onChildValidationFail({\n parent: oldParent,\n child: node,\n operation: 'remove',\n rule: removeResult.rule!,\n message: removeResult.message!,\n });\n }\n return false;\n }\n }\n }\n }\n\n // Convert position: get current absolute position, then make relative to new parent\n ctx._captureHistory();\n const absPos = oldParentId\n ? ctx.getAbsolutePosition(nodeId)\n : { x: node.position.x, y: node.position.y };\n const newParentAbs = ctx.getAbsolutePosition(newParentId);\n node.position.x = absPos.x - newParentAbs.x;\n node.position.y = absPos.y - newParentAbs.y;\n node.parentId = newParentId;\n\n ctx.nodes = sortNodesTopological(ctx.nodes);\n ctx._rebuildNodeMap();\n this._recomputeChildValidation();\n\n // Re-layout the new parent if it has childLayout\n if (newParentId) {\n const reparentNewParent = ctx._nodeMap.get(newParentId);\n if (reparentNewParent?.childLayout) {\n // Restore the node's originally configured dimensions so layout-\n // stretched values from the old parent don't carry over. Nodes\n // without initial dimensions get cleared so the new parent's\n // layout determines size from defaults.\n if (!node.childLayout) {\n const initial = ctx._initialDimensions.get(nodeId);\n node.dimensions = initial ? { ...initial } : undefined;\n }\n // Auto-assign order\n if (node.order == null) {\n const siblings = ctx.nodes.filter(\n (n: FlowNode) => n.parentId === newParentId && n.id !== node.id,\n );\n node.order = siblings.length > 0\n ? Math.max(...siblings.map((s: FlowNode) => s.order ?? 0)) + 1\n : 0;\n }\n }\n }\n\n // Re-layout from the topmost ancestor layout parents so size changes\n // propagate up (e.g. Columns grows → Step 1 repositions siblings).\n // layoutChildren recurses bottom-up, so calling it on the topmost\n // ancestor handles all descendants.\n const topmostAncestors = new Set();\n for (const pid of [newParentId, oldParentId]) {\n if (!pid) continue;\n let topmost: string | undefined;\n let walkId: string | undefined = pid;\n while (walkId) {\n const walkNode = ctx._nodeMap.get(walkId);\n if (!walkNode) break;\n if (walkNode.childLayout) topmost = walkId;\n walkId = walkNode.parentId;\n }\n if (topmost) topmostAncestors.add(topmost);\n }\n for (const aid of topmostAncestors) {\n ctx.layoutChildren?.(aid);\n }\n\n ctx._emit('node-reparent', { node, oldParentId, newParentId });\n return true;\n },\n };\n}\n","// ============================================================================\n// canvas-compute — Compute engine mixin for flow-canvas\n//\n// Delegates to the ComputeEngine for node-type registration and topological\n// data propagation. Two methods, zero dependencies on other mixins.\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { ComputeDefinition } from '../../core/compute';\n\nexport function createComputeMixin(ctx: CanvasContext) {\n return {\n registerCompute(nodeType: string, definition: ComputeDefinition): void {\n ctx._computeEngine.registerCompute(nodeType, definition);\n },\n\n compute(startNodeId?: string): Map> {\n const results = ctx._computeEngine.compute(ctx.nodes, ctx.edges, startNodeId);\n ctx._emit('compute-complete', { results });\n\n // Remeasure node dimensions after the DOM updates from $outputs changes.\n // $nextTick waits for Alpine to flush reactive DOM updates, then rAF\n // waits for the browser to layout, then we measure.\n ctx.$nextTick(() => {\n requestAnimationFrame(() => {\n const changed = new Set();\n for (const [nodeId] of results) {\n const el = ctx._nodeElements.get(nodeId);\n const node = ctx._nodeMap.get(nodeId);\n if (el && node) {\n // Clear inline dimensions so the element can grow to fit new content\n el.style.width = '';\n el.style.height = '';\n const w = el.offsetWidth;\n const h = el.offsetHeight;\n if (!node.dimensions || w !== node.dimensions.width || h !== node.dimensions.height) {\n node.dimensions = { width: w, height: h };\n changed.add(nodeId);\n }\n // A2: height control requires fixedDimensions to prevent the Alpine effect\n // from clearing inline height on leaf nodes. Compute is authoritative here.\n node.fixedDimensions = true;\n // Re-apply so the effect doesn't fight us\n el.style.width = w + 'px';\n el.style.height = h + 'px';\n }\n }\n // Refresh edge paths for nodes whose dimensions changed\n if (changed.size > 0) {\n ctx._refreshEdgePaths(changed);\n }\n });\n });\n\n return results;\n },\n };\n}\n","// ============================================================================\n// Rotation Utilities\n//\n// Pure math for rotating points around a center. Used by edge-utils and\n// floating-edge for geometry-aware handle rotation.\n// ============================================================================\n\n/**\n * Rotate a point (px, py) around a center (cx, cy) by the given angle in degrees.\n * Returns the rotated coordinates.\n */\nexport function rotatePoint(\n px: number,\n py: number,\n cx: number,\n cy: number,\n degrees: number,\n): { x: number; y: number } {\n const rad = (degrees * Math.PI) / 180;\n const cos = Math.cos(rad);\n const sin = Math.sin(rad);\n const dx = px - cx;\n const dy = py - cy;\n return {\n x: cx + dx * cos - dy * sin,\n y: cy + dx * sin + dy * cos,\n };\n}\n","// ============================================================================\n// Avoidant Edge Path\n//\n// Routes edges around obstacle nodes with smooth, obstacle-hugging curves.\n// Uses the same visibility graph + Dijkstra pathfinding as orthogonal routing\n// to find obstacle-free waypoints, then draws the straight runs and rounds each\n// corner with a bounded cubic-bezier fillet (buildRoundedPath) — smooth and\n// bezier-like, but it hugs the route instead of overshooting the way a\n// Catmull-Rom spline through the same points does.\n// Falls back to getBezierPath when no obstacles exist or routing fails.\n// (buildCatmullRomPath is retained here for the freeform `editable` edge type.)\n// ============================================================================\n\nimport type { HandlePosition, Rect } from '../types';\nimport type { EdgePathResult } from './utils';\nimport { getBezierPath } from './bezier';\nimport { findRoute, type RoutePoint } from './orthogonal';\n\nexport interface AvoidantPathParams {\n sourceX: number;\n sourceY: number;\n sourcePosition?: HandlePosition;\n targetX: number;\n targetY: number;\n targetPosition?: HandlePosition;\n obstacles?: Rect[];\n}\n\n// ── Catmull-Rom to cubic bezier ──────────────────────────────────────────────\n\n/**\n * Convert waypoints into a smooth SVG path using Catmull-Rom spline\n * interpolation, rendered as a series of cubic bezier (C) commands.\n *\n * Each segment from P[i] to P[i+1] uses neighboring points P[i-1] and P[i+2]\n * to compute tangent-based control points:\n * CP1 = P[i] + (P[i+1] - P[i-1]) / 6\n * CP2 = P[i+1] - (P[i+2] - P[i]) / 6\n *\n * For endpoints, the missing neighbor is mirrored from the nearest available.\n */\nexport function buildCatmullRomPath(waypoints: RoutePoint[]): string {\n if (waypoints.length < 2) return '';\n\n if (waypoints.length === 2) {\n return `M${waypoints[0].x},${waypoints[0].y} L${waypoints[1].x},${waypoints[1].y}`;\n }\n\n let path = `M${waypoints[0].x},${waypoints[0].y}`;\n\n for (let i = 0; i < waypoints.length - 1; i++) {\n const p0 = waypoints[Math.max(0, i - 1)];\n const p1 = waypoints[i];\n const p2 = waypoints[i + 1];\n const p3 = waypoints[Math.min(waypoints.length - 1, i + 2)];\n\n // Catmull-Rom to cubic bezier control points\n const cp1x = p1.x + (p2.x - p0.x) / 6;\n const cp1y = p1.y + (p2.y - p0.y) / 6;\n const cp2x = p2.x - (p3.x - p1.x) / 6;\n const cp2y = p2.y - (p3.y - p1.y) / 6;\n\n path += ` C${cp1x},${cp1y} ${cp2x},${cp2y} ${p2.x},${p2.y}`;\n }\n\n return path;\n}\n\n// ── Rounded-corner smoothing (avoidant) ──────────────────────────────────────\n\n/**\n * Corner radius (px) for avoidant edges. Clamped per-corner to half the shorter\n * adjacent segment, so short segments stay proportionate and the handle stub\n * departs cleanly. Bigger = more flowing; smaller = tighter.\n */\nexport const AVOIDANT_CORNER_RADIUS = 40;\n\n/**\n * Smooth an orthogonal waypoint route by keeping the straight runs and rounding\n * each interior corner with a bounded cubic-bezier fillet. Because the fillet\n * lives INSIDE the corner it hugs the route and can never overshoot — unlike a\n * Catmull-Rom spline through the same points, which bulges past sharp turns. With\n * a generous radius it still reads as a flowing bezier while staying visually\n * distinct from the tightly-rounded `orthogonal` edge type.\n */\nexport function buildRoundedPath(\n waypoints: RoutePoint[],\n radius: number = AVOIDANT_CORNER_RADIUS,\n): string {\n if (waypoints.length < 2) return '';\n\n if (waypoints.length === 2) {\n return `M${waypoints[0].x},${waypoints[0].y} L${waypoints[1].x},${waypoints[1].y}`;\n }\n\n let path = `M${waypoints[0].x},${waypoints[0].y}`;\n\n for (let i = 1; i < waypoints.length - 1; i++) {\n const a = waypoints[i - 1];\n const v = waypoints[i];\n const b = waypoints[i + 1];\n\n const dA = Math.hypot(v.x - a.x, v.y - a.y) || 1;\n const dB = Math.hypot(b.x - v.x, b.y - v.y) || 1;\n // Bound the fillet so adjacent corners can't overlap.\n const r = Math.min(radius, dA / 2, dB / 2);\n\n // Fillet enter/exit points: r before and after the corner along each segment.\n const px = v.x - (r * (v.x - a.x)) / dA;\n const py = v.y - (r * (v.y - a.y)) / dA;\n const qx = v.x + (r * (b.x - v.x)) / dB;\n const qy = v.y + (r * (b.y - v.y)) / dB;\n\n // Straight run up to the fillet, then a cubic bezier rounding the corner.\n // Control points are pulled toward the corner vertex (k) for a smooth arc.\n const k = 0.5;\n path += ` L${px},${py} C${px + k * (v.x - px)},${py + k * (v.y - py)} ${qx + k * (v.x - qx)},${qy + k * (v.y - qy)} ${qx},${qy}`;\n }\n\n const last = waypoints[waypoints.length - 1];\n path += ` L${last.x},${last.y}`;\n\n return path;\n}\n\n/**\n * Compute the midpoint along the waypoint polyline for label placement.\n */\nfunction getSplineMidpoint(\n waypoints: RoutePoint[],\n): { x: number; y: number; offsetX: number; offsetY: number } {\n if (waypoints.length < 2) {\n return { x: waypoints[0]?.x ?? 0, y: waypoints[0]?.y ?? 0, offsetX: 0, offsetY: 0 };\n }\n\n // Walk polyline segments using Euclidean distance (spline is smooth, not Manhattan)\n let totalLength = 0;\n const segLengths: number[] = [];\n\n for (let i = 1; i < waypoints.length; i++) {\n const dx = waypoints[i].x - waypoints[i - 1].x;\n const dy = waypoints[i].y - waypoints[i - 1].y;\n const len = Math.sqrt(dx * dx + dy * dy);\n segLengths.push(len);\n totalLength += len;\n }\n\n let remaining = totalLength / 2;\n\n for (let i = 0; i < segLengths.length; i++) {\n if (remaining <= segLengths[i]) {\n const t = segLengths[i] > 0 ? remaining / segLengths[i] : 0;\n const x = waypoints[i].x + (waypoints[i + 1].x - waypoints[i].x) * t;\n const y = waypoints[i].y + (waypoints[i + 1].y - waypoints[i].y) * t;\n\n return {\n x,\n y,\n offsetX: Math.abs(waypoints[waypoints.length - 1].x - waypoints[0].x) / 2,\n offsetY: Math.abs(waypoints[waypoints.length - 1].y - waypoints[0].y) / 2,\n };\n }\n remaining -= segLengths[i];\n }\n\n const last = waypoints[waypoints.length - 1];\n return { x: last.x, y: last.y, offsetX: 0, offsetY: 0 };\n}\n\n// ── Main export ──────────────────────────────────────────────────────────────\n\nexport function getAvoidantPath({\n sourceX,\n sourceY,\n sourcePosition = 'bottom',\n targetX,\n targetY,\n targetPosition = 'top',\n obstacles,\n}: AvoidantPathParams): EdgePathResult {\n // Fast path: no obstacles → delegate to bezier\n if (!obstacles || obstacles.length === 0) {\n return getBezierPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n });\n }\n\n const waypoints = findRoute(sourceX, sourceY, sourcePosition, targetX, targetY, targetPosition, obstacles);\n\n // No route found → fall back to bezier\n if (!waypoints) {\n return getBezierPath({\n sourceX,\n sourceY,\n sourcePosition,\n targetX,\n targetY,\n targetPosition,\n });\n }\n\n // Build a smooth path that rounds each route corner (hugs the route, no overshoot)\n const path = buildRoundedPath(waypoints);\n\n // Compute label midpoint\n const { x, y, offsetX, offsetY } = getSplineMidpoint(waypoints);\n\n return {\n path,\n labelPosition: { x, y },\n labelOffsetX: offsetX,\n labelOffsetY: offsetY,\n };\n}\n","// ============================================================================\n// Editable Edge Path\n//\n// User-controlled waypoints with 4 interpolation styles:\n// linear, step, catmull-rom, bezier (default).\n// ============================================================================\n\nimport type { HandlePosition } from '../types';\nimport { getBend, getEdgeCenter, type EdgePathResult } from './utils';\nimport { buildCatmullRomPath } from './avoidant';\n\nexport type EditablePathStyle = 'linear' | 'step' | 'smoothstep' | 'catmull-rom' | 'bezier';\n\nexport interface EditablePathParams {\n sourceX: number;\n sourceY: number;\n sourcePosition?: HandlePosition;\n targetX: number;\n targetY: number;\n targetPosition?: HandlePosition;\n controlPoints?: { x: number; y: number }[];\n pathStyle?: EditablePathStyle;\n borderRadius?: number;\n}\n\n/**\n * Build an SVG path through [source, ...controlPoints, target]\n * using the chosen interpolation style.\n */\nexport function getEditablePath(params: EditablePathParams): EdgePathResult {\n const {\n sourceX, sourceY,\n targetX, targetY,\n controlPoints = [],\n pathStyle = 'bezier',\n borderRadius = 5,\n } = params;\n\n const waypoints = [\n { x: sourceX, y: sourceY },\n ...controlPoints,\n { x: targetX, y: targetY },\n ];\n\n let path: string;\n\n switch (pathStyle) {\n case 'linear':\n path = buildLinearPath(waypoints);\n break;\n case 'step':\n path = buildStepPath(waypoints, 0);\n break;\n case 'smoothstep':\n path = buildSmoothStepPath(waypoints, borderRadius);\n break;\n case 'catmull-rom':\n case 'bezier':\n path = buildCatmullRomPath(waypoints.map((p, i) => ({ ...p, index: i })));\n break;\n default:\n path = buildLinearPath(waypoints);\n }\n\n // Label position: midpoint of the polyline by distance\n const mid = getPolylineMidpoint(waypoints);\n const center = getEdgeCenter({ sourceX, sourceY, targetX, targetY });\n\n return {\n path,\n labelPosition: mid,\n labelOffsetX: center.offsetX,\n labelOffsetY: center.offsetY,\n };\n}\n\n/** Straight line segments between each waypoint. */\nfunction buildLinearPath(points: { x: number; y: number }[]): string {\n if (points.length < 2) return '';\n let d = `M${points[0].x},${points[0].y}`;\n for (let i = 1; i < points.length; i++) {\n d += ` L${points[i].x},${points[i].y}`;\n }\n return d;\n}\n\n/** Orthogonal L-bends between each consecutive pair with rounded corners. */\nfunction buildStepPath(points: { x: number; y: number }[], borderRadius: number): string {\n if (points.length < 2) return '';\n if (points.length === 2) {\n return buildStepSegment(points[0], points[1], borderRadius);\n }\n\n // For 3+ points: straight segments between explicit waypoints\n // with rounded corners at each waypoint via getBend\n let d = `M${points[0].x},${points[0].y}`;\n\n for (let i = 1; i < points.length - 1; i++) {\n const prev = points[i - 1];\n const curr = points[i];\n const next = points[i + 1];\n d += getBend(prev.x, prev.y, curr.x, curr.y, next.x, next.y, borderRadius);\n }\n\n const last = points[points.length - 1];\n d += ` L${last.x},${last.y}`;\n return d;\n}\n\n/** Build an L-bend step segment between two points. */\nfunction buildStepSegment(\n from: { x: number; y: number },\n to: { x: number; y: number },\n borderRadius: number,\n): string {\n const midX = (from.x + to.x) / 2;\n // Route: horizontal to midX, then vertical to target Y, then horizontal to target\n const bend1 = getBend(from.x, from.y, midX, from.y, midX, to.y, borderRadius);\n const bend2 = getBend(midX, from.y, midX, to.y, to.x, to.y, borderRadius);\n return `M${from.x},${from.y}${bend1}${bend2} L${to.x},${to.y}`;\n}\n\n/**\n * Proper smoothstep: orthogonal (axis-aligned) routing between each pair\n * of waypoints with rounded corners, like the regular smoothstep edge type.\n */\nfunction buildSmoothStepPath(points: { x: number; y: number }[], borderRadius: number): string {\n if (points.length < 2) return '';\n if (points.length === 2) {\n return buildStepSegment(points[0], points[1], borderRadius);\n }\n\n // Expand waypoints by inserting orthogonal routing points between each pair.\n // Between A and B, route: A → (midX, A.y) → (midX, B.y) → B\n const expanded: { x: number; y: number }[] = [points[0]];\n\n for (let i = 0; i < points.length - 1; i++) {\n const a = points[i];\n const b = points[i + 1];\n const dx = Math.abs(b.x - a.x);\n const dy = Math.abs(b.y - a.y);\n\n if (dx < 1 || dy < 1) {\n // Already axis-aligned, just go straight\n expanded.push(b);\n } else {\n const midX = (a.x + b.x) / 2;\n expanded.push({ x: midX, y: a.y });\n expanded.push({ x: midX, y: b.y });\n expanded.push(b);\n }\n }\n\n // Draw the expanded path with rounded bends at each intermediate point\n let d = `M${expanded[0].x},${expanded[0].y}`;\n for (let i = 1; i < expanded.length - 1; i++) {\n const prev = expanded[i - 1];\n const curr = expanded[i];\n const next = expanded[i + 1];\n d += getBend(prev.x, prev.y, curr.x, curr.y, next.x, next.y, borderRadius);\n }\n const last = expanded[expanded.length - 1];\n d += ` L${last.x},${last.y}`;\n return d;\n}\n\n/** Find the midpoint along a polyline by accumulated distance. */\nfunction getPolylineMidpoint(points: { x: number; y: number }[]): { x: number; y: number } {\n if (points.length < 2) return points[0] ?? { x: 0, y: 0 };\n\n let totalLen = 0;\n const segLengths: number[] = [];\n for (let i = 0; i < points.length - 1; i++) {\n const dx = points[i + 1].x - points[i].x;\n const dy = points[i + 1].y - points[i].y;\n const len = Math.sqrt(dx * dx + dy * dy);\n segLengths.push(len);\n totalLen += len;\n }\n\n if (totalLen === 0) return points[0];\n\n let target = totalLen / 2;\n for (let i = 0; i < segLengths.length; i++) {\n if (target <= segLengths[i]) {\n const t = target / segLengths[i];\n return {\n x: points[i].x + (points[i + 1].x - points[i].x) * t,\n y: points[i].y + (points[i + 1].y - points[i].y) * t,\n };\n }\n target -= segLengths[i];\n }\n\n return points[points.length - 1];\n}\n","// ============================================================================\n// Shared Edge Path Utilities\n//\n// Pure computation functions for edge paths, extracted from the flow-edge\n// directive so they can be reused by flow-canvas (e.g. animate()).\n// No DOM or Alpine dependencies — pure geometry and dispatching.\n// ============================================================================\n\nimport type { FlowEdge, FlowNode, HandlePosition, Rect, ShapeDefinition } from './types';\nimport type { MarkerType, MarkerConfig } from './markers';\nimport type { EdgePathResult } from './edge-paths/utils';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT, getNodeVisualPosition } from './geometry';\nimport { builtinShapes, rectPerimeter } from './shapes';\nimport { rotatePoint } from './rotation';\nimport { getAvoidantPath } from './edge-paths/avoidant';\nimport { getBezierPath } from './edge-paths/bezier';\nimport { getEditablePath } from './edge-paths/editable';\nimport { getOrthogonalPath } from './edge-paths/orthogonal';\nimport { getSmoothStepPath } from './edge-paths/smoothstep';\nimport { getStraightPath } from './edge-paths/straight';\n\n/**\n * Compute the x,y coordinates where an edge connects to a node based on\n * the handle's position (top/right/bottom/left + corners).\n * When the node has a shape, uses the shape's perimeter function for\n * accurate connection points.\n */\nexport function getHandleCoords(\n node: FlowNode,\n position: HandlePosition,\n shapeRegistry?: Record,\n globalOrigin?: [number, number],\n): { x: number; y: number } {\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n const vp = getNodeVisualPosition(node, globalOrigin);\n\n let coords: { x: number; y: number };\n\n // Shape-aware perimeter point\n if (node.shape) {\n const shapeDef = shapeRegistry?.[node.shape] ?? builtinShapes[node.shape];\n if (shapeDef) {\n const local = shapeDef.perimeterPoint(w, h, position);\n coords = { x: vp.x + local.x, y: vp.y + local.y };\n } else {\n const local = rectPerimeter(w, h, position);\n coords = { x: vp.x + local.x, y: vp.y + local.y };\n }\n } else {\n // Rectangular fallback\n const local = rectPerimeter(w, h, position);\n coords = { x: vp.x + local.x, y: vp.y + local.y };\n }\n\n // Rotate around node center to match the CSS transform rotation.\n // Handles are inside the rotated DOM element so they visually rotate;\n // edge endpoints must follow.\n if (node.rotation) {\n const cx = vp.x + w / 2;\n const cy = vp.y + h / 2;\n coords = rotatePoint(coords.x, coords.y, cx, cy, node.rotation);\n }\n\n return coords;\n}\n\n/**\n * Normalize corner positions to the nearest cardinal direction for\n * path generators that only understand top/right/bottom/left.\n */\nexport function toCardinalPosition(pos: HandlePosition): 'top' | 'right' | 'bottom' | 'left' {\n switch (pos) {\n case 'top-left':\n case 'top-right':\n return 'top';\n case 'bottom-left':\n case 'bottom-right':\n return 'bottom';\n default:\n return pos;\n }\n}\n\n/**\n * Return a unit vector pointing outward from the node for a given handle position.\n */\nexport function getHandleOutwardDirection(position: HandlePosition): { x: number; y: number } {\n const DIAG = Math.SQRT1_2; // ~0.7071\n switch (position) {\n case 'top': return { x: 0, y: -1 };\n case 'bottom': return { x: 0, y: 1 };\n case 'left': return { x: -1, y: 0 };\n case 'right': return { x: 1, y: 0 };\n case 'top-left': return { x: -DIAG, y: -DIAG };\n case 'top-right': return { x: DIAG, y: -DIAG };\n case 'bottom-left': return { x: -DIAG, y: DIAG };\n case 'bottom-right': return { x: DIAG, y: DIAG };\n }\n}\n\n/** Stroke width used on edge paths (matches CSS `.flow-edges path` stroke-width). */\nconst EDGE_STROKE_WIDTH = 1.5;\n\n/** Ratio of arrow body depth to the marker viewBox (arrow polyline spans 5 of 20 units). */\nconst ARROW_DEPTH_RATIO = 5 / 20;\n\n/**\n * Shorten an edge endpoint away from the handle center so the marker tip\n * sits just outside the handle's visible boundary.\n */\nexport function shortenEndpoint(\n coords: { x: number; y: number },\n position: HandlePosition,\n measurement: { handleWidth: number; handleHeight: number } | null,\n marker: MarkerType | MarkerConfig | undefined,\n): { x: number; y: number } {\n if (!marker) return coords;\n\n const cfg = typeof marker === 'string' ? { type: marker } as MarkerConfig : marker;\n\n // Handle radius — use measured dimensions when available, else default 5px\n const handleRadius = measurement\n ? Math.min(measurement.handleWidth, measurement.handleHeight) / 2\n : 5;\n\n // Check for explicit offset override\n if (cfg.offset !== undefined) {\n const dir = getHandleOutwardDirection(position);\n return { x: coords.x + dir.x * cfg.offset, y: coords.y + dir.y * cfg.offset };\n }\n\n // Automatic calculation: handle radius + marker padding\n const markerWidth = cfg.width ?? 12.5;\n const markerDepth = markerWidth * EDGE_STROKE_WIDTH * ARROW_DEPTH_RATIO;\n const markerPadding = markerDepth * 0.4;\n const totalOffset = handleRadius + markerPadding;\n\n const dir = getHandleOutwardDirection(position);\n return { x: coords.x + dir.x * totalOffset, y: coords.y + dir.y * totalOffset };\n}\n\n/**\n * Compute the SVG path for an edge given source/target nodes and handle positions.\n * Dispatches to the appropriate path generator based on edge type.\n */\nexport function getEdgePath(\n edge: FlowEdge,\n sourceNode: FlowNode,\n targetNode: FlowNode,\n sourcePosition: HandlePosition = 'bottom',\n targetPosition: HandlePosition = 'top',\n sourceCoords?: { x: number; y: number },\n targetCoords?: { x: number; y: number },\n edgeTypes?: Record EdgePathResult>,\n obstacles?: Rect[],\n shapeRegistry?: Record,\n globalOrigin?: [number, number],\n defaultEdgeType?: string,\n) {\n const source = sourceCoords ?? getHandleCoords(sourceNode, sourcePosition, shapeRegistry, globalOrigin);\n const target = targetCoords ?? getHandleCoords(targetNode, targetPosition, shapeRegistry, globalOrigin);\n\n const params = {\n sourceX: source.x, sourceY: source.y, sourcePosition: toCardinalPosition(sourcePosition),\n targetX: target.x, targetY: target.y, targetPosition: toCardinalPosition(targetPosition),\n };\n\n const edgeType = edge.type ?? defaultEdgeType ?? 'bezier';\n\n // Check custom edge types first\n if (edgeTypes?.[edgeType]) {\n return edgeTypes[edgeType](params);\n }\n\n // For floating edges, use pathType to pick the path generator; otherwise use edge.type\n const pathStyle = edgeType === 'floating' ? (edge.pathType ?? 'bezier') : edgeType;\n\n switch (pathStyle) {\n case 'editable':\n return getEditablePath({\n ...params,\n controlPoints: edge.controlPoints,\n pathStyle: edge.pathStyle,\n });\n case 'avoidant':\n return getAvoidantPath({ ...params, obstacles });\n case 'orthogonal':\n return getOrthogonalPath({ ...params, obstacles });\n case 'smoothstep':\n return getSmoothStepPath(params);\n case 'straight':\n return getStraightPath({ sourceX: source.x, sourceY: source.y, targetX: target.x, targetY: target.y });\n case 'bezier':\n default:\n return getBezierPath(params);\n }\n}\n","// ============================================================================\n// Floating Edge Utilities\n//\n// Pure math functions for computing edge endpoints that connect to the closest\n// point on a node's rectangular border. No DOM or Alpine dependencies.\n// ============================================================================\n\nimport type { XYPosition, FlowNode, HandlePosition } from './types';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from './geometry';\nimport { rotatePoint } from './rotation';\n\n/**\n * Compute the intersection point where a line from a node's center toward\n * a target point crosses the node's rectangular boundary.\n *\n * Uses L1-norm (Manhattan) scaling to find exact border intersection.\n */\nexport function getNodeIntersection(node: FlowNode, targetCenter: XYPosition): XYPosition {\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n\n const nodeCenter: XYPosition = {\n x: node.position.x + w / 2,\n y: node.position.y + h / 2,\n };\n\n // When rotated, transform the target into the node's local (unrotated)\n // coordinate space so the L1-norm intersection works against the actual\n // rectangle, then rotate the result back to world space.\n const effectiveTarget = node.rotation\n ? rotatePoint(targetCenter.x, targetCenter.y, nodeCenter.x, nodeCenter.y, -node.rotation)\n : targetCenter;\n\n const dx = effectiveTarget.x - nodeCenter.x;\n const dy = effectiveTarget.y - nodeCenter.y;\n\n // Guard against zero direction vector (nodes stacked exactly)\n if (dx === 0 && dy === 0) {\n const fallback = { x: nodeCenter.x, y: nodeCenter.y - h / 2 };\n return node.rotation\n ? rotatePoint(fallback.x, fallback.y, nodeCenter.x, nodeCenter.y, node.rotation)\n : fallback;\n }\n\n const halfW = w / 2;\n const halfH = h / 2;\n\n // L1-norm intersection: scale direction to hit the rectangle boundary\n const absDx = Math.abs(dx);\n const absDy = Math.abs(dy);\n\n let t: number;\n if (absDx / halfW > absDy / halfH) {\n // Hits vertical edge (left or right)\n t = halfW / absDx;\n } else {\n // Hits horizontal edge (top or bottom)\n t = halfH / absDy;\n }\n\n const localIntersection = {\n x: nodeCenter.x + dx * t,\n y: nodeCenter.y + dy * t,\n };\n\n // Rotate back to world space\n return node.rotation\n ? rotatePoint(localIntersection.x, localIntersection.y, nodeCenter.x, nodeCenter.y, node.rotation)\n : localIntersection;\n}\n\n/**\n * Classify an intersection point on a node's border to a cardinal HandlePosition.\n * Uses 1px tolerance for corner cases.\n */\nexport function getEdgePosition(node: FlowNode, intersectionPoint: XYPosition): HandlePosition {\n const w = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const h = node.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n\n const cx = node.position.x + w / 2;\n const cy = node.position.y + h / 2;\n\n // For rotated nodes, classify by world-space displacement from center.\n // The intersection point is already in world space, and path generators\n // use world-space tangent directions, so world-space classification\n // produces the correct edge curvature.\n if (node.rotation) {\n const dx = intersectionPoint.x - cx;\n const dy = intersectionPoint.y - cy;\n if (Math.abs(dx) > Math.abs(dy)) {\n return dx > 0 ? 'right' : 'left';\n }\n return dy > 0 ? 'bottom' : 'top';\n }\n\n const tolerance = 1;\n const left = node.position.x;\n const right = node.position.x + w;\n const top = node.position.y;\n const bottom = node.position.y + h;\n\n if (Math.abs(intersectionPoint.x - left) <= tolerance) return 'left';\n if (Math.abs(intersectionPoint.x - right) <= tolerance) return 'right';\n if (Math.abs(intersectionPoint.y - top) <= tolerance) return 'top';\n if (Math.abs(intersectionPoint.y - bottom) <= tolerance) return 'bottom';\n\n // Fallback: determine by largest displacement from center\n const dx = intersectionPoint.x - cx;\n const dy = intersectionPoint.y - cy;\n\n if (Math.abs(dx) > Math.abs(dy)) {\n return dx > 0 ? 'right' : 'left';\n }\n\n return dy > 0 ? 'bottom' : 'top';\n}\n\n/**\n * Compute floating edge parameters for both source and target nodes.\n * Returns the intersection points and their cardinal positions.\n */\nexport function getFloatingEdgeParams(\n sourceNode: FlowNode,\n targetNode: FlowNode,\n): { sx: number; sy: number; tx: number; ty: number; sourcePos: HandlePosition; targetPos: HandlePosition } {\n const sw = sourceNode.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const sh = sourceNode.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n const tw = targetNode.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const th = targetNode.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n\n const sourceCenter: XYPosition = {\n x: sourceNode.position.x + sw / 2,\n y: sourceNode.position.y + sh / 2,\n };\n const targetCenter: XYPosition = {\n x: targetNode.position.x + tw / 2,\n y: targetNode.position.y + th / 2,\n };\n\n const sourceIntersection = getNodeIntersection(sourceNode, targetCenter);\n const targetIntersection = getNodeIntersection(targetNode, sourceCenter);\n\n const sourcePos = getEdgePosition(sourceNode, sourceIntersection);\n const targetPos = getEdgePosition(targetNode, targetIntersection);\n\n return {\n sx: sourceIntersection.x,\n sy: sourceIntersection.y,\n tx: targetIntersection.x,\n ty: targetIntersection.y,\n sourcePos,\n targetPos,\n };\n}\n\n/**\n * Simpler alternative that returns cardinal positions based on center-to-center direction.\n * Useful for advanced users who compute their own intersection points.\n */\nexport function getSimpleFloatingPosition(\n sourceCenter: XYPosition,\n targetCenter: XYPosition,\n): { sourcePos: HandlePosition; targetPos: HandlePosition } {\n const dx = targetCenter.x - sourceCenter.x;\n const dy = targetCenter.y - sourceCenter.y;\n\n let sourcePos: HandlePosition;\n let targetPos: HandlePosition;\n\n if (Math.abs(dx) > Math.abs(dy)) {\n sourcePos = dx > 0 ? 'right' : 'left';\n targetPos = dx > 0 ? 'left' : 'right';\n } else {\n sourcePos = dy > 0 ? 'bottom' : 'top';\n targetPos = dy > 0 ? 'top' : 'bottom';\n }\n\n return { sourcePos, targetPos };\n}\n","import type { EdgeGradient } from './types';\n\n/**\n * Type guard: is this color value a gradient object?\n */\nexport function isGradient(color: string | EdgeGradient | undefined): color is EdgeGradient {\n return typeof color === 'object' && color !== null && 'from' in color && 'to' in color;\n}\n\n/**\n * Generate a unique gradient def ID for an edge.\n * Format: {flowId}__grad__{edgeId}\n */\nexport function getGradientId(flowId: string, edgeId: string): string {\n return `${flowId}__grad__${edgeId}`;\n}\n\n/**\n * Extract a single solid color from any color value.\n * For gradients, returns the `from` color (used as fallback for selection highlight, etc.).\n */\nexport function resolveStrokeColor(color: string | EdgeGradient | undefined): string | null {\n if (!color) return null;\n if (isGradient(color)) return color.from;\n return color;\n}\n\n/**\n * Create or update an SVG element in a container.\n * Uses gradientUnits=\"userSpaceOnUse\" so coordinates are in SVG canvas space.\n *\n * @returns The gradient element.\n *\n * // TODO(future): Support radial gradients via \n * // TODO(future): Support multi-stop gradients (3+ color stops)\n * // TODO(future): Support path-following gradients via \n */\nexport function upsertGradientDef(\n defsEl: Element,\n id: string,\n gradient: EdgeGradient,\n x1: number,\n y1: number,\n x2: number,\n y2: number,\n): SVGLinearGradientElement {\n let gradEl = defsEl.querySelector(`#${CSS.escape(id)}`) as SVGLinearGradientElement | null;\n\n if (!gradEl) {\n gradEl = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');\n gradEl.id = id;\n gradEl.setAttribute('gradientUnits', 'userSpaceOnUse');\n gradEl.classList.add('flow-edge-gradient');\n\n const stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');\n stop1.setAttribute('offset', '0%');\n gradEl.appendChild(stop1);\n\n const stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');\n stop2.setAttribute('offset', '100%');\n gradEl.appendChild(stop2);\n\n defsEl.appendChild(gradEl);\n }\n\n // Update coordinates\n gradEl.setAttribute('x1', String(x1));\n gradEl.setAttribute('y1', String(y1));\n gradEl.setAttribute('x2', String(x2));\n gradEl.setAttribute('y2', String(y2));\n\n // Update stop colors\n const stops = gradEl.querySelectorAll('stop');\n stops[0]?.setAttribute('stop-color', gradient.from);\n stops[1]?.setAttribute('stop-color', gradient.to);\n\n return gradEl;\n}\n\n/**\n * Remove a gradient def by ID from a container.\n */\nexport function removeGradientDef(defsEl: Element, id: string): void {\n defsEl.querySelector(`#${CSS.escape(id)}`)?.remove();\n}\n","// ============================================================================\n// Schema Handle Geometry\n//\n// Pure math for locating a schema field row's handle center from state alone\n// — no DOM measurement. Schema rows are uniform, so once header/row/inset\n// metrics are known (see SchemaMetrics), every handle's position is\n// arithmetic. Consumed by edge code in place of getBoundingClientRect calls.\n// ============================================================================\n\nimport type { XYPosition, SchemaMetrics } from './types';\n\n/** Resolved center of a schema field row's handle, in flow space. */\nexport interface SchemaHandlePoint {\n x: number;\n y: number;\n position: 'left' | 'right';\n}\n\n/**\n * Row index of the field a handle id names, or `-1` when it names none.\n *\n * The match is EXACT — a trailing `-l`/`-r` is never stripped. That suffix is the\n * CONDENSED-node convention: on a rendered schema node no handle carries it, so the\n * DOM path (`measureHandleCoords`) finds no matching handle, falls through to\n * `inferSideFromHandleId`, and lands on the node's FIRST left/right handle — row 0,\n * not the stripped field's row. Stripping here would silently disagree with that\n * oracle, so an inexact id resolves to nothing and the caller falls back to the DOM.\n */\nexport function schemaFieldIndex(\n fields: ReadonlyArray<{ name: string }> | undefined,\n handleId: string,\n): number {\n if (!Array.isArray(fields)) return -1;\n return fields.findIndex((f) => f?.name === handleId);\n}\n\n/**\n * Handle center for a schema field row, derived purely from state.\n *\n * `fieldIndex` is the row, already resolved by `schemaFieldIndex` — the caller\n * needs the index for its own eligibility checks anyway, so threading it in keeps\n * each endpoint to a single scan of `fields`.\n *\n * Returns `null` when the fast path does not apply — the caller MUST then\n * fall back to DOM measurement. Correctness is never traded for speed.\n */\nexport function computeSchemaHandlePoint(\n node: {\n dimensions?: { width: number; height: number };\n data?: { fields?: Array<{ name: string }> };\n },\n absolutePosition: XYPosition,\n fieldIndex: number,\n side: 'left' | 'right',\n metrics: SchemaMetrics,\n): SchemaHandlePoint | null {\n const fields = node.data?.fields;\n if (!Array.isArray(fields)) return null;\n if (!Number.isInteger(fieldIndex) || fieldIndex < 0 || fieldIndex >= fields.length) return null;\n\n const { width, height } = node.dimensions ?? {};\n if (typeof width !== 'number' || !Number.isFinite(width)) return null;\n if (typeof height !== 'number' || !Number.isFinite(height)) return null;\n\n const { headerHeight, rowHeight, handleOffsetY, handleOffsetYLast, insetLeft, insetRight, insetTop } =\n metrics;\n if (\n !Number.isFinite(headerHeight) ||\n !Number.isFinite(rowHeight) ||\n !Number.isFinite(handleOffsetY) ||\n !Number.isFinite(handleOffsetYLast) ||\n !Number.isFinite(insetLeft) ||\n !Number.isFinite(insetRight) ||\n !Number.isFinite(insetTop)\n ) {\n return null;\n }\n\n // Row top = header + `fieldIndex` strides. The handle then sits `handleOffsetY` into\n // its row — a MEASURED offset, not `rowHeight / 2`: the theme's row border-bottom\n // shrinks the padding box that the handle's `top: 50%` resolves against, so the handle\n // center sits half a border above the row's box center. The last row lost that border,\n // so it gets its own measured offset. See SchemaMetrics.handleOffsetY.\n const offsetY = fieldIndex === fields.length - 1 ? handleOffsetYLast : handleOffsetY;\n const y = absolutePosition.y + insetTop + headerHeight + fieldIndex * rowHeight + offsetY;\n const x =\n side === 'left'\n ? absolutePosition.x + insetLeft\n : absolutePosition.x + width - insetRight;\n\n return { x, y, position: side };\n}\n","// ============================================================================\n// x-flow-edge Directive\n//\n// Binds to an SVG element and reactively updates the edge path, markers,\n// labels, and selection state. Includes an invisible interaction path for\n// click-to-select.\n//\n// Usage: \n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type { FlowEdge, FlowNode, HandlePosition, HandleType, Rect, Connection, XYPosition, SchemaMetrics, EndpointSpreadGrouping } from '../../core/types';\nimport type { MarkerType, MarkerConfig } from '../../core/markers';\nimport { computeSchemaHandlePoint, schemaFieldIndex } from '../../core/schema-geometry';\nimport { laneOffset, applyLaneOffset, resolveSpreadSpacing } from '../../core/endpoint-spread';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from '../../core/geometry';\nimport { normalizeMarker, getMarkerId } from '../../core/markers';\nimport { isConnectable } from '../../core/node-flags';\nimport { applyValidationClasses, clearValidationClasses, applyReconnectValidation, setDragLineValidating } from './flow-handle';\nimport {\n createConnectionLine,\n findSnapTarget,\n startConnectionAutoPan,\n type ConnectionLineInstance,\n} from '../connection-utils';\nimport { CONNECTION_ACTIVE_COLOR, DEFAULT_STROKE_COLOR, DRAG_THRESHOLD, DEFAULT_RECONNECT_SNAP_RADIUS } from '../../core/constants';\nimport { getFloatingEdgeParams } from '../../core/floating-edge';\nimport { debug } from '../../core/debug';\nimport { toAbsoluteNode } from '../../core/sub-flow';\nimport { isGradient, getGradientId, upsertGradientDef, removeGradientDef } from '../../core/gradients';\nimport {\n getHandleCoords,\n toCardinalPosition,\n getHandleOutwardDirection,\n shortenEndpoint,\n getEdgePath,\n} from '../../core/edge-utils';\nimport { matchesModifier } from '../../core/keyboard-shortcuts';\n\nconst BLOCKED_ATTRS = new Set(['x-data', 'x-init', 'x-bind', 'href', 'src', 'action', 'formaction', 'srcdoc']);\n\n/**\n * Resolve a drop target for an edge-body reconnect drag and route it through\n * `applyReconnectValidation` so the async `connectValidator` gates the mutation\n * exactly like the handle-pip reconnect path does.\n *\n * The caller (flow-edge's onPointerUp) computes `dropNodeId` + `dropHandleId`\n * from the pointer event (snapped handle or elementFromPoint fallback); this\n * function:\n *\n * - short-circuits `applied: false` if there is no drop node, or the drop\n * node is not connectable (no validator call, no event emission — matches\n * the prior inline chain's \"snap back silently\" behavior)\n * - otherwise builds the `newConnection` based on `draggedEnd` and delegates\n * to `applyReconnectValidation` which owns the sync + async validator chain\n * plus mutation + `flow-connect-rejected` dispatch\n * - on success, emits `reconnect` with `{ oldEdge, newConnection }` so\n * listeners see the same payload they saw before the refactor\n *\n * Extracted so the edge-body reconnect path is unit-testable without\n * simulating full pointer drags through SVG.\n */\nexport async function resolveEdgeBodyReconnect(params: {\n dropNodeId: string | undefined;\n dropHandleId: string | undefined;\n draggedEnd: HandleType;\n edge: FlowEdge;\n canvas: any;\n containerEl: HTMLElement;\n}): Promise<{ applied: boolean; reason?: string; newConnection?: Connection }> {\n const { dropNodeId, dropHandleId, draggedEnd, edge, canvas, containerEl } = params;\n\n if (!dropNodeId) {\n return { applied: false };\n }\n\n const dropNode = canvas.getNode(dropNodeId);\n if (dropNode && !isConnectable(dropNode)) {\n return { applied: false };\n }\n\n const newConnection: Connection = draggedEnd === 'target'\n ? { source: edge.source, sourceHandle: edge.sourceHandle, target: dropNodeId, targetHandle: dropHandleId }\n : { source: dropNodeId, sourceHandle: dropHandleId, target: edge.target, targetHandle: edge.targetHandle };\n\n const oldEdge = { ...edge };\n const result = await applyReconnectValidation({\n edge,\n newConnection,\n canvas,\n containerEl,\n endpoint: draggedEnd,\n });\n\n if (result.applied) {\n canvas._emit?.('reconnect', { oldEdge, newConnection });\n return { applied: true, newConnection };\n }\n\n return { applied: false, reason: result.reason, newConnection };\n}\n\nfunction resolveAnimationMode(animated: boolean | string | undefined): 'none' | 'dash' | 'pulse' | 'dot' {\n if (animated === true || animated === 'dash') return 'dash';\n if (animated === 'pulse') return 'pulse';\n if (animated === 'dot') return 'dot';\n return 'none';\n}\n\n\n/**\n * Infer which side a handle belongs to from its ID suffix.\n * Returns 'left' or 'right' if the ID ends with '-l' or '-r', else null.\n */\nfunction inferSideFromHandleId(handleId: string): HandlePosition | null {\n if (handleId.endsWith('-l')) return 'left';\n if (handleId.endsWith('-r')) return 'right';\n return null;\n}\n\n/**\n * Rotate a cardinal handle position to account for node rotation.\n * Maps the handle's local outward direction through the node's rotation\n * and returns the nearest world-space cardinal direction. This ensures\n * path generators (smoothstep, bezier, etc.) route edges correctly\n * from rotated handles.\n */\nfunction rotateHandlePos(position: HandlePosition, rotation: number | undefined): HandlePosition {\n if (!rotation) return position;\n\n const dir = getHandleOutwardDirection(position);\n const rad = (rotation * Math.PI) / 180;\n const cos = Math.cos(rad);\n const sin = Math.sin(rad);\n const rx = dir.x * cos - dir.y * sin;\n const ry = dir.x * sin + dir.y * cos;\n\n if (Math.abs(rx) > Math.abs(ry)) {\n return rx > 0 ? 'right' : 'left';\n }\n return ry > 0 ? 'bottom' : 'top';\n}\n\n/**\n * Return the center point of a DOMRect in screen coordinates.\n * Used with `pickClosestHandle` to disambiguate same-(id,type) handles.\n */\nfunction rectCenter(r: DOMRect): XYPosition {\n return { x: (r.left + r.right) / 2, y: (r.top + r.bottom) / 2 };\n}\n\n/**\n * Pick the handle element whose center is geometrically closest to\n * `opposingCenter` (the OTHER endpoint's screen-space rect center).\n *\n * Used when a node has multiple handles sharing the same (id, type) — for\n * example schema nodes, which stamp a real left-side target plus an\n * invisible right-side mirror per row. The picker lets the edge route to\n * whichever side is physically closer, avoiding edges that jump to the\n * far side of the node when the source is repositioned.\n *\n * NOTE: `opposingCenter` is expected in SCREEN coordinates (from a\n * `getBoundingClientRect` center), matching the space the handle rects\n * live in here. When it's not available, falls back to the first match.\n */\nfunction pickClosestHandle(\n handles: NodeListOf | Element[],\n opposingCenter: XYPosition | undefined,\n): Element | null {\n const list = Array.from(handles);\n if (list.length === 0) return null;\n if (list.length === 1 || !opposingCenter) return list[0];\n let best: Element | null = null;\n let bestDist = Infinity;\n for (const h of list) {\n const r = (h as HTMLElement).getBoundingClientRect();\n const cx = (r.left + r.right) / 2;\n const cy = (r.top + r.bottom) / 2;\n const dx = cx - opposingCenter.x;\n const dy = cy - opposingCenter.y;\n const d2 = dx * dx + dy * dy;\n if (d2 < bestDist) {\n bestDist = d2;\n best = h;\n }\n }\n return best;\n}\n\n/**\n * Look up a handle's declared position from the DOM.\n *\n * When multiple handles share the same (id, type) — e.g. schema-node rows\n * with mirror handles on the opposite side — `opposingCenter` (the other\n * endpoint's screen-space rect center) is used to pick the one closest to\n * that endpoint. Without `opposingCenter`, the first match wins.\n */\nexport function resolveHandlePosition(\n container: Element,\n nodeId: string,\n handleId: string | undefined,\n handleType: 'source' | 'target',\n node?: FlowNode,\n opposingCenter?: XYPosition,\n providedNodeEl?: HTMLElement | null,\n): HandlePosition {\n // Prefer the caller-resolved node element (O(1) via canvas._nodeElements);\n // fall back to a container-wide query only when it wasn't provided. All\n // handle lookups below are already scoped to this element.\n const nodeEl = providedNodeEl ?? container.querySelector(`[data-flow-node-id=\"${CSS.escape(nodeId)}\"]`);\n if (nodeEl) {\n // Try exact handle ID scoped to the correct type. Schema nodes and other\n // \"row-per-field\" layouts have a source + target on each row with the same\n // handle id — without the type filter, querySelector grabs whichever\n // appears first in the DOM and the edge lands on the wrong side.\n if (handleId) {\n const typed = nodeEl.querySelectorAll(\n `[data-flow-handle-id=\"${CSS.escape(handleId)}\"][data-flow-handle-type=\"${handleType}\"]`,\n );\n let handleEl = pickClosestHandle(typed, opposingCenter);\n if (!handleEl) {\n const untyped = nodeEl.querySelectorAll(\n `[data-flow-handle-id=\"${CSS.escape(handleId)}\"]`,\n );\n handleEl = pickClosestHandle(untyped, opposingCenter);\n }\n if (handleEl) {\n return (handleEl.getAttribute('data-flow-handle-position') as HandlePosition)\n ?? (handleType === 'source' ? 'bottom' : 'top');\n }\n }\n // If exact ID not found (e.g. condensed node), try a handle on the same\n // side inferred from the handle ID suffix ('-l' → left, '-r' → right).\n if (handleId) {\n const side = inferSideFromHandleId(handleId);\n if (side) {\n const sideEl = nodeEl.querySelector(`[data-flow-handle-position=\"${side}\"]`);\n if (sideEl) return side;\n }\n }\n // Fall back to any handle of the same type on the node\n const fallbackEl = nodeEl.querySelector(`[data-flow-handle-type=\"${handleType}\"]`);\n if (fallbackEl) {\n return (fallbackEl.getAttribute('data-flow-handle-position') as HandlePosition)\n ?? (handleType === 'source' ? 'bottom' : 'top');\n }\n }\n\n // Fall back to per-node defaults before hardcoded default\n if (node) {\n const nodeDefault = handleType === 'source' ? node.sourcePosition : node.targetPosition;\n if (nodeDefault) return nodeDefault;\n }\n\n return handleType === 'source' ? 'bottom' : 'top';\n}\n\ninterface HandleMeasurement {\n x: number;\n y: number;\n handleWidth: number;\n handleHeight: number;\n}\n\n/**\n * Row index of the field this endpoint's handle names, or `-1` when its handle\n * center must NOT be derived from state.\n *\n * Doubles as the eligibility check: every condition here exists because the DOM\n * path would pick up something the state path cannot see, and `-1` sends the\n * caller to `measureHandleCoords` — correctness is never traded for the rect\n * saving. Returning the index (rather than a boolean) keeps each endpoint to a\n * SINGLE scan of `fields`: the caller threads it straight into\n * `computeSchemaHandlePoint`.\n *\n * - no `data.fields` array → not a schema node at all\n * - `hidden` / `collapsed` / `condensed` → the node's rows are not laid out the\n * way `SchemaMetrics` describes (condensed nodes swap in a summary view via\n * consumer `x-show`, so `data.fields` no longer maps to rendered rows)\n * - `rotation` → `getBoundingClientRect` folds in the CSS transform for free;\n * arithmetic on `node.position` cannot\n * - a non-default per-node `nodeOrigin` → the node's painted top-left is not its\n * position; see the block below\n * - no finite `dimensions` → the right-hand handle x is `position.x + width`\n * - not rendered by `x-flow-schema`, or viewport-culled, or non-uniform rows →\n * see the three blocks below\n * - the handle id must name a field EXACTLY (see `schemaFieldIndex`)\n *\n * `nodeEl` is the endpoint's registered node element (`canvas._nodeElements`).\n * Only its ATTRIBUTES and INLINE STYLE are read here — never a rect — so the fast\n * path keeps its zero-`getBoundingClientRect` guarantee.\n */\nfunction schemaFieldIndexForEndpoint(\n node: FlowNode | undefined,\n handleId: string | undefined,\n nodeEl: HTMLElement | null | undefined,\n metrics: SchemaMetrics,\n): number {\n if (!node || !handleId) return -1;\n if (node.hidden || node.collapsed || node.condensed) return -1;\n if (node.rotation) return -1;\n\n // `nodeOrigin` is a PER-NODE property as well as a canvas config, and the node's own\n // value WINS: `flow-node` places the element at `absPos − dimensions × (node.nodeOrigin\n // ?? config.nodeOrigin ?? [0,0])`. `toAbsoluteNode` returns a ROOT node unchanged and\n // `getAbsolutePosition` folds in only the PARENTS' origins — never the node's own — so\n // under a per-node origin `node.position` is NOT the rendered border-box top-left, and\n // every state-derived endpoint on this node would land `dimensions × origin` away from\n // where the browser painted the handle. The canvas-level gate at the call site checks\n // only `config.nodeOrigin`, which a node carrying its own origin sails straight past.\n // Checking here covers BOTH endpoints — a node's own geometry AND its role as the\n // opposing node in `schemaHandleSide` (whose `nodeCenterX` reads the same unshifted\n // position, so an unguarded origin can flip the side too).\n const origin = node.nodeOrigin;\n if (origin && (origin[0] !== 0 || origin[1] !== 0)) return -1;\n\n // Rendered by `x-flow-schema`? Only then do the cached `SchemaMetrics` describe\n // this node's rows. A hand-rolled node (skip the directive, write `x-for` +\n // handles — a pattern flow-schema.ts's own docs invite) carries the SAME\n // `data.fields` shape and the same field-keyed handle ids with entirely different\n // row geometry, and state cannot tell the two apart. The directive stamps the\n // attribute on the NODE element, which works under both plain-directive and\n // WireFlow slot markup. No registered element ⇒ ineligible ⇒ DOM path ⇒ safe.\n if (!nodeEl?.hasAttribute('data-flow-schema-node')) return -1;\n\n // Viewport-culled? `_applyCulling` writes inline `display: none` on off-screen\n // node elements, and an edge still renders when only ONE endpoint is visible. The\n // DOM path DEGRADES on a culled endpoint — its handle rect is 0×0, so\n // `measureHandleCoords` returns null and the endpoint collapses to the node-edge\n // midpoint (`getHandleCoords`), with the side inferred from DOM order. The state\n // path cannot see `display: none` and would emit true row geometry instead, so\n // every edge crossing the culling boundary would render DIFFERENTLY than it does\n // today. Matching the degradation is DELIBERATE, not an oversight: this workstream\n // changes how endpoints are computed, never where they land. (Culling is 'auto' at\n // ≥150 nodes — precisely the scale this fast path exists for.)\n if (nodeEl.style.display === 'none') return -1;\n\n const width = node.dimensions?.width;\n const height = node.dimensions?.height;\n if (typeof width !== 'number' || !Number.isFinite(width)) return -1;\n if (typeof height !== 'number' || !Number.isFinite(height)) return -1;\n\n const fields = (node.data as { fields?: Array<{ name: string }> } | undefined)?.fields;\n if (!Array.isArray(fields) || fields.length === 0) return -1;\n\n // Are the rows actually uniform? The metrics are measured ONCE, from the first\n // schema node to render, and everything downstream assumes every schema node's\n // rows match them. Nothing enforces that: `.flow-schema-row-name` has no\n // `white-space: nowrap`, so a consumer who pins `dimensions.width` and writes a\n // long field name gets a WRAPPED, taller row — and every row below it would have\n // its endpoint land one row off, silently. `node.dimensions` is the measured\n // BORDER BOX (flow-canvas reads `borderBoxSize`), so the row model must reproduce\n // the node's height exactly. Two adds; catches wrapped rows, non-uniform rows and\n // bespoke markup, all of which fall back to the DOM path — which measures each\n // handle individually and is immune.\n //\n // The last row is `rowHeightLast`, not `rowHeight` — the theme removes its\n // border-bottom. Modelling it as `rowHeight` overshoots EVERY schema node's real\n // border box by that border, which used to fail this very check and disqualify the\n // whole canvas from the fast path (caught by the real-browser parity test; jsdom has\n // no stylesheet to be wrong about).\n const expectedHeight =\n metrics.insetTop\n + metrics.headerHeight\n + (fields.length - 1) * metrics.rowHeight\n + metrics.rowHeightLast\n + metrics.insetBottom;\n if (Math.abs(expectedHeight - height) > 0.5) return -1;\n\n return schemaFieldIndex(fields, handleId);\n}\n\n/**\n * Which side of a schema node an endpoint's handle sits on — replicating\n * `pickClosestHandle` without touching the DOM.\n *\n * Each schema row stamps a real handle plus an invisible mirror on the opposite\n * side, sharing one (id, type). The DOM path takes whichever is closer to the\n * OPPOSING endpoint's node-rect center. Both candidates share the row's center\n * y, so the y term cancels and the decision collapses to a comparison of the\n * opposing node's center x against the midpoint of this node's two candidate\n * handle x's.\n *\n * The tie-break is ASYMMETRIC on purpose. `pickClosestHandle` uses a strict\n * `d2 < bestDist`, so an exact tie leaves the FIRST handle in DOM order winning\n * — and `renderRow` appends the real target (left) before its right mirror, but\n * the real source (right) before its left mirror. So a tie sends a source right\n * and a target left. A vertically stacked pair of equal-width nodes hits this\n * exactly; symmetric rounding would flip the target to the wrong side.\n */\nfunction schemaHandleSide(\n node: FlowNode,\n absolutePosition: XYPosition,\n handleType: 'source' | 'target',\n opposingCenterX: number,\n metrics: SchemaMetrics,\n): 'left' | 'right' {\n const width = node.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const midX =\n absolutePosition.x + (metrics.insetLeft + (width - metrics.insetRight)) / 2;\n\n return handleType === 'source'\n ? (opposingCenterX >= midX ? 'right' : 'left') // tie → right (real source is first in DOM)\n : (opposingCenterX > midX ? 'right' : 'left'); // tie → left (real target is first in DOM)\n}\n\n/** Flow-space center x of a node — what `rectCenter(nodeEl.getBoundingClientRect())` yields. */\nfunction nodeCenterX(node: FlowNode): number {\n return node.position.x + (node.dimensions?.width ?? DEFAULT_NODE_WIDTH) / 2;\n}\n\ninterface ResolvedEndpoints {\n sourcePos: HandlePosition;\n targetPos: HandlePosition;\n srcMeasurement: HandleMeasurement;\n tgtMeasurement: HandleMeasurement;\n}\n\n/**\n * Both endpoints of a schema→schema edge, derived from state — the fast path.\n *\n * Returns `null` whenever the caller must fall back to DOM measurement: either\n * endpoint being ineligible (see `schemaFieldIndexForEndpoint`) disqualifies the\n * whole edge. The handle box size comes from the metrics rather than being zeroed:\n * `shortenEndpoint` derives its marker offset from\n * `min(handleWidth, handleHeight) / 2`, so zeros would pull every marker'd endpoint\n * ~5px off where the DOM path puts it.\n *\n * `sourceNode` / `targetNode` are the ABSOLUTE-position wrappers; `rawSource` /\n * `rawTarget` are the state nodes carrying `data.fields`. `nodeElements` is the\n * canvas's node-element registry — read for attributes/inline style only, never\n * for a rect.\n */\nfunction resolveSchemaEndpoints(\n rawSource: FlowNode,\n rawTarget: FlowNode,\n sourceNode: FlowNode,\n targetNode: FlowNode,\n sourceHandle: string | undefined,\n targetHandle: string | undefined,\n nodeElements: Map | undefined,\n metrics: SchemaMetrics,\n): ResolvedEndpoints | null {\n // One scan of `fields` per endpoint: the index resolved by the eligibility check\n // is the same one the geometry needs. Bail on the source before touching the\n // target — the common non-schema edge costs a single attribute read.\n const srcIndex = schemaFieldIndexForEndpoint(rawSource, sourceHandle, nodeElements?.get(rawSource.id), metrics);\n if (srcIndex < 0) return null;\n const tgtIndex = schemaFieldIndexForEndpoint(rawTarget, targetHandle, nodeElements?.get(rawTarget.id), metrics);\n if (tgtIndex < 0) return null;\n\n const srcSide = schemaHandleSide(rawSource, sourceNode.position, 'source', nodeCenterX(targetNode), metrics);\n const tgtSide = schemaHandleSide(rawTarget, targetNode.position, 'target', nodeCenterX(sourceNode), metrics);\n\n const src = computeSchemaHandlePoint(rawSource, sourceNode.position, srcIndex, srcSide, metrics);\n const tgt = computeSchemaHandlePoint(rawTarget, targetNode.position, tgtIndex, tgtSide, metrics);\n if (!src || !tgt) return null;\n\n const size = { handleWidth: metrics.handleWidth, handleHeight: metrics.handleHeight };\n return {\n sourcePos: src.position,\n targetPos: tgt.position,\n srcMeasurement: { x: src.x, y: src.y, ...size },\n tgtMeasurement: { x: tgt.x, y: tgt.y, ...size },\n };\n}\n\n/**\n * Measure a handle element's actual center position in flow coordinates.\n * Converts the handle's screen position directly via the container rect\n * and viewport, so CSS transforms (e.g. rotation) are correctly accounted for.\n * Returns null if the handle element cannot be found.\n *\n * When a node has multiple handles sharing the same (id, type) — e.g.\n * schema-node rows with opposite-side mirror handles — `opposingCenter`\n * (the OTHER endpoint's screen-space rect center) is used to pick the\n * geometrically closest match. Without it, the first match wins.\n */\nfunction measureHandleCoords(\n container: Element,\n nodeId: string,\n _node: FlowNode,\n handleId: string | undefined,\n handleType: 'source' | 'target',\n zoom: number,\n viewport: { x: number; y: number },\n opposingCenter?: XYPosition,\n providedNodeEl?: HTMLElement | null,\n): HandleMeasurement | null {\n // Prefer the caller-resolved node element (O(1) via canvas._nodeElements);\n // fall back to a container-wide query only when it wasn't provided.\n const nodeEl = providedNodeEl ?? (container.querySelector(`[data-flow-node-id=\"${CSS.escape(nodeId)}\"]`) as HTMLElement | null);\n if (!nodeEl) return null;\n\n let handleEl: HTMLElement | null = null;\n\n if (handleId) {\n // 1. Try exact handle ID scoped to the correct type (schema-style nodes\n // have same-id target + source per row; type disambiguates). When\n // multiple match (e.g. real + mirror), pick the geometrically closest\n // to the opposing endpoint. Fall back to id-only for backward compat\n // with nodes that only have one handle per id.\n const typed = nodeEl.querySelectorAll(\n `[data-flow-handle-id=\"${CSS.escape(handleId)}\"][data-flow-handle-type=\"${handleType}\"]`,\n );\n handleEl = pickClosestHandle(typed, opposingCenter) as HTMLElement | null;\n if (!handleEl) {\n const untyped = nodeEl.querySelectorAll(\n `[data-flow-handle-id=\"${CSS.escape(handleId)}\"]`,\n );\n handleEl = pickClosestHandle(untyped, opposingCenter) as HTMLElement | null;\n }\n // 2. If not found, try a handle on the same side (for condensed nodes)\n if (!handleEl) {\n const side = inferSideFromHandleId(handleId);\n if (side) {\n handleEl = nodeEl.querySelector(`[data-flow-handle-position=\"${side}\"]`);\n }\n }\n } else {\n // No handle ID — fall back to any handle of the same type\n handleEl = nodeEl.querySelector(`[data-flow-handle-type=\"${handleType}\"]`);\n }\n if (!handleEl) return null;\n\n const handleRect = handleEl.getBoundingClientRect();\n\n // Hidden handles (display:none) return zero-size rects — fall back to computed position\n if (handleRect.width === 0 && handleRect.height === 0) return null;\n\n // Convert handle center from screen space to flow space via the container,\n // bypassing the node rect entirely. This correctly handles CSS transforms\n // (rotation, scale) because getBoundingClientRect includes transforms.\n const containerRect = container.getBoundingClientRect();\n const handleCenterX = handleRect.left + handleRect.width / 2;\n const handleCenterY = handleRect.top + handleRect.height / 2;\n\n return {\n x: (handleCenterX - containerRect.left - viewport.x) / zoom,\n y: (handleCenterY - containerRect.top - viewport.y) / zoom,\n handleWidth: handleRect.width / zoom,\n handleHeight: handleRect.height / zoom,\n };\n}\n\n\nfunction getPointAtPercent(pathEl: SVGPathElement, t: number, totalLength?: number): { x: number; y: number } {\n const len = totalLength ?? pathEl.getTotalLength();\n const pt = pathEl.getPointAtLength(len * Math.max(0, Math.min(1, t)));\n return { x: pt.x, y: pt.y };\n}\n\nfunction isNearEndpoint(px: number, py: number, ex: number, ey: number, r: number): boolean {\n const dx = px - ex;\n const dy = py - ey;\n return Math.sqrt(dx * dx + dy * dy) <= r;\n}\n\n/** Distance from point P to line segment AB. */\nfunction pointToSegmentDist(\n p: { x: number; y: number },\n a: { x: number; y: number },\n b: { x: number; y: number },\n): number {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const lenSq = dx * dx + dy * dy;\n if (lenSq === 0) return Math.sqrt((p.x - a.x) ** 2 + (p.y - a.y) ** 2);\n let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq;\n t = Math.max(0, Math.min(1, t));\n const projX = a.x + t * dx;\n const projY = a.y + t * dy;\n return Math.sqrt((p.x - projX) ** 2 + (p.y - projY) ** 2);\n}\n\nexport function registerFlowEdgeDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-edge',\n (\n el,\n { expression },\n { evaluate, effect, cleanup },\n ) => {\n const gEl = el as unknown as SVGGElement;\n gEl.style.pointerEvents = 'auto';\n\n // Interaction path (wide invisible hit area for clicks)\n const interactionPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n interactionPath.setAttribute('fill', 'none');\n // Use inline styles so `.flow-edges path` CSS can't override them\n interactionPath.style.stroke = 'transparent';\n interactionPath.style.strokeWidth = '20'; // initial default; updated reactively in effect()\n interactionPath.style.pointerEvents = 'stroke';\n interactionPath.style.cursor = 'pointer';\n gEl.appendChild(interactionPath);\n\n // Visible path\n let pathEl = el.querySelector('path:not(:first-child)') as SVGPathElement | null;\n if (!pathEl) {\n pathEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n pathEl.setAttribute('fill', 'none');\n pathEl.setAttribute('stroke-width', '1.5');\n pathEl.style.pointerEvents = 'none';\n gEl.appendChild(pathEl);\n }\n\n // Label elements (created lazily, live in the flow-viewport as HTML overlays)\n let labelEl: HTMLDivElement | null = null;\n let labelStartEl: HTMLDivElement | null = null;\n let labelEndEl: HTMLDivElement | null = null;\n\n // Cached label-path length, keyed by the path `d` attribute. getTotalLength()\n // forces SVG geometry, so avoid re-measuring an unchanged path across effect\n // re-runs (e.g. selection/label-only updates that don't change routing).\n let cachedPathD: string | null = null;\n let cachedTotalLength = 0;\n\n // Dot animation state\n let dotCircle: SVGCircleElement | null = null;\n let currentAnimMode: 'none' | 'dash' | 'pulse' | 'dot' = 'none';\n let currentGradientId: string | null = null;\n let currentEdgeClass: string | null = null;\n\n function ensureDotAnimation(dotGEl: SVGGElement, pathD: string, containerEl: Element, edge: FlowEdge, durationOverride?: string): void {\n if (!dotCircle) {\n dotCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n dotCircle.classList.add('flow-edge-dot');\n dotCircle.style.pointerEvents = 'none';\n dotGEl.appendChild(dotCircle);\n }\n\n const flowContainer = containerEl.closest('.flow-container') as HTMLElement | null;\n const styles = flowContainer ? getComputedStyle(flowContainer) : null;\n const radius = edge.particleSize ?? (parseFloat(styles?.getPropertyValue('--flow-edge-dot-size').trim() ?? '4') || 4);\n const duration = durationOverride || styles?.getPropertyValue('--flow-edge-dot-duration').trim() || '2s';\n\n dotCircle.setAttribute('r', String(radius));\n\n if (edge.particleColor) {\n dotCircle.style.fill = edge.particleColor;\n } else {\n dotCircle.style.removeProperty('fill');\n }\n\n // Recreate when path changes (SMIL doesn't support reactive updates)\n const existingMotion = dotCircle.querySelector('animateMotion');\n if (existingMotion) existingMotion.remove();\n\n const motion = document.createElementNS('http://www.w3.org/2000/svg', 'animateMotion');\n motion.setAttribute('dur', duration);\n motion.setAttribute('repeatCount', 'indefinite');\n motion.setAttribute('path', pathD);\n dotCircle.appendChild(motion);\n }\n\n function removeDotAnimation(): void {\n dotCircle?.remove();\n dotCircle = null;\n }\n\n // Cached endpoint positions (flow coordinates, before marker shortening)\n let lastSrcCoords: { x: number; y: number } | null = null;\n let lastTgtCoords: { x: number; y: number } | null = null;\n\n // Cached visible endpoint positions (after marker shortening)\n let lastVisibleSrcCoords: { x: number; y: number } | null = null;\n let lastVisibleTgtCoords: { x: number; y: number } | null = null;\n\n // ── Edge selection handler (extracted for reuse) ───────────\n const handleEdgeClick = (e: MouseEvent) => {\n e.stopPropagation();\n\n const edge = evaluate(expression) as FlowEdge;\n if (!edge) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n\n canvas._emit('edge-click', { edge, event: e });\n\n if (matchesModifier(e, canvas._shortcuts?.multiSelect)) {\n if (canvas.selectedEdges.has(edge.id)) {\n canvas.selectedEdges.delete(edge.id);\n edge.selected = false;\n debug('selection', `Edge \"${edge.id}\" deselected (shift)`);\n } else {\n canvas.selectedEdges.add(edge.id);\n edge.selected = true;\n debug('selection', `Edge \"${edge.id}\" selected (shift)`);\n }\n } else {\n canvas.deselectAll();\n canvas.selectedEdges.add(edge.id);\n edge.selected = true;\n debug('selection', `Edge \"${edge.id}\" selected`);\n }\n canvas._emitSelectionChange();\n };\n\n const handleContextMenu = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const edge = evaluate(expression) as FlowEdge;\n if (!edge) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n\n const ctxTarget = e.target as SVGElement;\n if (ctxTarget.classList.contains('flow-edge-control-point')) {\n const idx = parseInt(ctxTarget.dataset.pointIndex ?? '', 10);\n if (!isNaN(idx)) {\n canvas._emit('edge-control-point-context-menu', {\n edge,\n pointIndex: idx,\n position: { x: e.clientX, y: e.clientY },\n event: e,\n });\n return;\n }\n }\n\n canvas._emit('edge-context-menu', { edge, event: e });\n };\n\n const handleDblClick = (e: MouseEvent) => {\n e.stopPropagation();\n e.preventDefault();\n\n const edge = evaluate(expression) as FlowEdge;\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!edge || !canvas) return;\n\n const resolvedEdgeType = edge.type ?? canvas._config?.defaultEdgeType ?? 'bezier';\n if (resolvedEdgeType !== 'editable') return;\n\n const target = e.target as SVGElement;\n\n if (target.classList.contains('flow-edge-control-point')) {\n const idx = parseInt(target.dataset.pointIndex ?? '', 10);\n if (!isNaN(idx) && edge.controlPoints) {\n canvas._captureHistory?.();\n edge.controlPoints.splice(idx, 1);\n canvas._emit('edge-control-point-change', { edge, action: 'remove', index: idx });\n }\n return;\n }\n\n if (target.classList.contains('flow-edge-midpoint')) {\n const segIdx = parseInt(target.dataset.segmentIndex ?? '', 10);\n if (!isNaN(segIdx)) {\n const flowPos = canvas.screenToFlowPosition(e.clientX, e.clientY);\n if (!edge.controlPoints) edge.controlPoints = [];\n canvas._captureHistory?.();\n edge.controlPoints.splice(segIdx, 0, { x: flowPos.x, y: flowPos.y });\n canvas._emit('edge-control-point-change', { edge, action: 'add', index: segIdx });\n }\n return;\n }\n\n if (target.closest('path')) {\n const flowPos = canvas.screenToFlowPosition(e.clientX, e.clientY);\n if (!edge.controlPoints) edge.controlPoints = [];\n\n const allPts = [\n lastSrcCoords ?? { x: 0, y: 0 },\n ...edge.controlPoints,\n lastTgtCoords ?? { x: 0, y: 0 },\n ];\n\n let bestIdx = 0;\n let bestDist = Infinity;\n for (let i = 0; i < allPts.length - 1; i++) {\n const d = pointToSegmentDist(flowPos, allPts[i], allPts[i + 1]);\n if (d < bestDist) { bestDist = d; bestIdx = i; }\n }\n\n canvas._captureHistory?.();\n edge.controlPoints.splice(bestIdx, 0, { x: flowPos.x, y: flowPos.y });\n canvas._emit('edge-control-point-change', { edge, action: 'add', index: bestIdx });\n }\n };\n\n const handleControlPointPointerDown = (e: PointerEvent) => {\n const target = e.target as SVGElement;\n if (!target.classList.contains('flow-edge-control-point') || e.button !== 0) return;\n\n e.stopPropagation();\n e.preventDefault();\n\n const edge = evaluate(expression) as FlowEdge;\n if (!edge?.controlPoints) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n\n const idx = parseInt(target.dataset.pointIndex ?? '', 10);\n if (isNaN(idx)) return;\n\n target.classList.add('dragging');\n let hasMoved = false;\n\n const onMove = (me: PointerEvent) => {\n if (!hasMoved) {\n canvas._captureHistory?.();\n hasMoved = true;\n }\n\n let pos = canvas.screenToFlowPosition(me.clientX, me.clientY);\n\n const snap = canvas._config?.snapToGrid;\n if (snap) {\n pos = {\n x: Math.round(pos.x / snap[0]) * snap[0],\n y: Math.round(pos.y / snap[1]) * snap[1],\n };\n }\n\n edge.controlPoints![idx] = pos;\n };\n\n const onUp = () => {\n document.removeEventListener('pointermove', onMove);\n document.removeEventListener('pointerup', onUp);\n target.classList.remove('dragging');\n if (hasMoved) {\n canvas._emit('edge-control-point-change', { edge, action: 'move', index: idx });\n }\n };\n\n document.addEventListener('pointermove', onMove);\n document.addEventListener('pointerup', onUp);\n };\n\n gEl.addEventListener('contextmenu', handleContextMenu);\n gEl.addEventListener('dblclick', handleDblClick);\n gEl.addEventListener('pointerdown', handleControlPointPointerDown, true);\n\n // ── Pointerdown: click vs. reconnection drag ──────────────\n let activeReconnectCleanup: (() => void) | null = null;\n\n const handlePointerDown = (e: PointerEvent) => {\n // Only primary button\n if (e.button !== 0) return;\n e.stopPropagation();\n\n const edge = evaluate(expression) as FlowEdge;\n if (!edge) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n\n // Check if pointer is near an endpoint for reconnection\n const reconnectSnapRadius = canvas._config?.reconnectSnapRadius ?? DEFAULT_RECONNECT_SNAP_RADIUS;\n const globalReconnectable = canvas._config?.edgesReconnectable !== false;\n const edgeReconnectable = edge.reconnectable ?? true;\n\n let draggedEnd: HandleType | null = null;\n\n if (globalReconnectable && edgeReconnectable !== false && lastSrcCoords && lastTgtCoords) {\n const flowPos = canvas.screenToFlowPosition(e.clientX, e.clientY);\n\n // Check both handle center and visible (marker-adjusted) endpoint\n const nearSrc = isNearEndpoint(flowPos.x, flowPos.y, lastSrcCoords.x, lastSrcCoords.y, reconnectSnapRadius)\n || (lastVisibleSrcCoords && isNearEndpoint(flowPos.x, flowPos.y, lastVisibleSrcCoords.x, lastVisibleSrcCoords.y, reconnectSnapRadius));\n const nearTgt = isNearEndpoint(flowPos.x, flowPos.y, lastTgtCoords.x, lastTgtCoords.y, reconnectSnapRadius)\n || (lastVisibleTgtCoords && isNearEndpoint(flowPos.x, flowPos.y, lastVisibleTgtCoords.x, lastVisibleTgtCoords.y, reconnectSnapRadius));\n\n if (nearTgt && (edgeReconnectable === true || edgeReconnectable === 'target')) {\n draggedEnd = 'target';\n } else if (nearSrc && (edgeReconnectable === true || edgeReconnectable === 'source')) {\n draggedEnd = 'source';\n }\n }\n\n if (!draggedEnd) {\n // Not near endpoint — simple click (use pointerup-once)\n const onUp = (upE: PointerEvent) => {\n document.removeEventListener('pointerup', onUp);\n handleEdgeClick(upE as unknown as MouseEvent);\n };\n document.addEventListener('pointerup', onUp, { once: true });\n return;\n }\n\n // Near an endpoint — track movement for drag threshold\n const startX = e.clientX;\n const startY = e.clientY;\n let dragging = false;\n let reconnectCleanedUp = false;\n let snappedHandle: HTMLElement | null = null;\n const connectionSnapRadius = canvas._config?.connectionSnapRadius ?? 20;\n\n // Connection line for visual feedback\n let tempSvg: SVGSVGElement | null = null;\n let reconnectLineInstance: ConnectionLineInstance | null = null;\n let connectAutoPan: ReturnType = null;\n let lastMoveX = startX;\n let lastMoveY = startY;\n\n const containerEl = el.closest('.flow-container') as HTMLElement;\n if (!containerEl) return;\n\n // Anchor = the end that stays fixed\n const anchorCoords = draggedEnd === 'target' ? lastSrcCoords! : lastTgtCoords!;\n\n const startReconnectionDrag = () => {\n dragging = true;\n\n // Dim the edge\n gEl.classList.add('flow-edge-reconnecting');\n\n // Emit event\n canvas._emit('reconnect-start', { edge, handleType: draggedEnd });\n debug('reconnect', `Reconnection drag started on edge \"${edge.id}\" (${draggedEnd} end)`);\n\n // Create connection line for visual feedback\n reconnectLineInstance = createConnectionLine({\n connectionLineType: canvas._config?.connectionLineType,\n connectionLineStyle: canvas._config?.connectionLineStyle,\n connectionLine: canvas._config?.connectionLine,\n containerEl: (gEl.closest('.flow-container') as HTMLElement) ?? undefined,\n });\n tempSvg = reconnectLineInstance.svg;\n\n const flowPos = canvas.screenToFlowPosition(startX, startY);\n reconnectLineInstance.update({\n fromX: anchorCoords.x, fromY: anchorCoords.y,\n toX: flowPos.x, toY: flowPos.y,\n source: edge.source, sourceHandle: edge.sourceHandle,\n });\n\n const viewportEl = containerEl.querySelector('.flow-viewport');\n if (viewportEl) {\n viewportEl.appendChild(tempSvg);\n }\n\n // Set pendingConnection for handle highlighting\n if (draggedEnd === 'target') {\n canvas.pendingConnection = {\n source: edge.source,\n sourceHandle: edge.sourceHandle,\n position: flowPos,\n };\n }\n\n // Set _pendingReconnection\n canvas._pendingReconnection = {\n edge,\n draggedEnd: draggedEnd!,\n anchorPosition: { ...anchorCoords },\n position: flowPos,\n };\n\n // Auto-pan\n connectAutoPan = startConnectionAutoPan(containerEl, canvas, lastMoveX, lastMoveY);\n\n if (draggedEnd === 'target') {\n applyValidationClasses(containerEl, edge.source, edge.sourceHandle ?? 'source', canvas, edge.id);\n }\n };\n\n const onPointerMove = (moveE: PointerEvent) => {\n lastMoveX = moveE.clientX;\n lastMoveY = moveE.clientY;\n\n if (!dragging) {\n const dist = Math.sqrt(\n (moveE.clientX - startX) ** 2 + (moveE.clientY - startY) ** 2,\n );\n if (dist >= DRAG_THRESHOLD) {\n startReconnectionDrag();\n }\n return;\n }\n\n // Update temp line\n const flowPos = canvas.screenToFlowPosition(moveE.clientX, moveE.clientY);\n\n // Connection snap: find closest handle within radius\n const snap = findSnapTarget({\n containerEl,\n handleType: draggedEnd === 'target' ? 'target' : 'source',\n excludeNodeId: draggedEnd === 'target' ? edge.source : edge.target,\n cursorFlowPos: flowPos,\n connectionSnapRadius,\n getNode: (id: string) => canvas.getNode(id),\n toFlowPosition: (sx: number, sy: number) => canvas.screenToFlowPosition(sx, sy),\n });\n\n if (snap.element !== snappedHandle) {\n snappedHandle?.classList.remove('flow-handle-active');\n snap.element?.classList.add('flow-handle-active');\n snappedHandle = snap.element;\n }\n\n reconnectLineInstance?.update({\n fromX: anchorCoords.x, fromY: anchorCoords.y,\n toX: snap.position.x, toY: snap.position.y,\n source: edge.source, sourceHandle: edge.sourceHandle,\n });\n\n // Update pending states\n const snapFlowPos = snap.position;\n if (draggedEnd === 'target' && canvas.pendingConnection) {\n canvas.pendingConnection = {\n ...canvas.pendingConnection,\n position: snapFlowPos,\n };\n }\n if (canvas._pendingReconnection) {\n canvas._pendingReconnection = {\n ...canvas._pendingReconnection,\n position: snapFlowPos,\n };\n }\n\n connectAutoPan?.updatePointer(moveE.clientX, moveE.clientY);\n };\n\n const cleanupReconnection = () => {\n if (reconnectCleanedUp) return;\n reconnectCleanedUp = true;\n\n document.removeEventListener('pointermove', onPointerMove);\n document.removeEventListener('pointerup', onPointerUp);\n connectAutoPan?.stop();\n connectAutoPan = null;\n reconnectLineInstance?.destroy();\n reconnectLineInstance = null;\n tempSvg = null;\n snappedHandle?.classList.remove('flow-handle-active');\n activeReconnectCleanup = null;\n gEl.classList.remove('flow-edge-reconnecting');\n clearValidationClasses(containerEl);\n canvas.pendingConnection = null;\n canvas._pendingReconnection = null;\n };\n\n const onPointerUp = async (upE: PointerEvent) => {\n if (!dragging) {\n // Below threshold — treat as click\n cleanupReconnection();\n handleEdgeClick(upE as unknown as MouseEvent);\n return;\n }\n\n // Guard against overlapping drops while an async connectValidator is pending.\n if (canvas._connectValidating) return;\n\n // Use snapped handle if available, otherwise fall back to elementFromPoint\n let handleEl: HTMLElement | null = snappedHandle;\n let dropTarget: Element | null = null;\n if (!handleEl) {\n dropTarget = document.elementFromPoint(upE.clientX, upE.clientY);\n const handleSelector = draggedEnd === 'target'\n ? '[data-flow-handle-type=\"target\"]'\n : '[data-flow-handle-type=\"source\"]';\n handleEl = dropTarget?.closest(handleSelector) as HTMLElement | null;\n }\n\n // Fall back to the node element itself (for handle-less nodes like floating edges)\n const nodeEl = handleEl\n ? handleEl.closest('[data-flow-node-id]') as HTMLElement | null\n : dropTarget?.closest('[data-flow-node-id]') as HTMLElement | null;\n\n const dropNodeId = nodeEl?.dataset.flowNodeId;\n const dropHandleId = handleEl?.dataset.flowHandleId;\n\n // Pulse the drag line while the async connectValidator (if any) awaits.\n // The class is a no-op on sync paths — setDragLineValidating only\n // adds/removes; the validator lifecycle inside applyReconnectValidation\n // is synchronous when no validator is configured.\n const dragLineEl = reconnectLineInstance?.svg ?? null;\n setDragLineValidating(dragLineEl, true);\n let result: Awaited>;\n try {\n result = await resolveEdgeBodyReconnect({\n dropNodeId,\n dropHandleId,\n draggedEnd: draggedEnd!,\n edge,\n canvas,\n containerEl,\n });\n } finally {\n setDragLineValidating(dragLineEl, false);\n }\n\n if (result.applied) {\n debug('reconnect', `Edge \"${edge.id}\" reconnected (${draggedEnd})`, result.newConnection);\n } else {\n debug('reconnect', `Edge \"${edge.id}\" reconnection cancelled — snapping back`, { reason: result.reason });\n }\n\n canvas._emit('reconnect-end', { edge, successful: result.applied });\n cleanupReconnection();\n };\n\n document.addEventListener('pointermove', onPointerMove);\n document.addEventListener('pointerup', onPointerUp);\n activeReconnectCleanup = cleanupReconnection;\n };\n\n gEl.addEventListener('pointerdown', handlePointerDown);\n\n // ── Hover feedback: show grab cursor near reconnectable endpoints ──\n const handlePointerMove = (e: PointerEvent) => {\n const edge = evaluate(expression) as FlowEdge;\n if (!edge) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n\n const globalReconnectable = canvas._config?.edgesReconnectable !== false;\n const edgeReconnectable = edge.reconnectable ?? true;\n\n if (!globalReconnectable || edgeReconnectable === false || !lastSrcCoords || !lastTgtCoords) {\n gEl.style.removeProperty('cursor');\n interactionPath.style.cursor = 'pointer';\n return;\n }\n\n const reconnectSnapRadius = canvas._config?.reconnectSnapRadius ?? DEFAULT_RECONNECT_SNAP_RADIUS;\n const flowPos = canvas.screenToFlowPosition(e.clientX, e.clientY);\n\n const nearSrc = (isNearEndpoint(flowPos.x, flowPos.y, lastSrcCoords.x, lastSrcCoords.y, reconnectSnapRadius)\n || (lastVisibleSrcCoords && isNearEndpoint(flowPos.x, flowPos.y, lastVisibleSrcCoords.x, lastVisibleSrcCoords.y, reconnectSnapRadius)))\n && (edgeReconnectable === true || edgeReconnectable === 'source');\n const nearTgt = (isNearEndpoint(flowPos.x, flowPos.y, lastTgtCoords.x, lastTgtCoords.y, reconnectSnapRadius)\n || (lastVisibleTgtCoords && isNearEndpoint(flowPos.x, flowPos.y, lastVisibleTgtCoords.x, lastVisibleTgtCoords.y, reconnectSnapRadius)))\n && (edgeReconnectable === true || edgeReconnectable === 'target');\n\n if (nearSrc || nearTgt) {\n gEl.style.cursor = 'grab';\n interactionPath.style.cursor = 'grab';\n } else {\n gEl.style.removeProperty('cursor');\n interactionPath.style.cursor = 'pointer';\n }\n };\n gEl.addEventListener('pointermove', handlePointerMove);\n\n // Handle selection via keyboard (Enter/Space)\n const handleEdgeKeyDown = (e: KeyboardEvent) => {\n if (e.key !== 'Enter' && e.key !== ' ') return;\n e.preventDefault();\n e.stopPropagation();\n\n const edge = evaluate(expression) as FlowEdge;\n if (!edge) return;\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n\n canvas._emit('edge-click', { edge, event: e });\n\n if (matchesModifier(e, canvas._shortcuts?.multiSelect)) {\n if (canvas.selectedEdges.has(edge.id)) {\n canvas.selectedEdges.delete(edge.id);\n edge.selected = false;\n } else {\n canvas.selectedEdges.add(edge.id);\n edge.selected = true;\n }\n } else {\n canvas.deselectAll();\n canvas.selectedEdges.add(edge.id);\n edge.selected = true;\n }\n canvas._emitSelectionChange();\n };\n gEl.addEventListener('keydown', handleEdgeKeyDown);\n\n // Focus-visible class for SVG (keyboard-only — skip mouse/pointer focus)\n const handleEdgeFocus = () => {\n if (gEl.matches(':focus-visible')) {\n gEl.classList.add('flow-edge-focused');\n }\n };\n const handleEdgeBlur = () => gEl.classList.remove('flow-edge-focused');\n gEl.addEventListener('focus', handleEdgeFocus);\n gEl.addEventListener('blur', handleEdgeBlur);\n\n // Prevent mousedown from bubbling to the container's d3-zoom handler.\n // pointerdown fires before mousedown — stopPropagation on pointerdown\n // does NOT suppress the separate mousedown event that d3-zoom listens for.\n const handleMouseDown = (e: MouseEvent) => { e.stopPropagation(); };\n gEl.addEventListener('mousedown', handleMouseDown);\n\n // Hover detection for label visibility (labels are separate DIV elements\n // in the viewport, not children of this , so CSS :hover won't cascade)\n const handleEdgeMouseEnter = () => {\n for (const lbl of [labelEl, labelStartEl, labelEndEl]) {\n if (lbl) lbl.classList.add('flow-edge-hovered');\n }\n };\n const handleEdgeMouseLeave = () => {\n for (const lbl of [labelEl, labelStartEl, labelEndEl]) {\n if (lbl) lbl.classList.remove('flow-edge-hovered');\n }\n };\n gEl.addEventListener('mouseenter', handleEdgeMouseEnter);\n gEl.addEventListener('mouseleave', handleEdgeMouseLeave);\n\n effect(() => {\n const edge = evaluate(expression) as FlowEdge;\n if (!edge || !pathEl) return;\n\n // Tag with edge ID so handle-initiated reconnection can find it\n gEl.setAttribute('data-flow-edge-id', edge.id);\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas?.nodes) return;\n\n // Resolve edge type using fallback: edge.type ?? canvas.defaultEdgeType ?? 'bezier'\n const resolvedEdgeType = edge.type ?? canvas._config?.defaultEdgeType ?? 'bezier';\n\n // ── WS-D zoom LOD ────────────────────────────────────────────────\n // Opt-in: at/under the configured zoom bucket, render as a straight\n // line (skip pathfinding + curvature). Only read `_zoomLevel` when\n // `edgeLod` is set, so the default path gains no new reactive dep.\n // `_zoomLevel` is bucketed, so this fires only on threshold crossings.\n const edgeLod = canvas._config?.edgeLod;\n let effectiveEdgeType = resolvedEdgeType;\n if (edgeLod) {\n const level = canvas._zoomLevel; // reactive, bucketed\n const simplify = edgeLod.simplifyAt === 'medium'\n ? (level === 'medium' || level === 'far')\n : level === 'far';\n if (simplify) {\n effectiveEdgeType = 'straight';\n }\n }\n\n // Reactive dependency — bumped each frame during layout animation\n // so edges re-measure DOM handle positions while CSS transitions run.\n void canvas._layoutAnimTick;\n // Reactive dependency — routing signal. Bumped per-edge by\n // _markDirtyEdges when a commit affects THIS edge's corridor, so only\n // the affected edges re-route instead of the whole edge graph.\n void canvas._edgeDirtyTicks?.get(edge.id);\n\n const rawSource = canvas.getNode(edge.source);\n const rawTarget = canvas.getNode(edge.target);\n if (!rawSource || !rawTarget) return;\n\n // Read sourcePosition/targetPosition to register Alpine reactive\n // dependencies so edges re-render when handle positions change.\n void rawSource.sourcePosition;\n void rawTarget.targetPosition;\n\n // Wrap with absolute positions so all downstream calculations use flow-space coords\n const sourceNode = toAbsoluteNode(rawSource, canvas._nodeMap, canvas._config?.nodeOrigin);\n const targetNode = toAbsoluteNode(rawTarget, canvas._nodeMap, canvas._config?.nodeOrigin);\n\n const container = el.closest('[x-data]') as Element;\n\n let sourcePos: HandlePosition;\n let targetPos: HandlePosition;\n let srcMeasurement: HandleMeasurement | null;\n let tgtMeasurement: HandleMeasurement | null;\n\n // ── Schema handle geometry (WS-G) ─────────────────────────────────\n // Schema rows are uniform, so once `_schemaMetrics` is cached every\n // handle center on a schema node is arithmetic — no rect reads. Resolved\n // BEFORE the standard branch so the fast path never touches the DOM.\n //\n // Reading `_schemaMetrics` here DOES track a dependency on the property's\n // identity (Alpine.raw() doesn't unwrap the merge-scope proxy — see the\n // `_obstacleSnapshot` note below). That is intended: metrics go\n // null → object exactly once, one tick after the first schema node\n // renders, so each edge re-runs once and upgrades from the DOM path to\n // this one. A colorMode change nulls the cache and it re-measures. It is\n // a one-time transition, not a per-frame dependency.\n //\n // `nodeOrigin` is a hard disqualifier: `toAbsoluteNode` only applies it\n // to nodes with a `parentId`, so under a non-default origin a ROOT node's\n // rendered top-left is `position − dimensions × origin`, not `position`,\n // and the arithmetic below would desync from what the browser painted.\n const schemaMetrics = canvas._schemaMetrics as SchemaMetrics | null | undefined;\n const configOrigin = canvas._config?.nodeOrigin;\n const schemaEndpoints =\n resolvedEdgeType !== 'floating'\n && canvas._config?.schemaHandleGeometry !== 'dom'\n && !!schemaMetrics\n && (!configOrigin || (configOrigin[0] === 0 && configOrigin[1] === 0))\n ? resolveSchemaEndpoints(\n rawSource,\n rawTarget,\n sourceNode,\n targetNode,\n edge.sourceHandle,\n edge.targetHandle,\n canvas._nodeElements as Map | undefined,\n schemaMetrics,\n )\n : null;\n\n if (resolvedEdgeType === 'floating') {\n // ── Floating: compute endpoints from node geometry ──\n const floating = getFloatingEdgeParams(sourceNode, targetNode);\n sourcePos = floating.sourcePos;\n targetPos = floating.targetPos;\n\n // Synthetic zero-size measurement (no physical handle)\n srcMeasurement = { x: floating.sx, y: floating.sy, handleWidth: 0, handleHeight: 0 };\n tgtMeasurement = { x: floating.tx, y: floating.ty, handleWidth: 0, handleHeight: 0 };\n\n lastSrcCoords = { x: floating.sx, y: floating.sy };\n lastTgtCoords = { x: floating.tx, y: floating.ty };\n } else if (schemaEndpoints) {\n // ── Schema fast path (WS-G): endpoints from state, zero rect reads ──\n sourcePos = schemaEndpoints.sourcePos;\n targetPos = schemaEndpoints.targetPos;\n srcMeasurement = schemaEndpoints.srcMeasurement;\n tgtMeasurement = schemaEndpoints.tgtMeasurement;\n lastSrcCoords = { x: srcMeasurement.x, y: srcMeasurement.y };\n lastTgtCoords = { x: tgtMeasurement.x, y: tgtMeasurement.y };\n } else {\n // ── Standard: resolve from handle elements ──\n // Compute screen-space centers of each endpoint node ONCE so the\n // handle-geometry picker (for nodes with same-(id,type) mirrors,\n // e.g. schema rows) can choose whichever side of the node is\n // physically closer to the OPPOSING endpoint.\n // Resolve endpoint elements in O(1) via the canvas node-element\n // registry, falling back to a container-wide query when an element\n // isn't registered (test mounts, mid-init edge cases).\n const sourceNodeEl = (canvas._nodeElements?.get(edge.source)\n ?? container.querySelector(`[data-flow-node-id=\"${CSS.escape(edge.source)}\"]`)) as HTMLElement | null;\n const targetNodeEl = (canvas._nodeElements?.get(edge.target)\n ?? container.querySelector(`[data-flow-node-id=\"${CSS.escape(edge.target)}\"]`)) as HTMLElement | null;\n const sourceCenter = sourceNodeEl ? rectCenter(sourceNodeEl.getBoundingClientRect()) : undefined;\n const targetCenter = targetNodeEl ? rectCenter(targetNodeEl.getBoundingClientRect()) : undefined;\n\n sourcePos = resolveHandlePosition(container, edge.source, edge.sourceHandle, 'source', rawSource, targetCenter, sourceNodeEl);\n targetPos = resolveHandlePosition(container, edge.target, edge.targetHandle, 'target', rawTarget, sourceCenter, targetNodeEl);\n\n // Read zoom/viewport from the raw object to avoid creating a reactive\n // dependency on viewport.zoom. Edge paths are in flow-space and\n // scale via CSS transform — they don't need recomputation on zoom.\n const rawViewport = Alpine.raw(canvas).viewport ?? { x: 0, y: 0, zoom: 1 };\n const zoom = rawViewport.zoom || 1;\n\n // Track rotation so edges re-render when nodes rotate.\n // Also rotate the handle cardinal direction to world space so path\n // generators route in the correct direction from rotated handles.\n const srcRotation = rawSource.rotation;\n const tgtRotation = rawTarget.rotation;\n sourcePos = rotateHandlePos(sourcePos, srcRotation);\n targetPos = rotateHandlePos(targetPos, tgtRotation);\n\n srcMeasurement = measureHandleCoords(container, edge.source, sourceNode, edge.sourceHandle, 'source', zoom, rawViewport, targetCenter, sourceNodeEl);\n tgtMeasurement = measureHandleCoords(container, edge.target, targetNode, edge.targetHandle, 'target', zoom, rawViewport, sourceCenter, targetNodeEl);\n\n // Cache handle center coords for reconnection hit-detection\n const fallbackSrc = getHandleCoords(sourceNode, sourcePos, canvas._shapeRegistry, canvas._config?.nodeOrigin);\n const fallbackTgt = getHandleCoords(targetNode, targetPos, canvas._shapeRegistry, canvas._config?.nodeOrigin);\n lastSrcCoords = srcMeasurement ?? fallbackSrc;\n lastTgtCoords = tgtMeasurement ?? fallbackTgt;\n }\n\n // Shorten endpoints so markers sit outside the handle boundary\n let adjustedSrc = shortenEndpoint(srcMeasurement ?? lastSrcCoords!, sourcePos, srcMeasurement, edge.markerStart);\n let adjustedTgt = shortenEndpoint(tgtMeasurement ?? lastTgtCoords!, targetPos, tgtMeasurement, edge.markerEnd);\n\n // WS-2: fan edges that share an endpoint handle across the row extent.\n // Gated: no-op unless spread is enabled for the endpoint's node. The lane\n // index/count come from the canvas grouping pass; the offset shifts the\n // attach point (and thus the route-cache key) so routing stays correct.\n if (resolvedEdgeType === 'orthogonal' || resolvedEdgeType === 'avoidant') {\n const grouping = Alpine.raw(canvas._endpointSpreadGrouping) as EndpointSpreadGrouping | null | undefined;\n if (grouping) {\n const srcSpacing = resolveSpreadSpacing(rawSource.endpointSpread ?? canvas._config?.avoidantEndpointSpread);\n if (srcSpacing !== null) {\n const g = grouping.get(`${edge.source}|${edge.sourceHandle ?? ''}`);\n const lane = g?.lanes.get(edge.id);\n if (g && lane !== undefined && g.count > 1) {\n const srcExtent = canvas._schemaMetrics?.rowHeight ?? srcMeasurement?.handleHeight ?? 0;\n adjustedSrc = applyLaneOffset(adjustedSrc, sourcePos, laneOffset(lane, g.count, srcExtent, srcSpacing));\n }\n }\n const tgtSpacing = resolveSpreadSpacing(rawTarget.endpointSpread ?? canvas._config?.avoidantEndpointSpread);\n if (tgtSpacing !== null) {\n const g = grouping.get(`${edge.target}|${edge.targetHandle ?? ''}`);\n const lane = g?.lanes.get(edge.id);\n if (g && lane !== undefined && g.count > 1) {\n const tgtExtent = canvas._schemaMetrics?.rowHeight ?? tgtMeasurement?.handleHeight ?? 0;\n adjustedTgt = applyLaneOffset(adjustedTgt, targetPos, laneOffset(lane, g.count, tgtExtent, tgtSpacing));\n }\n }\n }\n }\n\n // Cache visible endpoints for reconnection hit-detection\n lastVisibleSrcCoords = adjustedSrc;\n lastVisibleTgtCoords = adjustedTgt;\n\n // Compute obstacle rects for orthogonal routing (performance-gated).\n //\n // Obstacles now come from the shared `_obstacleSnapshot` (built once\n // per commit by `_commitNodeGeometry`, Workstream C) rather than each\n // edge rebuilding its own obstacle array per effect run. Re-routing is\n // triggered per-edge via `_edgeDirtyTicks` (read at the top of this\n // effect): `_markDirtyEdges` bumps only the edges whose corridor could\n // actually be affected by the changed node(s), so unaffected edges\n // don't re-run at all. `_layoutAnimTick` remains only the MEASUREMENT\n // signal (re-measure handle DOM during layout animation) — it is not a\n // routing dependency.\n //\n // NB: `canvas` is Alpine's merged-scope proxy, which Alpine.raw() does\n // not unwrap, so we unwrap the genuine reactive nested properties\n // instead (`Alpine.raw(canvas._obstacleSnapshot)` / `Alpine.raw(canvas.nodes)`).\n let obstacleRects: Rect[] | undefined;\n if (resolvedEdgeType === 'orthogonal' || resolvedEdgeType === 'avoidant') {\n // WS-D: while an endpoint node is being dragged, skip pathfinding and\n // let the router fall back to a bezier curve (empty-obstacle fast\n // path). `Set.has(id)` is key-scoped, so only edges touching the\n // dragged node re-run when `_draggingNodeIds` changes.\n const simplifyingOnDrag = canvas._config?.avoidantSimplifyOnDrag !== false\n && (canvas._draggingNodeIds?.has(edge.source) || canvas._draggingNodeIds?.has(edge.target));\n if (simplifyingOnDrag) {\n obstacleRects = undefined;\n } else {\n const snapshot = Alpine.raw(canvas._obstacleSnapshot) as\n | Array<{ id: string; x: number; y: number; width: number; height: number }>\n | null\n | undefined;\n if (snapshot) {\n // Shared snapshot (built once per commit). Filter out this edge's\n // own endpoints. Structurally assignable to Rect[] (extra `id` is\n // ignored by the router). Preserves node order from the snapshot,\n // which preserves `raw.nodes` order — so the router's VALUE-keyed\n // route cache still hits (same rects, same order as the legacy\n // per-edge build) and routes come out unchanged.\n obstacleRects = snapshot.filter((r) => r.id !== edge.source && r.id !== edge.target);\n } else {\n // Fallback for minimal mounts that never triggered a geometry\n // commit — legacy per-edge build, byte-identical to the prior\n // behavior so routes are unchanged when no snapshot exists yet.\n const rawNodes = Alpine.raw(canvas.nodes) as FlowNode[];\n const rawNodeMap = new Map(rawNodes.map((n): [string, FlowNode] => [n.id, n]));\n const nodeOrigin = canvas._config?.nodeOrigin;\n obstacleRects = rawNodes\n .filter((n: FlowNode) => n.id !== edge.source && n.id !== edge.target)\n .map((n: FlowNode) => {\n const abs = toAbsoluteNode(n, rawNodeMap, nodeOrigin);\n return {\n x: abs.position.x,\n y: abs.position.y,\n width: abs.dimensions?.width ?? DEFAULT_NODE_WIDTH,\n height: abs.dimensions?.height ?? DEFAULT_NODE_HEIGHT,\n };\n });\n }\n }\n }\n\n // Only the path geometry uses the LOD-resolved type; everything else\n // (markers, labels, edge classes) keeps the configured type via `edge`.\n const pathEdge = effectiveEdgeType === resolvedEdgeType ? edge : { ...edge, type: effectiveEdgeType };\n const { path, labelPosition } = getEdgePath(pathEdge, sourceNode, targetNode, sourcePos, targetPos, adjustedSrc, adjustedTgt, canvas._config?.edgeTypes, obstacleRects, canvas._shapeRegistry, canvas._config?.nodeOrigin, canvas._config?.defaultEdgeType);\n pathEl.setAttribute('d', path);\n interactionPath.setAttribute('d', path);\n\n // Record this edge's endpoint-bbox corridor AFTER routing so\n // `_markDirtyEdges` can test future changed-node rects against it\n // (mirrors `corridorObstacles()`'s own CORRIDOR_MARGIN expansion in\n // orthogonal.ts — an obstacle outside this range is pruned by the\n // router itself, so it provably cannot change this edge's route).\n // Non-reactive: written on the raw Map so recording a corridor never\n // triggers other edges' effects.\n if (resolvedEdgeType === 'orthogonal' || resolvedEdgeType === 'avoidant') {\n const rawCorridors = Alpine.raw(canvas._edgeCorridors) as\n | Map\n | undefined;\n rawCorridors?.set(edge.id, {\n minX: Math.min(adjustedSrc.x, adjustedTgt.x),\n minY: Math.min(adjustedSrc.y, adjustedTgt.y),\n maxX: Math.max(adjustedSrc.x, adjustedTgt.x),\n maxY: Math.max(adjustedSrc.y, adjustedTgt.y),\n });\n }\n\n // ── Editable edge control points ──────────────────────────\n const isEditable = resolvedEdgeType === 'editable';\n const showPoints = isEditable && (edge.showControlPoints || edge.selected);\n\n // Clear previous control point elements\n gEl.querySelectorAll('.flow-edge-control-point, .flow-edge-midpoint').forEach(cpEl => cpEl.remove());\n\n if (showPoints) {\n const points = edge.controlPoints ?? [];\n const zoom = canvas.viewport?.zoom ?? 1;\n const pointRadius = 6 / zoom; // counter-scale\n const midpointRadius = 5 / zoom;\n\n // Build full waypoint chain\n const srcPt = lastSrcCoords ?? { x: 0, y: 0 };\n const tgtPt = lastTgtCoords ?? { x: 0, y: 0 };\n const allPoints = [srcPt, ...points, tgtPt];\n const segCount = allPoints.length - 1;\n\n // Place midpoint ghosts on the actual SVG path curve (not linear midpoints)\n const totalLen = pathEl.getTotalLength?.() ?? 0;\n if (totalLen > 0) {\n // Find approximate path-length for each waypoint\n const waypointLens: number[] = [0];\n const sampleCount = 200;\n let wpIdx = 1;\n for (let s = 1; s <= sampleCount && wpIdx < allPoints.length; s++) {\n const len = (s / sampleCount) * totalLen;\n const pt = pathEl.getPointAtLength(len);\n const wp = allPoints[wpIdx];\n const dx = pt.x - wp.x;\n const dy = pt.y - wp.y;\n if (dx * dx + dy * dy < 25) { // within 5px\n waypointLens.push(len);\n wpIdx++;\n }\n }\n // Fill remaining if not all found\n while (waypointLens.length <= segCount) {\n waypointLens.push(totalLen);\n }\n\n for (let i = 0; i < segCount; i++) {\n const midLen = (waypointLens[i] + waypointLens[i + 1]) / 2;\n const pt = pathEl.getPointAtLength(midLen);\n const mid = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n mid.classList.add('flow-edge-midpoint');\n mid.setAttribute('cx', String(pt.x));\n mid.setAttribute('cy', String(pt.y));\n mid.setAttribute('r', String(midpointRadius));\n mid.dataset.segmentIndex = String(i);\n const titleEl = document.createElementNS('http://www.w3.org/2000/svg', 'title');\n titleEl.textContent = 'Double-click to add control point';\n mid.appendChild(titleEl);\n gEl.appendChild(mid);\n }\n }\n\n // Render control point handles\n for (let i = 0; i < points.length; i++) {\n const cp = document.createElementNS('http://www.w3.org/2000/svg', 'circle');\n cp.classList.add('flow-edge-control-point');\n cp.setAttribute('cx', String(points[i].x));\n cp.setAttribute('cy', String(points[i].y));\n cp.setAttribute('r', String(pointRadius));\n cp.dataset.pointIndex = String(i);\n gEl.appendChild(cp);\n }\n }\n\n // ── Editable edge cursor ──────────────────────────────────\n interactionPath.style.cursor = isEditable ? 'crosshair' : 'pointer';\n\n // ── Interaction width (per-edge → config default → 20) ──\n interactionPath.style.strokeWidth = String(\n edge.interactionWidth ?? canvas._config?.defaultInteractionWidth ?? 20,\n );\n\n // ── Markers ──────────────────────────────────────────────\n // When `_renderDualMarker` is set (bidirectional-edge collapse has\n // picked this edge as the primary of a reciprocal pair), mirror the\n // end marker onto the start so one path carries arrows at both ends.\n if (edge.markerStart != null) {\n const cfg = normalizeMarker(edge.markerStart);\n const id = getMarkerId(cfg, canvas._id);\n pathEl.setAttribute('marker-start', `url(#${id})`);\n } else if (edge._renderDualMarker && edge.markerEnd) {\n const cfg = normalizeMarker(edge.markerEnd);\n const id = getMarkerId(cfg, canvas._id);\n pathEl.setAttribute('marker-start', `url(#${id})`);\n } else {\n pathEl.removeAttribute('marker-start');\n }\n\n if (edge.markerEnd) {\n const cfg = normalizeMarker(edge.markerEnd);\n const id = getMarkerId(cfg, canvas._id);\n pathEl.setAttribute('marker-end', `url(#${id})`);\n } else {\n pathEl.removeAttribute('marker-end');\n }\n\n // ── Stroke width (inline style to override .flow-edges path CSS) ──\n const baseStrokeWidth = edge.strokeWidth ?? 1.5;\n\n // ── Animation mode ───────────────────────────────────────\n const mode = resolveAnimationMode(edge.animated);\n\n if (mode !== currentAnimMode) {\n pathEl.classList.remove('flow-edge-animated', 'flow-edge-pulse');\n if (currentAnimMode === 'dot') removeDotAnimation();\n currentAnimMode = mode;\n }\n\n switch (mode) {\n case 'dash': pathEl.classList.add('flow-edge-animated'); break;\n case 'pulse': pathEl.classList.add('flow-edge-pulse'); break;\n case 'dot': ensureDotAnimation(gEl, path, container, edge, edge.animationDuration); break;\n }\n\n // Per-edge animation duration override\n if (edge.animationDuration && mode !== 'none') {\n if (mode === 'dash' || mode === 'pulse') {\n pathEl.style.animationDuration = edge.animationDuration;\n }\n // Dot duration is applied inside ensureDotAnimation via the edge reference\n } else if (mode === 'dash' || mode === 'pulse') {\n pathEl.style.removeProperty('animation-duration');\n }\n\n // ── Custom CSS class ─────────────────────────────────────\n // Remove previous class from group if it changed.\n // edge.class may be a space-separated string of multiple tokens\n // (e.g. \"flow-edge-entering flow-edge-taken\") — spread into individual\n // tokens for classList operations which do not accept space-separated strings.\n if (currentEdgeClass && currentEdgeClass !== edge.class) {\n gEl.classList.remove(...currentEdgeClass.split(' ').filter(Boolean));\n }\n if (edge.class) {\n const animClass = mode === 'dash' ? ' flow-edge-animated' : mode === 'pulse' ? ' flow-edge-pulse' : '';\n pathEl.setAttribute('class', edge.class + animClass);\n // Also apply to the SVG group so edge styling can be driven at group level\n gEl.classList.add(...edge.class.split(' ').filter(Boolean));\n currentEdgeClass = edge.class;\n } else {\n // Remove edge.class from group if it was previously set but now cleared\n if (currentEdgeClass) {\n gEl.classList.remove(...currentEdgeClass.split(' ').filter(Boolean));\n currentEdgeClass = null;\n }\n }\n\n // ── Selection class + selected stroke styling ────────────\n gEl.setAttribute('aria-selected', String(!!edge.selected));\n if (edge.selected) {\n gEl.classList.add('flow-edge-selected');\n pathEl.style.strokeWidth = String(Math.max(baseStrokeWidth + 1, 2.5));\n pathEl.style.stroke = 'var(--flow-edge-stroke-selected, ' + CONNECTION_ACTIVE_COLOR + ')';\n } else {\n gEl.classList.remove('flow-edge-selected');\n pathEl.style.strokeWidth = String(baseStrokeWidth);\n\n const defsEl = canvas._markerDefsEl?.querySelector('defs') ?? null;\n\n if (isGradient(edge.color)) {\n // Gradient edge — create/update the def\n if (defsEl) {\n const gradId = getGradientId(canvas._id, edge.id);\n const reversed = edge.gradientDirection === 'target-source';\n\n // Use source/target handle positions for gradient coordinates\n const sx = lastSrcCoords!.x, sy = lastSrcCoords!.y;\n const tx = lastTgtCoords!.x, ty = lastTgtCoords!.y;\n\n // TODO(future): Support radial gradients — check for gradient.type === 'radial'\n // and create instead of \n // TODO(future): Support path-following gradients — would need SVG \n // with a gradient texture applied along the path curve\n upsertGradientDef(\n defsEl,\n gradId,\n reversed ? { from: edge.color.to, to: edge.color.from } : edge.color,\n sx, sy, tx, ty,\n );\n\n pathEl.style.stroke = `url(#${gradId})`;\n currentGradientId = gradId;\n }\n } else if (edge.color) {\n // Solid color — remove gradient if previously was gradient\n if (currentGradientId) {\n const defsForRemove = defsEl;\n if (defsForRemove) removeGradientDef(defsForRemove, currentGradientId);\n currentGradientId = null;\n }\n pathEl.style.stroke = edge.color;\n } else {\n // No color — remove gradient if present, clear inline stroke so CSS classes apply\n if (currentGradientId) {\n const defsForRemove = defsEl;\n if (defsForRemove) removeGradientDef(defsForRemove, currentGradientId);\n currentGradientId = null;\n }\n pathEl.style.removeProperty('stroke');\n }\n }\n\n // ── Row-highlight: highlight edges connected to selected rows ──\n // Track only the specific row keys this edge touches — NOT\n // selectedRows.size. Reading `.size` is an iterate-level dependency, so\n // any row select/deselect would re-run EVERY edge effect (re-pathfinding\n // the whole avoidant graph). `has(key)` tracks that key alone; `clear()`\n // still notifies previously-present keys, so un-highlighting works.\n const rowHighlighted = !edge.selected && (\n (edge.sourceHandle ? canvas.selectedRows?.has(edge.sourceHandle.replace(/-[lr]$/, '')) : false) ||\n (edge.targetHandle ? canvas.selectedRows?.has(edge.targetHandle.replace(/-[lr]$/, '')) : false)\n );\n\n if (rowHighlighted) {\n gEl.classList.add('flow-edge-row-highlighted');\n if (!edge.selected) {\n pathEl.style.strokeWidth = String(Math.max(baseStrokeWidth + 0.5, 2));\n pathEl.style.stroke = getComputedStyle(gEl.closest('.flow-container') as Element)\n .getPropertyValue('--flow-edge-row-highlight-color').trim() || '#3b82f6';\n }\n } else {\n gEl.classList.remove('flow-edge-row-highlighted');\n }\n\n // ── Keyboard focusability ────────────────────────────────\n const edgeFocusable = edge.focusable ?? (canvas._config?.edgesFocusable !== false);\n if (edgeFocusable) {\n gEl.setAttribute('tabindex', '0');\n gEl.setAttribute('role', edge.ariaRole ?? 'group');\n gEl.setAttribute('aria-label', edge.ariaLabel ?? (edge.label ? `Edge: ${edge.label}` : `Edge from ${edge.source} to ${edge.target}`));\n } else {\n gEl.removeAttribute('tabindex');\n gEl.removeAttribute('role');\n gEl.removeAttribute('aria-label');\n }\n\n // Custom DOM attributes\n if (edge.domAttributes) {\n for (const [key, val] of Object.entries(edge.domAttributes)) {\n if (key.startsWith('on') || BLOCKED_ATTRS.has(key.toLowerCase())) continue;\n gEl.setAttribute(key, val);\n }\n }\n\n // ── Labels ───────────────────────────────────────────────\n const ensureLabel = (\n existing: HTMLDivElement | null,\n text: string | undefined,\n cssClass: string,\n viewport: Element | null,\n edgeId: string,\n ): HTMLDivElement | null => {\n if (text) {\n // If closure lost reference (e.g., directive re-init), reclaim from DOM\n if (!existing && viewport) {\n const isStart = cssClass.includes('flow-edge-label-start');\n const isEnd = cssClass.includes('flow-edge-label-end');\n let selector = `[data-flow-edge-id=\"${edgeId}\"].flow-edge-label`;\n if (isStart) selector += '.flow-edge-label-start';\n else if (isEnd) selector += '.flow-edge-label-end';\n else selector += ':not(.flow-edge-label-start):not(.flow-edge-label-end)';\n existing = viewport.querySelector(selector) as HTMLDivElement | null;\n }\n if (!existing) {\n existing = document.createElement('div');\n existing.className = cssClass;\n existing.dataset.flowEdgeId = edgeId;\n if (viewport) viewport.appendChild(existing);\n }\n existing.textContent = text;\n return existing;\n }\n if (existing) { existing.remove(); }\n return null;\n };\n\n const viewport = el.closest('.flow-viewport');\n const labelVis = edge.labelVisibility ?? 'always';\n\n // Lazily measure + cache the path length, keyed by the `d` attribute set\n // above. Only called when a label actually needs it, so edges without\n // labels still never force SVG geometry.\n const getCachedTotalLength = (): number => {\n const currentD = pathEl.getAttribute('d') ?? '';\n if (currentD !== cachedPathD) {\n cachedPathD = currentD;\n cachedTotalLength = typeof pathEl.getTotalLength === 'function' ? (pathEl.getTotalLength() || 0) : 0;\n }\n return cachedTotalLength;\n };\n\n // Center label (uses labelPosition percentage or default midpoint)\n labelEl = ensureLabel(labelEl, edge.label, 'flow-edge-label', viewport, edge.id);\n if (labelEl) {\n const totalLength = getCachedTotalLength();\n if (totalLength > 0) {\n const t = edge.labelPosition ?? 0.5;\n const pt = getPointAtPercent(pathEl, t, totalLength);\n labelEl.style.left = `${pt.x}px`;\n labelEl.style.top = `${pt.y}px`;\n } else {\n labelEl.style.left = `${labelPosition.x}px`;\n labelEl.style.top = `${labelPosition.y}px`;\n }\n }\n\n // Start label (fixed pixel offset from source end)\n labelStartEl = ensureLabel(labelStartEl, edge.labelStart, 'flow-edge-label flow-edge-label-start', viewport, edge.id);\n if (labelStartEl) {\n const totalLength = getCachedTotalLength();\n if (totalLength > 0) {\n const offset = edge.labelStartOffset ?? 30;\n const pt = pathEl.getPointAtLength(Math.min(offset, totalLength / 2));\n labelStartEl.style.left = `${pt.x}px`;\n labelStartEl.style.top = `${pt.y}px`;\n }\n }\n\n // End label (fixed pixel offset from target end)\n labelEndEl = ensureLabel(labelEndEl, edge.labelEnd, 'flow-edge-label flow-edge-label-end', viewport, edge.id);\n if (labelEndEl) {\n const totalLength = getCachedTotalLength();\n if (totalLength > 0) {\n const offset = edge.labelEndOffset ?? 30;\n const pt = pathEl.getPointAtLength(Math.max(totalLength - offset, totalLength / 2));\n labelEndEl.style.left = `${pt.x}px`;\n labelEndEl.style.top = `${pt.y}px`;\n }\n }\n\n // Apply label visibility classes and custom edge class\n for (const lbl of [labelEl, labelStartEl, labelEndEl]) {\n if (!lbl) continue;\n lbl.classList.toggle('flow-edge-label-hover', labelVis === 'hover');\n lbl.classList.toggle('flow-edge-label-on-select', labelVis === 'selected');\n lbl.classList.toggle('flow-edge-label-selected', !!edge.selected);\n // Forward edge.class to label so label styling can track edge state.\n // edge.class may be space-separated (\"flow-edge-taken flow-edge-entering\")\n // — split into individual tokens for classList operations.\n if (edge.class) {\n lbl.classList.add(...edge.class.split(' ').filter(Boolean));\n } else if (currentEdgeClass) {\n lbl.classList.remove(...currentEdgeClass.split(' ').filter(Boolean));\n }\n }\n });\n\n cleanup(() => {\n // Remove gradient def if this edge had one\n if (currentGradientId) {\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n const defsEl = canvas?._markerDefsEl?.querySelector('defs');\n if (defsEl) removeGradientDef(defsEl, currentGradientId);\n }\n\n activeReconnectCleanup?.();\n removeDotAnimation();\n gEl.removeEventListener('contextmenu', handleContextMenu);\n gEl.removeEventListener('dblclick', handleDblClick);\n gEl.removeEventListener('pointerdown', handleControlPointPointerDown, true);\n gEl.removeEventListener('pointerdown', handlePointerDown);\n gEl.removeEventListener('pointermove', handlePointerMove);\n gEl.removeEventListener('keydown', handleEdgeKeyDown);\n gEl.removeEventListener('focus', handleEdgeFocus);\n gEl.removeEventListener('blur', handleEdgeBlur);\n gEl.removeEventListener('mousedown', handleMouseDown);\n gEl.removeEventListener('mouseenter', handleEdgeMouseEnter);\n gEl.removeEventListener('mouseleave', handleEdgeMouseLeave);\n labelEl?.remove();\n labelStartEl?.remove();\n labelEndEl?.remove();\n });\n },\n );\n}\n","// ============================================================================\n// canvas-dom — DOM flush mixin for flow-canvas\n//\n// Five methods that directly manipulate DOM elements to synchronize visual\n// state without waiting for Alpine's reactive update cycle. Used during\n// animations, drag operations, and viewport transforms where low-latency\n// updates are critical.\n//\n// Cross-mixin deps (optional chaining): ctx._applyBackground, ctx._applyCulling,\n// ctx.getNode, ctx.getEdge, ctx.getEdgePathElement, ctx.getAbsolutePosition\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { FlowNode, FlowEdge } from '../../core/types';\nimport { parseStyle } from '../../animate/interpolators';\nimport { getHandleCoords, shortenEndpoint, getEdgePath } from '../../core/edge-utils';\nimport { getFloatingEdgeParams } from '../../core/floating-edge';\nimport { toAbsoluteNode } from '../../core/sub-flow';\nimport { isGradient, getGradientId, upsertGradientDef } from '../../core/gradients';\nimport { resolveHandlePosition } from '../directives/flow-edge';\n\nexport function createDomMixin(ctx: CanvasContext, Alpine: any) {\n return {\n /** Write node positions directly to DOM elements (bypassing Alpine effects). */\n _flushNodePositions(nodeIds: Set) {\n for (const nodeId of nodeIds) {\n const node = ctx.getNode(nodeId) as FlowNode | undefined;\n if (!node) continue;\n const nodeEl = ctx._nodeElements.get(nodeId);\n if (!nodeEl) continue;\n const rawNode = Alpine.raw(node);\n const absPos = rawNode.parentId ? ctx.getAbsolutePosition(nodeId) : rawNode.position;\n const nodeOrig = rawNode.nodeOrigin ?? ctx._config.nodeOrigin ?? [0, 0];\n const nw = rawNode.dimensions?.width ?? 150;\n const nh = rawNode.dimensions?.height ?? 40;\n nodeEl.style.left = (absPos.x - nw * nodeOrig[0]) + 'px';\n nodeEl.style.top = (absPos.y - nh * nodeOrig[1]) + 'px';\n }\n },\n\n /** Write node styles directly to DOM elements (bypassing Alpine effects). */\n _flushNodeStyles(nodeIds: Set) {\n for (const nodeId of nodeIds) {\n const node = ctx.getNode(nodeId) as FlowNode | undefined;\n if (!node) continue;\n const nodeEl = ctx._nodeElements.get(nodeId);\n if (!nodeEl) continue;\n const rawNode = Alpine.raw(node);\n const style = rawNode.style;\n if (!style) continue;\n const parsed = typeof style === 'string' ? parseStyle(style) : style as Record;\n for (const [prop, value] of Object.entries(parsed)) {\n nodeEl.style.setProperty(prop, value);\n }\n }\n },\n\n /** Write edge color/strokeWidth directly to SVG elements (bypassing Alpine effects). */\n _flushEdgeStyles(edgeIds: Set) {\n for (const edgeId of edgeIds) {\n const edge = ctx.getEdge(edgeId) as FlowEdge | undefined;\n if (!edge) continue;\n const rawEdge = Alpine.raw(edge);\n const pathEl = ctx.getEdgePathElement(edgeId);\n if (!pathEl) continue;\n\n if (typeof rawEdge.color === 'string') {\n pathEl.style.stroke = rawEdge.color;\n }\n\n if (rawEdge.strokeWidth !== undefined) {\n pathEl.style.strokeWidth = String(rawEdge.strokeWidth);\n }\n }\n },\n\n /** Push current viewport state to the DOM (transform, background, culling). */\n _flushViewport() {\n if (ctx._viewportEl) {\n const vp = ctx.viewport;\n ctx._viewportEl.style.transform = `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`;\n }\n ctx._applyBackground();\n ctx._applyCulling();\n },\n\n /** Recompute SVG paths, label positions, and gradients for edges connected to the given node IDs. */\n _refreshEdgePaths(movedNodeIds: Set) {\n for (const edge of ctx.edges as FlowEdge[]) {\n if (!movedNodeIds.has(edge.source) && !movedNodeIds.has(edge.target)) continue;\n\n const rawSource = Alpine.raw(ctx.getNode(edge.source)) as FlowNode | undefined;\n const rawTarget = Alpine.raw(ctx.getNode(edge.target)) as FlowNode | undefined;\n if (!rawSource || !rawTarget) continue;\n\n const sourceNode = toAbsoluteNode(rawSource, ctx._nodeMap, ctx._config.nodeOrigin);\n const targetNode = toAbsoluteNode(rawTarget, ctx._nodeMap, ctx._config.nodeOrigin);\n\n let pathD: string;\n let labelPos: { x: number; y: number };\n let srcCoords: { x: number; y: number };\n let tgtCoords: { x: number; y: number };\n\n if (edge.type === 'floating') {\n const fp = getFloatingEdgeParams(sourceNode, targetNode);\n srcCoords = { x: fp.sx, y: fp.sy };\n tgtCoords = { x: fp.tx, y: fp.ty };\n const adjSrc = shortenEndpoint(srcCoords, fp.sourcePos, null, edge.markerStart);\n const adjTgt = shortenEndpoint(tgtCoords, fp.targetPos, null, edge.markerEnd);\n const result = getEdgePath(edge, sourceNode, targetNode, fp.sourcePos, fp.targetPos, adjSrc, adjTgt, undefined, undefined, ctx._shapeRegistry, ctx._config.nodeOrigin);\n pathD = result.path;\n labelPos = result.labelPosition;\n } else {\n const container = ctx._container as Element;\n // Compute screen-space centers of each endpoint node ONCE so the\n // handle-geometry picker (schema-node mirrors etc.) can pick the\n // side closer to the opposing endpoint.\n let sourceCenter: { x: number; y: number } | undefined;\n let targetCenter: { x: number; y: number } | undefined;\n if (container) {\n const srcEl = container.querySelector(\n `[data-flow-node-id=\"${CSS.escape(edge.source)}\"]`,\n ) as HTMLElement | null;\n const tgtEl = container.querySelector(\n `[data-flow-node-id=\"${CSS.escape(edge.target)}\"]`,\n ) as HTMLElement | null;\n if (srcEl) {\n const r = srcEl.getBoundingClientRect();\n sourceCenter = { x: (r.left + r.right) / 2, y: (r.top + r.bottom) / 2 };\n }\n if (tgtEl) {\n const r = tgtEl.getBoundingClientRect();\n targetCenter = { x: (r.left + r.right) / 2, y: (r.top + r.bottom) / 2 };\n }\n }\n const srcPos = container\n ? resolveHandlePosition(container, edge.source, edge.sourceHandle, 'source', rawSource, targetCenter)\n : (rawSource?.sourcePosition ?? 'bottom');\n const tgtPos = container\n ? resolveHandlePosition(container, edge.target, edge.targetHandle, 'target', rawTarget, sourceCenter)\n : (rawTarget?.targetPosition ?? 'top');\n srcCoords = getHandleCoords(sourceNode, srcPos, ctx._shapeRegistry, ctx._config.nodeOrigin);\n tgtCoords = getHandleCoords(targetNode, tgtPos, ctx._shapeRegistry, ctx._config.nodeOrigin);\n const adjSrc = shortenEndpoint(srcCoords, srcPos, null, edge.markerStart);\n const adjTgt = shortenEndpoint(tgtCoords, tgtPos, null, edge.markerEnd);\n const result = getEdgePath(edge, sourceNode, targetNode, srcPos, tgtPos, adjSrc, adjTgt, undefined, undefined, ctx._shapeRegistry, ctx._config.nodeOrigin);\n pathD = result.path;\n labelPos = result.labelPosition;\n }\n\n const pathEl = ctx.getEdgePathElement(edge.id);\n if (pathEl) {\n pathEl.setAttribute('d', pathD);\n const gEl = pathEl.parentElement;\n const interactionPath = gEl?.querySelector('path:first-child') as SVGPathElement | null;\n if (interactionPath && interactionPath !== pathEl) {\n interactionPath.setAttribute('d', pathD);\n }\n }\n\n if (isGradient(edge.color)) {\n const defsEl = ctx._markerDefsEl?.querySelector('defs');\n if (defsEl) {\n const gradId = getGradientId(ctx._id, edge.id);\n const reversed = edge.gradientDirection === 'target-source';\n upsertGradientDef(\n defsEl,\n gradId,\n reversed ? { from: edge.color.to, to: edge.color.from } : edge.color,\n srcCoords.x, srcCoords.y, tgtCoords.x, tgtCoords.y,\n );\n }\n }\n\n if ((edge.label || edge.labelStart || edge.labelEnd) && ctx._viewportEl) {\n if (edge.label) {\n const lbl = ctx._viewportEl.querySelector(\n `[data-flow-edge-id=\"${edge.id}\"].flow-edge-label:not(.flow-edge-label-start):not(.flow-edge-label-end)`,\n ) as HTMLElement | null;\n if (lbl) {\n lbl.style.left = `${labelPos.x}px`;\n lbl.style.top = `${labelPos.y}px`;\n }\n }\n\n if (edge.labelStart && pathEl) {\n const lbl = ctx._viewportEl.querySelector(\n `[data-flow-edge-id=\"${edge.id}\"].flow-edge-label-start`,\n ) as HTMLElement | null;\n if (lbl) {\n const len = pathEl.getTotalLength();\n const offset = edge.labelStartOffset ?? 30;\n const pt = pathEl.getPointAtLength(Math.min(offset, len / 2));\n lbl.style.left = `${pt.x}px`;\n lbl.style.top = `${pt.y}px`;\n }\n }\n\n if (edge.labelEnd && pathEl) {\n const lbl = ctx._viewportEl.querySelector(\n `[data-flow-edge-id=\"${edge.id}\"].flow-edge-label-end`,\n ) as HTMLElement | null;\n if (lbl) {\n const len = pathEl.getTotalLength();\n const offset = edge.labelEndOffset ?? 30;\n const pt = pathEl.getPointAtLength(Math.max(len - offset, len / 2));\n lbl.style.left = `${pt.x}px`;\n lbl.style.top = `${pt.y}px`;\n }\n }\n }\n }\n },\n\n /** Return the registered DOM element for a node by ID, or undefined if not mounted. */\n getNodeElement(id: string): HTMLElement | undefined {\n return ctx._nodeElements.get(id);\n },\n\n /** Walk up from any element to find the enclosing node's ID via the data-flow-node-id attribute.\n * Returns null if no ancestor (or the element itself) carries the attribute. */\n getNodeIdFromElement(el: HTMLElement): string | null {\n const match = el.closest('[data-flow-node-id]');\n return match ? match.getAttribute('data-flow-node-id') : null;\n },\n };\n}\n","// ============================================================================\n// canvas-config — Runtime config patching mixin for flow-canvas\n//\n// Applies partial configuration changes at runtime: pan/zoom settings,\n// background, debug mode, color mode, auto-layout, and behavioral flags.\n//\n// Cross-mixin deps (optional chaining): ctx._panZoom, ctx._applyBackground,\n// ctx._scheduleAutoLayout, ctx._container, ctx._colorModeHandle\n// ============================================================================\n\nimport type { CanvasContext } from './canvas-context';\nimport type { AutoLayoutConfig } from '../../core/types';\nimport { setDebugEnabled } from '../../core/debug';\nimport { createColorMode } from '../../core/color-mode';\n\nexport function createConfigMixin(ctx: CanvasContext) {\n return {\n _applyConfigPatch(changes: Record) {\n const c = ctx._config as any;\n\n for (const [key, val] of Object.entries(changes)) {\n if (val === undefined) continue;\n (c as any)[key] = val;\n\n switch (key) {\n case 'pannable':\n case 'zoomable':\n case 'minZoom':\n case 'maxZoom':\n case 'panOnScroll':\n case 'panOnScrollDirection':\n case 'panOnScrollSpeed':\n ctx._panZoom?.update({ [key]: val });\n break;\n case 'background':\n ctx._background = val;\n ctx._applyBackground();\n break;\n case 'backgroundGap':\n ctx._backgroundGap = val;\n if (ctx._container) {\n ctx._container.style.setProperty('--flow-bg-pattern-gap', String(val));\n }\n break;\n case 'patternColor':\n ctx._patternColorOverride = val;\n if (ctx._container) {\n ctx._container.style.setProperty('--flow-bg-pattern-color', val);\n }\n break;\n case 'debug':\n setDebugEnabled(!!val);\n break;\n case 'preventOverlap':\n (ctx._config as any).preventOverlap = val as boolean | number;\n break;\n case 'reconnectOnDelete':\n (ctx._config as any).reconnectOnDelete = val as boolean;\n break;\n case 'nodeOrigin':\n (ctx._config as any).nodeOrigin = val as [number, number];\n break;\n case 'preventCycles':\n (ctx._config as any).preventCycles = val as boolean;\n break;\n case 'loading':\n ctx._userLoading = !!val;\n break;\n case 'loadingText':\n ctx._loadingText = val as string;\n break;\n case 'colorMode':\n (ctx._config as any).colorMode = val as 'light' | 'dark' | 'system' | undefined;\n if (val && ctx._container) {\n if (!ctx._colorModeHandle) {\n ctx._colorModeHandle = createColorMode(ctx._container, val as any);\n } else {\n ctx._colorModeHandle.update(val as any);\n }\n } else if (!val && ctx._colorModeHandle) {\n ctx._colorModeHandle.destroy();\n ctx._colorModeHandle = null;\n }\n break;\n case 'autoLayout':\n c.autoLayout = val ? val as AutoLayoutConfig : undefined;\n ctx._autoLayoutFailed = false;\n if (val) {\n ctx._autoLayoutReady = true;\n ctx._scheduleAutoLayout();\n } else {\n ctx._autoLayoutReady = false;\n if (ctx._autoLayoutTimer) {\n clearTimeout(ctx._autoLayoutTimer);\n ctx._autoLayoutTimer = null;\n }\n }\n break;\n }\n }\n },\n };\n}\n","// ============================================================================\n// flowCanvas — Alpine Data Component (Orchestrator)\n//\n// The primary data component that manages flow state: nodes, edges, viewport.\n// Registers as Alpine.data('flowCanvas') and sets up pan/zoom on the container.\n//\n// Method implementations live in mixin modules (canvas-nodes, canvas-edges,\n// canvas-viewport, canvas-selection, canvas-history, canvas-animation,\n// canvas-collapse, canvas-condense, canvas-rows, canvas-layout,\n// canvas-validation, canvas-compute, canvas-dom, canvas-config). This\n// orchestrator file declares\n// reactive state, shared helpers, lifecycle (init/destroy), and wires\n// the mixin methods flat onto the Alpine data object via Object.defineProperties.\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type {\n FlowNode,\n FlowEdge,\n Viewport,\n XYPosition,\n Dimensions,\n FlowCanvasConfig,\n PendingReconnection,\n PendingKeyboardConnect,\n PatchableConfig,\n SchemaMetrics,\n EndpointSpreadGrouping,\n} from '../../core/types';\nimport { createPanZoom, type PanZoomInstance } from '../../core/pan-zoom';\nimport { screenToFlowPosition, getVisibleBounds, type Bounds } from '../../core/geometry';\nimport { setDebugEnabled, debug } from '../../core/debug';\nimport { DEFAULT_FIT_PADDING } from '../../core/constants';\nimport { FlowHistory } from '../../core/history';\nimport type { FlowTimeline } from '../../animate/timeline';\nimport { engine, type EngineHandle } from '../../animate/engine';\nimport { Animator } from '../../animate/animator';\nimport { normalizeMarker, getMarkerId, getMarkerSvg, registerMarker } from '../../core/markers';\nimport type { CustomMarkerRenderer } from '../../core/markers';\nimport { createMiniMap, type MiniMapInstance } from '../../core/minimap';\nimport { createControlsPanel, type ControlsPanelInstance } from '../../core/controls-panel';\nimport { createSelectionBox, type SelectionBoxInstance } from '../../core/selection-box';\nimport { createLasso, type LassoInstance } from '../../core/lasso';\nimport { getNodesInPolygon, getNodesFullyInPolygon, pointInPolygon } from '../../core/lasso-hit-test';\nimport { clearValidationClasses } from '../directives/flow-handle';\nimport { installHandleDelegation } from '../handle-delegation';\nimport { resolveShortcuts, matchesKey, matchesModifier, shouldCaptureNudge } from '../../core/keyboard-shortcuts';\nimport { isDraggable, isSelectable } from '../../core/node-flags';\nimport { attachLongPress } from '../../core/long-press';\nimport { getNodesInRect, getNodesFullyInRect, SpatialGrid, DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from '../../core/geometry';\nimport { CORRIDOR_MARGIN } from '../../core/edge-paths/orthogonal';\nimport { resolveSpreadSpacing } from '../../core/endpoint-spread';\nimport {\n buildNodeMap,\n reconcileChildrenIndex,\n getAbsolutePosition as getAbsolutePositionUtil,\n toAbsoluteNode,\n toAbsoluteNodes,\n sortNodesTopological,\n} from '../../core/sub-flow';\nimport type { CollapseState } from '../../core/collapse';\nimport { builtinShapes } from '../../core/shapes';\nimport { createColorMode, type ColorModeHandle } from '../../core/color-mode';\nimport type { CollabConfig } from '../../collab/types';\nimport { collabStore } from '../../collab/store';\nimport { getAddon, getRegistry } from '../../core/registry';\nimport { FlowAnnouncer } from '../../core/announcer';\nimport { ComputeEngine } from '../../core/compute';\nimport { registerWireEvents, registerWireCommands, registerCustomWireCommands } from '../../core/wire-bridge';\nimport { createLayoutDedup, type LayoutDedup } from './canvas-layout-dedup';\nimport { createBatch } from './canvas-batch';\nimport { clampDimensions } from '../../animate/clamp-dimensions';\n\n// ── Mixin factories ──────────────────────────────────────────────────────────\nimport { createNodesMixin } from './canvas-nodes';\nimport { createEdgesMixin } from './canvas-edges';\nimport { createViewportMixin } from './canvas-viewport';\nimport { createSelectionMixin } from './canvas-selection';\nimport { createHistoryMixin } from './canvas-history';\nimport { createAnimationMixin } from './canvas-animation';\nimport { createCollapseMixin } from './canvas-collapse';\nimport { createCondenseMixin } from './canvas-condense';\nimport { createRowsMixin } from './canvas-rows';\nimport { createLayoutMixin } from './canvas-layout';\nimport { createValidationMixin } from './canvas-validation';\nimport { createComputeMixin } from './canvas-compute';\nimport { createDomMixin } from './canvas-dom';\nimport { createConfigMixin } from './canvas-config';\nimport type { CanvasContext, ActiveParticle } from './canvas-context';\n\nlet instanceCounter = 0;\n\n// ── Background layer helpers ───────────────────────────────────────────────\ntype BgVariant = 'dots' | 'lines' | 'cross';\n\ninterface ResolvedBgLayer {\n variant: BgVariant;\n gap: number;\n color: string;\n}\n\nfunction bgLayerGradient(variant: BgVariant, color: string): string {\n switch (variant) {\n case 'lines':\n case 'cross':\n return `linear-gradient(0deg, ${color} 1px, transparent 1px), linear-gradient(90deg, ${color} 1px, transparent 1px)`;\n case 'dots':\n default:\n return `radial-gradient(circle, ${color} 1px, transparent 1px)`;\n }\n}\n\n// ── Viewport culling helpers (Workstream E) ────────────────────────────────\n/**\n * AABB test: does an edge's recorded route corridor intersect the visible\n * bounds? Corridors can extend beyond their endpoint nodes, so an edge with\n * both endpoints off-screen may still need to render. When no corridor has\n * been recorded for the edge, treat it as intersecting — never wrongly hide.\n */\nfunction corridorIntersectsBounds(\n corridor: { minX: number; minY: number; maxX: number; maxY: number } | undefined,\n bounds: Bounds,\n): boolean {\n if (!corridor) return true;\n return !(\n corridor.maxX < bounds.minX ||\n corridor.minX > bounds.maxX ||\n corridor.maxY < bounds.minY ||\n corridor.minY > bounds.maxY\n );\n}\n\nexport function registerFlowCanvas(Alpine: Alpine) {\n Alpine.data('flowCanvas', (config: FlowCanvasConfig = {}) => {\n const self: Record = {\n // ── Reactive State ────────────────────────────────────────────────\n /** Unique instance ID for SVG marker dedup, etc. */\n _id: `flow-${++instanceCounter}`,\n\n nodes: config.nodes ?? [] as FlowNode[],\n edges: config.edges ?? [] as FlowEdge[],\n viewport: {\n x: config.viewport?.x ?? 0,\n y: config.viewport?.y ?? 0,\n zoom: config.viewport?.zoom ?? 1,\n } as Viewport,\n\n /** Whether the canvas has completed initialization and first node measurement */\n ready: false,\n\n /** User-controlled loading flag, initialized from config.loading */\n _userLoading: (config.loading ?? false) as boolean,\n\n /** Custom text for the default loading indicator */\n _loadingText: (config.loadingText ?? 'Loading\\u2026') as string,\n\n /** Auto-injected loading overlay element (when config.loading: true and no directive) */\n _autoLoadingOverlay: null as HTMLElement | null,\n\n /** True when the canvas is still initializing OR the user has set loading */\n get isLoading(): boolean {\n return !this.ready || this._userLoading;\n },\n\n /** Whether interactivity (pan/zoom/drag) is enabled */\n isInteractive: true,\n\n /** Whether the canvas container is currently in fullscreen mode */\n isFullscreen: false,\n\n /** Fullscreen change handler (bound to document for cleanup) */\n _onFullscreenChange: null as (() => void) | null,\n\n /** Resolved target element while a fullscreen session is active. */\n _fullscreenTarget: null as HTMLElement | null,\n\n /** Currently active connection drag, or null */\n pendingConnection: null as { source: string; sourceHandle?: string; position: XYPosition } | null,\n\n /** Currently active edge reconnection drag, or null */\n _pendingReconnection: null as PendingReconnection | null,\n\n /** Keyboard-armed pending connection (source handle activated via Enter/Space), or null */\n _pendingKeyboardConnect: null as PendingKeyboardConnect | null,\n\n /** Set of selected node IDs */\n selectedNodes: new Set(),\n\n /** Set of selected edge IDs */\n selectedEdges: new Set(),\n\n /** Set of selected row IDs (format: nodeId.attrId) */\n selectedRows: new Set(),\n\n /** Context menu state — populated automatically by context menu events */\n contextMenu: {\n show: false,\n type: null as string | null,\n x: 0,\n y: 0,\n node: null as FlowNode | null,\n edge: null as FlowEdge | null,\n position: null as XYPosition | null,\n nodes: null as FlowNode[] | null,\n event: null as MouseEvent | null,\n },\n\n // ── Shape Registry ─────────────────────────────────────────────────\n _shapeRegistry: { ...builtinShapes, ...config.shapeTypes } as Record,\n\n // ── Background ────────────────────────────────────────────────────\n _background: config.background ?? 'dots' as FlowCanvasConfig['background'],\n _backgroundGap: config.backgroundGap ?? null as number | null,\n _patternColorOverride: config.patternColor ?? null as string | null,\n\n /**\n * Cached resolution of the `--flow-bg-pattern-gap` CSS variable. Reading it\n * requires `getComputedStyle`, a forced style recalc that is prohibitively\n * expensive to run on every viewport frame at schema scale. Populated on the\n * first successful read; invalidate (set `null`) on any theme/colorMode\n * change, since the active theme can redefine the variable.\n */\n _bgGapCache: null as number | null,\n\n /** Last backgroundImage string written to the container — lets `_applyBackground`\n * skip the (per-frame identical) gradient write. */\n _lastBgImage: null as string | null,\n\n /**\n * Cached header/row/handle geometry for `x-flow-schema` nodes, measured\n * once by the first schema node's `render()` (see flow-schema.ts). Plain\n * (non-reactive) field. `Alpine.raw(canvas)` does NOT unwrap Alpine's\n * merge-scope proxy, so a GET/SET through it still tracks/triggers the\n * underlying reactive object inside an active effect — this field is\n * safe only because flow-schema.ts reads and writes it OUTSIDE the\n * directive's `effect()` (deferred via `Alpine.nextTick`), not because of\n * `Alpine.raw()` itself. Invalidate (set `null`) on any theme/colorMode\n * change, same contract as `_bgGapCache` above.\n */\n _schemaMetrics: null as SchemaMetrics | null,\n\n _getBackgroundGap(): number {\n if (this._backgroundGap !== null) {\n return this._backgroundGap;\n }\n if (this._bgGapCache !== null) {\n return this._bgGapCache;\n }\n if (this._container) {\n const raw = getComputedStyle(this._container).getPropertyValue('--flow-bg-pattern-gap').trim();\n const parsed = parseFloat(raw);\n if (!isNaN(parsed)) {\n this._bgGapCache = parsed;\n return parsed;\n }\n }\n return 20;\n },\n\n _resolveBackgroundLayers(): ResolvedBgLayer[] {\n const bg = this._background;\n if (!bg || bg === 'none') return [];\n\n const defaultGap = this._getBackgroundGap();\n const defaultColor = this._patternColorOverride ?? 'var(--flow-bg-pattern-color)';\n\n if (Array.isArray(bg)) {\n return bg.map((layer: any) => ({\n variant: layer.variant ?? 'dots',\n gap: layer.gap ?? defaultGap,\n color: layer.color ?? defaultColor,\n }));\n }\n\n return [{ variant: bg as BgVariant, gap: defaultGap, color: defaultColor }];\n },\n\n backgroundStyle(): { backgroundImage: string; backgroundSize: string; backgroundPosition: string } {\n const layers = this._resolveBackgroundLayers();\n if (layers.length === 0) return { backgroundImage: '', backgroundSize: '', backgroundPosition: '' };\n\n const z = this.viewport.zoom;\n const posX = this.viewport.x;\n const posY = this.viewport.y;\n\n const images: string[] = [];\n const sizes: string[] = [];\n const positions: string[] = [];\n\n for (const layer of layers) {\n const gap = layer.gap * z;\n const effectiveGap = layer.variant === 'cross' ? gap / 2 : gap;\n images.push(bgLayerGradient(layer.variant, layer.color));\n if (layer.variant === 'lines' || layer.variant === 'cross') {\n sizes.push(`${effectiveGap}px ${effectiveGap}px, ${effectiveGap}px ${effectiveGap}px`);\n positions.push(`${posX}px ${posY}px, ${posX}px ${posY}px`);\n } else {\n sizes.push(`${gap}px ${gap}px`);\n positions.push(`${posX}px ${posY}px`);\n }\n }\n\n return {\n backgroundImage: images.join(', '),\n backgroundSize: sizes.join(', '),\n backgroundPosition: positions.join(', '),\n };\n },\n\n // ── Internal ──────────────────────────────────────────────────────\n // Strip collab from stored config — provider objects may contain\n // circular references (e.g. InMemoryProvider.peer) that crash\n // Alpine's deep-reactive proxy walker.\n _config: (() => { const { collab: _, ...rest } = config; return rest; })() as FlowCanvasConfig,\n _shortcuts: resolveShortcuts(config.keyboardShortcuts),\n _container: null as HTMLElement | null,\n _panZoom: null as PanZoomInstance | null,\n _onKeyDown: null as ((e: KeyboardEvent) => void) | null,\n _active: false,\n _zoomLevel: 'close' as 'far' | 'medium' | 'close',\n _onContainerPointerDown: null as ((e: PointerEvent) => void) | null,\n _onCanvasClick: null as ((e: MouseEvent) => void) | null,\n _onCanvasContextMenu: null as ((e: MouseEvent) => void) | null,\n _contextMenuBackdrop: null as HTMLElement | null,\n _markerDefsEl: null as SVGSVGElement | null,\n _minimap: null as MiniMapInstance | null,\n _controls: null as ControlsPanelInstance | null,\n _selectionBox: null as SelectionBoxInstance | null,\n _lasso: null as LassoInstance | null,\n _selectionTool: 'box' as 'box' | 'lasso',\n _onSelectionPointerDown: null as ((e: PointerEvent) => void) | null,\n _onSelectionPointerMove: null as ((e: PointerEvent) => void) | null,\n _onSelectionPointerUp: null as ((e: PointerEvent) => void) | null,\n _selectionShiftHeld: false,\n _selectionEffectiveMode: 'partial' as 'partial' | 'full',\n _suppressNextCanvasClick: false,\n /** Cleanup function for long-press listener */\n _longPressCleanup: null as (() => void) | null,\n /** Whether touch selection mode is currently active */\n _touchSelectionMode: false,\n /** Cleanup function for touch selection mode listeners */\n _touchSelectionCleanup: null as (() => void) | null,\n _nodeMap: new Map(),\n /**\n * Reactive parent-id → child-ids index, reconciled in `_rebuildNodeMap`.\n * Lets each node effect ask \"do I have children?\" via an O(1) keyed lookup\n * instead of scanning the whole nodes array (which subscribed every node\n * effect to the entire array — O(N²) on any array change).\n */\n _childrenIds: new Map(),\n /** Stores each node's originally configured dimensions (before layout stretch). */\n _initialDimensions: new Map(),\n _edgeMap: new Map(),\n _viewportEl: null as HTMLElement | null,\n _handleDelegationCleanup: null as (() => void) | null,\n /**\n * The element the delegated handle listener is CURRENTLY installed on, or null\n * when nothing is installed. Tracked separately from `_viewportEl` because the\n * two can diverge: `_viewportEl` is re-pointed the instant a replacement viewport\n * registers, while the listener stays on whatever node it was attached to until\n * `_registerViewportEl()` moves it. Comparing the two is what detects the move.\n */\n _handleDelegationEl: null as HTMLElement | null,\n /** Set in destroy(); read by the deferred install so a dead canvas never installs. */\n _handleDelegationTornDown: false,\n\n // ── Viewport frame coalescing ─────────────────────────────────────────\n /**\n * The most recent viewport from d3-zoom, written synchronously on every\n * transform (event rate). Reactive `viewport` and all viewport side-effects\n * are flushed from here once per animation frame; synchronous pointer-path\n * math (drag, auto-pan, coordinate transforms) must read this, not the\n * frame-lagged reactive `viewport`.\n */\n _viewportLive: null as Viewport | null,\n /** Pending requestAnimationFrame handle for the coalesced flush, or null. */\n _vpFrame: null as number | null,\n /** True when a user-driven move happened since the last flush (gates `viewport-move`). */\n _vpMoved: false,\n _history: null as FlowHistory | null,\n _announcer: null as FlowAnnouncer | null,\n _computeEngine: new ComputeEngine(),\n _computeDebounceTimer: null as ReturnType | null,\n _animationLocked: false,\n _activeTimelines: new Set(),\n _animationRegistry: new Map[]>(),\n _followHandle: null as EngineHandle | null,\n _animator: null as Animator | null,\n /** Saved pre-collapse state per group node ID */\n _collapseState: new Map(),\n /** Whether this canvas was hydrated from a pre-rendered static diagram */\n _hydratedFromStatic: false,\n\n // ── Layout Dedup ─────────────────────────────────────────────────────\n _layoutDedup: null as LayoutDedup | null,\n\n // ── Shared ResizeObserver (A1) ────────────────────────────────────────\n _resizeObserver: null as ResizeObserver | null,\n\n // ── childLayout watcher cleanup fns (keyed by node id) ───────────────\n _childLayoutCleanups: new Map void>>(),\n\n // ── Shared Particle Loop ────────────────────────────────────────────\n _activeParticles: new Set(),\n _particleEngineHandle: null as EngineHandle | null,\n /** Live CSSStyleDeclaration for the container — cached to avoid per-particle getComputedStyle calls. */\n _containerStyles: null as CSSStyleDeclaration | null,\n\n // ── Color Mode ────────────────────────────────────────────────────\n _colorModeHandle: null as ColorModeHandle | null,\n\n // ── Child Validation ─────────────────────────────────────────────\n _validationErrorCache: new Map() as Map,\n\n // ── Layout animation edge refresh ─────────────────────────────────\n /** Reactive tick bumped each frame during layout animation so edges re-measure DOM. */\n _layoutAnimTick: 0,\n _layoutAnimFrame: 0,\n\n // ── Shared obstacle cache (Workstream C) ─────────────────────────────\n /** Spatial index of node id → flow-space rect cells (committed geometry). Non-reactive: operate on it via `Alpine.raw(canvas._spatialGrid)` (raw the NESTED property — `Alpine.raw(canvas)` does NOT unwrap Alpine's merge-scope proxy; see flow-edge.ts). Also maintained for the viewport-culling overhaul (WS E), which will consume it — do not delete as dead code even though only WS C currently reads it. */\n _spatialGrid: new SpatialGrid(),\n /** Obstacle rects rebuilt once per commit. In the edge effect read it via `Alpine.raw(canvas._obstacleSnapshot)` (nested-raw) so the edge does NOT subscribe to every node's reactive state. */\n _obstacleSnapshot: null as Array<{ id: string; x: number; y: number; width: number; height: number }> | null,\n\n /** WS-2 endpoint-spread lanes; null until first computed. Mutated in place (see _obstacleSnapshot). */\n _endpointSpreadGrouping: null as EndpointSpreadGrouping | null,\n /** Reactive epoch bumped by _commitNodeGeometry (internal signal; edges must NOT subscribe to it). Reserved for the interaction-degradation/LOD workstream (WS D), which will consume it — do not delete as dead code even though only WS C currently reads it. */\n _obstacleEpoch: 0,\n /** REACTIVE Map edge id → tick. Edge effects read key-scoped `.get(edge.id)`; bumped by _markDirtyEdges. */\n _edgeDirtyTicks: new Map(),\n /** PLAIN Map edge id → endpoint-bbox corridor {minX,minY,maxX,maxY}. Written by edges post-route; read via Alpine.raw. */\n _edgeCorridors: new Map(),\n\n // ── Interaction degradation (Workstream D) ───────────────────────────\n /** REACTIVE Set of node ids currently being dragged. Edge effects read\n * key-scoped `.has(edge.source)` / `.has(edge.target)`, so ONLY edges\n * touching a dragged node re-run when the set changes. Populated in\n * flow-node `onDragStart` (incl. group-drag members), cleared on drag end.\n * Drives `avoidantSimplifyOnDrag` bezier degradation. */\n _draggingNodeIds: new Set(),\n\n // ── Auto-Layout ──────────────────────────────────────────────────\n _autoLayoutTimer: null as ReturnType | null,\n _autoLayoutReady: false,\n _autoLayoutFailed: false,\n\n // ── Viewport Culling (CSS-only, outside Alpine reactive system) ────\n _nodeElements: new Map(),\n _edgeSvgElements: new Map(),\n _visibleNodeIds: new Set(),\n /** PLAIN Set of edge ids currently culled (display:none). Used to gate display writes to visibility transitions only. */\n _culledEdgeIds: new Set(),\n /** Whether `_applyCulling` was active on the previous call — used to detect threshold-crossing-down / config-off so `_uncullEverything` can restore display exactly once. */\n _cullingWasActive: false,\n\n // ── Context Menu Auto-Populate ─────────────────────────────────────\n _contextMenuListeners: [] as Array<{ event: string; handler: EventListener }>,\n\n // ── Drop Zone ───────────────────────────────────────────────────────\n _onDropZoneDragOver: null as ((e: DragEvent) => void) | null,\n _onDropZoneDragleave: null as ((e: DragEvent) => void) | null,\n _onDropZoneDrop: null as ((e: DragEvent) => void) | null,\n\n // ── Event Dispatch ────────────────────────────────────────────────\n /**\n * Emit an event: debug log it, invoke the config callback, and\n * dispatch a DOM CustomEvent (flow-xxx) for Alpine @flow-xxx listeners.\n */\n _emit(event: string, detail?: any) {\n // viewport-change/-move fire once per animation frame; keep them out of the\n // debug log so `debug: true` doesn't churn 2-3 lines per frame during zoom.\n // viewport-move-start/-end still log so gesture bracketing stays debuggable.\n if (event !== 'viewport-change' && event !== 'viewport-move') {\n debug('event', event, detail);\n }\n\n // Config callback: 'node-click' → 'onNodeClick'\n const callbackName = 'on' + event.split('-').map(\n (s: string) => s.charAt(0).toUpperCase() + s.slice(1),\n ).join('');\n const callback = (config as any)[callbackName];\n if (typeof callback === 'function') {\n callback(detail);\n }\n\n // DOM event: catchable via @flow-node-click, @flow-connect, etc.\n this._container?.dispatchEvent(new CustomEvent(`flow-${event}`, {\n bubbles: true,\n detail,\n }));\n\n // Screen reader announcement\n this._announcer?.handleEvent(event, detail ?? {});\n\n // Auto-compute propagation\n if (config.computeMode === 'auto' && (event === 'nodes-change' || event === 'edges-change')) {\n if (this._computeDebounceTimer) clearTimeout(this._computeDebounceTimer);\n this._computeDebounceTimer = setTimeout(() => {\n this._computeDebounceTimer = null;\n this.compute();\n }, 16);\n }\n },\n\n /** Route a warning through the onError callback (if set) and console.warn. */\n _warn(code: string, message: string) {\n if (typeof config.onError === 'function') {\n config.onError(code, message);\n }\n console.warn(`[AlpineFlow] ${message}`);\n },\n\n _emitSelectionChange() {\n this._emit('selection-change', {\n nodes: [...this.selectedNodes],\n edges: [...this.selectedEdges],\n rows: [...this.selectedRows],\n });\n },\n\n _rebuildNodeMap() {\n this._nodeMap = buildNodeMap(this.nodes);\n reconcileChildrenIndex(this._childrenIds, this.nodes);\n },\n\n _rebuildEdgeMap() {\n this._edgeMap = new Map(this.edges.map((e: FlowEdge) => [e.id, e]));\n },\n\n /**\n * Rebuild the shared obstacle snapshot + SpatialGrid from committed node\n * geometry. Called imperatively at discrete geometry commit points (drag\n * end, resize, add/remove nodes, undo/redo, restore) — never inside a\n * reactive effect.\n *\n * `Alpine.raw(this)` does NOT unwrap Alpine's merge-scope proxy (it returns\n * the same proxy back), so reads go through the NESTED reactive property\n * instead — `Alpine.raw(this.nodes)` and `Alpine.raw(this._spatialGrid)` —\n * mirroring the precedent in flow-edge.ts's obstacle-rect computation.\n * The parent-lookup map is rebuilt from those raw nodes rather than reusing\n * `_nodeMap`, whose stored node values would still be reactive proxies even\n * once the Map container is raw. Rebuilds the grid from scratch each commit\n * (O(nodes); commit points are discrete user actions, not frames), which\n * also prunes entries for removed/hidden nodes.\n */\n _commitNodeGeometry(changedNodeIds?: string[]): void {\n // Shallow-copied BEFORE this commit mutates the snapshot array (see\n // below) so _markDirtyEdges can also test a changed node's OLD rect: a\n // node moving OUT of an edge's corridor must still dirty that edge, or\n // its route would go stale. The individual rect objects are always\n // freshly created below (never mutated after creation), so a shallow\n // array copy fully captures the \"before\" values.\n const existingSnapshot = Alpine.raw(this._obstacleSnapshot) as\n | Array<{ id: string; x: number; y: number; width: number; height: number }>\n | null;\n const prevSnapshot = existingSnapshot ? existingSnapshot.slice() : null;\n const rawNodes = Alpine.raw(this.nodes) as FlowNode[];\n const rawNodeMap = new Map(rawNodes.map((n): [string, FlowNode] => [n.id, n]));\n const nodeOrigin = this._config?.nodeOrigin;\n const grid = Alpine.raw(this._spatialGrid) as SpatialGrid; // nested-raw unwraps the class instance\n grid.clear();\n const rects: Array<{ id: string; x: number; y: number; width: number; height: number }> = [];\n for (const n of rawNodes) {\n const abs = toAbsoluteNode(n, rawNodeMap, nodeOrigin);\n const rect = {\n id: n.id,\n x: abs.position.x,\n y: abs.position.y,\n width: abs.dimensions?.width ?? DEFAULT_NODE_WIDTH,\n height: abs.dimensions?.height ?? DEFAULT_NODE_HEIGHT,\n };\n // Insert EVERY node (hidden included) into the SpatialGrid: viewport\n // culling (WS-E) sources candidates from the grid and filters hidden\n // nodes itself, so a node un-hidden BETWEEN geometry commits\n // (collapse→expand, wire showNode) must still be present in the grid to\n // be re-shown. Hidden nodes are still excluded from the OBSTACLE\n // snapshot below — they are not routing obstacles.\n grid.insert(n.id, rect.x, rect.y, rect.width, rect.height);\n if (n.hidden) continue; // not a routing obstacle\n rects.push(rect);\n }\n // Keep the SAME array reference across commits (mutate in place)\n // instead of reassigning `this._obstacleSnapshot` every time. Edges\n // read this snapshot via `Alpine.raw(canvas._obstacleSnapshot)` — even\n // wrapped in Alpine.raw(), the property GET on the reactive scope\n // (evaluated before raw() can unwrap the result) still tracks a\n // dependency on `_obstacleSnapshot`'s identity. Reassigning it on every\n // commit would therefore re-run EVERY orthogonal/avoidant edge's effect\n // on every commit (Vue's reactive `set` trap always triggers on a\n // changed reference), silently defeating dirty-corridor invalidation.\n // Mutating the existing array's contents in place instead means the\n // `set` trap never re-fires after the first commit (same reference in,\n // same reference out), leaving `_edgeDirtyTicks` as the only per-edge\n // routing signal edges subscribe to.\n if (existingSnapshot) {\n existingSnapshot.length = 0;\n existingSnapshot.push(...rects);\n } else {\n this._obstacleSnapshot = rects; // first commit only: null → array (real assignment)\n }\n this._obstacleEpoch++;\n this._markDirtyEdges(changedNodeIds, prevSnapshot);\n\n // WS-2: recompute endpoint-spread lanes (node moves reorder lanes) and\n // dirty exactly the edges whose lane changed so they re-route.\n const relaned = this._computeEndpointGrouping();\n if (relaned.size > 0) this._markEdgesDirtyById(relaned);\n },\n\n /**\n * WS-2: assign each avoidant/orthogonal edge a lane index per shared\n * `(node, handleId)`, ordered by the opposite endpoint's position so the\n * fan-in doesn't self-cross. No-op (empty grouping) when spread is disabled\n * everywhere. Returns the set of edge ids whose lane/group membership\n * changed since the last run, so the caller can dirty exactly those. The\n * grouping Map is mutated in place (same reference) so reading it in the\n * edge effect doesn't re-run every edge — mirrors `_obstacleSnapshot`.\n */\n _computeEndpointGrouping(): Set {\n const changed = new Set();\n const canvasSpread = resolveSpreadSpacing(this._config?.avoidantEndpointSpread);\n const rawNodes = Alpine.raw(this.nodes) as FlowNode[];\n const nodeMap = new Map(rawNodes.map((n): [string, FlowNode] => [n.id, n]));\n const nodeOrigin = this._config?.nodeOrigin;\n const center = (id: string): number => {\n const n = nodeMap.get(id);\n if (!n) return 0;\n const abs = toAbsoluteNode(n, nodeMap, nodeOrigin);\n // sort axis = Y (left/right schema handles fan vertically). T/B handles\n // are rare for schema; ordering by Y is still deterministic for them.\n return abs.position.y + (abs.dimensions?.height ?? 0) / 2;\n };\n const perNodeEnabled = (id: string): boolean => {\n const v = nodeMap.get(id)?.endpointSpread;\n return v !== undefined ? resolveSpreadSpacing(v) !== null : canvasSpread !== null;\n };\n\n // Bucket edges per (node, handle), recording the opposite endpoint's sort coord.\n const buckets = new Map>();\n const add = (nodeId: string, handleId: string | undefined, edgeId: string, oppId: string): void => {\n if (!perNodeEnabled(nodeId)) return; // only bucket spread-enabled endpoints\n const key = `${nodeId}|${handleId ?? ''}`;\n let b = buckets.get(key);\n if (!b) { b = []; buckets.set(key, b); }\n b.push({ edgeId, sortKey: center(oppId) });\n };\n const rawEdges = Alpine.raw(this.edges) as FlowEdge[];\n for (const e of rawEdges) {\n const t = e.type ?? this._config?.defaultEdgeType;\n if (t !== 'avoidant' && t !== 'orthogonal') continue; // only routed edges spread\n add(e.source, e.sourceHandle, e.id, e.target);\n add(e.target, e.targetHandle, e.id, e.source);\n }\n\n // Build the new grouping; diff against the old (raw) for the dirty set.\n const old = Alpine.raw(this._endpointSpreadGrouping) as EndpointSpreadGrouping | null;\n const next: EndpointSpreadGrouping = new Map();\n for (const [key, list] of buckets) {\n list.sort((p, q) => p.sortKey - q.sortKey || (p.edgeId < q.edgeId ? -1 : 1)); // deterministic\n const lanes = new Map();\n list.forEach((item, i) => lanes.set(item.edgeId, i));\n next.set(key, { count: list.length, lanes });\n const oldEntry = old?.get(key);\n for (const [edgeId, lane] of lanes) {\n if (!oldEntry || oldEntry.count !== list.length || oldEntry.lanes.get(edgeId) !== lane) {\n changed.add(edgeId);\n }\n }\n }\n // Edges that were in a group before but aren't now (group shrank/removed) also changed.\n if (old) {\n for (const [key, entry] of old) {\n if (!next.has(key)) { for (const edgeId of entry.lanes.keys()) changed.add(edgeId); }\n }\n }\n\n // Mutate the raw Map in place to preserve the reference (see _obstacleSnapshot).\n if (old) {\n old.clear();\n for (const [k, v] of next) old.set(k, v);\n } else {\n this._endpointSpreadGrouping = next; // first commit only: null → Map (real assignment)\n }\n return changed;\n },\n\n /**\n * Dirty exactly the edges whose ROUTE could have changed as a result of\n * `changedNodeIds` moving/resizing — instead of every avoidant/orthogonal\n * edge re-routing on every geometry commit.\n *\n * An edge is dirtied when either:\n * - it directly touches a changed node (source or target), or\n * - a changed node's rect (new OR old — see prevSnapshot) intersects\n * that edge's last-recorded corridor, expanded by `CORRIDOR_MARGIN`.\n * This mirrors `corridorObstacles()` in orthogonal.ts exactly: an\n * obstacle outside endpoint-bbox±CORRIDOR_MARGIN is pruned by the\n * router itself, so it provably cannot change that edge's route.\n *\n * Edges with no recorded corridor yet (never routed, or non-obstacle\n * edge types that never write one) are dirtied conservatively so routes\n * never go stale.\n *\n * `_edgeDirtyTicks` is bumped via `.set()` on the REACTIVE map (`this`,\n * not raw) so edge effects reading `_edgeDirtyTicks.get(edge.id)` are\n * notified; all other reads here go through nested-`Alpine.raw()` so\n * this method does not itself subscribe to node/edge state.\n */\n _markDirtyEdges(\n changedNodeIds?: string[],\n prevSnapshot?: Array<{ id: string; x: number; y: number; width: number; height: number }> | null,\n ): void {\n const ticks = this._edgeDirtyTicks; // reactive (set() must notify)\n const rawTicks = Alpine.raw(ticks) as Map;\n const rawEdges = Alpine.raw(this.edges) as FlowEdge[];\n const corridors = Alpine.raw(this._edgeCorridors) as Map;\n const newSnap = Alpine.raw(this._obstacleSnapshot) as\n | Array<{ id: string; x: number; y: number; width: number; height: number }>\n | null;\n const bump = (id: string): void => {\n ticks.set(id, (rawTicks.get(id) ?? 0) + 1);\n };\n\n if (!changedNodeIds || changedNodeIds.length === 0) {\n const liveIds = new Set();\n for (const e of rawEdges) {\n liveIds.add(e.id);\n bump(e.id); // full invalidation (undo/redo/fromObject/init)\n }\n // Prune routing state for edges removed outside removeEdges (e.g.\n // undo/redo/fromObject splice ctx.edges directly, bypassing the\n // cleanup removeEdges does) — otherwise these Maps leak entries for\n // churned edge ids over long sessions. Raw deletes: no reactive\n // trigger needed, the removed edges' effects are already torn down.\n for (const id of [...rawTicks.keys()]) {\n if (!liveIds.has(id)) rawTicks.delete(id);\n }\n for (const id of [...corridors.keys()]) {\n if (!liveIds.has(id)) corridors.delete(id);\n }\n return;\n }\n const changed = new Set(changedNodeIds);\n // Old-rect correctness: a node moving OUT of a corridor must still\n // dirty that edge, so test BOTH the node's new rect (post-commit\n // snapshot) and its old rect (pre-commit prevSnapshot).\n const changedRects: Array<{ x: number; y: number; width: number; height: number }> = [];\n for (const id of changed) {\n const nr = newSnap?.find((o) => o.id === id);\n if (nr) changedRects.push(nr);\n const pr = prevSnapshot?.find((o) => o.id === id);\n if (pr) changedRects.push(pr);\n }\n for (const e of rawEdges) {\n let dirty = changed.has(e.source) || changed.has(e.target);\n if (!dirty) {\n const corridor = corridors.get(e.id);\n if (corridor) {\n for (const r of changedRects) {\n if (\n r.x < corridor.maxX + CORRIDOR_MARGIN && r.x + r.width > corridor.minX - CORRIDOR_MARGIN &&\n r.y < corridor.maxY + CORRIDOR_MARGIN && r.y + r.height > corridor.minY - CORRIDOR_MARGIN\n ) {\n dirty = true;\n break;\n }\n }\n } else {\n dirty = true; // never-routed-yet OR non-obstacle edge — conservative; routes never go stale\n }\n }\n if (dirty) bump(e.id);\n }\n },\n\n /**\n * Bump the routing dirty tick for a specific set of edge ids (WS-2 re-lane).\n * Reuses the SAME tick-bump `_markDirtyEdges` uses: `.set()` on the REACTIVE\n * Map so edge effects reading `_edgeDirtyTicks.get(edge.id)` are notified.\n */\n _markEdgesDirtyById(ids: Set): void {\n const ticks = this._edgeDirtyTicks; // reactive (set() must notify)\n const rawTicks = Alpine.raw(ticks) as Map;\n for (const id of ids) {\n ticks.set(id, (rawTicks.get(id) ?? 0) + 1);\n }\n },\n\n /**\n * Hydrate from a pre-rendered static diagram.\n * Reads the render plan from data-flow-plan, populates node dimensions and\n * viewport from it, then strips the static markers so normal reactivity takes over.\n */\n _hydrateFromStatic() {\n const planAttr = this._container.getAttribute('data-flow-plan');\n if (!planAttr) return;\n\n let plan: any;\n try {\n plan = JSON.parse(planAttr);\n } catch {\n return;\n }\n\n // Populate node dimensions from the render plan\n const planNodeMap = new Map();\n for (const pn of plan.nodes ?? []) {\n planNodeMap.set(pn.id, { width: pn.width, height: pn.height });\n }\n\n for (const node of this.nodes as FlowNode[]) {\n const dims = planNodeMap.get(node.id);\n if (dims && !node.dimensions) {\n node.dimensions = { width: dims.width, height: dims.height };\n this._initialDimensions.set(node.id, { ...dims });\n }\n }\n\n // Set viewport from plan\n if (plan.viewport) {\n this.viewport.x = plan.viewport.x;\n this.viewport.y = plan.viewport.y;\n this.viewport.zoom = plan.viewport.zoom;\n }\n\n this._hydratedFromStatic = true;\n\n // Clean up static attributes\n this._container.removeAttribute('data-flow-static');\n this._container.removeAttribute('data-flow-plan');\n this._container.classList.remove('flow-static');\n },\n\n _captureHistory() {\n this._history?.capture({ nodes: this.nodes, edges: this.edges });\n },\n\n _snapshotHistory(): string | null {\n return this._history ? this._history.snapshot({ nodes: this.nodes, edges: this.edges }) : null;\n },\n\n _commitHistory(snapshot: string | null): void {\n if (snapshot !== null) this._history?.commit(snapshot);\n },\n\n _suspendHistory() {\n this._history?.suspend();\n },\n\n _resumeHistory() {\n this._history?.resume();\n },\n\n _applyBackground() {\n const el = this._container;\n if (!el) return;\n const style = this.backgroundStyle();\n // The gradient image is identical frame-to-frame; only size/position track\n // the viewport. Skip the redundant image write to avoid style churn.\n if (style.backgroundImage !== this._lastBgImage) {\n el.style.backgroundImage = style.backgroundImage;\n this._lastBgImage = style.backgroundImage;\n }\n el.style.backgroundSize = style.backgroundSize;\n el.style.backgroundPosition = style.backgroundPosition;\n },\n\n /**\n * d3-zoom transform handler. Runs on every wheel/pan event (120Hz+ on\n * trackpads). Only the transform write needs event-rate latency; the reactive\n * viewport write plus every side-effect (background, culling, zoom-level,\n * context-menu, events) is coalesced to a single flush per animation frame.\n */\n _onViewportTransform(vp: Viewport) {\n this._viewportLive = vp;\n if (this._viewportEl) {\n this._viewportEl.style.transform = `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`;\n }\n if (this._vpFrame !== null) return;\n this._vpFrame = requestAnimationFrame(() => {\n this._vpFrame = null;\n this._flushViewportFrame();\n });\n },\n\n /**\n * Apply the coalesced viewport state and its side-effects once per frame.\n * Reactive `viewport` is written here (not per event), so consumers watching\n * `viewport` re-run at frame rate rather than per wheel event.\n */\n _flushViewportFrame() {\n const vp = this._viewportLive;\n if (!vp) return;\n this.viewport.x = vp.x;\n this.viewport.y = vp.y;\n this.viewport.zoom = vp.zoom;\n this._applyBackground();\n this._applyCulling();\n this._applyZoomLevel(vp.zoom);\n if (this.contextMenu.show) { this.closeContextMenu(); }\n this._emit('viewport-change', { viewport: { ...vp } });\n if (this._vpMoved) {\n this._vpMoved = false;\n this._emit('viewport-move', { viewport: { ...vp } });\n }\n },\n\n /**\n * Gesture end (user released the wheel/pointer). Commit the end-state\n * synchronously so it is never a frame late, cancelling any pending frame.\n */\n _onViewportMoveEnd(vp: Viewport) {\n if (this._vpFrame !== null) {\n cancelAnimationFrame(this._vpFrame);\n this._vpFrame = null;\n }\n this._flushViewportFrame();\n this._emit('viewport-move-end', { viewport: { ...vp } });\n },\n\n /**\n * Toggle CSS display on off-screen nodes and edges.\n * Called from _flushViewportFrame — entirely outside Alpine's reactive system.\n * Writes are gated to visibility TRANSITIONS only (comparing against the\n * previous frame's `_visibleNodeIds`/`_culledEdgeIds`) to avoid unconditional\n * per-frame style writes, which devtools amplifies into mutation-record churn.\n */\n _applyCulling() {\n const cfg = config.viewportCulling ?? 'auto';\n const active =\n cfg === true || (cfg === 'auto' && this.nodes.length >= (config.cullingAutoThreshold ?? 150));\n if (!active) {\n if (this._cullingWasActive) this._uncullEverything(); // restore display on threshold-crossing down or config change\n return;\n }\n this._cullingWasActive = true;\n if (!this._container) return;\n\n const cw = this._container.clientWidth;\n const ch = this._container.clientHeight;\n if (cw === 0 || ch === 0) return;\n\n const buffer = config.cullingBuffer ?? 100;\n const bounds = getVisibleBounds(this.viewport, cw, ch, buffer);\n\n // Coarse (cell-granularity) superset of node ids whose committed rects\n // could overlap `bounds`. The grid is maintained by _commitNodeGeometry\n // at discrete commit points (C1) — culling only QUERIES it here.\n const grid = Alpine.raw(this._spatialGrid) as SpatialGrid;\n const candidates = grid.query(bounds);\n\n // `_draggingNodeIds` is provided by WS-D (interaction degradation);\n // read defensively so this branch is correct with or without it. The\n // grid holds committed geometry, so a node mid-drag has stale cells —\n // unioning the dragging set prevents culling a node that is being\n // dragged into view.\n const dragging = (this as unknown as { _draggingNodeIds?: Set })._draggingNodeIds;\n\n const visible = new Set();\n const testCandidate = (id: string) => {\n const node = this._nodeMap.get(id);\n if (!node || node.hidden) return;\n const w = node.dimensions?.width ?? 150;\n const h = node.dimensions?.height ?? 50;\n const pos = node.parentId\n ? getAbsolutePositionUtil(node, this._nodeMap, this._config.nodeOrigin)\n : node.position;\n const isVisible = !(\n pos.x + w < bounds.minX ||\n pos.x > bounds.maxX ||\n pos.y + h < bounds.minY ||\n pos.y > bounds.maxY\n );\n if (isVisible) visible.add(id);\n };\n\n for (const id of candidates) testCandidate(id);\n if (dragging) {\n for (const id of dragging) {\n if (!candidates.has(id)) testCandidate(id);\n }\n }\n\n // Sync inline display on every registered node element (mirroring the edge\n // loop below). Iterating `_nodeElements` — not just `candidates ∪ prev` — is\n // required for correctness: a node that is off-screen at the FIRST cull pass,\n // or added off-screen while culling is active, is in neither `visible` nor the\n // previous frame's set, so a set-difference-only diff would leave it rendered.\n // The write stays transition-only: `el.style.display` is read (a cheap\n // inline-style read — no reflow) and written only when it actually changes.\n // The expensive geometry predicate ran only over grid candidates above.\n for (const [id, el] of this._nodeElements) {\n const desired = visible.has(id) ? '' : 'none';\n if (el.style.display !== desired) el.style.display = desired;\n }\n\n // Edge culling: an edge is visible iff either endpoint node is visible,\n // or its recorded route corridor intersects the visible bounds\n // (corridors can extend beyond their endpoints). Rebuild the culled set\n // fresh each frame (mirroring `_visibleNodeIds`) so removed edges — which\n // `_edgeSvgElements` no longer iterates — never re-enter it: this prevents\n // unbounded growth of `_culledEdgeIds` under add/remove churn.\n const prevCulled = this._culledEdgeIds;\n const culled = new Set();\n for (const [edgeId, g] of this._edgeSvgElements) {\n const e = this._edgeMap.get(edgeId);\n if (!e) continue;\n // Edges hidden by the hidden/collapse effect are owned by flow-viewport\n // (flow-viewport.ts sets their inline display). Culling must not fight that\n // writer, or a culled→visible transition would un-hide them. Mirror\n // flow-viewport's isHidden predicate exactly. (A future `.flow-edge-hidden`\n // !important class — symmetric with `.flow-node-hidden` — would remove this\n // coupling; recommended to the owner in the PR.)\n const srcHidden = this._nodeMap.get(e.source)?.hidden;\n const tgtHidden = this._nodeMap.get(e.target)?.hidden;\n if (e.hidden || e._hiddenByCollapse || srcHidden || tgtHidden) {\n continue; // flow-viewport owns this edge's display\n }\n const vis =\n visible.has(e.source) ||\n visible.has(e.target) ||\n corridorIntersectsBounds(this._edgeCorridors.get(edgeId), bounds);\n const was = !prevCulled.has(edgeId);\n if (vis !== was) {\n g.style.display = vis ? '' : 'none';\n }\n if (!vis) culled.add(edgeId);\n }\n\n this._visibleNodeIds = visible;\n this._culledEdgeIds = culled;\n },\n\n /**\n * Restore CSS display on every element culling could have hidden and\n * reset the tracking sets, so a future re-activation of `_applyCulling`\n * starts clean. Called when the `viewportCulling: 'auto'` gate\n * deactivates (node count drops back below `cullingAutoThreshold`) after\n * having been active, or when culling is otherwise turned off.\n */\n _uncullEverything() {\n // Clear culling's inline display override on every tracked element; a\n // `node.hidden` node stays hidden via its `.flow-node-hidden` class (class,\n // not inline), so deferring to '' correctly re-hides only truly-hidden nodes.\n for (const el of this._nodeElements.values()) el.style.display = '';\n // Restore only edges CULLING hid; edges hidden by the hidden/collapse effect\n // are not in `_culledEdgeIds` and must stay hidden (they have no !important\n // class backstop, unlike nodes).\n for (const edgeId of this._culledEdgeIds) {\n const g = this._edgeSvgElements.get(edgeId);\n if (g) g.style.display = '';\n }\n this._visibleNodeIds = new Set();\n this._culledEdgeIds = new Set();\n this._cullingWasActive = false;\n },\n\n _getVisibleNodeIds(): Set {\n return this._visibleNodeIds;\n },\n\n _applyZoomLevel(zoom: number) {\n if (config.zoomLevels === false) return;\n const farThreshold = config.zoomLevels?.far ?? 0.4;\n const medThreshold = config.zoomLevels?.medium ?? 0.75;\n const newLevel: 'far' | 'medium' | 'close' =\n zoom < farThreshold ? 'far' : zoom < medThreshold ? 'medium' : 'close';\n if (newLevel !== this._zoomLevel) {\n this._zoomLevel = newLevel;\n this._container?.setAttribute('data-zoom-level', newLevel);\n }\n },\n\n getAbsolutePosition(nodeId: string): XYPosition {\n const node = this._nodeMap.get(nodeId);\n if (!node) {\n return { x: 0, y: 0 };\n }\n return getAbsolutePositionUtil(node, this._nodeMap, this._config.nodeOrigin);\n },\n\n // ── Init Helpers ─────────────────────────────────────────────────\n\n /** Enable debug logging if configured. */\n _initDebug() {\n if (config.debug) {\n setDebugEnabled(true);\n }\n },\n\n /** Set up container element, attributes, CSS custom properties, animator. */\n _initContainer() {\n this._container = this.$el as HTMLElement;\n this._container.setAttribute('data-flow-canvas', '');\n if (config.fitViewOnInit) {\n this._container.setAttribute('data-fit-view', '');\n }\n this._container.setAttribute('role', 'application');\n this._container.setAttribute('aria-label', config.ariaLabel ?? 'Flow diagram');\n this._containerStyles = getComputedStyle(this._container);\n this._animator = new Animator(engine);\n\n // Apply explicit background config as CSS custom properties\n if (config.patternColor) {\n this._container.style.setProperty('--flow-bg-pattern-color', config.patternColor);\n }\n if (config.backgroundGap) {\n this._container.style.setProperty('--flow-bg-pattern-gap', String(config.backgroundGap));\n }\n\n // Apply containerHeight config as inline --flow-container-height override.\n // The CSS rule `.flow-container { height: var(--flow-container-height, 400px); }`\n // reads this inline value first, winning over the default with specificity (0,0,0).\n const ch = config.containerHeight;\n if (ch !== undefined && ch !== 'auto') {\n let value: string | null = null;\n if (ch === 'fill') {\n value = '100%';\n } else if (typeof ch === 'number' && Number.isFinite(ch)) {\n value = `${ch}px`;\n } else if (typeof ch === 'string' && ch.trim()) {\n value = ch.trim();\n }\n if (value !== null) {\n this._container.style.setProperty('--flow-container-height', value);\n }\n }\n\n // Set initial zoom level attribute\n this._applyZoomLevel(this.viewport.zoom);\n },\n\n /** Create color mode handle if configured. */\n _initColorMode() {\n if (config.colorMode) {\n this._colorModeHandle = createColorMode(this._container, config.colorMode);\n }\n },\n\n /** Hydrate from static HTML, sort nodes, rebuild maps, capture initial dimensions. */\n _initHydration() {\n // Hydrate from pre-rendered static HTML if present\n if (this._container.hasAttribute('data-flow-static')) {\n this._hydrateFromStatic();\n }\n\n // Sort nodes parent-first and build lookup maps\n this.nodes = sortNodesTopological(this.nodes);\n this._rebuildNodeMap();\n this._rebuildEdgeMap();\n\n // Capture configured dimensions before layout stretch modifies them\n for (const node of this.nodes as FlowNode[]) {\n if (node.dimensions) {\n this._initialDimensions.set(node.id, { ...node.dimensions });\n }\n }\n },\n\n /** Create FlowHistory if configured. */\n _initHistory() {\n if (config.history) {\n this._history = new FlowHistory(config.historyMaxSize ?? 50);\n }\n },\n\n /** Create screen reader announcer. */\n _initAnnouncer() {\n if (config.announcements !== false && this._container) {\n const formatFn = typeof config.announcements === 'object'\n ? config.announcements.formatMessage\n : undefined;\n this._announcer = new FlowAnnouncer(this._container, formatFn);\n }\n },\n\n /** Set up collaboration bridge via collab addon plugin. */\n _initCollab() {\n if (config.collab && this._container) {\n const collabFactory = getAddon('collab');\n if (!collabFactory) {\n console.error('[AlpineFlow] Collaboration requires the collab plugin. Register it with: Alpine.plugin(AlpineFlowCollab)');\n return;\n }\n\n const container = this._container;\n const { Doc, Awareness, CollabBridge, CollabAwareness } = collabFactory;\n\n const collabConfig = config.collab as CollabConfig;\n const doc = new Doc();\n const awareness = new Awareness(doc);\n\n const bridge = new CollabBridge(doc, this, collabConfig.provider);\n const collabAwareness = new CollabAwareness(awareness, collabConfig.user);\n collabStore.set(container, { bridge, awareness: collabAwareness, doc });\n\n collabConfig.provider.connect(doc, awareness);\n\n if (collabConfig.cursors !== false) {\n let cursorThrottled = false;\n const throttleMs = collabConfig.throttle ?? 20;\n const onMouseMove = (e: MouseEvent) => {\n if (cursorThrottled) return;\n cursorThrottled = true;\n const rect = container.getBoundingClientRect();\n // Live viewport: reactive `viewport` lags a frame behind during zoom.\n const liveVp = this._viewportLive ?? this.viewport;\n const x = (e.clientX - rect.left - liveVp.x) / liveVp.zoom;\n const y = (e.clientY - rect.top - liveVp.y) / liveVp.zoom;\n collabAwareness.updateCursor({ x, y });\n setTimeout(() => { cursorThrottled = false; }, throttleMs);\n };\n const onMouseLeave = () => {\n collabAwareness.updateCursor(null);\n };\n container.addEventListener('mousemove', onMouseMove);\n container.addEventListener('mouseleave', onMouseLeave);\n const entry = collabStore.get(container)!;\n entry.cursorCleanup = () => {\n container.removeEventListener('mousemove', onMouseMove);\n container.removeEventListener('mouseleave', onMouseLeave);\n };\n }\n }\n },\n\n /** Create panZoom instance, viewport element fallback, apply background, register with store, setup marker defs. */\n _initPanZoom() {\n debug('init', `flowCanvas \"${this._id}\" initializing`, {\n nodes: this.nodes.map((n: FlowNode) => ({ id: n.id, type: n.type ?? 'default', position: n.position, parentId: n.parentId })),\n edges: this.edges.map((e: FlowEdge) => ({ id: e.id, source: e.source, target: e.target, type: e.type ?? 'default' })),\n config: { minZoom: config.minZoom, maxZoom: config.maxZoom, pannable: config.pannable, zoomable: config.zoomable, debug: config.debug },\n });\n\n this._panZoom = createPanZoom(this._container, {\n onTransformChange: (vp: Viewport) => {\n this._onViewportTransform(vp);\n },\n onMoveStart: (vp: Viewport) => {\n this._emit('viewport-move-start', { viewport: { ...vp } });\n },\n onMove: () => {\n // Coalesced: record that a user move happened; viewport-move is emitted\n // (at most once) from the per-frame flush.\n this._vpMoved = true;\n },\n onMoveEnd: (vp: Viewport) => {\n this._onViewportMoveEnd(vp);\n },\n minZoom: config.minZoom,\n maxZoom: config.maxZoom,\n pannable: config.pannable,\n zoomable: config.zoomable,\n translateExtent: config.translateExtent,\n isLocked: () => this._animationLocked,\n noPanClassName: config.noPanClassName ?? 'nopan',\n noWheelClassName: config.noWheelClassName,\n zoomOnDoubleClick: config.zoomOnDoubleClick,\n panOnDrag: config.panOnDrag,\n panActivationKeyCode: config.panActivationKeyCode,\n zoomActivationKeyCode: config.zoomActivationKeyCode,\n isTouchSelectionMode: () => this._touchSelectionMode,\n panOnScroll: config.panOnScroll,\n panOnScrollDirection: config.panOnScrollDirection,\n panOnScrollSpeed: config.panOnScrollSpeed,\n onScrollPan: (dx: number, dy: number) => {\n this.panBy(dx, dy);\n },\n });\n\n // If the user provided a custom initial viewport, sync d3-zoom to it.\n // createPanZoom initializes d3 with an identity transform which fires\n // onTransformChange({0,0,1}) and overwrites the user's viewport.\n if (config.viewport) {\n const vp = {\n x: config.viewport.x ?? 0,\n y: config.viewport.y ?? 0,\n zoom: config.viewport.zoom ?? 1,\n };\n this.viewport.x = vp.x;\n this.viewport.y = vp.y;\n this.viewport.zoom = vp.zoom;\n this._panZoom.setViewport(vp);\n }\n\n // The x-flow-viewport directive registers _viewportEl when it runs.\n // Defer a fallback query in case init() fires before the directive.\n this.$nextTick(() => {\n if (!this._viewportEl) {\n this._viewportEl = this._container?.querySelector('.flow-viewport') as HTMLElement | null;\n }\n if (this._viewportEl) {\n const vp = this.viewport;\n this._viewportEl.style.transform = `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`;\n }\n });\n // The container + theme are attached now, so drop any gap resolved before\n // the active theme was applied and let this first paint re-read it. Any\n // future theme/colorMode-change path that re-applies the background must\n // likewise invalidate `_bgGapCache` before calling `_applyBackground()`.\n this._bgGapCache = null;\n // Same rationale for schema geometry — border/padding can change with\n // the active theme, so drop any measurement taken before it was\n // attached and let the first schema node re-measure. Note: this is the\n // ONLY invalidation site today (same as `_bgGapCache` above); a runtime\n // colorMode change via `_applyConfigPatch` (canvas-config.ts) does not\n // currently invalidate either cache — see Task G1 report.\n this._schemaMetrics = null;\n this._applyBackground();\n\n // Register with global store so other components can access this instance\n (this as any).$store.flow.register(this._id, this);\n\n // Click-to-focus: activate this canvas on pointerdown\n this._onContainerPointerDown = () => {\n (this as any).$store.flow.activate(this._id);\n };\n this._container.addEventListener('pointerdown', this._onContainerPointerDown);\n\n // Auto-activate if this is the only instance (or first to register)\n if (Object.keys((this as any).$store.flow.instances).length === 1) {\n (this as any).$store.flow.activate(this._id);\n }\n\n // Set up SVG for edge markers (arrowheads etc.)\n this._setupMarkerDefs();\n },\n\n /**\n * Install the ONE delegated `pointerdown` listener that starts the connect /\n * reconnect gesture for every handle on this canvas — instead of one listener\n * per handle, of which a large schema graph has thousands.\n *\n * Attached on `.flow-viewport` in the CAPTURE phase. That placement is\n * load-bearing (it is what keeps a handle press from also dragging the node\n * or reordering a schema row, while still yielding to an active whiteboard\n * tool) — the reasoning is in `../handle-delegation.ts`.\n */\n _initHandleDelegation() {\n if (this._config?.delegatedHandleEvents === false) return;\n\n // `_viewportEl` is registered by the x-flow-viewport directive, which has\n // not run when init() executes — resolve on the same $nextTick the panZoom\n // fallback uses.\n this.$nextTick(() => {\n // Alpine's tickStack is global and is NOT drained on component teardown, so\n // this callback still fires for a canvas destroyed in the meantime. Without\n // this check it would install a capture listener that destroy() has already\n // finished cleaning up: a leaked listener holding the dead canvas (nodes,\n // edges, _nodeMap, spatial grid) alive, and — if the viewport survives a\n // morph and a fresh canvas mounts on it — one handle press driving both.\n if (this._handleDelegationTornDown) return;\n\n const viewportEl = (this._viewportEl\n ?? (this._container?.querySelector('.flow-viewport') as HTMLElement | null)) as HTMLElement | null;\n\n if (!viewportEl && this._container) {\n // Non-standard markup: handles do not exist without a viewport. Falling back\n // to the container costs the whiteboard-tool ordering guarantee (their capture\n // listener would land on the SAME element as ours, so suppression degrades to\n // registration order and we would win). Diagnosable rather than silent.\n debug('init', `flowCanvas \"${this._id}\" has no .flow-viewport — delegating handle pointerdown on the container instead; an active whiteboard tool may not suppress handle presses`);\n }\n\n const rootEl = (viewportEl ?? this._container) as HTMLElement | null;\n if (!rootEl) return;\n this._handleDelegationCleanup = installHandleDelegation(rootEl, this);\n this._handleDelegationEl = rootEl;\n });\n },\n\n /**\n * Register the `.flow-viewport` element with the canvas. Called by the\n * `x-flow-viewport` directive every time it initialises — which includes a RE-init\n * on a DIFFERENT element, if the viewport node is ever replaced wholesale rather\n * than patched in place after the canvas mounted.\n *\n * INVARIANT (load-bearing): while the delegated handle listener is installed, it\n * is bound to exactly one element, and that element MUST be the current viewport.\n * Break it and the listener is left on a detached node while handles render inside\n * the new one — so every handle in the canvas goes permanently inert. That failure\n * is silent and total, and delegation removed the safety net that used to cover it:\n * pre-delegation each handle re-attached its own listener on every re-stamp, so a\n * swapped viewport repaired itself. Hence: if delegation is already installed on a\n * different element, move it to the new one.\n *\n * A canvas that never installed (delegation off via `delegatedHandleEvents: false`,\n * or the deferred install has not run yet) has a null `_handleDelegationEl` and\n * falls out at the first guard — this only ever MOVES an existing listener, it\n * never creates one that `_initHandleDelegation` decided against.\n */\n _registerViewportEl(el: HTMLElement) {\n this._viewportEl = el;\n\n const installedOn = this._handleDelegationEl;\n if (!installedOn || installedOn === el) return;\n\n this._handleDelegationCleanup?.();\n this._handleDelegationCleanup = null;\n this._handleDelegationEl = null;\n\n // Destroyed canvas: take the stale listener off, put no new one on.\n if (this._handleDelegationTornDown) return;\n\n this._handleDelegationCleanup = installHandleDelegation(el, this);\n this._handleDelegationEl = el;\n debug('init', `flowCanvas \"${this._id}\" re-bound its delegated handle pointerdown listener to a replaced .flow-viewport`);\n },\n\n /** Canvas click handler, context menu handler, long press, touch selection mode, context menu event listeners. */\n _initClickHandlers() {\n // Click on canvas background → deselect all + emit pane-click\n this._onCanvasClick = (e: MouseEvent) => {\n if (this._suppressNextCanvasClick) {\n this._suppressNextCanvasClick = false;\n return;\n }\n\n if (this.pendingConnection) {\n this._emit('connect-end', {\n connection: null,\n source: this.pendingConnection.source,\n sourceHandle: this.pendingConnection.sourceHandle,\n position: this.screenToFlowPosition(e.clientX, e.clientY),\n });\n this.pendingConnection = null;\n this._container?.classList.remove('flow-connecting');\n if (this._container) clearValidationClasses(this._container);\n }\n\n const target = e.target as HTMLElement;\n if (target === this._container || target.classList.contains('flow-viewport')) {\n const position = this.screenToFlowPosition(e.clientX, e.clientY);\n this._emit('pane-click', { event: e, position });\n this.deselectAll();\n }\n };\n this._container.addEventListener('click', this._onCanvasClick);\n\n // Canvas right-click → pane or selection context menu\n this._onCanvasContextMenu = (e: MouseEvent) => {\n const target = e.target as HTMLElement;\n if (target === this._container || target.classList.contains('flow-viewport')) {\n e.preventDefault();\n\n if (this.selectedNodes.size > 1) {\n const nodes = this.nodes.filter((n: FlowNode) => this.selectedNodes.has(n.id));\n this._emit('selection-context-menu', { nodes, event: e });\n } else {\n const position = this.screenToFlowPosition(e.clientX, e.clientY);\n this._emit('pane-context-menu', { event: e, position });\n }\n }\n };\n this._container.addEventListener('contextmenu', this._onCanvasContextMenu);\n\n // Long-press handler for touch devices\n const longPressAction = config.longPressAction ?? 'context-menu';\n if (longPressAction) {\n this._longPressCleanup = attachLongPress(\n this._container,\n (e: PointerEvent) => {\n const target = e.target as HTMLElement;\n\n if (longPressAction === 'context-menu') {\n const nodeEl = target.closest('[data-flow-node-id]') as HTMLElement;\n if (nodeEl) {\n const nodeId = nodeEl.getAttribute('data-flow-node-id')!;\n const node = this._nodeMap.get(nodeId);\n if (node) {\n this._emit('node-context-menu', { node, event: e });\n return;\n }\n }\n\n const edgeEl = target.closest('.flow-edge-svg') as HTMLElement;\n if (edgeEl) {\n const edgeId = edgeEl.getAttribute('data-edge-id');\n const edge = edgeId ? this._edgeMap.get(edgeId) : undefined;\n if (edge) {\n this._emit('edge-context-menu', { edge, event: e });\n return;\n }\n }\n\n if (this.selectedNodes.size > 1) {\n const nodes = this.nodes.filter((n: FlowNode) => this.selectedNodes.has(n.id));\n this._emit('selection-context-menu', { nodes, event: e });\n } else {\n const position = this.screenToFlowPosition(e.clientX, e.clientY);\n this._emit('pane-context-menu', { event: e, position });\n }\n } else if (longPressAction === 'select') {\n const nodeEl = target.closest('[data-flow-node-id]') as HTMLElement;\n if (nodeEl) {\n const nodeId = nodeEl.getAttribute('data-flow-node-id')!;\n if (this.selectedNodes.has(nodeId)) {\n this.selectedNodes.delete(nodeId);\n } else {\n this.selectedNodes.add(nodeId);\n }\n }\n }\n },\n { duration: config.longPressDuration ?? 500 },\n );\n }\n\n // Two-finger-tap toggles touch selection mode\n if (config.touchSelectionMode !== false) {\n let lastPointerDown = 0;\n let pointerCount = 0;\n\n const onTouchPointerDown = (e: PointerEvent) => {\n if (e.pointerType !== 'touch') return;\n pointerCount++;\n if (pointerCount === 2) {\n const now = Date.now();\n if (now - lastPointerDown < 300) {\n this._touchSelectionMode = !this._touchSelectionMode;\n this._container?.classList.toggle('flow-touch-selection-mode', this._touchSelectionMode);\n }\n }\n lastPointerDown = Date.now();\n };\n\n const onTouchPointerUp = (e: PointerEvent) => {\n if (e.pointerType !== 'touch') return;\n pointerCount = Math.max(0, pointerCount - 1);\n if (pointerCount === 0) lastPointerDown = 0;\n };\n\n const container = this._container;\n if (!container) return;\n container.addEventListener('pointerdown', onTouchPointerDown);\n container.addEventListener('pointerup', onTouchPointerUp);\n container.addEventListener('pointercancel', onTouchPointerUp);\n\n const onVisibilityChange = () => {\n if (document.hidden) {\n pointerCount = 0;\n }\n };\n document.addEventListener('visibilitychange', onVisibilityChange);\n\n const indicator = document.createElement('div');\n indicator.className = 'flow-touch-selection-mode-indicator';\n indicator.textContent = 'Selection Mode \\u2014 tap with two fingers to exit';\n container.appendChild(indicator);\n\n this._touchSelectionCleanup = () => {\n container.removeEventListener('pointerdown', onTouchPointerDown);\n container.removeEventListener('pointerup', onTouchPointerUp);\n container.removeEventListener('pointercancel', onTouchPointerUp);\n document.removeEventListener('visibilitychange', onVisibilityChange);\n indicator.remove();\n };\n }\n\n // Auto-populate contextMenu state from context menu events\n const ctxEvents: Array<{ event: string; handler: EventListener }> = [\n { event: 'flow-node-context-menu', handler: ((e: CustomEvent) => {\n Object.assign(this.contextMenu, { show: true, type: 'node', x: e.detail.event.clientX, y: e.detail.event.clientY, node: e.detail.node, edge: null, position: null, nodes: null, event: e.detail.event });\n }) as EventListener },\n { event: 'flow-edge-context-menu', handler: ((e: CustomEvent) => {\n Object.assign(this.contextMenu, { show: true, type: 'edge', x: e.detail.event.clientX, y: e.detail.event.clientY, node: null, edge: e.detail.edge, position: null, nodes: null, event: e.detail.event });\n }) as EventListener },\n { event: 'flow-pane-context-menu', handler: ((e: CustomEvent) => {\n Object.assign(this.contextMenu, { show: true, type: 'pane', x: e.detail.event.clientX, y: e.detail.event.clientY, node: null, edge: null, position: e.detail.position, nodes: null, event: e.detail.event });\n }) as EventListener },\n { event: 'flow-selection-context-menu', handler: ((e: CustomEvent) => {\n Object.assign(this.contextMenu, { show: true, type: 'selection', x: e.detail.event.clientX, y: e.detail.event.clientY, node: null, edge: null, position: null, nodes: e.detail.nodes, event: e.detail.event });\n }) as EventListener },\n ];\n for (const entry of ctxEvents) {\n this._container.addEventListener(entry.event, entry.handler);\n }\n this._contextMenuListeners = ctxEvents;\n },\n\n /** Keyboard shortcut handler (delete, arrows, undo/redo, copy/paste/cut, selection tool toggle, escape). */\n _initKeyboard() {\n this._onKeyDown = (e: KeyboardEvent) => {\n if (!this._active) return;\n if (this._animationLocked) return;\n\n const tag = (e.target as HTMLElement).tagName;\n const shortcuts = this._shortcuts;\n\n // Escape → close context menu\n if (matchesKey(e.key, shortcuts.escape) && this.contextMenu.show) {\n this.closeContextMenu();\n return;\n }\n\n // Escape → cancel click-to-connect\n if (matchesKey(e.key, shortcuts.escape) && this.pendingConnection) {\n this._emit('connect-end', {\n connection: null,\n source: this.pendingConnection.source,\n sourceHandle: this.pendingConnection.sourceHandle,\n position: { x: 0, y: 0 },\n });\n this.pendingConnection = null;\n this._container?.classList.remove('flow-connecting');\n if (this._container) clearValidationClasses(this._container);\n return;\n }\n\n // Delete selected\n if (matchesKey(e.key, shortcuts.delete)) {\n if (tag === 'INPUT' || tag === 'TEXTAREA') return;\n this._deleteSelected();\n }\n\n // Toggle selection tool (box <-> lasso)\n if (matchesKey(e.key, this._shortcuts.selectionToolToggle) && !e.ctrlKey && !e.metaKey) {\n if (tag === 'INPUT' || tag === 'TEXTAREA') return;\n this._selectionTool = this._selectionTool === 'box' ? 'lasso' : 'box';\n return;\n }\n\n // Arrow keys → move selected nodes\n if (matchesKey(e.key, shortcuts.moveNodes)) {\n if (tag === 'INPUT' || tag === 'TEXTAREA') return;\n if (this._config?.disableKeyboardA11y) return;\n if (this.selectedNodes.size === 0) return;\n\n e.preventDefault();\n const step = matchesModifier(e, shortcuts.moveStepModifier)\n ? shortcuts.moveStep * shortcuts.moveStepMultiplier\n : shortcuts.moveStep;\n let dx = 0, dy = 0;\n\n switch (e.key) {\n case 'ArrowUp': dy = -step; break;\n case 'ArrowDown': dy = step; break;\n case 'ArrowLeft': dx = -step; break;\n case 'ArrowRight': dx = step; break;\n default: {\n const keys = Array.isArray(shortcuts.moveNodes) ? shortcuts.moveNodes : [shortcuts.moveNodes];\n const lk = e.key.length === 1 ? e.key.toLowerCase() : e.key;\n const idx = keys.findIndex((k: string) => (k.length === 1 ? k.toLowerCase() : k) === lk);\n if (idx === 0) dy = -step;\n else if (idx === 1) dy = step;\n else if (idx === 2) dx = -step;\n else if (idx === 3) dx = step;\n }\n }\n\n // Capture once per physical keypress: skip auto-repeat, empty\n // selection, and keys that produced no movement.\n if (shouldCaptureNudge(e.repeat, this.selectedNodes.size, dx, dy)) {\n this._captureHistory();\n }\n\n for (const nodeId of this.selectedNodes) {\n const node = this.getNode(nodeId);\n if (node && isDraggable(node)) {\n node.position.x += dx;\n node.position.y += dy;\n const kbCollab = this._container ? collabStore.get(this._container) : undefined;\n if (kbCollab?.bridge) {\n kbCollab.bridge.pushLocalNodeUpdate(node.id, { position: node.position });\n }\n }\n }\n }\n\n // Undo: Ctrl/Cmd + undo key (without Shift)\n if ((e.ctrlKey || e.metaKey) && !e.shiftKey && matchesKey(e.key, shortcuts.undo)) {\n if (tag === 'INPUT' || tag === 'TEXTAREA') return;\n e.preventDefault();\n this.undo();\n }\n\n // Redo: Ctrl/Cmd + Shift + redo key\n if ((e.ctrlKey || e.metaKey) && e.shiftKey && matchesKey(e.key, shortcuts.redo)) {\n if (tag === 'INPUT' || tag === 'TEXTAREA') return;\n e.preventDefault();\n this.redo();\n }\n\n // Copy/Paste/Cut: Ctrl/Cmd + key (without Shift)\n if ((e.ctrlKey || e.metaKey) && !e.shiftKey) {\n if (tag === 'INPUT' || tag === 'TEXTAREA') return;\n if (matchesKey(e.key, shortcuts.copy)) {\n e.preventDefault();\n this.copy();\n } else if (matchesKey(e.key, shortcuts.paste)) {\n e.preventDefault();\n this.paste();\n } else if (matchesKey(e.key, shortcuts.cut)) {\n e.preventDefault();\n this.cut();\n }\n }\n };\n document.addEventListener('keydown', this._onKeyDown);\n },\n\n /** Create minimap if configured. */\n _initMinimap() {\n if (config.minimap) {\n this._minimap = createMiniMap(this._container, {\n getState: () => ({\n nodes: toAbsoluteNodes(this.nodes, this._nodeMap, this._config.nodeOrigin),\n viewport: this.viewport,\n containerWidth: this._container?.clientWidth ?? 0,\n containerHeight: this._container?.clientHeight ?? 0,\n }),\n getViewportState: () => ({\n viewport: this.viewport,\n containerWidth: this._container?.clientWidth ?? 0,\n containerHeight: this._container?.clientHeight ?? 0,\n }),\n setViewport: (vp) => this._panZoom?.setViewport(vp),\n config,\n });\n this._minimap.render();\n this.$watch('nodes', () => this._minimap?.render());\n this.$watch('viewport', () => this._minimap?.updateViewport());\n }\n },\n\n /** Create controls panel if configured. */\n _initControls() {\n if (config.controls) {\n const controlsContainer = config.controlsContainer\n ? document.querySelector(config.controlsContainer) ?? this._container\n : this._container;\n\n const isExternal = controlsContainer !== this._container;\n this._controls = createControlsPanel(controlsContainer!, {\n position: config.controlsPosition ?? 'bottom-left',\n orientation: config.controlsOrientation ?? 'vertical',\n external: isExternal,\n showZoom: config.controlsShowZoom ?? true,\n showFitView: config.controlsShowFitView ?? true,\n showInteractive: config.controlsShowInteractive ?? true,\n showResetPanels: config.controlsShowResetPanels ?? false,\n onZoomIn: () => this.zoomIn(),\n onZoomOut: () => this.zoomOut(),\n onFitView: () => this.fitView({ padding: DEFAULT_FIT_PADDING }),\n onToggleInteractive: () => this.toggleInteractive(),\n onResetPanels: () => this.resetPanels(),\n onToggleFullscreen: () => this.toggleFullscreen(),\n });\n\n this.$watch('isInteractive', (val: boolean) => {\n this._controls?.update({ isInteractive: val });\n });\n\n this.$watch('isFullscreen', (val: boolean) => {\n this._controls?.update({ isFullscreen: val });\n });\n }\n },\n\n /**\n * Wire a document-level `fullscreenchange` listener so the reactive\n * `isFullscreen` flag stays accurate when the user exits via Escape\n * or any out-of-band means. Safe no-op when the Fullscreen API is\n * unavailable (e.g., restricted iframes).\n */\n _initFullscreen() {\n if (typeof document === 'undefined' || !('fullscreenEnabled' in document)) {\n return;\n }\n this._onFullscreenChange = () => {\n // The expected target is either the resolved custom target (while a\n // session is active) or the canvas container (default behavior).\n const expected = this._fullscreenTarget ?? this._container;\n const nowFullscreen = document.fullscreenElement === expected;\n if (nowFullscreen !== this.isFullscreen) {\n this.isFullscreen = nowFullscreen;\n this._container?.dispatchEvent(new CustomEvent('flow-fullscreen-change', {\n bubbles: true,\n detail: { isFullscreen: nowFullscreen },\n }));\n }\n // Clear resolved target when leaving fullscreen so the next request\n // resolves fresh (important if the config is patched at runtime).\n if (!nowFullscreen) {\n this._fullscreenTarget = null;\n }\n };\n document.addEventListener('fullscreenchange', this._onFullscreenChange);\n },\n\n /**\n * Resolve which element should enter fullscreen. Honors the optional\n * `fullscreenTarget` config (string selector / HTMLElement / function)\n * and falls back to the canvas container.\n */\n _resolveFullscreenTarget(): HTMLElement | null {\n if (!this._container) return null;\n const cfg = this._config?.fullscreenTarget;\n if (!cfg) return this._container;\n if (typeof cfg === 'string') {\n const ancestor = this._container.closest(cfg) as HTMLElement | null;\n if (ancestor) return ancestor;\n const first = document.querySelector(cfg) as HTMLElement | null;\n if (first) return first;\n console.warn(`[AlpineFlow] fullscreenTarget selector \"${cfg}\" did not match; falling back to canvas container.`);\n return this._container;\n }\n if (cfg instanceof HTMLElement) return cfg;\n if (typeof cfg === 'function') {\n try {\n const resolved = cfg(this._container);\n if (resolved instanceof HTMLElement) return resolved;\n } catch (err) {\n console.warn('[AlpineFlow] fullscreenTarget resolver threw:', err);\n }\n }\n return this._container;\n },\n\n /**\n * Toggle fullscreen on the canvas container (or the configured\n * `fullscreenTarget`). Requests fullscreen when not active, exits when\n * active. Warns and no-ops if the browser doesn't expose\n * `requestFullscreen` (e.g., restricted iframes).\n */\n toggleFullscreen(): void {\n if (!this._container) return;\n if (typeof document === 'undefined') return;\n const target = this._resolveFullscreenTarget();\n if (!target) return;\n if (document.fullscreenElement) {\n // Any fullscreen session — exit it. Works whether it's ours or not.\n document.exitFullscreen?.();\n return;\n }\n const req = target.requestFullscreen;\n if (typeof req !== 'function') {\n console.warn('[AlpineFlow] requestFullscreen is not available in this context');\n return;\n }\n this._fullscreenTarget = target;\n Promise.resolve(req.call(target)).catch((err: unknown) => {\n console.warn('[AlpineFlow] fullscreen request rejected:', err);\n this._fullscreenTarget = null;\n });\n },\n\n /** Selection box/lasso setup (pointerdown/pointermove/pointerup handlers). */\n _initSelection() {\n this._selectionBox = createSelectionBox(this._container);\n this._lasso = createLasso(this._container);\n this._selectionTool = config.selectionTool ?? 'box';\n\n this._onSelectionPointerDown = (e: PointerEvent) => {\n if (!this._config.selectionOnDrag && !this._touchSelectionMode && !matchesModifier(e, this._shortcuts.selectionBox)) {\n return;\n }\n\n const target = e.target as HTMLElement;\n if (target !== this._container && !target.classList.contains('flow-viewport')) {\n return;\n }\n\n e.stopPropagation();\n e.preventDefault();\n this._selectionShiftHeld = true;\n\n const configMode = this._config.selectionMode ?? 'partial';\n const toggleHeld = matchesModifier(e, this._shortcuts.selectionModeToggle);\n this._selectionEffectiveMode = toggleHeld\n ? (configMode === 'partial' ? 'full' : 'partial')\n : configMode;\n\n if (!this._container) return;\n const rect = this._container.getBoundingClientRect();\n const containerX = e.clientX - rect.left;\n const containerY = e.clientY - rect.top;\n\n if (this._selectionTool === 'lasso') {\n this._lasso!.start(containerX, containerY, this._selectionEffectiveMode);\n } else {\n this._selectionBox!.start(containerX, containerY, this._selectionEffectiveMode);\n }\n (e.target as HTMLElement).setPointerCapture(e.pointerId);\n };\n\n this._onSelectionPointerMove = (e: PointerEvent) => {\n const toolActive = this._selectionTool === 'lasso'\n ? this._lasso?.isActive()\n : this._selectionBox?.isActive();\n if (!toolActive) return;\n\n if (!this._container) return;\n const rect = this._container.getBoundingClientRect();\n const containerX = e.clientX - rect.left;\n const containerY = e.clientY - rect.top;\n\n if (this._selectionTool === 'lasso') {\n this._lasso!.update(containerX, containerY);\n } else {\n this._selectionBox!.update(containerX, containerY);\n }\n };\n\n this._onSelectionPointerUp = (e: PointerEvent) => {\n const toolActive = this._selectionTool === 'lasso'\n ? this._lasso?.isActive()\n : this._selectionBox?.isActive();\n if (!toolActive) return;\n\n (e.target as HTMLElement).releasePointerCapture(e.pointerId);\n this._suppressNextCanvasClick = true;\n\n const absNodes = toAbsoluteNodes(this.nodes, this._nodeMap, this._config.nodeOrigin);\n let hitNodes: FlowNode[];\n let hitEdgeIds: string[] = [];\n\n if (this._selectionTool === 'lasso') {\n const polygon = this._lasso!.end(this.viewport);\n if (!polygon) return;\n\n const hitAbsNodes = this._selectionEffectiveMode === 'full'\n ? getNodesFullyInPolygon(absNodes, polygon)\n : getNodesInPolygon(absNodes, polygon);\n const hitIds = new Set(hitAbsNodes.map((n: FlowNode) => n.id));\n hitNodes = this.nodes.filter((n: FlowNode) => hitIds.has(n.id));\n\n if (this._config.lassoSelectsEdges) {\n for (const edge of this.edges) {\n if (edge.hidden) continue;\n const pathEl = this._container?.querySelector(\n `[data-flow-edge-id=\"${CSS.escape(edge.id)}\"] path`\n ) as SVGPathElement | null;\n if (!pathEl) continue;\n const len = pathEl.getTotalLength();\n const samples = Math.max(10, Math.ceil(len / 20));\n let inside = 0;\n for (let s = 0; s <= samples; s++) {\n const pt = pathEl.getPointAtLength((s / samples) * len);\n if (pointInPolygon(pt.x, pt.y, polygon)) inside++;\n }\n const select = this._selectionEffectiveMode === 'full'\n ? inside === samples + 1\n : inside > 0;\n if (select) hitEdgeIds.push(edge.id);\n }\n }\n } else {\n const flowRect = this._selectionBox!.end(this.viewport);\n if (!flowRect) return;\n\n const hitAbsNodes = this._selectionEffectiveMode === 'full'\n ? getNodesFullyInRect(absNodes, flowRect, this._config.nodeOrigin)\n : getNodesInRect(absNodes, flowRect, this._config.nodeOrigin);\n const hitIds = new Set(hitAbsNodes.map((n: FlowNode) => n.id));\n hitNodes = this.nodes.filter((n: FlowNode) => hitIds.has(n.id));\n }\n\n if (!this._selectionShiftHeld) {\n this.deselectAll();\n }\n\n for (const node of hitNodes) {\n if (!isSelectable(node)) continue;\n if (node.hidden) continue;\n\n node.selected = true;\n this.selectedNodes.add(node.id);\n\n const nodeEl = this._container?.querySelector(`[data-flow-node-id=\"${CSS.escape(node.id)}\"]`);\n if (nodeEl) {\n nodeEl.classList.add('flow-node-selected');\n }\n }\n\n for (const edgeId of hitEdgeIds) {\n const edge = this.getEdge(edgeId);\n if (edge) {\n edge.selected = true;\n this.selectedEdges.add(edge.id);\n }\n }\n\n if (hitNodes.length > 0 || hitEdgeIds.length > 0) {\n this._emitSelectionChange();\n }\n\n this._selectionShiftHeld = false;\n };\n\n this._container.addEventListener('pointerdown', this._onSelectionPointerDown);\n this._container.addEventListener('pointermove', this._onSelectionPointerMove);\n this._container.addEventListener('pointerup', this._onSelectionPointerUp);\n },\n\n /** Drop zone drag/drop handlers if onDrop configured. */\n _initDropZone() {\n if (config.onDrop) {\n const mimeTypes = config.dropMimeTypes ?? ['application/alpineflow'];\n\n /**\n * Walk the elementsFromPoint stack (topmost DOM element first) inward\n * to find the deepest FlowNode element under the given client point.\n * This correctly handles nested containers (page → section → column):\n * the innermost positioned element is encountered first in the stack.\n */\n const findDeepestNodeAtPoint = (clientX: number, clientY: number): FlowNode | null => {\n const elements = document.elementsFromPoint(clientX, clientY);\n for (const el of elements) {\n const nodeEl = (el as HTMLElement).closest?.('[data-flow-node-id]') as HTMLElement | null;\n if (!nodeEl) { continue; }\n const id = nodeEl.getAttribute('data-flow-node-id');\n if (!id) { continue; }\n const node = this._nodeMap.get(id);\n if (node) { return node; }\n }\n return null;\n };\n\n this._onDropZoneDragOver = (e: DragEvent) => {\n if (!e.dataTransfer) { return; }\n // Only signal acceptance if at least one of our MIME types is present.\n const hasMatch = mimeTypes.some((m) => e.dataTransfer!.types.includes(m));\n if (!hasMatch) { return; }\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n this._container?.classList.add('flow-canvas-drag-over');\n };\n\n this._onDropZoneDragleave = (e: DragEvent) => {\n // Only remove the class when the pointer truly exits the container —\n // dragleave fires for every child boundary crossing, so check relatedTarget.\n if (!this._container) { return; }\n const related = e.relatedTarget as Node | null;\n if (related && this._container.contains(related)) { return; }\n this._container.classList.remove('flow-canvas-drag-over');\n };\n\n this._onDropZoneDrop = (e: DragEvent) => {\n e.preventDefault();\n this._container?.classList.remove('flow-canvas-drag-over');\n\n if (!e.dataTransfer || !config.onDrop) { return; }\n\n // Find which MIME type matched.\n let matchedMime: string | null = null;\n let raw: string | null = null;\n for (const mime of mimeTypes) {\n const value = e.dataTransfer.getData(mime);\n if (value) {\n matchedMime = mime;\n raw = value;\n break;\n }\n }\n if (!matchedMime || !raw) { return; }\n\n let data: any;\n try { data = JSON.parse(raw); } catch { data = raw; }\n\n if (!this._container) { return; }\n const position = screenToFlowPosition(\n e.clientX,\n e.clientY,\n this.viewport,\n this._container.getBoundingClientRect(),\n );\n\n const targetNode = findDeepestNodeAtPoint(e.clientX, e.clientY);\n\n const node = config.onDrop({ data, position, targetNode, mimeType: matchedMime });\n if (node) { this.addNodes(node, { center: true }); }\n };\n\n this._container.addEventListener('dragover', this._onDropZoneDragOver);\n this._container.addEventListener('dragleave', this._onDropZoneDragleave);\n this._container.addEventListener('drop', this._onDropZoneDrop);\n }\n },\n\n /**\n * Return the deepest FlowNode under the given client coordinates.\n * Uses document.elementsFromPoint and walks inward to find the first\n * element carrying a data-flow-node-id attribute.\n *\n * Useful for context menus, tooltips, and custom pointer interactions\n * beyond the built-in drop zone.\n */\n getNodeAtPoint(clientX: number, clientY: number): FlowNode | null {\n const elements = document.elementsFromPoint(clientX, clientY);\n for (const el of elements) {\n const nodeEl = (el as HTMLElement).closest?.('[data-flow-node-id]') as HTMLElement | null;\n if (!nodeEl) { continue; }\n const id = nodeEl.getAttribute('data-flow-node-id');\n if (!id) { continue; }\n const node = this._nodeMap.get(id);\n if (node) { return node; }\n }\n return null;\n },\n\n /**\n * Install per-property Alpine watchers on a container node's childLayout.\n *\n * Watches the six layout-affecting properties explicitly so that any\n * mutation — direct assignment or via wire-bridge — triggers a re-layout\n * through the existing safeLayoutChildren dedup (at most one call per\n * parent per frame). Unwatched props (e.g. custom user data on childLayout)\n * never cause spurious layouts.\n *\n * Uses Alpine.watch(getter, callback) — the same low-level primitive that\n * $watch delegates to — because $watch only accepts string expressions\n * evaluated in component scope, which can't address per-node sub-objects.\n */\n _installChildLayoutWatchers(node: FlowNode): void {\n if (!node.childLayout) return;\n\n // Defensive: tear down any pre-existing watchers for this node first.\n this._uninstallChildLayoutWatchers(node.id);\n\n const WATCHED_CHILD_LAYOUT_PROPS = [\n 'columns', 'gap', 'padding', 'headerHeight', 'direction', 'stretch',\n ] as const;\n const nodeId = node.id;\n const cleanups: Array<() => void> = [];\n for (const prop of WATCHED_CHILD_LAYOUT_PROPS) {\n const stop = Alpine.watch(\n () => (node.childLayout as any)?.[prop],\n () => { this._layoutDedup?.safeLayoutChildren(nodeId); },\n );\n cleanups.push(stop);\n }\n this._childLayoutCleanups.set(nodeId, cleanups);\n },\n\n _uninstallChildLayoutWatchers(nodeId: string): void {\n const cleanups = this._childLayoutCleanups.get(nodeId);\n if (!cleanups) return;\n for (const stop of cleanups) stop();\n this._childLayoutCleanups.delete(nodeId);\n },\n\n /** Create the shared ResizeObserver instance (A1). Called from _initChildLayout. */\n _resizeObserverInit(): void {\n if (typeof ResizeObserver === 'undefined') return; // SSR / very old browsers\n this._resizeObserver = new ResizeObserver((entries) => {\n const changedIds = new Set();\n for (const entry of entries) {\n const el = entry.target as HTMLElement;\n const nodeId = el.getAttribute('data-flow-node-id');\n if (!nodeId) continue;\n const node = this._nodeMap.get(nodeId);\n if (!node) continue;\n\n // Use border-box dimensions so `node.dimensions` matches the element's\n // visual footprint (padding + border included) — the same measurement\n // offsetWidth / offsetHeight produce, and what fitView and the minimap\n // expect. contentRect is the CONTENT BOX (excludes padding/border) and\n // would produce systematically under-sized bounds.\n const borderBox = entry.borderBoxSize?.[0];\n const rawWidth = borderBox ? borderBox.inlineSize : el.offsetWidth;\n const rawHeight = borderBox ? borderBox.blockSize : el.offsetHeight;\n\n // Skip viewport-culled elements (display:none produces 0×0).\n if (rawWidth === 0 && rawHeight === 0) continue;\n // Defense-in-depth: some browsers fire ResizeObserver for display:none\n // with non-zero size in specific cases. Check computed style.\n if (el.offsetParent === null && el.tagName !== 'BODY') continue;\n\n // Fixed-dim nodes: inline style is authoritative; don't write node.dimensions.\n if ((node as any).fixedDimensions === true) continue;\n\n // Round to integer — browser zoom produces fractional values, and the\n // 1px-threshold comparison needs stable integer values.\n const width = Math.round(rawWidth);\n const height = Math.round(rawHeight);\n\n // 1px threshold: skip micro-updates smaller than 1px on both axes.\n const current = node.dimensions;\n if (\n current\n && Math.abs((current.width ?? 0) - width) < 1\n && Math.abs((current.height ?? 0) - height) < 1\n ) {\n continue;\n }\n\n // A5: clamp to min/max bounds (fields may be undefined or partial).\n const clamped = clampDimensions(\n { width, height },\n (node as any).minDimensions,\n (node as any).maxDimensions,\n );\n node.dimensions = clamped;\n changedIds.add(nodeId);\n\n // Schedule parent re-layout (deduped per frame).\n if (node.parentId) {\n this._layoutDedup?.safeLayoutChildren(node.parentId);\n }\n }\n // Commit once per RO batch (not per entry) so multiple nodes resizing\n // in the same frame only trigger a single obstacle-grid rebuild.\n if (changedIds.size > 0) {\n this._commitNodeGeometry([...changedIds]);\n }\n });\n },\n\n /** Run initial child layouts for all layout parents. */\n _initChildLayout() {\n // Instantiate the layout dedup now that Alpine is ready and _container is set.\n this._layoutDedup = createLayoutDedup((parentId: string) => {\n this.layoutChildren(parentId);\n });\n\n // A1: Create the shared ResizeObserver now that Alpine + _nodeMap are ready.\n this._resizeObserverInit();\n\n // Wire bridge: detect $wire (Livewire) and activate bidirectional bridge\n if ((this as any).$wire) {\n const $wire = (this as any).$wire;\n if (config.wireEvents) {\n registerWireEvents(config, $wire, config.wireEvents);\n }\n const cleanupCommands = registerWireCommands(this, $wire);\n const cleanupCustom = registerCustomWireCommands(this, $wire);\n (this as any)._wireCleanup = () => { cleanupCommands(); cleanupCustom(); };\n debug('init', `wire bridge activated for \"${this._id}\"`);\n }\n\n debug('init', `flowCanvas \"${this._id}\" ready`);\n this._emit('init');\n this._recomputeChildValidation();\n\n // A3: Install childLayout property watchers for all initial container nodes.\n for (const node of this.nodes) {\n if (node.childLayout) {\n this._installChildLayoutWatchers(node);\n }\n }\n\n // Run initial child layout for all layout parents (bottom-up via recursion)\n for (const node of this.nodes) {\n if (node.childLayout && !node.parentId) {\n this.layoutChildren(node.id);\n }\n }\n // Also handle layout parents that are themselves children of non-layout parents\n for (const node of this.nodes) {\n if (node.childLayout && node.parentId) {\n const parent = this._nodeMap.get(node.parentId);\n if (!parent?.childLayout) {\n this.layoutChildren(node.id);\n }\n }\n }\n\n if (config.fitViewOnInit) {\n requestAnimationFrame(() => {\n this.fitView();\n });\n }\n\n // Populate the shared obstacle snapshot/grid once initial measurement\n // has settled, so avoidant/orthogonal edges have geometry to read on\n // their first route.\n this._commitNodeGeometry();\n },\n\n /** Call setup(canvas) on any addon that provides it. */\n _initAddons() {\n for (const [, addon] of getRegistry().entries()) {\n if (addon && typeof addon === 'object' && typeof addon.setup === 'function') {\n addon.setup(this);\n }\n }\n },\n\n /** Validate auto-layout dependency and start initial layout. */\n _initAutoLayout() {\n if (config.autoLayout) {\n const alg = config.autoLayout.algorithm;\n const registryKeys: Record = {\n dagre: 'layout:dagre',\n force: 'layout:force',\n hierarchy: 'layout:hierarchy',\n elk: 'layout:elk',\n };\n const pluginNames: Record = {\n dagre: 'AlpineFlowDagre',\n force: 'AlpineFlowForce',\n hierarchy: 'AlpineFlowHierarchy',\n elk: 'AlpineFlowElk',\n };\n const key = registryKeys[alg];\n if (key && getAddon(key)) {\n this._autoLayoutReady = true;\n this.$nextTick(() => this._runAutoLayout());\n } else if (key) {\n this._warn('AUTO_LAYOUT_MISSING_DEP', `autoLayout requires the ${alg} plugin. Register it with: Alpine.plugin(${pluginNames[alg]})`);\n }\n }\n },\n\n /** requestAnimationFrame ready flip, loading watch, loading overlay injection. */\n _initReady() {\n // Flip `ready` after the first paint so node dimensions are measured\n // and edges have accurate handle positions. When fitViewOnInit is set,\n // delay an extra frame so fitView completes before the viewport reveals.\n const frameCount = config.fitViewOnInit ? 2 : 1;\n let frame = 0;\n const tick = () => {\n frame++;\n if (frame < frameCount) {\n requestAnimationFrame(tick);\n return;\n }\n this.$nextTick(() => {\n this.ready = true;\n });\n };\n requestAnimationFrame(tick);\n\n // Toggle .flow-loading / .flow-ready CSS classes reactively\n this.$watch('isLoading', (loading: boolean) => {\n if (!this._container) return;\n this._container.classList.toggle('flow-loading', loading);\n this._container.classList.toggle('flow-ready', !loading);\n\n if (!loading && this._autoLoadingOverlay) {\n this._autoLoadingOverlay.remove();\n this._autoLoadingOverlay = null;\n }\n });\n if (this._container) {\n this._container.classList.add('flow-loading');\n }\n\n // Auto-inject loading overlay when loading: true and no x-flow-loading directive present\n if (config.loading && this._container && !this._container.querySelector('[data-flow-loading-directive]')) {\n const overlay = document.createElement('div');\n overlay.className = 'flow-loading-overlay';\n\n const loadingIndicator = document.createElement('div');\n loadingIndicator.className = 'flow-loading-indicator';\n\n const nodeEl = document.createElement('div');\n nodeEl.className = 'flow-loading-indicator-node';\n\n const textEl = document.createElement('div');\n textEl.className = 'flow-loading-indicator-text';\n textEl.textContent = this._loadingText;\n\n loadingIndicator.appendChild(nodeEl);\n loadingIndicator.appendChild(textEl);\n overlay.appendChild(loadingIndicator);\n this._container.appendChild(overlay);\n this._autoLoadingOverlay = overlay;\n }\n },\n\n // ── Lifecycle ─────────────────────────────────────────────────────\n init() {\n // Rebind ctx to the actual Alpine reactive proxy. In normal usage\n // (x-data=\"flowCanvas(...)\") this is the same object, but when users\n // spread ({...flowCanvas(...), extra}) Alpine wraps a different merged\n // object. Rebinding here ensures mixin closures mutate the correct\n // reactive proxy so Alpine detects changes.\n ctxTarget = this;\n\n this._initDebug();\n this._initContainer();\n this._initColorMode();\n this._initHydration();\n this._initHistory();\n this._initAnnouncer();\n this._initCollab();\n this._initPanZoom();\n this._initHandleDelegation();\n this._initClickHandlers();\n this._initKeyboard();\n this._initMinimap();\n this._initFullscreen();\n this._initControls();\n this._initSelection();\n this._initChildLayout();\n this._initAddons();\n this._initDropZone();\n this._initAutoLayout();\n this._initReady();\n },\n\n _setupMarkerDefs() {\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.style.cssText = 'position:absolute;width:0;height:0;overflow:hidden;';\n const defsEl = document.createElementNS('http://www.w3.org/2000/svg', 'defs');\n svg.appendChild(defsEl);\n this._container?.appendChild(svg);\n this._markerDefsEl = svg;\n this._updateMarkerDefs();\n\n this.$watch('edges', () => {\n this._updateMarkerDefs();\n });\n },\n\n _updateMarkerDefs() {\n if (!this._markerDefsEl) return;\n const defs = this._markerDefsEl.querySelector('defs')!;\n const seen = new Map();\n\n for (const edge of this.edges as FlowEdge[]) {\n for (const marker of [edge.markerStart, edge.markerEnd]) {\n if (!marker) continue;\n const cfg = normalizeMarker(marker);\n const id = getMarkerId(cfg, this._id);\n if (!seen.has(id)) {\n seen.set(id, getMarkerSvg(cfg, id));\n }\n }\n }\n\n // Remove stale markers, preserving non-marker children (gradients etc.)\n const existingMarkers = defs.querySelectorAll('marker');\n const existingIds = new Set();\n existingMarkers.forEach((m: SVGMarkerElement) => {\n if (!seen.has(m.id)) {\n m.remove();\n } else {\n existingIds.add(m.id);\n }\n });\n\n for (const [id, svgStr] of seen) {\n if (!existingIds.has(id)) {\n const parsed = new DOMParser().parseFromString(\n `${svgStr}`,\n 'image/svg+xml',\n );\n const marker = parsed.querySelector('marker');\n if (marker) defs.appendChild(document.importNode(marker, true));\n }\n }\n },\n\n destroy() {\n // Animations / particles / timelines. Lives in the animation mixin; called\n // from here because a mixin method named `destroy` would shadow THIS one.\n (this as any)._destroyAnimations?.();\n\n // Wire bridge cleanup\n (this as any)._wireCleanup?.();\n (this as any)._wireCleanup = null;\n\n // Alpine's nextTick runs off a global tickStack that component teardown does\n // NOT cancel. If we were destroyed in the same task init() queued the install\n // in (x-if flipping false, a Livewire morph, Alpine.destroyTree), the callback\n // still fires — this flag is what stops it installing a capture listener on\n // the viewport of a corpse.\n this._handleDelegationTornDown = true;\n this._handleDelegationCleanup?.();\n this._handleDelegationCleanup = null;\n this._handleDelegationEl = null;\n this._longPressCleanup?.();\n this._longPressCleanup = null;\n this._touchSelectionCleanup?.();\n this._touchSelectionCleanup = null;\n\n // Alpine invokes destroy() UNWRAPPED, inside cleanupAttributes() — there is no\n // try/catch around it. A throw escaping this method aborts Alpine's own cleanup\n // loop mid-way: the remaining directive undo()s never run and `_x_dataStack`\n // leaks. This `_emit` is the ONLY point in teardown that hands control to USER\n // code — `config.onDestroy` (invoked by _emit) and `config.formatAnnouncement`\n // (via the announcer inside _emit) — so it is the one boundary that has to be\n // sealed. A consumer's throwing callback must not corrupt Alpine's teardown.\n // (The DOM CustomEvent _emit dispatches is not a concern: dispatchEvent reports\n // listener exceptions to the global error handler rather than rethrowing.)\n try {\n this._emit('destroy');\n } catch (err) {\n console.error(\n `[AlpineFlow] a destroy callback threw while destroying flowCanvas \"${this._id}\"; teardown continued`,\n err,\n );\n }\n debug('destroy', `flowCanvas \"${this._id}\" destroying`);\n if (this._onCanvasClick && this._container) {\n this._container.removeEventListener('click', this._onCanvasClick);\n }\n if (this._onCanvasContextMenu && this._container) {\n this._container.removeEventListener('contextmenu', this._onCanvasContextMenu);\n }\n if (this._container) {\n for (const entry of this._contextMenuListeners) {\n this._container.removeEventListener(entry.event, entry.handler);\n }\n }\n this._contextMenuListeners = [];\n if (this._onKeyDown) {\n document.removeEventListener('keydown', this._onKeyDown);\n }\n if (this._onContainerPointerDown && this._container) {\n this._container.removeEventListener('pointerdown', this._onContainerPointerDown);\n }\n this._markerDefsEl?.remove();\n this._markerDefsEl = null;\n this._minimap?.destroy();\n this._minimap = null;\n this._controls?.destroy();\n this._controls = null;\n if (this._onFullscreenChange && typeof document !== 'undefined') {\n document.removeEventListener('fullscreenchange', this._onFullscreenChange);\n }\n this._onFullscreenChange = null;\n // Exit fullscreen if this canvas was holding it (either the container\n // or a configured fullscreenTarget wrapper).\n if (typeof document !== 'undefined') {\n const held = document.fullscreenElement;\n if (held && (held === this._container || held === this._fullscreenTarget)) {\n document.exitFullscreen?.().catch(() => {});\n }\n }\n this._fullscreenTarget = null;\n if (this._onSelectionPointerDown && this._container) {\n this._container.removeEventListener('pointerdown', this._onSelectionPointerDown);\n }\n if (this._onSelectionPointerMove && this._container) {\n this._container.removeEventListener('pointermove', this._onSelectionPointerMove);\n }\n if (this._onSelectionPointerUp && this._container) {\n this._container.removeEventListener('pointerup', this._onSelectionPointerUp);\n }\n this._selectionBox?.destroy();\n this._selectionBox = null;\n this._lasso?.destroy();\n this._lasso = null;\n this._viewportEl = null;\n if (this._container) {\n this._container.removeEventListener('dragover', this._onDropZoneDragOver);\n this._container.removeEventListener('dragleave', this._onDropZoneDragleave);\n this._container.removeEventListener('drop', this._onDropZoneDrop);\n }\n // Clean up follow tracking\n this._followHandle?.stop();\n this._followHandle = null;\n\n // Animator/timeline/particle teardown already ran at the top of this method,\n // in `_destroyAnimations()` (stopAll + destroyParticles + stop-and-clear every\n // active timeline). Only the reference drop is left to do here.\n this._animator = null;\n\n // Cancel pending layout animation frame\n if (this._layoutAnimFrame) {\n cancelAnimationFrame(this._layoutAnimFrame);\n this._layoutAnimFrame = 0;\n }\n\n // Clean up auto-layout timer\n if (this._autoLayoutTimer) {\n clearTimeout(this._autoLayoutTimer);\n this._autoLayoutTimer = null;\n }\n\n // Clean up color mode\n if (this._colorModeHandle) {\n this._colorModeHandle.destroy();\n this._colorModeHandle = null;\n }\n\n // Collab cleanup — objects live in the module-scoped collabStore WeakMap.\n //\n // OWNERSHIP RULE: the canvas disposes only what the canvas CONSTRUCTED —\n // the bridge, the awareness instance and the cursor layer. It deliberately\n // does NOT destroy `config.collab.provider`: the app constructed that and\n // passed it in, and nothing stops it sharing one provider across several\n // canvases. Destroying it here would kill collaboration on every OTHER\n // canvas still mounted on the same provider. If a caller wants the provider\n // torn down with the canvas, it destroys it itself.\n if (this._container) {\n const collabEntry = collabStore.get(this._container);\n if (collabEntry) {\n collabEntry.bridge.destroy();\n collabEntry.awareness.destroy();\n if (collabEntry.cursorCleanup) collabEntry.cursorCleanup();\n collabStore.delete(this._container);\n }\n }\n\n if (this._container) {\n this._container.removeAttribute('data-flow-canvas');\n }\n (this as any).$store.flow.unregister(this._id);\n if (this._vpFrame !== null) {\n cancelAnimationFrame(this._vpFrame);\n this._vpFrame = null;\n }\n this._panZoom?.destroy();\n this._panZoom = null;\n this._announcer?.destroy();\n this._announcer = null;\n if (this._computeDebounceTimer) { clearTimeout(this._computeDebounceTimer); this._computeDebounceTimer = null; }\n\n // Stop all childLayout watchers\n for (const nodeId of [...this._childLayoutCleanups.keys()]) {\n this._uninstallChildLayoutWatchers(nodeId);\n }\n\n // Disconnect shared ResizeObserver before disposing dedup (callback must\n // not fire safeLayoutChildren after dedup is gone).\n this._resizeObserver?.disconnect();\n this._resizeObserver = null;\n\n // Dispose layout dedup RAF handle\n this._layoutDedup?.dispose();\n this._layoutDedup = null;\n },\n\n // ── Remaining Flat Methods ────────────────────────────────────────\n\n /**\n * Set a node's rotation angle in degrees.\n */\n rotateNode(id: string, angle: number) {\n const node = this.nodes.find((n: FlowNode) => n.id === id);\n if (!node) return;\n this._captureHistory();\n node.rotation = angle;\n },\n\n /** Set the user-controlled loading state. */\n setLoading(value: boolean) {\n this._userLoading = value;\n },\n\n /** Update runtime config options. */\n patchConfig(changes: Partial) {\n this._applyConfigPatch(changes as Record);\n },\n\n // ── Context Menu ──────────────────────────────────────────────────\n closeContextMenu() {\n this.contextMenu.show = false;\n this.contextMenu.type = null;\n this.contextMenu.node = null;\n this.contextMenu.edge = null;\n this.contextMenu.position = null;\n this.contextMenu.nodes = null;\n this.contextMenu.event = null;\n },\n\n /**\n * Batch multiple canvas mutations so that layout reconciliation runs once\n * after the whole block rather than once per mutation. Nested calls join\n * the outermost batch. fn's return value is forwarded; layout still runs\n * even if fn throws.\n */\n batch(fn: () => T): T {\n if (!this._layoutDedup) return fn();\n return createBatch(this._layoutDedup)(fn);\n },\n\n get collab() {\n return this._container ? collabStore.get(this._container)?.awareness : undefined;\n },\n\n async toImage(options?: import('../../core/types').ToImageOptions): Promise {\n let captureFlowImage;\n try {\n ({ captureFlowImage } = await import('../../core/export'));\n } catch {\n throw new Error('toImage() requires html-to-image. Install it with: npm install html-to-image');\n }\n return captureFlowImage(\n this._container,\n this._viewportEl,\n this.nodes,\n this.viewport,\n options,\n );\n },\n };\n\n // ── Wire mixin methods flat onto the data object ──────────────────────\n // Object.defineProperties preserves getter/setter descriptors (e.g.\n // canUndo, canRedo, colorMode) that Object.assign would evaluate eagerly.\n //\n // ctx is a thin Proxy that delegates to ctxTarget. During construction\n // ctxTarget is `self`. In init(), it's rebound to `this` — the actual\n // Alpine reactive proxy. This handles the spread pattern\n // ({...flowCanvas(...), extra}) where Alpine wraps a different merged\n // object: mixin closures all share the same ctx Proxy, so rebinding\n // ctxTarget in init() makes every mixin method transparently use the\n // correct reactive proxy.\n let ctxTarget: any = self;\n const ctx = new Proxy(Object.create(null), {\n get(_: any, prop: string | symbol) { return ctxTarget[prop]; },\n set(_: any, prop: string | symbol, value: any) { ctxTarget[prop] = value; return true; },\n }) as unknown as CanvasContext;\n const mixins = [\n createNodesMixin(ctx),\n createEdgesMixin(ctx),\n createViewportMixin(ctx),\n createSelectionMixin(ctx),\n createHistoryMixin(ctx),\n createAnimationMixin(ctx),\n createCollapseMixin(ctx),\n createCondenseMixin(ctx),\n createRowsMixin(ctx),\n createLayoutMixin(ctx),\n createValidationMixin(ctx),\n createComputeMixin(ctx),\n createDomMixin(ctx, Alpine),\n createConfigMixin(ctx),\n ];\n for (const mixin of mixins) {\n Object.defineProperties(self, Object.getOwnPropertyDescriptors(mixin));\n }\n\n // Expose registerMarker on the instance so it's available via $flow\n (self as any).registerMarker = (type: string, renderer: CustomMarkerRenderer) => {\n registerMarker(type, renderer);\n };\n\n return self;\n });\n}\n","// ============================================================================\n// Drag Controller\n//\n// Wraps d3-drag for node dragging. Reworked from @xyflow/system XYDrag\n// (MIT License, Copyright 2019-2025 webkid GmbH) to use callbacks instead of\n// store mutations. Calculates snapped positions and multi-selection drag.\n// ============================================================================\n\nimport { drag, type DragBehavior } from 'd3-drag';\nimport { select, type Selection } from 'd3-selection';\nimport type { XYPosition, Viewport } from './types';\n\nexport interface DragOptions {\n /** Called when a drag gesture starts */\n onDragStart?: (event: DragStartEvent) => void;\n /** Called during dragging with the new position */\n onDrag?: (event: DragMoveEvent) => void;\n /** Called when a drag gesture ends */\n onDragEnd?: (event: DragEndEvent) => void;\n /** Get the current viewport for coordinate conversion */\n getViewport: () => Viewport;\n /** Get the current node position in flow coordinates (for offset-preserving drag) */\n getNodePosition: () => XYPosition;\n /** Snap to grid: false or [gridX, gridY] */\n snapToGrid?: false | [number, number];\n /** CSS selector to restrict drag initiation. At event time, the dragged\n * element is queried for a descendant matching this selector; if found,\n * only pointer events originating within that descendant start a drag.\n * When no match exists the drag is unrestricted (whole element). */\n filterSelector?: string;\n /** Fixed container for d3 pointer coordinates (prevents viewport transform from affecting drag) */\n container?: HTMLElement;\n /** Return true to block drag initiation (e.g. during animation lock). */\n isLocked?: () => boolean;\n /** CSS class name that prevents drag when on or inside event target. */\n noDragClassName?: string;\n /** Minimum pixel distance before drag starts. Default: 0 */\n dragThreshold?: number;\n}\n\nexport interface DragStartEvent {\n nodeId: string;\n position: XYPosition;\n sourceEvent: MouseEvent | TouchEvent;\n}\n\nexport interface DragMoveEvent {\n nodeId: string;\n position: XYPosition;\n delta: XYPosition;\n sourceEvent: MouseEvent | TouchEvent;\n}\n\nexport interface DragEndEvent {\n nodeId: string;\n position: XYPosition;\n sourceEvent: MouseEvent | TouchEvent;\n}\n\nexport interface DragInstance {\n /** Clean up d3 drag listeners */\n destroy(): void;\n}\n\n/**\n * Snap a position to the nearest grid point.\n */\nfunction snapPosition(position: XYPosition, grid: [number, number]): XYPosition {\n return {\n x: grid[0] * Math.round(position.x / grid[0]),\n y: grid[1] * Math.round(position.y / grid[1]),\n };\n}\n\n/**\n * Make a node element draggable.\n *\n * @example\n * ```ts\n * const dragger = createDrag(nodeEl, 'node-1', {\n * getViewport: () => canvas.viewport,\n * getNodePosition: () => canvas.getNode('node-1').position,\n * onDrag: ({ nodeId, position }) => {\n * canvas.getNode(nodeId).position = position;\n * },\n * });\n * ```\n */\nexport function createDrag(\n element: HTMLElement,\n nodeId: string,\n options: DragOptions,\n): DragInstance {\n const { onDragStart, onDrag, onDragEnd, getViewport, getNodePosition, snapToGrid = false, filterSelector, container, isLocked, noDragClassName, dragThreshold = 0 } = options;\n\n let startPosition: XYPosition = { x: 0, y: 0 };\n\n /** Convert a d3 drag event position to flow coordinates. */\n function eventToFlowPosition(event: { x: number; y: number }): XYPosition {\n const viewport = getViewport();\n return {\n x: (event.x - viewport.x) / viewport.zoom,\n y: (event.y - viewport.y) / viewport.zoom,\n };\n }\n\n const sel: Selection = select(element);\n\n const dragBehavior: DragBehavior = drag()\n // Tell d3 where the dragged element actually is (in screen coordinates)\n // so the node doesn't jump to the cursor on click.\n .subject(() => {\n const viewport = getViewport();\n const nodePos = getNodePosition();\n return {\n x: nodePos.x * viewport.zoom + viewport.x,\n y: nodePos.y * viewport.zoom + viewport.y,\n };\n })\n .on('start', (event) => {\n startPosition = eventToFlowPosition(event);\n onDragStart?.({ nodeId, position: startPosition, sourceEvent: event.sourceEvent });\n })\n .on('drag', (event) => {\n let position = eventToFlowPosition(event);\n\n if (snapToGrid) {\n position = snapPosition(position, snapToGrid);\n }\n\n const delta: XYPosition = {\n x: position.x - startPosition.x,\n y: position.y - startPosition.y,\n };\n\n onDrag?.({ nodeId, position, delta, sourceEvent: event.sourceEvent });\n })\n .on('end', (event) => {\n let position = eventToFlowPosition(event);\n\n if (snapToGrid) {\n position = snapPosition(position, snapToGrid);\n }\n\n onDragEnd?.({ nodeId, position, sourceEvent: event.sourceEvent });\n });\n\n if (container) {\n dragBehavior.container(() => container);\n }\n\n if (dragThreshold > 0) {\n dragBehavior.clickDistance(dragThreshold);\n }\n\n dragBehavior.filter((event: any) => {\n if (isLocked?.()) return false;\n if (noDragClassName && (event.target as HTMLElement)?.closest?.('.' + noDragClassName)) return false;\n if (filterSelector) {\n const handle = element.querySelector(filterSelector);\n return handle ? handle.contains(event.target as Node) : true;\n }\n return true;\n });\n\n sel.call(dragBehavior);\n\n return {\n destroy() {\n sel.on('.drag', null);\n },\n };\n}\n","import type { FlowNode } from './types';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT, getNodeVisualPosition } from './geometry';\n\nexport interface NodeBox {\n id: string;\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface HelperLineResult {\n /** Horizontal guide Y positions */\n horizontal: number[];\n /** Vertical guide X positions */\n vertical: number[];\n /** Snap offset to apply to dragged position */\n snapOffset: { x: number; y: number };\n}\n\n/**\n * Get bounding box for a node.\n */\nexport function nodeToBox(\n node: FlowNode,\n globalOrigin?: [number, number],\n): NodeBox {\n const vp = getNodeVisualPosition(node, globalOrigin);\n return {\n id: node.id,\n x: vp.x,\n y: vp.y,\n width: node.dimensions?.width ?? DEFAULT_NODE_WIDTH,\n height: node.dimensions?.height ?? DEFAULT_NODE_HEIGHT,\n };\n}\n\n/**\n * Compute alignment guides between a dragged box and other boxes.\n *\n * Checks 5 horizontal alignments (top-top, bottom-bottom, center-center, top-bottom, bottom-top)\n * and 5 vertical alignments (left-left, right-right, center-center, left-right, right-left).\n *\n * Returns deduplicated guide positions and the snap offset for the closest alignment per axis.\n */\nexport function computeHelperLines(\n dragged: NodeBox,\n others: NodeBox[],\n threshold: number,\n): HelperLineResult {\n const horizontal: Set = new Set();\n const vertical: Set = new Set();\n let snapX = 0;\n let snapY = 0;\n let closestDx = Infinity;\n let closestDy = Infinity;\n\n const dragCenterX = dragged.x + dragged.width / 2;\n const dragCenterY = dragged.y + dragged.height / 2;\n const dragRight = dragged.x + dragged.width;\n const dragBottom = dragged.y + dragged.height;\n\n for (const other of others) {\n const otherCenterX = other.x + other.width / 2;\n const otherCenterY = other.y + other.height / 2;\n const otherRight = other.x + other.width;\n const otherBottom = other.y + other.height;\n\n // ── Vertical alignments (X axis) ────────────────────────────\n const vChecks: [number, number][] = [\n [dragged.x, other.x], // left-left\n [dragRight, otherRight], // right-right\n [dragCenterX, otherCenterX], // center-center\n [dragged.x, otherRight], // left-right\n [dragRight, other.x], // right-left\n ];\n\n for (const [dragVal, otherVal] of vChecks) {\n const dx = otherVal - dragVal;\n if (Math.abs(dx) <= threshold) {\n vertical.add(otherVal);\n if (Math.abs(dx) < Math.abs(closestDx)) {\n closestDx = dx;\n snapX = dx;\n }\n }\n }\n\n // ── Horizontal alignments (Y axis) ──────────────────────────\n const hChecks: [number, number][] = [\n [dragged.y, other.y], // top-top\n [dragBottom, otherBottom], // bottom-bottom\n [dragCenterY, otherCenterY], // center-center\n [dragged.y, otherBottom], // top-bottom\n [dragBottom, other.y], // bottom-top\n ];\n\n for (const [dragVal, otherVal] of hChecks) {\n const dy = otherVal - dragVal;\n if (Math.abs(dy) <= threshold) {\n horizontal.add(otherVal);\n if (Math.abs(dy) < Math.abs(closestDy)) {\n closestDy = dy;\n snapY = dy;\n }\n }\n }\n }\n\n return {\n horizontal: [...horizontal],\n vertical: [...vertical],\n snapOffset: { x: snapX, y: snapY },\n };\n}\n","import type { XYPosition } from './types';\n\ninterface NodeCenter {\n id: string;\n center: XYPosition;\n}\n\nexport interface ProximityCandidate {\n source: string;\n target: string;\n targetId: string;\n distance: number;\n targetCenter: XYPosition;\n}\n\n/**\n * Infer source/target direction from two node positions.\n * Node further left = source. If X is similar (within 30px), node further up = source.\n */\nexport function inferDirection(\n centerA: XYPosition,\n centerB: XYPosition,\n idA: string,\n idB: string,\n): { source: string; target: string } {\n const dx = Math.abs(centerA.x - centerB.x);\n if (dx > 30) {\n return centerA.x < centerB.x\n ? { source: idA, target: idB }\n : { source: idB, target: idA };\n }\n return centerA.y < centerB.y\n ? { source: idA, target: idB }\n : { source: idB, target: idA };\n}\n\n/**\n * Find the closest node to the dragged node within the proximity threshold.\n */\nexport function findProximityCandidate(\n draggedId: string,\n draggedCenter: XYPosition,\n allNodes: NodeCenter[],\n threshold: number,\n): ProximityCandidate | null {\n let closest: ProximityCandidate | null = null;\n let minDist = threshold;\n\n for (const node of allNodes) {\n if (node.id === draggedId) continue;\n const dist = Math.sqrt(\n (draggedCenter.x - node.center.x) ** 2 + (draggedCenter.y - node.center.y) ** 2,\n );\n if (dist < minDist) {\n minDist = dist;\n const { source, target } = inferDirection(draggedCenter, node.center, draggedId, node.id);\n closest = { source, target, targetId: node.id, distance: dist, targetCenter: node.center };\n }\n }\n\n return closest;\n}\n","// ============================================================================\n// x-flow-node Directive\n//\n// Binds a DOM element as a draggable flow node. Manages d3-drag lifecycle,\n// selection on click, and dispatches flow events. Respects per-node flags:\n// draggable (default true) and selectable (default true).\n//\n// Usage:
...
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport { createDrag, type DragInstance } from '../../core/drag';\nimport type { FlowNode, FlowEdge, Viewport } from '../../core/types';\nimport { createAutoPan, type AutoPanInstance } from '../../core/auto-pan';\nimport { debug } from '../../core/debug';\nimport type { CoordinateExtent } from '../../core/types';\nimport {\n getDescendantIds,\n computeZIndex,\n clampToExtent,\n clampToParent,\n clampRootNodePosition,\n expandParentToFitChild,\n} from '../../core/sub-flow';\nimport { computeHelperLines, nodeToBox, type NodeBox } from '../../core/helper-lines';\nimport { matchesModifier } from '../../core/keyboard-shortcuts';\nimport { getNodeRect, clampToAvoidOverlap } from '../../core/intersection';\nimport { DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from '../../core/geometry';\nimport { resolveChildValidation } from '../../core/child-validation';\nimport { collabStore } from '../../collab/store';\nimport { createConnectionLine, findSnapTarget, startConnectionAutoPan, type ConnectionLineInstance } from '../connection-utils';\nimport { applyValidationClasses, clearValidationClasses, checkHandleLimits, runHandleValidators, applyConnectValidation } from './flow-handle';\nimport { isValidConnection, checkConnectionRules } from '../../core/connections';\nimport { findProximityCandidate, type ProximityCandidate } from '../../core/proximity-connect';\nimport { isDraggable, isConnectable, isSelectable } from '../../core/node-flags';\n\nconst BLOCKED_ATTRS = new Set(['x-data', 'x-init', 'x-bind', 'href', 'src', 'action', 'formaction', 'srcdoc']);\n\nlet proximityEdgeCounter = 0;\n\n/**\n * Commit a deferred drag-history snapshot (taken on pointerdown) — but only if\n * the pointer actually moved (`didDrag`). d3-drag fires its `start` handler on\n * every pointerdown, so a plain click-to-select would otherwise push a no-op\n * undo entry; gating on `didDrag` keeps clicks out of the history stack.\n * `pendingSnapshot === null` means history was disabled at drag start.\n */\nexport function commitDragHistory(\n canvas: { _commitHistory?: (snapshot: string | null) => void },\n didDrag: boolean,\n pendingSnapshot: string | null,\n): void {\n if (didDrag && pendingSnapshot !== null) {\n canvas._commitHistory?.(pendingSnapshot);\n }\n}\n\n/**\n * Reparent a node during a drag with history suspended. `reparentNode` captures\n * its own snapshot (for programmatic callers), but inside a drag that would push\n * a second entry on top of the drag's deferred pre-drag snapshot — the exact\n * no-op/duplicate entry this pass eliminates. Suspending keeps `commitDragHistory`\n * as the single authoritative undo entry for the whole drag. try/finally mirrors\n * the suspend/resume convention in canvas-selection.ts so a throw can't leak the\n * suspend depth.\n */\nexport function reparentWithoutCapture(\n canvas: {\n _suspendHistory?: () => void;\n _resumeHistory?: () => void;\n reparentNode?: (nodeId: string, parentId: string | null) => boolean;\n },\n nodeId: string,\n parentId: string | null,\n): void {\n canvas._suspendHistory?.();\n try {\n canvas.reparentNode?.(nodeId, parentId);\n } finally {\n canvas._resumeHistory?.();\n }\n}\n\n/** Check if the easy-connect modifier key is held. */\nexport function isEasyConnectKey(\n e: PointerEvent | { altKey: boolean; metaKey: boolean; shiftKey: boolean },\n key: 'alt' | 'meta' | 'shift',\n): boolean {\n switch (key) {\n case 'alt': return e.altKey;\n case 'meta': return e.metaKey;\n case 'shift': return e.shiftKey;\n }\n}\n\n/** Find the nearest source handle to a screen coordinate within a node element. */\nexport function findNearestSourceHandle(\n nodeEl: HTMLElement,\n clientX: number,\n clientY: number,\n): HTMLElement | null {\n const handles = nodeEl.querySelectorAll('[data-flow-handle-type=\"source\"]');\n if (handles.length === 0) return null;\n\n let closest: HTMLElement | null = null;\n let minDist = Infinity;\n\n handles.forEach((h) => {\n const el = h as HTMLElement;\n const rect = el.getBoundingClientRect();\n if (rect.width === 0 && rect.height === 0) return;\n const cx = rect.left + rect.width / 2;\n const cy = rect.top + rect.height / 2;\n const dist = Math.sqrt((clientX - cx) ** 2 + (clientY - cy) ** 2);\n if (dist < minDist) {\n minDist = dist;\n closest = el;\n }\n });\n\n return closest;\n}\n\nfunction renderHelperLines(\n horizontal: number[],\n vertical: number[],\n nodeBoxes: NodeBox[],\n): SVGSVGElement {\n let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;\n for (const b of nodeBoxes) {\n minX = Math.min(minX, b.x);\n maxX = Math.max(maxX, b.x + b.width);\n minY = Math.min(minY, b.y);\n maxY = Math.max(maxY, b.y + b.height);\n }\n const pad = 50;\n\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.style.cssText = 'position:absolute;top:0;left:0;width:1px;height:1px;overflow:visible;pointer-events:none;z-index:500;';\n\n for (const y of horizontal) {\n const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', String(minX - pad));\n line.setAttribute('y1', String(y));\n line.setAttribute('x2', String(maxX + pad));\n line.setAttribute('y2', String(y));\n line.classList.add('flow-guide-path');\n svg.appendChild(line);\n }\n\n for (const x of vertical) {\n const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');\n line.setAttribute('x1', String(x));\n line.setAttribute('y1', String(minY - pad));\n line.setAttribute('x2', String(x));\n line.setAttribute('y2', String(maxY + pad));\n line.classList.add('flow-guide-path');\n svg.appendChild(line);\n }\n\n return svg;\n}\n\n\nexport function registerFlowNodeDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-node',\n (\n el,\n { expression },\n { evaluate, effect, cleanup },\n ) => {\n let dragInstance: DragInstance | null = null;\n let didDrag = false;\n // Snapshot taken on drag start; committed on drag end only if didDrag.\n let pendingDragSnapshot: string | null = null;\n let dragStartSelected = false;\n let groupDragStartPositions: Map | null = null;\n let autoPan: AutoPanInstance | null = null;\n let helperLinesSvg: SVGSVGElement | null = null;\n let proximityPreviewLine: ConnectionLineInstance | null = null;\n let proximityCandidate: ProximityCandidate | null = null;\n let dropTargetId: string | null = null;\n let reorderDragActive = false;\n let reorderExcludeId: string | null = null;\n let reorderLastIdx = -1;\n let reorderParentId: string | null = null;\n let contentInjected = false;\n let prevNodeClasses: string[] = [];\n let prevShapeClass = '';\n let prevStyleProps: string[] = [];\n let lastDragNodeId: string | null = null;\n\n // Lean position effect — split out of the main node effect below so that a\n // position change (which fires on every pointermove during a drag) re-runs\n // ONLY this tiny left/top write, not the full class/dimension/style/rotation\n // effect. Mirrors the lean `_flushNodePositions` fast-path the animation loop\n // already uses; fixes the drag sluggishness noted at the drag-recreate guard.\n effect(() => {\n if (!el.isConnected) return;\n const node = evaluate(expression) as FlowNode;\n if (!node || node.hidden) return;\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas?.viewport) return;\n const absPos = node.parentId\n ? canvas.getAbsolutePosition(node.id)\n : (node.position ?? { x: 0, y: 0 });\n const nodeOrig = node.nodeOrigin ?? canvas._config?.nodeOrigin ?? [0, 0];\n const nw = node.dimensions?.width ?? 150;\n const nh = node.dimensions?.height ?? 40;\n el.style.left = (absPos.x - nw * nodeOrig[0]) + 'px';\n el.style.top = (absPos.y - nh * nodeOrig[1]) + 'px';\n });\n\n effect(() => {\n // Bail if the node element was detached (e.g. a Livewire morph after a\n // non-renderless server action) before this reactive effect re-ran.\n // Evaluating the node scope or resolving the canvas via Alpine.$data on\n // a torn-down element throws (`_x_dataStack` of null). See issue #21.\n if (!el.isConnected) return;\n\n const node = evaluate(expression) as FlowNode;\n if (!node) return;\n\n // Store the node ID on the DOM element so child directives (handles) can read it\n el.dataset.flowNodeId = node.id;\n\n // Store the node type on the DOM element for CSS selectors\n if (node.type) {\n el.dataset.flowNodeType = node.type;\n }\n\n // One-time template injection from nodeTypes registry\n if (!contentInjected) {\n // Defense in depth: closest() can still return null if the canvas\n // container lost its [x-data] mid-morph — never call $data on null.\n const canvasContainer = el.closest('[x-data]') as HTMLElement | null;\n const canvasForTypes = canvasContainer ? Alpine.$data(canvasContainer) : null;\n let injected = false;\n\n // Try nodeTypes registry first\n if (canvasForTypes?._config?.nodeTypes) {\n const nodeType = node.type ?? 'default';\n const typeConfig = canvasForTypes._config.nodeTypes[nodeType]\n ?? canvasForTypes._config.nodeTypes['default'];\n\n if (typeof typeConfig === 'string') {\n const tpl = document.querySelector(typeConfig) as HTMLTemplateElement;\n if (tpl?.content) {\n el.appendChild(tpl.content.cloneNode(true));\n injected = true;\n }\n } else if (typeof typeConfig === 'function') {\n typeConfig(node, el);\n injected = true;\n }\n }\n\n // If no nodeTypes matched and element is empty, inject default template:\n // target handle + label + source handle\n if (!injected && el.children.length === 0) {\n const target = document.createElement('div');\n target.setAttribute('x-flow-handle:target', '');\n const label = document.createElement('span');\n label.setAttribute('x-text', 'node.data.label');\n const source = document.createElement('div');\n source.setAttribute('x-flow-handle:source', '');\n el.appendChild(target);\n el.appendChild(label);\n el.appendChild(source);\n injected = true;\n }\n\n if (injected) {\n for (const child of Array.from(el.children)) {\n Alpine.addScopeToNode(child as HTMLElement, { node });\n Alpine.initTree(child as HTMLElement);\n }\n }\n\n contentInjected = true;\n }\n\n // ── Hidden flag ──────────────────────────────────────────\n if (node.hidden) {\n el.classList.add('flow-node-hidden');\n el.removeAttribute('tabindex');\n el.removeAttribute('role');\n el.removeAttribute('aria-label');\n dragInstance?.destroy();\n dragInstance = null;\n return;\n }\n el.classList.remove('flow-node-hidden');\n\n // Only recreate drag when node identity changes — not on every\n // position update. Destroying/recreating d3-drag on each effect\n // re-run is expensive (event listener teardown + setup) and causes\n // drag sluggishness because position writes trigger the effect\n // on every mousemove.\n if (lastDragNodeId !== node.id) {\n dragInstance?.destroy();\n dragInstance = null;\n lastDragNodeId = node.id;\n }\n\n // Walk up to find the flowCanvas data context\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas?.viewport) return;\n\n // Auto-apply base class and group class\n // 'nopan' prevents d3-zoom from intercepting mousedowns on nodes\n el.classList.add('flow-node', 'nopan');\n if (node.type === 'group') {\n el.classList.add('flow-node-group');\n } else {\n el.classList.remove('flow-node-group');\n }\n\n // Position is written by the dedicated lean effect above (split out so a\n // drag's per-move position change doesn't re-run this whole effect).\n\n // Auto-apply dimensions when explicitly set.\n // Height is conditional: containers (childLayout), fixed-dim nodes, and\n // any node that is a parent (has child nodes via parentId) need inline\n // height for child positioning or consumer-opted fixed sizing.\n // Plain leaf nodes get NO inline height — content determines height, and\n // the A1 ResizeObserver captures the natural post-render height back\n // into node.dimensions.\n if (node.dimensions) {\n // Access childLayout and fixedDimensions at the top of the effect block so\n // Alpine tracks them reactively in case they change after mount.\n const _childLayout = node.childLayout;\n const _fixedDimensions = node.fixedDimensions;\n // Treat \"has children\" as an implicit container signal — group nodes\n // with positioned children need their dimensions to hold. Reads the\n // reactive parent→children index by key, so this effect re-runs only\n // when THIS node's own child list changes — not on every array-level\n // change (the previous `nodes.some(...)` scan subscribed every node\n // effect to the whole array).\n const _hasChildren = (canvas._childrenIds?.get(node.id)?.length ?? 0) > 0;\n el.style.width = node.dimensions.width + 'px';\n if (_childLayout || _fixedDimensions || _hasChildren) {\n el.style.height = node.dimensions.height + 'px';\n } else {\n el.style.height = ''; // leaf node: let content determine height\n }\n }\n\n // Apply selected class reactively\n if (canvas.selectedNodes.has(node.id)) {\n el.classList.add('flow-node-selected');\n } else {\n el.classList.remove('flow-node-selected');\n }\n el.setAttribute('aria-selected', String(!!node.selected));\n\n // Validation state: toggle .flow-node-invalid based on _validationErrors\n if (node._validationErrors && node._validationErrors.length > 0) {\n el.classList.add('flow-node-invalid');\n } else {\n el.classList.remove('flow-node-invalid');\n }\n\n // RunState CSS class toggle (D2)\n const RUN_STATE_CLASSES = ['flow-node-running', 'flow-node-completed', 'flow-node-failed', 'flow-node-skipped'] as const;\n const currentRunState = node.runState; // access for Alpine reactivity tracking\n for (const cls of RUN_STATE_CLASSES) {\n el.classList.remove(cls);\n }\n if (currentRunState && currentRunState !== 'pending') {\n el.classList.add(`flow-node-${currentRunState}`);\n }\n\n // Auto-apply node.class reactively (remove old, add new)\n for (const cls of prevNodeClasses) {\n el.classList.remove(cls);\n }\n const newClasses = node.class ? node.class.split(/\\s+/).filter(Boolean) : [];\n for (const cls of newClasses) {\n el.classList.add(cls);\n }\n prevNodeClasses = newClasses;\n\n // Apply shape class reactively\n const newShape = node.shape ? `flow-node-${node.shape}` : '';\n if (prevShapeClass !== newShape) {\n if (prevShapeClass) el.classList.remove(prevShapeClass);\n if (newShape) el.classList.add(newShape);\n prevShapeClass = newShape;\n }\n\n // Apply inline clip-path for custom (non-built-in) shapes\n const canvasData = Alpine.$data(el.closest('[data-flow-canvas]') as HTMLElement);\n const customShapeDef = node.shape && canvasData?._shapeRegistry?.[node.shape];\n if (customShapeDef?.clipPath) {\n el.style.clipPath = customShapeDef.clipPath;\n } else {\n el.style.clipPath = '';\n }\n\n if (node.style) {\n const style = typeof node.style === 'string'\n ? Object.fromEntries(node.style.split(';').filter(Boolean).map(s => s.split(':').map(p => p.trim())))\n : node.style as Record;\n const currentProps: string[] = [];\n for (const [prop, value] of Object.entries(style)) {\n if (prop && value) {\n el.style.setProperty(prop, value as string);\n currentProps.push(prop);\n }\n }\n for (const prop of prevStyleProps) {\n if (!currentProps.includes(prop)) {\n el.style.removeProperty(prop);\n }\n }\n prevStyleProps = currentProps;\n } else if (prevStyleProps.length > 0) {\n for (const prop of prevStyleProps) {\n el.style.removeProperty(prop);\n }\n prevStyleProps = [];\n }\n\n // Auto-apply rotation transform\n if (node.rotation) {\n el.style.transform = `rotate(${node.rotation}deg)`;\n el.style.transformOrigin = 'center';\n } else {\n el.style.transform = '';\n }\n\n // Keyboard focusability\n const focusable = node.focusable ?? (canvas._config?.nodesFocusable !== false);\n if (focusable) {\n el.setAttribute('tabindex', '0');\n el.setAttribute('role', node.ariaRole ?? 'group');\n el.setAttribute('aria-label', node.ariaLabel ?? (node.data?.label ? `Node: ${node.data.label}` : `Node ${node.id}`));\n } else {\n el.removeAttribute('tabindex');\n el.removeAttribute('role');\n el.removeAttribute('aria-label');\n }\n\n // Custom DOM attributes\n if (node.domAttributes) {\n for (const [key, val] of Object.entries(node.domAttributes)) {\n if (key.startsWith('on') || BLOCKED_ATTRS.has(key.toLowerCase())) continue;\n el.setAttribute(key, val);\n }\n }\n\n // Auto-apply connectable state class\n if (!isConnectable(node)) {\n el.classList.add('flow-node-no-connect');\n } else {\n el.classList.remove('flow-node-no-connect');\n }\n\n // Collapsed state\n if (node.collapsed) {\n el.classList.add('flow-node-collapsed');\n } else {\n el.classList.remove('flow-node-collapsed');\n }\n\n // Condensed state — re-measure dimensions after toggle\n const wasCondensed = el.classList.contains('flow-node-condensed');\n if (node.condensed) {\n el.classList.add('flow-node-condensed');\n } else {\n el.classList.remove('flow-node-condensed');\n }\n if (!!node.condensed !== wasCondensed) {\n requestAnimationFrame(() => {\n // Use offsetWidth/offsetHeight — unaffected by CSS transforms (rotation)\n node.dimensions = {\n width: el.offsetWidth,\n height: el.offsetHeight,\n };\n debug('condense', `Node \"${node.id}\" re-measured after condense toggle`, node.dimensions);\n });\n }\n\n // Filtered state — CSS-driven visibility (no early return)\n if (node.filtered) {\n el.classList.add('flow-node-filtered');\n } else {\n el.classList.remove('flow-node-filtered');\n }\n\n // Handle visibility mode\n const handlesMode = node.handles ?? 'visible';\n el.classList.remove('flow-handles-hidden', 'flow-handles-hover', 'flow-handles-select');\n if (handlesMode !== 'visible') {\n el.classList.add(`flow-handles-${handlesMode}`);\n }\n\n // z-index: CSS handles the common case (.flow-node: 2, .flow-node-group: 0).\n // Only set inline z-index for child nodes or explicit overrides so that\n // Alpine's :style string binding can't wipe the CSS default.\n let z = computeZIndex(node, canvas._nodeMap);\n const elevated = canvas._config?.elevateNodesOnSelect !== false && canvas.selectedNodes.has(node.id);\n if (elevated) {\n // Group nodes elevate to 1 (above other groups, below regular nodes).\n // Regular nodes get +1000 to float above everything else.\n z += node.type === 'group' ? Math.max(1 - z, 0) : 1000;\n }\n if (reorderDragActive) z += 1000;\n const cssDefault = node.type === 'group' ? 0 : 2;\n if (z !== cssDefault) {\n el.style.zIndex = String(z);\n } else {\n el.style.removeProperty('z-index');\n }\n\n // ── Draggable flag ─────────────────────────────────────────\n if (!isDraggable(node)) {\n el.classList.add('flow-node-locked');\n dragInstance?.destroy();\n dragInstance = null;\n return;\n }\n\n el.classList.remove('flow-node-locked');\n\n // Check for a drag handle child — if present, only that element initiates drag.\n // Note: on first render the child x-flow-drag-handle directive may not have\n // run yet, so the CSS class is also set by that directive as a fallback.\n const handleEl = el.querySelector('[data-flow-drag-handle]') as HTMLElement | null;\n if (handleEl) {\n el.classList.add('flow-node-has-handle');\n } else {\n el.classList.remove('flow-node-has-handle');\n }\n\n const containerEl = el.closest('.flow-container') as HTMLElement;\n\n // Reuse existing drag instance — only create when missing (first\n // render or after node identity change)\n if (dragInstance) return;\n\n dragInstance = createDrag(el, node.id, {\n container: containerEl ?? undefined,\n filterSelector: '[data-flow-drag-handle]',\n isLocked: () => canvas._animationLocked,\n noDragClassName: canvas._config?.noDragClassName ?? 'nodrag',\n dragThreshold: canvas._config?.nodeDragThreshold ?? 0,\n getViewport: () => canvas.viewport as Viewport,\n getNodePosition: () => {\n const n = canvas.getNode(node.id);\n if (!n) return { x: 0, y: 0 };\n return n.parentId\n ? canvas.getAbsolutePosition(n.id)\n : { x: n.position.x, y: n.position.y };\n },\n snapToGrid: canvas._config?.snapToGrid ?? false,\n\n onDragStart({ nodeId, position, sourceEvent }) {\n el.classList.add('flow-node-dragging');\n didDrag = false;\n dragStartSelected = false;\n groupDragStartPositions = null;\n // Mark node as dragging so collab bridge preserves local position\n const dragStartCollab = canvas._container ? collabStore.get(canvas._container) : undefined;\n if (dragStartCollab?.bridge) dragStartCollab.bridge.setDragging(nodeId, true);\n // Clean up any stale proximity state\n proximityPreviewLine?.destroy();\n proximityPreviewLine = null;\n proximityCandidate = null;\n // Clean up any stale drop target state\n if (dropTargetId && containerEl) {\n containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(dropTargetId)}\"]`)\n ?.classList.remove('flow-node-drop-target');\n }\n dropTargetId = null;\n // Defer history capture: snapshot now, commit on drag end only if\n // the node actually moved (a plain click must not push an entry).\n pendingDragSnapshot = canvas._snapshotHistory?.() ?? null;\n debug('drag', `Node \"${nodeId}\" drag start`, position);\n const n = canvas.getNode(nodeId);\n if (n) {\n // Auto-select dragged node if selectNodesOnDrag is enabled (default: true)\n if (canvas._config?.selectNodesOnDrag !== false && n.selectable !== false && !canvas.selectedNodes.has(nodeId)) {\n if (!matchesModifier(sourceEvent, canvas._shortcuts?.multiSelect)) {\n canvas.deselectAll();\n }\n canvas.selectedNodes.add(nodeId);\n n.selected = true;\n canvas._emitSelectionChange();\n dragStartSelected = true;\n }\n\n canvas._emit('node-drag-start', { node: n });\n\n // If this node is part of a multi-selection, record starting positions\n // Exclude descendants of the dragged node — they follow via relative positioning\n if (canvas.selectedNodes.has(nodeId) && canvas.selectedNodes.size > 1) {\n const descendantIds = getDescendantIds(nodeId, canvas.nodes);\n groupDragStartPositions = new Map();\n for (const selectedId of canvas.selectedNodes) {\n if (selectedId === nodeId || descendantIds.has(selectedId)) {\n continue;\n }\n const other = canvas.getNode(selectedId);\n if (other && other.draggable !== false) {\n groupDragStartPositions.set(selectedId, { x: other.position.x, y: other.position.y });\n }\n }\n }\n\n // Mark the dragged node(s) so edges touching them degrade to a\n // simplified bezier route for the gesture (avoidantSimplifyOnDrag,\n // WS-D). Mutate the REACTIVE set so key-scoped `.has()` deps on the\n // touched edges re-run. Group-drag members are included so their\n // edges degrade too.\n canvas._draggingNodeIds.add(nodeId);\n if (groupDragStartPositions) {\n for (const otherId of groupDragStartPositions.keys()) {\n canvas._draggingNodeIds.add(otherId);\n }\n }\n }\n\n // Start auto-pan if enabled\n if (canvas._config?.autoPanOnNodeDrag !== false && containerEl) {\n autoPan = createAutoPan({\n container: containerEl,\n speed: canvas._config?.autoPanSpeed ?? 15,\n onPan(dx, dy) {\n // Reactive `viewport` is frame-coalesced, but auto-pan drives\n // setViewport synchronously and must measure the delta applied\n // this tick. `_viewportLive` is updated synchronously by every\n // transform (setViewport fires it immediately), so read that.\n const liveVp = () => canvas._viewportLive ?? canvas.viewport;\n const zoom = liveVp().zoom || 1;\n\n // Capture viewport before pan so we can measure actual delta\n const vpBefore = { x: liveVp().x, y: liveVp().y };\n\n // Pan the viewport (negative because CSS translate decreases to scroll right/down)\n canvas._panZoom?.setViewport({\n x: liveVp().x - dx,\n y: liveVp().y - dy,\n zoom,\n });\n\n // Actual delta applied (may differ from requested if translateExtent clamped it)\n const actualDx = vpBefore.x - liveVp().x;\n const actualDy = vpBefore.y - liveVp().y;\n\n // If viewport didn't move at all, it's fully clamped by translateExtent\n const vpHitBoundary = (actualDx === 0 && actualDy === 0);\n\n // Compensate node position by actual viewport delta only\n const currentNode = canvas.getNode(nodeId);\n let nodeHitBoundary = false;\n if (currentNode) {\n const prevX = currentNode.position.x;\n const prevY = currentNode.position.y;\n currentNode.position.x += actualDx / zoom;\n currentNode.position.y += actualDy / zoom;\n\n // Clamp after auto-pan adjustment\n const apClamped = clampRootNodePosition(currentNode.position, currentNode, canvas._config?.nodeExtent);\n currentNode.position.x = apClamped.x;\n currentNode.position.y = apClamped.y;\n\n // Check if node position actually changed\n nodeHitBoundary = (currentNode.position.x === prevX && currentNode.position.y === prevY);\n }\n\n // Move other selected nodes by actual delta too\n if (groupDragStartPositions) {\n for (const [otherId] of groupDragStartPositions) {\n const other = canvas.getNode(otherId);\n if (other) {\n other.position.x += actualDx / zoom;\n other.position.y += actualDy / zoom;\n\n // Clamp after auto-pan adjustment\n const gpClamped = clampRootNodePosition(other.position, other, canvas._config?.nodeExtent);\n other.position.x = gpClamped.x;\n other.position.y = gpClamped.y;\n }\n }\n }\n\n // Stop auto-pan if both viewport and node are at boundary\n return vpHitBoundary && nodeHitBoundary;\n },\n });\n // Initialize pointer position from the mousedown event so auto-pan\n // doesn't start with (0,0) — which is at the container edge and\n // would trigger immediate unwanted panning.\n if (sourceEvent instanceof MouseEvent) {\n autoPan.updatePointer(sourceEvent.clientX, sourceEvent.clientY);\n }\n autoPan.start();\n }\n },\n\n onDrag({ nodeId, position, delta, sourceEvent }) {\n didDrag = true;\n const n = canvas.getNode(nodeId);\n if (n) {\n // For child nodes, convert absolute drag position to relative\n if (n.parentId) {\n const parentAbs = canvas.getAbsolutePosition(n.parentId);\n let relX = position.x - parentAbs.x;\n let relY = position.y - parentAbs.y;\n\n const childDims = n.dimensions ?? { width: 150, height: 50 };\n const parent = canvas.getNode(n.parentId);\n\n // ── Layout-parent reorder drag ────────────────────────────────\n if (parent?.childLayout) {\n if (!reorderDragActive) {\n el.classList.add('flow-reorder-dragging');\n reorderParentId = n.parentId!;\n }\n reorderDragActive = true;\n reorderExcludeId = nodeId;\n\n // Position the dragged node freely (it's \"lifted out\" of flow).\n // Nodes with extent: 'parent' stay clamped (locked to parent).\n const canEscapeParent = n.extent !== 'parent';\n n.position.x = position.x - parentAbs.x;\n n.position.y = position.y - parentAbs.y;\n if (!canEscapeParent && parent.dimensions) {\n const clamped = clampToParent({ x: n.position.x, y: n.position.y }, childDims, parent.dimensions);\n n.position.x = clamped.x;\n n.position.y = clamped.y;\n }\n\n const dragW = n.dimensions?.width ?? 150;\n const dragH = n.dimensions?.height ?? 50;\n\n // ── Drop-target detection during reorder ──────────────────────\n // Run BEFORE swap detection so reorderParentId is current.\n if (canEscapeParent) {\n const pw = parent.dimensions?.width ?? 150;\n const ph = parent.dimensions?.height ?? 50;\n const relCx = n.position.x + dragW / 2;\n const relCy = n.position.y + dragH / 2;\n // Hysteresis: require the center to be further inside to\n // *enter* a target than to *stay* inside. Prevents jitter\n // when the cursor hovers near a boundary.\n const HYSTERESIS = 12;\n // If already inside the original parent, use full bounds (easy to stay).\n // If re-entering, require center to be inset by HYSTERESIS.\n const pInset = reorderParentId === n.parentId ? 0 : HYSTERESIS;\n const insideParent = relCx >= pInset && relCx <= pw - pInset\n && relCy >= pInset && relCy <= ph - pInset;\n\n // Build ancestor set — always exclude when inside parent,\n // only exclude immediate parent when outside.\n const ancestorIds = new Set();\n let walkId: string | undefined = n.parentId;\n while (walkId) {\n ancestorIds.add(walkId);\n walkId = canvas.getNode(walkId)?.parentId;\n }\n\n const absCx = position.x + dragW / 2;\n const absCy = position.y + dragH / 2;\n const dragDescendants = getDescendantIds(n.id, canvas.nodes);\n let bestTarget: FlowNode | null = null;\n\n // When inside the original parent, exclude all ancestors\n // (can only target non-ancestor groups like siblings).\n // When outside, exclude only the immediate parent so the\n // node can reach ancestor levels (e.g. Step 1).\n // Also honour acceptsDrop predicates on targets.\n const droppables = canvas.nodes.filter(\n (o: FlowNode) => o.id !== n.id\n && (o.droppable || o.childLayout) && !o.hidden\n && !dragDescendants.has(o.id)\n && (insideParent ? !ancestorIds.has(o.id) : o.id !== n.parentId)\n && (!o.acceptsDrop || o.acceptsDrop(n)),\n );\n\n for (const d of droppables) {\n const dAbsPos = d.parentId ? canvas.getAbsolutePosition(d.id) : d.position;\n const dw = d.dimensions?.width ?? 150;\n const dh = d.dimensions?.height ?? 50;\n // Hysteresis: current target uses full bounds (easy to stay),\n // other targets require center inset by HYSTERESIS (harder to enter).\n const inset = d.id === dropTargetId ? 0 : HYSTERESIS;\n if (absCx >= dAbsPos.x + inset && absCx <= dAbsPos.x + dw - inset\n && absCy >= dAbsPos.y + inset && absCy <= dAbsPos.y + dh - inset) {\n bestTarget = d;\n }\n }\n\n const newTargetId = bestTarget?.id ?? null;\n if (newTargetId !== dropTargetId) {\n if (dropTargetId && containerEl) {\n containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(dropTargetId)}\"]`)\n ?.classList.remove('flow-node-drop-target');\n }\n if (newTargetId && containerEl) {\n containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(newTargetId)}\"]`)\n ?.classList.add('flow-node-drop-target');\n }\n dropTargetId = newTargetId;\n\n // ── Cross-parent reorder: switch active layout parent ──\n const newTarget = newTargetId ? canvas.getNode(newTargetId) : null;\n const prevReorderParent = reorderParentId;\n\n if (newTarget?.childLayout && newTargetId !== reorderParentId) {\n // Hovering over a new layout group — switch swap detection.\n // Old parent: omit dragged node entirely so it shrinks.\n if (prevReorderParent) {\n canvas.layoutChildren(prevReorderParent, { omitFromComputation: nodeId, shallow: true });\n canvas.propagateLayoutUp(prevReorderParent, { omitFromComputation: nodeId });\n }\n reorderParentId = newTargetId;\n // Assign order at end of new parent's sibling list\n const newSiblings = canvas.nodes\n .filter((s: FlowNode) => s.parentId === newTargetId && s.id !== nodeId)\n .sort((a: FlowNode, b: FlowNode) => (a.order ?? Infinity) - (b.order ?? Infinity));\n const insertIdx = newSiblings.length;\n const ordered: FlowNode[] = [...newSiblings];\n ordered.splice(insertIdx, 0, n);\n for (let i = 0; i < ordered.length; i++) {\n ordered[i].order = i;\n }\n reorderLastIdx = insertIdx;\n // New parent: include dragged node in computation (grows)\n // but don't apply its position (it's being dragged freely).\n // Reset dimensions to initial/default so stretched size from\n // the old parent doesn't inflate the new parent.\n const initDims = canvas._initialDimensions?.get(nodeId);\n const previewNode = { ...n, dimensions: initDims ? { ...initDims } : undefined };\n canvas.layoutChildren(newTargetId, { excludeId: nodeId, includeNode: previewNode, shallow: true });\n canvas.propagateLayoutUp(newTargetId, { includeNode: previewNode });\n } else if (insideParent && reorderParentId !== n.parentId) {\n // Re-entered the original parent\n if (prevReorderParent && prevReorderParent !== n.parentId) {\n canvas.layoutChildren(prevReorderParent, { omitFromComputation: nodeId, shallow: true });\n canvas.propagateLayoutUp(prevReorderParent, { omitFromComputation: nodeId });\n }\n reorderParentId = n.parentId!;\n reorderLastIdx = -1;\n } else if (!newTargetId && !insideParent) {\n // Outside all groups — free float, no layout\n if (prevReorderParent) {\n canvas.layoutChildren(prevReorderParent, { omitFromComputation: nodeId, shallow: true });\n canvas.propagateLayoutUp(prevReorderParent, { omitFromComputation: nodeId });\n }\n reorderParentId = null;\n reorderLastIdx = -1;\n }\n }\n }\n\n // ── Swap detection among siblings ─────────────────────────────\n // Only runs when hovering over a layout group. When\n // reorderParentId is null the node is free-floating.\n if (reorderParentId) {\n const activeReorderParent = canvas.getNode(reorderParentId);\n const activeLayout = activeReorderParent?.childLayout ?? parent.childLayout;\n const siblings = canvas.nodes\n .filter((s: FlowNode) => s.parentId === reorderParentId && s.id !== nodeId)\n .sort((a: FlowNode, b: FlowNode) => (a.order ?? Infinity) - (b.order ?? Infinity));\n\n // Compute drag position in the active reorder parent's space.\n // n.position is relative to the actual parent, so when the\n // active reorder parent differs we convert from absolute.\n let dragPosX: number, dragPosY: number;\n if (reorderParentId !== n.parentId) {\n const activeParentAbs = activeReorderParent?.parentId\n ? canvas.getAbsolutePosition(reorderParentId)\n : activeReorderParent?.position ?? { x: 0, y: 0 };\n dragPosX = position.x - activeParentAbs.x;\n dragPosY = position.y - activeParentAbs.y;\n } else {\n dragPosX = n.position.x;\n dragPosY = n.position.y;\n }\n\n // Find which sibling slot the dragged node should occupy.\n //\n // Swap detection inspired by SortableJS (MIT, RubaXa/owenm).\n // Uses directional thresholds with leading-edge comparison:\n // - Downward/rightward: dragged node's bottom/right edge vs\n // sibling's threshold point from the top/left.\n // - Upward/leftward: dragged node's top/left edge vs\n // sibling's inverted threshold point (from the bottom/right).\n // This ensures the threshold represents actual overlap fraction\n // regardless of the dragged node's size, and mirrors SortableJS's\n // inverted swap zones that prevent swap glitching.\n // See: https://github.com/SortableJS/Sortable/wiki/Swap-Thresholds-and-Direction\n const threshold = activeLayout!.swapThreshold ?? 0.5;\n\n // Initialise reorderLastIdx on first drag from the node's order\n // so we don't fire a redundant layout and so the directional\n // threshold inversion picks the right side from the start.\n if (reorderLastIdx === -1) {\n if (reorderParentId === n.parentId) {\n const myOrder = n.order ?? 0;\n reorderLastIdx = siblings.filter((s: FlowNode) => (s.order ?? 0) < myOrder).length;\n } else {\n // Entering a new parent — default to end slot\n reorderLastIdx = siblings.length;\n }\n }\n\n const curIdx = reorderLastIdx;\n let targetIdx = siblings.length; // default: end\n for (let i = 0; i < siblings.length; i++) {\n const sib = siblings[i];\n const sibW = sib.dimensions?.width ?? 150;\n const sibH = sib.dimensions?.height ?? 50;\n // Invert threshold for siblings above/left of the current slot\n // so both directions require the same penetration fraction.\n const t = i < curIdx ? (1 - threshold) : threshold;\n const swapY = sib.position.y + sibH * t;\n const swapX = sib.position.x + sibW * t;\n\n if (activeLayout!.direction === 'grid') {\n const dragCenter = {\n x: dragPosX + dragW / 2,\n y: dragPosY + dragH / 2,\n };\n const sibCenterY = sib.position.y + sibH / 2;\n if (dragCenter.y < sib.position.y) { targetIdx = i; break; }\n if (Math.abs(dragCenter.y - sibCenterY) < sibH / 2 && dragCenter.x < swapX) { targetIdx = i; break; }\n } else if (activeLayout!.direction === 'vertical') {\n // Use leading edge: top when moving up, bottom when moving down\n const edge = i < curIdx ? dragPosY : dragPosY + dragH;\n if (edge < swapY) { targetIdx = i; break; }\n } else {\n const edge = i < curIdx ? dragPosX : dragPosX + dragW;\n if (edge < swapX) { targetIdx = i; break; }\n }\n }\n\n // Only re-layout when the target slot actually changes\n if (targetIdx !== reorderLastIdx) {\n reorderLastIdx = targetIdx;\n\n // Reassign order values with the dragged node at targetIdx\n const ordered: FlowNode[] = [...siblings];\n ordered.splice(targetIdx, 0, n);\n for (let i = 0; i < ordered.length; i++) {\n ordered[i].order = i;\n }\n\n // Defer sibling layout to next frame so the dragged node stays\n // responsive — position writes don't block the current mousemove.\n const reorderContainer = el.closest('.flow-container') as HTMLElement;\n reorderContainer?.classList.add('flow-layout-animating');\n\n if (canvas._layoutAnimFrame) cancelAnimationFrame(canvas._layoutAnimFrame);\n const draggedId = n.id;\n const layoutParentId = reorderParentId;\n const isCrossParent = layoutParentId !== n.parentId;\n canvas._layoutAnimFrame = requestAnimationFrame(() => {\n if (isCrossParent && layoutParentId) {\n // Cross-parent: include dragged node in target parent's\n // computation (it grows), skip applying its position.\n // Use initial/default dimensions so stretched size from\n // the old parent doesn't inflate the new parent.\n const draggedNode = canvas.getNode(draggedId);\n let previewDrag: FlowNode | undefined;\n if (draggedNode) {\n const initD = canvas._initialDimensions?.get(draggedId);\n previewDrag = { ...draggedNode, dimensions: initD ? { ...initD } : undefined };\n }\n canvas.layoutChildren(layoutParentId, {\n excludeId: draggedId,\n includeNode: previewDrag,\n shallow: true,\n });\n canvas.propagateLayoutUp(layoutParentId, {\n includeNode: previewDrag,\n });\n } else {\n // Same-parent reorder: exclude dragged from position\n // but keep it in computation\n canvas.layoutChildren(layoutParentId, draggedId, true);\n }\n\n // Continue ticking edge re-measurement while CSS transitions run\n const animStart = performance.now();\n const animDuration = 300;\n const tickEdges = () => {\n canvas._layoutAnimTick++;\n if (performance.now() - animStart < animDuration) {\n canvas._layoutAnimFrame = requestAnimationFrame(tickEdges);\n } else {\n canvas._layoutAnimFrame = 0;\n }\n };\n canvas._layoutAnimFrame = requestAnimationFrame(tickEdges);\n });\n }\n }\n\n // Feed pointer position to auto-pan\n if (autoPan && sourceEvent instanceof MouseEvent) {\n autoPan.updatePointer(sourceEvent.clientX, sourceEvent.clientY);\n }\n\n return; // Skip normal child drag logic\n }\n\n // Clamp to parent bounds if extent === 'parent'\n if (n.extent === 'parent' && parent?.dimensions) {\n const clamped = clampToParent(\n { x: relX, y: relY },\n childDims,\n parent.dimensions,\n );\n relX = clamped.x;\n relY = clamped.y;\n } else if (Array.isArray(n.extent)) {\n // Per-node coordinate extent in local (relative) space\n const clamped = clampToExtent({ x: relX, y: relY }, n.extent as CoordinateExtent, childDims);\n relX = clamped.x;\n relY = clamped.y;\n }\n\n // preventChildEscape: clamp to parent bounds (stronger than extent: 'parent')\n if (!n.extent || n.extent !== 'parent') {\n const parentRules = resolveChildValidation(\n parent!,\n canvas._config?.childValidationRules ?? {},\n );\n const preventEscape = parentRules?.preventChildEscape || !!parent?.childLayout;\n if (preventEscape && parent?.dimensions) {\n const clamped = clampToParent(\n { x: relX, y: relY },\n childDims,\n parent.dimensions,\n );\n relX = clamped.x;\n relY = clamped.y;\n }\n }\n\n // Expand parent if expandParent is set\n if (n.expandParent && parent?.dimensions) {\n const expanded = expandParentToFitChild(\n { x: relX, y: relY },\n childDims,\n parent.dimensions,\n );\n if (expanded) {\n parent.dimensions.width = expanded.width;\n parent.dimensions.height = expanded.height;\n }\n }\n\n n.position.x = relX;\n n.position.y = relY;\n } else {\n // Root node: apply per-node or global nodeExtent\n const clamped = clampRootNodePosition(position, n, canvas._config?.nodeExtent);\n n.position.x = clamped.x;\n n.position.y = clamped.y;\n }\n\n // When snapping to grid, the position jumps between grid points.\n // Flush the node's CSS position and bump edge re-measurement so\n // edges update immediately on the snap frame.\n if (canvas._config?.snapToGrid) {\n const nodeOrig = n.nodeOrigin ?? canvas._config?.nodeOrigin ?? [0, 0];\n const nw = n.dimensions?.width ?? 150;\n const nh = n.dimensions?.height ?? 40;\n const absPos = n.parentId ? canvas.getAbsolutePosition(n.id) : n.position;\n el.style.left = (absPos.x - nw * nodeOrig[0]) + 'px';\n el.style.top = (absPos.y - nh * nodeOrig[1]) + 'px';\n canvas._layoutAnimTick++;\n }\n\n canvas._emit('node-drag', { node: n, position });\n\n // Move other selected nodes by the same delta\n if (groupDragStartPositions) {\n for (const [otherId, startPos] of groupDragStartPositions) {\n const other = canvas.getNode(otherId);\n if (other) {\n let newX = startPos.x + delta.x;\n let newY = startPos.y + delta.y;\n\n const otherClamped = clampRootNodePosition({ x: newX, y: newY }, other, canvas._config?.nodeExtent);\n other.position.x = otherClamped.x;\n other.position.y = otherClamped.y;\n }\n }\n }\n\n // ── Helper lines ──────────────────────────────────────\n const hlConfig = canvas._config?.helperLines;\n if (hlConfig) {\n const hlSnap = typeof hlConfig === 'object' ? (hlConfig.snap ?? true) : true;\n const hlThreshold = typeof hlConfig === 'object' ? (hlConfig.threshold ?? 5) : 5;\n\n // Resolve absolute position for a node (child nodes store relative positions)\n const absBox = (nd: FlowNode): NodeBox => {\n const pos = nd.parentId ? canvas.getAbsolutePosition(nd.id) : nd.position;\n return nodeToBox({ ...nd, position: pos }, canvas._config?.nodeOrigin);\n };\n\n // Build dragged box (single node or selection group bounding box)\n const draggedNodes = (canvas.selectedNodes.size > 1 && canvas.selectedNodes.has(nodeId))\n ? canvas.nodes.filter((nd: FlowNode) => canvas.selectedNodes.has(nd.id))\n : [n];\n const draggedBoxes = draggedNodes.map(absBox);\n const draggedBox: NodeBox = {\n id: nodeId,\n x: Math.min(...draggedBoxes.map((b: NodeBox) => b.x)),\n y: Math.min(...draggedBoxes.map((b: NodeBox) => b.y)),\n width: Math.max(...draggedBoxes.map((b: NodeBox) => b.x + b.width)) - Math.min(...draggedBoxes.map((b: NodeBox) => b.x)),\n height: Math.max(...draggedBoxes.map((b: NodeBox) => b.y + b.height)) - Math.min(...draggedBoxes.map((b: NodeBox) => b.y)),\n };\n\n // Other nodes (not in selection)\n const otherBoxes = (canvas.nodes as FlowNode[])\n .filter((nd: FlowNode) =>\n !canvas.selectedNodes.has(nd.id) &&\n nd.id !== nodeId &&\n nd.hidden !== true &&\n nd.filtered !== true\n )\n .map(absBox);\n\n const result = computeHelperLines(draggedBox, otherBoxes, hlThreshold);\n\n // Apply snap offset\n if (hlSnap && (result.snapOffset.x !== 0 || result.snapOffset.y !== 0)) {\n n.position.x += result.snapOffset.x;\n n.position.y += result.snapOffset.y;\n // Also offset group members\n if (groupDragStartPositions) {\n for (const [otherId] of groupDragStartPositions) {\n const other = canvas.getNode(otherId);\n if (other) {\n other.position.x += result.snapOffset.x;\n other.position.y += result.snapOffset.y;\n }\n }\n }\n }\n\n // Render guide lines\n helperLinesSvg?.remove();\n if (result.horizontal.length > 0 || result.vertical.length > 0) {\n const viewportEl = containerEl?.querySelector('.flow-viewport');\n if (viewportEl) {\n const allBoxes = (canvas.nodes as FlowNode[]).map(absBox);\n helperLinesSvg = renderHelperLines(result.horizontal, result.vertical, allBoxes);\n viewportEl.appendChild(helperLinesSvg);\n }\n } else {\n helperLinesSvg = null;\n }\n\n canvas._emit('helper-lines-change', {\n horizontal: result.horizontal,\n vertical: result.vertical,\n });\n }\n }\n\n // ── Collision prevention ──────────────────────────────────\n if (canvas._config?.preventOverlap) {\n const gap = typeof canvas._config.preventOverlap === 'number' ? canvas._config.preventOverlap : 5;\n const nw = n.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const nh = n.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n const selectedIds = canvas.selectedNodes as Set;\n const otherRects = canvas.nodes\n .filter((o: FlowNode) => o.id !== n.id && !o.hidden && !selectedIds.has(o.id))\n .map((o: FlowNode) => getNodeRect(o, canvas._config?.nodeOrigin));\n const clamped = clampToAvoidOverlap(n.position, nw, nh, otherRects, gap);\n n.position.x = clamped.x;\n n.position.y = clamped.y;\n }\n\n // ── Drop-target detection (reparent on drop) ──────────────────\n if (!n.parentId) {\n // Root node: check for droppable group targets (exclude own descendants)\n const dragDescendants = getDescendantIds(n.id, canvas.nodes);\n const droppables = canvas.nodes.filter(\n (o: FlowNode) => o.id !== n.id && o.droppable && !o.hidden\n && !dragDescendants.has(o.id)\n && (!o.acceptsDrop || o.acceptsDrop(n)),\n );\n const absRect = getNodeRect(n, canvas._config?.nodeOrigin);\n // When multiple droppables overlap, last-wins; topological order\n // puts children after parents, so inner groups win over outer groups.\n let bestTarget: FlowNode | null = null;\n const DROP_HYSTERESIS = 12;\n for (const d of droppables) {\n // Use absolute position for nested droppable groups\n const dAbsPos = d.parentId ? canvas.getAbsolutePosition(d.id) : d.position;\n const dw = d.dimensions?.width ?? DEFAULT_NODE_WIDTH;\n const dh = d.dimensions?.height ?? DEFAULT_NODE_HEIGHT;\n // Check if the dragged node's center is inside the droppable.\n // Hysteresis: current target uses full bounds, others require inset.\n const cx = absRect.x + absRect.width / 2;\n const cy = absRect.y + absRect.height / 2;\n const inset = d.id === dropTargetId ? 0 : DROP_HYSTERESIS;\n if (cx >= dAbsPos.x + inset && cx <= dAbsPos.x + dw - inset\n && cy >= dAbsPos.y + inset && cy <= dAbsPos.y + dh - inset) {\n bestTarget = d;\n }\n }\n\n const newTargetId = bestTarget?.id ?? null;\n if (newTargetId !== dropTargetId) {\n // Remove old highlight\n if (dropTargetId && containerEl) {\n const oldEl = containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(dropTargetId)}\"]`);\n oldEl?.classList.remove('flow-node-drop-target');\n }\n // Add new highlight\n if (newTargetId && containerEl) {\n const newEl = containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(newTargetId)}\"]`);\n newEl?.classList.add('flow-node-drop-target');\n }\n dropTargetId = newTargetId;\n }\n }\n\n // ── Proximity Connect preview ────────────────────────────────\n if (canvas._config?.proximityConnect) {\n const threshold = canvas._config.proximityConnectDistance ?? 150;\n const draggedDims = n.dimensions ?? { width: 150, height: 50 };\n const draggedCenter = {\n x: n.position.x + draggedDims.width / 2,\n y: n.position.y + draggedDims.height / 2,\n };\n\n const allCenters = canvas.nodes\n .filter((o: FlowNode) => o.id !== n.id && !o.hidden)\n .map((o: FlowNode) => ({\n id: o.id,\n center: {\n x: o.position.x + (o.dimensions?.width ?? 150) / 2,\n y: o.position.y + (o.dimensions?.height ?? 50) / 2,\n },\n }));\n\n const candidate = findProximityCandidate(n.id, draggedCenter, allCenters, threshold);\n\n if (candidate) {\n // Check if edge already exists between this pair\n const edgeExists = canvas.edges.some(\n (e: FlowEdge) =>\n (e.source === candidate.source && e.target === candidate.target) ||\n (e.source === candidate.target && e.target === candidate.source),\n );\n\n if (!edgeExists) {\n proximityCandidate = candidate;\n\n // Show/update preview line\n if (!proximityPreviewLine) {\n proximityPreviewLine = createConnectionLine({\n connectionLineType: canvas._config?.connectionLineType,\n connectionLineStyle: canvas._config?.connectionLineStyle,\n connectionLine: canvas._config?.connectionLine,\n });\n const viewportEl = containerEl?.querySelector('.flow-viewport');\n if (viewportEl) viewportEl.appendChild(proximityPreviewLine.svg);\n }\n\n proximityPreviewLine.update({\n fromX: draggedCenter.x, fromY: draggedCenter.y,\n toX: candidate.targetCenter.x, toY: candidate.targetCenter.y,\n source: candidate.source,\n });\n } else {\n proximityPreviewLine?.destroy();\n proximityPreviewLine = null;\n proximityCandidate = null;\n }\n } else {\n proximityPreviewLine?.destroy();\n proximityPreviewLine = null;\n proximityCandidate = null;\n }\n }\n\n // Sync position and cursor to collab peers\n const dragCollab = canvas._container ? collabStore.get(canvas._container) : undefined;\n if (dragCollab?.bridge) {\n dragCollab.bridge.pushLocalNodeUpdate(nodeId, { position: n.position });\n if (groupDragStartPositions) {\n for (const [otherId] of groupDragStartPositions) {\n const other = canvas.getNode(otherId);\n if (other) {\n dragCollab.bridge.pushLocalNodeUpdate(otherId, { position: other.position });\n }\n }\n }\n // Update cursor position during drag — the container's mousemove\n // listener doesn't fire while d3-drag captures pointer events.\n if (dragCollab.awareness && sourceEvent instanceof MouseEvent && canvas._container) {\n const rect = canvas._container.getBoundingClientRect();\n // Live viewport: reactive `viewport` lags a frame during auto-pan.\n const liveVp = canvas._viewportLive ?? canvas.viewport;\n const cx = (sourceEvent.clientX - rect.left - liveVp.x) / liveVp.zoom;\n const cy = (sourceEvent.clientY - rect.top - liveVp.y) / liveVp.zoom;\n dragCollab.awareness.updateCursor({ x: cx, y: cy });\n }\n }\n\n // Feed pointer position to auto-pan\n if (autoPan && sourceEvent instanceof MouseEvent) {\n autoPan.updatePointer(sourceEvent.clientX, sourceEvent.clientY);\n }\n },\n\n onDragEnd({ nodeId, position }) {\n // Capture moved node ids BEFORE groupDragStartPositions is nulled\n // below (both the reorder/reparent branch and the normal-drag\n // branch null it), so the obstacle-cache commit at the end of\n // each branch knows which nodes' geometry actually changed.\n const movedIds = groupDragStartPositions\n ? [nodeId, ...groupDragStartPositions.keys()]\n : [nodeId];\n\n // Gesture over: clear the dragging set so edges touching these nodes\n // re-route with real obstacles again. Cleared unconditionally (even a\n // click fired onDragStart). This mutation and the _commitNodeGeometry\n // commit later in each branch coalesce into ONE reactive flush, so\n // affected edges re-run exactly once with the final geometry.\n canvas._draggingNodeIds.clear();\n\n el.classList.remove('flow-node-dragging');\n debug('drag', `Node \"${nodeId}\" drag end`, position);\n\n // Unmark node as dragging so collab bridge resumes normal sync\n const dragEndCollab = canvas._container ? collabStore.get(canvas._container) : undefined;\n if (dragEndCollab?.bridge) dragEndCollab.bridge.setDragging(nodeId, false);\n\n // Stop auto-pan\n autoPan?.stop();\n autoPan = null;\n\n // Clean up helper lines\n helperLinesSvg?.remove();\n helperLinesSvg = null;\n if (canvas._config?.helperLines) {\n canvas._emit('helper-lines-change', { horizontal: [], vertical: [] });\n }\n\n const n = canvas.getNode(nodeId);\n if (n) {\n canvas._emit('node-drag-end', { node: n, position });\n }\n\n // ── Layout-parent reorder: snap to layout slot or reparent ───\n if (reorderDragActive && n?.parentId) {\n el.classList.remove('flow-reorder-dragging');\n const wasReorderParent = reorderParentId;\n reorderDragActive = false;\n reorderExcludeId = null;\n reorderLastIdx = -1;\n reorderParentId = null;\n\n // Stop animation tick loop and remove animation class before final\n // layout so DOM updates instantly and edges measure correct positions\n if (canvas._layoutAnimFrame) { cancelAnimationFrame(canvas._layoutAnimFrame); canvas._layoutAnimFrame = 0; }\n const reorderContainer = el.closest('.flow-container') as HTMLElement;\n reorderContainer?.classList.remove('flow-layout-animating');\n\n // If dragged over a drop target, reparent (layout runs inside reparentNode)\n if (dropTargetId) {\n if (containerEl) {\n containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(dropTargetId)}\"]`)\n ?.classList.remove('flow-node-drop-target');\n }\n reparentWithoutCapture(canvas, nodeId, dropTargetId);\n dropTargetId = null;\n } else if (wasReorderParent && wasReorderParent !== n.parentId) {\n // Was previewing in another group but dropped outside all\n // targets — fully remove from temp parent and snap back to own parent\n canvas.layoutChildren(wasReorderParent, { omitFromComputation: nodeId, shallow: true });\n canvas.propagateLayoutUp(wasReorderParent, { omitFromComputation: nodeId });\n canvas.layoutChildren(n.parentId);\n canvas._emit('child-reorder', {\n nodeId,\n parentId: n.parentId,\n order: n.order,\n });\n } else {\n // Snap back into layout slot (or was free-floating)\n canvas.layoutChildren(n.parentId);\n canvas._emit('child-reorder', {\n nodeId,\n parentId: n.parentId,\n order: n.order,\n });\n }\n\n groupDragStartPositions = null;\n // Reorder/reparent repositions siblings in the layout container.\n // Obstacle geometry for avoidant/orthogonal edges is non-reactive\n // (see flow-edge.ts), so bump the layout tick to re-route edges\n // that treat those siblings as obstacles.\n canvas._layoutAnimTick++;\n canvas._commitNodeGeometry(movedIds);\n // This branch commits a real node-state mutation (reparent/reorder),\n // so commit the deferred snapshot before returning.\n commitDragHistory(canvas, didDrag, pendingDragSnapshot);\n pendingDragSnapshot = null;\n didDrag = false;\n return; // Skip normal onDragEnd logic\n }\n\n // ── Reparent on drop / detach on drag-out ─────────────────────\n if (n && dropTargetId) {\n // Clean up drop target highlight\n if (containerEl) {\n const targetEl = containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(dropTargetId)}\"]`);\n targetEl?.classList.remove('flow-node-drop-target');\n }\n reparentWithoutCapture(canvas, nodeId, dropTargetId);\n dropTargetId = null;\n } else if (n && n.parentId && !dropTargetId) {\n // Check if child was dragged outside parent bounds (detach)\n const parentRules = resolveChildValidation(\n canvas.getNode(n.parentId)!,\n canvas._config?.childValidationRules ?? {},\n );\n const detachParent = canvas.getNode(n.parentId);\n if (!parentRules?.preventChildEscape && !detachParent?.childLayout) {\n if (detachParent?.dimensions) {\n const relX = n.position.x;\n const relY = n.position.y;\n const childW = n.dimensions?.width ?? 150;\n const childH = n.dimensions?.height ?? 50;\n const outside =\n relX + childW < 0 ||\n relY + childH < 0 ||\n relX > detachParent.dimensions.width ||\n relY > detachParent.dimensions.height;\n if (outside) {\n reparentWithoutCapture(canvas, nodeId, null);\n }\n }\n }\n dropTargetId = null;\n } else {\n // Clean up in case drop target was set but node doesn't exist\n if (dropTargetId && containerEl) {\n const targetEl = containerEl.querySelector(`[data-flow-node-id=\"${CSS.escape(dropTargetId)}\"]`);\n targetEl?.classList.remove('flow-node-drop-target');\n }\n dropTargetId = null;\n }\n\n // ── Proximity Connect: create edge on drop ────────────────────\n if (canvas._config?.proximityConnect && proximityCandidate) {\n const cand = proximityCandidate;\n proximityPreviewLine?.destroy();\n proximityPreviewLine = null;\n proximityCandidate = null;\n\n // Check callback\n let allowed = true;\n if (canvas._config.onProximityConnect) {\n const result = canvas._config.onProximityConnect({\n source: cand.source,\n target: cand.target,\n distance: cand.distance,\n });\n if (result === false) allowed = false;\n }\n\n if (allowed) {\n const connection = {\n source: cand.source,\n sourceHandle: 'source',\n target: cand.target,\n targetHandle: 'target',\n };\n\n // Run validation chain\n //\n // NB: proximity-connect is user-passive — the system auto-suggests\n // edges as nodes drift near each other. Rejections here are not\n // user-initiated connection attempts, so we intentionally do NOT\n // dispatch flow-connect-rejected or console.warn. Surfacing those\n // signals would be noisy (fires on every tick the validation\n // fails) and would confuse consumers who expect that event to\n // mean \"the user tried to connect and was rejected.\"\n if (isValidConnection(connection, canvas.edges, { preventCycles: canvas._config?.preventCycles })) {\n const isRulesOk = checkConnectionRules(connection, canvas._config?.connectionRules, canvas._nodeMap);\n const isHandleOk = isRulesOk && (containerEl ? checkHandleLimits(containerEl, connection, canvas.edges) : true);\n const isValidatorOk = isHandleOk && (containerEl ? runHandleValidators(containerEl, connection) : true);\n const isCustomOk = isValidatorOk && (!canvas._config.isValidConnection || canvas._config.isValidConnection(connection));\n\n if (isCustomOk) {\n // Optional confirmation animation\n if (canvas._config.proximityConnectConfirm) {\n const srcEl = containerEl?.querySelector(`[data-flow-node-id=\"${CSS.escape(cand.source)}\"]`);\n const tgtEl = containerEl?.querySelector(`[data-flow-node-id=\"${CSS.escape(cand.target)}\"]`);\n srcEl?.classList.add('flow-proximity-confirm');\n tgtEl?.classList.add('flow-proximity-confirm');\n setTimeout(() => {\n srcEl?.classList.remove('flow-proximity-confirm');\n tgtEl?.classList.remove('flow-proximity-confirm');\n }, 400);\n }\n\n const edgeId = `e-${cand.source}-${cand.target}-${Date.now()}-${proximityEdgeCounter++}`;\n canvas.addEdges({ id: edgeId, ...connection });\n canvas._emit('connect', { connection });\n }\n }\n }\n } else {\n // Clean up preview if drag ended without candidate\n proximityPreviewLine?.destroy();\n proximityPreviewLine = null;\n proximityCandidate = null;\n }\n\n groupDragStartPositions = null;\n\n // Obstacle geometry for avoidant/orthogonal edges is read\n // non-reactively (see flow-edge.ts), so a dragged node that is an\n // *obstacle* for other edges does not re-route them through its own\n // position dependency. Bump the layout tick once the drag settles so\n // those routes re-measure against the moved obstacle.\n if (didDrag) {\n canvas._layoutAnimTick++;\n canvas._commitNodeGeometry(movedIds);\n }\n\n // Commit the deferred history snapshot only if the node actually\n // moved; a plain click leaves it uncommitted.\n commitDragHistory(canvas, didDrag, pendingDragSnapshot);\n pendingDragSnapshot = null;\n // Reset so the next click isn't treated as a drag.\n // d3-drag already suppresses the click event immediately\n // after a real drag, so this is safe.\n didDrag = false;\n },\n });\n });\n\n // ── Easy Connect: modifier key + drag starts a connection ──────\n {\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (canvas?._config?.easyConnect) {\n const easyConnectKey = canvas._config.easyConnectKey ?? 'alt';\n\n const onEasyConnectPointerDown = (e: PointerEvent) => {\n if (!isEasyConnectKey(e, easyConnectKey)) return;\n // Don't intercept if target is a handle (let normal handle logic run)\n if ((e.target as HTMLElement).closest('[data-flow-handle-type]')) return;\n\n const currentCanvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!currentCanvas) return;\n if (currentCanvas._animationLocked) return;\n // Overlap guard: don't start a new easy-connect drag while a\n // previously-dropped connection is still awaiting its async\n // connectValidator — matches the drag-to-connect + click-to-connect\n // guards so two pending validators can't race.\n if (currentCanvas._connectValidating) return;\n\n const node = evaluate(expression) as FlowNode;\n if (!node) return;\n\n const sourceNode = currentCanvas.getNode(node.id);\n if (!sourceNode || sourceNode.connectable === false) return;\n\n e.preventDefault();\n e.stopPropagation();\n e.stopImmediatePropagation();\n\n // Find nearest source handle or use node center\n const sourceHandle = findNearestSourceHandle(el, e.clientX, e.clientY);\n const handleId = sourceHandle?.dataset.flowHandleId ?? 'source';\n\n el.classList.add('flow-easy-connecting');\n\n const containerEl = el.closest('.flow-container') as HTMLElement;\n if (!containerEl) return;\n\n // Compute source position (handle center or node center).\n // Live viewport: reactive `viewport` may lag a frame behind a zoom.\n const initVp = currentCanvas._viewportLive ?? currentCanvas.viewport;\n const initZoom = initVp?.zoom || 1;\n const initVpX = initVp?.x || 0;\n const initVpY = initVp?.y || 0;\n const initContainerRect = containerEl.getBoundingClientRect();\n\n let sourceX: number, sourceY: number;\n if (sourceHandle) {\n const handleRect = sourceHandle.getBoundingClientRect();\n sourceX = (handleRect.left + handleRect.width / 2 - initContainerRect.left - initVpX) / initZoom;\n sourceY = (handleRect.top + handleRect.height / 2 - initContainerRect.top - initVpY) / initZoom;\n } else {\n const nodeRect = el.getBoundingClientRect();\n sourceX = (nodeRect.left + nodeRect.width / 2 - initContainerRect.left - initVpX) / initZoom;\n sourceY = (nodeRect.top + nodeRect.height / 2 - initContainerRect.top - initVpY) / initZoom;\n }\n\n currentCanvas._emit('connect-start', { source: node.id, sourceHandle: handleId });\n\n const connectionLineInstance = createConnectionLine({\n connectionLineType: currentCanvas._config?.connectionLineType,\n connectionLineStyle: currentCanvas._config?.connectionLineStyle,\n connectionLine: currentCanvas._config?.connectionLine,\n });\n const viewportEl = containerEl.querySelector('.flow-viewport');\n if (viewportEl) viewportEl.appendChild(connectionLineInstance.svg);\n\n connectionLineInstance.update({ fromX: sourceX, fromY: sourceY, toX: sourceX, toY: sourceY, source: node.id, sourceHandle: handleId });\n\n currentCanvas.pendingConnection = { source: node.id, sourceHandle: handleId, position: { x: sourceX, y: sourceY } };\n applyValidationClasses(containerEl, node.id, handleId, currentCanvas);\n\n let connectAutoPan = startConnectionAutoPan(containerEl, currentCanvas, e.clientX, e.clientY);\n let snappedHandle: HTMLElement | null = null;\n const connectionSnapRadius = currentCanvas._config?.connectionSnapRadius ?? 20;\n\n const onPointerMove = (moveE: PointerEvent) => {\n const cursorFlowPos = currentCanvas.screenToFlowPosition(moveE.clientX, moveE.clientY);\n const snap = findSnapTarget({\n containerEl, handleType: 'target', excludeNodeId: node.id,\n cursorFlowPos, connectionSnapRadius,\n getNode: (id: string) => currentCanvas.getNode(id),\n toFlowPosition: (sx: number, sy: number) => currentCanvas.screenToFlowPosition(sx, sy),\n });\n\n if (snap.element !== snappedHandle) {\n snappedHandle?.classList.remove('flow-handle-active');\n snap.element?.classList.add('flow-handle-active');\n snappedHandle = snap.element;\n }\n\n connectionLineInstance.update({ fromX: sourceX, fromY: sourceY, toX: snap.position.x, toY: snap.position.y, source: node.id, sourceHandle: handleId });\n currentCanvas.pendingConnection = { ...currentCanvas.pendingConnection, position: snap.position };\n connectAutoPan?.updatePointer(moveE.clientX, moveE.clientY);\n };\n\n const onPointerUp = async (upE: PointerEvent) => {\n connectAutoPan?.stop();\n connectAutoPan = null;\n document.removeEventListener('pointermove', onPointerMove);\n document.removeEventListener('pointerup', onPointerUp);\n\n connectionLineInstance.destroy();\n snappedHandle?.classList.remove('flow-handle-active');\n clearValidationClasses(containerEl);\n el.classList.remove('flow-easy-connecting');\n\n const dropPosition = currentCanvas.screenToFlowPosition(upE.clientX, upE.clientY);\n const connectEndBase = { source: node.id, sourceHandle: handleId, position: dropPosition };\n\n // Clear pendingConnection before the async validator runs so the\n // source handle isn't stuck in a \"connecting\" state during the\n // await. The validator's own events drive any UI feedback.\n currentCanvas.pendingConnection = null;\n\n // Find target handle\n let targetHandle: HTMLElement | null = snappedHandle;\n if (!targetHandle) {\n const dropTarget = document.elementFromPoint(upE.clientX, upE.clientY);\n targetHandle = dropTarget?.closest('[data-flow-handle-type=\"target\"]') as HTMLElement | null;\n }\n\n if (!targetHandle) {\n currentCanvas._emit('connect-end', { connection: null, ...connectEndBase });\n return;\n }\n\n const targetNodeEl = targetHandle.closest('[x-flow-node]') as HTMLElement | null;\n const targetNodeId = targetNodeEl?.dataset.flowNodeId;\n const targetHandleId = targetHandle.dataset.flowHandleId ?? 'target';\n\n if (!targetNodeId) {\n currentCanvas._emit('connect-end', { connection: null, ...connectEndBase });\n return;\n }\n\n const connection = { source: node.id, sourceHandle: handleId, target: targetNodeId, targetHandle: targetHandleId };\n\n // Delegate the full validator chain (sync + async connectValidator)\n // to the shared helper. It creates the edge + emits `connect` on\n // success, and dispatches `flow-connect-rejected` on failure, so\n // easy-connect stays in parity with drag-to-connect, click-to-\n // connect, and keyboard-connect.\n const result = await applyConnectValidation({ connection, canvas: currentCanvas, containerEl });\n currentCanvas._emit('connect-end', {\n connection: result.applied ? connection : null,\n ...connectEndBase,\n });\n };\n\n document.addEventListener('pointermove', onPointerMove);\n document.addEventListener('pointerup', onPointerUp);\n };\n\n el.addEventListener('pointerdown', onEasyConnectPointerDown, { capture: true });\n\n cleanup(() => {\n el.removeEventListener('pointerdown', onEasyConnectPointerDown, { capture: true });\n });\n }\n }\n\n // Handle selection via keyboard (Enter/Space)\n const handleKeyDown = (e: KeyboardEvent) => {\n if (e.key !== 'Enter' && e.key !== ' ') return;\n e.preventDefault();\n\n const node = evaluate(expression) as FlowNode;\n if (!node) return;\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n if (canvas._animationLocked) return;\n if (!isSelectable(node)) return;\n\n canvas._emit('node-click', { node, event: e });\n e.stopPropagation();\n\n if (matchesModifier(e, canvas._shortcuts?.multiSelect)) {\n if (canvas.selectedNodes.has(node.id)) {\n canvas.selectedNodes.delete(node.id);\n node.selected = false;\n } else {\n canvas.selectedNodes.add(node.id);\n node.selected = true;\n }\n } else {\n canvas.deselectAll();\n canvas.selectedNodes.add(node.id);\n node.selected = true;\n }\n canvas._emitSelectionChange();\n };\n el.addEventListener('keydown', handleKeyDown);\n\n // Focus auto-pan: scroll viewport so focused node is visible\n const handleFocus = () => {\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas?._config?.autoPanOnNodeFocus) return;\n const node = evaluate(expression) as FlowNode;\n if (!node) return;\n // Pan to center the node in the viewport\n const pos = node.parentId\n ? canvas.getAbsolutePosition(node.id)\n : node.position;\n canvas.setCenter(\n pos.x + (node.dimensions?.width ?? 150) / 2,\n pos.y + (node.dimensions?.height ?? 40) / 2,\n );\n };\n el.addEventListener('focus', handleFocus);\n\n // Handle selection via click (mouseup without drag movement)\n const handleClick = (e: MouseEvent) => {\n if (didDrag) return;\n\n const node = evaluate(expression) as FlowNode;\n if (!node) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas) return;\n if (canvas._animationLocked) return;\n\n // Always emit node-click, even if not selectable\n canvas._emit('node-click', { node, event: e });\n\n // ── Selectable flag ────────────────────────────────────────\n if (!isSelectable(node)) return;\n\n // Stop the click from reaching the canvas background deselect handler\n e.stopPropagation();\n\n // If drag start already handled selection (node wasn't previously selected),\n // skip re-processing to avoid toggling it back off\n if (dragStartSelected) {\n dragStartSelected = false;\n return;\n }\n\n if (matchesModifier(e, canvas._shortcuts?.multiSelect)) {\n // Toggle selection\n if (canvas.selectedNodes.has(node.id)) {\n canvas.selectedNodes.delete(node.id);\n node.selected = false;\n el.classList.remove('flow-node-selected');\n debug('selection', `Node \"${node.id}\" deselected (shift)`);\n } else {\n canvas.selectedNodes.add(node.id);\n node.selected = true;\n el.classList.add('flow-node-selected');\n debug('selection', `Node \"${node.id}\" selected (shift)`);\n }\n } else {\n // Single select — clear others first\n canvas.deselectAll();\n canvas.selectedNodes.add(node.id);\n node.selected = true;\n el.classList.add('flow-node-selected');\n debug('selection', `Node \"${node.id}\" selected`);\n }\n canvas._emitSelectionChange();\n };\n\n el.addEventListener('click', handleClick);\n\n // Handle right-click context menu\n const handleContextMenu = (e: MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const node = evaluate(expression) as FlowNode;\n if (!node) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement) as any;\n if (!canvas) return;\n\n // When the node is part of a multi-selection, show selection menu instead\n if (canvas.selectedNodes.size > 1 && canvas.selectedNodes.has(node.id)) {\n const nodes = canvas.nodes.filter((n: FlowNode) => canvas.selectedNodes.has(n.id));\n canvas._emit('selection-context-menu', { nodes, event: e });\n } else {\n canvas._emit('node-context-menu', { node, event: e });\n }\n };\n el.addEventListener('contextmenu', handleContextMenu);\n\n // Measure node dimensions after first paint so edge paths can\n // connect to the correct handle positions (center-bottom / center-top).\n requestAnimationFrame(() => {\n if (!el.isConnected) return;\n\n const node = evaluate(expression) as FlowNode;\n if (!node) return;\n\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n\n // Use offsetWidth/offsetHeight — unaffected by CSS transforms (rotation).\n // getBoundingClientRect() returns the inflated axis-aligned bounding box\n // of rotated elements, which would make the node grow.\n node.dimensions = {\n width: el.offsetWidth,\n height: el.offsetHeight,\n };\n debug('init', `Node \"${node.id}\" measured`, node.dimensions);\n\n // Register element for CSS-based viewport culling (outside reactive system)\n canvas?._nodeElements?.set(node.id, el);\n\n // A1: Begin ResizeObserver tracking unless the node opts out.\n if (node.resizeObserver !== false && canvas?._resizeObserver) {\n canvas._resizeObserver.observe(el);\n }\n });\n\n cleanup(() => {\n dragInstance?.destroy();\n helperLinesSvg?.remove();\n helperLinesSvg = null;\n proximityPreviewLine?.destroy();\n proximityPreviewLine = null;\n el.removeEventListener('keydown', handleKeyDown);\n el.removeEventListener('focus', handleFocus);\n el.removeEventListener('click', handleClick);\n el.removeEventListener('contextmenu', handleContextMenu);\n\n // Unregister from viewport culling and ResizeObserver\n const nodeId = el.dataset.flowNodeId;\n if (nodeId) {\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n canvas?._nodeElements?.delete(nodeId);\n canvas?._resizeObserver?.unobserve(el);\n canvas?._draggingNodeIds?.delete(nodeId);\n }\n });\n },\n );\n}\n","// ============================================================================\n// Resize Utilities\n//\n// Pure functions for computing node resize operations. Handles direction-aware\n// delta application, constraint clamping, and position adjustment for\n// top/left handles where position shifts inversely with dimension growth.\n// ============================================================================\n\nimport type { XYPosition, Dimensions } from './types';\n\n/** Which edge or corner is being dragged */\nexport type ResizeDirection =\n | 'top-left' | 'top' | 'top-right'\n | 'left' | 'right'\n | 'bottom-left' | 'bottom' | 'bottom-right';\n\n/** Min/max size constraints for resizable nodes */\nexport interface ResizeConstraints {\n minWidth: number;\n minHeight: number;\n maxWidth: number;\n maxHeight: number;\n}\n\nexport const DEFAULT_RESIZE_CONSTRAINTS: ResizeConstraints = {\n minWidth: 30,\n minHeight: 30,\n maxWidth: Infinity,\n maxHeight: Infinity,\n};\n\n/** Result of a resize computation */\nexport interface ResizeResult {\n position: XYPosition;\n dimensions: Dimensions;\n}\n\n/**\n * Compute the new position and dimensions after a resize drag.\n *\n * For left/top handles, position decreases by the same amount dimensions\n * increase. The actual delta is derived after clamping to prevent position\n * drift when hitting constraint boundaries.\n *\n * @param direction - Which handle is being dragged\n * @param delta - Mouse movement in flow coordinates since drag start\n * @param startPosition - Node position at drag start\n * @param startDimensions - Node dimensions at drag start\n * @param constraints - Min/max size constraints\n * @param snapToGrid - Optional grid snapping [gridX, gridY]\n */\nexport function computeResize(\n direction: ResizeDirection,\n delta: XYPosition,\n startPosition: XYPosition,\n startDimensions: Dimensions,\n constraints: ResizeConstraints,\n snapToGrid?: false | [number, number],\n): ResizeResult {\n const { minWidth, minHeight, maxWidth, maxHeight } = constraints;\n\n const affectsLeft = direction.includes('left');\n const affectsRight = direction.includes('right');\n const affectsTop = direction.includes('top');\n const affectsBottom = direction.includes('bottom');\n\n // Compute raw new width\n let newWidth = startDimensions.width;\n if (affectsRight) {\n newWidth = startDimensions.width + delta.x;\n } else if (affectsLeft) {\n newWidth = startDimensions.width - delta.x;\n }\n\n // Compute raw new height\n let newHeight = startDimensions.height;\n if (affectsBottom) {\n newHeight = startDimensions.height + delta.y;\n } else if (affectsTop) {\n newHeight = startDimensions.height - delta.y;\n }\n\n // Clamp to constraints\n newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));\n newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));\n\n // Snap dimensions to grid if configured\n if (snapToGrid) {\n newWidth = snapToGrid[0] * Math.round(newWidth / snapToGrid[0]);\n newHeight = snapToGrid[1] * Math.round(newHeight / snapToGrid[1]);\n // Re-clamp after snapping\n newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));\n newHeight = Math.max(minHeight, Math.min(maxHeight, newHeight));\n }\n\n // Compute actual deltas (after clamping) to derive position shift\n const actualDeltaW = newWidth - startDimensions.width;\n const actualDeltaH = newHeight - startDimensions.height;\n\n // Position only shifts for left/top handles\n const newX = affectsLeft ? startPosition.x - actualDeltaW : startPosition.x;\n const newY = affectsTop ? startPosition.y - actualDeltaH : startPosition.y;\n\n return {\n position: { x: newX, y: newY },\n dimensions: { width: newWidth, height: newHeight },\n };\n}\n","// ============================================================================\n// x-flow-resizer Directive\n//\n// Adds resize handles to a flow node. Handles are absolutely positioned\n// squares (corners) or rectangles (edges) that the user can drag to resize\n// the node. Respects per-node `resizable` flag and min/max constraints.\n//\n// Usage:\n//
— all 8 handles\n//
— 4 corner handles only\n//
— 4 edge handles only\n//
— single handle\n//
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type { FlowNode, Dimensions } from '../../core/types';\nimport {\n computeResize,\n DEFAULT_RESIZE_CONSTRAINTS,\n type ResizeDirection,\n type ResizeConstraints,\n} from '../../core/resize';\nimport { debug } from '../../core/debug';\nimport { isResizable } from '../../core/node-flags';\n\nconst ALL_CORNERS: ResizeDirection[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];\nconst ALL_EDGES: ResizeDirection[] = ['top', 'right', 'bottom', 'left'];\nconst ALL_HANDLES: ResizeDirection[] = [...ALL_CORNERS, ...ALL_EDGES];\n\nconst CURSOR_MAP: Record = {\n 'top-left': 'nwse-resize',\n 'top-right': 'nesw-resize',\n 'bottom-left': 'nesw-resize',\n 'bottom-right': 'nwse-resize',\n 'top': 'ns-resize',\n 'bottom': 'ns-resize',\n 'left': 'ew-resize',\n 'right': 'ew-resize',\n};\n\nexport function registerFlowResizerDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-resizer',\n (\n el,\n { expression, modifiers },\n { evaluate, effect, cleanup },\n ) => {\n // ── Determine which handles to show ──────────────────────────────\n const directions = resolveDirections(modifiers);\n\n // ── Parse constraints from expression ────────────────────────────\n let constraints: ResizeConstraints = { ...DEFAULT_RESIZE_CONSTRAINTS };\n if (expression) {\n try {\n const parsed = evaluate(expression) as Partial;\n constraints = { ...DEFAULT_RESIZE_CONSTRAINTS, ...parsed };\n } catch {\n // Expression might not be valid — use defaults\n }\n }\n\n // ── Create handle elements ───────────────────────────────────────\n const handleEls: HTMLDivElement[] = [];\n\n for (const dir of directions) {\n const handle = document.createElement('div');\n handle.className = `flow-resizer-handle flow-resizer-handle-${dir}`;\n handle.style.cursor = CURSOR_MAP[dir];\n handle.dataset.flowResizeDirection = dir;\n el.appendChild(handle);\n handleEls.push(handle);\n\n // ── Pointer event: start resize ────────────────────────────────\n handle.addEventListener('pointerdown', (e: PointerEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const nodeEl = el.closest('[x-flow-node]') as HTMLElement | null;\n if (!nodeEl) return;\n\n const canvasEl = el.closest('[x-data]') as HTMLElement | null;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl);\n const nodeId = nodeEl.dataset.flowNodeId;\n if (!nodeId || !canvas) return;\n\n const node = canvas.getNode(nodeId) as FlowNode | undefined;\n if (!node) return;\n if (!isResizable(node)) return;\n\n // A2: height control requires fixedDimensions to prevent the Alpine effect\n // from clearing inline height on leaf nodes. User dragged a handle — they\n // chose a size, so the node is now explicitly fixed-dimension.\n node.fixedDimensions = true;\n\n // Merge node-level min/maxDimensions with directive constraints.\n // Node properties serve as base; directive expression overrides.\n const effectiveConstraints: ResizeConstraints = { ...constraints };\n if (node.minDimensions?.width != null && constraints.minWidth === DEFAULT_RESIZE_CONSTRAINTS.minWidth) {\n effectiveConstraints.minWidth = node.minDimensions.width;\n }\n if (node.minDimensions?.height != null && constraints.minHeight === DEFAULT_RESIZE_CONSTRAINTS.minHeight) {\n effectiveConstraints.minHeight = node.minDimensions.height;\n }\n if (node.maxDimensions?.width != null && constraints.maxWidth === DEFAULT_RESIZE_CONSTRAINTS.maxWidth) {\n effectiveConstraints.maxWidth = node.maxDimensions.width;\n }\n if (node.maxDimensions?.height != null && constraints.maxHeight === DEFAULT_RESIZE_CONSTRAINTS.maxHeight) {\n effectiveConstraints.maxHeight = node.maxDimensions.height;\n }\n\n // Ensure dimensions exist\n if (!node.dimensions) {\n const zoom = canvas.viewport?.zoom || 1;\n const rect = nodeEl.getBoundingClientRect();\n node.dimensions = { width: rect.width / zoom, height: rect.height / zoom };\n }\n\n const startPosition = { x: node.position.x, y: node.position.y };\n const startDimensions = { width: node.dimensions.width, height: node.dimensions.height };\n const zoom = canvas.viewport?.zoom || 1;\n const startClientX = e.clientX;\n const startClientY = e.clientY;\n\n canvas._captureHistory?.();\n debug('resize', `Resize start on \"${nodeId}\" (${dir})`, startDimensions);\n canvas._emit('node-resize-start', { node, dimensions: { ...startDimensions } });\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n const delta = {\n x: (moveEvent.clientX - startClientX) / zoom,\n y: (moveEvent.clientY - startClientY) / zoom,\n };\n\n const result = computeResize(\n dir,\n delta,\n startPosition,\n startDimensions,\n effectiveConstraints,\n canvas._config?.snapToGrid ?? false,\n );\n\n // Update node state\n node.position.x = result.position.x;\n node.position.y = result.position.y;\n node.dimensions!.width = result.dimensions.width;\n node.dimensions!.height = result.dimensions.height;\n\n // Update DOM directly for smooth visual feedback\n // For child nodes, result.position is relative — compute absolute for DOM\n if (node.parentId) {\n const abs = canvas.getAbsolutePosition(node.id);\n nodeEl.style.left = `${abs.x}px`;\n nodeEl.style.top = `${abs.y}px`;\n } else {\n nodeEl.style.left = `${result.position.x}px`;\n nodeEl.style.top = `${result.position.y}px`;\n }\n nodeEl.style.width = `${result.dimensions.width}px`;\n nodeEl.style.height = `${result.dimensions.height}px`;\n\n // Obstacle geometry for avoidant/orthogonal edges is read\n // non-reactively (see flow-edge.ts), so a resized obstacle node does\n // not re-route dependent edges on its own. Bump the layout tick each\n // frame so endpoint measurement tracks the resize live; obstacle\n // geometry itself only refreshes when the ResizeObserver commits\n // (via _commitNodeGeometry), i.e. at commit cadence, not every frame.\n canvas._layoutAnimTick++;\n\n canvas._emit('node-resize', { node, dimensions: { ...result.dimensions } });\n };\n\n const onPointerUp = () => {\n document.removeEventListener('pointermove', onPointerMove);\n document.removeEventListener('pointerup', onPointerUp);\n document.removeEventListener('pointercancel', onPointerUp);\n\n debug('resize', `Resize end on \"${nodeId}\"`, node.dimensions);\n canvas._emit('node-resize-end', { node, dimensions: { ...node.dimensions! } });\n };\n\n document.addEventListener('pointermove', onPointerMove);\n document.addEventListener('pointerup', onPointerUp);\n document.addEventListener('pointercancel', onPointerUp);\n });\n }\n\n // ── Reactively hide handles when resizable === false ─────────────\n effect(() => {\n const nodeEl = el.closest('[x-flow-node]') as HTMLElement | null;\n if (!nodeEl) return;\n\n const canvasEl = el.closest('[x-data]') as HTMLElement | null;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl);\n const nodeId = nodeEl.dataset.flowNodeId;\n if (!nodeId || !canvas) return;\n\n const node = canvas.getNode(nodeId) as FlowNode | undefined;\n if (!node) return;\n\n const hidden = !isResizable(node);\n for (const h of handleEls) {\n h.style.display = hidden ? 'none' : '';\n }\n });\n\n // ── Cleanup ──────────────────────────────────────────────────────\n cleanup(() => {\n for (const h of handleEls) {\n h.remove();\n }\n });\n },\n );\n}\n\n/**\n * Determine which resize directions are active based on modifiers.\n *\n * - No position modifiers → all 8 handles\n * - `.corners` → 4 corner handles\n * - `.edges` → 4 edge handles\n * - Specific directions like `.bottom.right` → single handle\n */\nfunction resolveDirections(modifiers: string[]): ResizeDirection[] {\n if (modifiers.includes('corners')) {\n return ALL_CORNERS;\n }\n\n if (modifiers.includes('edges')) {\n return ALL_EDGES;\n }\n\n const hasTop = modifiers.includes('top');\n const hasBottom = modifiers.includes('bottom');\n const hasLeft = modifiers.includes('left');\n const hasRight = modifiers.includes('right');\n\n // If specific directions are specified, build the single handle\n if (hasTop || hasBottom || hasLeft || hasRight) {\n if (hasTop && hasLeft) return ['top-left'];\n if (hasTop && hasRight) return ['top-right'];\n if (hasBottom && hasLeft) return ['bottom-left'];\n if (hasBottom && hasRight) return ['bottom-right'];\n if (hasTop) return ['top'];\n if (hasBottom) return ['bottom'];\n if (hasLeft) return ['left'];\n if (hasRight) return ['right'];\n }\n\n // Default: all 8 handles\n return ALL_HANDLES;\n}\n","// ============================================================================\n// x-flow-rotate Directive\n//\n// Adds a drag-to-rotate handle to a flow node. Drag the handle to rotate\n// the node interactively. Updates node.rotation in degrees.\n//\n// Usage:\n//
— free rotation\n//
— snap to 15° increments\n//
— snap to 45° increments\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type { FlowNode } from '../../core/types';\n\n/**\n * Compute the rotation angle (0-360) from a pointer position relative to a center point.\n * 0° = directly above center (12 o'clock), clockwise.\n */\nexport function computeRotationAngle(\n pointerX: number,\n pointerY: number,\n centerX: number,\n centerY: number,\n): number {\n const rad = Math.atan2(pointerX - centerX, -(pointerY - centerY));\n const deg = (rad * 180) / Math.PI;\n return ((deg % 360) + 360) % 360;\n}\n\n/**\n * Snap an angle to the nearest increment.\n */\nexport function snapAngle(angle: number, increment: number): number {\n const snapped = Math.round(angle / increment) * increment;\n return ((snapped % 360) + 360) % 360;\n}\n\nexport function registerFlowRotateDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-rotate',\n (el, { expression, modifiers }, { evaluate, effect, cleanup }) => {\n const shouldSnap = modifiers.includes('snap');\n const snapIncrement = shouldSnap && expression ? Number(evaluate(expression)) || 15 : 15;\n\n // Style the handle\n el.classList.add('flow-rotate-handle');\n el.style.cursor = 'grab';\n\n const onPointerDown = (e: PointerEvent) => {\n e.preventDefault();\n e.stopPropagation();\n\n const nodeEl = el.closest('[x-flow-node]') as HTMLElement | null;\n if (!nodeEl) return;\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement | null;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n const nodeId = nodeEl.dataset.flowNodeId;\n if (!nodeId || !canvas) return;\n\n const node = canvas.getNode(nodeId) as FlowNode | undefined;\n if (!node) return;\n\n const nodeRect = nodeEl.getBoundingClientRect();\n const centerX = nodeRect.left + nodeRect.width / 2;\n const centerY = nodeRect.top + nodeRect.height / 2;\n\n canvas._captureHistory();\n el.style.cursor = 'grabbing';\n\n const onPointerMove = (moveEvent: PointerEvent) => {\n let angle = computeRotationAngle(\n moveEvent.clientX,\n moveEvent.clientY,\n centerX,\n centerY,\n );\n\n if (shouldSnap) {\n angle = snapAngle(angle, snapIncrement);\n }\n\n node.rotation = angle;\n };\n\n const onPointerUp = () => {\n document.removeEventListener('pointermove', onPointerMove);\n document.removeEventListener('pointerup', onPointerUp);\n el.style.cursor = 'grab';\n canvas._emit('node-rotate-end', { node, rotation: node.rotation });\n };\n\n document.addEventListener('pointermove', onPointerMove);\n document.addEventListener('pointerup', onPointerUp);\n };\n\n el.addEventListener('pointerdown', onPointerDown);\n\n cleanup(() => {\n el.removeEventListener('pointerdown', onPointerDown);\n el.classList.remove('flow-rotate-handle');\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-drag-handle Directive\n//\n// Marks an element as the drag handle for its parent x-flow-node. When present,\n// only pointerdown events originating from within this element will initiate\n// a node drag gesture. Without it, the entire node body is draggable.\n//\n// Usage:
Drag here
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nexport function registerFlowDragHandleDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-drag-handle',\n (el) => {\n el.setAttribute('data-flow-drag-handle', '');\n el.classList.add('flow-drag-handle');\n\n // Apply the has-handle class to the parent node element immediately.\n // The x-flow-node effect also does this, but it runs before child\n // directives initialize so it misses the handle on the first pass.\n const nodeEl = el.closest('[x-flow-node]');\n if (nodeEl) {\n nodeEl.classList.add('flow-node-has-handle');\n }\n },\n );\n}\n","// ============================================================================\n// x-flow-draggable directive\n//\n// Makes any element draggable onto a flowCanvas drop zone. Sets up HTML5\n// drag-and-drop with the `application/alpineflow` MIME type.\n//\n// Usage:\n//
Input
\n//
Process
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nconst MIME_TYPE = 'application/alpineflow';\n\nexport function registerFlowDraggableDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-draggable',\n (el, { expression }, { evaluate }) => {\n el.setAttribute('draggable', 'true');\n el.style.cursor = 'grab';\n\n el.addEventListener('dragstart', (event: DragEvent) => {\n if (!event.dataTransfer) return;\n const value = evaluate(expression);\n const payload = typeof value === 'string' ? value : JSON.stringify(value);\n event.dataTransfer.setData(MIME_TYPE, payload);\n event.dataTransfer.effectAllowed = 'move';\n });\n },\n );\n}\n","// ============================================================================\n// Bidirectional Edge Collapse\n//\n// When `canvas.collapseBidirectionalEdges` is enabled, reciprocal edge pairs\n// (A→B + B→A) can be rendered as a single path with markers at BOTH ends.\n// Both edges remain in `canvas.edges`; only rendering changes.\n//\n// Pure helpers — no DOM access.\n// ============================================================================\n\n/** A pair of reciprocal edges identified by id. */\nexport interface BidirectionalPair {\n /** The edge that remains visible and renders a dual (start + end) marker. */\n primaryId: string;\n /** The edge that is hidden from rendering because its partner already draws the pair. */\n mirrorId: string;\n}\n\n/**\n * Detect reciprocal edge pairs (A→B + B→A). Each edge participates in at most\n * one pair; the first matching mirror wins. Self-loops (source === target) are\n * ignored.\n */\nexport function detectBidirectionalPairs(\n edges: E[],\n): BidirectionalPair[] {\n const pairs: BidirectionalPair[] = [];\n const seen = new Set();\n\n for (const edge of edges) {\n if (seen.has(edge.id)) {\n continue;\n }\n if (edge.source === edge.target) {\n continue;\n }\n const mirror = edges.find(\n (e) =>\n e.id !== edge.id\n && e.source === edge.target\n && e.target === edge.source\n && !seen.has(e.id),\n );\n if (mirror) {\n pairs.push({ primaryId: edge.id, mirrorId: mirror.id });\n seen.add(edge.id);\n seen.add(mirror.id);\n }\n }\n\n return pairs;\n}\n\n/**\n * Decorate edges with collapse flags. Primary edges in a reciprocal pair get\n * `_renderDualMarker`; mirror edges get `_hiddenByCollapse`. Non-paired edges\n * are returned unchanged (flag fields are `undefined`).\n *\n * Returns fresh spread copies — does NOT mutate the input array. The live\n * canvas wiring in `flow-viewport.ts` writes flags directly onto the reactive\n * edge objects instead so Alpine's reactivity picks them up; this helper\n * exists for pure-function testing and external consumers. Do not wire it\n * into the directive layer without understanding the reactivity trade-off.\n */\nexport function collapseBidirectionalPairs(\n edges: E[],\n): Array {\n const pairs = detectBidirectionalPairs(edges);\n const primaryIds = new Set(pairs.map((p) => p.primaryId));\n const mirrorIds = new Set(pairs.map((p) => p.mirrorId));\n\n return edges.map((e) => ({\n ...e,\n _renderDualMarker: primaryIds.has(e.id) || undefined,\n _hiddenByCollapse: mirrorIds.has(e.id) || undefined,\n }));\n}\n","// ============================================================================\n// x-flow-viewport directive\n//\n// Replaces the manual `.flow-viewport` div + edge loop boilerplate. When placed\n// on an element, it auto-applies the viewport class and reactively renders the\n// full edge SVG layer. Developers only need to provide the node loop.\n//\n// Usage:\n//
\n// \n//
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport { detectBidirectionalPairs } from '../../core/bidirectional-edges';\n\nexport function registerFlowViewportDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-viewport',\n (el, {}, { effect, cleanup }) => {\n el.classList.add('flow-viewport');\n\n // Walk up to find the flowCanvas data context\n const canvas = Alpine.$data(el.closest('[x-data]') as HTMLElement);\n if (!canvas?.edges) return;\n\n // Register this element with the canvas: it is the element the viewport CSS\n // transform is applied to, and the element the canvas's single delegated handle\n // `pointerdown` listener is bound to. init() may run before this directive, so\n // `_viewportEl` could be null there.\n //\n // Routed through `_registerViewportEl` rather than assigned, because if THIS is\n // a replacement viewport node (the directive re-initialising on a different\n // element) the delegated listener is still sitting on the old, now-detached one\n // and has to be moved — otherwise every handle in the canvas silently goes inert.\n // See `_registerViewportEl` in flow-canvas.ts for the full invariant.\n if (typeof canvas._registerViewportEl === 'function') {\n canvas._registerViewportEl(el);\n } else {\n canvas._viewportEl = el;\n }\n const vp = canvas.viewport;\n if (vp) {\n el.style.transform = `translate(${vp.x}px, ${vp.y}px) scale(${vp.zoom})`;\n }\n\n // Create the edge container\n const edgesDiv = document.createElement('div');\n edgesDiv.classList.add('flow-edges');\n el.insertBefore(edgesDiv, el.firstChild);\n\n // Track rendered edge SVGs by edge id\n const edgeSvgMap = new Map();\n\n effect(() => {\n // Read edges reactively\n const edges: { id: string }[] = canvas.edges;\n const currentIds = new Set(edges.map((e) => e.id));\n\n // Remove SVGs for edges that no longer exist\n for (const [id, svg] of edgeSvgMap) {\n if (!currentIds.has(id)) {\n Alpine.destroyTree(svg);\n svg.remove();\n edgeSvgMap.delete(id);\n canvas._edgeSvgElements?.delete(id);\n }\n }\n\n // Add SVGs for new edges\n for (const edge of edges) {\n if (edgeSvgMap.has(edge.id)) continue;\n\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('class', 'flow-edge-svg');\n const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');\n svg.appendChild(g);\n\n // Wire up the x-flow-edge directive via Alpine\n Alpine.addScopeToNode(g as unknown as HTMLElement, { edge });\n (g as Element).setAttribute('x-flow-edge', 'edge');\n\n // Insert into DOM inside mutateDom so Alpine's MutationObserver\n // doesn't see the new element and trigger a duplicate init.\n Alpine.mutateDom(() => {\n edgesDiv.appendChild(svg);\n });\n\n edgeSvgMap.set(edge.id, svg);\n\n // Register for CSS-based viewport culling\n canvas._edgeSvgElements?.set(edge.id, svg);\n\n Alpine.initTree(g as unknown as HTMLElement);\n }\n\n // Remove pre-rendered static edges SVG (replaced by reactive edges)\n const containerEl = el.closest('[data-flow-canvas]') ?? el;\n const staticEdges = containerEl.querySelector('.flow-edges-static');\n if (staticEdges) staticEdges.remove();\n\n // ── Bidirectional edge collapse ─────────────────────────────\n // When enabled, reciprocal pairs (A→B + B→A) render as one path\n // with markers at both ends. The mirror edge is hidden; the primary\n // edge is tagged with `_renderDualMarker` so flow-edge.ts adds\n // `marker-start`. Both edges remain in `canvas.edges`.\n //\n // Flags are written onto the live edge objects (guarded against\n // no-op writes to avoid reactivity loops). When the option is off,\n // existing flags are cleared so toggling behaves correctly.\n const collapseOn = !!canvas._config?.collapseBidirectionalEdges;\n const primaryIds = new Set();\n const mirrorIds = new Set();\n if (collapseOn) {\n const pairs = detectBidirectionalPairs(\n edges as { id: string; source: string; target: string }[],\n );\n for (const p of pairs) {\n primaryIds.add(p.primaryId);\n mirrorIds.add(p.mirrorId);\n }\n }\n\n for (const edge of edges as { id: string; _renderDualMarker?: boolean; _hiddenByCollapse?: boolean }[]) {\n const wantPrimary = primaryIds.has(edge.id);\n const wantMirror = mirrorIds.has(edge.id);\n if (!!edge._renderDualMarker !== wantPrimary) {\n edge._renderDualMarker = wantPrimary ? true : undefined;\n }\n if (!!edge._hiddenByCollapse !== wantMirror) {\n edge._hiddenByCollapse = wantMirror ? true : undefined;\n }\n }\n\n // Update edge visibility based on hidden flags\n for (const edge of edges as { id: string; hidden?: boolean; source?: string; target?: string; _hiddenByCollapse?: boolean }[]) {\n const svg = edgeSvgMap.get(edge.id);\n if (!svg) continue;\n\n const sourceNode = canvas.getNode?.(edge.source);\n const targetNode = canvas.getNode?.(edge.target);\n const isHidden = edge.hidden || edge._hiddenByCollapse || sourceNode?.hidden || targetNode?.hidden;\n\n svg.style.display = isHidden ? 'none' : '';\n }\n\n // Update edge filtered class based on node filtered flags\n for (const edge of edges as { id: string; source?: string; target?: string }[]) {\n const svg = edgeSvgMap.get(edge.id);\n if (!svg) continue;\n const sourceNode = canvas.getNode?.(edge.source);\n const targetNode = canvas.getNode?.(edge.target);\n const isFiltered = sourceNode?.filtered || targetNode?.filtered;\n if (isFiltered) {\n svg.classList.add('flow-edge-filtered');\n } else {\n svg.classList.remove('flow-edge-filtered');\n }\n }\n });\n\n cleanup(() => {\n for (const [id, svg] of edgeSvgMap) {\n Alpine.destroyTree(svg);\n svg.remove();\n canvas._edgeSvgElements?.delete(id);\n }\n edgeSvgMap.clear();\n edgesDiv.remove();\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-panel Directive\n//\n// Floating overlay panel that sits above the canvas. Anchors to one of 8\n// positions, is draggable and resizable by default.\n//\n// Modifiers:\n// Position: top, bottom, left, right, top-left, top-right (default),\n// bottom-left, bottom-right\n// .locked — not draggable (resizable unless .no-resize/.static)\n// .static — no drag, no resize\n// .no-resize — draggable but not resizable\n// .constrained — prevent dragging outside the container bounds\n// .fill-width — spans full container width, implies fixed + no-resize\n// .fill-height — spans full container height, implies fixed + no-resize\n// .fill — spans both width and height, implies fixed + no-resize\n//\n// Events (dispatched on the flow-container element):\n// flow-panel-drag-start { panel, position }\n// flow-panel-drag { panel, position }\n// flow-panel-drag-end { panel, position }\n// flow-panel-resize-start { panel, dimensions }\n// flow-panel-resize { panel, dimensions }\n// flow-panel-resize-end { panel, dimensions }\n//\n// Reset:\n// Listens for a 'flow-panel-reset' CustomEvent on the container to restore\n// the panel to its original state. The flowCanvas component exposes a\n// resetPanels() method for this.\n//\n// Usage:\n//
Content
\n//
Locked, resizable
\n//
Full-width header
\n//
Sidebar
\n//
Static overlay
\n//
Defaults to top-right
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type { HandlePosition } from '../../core/types';\n\nconst VALID_POSITIONS: HandlePosition[] = [\n 'top', 'bottom', 'left', 'right',\n 'top-left', 'top-right', 'bottom-left', 'bottom-right',\n];\n\nconst INTERACTIVE_SELECTORS = 'a, button, input, textarea, select, [contenteditable]';\n\nconst MIN_WIDTH = 100;\nconst MIN_HEIGHT = 60;\n\nconst TOP_POSITIONS = new Set(['top', 'top-left', 'top-right']);\nconst BOTTOM_POSITIONS = new Set(['bottom', 'bottom-left', 'bottom-right']);\nconst LEFT_POSITIONS = new Set(['left', 'top-left', 'bottom-left']);\nconst RIGHT_POSITIONS = new Set(['right', 'top-right', 'bottom-right']);\n\ninterface PanelModifiers {\n position: HandlePosition;\n isStatic: boolean;\n isFixed: boolean;\n noResize: boolean;\n constrained: boolean;\n fillWidth: boolean;\n fillHeight: boolean;\n}\n\n/**\n * Parse position and behavior flags from Alpine directive modifiers.\n */\nfunction parseModifiers(value: string, modifiers: string[]): PanelModifiers {\n const modSet = new Set(modifiers);\n\n const isStatic = modSet.has('static');\n const noResize = modSet.has('no-resize') || modSet.has('noresize');\n const isFixed = modSet.has('locked');\n const constrained = modSet.has('constrained');\n\n // .fill = both, .fill-width = width only, .fill-height = height only\n let fillWidth = modSet.has('fill-width') || modSet.has('fill');\n let fillHeight = modSet.has('fill-height') || modSet.has('fill');\n\n // Position from colon value (e.g. x-flow-panel:bottom-left)\n const position: HandlePosition = value && VALID_POSITIONS.includes(value as HandlePosition)\n ? (value as HandlePosition)\n : 'top-right';\n\n return { position, isStatic, isFixed, noResize, constrained, fillWidth, fillHeight };\n}\n\n/**\n * Dispatch a panel event on the container element, following the flow-* convention.\n */\nfunction emitPanelEvent(container: HTMLElement, event: string, detail: Record): void {\n container.dispatchEvent(new CustomEvent(`flow-${event}`, {\n bubbles: true,\n detail,\n }));\n}\n\n/**\n * Clamp a position so the panel stays fully inside the container.\n */\nfunction clampPosition(\n left: number,\n top: number,\n panelWidth: number,\n panelHeight: number,\n containerWidth: number,\n containerHeight: number,\n): { left: number; top: number } {\n return {\n left: Math.max(0, Math.min(left, containerWidth - panelWidth)),\n top: Math.max(0, Math.min(top, containerHeight - panelHeight)),\n };\n}\n\n/**\n * Apply fill inline styles based on which dimensions should be filled\n * and which anchor position is set. Fill overrides the CSS class positioning\n * so the panel stretches edge-to-edge within the container.\n */\nfunction applyFillStyles(\n el: HTMLElement,\n position: HandlePosition,\n fillWidth: boolean,\n fillHeight: boolean,\n): void {\n el.style.transform = 'none';\n el.style.borderRadius = '0';\n\n if (fillWidth) {\n el.style.left = '0';\n el.style.right = '0';\n el.style.width = 'auto';\n }\n\n if (fillHeight) {\n el.style.top = '0';\n el.style.bottom = '0';\n el.style.height = 'auto';\n }\n\n // Flush the perpendicular edge to 0 for a clean full-span look.\n // e.g. top.fill-width → top: 0 (no 12px gap)\n if (fillWidth && !fillHeight) {\n if (TOP_POSITIONS.has(position)) el.style.top = '0';\n if (BOTTOM_POSITIONS.has(position)) el.style.bottom = '0';\n }\n if (fillHeight && !fillWidth) {\n if (LEFT_POSITIONS.has(position)) el.style.left = '0';\n if (RIGHT_POSITIONS.has(position)) el.style.right = '0';\n }\n}\n\nexport function registerFlowPanelDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-panel',\n (el: HTMLElement, { value, modifiers }: { value: string; modifiers: string[] }, { cleanup }: { cleanup: (fn: () => void) => void }) => {\n const {\n position, isStatic, isFixed, noResize, constrained,\n fillWidth, fillHeight,\n } = parseModifiers(value, modifiers);\n\n const isFill = fillWidth || fillHeight;\n const canDrag = !isStatic && !isFixed && !isFill;\n const canResize = !isStatic && !noResize && !isFill;\n\n // --- CSS classes ---\n el.classList.add('flow-panel', `flow-panel-${position}`);\n if (isStatic) el.classList.add('flow-panel-static');\n if (isFixed || isFill) el.classList.add('flow-panel-locked');\n if (noResize || isFill) el.classList.add('flow-panel-no-resize');\n if (fillWidth) el.classList.add('flow-panel-fill-width');\n if (fillHeight) el.classList.add('flow-panel-fill-height');\n\n // --- Fill positioning (inline styles override CSS class positioning) ---\n if (isFill) {\n applyFillStyles(el, position, fillWidth, fillHeight);\n }\n\n // --- Event isolation: prevent canvas pan/zoom ---\n const stopProp = (e: Event) => e.stopPropagation();\n el.addEventListener('mousedown', stopProp);\n el.addEventListener('pointerdown', stopProp);\n el.addEventListener('wheel', stopProp);\n\n // --- Container reference ---\n const container = el.parentElement!;\n\n // --- Reset support ---\n // Snapshot the initial inline styles AFTER fill setup so reset\n // restores fill panels to their fill state, not to empty styles.\n const initialStyles = {\n left: el.style.left,\n top: el.style.top,\n right: el.style.right,\n bottom: el.style.bottom,\n transform: el.style.transform,\n width: el.style.width,\n height: el.style.height,\n borderRadius: el.style.borderRadius,\n };\n\n const originalPositionClass = `flow-panel-${position}`;\n\n const onReset = () => {\n el.style.left = initialStyles.left;\n el.style.top = initialStyles.top;\n el.style.right = initialStyles.right;\n el.style.bottom = initialStyles.bottom;\n el.style.transform = initialStyles.transform;\n el.style.width = initialStyles.width;\n el.style.height = initialStyles.height;\n el.style.borderRadius = initialStyles.borderRadius;\n if (!el.classList.contains(originalPositionClass)) {\n el.classList.add(originalPositionClass);\n }\n };\n\n container.addEventListener('flow-panel-reset', onReset);\n\n // --- Register panel on container for discovery ---\n if (!(container as any).__flowPanels) {\n (container as any).__flowPanels = new Set();\n }\n (container as any).__flowPanels.add(el);\n\n let resizeHandle: HTMLElement | null = null;\n\n if (canDrag) {\n // --- Draggable ---\n let isDragging = false;\n let dragStartX = 0;\n let dragStartY = 0;\n let elStartLeft = 0;\n let elStartTop = 0;\n\n const getPanelPosition = (): { x: number; y: number } => {\n const rect = el.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n return {\n x: rect.left - containerRect.left,\n y: rect.top - containerRect.top,\n };\n };\n\n const onDragPointerMove = (e: PointerEvent) => {\n if (!isDragging) return;\n let newLeft = elStartLeft + (e.clientX - dragStartX);\n let newTop = elStartTop + (e.clientY - dragStartY);\n\n if (constrained) {\n const clamped = clampPosition(\n newLeft, newTop,\n el.offsetWidth, el.offsetHeight,\n container.clientWidth, container.clientHeight,\n );\n newLeft = clamped.left;\n newTop = clamped.top;\n }\n\n el.style.left = `${newLeft}px`;\n el.style.top = `${newTop}px`;\n\n emitPanelEvent(container, 'panel-drag', {\n panel: el,\n position: { x: newLeft, y: newTop },\n });\n };\n\n const onDragPointerUp = () => {\n if (!isDragging) return;\n isDragging = false;\n document.removeEventListener('pointermove', onDragPointerMove);\n document.removeEventListener('pointerup', onDragPointerUp);\n document.removeEventListener('pointercancel', onDragPointerUp);\n\n const pos = getPanelPosition();\n emitPanelEvent(container, 'panel-drag-end', {\n panel: el,\n position: pos,\n });\n };\n\n const onDragPointerDown = (e: PointerEvent) => {\n const target = e.target as HTMLElement;\n if (target.closest(INTERACTIVE_SELECTORS) || target.closest('.flow-panel-resize-handle')) {\n return;\n }\n\n isDragging = true;\n dragStartX = e.clientX;\n dragStartY = e.clientY;\n\n // Switch from CSS anchor positioning to explicit top/left\n const rect = el.getBoundingClientRect();\n const containerRect = container.getBoundingClientRect();\n elStartLeft = rect.left - containerRect.left;\n elStartTop = rect.top - containerRect.top;\n\n // Clear anchor positioning\n el.style.bottom = 'auto';\n el.style.right = 'auto';\n el.style.transform = 'none';\n el.style.left = `${elStartLeft}px`;\n el.style.top = `${elStartTop}px`;\n\n document.addEventListener('pointermove', onDragPointerMove);\n document.addEventListener('pointerup', onDragPointerUp);\n document.addEventListener('pointercancel', onDragPointerUp);\n\n emitPanelEvent(container, 'panel-drag-start', {\n panel: el,\n position: { x: elStartLeft, y: elStartTop },\n });\n };\n\n el.addEventListener('pointerdown', onDragPointerDown);\n\n // --- Resizable (bottom-right grip) ---\n if (canResize) {\n resizeHandle = document.createElement('div');\n resizeHandle.classList.add('flow-panel-resize-handle');\n el.appendChild(resizeHandle);\n\n let isResizing = false;\n let resizeStartX = 0;\n let resizeStartY = 0;\n let startWidth = 0;\n let startHeight = 0;\n\n const onResizePointerMove = (e: PointerEvent) => {\n if (!isResizing) return;\n const newWidth = Math.max(MIN_WIDTH, startWidth + (e.clientX - resizeStartX));\n const newHeight = Math.max(MIN_HEIGHT, startHeight + (e.clientY - resizeStartY));\n el.style.width = `${newWidth}px`;\n el.style.height = `${newHeight}px`;\n\n emitPanelEvent(container, 'panel-resize', {\n panel: el,\n dimensions: { width: newWidth, height: newHeight },\n });\n };\n\n const onResizePointerUp = () => {\n if (!isResizing) return;\n isResizing = false;\n document.removeEventListener('pointermove', onResizePointerMove);\n document.removeEventListener('pointerup', onResizePointerUp);\n document.removeEventListener('pointercancel', onResizePointerUp);\n\n emitPanelEvent(container, 'panel-resize-end', {\n panel: el,\n dimensions: { width: el.offsetWidth, height: el.offsetHeight },\n });\n };\n\n const onResizePointerDown = (e: PointerEvent) => {\n e.stopPropagation();\n isResizing = true;\n resizeStartX = e.clientX;\n resizeStartY = e.clientY;\n startWidth = el.offsetWidth;\n startHeight = el.offsetHeight;\n\n document.addEventListener('pointermove', onResizePointerMove);\n document.addEventListener('pointerup', onResizePointerUp);\n document.addEventListener('pointercancel', onResizePointerUp);\n\n emitPanelEvent(container, 'panel-resize-start', {\n panel: el,\n dimensions: { width: startWidth, height: startHeight },\n });\n };\n\n resizeHandle.addEventListener('pointerdown', onResizePointerDown);\n\n cleanup(() => {\n el.removeEventListener('pointerdown', onDragPointerDown);\n resizeHandle?.removeEventListener('pointerdown', onResizePointerDown);\n document.removeEventListener('pointermove', onDragPointerMove);\n document.removeEventListener('pointerup', onDragPointerUp);\n document.removeEventListener('pointercancel', onDragPointerUp);\n document.removeEventListener('pointermove', onResizePointerMove);\n document.removeEventListener('pointerup', onResizePointerUp);\n document.removeEventListener('pointercancel', onResizePointerUp);\n resizeHandle?.remove();\n el.removeEventListener('mousedown', stopProp);\n el.removeEventListener('pointerdown', stopProp);\n el.removeEventListener('wheel', stopProp);\n container.removeEventListener('flow-panel-reset', onReset);\n (container as any).__flowPanels?.delete(el);\n });\n } else {\n // Draggable but no resize\n cleanup(() => {\n el.removeEventListener('pointerdown', onDragPointerDown);\n document.removeEventListener('pointermove', onDragPointerMove);\n document.removeEventListener('pointerup', onDragPointerUp);\n document.removeEventListener('pointercancel', onDragPointerUp);\n el.removeEventListener('mousedown', stopProp);\n el.removeEventListener('pointerdown', stopProp);\n el.removeEventListener('wheel', stopProp);\n container.removeEventListener('flow-panel-reset', onReset);\n (container as any).__flowPanels?.delete(el);\n });\n }\n } else {\n // Fixed / static / fill — no drag\n cleanup(() => {\n el.removeEventListener('mousedown', stopProp);\n el.removeEventListener('pointerdown', stopProp);\n el.removeEventListener('wheel', stopProp);\n container.removeEventListener('flow-panel-reset', onReset);\n (container as any).__flowPanels?.delete(el);\n });\n }\n },\n );\n}\n","// ============================================================================\n// x-flow-node-toolbar Directive\n//\n// Floating toolbar that appears adjacent to a node. Positions itself outside\n// the node boundary and counter-scales against viewport zoom so it stays at\n// a fixed pixel size. Visibility is user-controlled via x-show / x-if.\n//\n// Usage:\n//
...
— top center (default)\n//
...
— bottom center\n//
...
— right side, aligned end\n//
...
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\ntype ToolbarPosition = 'top' | 'bottom' | 'left' | 'right';\ntype ToolbarAlign = 'start' | 'center' | 'end';\n\nexport function registerFlowNodeToolbarDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-node-toolbar',\n (\n el,\n { value, modifiers },\n { effect, cleanup },\n ) => {\n // ── Parse position and alignment from value ──────────────────────\n const position: ToolbarPosition = resolvePosition(value);\n const align: ToolbarAlign = resolveAlign(modifiers);\n\n // ── Base styling ─────────────────────────────────────────────────\n el.classList.add('flow-node-toolbar');\n el.style.position = 'absolute';\n\n // ── Prevent toolbar interactions from triggering node drag/select ─\n const stopPointerDown = (e: PointerEvent) => { e.stopPropagation(); };\n const stopClick = (e: MouseEvent) => { e.stopPropagation(); };\n el.addEventListener('pointerdown', stopPointerDown);\n el.addEventListener('click', stopClick);\n\n // ── Reactive positioning and inverse-scale ───────────────────────\n //\n // Position the toolbar at an anchor point on the node edge, then use\n // percentage-based CSS translate for centering. This avoids measuring\n // el.offsetWidth/Height (which returns 0 when hidden via x-show).\n effect(() => {\n const nodeEl = el.closest('[x-flow-node]') as HTMLElement | null;\n if (!nodeEl) return;\n\n const canvasEl = el.closest('[x-data]') as HTMLElement | null;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl);\n if (!canvas?.viewport) return;\n\n const zoom: number = canvas.viewport.zoom || 1;\n const offset = parseInt(el.getAttribute('data-flow-offset') ?? '10', 10);\n\n // Measure node dimensions (from reactive state or DOM)\n const nodeId = nodeEl.dataset.flowNodeId;\n const node = nodeId ? canvas.getNode(nodeId) : null;\n const nodeWidth = node?.dimensions?.width ?? nodeEl.offsetWidth;\n const nodeHeight = node?.dimensions?.height ?? nodeEl.offsetHeight;\n\n const scaledOffset = offset / zoom;\n\n // Compute anchor point on the node edge\n let anchorX: number;\n let anchorY: number;\n let tx: string;\n let ty: string;\n\n if (position === 'top' || position === 'bottom') {\n anchorY = position === 'top' ? -scaledOffset : nodeHeight + scaledOffset;\n ty = position === 'top' ? '-100%' : '0%';\n\n if (align === 'start') {\n anchorX = 0;\n tx = '0%';\n } else if (align === 'end') {\n anchorX = nodeWidth;\n tx = '-100%';\n } else {\n anchorX = nodeWidth / 2;\n tx = '-50%';\n }\n } else {\n anchorX = position === 'left' ? -scaledOffset : nodeWidth + scaledOffset;\n tx = position === 'left' ? '-100%' : '0%';\n\n if (align === 'start') {\n anchorY = 0;\n ty = '0%';\n } else if (align === 'end') {\n anchorY = nodeHeight;\n ty = '-100%';\n } else {\n anchorY = nodeHeight / 2;\n ty = '-50%';\n }\n }\n\n el.style.left = `${anchorX}px`;\n el.style.top = `${anchorY}px`;\n el.style.transformOrigin = '0 0';\n el.style.transform = `scale(${1 / zoom}) translate(${tx}, ${ty})`;\n });\n\n // ── Cleanup ──────────────────────────────────────────────────────\n cleanup(() => {\n el.removeEventListener('pointerdown', stopPointerDown);\n el.removeEventListener('click', stopClick);\n el.classList.remove('flow-node-toolbar');\n });\n },\n );\n}\n\nfunction resolvePosition(value: string): ToolbarPosition {\n if (value === 'bottom') return 'bottom';\n if (value === 'left') return 'left';\n if (value === 'right') return 'right';\n return 'top';\n}\n\nfunction resolveAlign(modifiers: string[]): ToolbarAlign {\n if (modifiers.includes('start')) return 'start';\n if (modifiers.includes('end')) return 'end';\n return 'center';\n}\n","// ============================================================================\n// x-flow-context-menu Directive\n//\n// Renders a positioned context menu overlay when a right-click event of the\n// matching type fires on the parent flow canvas. The directive manages its own\n// visibility, fixed-position placement, dismiss backdrop, scroll dismiss,\n// viewport overflow prevention, keyboard navigation, and ARIA roles.\n//\n// Modifiers:\n// .node — show on node right-click\n// .edge — show on edge right-click\n// .pane — show on pane (background) right-click\n// .selection — show on multi-selection right-click\n//\n// Expression (optional):\n// Evaluates to a config object with positioning offsets:\n// { offsetX: number, offsetY: number }\n//\n// The parent flowCanvas data exposes:\n// contextMenu.node — the right-clicked FlowNode (type 'node')\n// contextMenu.edge — the right-clicked FlowEdge (type 'edge')\n// contextMenu.position — flow-space XYPosition (type 'pane')\n// contextMenu.nodes — selected FlowNode[] (type 'selection')\n// closeContextMenu() — dismiss and reset\n//\n// Submenu classes (optional, enables keyboard navigation):\n// .flow-context-submenu — wrapper for a nested submenu\n// .flow-context-submenu-trigger — parent element that contains the submenu\n//\n// Usage:\n//
\n// \n//
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nexport function registerFlowContextMenuDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-context-menu',\n (\n el,\n { modifiers, expression },\n { effect, evaluate, cleanup },\n ) => {\n const type = modifiers[0]; // 'node', 'edge', 'pane', 'selection'\n if (!type) {\n console.warn('[AlpineFlow] x-flow-context-menu requires a type modifier: .node, .edge, .pane, or .selection');\n return;\n }\n\n const htmlEl = el as HTMLElement;\n const canvasEl = htmlEl.closest('[x-data]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n\n // Evaluate expression for offset config\n let offsetX = 0;\n let offsetY = 0;\n if (expression) {\n const config = evaluate(expression) as any;\n offsetX = config?.offsetX ?? 0;\n offsetY = config?.offsetY ?? 0;\n }\n\n // Set ARIA role on the menu element and make it focusable\n // so keyboard events are captured without highlighting a child item\n htmlEl.setAttribute('role', 'menu');\n htmlEl.setAttribute('tabindex', '-1');\n\n // Hide initially\n htmlEl.style.display = 'none';\n\n // Create shared dismiss backdrop (one per directive instance)\n const backdrop = document.createElement('div');\n backdrop.style.cssText = 'position:fixed;inset:0;z-index:4999;display:none;';\n canvasEl.appendChild(backdrop);\n\n /** Tracks the element that had focus before the menu opened */\n let previousFocus: HTMLElement | null = null;\n\n /** Viewport edge padding (px) */\n const EDGE_PAD = 4;\n\n const show = () => {\n previousFocus = document.activeElement as HTMLElement | null;\n const rawX = canvas.contextMenu.x + offsetX;\n const rawY = canvas.contextMenu.y + offsetY;\n\n // Make visible at initial position so we can measure\n htmlEl.style.display = '';\n htmlEl.style.position = 'fixed';\n htmlEl.style.left = rawX + 'px';\n htmlEl.style.top = rawY + 'px';\n htmlEl.style.zIndex = '5000';\n\n // Set ARIA roles on direct child interactive elements\n htmlEl.querySelectorAll(':scope > button, :scope > [role=\"menuitem\"]').forEach((child) => {\n child.setAttribute('role', 'menuitem');\n if (!child.hasAttribute('tabindex')) {\n child.setAttribute('tabindex', '-1');\n }\n });\n\n // Measure and clamp to viewport\n const rect = htmlEl.getBoundingClientRect();\n const vw = window.innerWidth;\n const vh = window.innerHeight;\n\n let clampedX = rawX;\n let clampedY = rawY;\n\n if (rect.right > vw - EDGE_PAD) {\n clampedX = vw - rect.width - EDGE_PAD;\n }\n if (rect.bottom > vh - EDGE_PAD) {\n clampedY = vh - rect.height - EDGE_PAD;\n }\n if (clampedX < EDGE_PAD) {\n clampedX = EDGE_PAD;\n }\n if (clampedY < EDGE_PAD) {\n clampedY = EDGE_PAD;\n }\n\n htmlEl.style.left = clampedX + 'px';\n htmlEl.style.top = clampedY + 'px';\n\n backdrop.style.display = '';\n\n // Focus the menu container so keyboard events are captured\n htmlEl.focus({ preventScroll: true });\n };\n\n const hide = () => {\n htmlEl.style.display = 'none';\n backdrop.style.display = 'none';\n if (previousFocus && document.contains(previousFocus)) {\n previousFocus.focus({ preventScroll: true });\n previousFocus = null;\n }\n };\n\n // React to contextMenu state changes\n effect(() => {\n const cm = canvas.contextMenu;\n if (cm.show && cm.type === type) {\n show();\n } else {\n hide();\n }\n });\n\n // Dismiss on backdrop click or right-click\n backdrop.addEventListener('click', () => canvas.closeContextMenu());\n backdrop.addEventListener('contextmenu', (e: MouseEvent) => {\n e.preventDefault();\n canvas.closeContextMenu();\n });\n\n // Dismiss on page scroll (capture phase to catch all scroll events)\n const onScroll = () => {\n if (canvas.contextMenu.show && canvas.contextMenu.type === type) {\n canvas.closeContextMenu();\n }\n };\n window.addEventListener('scroll', onScroll, true);\n\n // Keyboard navigation\n const getTopLevelItems = (): HTMLElement[] => {\n return Array.from(htmlEl.querySelectorAll(\n ':scope > button:not([disabled]), :scope > [role=\"menuitem\"]:not([disabled])',\n ));\n };\n\n const getSubmenuItems = (submenu: HTMLElement): HTMLElement[] => {\n return Array.from(submenu.querySelectorAll(\n 'button:not([disabled])',\n ));\n };\n\n const onMenuKeyDown = (e: KeyboardEvent) => {\n if (!canvas.contextMenu.show || canvas.contextMenu.type !== type) return;\n if (htmlEl.style.display === 'none') return;\n\n const active = document.activeElement as HTMLElement;\n\n // Determine if focus is inside a submenu\n const activeSubmenu = active?.closest('.flow-context-submenu') as HTMLElement | null;\n const items = activeSubmenu ? getSubmenuItems(activeSubmenu) : getTopLevelItems();\n if (items.length === 0) return;\n\n const currentIndex = items.indexOf(active);\n\n switch (e.key) {\n case 'ArrowDown': {\n e.preventDefault();\n const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;\n items[next].focus({ preventScroll: true });\n break;\n }\n case 'ArrowUp': {\n e.preventDefault();\n const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;\n items[prev].focus({ preventScroll: true });\n break;\n }\n case 'Tab': {\n e.preventDefault();\n if (e.shiftKey) {\n const prev = currentIndex > 0 ? currentIndex - 1 : items.length - 1;\n items[prev].focus({ preventScroll: true });\n } else {\n const next = currentIndex < items.length - 1 ? currentIndex + 1 : 0;\n items[next].focus({ preventScroll: true });\n }\n break;\n }\n case 'Enter':\n case ' ': {\n e.preventDefault();\n active?.click();\n break;\n }\n case 'ArrowRight': {\n // Open submenu if the active element has one\n if (!activeSubmenu) {\n const submenu = active?.querySelector('.flow-context-submenu');\n if (submenu) {\n e.preventDefault();\n const firstBtn = submenu.querySelector('button:not([disabled])');\n firstBtn?.focus({ preventScroll: true });\n }\n }\n break;\n }\n case 'ArrowLeft': {\n // Navigate back from submenu to parent trigger\n if (activeSubmenu) {\n e.preventDefault();\n const trigger = activeSubmenu.closest('.flow-context-submenu-trigger');\n trigger?.focus({ preventScroll: true });\n }\n break;\n }\n }\n };\n\n htmlEl.addEventListener('keydown', onMenuKeyDown);\n\n cleanup(() => {\n backdrop.remove();\n window.removeEventListener('scroll', onScroll, true);\n htmlEl.removeEventListener('keydown', onMenuKeyDown);\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-animate Directive\n//\n// Fires a one-shot animation in response to a DOM event or programmatic call.\n// Supports named animations (for playAnimation()), event modifiers, queuing,\n// auto-reverse, and once-only playback.\n//\n// Syntax:\n// \n// \n// \n// \n// \n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nexport function registerFlowCollapseDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-collapse',\n (el, { expression, modifiers }, { evaluate, effect, cleanup }) => {\n const isAll = modifiers.includes('all');\n const isExpand = modifiers.includes('expand');\n const isChildren = modifiers.includes('children');\n const isInstant = modifiers.includes('instant');\n\n const onClick = () => {\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas) return;\n\n if (isAll) {\n for (const node of canvas.nodes) {\n if (isExpand) {\n canvas.expandNode?.(node.id, { animate: !isInstant });\n } else {\n canvas.collapseNode?.(node.id, { animate: !isInstant });\n }\n }\n el.setAttribute('aria-expanded', String(isExpand));\n return;\n }\n\n if (isChildren && expression) {\n const parentId = evaluate(expression) as string;\n if (!parentId) return;\n for (const node of canvas.nodes) {\n if (node.parentId === parentId) {\n if (isExpand) {\n canvas.expandNode?.(node.id, { animate: !isInstant });\n } else {\n canvas.collapseNode?.(node.id, { animate: !isInstant });\n }\n }\n }\n el.setAttribute('aria-expanded', String(isExpand));\n return;\n }\n\n // Original single-node toggle behavior\n const nodeId = evaluate(expression) as string;\n if (!nodeId || !canvas?.toggleNode) return;\n canvas.toggleNode(nodeId, { animate: !isInstant });\n };\n\n el.addEventListener('click', onClick);\n el.setAttribute('data-flow-collapse', '');\n el.style.cursor = 'pointer';\n\n // Reactive ARIA attributes (only for single-node mode)\n if (!isAll && !isChildren) {\n effect(() => {\n const nodeId = evaluate(expression) as string;\n if (!nodeId) return;\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?.isCollapsed) return;\n\n const collapsed = canvas.isCollapsed(nodeId);\n el.setAttribute('aria-expanded', String(!collapsed));\n\n const nodeEl = el.closest('[x-flow-node]');\n if (nodeEl) {\n el.setAttribute('aria-controls', nodeEl.id || nodeId);\n }\n });\n }\n\n cleanup(() => {\n el.removeEventListener('click', onClick);\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-condense Directive\n//\n// Marks an element as a condense toggle for its nearest parent x-flow-node.\n// Clicking toggles the node's condensed state (summary view).\n//\n// Usage:\n// \n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nexport function registerFlowCondenseDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-condense',\n (el, { expression, modifiers }, { evaluate, effect, cleanup }) => {\n const onClick = () => {\n const nodeId = evaluate(expression) as string;\n if (!nodeId) return;\n\n const canvasEl = el.closest('[x-data]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?.toggleCondense) return;\n\n canvas.toggleCondense(nodeId);\n };\n\n el.addEventListener('click', onClick);\n el.setAttribute('data-flow-condense', '');\n el.style.cursor = 'pointer';\n\n // Reactive ARIA attributes\n effect(() => {\n const nodeId = evaluate(expression) as string;\n if (!nodeId) return;\n\n const canvasEl = el.closest('[x-data]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?.isCondensed) return;\n\n const condensed = canvas.isCondensed(nodeId);\n el.setAttribute('aria-expanded', String(!condensed));\n });\n\n cleanup(() => {\n el.removeEventListener('click', onClick);\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-schema Directive\n//\n// Renders a schema node: header + one row per field + per-row labelled handles.\n// Reads `node.data.label` and `node.data.fields` from the bound scope.\n//\n// Usage:\n//
\n//\n// The directive fully owns the element's children. Users who want custom\n// rendering should skip this directive and write x-for + handles manually.\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport type { FlowSchemaField, SchemaMetrics } from '../../core/types';\n\ntype SchemaData = { label?: string; fields?: FlowSchemaField[]; [k: string]: unknown };\ntype NodeRef = { data?: SchemaData } | undefined | null;\n\n/**\n * Remove every child from an element without using innerHTML.\n * Matches the XSS-safe clear pattern the security hook prefers.\n */\nfunction clearChildren(el: HTMLElement): void {\n while (el.firstChild) el.removeChild(el.firstChild);\n}\n\nexport function registerFlowSchemaDirective(Alpine: Alpine) {\n Alpine.directive('flow-schema', (el, _spec, { evaluate, effect, cleanup }) => {\n const host = el as HTMLElement;\n\n const readNode = (): NodeRef => {\n try {\n // Alpine re-throws expression errors asynchronously, so this catch\n // can't stop a missing/torn-down `node` scope from surfacing as an\n // uncaught \"node is not defined\" — guard the reference itself. See #21.\n return (evaluate(\"typeof node !== 'undefined' ? node : null\") as NodeRef) ?? null;\n } catch {\n return null;\n }\n };\n\n /**\n * Resolve whether this canvas has opted into drag-to-reorder on rows.\n * Read once per directive bind — the flag is not expected to change\n * between mounts. Walks to the closest `.flow-container` and reads\n * `canvas._config.rowsReorderable`.\n */\n const readRowsReorderable = (): boolean => {\n try {\n const canvasEl = host.closest('.flow-container') as HTMLElement | null;\n if (!canvasEl) return false;\n const canvas = (Alpine as any).$data?.(canvasEl);\n return !!canvas?._config?.rowsReorderable;\n } catch {\n return false;\n }\n };\n\n /**\n * Resolve whether this canvas has opted into keyboard field navigation\n * on rows. Reuses the existing `keyboardConnect` flag — consumers who\n * opt into keyboard-driven connect almost always also want keyboard row\n * focus, so we bundle the two.\n */\n const readKeyboardNav = (): boolean => {\n try {\n const canvasEl = host.closest('.flow-container') as HTMLElement | null;\n if (!canvasEl) return false;\n const canvas = (Alpine as any).$data?.(canvasEl);\n return !!canvas?._config?.keyboardConnect;\n } catch {\n return false;\n }\n };\n\n /**\n * Resolve the owning canvas Alpine scope (or null). Same `.flow-container`\n * walk as `readRowsReorderable` / `readKeyboardNav`.\n */\n const readCanvas = (): any | null => {\n try {\n const canvasEl = host.closest('.flow-container') as HTMLElement | null;\n if (!canvasEl) return null;\n return (Alpine as any).$data?.(canvasEl) ?? null;\n } catch {\n return null;\n }\n };\n\n /**\n * Measure header/row/handle geometry once per canvas, from the first\n * schema node that successfully renders **≥2 rows**. Two rows is the\n * minimum because `rowHeight` is a row STRIDE — the gap between two\n * consecutive row TOPS — and a single row gives nothing to stride\n * against: the theme drops `border-bottom` on `.flow-schema-row:last-child`,\n * so a lone row is one border SHORTER than a normal row and would be\n * mistaken for the stride, modelling every multi-row node on the canvas\n * one border too short per row. See the guard below.\n *\n * Schema rows are otherwise uniform, so a single measurement covers every\n * schema node on the canvas — later edge-geometry work reads this cache\n * instead of measuring handle DOM per edge. (Task G1 — state-derived\n * schema handle geometry.)\n *\n * `_schemaMetrics` is a plain, non-reactive field, but `canvas` here is\n * `Alpine.$data(el)` — Alpine's merge-scope proxy — and `Alpine.raw()`\n * does NOT unwrap that proxy; it returns the same proxy back (see\n * flow-canvas.ts:515-517 and flow-edge.ts:1134-1136). A property GET\n * through `Alpine.raw(canvas)` still forwards to the underlying reactive\n * object and calls Vue's `track()` when read inside an active effect, and\n * a SET still calls `trigger()`. Safety here does NOT come from\n * `Alpine.raw()` — it comes from calling this function OUTSIDE the\n * directive's `effect()`, via `Alpine.nextTick`, so `activeEffect` is\n * unset by the time the guard read, the `viewport.zoom` read, and the\n * `_schemaMetrics` write happen; none of them track or retrigger any\n * mounted schema node's render effect. The deliberate consequence: the\n * cache lands one tick after the first schema render, not synchronously\n * within it — edge code (a later task) falls back to the DOM path until\n * it lands, then upgrades.\n */\n const measureSchemaMetrics = (): void => {\n Alpine.nextTick(() => {\n const canvas = readCanvas();\n if (!canvas) return;\n const raw = Alpine.raw(canvas);\n if (raw._schemaMetrics != null) return;\n\n const headerEl = host.querySelector(':scope > .flow-schema-header');\n const schemaBodyEl = host.querySelector(':scope > .flow-schema-body');\n const rows = host.querySelectorAll('.flow-schema-row');\n\n // Measure from a node with ≥2 rows. The stride (`rowHeight`) is the gap between\n // two consecutive row TOPS, and `rowHeightLast` only means anything next to it —\n // the theme's `.flow-schema-row:last-child` rule drops the row's border-bottom, so\n // a SINGLE-row node has nothing to compare against and would report the shortened\n // height as the stride, modelling every multi-row node on the canvas one border too\n // short PER ROW. Skipping is free: this runs from every schema node's render, so the\n // next node with ≥2 rows lands the cache. If NO schema node has 2 rows, metrics\n // never land and edges keep using the DOM path — correct, and there was nothing to\n // save at one row a node anyway.\n if (rows.length < 2) return;\n\n const rowEl = rows[0];\n const secondRowEl = rows[1];\n const lastRowEl = rows[rows.length - 1];\n const handleEl = rowEl.querySelector('.flow-schema-handle');\n const lastHandleEl = lastRowEl.querySelector('.flow-schema-handle');\n if (!headerEl || !schemaBodyEl || !handleEl || !lastHandleEl) return;\n\n // Insets are consumed against the NODE's border box — edge code adds them\n // to `node.position` / `node.dimensions`, which describe the `.flow-node`\n // element. Anchor them there, not on `host`: under WireFlow's slot markup\n // (`
`)\n // the two are different elements and `host` sits one border-width inside\n // the node, which would shift every state-derived endpoint by that border.\n // In directive markup (`x-flow-node` + `x-flow-schema` on one element)\n // `closest` returns `host` itself and nothing changes.\n const nodeEl = (host.closest('[data-flow-node-id]') as HTMLElement | null) ?? host;\n\n const zoom = raw.viewport?.zoom || 1;\n const nodeRect = nodeEl.getBoundingClientRect();\n const headerRect = headerEl.getBoundingClientRect();\n const bodyRect = schemaBodyEl.getBoundingClientRect();\n const rowRect = rowEl.getBoundingClientRect();\n const secondRowRect = secondRowEl.getBoundingClientRect();\n const lastRowRect = lastRowEl.getBoundingClientRect();\n const handleRect = handleEl.getBoundingClientRect();\n const lastHandleRect = lastHandleEl.getBoundingClientRect();\n\n // The STRIDE between rows, not row 0's height — see SchemaMetrics.rowHeight.\n const rowHeight = (secondRowRect.top - rowRect.top) / zoom;\n const rowHeightLast = lastRowRect.height / zoom;\n // A zero stride/height means layout hasn't happened (or jsdom) — caching\n // zeros here would poison every later edge's endpoint geometry.\n if (rowHeight <= 0 || rowHeightLast <= 0) return;\n\n const metrics: SchemaMetrics = {\n headerHeight: headerRect.height / zoom,\n rowHeight,\n // NOT the same as `rowHeight` under the shipped theme — the last row loses\n // its border-bottom. See SchemaMetrics.rowHeightLast.\n rowHeightLast,\n // Where the handle actually sits inside its row. MEASURED, not `rowHeight / 2`:\n // `top: 50%` resolves against the row's PADDING box, which the theme's\n // border-bottom shrinks. See SchemaMetrics.handleOffsetY.\n handleOffsetY: (handleRect.top + handleRect.height / 2 - rowRect.top) / zoom,\n handleOffsetYLast:\n (lastHandleRect.top + lastHandleRect.height / 2 - lastRowRect.top) / zoom,\n insetLeft: (rowRect.left - nodeRect.left) / zoom,\n insetRight: (nodeRect.right - rowRect.right) / zoom,\n insetTop: (headerRect.top - nodeRect.top) / zoom,\n // Closes the row model: with insetBottom, a consumer can reconstruct the\n // node's expected border-box height and so DETECT non-uniform rows (a\n // wrapped field name — nothing in the CSS forces `white-space: nowrap`)\n // instead of assuming uniformity. See `flow-edge.ts`'s eligibility check.\n insetBottom: (nodeRect.bottom - bodyRect.bottom) / zoom,\n handleWidth: handleRect.width / zoom,\n handleHeight: handleRect.height / zoom,\n };\n raw._schemaMetrics = metrics;\n });\n };\n\n host.classList.add('flow-schema-node');\n\n // Mark the NODE element as schema-rendered, so edge code can tell a node whose\n // rows THIS directive laid out (uniform header + rows, the layout `SchemaMetrics`\n // describes) from a hand-rolled node that merely carries the same `data.fields`\n // shape and field-keyed handles — a pattern this file's own docblock invites, and\n // one whose row geometry is the consumer's business. State alone cannot tell them\n // apart, and getting it wrong renders the edge on the wrong row.\n //\n // The stamp goes on the NODE element, not on `host`: the two are the same element\n // under directive markup, but under WireFlow's slot markup `host` is a CHILD of\n // the node (the same insight behind the metrics-anchor `closest` above). Edge code\n // looks the element up via `canvas._nodeElements`, which registers node elements.\n //\n // Resolution order: try synchronously first. That covers WireFlow's slot markup,\n // where `host` is a CHILD of the node element and Alpine's parent-before-child tree\n // walk guarantees the ancestor's `data-flow-node-id` is already stamped by the time\n // this directive initializes. It also covers the common same-element case where\n // `x-flow-node` happens to run first.\n //\n // If synchronous resolution fails, we're in the SAME-ELEMENT case\n // (`
`) with `x-flow-schema` initializing BEFORE\n // `x-flow-node`'s own init-time effect has written `data-flow-node-id` — an\n // attribute-declaration-order dependency, not a markup shape. Defer through\n // `Alpine.nextTick`, exactly like `measureSchemaMetrics` above: by the next tick\n // every directive on this element (including `x-flow-node`) has already run, so\n // attribute order can no longer hide the marker. `destroyed` guards a nextTick\n // callback that fires after `cleanup()` from leaving a stale marker behind.\n let schemaNodeEl = host.closest('[data-flow-node-id]') as HTMLElement | null;\n let destroyed = false;\n if (schemaNodeEl) {\n schemaNodeEl.setAttribute('data-flow-schema-node', '');\n } else {\n Alpine.nextTick(() => {\n if (destroyed || !host.isConnected) return;\n schemaNodeEl = host.closest('[data-flow-node-id]') as HTMLElement | null;\n schemaNodeEl?.setAttribute('data-flow-schema-node', '');\n });\n }\n\n // Persistent scaffold + keyed row registry. Reused across renders so that a\n // single tracked change reconciles rows in place instead of tearing down and\n // re-initializing every row (one rename keystroke used to destroy + re-stamp\n // ~180 elements per node).\n let headerEl: HTMLElement | null = null;\n let bodyEl: HTMLElement | null = null;\n const rowByName = new Map();\n\n const ensureScaffold = (): void => {\n if (headerEl && bodyEl) return;\n clearChildren(host);\n rowByName.clear();\n headerEl = document.createElement('div');\n headerEl.className = 'flow-schema-header';\n host.appendChild(headerEl);\n bodyEl = document.createElement('div');\n bodyEl.className = 'flow-schema-body';\n host.appendChild(bodyEl);\n };\n\n const render = (): void => {\n const node = readNode() as (NodeRef & { id?: unknown }) | null;\n const data = node?.data;\n if (!data) {\n // Tear the rows down explicitly. Only individual rows carry Alpine's\n // init marker (initTree is per-row), so removing the unmarked bodyEl\n // would NOT cascade Alpine's auto-cleanup to them — the row scopes would\n // leak, still subscribed to reactive state.\n for (const row of rowByName.values()) {\n Alpine.destroyTree(row);\n }\n rowByName.clear();\n clearChildren(host);\n headerEl = null;\n bodyEl = null;\n return;\n }\n\n ensureScaffold();\n\n const label = typeof data.label === 'string' ? data.label : '';\n const fields = Array.isArray(data.fields) ? data.fields : [];\n const nodeId = typeof node?.id === 'string' ? node.id : '';\n\n if (typeof data.kind === 'string' && data.kind) {\n host.setAttribute('data-flow-schema-kind', data.kind);\n } else {\n host.removeAttribute('data-flow-schema-kind');\n }\n\n if (headerEl!.textContent !== label) headerEl!.textContent = label;\n\n const rowsReorderable = readRowsReorderable();\n const keyboardNav = readKeyboardNav();\n\n // ── Reconcile rows keyed by field.name ──\n // 1. Create missing rows / update surviving rows in place.\n const seen = new Set();\n for (const field of fields) {\n seen.add(field.name);\n const existing = rowByName.get(field.name);\n if (existing) {\n updateRow(existing, field);\n } else {\n const row = renderRow(field, nodeId, rowsReorderable, keyboardNav);\n rowByName.set(field.name, row);\n bodyEl!.appendChild(row);\n // Activate x-flow-handle + x-flow-row-select on this one new row.\n Alpine.initTree(row);\n }\n }\n // 2. Destroy rows whose fields are gone.\n for (const [name, row] of rowByName) {\n if (!seen.has(name)) {\n Alpine.destroyTree(row);\n row.remove();\n rowByName.delete(name);\n }\n }\n // 3. Order rows to match `fields`, moving only those out of position\n // (a straight append-all would churn every row's DOM position each\n // render; moving a node preserves its listeners and Alpine state).\n let cursor: ChildNode | null = bodyEl!.firstChild;\n for (const field of fields) {\n const row = rowByName.get(field.name);\n if (!row) continue;\n if (cursor === row) {\n cursor = cursor.nextSibling;\n } else {\n bodyEl!.insertBefore(row, cursor);\n }\n }\n\n measureSchemaMetrics();\n };\n\n /**\n * Update the presentation of a surviving row in place — key/required\n * classes, the optional icon span, and the name/type text — WITHOUT\n * touching the row's handles. Handles are keyed by field.name (the row's\n * reconcile key), so a surviving row's handles are already correct.\n */\n const updateRow = (row: HTMLElement, field: FlowSchemaField): void => {\n if (row.dataset.flowSchemaField !== field.name) {\n row.dataset.flowSchemaField = field.name;\n }\n row.classList.toggle('flow-schema-row--pk', field.key === 'primary');\n row.classList.toggle('flow-schema-row--fk', field.key === 'foreign');\n row.classList.toggle('flow-schema-row--required', !!field.required);\n\n // Icon span: renderRow inserts it (between the target handle and the name)\n // only when field.icon is set — keep it in sync as icons come and go.\n let icon = row.querySelector('.flow-schema-row-icon');\n const nameEl = row.querySelector('.flow-schema-row-name');\n if (field.icon) {\n if (!icon) {\n icon = document.createElement('span');\n icon.className = 'flow-schema-row-icon';\n row.insertBefore(icon, nameEl);\n }\n if (icon.textContent !== field.icon) icon.textContent = field.icon;\n } else if (icon) {\n icon.remove();\n }\n\n if (nameEl && nameEl.textContent !== field.name) nameEl.textContent = field.name;\n const typeEl = row.querySelector('.flow-schema-row-type');\n if (typeEl && typeEl.textContent !== field.type) typeEl.textContent = field.type;\n };\n\n const renderRow = (\n field: FlowSchemaField,\n nodeId: string,\n rowsReorderable: boolean,\n keyboardNav: boolean,\n ): HTMLElement => {\n const row = document.createElement('div');\n row.className = 'flow-schema-row';\n row.dataset.flowSchemaField = field.name;\n if (field.key === 'primary') row.classList.add('flow-schema-row--pk');\n if (field.key === 'foreign') row.classList.add('flow-schema-row--fk');\n if (field.required) row.classList.add('flow-schema-row--required');\n\n // Row-select wiring. Stamps the x-flow-row-select directive with\n // the `nodeId.fieldName` convention the schema inspector scaffolding\n // expects. Without this, clicking a row would never populate\n // canvas.selectedRows and the row-scope inspector slot stays empty.\n if (nodeId) {\n row.setAttribute(\n 'x-flow-row-select',\n JSON.stringify(`${nodeId}.${field.name}`),\n );\n }\n\n // Drag-to-reorder opt-in. When `canvas._config.rowsReorderable` is\n // truthy, stamp `x-schema-reorderable` on each row — the directive is\n // registered by the schema addon and activated via Alpine.initTree(body)\n // below.\n if (rowsReorderable) {\n row.setAttribute('x-schema-reorderable', '');\n }\n\n // Keyboard field navigation opt-in. When `canvas._config.keyboardConnect`\n // is truthy, stamp `x-schema-keyboard-nav` on each row so the directive\n // can attach tabindex/role/aria + key handlers. Directive is registered\n // by the schema addon; activation happens via Alpine.initTree(body)\n // below.\n if (keyboardNav && nodeId) {\n row.setAttribute(\n 'x-schema-keyboard-nav',\n JSON.stringify(`${nodeId}.${field.name}`),\n );\n }\n\n // Target handle (left). We set the x-flow-handle directive attribute\n // so that after Alpine.initTree(host) runs below, the handle's\n // pointerdown → drag-to-connect pipeline activates on this element.\n // Without the directive, the div has the right classes + data-attrs\n // but no pointer handlers — clicks fall through to the node drag.\n const target = document.createElement('div');\n target.className = 'flow-schema-handle flow-schema-handle--target';\n target.setAttribute('x-flow-handle:target.left', JSON.stringify(field.name));\n row.appendChild(target);\n\n if (field.icon) {\n const icon = document.createElement('span');\n icon.className = 'flow-schema-row-icon';\n icon.textContent = field.icon;\n row.appendChild(icon);\n }\n\n const name = document.createElement('span');\n name.className = 'flow-schema-row-name';\n name.textContent = field.name;\n row.appendChild(name);\n\n const type = document.createElement('span');\n type.className = 'flow-schema-row-type';\n type.textContent = field.type;\n row.appendChild(type);\n\n // Source handle (right) — same directive pattern as target.\n const source = document.createElement('div');\n source.className = 'flow-schema-handle flow-schema-handle--source';\n source.setAttribute('x-flow-handle:source.right', JSON.stringify(field.name));\n row.appendChild(source);\n\n // Mirror handles — invisible, non-interactive copies of the real\n // target/source on the OPPOSITE side of the row. They share the same\n // (id, type) with their real counterparts so the edge-geometry picker\n // (see flow-edge.ts `pickClosestHandle`) can route an edge to whichever\n // side is physically closer to the other endpoint. CSS gives them\n // `visibility: hidden; pointer-events: none`, but they remain in the\n // layout so `getBoundingClientRect` returns a measurable position.\n const mirrorTarget = document.createElement('div');\n mirrorTarget.className =\n 'flow-schema-handle flow-schema-handle--target flow-schema-handle--mirror';\n mirrorTarget.setAttribute('x-flow-handle:target.right', JSON.stringify(field.name));\n row.appendChild(mirrorTarget);\n\n const mirrorSource = document.createElement('div');\n mirrorSource.className =\n 'flow-schema-handle flow-schema-handle--source flow-schema-handle--mirror';\n mirrorSource.setAttribute('x-flow-handle:source.left', JSON.stringify(field.name));\n row.appendChild(mirrorSource);\n\n return row;\n };\n\n // Reactive: effect() fires on init and on any reactive read touched inside.\n effect(() => {\n // Skip detached elements: a Livewire morph (or teardown) can flush this\n // effect after the node element leaves the DOM, and evaluate('node') then\n // throws because the Alpine scope is gone. See issue #21.\n if (!host.isConnected) return;\n\n // Touch label + field properties so Alpine subscribes to mutations.\n const data = readNode()?.data;\n void data?.label;\n void data?.kind;\n const fields = data?.fields;\n if (Array.isArray(fields)) {\n for (const f of fields) {\n void f.name;\n void f.type;\n void f.key;\n void f.required;\n void f.icon;\n }\n }\n render();\n });\n\n cleanup(() => {\n destroyed = true;\n for (const row of rowByName.values()) {\n Alpine.destroyTree(row);\n }\n rowByName.clear();\n clearChildren(host);\n headerEl = null;\n bodyEl = null;\n host.classList.remove('flow-schema-node');\n schemaNodeEl?.removeAttribute('data-flow-schema-node');\n });\n });\n}\n","// ============================================================================\n// x-flow-wait Directive\n//\n// Renders a wait node: header (icon? + label + formatted duration), top target\n// handle, bottom source handle. Reads `node.data.durationMs`,\n// `node.data.label`, and `node.data.icon` from the bound scope.\n//\n// Usage:\n//
\n//\n// The directive fully owns the element's children. Consumers who want\n// custom rendering should skip this directive and render manually.\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\ntype WaitData = {\n durationMs?: number;\n label?: string;\n icon?: string;\n [k: string]: unknown;\n};\ntype NodeRef = { id?: unknown; data?: WaitData } | undefined | null;\n\n/**\n * Format a duration in milliseconds as a compact human-readable string:\n * <1s → \"{n}ms\" (e.g. 500ms)\n * <1m → \"{n}s\" / \"{n.n}s\" (e.g. 3s, 2.5s)\n * else → \"{n}m\" / \"{n}m {s}s\" (e.g. 2m, 1m 30s)\n */\nexport function formatWaitDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return '';\n if (ms < 1000) return `${ms}ms`;\n if (ms < 60_000) {\n const seconds = ms / 1000;\n return ms % 1000 === 0 ? `${seconds}s` : `${seconds.toFixed(1)}s`;\n }\n const minutes = Math.floor(ms / 60_000);\n const seconds = Math.floor((ms % 60_000) / 1000);\n return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;\n}\n\n/** Remove every child from an element without using innerHTML (XSS-safe). */\nfunction clearChildren(el: HTMLElement): void {\n while (el.firstChild) el.removeChild(el.firstChild);\n}\n\nexport function registerFlowWaitDirective(Alpine: Alpine) {\n Alpine.directive('flow-wait', (el, _spec, { evaluate, effect, cleanup }) => {\n const host = el as HTMLElement;\n\n const readNode = (): NodeRef => {\n try {\n // Alpine re-throws expression errors asynchronously, so this\n // catch can't stop a missing/torn-down `node` scope from\n // surfacing as an uncaught \"node is not defined\". See issue #21.\n return (evaluate(\"typeof node !== 'undefined' ? node : null\") as NodeRef) ?? null;\n } catch {\n return null;\n }\n };\n\n host.classList.add('flow-wait-node');\n host.setAttribute('data-flow-wait', 'true');\n\n const render = (): void => {\n clearChildren(host);\n\n const node = readNode();\n const data = node?.data;\n if (!data) return;\n\n const label = typeof data.label === 'string' && data.label ? data.label : 'Wait';\n const icon = typeof data.icon === 'string' && data.icon ? data.icon : '';\n const ms = typeof data.durationMs === 'number' ? data.durationMs : NaN;\n\n const header = document.createElement('div');\n header.className = 'flow-wait-header';\n\n if (icon) {\n const iconEl = document.createElement('span');\n iconEl.className = 'flow-wait-icon';\n iconEl.textContent = icon;\n header.appendChild(iconEl);\n }\n\n const labelEl = document.createElement('span');\n labelEl.className = 'flow-wait-label';\n labelEl.textContent = label;\n header.appendChild(labelEl);\n\n const durationEl = document.createElement('span');\n durationEl.className = 'flow-wait-duration';\n durationEl.textContent = formatWaitDuration(ms);\n header.appendChild(durationEl);\n\n host.appendChild(header);\n\n const target = document.createElement('div');\n target.className = 'flow-wait-handle flow-wait-handle--target';\n target.setAttribute('x-flow-handle:target.top', JSON.stringify('in'));\n host.appendChild(target);\n\n const source = document.createElement('div');\n source.className = 'flow-wait-handle flow-wait-handle--source';\n source.setAttribute('x-flow-handle:source.bottom', JSON.stringify('out'));\n host.appendChild(source);\n\n // Activate the freshly stamped x-flow-handle directives.\n Alpine.initTree(host);\n };\n\n effect(() => {\n // Skip detached elements: a Livewire morph (or teardown) can flush\n // this effect after the node element leaves the DOM, and\n // evaluate('node') then throws (scope gone). See issue #21.\n if (!host.isConnected) return;\n\n // Touch reactive data so Alpine subscribes to changes.\n const data = readNode()?.data;\n void data?.durationMs;\n void data?.label;\n void data?.icon;\n render();\n });\n\n cleanup(() => {\n clearChildren(host);\n host.classList.remove('flow-wait-node');\n host.removeAttribute('data-flow-wait');\n });\n });\n}\n","// ============================================================================\n// alpineflow/workflow — prettyPrintCondition\n//\n// Pure helper: turns a FlowCondition descriptor into a compact human-readable\n// string for the condition-node body. Mirrored on the PHP side by\n// FlowConditionNode::prettyPrintCondition() so the SSR fallback matches the\n// runtime render.\n//\n// Usage:\n// prettyPrintCondition({ field: 'plan', op: 'equals', value: 'annual' })\n// → \"plan == 'annual'\"\n// ============================================================================\n\nexport interface FlowConditionShape {\n field: string;\n op: string;\n value?: unknown;\n}\n\nconst OP_MAP: Record = {\n equals: '==',\n notEquals: '!=',\n greaterThan: '>',\n lessThan: '<',\n greaterThanOrEqual: '>=',\n lessThanOrEqual: '<=',\n};\n\nfunction formatValue(v: unknown): string {\n if (v === null) return 'null';\n if (v === undefined) return 'null';\n if (typeof v === 'string') return `'${v}'`;\n if (Array.isArray(v)) return `[${v.map(formatValue).join(', ')}]`;\n return String(v);\n}\n\nexport function prettyPrintCondition(condition: FlowConditionShape): string {\n const { field, op, value } = condition;\n if (op in OP_MAP) {\n return `${field} ${OP_MAP[op]} ${formatValue(value)}`;\n }\n if (op === 'in') return `${field} in ${formatValue(value)}`;\n if (op === 'notIn') return `${field} not in ${formatValue(value)}`;\n if (op === 'exists') return `${field} exists`;\n if (op === 'matches') return `${field} ~ /${String(value)}/`;\n return `${field} ${op} ${formatValue(value)}`;\n}\n","// ============================================================================\n// x-flow-condition Directive\n//\n// Renders a workflow condition node:\n// - .flow-condition-node host\n// - .flow-condition-header with node.data.label (default \"Condition\")\n// - .flow-condition-body with prettyPrintCondition(condition) OR\n// evaluateLabel / \"[custom evaluator]\" when node.data.evaluate is used\n// - 3 handles: target + true + false (sourceHandle stamped on each)\n//\n// Reactive: re-renders when node.data.condition / label / evaluate /\n// evaluateLabel / direction / _branchTaken mutate. The host is also\n// stamped with `data-flow-condition-direction` and (when set)\n// `data-flow-condition-branch-taken` so themes can decorate the chosen\n// branch and mute the other.\n//\n// Usage:\n//
\n//\n// The directive value (Alpine expression) is the direction — defaults to\n// 'horizontal'. Pass `'vertical'` to flip the layout.\n//\n// XSS-safe: all dynamic text uses textContent; no innerHTML anywhere.\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\nimport { prettyPrintCondition } from '../../workflow/condition-pretty-print';\n\ntype ConditionData = {\n label?: string;\n condition?: { field: string; op: string; value?: unknown };\n evaluate?: (...args: any[]) => unknown;\n evaluateLabel?: string;\n direction?: 'horizontal' | 'vertical';\n _branchTaken?: 'true' | 'false' | string | null;\n [k: string]: unknown;\n};\ntype NodeRef = { id?: unknown; data?: ConditionData } | undefined | null;\n\nfunction clearChildren(el: HTMLElement): void {\n while (el.firstChild) el.removeChild(el.firstChild);\n}\n\nfunction resolveDirection(value: unknown, fallback: ConditionData['direction']): 'horizontal' | 'vertical' {\n if (value === 'vertical' || value === 'horizontal') return value;\n if (fallback === 'vertical' || fallback === 'horizontal') return fallback;\n return 'horizontal';\n}\n\nexport function registerFlowConditionDirective(Alpine: Alpine) {\n Alpine.directive('flow-condition', (el, { expression }, { evaluate, effect, cleanup }) => {\n const host = el as HTMLElement;\n\n const readNode = (): NodeRef => {\n try {\n // Alpine re-throws expression errors asynchronously, so this\n // catch can't stop a missing/torn-down `node` scope from\n // surfacing as an uncaught \"node is not defined\". See issue #21.\n return (evaluate(\"typeof node !== 'undefined' ? node : null\") as NodeRef) ?? null;\n } catch {\n return null;\n }\n };\n\n const evaluateDirective = (): unknown => {\n if (!expression) return undefined;\n try {\n return evaluate(expression);\n } catch {\n return expression;\n }\n };\n\n host.classList.add('flow-condition-node');\n\n const render = (): void => {\n const node = readNode();\n const data = node?.data ?? {};\n\n const direction = resolveDirection(evaluateDirective(), data.direction);\n host.setAttribute('data-flow-condition-direction', direction);\n\n const branchTaken = data._branchTaken;\n if (branchTaken === 'true' || branchTaken === 'false') {\n host.setAttribute('data-flow-condition-branch-taken', branchTaken);\n } else {\n host.removeAttribute('data-flow-condition-branch-taken');\n }\n\n clearChildren(host);\n\n const label = typeof data.label === 'string' && data.label ? data.label : 'Condition';\n const header = document.createElement('div');\n header.className = 'flow-condition-header';\n header.textContent = label;\n host.appendChild(header);\n\n const body = document.createElement('div');\n body.className = 'flow-condition-body';\n if (data.condition && typeof data.condition === 'object') {\n body.textContent = prettyPrintCondition(data.condition as any);\n } else if (typeof data.evaluate === 'function') {\n body.textContent = typeof data.evaluateLabel === 'string' && data.evaluateLabel\n ? data.evaluateLabel\n : '[custom evaluator]';\n } else {\n body.textContent = '';\n }\n host.appendChild(body);\n\n const target = document.createElement('div');\n target.className = 'flow-condition-handle-target';\n target.setAttribute('data-flow-handle-direction', 'target');\n target.setAttribute('x-flow-handle:target', JSON.stringify('in'));\n host.appendChild(target);\n\n const trueHandle = document.createElement('div');\n trueHandle.className = 'flow-condition-handle-source flow-condition-handle--true';\n trueHandle.setAttribute('data-flow-handle-direction', 'source');\n trueHandle.setAttribute('data-source-handle', 'true');\n trueHandle.setAttribute('x-flow-handle:source', JSON.stringify('true'));\n host.appendChild(trueHandle);\n\n const falseHandle = document.createElement('div');\n falseHandle.className = 'flow-condition-handle-source flow-condition-handle--false';\n falseHandle.setAttribute('data-flow-handle-direction', 'source');\n falseHandle.setAttribute('data-source-handle', 'false');\n falseHandle.setAttribute('x-flow-handle:source', JSON.stringify('false'));\n host.appendChild(falseHandle);\n\n // Activate freshly-stamped x-flow-handle directives.\n Alpine.initTree(host);\n };\n\n effect(() => {\n // Skip detached elements: a Livewire morph (or teardown) can flush\n // this effect after the node element leaves the DOM, and\n // evaluate('node') then throws (scope gone). See issue #21.\n if (!host.isConnected) return;\n\n const data = readNode()?.data;\n // Touch reactive fields so Alpine subscribes.\n void data?.label;\n void data?.condition;\n void (data?.condition as any)?.field;\n void (data?.condition as any)?.op;\n void (data?.condition as any)?.value;\n void data?.evaluate;\n void data?.evaluateLabel;\n void data?.direction;\n void data?._branchTaken;\n render();\n });\n\n cleanup(() => {\n clearChildren(host);\n host.classList.remove('flow-condition-node');\n host.removeAttribute('data-flow-condition-direction');\n host.removeAttribute('data-flow-condition-branch-taken');\n });\n });\n}\n","// ============================================================================\n// x-flow-row-select Directive\n//\n// Marks a row element as selectable. Clicking selects the row, shift-clicking\n// toggles without clearing other selections (multi-select).\n//\n// Usage:\n//
Row content
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nexport function registerFlowRowSelectDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-row-select',\n (el, { expression }, { evaluate, effect, cleanup }) => {\n el.classList.add('nodrag');\n el.style.cursor = 'pointer';\n el.setAttribute('data-flow-row-select', '');\n\n const onClick = (e: MouseEvent) => {\n e.stopPropagation();\n\n const rowId = evaluate(expression) as string;\n if (!rowId) return;\n\n const canvasEl = el.closest('[x-data]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?.toggleRowSelect) return;\n\n if (e.shiftKey) {\n // Shift+click: toggle this row without clearing others\n canvas.toggleRowSelect(rowId);\n } else {\n // Normal click: select only this row\n canvas.deselectAllRows();\n canvas.selectRow(rowId);\n }\n };\n\n el.addEventListener('click', onClick);\n\n // Reactive CSS class and ARIA\n effect(() => {\n const rowId = evaluate(expression) as string;\n if (!rowId) return;\n\n const canvasEl = el.closest('[x-data]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?.isRowSelected) return;\n\n const selected = canvas.isRowSelected(rowId);\n el.classList.toggle('flow-row-selected', selected);\n el.setAttribute('aria-selected', String(selected));\n });\n\n cleanup(() => {\n el.removeEventListener('click', onClick);\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-detail Directive\n//\n// Shows/hides elements based on the canvas zoom level. Use modifiers to\n// specify which level(s) the element should be visible at, or an expression\n// with { min, max } for custom zoom thresholds.\n//\n// Usage:\n//
Full content — visible at close zoom
\n//
Label only — visible at medium zoom
\n//
Dot — visible when zoomed far out
\n//
Visible at medium AND close
\n//
Show above 0.8x zoom
\n//
Show between 0.5x and 1.2x
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nexport function registerFlowDetailDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-detail',\n (el, { expression, modifiers }, { evaluate, effect, cleanup }) => {\n // Custom zoom thresholds via expression: { min?: number, max?: number }\n if (expression) {\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?.viewport) return;\n\n const originalDisplay = el.style.display;\n\n effect(() => {\n const thresholds = evaluate(expression) as { min?: number; max?: number };\n const zoom = canvas.viewport.zoom;\n const aboveMin = thresholds.min === undefined || zoom >= thresholds.min;\n const belowMax = thresholds.max === undefined || zoom <= thresholds.max;\n el.style.display = (aboveMin && belowMax) ? originalDisplay : 'none';\n });\n\n cleanup(() => {\n el.style.display = originalDisplay;\n });\n\n return;\n }\n\n // Modifier-based preset levels\n const levels: Set = new Set(modifiers.filter(m => m === 'far' || m === 'medium' || m === 'close'));\n if (levels.size === 0) return;\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?._zoomLevel) return;\n\n const originalDisplay = el.style.display;\n\n effect(() => {\n const currentLevel = canvas._zoomLevel as string;\n if (levels.has(currentLevel)) {\n el.style.display = originalDisplay;\n } else {\n el.style.display = 'none';\n }\n });\n\n cleanup(() => {\n el.style.display = originalDisplay;\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-devtools Directive\n//\n// Configurable devtools overlay for AlpineFlow canvases. Shows performance\n// metrics, event log, viewport state, selection inspector, and animation\n// activity. Replaces the old Blade component.\n//\n// Modifiers (shorthand):\n// x-flow-devtools — all sections\n// x-flow-devtools.perf — performance only\n// x-flow-devtools.perf.events — performance + events\n// x-flow-devtools.events.viewport.activity — events + viewport + activity\n//\n// Expression (granular):\n// x-flow-devtools=\"{ perf: ['fps', 'counts'], events: { max: 50 } }\"\n//\n// Precedence: expression wins over modifiers. No expression + no modifiers = all.\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\ninterface DevtoolsConfig {\n perf?: boolean | ('fps' | 'memory' | 'counts' | 'visible')[];\n events?: boolean | { max?: number };\n viewport?: boolean;\n state?: boolean;\n activity?: boolean;\n}\n\ntype PerfItem = 'fps' | 'memory' | 'counts' | 'visible';\n\nconst ALL_SECTIONS: (keyof DevtoolsConfig)[] = ['perf', 'events', 'viewport', 'state', 'activity'];\nconst ALL_PERF_ITEMS: PerfItem[] = ['fps', 'memory', 'counts', 'visible'];\nconst DEFAULT_EVENT_MAX = 30;\n\n// ── Config Resolution ────────────────────────────────────────────────────────\n\nfunction resolveConfig(expression: DevtoolsConfig | null, modifiers: string[]): DevtoolsConfig {\n // Expression takes precedence\n if (expression && typeof expression === 'object' && Object.keys(expression).length > 0) {\n return expression;\n }\n\n // Modifiers → config\n const sectionMods = modifiers.filter(m => ALL_SECTIONS.includes(m as keyof DevtoolsConfig));\n\n // No modifiers = all sections\n if (sectionMods.length === 0) {\n return { perf: true, events: true, viewport: true, state: true, activity: true };\n }\n\n const config: DevtoolsConfig = {};\n for (const mod of sectionMods) {\n config[mod as keyof DevtoolsConfig] = true;\n }\n return config;\n}\n\nfunction shouldShowPerf(config: DevtoolsConfig): PerfItem[] {\n if (!config.perf) return [];\n if (config.perf === true) return [...ALL_PERF_ITEMS];\n return config.perf.filter(item => ALL_PERF_ITEMS.includes(item));\n}\n\nfunction getEventMax(config: DevtoolsConfig): number {\n if (!config.events) return 0;\n if (config.events === true) return DEFAULT_EVENT_MAX;\n return config.events.max ?? DEFAULT_EVENT_MAX;\n}\n\n// ── DOM Helpers ──────────────────────────────────────────────────────────────\n\nfunction createSection(title: string, className: string): { wrapper: HTMLDivElement; content: HTMLDivElement } {\n const wrapper = document.createElement('div');\n wrapper.className = `flow-devtools-section ${className}`;\n const header = document.createElement('div');\n header.className = 'flow-devtools-section-title';\n header.textContent = title;\n wrapper.appendChild(header);\n const content = document.createElement('div');\n content.className = 'flow-devtools-section-content';\n wrapper.appendChild(content);\n return { wrapper, content };\n}\n\nfunction createRow(label: string, className: string): { row: HTMLDivElement; valueEl: HTMLSpanElement } {\n const row = document.createElement('div');\n row.className = `flow-devtools-row ${className}`;\n const labelEl = document.createElement('span');\n labelEl.className = 'flow-devtools-label';\n labelEl.textContent = label;\n const valueEl = document.createElement('span');\n valueEl.className = 'flow-devtools-value';\n valueEl.textContent = '—';\n row.appendChild(labelEl);\n row.appendChild(valueEl);\n return { row, valueEl };\n}\n\n// ── Directive Registration ───────────────────────────────────────────────────\n\nexport function registerFlowDevtoolsDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-devtools',\n (el: HTMLElement, { expression, modifiers }: { expression: string; modifiers: string[] }, { evaluate, effect, cleanup }: { evaluate: (expr: string) => any; effect: (fn: () => void) => void; cleanup: (fn: () => void) => void }) => {\n\n // ── Resolve config ─────────────────────────────────────────────\n let parsed: DevtoolsConfig | null = null;\n if (expression) {\n try { parsed = evaluate(expression) as DevtoolsConfig; } catch { /* use modifiers */ }\n }\n const config = resolveConfig(parsed, modifiers);\n\n // ── Find canvas scope ──────────────────────────────────────────\n // Use the same pattern as other directives: walk up to the nearest\n // [x-data] element and get Alpine reactive data from it.\n const canvasEl = el.closest('[x-data]') as HTMLElement | null;\n if (!canvasEl) return;\n\n // The .flow-container element is used for event listening\n const container = el.closest('.flow-container') as HTMLElement | null;\n if (!container) return;\n\n // ── Build DOM ──────────────────────────────────────────────────\n el.classList.add('flow-devtools', 'canvas-overlay');\n el.setAttribute('data-flow-devtools', '');\n\n // Stop canvas zoom on scroll over devtools panel\n const stopWheel = (e: Event) => e.stopPropagation();\n el.addEventListener('wheel', stopWheel);\n\n // Toggle button\n const toggleBtn = document.createElement('button');\n toggleBtn.className = 'flow-devtools-toggle nopan';\n toggleBtn.title = 'Devtools';\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n svg.setAttribute('width', '14');\n svg.setAttribute('height', '14');\n svg.setAttribute('viewBox', '0 0 24 24');\n svg.setAttribute('fill', 'none');\n svg.setAttribute('stroke', 'currentColor');\n svg.setAttribute('stroke-width', '2');\n svg.setAttribute('stroke-linecap', 'round');\n svg.setAttribute('stroke-linejoin', 'round');\n const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');\n polyline.setAttribute('points', '22 12 18 12 15 21 9 3 6 12 2 12');\n svg.appendChild(polyline);\n toggleBtn.appendChild(svg);\n el.appendChild(toggleBtn);\n\n // Panel — user-select: none so dragging to pan doesn't select text\n const panel = document.createElement('div');\n panel.className = 'flow-devtools-panel';\n panel.style.display = 'none';\n panel.style.userSelect = 'none';\n el.appendChild(panel);\n\n let expanded = false;\n const onToggleClick = () => {\n expanded = !expanded;\n panel.style.display = expanded ? '' : 'none';\n toggleBtn.title = expanded ? 'Collapse' : 'Devtools';\n // Start/stop RAF loop based on visibility\n if (expanded) startRaf(); else stopRaf();\n };\n toggleBtn.addEventListener('click', onToggleClick);\n\n // ── Perf section ───────────────────────────────────────────────\n const perfItems = shouldShowPerf(config);\n let fpsValueEl: HTMLSpanElement | null = null;\n let memoryValueEl: HTMLSpanElement | null = null;\n let countsNodeEl: HTMLSpanElement | null = null;\n let countsEdgeEl: HTMLSpanElement | null = null;\n let visibleValueEl: HTMLSpanElement | null = null;\n\n if (perfItems.length > 0) {\n const { wrapper, content } = createSection('Performance', 'flow-devtools-perf');\n if (perfItems.includes('fps')) {\n const { row, valueEl } = createRow('FPS', 'flow-devtools-fps');\n fpsValueEl = valueEl;\n content.appendChild(row);\n }\n if (perfItems.includes('memory')) {\n const { row, valueEl } = createRow('Memory', 'flow-devtools-memory');\n memoryValueEl = valueEl;\n content.appendChild(row);\n }\n if (perfItems.includes('counts')) {\n const nodeRow = createRow('Nodes', 'flow-devtools-counts');\n countsNodeEl = nodeRow.valueEl;\n content.appendChild(nodeRow.row);\n const edgeRow = createRow('Edges', 'flow-devtools-counts');\n countsEdgeEl = edgeRow.valueEl;\n content.appendChild(edgeRow.row);\n }\n if (perfItems.includes('visible')) {\n const { row, valueEl } = createRow('Visible', 'flow-devtools-visible');\n visibleValueEl = valueEl;\n content.appendChild(row);\n }\n panel.appendChild(wrapper);\n }\n\n // ── Events section ─────────────────────────────────────────────\n const eventMax = getEventMax(config);\n let eventListEl: HTMLDivElement | null = null;\n\n if (eventMax > 0) {\n const { wrapper, content } = createSection('Events', 'flow-devtools-events');\n\n const clearBtn = document.createElement('button');\n clearBtn.className = 'flow-devtools-clear-btn nopan';\n clearBtn.textContent = 'Clear';\n clearBtn.addEventListener('click', () => {\n if (eventListEl) eventListEl.textContent = '';\n eventEntries.length = 0;\n });\n wrapper.querySelector('.flow-devtools-section-title')!.appendChild(clearBtn);\n\n eventListEl = document.createElement('div');\n eventListEl.className = 'flow-devtools-event-list';\n content.appendChild(eventListEl);\n panel.appendChild(wrapper);\n }\n\n // ── Viewport section ───────────────────────────────────────────\n let vpXEl: HTMLSpanElement | null = null;\n let vpYEl: HTMLSpanElement | null = null;\n let vpZoomEl: HTMLSpanElement | null = null;\n\n if (config.viewport) {\n const { wrapper, content } = createSection('Viewport', 'flow-devtools-viewport');\n const xRow = createRow('X', 'flow-devtools-vp-x');\n vpXEl = xRow.valueEl;\n content.appendChild(xRow.row);\n const yRow = createRow('Y', 'flow-devtools-vp-y');\n vpYEl = yRow.valueEl;\n content.appendChild(yRow.row);\n const zoomRow = createRow('Zoom', 'flow-devtools-vp-zoom');\n vpZoomEl = zoomRow.valueEl;\n content.appendChild(zoomRow.row);\n panel.appendChild(wrapper);\n }\n\n // ── State section ──────────────────────────────────────────────\n let stateContentEl: HTMLDivElement | null = null;\n\n if (config.state) {\n const { wrapper, content } = createSection('Selection', 'flow-devtools-state');\n stateContentEl = document.createElement('div');\n stateContentEl.className = 'flow-devtools-state-content';\n stateContentEl.textContent = 'No selection';\n content.appendChild(stateContentEl);\n panel.appendChild(wrapper);\n }\n\n // ── Activity section ───────────────────────────────────────────\n let animValueEl: HTMLSpanElement | null = null;\n let particleValueEl: HTMLSpanElement | null = null;\n let followValueEl: HTMLSpanElement | null = null;\n let timelineValueEl: HTMLSpanElement | null = null;\n\n if (config.activity) {\n const { wrapper, content } = createSection('Activity', 'flow-devtools-activity');\n const animRow = createRow('Animations', 'flow-devtools-anim');\n animValueEl = animRow.valueEl;\n content.appendChild(animRow.row);\n const partRow = createRow('Particles', 'flow-devtools-particles');\n particleValueEl = partRow.valueEl;\n content.appendChild(partRow.row);\n const followRow = createRow('Follow', 'flow-devtools-follow');\n followValueEl = followRow.valueEl;\n content.appendChild(followRow.row);\n const tlRow = createRow('Timelines', 'flow-devtools-timelines');\n timelineValueEl = tlRow.valueEl;\n content.appendChild(tlRow.row);\n panel.appendChild(wrapper);\n }\n\n // ── RAF loop (FPS + memory) ────────────────────────────────────\n let rafId: number | null = null;\n let rafRunning = false;\n let frames = 0;\n let lastTime = performance.now();\n const needsRaf = !!(fpsValueEl || memoryValueEl);\n\n const tick = () => {\n if (!rafRunning) return;\n frames++;\n const now = performance.now();\n if (now - lastTime >= 1000) {\n if (fpsValueEl) {\n fpsValueEl.textContent = String(Math.round((frames * 1000) / (now - lastTime)));\n }\n frames = 0;\n lastTime = now;\n if (memoryValueEl && (performance as any).memory) {\n memoryValueEl.textContent = Math.round((performance as any).memory.usedJSHeapSize / 1048576) + ' MB';\n }\n }\n rafId = requestAnimationFrame(tick);\n };\n\n const startRaf = () => {\n if (!needsRaf || rafRunning) return;\n rafRunning = true;\n frames = 0;\n lastTime = performance.now();\n rafId = requestAnimationFrame(tick);\n };\n\n const stopRaf = () => {\n rafRunning = false;\n if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }\n };\n\n // ── Event log listener ─────────────────────────────────────────\n interface EventEntry { name: string; time: number; detail: string }\n const eventEntries: EventEntry[] = [];\n\n const FLOW_EVENTS_LIST = [\n 'flow-init', 'flow-connect', 'flow-disconnect',\n 'flow-node-add', 'flow-node-remove', 'flow-edge-add', 'flow-edge-remove',\n 'flow-selection-change', 'flow-viewport-change',\n 'flow-viewport-move-start', 'flow-viewport-move', 'flow-viewport-move-end',\n 'flow-node-drag-start', 'flow-node-drag', 'flow-node-drag-end',\n 'flow-node-click', 'flow-edge-click',\n 'flow-node-condense', 'flow-node-uncondense',\n 'flow-undo', 'flow-redo',\n ];\n\n let flowEventHandler: ((e: Event) => void) | null = null;\n\n if (eventMax > 0 && eventListEl) {\n flowEventHandler = (e: Event) => {\n // No log work while collapsed — the panel is hidden, and viewport-move/\n // -change fire at frame rate. Rows are rebuilt from live events on expand.\n if (!expanded) return;\n const ce = e as CustomEvent;\n const name = ce.type.replace('flow-', '');\n let detailStr = '';\n try {\n detailStr = ce.detail ? JSON.stringify(ce.detail).slice(0, 80) : '';\n } catch {\n detailStr = '[circular]';\n }\n\n eventEntries.unshift({ name, time: Date.now(), detail: detailStr });\n\n // Prepend new entry row (avoid full rebuild)\n const listEl = eventListEl!;\n const row = document.createElement('div');\n row.className = 'flow-devtools-event-entry';\n\n const nameSpan = document.createElement('span');\n nameSpan.className = 'flow-devtools-event-name';\n nameSpan.textContent = name;\n\n const ageSpan = document.createElement('span');\n ageSpan.className = 'flow-devtools-event-age';\n ageSpan.textContent = 'now';\n\n const detailSpan = document.createElement('span');\n detailSpan.className = 'flow-devtools-event-detail';\n detailSpan.textContent = detailStr;\n\n row.appendChild(nameSpan);\n row.appendChild(ageSpan);\n row.appendChild(detailSpan);\n listEl.prepend(row);\n\n // Trim oldest if over limit\n while (listEl.children.length > eventMax) {\n listEl.removeChild(listEl.lastChild!);\n eventEntries.pop();\n }\n };\n for (const evt of FLOW_EVENTS_LIST) {\n container.addEventListener(evt, flowEventHandler);\n }\n }\n\n // ── Alpine effects for reactive data ───────────────────────────\n // Two effects, split by update cadence. The viewport values change every\n // animation frame during zoom/pan; keeping them in their own effect means\n // a frame only touches three text nodes and never re-runs the (expensive)\n // selection serialization or the counts/activity reads below.\n effect(() => {\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas || !canvas.viewport) return;\n\n if (vpXEl) vpXEl.textContent = Math.round(canvas.viewport.x).toString();\n if (vpYEl) vpYEl.textContent = Math.round(canvas.viewport.y).toString();\n if (vpZoomEl) vpZoomEl.textContent = canvas.viewport.zoom.toFixed(2);\n });\n\n // Data effect: counts, visible, selection JSON, activity. Deliberately does\n // NOT read canvas.viewport, so viewport frames never trigger it.\n effect(() => {\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas) return;\n\n // Counts\n if (countsNodeEl) countsNodeEl.textContent = String(canvas.nodes?.length ?? 0);\n if (countsEdgeEl) countsEdgeEl.textContent = String(canvas.edges?.length ?? 0);\n\n // Visible\n if (visibleValueEl && canvas._getVisibleNodeIds) {\n visibleValueEl.textContent = String(canvas._getVisibleNodeIds().size);\n }\n\n // State (selection)\n if (stateContentEl) {\n const selNodes = canvas.selectedNodes as Set | undefined;\n const selEdges = canvas.selectedEdges as Set | undefined;\n const hasSelection = (selNodes?.size ?? 0) > 0 || (selEdges?.size ?? 0) > 0;\n\n if (!hasSelection) {\n stateContentEl.textContent = 'No selection';\n } else {\n // Build compact JSON display using safe DOM methods\n stateContentEl.textContent = '';\n if (selNodes && selNodes.size > 0) {\n for (const nodeId of selNodes) {\n const node = canvas.getNode?.(nodeId);\n if (!node) continue;\n const pre = document.createElement('pre');\n pre.className = 'flow-devtools-json';\n pre.textContent = JSON.stringify({ id: node.id, position: node.position, data: node.data }, null, 2);\n stateContentEl.appendChild(pre);\n }\n }\n if (selEdges && selEdges.size > 0) {\n for (const edgeId of selEdges) {\n const edge = canvas.edges?.find((e: any) => e.id === edgeId);\n if (!edge) continue;\n const pre = document.createElement('pre');\n pre.className = 'flow-devtools-json';\n pre.textContent = JSON.stringify({ id: edge.id, source: edge.source, target: edge.target, type: edge.type }, null, 2);\n stateContentEl.appendChild(pre);\n }\n }\n }\n }\n\n // Activity\n if (animValueEl) {\n const count = canvas._animator?._groups?.size ?? 0;\n animValueEl.textContent = String(count);\n }\n if (particleValueEl) {\n particleValueEl.textContent = String(canvas._activeParticles?.size ?? 0);\n }\n if (followValueEl) {\n followValueEl.textContent = canvas._followHandle ? 'Active' : 'Idle';\n }\n if (timelineValueEl) {\n timelineValueEl.textContent = String(canvas._activeTimelines?.size ?? 0);\n }\n });\n\n // ── Cleanup ────────────────────────────────────────────────────\n cleanup(() => {\n // 1. Stop RAF\n stopRaf();\n\n // 2. Remove toggle listener\n toggleBtn.removeEventListener('click', onToggleClick);\n\n // 3. Remove event listeners\n if (flowEventHandler) {\n for (const evt of FLOW_EVENTS_LIST) {\n container.removeEventListener(evt, flowEventHandler);\n }\n }\n // 4. Remove interaction stoppers\n el.removeEventListener('wheel', stopWheel);\n\n // 5. Clear DOM\n el.textContent = '';\n\n // 6. Null references\n fpsValueEl = null;\n memoryValueEl = null;\n countsNodeEl = null;\n countsEdgeEl = null;\n visibleValueEl = null;\n eventListEl = null;\n vpXEl = null;\n vpYEl = null;\n vpZoomEl = null;\n stateContentEl = null;\n animValueEl = null;\n particleValueEl = null;\n followValueEl = null;\n timelineValueEl = null;\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-action Directive\n//\n// Declarative canvas action buttons. Replaces repetitive @click + :disabled +\n// aria-* boilerplate for common canvas operations.\n//\n// Usage:\n// \n// \n// \n// \n// \n// \n// \n// \n// \n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\n// ── Types ────────────────────────────────────────────────────────────────────\n\nexport interface ActionDef {\n method: string;\n disabledWhen?: (canvas: any) => boolean;\n aria?: 'disabled' | 'pressed';\n passExpression?: boolean;\n}\n\n// ── Action Map ───────────────────────────────────────────────────────────────\n\nexport const ACTION_MAP: Record = {\n 'undo': { method: 'undo', disabledWhen: (c) => !c.canUndo, aria: 'disabled' },\n 'redo': { method: 'redo', disabledWhen: (c) => !c.canRedo, aria: 'disabled' },\n 'fit-view': { method: 'fitView', passExpression: true },\n 'zoom-in': {\n method: 'zoomIn',\n disabledWhen: (c) => c.viewport.zoom >= (c._config?.maxZoom ?? 2),\n aria: 'disabled',\n },\n 'zoom-out': {\n method: 'zoomOut',\n disabledWhen: (c) => c.viewport.zoom <= (c._config?.minZoom ?? 0.5),\n aria: 'disabled',\n },\n 'toggle-interactive': { method: 'toggleInteractive', aria: 'pressed' },\n 'clear': { method: '$clear', disabledWhen: (c) => c.nodes.length === 0, aria: 'disabled' },\n 'reset': { method: '$reset' },\n 'export': { method: 'toImage', passExpression: true },\n};\n\n// ── Pure Functions (exported for testing) ────────────────────────────────────\n\nexport function getActionDef(actionName: string): ActionDef | null {\n return ACTION_MAP[actionName] ?? null;\n}\n\nexport function isActionDisabled(actionName: string, canvas: any): boolean {\n const def = ACTION_MAP[actionName];\n if (!def?.disabledWhen) {\n return false;\n }\n return def.disabledWhen(canvas);\n}\n\nexport function getAriaAttr(actionName: string): 'disabled' | 'pressed' | null {\n return ACTION_MAP[actionName]?.aria ?? null;\n}\n\n// ── Directive Registration ───────────────────────────────────────────────────\n\nexport function registerFlowActionDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-action',\n (el, { value, expression }, { evaluate, effect, cleanup }) => {\n const actionName = value;\n const def = getActionDef(actionName);\n if (!def) {\n return;\n }\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) {\n return;\n }\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas) {\n return;\n }\n\n const onClick = () => {\n const method = canvas[def.method];\n if (typeof method !== 'function') {\n return;\n }\n\n if (def.passExpression && expression) {\n method.call(canvas, evaluate(expression));\n } else {\n method.call(canvas);\n }\n };\n\n el.addEventListener('click', onClick);\n\n if (def.disabledWhen || def.aria) {\n effect(() => {\n if (def.disabledWhen) {\n const disabled = def.disabledWhen(canvas);\n (el as HTMLButtonElement).disabled = disabled;\n if (def.aria === 'disabled') {\n el.setAttribute('aria-disabled', String(disabled));\n }\n }\n\n if (def.aria === 'pressed') {\n el.setAttribute('aria-pressed', String(!canvas.isInteractive));\n }\n });\n }\n\n cleanup(() => {\n el.removeEventListener('click', onClick);\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-filter Directive\n//\n// Declarative node and row filtering.\n//\n// Usage:\n// \n// \n// \n// \n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\ntype FilterArg = 'node' | 'row';\n\nexport interface FilterParseResult {\n type: FilterArg;\n isClear: boolean;\n}\n\nexport function parseFilterDirective(\n arg: string,\n modifiers: string[],\n): FilterParseResult | null {\n if (arg !== 'node' && arg !== 'row') return null;\n\n const isClear = modifiers.includes('clear');\n\n return { type: arg, isClear };\n}\n\n// Track which predicate function reference is active per canvas\nconst activeNodeFilterRefs = new WeakMap();\n\nexport function registerFlowFilterDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-filter',\n (el, { value, expression, modifiers }, { evaluate, effect, cleanup }) => {\n const parsed = parseFilterDirective(value, modifiers);\n if (!parsed) return;\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas) return;\n\n let myPredicate: Function | null = null;\n\n const onClick = () => {\n if (parsed.isClear) {\n if (parsed.type === 'node') {\n canvas.clearNodeFilter();\n activeNodeFilterRefs.set(canvasEl, null);\n } else {\n for (const node of canvas.nodes) {\n if (node.rowFilter && node.rowFilter !== 'all') {\n canvas.setRowFilter(node.id, 'all');\n }\n }\n }\n return;\n }\n\n if (parsed.type === 'node' && expression) {\n // Wrap in array to prevent Alpine's evaluate() from auto-calling the function\n myPredicate = (evaluate(`[${expression}]`) as Function[])[0];\n canvas.setNodeFilter(myPredicate);\n activeNodeFilterRefs.set(canvasEl, myPredicate);\n } else if (parsed.type === 'row' && expression) {\n const config = evaluate(expression) as { node: string; predicate: (row: any) => boolean };\n canvas.setRowFilter(config.node, config.predicate);\n }\n };\n\n el.addEventListener('click', onClick);\n el.style.cursor = 'pointer';\n\n // Reactive active state for node filters\n if (parsed.type === 'node' && !parsed.isClear) {\n effect(() => {\n void canvas.nodes.length;\n const active = activeNodeFilterRefs.get(canvasEl) === myPredicate && myPredicate !== null;\n el.classList.toggle('flow-filter-active', active);\n el.setAttribute('aria-pressed', String(active));\n });\n }\n\n cleanup(() => {\n el.removeEventListener('click', onClick);\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-follow Directive\n//\n// Declarative camera tracking.\n//\n// Usage:\n// \n// \n// \n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\ninterface FollowOptions {\n target: string;\n zoom?: number;\n speed?: number;\n}\n\nexport function parseFollowExpression(value: unknown): FollowOptions | null {\n if (typeof value === 'string') {\n return { target: value };\n }\n if (value && typeof value === 'object' && 'target' in value) {\n const obj = value as Record;\n return {\n target: obj.target as string,\n zoom: typeof obj.zoom === 'number' ? obj.zoom : undefined,\n speed: typeof obj.speed === 'number' ? obj.speed : undefined,\n };\n }\n return null;\n}\n\nexport function registerFlowFollowDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-follow',\n (el, { expression, modifiers }, { evaluate, effect, cleanup }) => {\n const isToggle = modifiers.includes('toggle');\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas?.follow) return;\n\n let followHandle: { stop: () => void; finished?: Promise } | null = null;\n\n const updateState = (active: boolean) => {\n el.classList.toggle('flow-following', active);\n el.setAttribute('aria-pressed', String(active));\n };\n\n const onClick = () => {\n if (!expression) return;\n const value = evaluate(expression);\n const opts = parseFollowExpression(value);\n if (!opts) return;\n\n // Toggle: stop if already following\n if (isToggle && followHandle) {\n followHandle.stop();\n followHandle = null;\n updateState(false);\n return;\n }\n\n // Stop any existing follow before starting new one\n if (followHandle) {\n followHandle.stop();\n }\n\n const followOpts: Record = {};\n if (opts.zoom !== undefined) followOpts.zoom = opts.zoom;\n if (opts.speed !== undefined) followOpts.speed = opts.speed;\n\n followHandle = canvas.follow(opts.target, followOpts);\n updateState(true);\n\n // Listen for follow completion\n if (followHandle?.finished) {\n followHandle.finished.then(() => {\n followHandle = null;\n updateState(false);\n });\n }\n };\n\n el.addEventListener('click', onClick);\n\n cleanup(() => {\n el.removeEventListener('click', onClick);\n if (followHandle) {\n followHandle.stop();\n followHandle = null;\n }\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-snapshot Directive\n//\n// Declarative state capture/restore.\n//\n// Usage:\n// \n// \n// \n// \n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\ntype SnapshotAction = 'save' | 'restore';\n\nexport interface SnapshotParseResult {\n action: SnapshotAction;\n persist: boolean;\n}\n\nexport function parseSnapshotDirective(\n arg: string,\n modifiers: string[],\n): SnapshotParseResult | null {\n if (arg !== 'save' && arg !== 'restore') return null;\n return { action: arg, persist: modifiers.includes('persist') };\n}\n\n// Module-scoped in-memory snapshot storage\nconst snapshots = new Map();\n\nexport function saveSnapshot(key: string, data: object): void {\n snapshots.set(key, data);\n}\n\nexport function getSnapshot(key: string): object | null {\n return snapshots.get(key) ?? null;\n}\n\nexport function hasSnapshot(key: string): boolean {\n return snapshots.has(key);\n}\n\nexport function clearAllSnapshots(): void {\n snapshots.clear();\n}\n\nexport function getLocalStorageKey(key: string): string {\n return `alpineflow-snapshot-${key}`;\n}\n\nexport function registerFlowSnapshotDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-snapshot',\n (el, { value, expression, modifiers }, { evaluate, effect, cleanup }) => {\n const parsed = parseSnapshotDirective(value, modifiers);\n if (!parsed) return;\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas) return;\n\n const onClick = () => {\n if (!expression) return;\n const key = evaluate(expression) as string;\n if (!key) return;\n\n if (parsed.action === 'save') {\n const data = canvas.toObject();\n if (parsed.persist) {\n localStorage.setItem(getLocalStorageKey(key), JSON.stringify(data));\n } else {\n saveSnapshot(key, data);\n }\n } else {\n let data: object | null = null;\n if (parsed.persist) {\n const raw = localStorage.getItem(getLocalStorageKey(key));\n if (raw) {\n try { data = JSON.parse(raw); } catch { /* ignore malformed */ }\n }\n } else {\n data = getSnapshot(key);\n }\n if (data) {\n canvas.fromObject(data);\n }\n }\n };\n\n el.addEventListener('click', onClick);\n\n // Reactive disabled state for restore buttons\n if (parsed.action === 'restore') {\n effect(() => {\n if (!expression) return;\n const key = evaluate(expression) as string;\n if (!key) return;\n\n let exists: boolean;\n if (parsed.persist) {\n exists = localStorage.getItem(getLocalStorageKey(key)) !== null;\n } else {\n void canvas.nodes.length;\n exists = hasSnapshot(key);\n }\n\n (el as HTMLButtonElement).disabled = !exists;\n el.setAttribute('aria-disabled', String(!exists));\n });\n }\n\n cleanup(() => {\n el.removeEventListener('click', onClick);\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-loading Directive\n//\n// Shows a loading overlay while the canvas is initializing or the user has\n// set loading: true. If the element has no children, a default pulsing\n// node indicator is injected.\n//\n// Usage:\n//
\n//
Custom spinner...
\n//
\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nfunction createDefaultIndicator(text?: string): HTMLElement {\n const indicator = document.createElement('div');\n indicator.className = 'flow-loading-indicator';\n\n const nodeEl = document.createElement('div');\n nodeEl.className = 'flow-loading-indicator-node';\n\n const textEl = document.createElement('div');\n textEl.className = 'flow-loading-indicator-text';\n textEl.textContent = text ?? 'Loading\\u2026';\n\n indicator.appendChild(nodeEl);\n indicator.appendChild(textEl);\n return indicator;\n}\n\nexport function registerFlowLoadingDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-loading',\n (el, { modifiers }, { effect, cleanup }) => {\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl) as any;\n if (!canvas) return;\n\n // Add overlay class\n el.classList.add('flow-loading-overlay');\n\n // Inject default indicator if no custom content\n const hasCustomContent = el.childElementCount > 0 || el.textContent!.trim().length > 0;\n if (!hasCustomContent) {\n el.appendChild(createDefaultIndicator(canvas._loadingText));\n }\n\n const useFade = modifiers.includes('fade');\n if (useFade) {\n el.classList.add('flow-loading-fade');\n }\n\n canvasEl.setAttribute('data-flow-loading-directive', '');\n\n let pendingTransitionEnd: ((e: Event) => void) | null = null;\n\n effect(() => {\n const loading = canvas.isLoading;\n\n if (loading) {\n el.style.display = 'flex';\n if (useFade) {\n el.classList.remove('flow-loading-fade-out');\n if (pendingTransitionEnd) {\n el.removeEventListener('transitionend', pendingTransitionEnd);\n pendingTransitionEnd = null;\n }\n }\n } else {\n if (useFade) {\n if (pendingTransitionEnd) {\n el.removeEventListener('transitionend', pendingTransitionEnd);\n }\n el.classList.add('flow-loading-fade-out');\n const onEnd = () => {\n el.style.display = 'none';\n el.removeEventListener('transitionend', onEnd);\n pendingTransitionEnd = null;\n };\n pendingTransitionEnd = onEnd;\n el.addEventListener('transitionend', onEnd);\n } else {\n el.style.display = 'none';\n }\n }\n });\n\n cleanup(() => {\n if (pendingTransitionEnd) {\n el.removeEventListener('transitionend', pendingTransitionEnd);\n pendingTransitionEnd = null;\n }\n canvasEl.removeAttribute('data-flow-loading-directive');\n el.style.display = '';\n el.classList.remove('flow-loading-overlay', 'flow-loading-fade', 'flow-loading-fade-out');\n });\n },\n );\n}\n","// ============================================================================\n// x-flow-edge-toolbar Directive\n//\n// Floating toolbar positioned at a point along an edge path. Counter-scales\n// against viewport zoom like x-flow-node-toolbar. Rendered as an HTML\n// overlay in .flow-viewport.\n//\n// Usage:\n//
...
— midpoint, above\n//
...
— midpoint, below\n//
...
— 30% along path\n// ============================================================================\n\nimport type { Alpine } from 'alpinejs';\n\nexport function registerFlowEdgeToolbarDirective(Alpine: Alpine) {\n Alpine.directive(\n 'flow-edge-toolbar',\n (\n el,\n { expression, modifiers },\n { evaluate, effect, cleanup },\n ) => {\n // ── Find parent edge and canvas ────────────────────────────────\n const edgeGroupEl = el.closest('[data-flow-edge-id]') as SVGGElement | null;\n if (!edgeGroupEl) return;\n\n const edgeId = edgeGroupEl.dataset.flowEdgeId!;\n\n const canvasEl = el.closest('[data-flow-canvas]') as HTMLElement | null;\n if (!canvasEl) return;\n\n const canvas = Alpine.$data(canvasEl);\n if (!canvas) return;\n\n // ── Relocate to viewport as HTML overlay ───────────────────────\n const viewport = canvasEl.querySelector('.flow-viewport') as HTMLElement | null;\n if (!viewport) return;\n\n // Preserve the x-for loop variable (`edge`) before moving the element\n // out of its original parent hierarchy. Canvas methods (removeEdges, etc.)\n // remain accessible since .flow-viewport is inside the canvas x-data scope.\n try {\n const edge = evaluate('edge');\n if (edge) Alpine.addScopeToNode(el as HTMLElement, { edge });\n } catch { /* edge not in scope — toolbar used outside x-for */ }\n\n viewport.appendChild(el);\n\n // ── Base styling ───────────────────────────────────────────────\n el.classList.add('flow-edge-toolbar');\n el.style.position = 'absolute';\n\n // ── Prevent interactions from triggering edge actions ──────────\n const stopPointerDown = (e: PointerEvent) => { e.stopPropagation(); };\n const stopClick = (e: MouseEvent) => { e.stopPropagation(); };\n el.addEventListener('pointerdown', stopPointerDown);\n el.addEventListener('click', stopClick);\n\n // ── Parse options ──────────────────────────────────────────────\n const below = modifiers.includes('below');\n const defaultOffset = 20;\n\n // ── Reactive positioning ───────────────────────────────────────\n effect(() => {\n // Self-cleanup: toolbar was moved out of x-for's DOM tree, so Alpine's\n // template cleanup won't reach it when the edge is removed.\n // Read canvas.edges (reactive array) to ensure this effect re-runs on removal.\n if (!canvas.edges.some((e: any) => e.id === edgeId)) {\n el.removeEventListener('pointerdown', stopPointerDown);\n el.removeEventListener('click', stopClick);\n el.classList.remove('flow-edge-toolbar');\n el.remove();\n return;\n }\n\n const zoom: number = canvas.viewport?.zoom || 1;\n const offset = parseInt(el.getAttribute('data-flow-offset') ?? String(defaultOffset), 10);\n\n // Evaluate position expression (0-1 along path), default 0.5\n let t = 0.5;\n if (expression) {\n const evaluated = evaluate(expression);\n if (typeof evaluated === 'number') t = evaluated;\n }\n\n // Find the visible path element (second in the edge group)\n const paths = edgeGroupEl.querySelectorAll('path');\n const pathEl = paths.length > 1 ? (paths[1] as SVGPathElement) : (paths[0] as SVGPathElement | undefined);\n if (!pathEl) return;\n\n const totalLength = pathEl.getTotalLength?.();\n if (!totalLength) return;\n\n const pt = pathEl.getPointAtLength(totalLength * Math.max(0, Math.min(1, t)));\n\n // Apply perpendicular offset (above or below)\n const scaledOffset = offset / zoom;\n const yOffset = below ? scaledOffset : -scaledOffset;\n\n el.style.left = `${pt.x}px`;\n el.style.top = `${pt.y + yOffset}px`;\n el.style.transformOrigin = '0 0';\n el.style.transform = `scale(${1 / zoom}) translate(-50%, ${below ? '0%' : '-100%'})`;\n });\n\n // ── Cleanup ────────────────────────────────────────────────────\n cleanup(() => {\n el.removeEventListener('pointerdown', stopPointerDown);\n el.removeEventListener('click', stopClick);\n el.classList.remove('flow-edge-toolbar');\n el.remove();\n });\n },\n );\n}\n","// ============================================================================\n// $flow Magic\n//\n// Provides convenient access to the nearest flowCanvas data component\n// from any descendant element.\n//\n// Usage: