@nodegx/preview 0.2.7 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13786,7 +13786,7 @@ var require_common2 = __commonJS({
13786
13786
  exports2.ownProp = ownProp;
13787
13787
  exports2.makeAbs = makeAbs;
13788
13788
  exports2.finish = finish2;
13789
- exports2.mark = mark;
13789
+ exports2.mark = mark2;
13790
13790
  exports2.isIgnored = isIgnored;
13791
13791
  exports2.childrenIgnored = childrenIgnored;
13792
13792
  function ownProp(obj, field) {
@@ -13922,7 +13922,7 @@ var require_common2 = __commonJS({
13922
13922
  });
13923
13923
  self2.found = all;
13924
13924
  }
13925
- function mark(self2, p) {
13925
+ function mark2(self2, p) {
13926
13926
  var abs = makeAbs(self2, p);
13927
13927
  var c = self2.cache[abs];
13928
13928
  var m = p;
@@ -19380,10 +19380,10 @@ var require_nodescope = __commonJS({
19380
19380
  } else {
19381
19381
  const parameters = nodeModel.parameters;
19382
19382
  var inputNames = Object.keys(parameters);
19383
- const RUN_ON_CHANGE_PREFIX4 = "runOnChange-";
19383
+ const RUN_ON_CHANGE_PREFIX5 = "runOnChange-";
19384
19384
  const metadata = this.context.nodeRegister.hasNode(node.name) ? this.context.nodeRegister.getNodeMetadata(node.name) : void 0;
19385
19385
  const priorityOf = function(name2) {
19386
- if (name2.indexOf(RUN_ON_CHANGE_PREFIX4) === 0) return Number.MAX_SAFE_INTEGER;
19386
+ if (name2.indexOf(RUN_ON_CHANGE_PREFIX5) === 0) return Number.MAX_SAFE_INTEGER;
19387
19387
  const input2 = metadata ? metadata.inputs[name2] : void 0;
19388
19388
  return input2 ? input2.inputPriority : 0;
19389
19389
  };
@@ -21345,11 +21345,11 @@ var require_editorconnection = __commonJS({
21345
21345
  });
21346
21346
  };
21347
21347
  var serviceRequests = {};
21348
- EditorConnection.prototype.sendServiceRequest = function(request, callback) {
21349
- request.token = guid4();
21350
- request.clientId = this.clientId;
21351
- serviceRequests[request.token] = callback;
21352
- this.send(request);
21348
+ EditorConnection.prototype.sendServiceRequest = function(request2, callback) {
21349
+ request2.token = guid4();
21350
+ request2.clientId = this.clientId;
21351
+ serviceRequests[request2.token] = callback;
21352
+ this.send(request2);
21353
21353
  };
21354
21354
  EditorConnection.prototype.close = function() {
21355
21355
  this.reconnectOnClose = false;
@@ -23328,7 +23328,10 @@ var require_nodedefinition = __commonJS({
23328
23328
  node.registerOutput(name2, {
23329
23329
  getter: function() {
23330
23330
  return void 0;
23331
- }
23331
+ },
23332
+ onFirstConnectionAdded: output.onFirstConnectionAdded,
23333
+ onLastConnectionRemoved: output.onLastConnectionRemoved
23334
+ // a signal keeps its wire hooks (P121-TWO003)
23332
23335
  });
23333
23336
  } else {
23334
23337
  node.registerOutput(name2, output);
@@ -24781,12 +24784,12 @@ var require_restnode = __commonJS({
24781
24784
  this.scheduleAfterInputsHaveUpdated(this.doFetch.bind(this));
24782
24785
  }
24783
24786
  },
24784
- doResponse: function(status, response, request) {
24787
+ doResponse: function(status, response, request2) {
24785
24788
  if (this._internal.responseFunc) {
24786
24789
  this._internal.responseFunc.apply(this._internal.self, [
24787
24790
  this._internal.inputValues,
24788
24791
  this._internal.outputValuesProxy,
24789
- { status, content: response, request }
24792
+ { status, content: response, request: request2 }
24790
24793
  ]);
24791
24794
  }
24792
24795
  this._internal.inspectData = { status, content: response };
@@ -24796,11 +24799,11 @@ var require_restnode = __commonJS({
24796
24799
  this.sendSignalOnOutput("failure");
24797
24800
  }
24798
24801
  },
24799
- doExternalFetch: function(request) {
24800
- let url = request.resource;
24801
- if (Object.keys(request.parameters).length > 0) {
24802
- const parameters = Object.keys(request.parameters).map(function(p) {
24803
- return p + "=" + encodeURIComponent(request.parameters[p]);
24802
+ doExternalFetch: function(request2) {
24803
+ let url = request2.resource;
24804
+ if (Object.keys(request2.parameters).length > 0) {
24805
+ const parameters = Object.keys(request2.parameters).map(function(p) {
24806
+ return p + "=" + encodeURIComponent(request2.parameters[p]);
24804
24807
  });
24805
24808
  url += "?" + parameters.join("&");
24806
24809
  }
@@ -24808,9 +24811,9 @@ var require_restnode = __commonJS({
24808
24811
  const _this = this;
24809
24812
  const xhr = new window.XMLHttpRequest();
24810
24813
  this._xhr = xhr;
24811
- xhr.open(request.method, url, true);
24812
- for (const header in request.headers) {
24813
- xhr.setRequestHeader(header, request.headers[header]);
24814
+ xhr.open(request2.method, url, true);
24815
+ for (const header in request2.headers) {
24816
+ xhr.setRequestHeader(header, request2.headers[header]);
24814
24817
  }
24815
24818
  xhr.onreadystatechange = function() {
24816
24819
  let sentResponse = false;
@@ -24822,11 +24825,11 @@ var require_restnode = __commonJS({
24822
24825
  if (responseType) {
24823
24826
  responseType = responseType.toLowerCase();
24824
24827
  const responseData = responseType.indexOf("json") !== -1 ? JSON.parse(rawResponse) : rawResponse;
24825
- _this.doResponse(statusCode, responseData, request);
24828
+ _this.doResponse(statusCode, responseData, request2);
24826
24829
  sentResponse = true;
24827
24830
  }
24828
24831
  if (sentResponse === false) {
24829
- _this.doResponse(statusCode, rawResponse, request);
24832
+ _this.doResponse(statusCode, rawResponse, request2);
24830
24833
  }
24831
24834
  }
24832
24835
  };
@@ -24838,30 +24841,30 @@ var require_restnode = __commonJS({
24838
24841
  delete this._xhr;
24839
24842
  _this.sendSignalOnOutput("canceled");
24840
24843
  };
24841
- if (request.content) {
24844
+ if (request2.content) {
24842
24845
  xhr.setRequestHeader("Content-Type", "application/json");
24843
- xhr.send(JSON.stringify(request.content));
24846
+ xhr.send(JSON.stringify(request2.content));
24844
24847
  } else {
24845
24848
  xhr.send();
24846
24849
  }
24847
24850
  } else {
24848
24851
  const headers = Object.assign(
24849
24852
  {},
24850
- request.headers,
24851
- request.content ? {
24853
+ request2.headers,
24854
+ request2.content ? {
24852
24855
  "Content-Type": "application/json"
24853
24856
  } : {}
24854
24857
  );
24855
24858
  fetch(url, {
24856
- method: request.method,
24859
+ method: request2.method,
24857
24860
  headers,
24858
- body: request.content ? JSON.stringify(request.content) : void 0
24861
+ body: request2.content ? JSON.stringify(request2.content) : void 0
24859
24862
  }).then((response) => {
24860
24863
  const responseType = response.headers.get("content-type");
24861
24864
  if (responseType) {
24862
24865
  if (responseType.indexOf("/json") !== -1) {
24863
24866
  response.json().then((json) => {
24864
- this.doResponse(response.status, json, request);
24867
+ this.doResponse(response.status, json, request2);
24865
24868
  });
24866
24869
  } else {
24867
24870
  if (this.context.editorConnection) {
@@ -24877,7 +24880,7 @@ var require_restnode = __commonJS({
24877
24880
  }
24878
24881
  } else {
24879
24882
  response.text().then((raw) => {
24880
- this.doResponse(response.status, raw, request);
24883
+ this.doResponse(response.status, raw, request2);
24881
24884
  });
24882
24885
  }
24883
24886
  }).catch((e) => {
@@ -24895,7 +24898,7 @@ var require_restnode = __commonJS({
24895
24898
  resource = resource.replace("{" + key + "}", this._internal.inputValues[key]);
24896
24899
  }
24897
24900
  }
24898
- const request = {
24901
+ const request2 = {
24899
24902
  resource,
24900
24903
  headers: {},
24901
24904
  method: this._internal.method !== void 0 ? this._internal.method : "GET",
@@ -24905,10 +24908,10 @@ var require_restnode = __commonJS({
24905
24908
  this._internal.requestFunc.apply(this._internal.self, [
24906
24909
  this._internal.inputValues,
24907
24910
  this._internal.outputValuesProxy,
24908
- request
24911
+ request2
24909
24912
  ]);
24910
24913
  }
24911
- this.doExternalFetch(request);
24914
+ this.doExternalFetch(request2);
24912
24915
  },
24913
24916
  cancelFetch: function() {
24914
24917
  if (typeof _noodl_cloud_runtime_version === "undefined") {
@@ -27291,6 +27294,7 @@ var require_simplejavascript = __commonJS({
27291
27294
  return names;
27292
27295
  }
27293
27296
  function publishHeldWrites(node) {
27297
+ sendHeldChanges(node);
27294
27298
  node._internal.heldWrites.forEach((prop) => {
27295
27299
  node.registerOutputIfNeeded("out-" + prop);
27296
27300
  node.flagOutputDirty("out-" + prop);
@@ -27334,7 +27338,7 @@ var require_simplejavascript = __commonJS({
27334
27338
  if (value2 !== this._internal.outputValues[prop]) {
27335
27339
  this.registerOutputIfNeeded("out-" + prop);
27336
27340
  this._internal.outputValues[prop] = value2;
27337
- this.flagOutputDirty("out-" + prop);
27341
+ holdChange(this, prop);
27338
27342
  this._internal.heldWrites.delete(prop);
27339
27343
  } else {
27340
27344
  this._internal.heldWrites.add(prop);
@@ -27513,7 +27517,7 @@ var require_simplejavascript = __commonJS({
27513
27517
  });
27514
27518
  },
27515
27519
  runScript: async function() {
27516
- const func = this._internal.func;
27520
+ const func = sendingChangesAfterTheSyncPart(this, this._internal.func);
27517
27521
  const tokens = this._internal.pendingRunOutcomes || [];
27518
27522
  this._internal.pendingRunOutcomes = void 0;
27519
27523
  if (func === void 0) {
@@ -27955,6 +27959,32 @@ var require_simplejavascript = __commonJS({
27955
27959
  });
27956
27960
  }
27957
27961
  };
27962
+ function holdChange(node, prop) {
27963
+ const internal2 = node._internal;
27964
+ if (!internal2.heldChanges) internal2.heldChanges = /* @__PURE__ */ new Set();
27965
+ internal2.heldChanges.add(prop);
27966
+ if (internal2.heldChangesQueued) return;
27967
+ internal2.heldChangesQueued = true;
27968
+ Promise.resolve().then(() => sendHeldChanges(node));
27969
+ }
27970
+ function sendHeldChanges(node) {
27971
+ const internal2 = node._internal;
27972
+ internal2.heldChangesQueued = false;
27973
+ const held = internal2.heldChanges;
27974
+ if (!held || held.size === 0 || node._deleted) return;
27975
+ internal2.heldChanges = /* @__PURE__ */ new Set();
27976
+ held.forEach((prop) => node.flagOutputDirty("out-" + prop));
27977
+ }
27978
+ function sendingChangesAfterTheSyncPart(node, func) {
27979
+ if (func === void 0) return void 0;
27980
+ return function(...args) {
27981
+ try {
27982
+ return func.apply(this, args);
27983
+ } finally {
27984
+ sendHeldChanges(node);
27985
+ }
27986
+ };
27987
+ }
27958
27988
  module2.exports = SimpleJavascriptNodeModule;
27959
27989
  }
27960
27990
  });
@@ -29891,22 +29921,22 @@ function connectionKey(dialect, base, token, where2) {
29891
29921
  const filter2 = filterKey(where2);
29892
29922
  return dialect + " " + base + " " + token + (filter2 ? " #" + filter2 : "");
29893
29923
  }
29894
- function connectionFor(request) {
29895
- const host = request.host || DEFAULT_HOST2;
29924
+ function connectionFor(request2) {
29925
+ const host = request2.host || DEFAULT_HOST2;
29896
29926
  let pool = POOLS.get(host);
29897
29927
  if (!pool) {
29898
29928
  pool = /* @__PURE__ */ new Map();
29899
29929
  POOLS.set(host, pool);
29900
29930
  }
29901
- const key = connectionKey(request.dialect.name, request.base, request.token, request.where);
29931
+ const key = connectionKey(request2.dialect.name, request2.base, request2.token, request2.where);
29902
29932
  const existing = pool.get(key);
29903
29933
  if (existing) return existing;
29904
29934
  const created = new SharedSseConnection(
29905
- request.dialect,
29906
- request.base,
29907
- request.token,
29908
- request.EventSourceImpl,
29909
- request.fetchImpl,
29935
+ request2.dialect,
29936
+ request2.base,
29937
+ request2.token,
29938
+ request2.EventSourceImpl,
29939
+ request2.fetchImpl,
29910
29940
  () => {
29911
29941
  const current = POOLS.get(host);
29912
29942
  if (current && current.get(key) === created) current.delete(key);
@@ -32970,8 +33000,8 @@ var init_probes = __esm({
32970
33000
  const cacheKey = `${backendId}\0${key}`;
32971
33001
  const existing = this._inFlight.get(cacheKey);
32972
33002
  if (existing) return existing;
32973
- const request = { key, probe: capability.probe, url: url.replace(/\/+$/, ""), type };
32974
- const pending = this._runner(request).then((outcome) => {
33003
+ const request2 = { key, probe: capability.probe, url: url.replace(/\/+$/, ""), type };
33004
+ const pending = this._runner(request2).then((outcome) => {
32975
33005
  this._record(backendId, key, outcome);
32976
33006
  return outcome;
32977
33007
  }).catch(() => {
@@ -41514,8 +41544,8 @@ var require_dbmodelcrudbase = __commonJS({
41514
41544
  const store = CloudStore7.forBackend(modelScope, backendId);
41515
41545
  return store ? store.currentUserId() : void 0;
41516
41546
  } else {
41517
- const request = modelScope.get("Request");
41518
- return request.UserId;
41547
+ const request2 = modelScope.get("Request");
41548
+ return request2.UserId;
41519
41549
  }
41520
41550
  }
41521
41551
  function _addAccessControl(def2) {
@@ -69189,6 +69219,133 @@ var init_tooltips = __esm({
69189
69219
  }
69190
69220
  });
69191
69221
 
69222
+ // ../noodl-viewer-react/src/keyboard-click.ts
69223
+ function holdsPressable(el) {
69224
+ return el.querySelector(PRESSABLE_INSIDE) !== null;
69225
+ }
69226
+ function clickIsWired(node) {
69227
+ if (!node.hasOutput("onClick")) return false;
69228
+ return reachesSomething(node.getOutput("onClick"), 0);
69229
+ }
69230
+ function reachesSomething(output, depth) {
69231
+ if (!output.hasConnections()) return false;
69232
+ if (!Array.isArray(output.connections)) return true;
69233
+ return output.connections.some((c) => !goesNowhere(c, depth));
69234
+ }
69235
+ function goesNowhere(connection, depth) {
69236
+ const target = connection.node;
69237
+ if (!target || target.name !== "Component Outputs" || typeof connection.inputPortName !== "string") return false;
69238
+ const owner = target.nodeScope?.componentOwner;
69239
+ if (!owner || depth > 16 || owner._internal?.creatorCallbacks) return false;
69240
+ if (typeof owner.hasOutput !== "function" || typeof owner.getOutput !== "function") return false;
69241
+ if (!owner.hasOutput(connection.inputPortName)) return true;
69242
+ return !reachesSomething(owner.getOutput(connection.inputPortName), depth + 1);
69243
+ }
69244
+ function isNativelyInteractive(el) {
69245
+ const tag = el.tagName;
69246
+ if (tag === "BUTTON" || tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA" || tag === "SUMMARY") return true;
69247
+ if ((tag === "A" || tag === "AREA") && el.hasAttribute("href")) return true;
69248
+ if ((tag === "VIDEO" || tag === "AUDIO") && el.hasAttribute("controls")) return true;
69249
+ return el.isContentEditable === true;
69250
+ }
69251
+ function isSpace(e) {
69252
+ return e.key === " " || e.key === "Spacebar";
69253
+ }
69254
+ function isOwnKey(el, e) {
69255
+ return e.target === el && !e.defaultPrevented && !e.altKey && !e.ctrlKey && !e.metaKey && !e.isComposing;
69256
+ }
69257
+ function mark(el) {
69258
+ let spaceArmed = false;
69259
+ const onKeyDown2 = (e) => {
69260
+ if (!isOwnKey(el, e)) return;
69261
+ if (e.key === "Enter") {
69262
+ e.preventDefault();
69263
+ if (!e.repeat) el.click();
69264
+ } else if (isSpace(e)) {
69265
+ e.preventDefault();
69266
+ if (!e.repeat) spaceArmed = true;
69267
+ }
69268
+ };
69269
+ const onKeyUp = (e) => {
69270
+ if (!isSpace(e)) return;
69271
+ const armed = spaceArmed;
69272
+ spaceArmed = false;
69273
+ if (armed && e.target === el && !e.defaultPrevented) el.click();
69274
+ };
69275
+ const m = { el, added: [], onKeyDown: onKeyDown2, onKeyUp, observer: null };
69276
+ if (isNativelyInteractive(el)) return m;
69277
+ refresh(m);
69278
+ if (typeof MutationObserver !== "undefined") {
69279
+ m.observer = new MutationObserver(() => refresh(m));
69280
+ m.observer.observe(el, {
69281
+ childList: true,
69282
+ subtree: true,
69283
+ attributes: true,
69284
+ attributeFilter: ["tabindex", "contenteditable", "href", "controls", "type"]
69285
+ });
69286
+ }
69287
+ return m;
69288
+ }
69289
+ function refresh(m) {
69290
+ const { el } = m;
69291
+ const want = !holdsPressable(el);
69292
+ if (want && !m.added.length) {
69293
+ if (!el.hasAttribute("tabindex")) {
69294
+ el.setAttribute("tabindex", "0");
69295
+ m.added.push("tabindex");
69296
+ }
69297
+ if (!el.hasAttribute("role")) {
69298
+ el.setAttribute("role", "button");
69299
+ m.added.push("role");
69300
+ }
69301
+ if (m.added.length) {
69302
+ el.setAttribute(KEYBOARD_CLICK_ATTRIBUTE, "");
69303
+ m.added.push(KEYBOARD_CLICK_ATTRIBUTE);
69304
+ el.addEventListener("keydown", m.onKeyDown);
69305
+ el.addEventListener("keyup", m.onKeyUp);
69306
+ }
69307
+ } else if (!want && m.added.length) {
69308
+ giveBack(m);
69309
+ }
69310
+ }
69311
+ function giveBack(m) {
69312
+ for (const name2 of m.added) m.el.removeAttribute(name2);
69313
+ m.added = [];
69314
+ m.el.removeEventListener("keydown", m.onKeyDown);
69315
+ m.el.removeEventListener("keyup", m.onKeyUp);
69316
+ }
69317
+ function unmark(m) {
69318
+ m.observer?.disconnect();
69319
+ giveBack(m);
69320
+ }
69321
+ function syncKeyboardClick(node) {
69322
+ const el = typeof node.getDOMElement === "function" ? node.getDOMElement() : null;
69323
+ const want = el && clickIsWired(node) ? el : null;
69324
+ const was = marked.get(node);
69325
+ if (was && was.el === want) return;
69326
+ if (was) {
69327
+ unmark(was);
69328
+ marked.delete(node);
69329
+ }
69330
+ if (want) marked.set(node, mark(want));
69331
+ }
69332
+ var KEYBOARD_CLICK_ATTRIBUTE, PRESSABLE_INSIDE, marked, KEYBOARD_CLICK_HOOKS;
69333
+ var init_keyboard_click = __esm({
69334
+ "../noodl-viewer-react/src/keyboard-click.ts"() {
69335
+ KEYBOARD_CLICK_ATTRIBUTE = "data-ndl-keyboard-click";
69336
+ PRESSABLE_INSIDE = 'button, input:not([type="hidden"]), select, textarea, summary, a[href], area[href], video[controls], audio[controls], [contenteditable=""], [contenteditable="true"], [tabindex]:not([tabindex="-1"])';
69337
+ marked = /* @__PURE__ */ new WeakMap();
69338
+ KEYBOARD_CLICK_HOOKS = {
69339
+ onFirstConnectionAdded() {
69340
+ syncKeyboardClick(this);
69341
+ },
69342
+ onLastConnectionRemoved() {
69343
+ syncKeyboardClick(this);
69344
+ }
69345
+ };
69346
+ }
69347
+ });
69348
+
69192
69349
  // ../noodl-viewer-react/src/node-shared-port-definitions.ts
69193
69350
  function humanCorner(suffix) {
69194
69351
  return suffix.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
@@ -69456,6 +69613,7 @@ var init_node_shared_port_definitions = __esm({
69456
69613
  init_fontloader();
69457
69614
  init_media_source();
69458
69615
  init_tooltips();
69616
+ init_keyboard_click();
69459
69617
  SIZE_MODES = ["explicit", "contentWidth", "contentHeight", "contentSize"];
69460
69618
  node_shared_port_definitions_default = {
69461
69619
  addInputProps: addInputProps2,
@@ -69963,7 +70121,9 @@ var init_node_shared_port_definitions = __esm({
69963
70121
  description: "Fires when this element is clicked or tapped",
69964
70122
  group: "Events",
69965
70123
  type: "signal",
69966
- propPath: "pointer"
70124
+ propPath: "pointer",
70125
+ ...KEYBOARD_CLICK_HOOKS
70126
+ // P121-TWO003-GROUPKEYBOARD: wired, the element is a button to Tab and a screen reader
69967
70127
  },
69968
70128
  pointerDown: {
69969
70129
  displayName: "Pointer Down",
@@ -73684,6 +73844,7 @@ function createNodeFromReactComponent(def2) {
73684
73844
  }
73685
73845
  if (element) syncDataAttributesOnElement(this);
73686
73846
  if (element) elementChanged(this, element);
73847
+ syncKeyboardClick(this);
73687
73848
  },
73688
73849
  /**
73689
73850
  * NDA-012 (Visual) check A3 — run an imperative action on the inner React component,
@@ -74110,6 +74271,7 @@ var init_react_component_node = __esm({
74110
74271
  init_mergedeep();
74111
74272
  init_node_shared_port_definitions();
74112
74273
  init_node_transitions();
74274
+ init_keyboard_click();
74113
74275
  ICON_SOURCE_DIAGNOSTIC = "visual/icon-source-not-an-icon";
74114
74276
  DATA_ATTRIBUTE_NAME_PATTERN = "^data-[a-z0-9][a-z0-9._-]*$";
74115
74277
  DATA_ATTRIBUTE_NAME = new RegExp(DATA_ATTRIBUTE_NAME_PATTERN);
@@ -80880,6 +81042,38 @@ function _typeName(t) {
80880
81042
  if (typeof t === "object") return t.name;
80881
81043
  else return t;
80882
81044
  }
81045
+ function isInsideRow(node, row) {
81046
+ if (node === row) return true;
81047
+ let owner = (node.parentNodeScope || node.nodeScope)?.componentOwner;
81048
+ for (let depth = 0; owner && depth < 64; depth++) {
81049
+ if (owner === row) return true;
81050
+ const next = (owner.parentNodeScope || owner.nodeScope)?.componentOwner;
81051
+ if (next === owner) return false;
81052
+ owner = next;
81053
+ }
81054
+ return false;
81055
+ }
81056
+ function settleRow(context, row) {
81057
+ const self2 = row;
81058
+ for (let pass = 0; pass < 10; pass++) {
81059
+ const dirty = context._dirtyNodes;
81060
+ if (!Array.isArray(dirty)) return;
81061
+ const mine = [];
81062
+ for (let i2 = 0; i2 < dirty.length; i2++) {
81063
+ const node = dirty[i2];
81064
+ if (node && node._dirty && !node._deleted && mine.indexOf(node) === -1 && isInsideRow(node, self2))
81065
+ mine.push(node);
81066
+ }
81067
+ if (mine.length === 0) return;
81068
+ for (let i2 = 0; i2 < mine.length; i2++) {
81069
+ try {
81070
+ mine[i2].update();
81071
+ } catch (e) {
81072
+ console.error(e);
81073
+ }
81074
+ }
81075
+ }
81076
+ }
80883
81077
  var import_react16, import_runtime4, import_collection, import_model5, ITEMS_DIAGNOSTIC, defaultDynamicScript, ForEachDefinition, defaultMapCode, ForEachModule, foreach_default;
80884
81078
  var init_foreach = __esm({
80885
81079
  "../noodl-viewer-react/src/nodes/std-library/data/foreach.tsx"() {
@@ -81253,6 +81447,7 @@ var init_foreach = __esm({
81253
81447
  }
81254
81448
  }
81255
81449
  };
81450
+ settleRow(this.context, itemNode);
81256
81451
  const forEachActions = itemNode.nodeScope.getNodesWithType("For Each Actions");
81257
81452
  for (let j = 0; j < forEachActions.length; j++) {
81258
81453
  forEachActions[j].signalAdded();
@@ -90070,10 +90265,16 @@ function Text(props) {
90070
90265
  ...pointerProps(props),
90071
90266
  style: style2
90072
90267
  },
90073
- renderableText(props.text)
90268
+ style2.textOverflow === "ellipsis" ? (
90269
+ // P18-S112-FLEXELLIPSIS — the Text is `display: flex` (its default CSS, which Text Vertical Align's `alignItems`
90270
+ // needs), so its words were an anonymous flex item and `text-overflow` on the element drew no "…": Ellipsis
90271
+ // looked the same as Clip. The words go in a block of their own that may shrink to the box and draws the "…";
90272
+ // the element keeps its flex, so both alignments still place them.
90273
+ /* @__PURE__ */ import_react22.default.createElement("span", { style: { ...ELLIPSIS_LINE, overflow: style2.overflow } }, renderableText(props.text))
90274
+ ) : renderableText(props.text)
90074
90275
  );
90075
90276
  }
90076
- var import_react22;
90277
+ var import_react22, ELLIPSIS_LINE;
90077
90278
  var init_Text = __esm({
90078
90279
  "../noodl-viewer-react/src/components/visual/Text/Text.tsx"() {
90079
90280
  import_react22 = __toESM(require_react());
@@ -90081,6 +90282,7 @@ var init_Text = __esm({
90081
90282
  init_layout();
90082
90283
  init_pointerlisteners();
90083
90284
  init_noodl_root_ref();
90285
+ ELLIPSIS_LINE = { minWidth: 0, textOverflow: "ellipsis", whiteSpace: "nowrap" };
90084
90286
  }
90085
90287
  });
90086
90288
 
@@ -90454,7 +90656,6 @@ var init_Video = __esm({
90454
90656
  ...videoProps,
90455
90657
  playsInline: true,
90456
90658
  src,
90457
- ...pointerProps(this.props),
90458
90659
  ref: (video) => {
90459
90660
  this.video = video;
90460
90661
  this.props.innerRef(video);
@@ -90473,7 +90674,7 @@ var init_Video = __esm({
90473
90674
  this.canPlay = false;
90474
90675
  }
90475
90676
  setSourceObject(src) {
90476
- if (this.video.srcObject !== src) {
90677
+ if (this.video && this.video.srcObject !== src) {
90477
90678
  this.video.srcObject = src;
90478
90679
  this.canPlay = false;
90479
90680
  }
@@ -90579,6 +90780,7 @@ var init_Video = __esm({
90579
90780
  CachedVideo,
90580
90781
  {
90581
90782
  ...props.dom,
90783
+ ...pointerProps(this.props),
90582
90784
  className: withDefaultsClass(props.className, defaultsClass),
90583
90785
  style: style2,
90584
90786
  innerRef: (video) => {
@@ -97687,6 +97889,7 @@ var init_parentcomponentobject = __esm({
97687
97889
  */
97688
97890
  nodeScopeDidInitialize() {
97689
97891
  this.context.scheduleAfterUpdate(() => {
97892
+ if (this._deleted) return;
97690
97893
  this._internal.resolutionIsLoud = true;
97691
97894
  this.onComponentStateNodesChanged();
97692
97895
  });
@@ -102063,7 +102266,7 @@ var init_signinwith = __esm({
102063
102266
  const tokens = [this.beginOutcome()];
102064
102267
  if (state2.succeeded) {
102065
102268
  this._internal.notice = state2.notice;
102066
- this._internal.error = void 0;
102269
+ this._internal.error = "";
102067
102270
  this.flagOutputDirty("notice");
102068
102271
  this.flagOutputDirty("error");
102069
102272
  reportOutcomes(this, tokens, "done");
@@ -114719,7 +114922,7 @@ var require_package = __commonJS({
114719
114922
  "package.json"(exports2, module2) {
114720
114923
  module2.exports = {
114721
114924
  name: "@nodegx/preview",
114722
- version: "0.2.7",
114925
+ version: "0.2.8",
114723
114926
  description: "Standalone live-preview harness: renders a NodeGX (OpenNoodl) v2 project in a browser with no editor process, and reloads it when the files on disk change.",
114724
114927
  license: "GPL-3.0-only",
114725
114928
  homepage: "https://nodegx.io",
@@ -164665,7 +164868,7 @@ var node_catalog_default = {
164665
164868
  "always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
164666
164869
  ],
164667
164870
  visible: [
164668
- "always: shown or hidden at the next paint, its space kept; hidden, a Focus does not reach it"
164871
+ "always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
164669
164872
  ]
164670
164873
  },
164671
164874
  warnings: {
@@ -166850,7 +167053,7 @@ var node_catalog_default = {
166850
167053
  "always: the page, or the scrolling Group holding it, scrolls until the box can be seen"
166851
167054
  ],
166852
167055
  visible: [
166853
- "always: shown or hidden at the next paint, its space kept; hidden, a Focus does not reach it"
167056
+ "always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
166854
167057
  ]
166855
167058
  },
166856
167059
  warnings: {
@@ -169462,7 +169665,7 @@ var node_catalog_default = {
169462
169665
  "always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
169463
169666
  ],
169464
169667
  visible: [
169465
- "always: shown or hidden at the next paint, its space kept; hidden, a Focus does not reach it"
169668
+ "always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
169466
169669
  ]
169467
169670
  },
169468
169671
  warnings: {
@@ -171644,7 +171847,7 @@ var node_catalog_default = {
171644
171847
  "always: the page, or the scrolling Group holding it, scrolls until the button can be seen"
171645
171848
  ],
171646
171849
  visible: [
171647
- "always: shown or hidden at the next paint, its space kept; hidden, it takes no click and a Focus does not reach it"
171850
+ "always: shown or hidden at once, its space kept; hidden, it takes no click and a Focus does not reach it"
171648
171851
  ]
171649
171852
  },
171650
171853
  warnings: {
@@ -174049,7 +174252,7 @@ var node_catalog_default = {
174049
174252
  "always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
174050
174253
  ],
174051
174254
  visible: [
174052
- "always: shown or hidden at the next paint, its space kept; hidden, a Focus does not reach it"
174255
+ "always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
174053
174256
  ]
174054
174257
  },
174055
174258
  warnings: {
@@ -176919,7 +177122,7 @@ var node_catalog_default = {
176919
177122
  "always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
176920
177123
  ],
176921
177124
  visible: [
176922
- "always: shown or hidden at the next paint, its space kept; hidden, a Focus does not reach it"
177125
+ "always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
176923
177126
  ],
176924
177127
  opacity: [
176925
177128
  "always: how see-through it is; at exactly 0 it takes no pointer, and an act goes to what is behind it"
@@ -181761,7 +181964,7 @@ var node_catalog_default = {
181761
181964
  },
181762
181965
  isSignal: false,
181763
181966
  default: false,
181764
- description: "Whether a request with no valid session token is allowed to run this function at all"
181967
+ description: "Whether a request with no valid session token is allowed to run this function at all. A run the app starts itself \u2014 a schedule, a row change, a run from Studio \u2014 is always allowed and sees nobody signed in; a webhook is a request like any other"
181765
181968
  },
181766
181969
  {
181767
181970
  name: "params",
@@ -183818,14 +184021,14 @@ var TOKEN_COOKIE = "nodegx_token";
183818
184021
  function mintToken() {
183819
184022
  return (0, import_node_crypto.randomBytes)(32).toString("hex");
183820
184023
  }
183821
- function resolveAccess(request = {}) {
183822
- const shared = request.share === true;
184024
+ function resolveAccess(request2 = {}) {
184025
+ const shared = request2.share === true;
183823
184026
  return {
183824
- host: shared ? request.host || ALL_INTERFACES : LOOPBACK,
184027
+ host: shared ? request2.host || ALL_INTERFACES : LOOPBACK,
183825
184028
  shared,
183826
184029
  // A shared server always has a token. An unshared one still gets one, so that turning
183827
184030
  // sharing on later cannot find itself without a credential and invent a weaker path.
183828
- token: request.token || mintToken()
184031
+ token: request2.token || mintToken()
183829
184032
  };
183830
184033
  }
183831
184034
  function tokenMatches(candidate, token) {
@@ -183841,15 +184044,15 @@ function isLoopbackAddress(address) {
183841
184044
  if (addr === "::1") return true;
183842
184045
  return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(addr);
183843
184046
  }
183844
- function tokenFromRequest(request) {
183845
- const headers = request.headers || {};
184047
+ function tokenFromRequest(request2) {
184048
+ const headers = request2.headers || {};
183846
184049
  const header = headers[TOKEN_HEADER];
183847
184050
  if (typeof header === "string" && header.length > 0) return header;
183848
184051
  const auth = headers["authorization"];
183849
184052
  if (typeof auth === "string" && /^Bearer\s+/i.test(auth)) return auth.replace(/^Bearer\s+/i, "");
183850
- if (typeof request.url === "string") {
184053
+ if (typeof request2.url === "string") {
183851
184054
  try {
183852
- const query = new URL(request.url, "http://placeholder.invalid").searchParams.get(TOKEN_QUERY);
184055
+ const query = new URL(request2.url, "http://placeholder.invalid").searchParams.get(TOKEN_QUERY);
183853
184056
  if (query) return query;
183854
184057
  } catch {
183855
184058
  }
@@ -183863,11 +184066,11 @@ function tokenFromRequest(request) {
183863
184066
  }
183864
184067
  return null;
183865
184068
  }
183866
- function authoriseRequest(request, access) {
183867
- if (!access.shared || isLoopbackAddress(request.remoteAddress)) {
184069
+ function authoriseRequest(request2, access) {
184070
+ if (!access.shared || isLoopbackAddress(request2.remoteAddress)) {
183868
184071
  return { ok: true, reason: "loopback", setCookie: null };
183869
184072
  }
183870
- const presented = tokenFromRequest(request);
184073
+ const presented = tokenFromRequest(request2);
183871
184074
  if (presented === null) {
183872
184075
  return { ok: false, reason: "no-token", status: 401, body: UNAUTHORISED_BODY };
183873
184076
  }
@@ -185708,8 +185911,8 @@ function renderMarkdown(md) {
185708
185911
  continue;
185709
185912
  }
185710
185913
  if (lines.every((l) => /^\s*>\s?/.test(l))) {
185711
- const quoted = lines.map((l) => l.replace(/^\s*>\s?/, "")).join("\n");
185712
- html.push(`<blockquote>${renderMarkdown(quoted)}</blockquote>`);
185914
+ const quoted2 = lines.map((l) => l.replace(/^\s*>\s?/, "")).join("\n");
185915
+ html.push(`<blockquote>${renderMarkdown(quoted2)}</blockquote>`);
185713
185916
  continue;
185714
185917
  }
185715
185918
  if (lines.every((l) => /^\s*[-*]\s+/.test(l))) {
@@ -190891,18 +191094,18 @@ function capabilityProbes() {
190891
191094
  if (!instance) instance = new CapabilityProbeCache({ runner: runProbe });
190892
191095
  return instance;
190893
191096
  }
190894
- async function runProbe(request) {
190895
- switch (request.key) {
191097
+ async function runProbe(request2) {
191098
+ switch (request2.key) {
190896
191099
  case "realtime.subscribe":
190897
- return probeRealtime(request);
191100
+ return probeRealtime(request2);
190898
191101
  case "data.aggregate":
190899
- return probeAggregate(request);
191102
+ return probeAggregate(request2);
190900
191103
  default:
190901
- throw new Error(`No probe implemented for ${request.key}`);
191104
+ throw new Error(`No probe implemented for ${request2.key}`);
190902
191105
  }
190903
191106
  }
190904
- function probeRealtime(request) {
190905
- const url = websocketUrl(request.url, request.probe.path);
191107
+ function probeRealtime(request2) {
191108
+ const url = websocketUrl(request2.url, request2.probe.path);
190906
191109
  return new Promise((resolve5) => {
190907
191110
  let settled = false;
190908
191111
  let socket = null;
@@ -190935,8 +191138,8 @@ function probeRealtime(request) {
190935
191138
  socket.onclose = () => settle({ verdict: "unsupported", at: Date.now(), detail: "the websocket closed without upgrading" });
190936
191139
  });
190937
191140
  }
190938
- async function probeAggregate(request) {
190939
- const response = await fetchWithDeadline(`${request.url}${request.probe.path}`, request.probe.method);
191141
+ async function probeAggregate(request2) {
191142
+ const response = await fetchWithDeadline(`${request2.url}${request2.probe.path}`, request2.probe.method);
190940
191143
  if (!response) return { verdict: "unsupported", at: Date.now(), detail: "the server did not answer" };
190941
191144
  if (response.status !== 200) {
190942
191145
  return { verdict: "unsupported", at: Date.now(), detail: `the server answered ${response.status}` };
@@ -194529,6 +194732,55 @@ function cloudPortMeaning(type, plug) {
194529
194732
  init_scriptfiles();
194530
194733
  var FUNCTION_NODE_TYPE = "JavaScriptFunction";
194531
194734
 
194735
+ // ../noodl-editor/src/editor/src/models/nodelibrary/cloudDynamicPorts.ts
194736
+ init_function_ports();
194737
+ function proplist(parameters, name2) {
194738
+ const value2 = parameters[name2];
194739
+ return Array.isArray(value2) ? value2 : [];
194740
+ }
194741
+ function scriptPortsFromSource(script) {
194742
+ const ports = [];
194743
+ if (typeof script !== "string") return ports;
194744
+ const names = functionScriptPortNames(script);
194745
+ const add = (name2, plug, type, prefix) => {
194746
+ if (ports.some((p) => p.name === prefix + name2 && p.plug === plug)) return;
194747
+ ports.push({ name: prefix + name2, displayName: name2, plug, type, group: plug === "input" ? "Inputs" : "Outputs" });
194748
+ };
194749
+ for (const name2 of names.inputs) add(name2, "input", "*", "in-");
194750
+ for (const name2 of names.signals) add(name2, "output", "signal", "out-");
194751
+ for (const name2 of names.outputs) add(name2, "output", "*", "out-");
194752
+ return ports;
194753
+ }
194754
+ function scriptPortsForNode(node) {
194755
+ const parameters = node.parameters || {};
194756
+ const ports = [];
194757
+ for (const spec of proplist(parameters, "scriptOutputs")) {
194758
+ if (!spec.label) continue;
194759
+ ports.push({
194760
+ name: "out-" + spec.label,
194761
+ displayName: spec.label,
194762
+ plug: "output",
194763
+ type: parameters["outtype-" + spec.label] || "*",
194764
+ group: "Outputs"
194765
+ });
194766
+ }
194767
+ for (const spec of proplist(parameters, "scriptInputs")) {
194768
+ if (!spec.label) continue;
194769
+ ports.push({
194770
+ name: "in-" + spec.label,
194771
+ displayName: spec.label,
194772
+ plug: "input",
194773
+ type: parameters["intype-" + spec.label] || "string",
194774
+ group: "Inputs"
194775
+ });
194776
+ }
194777
+ for (const port of scriptPortsFromSource(parameters["functionScript"])) {
194778
+ if (ports.some((p) => p.name === port.name && p.plug === port.plug)) continue;
194779
+ ports.push(port);
194780
+ }
194781
+ return ports;
194782
+ }
194783
+
194532
194784
  // ../noodl-editor/src/editor/src/validation/functionPorts.ts
194533
194785
  var SCRIPT_NODE_API_MEMBERS = [
194534
194786
  "Inputs",
@@ -194784,7 +195036,11 @@ var AUTHORED_BLOCKING_WARNINGS = /* @__PURE__ */ new Set([
194784
195036
  // Bug P121-TWO003-SCRIPTCOMPILE — the runtime's own compile fails, so every Run fails. No benign
194785
195037
  // reading for a graph an agent just wrote; the write said nothing before, and the first person
194786
195038
  // to run the function got "That could not be done."
194787
- "function-script-not-compiled" /* FunctionScriptNotCompiled */
195039
+ "function-script-not-compiled" /* FunctionScriptNotCompiled */,
195040
+ // Bug P127-BKO006-RUNONCHANGEGUESS — a `runOnChange-` name a Function does not have is never read
195041
+ // (2,805 right and 5 wrong over 2,718 Functions, all five agent-written); the message names the
195042
+ // right one.
195043
+ "function-run-on-change-unknown" /* FunctionRunOnChangeUnknown */
194788
195044
  ]);
194789
195045
 
194790
195046
  // ../noodl-editor/src/editor/src/validation/diagnosticExamples.ts
@@ -194924,6 +195180,12 @@ var DIAGNOSTIC_EXAMPLES = [
194924
195180
  code: "function-guards-an-open-collection" /* FunctionGuardsAnOpenCollection */,
194925
195181
  examples: ["cloud-members-can-edit-my-records"],
194926
195182
  why: `the records only a function creates, with the collection's create rule "nobody": the function runs as the system`
195183
+ },
195184
+ // P112-OWN018-FANOUT (ruled 2026-10-27 "Teach it and warn") — the warning carries the chained shape.
195185
+ {
195186
+ code: "condition-reads-a-siblings-change" /* ConditionReadsASiblingsChange */,
195187
+ examples: ["score-then-move-on"],
195188
+ why: "the press evaluating the gate that reads, and that gate's Done evaluating the gate that moves on, so the score is decided first"
194927
195189
  }
194928
195190
  ];
194929
195191
 
@@ -196510,6 +196772,156 @@ var functionNeverReruns = {
196510
196772
  }
196511
196773
  };
196512
196774
 
196775
+ // ../noodl-editor/src/editor/src/validation/rules/functionRunOnChangeUnknown.ts
196776
+ var RUN_ON_CHANGE_PREFIX4 = "runOnChange-";
196777
+ var FUNCTION_INPUT_PREFIX = "in-";
196778
+ var functionRunOnChangeUnknown = {
196779
+ code: "function-run-on-change-unknown" /* FunctionRunOnChangeUnknown */,
196780
+ description: "A Function has a Run On Value Change setting under a name it does not have (its boxes are runOnChange-in-<input>).",
196781
+ defaultEnabled: true,
196782
+ run(ctx) {
196783
+ const out = [];
196784
+ for (const { component } of ctx.components) {
196785
+ for (const node of component.nodes) {
196786
+ if (node.type !== FUNCTION_NODE_TYPE || !node.parameters) continue;
196787
+ const guessed = Object.keys(node.parameters).filter(
196788
+ (key) => key.startsWith(RUN_ON_CHANGE_PREFIX4) && !key.startsWith(RUN_ON_CHANGE_PREFIX4 + FUNCTION_INPUT_PREFIX)
196789
+ );
196790
+ if (guessed.length === 0) continue;
196791
+ const inputs = scriptPortsForNode(node).filter((p) => p.plug === "input" && p.name.startsWith(FUNCTION_INPUT_PREFIX)).map((p) => p.name.slice(FUNCTION_INPUT_PREFIX.length));
196792
+ const name2 = node.label ? `"${node.label}"` : "This Function";
196793
+ for (const key of guessed) {
196794
+ const wanted = key.slice(RUN_ON_CHANGE_PREFIX4.length);
196795
+ const input2 = inputs.includes(wanted) ? wanted : nearest(wanted, inputs);
196796
+ const right = input2 !== void 0 ? `${RUN_ON_CHANGE_PREFIX4}${FUNCTION_INPUT_PREFIX}${input2}` : void 0;
196797
+ out.push({
196798
+ code: "function-run-on-change-unknown" /* FunctionRunOnChangeUnknown */,
196799
+ severity: "warning",
196800
+ message: `${name2} has no setting "${key}", so nothing reads it` + (input2 === wanted ? ` and a new "${wanted}" still runs it. ` : ". ") + `A Function's Run On Value Change box for an input is "runOnChange-in-<input>"` + (right ? input2 === wanted ? `: write "${right}".` : `; its script reads no "${wanted}" (the nearest input is "${input2}": "${right}").` : `, and its script reads no inputs.`),
196801
+ location: {
196802
+ component: component.name,
196803
+ nodeId: node.id,
196804
+ nodeType: node.type,
196805
+ nodeLabel: node.label,
196806
+ port: key,
196807
+ plug: "input"
196808
+ },
196809
+ ...right ? { suggestion: `${right}: ${JSON.stringify(node.parameters[key])}` } : {}
196810
+ });
196811
+ }
196812
+ }
196813
+ }
196814
+ return out;
196815
+ }
196816
+ };
196817
+
196818
+ // ../noodl-editor/src/editor/src/validation/rules/conditionReadsASiblingsChange.ts
196819
+ var CONDITION = "Condition";
196820
+ var conditionReadsASiblingsChange = {
196821
+ code: "condition-reads-a-siblings-change" /* ConditionReadsASiblingsChange */,
196822
+ description: "Two Conditions evaluated by one signal, where one's On True or On False changes what the other reads.",
196823
+ defaultEnabled: true,
196824
+ run(ctx) {
196825
+ const out = [];
196826
+ const { catalog } = ctx;
196827
+ for (const { component, nodeById } of ctx.components) {
196828
+ let exclusiveStates = function(a, b) {
196829
+ const stateOf = (gate) => {
196830
+ const c = (incoming.get(gate) ?? []).find((w) => w.toProperty === "condition");
196831
+ return c && nodeById.get(c.fromId)?.type === "States" && c.fromProperty.startsWith("at-") ? c : void 0;
196832
+ };
196833
+ const sa = stateOf(a);
196834
+ const sb = stateOf(b);
196835
+ return !!sa && !!sb && sa.fromId === sb.fromId && sa.fromProperty !== sb.fromProperty;
196836
+ }, feedingValues = function(gate) {
196837
+ const seen = /* @__PURE__ */ new Set();
196838
+ const stack = (incoming.get(gate) ?? []).filter((c) => c.toProperty === "condition").map((c) => c.fromId);
196839
+ while (stack.length > 0) {
196840
+ const id = stack.pop();
196841
+ if (id === gate || seen.has(id)) continue;
196842
+ seen.add(id);
196843
+ if (!passesThrough(id)) continue;
196844
+ const type = nodeById.get(id)?.type;
196845
+ for (const c of incoming.get(id) ?? []) {
196846
+ if (type && catalog.getPort(type, "input", c.toProperty)?.isSignal) continue;
196847
+ stack.push(c.fromId);
196848
+ }
196849
+ }
196850
+ return seen;
196851
+ }, reaches2 = function(gate, targets) {
196852
+ if (targets.size === 0) return false;
196853
+ const seen = /* @__PURE__ */ new Set([gate]);
196854
+ const stack = (outgoing.get(gate) ?? []).map((c) => c.toId);
196855
+ while (stack.length > 0) {
196856
+ const id = stack.pop();
196857
+ if (targets.has(id)) return true;
196858
+ if (seen.has(id)) continue;
196859
+ seen.add(id);
196860
+ if (!passesThrough(id)) continue;
196861
+ for (const c of outgoing.get(id) ?? []) stack.push(c.toId);
196862
+ }
196863
+ return false;
196864
+ };
196865
+ const outgoing = /* @__PURE__ */ new Map();
196866
+ const incoming = /* @__PURE__ */ new Map();
196867
+ for (const c of component.connections) {
196868
+ (outgoing.get(c.fromId) ?? outgoing.set(c.fromId, []).get(c.fromId)).push(c);
196869
+ (incoming.get(c.toId) ?? incoming.set(c.toId, []).get(c.toId)).push(c);
196870
+ }
196871
+ const isCondition = (id) => nodeById.get(id)?.type === CONDITION;
196872
+ const passesThrough = (id) => {
196873
+ const type = nodeById.get(id)?.type;
196874
+ const known = type ? catalog.getNode(type) : void 0;
196875
+ return !!known && !known.isVisual;
196876
+ };
196877
+ const gatesBySource = /* @__PURE__ */ new Map();
196878
+ for (const c of component.connections) {
196879
+ if (c.toProperty !== "eval" || !isCondition(c.toId)) continue;
196880
+ const key = `${c.fromId}\0${c.fromProperty}`;
196881
+ const gates = gatesBySource.get(key) ?? [];
196882
+ if (!gates.includes(c.toId)) gates.push(c.toId);
196883
+ gatesBySource.set(key, gates);
196884
+ }
196885
+ const reported = /* @__PURE__ */ new Set();
196886
+ for (const [key, gates] of gatesBySource) {
196887
+ if (gates.length < 2) continue;
196888
+ const sourceId = key.split("\0")[0];
196889
+ for (const reader of gates) {
196890
+ if (reported.has(reader)) continue;
196891
+ const feeds = feedingValues(reader);
196892
+ for (const writer of gates) {
196893
+ if (writer === reader) continue;
196894
+ if (exclusiveStates(reader, writer)) continue;
196895
+ if (!reaches2(writer, feeds)) continue;
196896
+ reported.add(reader);
196897
+ const r = nodeById.get(reader);
196898
+ const w = nodeById.get(writer);
196899
+ const rName = r.label ? `"${r.label}"` : "This Condition";
196900
+ const wName = w.label ? `"${w.label}"` : "another Condition";
196901
+ const sName = nodeById.get(sourceId)?.label ? `"${nodeById.get(sourceId).label}"` : "the same signal";
196902
+ out.push({
196903
+ code: "condition-reads-a-siblings-change" /* ConditionReadsASiblingsChange */,
196904
+ severity: "warning",
196905
+ message: `${rName} and ${wName} are both evaluated by ${sName}, and what ${wName} does changes what ${rName} reads. ${rName} reads its Condition after that change, whichever order the two are wired in. If it should decide on the value from before, chain them: ${rName}'s Done into ${wName}'s Evaluate, instead of both from ${sName}.`,
196906
+ location: {
196907
+ component: component.name,
196908
+ nodeId: r.id,
196909
+ nodeType: r.type,
196910
+ nodeLabel: r.label,
196911
+ port: "eval",
196912
+ plug: "input"
196913
+ },
196914
+ suggestion: `${r.id}.done \u2192 ${w.id}.eval`
196915
+ });
196916
+ break;
196917
+ }
196918
+ }
196919
+ }
196920
+ }
196921
+ return out;
196922
+ }
196923
+ };
196924
+
196513
196925
  // ../noodl-editor/src/editor/src/validation/rules/layoutFillTraps.ts
196514
196926
  var GROUP = "Group";
196515
196927
  var CONTENT_WIDTH = /* @__PURE__ */ new Set(["contentSize", "contentWidth"]);
@@ -196617,6 +197029,97 @@ var rowOfWordsFillsItsColumn = {
196617
197029
  }
196618
197030
  };
196619
197031
 
197032
+ // ../noodl-editor/src/editor/src/validation/rules/clickAreaHoldsAControl.ts
197033
+ var CONTROL_TYPES = /* @__PURE__ */ new Map([
197034
+ ["net.noodl.controls.button", "Button"],
197035
+ ["net.noodl.controls.textinput", "Text Input"],
197036
+ ["net.noodl.controls.checkbox", "Checkbox"],
197037
+ ["net.noodl.controls.radiobutton", "Radio Button"],
197038
+ ["net.noodl.controls.options", "Dropdown"],
197039
+ ["net.noodl.controls.range", "Slider"]
197040
+ ]);
197041
+ var CLICK = "onClick";
197042
+ var COMPONENT_OUTPUTS = "Component Outputs";
197043
+ var clickWired = (component, node) => component.connections.some((c) => c.fromId === node.id && c.fromProperty === CLICK);
197044
+ function reaches(component, nodeById, node, port, used) {
197045
+ return component.connections.some((c) => {
197046
+ if (c.fromId !== node.id || c.fromProperty !== port) return false;
197047
+ if (nodeById.get(c.toId)?.type !== COMPONENT_OUTPUTS) return true;
197048
+ return used === "all" || used.has(c.toProperty);
197049
+ });
197050
+ }
197051
+ var quoted = (node) => node.label ? ` ("${node.label}")` : "";
197052
+ var clickAreaHoldsAControl = {
197053
+ code: "click-area-holds-a-control" /* ClickAreaHoldsAControl */,
197054
+ description: "An element whose Click is wired holds a control, so the keyboard reaches the control and not the element.",
197055
+ defaultEnabled: true,
197056
+ run(ctx) {
197057
+ const out = [];
197058
+ const byPath = new Map(ctx.components.map((c) => [refToPath(c.component.name), c]));
197059
+ const memo = /* @__PURE__ */ new Map();
197060
+ const componentHolds = (ref, used) => {
197061
+ const path8 = refToPath(ref);
197062
+ const key = `${path8}|${[...used].sort().join(",")}`;
197063
+ if (memo.has(key)) return memo.get(key) ?? null;
197064
+ memo.set(key, null);
197065
+ const entry = byPath.get(path8);
197066
+ let found = null;
197067
+ if (entry) {
197068
+ for (const node of entry.component.nodes) {
197069
+ found = held(node, entry.component, entry.nodeById, used);
197070
+ if (found) break;
197071
+ }
197072
+ }
197073
+ memo.set(key, found);
197074
+ return found;
197075
+ };
197076
+ function held(node, component, nodeById, used) {
197077
+ const control = CONTROL_TYPES.get(node.type);
197078
+ if (control) return `a ${control}${quoted(node)}`;
197079
+ if (reaches(component, nodeById, node, CLICK, used)) {
197080
+ return `${node.label ? `"${node.label}"` : "an element"}, which answers a click of its own`;
197081
+ }
197082
+ if (isComponentRef(node.type)) {
197083
+ const ports = new Set(component.connections.filter((c) => c.fromId === node.id).map((c) => c.fromProperty));
197084
+ const usedHere = new Set([...ports].filter((port) => reaches(component, nodeById, node, port, used)));
197085
+ const inside = componentHolds(node.type, usedHere);
197086
+ if (inside) return `the component ${node.label ? `"${node.label}"` : `"${refToPath(node.type)}"`}, which holds ${inside}`;
197087
+ }
197088
+ return null;
197089
+ }
197090
+ for (const { component, nodeById } of ctx.components) {
197091
+ for (const area of component.nodes) {
197092
+ if (CONTROL_TYPES.has(area.type) || !clickWired(component, area)) continue;
197093
+ if (!ctx.catalog.getNode(area.type)?.isVisual) continue;
197094
+ let inner = null;
197095
+ const stack = [...area.children ?? []];
197096
+ while (stack.length && !inner) {
197097
+ const node = nodeById.get(stack.shift());
197098
+ if (!node) continue;
197099
+ inner = held(node, component, nodeById, "all");
197100
+ stack.push(...node.children ?? []);
197101
+ }
197102
+ if (!inner) continue;
197103
+ const name2 = area.label ? `"${area.label}"` : `This ${ctx.catalog.getNode(area.type)?.displayName ?? area.type}`;
197104
+ out.push({
197105
+ code: "click-area-holds-a-control" /* ClickAreaHoldsAControl */,
197106
+ severity: "info",
197107
+ message: `${name2} answers a click. It holds ${inner}. The Tab key stops on what it holds instead. A person using a keyboard or a screen reader cannot press ${area.label ? name2 : "it"} itself. If its click does something nothing inside it does, give that its own Button.`,
197108
+ location: {
197109
+ component: component.name,
197110
+ nodeId: area.id,
197111
+ nodeType: area.type,
197112
+ ...area.label ? { nodeLabel: area.label } : {},
197113
+ port: CLICK,
197114
+ plug: "output"
197115
+ }
197116
+ });
197117
+ }
197118
+ }
197119
+ return out;
197120
+ }
197121
+ };
197122
+
196620
197123
  // ../noodl-editor/src/editor/src/validation/rules/index.ts
196621
197124
  var ALL_RULES = [
196622
197125
  duplicateNodeId,
@@ -196673,6 +197176,10 @@ var ALL_RULES = [
196673
197176
  // arrangement ones.
196674
197177
  functionScriptNotCompiled,
196675
197178
  functionNeverReruns,
197179
+ functionRunOnChangeUnknown,
197180
+ // P112-OWN018-FANOUT (ruled 2026-10-27 "Teach it and warn") — beside the Function rules: what a graph's signals do in
197181
+ // what order, read from the wires. Two gates off one signal, one changing what the other reads.
197182
+ conditionReadsASiblingsChange,
196676
197183
  repeatedSiblingSubtree,
196677
197184
  // DEF-025 — beside `repeatedSiblingSubtree` because both read the visual
196678
197185
  // tree's arrangement rather than wires: that one asks whether siblings
@@ -196686,6 +197193,9 @@ var ALL_RULES = [
196686
197193
  // against the shipped corpus before they were written — this one is 0 hits
196687
197194
  // on all ten templates.
196688
197195
  dialogWithoutName,
197196
+ // P121-TWO003-NESTEDBUTTON (ruled 2026-10-27) — beside the two above: what a keyboard and a screen reader reach, read
197197
+ // from the arrangement. An info, live, so the Problems panel and an agent's write both say it.
197198
+ clickAreaHoldsAControl,
196689
197199
  // P112-OWN020-PILLWRAP / FILLGROW (ruled 2026-10-26: warn and teach) — a node with no size setting fills its parent,
196690
197200
  // read from the arrangement like the two above, and live so a person sees it while editing.
196691
197201
  contentRowSplitsItsChildren,
@@ -197464,6 +197974,7 @@ function validationWarningLines(state2) {
197464
197974
  if (state2.kind !== "ok" || !state2.report) return [];
197465
197975
  return state2.report.diagnostics.filter((d) => d.severity === "warning").map((d) => `${d.code} in ${d.component}${d.nodeLabel || d.nodeId ? ` (${d.nodeLabel ?? d.nodeId})` : ""}: ${d.message}`);
197466
197976
  }
197977
+ var BACKEND_PREFIX = "/__nodegx/backend";
197467
197978
  var MIME = {
197468
197979
  ".html": "text/html; charset=utf-8",
197469
197980
  ".js": "application/javascript; charset=utf-8",
@@ -197577,6 +198088,10 @@ var PreviewServer = class {
197577
198088
  const url = new URL(req.url ?? "/", "http://localhost");
197578
198089
  const pathname = decodeURIComponent(url.pathname);
197579
198090
  if (pathname === "/__preview/events") return this.serveEvents(req, res);
198091
+ const backendPort = this.sharedBackendPort();
198092
+ if (backendPort && (pathname === BACKEND_PREFIX || pathname.startsWith(BACKEND_PREFIX + "/"))) {
198093
+ return this.proxyToBackend(req, res, backendPort);
198094
+ }
197580
198095
  if (pathname === "/__preview/state") {
197581
198096
  const status = {
197582
198097
  state: this.state,
@@ -197600,7 +198115,11 @@ var PreviewServer = class {
197600
198115
  if (!bundle) return this.notFound(res);
197601
198116
  return this.sendDynamic(res, bundle, "application/json; charset=utf-8");
197602
198117
  }
197603
- if (pathname === "/nodegx-config.json") return this.sendDynamic(res, "{}", "application/json; charset=utf-8");
198118
+ if (pathname === "/nodegx-config.json") {
198119
+ const origin = backendPort ? pageOrigin(req) : null;
198120
+ const config = origin ? { cloudservices: { endpoint: origin + BACKEND_PREFIX } } : {};
198121
+ return this.sendDynamic(res, JSON.stringify(config), "application/json; charset=utf-8");
198122
+ }
197604
198123
  const asset = pathname.slice(1);
197605
198124
  if (DEPLOY_ASSETS.includes(asset)) {
197606
198125
  return this.sendFile(req, res, deployAssetPath(asset));
@@ -197651,6 +198170,46 @@ var PreviewServer = class {
197651
198170
  this.clients.delete(res);
197652
198171
  });
197653
198172
  }
198173
+ /** The backend port a SHARED preview carries, else undefined. */
198174
+ sharedBackendPort() {
198175
+ return this.access.shared && this.options.backendPort ? this.options.backendPort : void 0;
198176
+ }
198177
+ /**
198178
+ * P112-OWN023-PHONEPREVIEW — one request on to the backend on loopback, streamed both ways (its realtime channel is a
198179
+ * long `text/event-stream`). The backend is told where the page reached it (`Host`, `X-Forwarded-Prefix`), so a
198180
+ * File's url it answers with leads back through here; the preview's own token is not passed on.
198181
+ */
198182
+ proxyToBackend(req, res, port) {
198183
+ const raw = req.url ?? "/";
198184
+ const rest2 = raw.slice(BACKEND_PREFIX.length) || "/";
198185
+ const headers = { ...req.headers };
198186
+ delete headers[TOKEN_HEADER];
198187
+ const cookie = withoutCookie(req.headers.cookie, TOKEN_COOKIE);
198188
+ if (cookie) headers.cookie = cookie;
198189
+ else delete headers.cookie;
198190
+ headers["x-forwarded-prefix"] = BACKEND_PREFIX;
198191
+ headers["x-forwarded-proto"] = "http";
198192
+ const peer = req.socket.remoteAddress;
198193
+ if (peer) headers["x-forwarded-for"] = peer;
198194
+ const upstream = http.request(
198195
+ { host: "127.0.0.1", port, method: req.method, path: rest2.startsWith("/") ? rest2 : "/" + rest2, headers },
198196
+ (answer) => {
198197
+ res.writeHead(answer.statusCode ?? 502, answer.headers);
198198
+ answer.pipe(res);
198199
+ }
198200
+ );
198201
+ upstream.on("error", (err) => {
198202
+ if (res.headersSent) {
198203
+ res.destroy(err);
198204
+ return;
198205
+ }
198206
+ res.writeHead(502, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
198207
+ res.end(`The app's backend on this computer (port ${port}) did not answer: ${err.message}
198208
+ `);
198209
+ });
198210
+ res.on("close", () => upstream.destroy());
198211
+ req.pipe(upstream);
198212
+ }
197654
198213
  sendJson(res, body) {
197655
198214
  this.sendDynamic(res, JSON.stringify(body, null, 2), "application/json; charset=utf-8");
197656
198215
  }
@@ -197695,6 +198254,15 @@ var PreviewServer = class {
197695
198254
  res.end("Not found");
197696
198255
  }
197697
198256
  };
198257
+ function pageOrigin(req) {
198258
+ const host = String(req.headers.host || "");
198259
+ return /^[A-Za-z0-9.\-[\]:]+$/.test(host) ? `http://${host}` : null;
198260
+ }
198261
+ function withoutCookie(header, name2) {
198262
+ if (!header) return void 0;
198263
+ const kept = header.split(";").map((part) => part.trim()).filter((part) => part && part.split("=")[0].trim() !== name2);
198264
+ return kept.length ? kept.join("; ") : void 0;
198265
+ }
197698
198266
 
197699
198267
  // src/watcher.ts
197700
198268
  var import_chokidar = __toESM(require("chokidar"));
@@ -197747,6 +198315,7 @@ Options:
197747
198315
  --port <n> Port to listen on (default 8575, 0 for any free port).
197748
198316
  --host <addr> Address to bind (default 127.0.0.1). Any address but loopback also turns
197749
198317
  on the token: the printed URL carries it, and without it nothing is served.
198318
+ A phone on the same Wi-Fi then reaches the app's own backend through it too.
197750
198319
  --token <t> Use this token rather than minting one.
197751
198320
  --debounce <ms> Quiet period before rebuilding after a change (default 120).
197752
198321
  --no-watch Render once and serve; do not watch for changes.
@@ -197860,7 +198429,8 @@ async function main() {
197860
198429
  log(`noodl-preview \u2014 ${dir} (${format}), ${types} node types`);
197861
198430
  const shared = args.host !== "127.0.0.1" && args.host !== "localhost";
197862
198431
  const access = resolveAccess({ share: shared, host: args.host, token: args.token });
197863
- const server = new PreviewServer({ projectDir: dir, port: args.port, host: args.host, access });
198432
+ const backendPort = format === "v2" ? namedBackend(dir)?.port : void 0;
198433
+ const server = new PreviewServer({ projectDir: dir, port: args.port, host: args.host, access, backendPort });
197864
198434
  let port;
197865
198435
  try {
197866
198436
  port = await server.listen();