@vmz/core 0.0.3 → 0.0.4

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/dist/dom.js CHANGED
@@ -115,7 +115,7 @@ export function __vmzPrecisionSnapshot() {
115
115
  };
116
116
  }
117
117
  /**
118
- * @param { => any} fn
118
+ * @param {() => any} fn
119
119
  * @param {string | null} [depKey]
120
120
  * @param {number | string | null} [bindingId]
121
121
  */
@@ -166,16 +166,23 @@ async function resolveComponent(name) {
166
166
  * @param {new (props?: object) => any} Component
167
167
  * @param {object} [props]
168
168
  */
169
- export async function renderToString(Component, props = {}) {
169
+ export async function renderToString(Component, props = {}, opts = {}) {
170
+ const signal = opts && opts.signal;
171
+ if (signal && signal.aborted)
172
+ return '';
170
173
  const inst = createInstance(Component, props);
171
174
  if (typeof inst.onMount === 'function') {
172
175
  await inst.onMount();
173
176
  }
177
+ if (signal && signal.aborted)
178
+ return '';
174
179
  // production Direct emit: SSR only via Direct serialize schedule — never `render`.
175
180
  if (!(Component && Component.__vmzDirect && typeof Component.__vmzCreate === 'function')) {
176
181
  throw new Error(`vmz:dom renderToString() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
177
182
  }
178
183
  const root = await runDirectSerializeTreeWithMounts(Component, inst);
184
+ if (opts && opts.slotHtml != null)
185
+ injectDefaultSlotHtml(root, opts.slotHtml);
179
186
  return flattenSerializeNode(root);
180
187
  }
181
188
  /**
@@ -184,7 +191,7 @@ export async function renderToString(Component, props = {}) {
184
191
  * Supports AbortSignal for cancel; consumers should respect backpressure (await between chunks).
185
192
  * @param {new (props?: object) => any} Component
186
193
  * @param {object} [props]
187
- * @param {{ signal?: AbortSignal }} [opts]
194
+ * @param {{ signal?: AbortSignal, slotHtml?: string }} [opts]
188
195
  * @returns {AsyncGenerator<string, void, void>}
189
196
  */
190
197
  export async function* renderToStream(Component, props = {}, opts = {}) {
@@ -203,6 +210,8 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
203
210
  throw new Error(`vmz:dom renderToStream() requires __vmzCreate (Direct); blueprint render() removed (production Direct emit)`);
204
211
  }
205
212
  const root = await runDirectSerializeTreeWithMounts(Component, inst);
213
+ if (opts && opts.slotHtml != null)
214
+ injectDefaultSlotHtml(root, opts.slotHtml);
206
215
  if (aborted())
207
216
  return;
208
217
  for (const chunk of streamSerializeChunks(root)) {
@@ -218,6 +227,111 @@ export async function* renderToStream(Component, props = {}, opts = {}) {
218
227
  destroy(inst);
219
228
  }
220
229
  }
230
+ /**
231
+ * Fill the layout-owned default `<slot>` with pre-rendered HTML (layout SSR wrap).
232
+ * Skips nested component hosts (`data-vmz`) — their slots are for child projection
233
+ * (e.g. Button label), not the page outlet. Without this, DFS hits LocaleToggle→Button
234
+ * before Layout's `<main><slot>`, and the entire page HTML lands inside a button.
235
+ * @param {any} node
236
+ * @param {string} html
237
+ */
238
+ function injectDefaultSlotHtml(node, html) {
239
+ if (!node || typeof node !== 'object')
240
+ return false;
241
+ if (node.__kind === 'el' && node.tag === 'slot' && !(node.attrs && node.attrs.name)) {
242
+ node.__rawHtml = String(html ?? '');
243
+ node.children = [];
244
+ return true;
245
+ }
246
+ // Nested Direct component wrapper from serializeApi.component — do not search inside.
247
+ if (node.__kind === 'el' && node.attrs && node.attrs['data-vmz'] != null) {
248
+ return false;
249
+ }
250
+ const kids = node.children;
251
+ if (Array.isArray(kids)) {
252
+ for (const c of kids) {
253
+ if (injectDefaultSlotHtml(c, html))
254
+ return true;
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ /**
260
+ * Live-DOM counterpart: first default `<slot>` owned by this tree, not by a nested
261
+ * `[data-vmz]` component (Button/Link labels, etc.).
262
+ * @param {Element | null | undefined} root
263
+ * @returns {Element | null}
264
+ */
265
+ export function findOwnedDefaultSlot(root) {
266
+ if (!root || root.nodeType !== 1)
267
+ return null;
268
+ const tag = String(root.tagName || '').toLowerCase();
269
+ if (tag === 'slot' && !root.getAttribute('name'))
270
+ return root;
271
+ const kids = root.children;
272
+ if (!kids || !kids.length)
273
+ return null;
274
+ for (let i = 0; i < kids.length; i++) {
275
+ const c = kids[i];
276
+ if (c.nodeType !== 1)
277
+ continue;
278
+ // Nested component host — its slots are not the layout page outlet.
279
+ if (c.hasAttribute('data-vmz'))
280
+ continue;
281
+ const hit = findOwnedDefaultSlot(c);
282
+ if (hit)
283
+ return hit;
284
+ }
285
+ return null;
286
+ }
287
+ /**
288
+ * Hydrate/mount a file-route page inside an optional layout chain (outer → inner).
289
+ * Mirrors SSR `slotHtml` wrapping: each layout's owned default slot becomes the
290
+ * outlet for the next layout or the page. Retains layout instances on `container`
291
+ * so SPA transitions can dispose only the page host.
292
+ * @param {new (props?: object) => any} Page
293
+ * @param {Element} container
294
+ * @param {object} [props]
295
+ * @param {Array<new (props?: object) => any>} [layoutCtors] outer → inner
296
+ * @param {{ preserveState?: boolean | Record<string, unknown>, skipOnMount?: boolean }} [opts]
297
+ */
298
+ export async function hydrateRoute(Page, container, props = {}, layoutCtors = [], opts = {}) {
299
+ if (typeof document === 'undefined') {
300
+ throw new Error('vmz:dom hydrateRoute() requires a document (browser)');
301
+ }
302
+ if (container.__vmzInst) {
303
+ destroy(container.__vmzInst);
304
+ container.__vmzInst = null;
305
+ }
306
+ container.__vmzPageHost = null;
307
+ container.__vmzLayoutInsts = null;
308
+ /** @type {object[]} */
309
+ const layoutInsts = [];
310
+ let host = container;
311
+ const ctors = Array.isArray(layoutCtors) ? layoutCtors.filter(Boolean) : [];
312
+ for (const Layout of ctors) {
313
+ const inst = await mount(Layout, host, {});
314
+ layoutInsts.push(inst);
315
+ const slot = findOwnedDefaultSlot(inst.__vmzDomRoot);
316
+ const outlet = document.createElement('div');
317
+ outlet.setAttribute('data-vmz-outlet', '');
318
+ if (slot && slot.parentNode)
319
+ slot.replaceWith(outlet);
320
+ else if (inst.__vmzDomRoot && typeof inst.__vmzDomRoot.appendChild === 'function') {
321
+ inst.__vmzDomRoot.appendChild(outlet);
322
+ }
323
+ else {
324
+ host.appendChild(outlet);
325
+ }
326
+ host = outlet;
327
+ }
328
+ const pageInst = await hydrate(Page, host, props, opts);
329
+ container.__vmzPageHost = host;
330
+ container.__vmzLayoutInsts = layoutInsts;
331
+ // Outer layout (or page if no layouts) owns the #app instance for destroy().
332
+ container.__vmzInst = layoutInsts[0] || pageInst;
333
+ return pageInst;
334
+ }
221
335
  /**
222
336
  * Mount once; later updates are dep patches only (never re-run structure).
223
337
  * Requires compiler `__vmzCreate` (production Direct emit — no blueprint fallback).
@@ -288,12 +402,25 @@ async function createFromComponent(Component, inst) {
288
402
  * @param {object} inst
289
403
  */
290
404
  function runDirectCreate(Component, inst) {
405
+ // Nested component creates (e.g. Button inside parent ifBlock branch) must not
406
+ // leak bindAttr/bindText into the parent's `_branchBinds` / `_itemPatches` sink —
407
+ // that steals numeric BindingIds (0) and corrupts parent deps (density → type).
408
+ const prevInst = directApi._inst;
409
+ const prevBranch = directApi._branchBinds;
410
+ const prevItems = directApi._itemPatches;
411
+ const prevEach = directApi._eachCtx;
291
412
  directApi._inst = inst;
413
+ directApi._branchBinds = null;
414
+ directApi._itemPatches = null;
415
+ directApi._eachCtx = null;
292
416
  try {
293
417
  return Component.__vmzCreate.call(inst, directApi);
294
418
  }
295
419
  finally {
296
- directApi._inst = null;
420
+ directApi._inst = prevInst;
421
+ directApi._branchBinds = prevBranch;
422
+ directApi._itemPatches = prevItems;
423
+ directApi._eachCtx = prevEach;
297
424
  }
298
425
  }
299
426
  /**
@@ -369,6 +496,8 @@ function flattenSerializeNode(node) {
369
496
  if (node.__kind === 'el') {
370
497
  const tag = node.tag || 'div';
371
498
  if (tag === 'slot') {
499
+ if (node.__rawHtml != null)
500
+ return String(node.__rawHtml);
372
501
  return (node.children || []).map(flattenSerializeNode).join('');
373
502
  }
374
503
  const { open } = serializeOpenTag(node);
@@ -404,6 +533,10 @@ function* streamSerializeChunks(node) {
404
533
  if (node.__kind === 'el') {
405
534
  const tag = node.tag || 'div';
406
535
  if (tag === 'slot') {
536
+ if (node.__rawHtml != null) {
537
+ yield String(node.__rawHtml);
538
+ return;
539
+ }
407
540
  for (const c of node.children || [])
408
541
  yield* streamSerializeChunks(c);
409
542
  return;
@@ -476,10 +609,7 @@ const serializeApi = {
476
609
  attr(el, name, value) {
477
610
  if (!el || el.__kind !== 'el')
478
611
  return;
479
- if (value == null)
480
- delete el.attrs[name];
481
- else
482
- el.attrs[name] = String(value);
612
+ applySerializeAttr(el, name, value);
483
613
  },
484
614
  on() {
485
615
  /* events are no-ops during SSR */
@@ -502,11 +632,50 @@ const serializeApi = {
502
632
  catch {
503
633
  raw = null;
504
634
  }
505
- const key = name === 'className' ? 'class' : name;
506
- if (raw == null)
507
- delete el.attrs[key];
508
- else
509
- el.attrs[key] = String(raw);
635
+ applySerializeAttr(el, name, raw);
636
+ },
637
+ bindComponentProp() {
638
+ /* SSR: props already resolved into the child instance at create */
639
+ },
640
+ projectDefaultSlot(hostEl, node) {
641
+ if (!hostEl || node == null)
642
+ return;
643
+ // serializeApi.component returns a serialize el tree (or island shell).
644
+ const root = hostEl.__kind === 'el' ? hostEl : null;
645
+ const findSlot = (n) => {
646
+ if (!n || n.__kind !== 'el')
647
+ return null;
648
+ if (n.tag === 'slot' && !(n.attrs && n.attrs.name))
649
+ return n;
650
+ for (const c of n.children || []) {
651
+ const hit = findSlot(c);
652
+ if (hit)
653
+ return hit;
654
+ }
655
+ return null;
656
+ };
657
+ // Prefer searching the component body (first child of host wrapper).
658
+ let slot = null;
659
+ if (root) {
660
+ for (const c of root.children || []) {
661
+ slot = findSlot(c);
662
+ if (slot)
663
+ break;
664
+ }
665
+ if (!slot)
666
+ slot = findSlot(root);
667
+ }
668
+ if (slot) {
669
+ slot.__rawHtml = null;
670
+ if (!Array.isArray(slot.children))
671
+ slot.children = [];
672
+ // Append — multiple projectDefaultSlot calls must accumulate (SSR).
673
+ // Client path replaces the live <slot> then appends siblings; serialize must push.
674
+ slot.children.push(node);
675
+ return;
676
+ }
677
+ if (root)
678
+ root.appendChild(node);
510
679
  },
511
680
  setHtml(el, value) {
512
681
  if (!el || el.__kind !== 'el')
@@ -584,7 +753,7 @@ const serializeApi = {
584
753
  }
585
754
  const dom = spec.createItem.call(inst, serializeApi, box);
586
755
  if (dom) {
587
- // Parity with Direct eachBlock: keyed items expose data-vmz-key for hydrate/tests.
756
+ // SSR only: serialize key into HTML for hydrate/debug. Direct client does not write this attr.
588
757
  if (dom.__kind === 'el')
589
758
  serializeApi.attr(dom, 'data-vmz-key', String(k));
590
759
  frag.appendChild(dom);
@@ -706,10 +875,7 @@ const directApi = {
706
875
  return document.createDocumentFragment();
707
876
  },
708
877
  attr(el, name, value) {
709
- if (value == null)
710
- el.removeAttribute(name);
711
- else
712
- el.setAttribute(name, String(value));
878
+ applyDomAttr(el, name, value);
713
879
  },
714
880
  on(el, type, handler) {
715
881
  const inst = directApi._inst;
@@ -733,7 +899,7 @@ const directApi = {
733
899
  * @param {object} inst
734
900
  * @param {number|string|null} bindingId
735
901
  * @param {string[]} deps
736
- * @param { => any} get
902
+ * @param {() => any} get
737
903
  * @param {Text} textNode
738
904
  * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
739
905
  */
@@ -746,7 +912,7 @@ const directApi = {
746
912
  * @param {object} inst
747
913
  * @param {number|string|null} bindingId
748
914
  * @param {string[]} deps
749
- * @param { => any} get
915
+ * @param {() => any} get
750
916
  * @param {Element} el
751
917
  * @param {string} name
752
918
  * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
@@ -754,13 +920,14 @@ const directApi = {
754
920
  bindAttr(inst, bindingId, deps, get, el, name, cf) {
755
921
  wireDirectBind(inst, bindingId, deps, get, (raw) => {
756
922
  if (name === 'class' || name === 'className') {
757
- el.setAttribute('class', String(raw ?? ''));
758
- }
759
- else if (raw == null) {
760
- el.removeAttribute(name);
923
+ const s = String(raw ?? '');
924
+ if (s)
925
+ el.setAttribute('class', s);
926
+ else if (el.hasAttribute('class'))
927
+ el.removeAttribute('class');
761
928
  }
762
929
  else {
763
- el.setAttribute(name, String(raw));
930
+ applyDomAttr(el, name, raw);
764
931
  }
765
932
  }, cf);
766
933
  },
@@ -772,7 +939,7 @@ const directApi = {
772
939
  * @param {object} inst
773
940
  * @param {number|string|null} bindingId
774
941
  * @param {string[]} deps
775
- * @param { => any} get
942
+ * @param {() => any} get
776
943
  * @param {Element} el
777
944
  * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
778
945
  */
@@ -841,6 +1008,62 @@ const directApi = {
841
1008
  }
842
1009
  return host;
843
1010
  },
1011
+ /**
1012
+ * Keep nested Direct child props live with parent field writes.
1013
+ * @param {object} hostInst
1014
+ * @param {HTMLElement} hostEl
1015
+ * @param {string} propName
1016
+ * @param {string[]} deps
1017
+ * @param {() => any} get
1018
+ */
1019
+ bindComponentProp(hostInst, hostEl, propName, deps, get) {
1020
+ // Use a stable BindingId so flushPending schedules this patch via the IR
1021
+ // path. A null bindingId only lands in `__vmzBinders` and was skipped when
1022
+ // the same parent field also had bindText/bindAttr BindingIds.
1023
+ if (hostEl && hostEl.__vmzPropBindSeq == null) {
1024
+ hostEl.__vmzPropBindSeq = ++directPropBindSeq;
1025
+ }
1026
+ const seq = hostEl && hostEl.__vmzPropBindSeq != null ? hostEl.__vmzPropBindSeq : ++directPropBindSeq;
1027
+ const bindingId = `pc:${seq}:${propName}`;
1028
+ wireDirectBind(hostInst, bindingId, deps, get, (raw) => {
1029
+ const child = hostEl && hostEl.__vmzInst;
1030
+ if (!child || child.__vmzDestroyed)
1031
+ return;
1032
+ if (typeof propName !== 'string' || !propName || propName.startsWith('#'))
1033
+ return;
1034
+ child[propName] = raw;
1035
+ scheduleRefresh(child, { type: 'replace', root: propName });
1036
+ });
1037
+ },
1038
+ /**
1039
+ * Project parent children into nested Direct component default `<slot>`.
1040
+ * @param {HTMLElement} hostEl
1041
+ * @param {Node} node
1042
+ */
1043
+ projectDefaultSlot(hostEl, node) {
1044
+ if (!hostEl || node == null)
1045
+ return;
1046
+ const child = hostEl.__vmzInst;
1047
+ const root = (child && child.__vmzDomRoot) || hostEl;
1048
+ /** @type {Element | null} */
1049
+ let slot = null;
1050
+ if (root && root.nodeType === 1) {
1051
+ if (String(root.tagName || '').toLowerCase() === 'slot' && !root.getAttribute('name')) {
1052
+ slot = root;
1053
+ }
1054
+ else if (typeof root.querySelector === 'function') {
1055
+ slot = root.querySelector('slot:not([name])');
1056
+ }
1057
+ }
1058
+ if (slot && slot.parentNode) {
1059
+ slot.replaceWith(node);
1060
+ return;
1061
+ }
1062
+ if (root && typeof root.appendChild === 'function')
1063
+ root.appendChild(node);
1064
+ else
1065
+ hostEl.appendChild(node);
1066
+ },
844
1067
  /**
845
1068
  * Direct if/else — no blueprint `kind: "if"` dispatch.
846
1069
  * @param {object} inst
@@ -958,7 +1181,7 @@ const directApi = {
958
1181
  },
959
1182
  /**
960
1183
  * Direct keyed each — no blueprint `kind: "each"` dispatch.
961
- * /: Set/Map + Fragment batch insert; item-local binds; host selected; event delegate.
1184
+ * /: Set/Map + Fragment batch insert; item-local binds; host field dispatch; event delegate.
962
1185
  * @param {object} inst
963
1186
  * @param {number|string|null} bindingId
964
1187
  * @param {string[]} deps
@@ -989,6 +1212,9 @@ const directApi = {
989
1212
  /** @type {Element | null} */
990
1213
  let delegateRoot = null;
991
1214
  const itemKey = (box) => {
1215
+ if (rowKeyField != null && box && box.item != null) {
1216
+ return box.item[rowKeyField];
1217
+ }
992
1218
  if (typeof spec.key === 'function') {
993
1219
  try {
994
1220
  return spec.key.call(inst, box);
@@ -999,6 +1225,13 @@ const directApi = {
999
1225
  }
1000
1226
  return box.index;
1001
1227
  };
1228
+ /** Reused for keyed lookups — avoid per-row `{item,index}` alloc on create/update. */
1229
+ const keyScratch = { item: null, index: 0 };
1230
+ const keyOf = (item, index) => {
1231
+ keyScratch.item = item;
1232
+ keyScratch.index = index;
1233
+ return itemKey(keyScratch);
1234
+ };
1002
1235
  const readList = () => {
1003
1236
  let list = [];
1004
1237
  try {
@@ -1012,11 +1245,31 @@ const directApi = {
1012
1245
  return list;
1013
1246
  };
1014
1247
  const runEntryPatches = (entry, depKey, onlyBindingId) => {
1015
- if (!entry || !entry.patches)
1248
+ if (!entry)
1249
+ return;
1250
+ if (entryIsBp(entry) && applyBp) {
1251
+ if (onlyBindingId != null && blueprintBindIds && !blueprintBindIds.has(String(onlyBindingId))) {
1252
+ return;
1253
+ }
1254
+ try {
1255
+ applyBp(entry);
1256
+ }
1257
+ catch (err) {
1258
+ console.error('vmz:dom each item', err);
1259
+ }
1260
+ return;
1261
+ }
1262
+ if (!entry.patches)
1016
1263
  return;
1017
1264
  for (const p of entry.patches) {
1018
- if (onlyBindingId != null && p.__vmzBindingId != null && String(p.__vmzBindingId) !== String(onlyBindingId)) {
1019
- continue;
1265
+ if (onlyBindingId != null) {
1266
+ if (p.__vmzBindingIds) {
1267
+ if (!p.__vmzBindingIds.has(String(onlyBindingId)))
1268
+ continue;
1269
+ }
1270
+ else if (p.__vmzBindingId != null && String(p.__vmzBindingId) !== String(onlyBindingId)) {
1271
+ continue;
1272
+ }
1020
1273
  }
1021
1274
  try {
1022
1275
  runPatch(p, depKey, onlyBindingId);
@@ -1032,14 +1285,21 @@ const directApi = {
1032
1285
  const runAt = (i) => {
1033
1286
  if (i < 0 || i >= list.length)
1034
1287
  return;
1035
- const box = { item: list[i], index: i };
1036
- const k = itemKey(box);
1288
+ const item = list[i];
1289
+ const k = rowKeyOf(item, i);
1037
1290
  const entry = keyed.get(k);
1038
1291
  if (!entry)
1039
1292
  return;
1040
- entry.box.item = list[i];
1041
- entry.box.index = i;
1042
- tagItemPatches(entry.patches, i);
1293
+ if (entry.nodeType === 1)
1294
+ entry.__vmzBox = item;
1295
+ else {
1296
+ entry.item = item;
1297
+ entry.index = i;
1298
+ if (entry.dom && entry.bp)
1299
+ entry.dom.__vmzBox = item;
1300
+ }
1301
+ if (entry.patches)
1302
+ tagItemPatches(entry.patches, i);
1043
1303
  runEntryPatches(entry, (leafDeps && leafDeps[0]) || null, onlyBindingId);
1044
1304
  };
1045
1305
  if (allowIdx) {
@@ -1079,11 +1339,16 @@ const directApi = {
1079
1339
  return;
1080
1340
  const trie = inst.__vmzFlushTrie;
1081
1341
  const hostFields = [];
1342
+ let listReplaced = false;
1082
1343
  for (const d of leafDeps || []) {
1083
1344
  if (!d)
1084
1345
  continue;
1085
- if (d.includes('.*') || (d.includes('[') && d.includes(']')))
1346
+ if (d.includes('.*') || (d.includes('[') && d.includes(']'))) {
1347
+ const root = depRootField(d);
1348
+ if (trie && root && trie[root] && trie[root].replace)
1349
+ listReplaced = true;
1086
1350
  continue;
1351
+ }
1087
1352
  hostFields.push(depRootField(d) || d);
1088
1353
  }
1089
1354
  const hostDirty = !!trie &&
@@ -1091,6 +1356,9 @@ const directApi = {
1091
1356
  const n = trie[f];
1092
1357
  return n && (n.replace || n.dirty);
1093
1358
  });
1359
+ // Full list replace is owned by eachBlock apply() — skip leaf re-walk.
1360
+ if (listReplaced && !hostDirty)
1361
+ return;
1094
1362
  if (hostDirty && hostFields.length) {
1095
1363
  refreshHostKeyed(hostFields, bId);
1096
1364
  return;
@@ -1156,9 +1424,16 @@ const directApi = {
1156
1424
  let n = /** @type {Node | null} */ (ev.target);
1157
1425
  while (n && n !== delegateRoot) {
1158
1426
  if (n.nodeType === 1) {
1159
- const bag = /** @type {Element} */ (n).__vmzEvt;
1427
+ const el = /** @type {Element} */ (n);
1428
+ const act = el.__vmzAct || el.getAttribute('data-vmz-act');
1429
+ if (typeof act === 'string' && act) {
1430
+ actionHandler(act).call(inst, ev, el);
1431
+ return;
1432
+ }
1433
+ const bag = el.__vmzEvt;
1160
1434
  if (bag && typeof bag[type] === 'function') {
1161
- bag[type].call(inst, ev);
1435
+ // Pass the element so shared each-item handlers can read __vmzBox.
1436
+ bag[type].call(inst, ev, el);
1162
1437
  return;
1163
1438
  }
1164
1439
  }
@@ -1183,27 +1458,781 @@ const directApi = {
1183
1458
  if (node.nodeType === 1) {
1184
1459
  if (node.__vmzEvt)
1185
1460
  node.__vmzEvt = null;
1461
+ if (node.__vmzBox)
1462
+ node.__vmzBox = null;
1463
+ if (node.__vmzAct)
1464
+ node.__vmzAct = null;
1465
+ if (node.__vmzKey != null)
1466
+ node.__vmzKey = null;
1186
1467
  for (let c = node.firstChild; c; c = c.nextSibling)
1187
1468
  walk(c);
1188
1469
  }
1189
1470
  };
1190
1471
  walk(root);
1191
1472
  };
1473
+ const pathFromRoot = (root, node) => {
1474
+ /** @type {number[]} */
1475
+ const path = [];
1476
+ let n = /** @type {Node | null} */ (node);
1477
+ while (n && n !== root) {
1478
+ const parent = n.parentNode;
1479
+ if (!parent)
1480
+ return null;
1481
+ let i = 0;
1482
+ for (let c = parent.firstChild; c; c = c.nextSibling) {
1483
+ if (c === n)
1484
+ break;
1485
+ i++;
1486
+ }
1487
+ path.push(i);
1488
+ n = parent;
1489
+ }
1490
+ if (n !== root)
1491
+ return null;
1492
+ path.reverse();
1493
+ return path;
1494
+ };
1495
+ const nodeAtPath = (root, path) => {
1496
+ let n = /** @type {Node | null} */ (root);
1497
+ for (let i = 0; i < path.length; i++) {
1498
+ if (!n)
1499
+ return null;
1500
+ n = n.childNodes[path[i]] || null;
1501
+ }
1502
+ return n;
1503
+ };
1504
+ /**
1505
+ * Shared each-item event handlers (one per method name for the whole block).
1506
+ * Element carries `__vmzBox`; delegate passes the element as 2nd arg.
1507
+ * @type {Record<string, (ev: Event, el: Element) => void>}
1508
+ */
1509
+ const sharedActions = Object.create(null);
1510
+ /** @type {Record<string, string>} method → item field for action arg (fallback blueprint). */
1511
+ const actionArgFields = Object.create(null);
1512
+ const actionHandler = (method) => {
1513
+ if (!sharedActions[method]) {
1514
+ sharedActions[method] = function (ev, el) {
1515
+ let n = /** @type {Node | null} */ (el);
1516
+ while (n && n.nodeType === 1) {
1517
+ const box = /** @type {Element} */ (n).__vmzBox;
1518
+ if (box) {
1519
+ const item = box.item != null ? box.item : box;
1520
+ const argField = rowActArgField != null ? rowActArgField : actionArgFields[method] != null ? actionArgFields[method] : null;
1521
+ if (argField == null || item == null)
1522
+ return;
1523
+ const arg = item[argField];
1524
+ const fn = this[method];
1525
+ if (typeof fn === 'function')
1526
+ fn.call(this, arg);
1527
+ return;
1528
+ }
1529
+ n = n.parentNode;
1530
+ }
1531
+ };
1532
+ }
1533
+ return sharedActions[method];
1534
+ };
1535
+ /**
1536
+ * @returns {{ method: string, argField: string } | null}
1537
+ */
1538
+ const parseActionMethod = (handler) => {
1539
+ if (typeof handler !== 'function')
1540
+ return null;
1541
+ try {
1542
+ const src = Function.prototype.toString.call(handler);
1543
+ // this.m(box.item.<field>) — field from author surface.
1544
+ const m = src.match(/this\.([A-Za-z_$][\w$]*)\s*\(\s*[A-Za-z_$][\w$]*\.item\.([A-Za-z_$][\w$]*)\s*\)/);
1545
+ return m ? { method: m[1], argField: m[2] } : null;
1546
+ }
1547
+ catch {
1548
+ return null;
1549
+ }
1550
+ };
1551
+ /**
1552
+ * Row blueprint: first createItem records dynamic slots; later rows clone + hydrate
1553
+ * without re-running compiled createItem (avoids per-row get/CF/on closures).
1554
+ * @type {null | {
1555
+ * tpl: Element,
1556
+ * texts: Array<{ path: number[], bindingId: any, deps: string[], field: string, get: (root: Element) => Node }>,
1557
+ * attrs: Array<{ path: number[], bindingId: any, deps: string[], name: string, onVal: string, offVal: string, get: (root: Element) => Element }>,
1558
+ * ons: Array<{ path: number[], type: string, method: string, get: (root: Element) => Element }>,
1559
+ * bindIds: Set<string>,
1560
+ * }}
1561
+ */
1562
+ let blueprint = null;
1563
+ let blueprintOk = true;
1564
+ /** @type {Set<string> | null} */
1565
+ let blueprintBindIds = null;
1566
+ /** @type {null | ((root: Element, entry: any) => void)} */
1567
+ let hydrateBp = null;
1568
+ /** @type {null | ((entry: any) => void)} */
1569
+ let applyBp = null;
1570
+ /**
1571
+ * Compile-time rowKernel installed — static HTML rows, no nested component dispose.
1572
+ * Shape-specific walks live in emitted hydrate/apply (Rust), not here.
1573
+ */
1574
+ let hasRowKernel = false;
1575
+ /** @type {string | null} item field used as key when rowKernel.keyField is set */
1576
+ let rowKeyField = null;
1577
+ /** @type {string | null} item field passed to delegated actions (from rowKernel.actArgField) */
1578
+ let rowActArgField = null;
1579
+ /** Recycle object bp entries (runtime-recorded blueprint fallback). */
1580
+ /** @type {any[]} */
1581
+ const entryPool = [];
1582
+ const allocBpEntry = () => {
1583
+ const e = entryPool.pop();
1584
+ if (e)
1585
+ return e;
1586
+ return { item: null, dom: null, bp: 1, t0: null, t1: null };
1587
+ };
1588
+ const releaseBpEntry = (entry) => {
1589
+ if (!entry)
1590
+ return;
1591
+ // DOM-as-entry (Element): drop expandos.
1592
+ if (entry.nodeType === 1) {
1593
+ entry.__vmzBox = null;
1594
+ entry.__vmzT0 = null;
1595
+ entry.__vmzT1 = null;
1596
+ entry.__vmzBp = null;
1597
+ return;
1598
+ }
1599
+ if (!entry.bp || entryPool.length >= 4096)
1600
+ return;
1601
+ entry.item = null;
1602
+ entry.dom = null;
1603
+ entry.t0 = null;
1604
+ entry.t1 = null;
1605
+ entry.a0 = null;
1606
+ entry.patches = null;
1607
+ entryPool.push(entry);
1608
+ };
1609
+ const entryDom = (entry) => (entry && entry.nodeType === 1 ? entry : entry && entry.dom);
1610
+ const entryIsBp = (entry) => !!(entry && (entry.nodeType === 1 || entry.bp || entry.__vmzBp));
1611
+ const entryItem = (entry) => {
1612
+ if (!entry)
1613
+ return null;
1614
+ if (entry.nodeType === 1)
1615
+ return entry.__vmzBox;
1616
+ if (entry.bp)
1617
+ return entry.item;
1618
+ return entry.box && entry.box.item;
1619
+ };
1620
+ const rowKeyOf = (item, index) => {
1621
+ if (rowKeyField != null && item != null)
1622
+ return item[rowKeyField];
1623
+ return keyOf(item, index);
1624
+ };
1625
+ /** Drop all row DOM between markers; rowKernel rows skip per-node dispose walks. */
1626
+ const fastWipeRows = () => {
1627
+ const parent = end.parentNode;
1628
+ if (!parent) {
1629
+ keyed.clear();
1630
+ return;
1631
+ }
1632
+ let node = start.nextSibling;
1633
+ if (node && node !== end) {
1634
+ if (hasRowKernel || (blueprint && blueprintOk)) {
1635
+ const range = document.createRange();
1636
+ range.setStartBefore(node);
1637
+ range.setEndBefore(end);
1638
+ range.deleteContents();
1639
+ }
1640
+ else {
1641
+ while (node && node !== end) {
1642
+ const next = node.nextSibling;
1643
+ noteDomRemove();
1644
+ clearDomEvt(node);
1645
+ disposeDomTree(node);
1646
+ node.remove();
1647
+ node = next;
1648
+ }
1649
+ }
1650
+ }
1651
+ for (const [, entry] of keyed)
1652
+ releaseBpEntry(entry);
1653
+ keyed.clear();
1654
+ };
1655
+ const makeChildGetter = (path) => {
1656
+ const len = path.length;
1657
+ if (len === 0)
1658
+ return (root) => root;
1659
+ if (len === 1) {
1660
+ const a = path[0];
1661
+ return (root) => root.childNodes[a];
1662
+ }
1663
+ if (len === 2) {
1664
+ const a = path[0];
1665
+ const b = path[1];
1666
+ return (root) => root.childNodes[a].childNodes[b];
1667
+ }
1668
+ if (len === 3) {
1669
+ const a = path[0];
1670
+ const b = path[1];
1671
+ const c = path[2];
1672
+ return (root) => root.childNodes[a].childNodes[b].childNodes[c];
1673
+ }
1674
+ return (root) => {
1675
+ let n = /** @type {Node} */ (root);
1676
+ for (let i = 0; i < len; i++)
1677
+ n = n.childNodes[path[i]];
1678
+ return n;
1679
+ };
1680
+ };
1681
+ const userCreateItem = spec.createItem;
1682
+ // Compile-time row kernel (Rust Direct emit) — skip runtime blueprint recording.
1683
+ if (spec.rowKernel && typeof spec.rowKernel.html === 'string' && typeof spec.rowKernel.hydrate === 'function') {
1684
+ try {
1685
+ const tplHost = document.createElement('template');
1686
+ tplHost.innerHTML = spec.rowKernel.html;
1687
+ const row = tplHost.content.firstElementChild;
1688
+ if (row && row.nodeType === 1) {
1689
+ blueprint = {
1690
+ tpl: /** @type {Element} */ (row.cloneNode(true)),
1691
+ texts: [],
1692
+ attrs: [],
1693
+ ons: [],
1694
+ bindIds: new Set(),
1695
+ };
1696
+ blueprintOk = true;
1697
+ hasRowKernel = true;
1698
+ rowKeyField = typeof spec.rowKernel.keyField === 'string' && spec.rowKernel.keyField ? spec.rowKernel.keyField : null;
1699
+ rowActArgField =
1700
+ typeof spec.rowKernel.actArgField === 'string' && spec.rowKernel.actArgField ? spec.rowKernel.actArgField : null;
1701
+ blueprintBindIds = new Set(['__vmzRk']);
1702
+ for (const ev of spec.rowKernel.events || [])
1703
+ needDelegate(ev);
1704
+ for (const hf of spec.rowKernel.hostFields || []) {
1705
+ if (typeof hf === 'string' && hf)
1706
+ ensureHostDispatcher(hf);
1707
+ }
1708
+ // Leaf path writes (`rows.0.label`) need `rows.*.label`, not bare `rows.*`.
1709
+ {
1710
+ const listRoot = depRootField((deps && deps[0]) || '') || (deps && deps[0]) || '';
1711
+ if (listRoot) {
1712
+ /** @type {string[]} */
1713
+ const leafDeps = [`${listRoot}.*`];
1714
+ const fields = Array.isArray(spec.rowKernel.itemFields) ? spec.rowKernel.itemFields : [];
1715
+ for (const f of fields) {
1716
+ if (typeof f === 'string' && f)
1717
+ leafDeps.push(`${listRoot}.*.${f}`);
1718
+ }
1719
+ ensureListDispatcher('__vmzRk', leafDeps);
1720
+ }
1721
+ }
1722
+ const rkHydrate = spec.rowKernel.hydrate;
1723
+ const rkApply = spec.rowKernel.apply;
1724
+ hydrateBp = (root, entry) => {
1725
+ const item = entry && typeof entry === 'object' && 'item' in entry && entry.item != null
1726
+ ? entry.item
1727
+ : entry && entry.__vmzBox != null
1728
+ ? entry.__vmzBox
1729
+ : entry;
1730
+ rkHydrate.call(inst, root, item);
1731
+ };
1732
+ applyBp = (entry) => {
1733
+ const root = entry && entry.nodeType === 1 ? entry : entry.dom;
1734
+ const item = entry && entry.nodeType === 1 ? entry.__vmzBox : entry.item;
1735
+ if (typeof rkApply === 'function')
1736
+ rkApply.call(inst, root, item);
1737
+ };
1738
+ }
1739
+ }
1740
+ catch (err) {
1741
+ console.error('vmz:dom rowKernel', err);
1742
+ blueprint = null;
1743
+ blueprintOk = true;
1744
+ hasRowKernel = false;
1745
+ rowKeyField = null;
1746
+ rowActArgField = null;
1747
+ hydrateBp = null;
1748
+ applyBp = null;
1749
+ }
1750
+ }
1751
+ const probeItemField = (get, box) => {
1752
+ const item = box.item;
1753
+ if (!item || (typeof item !== 'object' && typeof item !== 'function'))
1754
+ return null;
1755
+ let field = null;
1756
+ const proxy = new Proxy(item, {
1757
+ get(t, p, r) {
1758
+ if (typeof p === 'string' || typeof p === 'symbol')
1759
+ field = String(p);
1760
+ return Reflect.get(t, p, r);
1761
+ },
1762
+ });
1763
+ const prev = box.item;
1764
+ box.item = proxy;
1765
+ try {
1766
+ get.call(inst);
1767
+ }
1768
+ catch {
1769
+ /* ignore */
1770
+ }
1771
+ box.item = prev;
1772
+ return field;
1773
+ };
1774
+ /**
1775
+ * Probe on/off class strings for `this.<host> === item.<itemField> ? … : …`.
1776
+ * Host/item field names come from binding deps — not hardcoded.
1777
+ */
1778
+ const probeHostItemClass = (get, box, hostField, itemField) => {
1779
+ if (!hostField || !itemField)
1780
+ return { onVal: '', offVal: '' };
1781
+ const prev = inst[hostField];
1782
+ const matchVal = box.item != null ? box.item[itemField] : undefined;
1783
+ let onVal = '';
1784
+ let offVal = '';
1785
+ const quiet = !!inst.__vmzQuiet;
1786
+ inst.__vmzQuiet = true;
1787
+ try {
1788
+ inst[hostField] = matchVal;
1789
+ onVal = String(get.call(inst) ?? '');
1790
+ // Distinct off value for number / other keys.
1791
+ if (typeof matchVal === 'number') {
1792
+ inst[hostField] = matchVal === 0 ? -1 : 0;
1793
+ if (inst[hostField] === matchVal)
1794
+ inst[hostField] = undefined;
1795
+ }
1796
+ else {
1797
+ inst[hostField] = matchVal === '' ? '__vmz_off__' : '';
1798
+ if (inst[hostField] === matchVal)
1799
+ inst[hostField] = undefined;
1800
+ }
1801
+ offVal = String(get.call(inst) ?? '');
1802
+ }
1803
+ catch {
1804
+ onVal = '';
1805
+ offVal = '';
1806
+ }
1807
+ finally {
1808
+ inst[hostField] = prev;
1809
+ inst.__vmzQuiet = quiet;
1810
+ }
1811
+ return { onVal, offVal };
1812
+ };
1813
+ const sealBlueprintDispatchers = () => {
1814
+ if (!blueprint || blueprintBindIds)
1815
+ return;
1816
+ /** @type {Set<string>} */
1817
+ const ids = new Set();
1818
+ for (const s of blueprint.texts) {
1819
+ if (s.bindingId != null) {
1820
+ ids.add(String(s.bindingId));
1821
+ ensureListDispatcher(s.bindingId, s.deps);
1822
+ }
1823
+ }
1824
+ for (const s of blueprint.attrs) {
1825
+ if (s.bindingId != null) {
1826
+ ids.add(String(s.bindingId));
1827
+ ensureListDispatcher(s.bindingId, s.deps);
1828
+ }
1829
+ for (const d of s.deps || []) {
1830
+ if (!d || d.includes('.*') || (d.includes('[') && d.includes(']')))
1831
+ continue;
1832
+ const rootField = depRootField(d) || d;
1833
+ if (rootField && rootField.indexOf('.') < 0)
1834
+ ensureHostDispatcher(rootField);
1835
+ }
1836
+ }
1837
+ for (const s of blueprint.ons) {
1838
+ needDelegate(s.type);
1839
+ }
1840
+ blueprintBindIds = ids;
1841
+ blueprint.bindIds = ids;
1842
+ compileBlueprintKernels();
1843
+ };
1844
+ const compileBlueprintKernels = () => {
1845
+ if (!blueprint || hydrateBp)
1846
+ return;
1847
+ const textSlots = blueprint.texts;
1848
+ const attrSlots = blueprint.attrs;
1849
+ const onSlots = blueprint.ons;
1850
+ const nText = textSlots.length;
1851
+ const nAttr = attrSlots.length;
1852
+ const nOn = onSlots.length;
1853
+ // Fallback only (no compile-time rowKernel). Field/path walks come from
1854
+ // recorded slots — shape-specific kernels belong in row_kernel.rs.
1855
+ hydrateBp = (root, entry) => {
1856
+ const item = entry && entry.item != null ? entry.item : entry;
1857
+ root.__vmzBox = item;
1858
+ /** @type {Array<Text>} */
1859
+ const textNodes = new Array(nText);
1860
+ /** @type {Array<Element>} */
1861
+ const attrEls = new Array(nAttr);
1862
+ for (let i = 0; i < nText; i++)
1863
+ textNodes[i] = /** @type {Text} */ (textSlots[i].get(root));
1864
+ for (let i = 0; i < nAttr; i++)
1865
+ attrEls[i] = attrSlots[i].get(root);
1866
+ for (let i = 0; i < nOn; i++) {
1867
+ const el = onSlots[i].get(root);
1868
+ if (!el.getAttribute('data-vmz-act')) {
1869
+ el.setAttribute('data-vmz-act', onSlots[i].method);
1870
+ el.__vmzAct = onSlots[i].method;
1871
+ }
1872
+ }
1873
+ for (let i = 0; i < nText; i++) {
1874
+ const v = item == null ? '' : item[textSlots[i].field];
1875
+ textNodes[i].nodeValue = v == null ? '' : v + '';
1876
+ }
1877
+ for (let i = 0; i < nAttr; i++) {
1878
+ const s = attrSlots[i];
1879
+ const el = attrEls[i];
1880
+ const host = s.hostField;
1881
+ const itemKey = s.itemField;
1882
+ if (!host || !itemKey)
1883
+ continue;
1884
+ const hv = inst[host];
1885
+ if (hv != null && item && hv === item[itemKey]) {
1886
+ if (s.name === 'class' || s.name === 'className')
1887
+ el.className = s.onVal;
1888
+ else
1889
+ applyDomAttr(el, s.name, s.onVal);
1890
+ }
1891
+ else if (s.offVal) {
1892
+ if (s.name === 'class' || s.name === 'className')
1893
+ el.className = s.offVal;
1894
+ else
1895
+ applyDomAttr(el, s.name, s.offVal);
1896
+ }
1897
+ }
1898
+ entry.tn = textNodes;
1899
+ entry.ae = attrEls;
1900
+ entry.dom = root;
1901
+ entry.bp = true;
1902
+ };
1903
+ applyBp = (entry) => {
1904
+ const item = entry.item != null ? entry.item : entry.__vmzBox;
1905
+ const textNodes = entry.tn;
1906
+ const attrEls = entry.ae;
1907
+ for (let i = 0; i < nText; i++) {
1908
+ const v = item == null ? '' : item[textSlots[i].field];
1909
+ textNodes[i].nodeValue = v == null ? '' : v + '';
1910
+ }
1911
+ for (let i = 0; i < nAttr; i++) {
1912
+ const s = attrSlots[i];
1913
+ const el = attrEls[i];
1914
+ if (!s.hostField || !s.itemField)
1915
+ continue;
1916
+ const raw = item && inst[s.hostField] === item[s.itemField] ? s.onVal : s.offVal;
1917
+ if (s.name === 'class' || s.name === 'className')
1918
+ el.className = raw;
1919
+ else
1920
+ applyDomAttr(el, s.name, raw);
1921
+ }
1922
+ };
1923
+ };
1924
+ const wireBlueprintItem = (root, box, patches) => {
1925
+ if (!blueprint)
1926
+ return null;
1927
+ sealBlueprintDispatchers();
1928
+ const entry = {
1929
+ item: box.item,
1930
+ index: box.index,
1931
+ dom: root,
1932
+ bp: true,
1933
+ t0: null,
1934
+ t1: null,
1935
+ a0: null,
1936
+ tn: null,
1937
+ ae: null,
1938
+ patches: patches || null,
1939
+ };
1940
+ hydrateBp(root, entry);
1941
+ if (patches) {
1942
+ const applyAll = () => applyBp(entry);
1943
+ applyAll.__vmzBindingIds = blueprintBindIds;
1944
+ applyAll.__vmzBindingId = null;
1945
+ applyAll.__vmzItemLocal = true;
1946
+ applyAll.__vmzBpEntry = entry;
1947
+ patches.push(applyAll);
1948
+ }
1949
+ return entry;
1950
+ };
1951
+ const recordFirstItem = (api, box, patches) => {
1952
+ /** @type {Element | null} */
1953
+ let root = null;
1954
+ /** Pending slots keep live node refs — Direct emit binds before appendChild. */
1955
+ /** @type {{ texts: any[], attrs: any[], ons: any[] }} */
1956
+ const pending = { texts: [], attrs: [], ons: [] };
1957
+ let recordFailed = false;
1958
+ const recordingApi = Object.assign({}, api, {
1959
+ el(tag) {
1960
+ const el = api.el(tag);
1961
+ if (!root)
1962
+ root = el;
1963
+ return el;
1964
+ },
1965
+ // Capture only — do not wireDirectBind (would orphan first-row binders).
1966
+ bindText(i, bindingId, deps, get, textNode, cf) {
1967
+ if (!root)
1968
+ return;
1969
+ // Blueprint recording aborted: fall back to normal wiring for remaining binds.
1970
+ if (recordFailed) {
1971
+ api.bindText(i, bindingId, deps, get, textNode, cf);
1972
+ return;
1973
+ }
1974
+ try {
1975
+ const raw = get.call(inst);
1976
+ if (textNode.nodeType === 3) /** @type {Text} */
1977
+ (textNode).nodeValue = String(raw ?? '');
1978
+ else
1979
+ textNode.textContent = String(raw ?? '');
1980
+ }
1981
+ catch {
1982
+ /* ignore */
1983
+ }
1984
+ pending.texts.push({
1985
+ node: textNode,
1986
+ bindingId,
1987
+ deps: Array.isArray(deps) ? deps.slice() : [],
1988
+ getFn: get,
1989
+ });
1990
+ },
1991
+ bindAttr(i, bindingId, deps, get, el, name, cf) {
1992
+ if (!root)
1993
+ return;
1994
+ if (recordFailed) {
1995
+ api.bindAttr(i, bindingId, deps, get, el, name, cf);
1996
+ return;
1997
+ }
1998
+ // Class bind eligible when deps include a bare host field (any name).
1999
+ const hasHostDep = (deps || []).some((d) => d && !d.includes('.*') && !d.includes('[') && String(d).indexOf('.') < 0);
2000
+ if ((name === 'class' || name === 'className') && hasHostDep) {
2001
+ try {
2002
+ const raw = get.call(inst);
2003
+ el.className = raw == null ? '' : String(raw);
2004
+ }
2005
+ catch {
2006
+ /* ignore */
2007
+ }
2008
+ pending.attrs.push({
2009
+ node: el,
2010
+ bindingId,
2011
+ deps: Array.isArray(deps) ? deps.slice() : [],
2012
+ name,
2013
+ getFn: get,
2014
+ });
2015
+ }
2016
+ else {
2017
+ recordFailed = true;
2018
+ api.bindAttr(i, bindingId, deps, get, el, name, cf);
2019
+ }
2020
+ },
2021
+ on(el, type, handler) {
2022
+ if (recordFailed) {
2023
+ api.on(el, type, handler);
2024
+ return;
2025
+ }
2026
+ const parsed = parseActionMethod(handler);
2027
+ if (parsed && root) {
2028
+ pending.ons.push({
2029
+ node: el,
2030
+ type,
2031
+ method: parsed.method,
2032
+ argField: parsed.argField,
2033
+ });
2034
+ actionArgFields[parsed.method] = parsed.argField;
2035
+ if (rowActArgField == null)
2036
+ rowActArgField = parsed.argField;
2037
+ root.__vmzBox = box;
2038
+ el.__vmzAct = parsed.method;
2039
+ // Attribute survives cloneNode — hydrate skips per-row __vmzAct writes.
2040
+ el.setAttribute('data-vmz-act', parsed.method);
2041
+ needDelegate(type);
2042
+ return;
2043
+ }
2044
+ api.on(el, type, handler);
2045
+ recordFailed = true;
2046
+ },
2047
+ });
2048
+ const dom = userCreateItem.call(inst, recordingApi, box);
2049
+ if (!recordFailed && dom && dom.nodeType === 1 && root === dom && (pending.texts.length > 0 || pending.attrs.length > 0)) {
2050
+ /** @type {any[]} */
2051
+ const texts = [];
2052
+ /** @type {any[]} */
2053
+ const attrs = [];
2054
+ /** @type {any[]} */
2055
+ const ons = [];
2056
+ for (const p of pending.texts) {
2057
+ const path = pathFromRoot(root, p.node);
2058
+ const field = probeItemField(p.getFn, box);
2059
+ if (!path || !field) {
2060
+ recordFailed = true;
2061
+ break;
2062
+ }
2063
+ texts.push({
2064
+ path,
2065
+ bindingId: p.bindingId,
2066
+ deps: p.deps,
2067
+ field,
2068
+ get: makeChildGetter(path),
2069
+ });
2070
+ }
2071
+ if (!recordFailed) {
2072
+ for (const p of pending.attrs) {
2073
+ const path = pathFromRoot(root, p.node);
2074
+ if (!path) {
2075
+ recordFailed = true;
2076
+ break;
2077
+ }
2078
+ const hostField = (() => {
2079
+ for (const d of p.deps || []) {
2080
+ if (!d)
2081
+ continue;
2082
+ if (d.includes('.*') || d.includes('['))
2083
+ continue;
2084
+ if (String(d).indexOf('.') < 0)
2085
+ return String(d);
2086
+ }
2087
+ return null;
2088
+ })();
2089
+ let itemField = null;
2090
+ for (const d of p.deps || []) {
2091
+ if (!d)
2092
+ continue;
2093
+ if (d.includes('.*')) {
2094
+ const m = String(d).match(/\*\.([A-Za-z_$][\w$]*)$/);
2095
+ if (m)
2096
+ itemField = m[1];
2097
+ }
2098
+ }
2099
+ if (!hostField || !itemField) {
2100
+ recordFailed = true;
2101
+ break;
2102
+ }
2103
+ const { onVal, offVal } = probeHostItemClass(p.getFn, box, hostField, itemField);
2104
+ attrs.push({
2105
+ path,
2106
+ bindingId: p.bindingId,
2107
+ deps: p.deps,
2108
+ name: p.name,
2109
+ onVal,
2110
+ offVal,
2111
+ hostField,
2112
+ itemField,
2113
+ get: makeChildGetter(path),
2114
+ });
2115
+ }
2116
+ }
2117
+ if (!recordFailed) {
2118
+ for (const p of pending.ons) {
2119
+ const path = pathFromRoot(root, p.node);
2120
+ if (!path) {
2121
+ recordFailed = true;
2122
+ break;
2123
+ }
2124
+ ons.push({
2125
+ path,
2126
+ type: p.type,
2127
+ method: p.method,
2128
+ get: makeChildGetter(path),
2129
+ });
2130
+ }
2131
+ }
2132
+ if (!recordFailed) {
2133
+ const tpl = /** @type {Element} */ (dom.cloneNode(true));
2134
+ clearDomEvt(tpl);
2135
+ blueprint = {
2136
+ tpl,
2137
+ texts,
2138
+ attrs,
2139
+ ons,
2140
+ bindIds: new Set(),
2141
+ };
2142
+ }
2143
+ }
2144
+ if (!blueprint)
2145
+ blueprintOk = false;
2146
+ return dom;
2147
+ };
2148
+ const createItem = (api, box, patches) => {
2149
+ if (blueprint && blueprintOk) {
2150
+ const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
2151
+ wireBlueprintItem(root, box, patches);
2152
+ return root;
2153
+ }
2154
+ if (blueprintOk && typeof userCreateItem === 'function') {
2155
+ const dom = recordFirstItem(api, box, patches);
2156
+ if (blueprint && dom && dom.nodeType === 1) {
2157
+ patches.length = 0;
2158
+ wireBlueprintItem(dom, box, patches);
2159
+ }
2160
+ return dom;
2161
+ }
2162
+ return userCreateItem.call(inst, api, box);
2163
+ };
2164
+ /**
2165
+ * Reorder / place item DOM with minimal mutations.
2166
+ * - already-correct → no-op
2167
+ * - pure 2-node swap → 1–2 insertBefore (common keyed list swap)
2168
+ * - append prefix → Fragment insert only new tail
2169
+ * - create / replace / complex → Fragment rebuild before `end`
2170
+ */
1192
2171
  const reconcileDomOrder = (nextNodes) => {
1193
2172
  const parent = end.parentNode;
1194
2173
  if (!parent)
1195
2174
  return;
1196
- let ok = true;
1197
- let n = start.nextSibling;
1198
- for (let i = 0; i < nextNodes.length; i++) {
1199
- if (n !== nextNodes[i]) {
1200
- ok = false;
1201
- break;
2175
+ /** @type {ChildNode[]} */
2176
+ const curr = [];
2177
+ for (let n = start.nextSibling; n && n !== end; n = n.nextSibling) {
2178
+ curr.push(n);
2179
+ }
2180
+ if (curr.length === nextNodes.length) {
2181
+ let same = true;
2182
+ for (let i = 0; i < curr.length; i++) {
2183
+ if (curr[i] !== nextNodes[i]) {
2184
+ same = false;
2185
+ break;
2186
+ }
2187
+ }
2188
+ if (same)
2189
+ return;
2190
+ // Fast path: exactly two positions swapped (benchmark swaprows).
2191
+ /** @type {number[]} */
2192
+ const diff = [];
2193
+ for (let i = 0; i < curr.length; i++) {
2194
+ if (curr[i] !== nextNodes[i])
2195
+ diff.push(i);
2196
+ }
2197
+ if (diff.length === 2 && curr[diff[0]] === nextNodes[diff[1]] && curr[diff[1]] === nextNodes[diff[0]]) {
2198
+ const a = curr[diff[0]];
2199
+ const b = curr[diff[1]];
2200
+ const aNext = a.nextSibling;
2201
+ const bNext = b.nextSibling;
2202
+ noteDomMove();
2203
+ noteDomMove();
2204
+ if (aNext === b) {
2205
+ parent.insertBefore(b, a);
2206
+ }
2207
+ else if (bNext === a) {
2208
+ parent.insertBefore(a, b);
2209
+ }
2210
+ else {
2211
+ parent.insertBefore(b, aNext);
2212
+ parent.insertBefore(a, bNext);
2213
+ }
2214
+ return;
1202
2215
  }
1203
- n = n.nextSibling;
1204
2216
  }
1205
- if (ok && n === end)
1206
- return;
2217
+ // Append-only: existing live prefix unchanged, only new tail detached.
2218
+ if (curr.length < nextNodes.length && curr.length > 0) {
2219
+ let prefix = true;
2220
+ for (let i = 0; i < curr.length; i++) {
2221
+ if (curr[i] !== nextNodes[i]) {
2222
+ prefix = false;
2223
+ break;
2224
+ }
2225
+ }
2226
+ if (prefix) {
2227
+ const batch = document.createDocumentFragment();
2228
+ for (let i = curr.length; i < nextNodes.length; i++) {
2229
+ batch.appendChild(nextNodes[i]);
2230
+ }
2231
+ parent.insertBefore(batch, end);
2232
+ return;
2233
+ }
2234
+ }
2235
+ // Create / replace / complex reorder: one Fragment write.
1207
2236
  const batch = document.createDocumentFragment();
1208
2237
  for (const dom of nextNodes) {
1209
2238
  if (dom.parentNode)
@@ -1217,26 +2246,40 @@ const directApi = {
1217
2246
  return;
1218
2247
  const applied = ++gen;
1219
2248
  const list = readList();
1220
- const seen = new Set();
1221
- const nextNodes = [];
1222
- for (let i = 0; i < list.length; i++) {
1223
- if (applied !== gen || inst.__vmzDestroyed)
1224
- return;
1225
- const box = { item: list[i], index: i };
1226
- const k = itemKey(box);
1227
- if (seen.has(k)) {
2249
+ const n = list.length;
2250
+ // Clear all rows.
2251
+ if (n === 0) {
2252
+ if (keyed.size)
2253
+ fastWipeRows();
2254
+ return;
2255
+ }
2256
+ // Full replace (no key reuse): wipe then fall into fresh create.
2257
+ if (keyed.size > 0 && hasRowKernel) {
2258
+ let reuse = false;
2259
+ for (let i = 0; i < n; i++) {
2260
+ if (keyed.has(rowKeyOf(list[i], i))) {
2261
+ reuse = true;
2262
+ break;
2263
+ }
1228
2264
  }
1229
- seen.add(k);
1230
- let entry = keyed.get(k);
1231
- if (!entry) {
1232
- const patches = [];
2265
+ if (!reuse)
2266
+ fastWipeRows();
2267
+ }
2268
+ // Fresh create into empty each: record blueprint once, then clone-only.
2269
+ if (keyed.size === 0 && n > 0) {
2270
+ const parent = end.parentNode;
2271
+ const batch = document.createDocumentFragment();
2272
+ let startIdx = 0;
2273
+ if (!blueprint || !blueprintOk) {
2274
+ const box0 = { item: list[0], index: 0 };
2275
+ const patches0 = [];
1233
2276
  const prevPatches = directApi._itemPatches;
1234
2277
  const prevCtx = directApi._eachCtx;
1235
- directApi._itemPatches = patches;
2278
+ directApi._itemPatches = patches0;
1236
2279
  directApi._eachCtx = eachCtx;
1237
- let dom = null;
2280
+ let dom0 = null;
1238
2281
  try {
1239
- dom = spec.createItem.call(inst, directApi, box);
2282
+ dom0 = createItem(directApi, box0, patches0);
1240
2283
  }
1241
2284
  finally {
1242
2285
  directApi._itemPatches = prevPatches;
@@ -1244,24 +2287,235 @@ const directApi = {
1244
2287
  }
1245
2288
  if (applied !== gen || inst.__vmzDestroyed)
1246
2289
  return;
1247
- tagItemPatches(patches, i);
1248
- if (dom) {
1249
- if (dom.nodeType === 1) {
1250
- dom.setAttribute('data-vmz-key', String(k));
2290
+ if (!dom0)
2291
+ return;
2292
+ const k0 = itemKey(box0);
2293
+ if (dom0.nodeType === 1) /** @type {Element} */
2294
+ (dom0).__vmzKey = k0;
2295
+ // First row may already be blueprint-wired (patches cleared + hydrate).
2296
+ let entry0 = keyed.get(k0);
2297
+ if (!entry0) {
2298
+ if (patches0.length && patches0[0] && patches0[0].__vmzBpEntry) {
2299
+ entry0 = patches0[0].__vmzBpEntry;
2300
+ entry0.patches = patches0;
2301
+ }
2302
+ else if (blueprint && blueprintOk) {
2303
+ entry0 = wireBlueprintItem(/** @type {Element} */ (dom0), box0, patches0);
2304
+ }
2305
+ else {
2306
+ tagItemPatches(patches0, 0);
2307
+ entry0 = { box: box0, dom: dom0, patches: patches0 };
2308
+ }
2309
+ keyed.set(k0, entry0);
2310
+ }
2311
+ batch.appendChild(dom0);
2312
+ startIdx = 1;
2313
+ }
2314
+ if (blueprint && blueprintOk) {
2315
+ sealBlueprintDispatchers();
2316
+ const tpl = blueprint.tpl;
2317
+ if (hasRowKernel && spec.rowKernel && typeof spec.rowKernel.create === 'function') {
2318
+ // Shape-specific create loop is Rust-emitted (rowKernel.create).
2319
+ // Direct parent.insertBefore (no Fragment). When parent has only the
2320
+ // each markers as children, detach parent for the fill then reattach —
2321
+ // same structural trick as hand-tuned keyed apps (not app-specific).
2322
+ if (parent) {
2323
+ let detached = null;
2324
+ let reinsertAt = null;
2325
+ if (parent.nodeType === 1 && parent.parentNode) {
2326
+ let onlyMarkers = true;
2327
+ for (let c = parent.firstChild; c; c = c.nextSibling) {
2328
+ if (c !== start && c !== end) {
2329
+ onlyMarkers = false;
2330
+ break;
2331
+ }
2332
+ }
2333
+ if (onlyMarkers) {
2334
+ detached = parent.parentNode;
2335
+ reinsertAt = parent.nextSibling;
2336
+ detached.removeChild(parent);
2337
+ }
2338
+ }
2339
+ spec.rowKernel.create.call(inst, list, startIdx, tpl, keyed, parent, end, rowKeyOf);
2340
+ if (detached)
2341
+ detached.insertBefore(parent, reinsertAt);
2342
+ }
2343
+ else {
2344
+ const hydrate = spec.rowKernel.hydrate;
2345
+ for (let i = startIdx; i < n; i++) {
2346
+ if (applied !== gen || inst.__vmzDestroyed)
2347
+ return;
2348
+ const item = list[i];
2349
+ const root = /** @type {Element} */ (tpl.cloneNode(true));
2350
+ hydrate.call(inst, root, item);
2351
+ const k = rowKeyOf(item, i);
2352
+ root.__vmzKey = k;
2353
+ keyed.set(k, root);
2354
+ batch.appendChild(root);
2355
+ }
2356
+ }
2357
+ }
2358
+ else if (hasRowKernel && hydrateBp) {
2359
+ const hydrate = spec.rowKernel.hydrate;
2360
+ for (let i = startIdx; i < n; i++) {
2361
+ if (applied !== gen || inst.__vmzDestroyed)
2362
+ return;
2363
+ const item = list[i];
2364
+ const root = /** @type {Element} */ (tpl.cloneNode(true));
2365
+ hydrate.call(inst, root, item);
2366
+ const k = rowKeyOf(item, i);
2367
+ root.__vmzKey = k;
2368
+ keyed.set(k, root);
2369
+ batch.appendChild(root);
2370
+ }
2371
+ }
2372
+ else {
2373
+ for (let i = startIdx; i < n; i++) {
2374
+ if (applied !== gen || inst.__vmzDestroyed)
2375
+ return;
2376
+ const item = list[i];
2377
+ const k = keyOf(item, i);
2378
+ const root = /** @type {Element} */ (tpl.cloneNode(true));
2379
+ const entry = {
2380
+ item,
2381
+ index: i,
2382
+ dom: root,
2383
+ bp: true,
2384
+ t0: null,
2385
+ t1: null,
2386
+ a0: null,
2387
+ patches: null,
2388
+ };
2389
+ hydrateBp(root, entry);
2390
+ root.__vmzKey = k;
2391
+ keyed.set(k, entry);
2392
+ batch.appendChild(root);
1251
2393
  }
1252
- entry = { box, dom, patches };
1253
- keyed.set(k, entry);
1254
2394
  }
1255
2395
  }
1256
2396
  else {
1257
- entry.box.item = list[i];
1258
- entry.box.index = i;
1259
- tagItemPatches(entry.patches, i);
1260
- for (const p of entry.patches)
1261
- runPatch(p, null);
2397
+ for (let i = startIdx; i < n; i++) {
2398
+ if (applied !== gen || inst.__vmzDestroyed)
2399
+ return;
2400
+ const box = { item: list[i], index: i };
2401
+ const k = itemKey(box);
2402
+ const patches = [];
2403
+ const prevPatches = directApi._itemPatches;
2404
+ const prevCtx = directApi._eachCtx;
2405
+ directApi._itemPatches = patches;
2406
+ directApi._eachCtx = eachCtx;
2407
+ let dom = null;
2408
+ try {
2409
+ dom = createItem(directApi, box, patches);
2410
+ }
2411
+ finally {
2412
+ directApi._itemPatches = prevPatches;
2413
+ directApi._eachCtx = prevCtx;
2414
+ }
2415
+ if (applied !== gen || inst.__vmzDestroyed)
2416
+ return;
2417
+ if (!dom)
2418
+ continue;
2419
+ tagItemPatches(patches, i);
2420
+ if (dom.nodeType === 1) /** @type {Element} */
2421
+ (dom).__vmzKey = k;
2422
+ keyed.set(k, { box, dom, patches });
2423
+ batch.appendChild(dom);
2424
+ }
2425
+ }
2426
+ if (applied !== gen || inst.__vmzDestroyed)
2427
+ return;
2428
+ if (parent && batch.firstChild)
2429
+ parent.insertBefore(batch, end);
2430
+ if (end.isConnected)
2431
+ ensureDelegateAttached();
2432
+ else
2433
+ queueMicrotask(() => {
2434
+ if (!inst.__vmzDestroyed)
2435
+ ensureDelegateAttached();
2436
+ });
2437
+ return;
2438
+ }
2439
+ const seen = new Set();
2440
+ /** @type {Node[]} */
2441
+ const nextNodes = [];
2442
+ for (let i = 0; i < n; i++) {
2443
+ if (applied !== gen || inst.__vmzDestroyed)
2444
+ return;
2445
+ const item = list[i];
2446
+ const k = rowKeyOf(item, i);
2447
+ seen.add(k);
2448
+ let entry = keyed.get(k);
2449
+ if (!entry) {
2450
+ if (hasRowKernel && blueprint && blueprintOk && hydrateBp) {
2451
+ const root = /** @type {Element} */ (blueprint.tpl.cloneNode(true));
2452
+ spec.rowKernel.hydrate.call(inst, root, item);
2453
+ root.__vmzKey = k;
2454
+ keyed.set(k, root);
2455
+ entry = root;
2456
+ }
2457
+ else {
2458
+ const box = { item, index: i };
2459
+ const patches = [];
2460
+ const prevPatches = directApi._itemPatches;
2461
+ const prevCtx = directApi._eachCtx;
2462
+ directApi._itemPatches = patches;
2463
+ directApi._eachCtx = eachCtx;
2464
+ let dom = null;
2465
+ try {
2466
+ dom = createItem(directApi, box, patches);
2467
+ }
2468
+ finally {
2469
+ directApi._itemPatches = prevPatches;
2470
+ directApi._eachCtx = prevCtx;
2471
+ }
2472
+ if (applied !== gen || inst.__vmzDestroyed)
2473
+ return;
2474
+ tagItemPatches(patches, i);
2475
+ if (dom) {
2476
+ if (dom.nodeType === 1) {
2477
+ // Client identity: expando only (see 01 each identity). SSR uses data-vmz-key.
2478
+ /** @type {Element} */ (dom).__vmzKey = k;
2479
+ }
2480
+ entry = { box, dom, patches };
2481
+ keyed.set(k, entry);
2482
+ }
2483
+ }
2484
+ }
2485
+ else {
2486
+ const sameItem = entryItem(entry) === item;
2487
+ if (entry.nodeType === 1) {
2488
+ entry.__vmzBox = item;
2489
+ }
2490
+ else if (entry.bp) {
2491
+ entry.item = item;
2492
+ entry.index = i;
2493
+ if (entry.dom)
2494
+ entry.dom.__vmzBox = entry.item;
2495
+ }
2496
+ else {
2497
+ entry.box.item = item;
2498
+ entry.box.index = i;
2499
+ tagItemPatches(entry.patches, i);
2500
+ }
2501
+ // Pure reorder (swap / move) keeps object identity — skip leaf patches.
2502
+ if (!sameItem) {
2503
+ if (entryIsBp(entry) && applyBp) {
2504
+ try {
2505
+ applyBp(entry);
2506
+ }
2507
+ catch (err) {
2508
+ console.error('vmz:dom each item', err);
2509
+ }
2510
+ }
2511
+ else if (entry.patches) {
2512
+ for (const p of entry.patches)
2513
+ runPatch(p, null);
2514
+ }
2515
+ }
1262
2516
  }
1263
2517
  if (entry)
1264
- nextNodes.push(entry.dom);
2518
+ nextNodes.push(entryDom(entry));
1265
2519
  }
1266
2520
  if (applied !== gen || inst.__vmzDestroyed)
1267
2521
  return;
@@ -1269,11 +2523,19 @@ const directApi = {
1269
2523
  if (seen.has(k))
1270
2524
  continue;
1271
2525
  noteDomRemove();
1272
- clearDomEvt(entry.dom);
1273
- disposeDomTree(entry.dom);
1274
- if (entry.dom && entry.dom.parentNode)
1275
- entry.dom.remove();
2526
+ const dom = entryDom(entry);
2527
+ if (hasRowKernel) {
2528
+ if (dom && dom.parentNode)
2529
+ dom.remove();
2530
+ }
2531
+ else {
2532
+ clearDomEvt(dom);
2533
+ disposeDomTree(dom);
2534
+ if (dom && dom.parentNode)
2535
+ dom.remove();
2536
+ }
1276
2537
  keyed.delete(k);
2538
+ releaseBpEntry(entry);
1277
2539
  }
1278
2540
  reconcileDomOrder(nextNodes);
1279
2541
  // First apply may run while start/end still sit in a DocumentFragment
@@ -1291,27 +2553,41 @@ const directApi = {
1291
2553
  directApi._itemPatches.push(apply);
1292
2554
  start.__vmzDispose = () => {
1293
2555
  teardownDelegate();
1294
- for (const [, entry] of [...keyed.entries()]) {
1295
- clearDomEvt(entry.dom);
1296
- disposeDomTree(entry.dom);
1297
- if (entry.dom && entry.dom.parentNode)
1298
- entry.dom.remove();
1299
- }
1300
- keyed.clear();
2556
+ fastWipeRows();
1301
2557
  };
1302
2558
  const softDeps = [...new Set((deps || []).map((d) => `${depRootField(d)}.*`))];
1303
2559
  const softRefresh = () => {
1304
2560
  if (inst.__vmzDestroyed)
1305
2561
  return;
2562
+ const trie = inst.__vmzFlushTrie;
2563
+ const listRoot = depRootField((deps && deps[0]) || '') || '';
2564
+ // Full list replace is owned by apply(); soft channel is item/structure churn.
2565
+ if (trie && listRoot && trie[listRoot] && trie[listRoot].replace)
2566
+ return;
1306
2567
  const list = readList();
1307
- const softKey = softDeps[0] || `${depRootField((deps && deps[0]) || '')}.*`;
2568
+ const softKey = softDeps[0] || `${listRoot}.*`;
1308
2569
  for (let i = 0; i < list.length; i++) {
1309
- const box = { item: list[i], index: i };
1310
- const k = itemKey(box);
2570
+ const item = list[i];
2571
+ const k = rowKeyOf(item, i);
1311
2572
  const entry = keyed.get(k);
1312
2573
  if (!entry)
1313
2574
  continue;
1314
- entry.box.item = list[i];
2575
+ if (entryIsBp(entry)) {
2576
+ if (entry.nodeType === 1)
2577
+ entry.__vmzBox = item;
2578
+ else {
2579
+ entry.item = item;
2580
+ entry.index = i;
2581
+ if (entry.dom)
2582
+ entry.dom.__vmzBox = item;
2583
+ }
2584
+ if (applyBp)
2585
+ applyBp(entry);
2586
+ else if (hydrateBp)
2587
+ hydrateBp(entryDom(entry), entry);
2588
+ continue;
2589
+ }
2590
+ entry.box.item = item;
1315
2591
  entry.box.index = i;
1316
2592
  tagItemPatches(entry.patches, i);
1317
2593
  for (const p of entry.patches) {
@@ -1337,7 +2613,7 @@ const directApi = {
1337
2613
  /**
1338
2614
  * @param {object} inst
1339
2615
  * @param {string[]} deps
1340
- * @param { => any} fn
2616
+ * @param {() => any} fn
1341
2617
  * @param {number|string|null|undefined} bindingId
1342
2618
  */
1343
2619
  function trackDirectBind(inst, deps, fn, bindingId = null) {
@@ -1362,14 +2638,14 @@ function trackDirectBind(inst, deps, fn, bindingId = null) {
1362
2638
  * @param {object} inst
1363
2639
  * @param {number|string|null} bindingId
1364
2640
  * @param {string[]} deps
1365
- * @param { => any} get
2641
+ * @param {() => any} get
1366
2642
  * @param {(raw: any) => void} write
1367
2643
  * @param {{ stable: string[], branches: Array<{ cond?: => any, deps: string[] }> } | null | undefined} [cf]
1368
2644
  */
1369
2645
  function wireDirectBind(inst, bindingId, deps, get, write, cf) {
1370
2646
  let activeBranch = -1;
1371
2647
  /** @type {string[]} */
1372
- let liveDeps = Array.isArray(deps) ? [...deps] : [];
2648
+ let liveDeps = Array.isArray(deps) ? deps.slice() : [];
1373
2649
  const pickCf = () => {
1374
2650
  if (!cf || !Array.isArray(cf.branches))
1375
2651
  return -1;
@@ -1387,6 +2663,11 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
1387
2663
  }
1388
2664
  return cf.branches.length - 1;
1389
2665
  };
2666
+ // Item-local CF whose branches only gate the same stable deps: skip branch switching.
2667
+ let simpleCf = false;
2668
+ if (cf && Array.isArray(cf.branches) && directApi._itemPatches) {
2669
+ simpleCf = cf.branches.every((b) => !b.deps || b.deps.length === 0);
2670
+ }
1390
2671
  const apply = () => {
1391
2672
  if (precision.enabled) {
1392
2673
  precision.bindingEvals++;
@@ -1404,7 +2685,7 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
1404
2685
  raw = null;
1405
2686
  }
1406
2687
  write(raw);
1407
- if (!cf || !Array.isArray(cf.branches))
2688
+ if (!cf || !Array.isArray(cf.branches) || simpleCf || apply.__vmzItemLocal)
1408
2689
  return;
1409
2690
  const next = pickCf();
1410
2691
  if (next === activeBranch)
@@ -1413,21 +2694,19 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
1413
2694
  const branch = cf.branches[next];
1414
2695
  const nextDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
1415
2696
  const uniq = [...new Set(nextDeps)];
1416
- // Item-local binds must never enter the global binder table ( / jfb select).
1417
- if (apply.__vmzItemLocal) {
1418
- liveDeps = uniq;
1419
- return;
1420
- }
1421
2697
  unregisterBind(inst, liveDeps, apply, bindingId);
1422
2698
  liveDeps = uniq;
1423
2699
  registerBind(inst, liveDeps, apply, bindingId);
1424
2700
  };
1425
- if (cf && Array.isArray(cf.branches)) {
2701
+ if (cf && Array.isArray(cf.branches) && !simpleCf) {
1426
2702
  activeBranch = pickCf();
1427
2703
  const branch = cf.branches[activeBranch];
1428
2704
  liveDeps = [...(cf.stable || []), ...((branch && branch.deps) || [])];
1429
2705
  liveDeps = [...new Set(liveDeps)];
1430
2706
  }
2707
+ else if (cf && Array.isArray(cf.branches) && simpleCf) {
2708
+ liveDeps = Array.isArray(cf.stable) && cf.stable.length ? cf.stable : liveDeps;
2709
+ }
1431
2710
  // Mark before first apply so CF branch switches never hit global registerBind.
1432
2711
  if (directApi._itemPatches)
1433
2712
  apply.__vmzItemLocal = true;
@@ -1437,6 +2716,74 @@ function wireDirectBind(inst, bindingId, deps, get, write, cf) {
1437
2716
  function isEventPropName(name) {
1438
2717
  return typeof name === 'string' && /^on[A-Z]/.test(name);
1439
2718
  }
2719
+ /** Monotonic id for `bindComponentProp` BindingIds (per process). */
2720
+ let directPropBindSeq = 0;
2721
+ /** HTML boolean attributes: presence means true; `false`/`null` must remove the attr. */
2722
+ const BOOLEAN_HTML_ATTRS = new Set([
2723
+ 'disabled',
2724
+ 'checked',
2725
+ 'selected',
2726
+ 'readonly',
2727
+ 'required',
2728
+ 'multiple',
2729
+ 'hidden',
2730
+ 'autofocus',
2731
+ 'autoplay',
2732
+ 'controls',
2733
+ 'loop',
2734
+ 'muted',
2735
+ 'open',
2736
+ 'novalidate',
2737
+ 'formnovalidate',
2738
+ 'defer',
2739
+ 'async',
2740
+ 'ismap',
2741
+ 'default',
2742
+ 'inert',
2743
+ ]);
2744
+ /**
2745
+ * @param {Element} el
2746
+ * @param {string} name
2747
+ * @param {any} value
2748
+ */
2749
+ function applyDomAttr(el, name, value) {
2750
+ const key = name === 'className' ? 'class' : name;
2751
+ if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
2752
+ if (value === false || value == null || value === '') {
2753
+ el.removeAttribute(key);
2754
+ }
2755
+ else {
2756
+ el.setAttribute(key, value === true ? '' : String(value));
2757
+ }
2758
+ return;
2759
+ }
2760
+ if (value == null || value === false)
2761
+ el.removeAttribute(key);
2762
+ else
2763
+ el.setAttribute(key, value === true ? '' : String(value));
2764
+ }
2765
+ /**
2766
+ * Serialize-tree attr write (SSR).
2767
+ * @param {any} el
2768
+ * @param {string} name
2769
+ * @param {any} value
2770
+ */
2771
+ function applySerializeAttr(el, name, value) {
2772
+ if (!el || el.__kind !== 'el')
2773
+ return;
2774
+ const key = name === 'className' ? 'class' : name;
2775
+ if (BOOLEAN_HTML_ATTRS.has(String(key).toLowerCase())) {
2776
+ if (value === false || value == null || value === '')
2777
+ delete el.attrs[key];
2778
+ else
2779
+ el.attrs[key] = value === true ? '' : String(value);
2780
+ return;
2781
+ }
2782
+ if (value == null || value === false)
2783
+ delete el.attrs[key];
2784
+ else
2785
+ el.attrs[key] = value === true ? '' : String(value);
2786
+ }
1440
2787
  function stripFns(obj) {
1441
2788
  /** @type {Record<string, unknown>} */
1442
2789
  const out = {};
@@ -1663,18 +3010,15 @@ function runDirectResume(Component, inst, container) {
1663
3010
  return document.createTextNode(String(s ?? ''));
1664
3011
  },
1665
3012
  attr(el, name, value) {
1666
- if (name === 'className')
1667
- el.setAttribute('class', String(value ?? ''));
1668
- else if (value == null || value === false)
1669
- el.removeAttribute(name);
1670
- else
1671
- el.setAttribute(name, value === true ? '' : String(value));
3013
+ applyDomAttr(el, name, value);
1672
3014
  },
1673
3015
  on(el, type, handler) {
1674
3016
  el.addEventListener(type, handler);
1675
3017
  },
1676
3018
  bindText: directApi.bindText,
1677
3019
  bindAttr: directApi.bindAttr,
3020
+ bindComponentProp: directApi.bindComponentProp,
3021
+ projectDefaultSlot: directApi.projectDefaultSlot,
1678
3022
  setHtml: directApi.setHtml,
1679
3023
  bindHtml: directApi.bindHtml,
1680
3024
  ifBlock: directApi.ifBlock,
@@ -1838,7 +3182,7 @@ function eventEntryType(strategy) {
1838
3182
  }
1839
3183
  function scheduleClientOn(el, strategy, fn) {
1840
3184
  const run = () => {
1841
- Promise.resolve(fn).catch((err) => console.error('vmz:dom island', err));
3185
+ Promise.resolve(fn()).catch((err) => console.error('vmz:dom island', err));
1842
3186
  };
1843
3187
  if (isEventEntryStrategy(strategy)) {
1844
3188
  if (!el || typeof el.addEventListener !== 'function') {
@@ -2325,8 +3669,9 @@ function notifyOwners(owners, localSegs) {
2325
3669
  }
2326
3670
  /**
2327
3671
  * Field-owned write traps for plain objects / arrays on state fields.
2328
- * Plain objects: WriteBarrier via defineProperty (no Proxy).
2329
- * Arrays: transitional Proxy until keyed collection barriers land.
3672
+ * Plain objects: WriteBarrier via defineProperty (no Proxy).
3673
+ * Arrays: transitional Proxy tracks list identity/mutators only; elements stay plain
3674
+ * (no per-item wrap on large assign — nested notifies via `__vmzWritePath`).
2330
3675
  * Shared raw objects notify **all** current owners.
2331
3676
  *
2332
3677
  * @param {any} value
@@ -2408,6 +3753,12 @@ function installOwnedProp(obj, prop, entry) {
2408
3753
  },
2409
3754
  });
2410
3755
  }
3756
+ /**
3757
+ * Transitional array Proxy: track list identity / mutators only.
3758
+ * Elements stay plain — no per-item defineProperty on `this.rows = largeArray`
3759
+ * (design: WriteBarrier / list replace must not wrap 1k items). Nested field
3760
+ * notifies go through `__vmzWritePath` or whole-array replace.
3761
+ */
2411
3762
  function wrapArray(arr, report, pathSegs) {
2412
3763
  const existing = reactiveProxies.get(arr);
2413
3764
  if (existing) {
@@ -2421,88 +3772,32 @@ function wrapArray(arr, report, pathSegs) {
2421
3772
  kind: 'proxy',
2422
3773
  };
2423
3774
  addOwner(entry, report, pathSegs);
2424
- for (let i = 0; i < arr.length; i++) {
2425
- const item = arr[i];
2426
- for (const o of entry.owners) {
2427
- arr[i] = wrapReactive(item, o.report, [...o.baseSegs, String(i)]);
2428
- }
2429
- }
3775
+ const isArrayIndex = (prop) => typeof prop === 'string' && prop !== 'length' && String(Number(prop)) === prop;
2430
3776
  const proxy = new Proxy(arr, {
2431
3777
  get(target, prop, receiver) {
2432
3778
  if (typeof prop === 'string' && ARRAY_MUTATORS.has(prop)) {
2433
3779
  const fn = target[prop];
2434
3780
  return (...args) => {
2435
- const wrappedArgs = args.map((a, idx) => {
2436
- if (prop === 'splice' && idx >= 2) {
2437
- let w = a;
2438
- for (const o of entry.owners) {
2439
- w = wrapReactive(a, o.report, o.baseSegs.slice());
2440
- }
2441
- return w;
2442
- }
2443
- if ((prop === 'push' || prop === 'unshift') && typeof a === 'object') {
2444
- let w = a;
2445
- for (const o of entry.owners) {
2446
- w = wrapReactive(a, o.report, o.baseSegs.slice());
2447
- }
2448
- return w;
2449
- }
2450
- if (prop === 'fill') {
2451
- let w = a;
2452
- for (const o of entry.owners) {
2453
- w = wrapReactive(a, o.report, o.baseSegs.slice());
2454
- }
2455
- return w;
2456
- }
2457
- return a;
2458
- });
2459
- const ret = fn.apply(target, wrappedArgs);
3781
+ const ret = fn.apply(target, args);
2460
3782
  notifyOwners(entry.owners, null);
2461
3783
  return ret;
2462
3784
  };
2463
3785
  }
2464
- const v = Reflect.get(target, prop, receiver);
2465
- if (v && typeof v === 'object' && typeof prop === 'string') {
2466
- let nested = null;
2467
- for (const o of entry.owners) {
2468
- nested = wrapReactive(v, o.report, [...o.baseSegs, prop]);
2469
- }
2470
- return nested;
2471
- }
2472
- return v;
3786
+ // Indices / length / methods: return as-is (plain elements).
3787
+ return Reflect.get(target, prop, receiver);
2473
3788
  },
2474
3789
  set(target, prop, next, receiver) {
2475
- const isIndex = typeof prop === 'string' && prop !== 'length' && String(Number(prop)) === prop;
2476
- const allRoot = entry.owners.every((o) => o.baseSegs.length === 0);
2477
- if (isIndex && allRoot) {
2478
- let wrapped = next;
2479
- for (const o of entry.owners) {
2480
- wrapped = wrapReactive(next, o.report, [...o.baseSegs, prop]);
2481
- }
2482
- const prev = target[prop];
2483
- if (Object.is(prev, wrapped))
2484
- return true;
2485
- const ok = Reflect.set(target, prop, wrapped, receiver);
2486
- if (ok)
2487
- notifyOwners(entry.owners, null);
2488
- return ok;
2489
- }
2490
- const local = prop === 'length' || typeof prop !== 'string' ? [] : [prop];
2491
- let wrapped = next;
2492
- if (prop !== 'length') {
2493
- for (const o of entry.owners) {
2494
- wrapped = wrapReactive(next, o.report, [...o.baseSegs, ...local]);
2495
- }
2496
- }
2497
3790
  const prev = target[prop];
2498
- if (Object.is(prev, wrapped))
3791
+ if (Object.is(prev, next))
2499
3792
  return true;
2500
- const ok = Reflect.set(target, prop, wrapped, receiver);
3793
+ const ok = Reflect.set(target, prop, next, receiver);
2501
3794
  if (ok) {
2502
- if (prop === 'length')
3795
+ if (prop === 'length' || isArrayIndex(prop))
2503
3796
  notifyOwners(entry.owners, null);
3797
+ else if (typeof prop === 'string')
3798
+ notifyOwners(entry.owners, [prop]);
2504
3799
  else
2505
- notifyOwners(entry.owners, local);
3800
+ notifyOwners(entry.owners, null);
2506
3801
  }
2507
3802
  return ok;
2508
3803
  },
@@ -2532,7 +3827,7 @@ function wrapArray(arr, report, pathSegs) {
2532
3827
  * string form is transitional field-root alias for replace.
2533
3828
  */
2534
3829
  function scheduleRefresh(inst, notice) {
2535
- if (!inst || inst.__vmzDestroyed)
3830
+ if (!inst || inst.__vmzDestroyed || inst.__vmzQuiet)
2536
3831
  return;
2537
3832
  const n = typeof notice === 'string' ? { type: 'replace', root: notice } : notice;
2538
3833
  if (!n || !n.root)
@@ -2635,9 +3930,10 @@ export async function flushPending(inst) {
2635
3930
  jobs.push(...refreshBinding(inst, id, trie));
2636
3931
  }
2637
3932
  for (const key of binderKeysMatchingTrie(inst, trie)) {
2638
- if (coveredDeps[key])
2639
- continue;
2640
- if (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length) {
3933
+ if (coveredDeps[key] || (inst.__vmzDepToBindings && inst.__vmzDepToBindings[key]?.length)) {
3934
+ // BindingId path already flushed IR patches for this dep.
3935
+ // Still run binder-only patches (bindComponentProp uses bindingId null).
3936
+ jobs.push(...refreshFieldBinderOnly(inst, key));
2641
3937
  continue;
2642
3938
  }
2643
3939
  jobs.push(...refreshField(inst, key));
@@ -2930,6 +4226,32 @@ function refreshField(inst, field) {
2930
4226
  }
2931
4227
  return jobs;
2932
4228
  }
4229
+ /**
4230
+ * Run `__vmzBinders` patches that are not owned by a BindingId entry.
4231
+ * Needed so `bindComponentProp` (bindingId null) still flushes when the same
4232
+ * dep also has IR bindText/bindAttr BindingIds.
4233
+ * @returns {Promise[]}
4234
+ */
4235
+ function refreshFieldBinderOnly(inst, field) {
4236
+ const binders = inst.__vmzBinders;
4237
+ const jobs = [];
4238
+ if (!inst || inst.__vmzDestroyed || !field || !binders || !binders[field]) {
4239
+ return jobs;
4240
+ }
4241
+ for (const fn of binders[field]) {
4242
+ if (patchHasBindingId(inst, fn))
4243
+ continue;
4244
+ try {
4245
+ const ret = runPatch(fn, field, null);
4246
+ if (ret && typeof ret.then === 'function')
4247
+ jobs.push(ret);
4248
+ }
4249
+ catch (err) {
4250
+ console.error('vmz:dom patch', err);
4251
+ }
4252
+ }
4253
+ return jobs;
4254
+ }
2933
4255
  /**
2934
4256
  * @param {object} inst
2935
4257
  * @param {number|string} bindingId
@@ -2963,7 +4285,7 @@ function reindexBindingDeps(inst, bindingId, deps) {
2963
4285
  /**
2964
4286
  * @param {object} inst
2965
4287
  * @param {string[]} deps
2966
- * @param { => any} fn
4288
+ * @param {() => any} fn
2967
4289
  * @param {number|string|null|undefined} [bindingId]
2968
4290
  */
2969
4291
  function registerBind(inst, deps, fn, bindingId = null) {
@@ -2990,7 +4312,7 @@ function registerBind(inst, deps, fn, bindingId = null) {
2990
4312
  /**
2991
4313
  * @param {object} inst
2992
4314
  * @param {string[]} deps
2993
- * @param { => any} fn
4315
+ * @param {() => any} fn
2994
4316
  * @param {number|string|null|undefined} [bindingId]
2995
4317
  */
2996
4318
  function unregisterBind(inst, deps, fn, bindingId = null) {