nodality 1.3.17 → 1.3.19

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.
Files changed (248) hide show
  1. package/dist/animator.cjs.js +1 -1
  2. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  3. package/dist/animator.esm.js.LICENSE.txt +1 -1
  4. package/dist/audionew.cjs.js +1 -1
  5. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  6. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  7. package/dist/base.cjs.js +1 -1
  8. package/dist/base.cjs.js.LICENSE.txt +1 -1
  9. package/dist/base.esm.js.LICENSE.txt +1 -1
  10. package/dist/beta-desktop-bar.cjs.js +1 -1
  11. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  12. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  13. package/dist/beta-mobile-bar.cjs.js +1 -1
  14. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  15. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  16. package/dist/bundle.umd.js +1 -1
  17. package/dist/bundle.umd.js.LICENSE.txt +1 -1
  18. package/dist/button.cjs.js +1 -1
  19. package/dist/button.cjs.js.LICENSE.txt +1 -1
  20. package/dist/button.esm.js.LICENSE.txt +1 -1
  21. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  22. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  23. package/dist/center.cjs.js +1 -1
  24. package/dist/center.cjs.js.LICENSE.txt +1 -1
  25. package/dist/center.esm.js.LICENSE.txt +1 -1
  26. package/dist/checkbox.cjs.js +1 -1
  27. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  28. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  29. package/dist/code.cjs.js +1 -1
  30. package/dist/code.cjs.js.LICENSE.txt +1 -1
  31. package/dist/code.esm.js.LICENSE.txt +1 -1
  32. package/dist/container.cjs.js +1 -1
  33. package/dist/container.cjs.js.LICENSE.txt +1 -1
  34. package/dist/container.esm.js.LICENSE.txt +1 -1
  35. package/dist/data-list.cjs.js +1 -1
  36. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  37. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  38. package/dist/designer.cjs.js +1 -1
  39. package/dist/designer.cjs.js.LICENSE.txt +1 -1
  40. package/dist/designer.esm.js +1 -1
  41. package/dist/designer.esm.js.LICENSE.txt +1 -1
  42. package/dist/element-mapper.cjs.js +1 -1
  43. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  44. package/dist/element-mapper.esm.js +1 -1
  45. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  46. package/dist/finalresult.esm.js +1 -1
  47. package/dist/finalresult.esm.js.LICENSE.txt +1 -1
  48. package/dist/flex-card.cjs.js +1 -1
  49. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  50. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  51. package/dist/flex-grid.cjs.js +1 -1
  52. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  53. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  54. package/dist/flex-row.cjs.js +1 -1
  55. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  56. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  57. package/dist/floating-input.cjs.js +1 -1
  58. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  59. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  60. package/dist/free.cjs.js +1 -1
  61. package/dist/free.cjs.js.LICENSE.txt +1 -1
  62. package/dist/free.esm.js.LICENSE.txt +1 -1
  63. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  64. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  65. package/dist/image-picker.cjs.js +1 -1
  66. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  67. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  68. package/dist/image.cjs.js +1 -1
  69. package/dist/image.cjs.js.LICENSE.txt +1 -1
  70. package/dist/image.esm.js +1 -1
  71. package/dist/image.esm.js.LICENSE.txt +1 -1
  72. package/dist/index.cjs.js +1 -1
  73. package/dist/index.cjs.js.LICENSE.txt +1 -1
  74. package/dist/index.d.ts +3 -2
  75. package/dist/index.esm.js +1 -1
  76. package/dist/index.esm.js.LICENSE.txt +1 -1
  77. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  78. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  79. package/dist/link-getter.cjs.js +1 -1
  80. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  81. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  82. package/dist/link.cjs.js +1 -1
  83. package/dist/link.cjs.js.LICENSE.txt +1 -1
  84. package/dist/link.esm.js.LICENSE.txt +1 -1
  85. package/dist/meta-adder.cjs.js +1 -1
  86. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  87. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  88. package/dist/modal-2025.cjs.js +1 -1
  89. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  90. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  91. package/dist/multiswitcher.cjs.js +1 -1
  92. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  93. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  94. package/dist/new-nav-bar.cjs.js +1 -1
  95. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  96. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  97. package/dist/picker.cjs.js +1 -1
  98. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  99. package/dist/picker.esm.js.LICENSE.txt +1 -1
  100. package/dist/progress.cjs.js +1 -1
  101. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  102. package/dist/progress.esm.js.LICENSE.txt +1 -1
  103. package/dist/radio.cjs.js +1 -1
  104. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  105. package/dist/radio.esm.js.LICENSE.txt +1 -1
  106. package/dist/range.cjs.js +1 -1
  107. package/dist/range.cjs.js.LICENSE.txt +1 -1
  108. package/dist/range.esm.js.LICENSE.txt +1 -1
  109. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  110. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  111. package/dist/side-bar.cjs.js +1 -1
  112. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  113. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  114. package/dist/side-nav-bar.cjs.js +1 -1
  115. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  116. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  117. package/dist/simple-bar.cjs.js +1 -1
  118. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  119. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  120. package/dist/slider-2025.cjs.js +1 -1
  121. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  122. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  123. package/dist/spacer.cjs.js +1 -1
  124. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  125. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  126. package/dist/stack.cjs.js +1 -1
  127. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  128. package/dist/stack.esm.js.LICENSE.txt +1 -1
  129. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  130. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  131. package/dist/table.cjs.js +1 -1
  132. package/dist/table.cjs.js.LICENSE.txt +1 -1
  133. package/dist/table.esm.js.LICENSE.txt +1 -1
  134. package/dist/text-field.cjs.js +1 -1
  135. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  136. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  137. package/dist/text.cjs.js +1 -1
  138. package/dist/text.cjs.js.LICENSE.txt +1 -1
  139. package/dist/text.esm.js.LICENSE.txt +1 -1
  140. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  141. package/dist/theme.esm.js.LICENSE.txt +1 -1
  142. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  143. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  144. package/dist/ulist.cjs.js +1 -1
  145. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  146. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  147. package/dist/video.cjs.js +1 -1
  148. package/dist/video.cjs.js.LICENSE.txt +1 -1
  149. package/dist/video.esm.js.LICENSE.txt +1 -1
  150. package/dist/wrap.cjs.js +1 -1
  151. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  152. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  153. package/dist/zoom-card.cjs.js +1 -1
  154. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  155. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  156. package/layout/animator.js +1 -1
  157. package/layout/audio.js +1 -1
  158. package/layout/audionew.js +1 -1
  159. package/layout/base.js +1 -1
  160. package/layout/beta-desktop-bar.js +1 -1
  161. package/layout/beta-mobile-bar.js +1 -1
  162. package/layout/button.js +1 -1
  163. package/layout/center.js +1 -1
  164. package/layout/checkbox.js +1 -1
  165. package/layout/circle.js +1 -1
  166. package/layout/code.js +1 -1
  167. package/layout/container.js +1 -1
  168. package/layout/dropdown-2025.js +1 -1
  169. package/layout/flex-card.js +1 -1
  170. package/layout/flex-grid.js +1 -1
  171. package/layout/flex-row.js +1 -1
  172. package/layout/form-components/custom.js +1 -1
  173. package/layout/form-components/data-list.js +1 -1
  174. package/layout/form-components/floating-input.js +1 -1
  175. package/layout/form-components/form-all.js +1 -1
  176. package/layout/form-components/form.js +1 -1
  177. package/layout/form-components/image-picker.js +1 -1
  178. package/layout/form-components/picker.js +1 -1
  179. package/layout/form-components/radio.js +1 -1
  180. package/layout/form-components/radiogroup.js +1 -1
  181. package/layout/form-components/range.js +1 -1
  182. package/layout/free.js +1 -1
  183. package/layout/grid-overlay.js +1 -1
  184. package/layout/grid-switcher.js +1 -1
  185. package/layout/grid.js +1 -1
  186. package/layout/horizontal-scroller.js +1 -1
  187. package/layout/image.js +22 -3
  188. package/layout/index.js +1 -1
  189. package/layout/link.js +1 -1
  190. package/layout/list.js +1 -1
  191. package/layout/meta-adder.js +1 -1
  192. package/layout/modal-2025.js +1 -1
  193. package/layout/morph.js +1 -1
  194. package/layout/multiswitcher.js +1 -1
  195. package/layout/nav-bar.js +1 -1
  196. package/layout/nav-factor/custom-div.js +1 -1
  197. package/layout/new-nav-bar.js +1 -1
  198. package/layout/polygon.js +1 -1
  199. package/layout/prerender-site.js +1 -1
  200. package/layout/prerender.js +1 -1
  201. package/layout/progress.js +1 -1
  202. package/layout/row.js +1 -1
  203. package/layout/scroll-video.js +1 -1
  204. package/layout/shop.js +8 -1
  205. package/layout/side-bar.js +1 -1
  206. package/layout/side-nav-bar.js +1 -1
  207. package/layout/simple-bar.js +1 -1
  208. package/layout/slider-2025.js +1 -1
  209. package/layout/spacer.js +1 -1
  210. package/layout/stack.js +1 -1
  211. package/layout/svg.js +1 -1
  212. package/layout/switcher.js +1 -1
  213. package/layout/table.js +1 -1
  214. package/layout/text-field.js +1 -1
  215. package/layout/text.js +1 -1
  216. package/layout/ulist.js +1 -1
  217. package/layout/video.js +1 -1
  218. package/layout/wrap.js +1 -1
  219. package/layout/zoom-card.js +1 -1
  220. package/lib/agent-surface.js +1 -1
  221. package/lib/card-getter.js +1 -1
  222. package/lib/check-page.js +58 -1
  223. package/lib/codegen.js +1 -1
  224. package/lib/data.js +1 -1
  225. package/lib/designer.js +1 -1
  226. package/lib/element-mapper.js +1 -1
  227. package/lib/element-params.generated.js +1 -1
  228. package/lib/grid-geometry.js +1 -1
  229. package/lib/keyframe-animation.js +1 -1
  230. package/lib/known-inert.js +1 -1
  231. package/lib/link-getter.js +1 -1
  232. package/lib/morph-node.js +1 -1
  233. package/lib/normalize-spec.js +1 -1
  234. package/lib/parse-html.js +1 -1
  235. package/lib/raster-inspect.js +1 -1
  236. package/lib/raster-ops.js +43 -6
  237. package/lib/raster-presets.js +1 -1
  238. package/lib/resolve-refs.js +1 -1
  239. package/lib/scroll-video.js +1 -1
  240. package/lib/seo.js +1 -1
  241. package/lib/stacker.js +1 -1
  242. package/lib/suggest.js +1 -1
  243. package/lib/theme.js +1 -1
  244. package/lib/transform-anim.js +1 -1
  245. package/lib/transition.js +1 -1
  246. package/lib/validate-nodes.js +29 -2
  247. package/lib/webmcp-adapter.js +1 -1
  248. package/package.json +1 -1
