@lmjs/core 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/_re.js CHANGED
@@ -17,6 +17,7 @@ class _v {
17
17
  static name;
18
18
  static type;
19
19
  static vars;
20
+ static fns;
20
21
  static rvs;
21
22
  static _pv;
22
23
  static mx;
@@ -29,6 +30,16 @@ class _v {
29
30
  this.hst = obj.hst ?? [];
30
31
  this.views = obj.views ?? [];
31
32
  this.vars = obj.vars ?? {};
33
+ // 2026-09-17: per-instance event-handler functions (see walk.js's
34
+ // matching addition, and lstnrs.js's evalEvAttr) — a subview's own
35
+ // `function bump(){}` still becomes a real `window.bump` too (plain
36
+ // <script>-tag semantics, unchanged), but multiple mounted
37
+ // instances of the same subview file all define a same-named
38
+ // function, so window.bump silently becomes whichever instance's
39
+ // script ran last — every instance's @click ended up calling THAT
40
+ // one instance's handler regardless of which was actually clicked.
41
+ // .fns is this instance's own, unambiguous copy.
42
+ this.fns = obj.fns ?? {};
32
43
  this.rvs = obj.rvs ?? {};
33
44
  this._pv = obj._pv ?? null;
34
45
  this.mx = obj.mx ?? [];
@@ -110,6 +121,67 @@ function reportLumenError(info) {
110
121
 
111
122
  function _x(_x) {
112
123
  var currPath = [];
124
+ // 2026-09-17: generalized from a position-specific check
125
+ // (`currPath[1] == "vars"`, only ever matched a flat `_vt.<root>.vars`
126
+ // write) to a real owner-walk — needed for per-instance subview
127
+ // scoping (`_vt.View.views[i].vars`, and its nested form
128
+ // `_vt.View.views[0].views[2].vars`), where "vars" can sit at any
129
+ // depth. `path` is currPath *without* the trailing "vars" segment.
130
+ // `Global` is a real special case, not a bug in this generalization:
131
+ // it has no `._re` of its own (see the object literal below — just
132
+ // `{vars:{}}`) since Global-var writes need to re-render whichever
133
+ // view is *currently* looking at that value, exactly like the
134
+ // pre-existing behavior this preserves. Everything else (View itself,
135
+ // or any views[i]/views[i].views[j]/... chain) is walked against the
136
+ // raw (non-proxied) tree — using the outer `_x` param directly, not
137
+ // the `_vt` proxy — so this traversal itself can't re-enter these
138
+ // traps and corrupt the in-flight currPath.
139
+ function _dispatchVarsUpdate(key) {
140
+ // "vars" can be anywhere in currPath, not just at index 1 — find
141
+ // it. The var name to update() is whatever comes right after it
142
+ // in currPath (e.g. `_vt.View.vars["obj"].nested = x` — the write
143
+ // itself is on "obj"'s own proxy, so `key` here is "nested", but
144
+ // the reactive var is "obj", one segment before it in currPath);
145
+ // if "vars" is the *last* segment, there's no next segment yet —
146
+ // that means this write IS the var (`_vt.View.vars["x"] = ...`),
147
+ // so `key` itself (the set() trap's own key) is the var name.
148
+ let varsIdx = currPath.indexOf("vars");
149
+ if (varsIdx === -1) return;
150
+ let varName = varsIdx < currPath.length - 1 ? currPath[varsIdx + 1] : key;
151
+ let rootPath = currPath.slice(0, varsIdx);
152
+ // Global has no ._re of its own (see its plain {vars:{}} literal
153
+ // below) — a Global write always re-renders whichever view is
154
+ // currently looking at it, exactly like the pre-existing behavior
155
+ // this preserves, for both the flat and nested-property case.
156
+ if (rootPath[0] === "Global") {
157
+ // 2026-09-18, real gap found and fixed: this file's own design
158
+ // comment above _vt's declaration says a Global var should be
159
+ // usable by "ordinary view code" just by referencing the
160
+ // name — but nothing ever made that true for a VIEW's own
161
+ // <script> statements (as opposed to mustaches/:if conditions,
162
+ // which already fall back to _vt.Global.vars via getVal()/
163
+ // evalExp()'s own merge). A view's compiled script only
164
+ // rewrites references to names IT ITSELF declares — it has no
165
+ // way to know index.js declared some other name as Global, so
166
+ // a bare reference to it (e.g. `rx.post(...)` where `rx =
167
+ // new RX(...)` was set in index.js's init()) resolved via the
168
+ // real JS scope chain, which only ever finds a real `window.rx`
169
+ // if something put it there — nothing did. Mirroring every
170
+ // Global write onto `window[varName]` too makes plain bare-
171
+ // identifier references resolve correctly and naturally,
172
+ // with zero per-script rewriting needed.
173
+ try {
174
+ if (typeof window !== 'undefined') window[varName] = _x.Global.vars[varName];
175
+ } catch (e) { }
176
+ if (_vt.View._re) _vt.View._re.update(varName);
177
+ return;
178
+ }
179
+ let owner = _x;
180
+ for (let i = 0; i < rootPath.length && owner; i++) {
181
+ owner = owner[rootPath[i]];
182
+ }
183
+ if (owner && owner._re) owner._re.update(varName);
184
+ }
113
185
  const handler = {
114
186
  get(target, key) {
115
187
  if (key == "__isProxy") return true;
@@ -118,7 +190,17 @@ function _x(_x) {
118
190
  // so a stale tail from a prior _vt.Global read (which never hit
119
191
  // "View" to reset it) would otherwise get prepended to the next
120
192
  // chain and corrupt which key set()'s update(key) fires for.
121
- if (key == "View" || key == "Global") currPath = [];
193
+ // 2026-09-18: generalized from a hardcoded `key == "View" ||
194
+ // key == "Global"` name list (which needed editing every time a
195
+ // new permanent root, e.g. "Widgets", got added) to the one
196
+ // thing that's actually invariant: `target === _x` is only ever
197
+ // true for THIS proxy's own outermost target (the raw root
198
+ // object this whole closure was built from) — any nested proxy
199
+ // in the tree wraps some OTHER object as its target, never `_x`
200
+ // itself. So this fires on any fresh top-level access
201
+ // (`_vt.View`, `_vt.Global`, `_vt.Widgets`, ...) regardless of
202
+ // the key name, with nothing new to maintain per root added.
203
+ if (target === _x) currPath = [];
122
204
  currPath.push(key);
123
205
  if (typeof target[key] === 'object' && target[key] !== null && key != "_re") {
124
206
  // cl("salsaa", target, key)
@@ -136,31 +218,12 @@ function _x(_x) {
136
218
  // var _View = currPath[1] == "views" ? currPath[2] : "";
137
219
 
138
220
  try {
139
- // cl(["hiiiiii", currPath, key, value]);
140
221
  // `_vt.View._re` genuinely doesn't exist yet during
141
222
  // index.js's own bootstrap (before any view has mounted —
142
223
  // see index-bootstrap.js) — that's the normal case now,
143
- // not an error, so it's guarded here rather than left to
144
- // throw into the catch below on every such write.
145
- if (currPath.length == 2 && currPath[1] == "vars") {
146
- if (_vt.View._re) _vt.View._re.update(key);
147
- } else if (currPath.length > 2 && currPath[1] == "vars") {
148
- let ky = clone(currPath[2]);
149
- if (_vt.View._re) _vt.View._re.update(ky);
150
- }
151
- // if (currPath.length) {
152
- // _vt.View._re.set(key, value);
153
- // }
154
- // cl(currPath, _View);
155
- // eval("var kira = " + "_vt." + currPath.join("."));
156
- // if (kira.__isProxy) {
157
- // currPath.push(key);
158
- // eval("kira = " + "_vt." + currPath.join("."));
159
- // }
160
-
161
- // if (kira) {
162
- // // if (_el.data('_re')) _el.data('_re').set(key, value);
163
- // }
224
+ // not an error, so it's guarded (inside _dispatchVarsUpdate)
225
+ // rather than left to throw into the catch below.
226
+ _dispatchVarsUpdate(key);
164
227
  } catch (e) {
165
228
  cl(e);
166
229
  }
@@ -175,31 +238,7 @@ function _x(_x) {
175
238
  delete target[key];
176
239
 
177
240
  try {
178
- // cl(["hiiiiii", currPath, key, value]);
179
- // `_vt.View._re` genuinely doesn't exist yet during
180
- // index.js's own bootstrap (before any view has mounted —
181
- // see index-bootstrap.js) — that's the normal case now,
182
- // not an error, so it's guarded here rather than left to
183
- // throw into the catch below on every such write.
184
- if (currPath.length == 2 && currPath[1] == "vars") {
185
- if (_vt.View._re) _vt.View._re.update(key);
186
- } else if (currPath.length > 2 && currPath[1] == "vars") {
187
- let ky = clone(currPath[2]);
188
- if (_vt.View._re) _vt.View._re.update(ky);
189
- }
190
- // if (currPath.length) {
191
- // _vt.View._re.set(key, value);
192
- // }
193
- // cl(currPath, _View);
194
- // eval("var kira = " + "_vt." + currPath.join("."));
195
- // if (kira.__isProxy) {
196
- // currPath.push(key);
197
- // eval("kira = " + "_vt." + currPath.join("."));
198
- // }
199
-
200
- // if (kira) {
201
- // // if (_el.data('_re')) _el.data('_re').set(key, value);
202
- // }
241
+ _dispatchVarsUpdate(key);
203
242
  } catch (e) {
204
243
  cl(e);
205
244
  }
@@ -240,11 +279,73 @@ function _x(_x) {
240
279
  // its vars (V1's documented convention for app-wide state like `isAdmin`)
241
280
  // need to outlive any single view. concatVarsAtLevel() below merges it into
242
281
  // every scope lookup so ordinary view code can just reference the name.
282
+ // 2026-09-17, real bug found and fixed: `.fns` was missing here (only
283
+ // `.vars` existed) — `walk.js`'s `_fnRegisterStatement()` (added for
284
+ // per-instance subview scoping, see the per-instance-scoping work above)
285
+ // unconditionally builds `<targetRoot>.fns["name"] = name;` for EVERY
286
+ // top-level function declaration, regardless of target — including
287
+ // `targetKey: "Global"`, used to compile index.js/project scripts. Any
288
+ // real project with so much as one function declaration in index.js (an
289
+ // extremely common pattern) crashed with "Cannot set properties of
290
+ // undefined (setting '<fnName>')" the moment that line ran. Nearly
291
+ // invisible in `lm serve` dev mode — index-bootstrap.js's synchronous XHR
292
+ // fetch-and-eval of the compiled index.js is wrapped in a try/catch that
293
+ // only `console.log`s the failure (never `console.error`/throws), so nothing
294
+ // looked broken as long as the crash landed after anything load-bearing
295
+ // (Reactor()) in that same script. Fatal in a real `lm build` production
296
+ // bundle, where the compiled index.js is concatenated inline, unguarded —
297
+ // an uncaught exception there aborts every remaining statement in the
298
+ // whole bundle file, breaking the entire app on every real production
299
+ // site with this pattern. Found deploying lumenjs.com's own real build.
300
+ // "Widgets" (2026-09-18): a third permanent root, sibling of Global/View,
301
+ // generalizing V1's hardcoded hasNav/hasHeader/hasFooter widget-loading past
302
+ // its fixed 3-name list — see renderView()'s layout branch below for the
303
+ // resolve/mount/diff logic, and fcs.js's annotateLayoutRegions() for how a
304
+ // layout's own bare attributes (e.g. `<nav nav>`) become the region names
305
+ // used to key this object. Deliberately NOT an ancestor of View in the
306
+ // scope tree (widget regions and [body] are DOM SIBLINGS under the layout
307
+ // root, never one containing the other — unlike a subview's real, singular
308
+ // parent, there's no single unambiguous "the" widget a view could shadow
309
+ // into) — each entry gets its own real `._re` instead (see
310
+ // _dispatchVarsUpdate's generic owner-walk below, which already handles
311
+ // this with zero special-casing once an entry has `._re` set, same as any
312
+ // subview). Starts empty; populated lazily, one entry per declared region
313
+ // name, the first time a layout using that region is mounted.
243
314
  let _vt = _x({
244
315
  "View": new _v({}),
245
- "Global": { "vars": {} }
316
+ "Global": { "vars": {}, "fns": {} },
317
+ "Widgets": {}
246
318
  });
247
319
 
320
+ // 2026-09-18: shared by getVal()/evalExp()/concatVarsAtLevel() below, so the
321
+ // "first-declared widget wins on a name collision" rule is defined exactly
322
+ // once. Object key iteration order for string keys is real insertion order,
323
+ // so this naturally checks widgets in the order they were first mounted —
324
+ // which, in practice, is document order (renderView()'s layout branch walks
325
+ // `hstL.regions`, itself built in document order by fcs.js's
326
+ // annotateLayoutRegions()).
327
+ function _lookupInWidgets(name) {
328
+ for (const wname in _vt.Widgets) {
329
+ if (_vt.Widgets[wname].vars.hasOwnProperty(name)) return _vt.Widgets[wname].vars[name];
330
+ }
331
+ return undefined;
332
+ }
333
+
334
+ // concatVarsAtLevel() below merges via object spread, not the loop-with-
335
+ // break shape _lookupInWidgets() uses — spread's "later wins" needs the
336
+ // widgets merged in REVERSE declaration order so the first-declared widget
337
+ // still wins a name collision, matching _lookupInWidgets()'s own semantics
338
+ // exactly (both must agree, since getVal()/evalExp() use one and
339
+ // concatVarsAtLevel()/lookup()/scopedEval() use the other for the same read).
340
+ function _mergedWidgetsVars() {
341
+ let out = {};
342
+ let names = Object.keys(_vt.Widgets).reverse();
343
+ for (const wname of names) {
344
+ out = { ...out, ..._vt.Widgets[wname].vars };
345
+ }
346
+ return out;
347
+ }
348
+
248
349
  class _lm {
249
350
  _RealDOM = [];
250
351
  _effects = {};
@@ -388,7 +489,6 @@ class _lm {
388
489
  } else {
389
490
  effect.nd.setAttribute(effect.name, this.render(effect));
390
491
  if (effect.name == "view") {
391
- cl("We Must Trigger View Change", effect, this.render(effect));
392
492
  effect.nd.subPath = this.render(effect);
393
493
  renderView(this.render(effect), true, {});
394
494
  }
@@ -407,6 +507,19 @@ class _lm {
407
507
  this.updateCXRs();
408
508
  this.updateLXRs();
409
509
  this.updateVXRs();
510
+
511
+ // 2026-09-18: same Reactor({tick}) fix as update() below — also
512
+ // fired on the very first render (not just subsequent reactive
513
+ // updates), since a real tick() use (e.g. hljs.highlightAll())
514
+ // needs to run on initial content too, not just after the first
515
+ // change.
516
+ if (typeof appSettings !== 'undefined' && appSettings && typeof appSettings.tick === 'function') {
517
+ try {
518
+ appSettings.tick();
519
+ } catch (e) {
520
+ cl(e);
521
+ }
522
+ }
410
523
  }
411
524
 
412
525
  chainConnected(cx) {
@@ -423,13 +536,25 @@ class _lm {
423
536
  let subsNames = [];
424
537
  for (let i = 0; i < this.view.views.length; i++) {
425
538
  const _view = this.view.views[i];
426
- cl("We Must Render A SubView", _view, subsNames.includes(_view.subPath), subsNames, _view.subPath);
427
539
  if (!subsNames.includes(_view.subPath)) subsNames.push(_view.subPath);
428
540
  }
429
541
  // cl(subsNames);
542
+ // 2026-09-17: real bug found and fixed — this used to always call
543
+ // the plain, no-args renderView(n, true, {}), which only ever
544
+ // searches _vt.View.views (the MAIN view's own array). That's
545
+ // correct when `this` IS the main view, but updateVXRs() also
546
+ // runs on every SUBVIEW instance's own _re (renderAll() calls it
547
+ // unconditionally) — for a subview that itself hosts further
548
+ // subviews, `this.view.views` holds THOSE, not the main view's,
549
+ // so the old call could never find them: nested subviews never
550
+ // rendered at all. Passing this.view.views + this.view.scopePath
551
+ // (set once, in renderHST(), when this instance itself was
552
+ // rendered — ["View"] for the main view, unchanged) lets
553
+ // renderView() search the RIGHT array and address the RIGHT
554
+ // nested scope regardless of how deep this instance is.
430
555
  for (let i = 0; i < subsNames.length; i++) {
431
556
  const n = subsNames[i];
432
- renderView(n, true, {});
557
+ renderView(n, true, {}, 'views', this.view.views, this.view.scopePath || ["View"]);
433
558
  }
434
559
  }
435
560
 
@@ -531,7 +656,6 @@ class _lm {
531
656
  } else {
532
657
  effect.nd.setAttribute(effect.name, this.render(effect));
533
658
  if (effect.name == "view") {
534
- cl("We Must Trigger View Change", effect, this.render(effect));
535
659
  effect.nd.subPath = this.render(effect);
536
660
  renderView(this.render(effect), true, {});
537
661
  }
@@ -551,6 +675,24 @@ class _lm {
551
675
  const sbscr = this.sbscrbs[sbscsi];
552
676
  sbscr._re.update(k);
553
677
  }
678
+
679
+ // 2026-09-18: Reactor({tick}) — documented ("runs after every
680
+ // re-render, anywhere", lumenjs-spec.md §5.4/llms.md §3) but never
681
+ // actually called anywhere in this runtime — see this fix's fuller
682
+ // comment at Reactor()'s own definition (vendor/reconnecting-
683
+ // websocket.js) for how this was found. update() is the real
684
+ // per-reactive-change render point (unlike renderAll(), which only
685
+ // ever runs once per _lm instance, at initial mount) — this is
686
+ // where "after every re-render, anywhere" actually lives. Guarded
687
+ // on appSettings existing since update() can fire during index.js's
688
+ // own bootstrap, before Reactor() has run.
689
+ if (typeof appSettings !== 'undefined' && appSettings && typeof appSettings.tick === 'function') {
690
+ try {
691
+ appSettings.tick();
692
+ } catch (e) {
693
+ cl(e);
694
+ }
695
+ }
554
696
  }
555
697
 
556
698
  async updateLXRs(k) {
@@ -632,12 +774,61 @@ class _lm {
632
774
  vx['value'] = marray[index]['value'];
633
775
  } else {
634
776
  if (forX['as']['k']) vx[forX['as']['k']] = marray[index];
777
+ // 2026-09-17, real bug found and fixed: a bare
778
+ // `:for="items"` (no `as item` alias) is
779
+ // documented — and was V1's real, working
780
+ // behavior — to spread each item's own object
781
+ // keys directly into loop scope, so
782
+ // `items = [{number:1}]` lets a mustache read
783
+ // `{{number}}` with no alias at all. Nothing in
784
+ // this branch ever did that spread — `vx` had
785
+ // no alias key AND no spread keys, so a bare
786
+ // mustache like this always evaluated as an
787
+ // undefined identifier and silently rendered
788
+ // blank. Only spread for a plain-object item —
789
+ // a primitive (string/number) array item has no
790
+ // keys to spread and is read via the untouched
791
+ // alias path instead.
792
+ else if (marray[index] && typeof marray[index] === 'object') {
793
+ for (const k2 in marray[index]) {
794
+ if (Object.prototype.hasOwnProperty.call(marray[index], k2)) {
795
+ vx[k2] = marray[index][k2];
796
+ }
797
+ }
798
+ }
635
799
  }
636
800
  // cl(forX, vx);
637
801
  // cl("VXXXX", vx, this);
638
802
 
639
803
  if (forIf) {
640
- let isTrue = this.evalExp(forIf, vx);
804
+ // 2026-09-17, real bug found and fixed:
805
+ // evalExp()'s 2nd param is a NAMES array to
806
+ // look up in _vt.View.vars/_vt.Global.vars (see
807
+ // its other real call site, `this.evalExp(cx.content,
808
+ // this.reactiveVariables)`) — but `vx` here is
809
+ // a VALUES object (the just-built loop-item
810
+ // scope), not a names array. `vars.length` on a
811
+ // plain object is undefined, so evalExp's own
812
+ // `for (i=0; i<vars.length; i++)` loop never
813
+ // ran even once — every `:for-if` condition
814
+ // referencing the loop item (`item.value > 10`,
815
+ // the exact documented example) always
816
+ // evaluated against an empty scope and either
817
+ // threw (caught, filtered out) or read
818
+ // `undefined`. evalExp() already has a real,
819
+ // working mechanism for exactly this — it
820
+ // merges `this.vrs` (the loop-scope override
821
+ // object) at the highest priority — so route
822
+ // through that instead of a second, broken
823
+ // parameter shape.
824
+ let _prevVrs = this.vrs;
825
+ this.vrs = vx;
826
+ let isTrue;
827
+ try {
828
+ isTrue = this.evalExp(forIf, []);
829
+ } finally {
830
+ this.vrs = _prevVrs;
831
+ }
641
832
  if (!isTrue) continue;
642
833
  }
643
834
 
@@ -670,7 +861,29 @@ class _lm {
670
861
 
671
862
  // cl("ATR", arrayToRender);
672
863
  let oldATR = cx.atr;
673
- cx.atr = arrayToRender;
864
+ // 2026-09-17, real bug found and fixed: cx.atr used to
865
+ // store a reference to arrayToRender's own item objects,
866
+ // not an independent snapshot — arrayToRender's items are
867
+ // the actual current elements of the reactive array (e.g.
868
+ // _vt.View.vars["features"]), so a completely ordinary
869
+ // update pattern (mutate an item in place inside .map(),
870
+ // return the same reference — `f.done = !f.done; return
871
+ // f;`, not "rebuild a new object") left oldATR and the
872
+ // next render's arrayToRender pointing at the exact same,
873
+ // already-mutated objects by the time compareArrays()
874
+ // ran. deepCompare()'s JSON.stringify comparison was
875
+ // never wrong — it was comparing an object against
876
+ // itself, so every 'update' action was silently lost and
877
+ // the DOM never re-rendered. clone() (already used a few
878
+ // lines above for `vals`) gives cx.atr a true independent
879
+ // snapshot instead. Found clicking a real @click-toggled
880
+ // :for item on lumenjs.com's own real V2 homepage — no
881
+ // existing test used an in-place item mutation, only
882
+ // whole-array reassignment with new items (see render-
883
+ // hooks.integration.test.js's `images = [...,'c']`),
884
+ // which never exposed this since a length change always
885
+ // hits compareArrays()'s add/remove branches regardless.
886
+ cx.atr = clone(arrayToRender);
674
887
  // cl(oldATR, arrayToRender);
675
888
 
676
889
  const actions = this.compareArrays(oldATR, arrayToRender);
@@ -914,16 +1127,38 @@ class _lm {
914
1127
  // lookup()/scopedEval(); getVal() (what mustache text nodes
915
1128
  // actually call) had its own separate, unmerged read here
916
1129
  // and was silently rendering these as empty.
1130
+ // 2026-09-18: inserted a middle tier — _lookupInWidgets()
1131
+ // (all currently-mounted widgets' vars, first-declared
1132
+ // wins) — between View and Global, per this session's
1133
+ // agreed read-fallback order.
917
1134
  let __name = this.reactiveVariables[i];
918
- vars[__name] = _vt.View.vars.hasOwnProperty(__name)
919
- ? _vt.View.vars[__name]
920
- : _vt.Global.vars[__name];
1135
+ if (_vt.View.vars.hasOwnProperty(__name)) {
1136
+ vars[__name] = _vt.View.vars[__name];
1137
+ } else {
1138
+ let _wv = _lookupInWidgets(__name);
1139
+ vars[__name] = _wv !== undefined ? _wv : _vt.Global.vars[__name];
1140
+ }
921
1141
  }
922
1142
  for (const ky in this.vrs) {
923
1143
  if (Object.prototype.hasOwnProperty.call(this.vrs, ky)) {
924
1144
  vars[ky] = this.vrs[ky]
925
1145
  }
926
1146
  }
1147
+ // 2026-09-17: per-instance subview scoping — a subview's own
1148
+ // declared vars live on `this.view.vars` (wired up in
1149
+ // renderHST()/renderView() to be the SAME object its compiled
1150
+ // script's `_vt.View.views[i].vars[...]` writes reach), not
1151
+ // in the flat `_vt.View.vars` checked above (that's still
1152
+ // only ever the MAIN view). Checked last/highest-priority,
1153
+ // same shadowing shape as the `this.vrs` merge just above —
1154
+ // an instance's own var wins over anything it inherited.
1155
+ if (this.view && this.view.vars) {
1156
+ for (const ky in this.view.vars) {
1157
+ if (Object.prototype.hasOwnProperty.call(this.view.vars, ky)) {
1158
+ vars[ky] = this.view.vars[ky];
1159
+ }
1160
+ }
1161
+ }
927
1162
  } catch (e) {
928
1163
  cl(e);
929
1164
  }
@@ -1033,8 +1268,10 @@ class _lm {
1033
1268
  // Global vars (index.js's top-level state, see _vt.Global
1034
1269
  // above) sit at the lowest priority here — a view or a
1035
1270
  // narrower scope declaring the same name shadows it, same
1036
- // as real JS scoping would.
1037
- concatenatedVars = { ..._vt.Global.vars, ...parent.view.vars, ...concatenatedVars };
1271
+ // as real JS scoping would. 2026-09-18: widgets' vars sit
1272
+ // one tier above Global, below the view's own — see
1273
+ // _mergedWidgetsVars()'s comment for the ordering rationale.
1274
+ concatenatedVars = { ..._vt.Global.vars, ..._mergedWidgetsVars(), ...parent.view.vars, ...concatenatedVars };
1038
1275
  }
1039
1276
  return concatenatedVars;
1040
1277
  }
@@ -1157,16 +1394,97 @@ class _lm {
1157
1394
  createEl(tag, attrs, children, events, doc) {
1158
1395
  const _el = document.createElement(tag);
1159
1396
 
1397
+ // 2026-09-17: every element knows which _lm instance rendered it —
1398
+ // needed so a click on it can resolve its @click handler against
1399
+ // the RIGHT instance's own .fns (see lstnrs.js's evalEvAttr), and
1400
+ // (below) so an @init handler on a subview mount resolves against
1401
+ // ITS creator's own functions/scope, not window. Stamped before
1402
+ // anything below that might need it. Non-enumerable: real bug
1403
+ // found and fixed — an ordinary DOM element has no own enumerable
1404
+ // properties by default, so JSON.stringify(someElement) safely
1405
+ // produced "{}" everywhere this codebase already did that (e.g. a
1406
+ // real test asserting on JSON.stringify(view._slots), real DOM
1407
+ // nodes). An ENUMERABLE back-reference here closes a genuine
1408
+ // cycle (element -> _ownerRe -> that instance's own _RealDOM ->
1409
+ // the same element) and breaks every one of those call sites. Defined this way
1410
+ // instead of a plain `=` assignment so it's reachable
1411
+ // (`el._ownerRe`) but invisible to JSON.stringify/for-in/
1412
+ // Object.keys, matching how a parent-pointer is normally done.
1413
+ Object.defineProperty(_el, '_ownerRe', { value: this, enumerable: false, configurable: true, writable: true });
1414
+
1160
1415
  _el.isSub = false;
1161
1416
  if (attrs.hasOwnProperty('view')) {
1162
1417
  _el.isSub = true;
1163
1418
  _el.subPath = attrs['view'];
1419
+ // 2026-09-17: per-instance subview scoping. `views[]` used to
1420
+ // hold just the raw element — every instance of the same
1421
+ // subview file shared one flat _vt.View.vars, so two mounts of
1422
+ // the same .view file collided on the same variable slots (a
1423
+ // real, confirmed bug — clicking one instance changed a value
1424
+ // neither instance's DOM reflected). DOM elements already
1425
+ // carry arbitrary extra JS properties throughout this file
1426
+ // (.isSub, .subPath, .events) — .vars/.views here follow that
1427
+ // same convention rather than a new parallel structure. .vars
1428
+ // is this instance's own scope, addressed at
1429
+ // _vt.View.views[i].vars once renderHST() wires up the path
1430
+ // (see renderView()); .views lets this instance itself host
1431
+ // further nested subviews the same way.
1432
+ _el.vars = {};
1433
+ _el.views = [];
1434
+ _el.fns = {};
1435
+ // 2026-09-17: `@init="fn"` — the actual, decided mechanism
1436
+ // for giving a subview mount its own data at creation time
1437
+ // (replaces an earlier, narrower `:data="name"` attempt —
1438
+ // reusing the existing @before-render/@after-render hook
1439
+ // machinery instead of a bespoke attribute, per real
1440
+ // discussion of real use cases: a subview file mounted
1441
+ // multiple times needing distinct config per instance — the
1442
+ // same widget showing a different crypto coin per mount, or
1443
+ // a :for item needing its own data slice). `fn`'s return
1444
+ // value (a plain object) is merged into this instance's own
1445
+ // `.vars`, so it can hand over multiple named values at once
1446
+ // — not tied to matching a parent variable's name the way
1447
+ // `:data` was. Reads an ancestor's own variable by name
1448
+ // still needs no special syntax at all (getVal()/evalExp()'s
1449
+ // existing fallthrough already covers that); @init is for
1450
+ // data that's computed/selected specifically for this one
1451
+ // mount. fireRenderHook() needed a real fix first (used to
1452
+ // discard its handler's return value entirely) — see its own
1453
+ // comment. Deliberately one-way — a subview writing back to
1454
+ // a PARENT variable (a steps-form wizard sharing accumulated
1455
+ // state across steps, for instance) is a real, different,
1456
+ // not-yet-designed mechanism, not what this covers.
1457
+ //
1458
+ // Calls evalEvAttr() directly rather than fireRenderHook()
1459
+ // (which every other @before-render/@after-render call site
1460
+ // uses): createEl() itself isn't async, and this result has
1461
+ // to be ready synchronously, before the subview actually
1462
+ // renders (a later, separate, real async call). An @init
1463
+ // handler that returns a Promise can't be awaited here —
1464
+ // deliberately unsupported for now, not silently broken:
1465
+ // only a plain object return is used.
1466
+ if (doc && doc.evs && doc.evs.hasOwnProperty('@init')) {
1467
+ let _initAttr = doc.evs['@init'];
1468
+ if (_initAttr) {
1469
+ // 3rd arg: the CALLING scope's own loop-local bindings
1470
+ // (e.g. `item` from `:for="items as item"`) — these
1471
+ // aren't real JS variables an @init handler could
1472
+ // otherwise close over (it's an ordinary top-level
1473
+ // function, not defined inside the loop), so without
1474
+ // this there'd be no way for it to know which
1475
+ // iteration it's being called for at all.
1476
+ let _initResult = evalEvAttr(_initAttr, { cType: 'init' }, $(_el), 'init', this.vrs);
1477
+ if (_initResult && typeof _initResult === 'object' && typeof _initResult.then !== 'function') {
1478
+ Object.assign(_el.vars, _initResult);
1479
+ }
1480
+ }
1481
+ }
1164
1482
  this.view.views.push(_el);
1165
1483
  }
1166
1484
 
1167
1485
  _el.events = {};
1168
1486
  for (const prop in attrs) {
1169
- if (prop == "view" || prop == ":if" || prop == ":else-if" || prop == ":else" || prop == ":for" || prop == ":for-limit" || prop == ":for-offset" || prop == ":for-if") continue;
1487
+ if (prop == "view" || prop == ":data" || prop == ":if" || prop == ":else-if" || prop == ":else" || prop == ":for" || prop == ":for-limit" || prop == ":for-offset" || prop == ":for-if") continue;
1170
1488
  try {
1171
1489
  // cl("DD", doc);
1172
1490
  let val = (doc && doc.ax.hasOwnProperty(prop)) ? "" : attrs[prop];
@@ -1265,10 +1583,48 @@ class _lm {
1265
1583
  // value scopedEval() would otherwise have found in
1266
1584
  // _vt.Global.vars — breaking :if/:for conditions on any
1267
1585
  // index.js-declared var, not just mustache text (getVal()).
1586
+ // 2026-09-18: same middle tier as getVal() — see its
1587
+ // matching comment.
1268
1588
  let __name = vars[i];
1269
- rvars[__name] = _vt.View.vars.hasOwnProperty(__name)
1270
- ? _vt.View.vars[__name]
1271
- : _vt.Global.vars[__name];
1589
+ if (_vt.View.vars.hasOwnProperty(__name)) {
1590
+ rvars[__name] = _vt.View.vars[__name];
1591
+ } else {
1592
+ let _wv = _lookupInWidgets(__name);
1593
+ rvars[__name] = _wv !== undefined ? _wv : _vt.Global.vars[__name];
1594
+ }
1595
+ }
1596
+ // 2026-09-17, real bug found and fixed: getVal() (mustache
1597
+ // text, e.g. {{f.name}}) has always merged this.vrs in after
1598
+ // the View/Global loop above — the local :for-loop-scoped
1599
+ // binding (f, k, v, index — set via tx._re.vrs = vx, see the
1600
+ // :for update path above) correctly shadows/supplies names a
1601
+ // view's own script never declares. evalExp() (used for
1602
+ // :if="f.done"/:else-if conditions) never had the same merge
1603
+ // — any :if/:else-if condition referencing a :for loop
1604
+ // variable's property could never evaluate correctly, in any
1605
+ // LumenJS V2 project, not just this page. It "looked" correct
1606
+ // on first render only by coincidence: :else isn't gated by
1607
+ // evalExp at all, so an all-false-by-default loop (every item
1608
+ // starting unchecked) rendered the same whether the condition
1609
+ // genuinely evaluated false or silently threw and was caught
1610
+ // below. Found clicking a real @click-toggled :for item on
1611
+ // lumenjs.com's own real V2 homepage.
1612
+ for (const ky in this.vrs) {
1613
+ if (Object.prototype.hasOwnProperty.call(this.vrs, ky)) {
1614
+ rvars[ky] = this.vrs[ky];
1615
+ }
1616
+ }
1617
+ // 2026-09-17: same per-instance subview merge as getVal()
1618
+ // above — a subview's own vars live on this.view.vars, not
1619
+ // the flat _vt.View.vars checked above (always the main
1620
+ // view). See getVal()'s matching comment for the full
1621
+ // rationale.
1622
+ if (this.view && this.view.vars) {
1623
+ for (const ky in this.view.vars) {
1624
+ if (Object.prototype.hasOwnProperty.call(this.view.vars, ky)) {
1625
+ rvars[ky] = this.view.vars[ky];
1626
+ }
1627
+ }
1272
1628
  }
1273
1629
  } catch (e) {
1274
1630
  cl(e);
@@ -1615,6 +1971,22 @@ class _lm {
1615
1971
  }
1616
1972
  (par.view._slots ?? (par.view._slots = {}))[slotName] = childs;
1617
1973
  }
1974
+ } else if (doc.attrs && doc.attrs.hasOwnProperty('tpl') && !doc.attrs.hasOwnProperty(':for')) {
1975
+ // 2026-09-17: `tpl="name"` used WITHOUT `:for=` on the
1976
+ // same element — the real, documented (lumenjs-
1977
+ // spec.md §4.3) shape always pairs the two, and only
1978
+ // makes sense paired: a template renders inside
1979
+ // whatever repeated context it's used from (see
1980
+ // renderSection()'s own tpl handling, for the
1981
+ // WITH-:for case — an element carrying both never
1982
+ // reaches this branch at all, since :for makes it a
1983
+ // 'sections'-type node the switch above already
1984
+ // dispatched elsewhere). Reported as a real error
1985
+ // rather than silently rendering nothing.
1986
+ reportLumenError({
1987
+ stage: 'tpl',
1988
+ error: new Error('tpl="' + doc.attrs['tpl'] + '" must be used together with :for on the same element — templates only render inside a repeated/list context.'),
1989
+ });
1618
1990
  } else {
1619
1991
  el = par.createEl(doc.name, doc.attrs, [], doc.evs, doc);
1620
1992
 
@@ -1797,7 +2169,26 @@ class _lm {
1797
2169
  }
1798
2170
 
1799
2171
 
1800
- async function renderHST(hst, n, type = 'main', tx, _pv = null) {
2172
+ // 2026-09-17: `scopePath` (new) where in the _vt tree THIS render's own
2173
+ // compiled <script> should write/read its reactive vars. Defaults to
2174
+ // ["View"] (the main view, unchanged behavior for every existing caller).
2175
+ // A subview mount passes its own ["View","views",i] (or the nested form
2176
+ // for a subview hosting a subview) — see renderView()'s isSub branch.
2177
+ // `ownVars`/`ownFns`/`ownViews` (new): for a subview render, the SAME
2178
+ // objects the mount element's `.vars`/`.fns`/`.views` already point to
2179
+ // (not fresh `{}`/`[]`s) — so a write through the compiled script's
2180
+ // `_vt.View.views[i].vars[...]`/`.fns[...]` path (which resolves to that
2181
+ // element's `.vars`/`.fns`) and a read through this instance's own
2182
+ // `getVal()`/`evalExp()` (`this.view.vars`) or `evalEvAttr()`
2183
+ // (`this.view.fns`) see the exact same data. `ownViews` matters for
2184
+ // NESTED subviews specifically: createEl() pushes onto `this.view.views`
2185
+ // while THIS instance is rendering — without linking that to the mount
2186
+ // element's own `.views`, a subview hosting further subviews pushed them
2187
+ // into a freshly-constructed, orphaned array nothing else could ever
2188
+ // find (real bug found testing exactly this — nested subviews mounted
2189
+ // and rendered their own content correctly, but their OWN vars/state
2190
+ // were unreachable from outside, landing in a throwaway array).
2191
+ async function renderHST(hst, n, type = 'main', tx, _pv = null, scopePath = ["View"], ownVars, ownFns, ownViews) {
1801
2192
 
1802
2193
  var reactiveVariables = hst.reactiveVars;
1803
2194
  hst = hst.hst;
@@ -1806,10 +2197,16 @@ async function renderHST(hst, n, type = 'main', tx, _pv = null) {
1806
2197
  "name": n,
1807
2198
  "type": type,
1808
2199
  "hst": hst,
1809
- "vars": tx?.vx,
2200
+ "vars": ownVars ?? tx?.vx,
2201
+ "fns": ownFns,
2202
+ "views": ownViews,
1810
2203
  "rvs": reactiveVariables,
1811
2204
  "_pv": _pv
1812
2205
  }));
2206
+ // 2026-09-17: persisted so THIS instance can later render ITS OWN
2207
+ // subviews (updateVXRs() -> renderView()) at the right nested path —
2208
+ // see both of those for the other half of this.
2209
+ _re.view.scopePath = scopePath;
1813
2210
 
1814
2211
  if (Object.keys(_re._cc).length) {
1815
2212
 
@@ -1882,7 +2279,7 @@ async function renderHST(hst, n, type = 'main', tx, _pv = null) {
1882
2279
  const jses = _re._jj[ky];
1883
2280
  for (let inde = 0; inde < jses.length; inde++) {
1884
2281
  const nd = jses[inde].nd;
1885
- let code = getWatcher(nd._jst, _re.view, reactiveVariables).code;
2282
+ let code = getWatcher(nd._jst, _re.view, reactiveVariables, scopePath).code;
1886
2283
  code = `try { ` + code + ` } catch (e) { reportLumenError({ stage: 'script', error: e }); }`;
1887
2284
  // sourceURL: DevTools shows this <script>'s errors against
1888
2285
  // "<viewname>.view" instead of an anonymous VM context.
@@ -1936,8 +2333,16 @@ async function fireRenderHook(cx, n, containerEl, extra) {
1936
2333
  let ev = Object.assign({ cType: n }, extra || {});
1937
2334
  let result = evalEvAttr(attrVal, ev, $(containerEl), n);
1938
2335
  if (result && typeof result.then === 'function') {
1939
- try { await result; } catch (e) { }
2336
+ // 2026-09-17: this used to just await-and-discard. @init (new,
2337
+ // subview mounts) needs the handler's actual return value — its
2338
+ // whole purpose is "compute this instance's own initial data" —
2339
+ // so a Promise result is now awaited AND returned, not thrown
2340
+ // away. Every existing @before-render/@after-render caller
2341
+ // already ignores this function's return value (none of them
2342
+ // needed it), so this is additive, not a behavior change for them.
2343
+ try { return await result; } catch (e) { return undefined; }
1940
2344
  }
2345
+ return result;
1941
2346
  }
1942
2347
 
1943
2348
  // 2026-09-16: V1's real automatic plugin-init pass — [sl] (Select2),
@@ -2126,7 +2531,45 @@ async function renderSection(tx, doc, par, k, sectionsData) {
2126
2531
 
2127
2532
  var hst = doc.children;
2128
2533
 
2129
- let _re = await renderHST({ hst, mxes: [] }, tx.key, 'section', tx, par);
2534
+ // 2026-09-17: `tpl="name"` on a :for element (`<li :for="items as
2535
+ // item" tpl="row"></li>`, the real, documented — lumenjs-spec.md
2536
+ // §4.3 — shape; never built in V2 until now). A :for element's own
2537
+ // `doc.children` is empty (there's nothing between its tags in the
2538
+ // source), which is exactly what's rendered per item above — so
2539
+ // this substitutes the template file's own HST as this section's
2540
+ // children instead, walked by the SAME engine instance rendering
2541
+ // everything else here (this section already has its own vx/vrs —
2542
+ // no separate scope, no isolated instance, just this item's content).
2543
+ if (doc.attrs && doc.attrs.hasOwnProperty('tpl')) {
2544
+ let _payload = (typeof _vcD !== 'undefined' && _vcD) || (typeof _vcData !== 'undefined' ? _vcData : undefined);
2545
+ let _tplName = doc.attrs['tpl'];
2546
+ let _tplKey = btoa("src/tpls/" + _tplName + ".tpl");
2547
+ let _tplEntry = _payload && _payload.tpls && _payload.tpls[_tplKey];
2548
+ if (_tplEntry) {
2549
+ hst = _tplEntry.hst;
2550
+ } else {
2551
+ reportLumenError({
2552
+ stage: 'tpl',
2553
+ error: new Error('tpl="' + _tplName + '" — no such file at src/tpls/' + _tplName + '.tpl'),
2554
+ });
2555
+ }
2556
+ }
2557
+
2558
+ // 2026-09-17: real bug found and fixed — a subview mounted INSIDE a
2559
+ // :for item (e.g. <div :for="items as item"><div view="widgets/
2560
+ // card"></div></div>) never worked at all: renderHST() used to always
2561
+ // build a fresh, empty `.views` array for whatever it constructs
2562
+ // (see _v's constructor), so createEl() calls made while rendering
2563
+ // THIS item's own content pushed the subview's mount element onto a
2564
+ // throwaway array nothing else could ever find — renderView()'s
2565
+ // isSub search only ever looks at the ENCLOSING view's real `.views`
2566
+ // (or a nested subview's own, itself wired the same way). Passing
2567
+ // par's own `.view.views`/`.scopePath` through here makes every
2568
+ // loop item share the SAME real registry as its enclosing view, so
2569
+ // subview mounts inside a loop accumulate into one addressable array
2570
+ // (sequential index, in render order) exactly like a static mount —
2571
+ // same mechanism either way, just a dynamically-arriving index.
2572
+ let _re = await renderHST({ hst, mxes: [] }, tx.key, 'section', tx, par, par?.view?.scopePath || ["View"], undefined, undefined, par?.view?.views);
2130
2573
  // if(par) {
2131
2574
  // _re.reactiveVariables = par.reactiveVariables;
2132
2575
  // }
@@ -2157,11 +2600,18 @@ async function renderSection(tx, doc, par, k, sectionsData) {
2157
2600
  return _re;
2158
2601
  }
2159
2602
 
2160
- async function renderView(n, isSub, d, type = 'views') {
2603
+ // `viewsArr`/`scopeBase` (2026-09-17): which subview registry to search
2604
+ // and which scope path to address matches under — default to the main
2605
+ // view's own (_vt.View.views / ["View"]) for the top-level, non-recursive
2606
+ // caller (ws.js's real navigation, index-bootstrap.js, etc. — all
2607
+ // unaffected, still work exactly as before). A subview instance rendering
2608
+ // ITS OWN subviews (updateVXRs(), above) passes its own `.views`/
2609
+ // `.scopePath` instead, so nested subviews resolve against the RIGHT
2610
+ // array at the RIGHT nested address instead of always the main view's.
2611
+ async function renderView(n, isSub, d, type = 'views', viewsArr, scopeBase = ["View"]) {
2161
2612
  let filePath = "src/views/" + n + ".view"
2162
2613
  if (type == 'layouts') filePath = "src/layouts/" + n + ".layout";
2163
2614
 
2164
- cl(arguments);
2165
2615
  let fileKey = btoa(filePath);
2166
2616
  if (!isSub) {
2167
2617
  View.props = d ?? {};
@@ -2213,23 +2663,42 @@ async function renderView(n, isSub, d, type = 'views') {
2213
2663
  // return;
2214
2664
 
2215
2665
  if (isSub) {
2666
+ // 2026-09-17: per-instance subview scoping — each matched
2667
+ // element needs ITS OWN index within the search array (not its
2668
+ // position in this filtered-by-subPath list) so its compiled
2669
+ // script rewrites into `<scopeBase>.views[<that exact index>].vars`,
2670
+ // not the flat `_vt.View.vars` every subview used to collide on.
2671
+ // Searches viewsArr (this main view's _vt.View.views by
2672
+ // default — unchanged for the top-level caller) or, when
2673
+ // called recursively from a subview instance's own
2674
+ // updateVXRs(), THAT instance's own `.views` — see
2675
+ // updateVXRs() for why nested subviews need this.
2676
+ let searchArr = viewsArr || _vt.View.views;
2216
2677
  let els = [];
2217
- for (let i = 0; i < _vt.View.views.length; i++) {
2218
- let _el = _vt.View.views[i];
2678
+ for (let i = 0; i < searchArr.length; i++) {
2679
+ let _el = searchArr[i];
2219
2680
  if (_el.__isProxy) _el = _el.target;
2220
- if (_el.subPath == n) els.push(_el);
2681
+ if (_el.subPath == n) els.push({ el: _el, viewsIndex: i });
2221
2682
  }
2222
2683
  if (els.length) {
2223
2684
  // cl(els);
2224
2685
  for (let i = 0; i < els.length; i++) {
2225
- let _re = await renderHST(hst, n, 'sub');
2226
- const el = els[i];
2686
+ const { el, viewsIndex } = els[i];
2687
+ // Create/reuse el.vars BEFORE rendering, so the fresh
2688
+ // _v instance renderHST() constructs uses this EXACT
2689
+ // object as its own .vars — see renderHST()'s ownVars
2690
+ // param comment for why that identity matters.
2691
+ el.vars = el.vars || {};
2692
+ el.views = el.views || [];
2693
+ el.fns = el.fns || {};
2694
+ let _re = await renderHST(hst, n, 'sub', undefined, null, scopeBase.concat(["views", viewsIndex]), el.vars, el.fns, el.views);
2695
+ el._re = _re;
2227
2696
  el.innerHTML = "";
2228
2697
  // cl(el);
2229
2698
  el.append(..._re._RealDOM);
2230
2699
  _re.renderAll();
2231
2700
  }
2232
- // _re._RealDOM.forEach(_el => el[0].appendChild(_el))
2701
+ // _re._RealDOM.forEach(_el => el[0].appendChild(_el))
2233
2702
  // cl(_re);
2234
2703
  }
2235
2704
  } else {
@@ -2255,12 +2724,21 @@ async function renderView(n, isSub, d, type = 'views') {
2255
2724
 
2256
2725
  let appSelector = appSettings.App ?? "[app]";
2257
2726
  var appContainer = $(appSelector);
2727
+ // 2026-09-18: whether THIS call just replaced the layout's
2728
+ // own DOM wholesale — needed below by the widget
2729
+ // resolve/mount/diff pass, since a widget whose resolution
2730
+ // hasn't changed still needs its already-rendered nodes
2731
+ // reattached to the fresh layout element (the old one they
2732
+ // were living in is gone), even though nothing about the
2733
+ // widget itself needs re-rendering.
2734
+ let layoutJustSwapped = false;
2258
2735
  if (appContainer.length) {
2259
2736
  let currentLayout = $(appSelector).data('layout');
2260
2737
  if (currentLayout != layout) {
2261
2738
  let _rel = await renderHST(hstL, layout, 'layout');
2262
2739
  appContainer.data('layout', layout).html(_rel._RealDOM);
2263
2740
  _rel.renderAll();
2741
+ layoutJustSwapped = true;
2264
2742
  } else {
2265
2743
  // cl("Same Layout");
2266
2744
  }
@@ -2270,6 +2748,59 @@ async function renderView(n, isSub, d, type = 'views') {
2270
2748
  appContainer = $(appSelector);
2271
2749
  appContainer.data('layout', layout).html(_rel._RealDOM);
2272
2750
  _rel.renderAll();
2751
+ layoutJustSwapped = true;
2752
+ }
2753
+
2754
+ // Persistent layout widgets (_vt.Widgets, 2026-09-18) — see
2755
+ // that object's own declaration comment for the design.
2756
+ // Runs on EVERY navigation, not gated on layoutJustSwapped:
2757
+ // a view's own settings (hasNav: false, hasNav: "nav2", ...)
2758
+ // can change what belongs in a region even when the layout
2759
+ // itself didn't change at all. hstL.regions is precomputed
2760
+ // build-time by fcs.js's annotateLayoutRegions() — the bare
2761
+ // (valueless) attribute names the CURRENTLY MOUNTED layout
2762
+ // itself declares (e.g. ["header","nav","footer"] for
2763
+ // `<header header>`/`<nav nav>`/`<footer footer>`).
2764
+ let declaredRegions = (hstL && hstL.regions) || [];
2765
+ for (const regionName of declaredRegions) {
2766
+ let settingsKey = "has" + regionName[0].toUpperCase() + regionName.slice(1);
2767
+ let want = _re.view.settings.hasOwnProperty(settingsKey) ? _re.view.settings[settingsKey] : true;
2768
+ let resolvedFile = want === false ? null : (want === true ? regionName : want);
2769
+
2770
+ let w = _vt.Widgets[regionName] || (_vt.Widgets[regionName] = { vars: {}, fns: {}, views: [], _re: null, _resolvedFile: undefined });
2771
+
2772
+ if (resolvedFile !== w._resolvedFile) {
2773
+ // Resolution genuinely changed since the last time
2774
+ // this region was resolved — mount fresh, or clear.
2775
+ if (resolvedFile === null) {
2776
+ $('[' + regionName + ']').html('');
2777
+ w._re = null;
2778
+ } else {
2779
+ let wFilePath = "src/views/widgets/" + resolvedFile + ".view";
2780
+ let wHst = _payload['views'][btoa(wFilePath)];
2781
+ if (wHst) {
2782
+ let _rew = await renderHST(wHst, resolvedFile, 'widget', undefined, null, ["Widgets", regionName], w.vars, w.fns, w.views);
2783
+ w._re = _rew;
2784
+ $('[' + regionName + ']').html(_rew._RealDOM);
2785
+ _rew.renderAll();
2786
+ }
2787
+ }
2788
+ w._resolvedFile = resolvedFile;
2789
+ } else if (layoutJustSwapped && w._re) {
2790
+ // Same widget as before, but the layout element it
2791
+ // lives in was just replaced wholesale — reattach
2792
+ // the EXISTING rendered nodes (.html() moves real
2793
+ // node references, same as [body]/[slot] below,
2794
+ // it doesn't clone) rather than re-rendering. The
2795
+ // widget's own state (w.vars/.fns/.views) never
2796
+ // lived on the old DOM to begin with, so nothing
2797
+ // was lost — only the attachment point needed
2798
+ // fixing up.
2799
+ $('[' + regionName + ']').html(w._re._RealDOM);
2800
+ }
2801
+ // else: nothing changed and the layout didn't just
2802
+ // swap — this region's DOM/state is left completely
2803
+ // untouched, on purpose (the whole point of this).
2273
2804
  }
2274
2805
 
2275
2806
  // return;