1/*! Personyze on-site components (fresh-service/webdriver-apps) */ 2(function() 3{'use strict';
4// ../../../../deno-dir/npm/registry.npmjs.org/preact/10.22.0/dist/preact.module.js 5var n; 6var l; 7var u; 8var t; 9var i; 10var o; 11var r; 12var f; 13var e; 14var c; 15var s; 16var a; 17var h = {}; 18var p = []; 19var v = /acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i; 20var y = Array.isArray; 21function d(n2, l3) { 22 for (var u4 in l3) n2[u4] = l3[u4]; 23 return n2; 24} 25function w(n2) { 26 var l3 = n2.parentNode; 27 l3 && l3.removeChild(n2); 28} 29function _(l3, u4, t4) { 30 var i4, o3, r3, f4 = {}; 31 for (r3 in u4) "key" == r3 ? i4 = u4[r3] : "ref" == r3 ? o3 = u4[r3] : f4[r3] = u4[r3]; 32 if (arguments.length > 2 && (f4.children = arguments.length > 3 ? n.call(arguments, 2) : t4), "function" == typeof l3 && null != l3.defaultProps) for (r3 in l3.defaultProps) void 0 === f4[r3] && (f4[r3] = l3.defaultProps[r3]); 33 return g(l3, f4, i4, o3, null); 34} 35function g(n2, t4, i4, o3, r3) { 36 var f4 = { 37 type: n2, 38 props: t4, 39 key: i4, 40 ref: o3, 41 __k: null, 42 __: null, 43 __b: 0, 44 __e: null, 45 __d: void 0, 46 __c: null, 47 constructor: void 0, 48 __v: null == r3 ? ++u : r3, 49 __i: -1, 50 __u: 0 51 }; 52 return null == r3 && null != l.vnode && l.vnode(f4), f4; 53} 54function k(n2) { 55 return n2.children; 56} 57function b(n2, l3) { 58 this.props = n2, this.context = l3; 59} 60function x(n2, l3) { 61 if (null == l3) return n2.__ ? x(n2.__, n2.__i + 1) : null; 62 for (var u4; l3 < n2.__k.length; l3++) if (null != (u4 = n2.__k[l3]) && null != u4.__e) return u4.__e; 63 return "function" == typeof n2.type ? x(n2) : null; 64} 65function C(n2) { 66 var l3, u4; 67 if (null != (n2 = n2.__) && null != n2.__c) { 68 for (n2.__e = n2.__c.base = null, l3 = 0; l3 < n2.__k.length; l3++) if (null != (u4 = n2.__k[l3]) && null != u4.__e) { 69 n2.__e = n2.__c.base = u4.__e; 70 break; 71 } 72 return C(n2); 73 } 74} 75function M(n2) { 76 (!n2.__d && (n2.__d = true) && i.push(n2) && !P.__r++ || o !== l.debounceRendering) && ((o = l.debounceRendering) || r)(P); 77} 78function P() { 79 var n2, u4, t4, o3, r3, e3, c3, s3; 80 for (i.sort(f); n2 = i.shift(); ) n2.__d && (u4 = i.length, o3 = void 0, e3 = (r3 = (t4 = n2).__v).__e, c3 = [], s3 = [], t4.__P && ((o3 = d({}, r3)).__v = r3.__v + 1, l.vnode && l.vnode(o3), O(t4.__P, o3, r3, t4.__n, t4.__P.namespaceURI, 32 & r3.__u ? [ 81 e3 82 ] : null, c3, null == e3 ? x(r3) : e3, !!(32 & r3.__u), s3), o3.__v = r3.__v, o3.__.__k[o3.__i] = o3, j(c3, o3, s3), o3.__e != e3 && C(o3)), i.length > u4 && i.sort(f)); 83 P.__r = 0; 84} 85function S(n2, l3, u4, t4, i4, o3, r3, f4, e3, c3, s3) { 86 var a3, v3, y3, d3, w3, _3 = t4 && t4.__k || p, g2 = l3.length; 87 for (u4.__d = e3, $(u4, l3, _3), e3 = u4.__d, a3 = 0; a3 < g2; a3++) null != (y3 = u4.__k[a3]) && "boolean" != typeof y3 && "function" != typeof y3 && (v3 = -1 === y3.__i ? h : _3[y3.__i] || h, y3.__i = a3, O(n2, y3, v3, i4, o3, r3, f4, e3, c3, s3), d3 = y3.__e, y3.ref && v3.ref != y3.ref && (v3.ref && N(v3.ref, null, y3), s3.push(y3.ref, y3.__c || d3, y3)), null == w3 && null != d3 && (w3 = d3), 65536 & y3.__u || v3.__k === y3.__k ? (e3 && !e3.isConnected && (e3 = x(v3)), e3 = I(y3, e3, n2)) : "function" == typeof y3.type && void 0 !== y3.__d ? e3 = y3.__d : d3 && (e3 = d3.nextSibling), y3.__d = void 0, y3.__u &= -196609); 88 u4.__d = e3, u4.__e = w3; 89} 90function $(n2, l3, u4) { 91 var t4, i4, o3, r3, f4, e3 = l3.length, c3 = u4.length, s3 = c3, a3 = 0; 92 for (n2.__k = [], t4 = 0; t4 < e3; t4++) r3 = t4 + a3, null != (i4 = n2.__k[t4] = null == (i4 = l3[t4]) || "boolean" == typeof i4 || "function" == typeof i4 ? null : "string" == typeof i4 || "number" == typeof i4 || "bigint" == typeof i4 || i4.constructor == String ? g(null, i4, null, null, null) : y(i4) ? g(k, { 93 children: i4 94 }, null, null, null) : void 0 === i4.constructor && i4.__b > 0 ? g(i4.type, i4.props, i4.key, i4.ref ? i4.ref : null, i4.__v) : i4) ? (i4.__ = n2, i4.__b = n2.__b + 1, f4 = L(i4, u4, r3, s3), i4.__i = f4, o3 = null, -1 !== f4 && (s3--, (o3 = u4[f4]) && (o3.__u |= 131072)), null == o3 || null === o3.__v ? (-1 == f4 && a3--, "function" != typeof i4.type && (i4.__u |= 65536)) : f4 !== r3 && (f4 === r3 + 1 ? a3++ : f4 > r3 ? s3 > e3 - r3 ? a3 += f4 - r3 : a3-- : f4 < r3 ? f4 == r3 - 1 && (a3 = f4 - r3) : a3 = 0, f4 !== t4 + a3 && (i4.__u |= 65536))) : (o3 = u4[r3]) && null == o3.ke
vendor: 4,430 bytes, lines 94-165
94y && o3.__e && 0 == (131072 & o3.__u) && (o3.__e == n2.__d && (n2.__d = x(o3)), V(o3, o3, false), u4[r3] = null, s3--); 95 if (s3) for (t4 = 0; t4 < c3; t4++) null != (o3 = u4[t4]) && 0 == (131072 & o3.__u) && (o3.__e == n2.__d && (n2.__d = x(o3)), V(o3, o3)); 96} 97function I(n2, l3, u4) { 98 var t4, i4; 99 if ("function" == typeof n2.type) { 100 for (t4 = n2.__k, i4 = 0; t4 && i4 < t4.length; i4++) t4[i4] && (t4[i4].__ = n2, l3 = I(t4[i4], l3, u4)); 101 return l3; 102 } 103 n2.__e != l3 && (u4.insertBefore(n2.__e, l3 || null), l3 = n2.__e); 104 do { 105 l3 = l3 && l3.nextSibling; 106 } while (null != l3 && 8 === l3.nodeType); 107 return l3; 108} 109function L(n2, l3, u4, t4) { 110 var i4 = n2.key, o3 = n2.type, r3 = u4 - 1, f4 = u4 + 1, e3 = l3[u4]; 111 if (null === e3 || e3 && i4 == e3.key && o3 === e3.type && 0 == (131072 & e3.__u)) return u4; 112 if (t4 > (null != e3 && 0 == (131072 & e3.__u) ? 1 : 0)) for (; r3 >= 0 || f4 < l3.length; ) { 113 if (r3 >= 0) { 114 if ((e3 = l3[r3]) && 0 == (131072 & e3.__u) && i4 == e3.key && o3 === e3.type) return r3; 115 r3--; 116 } 117 if (f4 < l3.length) { 118 if ((e3 = l3[f4]) && 0 == (131072 & e3.__u) && i4 == e3.key && o3 === e3.type) return f4; 119 f4++; 120 } 121 } 122 return -1; 123} 124function T(n2, l3, u4) { 125 "-" === l3[0] ? n2.setProperty(l3, null == u4 ? "" : u4) : n2[l3] = null == u4 ? "" : "number" != typeof u4 || v.test(l3) ? u4 : u4 + "px"; 126} 127function A(n2, l3, u4, t4, i4) { 128 var o3; 129 n: if ("style" === l3) if ("string" == typeof u4) n2.style.cssText = u4; 130 else { 131 if ("string" == typeof t4 && (n2.style.cssText = t4 = ""), t4) for (l3 in t4) u4 && l3 in u4 || T(n2.style, l3, ""); 132 if (u4) for (l3 in u4) t4 && u4[l3] === t4[l3] || T(n2.style, l3, u4[l3]); 133 } 134 else if ("o" === l3[0] && "n" === l3[1]) o3 = l3 !== (l3 = l3.replace(/(PointerCapture)$|Capture$/i, "$1")), l3 = l3.toLowerCase() in n2 || "onFocusOut" === l3 || "onFocusIn" === l3 ? l3.toLowerCase().slice(2) : l3.slice(2), n2.l || (n2.l = {}), n2.l[l3 + o3] = u4, u4 ? t4 ? u4.u = t4.u : (u4.u = e, n2.addEventListener(l3, o3 ? s : c, o3)) : n2.removeEventListener(l3, o3 ? s : c, o3); 135 else { 136 if ("http://www.w3.org/2000/svg" == i4) l3 = l3.replace(/xlink(H|:h)/, "h").replace(/sName$/, "s"); 137 else if ("width" != l3 && "height" != l3 && "href" != l3 && "list" != l3 && "form" != l3 && "tabIndex" != l3 && "download" != l3 && "rowSpan" != l3 && "colSpan" != l3 && "role" != l3 && l3 in n2) try { 138 n2[l3] = null == u4 ? "" : u4; 139 break n; 140 } catch (n3) { 141 } 142 "function" == typeof u4 || (null == u4 || false === u4 && "-" !== l3[4] ? n2.removeAttribute(l3) : n2.setAttribute(l3, u4)); 143 } 144} 145function F(n2) { 146 return function(u4) { 147 if (this.l) { 148 var t4 = this.l[u4.type + n2]; 149 if (null == u4.t) u4.t = e++; 150 else if (u4.t < t4.u) return; 151 return t4(l.event ? l.event(u4) : u4); 152 } 153 }; 154} 155function O(n2, u4, t4, i4, o3, r3, f4, e3, c3, s3) { 156 var a3, h3, p3, v3, w3, _3, g2, m2, x2, C3, M2, P2, $2, I2, H, L2 = u4.type; 157 if (void 0 !== u4.constructor) return null; 158 128 & t4.__u && (c3 = !!(32 & t4.__u), r3 = [ 159 e3 = u4.__e = t4.__e 160 ]), (a3 = l.__b) && a3(u4); 161 n: if ("function" == typeof L2) try { 162 if (m2 = u4.props, x2 = (a3 = L2.contextType) && i4[a3.__c], C3 = a3 ? x2 ? x2.props.value : a3.__ : i4, t4.__c ? g2 = (h3 = u4.__c = t4.__c).__ = h3.__E : ("prototype" in L2 && L2.prototype.render ? u4.__c = h3 = new L2(m2, C3) : (u4.__c = h3 = new b(m2, C3), h3.constructor = L2, h3.render = q), x2 && x2.sub(h3), h3.props = m2, h3.state || (h3.state = {}), h3.context = C3, h3.__n = i4, p3 = h3.__d = true, h3.__h = [], h3._sb = []), null == h3.__s && (h3.__s = h3.state), null != L2.getDerivedStateFromProps && (h3.__s == h3.state && (h3.__s = d({}, h3.__s)), d(h3.__s, L2.getDerivedStateFromProps(m2, h3.__s))), v3 = h3.props, w3 = h3.state, h3.__v = u4, p3) null == L2.getDerivedStateFromProps && null != h3.componentWillMount && h3.componentWillMount(), null != h3.componentDidMount && h3.__h.push(h3.componentDidMount); 163 else { 164 if (null == L2.getDerivedStateFromProps && m2 !== v3 && null != h3.componentWillReceiveProps && h3.componentWillReceiveProps(m2, C3), !h3.__e && (null != h3.shouldComponentUpdate && false === h3.shouldComponentUpdate(m2, h3.__s, C3) || u4.__v === t4.__v)) { 165 for (u4.__v !== t4.__v && (h3.props = m2, h3.state = h3.__s, h3.__d = false), u4.__e = t4.__e, u4.__k = t4.__k, u4.__k.forEac
vendor: 1,103 bytes, lines 165-181
165h(function(n3) { 166 n3 && (n3.__ = u4); 167 }), M2 = 0; M2 < h3._sb.length; M2++) h3.__h.push(h3._sb[M2]); 168 h3._sb = [], h3.__h.length && f4.push(h3); 169 break n; 170 } 171 null != h3.componentWillUpdate && h3.componentWillUpdate(m2, h3.__s, C3), null != h3.componentDidUpdate && h3.__h.push(function() { 172 h3.componentDidUpdate(v3, w3, _3); 173 }); 174 } 175 if (h3.context = C3, h3.props = m2, h3.__P = n2, h3.__e = false, P2 = l.__r, $2 = 0, "prototype" in L2 && L2.prototype.render) { 176 for (h3.state = h3.__s, h3.__d = false, P2 && P2(u4), a3 = h3.render(h3.props, h3.state, h3.context), I2 = 0; I2 < h3._sb.length; I2++) h3.__h.push(h3._sb[I2]); 177 h3._sb = []; 178 } else do { 179 h3.__d = false, P2 && P2(u4), a3 = h3.render(h3.props, h3.state, h3.context), h3.state = h3.__s; 180 } while (h3.__d && ++$2 < 25); 181 h3.state = h3.__s, null != h3.getChildContext && (i4 = d(d({}, i4), h3.getChildContext())), p3 || null == h3.getSnapshotBeforeUpdate || (_3 = h3.getSnapshotBeforeUpdate(v3, w3)), S(n2, y(H = null != a3 && a3.type === k && null == a3.ke
181y ? a3.props.children : a3) ? H : [ 182 H 183 ], u4, t4, i4, o3, r3, f4, e3, c3, s3), h3.base = u4.__e, u4.__u &= -161, h3.__h.length && f4.push(h3), g2 && (h3.__E = h3.__ = null); 184 } catch (n3) { 185 u4.__v = null, c3 || null != r3 ? (u4.__e = e3, u4.__u |= c3 ? 160 : 32, r3[r3.indexOf(e3)] = null) : (u4.__e = t4.__e, u4.__k = t4.__k), l.__e(n3, u4, t4); 186 } 187 else null == r3 && u4.__v === t4.__v ? (u4.__k = t4.__k, u4.__e = t4.__e) : u4.__e = z(t4.__e, u4, t4, i4, o3, r3, f4, c3, s3); 188 (a3 = l.diffed) && a3(u4); 189} 190function j(n2, u4, t4) { 191 u4.__d = void 0; 192 for (var i4 = 0; i4 < t4.length; i4++) N(t4[i4], t4[++i4], t4[++i4]); 193 l.__c && l.__c(u4, n2), n2.some(function(u5) { 194 try { 195 n2 = u5.__h, u5.__h = [], n2.some(function(n3) { 196 n3.call(u5); 197 }); 198 } catch (n3) { 199 l.__e(n3, u5.__v); 200 } 201 }); 202} 203function z(l3, u4, t4, i4, o3, r3, f4, e3, c3) { 204 var s3, a3, p3, v3, d3, _3, g2, m2 = t4.props, k3 = u4.props, b2 = u4.type; 205 if ("svg" === b2 ? o3 = "http://www.w3.org/2000/svg" : "math" === b2 ? o3 = "http://www.w3.org/1998/Math/MathML" : o3 || (o3 = "http://www.w3.org/1999/xhtml"), null != r3) { 206 for (s3 = 0; s3 < r3.length; s3++) if ((d3 = r3[s3]) && "setAttribute" in d3 == !!b2 && (b2 ? d3.localName === b2 : 3 === d3.nodeType)) { 207 l3 = d3, r3[s3] = null; 208 break; 209 } 210 } 211 if (null == l3) { 212 if (null === b2) return document.createTextNode(k3); 213 l3 = document.createElementNS(o3, b2, k3.is && k3), r3 = null, e3 = false; 214 } 215 if (null === b2) m2 === k3 || e3 && l3.data === k3 || (l3.data = k3); 216 else { 217 if (r3 = r3 && n.call(l3.childNodes), m2 = t4.props || h, !e3 && null != r3) for (m2 = {}, s3 = 0; s3 < l3.attributes.length; s3++) m2[(d3 = l3.attributes[s3]).name] = d3.value; 218 for (s3 in m2) if (d3 = m2[s3], "children" == s3) ; 219 else if ("dangerouslySetInnerHTML" == s3) p3 = d3; 220 else if ("key" !== s3 && !(s3 in k3)) { 221 if ("value" == s3 && "defaultValue" in k3 || "checked" == s3 && "defaultChecked" in k3) continue; 222 A(l3, s3, null, d3, o3); 223 } 224 for (s3 in k3) d3 = k3[s3], "children" == s3 ? v3 = d3 : "dangerouslySetInnerHTML" == s3 ? a3 = d3 : "value" == s3 ? _3 = d3 : "checked" == s3 ? g2 = d3 : "key" === s3 || e3 && "function" != typeof d3 || m2[s3] === d3 || A(l3, s3, d3, m2[s3], o3); 225 if (a3) e3 || p3 && (a3.__html === p3.__html || a3.__html === l3.innerHTML) || (l3.innerHTML = a3.__html), u4.__k = []; 226 else if (p3 && (l3.innerHTML = ""), S(l3, y(v3) ? v3 : [ 227 v3 228 ], u4, t4, i4, "foreignObject" === b2 ? "http://www.w3.org/1999/xhtml" : o3, r3, f4, r3 ? r3[0] : t4.__k && x(t4, 0), e3, c3), null != r3) for (s3 = r3.length; s3--; ) null != r3[s3] && w(r3[s3]); 229 e3 || (s3 = "value", void 0 !== _3 && (_3 !== l3[s3] || "progress" === b2 && !_3 || "option" === b2 && _3 !== m2[s3]) && A(l3, s3, _3, m2[s3], o3), s3 = "checked", void 0 !== g2 && g2 !== l3[s3] && A(l3, s3, g2, m2[s3], o3)); 230 } 231 return l3; 232} 233function N(n2, u4, t4) { 234 try { 235 "function" == typeof n2 ? n2(u4) : n2.current = u4; 236 } catch (n3) { 237 l.__e(n3, t4); 238 } 239} 240function V(n2, u4, t4) { 241 var i4, o3; 242 if (l.unmount && l.unmount(n2), (i4 = n2.ref) && (i4.current && i4.current !== n2.__e || N(i4, null, u4)), null != (i4 = n2.__c)) { 243 if (i4.componentWillUnmount) try { 244 i4.componentWillUnmount(); 245 } catch (n3) { 246 l.__e(n3, u4); 247 } 248 i4.base = i4.__P = null; 249 } 250 if (i4 = n2.__k) for (o3 = 0; o3 < i4.length; o3++) i4[o3] && V(i4[o3], u4, t4 || "function" != typeof n2.type); 251 t4 || null == n2.__e || w(n2.__e), n2.__c = n2.__ = n2.__e = n2.__d = void 0; 252} 253function q(n2, l3, u4) { 254 return this.constructor(n2, u4); 255} 256function B(u4, t4, i4) { 257 var o3, r3, f4, e3; 258 l.__ && l.__(u4, t4), r3 = (o3 = "function" == typeof i4) ? null : i4 && i4.__k || t4.__k, f4 = [], e3 = [], O(t4, u4 = (!o3 && i4 || t4).__k = _(k, null, [ 259 u4 260 ]), r3 || h, h, t4.namespaceURI, !o3 && i4 ? [ 261 i4 262 ] : r3 ? null : t4.firstChild ? n.call(t4.childNodes) : null, f4, !o3 && i4 ? i4 : r3 ? r3.__e : t4.firstChild, o3, e3), j(f4, u4, e3); 263} 264n = p.slice, l = { 265 __e: function(n2, l3, u4, t4) { 266 for (var i4, o3, r3; l3 = l3.__; ) if ((i4 = l3.__c) && !i4.__) try { 267 if ((o3 = i4.constructor) && null != o3.getDerivedStateFromError && (i4.setState(o3.getDerivedStateFromError(n2)), r3 = i4.__d), null != i4.componentDidCatch && (i4.componentDidCatch(n2, t4 || {}), r3 = i4.__d), r3) return i4.__E = i4; 268 } catch (l4) { 269 n2 = l4; 270 } 271 throw n2; 272 } 273}, u = 0, t = function(n2) { 274 return null != n2 && null == n2.constructor; 275}, b.prototype.setState = function(n2, l3) { 276 var u4; 277 u4 = null != this.__s && this.__s !== this.state ? this.__s : this.__s = d({}, this.state), "function" == typeof n2 && (n2 = n2(d({}, u4), this.props)), n2 && d(u4, n2), null != n2 && this.__v && (l3 && this._sb.push(l3), M(this)); 278}, b.prototype.forceUpdate = function(n2) { 279 this.__v && (this.__e = true, n2 && this.__h.push(n2), M(this)); 280}, b.prototype.render = k, i = [], r = "function" == typeof Promise ? Promise.prototype.then.bind(Prom
280ise.resolve()) : setTimeout, f = function(n2, l3) { 281 return n2.__v.__b - l3.__v.__b; 282}, P.__r = 0, e = 0, c = F(false), s = F(true), a = 0; 283
284// src/components/live/liveStyle.ts 285var LIVE_STYLE = ` .le-root 286 { --le-chrome: #12182A; 287 --le-chrome-2: #171D31; 288 --le-chrome-3: #1E2640; 289 --le-line: rgba(255, 255, 255, 0.08); 290 --le-line-2: rgba(255, 255, 255, 0.14); 291 --le-fg: #E8EDF7; 292 --le-muted: #94A3C1; 293 --le-muted-2: #6A7698; 294 --le-brand: #12B886; 295 --le-brand-soft: rgba(18, 184, 134, 0.14); 296 --le-brand-fg: #7BDFAE; 297 --le-info: #60A5FA; 298 --le-info-soft: rgba(96, 165, 250, 0.15); 299 --le-danger: #EF4444; 300 --le-danger-soft: rgba(239, 68, 68, 0.14); 301 --le-danger-fg: #FF9B9B; 302 --le-mono: ui-monospace, Menlo, Consolas, monospace; 303 --le-shadow: 0 1.5em 3em rgba(0, 0, 0, 0.35); 304 305 position: fixed; 306 z-index: 2147483646; 307 color: var(--le-fg); 308 direction: ltr; 309 text-align: left; 310 font: normal normal 400 13px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; 311 } 312 .le-root * 313 { box-sizing: border-box; 314 } 315 .le-root svg 316 { flex: 0 0 auto; 317 width: 1.1em; 318 height: 1.1em; 319 fill: none; 320 stroke: currentColor; 321 stroke-width: 2; 322 stroke-linecap: round; 323 stroke-linejoin: round; 324 } 325 .le-root kbd 326 { padding: 0 0.4em; 327 border: 1px solid var(--le-line); 328 border-radius: 0.3em; 329 background-color: rgba(255, 255, 255, 0.08); 330 color: var(--le-muted); 331 font-family: var(--le-mono); 332 font-size: 85%; 333 } 334 335 /* ===== tooltips ===== 336 The design's own, instead of the browser's title attribute: a sentence of explanation fits, 337 it is styled like the rest of the chrome, and the delay is short enough to read as an answer 338 rather than an accident. 339 340 ONE FLOATING ELEMENT for all of them - TipLayer in liveTooltip.tsx - fixed to the window and 341 kept inside it. They used to be a ::after on each control, drawn in that control's box, so a 342 scrolling area clipped them: "Apply on" in the panel body came up cut off at the panel's left 343 edge (Jacob, 29 Sep 26). A control only carries data-tooltip (and data-tooltip-pos: top or 344 left, below by default). 345 346 position:relative stays on every trigger because layout had come to rely on it - see the 347 .le-peek rule, which re-asserts its own position over this one. */ 348 .le-root [data-tooltip] 349 { position: relative; 350 } 351 .le-root.le-tooltip 352 { z-index: 2147483647; 353 left: 0; 354 top: 0; 355 width: max-content; 356 max-width: min(20em, calc(100vw - 16px)); 357 padding: 0.45em 0.7em; 358 border: 1px solid var(--le-line-2); 359 border-radius: 0.45em; 360 background-color: #060913; 361 box-shadow: 0 0.6em 1.5em rgba(0, 0, 0, 0.5); 362 color: var(--le-fg); 363 font-size: 11.5px; 364 line-height: 1.4; 365 white-space: normal; 366 text-align: left; 367 pointer-events: none; 368 opacity: 0; 369 transition: opacity 0.12s ease-out; 370 } 371 .le-root.le-tooltip.le-on 372 { opacity: 1; 373 } 374 375 /* ===== toolbar ===== */ 376 .le-toolbar 377 { /* Narrow on purpose: this floats over the customer's page, and the tiles below stack their 378 icon over their label, so three of them fit in far less than the 42em the row layout 379 needed. */ 380 width: 32em; 381 /* A column so the body is the one part that gives: every other row is fixed height, and 382 the panel as a whole is capped to the window. Without a cap a tall panel simply hung off 383 the bottom with its Save button unreachable. 384 385 THE CAP IS A CONSTANT, AND THAT IS THE WHOLE POINT. It used to be calc(100vh - top), 386 set inline because only the component knows where it has been dragged to - which made 387 the panel's HEIGHT a function of its own position. Drag a 388 full-height panel down and it did not move: its top edge went down, the cap shrank by 389 exactly as much, and the bottom stayed put. It read as resizing rather than dragging, 390 and only on the panels tall enough to hit the cap - a widget with twenty settings - 391 which is exactly how it was reported (Jacob, 24 Aug 26: "instead of dragging, it 392 increased the size of the whole window... if the window only has four items I can drag 393 it"). Position and size are now independent: the cap is the window, and the toolbar 394 clamps its own top so the bottom edge stays on screen. */ 395 /* And no taller than a window somebody would open by hand. The cap alone let the busiest 396 tab - Insert widget, twenty-eight tiles - draw a full-height wall down the side of the 397 page (Jacob, 24 Aug 26: "it opens really tall, too big... it needs to be the same size 398 as the normal window, just with some scroll"). The body scrolls; the panel stays a 399 panel. It also leaves room to drag it, which a full-height one has nowhere to do. */ 400 max-height: min(calc(100vh - 1rem), 42em); 401 display: flex; 402 flex-direction: column; 403 max-width: 96vw; 404 border: 1px solid var(--le-line); 405 border-radius: 1.1em; 406 background-color: var(--le-chrome); 407 box-shadow: var(--le-shadow); 408 } 409 .le-toolbar.le-dragging 410 { box-shadow: 0 2em 4.5em rgba(0, 0, 0, 0.55); 411 } 412 .le-toolbar.le-collapsed 413 { width: auto; 414 min-width: 34em; 415 } 416 417 .le-head 418 { display: flex; 419 gap: 0.6em; 420 align-items: center; 421 padding: 0.6em 0.8em; 422 border-bottom: 1px solid var(--le-line); 423 cursor: grab; 424 user-select: none; 425 } 426 .le-toolbar.le-collapsed .le-head 427 { border-bottom: none; 428 } 429 .le-head:active 430 { cursor: grabbing; 431 } 432 /* THE HANDLE HAS TO LOOK LIKE A HANDLE. It was two grey dots against a dark bar - the panel 433 moves, which matters more here than in most tools because the panel stands between you and 434 the page you are editing, and people were not finding it (Jacob, 25 Aug 26). Given a 435 surface of its own, so it reads as something you take hold of. */ 436 .le-grip 437 { display: inline-flex; 438 flex: 0 0 auto; 439 align-items: center; 440 font-weight: 600; 441 white-space: nowrap; 442 padding: 0.32em 0.5em; 443 border: 1px solid var(--le-info); 444 border-radius: 0.45em; 445 /* COLOURED, because it is the only thing on this bar you GRAB rather than click, and it no 446 longer says so in words. Against the dark chrome and the outlined buttons beside it, 447 the one filled blue block reads as a handle at a glance. */ 448 background-color: var(--le-info); 449 color: var(--le-chrome); 450 letter-spacing: -0.15em;
450 /* the two glyphs make one 2x3 dot grid, not two ellipses */ 451 font-size: 104%; 452 cursor: grab; 453 } 454 /* THE ROW MUST NOT OUTGROW THE PANEL. The toolbar is a fixed 32em and every icon button is 455 flex:0 0 auto, so anything that made the row wider pushed the LAST button - Close - out 456 past the panel's own rounded corner, floating on the page beside it. Reported with a 457 screenshot (Jacob, 28 Aug 26). 458 459 Dropping the word "Move" bought back the space, but not durably: the Navigate button grows 460 by " . ON" when it is on, which spends it again. So the two labelled buttons are the 461 ones that give, and the fixed-size icons keep their place. */ 462 .le-head .le-mode 463 { flex: 0 1 auto; 464 min-width: 0; 465 overflow: hidden; 466 } 467 .le-head:active .le-grip 468 { border-color: var(--le-brand); 469 background-color: var(--le-brand-soft); 470 color: var(--le-brand-fg); 471 cursor: grabbing; 472 } 473 .le-grip:hover 474 { border-color: var(--le-brand); 475 color: var(--le-brand-fg); 476 } 477 .le-spacer 478 { flex: 10000 1 0; 479 } 480 481 .le-mode 482 { display: inline-flex; 483 gap: 0.6em; 484 align-items: center; 485 padding: 0.35em 0.9em 0.35em 0.75em; 486 border: 1px solid var(--le-line); 487 border-radius: 999px; 488 background-color: rgba(255, 255, 255, 0.04); 489 color: var(--le-muted); 490 font: inherit; 491 font-weight: 700; 492 cursor: pointer; 493 } 494 .le-mode:hover 495 { background-color: var(--le-chrome-2); 496 color: var(--le-fg); 497 } 498 .le-mode.le-on 499 { border-color: rgba(96, 165, 250, 0.45); 500 background-color: var(--le-info-soft); 501 color: #CFE0FF; 502 } 503 .le-mode .le-dot 504 { width: 0.6em; 505 height: 0.6em; 506 border-radius: 50%; 507 background-color: currentColor; 508 opacity: 0.35; 509 } 510 .le-mode.le-on .le-dot 511 { background-color: var(--le-info); 512 opacity: 1; 513 box-shadow: 0 0 0 0.23em rgba(96, 165, 250, 0.25); 514 } 515 516 .le-icon 517 { display: inline-flex; 518 flex: 0 0 auto; 519 align-items: center; 520 justify-content: center; 521 width: 2.15em; 522 height: 2.15em; 523 padding: 0; 524 border: 1px solid var(--le-line); 525 border-radius: 0.55em; 526 background: transparent; 527 color: var(--le-muted); 528 font: inherit; 529 cursor: pointer; 530 } 531 .le-icon:hover:not(:disabled) 532 { background-color: var(--le-chrome-2); 533 color: var(--le-fg); 534 } 535 .le-icon:disabled 536 { opacity: 0.35;
537 cursor: default; 538 } 539 540 /* ===== breadcrumb ===== */ 541 .le-path 542 { display: flex; 543 gap: 0.15em; 544 align-items: center; 545 overflow-x: auto; 546 padding: 0.6em 0.8em; 547 border-bottom: 1px solid var(--le-line); 548 background-color: var(--le-chrome-2); 549 white-space: nowrap; 550 } 551 .le-path::-webkit-scrollbar 552 { height: 0.3em; 553 } 554 .le-path::-webkit-scrollbar-thumb 555 { border-radius: 0.3em; 556 background-color: rgba(255, 255, 255, 0.1); 557 } 558 .le-crumb 559 { display: inline-flex; 560 flex: 0 0 auto; 561 gap: 0.3em; 562 align-items: center; 563 padding: 0.3em 0.6em; 564 border: 1px solid transparent; 565 border-radius: 0.45em; 566 background: transparent; 567 color: var(--le-muted); 568 font-family: var(--le-mono); 569 font-size: 92%; 570 cursor: pointer; 571 } 572 .le-crumb:hover 573 { background-color: var(--le-chrome-3); 574 color: var(--le-fg); 575 } 576 .le-crumb.le-on 577 { border-color: rgba(18, 184, 134, 0.3); 578 background-color: var(--le-brand-soft); 579 color: var(--le-brand); 580 } 581 .le-crumb b 582 { font-weight: 700; 583 } 584 .le-crumb span 585 { opacity: 0.7; 586 } 587 .le-crumb-sep 588 { padding: 0 0.15em; 589 color: var(--le-muted-2); 590 user-select: none; 591 } 592 .le-path-empty 593 { color: var(--le-muted-2); 594 font-size: 92%; 595 } 596 .le-crumb-add 597 { display: inline-grid; 598 flex: 0 0 auto; 599 place-items: center; 600 width: 1.6em; 601 height: 1.6em; 602 margin-left: 0.2em; 603 border: 1px dashed var(--le-line-2); 604 border-radius: 0.45em; 605 background: transparent; 606 color: var(--le-muted-2); 607 font: inherit; 608 line-height: 1; 609 cursor: pointer; 610 } 611 .le-crumb-add:hover 612 { border-style: solid; 613 background-color: var(--le-chrome-3); 614 color: var(--le-fg); 615 } 616 .le-crumb-add.le-on 617 { border-style: solid; 618 border-color: rgba(18, 184, 134, 0.3); 619 background-color: var(--le-brand-soft); 620 color: var(--le-brand); 621 } 622 .le-selrow 623 { display: flex; 624 flex-wrap: wrap; 625 gap: 0.4em; 626 align-items: center; 627 padding: 0.55em 0.8em; 628 border-bottom: 1px solid var(--le-line); 629 background-color: var(--le-chrome-2); 630 } 631 .le-selrow .le-input 632 { flex: 1 1 12em; 633 min-width: 0; 634 font-family: var(--le-mono); 635 } 636 .le-selrow .le-input.le-bad 637 { border-color: var(--le-danger); 638 } 639 .le-selrow-err 640 { flex: 1 0 100%; 641 color: var(--le-danger-fg); 642 font-size: 88%; 643 } 644 .le-grouprow 645 { display: flex; 646 gap: 0.5em; 647 align-items: center; 648 padding: 0.45em 0.8em; 649 border-bottom: 1px solid var(--le-line); 650 background-color: var(--le-chrome-2); 651 color: var(--le-muted); 652 font-size: 92%; 653 cursor: pointer; 654 } 655 .le-grouprow:hover 656 { color: var(--le-fg); 657 } 658 .le-grouprow.le-on 659 { background-color: var(--le-brand-soft); 660 color: var(--le-fg); 661 } 662 .le-grouprow b 663 { color: var(--le-brand-fg); 664 } 665 .le-grouprow code 666 { overflow: hidden; 667 margin-left: auto; 668 max-width: 45%; 669 color: var(--le-muted-2); 670 font-family: var(--le-mono); 671 font-size: 92%; 672 white-space: nowrap; 673 text-overflow: ellipsis; 674 } 675 676 /* ===== smart row ===== */ 677 .le-smart 678 { display: flex; 679 gap: 0.6em; 680 align-items: center; 681 padding: 0.6em 0.75em; 682 border-bottom: 1px solid var(--le-line); 683 background-color: rgba(255, 255, 255, 0.015); 684 } 685 .le-badge 686 { display: inline-flex; 687 flex: 0 0 auto; 688 gap: 0.4em; 689 align-items: center; 690 padding: 0.25em 0.7em; 691 border-radius: 999px; 692 background-color: var(--le-brand); 693 color: #08231C; 694 font-size: 77%; 695 font-weight: 900; 696 letter-spacing: 0.04em; 697 text-transform: uppercase; 698 } 699 .le-badge svg 700 { width: 0.85em; 701 height: 0.85em; 702 stroke-width: 2.5; 703 } 704 .le-smart-tiles 705 { display: flex; 706 flex: 10000 1 0; 707 gap: 0.45em; 708 overflow-x: auto; 709 scrollbar-width: thin; 710 } 711 .le-smart-tiles::-webkit-scrollbar 712 { height: 0.3em; 713 } 714 .le-smart-tiles::-webkit-scrollbar-thumb 715 { border-radius: 0.3em; 716 background-color: var(--le-line); 717 } 718 .le-smart-tile 719 { display: inline-flex; 720 flex: 0 0 auto; 721 gap: 0.4em; 722 align-items: center; 723 padding: 0.45em 0.75em; 724 border: 1px solid var(--le-line); 725 border-radius: 0.6em; 726 background-color: var(--le-chrome-2); 727 color: var(--le-fg); 728 font: inherit; 729 font-size: 85%; 730 font-weight: 600; 731 white-space: nowrap; 732 cursor: pointer; 733 } 734 .le-smart-tile:hover 735 { border-color: var(--le-line-2); 736 background-color: var(--le-chrome-3); 737 } 738 /* THE ONE COLOURED CONTROL IN THE ROW, and deliberately so. Every other tile there edits the 739 element in front of you; this one opens the settings BEHIND it - the questions, the 740 segments, the end date - which is the thing nobody found because it looked like all the 741 rest (Jacob, twice). Amber rather than the editor's own green, which already means "this 742 is applied". */ 743 .le-smart-tile.le-wkey 744 { border-color: rgba(245, 158, 11, 0.55); 745 background-color: rgba(245, 158, 11, 0.16); 746 color: #FCD34D; 747 } 748 .le-smart-tile.le-wkey:hover 749 { border-color: #F59E0B; 750 background-color: rgba(245, 158, 11, 0.28); 751 color: #FDE68A; 752 } 753 .le-smart-tile.le-wkey svg 754 { opacity: 1; 755 } 756 .le-smart-tile svg 757 { width: 0.95em; 758 height: 0.95em; 759 opacity: 0.8; 760 } 761 762 /* ===== tabs ===== */ 763 .le-tabs 764 { display: flex; 765 gap: 0.15em; 766 overflow-x: auto; 767 padding: 0.45em 0.75em 0; 768 border-bottom: 1px solid var(--le-line); 769 } 770 .le-tab 771 { margin-bottom: -1px; 772 padding: 0.6em 0.9em; 773 border: none; 774 border-bottom: 2px solid transparent; 775 border-radius: 0.55em 0.55em 0 0; 776 background: transparent; 777 color: var(--le-muted); 778 font: inherit; 779 font-weight: 600; 780 white-space: nowrap; 781 cursor: pointer; 782 } 783 .le-tab:hover 784 { background-color: rgba(255, 255, 255, 0.03); 785 color: var(--le-fg); 786 } 787 .le-tab.le-on 788 { border-bottom-color: var(--le-brand); 789 color: var(--le-fg); 790 } 791 792 /* ===== tile grid ===== */ 793 .le-body 794 { /* The only part that scrolls, and only once the toolbar has run out of window - the fixed 795 50vh cap this used to carry made the busiest tab scroll even when there was room below. */ 796 flex: 1 1 auto; 797 min-height: 0; 798 overflow: auto; 799 padding: 0.75em; 800 } 801 .le-toolbar > *:not(.le-body) 802 { flex: 0 0 auto; 803 } 804 .le-grid 805 { display: grid; 806 grid-template-columns: repeat(3, 1fr); 807 gap: 0.6em; 808 } 809 /* A widget tile says it can be dragged by behaving like something you can pick up. The cursor 810 is the whole affordance - a badge would be one more thing on a tile that already carries an 811 icon, a name and a shortcut. */ 812 .le-tile[data-drag] 813 { cursor: grab; 814 } 815 .le-tile[data-drag]:active 816 { cursor: grabbing; 817 } 818 .le-tile 819 { position: relative; 820 display: flex; 821 flex-direction: column; 822 gap: 0.45em; 823 align-items: center; 824 justify-content: center; 825 min-height: 5.6em; 826 padding: 0.8em 0.45em 0.7em; 827 border: 1px solid var(--le-line); 828 border-radius: 0.75em; 829 background-color: var(--le-chrome-2); 830 font: inherit; 831 text-align: center; 832 cursor: pointer; 833 } 834 .le-tile:hover 835 { border-color: var(--le-line-2); 836 background-color: var(--le-chrome-3); 837 } 838 .le-tile:active 839 { transform: translateY(1px); 840 } 841 .le-tile .le-tile-ico 842 { display: grid; 843 place-items: center; 844 width: 2.15em; 845 height: 2.15em; 846 border-radius: 0.55em; 847 background-color: rgba(255, 255, 255, 0.06); 848 color: var(--le-fg); 849 } 850 .le-tile .le-tile-txt 851 { width: 100%; 852 min-width: 0; 853 } 854 .le-tile h4 855 { margin: 0; 856 color: var(--le-fg); 857 font-size: 92%; 858 font-weight: 700; 859 line-height: 1.2; 860 } 861 /* The subtitle moved to the tile's tooltip. Three columns in a 32em toolbar leaves ~9em of
862 width, which is not a line of prose - and the icon with the title already says it. */ 863 .le-tile p 864 { display: none; 865 } 866 .le-tile > kbd 867 { position: absolute; 868 top: 0.3em; 869 right: 0.35em; 870 opacity: 0.75; 871 } 872 .le-tile-badge 873 { position: absolute; 874 top: 0.3em; 875 right: 0.35em; 876 padding: 0.1em 0.45em; 877 border-radius: 0.3em; 878 background-color: var(--le-brand); 879 color: #08231C; 880 font-size: 70%; 881 font-weight: 900; 882 letter-spacing: 0.04em; 883 text-transform: uppercase; 884 } 885 886 /* ===== sub-panel ===== */ 887 .le-sub-head 888 { display: flex; 889 gap: 0.6em; 890 align-items: center; 891 margin-bottom: 0.9em; 892 padding: 0.45em 0.3em 0.75em; 893 border-bottom: 1px solid var(--le-line); 894 } 895 .le-sub-head b 896 { font-size: 100%; 897 font-weight: 800; 898 } 899 .le-back 900 { display: inline-flex; 901 gap: 0.3em; 902 align-items: center; 903 padding: 0.3em 0.6em 0.3em 0.45em; 904 border: 1px solid var(--le-line); 905 border-radius: 0.45em; 906 background: transparent; 907 color: var(--le-muted); 908 font: inherit; 909 font-size: 92%; 910 font-weight: 600; 911 cursor: pointer; 912 } 913 .le-back:hover 914 { border-color: var(--le-line-2); 915 background-color: var(--le-chrome-2); 916 color: var(--le-fg); 917 } 918 .le-target 919 { display: inline-flex; 920 gap: 0.45em; 921 align-items: center; 922 max-width: 100%; 923 margin-bottom: 0.9em; 924 padding: 0.3em 0.6em; 925 border: 1px solid var(--le-line); 926 border-radius: 0.45em; 927 background-color: var(--le-chrome-2); 928 color: var(--le-muted); 929 font-family: var(--le-mono); 930 font-size: 85%; 931 } 932 .le-target::before 933 { content: ""; 934 flex: 0 0 auto; 935 width: 0.45em; 936 height: 0.45em; 937 border-radius: 50%; 938 background-color: var(--le-brand); 939 box-shadow: 0 0 0 0.23em var(--le-brand-soft); 940 } 941 .le-target code 942 { overflow: hidden; 943 color: var(--le-brand); 944 font-family: inherit; 945 white-space: nowrap; 946 text-overflow: ellipsis; 947 } 948 /* A chip in the footer beside Cancel and Save, green when on - the same "chosen" green as Put 949 it and Apply on, because it is the same kind of answer. */ 950 .le-count-clicks 951 { display: inline-flex; 952 flex: 0 0 auto; 953 gap: 0.4em; 954 align-items: center; 955 padding: 0.3em 0.6em; 956 border: 1px solid var(--le-line); 957 border-radius: 0.45em; 958 background-color: var(--le-chrome-2); 959 color: var(--le-muted); 960 font-size: 85%; 961 font-weight: 600; 962 white-space: nowrap; 963 cursor: pointer; 964 user-select: none; 965 } 966 .le-count-clicks:hover 967 { border-color: var(--le-line-2); 968 color: var(--le-fg); 969 } 970 .le-count-clicks.le-on 971 { border-color: var(--le-brand); 972 background-color: var(--le-brand-soft); 973 color: var(--le-brand-fg); 974 } 975 .le-count-clicks.le-locked 976 { cursor: default; 977 } 978 .le-count-clicks .le-check 979 { width: 1em; 980 height: 1em; 981 margin: 0; 982 accent-color: var(--le-brand); 983 } 984 /* The same answer on a row of the changes list, as a toggle: green while that change counts 985 clicks, a quiet outline while it does not. */ 986 .le-clicks 987 { display: inline-flex; 988 flex: 0 0 auto; 989 gap: 0.3em; 990 align-items: center; 991 padding: 0.1em 0.45em; 992 border: 1px solid var(--le-line); 993 border-radius: 0.9em; 994 background: transparent; 995 color: var(--le-muted); 996 font: inherit; 997 font-size: 78%; 998 white-space: nowrap; 999 cursor: pointer; 1000 } 1001 .le-clicks svg 1002 { width: 1em; 1003 height: 1em; 1004 } 1005 .le-clicks:hover:not(:disabled) 1006 { border-color: var(--le-line-2); 1007 color: var(--le-fg); 1008 } 1009 .le-clicks.le-on 1010 { border-color: var(--le-brand); 1011 background-color: var(--le-brand-soft); 1012 color: var(--le-brand-fg); 1013 } 1014 .le-clicks.le-warn 1015 { border-color: rgba(245, 158, 11, 0.55); 1016 background-color: rgba(245, 158, 11, 0.16); 1017 color: #FCD34D; 1018 } 1019 .le-clicks:disabled 1020 { opacity: 0.5; 1021 cursor: default; 1022 } 1023 1024 /* Sections of one sub-panel. Segmented pills on purpose - the toolbar's own tabs are underlined, 1025 and these switch which part of a form you are on, not what the editor is offering. */ 1026 .le-style-note 1027 { margin-bottom: 0.8em; 1028 } 1029 .le-ptabs 1030 { display: flex; 1031 gap: 0.15em; 1032 overflow-x: auto; 1033 margin-bottom: 1em; 1034 padding: 0.25em; 1035 border: 1px solid var(--le-line); 1036 border-radius: 0.6em; 1037 background-color: var(--le-chrome-2); 1038 } 1039 .le-ptabs::-webkit-scrollbar 1040 { height: 0.25em; 1041 } 1042 .le-ptab 1043 { flex: 1 1 auto; 1044 padding: 0.4em 0.7em; 1045 border: none; 1046 border-radius: 0.42em; 1047 background: transparent; 1048 color: var(--le-muted); 1049 font: inherit; 1050 font-size: 92%; 1051 font-weight: 600; 1052 white-space: nowrap; 1053 cursor: pointer; 1054 } 1055 .le-ptab:hover 1056 { color: var(--le-fg); 1057 } 1058 .le-ptab.le-on 1059 { background-color: var(--le-chrome-3);
1060 color: var(--le-fg); 1061 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); 1062 } 1063 1064 /* The AI card as one line, for a panel where the whole card would push the controls off screen. */ 1065 .le-aicard-open 1066 { display: flex; 1067 width: 100%; 1068 gap: 0.6em; 1069 align-items: center; 1070 margin-bottom: 1em; 1071 padding: 0.5em 0.7em; 1072 border: 1px solid rgba(168, 85, 247, 0.28); 1073 border-radius: 0.6em; 1074 background: linear-gradient(160deg, rgba(168, 85, 247, 0.12), rgba(18, 184, 134, 0.05)); 1075 color: var(--le-fg); 1076 font: inherit; 1077 font-weight: 700; 1078 text-align: left; 1079 cursor: pointer; 1080 } 1081 .le-aicard-open:hover 1082 { border-color: rgba(168, 85, 247, 0.5); 1083 } 1084 .le-aicard-open > span:nth-child(2) 1085 { flex: 1 1 auto; 1086 } 1087 .le-aicard-open > svg 1088 { flex: 0 0 auto; 1089 color: var(--le-muted-2); 1090 } 1091 1092 .le-sec 1093 { margin-bottom: 1.3em; 1094 } 1095 /* A hairline beside the heading: the sections used to run together in one flat column, which is 1096 what made a long panel (Style has six of them) hard to scan. */ 1097 .le-sec > h5 1098 { display: flex; 1099 gap: 0.7em; 1100 align-items: center; 1101 margin: 0 0 0.7em; 1102 color: var(--le-muted-2); 1103 font-size: 77%; 1104 font-weight: 800; 1105 letter-spacing: 0.06em; 1106 text-transform: uppercase; 1107 } 1108 .le-sec > h5::after 1109 { flex: 1 1 auto; 1110 height: 1px; 1111 background-color: var(--le-line); 1112 content: ""; 1113 } 1114 .le-field 1115 { display: grid; 1116 grid-template-columns: 7.2em 1fr; 1117 gap: 0.75em; 1118 align-items: center; 1119 min-height: 2.3em; 1120 } 1121 .le-field > label 1122 { color: var(--le-muted); 1123 font-size: 92%; 1124 line-height: 1.2; 1125 } 1126 .le-field-group 1127 { display: flex; 1128 flex-wrap: wrap; 1129 gap: 0.45em; 1130 align-items: center; 1131 } 1132 .le-hint 1133 { margin: 0 0 0.6em; 1134 color: var(--le-muted); 1135 font-size: 85%; 1136 line-height: 1.5; 1137 } 1138 1139 .le-color 1140 { display: flex; 1141 gap: 0.45em; 1142 align-items: center; 1143 padding: 0.23em 0.45em 0.23em 0.3em; 1144 border: 1px solid var(--le-line); 1145 border-radius: 0.45em; 1146 background-color: var(--le-chrome-2); 1147 } 1148 .le-color .le-swatch 1149 { position: relative; 1150 overflow: hidden; 1151 width: 1.7em; 1152 height: 1.7em; 1153 padding: 0; 1154 /* A ring rather than a border, and a light one: the swatch sits on the dark chrome, and a 1155 near-black colour (which a page's body text usually is) was indistinguishable from an 1156 empty box without it. */ 1157 border: none; 1158 border-radius: 0.3em; 1159 box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35), 0 0 0 1px rgba(0, 0, 0, 0.4); 1160 cursor: pointer; 1161 } 1162 /* Nothing set: the diagonal says "no colour of its own" rather than showing a black square. */ 1163 .le-color .le-swatch.le-empty 1164 { background-image: linear-gradient(135deg, transparent 44%, var(--le-danger) 44%, var(--le-danger) 56%, transparent 56%); 1165 background-color: var(--le-chrome-3); 1166 } 1167 .le-color .le-swatch input 1168 { position: absolute; 1169 inset: -0.3em; 1170 width: calc(100% + 0.6em); 1171 height: calc(100% + 0.6em); 1172 padding: 0; 1173 border: none; 1174 opacity: 0; 1175 cursor: pointer; 1176 } 1177 .le-color input[type=text] 1178 { flex: 10000 1 0; 1179 min-width: 0; 1180 padding: 0.23em 0.15em; 1181 border: none; 1182 background: transparent; 1183 color: var(--le-fg); 1184 outline: none; 1185 font-family: var(--le-mono); 1186 font-size: 92%; 1187 } 1188 1189 .le-num 1190 { display: inline-flex; 1191 align-items: center; 1192 border: 1px solid var(--le-line); 1193 border-radius: 0.45em; 1194 background-color: var(--le-chrome-2); 1195 } 1196 .le-num:focus-within, 1197 .le-color:focus-within 1198 { border-color: rgba(18, 184, 134, 0.5); 1199 } 1200 .le-num input 1201 { /* Wide enough for a computed value with a decimal ("29.4") next to the spinner, which the 1202 browser draws INSIDE the box - at the old 4.5em the digits went under it. */ 1203 width: 5.4em; 1204 padding: 0.4em 0.1em 0.4em 0.55em; 1205 border: none; 1206 background: transparent; 1207 color: var(--le-fg); 1208 outline: none; 1209 font: inherit; 1210 font-size: 92%; 1211 font-variant-numeric: tabular-nums; 1212 text-align: left; 1213 } 1214 .le-num .le-unit 1215 { padding: 0.4em 0.6em; 1216 border-left: 1px solid var(--le-line); 1217 color: var(--le-muted); 1218 font-size: 92%; 1219 user-select: none; 1220 } 1221 1222 .le-input, 1223 .le-select 1224 { max-width: 100%; 1225 padding: 0.4em 0.6em; 1226 border: 1px solid var(--le-line); 1227 border-radius: 0.45em; 1228 background-color: var(--le-chrome-2); 1229 color: var(--le-fg); 1230 outline: none; 1231 font: inherit; 1232 font-size: 92%; 1233 } 1234 .le-input 1235 { width: 100%; 1236 } 1237 .le-input:focus, 1238 .le-area:focus 1239 { border-color: rgba(18, 184, 134, 0.5); 1240 } 1241 .le-area 1242 { width: 100%; 1243 min-height: 5em; 1244 padding: 0.6em 0.75em; 1245 border: 1px solid var(--le-line); 1246 border-radius: 0.55em; 1247 background-color: var(--le-chrome-2); 1248 color: var(--le-fg); 1249 outline: none; 1250 font: inherit; 1251 font-size: 100%; 1252 line-height: 1.4; 1253 resize: vertical; 1254 } 1255 .le-area.le-mono 1256 { font-family: var(--le-mono); 1257 font-size: 92%; 1258 } 1259 1260 .le-icongroup 1261 { display: inline-flex; 1262 gap: 0.15em; 1263 padding: 0.15em; 1264 border: 1px solid var(--le-line); 1265 border-radius: 0.45em; 1266 background-color: var(--le-chrome-2); 1267 } 1268 .le-icongroup button 1269 { display: grid; 1270 place-items: center; 1271 width: 2.15em; 1272 height: 1.85em; 1273 border: none; 1274 border-radius: 0.3em; 1275 background: transparent; 1276 color: var(--le-muted); 1277 font: inherit; 1278 cursor: pointer; 1279 } 1280 .le-icongroup button:hover, 1281 .le-icongroup button.le-on 1282 { background-color: var(--le-chrome-3);
1283 color: var(--le-fg); 1284 } 1285 1286 .le-presets 1287 { display: flex; 1288 flex-wrap: wrap; 1289 gap: 0.45em; 1290 } 1291 .le-preset 1292 { padding: 0.4em 0.75em; 1293 border: 1px solid var(--le-line); 1294 border-radius: 999px; 1295 background-color: var(--le-chrome-2); 1296 color: var(--le-muted); 1297 font: inherit; 1298 font-size: 92%; 1299 font-weight: 600; 1300 cursor: pointer; 1301 } 1302 .le-preset:hover 1303 { border-color: var(--le-line-2); 1304 background-color: var(--le-chrome-3); 1305 color: var(--le-fg); 1306 } 1307 1308 .le-btn 1309 { display: inline-flex; 1310 gap: 0.4em; 1311 align-items: center; 1312 justify-content: center; 1313 padding: 0.45em 0.9em; 1314 border: 1px solid var(--le-line); 1315 border-radius: 0.5em; 1316 background: transparent; 1317 color: var(--le-muted); 1318 font: inherit; 1319 font-size: 92%; 1320 font-weight: 600; 1321 white-space: nowrap; 1322 cursor: pointer; 1323 } 1324 .le-btn:hover:not(:disabled) 1325 { border-color: var(--le-line-2); 1326 background-color: var(--le-chrome-3); 1327 color: var(--le-fg); 1328 } 1329 .le-btn:disabled 1330 { opacity: 0.5; 1331 cursor: default; 1332 } 1333 .le-btn.le-primary 1334 { border-color: transparent; 1335 background-color: var(--le-brand); 1336 color: #08231C; 1337 font-weight: 800; 1338 } 1339 .le-btn.le-primary:hover:not(:disabled) 1340 { background-color: #15CE97; 1341 } 1342 .le-btn.le-danger 1343 { border-color: rgba(239, 68, 68, 0.3); 1344 color: var(--le-danger-fg); 1345 } 1346 .le-btn.le-danger:hover:not(:disabled) 1347 { background-color: var(--le-danger-soft);
1348 color: #FFF; 1349 } 1350 .le-btn-row-note 1351 { flex: 1 1 auto; 1352 margin: 0 auto 0 0; 1353 font-size: 85%; 1354 } 1355 /* ONE ROW, AND A ROW OF THE PANEL - not of the scrolling body above it. 1356 1357 It used to be sticky inside .le-body, which cost three things at once: a strip of dead 1358 space below it where the scrolled content showed through, a tooltip clipped by that 1359 element's own overflow:auto, and a footer tall enough - a full-width select with a paragraph 1360 under it - to crowd the panel it belongs to. As a sibling it sits flush on the bottom edge, 1361 needs no sticky, and can draw over the body. Reported with screenshots (Jacob, 26 Aug 26). */ 1362 .le-foot 1363 { display: flex; 1364 flex-wrap: wrap; 1365 align-items: center; 1366 gap: 0.5em; 1367 padding: 0.6em 0.75em; 1368 border-top: 1px solid var(--le-line); 1369 background: var(--le-chrome); 1370 border-radius: 0 0 1.1em 1.1em; 1371 } 1372 /* Apply on to the left, the save group to the right. Neither shrinks - squeezed, the picker was 1373 cut to "Sa..." - so on a panel too narrow for both, the save group drops to a line of its own, 1374 still right-aligned and still whole. */ 1375 .le-foot-scope 1376 { display: flex; 1377 flex: 0 0 auto; 1378 gap: 0.5em; 1379 align-items: center; 1380 min-width: 0; 1381 } 1382 .le-foot-save 1383 { display: flex; 1384 gap: 0.5em; 1385 align-items: center; 1386 margin-left: auto; 1387 } 1388 .le-scope-lbl 1389 { display: inline-flex; 1390 align-items: center; 1391 gap: 0.3em; 1392 flex: 0 0 auto; 1393 font-size: 85%; 1394 opacity: 0.8; 1395 cursor: help; 1396 } 1397 .le-scope-lbl svg 1398 { width: 1.05em; 1399 height: 1.05em; 1400 opacity: 0.7; 1401 } 1402 /* Drawn as a choice already made - the green of the selected "Put it" button - because it is 1403 one: a dropdown in the footer's grey read as small print, and it decides which pages this 1404 edit reaches (Jacob, 28 Sep 26). The insert panels ask the same thing as buttons instead. */ 1405 /* Both classes: .le-scope-pick, further down, is the plain grey picker the changes list uses, 1406 and at equal weight the later rule won - the footer stayed grey. */ 1407 .le-scope-pick.le-scope-foot 1408 { flex: 0 0 auto; 1409 min-width: 7em; 1410 padding: 0.25em 0.45em; 1411 border-color: var(--le-brand); 1412 border-radius: 0.45em; 1413 background-color: var(--le-brand-soft); 1414 color: var(--le-brand-fg); 1415 font-size: 85%; 1416 font-weight: 600; 1417 cursor: pointer; 1418 } 1419 .le-scope-pick.le-scope-foot:hover 1420 { border-color: var(--le-brand); 1421 color: var(--le-fg); 1422 } 1423 /* In a Where section it is a field label like "Put it" above it, not footer small print. */ 1424 .le-field > .le-scope-lbl 1425 { opacity: 1; 1426 } 1427 /* Two rows of the same buttons, one under the other: apart enough to read as two questions. */ 1428 .le-where-scope 1429 { margin-top: 0.5em; 1430 } 1431 /* Same amber as the smart row's, for the same reason: this is the way INTO the widget, not 1432 another edit of the element it sits in. One colour, two places, one meaning. */ 1433 .le-btn.le-wkey 1434 { border-color: rgba(245, 158, 11, 0.55); 1435 background-color: rgba(245, 158, 11, 0.16); 1436 color: #FCD34D; 1437 } 1438 .le-btn.le-wkey:hover:not(:disabled) 1439 { border-color: #F59E0B; 1440 background-color: rgba(245, 158, 11, 0.28); 1441 color: #FDE68A; 1442 } 1443 1444 /* One answer for the whole action, above the rows. The rows are where a change says what it 1445 does; this is where somebody says what they meant. */ 1446 .le-side-all 1447 { display: flex; 1448 align-items: center; 1449 gap: 0.5em; 1450 padding: 0.5em 0.9em; 1451 border-bottom: 1px solid var(--le-line); 1452 font-size: 88%; 1453 color: var(--le-muted); 1454 } 1455 .le-side-all .le-scope-pick 1456 { flex: 1 1 auto; 1457 font-size: 100%; 1458 } 1459 1460 /* A choice of three, all three visible. A dropdown hides two of its answers until it is 1461 opened, and this is the one people change most often. */ 1462 .le-seg 1463 { display: flex; 1464 gap: 0.35em; 1465 width: 100%; 1466 } 1467 .le-seg-btn 1468 { flex: 1 1 0; 1469 display: flex; 1470 flex-direction: column; 1471 align-items: center; 1472 gap: 0.25em; 1473 padding: 0.5em 0.3em; 1474 border: 1px solid var(--le-line); 1475 border-radius: 0.55em; 1476 background-color: var(--le-chrome-2); 1477 color: var(--le-muted); 1478 font: inherit; 1479 font-size: 85%; 1480 font-weight: 600; 1481 cursor: pointer; 1482 } 1483 .le-seg-btn svg 1484 { width: 1.5em; 1485 height: 1.5em; 1486 fill: none; 1487 stroke: currentColor; 1488 stroke-width: 1.6; 1489 opacity: 0.85; 1490 } 1491 .le-seg-btn:hover 1492 { border-color: var(--le-line-2); 1493 background-color: var(--le-chrome-3);
1494 color: var(--le-fg); 1495 } 1496 .le-seg-btn.le-on 1497 { border-color: var(--le-brand); 1498 background-color: var(--le-brand-soft); 1499 color: var(--le-brand-fg); 1500 } 1501 .le-seg-btn.le-on svg 1502 { opacity: 1; 1503 } 1504 1505 .le-timer 1506 { display: flex; 1507 flex-direction: column; 1508 gap: 0.5em; 1509 width: 100%; 1510 } 1511 .le-timer .le-field 1512 { margin: 0; 1513 } 1514 .le-inline-check 1515 { display: flex; 1516 align-items: center; 1517 gap: 0.45em; 1518 margin-top: 0.4em; 1519 font-size: 88%; 1520 color: var(--le-muted); 1521 cursor: pointer; 1522 } 1523 1524 .le-scope-row 1525 { display: flex; 1526 flex-wrap: wrap; 1527 align-items: center; 1528 gap: 0.4em; 1529 margin-top: 0.3em; 1530 } 1531 .le-scope-pick 1532 { flex: 0 1 auto; 1533 max-width: 100%; 1534 padding: 0.1em 0.3em; 1535 border: 1px solid var(--le-line); 1536 border-radius: 0.3em; 1537 background: transparent; 1538 color: var(--le-muted); 1539 font: inherit; 1540 font-size: 80%; 1541 } 1542 .le-scope-pick:hover 1543 { border-color: var(--le-line-2); 1544 color: var(--le-fg); 1545 } 1546 /* Not a warning colour: the change is not broken, it is simply for somewhere else. A change 1547 that IS broken has no row at all. */ 1548 .le-offpage 1549 { padding: 0.05em 0.45em; 1550 border-radius: 0.9em; 1551 background: var(--le-info-soft); 1552 color: var(--le-info); 1553 font-size: 78%; 1554 white-space: nowrap; 1555 } 1556 .le-scope-chip 1557 { display: inline-block; 1558 margin-left: 0.4em; 1559 padding: 0.05em 0.45em; 1560 border: 1px solid var(--le-line); 1561 border-radius: 0.9em; 1562 font-size: 78%; 1563 opacity: 0.75; 1564 vertical-align: 0.05em; 1565 } 1566 1567 /* ALWAYS REACHABLE. A long panel \u2014 a widget with twenty settings, a list of FAQ rows \u2014 scrolled 1568 Save off the bottom, and a Save you have to hunt for reads as a panel with no way out 1569 (Jacob). Stuck to the bottom of the scrolling area instead, with the panel's own background 1570 behind it so the controls do not show through as they pass under. 1571 A z-index because a colour input's swatch and the rows editor both paint above the flow. */ 1572 .le-btn-row 1573 { position: sticky; 1574 bottom: 0; 1575 z-index: 3; 1576 background: var(--le-chrome); 1577 display: flex; 1578 gap: 0.6em; 1579 align-items: center; 1580 justify-content: flex-end; 1581 margin-top: 0.9em; 1582 padding-top: 0.9em; 1583 border-top: 1px solid var(--le-line); 1584 } 1585 1586 /* Formatting toolbar of the rich-text editor */ 1587 .le-rt 1588 { display: flex; 1589 flex-wrap: wrap; 1590 gap: 0.3em; 1591 margin-bottom: 0.6em; 1592 } 1593 .le-rt button 1594 { min-width: 2.15em; 1595 height: 1.85em; 1596 padding: 0 0.45em; 1597 border: 1px solid var(--le-line); 1598 border-radius: 0.3em; 1599 background-color: var(--le-chrome-2); 1600 color: var(--le-fg); 1601 font: inherit; 1602 font-size: 92%; 1603 cursor: pointer; 1604 } 1605 .le-rt button:hover 1606 { background-color: var(--le-chrome-3); 1607 } 1608 1609 /* ===== notices ===== */ 1610 .le-notice 1611 { display: flex; 1612 gap: 0.6em; 1613 align-items: flex-start; 1614 margin-bottom: 0.75em; 1615 padding: 0.6em 0.75em; 1616 border: 1px solid rgba(96, 165, 250, 0.25); 1617 border-radius: 0.6em; 1618 background-color: rgba(96, 165, 250, 0.07); 1619 color: var(--le-muted); 1620 font-size: 92%; 1621 line-height: 1.5; 1622 } 1623 .le-notice.le-warn 1624 { border-color: rgba(239, 68, 68, 0.35); 1625 background-color: var(--le-danger-soft);
1626 color: var(--le-danger-fg); 1627 } 1628 .le-notice > div 1629 { flex: 10000 1 0; 1630 } 1631 .le-notice b 1632 { color: var(--le-fg); 1633 } 1634 1635 /* ===== navigate-mode banner ===== */ 1636 .le-navbanner 1637 { top: 1em; 1638 left: 50%; 1639 display: flex; 1640 gap: 0.6em; 1641 align-items: center; 1642 max-width: min(92vw, 42em); 1643 padding: 0.55em 1.1em 0.55em 0.9em; 1644 border: 1px solid rgba(96, 165, 250, 0.45); 1645 border-radius: 999px; 1646 /* Opaque, unlike every other blue-tinted surface here: this one floats over the CUSTOMER's 1647 page rather than over the editor's own chrome. The mockup's translucent 1648 rgba(96,165,250,.15) was chosen against its near-black demo page; on a white site it 1649 turns into a smear with the page's own text showing through it. */ 1650 background-color: var(--le-chrome); 1651 color: var(--le-fg); 1652 /* No font-size of its own on purpose. The .le-root rule and this one land on the SAME 1653 element, so a percentage here re-bases on the host's initial 16px instead of the 13px 1654 every other surface inherits - which made this banner render ~13% larger than the rest 1655 of the chrome, and would put the em offsets below out of step with the sidebar's 32em. */ 1656 font-weight: 500; 1657 line-height: 1.35; 1658 transform: translateX(-50%); 1659 box-shadow: 0 0.75em 2.3em rgba(0, 0, 0, 0.35); 1660 /* Navigate mode means clicks reach the page like a visitor's. A chip pinned over the site's 1661 own menu must not be what swallows them - so the banner is see-through to the pointer, 1662 and the only interactive thing it ever holds opts back in below. */ 1663 pointer-events: none; 1664 } 1665 /* Centred on the page, not on the window: the sidebar covers the right 32em of it, and a banner 1666 centred on the whole viewport runs under it - which is where the end of the sentence went. */ 1667 .le-navbanner.le-avoid-right 1668 { transform: translateX(calc(-50% - 16em)); 1669 } 1670 .le-navbanner.le-avoid-left 1671 { transform: translateX(calc(-50% + 16em)); 1672 } 1673 .le-navbanner b 1674 { color: #FFF; 1675 font-weight: 700; 1676 } 1677 .le-navbanner kbd 1678 { border-color: rgba(255, 255, 255, 0.25); 1679 background-color: rgba(255, 255, 255, 0.14); 1680 color: #FFF; 1681 } 1682 .le-navbanner svg 1683 { color: #FBBF55; 1684 } 1685 .le-navbanner .le-btn 1686 { flex: 0 0 auto; 1687 pointer-events: auto; 1688 } 1689 1690 /* ===== right sidebar ===== */ 1691 .le-side 1692 { /* Below everything else the tool draws. Every .le-root shares one z-index, so which of two 1693 overlapping panels won came down to DOM order - and this one renders last, so it buried 1694 the floating toolbar whenever the two met. The toolbar is where the work happens and it 1695 is the one that moves, so it is the one that stays on top. (The panels also step around 1696 each other - see the docking effect in LiveTool - so this only settles the cases where 1697 they cannot both fit, like a 375px mobile preview.) */ 1698 z-index: 2147483645; 1699 top: 0; 1700 right: 0; 1701 bottom: 0; 1702 display: flex; 1703 flex-direction: column; 1704 width: 32em; 1705 max-width: 92vw; 1706 border-left: 1px solid var(--le-line); 1707 background-color: var(--le-chrome); 1708 box-shadow: -0.75em 0 2.3em rgba(0, 0, 0, 0.35); 1709 } 1710 .le-side.le-left 1711 { right: auto; 1712 left: 0; 1713 border-right: 1px solid var(--le-line); 1714 border-left: none; 1715 box-shadow: 0.75em 0 2.3em rgba(0, 0, 0, 0.35); 1716 } 1717 .le-side.le-collapsed 1718 { transform: translateX(calc(100% - 2.15em)); 1719 } 1720 .le-side.le-left.le-collapsed 1721 { transform: translateX(calc(-100% + 2.15em)); 1722 } 1723 .le-peek 1724 { position: absolute; 1725 top: 50%; 1726 left: -1.1em; 1727 display: grid; 1728 place-items: center; 1729 width: 2.15em; 1730 height: 4.3em; 1731 border: 1px solid var(--le-line); 1732 border-left: none; 1733 border-radius: 0 1.1em 1.1em 0; 1734 background-color: var(--le-chrome); 1735 color: var(--le-muted); 1736 cursor: pointer; 1737 transform: translateY(-50%); 1738 } 1739 /* Re-asserted above the [data-tooltip] rule near the top of this sheet, which sets 1740 position:relative on every tooltip trigger and outranks the .le-peek rule (0,2,0 against 1741 0,1,0). This tab is a trigger AND positions itself; losing that put it back in the flex flow, 1742 where it held a 4em band of nothing at the top of the sidebar while its own top:50% still 1743 drew it at the middle. Any other trigger that positions itself has to do the same. */ 1744 .le-root.le-side .le-peek 1745 { position: absolute; 1746 } 1747 .le-side.le-left .le-peek 1748 { right: -1.1em; 1749 left: auto;
1750 border-right: none; 1751 border-left: 1px solid var(--le-line); 1752 border-radius: 1.1em 0 0 1.1em; 1753 } 1754 1755 .le-side-top 1756 { display: flex; 1757 flex: 0 0 auto; 1758 gap: 0.75em; 1759 align-items: center; 1760 padding: 0.9em 1.1em; 1761 border-bottom: 1px solid var(--le-line); 1762 } 1763 .le-logo 1764 { display: grid; 1765 flex: 0 0 auto; 1766 place-items: center; 1767 overflow: hidden; 1768 width: 2.3em; 1769 height: 2.3em; 1770 border-radius: 0.55em; 1771 background-color: #FFF; 1772 } 1773 .le-logo img 1774 { display: block; 1775 width: 1.7em; 1776 height: 1.7em; 1777 } 1778 .le-side-title 1779 { flex: 10000 1 0; 1780 min-width: 0; 1781 } 1782 .le-side-title b 1783 { display: block; 1784 font-size: 108%; 1785 font-weight: 800; 1786 } 1787 .le-ctx 1788 { display: flex; 1789 gap: 0.4em; 1790 align-items: center; 1791 overflow: hidden; 1792 color: var(--le-muted); 1793 font-size: 85%; 1794 } 1795 .le-ctx-campaign 1796 { overflow: hidden; 1797 max-width: 42%; 1798 font-weight: 600; 1799 white-space: nowrap; 1800 text-overflow: ellipsis; 1801 } 1802 .le-ctx-action 1803 { overflow: hidden; 1804 flex: 10000 1 0; 1805 min-width: 0; 1806 color: var(--le-fg); 1807 font-weight: 700; 1808 white-space: nowrap; 1809 text-overflow: ellipsis; 1810 } 1811 1812 .le-urlbar-wrap 1813 { flex: 0 0 auto; 1814 padding: 0.75em 0.9em; 1815 border-bottom: 1px solid var(--le-line); 1816 } 1817 .le-urlbar 1818 { display: flex; 1819 gap: 0.3em; 1820 align-items: center; 1821 padding: 0.3em 0.3em 0.3em 0.75em; 1822 border: 1px solid var(--le-line); 1823 border-radius: 0.7em; 1824 background-color: var(--le-chrome-2); 1825 } 1826 .le-urlbar input 1827 { flex: 10000 1 0; 1828 min-width: 0; 1829 padding: 0.45em 0.15em; 1830 border: none; 1831 background: transparent; 1832 color: var(--le-fg); 1833 outline: none; 1834 font-family: var(--le-mono); 1835 font-size: 92%; 1836 } 1837 .le-urlbar .le-go 1838 { padding: 0.45em 1.1em; 1839 border: none; 1840 border-radius: 0.45em; 1841 background-color: var(--le-brand); 1842 color: #08231C; 1843 font: inherit; 1844 font-weight: 800; 1845 cursor: pointer; 1846 } 1847 1848 .le-changes-head 1849 { display: flex; 1850 flex: 0 0 auto; 1851 gap: 0.75em; 1852 align-items: center; 1853 padding: 0.75em 0.9em; 1854 border-bottom: 1px solid var(--le-line); 1855 background-color: var(--le-chrome-2); 1856 } 1857 .le-changes-head b 1858 { font-size: 100%; 1859 } 1860 .le-count 1861 { padding: 0.15em 0.6em; 1862 border: 1px solid var(--le-line); 1863 border-radius: 999px; 1864 background-color: rgba(255, 255, 255, 0.06); 1865 color: var(--le-muted); 1866 font-family: var(--le-mono); 1867 font-size: 85%; 1868 } 1869 .le-nav-row 1870 { display: inline-flex; 1871 gap: 0.3em; 1872 align-items: center; 1873 margin-left: auto; 1874 } 1875 .le-nav-row .le-icon 1876 { width: 1.85em; 1877 height: 1.85em; 1878 } 1879 1880 .le-discardrow 1881 { display: flex; 1882 flex: 0 0 auto; 1883 flex-wrap: wrap; 1884 gap: 0.4em; 1885 align-items: center; 1886 justify-content: flex-end; 1887 padding: 0.5em 0.9em; 1888 border-bottom: 1px solid var(--le-line); 1889 } 1890 .le-discardrow > span 1891 { flex: 1 1 10em; 1892 color: var(--le-muted); 1893 font-size: 92%; 1894 } 1895 .le-side-body 1896 { display: grid; 1897 flex: 10000 1 auto; 1898 gap: 0.6em; 1899 align-content: start; 1900 overflow: auto; 1901 padding: 0.75em; 1902 } 1903 .le-change 1904 { display: grid; 1905 gap: 0.6em; 1906 padding: 0.75em 0.9em; 1907 border: 1px solid var(--le-line); 1908 border-radius: 0.75em; 1909 background-color: var(--le-chrome-2); 1910 cursor: pointer; 1911 } 1912 .le-change.le-on 1913 { border-color: rgba(18, 184, 134, 0.5); 1914 box-shadow: 0 0 0 1px rgba(18, 184, 134, 0.25); 1915 } 1916 .le-change.le-undone 1917 { opacity: 0.5; 1918 } 1919 .le-change-top 1920 { display: flex; 1921 gap: 0.6em; 1922 align-items: flex-start; 1923 } 1924 .le-tag 1925 { flex: 0 0 auto; 1926 margin-top: 0.1em; 1927 padding: 0.23em 0.55em; 1928 border-radius: 0.3em; 1929 font-size: 77%; 1930 font-weight: 800; 1931 letter-spacing: 0.04em; 1932 line-height: 1.2; 1933 text-transform: uppercase; 1934 } 1935 .le-tag.le-t-text { background-color: rgba(96, 165, 250, 0.15); color: #7FB5FF } 1936 .le-tag.le-t-style { background-color: rgba(168, 85, 247, 0.15); color: #C59BFF } 1937 .le-tag.le-t-insert { background-color: rgba(18, 184, 134, 0.18); color: #4DD5A9 } 1938 .le-tag.le-t-delete { background-color: rgba(239, 68, 68, 0.15); color: #FF8787 } 1939 .le-tag.le-t-move { background-color: rgba(245, 158, 11, 0.15); color: #FBBF55 } 1940 .le-tag.le-t-behavior { background-color: rgba(96, 165, 250, 0.15); color: #7FB5FF } 1941 .le-tag.le-t-other { background-color: rgba(255, 255, 255, 0.08); color: var(--le-muted) } 1942 .le-change-desc 1943 { flex: 10000 1 0; 1944 color: var(--le-fg); 1945 font-weight: 600; 1946 line-height: 1.35; 1947 word-break: break-word; 1948 } 1949 .le-selector 1950 { display: inline-block; 1951 overflow: hidden; 1952 max-width: 100%; 1953 padding: 0.23em 0.55em; 1954 border-radius: 0.3em; 1955 background-color: rgba(255, 255, 255, 0.03); 1956 color: var(--le-muted); 1957 font-family: var(--le-mono); 1958 font-size: 85%; 1959 white-space: nowrap; 1960 text-overflow: ellipsis; 1961 } 1962 .le-diff 1963 { display: grid; 1964 grid-template-columns: auto 1fr; 1965 gap: 0.3em 0.75em; 1966 padding: 0.6em 0.75em; 1967 border-radius: 0.45em; 1968 background-color: rgba(0, 0, 0, 0.22); 1969 font-family: var(--le-mono); 1970 font-size: 85%; 1971 } 1972 .le-diff .le-lbl 1973 { padding-top: 0.15em; 1974 color: var(--le-muted-2); 1975 font-size: 77%; 1976 letter-spacing: 0.04em; 1977 } 1978 .le-diff .le-before 1979 { overflow: hidden; 1980 color: #FF9B9B; 1981 text-decoration: line-through; 1982 text-decoration-color: rgba(255, 155, 155, 0.4); 1983 word-break: break-word; 1984 } 1985 .le-diff .le-after 1986 { overflow: hidden; 1987 color: #7BDFAE; 1988 word-break: break-word; 1989 } 1990 .le-change-actions 1991 { display: flex; 1992 gap: 0.45em; 1993 align-items: center; 1994 justify-content: flex-end; 1995 } 1996 .le-empty 1997 { padding: 3em 1.5em; 1998 border: 1px dashed var(--le-line); 1999 border-radius: 0.75em; 2000 color: var(--le-muted); 2001 text-align: center; 2002 } 2003 .le-empty h4 2004 { margin: 0 0 0.3em; 2005 color: var(--le-fg); 2006 font-size: 108%; 2007 } 2008 .le-empty p 2009 { margin: 0; 2010 font-size: 92%; 2011 line-height: 1.5; 2012 } 2013 2014 .le-side-foot 2015 { display: grid; 2016 flex: 0 0 auto; 2017 gap: 0.6em; 2018 padding: 0.9em; 2019 border-top: 1px solid var(--le-line); 2020 } 2021 .le-tip 2022 { display: flex; 2023 flex: 0 0 auto; 2024 gap: 0.75em; 2025 align-items: flex-start; 2026 margin: 0.75em 0.9em; 2027 padding: 0.75em 0.9em; 2028 border: 1px solid rgba(96, 165, 250, 0.18); 2029 border-radius: 0.6em; 2030 background-color: rgba(96, 165, 250, 0.06); 2031 color: var(--le-muted); 2032 font-size: 92%; 2033 } 2034 .le-tip b 2035 { color: var(--le-fg); 2036 } 2037 .le-tip button 2038 { margin-left: auto; 2039 padding: 0 0.3em; 2040 border: none; 2041 background: transparent; 2042 color: var(--le-muted); 2043 font: inherit; 2044 cursor: pointer; 2045 } 2046 2047 /* The way back after the editor has been put away. Top-centre and in the brand colour, be
2047cause 2048 while it is showing this is the ONLY control on screen - the sidebar and its Done button are 2049 hidden too - and in a corner it read as a dismissed notification. Nothing else of the editor 2050 is rendered meanwhile, so it cannot collide with the banners or the device pill. */ 2051 .le-reopen 2052 { top: 1.1em; 2053 left: 50%; 2054 transform: translateX(-50%); 2055 } 2056 .le-reopen-btn 2057 { display: flex; 2058 gap: 0.6em; 2059 align-items: center; 2060 max-width: 92vw; 2061 padding: 0.7em 1.2em 0.7em 0.9em; 2062 border: none; 2063 border-radius: 999px; 2064 background-color: var(--le-brand); 2065 color: #08231C; 2066 font: inherit; 2067 font-weight: 800; 2068 white-space: nowrap; 2069 cursor: pointer; 2070 box-shadow: var(--le-shadow), 0 0 0 0.35em rgba(18, 184, 134, 0.2); 2071 } 2072 .le-reopen-btn:hover 2073 { background-color: #16CE97; 2074 } 2075 .le-reopen-btn img 2076 { display: block; 2077 flex: 0 0 auto; 2078 width: 1.4em; 2079 height: 1.4em; 2080 } 2081 .le-reopen-btn svg 2082 { flex: 0 0 auto; 2083 width: 1.2em; 2084 height: 1.2em; 2085 } 2086 .le-reopen-n 2087 { padding: 0.15em 0.6em; 2088 border-radius: 999px; 2089 background-color: rgba(0, 0, 0, 0.22); 2090 font-size: 85%; 2091 font-weight: 700; 2092 } 2093 2094 /* ===== device picker ===== */ 2095 .le-devices 2096 { bottom: 1.4em; 2097 left: 50%; 2098 display: flex; 2099 gap: 0.15em; 2100 padding: 0.3em; 2101 border: 1px solid var(--le-line); 2102 border-radius: 999px; 2103 background-color: var(--le-chrome); 2104 transform: translateX(-50%); 2105 box-shadow: var(--le-shadow); 2106 } 2107 .le-device 2108 { display: inline-flex; 2109 gap: 0.45em; 2110 align-items: center; 2111 padding: 0.45em 0.9em; 2112 border: none; 2113 border-radius: 999px; 2114 background: transparent; 2115 color: var(--le-muted); 2116 font: inherit; 2117 font-size: 92%; 2118 font-weight: 600; 2119 cursor: pointer; 2120 } 2121 .le-device:hover 2122 { background-color: var(--le-chrome-2); 2123 color: var(--le-fg); 2124 } 2125 .le-device.le-on 2126 { background-color: var(--le-chrome-3); 2127 color: var(--le-fg); 2128 } 2129 .le-device .le-size 2130 { opacity: 0.7; 2131 font-family: var(--le-mono); 2132 font-size: 77%; 2133 } 2134 2135 /* ===== in-panel AI card ===== */ 2136 .le-aicard 2137 { margin-bottom: 1em; 2138 padding: 0.75em; 2139 border: 1px solid rgba(168, 85, 247, 0.28); 2140 border-radius: 0.7em; 2141 background: linear-gradient(160deg, rgba(168, 85, 247, 0.1), rgba(18, 184, 134, 0.06)); 2142 } 2143 .le-aicard-head 2144 { display: flex; 2145 gap: 0.5em; 2146 align-items: center; 2147 margin-bottom: 0.6em; 2148 } 2149 .le-aicard-ico 2150 { display: grid; 2151 flex: 0 0 auto; 2152 place-items: center; 2153 width: 1.7em; 2154 height: 1.7em; 2155 border-radius: 0.4em; 2156 background: linear-gradient(135deg, #A855F7, #12B886); 2157 color: #FFF; 2158 } 2159 .le-aicard-ico svg 2160 { width: 0.95em; 2161 height: 0.95em; 2162 } 2163 .le-aicard-head h5 2164 { margin: 0; 2165 font-size: 95%; 2166 font-weight: 700; 2167 } 2168 .le-aicard > .le-area 2169 { margin-bottom: 0.5em; 2170 } 2171 .le-chips 2172 { display: flex; 2173 flex-wrap: wrap; 2174 gap: 0.35em; 2175 margin-bottom: 0.6em; 2176 } 2177 .le-chip 2178 { padding: 0.25em 0.65em; 2179 border: 1px solid var(--le-line-2); 2180 border-radius: 999px; 2181 background-color: rgba(255, 255, 255, 0.05); 2182 color: var(--le-muted); 2183 font: inherit; 2184 font-size: 88%; 2185 cursor: pointer; 2186 } 2187 .le-chip:hover:not(:disabled) 2188 { border-color: rgba(168, 85, 247, 0.5); 2189 background-color: rgba(168, 85, 247, 0.15); 2190 color: var(--le-fg); 2191 } 2192 .le-chip:disabled 2193 { opacity: 0.4; 2194 cursor: default; 2195 } 2196 .le-btn.le-ai 2197 { width: 100%; 2198 justify-content: center; 2199 border-color: rgba(168, 85, 247, 0.45); 2200 background-color: rgba(168, 85, 247, 0.18); 2201 color: #E4CCFF; 2202 } 2203 .le-btn.le-ai:hover:not(:disabled) 2204 { background-color: rgba(168, 85, 247, 0.3); 2205 color: #FFF; 2206 } 2207 .le-aicard-result 2208 { margin-top: 0.7em; 2209 padding-top: 0.7em; 2210 border-top: 1px solid var(--le-line); 2211 } 2212 .le-aicard-label 2213 { margin-bottom: 0.35em; 2214 color: var(--le-muted-2); 2215 font-size: 78%; 2216 font-weight: 700; 2217 letter-spacing: 0.06em; 2218 text-transform: uppercase; 2219 } 2220 .le-aicard-text 2221 { max-height: 12em; 2222 overflow-y: auto; 2223 padding: 0.55em 0.7em; 2224 border: 1px solid var(--le-line); 2225 border-radius: 0.5em; 2226 background-color: rgba(0, 0, 0, 0.25); 2227 white-space: pre-wrap; 2228 word-break: break-word; 2229 } 2230 .le-aicard-text.le-mono 2231 { font-family: var(--le-mono); 2232 font-size: 90%; 2233 } 2234 .le-aicard-result .le-btn-row 2235 { /* The result already has the rule above it - the shared row would draw a second one. */ 2236 margin-top: 0.5em; 2237 padding-top: 0; 2238 border-top: none; 2239 } 2240 /* A style answer, as the rows it will be written as - so it can be read before it is applied. */ 2241 .le-decls 2242 { max-height: 14em; 2243 overflow-y: auto; 2244 border: 1px solid var(--le-line); 2245 border-radius: 0.5em; 2246 background-color: rgba(0, 0, 0, 0.25); 2247 } 2248 .le-decl 2249 { display: flex; 2250 gap: 0.6em; 2251 align-items: center; 2252 padding: 0.35em 0.7em; 2253 border-bottom: 1px solid var(--le-line); 2254 } 2255 .le-decl:last-child 2256 { border-bottom: none; 2257 } 2258 .le-decl code 2259 { flex: 0 0 auto; 2260 color: var(--le-muted); 2261 font-family: var(--le-mono); 2262 font-size: 88%; 2263 } 2264 .le-decl span 2265 { flex: 1 1 auto; 2266 overflow: hidden; 2267 font-family: var(--le-mono); 2268 font-size: 88%; 2269 text-align: right; 2270 text-overflow: ellipsis; 2271 white-space: nowrap; 2272 } 2273 .le-decl-swatch 2274 { flex: 0 0 auto; 2275 width: 1.1em; 2276 height: 1.1em; 2277 border-radius: 0.25em; 2278 box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35); 2279 } 2280 2281 /* ===== media library ===== */ 2282 /* A widget's list settings \u2014 the FAQ's questions, the wheel's segments. One editor for all of 2283 them, so a row is a small stack of labelled fields with its own number and a way out. */ 2284 /* A control that needs the width: the label goes above rather than
2284into the 7.2em column, so 2285 a list of rows has the whole panel to lay out in. */ 2286 /* THE ONE TILE THAT MATTERS when a widget is selected. Everything else on that row edits the 2287 markup a widget produced \u2014 its text, its colours \u2014 which is almost never what somebody 2288 wants after clicking a countdown. So it is drawn as the primary action, in the accent the 2289 editor already uses for Save, rather than as the fifth grey tile along. */ 2290 .le-tile.le-tile-primary 2291 { border-color: var(--le-accent); 2292 background: color-mix(in srgb, var(--le-accent) 18%, var(--le-chrome-2)); 2293 } 2294 .le-tile.le-tile-primary:hover 2295 { background: color-mix(in srgb, var(--le-accent) 30%, var(--le-chrome-2)); 2296 } 2297 .le-tile.le-tile-primary .le-tile-ico 2298 { color: var(--le-accent); 2299 } 2300 .le-field-wide 2301 { display: block; 2302 } 2303 .le-field-wide > label 2304 { display: block; 2305 margin-bottom: 0.35em; 2306 } 2307 /* AND THE CONTROL TAKES THE WIDTH IT WAS GIVEN. Dropping the label above freed the whole row, 2308 but the group is a wrapping flex row and a flex item is only as wide as its content - so a 2309 list of FAQ rows or numbered steps still drew in half the panel, each input a few words 2310 wide, with the other half empty beside it (Jacob's screenshots, 28 Sep 26). Stacked and 2311 stretched instead: a wide control is one thing per line, full width. */ 2312 .le-field-wide > .le-field-group 2313 { flex-direction: column; 2314 flex-wrap: nowrap; 2315 align-items: stretch; 2316 } 2317 /* A widget's checkbox setting: the box, then its words, on one line across the panel. */ 2318 .le-wcheck 2319 { padding: 0.15em 0 0.3em; 2320 } 2321 .le-wcheck .le-inline-check 2322 { margin-top: 0; 2323 color: var(--le-fg); 2324 font-size: 92%; 2325 } 2326 /* Inside a row the labels go above too \u2014 a row is already inside one indent, and a second 2327 label column would leave the input a few characters wide. */ 2328 .le-wr .le-field 2329 { display: block; 2330 } 2331 .le-wr .le-field > label 2332 { display: block; 2333 margin-bottom: 0.2em; 2334 opacity: 0.7; 2335 text-transform: none; 2336 } 2337 .le-wr .le-field-group > * 2338 { width: 100%; 2339 } 2340 .le-wr 2341 { display: flex; 2342 flex-direction: column; 2343 gap: 0.5em; 2344 } 2345 .le-wr-row 2346 { border: 1px solid var(--le-line); 2347 border-radius: 0.4em; 2348 padding: 0.5em 0.6em; 2349 background: var(--le-chrome-2); 2350 } 2351 .le-wr-head 2352 { display: flex; 2353 align-items: center; 2354 justify-content: space-between; 2355 margin-bottom: 0.3em; 2356 opacity: 0.8; 2357 } 2358 .le-wr-x 2359 { background: transparent; 2360 border: 0; 2361 color: inherit; 2362 cursor: pointer; 2363 font-size: 130%; 2364 line-height: 1; 2365 padding: 0 0.2em; 2366 } 2367 .le-wr-x:hover 2368 { color: #FCA5A5; 2369 } 2370 .le-wr-add 2371 { align-self: flex-start; 2372 background: var(--le-chrome-3); 2373 border: 1px solid var(--le-line); 2374 border-radius: 0.35em; 2375 color: inherit; 2376 cursor: pointer; 2377 font: inherit; 2378 padding: 0.35em 0.8em; 2379 } 2380 .le-wr-add:hover 2381 { background: var(--le-accent); 2382 color: #fff; 2383 } 2384 /* A checkbox big enough to hit, in the editor's own colour rather than the browser's blue. */ 2385 .le-check 2386 { width: 1.15em; 2387 height: 1.15em; 2388 accent-color: var(--le-accent); 2389 cursor: pointer; 2390 } 2391 .le-ta 2392 { min-height: 4.5em; 2393 resize: vertical; 2394 font: inherit; 2395 } 2396 /* The widget list: scrolls rather than paging, because the whole point of showing it as a 2397 list is that you can read what is on offer without opening anything. Capped at about six 2398 rows so the Where field above it and the note below stay on screen with it. */ 2399 .le-wl 2400 { max-height: 13em; 2401 overflow-y: auto; 2402 display: flex; 2403 flex-direction: column; 2404 gap: 0.15em; 2405 border: 1px solid var(--le-line); 2406 border-radius: 0.4em; 2407 padding: 0.25em; 2408 background: var(--le-chrome-2); 2409 } 2410 .le-wl-item 2411 { display: flex; 2412 align-items: center; 2413 gap: 0.5em; 2414 width: 100%; 2415 text-align: left; 2416 background: transparent; 2417 border: 0; 2418 border-radius: 0.3em; 2419 color: inherit; 2420 font: inherit; 2421 padding: 0.45em 0.6em; 2422 cursor: pointer; 2423 } 2424 .le-wl-item:hover 2425 { background: var(--le-chrome-3); 2426 } 2427 .le-wl-item.on 2428 { background: var(--le-accent); 2429 color: #fff; 2430 } 2431 .le-wl-name 2432 { flex: 1 1 auto; 2433 overflow: hidden; 2434 text-overflow: ellipsis; 2435 white-space: nowrap; 2436 } 2437 .le-wl-note 2438 { font-size: 85%; 2439 opacity: 0.75; 2440 line-height: 1.45; 2441 } 2442 /* The icon grid. Names under the glyphs on purpose - the icon font comes cross-origin from the 2443 panel host and may not arrive, and a grid of empty boxes is worse than the text box this 2444 replaced. With the name there it still picks correctly either way. */ 2445 .le-icogrid 2446 { display: grid; 2447 /* The field's value column is a FLEX ROW, so a grid dropped in it sizes to its content - 2448 which for auto-fill means one column, and 52 icons drew as 52 stacked rows. Width first, 2449 then the columns have something to fill. */ 2450 width: 100%; 2451 grid-template-columns: repeat(auto-fill, minmax(5.2em, 1fr)); 2452 gap: 0.35em; 2453 max-height: 16em; 2454 overflow-y: auto; 2455 margin-top: 0.5em; 2456 } 2457 .le-icogrid-i 2458 { display: flex; 2459 flex-direction: column; 2460 align-items: center; 2461 gap: 0.25em; 2462 padding: 0.45em 0.2em; 2463 border: 1px solid var(--le-line); 2464 border-radius: 0.35em; 2465 background: none; 2466 color: inherit; 2467 cursor: pointer; 2468 font: inherit; 2469 } 2470 .le-icogrid-i:hover 2471 { border-color: var(--le-line-2); 2472 } 2473 .le-icogrid-i.is-on 2474 { border-color: var(--le-brand); 2475 background: var(--le-brand-soft); 2476 } 2477 .le-icogrid-i > span 2478 { font-size: 78%; 2479 opacity: 0.8; 2480 overflow: hidden; 2481 text-overflow: ellipsis; 2482 white-space: nowrap; 2483 max-width: 100%; 2484 } 2485 .le-ico 2486 { font-size: 1.25em; 2487 font-style: normal; 2488 line-height: 1; 2489 min-height: 1.25em; 2490 } 2491 .le-linkbtn 2492 { background: none; 2493 border: 0; 2494 padding: 0; 2495 color: var(--le-brand-fg);
2496 cursor: pointer; 2497 font: inherit; 2498 text-decoration: underline; 2499 } 2500 .le-media-grid 2501 { display: grid; 2502 grid-template-columns: repeat(auto-fill, minmax(8em, 1fr)); 2503 gap: 0.6em; 2504 } 2505 .le-media 2506 { display: flex; 2507 flex-direction: column; 2508 gap: 0.4em; 2509 padding: 0.5em; 2510 border: 1px solid var(--le-line); 2511 border-radius: 0.55em; 2512 background-color: var(--le-chrome-2); 2513 color: inherit; 2514 font: inherit; 2515 cursor: pointer; 2516 } 2517 .le-media:hover 2518 { border-color: rgba(18, 184, 134, 0.5); 2519 background-color: var(--le-chrome-3); 2520 } 2521 .le-media-thumb 2522 { display: grid; 2523 place-items: center; 2524 height: 5em; 2525 overflow: hidden; 2526 border-radius: 0.35em; 2527 /* A checkerboard, so a transparent PNG does not read as a missing one. */ 2528 background-color: #FFF; 2529 background-image: 2530 linear-gradient(45deg, #D8DDE8 25%, transparent 25%, transparent 75%, #D8DDE8 75%), 2531 linear-gradient(45deg, #D8DDE8 25%, transparent 25%, transparent 75%, #D8DDE8 75%); 2532 background-position: 0 0, 0.5em 0.5em; 2533 background-size: 1em 1em; 2534 color: var(--le-muted-2); 2535 } 2536 .le-media-thumb img 2537 { max-width: 100%; 2538 max-height: 100%; 2539 object-fit: contain; 2540 } 2541 .le-media-name 2542 { overflow: hidden; 2543 color: var(--le-muted); 2544 font-size: 85%; 2545 white-space: nowrap; 2546 text-overflow: ellipsis; 2547 } 2548 2549 /* ===== page-level AI (sidebar) ===== */ 2550 .le-pageai 2551 { flex: 0 0 auto; 2552 margin: 0 0.9em 0.6em; 2553 border: 1px solid rgba(168, 85, 247, 0.28); 2554 border-radius: 0.6em; 2555 background: linear-gradient(160deg, rgba(168, 85, 247, 0.12), rgba(18, 184, 134, 0.05)); 2556 } 2557 .le-pageai-head 2558 { display: flex; 2559 width: 100%; 2560 gap: 0.6em; 2561 align-items: center; 2562 padding: 0.6em 0.7em; 2563 border: none; 2564 background: transparent; 2565 color: inherit; 2566 font: inherit; 2567 text-align: left; 2568 cursor: pointer; 2569 } 2570 .le-pageai-ico 2571 { display: grid; 2572 flex: 0 0 auto; 2573 place-items: center; 2574 width: 1.7em; 2575 height: 1.7em; 2576 border-radius: 0.4em; 2577 background: linear-gradient(135deg, #A855F7, #12B886); 2578 color: #FFF; 2579 } 2580 .le-pageai-ico svg 2581 { width: 0.95em; 2582 height: 0.95em; 2583 } 2584 .le-pageai-txt 2585 { flex: 1 1 auto; 2586 min-width: 0; 2587 } 2588 .le-pageai-txt b 2589 { display: block; 2590 font-size: 95%; 2591 font-weight: 700; 2592 } 2593 .le-pageai-txt small 2594 { display: block; 2595 overflow: hidden; 2596 color: var(--le-muted); 2597 font-size: 85%; 2598 white-space: nowrap; 2599 text-overflow: ellipsis; 2600 } 2601 .le-pageai-head > svg 2602 { flex: 0 0 auto; 2603 color: var(--le-muted-2); 2604 } 2605 .le-pageai-body 2606 { padding: 0 0.7em 0.7em; 2607 } 2608 .le-pageai-body .le-area 2609 { min-height: 3.6em; 2610 margin-bottom: 0.5em; 2611 } 2612 .le-pageai-body .le-btn-bar 2613 { justify-content: flex-end; 2614 } 2615 .le-pageai-body .le-btn.le-ai 2616 { /* Full width is right for the card inside a sub-panel, not for a two-button row. */ 2617 width: auto; 2618 } 2619 2620 /* ===== command palette ===== */ 2621 .le-cmdk-overlay 2622 { inset: 0; 2623 z-index: 2147483647; 2624 display: flex; 2625 align-items: flex-start; 2626 justify-content: center; 2627 padding-top: 12vh; 2628 background-color: rgba(4, 8, 20, 0.55); 2629 } 2630 .le-cmdk 2631 { display: flex; 2632 flex-direction: column; 2633 width: min(38em, 92vw); 2634 max-height: 66vh; 2635 overflow: hidden; 2636 border: 1px solid var(--le-line-2); 2637 border-radius: 0.9em; 2638 background-color: var(--le-chrome); 2639 box-shadow: var(--le-shadow); 2640 } 2641 .le-cmdk-input 2642 { display: flex; 2643 flex: 0 0 auto; 2644 gap: 0.6em; 2645 align-items: center; 2646 padding: 0.75em 0.95em; 2647 border-bottom: 1px solid var(--le-line); 2648 } 2649 .le-cmdk-input svg 2650 { width: 1.2em; 2651 height: 1.2em; 2652 color: var(--le-muted-2); 2653 } 2654 .le-cmdk-input input 2655 { flex: 1 1 auto; 2656 min-width: 0; 2657 padding: 0; 2658 border: none; 2659 outline: none; 2660 background: transparent; 2661 color: var(--le-fg); 2662 font: inherit; 2663 font-size: 115%; 2664 } 2665 .le-cmdk-input input::placeholder 2666 { color: var(--le-muted-2); 2667 } 2668 .le-cmdk-list 2669 { flex: 1 1 auto; 2670 min-height: 0; 2671 overflow-y: auto; 2672 padding: 0.4em; 2673 } 2674 .le-cmdk-group 2675 { padding: 0.7em 0.6em 0.3em; 2676 color: var(--le-muted-2); 2677 font-size: 78%; 2678 font-weight: 700; 2679 letter-spacing: 0.06em; 2680 text-transform: uppercase; 2681 } 2682 .le-cmdk-item 2683 { display: flex; 2684 width: 100%; 2685 gap: 0.7em; 2686 align-items: center; 2687 padding: 0.5em 0.6em; 2688 border: none; 2689 border-radius: 0.5em; 2690 background: transparent; 2691 color: inherit; 2692 font: inherit; 2693 text-align: left; 2694 cursor: pointer; 2695 } 2696 .le-cmdk-item.le-on 2697 { background-color: var(--le-chrome-3); 2698 } 2699 .le-cmdk-txt 2700 { flex: 1 1 auto; 2701 min-width: 0; 2702 } 2703 .le-cmdk-txt b 2704 { display: block; 2705 overflow: hidden; 2706 font-weight: 600; 2707 white-space: nowrap; 2708 text-overflow: ellipsis; 2709 } 2710 .le-cmdk-txt small 2711 { display: block; 2712 overflow: hidden; 2713 color: var(--le-muted); 2714 font-size: 85%; 2715 white-space: nowrap; 2716 text-overflow: ellipsis; 2717 } 2718 .le-cmdk-key 2719 { flex: 0 0 auto; 2720 padding: 0.1em 0.45em; 2721 border: 1px solid var(--le-line); 2722 border-radius: 0.3em; 2723 background-color: rgba(255, 255, 255, 0.06); 2724 color: var(--le-muted); 2725 font-family: var(--le-mono); 2726 font-size: 82%; 2727 } 2728 .le-cmdk-empty 2729 { padding: 1.6em 0.8em; 2730 color: var(--le-muted-2); 2731 text-align: center; 2732 } 2733 .le-cmdk-foot 2734 { display: flex; 2735 flex: 0 0 auto; 2736 gap: 1.2em; 2737 padding: 0.55em 0.95em; 2738 border-top: 1px solid var(--le-line); 2739 background-color: var(--le-chrome-2); 2740 color: var(--le-muted-2); 2741 font-size: 82%; 2742 } 2743 .le-cmdk-foot kbd 2744 { margin-right: 0.2em; 2745 } 2746 2747 /* ===== "Insert variable" picker ===== */ 2748 .le-vp-overlay 2749 { inset: 0; 2750 z-index: 2147483647; 2751 display: flex; 2752 align-items: flex-start; 2753 justify-content: center; 2754 padding-top: 6vh; 2755 background-color: rgba(4, 8, 20, 0.55); 2756 } 2757 .le-vp 2758 { display: flex; 2759 flex-direction: column;
2760 width: min(46em, 92vw); 2761 max-height: 82vh; 2762 overflow: hidden; 2763 border: 1px solid var(--le-line-2); 2764 border-radius: 0.9em; 2765 background-color: var(--le-chrome); 2766 box-shadow: var(--le-shadow); 2767 } 2768 .le-vp-head 2769 { display: flex; 2770 flex: 0 0 auto; 2771 gap: 0.6em; 2772 align-items: flex-start; 2773 padding: 0.85em 1.1em; 2774 border-bottom: 1px solid var(--le-line); 2775 background-color: var(--le-chrome-2); 2776 } 2777 .le-vp-head h3 2778 { margin: 0; 2779 font-size: 108%; 2780 font-weight: 700; 2781 } 2782 .le-vp-head p 2783 { margin: 0.15em 0 0; 2784 color: var(--le-muted); 2785 font-size: 88%; 2786 } 2787 .le-vp-head .le-icon 2788 { margin-left: auto; 2789 } 2790 .le-vp-search 2791 { flex: 0 0 auto; 2792 padding: 0.6em 1.1em; 2793 border-bottom: 1px solid var(--le-line); 2794 } 2795 .le-vp-body 2796 { /* Takes the leftover height, and is the ONLY part that gives it back - every sibling is 2797 flex 0 0 auto, so the func editor can't be squashed and the Insert button can't be 2798 pushed out of the modal on a short viewport. */ 2799 flex: 1 1 auto; 2800 min-height: 0; 2801 overflow-y: auto; 2802 padding: 1em 1.1em; 2803 } 2804 .le-vp-sec 2805 { margin-bottom: 1.4em; 2806 } 2807 .le-vp-sec h4 2808 { margin: 0 0 0.5em; 2809 color: var(--le-muted); 2810 font-size: 81%; 2811 font-weight: 700; 2812 letter-spacing: 0.06em; 2813 text-transform: uppercase; 2814 } 2815 .le-vp-grid 2816 { display: grid; 2817 grid-template-columns: repeat(auto-fill, minmax(13em, 1fr)); 2818 gap: 0.3em; 2819 } 2820 .le-vp-var 2821 { /* overflow-wrap anywhere, NOT break-word - it's the one that also shrinks the min-content 2822 width, so a long token can't push its 1fr column past its share and scroll the body 2823 sideways. */ 2824 display: block; 2825 min-width: 0; 2826 padding: 0.4em 0.55em; 2827 overflow-wrap: anywhere; 2828 border: 1px solid var(--le-line); 2829 border-radius: 0.4em; 2830 background-color: var(--le-chrome-2); 2831 color: var(--le-fg); 2832 text-align: left; 2833 font: inherit; 2834 font-size: 88%; 2835 cursor: pointer; 2836 } 2837 .le-vp-var:hover 2838 { border-color: var(--le-line-2); 2839 background-color: var(--le-chrome-3); 2840 } 2841 .le-vp-var.le-on 2842 { border-color: var(--le-brand); 2843 background-color: var(--le-brand-soft); 2844 color: var(--le-brand-fg); 2845 } 2846 .le-vp-tok 2847 { margin-right: 0.4em; 2848 color: #F0B35C; 2849 font-family: var(--le-mono); 2850 font-size: 88%; 2851 font-weight: 700; 2852 } 2853 .le-vp-empty 2854 { margin-bottom: 1em; 2855 color: var(--le-muted-2); 2856 font-size: 88%; 2857 } 2858 .le-vp-empty code 2859 { font-family: var(--le-mono); 2860 } 2861 .le-vp-picked 2862 { display: flex; 2863 flex: 0 0 auto; 2864 flex-direction: column; 2865 gap: 0.5em; 2866 max-height: 40vh; 2867 padding: 0.7em 1.1em; 2868 overflow-y: auto; 2869 border-top: 1px solid var(--le-line); 2870 background-color: var(--le-chrome-2); 2871 } 2872 .le-vp-expr 2873 { color: #F0B35C; 2874 font-family: var(--le-mono); 2875 font-size: 88%; 2876 word-break: break-all; 2877 } 2878 .le-vp-funcs 2879 { display: flex; 2880 flex-direction: column; 2881 gap: 0.3em; 2882 } 2883 .le-vp-funcs-hd 2884 { color: var(--le-muted); 2885 font-size: 78%; 2886 font-weight: 700; 2887 letter-spacing: 0.05em; 2888 text-transform: uppercase; 2889 } 2890 .le-vp-func 2891 { display: flex; 2892 flex-direction: column; 2893 gap: 0.3em; 2894 padding: 0.4em 0.5em; 2895 border: 1px solid var(--le-line); 2896 border-radius: 0.4em; 2897 background-color: var(--le-chrome-3); 2898 } 2899 .le-vp-func-hd 2900 { display: flex; 2901 gap: 0.4em; 2902 align-items: center; 2903 } 2904 .le-vp-func-hd span 2905 { flex: 10000 1 0; 2906 font-size: 88%; 2907 font-weight: 600; 2908 } 2909 .le-vp-func-hd button 2910 { display: grid; 2911 place-items: center; 2912 padding: 0.1em; 2913 border: none; 2914 background: transparent; 2915 color: var(--le-muted); 2916 cursor: pointer; 2917 } 2918 .le-vp-func-hd button:hover 2919 { color: var(--le-danger-fg); 2920 } 2921 .le-vp-func-arg 2922 { display: flex; 2923 gap: 0.4em; 2924 align-items: center; 2925 } 2926 .le-vp-func-arg > span 2927 { min-width: 9em; 2928 color: var(--le-muted); 2929 font-size: 83%; 2930 } 2931 .le-vp-foot 2932 { display: flex; 2933 flex: 0 0 auto; 2934 gap: 0.6em; 2935 align-items: center; 2936 padding: 0.6em 1.1em; 2937 border-top: 1px solid var(--le-line); 2938 background-color: var(--le-chrome-2); 2939 } 2940 .le-vp-hint 2941 { margin-right: auto; 2942 color: var(--le-muted-2); 2943 font-size: 83%; 2944 } 2945 2946 /* The AI assistant's chat panel (AiPanel). It lives in the toolbar body like the editing 2947 sub-panels, so it inherits their width; only the transcript and the input row are its own. 2948 */ 2949 .le-ai-log 2950 { display: flex; 2951 flex-direction: column; 2952 gap: 0.6em; 2953 overflow-y: auto; 2954 max-height: 24em; 2955 min-height: 8em; 2956 margin-bottom: 0.9em; 2957 padding: 0.7em; 2958 border: 1px solid var(--le-line); 2959 border-radius: 0.6em; 2960 background-color: var(--le-chrome-2); 2961 }
2962 .le-ai-hint 2963 { color: var(--le-muted); 2964 font-size: 92%; 2965 } 2966 .le-ai-msg 2967 { max-width: 92%; 2968 padding: 0.5em 0.75em; 2969 border-radius: 0.6em; 2970 white-space: pre-wrap; 2971 overflow-wrap: break-word; 2972 } 2973 .le-ai-user 2974 { align-self: flex-end; 2975 border: 1px solid var(--le-line-2); 2976 background-color: var(--le-chrome-3); 2977 } 2978 .le-ai-assistant 2979 { align-self: flex-start; 2980 border: 1px solid var(--le-brand-soft); 2981 background-color: var(--le-brand-soft); 2982 color: var(--le-fg); 2983 } 2984 .le-ai-status 2985 { display: flex; 2986 gap: 0.45em; 2987 align-items: baseline; 2988 color: var(--le-muted-2); 2989 font-size: 85%; 2990 } 2991 .le-ai-status svg 2992 { align-self: center; 2993 } 2994 .le-ai-error 2995 { color: var(--le-danger-fg); 2996 } 2997 .le-ai-busy 2998 { display: flex; 2999 gap: 0.6em; 3000 align-items: center; 3001 color: var(--le-muted); 3002 font-size: 92%; 3003 } 3004 .le-ai-pulse 3005 { flex: 0 0 auto; 3006 width: 0.6em; 3007 height: 0.6em; 3008 border-radius: 50%; 3009 background-color: var(--le-info); 3010 animation: le-ai-pulse 1.1s ease-in-out infinite; 3011 } 3012 @keyframes le-ai-pulse 3013 { 0%, 100% 3014 { opacity: 0.35; 3015 transform: scale(0.8); 3016 } 3017 50% 3018 { opacity: 1; 3019 transform: scale(1.15); 3020 } 3021 } 3022 .le-ai-inputrow 3023 { display: flex; 3024 gap: 0.6em; 3025 align-items: flex-end; 3026 } 3027 .le-ai-budget 3028 { margin-top: 0.5em; 3029 font-size: 0.82em; 3030 color: var(--le-muted); 3031 } 3032 .le-ai-budget-out 3033 { color: var(--le-danger-fg); 3034 } 3035 .le-ai-input 3036 { flex: 1 1 auto; 3037 padding: 0.55em 0.7em; 3038 border: 1px solid var(--le-line-2); 3039 border-radius: 0.55em; 3040 background-color: var(--le-chrome-2); 3041 color: var(--le-fg); 3042 font: inherit; 3043 resize: vertical; 3044 } 3045 .le-ai-input:focus 3046 { outline: none; 3047 border-color: var(--le-brand); 3048 } 3049 .le-ai-input:disabled 3050 { opacity: 0.6; 3051 } 3052 3053 /* Row of actions under an editor box (Insert variable / Variations). Wraps rather than 3054 overflowing: the sub-panel is only as wide as the toolbar. */ 3055 .le-btn-bar 3056 { display: flex; 3057 flex-wrap: wrap; 3058 gap: 0.5em; 3059 align-items: center; 3060 margin-top: 0.6em; 3061 } 3062 /* Count of active cases on the Variations button. */ 3063 .le-count 3064 { display: inline-grid; 3065 place-items: center; 3066 min-width: 1.4em; 3067 height: 1.4em; 3068 padding: 0 0.35em; 3069 border-radius: 999px; 3070 background-color: var(--le-brand); 3071 color: #08231C; 3072 font-size: 78%; 3073 font-weight: 800; 3074 } 3075 3076 /* ===== variations dialog ===== */ 3077 .le-vd-overlay 3078 { inset: 0; 3079 z-index: 2147483647; 3080 display: flex; 3081 align-items: flex-start; 3082 justify-content: center; 3083 padding-top: 5vh; 3084 background-color: rgba(4, 8, 20, 0.55); 3085 } 3086 .le-vd 3087 { display: flex; 3088 flex-direction: column; 3089 width: min(52em, 94vw); 3090 max-height: 86vh; 3091 overflow: hidden; 3092 border: 1px solid var(--le-line-2); 3093 border-radius: 0.9em; 3094 background-color: var(--le-chrome); 3095 box-shadow: var(--le-shadow); 3096 } 3097 .le-vd-head 3098 { display: flex; 3099 flex: 0 0 auto; 3100 gap: 0.6em; 3101 align-items: flex-start; 3102 padding: 0.85em 1.1em; 3103 border-bottom: 1px solid var(--le-line); 3104 background-color: var(--le-chrome-2); 3105 } 3106 .le-vd-head h3 3107 { display: flex; 3108 gap: 0.4em; 3109 align-items: center; 3110 margin: 0; 3111 font-size: 108%; 3112 font-weight: 700; 3113 } 3114 .le-vd-head p 3115 { margin: 0.15em 0 0; 3116 color: var(--le-muted); 3117 font-size: 88%; 3118 } 3119 .le-vd-tag 3120 { color: var(--le-muted-2); 3121 font-size: 80%; 3122 font-weight: 400; 3123 } 3124 .le-vd-head .le-icon 3125 { margin-left: auto; 3126 } 3127 .le-vd-body 3128 { flex: 1 1 auto; 3129 min-height: 0; 3130 padding: 0.9em 1.1em; 3131 overflow-y: auto; 3132 } 3133 .le-vd-case, 3134 .le-vd-default 3135 { margin-bottom: 0.9em; 3136 padding: 0.7em 0.8em; 3137 border: 1px solid var(--le-line); 3138 border-radius: 0.6em; 3139 background-color: var(--le-chrome-2); 3140 } 3141 .le-vd-default 3142 { margin-bottom: 0; 3143 border-style: dashed; 3144 } 3145 .le-vd-case-head 3146 { display: flex; 3147 gap: 0.4em; 3148 align-items: center; 3149 margin-bottom: 0.5em; 3150 } 3151 .le-vd-label 3152 { display: block; 3153 margin: 0.6em 0 0.3em; 3154 color: var(--le-muted); 3155 font-size: 82%; 3156 font-weight: 600; 3157 letter-spacing: 0.04em; 3158 text-transform: uppercase; 3159 } 3160 .le-vd-case > .le-vd-label:first-of-type 3161 { margin-top: 0; 3162 } 3163 .le-vd-content 3164 { display: flex; 3165 gap: 0.5em; 3166 align-items: flex-start; 3167 } 3168 .le-vd-content > .le-area, 3169 .le-vd-content > .le-input 3170 { flex: 10000 1 0; 3171 } 3172 .le-vd-foot 3173 { display: flex; 3174 flex: 0 0 auto; 3175 gap: 0.6em; 3176 align-items: center; 3177 padding: 0.7em 1.1em; 3178 border-top: 1px solid var(--le-line); 3179 background-color: var(--le-chrome-2); 3180 } 3181 3182 /* ===== rule builder ===== */ 3183 .le-rb 3184 { display: flex; 3185 flex-direction: column; 3186 gap: 0.35em; 3187 } 3188 .le-rb-row 3189 { display: flex; 3190 flex-wrap: wrap; 3191 gap: 0.35em; 3192 align-items: center; 3193 } 3194 .le-rb-comb 3195 { flex: 0 0 auto; 3196 width: 4.5em; 3197 } 3198 .le-rb-if 3199 { color: var(--le-muted-2); 3200 font-size: 88%; 3201 font-style: italic; 3202 text-align: center; 3203 } 3204 /* The three controls share the row's width rather than each sizing to its own longest option: 3205 a visitor-data column name can be long, and without this the value box gets squeezed to 3206 nothing on a narrow editor window. */ 3207 .le-rb-field 3208 { flex: 3 1 8em; 3209 min-width: 0; 3210 } 3211 .le-rb-op 3212 { flex: 2 1 7em; 3213 min-width: 0; 3214 } 3215 .le-rb-val 3216 { flex: 2 1 6em; 3217 min-width: 0; 3218 width: auto; 3219 } 3220 .le-rb-foot 3221 { display: flex; 3222 gap: 0.5em; 3223 align-items: center; 3224 } 3225 .le-rb-hint 3226 { flex: 10000 1 0; 3227 color: var(--le-muted-2); 3228 font-size: 80%; 3229 } 3230 /* Rules/SQL switch. A segmented pair rather than one toggle button, because it only ever 3231 appears alongside a condition that is already in one of the two modes - the point is to 3232 show WHICH, not just to offer the other. */ 3233 .le-rb-modes 3234 { display: inline-flex; 3235 flex: 0 0 auto; 3236 gap: 0.15em; 3237 padding: 0.15em; 3238 border: 1px solid var(--le-line); 3239 border-radius: 0.45em; 3240 background-color: var(--le-chrome-2); 3241 } 3242 .le-rb-modes button 3243 { padding: 0.3em 0.6em; 3244 border: none; 3245 border-radius: 0.3em; 3246 background: transparent; 3247 color: var(--le-muted); 3248 font: inherit; 3249 font-size: 85%; 3250 font-weight: 600; 3251 white-space: nowrap; 3252 cursor: pointer; 3253 } 3254 .le-rb-modes button:hover:not(:disabled) 3255 { background-color: var(--le-chrome-3);
3256 color: var(--le-fg); 3257 } 3258 .le-rb-modes button.le-on 3259 { background-color: var(--le-chrome-3); 3260 color: var(--le-fg); 3261 } 3262 .le-rb-modes button:disabled 3263 { opacity: 0.45; 3264 cursor: default; 3265 } 3266 3267 /* The editor's own window is around 1200-1500px, so these are for a genuinely small screen - 3268 not for the ordinary case. A breakpoint at the mockup's 900px would make the sidebar cover 3269 the whole editing window. 3270 */ 3271 @media (max-width: 760px) 3272 { .le-toolbar 3273 { width: 95vw; 3274 } 3275 .le-grid 3276 { grid-template-columns: repeat(2, 1fr); 3277 } 3278 } 3279 @media (max-width: 620px) 3280 { .le-side 3281 { width: 100vw; 3282 max-width: 100vw; 3283 } 3284 .le-peek 3285 { display: none; 3286 } 3287 } 3288 @media (max-width: 1000px) 3289 { /* Too narrow to dodge the sidebar - shifting would only push the banner off the other edge. */ 3290 .le-navbanner.le-avoid-right, 3291 .le-navbanner.le-avoid-left 3292 { transform: translateX(-50%); 3293 } 3294 } 3295`; 3296
vendor: 715 bytes, lines 3297-3325
3297// ../../../../deno-dir/npm/registry.npmjs.org/preact/10.22.0/jsx-runtime/dist/jsxRuntime.module.js 3298var f2 = 0; 3299var i2 = Array.isArray; 3300function u2(e3, t4, n2, o3, i4, u4) { 3301 t4 || (t4 = {}); 3302 var a3, c3, p3 = t4; 3303 if ("ref" in p3) for (c3 in p3 = {}, t4) "ref" == c3 ? a3 = t4[c3] : p3[c3] = t4[c3]; 3304 var l3 = { 3305 type: e3, 3306 props: p3, 3307 key: n2, 3308 ref: a3, 3309 __k: null, 3310 __: null, 3311 __b: 0, 3312 __e: null, 3313 __d: void 0, 3314 __c: null, 3315 constructor: void 0, 3316 __v: --f2, 3317 __i: -1, 3318 __u: 0, 3319 __source: i4, 3320 __self: u4 3321 }; 3322 if ("function" == typeof e3 && (a3 = e3.defaultProps)) for (c3 in a3) void 0 === p3[c3] && (p3[c3] = a3[c3]); 3323 return l.vnode && l.vnode(l3), l3; 3324} 3325
3326// ../../../../deno-dir/npm/registry.npmjs.org/preact/10.22.0/hooks/dist/hooks.module.js 3327var t2; 3328var r2; 3329var u3; 3330var i3; 3331var o2 = 0; 3332var f3 = []; 3333var c2 = []; 3334var e2 = l; 3335var a2 = e2.__b; 3336var v2 = e2.__r; 3337var l2 = e2.diffed; 3338var m = e2.__c; 3339var s2 = e2.unmount; 3340var d2 = e2.__; 3341function h2(n2, t4) { 3342 e2.__h && e2.__h(r2, n2, o2 || t4), o2 = 0; 3343 var u4 = r2.__H || (r2.__H = { 3344 __: [], 3345 __h: [] 3346 }); 3347 return n2 >= u4.__.length && u4.__.push({ 3348 __V: c2 3349 }), u4.__[n2]; 3350} 3351function p2(n2) { 3352 return o2 = 1, y2(D, n2); 3353} 3354function y2(n2, u4, i4) { 3355 var o3 = h2(t2++, 2); 3356 if (o3.t = n2, !o3.__c && (o3.__ = [ 3357 i4 ? i4(u4) : D(void 0, u4), 3358 function(n3) { 3359 var t4 = o3.__N ? o3.__N[0] : o3.__[0], r3 = o3.t(t4, n3); 3360 t4 !== r3 && (o3.__N = [ 3361 r3, 3362 o3.__[1] 3363 ], o3.__c.setState({})); 3364 } 3365 ], o3.__c = r2, !r2.u)) { 3366 var f4 = function(n3, t4, r3) { 3367 if (!o3.__c.__H) return true; 3368 var u5 = o3.__c.__H.__.filter(function(n4) { 3369 return !!n4.__c; 3370 }); 3371 if (u5.every(function(n4) { 3372 return !n4.__N; 3373 })) return !c3 || c3.call(this, n3, t4, r3); 3374 var i5 = false; 3375 return u5.forEach(function(n4) { 3376 if (n4.__N) { 3377 var t5 = n4.__[0]; 3378 n4.__ = n4.__N, n4.__N = void 0, t5 !== n4.__[0] && (i5 = true); 3379 } 3380 }), !(!i5 && o3.__c.props === n3) && (!c3 || c3.call(this, n3, t4, r3)); 3381 }; 3382 r2.u = true; 3383 var c3 = r2.shouldComponentUpdate, e3 = r2.componentWillUpdate; 3384 r2.componentWillUpdate = function(n3, t4, r3) { 3385 if (this.__e) { 3386 var u5 = c3; 3387 c3 = void 0, f4(n3, t4, r3), c3 = u5; 3388 } 3389 e3 && e3.call(this, n3, t4, r3); 3390 }, r2.shouldComponentUpdate = f4; 3391 } 3392 return o3.__N || o3.__; 3393} 3394function _2(n2, u4) { 3395 var i4 = h2(t2++, 3); 3396 !e2.__s && C2(i4.__H, u4) && (i4.__ = n2, i4.i = u4, r2.__H.__h.push(i4)); 3397} 3398function F2(n2) { 3399 return o2 = 5, q2(function() { 3400 return { 3401 current: n2 3402 }; 3403 }, []); 3404} 3405function q2(n2, r3) { 3406 var u4 = h2(t2++, 7); 3407 return C2(u4.__H, r3) ? (u4.__V = n2(), u4.i = r3, u4.__h = n2, u4.__V) : u4.__; 3408} 3409function j2() { 3410 for (var n2; n2 = f3.shift(); ) if (n2.__P && n2.__H) try { 3411 n2.__H.__h.forEach(z2), n2.__H.__h.forEach(B2), n2.__H.__h = []; 3412 } catch (t4) { 3413 n2.__H.__h = [], e2.__e(t4, n2.__v); 3414 } 3415} 3416e2.__b = function(n2) { 3417 r2 = null, a2 && a2(n2); 3418}, e2.__ = function(n2, t4) { 3419 n2 && t4.__k && t4.__k.__m && (n2.__m = t4.__k.__m), d2 && d2(n2, t4); 3420}, e2.__r = function(n2) { 3421 v2 && v2(n2), t2 = 0; 3422 var i4 = (r2 = n2.__c).__H; 3423 i4 && (u3 === r2 ? (i4.__h = [], r2.__h = [], i4.__.forEach(function(n3) { 3424 n3.__N && (n3.__ = n3.__N), n3.__V = c2, n3.__N = n3.i = void 0; 3425 })) : (i4.__h.forEach(z2), i4.__h.forEach(B2), i4.__h = [], t2 = 0)), u3 = r2; 3426}, e2.diffed = function(n2) { 3427 l2 && l2(n2); 3428 var t4 = n2.__c; 3429 t4 && t4.__H && (t4.__H.__h.length && (1 !== f3.push(t4) && i3 === e2.requestAnimationFrame || ((i3 = e2.requestAnimationFrame) || w2)(j2)), t4.__H.__.forEach(function(n3) { 3430 n3.i && (n3.__H = n3.i), n3.__V !== c2 && (n3.__ = n3.__V), n3.i = void 0, n3.__V = c2; 3431 })), u3 = r2 = null; 3432}, e2.__c = function(n2, t4) { 3433 t4.some(function(n3) { 3434 try { 3435 n3.__h.forEach(z2), n3.__h = n3.__h.filter(function(n4) { 3436 return !n4.__ || B2(n4); 3437 }); 3438 } catch (r3) { 3439 t4.some(function(n4) { 3440 n4.__h && (n4.__h = []); 3441 }), t4 = [], e2.__e(r3, n3.__v); 3442 } 3443 }), m && m(n2, t4); 3444}, e2.unmount = function(n2) { 3445 s2 && s2(n2); 3446 var t4, r3 = n2.__c; 3447 r3 && r3.__H && (r3.__H.__.forEach(function(n3) { 3448 try { 3449 z2(n3); 3450 } catch (n4) { 3451 t4 = n4; 3452 } 3453 }), r3.__H = void 0, t4 && e2.__e(t4, r3.__v)); 3454}; 3455var k2 = "function" == typeof requestAnimationFrame; 3456function w2(n2) { 3457 var t4, r3 = function() { 3458 clearTimeout(u4), k2 && cancelAnimationFrame(t4), setTimeout(n2); 3459 }, u4 = setTimeout(r3, 100); 3460 k2 && (t4 = requestAnimationFrame(r3)); 3461} 3462function z2(n2) { 3463 var t4 = r2, u4 = n2.__c; 3464 "function" == typeof u4 && (n2.__c = void 0, u4()), r2 = t4; 3465} 3466function B2(n2) { 3467 var t4 = r2; 3468 n2.__c = n2.__(), r2 = t4; 3469} 3470function C2(n2, t4) { 3471 return !n2 || n2.length !== t4.length || t4.some(function(t5, r3) { 3472 return t5 !== n2[r3]; 3473 }); 3474} 3475function D(n2, t4) { 3476 return "function" == typeof t4 ? t4(n2) : t4; 3477} 3478
3479// src/components/live/widgetCompile.ts 3480var TIMEOUT_MS = 8e3; 3481var pending = /* @__PURE__ */ new Map(); 3482var seq = 0; 3483function resolveWidgetHtml(reqId, html) { 3484 const key = String(reqId ?? ""); 3485 const found = pending.get(key); 3486 if (found) { 3487 pending.delete(key); 3488 clearTimeout(found.timer); 3489 found.resolve(typeof html == "string" && html ? html : null); 3490 } 3491} 3492function requestWidgetHtml(send, type, props) { 3493 const reqId = "w" + ++seq; 3494 return new Promise((resolve) => { 3495 const timer = setTimeout(() => { 3496 pending.delete(reqId); 3497 resolve(null); 3498 }, TIMEOUT_MS); 3499 pending.set(reqId, { 3500 resolve, 3501 timer 3502 }); 3503 try { 3504 send({ 3505 widget_compile: { 3506 req_id: reqId, 3507 type, 3508 props 3509 } 3510 }); 3511 } catch { 3512 pending.delete(reqId); 3513 clearTimeout(timer); 3514 resolve(null); 3515 } 3516 }); 3517} 3518
3519// src/components/live/pageFreeze.ts 3520var FROZEN_EVENTS = [ 3521 "click", 3522 "auxclick", 3523 "submit", 3524 "dragstart" 3525]; 3526function isOwnUi(ev, host) { 3527 const path = typeof ev.composedPath == "function" ? ev.composedPath() : []; 3528 if (path.indexOf(host) != -1) { 3529 return true; 3530 } 3531 return !!ev.target && host.contains(ev.target); 3532} 3533function installPageFreeze(doc, host) { 3534 function freeze(ev) { 3535 if (isOwnUi(ev, host)) { 3536 return; 3537 } 3538 ev.preventDefault(); 3539 ev.stopPropagation(); 3540 } 3541 for (const type of FROZEN_EVENTS) { 3542 doc.addEventListener(type, freeze, true); 3543 } 3544 return () => { 3545 for (const type of FROZEN_EVENTS) { 3546 doc.removeEventListener(type, freeze, true); 3547 } 3548 }; 3549} 3550
3551// src/components/live/liveIcons.tsx 3552var li = { 3553 pencil: /* @__PURE__ */ u2("svg", { 3554 viewBox: "0 0 24 24", 3555 children: [ 3556 /* @__PURE__ */ u2("path", { 3557 d: "M12 20h9" 3558 }), 3559 /* @__PURE__ */ u2("path", { 3560 d: "M16.5 3.5l4 4L7 21H3v-4z" 3561 }) 3562 ] 3563 }), 3564 lines: /* @__PURE__ */ u2("svg", { 3565 viewBox: "0 0 24 24", 3566 children: /* @__PURE__ */ u2("path", { 3567 d: "M4 7h16M4 12h10M4 17h16" 3568 }) 3569 }), 3570 palette: /* @__PURE__ */ u2("svg", { 3571 viewBox: "0 0 24 24", 3572 children: [ 3573 /* @__PURE__ */ u2("circle", { 3574 cx: "13.5", 3575 cy: "6.5", 3576 r: ".5", 3577 fill: "currentColor" 3578 }), 3579 /* @__PURE__ */ u2("circle", { 3580 cx: "17.5", 3581 cy: "10.5", 3582 r: ".5", 3583 fill: "currentColor" 3584 }), 3585 /* @__PURE__ */ u2("circle", { 3586 cx: "8.5", 3587 cy: "7.5", 3588 r: ".5", 3589 fill: "currentColor" 3590 }), 3591 /* @__PURE__ */ u2("circle", { 3592 cx: "6.5", 3593 cy: "12.5", 3594 r: ".5", 3595 fill: "currentColor" 3596 }),
3597 /* @__PURE__ */ u2("path", { 3598 d: "M12 2a10 10 0 1 0 5 18c1 0 2-1 2-2v-1c0-1-1-2-2-2h-1a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3h1c1 0 2-1 2-2A10 10 0 0 0 12 2z" 3599 }) 3600 ] 3601 }), 3602 copy: /* @__PURE__ */ u2("svg", { 3603 viewBox: "0 0 24 24", 3604 children: [ 3605 /* @__PURE__ */ u2("rect", { 3606 x: "8", 3607 y: "8", 3608 width: "12", 3609 height: "12", 3610 rx: "2" 3611 }), 3612 /* @__PURE__ */ u2("path", { 3613 d: "M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" 3614 }) 3615 ] 3616 }), 3617 trash: /* @__PURE__ */ u2("svg", { 3618 viewBox: "0 0 24 24", 3619 children: [ 3620 /* @__PURE__ */ u2("polyline", { 3621 points: "3 6 5 6 21 6" 3622 }), 3623 /* @__PURE__ */ u2("path", { 3624 d: "M19 6l-1 14H6L5 6" 3625 }), 3626 /* @__PURE__ */ u2("path", { 3627 d: "M10 11v6M14 11v6" 3628 }) 3629 ] 3630 }), 3631 code: /* @__PURE__ */ u2("svg", { 3632 viewBox: "0 0 24 24", 3633 children: [ 3634 /* @__PURE__ */ u2("polyline", { 3635 points: "16 18 22 12 16 6" 3636 }), 3637 /* @__PURE__ */ u2("polyline", { 3638 points: "8 6 2 12 8 18" 3639 }) 3640 ] 3641 }), 3642 move: /* @__PURE__ */ u2("svg", { 3643 viewBox: "0 0 24 24", 3644 children: [
3645 /* @__PURE__ */ u2("path", { 3646 d: "M5 9l-3 3 3 3" 3647 }), 3648 /* @__PURE__ */ u2("path", { 3649 d: "M19 9l3 3-3 3" 3650 }), 3651 /* @__PURE__ */ u2("path", { 3652 d: "M9 5l3-3 3 3" 3653 }), 3654 /* @__PURE__ */ u2("path", { 3655 d: "M9 19l3 3 3-3" 3656 }), 3657 /* @__PURE__ */ u2("path", { 3658 d: "M2 12h20M12 2v20" 3659 }) 3660 ] 3661 }), 3662 spacing: /* @__PURE__ */ u2("svg", { 3663 viewBox: "0 0 24 24", 3664 children: [ 3665 /* @__PURE__ */ u2("rect", { 3666 x: "4", 3667 y: "4", 3668 width: "16", 3669 height: "16" 3670 }), 3671 /* @__PURE__ */ u2("rect", { 3672 x: "8", 3673 y: "8", 3674 width: "8", 3675 height: "8", 3676 fill: "currentColor", 3677 opacity: ".2" 3678 }) 3679 ] 3680 }), 3681 align: /* @__PURE__ */ u2("svg", { 3682 viewBox: "0 0 24 24", 3683 children: /* @__PURE__ */ u2("path", { 3684 d: "M4 6h16M4 12h10M4 18h16" 3685 }) 3686 }), 3687 position: /* @__PURE__ */ u2("svg", { 3688 viewBox: "0 0 24 24", 3689 children: [ 3690 /* @__PURE__ */ u2("circle", { 3691 cx: "12", 3692 cy: "12", 3693 r: "4" 3694 }),
3695 /* @__PURE__ */ u2("path", { 3696 d: "M12 2v2M12 20v2M2 12h2M20 12h2M4.9 4.9l1.5 1.5M17.6 17.6l1.5 1.5M4.9 19.1l1.5-1.5M17.6 6.4l1.5-1.5" 3697 }) 3698 ] 3699 }), 3700 image: /* @__PURE__ */ u2("svg", { 3701 viewBox: "0 0 24 24", 3702 children: [ 3703 /* @__PURE__ */ u2("rect", { 3704 x: "3", 3705 y: "5", 3706 width: "18", 3707 height: "14", 3708 rx: "2" 3709 }), 3710 /* @__PURE__ */ u2("circle", { 3711 cx: "8", 3712 cy: "11", 3713 r: "2" 3714 }), 3715 /* @__PURE__ */ u2("path", { 3716 d: "M21 15l-5-5-4 4-2-2-4 4" 3717 }) 3718 ] 3719 }), 3720 link: /* @__PURE__ */ u2("svg", { 3721 viewBox: "0 0 24 24", 3722 children: [ 3723 /* @__PURE__ */ u2("path", { 3724 d: "M10 13a5 5 0 0 1 0-7l1-1a5 5 0 0 1 7 7l-1 1" 3725 }), 3726 /* @__PURE__ */ u2("path", { 3727 d: "M14 11a5 5 0 0 1 0 7l-1 1a5 5 0 0 1-7-7l1-1" 3728 }) 3729 ] 3730 }), 3731 button: /* @__PURE__ */ u2("svg", { 3732 viewBox: "0 0 24 24", 3733 children: /* @__PURE__ */ u2("rect", { 3734 x: "5", 3735 y: "8", 3736 width: "14", 3737 height: "8", 3738 rx: "2" 3739 }) 3740 }), 3741 block: /* @__PURE__ */ u2("svg", { 3742 viewBox: "0 0 24 24", 3743 children: [
3744 /* @__PURE__ */ u2("rect", { 3745 x: "4", 3746 y: "4", 3747 width: "7", 3748 height: "7" 3749 }), 3750 /* @__PURE__ */ u2("rect", { 3751 x: "13", 3752 y: "4", 3753 width: "7", 3754 height: "7" 3755 }), 3756 /* @__PURE__ */ u2("rect", { 3757 x: "4", 3758 y: "13", 3759 width: "7", 3760 height: "7" 3761 }), 3762 /* @__PURE__ */ u2("rect", { 3763 x: "13", 3764 y: "13", 3765 width: "7", 3766 height: "7" 3767 }) 3768 ] 3769 }), 3770 video: /* @__PURE__ */ u2("svg", { 3771 viewBox: "0 0 24 24", 3772 children: [ 3773 /* @__PURE__ */ u2("polygon", { 3774 points: "23 7 16 12 23 17 23 7" 3775 }), 3776 /* @__PURE__ */ u2("rect", { 3777 x: "1", 3778 y: "5", 3779 width: "15", 3780 height: "14", 3781 rx: "2" 3782 }) 3783 ] 3784 }), 3785 bolt: /* @__PURE__ */ u2("svg", { 3786 viewBox: "0 0 24 24", 3787 children: /* @__PURE__ */ u2("path", { 3788 d: "M13 10V3L4 14h7v7l9-11z" 3789 }) 3790 }), 3791 clock: /* @__PURE__ */ u2("svg", { 3792 viewBox: "0 0 24 24", 3793 children: [
3794 /* @__PURE__ */ u2("circle", { 3795 cx: "12", 3796 cy: "12", 3797 r: "9" 3798 }), 3799 /* @__PURE__ */ u2("path", { 3800 d: "M12 7v5l3 2" 3801 }) 3802 ] 3803 }), 3804 checkbox: /* @__PURE__ */ u2("svg", { 3805 viewBox: "0 0 24 24", 3806 children: [ 3807 /* @__PURE__ */ u2("path", { 3808 d: "M9 11l3 3L22 4" 3809 }), 3810 /* @__PURE__ */ u2("path", { 3811 d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" 3812 }) 3813 ] 3814 }), 3815 chart: /* @__PURE__ */ u2("svg", { 3816 viewBox: "0 0 24 24", 3817 children: [ 3818 /* @__PURE__ */ u2("path", { 3819 d: "M3 3v18h18" 3820 }), 3821 /* @__PURE__ */ u2("path", { 3822 d: "M7 14l3-3 3 3 5-5" 3823 }) 3824 ] 3825 }), 3826 heading: /* @__PURE__ */ u2("svg", { 3827 viewBox: "0 0 24 24", 3828 children: /* @__PURE__ */ u2("path", { 3829 d: "M6 4v16M18 4v16M6 12h12" 3830 }) 3831 }), 3832 upload: /* @__PURE__ */ u2("svg", { 3833 viewBox: "0 0 24 24", 3834 children: [ 3835 /* @__PURE__ */ u2("path", { 3836 d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" 3837 }),
3838 /* @__PURE__ */ u2("polyline", { 3839 points: "17 8 12 3 7 8" 3840 }), 3841 /* @__PURE__ */ u2("path", { 3842 d: "M12 3v12" 3843 }) 3844 ] 3845 }), 3846 list: /* @__PURE__ */ u2("svg", { 3847 viewBox: "0 0 24 24", 3848 children: [ 3849 /* @__PURE__ */ u2("circle", { 3850 cx: "5", 3851 cy: "6", 3852 r: "1.2", 3853 fill: "currentColor" 3854 }), 3855 /* @__PURE__ */ u2("circle", { 3856 cx: "5", 3857 cy: "12", 3858 r: "1.2", 3859 fill: "currentColor" 3860 }), 3861 /* @__PURE__ */ u2("circle", { 3862 cx: "5", 3863 cy: "18", 3864 r: "1.2", 3865 fill: "currentColor" 3866 }), 3867 /* @__PURE__ */ u2("path", { 3868 d: "M10 6h10M10 12h10M10 18h10" 3869 }) 3870 ] 3871 }), 3872 props: /* @__PURE__ */ u2("svg", { 3873 viewBox: "0 0 24 24", 3874 children: [ 3875 /* @__PURE__ */ u2("path", { 3876 d: "M4 6h16M4 12h16M4 18h10" 3877 }), 3878 /* @__PURE__ */ u2("circle", { 3879 cx: "18", 3880 cy: "18", 3881 r: "2" 3882 }) 3883 ] 3884 }), 3885 cursor: /* @__PURE__ */ u2("svg", { 3886 viewBox: "0 0 24 24", 3887 children: /* @__PURE__ */ u2("path", { 3888 d: "M3 3l7 18 2-8 8-2L3 3z" 3889 }) 3890 }), 3891 search: /* @__PURE__ */ u2("svg", { 3892 viewBox: "0 0 24 24", 3893 children: [
3894 /* @__PURE__ */ u2("circle", { 3895 cx: "11", 3896 cy: "11", 3897 r: "7" 3898 }), 3899 /* @__PURE__ */ u2("path", { 3900 d: "M21 21l-4.3-4.3" 3901 }) 3902 ] 3903 }), 3904 undo: /* @__PURE__ */ u2("svg", { 3905 viewBox: "0 0 24 24", 3906 children: [ 3907 /* @__PURE__ */ u2("path", { 3908 d: "M9 14l-4-4 4-4" 3909 }), 3910 /* @__PURE__ */ u2("path", { 3911 d: "M5 10h9a5 5 0 0 1 0 10h-2" 3912 }) 3913 ] 3914 }), 3915 redo: /* @__PURE__ */ u2("svg", { 3916 viewBox: "0 0 24 24", 3917 children: [ 3918 /* @__PURE__ */ u2("path", { 3919 d: "M15 14l4-4-4-4" 3920 }), 3921 /* @__PURE__ */ u2("path", { 3922 d: "M19 10h-9a5 5 0 0 0 0 10h2" 3923 }) 3924 ] 3925 }), 3926 chevronUp: /* @__PURE__ */ u2("svg", { 3927 viewBox: "0 0 24 24", 3928 children: /* @__PURE__ */ u2("path", { 3929 d: "M18 15l-6-6-6 6" 3930 }) 3931 }), 3932 chevronDown: /* @__PURE__ */ u2("svg", { 3933 viewBox: "0 0 24 24", 3934 children: /* @__PURE__ */ u2("path", { 3935 d: "M6 9l6 6 6-6" 3936 }) 3937 }), 3938 chevronLeft: /* @__PURE__ */ u2("svg", { 3939 viewBox: "0 0 24 24", 3940 children: /* @__PURE__ */ u2("path", { 3941 d: "M15 6l-6 6 6 6" 3942 }) 3943 }), 3944 chevronRight: /* @__PURE__ */ u2("svg", { 3945 viewBox: "0 0 24 24", 3946 children: /* @__PURE__ */ u2("path", { 3947 d: "M9 6l6 6-6 6" 3948 }) 3949 }), 3950 cross: /* @__PURE__ */ u2("svg", { 3951 viewBox: "0 0 24 24", 3952 children: /* @__PURE__ */ u2("path", { 3953 d: "M18 6L6 18M6 6l12 12" 3954 }) 3955 }), 3956 warn: /* @__PURE__ */ u2("svg", { 3957 viewBox: "0 0 24 24", 3958 children: [
3959 /* @__PURE__ */ u2("path", { 3960 d: "M12 3l9 16H3z" 3961 }), 3962 /* @__PURE__ */ u2("path", { 3963 d: "M12 9v5M12 17h.01" 3964 }) 3965 ] 3966 }), 3967 desktop: /* @__PURE__ */ u2("svg", { 3968 viewBox: "0 0 24 24", 3969 children: [ 3970 /* @__PURE__ */ u2("rect", { 3971 x: "3", 3972 y: "4", 3973 width: "18", 3974 height: "12", 3975 rx: "1" 3976 }), 3977 /* @__PURE__ */ u2("path", { 3978 d: "M8 20h8M12 16v4" 3979 }) 3980 ] 3981 }), 3982 tablet: /* @__PURE__ */ u2("svg", { 3983 viewBox: "0 0 24 24", 3984 children: [ 3985 /* @__PURE__ */ u2("rect", { 3986 x: "5", 3987 y: "3", 3988 width: "14", 3989 height: "18", 3990 rx: "2" 3991 }), 3992 /* @__PURE__ */ u2("path", { 3993 d: "M11 18h2" 3994 }) 3995 ] 3996 }), 3997 mobile: /* @__PURE__ */ u2("svg", { 3998 viewBox: "0 0 24 24", 3999 children: [ 4000 /* @__PURE__ */ u2("rect", { 4001 x: "7", 4002 y: "2", 4003 width: "10", 4004 height: "20", 4005 rx: "2" 4006 }), 4007 /* @__PURE__ */ u2("path", { 4008 d: "M11 19h2" 4009 }) 4010 ] 4011 }), 4012 /* Every screen: a monitor with a phone in front of it - "Show on: All devices". */ 4013 allDevices: /* @__PURE__ */ u2("svg", { 4014 viewBox: "0 0 24 24", 4015 children: [
4016 /* @__PURE__ */ u2("path", { 4017 d: "M15 15H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v3" 4018 }), 4019 /* @__PURE__ */ u2("path", { 4020 d: "M8 19h5M10 15v4" 4021 }), 4022 /* @__PURE__ */ u2("rect", { 4023 x: "16", 4024 y: "10", 4025 width: "6", 4026 height: "11", 4027 rx: "1" 4028 }) 4029 ] 4030 }), 4031 send: /* @__PURE__ */ u2("svg", { 4032 viewBox: "0 0 24 24", 4033 children: [ 4034 /* @__PURE__ */ u2("path", { 4035 d: "M22 2L11 13" 4036 }), 4037 /* @__PURE__ */ u2("path", { 4038 d: "M22 2l-7 20-4-9-9-4z" 4039 }) 4040 ] 4041 }), 4042 play: /* @__PURE__ */ u2("svg", { 4043 viewBox: "0 0 24 24", 4044 children: /* @__PURE__ */ u2("polygon", { 4045 points: "5 3 19 12 5 21 5 3" 4046 }) 4047 }), 4048 gear: /* @__PURE__ */ u2("svg", { 4049 viewBox: "0 0 24 24", 4050 children: [ 4051 /* @__PURE__ */ u2("circle", { 4052 cx: "12", 4053 cy: "12", 4054 r: "3" 4055 }), 4056 /* @__PURE__ */ u2("path", { 4057 d: "M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z" 4058 }) 4059 ] 4060 }), 4061 reload: /* @__PURE__ */ u2("svg", { 4062 viewBox: "0 0 24 24", 4063 children: [
4064 /* @__PURE__ */ u2("path", { 4065 d: "M21 12a9 9 0 1 1-3-6.7L21 8" 4066 }), 4067 /* @__PURE__ */ u2("path", { 4068 d: "M21 3v5h-5" 4069 }) 4070 ] 4071 }), 4072 dock: /* @__PURE__ */ u2("svg", { 4073 viewBox: "0 0 24 24", 4074 children: [ 4075 /* @__PURE__ */ u2("rect", { 4076 x: "3", 4077 y: "4", 4078 width: "18", 4079 height: "16", 4080 rx: "2" 4081 }), 4082 /* @__PURE__ */ u2("path", { 4083 d: "M15 4v16" 4084 }) 4085 ] 4086 }), 4087 text: /* @__PURE__ */ u2("svg", { 4088 viewBox: "0 0 24 24", 4089 children: /* @__PURE__ */ u2("path", { 4090 d: "M4 7V5h16v2M9 19h6M12 5v14" 4091 }) 4092 }), 4093 ext: /* @__PURE__ */ u2("svg", { 4094 viewBox: "0 0 24 24", 4095 children: [ 4096 /* @__PURE__ */ u2("path", { 4097 d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" 4098 }), 4099 /* @__PURE__ */ u2("polyline", { 4100 points: "15 3 21 3 21 9" 4101 }), 4102 /* @__PURE__ */ u2("path", { 4103 d: "M10 14L21 3" 4104 }) 4105 ] 4106 }), 4107 braces: /* @__PURE__ */ u2("svg", { 4108 viewBox: "0 0 24 24", 4109 children: [
4110 /* @__PURE__ */ u2("path", { 4111 d: "M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1" 4112 }), 4113 /* @__PURE__ */ u2("path", { 4114 d: "M16 3h1a2 2 0 0 1 2 2v5a2 2 0 0 0 2 2 2 2 0 0 0-2 2v5a2 2 0 0 1-2 2h-1" 4115 }) 4116 ] 4117 }), 4118 sparkle: /* @__PURE__ */ u2("svg", { 4119 viewBox: "0 0 24 24", 4120 children: [ 4121 /* @__PURE__ */ u2("path", { 4122 d: "M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z" 4123 }), 4124 /* @__PURE__ */ u2("path", { 4125 d: "M19 15l.9 2.1L22 18l-2.1.9L19 21l-.9-2.1L16 18l2.1-.9z" 4126 }) 4127 ] 4128 }), 4129 stop: /* @__PURE__ */ u2("svg", { 4130 viewBox: "0 0 24 24", 4131 children: /* @__PURE__ */ u2("rect", { 4132 x: "6", 4133 y: "6", 4134 width: "12", 4135 height: "12", 4136 rx: "1" 4137 }) 4138 }), 4139 alignLeft: /* @__PURE__ */ u2("svg", { 4140 viewBox: "0 0 24 24", 4141 children: /* @__PURE__ */ u2("path", { 4142 d: "M4 6h16M4 12h10M4 18h16" 4143 }) 4144 }), 4145 alignCenter: /* @__PURE__ */ u2("svg", { 4146 viewBox: "0 0 24 24", 4147 children: /* @__PURE__ */ u2("path", { 4148 d: "M4 6h16M7 12h10M4 18h16" 4149 }) 4150 }), 4151 alignRight: /* @__PURE__ */ u2("svg", { 4152 viewBox: "0 0 24 24", 4153 children: /* @__PURE__ */ u2("path", { 4154 d: "M4 6h16M10 12h10M4 18h16" 4155 }) 4156 }), 4157 alignJustify: /* @__PURE__ */ u2("svg", { 4158 viewBox: "0 0 24 24", 4159 children: /* @__PURE__ */ u2("path", { 4160 d: "M4 6h16M4 12h16M4 18h16" 4161 }) 4162 }), 4163 drop: /* @__PURE__ */ u2("svg", { 4164 viewBox: "0 0 24 24", 4165 children: /* @__PURE__ */ u2("path", { 4166 d: "M12 3s6 6.6 6 10.5a6 6 0 0 1-12 0C6 9.6 12 3 12 3z" 4167 }) 4168 }), 4169 scroll: /* @__PURE__ */ u2("svg", { 4170 viewBox: "0 0 24 24", 4171 children: /* @__PURE__ */ u2("path", { 4172 d: "M12 5v14M5 12l7 7 7-7" 4173 }) 4174 }), 4175 eye: /* @__PURE__ */ u2("svg", { 4176 viewBox: "0 0 24 24", 4177 children: [
4178 /* @__PURE__ */ u2("path", { 4179 d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" 4180 }), 4181 /* @__PURE__ */ u2("circle", { 4182 cx: "12", 4183 cy: "12", 4184 r: "3" 4185 }) 4186 ] 4187 }), 4188 info: /* @__PURE__ */ u2("svg", { 4189 viewBox: "0 0 24 24", 4190 children: [ 4191 /* @__PURE__ */ u2("circle", { 4192 cx: "12", 4193 cy: "12", 4194 r: "9" 4195 }), 4196 /* @__PURE__ */ u2("path", { 4197 d: "M12 11v5M12 8h.01" 4198 }) 4199 ] 4200 }), 4201 /* Where a new element goes, drawn as what it looks like on the page: the picked element as a 4202 solid box, and the new one as a dashed one above it, inside it, or below it. */ 4203 putBefore: /* @__PURE__ */ u2("svg", { 4204 viewBox: "0 0 24 24", 4205 children: [ 4206 /* @__PURE__ */ u2("rect", { 4207 x: "4", 4208 y: "3", 4209 width: "16", 4210 height: "6", 4211 rx: "1", 4212 "stroke-dasharray": "3 2" 4213 }), 4214 /* @__PURE__ */ u2("rect", { 4215 x: "4", 4216 y: "13", 4217 width: "16", 4218 height: "8", 4219 rx: "1" 4220 }) 4221 ] 4222 }), 4223 putInside: /* @__PURE__ */ u2("svg", { 4224 viewBox: "0 0 24 24", 4225 children: [
4226 /* @__PURE__ */ u2("rect", { 4227 x: "3", 4228 y: "4", 4229 width: "18", 4230 height: "16", 4231 rx: "1" 4232 }), 4233 /* @__PURE__ */ u2("rect", { 4234 x: "7", 4235 y: "12", 4236 width: "10", 4237 height: "5", 4238 rx: "1", 4239 "stroke-dasharray": "3 2" 4240 }) 4241 ] 4242 }), 4243 putAfter: /* @__PURE__ */ u2("svg", { 4244 viewBox: "0 0 24 24", 4245 children: [ 4246 /* @__PURE__ */ u2("rect", { 4247 x: "4", 4248 y: "3", 4249 width: "16", 4250 height: "8", 4251 rx: "1" 4252 }), 4253 /* @__PURE__ */ u2("rect", { 4254 x: "4", 4255 y: "15", 4256 width: "16", 4257 height: "6", 4258 rx: "1", 4259 "stroke-dasharray": "3 2" 4260 }) 4261 ] 4262 }), 4263 scopePage: /* @__PURE__ */ u2("svg", { 4264 viewBox: "0 0 24 24", 4265 children: [ 4266 /* @__PURE__ */ u2("path", { 4267 d: "M6 3h8l4 4v14H6z" 4268 }), 4269 /* @__PURE__ */ u2("path", { 4270 d: "M14 3v4h4" 4271 }) 4272 ] 4273 }), 4274 scopeShape: /* @__PURE__ */ u2("svg", { 4275 viewBox: "0 0 24 24", 4276 children: [
4277 /* @__PURE__ */ u2("path", { 4278 d: "M8 6V4.5A1.5 1.5 0 0 1 9.5 3h9A1.5 1.5 0 0 1 20 4.5v11a1.5 1.5 0 0 1-1.5 1.5H17" 4279 }), 4280 /* @__PURE__ */ u2("rect", { 4281 x: "4", 4282 y: "6", 4283 width: "13", 4284 height: "15", 4285 rx: "1.5" 4286 }), 4287 /* @__PURE__ */ u2("path", { 4288 d: "M7 10h7M7 14h5" 4289 }) 4290 ] 4291 }), 4292 scopeAny: /* @__PURE__ */ u2("svg", { 4293 viewBox: "0 0 24 24", 4294 children: [ 4295 /* @__PURE__ */ u2("circle", { 4296 cx: "12", 4297 cy: "12", 4298 r: "9" 4299 }), 4300 /* @__PURE__ */ u2("path", { 4301 d: "M3 12h18" 4302 }), 4303 /* @__PURE__ */ u2("path", { 4304 d: "M12 3a14 14 0 0 1 0 18a14 14 0 0 1 0-18z" 4305 }) 4306 ] 4307 }) 4308}; 4309
4310// src/components/live/liveKit.tsx 4311function Field(props) { 4312 return /* @__PURE__ */ u2("div", { 4313 class: props.wide ? "le-field le-field-wide" : "le-field", 4314 children: [ 4315 /* @__PURE__ */ u2("label", { 4316 children: props.label 4317 }), 4318 /* @__PURE__ */ u2("div", { 4319 class: "le-field-group", 4320 children: props.children 4321 }) 4322 ] 4323 }); 4324} 4325function Section(props) { 4326 return /* @__PURE__ */ u2("div", { 4327 class: "le-sec", 4328 children: [ 4329 props.title && /* @__PURE__ */ u2("h5", { 4330 children: props.title 4331 }), 4332 props.children 4333 ] 4334 }); 4335} 4336function toHexColor(value) { 4337 const text = (value ?? "").trim(); 4338 if (/^#[0-9A-F]{6}$/i.test(text)) { 4339 return text.toUpperCase(); 4340 } 4341 if (/^#[0-9A-F]{3}$/i.test(text)) { 4342 return ("#" + text[1] + text[1] + text[2] + text[2] + text[3] + text[3]).toUpperCase(); 4343 } 4344 const m2 = text.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i); 4345 if (m2) { 4346 const hex = (n2) => Math.max(0, Math.min(255, Math.round(parseFloat(n2)))).toString(16).padStart(2, "0"); 4347 return ("#" + hex(m2[1]) + hex(m2[2]) + hex(m2[3])).toUpperCase(); 4348 } 4349 return ""; 4350} 4351function ColorInput(props) { 4352 const hex = toHexColor(props.value); 4353 const [draft, setDraft] = p2(null); 4354 const shown = draft ?? (hex || props.value); 4355 return /* @__PURE__ */ u2("div", { 4356 class: "le-color", 4357 children: [
4358 /* @__PURE__ */ u2("label", { 4359 class: "le-swatch" + (hex ? "" : " le-empty"), 4360 style: hex ? "background-color:" + hex : "", 4361 title: hex ? "Pick a color - now " + hex : "Pick a color", 4362 children: /* @__PURE__ */ u2("input", { 4363 type: "color", 4364 value: hex || "#000000", 4365 onInput: (ev) => { 4366 setDraft(null); 4367 props.onChange(ev.target.value.toUpperCase()); 4368 } 4369 }) 4370 }), 4371 /* @__PURE__ */ u2("input", { 4372 type: "text", 4373 value: shown, 4374 placeholder: "auto", 4375 spellcheck: false, 4376 onInput: (ev) => { 4377 const text = ev.target.value; 4378 setDraft(text); 4379 props.onChange(text); 4380 }, 4381 onBlur: () => setDraft(null) 4382 }) 4383 ] 4384 }); 4385} 4386function NumInput(props) { 4387 return /* @__PURE__ */ u2("div", { 4388 class: "le-num", 4389 children: [ 4390 /* @__PURE__ */ u2("input", { 4391 type: "number", 4392 value: props.value, 4393 min: props.min, 4394 max: props.max, 4395 step: props.step, 4396 onInput: (ev) => props.onChange(ev.target.value) 4397 }),
4398 /* @__PURE__ */ u2("span", { 4399 class: "le-unit", 4400 children: props.unit 4401 }) 4402 ] 4403 }); 4404} 4405function SelectInput(props) { 4406 return /* @__PURE__ */ u2("select", { 4407 class: "le-select", 4408 value: props.value, 4409 onChange: (ev) => props.onChange(ev.target.value), 4410 children: props.options.map(([value, label]) => /* @__PURE__ */ u2("option", { 4411 value, 4412 children: label 4413 }, value)) 4414 }); 4415} 4416function TextInput(props) { 4417 return /* @__PURE__ */ u2("input", { 4418 class: "le-input", 4419 type: "text", 4420 value: props.value, 4421 placeholder: props.placeholder, 4422 onInput: (ev) => props.onChange(ev.target.value) 4423 }); 4424} 4425function IconGroup(props) { 4426 return /* @__PURE__ */ u2("div", { 4427 class: "le-icongroup", 4428 children: props.choices.map((choice) => /* @__PURE__ */ u2("button", { 4429 type: "button", 4430 title: choice.title, 4431 class: choice.value == props.value ? "le-on" : void 0, 4432 onClick: () => props.onChange(choice.value == props.value ? "" : choice.value), 4433 children: choice.icon ?? choice.text 4434 }, choice.value)) 4435 }); 4436} 4437function PanelTabs(props) { 4438 return /* @__PURE__ */ u2("div", { 4439 class: "le-ptabs", 4440 role: "tablist", 4441 children: props.tabs.map((tab) => /* @__PURE__ */ u2("button", { 4442 type: "button", 4443 role: "tab", 4444 "aria-selected": tab.id == props.value, 4445 class: "le-ptab" + (tab.id == props.value ? " le-on" : ""), 4446 title: tab.title, 4447 onClick: () => props.onChange(tab.id), 4448 children: tab.label 4449 }, tab.id)) 4450 }); 4451} 4452function CountClicksCheck(props) { 4453 const title = props.counted ? "Clicks on this element are already counted by an earlier change - switch that off in the list of changes." : (props.isInsert ? "Clicks on the new element" : "Clicks on this element") + " count toward this action's Clicks in the campaign report."; 4454 return /* @__PURE__ */ u2("label", { 4455 class: "le-count-clicks" + (props.on ? " le-on" : "") + (props.counted ? " le-locked" : ""), 4456 "data-tooltip": title, 4457 "data-tooltip-pos": "top", 4458 children: [
4459 /* @__PURE__ */ u2("input", { 4460 type: "checkbox", 4461 class: "le-check", 4462 checked: props.on, 4463 disabled: props.counted, 4464 onChange: (ev) => props.onChange(ev.target.checked) 4465 }), 4466 props.counted ? "Clicks counted" : "Count clicks" 4467 ] 4468 }); 4469} 4470function SubPanelHead(props) { 4471 return /* @__PURE__ */ u2(k, { 4472 children: [ 4473 /* @__PURE__ */ u2("div", { 4474 class: "le-sub-head", 4475 children: [ 4476 /* @__PURE__ */ u2("button", { 4477 type: "button", 4478 class: "le-back", 4479 onClick: props.onBack, 4480 title: "Discard what you changed here and go back to the tiles - the same as Cancel below", 4481 children: [ 4482 li.chevronLeft, 4483 "Back" 4484 ] 4485 }), 4486 /* @__PURE__ */ u2("b", { 4487 children: props.title 4488 }), 4489 /* @__PURE__ */ u2("div", { 4490 class: "le-spacer" 4491 }), 4492 props.extra 4493 ] 4494 }), 4495 /* @__PURE__ */ u2("div", { 4496 class: "le-target", 4497 children: [ 4498 "Editing ",
4499 /* @__PURE__ */ u2("code", { 4500 title: props.target, 4501 children: props.target 4502 }) 4503 ] 4504 }) 4505 ] 4506 }); 4507} 4508
4509// src/components/live/widgetCatalogue.ts 4510var WIDGET_CATALOGUE = [ 4511 { 4512 "id": "countdown", 4513 "label": "Countdown", 4514 "html": '<div class="pz-block-timer" data-pz-timer="{"type":"restart_everytime","fixed_date_date":"","fixed_date_timezone":"","fixed_date_time":"","cookie_based_days":0,"cookie_based_hours":1,"cookie_based_minutes":0,"cookie_based_restart":0,"cookie_based_restart_after":"hours","cookie_based_restart_after_hours":1,"cookie_based_restart_after_weekday":0,"restart_everytime_days":0,"restart_everytime_hours":1,"restart_everytime_minutes":0}" style="text-align:center; margin:16px 0; display:flex; flex-wrap:wrap; gap:6px; justify-content:center;"><span style="background:#0F172A; color:#fff; padding:8px 12px; border-radius:6px; min-width:46px; display:inline-block; text-align:center;"><strong data-pz-timer-unit="d" style="font-size:18px; font-weight:800; display:block;">0</strong><small style="font-size:9px; opacity:0.7; text-transform:uppercase;">Day(s)</small></span><span style="background:#0F172A; color:#fff; padding:8px 12px; border-radius:6px; min-width:46px; display:inline-block; text-align:center;"><strong data-pz-timer-unit="h" style="font-size:18px; font-weight:800; display:block;">1</strong><small style="font-size:9px; opacity:0.7; text-transform:uppercase;">Hour(s)</small></span><span style="background:#0F172A; color:#fff; padding:8px 12px; border-radius:6px; min-width:46px; display:inline-block; text-align:center;"><strong data-pz-timer-unit="m" data-pz-timer-pad="2" style="font-size:18px; font-weight:800; display:block;">00</strong><small style="font-size:9px; opacity:0.7; text-transform:uppercase;">Minute(s)</small></span><span style="background:#0F172A; color:#fff; padding:8px 12px; border-radius:6px; min-width:46px; display:inline-block; text-align:center;"><strong data-pz-timer-unit="s" data-pz-timer-pad="2" style="font-size:18px; font-weight:800; display:block;">00</strong><small style="font-size:9px; opacity:0.7; text-transform:uppercase;">Second(s)</small></span></div>', 4515 "props": { 4516 "anim": "", 4517 "device": "", 4518 "cssMobile": "", 4519 "beside": "", 4520 "width": "", 4521 "boxAlign": "center", 4522 "spaceTop": "", 4523 "spaceBottom": "", 4524 "boxBg": "", 4525 "boxPad": "", 4526 "boxBorder": 0, 4527 "boxBorderColor": "#E2E8F0", 4528 "boxCorner": "", 4529 "boxShadow": "", 4530 "animLoop": "", 4531 "css": "", 4532 "timer": '{"type":"restart_everytime","fixed_date_date":"","fixed_date_timezone":"","fixed_date_time":"","cookie_based_days":0,"cookie_based_hours":1,"cookie_based_minutes":0,"cookie_based_restart":0,"cookie_based_restart_after":"hours","cookie_based_restart_after_hours":1,"cookie_based_restart_after_weekday":0,"restart_everytime_days":0,"restart_everytime_hours":1,"restart_everytime_minutes":0}', 4533 "capDays": "Day(s)", 4534 "capHours": "Hour(s)", 4535 "capMinutes": "Minute(s)", 4536 "capSeconds": "Second(s)", 4537 "cellBg": "#0F172A", 4538 "cellColor": "#fff", 4539 "styleKind": "boxes", 4540 "digitSize": 18, 4541 "boxRadius": 6 4542 }, 4543 "settings": [ 4544 { 4545 "key": "timer", 4546 "label": "Countdown", 4547 "kind": "timer", 4548 "tab": "content", 4549 "hint": "Fixed date, per-visitor cookie, or every visit \u2014 and, at the bottom, what happens when it reaches zero: sit at zeros, swap in a message, or disappear." 4550 }, 4551 { 4552 "key": "capDays", 4553 "label": "Days caption (blank hides days)", 4554 "kind": "text", 4555 "tab": "content", 4556 "hint": "Blank caption hides the unit; hidden units roll into the next." 4557 }, 4558 { 4559 "key": "capHours", 4560 "label": "Hours caption (blank hides hours)", 4561 "kind": "text", 4562 "tab": "content" 4563 }, 4564 { 4565 "key": "capMinutes", 4566 "label": "Minutes caption (blank hides minutes)", 4567 "kind": "text", 4568 "tab": "content" 4569 }, 4570 { 4571 "key": "capSeconds", 4572 "label": "Seconds caption (blank hides seconds)", 4573 "kind": "text", 4574 "tab": "content" 4575 }, 4576 { 4577 "key": "styleKind", 4578 "label": "Style", 4579 "kind": "select", 4580 "tab": "style",
4581 "hint": "Boxes, pills, or bare digits with colons.", 4582 "options": [ 4583 [ 4584 "boxes", 4585 "Boxes" 4586 ], 4587 [ 4588 "pills", 4589 "Pills" 4590 ], 4591 [ 4592 "minimal", 4593 "Minimal \u2014 digits and colons" 4594 ] 4595 ] 4596 }, 4597 { 4598 "key": "digitSize", 4599 "label": "Digit size", 4600 "kind": "number", 4601 "tab": "style", 4602 "unit": "px", 4603 "min": 12, 4604 "max": 64 4605 }, 4606 { 4607 "key": "boxRadius", 4608 "label": "Corner radius", 4609 "kind": "number", 4610 "tab": "style", 4611 "unit": "px", 4612 "min": 0, 4613 "max": 100 4614 }, 4615 { 4616 "key": "cellBg", 4617 "label": "Cell background", 4618 "kind": "color", 4619 "tab": "style" 4620 }, 4621 { 4622 "key": "cellColor", 4623 "label": "Cell text", 4624 "kind": "color", 4625 "tab": "style" 4626 } 4627 ] 4628 }, 4629 { 4630 "id": "coupon", 4631 "label": "Coupon", 4632 "html": '<div style="display:flex; align-items:stretch; max-width:280px; margin:12px auto; border-radius:8px; overflow:hidden; border:2px dashed #92400E;"><span class="pz-coupon-code" style="flex:1; background:#FEF3C7; color:#92400E; padding:10px 14px; font-family:monospace; font-weight:700; letter-spacing:2px; text-align:center;">WELCOME10</span><button type="button" class="pz-copy-code" data-copied-label="COPIED!" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; background:#92400E; color:#fff; border:0; padding:0 14px; font-weight:700; font-size:11px; cursor:pointer;">COPY</button></div>', 4633 "props": { 4634 "anim": "", 4635 "device": "", 4636 "cssMobile": "", 4637 "beside": "", 4638 "width": "", 4639 "boxAlign": "center", 4640 "spaceTop": "", 4641 "spaceBottom": "", 4642 "boxBg": "", 4643 "boxPad": "", 4644 "boxBorder": 0, 4645 "boxBorderColor": "#E2E8F0", 4646 "boxCorner": "", 4647 "boxShadow": "", 4648 "hoverBg": "", 4649 "hoverText": "", 4650 "report": "", 4651 "reportEvent": "", 4652 "reportValue": "", 4653 "animLoop": "", 4654 "css": "", 4655 "code": "WELCOME10", 4656 "bg": "#FEF3C7", 4657 "color": "#92400E", 4658 "borderStyle": "dashed", 4659 "copyLabel": "COPY", 4660 "copiedLabel": "COPIED!" 4661 }, 4662 "settings": [ 4663 { 4664 "key": "code", 4665 "label": "Coupon code", 4666 "kind": "text", 4667 "tab": "content" 4668 }, 4669 { 4670 "key": "bg", 4671 "label": "Background", 4672 "kind": "color", 4673 "tab": "style" 4674 }, 4675 { 4676 "key": "color", 4677 "label": "Text & border", 4678 "kind": "color", 4679 "tab": "style" 4680 }, 4681 { 4682 "key": "borderStyle", 4683 "label": "Border style", 4684 "kind": "select", 4685 "tab": "style", 4686 "options": [ 4687 [ 4688 "dashed", 4689 "Dashed" 4690 ], 4691 [ 4692 "solid", 4693 "Solid" 4694 ], 4695 [ 4696 "dotted", 4697 "Dotted" 4698 ] 4699 ] 4700 }, 4701 { 4702 "key": "copyLabel", 4703 "label": "Copy button label", 4704 "kind": "text", 4705 "tab": "content", 4706 "hint": "One click copies the code to the visitor\u2019s clipboard and briefly shows the \u201Ccopied\u201D label." 4707 }, 4708 { 4709 "key": "copiedLabel", 4710 "label": "Label after copying", 4711 "kind": "text", 4712 "tab": "content" 4713 }, 4714 { 4715 "key": "hoverBg", 4716 "label": "Background on hover", 4717 "kind": "color", 4718 "tab": "style", 4719 "hint": "What it changes to under the mouse. Blank keeps it still." 4720 }, 4721 { 4722 "key": "hoverText", 4723 "label": "Text on hover", 4724 "kind": "color", 4725 "tab": "style" 4726 }, 4727 { 4728 "key": "report", 4729 "label": "When clicked, report", 4730 "kind": "select", 4731 "tab": "content", 4732 "hint": "Beside the click itself, which is always counted.", 4733 "options": [ 4734 [ 4735 "", 4736 "Nothing extra" 4737 ], 4738 [ 4739 "goal", 4740 "This action reached its goal" 4741 ], 4742 [ 4743 "event", 4744 "A conversion goal" 4745 ], 4746 [ 4747 "both", 4748 "Both" 4749 ] 4750 ] 4751 }, 4752 { 4753 "key": "reportEvent", 4754 "label": "Which goal", 4755 "kind": "goal", 4756 "tab": "content", 4757 "hint": "Event-driven goals only \u2014 the kind that waits for a named event. Other goal types watch the page themselves and need nothing here." 4758 }, 4759 { 4760 "key": "reportValue", 4761 "label": "Value", 4762 "kind": "text", 4763 "tab": "content", 4764 "hint": "Optional \u2014 a revenue amount, a plan name. Variables work here." 4765 } 4766 ] 4767 }, 4768 { 4769 "id": "wheel", 4770 "label": "Prize Wheel", 4771 "html": '<div class="pz-block-wheel" data-pz-wheel="{"w":[3,3,2,4,3,1],"once":1,"snd":0,"cel":"confetti"}" style="text-align:center; margin:16px 0;"><div style="width:260px; max-width:100%; margin:0 auto;"><div style="position:relative; height:0; padding-bottom:100%;"><div class="pz-wheel-disc" style="position:absolute; inset:0; border-radius:50%; background:conic-gradient(from -30deg, #0EA5E9 0deg 60deg, #F1F5F9 60deg 120deg, #818CF8 120deg 180deg, #E2E8F0 180deg 240deg, #F97316 240deg 300deg, #10B981 300deg 360deg); box-shadow:0 6px 20px rgba(15,23,42,0.22); transition:transform 4.2s cubic-bezier(0.15,0.7,0.15,1);"><span style="position:absolute; inset:0; transform:rotate(0deg);"><span style="display:block; width:58%; margin:6% auto 0; text-align:center; font:700 12px/1.2 inherit; color:#0F172A; overflow:hidden;">10% off</span></span><span style="position:absolute; inset:0; transform:rotate(60deg);"><span style="display:block; width:58%; margin:6% auto 0; text-align:center; font:700 12px/1.2 inherit; color:#0F172A; overflow:hidden;">Free shipping</span></span><span style="position:absolute; inset:0; transform:rotate(120deg);"><span style="display:block; width:58%; margin:6% auto 0; text-align:center; font:700 12px/1.2 inherit; color:#0F172A; overflow:hidden;">20% off</span></span><span style="position:absolute; inset:0; transform:rotate(180deg);"><span style="display:block; width:58%; margin:6% auto 0; text-align:center; font:700 12px/1.2 inherit; color:#0F172A; overflow:hidden;">Try again</span></span><span style="position:absolute; inset:0; transform:rotate(240deg);"><span style="display:block; width:58%; margin:6% auto 0; text-align:center; font:700 12px/1.2 inherit; color:#0F172A; overflow:hidden;">5% off</span></span><span style="position:absolute; inset:0; transform:rotate(300deg);"><span style="display:block; width:58%; margin:6% auto 0; text-align:center; font:700 12px/1.2 inherit; color:#0F172A; overflow:hidden;">Free gift</span></span></div><div style="position:absolute; top:-6px; left:50%; margin-left:-9px; width:0; height:0; border-left:9px solid transparent; border-right:9px solid transparent; border-top:16px solid #0F172A; z-index:2;"></div></div></div><button type="button" class="pz-wheel-go" data-personyze-click-target="1" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; margin-top:14px; background:#DB2777; color:#FFFFFF; border:0; padding:11px 26px; border-radius:8px; font:inherit; font-size:14px; font-weight:700; cursor:pointer;">Spin the wheel</button><div class="pz-wheel-result" style="display:none; margin-top:12px; font-size:14px; color:#334155;"></div>
4771<i data-pz-wheel-prize="0" style="display:none"><b>You won 10% off!</b><div style="display:flex; align-items:stretch; max-width:280px; margin:12px auto; border-radius:8px; overflow:hidden; border:2px dashed #92400E;"><span class="pz-coupon-code" style="flex:1; background:#FEF3C7; color:#92400E; padding:10px 14px; font-family:monospace; font-weight:700; letter-spacing:2px; text-align:center;">SPIN10</span><button type="button" class="pz-copy-code" data-copied-label="COPIED!" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; background:#92400E; color:#fff; border:0; padding:0 14px; font-weight:700; font-size:11px; cursor:pointer;">COPY</button></div></i><i data-pz-wheel-prize="1" style="display:none"><b>Free shipping is yours.</b><div style="display:flex; align-items:stretch; max-width:280px; margin:12px auto; border-radius:8px; overflow:hidden; border:2px dashed #92400E;"><span class="pz-coupon-code" style="flex:1; background:#FEF3C7; color:#92400E; padding:10px 14px; font-family:monospace; font-weight:700; letter-spacing:2px; text-align:center;">FREESHIP</span><button type="button" class="pz-copy-code" data-copied-label="COPIED!" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; background:#92400E; color:#fff; border:0; padding:0 14px; font-weight:700; font-size:11px; cursor:pointer;">COPY</button></div></i><i data-pz-wheel-prize="2" style="display:none"><b>You won 20% off!</b><div style="display:flex; align-items:stretch; max-width:280px; margin:12px auto; border-radius:8px; overflow:hidden; border:2px dashed #92400E;"><span class="pz-coupon-code" style="flex:1; background:#FEF3C7; color:#92400E; padding:10px 14px; font-family:monospace; font-weight:700; letter-spacing:2px; text-align:center;">SPIN20</span><button type="button" class="pz-copy-code" data-copied-label="COPIED!" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; background:#92400E; color:#fff; border:0; padding:0 14px; font-weight:700; font-size:11px; cursor:pointer;">COPY</button></div></i><i data-pz-wheel-prize="3" style="display:none"><b>No luck this time.</b></i><i data-pz-wheel-prize="4" style="display:none"><b>You won 5% off!</b><div style="display:flex; align-items:stretch; max-width:280px; margin:12px auto; border-radius:8px; overflow:hidden; border:2px dashed #92400E;"><span class="pz-coupon-code" style="flex:1; background:#FEF3C7; color:#92400E; padding:10px 14px; font-family:monospace; font-weight:700; letter-spacing:2px; text-align:center;">SPIN5</span><button type="button" class="pz-copy-code" data-copied-label="COPIED!" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; background:#92400E; color:#fff; border:0; padding:0 14px; font-weight:700; font-size:11px; cursor:pointer;">COPY</button></div></i><i data-pz-wheel-prize="5" style="display:none"><b>A free gift \u2014 it is in the bag.</b><div style="display:flex; align-items:stretch; max-width:280px; margin:12px auto; border-radius:8px; overflow:hidden; border:2px dashed #92400E;"><span class="pz-coupon-code" style="flex:1; background:#FEF3C7; color:#92400E; padding:10px 14px; font-family:monospace; font-weight:700; letter-spacing:2px; text-align:center;">GIFT</span><button type="button" class="pz-copy-code" data-copied-label="COPIED!" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; background:#92400E; color:#fff; border:0; padding:0 14px; font-weight:700; font-size:11px; cursor:pointer;">COPY</button></div></i></div>', 4772 "props": { 4773 "anim": "", 4774 "device": "", 4775 "cssMobile": "", 4776 "beside": "", 4777 "width": "", 4778 "boxAlign": "center", 4779 "spaceTop": "", 4780 "spaceBottom": "", 4781 "boxBg": "", 4782 "boxPad": "", 4783 "boxBorder": 0, 4784 "boxBorderColor": "#E2E8F0", 4785 "boxCorner": "", 4786 "boxShadow": "", 4787 "report": "", 4788 "reportEvent": "", 4789 "reportValue": "", 4790 "css": "", 4791 "segments": '[{"label":"10% off","color":"#0EA5E9","weight":3,"prize":"You won 10% off!","code":"SPIN10"},{"label":"Free shipping","color":"#F1F5F9","weight":3,"prize":"Free shipping is y
4791ours.","code":"FREESHIP"},{"label":"20% off","color":"#818CF8","weight":2,"prize":"You won 20% off!","code":"SPIN20"},{"label":"Try again","color":"#E2E8F0","weight":4,"prize":"No luck this time.","code":""},{"label":"5% off","color":"#F97316","weight":3,"prize":"You won 5% off!","code":"SPIN5"},{"label":"Free gift","color":"#10B981","weight":1,"prize":"A free gift \u2014 it is in the bag.","code":"GIFT"}]', 4792 "size": 260, 4793 "spinText": "Spin the wheel", 4794 "spinBg": "#DB2777", 4795 "spinColor": "#FFFFFF", 4796 "pointer": "#0F172A", 4797 "labelColor": "#0F172A", 4798 "labelDir": "radial", 4799 "once": "1", 4800 "sound": "", 4801 "celebrate": "confetti", 4802 "codeStyle": "coupon", 4803 "codeBg": "#FEF3C7", 4804 "codeColor": "#92400E", 4805 "copyLabel": "COPY", 4806 "copiedLabel": "COPIED!" 4807 }, 4808 "settings": [ 4809 { 4810 "key": "segments", 4811 "label": "Segments", 4812 "kind": "segment_list", 4813 "tab": "content", 4814 "hint": "Label, colour, odds and what winning it says." 4815 }, 4816 { 4817 "key": "spinText", 4818 "label": "Button label", 4819 "kind": "text", 4820 "tab": "content" 4821 }, 4822 { 4823 "key": "size", 4824 "label": "Wheel size", 4825 "kind": "number", 4826 "tab": "style", 4827 "unit": "px", 4828 "min": 140, 4829 "max": 520 4830 }, 4831 { 4832 "key": "spinBg", 4833 "label": "Button background", 4834 "kind": "color", 4835 "tab": "style" 4836 }, 4837 { 4838 "key": "spinColor", 4839 "label": "Button text", 4840 "kind": "color", 4841 "tab": "style" 4842 }, 4843 { 4844 "key": "pointer", 4845 "label": "Pointer colour", 4846 "kind": "color", 4847 "tab": "style" 4848 }, 4849 { 4850 "key": "labelColor", 4851 "label": "Segment text", 4852 "kind": "color", 4853 "tab": "style" 4854 }, 4855 { 4856 "key": "labelDir", 4857 "label": "Segment text direction", 4858 "kind": "select", 4859 "tab": "style", 4860 "hint": "Along the spoke, or upright so every label reads normally.", 4861 "options": [ 4862 [ 4863 "radial", 4864 "Along the segment" 4865 ], 4866 [ 4867 "flat", 4868 "Upright \u2014 always horizontal" 4869 ] 4870 ] 4871 }, 4872 { 4873 "key": "celebrate", 4874 "label": "When they win", 4875 "kind": "select", 4876 "tab": "content", 4877 "hint": "A little payoff when the wheel stops.", 4878 "options": [ 4879 [ 4880 "confetti", 4881 "Confetti" 4882 ], 4883 [ 4884 "glow", 4885 "Glow and pulse" 4886 ], 4887 [ 4888 "", 4889 "Nothing" 4890 ] 4891 ] 4892 }, 4893 { 4894 "key": "sound", 4895 "label": "Sound while it spins", 4896 "kind": "checkbox", 4897 "tab": "content", 4898 "hint": "Ticks as it passes each segment, then a chime. Off by default \u2014 it starts on their click, never on its own." 4899 }, 4900 { 4901 "key": "once", 4902 "label": "One spin per visitor", 4903 "kind": "checkbox", 4904 "tab": "content", 4905 "hint": "Their result is remembered and shown again instead of a second spin." 4906 }, 4907 { 4908 "key": "codeStyle", 4909 "label": "How the prize code appears", 4910 "kind": "select", 4911 "tab": "content", 4912 "hint": "A claimable coupon with a Copy button, or plain text for prizes that are not codes.", 4913 "options": [ 4914 [ 4915 "coupon", 4916 "Coupon box with a Copy button" 4917 ], 4918 [ 4919 "text", 4920 "Plain text" 4921 ] 4922 ] 4923 }, 4924 { 4925 "key": "copyLabel", 4926 "label": "Copy button label", 4927 "kind": "text", 4928 "tab": "content" 4929 }, 4930 { 4931 "key": "copiedLabel", 4932 "label": "Label after copying", 4933 "kind": "text", 4934 "tab": "content" 4935 }, 4936 { 4937 "key": "codeBg", 4938 "label": "Coupon background", 4939 "kind": "color", 4940 "tab": "style" 4941 }, 4942 { 4943 "key": "codeColor", 4944 "label": "Coupon text & border", 4945 "kind": "color", 4946 "tab": "style" 4947 }, 4948 { 4949 "key": "report", 4950 "label": "When clicked, report", 4951 "kind": "select", 4952 "tab": "content", 4953 "hint": "Beside the click itself, which is always counted.", 4954 "options": [ 4955 [ 4956 "", 4957 "Nothing extra" 4958 ], 4959 [ 4960 "goal", 4961 "This action reached its goal" 4962 ], 4963 [ 4964 "event", 4965 "A conversion goal" 4966 ], 4967 [ 4968 "both", 4969 "Both" 4970 ] 4971 ] 4972 }, 4973 { 4974 "key": "reportEvent", 4975 "label": "Which goal", 4976 "kind": "goal", 4977 "tab": "content", 4978 "hint": "Event-driven goals only \u2014 the kind that waits for a named event. Other goal types watch the page themselves and need nothing here." 4979 }, 4980 { 4981 "key": "reportValue", 4982 "label": "Value", 4983 "kind": "text", 4984 "tab": "content", 4985 "hint": "Optional \u2014 a revenue amount, a plan name. Variables work here." 4986 } 4987 ] 4988 }, 4989 { 4990 "id": "progress", 4991 "label": "Progress Bar", 4992 "html": '<div class="pz-block-pbar" data-pz-pbar="{"goal":100,"pre":"$","suf":""}" style="margin:14px 0;"><i class="pz-pbar-value" style="display:none"></i><i class="pz-pbar-tpl" style="display:none">You are {left} away from free shipping</i><i class="pz-pbar-tpl-done" style="display:none">You unlocked free shipping!</i><div class="pz-pbar-msg" style="font-size:13px; font-weight:600; color:#0F172A; margin-bottom:7px; text-align:center;">You are $100 away from free shipping</div><div style="background:#E2E8F0; border-radius:999px; overflow:hidden; height:10px;"><div class="pz-pbar-fill" style="width:0%; height:100%; background:#10B981; border-radius:999px; transition:width 0.6s ease;"></div></div></div>', 4993 "props": { 4994 "anim": "", 4995 "device": "", 4996 "cssMobile": "", 4997 "beside": "", 4998 "width": "", 4999 "boxAlign": "center", 5000 "spaceTop": "", 5001 "spaceBottom": "", 5002 "boxBg": "", 5003 "boxPad": "", 5004 "boxBorder": 0, 5005 "boxBorderColor": "#E2E8F0", 5006 "boxCorner": "", 5007 "boxShadow": "", 5008 "css": "", 5009 "value": "", 5010 "goal": 100, 5011 "prefix": "$", 5012 "suffix": "", 5013 "text": "You are {left} away from free shipping", 5014 "doneText": "You unlocked free shipping!", 5015 "fill": "#10B981", 5016 "track": "#E2E8F0", 5017 "color": "#0F172A", 5018 "height": 10, 5019 "radius": 999, 5020 "demoValue": 65 5021 }, 5022 "settings": [ 5023 { 5024 "key": "value", 5025 "label": "Value", 5026 "kind": "visitor_field", 5027 "tab": "content", 5028 "hint": "The profile field holding the amount so far \u2014 a cart total, points, steps." 5029 }, 5030 { 5031 "key": "goal", 5032 "label": "Goal", 5033 "kind": "number", 5034 "tab": "content", 5035 "min": 1, 5036 "max": 1e6 5037 }, 5038 { 5039 "key": "text", 5040 "label": "Message", 5041 "kind": "text", 5042 "tab": "content", 5043 "hint": "{left}, {value} and {goal} fill in live." 5044 }, 5045 { 5046 "key": "doneText", 5047 "label": "Message once reached", 5048 "kind": "text", 5049 "tab": "content" 5050 }, 5051 { 5052 "key": "prefix", 5053 "label": "Before the number", 5054 "kind": "text", 5055 "tab": "content", 5056 "hint": "A currency symbol, usually." 5057 }, 5058 { 5059 "key": "suffix", 5060 "label": "After the number", 5061 "kind": "text", 5062 "tab": "content", 5063 "hint": "Points, items, km \u2014 whatever it counts." 5064 }, 5065 { 5066 "key": "demoValue", 5067 "label": "Preview value", 5068 "kind": "number", 5069 "tab": "content", 5070 "hint": "Editor only \u2014 what the bar shows here, where no visitor exists.", 5071 "min": 0, 5072 "max": 1e6 5073 }, 5074 { 5075 "key": "fill", 5076 "label": "Bar colour", 5077 "kind": "color", 5078 "tab": "style" 5079 }, 5080 { 5081 "key": "track", 5082 "label": "Track colour", 5083 "kind": "color", 5084 "tab": "style" 5085 }, 5086 { 5087 "key": "color", 5088 "label": "Text colour", 5089 "kind": "color", 5090 "tab": "style" 5091 }, 5092 { 5093 "key": "height", 5094 "label": "Bar thickness", 5095 "kind": "number", 5096 "tab": "style", 5097 "unit": "px", 5098 "min": 3, 5099 "max": 60 5100 }, 5101 { 5102 "key": "radius", 5103 "label": "Corner radius", 5104 "kind": "number", 5105 "tab": "style", 5106 "unit": "px", 5107 "min": 0, 5108 "max": 999 5109 } 5110 ] 5111 }, 5112 { 5113 "id": "stock", 5114 "label": "Stock Urgency", 5115 "html": '<div class="pz-block-stock" data-pz-stock="{"max":10}" style="text-align:center; margin:12px 0;"><span style="display:inline-block; background:#FEE2E2; color:#B91C1C; border-radius:999px; padding:6px 14px; font-size:13px; font-weight:700;">Only <b class="pz-stock-n">3</b> left in stock</span></div>', 5116 "props": { 5117 "anim": "", 5118 "device": "", 5119 "cssMobile": "", 5120 "beside": "", 5121 "width": "", 5122 "boxAlign": "center", 5123 "spaceTop": "", 5124 "spaceBottom": "", 5125 "boxBg": "", 5126 "boxPad": "", 5127 "boxBorder": 0, 5128 "boxBorderColor": "#E2E8F0", 5129 "boxCorner": "", 5130 "boxShadow": "", 5131 "css": "", 5132 "value": "", 5133 "fixed": 3, 5134 "max": 10, 5135 "text": "Only {count} left in stock", 5136 "color": "#B91C1C", 5137 "bg": "#FEE2E2", 5138 "fontSize": 13, 5139 "align": "center", 5140 "demoValue": 3
5141 }, 5142 "settings": [ 5143 { 5144 "key": "value", 5145 "label": "Where the number comes from", 5146 "kind": "visitor_field", 5147 "tab": "content", 5148 "hint": "The field holding what is left. Leave it blank to type a number instead." 5149 }, 5150 { 5151 "key": "fixed", 5152 "label": "Number", 5153 "kind": "number", 5154 "tab": "content", 5155 "hint": "Used when no field is chosen.", 5156 "min": 0, 5157 "max": 1e6 5158 }, 5159 { 5160 "key": "text", 5161 "label": "Message", 5162 "kind": "text", 5163 "tab": "content", 5164 "hint": "{count} fills in live." 5165 }, 5166 { 5167 "key": "max", 5168 "label": "Only show at or below", 5169 "kind": "number", 5170 "tab": "content", 5171 "hint": "Above this the badge hides itself \u2014 scarcity that is not scarce reads as a gimmick.", 5172 "min": 1, 5173 "max": 1e6 5174 }, 5175 { 5176 "key": "demoValue", 5177 "label": "Preview number", 5178 "kind": "number", 5179 "tab": "content", 5180 "hint": "Editor only \u2014 what this shows here, where no visitor exists.", 5181 "min": 0, 5182 "max": 1e6 5183 }, 5184 { 5185 "key": "bg", 5186 "label": "Background", 5187 "kind": "color", 5188 "tab": "style" 5189 }, 5190 { 5191 "key": "color", 5192 "label": "Text colour", 5193 "kind": "color", 5194 "tab": "style" 5195 }, 5196 { 5197 "key": "fontSize", 5198 "label": "Font size", 5199 "kind": "number", 5200 "tab": "style", 5201 "unit": "px", 5202 "min": 9, 5203 "max": 40 5204 }, 5205 { 5206 "key": "align", 5207 "label": "Alignment", 5208 "kind": "align", 5209 "tab": "style" 5210 } 5211 ] 5212 }, 5213 { 5214 "id": "button", 5215 "label": "CTA Button", 5216 "html": '<div style="text-align:center;"><a href="#" data-personyze-click-target="" style="background:#DB2777;color:#fff;border:0; display:inline-block; text-decoration:none;padding:12px 28px;border-radius:8px;font-size:14px;font-weight:700;cursor:pointer; margin:10px 0;">Call to Action</a></div>', 5217 "props": { 5218 "anim": "", 5219 "device": "", 5220 "cssMobile": "", 5221 "beside": "", 5222 "width": "", 5223 "boxAlign": "center", 5224 "spaceTop": "", 5225 "spaceBottom": "", 5226 "boxBg": "", 5227 "boxPad": "", 5228 "boxBorder": 0, 5229 "boxBorderColor": "#E2E8F0", 5230 "boxCorner": "", 5231 "boxShadow": "", 5232 "hoverBg": "", 5233 "hoverText": "", 5234 "report": "", 5235 "reportEvent": "", 5236 "reportValue": "", 5237 "animLoop": "", 5238 "css": "", 5239 "text": "Call to Action", 5240 "link": 'href="#" data-personyze-click-target="1"', 5241 "bg": "#DB2777", 5242 "color": "#fff", 5243 "radius": 8, 5244 "fontSize": 14, 5245 "weight": 700, 5246 "padX": 28, 5247 "padY": 12, 5248 "align": "center" 5249 }, 5250 "settings": [ 5251 { 5252 "key": "text", 5253 "label": "Label", 5254 "kind": "text", 5255 "tab": "content" 5256 }, 5257 { 5258 "key": "link", 5259 "label": "Link", 5260 "kind": "link", 5261 "tab": "content" 5262 }, 5263 { 5264 "key": "bg", 5265 "label": "Background", 5266 "kind": "color", 5267 "tab": "style" 5268 }, 5269 { 5270 "key": "color", 5271 "label": "Text color", 5272 "kind": "color", 5273 "tab": "style" 5274 }, 5275 { 5276 "key": "radius", 5277 "label": "Corner radius", 5278 "kind": "number", 5279 "tab": "style", 5280 "unit": "px", 5281 "min": 0, 5282 "max": 999 5283 }, 5284 { 5285 "key": "fontSize", 5286 "label": "Font size", 5287 "kind": "number", 5288 "tab": "style", 5289 "unit": "px", 5290 "min": 8, 5291 "max": 48 5292 }, 5293 { 5294 "key": "weight", 5295 "label": "Font weight", 5296 "kind": "number", 5297 "tab": "style", 5298 "min": 100, 5299 "max": 900 5300 }, 5301 { 5302 "key": "padX", 5303 "label": "Padding, horizontal", 5304 "kind": "number", 5305 "tab": "style", 5306 "unit": "px", 5307 "min": 0, 5308 "max": 200 5309 }, 5310 { 5311 "key": "padY", 5312 "label": "Padding, vertical", 5313 "kind": "number", 5314 "tab": "style", 5315 "unit": "px", 5316 "min": 0, 5317 "max": 200 5318 }, 5319 { 5320 "key": "align", 5321 "label": "Alignment", 5322 "kind": "align", 5323 "tab": "style" 5324 }, 5325 { 5326 "key": "hoverBg",
5327 "label": "Background on hover", 5328 "kind": "color", 5329 "tab": "style", 5330 "hint": "What it changes to under the mouse. Blank keeps it still." 5331 }, 5332 { 5333 "key": "hoverText", 5334 "label": "Text on hover", 5335 "kind": "color", 5336 "tab": "style" 5337 }, 5338 { 5339 "key": "report", 5340 "label": "When clicked, report", 5341 "kind": "select", 5342 "tab": "content", 5343 "hint": "Beside the click itself, which is always counted.", 5344 "options": [ 5345 [ 5346 "", 5347 "Nothing extra" 5348 ], 5349 [ 5350 "goal", 5351 "This action reached its goal" 5352 ], 5353 [ 5354 "event", 5355 "A conversion goal" 5356 ], 5357 [ 5358 "both", 5359 "Both" 5360 ] 5361 ] 5362 }, 5363 { 5364 "key": "reportEvent", 5365 "label": "Which goal", 5366 "kind": "goal", 5367 "tab": "content", 5368 "hint": "Event-driven goals only \u2014 the kind that waits for a named event. Other goal types watch the page themselves and need nothing here." 5369 }, 5370 { 5371 "key": "reportValue", 5372 "label": "Value", 5373 "kind": "text", 5374 "tab": "content", 5375 "hint": "Optional \u2014 a revenue amount, a plan name. Variables work here." 5376 } 5377 ] 5378 }, 5379 { 5380 "id": "headline", 5381 "label": "Headline & Subtitle", 5382 "html": '<h1 style="text-align:center;font-size:30px;font-weight:800;color:#0F172A;line-height:1.2; margin:14px 0 8px;">Your Main Headline Goes Here</h1><p style="text-align:center; font-size:14px;color:#64748B; margin:0 0 18px;">Add a short description or supporting text that highlights your value.</p>', 5383 "props": { 5384 "anim": "", 5385 "device": "", 5386 "cssMobile": "", 5387 "beside": "", 5388 "width": "", 5389 "boxAlign": "center", 5390 "spaceTop": "", 5391 "spaceBottom": "", 5392 "boxBg": "", 5393 "boxPad": "", 5394 "boxBorder": 0, 5395 "boxBorderColor": "#E2E8F0", 5396 "boxCorner": "", 5397 "boxShadow": "", 5398 "css": "", 5399 "headline": "Your Main Headline Goes Here", 5400 "sub": "Add a short description or supporting text that highlights your value.", 5401 "color": "#0F172A", 5402 "subColor": "#64748B", 5403 "fontSize": 30, 5404 "subSize": 14, 5405 "weight": 800, 5406 "align": "center" 5407 }, 5408 "settings": [ 5409 { 5410 "key": "headline", 5411 "label": "Headline", 5412 "kind": "text", 5413 "tab": "content" 5414 }, 5415 { 5416 "key": "sub", 5417 "label": "Subtitle", 5418 "kind": "textarea", 5419 "tab": "content" 5420 }, 5421 { 5422 "key": "color", 5423 "label": "Headline color", 5424 "kind": "color", 5425 "tab": "style" 5426 }, 5427 { 5428 "key": "subColor", 5429 "label": "Subtitle color", 5430 "kind": "color", 5431 "tab": "style" 5432 }, 5433 { 5434 "key": "fontSize", 5435 "label": "Headline size", 5436 "kind": "number", 5437 "tab": "style", 5438 "unit": "px", 5439 "min": 10, 5440 "max": 96 5441 }, 5442 { 5443 "key": "subSize", 5444 "label": "Subtitle size", 5445 "kind": "number", 5446 "tab": "style", 5447 "unit": "px", 5448 "min": 10, 5449 "max": 40 5450 }, 5451 { 5452 "key": "weight", 5453 "label": "Font weight", 5454 "kind": "number", 5455 "tab": "style", 5456 "min": 100, 5457 "max": 900 5458 }, 5459 { 5460 "key": "align", 5461 "label": "Alignment", 5462 "kind": "align", 5463 "tab": "style" 5464 } 5465 ] 5466 }, 5467 { 5468 "id": "paragraph", 5469 "label": "Paragraph", 5470 "html": '<p style="text-align:left;font-size:14px;color:#334155; margin:8px 0;">A new paragraph of text. Click to edit.</p>', 5471 "props": { 5472 "anim": "", 5473 "device": "", 5474 "cssMobile": "", 5475 "beside": "", 5476 "width": "", 5477 "boxAlign": "center", 5478 "spaceTop": "", 5479 "spaceBottom": "", 5480 "boxBg": "", 5481 "boxPad": "", 5482 "boxBorder": 0, 5483 "boxBorderColor": "#E2E8F0", 5484 "boxCorner": "", 5485 "boxShadow": "", 5486 "css": "", 5487 "text": "A new paragraph of text. Click to edit.", 5488 "color": "#334155", 5489 "fontSize": 14, 5490 "align": "left" 5491 }, 5492 "settings": [ 5493 { 5494 "key": "text", 5495 "label": "Text", 5496 "kind": "textarea", 5497 "tab": "content" 5498 }, 5499 { 5500 "key": "color", 5501 "label": "Color", 5502 "kind": "color", 5503 "tab": "style" 5504 }, 5505 { 5506 "key": "fontSize", 5507 "label": "Font size", 5508 "kind": "number", 5509 "tab": "style", 5510 "unit": "px", 5511 "min": 8, 5512 "max": 72 5513 }, 5514 { 5515 "key": "align", 5516 "label": "Alignment", 5517 "kind": "align", 5518 "tab": "style" 5519 } 5520 ] 5521 }, 5522 { 5523 "id": "chip", 5524 "label": "Chip / Pill", 5525 "html": '<div style="text-align:center"><span style="color:#047857;background:#ECFDF5;border:1px solid #A7F3D0;border-radius:999px;font-size:13px;font-weight:600;padding:6px 14px; display:inline-block;">\u2605 Special Offer</span></div>', 5526 "props": { 5527 "anim": "", 5528 "device": "", 5529 "cssMobile": "", 5530 "beside": "", 5531 "width": "", 5532 "boxAlign": "center", 5533 "spaceTop": "", 5534 "spaceBottom": "", 5535 "boxBg": "", 5536 "boxPad": "", 5537 "boxBorder": 0, 5538 "boxBorderColor": "#E2E8F0", 5539 "boxCorner": "", 5540 "boxShadow": "", 5541 "hoverBg": "", 5542 "hoverText": "", 5543 "report": "", 5544 "reportEvent": "", 5545 "reportValue": "", 5546 "animLoop": "", 5547 "css": "", 5548 "text": "\u2605 Special Offer", 5549 "color": "#047857", 5550 "bg": "#ECFDF5", 5551 "border": "#A7F3D0", 5552 "radius": 999, 5553 "fontSize": 13, 5554 "weight": 600, 5555 "align": "center" 5556 }, 5557 "settings": [ 5558 { 5559 "key": "text", 5560 "label": "Text", 5561 "kind": "text", 5562 "tab": "content" 5563 }, 5564 { 5565 "key": "color", 5566 "label": "Text color", 5567 "kind": "color", 5568 "tab": "style" 5569 }, 5570 { 5571 "key": "bg",
5572 "label": "Background", 5573 "kind": "color", 5574 "tab": "style" 5575 }, 5576 { 5577 "key": "border", 5578 "label": "Border color", 5579 "kind": "color", 5580 "tab": "style" 5581 }, 5582 { 5583 "key": "radius", 5584 "label": "Corner radius", 5585 "kind": "number", 5586 "tab": "style", 5587 "unit": "px", 5588 "min": 0, 5589 "max": 999 5590 }, 5591 { 5592 "key": "fontSize", 5593 "label": "Font size", 5594 "kind": "number", 5595 "tab": "style", 5596 "unit": "px", 5597 "min": 8, 5598 "max": 72 5599 }, 5600 { 5601 "key": "weight", 5602 "label": "Font weight", 5603 "kind": "number", 5604 "tab": "style", 5605 "min": 100, 5606 "max": 900 5607 }, 5608 { 5609 "key": "align", 5610 "label": "Alignment", 5611 "kind": "align", 5612 "tab": "style" 5613 }, 5614 { 5615 "key": "hoverBg", 5616 "label": "Background on hover", 5617 "kind": "color", 5618 "tab": "style", 5619 "hint": "What it changes to under the mouse. Blank keeps it still." 5620 }, 5621 { 5622 "key": "hoverText", 5623 "label": "Text on hover", 5624 "kind": "color", 5625 "tab": "style" 5626 }, 5627 { 5628 "key": "report", 5629 "label": "When clicked, report", 5630 "kind": "select", 5631 "tab": "content", 5632 "hint": "Beside the click itself, which is always counted.", 5633 "options": [ 5634 [ 5635 "", 5636 "Nothing extra" 5637 ], 5638 [ 5639 "goal", 5640 "This action reached its goal" 5641 ], 5642 [ 5643 "event", 5644 "A conversion goal" 5645 ], 5646 [ 5647 "both", 5648 "Both" 5649 ] 5650 ] 5651 }, 5652 { 5653 "key": "reportEvent", 5654 "label": "Which goal", 5655 "kind": "goal", 5656 "tab": "content", 5657 "hint": "Event-driven goals only \u2014 the kind that waits for a named event. Other goal types watch the page themselves and need nothing here." 5658 }, 5659 { 5660 "key": "reportValue", 5661 "label": "Value", 5662 "kind": "text", 5663 "tab": "content", 5664 "hint": "Optional \u2014 a revenue amount, a plan name. Variables work here." 5665 } 5666 ] 5667 }, 5668 { 5669 "id": "checklist", 5670 "label": "Checklist", 5671 "html": '<div style="background:#F8FAFC; border-radius:12px; padding:16px 18px; margin:18px 0;"><h4 style="margin:0 0 10px; font-size:14px; font-weight:700; color:#0F172A;">This section can highlight benefits:</h4><ul style="list-style:none; padding:0; margin:0;"><li style="display:flex; align-items:center; gap:10px; font-size:13px; color:#334155; margin-bottom:6px;"><span style="width:12px; height:12px; border-radius:50%; background:#10B981; flex:none; display:inline-block;"></span><span style="flex:1;">Highlight key feature or benefit #1</span></li><li style="display:flex; align-items:center; gap:10px; font-size:13px; color:#334155; margin-bottom:6px;"><span style="width:12px; height:12px; border-radius:50%; background:#10B981; flex:none; display:inline-block;"></span><span style="flex:1;">Highlight key feature or benefit #2</span></li><li style="display:flex; align-items:center; gap:10px; font-size:13px; color:#334155; margin-bottom:6px;"><span style="width:12px; height:12px; border-radius:50%; background:#10B981; flex:none; display:inline-block;"></span><span style="flex:1;">Highlight key feature or benefit #3</span></li></ul></div>', 5672 "props": { 5673 "anim": "", 5674 "device": "", 5675 "cssMobile": "", 5676 "beside": "", 5677 "width": "", 5678 "boxAlign": "center", 5679 "spaceTop": "", 5680 "spaceBottom": "", 5681 "boxBg": "", 5682 "boxPad": "", 5683 "boxBorder": 0, 5684 "boxBorderColor": "#E2E8F0", 5685 "boxCorner": "", 5686 "boxShadow": "", 5687 "css": "", 5688 "title": "This section can highlight benefits:", 5689 "items": [ 5690 "Highlight key feature or benefit #1", 5691 "Highlight key feature or benefit #2", 5692 "Highlight key feature or benefit #3" 5693 ], 5694 "dotColor": "#10B981", 5695 "bg": "#F8FAFC" 5696 }, 5697 "settings": [ 5698 { 5699 "key": "title", 5700 "label": "Title", 5701 "kind": "text", 5702 "tab": "content" 5703 }, 5704 { 5705 "key": "items", 5706 "label": "Items", 5707 "kind": "list", 5708 "tab": "content" 5709 }, 5710 { 5711 "key": "dotColor", 5712 "label": "Bullet color", 5713 "kind": "color", 5714 "tab": "style" 5715 }, 5716 { 5717 "key": "bg",
5718 "label": "Background", 5719 "kind": "color", 5720 "tab": "style" 5721 } 5722 ] 5723 }, 5724 { 5725 "id": "divider", 5726 "label": "Divider", 5727 "html": '<hr style="border:0; border-top:1px solid #E2E8F0; margin:16px 0;">', 5728 "props": { 5729 "anim": "", 5730 "device": "", 5731 "cssMobile": "", 5732 "beside": "", 5733 "width": "", 5734 "boxAlign": "center", 5735 "spaceTop": "", 5736 "spaceBottom": "", 5737 "boxBg": "", 5738 "boxPad": "", 5739 "boxBorder": 0, 5740 "boxBorderColor": "#E2E8F0", 5741 "boxCorner": "", 5742 "boxShadow": "", 5743 "css": "", 5744 "color": "#E2E8F0", 5745 "thickness": 1, 5746 "marginY": 16 5747 }, 5748 "settings": [ 5749 { 5750 "key": "color", 5751 "label": "Color", 5752 "kind": "color", 5753 "tab": "style" 5754 }, 5755 { 5756 "key": "thickness", 5757 "label": "Thickness", 5758 "kind": "number", 5759 "tab": "style", 5760 "unit": "px", 5761 "min": 1, 5762 "max": 20 5763 }, 5764 { 5765 "key": "marginY", 5766 "label": "Vertical spacing", 5767 "kind": "number", 5768 "tab": "style", 5769 "unit": "px", 5770 "min": 0, 5771 "max": 200 5772 } 5773 ] 5774 }, 5775 { 5776 "id": "icon", 5777 "label": "Icon", 5778 "html": '<div style="text-align:center; margin:10px 0;"><i class="$icon star" style="font-size:42px; color:#0F172A;"></i></div>', 5779 "props": { 5780 "anim": "", 5781 "device": "", 5782 "cssMobile": "", 5783 "beside": "", 5784 "width": "", 5785 "boxAlign": "center", 5786 "spaceTop": "", 5787 "spaceBottom": "", 5788 "boxBg": "", 5789 "boxPad": "", 5790 "boxBorder": 0, 5791 "boxBorderColor": "#E2E8F0", 5792 "boxCorner": "", 5793 "boxShadow": "", 5794 "css": "", 5795 "name": "star", 5796 "size": 42, 5797 "color": "#0F172A", 5798 "align": "center" 5799 }, 5800 "settings": [ 5801 { 5802 "key": "name", 5803 "label": "Icon", 5804 "kind": "icon", 5805 "tab": "content" 5806 }, 5807 { 5808 "key": "size", 5809 "label": "Size", 5810 "kind": "number", 5811 "tab": "style", 5812 "unit": "px", 5813 "min": 8, 5814 "max": 200 5815 }, 5816 { 5817 "key": "color", 5818 "label": "Color", 5819 "kind": "color", 5820 "tab": "style" 5821 }, 5822 { 5823 "key": "align", 5824 "label": "Alignment", 5825 "kind": "align", 5826 "tab": "style" 5827 } 5828 ] 5829 }, 5830 { 5831 "id": "rotatingtext", 5832 "label": "Rotating Text", 5833 "html": '<div class="pz-rotw" style="display:grid; text-align:center; font-size:14px; font-weight:600; color:#111827; margin:8px 0;"><style>@keyframes pz-rotf-3{0%{opacity:1}28.33%{opacity:1}33.33%{opacity:0}
583395%{opacity:0}100%{opacity:1}}@media (prefers-reduced-motion:reduce){.pz-rotw>div{animation:none !important}}</style><div style="grid-area:1/1; animation:pz-rotf-3 12s linear infinite; animation-delay:-12s;">Free shipping on orders over $50</div><div style="grid-area:1/1; opacity:0; animation:pz-rotf-3 12s linear infinite; animation-delay:-8s;">Extra 10% off today \u2014 code SAVE10</div><div style="grid-area:1/1; opacity:0; animation:pz-rotf-3 12s linear infinite; animation-delay:-4s;">New arrivals just dropped</div></div>', 5834 "props": { 5835 "anim": "", 5836 "device": "", 5837 "cssMobile": "", 5838 "beside": "", 5839 "width": "", 5840 "boxAlign": "center", 5841 "spaceTop": "", 5842 "spaceBottom": "", 5843 "boxBg": "", 5844 "boxPad": "", 5845 "boxBorder": 0, 5846 "boxBorderColor": "#E2E8F0", 5847 "boxCorner": "", 5848 "boxShadow": "", 5849 "css": "", 5850 "lines": [ 5851 "Free shipping on orders over $50", 5852 "Extra 10% off today \u2014 code SAVE10", 5853 "New arrivals just dropped" 5854 ], 5855 "secs": 4, 5856 "mode": "fade", 5857 "fontSize": 14, 5858 "color": "#111827", 5859 "align": "center" 5860 }, 5861 "settings": [ 5862 { 5863 "key": "lines", 5864 "label": "Messages", 5865 "kind": "list", 5866 "tab": "content", 5867 "hint": "One shows at a time, in order \u2014 variables work too." 5868 }, 5869 { 5870 "key": "secs", 5871 "label": "Seconds per message", 5872 "kind": "number", 5873 "tab": "style", 5874 "unit": "s", 5875 "min": 1, 5876 "max": 120 5877 }, 5878 { 5879 "key": "mode", 5880 "label": "Transition", 5881 "kind": "select", 5882 "tab": "style", 5883 "options": [ 5884 [ 5885 "fade", 5886 "Cross-fade" 5887 ], 5888 [ 5889 "slide", 5890 "Slide up" 5891 ] 5892 ] 5893 }, 5894 { 5895 "key": "fontSize", 5896 "label": "Font size", 5897 "kind": "number", 5898 "tab": "style", 5899 "unit": "px", 5900 "min": 8, 5901 "max": 60 5902 }, 5903 { 5904 "key": "color", 5905 "label": "Text color", 5906 "kind": "color", 5907 "tab": "style" 5908 }, 5909 { 5910 "key": "align", 5911 "label": "Alignment", 5912 "kind": "align", 5913 "tab": "style" 5914 } 5915 ] 5916 }, 5917 { 5918 "id": "faq", 5919 "label": "FAQ / Accordion", 5920 "html": `<style>.pz-faq-i>summary::-webkit-details-marker{display:none}.pz-faq-i>summary::marker{content:''}.pz-faq-i[open]>summary .pz-faq-mark{visibility:hidden}.pz-faq-i[open]>summary .pz-faq-mark::after{content:'\u2013'; visibility:visible}</style><div class="pz-block-faq" style="background:#FFFFFF; margin:14px 0; text-align:left;"><details class="pz-faq-i" style="border-bottom:1px solid #E2E8F0;"><summary style="list-style:none; cursor:pointer; display:flex; align-items:center; gap:10px; padding:12px 2px; font-size:14px; font-weight:700; color:#0F172A;"><span style="flex:1;">How long does delivery take?</span><span class="pz-faq-mark" style="flex:none; font-size:13px; line-height:1;">+</span></summary><div style="padding:0 2px 14px; font-size:13px; line-height:1.55; color:#475569;">Two to four working days, and you get a tracking link the moment it ships.</div></details><details class="pz-faq-i" style="border-bottom:1px solid #E2E8F0;"><summary style="list-style:none; cursor:pointer; display:flex; align-items:center; gap:10px; padding:12px 2px; font-size:14px; font-weight:700; color:#0F172A;"><span style="flex:1;">Can I return it?</span><span class="pz-faq-mark" style="flex:none; font-size:13px; line-height:1;">+</span></summary><div style="padding:0 2px 14px; font-size:13px; line-height:1.55; color:#475569;">Yes \u2014 thirty days, no questions, return shipping on us.</div></details></div>`, 5921 "props": { 5922 "anim": "", 5923 "device": "", 5924 "cssMobile": "", 5925 "beside": "", 5926 "width": "", 5927 "boxAlign": "center", 5928 "spaceTop": "", 5929 "spaceBottom": "", 5930 "boxBg": "", 5931 "boxPad": "", 5932 "boxBorder": 0, 5933 "boxBorderColor": "#E2E8F0", 5934 "boxCorner": "", 5935 "boxShadow": "", 5936 "css": "", 5937 "items": '[{"q":"How long does delivery take?","a":"Two to four working days, and you get a tracking link the moment it ships."},{"q":"Can I return it?","a":"Yes \u2014 thirty days, no questions, return shipping on us."}]', 5938 "openFirst": "", 5939 "bg": "#FFFFFF", 5940 "color": "#0F172A", 5941 "answerColor": "#475569", 5942 "border": "#E2E8F0", 5943 "fontSize": 14 5944 }, 5945 "settings": [ 5946 { 5947 "key": "items", 5948 "label": "Questions", 5949 "kind": "faq_list", 5950 "tab": "content", 5951 "hint": "Each one opens on its own; the rest stay closed." 5952 }, 5953 { 5954 "key": "openFirst", 5955 "label": "Start with the first one open", 5956 "kind": "checkbox", 5957 "tab": "content" 5958 }, 5959 { 5960 "key": "bg",
5961 "label": "Background", 5962 "kind": "color", 5963 "tab": "style" 5964 }, 5965 { 5966 "key": "color", 5967 "label": "Question colour", 5968 "kind": "color", 5969 "tab": "style" 5970 }, 5971 { 5972 "key": "answerColor", 5973 "label": "Answer colour", 5974 "kind": "color", 5975 "tab": "style" 5976 }, 5977 { 5978 "key": "border", 5979 "label": "Divider colour", 5980 "kind": "color", 5981 "tab": "style" 5982 }, 5983 { 5984 "key": "fontSize", 5985 "label": "Question size", 5986 "kind": "number", 5987 "tab": "style", 5988 "unit": "px", 5989 "min": 9, 5990 "max": 32 5991 } 5992 ] 5993 }, 5994 { 5995 "id": "rating", 5996 "label": "Star Rating", 5997 "html": '<div class="pz-block-rating" style="text-align:center; margin:12px 0;"><span style="display:inline-flex; align-items:center; gap:8px;"><span style="position:relative; display:inline-block; font-size:20px; line-height:1; letter-spacing:2px; white-space:nowrap;"><span style="color:#E2E8F0;">\u2605\u2605\u2605\u2605\u2605</span><span style="position:absolute; left:0; top:0; width:90%; overflow:hidden; color:#B45309;">\u2605\u2605\u2605\u2605\u2605</span></span><b style="font-size:14px; color:#0F172A;">4.5</b><span style="font-size:12px; color:#64748B;">from 214 reviews</span></span></div>', 5998 "props": { 5999 "anim": "", 6000 "device": "", 6001 "cssMobile": "", 6002 "beside": "", 6003 "width": "", 6004 "boxAlign": "center", 6005 "spaceTop": "", 6006 "spaceBottom": "", 6007 "boxBg": "", 6008 "boxPad": "", 6009 "boxBorder": 0, 6010 "boxBorderColor": "#E2E8F0", 6011 "boxCorner": "", 6012 "boxShadow": "", 6013 "css": "", 6014 "source": "fixed", 6015 "value": 4.5, 6016 "outOf": 5, 6017 "note": "from 214 reviews", 6018 "color": "#B45309", 6019 "empty": "#E2E8F0", 6020 "size": 20, 6021 "align": "center", 6022 "showNumber": "1" 6023 }, 6024 "settings": [ 6025 { 6026 "key": "source", 6027 "label": "Where the score comes from", 6028 "kind": "select", 6029 "tab": "content", 6030 "hint": "A number you type, or the rating the catalog holds for the product this visitor last looked at.", 6031 "options": [ 6032 [ 6033 "fixed", 6034 "A number I set" 6035 ], 6036 [ 6037 "catalog", 6038 "The visitor\u2019s last-viewed product" 6039 ] 6040 ] 6041 }, 6042 { 6043 "key": "value", 6044 "label": "Score", 6045 "kind": "number", 6046 "tab": "content", 6047 "min": 0, 6048 "max": 5 6049 }, 6050 { 6051 "key": "outOf", 6052 "label": "Out of", 6053 "kind": "number", 6054 "tab": "content", 6055 "min": 3, 6056 "max": 10 6057 }, 6058 { 6059 "key": "note", 6060 "label": "Text beside the stars", 6061 "kind": "text", 6062 "tab": "content", 6063 "hint": "\u201Cfrom 214 reviews\u201D. Blank for stars alone." 6064 }, 6065 { 6066 "key": "showNumber", 6067 "label": "Show the score as a number too", 6068 "kind": "checkbox", 6069 "tab": "content" 6070 }, 6071 { 6072 "key": "color", 6073 "label": "Star colour", 6074 "kind": "color", 6075 "tab": "style" 6076 }, 6077 { 6078 "key": "empty", 6079 "label": "Empty star colour", 6080 "kind": "color", 6081 "tab": "style" 6082 }, 6083 { 6084 "key": "size", 6085 "label": "Star size", 6086 "kind": "number", 6087 "tab": "style", 6088 "unit": "px", 6089 "min": 8, 6090 "max": 60 6091 }, 6092 { 6093 "key": "align", 6094 "label": "Alignment", 6095 "kind": "align", 6096 "tab": "style" 6097 } 6098 ] 6099 }, 6100 { 6101 "id": "steps", 6102 "label": "Numbered Steps", 6103 "html": '<div style="display:flex; flex-wrap:wrap; gap:14px; margin:16px 0;"><div style="flex:1 1 0; min-width:0; display:flex; flex-direction:column; align-items:center; text-align:center; gap:8px;"><span style="flex:none; width:26px; height:26px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:#0F172A; color:#FFFFFF; font-size:12px; font-weight:800;">1</span><span style="font-size:13px; line-height:1.45; color:#334155;">Sign up in thirty seconds</span></div><div style="flex:1 1 0; min-width:0; display:flex; flex-direction:column; align-items:center; text-align:center; gap:8px;"><span style="flex:none; width:26px; height:26px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:#0F172A; color:#FFFFFF; font-size:12px; font-weight:800;">2</span><span style="font-size:13px; line-height:1.45; color:#334155;">Tell us what you like</span></div><div style="flex:1 1 0; min-width:0; display:flex; flex-direction:column; align-items:center; text-align:center; gap:8px;"><span style="flex:none; width:26px; height:26px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:#0F172A; color:#FFFFFF; font-size:12px; font-weight:800;">3</span><span style="font-size:13px; line-height:1.45; color:#334155;">Get picks that fit</span></div></div>', 6104 "props": { 6105 "anim": "", 6106 "device": "", 6107 "cssMobile": "", 6108 "beside": "", 6109 "width": "", 6110 "boxAlign": "center", 6111 "spaceTop": "", 6112 "spaceBottom": "", 6113 "boxBg": "", 6114 "boxPad": "", 6115 "boxBorder": 0, 6116 "boxBorderColor": "#E2E8F0", 6117 "boxCorner": "", 6118 "boxShadow": "", 6119 "css": "", 6120 "items": [ 6121 "Sign up in thirty seconds", 6122 "Tell us what you like", 6123 "Get picks that fit" 6124 ], 6125 "layout": "row", 6126 "numberBg": "#0F172A", 6127 "numberColor": "#FFFFFF", 6128 "color": "#334155", 6129 "fontSize": 13 6130 }, 6131 "settings": [ 6132 { 6133 "key": "items", 6134 "label": "Steps", 6135 "kind": "list", 6136 "tab": "content" 6137 }, 6138 { 6139 "key": "layout", 6140 "label": "Layout", 6141 "kind": "select", 6142 "tab": "style", 6143 "hint": "Across for three short steps, down for longer ones.", 6144 "options": [ 6145 [ 6146 "row", 6147 "Across" 6148 ], 6149 [ 6150 "col", 6151 "Down" 6152 ] 6153 ] 6154 }, 6155 { 6156 "key": "numberBg", 6157 "label": "Number background", 6158 "kind": "color", 6159 "tab": "style" 6160 }, 6161 { 6162 "key": "numberColor", 6163 "label": "Number colour", 6164 "kind": "color", 6165 "tab": "style" 6166 }, 6167 { 6168 "key": "color", 6169 "label": "Text colour", 6170 "kind": "color", 6171 "tab": "style" 6172 }, 6173 { 6174 "key": "fontSize", 6175 "label": "Font size", 6176 "kind": "number", 6177 "tab": "style", 6178 "unit": "px", 6179 "min": 9, 6180 "max": 32 6181 } 6182 ] 6183 }, 6184 { 6185 "id": "compare", 6186 "label": "Comparison Table", 6187 "html": '<table style="width:100%; border-collapse:collapse; margin:16px 0; text-align:left;"><tr><th style="padding:9px 10px; text-align:left; font-size:13px;"></th><th style="padding:9px 10px; text-align:center; font-size:13px; font-weight:800; background:#0F172A; color:#FFFFFF;">Us</th><th style="padding:9px 10px; text-align:center; font-size:13px; font-weight:800; background:#0F172A; color:#FFFFFF;">Them</th></tr><tr><td style="padding:9px 10px; border-top:1px solid #E2E8F0; font-size:13px; color:#334155;">Free delivery</td><td style="padding:9px 10px; border-top:1px solid #E2E8F0; text-align:center; font-size:13px; color:#334155;"><span style="color:#047857; font-weight:800;">✓</span></td><td style="padding:9px 10px; border-top:1px solid #E2E8F0; text-align:center; font-size:13px; color:#334155;"><span style="color:#CBD5E1; font-weight:800;">✕
6187</span></td></tr><tr><td style="padding:9px 10px; border-top:1px solid #E2E8F0; font-size:13px; color:#334155;">Thirty-day returns</td><td style="padding:9px 10px; border-top:1px solid #E2E8F0; text-align:center; font-size:13px; color:#334155;"><span style="color:#047857; font-weight:800;">✓</span></td><td style="padding:9px 10px; border-top:1px solid #E2E8F0; text-align:center; font-size:13px; color:#334155;">14 days</td></tr><tr><td style="padding:9px 10px; border-top:1px solid #E2E8F0; font-size:13px; color:#334155;">Support</td><td style="padding:9px 10px; border-top:1px solid #E2E8F0; text-align:center; font-size:13px; color:#334155;">7 days a week</td><td style="padding:9px 10px; border-top:1px solid #E2E8F0; text-align:center; font-size:13px; color:#334155;">Weekdays</td></tr></table>', 6188 "props": { 6189 "anim": "", 6190 "device": "", 6191 "cssMobile": "", 6192 "beside": "", 6193 "width": "", 6194 "boxAlign": "center", 6195 "spaceTop": "", 6196 "spaceBottom": "", 6197 "boxBg": "", 6198 "boxPad": "", 6199 "boxBorder": 0, 6200 "boxBorderColor": "#E2E8F0", 6201 "boxCorner": "", 6202 "boxShadow": "", 6203 "css": "", 6204 "colA": "Us", 6205 "colB": "Them", 6206 "rows": '[{"label":"Free delivery","a":"yes","b":"no"},{"label":"Thirty-day returns","a":"yes","b":"14 days"},{"label":"Support","a":"7 days a week","b":"Weekdays"}]', 6207 "headBg": "#0F172A", 6208 "headColor": "#FFFFFF", 6209 "yes": "#047857", 6210 "no": "#CBD5E1", 6211 "color": "#334155", 6212 "border": "#E2E8F0", 6213 "fontSize": 13 6214 }, 6215 "settings": [ 6216 { 6217 "key": "rows", 6218 "label": "Rows", 6219 "kind": "table_rows", 6220 "tab": "content", 6221 "hint": "Type yes or no for a tick or a cross; anything else shows as written." 6222 }, 6223 { 6224 "key": "colA", 6225 "label": "First column heading", 6226 "kind": "text", 6227 "tab": "content" 6228 }, 6229 { 6230 "key": "colB", 6231 "label": "Second column heading", 6232 "kind": "text", 6233 "tab": "content" 6234 }, 6235 { 6236 "key": "headBg", 6237 "label": "Heading background", 6238 "kind": "color", 6239 "tab": "style" 6240 }, 6241 { 6242 "key": "headColor", 6243 "label": "Heading text", 6244 "kind": "color", 6245 "tab": "style" 6246 }, 6247 { 6248 "key": "yes", 6249 "label": "Tick colour", 6250 "kind": "color", 6251 "tab": "style" 6252 }, 6253 { 6254 "key": "no", 6255 "label": "Cross colour", 6256 "kind": "color", 6257 "tab": "style" 6258 }, 6259 { 6260 "key": "color", 6261 "label": "Text colour", 6262 "kind": "color", 6263 "tab": "style" 6264 }, 6265 { 6266 "key": "border", 6267 "label": "Line colour", 6268 "kind": "color", 6269 "tab": "style" 6270 }, 6271 { 6272 "key": "fontSize", 6273 "label": "Font size", 6274 "kind": "number", 6275 "tab": "style", 6276 "unit": "px", 6277 "min": 9, 6278 "max": 24 6279 } 6280 ] 6281 }, 6282 { 6283 "id": "mediatext", 6284 "label": "Media + Text", 6285 "html": '<style>@media (max-width:767px){.pz-mt{flex-direction:column !important; align-items:stretch}.pz-mt>*{flex-basis:auto !important}}</style><div class="pz-mt" style="display:flex; align-items:center; gap:18px; margin:14px 0;"><div style="flex:1 1 0; min-width:0;"><h2 style="margin:0 0 8px; font-size:20px; font-weight:800; color:#0F172A;">A picture and a point</h2><p style="margin:0; font-size:14px; line-height:1.55; color:#334155;">Put the supporting words right next to the visual.</p></div></div>', 6286 "props": { 6287 "anim": "", 6288 "device": "", 6289 "cssMobile": "", 6290 "beside": "", 6291 "width": "", 6292 "boxAlign": "center", 6293 "spaceTop": "", 6294 "spaceBottom": "", 6295 "boxBg": "", 6296 "boxPad": "", 6297 "boxBorder": 0, 6298 "boxBorderColor": "#E2E8F0", 6299 "boxCorner": "", 6300 "boxShadow": "", 6301 "hoverBg": "", 6302 "hoverText": "", 6303 "report": "", 6304 "reportEvent": "", 6305 "reportValue": "", 6306 "css": "", 6307 "image": "", 6308 "imageAlt": "", 6309 "imagePos": "left", 6310 "imageWidth": 40, 6311 "headline": "A picture and a point", 6312 "text": "Put the supporting words right next to the visual.", 6313 "headlineColor": "#0F172A", 6314 "textColor": "#334155"
6315 }, 6316 "settings": [ 6317 { 6318 "key": "image", 6319 "label": "Image", 6320 "kind": "media", 6321 "tab": "content" 6322 }, 6323 { 6324 "key": "imageAlt", 6325 "label": "Alt text", 6326 "kind": "text", 6327 "tab": "content" 6328 }, 6329 { 6330 "key": "imagePos", 6331 "label": "Image side", 6332 "kind": "select", 6333 "tab": "style", 6334 "options": [ 6335 [ 6336 "left", 6337 "Left" 6338 ], 6339 [ 6340 "right", 6341 "Right" 6342 ] 6343 ] 6344 }, 6345 { 6346 "key": "imageWidth", 6347 "label": "Image width", 6348 "kind": "number", 6349 "tab": "style", 6350 "hint": "The share of the row the picture takes; the text gets the rest.", 6351 "unit": "%", 6352 "min": 15, 6353 "max": 70 6354 }, 6355 { 6356 "key": "headline", 6357 "label": "Headline", 6358 "kind": "text", 6359 "tab": "content" 6360 }, 6361 { 6362 "key": "text", 6363 "label": "Text", 6364 "kind": "textarea", 6365 "tab": "content" 6366 }, 6367 { 6368 "key": "headlineColor", 6369 "label": "Headline color", 6370 "kind": "color", 6371 "tab": "style" 6372 }, 6373 { 6374 "key": "textColor", 6375 "label": "Text color", 6376 "kind": "color", 6377 "tab": "style" 6378 }, 6379 { 6380 "key": "hoverBg", 6381 "label": "Background on hover", 6382 "kind": "color", 6383 "tab": "style", 6384 "hint": "What it changes to under the mouse. Blank keeps it still." 6385 }, 6386 { 6387 "key": "hoverText", 6388 "label": "Text on hover", 6389 "kind": "color", 6390 "tab": "style" 6391 }, 6392 { 6393 "key": "report", 6394 "label": "When clicked, report", 6395 "kind": "select", 6396 "tab": "content", 6397 "hint": "Beside the click itself, which is always counted.", 6398 "options": [ 6399 [ 6400 "", 6401 "Nothing extra" 6402 ], 6403 [ 6404 "goal", 6405 "This action reached its goal" 6406 ], 6407 [ 6408 "event", 6409 "A conversion goal" 6410 ], 6411 [ 6412 "both", 6413 "Both" 6414 ] 6415 ] 6416 }, 6417 { 6418 "key": "reportEvent", 6419 "label": "Which goal", 6420 "kind": "goal", 6421 "tab": "content", 6422 "hint": "Event-driven goals only \u2014 the kind that waits for a named event. Other goal types watch the page themselves and need nothing here." 6423 }, 6424 { 6425 "key": "reportValue", 6426 "label": "Value", 6427 "kind": "text", 6428 "tab": "content", 6429 "hint": "Optional \u2014 a revenue amount, a plan name. Variables work here." 6430 } 6431 ] 6432 }, 6433 { 6434 "id": "menu", 6435 "label": "Menu", 6436 "html": '<style>.pz-menu-h-4277318641 a:hover{color:#0EA5E9}.pz-menu-sub{position:absolute; left:0; top:100%; z-index:5; display:none}.pz-menu-i:hover>.pz-menu-sub,.pz-menu-i:focus-within>.pz-menu-sub{display:block}.pz-menu-v .pz-menu-i>a,.pz-menu-v .pz-menu-i>span{padding:8px 0}.pz-menu-v .pz-menu-sub{position:static; box-shadow:none; padding-left:12px}@media (hover:none){.pz-menu-sub{position:static; display:block; box-shadow:none; padding-left:12px}}.pz-menu-bt{display:none; position:relative; cursor:pointer; align-items:center; gap:8px; padding:6px 0; user-select:none}.pz-menu-bt input{position:absolute; width:0; height:0; opacity:0}.pz-menu-bars{display:inline-block; width:22px; flex:none}.pz-menu-bars i{display:block; height:2px; margin:4px 0; background:currentColor; border-radius:2px}@supports selector(:has(*)){@media (max-width:767px){.pz-menu-cm{text-align:right!important}.pz-menu-cm.pz-menu-bl{text-align:left!important}.pz-menu-cm .pz-menu-bt{display:inline-flex}.pz-menu-cm .pz-menu-l{display:none!important}.pz-menu-cm:has(.pz-menu-cb:checked) .pz-menu-l{display:block!important}.pz-menu-cm:has(.pz-menu-cb:checked) .pz-menu-sub{position:static; display:block; box-shadow:none; padding-left:12px}}}</style><nav class="pz-block-menu pz-menu-h-4277318641 pz-menu-cm" style="text-align:center; margin:12px 0; font-size:14px; font-weight:600;"><label class="pz-menu-bt" style="color:#0F172A;"><input type="checkbox" class="pz-menu-cb" aria-label="Menu"><span class="pz-menu-bars"><i></i><i></i><i></i></span></label><ul class="pz-menu-l" style="list-style:none; margin:0; padding:0; display:inline-flex; gap:20px; flex-wrap:wrap; align-items:center;text-align:center;"><li class="pz-menu-i" style="list-style:none; position:relative;"><a href="/collections/all" data-personyze-click-target="" style="display:block; padding:6px 0; color:#0F172A; text-decoration:none; white-space:nowrap;">Shop</a><ul class="pz-menu-sub" style="list-style:none; margin:0; padding:6px 0; background:#FFFFFF; border-radius:10px; box-shadow:0 10px 30px rgba(15,23,42,0.16); min-width:100%;"><li style="list-style:none;"><a href="/collections/new" data-personyze-click-target="" style="display:block; padding:7px 14px; color:#0F172A; text-decoration:none; white-space:nowrap; font-size:13px;">New arrivals</a></li><li style="list-style:none;"><a href="/collections/best" data-personyze-click-target="" style="display:block; padding:7px 14px; color:#0F172A; text-decoration:none; white-space:nowrap; font-size:13px;">Best sellers</a></li><li style="list-style:none;"><a href="/collections/sale" data-personyze-click-target="" style="display:block; padding:7px 14px; color:#0F172A; text-decoration:none; white-space:nowrap; font-size:13px;">Sale</a></li></ul></li><li class="pz-menu-i" style="list-style:none; position:relative;"><a href="/pages/about" data-personyze-click-target="" style="display:block; padding:6px 0; color:#0F172A; text-decoration:none; white-space:nowrap;">About</a></li><li class="pz-menu-i" style="list-style:none; position:relative;"><a href="/pages/contact" data-personyze-click-target="" style="display:block; padding:6px 0; color:#0F172A; text-decoration:none; white-space:nowrap;">Contact</a></li></ul></nav>', 6437 "props": { 6438 "anim": "", 6439 "device": "", 6440 "cssMobile": "", 6441 "beside": "", 6442 "width": "", 6443 "boxAlign": "center", 6444 "spaceTop": "", 6445 "spaceBottom": "", 6446 "boxBg": "", 6447 "boxPad": "", 6448 "boxBorder": 0, 6449 "boxBorderColor": "#E2E8F0", 6450 "boxCorner": "", 6451 "boxShadow": "", 6452 "css": "", 6453 "items": '[{"label":"Shop","link":"href=\\"/collections/all\\" data-personyze-click-target=\\"1\\"","children":[{"label":"New arrivals","link":"href=\\"/collections/new\\" data-personyze-click-target=\\"1\\""},{"label":"Best sellers","link":"href=\\"/collections/best\\" data-personyze-click-target=\\"1\\""},{"label":"Sale","link":"href=\\"/collections/sale\\" data-personyze-click-target=\\"1\\""}]},{"label":"About","link":"href=\\"/pages/about\\" data-personyze-click-target=\\"1\\"","children":[]},{"label":"Contact","link":"href=\\"/pages/contact\\" data-personyze-click-target=\\"1\\"","children":[]}]', 6454 "orientation": "horizontal", 6455 "align": "center", 6456 "color": "#0F172A", 6457 "hoverColor": "#0EA5E9", 6458 "subBg": "#FFFFFF", 6459 "fontSize": 14, 6460 "gap": 20, 6461 "radius": 10, 6462 "collapse": "mobile", 6463 "burgerSide": "", 6464 "burgerLabel": "" 6465 }, 6466 "settings": [ 6467 { 6468 "key": "items", 6469 "label": "Menu entries", 6470 "kind": "menu_list", 6471 "tab": "content", 6472 "hint": "Each entry can hold its own sub-entries." 6473 }, 6474 { 6475 "key": "orientation", 6476 "label": "Direction", 6477 "kind": "select", 6478 "tab": "style", 6479 "hint": "A row for a bar, a column for a side panel.", 6480 "options": [ 6481 [ 6482 "horizontal", 6483 "Across \u2014 a row" 6484 ], 6485 [ 6486 "vertical", 6487 "Down \u2014 a column" 6488 ] 6489 ] 6490 }, 6491 { 6492 "key": "align", 6493 "label": "Alignment", 6494 "kind": "align", 6495 "tab": "style" 6496 }, 6497 { 6498 "key": "color", 6499 "label": "Text colour", 6500 "kind": "color", 6501 "tab": "style" 6502 }, 6503 { 6504 "key": "hoverColor", 6505 "label": "Colour on hover", 6506 "kind": "color", 6507 "tab": "style" 6508 }, 6509 { 6510 "key": "fontSize", 6511 "label": "Font size", 6512 "kind": "number", 6513 "tab": "style", 6514 "unit": "px", 6515 "min": 9, 6516 "max": 40 6517 }, 6518 { 6519 "key": "gap", 6520 "label": "Spacing between entries", 6521 "kind": "number", 6522 "tab": "style", 6523 "unit": "px", 6524 "min": 0, 6525 "max": 80 6526 }, 6527 { 6528 "key": "collapse", 6529 "label": "Collapse into a menu icon", 6530 "kind": "select", 6531 "tab": "content", 6532 "hint": "The entries hide behind a \u2630 icon and open when it is clicked.", 6533 "options": [ 6534 [ 6535 "", 6536 "Never \u2014 always show the entries" 6537 ], 6538 [ 6539 "mobile", 6540 "On phones (\u2264767px)" 6541 ], 6542 [ 6543 "always", 6544 "Always \u2014 icon on every screen" 6545 ] 6546 ] 6547 }, 6548 { 6549 "key": "burgerSide", 6550 "label": "Icon sits", 6551 "kind": "select", 6552 "tab": "content", 6553 "hint": "Which end of the strip the menu icon sits at once the entries are collapsed behind it.", 6554 "options": [ 6555 [ 6556 "", 6557 "Right" 6558 ], 6559 [ 6560 "left", 6561 "Left" 6562 ] 6563 ] 6564 }, 6565 { 6566 "key": "burgerLabel", 6567 "label": "Text beside the icon", 6568 "kind": "text", 6569 "tab": "content", 6570 "hint": 'Blank for the icon alone \u2014 "Menu" if the icon needs a word.' 6571 }, 6572 { 6573 "key": "subBg", 6574 "label": "Submenu background", 6575 "kind": "color", 6576 "tab": "style" 6577 }, 6578 { 6579 "key": "radius",
6580 "label": "Submenu corner radius", 6581 "kind": "number", 6582 "tab": "style", 6583 "unit": "px", 6584 "min": 0, 6585 "max": 40 6586 } 6587 ] 6588 }, 6589 { 6590 "id": "imageslider", 6591 "label": "Image Slider", 6592 "html": "", 6593 "props": { 6594 "anim": "", 6595 "device": "", 6596 "cssMobile": "", 6597 "beside": "", 6598 "width": "", 6599 "boxAlign": "center", 6600 "spaceTop": "", 6601 "spaceBottom": "", 6602 "boxBg": "", 6603 "boxPad": "", 6604 "boxBorder": 0, 6605 "boxBorderColor": "#E2E8F0", 6606 "boxCorner": "", 6607 "boxShadow": "", 6608 "css": "", 6609 "slides": '[{"url":""}]', 6610 "height": 220, 6611 "radius": 10, 6612 "link": "" 6613 }, 6614 "settings": [ 6615 { 6616 "key": "slides", 6617 "label": "Slides", 6618 "kind": "slide_list", 6619 "tab": "content", 6620 "hint": "One picture per slide; a second slide brings the arrows." 6621 }, 6622 { 6623 "key": "height", 6624 "label": "Height", 6625 "kind": "number", 6626 "tab": "style", 6627 "unit": "px", 6628 "min": 60, 6629 "max": 800 6630 }, 6631 { 6632 "key": "radius", 6633 "label": "Corner radius", 6634 "kind": "number", 6635 "tab": "style", 6636 "unit": "px", 6637 "min": 0, 6638 "max": 100 6639 }, 6640 { 6641 "key": "link", 6642 "label": "Link for slides with none of their own", 6643 "kind": "link", 6644 "tab": "content" 6645 } 6646 ] 6647 }, 6648 { 6649 "id": "logos", 6650 "label": "Logo Row", 6651 "html": "", 6652 "props": { 6653 "anim": "", 6654 "device": "", 6655 "cssMobile": "", 6656 "beside": "", 6657 "width": "", 6658 "boxAlign": "center", 6659 "spaceTop": "", 6660 "spaceBottom": "", 6661 "boxBg": "", 6662 "boxPad": "", 6663 "boxBorder": 0, 6664 "boxBorderColor": "#E2E8F0", 6665 "boxCorner": "", 6666 "boxShadow": "", 6667 "css": "", 6668 "logos": [ 6669 "" 6670 ], 6671 "height": 32, 6672 "gap": 24, 6673 "gray": "1", 6674 "source": "", 6675 "withStories": "1", 6676 "count": 12, 6677 "order": "featured", 6678 "matchIndustry": "prefer", 6679 "matchCompanySize": "prefer", 6680 "matchCountry": "", 6681 "matchRegion": "", 6682 "matchCity": "", 6683 "matchDomain": "", 6684 "matchUrl": "", 6685 "matchTag": "", 6686 "matchTagAttr": "" 6687 }, 6688 "settings": [ 6689 { 6690 "key": "source", 6691 "label": "Logos come from", 6692 "kind": "select", 6693 "tab": "content", 6694 "hint": "Your customer logos on the Reviews page are picked for each visitor - the logos of their industry, company size or country first.", 6695 "options": [ 6696 [ 6697 "", 6698 "Logos I add here" 6699 ], 6700 [ 6701 "imported", 6702 "The Reviews page" 6703 ] 6704 ] 6705 }, 6706 { 6707 "key": "logos", 6708 "label": "Logos", 6709 "kind": "media_list", 6710 "tab": "content", 6711 "hint": "Transparent PNGs sit best." 6712 }, 6713 { 6714 "key": "withStories", 6715 "label": "Also the company logos on testimonials and case studies", 6716 "kind": "checkbox", 6717 "tab": "content", 6718 "hint": "A company that also has a logo entry shows once." 6719 }, 6720 { 6721 "key": "count", 6722 "label": "How many", 6723 "kind": "number", 6724 "tab": "content", 6725 "min": 1, 6726 "max": 24 6727 }, 6728 { 6729 "key": "order", 6730 "label": "Order", 6731 "kind": "select", 6732 "tab": "content", 6733 "options": [ 6734 [ 6735 "featured", 6736 "Featured first" 6737 ], 6738 [ 6739 "", 6740 "Newest first" 6741 ], 6742 [ 6743 "random", 6744 "Random" 6745 ] 6746 ] 6747 }, 6748 { 6749 "key": "matchIndustry", 6750 "label": "Industry", 6751 "kind": "select", 6752 "tab": "content", 6753 "options": [ 6754 [ 6755 "", 6756 "Ignore" 6757 ], 6758 [ 6759 "prefer", 6760 "Matching logos first" 6761 ], 6762 [ 6763 "either", 6764 "Hide logos tagged for others" 6765 ], 6766 [ 6767 "only", 6768 "Only matching logos" 6769 ] 6770 ] 6771 }, 6772 { 6773 "key": "matchCompanySize", 6774 "label": "Company size", 6775 "kind": "select", 6776 "tab": "content", 6777 "options": [ 6778 [ 6779 "", 6780 "Ignore" 6781 ], 6782 [ 6783 "prefer", 6784 "Matching logos first" 6785 ], 6786 [ 6787 "either", 6788 "Hide logos tagged for others" 6789 ], 6790 [ 6791 "only", 6792 "Only matching logos" 6793 ] 6794 ] 6795 }, 6796 { 6797 "key": "matchCountry", 6798 "label": "Country", 6799 "kind": "select", 6800 "tab": "content", 6801 "options": [ 6802 [ 6803 "", 6804 "Ignore" 6805 ], 6806 [ 6807 "prefer", 6808 "Matching logos first" 6809 ], 6810 [ 6811 "either", 6812 "Hide logos tagged for others" 6813 ], 6814 [ 6815 "only", 6816 "Only matching logos" 6817 ] 6818 ] 6819 }, 6820 { 6821 "key": "matchRegion", 6822 "label": "State or region", 6823 "kind": "select", 6824 "tab": "content", 6825 "options": [ 6826 [ 6827 "", 6828 "Ignore" 6829 ], 6830 [ 6831 "prefer", 6832 "Matching logos first" 6833 ], 6834 [ 6835 "either", 6836 "Hide logos tagged for others" 6837 ], 6838 [ 6839 "only", 6840 "Only matching logos" 6841 ] 6842 ] 6843 }, 6844 { 6845 "key": "matchCity", 6846 "label": "City", 6847 "kind": "select", 6848 "tab": "content", 6849 "options": [ 6850 [ 6851 "", 6852 "Ignore" 6853 ], 6854 [ 6855 "prefer", 6856 "Matching logos first" 6857 ], 6858 [ 6859 "either", 6860 "Hide logos tagged for others" 6861 ], 6862 [ 6863 "only", 6864 "Only matching logos" 6865 ] 6866 ] 6867 }, 6868 {
6869 "key": "matchDomain", 6870 "label": "Website", 6871 "kind": "select", 6872 "tab": "content", 6873 "options": [ 6874 [ 6875 "", 6876 "Ignore" 6877 ], 6878 [ 6879 "prefer", 6880 "Matching logos first" 6881 ], 6882 [ 6883 "either", 6884 "Hide logos tagged for others" 6885 ], 6886 [ 6887 "only", 6888 "Only matching logos" 6889 ] 6890 ] 6891 }, 6892 { 6893 "key": "matchUrl", 6894 "label": "Page path", 6895 "kind": "select", 6896 "tab": "content", 6897 "options": [ 6898 [ 6899 "", 6900 "Ignore" 6901 ], 6902 [ 6903 "prefer", 6904 "Matching logos first" 6905 ], 6906 [ 6907 "either", 6908 "Hide logos tagged for others" 6909 ], 6910 [ 6911 "only", 6912 "Only matching logos" 6913 ] 6914 ] 6915 }, 6916 { 6917 "key": "matchTag", 6918 "label": "Your own fields", 6919 "kind": "select", 6920 "tab": "content", 6921 "hint": "Logos tagged on the Reviews page with a visitor attribute\u2019s value, e.g. plan = pro.", 6922 "options": [ 6923 [ 6924 "", 6925 "Ignore" 6926 ], 6927 [ 6928 "prefer", 6929 "Matching logos first" 6930 ], 6931 [ 6932 "either", 6933 "Hide logos tagged for others" 6934 ], 6935 [ 6936 "only", 6937 "Only matching logos" 6938 ] 6939 ] 6940 }, 6941 { 6942 "key": "height", 6943 "label": "Logo height", 6944 "kind": "number", 6945 "tab": "style", 6946 "unit": "px", 6947 "min": 12, 6948 "max": 120 6949 }, 6950 { 6951 "key": "gap", 6952 "label": "Spacing", 6953 "kind": "number", 6954 "tab": "style", 6955 "unit": "px", 6956 "min": 4, 6957 "max": 80 6958 }, 6959 { 6960 "key": "gray", 6961 "label": "Mute the colors (grayscale)", 6962 "kind": "checkbox", 6963 "tab": "style", 6964 "hint": "The quiet editorial look." 6965 } 6966 ] 6967 }, 6968 { 6969 "id": "testimonials", 6970 "label": "Testimonials", 6971 "html": "", 6972 "props": { 6973 "anim": "", 6974 "device": "", 6975 "cssMobile": "", 6976 "beside": "", 6977 "width": "", 6978 "boxAlign": "center", 6979 "spaceTop": "", 6980 "spaceBottom": "", 6981 "boxBg": "", 6982 "boxPad": "", 6983 "boxBorder": 0, 6984 "boxBorderColor": "#E2E8F0", 6985 "boxCorner": "", 6986 "boxShadow": "", 6987 "css": "", 6988 "items": '[{"q":"","name":"","img":""}]', 6989 "layout": "", 6990 "stars": "5", 6991 "starColor": "#B45309", 6992 "quoteColor": "#334155", 6993 "nameColor": "#64748B", 6994 "cardBg": "#F8FAFC", 6995 "source": "", 6996 "count": 3, 6997 "order": "", 6998 "minRating": "", 6999 "fromSource": "", 7000 "kinds": "review", 7001 "matchProduct": "", 7002 "matchCountry": "", 7003 "matchRegion": "", 7004 "matchCity": "", 7005 "matchGender": "", 7006 "matchIndustry": "", 7007 "matchCompanySize": "", 7008 "matchUrl": "", 7009 "matchDomain": "", 7010 "matchInterest": "", 7011 "matchCategory": "", 7012 "interestPick": "", 7013 "matchTag": "", 7014 "matchTagAttr": "", 7015 "showLogo": "1", 7016 "showCover": "1", 7017 "ctaText": "Read the story", 7018 "showTitle": "1", 7019 "maxLength": 280, 7020 "verifiedText": "Verified buyer", 7021 "showSource": "1", 7022 "viaText": "via", 7023 "showReply": "", 7024 "replyLabel": "Reply:", 7025 "about": "", 7026 "withPhotos": "", 7027 "showAvatar": "", 7028 "showProduct": "", 7029 "recommendText": "", 7030 "showRole": "1", 7031 "showPhotos": "1", 7032 "showDate": "1", 7033 "showLink": "", 7034 "linkText": "Read on {source}", 7035 "showSummary": "", 7036 "summaryText": "\u2605 {rating} \xB7 {n} reviews", 7037 "numberStyle": "" 7038 }, 7039 "settings": [ 7040 { 7041 "key": "source", 7042 "label": "Reviews come from", 7043 "kind": "select", 7044 "tab": "content", 7045 "hint": "Your saved reviews, testimonials and case studies: from a review platform, a file, your own review form or added by hand on the Reviews page.", 7046 "options": [ 7047 [ 7048 "", 7049 "Quotes I type here" 7050 ], 7051 [ 7052 "imported", 7053 "The Reviews page" 7054 ] 7055 ] 7056 }, 7057 { 7058 "key": "items", 7059 "label": "Testimonials", 7060 "kind": "quote_list", 7061 "tab": "content", 7062 "hint": "Quote, name, optional photo per row." 7063 }, 7064 { 7065 "key": "count", 7066 "label": "How many", 7067 "kind": "number", 7068 "tab": "content", 7069 "min": 1, 7070 "max": 24 7071 }, 7072 { 7073 "key": "order", 7074 "label": "Order", 7075 "kind": "select", 7076 "tab": "content", 7077 "options": [ 7078 [ 7079 "", 7080 "Newest first" 7081 ], 7082 [ 7083 "best", 7084 "Highest rated first" 7085 ], 7086 [ 7087 "featured", 7088 "Featured first" 7089 ], 7090 [ 7091 "helpful", 7092 "Most helpful first" 7093 ], 7094 [ 7095 "random", 7096 "Random" 7097 ] 7098 ] 7099 }, 7100 { 7101 "key": "about", 7102 "label": "Reviews of", 7103 "kind": "select", 7104 "tab": "content", 7105 "options": [ 7106 [ 7107 "", 7108 "Anything" 7109 ], 7110 [ 7111 "store", 7112 "Your business only" 7113 ], 7114 [ 7115 "product", 7116 "Products only" 7117 ], 7118 [ 7119 "content", 7120 "Pages & articles only" 7121 ] 7122 ] 7123 }, 7124 { 7125 "key": "withPhotos", 7126 "label": "Only reviews with photos", 7127 "kind": "checkbox", 7128 "tab": "content" 7129 }, 7130 { 7131 "key": "minRating", 7132 "label": "Minimum rating", 7133 "kind": "select", 7134 "tab": "content", 7135 "hint": "Hides reviews rated below it, and anything without a rating - most testimonials, case studies and press quotes have none.", 7136 "options": [ 7137 [ 7138 "", 7139 "Any rating" 7140 ], 7141 [ 7142 "3", 7143 "3 stars and up" 7144 ], 7145 [ 7146 "4", 7147 "4 stars and up" 7148 ], 7149 [ 7150 "5", 7151 "5 stars only" 7152 ] 7153 ] 7154 }, 7155 { 7156 "key": "matchProduct", 7157 "label": "Product", 7158 "kind": "select", 7159 "tab": "content", 7160 "hint": 'Reviews of the product on the page, or of the last one they bought. "Only" hides reviews of no product; "or of no product" keeps those too; "prefer" puts the matches first.', 7161 "options": [ 7162 [ 7163 "", 7164 "Any product" 7165 ], 7166 [ 7167 "viewing", 7168 "Prefer the product being viewed" 7169 ], 7170 [ 7171 "viewing_either", 7172 "The product being viewed, or of no product" 7173 ], 7174 [ 7175 "viewing_only", 7176 "Only the product being viewed" 7177 ], 7178 [ 7179 "last", 7180 "Prefer the last product they bought" 7181 ], 7182 [ 7183 "last_either", 7184 "The last product they bought, or of no product" 7185 ], 7186 [ 7187 "last_only", 7188 "Only the last product they bought" 7189 ] 7190 ] 7191 }, 7192 { 7193 "key": "matchCountry", 7194 "label": "Country", 7195 "kind": "select", 7196 "tab": "content", 7197 "hint": "Matches when any of the review\u2019s values is the visitor\u2019s.", 7198 "options": [ 7199 [ 7200 "", 7201 "Ignore" 7202 ], 7203 [ 7204 "prefer", 7205 "Prefer matching reviews (more matches first)" 7206 ], 7207 [ 7208 "either", 7209 "Matching reviews, and ones with no value for this field" 7210 ], 7211 [ 7212 "only", 7213 "Only matching reviews (ones with no value are hidden)" 7214 ] 7215 ] 7216 }, 7217 { 7218 "key": "matchRegion", 7219 "label": "State or region", 7220 "kind": "select", 7221 "tab": "content", 7222 "hint": "Matches when any of the review\u2019s regions is the visitor\u2019s, by name (Texas) or code (TX).", 7223 "options": [ 7224 [ 7225 "", 7226 "Ignore" 7227 ], 7228 [ 7229 "prefer", 7230 "Prefer matching reviews (more matches first)" 7231 ], 7232 [ 7233 "either", 7234 "Matching reviews, and ones with no value for this field" 7235 ], 7236 [ 7237 "only", 7238 "Only matching reviews (ones with no value are hidden)" 7239 ] 7240 ] 7241 }, 7242 { 7243 "key": "matchCity", 7244 "label": "City", 7245 "kind": "select", 7246 "tab": "content", 7247 "hint": "Matches when any of the review\u2019s values is the visitor\u2019s.", 7248 "options": [ 7249 [ 7250 "", 7251 "Ignore" 7252 ], 7253 [ 7254 "prefer", 7255 "Prefer matching reviews (more matches first)" 7256 ], 7257 [ 7258 "either", 7259 "Matching reviews, and ones with no value for this field" 7260 ], 7261 [ 7262 "only", 7263 "Only matching reviews (ones with no value are hidden)" 7264 ] 7265 ] 7266 }, 7267 { 7268 "key": "matchGender", 7269 "label": "Gender", 7270 "kind": "select", 7271 "tab": "content", 7272 "hint": "Only visitors whose gender is known match - most are not.", 7273 "options": [ 7274 [ 7275 "", 7276 "Ignore" 7277 ], 7278 [ 7279 "prefer", 7280 "Prefer matching reviews (more matches first)" 7281 ], 7282 [ 7283 "either", 7284 "Matching reviews, and ones with no value for this field" 7285 ], 7286 [ 7287 "only", 7288 "Only matching reviews (ones with no value are hidden)" 7289 ] 7290 ] 7291 }, 7292 { 7293 "key": "matchIndustry", 7294 "label": "Industry", 7295 "kind": "select", 7296 "tab": "content", 7297 "hint": "Matches when any of the review\u2019s values is the visitor\u2019s.", 7298 "options": [ 7299 [ 7300 "", 7301 "Ignore" 7302 ], 7303 [ 7304 "prefer", 7305 "Prefer matching reviews (more matches first)" 7306 ], 7307 [ 7308 "either", 7309 "Matching reviews, and ones with no value for this field" 7310 ], 7311 [ 7312 "only", 7313 "Only matching reviews (ones with no value are hidden)" 7314 ] 7315 ] 7316 }, 7317 { 7318 "key": "matchCompanySize", 7319 "label": "Company size", 7320 "kind": "select", 7321 "tab": "content", 7322 "hint": "Matches when any of the review\u2019s values is the visitor\u2019s.", 7323 "options": [ 7324 [ 7325 "", 7326 "Ignore" 7327 ], 7328 [ 7329 "prefer", 7330 "Prefer matching reviews (more matches first)" 7331 ], 7332 [ 7333 "either", 7334 "Matching reviews, and ones with no value for this field" 7335 ], 7336 [ 7337 "only", 7338 "Only matching reviews (ones with no value are hidden)" 7339 ] 7340 ] 7341 }, 7342 { 7343 "key": "matchUrl", 7344 "label": "Page path", 7345 "kind": "select", 7346 "tab": "content", 7347 "hint": "Matches when any of the review\u2019s paths is the page: the path itself, or also every page under it when it ends with / or *. A lone / is the home page.", 7348 "options": [ 7349 [ 7350 "", 7351 "Ignore" 7352 ], 7353 [ 7354 "prefer", 7355 "Prefer matching reviews (more matches first)" 7356 ], 7357 [ 7358 "either", 7359 "Matching reviews, and ones with no value for this field" 7360 ], 7361 [ 7362 "only", 7363 "Only matching reviews (ones with no value are hidden)" 7364 ] 7365 ] 7366 }, 7367 {
7368 "key": "matchDomain", 7369 "label": "Website", 7370 "kind": "select", 7371 "tab": "content", 7372 "hint": "For an account with several sites: matches when any of the review\u2019s websites is the one the visitor is on.", 7373 "options": [ 7374 [ 7375 "", 7376 "Ignore" 7377 ], 7378 [ 7379 "prefer", 7380 "Prefer matching reviews (more matches first)" 7381 ], 7382 [ 7383 "either", 7384 "Matching reviews, and ones with no value for this field" 7385 ], 7386 [ 7387 "only", 7388 "Only matching reviews (ones with no value are hidden)" 7389 ] 7390 ] 7391 }, 7392 { 7393 "key": "matchInterest", 7394 "label": "Interests", 7395 "kind": "select", 7396 "tab": "content", 7397 "hint": "Reviews tagged with the visitor\u2019s interest.", 7398 "options": [ 7399 [ 7400 "", 7401 "Ignore" 7402 ], 7403 [ 7404 "prefer", 7405 "Prefer matching reviews (more matches first)" 7406 ], 7407 [ 7408 "either", 7409 "Matching reviews, and ones with no value for this field" 7410 ], 7411 [ 7412 "only", 7413 "Only matching reviews (ones with no value are hidden)" 7414 ] 7415 ] 7416 }, 7417 { 7418 "key": "matchCategory", 7419 "label": "Categories", 7420 "kind": "select", 7421 "tab": "content", 7422 "hint": "Reviews tagged with a category the visitor browsed.", 7423 "options": [ 7424 [ 7425 "", 7426 "Ignore" 7427 ], 7428 [ 7429 "prefer", 7430 "Prefer matching reviews (more matches first)" 7431 ], 7432 [ 7433 "either", 7434 "Matching reviews, and ones with no value for this field" 7435 ], 7436 [ 7437 "only", 7438 "Only matching reviews (ones with no value are hidden)" 7439 ] 7440 ] 7441 }, 7442 { 7443 "key": "interestPick", 7444 "label": "Which of the visitor\u2019s interests", 7445 "kind": "select", 7446 "tab": "content", 7447 "hint": "A visitor has many. The strongest is the one they came back to most.", 7448 "options": [ 7449 [ 7450 "", 7451 "Their strongest" 7452 ], 7453 [ 7454 "latest", 7455 "Their latest" 7456 ], 7457 [ 7458 "any", 7459 "Any of them" 7460 ] 7461 ] 7462 }, 7463 { 7464 "key": "matchTag", 7465 "label": "Your own fields", 7466 "kind": "select", 7467 "tab": "content", 7468 "hint": "Reviews tagged on the Reviews page with a visitor attribute\u2019s value, e.g. plan = pro.", 7469 "options": [ 7470 [ 7471 "", 7472 "Ignore" 7473 ], 7474 [ 7475 "prefer", 7476 "Prefer matching reviews (more matches first)" 7477 ], 7478 [ 7479 "either", 7480 "Matching reviews, and ones with no value for this field" 7481 ], 7482 [ 7483 "only", 7484 "Only matching reviews (ones with no value are hidden)" 7485 ] 7486 ] 7487 }, 7488 { 7489 "key": "showTitle", 7490 "label": "Show the review title", 7491 "kind": "checkbox", 7492 "tab": "content" 7493 }, 7494 { 7495 "key": "maxLength", 7496 "label": "Shorten reviews to", 7497 "kind": "number", 7498 "tab": "content", 7499 "unit": "characters", 7500 "min": 40, 7501 "max": 2e3 7502 }, 7503 { 7504 "key": "showAvatar", 7505 "label": "Show the reviewer\u2019s photo", 7506 "kind": "checkbox", 7507 "tab": "content", 7508 "hint": "When the review platform has one." 7509 }, 7510 { 7511 "key": "showPhotos", 7512 "label": "Show the reviewer\u2019s photos", 7513 "kind": "checkbox", 7514 "tab": "content", 7515 "hint": "Up to three, when the review has them." 7516 }, 7517 { 7518 "key": "showDate", 7519 "label": "Show the date", 7520 "kind": "checkbox", 7521 "tab": "content" 7522 }, 7523 { 7524 "key": "showProduct", 7525 "label": "Show what it is about", 7526 "kind": "checkbox", 7527 "tab": "content", 7528 "hint": "The product\u2019s name and variant, or the page\u2019s title - for a block that shows reviews of many products or pages." 7529 }, 7530 { 7531 "key": "recommendText", 7532 "label": "\u201CWould recommend\u201D label", 7533 "kind": "text", 7534 "tab": "content", 7535 "hint": "Shown on reviews whose author said they would recommend it. Empty hides it." 7536 }, 7537 { 7538 "key": "showLogo", 7539 "label": "Show the company logo", 7540 "kind": "checkbox", 7541 "tab": "content", 7542 "hint": "The customer\u2019s or publication\u2019s logo, on testimonials, case studies and press quotes that have one." 7543 }, 7544 { 7545 "key": "showCover", 7546 "label": "Show the cover picture", 7547 "kind": "checkbox", 7548 "tab": "content", 7549 "hint": "A case study\u2019s picture, above the card." 7550 }, 7551 { 7552 "key": "ctaText", 7553 "label": "Link label", 7554 "kind": "text", 7555 "tab": "content", 7556 "hint": "For a case study or article with a link and no label of its own." 7557 }, 7558 { 7559 "key": "showRole", 7560 "label": "Show job title and company", 7561 "kind": "checkbox", 7562 "tab": "content", 7563 "hint": "For B2B testimonials, when the review has them." 7564 }, 7565 { 7566 "key": "verifiedText", 7567 "label": "Verified buyer label", 7568 "kind": "text", 7569 "tab": "content", 7570 "hint": "Shown on reviews from verified buyers. Empty hides it." 7571 }, 7572 { 7573 "key": "showSource", 7574 "label": "Show where the review came from", 7575 "kind": "checkbox", 7576 "tab": "content" 7577 }, 7578 { 7579 "key": "viaText", 7580 "label": "Word before the platform", 7581 "kind": "text", 7582 "tab": "content" 7583 }, 7584 { 7585 "key": "showReply", 7586 "label": "Show your reply", 7587 "kind": "checkbox", 7588 "tab": "content" 7589 }, 7590 { 7591 "key": "replyLabel", 7592 "label": "Reply label", 7593 "kind": "text", 7594 "tab": "content" 7595 }, 7596 { 7597 "key": "showLink", 7598 "label": "Link to the review on its platform", 7599 "kind": "checkbox", 7600 "tab": "content", 7601 "hint": "Opens in a new tab. Only reviews that have a link and a platform name." 7602 }, 7603 { 7604 "key": "linkText", 7605 "label": "Platform link label", 7606 "kind": "text", 7607 "tab": "content", 7608 "hint": "The word source in curly braces stands for the platform\u2019s name." 7609 }, 7610 { 7611 "key": "showSummary", 7612 "label": "Show the overall rating above the cards", 7613 "kind": "checkbox", 7614 "tab": "content", 7615 "hint": "Your average rating and how many rated reviews it is from, e.g. 4.8 from 1,234." 7616 }, 7617 { 7618 "key": "summaryText", 7619 "label": "Overall rating text", 7620 "kind": "text", 7621 "tab": "content", 7622 "hint": "The words rating and n in curly braces stand for the average and the count." 7623 }, 7624 { 7625 "key": "numberStyle", 7626 "label": "Number style", 7627 "kind": "select", 7628 "tab": "content", 7629 "hint": "How the count and the average are written. Pick the one your site\u2019s language uses.", 7630 "options": [ 7631 [ 7632 "", 7633 "1,234 and 4.8" 7634 ], 7635 [ 7636 "dot", 7637 "1.234 and 4,8" 7638 ], 7639 [ 7640 "space", 7641 "1 234 and 4,8" 7642 ] 7643 ] 7644 }, 7645 { 7646 "key": "layout", 7647 "label": "Layout", 7648 "kind": "select", 7649 "tab": "style", 7650 "hint": "Slider shows one card at a time.", 7651 "options": [ 7652 [ 7653 "", 7654 "Stacked" 7655 ], 7656 [ 7657 "slider", 7658 "Slider" 7659 ] 7660 ] 7661 }, 7662 { 7663 "key": "stars", 7664 "label": "Stars", 7665 "kind": "select", 7666 "tab": "style", 7667 "options": [ 7668 [ 7669 "0", 7670 "None" 7671 ], 7672 [ 7673 "5", 7674 "5 stars" 7675 ], 7676 [ 7677 "4", 7678 "4 stars" 7679 ], 7680 [ 7681 "3", 7682 "3 stars" 7683 ], 7684 [ 7685 "2", 7686 "2 stars" 7687 ], 7688 [ 7689 "one", 7690 "1 star" 7691 ] 7692 ] 7693 }, 7694 { 7695 "key": "starColor", 7696 "label": "Star color", 7697 "kind": "color", 7698 "tab": "style" 7699 }, 7700 { 7701 "key": "quoteColor", 7702 "label": "Quote color", 7703 "kind": "color", 7704 "tab": "style" 7705 }, 7706 { 7707 "key": "nameColor",
7708 "label": "Name color", 7709 "kind": "color", 7710 "tab": "style" 7711 }, 7712 { 7713 "key": "cardBg", 7714 "label": "Card background", 7715 "kind": "color", 7716 "tab": "style" 7717 } 7718 ] 7719 }, 7720 { 7721 "id": "reviewrequest", 7722 "label": "Ask for a Review", 7723 "html": "", 7724 "props": { 7725 "anim": "", 7726 "device": "", 7727 "cssMobile": "", 7728 "beside": "", 7729 "width": "", 7730 "boxAlign": "center", 7731 "spaceTop": "", 7732 "spaceBottom": "", 7733 "boxBg": "", 7734 "boxPad": "", 7735 "boxBorder": 0, 7736 "boxBorderColor": "#E2E8F0", 7737 "boxCorner": "", 7738 "boxShadow": "", 7739 "css": "", 7740 "heading": "Enjoyed your experience?", 7741 "text": "A short review helps other people find us. It only takes a minute.", 7742 "buttonText": "Review us on {platform}", 7743 "google": "", 7744 "trustpilot": "", 7745 "g2": "", 7746 "capterra": "", 7747 "facebook": "", 7748 "customName": "", 7749 "customUrl": "", 7750 "product": "", 7751 "productImage": "1", 7752 "productButton": "Review {product}", 7753 "productAnchor": "", 7754 "layout": "", 7755 "bg": "#0F172A", 7756 "color": "#FFFFFF", 7757 "radius": 8, 7758 "align": "center" 7759 }, 7760 "settings": [ 7761 { 7762 "key": "product", 7763 "label": "Ask about", 7764 "kind": "select", 7765 "tab": "style", 7766 "hint": "With a product, the block shows only to visitors with a purchase on record, and {product} in the text becomes its name.", 7767 "options": [ 7768 [ 7769 "", 7770 "Their experience in general" 7771 ], 7772 [ 7773 "last", 7774 "The last product they bought" 7775 ] 7776 ] 7777 }, 7778 { 7779 "key": "heading", 7780 "label": "Heading", 7781 "kind": "text", 7782 "tab": "content" 7783 }, 7784 { 7785 "key": "text", 7786 "label": "Text", 7787 "kind": "textarea", 7788 "tab": "content" 7789 }, 7790 { 7791 "key": "buttonText", 7792 "label": "Button label", 7793 "kind": "text", 7794 "tab": "content", 7795 "hint": "{platform} is replaced by the platform\u2019s name." 7796 }, 7797 { 7798 "key": "productImage", 7799 "label": "Show the product\u2019s picture", 7800 "kind": "checkbox", 7801 "tab": "style" 7802 }, 7803 { 7804 "key": "productButton", 7805 "label": "Product review button", 7806 "kind": "text", 7807 "tab": "content", 7808 "hint": "Goes to the product\u2019s page on your site. Blank for none." 7809 }, 7810 { 7811 "key": "productAnchor", 7812 "label": "Review form anchor", 7813 "kind": "text", 7814 "tab": "content", 7815 "hint": "Added to the product page link so it opens at your review form, like #judgeme_product_reviews." 7816 }, 7817 { 7818 "key": "google", 7819 "label": "Google", 7820 "kind": "text", 7821 "tab": "content", 7822 "hint": "Your review link from Google Business Profile (Ask for reviews), or your Place ID." 7823 }, 7824 { 7825 "key": "trustpilot", 7826 "label": "Trustpilot", 7827 "kind": "text", 7828 "tab": "content", 7829 "hint": "Your Trustpilot review link, or just your domain, like example.com." 7830 }, 7831 { 7832 "key": "g2", 7833 "label": "G2", 7834 "kind": "text", 7835 "tab": "content" 7836 }, 7837 { 7838 "key": "capterra", 7839 "label": "Capterra", 7840 "kind": "text", 7841 "tab": "content" 7842 }, 7843 { 7844 "key": "facebook", 7845 "label": "Facebook", 7846 "kind": "text", 7847 "tab": "content" 7848 }, 7849 { 7850 "key": "customName", 7851 "label": "Other platform name", 7852 "kind": "text", 7853 "tab": "content" 7854 }, 7855 { 7856 "key": "customUrl", 7857 "label": "Other platform link", 7858 "kind": "text", 7859 "tab": "content", 7860 "hint": "A full link. It may contain a visitor variable, like a store\u2019s own review link." 7861 }, 7862 { 7863 "key": "layout", 7864 "label": "Layout", 7865 "kind": "select", 7866 "tab": "style", 7867 "options": [ 7868 [ 7869 "", 7870 "Stacked" 7871 ], 7872 [ 7873 "row", 7874 "Side by side" 7875 ] 7876 ] 7877 }, 7878 { 7879 "key": "bg", 7880 "label": "Button background", 7881 "kind": "color", 7882 "tab": "style" 7883 }, 7884 { 7885 "key": "color", 7886 "label": "Button text color", 7887 "kind": "color", 7888 "tab": "style" 7889 }, 7890 { 7891 "key": "radius", 7892 "label": "Corner radius", 7893 "kind": "number", 7894 "tab": "style", 7895 "unit": "px", 7896 "min": 0, 7897 "max": 999 7898 }, 7899 { 7900 "key": "align", 7901 "label": "Alignment", 7902 "kind": "align", 7903 "tab": "style" 7904 } 7905 ] 7906 }, 7907 { 7908 "id": "slot", 7909 "label": "Personyze Placeholder", 7910 "html": '<div class="pz-slot pz-slot-slot-1"></div>', 7911 "props": { 7912 "anim": "", 7913 "device": "", 7914 "cssMobile": "", 7915 "beside": "", 7916 "width": "", 7917 "boxAlign": "center", 7918 "spaceTop": "", 7919 "spaceBottom": "", 7920 "boxBg": "", 7921 "boxPad": "", 7922 "boxBorder": 0, 7923 "boxBorderColor": "#E2E8F0", 7924 "boxCorner": "", 7925 "boxShadow": "", 7926 "css": "", 7927 "name": "slot-1", 7928 "minHeight": 0 7929 }, 7930 "settings": [ 7931 { 7932 "key": "name", 7933 "label": "Placeholder name", 7934 "kind": "text", 7935 "tab": "content", 7936 "hint": "Create a container with the CSS selector shown below, then point another action at that container." 7937 }, 7938 { 7939 "key": "minHeight", 7940 "label": "Reserve height", 7941 "kind": "number", 7942 "tab": "style", 7943 "hint": "Holds the space open before the other action arrives, so the page does not jump.", 7944 "unit": "px", 7945 "min": 0, 7946 "max": 2e3 7947 } 7948 ] 7949 }, 7950 { 7951 "id": "form", 7952 "label": "Form", 7953 "html": '<div style="max-width:420px; margin-left:auto; margin-right:auto;"><div class="$cform pzbform" ontplsubmit="var a=arguments;personyze.push(['Submit','${action_id:html}',this._get_value_ex(),a[1],a[2]]);return true;" ontplaftersubmit="var r=this.querySelector('.pz-form-thanks'); if (r) r.style.display='';var f=this.querySelectorAll('.pz-form-field,.pz-form-submit'); for (var i=0;i<f.length;i++) f[i].style.display='none';" style="margin:14px 0; text-align:left;"><div class="pz-form-field" style="margin-bottom:10px;"><label style="display:block; font-size:12px; font-weight:600; color:#334155; margin-bottom:4px;">Email *</label><input form="pzbform" name="email" type="email" required="1" data-prop-column="email" placeholder="[email protected]" style="width:100%; box-sizing:border-box; padding:9px 11px; border:1px solid #CBD5E1; border-radius:6px; font:inherit; font-size:14px;"></div>
7953<button form="pzbform" type="submit" class="pz-form-submit" style="text-align:center; margin:0; text-transform:none; letter-spacing:normal; background:#DB2777; color:#fff; border:0; box-sizing:border-box; width:100%; padding:11px 18px; border-radius:8px; font:inherit; font-size:14px; font-weight:700; cursor:pointer;">Subscribe</button><div class="pz-form-thanks" style="display:none; text-align:center; padding:14px 0; font-size:14px; color:#334155;">Thank you!</div></div></div>', 7954 "props": { 7955 "anim": "", 7956 "device": "", 7957 "cssMobile": "", 7958 "beside": "", 7959 "width": "420px", 7960 "boxAlign": "center", 7961 "spaceTop": "", 7962 "spaceBottom": "", 7963 "boxBg": "", 7964 "boxPad": "", 7965 "boxBorder": 0, 7966 "boxBorderColor": "#E2E8F0", 7967 "boxCorner": "", 7968 "boxShadow": "", 7969 "hoverBg": "", 7970 "hoverText": "", 7971 "report": "", 7972 "reportEvent": "", 7973 "reportValue": "", 7974 "css": "", 7975 "fields": '[{"label":"Email","column":"email","type":"email","placeholder":"[email protected]","required":true}]', 7976 "submitText": "Subscribe", 7977 "layout": "stacked", 7978 "submitBg": "#DB2777", 7979 "submitColor": "#fff", 7980 "thanksText": "Thank you!", 7981 "prefill": "", 7982 "after": "message", 7983 "redirectUrl": "", 7984 "redirectDelay": 2, 7985 "afterJs": "" 7986 }, 7987 "settings": [ 7988 { 7989 "key": "fields", 7990 "label": "Fields", 7991 "kind": "form_fields", 7992 "tab": "content", 7993 "hint": "Each field saves into the visitor\u2019s profile column you pick; submissions appear in the Forms report." 7994 }, 7995 { 7996 "key": "layout", 7997 "label": "Field layout", 7998 "kind": "select", 7999 "tab": "style", 8000 "hint": "Side by side is the signup-bar shape; it wraps to stacked on phones.", 8001 "options": [ 8002 [ 8003 "stacked", 8004 "Stacked \u2014 one field per row" 8005 ], 8006 [ 8007 "inline", 8008 "Side by side \u2014 fields and button on one row" 8009 ] 8010 ] 8011 }, 8012 { 8013 "key": "submitText", 8014 "label": "Submit button label", 8015 "kind": "text", 8016 "tab": "content" 8017 }, 8018 { 8019 "key": "thanksText", 8020 "label": "Thank-you message", 8021 "kind": "text", 8022 "tab": "content", 8023 "hint": "Replaces the fields after a successful submit." 8024 }, 8025 { 8026 "key": "prefill", 8027 "label": "Fill in what we already know", 8028 "kind": "checkbox", 8029 "tab": "content", 8030 "hint": "A field whose profile column already has a value for this visitor arrives filled in. Empty for anyone we do not know." 8031 }, 8032 { 8033 "key": "after", 8034 "label": "After a successful submit", 8035 "kind": "select", 8036 "tab": "content", 8037 "options": [ 8038 [ 8039 "message", 8040 "Show the thank-you message" 8041 ], 8042 [ 8043 "redirect", 8044 "Send them to a page" 8045 ], 8046 [ 8047 "close", 8048 "Close the popup" 8049 ] 8050 ] 8051 }, 8052 { 8053 "key": "redirectUrl", 8054 "label": "Page to send them to", 8055 "kind": "url", 8056 "tab": "content" 8057 }, 8058 { 8059 "key": "redirectDelay", 8060 "label": "Wait before leaving", 8061 "kind": "number", 8062 "tab": "content", 8063 "hint": "Long enough to read the thank-you. Zero leaves at once.", 8064 "unit": "s", 8065 "min": 0, 8066 "max": 30 8067 }, 8068 { 8069 "key": "afterJs", 8070 "label": "Run JavaScript after submit", 8071 "kind": "source_javascript", 8072 "tab": "content", 8073 "hint": "Receives (form, values). For a conversion pixel, a dataLayer push, your own redirect." 8074 }, 8075 { 8076 "key": "submitBg", 8077 "label": "Button background", 8078 "kind": "color", 8079 "tab": "style" 8080 }, 8081 { 8082 "key": "submitColor", 8083 "label": "Button text", 8084 "kind": "color", 8085 "tab": "style" 8086 }, 8087 { 8088 "key": "hoverBg", 8089 "label": "Background on hover", 8090 "kind": "color", 8091 "tab": "style", 8092 "hint": "What it changes to under the mouse. Blank keeps it still." 8093 }, 8094 { 8095 "key": "hoverText", 8096 "label": "Text on hover", 8097 "kind": "color", 8098 "tab": "style" 8099 }, 8100 { 8101 "key": "report", 8102 "label": "When clicked, report", 8103 "kind": "select", 8104 "tab": "content", 8105 "hint": "Beside the click itself, which is always counted.", 8106 "options": [ 8107 [ 8108 "", 8109 "Nothing extra" 8110 ], 8111 [ 8112 "goal", 8113 "This action reached its goal" 8114 ], 8115 [ 8116 "event", 8117 "A conversion goal" 8118 ], 8119 [ 8120 "both", 8121 "Both" 8122 ] 8123 ] 8124 }, 8125 { 8126 "key": "reportEvent", 8127 "label": "Which goal", 8128 "kind": "goal", 8129 "tab": "content", 8130 "hint": "Event-driven goals only \u2014 the kind that waits for a named event. Other goal types watch the page themselves and need nothing here." 8131 }, 8132 { 8133 "key": "reportValue", 8134 "label": "Value", 8135 "kind": "text", 8136 "tab": "content", 8137 "hint": "Optional \u2014 a revenue amount, a plan name. Variables work here." 8138 } 8139 ] 8140 }, 8141 { 8142 "id": "chat", 8143 "label": "AI Chat", 8144 "html": '<div class="pz-block-chat" data-pz-chat="{}" style="min-height:520px;"></div>', 8145 "props": { 8146 "anim": "", 8147 "device": "", 8148 "cssMobile": "", 8149 "beside": "", 8150 "width": "", 8151 "boxAlign": "center", 8152 "spaceTop": "", 8153 "spaceBottom": "", 8154 "boxBg": "", 8155 "boxPad": "", 8156 "boxBorder": 0, 8157 "boxBorderColor": "#E2E8F0", 8158 "boxCorner": "", 8159 "boxShadow": "", 8160 "css": "", 8161 "minHeight": 520, 8162 "withBubble": "", 8163 "accent": "", 8164 "greeting": "", 8165 "quick": "", 8166 "title": "", 8167 "placeholder": "", 8168 "send": "", 8169 "disclaimer": "", 8170 "show_notice": "", 8171 "ui_lang": "", 8172 "show_home": "", 8173 "home_title": "", 8174 "home_text": "", 8175 "follow_ups": "", 8176 "answer_feedback": "", 8177 "search_only": "", 8178 "knowledge_tag": "", 8179 "knowledge_miss": "wider", 8180 "catalogue_source": "", 8181 "show_items": "", 8182 "allow_human": "1", 8183 "meeting_offer": "", 8184 "own_instructions": "", 8185 "own_never": "", 8186 "event_started": "", 8187 "event_email": "", 8188 "event_human": "", 8189 "custom_css": "" 8190 }, 8191 "settings": [ 8192 { 8193 "key": "greeting", 8194 "label": "Greeting", 8195 "kind": "textarea", 8196 "tab": "content", 8197 "hint": "The first line the visitor reads. Blank follows the assistant." 8198 }, 8199 { 8200 "key": "quick", 8201 "label": "Quick questions", 8202 "kind": "textarea", 8203 "tab": "content", 8204 "hint": "One per line. They appear as buttons before the visitor types anything." 8205 }, 8206 { 8207 "key": "title", 8208 "label": "Header text", 8209 "kind": "text", 8210 "tab": "content", 8211 "hint": "From the assistant" 8212 }, 8213 { 8214 "key": "placeholder", 8215 "label": "Message box", 8216 "kind": "text", 8217 "tab": "content", 8218 "hint": "From the assistant" 8219 }, 8220 { 8221 "key": "send", 8222 "label": "Send button", 8223 "kind": "text", 8224 "tab": "content", 8225 "hint": "From the assistant" 8226 }, 8227 { 8228 "key": "disclaimer", 8229 "label": "Small print", 8230 "kind": "text", 8231 "tab": "content", 8232 "hint": "A placement may reword the AI disclosure, never remove it." 8233 }, 8234 { 8235 "key": "show_notice", 8236 "label": "Show the small print", 8237 "kind": "select", 8238 "tab": "content", 8239 "options": [ 8240 [ 8241 "", 8242 "From the assistant" 8243 ], 8244 [ 8245 "1", 8246 "Yes" 8247 ], 8248 [ 8249 "0", 8250 "No" 8251 ] 8252 ] 8253 }, 8254 { 8255 "key": "ui_lang", 8256 "label": "The chat's own words", 8257 "kind": "select", 8258 "tab": "content", 8259 "options": [ 8260 [ 8261 "", 8262 "From the assistant" 8263 ], 8264 [ 8265 "en", 8266 "English" 8267 ], 8268 [ 8269 "page", 8270 "The page's language" 8271 ] 8272 ] 8273 }, 8274 { 8275 "key": "show_home", 8276 "label": "Open on a home screen", 8277 "kind": "select", 8278 "tab": "content", 8279 "options": [ 8280 [ 8281 "", 8282 "From the assistant" 8283 ], 8284 [ 8285 "1", 8286 "Yes" 8287 ], 8288 [ 8289 "0", 8290 "No" 8291 ] 8292 ] 8293 }, 8294 { 8295 "key": "home_title", 8296 "label": "Headline", 8297 "kind": "text", 8298 "tab": "content", 8299 "hint": "From the assistant" 8300 }, 8301 { 8302 "key": "home_text", 8303 "label": "Line under it", 8304 "kind": "text", 8305 "tab": "content", 8306 "hint": "From the assistant" 8307 }, 8308 { 8309 "key": "follow_ups", 8310 "label": "Suggest what to ask next", 8311 "kind": "select", 8312 "tab": "content", 8313 "options": [ 8314 [ 8315 "", 8316 "From the assistant" 8317 ], 8318 [ 8319 "1", 8320 "Yes" 8321 ], 8322 [ 8323 "0", 8324 "No" 8325 ] 8326 ] 8327 }, 8328 { 8329 "key": "answer_feedback", 8330 "label": "Thumbs up and down on each answer", 8331 "kind": "select", 8332 "tab": "content", 8333 "options": [ 8334 [ 8335 "", 8336 "From the assistant" 8337 ], 8338 [ 8339 "1", 8340 "Yes" 8341 ], 8342 [ 8343 "0", 8344 "No" 8345 ] 8346 ] 8347 }, 8348 { 8349 "key": "accent", 8350 "label": "Colour", 8351 "kind": "color", 8352 "tab": "style", 8353 "hint": "From the assistant" 8354 }, 8355 { 8356 "key": "minHeight", 8357 "label": "Reserve height", 8358 "kind": "number", 8359 "tab": "style", 8360 "hint": "Holds the space open until the chat mounts, so the page does not jump.", 8361 "unit": "px", 8362 "min": 0, 8363 "max": 2e3 8364 }, 8365 { 8366 "key": "knowledge_tag", 8367 "label": "Answer from", 8368 "kind": "knowledge_tag", 8369 "tab": "content", 8370 "hint": "Sources carrying one tag, instead of everything. Tag them on the Knowledge screen \u2014 the same tags campaigns and actions use." 8371 }, 8372 { 8373 "key": "knowledge_miss", 8374 "label": "When it is asked something outside that", 8375 "kind": "select", 8376 "tab": "content", 8377 "options": [ 8378 [ 8379 "wider", 8380 "Answer from the whole knowledge base" 8381 ], 8382 [ 8383 "wider_say", 8384 "Answer from the whole knowledge base, and say it is outside what this box covers" 8385 ], 8386 [ 8387 "say", 8388 "Say it cannot help with that here" 8389 ] 8390 ] 8391 }, 8392 { 8393 "key": "catalogue_source", 8394 "label": "Search which catalogue", 8395 "kind": "catalogue_source", 8396 "tab": "content", 8397 "hint": "One product feed instead of all of them. A shop that imports a feed per country can point this placement at the one its visitors read \u2014 so a Spanish page searches Spanish titles and never offers a product named in another language. Items that came from no feed at all are always included; they are yours rather than any storefront's." 8398 }, 8399 { 8400 "key": "allow_human", 8401 "label": "Can it hand over to a person?", 8402 "kind": "select", 8403 "tab": "content", 8404 "hint": 'With this off the assistant never offers a person and never opens a case, whatever the visitor asks. Pair it with "When it does not know \u2192 take an email address instead" on the Behaviour screen, so somebody who needs a human still leaves you a way to reach them.', 8405 "options": [ 8406 [ 8407 "1", 8408 "Yes \u2014 pass them to the team" 8409 ], 8410 [ 8411 "0", 8412 "No \u2014 take an email and reply later" 8413 ] 8414 ] 8415 }, 8416 { 8417 "key": "own_instructions", 8418 "label": "Extra instructions", 8419 "kind": "textarea", 8420 "tab": "content", 8421 "hint": "Added to the assistant\u2019s own instructions, here only. It can make this placement stricter, never looser." 8422 }, 8423 { 8424 "key": "own_never", 8425 "label": "It must never", 8426 "kind": "textarea", 8427 "tab": "content", 8428 "hint": "One rule per line, for this placement only." 8429 }, 8430 { 8431 "key": "show_items", 8432 "label": "How it replies", 8433 "kind": "select", 8434 "tab": "content", 8435 "options": [ 8436 [ 8437 "", 8438 "An answer, with what it found underneath" 8439 ], 8440 [ 8441 "text", 8442 "Text only \u2014 no cards" 8443 ] 8444 ] 8445 }, 8446 { 8447 "key": "meeting_offer", 8448 "label": "Offering a meeting", 8449 "kind": "select", 8450 "tab": "content", 8451 "options": [ 8452 [ 8453 "", 8454 "From the assistant" 8455 ], 8456 [ 8457 "off", 8458 "Do not offer meetings on these pages" 8459 ] 8460 ] 8461 }, 8462 { 8463 "key": "event_started", 8464 "label": "When a conversation starts", 8465 "kind": "text", 8466 "tab": "content", 8467 "hint": "e.g. chat_started" 8468 }, 8469 { 8470 "key": "event_email", 8471 "label": "When a visitor gives their email", 8472 "kind": "text", 8473 "tab": "content", 8474 "hint": "e.g. chat_lead" 8475 }, 8476 { 8477 "key": "event_human", 8478 "label": "When it is passed to a person", 8479 "kind": "text", 8480 "tab": "content", 8481 "hint": "e.g. chat_handover" 8482 }, 8483 { 8484 "key": "withBubble", 8485 "label": "May share a page with another chat.", 8486 "kind": "checkbox", 8487 "tab": "style", 8488 "hint": "Off by default: two chats open on one page is a question a visitor should not have to answer." 8489 }, 8490 { 8491 "key": "custom_css", 8492 "label": "Extra CSS", 8493 "kind": "textarea", 8494 "tab": "content" 8495 } 8496 ] 8497 }, 8498 { 8499 "id": "ask", 8500 "label": "AI Search", 8501 "html": '<div class="pz-block-chat pz-block-ask" data-pz-chat="{"no_human":1,"ask":1}"></div>', 8502 "props": { 8503 "anim": "", 8504 "device": "", 8505 "cssMobile": "", 8506 "beside": "", 8507 "width": "", 8508 "boxAlign": "center", 8509 "spaceTop": "", 8510 "spaceBottom": "", 8511 "boxBg": "", 8512 "boxPad": "", 8513 "boxBorder": 0, 8514 "boxBorderColor": "#E2E8F0", 8515 "boxCorner": "", 8516 "boxShadow": "", 8517 "css": "", 8518 "withBubble": "", 8519 "accent": "", 8520 "ask_placeholder": "", 8521 "ask_button": "", 8522 "ask_note": "", 8523 "results_only": "", 8524 "search_only": "", 8525 "knowledge_tag": "", 8526 "knowledge_miss": "wider", 8527 "catalogue_source": "", 8528 "show_items": "", 8529 "allow_human": "", 8530 "own_instructions": "", 8531 "own_never": "" 8532 }, 8533 "settings": [ 8534 { 8535 "key": "ask_placeholder", 8536 "label": "Ask", 8537 "kind": "text", 8538 "tab": "content", 8539 "hint": "What the empty box says. It has to explain itself before anybody types in it." 8540 }, 8541 { 8542 "key": "ask_button", 8543 "label": "Button label", 8544 "kind": "text", 8545 "tab": "content", 8546 "hint": "From the assistant" 8547 }, 8548 { 8549 "key": "ask_note", 8550 "label": "Small print", 8551 "kind": "text", 8552 "tab": "content", 8553 "hint": "The line under the box. It may be reworded, never removed \u2014 this is where it says an assistant answers, not a search engine." 8554 }, 8555 { 8556 "key": "results_only", 8557 "label": "What happens when somebody asks", 8558 "kind": "select", 8559 "tab": "content", 8560 "options": [ 8561 [ 8562 "", 8563 "It answers the question" 8564 ], 8565 [ 8566 "1", 8567 "It just lists what it found \u2014 no answer" 8568 ] 8569 ] 8570 }, 8571 { 8572 "key": "acc
8572ent", 8573 "label": "Colour", 8574 "kind": "color", 8575 "tab": "style", 8576 "hint": "From the assistant" 8577 }, 8578 { 8579 "key": "knowledge_tag", 8580 "label": "Answer from", 8581 "kind": "knowledge_tag", 8582 "tab": "content", 8583 "hint": "Sources carrying one tag, instead of everything. Tag them on the Knowledge screen \u2014 the same tags campaigns and actions use." 8584 }, 8585 { 8586 "key": "knowledge_miss", 8587 "label": "When it is asked something outside that", 8588 "kind": "select", 8589 "tab": "content", 8590 "options": [ 8591 [ 8592 "wider", 8593 "Answer from the whole knowledge base" 8594 ], 8595 [ 8596 "wider_say", 8597 "Answer from the whole knowledge base, and say it is outside what this box covers" 8598 ], 8599 [ 8600 "say", 8601 "Say it cannot help with that here" 8602 ] 8603 ] 8604 }, 8605 { 8606 "key": "catalogue_source", 8607 "label": "Search which catalogue", 8608 "kind": "catalogue_source", 8609 "tab": "content", 8610 "hint": "One product feed instead of all of them. A shop that imports a feed per country can point this placement at the one its visitors read \u2014 so a Spanish page searches Spanish titles and never offers a product named in another language. Items that came from no feed at all are always included; they are yours rather than any storefront's." 8611 }, 8612 { 8613 "key": "allow_human", 8614 "label": "Can it hand over to a person?", 8615 "kind": "select", 8616 "tab": "content", 8617 "hint": "A search box that quietly turns into a support queue is not what the page put there, so this is off unless you switch it on. The assistant simply keeps answering \u2014 and if somebody really wants a person, a chat placement is the thing to give them.", 8618 "options": [ 8619 [ 8620 "", 8621 "No \u2014 it answers, and that is all" 8622 ], 8623 [ 8624 "1", 8625 "Yes \u2014 it can pass the visitor to the team" 8626 ] 8627 ] 8628 }, 8629 { 8630 "key": "own_instructions", 8631 "label": "Extra instructions", 8632 "kind": "textarea", 8633 "tab": "content", 8634 "hint": "Added to the assistant\u2019s own instructions, here only. It can make this placement stricter, never looser." 8635 }, 8636 { 8637 "key": "own_never", 8638 "label": "It must never", 8639 "kind": "textarea", 8640 "tab": "content", 8641 "hint": "One rule per line, for this placement only." 8642 }, 8643 { 8644 "key": "show_items", 8645 "label": "How it replies", 8646 "kind": "select", 8647 "tab": "content", 8648 "options": [ 8649 [ 8650 "", 8651 "An answer, with what it found underneath" 8652 ], 8653 [ 8654 "text", 8655 "Text only \u2014 no cards" 8656 ] 8657 ] 8658 }, 8659 { 8660 "key": "withBubble", 8661 "label": "May share a page with another chat.", 8662 "kind": "checkbox", 8663 "tab": "style", 8664 "hint": "Off by default: two chats open on one page is a question a visitor should not have to answer." 8665 } 8666 ] 8667 }, 8668 { 8669 "id": "booking", 8670 "label": "Book a meeting", 8671 "html": '<div class="pz-block-booking" data-pz-booking="{}" style="min-height:460px;"></div>', 8672 "props": { 8673 "anim": "", 8674 "device": "", 8675 "cssMobile": "", 8676 "beside": "", 8677 "width": "", 8678 "boxAlign": "center", 8679 "spaceTop": "", 8680 "spaceBottom": "", 8681 "boxBg": "", 8682 "boxPad": "", 8683 "boxBorder": 0, 8684 "boxBorderColor": "#E2E8F0", 8685 "boxCorner": "", 8686 "boxShadow": "", 8687 "types": "", 8688 "look": "", 8689 "accent": "", 8690 "heading": "", 8691 "display": "inline", 8692 "buttonText": "", 8693 "minHeight": 460, 8694 "css": "" 8695 }, 8696 "settings": [ 8697 { 8698 "key": "types", 8699 "label": "Which meeting", 8700 "kind": "meeting_types", 8701 "tab": "content", 8702 "hint": "One meeting type goes straight to the times. Several lets the visitor choose first." 8703 }, 8704 { 8705 "key": "display", 8706 "label": "How it appears", 8707 "kind": "select", 8708 "tab": "content", 8709 "hint": "A panel in the page is booked far more often than one behind a button.", 8710 "options": [ 8711 [ 8712 "inline", 8713 "In the design" 8714 ], 8715 [ 8716 "button", 8717 "Behind a button" 8718 ] 8719 ] 8720 }, 8721 { 8722 "key": "buttonText", 8723 "label": "Button label", 8724 "kind": "text", 8725 "tab": "content", 8726 "hint": "From the account" 8727 }, 8728 { 8729 "key": "heading", 8730 "label": "Heading", 8731 "kind": "text", 8732 "tab": "content", 8733 "hint": "From the meeting type" 8734 }, 8735 { 8736 "key": "look", 8737 "label": "Look", 8738 "kind": "select", 8739 "tab": "style", 8740 "options": [ 8741 [ 8742 "", 8743 "From the account" 8744 ], 8745 [ 8746 "card", 8747 "Card" 8748 ], 8749 [ 8750 "minimal", 8751 "Minimal" 8752 ], 8753 [ 8754 "dark", 8755 "Dark" 8756 ], 8757 [ 8758 "compact", 8759 "Compact" 8760 ] 8761 ] 8762 }, 8763 { 8764 "key": "accent", 8765 "label": "Colour", 8766 "kind": "color", 8767 "tab": "style", 8768 "hint": "From the account" 8769 }, 8770 { 8771 "key": "minHeight", 8772 "label": "Reserve height", 8773 "kind": "number", 8774 "tab": "style", 8775 "hint": "Holds the space open until the times arrive, so the design does not jump under the cursor.", 8776 "unit": "px", 8777 "min": 0, 8778 "max": 2e3 8779 } 8780 ] 8781 } 8782]; 8783var WIDGET_GATES = { 8784 "form": "form", 8785 "chat": "chat", 8786 "ask": "chat", 8787 "booking": "meetings" 8788}; 8789var FA_FONT = "PersonyzeFA47"; 8790var ICON_LIBRARY = [ 8791 [ 8792 "shopping-cart", 8793 "f07a" 8794 ], 8795 [ 8796 "cart-plus", 8797 "f217" 8798 ], 8799 [ 8800 "cart-arrow-down", 8801 "f218" 8802 ], 8803 [ 8804 "shopping-bag", 8805 "f290" 8806 ], 8807 [ 8808 "credit-card", 8809 "f09d" 8810 ], 8811 [ 8812 "tag", 8813 "f02b" 8814 ], 8815 [ 8816 "tags", 8817 "f02c" 8818 ], 8819 [ 8820 "gift", 8821 "f06b" 8822 ], 8823 [ 8824 "user", 8825 "f007" 8826 ], 8827 [ 8828 "users", 8829 "f0c0" 8830 ], 8831 [ 8832 "heart", 8833 "f004" 8834 ], 8835 [ 8836 "heart-o", 8837 "f08a" 8838 ], 8839 [ 8840 "star", 8841 "f005" 8842 ], 8843 [ 8844 "star-o", 8845 "f006" 8846 ], 8847 [ 8848 "bookmark", 8849 "f02e" 8850 ], 8851 [ 8852 "bookmark-o", 8853 "f097" 8854 ], 8855 [ 8856 "bell", 8857 "f0f3" 8858 ], 8859 [ 8860 "bell-o", 8861 "f0a2" 8862 ], 8863 [ 8864 "fire", 8865 "f06d" 8866 ], 8867 [ 8868 "bolt", 8869 "f0e7" 8870 ], 8871 [ 8872 "check", 8873 "f00c" 8874 ], 8875 [ 8876 "check-circle", 8877 "f058" 8878 ], 8879 [ 8880 "times", 8881 "f00d" 8882 ], 8883 [ 8884 "times-circle", 8885 "f057" 8886 ], 8887 [ 8888 "arrow-right", 8889 "f061" 8890 ], 8891 [ 8892 "arrow-left", 8893 "f060" 8894 ], 8895 [ 8896 "angle-right", 8897 "f105" 8898 ], 8899 [ 8900 "angle-left", 8901 "f104" 8902 ], 8903 [ 8904 "chevron-right", 8905 "f054" 8906 ], 8907 [ 8908 "chevron-left", 8909 "f053" 8910 ], 8911 [ 8912 "search", 8913 "f002" 8914 ], 8915 [ 8916 "envelope", 8917 "f0e0" 8918 ], 8919 [ 8920 "phone", 8921 "f095" 8922 ], 8923 [ 8924 "clock-o", 8925 "f017" 8926 ], 8927 [ 8928 "calendar", 8929 "f073" 8930 ], 8931 [ 8932 "map-marker", 8933 "f041" 8934 ], 8935 [ 8936 "info-circle", 8937 "f05a" 8938 ], 8939 [ 8940 "question-circle", 8941 "f059" 8942 ], 8943 [ 8944 "exclamation-circle", 8945 "f06a" 8946 ], 8947 [ 8948 "thumbs-up", 8949 "f164" 8950 ], 8951 [ 8952 "home", 8953 "f015" 8954 ], 8955 [ 8956 "cog", 8957 "f013" 8958 ], 8959 [ 8960 "lock", 8961 "f023" 8962 ], 8963 [ 8964 "eye", 8965 "f06e" 8966 ], 8967 [ 8968 "download", 8969 "f019" 8970 ], 8971 [ 8972 "external-link", 8973 "f08e" 8974 ], 8975 [ 8976 "play", 8977 "f04b" 8978 ], 8979 [ 8980 "paper-plane", 8981 "f1d8" 8982 ], 8983 [ 8984 "facebook", 8985 "f09a" 8986 ], 8987 [ 8988 "twitter", 8989 "f099" 8990 ], 8991 [ 8992 "instagram", 8993 "f16d" 8994 ], 8995 [ 8996 "whatsapp", 8997 "f232" 8998 ] 8999]; 9000
9001// src/components/live/liveTiles.ts 9002var INSTANT_TOOLS = [ 9003 "duplicate", 9004 "delete", 9005 "action-click" 9006]; 9007var TABS = [ 9008 { 9009 id: "basics", 9010 label: "Basics", 9011 title: "Core actions - edit text, style, duplicate, delete" 9012 }, 9013 { 9014 id: "layout", 9015 label: "Layout", 9016 title: "Size, spacing, alignment, position" 9017 }, 9018 { 9019 id: "insert", 9020 label: "Insert widget", 9021 title: "Add something to the page - a countdown, a coupon, a form field, an image" 9022 }, 9023 { 9024 id: "behavior", 9025 label: "Behavior", 9026 title: "Interactions - on-click actions, delay" 9027 }, 9028 { 9029 id: "tracking", 9030 label: "Tracking", 9031 title: "Analytics - click reporting, custom events" 9032 } 9033]; 9034var TILES = [ 9035 { 9036 tool: "text", 9037 tab: "basics", 9038 icon: "pencil", 9039 title: "Edit text", 9040 sub: "Type right on the page", 9041 key: "E" 9042 }, 9043 { 9044 tool: "rich", 9045 tab: "basics", 9046 icon: "lines", 9047 title: "Rich editor", 9048 sub: "Bold, links, lists", 9049 key: "R" 9050 }, 9051 { 9052 tool: "style", 9053 tab: "basics", 9054 icon: "palette", 9055 title: "Style", 9056 sub: "Color, font, bg", 9057 key: "S" 9058 }, 9059 { 9060 tool: "replace-image", 9061 tab: "basics", 9062 icon: "image", 9063 title: "Replace image", 9064 sub: "New image URL" 9065 }, 9066 { 9067 tool: "props", 9068 tab: "basics", 9069 icon: "props", 9070 title: "Properties", 9071 sub: "Link, alt, tooltip, id" 9072 }, 9073 { 9074 tool: "duplicate", 9075 tab: "basics", 9076 icon: "copy", 9077 title: "Duplicate", 9078 sub: "Copy element", 9079 key: "D" 9080 }, 9081 // Backspace has no printable name, so it is not looked up through `key` like the others - 9082 // [[LiveTool]] matches the event itself. The label is here only to be drawn on the tile. 9083 { 9084 tool: "delete", 9085 tab: "basics", 9086 icon: "trash", 9087 title: "Delete", 9088 sub: "Remove element", 9089 key: "\u232B" 9090 }, 9091 { 9092 tool: "source", 9093 tab: "basics", 9094 icon: "code", 9095 title: "HTML source", 9096 sub: "Edit raw markup", 9097 key: "H" 9098 }, 9099 // Layout 9100 { 9101 tool: "move", 9102 tab: "layout", 9103 alsoOn: "basics", 9104 icon: "move", 9105 title: "Move & resize", 9106 sub: "Drag corners", 9107 key: "M" 9108 }, 9109 { 9110 tool: "spacing", 9111 tab: "layout", 9112 icon: "spacing", 9113 title: "Spacing", 9114 sub: "Margin, padding" 9115 }, 9116 { 9117 tool: "alignment", 9118 tab: "layout", 9119 icon: "align", 9120 title: "Alignment", 9121 sub: "Text, flex & grid" 9122 }, 9123 { 9124 tool: "position", 9125 tab: "layout", 9126 icon: "position", 9127 title: "Position", 9128 sub: "Z-index, pin, float" 9129 }, 9130 { 9131 tool: "visibility", 9132 tab: "layout", 9133 icon: "eye", 9134 title: "Visibility", 9135 sub: "Show or hide per device", 9136 tooltip: "Hide this element on phones, tablets or desktops. Written as a media query, so it follows the visitor's real screen width" 9137 }, 9138 // Insert 9139 { 9140 tool: "insert-image", 9141 tab: "insert", 9142 icon: "image", 9143 title: "Image", 9144 sub: "From URL" 9145 }, 9146 { 9147 tool: "insert-link", 9148 tab: "insert", 9149 icon: "link", 9150 title: "Link", 9151 sub: "Destination URL" 9152 }, 9153 /* NO PLAIN BUTTON TILE. It inserted a bare `<button>` with a label and nothing else; the 9154 builder's CTA Button is the same element with its colours, padding, radius, link and hover 9155 behind it, and it is in the widget list below. Jacob's call, and the reason is that one of 9156 them can be edited afterwards and the other cannot. The old change shape still applies, so 9157 buttons inserted before this keep working. */ 9158 { 9159 tool: "insert-block", 9160 tab: "insert", 9161 icon: "block", 9162 title: "Block", 9163 sub: "Text or message box" 9164 }, 9165 { 9166 tool: "insert-video", 9167 tab: "insert", 9168 icon: "video", 9169 title: "Video", 9170 sub: "MP4, audio, embed" 9171 }, 9172 { 9173 tool: "insert-html", 9174 tab: "insert", 9175 icon: "code", 9176 title: "Custom HTML", 9177 sub: "Raw snippet" 9178 }, 9179 /* THE READY-MADE WIDGETS, one tile each, beside Image and Link rather than behind a button 9180 that opens a list — a palette is something you scan. They share one icon on purpose: what 9181 distinguishes them is the name, and ten hand-drawn glyphs would be ten things to keep 9182 true. Generated from the catalogue, so a widget added to the block builder appears here 9183 without this file being touched. */ 9184 ...WIDGET_CATALOGUE.map((w3) => ({ 9185 tool: `insert-widget-${w3.id}`,
9186 tab: "insert", 9187 icon: "block", 9188 title: w3.label, 9189 sub: "Widget" 9190 })), 9191 /* Editing one that is already there. Offered on the Basics row for a widget rather than in 9192 Insert, because by then you are not adding anything — you clicked a countdown and want its 9193 end date. It leads that row for the same reason. */ 9194 { 9195 tool: "widget-edit", 9196 tab: "basics", 9197 icon: "block", 9198 title: "Widget settings", 9199 sub: "What it says and how it looks", 9200 primary: true 9201 }, 9202 // Behavior 9203 { 9204 tool: "onclick", 9205 tab: "behavior", 9206 icon: "bolt", 9207 title: "On click", 9208 sub: "Run JS, fire an action" 9209 }, 9210 { 9211 tool: "scroll-to", 9212 tab: "behavior", 9213 icon: "scroll", 9214 title: "Scroll to", 9215 sub: "Jump to another element", 9216 tooltip: "Clicking this element scrolls the page to somewhere else on it - what an anchor link does, without needing one" 9217 }, 9218 { 9219 tool: "delay", 9220 tab: "behavior", 9221 icon: "clock", 9222 title: "Delay execution", 9223 sub: "Wait for this element" 9224 }, 9225 // Tracking 9226 { 9227 tool: "action-click", 9228 tab: "tracking", 9229 icon: "checkbox", 9230 title: "Action click", 9231 sub: "Count in this action's stats", 9232 badge: "Action", 9233 tooltip: "Clicks on this element count toward this action's Click metric in the campaign's statistics report" 9234 }, 9235 { 9236 tool: "custom-event", 9237 tab: "tracking", 9238 icon: "chart", 9239 title: "Custom event", 9240 sub: "Trigger a goal or event" 9241 } 9242]; 9243var TAG_HUMAN_NAMES = { 9244 b: "Bold text", 9245 strong: "Bold text", 9246 i: "Italic text", 9247 u: "Underlined text", 9248 s: "Linethrough text", 9249 strike: "Linethrough text", 9250 em: "Emphasized text", 9251 a: "Hyperlink", 9252 img: "Image", 9253 p: "Paragraph", 9254 center: "Center block", 9255 table: "Table element", 9256 tr: "Table row", 9257 th: "Table header cell", 9258 td: "Table cell", 9259 caption: "Table caption", 9260 thead: "Table head group", 9261 h1: "Heading, level 1", 9262 h2: "Heading, level 2", 9263 h3: "Heading, level 3", 9264 h4: "Heading, level 4", 9265 h5: "Heading, level 5", 9266 h6: "Heading, level 6", 9267 ol: "Ordered list", 9268 ul: "Unordered list", 9269 dl: "Definition list", 9270 li: "List item", 9271 dt: "Definition term", 9272 dd: "Description", 9273 form: "Form", 9274 input: "Input element", 9275 select: "Dropdown list", 9276 textarea: "Multiline text input element", 9277 button: "Button", 9278 div: "Divider", 9279 span: "Span element", 9280 iframe: "Inline frame", 9281 embed: "Embedded plugin", 9282 object: "Embedded ActiveX object", 9283 video: "Video", 9284 audio: "Audio", 9285 canvas: "Canvas", 9286 svg: "Scalable Vector Graphics (SVG)" 9287}; 9288function tagHumanName(elem) { 9289 const tag = elem?.localName ?? ""; 9290 return TAG_HUMAN_NAMES[tag] ?? tag.toUpperCase(); 9291} 9292function canPutElem(elem, toElem) { 9293 if (!elem || !toElem || typeof elem != "string" && elem === toElem) { 9294 return { 9295 aside: false, 9296 inside: false 9297 }; 9298 } 9299 const elemName = "[" + (typeof elem == "string" ? elem : elem.localName) + "]"; 9300 const toElemName = "[" + toElem.localName + "]"; 9301 const available = { 9302 aside: true, 9303 inside: true 9304 }; 9305 switch (elemName) { 9306 case "[body]": 9307 available.aside = false;
9308 available.inside = false; 9309 break; 9310 case "[caption]": 9311 case "[thead]": 9312 case "[tbody]": 9313 case "[tfoot]": 9314 if (toElemName != "[table]") available.inside = false; 9315 if ("[caption][thead][tbody][tfoot]".indexOf(toElemName) == -1) available.aside = false; 9316 break; 9317 case "[tr]": 9318 if ("[caption][thead][tbody][tfoot]".indexOf(toElemName) == -1) available.inside = false; 9319 if (toElemName != "[tr]") available.aside = false; 9320 break; 9321 case "[th]": 9322 case "[td]": 9323 if (toElemName != "[tr]") available.inside = false; 9324 if ("[th][td]".indexOf(toElemName) == -1) available.aside = false; 9325 break; 9326 case "[li]": 9327 if ("[ul][ol][menu]".indexOf(toElemName) == -1) available.inside = false; 9328 if (toElemName != "[li]") available.aside = false; 9329 break; 9330 } 9331 switch (toElemName) { 9332 case "[body]": 9333 available.aside = false; 9334 break; 9335 case "[iframe]": 9336 case "[object]": 9337 case "[embed]": 9338 case "[video]": 9339 case "[audio]": 9340 case "[canvas]": 9341 case "[svg]": 9342 case "[img]": 9343 case "[hr]": 9344 case "[input]": 9345 case "[textarea]": 9346 case "[select]": 9347 available.inside = false;
9348 break; 9349 case "[table]": 9350 if ("[caption][thead][tbody][tfoot]".indexOf(elemName) == -1) available.inside = false; 9351 break; 9352 case "[caption]": 9353 case "[thead]": 9354 case "[tbody]": 9355 case "[tfoot]": 9356 if (elemName != "[tr]") available.inside = false; 9357 if ("[caption][thead][tbody][tfoot]".indexOf(elemName) == -1) available.aside = false; 9358 break; 9359 case "[tr]": 9360 if (elemName != "[tr]") available.aside = false; 9361 if ("[th][td]".indexOf(elemName) == -1) available.inside = false; 9362 break; 9363 case "[th]": 9364 case "[td]": 9365 if ("[th][td]".indexOf(elemName) == -1) available.aside = false; 9366 break; 9367 case "[ul]": 9368 case "[ol]": 9369 case "[menu]": 9370 if (elemName != "[li]") available.inside = false; 9371 break; 9372 } 9373 return available; 9374} 9375function editHtmlKind(elem) { 9376 if (!elem) { 9377 return 0; 9378 } 9379 const tag = elem.localName; 9380 if (tag == "table" || tag == "thead" || tag == "tbody" || tag == "tfoot" || tag == "tr" || tag == "iframe" || tag == "img" || tag == "hr" || tag == "br" || tag == "select" || tag == "textarea") { 9381 return 0; 9382 } 9383 if (tag == "input") { 9384 const type = elem.type; 9385 return type == "button" || type == "submit" || type == "reset" ? 0 : 2; 9386 } 9387 return 1; 9388} 9389function imageUrlOf(elem) { 9390 if (!elem) { 9391 return ""; 9392 } 9393 const src = elem.src; 9394 if (src) { 9395 return src; 9396 } 9397 const background = getComputedStyle(elem).backgroundImage || ""; 9398 const m2 = background.match(/^url\(["']?([^"')]*)/i); 9399 return m2 ? decodeURI(m2[1]) : ""; 9400} 9401function dragCapability(elem) { 9402 if (!elem || elem == document.body || elem == document.documentElement) { 9403 return { 9404 canDrag: false, 9405 canResize: false 9406 }; 9407 } 9408 const hasWidthProp = elem.width != null; 9409 const canResize = hasWidthProp || getComputedStyle(elem).display.indexOf("block") != -1; 9410 return { 9411 canDrag: true, 9412 canResize 9413 }; 9414} 9415function tileOnTab(tile, tab) { 9416 return tile.tab == tab || tile.alsoOn == tab; 9417} 9418function widgetOfTile(tool) { 9419 return tool.startsWith("insert-widget-") ? tool.slice("insert-widget-".length) : ""; 9420} 9421function availableTiles(elem, allow) { 9422 if (!elem) { 9423 return []; 9424 } 9425 const gateOpen = (gate) => !!allow?.[gate]; 9426 const htmlKind = editHtmlKind(elem); 9427 const hasImage = !!imageUrlOf(elem); 9428 const { canDrag } = dragCapability(elem); 9429 const canInsert = canPutElem("span", elem); 9430 const isBody = elem == document.body; 9431 return TILES.filter((tile) => { 9432 switch (tile.tool) { 9433 case "text": 9434 return htmlKind != 0; 9435 case "rich": 9436 case "source": 9437 return htmlKind == 1; 9438 case "replace-image": 9439 return hasImage; 9440 case "move": 9441 return canDrag; 9442 case "duplicate": 9443 case "delete": 9444 return !isBody; 9445 case "insert-image": 9446 case "insert-link": 9447 case "insert-button": 9448 case "insert-block": 9449 case "insert-video": 9450 case "insert-html": 9451 return canInsert.inside || canInsert.aside; 9452 default: { 9453 const widget = widgetOfTile(tile.tool); 9454 if (!widget) { 9455 return true; 9456 } 9457 const gate = WIDGET_GATES[widget]; 9458 return (!gate || gateOpen(gate)) && (canInsert.inside || canInsert.aside); 9459 } 9460 } 9461 }); 9462} 9463function elementType(elem) { 9464 if (!elem) { 9465 return ""; 9466 } 9467 const tag = elem.localName; 9468 if (tag == "img") return "image"; 9469 if (tag == "a") return "link"; 9470 if (tag == "button" || elem.matches('[role="button"]')) return "button"; 9471 if (tag == "input" || tag == "textarea" || tag == "select") return "field"; 9472 if (tag == "form") return "form"; 9473 if (tag == "video" || tag == "audio" || tag == "iframe") return "media"; 9474 if (/^h[1-6]$/.test(tag)) return "heading"; 9475 if (tag == "ul" || tag == "ol") return "list"; 9476 if (tag == "li") return "list-item"; 9477 if (tag == "p" || tag == "span" || tag == "strong" || tag == "em") return "text"; 9478 if (elem.children.length && (elem.textContent ?? "").trim().length < 200) return "container"; 9479 return "generic"; 9480} 9481var SMART_LABELS = { 9482 image: "Image", 9483 link: "Link", 9484 button: "Button", 9485 field: "Form field", 9486 form: "Form", 9487 media: "Media", 9488 heading: "Heading", 9489 list: "List", 9490 "list-item": "List item", 9491 text: "Text", 9492 container: "Container", 9493 generic: "Element" 9494}; 9495var SMART_BADGE_ICONS = { 9496 image: "image", 9497 link: "link", 9498 button: "checkbox", 9499 field: "text", 9500 form: "send", 9501 media: "play", 9502 heading: "heading", 9503 list: "list", 9504 "list-item": "list", 9505 text: "pencil", 9506 container: "move", 9507 generic: "palette" 9508}; 9509var SMART_TOOLS = { 9510 image: [ 9511 "replace-image", 9512 "props", 9513 "style", 9514 "move" 9515 ], 9516 link: [ 9517 "props", 9518 "text", 9519 "style", 9520 "onclick" 9521 ], 9522 button: [ 9523 "text", 9524 "onclick", 9525 "style", 9526 "action-click" 9527 ], 9528 field: [ 9529 "props", 9530 "style" 9531 ], 9532 form: [ 9533 "props", 9534 "onclick" 9535 ], 9536 media: [ 9537 "props", 9538 "style", 9539 "move" 9540 ], 9541 heading: [ 9542 "text", 9543 "style",
9544 "source" 9545 ], 9546 text: [ 9547 "text", 9548 "rich", 9549 "style" 9550 ], 9551 list: [ 9552 "source", 9553 "style", 9554 "insert-block" 9555 ], 9556 "list-item": [ 9557 "text", 9558 "duplicate", 9559 "delete" 9560 ], 9561 container: [ 9562 "style", 9563 "spacing", 9564 "duplicate" 9565 ], 9566 generic: [ 9567 "text", 9568 "style", 9569 "duplicate", 9570 "delete" 9571 ] 9572}; 9573function widgetHostOf(elem) { 9574 for (let e3 = elem; e3; e3 = e3.parentElement) { 9575 if (e3.getAttribute?.("data-pz-widget")) { 9576 return e3; 9577 } 9578 } 9579 return null; 9580} 9581function smartRowFor(elem, allow) { 9582 const type = elementType(elem); 9583 if (!type) { 9584 return null; 9585 } 9586 const allowed = new Set(availableTiles(elem, allow).map((v3) => v3.tool)); 9587 const tiles = new Array(); 9588 if (widgetHostOf(elem)) { 9589 const wt = TILES.find((v3) => v3.tool == "widget-edit"); 9590 if (wt) { 9591 tiles.push(wt); 9592 } 9593 } 9594 for (const tool of SMART_TOOLS[type] ?? SMART_TOOLS.generic) { 9595 const tile = TILES.find((v3) => v3.tool == tool); 9596 if (tile && allowed.has(tool)) { 9597 tiles.push(tile); 9598 } 9599 } 9600 return { 9601 label: SMART_LABELS[type] ?? "Element", 9602 badgeIcon: SMART_BADGE_ICONS[type] ?? "palette", 9603 tiles 9604 }; 9605} 9606
9607// src/modules/jsapi/private/jsapi.ts 9608var N_ATTEMPTS = 3; 9609var CACHE_MAX_QUERIES = 32; 9610var MAX_URLENCODED_DATA = 7 * 1024; 9611var config = null; 9612var cache = /* @__PURE__ */ new Map(); 9613function configureJsapi(newConfig) { 9614 config = newConfig; 9615 cache.clear(); 9616} 9617function isJsapiConfigured() { 9618 return config != null; 9619} 9620function getJsapiServerId() { 9621 return config?.serverId ?? 0; 9622} 9623function getJsapiOrigin() { 9624 try { 9625 return config ? new URL(config.url, location.href).origin : ""; 9626 } catch { 9627 return ""; 9628 } 9629} 9630async function query(sql, params, noCache = false) { 9631 if (!config) { 9632 throw new Error("The Simulator has no database connection."); 9633 } 9634 const postQuery = { 9635 sql, 9636 params 9637 }; 9638 const post = JSON.stringify(postQuery); 9639 if (!noCache) { 9640 const hit = cache.get(post); 9641 if (hit) { 9642 return hit; 9643 } 9644 } 9645 const isBig = post.length > MAX_URLENCODED_DATA; 9646 const response = await fetchRetrying(config.url, { 9647 mode: "cors", 9648 credentials: "include", 9649 headers: { 9650 "Content-Type": isBig ? "application/json" : "application/x-www-form-urlencoded" 9651 }, 9652 method: "POST", 9653 body: isBig ? JSON.stringify({ 9654 data: [ 9655 postQuery 9656 ], 9657 server_id: config.serverId 9658 }) : `data=${encodeURIComponent("[" + post + "]")}&server_id=${config.serverId}` 9659 }); 9660 if (!response.ok) { 9661 throw new Error(response.statusText || `Database request failed (${response.status})`); 9662 } 9663 const resultsets = toResultsets(await response.text()); 9664 if (!noCache && resultsets.every((v3) => v3.fields)) { 9665 while (cache.size >= CACHE_MAX_QUERIES) { 9666 const oldest = cache.keys().next().value; 9667 if (oldest == void 0) { 9668 break; 9669 } 9670 cache.delete(oldest); 9671 } 9672 cache.set(post, resultsets); 9673 } 9674 return resultsets; 9675} 9676async function queryRow(sql, params, noCache = false) { 9677 const resultsets = await query(sql, params, noCache); 9678 return resultsets[0]?.[0]; 9679} 9680async function queryRows(sql, params, noCache = false) { 9681 const resultsets = await query(sql, params, noCache); 9682 return resultsets[0] ?? []; 9683} 9684async function fetchRetrying(url, init) { 9685 let lastError; 9686 for (let i4 = 0; i4 < N_ATTEMPTS; i4++) { 9687 try { 9688 return await fetch(url, init); 9689 } catch (e3) { 9690 lastError = e3; 9691 } 9692 } 9693 console.error(lastError); 9694 throw new Error("Failed to reach the Personyze server. Please check your network connection."); 9695} 9696function toResultsets(text) { 9697 const pos = text.indexOf("\n\n//"); 9698 const data = JSON.parse(pos == -1 ? text : text.slice(0, pos)); 9699 const raw = data?.[0]; 9700 if (raw?.error) { 9701 throw new Error(raw.error + ""); 9702 } 9703 if (!Array.isArray(raw)) { 9704 throw new Error("Unexpected answer from the Personyze server."); 9705 } 9706 const resultsets = new Array(); 9707 for (const rset of raw) { 9708 if (rset?.error) { 9709 throw new Error(rset.error + ""); 9710 } 9711 const fields = rset?.fields; 9712 const rows = rset?.rows; 9713 const converted = []; 9714 if (fields && rows) { 9715 for (const row of rows) { 9716 const obj = {}; 9717 for (let i4 = 0; i4 < fields.length; i4++) { 9718 obj[fields[i4]] = row[i4]; 9719 } 9720 converted.push(obj); 9721 } 9722 converted.fields = fields; 9723 } 9724 resultsets.push(converted); 9725 } 9726 return resultsets; 9727} 9728
9729// src/components/useAsync.ts 9730function useAsync(load, deps, onError) { 9731 const [data, setData] = p2(); 9732 const [isBusy, setIsBusy] = p2(true); 9733 const [tick, setTick] = p2(0); 9734 _2(() => { 9735 let isCurrent = true; 9736 setIsBusy(true); 9737 load().then((value) => { 9738 if (isCurrent) { 9739 setData(value); 9740 setIsBusy(false); 9741 } 9742 }, (error) => { 9743 if (isCurrent) { 9744 setIsBusy(false); 9745 onError(error); 9746 } 9747 }); 9748 return () => { 9749 isCurrent = false; 9750 }; 9751 }, [ 9752 ...deps, 9753 tick 9754 ]); 9755 return { 9756 data, 9757 isBusy, 9758 reload: () => setTick((v3) => v3 + 1) 9759 }; 9760} 9761
9762// src/modules/webaddress/mod.ts 9763var HAS_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i; 9764var OPAQUE_SCHEME = /^(?:mailto|tel|sms|data|javascript|file|blob|geo|whatsapp|skype|viber|callto):/i; 9765var BARE_HOST = /^[\w\-.]+\.[a-z]{2,}(?:[:/?#]|$)/i; 9766var BARE_LOCALHOST = /^localhost(?::\d+)?(?:[/?#]|$)/i; 9767var FILE_EXTENSION = /\.(?:html?|xht|xhtml|php\d?|asp|aspx|jsp|cgi|pl|py|rb|js|mjs|css|map|json|xml|rss|atom|txt|md|csv|tsv|pdf|zip|gz|png|jpe?g|gif|svg|webp|avif|ico|bmp|mp[34]|m4[av]|wav|ogg|webm|mov|avi|woff2?|ttf|otf|eot)$/i; 9768var CARRIES_VARIABLE = /\$\{|\{\{/; 9769function withScheme(raw) { 9770 const v3 = (raw ?? "").trim(); 9771 if (!v3 || HAS_SCHEME.test(v3) || OPAQUE_SCHEME.test(v3) || CARRIES_VARIABLE.test(v3)) { 9772 return v3; 9773 } 9774 if (v3.startsWith("//")) { 9775 return "https:" + v3; 9776 } 9777 if (/^[/?#.]/.test(v3) || /\s/.test(v3)) { 9778 return v3; 9779 } 9780 if (!/[/?#:]/.test(v3) && FILE_EXTENSION.test(v3)) { 9781 return v3; 9782 } 9783 return BARE_HOST.test(v3) || BARE_LOCALHOST.test(v3) ? "https://" + v3 : v3; 9784} 9785
9786// src/components/live/MediaPicker.tsx 9787var MEDIA_QUERY = "SELECT id, filename, filename_inner, thumbnail, mime_type FROM uploaded_media ORDER BY mtime DESC LIMIT 200"; 9788function mediaUrl(filename) { 9789 const origin = getJsapiOrigin(); 9790 const serverId = getJsapiServerId(); 9791 return !origin || !serverId || !filename ? "" : `${origin}/upload/${serverId}/${filename}`; 9792} 9793function MediaPicker(props) { 9794 const [search, setSearch] = p2(""); 9795 const [error, setError] = p2(""); 9796 const rows = useAsync(async () => !isJsapiConfigured() ? [] : await queryRows(MEDIA_QUERY), [], (e3) => { 9797 console.error(e3); 9798 setError(e3 instanceof Error ? e3.message : "Could not read the media library."); 9799 }); 9800 const shown = q2(() => { 9801 const term = search.trim().toLowerCase(); 9802 return (rows.data ?? []).filter((row) => (props.accept != "image" || String(row.mime_type ?? "").startsWith("image/")) && (!term || String(row.filename ?? "").toLowerCase().indexOf(term) != -1)); 9803 }, [ 9804 rows.data, 9805 search, 9806 props.accept 9807 ]); 9808 return /* @__PURE__ */ u2("div", { 9809 class: "le-root le-vd-overlay", 9810 onMouseDown: (ev) => { 9811 ev.stopPropagation(); 9812 if (ev.target === ev.currentTarget) { 9813 props.onCancel(); 9814 } 9815 }, 9816 children: /* @__PURE__ */ u2("div", { 9817 class: "le-vd", 9818 role: "dialog", 9819 "aria-modal": "true", 9820 "aria-label": "Media library", 9821 children: [
9822 /* @__PURE__ */ u2("div", { 9823 class: "le-vd-head", 9824 children: [ 9825 /* @__PURE__ */ u2("div", { 9826 children: [ 9827 /* @__PURE__ */ u2("h3", { 9828 children: [ 9829 li.image, 9830 " Media library" 9831 ] 9832 }), 9833 /* @__PURE__ */ u2("p", { 9834 children: "Files uploaded to this Personyze account. Upload new ones from the panel." 9835 }) 9836 ] 9837 }), 9838 /* @__PURE__ */ u2("button", { 9839 type: "button", 9840 class: "le-icon", 9841 title: "Close", 9842 onClick: props.onCancel, 9843 children: li.cross 9844 }) 9845 ] 9846 }), 9847 /* @__PURE__ */ u2("div", { 9848 class: "le-vp-search", 9849 children: /* @__PURE__ */ u2("input", { 9850 class: "le-input", 9851 type: "text", 9852 value: search, 9853 placeholder: "Search by file name...", 9854 onInput: (ev) => setSearch(ev.target.value) 9855 }) 9856 }),
9857 /* @__PURE__ */ u2("div", { 9858 class: "le-vd-body", 9859 children: error ? /* @__PURE__ */ u2("div", { 9860 class: "le-notice le-warn", 9861 children: [ 9862 li.warn, 9863 /* @__PURE__ */ u2("div", { 9864 children: error 9865 }) 9866 ] 9867 }) : rows.isBusy ? /* @__PURE__ */ u2("div", { 9868 class: "le-empty", 9869 children: /* @__PURE__ */ u2("p", { 9870 children: "Loading..." 9871 }) 9872 }) : shown.length == 0 ? /* @__PURE__ */ u2("div", { 9873 class: "le-empty", 9874 children: [ 9875 /* @__PURE__ */ u2("h4", { 9876 children: search ? "Nothing matches" : "No uploaded media" 9877 }), 9878 /* @__PURE__ */ u2("p", { 9879 children: search ? "Try a different name." : "Upload files from the Personyze panel, then they appear here." 9880 }) 9881 ] 9882 }) : /* @__PURE__ */ u2("div", { 9883 class: "le-media-grid", 9884 children: shown.map((row) => { 9885 const url = mediaUrl(row.filename_inner); 9886 const thumb = row.thumbnail ? mediaUrl(row.thumbnail) : url; 9887 return /* @__PURE__ */ u2("button", { 9888 type: "button", 9889 class: "le-media", 9890 title: row.filename + " - " + url, 9891 onClick: () => props.onPick(url), 9892 children: [
9893 /* @__PURE__ */ u2("span", { 9894 class: "le-media-thumb", 9895 children: String(row.mime_type ?? "").startsWith("image/") ? /* @__PURE__ */ u2("img", { 9896 src: thumb, 9897 alt: "", 9898 loading: "lazy" 9899 }) : li.video 9900 }), 9901 /* @__PURE__ */ u2("span", { 9902 class: "le-media-name", 9903 children: row.filename 9904 }) 9905 ] 9906 }, row.id); 9907 }) 9908 }) 9909 }), 9910 /* @__PURE__ */ u2("div", { 9911 class: "le-vd-foot", 9912 children: [ 9913 /* @__PURE__ */ u2("div", { 9914 class: "le-spacer" 9915 }), 9916 /* @__PURE__ */ u2("button", { 9917 type: "button", 9918 class: "le-btn", 9919 onClick: props.onCancel, 9920 children: "Cancel" 9921 }) 9922 ] 9923 }) 9924 ] 9925 }) 9926 }); 9927} 9928function MediaUrlInput(props) { 9929 const [isOpen, setIsOpen] = p2(false); 9930 return /* @__PURE__ */ u2(k, { 9931 children: [
9932 /* @__PURE__ */ u2("div", { 9933 class: "le-field-group", 9934 style: "flex:1; min-width:0", 9935 children: [ 9936 /* @__PURE__ */ u2("input", { 9937 class: "le-input", 9938 type: "text", 9939 style: "flex:1 1 8em; min-width:0", 9940 value: props.value, 9941 placeholder: props.placeholder ?? "https://...", 9942 onInput: (ev) => props.onChange(ev.target.value), 9943 /* Leaving the box supplies the scheme for anything that can only be a 9944 host. A relative path or a bare filename — both legitimate here, the 9945 page being edited is on the customer's own site — is left as typed; 9946 see the rule in ../../modules/webaddress/mod.ts. */ 9947 onBlur: (ev) => props.onChange(withScheme(ev.target.value)) 9948 }), 9949 isJsapiConfigured() && /* @__PURE__ */ u2("button", { 9950 type: "button", 9951 class: "le-btn", 9952 title: "Pick from this account's uploaded media", 9953 onClick: () => setIsOpen(true), 9954 children: [ 9955 li.image, 9956 "Library" 9957 ] 9958 }) 9959 ] 9960 }), 9961 isOpen && /* @__PURE__ */ u2(MediaPicker, { 9962 accept: props.accept, 9963 onPick: (url) => { 9964 setIsOpen(false); 9965 props.onChange(url); 9966 }, 9967 onCancel: () => setIsOpen(false) 9968 }) 9969 ] 9970 }); 9971} 9972
9973// src/components/live/accountLists.ts 9974var cache2 = /* @__PURE__ */ new Map(); 9975function useList(key, sql, map, params) { 9976 const [state, setState] = p2({ 9977 rows: [], 9978 loaded: false 9979 }); 9980 _2(() => { 9981 if (!isJsapiConfigured()) { 9982 setState({ 9983 rows: [], 9984 loaded: true 9985 }); 9986 return; 9987 } 9988 let live = true; 9989 let p3 = cache2.get(key); 9990 if (!p3) { 9991 cache2.set(key, p3 = queryRows(sql, params)); 9992 } 9993 p3.then((rows) => { 9994 if (live) setState({ 9995 rows: map(rows), 9996 loaded: true 9997 }); 9998 }, (error) => { 9999 cache2.delete(key); 10000 console.error(error); 10001 if (live) setState({ 10002 rows: [], 10003 loaded: true 10004 }); 10005 }); 10006 return () => { 10007 live = false; 10008 }; 10009 }, [ 10010 key 10011 ]); 10012 return state; 10013} 10014function useGoals() { 10015 return useList("goals", `SELECT id, name, html_id FROM central.acc_containers 10016 WHERE Find_in_set('goal event', container_types) AND Find_in_set('custom event', container_types) 10017 ORDER BY name`, (rows) => rows.filter((r3) => String(r3.html_id ?? "").trim() !== "").map((r3) => ({ 10018 name: String(r3.name ?? r3.html_id), 10019 event: String(r3.html_id) 10020 }))); 10021} 10022function useProfileFields() { 10023 return useList("profile-fields", "SELECT name, title FROM other_columns_info WHERE table_name='social' ORDER BY ordinal DESC", (rows) => rows.map((r3) => ({ 10024 name: String(r3.name), 10025 label: String(r3.title || r3.name) 10026 }))); 10027} 10028function useKnowledgeTags() { 10029 return useList("knowledge-tags", "SELECT id, name FROM central.tags ORDER BY name", (rows) => rows.map((r3) => String(r3.name)).filter((n2) => n2 !== "")); 10030} 10031function useCatalogueFeeds() { 10032 return useList("catalogue-feeds", "SELECT DISTINCT data_sources FROM archive.site_db_products WHERE data_sources <> '' LIMIT 50", (rows) => [ 10033 ...new Set(rows.flatMap((r3) => String(r3.data_sources ?? "").split(";")).map((x2) => x2.trim()).filter(Boolean)) 10034 ].sort()); 10035} 10036function useMeetingTypes() { 10037 return useList("meeting-types", `SELECT slug, title, is_active FROM central.acc_sched_event_types 10038 WHERE server_id=@server_id ORDER BY is_active DESC, title LIMIT 200`, (rows) => rows.map((r3) => ({ 10039 slug: String(r3.slug), 10040 title: String(r3.title || r3.slug), 10041 active: Number(r3.is_active) > 0 10042 })), { 10043 server_id: getJsapiServerId() 10044 }); 10045} 10046
10047// src/components/live/panels/WidgetPanel.tsx 10048var ROW_KINDS = /* @__PURE__ */ new Set([ 10049 "faq_list", 10050 "segment_list", 10051 "list", 10052 "quote_list", 10053 "table_rows", 10054 "menu_list", 10055 "slide_list", 10056 "media_list", 10057 "form_fields" 10058]); 10059var COLOR_KEY = /colou?r$|^bg$|bg$/i; 10060var IMAGE_KEY = /^(img|image|photo|avatar|thumb(nail)?)$/i; 10061var ROW_FIELDS = { 10062 faq_list: { 10063 q: { 10064 label: "Question" 10065 }, 10066 a: { 10067 label: "Answer" 10068 } 10069 }, 10070 quote_list: { 10071 q: { 10072 label: "Quote" 10073 }, 10074 name: { 10075 label: "Name" 10076 }, 10077 img: { 10078 label: "Photo", 10079 control: "image" 10080 } 10081 }, 10082 table_rows: { 10083 label: { 10084 label: "Row" 10085 }, 10086 a: { 10087 label: "First column" 10088 }, 10089 b: { 10090 label: "Second column" 10091 } 10092 }, 10093 segment_list: { 10094 label: { 10095 label: "Label" 10096 }, 10097 color: { 10098 label: "Colour" 10099 }, 10100 weight: { 10101 label: "Chance" 10102 }, 10103 prize: { 10104 label: "Prize message" 10105 }, 10106 code: { 10107 label: "Prize code" 10108 } 10109 }, 10110 menu_list: { 10111 label: { 10112 label: "Label" 10113 }, 10114 link: { 10115 label: "Link", 10116 control: "link" 10117 } 10118 }, 10119 slide_list: { 10120 url: { 10121 label: "Picture", 10122 control: "image" 10123 }, 10124 link: { 10125 label: "Link", 10126 control: "link" 10127 } 10128 }, 10129 form_fields: { 10130 label: { 10131 label: "Label" 10132 }, 10133 column: { 10134 label: "Saves to" 10135 }, 10136 type: { 10137 label: "Type" 10138 }, 10139 placeholder: { 10140 label: "Placeholder" 10141 }, 10142 required: { 10143 label: "Required", 10144 control: "check" 10145 } 10146 } 10147}; 10148function rowFieldLabel(kind, key) { 10149 const named = ROW_FIELDS[kind]?.[key]?.label; 10150 if (named) { 10151 return named; 10152 } 10153 const words = key.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase(); 10154 return words.charAt(0).toUpperCase() + words.slice(1); 10155} 10156function rowControlOf(kind, key) { 10157 const named = ROW_FIELDS[kind]?.[key]?.control; 10158 return named ?? (COLOR_KEY.test(key) ? "color" : IMAGE_KEY.test(key) ? "image" : "text"); 10159} 10160function parseRows(value, shapeOf = value) { 10161 const asText = typeof shapeOf == "string"; 10162 const model = toList(shapeOf); 10163 const bare = model.length > 0 && !(typeof model[0] == "object" && model[0]); 10164 const rows = toList(value).map((r3) => typeof r3 == "object" && r3 ? r3 : { 10165 value: r3 10166 }); 10167 return { 10168 rows, 10169 bare, 10170 asText 10171 }; 10172} 10173function toList(value) { 10174 let list = value; 10175 if (typeof list == "string") { 10176 try { 10177 list = JSON.parse(list || "[]"); 10178 } catch { 10179 list = []; 10180 } 10181 } 10182 return Array.isArray(list) ? list : []; 10183} 10184function writeRows(next, { bare, asText }) { 10185 const out = bare ? next.map((r3) => String(r3.value ?? "")) : next; 10186 return asText ? JSON.stringify(out) : out; 10187} 10188function RowsEditor(props) { 10189 const parsed = parseRows(props.value, props.shapeOf); 10190 const { rows, bare } = parsed; 10191 const allKeys = [ 10192 ...new Set(rows.flatMap((r3) => Object.keys(r3))) 10193 ]; 10194 const nested = allKeys.filter((k3) => rows.some((r3) => typeof r3[k3] == "object" && r3[k3])); 10195 const keys = allKeys.filter((k3) => !nested.includes(k3)); 10196 const write = (next) => props.onChange(writeRows(next, parsed)); 10197 const setCell = (i4, key, v3) => { 10198 const next = rows.map((r3, j3) => j3 === i4 ? { 10199 ...r3, 10200 [key]: v3 10201 } : r3); 10202 write(next); 10203 }; 10204 return /* @__PURE__ */ u2("div", { 10205 class: "le-wr", 10206 children: [ 10207 rows.map((row, i4) => /* @__PURE__ */ u2("div", { 10208 class: "le-wr-row", 10209 children: [
10210 /* @__PURE__ */ u2("div", { 10211 class: "le-wr-head", 10212 children: [ 10213 /* @__PURE__ */ u2("b", { 10214 children: [ 10215 "#", 10216 i4 + 1 10217 ] 10218 }), 10219 /* @__PURE__ */ u2("button", { 10220 type: "button", 10221 class: "le-wr-x", 10222 title: "Remove this one", 10223 onClick: () => write(rows.filter((_3, j3) => j3 !== i4)), 10224 children: "\xD7" 10225 }) 10226 ] 10227 }), 10228 keys.map((key) => { 10229 const control = rowControlOf(props.kind, key); 10230 const str2 = String(row[key] ?? ""); 10231 return ( 10232 // it is not a label anybody should read. 10233 /* @__PURE__ */ u2(Field, { 10234 label: bare ? "" : rowFieldLabel(props.kind, key), 10235 children: control == "color" ? /* @__PURE__ */ u2(ColorInput, { 10236 value: str2, 10237 onChange: (v3) => setCell(i4, key, v3) 10238 }) : control == "image" ? /* @__PURE__ */ u2(MediaUrlInput, { 10239 value: str2, 10240 accept: "image", 10241 onChange: (v3) => setCell(i4, key, String(v3 ?? "")) 10242 }
10242) : control == "link" ? /* @__PURE__ */ u2(LinkInput, { 10243 value: str2, 10244 onChange: (v3) => setCell(i4, key, v3) 10245 }) : control == "check" ? /* @__PURE__ */ u2("input", { 10246 type: "checkbox", 10247 class: "le-check", 10248 checked: row[key] === true || str2 !== "" && str2 !== "0" && str2 !== "false", 10249 onChange: (ev) => setCell(i4, key, ev.target.checked) 10250 }) : /* @__PURE__ */ u2(TextInput, { 10251 value: str2, 10252 onChange: (v3) => setCell(i4, key, v3) 10253 }) 10254 }, key) 10255 ); 10256 }) 10257 ] 10258 }, i4)), 10259 /* @__PURE__ */ u2("button", { 10260 type: "button", 10261 class: "le-wr-add", 10262 onClick: () => { 10263 const last = rows[rows.length - 1] ?? {}; 10264 const blank = {}; 10265 for (const k3 of keys.length ? keys : [ 10266 "value" 10267 ]) { 10268 blank[k3] = COLOR_KEY.test(k3) || !isNaN(Number(last[k3])) ? last[k3] ?? "" : ""; 10269 } 10270 write([ 10271 ...rows, 10272 blank 10273 ]); 10274 }, 10275 children: "+ Add" 10276 }),
10277 !rows.length && /* @__PURE__ */ u2("div", { 10278 class: "le-wl-note", 10279 children: "Nothing here yet \u2014 add the first one." 10280 }), 10281 nested.length > 0 && /* @__PURE__ */ u2("div", { 10282 class: "le-wl-note", 10283 children: [ 10284 nested.join(", "), 10285 ": edited in the Personyze panel, and kept as it is here." 10286 ] 10287 }) 10288 ] 10289 }); 10290} 10291var TIMER_MODES = [ 10292 [ 10293 "fixed_date", 10294 "To a date", 10295 "Everyone sees the same deadline" 10296 ], 10297 [ 10298 "cookie_based", 10299 "Per visitor", 10300 "Starts when this visitor first sees it" 10301 ], 10302 [ 10303 "restart_everytime", 10304 "Every visit", 10305 "Starts again on every page view" 10306 ] 10307]; 10308function TimerInput(props) { 10309 let cfg = {}; 10310 try { 10311 const parsed = JSON.parse(String(props.value ?? "{}")); 10312 if (parsed && typeof parsed == "object") { 10313 cfg = parsed; 10314 } 10315 } catch { 10316 } 10317 const type = String(cfg.type ?? "restart_everytime"); 10318 const put = (patch) => props.onChange(JSON.stringify({ 10319 ...cfg, 10320 ...patch 10321 })); 10322 const num = (key) => String(cfg[key] ?? ""); 10323 const asDate = (() => { 10324 const secs = Number(cfg.fixed_date_date) || 0; 10325 return secs > 0 ? new Date(secs * 1e3).toISOString().slice(0, 10) : ""; 10326 })(); 10327 const duration = (prefix) => [ 10328 [ 10329 "days", 10330 "Days" 10331 ], 10332 [ 10333 "hours", 10334 "Hours" 10335 ], 10336 [ 10337 "minutes", 10338 "Minutes" 10339 ] 10340 ].map(([unit, label]) => /* @__PURE__ */ u2(Field, { 10341 label, 10342 children: /* @__PURE__ */ u2(NumInput, { 10343 value: num(prefix + "_" + unit), 10344 unit: "", 10345 min: 0, 10346 max: 999, 10347 onChange: (v3) => put({ 10348 [prefix + "_" + unit]: Number(v3) || 0 10349 }) 10350 }) 10351 }, unit)); 10352 return /* @__PURE__ */ u2("div", { 10353 class: "le-timer", 10354 children: [
10355 /* @__PURE__ */ u2("div", { 10356 class: "le-seg", 10357 children: TIMER_MODES.map(([id, label, hint]) => /* @__PURE__ */ u2("button", { 10358 type: "button", 10359 class: "le-seg-btn" + (type == id ? " le-on" : ""), 10360 title: hint, 10361 onClick: () => put({ 10362 type: id 10363 }), 10364 children: label 10365 }, id)) 10366 }), 10367 type == "fixed_date" ? /* @__PURE__ */ u2(k, { 10368 children: [ 10369 /* @__PURE__ */ u2(Field, { 10370 label: "Date", 10371 children: /* @__PURE__ */ u2("input", { 10372 type: "date", 10373 class: "le-input", 10374 value: asDate, 10375 onInput: (ev) => { 10376 const v3 = ev.target.value; 10377 const [y3, m2, d3] = v3.split("-").map(Number); 10378 put({ 10379 fixed_date_date: v3 ? Math.floor(Date.UTC(y3, m2 - 1, d3) / 1e3) : 0 10380 }); 10381 } 10382 }) 10383 }), 10384 /* @__PURE__ */ u2(Field, { 10385 label: "Time", 10386 children: /* @__PURE__ */ u2("input", { 10387 type: "time", 10388 class: "le-input", 10389 value: String(cfg.fixed_date_time ?? ""), 10390 onInput: (ev) => put({ 10391 fixed_date_time: ev.target.value 10392 }) 10393 }) 10394 }),
10395 /* @__PURE__ */ u2(Field, { 10396 label: "Hours from UTC", 10397 children: /* @__PURE__ */ u2(NumInput, { 10398 value: num("fixed_date_timezone"), 10399 unit: "h", 10400 min: -12, 10401 max: 14, 10402 onChange: (v3) => put({ 10403 fixed_date_timezone: Number(v3) || 0 10404 }) 10405 }) 10406 }) 10407 ] 10408 }) : type == "cookie_based" ? /* @__PURE__ */ u2(k, { 10409 children: duration("cookie_based") 10410 }) : /* @__PURE__ */ u2(k, { 10411 children: duration("restart_everytime") 10412 }) 10413 ] 10414 }); 10415} 10416function LinkInput(props) { 10417 const href = props.value.match(/href="([^"]*)"/)?.[1] ?? ""; 10418 const blank = /target="_blank"/.test(props.value); 10419 const write = (url, newTab) => props.onChange(`href="${url.replace(/"/g, """)}"` + (newTab ? ' target="_blank" rel="noopener"' : "") + ' data-personyze-click-target="1"'); 10420 return /* @__PURE__ */ u2(k, { 10421 children: [ 10422 /* @__PURE__ */ u2(TextInput, { 10423 value: href, 10424 placeholder: "https://...", 10425 onChange: (v3) => write(v3, blank) 10426 }),
10427 /* @__PURE__ */ u2("label", { 10428 class: "le-inline-check", 10429 children: [ 10430 /* @__PURE__ */ u2("input", { 10431 type: "checkbox", 10432 class: "le-check", 10433 checked: blank, 10434 onChange: (ev) => write(href, ev.target.checked) 10435 }), 10436 "Open in a new tab" 10437 ] 10438 }) 10439 ] 10440 }); 10441} 10442function ListInput(props) { 10443 const { value, rows, loaded } = props; 10444 if (loaded && rows.length === 0) { 10445 return /* @__PURE__ */ u2(k, { 10446 children: [ 10447 /* @__PURE__ */ u2(TextInput, { 10448 value, 10449 onChange: props.onChange 10450 }), 10451 props.note && /* @__PURE__ */ u2("div", { 10452 class: "le-wl-note", 10453 children: props.note 10454 }) 10455 ] 10456 }); 10457 } 10458 const known = value === "" || rows.some((r3) => r3[0] === value); 10459 return /* @__PURE__ */ u2(k, { 10460 children: [ 10461 /* @__PURE__ */ u2(SelectInput, { 10462 value, 10463 options: [ 10464 [ 10465 "", 10466 props.emptyLabel 10467 ], 10468 ...rows, 10469 // Gone, or not arrived. Either way it stays selected rather than resetting. 10470 ...known ? [] : [ 10471 [ 10472 value, 10473 `${value} \u2014 not in this account` 10474 ] 10475 ] 10476 ], 10477 onChange: props.onChange 10478 }),
10479 !known && /* @__PURE__ */ u2("div", { 10480 class: "le-wl-note", 10481 children: "Nothing in this account is called that any more." 10482 }) 10483 ] 10484 }); 10485} 10486function GoalInput(props) { 10487 const { rows, loaded } = useGoals(); 10488 return /* @__PURE__ */ u2(ListInput, { 10489 value: props.value, 10490 rows: rows.map((g2) => [ 10491 g2.event, 10492 g2.name 10493 ]), 10494 loaded, 10495 emptyLabel: "(no goal)", 10496 note: 'No event-driven goal in this account yet. Create one under Goals, type "JavaScript code".', 10497 onChange: props.onChange 10498 }); 10499} 10500function ProfileFieldInput(props) { 10501 const { rows, loaded } = useProfileFields(); 10502 const simple = /^\$\{(?:u->)?([\w-]+)\}$/.exec(props.value); 10503 const custom = props.value !== "" && !simple; 10504 if (custom) { 10505 return /* @__PURE__ */ u2(k, { 10506 children: [ 10507 /* @__PURE__ */ u2(TextInput, { 10508 value: props.value, 10509 onChange: props.onChange 10510 }), 10511 /* @__PURE__ */ u2("div", { 10512 class: "le-wl-note", 10513 children: "Kept as written - this is not a plain profile field." 10514 }) 10515 ] 10516 }); 10517 } 10518 return /* @__PURE__ */ u2(ListInput, { 10519 value: simple ? simple[1] : "", 10520 rows: rows.map((c3) => [ 10521 c3.name, 10522 c3.label 10523 ]), 10524 loaded, 10525 emptyLabel: "(no field)", 10526 note: "This account has no profile fields yet.", 10527 onChange: (v3) => props.onChange(v3 ? "${" + v3 + "}" : "") 10528 }); 10529} 10530function IconInput(props) { 10531 const [q3, setQ] = p2(""); 10532 const origin = getJsapiOrigin(); 10533 const list = ICON_LIBRARY.filter(([name]) => !q3 || name.indexOf(q3.toLowerCase()) !== -1); 10534 return /* @__PURE__ */ u2(k, { 10535 children: [
10536 origin && /* @__PURE__ */ u2("style", { 10537 children: `@font-face{font-family:'${FA_FONT}';font-display:swap;src:url('${origin}/font-awesome/fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('${origin}/font-awesome/fonts/fontawesome-webfont.woff?v=4.7.0') format('woff')}` + ICON_LIBRARY.map(([n2, c3]) => `.le-ico-${n2}::before{content:"\\${c3}"}`).join("") 10538 }), 10539 /* @__PURE__ */ u2(TextInput, { 10540 value: q3, 10541 placeholder: "Search icons\u2026", 10542 onChange: setQ 10543 }), 10544 /* @__PURE__ */ u2("div", { 10545 class: "le-icogrid", 10546 children: list.map(([name]) => /* @__PURE__ */ u2("button", { 10547 type: "button", 10548 title: name, 10549 class: "le-icogrid-i" + (name === props.value ? " is-on" : ""), 10550 onClick: () => props.onChange(name), 10551 children: [ 10552 /* @__PURE__ */ u2("i", { 10553 class: `le-ico le-ico-${name}`, 10554 style: { 10555 fontFamily: FA_FONT 10556 } 10557 }), 10558 /* @__P
10558URE__ */ u2("span", { 10559 children: name 10560 }) 10561 ] 10562 }, name)) 10563 }), 10564 list.length === 0 && /* @__PURE__ */ u2("div", { 10565 class: "le-wl-note", 10566 children: "No icon matches that." 10567 }), 10568 props.value !== "" && !ICON_LIBRARY.some(([n2]) => n2 === props.value) && /* @__PURE__ */ u2("div", { 10569 class: "le-wl-note", 10570 children: [ 10571 'Currently set to "', 10572 props.value, 10573 '", which is not in this list.' 10574 ] 10575 }) 10576 ] 10577 }); 10578} 10579function UrlInput(props) { 10580 let resolved = ""; 10581 const here = globalThis.location?.href; 10582 if (here && props.value !== "" && !/^(https?:|\/\/|\/|#|\{|\$)/i.test(props.value)) { 10583 try { 10584 resolved = new URL(/^[\w.-]+\.[a-z]{2,}(\/|$)/i.test(props.value) ? "https://" + props.value : props.value, here).href; 10585 } catch { 10586 resolved = ""; 10587 } 10588 } 10589 return /* @__PURE__ */ u2(k, { 10590 children: [ 10591 /* @__PURE__ */ u2(TextInput, { 10592 value: props.value, 10593 placeholder: "https://\u2026", 10594 onChange: props.onChange 10595 }), 10596 resolved !== "" && /* @__PURE__ */ u2("div", { 10597 class: "le-wl-note", 10598 children: [ 10599 "Goes to ", 10600 resolved, 10601 " ",
10602 /* @__PURE__ */ u2("button", { 10603 type: "button", 10604 class: "le-linkbtn", 10605 onClick: () => props.onChange(resolved), 10606 children: "use this" 10607 }) 10608 ] 10609 }) 10610 ] 10611 }); 10612} 10613function MeetingTypesInput(props) { 10614 const { rows, loaded } = useMeetingTypes(); 10615 const picked = String(props.value ?? "").split(",").map((x2) => x2.trim()).filter(Boolean); 10616 if (!loaded) { 10617 return picked.length ? /* @__PURE__ */ u2("div", { 10618 class: "le-wl-note", 10619 children: picked.join(", ") 10620 }) : null; 10621 } 10622 if (rows.length === 0) { 10623 return /* @__PURE__ */ u2(k, { 10624 children: [ 10625 /* @__PURE__ */ u2(TextInput, { 10626 value: props.value, 10627 onChange: props.onChange 10628 }), 10629 /* @__PURE__ */ u2("div", { 10630 class: "le-wl-note", 10631 children: "No meeting types yet \u2014 create them under Meetings in the Personyze panel." 10632 }) 10633 ] 10634 }); 10635 } 10636 const known = new Set(rows.map((r3) => r3.slug)); 10637 const unknown = picked.filter((s3) => !known.has(s3)); 10638 const toggle = (slug, on) => { 10639 const next = rows.map((r3) => r3.slug).filter((s3) => s3 === slug ? on : picked.includes(s3)); 10640 props.onChange([ 10641 ...next, 10642 ...unknown 10643 ].join(",")); 10644 }; 10645 return /* @__PURE__ */ u2(k, { 10646 children: [ 10647 rows.map((r3) => /* @__PURE__ */ u2("label", { 10648 class: "le-inline-check", 10649 children: [
10650 /* @__PURE__ */ u2("input", { 10651 type: "checkbox", 10652 class: "le-check", 10653 checked: picked.includes(r3.slug), 10654 onChange: (ev) => toggle(r3.slug, ev.target.checked) 10655 }), 10656 r3.title, 10657 r3.active ? "" : " (switched off)" 10658 ] 10659 }, r3.slug)), 10660 unknown.length > 0 && /* @__PURE__ */ u2("div", { 10661 class: "le-wl-note", 10662 children: [ 10663 "Kept, but no meeting type is called this any more: ", 10664 unknown.join(", ") 10665 ] 10666 }) 10667 ] 10668 }); 10669} 10670function KnowledgeTagInput(props) { 10671 const { rows, loaded } = useKnowledgeTags(); 10672 return /* @__PURE__ */ u2(ListInput, { 10673 value: props.value, 10674 rows: rows.map((n2) => [ 10675 n2, 10676 n2 10677 ]), 10678 loaded, 10679 emptyLabel: "The whole knowledge base", 10680 note: "No tags yet \u2014 tag a knowledge source first.", 10681 onChange: props.onChange 10682 }); 10683} 10684function FeedInput(props) { 10685 const { rows, loaded } = useCatalogueFeeds(); 10686 return /* @__PURE__ */ u2(ListInput, { 10687 value: props.value, 10688 rows: rows.length < 2 ? [] : rows.map((n2) => [ 10689 n2, 10690 n2 10691 ]), 10692 loaded, 10693 emptyLabel: "Every feed", 10694 note: "This account imports from one feed, so there is nothing to narrow to.", 10695 onChange: props.onChange 10696 }); 10697} 10698function Control(props) { 10699 const { setting, value } = props; 10700 const str2 = value == void 0 ? "" : String(value); 10701 if (ROW_KINDS.has(setting.kind)) { 10702 return /* @__PURE__ */ u2(RowsEditor, { 10703 kind: setting.kind, 10704 value, 10705 shapeOf: props.shapeOf, 10706 onChange: props.onChange 10707 }); 10708 } 10709 switch (setting.kind) { 10710 case "timer": 10711 return /* @__PURE__ */ u2(TimerInput, { 10712 value, 10713 onChange: props.onChange 10714 }); 10715 case "media": 10716 return /* @__PURE__ */ u2(MediaUrlInput, { 10717 value: str2, 10718 accept: "image", 10719 onChange: props.onChange 10720 }); 10721 case "link": 10722 return /* @__PURE__ */ u2(LinkInput, { 10723 value: str2, 10724 onChange: props.onChange 10725 }); 10726 case "source_javascript": 10727 return /* @__PURE__ */ u2("textarea", { 10728 class: "le-area", 10729 rows: 4, 10730 value: str2, 10731 onInput: (ev) => props.onChange(ev.target.value) 10732 }); 10733 case "color": 10734 return /* @__PURE__ */ u2(ColorInput, { 10735 value: str2, 10736 onChange: props.onChange 10737 }); 10738 case "number": 10739 return /* @__PURE__ */ u2(NumInput, { 10740 value: str2, 10741 unit: setting.unit ?? "px", 10742 min: setting.min, 10743 max: setting.max, 10744 onChange: props.onChange 10745 }); 10746 case "select": 10747 case "align": 10748 return /* @__PURE__ */ u2(SelectInput, { 10749 value: str2, 10750 options: setting.options ?? [], 10751 onChange: props.onChange 10752 }); 10753 case "checkbox": 10754 return /* @__PURE__ */ u2("input", { 10755 type: "checkbox", 10756 class: "le-check", 10757 checked: str2 !== "" && str2 !== "0", 10758 onChange: (ev) => props.onChange(ev.target.checked ? "1" : "") 10759 }); 10760 case "textarea": 10761 return /* @__PURE__ */ u2("textarea", { 10762 class: "le-input le-ta", 10763 value: str2, 10764 onInput: (ev) => props.onChange(ev.target.value) 10765 }); 10766 case "goal": 10767 return /* @__PURE__ */ u2(GoalInput, { 10768 value: str2, 10769 onChange: props.onChange 10770 }); 10771 case "visitor_field": 10772 return /* @__PURE__ */ u2(ProfileFieldInput, { 10773 value: str2, 10774 onChange: props.onChange 10775 }); 10776 case "icon": 10777 return /* @__PURE__ */ u2(IconInput, { 10778 value: str2, 10779 onChange: props.onChange 10780 }); 10781 case "url": 10782 return /* @__PURE__ */ u2(UrlInput, { 10783 value: str2, 10784 onChange: props.onChange 10785 }); 10786 case "knowledge_tag": 10787 return /* @__PURE__ */ u2(KnowledgeTagInput, { 10788 value: str2, 10789 onChange: props.onChange 10790 }); 10791 case "catalogue_source": 10792 return /* @__PURE__ */ u2(FeedInput, { 10793 value: str2, 10794 onChange: props.onChange 10795 }); 10796 case "meeting_types": 10797 return /* @__PURE__ */ u2(MeetingTypesInput, { 10798 value: str2, 10799 onChange: props.onChange 10800 }); 10801 default: 10802 return /* @__PURE__ */ u2(TextInput, { 10803 value: str2, 10804 onChange: props.onChange 10805 }); 10806 } 10807} 10808function WidgetPanel(props) { 10809 const [tab, setTab] = p2("content"); 10810 const shown = props.entry.settings.filter((s3) => s3.tab === tab); 10811 return /* @__PURE__ */ u2(k, { 10812 children: [
10813 /* @__PURE__ */ u2(PanelTabs, { 10814 tabs: [ 10815 { 10816 id: "content", 10817 label: "Content", 10818 title: "What it says" 10819 }, 10820 { 10821 id: "style", 10822 label: "Style", 10823 title: "How it looks" 10824 } 10825 ], 10826 value: tab, 10827 onChange: setTab 10828 }), 10829 shown.length === 0 ? /* @__PURE__ */ u2("div", { 10830 class: "le-wl-note", 10831 children: "Nothing to set here for this one." 10832 }) : shown.map((s3) => ( 10833 /* A CHECKBOX IS ONE LINE: the box, then what it means. In the label column its 10834 words wrapped to three lines beside a lone box (Jacob's screenshot, 28 Sep 26), 10835 and a label beside a box reads as a heading, not as the thing you tick. */ 10836 s3.kind === "checkbox" ? /* @__PURE__ */ u2("div", { 10837 class: "le-wcheck", 10838 children: [ 10839 /* @__PURE__ */ u2("label", { 10840 class: "le-inline-check", 10841 children: [ 10842 /* @__PURE__ */ u2(Control, { 10843 setting: s3, 10844 value: props.values[s3.key], 10845 shapeOf: props.entry.props[s3.key], 10846 onChange: (v3) => props.onChange(s3.key, v3) 10847 }), 10848 s3.label 10849 ] 10850 }), 10851 s3.hint && /* @__PURE__ */ u2("div", { 10852 class: "le-wl-note", 10853 children: s3.hint 10854 }) 10855 ] 10856 }, s3.key) : ( 10857 /* A list setting takes the whole panel: label above, rows below. Squeezed 10858 into the label column it left each field a few characters wide. 10859 10860 The icon grid is here for the same reason and it is worth naming: in the 10861 value column its `auto-fill` had room for exactly ONE column, so 52 icons 10862 drew as 52 stacked rows - a grid that is a list, which is the control it 10863 replaced. Only visible by looking at it; the render test counted 52 10864 buttons and was perfectly happy. */
10865 /* @__PURE__ */ u2(Field, { 10866 label: s3.label, 10867 wide: ROW_KINDS.has(s3.kind) || s3.kind === "textarea" || s3.kind === "icon", 10868 children: [ 10869 /* @__PURE__ */ u2(Control, { 10870 setting: s3, 10871 value: props.values[s3.key], 10872 shapeOf: props.entry.props[s3.key], 10873 onChange: (v3) => props.onChange(s3.key, v3) 10874 }), 10875 s3.hint && /* @__PURE__ */ u2("div", { 10876 class: "le-wl-note", 10877 children: s3.hint 10878 }) 10879 ] 10880 }, s3.key) 10881 ) 10882 )) 10883 ] 10884 }); 10885} 10886
10887// src/components/live/panels/WidgetEditPanel.tsx 10888function savedProps(elem) { 10889 try { 10890 const v3 = JSON.parse(elem.getAttribute("data-pz-props") || "{}"); 10891 return v3 && typeof v3 == "object" && !Array.isArray(v3) ? v3 : {}; 10892 } catch { 10893 return {}; 10894 } 10895} 10896function WidgetEditPanel(props) { 10897 const { session } = props; 10898 const host = widgetHostOf(session.elem); 10899 const id = host?.getAttribute("data-pz-widget") ?? ""; 10900 const entry = WIDGET_CATALOGUE.find((e3) => e3.id == id); 10901 const [values, setValues] = p2(() => ({ 10902 ...entry?.props ?? {}, 10903 ...host ? savedProps(host) : {} 10904 })); 10905 const [html, setHtml] = p2(""); 10906 _2(() => { 10907 const prev = session.prevChanges?.[0]; 10908 if (!entry || !html) { 10909 return; 10910 } 10911 const source = prev?.change ?? findInsertChange(session.prevChanges); 10912 if (!source) { 10913 return; 10914 } 10915 const change = structuredClone(source); 10916 const spec = change.e; 10917 if (!Array.isArray(spec)) { 10918 return; 10919 } 10920 spec[1] = { 10921 ...spec[1] ?? {}, 10922 "data-pz-widget": entry.id, 10923 "data-pz-props": JSON.stringify(values) 10924 }; 10925 spec[3] = html; 10926 session.prevChanges = [ 10927 { 10928 n: prev?.n ?? 0, 10929 change 10930 } 10931 ]; 10932 session.onPreview(); 10933 }); 10934 if (!entry || !host) { 10935 return null; 10936 } 10937 return /* @__PURE__ */ u2(Section, { 10938 title: entry.label, 10939 children: [
10940 /* @__PURE__ */ u2(WidgetPanel, { 10941 entry, 10942 values, 10943 onChange: (key, value) => { 10944 const next = { 10945 ...values, 10946 [key]: value 10947 }; 10948 setValues(next); 10949 if (session.report) { 10950 void requestWidgetHtml(session.report, entry.id, next).then((fresh) => { 10951 if (fresh != null) { 10952 setHtml(fresh); 10953 } 10954 }); 10955 } 10956 } 10957 }), 10958 html === "" && /* @__PURE__ */ u2("div", { 10959 class: "le-wl-note", 10960 children: "Nothing to show yet - this one needs content before it appears on the page." 10961 }), 10962 !session.report && /* @__PURE__ */ u2("div", { 10963 class: "le-wl-note", 10964 children: "The Personyze window is not reachable, so these settings cannot be redrawn. Reopen the editor from the panel to change them." 10965 }) 10966 ] 10967 }); 10968} 10969function findInsertChange(prev) { 10970 return prev?.find((p3) => Array.isArray(p3.change?.e))?.change; 10971} 10972
10973// src/modules/ailive/private/serialize.ts 10974var OPAQUE = /* @__PURE__ */ new Set([ 10975 "script", 10976 "style", 10977 "noscript", 10978 "template", 10979 "svg", 10980 "iframe", 10981 "object", 10982 "embed", 10983 "canvas", 10984 "video", 10985 "audio", 10986 "select" 10987]); 10988var SHOWN_ATTRS = [ 10989 [ 10990 "href", 10991 60 10992 ], 10993 [ 10994 "src", 10995 60 10996 ], 10997 [ 10998 "alt", 10999 40 11000 ], 11001 [ 11002 "title", 11003 40 11004 ], 11005 [ 11006 "type", 11007 20 11008 ], 11009 [ 11010 "name", 11011 30 11012 ], 11013 [ 11014 "role", 11015 20 11016 ], 11017 [ 11018 "aria-label", 11019 40 11020 ], 11021 [ 11022 "placeholder", 11023 40 11024 ] 11025]; 11026var MAX_CLASSES = 5; 11027var MAX_TEXT_CHARS = 80; 11028var MAX_CHILDREN = 30; 11029var MAX_DEPTH = 30; 11030var XML_ELEMENT_NODE = 1; 11031var XML_TEXT_NODE = 3; 11032function pageSkeleton(ignore, maxChars = 6e4) { 11033 const out = new Array(); 11034 let spent = 0; 11035 let truncated = false; 11036 function emit(line) { 11037 if (spent + line.length > maxChars) { 11038 truncated = true; 11039 return false; 11040 } 11041 out.push(line); 11042 spent += line.length + 1; 11043 return true; 11044 } 11045 function walk(elem, depth) { 11046 if (ignore.indexOf(elem) != -1) { 11047 return true; 11048 } 11049 const indent = " ".repeat(depth); 11050 if (depth > MAX_DEPTH) { 11051 return emit(indent + "\u2026"); 11052 } 11053 let line = indent + "<" + elem.localName; 11054 if (elem.id) { 11055 line += " #" + elem.id; 11056 } 11057 const classes = Array.from(elem.classList).slice(0, MAX_CLASSES); 11058 if (classes.length) { 11059 line += " ." + classes.join("."); 11060 } 11061 for (const [name, max] of SHOWN_ATTRS) { 11062 const value = elem.getAttribute(name); 11063 if (value) { 11064 line += ` ${name}="${cut(value, max)}"`; 11065 } 11066 } 11067 line += ">"; 11068 const isOpaque = OPAQUE.has(elem.localName); 11069 if (!isOpaque) { 11070 let text = ""; 11071 for (let node = elem.firstChild; node; node = node.nextSibling) { 11072 if (node.nodeType == XML_TEXT_NODE) { 11073 text += node.data; 11074 } 11075 } 11076 text = text.replace(/\s+/g, " ").trim(); 11077 if (text) { 11078 line += ` "${cut(text, MAX_TEXT_CHARS)}"`; 11079 } 11080 } 11081 if (!emit(line)) { 11082 return false; 11083 } 11084 if (isOpaque) { 11085 return true; 11086 } 11087 let nShown = 0; 11088 let nSkipped = 0; 11089 for (let node = elem.firstChild; node; node = node.nextSibling) { 11090 if (node.nodeType != XML_ELEMENT_NODE) { 11091 continue; 11092 } 11093 if (nShown >= MAX_CHILDREN) { 11094 nSkipped++; 11095 continue; 11096 } 11097 if (!walk(node, depth + 1)) { 11098 return false; 11099 } 11100 nShown++; 11101 } 11102 if (nSkipped) { 11103 return emit(" ".repeat(depth + 1) + `\u2026 ${nSkipped} more elements`); 11104 } 11105 return true; 11106 } 11107 walk(document.body, 0); 11108 if (truncated) { 11109 out.push("\u2026 (the skeleton was cut here - the page continues)"); 11110 } 11111 return out.join("\n"); 11112} 11113function elemHtml(elem, maxChars) { 11114 const html = elem.outerHTML; 11115 if (html.length <= maxChars) { 11116 return html; 11117 } 11118 return html.slice(0, maxChars) + ` 11119<!-- truncated: ${html.length - maxChars} more characters; the full element is ${html.length} characters -->`; 11120} 11121function cut(text, maxChars) { 11122 return text.length <= maxChars ? text : text.slice(0, maxChars - 1) + "\u2026"; 11123} 11124
11125// src/modules/pageeditor/private/changes.ts 11126var URL_NOISE = /^(utm_|_S_T$|gclid$|fbclid$|msclkid$|dclid$|ttclid$|twclid$|igshid$|mc_eid$|_ga$)/i; 11127function scopeUrlKey(href) { 11128 try { 11129 const url = new URL(href); 11130 const names = []; 11131 url.searchParams.forEach((_v, name) => { 11132 if (!URL_NOISE.test(name)) names.push(name); 11133 }); 11134 names.sort(); 11135 const query2 = names.map((name) => name + "=" + url.searchParams.get(name)).join("&"); 11136 return url.origin + url.pathname.replace(/\/$/, "") + (query2 ? "?" + query2 : ""); 11137 } catch { 11138 return href; 11139 } 11140} 11141function relatedChanges(changes, index) { 11142 const self = changes[index]?.qelem_id; 11143 if (typeof self != "string" || !self) { 11144 return [ 11145 index 11146 ]; 11147 } 11148 const inside = (outer, inner) => inner.length > outer.length && inner.startsWith(outer) && /^[\s>]/.test(inner.slice(outer.length)); 11149 const out = []; 11150 for (let i4 = 0; i4 < changes.length; i4++) { 11151 const other = changes[i4].qelem_id; 11152 if (typeof other == "string" && other && (other == self || inside(self, other) || inside(other, self))) { 11153 out.push(i4); 11154 } 11155 } 11156 return out.length ? out : [ 11157 index 11158 ]; 11159} 11160var CLICK_EVENT = Object.freeze({ 11161 onwhat: "click", 11162 action_id: -1, 11163 action_state: "extra" 11164}); 11165function isClickEvent(event) { 11166 const e3 = event ?? {}; 11167 return e3.onwhat == "click" && Number(e3.action_id) == -1 && e3.action_state == "extra"; 11168} 11169function countsClicks(change) { 11170 return Array.isArray(change.events) && change.events.some(isClickEvent); 11171} 11172function eventsWithClicks(change, on) { 11173 const others = (Array.isArray(change.events) ? change.events : []).filter((e3) => !isClickEvent(e3)); 11174 const next = on ? [ 11175 ...others, 11176 { 11177 ...CLICK_EVENT 11178 } 11179 ] : others; 11180 return next.length ? next : void 0; 11181} 11182var PHONE_SCREEN_MEDIA = "(max-width: 767px)"; 11183function deviceAllows(device, isPhone) { 11184 return device == "mobile" ? isPhone : device == "desktop" ? !isPhone : true; 11185} 11186function isPhoneScreen() { 11187 return !!globalThis.matchMedia?.(PHONE_SCREEN_MEDIA).matches; 11188} 11189function sameScopeUrl(recorded, current) { 11190 return !recorded || scopeUrlKey(recorded) == scopeUrlKey(current); 11191} 11192function varRanges(html) { 11193 const ranges = new Array(); 11194 let plainFrom = 0; 11195 for (let i4 = 0, iEnd = html.length; i4 < iEnd; i4++) { 11196 if (html.charCodeAt(i4) != 36 || html.charAt(i4 + 1) != "{") { 11197 continue; 11198 } 11199 const from = i4; 11200 let depth = 0; 11201 let quote2 = ""; 11202 let j3 = i4 + 1; 11203 for (; j3 < iEnd; j3++) { 11204 const c3 = html.charAt(j3); 11205 if (quote2) { 11206 if (c3 == "\\") { 11207 j3++; 11208 } else if (c3 == quote2) { 11209 quote2 = ""; 11210 } 11211 } else if (c3 == '"' || c3 == "'" || c3 == "`") { 11212 quote2 = c3; 11213 } else if (c3 == "{") { 11214 depth++; 11215 } else if (c3 == "}") { 11216 if (--depth == 0) { 11217 break; 11218 } 11219 } 11220 } 11221 if (j3 >= iEnd) { 11222 break; 11223 } 11224 if (from > plainFrom) { 11225 ranges.push({ 11226 from: plainFrom, 11227 to: from, 11228 canInsertVar: true 11229 }); 11230 } 11231 ranges.push({ 11232 from, 11233 to: j3 + 1, 11234 canInsertVar: false 11235 }); 11236 plainFrom = j3 + 1; 11237 i4 = j3; 11238 } 11239 if (plainFrom < html.length || ranges.length == 0) { 11240 ranges.push({ 11241 from: plainFrom, 11242 to: html.length, 11243 canInsertVar: true 11244 }); 11245 } 11246 return ranges; 11247} 11248var PART_THRESHOLD = 0.1; 11249function getEditTextOrHtml(base, cur, isHtml) { 11250 function isValidHtml(html) { 11251 const div = document.createElement("div"); 11252 div.innerHTML = html; 11253 return div.innerHTML == html; 11254 } 11255 function indexInCur(substr, findFrom) { 11256 let pos2 = cur.indexOf(substr, findFrom); 11257 if (pos2 != -1) { 11258 for (const { to, canInsertVar } of curTokens) { 11259 if (to > pos2) { 11260 if (canInsertVar) { 11261 break; 11262 } 11263 pos2 = cur.indexOf(substr, to); 11264 } 11265 } 11266 } 11267 return pos2; 11268 } 11269 function lastIndexInCur(substr, findFrom) { 11270 let pos2 = cur.lastIndexOf(substr, findFrom); 11271 if (pos2 != -1) { 11272 for (let i5 = curTokens.length - 1; i5 >= 0; i5--) { 11273 const { from, canInsertVar } = curTokens[i5]; 11274 if (from <= pos2) { 11275 if (canInsertVar) { 11276 break; 11277 } 11278 pos2 = cur.lastIndexOf(substr, from - 1); 11279 if (pos2 == -1) { 11280 break; 11281 } 11282 } 11283 } 11284 } 11285 return pos2; 11286 } 11287 const baseLen = base.length; 11288 const curLen = cur.length; 11289 const baseTokens = varRanges(base); 11290 const curTokens = varRanges(cur); 11291 const baseFirstToken = baseTokens[0]; 11292 const baseLastToken = baseTokens[baseTokens.length - 1]; 11293 let basePrefixPos = -1; 11294 let basePrefixLen = -1; 11295 const maxBasePrefixLen = !baseFirstToken?.canInsertVar ? 0 : baseFirstToken.to; 11296 let baseSuffixPos = -1; 11297 let baseSuffixLen = -1; 11298 const maxBaseSuffixLen = !baseLastToken?.canInsertVar ? 0 : baseLastToken.to - baseLastToken.from; 11299 let i4 = 0; 11300 for (let inside = false; i4 < baseLen * (1 - PART_THRESHOLD) && basePrefixPos == -1; i4++) { 11301 const c3 = base.charAt(baseLen - i4 - 1); 11302 if (!inside) { 11303 if (baseLen - i4 <= maxBasePrefixLen) { 11304 for (let from = 0; true; from = basePrefixPos + 1) { 11305 const prefix = base.slice(0, baseLen - i4); 11306 basePrefixPos = indexInCur(prefix, from); 11307 if (isHtml && basePrefixPos != -1) { 11308 if (prefix.indexOf("<") == -1) { 11309 const tmp = cur.slice(0, basePrefixPos); 11310 const lt = tmp.lastIndexOf("<"); 11311 const gt = tmp.lastIndexOf(">"); 11312 if (lt != -1 && (gt == -1 || lt > gt)) { 11313 continue; 11314 } 11315 } 11316 if (!isValidHtml(prefix)) { 11317 basePrefixPos = -1; 11318 } 11319 } 11320 break; 11321 } 11322 } 11323 inside = isHtml && c3 == ">"; 11324 } else { 11325 inside = c3 != "<"; 11326 } 11327 } 11328 basePrefixLen = baseLen - i4 + 1; 11329 if (basePrefixPos == -1 || basePrefixLen != baseLen) { 11330 i4 = 0; 11331 for (let inside = false; i4 < baseLen * (1 - PART_THRESHOLD) && baseSuffixPos == -1; i4++) { 11332 const c3 = base.charAt(i4); 11333 if (!inside) { 11334 if (baseLen - i4 <= maxBaseSuffixLen) { 11335 for (let from = curLen; true; from = baseSuffixPos - 1) { 11336 const suffix = base.slice(i4); 11337 baseSuffixPos = lastIndexInCur(suffix, from); 11338 if (isHtml && baseSuffixPos != -1) { 11339 if (suffix.indexOf(">") == -1) {
11340 const tmp = cur.slice(baseSuffixPos + baseLen - i4); 11341 const gt = tmp.indexOf(">"); 11342 const lt = tmp.indexOf("<"); 11343 if (gt != -1 && (lt == -1 || gt < lt)) { 11344 continue; 11345 } 11346 } 11347 if (!isValidHtml(suffix)) { 11348 baseSuffixPos = -1; 11349 } 11350 } 11351 break; 11352 } 11353 } 11354 inside = isHtml && c3 == "<"; 11355 } else { 11356 inside = c3 != ">"; 11357 } 11358 } 11359 baseSuffixLen = baseLen - i4 + 1; 11360 } 11361 const overlap = basePrefixPos + basePrefixLen - baseSuffixPos; 11362 if (basePrefixPos != -1 && baseSuffixPos != -1 && overlap > 0) { 11363 if (basePrefixLen >= baseSuffixLen) { 11364 if (basePrefixPos >= baseSuffixPos) { 11365 baseSuffixPos = -1; 11366 } else { 11367 baseSuffixPos += overlap; 11368 baseSuffixLen -= overlap; 11369 if (baseSuffixLen < baseLen * PART_THRESHOLD) { 11370 baseSuffixPos = -1; 11371 } 11372 } 11373 } else { 11374 if (basePrefixPos >= baseSuffixPos) { 11375 basePrefixPos = -1; 11376 } else { 11377 basePrefixLen -= overlap; 11378 if (basePrefixLen < baseLen * PART_THRESHOLD) { 11379 basePrefixPos = -1; 11380 } 11381 } 11382 } 11383 } 11384 const actions = new Array(); 11385 let pos = 0; 11386 if (basePrefixPos > pos) { 11387 actions.push(cur.substr(pos, basePrefixPos)); 11388 } 11389 if (basePrefixPos != -1) { 11390 if (basePrefixLen == baseLen) { 11391 actions.push([ 11392 0 11393 ]); 11394 } else if (basePrefixLen <= baseLen / 2) { 11395 actions.push([ 11396 0, 11397 basePrefixLen 11398 ]); 11399 } else { 11400 actions.push([ 11401 0, 11402 basePrefixLen - baseLen 11403 ]); 11404 } 11405 pos = basePrefixPos + basePrefixLen; 11406 } 11407 if (baseSuffixPos > pos) { 11408 actions.push(cur.slice(pos, baseSuffixPos)); 11409 } 11410 if (baseSuffixPos != -1) { 11411 if (basePrefixLen + baseSuffixLen == baseLen) { 11412 actions.push([ 11413 basePrefixLen 11414 ]); 11415 } else if (baseSuffixLen <= baseLen / 2) { 11416 actions.push([ 11417 -baseSuffixLen 11418 ]); 11419 } else { 11420 actions.push([ 11421 baseLen - baseSuffixLen 11422 ]); 11423 } 11424 pos = baseSuffixPos + baseSuffixLen; 11425 } 11426 if (pos < cur.length) { 11427 actions.push(cur.slice(pos)); 11428 } 11429 return actions; 11430} 11431function applyTextSplices(base, splices) { 11432 let text = ""; 11433 for (const splice of splices) { 11434 if (typeof splice == "string") { 11435 text += splice; 11436 } else { 11437 let pos = splice[0]; 11438 let len = (splice.length > 1 ? splice[1] : 0) | 0; 11439 if (pos < 0) { 11440 pos += base.length; 11441 } 11442 if (len <= 0) { 11443 len = base.length + len - pos; 11444 } 11445 text += base.substr(pos, len); 11446 } 11447 } 11448 return text; 11449} 11450
11451// src/modules/picker/private/fingerprint.ts 11452var FP_DEPTH = 6; 11453var FP_CLASSES = 2; 11454var VOLATILE = /^(is-|has-|js-|ng-|v-|sc-|css-)|(^|-)(active|open|opened|closed|hover|hovered|focus|focused|selected|current|sticky|stuck|visible|hidden|show|shown|collapsed|expanded|loading|loaded|animated|animating|in|out)$/i; 11455var GENERATED = /^[a-z]*[0-9a-f]{6,}$|^(css|sc|jsx|emotion)-|\d{4,}|-[0-9a-f]{6,}$|-\d{3,}$/i; 11456function isStableClass(name) { 11457 return name.length > 1 && name.length < 40 && !VOLATILE.test(name) && !GENERATED.test(name); 11458} 11459function stepOf(elem) { 11460 const classes = Array.from(elem.classList ?? []).filter(isStableClass).slice(0, FP_CLASSES); 11461 return classes.length ? { 11462 t: elem.localName, 11463 c: classes 11464 } : { 11465 t: elem.localName 11466 }; 11467} 11468function fingerprintOf(elem, depth = FP_DEPTH) { 11469 const out = []; 11470 for (let node = elem; node && out.length < depth; node = node.parentElement) { 11471 const tag = node.localName; 11472 if (tag === "body" || tag === "html") { 11473 break; 11474 } 11475 out.push(stepOf(node)); 11476 } 11477 return out; 11478} 11479function fingerprintMatches(elem, fp) { 11480 if (!fp || !fp.length) { 11481 return true; 11482 } 11483 let node = elem; 11484 for (const step of fp) { 11485 if (!node || node.localName !== step.t) { 11486 return false; 11487 } 11488 if (step.c?.length) { 11489 const has = Array.from(node.classList ?? []); 11490 if (!step.c.some((c3) => has.includes(c3))) { 11491 return false; 11492 } 11493 } 11494 node = node.parentElement; 11495 } 11496 return true; 11497} 11498
11499// src/modules/picker/private/selector.ts 11500var RE_VOLATILE_TAIL = /[_\-.][0-9A-F]{4,}$/i; 11501var RE_CSS_IN_JS = /^(?:css|sc|jsx|emotion|svelte|ng)-[0-9a-z]{4,}$/i; 11502var RE_OWN_CLASS = /^(?:\$|_st|personyze|qs-)/i; 11503function isStableName(name) { 11504 return !!name && !RE_VOLATILE_TAIL.test(name) && !RE_CSS_IN_JS.test(name) && !RE_OWN_CLASS.test(name); 11505} 11506function cssEscape(name) { 11507 return typeof CSS != "undefined" && CSS.escape ? CSS.escape(name) : name.replace(/[^\w-]/g, (ch) => "\\" + ch); 11508} 11509function matchesOnly(selector, elem) { 11510 if (!selector) { 11511 return false; 11512 } 11513 try { 11514 const found = elem.ownerDocument.querySelectorAll(selector); 11515 return found.length == 1 && found[0] == elem; 11516 } catch { 11517 return false; 11518 } 11519} 11520function indexOfType(elem) { 11521 let index = 1; 11522 for (const sib of elem.parentElement?.children ?? []) { 11523 if (sib == elem) { 11524 break; 11525 } 11526 if (sib.localName == elem.localName) { 11527 index++; 11528 } 11529 } 11530 return index; 11531} 11532function groupStep(localName, classNames) { 11533 let step = localName; 11534 for (const name of classNames) { 11535 if (isStableName(name)) { 11536 step += "." + cssEscape(name); 11537 } 11538 } 11539 return step; 11540} 11541function elemToStep(elem) { 11542 const step = groupStep(elem.localName, elem.classList); 11543 let nMatching = 0; 11544 for (const sib of elem.parentElement?.children ?? []) { 11545 try { 11546 if (sib.matches(step)) { 11547 nMatching++; 11548 } 11549 } catch { 11550 nMatching = 2; 11551 break; 11552 } 11553 } 11554 return nMatching > 1 ? `${step}:nth-of-type(${indexOfType(elem)})` : step; 11555} 11556function structuralPath(elem) { 11557 const doc = elem.ownerDocument; 11558 const steps = new Array(); 11559 for (let node = elem; node && node != doc.documentElement; node = node.parentElement) { 11560 if (node == doc.body) { 11561 steps.unshift("body"); 11562 break; 11563 } 11564 steps.unshift(`${node.localName}:nth-of-type(${indexOfType(node)})`); 11565 } 11566 return steps.join(" > "); 11567} 11568function elemToSelector(elem) { 11569 const doc = elem.ownerDocument; 11570 if (!doc || !doc.body || !doc.body.contains(elem)) { 11571 return ""; 11572 } 11573 const steps = new Array(); 11574 for (let node = elem; node && node != doc.body && node != doc.documentElement; node = node.parentElement) { 11575 const id = node.getAttribute("id") ?? ""; 11576 if (isStableName(id) && doc.querySelectorAll("#" + cssEscape(id)).length == 1) { 11577 steps.unshift("#" + cssEscape(id)); 11578 break; 11579 } 11580 steps.unshift(elemToStep(node)); 11581 const selector2 = steps.join(" > "); 11582 if (matchesOnly(selector2, elem)) { 11583 return selector2; 11584 } 11585 } 11586 const selector = steps.join(" > "); 11587 return matchesOnly(selector, elem) ? selector : structuralPath(elem); 11588} 11589var RE_NTH_OF_TYPE = /:nth-of-type\(\d+\)/g; 11590function elemToGroupSelector(elem) { 11591 const unique = elemToSelector(elem); 11592 if (!unique) { 11593 return ""; 11594 } 11595 const loose = unique.replace(RE_NTH_OF_TYPE, ""); 11596 if (findMatches(loose, elem.ownerDocument).length > 1) { 11597 return loose; 11598 } 11599 const own = groupStep(elem.localName, elem.classList); 11600 if (own.indexOf(".") != -1 && findMatches(own, elem.ownerDocument).length > 1) { 11601 return own; 11602 } 11603 return loose; 11604} 11605function findMatches(selector, doc = document) { 11606 if (!selector) { 11607 return []; 11608 } 11609 try { 11610 return Array.from(doc.querySelectorAll(selector)); 11611 } catch { 11612 return []; 11613 } 11614} 11615function describeElem(elem) { 11616 let text = elem.localName; 11617 if (elem.id) { 11618 text += "#" + elem.id; 11619 } 11620 for (const name of elem.classList) { 11621 text += "." + name; 11622 } 11623 return text; 11624} 11625function ancestorChain(elem) { 11626 const doc = elem.ownerDocument; 11627 const chain = new Array(); 11628 for (let node = elem; node && node != doc.body && node != doc.documentElement; node = node.parentElement) { 11629 chain.unshift(node); 11630 } 11631 return chain; 11632} 11633
11634// src/modules/picker/private/picker.ts 11635var Z_INDEX = 2147483646; 11636var Picker = class { 11637 #layer; 11638 #highlights = new Array(); 11639 #select; 11640 #hovered; 11641 #isTracking = false; 11642 #timer = 0; 11643 /** Outline these elements until the next call. `label` writes the element's name into the 11644 corner of the box, which is what makes a hover read as "this is what you'd get". 11645 **/ 11646 highlight(elems, color = "#12B886", label = false) { 11647 this.#highlights = []; 11648 for (const elem of elems) { 11649 if (elem && elem.isConnected) { 11650 this.#highlights.push({ 11651 elem, 11652 color, 11653 label 11654 }); 11655 } 11656 } 11657 this.#draw(); 11658 this.#trackViewport(this.#highlights.length > 0); 11659 } 11660 /** Enter picking mode: highlight what's under the cursor, and swallow the next click. 11661 **/ 11662 selectBegin(options) { 11663 this.selectEnd(); 11664 this.#select = options; 11665 this.#hovered = void 0; 11666 for (const type of [ 11667 "mousemove", 11668 "mouseover", 11669 "click", 11670 "mousedown", 11671 "mouseup", 11672 "keydown" 11673 ]) { 11674 document.addEventListener(type, this.#onEvent, true); 11675 } 11676 } 11677 selectEnd() { 11678 if (this.#select) { 11679 this.#select = void 0; 11680 this.#hovered = void 0; 11681 for (const type of [ 11682 "mousemove", 11683 "mouseover", 11684 "click", 11685 "mousedown", 11686 "mouseup", 11687 "keydown" 11688 ]) { 11689 document.removeEventListener(type, this.#onEvent, true); 11690 } 11691 this.highlight([]); 11692 } 11693 } 11694 isSelecting() { 11695 return !!this.#select; 11696 } 11697 destroy() { 11698 this.selectEnd(); 11699 this.highlight([]); 11700 this.#layer?.remove(); 11701 this.#layer = void 0; 11702 } 11703 /** Single capture-phase handler for the whole picking session. Capture phase and 11704 `stopPropagation()` are what keep the page from reacting: the click never reaches the 11705 page's own listeners, and `preventDefault()` keeps links and submit buttons inert. 11706 **/ 11707 #onEvent = (event) => { 11708 const select = this.#select; 11709 if (!select) { 11710 return; 11711 } 11712 if (event.type == "keydown") { 11713 if (event.key == "Escape") { 11714 event.preventDefault(); 11715 event.stopPropagation(); 11716 select.onSelect(null); 11717 } 11718 return; 11719 } 11720 const target = this.#targetOf(event, select.ignore); 11721 if (!target) { 11722 if (this.#hovered) { 11723 this.#hovered = void 0; 11724 this.highlight([]); 11725 select.onHover?.(null); 11726 } 11727 return; 11728 } 11729 event.preventDefault(); 11730 event.stopPropagation(); 11731 if (event.type == "click") { 11732 select.onSelect(target); 11733 } else if (event.type == "mousemove" || event.type == "mouseover") { 11734 if (this.#hovered != target) { 11735 this.#hovered = target; 11736 this.highlight([ 11737 target 11738 ], "#12B886", true); 11739 select.onHover?.(target); 11740 } 11741 } 11742 }; 11743 /** The element the event points at, or null when it's the tool's own UI. `composedPath()` is 11744 what sees through the panel's shadow root, whose events retarget to the host element. 11745 **/ 11746 #targetOf(event, ignore) { 11747 const path = typeof event.composedPath == "function" ? event.composedPath() : []; 11748 const target = path[0] ?? event.target; 11749 if (!(target instanceof Element)) { 11750 return null; 11751 } 11752 for (const root of ignore) { 11753 if (root == target || root.contains(target) || path.indexOf(root) != -1) { 11754 return null; 11755 } 11756 } 11757 return target; 11758 } 11759 #draw = () => { 11760 const layer = this.#ensureLayer(); 11761 while (layer.childNodes.length > this.#highlights.length) { 11762 layer.lastChild.remove(); 11763 } 11764 while (layer.childNodes.length < this.#highlights.length) { 11765 const box = document.createElement("div"); 11766 box.appendChild(document.createElement("span")); 11767 layer.appendChild(box); 11768 } 11769 for (let i4 = 0; i4 < this.#highlights.length; i4++) { 11770 const { elem, color, label } = this.#highlights[i4]; 11771 const box = layer.childNodes[i4]; 11772 const rect = elem.getBoundingClientRect(); 11773 box.style.cssText = `position:absolute; box-sizing:border-box; pointer-events:none; border:2px solid ${color}; background-color:${color}22; border-radius:2px; left:${rect.left + globalThis.scrollX}px; top:${rect.top + globalThis.scrollY}px; width:${Math.max(rect.width, 4)}px; height:${Math.max(rect.height, 4)}px`; 11774 const tag = box.firstChild; 11775 tag.textContent = !label ? "" : describeElem(elem).slice(0, 80); 11776 tag.style.cssText = !label ? "display:none" : `position:absolute; left:0; top:-1.5em; max-width:100%; overflow:hidden; white-space:nowrap; box-sizing:border-box; padding:0 0.4em; border-radius:2px; background-color:${color}; color:#08231C; font:600 11px/1.5em ui-monospace, Menlo, Consolas, monospace`; 11777 } 11778 }; 11779 #ensureLayer() { 11780 let layer = this.#layer; 11781 if (!layer?.isConnected) { 11782 layer = document.createElement("div"); 11783 layer.style.cssText = `position:absolute; left:0; top:0; width:0; height:0; margin:0; padding:0; border:none; pointer-events:none; z-index:${Z_INDEX}`; 11784 document.body.appendChild(layer); 11785 this.#layer = layer; 11786 } 11787 return layer; 11788 } 11789 /** Highlight boxes are placed in page coordinates, so they only need redrawing when the page 11790 moves under them - but an element can also be resized by the page itself, hence the poll. 11791 **/ 11792 #trackViewport(on) { 11793 if (on != this.#isTracking) { 11794 this.#isTracking = on; 11795 if (on) { 11796 globalThis.addEventListener("scroll", this.#draw, true); 11797 globalThis.addEventListener("resize", this.#draw); 11798 this.#timer = setInterval(this.#draw, 500); 11799 } else { 11800 globalThis.removeEventListener("scroll", this.#draw, true); 11801 globalThis.removeEventListener("resize", this.#draw); 11802 clearInterval(this.#timer); 11803 } 11804 } 11805 } 11806}; 11807
11808// src/modules/util/mod.ts 11809var RE_CSS_RULE = /([^;:\s]+)\s*:\s*([^;]*[^;\s])/g; 11810function parseStyleAttr(style) { 11811 const result = {}; 11812 if (style) { 11813 RE_CSS_RULE.lastIndex = 0; 11814 let m2; 11815 while (m2 = RE_CSS_RULE.exec(style)) { 11816 result[m2[1].toLowerCase()] = m2[2]; 11817 } 11818 } 11819 return result; 11820} 11821function styleRecordToCss(record) { 11822 let result = ""; 11823 for (const name in record) { 11824 const value = record[name]; 11825 if (value != void 0 && value !== "") { 11826 result += (result ? "; " : "") + name + ":" + value; 11827 } 11828 } 11829 return result; 11830} 11831
11832// src/modules/tracker/private/tracker.ts 11833function getPageUrl() { 11834 try { 11835 return getTracker()?.get_location?.()?.href || location.href; 11836 } catch { 11837 return location.href; 11838 } 11839} 11840function getTracker() { 11841 return globalThis._S_T; 11842} 11843function getPanelWindow() { 11844 const scope = globalThis; 11845 const thisWindow = globalThis; 11846 return scope._S_T_OPENER ?? globalThis.opener ?? (parent != thisWindow ? parent : null); 11847} 11848
11849// src/modules/pageeditor/private/pageEditor.ts 11850var XML_ELEMENT_NODE2 = 1; 11851var XML_TEXT_NODE2 = 3; 11852var XML_CDATA_SECTION_NODE = 4; 11853var elemPrevChanges = /* @__PURE__ */ new WeakMap(); 11854var insertedBy = /* @__PURE__ */ new WeakMap(); 11855function cloneElem(elem) { 11856 const copy = elem.cloneNode(true); 11857 const changes = elemPrevChanges.get(elem); 11858 if (changes) { 11859 elemPrevChanges.set(copy, changes.map((v3) => ({ 11860 n: v3.n, 11861 change: v3.change 11862 }))); 11863 } 11864 const insert = insertedBy.get(elem); 11865 if (insert) { 11866 insertedBy.set(copy, insert); 11867 } 11868 return copy; 11869} 11870function insertOf(elem) { 11871 return insertedBy.get(elem); 11872} 11873function getElemPrevChanges(elem) { 11874 return elemPrevChanges.get(elem) ?? []; 11875} 11876function resolveQelem(qelemId, findAll) { 11877 if (qelemId != void 0) { 11878 const tracker = getTracker(); 11879 if (tracker?.qelem) { 11880 try { 11881 const found = tracker.qelem(qelemId, findAll); 11882 if (findAll) { 11883 return Array.isArray(found) ? found.filter((v3) => v3 instanceof Element) : []; 11884 } 11885 return found instanceof Element ? found : null; 11886 } catch (e3) { 11887 console.error(e3); 11888 } 11889 } 11890 if (typeof qelemId == "string") { 11891 try { 11892 if (findAll) { 11893 return Array.from(document.querySelectorAll(qelemId)); 11894 } 11895 return document.querySelector(qelemId); 11896 } catch { 11897 } 11898 } 11899 } 11900 return findAll ? [] : null; 11901} 11902function qelemIdToText(qelemId) { 11903 if (qelemId == void 0) { 11904 return ""; 11905 } 11906 if (typeof qelemId == "string") { 11907 return qelemId; 11908 } 11909 try { 11910 return getTracker()?.qelem_id_to_css?.(qelemId) ?? JSON.stringify(qelemId); 11911 } catch { 11912 return JSON.stringify(qelemId); 11913 } 11914} 11915function isTextNode(node) { 11916 return !!node && (node.nodeType == XML_TEXT_NODE2 || node.nodeType == XML_CDATA_SECTION_NODE); 11917} 11918function getAttributes(elem) { 11919 const attrs = {}; 11920 for (const attr of elem.attributes) { 11921 attrs[attr.name] = attr.value; 11922 } 11923 return attrs; 11924} 11925function setAttributes(elem, attrs) { 11926 if (attrs) { 11927 for (const name in attrs) { 11928 const value = attrs[name]; 11929 try { 11930 if (value == null) { 11931 elem.removeAttribute(name); 11932 } else { 11933 elem.setAttribute(name, value); 11934 } 11935 } catch (e3) { 11936 console.error(e3); 11937 } 11938 } 11939 } 11940} 11941function newElemChanges(spec) { 11942 if (typeof spec == "string") { 11943 return document.createTextNode(spec); 11944 } 11945 const [nodeName, nodeAttrs, nodeChildNodes, nodeInnerHtml] = spec; 11946 let elem = document.createElement(nodeName); 11947 if ((nodeName == "video" || nodeName == "audio") && typeof elem.play != "function") { 11948 elem = document.createElement("embed"); 11949 } 11950 setAttributes(elem, nodeAttrs); 11951 if (nodeInnerHtml != null) { 11952 elem.innerHTML = nodeInnerHtml; 11953 } else if (nodeChildNodes) { 11954 for (const child of nodeChildNodes) { 11955 elem.appendChild(newElemChanges(child)); 11956 } 11957 } 11958 return elem; 11959} 11960function patchChildren(elem, newElem) { 11961 let child = elem.firstChild; 11962 let newChild = newElem.firstChild; 11963 while (child || newChild) { 11964 let next = child?.nextSibling ?? null; 11965 let newNext = newChild?.nextSibling ?? null; 11966 if (!child) { 11967 elem.appendChild(newChild); 11968 } else if (!newChild) { 11969 elem.removeChild(child); 11970 } else if (child.nodeType != XML_ELEMENT_NODE2) { 11971 if (child.nodeType == XML_TEXT_NODE2 && newChild.nodeType == XML_TEXT_NODE2) { 11972 child.data = newChild.data; 11973 } else { 11974 elem.removeChild(child); 11975 newNext = newChild; 11976 } 11977 } else if (newChild.nodeType != XML_ELEMENT_NODE2) { 11978 elem.insertBefore(newChild, child); 11979 next = child; 11980 } else if (child.nodeName != newChild.nodeName) { 11981 let hasElemToPatch = false; 11982 for (let n2 = newNext; n2 && !hasElemToPatch; n2 = n2.nextElementSibling) { 11983 hasElemToPatch = child.nodeName == n2.nodeName; 11984 } 11985 if (hasElemToPatch) { 11986 elem.insertBefore(newChild, child); 11987 next = child; 11988 } else { 11989 elem.removeChild(child); 11990 newNext = newChild; 11991 } 11992 } else { 11993 const names = new Array(); 11994 for (const attr of newChild.attributes) { 11995 try { 11996 child.setAttribute(attr.name, attr.value); 11997 names.push(attr.name); 11998 } catch (e3) { 11999 console.error(e3); 12000 } 12001 } 12002 const childAttrs = child.attributes; 12003 for (let i4 = 0; i4 < childAttrs.length; i4++) { 12004 const attr = childAttrs[i4];
12005 if (names.indexOf(attr.name) == -1) { 12006 child.removeAttribute(attr.name); 12007 i4--; 12008 } 12009 } 12010 patchChildren(child, newChild); 12011 } 12012 child = next; 12013 newChild = newNext; 12014 } 12015} 12016function editTextOrHtml(node, isHtml, splices) { 12017 const base = isHtml ? node.innerHTML : node.data; 12018 const text = applyTextSplices(base, splices); 12019 if (!isHtml) { 12020 node.data = text; 12021 } else { 12022 const replacement = document.createElement(node.nodeName); 12023 replacement.innerHTML = text; 12024 patchChildren(node, replacement); 12025 } 12026} 12027function pickNode(refNode, where) { 12028 return where == "before" ? refNode.previousSibling : where == "after" ? refNode.nextSibling : refNode.lastChild; 12029} 12030function putElem(elem, newNode, where, removeNodes) { 12031 let elemParentNode = null; 12032 let elemNextSibling = null; 12033 if (newNode.contains(elem)) { 12034 elemParentNode = newNode.parentNode; 12035 elemNextSibling = newNode.nextSibling; 12036 } 12037 const beforeNode = where == "before" ? elem : where == "after" ? elem.nextSibling : null; 12038 const insideNode = where == "inside" ? elem : elem.parentNode; 12039 insideNode?.insertBefore(newNode, beforeNode); 12040 elemParentNode?.insertBefore(elem, elemNextSibling); 12041 removeNodes.push({ 12042 node: newNode, 12043 beforeNode, 12044 insideNode 12045 }); 12046} 12047function applyHtmlChangeOnElem(elem, change, removeNodes, addNodes) { 12048 let changed = false; 12049 if (change.inner_html) { 12050 editTextOrHtml(elem, true, change.inner_html); 12051 changed = true; 12052 } 12053 if (change.text) { 12054 const { where } = change; 12055 let node = pickNode(elem, where); 12056 if (isTextNode(node)) { 12057 let refNode = node; 12058 while (true) { 12059 const furtherNode = pickNode(refNode, where == "inside" ? "before" : where); 12060 if (!isTextNode(furtherNode)) { 12061 break; 12062 } 12063 if (furtherNode.length > 0) { 12064 if (where == "after") { 12065 node.data = node.data + furtherNode.data; 12066 addNodes.push({ 12067 node: furtherNode, 12068 afterNode: refNode 12069 }); 12070 } else { 12071 node.data = furtherNode.data + node.data; 12072 addNodes.push({ 12073 node: furtherNode, 12074 beforeNode: refNode 12075 }); 12076 } 12077 furtherNode.parentNode?.removeChild(furtherNode); 12078 } else { 12079 refNode = furtherNode; 12080 } 12081 } 12082 } else { 12083 node = document.createTextNode(""); 12084 putElem(elem, node, where, removeNodes); 12085 } 12086 editTextOrHtml(node, false, change.text); 12087 } 12088 if (elem.nodeType == XML_ELEMENT_NODE2) { 12089 setAttributes(elem, change.attrs); 12090 } 12091 if (change.style) { 12092 changed = true; 12093 } 12094 if (change.attrs) { 12095 if (change.attrs["class"] != null) { 12096 elem.className = change.attrs["class"]; 12097 } 12098 changed = true; 12099 } 12100 if (change.dup) { 12101 for (let k3 = change.dup - 1; k3 >= 0; k3--) { 12102 const copy = cloneElem(elem); 12103 elem.parentNode?.insertBefore(copy, elem); 12104 removeNodes.push({ 12105 node: copy, 12106 beforeNode: elem 12107 }); 12108 changed = true; 12109 } 12110 } 12111 if (change.mv) { 12112 const target = resolveQelem(change.mv_qelem_id); 12113 if (target) { 12114 const stub = document.createTextNode(""); 12115 elem.parentNode?.insertBefore(stub, elem); 12116 putElem(target, cloneElem(elem), change.mv, removeNodes); 12117 elem.parentNode?.removeChild(elem); 12118 elem = stub; 12119 changed = true; 12120 } 12121 } 12122 if (change.rm_shallow && elem.firstChild) { 12123 while (elem.firstChild) { 12124 removeNodes.push({ 12125 node: elem.firstChild, 12126 beforeNode: elem 12127 }); 12128 elem.parentNode?.insertBefore(elem.firstChild, elem); 12129 } 12130 elem.parentNode?.removeChild(elem); 12131 elem = removeNodes.pop().node; 12132 changed = true; 12133 } 12134 if (change.rm || change.rm_shallow && !elem.firstChild) { 12135 const stub = document.createTextNode(""); 12136 elem.parentNode?.insertBefore(stub, elem); 12137 elem.parentNode?.removeChild(elem); 12138 elem = stub; 12139 changed = true; 12140 } 12141 if (change.e_shallow || change.e) { 12142 const added = newElemChanges(change.e_shallow ?? change.e); 12143 if (change.e) { 12144 insertedBy.set(added, change); 12145 putElem(elem, added, change.where, removeNodes); 12146 } else { 12147 let nNext = Number(change.n_next) || 1; 12148 elem.parentNode?.insertBefore(added, elem); 12149 added.appendChild(elem); 12150 while (--nNext > 0 && added.nextSibling) { 12151 const sibling = added.nextSibling; 12152 const copy = cloneElem(sibling); 12153 sibling.parentNode?.removeChild(sibling); 12154 addNodes.push({ 12155 node: sibling, 12156 afterNode: elem 12157 }); 12158 added.appendChild(copy); 12159 removeNodes.push({ 12160 node: added.firstChild, 12161 insideNode: added 12162 }); 12163 } 12164 elem = added; 12165 } 12166 changed = true; 12167 } 12168 if (change.events) { 12169 changed = true; 12170 } 12171 if (change.delay) { 12172 changed = true; 12173 } 12174 return changed ? elem : null; 12175} 12176var ElemSnapshot = class { 12177 change = null; 12178 #elem; 12179 #qelemId; 12180 /** Taken here, and not when the change is saved, because by then the element may be a wrapper 12181 that did not exist yet, or a copy that replaced the one the user pointed at. **/ 12182 #fp; 12183 #elems = []; 12184 #backupElems = []; 12185 #removeNodes = []; 12186 #addNodes = []; 12187 #nSnap; 12188 constructor(elem, qelemId, nSnap) { 12189 this.#elem = elem; 12190 this.#nSnap = nSnap; 12191 this.#fp = elem.nodeType == XML_ELEMENT_NODE2 ? fingerprintOf(elem) : []; 12192 this.#qelemId = qelemId ?? (elem.nodeType == XML_ELEMENT_NODE2 ? elemToSelector(elem) : ""); 12193 for (const found of resolveQelem(this.#qelemId, true)) { 12194 this.#elems.push(found); 12195 this.#backupElems.push(cloneElem(found)); 12196 } 12197 if (this.#elems.length == 0) { 12198 this.#elems.push(elem); 12199 this.#backupElems.push(cloneElem(elem)); 12200 } 12201 this.undoOrRedo(); 12202 } 12203 getSubjElem() { 12204 return this.#elem; 12205 } 12206 getQelemId() { 12207 return this.#qelemId; 12208 } 12209 /** Swap the working copies with the pristine ones, and the inserted nodes with the removed one
12209s. 12210 Being its own inverse is what makes this both undo and redo. 12211 **/ 12212 undoOrRedo(withScroll) { 12213 for (let i4 = 0; i4 < this.#elems.length; i4++) { 12214 const curElem = this.#elems[i4]; 12215 const { parentElement } = curElem; 12216 if (parentElement) { 12217 parentElement.insertBefore(this.#backupElems[i4], curElem); 12218 parentElement.removeChild(curElem); 12219 if (curElem == this.#elem) { 12220 this.#elem = this.#backupElems[i4]; 12221 } 12222 } 12223 } 12224 const tmpElems = this.#elems; 12225 this.#elems = this.#backupElems; 12226 this.#backupElems = tmpElems; 12227 for (const { node } of this.#removeNodes) { 12228 node.parentNode?.removeChild(node); 12229 } 12230 for (const ref of this.#addNodes) { 12231 if (ref.beforeNode) { 12232 ref.beforeNode.parentNode?.insertBefore(ref.node, ref.beforeNode); 12233 } else if (ref.afterNode) { 12234 ref.afterNode.parentNode?.insertBefore(ref.node, ref.afterNode.nextSibling); 12235 } else { 12236 ref.insideNode?.appendChild(ref.node); 12237 } 12238 } 12239 const tmpNodes = this.#removeNodes; 12240 this.#removeNodes = this.#addNodes; 12241 this.#addNodes = tmpNodes; 12242 if (withScroll && this.#elem?.nodeType == XML_ELEMENT_NODE2) { 12243 try { 12244 this.#elem.scrollIntoView({ 12245 block: "center" 12246 }); 12247 } catch (e3) { 12248 console.error(e3); 12249 } 12250 } 12251 return this.#elem; 12252 } 12253 /** What the element's ancestry looked like when this snapshot opened. **/ 12254 getFingerprint() { 12255 return this.#fp; 12256 } 12257 /** Measure what the user did to the working copy and record it. Returns the subject element, or 12258 undefined when nothing changed. 12259 **/ 12260 saveDiff() { 12261 let backupElem; 12262 for (let i4 = 0; i4 < this.#elems.length; i4++) { 12263 if (this.#elems[i4] === this.#elem) { 12264 backupElem = this.#backupElems[i4]; 12265 break; 12266 } 12267 } 12268 if (!backupElem || this.#elem.nodeType != XML_ELEMENT_NODE2) { 12269 return; 12270 } 12271 const elem = this.#elem; 12272 const change = {}; 12273 analizeAttrAndStyleChanges(backupElem, elem, change); 12274 if (elem.innerHTML != backupElem.innerHTML) { 12275 change.inner_html = getEditTextOrHtml(backupElem.innerHTML, elem.innerHTML, true); 12276 } 12277 if (change.style || change.attrs || change.inner_html) { 12278 change.qelem_id = this.#qelemId; 12279 return this.#applyHtmlChange(change, true) ?? void 0; 12280 } 12281 } 12282 /** Record a change the caller describes outright (delete, duplicate, insert, an event...). 12283 **/ 12284 customChange(change) { 12285 if (!change) { 12286 return this.#elem; 12287 } 12288 change.qelem_id = this.#qelemId; 12289 return this.#applyHtmlChange(change); 12290 } 12291 /** Run `change` over every element the selector matched. `isEdited` means the subject element 12292 already carries the new innerHTML (the user typed it), so only its siblings need that part. 12293 **/ 12294 #applyHtmlChange(change, isEdited) { 12295 for (let i4 = 0; i4 < this.#elems.length; i4++) { 12296 const elem = this.#elems[i4]; 12297 const alreadyChanged = !!change.inner_html && isEdited && elem == this.#elem; 12298 let changeToApply = change; 12299 if (alreadyChanged) { 12300 changeToApply = { 12301 ...change 12302 }; 12303 delete changeToApply.inner_html; 12304 } 12305 let after = applyHtmlChangeOnElem(elem, changeToApply, this.#removeNodes, this.#addNodes); 12306 if (alreadyChanged) { 12307 after = elem; 12308 } 12309 if (after && after != elem) { 12310 this.#elems[i4] = after; 12311 if (elem === this.#elem) { 12312 this.#elem = after; 12313 } 12314 } 12315 if (!after) { 12316 return null; 12317 } 12318 const prev = elemPrevChanges.get(elem); 12319 if (prev) { 12320 prev.push({ 12321 n: this.#nSnap, 12322 change 12323 }); 12324 } else { 12325 elemPrevChanges.set(elem, [ 12326 { 12327 n: this.#nSnap, 12328 change 12329 } 12330 ]); 12331 } 12332 } 12333 this.change = change; 12334 return this.#elem; 12335 } 12336}; 12337function analizeAttrAndStyleChanges(backupElem, elem, change) { 12338 const attrs = {}; 12339 const elemAttributes = getAttributes(elem); 12340 const backupAttributes = getAttributes(backupElem); 12341 for (const name in elemAttributes) { 12342 if (name != "style" && name != "contenteditable" && backupAttributes[name] != elemAttributes[name]) { 12343 attrs[name] = elemAttributes[name]; 12344 change.attrs = attrs; 12345 } 12346 } 12347 for (const name in backupAttributes) { 12348 if (name != "style" && name != "contenteditable" && !(name in attrs) && elemAttributes[name] != backupAttributes[name]) { 12349 attrs[name] = null; 12350 change.attrs = attrs; 12351 } 12352 } 12353 const elemStyle = elem.style; 12354 const backupStyle = backupElem.style; 12355 const style = {}; 12356 const styleRuleNames = new Array(); 12357 for (const name in elemStyle) { 12358 if (name != "cssText" && name != "length" && isNaN(parseInt(name)) && typeof elemStyle[name] == "string") { 12359 if (elemStyle[name] != backupStyle[name]) { 12360 styleRuleNames.push(name); 12361 style[name] = elemStyle[name]; 12362 } 12363 } 12364 } 12365 styleRuleNames.sort(); 12366 for (let i4 = 0, iEnd = styleRuleNames.length - 1; i4 < iEnd; i4++) { 12367 if (styleRuleNames[i4 + 1].indexOf(styleRuleNames[i4]) == 0) { 12368 delete style[styleRuleNames[i4]]; 12369 styleRuleNames.splice(i4--, 1); 12370 iEnd--; 12371 } 12372 } 12373 let styleAttr = ""; 12374 for (const name in style) { 12375 if (style[name] !== "") { 12376 styleAttr += "; " + name.replace(/[A-Z]/g, (m2) => "-" + m2.toLowerCase()) + ":" + style[name]; 12377 } 12378 } 12379 if (styleAttr != (backupAttributes.style || "")) { 12380 const rules = parseStyleAttr((elemAttributes.style || "") + styleAttr); 12381 attrs.style = styleRecordToCss(rules) || null; 12382 change.attrs = attrs; 12383 } 12384} 12385function pageUrl() { 12386 return getPageUrl(); 12387} 12388function changeAppliesHere(change) { 12389 if (change.scope == "page") { 12390 return sameScopeUrl(change.scope_url, pageUrl()); 12391 } 12392 if (change.scope == "shape" && change.scope_fp?.length) { 12393 const elems = resolveQelem(change.qelem_id, true); 12394 return elems.length == 0 || elems.some((elem) => elem.nodeType == XML_ELEMENT_NODE2 && fingerprintMatches(elem, change.scope_fp)); 12395 } 12396 return true; 12397} 12398var PageEditor = class { 12399 #snaps = []; 12400 #curSnap = null; 12401 #n = 0; 12402 #options; 12403 #pendingTimer = 0; 12404 #bypass = false;
12405 /** The change objects this editor is REPLAYING rather than authoring - see [[#stampScope]]. 12406 12407 Membership, not a flag that is on for a while: replaying is asynchronous (it waits, a second 12408 at a time, for an element that has not rendered yet) and the user can edit something else in 12409 the meantime. A flag would be on during that edit and silently leave it unscoped. 12410 **/ 12411 #replayed = /* @__PURE__ */ new WeakSet(); 12412 constructor(options = {}) { 12413 this.#options = options; 12414 } 12415 /** Begin editing `elem`. Returns the working copy the caller must use from now on - the element 12416 it passed in is no longer the one in the page. 12417 **/ 12418 moveCursor(elem, qelemId) { 12419 this.discardAtCursor(); 12420 this.#curSnap = new ElemSnapshot(elem, qelemId, this.#n); 12421 return this.#curSnap.getSubjElem(); 12422 } 12423 /** The `qelem_id` the open snapshot is recording against. **/ 12424 cursorQelemId() { 12425 return this.#curSnap?.getQelemId(); 12426 } 12427 hasCursor() { 12428 return !!this.#curSnap; 12429 } 12430 changeDup(dupTimes) { 12431 return this.#requireCursor().customChange({ 12432 dup: dupTimes 12433 }); 12434 } 12435 changeMv(where, toElem, toQelemId) { 12436 const snap = this.#requireCursor(); 12437 try { 12438 toQelemId = toQelemId || (toElem ? elemToSelector(toElem) : ""); 12439 return snap.customChange({ 12440 mv: where, 12441 mv_qelem_id: toQelemId 12442 }); 12443 } catch (e3) { 12444 console.error(e3); 12445 return null; 12446 } 12447 } 12448 changeRm() { 12449 return this.#requireCursor().customChange({ 12450 rm: true 12451 }); 12452 } 12453 changeCustom(change) { 12454 return this.#requireCursor().customChange(change); 12455 } 12456 changesCustom(changes) { 12457 let result = null; 12458 for (const change of changes) { 12459 result = this.changeCustom(change) || result; 12460 } 12461 return result; 12462 } 12463 /** Close the open snapshot, keeping whatever it turned out to be. A snapshot that amounts to 12464 nothing is rolled back instead of becoming an empty entry. 12465 **/ 12466 saveAtCursor() { 12467 let elem; 12468 const snap = this.#curSnap; 12469 if (snap) { 12470 if (!snap.change) { 12471 elem = snap.saveDiff(); 12472 } 12473 if (snap.change) { 12474 this.#stampScope(snap); 12475 this.#snaps[this.#n++] = snap; 12476 this.#snaps.length = this.#n; 12477 this.#curSnap = null; 12478 this.#options.onChange?.(); 12479 } else { 12480 elem = snap.undoOrRedo(); 12481 } 12482 this.#curSnap = null; 12483 } 12484 return elem; 12485 } 12486 discardAtCursor() { 12487 if (this.#curSnap) { 12488 this.#curSnap.undoOrRedo(); 12489 this.#curSnap = null; 12490 } 12491 } 12492 undo(withScroll) { 12493 this.discardAtCursor(); 12494 if (this.#n > 0) { 12495 this.#snaps[this.#n - 1].undoOrRedo(withScroll); 12496 this.#n--; 12497 this.#options.onChange?.(); 12498 } 12499 } 12500 redo(withScroll) { 12501 this.discardAtCursor(); 12502 if (this.#n < this.#snaps.length) { 12503 this.#snaps[this.#n].undoOrRedo(withScroll); 12504 this.#n++; 12505 this.#options.onChange?.(); 12506 } 12507 } 12508 canUndo() { 12509 return this.#n > 0; 12510 } 12511 canRedo() { 12512 return this.#n < this.#snaps.length; 12513 } 12514 getNChanges(includeUndone) { 12515 return includeUndone ? this.#snaps.length : this.#n; 12516 } 12517 getChanges(includeUndone) { 12518 const changes = new Array(); 12519 const till = includeUndone ? this.#snaps.length : this.#n; 12520 for (let i4 = 0; i4 < till; i4++) { 12521 changes.push(this.#snaps[i4].change); 12522 } 12523 return changes; 12524 } 12525 /** Replay a change list onto this page - what happens when the panel hands the action's stored 12526 edits to a freshly loaded page. 12527 12528 Asynchronous because an element may simply not be there yet: the legacy retried once a second 12529 and offered a "Bypass" button, and so does this ([[bypassPending]]). 12530 **/ 12531 addChanges(changes) { 12532 clearTimeout(this.#pendingTimer); 12533 if (!changes || changes.length == 0) { 12534 this.#options.onPending?.(null); 12535 return Promise.resolve(); 12536 } 12537 const remaining = changes.map((v3) => structuredClone(v3)); 12538 return new Promise((resolve) => { 12539 const addRemaining = () => { 12540 while (remaining.length) { 12541 const change = remaining[0]; 12542 const elem = resolveQelem(change.qelem_id); 12543 if (!elem) { 12544 if (this.#bypass) { 12545 this.#bypass = false;
12546 remaining.shift(); 12547 continue; 12548 } 12549 this.#options.onPending?.({ 12550 change, 12551 target: qelemIdToText(change.qelem_id) 12552 }); 12553 this.#pendingTimer = setTimeout(addRemaining, 1e3); 12554 return; 12555 } 12556 remaining.shift(); 12557 this.#replayed.add(change); 12558 this.moveCursor(elem, change.qelem_id); 12559 this.changeCustom(change); 12560 this.saveAtCursor(); 12561 } 12562 this.#options.onPending?.(null); 12563 resolve(); 12564 }; 12565 addRemaining(); 12566 }); 12567 } 12568 /** Skip the change [[addChanges]] is waiting on. **/ 12569 bypassPending() { 12570 this.#bypass = true; 12571 } 12572 /** Apply a change list without waiting for anything - used when the caller already knows every 12573 element is there. 12574 **/ 12575 addChangesSync(changes, areNew) { 12576 if (changes) { 12577 for (const change of changes) { 12578 if (!areNew) { 12579 this.#replayed.add(change); 12580 } 12581 const elem = resolveQelem(change.qelem_id); 12582 if (elem) { 12583 this.moveCursor(elem, change.qelem_id); 12584 this.changeCustom(change); 12585 this.saveAtCursor(); 12586 } 12587 } 12588 } 12589 } 12590 /** Undo everything and forget it. `noUndo` leaves the page as it is and only clears the list, 12591 which is what a navigation to another URL needs. 12592 **/ 12593 reset(noUndo) { 12594 clearTimeout(this.#pendingTimer); 12595 this.discardAtCursor(); 12596 if (!noUndo) { 12597 while (this.#n > 0) { 12598 this.#snaps[this.#n - 1].undoOrRedo(); 12599 this.#n--; 12600 } 12601 } 12602 this.#n = 0; 12603 this.#snaps = []; 12604 this.#options.onPending?.(null); 12605 this.#options.onChange?.(); 12606 } 12607 getElemPrevChanges(elem) { 12608 return getElemPrevChanges(elem); 12609 } 12610 /** Replace an already-recorded change in place: undo back to it, substitute, redo the rest. Used 12611 by the on-click editor, which edits the events of a change made earlier rather than adding a 12612 new one. Port of `set_elem_prev_changes_changes()`. 12613 **/ 12614 setElemPrevChangesChanges(elem, changesChanges) { 12615 this.discardAtCursor(); 12616 if (changesChanges) { 12617 for (const { change, n: nSnap } of changesChanges) { 12618 const neChanges = elemPrevChanges.get(elem); 12619 let iOrig = -1; 12620 if (neChanges) { 12621 for (let j3 = 0; j3 < neChanges.length; j3++) { 12622 if (neChanges[j3].n == nSnap) { 12623 iOrig = j3; 12624 break; 12625 } 12626 } 12627 } 12628 if (iOrig == -1) { 12629 throw new Error(`Change #${nSnap} not found`); 12630 } 12631 const changeOrig = neChanges[iOrig].change; 12632 if (JSON.stringify(change) != JSON.stringify(changeOrig)) { 12633 const forRedo = new Array(); 12634 while (this.#n > nSnap) { 12635 forRedo.unshift(this.#snaps[this.#n - 1].change); 12636 this.undo(); 12637 } 12638 forRedo[0] = change; 12639 this.addChangesSync(forRedo); 12640 } 12641 } 12642 } 12643 } 12644 /** Record how widely this change is allowed to land, at the moment it becomes an entry. 12645 12646 TWO THINGS ARE DELIBERATELY LEFT ALONE. A change that is only being REPLAYED - the action's 12647 stored list coming back onto the page, or the undo/redo dance `setElemPrevChangesChanges` 12648 does - keeps whatever it already said, including saying nothing. And a change that already 12649 carries a scope is never re-stamped. Between them, an action authored before this existed 12650 goes on applying exactly where it always did, even after it is opened and re-saved. 12651 **/ 12652 #stampScope(snap) { 12653 const change = snap.change; 12654 if (!change || this.#replayed.has(change) || change.scope != void 0) { 12655 return; 12656 } 12657 const device = this.#options.getDevice?.(); 12658 if (device) { 12659 change.device = device; 12660 } 12661 const scope = this.#options.getScope?.() ?? "any"; 12662 change.scope = scope; 12663 if (scope == "page") { 12664 change.scope_url = pageUrl(); 12665 } else if (scope == "shape") { 12666 const fp = snap.getFingerprint(); 12667 if (fp.length) { 12668 change.scope_fp = fp; 12669 } 12670 } 12671 } 12672 /** Change how widely an ALREADY RECORDED change may land. 12673 12674 Answering "Apply on" at save time is answering it at the only moment somebody is thinking 12675 about that one edit - but getting it wrong then meant undoing the edit and making it again. 12676 The scope is not part of what the change DOES, so it can be rewritten in place: the entry
12677 keeps its selector, its splices and its position in the list. 12678 12679 The fingerprint is taken FROM THE PAGE AS IT IS NOW, not from what the snapshot remembers. 12680 They are the same thing while the editor is still on the page the change was made on, and 12681 on any other page there is nothing honest to record - so a shape that cannot be measured 12682 here leaves the change unscoped rather than pinned to the wrong structure. 12683 **/ 12684 /** Would [[setChangeScope]] accept this, without doing it? Asked when a whole group of changes 12685 is being re-scoped together: half a group narrowed and half left wide is the state the 12686 grouping exists to prevent. 12687 **/ 12688 canSetChangeScope(index, scope) { 12689 const change = this.#snaps[index]?.change; 12690 if (!change) { 12691 return false; 12692 } 12693 if (scope != "shape") { 12694 return true; 12695 } 12696 const elem = resolveQelem(change.qelem_id); 12697 return elem?.nodeType == XML_ELEMENT_NODE2 && fingerprintOf(elem).length > 0; 12698 } 12699 setChangeScope(index, scope) { 12700 const change = this.#snaps[index]?.change; 12701 if (!change) { 12702 return false; 12703 } 12704 let fp = []; 12705 if (scope == "shape") { 12706 const elem = resolveQelem(change.qelem_id); 12707 fp = elem?.nodeType == XML_ELEMENT_NODE2 ? fingerprintOf(elem) : []; 12708 if (!fp.length) { 12709 return false; 12710 } 12711 } 12712 delete change.scope_url; 12713 delete change.scope_fp; 12714 change.scope = scope; 12715 if (scope == "page") { 12716 change.scope_url = pageUrl(); 12717 } else if (fp.length) { 12718 change.scope_fp = fp; 12719 } 12720 this.#options.onChange?.(); 12721 return true; 12722 } 12723 /** Which screens an ALREADY RECORDED change is for - the changes list's "Show on". Like the scope, 12724 not part of what the change does, so it is rewritten in place. '' is every screen. **/ 12725 setChangeDevice(index, device) { 12726 const change = this.#snaps[index]?.change; 12727 if (!change) { 12728 return false; 12729 } 12730 if (device) { 12731 change.device = device; 12732 } else { 12733 delete change.device; 12734 } 12735 this.#options.onChange?.(); 12736 return true; 12737 } 12738 /** Count clicks on an ALREADY RECORDED change's element, or stop counting them - the changes 12739 list's click toggle. Like the scope, this is not part of what the change does to the page: 12740 the event only wires up the visitor's click, so it is rewritten in place and the entry keeps 12741 its selector, its splices and its position. A removed element has nothing to click. 12742 **/ 12743 setChangeClicks(index, on) { 12744 const change = this.#snaps[index]?.change; 12745 if (!change || change.rm || change.rm_shallow) { 12746 return false; 12747 } 12748 const events = eventsWithClicks(change, on); 12749 if (events) { 12750 change.events = events; 12751 } else { 12752 delete change.events; 12753 } 12754 this.#options.onChange?.(); 12755 return true; 12756 } 12757 #requireCursor() { 12758 const snap = this.#curSnap; 12759 if (!snap) { 12760 throw new Error("Call moveCursor(elem) first"); 12761 } 12762 return snap; 12763 } 12764}; 12765
12766// src/modules/ailive/private/aiLiveSession.ts 12767var MAX_ROUNDS = 10; 12768var SKELETON_MAX_CHARS = 6e4; 12769var SELECTED_HTML_MAX_CHARS = 4e3; 12770var GET_HTML_DEFAULT_CHARS = 2e4; 12771var GET_HTML_MAX_CHARS = 5e4; 12772var MAX_LISTED_MATCHES = 10; 12773var MAX_CHANGES_PER_CALL = 50; 12774var DEFAULT_STYLE_PROPS = [ 12775 "display", 12776 "position", 12777 "top", 12778 "left", 12779 "right", 12780 "bottom", 12781 "z-index", 12782 "float", 12783 "width", 12784 "height", 12785 "margin", 12786 "padding", 12787 "border", 12788 "border-radius", 12789 "box-sizing", 12790 "flex-direction", 12791 "flex-wrap", 12792 "justify-content", 12793 "align-items", 12794 "gap", 12795 "grid-template-columns", 12796 "color", 12797 "background-color", 12798 "background-image", 12799 "opacity", 12800 "overflow", 12801 "font-family", 12802 "font-size", 12803 "font-weight", 12804 "line-height", 12805 "text-align", 12806 "text-transform" 12807]; 12808var CHANGE_EFFECT_FIELDS = [ 12809 "inner_html", 12810 "text", 12811 "attrs", 12812 "dup", 12813 "mv", 12814 "rm", 12815 "rm_shallow", 12816 "e", 12817 "e_shallow", 12818 "events", 12819 "delay" 12820]; 12821var AiLiveSession = class { 12822 transcript = new Array(); 12823 isBusy = false; 12824 #options; 12825 #messages; 12826 #budget; 12827 #isStopRequested = false; 12828 constructor(options) { 12829 this.#options = options; 12830 } 12831 /** What was left of the AI allowance as of the last turn, or `undefined` before the first one. 12832 The panel reads it to keep its meter honest while a session runs. 12833 **/ 12834 get budget() { 12835 return this.#budget; 12836 } 12837 /** Send one user prompt and run the tool loop until the model answers in text (or the round cap 12838 or the user stops it). Rejections never propagate - a failure becomes an `error` entry. 12839 **/ 12840 async send(prompt) { 12841 if (this.isBusy || !prompt.trim()) { 12842 return; 12843 } 12844 this.isBusy = true; 12845 this.#isStopRequested = false; 12846 this.#say("user", prompt); 12847 try { 12848 let request = this.#promptRequest(prompt.trim()); 12849 for (let round = 0; true; round++) { 12850 const messagesBefore = this.#messages; 12851 const turn = await this.#callServer(request); 12852 this.#messages = turn.messages; 12853 if (turn.text) { 12854 this.#say("assistant", turn.text); 12855 } 12856 if (!turn.tool_calls.length) { 12857 break; 12858 } 12859 if (this.#isStopRequested || round >= MAX_ROUNDS) { 12860 this.#messages = messagesBefore; 12861 this.#say("status", this.#isStopRequested ? "Stopped." : "The AI did not finish in the allotted number of steps. You can rephrase or continue."); 12862 break; 12863 } 12864 const results = new Array(); 12865 for (const call of turn.tool_calls) { 12866 results.push({ 12867 id: call.id, 12868 ...this.#runTool(call.name, call.input) 12869 }); 12870 } 12871 request = { 12872 messages: this.#messages, 12873 tool_results: results 12874 }; 12875 } 12876 } catch (e3) { 12877 console.error(e3); 12878 this.#say("error", e3 instanceof Error ? e3.message : "The AI request failed."); 12879 } 12880 this.isBusy = false; 12881 this.#options.onUpdate(); 12882 } 12883 /** Ask the running loop to wind down after the turn in flight. **/ 12884 stop() { 12885 this.#isStopRequested = true; 12886 } 12887 #say(role, text) { 12888 this.transcript.push({ 12889 role, 12890 text 12891 }); 12892 this.#options.onUpdate(); 12893 } 12894 /** The request of a user-prompt turn: the page context once (the first turn), the current 12895 selection each time (it may have moved between prompts). 12896 **/ 12897 #promptRequest(prompt) { 12898 const request = { 12899 prompt 12900 }; 12901 if (this.#messages) { 12902 request.messages = this.#messages; 12903 } else { 12904 request.page = { 12905 url: location.href, 12906 title: document.title, 12907 viewport: `${globalThis.innerWidth}x${globalThis.innerHeight}`, 12908 skeleton: pageSkeleton([ 12909 this.#options.hostElem 12910 ], SKELETON_MAX_CHARS) 12911 }; 12912 } 12913 const selected = this.#options.getSelected(); 12914 if (selected) { 12915 const selector = elemToSelector(selected); 12916 if (selector) { 12917 request.selected = { 12918 selector, 12919 html: elemHtml(selected, SELECTED_HTML_MAX_CHARS) 12920 }; 12921 } 12922 } 12923 return request; 12924 } 12925 async #callServer(request) { 12926 const row = await queryRow("CALL acc_actions.ai_live_edit(@request, @action_id, @action_version)", { 12927 request: JSON.stringify(request), 12928 action_id: this.#options.getActionId(), 12929 action_version: 0 12930 }, true); 12931 const parsed = JSON.parse(String(row?.result ?? "")); 12932 if (!Array.isArray(parsed?.messages) || !Array.isArray(parsed?.tool_calls)) { 12933 throw new Error("Unexpected answer from the AI service."); 12934 } 12935 if (parsed.budget && typeof parsed.budget.actionsLeft == "number") { 12936 this.#budget = parsed.budget; 12937 } 12938 return { 12939 messages: parsed.messages, 12940 text: typeof parsed.text == "string" ? parsed.text : "", 12941 tool_calls: parsed.tool_calls, 12942 budget: parsed.budget 12943 }; 12944 } 12945 /** Execute one tool call against the live page. Anything wrong comes back as an error result 12946 for the model to react to, not as an exception - a bad selector is the model's mistake to 12947 fix, not ours to crash on. 12948 **/ 12949 #runTool(name, input) { 12950 try { 12951 switch (name) { 12952 case "query": 12953 return this.#toolQuery(str(input.selector)); 12954 case "get_html": 12955 return this.#toolGetHtml(str(input.selector), Number(input.max_chars) || GET_HTML_DEFAULT_CHARS); 12956 case "get_computed_style": 12957 return this.#toolGetComputedStyle(str(input.selector), Array.isArray(input.props) ? input.props.map((v3) => v3 + "") : void 0); 12958 case "get_boxes": 12959 return this.#toolGetBoxes(str(input.selector)); 12960 case "propose_changes": 12961 return this.#toolProposeChanges(str(input.summary), input.changes); 12962 } 12963 return { 12964 content: `Unknown tool: ${name}`, 12965 is_error: true 12966 }; 12967 } catch (e3) { 12968 console.error(e3); 12969 return { 12970 content: e3 instanceof Error ? e3.message : "The tool failed", 12971 is_error: true 12972 }; 12973 } 12974 } 12975 #find(selector) { 12976 if (!selector) { 12977 throw new Error("No selector given"); 12978 } 12979 try { 12980 return Array.from(document.querySelectorAll(selector)).filter((v3) => v3 != this.#options.hostElem); 12981 } catch { 12982 throw new Error(`Not a valid CSS selector: ${selector}`); 12983 } 12984 } 12985 #toolQuery(selector) { 12986 const found = this.#find(selector); 12987 this.#say("status", `Checked selector ${selector} \u2014 ${found.length} match${found.length == 1 ? "" : "es"}`); 12988 if (!found.length) { 12989 return { 12990 content: "0 elements match." 12991 }; 12992 } 12993 const lines = [ 12994 `${found.length} element${found.length == 1 ? "" : "s"} match.` 12995 ]; 12996 for (let i4 = 0, iEnd = Math.min(found.length, MAX_LISTED_MATCHES); i4 < iEnd; i4++) { 12997 const elem = found[i4]; 12998 const rect = elem.getBoundingClientRect(); 12999 const text = (elem.textContent ?? "").replace(/\s+/g, " ").trim(); 13000 lines.push(`${i4 + 1}. ${describeElem(elem)} (${Math.round(rect.width)}x${Math.round(rect.height)} at ${Math.round(rect.left)},${Math.round(rect.top)})${text ? ` "${text.length > 60 ? text.slice(0, 59) + "\u2026" : text}"` : ""}`); 13001 } 13002 if (found.length > MAX_LISTED_MATCHES) { 13003 lines.push(`\u2026 and ${found.length - MAX_LISTED_MATCHES} more`); 13004 } 13005 return { 13006 content: lines.join("\n") 13007 }; 13008 } 13009 #toolGetHtml(selector, maxChars) { 13010 const found = this.#find(selector); 13011 this.#say("status", `Read the HTML of ${selector}`); 13012 if (!found.length) { 13013 return { 13014 content: `Nothing matches ${selector}`, 13015 is_error: true 13016 }; 13017 } 13018 return {
13019 content: elemHtml(found[0], Math.min(Math.max(maxChars, 100), GET_HTML_MAX_CHARS)) 13020 }; 13021 } 13022 #toolGetComputedStyle(selector, props) { 13023 const found = this.#find(selector); 13024 this.#say("status", `Read the computed style of ${selector}`); 13025 if (!found.length) { 13026 return { 13027 content: `Nothing matches ${selector}`, 13028 is_error: true 13029 }; 13030 } 13031 const style = getComputedStyle(found[0]); 13032 const names = props?.length ? props : DEFAULT_STYLE_PROPS; 13033 const lines = new Array(); 13034 for (const nameRaw of names.slice(0, 100)) { 13035 const name = nameRaw.trim(); 13036 if (name) { 13037 lines.push(`${name}: ${style.getPropertyValue(name) || "(empty)"}`); 13038 } 13039 } 13040 return { 13041 content: lines.join("\n") 13042 }; 13043 } 13044 #toolGetBoxes(selector) { 13045 const found = this.#find(selector); 13046 this.#say("status", `Measured ${selector}`); 13047 if (!found.length) { 13048 return { 13049 content: `Nothing matches ${selector}`, 13050 is_error: true 13051 }; 13052 } 13053 const lines = new Array(); 13054 for (let i4 = 0, iEnd = Math.min(found.length, MAX_LISTED_MATCHES); i4 < iEnd; i4++) { 13055 const elem = found[i4]; 13056 const rect = elem.getBoundingClientRect(); 13057 const style = getComputedStyle(elem); 13058 const isVisible = rect.width > 0 && rect.height > 0 && style.display != "none" && style.visibility != "hidden"; 13059 lines.push(`${i4 + 1}. ${describeElem(elem)}: left=${Math.round(rect.left)} top=${Math.round(rect.top)} width=${Math.round(rect.width)} height=${Math.round(rect.height)}${isVisible ? "" : " (not visible)"}`); 13060 } 13061 lines.push(`(viewport: ${globalThis.innerWidth}x${globalThis.innerHeight}, page scrolled to ${Math.round(globalThis.scrollX)},${Math.round(globalThis.scrollY)})`); 13062 return { 13063 content: lines.join("\n") 13064 }; 13065 } 13066 /** Validate every record, then apply them all through the editor. All-or-nothing: a rejected 13067 record means nothing was applied, and the error names it - so the model can fix that one 13068 record instead of guessing which half of its edit went through. 13069 **/ 13070 #toolProposeChanges(summary, changesRaw) { 13071 if (!Array.isArray(changesRaw) || !changesRaw.length) { 13072 return { 13073 content: "No change records were given.", 13074 is_error: true 13075 }; 13076 } 13077 if (changesRaw.length > MAX_CHANGES_PER_CALL) { 13078 return { 13079 content: `Too many change records (${changesRaw.length}). Use selectors that match groups of elements instead.`, 13080 is_error: true 13081 }; 13082 } 13083 const changes = new Array(); 13084 for (let i4 = 0; i4 < changesRaw.length; i4++) { 13085 const change = changesRaw[i4]; 13086 const error = validateChange(change); 13087 if (error) { 13088 return { 13089 content: `Change #${i4 + 1} was rejected, and NO changes were applied: ${error}`, 13090 is_error: true 13091 }; 13092 } 13093 changes.push(change); 13094 } 13095 for (const change of changes) { 13096 delete change.scope; 13097 delete change.scope_url; 13098 delete change.scope_fp; 13099 } 13100 this.#options.editor.addChangesSync(changes, true); 13101 this.#say("status", `Applied ${changes.length} change${changes.length == 1 ? "" : "s"}${summary ? " \u2014 " + summary : ""}`); 13102 return { 13103 content: `Applied ${changes.length} change record${changes.length == 1 ? "" : "s"}. The user sees the result now.` 13104 }; 13105 } 13106}; 13107function str(value) { 13108 return typeof value == "string" ? value : ""; 13109} 13110function validateChange(change) { 13111 if (typeof change != "object" || change == null || Array.isArray(change)) { 13112 return "a change record must be a JSON object"; 13113 } 13114 const record = change; 13115 if (typeof record.qelem_id != "string" || !record.qelem_id) { 13116 return "qelem_id (a CSS selector string) is required"; 13117 } 13118 if (!CHANGE_EFFECT_FIELDS.some((name) => record[name] != void 0)) { 13119 return `the record has no effect - none of ${CHANGE_EFFECT_FIELDS.join(", ")} is set`; 13120 } 13121 if (resolveQelem(record.qelem_id, true).length == 0) { 13122 return `the selector ${record.qelem_id} matches no element on the page (selectors must match elements that are on the page before your changes)`; 13123 } 13124 if (record.mv != void 0) { 13125 if (typeof record.mv_qelem_id != "string" || !record.mv_qelem_id) { 13126 return "mv requires mv_qelem_id (a CSS selector string)"; 13127 } 13128 if (!resolveQelem(record.mv_qelem_id)) { 13129 return `the move target ${record.mv_qelem_id} matches no element on the page`; 13130 } 13131 } 13132 if ((record.e != void 0 || record.text != void 0) && record.where !== "before" && record.where !== "after" && record.where !== "inside") { 13133 return `'e' and 'text' require where: 'before' | 'after' | 'inside'`; 13134 } 13135 return void 0; 13136} 13137
13138// src/modules/ailive/private/aiSuggest.ts 13139async function aiSuggest(request) { 13140 const payload = { 13141 prompt: request.instruction, 13142 suggest: { 13143 kind: request.kind, 13144 current: request.current, 13145 context: request.context ?? "" 13146 }, 13147 page: { 13148 url: location.href, 13149 title: document.title, 13150 viewport: "", 13151 skeleton: "" 13152 } 13153 }; 13154 const selector = request.elem ? elemToSelector(request.elem) : ""; 13155 if (selector) { 13156 payload.selected = { 13157 selector 13158 }; 13159 } 13160 const row = await queryRow("CALL acc_actions.ai_live_edit(@request, @action_id, @action_version)", { 13161 request: JSON.stringify(payload), 13162 action_id: request.actionId, 13163 action_version: 0 13164 }, true); 13165 const raw = String(row?.result ?? ""); 13166 if (!raw) { 13167 throw new Error("The AI service returned an empty answer. Your editing session may have expired - reopen the editor from Personyze."); 13168 } 13169 let parsed; 13170 try { 13171 parsed = JSON.parse(raw); 13172 } catch { 13173 throw new Error("The AI service answered with something unreadable: " + raw.slice(0, 120)); 13174 } 13175 const text = typeof parsed?.text == "string" ? parsed.text.trim() : ""; 13176 if (!text) { 13177 if (Array.isArray(parsed?.tool_calls) && parsed.tool_calls.length) { 13178 throw new Error("The AI service is running an older version that does not support in-panel suggestions yet. The AI assistant button still works."); 13179 } 13180 throw new Error("The AI returned nothing. Try rephrasing what you want."); 13181 } 13182 return stripCodeFence(text); 13183} 13184function stripCodeFence(text) { 13185 const m2 = text.match(/^```[^\n]*\n([\s\S]*)\n?```$/); 13186 return m2 ? m2[1].trim() : text; 13187} 13188function parseDeclarations(text) { 13189 let body = stripCodeFence(text).trim(); 13190 const rule = body.match(/^[^{}]*\{([\s\S]*)\}\s*$/); 13191 if (rule) { 13192 body = rule[1]; 13193 } 13194 const out = new Array(); 13195 for (const part of body.split(/[;\n]+/)) { 13196 const m2 = part.match(/^\s*([-a-zA-Z][-a-zA-Z0-9]*)\s*:\s*(.+?)\s*$/); 13197 if (m2 && !out.some((v3) => v3[0] == m2[1].toLowerCase())) { 13198 out.push([ 13199 m2[1].toLowerCase(), 13200 m2[2].replace(/;+$/, "").trim() 13201 ]); 13202 } 13203 } 13204 return out; 13205} 13206
13207// src/modules/toolbridge/private/toolbridge.ts 13208var ACK_TIMEOUT = 4e4; 13209var RESEND_EACH = 1e3; 13210var GET_VALUE_TRIES = 60; 13211var MESSAGE_TYPE_GET_VALUE = "(GET_VALUE)"; 13212var MESSAGE_TYPE_VALUE = "(VALUE)"; 13213var MESSAGE_TYPE_SLAVE = "slave"; 13214var ToolBridge = class { 13215 #options; 13216 #pending = new Array(); 13217 #timer = 0; 13218 #isStopped = false; 13219 #lastValue = null; 13220 constructor(options) { 13221 this.#options = options; 13222 } 13223 /** Start listening, then ask the panel for the value being edited. Resolves true once the panel 13224 has answered, false if it never did. 13225 **/ 13226 async start() { 13227 globalThis.addEventListener("message", this.#onMessage); 13228 for (let i4 = 0; i4 < GET_VALUE_TRIES; i4++) { 13229 if (this.#isStopped || await this.#send({ 13230 get_value: true 13231 }, MESSAGE_TYPE_GET_VALUE)) { 13232 return !this.#isStopped; 13233 } 13234 } 13235 return false; 13236 } 13237 /** Hand the result back to the panel. Resolves true once the panel acknowledges it - the caller 13238 must not close the window before that, or the result is lost. 13239 **/ 13240 setValue(value) { 13241 this.#lastValue = value; 13242 return this.#send({ 13243 value 13244 }, MESSAGE_TYPE_VALUE); 13245 } 13246 /** Push an incremental update to the panel - for the WYSIWYG editor, `{page_is_loaded, href, 13247 n_changes, html_changes}`. Resolves true once the panel acknowledges it. 13248 13249 Only the latest one matters, so an unanswered previous update is superseded rather than 13250 queued: a burst of keystrokes must not turn into a backlog of resends. 13251 **/ 13252 reportMessage(messageData) { 13253 return this.#send({ 13254 message_data: messageData 13255 }, MESSAGE_TYPE_SLAVE); 13256 } 13257 destroy() { 13258 this.#isStopped = true; 13259 globalThis.removeEventListener("message", this.#onMessage); 13260 clearInterval(this.#timer); 13261 this.#timer = 0; 13262 for (const pending2 of this.#pending.splice(0)) { 13263 pending2.resolve(false); 13264 } 13265 } 13266 #send(message, messageType) { 13267 message.buffer_key = this.#options.bufferKey; 13268 message.message_type = messageType; 13269 for (let i4 = this.#pending.length - 1; i4 >= 0; i4--) { 13270 if (this.#pending[i4].message.message_type == messageType) { 13271 this.#pending.splice(i4, 1)[0].resolve(false); 13272 } 13273 } 13274 this.#post(message); 13275 return new Promise((resolve) => { 13276 const now = Date.now(); 13277 this.#pending.push({ 13278 message, 13279 resolve, 13280 startTime: now, 13281 lastTime: now 13282 }); 13283 this.#startStopResend(); 13284 }); 13285 } 13286 /** Reply that needs no acknowledgement of its own: it IS the acknowledgement of `messageType`. 13287 **/ 13288 #postAck(messageType, extra) { 13289 this.#post({ 13290 ...extra, 13291 buffer_key: this.#options.bufferKey, 13292 message_type: messageType 13293 }); 13294 } 13295 #post(message) { 13296 getPanelWindow()?.postMessage(message, this.#options.guiOrigin); 13297 } 13298 #onMessage = (event) => { 13299 const { guiOrigin, bufferKey, onValue, onMessageData } = this.#options; 13300 if (guiOrigin != "*" && event.origin != guiOrigin) { 13301 return; 13302 } 13303 const message = event.data; 13304 if (!message || typeof message != "object") { 13305 return; 13306 } 13307 const messageType = typeof message.message_type == "string" ? message.message_type : ""; 13308 if (!messageType || message.buffer_key != bufferKey) { 13309 return; 13310 } 13311 if ("value" in message) { 13312 const value = message.value; 13313 if (value && typeof value == "object") { 13314 onValue(value); 13315 } 13316 } else if (message.message_data && typeof message.message_data == "object") { 13317 onMessageData?.(message.message_data); 13318 } 13319 const i4 = this.#pending.findIndex((v3) => v3.message.message_type == messageType); 13320 if (i4 != -1) { 13321 this.#pending.splice(i4, 1)[0].resolve(true); 13322 this.#startStopResend(); 13323 } else if (message.get_value === true) { 13324 this.#postAck(messageType, { 13325 value: this.#lastValue 13326 }); 13327 } else if ("value" in message || message.message_data) { 13328 this.#postAck(messageType); 13329 } 13330 }; 13331 #startStopResend() { 13332 if (this.#pending.length == 0) { 13333 clearInterval(this.#timer); 13334 this.#timer = 0; 13335 } else if (this.#timer == 0) { 13336 this.#timer = setInterval(() => { 13337 const now = Date.now(); 13338 for (let i4 = this.#pending.length - 1; i4 >= 0; i4--) { 13339 const pending2 = this.#pending[i4]; 13340 if (now - pending2.startTime > ACK_TIMEOUT) { 13341 this.#pending.splice(i4, 1); 13342 pending2.resolve(false); 13343 } else if (now - pending2.lastTime > RESEND_EACH) {
13344 pending2.lastTime = now; 13345 this.#post(pending2.message); 13346 } 13347 } 13348 this.#startStopResend(); 13349 }, RESEND_EACH); 13350 } 13351 } 13352}; 13353
13354// src/components/live/CommandPalette.tsx 13355function filterCommands(commands, query2) { 13356 const terms = query2.toLowerCase().split(/\s+/).filter((v3) => v3); 13357 if (!terms.length) { 13358 return commands; 13359 } 13360 return commands.filter((command) => { 13361 const text = (command.label + " " + (command.hint ?? "") + " " + command.group).toLowerCase(); 13362 return terms.every((term) => text.indexOf(term) != -1); 13363 }); 13364} 13365function CommandPalette(props) { 13366 const [query2, setQuery] = p2(""); 13367 const [active, setActive] = p2(0); 13368 const inputRef = F2(null); 13369 const listRef = F2(null); 13370 const items = q2(() => filterCommands(props.commands, query2), [ 13371 props.commands, 13372 query2 13373 ]); 13374 const cur = Math.min(active, Math.max(0, items.length - 1)); 13375 _2(() => { 13376 inputRef.current?.focus(); 13377 }, []); 13378 _2(() => { 13379 listRef.current?.querySelector(".le-on")?.scrollIntoView({ 13380 block: "nearest" 13381 }); 13382 }, [ 13383 cur, 13384 items.length 13385 ]); 13386 function run(command) { 13387 if (command) { 13388 props.onClose(); 13389 command.run(); 13390 } 13391 } 13392 function onKeyDown(ev) { 13393 switch (ev.key) { 13394 case "ArrowDown": 13395 ev.preventDefault(); 13396 setActive(cur + 1 < items.length ? cur + 1 : 0); 13397 break; 13398 case "ArrowUp": 13399 ev.preventDefault(); 13400 setActive(cur > 0 ? cur - 1 : items.length - 1); 13401 break; 13402 case "Enter": 13403 ev.preventDefault(); 13404 run(items[cur]); 13405 break; 13406 case "Escape": 13407 ev.preventDefault(); 13408 props.onClose(); 13409 break; 13410 } 13411 } 13412 let lastGroup = ""; 13413 return /* @__PURE__ */ u2("div", { 13414 class: "le-root le-cmdk-overlay", 13415 onMouseDown: (ev) => { 13416 if (ev.target == ev.currentTarget) props.onClose(); 13417 }, 13418 children: /* @__PURE__ */ u2("div", { 13419 class: "le-cmdk", 13420 role: "dialog", 13421 "aria-label": "Command palette", 13422 children: [
13423 /* @__PURE__ */ u2("div", { 13424 class: "le-cmdk-input", 13425 children: [ 13426 li.search, 13427 /* @__PURE__ */ u2("input", { 13428 ref: inputRef, 13429 type: "text", 13430 value: query2, 13431 placeholder: "Search actions, tools, changes...", 13432 autocomplete: "off", 13433 "aria-label": "Search commands", 13434 onInput: (ev) => { 13435 setQuery(ev.target.value); 13436 setActive(0); 13437 }, 13438 onKeyDown 13439 }), 13440 /* @__PURE__ */ u2("kbd", { 13441 children: "Esc" 13442 }) 13443 ] 13444 }), 13445 /* @__PURE__ */ u2("div", { 13446 class: "le-cmdk-list", 13447 ref: listRef, 13448 children: items.length == 0 ? /* @__PURE__ */ u2("div", { 13449 class: "le-cmdk-empty", 13450 children: [ 13451 'Nothing matches "', 13452 query2, 13453 '".' 13454 ] 13455 }) : items.map((command, i4) => { 13456 const head = command.group != lastGroup ? command.group : ""; 13457 lastGroup = command.group; 13458 return /* @__PURE__ */ u2(k, { 13459 children: [ 13460 head && /* @__PURE__ */ u2("div", { 13461 class: "le-cmdk-group", 13462 children: head 13463 }, "g" + i4),
13464 /* @__PURE__ */ u2("button", { 13465 type: "button", 13466 class: "le-cmdk-item" + (i4 == cur ? " le-on" : ""), 13467 // Selecting with the mouse must not steal the focus from the box. 13468 onMouseDown: (ev) => ev.preventDefault(), 13469 onMouseEnter: () => setActive(i4), 13470 onClick: () => run(command), 13471 children: [ 13472 /* @__PURE__ */ u2("div", { 13473 class: "le-cmdk-txt", 13474 children: [ 13475 /* @__PURE__ */ u2("b", { 13476 children: command.label 13477 }), 13478 command.hint && /* @__PURE__ */ u2("small", { 13479 children: command.hint 13480 }) 13481 ] 13482 }), 13483 command.shortcut && /* @__PURE__ */ u2("span", { 13484 class: "le-cmdk-key", 13485 children: command.shortcut 13486 }) 13487 ] 13488 }, i4) 13489 ] 13490 }); 13491 }) 13492 }),
13493 /* @__PURE__ */ u2("div", { 13494 class: "le-cmdk-foot", 13495 children: [ 13496 /* @__PURE__ */ u2("span", { 13497 children: [ 13498 /* @__PURE__ */ u2("kbd", { 13499 children: "\u2191" 13500 }), 13501 /* @__PURE__ */ u2("kbd", { 13502 children: "\u2193" 13503 }), 13504 " to navigate" 13505 ] 13506 }), 13507 /* @__PURE__ */ u2("span", { 13508 children: [ 13509 /* @__PURE__ */ u2("kbd", { 13510 children: "\u21B5" 13511 }), 13512 " to select" 13513 ] 13514 }), 13515 /* @__PURE__ */ u2("span", { 13516 children: [ 13517 /* @__PURE__ */ u2("kbd", { 13518 children: "Esc" 13519 }), 13520 " to close" 13521 ] 13522 }) 13523 ] 13524 }) 13525 ] 13526 }) 13527 }); 13528} 13529
13530// src/components/live/Toolbar.tsx 13531var MAX_CRUMBS = 5; 13532function topInView(want, root) { 13533 return Math.max(8, Math.min(want, globalThis.innerHeight - root.offsetHeight - 8)); 13534} 13535function Toolbar(props) { 13536 const rootRef = F2(null); 13537 const dragRef = F2(null); 13538 const [isSelectorOpen, setIsSelectorOpen] = p2(false); 13539 _2(() => { 13540 const onMove = (ev) => { 13541 const drag = dragRef.current; 13542 const root = rootRef.current; 13543 if (drag && root) { 13544 ev.preventDefault(); 13545 props.onMove({ 13546 left: Math.max(8, Math.min(globalThis.innerWidth - root.offsetWidth - 8, ev.clientX - drag.dx)), 13547 top: topInView(ev.clientY - drag.dy, root) 13548 }); 13549 } 13550 }; 13551 const onUp = () => { 13552 dragRef.current = null; 13553 rootRef.current?.classList.remove("le-dragging"); 13554 }; 13555 document.addEventListener("mousemove", onMove, true); 13556 document.addEventListener("mouseup", onUp, true); 13557 return () => { 13558 document.removeEventListener("mousemove", onMove, true); 13559 document.removeEventListener("mouseup", onUp, true); 13560 }; 13561 }, []); 13562 _2(() => { 13563 const root = rootRef.current; 13564 if (!root || typeof ResizeObserver != "function") { 13565 return; 13566 } 13567 const obs = new ResizeObserver(() => { 13568 if (!dragRef.current) { 13569 const want = topInView(props.position.top, root); 13570 if (want != props.position.top) { 13571 props.onMove({ 13572 ...props.position, 13573 top: want 13574 }); 13575 } 13576 } 13577 }); 13578 obs.observe(root); 13579 return () => obs.disconnect(); 13580 }, [ 13581 props.position.top, 13582 props.position.left 13583 ]); 13584 function onHeadDown(ev) { 13585 if (ev.target.closest("button")) { 13586 return; 13587 } 13588 const root = rootRef.current; 13589 if (root) { 13590 const rect = root.getBoundingClientRect(); 13591 dragRef.current = { 13592 dx: ev.clientX - rect.left, 13593 dy: ev.clientY - rect.top 13594 }; 13595 root.classList.add("le-dragging"); 13596 ev.preventDefault(); 13597 } 13598 } 13599 const crumbs = props.selected ? ancestorChain(props.selected) : []; 13600 const shown = crumbs.slice(-MAX_CRUMBS); 13601 const smart = smartRowFor(props.selected, props.allow); 13602 const tiles = availableTiles(props.selected, props.allow); 13603 return /* @__PURE__ */ u2("div", { 13604 ref: rootRef, 13605 class: "le-root le-toolbar" + (props.isCollapsed ? " le-collapsed" : ""), 13606 style: "left:" + props.position.left + "px; top:" + props.position.top + "px", 13607 role: "dialog", 13608 "aria-label": "Personyze Live Editor", 13609 children: [
13610 /* @__PURE__ */ u2("div", { 13611 class: "le-head", 13612 onMouseDown: onHeadDown, 13613 children: [ 13614 /* @__PURE__ */ u2("span", { 13615 class: "le-grip", 13616 "aria-label": "Drag to move the editor", 13617 "data-tooltip": "Drag anywhere on this bar to move the editor out of your way", 13618 children: "\u22EE\u22EE" 13619 }), 13620 /* @__PURE__ */ u2("button", { 13621 type: "button", 13622 class: "le-mode" + (props.isNavigate ? " le-on" : ""), 13623 "data-tooltip": "Turn on to click through the page like a visitor. While off, clicks select elements to edit. Shortcut: N", 13624 onClick: () => props.onNavigate(!props.isNavigate), 13625 children: [ 13626 /* @__PURE__ */ u2("span", { 13627 class: "le-dot" 13628 }), 13629 li.cursor, 13630 props.isNavigate ? "Navigate \xB7 ON" : "Navigate" 13631 ] 13632 }), 13633 props.hasAi && /* @__PURE__ */ u2("button", { 13634 type: "button", 13635 class: "le-mode" + (props.isAiOpen ? " le-on" : ""), 13636 "data-tooltip": "Describe an edit in plain words, and the AI makes it on this page", 13637 onClick: () => props.onAi(!props.isAiOpen), 13638 children: [ 13639 li.sparkle, 13640 "AI" 13641 ] 13642 }),
13643 /* @__PURE__ */ u2("div", { 13644 class: "le-spacer" 13645 }), 13646 /* @__PURE__ */ u2("button", { 13647 type: "button", 13648 class: "le-icon", 13649 "data-tooltip": "Search every action, tool and change \u2318K", 13650 onClick: props.onSearch, 13651 children: li.search 13652 }), 13653 /* @__PURE__ */ u2("button", { 13654 type: "button", 13655 class: "le-icon", 13656 "data-tooltip": "Undo the last change \u2318Z", 13657 disabled: !props.canUndo, 13658 onClick: props.onUndo, 13659 children: li.undo 13660 }), 13661 /* @__PURE__ */ u2("button", { 13662 type: "button", 13663 class: "le-icon", 13664 "data-tooltip": "Redo \u2318\u21E7Z", 13665 disabled: !props.canRedo, 13666 onClick: props.onRedo, 13667 children: li.redo 13668 }), 13669 /* @__PURE__ */ u2("button", { 13670 type: "button", 13671 class: "le-icon", 13672 "data-tooltip": (props.isCollapsed ? "Expand the toolbar" : "Collapse the toolbar to one line") + " \u2318\\", 13673 onClick: () => props.onCollapse(!props.isCollapsed), 13674 children: props.isCollapsed ? li.chevronDown : li.chevronUp 13675 }),
13676 /* @__PURE__ */ u2("button", { 13677 type: "button", 13678 class: "le-icon", 13679 "data-tooltip": "Put the editor away. Your changes stay on the page, and a button to bring it back appears in the corner.", 13680 onClick: props.onClose, 13681 children: li.cross 13682 }) 13683 ] 13684 }), 13685 !props.isCollapsed && /* @__PURE__ */ u2(k, { 13686 children: [ 13687 /* @__PURE__ */ u2("div", { 13688 class: "le-path", 13689 "aria-label": "Selected element path", 13690 children: [ 13691 shown.length == 0 ? /* @__PURE__ */ u2("span", { 13692 class: "le-path-empty", 13693 children: "Hover the page and click an element to edit it." 13694 }) : shown.map((node, i4) => { 13695 const isCurrent = i4 == shown.length - 1; 13696 const meta = node.id ? "#" + node.id : node.classList.length ? "." + node.classList[0] : ""; 13697 return /* @__PURE__ */ u2(k, { 13698 children: [ 13699 i4 > 0 && /* @__PURE__ */ u2("span", { 13700 class: "le-crumb-sep", 13701 children: "\u203A" 13702 }, "s" + i4),
13703 /* @__PURE__ */ u2("button", { 13704 type: "button", 13705 class: "le-crumb" + (isCurrent ? " le-on" : ""), 13706 title: isCurrent ? "The selected element - " + tagHumanName(node) : "Select the parent: " + node.localName + meta, 13707 onMouseEnter: () => props.onHoverCrumb(node), 13708 onMouseLeave: () => props.onHoverCrumb(null), 13709 onClick: () => props.onSelect(node), 13710 children: [ 13711 /* @__PURE__ */ u2("b", { 13712 children: node.localName 13713 }), 13714 meta && /* @__PURE__ */ u2("span", { 13715 children: meta 13716 }) 13717 ] 13718 }, "c" + i4) 13719 ] 13720 }); 13721 }), 13722 /* @__PURE__ */ u2("button", { 13723 type: "button", 13724 class: "le-crumb-add" + (isSelectorOpen ? " le-on" : ""), 13725 title: "Pick by CSS selector - for an element the mouse cannot reach", 13726 onClick: () => setIsSelectorOpen((v3) => !v3), 13727 children: "+" 13728 }) 13729 ] 13730 }), 13731 isSelectorOpen && /* @__PURE__ */ u2(SelectorRow, { 13732 onSelect: props.onSelectBySelector, 13733 onClose: () => setIsSelectorOpen(false) 13734 }), 13735 props.group && !props.subpanel && /* @__PURE__ */ u2("label", { 13736 class: "le-grouprow" + (props.isGroup ? " le-on" : ""), 13737 title: "Record the edit against " + props.group.selector + ", so it applies to every one of them - on this page and on any other page that has them", 13738 children: [
13739 /* @__PURE__ */ u2("input", { 13740 type: "checkbox", 13741 checked: props.isGroup, 13742 onChange: (ev) => props.onGroup(ev.target.checked) 13743 }), 13744 /* @__PURE__ */ u2("span", { 13745 children: [ 13746 "Apply to all ", 13747 /* @__PURE__ */ u2("b", { 13748 children: props.group.count 13749 }), 13750 " elements like this one" 13751 ] 13752 }), 13753 /* @__PURE__ */ u2("code", { 13754 children: props.group.selector 13755 }) 13756 ] 13757 }), 13758 props.subpanel ? /* @__PURE__ */ u2(k, { 13759 children: [ 13760 /* @__PURE__ */ u2("div", { 13761 class: "le-body", 13762 children: props.subpanel 13763 }), 13764 props.subpanelFoot 13765 ] 13766 }) : /* @__PURE__ */ u2(k, { 13767 children: [ 13768 props.notice && /* @__PURE__ */ u2("div", { 13769 class: "le-notice le-warn", 13770 children: [ 13771 li.warn,
13772 /* @__PURE__ */ u2("div", { 13773 children: props.notice 13774 }), 13775 /* @__PURE__ */ u2("button", { 13776 type: "button", 13777 class: "le-icon", 13778 title: "Dismiss", 13779 onClick: props.onDismissNotice, 13780 children: li.cross 13781 }) 13782 ] 13783 }), 13784 smart && smart.tiles.length > 0 && /* @__PURE__ */ u2("div", { 13785 class: "le-smart", 13786 children: [ 13787 /* @__PURE__ */ u2("span", { 13788 class: "le-badge", 13789 children: [ 13790 li[smart.badgeIcon], 13791 smart.label 13792 ] 13793 }), 13794 /* @__PURE__ */ u2("div", { 13795 class: "le-smart-tiles", 13796 children: smart.tiles.map((tile) => /* @__PURE__ */ u2("button", { 13797 type: "button", 13798 class: "le-smart-tile" + (tile.tool == "widget-edit" ? " le-wkey" : ""), 13799 title: tile.tooltip ?? tile.sub, 13800 onClick: () => props.onTool(tile.tool), 13801 children: [ 13802 li[tile.icon], 13803 tile.title 13804 ] 13805 }, tile.tool)) 13806 }) 13807 ] 13808 }),
13809 /* @__PURE__ */ u2("div", { 13810 class: "le-tabs", 13811 role: "tablist", 13812 children: TABS.map((tab) => /* @__PURE__ */ u2("button", { 13813 type: "button", 13814 class: "le-tab" + (tab.id == props.tab ? " le-on" : ""), 13815 title: tab.title, 13816 onClick: () => props.onTab(tab.id), 13817 children: tab.label 13818 }, tab.id)) 13819 }), 13820 /* @__PURE__ */ u2("div", { 13821 class: "le-body", 13822 children: /* @__PURE__ */ u2(TileGrid, { 13823 tiles: tiles.filter((v3) => tileOnTab(v3, props.tab)), 13824 hasSelection: !!props.selected, 13825 onTool: props.onTool, 13826 onDragTool: props.onDragTool 13827 }) 13828 }) 13829 ] 13830 }) 13831 ] 13832 }) 13833 ] 13834 }); 13835} 13836function SelectorRow(props) { 13837 const [selector, setSelector] = p2(""); 13838 const [error, setError] = p2(""); 13839 const inputRef = F2(null); 13840 _2(() => { 13841 inputRef.current?.focus(); 13842 }, []); 13843 function go() { 13844 const text = selector.trim(); 13845 if (!text) { 13846 return; 13847 } 13848 const message = props.onSelect(text); 13849 setError(message); 13850 if (!message) { 13851 props.onClose(); 13852 } 13853 } 13854 return /* @__PURE__ */ u2("div", { 13855 class: "le-selrow", 13856 children: [
13857 /* @__PURE__ */ u2("input", { 13858 ref: inputRef, 13859 class: "le-input" + (error ? " le-bad" : ""), 13860 type: "text", 13861 value: selector, 13862 placeholder: "e.g. .hero .cta, #nav a:last-child", 13863 "aria-label": "CSS selector", 13864 onInput: (ev) => { 13865 setSelector(ev.target.value); 13866 setError(""); 13867 }, 13868 onKeyDown: (ev) => { 13869 if (ev.key == "Enter") { 13870 go(); 13871 } else if (ev.key == "Escape") { 13872 props.onClose(); 13873 } 13874 } 13875 }), 13876 /* @__PURE__ */ u2("button", { 13877 type: "button", 13878 class: "le-btn le-primary", 13879 onClick: go, 13880 children: "Select" 13881 }), 13882 /* @__PURE__ */ u2("button", { 13883 type: "button", 13884 class: "le-btn", 13885 onClick: props.onClose, 13886 children: "Cancel" 13887 }), 13888 error && /* @__PURE__ */ u2("div", { 13889 class: "le-selrow-err", 13890 children: error 13891 }) 13892 ] 13893 }); 13894} 13895function TileGrid(props) { 13896 if (!props.hasSelection) { 13897 return /* @__PURE__ */ u2("div", { 13898 class: "le-empty", 13899 children: [
13900 /* @__PURE__ */ u2("h4", { 13901 children: "Nothing selected" 13902 }), 13903 /* @__PURE__ */ u2("p", { 13904 children: "Click any element on the page to see what you can do with it." 13905 }) 13906 ] 13907 }); 13908 } 13909 if (props.tiles.length == 0) { 13910 return /* @__PURE__ */ u2("div", { 13911 class: "le-empty", 13912 children: [ 13913 /* @__PURE__ */ u2("h4", { 13914 children: "Not for this element" 13915 }), 13916 /* @__PURE__ */ u2("p", { 13917 children: "Nothing on this tab applies to what you selected. Try another tab, or pick its parent in the path above." 13918 }) 13919 ] 13920 }); 13921 } 13922 return /* @__PURE__ */ u2("div", { 13923 class: "le-grid", 13924 children: props.tiles.map((tile) => /* @__PURE__ */ u2("button", { 13925 type: "button", 13926 /* A tile may declare itself the primary action for what is selected — the widget 13927 settings do, because after clicking a countdown that is the only thing anybody 13928 came for. */ 13929 class: "le-tile" + (tile.primary ? " le-tile-primary" : ""), 13930 title: tile.tooltip ?? tile.sub, 13931 onClick: () => props.onTool(tile.tool), 13932 /* A widget tile can also be DRAGGED onto the page, which is how somebody says 13933 "this one, there" in one gesture instead of selecting first and inserting 13934 second. Only the widgets: every other tile acts on the element already 13935 selected and has nowhere to be dropped. 13936 13937 POINTER EVENTS, NOT HTML5 DRAG-AND-DROP. The tile lives in a shadow root 13938 over somebody else's page, and native dragging there means a drag image, 13939 a `dragover` that has to be preventDefault-ed on every element underneath, 13940 and behaviour that differs across browsers and iframes. It is also 13941 untestable from outside the browser: Chrome drives native drags through its 13942 own controller, so nothing a script dispatches reaches the handler - which 13943 is how this shipped once unverified. A pointer drag is the same gesture, 13944 works the same over any page, and can be exercised. */ 13945 "data-drag": !!props.onDragTool && String(tile.tool).startsWith("insert-widget-") ? "1" : void 0, 13946 onMouseDown: (ev) => { 13947 if (!props.onDragTool || !String(tile.tool).startsWith("insert-widget-")) { 13948 return; 13949 } 13950 props.onDragTool(tile.tool, { 13951 x: ev.clientX, 13952 y: ev.clientY 13953 }); 13954 }, 13955 children: [
13956 /* @__PURE__ */ u2("div", { 13957 class: "le-tile-ico", 13958 children: li[tile.icon] 13959 }), 13960 /* @__PURE__ */ u2("div", { 13961 class: "le-tile-txt", 13962 children: [ 13963 /* @__PURE__ */ u2("h4", { 13964 children: tile.title 13965 }), 13966 /* @__PURE__ */ u2("p", { 13967 children: tile.sub 13968 }) 13969 ] 13970 }), 13971 tile.badge ? /* @__PURE__ */ u2("span", { 13972 class: "le-tile-badge", 13973 children: tile.badge 13974 }) : tile.key ? /* @__PURE__ */ u2("kbd", { 13975 children: tile.key 13976 }) : null 13977 ] 13978 }, tile.tool)) 13979 }); 13980} 13981
13982// src/components/live/liveScope.tsx 13983var SCOPE_OPTIONS = [ 13984 { 13985 id: "page", 13986 label: "This page only", 13987 short: "This page", 13988 hint: "Only this address, among the pages the campaign targets. Ad parameters in the link are ignored." 13989 }, 13990 { 13991 id: "shape", 13992 label: "Pages that look the same", 13993 short: "Same layout", 13994 hint: "Pages the campaign targets where this element sits in the same structure. Text is never compared." 13995 }, 13996 { 13997 id: "any", 13998 label: "Anywhere it matches", 13999 short: "Anywhere", 14000 hint: "Every page the campaign targets where the saved selector finds something." 14001 } 14002]; 14003var SCOPE_TIP = "The campaign still decides where it runs - its page groups, audience and A/B split come first, and this does not change them. It only narrows where this one change lands inside that."; 14004var SCOPE_ICON = { 14005 page: "scopePage", 14006 shape: "scopeShape", 14007 any: "scopeAny" 14008}; 14009function ScopeLabel(props) { 14010 return /* @__PURE__ */ u2("label", { 14011 for: props.for, 14012 class: "le-scope-lbl", 14013 "data-tooltip": SCOPE_TIP, 14014 "data-tooltip-pos": "top", 14015 tabIndex: 0, 14016 children: [ 14017 "Apply on ", 14018 li.info 14019 ] 14020 }); 14021} 14022function ScopeSeg(props) { 14023 return /* @__PURE__ */ u2("div", { 14024 class: "le-seg", 14025 role: "group", 14026 "aria-label": "Apply on", 14027 children: SCOPE_OPTIONS.map((v3) => /* @__PURE__ */ u2("button", { 14028 type: "button", 14029 class: "le-seg-btn" + (props.value == v3.id ? " le-on" : ""), 14030 "data-tooltip": v3.label + " - " + v3.hint, 14031 "data-tooltip-pos": "top", 14032 "aria-pressed": props.value == v3.id, 14033 onClick: () => props.onChange(v3.id), 14034 children: [ 14035 li[SCOPE_ICON[v3.id]], 14036 v3.short 14037 ] 14038 }, v3.id)) 14039 }); 14040} 14041var SHOW_ON_OPTIONS = [ 14042 { 14043 id: "", 14044 label: "All devices", 14045 icon: "allDevices", 14046 hint: "Every screen size." 14047 }, 14048 { 14049 id: "desktop", 14050 label: "Desktop & tablet", 14051 icon: "desktop", 14052 hint: "Screens wider than 767px - desktops, laptops and tablets." 14053 }, 14054 { 14055 id: "mobile", 14056 label: "Phones only", 14057 icon: "mobile", 14058 hint: "Screens up to 767px wide." 14059 } 14060]; 14061var SHOW_ON_TIP = "Which screens this change is for. A different headline on phones is two changes: one for desktop & tablet, one for phones only. Decided when the page loads."; 14062function ShowOnLabel(props) { 14063 return /* @__PURE__ */ u2("label", { 14064 for: props.for, 14065 class: "le-scope-lbl", 14066 "data-tooltip": SHOW_ON_TIP, 14067 "data-tooltip-pos": "top", 14068 tabIndex: 0, 14069 children: [ 14070 "Show on ", 14071 li.info 14072 ] 14073 }); 14074} 14075function DeviceSeg(props) { 14076 return /* @__PURE__ */ u2("div", { 14077 class: "le-seg", 14078 role: "group", 14079 "aria-label": "Show on", 14080 children: SHOW_ON_OPTIONS.map((v3) => /* @__PURE__ */ u2("button", { 14081 type: "button", 14082 class: "le-seg-btn" + ((props.value ?? "") == v3.id ? " le-on" : ""), 14083 "data-tooltip": v3.hint, 14084 "data-tooltip-pos": "top", 14085 "aria-pressed": (props.value ?? "") == v3.id, 14086 onClick: () => props.onChange(v3.id), 14087 children: [ 14088 li[v3.icon], 14089 v3.label 14090 ] 14091 }, v3.id || "all")) 14092 }); 14093} 14094
14095// src/components/live/clickCount.ts 14096var CLICKABLE = "a[href], button, input[type=submit], input[type=button], input[type=image], [role=button], [role=link]"; 14097function isClickable(elem) { 14098 return !!elem?.closest?.(CLICKABLE); 14099} 14100var NO_CLICK_TOOLS = /* @__PURE__ */ new Set([ 14101 "visibility", 14102 "onclick", 14103 "custom-event", 14104 "scroll-to", 14105 "delay" 14106]); 14107function toolOffersClicks(tool) { 14108 return !!tool && !NO_CLICK_TOOLS.has(tool); 14109} 14110function widgetIsClickable(widgetId) { 14111 const html = WIDGET_CATALOGUE.find((w3) => w3.id == widgetId)?.html ?? ""; 14112 return /data-personyze-click-target|<a\s|<button[\s>]/i.test(html); 14113} 14114function insertCountsByDefault(kind, widgetId) { 14115 return kind == "link" || kind == "button" || kind == "widget" && widgetIsClickable(widgetId); 14116} 14117function actionHasForm(changes) { 14118 return changes.some((c3) => JSON.stringify(c3.e ?? "").includes('"data-pz-widget":"form"')); 14119} 14120function clicksAlreadyCounted(elem) { 14121 if (!elem) { 14122 return false; 14123 } 14124 const insert = insertOf(elem); 14125 if (insert && countsClicks(insert)) { 14126 return true; 14127 } 14128 return getElemPrevChanges(elem).some((p3) => !p3.change.e && countsClicks(p3.change)); 14129} 14130function changeOffersClicks(change) { 14131 if (change.rm || change.rm_shallow || change.delay) { 14132 return false; 14133 } 14134 const edits = !!(change.inner_html || change.text || change.attrs || change.e || change.e_shallow || change.dup || change.mv); 14135 return edits || countsClicks(change); 14136} 14137
14138// src/components/live/changeSummary.ts 14139function splicesToText(splices) { 14140 let text = ""; 14141 for (const splice of splices) { 14142 text += typeof splice == "string" ? splice : "\u2026"; 14143 } 14144 return text.replace(/\s+/g, " ").trim(); 14145} 14146var RE_COLOR = /(#[0-9A-F]{3,8}\b|rgba?\([^)]*\))/gi; 14147function summarizeChange(change) { 14148 const target = qelemIdToText(change.qelem_id) || "the page"; 14149 if (change.rm || change.rm_shallow) { 14150 return { 14151 kind: "delete", 14152 title: change.rm_shallow ? "Removed the element, kept its contents" : "Removed the element", 14153 target 14154 }; 14155 } 14156 if (change.dup) { 14157 const nCopies = Number(change.dup) || 0; 14158 return { 14159 kind: "insert", 14160 title: nCopies == 1 ? "Duplicated the element" : `Made ${nCopies + 1} copies of the element`, 14161 target 14162 }; 14163 } 14164 if (change.mv) { 14165 return { 14166 kind: "move", 14167 title: `Moved the element ${change.mv} ${qelemIdToText(change.mv_qelem_id)}`, 14168 target 14169 }; 14170 } 14171 if (change.e || change.e_shallow) { 14172 const spec = change.e ?? change.e_shallow; 14173 const tag = typeof spec == "string" ? "text" : "<" + spec[0] + ">"; 14174 const where = change.e ? change.where ?? "inside" : "around"; 14175 return { 14176 kind: "insert", 14177 title: `Added ${tag} ${where} the element`, 14178 target 14179 }; 14180 }
14181 if (change.events && !(change.inner_html || change.text || change.attrs)) { 14182 const parts = describeEvents(change.events); 14183 return { 14184 kind: "behavior", 14185 title: parts || "Added an event tracker", 14186 target 14187 }; 14188 } 14189 if (change.delay) { 14190 const ms = change.delay.ms || 0; 14191 const reqText = change.delay.req_text; 14192 let title = "Later changes wait for this element"; 14193 if (reqText) { 14194 title += ` to say "${reqText}"`; 14195 } 14196 if (ms) { 14197 title += `, then ${ms} ms`; 14198 } 14199 return { 14200 kind: "behavior", 14201 title, 14202 target 14203 }; 14204 } 14205 if (change.inner_html || change.text) { 14206 const style = change.attrs?.style; 14207 const after = splicesToText(change.inner_html ?? change.text); 14208 return { 14209 kind: "text", 14210 title: change.inner_html ? "Changed the content" : "Changed the text", 14211 target, 14212 after: style == void 0 ? after : after + (after ? "; " : "") + "style: " + style, 14213 colors: colorsIn(style ?? "") 14214 }; 14215 } 14216 if (change.attrs) { 14217 const names = Object.keys(change.attrs); 14218 const style = change.attrs.style; 14219 if (names.length == 1 && names[0] == "style") { 14220 return { 14221 kind: "style", 14222 title: "Restyled the element", 14223 target, 14224 after: style ?? "(cleared)", 14225 colors: colorsIn(style ?? "") 14226 }; 14227 } 14228 const shown = names.filter((v3) => v3 != "style"); 14229 const after = shown.map((name) => name + ": " + (change.attrs[name] ?? "(removed)")).join("; "); 14230 return { 14231 kind: style != void 0 ? "style" : "other", 14232 title: shown.length == 1 ? `Changed ${shown[0]}` : `Changed ${shown.length} attributes`, 14233 target, 14234 after: style == void 0 ? after : after + (after ? "; " : "") + "style: " + style, 14235 colors: colorsIn(style ?? "") 14236 }; 14237 } 14238 return { 14239 kind: "other", 14240 title: "Edited the element", 14241 target 14242 }; 14243} 14244function describeEvents(events) { 14245 return !Array.isArray(events) ? "" : events.map((v3) => describeEvent(v3 ?? {})).filter((v3) => v3).join("; "); 14246} 14247function describeEvent(event) { 14248 const on = "On " + (event.onwhat ?? "click"); 14249 if (Number(event.action_id) == -1 && event.action_state == "extra") { 14250 return on + ": count as a click on this action"; 14251 } 14252 if (event.fire_action != void 0) { 14253 return on + ": execute action " + event.fire_action; 14254 } 14255 if (event.custom_event != void 0) { 14256 return on + ": trigger custom event " + event.custom_event + (event.value ? " = " + event.value : ""); 14257 } 14258 if (event.rm_elem) { 14259 return on + ": remove " + event.rm_elem; 14260 } 14261 if (event.js) { 14262 return on + ": run Javascript"; 14263 } 14264 return on; 14265} 14266function colorsIn(text) { 14267 const colors = text.match(RE_COLOR); 14268 return colors ? colors.slice(0, 4) : []; 14269} 14270
14271// src/components/live/ChangesSidebar.tsx 14272var PAGE_AI_CHIPS = [ 14273 "Rewrite this page for enterprise buyers", 14274 "Make the calls to action more urgent", 14275 "Tighten every paragraph" 14276]; 14277var SCOPE_CHIP = { 14278 page: { 14279 label: "this page", 14280 hint: "Applies only on the address it was recorded on" 14281 }, 14282 shape: { 14283 label: "same layout", 14284 hint: "Applies on pages where this element sits in the same structure" 14285 }, 14286 any: { 14287 label: "anywhere", 14288 hint: "Applies on every page where the saved selector matches" 14289 } 14290}; 14291function widgetTypeOf(change) { 14292 const spec = change.e; 14293 const attrs = Array.isArray(spec) && typeof spec[0] == "string" ? spec[1] : null; 14294 return attrs && typeof attrs == "object" ? String(attrs["data-pz-widget"] ?? "") : ""; 14295} 14296var SCOPE_PICKER = [ 14297 [ 14298 "page", 14299 "This page only" 14300 ], 14301 [ 14302 "shape", 14303 "Pages that look the same" 14304 ], 14305 [ 14306 "any", 14307 "Anywhere it matches" 14308 ] 14309]; 14310function ClicksToggle(props) { 14311 const what = props.isInsert ? "the added element" : "this element"; 14312 const title = props.blocked ? props.on ? "This action has a form, so this click is reported as a form SUBMISSION. Click to stop counting it." : "This action has a form, so its Clicks number counts form submissions - clicks are not counted here." : props.on ? `Clicks on ${what} count toward this action's Clicks. Click to stop counting them.` : `Count clicks on ${what} toward this action's Clicks.`; 14313 return /* @__PURE__ */ u2("button", { 14314 type: "button", 14315 class: "le-clicks" + (props.on ? props.blocked ? " le-warn" : " le-on" : ""), 14316 "aria-pressed": props.on, 14317 disabled: props.disabled || !props.on && props.blocked, 14318 "data-tooltip": title, 14319 "data-tooltip-pos": "left", 14320 onClick: (ev) => { 14321 ev.stopPropagation(); 14322 props.onToggle(!props.on); 14323 }, 14324 children: [ 14325 li.cursor, 14326 props.on ? "Counts clicks" : "Count clicks" 14327 ] 14328 }); 14329} 14330function ChangesSidebar(props) { 14331 const [href, setHref] = p2(props.href); 14332 const [hrefEdited, setHrefEdited] = p2(false); 14333 const [confirming, setConfirming] = p2(-1); 14334 const [isDiscarding, setIsDiscarding] = p2(false); 14335 const [isTipShown, setIsTipShown] = p2(true); 14336 const bodyRef = F2(null); 14337 _2(() => { 14338 bodyRef.current?.querySelector(".le-change.le-on")?.scrollIntoView({ 14339 block: "nearest" 14340 }); 14341 }, [ 14342 props.current 14343 ]); 14344 const shownHref = hrefEdited ? href : props.href; 14345 const pausedTip = props.isPeeking ? "Put the changes back first - editing is paused while you compare" : ""; 14346 return /* @__PURE__ */ u2("aside", { 14347 class: "le-root le-side" + (props.isLeft ? " le-left" : "") + (props.isCollapsed ? " le-collapsed" : ""), 14348 children: [
14349 /* @__PURE__ */ u2("div", { 14350 class: "le-peek", 14351 "data-tooltip": "Show or hide this panel", 14352 "data-tooltip-pos": "left", 14353 onClick: () => props.onCollapse(!props.isCollapsed), 14354 children: props.isCollapsed == props.isLeft ? li.chevronLeft : li.chevronRight 14355 }), 14356 /* @__PURE__ */ u2("div", { 14357 class: "le-side-top", 14358 children: [ 14359 props.logoSrc && /* @__PURE__ */ u2("span", { 14360 class: "le-logo", 14361 children: /* @__PURE__ */ u2("img", { 14362 src: props.logoSrc, 14363 alt: "Personyze" 14364 }) 14365 }), 14366 /* @__PURE__ */ u2("div", { 14367 class: "le-side-title", 14368 children: [ 14369 /* @__PURE__ */ u2("b", { 14370 children: "Live Editor" 14371 }), 14372 /* @__PURE__ */ u2("small", { 14373 class: "le-ctx", 14374 "data-tooltip": "You're editing inside this campaign and action - 'Done editing' returns you to Personyze", 14375 "data-tooltip-pos": "left", 14376 children: [ 14377 props.campaignName && /* @__PURE__ */ u2(k, { 14378 children: [
14379 /* @__PURE__ */ u2("span", { 14380 class: "le-ctx-campaign", 14381 children: props.campaignName 14382 }), 14383 /* @__PURE__ */ u2("span", { 14384 children: "\u203A" 14385 }) 14386 ] 14387 }), 14388 /* @__PURE__ */ u2("span", { 14389 class: "le-ctx-action", 14390 children: props.actionName || "this action" 14391 }) 14392 ] 14393 }) 14394 ] 14395 }), 14396 /* @__PURE__ */ u2("button", { 14397 type: "button", 14398 class: "le-icon", 14399 "data-tooltip": "Move this panel to the other side", 14400 "data-tooltip-pos": "left", 14401 onClick: () => props.onDock(!props.isLeft), 14402 children: li.dock 14403 }) 14404 ] 14405 }), 14406 /* @__PURE__ */ u2("div", { 14407 class: "le-urlbar-wrap", 14408 children: /* @__PURE__ */ u2("div", { 14409 class: "le-urlbar", 14410 children: [
14411 /* @__PURE__ */ u2("input", { 14412 value: shownHref, 14413 "aria-label": "Page being edited", 14414 onInput: (ev) => { 14415 setHrefEdited(true); 14416 setHref(ev.target.value); 14417 }, 14418 onKeyDown: (ev) => { 14419 if (ev.key == "Enter") { 14420 props.onGo(shownHref); 14421 } 14422 } 14423 }), 14424 /* @__PURE__ */ u2("button", { 14425 type: "button", 14426 class: "le-icon", 14427 "data-tooltip": "Reload the page", 14428 "data-tooltip-pos": "left", 14429 onClick: props.onReload, 14430 children: li.reload 14431 }), 14432 /* @__PURE__ */ u2("button", { 14433 type: "button", 14434 class: "le-go", 14435 title: "Load this URL in the editor", 14436 onClick: () => props.onGo(shownHref), 14437 children: "Go" 14438 }) 14439 ] 14440 }) 14441 }), 14442 props.hasAi && !props.isPeeking && /* @__PURE__ */ u2(PageAiCard, { 14443 onSend: props.onPageAi 14444 }),
14445 /* @__PURE__ */ u2("div", { 14446 class: "le-changes-head", 14447 children: [ 14448 /* @__PURE__ */ u2("b", { 14449 children: "Changes" 14450 }), 14451 /* @__PURE__ */ u2("span", { 14452 class: "le-count", 14453 children: !props.changes.length ? "0" : `${Math.min(props.current + 1, props.changes.length)} of ${props.changes.length}` 14454 }), 14455 /* @__PURE__ */ u2("div", { 14456 class: "le-nav-row", 14457 children: [ 14458 /* @__PURE__ */ u2("button", { 14459 type: "button", 14460 class: "le-icon", 14461 "data-tooltip": "Previous change K", 14462 "data-tooltip-pos": "left", 14463 disabled: !props.changes.length, 14464 onClick: () => props.onCurrent((props.current - 1 + props.changes.length) % props.changes.length), 14465 children: li.chevronLeft 14466 }), 14467 /* @__PURE__ */ u2("button", { 14468 type: "button", 14469 class: "le-icon", 14470 "data-tooltip": "Next change J", 14471 "data-tooltip-pos": "left", 14472 disabled: !props.changes.length, 14473 onClick: () => props.onCurrent((props.current + 1) % props.changes.length), 14474 children: li.chevronRight 14475 }) 14476 ] 14477 }) 14478 ] 14479 }), 14480 props.changes.length >
14480 0 && (isDiscarding ? /* @__PURE__ */ u2("div", { 14481 class: "le-discardrow", 14482 children: [ 14483 /* @__PURE__ */ u2("span", { 14484 children: [ 14485 "Undo all ", 14486 props.changes.length, 14487 " and put the page back?" 14488 ] 14489 }), 14490 /* @__PURE__ */ u2("button", { 14491 type: "button", 14492 class: "le-btn", 14493 onClick: () => setIsDiscarding(false), 14494 children: "Keep them" 14495 }), 14496 /* @__PURE__ */ u2("button", { 14497 type: "button", 14498 class: "le-btn le-danger", 14499 onClick: () => { 14500 setIsDiscarding(false); 14501 props.onDiscardAll(); 14502 }, 14503 children: "Discard all" 14504 }) 14505 ] 14506 }) : /* @__PURE__ */ u2("div", { 14507 class: "le-discardrow", 14508 children: [ 14509 /* @__PURE__ */ u2("button", { 14510 type: "button", 14511 class: "le-btn" + (props.isPeeking ? " le-primary" : ""), 14512 title: props.isPeeking ? "Put your changes back on the page" : "Roll every change back so you can see the page as it arrived. Nothing is lost - press again to put them back.", 14513 onClick: props.onPeek, 14514 children: [ 14515 li.eye, 14516 props.isPeeking ? "Put changes back" : "Show original" 14517 ] 14518 }),
14519 /* @__PURE__ */ u2("button", { 14520 type: "button", 14521 class: "le-btn", 14522 disabled: props.isPeeking, 14523 title: "Undo every change and empty this list - the page goes back to how it arrived. Nothing is stored until you press Done editing, so this cannot lose anything already saved.", 14524 onClick: () => setIsDiscarding(true), 14525 children: [ 14526 li.trash, 14527 "Discard all" 14528 ] 14529 }) 14530 ] 14531 })), 14532 props.changes.length > 1 && /* @__PURE__ */ u2("div", { 14533 class: "le-side-all", 14534 children: [ 14535 /* @__PURE__ */ u2("label", { 14536 for: "le-scope-all", 14537 children: "Apply all on" 14538 }), 14539 /* @__PURE__ */ u2("select", { 14540 id: "le-scope-all", 14541 class: "le-scope-pick", 14542 title: "Set where EVERY change in this action is allowed to land", 14543 onChange: (ev) => { 14544 const sel = ev.currentTarget; 14545 const next = sel.value; 14546 sel.value = ""; 14547 if (next) { 14548 props.onScopeAll(next); 14549 } 14550 }, 14551 children: [
14552 /* @__PURE__ */ u2("option", { 14553 value: "", 14554 children: "Choose\u2026" 14555 }), 14556 SCOPE_PICKER.map(([id, label]) => /* @__PURE__ */ u2("option", { 14557 value: id, 14558 children: label 14559 })) 14560 ] 14561 }) 14562 ] 14563 }), 14564 /* @__PURE__ */ u2("div", { 14565 class: "le-side-body", 14566 ref: bodyRef, 14567 children: props.changes.length == 0 ? /* @__PURE__ */ u2("div", { 14568 class: "le-empty", 14569 children: [ 14570 /* @__PURE__ */ u2("h4", { 14571 children: "No changes yet" 14572 }), 14573 /* @__PURE__ */ u2("p", { 14574 children: [ 14575 "Click any element on the page to start editing.", 14576 /* @__PURE__ */ u2("br", {}), 14577 "What you change shows up here as a reviewable list." 14578 ] 14579 }) 14580 ] 14581 }) : props.changes.map((change, i4) => { 14582 const summary = summarizeChange(change); 14583 const isUndone = i4 >= props.nChanges; 14584 return /* @__PURE__ */ u2("div", { 14585 class: "le-change" + (i4 == props.current ? " le-on" : "") + (isUndone ? " le-undone" : ""), 14586 onClick: () => props.onCurrent(i4), 14587 onMouseEnter: () => props.onHover(change), 14588 onMouseLeave: () => props.onHover(null), 14589 children: [
14590 /* @__PURE__ */ u2("div", { 14591 class: "le-change-top", 14592 children: [ 14593 /* @__PURE__ */ u2("span", { 14594 class: "le-tag le-t-" + summary.kind, 14595 title: "Change type: " + summary.kind, 14596 children: summary.kind 14597 }), 14598 /* @__PURE__ */ u2("div", { 14599 class: "le-change-desc", 14600 children: summary.title 14601 }) 14602 ] 14603 }), 14604 /* @__PURE__ */ u2("code", { 14605 class: "le-selector", 14606 title: summary.target, 14607 children: summary.target 14608 }), 14609 /* @__PURE__ */ u2("span", { 14610 class: "le-scope-row", 14611 children: [ 14612 /* @__PURE__ */ u2("select", { 14613 class: "le-scope-pick", 14614 title: "Where this change is allowed to land", 14615 value: String(change.scope ?? ""), 14616 onClick: (ev) => ev.stopPropagation(), 14617 onChange: (ev) => { 14618 ev.stopPropagation(); 14619 props.onScope(i4, ev.currentTarget.value); 14620 }, 14621 children: [ 14622 !SCOPE_CHIP[String(change.scope)] && /* @__PURE__ */ u2("option", { 14623 value: "", 14624 children: "Apply on: not set" 14625 }), 14626 SCOPE_PICKER.map(([id, label]) => /* @__PURE__ */ u2("option", { 14627 value: id, 14628 children: "Apply on: " + label 14629 })) 14630 ] 14631 }),
14632 /* @__PURE__ */ u2("select", { 14633 class: "le-scope-pick", 14634 "aria-label": "Show on", 14635 "data-tooltip": "Which screens this change is for - phones are up to 767px wide", 14636 "data-tooltip-pos": "left", 14637 value: String(change.device ?? ""), 14638 onClick: (ev) => ev.stopPropagation(), 14639 onChange: (ev) => { 14640 ev.stopPropagation(); 14641 props.onDevice(i4, ev.currentTarget.value); 14642 }, 14643 children: SHOW_ON_OPTIONS.map((v3) => /* @__PURE__ */ u2("option", { 14644 value: v3.id, 14645 children: v3.label 14646 })) 14647 }), 14648 changeOffersClicks(change) && /* @__PURE__ */ u2(ClicksToggle, { 14649 on: countsClicks(change), 14650 isInsert: !!change.e, 14651 blocked: props.clicksBlocked, 14652 disabled: isUndone, 14653 onToggle: (on) => props.onClicks(i4, on) 14654 }), 14655 !deviceAllows(change.device, isPhoneScreen()) && /* @__PURE__ */ u2("span", { 14656 class: "le-offpage", 14657 title: "The editor shows every change. A visitor at this screen width would not get this one.", 14658 children: change.device == "mobile" ? "phones only - not at this width" : "not on phones - not at this width" 14659 }),
14660 !changeAppliesHere(change) && /* @__PURE__ */ u2("span", { 14661 class: "le-offpage", 14662 title: (change.scope == "page" ? "Recorded for " + String(change.scope_url ?? "another address") + "." : "This page is not built the same way as the one it was recorded on.") + " You can see it here, but a visitor on this page would not.", 14663 children: "visitors here won't see this" 14664 }) 14665 ] 14666 }), 14667 summary.after != void 0 && /* @__PURE__ */ u2("div", { 14668 class: "le-diff", 14669 children: [ 14670 /* @__PURE__ */ u2("div", { 14671 class: "le-lbl", 14672 children: "NOW" 14673 }), 14674 /* @__PURE__ */ u2("div", { 14675 class: "le-after", 14676 children: [ 14677 summary.colors?.map((color) => /* @__PURE__ */ u2("i", { 14678 style: "display:inline-block; width:0.9em; height:0.9em; margin-right:0.3em; vertical-align:-0.1em; border:1px solid rgba(255,255,255,.15); border-radius:0.2em; background:" + color 14679 }, color)), 14680 summary.after.length > 200 ? summary.after.slice(0, 200) + "\u2026" : summary.after 14681 ] 14682 }) 14683 ] 14684 }), 14685 isUndone && /* @__PURE__ */ u2("div", { 14686 class: "le-hint", 14687 style: "margin:0", 14688 children: "Undone - it will not be saved." 14689 }),
14690 confirming == i4 ? /* @__PURE__ */ u2("div", { 14691 class: "le-change-actions", 14692 children: [ 14693 /* @__PURE__ */ u2("span", { 14694 class: "le-hint", 14695 style: "margin:0; flex:10000 1 0", 14696 children: "Undo everything back to here?" 14697 }), 14698 /* @__PURE__ */ u2("button", { 14699 type: "button", 14700 class: "le-btn", 14701 onClick: (ev) => { 14702 ev.stopPropagation(); 14703 setConfirming(-1); 14704 }, 14705 children: "Cancel" 14706 }), 14707 /* @__PURE__ */ u2("button", { 14708 type: "button", 14709 class: "le-btn le-danger", 14710 onClick: (ev) => { 14711 ev.stopPropagation(); 14712 setConfirming(-1); 14713 props.onJump(i4); 14714 }, 14715 children: "Undo" 14716 }) 14717 ] 14718 }) : /* @__PURE__ */ u2("div", { 14719 class: "le-change-actions", 14720 children: [
14721 widgetTypeOf(change) && /* @__PURE__ */ u2("button", { 14722 type: "button", 14723 class: "le-btn le-wkey", 14724 title: "Open this widget's settings", 14725 onClick: (ev) => { 14726 ev.stopPropagation(); 14727 props.onWidgetSettings(change); 14728 }, 14729 children: "Widget settings" 14730 }), 14731 /* @__PURE__ */ u2("button", { 14732 type: "button", 14733 class: "le-btn", 14734 title: "Scroll to it and highlight it", 14735 onClick: (ev) => { 14736 ev.stopPropagation(); 14737 props.onFocus(change); 14738 }, 14739 children: "Focus on page" 14740 }), 14741 !isUndone ? /* @__PURE__ */ u2("button", { 14742 type: "button", 14743 class: "le-btn le-danger", 14744 disabled: props.isPeeking, 14745 title: pausedTip || "Undo this change and everything after it", 14746 onClick: (ev) => { 14747 ev.stopPropagation(); 14748 setConfirming(i4); 14749 }, 14750 children: "Undo" 14751 }) : /* @__PURE__ */ u2("button", { 14752 type: "button", 14753 class: "le-btn", 14754 disabled: props.isPeeking, 14755 title: pausedTip || "Redo this change", 14756 onClick: (ev) => { 14757 ev.stopPropagation(); 14758 props.onJump(i4 + 1); 14759 }, 14760 children: "Redo" 14761 }) 14762 ] 14763 }) 14764 ] 14765 }, i4); 14766 }) 14767 }),
14768 isTipShown && /* @__PURE__ */ u2("div", { 14769 class: "le-tip", 14770 children: [ 14771 /* @__PURE__ */ u2("span", { 14772 children: li.pencil 14773 }), 14774 /* @__PURE__ */ u2("div", { 14775 children: [ 14776 /* @__PURE__ */ u2("b", { 14777 children: "Tip:" 14778 }), 14779 " hover an element to highlight it, click to select. ", 14780 /* @__PURE__ */ u2("kbd", { 14781 children: "N" 14782 }), 14783 " toggles Navigate mode, ", 14784 /* @__PURE__ */ u2("kbd", { 14785 children: "Esc" 14786 }), 14787 " comes back." 14788 ] 14789 }), 14790 /* @__PURE__ */ u2("button", { 14791 type: "button", 14792 title: "Dismiss this tip", 14793 onClick: () => setIsTipShown(false), 14794 children: "\xD7" 14795 }) 14796 ] 14797 }), 14798 /* @__PURE__ */ u2("div", { 14799 class: "le-side-foot", 14800 children: /* @__PURE__ */ u2("button", { 14801 type: "button", 14802 class: "le-btn le-primary", 14803 onClick: props.onDone, 14804 children: "Done editing - back to Personyze" 14805 }) 14806 }) 14807 ] 14808 }); 14809} 14810function PageAiCard(props) { 14811 const [isOpen, setIsOpen] = p2(false); 14812 const [prompt, setPrompt] = p2(""); 14813 const boxRef = F2(null); 14814 function send() { 14815 const text = prompt.trim(); 14816 if (text) { 14817 setPrompt(""); 14818 setIsOpen(false); 14819 props.onSend(text); 14820 } 14821 } 14822 return /* @__PURE__ */ u2("div", { 14823 class: "le-pageai", 14824 children: [
14825 /* @__PURE__ */ u2("button", { 14826 type: "button", 14827 class: "le-pageai-head", 14828 "aria-expanded": isOpen, 14829 onClick: () => { 14830 setIsOpen((v3) => !v3); 14831 setTimeout(() => boxRef.current?.focus(), 0); 14832 }, 14833 children: [ 14834 /* @__PURE__ */ u2("span", { 14835 class: "le-pageai-ico", 14836 children: li.sparkle 14837 }), 14838 /* @__PURE__ */ u2("span", { 14839 class: "le-pageai-txt", 14840 children: [ 14841 /* @__PURE__ */ u2("b", { 14842 children: "Page-level AI" 14843 }), 14844 /* @__PURE__ */ u2("small", { 14845 children: "Ask for changes across the whole page" 14846 }) 14847 ] 14848 }), 14849 isOpen ? li.chevronUp : li.chevronDown 14850 ] 14851 }), 14852 isOpen && /* @__PURE__ */ u2("div", { 14853 class: "le-pageai-body", 14854 children: [ 14855 /* @__PURE__ */ u2("textarea", { 14856 ref: boxRef, 14857 class: "le-area", 14858 rows: 2, 14859 value: prompt, 14860 placeholder: "e.g. 'rewrite for enterprise buyers and make the CTAs more urgent'", 14861 onInput: (ev) => setPrompt(ev.target.value), 14862 onKeyDown: (ev) => { 14863 if (ev.key == "Enter" && !ev.shiftKey) { 14864 ev.preventDefault(); 14865 send(); 14866 } 14867 } 14868 }),
14869 /* @__PURE__ */ u2("div", { 14870 class: "le-chips", 14871 children: PAGE_AI_CHIPS.map((chip) => /* @__PURE__ */ u2("button", { 14872 type: "button", 14873 class: "le-chip", 14874 onClick: () => { 14875 setPrompt(chip); 14876 boxRef.current?.focus(); 14877 }, 14878 children: chip 14879 }, chip)) 14880 }), 14881 /* @__PURE__ */ u2("div", { 14882 class: "le-btn-bar", 14883 children: [ 14884 /* @__PURE__ */ u2("button", { 14885 type: "button", 14886 class: "le-btn", 14887 onClick: () => setIsOpen(false), 14888 children: "Cancel" 14889 }), 14890 /* @__PURE__ */ u2("button", { 14891 type: "button", 14892 class: "le-btn le-ai", 14893 disabled: !prompt.trim(), 14894 onClick: send, 14895 children: [ 14896 li.sparkle, 14897 "Suggest edits" 14898 ] 14899 }) 14900 ] 14901 }) 14902 ] 14903 }) 14904 ] 14905 }); 14906} 14907
14908// src/components/live/DevicePicker.tsx 14909var DEVICE_WIDTHS = { 14910 desktop: 0, 14911 tablet: 768, 14912 mobile: 375 14913}; 14914function DevicePicker(props) { 14915 const devices = [ 14916 [ 14917 "desktop", 14918 "Desktop", 14919 "" 14920 ], 14921 [ 14922 "tablet", 14923 "Tablet", 14924 "768" 14925 ], 14926 [ 14927 "mobile", 14928 "Mobile", 14929 "375" 14930 ] 14931 ]; 14932 return /* @__PURE__ */ u2("div", { 14933 class: "le-root le-devices", 14934 role: "group", 14935 "aria-label": "Preview device", 14936 children: devices.map(([id, label, size]) => /* @__PURE__ */ u2("button", { 14937 type: "button", 14938 class: "le-device" + (id == props.device ? " le-on" : ""), 14939 "data-tooltip": id == "desktop" ? "Back to the window size you started with" : "Resize the window to " + size + "px wide", 14940 "data-tooltip-pos": "top", 14941 onClick: () => props.onDevice(id), 14942 children: [ 14943 li[id], 14944 label, 14945 size && /* @__PURE__ */ u2("span", { 14946 class: "le-size", 14947 children: size 14948 }) 14949 ] 14950 }, id)) 14951 }); 14952} 14953
14954// src/components/live/liveTooltip.tsx 14955var DELAY_MS = 350; 14956var GAP = 8; 14957var MARGIN = 8; 14958function placeTip(r3, tip, prefer, vw, vh) { 14959 const clampX = (x2) => Math.max(MARGIN, Math.min(x2, vw - MARGIN - tip.width)); 14960 const clampY = (y3) => Math.max(MARGIN, Math.min(y3, vh - MARGIN - tip.height)); 14961 if (prefer == "left") { 14962 const left = r3.left - GAP - tip.width; 14963 if (left >= MARGIN) { 14964 return { 14965 left, 14966 top: clampY(r3.top + r3.height / 2 - tip.height / 2), 14967 side: "left" 14968 }; 14969 } 14970 } 14971 const below = r3.bottom + GAP; 14972 const above = r3.top - GAP - tip.height; 14973 const fitsBelow = below + tip.height <= vh - MARGIN; 14974 const fitsAbove = above >= MARGIN; 14975 const side = prefer == "top" ? fitsAbove || !fitsBelow ? "top" : "bottom" : fitsBelow || !fitsAbove ? "bottom" : "top"; 14976 return { 14977 left: clampX(r3.left + r3.width / 2 - tip.width / 2), 14978 top: clampY(side == "top" ? above : below), 14979 side 14980 }; 14981} 14982function TipLayer() { 14983 const ref = F2(null); 14984 _2(() => { 14985 const tip = ref.current; 14986 if (!tip) { 14987 return; 14988 } 14989 const root = tip.getRootNode(); 14990 let timer = 0; 14991 let target = null; 14992 const hide = () => { 14993 clearTimeout(timer); 14994 target = null; 14995 tip.classList.remove("le-on"); 14996 }; 14997 const place = (el) => { 14998 tip.textContent = el.getAttribute("data-tooltip") ?? ""; 14999 tip.style.left = "0px"; 15000 tip.style.top = "0px"; 15001 const p3 = placeTip(el.getBoundingClientRect(), tip.getBoundingClientRect(), el.getAttribute("data-tooltip-pos") ?? "bottom", globalThis.innerWidth, globalThis.innerHeight); 15002 tip.style.left = Math.round(p3.left) + "px"; 15003 tip.style.top = Math.round(p3.top) + "px"; 15004 tip.setAttribute("data-side", p3.side); 15005 tip.classList.add("le-on"); 15006 }; 15007 const arm = (el, delay) => { 15008 if (el === target) { 15009 return; 15010 } 15011 hide(); 15012 if (!el?.getAttribute("data-tooltip")) { 15013 return; 15014 } 15015 target = el; 15016 timer = setTimeout(() => { 15017 if (target === el && el.isConnected) place(el); 15018 }, delay); 15019 }; 15020 const tipOf = (ev) => ev.target?.closest?.("[data-tooltip]") ?? null; 15021 const onOver = (ev) => arm(tipOf(ev), DELAY_MS); 15022 const onOut = (ev) => { 15023 const to = ev.relatedTarget; 15024 if (!(target && to && target.contains(to))) { 15025 hide(); 15026 } 15027 }; 15028 const onFocus = (ev) => arm(tipOf(ev), 0); 15029 const onKey = (ev) => { 15030 if (ev.key == "Escape") hide(); 15031 }; 15032 root.addEventListener("pointerover", onOver); 15033 root.addEventListener("pointerout", onOut); 15034 root.addEventListener("focusin", onFocus); 15035 root.addEventListener("focusout", hide); 15036 root.addEventListener("pointerdown", hide); 15037 root.addEventListener("keydown", onKey); 15038 globalThis.addEventListener("scroll", hide, true); 15039 return () => { 15040 hide(); 15041 root.removeEventListener("pointerover", onOver); 15042 root.removeEventListener("pointerout", onOut); 15043 root.removeEventListener("focusin", onFocus); 15044 root.removeEventListener("focusout", hide); 15045 root.removeEventListener("pointerdown", hide); 15046 root.removeEventListener("keydown", onKey); 15047 globalThis.removeEventListener("scroll", hide, true); 15048 }; 15049 }, []); 15050 return /* @__PURE__ */ u2("div", { 15051 ref, 15052 class: "le-root le-tooltip", 15053 role: "tooltip", 15054 "aria-hidden": "true" 15055 }); 15056} 15057
15058// src/components/live/panels/caretOwner.ts 15059var PAGE_GESTURES = [ 15060 "mousedown", 15061 "keydown" 15062]; 15063function installPageTakeover(elem, onPageTook) { 15064 for (const type of PAGE_GESTURES) { 15065 elem.addEventListener(type, onPageTook); 15066 } 15067 return () => { 15068 for (const type of PAGE_GESTURES) { 15069 elem.removeEventListener(type, onPageTook); 15070 } 15071 }; 15072} 15073function installSavedSelection(doc, elem, opts) { 15074 function handler() { 15075 if (opts.skip?.()) { 15076 return; 15077 } 15078 const sel = doc.getSelection(); 15079 if (!sel) { 15080 return; 15081 } 15082 const cur = []; 15083 for (let i4 = 0; i4 < sel.rangeCount; i4++) { 15084 const r3 = sel.getRangeAt(i4); 15085 const container = r3.commonAncestorContainer; 15086 if (container === elem || elem.contains(container)) { 15087 cur.push(r3.cloneRange()); 15088 } 15089 } 15090 if (cur.length) { 15091 opts.onSaved(cur); 15092 } 15093 } 15094 doc.addEventListener("selectionchange", handler); 15095 return () => doc.removeEventListener("selectionchange", handler); 15096} 15097
15098// src/modules/vartoken/private/vartoken.ts 15099var RE_TOKEN = /^\$\{(\w+)\s*((?:[^'"}]|'[^']*'|"[^"]*")*)\}$/; 15100var RE_PATH_STEP = /^-(?:>|>)\s*(\w+|(?:'[^']*')+|(?:"[^"]*")+)\s*/; 15101var RE_SUBST_FUNC = /^:(\w+)(?:\(((?:'[^']*'|"[^"]*"|[^'"\)])*)\))?\s*/; 15102var RE_FUNC_ARGS = /\s*(?:'[^']*')+\s*|\s*(?:"[^"]*")+\s*|[^'"\),]+/g; 15103var DIRECTIVES = [ 15104 "foreach", 15105 "if", 15106 "else", 15107 "case", 15108 "when", 15109 "end", 15110 "endforeach", 15111 "endif", 15112 "endcase", 15113 "menu" 15114]; 15115function unquote(s3) { 15116 if (s3.length >= 2 && s3[0] == "'" && s3[s3.length - 1] == "'") { 15117 return s3.slice(1, -1).replaceAll("''", "'"); 15118 } 15119 if (s3.length >= 2 && s3[0] == '"' && s3[s3.length - 1] == '"') { 15120 return s3.slice(1, -1).replaceAll('""', '"'); 15121 } 15122 return s3; 15123} 15124function quoteStr(s3) { 15125 return "'" + s3.replaceAll("'", "''") + "'"; 15126} 15127function argToLiteral(arg) { 15128 return /^[+-]?\d+$/.test(arg) ? arg : quoteStr(arg); 15129} 15130function splitFuncArgs(argsStr) { 15131 if (!argsStr) { 15132 return []; 15133 } 15134 const out = []; 15135 for (const m2 of argsStr.matchAll(RE_FUNC_ARGS)) { 15136 const tok = m2[0].trim(); 15137 if (tok) { 15138 out.push(unquote(tok)); 15139 } 15140 } 15141 return out; 15142} 15143function pathToString(path) { 15144 let out = path[0] ?? ""; 15145 for (let i4 = 1; i4 < path.length; i4++) { 15146 const step = path[i4]; 15147 out += "->" + (/^[a-z_]\w*$/i.test(step) ? step : quoteStr(step)); 15148 } 15149 return out; 15150} 15151function funcsToString(funcs) { 15152 let out = ""; 15153 for (const f4 of funcs) { 15154 out += ":" + f4.name + (f4.args.length ? "(" + f4.args.map(argToLiteral).join(", ") + ")" : ""); 15155 } 15156 return out; 15157} 15158function parseVarToken(text) { 15159 const m2 = RE_TOKEN.exec(text.trim()); 15160 if (!m2) { 15161 return void 0; 15162 } 15163 const ident = m2[1].toLowerCase(); 15164 let rest = m2[2] ?? ""; 15165 if (DIRECTIVES.includes(ident) || rest.startsWith("(")) { 15166 return void 0; 15167 } 15168 const path = [ 15169 ident 15170 ]; 15171 while (true) { 15172 const pm = RE_PATH_STEP.exec(rest); 15173 if (!pm) { 15174 break; 15175 } 15176 const tok = pm[1]; 15177 path.push(tok[0] == "'" || tok[0] == '"' ? unquote(tok) : tok.toLowerCase()); 15178 rest = rest.slice(pm[0].length); 15179 } 15180 let htmlEncoded = false; 15181 let jsonEncoded = false; 15182 let defaultValue; 15183 const funcs = []; 15184 while (true) { 15185 const fm = RE_SUBST_FUNC.exec(rest); 15186 if (!fm) { 15187 break; 15188 } 15189 const name = fm[1].toLowerCase(); 15190 if (name == "html") { 15191 htmlEncoded = true; 15192 } else if (name == "json") { 15193 jsonEncoded = true; 15194 } else if (name == "default") { 15195 defaultValue = splitFuncArgs(fm[2])[0] ?? ""; 15196 } else { 15197 funcs.push({ 15198 name, 15199 args: splitFuncArgs(fm[2]) 15200 }); 15201 } 15202 rest = rest.slice(fm[0].length); 15203 } 15204 if (rest.trim() != "") { 15205 return void 0; 15206 } 15207 return { 15208 path, 15209 funcs, 15210 defaultValue, 15211 htmlEncoded, 15212 jsonEncoded 15213 }; 15214} 15215function varExpr(path, funcs, flags = {}) { 15216 let body = pathToString(path) + funcsToString(funcs.filter((f4) => f4.name != "default")); 15217 if (flags.defaultValue != null && flags.defaultValue !== "") { 15218 body += ":default(" + quoteStr(flags.defaultValue) + ")"; 15219 } 15220 if (flags.jsonEncoded) { 15221 body += ":json"; 15222 } 15223 if (flags.htmlEncoded) { 15224 body += ":html"; 15225 } 15226 return "${" + body + "}"; 15227} 15228var RE_ANY_TOKEN = /\$\{[^}]*\}/g; 15229function tokenAtCaret(text, sel) { 15230 RE_ANY_TOKEN.lastIndex = 0; 15231 let m2; 15232 while (m2 = RE_ANY_TOKEN.exec(text)) { 15233 const start2 = m2.index; 15234 const end = start2 + m2[0].length; 15235 if (sel >= start2 && sel <= end) { 15236 return { 15237 start: start2, 15238 end, 15239 text: m2[0] 15240 }; 15241 } 15242 } 15243 return void 0; 15244} 15245var KNOWN_FUNCS = [ 15246 { 15247 name: "ellipsis", 15248 title: 'Cut long text, and show "\u2026" at the end, if was cut', 15249 args: [ 15250 { 15251 title: "Show at most letters", 15252 inputType: "number", 15253 defaultValue: "50" 15254 } 15255 ] 15256 }, 15257 { 15258 name: "replace", 15259 title: "Replace text", 15260 args: [ 15261 { 15262 title: "Find" 15263 }, 15264 { 15265 title: "Replace with" 15266 } 15267 ] 15268 }, 15269 // Kept in step with the panel's catalog. The default separator is not 15270 // decoration: `st_sud__text_before(x, '')` returns '' (Locate('', x) is 1), 15271 // so an empty box would blank the value the moment the row is added. 15272 { 15273 name: "text_before", 15274 title: "Keep only the text before a word", 15275 args: [ 15276 { 15277 title: "Word to cut at", 15278 defaultValue: " - " 15279 } 15280 ] 15281 }, 15282 { 15283 name: "text_after", 15284 title: "Keep the text from a word on", 15285 args: [ 15286 { 15287 title: "Word to start at", 15288 defaultValue: " - " 15289 } 15290 ] 15291 }, 15292 { 15293 name: "lc", 15294 title: "Convert text to lower case", 15295 args: [] 15296 }, 15297 { 15298 name: "uc", 15299 title: "Convert text to upper case", 15300 args: [] 15301 }, 15302 { 15303 name: "price", 15304 title: "Format price", 15305 args: [ 15306 { 15307 title: "Price format", 15308 inputType: "price" 15309 } 15310 ] 15311 }, 15312 { 15313 name: "ceil", 15314 title: "Round number towards infinity", 15315 args: [] 15316 }, 15317 { 15318 name: "floor", 15319 title: "Round number towards minus infinity", 15320 args: [] 15321 }, 15322 { 15323 name: "round", 15324 title: "Round number", 15325 args: [] 15326 } 15327]; 15328function funcTitle(func) { 15329 return KNOWN_FUNCS.find((d3) => d3.name == func.name.toLowerCase())?.title ?? func.name; 15330} 15331function funcArgRows(func) { 15332 const def = KNOWN_FUNCS.find((d3) => d3.name == func.name.toLowerCase()); 15333 if (def) { 15334 return def.args; 15335 } 15336 return func.args.map((v3, i4) => ({ 15337 title: `Argument ${i4 + 1}`, 15338 defaultValue: v3 15339 })); 15340} 15341function newFunc(name) { 15342 const def = KNOWN_FUNCS.find((d3) => d3.name == name.toLowerCase()); 15343 return { 15344 name, 15345 args: (def?.args ?? []).map((a3) => a3.defaultValue ?? "") 15346 }; 15347} 15348
15349// src/components/live/VarPicker.tsx 15350var SESSION_VARS = [ 15351 { 15352 name: "vid", 15353 label: "Page view ID" 15354 }, 15355 { 15356 name: "1_vid", 15357 label: "First page view ID in session" 15358 }, 15359 { 15360 name: "st", 15361 label: "Session counter" 15362 }, 15363 { 15364 name: "country", 15365 label: "User country" 15366 }, 15367 { 15368 name: "city", 15369 label: "User city" 15370 }, 15371 { 15372 name: "ip", 15373 label: "User IP" 15374 }, 15375 { 15376 name: "ur", 15377 label: "URL of current page" 15378 }, 15379 { 15380 name: "rf", 15381 label: "Referrer of current page" 15382 }, 15383 { 15384 name: "srch_word", 15385 label: "Search word" 15386 }, 15387 { 15388 name: "1_srch_word", 15389 label: "Search word in first page view" 15390 } 15391]; 15392function useUserColumns() { 15393 const [cols, setCols] = p2([]); 15394 _2(() => { 15395 if (isJsapiConfigured()) { 15396 queryRows("SELECT name, title FROM other_columns_info WHERE table_name='social' ORDER BY ordinal DESC").then((rows) => setCols(rows.map((r3) => ({ 15397 path: [ 15398 String(r3.name) 15399 ], 15400 label: String(r3.title || r3.name) 15401 }))), (error) => { 15402 console.error(error); 15403 }); 15404 } 15405 }, []); 15406 return cols; 15407} 15408function FuncsEditor(props) { 15409 function setArg(funcI, argI, value) { 15410 props.onChange(props.funcs.map((f4, i4) => i4 != funcI ? f4 : { 15411 ...f4, 15412 args: Object.assign([ 15413 ...f4.args 15414 ], { 15415 [argI]: value 15416 }) 15417 })); 15418 } 15419 return /* @__PURE__ */ u2("div", { 15420 class: "le-vp-funcs", 15421 children: [
15422 /* @__PURE__ */ u2("div", { 15423 class: "le-vp-funcs-hd", 15424 children: "Convert the value" 15425 }), 15426 props.funcs.map((f4, i4) => /* @__PURE__ */ u2("div", { 15427 class: "le-vp-func", 15428 children: [ 15429 /* @__PURE__ */ u2("div", { 15430 class: "le-vp-func-hd", 15431 children: [ 15432 /* @__PURE__ */ u2("span", { 15433 children: funcTitle(f4) 15434 }), 15435 /* @__PURE__ */ u2("button", { 15436 type: "button", 15437 title: "Remove", 15438 onClick: () => props.onChange(props.funcs.filter((_3, k3) => k3 != i4)), 15439 children: li.cross 15440 }) 15441 ] 15442 }), 15443 funcArgRows(f4).map((a3, argI) => /* @__PURE__ */ u2("label", { 15444 class: "le-vp-func-arg", 15445 children: [ 15446 /* @__PURE__ */ u2("span", { 15447 children: a3.title || `Argument ${argI + 1}` 15448 }), 15449 /* @__PURE__ */ u2("input", { 15450 type: a3.inputType == "number" ? "number" : "text", 15451 class: "le-input", 15452 value: f4.args[argI] ?? a3.defaultValue ?? "", 15453 placeholder: a3.inputType == "price" ? "e.g. $#,###.##" : void 0, 15454 onInput: (ev) => setArg(i4, argI, ev.target.value) 15455 }) 15456 ] 15457 }, argI)) 15458 ] 15459 }, `${i4}-${f4.name}`)),
15460 /* @__PURE__ */ u2("select", { 15461 class: "le-select", 15462 value: "", 15463 onChange: (ev) => { 15464 const name = ev.target.value; 15465 ev.target.value = ""; 15466 if (name) { 15467 props.onChange([ 15468 ...props.funcs, 15469 newFunc(name) 15470 ]); 15471 } 15472 }, 15473 children: [ 15474 /* @__PURE__ */ u2("option", { 15475 value: "", 15476 children: "+ Add function" 15477 }), 15478 KNOWN_FUNCS.map((d3) => /* @__PURE__ */ u2("option", { 15479 value: d3.name, 15480 children: d3.title 15481 }, d3.name)) 15482 ] 15483 }) 15484 ] 15485 }); 15486} 15487function VarPicker(props) { 15488 const start2 = q2(() => parseVarToken(props.initialToken ?? ""), []); 15489 const [selected, setSelected] = p2(start2?.path); 15490 const [funcs, setFuncs] = p2(start2?.funcs ?? []); 15491 const [query2, setQuery] = p2(""); 15492 const searchRef = F2(null); 15493 const userCols = useUserColumns(); 15494 _2(() => searchRef.current?.focus(), []); 15495 function commit() { 15496 if (selected) { 15497 props.onPick(varExpr(selected, funcs, start2 ?? {})); 15498 } 15499 } 15500 function onKeyDown(ev) { 15501 if (ev.key == "Escape") { 15502 ev.stopPropagation(); 15503 props.onCancel(); 15504 } else if (ev.key == "Enter" && selected) { 15505 ev.stopPropagation(); 15506 ev.preventDefault(); 15507 commit(); 15508 } 15509 } 15510 const sections = q2(() => [ 15511 { 15512 title: "User variables", 15513 entries: [ 15514 { 15515 path: [ 15516 "id" 15517 ], 15518 label: "User ID" 15519 }, 15520 ...userCols 15521 ] 15522 }, 15523 { 15524 title: "Session & context", 15525 entries: SESSION_VARS.map((v3) => ({ 15526 path: [ 15527 v3.name 15528 ], 15529 label: v3.label 15530 })) 15531 } 15532 ], [ 15533 userCols 15534 ]); 15535 const q3 = query2.trim().toLowerCase(); 15536 const shown = !q3 ? sections : sections.map((s3) => ({ 15537 title: s3.title, 15538 entries: s3.entries.filter((e3) => e3.label.toLowerCase().includes(q3) || e3.path.join("->").toLowerCase().includes(q3)) 15539 })).filter((s3) => s3.entries.length > 0); 15540 const selectedKey = selected?.join("->") ?? ""; 15541 const unparsed = props.initialToken && !start2 ? props.initialToken : ""; 15542 return ( 15543 // keep the page selection, which would make this modal's inputs unfocusable. 15544 /* @__PURE__ */ u2("div", { 15545 class: "le-root le-vp-overlay", 15546 onMouseDown: (ev) => { 15547 ev.stopPropagation(); 15548 if (ev.target === ev.currentTarget) { 15549 props.onCancel(); 15550 } 15551 }, 15552 onKeyDown, 15553 children: /* @__PURE__ */ u2("div", { 15554 class: "le-vp", 15555 role: "dialog", 15556 "aria-modal": "true", 15557 "aria-label": "Insert variable", 15558 children: [
15559 /* @__PURE__ */ u2("div", { 15560 class: "le-vp-head", 15561 children: [ 15562 /* @__PURE__ */ u2("div", { 15563 children: [ 15564 /* @__PURE__ */ u2("h3", { 15565 children: start2 ? "Edit variable" : "Insert variable" 15566 }), 15567 /* @__PURE__ */ u2("p", { 15568 children: "Pick a field, and optionally convert its value." 15569 }) 15570 ] 15571 }), 15572 /* @__PURE__ */ u2("button", { 15573 type: "button", 15574 class: "le-icon", 15575 title: "Close", 15576 onClick: props.onCancel, 15577 children: li.cross 15578 }) 15579 ] 15580 }), 15581 /* @__PURE__ */ u2("div", { 15582 class: "le-vp-search", 15583 children: /* @__PURE__ */ u2("input", { 15584 ref: searchRef, 15585 type: "search", 15586 class: "le-input", 15587 value: query2, 15588 placeholder: "Search variables\u2026", 15589 "aria-label": "Search variables", 15590 onInput: (ev) => setQuery(ev.target.value) 15591 }) 15592 }),
15593 /* @__PURE__ */ u2("div", { 15594 class: "le-vp-body", 15595 children: [ 15596 unparsed && /* @__PURE__ */ u2("div", { 15597 class: "le-vp-empty", 15598 children: [ 15599 /* @__PURE__ */ u2("code", { 15600 children: unparsed 15601 }), 15602 " is a calculated value - it can't be edited here. Picking a variable below replaces it." 15603 ] 15604 }), 15605 shown.map((s3) => /* @__PURE__ */ u2("div", { 15606 class: "le-vp-sec", 15607 children: [ 15608 /* @__PURE__ */ u2("h4", { 15609 children: s3.title 15610 }), 15611 /* @__PURE__ */ u2("div", { 15612 class: "le-vp-grid", 15613 children: s3.entries.map((e3) => { 15614 const key = e3.path.join("->"); 15615 return /* @__PURE__ */ u2("button", { 15616 type: "button", 15617 class: "le-vp-var" + (key == selectedKey ? " le-on" : ""), 15618 onClick: () => setSelected(e3.path), 15619 onDblClick: () => props.onPick(varExpr(e3.path, funcs, start2 ?? {})), 15620 children: [
15621 /* @__PURE__ */ u2("span", { 15622 class: "le-vp-tok", 15623 children: varExpr(e3.path, []) 15624 }), 15625 e3.label 15626 ] 15627 }, key); 15628 }) 15629 }) 15630 ] 15631 }, s3.title)), 15632 shown.length == 0 && /* @__PURE__ */ u2("div", { 15633 class: "le-vp-empty", 15634 children: [ 15635 'No variable matches "', 15636 query2, 15637 '".' 15638 ] 15639 }) 15640 ] 15641 }), 15642 selected && /* @__PURE__ */ u2("div", { 15643 class: "le-vp-picked", 15644 children: [ 15645 /* @__PURE__ */ u2("div", { 15646 class: "le-vp-expr", 15647 children: varExpr(selected, funcs, start2 ?? {}) 15648 }), 15649 /* @__PURE__ */ u2(FuncsEditor, { 15650 funcs, 15651 onChange: setFuncs 15652 }) 15653 ] 15654 }),
15655 /* @__PURE__ */ u2("div", { 15656 class: "le-vp-foot", 15657 children: [ 15658 /* @__PURE__ */ u2("span", { 15659 class: "le-vp-hint", 15660 children: selected ? "Double-click a variable to insert it right away." : "Pick a variable to insert." 15661 }), 15662 /* @__PURE__ */ u2("button", { 15663 type: "button", 15664 class: "le-btn", 15665 onClick: props.onCancel, 15666 children: "Cancel" 15667 }), 15668 /* @__PURE__ */ u2("button", { 15669 type: "button", 15670 class: "le-btn le-primary", 15671 disabled: !selected, 15672 onClick: commit, 15673 children: start2 ? "Apply" : "Insert" 15674 }) 15675 ] 15676 }) 15677 ] 15678 }) 15679 }) 15680 ); 15681} 15682
15683// src/components/live/VarButton.tsx 15684function VarButton(props) { 15685 const [isOpen, setIsOpen] = p2(false); 15686 const [editing, setEditing] = p2(void 0); 15687 const atCaret = props.text != void 0 ? tokenAtCaret(props.text, props.caret ?? 0) : void 0; 15688 return /* @__PURE__ */ u2(k, { 15689 children: [ 15690 /* @__PURE__ */ u2("button", { 15691 type: "button", 15692 class: "le-btn", 15693 onClick: () => { 15694 setEditing(atCaret); 15695 setIsOpen(true); 15696 }, 15697 children: [ 15698 li.braces, 15699 props.label ?? (atCaret ? "Edit variable" : "Insert variable") 15700 ] 15701 }), 15702 isOpen && /* @__PURE__ */ u2(VarPicker, { 15703 initialToken: editing?.text, 15704 onPick: (token) => { 15705 setIsOpen(false); 15706 if (props.onInsert && props.text != void 0) { 15707 const start2 = editing ? editing.start : props.caret ?? props.text.length; 15708 const end = editing ? editing.end : props.caret ?? props.text.length; 15709 props.onInsert(props.text.slice(0, start2) + token + props.text.slice(end), start2 + token.length); 15710 } 15711 props.onPick?.(token); 15712 }, 15713 onCancel: () => setIsOpen(false) 15714 }) 15715 ] 15716 }); 15717} 15718
15719// src/modules/casesyntax/private/casesyntax.ts 15720var CASE_OPEN_RE = /^\$\{case\b[^}]*\}/i; 15721var CASE_END_RE = /\$\{end\}\s*$/i; 15722var BLOCK_TOKEN_RE = /\$\{(case\b[^}]*|if\b[^}]*|foreach\b[^}]*|when\b[^}]*|else|end)\}/gi; 15723var WHEN_PREFIX = "${when "; 15724var ELSE_TOKEN = "${else}"; 15725var CASE_OPEN_TOKEN = "${case}"; 15726var CASE_END_TOKEN = "${end}"; 15727function parse(value) { 15728 const trimmed = value.trim(); 15729 const openMatch = trimmed.match(CASE_OPEN_RE); 15730 if (!openMatch || !CASE_END_RE.test(trimmed)) { 15731 return null; 15732 } 15733 const expr = openMatch[0].slice("${case".length, -1).trim(); 15734 if (expr) { 15735 return null; 15736 } 15737 const markers = []; 15738 let depth = 0; 15739 let closedAt = -1; 15740 let m2; 15741 BLOCK_TOKEN_RE.lastIndex = 0; 15742 while (m2 = BLOCK_TOKEN_RE.exec(trimmed)) { 15743 const word = m2[1].toLowerCase(); 15744 if (word === "end") { 15745 depth--; 15746 if (depth === 0) { 15747 closedAt = m2.index; 15748 break; 15749 } 15750 if (depth < 0) { 15751 return null; 15752 } 15753 } else if (word === "else" || word.startsWith("when")) { 15754 if (depth === 1) { 15755 markers.push({ 15756 start: m2.index, 15757 end: m2.index + m2[0].length, 15758 when: word === "else" ? "" : m2[0].slice(WHEN_PREFIX.length, -1).trim() 15759 }); 15760 } 15761 } else { 15762 depth++; 15763 } 15764 } 15765 if (closedAt < 0 || closedAt + CASE_END_TOKEN.length !== trimmed.length || markers.length === 0) { 15766 return null; 15767 } 15768 const whens = []; 15769 for (let i4 = 0; i4 < markers.length; i4++) { 15770 const start2 = markers[i4].end; 15771 const end = i4 + 1 < markers.length ? markers[i4 + 1].start : closedAt; 15772 whens.push({ 15773 when: markers[i4].when, 15774 then: trimmed.slice(start2, end) 15775 }); 15776 } 15777 return { 15778 expr: "", 15779 whens 15780 }; 15781} 15782function serialize(value) { 15783 if (!value.whens.length) { 15784 return ""; 15785 } 15786 const parts = [ 15787 CASE_OPEN_TOKEN 15788 ]; 15789 for (const { when, then } of value.whens) { 15790 parts.push(when === "" ? ELSE_TOKEN : WHEN_PREFIX + when + "}"); 15791 parts.push(then); 15792 } 15793 parts.push(CASE_END_TOKEN); 15794 return parts.join(""); 15795} 15796function deriveCases(value) { 15797 const parsed = parse(value); 15798 if (!parsed) { 15799 return { 15800 activeCases: [], 15801 elseValue: value 15802 }; 15803 } 15804 return { 15805 activeCases: parsed.whens.filter((w3) => w3.when !== ""), 15806 elseValue: parsed.whens.find((w3) => w3.when === "")?.then ?? "" 15807 }; 15808} 15809function composeCases(activeCases, elseValue) { 15810 if (activeCases.length === 0) { 15811 return elseValue; 15812 } 15813 return serialize({ 15814 expr: "", 15815 whens: [ 15816 ...activeCases, 15817 { 15818 when: "", 15819 then: elseValue 15820 } 15821 ] 15822 }); 15823} 15824
15825// src/modules/i18n/private/i18n.ts 15826function en(text) { 15827 return text; 15828} 15829
15830// src/modules/rulesyntax/private/rulesyntax.ts 15831function emptyGroup(combinator = "and") { 15832 return { 15833 type: "group", 15834 not: false, 15835 combinator, 15836 children: [] 15837 }; 15838} 15839var NAME_PAT = `([a-zA-Z_]\\w*(?:\\(\\))?(?:->[a-zA-Z_]\\w*)?)`; 15840var VAL_PAT = `('(?:[^']|'')*'|-?\\d+(?:\\.\\d+)?|[a-zA-Z_]\\w*(?:->[a-zA-Z_]\\w*)?)`; 15841var LIST_ITEM_PAT = `(?:'(?:[^']|'')*'|-?\\d+(?:\\.\\d+)?)`; 15842var LIST_PAT = `(${LIST_ITEM_PAT}(?:\\s*,\\s*${LIST_ITEM_PAT})*)`; 15843var LIST_RE = new RegExp(`^${LIST_PAT}$`); 15844function quote(v3) { 15845 return "'" + v3.replace(/'/g, "''") + "'"; 15846} 15847function unquote2(v3) { 15848 if (v3.length >= 2 && v3[0] === "'" && v3[v3.length - 1] === "'") { 15849 return v3.slice(1, -1).replace(/''/g, "'"); 15850 } 15851 return v3; 15852} 15853function classifyValue(raw) { 15854 if (raw.length >= 2 && raw[0] === "'" && raw[raw.length - 1] === "'") { 15855 return { 15856 value: unquote2(raw), 15857 valueField: false 15858 }; 15859 } 15860 if (/^-?\d/.test(raw)) { 15861 return { 15862 value: raw, 15863 valueField: false 15864 }; 15865 } 15866 return { 15867 value: raw, 15868 valueField: true 15869 }; 15870} 15871var NAME_FIRST = (m2) => ({ 15872 name: m2[1], 15873 ...classifyValue(m2[2] ?? "") 15874}); 15875var VALUE_FIRST = (m2) => ({ 15876 name: m2[2], 15877 ...classifyValue(m2[1] ?? "") 15878}); 15879var NAME_ONLY = (m2) => ({ 15880 name: m2[1], 15881 value: "", 15882 valueField: false 15883}); 15884var NAME_AND_LIST = (m2) => ({ 15885 name: m2[1], 15886 value: m2[2] ?? "", 15887 valueField: false 15888}); 15889function normalizeInList(list) { 15890 const trimmed = list.trim(); 15891 if (LIST_RE.test(trimmed)) return trimmed; 15892 return trimmed.split(",").map((s3) => s3.trim()).filter((s3) => s3.length > 0).map((s3) => /^-?\d+(?:\.\d+)?$/.test(s3) ? s3 : quote(s3)).join(","); 15893} 15894function serializeIn(name, list, negated) { 15895 const items = normalizeInList(list); 15896 return items ? `${name} ${negated ? "not in" : "in"} (${items})` : ""; 15897} 15898var OPERATIONS = [ 15899 { 15900 value: "equals", 15901 label: en("Exactly matches"), 15902 category: "string", 15903 hasValue: true, 15904 serialize: (n2, v3) => `${n2}=${v3}`, 15905 matchRegex: new RegExp(`^${NAME_PAT}\\s*=\\s*${VAL_PAT}$`), 15906 extract: NAME_FIRST 15907 }, 15908 { 15909 value: "not_equals", 15910 label: en("Doesn't match"), 15911 category: "string", 15912 hasValue: true, 15913 serialize: (n2, v3) => `${n2}!=${v3}`, 15914 matchRegex: new RegExp(`^${NAME_PAT}\\s*!=\\s*${VAL_PAT}$`), 15915 extract: NAME_FIRST 15916 }, 15917 { 15918 value: "contains", 15919 label: en("Contains"), 15920 category: "string", 15921 hasValue: true, 15922 serialize: (n2, v3) => `Locate(${v3}, ${n2})`, 15923 matchRegex: new RegExp(`^Locate\\(\\s*${VAL_PAT}\\s*,\\s*${NAME_PAT}\\s*\\)$`), 15924 extract: VALUE_FIRST 15925 }, 15926 { 15927 value: "not_contains", 15928 label: en("Doesn't contain"), 15929 category: "string", 15930 hasValue: true, 15931 serialize: (n2, v3) => `Locate(${v3}, ${n2})=0`, 15932 matchRegex: new RegExp(`^Locate\\(\\s*${VAL_PAT}\\s*,\\s*${NAME_PAT}\\s*\\)=0$`), 15933 extract: VALUE_FIRST 15934 }, 15935 { 15936 value: "starts", 15937 label: en("Starts with"), 15938 category: "string", 15939 hasValue: true, 15940 serialize: (n2, v3) => `Locate(${v3}, ${n2})=1`, 15941 matchRegex: new RegExp(`^Locate\\(\\s*${VAL_PAT}\\s*,\\s*${NAME_PAT}\\s*\\)=1$`), 15942 extract: VALUE_FIRST 15943 }, 15944 { 15945 value: "ends", 15946 label: en("Ends with"), 15947 category: "string", 15948 hasValue: true, 15949 serialize: (n2, v3) => `Right(${n2}, Char_length(${v3}))=${v3}`, 15950 matchRegex: new RegExp(`^Right\\(\\s*${NAME_PAT}\\s*,\\s*Char_length\\(\\s*${VAL_PAT}\\s*\\)\\s*\\)=${VAL_PAT}$`), 15951 extract: NAME_FIRST 15952 }, 15953 { 15954 value: "regexp", 15955 label: en("Matches regexp"), 15956 category: "string", 15957 hasValue: true, 15958 serialize: (n2, v3) => `${n2} REGEXP ${v3}`, 15959 matchRegex: new RegExp(`^${NAME_PAT}\\s+REGEXP\\s+${VAL_PAT}$`, "i"), 15960 extract: NAME_FIRST 15961 }, 15962 { 15963 value: "empty", 15964 label: en("Is empty"), 15965 category: "string", 15966 hasValue: false, 15967 serialize: (n2) => `(${n2} IS NULL OR ${n2}='')`, 15968 // The `\s*` around the comparison is not cosmetic: actions in the field store this 15969 // fragment spaced out (`first_name != ''`), and without it such a rule fails to parse 15970 // and the whole condition drops to the raw-SQL fallback in every builder that reads
15970it. 15971 matchRegex: new RegExp(`^\\(\\s*${NAME_PAT}\\s+IS\\s+NULL\\s+OR\\s+\\w+(?:->\\w+)?\\s*=\\s*''\\s*\\)$`, "i"), 15972 extract: NAME_ONLY 15973 }, 15974 { 15975 value: "not_empty", 15976 label: en("Is not empty"), 15977 category: "string", 15978 hasValue: false, 15979 serialize: (n2) => `(${n2} IS NOT NULL AND ${n2}!='')`, 15980 matchRegex: new RegExp(`^\\(\\s*${NAME_PAT}\\s+IS\\s+NOT\\s+NULL\\s+AND\\s+\\w+(?:->\\w+)?\\s*!=\\s*''\\s*\\)$`, "i"), 15981 extract: NAME_ONLY 15982 }, 15983 // Numeric operators 15984 { 15985 value: "gt", 15986 label: en("> greater than"), 15987 category: "numeric", 15988 hasValue: true, 15989 serialize: (n2, v3) => `${n2}>${v3}`, 15990 matchRegex: new RegExp(`^${NAME_PAT}\\s*>\\s*${VAL_PAT}$`), 15991 extract: NAME_FIRST 15992 }, 15993 { 15994 value: "gte", 15995 label: en("\u2265 greater than or equal"), 15996 category: "numeric", 15997 hasValue: true, 15998 serialize: (n2, v3) => `${n2}>=${v3}`, 15999 matchRegex: new RegExp(`^${NAME_PAT}\\s*>=\\s*${VAL_PAT}$`), 16000 extract: NAME_FIRST 16001 }, 16002 { 16003 value: "lt", 16004 label: en("< less than"), 16005 category: "numeric", 16006 hasValue: true, 16007 serialize: (n2, v3) => `${n2}<${v3}`, 16008 matchRegex: new RegExp(`^${NAME_PAT}\\s*<\\s*${VAL_PAT}$`), 16009 extract: NAME_FIRST 16010 }, 16011 { 16012 value: "lte", 16013 label: en("\u2264 less than or equal"), 16014 category: "numeric", 16015 hasValue: true, 16016 serialize: (n2, v3) => `${n2}<=${v3}`, 16017 matchRegex: new RegExp(`^${NAME_PAT}\\s*<=\\s*${VAL_PAT}$`), 16018 extract: NAME_FIRST 16019 }, 16020 // Date operators 16021 { 16022 value: "days_since_gt", 16023 label: en("days ago >"), 16024 category: "date", 16025 hasValue: true, 16026 serialize: (n2, v3) => `(Unix_timestamp()-${n2})/(24*60*60) > ${v3}`, 16027 matchRegex: new RegExp(`^\\(Unix_timestamp\\(\\)-${NAME_PAT}\\)/\\(24\\*60\\*60\\)\\s*>\\s*${VAL_PAT}$`), 16028 extract: NAME_FIRST 16029 }, 16030 { 16031 value: "days_since_gte", 16032 label: en("days ago \u2265"), 16033 category: "date", 16034 hasValue: true, 16035 serialize: (n2, v3) => `(Unix_timestamp()-${n2})/(24*60*60) >= ${v3}`, 16036 matchRegex: new RegExp(`^\\(Unix_timestamp\\(\\)-${NAME_PAT}\\)/\\(24\\*60\\*60\\)\\s*>=\\s*${VAL_PAT}$`), 16037 extract: NAME_FIRST 16038 }, 16039 { 16040 value: "days_since_lt", 16041 label: en("days ago <"), 16042 category: "date", 16043 hasValue: true, 16044 serialize: (n2, v3) => `(Unix_timestamp()-${n2})/(24*60*60) < ${v3}`, 16045 matchRegex: new RegExp(`^\\(Unix_timestamp\\(\\)-${NAME_PAT}\\)/\\(24\\*60\\*60\\)\\s*<\\s*${VAL_PAT}$`), 16046 extract: NAME_FIRST 16047 }, 16048 { 16049 value: "days_since_lte", 16050 label: en("days ago \u2264"), 16051 category: "date", 16052 hasValue: true, 16053 serialize: (n2, v3) => `(Unix_timestamp()-${n2})/(24*60*60) <= ${v3}`, 16054 matchRegex: new RegExp(`^\\(Unix_timestamp\\(\\)-${NAME_PAT}\\)/\\(24\\*60\\*60\\)\\s*<=\\s*${VAL_PAT}$`), 16055 extract: NAME_FIRST 16056 }, 16057 { 16058 value: "days_till_gt", 16059 label: en("days till >"), 16060 category: "date", 16061 hasValue: true, 16062 serialize: (n2, v3) => `(${n2}-Unix_timestamp())/(24*60*60) > ${v3}`, 16063 matchRegex: new RegExp(`^\\(${NAME_PAT}-Unix_timestamp\\(\\)\\)/\\(24\\*60\\*60\\)\\s*>\\s*${VAL_PAT}$`), 16064 extract: NAME_FIRST 16065 }, 16066 { 16067 value: "days_till_gte", 16068 label: en("days till \u2265"), 16069 category: "date", 16070 hasValue: true, 16071 serialize: (n2, v3) => `(${n2}-Unix_timestamp())/(24*60*60) >= ${v3}`, 16072 matchRegex: new RegExp(`^\\(${NAME_PAT}-Unix_timestamp\\(\\)\\)/\\(24\\*60\\*60\\)\\s*>=\\s*${VAL_PAT}$`), 16073 extract: NAME_FIRST 16074 }, 16075 { 16076 value: "days_till_lt", 16077 label: en("days till <"), 16078 category: "date", 16079 hasValue: true, 16080 serialize: (n2, v3) => `(${n2}-Unix_timestamp())/(24*60*60) < ${v3}`, 16081 matchRegex: new RegExp(`^\\(${NAME_PAT}-Unix_timestamp\\(\\)\\)/\\(24\\*60\\*60\\)\\s*<\\s*${VAL_PAT}$`), 16082 extract: NAME_FIRST 16083 }, 16084 { 16085 value: "days_till_lte", 16086 label: en("days till \u2264"), 16087 category: "date", 16088 hasValue: true, 16089 serialize: (n2, v3) => `(${n2}-Unix_timestamp())/(24*60*60) <= ${v3}`, 16090 matchRegex: new RegExp(`^\\(${NAME_PAT}-Unix_timestamp\\(\\)\\)/\\(24\\*60\\*60\\)\\s*<=\\s*${VAL_PAT}$`), 16091 extract: NAME_FIRST 16092 }, 16093 // Unix-timestamp column vs. now minus an offset in seconds, e.g. 16094 // `interaction_time > unix_timestamp()-1814400` (interacted < 21 days ago). 16095 // The value is raw seconds embedded in the arithmetic, so it is emitted bare 16096 // (rawValue) and matched as digits — mirrors the catalog's own interaction- 16097 // time window (recomCatalog.ts) and the legacy `unix_timestamp()-N` fragments. 16098 // Value names follow the SQL comparator; labels read as elapsed "… ago". 16099 { 16100 value: "unixts_gt", 16101 label: en("less than (sec) ago"),
16102 category: "date", 16103 hasValue: true, 16104 rawValue: true, 16105 serialize: (n2, v3) => `${n2} > unix_timestamp()-${v3}`, 16106 matchRegex: new RegExp(`^${NAME_PAT}\\s*>\\s*unix_timestamp\\(\\)\\s*-\\s*(-?\\d+)$`, "i"), 16107 extract: (m2) => ({ 16108 name: m2[1], 16109 value: m2[2], 16110 valueField: false 16111 }) 16112 }, 16113 { 16114 value: "unixts_gte", 16115 label: en("at most (sec) ago"), 16116 category: "date", 16117 hasValue: true, 16118 rawValue: true, 16119 serialize: (n2, v3) => `${n2} >= unix_timestamp()-${v3}`, 16120 matchRegex: new RegExp(`^${NAME_PAT}\\s*>=\\s*unix_timestamp\\(\\)\\s*-\\s*(-?\\d+)$`, "i"), 16121 extract: (m2) => ({ 16122 name: m2[1], 16123 value: m2[2], 16124 valueField: false 16125 }) 16126 }, 16127 { 16128 value: "unixts_lt", 16129 label: en("more than (sec) ago"), 16130 category: "date", 16131 hasValue: true, 16132 rawValue: true, 16133 serialize: (n2, v3) => `${n2} < unix_timestamp()-${v3}`, 16134 matchRegex: new RegExp(`^${NAME_PAT}\\s*<\\s*unix_timestamp\\(\\)\\s*-\\s*(-?\\d+)$`, "i"), 16135 extract: (m2) => ({ 16136 name: m2[1], 16137 value: m2[2], 16138 valueField: false 16139 }) 16140 }, 16141 { 16142 value: "unixts_lte", 16143 label: en("at least (sec) ago"), 16144 category: "date", 16145 hasValue: true, 16146 rawValue: true, 16147 serialize: (n2, v3) => `${n2} <= unix_timestamp()-${v3}`, 16148 matchRegex: new RegExp(`^${NAME_PAT}\\s*<=\\s*unix_timestamp\\(\\)\\s*-\\s*(-?\\d+)$`, "i"), 16149 extract: (m2) => ({ 16150 name: m2[1], 16151 value: m2[2], 16152 valueField: false 16153 }) 16154 }, 16155 // Distance (point-typed columns) 16156 { 16157 value: "distance_gt", 16158 label: en("distance from user >"), 16159 category: "distance", 16160 hasValue: true, 16161 serialize: (n2, v3) => `distance_to_point(${n2}) > ${v3}`, 16162 matchRegex: new RegExp(`^distance_to_point\\(${NAME_PAT}\\)\\s*>\\s*${VAL_PAT}$`), 16163 extract: NAME_FIRST 16164 }, 16165 { 16166 value: "distance_gte", 16167 label: en("distance from user \u2265"), 16168 category: "distance", 16169 hasValue: true, 16170 serialize: (n2, v3) => `distance_to_point(${n2}) >= ${v3}`, 16171 matchRegex: new RegExp(`^distance_to_point\\(${NAME_PAT}\\)\\s*>=\\s*${VAL_PAT}$`), 16172 extract: NAME_FIRST 16173 }, 16174 { 16175 value: "distance_lt", 16176 label: en("distance from user <"), 16177 category: "distance", 16178 hasValue: true, 16179 serialize: (n2, v3) => `distance_to_point(${n2}) < ${v3}`, 16180 matchRegex: new RegExp(`^distance_to_point\\(${NAME_PAT}\\)\\s*<\\s*${VAL_PAT}$`), 16181 extract: NAME_FIRST 16182 }, 16183 { 16184 value: "distance_lte", 16185 label: en("distance from user \u2264"), 16186 category: "distance", 16187 hasValue: true, 16188 serialize: (n2, v3) => `distance_to_point(${n2}) <= ${v3}`, 16189 matchRegex: new RegExp(`^distance_to_point\\(${NAME_PAT}\\)\\s*<=\\s*${VAL_PAT}$`), 16190 extract: NAME_FIRST 16191 }, 16192 // Set membership. The legacy `$input_recommendations` widget wrote these from 16193 // its "Based on" controls — `p->interaction_status in ('extra')` for a 16194 // recommendation seeded from the cart, `p->for_period in ('day','week')` for a 16195 // popularity window (input_recommendations.php:380) — so EVERY widget built that 16196 // way carried a fragment with no operator here to match, and the whole rule 16197 // builder dropped to raw SQL with "…an expression the rule builder doesn't 16198 // recognize yet". Nothing else in this table matches an `in (`, and [[parseOne]] 16199 // runs after [[parseLocation]] and [[parseExpr]], so these two entries can only 16200 // ever claim fragments that used to parse as nothing at all: a geo 16201 // `country_code in (…)` and a `weekday in (0,1,2)` are both taken before the 16202 // operator table is consulted. 16203 // 16204 // `rawValue` because the captured list is re-emitted verbatim — see 16205 // [[normalizeInList]] for why anything cleverer rewrites stored SQL. 16206 { 16207 value: "in", 16208 label: en("Is one of"), 16209 category: "string", 16210 hasValue: true, 16211 rawValue: true, 16212 serialize: (n2, v3) => serializeIn(n2, v3, false), 16213 matchRegex: new RegExp(`^${NAME_PAT}\\s+in\\s*\\(\\s*${LIST_PAT}\\s*\\)$`, "i"), 16214 extract: NAME_AND_LIST 16215 }, 16216 { 16217 value: "not_in", 16218 label: en("Is not one of"), 16219 category: "string", 16220 hasValue: true, 16221 rawValue: true, 16222 serialize: (n2, v3) => serializeIn(n2, v3, true), 16223 matchRegex: new RegExp(`^${NAME_PAT}\\s+not\\s+in\\s*\\(\\s*${LIST_PAT}\\s*\\)$`, "i"), 16224 extract: NAME_AND_LIST 16225 } 16226]; 16227var OPS_BY_VALUE = new Map(OPERATIONS.map((o3) => [ 16228 o3.value, 16229 o3 16230])); 16231function getOperator(value) { 16232 return OPS_BY_VALUE.get(value); 16233} 16234function splitTopLevelByOp(sql, op) { 16235 const sep = " " + op + " "; 16236 const parts = []; 16237 let depth = 0; 16238 let inQuote = false;
16239 let last = 0; 16240 let i4 = 0; 16241 let pendingBetween = 0; 16242 while (i4 < sql.length) { 16243 const ch = sql[i4]; 16244 if (inQuote) { 16245 if (ch === "'") { 16246 if (sql[i4 + 1] === "'") { 16247 i4 += 2; 16248 continue; 16249 } 16250 inQuote = false; 16251 } 16252 i4++; 16253 continue; 16254 } 16255 if (ch === "'") { 16256 inQuote = true; 16257 i4++; 16258 continue; 16259 } 16260 if (ch === "(") { 16261 depth++; 16262 i4++; 16263 continue; 16264 } 16265 if (ch === ")") { 16266 depth--; 16267 i4++; 16268 continue; 16269 } 16270 if (op === "and" && depth === 0 && sql.slice(i4, i4 + 9).toLowerCase() === " between ") { 16271 pendingBetween++; 16272 i4 += 9; 16273 continue; 16274 } 16275 if (depth === 0 && sql.slice(i4, i4 + sep.length).toLowerCase() === sep) { 16276 if (op === "and" && pendingBetween > 0) { 16277 pendingBetween--; 16278 i4 += sep.length; 16279 continue; 16280 } 16281 parts.push(sql.slice(last, i4).trim()); 16282 i4 += sep.length; 16283 last = i4; 16284 continue; 16285 } 16286 i4++; 16287 } 16288 if (inQuote || depth !== 0) return null; 16289 parts.push(sql.slice(last).trim()); 16290 return parts; 16291} 16292function isWholeWrap(s3) { 16293 if (s3[0] !== "(" || s3[s3.length - 1] !== ")") return false; 16294 let depth = 0; 16295 let inQuote = false; 16296 for (let i4 = 0; i4 < s3.length; i4++) { 16297 const ch = s3[i4]; 16298 if (inQuote) { 16299 if (ch === "'") { 16300 if (s3[i4 + 1] === "'") { 16301 i4++; 16302 continue; 16303 } 16304 inQuote = false; 16305 } 16306 continue; 16307 } 16308 if (ch === "'") { 16309 inQuote = true; 16310 continue; 16311 } 16312 if (ch === "(") { 16313 depth++; 16314 } else if (ch === ")") { 16315 depth--; 16316 if (depth === 0 && i4 !== s3.length - 1) return false; 16317 } 16318 } 16319 return depth === 0; 16320} 16321function parseOne(fragment) { 16322 for (const op of OPERATIONS) { 16323 const m2 = fragment.match(op.matchRegex); 16324 if (m2) { 16325 const { name, value, valueField } = op.extract(m2); 16326 return { 16327 kind: "field", 16328 field: name, 16329 operator: op.value, 16330 value, 16331 valueField 16332 }; 16333 } 16334 } 16335 return null; 16336} 16337var COUNTRY_LIST_RE = /^country_code\s+(?:(not)\s+)?in\s*\(\s*((?:'[A-Z]{2}'\s*,\s*)*'[A-Z]{2}')\s*\)$/i; 16338var REGION_LIST_RE = /^\(\s*country_code\s*,\s*region_code\s*\)\s+(?:(not)\s+)?in\s*\(\s*((?:\(\s*'[A-Z]{2}'\s*,\s*'(?:[^']|'')+'\s*\)\s*,\s*)*\(\s*'[A-Z]{2}'\s*,\s*'(?:[^']|'')+'\s*\))\s*\)$/i; 16339var CITY_LIST_RE = /^\(\s*country_code\s*,\s*region_code\s*,\s*city\s*\)\s+(?:(not)\s+)?in\s*\(\s*((?:\(\s*'[A-Z]{2}'\s*,\s*'(?:[^']|'')+'\s*,\s*'(?:[^']|'')*'\s*\)\s*,\s*)*\(\s*'[A-Z]{2}'\s*,\s*'(?:[^']|'')+'\s*,\s*'(?:[^']|'')*'\s*\))\s*\)$/i; 16340function parseSingleGeo(s3) { 16341 const c3 = s3.match(COUNTRY_LIST_RE); 16342 if (c3) { 16343 const items = c3[2].split(/\s*,\s*/).map((v3) => unquote2(v3)); 16344 return { 16345 sub: "countries", 16346 not: !!c3[1], 16347 items 16348 }; 16349 } 16350 const r3 = s3.match(REGION_LIST_RE); 16351 if (r3) { 16352 const inner = r3[2]; 16353 const tuples = []; 16354 const tupleRe = /\(\s*'([A-Z]{2})'\s*,\s*'((?:[^']|'')+)'\s*\)/gi; 16355 let m2; 16356 while (m2 = tupleRe.exec(inner)) tuples.push(`${m2[1]}-${m2[2]}`); 16357 return { 16358 sub: "regions", 16359 not: !!r3[1], 16360 items: tuples 16361 }; 16362 } 16363 const ci = s3.match(CITY_LIST_RE); 16364 if (ci) { 16365 const inner = ci[2]; 16366 const tuples = []; 16367 const tupleRe = /\(\s*'([A-Z]{2})'\s*,\s*'((?:[^']|'')+)'\s*,\s*'((?:[^']|'')*)'\s*\)/gi; 16368 let m2; 16369 while (m2 = tupleRe.exec(inner)) tuples.push(`${m2[1]}-${m2[2]}-${unquote2("'" + m2[3] + "'")}`); 16370 return { 16371 sub: "cities", 16372 not: !!ci[1], 16373 items: tuples 16374 }; 16375 } 16376 return null; 16377} 16378function emptyLocation(positive) { 16379 return { 16380 kind: "location", 16381 positive, 16382 countries: [], 16383 regions: [], 16384 cities: [], 16385 t_countries: [], 16386 t_regions: [], 16387 t_cities: [] 16388 }; 16389} 16390function addGeoFragment(loc, f4) { 16391 return { 16392 ...loc, 16393 [f4.sub]: [ 16394 ...loc[f4.sub], 16395 ...f4.items 16396 ], 16397 ["t_" + f4.sub]: [ 16398 ...loc["t_" + f4.sub], 16399 ...f4.items 16400 ] 16401 }; 16402} 16403function parseLocation(sql) { 16404 const trimmed = sql.trim(); 16405 if (!/\bcountry_code\b/i.test(trimmed)) return null; 16406 if (trimmed.startsWith("(") && trimmed.endsWith(")")) { 16407 const inner = trimmed.slice(1, -1).trim(); 16408 const parts = splitTopLevelByOp(inner, "or"); 16409 if (parts && parts.length > 1) { 16410 let loc = emptyLocation(true); 16411 for (const p3 of parts) { 16412 const f4 = parseSingleGeo(p3); 16413 if (!f4 || f4.not) return null; 16414 loc = addGeoFragment(loc, f4); 16415 } 16416 return loc; 16417 } 16418 } 16419 const andParts = splitTopLevelByOp(trimmed, "and"); 16420 if (andParts && andParts.length > 1) { 16421 let loc = emptyLocation(false); 16422 for (const p3 of andParts) { 16423 const f4 = parseSingleGeo(p3); 16424 if (!f4 || !f4.not) return null; 16425 loc = addGeoFragment(loc, f4); 16426 } 16427 return loc; 16428 } 16429 const single = parseSingleGeo(trimmed); 16430 if (single) { 16431 return addGeoFragment(emptyLocation(!single.not), single); 16432 } 16433 return null; 16434} 16435function sqlQuote(v3) { 16436 return "'" + v3.replace(/'/g, "''") + "'"; 16437} 16438function serializeLocation(loc) { 16439 const parts = []; 16440 const opIn = loc.positive ? "in" : "not in"; 16441 if (loc.countries.length > 0) { 16442 parts.push(`country_code ${opIn} (${loc.countries.map(sqlQuote).join(", ")})`); 16443 } 16444 if (loc.regions.length > 0) { 16445 const tuples = loc.regions.map((v3) => { 16446 const [cc, rc] = v3.split("-"); 16447 return `(${sqlQuote(cc)}, ${sqlQuote(rc)})`; 16448 }); 16449 parts.push(`(country_code, region_code) ${opIn} (${tuples.join(", ")})`); 16450 } 16451 if (loc.cities.length > 0) { 16452 const tuples = loc.cities.map((v3) => { 16453 const cc = v3.slice(0, 2); 16454 const rest = v3.slice(3); 16455 const dash = rest.indexOf("-"); 16456 const rc = dash >= 0 ? rest.slice(0, dash) : rest; 16457 const cn = dash >= 0 ? rest.slice(dash + 1) : ""; 16458 return `(${sqlQuote(cc)}, ${sqlQuote(rc)}, ${sqlQuote(cn)})`; 16459 }); 16460 parts.push(`(country_code, region_code, city) ${opIn} (${tuples.join(", ")})`); 16461 } 16462 if (parts.length === 0) return ""; 16463 if (loc.positive) { 16464 return parts.length === 1 ? parts[0] : "(" + parts.join(" or ") + ")"; 16465 } 16466 return parts.join(" and "); 16467} 16468function serializeExpr(r3) { 16469 switch (r3.sub) { 16470 case "weekdays": 16471 return r3.values.length ? `weekday in (${r3.values.join(",")})` : ""; 16472 case "monthdays": 16473 return r3.values.length ? `day in (${r3.values.map((v3) => v3 > 0 ? String(v3) : `last_month_day-${-v3}`).join(",")})` : ""; 16474 case "dates": 16475 return r3.from && r3.to ? `ss between ${r3.from} and ${r3.to}` : ""; 16476 case "time": 16477 return r3.from && r3.to ? `daytime between ${r3.from} and ${r3.to}` : ""; 16478 } 16479} 16480function parseExpr(fragment) { 16481 let m2 = fragment.match(/^weekday\s+in\s*\(\s*([\d\s,]+?)\s*\)$/i); 16482 if (m2) { 16483 const values = m2[1].split(",").map((s3) => parseInt(s3.trim(), 10)).filter((n2) => Number.isFinite(n2)); 16484 return { 16485 kind: "expr", 16486 sub: "weekdays", 16487 values, 16488 from: "", 16489 to: "" 16490 }; 16491 } 16492 m2 = fragment.match(/^day\s+in\s*\(\s*(.+?)\s*\)$/i); 16493 if (m2) { 16494 const values = []; 16495 for (const raw of m2[1].split(",")) { 16496 const it = raw.trim(); 16497 if (/^\d+$/.test(it)) values.push(parseInt(it, 10)); 16498 else { 16499 const mm = it.match(/^last_month_day(?:\s*-\s*(\d+))?$/i); 16500 if (!mm) return null; 16501 values.push(-(mm[1] ? parseInt(mm[1], 10) : 0)); 16502 } 16503 } 16504 return { 16505 kind: "expr", 16506 sub: "monthdays", 16507 values, 16508 from: "", 16509 to: "" 16510 }; 16511 } 16512 m2 = fragment.match(/^ss\s+between\s+(\d+)\s+and\s+(\d+)$/i); 16513 if (m2) return { 16514 kind: "expr", 16515 sub: "dates", 16516 values: [], 16517 from: m2[1], 16518 to: m2[2] 16519 }; 16520 m2 = fragment.match(/^daytime\s+between\s+(\d+)\s+and\s+(\d+)$/i); 16521 if (m2) return { 16522 kind: "expr", 16523 sub: "time", 16524 values: [], 16525 from: m2[1], 16526 to: m2[2] 16527 }; 16528 return null; 16529} 16530function parseRuleFragment(fragment) { 16531 const loc = parseLocation(fragment); 16532 if (loc) return loc; 16533 const expr = parseExpr(fragment); 16534 if (expr) return expr; 16535 return parseOne(fragment); 16536} 16537function serializeRule(r3) { 16538 if (r3.kind === "location") return serializeLocation(r3); 16539 if (r3.kind === "expr") return serializeExpr(r3); 16540 const op = OPS_BY_VALUE.get(r3.operator); 16541 if (!op) return "";
16542 const valueToken = r3.valueField ? r3.value : op.rawValue ? r3.value : quote(r3.value); 16543 return op.serialize(r3.field, valueToken); 16544} 16545var PRECEDENCE = [ 16546 "or", 16547 "xor", 16548 "and" 16549]; 16550var RANK = { 16551 or: 0, 16552 xor: 1, 16553 and: 2 16554}; 16555function serializeNode(node, parentCombinator) { 16556 if (node.type === "leaf") { 16557 const sql = serializeRule(node.rule); 16558 if (!sql) return ""; 16559 return node.not ? `not (${sql})` : sql; 16560 } 16561 const parts = []; 16562 for (const child of node.children) { 16563 const s3 = serializeNode(child, node.combinator); 16564 if (s3) parts.push(s3); 16565 } 16566 if (parts.length === 0) return ""; 16567 const body = parts.length === 1 ? parts[0] : parts.join(" " + node.combinator + " "); 16568 if (node.not) return `not (${body})`; 16569 if (parts.length > 1 && parentCombinator !== null && RANK[node.combinator] < RANK[parentCombinator]) { 16570 return `(${body})`; 16571 } 16572 return body; 16573} 16574function serialize2(root) { 16575 return serializeNode(root, null); 16576} 16577function negate(node) { 16578 return { 16579 ...node, 16580 not: !node.not 16581 }; 16582} 16583function parseNode(sql) { 16584 const trimmed = sql.trim(); 16585 if (!trimmed) return null; 16586 const notMatch = /^not\b\s*/i.exec(trimmed); 16587 if (notMatch) { 16588 const rest = trimmed.slice(notMatch[0].length).trim(); 16589 if (isWholeWrap(rest)) { 16590 const inner = parseNode(rest.slice(1, -1).trim()); 16591 if (inner) return negate(inner); 16592 } else { 16593 const r3 = parseRuleFragment(rest); 16594 if (r3) return { 16595 type: "leaf", 16596 not: true, 16597 rule: r3 16598 }; 16599 } 16600 } 16601 const whole = parseRuleFragment(trimmed); 16602 if (whole) return { 16603 type: "leaf", 16604 not: false, 16605 rule: whole 16606 }; 16607 for (const op of PRECEDENCE) { 16608 const parts = splitTopLevelByOp(trimmed, op); 16609 if (parts === null) return null; 16610 if (parts.length > 1) { 16611 const children = []; 16612 for (const p3 of parts) { 16613 const child = parseNode(p3); 16614 if (!child) return null; 16615 children.push(child); 16616 } 16617 return { 16618 type: "group", 16619 not: false, 16620 combinator: op, 16621 children 16622 }; 16623 } 16624 } 16625 if (isWholeWrap(trimmed)) return parseNode(trimmed.slice(1, -1).trim()); 16626 return null; 16627} 16628function parse2(sql) { 16629 if (!sql.trim()) return emptyGroup("and"); 16630 const node = parseNode(sql); 16631 if (!node) return null; 16632 return node.type === "group" ? node : { 16633 type: "group", 16634 not: false, 16635 combinator: "and", 16636 children: [ 16637 node 16638 ] 16639 }; 16640} 16641
16642// src/components/live/RuleBuilder.tsx 16643var FIELD_CATALOG = [ 16644 { 16645 value: "url_host_port", 16646 label: "URL domain", 16647 group: "Current page", 16648 kind: "text" 16649 }, 16650 { 16651 value: "url_path", 16652 label: "URL path", 16653 group: "Current page", 16654 kind: "text" 16655 }, 16656 { 16657 value: "ur", 16658 label: "Whole URL", 16659 group: "Current page", 16660 kind: "text" 16661 }, 16662 { 16663 value: "country", 16664 label: "Country", 16665 group: "Visitor", 16666 kind: "text" 16667 }, 16668 { 16669 value: "city", 16670 label: "City", 16671 group: "Visitor", 16672 kind: "text" 16673 }, 16674 { 16675 value: "lang_0", 16676 label: "Primary browser language", 16677 group: "Visitor", 16678 kind: "language" 16679 }, 16680 { 16681 value: "known_device_type", 16682 label: "Device type", 16683 group: "Visitor", 16684 kind: "device" 16685 }, 16686 { 16687 value: "is_returning_user", 16688 label: "Visit type", 16689 group: "Visitor", 16690 kind: "visit" 16691 }, 16692 { 16693 value: "ip", 16694 label: "IP", 16695 group: "Visitor", 16696 kind: "text" 16697 }, 16698 { 16699 value: "landing_url", 16700 label: "Landing page - Full URL", 16701 group: "Landing page", 16702 kind: "text" 16703 }, 16704 { 16705 value: "landing_host_port", 16706 label: "Landing page - Host name", 16707 group: "Landing page", 16708 kind: "text" 16709 }, 16710 { 16711 value: "landing_path", 16712 label: "Landing page - Path", 16713 group: "Landing page", 16714 kind: "text" 16715 }, 16716 { 16717 value: "part_of_day()", 16718 label: "Part of day", 16719 group: "Date & time", 16720 kind: "partofday" 16721 } 16722]; 16723var DEVICE_OPTIONS = [ 16724 [ 16725 "regular", 16726 "Desktop" 16727 ], 16728 [ 16729 "tablet", 16730 "Tablet" 16731 ], 16732 [ 16733 "phone", 16734 "Phone" 16735 ], 16736 [ 16737 "tablet-sdk", 16738 "Tablet (SDK)" 16739 ], 16740 [ 16741 "phone-sdk", 16742 "Phone (SDK)" 16743 ] 16744]; 16745var VISIT_OPTIONS = [ 16746 [ 16747 "0", 16748 "New user" 16749 ], 16750 [ 16751 "1", 16752 "Returning user" 16753 ] 16754]; 16755var PART_OF_DAY_OPTIONS = [ 16756 [ 16757 "morning", 16758 "Morning" 16759 ], 16760 [ 16761 "afternoon", 16762 "Afternoon" 16763 ], 16764 [ 16765 "evening", 16766 "Evening" 16767 ], 16768 [ 16769 "night", 16770 "Night" 16771 ] 16772]; 16773var LANGUAGE_OPTIONS = [ 16774 [ 16775 "en", 16776 "English" 16777 ], 16778 [ 16779 "es", 16780 "Spanish" 16781 ], 16782 [ 16783 "fr", 16784 "French" 16785 ], 16786 [ 16787 "de", 16788 "German" 16789 ], 16790 [ 16791 "it", 16792 "Italian" 16793 ], 16794 [ 16795 "pt", 16796 "Portuguese" 16797 ], 16798 [ 16799 "nl", 16800 "Dutch" 16801 ], 16802 [ 16803 "ru", 16804 "Russian" 16805 ], 16806 [ 16807 "pl", 16808 "Polish" 16809 ], 16810 [ 16811 "tr", 16812 "Turkish" 16813 ], 16814 [ 16815 "ar", 16816 "Arabic" 16817 ], 16818 [ 16819 "he", 16820 "Hebrew" 16821 ], 16822 [ 16823 "zh", 16824 "Chinese" 16825 ], 16826 [ 16827 "ja", 16828 "Japanese" 16829 ], 16830 [ 16831 "ko", 16832 "Korean" 16833 ] 16834]; 16835var EQUALITY_ONLY = [ 16836 "equals", 16837 "not_equals" 16838]; 16839var TEXT_OPERATORS = [ 16840 "equals", 16841 "not_equals", 16842 "contains", 16843 "not_contains", 16844 "starts", 16845 "ends", 16846 "regexp", 16847 "empty", 16848 "not_empty" 16849]; 16850function optionsFor(kind) { 16851 switch (kind) { 16852 case "language": 16853 return LANGUAGE_OPTIONS; 16854 case "device": 16855 return DEVICE_OPTIONS; 16856 case "visit": 16857 return VISIT_OPTIONS; 16858 case "partofday": 16859 return PART_OF_DAY_OPTIONS; 16860 } 16861 return null; 16862} 16863function toFlat(sql) { 16864 if (!sql.trim()) { 16865 return { 16866 combinator: "and", 16867 rules: [] 16868 }; 16869 } 16870 const root = parse2(sql); 16871 if (!root || root.not) { 16872 return void 0; 16873 } 16874 const rules = []; 16875 for (const child of root.children) { 16876 if (child.type != "leaf" || child.not || child.rule.kind != "field") { 16877 return void 0; 16878 } 16879 rules.push(child.rule); 16880 } 16881 return { 16882 combinator: root.combinator, 16883 rules 16884 }; 16885} 16886function fromFlat(flat) { 16887 const root = { 16888 type: "group", 16889 not: false, 16890 combinator: flat.combinator, 16891 children: flat.rules.map((rule) => ({ 16892 type: "leaf", 16893 not: false, 16894 rule 16895 })) 16896 }; 16897 return serialize2(root); 16898} 16899function ModeToggle(props) { 16900 return /* @__PURE__ */ u2("div", { 16901 class: "le-rb-modes", 16902 children: [
16903 /* @__PURE__ */ u2("button", { 16904 type: "button", 16905 class: props.isRaw ? void 0 : "le-on", 16906 disabled: !props.canRules, 16907 title: props.canRules ? "Edit this condition as rules" : "This condition has a shape the rule editor can't draw, so it stays as SQL", 16908 onClick: () => props.onMode(false), 16909 children: "Edit as rules" 16910 }), 16911 /* @__PURE__ */ u2("button", { 16912 type: "button", 16913 class: props.isRaw ? "le-on" : void 0, 16914 title: "Edit this condition as SQL", 16915 onClick: () => props.onMode(true), 16916 children: "Edit as SQL" 16917 }) 16918 ] 16919 }); 16920} 16921function RuleBuilder(props) { 16922 const flat = toFlat(props.value); 16923 const [isRaw, setIsRaw] = p2(flat == void 0); 16924 const [isSqlOffered] = p2(flat == void 0); 16925 const fields = q2(() => FIELD_CATALOG.concat(props.userColumns.map((c3) => ({ 16926 value: c3.value, 16927 label: c3.label, 16928 group: "Visitor data", 16929 kind: "text" 16930 }))), [ 16931 props.userColumns 16932 ]); 16933 if (isRaw || !flat) { 16934 return /* @__PURE__ */ u2("div", { 16935 class: "le-rb", 16936 children: [
16937 /* @__PURE__ */ u2("textarea", { 16938 class: "le-area le-mono", 16939 rows: 2, 16940 value: props.value, 16941 placeholder: "country='FR' and city='Paris'", 16942 onInput: (ev) => props.onChange(ev.target.value) 16943 }), 16944 /* @__PURE__ */ u2("div", { 16945 class: "le-rb-foot", 16946 children: [ 16947 /* @__PURE__ */ u2("span", { 16948 class: "le-rb-hint", 16949 children: "Condition as SQL, the way the panel's rule builder writes it." 16950 }), 16951 /* @__PURE__ */ u2(ModeToggle, { 16952 isRaw: true, 16953 canRules: toFlat(props.value) != void 0, 16954 onMode: setIsRaw 16955 }) 16956 ] 16957 }) 16958 ] 16959 }); 16960 } 16961 function update(next) { 16962 props.onChange(fromFlat(next)); 16963 } 16964 function setRule(i4, patch) { 16965 update({ 16966 ...flat, 16967 rules: flat.rules.map((r3, k3) => k3 != i4 ? r3 : { 16968 ...r3, 16969 ...patch 16970 }) 16971 }); 16972 } 16973 return /* @__PURE__ */ u2("div", { 16974 class: "le-rb", 16975 children: [ 16976 flat.rules.map((rule, i4) => { 16977 const field = fields.find((f4) => f4.value == rule.field); 16978 const choices = field ? optionsFor(field.kind) : null; 16979 const operators = choices ? EQUALITY_ONLY : TEXT_OPERATORS; 16980 const hasValue = getOperator(rule.operator)?.hasValue ?? true; 16981 return /* @__PURE__ */ u2("div", { 16982 class: "le-rb-row", 16983 children: [ 16984 i4 > 0 && /* @__PURE__ */ u2("select", { 16985 class: "le-select le-rb-comb", 16986 value: flat.combinator, 16987 onChange: (ev) => update({ 16988 ...flat, 16989 combinator: ev.target.value 16990 }), 16991 children: [
16992 /* @__PURE__ */ u2("option", { 16993 value: "and", 16994 children: "and" 16995 }), 16996 /* @__PURE__ */ u2("option", { 16997 value: "or", 16998 children: "or" 16999 }) 17000 ] 17001 }), 17002 i4 == 0 && /* @__PURE__ */ u2("span", { 17003 class: "le-rb-comb le-rb-if", 17004 children: "if" 17005 }), 17006 /* @__PURE__ */ u2("select", { 17007 class: "le-select le-rb-field", 17008 value: rule.field, 17009 onChange: (ev) => { 17010 const next = ev.target.value; 17011 const nextField = fields.find((f4) => f4.value == next); 17012 const nextChoices = nextField ? optionsFor(nextField.kind) : null; 17013 setRule(i4, nextChoices ? { 17014 field: next, 17015 operator: "equals", 17016 value: nextChoices[0][0] 17017 } : { 17018 field: next 17019 }); 17020 }, 17021 children: [ 17022 ...new Set(fields.map((f4) => f4.group)) 17023 ].map((group) => /* @__PURE__ */ u2("optgroup", { 17024 label: group, 17025 children: fields.filter((f4) => f4.group == group).map((f4) => /* @__PURE__ */ u2("option", { 17026 value: f4.value, 17027 children: f4.label 17028 }, f4.value)) 17029 }, group)) 17030 }),
17031 /* @__PURE__ */ u2("select", { 17032 class: "le-select le-rb-op", 17033 value: rule.operator, 17034 onChange: (ev) => setRule(i4, { 17035 operator: ev.target.value 17036 }), 17037 children: operators.map((op) => { 17038 const def = OPERATIONS.find((d3) => d3.value == op); 17039 return def && /* @__PURE__ */ u2("option", { 17040 value: op, 17041 children: def.label 17042 }, op); 17043 }) 17044 }), 17045 hasValue && (choices ? /* @__PURE__ */ u2("select", { 17046 class: "le-select le-rb-val", 17047 value: rule.value, 17048 onChange: (ev) => setRule(i4, { 17049 value: ev.target.value 17050 }), 17051 children: choices.map(([v3, label]) => /* @__PURE__ */ u2("option", { 17052 value: v3, 17053 children: label 17054 }, v3)) 17055 }) : /* @__PURE__ */ u2("input", { 17056 class: "le-input le-rb-val", 17057 type: "text", 17058 value: rule.value, 17059 placeholder: "value", 17060 onInput: (ev) => setRule(i4, { 17061 value: ev.target.value 17062 }) 17063 })),
17064 /* @__PURE__ */ u2("button", { 17065 type: "button", 17066 class: "le-icon", 17067 title: "Remove this condition", 17068 onClick: () => update({ 17069 ...flat, 17070 rules: flat.rules.filter((_3, k3) => k3 != i4) 17071 }), 17072 children: li.cross 17073 }) 17074 ] 17075 }, i4); 17076 }), 17077 /* @__PURE__ */ u2("div", { 17078 class: "le-rb-foot", 17079 children: [ 17080 /* @__PURE__ */ u2("button", { 17081 type: "button", 17082 class: "le-btn", 17083 onClick: () => update({ 17084 ...flat, 17085 rules: [ 17086 ...flat.rules, 17087 { 17088 kind: "field", 17089 field: fields[0].value, 17090 operator: "equals", 17091 value: "" 17092 } 17093 ] 17094 }), 17095 children: "+ Add condition" 17096 }), 17097 /* @__PURE__ */ u2("span", { 17098 class: "le-rb-hint", 17099 children: flat.rules.length == 0 ? "No condition yet - this case would match everyone." : "" 17100 }),
17101 isSqlOffered && /* @__PURE__ */ u2(ModeToggle, { 17102 isRaw: false, 17103 canRules: true, 17104 onMode: setIsRaw 17105 }) 17106 ] 17107 }) 17108 ] 17109 }); 17110} 17111
17112// src/components/live/AiSuggestCard.tsx 17113function AiSuggestCard(props) { 17114 const [isOpen, setIsOpen] = p2(!props.collapsible); 17115 const [prompt, setPrompt] = p2(""); 17116 const [result, setResult] = p2(""); 17117 const [error, setError] = p2(""); 17118 const [isBusy, setIsBusy] = p2(false); 17119 async function generate(instruction) { 17120 const text = instruction.trim(); 17121 if (!text || isBusy) { 17122 return; 17123 } 17124 setIsBusy(true); 17125 setError(""); 17126 setResult(""); 17127 try { 17128 setResult(await aiSuggest({ 17129 kind: props.kind, 17130 current: props.getCurrent(), 17131 context: props.getContext?.(), 17132 instruction: text, 17133 elem: props.elem, 17134 actionId: props.actionId 17135 })); 17136 } catch (e3) { 17137 console.error(e3); 17138 setError(e3 instanceof Error ? e3.message : "The AI request failed."); 17139 } 17140 setIsBusy(false); 17141 } 17142 if (!isOpen) { 17143 return /* @__PURE__ */ u2("button", { 17144 type: "button", 17145 class: "le-aicard-open", 17146 onClick: () => setIsOpen(true), 17147 children: [
17148 /* @__PURE__ */ u2("span", { 17149 class: "le-aicard-ico", 17150 children: li.sparkle 17151 }), 17152 /* @__PURE__ */ u2("span", { 17153 children: props.title 17154 }), 17155 li.chevronDown 17156 ] 17157 }); 17158 } 17159 return /* @__PURE__ */ u2("div", { 17160 class: "le-aicard", 17161 children: [ 17162 /* @__PURE__ */ u2("div", { 17163 class: "le-aicard-head", 17164 children: [ 17165 /* @__PURE__ */ u2("span", { 17166 class: "le-aicard-ico", 17167 children: li.sparkle 17168 }), 17169 /* @__PURE__ */ u2("h5", { 17170 children: props.title 17171 }) 17172 ] 17173 }), 17174 /* @__PURE__ */ u2("textarea", { 17175 class: "le-area", 17176 rows: 2, 17177 value: prompt, 17178 placeholder: props.placeholder, 17179 disabled: isBusy, 17180 onInput: (ev) => setPrompt(ev.target.value), 17181 onKeyDown: (ev) => { 17182 if (ev.key == "Enter" && !ev.shiftKey) { 17183 ev.preventDefault(); 17184 void generate(prompt); 17185 } 17186 } 17187 }),
17188 /* @__PURE__ */ u2("div", { 17189 class: "le-chips", 17190 children: props.chips.map((chip) => /* @__PURE__ */ u2("button", { 17191 type: "button", 17192 class: "le-chip", 17193 title: "Ask for: " + chip, 17194 disabled: isBusy, 17195 onClick: () => { 17196 setPrompt(chip); 17197 void generate(chip); 17198 }, 17199 children: chip 17200 }, chip)) 17201 }), 17202 /* @__PURE__ */ u2("button", { 17203 type: "button", 17204 class: "le-btn le-ai", 17205 disabled: isBusy || !prompt.trim(), 17206 onClick: () => void generate(prompt), 17207 children: [ 17208 li.sparkle, 17209 isBusy ? "Thinking..." : "Generate" 17210 ] 17211 }), 17212 error && /* @__PURE__ */ u2("div", { 17213 class: "le-notice le-warn", 17214 children: [ 17215 li.warn, 17216 /* @__PURE__ */ u2("div", { 17217 children: error 17218 }) 17219 ] 17220 }), 17221 result && /* @__PURE__ */ u2(Result, { 17222 kind: props.kind, 17223 result, 17224 onDiscard: () => setResult(""), 17225 onApply: () => { 17226 props.onApply(result); 17227 setResult(""); 17228 } 17229 }) 17230 ] 17231 }); 17232} 17233function Result(props) { 17234 const declarations = props.kind == "style" ? parseDeclarations(props.result) : []; 17235 const isUnusable = props.kind == "style" && declarations.length == 0; 17236 return /* @__PURE__ */ u2("div", { 17237 class: "le-aicard-result", 17238 children: [
17239 /* @__PURE__ */ u2("div", { 17240 class: "le-aicard-label", 17241 children: props.kind == "style" ? "Properties to set" : props.kind == "html" ? "Suggested markup" : "Suggested rewrite" 17242 }), 17243 props.kind == "style" ? isUnusable ? /* @__PURE__ */ u2("div", { 17244 class: "le-aicard-text le-mono", 17245 children: props.result 17246 }) : /* @__PURE__ */ u2("div", { 17247 class: "le-decls", 17248 children: declarations.map(([name, value]) => /* @__PURE__ */ u2("div", { 17249 class: "le-decl", 17250 children: [ 17251 /* @__PURE__ */ u2("code", { 17252 children: name 17253 }), 17254 /* @__PURE__ */ u2("span", { 17255 children: value 17256 }), 17257 /^(#|rgb|hsl)/i.test(value) && /* @__PURE__ */ u2("i", { 17258 class: "le-decl-swatch", 17259 style: "background:" + value 17260 }) 17261 ] 17262 }, name)) 17263 }) : /* @__PURE__ */ u2("div", { 17264 class: "le-aicard-text" + (props.kind == "html" ? " le-mono" : ""), 17265 children: props.result 17266 }), 17267 isUnusable && /* @__PURE__ */ u2("p", { 17268 class: "le-hint", 17269 style: "margin:0.5em 0 0", 17270 children: "No CSS declarations could be read out of that. Discard it and rephrase." 17271 }),
17272 /* @__PURE__ */ u2("div", { 17273 class: "le-btn-row", 17274 children: [ 17275 /* @__PURE__ */ u2("button", { 17276 type: "button", 17277 class: "le-btn", 17278 onClick: props.onDiscard, 17279 children: "Discard" 17280 }), 17281 /* @__PURE__ */ u2("button", { 17282 type: "button", 17283 class: "le-btn le-primary", 17284 disabled: isUnusable, 17285 onClick: props.onApply, 17286 children: "Apply" 17287 }) 17288 ] 17289 }) 17290 ] 17291 }); 17292} 17293
17294// src/components/live/VariationsDialog.tsx 17295var CASE_CHIPS = [ 17296 "Address this audience directly", 17297 "Shorter", 17298 "Benefit-led", 17299 "More urgent" 17300]; 17301function ContentBox(props) { 17302 const [caret, setCaret] = p2(0); 17303 function capture(ev) { 17304 setCaret(ev.target.selectionStart ?? 0); 17305 } 17306 const common = { 17307 value: props.value, 17308 placeholder: props.placeholder, 17309 onInput: (ev) => props.onChange(ev.target.value), 17310 onSelect: capture, 17311 onClick: capture, 17312 onKeyUp: capture, 17313 onFocus: capture 17314 }; 17315 return /* @__PURE__ */ u2("div", { 17316 class: "le-vd-content", 17317 children: [ 17318 props.multiline ? /* @__PURE__ */ u2("textarea", { 17319 class: "le-area", 17320 rows: 2, 17321 ...common 17322 }) : /* @__PURE__ */ u2("input", { 17323 class: "le-input", 17324 type: "text", 17325 ...common 17326 }), 17327 /* @__PURE__ */ u2(VarButton, { 17328 text: props.value, 17329 caret, 17330 onInsert: (next, pos) => { 17331 props.onChange(next); 17332 setCaret(pos); 17333 } 17334 }) 17335 ] 17336 }); 17337} 17338function CaseAi(props) { 17339 const [isOpen, setIsOpen] = p2(false); 17340 const rule = props.when.trim(); 17341 if (!isOpen) { 17342 return /* @__PURE__ */ u2("div", { 17343 class: "le-btn-bar", 17344 children: /* @__PURE__ */ u2("button", { 17345 type: "button", 17346 class: "le-btn le-ai", 17347 style: "width:auto", 17348 title: rule ? "Write this variation with AI, using the rule above as the audience" : "Set a condition first - the AI writes for the audience it describes", 17349 disabled: !rule, 17350 onClick: () => setIsOpen(true), 17351 children: [ 17352 li.sparkle, 17353 "Generate for this case" 17354 ] 17355 }) 17356 }); 17357 } 17358 return /* @__PURE__ */ u2(AiSuggestCard, { 17359 title: "Generate for this case", 17360 kind: "text", 17361 getCurrent: () => props.then, 17362 getContext: () => "This variation is shown only to visitors matching: " + rule, 17363 elem: props.elem, 17364 actionId: props.actionId, 17365 placeholder: "What should this audience read? e.g. 'address CFOs, emphasize ROI, under 10 words'", 17366 chips: CASE_CHIPS, 17367 onApply: props.onApply 17368 }); 17369} 17370function VariationsDialog(props) { 17371 const start2 = deriveCases(props.value); 17372 const [cases, setCases] = p2(start2.activeCases.length ? start2.activeCases : [ 17373 { 17374 when: "", 17375 then: "" 17376 } 17377 ]); 17378 const [elseValue, setElseValue] = p2(start2.elseValue); 17379 const userColumns = useUserColumns().filter((c3) => c3.path.length == 1).map((c3) => ({ 17380 value: c3.path[0], 17381 label: c3.label 17382 })); 17383 _2(() => { 17384 function onKey(ev) { 17385 if (ev.key == "Escape") { 17386 ev.stopPropagation(); 17387 props.onCancel(); 17388 } 17389 } 17390 document.addEventListener("keydown", onKey); 17391 return () => document.removeEventListener("keydown", onKey); 17392 }, []); 17393 function save() { 17394 props.onSave(composeCases(cases.filter((c3) => c3.when.trim() != ""), elseValue)); 17395 } 17396 function setCase(i4, patch) { 17397 setCases(cases.map((c3, k3) => k3 != i4 ? c3 : { 17398 ...c3, 17399 ...patch 17400 })); 17401 } 17402 return /* @__PURE__ */ u2("div", { 17403 class: "le-root le-vd-overlay", 17404 onMouseDown: (ev) => { 17405 ev.stopPropagation(); 17406 if (ev.target === ev.currentTarget) { 17407 props.onCancel(); 17408 } 17409 }, 17410 children: /* @__PURE__ */ u2("div", { 17411 class: "le-vd", 17412 role: "dialog", 17413 "aria-modal": "true", 17414 "aria-label": "Variations", 17415 children: [
17416 /* @__PURE__ */ u2("div", { 17417 class: "le-vd-head", 17418 children: [ 17419 /* @__PURE__ */ u2("div", { 17420 children: [ 17421 /* @__PURE__ */ u2("h3", { 17422 children: [ 17423 li.bolt, 17424 " Variations ", 17425 /* @__PURE__ */ u2("span", { 17426 class: "le-vd-tag", 17427 children: "(audience-based)" 17428 }) 17429 ] 17430 }), 17431 /* @__PURE__ */ u2("p", { 17432 children: "The first matching case wins; everyone else gets the default." 17433 }) 17434 ] 17435 }), 17436 /* @__PURE__ */ u2("button", { 17437 type: "button", 17438 class: "le-icon", 17439 title: "Close", 17440 onClick: props.onCancel, 17441 children: li.cross 17442 }) 17443 ] 17444 }), 17445 /* @__PURE__ */ u2("div", { 17446 class: "le-vd-body", 17447 children: [ 17448 cases.map((c3, i4) => /* @__PURE__ */ u2("div", { 17449 class: "le-vd-case", 17450 children: [
17451 /* @__PURE__ */ u2("div", { 17452 class: "le-vd-case-head", 17453 children: [ 17454 /* @__PURE__ */ u2("b", { 17455 children: [ 17456 "Case ", 17457 i4 + 1 17458 ] 17459 }), 17460 /* @__PURE__ */ u2("div", { 17461 class: "le-spacer" 17462 }), 17463 /* @__PURE__ */ u2("button", { 17464 type: "button", 17465 class: "le-icon", 17466 title: "Remove this case", 17467 onClick: () => setCases(cases.filter((_3, k3) => k3 != i4)), 17468 children: li.cross 17469 }) 17470 ] 17471 }), 17472 /* @__PURE__ */ u2("label", { 17473 class: "le-vd-label", 17474 children: "If visitor is:" 17475 }), 17476 /* @__PURE__ */ u2(RuleBuilder, { 17477 value: c3.when, 17478 userColumns, 17479 onChange: (when) => setCase(i4, { 17480 when 17481 }) 17482 }),
17483 /* @__PURE__ */ u2("label", { 17484 class: "le-vd-label", 17485 children: "Then show:" 17486 }), 17487 /* @__PURE__ */ u2(ContentBox, { 17488 value: c3.then, 17489 multiline: props.multiline, 17490 placeholder: "Content for this audience", 17491 onChange: (then) => setCase(i4, { 17492 then 17493 }) 17494 }), 17495 isJsapiConfigured() && /* @__PURE__ */ u2(CaseAi, { 17496 when: c3.when, 17497 then: c3.then, 17498 actionId: props.actionId ?? 0, 17499 elem: props.elem ?? null, 17500 onApply: (then) => setCase(i4, { 17501 then 17502 }) 17503 }) 17504 ] 17505 }, i4)), 17506 /* @__PURE__ */ u2("div", { 17507 class: "le-vd-default", 17508 children: [ 17509 /* @__PURE__ */ u2("label", { 17510 class: "le-vd-label", 17511 children: "Default - when no case matches:" 17512 }),
17513 /* @__PURE__ */ u2(ContentBox, { 17514 value: elseValue, 17515 multiline: props.multiline, 17516 placeholder: "Content everyone else sees", 17517 onChange: setElseValue 17518 }) 17519 ] 17520 }) 17521 ] 17522 }), 17523 /* @__PURE__ */ u2("div", { 17524 class: "le-vd-foot", 17525 children: [ 17526 /* @__PURE__ */ u2("button", { 17527 type: "button", 17528 class: "le-btn", 17529 onClick: () => setCases([ 17530 ...cases, 17531 { 17532 when: "", 17533 then: "" 17534 } 17535 ]), 17536 children: "+ Add case" 17537 }), 17538 /* @__PURE__ */ u2("div", { 17539 class: "le-spacer" 17540 }), 17541 /* @__PURE__ */ u2("button", { 17542 type: "button", 17543 class: "le-btn", 17544 onClick: props.onCancel, 17545 children: "Cancel" 17546 }), 17547 /* @__PURE__ */ u2("button", { 17548 type: "button", 17549 class: "le-btn le-primary", 17550 onClick: save, 17551 children: "Done" 17552 }) 17553 ] 17554 }) 17555 ] 17556 }) 17557 }); 17558} 17559
17560// src/components/live/panels/TextPanels.tsx 17561var TEXT_CHIPS = [ 17562 "Make it punchier", 17563 "Shorter", 17564 "Benefit-focused", 17565 "More urgent", 17566 "Match brand voice" 17567]; 17568var HTML_CHIPS = [ 17569 "Simplify the markup", 17570 "Make the wording punchier", 17571 "Shorter" 17572]; 17573function useContentEditable(props, onInput) { 17574 const { session } = props; 17575 const [text, setText] = p2(() => session.elem.textContent ?? ""); 17576 _2(() => { 17577 const elem = session.elem; 17578 elem.contentEditable = "true"; 17579 try { 17580 elem.focus(); 17581 } catch { 17582 } 17583 const handler = () => { 17584 setText(elem.textContent ?? ""); 17585 onInput?.(); 17586 session.onPreview(); 17587 }; 17588 elem.addEventListener("input", handler); 17589 elem.addEventListener("keyup", handler); 17590 elem.addEventListener("mouseup", handler); 17591 return () => { 17592 elem.removeEventListener("input", handler); 17593 elem.removeEventListener("keyup", handler); 17594 elem.removeEventListener("mouseup", handler); 17595 elem.contentEditable = "false"; 17596 elem.removeAttribute("contenteditable"); 17597 }; 17598 }, [ 17599 session.elem 17600 ]); 17601 return [ 17602 text, 17603 setText 17604 ]; 17605} 17606function useSavedPageSelection(elem, onOwn, skip) { 17607 const ranges = F2([]); 17608 const onOwnRef = F2(onOwn); 17609 onOwnRef.current = onOwn; 17610 const skipRef = F2(skip); 17611 skipRef.current = skip; 17612 _2(() => { 17613 ranges.current = []; 17614 return installSavedSelection(document, elem, { 17615 skip: () => skipRef.current?.() ?? false, 17616 onSaved: (cur) => { 17617 ranges.current = cur; 17618 onOwnRef.current?.(); 17619 } 17620 }); 17621 }, [ 17622 elem 17623 ]); 17624 return ranges; 17625} 17626function insertTextOnPage(elem, savedRanges, text) { 17627 const sel = document.getSelection(); 17628 if (sel && savedRanges.length) { 17629 try { 17630 elem.focus(); 17631 } catch { 17632 } 17633 sel.removeAllRanges(); 17634 for (const r3 of savedRanges) { 17635 sel.addRange(r3); 17636 } 17637 let inserted = false; 17638 try { 17639 inserted = document.execCommand("insertText", false, text); 17640 } catch { 17641 inserted = false; 17642 } 17643 if (!inserted) { 17644 const r3 = savedRanges[savedRanges.length - 1]; 17645 r3.deleteContents(); 17646 const node = document.createTextNode(text); 17647 r3.insertNode(node); 17648 r3.setStartAfter(node); 17649 r3.collapse(true); 17650 } 17651 } else { 17652 elem.appendChild(document.createTextNode(text)); 17653 } 17654} 17655function useBoxCaret() { 17656 const [caret, setCaret] = p2(void 0); 17657 function capture(ev) { 17658 const el = ev.target; 17659 setCaret({ 17660 start: el.selectionStart ?? 0, 17661 end: el.selectionEnd ?? 0 17662 }); 17663 } 17664 return [ 17665 caret, 17666 setCaret, 17667 capture 17668 ]; 17669} 17670function VariationsButton(props) { 17671 const [isOpen, setIsOpen] = p2(false); 17672 const n2 = deriveCases(props.value).activeCases.length; 17673 return /* @__PURE__ */ u2(k, { 17674 children: [
17675 /* @__PURE__ */ u2("button", { 17676 type: "button", 17677 class: "le-btn", 17678 title: "Show different content to different audiences", 17679 onClick: () => setIsOpen(true), 17680 children: [ 17681 li.bolt, 17682 "Variations", 17683 n2 > 0 && /* @__PURE__ */ u2("span", { 17684 class: "le-count", 17685 children: n2 17686 }) 17687 ] 17688 }), 17689 isOpen && /* @__PURE__ */ u2(VariationsDialog, { 17690 value: props.value, 17691 multiline: props.multiline, 17692 actionId: props.actionId, 17693 elem: props.elem, 17694 onSave: (value) => { 17695 setIsOpen(false); 17696 props.onSave(value); 17697 }, 17698 onCancel: () => setIsOpen(false) 17699 }) 17700 ] 17701 }); 17702} 17703function restoreBoxCaret(el, pos) { 17704 setTimeout(() => { 17705 if (el) { 17706 el.focus(); 17707 el.setSelectionRange(pos, pos); 17708 } 17709 }, 0); 17710} 17711function TextPanel(props) { 17712 const { session } = props; 17713 const isInput = editHtmlKind(session.elem) == 2; 17714 if (isInput) { 17715 return /* @__PURE__ */ u2(InputValuePanel, { 17716 ...props 17717 }); 17718 } 17719 return /* @__PURE__ */ u2(InPlacePanel, { 17720 ...props 17721 }); 17722} 17723function InPlacePanel(props) { 17724 const { session } = props; 17725 const [text, setText] = useContentEditable(props); 17726 const mirrorRef = F2(null); 17727 const [mirrorCaret, setMirrorCaret, captureMirrorCaret] = useBoxCaret(); 17728 const pageRanges = useSavedPageSelection(session.elem, void 0, () => mirrorRef.current?.matches(":focus") ?? false); 17729 _2(() => installPageTakeover(session.elem, () => setMirrorCaret(void 0)), [ 17730 session.elem 17731 ]); 17732 function apply(next) { 17733 setText(next); 17734 session.elem.textContent = next; 17735 session.onPreview(); 17736 } 17737 return /* @__PURE__ */ u2(k, { 17738 children: [
17739 /* @__PURE__ */ u2("div", { 17740 class: "le-notice", 17741 children: [ 17742 li.pencil, 17743 /* @__PURE__ */ u2("div", { 17744 children: "Type straight on the page - the element is editable while this panel is open. The box below mirrors it, and replaces the whole text when you edit it here." 17745 }) 17746 ] 17747 }), 17748 isJsapiConfigured() && /* @__PURE__ */ u2(AiSuggestCard, { 17749 title: "Edit text with AI", 17750 kind: "text", 17751 getCurrent: () => text, 17752 elem: session.elem, 17753 actionId: props.actionId, 17754 placeholder: "Tell AI what to change... e.g. 'make this headline punchier and under 8 words'", 17755 chips: TEXT_CHIPS, 17756 onApply: apply 17757 }), 17758 /* @__PURE__ */ u2(Section, { 17759 title: "Content", 17760 children: [ 17761 /* @__PURE__ */ u2("textarea", { 17762 ref: mirrorRef, 17763 class: "le-area", 17764 rows: 3, 17765 value: text, 17766 onInput: (ev) => apply(ev.target.value), 17767 onSelect: captureMirrorCaret, 17768 onClick: captureMirrorCaret, 17769 onKeyUp: captureMirrorCaret, 17770 onFocus: captureMirrorCaret 17771 }),
17772 /* @__PURE__ */ u2("div", { 17773 class: "le-btn-bar", 17774 children: [ 17775 mirrorCaret ? /* @__PURE__ */ u2(VarButton, { 17776 text, 17777 caret: mirrorCaret.start, 17778 onInsert: (next, pos) => { 17779 apply(next); 17780 setMirrorCaret({ 17781 start: pos, 17782 end: pos 17783 }); 17784 restoreBoxCaret(mirrorRef.current, pos); 17785 } 17786 }) : /* @__PURE__ */ u2(VarButton, { 17787 onPick: (token) => { 17788 insertTextOnPage(session.elem, pageRanges.current, token); 17789 setText(session.elem.textContent ?? ""); 17790 session.onPreview(); 17791 } 17792 }), 17793 /* @__PURE__ */ u2(VariationsButton, { 17794 value: text, 17795 multiline: true, 17796 actionId: props.actionId, 17797 elem: session.elem, 17798 onSave: apply 17799 }) 17800 ] 17801 }) 17802 ] 17803 }) 17804 ] 17805 }); 17806} 17807function InputValuePanel(props) { 17808 const { session } = props; 17809 const input = session.elem; 17810 const [value, setValue] = p2(() => input.value ?? ""); 17811 const boxRef = F2(null); 17812 const [caret, setCaret, captureCaret] = useBoxCaret(); 17813 function apply(next) { 17814 setValue(next); 17815 input.value = next; 17816 input.setAttribute("value", next); 17817 session.onPreview(); 17818 } 17819 return /* @__PURE__ */ u2(k, { 17820 children: [ 17821 isJsapiConfigured() && /* @__PURE__ */ u2(AiSuggestCard, { 17822 title: "Edit this value with AI", 17823 kind: "text", 17824 getCurrent: () => value, 17825 elem: session.elem, 17826 actionId: props.actionId, 17827 placeholder: "Tell AI what to change... e.g. 'a friendlier placeholder'", 17828 chips: TEXT_CHIPS, 17829 onApply: apply 17830 }),
17831 /* @__PURE__ */ u2(Section, { 17832 title: "Value", 17833 children: [ 17834 /* @__PURE__ */ u2("input", { 17835 ref: boxRef, 17836 class: "le-input", 17837 type: "text", 17838 value, 17839 placeholder: "Value of the field", 17840 onInput: (ev) => apply(ev.target.value), 17841 onSelect: captureCaret, 17842 onClick: captureCaret, 17843 onKeyUp: captureCaret, 17844 onFocus: captureCaret 17845 }), 17846 /* @__PURE__ */ u2("div", { 17847 class: "le-btn-bar", 17848 children: [ 17849 /* @__PURE__ */ u2(VarButton, { 17850 text: value, 17851 caret: caret?.start ?? value.length, 17852 onInsert: (next, pos) => { 17853 apply(next); 17854 setCaret({ 17855 start: pos, 17856 end: pos 17857 }); 17858 restoreBoxCaret(boxRef.current, pos); 17859 } 17860 }), 17861 /* @__PURE__ */ u2(VariationsButton, { 17862 value, 17863 actionId: props.actionId, 17864 elem: session.elem, 17865 onSave: apply 17866 }) 17867 ] 17868 }) 17869 ] 17870 }) 17871 ] 17872 }); 17873} 17874var FORMAT_BLOCKS = [ 17875 [ 17876 "", 17877 "Formatting..." 17878 ], 17879 [ 17880 "P", 17881 "Paragraph" 17882 ], 17883 [ 17884 "PRE", 17885 "Preserve whitespace" 17886 ], 17887 [ 17888 "ADDRESS", 17889 "Address" 17890 ], 17891 [ 17892 "H1", 17893 "Heading, level 1" 17894 ], 17895 [ 17896 "H2", 17897 "Heading, level 2" 17898 ], 17899 [ 17900 "H3", 17901 "Heading, level 3" 17902 ], 17903 [ 17904 "H4", 17905 "Heading, level 4" 17906 ], 17907 [ 17908 "H5", 17909 "Heading, level 5" 17910 ], 17911 [ 17912 "H6", 17913 "Heading, level 6" 17914 ] 17915]; 17916function RichPanel(props) { 17917 const { session } = props; 17918 useContentEditable(props); 17919 const [linkUrl, setLinkUrl] = p2(""); 17920 const [isLinkOpen, setIsLinkOpen] = p2(false); 17921 const [selectedText, setSelectedText] = p2(""); 17922 const lastSelection = F2([]); 17923 const pageRanges = useSavedPageSelection(session.elem, () => { 17924 const sel = document.getSelection(); 17925 const text = sel?.toString() ?? ""; 17926 if (text && sel) { 17927 const ranges = new Array(); 17928 for (let i4 = 0; i4 < sel.rangeCount; i4++) { 17929 ranges.push(sel.getRangeAt(i4).cloneRange()); 17930 } 17931 lastSelection.current = ranges; 17932 setSelectedText(text); 17933 } 17934 }); 17935 function restoreSelection() { 17936 const sel = document.getSelection(); 17937 const saved = lastSelection.current; 17938 if (!sel || !saved.length) { 17939 return false; 17940 } 17941 try { 17942 session.elem.focus(); 17943 } catch { 17944 } 17945 sel.removeAllRanges(); 17946 for (const r3 of saved) { 17947 sel.addRange(r3); 17948 } 17949 return true; 17950 } 17951 function command(name, value, useCss = false) { 17952 try { 17953 restoreSelection(); 17954 try { 17955 document.execCommand("styleWithCSS", false, useCss ? "true" : "false"); 17956 } catch { 17957 } 17958 document.execCommand(name, false, value ?? void 0); 17959 session.onPreview(); 17960 } catch (e3) { 17961 console.error(e3); 17962 } 17963 } 17964 function keepSelection(ev) { 17965 ev.preventDefault(); 17966 } 17967 return /* @__PURE__ */ u2(k, { 17968 children: [
17969 /* @__PURE__ */ u2("div", { 17970 class: "le-notice", 17971 children: [ 17972 li.lines, 17973 selectedText.trim() ? /* @__PURE__ */ u2("div", { 17974 children: [ 17975 "These controls apply to ", 17976 /* @__PURE__ */ u2("b", { 17977 children: [ 17978 '"', 17979 selectedText.length > 60 ? selectedText.slice(0, 59) + "\u2026" : selectedText, 17980 '"' 17981 ] 17982 }), 17983 " - the last text you selected on the page. Select something else to move them." 17984 ] 17985 }) : /* @__PURE__ */ u2("div", { 17986 children: [ 17987 "Select some text ", 17988 /* @__PURE__ */ u2("b", { 17989 children: "on the page" 17990 }), 17991 " first - these controls act on a selection, and do nothing without one." 17992 ] 17993 }) 17994 ] 17995 }), 17996 /* @__PURE__ */ u2(Section, { 17997 title: "Inline", 17998 children: /* @__PURE__ */ u2("div", { 17999 class: "le-rt", 18000 onMouseDown: keepSelection, 18001 children: [
18002 /* @__PURE__ */ u2("button", { 18003 type: "button", 18004 title: "Bold", 18005 style: "font-weight:900", 18006 onClick: () => command("Bold"), 18007 children: "B" 18008 }), 18009 /* @__PURE__ */ u2("button", { 18010 type: "button", 18011 title: "Italic", 18012 style: "font-style:italic", 18013 onClick: () => command("Italic"), 18014 children: "I" 18015 }), 18016 /* @__PURE__ */ u2("button", { 18017 type: "button", 18018 title: "Underline", 18019 style: "text-decoration:underline", 18020 onClick: () => command("Underline"), 18021 children: "U" 18022 }), 18023 /* @__PURE__ */ u2("button", { 18024 type: "button", 18025 title: "Strike through", 18026 style: "text-decoration:line-through", 18027 onClick: () => command("StrikeThrough"), 18028 children: "S" 18029 }), 18030 /* @__PURE__ */ u2("button", { 18031 type: "button", 18032 title: "Subscript", 18033 onClick: () => command("Subscript"), 18034 children: "x\xB2" 18035 }),
18036 /* @__PURE__ */ u2("button", { 18037 type: "button", 18038 title: "Superscript", 18039 onClick: () => command("Superscript"), 18040 children: "x\u2082" 18041 }), 18042 /* @__PURE__ */ u2("button", { 18043 type: "button", 18044 title: "Bigger", 18045 onClick: () => command("IncreaseFontSize"), 18046 children: "F+" 18047 }), 18048 /* @__PURE__ */ u2("button", { 18049 type: "button", 18050 title: "Smaller", 18051 onClick: () => command("DecreaseFontSize"), 18052 children: "F-" 18053 }), 18054 /* @__PURE__ */ u2("button", { 18055 type: "button", 18056 title: "Remove formatting", 18057 onClick: () => { 18058 command("RemoveFormat"); 18059 command("Unlink"); 18060 }, 18061 children: "\u232B" 18062 }) 18063 ] 18064 }) 18065 }), 18066 /* @__PURE__ */ u2(Section, { 18067 title: "Color", 18068 children: /* @__PURE__ */ u2("div", { 18069 class: "le-field-group", 18070 children: [
18071 /* @__PURE__ */ u2("label", { 18072 class: "le-btn", 18073 title: "Color of the selected text", 18074 children: [ 18075 "Text", 18076 /* @__PURE__ */ u2("input", { 18077 type: "color", 18078 style: "width:1.5em; height:1.5em; padding:0; border:none; background:transparent", 18079 onInput: (ev) => command("ForeColor", ev.target.value, true) 18080 }) 18081 ] 18082 }), 18083 /* @__PURE__ */ u2("label", { 18084 class: "le-btn", 18085 title: "Highlight behind the selected text", 18086 children: [ 18087 "Highlight", 18088 /* @__PURE__ */ u2("input", { 18089 type: "color", 18090 style: "width:1.5em; height:1.5em; padding:0; border:none; background:transparent", 18091 onInput: (ev) => { 18092 const color = ev.target.value; 18093 command("HiliteColor", color, true); 18094 if (!document.queryCommandValue?.("hiliteColor")) { 18095 command("BackColor", color, true); 18096 } 18097 } 18098 }) 18099 ] 18100 }) 18101 ] 18102 }) 18103 }),
18104 /* @__PURE__ */ u2(Section, { 18105 title: "Block", 18106 children: [ 18107 /* @__PURE__ */ u2(Field, { 18108 label: "Type", 18109 children: /* @__PURE__ */ u2(SelectInput, { 18110 value: "", 18111 options: FORMAT_BLOCKS, 18112 onChange: (value) => value && command("FormatBlock", value) 18113 }) 18114 }), 18115 /* @__PURE__ */ u2("div", { 18116 class: "le-rt", 18117 onMouseDown: keepSelection, 18118 children: [ 18119 /* @__PURE__ */ u2("button", { 18120 type: "button", 18121 title: "Align left", 18122 onClick: () => command("JustifyLeft"), 18123 children: "\u25E7" 18124 }), 18125 /* @__PURE__ */ u2("button", { 18126 type: "button", 18127 title: "Align center", 18128 onClick: () => command("JustifyCenter"), 18129 children: "\u25A3" 18130 }), 18131 /* @__PURE__ */ u2("button", { 18132 type: "button", 18133 title: "Align right", 18134 onClick: () => command("JustifyRight"), 18135 children: "\u25E8" 18136 }),
18137 /* @__PURE__ */ u2("button", { 18138 type: "button", 18139 title: "Justify", 18140 onClick: () => command("JustifyFull"), 18141 children: "\u25A4" 18142 }), 18143 /* @__PURE__ */ u2("button", { 18144 type: "button", 18145 title: "Bulleted list", 18146 onClick: () => command("InsertUnorderedList"), 18147 children: "\u2022\u2261" 18148 }), 18149 /* @__PURE__ */ u2("button", { 18150 type: "button", 18151 title: "Numbered list", 18152 onClick: () => command("InsertOrderedList"), 18153 children: "1\u2261" 18154 }), 18155 /* @__PURE__ */ u2("button", { 18156 type: "button", 18157 title: "Outdent", 18158 onClick: () => command("Outdent"), 18159 children: "\u21E4" 18160 }), 18161 /* @__PURE__ */ u2("button", { 18162 type: "button", 18163 title: "Indent", 18164 onClick: () => command("Indent"), 18165 children: "\u21E5" 18166 }),
18167 /* @__PURE__ */ u2("button", { 18168 type: "button", 18169 title: "Horizontal rule", 18170 onClick: () => command("InsertHorizontalRule"), 18171 children: "\u2015" 18172 }), 18173 /* @__PURE__ */ u2("button", { 18174 type: "button", 18175 title: "Paragraph", 18176 onClick: () => command("InsertParagraph"), 18177 children: "\xB6" 18178 }) 18179 ] 18180 }) 18181 ] 18182 }), 18183 /* @__PURE__ */ u2(Section, { 18184 title: "Dynamic value", 18185 children: /* @__PURE__ */ u2("div", { 18186 class: "le-btn-bar", 18187 onMouseDown: keepSelection, 18188 children: [ 18189 /* @__PURE__ */ u2(VarButton, { 18190 onPick: (token) => { 18191 insertTextOnPage(session.elem, pageRanges.current, token); 18192 session.onPreview(); 18193 } 18194 }), 18195 /* @__PURE__ */ u2(VariationsButton, { 18196 value: session.elem.innerHTML, 18197 multiline: true, 18198 actionId: props.actionId, 18199 elem: session.elem, 18200 onSave: (value) => { 18201 session.elem.innerHTML = value; 18202 session.onPreview(); 18203 } 18204 }) 18205 ] 18206 }) 18207 }),
18208 /* @__PURE__ */ u2(Section, { 18209 title: "Link", 18210 children: !isLinkOpen ? /* @__PURE__ */ u2("div", { 18211 class: "le-btn-bar", 18212 onMouseDown: keepSelection, 18213 children: [ 18214 /* @__PURE__ */ u2("button", { 18215 type: "button", 18216 class: "le-btn", 18217 disabled: !selectedText.trim(), 18218 title: selectedText.trim() ? "Turn the selected text into a link" : "Select the text to link first", 18219 onClick: () => setIsLinkOpen(true), 18220 children: [ 18221 li.link, 18222 " Link the selection" 18223 ] 18224 }), 18225 /* @__PURE__ */ u2("button", { 18226 type: "button", 18227 class: "le-btn", 18228 title: "Remove the link from the selection", 18229 onClick: () => command("Unlink"), 18230 children: "Unlink" 18231 }) 18232 ] 18233 }) : /* @__PURE__ */ u2(k, { 18234 children: [ 18235 /* @__PURE__ */ u2("div", { 18236 class: "le-field-group", 18237 children: /* @__PURE__ */ u2(TextInput, { 18238 value: linkUrl, 18239 placeholder: "https://...", 18240 onChange: setLinkUrl 18241 }) 18242 }),
18243 /* @__PURE__ */ u2("div", { 18244 class: "le-btn-bar", 18245 children: [ 18246 /* @__PURE__ */ u2("button", { 18247 type: "button", 18248 class: "le-btn", 18249 onClick: () => { 18250 setIsLinkOpen(false); 18251 setLinkUrl(""); 18252 }, 18253 children: "Cancel" 18254 }), 18255 /* @__PURE__ */ u2("button", { 18256 type: "button", 18257 class: "le-btn le-primary", 18258 disabled: !linkUrl.trim(), 18259 onClick: () => { 18260 command("CreateLink", linkUrl.trim()); 18261 setIsLinkOpen(false); 18262 setLinkUrl(""); 18263 }, 18264 children: "Apply" 18265 }) 18266 ] 18267 }) 18268 ] 18269 }) 18270 }) 18271 ] 18272 }); 18273} 18274function SourcePanel(props) { 18275 const { session } = props; 18276 const [html, setHtml] = p2(() => session.elem.innerHTML); 18277 const boxRef = F2(null); 18278 const [caret, setCaret, captureCaret] = useBoxCaret(); 18279 const errorRef = F2(""); 18280 const [error, setError] = p2(""); 18281 function apply(value) { 18282 setHtml(value); 18283 try { 18284 session.elem.innerHTML = value; 18285 if (errorRef.current) { 18286 errorRef.current = ""; 18287 setError(""); 18288 } 18289 } catch (e3) { 18290 const message = e3 instanceof Error ? e3.message : String(e3); 18291 errorRef.current = message; 18292 setError(message); 18293 } 18294 session.onPreview(); 18295 } 18296 return /* @__PURE__ */ u2(k, { 18297 children: [
18298 /* @__PURE__ */ u2("div", { 18299 class: "le-notice", 18300 children: [ 18301 li.code, 18302 /* @__PURE__ */ u2("div", { 18303 children: [ 18304 "Everything inside the element. The outer tag and its attributes are edited in ", 18305 /* @__PURE__ */ u2("b", { 18306 children: "Properties" 18307 }), 18308 "." 18309 ] 18310 }) 18311 ] 18312 }), 18313 error && /* @__PURE__ */ u2("div", { 18314 class: "le-notice le-warn", 18315 children: [ 18316 li.warn, 18317 /* @__PURE__ */ u2("div", { 18318 children: error 18319 }) 18320 ] 18321 }), 18322 isJsapiConfigured() && /* @__PURE__ */ u2(AiSuggestCard, { 18323 title: "Edit the markup with AI", 18324 kind: "html", 18325 getCurrent: () => html, 18326 elem: session.elem, 18327 actionId: props.actionId, 18328 placeholder: "Describe the markup you want... e.g. 'wrap each line in its own paragraph'", 18329 chips: HTML_CHIPS, 18330 onApply: apply 18331 }), 18332 /* @__PURE__ */ u2(Section, { 18333 title: "Inner HTML", 18334 children: [
18335 /* @__PURE__ */ u2("textarea", { 18336 ref: boxRef, 18337 class: "le-area le-mono", 18338 rows: 10, 18339 value: html, 18340 onInput: (ev) => apply(ev.target.value), 18341 onSelect: captureCaret, 18342 onClick: captureCaret, 18343 onKeyUp: captureCaret, 18344 onFocus: captureCaret 18345 }), 18346 /* @__PURE__ */ u2("div", { 18347 class: "le-btn-bar", 18348 children: [ 18349 /* @__PURE__ */ u2(VarButton, { 18350 text: html, 18351 caret: caret?.start ?? html.length, 18352 onInsert: (next, pos) => { 18353 apply(next); 18354 setCaret({ 18355 start: pos, 18356 end: pos 18357 }); 18358 restoreBoxCaret(boxRef.current, pos); 18359 } 18360 }), 18361 /* @__PURE__ */ u2(VariationsButton, { 18362 value: html, 18363 multiline: true, 18364 actionId: props.actionId, 18365 elem: session.elem, 18366 onSave: apply 18367 }) 18368 ] 18369 }) 18370 ] 18371 }) 18372 ] 18373 }); 18374} 18375
18376// src/components/live/panels/StylePanels.tsx 18377function useStyleEditor(session) { 18378 const [, setVersion] = p2(0); 18379 const style = session.elem.style; 18380 function get(name) { 18381 const own2 = style[name]; 18382 if (own2) { 18383 return own2; 18384 } 18385 try { 18386 return getComputedStyle(session.elem)[name] || ""; 18387 } catch { 18388 return ""; 18389 } 18390 } 18391 function own(name) { 18392 return style[name] ?? ""; 18393 } 18394 function getNum(name, useComputed = true) { 18395 const value = useComputed ? get(name) : own(name); 18396 const n2 = parseFloat(value); 18397 return isNaN(n2) ? "" : String(Math.round(n2 * 1e3) / 1e3); 18398 } 18399 function set(name, value) { 18400 style[name] = value; 18401 setVersion((v3) => v3 + 1); 18402 session.onPreview(); 18403 } 18404 function setNum(name, value, unit = "px") { 18405 set(name, value === "" ? "" : value + unit); 18406 } 18407 function setMany(values) { 18408 for (const name in values) { 18409 style[name] = values[name]; 18410 } 18411 setVersion((v3) => v3 + 1); 18412 session.onPreview(); 18413 } 18414 function setDeclarations(declarations) { 18415 const css = session.elem.style; 18416 for (const [name, value] of declarations) { 18417 try { 18418 css.setProperty(name, value); 18419 } catch (e3) { 18420 console.error(e3); 18421 } 18422 } 18423 setVersion((v3) => v3 + 1); 18424 session.onPreview(); 18425 } 18426 function describeComputed(names) { 18427 const computed = getComputedStyle(session.elem); 18428 return names.map((name) => name + ": " + (computed.getPropertyValue(name) || "(unset)")).join("\n"); 18429 } 18430 return { 18431 get, 18432 own, 18433 getNum, 18434 set, 18435 setNum, 18436 setMany, 18437 setDeclarations, 18438 describeComputed 18439 }; 18440} 18441var FONT_WEIGHTS = [ 18442 [ 18443 "", 18444 "Inherit" 18445 ], 18446 [ 18447 "400", 18448 "400 - Regular" 18449 ], 18450 [ 18451 "500", 18452 "500 - Medium" 18453 ], 18454 [ 18455 "600", 18456 "600 - Semibold" 18457 ], 18458 [ 18459 "700", 18460 "700 - Bold" 18461 ], 18462 [ 18463 "800", 18464 "800 - Extra-bold" 18465 ], 18466 [ 18467 "900", 18468 "900 - Black" 18469 ] 18470]; 18471var BORDER_STYLES = [ 18472 [ 18473 "", 18474 "Inherit" 18475 ], 18476 [ 18477 "none", 18478 "None" 18479 ], 18480 [ 18481 "solid", 18482 "Solid" 18483 ], 18484 [ 18485 "dashed", 18486 "Dashed" 18487 ], 18488 [ 18489 "dotted", 18490 "Dotted" 18491 ] 18492]; 18493var ALIGN_CHOICES = [ 18494 { 18495 value: "left", 18496 title: "Left", 18497 icon: li.alignLeft 18498 }, 18499 { 18500 value: "center", 18501 title: "Center", 18502 icon: li.alignCenter 18503 }, 18504 { 18505 value: "right", 18506 title: "Right", 18507 icon: li.alignRight 18508 }, 18509 { 18510 value: "justify", 18511 title: "Justify", 18512 icon: li.alignJustify 18513 } 18514]; 18515var PRESETS = [ 18516 { 18517 label: "Heading 1", 18518 title: "36px, bold, line-height 1.2", 18519 style: { 18520 fontSize: "36px", 18521 fontWeight: "700", 18522 lineHeight: "1.2" 18523 } 18524 }, 18525 { 18526 label: "Heading 2", 18527 title: "28px, semibold, line-height 1.3", 18528 style: { 18529 fontSize: "28px", 18530 fontWeight: "600", 18531 lineHeight: "1.3" 18532 } 18533 }, 18534 { 18535 label: "Body", 18536 title: "16px, regular, line-height 1.5", 18537 style: { 18538 fontSize: "16px", 18539 fontWeight: "400", 18540 lineHeight: "1.5" 18541 } 18542 }, 18543 { 18544 label: "Caption", 18545 title: "13px, regular", 18546 style: { 18547 fontSize: "13px", 18548 fontWeight: "400", 18549 lineHeight: "1.4" 18550 } 18551 }, 18552 { 18553 label: "Muted", 18554 title: "Current size at 50% opacity", 18555 style: { 18556 opacity: "0.5" 18557 } 18558 }, 18559 { 18560 label: "Brand accent", 18561 title: "Personyze green #12B886", 18562 style: { 18563 color: "#12B886" 18564 } 18565 } 18566]; 18567var AI_CONTEXT_PROPS = [ 18568 "color", 18569 "background-color", 18570 "font-family", 18571 "font-size", 18572 "font-weight", 18573 "line-height", 18574 "letter-spacing", 18575 "text-align", 18576 "text-transform", 18577 "border", 18578 "border-radius", 18579 "opacity", 18580 "padding", 18581 "margin", 18582 "display" 18583]; 18584var STYLE_CHIPS = [ 18585 "Make it bolder", 18586 "More contrast", 18587 "Softer and rounded", 18588 "Match the brand green", 18589 "Bigger and tighter" 18590]; 18591function StylePanel(props) { 18592 const { session } = props; 18593 const style = useStyleEditor(session); 18594 const pageCssRules = q2(() => matchingRules(session.elem), [ 18595 session.elem 18596 ]); 18597 const [tab, setTab] = p2("text"); 18598 _2(() => setTab("text"), [ 18599 session.elem 18600 ]); 18601 const tabs = [ 18602 { 18603 id: "text", 18604 label: "Text", 18605 title: "Colour, size, weight, spacing, alignment" 18606 }, 18607 { 18608 id: "bg",
18609 label: "Background", 18610 title: "Background colour and image" 18611 }, 18612 { 18613 id: "box", 18614 label: "Box", 18615 title: "Border, corner radius, opacity" 18616 }, 18617 { 18618 id: "presets", 18619 label: "Presets", 18620 title: "Ready-made looks" 18621 }, 18622 ...pageCssRules.length ? [ 18623 { 18624 id: "css", 18625 label: "Page CSS", 18626 title: "Rules from the site's own stylesheets that match this element" 18627 } 18628 ] : [] 18629 ]; 18630 const cur = tabs.some((v3) => v3.id == tab) ? tab : "text"; 18631 return /* @__PURE__ */ u2(k, { 18632 children: [ 18633 /* @__PURE__ */ u2("p", { 18634 class: "le-hint le-style-note", 18635 title: "A box left empty leaves that property to the page", 18636 children: [ 18637 "Written as ", 18638 /* @__PURE__ */ u2("b", { 18639 children: "inline style" 18640 }), 18641 ", so it beats the site's own stylesheet." 18642 ] 18643 }), 18644 isJsapiConfigured() && /* @__PURE__ */ u2(AiSuggestCard, { 18645 title: "Edit style with AI", 18646 kind: "style", 18647 collapsible: true, 18648 // The declarations already ON the element, which is what the AI is amending. 18649 getCurrent: () => session.elem.getAttribute("style") ?? "", 18650 getContext: () => style.describeComputed(AI_CONTEXT_PROPS), 18651 elem: session.elem, 18652 actionId: props.actionId, 18653 placeholder: "Describe the look you want... e.g. 'bolder and more premium' or 'match the hero above'", 18654 chips: STYLE_CHIPS, 18655 onApply: (text) => style.setDeclarations(parseDeclarations(text)) 18656 }),
18657 /* @__PURE__ */ u2(PanelTabs, { 18658 tabs, 18659 value: cur, 18660 onChange: setTab 18661 }), 18662 cur == "text" && /* @__PURE__ */ u2(Section, { 18663 children: [ 18664 /* @__PURE__ */ u2(Field, { 18665 label: "Color", 18666 children: /* @__PURE__ */ u2(ColorInput, { 18667 value: style.get("color"), 18668 onChange: (value) => style.set("color", value) 18669 }) 18670 }), 18671 /* @__PURE__ */ u2(Field, { 18672 label: "Size", 18673 children: [ 18674 /* @__PURE__ */ u2(NumInput, { 18675 value: style.getNum("fontSize"), 18676 unit: "px", 18677 min: 4, 18678 max: 400, 18679 onChange: (value) => style.setNum("fontSize", value) 18680 }), 18681 /* @__PURE__ */ u2(SelectInput, { 18682 value: style.own("fontWeight"), 18683 options: FONT_WEIGHTS, 18684 onChange: (value) => style.set("fontWeight", value) 18685 }) 18686 ] 18687 }),
18688 /* @__PURE__ */ u2(Field, { 18689 label: "Line height", 18690 children: /* @__PURE__ */ u2(NumInput, { 18691 value: style.getNum("lineHeight"), 18692 unit: "px", 18693 step: 1, 18694 onChange: (value) => style.setNum("lineHeight", value) 18695 }) 18696 }), 18697 /* @__PURE__ */ u2(Field, { 18698 label: "Letter space", 18699 children: /* @__PURE__ */ u2(NumInput, { 18700 value: style.getNum("letterSpacing", false), 18701 unit: "px", 18702 step: 0.1, 18703 onChange: (value) => style.setNum("letterSpacing", value) 18704 }) 18705 }), 18706 /* @__PURE__ */ u2(Field, { 18707 label: "Align", 18708 children: /* @__PURE__ */ u2(IconGroup, { 18709 value: style.get("textAlign"), 18710 choices: ALIGN_CHOICES, 18711 onChange: (value) => style.set("textAlign", value) 18712 }) 18713 }), 18714 /* @__PURE__ */ u2(Field, { 18715 label: "Transform", 18716 children: /* @__PURE__ */ u2(SelectInput, { 18717 value: style.own("textTransform"), 18718 options: [ 18719 [ 18720 "", 18721 "Inherit" 18722 ], 18723 [ 18724 "none", 18725 "None" 18726 ], 18727 [ 18728 "uppercase", 18729 "UPPERCASE" 18730 ], 18731 [ 18732 "lowercase", 18733 "lowercase" 18734 ], 18735 [ 18736 "capitalize", 18737 "Capitalize" 18738 ] 18739 ], 18740 onChange: (value) => style.set("textTransform", value) 18741 }) 18742 }) 18743 ] 18744 }), 18745 cur == "bg" && /* @__PURE__ */ u2(Section, { 18746 children: [
18747 /* @__PURE__ */ u2(Field, { 18748 label: "Color", 18749 children: /* @__PURE__ */ u2(ColorInput, { 18750 value: style.own("backgroundColor"), 18751 onChange: (value) => style.set("backgroundColor", value) 18752 }) 18753 }), 18754 /* @__PURE__ */ u2(Field, { 18755 label: "Image", 18756 children: /* @__PURE__ */ u2(MediaUrlInput, { 18757 value: (style.own("backgroundImage").match(/^url\(["']?([^"')]*)/i) ?? [ 18758 "", 18759 "" 18760 ])[1], 18761 placeholder: "https://... (empty for none)", 18762 accept: "image", 18763 onChange: (value) => style.set("backgroundImage", value ? "url(" + encodeURI(value) + ")" : "") 18764 }) 18765 }), 18766 /* @__PURE__ */ u2(Field, { 18767 label: "Fit", 18768 children: /* @__PURE__ */ u2(SelectInput, { 18769 value: style.own("backgroundSize"), 18770 options: [ 18771 [ 18772 "", 18773 "Inherit" 18774 ], 18775 [ 18776 "auto", 18777 "Auto" 18778 ], 18779 [ 18780 "cover", 18781 "Cover" 18782 ], 18783 [ 18784 "contain", 18785 "Contain" 18786 ] 18787 ], 18788 onChange: (value) => style.set("backgroundSize", value) 18789 }) 18790 }) 18791 ] 18792 }), 18793 cur == "box" && /* @__PURE__ */ u2(Section, { 18794 children: [
18795 /* @__PURE__ */ u2(Field, { 18796 label: "Width", 18797 children: [ 18798 /* @__PURE__ */ u2(NumInput, { 18799 value: style.getNum("borderWidth", false), 18800 unit: "px", 18801 min: 0, 18802 onChange: (value) => style.setNum("borderWidth", value) 18803 }), 18804 /* @__PURE__ */ u2(SelectInput, { 18805 value: style.own("borderStyle"), 18806 options: BORDER_STYLES, 18807 onChange: (value) => style.set("borderStyle", value) 18808 }) 18809 ] 18810 }), 18811 /* @__PURE__ */ u2(Field, { 18812 label: "Color", 18813 children: /* @__PURE__ */ u2(ColorInput, { 18814 value: style.own("borderColor"), 18815 onChange: (value) => style.set("borderColor", value) 18816 }) 18817 }), 18818 /* @__PURE__ */ u2(Field, { 18819 label: "Radius", 18820 children: /* @__PURE__ */ u2(NumInput, { 18821 value: style.getNum("borderRadius", false), 18822 unit: "px", 18823 min: 0, 18824 onChange: (value) => style.setNum("borderRadius", value) 18825 }) 18826 }),
18827 /* @__PURE__ */ u2(Field, { 18828 label: "Opacity", 18829 children: /* @__PURE__ */ u2(NumInput, { 18830 value: style.getNum("opacity", false), 18831 unit: "", 18832 min: 0, 18833 max: 1, 18834 step: 0.05, 18835 onChange: (value) => style.set("opacity", value) 18836 }) 18837 }) 18838 ] 18839 }), 18840 cur == "presets" && /* @__PURE__ */ u2(Section, { 18841 children: [ 18842 /* @__PURE__ */ u2("p", { 18843 class: "le-hint", 18844 children: "One tap sets several properties at once." 18845 }), 18846 /* @__PURE__ */ u2("div", { 18847 class: "le-presets", 18848 children: PRESETS.map((preset) => /* @__PURE__ */ u2("button", { 18849 type: "button", 18850 class: "le-preset", 18851 title: preset.title, 18852 onClick: () => style.setMany(preset.style), 18853 children: preset.label 18854 }, preset.label)) 18855 }) 18856 ] 18857 }), 18858 cur == "css" && /* @__PURE__ */ u2(PageCssSection, { 18859 session, 18860 initialRules: pageCssRules 18861 }) 18862 ] 18863 }); 18864} 18865function SpacingPanel(props) { 18866 const style = useStyleEditor(props.session); 18867 const sides = [ 18868 [ 18869 "Top", 18870 "Top" 18871 ], 18872 [ 18873 "Right", 18874 "Right" 18875 ], 18876 [ 18877 "Bottom", 18878 "Bottom" 18879 ], 18880 [ 18881 "Left", 18882 "Left" 18883 ] 18884 ]; 18885 return /* @__PURE__ */ u2(k, { 18886 children: [
18887 /* @__PURE__ */ u2("p", { 18888 class: "le-hint", 18889 children: "Leave a box empty to keep whatever the site's own stylesheet says." 18890 }), 18891 /* @__PURE__ */ u2(Section, { 18892 title: "Margin", 18893 children: sides.map(([side, label]) => /* @__PURE__ */ u2(Field, { 18894 label, 18895 children: /* @__PURE__ */ u2(NumInput, { 18896 value: style.getNum("margin" + side, false), 18897 unit: "px", 18898 step: 1, 18899 onChange: (value) => style.setNum("margin" + side, value) 18900 }) 18901 }, "m" + side)) 18902 }), 18903 /* @__PURE__ */ u2(Section, { 18904 title: "Padding", 18905 children: sides.map(([side, label]) => /* @__PURE__ */ u2(Field, { 18906 label, 18907 children: /* @__PURE__ */ u2(NumInput, { 18908 value: style.getNum("padding" + side, false), 18909 unit: "px", 18910 min: 0, 18911 step: 1, 18912 onChange: (value) => style.setNum("padding" + side, value) 18913 }) 18914 }, "p" + side)) 18915 }) 18916 ] 18917 }); 18918} 18919function AlignmentPanel(props) { 18920 const style = useStyleEditor(props.session); 18921 const display = style.get("display"); 18922 const isFlexish = display.indexOf("flex") != -1 || display.indexOf("grid") != -1; 18923 return /* @__PURE__ */ u2(k, { 18924 children: [
18925 /* @__PURE__ */ u2(Section, { 18926 title: "Text", 18927 children: [ 18928 /* @__PURE__ */ u2(Field, { 18929 label: "Align", 18930 children: /* @__PURE__ */ u2(IconGroup, { 18931 value: style.get("textAlign"), 18932 choices: ALIGN_CHOICES, 18933 onChange: (value) => style.set("textAlign", value) 18934 }) 18935 }), 18936 /* @__PURE__ */ u2(Field, { 18937 label: "Vertical", 18938 children: /* @__PURE__ */ u2(SelectInput, { 18939 value: style.own("verticalAlign"), 18940 options: [ 18941 [ 18942 "", 18943 "Inherit" 18944 ], 18945 [ 18946 "baseline", 18947 "Baseline" 18948 ], 18949 [ 18950 "top", 18951 "Top" 18952 ], 18953 [ 18954 "middle", 18955 "Middle" 18956 ], 18957 [ 18958 "bottom", 18959 "Bottom" 18960 ] 18961 ], 18962 onChange: (value) => style.set("verticalAlign", value) 18963 }) 18964 }) 18965 ] 18966 }),
18967 /* @__PURE__ */ u2(Section, { 18968 title: "Layout", 18969 children: [ 18970 /* @__PURE__ */ u2(Field, { 18971 label: "Display", 18972 children: /* @__PURE__ */ u2(SelectInput, { 18973 value: style.own("display"), 18974 options: [ 18975 [ 18976 "", 18977 "Inherit (" + display + ")" 18978 ], 18979 [ 18980 "block", 18981 "Block" 18982 ], 18983 [ 18984 "inline-block", 18985 "Inline block" 18986 ], 18987 [ 18988 "inline", 18989 "Inline" 18990 ], 18991 [ 18992 "flex", 18993 "Flex" 18994 ], 18995 [ 18996 "inline-flex", 18997 "Inline flex" 18998 ], 18999 [ 19000 "grid", 19001 "Grid" 19002 ], 19003 [ 19004 "none", 19005 "Hidden" 19006 ] 19007 ], 19008 onChange: (value) => style.set("display", value) 19009 }) 19010 }), 19011 isFlexish && /* @__PURE__ */ u2(k, { 19012 children: [
19013 /* @__PURE__ */ u2(Field, { 19014 label: "Direction", 19015 children: /* @__PURE__ */ u2(SelectInput, { 19016 value: style.own("flexDirection"), 19017 options: [ 19018 [ 19019 "", 19020 "Inherit" 19021 ], 19022 [ 19023 "row", 19024 "Row" 19025 ], 19026 [ 19027 "row-reverse", 19028 "Row reversed" 19029 ], 19030 [ 19031 "column", 19032 "Column" 19033 ], 19034 [ 19035 "column-reverse", 19036 "Column reversed" 19037 ] 19038 ], 19039 onChange: (value) => style.set("flexDirection", value) 19040 }) 19041 }), 19042 /* @__PURE__ */ u2(Field, { 19043 label: "Justify", 19044 children: /* @__PURE__ */ u2(SelectInput, { 19045 value: style.own("justifyContent"), 19046 options: [ 19047 [ 19048 "", 19049 "Inherit" 19050 ], 19051 [ 19052 "flex-start", 19053 "Start" 19054 ], 19055 [ 19056 "center", 19057 "Center" 19058 ], 19059 [ 19060 "flex-end", 19061 "End" 19062 ], 19063 [ 19064 "space-between", 19065 "Space between" 19066 ], 19067 [ 19068 "space-around", 19069 "Space around" 19070 ] 19071 ], 19072 onChange: (value) => style.set("justifyContent", value) 19073 }) 19074 }),
19075 /* @__PURE__ */ u2(Field, { 19076 label: "Items", 19077 children: /* @__PURE__ */ u2(SelectInput, { 19078 value: style.own("alignItems"), 19079 options: [ 19080 [ 19081 "", 19082 "Inherit" 19083 ], 19084 [ 19085 "stretch", 19086 "Stretch" 19087 ], 19088 [ 19089 "flex-start", 19090 "Start" 19091 ], 19092 [ 19093 "center", 19094 "Center" 19095 ], 19096 [ 19097 "flex-end", 19098 "End" 19099 ], 19100 [ 19101 "baseline", 19102 "Baseline" 19103 ] 19104 ], 19105 onChange: (value) => style.set("alignItems", value) 19106 }) 19107 }), 19108 /* @__PURE__ */ u2(Field, { 19109 label: "Gap", 19110 children: /* @__PURE__ */ u2(NumInput, { 19111 value: style.getNum("gap", false), 19112 unit: "px", 19113 min: 0, 19114 onChange: (value) => style.setNum("gap", value) 19115 }) 19116 }) 19117 ] 19118 }) 19119 ] 19120 }) 19121 ] 19122 }); 19123} 19124function PositionPanel(props) { 19125 const style = useStyleEditor(props.session); 19126 const position = style.own("position") || style.get("position"); 19127 const isPositioned = position && position != "static"; 19128 return /* @__PURE__ */ u2(k, { 19129 children: /* @__PURE__ */ u2(Section, { 19130 title: "Position", 19131 children: [
19132 /* @__PURE__ */ u2(Field, { 19133 label: "Mode", 19134 children: /* @__PURE__ */ u2(SelectInput, { 19135 value: style.own("position"), 19136 options: [ 19137 [ 19138 "", 19139 "Inherit (" + style.get("position") + ")" 19140 ], 19141 [ 19142 "static", 19143 "Static" 19144 ], 19145 [ 19146 "relative", 19147 "Relative" 19148 ], 19149 [ 19150 "absolute", 19151 "Absolute" 19152 ], 19153 [ 19154 "fixed", 19155 "Fixed" 19156 ], 19157 [ 19158 "sticky", 19159 "Sticky" 19160 ] 19161 ], 19162 onChange: (value) => style.set("position", value) 19163 }) 19164 }), 19165 isPositioned && /* @__PURE__ */ u2(k, { 19166 children: [ 19167 /* @__PURE__ */ u2(Field, { 19168 label: "Top", 19169 children: /* @__PURE__ */ u2(NumInput, { 19170 value: style.getNum("top", false), 19171 unit: "px", 19172 onChange: (value) => style.setNum("top", value) 19173 }) 19174 }),
19175 /* @__PURE__ */ u2(Field, { 19176 label: "Left", 19177 children: /* @__PURE__ */ u2(NumInput, { 19178 value: style.getNum("left", false), 19179 unit: "px", 19180 onChange: (value) => style.setNum("left", value) 19181 }) 19182 }), 19183 /* @__PURE__ */ u2(Field, { 19184 label: "Z-index", 19185 children: /* @__PURE__ */ u2(NumInput, { 19186 value: style.own("zIndex"), 19187 unit: "", 19188 onChange: (value) => style.set("zIndex", value) 19189 }) 19190 }) 19191 ] 19192 }), 19193 /* @__PURE__ */ u2(Field, { 19194 label: "Float", 19195 children: /* @__PURE__ */ u2(SelectInput, { 19196 value: style.own("cssFloat"), 19197 options: [ 19198 [ 19199 "", 19200 "Inherit" 19201 ], 19202 [ 19203 "none", 19204 "None" 19205 ], 19206 [ 19207 "left", 19208 "Left" 19209 ], 19210 [ 19211 "right", 19212 "Right" 19213 ] 19214 ], 19215 onChange: (value) => style.set("cssFloat", value) 19216 }) 19217 }),
19218 /* @__PURE__ */ u2(Field, { 19219 label: "Overflow", 19220 children: /* @__PURE__ */ u2(SelectInput, { 19221 value: style.own("overflow"), 19222 options: [ 19223 [ 19224 "", 19225 "Inherit" 19226 ], 19227 [ 19228 "visible", 19229 "Visible" 19230 ], 19231 [ 19232 "hidden", 19233 "Hidden" 19234 ], 19235 [ 19236 "auto", 19237 "Auto" 19238 ], 19239 [ 19240 "scroll", 19241 "Scroll" 19242 ] 19243 ], 19244 onChange: (value) => style.set("overflow", value) 19245 }) 19246 }) 19247 ] 19248 }) 19249 }); 19250} 19251var DEVICE_BANDS = [ 19252 { 19253 id: "mobile", 19254 label: "Phones", 19255 hint: "up to 767px", 19256 media: "(max-width: 767px)" 19257 }, 19258 { 19259 id: "tablet", 19260 label: "Tablets", 19261 hint: "768 - 1023px", 19262 media: "(min-width: 768px) and (max-width: 1023px)" 19263 }, 19264 { 19265 id: "desktop", 19266 label: "Desktops", 19267 hint: "1024px and wider", 19268 media: "(min-width: 1024px)" 19269 } 19270]; 19271function VisibilityPanel(props) { 19272 const { session } = props; 19273 const [hidden, setHidden] = p2([]); 19274 const selector = session.targetSelector || elemToSelector(session.elem); 19275 const styleElemRef = F2(null); 19276 const changesRef = F2([]); 19277 _2(() => () => { 19278 styleElemRef.current?.remove(); 19279 styleElemRef.current = null; 19280 }, []); 19281 function commit(next) { 19282 setHidden(next); 19283 let css = ""; 19284 for (const band of DEVICE_BANDS) { 19285 if (next.indexOf(band.id) != -1) { 19286 css += "@media " + band.media + "\n{ " + selector + " { display: none !important }\n}\n"; 19287 } 19288 } 19289 styleElemRef.current?.remove(); 19290 styleElemRef.current = null; 19291 if (css) { 19292 const styleElem = document.createElement("style"); 19293 styleElem.setAttribute("data-s-t-css", "1"); 19294 styleElem.textContent = css; 19295 document.head.appendChild(styleElem); 19296 styleElemRef.current = styleElem; 19297 } 19298 for (const change of changesRef.current) { 19299 const i4 = session.extraChanges.indexOf(change); 19300 if (i4 != -1) { 19301 session.extraChanges.splice(i4, 1); 19302 } 19303 } 19304 changesRef.current = !css ? [] : [ 19305 { 19306 qelem_id: "head", 19307 where: "inside", 19308 e: [ 19309 "style", 19310 { 19311 "data-s-t-css": "1" 19312 }, 19313 [ 19314 css 19315 ] 19316 ] 19317 }, 19318 { 19319 qelem_id: "html", 19320 attrs: { 19321 "data-s-t-css": "1" 19322 } 19323 } 19324 ]; 19325 session.extraChanges.push(...changesRef.current); 19326 session.onPreview(); 19327 } 19328 return /* @__PURE__ */ u2(k, { 19329 children: [
19330 /* @__PURE__ */ u2("div", { 19331 class: "le-notice", 19332 children: [ 19333 li.eye, 19334 /* @__PURE__ */ u2("div", { 19335 children: [ 19336 "Hiding is per screen width, so it needs a media query rather than a style on the element - it is written into the page's stylesheet and applies to", 19337 /* @__PURE__ */ u2("code", { 19338 class: "le-selector", 19339 style: "margin:0 0.3em", 19340 children: selector 19341 }), 19342 ", anything else that selector matches included." 19343 ] 19344 }) 19345 ] 19346 }), 19347 /* @__PURE__ */ u2(Section, { 19348 title: "Show this element on", 19349 children: DEVICE_BANDS.map((band) => { 19350 const isHidden = hidden.indexOf(band.id) != -1; 19351 return /* @__PURE__ */ u2(Field, { 19352 label: band.label, 19353 children: [ 19354 /* @__PURE__ */ u2("label", { 19355 class: "le-btn" + (isHidden ? "" : " le-primary"), 19356 children: [
19357 /* @__PURE__ */ u2("input", { 19358 type: "checkbox", 19359 checked: !isHidden, 19360 onChange: (ev) => commit(ev.target.checked ? hidden.filter((v3) => v3 != band.id) : hidden.concat(band.id)) 19361 }), 19362 isHidden ? "Hidden" : "Shown" 19363 ] 19364 }), 19365 /* @__PURE__ */ u2("span", { 19366 class: "le-hint", 19367 style: "margin:0", 19368 children: band.hint 19369 }) 19370 ] 19371 }, band.id); 19372 }) 19373 }), 19374 hidden.length == DEVICE_BANDS.length && /* @__PURE__ */ u2("div", { 19375 class: "le-notice le-warn", 19376 children: [ 19377 li.warn, 19378 /* @__PURE__ */ u2("div", { 19379 children: [ 19380 "Hidden on every width - no visitor will see it. ", 19381 /* @__PURE__ */ u2("b", { 19382 children: "Delete" 19383 }), 19384 " says that more plainly." 19385 ] 19386 }) 19387 ] 19388 }) 19389 ] 19390 }); 19391} 19392function PageCssSection(props) { 19393 const { session } = props; 19394 const [rules, setRules] = p2(props.initialRules); 19395 const styleElemRef = F2(null); 19396 const changesRef = F2([]); 19397 _2(() => { 19398 setRules(props.initialRules); 19399 return () => { 19400 styleElemRef.current?.remove(); 19401 styleElemRef.current = null; 19402 }; 19403 }, [ 19404 session.elem 19405 ]); 19406 function commit(next) { 19407 setRules(next); 19408 let css = ""; 19409 for (const rule of next) { 19410 const diff = diffDeclarations(rule.orig, rule.text); 19411 if (diff.length) { 19412 css += rule.selectorText + "\n{ " + diff.join(";\n ") + "\n}\n"; 19413 } 19414 } 19415 styleElemRef.current?.remove(); 19416 styleElemRef.current = null; 19417 if (css) { 19418 const styleElem = document.createElement("style"); 19419 styleElem.setAttribute("data-s-t-css", "1"); 19420 styleElem.textContent = css; 19421 document.head.appendChild(styleElem); 19422 styleElemRef.current = styleElem; 19423 } 19424 for (const change of changesRef.current) { 19425 const i4 = session.extraChanges.indexOf(change); 19426 if (i4 != -1) { 19427 session.extraChanges.splice(i4, 1); 19428 } 19429 } 19430 changesRef.current = !css ? [] : [ 19431 // "inside" anyway, but the editor's own applier reads it as "next to", which would have 19432 // previewed the stylesheet outside <head>. 19433 { 19434 qelem_id: "head", 19435 where: "inside", 19436 e: [ 19437 "style", 19438 { 19439 "data-s-t-css": "1" 19440 }, 19441 [ 19442 css 19443 ] 19444 ] 19445 }, 19446 { 19447 qelem_id: "html", 19448 attrs: { 19449 "data-s-t-css": "1" 19450 } 19451 } 19452 ]; 19453 session.extraChanges.push(...changesRef.current); 19454 session.onPreview(); 19455 } 19456 if (rules.length == 0) { 19457 return null; 19458 } 19459 return /* @__PURE__ */ u2(Section, { 19460 title: "Page CSS rules", 19461 children: [
19462 /* @__PURE__ */ u2("p", { 19463 class: "le-hint", 19464 children: "Rules from the site's own stylesheets that match this element. Editing one here adds an override for the whole page - it can affect other elements the rule matches." 19465 }), 19466 rules.map((rule, i4) => /* @__PURE__ */ u2("div", { 19467 style: "margin-bottom:0.6em", 19468 children: [ 19469 /* @__PURE__ */ u2("code", { 19470 class: "le-selector", 19471 title: rule.selectorText, 19472 children: rule.selectorText 19473 }), 19474 /* @__PURE__ */ u2("textarea", { 19475 class: "le-area le-mono", 19476 rows: Math.min(8, rule.text.split(";").length + 1), 19477 value: rule.text, 19478 onInput: (ev) => { 19479 const next = rules.slice(); 19480 next[i4] = { 19481 ...rule, 19482 text: ev.target.value 19483 }; 19484 commit(next); 19485 } 19486 }) 19487 ] 19488 }, rule.selectorText)) 19489 ] 19490 }); 19491} 19492function matchingRules(elem) { 19493 const found = /* @__PURE__ */ new Map(); 19494 for (const sheet of document.styleSheets) { 19495 try { 19496 for (const rule of sheet.cssRules) { 19497 const styleRule = rule; 19498 if (styleRule.selectorText && elem.matches(styleRule.selectorText)) { 19499 const selectorText = styleRule.selectorText.trim(); 19500 const list = found.get(selectorText); 19501 if (list) { 19502 list.push(styleRule.style.cssText); 19503 } else { 19504 found.set(selectorText, [ 19505 styleRule.style.cssText 19506 ]); 19507 } 19508 } 19509 } 19510 } catch { 19511 } 19512 } 19513 const rules = new Array(); 19514 for (const [selectorText, list] of found) { 19515 const text = list.join(";\n"); 19516 rules.push({ 19517 selectorText, 19518 orig: text, 19519 text 19520 }); 19521 } 19522 return rules; 19523} 19524function diffDeclarations(orig, text) { 19525 const before = parseStyleAttr(orig); 19526 const after = parseStyleAttr(text); 19527 const diff = new Array(); 19528 for (const name in after) { 19529 if (before[name] != after[name]) { 19530 diff.push(name + ":" + after[name]); 19531 } 19532 } 19533 return diff; 19534} 19535function resetInlineStyle(session) { 19536 session.elem.removeAttribute("style"); 19537 session.onPreview(); 19538} 19539
19540// src/components/live/panels/PropsPanels.tsx 19541var COMMON_ATTRS = [ 19542 { 19543 name: "title", 19544 label: "Tooltip", 19545 placeholder: "Shown on hover" 19546 }, 19547 { 19548 name: "id", 19549 label: "Id" 19550 }, 19551 { 19552 name: "class", 19553 label: "Classes" 19554 } 19555]; 19556var ATTRS_BY_TAG = { 19557 a: [ 19558 { 19559 name: "href", 19560 label: "URL (href)", 19561 placeholder: "https://...", 19562 url: true 19563 }, 19564 { 19565 name: "target", 19566 label: "Opens in", 19567 options: [ 19568 [ 19569 "", 19570 "Same tab" 19571 ], 19572 [ 19573 "_blank", 19574 "New tab" 19575 ], 19576 [ 19577 "_parent", 19578 "Parent frame" 19579 ], 19580 [ 19581 "_top", 19582 "Top frame" 19583 ] 19584 ] 19585 }, 19586 { 19587 name: "rel", 19588 label: "Rel", 19589 placeholder: "nofollow, noopener..." 19590 } 19591 ], 19592 img: [ 19593 { 19594 name: "src", 19595 label: "URL", 19596 placeholder: "https://...", 19597 url: true 19598 }, 19599 { 19600 name: "alt", 19601 label: "Alt text", 19602 placeholder: "What the image shows" 19603 }, 19604 { 19605 name: "width", 19606 label: "Width" 19607 }, 19608 { 19609 name: "height", 19610 label: "Height" 19611 }, 19612 { 19613 name: "loading", 19614 label: "Loading", 19615 options: [ 19616 [ 19617 "", 19618 "Default" 19619 ], 19620 [ 19621 "lazy", 19622 "Lazy" 19623 ], 19624 [ 19625 "eager", 19626 "Eager" 19627 ] 19628 ] 19629 } 19630 ], 19631 input: [ 19632 { 19633 name: "name", 19634 label: "Name" 19635 }, 19636 { 19637 name: "placeholder", 19638 label: "Placeholder" 19639 }, 19640 { 19641 name: "required", 19642 label: "Required", 19643 boolean: true 19644 }, 19645 { 19646 name: "disabled", 19647 label: "Disabled", 19648 boolean: true 19649 } 19650 ], 19651 textarea: [ 19652 { 19653 name: "name", 19654 label: "Name" 19655 }, 19656 { 19657 name: "placeholder", 19658 label: "Placeholder" 19659 }, 19660 { 19661 name: "required", 19662 label: "Required", 19663 boolean: true 19664 }, 19665 { 19666 name: "disabled", 19667 label: "Disabled", 19668 boolean: true 19669 } 19670 ], 19671 select: [ 19672 { 19673 name: "name", 19674 label: "Name" 19675 }, 19676 { 19677 name: "required", 19678 label: "Required", 19679 boolean: true 19680 }, 19681 { 19682 name: "disabled", 19683 label: "Disabled", 19684 boolean: true 19685 } 19686 ], 19687 button: [ 19688 { 19689 name: "type", 19690 label: "Type", 19691 options: [ 19692 [ 19693 "", 19694 "Default" 19695 ], 19696 [ 19697 "button", 19698 "Button" 19699 ], 19700 [ 19701 "submit", 19702 "Submit" 19703 ], 19704 [ 19705 "reset", 19706 "Reset" 19707 ] 19708 ] 19709 }, 19710 { 19711 name: "disabled", 19712 label: "Disabled", 19713 boolean: true 19714 } 19715 ], 19716 form: [ 19717 { 19718 name: "action", 19719 label: "Posts to", 19720 placeholder: "https://...", 19721 url: true 19722 }, 19723 { 19724 name: "method", 19725 label: "Method", 19726 options: [ 19727 [ 19728 "", 19729 "Default" 19730 ], 19731 [ 19732 "get", 19733 "GET" 19734 ], 19735 [ 19736 "post", 19737 "POST" 19738 ] 19739 ] 19740 }, 19741 { 19742 name: "target", 19743 label: "Opens in", 19744 options: [ 19745 [ 19746 "", 19747 "Same tab" 19748 ], 19749 [ 19750 "_blank", 19751 "New tab" 19752 ] 19753 ] 19754 } 19755 ], 19756 iframe: [ 19757 { 19758 name: "src", 19759 label: "URL", 19760 placeholder: "https://...", 19761 url: true 19762 }, 19763 { 19764 name: "width", 19765 label: "Width" 19766 }, 19767 { 19768 name: "height", 19769 label: "Height" 19770 } 19771 ], 19772 video: [ 19773 { 19774 name: "src", 19775 label: "URL", 19776 placeholder: "https://..." 19777 }, 19778 { 19779 name: "poster", 19780 label: "Poster", 19781 placeholder: "https://..." 19782 }, 19783 { 19784 name: "autoplay", 19785 label: "Autoplay", 19786 boolean: true 19787 }, 19788 { 19789 name: "controls", 19790 label: "Controls", 19791 boolean: true 19792 }, 19793 { 19794 name: "loop", 19795 label: "Loop", 19796 boolean: true 19797 }, 19798 { 19799 name: "muted", 19800 label: "Muted", 19801 boolean: true 19802 } 19803 ], 19804 audio: [ 19805 { 19806 name: "src", 19807 label: "URL", 19808 placeholder: "https://..." 19809 }, 19810 { 19811 name: "autoplay", 19812 label: "Autoplay", 19813 boolean: true 19814 }, 19815 { 19816 name: "controls", 19817 label: "Controls", 19818 boolean: true 19819 }, 19820 { 19821 name: "loop", 19822 label: "Loop", 19823 boolean: true 19824 } 19825 ] 19826}; 19827function PropsPanel(props) { 19828 const { session } = props; 19829 const [, setVersion] = p2(0); 19830 const elem = session.elem; 19831 const specs = [ 19832 ...ATTRS_BY_TAG[elem.localName] ?? [], 19833 ...COMMON_ATTRS 19834 ]; 19835 function setAttr(name, value) { 19836 if (!value) { 19837 elem.removeAttribute(name); 19838 } else {
19839 try { 19840 elem.setAttribute(name, value); 19841 } catch (e3) { 19842 console.error(e3); 19843 } 19844 } 19845 setVersion((v3) => v3 + 1); 19846 session.onPreview(); 19847 } 19848 const known = new Set(specs.map((v3) => v3.name)); 19849 const others = Array.from(elem.attributes).map((v3) => v3.name).filter((name) => !known.has(name) && name != "style" && name != "contenteditable"); 19850 return /* @__PURE__ */ u2(k, { 19851 children: [ 19852 /* @__PURE__ */ u2(Section, { 19853 title: elem.localName.toUpperCase() + " attributes", 19854 children: specs.map((spec) => /* @__PURE__ */ u2(Field, { 19855 label: spec.label, 19856 children: spec.boolean ? /* @__PURE__ */ u2("label", { 19857 class: "le-btn", 19858 children: [ 19859 /* @__PURE__ */ u2("input", { 19860 type: "checkbox", 19861 checked: elem.hasAttribute(spec.name), 19862 onChange: (ev) => setAttr(spec.name, ev.target.checked ? spec.name : "") 19863 }), 19864 elem.hasAttribute(spec.name) ? "Yes" : "No" 19865 ] 19866 }) : spec.options ? /* @__PURE__ */ u2(SelectInput, { 19867 value: elem.getAttribute(spec.name) ?? "", 19868 options: spec.options, 19869 onChange: (value) => setAttr(spec.name, value) 19870 }) : /* @__PURE__ */ u2(TextInput, { 19871 value: elem.getAttribute(spec.name) ?? "", 19872 placeholder: spec.placeholder, 19873 onChange: (value) => setAttr(spec.name, spec.url ? withScheme(value) : value) 19874 }) 19875 }, spec.name)) 19876 }), 19877 others.length > 0 && /* @__PURE__ */ u2(Section, { 19878 title: "Other attributes", 19879 children: others.map((name) => /* @__PURE__ */ u2(Field, { 19880 label: name, 19881 children: /* @__PURE__ */ u2(TextInput, { 19882 value: elem.getAttribute(name) ?? "", 19883 onChange: (value) => setAttr(name, value) 19884 }) 19885 }, name)) 19886 }) 19887 ] 19888 }); 19889} 19890function ReplaceImagePanel(props) { 19891 const { session } = props; 19892 const elem = session.elem; 19893 const [url, setUrl] = p2(() => { 19894 const current = imageUrlOf(elem); 19895 return current && getTracker()?.url_from_proxy?.(current) || current; 19896 }); 19897 return /* @__PURE__ */ u2(k, { 19898 children: [
19899 /* @__PURE__ */ u2("div", { 19900 class: "le-notice", 19901 children: [ 19902 li.image, 19903 /* @__PURE__ */ u2("div", { 19904 children: "Paste the address of the new image, or pick one from this account's media library. Uploading new files is done in the Personyze panel." 19905 }) 19906 ] 19907 }), 19908 /* @__PURE__ */ u2(Section, { 19909 title: "Image URL", 19910 children: /* @__PURE__ */ u2(MediaUrlInput, { 19911 value: url, 19912 placeholder: "https://...", 19913 accept: "image", 19914 onChange: (value) => { 19915 setUrl(value); 19916 if (elem.localName == "img") { 19917 elem.setAttribute("src", value); 19918 } else { 19919 elem.style.backgroundImage = value ? "url(" + encodeURI(value) + ")" : ""; 19920 } 19921 session.onPreview(); 19922 } 19923 }) 19924 }), 19925 url && /* @__PURE__ */ u2(Section, { 19926 title: "Preview", 19927 children: /* @__PURE__ */ u2("img", { 19928 src: url, 19929 alt: "", 19930 style: "max-width:100%; max-height:10em; border-radius:0.45em" 19931 }) 19932 }) 19933 ] 19934 }); 19935} 19936
19937// src/components/live/panels/MovePanel.tsx 19938var RE_ROTATE = /(^|,)\s*rotate\(([+-]?(?:\d+(?:\.\d*)?|\.\d+))([a-z]+)\)/i; 19939function parseAngle(transform) { 19940 const m2 = (transform || "").match(RE_ROTATE); 19941 return m2 ? parseFloat(m2[2]) : 0; 19942} 19943function withAngle(transform, angle) { 19944 const rotate = "rotate(" + angle + "deg)"; 19945 const text = (transform || "").trim(); 19946 const m2 = text.match(RE_ROTATE); 19947 if (m2) { 19948 return text.slice(0, m2.index) + m2[1] + rotate + text.slice(m2.index + m2[0].length); 19949 } 19950 return !text ? rotate : text + "," + rotate; 19951} 19952function MovePanel(props) { 19953 const { session } = props; 19954 const elem = session.elem; 19955 const { canResize } = dragCapability(elem); 19956 const [, setVersion] = p2(0); 19957 const [moveTo, setMoveTo] = p2(null); 19958 const gripRef = F2(null); 19959 _2(() => { 19960 const origCursor = elem.style.cursor; 19961 const origPosition = elem.style.position; 19962 elem.style.cursor = "move"; 19963 if (getComputedStyle(elem).position == "static") { 19964 elem.style.position = "relative"; 19965 } 19966 let drag = null; 19967 const onDown = (ev) => { 19968 if (ev.button != 0) { 19969 return; 19970 } 19971 ev.preventDefault(); 19972 ev.stopPropagation(); 19973 drag = { 19974 startX: ev.clientX, 19975 startY: ev.clientY, 19976 left: parseFloat(getComputedStyle(elem).left) || 0, 19977 top: parseFloat(getComputedStyle(elem).top) || 0 19978 }; 19979 }; 19980 const onMove = (ev) => { 19981 if (drag) { 19982 ev.preventDefault(); 19983 elem.style.left = drag.left + ev.clientX - drag.startX + "px"; 19984 elem.style.top = drag.top + ev.clientY - drag.startY + "px"; 19985 setVersion((v3) => v3 + 1); 19986 session.onPreview(); 19987 } 19988 }; 19989 const onUp = () => { 19990 drag = null; 19991 }; 19992 elem.addEventListener("mousedown", onDown, true); 19993 document.addEventListener("mousemove", onMove, true); 19994 document.addEventListener("mouseup", onUp, true); 19995 return () => { 19996 elem.removeEventListener("mousedown", onDown, true); 19997 document.removeEventListener("mousemove", onMove, true); 19998 document.removeEventListener("mouseup", onUp, true); 19999 elem.style.cursor = origCursor; 20000 if (!elem.style.left && !elem.style.top && origPosition != elem.style.position) { 20001 elem.style.position = origPosition; 20002 } 20003 }; 20004 }, [ 20005 elem 20006 ]); 20007 _2(() => { 20008 if (!canResize) { 20009 return; 20010 } 20011 const grip = document.createElement("div"); 20012 grip.style.cssText = "position:absolute; z-index:2147483645; width:14px; height:14px; box-sizing:border-box; border:2px solid #12B886; border-radius:3px; background-color:#0B1220; cursor:nwse-resize"; 20013 document.body.appendChild(grip); 20014 gripRef.current = grip; 20015 const place = () => { 20016 const rect = elem.getBoundingClientRect(); 20017 grip.style.left = rect.right + globalThis.scrollX - 7 + "px"; 20018 grip.style.top = rect.bottom + globalThis.scrollY - 7 + "px"; 20019 }; 20020 place(); 20021 let drag = null; 20022 const onDown = (ev) => { 20023 ev.preventDefault(); 20024 ev.stopPropagation(); 20025 const rect = elem.getBoundingClientRect(); 20026 drag = { 20027 startX: ev.clientX, 20028 startY: ev.clientY, 20029 width: rect.width, 20030 height: rect.height 20031 }; 20032 }; 20033 const onMove = (ev) => { 20034 if (drag) { 20035 ev.preventDefault(); 20036 elem.style.width = Math.max(1, drag.width + ev.clientX - drag.startX) + "px"; 20037 elem.style.height = Math.max(1, drag.height + ev.clientY - drag.startY) + "px"; 20038 place(); 20039 setVersion((v3) => v3 + 1); 20040 session.onPreview(); 20041 } 20042 }; 20043 const onUp = () => { 20044 drag = null; 20045 }; 20046 grip.addEventListener("mousedown", onDown, true); 20047 document.addEventListener("mousemove", onMove, true); 20048 document.addEventListener("mouseup", onUp, true); 20049 const timer = setInterval(place, 300); 20050 globalThis.addEventListener("scroll", place, true); 20051 globalThis.addEventListener("resize", place); 20052 return () => { 20053 clearInterval(timer); 20054 globalThis.removeEventListener("scroll", place, true); 20055 globalThis.removeEventListener("resize", place); 20056 document.removeEventListener("mousemove", onMove, true); 20057 document.removeEventListener("mouseup", onUp, true); 20058 grip.remove(); 20059 gripRef.current = null; 20060 }; 20061 }, [ 20062 elem, 20063 canResize 20064 ]); 20065 function setStyle(name, value) { 20066 elem.style[name] = value; 20067 setVersion((v3) => v3 + 1); 20068 session.onPreview(); 20069 } 20070 function num(name) { 20071 const value = elem.style[name]; 20072 const n2 = parseFloat(value); 20073 return isNaN(n2) ? "" : String(Math.round(n2 * 100) / 100); 20074 } 20075 function chooseMoveTo(target, where) { 20076 session.extraChanges.length = 0; 20077 if (!target) { 20078 setMoveTo(null); 20079 return; 20080 } 20081 setMoveTo({ 20082 elem: target, 20083 where 20084 }); 20085 session.extraChanges.push({ 20086 mv: where, 20087 mv_qelem_id: elemToSelector(target) 20088 }); 20089 } 20090 return /* @__PURE__ */ u2(k, { 20091 children: [
20092 /* @__PURE__ */ u2("div", { 20093 class: "le-notice", 20094 children: [ 20095 li.move, 20096 /* @__PURE__ */ u2("div", { 20097 children: [ 20098 "Drag the element on the page", 20099 canResize && ", or pull the green corner grip to resize it", 20100 ". The numbers below follow along." 20101 ] 20102 }) 20103 ] 20104 }), 20105 /* @__PURE__ */ u2(Section, { 20106 title: "Offset", 20107 children: [ 20108 /* @__PURE__ */ u2(Field, { 20109 label: "Left", 20110 children: /* @__PURE__ */ u2(NumInput, { 20111 value: num("left"), 20112 unit: "px", 20113 onChange: (value) => setStyle("left", value === "" ? "" : value + "px") 20114 }) 20115 }), 20116 /* @__PURE__ */ u2(Field, { 20117 label: "Top", 20118 children: /* @__PURE__ */ u2(NumInput, { 20119 value: num("top"), 20120 unit: "px", 20121 onChange: (value) => setStyle("top", value === "" ? "" : value + "px") 20122 }) 20123 }),
20124 /* @__PURE__ */ u2(Field, { 20125 label: "Position", 20126 children: /* @__PURE__ */ u2(SelectInput, { 20127 value: elem.style.position || "", 20128 options: [ 20129 [ 20130 "", 20131 "Inherit" 20132 ], 20133 [ 20134 "relative", 20135 "Relative" 20136 ], 20137 [ 20138 "absolute", 20139 "Absolute" 20140 ], 20141 [ 20142 "fixed", 20143 "Fixed" 20144 ] 20145 ], 20146 onChange: (value) => setStyle("position", value) 20147 }) 20148 }) 20149 ] 20150 }), 20151 canResize && /* @__PURE__ */ u2(Section, { 20152 title: "Size", 20153 children: [ 20154 /* @__PURE__ */ u2(Field, { 20155 label: "Width", 20156 children: /* @__PURE__ */ u2(NumInput, { 20157 value: num("width"), 20158 unit: "px", 20159 min: 0, 20160 onChange: (value) => setStyle("width", value === "" ? "" : value + "px") 20161 }) 20162 }),
20163 /* @__PURE__ */ u2(Field, { 20164 label: "Height", 20165 children: /* @__PURE__ */ u2(NumInput, { 20166 value: num("height"), 20167 unit: "px", 20168 min: 0, 20169 onChange: (value) => setStyle("height", value === "" ? "" : value + "px") 20170 }) 20171 }) 20172 ] 20173 }), 20174 /* @__PURE__ */ u2(Section, { 20175 title: "Rotate", 20176 children: /* @__PURE__ */ u2(Field, { 20177 label: "Angle", 20178 children: /* @__PURE__ */ u2(NumInput, { 20179 value: String(parseAngle(elem.style.transform)), 20180 unit: "deg", 20181 min: -360, 20182 max: 360, 20183 onChange: (value) => setStyle("transform", withAngle(elem.style.transform, parseFloat(value) || 0)) 20184 }) 20185 }) 20186 }), 20187 /* @__PURE__ */ u2(Section, { 20188 title: "Move somewhere else", 20189 children: [ 20190 /* @__PURE__ */ u2("p", { 20191 class: "le-hint", 20192 children: "Puts the element in another place in the page. This is recorded as a move, so it happens for the visitor too - not just here." 20193 }), 20194 !moveTo ? /* @__PURE__ */ u2("button", { 20195 type: "button", 20196 class: "le-btn", 20197 onClick: () => props.onPickElement((target) => chooseMoveTo(target, "inside")), 20198 children: [ 20199 li.cursor, 20200 " Pick the destination on the page" 20201 ] 20202 }) : /* @__PURE__ */ u2(k, { 20203 children: [
20204 /* @__PURE__ */ u2("div", { 20205 class: "le-target", 20206 children: /* @__PURE__ */ u2("code", { 20207 title: describeElem(moveTo.elem), 20208 children: describeElem(moveTo.elem) 20209 }) 20210 }), 20211 /* @__PURE__ */ u2(Field, { 20212 label: "Put", 20213 children: /* @__PURE__ */ u2(SelectInput, { 20214 value: moveTo.where, 20215 options: [ 20216 [ 20217 "inside", 20218 "Inside it" 20219 ], 20220 [ 20221 "before", 20222 "Before it" 20223 ], 20224 [ 20225 "after", 20226 "After it" 20227 ] 20228 ], 20229 onChange: (value) => chooseMoveTo(moveTo.elem, value) 20230 }) 20231 }), 20232 /* @__PURE__ */ u2("button", { 20233 type: "button", 20234 class: "le-btn le-danger", 20235 onClick: () => chooseMoveTo(null, "inside"), 20236 children: "Cancel the move" 20237 }) 20238 ] 20239 }) 20240 ] 20241 }) 20242 ] 20243 }); 20244} 20245
20246// src/components/live/panels/InsertPanel.tsx 20247function autoMediaType(src) { 20248 const m2 = src.match(/.\.(\w{1,8})$/); 20249 switch (m2?.[1]?.toLowerCase()) { 20250 case "jpg": 20251 case "jpeg": 20252 case "png": 20253 case "gif": 20254 case "webp": 20255 case "svg": 20256 case "tiff": 20257 return "img"; 20258 case "mpg": 20259 case "mp4": 20260 case "webm": 20261 case "ogv": 20262 return "video"; 20263 case "mp3": 20264 case "ogg": 20265 case "wav": 20266 return "audio"; 20267 case "swf": 20268 return "embed"; 20269 } 20270 return null; 20271} 20272var MEDIA_TYPES = [ 20273 [ 20274 "auto", 20275 "Detect from the URL" 20276 ], 20277 [ 20278 "img", 20279 "Image" 20280 ], 20281 [ 20282 "video", 20283 "Video" 20284 ], 20285 [ 20286 "audio", 20287 "Audio" 20288 ], 20289 [ 20290 "embed", 20291 "Embedded plugin" 20292 ] 20293]; 20294function InsertPanel(props) { 20295 const { session, kind } = props; 20296 const canPut = canPutElem("span", session.elem); 20297 const wheres = []; 20298 if (canPut.aside) { 20299 wheres.push([ 20300 "before", 20301 "Before", 20302 "Above the element you picked, as its sibling" 20303 ]); 20304 } 20305 if (canPut.inside) { 20306 wheres.push([ 20307 "inside", 20308 "Inside", 20309 "Within the element you picked, at its end" 20310 ]); 20311 } 20312 if (canPut.aside) { 20313 wheres.push([ 20314 "after", 20315 "After", 20316 "Below the element you picked, as its sibling" 20317 ]); 20318 } 20319 const [where, setWhere] = p2(wheres[0]?.[0] ?? "inside"); 20320 const [src, setSrc] = p2(""); 20321 const [href, setHref] = p2(""); 20322 const [text, setText] = p2(kind == "link" ? "Click here" : kind == "button" ? "Click here" : ""); 20323 const [html, setHtml] = p2(""); 20324 const [width, setWidth] = p2(""); 20325 const [height, setHeight] = p2(""); 20326 const [mediaType, setMediaType] = p2("auto"); 20327 const [isMessageBox, setIsMessageBox] = p2(false); 20328 const entry = WIDGET_CATALOGUE.find((e3) => e3.id == props.widgetId); 20329 const [widgetProps, setWidgetProps] = p2(() => ({ 20330 ...entry?.props ?? {} 20331 })); 20332 const [widgetHtml, setWidgetHtml] = p2(entry?.html ?? ""); 20333 const resolvedMedia = mediaType != "auto" ? mediaType : autoMediaType(src); 20334 _2(() => { 20335 session.extraChanges.length = 0; 20336 const spec = buildSpec(); 20337 if (spec) { 20338 session.extraChanges.push({ 20339 where, 20340 e: spec 20341 }); 20342 } 20343 }); 20344 function buildSpec() { 20345 switch (kind) { 20346 case "link": 20347 return !href && !text ? null : [ 20348 "a", 20349 { 20350 href 20351 }, 20352 [ 20353 text 20354 ] 20355 ]; 20356 case "button": 20357 return !text ? null : [ 20358 "button", 20359 { 20360 type: "button" 20361 }, 20362 [ 20363 text 20364 ] 20365 ]; 20366 case "block": 20367 if (!html) { 20368 return null; 20369 } 20370 return isMessageBox ? [ 20371 "div", 20372 { 20373 "class": "$personyze_message_box" 20374 }, 20375 null, 20376 html + '<div><button class="message-box-close">Close</button></div>' 20377 ] : [ 20378 "div", 20379 null, 20380 null, 20381 html 20382 ]; 20383 case "html": 20384 return !html ? null : [ 20385 "div", 20386 null, 20387 null, 20388 html 20389 ]; 20390 case "widget": { 20391 const w3 = entry; 20392 return !w3 ? null : [ 20393 "div", 20394 { 20395 "data-pz-widget": w3.id, 20396 "data-pz-props": JSON.stringify(widgetProps) 20397 }, 20398 null, 20399 widgetHtml || w3.html 20400 ]; 20401 } 20402 case "image": 20403 case "video": { 20404 if (!src) { 20405 return null; 20406 } 20407 const type = resolvedMedia || (kind == "image" ? "img" : "video"); 20408 const attrs = { 20409 src 20410 }; 20411 if (parseInt(width)) { 20412 attrs.width = String(parseInt(width)); 20413 } 20414 if (parseInt(height)) { 20415 attrs.height = String(parseInt(height)); 20416 } 20417 if (type == "embed") { 20418 attrs.type = "application/x-shockwave-flash"; 20419 } 20420 if (type == "img" && href) { 20421 attrs.href = href; 20422 } 20423 return [ 20424 type, 20425 attrs 20426 ]; 20427 } 20428 } 20429 return null; 20430 } 20431 const whereSection = /* @__PURE__ */ u2(Section, { 20432 title: "Where", 20433 children: [
20434 /* @__PURE__ */ u2(Field, { 20435 label: "Put it", 20436 children: /* @__PURE__ */ u2("div", { 20437 class: "le-seg", 20438 children: wheres.map(([id, label, hint]) => /* @__PURE__ */ u2("button", { 20439 type: "button", 20440 class: "le-seg-btn" + (where == id ? " le-on" : ""), 20441 "data-tooltip": hint, 20442 "data-tooltip-pos": "top", 20443 "aria-pressed": where == id, 20444 onClick: () => setWhere(id), 20445 children: [ 20446 li[id == "before" ? "putBefore" : id == "after" ? "putAfter" : "putInside"], 20447 label 20448 ] 20449 }, id)) 20450 }) 20451 }), 20452 /* @__PURE__ */ u2("div", { 20453 class: "le-field le-where-scope", 20454 children: [ 20455 /* @__PURE__ */ u2(ScopeLabel, {}), 20456 /* @__PURE__ */ u2("div", { 20457 class: "le-field-group", 20458 children: /* @__PURE__ */ u2(ScopeSeg, { 20459 value: props.scope, 20460 onChange: props.onScope 20461 }) 20462 }) 20463 ] 20464 }),
20465 /* @__PURE__ */ u2("div", { 20466 class: "le-field le-where-scope", 20467 children: [ 20468 /* @__PURE__ */ u2(ShowOnLabel, {}), 20469 /* @__PURE__ */ u2("div", { 20470 class: "le-field-group", 20471 children: /* @__PURE__ */ u2(DeviceSeg, { 20472 value: props.device, 20473 onChange: props.onDevice 20474 }) 20475 }) 20476 ] 20477 }) 20478 ] 20479 }); 20480 if (wheres.length == 0) { 20481 return /* @__PURE__ */ u2("div", { 20482 class: "le-notice le-warn", 20483 children: [ 20484 li.warn, 20485 /* @__PURE__ */ u2("div", { 20486 children: "Nothing can be added next to this element - HTML does not allow it here. Select its parent and try again." 20487 }) 20488 ] 20489 }); 20490 } 20491 return /* @__PURE__ */ u2(k, { 20492 children: [ 20493 kind != "widget" && whereSection, 20494 (kind == "image" || kind == "video") && /* @__PURE__ */ u2(Section, { 20495 title: kind == "image" ? "Image" : "Video", 20496 children: [ 20497 /* @__PURE__ */ u2(Field, { 20498 label: "URL", 20499 children: /* @__PURE__ */ u2(MediaUrlInput, { 20500 value: src, 20501 placeholder: "https://...", 20502 accept: kind == "image" ? "image" : "any", 20503 onChange: setSrc 20504 }) 20505 }),
20506 /* @__PURE__ */ u2(Field, { 20507 label: "Type", 20508 children: /* @__PURE__ */ u2(SelectInput, { 20509 value: mediaType, 20510 options: MEDIA_TYPES, 20511 onChange: setMediaType 20512 }) 20513 }), 20514 mediaType == "auto" && /* @__PURE__ */ u2("p", { 20515 class: "le-hint", 20516 children: resolvedMedia ? "Detected: " + (MEDIA_TYPES.find((v3) => v3[0] == resolvedMedia)?.[1] ?? resolvedMedia) : "Couldn't tell the type from the URL - please choose it." 20517 }), 20518 /* @__PURE__ */ u2(Field, { 20519 label: "Width", 20520 children: /* @__PURE__ */ u2(TextInput, { 20521 value: width, 20522 placeholder: "auto", 20523 onChange: setWidth 20524 }) 20525 }), 20526 /* @__PURE__ */ u2(Field, { 20527 label: "Height", 20528 children: /* @__PURE__ */ u2(TextInput, { 20529 value: height, 20530 placeholder: resolvedMedia == "img" ? "auto" : "please specify", 20531 onChange: setHeight 20532 }) 20533 }), 20534 resolvedMedia == "img" && /* @__PURE__ */ u2(Field, { 20535 label: "Links to", 20536 children: /* @__PURE__ */ u2(TextInput, { 20537 value: href, 20538 placeholder: "https://... (optional)", 20539 onChange: setHref 20540 }) 20541 }) 20542 ] 20543 }), 20544 kind == "widget" && entry && /* @__PURE__ */ u2(Section, { 20545 title: entry.label, 20546 children: [
20547 /* @__PURE__ */ u2(WidgetPanel, { 20548 entry, 20549 values: widgetProps, 20550 onChange: (key, value) => { 20551 const next = { 20552 ...widgetProps, 20553 [key]: value 20554 }; 20555 setWidgetProps(next); 20556 if (props.session.report) { 20557 void requestWidgetHtml(props.session.report, entry.id, next).then((html2) => { 20558 if (html2 != null) { 20559 setWidgetHtml(html2); 20560 } 20561 }); 20562 } 20563 } 20564 }), 20565 widgetHtml === "" && /* @__PURE__ */ u2("div", { 20566 class: "le-wl-note", 20567 children: "Nothing to show yet - this one needs content before it appears on the page." 20568 }), 20569 !props.session.report && /* @__PURE__ */ u2("div", { 20570 class: "le-wl-note", 20571 children: "The Personyze window is not reachable, so these settings cannot be redrawn. Reopen the editor from the panel to change them." 20572 }) 20573 ] 20574 }), 20575 kind == "link" && /* @__PURE__ */ u2(Section, { 20576 title: "Link", 20577 children: [
20578 /* @__PURE__ */ u2(Field, { 20579 label: "Text", 20580 children: /* @__PURE__ */ u2(TextInput, { 20581 value: text, 20582 onChange: setText 20583 }) 20584 }), 20585 /* @__PURE__ */ u2(Field, { 20586 label: "URL", 20587 children: /* @__PURE__ */ u2(TextInput, { 20588 value: href, 20589 placeholder: "https://...", 20590 onChange: setHref 20591 }) 20592 }) 20593 ] 20594 }), 20595 kind == "button" && /* @__PURE__ */ u2(Section, { 20596 title: "Button", 20597 children: [ 20598 /* @__PURE__ */ u2(Field, { 20599 label: "Label", 20600 children: /* @__PURE__ */ u2(TextInput, { 20601 value: text, 20602 onChange: setText 20603 }) 20604 }), 20605 /* @__PURE__ */ u2("p", { 20606 class: "le-hint", 20607 children: [ 20608 "What the button does is set up afterwards, with ", 20609 /* @__PURE__ */ u2("b", { 20610 children: "On click" 20611 }), 20612 "." 20613 ] 20614 }) 20615 ] 20616 }),
20617 (kind == "block" || kind == "html") && /* @__PURE__ */ u2(Section, { 20618 title: kind == "block" ? "Block" : "HTML", 20619 children: [ 20620 kind == "block" && /* @__PURE__ */ u2(Field, { 20621 label: "Kind", 20622 children: /* @__PURE__ */ u2(SelectInput, { 20623 value: isMessageBox ? "message_box" : "div", 20624 options: [ 20625 [ 20626 "div", 20627 "Plain block" 20628 ], 20629 [ 20630 "message_box", 20631 "Message box (with a Close button)" 20632 ] 20633 ], 20634 onChange: (value) => setIsMessageBox(value == "message_box") 20635 }) 20636 }), 20637 /* @__PURE__ */ u2("textarea", { 20638 class: "le-area le-mono", 20639 rows: 6, 20640 value: html, 20641 placeholder: kind == "block" ? "Text or HTML to show" : "<div>...</div>", 20642 onInput: (ev) => setHtml(ev.target.value) 20643 }) 20644 ] 20645 }), 20646 kind == "widget" && whereSection,
20647 /* @__PURE__ */ u2("div", { 20648 class: "le-notice", 20649 children: [ 20650 li.block, 20651 /* @__PURE__ */ u2("div", { 20652 children: "The new element appears when you save - it is added as a change, so nothing on the page moves while you fill this in." 20653 }) 20654 ] 20655 }) 20656 ] 20657 }); 20658} 20659
20660// src/components/live/panels/BehaviorPanels.tsx 20661function eventsAvailable(elem) { 20662 const events = [ 20663 [ 20664 "click", 20665 "Click" 20666 ], 20667 [ 20668 "mouseover", 20669 "Mouse over" 20670 ], 20671 [ 20672 "mouseout", 20673 "Mouse out" 20674 ] 20675 ]; 20676 switch (elem.localName) { 20677 case "select": 20678 case "a": 20679 events.push([ 20680 "focus", 20681 "Focus in" 20682 ], [ 20683 "blur", 20684 "Focus out" 20685 ]); 20686 break; 20687 case "input": 20688 case "textarea": 20689 events.push([ 20690 "focus", 20691 "Focus in" 20692 ], [ 20693 "blur", 20694 "Focus out" 20695 ], [ 20696 "keypress", 20697 "Key press" 20698 ]); 20699 break; 20700 default: 20701 if (elem.tabIndex != null && elem.tabIndex != -1) { 20702 events.push([ 20703 "focus", 20704 "Focus in" 20705 ], [ 20706 "blur", 20707 "Focus out" 20708 ]); 20709 } 20710 } 20711 return events; 20712} 20713var TRACK_TYPES = [ 20714 [ 20715 "js", 20716 "Execute custom Javascript" 20717 ], 20718 [ 20719 "fire_action", 20720 "Execute another action" 20721 ], 20722 [ 20723 "custom_event", 20724 "Trigger a custom event or goal" 20725 ], 20726 [ 20727 "action_extra_this", 20728 'Set "click" for this action CTR' 20729 ], 20730 [ 20731 "rm_elem", 20732 "Dismiss an element from the page" 20733 ] 20734]; 20735var TRACK_TYPE_HINTS = { 20736 js: "When the event happens, this Javascript runs on the page.", 20737 fire_action: "When the event happens, another action of this account is executed.", 20738 custom_event: `When the event happens, a "custom event" (defined in the account's containers) is sent to Personyze.`, 20739 action_extra_this: "Counts the interaction as a click on this action, which is what the campaign's CTR is measured from.", 20740 rm_elem: "When the event happens, the element you choose is removed from the page." 20741}; 20742function trackTypeOf(event) { 20743 if (event.custom_event != void 0) return "custom_event"; 20744 if (event.fire_action != void 0) return "fire_action"; 20745 if (event.action_id == -1 && event.action_state == "extra") return "action_extra_this"; 20746 if (event.rm_elem) return "rm_elem"; 20747 return "js"; 20748} 20749function draftFrom(event, defaultOnwhat) { 20750 return { 20751 onwhat: event?.onwhat ?? defaultOnwhat, 20752 type: event ? trackTypeOf(event) : "js", 20753 js: event?.js ?? "", 20754 once: !!event?.once, 20755 fireAction: event?.fire_action == void 0 ? "" : String(event.fire_action), 20756 containerId: event?.custom_event == void 0 ? "" : String(event.custom_event), 20757 customValue: event?.value ?? "", 20758 rmElem: typeof event?.rm_elem == "string" ? event.rm_elem : "" 20759 }; 20760} 20761function draftToEvent(draft) { 20762 switch (draft.type) { 20763 case "custom_event": 20764 return !draft.containerId ? null : { 20765 onwhat: draft.onwhat, 20766 custom_event: draft.containerId, 20767 value: draft.customValue 20768 }; 20769 case "fire_action": 20770 return !draft.fireAction ? null : { 20771 onwhat: draft.onwhat, 20772 fire_action: draft.fireAction 20773 }; 20774 case "action_extra_this": 20775 return { 20776 onwhat: draft.onwhat, 20777 action_id: -1, 20778 action_state: "extra" 20779 }; 20780 case "js": { 20781 const js = draft.js.trim(); 20782 return !js ? null : { 20783 onwhat: draft.onwhat, 20784 js, 20785 once: draft.once ? 1 : 0 20786 }; 20787 } 20788 case "rm_elem": 20789 return !draft.rmElem ? null : { 20790 onwhat: draft.onwhat, 20791 rm_elem: draft.rmElem 20792 }; 20793 } 20794} 20795function OnClickPanel(props) { 20796 const { session, actionId } = props; 20797 const events = eventsAvailable(session.elem); 20798 const existing = collectExisting(session.elem); 20799 const [drafts, setDrafts] = p2(() => existing.map((v3) => draftFrom(v3.event, events[0][0]))); 20800 const [fresh, setFresh] = p2(() => { 20801 const draft = draftFrom(null, events[0][0]); 20802 return props.initialType ? { 20803 ...draft, 20804 type: props.initialType 20805 } : draft; 20806 }); 20807 _2(() => { 20808 session.extraChanges.length = 0; 20809 const newEvent = draftToEvent(fresh); 20810 if (newEvent) { 20811 session.extraChanges.push({ 20812 events: [ 20813 newEvent 20814 ] 20815 }); 20816 } 20817 const rewrites = new Array(); 20818 for (let i4 = 0; i4 < existing.length; i4++) { 20819 const edited = draftToEvent(drafts[i4]); 20820 if (edited && JSON.stringify(edited) != JSON.stringify(existing[i4].event)) { 20821 const change = structuredClone(existing[i4].change); 20822 const list = change.events; 20823 if (list) { 20824 list[existing[i4].eventIndex] = edited; 20825 rewrites.push({ 20826 n: existing[i4].n, 20827 change 20828 }); 20829 } 20830 } 20831 } 20832 session.prevChanges = rewrites.length ? rewrites : void 0; 20833 }); 20834 return /* @__PURE__ */ u2(k, { 20835 children: [ 20836 existing.map((item, i4) => /* @__PURE__ */ u2(Section, { 20837 title: "Change #" + (item.n + 1) + (item.eventIndex ? "." + (item.eventIndex + 1) : ""), 20838 children: /* @__PURE__ */ u2(EventFields, { 20839 draft: drafts[i4], 20840 events, 20841 actionId, 20842 session, 20843 onPickElement: props.onPickElement, 20844 onError: props.onError, 20845 onChange: (next) => setDrafts((list) => list.map((v3, j3) => j3 == i4 ? next : v3)) 20846 }) 20847 }, "e" + i4)),
20848 /* @__PURE__ */ u2(Section, { 20849 title: existing.length ? "New event" : "When the visitor...", 20850 children: /* @__PURE__ */ u2(EventFields, { 20851 draft: fresh, 20852 events, 20853 actionId, 20854 session, 20855 onPickElement: props.onPickElement, 20856 onError: props.onError, 20857 onChange: setFresh 20858 }) 20859 }) 20860 ] 20861 }); 20862} 20863function collectExisting(elem) { 20864 const items = new Array(); 20865 for (const prev of getElemPrevChanges(elem)) { 20866 const list = prev.change.e ? void 0 : prev.change.events; 20867 if (list) { 20868 for (let j3 = 0; j3 < list.length; j3++) { 20869 items.push({ 20870 n: prev.n, 20871 change: prev.change, 20872 event: list[j3], 20873 eventIndex: j3 20874 }); 20875 } 20876 } 20877 } 20878 return items; 20879} 20880function EventFields(props) { 20881 const { draft, onChange } = props; 20882 const set = (patch) => onChange({ 20883 ...draft, 20884 ...patch 20885 }); 20886 const hasSql = isJsapiConfigured(); 20887 const actions = useAsync(async () => !hasSql || draft.type != "fire_action" ? [] : await queryRows("SELECT id, If(name<>'', name, Concat('ID ', id)) AS name FROM actions WHERE id!=@action_id ORDER BY name LIMIT 500", { 20888 action_id: props.actionId 20889 }), [ 20890 hasSql, 20891 draft.type, 20892 props.actionId 20893 ], props.onError); 20894 const containers = useAsync(async () => !hasSql || draft.type != "custom_event" ? [] : await queryRows("SELECT id, If(name<>'', name, Concat('ID ', id)) AS name FROM containers WHERE Find_in_set('custom event', container_types) ORDER BY name LIMIT 500"), [ 20895 hasSql, 20896 draft.type 20897 ], props.onError); 20898 return /* @__PURE__ */ u2(k, { 20899 children: [
20900 /* @__PURE__ */ u2(Field, { 20901 label: "Event", 20902 children: /* @__PURE__ */ u2(SelectInput, { 20903 value: draft.onwhat, 20904 options: props.events, 20905 onChange: (value) => set({ 20906 onwhat: value 20907 }) 20908 }) 20909 }), 20910 /* @__PURE__ */ u2(Field, { 20911 label: "Then", 20912 children: /* @__PURE__ */ u2(SelectInput, { 20913 value: draft.type, 20914 options: TRACK_TYPES, 20915 onChange: (value) => set({ 20916 type: value 20917 }) 20918 }) 20919 }), 20920 /* @__PURE__ */ u2("p", { 20921 class: "le-hint", 20922 children: TRACK_TYPE_HINTS[draft.type] 20923 }), 20924 draft.type == "js" && /* @__PURE__ */ u2(k, { 20925 children: [ 20926 /* @__PURE__ */ u2("textarea", { 20927 class: "le-area le-mono", 20928 rows: 6, 20929 value: draft.js, 20930 placeholder: "Javascript to run", 20931 onInput: (ev) => set({ 20932 js: ev.target.value 20933 }) 20934 }), 20935 /* @__PURE__ */ u2(Field, { 20936 label: "Once", 20937 children: /* @__PURE__ */ u2("label", { 20938 class: "le-btn", 20939 children: [
20940 /* @__PURE__ */ u2("input", { 20941 type: "checkbox", 20942 checked: draft.once, 20943 onChange: (ev) => set({ 20944 once: ev.target.checked 20945 }) 20946 }), 20947 "Only the first time on this page" 20948 ] 20949 }) 20950 }) 20951 ] 20952 }), 20953 draft.type == "fire_action" && /* @__PURE__ */ u2(Field, { 20954 label: "Action", 20955 children: !hasSql ? /* @__PURE__ */ u2(TextInput, { 20956 value: draft.fireAction, 20957 placeholder: "Action id", 20958 onChange: (value) => set({ 20959 fireAction: value 20960 }) 20961 }) : /* @__PURE__ */ u2(SelectInput, { 20962 value: draft.fireAction, 20963 options: [ 20964 [ 20965 "", 20966 actions.isBusy ? "Loading..." : "Choose..." 20967 ], 20968 ...(actions.data ?? []).map((row) => [ 20969 String(row.id), 20970 String(row.name) 20971 ]) 20972 ], 20973 onChange: (value) => set({ 20974 fireAction: value 20975 }) 20976 }) 20977 }), 20978 draft.type == "custom_event" && /* @__PURE__ */ u2(k, { 20979 children: [
20980 /* @__PURE__ */ u2(Field, { 20981 label: "Container", 20982 children: !hasSql ? /* @__PURE__ */ u2(TextInput, { 20983 value: draft.containerId, 20984 placeholder: "Container id", 20985 onChange: (value) => set({ 20986 containerId: value 20987 }) 20988 }) : /* @__PURE__ */ u2(SelectInput, { 20989 value: draft.containerId, 20990 options: [ 20991 [ 20992 "", 20993 containers.isBusy ? "Loading..." : "Choose..." 20994 ], 20995 ...(containers.data ?? []).map((row) => [ 20996 String(row.id), 20997 String(row.name) 20998 ]) 20999 ], 21000 onChange: (value) => set({ 21001 containerId: value 21002 }) 21003 }) 21004 }), 21005 /* @__PURE__ */ u2(Field, { 21006 label: "Value", 21007 children: /* @__PURE__ */ u2(TextInput, { 21008 value: draft.customValue, 21009 placeholder: "Optional", 21010 onChange: (value) => set({ 21011 customValue: value 21012 }) 21013 }) 21014 }) 21015 ] 21016 }), 21017 draft.type == "rm_elem" && /* @__PURE__ */ u2(Field, { 21018 label: "Element", 21019 children: [
21020 /* @__PURE__ */ u2(TextInput, { 21021 value: draft.rmElem, 21022 placeholder: "CSS selector", 21023 onChange: (value) => set({ 21024 rmElem: value 21025 }) 21026 }), 21027 /* @__PURE__ */ u2("button", { 21028 type: "button", 21029 class: "le-btn", 21030 onClick: () => props.onPickElement((elem) => elem && set({ 21031 rmElem: elemToSelector(elem) 21032 })), 21033 children: [ 21034 li.cursor, 21035 " Choose" 21036 ] 21037 }) 21038 ] 21039 }) 21040 ] 21041 }); 21042} 21043var SCROLL_BLOCKS = [ 21044 [ 21045 "start", 21046 "Top of the screen" 21047 ], 21048 [ 21049 "center", 21050 "Middle of the screen" 21051 ], 21052 [ 21053 "end", 21054 "Bottom of the screen" 21055 ] 21056]; 21057function ScrollToPanel(props) { 21058 const { session } = props; 21059 const events = eventsAvailable(session.elem); 21060 const [onwhat, setOnwhat] = p2(events[0][0]); 21061 const [target, setTarget] = p2(""); 21062 const [block, setBlock] = p2("start"); 21063 const [isSmooth, setIsSmooth] = p2(true); 21064 const [offset, setOffset] = p2("0"); 21065 _2(() => { 21066 session.extraChanges.length = 0; 21067 if (target.trim()) { 21068 session.extraChanges.push({ 21069 events: [ 21070 { 21071 onwhat, 21072 js: scrollJs(target.trim(), block, isSmooth, parseInt(offset) || 0) 21073 } 21074 ] 21075 }); 21076 } 21077 }); 21078 return /* @__PURE__ */ u2(k, { 21079 children: [
21080 /* @__PURE__ */ u2("div", { 21081 class: "le-notice", 21082 children: [ 21083 li.scroll, 21084 /* @__PURE__ */ u2("div", { 21085 children: [ 21086 "Point at where the page should scroll to. It is recorded as an on-click script, so it works on any element - the target needs no ", 21087 /* @__PURE__ */ u2("code", { 21088 children: "id" 21089 }), 21090 " and no anchor link." 21091 ] 21092 }) 21093 ] 21094 }), 21095 /* @__PURE__ */ u2(Section, { 21096 title: "Destination", 21097 children: [ 21098 /* @__PURE__ */ u2(Field, { 21099 label: "Element", 21100 children: /* @__PURE__ */ u2(TextInput, { 21101 value: target, 21102 placeholder: "CSS selector, or pick it on the page", 21103 onChange: setTarget 21104 }) 21105 }), 21106 /* @__PURE__ */ u2("div", { 21107 class: "le-btn-bar", 21108 children: [ 21109 /* @__PURE__ */ u2("button", { 21110 type: "button", 21111 class: "le-btn", 21112 onClick: () => props.onPickElement((elem) => { 21113 if (elem) setTarget(elemToSelector(elem)); 21114 }), 21115 children: [ 21116 li.cursor, 21117 "Pick on the page" 21118 ] 21119 }), 21120 target && /* @__PURE__ */ u2("span", { 21121 class: "le-hint", 21122 style: "margin:0", 21123 children: describeElem(document.querySelector(safeSelector(target)) ?? session.elem) 21124 }) 21125 ] 21126 }) 21127 ] 21128 }),
21129 /* @__PURE__ */ u2(Section, { 21130 title: "How", 21131 children: [ 21132 /* @__PURE__ */ u2(Field, { 21133 label: "On", 21134 children: /* @__PURE__ */ u2(SelectInput, { 21135 value: onwhat, 21136 options: events, 21137 onChange: setOnwhat 21138 }) 21139 }), 21140 /* @__PURE__ */ u2(Field, { 21141 label: "Align to", 21142 children: /* @__PURE__ */ u2(SelectInput, { 21143 value: block, 21144 options: SCROLL_BLOCKS, 21145 onChange: setBlock 21146 }) 21147 }), 21148 /* @__PURE__ */ u2(Field, { 21149 label: "Offset", 21150 children: [ 21151 /* @__PURE__ */ u2(TextInput, { 21152 value: offset, 21153 placeholder: "0", 21154 onChange: setOffset 21155 }), 21156 /* @__PURE__ */ u2("span", { 21157 class: "le-hint", 21158 style: "margin:0", 21159 children: "px, for a sticky header" 21160 }) 21161 ] 21162 }),
21163 /* @__PURE__ */ u2(Field, { 21164 label: "Motion", 21165 children: /* @__PURE__ */ u2("label", { 21166 class: "le-btn", 21167 children: [ 21168 /* @__PURE__ */ u2("input", { 21169 type: "checkbox", 21170 checked: isSmooth, 21171 onChange: (ev) => setIsSmooth(ev.target.checked) 21172 }), 21173 "Smooth" 21174 ] 21175 }) 21176 }) 21177 ] 21178 }) 21179 ] 21180 }); 21181} 21182function safeSelector(selector) { 21183 try { 21184 document.querySelector(selector); 21185 return selector; 21186 } catch { 21187 return ":root"; 21188 } 21189} 21190function scrollJs(selector, block, isSmooth, offset) { 21191 const behavior = isSmooth ? "smooth" : "auto"; 21192 const target = JSON.stringify(selector); 21193 if (!offset) { 21194 return `var t=document.querySelector(${target}); if(t) t.scrollIntoView({behavior:${JSON.stringify(behavior)},block:${JSON.stringify(block)}});`; 21195 } 21196 return `var t=document.querySelector(${target}); if(t) scrollTo({top:t.getBoundingClientRect().top+scrollY-${offset},behavior:${JSON.stringify(behavior)}});`; 21197} 21198function DelayPanel(props) { 21199 const { session } = props; 21200 const [ms, setMs] = p2("0"); 21201 const [hasReqText, setHasReqText] = p2(false); 21202 const [reqText, setReqText] = p2(""); 21203 _2(() => { 21204 const delay = { 21205 ms: parseInt(ms) || 0 21206 }; 21207 if (hasReqText) { 21208 delay.req_text = reqText; 21209 } 21210 session.extraChanges.length = 0; 21211 session.extraChanges.push({ 21212 delay 21213 }); 21214 }); 21215 return /* @__PURE__ */ u2(k, { 21216 children: [
21217 /* @__PURE__ */ u2("div", { 21218 class: "le-notice", 21219 children: [ 21220 li.clock, 21221 /* @__PURE__ */ u2("div", { 21222 children: [ 21223 "Every change recorded ", 21224 /* @__PURE__ */ u2("b", { 21225 children: "after" 21226 }), 21227 " this one waits until ", 21228 /* @__PURE__ */ u2("b", { 21229 children: describeElem(session.elem) 21230 }), 21231 " is on the page. Use it when the part you edited is rendered by the site's own script." 21232 ] 21233 }) 21234 ] 21235 }), 21236 /* @__PURE__ */ u2(Section, { 21237 title: "Wait for", 21238 children: [ 21239 /* @__PURE__ */ u2(Field, { 21240 label: "Text", 21241 children: /* @__PURE__ */ u2("label", { 21242 class: "le-btn", 21243 children: [ 21244 /* @__PURE__ */ u2("input", { 21245 type: "checkbox", 21246 checked: hasReqText, 21247 onChange: (ev) => setHasReqText(ev.target.checked) 21248 }), 21249 "Must contain text" 21250 ] 21251 }) 21252 }), 21253 hasReqText && /* @__PURE__ */ u2(Field, { 21254 label: "The text", 21255 children: /* @__PURE__ */ u2(TextInput, { 21256 value: reqText, 21257 placeholder: "Text the element must show", 21258 onChange: setReqText 21259 }) 21260 }),
21261 /* @__PURE__ */ u2(Field, { 21262 label: "And then", 21263 children: [ 21264 /* @__PURE__ */ u2(TextInput, { 21265 value: ms, 21266 placeholder: "0", 21267 onChange: setMs 21268 }), 21269 /* @__PURE__ */ u2("span", { 21270 class: "le-hint", 21271 style: "margin:0", 21272 children: "ms of extra delay" 21273 }) 21274 ] 21275 }) 21276 ] 21277 }) 21278 ] 21279 }); 21280} 21281
21282// src/components/live/panels/AiPanel.tsx 21283function AiPanel(props) { 21284 const { session, selected } = props; 21285 const inputRef = F2(null); 21286 const logRef = F2(null); 21287 _2(() => { 21288 const log = logRef.current; 21289 if (log) { 21290 log.scrollTop = log.scrollHeight; 21291 } 21292 }, [ 21293 session.transcript.length, 21294 session.isBusy 21295 ]); 21296 function send() { 21297 const input = inputRef.current; 21298 const prompt = input?.value.trim() ?? ""; 21299 if (prompt && !session.isBusy) { 21300 if (input) { 21301 input.value = ""; 21302 } 21303 void session.send(prompt); 21304 } 21305 } 21306 return /* @__PURE__ */ u2(k, { 21307 children: [ 21308 /* @__PURE__ */ u2("div", { 21309 class: "le-sub-head", 21310 children: [ 21311 /* @__PURE__ */ u2("button", { 21312 type: "button", 21313 class: "le-back", 21314 onClick: props.onBack, 21315 title: "Back to all actions", 21316 children: [ 21317 li.chevronLeft, 21318 "Back" 21319 ] 21320 }), 21321 /* @__PURE__ */ u2("b", { 21322 children: "AI assistant" 21323 }),
21324 /* @__PURE__ */ u2("div", { 21325 class: "le-spacer" 21326 }), 21327 session.isBusy && /* @__PURE__ */ u2("button", { 21328 type: "button", 21329 class: "le-btn", 21330 title: "Stop after the current step", 21331 onClick: () => session.stop(), 21332 children: [ 21333 li.stop, 21334 "Stop" 21335 ] 21336 }) 21337 ] 21338 }), 21339 /* @__PURE__ */ u2("div", { 21340 class: "le-target", 21341 children: selected ? /* @__PURE__ */ u2(k, { 21342 children: [ 21343 "Selected ", 21344 /* @__PURE__ */ u2("code", { 21345 title: describeElem(selected), 21346 children: describeElem(selected) 21347 }), 21348 ' \u2014 "this" in your request means it.' 21349 ] 21350 }) : /* @__PURE__ */ u2(k, { 21351 children: "Nothing selected \u2014 describe the elements in words, or click one first." 21352 }) 21353 }), 21354 /* @__PURE__ */ u2("div", { 21355 class: "le-ai-log", 21356 ref: logRef, 21357 children: [ 21358 session.transcript.length == 0 && /* @__PURE__ */ u2("div", { 21359 class: "le-ai-hint", 21360 children: "Describe an edit in plain words, and the assistant will make it on this page \u2014 move or restyle elements, insert new content, add behavior. The result appears in the change list like any other edit, and every step can be undone." 21361 }), 21362 session.transcript.map((entry, i4) => /* @__PURE__ */ u2(Entry, { 21363 entry 21364 }, i4)),
21365 session.isBusy && /* @__PURE__ */ u2("div", { 21366 class: "le-ai-busy", 21367 children: [ 21368 /* @__PURE__ */ u2("span", { 21369 class: "le-ai-pulse" 21370 }), 21371 "Working\u2026" 21372 ] 21373 }) 21374 ] 21375 }), 21376 /* @__PURE__ */ u2("div", { 21377 class: "le-ai-inputrow", 21378 children: [ 21379 /* @__PURE__ */ u2("textarea", { 21380 ref: inputRef, 21381 class: "le-ai-input", 21382 rows: 2, 21383 placeholder: "E.g.: move this banner under the menu", 21384 disabled: session.isBusy, 21385 onKeyDown: (ev) => { 21386 if (ev.key == "Enter" && !ev.shiftKey) { 21387 ev.preventDefault(); 21388 send(); 21389 } 21390 } 21391 }), 21392 /* @__PURE__ */ u2("button", { 21393 type: "button", 21394 class: "le-btn le-primary", 21395 disabled: session.isBusy, 21396 title: "Send (Enter)", 21397 onClick: send, 21398 children: li.send 21399 }) 21400 ] 21401 }),
21402 /* @__PURE__ */ u2(AiBudgetLine, { 21403 session 21404 }) 21405 ] 21406 }); 21407} 21408function AiBudgetLine(props) { 21409 const budget = props.session.budget; 21410 if (!budget || budget.unlimited || budget.actionsTotal <= 0) { 21411 return null; 21412 } 21413 const isOut = budget.actionsLeft <= 0; 21414 if (isOut) { 21415 return /* @__PURE__ */ u2("div", { 21416 class: "le-ai-budget le-ai-budget-out", 21417 children: budget.canOverage ? "Monthly AI allowance used \u2014 further AI is added to your invoice." : "You've used this month's AI allowance. Add AI credits in Billing to keep going." 21418 }); 21419 } 21420 return /* @__PURE__ */ u2("div", { 21421 class: "le-ai-budget", 21422 children: [ 21423 budget.actionsLeft, 21424 " AI ", 21425 budget.actionsLeft == 1 ? "action" : "actions", 21426 " left this month" 21427 ] 21428 }); 21429} 21430function Entry(props) { 21431 const { entry } = props; 21432 switch (entry.role) { 21433 case "user": 21434 return /* @__PURE__ */ u2("div", { 21435 class: "le-ai-msg le-ai-user", 21436 children: entry.text 21437 }); 21438 case "assistant": 21439 return /* @__PURE__ */ u2("div", { 21440 class: "le-ai-msg le-ai-assistant", 21441 children: entry.text 21442 }); 21443 case "error": 21444 return /* @__PURE__ */ u2("div", { 21445 class: "le-ai-status le-ai-error", 21446 children: [ 21447 li.warn,
21448 /* @__PURE__ */ u2("span", { 21449 children: entry.text 21450 }) 21451 ] 21452 }); 21453 } 21454 return /* @__PURE__ */ u2("div", { 21455 class: "le-ai-status", 21456 children: entry.text 21457 }); 21458} 21459
21460// src/components/live/LiveTool.tsx 21461var ADD_ONLY_TOOLS = /* @__PURE__ */ new Set([ 21462 "insert-image", 21463 "insert-link", 21464 "insert-button", 21465 "insert-block", 21466 "insert-video", 21467 "insert-html", 21468 "onclick", 21469 "scroll-to", 21470 "custom-event", 21471 "delay", 21472 "visibility" 21473]); 21474var INSERT_KINDS = { 21475 "insert-image": "image", 21476 "insert-link": "link", 21477 "insert-button": "button", 21478 "insert-block": "block", 21479 "insert-video": "video", 21480 "insert-html": "html" 21481}; 21482function widgetOfTool(tool) { 21483 const m2 = String(tool ?? "").match(/^insert-widget-(.+)$/); 21484 return m2 ? m2[1] : ""; 21485} 21486function inheritedDevice(elem) { 21487 for (let node = elem; node; node = node.parentElement) { 21488 const insert = insertOf(node); 21489 if (insert) { 21490 return insert.device ?? ""; 21491 } 21492 } 21493 return ""; 21494} 21495function isAddOnly(tool) { 21496 return ADD_ONLY_TOOLS.has(tool) || !!widgetOfTool(tool); 21497} 21498function insertKindOf(tool) { 21499 return !tool ? void 0 : widgetOfTool(tool) ? "widget" : INSERT_KINDS[tool]; 21500} 21501var SCOPE_KEY = "pz_live_scope"; 21502function loadScope() { 21503 try { 21504 const saved = localStorage.getItem(SCOPE_KEY); 21505 if (saved == "page" || saved == "shape" || saved == "any") { 21506 return saved; 21507 } 21508 } catch { 21509 } 21510 return "shape"; 21511} 21512function saveScope(scope) { 21513 try { 21514 localStorage.setItem(SCOPE_KEY, scope); 21515 } catch { 21516 } 21517} 21518function unreachableReason(elem) { 21519 const root = elem.getRootNode?.(); 21520 if (root && root !== document && root.host) { 21521 const host = root.host; 21522 return `This is inside <${host.localName}>, a component that keeps its own content sealed off from the page. Changes to it cannot be found again on a visitor's page, so it cannot be edited here \u2014 try the element around it.`; 21523 } 21524 if (!elemToSelector(elem)) { 21525 return "This element cannot be addressed on a visitor's page, so a change to it would never be found again. Try the element around it."; 21526 } 21527 return ""; 21528} 21529var DRAG_SLOP = 8; 21530function LiveTool(props) { 21531 const [selected, setSelected] = p2(null); 21532 const [tab, setTab] = p2("basics"); 21533 const [tool, setTool] = p2(null); 21534 const [isNavigate, setIsNavigate] = p2(false); 21535 const [isCollapsed, setIsCollapsed] = p2(false); 21536 const [toolbarPos, setToolbarPos] = p2(() => ({ 21537 left: 18, 21538 top: Math.max(16, Math.min(360, globalThis.innerHeight - 640)) 21539 })); 21540 const [changes, setChanges] = p2([]); 21541 const [nChanges, setNChanges] = p2(0); 21542 const [current, setCurrent] = p2(0); 21543 const [href, setHref] = p2(() => getPageUrl()); 21544 const [pending2, setPending] = p2(null); 21545 const [allow, setAllow] = p2({}); 21546 const [dragFrom, setDragFrom] = p2(null); 21547 const [scope, setScope] = p2(loadScope); 21548 const scopeRef = F2(scope); 21549 scopeRef.current = scope; 21550 const pickScope = (next) => { 21551 setScope(next); 21552 saveScope(next); 21553 }; 21554 const [showOn, setShowOn] = p2(""); 21555 const showOnRef = F2(""); 21556 showOnRef.current = showOn; 21557 const pickShowOn = (next) => { 21558 showOnRef.current = next; 21559 setShowOn(next); 21560 }; 21561 const [clicks, setClicks] = p2({ 21562 on: false, 21563 counted: false, 21564 touched: false 21565 }); 21566 const [error, setError] = p2(""); 21567 const [device, setDevice] = p2("desktop"); 21568 const [isSideCollapsed, setIsSideCollapsed] = p2(false); 21569 const [isSideLeft, setIsSideLeft] = p2(false); 21570 const [isStopped, setIsStopped] = p2(false); 21571 const [isHidden, setIsHidden] = p2(false); 21572 const [isPicking, setIsPicking] = p2(false); 21573 const [isAiOpen, setIsAiOpen] = p2(false); 21574 const [isSearchOpen, setIsSearchOpen] = p2(false); 21575 const [isPeeking, setIsPeeking] = p2(false); 21576 const [, setAiTick] = p2(0); 21577 const pickerRef = F2(null); 21578 const editorRef = F2(null); 21579 const aiRef = F2(null); 21580 const selectedRef = F2(null); 21581 const bridgeRef = F2(null); 21582 const sessionRef = F2(null); 21583 const actionIdRef = F2(0); 21584 const [names, setNames] = p2({ 21585 campaign: "", 21586 action: "" 21587 }); 21588 const [isGroup, setIsGroup] = p2(false); 21589 const isGroupRef = F2(false); 21590 isGroupRef.current = isGroup; 21591 const isSettingValueRef = F2(false); 21592 const isGotValueRef = F2(false); 21593 const isPageLoadedRef = F2(true); 21594 const isInitedRef = F2(false); 21595 const origSizeRef = F2(null); 21596 const pickOnceRef = F2(null); 21597 const previewTimerRef = F2(0); 21598 const peekDepthRef = F2(0); 21599 const isPeekingRef = F2(false); 21600 const isOwnWindow = typeof globalThis.opener == "object" && !!globalThis.opener;
21601 function postStatus(withChanges, isPeekStarting = false) { 21602 const editor = editorRef.current; 21603 const bridge = bridgeRef.current; 21604 if (!editor || !bridge || isSettingValueRef.current || isPeekingRef.current) { 21605 return; 21606 } 21607 const isReady = isPageLoadedRef.current && isGotValueRef.current && document.readyState == "complete"; 21608 const message = { 21609 page_is_loaded: isReady, 21610 href: getPageUrl(), 21611 n_changes: editor.getNChanges(), 21612 // Whether the AI is reachable at all. The panel's column hides its Page-level AI card 21613 // otherwise, rather than offering a box that answers nothing. 21614 has_ai: isJsapiConfigured() && !!aiRef.current, 21615 // False on every ordinary status, because the guard above means this only runs with the 21616 // peek OFF - which is what takes the panel out of "Showing the original" in the very 21617 // message that brings the real counts back. True only for the one status [[togglePeek]] 21618 // sends on its way IN, before the flag goes up. See there for why it is that status 21619 // rather than a message of its own. 21620 is_peeking: isPeekStarting 21621 }; 21622 if (withChanges && isGotValueRef.current) { 21623 message.html_changes = editor.getChanges(true); 21624 } 21625 void bridge.reportMessage(message); 21626 if (!isInitedRef.current && isReady && !isPeekStarting) { 21627 isInitedRef.current = true; 21628 beginPicking(); 21629 } 21630 } 21631 function refreshChanges() { 21632 const editor = editorRef.current; 21633 if (editor) { 21634 setChanges(editor.getChanges(true)); 21635 setNChanges(editor.getNChanges()); 21636 setHref(getPageUrl()); 21637 postStatus(true); 21638 } 21639 } 21640 function groupSelectorOf(elem) { 21641 const selector = elemToGroupSelector(elem); 21642 return selector && findMatches(selector, document).length > 1 ? selector : ""; 21643 } 21644 function beginPicking() { 21645 const picker = pickerRef.current; 21646 if (!picker || isNavigate || sessionRef.current || isStopped) { 21647 return; 21648 } 21649 setIsPicking(true); 21650 picker.selectBegin({ 21651 ignore: [ 21652 props.hostElem 21653 ], 21654 onSelect: (elem) => { 21655 const once = pickOnceRef.current; 21656 if (once) { 21657 pickOnceRef.current = null; 21658 picker.selectEnd(); 21659 setIsPicking(false); 21660 once(elem); 21661 return; 21662 } 21663 if (elem && elem != document.body && elem != document.documentElement) { 21664 const why = unreachableReason(elem); 21665 if (why) { 21666 setError(why); 21667 return; 21668 } 21669 setError(""); 21670 setSelected(elem); 21671 picker.highlight([ 21672 elem 21673 ]); 21674 } 21675 } 21676 }); 21677 } 21678 function endPicking() { 21679 pickerRef.current?.selectEnd(); 21680 setIsPicking(false); 21681 } 21682 function pickElement(onPicked) { 21683 const picker = pickerRef.current; 21684 if (picker) { 21685 pickOnceRef.current = onPicked; 21686 setIsPicking(true); 21687 picker.selectBegin({ 21688 ignore: [ 21689 props.hostElem 21690 ], 21691 onSelect: (elem) => { 21692 pickOnceRef.current = null; 21693 picker.selectEnd(); 21694 setIsPicking(false); 21695 onPicked(elem); 21696 if (sessionRef.current) { 21697 picker.highlight([ 21698 sessionRef.current.elem 21699 ]); 21700 } 21701 } 21702 }); 21703 } 21704 } 21705 _2(() => { 21706 const picker = new Picker(); 21707 pickerRef.current = picker; 21708 const editor = new PageEditor({ 21709 onChange: refreshChanges, 21710 onPending: setPending, 21711 getScope: () => scopeRef.current, 21712 getDevice: () => showOnRef.current 21713 }); 21714 editorRef.current = editor; 21715 if (isJsapiConfigured()) { 21716 aiRef.current = new AiLiveSession({ 21717 editor, 21718 hostElem: props.hostElem, 21719 getActionId: () => actionIdRef.current, 21720 getSelected: () => selectedRef.current, 21721 onUpdate: () => setAiTick((tick) => tick + 1) 21722 }); 21723 } 21724 const bridge = new ToolBridge({ 21725 bufferKey: props.bufferKey, 21726 guiOrigin: props.guiOrigin, 21727 onValue: (value) => void applyValue(value), 21728 onMessageData: (data) => onCommand(data) 21729 }); 21730 bridgeRef.current = bridge; 21731 void bridge.start(); 21732 const onLoad = () => postStatus(false); 21733 const onBeforeUnload = () => { 21734 isPageLoadedRef.current = false;
21735 postStatus(false); 21736 setTimeout(() => { 21737 isPageLoadedRef.current = true; 21738 postStatus(false); 21739 }, 2e3); 21740 }; 21741 globalThis.addEventListener("load", onLoad); 21742 globalThis.addEventListener("beforeunload", onBeforeUnload); 21743 postStatus(false); 21744 return () => { 21745 globalThis.removeEventListener("load", onLoad); 21746 globalThis.removeEventListener("beforeunload", onBeforeUnload); 21747 clearTimeout(previewTimerRef.current); 21748 bridge.destroy(); 21749 picker.destroy(); 21750 }; 21751 }, []); 21752 async function applyValue(value) { 21753 const editor = editorRef.current; 21754 if (!editor) { 21755 return; 21756 } 21757 if (value.action_id) { 21758 actionIdRef.current = Number(value.action_id) | 0; 21759 } 21760 if (value.campaign_name != void 0 || value.action_name != void 0) { 21761 setNames({ 21762 campaign: String(value.campaign_name ?? ""), 21763 action: String(value.action_name ?? "") 21764 }); 21765 } 21766 if (value.allow_form != void 0 || value.allow_chat != void 0 || value.allow_meetings != void 0) { 21767 setAllow({ 21768 form: !!value.allow_form, 21769 chat: !!value.allow_chat, 21770 meetings: !!value.allow_meetings 21771 }); 21772 } 21773 const htmlChanges = value.html_changes; 21774 const wanted = value.n_changes; 21775 if (htmlChanges && wanted != null) { 21776 try { 21777 isSettingValueRef.current = true; 21778 closeSession(false); 21779 isPeekingRef.current = false; 21780 peekDepthRef.current = 0; 21781 setIsPeeking(false); 21782 editor.reset(); 21783 await editor.addChanges(htmlChanges); 21784 setAppliedTo(wanted); 21785 } catch (e3) { 21786 console.error(e3); 21787 setError(e3 instanceof Error ? e3.message : "Operation failed"); 21788 void bridgeRef.current?.reportMessage({ 21789 error: "Operation failed" 21790 }); 21791 } finally { 21792 isSettingValueRef.current = false; 21793 isGotValueRef.current = true; 21794 } 21795 refreshChanges(); 21796 } 21797 } 21798 function onCommand(data) { 21799 const editor = editorRef.current; 21800 if (!editor) { 21801 return; 21802 } 21803 try { 21804 switch (data.command) { 21805 case "widget_html": 21806 resolveWidgetHtml(data.req_id, data.html); 21807 break; 21808 case "undo": 21809 undoOne(); 21810 break; 21811 case "redo": 21812 redoOne(); 21813 break; 21814 case "highlight_elems": 21815 if (data.qelem_id == void 0) { 21816 previewChange(null); 21817 } else { 21818 pickerRef.current?.highlight(resolveQelem(data.qelem_id, true)); 21819 } 21820 break; 21821 case "focus_elems": 21822 focusQelem(data.qelem_id); 21823 break; 21824 case "peek": { 21825 const wanted = data.on == void 0 ? !isPeekingRef.current : !!data.on; 21826 if (wanted != isPeekingRef.current) { 21827 togglePeek(); 21828 } 21829 break; 21830 } 21831 case "discard_all": 21832 discardAll(); 21833 break; 21834 case "page_ai": { 21835 const prompt = String(data.prompt ?? "").trim(); 21836 if (prompt && aiRef.current && !isEditingPaused()) { 21837 closeSession(false); 21838 setSelected(null); 21839 setIsAiOpen(true); 21840 void aiRef.current.send(prompt); 21841 } 21842 break; 21843 } 21844 case "stop": 21845 closeSession(false); 21846 endPicking(); 21847 setIsStopped(true); 21848 break; 21849 } 21850 } catch (e3) { 21851 console.error(e3); 21852 void bridgeRef.current?.reportMessage({ 21853 error: "Operation failed" 21854 }); 21855 } 21856 } 21857 function isEditingPaused() { 21858 return isPeekingRef.current; 21859 } 21860 function undoOne() { 21861 if (!isEditingPaused()) { 21862 editorRef.current?.undo(true); 21863 } 21864 } 21865 function redoOne() { 21866 if (!isEditingPaused()) { 21867 editorRef.current?.redo(true); 21868 } 21869 } 21870 function openAi() { 21871 if (!isEditingPaused()) { 21872 closeSession(false); 21873 setIsAiOpen(true); 21874 } 21875 } 21876 function openTool(next, on) { 21877 const editor = editorRef.current; 21878 const selected2 = on ?? selectedRef.current; 21879 if (!editor || !selected2 || isEditingPaused()) { 21880 return; 21881 } 21882 const qelemId = (isGroupRef.current ? groupSelectorOf(selected2) : "") || elemToSelector(selected2); 21883 pickShowOn(inheritedDevice(selected2)); 21884 if (INSTANT_TOOLS.indexOf(next) != -1) {
21885 try { 21886 editor.moveCursor(selected2, qelemId); 21887 switch (next) { 21888 case "duplicate": 21889 editor.changeDup(1); 21890 break; 21891 case "delete": 21892 editor.changeRm(); 21893 break; 21894 case "action-click": 21895 if (actionHasForm(editor.getChanges())) { 21896 throw new Error("This action has a form, so its Clicks number counts form submissions - a click here would be reported as a submission."); 21897 } 21898 if (clicksAlreadyCounted(selected2)) { 21899 throw new Error("Clicks on this element are already counted - see the list of changes."); 21900 } 21901 editor.changeCustom({ 21902 events: [ 21903 { 21904 ...CLICK_EVENT 21905 } 21906 ] 21907 }); 21908 break; 21909 } 21910 editor.saveAtCursor(); 21911 } catch (e3) { 21912 console.error(e3); 21913 setError(e3 instanceof Error ? e3.message : "Operation failed"); 21914 editor.discardAtCursor(); 21915 } 21916 setSelected(resolveQelem(qelemId)); 21917 return; 21918 } 21919 endPicking(); 21920 const working = editor.moveCursor(selected2, qelemId); 21921 if (!(working instanceof Element)) { 21922 editor.discardAtCursor(); 21923 return; 21924 } 21925 sessionRef.current = { 21926 elem: working, 21927 qelemId, 21928 targetSelector: qelemId, 21929 editor, 21930 extraChanges: [], 21931 subjElemEdited: !isAddOnly(next), 21932 onPreview: () => { 21933 pickerRef.current?.highlight([ 21934 sessionRef.current?.elem ?? working 21935 ]); 21936 }, 21937 /* The way a panel asks the Personyze window for work this bundle must not do — today 21938 that is compiling a widget's markup. Absent when there is no bridge, and the panel 21939 that needs it says so rather than silently doing nothing. */ 21940 report: (data) => void bridgeRef.current?.reportMessage(data) 21941 }; 21942 const insertKind = insertKindOf(next); 21943 const counted = !insertKind && clicksAlreadyCounted(working); 21944 setClicks({ 21945 counted, 21946 on: counted || (insertKind ? insertCountsByDefault(insertKind, widgetOfTool(next)) : isClickable(working)), 21947 touched: false 21948 }); 21949 setSelected(working); 21950 setTool(next); 21951 pickerRef.current?.highlight([ 21952 working 21953 ]); 21954 } 21955 function closeSession(withSave) { 21956 const session2 = sessionRef.current; 21957 const editor = editorRef.current; 21958 sessionRef.current = null; 21959 if (!session2 || !editor) { 21960 setTool(null); 21961 return; 21962 } 21963 try { 21964 if (withSave) { 21965 const addClicks = clicks.on && !clicks.counted && toolOffersClicks(tool) && !actionHasForm([ 21966 ...editor.getChanges(), 21967 ...session2.extraChanges 21968 ]); 21969 const nBefore = editor.getNChanges(); 21970 let edited; 21971 if (session2.subjElemEdited) { 21972 editor.saveAtCursor(); 21973 if (editor.getNChanges() > nBefore) { 21974 edited = editor.getChanges()[editor.getNChanges() - 1]; 21975 } 21976 } else { 21977 editor.discardAtCursor(); 21978 } 21979 if (session2.prevChanges?.length) { 21980 editor.setElemPrevChangesChanges(session2.elem, session2.prevChanges); 21981 } 21982 const extra = session2.extraChanges.filter((v3) => v3); 21983 for (const change of extra) { 21984 if (change.qelem_id == void 0) { 21985 change.qelem_id = session2.qelemId; 21986 } 21987 } 21988 const insert = extra.find((c3) => c3.e); 21989 const carrier = insert ?? (edited ? void 0 : extra.find((c3) => c3.mv)); 21990 if (addClicks && carrier) { 21991 carrier.events = [ 21992 ...carrier.events ?? [], 21993 { 21994 ...CLICK_EVENT 21995 } 21996 ]; 21997 } else if (addClicks && !edited && clicks.touched && !insertKindOf(tool)) { 21998 extra.push({ 21999 qelem_id: session2.qelemId, 22000 events: [ 22001 { 22002 ...CLICK_EVENT 22003 } 22004 ] 22005 }); 22006 } 22007 if (extra.length) { 22008 editor.addChangesSync(extra, true); 22009 } 22010 if (addClicks && edited && !carrier) { 22011 const at = editor.getChanges().indexOf(edited); 22012 if (at != -1) { 22013 editor.setChangeClicks(at, true); 22014 } 22015 } 22016 } else {
22017 editor.discardAtCursor(); 22018 } 22019 } catch (e3) { 22020 console.error(e3); 22021 setError(e3 instanceof Error ? e3.message : "Operation failed"); 22022 } 22023 setTool(null); 22024 setSelected(resolveQelem(session2.qelemId)); 22025 } 22026 _2(() => { 22027 if (isStopped || isHidden || isNavigate || tool || isPeeking) { 22028 endPicking(); 22029 } else if (isInitedRef.current) { 22030 beginPicking(); 22031 } 22032 }, [ 22033 isNavigate, 22034 tool, 22035 isStopped, 22036 isPeeking, 22037 isHidden 22038 ]); 22039 _2(() => isStopped || isNavigate ? void 0 : installPageFreeze(document, props.hostElem), [ 22040 isNavigate, 22041 isStopped 22042 ]); 22043 _2(() => { 22044 if (!isOwnWindow) { 22045 return; 22046 } 22047 function avoidSide2() { 22048 const root = props.hostElem.shadowRoot; 22049 const side = root?.querySelector(".le-side"); 22050 const toolbar = root?.querySelector(".le-toolbar"); 22051 if (!side || !toolbar) { 22052 return; 22053 } 22054 const rect = side.getBoundingClientRect(); 22055 const width = toolbar.offsetWidth; 22056 setToolbarPos((pos) => { 22057 if (pos.left + width <= rect.left || pos.left >= rect.right) { 22058 return pos; 22059 } 22060 const left = isSideLeft ? Math.min(globalThis.innerWidth - width - 8, rect.right + 12) : Math.max(8, rect.left - width - 12); 22061 return left == pos.left ? pos : { 22062 ...pos, 22063 left 22064 }; 22065 }); 22066 } 22067 avoidSide2(); 22068 globalThis.addEventListener("resize", avoidSide2); 22069 return () => globalThis.removeEventListener("resize", avoidSide2); 22070 }, [ 22071 isSideLeft, 22072 isSideCollapsed, 22073 isOwnWindow 22074 ]); 22075 _2(() => { 22076 const drag = dragFrom; 22077 if (!drag) { 22078 return; 22079 } 22080 let moved = false; 22081 const targetAt = (x2, y3) => { 22082 const at = document.elementFromPoint(x2, y3); 22083 return !at || at == props.hostElem || props.hostElem.contains(at) || at == document.documentElement ? null : at; 22084 }; 22085 const onMove = (ev) => { 22086 if (!moved && Math.abs(ev.clientX - drag.from.x) + Math.abs(ev.clientY - drag.from.y) < DRAG_SLOP) { 22087 return; 22088 } 22089 moved = true; 22090 ev.preventDefault(); 22091 const at = targetAt(ev.clientX, ev.clientY); 22092 pickerRef.current?.highlight(at ? [ 22093 at 22094 ] : [], "#F59E0B"); 22095 }; 22096 const onUp = (ev) => { 22097 setDragFrom(null); 22098 const at = moved ? targetAt(ev.clientX, ev.clientY) : null; 22099 if (!at) { 22100 pickerRef.current?.highlight(selectedRef.current ? [ 22101 selectedRef.current 22102 ] : []); 22103 return; 22104 } 22105 const why = unreachableReason(at); 22106 if (why) { 22107 setError(why); 22108 pickerRef.current?.highlight(selectedRef.current ? [ 22109 selectedRef.current 22110 ] : []); 22111 return; 22112 } 22113 setError(""); 22114 setSelected(at); 22115 pickerRef.current?.highlight([ 22116 at 22117 ]); 22118 openTool(drag.tool, at); 22119 }; 22120 document.addEventListener("mousemove", onMove, true); 22121 document.addEventListener("mouseup", onUp, true); 22122 return () => { 22123 document.removeEventListener("mousemove", onMove, true); 22124 document.removeEventListener("mouseup", onUp, true); 22125 }; 22126 }, [ 22127 dragFrom 22128 ]); 22129 _2(() => { 22130 const root = props.hostElem.shadowRoot?.querySelector(".le-toolbar"); 22131 if (!selected || !root || tool) { 22132 return; 22133 } 22134 const gap = 12; 22135 const panel = root.getBoundingClientRect(); 22136 const elem = selected.getBoundingClientRect(); 22137 const overlaps = panel.left < elem.right + gap && panel.right + gap > elem.left && panel.top < elem.bottom + gap && panel.bottom + gap > elem.top; 22138 if (!overlaps || !elem.width || !elem.height) { 22139 return; 22140 } 22141 const maxLeft = globalThis.innerWidth - panel.width - 8; 22142 const maxTop = globalThis.innerHeight - panel.height - 8; 22143 const candidates = [ 22144 { 22145 left: elem.right + gap, 22146 top: toolbarPos.top 22147 }, 22148 { 22149 left: elem.left - panel.width - gap, 22150 top: toolbarPos.top 22151 }, 22152 { 22153 left: toolbarPos.left, 22154 top: elem.bottom + gap 22155 }, 22156 { 22157 left: toolbarPos.left, 22158 top: elem.top - panel.height - gap 22159 } 22160 ]; 22161 const fits = candidates.find((p3) => p3.left >= 8 && p3.left <= maxLeft && p3.top >
22161= 8 && p3.top <= maxTop && !(p3.left < elem.right + gap && p3.left + panel.width + gap > elem.left && p3.top < elem.bottom + gap && p3.top + panel.height + gap > elem.top)); 22162 if (fits) { 22163 setToolbarPos(fits); 22164 } 22165 }, [ 22166 selected, 22167 tool 22168 ]); 22169 _2(() => { 22170 if (selected && !isPicking && !tool) { 22171 pickerRef.current?.highlight([ 22172 selected 22173 ]); 22174 } 22175 }, [ 22176 selected, 22177 isPicking, 22178 tool 22179 ]); 22180 _2(() => { 22181 selectedRef.current = selected; 22182 }, [ 22183 selected 22184 ]); 22185 _2(() => { 22186 function onKeyDown(ev) { 22187 const target2 = ev.composedPath()[0]; 22188 const isTyping = !!target2 && (target2.matches?.("input, textarea, select") || target2.isContentEditable); 22189 if ((ev.metaKey || ev.ctrlKey) && ev.key.toLowerCase() == "k") { 22190 ev.preventDefault(); 22191 setIsSearchOpen((v3) => !v3); 22192 return; 22193 } 22194 if (ev.key == "Escape") { 22195 if (isSearchOpen) { 22196 setIsSearchOpen(false); 22197 } else if (tool) { 22198 closeSession(false); 22199 } else if (isAiOpen) { 22200 setIsAiOpen(false); 22201 } else if (isNavigate) { 22202 setIsNavigate(false); 22203 } 22204 return; 22205 } 22206 if (isTyping || isSearchOpen) { 22207 return; 22208 } 22209 if ((ev.metaKey || ev.ctrlKey) && ev.key.toLowerCase() == "z") { 22210 ev.preventDefault(); 22211 if (ev.shiftKey) { 22212 redoOne(); 22213 } else { 22214 undoOne(); 22215 } 22216 return; 22217 } 22218 if ((ev.metaKey || ev.ctrlKey) && ev.key == "\\") { 22219 ev.preventDefault(); 22220 setIsCollapsed((v3) => !v3); 22221 return; 22222 } 22223 if (ev.metaKey || ev.ctrlKey || ev.altKey) { 22224 return; 22225 } 22226 if ((ev.key == "Backspace" || ev.key == "Delete") && !tool && !isAiOpen && selected) { 22227 ev.preventDefault(); 22228 openTool("delete"); 22229 return; 22230 } 22231 const key = ev.key.toUpperCase(); 22232 if (key == "N") { 22233 ev.preventDefault(); 22234 setIsNavigate((v3) => !v3); 22235 return; 22236 } 22237 if ((key == "J" || key == "K") && isOwnWindow && changes.length) { 22238 ev.preventDefault(); 22239 setCurrent((v3) => (v3 + (key == "J" ? 1 : changes.length - 1)) % changes.length); 22240 return; 22241 } 22242 if (!tool && !isAiOpen && selected) { 22243 const tile = TILES.find((v3) => v3.key == key); 22244 if (tile) { 22245 ev.preventDefault(); 22246 openTool(tile.tool); 22247 } 22248 } 22249 } 22250 document.addEventListener("keydown", onKeyDown, true); 22251 return () => document.removeEventListener("keydown", onKeyDown, true); 22252 }, [ 22253 tool, 22254 selected, 22255 isNavigate, 22256 isAiOpen, 22257 isSearchOpen, 22258 changes.length 22259 ]); 22260 function onDevice(next) { 22261 setDevice(next); 22262 if (!origSizeRef.current) { 22263 origSizeRef.current = { 22264 width: globalThis.outerWidth, 22265 height: globalThis.outerHeight 22266 }; 22267 } 22268 const width = DEVICE_WIDTHS[next]; 22269 try { 22270 if (!width) { 22271 globalThis.resizeTo(origSizeRef.current.width, origSizeRef.current.height); 22272 } else { 22273 globalThis.resizeTo(width + (globalThis.outerWidth - globalThis.innerWidth), globalThis.outerHeight); 22274 } 22275 } catch (e3) { 22276 console.error(e3); 22277 setError("This browser would not let the editor resize the window."); 22278 } 22279 } 22280 function setAppliedTo(n2, withScroll) { 22281 const editor = editorRef.current; 22282 if (!editor) { 22283 return; 22284 } 22285 for (let prev = -1; editor.getNChanges() != n2 && editor.getNChanges() != prev; ) { 22286 prev = editor.getNChanges(); 22287 if (prev > n2) { 22288 editor.undo(withScroll); 22289 } else { 22290 editor.redo(withScroll); 22291 } 22292 } 22293 } 22294 function jumpTo(n2) { 22295 setAppliedTo(n2, true); 22296 } 22297 function discardAll() { 22298 closeSession(false); 22299 setCurrent(0); 22300 isPeekingRef.current = false; 22301 peekDepthRef.current = 0; 22302 setIsPeeking(false); 22303 try { 22304 editorRef.current?.reset(); 22305 } catch (e3) { 22306 console.error(e3); 22307 setError(e3 instanceof Error ? e3.message : "Could not discard the changes"); 22308 } 22309 setSelected(null); 22310 } 22311 function togglePeek() { 22312 const editor = editorRef.current; 22313 if (!editor) { 22314 return; 22315 } 22316 if (!isPeekingRef.current) { 22317 closeSession(false); 22318 endPicking(); 22319 pickerRef.current?.highlight([]); 22320 peekDepthRef.current = editor.getNChanges(); 22321 postStatus(true, true); 22322 isPeekingRef.current = true; 22323 setIsPeeking(true); 22324 setAppliedTo(0); 22325 } else { 22326 setAppliedTo(peekDepthRef.current); 22327 isPeekingRef.current = false;
22328 setIsPeeking(false); 22329 postStatus(true); 22330 } 22331 } 22332 function focusQelem(qelemId) { 22333 closeSession(false); 22334 const elems = resolveQelem(qelemId, true); 22335 pickerRef.current?.highlight(elems, "#F59E0B"); 22336 elems[0]?.scrollIntoView({ 22337 block: "center", 22338 behavior: "smooth" 22339 }); 22340 if (elems[0]) { 22341 setSelected(elems[0]); 22342 } 22343 } 22344 function setScopeOn(indices, scope2) { 22345 const editor = editorRef.current; 22346 if (!editor) { 22347 return; 22348 } 22349 const refused = indices.filter((i4) => !editor.canSetChangeScope(i4, scope2)); 22350 if (refused.length) { 22351 setError(indices.length == 1 ? "That change was made on another page, so there is no layout here to compare it with. Open the page it belongs to and set it there." : "Some of these changes were made on another page, so there is no layout here to compare them with. Open that page to set them together."); 22352 return; 22353 } 22354 let done2 = false; 22355 for (const i4 of indices) { 22356 done2 = editor.setChangeScope(i4, scope2) || done2; 22357 } 22358 if (done2) { 22359 setError(""); 22360 refreshChanges(); 22361 } 22362 } 22363 function setClicksOn(index, on) { 22364 const editor = editorRef.current; 22365 if (!editor) { 22366 return; 22367 } 22368 const applied = editor.getChanges(); 22369 const change = applied[index]; 22370 if (on && change) { 22371 if (actionHasForm(applied)) { 22372 setError("This action has a form, so its Clicks number counts form submissions - a click here would be reported as a submission."); 22373 return; 22374 } 22375 const twin = !change.e && applied.some((c3, i4) => i4 != index && !c3.e && countsClicks(c3) && JSON.stringify(c3.qelem_id) == JSON.stringify(change.qelem_id)); 22376 if (twin) { 22377 setError("Clicks on this element are already counted by another change in the list."); 22378 return; 22379 } 22380 } 22381 if (editor.setChangeClicks(index, on)) { 22382 setError(""); 22383 refreshChanges(); 22384 } 22385 } 22386 function setDeviceOn(index, device2) { 22387 const editor = editorRef.current; 22388 if (!editor) { 22389 return; 22390 } 22391 let done2 = false; 22392 for (const i4 of relatedChanges(changes, index)) { 22393 done2 = editor.setChangeDevice(i4, device2) || done2; 22394 } 22395 if (done2) { 22396 setError(""); 22397 refreshChanges(); 22398 } 22399 } 22400 function focusChange(change) { 22401 focusQelem(change.qelem_id); 22402 } 22403 function openWidgetSettings(change) { 22404 const type = String((Array.isArray(change.e) && typeof change.e[0] == "string" ? change.e[1] : null)?.["data-pz-widget"] ?? ""); 22405 if (!type) { 22406 return; 22407 } 22408 const want = `[data-pz-widget="${type.replace(/"/g, "")}"]`; 22409 const anchor = resolveQelem(change.qelem_id); 22410 const near = anchor?.nodeType == 1 ? anchor : null; 22411 let host = null; 22412 for (let at = near; at && !host; at = at.parentElement) { 22413 host = at.matches?.(want) ? at : at.querySelector(want); 22414 } 22415 host ??= document.querySelector(want); 22416 if (!host) { 22417 setError("That widget is not on this page any more."); 22418 return; 22419 } 22420 closeSession(false); 22421 setSelected(host); 22422 pickerRef.current?.highlight([ 22423 host 22424 ]); 22425 host.scrollIntoView({ 22426 block: "center", 22427 behavior: "smooth" 22428 }); 22429 openTool("widget-edit", host); 22430 } 22431 function previewChange(change) { 22432 clearTimeout(previewTimerRef.current); 22433 const picker = pickerRef.current; 22434 if (!change) { 22435 picker?.highlight(selected ? [ 22436 selected 22437 ] : []); 22438 return; 22439 } 22440 const elems = resolveQelem(change.qelem_id, true); 22441 picker?.highlight(elems, "#F59E0B"); 22442 previewTimerRef.current = setTimeout(() => { 22443 const elem = elems[0]; 22444 const rect = elem?.getBoundingClientRect(); 22445 if (rect && (rect.top < 0 || rect.bottom > globalThis.innerHeight)) { 22446 elem.scrollIntoView({ 22447 block: "center", 22448 behavior: "smooth" 22449 }); 22450 } 22451 }, 120); 22452 } 22453 function selectBySelector(selector) { 22454 let elem = null; 22455 try { 22456 elem = document.querySelector(selector); 22457 } catch { 22458 return "That is not a valid CSS selector."; 22459 } 22460 if (!elem) { 22461 return "Nothing on this page matches it."; 22462 } 22463 if (elem == props.hostElem || props.hostElem.contains(elem)) { 22464 return "That is the editor's own toolbar, not the page."; 22465 } 22466 closeSession(false); 22467 setSelected(elem); 22468 pickerRef.current?.highlight([ 22469 elem 22470 ]); 22471 elem.scrollIntoView({ 22472 block: "center", 22473 behavior: "smooth" 22474 }); 22475 return ""; 22476 } 22477 function buildCommands() { 22478 const commands = []; 22479 commands.push({ 22480 group: "Editor", 22481 label: isNavigate ? "Turn Navigate mode off" : "Turn Navigate mode on", 22482 hint: "Click through the page like a visitor", 22483 shortcut: "N", 22484 run: () => setIsNavigate(!isNavigate) 22485 }, {
22486 group: "Editor", 22487 label: "Undo the last change", 22488 hint: "Reverse the most recent edit", 22489 shortcut: "\u2318Z", 22490 run: undoOne 22491 }, { 22492 group: "Editor", 22493 label: "Redo", 22494 hint: "Reapply the last undone change", 22495 shortcut: "\u2318\u21E7Z", 22496 run: redoOne 22497 }, { 22498 group: "Editor", 22499 label: isCollapsed ? "Expand the toolbar" : "Collapse the toolbar", 22500 hint: "Shrink it to one line, out of the way", 22501 shortcut: "\u2318\\", 22502 run: () => setIsCollapsed(!isCollapsed) 22503 }); 22504 if (changes.length) { 22505 commands.push({ 22506 group: "Editor", 22507 label: isPeeking ? "Put the changes back" : "Show the original page", 22508 hint: "Roll every change back to compare, without giving them up", 22509 run: togglePeek 22510 }, { 22511 group: "Editor", 22512 label: "Discard all changes", 22513 hint: "Undo everything - the page goes back to how it arrived", 22514 run: discardAll 22515 }); 22516 } 22517 if (isJsapiConfigured()) { 22518 commands.push({ 22519 group: "Editor", 22520 label: "AI assistant", 22521 hint: "Describe an edit in plain words", 22522 run: openAi 22523 }); 22524 } 22525 if (!tool) { 22526 for (const tile of availableTiles(selected)) { 22527 commands.push({ 22528 group: "This element", 22529 label: tile.title, 22530 hint: tile.sub, 22531 shortcut: tile.key, 22532 run: () => openTool(tile.tool) 22533 }); 22534 } 22535 } 22536 if (selected) { 22537 for (const node of ancestorChain(selected).slice(0, -1).reverse()) { 22538 commands.push({ 22539 group: "Select a parent", 22540 label: describeElem(node), 22541 hint: tagHumanName(node), 22542 run: () => { 22543 closeSession(false); 22544 setSelected(node); 22545 } 22546 }); 22547 } 22548 } 22549 if (isOwnWindow) { 22550 commands.push({ 22551 group: "Preview", 22552 label: "Desktop preview", 22553 hint: "The window size you started with", 22554 run: () => onDevice("desktop") 22555 }, { 22556 group: "Preview", 22557 label: "Tablet preview", 22558 hint: "768px wide", 22559 run: () => onDevice("tablet") 22560 }, { 22561 group: "Preview", 22562 label: "Mobile preview", 22563 hint: "375px wide", 22564 run: () => onDevice("mobile") 22565 }, { 22566 group: "Preview", 22567 label: isSideLeft ? "Dock the panel to the right" : "Dock the panel to the left", 22568 hint: "Move the change list to the other side", 22569 run: () => setIsSideLeft(!isSideLeft) 22570 }, { 22571 group: "Preview", 22572 label: "Done editing - back to Personyze", 22573 hint: "Save and return", 22574 run: done 22575 }); 22576 } 22577 for (const [i4, change] of changes.entries()) { 22578 const summary = summarizeChange(change); 22579 commands.push({ 22580 group: "Changes", 22581 label: summary.title, 22582 hint: summary.target + (i4 >= nChanges ? " - undone" : ""), 22583 run: () => { 22584 setCurrent(i4); 22585 focusChange(change); 22586 } 22587 }); 22588 } 22589 return commands; 22590 } 22591 function done() { 22592 const bridge = bridgeRef.current; 22593 const editor = editorRef.current; 22594 if (isPeekingRef.current) { 22595 togglePeek(); 22596 } 22597 closeSession(true); 22598 const finish = () => { 22599 try { 22600 getPanelWindow()?.focus(); 22601 globalThis.close(); 22602 } catch (e3) { 22603 console.error(e3); 22604 } 22605 }; 22606 if (bridge && editor) { 22607 bridge.reportMessage({ 22608 page_is_loaded: true, 22609 href: getPageUrl(), 22610 n_changes: editor.getNChanges(), 22611 html_changes: editor.getChanges(true) 22612 }).then(finish, finish); 22613 } else { 22614 finish(); 22615 } 22616 } 22617 if (isStopped) { 22618 return null; 22619 } 22620 if (isHidden) { 22621 const logoSrc = props.counterUrl ? props.counterUrl + "/images/logo-icon.svg" : ""; 22622 return /* @__PURE__ */ u2("div", { 22623 class: "le-root le-reopen", 22624 children: /* @__PURE__ */ u2("button", { 22625 type: "button", 22626 class: "le-reopen-btn", 22627 title: "Bring the editor back and carry on where you left off", 22628 onClick: () => setIsHidden(false), 22629 children: [
22630 logoSrc ? /* @__PURE__ */ u2("img", { 22631 src: logoSrc, 22632 alt: "", 22633 onError: (ev) => { 22634 ev.target.style.display = "none"; 22635 } 22636 }) : li.pencil, 22637 /* @__PURE__ */ u2("span", { 22638 children: "Reopen the Personyze editor" 22639 }), 22640 nChanges > 0 && /* @__PURE__ */ u2("span", { 22641 class: "le-reopen-n", 22642 children: [ 22643 nChanges, 22644 " change", 22645 nChanges == 1 ? "" : "s" 22646 ] 22647 }) 22648 ] 22649 }) 22650 }); 22651 } 22652 const session = sessionRef.current; 22653 const target = selected ? describeElem(selected) : ""; 22654 const groupInfo = (() => { 22655 const selector = !selected || tool ? "" : groupSelectorOf(selected); 22656 return selector ? { 22657 selector, 22658 count: findMatches(selector, document).length 22659 } : null; 22660 })(); 22661 const avoidSide = !isOwnWindow || isSideCollapsed ? "" : isSideLeft ? " le-avoid-left" : " le-avoid-right"; 22662 return /* @__PURE__ */ u2(k, { 22663 children: [
22664 /* @__PURE__ */ u2(Toolbar, { 22665 selected, 22666 tab, 22667 isNavigate, 22668 isCollapsed, 22669 canUndo: nChanges > 0, 22670 canRedo: nChanges < changes.length, 22671 hasAi: isJsapiConfigured(), 22672 allow, 22673 onDragTool: (tool2, from) => setDragFrom({ 22674 tool: tool2, 22675 from 22676 }), 22677 notice: tool ? "" : error, 22678 onDismissNotice: () => setError(""), 22679 isAiOpen, 22680 position: toolbarPos, 22681 group: groupInfo, 22682 isGroup, 22683 onGroup: (next) => { 22684 setIsGroup(next); 22685 pickerRef.current?.highlight(next && groupInfo ? findMatches(groupInfo.selector, document) : selected ? [ 22686 selected 22687 ] : []); 22688 }, 22689 onSelect: (elem) => { 22690 closeSession(false); 22691 setSelected(elem); 22692 }, 22693 onSelectBySelector: selectBySelector, 22694 onHoverCrumb: (elem) => pickerRef.current?.highlight(elem ? [ 22695 elem 22696 ] : selected ? [ 22697 selected 22698 ] : []), 22699 onTab: setTab, 22700 onTool: openTool, 22701 onNavigate: setIsNavigate, 22702 onAi: (next) => { 22703 if (!next) setIsAiOpen(false); 22704 else openAi(); 22705 }, 22706 onSearch: () => setIsSearchOpen(true), 22707 onCollapse: setIsCollapsed, 22708 onUndo: undoOne, 22709 onRedo: redoOne, 22710 onClose: () => { 22711 closeSession(false); 22712 endPicking(); 22713 setIsHidden(true); 22714 }, 22715 onMove: setToolbarPos, 22716 subpanelFoot: !(tool && session) ? null : ( 22717 /* ONE ROW. "Apply on" sits with the buttons because it is part of committing 22718 the edit, not a property of the element - and because a full-width control 22719 with a paragraph under it made the footer taller than the panel it belongs 22720 to (Jacob, 26 Aug 26). What it means lives in the tooltip on the label. 22721 22722 EXCEPT WHEN THE PANEL ASKS "WHERE" ITSELF. An insert opens on a Where section - 22723 before, inside, after - and "Apply on" is the other half of that same question, 22724 so there it sits under "Put it" as the same three buttons and the footer is 22725 just Cancel and Save (Jacob, 28 Sep 26). One place per panel, never two. */
22726 /* @__PURE__ */ u2("div", { 22727 class: "le-foot", 22728 children: [ 22729 !insertKindOf(tool) && /* @__PURE__ */ u2("div", { 22730 class: "le-foot-scope", 22731 children: [ 22732 /* @__PURE__ */ u2(ScopeLabel, { 22733 for: "le-scope-sel" 22734 }), 22735 /* @__PURE__ */ u2("select", { 22736 id: "le-scope-sel", 22737 class: "le-scope-pick le-scope-foot", 22738 "data-tooltip": ((o3) => o3 ? o3.label + " - " + o3.hint : "")(SCOPE_OPTIONS.find((v3) => v3.id == scope)), 22739 "data-tooltip-pos": "top", 22740 value: scope, 22741 onChange: (e3) => pickScope(e3.currentTarget.value), 22742 children: SCOPE_OPTIONS.map((v3) => /* @__PURE__ */ u2("option", { 22743 value: v3.id, 22744 title: v3.hint, 22745 children: v3.short 22746 })) 22747 }) 22748 ] 22749 }),
22750 !insertKindOf(tool) && /* @__PURE__ */ u2("div", { 22751 class: "le-foot-scope", 22752 children: /* @__PURE__ */ u2("select", { 22753 id: "le-device-sel", 22754 class: "le-scope-pick le-scope-foot", 22755 "aria-label": "Show on", 22756 "data-tooltip": "Show on: " + SHOW_ON_TIP, 22757 "data-tooltip-pos": "top", 22758 value: showOn, 22759 onChange: (e3) => pickShowOn(e3.currentTarget.value), 22760 children: SHOW_ON_OPTIONS.map((v3) => /* @__PURE__ */ u2("option", { 22761 value: v3.id, 22762 title: v3.hint, 22763 children: v3.label 22764 })) 22765 }) 22766 }), 22767 /* @__PURE__ */ u2("div", { 22768 class: "le-foot-save", 22769 children: [ 22770 toolOffersClicks(tool) && widgetOfTool(tool) != "form" && !actionHasForm(changes.slice(0, nChanges)) && /* @__PURE__ */ u2(CountClicksCheck, { 22771 on: clicks.on, 22772 counted: clicks.counted, 22773 isInsert: !!insertKindOf(tool), 22774 onChange: (on) => setClicks({ 22775 on, 22776 counted: false, 22777 touched: true 22778 }) 22779 }),
22780 /* @__PURE__ */ u2("button", { 22781 type: "button", 22782 class: "le-btn", 22783 "data-tooltip": "Discard what you changed here and go back to the tiles", 22784 "data-tooltip-pos": "top", 22785 onClick: () => closeSession(false), 22786 children: "Cancel" 22787 }), 22788 /* @__PURE__ */ u2("button", { 22789 type: "button", 22790 class: "le-btn le-primary", 22791 "data-tooltip": "Keep this edit and go back to the tiles", 22792 "data-tooltip-pos": "top", 22793 onClick: () => closeSession(true), 22794 children: "Save" 22795 }) 22796 ] 22797 }) 22798 ] 22799 }) 22800 ), 22801 subpanel: !(tool && session) ? !isAiOpen || !aiRef.current ? null : /* @__PURE__ */ u2(AiPanel, { 22802 session: aiRef.current, 22803 selected, 22804 onBack: () => setIsAiOpen(false) 22805 }) : /* @__PURE__ */ u2(k, { 22806 children: [
22807 /* @__PURE__ */ u2(SubPanelHead, { 22808 title: TILES.find((v3) => v3.tool == tool)?.title ?? "", 22809 target: target || tagHumanName(session.elem), 22810 onBack: () => closeSession(false), 22811 extra: tool == "style" || tool == "spacing" || tool == "alignment" || tool == "position" ? /* @__PURE__ */ u2("button", { 22812 type: "button", 22813 class: "le-btn", 22814 title: "Drop every inline style on this element - all tabs, not just this one", 22815 onClick: () => resetInlineStyle(session), 22816 children: "Reset all" 22817 }) : void 0 22818 }), 22819 error && /* @__PURE__ */ u2("div", { 22820 class: "le-notice le-warn", 22821 children: [ 22822 li.warn, 22823 /* @__PURE__ */ u2("div", { 22824 children: error 22825 }), 22826 /* @__PURE__ */ u2("button", { 22827 type: "button", 22828 class: "le-icon", 22829 title: "Dismiss", 22830 onClick: () => setError(""), 22831 children: li.cross 22832 }) 22833 ] 22834 }),
22835 /* @__PURE__ */ u2(ToolBody, { 22836 tool, 22837 session, 22838 actionId: actionIdRef.current, 22839 scope, 22840 onScope: pickScope, 22841 device: showOn, 22842 onDevice: pickShowOn, 22843 onPickElement: pickElement, 22844 onError: (e3) => setError(e3 instanceof Error ? e3.message : String(e3)) 22845 }) 22846 ] 22847 }) 22848 }), 22849 isSearchOpen && /* @__PURE__ */ u2(CommandPalette, { 22850 commands: buildCommands(), 22851 onClose: () => setIsSearchOpen(false) 22852 }), 22853 pending2 && /* @__PURE__ */ u2("div", { 22854 class: "le-root le-navbanner" + avoidSide, 22855 style: "top:auto; bottom:6em", 22856 children: [ 22857 li.warn, 22858 /* @__PURE__ */ u2("span", { 22859 children: [ 22860 "An earlier edit points at ", 22861 /* @__PURE__ */ u2("b", { 22862 children: pending2.target 22863 }), 22864 ", which is not on this page (yet)." 22865 ] 22866 }),
22867 /* @__PURE__ */ u2("button", { 22868 type: "button", 22869 class: "le-btn", 22870 onClick: () => editorRef.current?.bypassPending(), 22871 children: "Skip it" 22872 }) 22873 ] 22874 }), 22875 isPeeking && /* @__PURE__ */ u2("div", { 22876 class: "le-root le-navbanner" + avoidSide, 22877 children: [ 22878 /* @__PURE__ */ u2("span", { 22879 children: "\u{1F441}" 22880 }), 22881 /* @__PURE__ */ u2("span", { 22882 children: [ 22883 /* @__PURE__ */ u2("b", { 22884 children: "Showing the original" 22885 }), 22886 " - your ", 22887 changes.length, 22888 " ", 22889 changes.length == 1 ? "change is" : "changes are", 22890 " kept, and editing is paused." 22891 ] 22892 }), 22893 /* @__PURE__ */ u2("button", { 22894 type: "button", 22895 class: "le-btn", 22896 onClick: togglePeek, 22897 children: "Put them back" 22898 }) 22899 ] 22900 }), 22901 isNavigate && !isPeeking && /* @__PURE__ */ u2("div", { 22902 class: "le-root le-navbanner" + avoidSide, 22903 children: [
22904 /* @__PURE__ */ u2("span", { 22905 children: "\u{1F446}" 22906 }), 22907 /* @__PURE__ */ u2("span", { 22908 children: [ 22909 /* @__PURE__ */ u2("b", { 22910 children: "Navigate mode" 22911 }), 22912 " - clicks go to the page. ", 22913 /* @__PURE__ */ u2("kbd", { 22914 children: "Esc" 22915 }), 22916 " to edit." 22917 ] 22918 }) 22919 ] 22920 }), 22921 isOwnWindow && /* @__PURE__ */ u2(k, { 22922 children: [ 22923 /* @__PURE__ */ u2(ChangesSidebar, { 22924 changes, 22925 nChanges, 22926 current, 22927 href, 22928 campaignName: names.campaign, 22929 actionName: names.action || (actionIdRef.current > 0 ? "Action " + actionIdRef.current : "New action"), 22930 logoSrc: props.counterUrl ? props.counterUrl + "/images/logo-icon.svg" : "", 22931 isCollapsed: isSideCollapsed, 22932 isLeft: isSideLeft, 22933 onCurrent: setCurrent, 22934 onWidgetSettings: openWidgetSettings, 22935 onScope: (index, next) => setScopeOn(relatedChanges(changes, index), next), 22936 onScopeAll: (next) => setScopeOn(changes.map((_3, i4) => i4), next), 22937 onClicks: setClicksOn, 22938 onDevice: setDeviceOn, 22939 clicksBlocked: actionHasForm(changes.slice(0, nChanges)), 22940 onJump: jumpTo, 22941 onFocus: focusChange, 22942 onHover: previewChange, 22943 onGo: (url) => { 22944 globalThis.location.href = url; 22945 }, 22946 onReload: () => globalThis.location.reload(), 22947 onDone: done, 22948 onDiscardAll: discardAll, 22949 isPeeking, 22950 onPeek: togglePeek, 22951 onCollapse: setIsSideCollapsed, 22952 onDock: setIsSideLeft, 22953 hasAi: isJsapiConfigured() && !!aiRef.current, 22954 onPageAi: (prompt) => { 22955 if (isEditingPaused()) { 22956 return; 22957 } 22958 closeSession(false); 22959 setSelected(null); 22960 setIsAiOpen(true); 22961 void aiRef.current?.send(prompt); 22962 } 22963 }),
22964 /* @__PURE__ */ u2(DevicePicker, { 22965 device, 22966 onDevice 22967 }) 22968 ] 22969 }), 22970 /* @__PURE__ */ u2(TipLayer, {}) 22971 ] 22972 }); 22973} 22974function ToolBody(props) { 22975 const { tool, session } = props; 22976 const insertKind = insertKindOf(tool); 22977 if (insertKind) { 22978 return /* @__PURE__ */ u2(InsertPanel, { 22979 session, 22980 kind: insertKind, 22981 widgetId: widgetOfTool(tool), 22982 scope: props.scope, 22983 onScope: props.onScope, 22984 device: props.device, 22985 onDevice: props.onDevice 22986 }); 22987 } 22988 switch (tool) { 22989 case "widget-edit": 22990 return /* @__PURE__ */ u2(WidgetEditPanel, { 22991 session 22992 }); 22993 case "text": 22994 return /* @__PURE__ */ u2(TextPanel, { 22995 session, 22996 actionId: props.actionId 22997 }); 22998 case "rich": 22999 return /* @__PURE__ */ u2(RichPanel, { 23000 session, 23001 actionId: props.actionId 23002 }); 23003 case "source": 23004 return /* @__PURE__ */ u2(SourcePanel, { 23005 session, 23006 actionId: props.actionId 23007 }); 23008 case "style": 23009 return /* @__PURE__ */ u2(StylePanel, { 23010 session, 23011 actionId: props.actionId 23012 }); 23013 case "spacing": 23014 return /* @__PURE__ */ u2(SpacingPanel, { 23015 session 23016 }); 23017 case "alignment": 23018 return /* @__PURE__ */ u2(AlignmentPanel, { 23019 session 23020 }); 23021 case "position": 23022 return /* @__PURE__ */ u2(PositionPanel, { 23023 session 23024 }); 23025 case "visibility": 23026 return /* @__PURE__ */ u2(VisibilityPanel, { 23027 session 23028 }); 23029 case "props": 23030 return /* @__PURE__ */ u2(PropsPanel, { 23031 session 23032 }); 23033 case "replace-image": 23034 return /* @__PURE__ */ u2(ReplaceImagePanel, { 23035 session 23036 }); 23037 case "move": 23038 return /* @__PURE__ */ u2(MovePanel, { 23039 session, 23040 onPickElement: props.onPickElement 23041 }); 23042 case "onclick": 23043 return /* @__PURE__ */ u2(OnClickPanel, { 23044 session, 23045 actionId: props.actionId, 23046 onPickElement: props.onPickElement, 23047 onError: props.onError 23048 }); 23049 case "custom-event": 23050 return /* @__PURE__ */ u2(OnClickPanel, { 23051 session, 23052 actionId: props.actionId, 23053 initialType: "custom_event", 23054 onPickElement: props.onPickElement, 23055 onError: props.onError 23056 }); 23057 case "scroll-to": 23058 return /* @__PURE__ */ u2(ScrollToPanel, { 23059 session, 23060 onPickElement: props.onPickElement 23061 }); 23062 case "delay": 23063 return /* @__PURE__ */ u2(DelayPanel, { 23064 session 23065 }); 23066 } 23067 return null; 23068} 23069
23070// src/modules/toplayer/mod.ts 23071function raiseToTopLayer(elem) { 23072 if (typeof elem.showPopover != "function") { 23073 return false; 23074 } 23075 try { 23076 elem.setAttribute("popover", "manual"); 23077 elem.showPopover(); 23078 return true; 23079 } catch { 23080 elem.removeAttribute("popover"); 23081 return false; 23082 } 23083} 23084
23085// src/apps/simulator-live.ts 23086var HOST_STYLE = "all: initial !important; position: fixed !important; z-index: 2147483647 !important; top: 0 !important; left: 0 !important; width: 0 !important; height: 0 !important"; 23087function start(options) { 23088 if (options.jsapi_url) { 23089 configureJsapi({ 23090 url: options.jsapi_url, 23091 serverId: options.server_id || 0 23092 }); 23093 } 23094 const api = globalScope.personyzeSimulatorLive; 23095 if (api) { 23096 api.session = { 23097 jsapi_url: options.jsapi_url ?? "", 23098 server_id: options.server_id ?? 0, 23099 buffer_key: options.buffer_key 23100 }; 23101 } 23102 const hostElem = document.createElement("div"); 23103 hostElem.setAttribute("style", HOST_STYLE); 23104 document.body.appendChild(hostElem); 23105 raiseToTopLayer(hostElem); 23106 const shadow = hostElem.attachShadow({ 23107 mode: "open" 23108 }); 23109 const style = document.createElement("style"); 23110 style.textContent = LIVE_STYLE; 23111 shadow.appendChild(style); 23112 const mount = document.createElement("div"); 23113 shadow.appendChild(mount); 23114 B(_(LiveTool, { 23115 bufferKey: options.buffer_key, 23116 guiOrigin: options.gui_origin || "*", 23117 counterUrl: options.counter_url || "", 23118 hostElem 23119 }), mount); 23120 return { 23121 destroy() { 23122 B(null, mount); 23123 hostElem.remove(); 23124 } 23125 }; 23126} 23127var globalScope = globalThis; 23128if (globalScope.personyzeSimulatorLive == void 0) { 23129 globalScope.personyzeSimulatorLive = { 23130 version: 1, 23131 start 23132 }; 23133} 23134})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.