package/lib/check-page.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -274,6 +274,53 @@ const IN_PAGE = () => {
274
274
  }
275
275
  }
276
276
  }
277
+
278
+ // A shop product's loading state against its loaded state. Until the
279
+ // shop answers, a product shows its static copy (`-static` ids) in the
280
+ // placeholder; then Shopify stamps the <template> and hides the copy. A
281
+ // page can look right in both and still jump between them — or, as on
282
+ // the suitcase site before 1.3.18, be wrong only while loading (price
283
+ // and options run into one line). Nothing above measures that, because
284
+ // nothing overflows. So the stamp is done here, the way Shopify does it,
285
+ // each live element is measured against its static copy, and the stamp
286
+ // is undone. The shop script itself is blocked (see checkPage), so this
287
+ // is deterministic and needs no store.
288
+ for (const ctx of document.querySelectorAll("shopify-context")) {
289
+ const tpl = [...ctx.children].find((c) => c.tagName === "TEMPLATE");
290
+ const ph = [...ctx.children].find((c) => c.hasAttribute("shopify-loading-placeholder"));
291
+ if (!tpl || !ph || ph.hidden || !ph.getClientRects) continue;
292
+ const box = ctx.getBoundingClientRect();
293
+ const at = (el) => {
294
+ const r = el.getBoundingClientRect();
295
+ return { x: r.left - box.left, y: r.top - box.top, w: r.width, h: r.height };
296
+ };
297
+ const before = new Map();
298
+ for (const el of ph.querySelectorAll("[id$='-static']")) {
299
+ if (el.getClientRects().length) before.set(el.id.replace(/-static$/, ""), at(el));
300
+ }
301
+ const stamped = [...tpl.content.cloneNode(true).childNodes];
302
+ stamped.forEach((n) => ctx.appendChild(n));
303
+ ph.hidden = true;
304
+ const prev = ph.style.display;
305
+ ph.style.display = "none";
306
+ let worst = null;
307
+ for (const [id, a] of before) {
308
+ const live = ctx.querySelector(`[id="${CSS.escape(id)}"]`);
309
+ if (!live || !live.getClientRects().length) continue;
310
+ const b = at(live);
311
+ const move = Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y));
312
+ const resize = Math.max(Math.abs(a.w - b.w), Math.abs(a.h - b.h));
313
+ const d = Math.max(move, resize);
314
+ if (d > 8 && (!worst || d > worst.d)) worst = { id, d: Math.round(d), move: Math.round(move), resize: Math.round(resize) };
315
+ }
316
+ stamped.forEach((n) => n.parentNode && n.parentNode.removeChild(n));
317
+ ph.style.display = prev;
318
+ ph.hidden = false;
319
+ if (worst) {
320
+ add("LOADING_LAYOUT_SHIFT", ctx, `#${worst.id}: ${worst.d}px`,
321
+ `the loading copy and the loaded element differ by ${worst.move}px in position and ${worst.resize}px in size — the product jumps when the shop answers, and reads differently until it does`);
322
+ }
323
+ }
277
324
  return out;
278
325
  };
279
326
 
@@ -335,6 +382,11 @@ const REPAIRS = {
335
382
  suggestions: [`maxWidth: "100%" on whichever element the other findings name`],
336
383
  valid: ["the document may not be wider than the viewport"],
337
384
  },
385
+ LOADING_LAYOUT_SHIFT: {
386
+ suggestions: [`give the static copy the same text and layout keys as the live element`,
387
+ `upgrade nodality: since 1.3.18 the placeholder lays out in the product's own grid`],
388
+ valid: ["a product's loading copy sits where its loaded element will, within 8px"],
389
+ },
338
390
  CONTENT_CLIPPED: {
339
391
  suggestions: [`height: "auto"`, `overflow: "visible"`],
340
392
  valid: ["a box with `overflow: hidden` must be tall enough for its text"],
@@ -385,6 +437,11 @@ export async function checkPage(html, opts = {}) {
385
437
  for (const vp of viewports) {
386
438
  const page = await browser.newPage({ viewport: { width: vp.width, height: vp.height } });
387
439
  try {
440
+ // The shop script never loads here: a product is checked in
441
+ // the state every visitor sees first, the same on every run
442
+ // and without a store. LOADING_LAYOUT_SHIFT compares it with
443
+ // the loaded state by stamping the template itself.
444
+ await page.route(/cdn\.shopify\.com\/storefront\//, (r) => r.abort());
388
445
  await page.setContent(doc, { waitUntil });
389
446
  const found = await page.evaluate(IN_PAGE);
390
447
  for (const f of found) {
package/lib/codegen.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/data.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/designer.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/morph-node.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/parse-html.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/raster-ops.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -21,8 +21,12 @@
21
21
  // snapshot (default) — XMLSerializer -> SVG foreignObject -> Image ->
22
22
  // texture. Works in every browser. Nodality output is inline-styled,
23
23
  // so fidelity is high. Static: recaptured on resize (or via
24
- // handle.refresh()). External images / webfonts do not load inside
25
- // the SVG image context — system fonts and data: URIs only.
24
+ // handle.refresh()). External images do not load inside the SVG
25
+ // image context (data: URIs do). Web fonts DO arrive: the faces the
26
+ // subtree uses are fetched and embedded as data: URIs (see "Web fonts
27
+ // inside the snapshot"); only a font host that refuses CORS falls back
28
+ // to a local typeface. (This line said web fonts never load until
29
+ // October 2026, long after embedding shipped.)
26
30
  // live (the DEFAULT where it applies; opt OUT with `live: false` on any
27
31
  // node) — the emerging HTML-in-Canvas API (WICG, Chrome origin trial):
28
32
  // the mount subtree moves inside the effect <canvas layoutsubtree> and
@@ -108,12 +112,26 @@ const DRIVERS = {
108
112
  };
109
113
  const DRIVER_NAMES = Object.keys(DRIVERS);
110
114
 
115
+ // What each driver does, for describeOps(). The resting behaviour is the part
116
+ // an agent cannot infer from a name: whether the effect is visible before
117
+ // anyone interacts, which decides whether a page looks the same at rest.
118
+ const DRIVER_DOCS = {
119
+ static: "The element's centre at full strength, always on. The default for ops that need a focus.",
120
+ mouse: "Follows the pointer at full strength, and stays on where the pointer last was — visible at rest.",
121
+ hover: "Follows the pointer, faded in and out with hover — nothing shows at rest or without a pointer (a phone).",
122
+ scroll: "A band that travels up the element as it crosses the viewport, at full strength.",
123
+ time: "Hands-free: the focus drifts on a Lissajous path, always on.",
124
+ };
125
+
111
126
  const REGISTRY = {
112
127
  hexalize: {
113
128
  doc: {
114
129
  summary: "Hexagonal cell grid. Snaps sampling to a hex lattice and "
115
130
  + "draws cell borders; with `lift`, cells near the driver swell "
116
- + "toward the viewer and their content magnifies to match.",
131
+ + "toward the viewer and their content magnifies to match. Away "
132
+ + "from the focus a lifted chain is untouched — no magnification, "
133
+ + "no border — so with `by: \"hover\"` the element is "
134
+ + "pixel-identical at rest and the cells appear only under the pointer.",
117
135
  params: {
118
136
  size: { default: 24, unit: "px", summary: "lattice pitch — the width of one cell" },
119
137
  lift: {
@@ -442,6 +460,10 @@ ${NB.map(([x, y]) => probe(x, y)).join("")}
442
460
  },
443
461
  },
444
462
  stage: "color",
463
+ // It colours `edge`, which only a cell-stage op writes. Declared so the
464
+ // validator can say so (INERT_RASTER_OP) instead of the chain
465
+ // validating clean and drawing nothing.
466
+ needs: "cell",
445
467
  decl: (p) => `uniform vec3 ${p}color; uniform float ${p}strength;`,
446
468
  code: (p) => `
447
469
  {
@@ -4094,11 +4116,26 @@ function applyRasterPipeline(el, rasterNodes, opts) {
4094
4116
 
4095
4117
  if (live) {
4096
4118
  canvas.addEventListener("paint", onPaint);
4097
- setTimeout(() => {
4119
+ // The 1500ms is a budget for a page someone is looking at. A hidden
4120
+ // document — a tab opened in the background — paints nothing at all,
4121
+ // so timing it from load sent every background-opened page to the
4122
+ // snapshot for good, even in a browser with the live API. The clock
4123
+ // starts when the page is first shown.
4124
+ const startPaintClock = () => setTimeout(() => {
4098
4125
  if (!destroyed && mode === "live" && !textureReady) {
4099
4126
  fallbackToSnapshot("no paint event within 1500ms");
4100
4127
  }
4101
4128
  }, 1500);
4129
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
4130
+ const onShown = () => {
4131
+ if (document.visibilityState === "hidden") return;
4132
+ document.removeEventListener("visibilitychange", onShown);
4133
+ if (!destroyed) startPaintClock();
4134
+ };
4135
+ document.addEventListener("visibilitychange", onShown);
4136
+ } else {
4137
+ startPaintClock();
4138
+ }
4102
4139
  }
4103
4140
 
4104
4141
  // Pointer -> u_mouse (listen on the host so the overlay stays
@@ -4963,7 +5000,7 @@ function applyRasterPipeline(el, rasterNodes, opts) {
4963
5000
 
4964
5001
  export {
4965
5002
  applyRasterPipeline, registerRasterOp, RASTER_OP_NAMES,
4966
- isHTMLInCanvasAvailable, DRIVER_NAMES,
5003
+ isHTMLInCanvasAvailable, DRIVER_NAMES, DRIVER_DOCS,
4967
5004
  // Phase H3. The registry is already effectively public — registerRasterOp
4968
5005
  // mutates it — and a dev tool needs to read an op's stage and params to
4969
5006
  // show them. `isStructuralChange` is exported so the inspector can label
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/seo.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/stacker.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/suggest.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/theme.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/lib/transition.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -26,7 +26,7 @@
26
26
  */
27
27
 
28
28
  import {
29
- REGISTRY, RASTER_OP_NAMES, DRIVER_NAMES, RASTER_UNITS,
29
+ REGISTRY, RASTER_OP_NAMES, DRIVER_NAMES, DRIVER_DOCS, RASTER_UNITS,
30
30
  FRAMEWORK_DOC, EASING_NAMES,
31
31
  } from "./raster-ops.js";
32
32
  import { presetNames, presetInfo } from "./raster-presets.js";
@@ -1026,6 +1026,28 @@ export function validateNodes(nodes, elements, defs) {
1026
1026
  }
1027
1027
  });
1028
1028
 
1029
+ // An op that only colours what an earlier stage produced draws nothing
1030
+ // without it: `edges` reads the cell borders a cell op (hexalize) writes.
1031
+ // The chain is per target — raster nodes on one element compile into one
1032
+ // pass, in array order — so the producer must come earlier on the same
1033
+ // target. Declared on the op (`needs`), not listed here.
1034
+ const stagesOf = (d) => [].concat((d && d.stage) || []);
1035
+ const seen = new Map(); // target key -> stages written so far
1036
+ nodes.forEach((node, i) => {
1037
+ if (!node || typeof node.op !== "string") return;
1038
+ const d = REGISTRY[node.op];
1039
+ if (!d) return;
1040
+ const key = Array.isArray(node.target) ? node.target.map((t) => String(t).replace(/^#/, "")).sort().join(",") : "";
1041
+ const have = seen.get(key) || new Set();
1042
+ if (d.needs && !have.has(d.needs)) {
1043
+ const producers = RASTER_OP_NAMES.filter((n) => stagesOf(REGISTRY[n]).includes(d.needs));
1044
+ warn("INERT_RASTER_OP", `nodes[${i}].op`, node.op, producers, producers,
1045
+ `"${node.op}" draws nothing unless a ${d.needs}-stage op (${producers.join(", ")}) comes before it on the same target.`);
1046
+ }
1047
+ stagesOf(d).forEach((s) => have.add(s));
1048
+ seen.set(key, have);
1049
+ });
1050
+
1029
1051
  return { ok: errors.length === 0, errors, warnings };
1030
1052
  }
1031
1053
 
@@ -1043,6 +1065,8 @@ export function describeOps() {
1043
1065
  op: name,
1044
1066
  stage: Array.isArray(def.stage) ? def.stage : [def.stage].filter(Boolean),
1045
1067
  summary: doc.summary || "",
1068
+ // What must come before it on the same target for it to draw.
1069
+ ...(def.needs ? { needs: def.needs } : {}),
1046
1070
  params: Object.keys(params).map((p) => ({
1047
1071
  name: p,
1048
1072
  default: params[p].default,
@@ -1071,6 +1095,9 @@ export function describeOps() {
1071
1095
  summary: FRAMEWORK_DOC[k].summary || "",
1072
1096
  })),
1073
1097
  drivers: DRIVER_NAMES,
1098
+ // Names stay a plain list above for existing readers; this says what
1099
+ // each does, including whether anything shows at rest.
1100
+ driverDocs: DRIVER_NAMES.map((name) => ({ name, summary: DRIVER_DOCS[name] || "" })),
1074
1101
  easings: EASING_NAMES,
1075
1102
  units: RASTER_UNITS,
1076
1103
  };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.17
2
+ * nodality v1.3.19
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodality",
3
- "version": "1.3.17",
3
+ "version": "1.3.19",
4
4
  "description": "A lightweight library for declarative UI elements.",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",