nodality 1.3.7 → 1.3.9

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 (245) hide show
  1. package/bin/nodality.mjs +1 -1
  2. package/dist/animator.cjs.js +1 -1
  3. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  4. package/dist/animator.esm.js.LICENSE.txt +1 -1
  5. package/dist/audionew.cjs.js +1 -1
  6. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  7. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  8. package/dist/base.cjs.js +1 -1
  9. package/dist/base.cjs.js.LICENSE.txt +1 -1
  10. package/dist/base.esm.js.LICENSE.txt +1 -1
  11. package/dist/beta-desktop-bar.cjs.js +1 -1
  12. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  13. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  14. package/dist/beta-mobile-bar.cjs.js +1 -1
  15. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  16. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  17. package/dist/bundle.umd.js +1 -1
  18. package/dist/bundle.umd.js.LICENSE.txt +1 -1
  19. package/dist/button.cjs.js +1 -1
  20. package/dist/button.cjs.js.LICENSE.txt +1 -1
  21. package/dist/button.esm.js.LICENSE.txt +1 -1
  22. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  23. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  24. package/dist/center.cjs.js +1 -1
  25. package/dist/center.cjs.js.LICENSE.txt +1 -1
  26. package/dist/center.esm.js.LICENSE.txt +1 -1
  27. package/dist/checkbox.cjs.js +1 -1
  28. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  29. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  30. package/dist/code.cjs.js +1 -1
  31. package/dist/code.cjs.js.LICENSE.txt +1 -1
  32. package/dist/code.esm.js.LICENSE.txt +1 -1
  33. package/dist/container.cjs.js +1 -1
  34. package/dist/container.cjs.js.LICENSE.txt +1 -1
  35. package/dist/container.esm.js.LICENSE.txt +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 +2 -2
  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.LICENSE.txt +1 -1
  71. package/dist/index.cjs.js +1 -1
  72. package/dist/index.cjs.js.LICENSE.txt +1 -1
  73. package/dist/index.d.ts +17 -2
  74. package/dist/index.esm.js +2 -2
  75. package/dist/index.esm.js.LICENSE.txt +1 -1
  76. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  77. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  78. package/dist/link-getter.cjs.js +1 -1
  79. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  80. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  81. package/dist/link.cjs.js +1 -1
  82. package/dist/link.cjs.js.LICENSE.txt +1 -1
  83. package/dist/link.esm.js.LICENSE.txt +1 -1
  84. package/dist/meta-adder.cjs.js +1 -1
  85. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  86. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  87. package/dist/modal-2025.cjs.js +1 -1
  88. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  89. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  90. package/dist/multiswitcher.cjs.js +1 -1
  91. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  92. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  93. package/dist/new-nav-bar.cjs.js +1 -1
  94. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  95. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  96. package/dist/picker.cjs.js +1 -1
  97. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  98. package/dist/picker.esm.js.LICENSE.txt +1 -1
  99. package/dist/progress.cjs.js +1 -1
  100. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  101. package/dist/progress.esm.js.LICENSE.txt +1 -1
  102. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  103. package/dist/radio.esm.js.LICENSE.txt +1 -1
  104. package/dist/range.cjs.js.LICENSE.txt +1 -1
  105. package/dist/range.esm.js.LICENSE.txt +1 -1
  106. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  107. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  108. package/dist/side-bar.cjs.js +1 -1
  109. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  110. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  111. package/dist/side-nav-bar.cjs.js +1 -1
  112. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  113. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  114. package/dist/simple-bar.cjs.js +1 -1
  115. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  116. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  117. package/dist/slider-2025.cjs.js +1 -1
  118. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  119. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  120. package/dist/spacer.cjs.js +1 -1
  121. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  122. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  123. package/dist/stack.cjs.js +1 -1
  124. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  125. package/dist/stack.esm.js.LICENSE.txt +1 -1
  126. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  127. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  128. package/dist/table.cjs.js +1 -1
  129. package/dist/table.cjs.js.LICENSE.txt +1 -1
  130. package/dist/table.esm.js.LICENSE.txt +1 -1
  131. package/dist/text-field.cjs.js +1 -1
  132. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  133. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  134. package/dist/text.cjs.js +1 -1
  135. package/dist/text.cjs.js.LICENSE.txt +1 -1
  136. package/dist/text.esm.js.LICENSE.txt +1 -1
  137. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  138. package/dist/theme.esm.js.LICENSE.txt +1 -1
  139. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  140. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  141. package/dist/ulist.cjs.js +1 -1
  142. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  143. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  144. package/dist/video.cjs.js +1 -1
  145. package/dist/video.cjs.js.LICENSE.txt +1 -1
  146. package/dist/video.esm.js +1 -1
  147. package/dist/video.esm.js.LICENSE.txt +1 -1
  148. package/dist/wrap.cjs.js +1 -1
  149. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  150. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  151. package/dist/zoom-card.cjs.js +1 -1
  152. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  153. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  154. package/layout/animator.js +1 -1
  155. package/layout/audio.js +1 -1
  156. package/layout/audionew.js +1 -1
  157. package/layout/base.js +1 -1
  158. package/layout/beta-desktop-bar.js +1 -1
  159. package/layout/beta-mobile-bar.js +1 -1
  160. package/layout/button.js +1 -1
  161. package/layout/center.js +1 -1
  162. package/layout/checkbox.js +1 -1
  163. package/layout/circle.js +1 -1
  164. package/layout/code.js +1 -1
  165. package/layout/container.js +1 -1
  166. package/layout/dropdown-2025.js +1 -1
  167. package/layout/flex-card.js +1 -1
  168. package/layout/flex-grid.js +1 -1
  169. package/layout/flex-row.js +1 -1
  170. package/layout/form-components/custom.js +1 -1
  171. package/layout/form-components/data-list.js +1 -1
  172. package/layout/form-components/floating-input.js +1 -1
  173. package/layout/form-components/form-all.js +1 -1
  174. package/layout/form-components/form.js +1 -1
  175. package/layout/form-components/image-picker.js +1 -1
  176. package/layout/form-components/picker.js +1 -1
  177. package/layout/form-components/radio.js +1 -1
  178. package/layout/form-components/radiogroup.js +1 -1
  179. package/layout/form-components/range.js +1 -1
  180. package/layout/free.js +1 -1
  181. package/layout/grid-overlay.js +296 -0
  182. package/layout/grid-switcher.js +1 -1
  183. package/layout/grid.js +1 -1
  184. package/layout/horizontal-scroller.js +1 -1
  185. package/layout/image.js +1 -1
  186. package/layout/index.js +5 -1
  187. package/layout/link.js +1 -1
  188. package/layout/list.js +1 -1
  189. package/layout/meta-adder.js +1 -1
  190. package/layout/modal-2025.js +1 -1
  191. package/layout/morph.js +1 -1
  192. package/layout/multiswitcher.js +1 -1
  193. package/layout/nav-bar.js +1 -1
  194. package/layout/nav-factor/custom-div.js +1 -1
  195. package/layout/new-nav-bar.js +1 -1
  196. package/layout/polygon.js +1 -1
  197. package/layout/prerender-site.js +1 -1
  198. package/layout/prerender.js +1 -1
  199. package/layout/progress.js +1 -1
  200. package/layout/row.js +1 -1
  201. package/layout/scroll-video.js +1 -1
  202. package/layout/side-bar.js +1 -1
  203. package/layout/side-nav-bar.js +1 -1
  204. package/layout/simple-bar.js +1 -1
  205. package/layout/slider-2025.js +1 -1
  206. package/layout/spacer.js +1 -1
  207. package/layout/stack.js +1 -1
  208. package/layout/svg.js +1 -1
  209. package/layout/switcher.js +1 -1
  210. package/layout/table.js +1 -1
  211. package/layout/text-field.js +1 -1
  212. package/layout/text.js +1 -1
  213. package/layout/ulist.js +1 -1
  214. package/layout/video.js +203 -38
  215. package/layout/wrap.js +1 -1
  216. package/layout/zoom-card.js +1 -1
  217. package/lib/agent-surface.js +1 -1
  218. package/lib/card-getter.js +1 -1
  219. package/lib/check-page.js +1 -1
  220. package/lib/codegen.js +1 -1
  221. package/lib/data.js +1 -1
  222. package/lib/designer.js +4 -2
  223. package/lib/element-mapper.js +36 -3
  224. package/lib/element-params.generated.js +2 -2
  225. package/lib/grid-geometry.js +137 -0
  226. package/lib/keyframe-animation.js +1 -1
  227. package/lib/link-getter.js +1 -1
  228. package/lib/morph-node.js +1 -1
  229. package/lib/normalize-spec.js +1 -1
  230. package/lib/parse-html.js +1 -1
  231. package/lib/raster-inspect.js +1 -1
  232. package/lib/raster-ops.js +42 -3
  233. package/lib/raster-presets.js +1 -1
  234. package/lib/resolve-refs.js +1 -1
  235. package/lib/scroll-video.js +1 -1
  236. package/lib/seo.js +1 -1
  237. package/lib/stacker.js +1 -1
  238. package/lib/suggest.js +1 -1
  239. package/lib/theme.js +1 -1
  240. package/lib/transform-anim.js +1 -1
  241. package/lib/transition.js +1 -1
  242. package/lib/validate-nodes.js +1 -1
  243. package/lib/webmcp-adapter.js +1 -1
  244. package/package.json +1 -1
  245. package/schema.json +164 -5
@@ -0,0 +1,137 @@
1
+ /*!
2
+ * nodality v1.3.9
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ // grid-geometry.js — the arithmetic behind GridOverlay.
8
+ //
9
+ // One pure function turns a box size and a grid description into the pixel
10
+ // position of every line and every intersection. It lives apart from the
11
+ // component for two reasons:
12
+ //
13
+ // • It is testable in bare Node. No DOM, no window, nothing at import.
14
+ // • Anything that must land ON the grid needs the same numbers. The
15
+ // overlay draws the lines; a layout component that places content in
16
+ // the cells has to agree with it to the pixel, and two copies of this
17
+ // maths would drift.
18
+ //
19
+ // Conventions
20
+ // columns / rows count CELLS, not lines. N cells → N+1 lines. 0 → no
21
+ // on that axis (and therefore no intersections).
22
+ // inset distance of the outermost lines from the box edge, in px.
23
+ // A number applies to both axes; { x, y } sets them apart.
24
+ // lineWidth used only to snap positions so 1px lines land on a pixel
25
+ // boundary and stay sharp instead of smearing over two.
26
+
27
+ const clampCount = (n) => {
28
+ const v = Math.floor(Number(n));
29
+ return Number.isFinite(v) && v > 0 ? v : 0;
30
+ };
31
+
32
+ const toInset = (inset) => {
33
+ if (inset && typeof inset === "object") {
34
+ return { x: Math.max(0, Number(inset.x) || 0), y: Math.max(0, Number(inset.y) || 0) };
35
+ }
36
+ const v = Math.max(0, Number(inset) || 0);
37
+ return { x: v, y: v };
38
+ };
39
+
40
+ // Odd stroke widths centre on a half pixel; even ones on a whole pixel.
41
+ const snap = (v, lineWidth) => {
42
+ const w = Math.max(1, Math.round(Number(lineWidth) || 1));
43
+ return w % 2 === 1 ? Math.round(v - 0.5) + 0.5 : Math.round(v);
44
+ };
45
+
46
+ // Positions of `count + 1` lines spread evenly from `start` to `end`.
47
+ const linePositions = (count, start, end, lineWidth) => {
48
+ if (count === 0 || end <= start) return [];
49
+ const step = (end - start) / count;
50
+ const out = [];
51
+ for (let i = 0; i <= count; i++) out.push(snap(start + step * i, lineWidth));
52
+ return out;
53
+ };
54
+
55
+ function gridGeometry({ width = 0, height = 0, columns = 4, rows = 2, inset = 0, lineWidth = 1 } = {}) {
56
+ const w = Math.max(0, Number(width) || 0);
57
+ const h = Math.max(0, Number(height) || 0);
58
+ const c = clampCount(columns);
59
+ const r = clampCount(rows);
60
+ const pad = toInset(inset);
61
+
62
+ // An inset larger than half the box leaves no room for a grid.
63
+ const left = Math.min(pad.x, w / 2);
64
+ const top = Math.min(pad.y, h / 2);
65
+
66
+ const x = linePositions(c, left, w - left, lineWidth);
67
+ const y = linePositions(r, top, h - top, lineWidth);
68
+
69
+ const intersections = [];
70
+ for (let row = 0; row < y.length; row++) {
71
+ for (let col = 0; col < x.length; col++) {
72
+ intersections.push({ col, row, x: x[col], y: y[row] });
73
+ }
74
+ }
75
+
76
+ return {
77
+ width: w,
78
+ height: h,
79
+ columns: c,
80
+ rows: r,
81
+ inset: { x: left, y: top },
82
+ cell: {
83
+ width: c ? (w - 2 * left) / c : 0,
84
+ height: r ? (h - 2 * top) / r : 0,
85
+ },
86
+ x,
87
+ y,
88
+ intersections,
89
+ };
90
+ }
91
+
92
+ // Options for the current viewport. Below `breakpoint` the keys in `mobile`
93
+ // replace their desktop counterparts; everything else carries over.
94
+ function resolveGridOptions(options = {}, viewportWidth = Infinity) {
95
+ const { mobile, ...base } = options || {};
96
+ const breakpoint = Number(base.breakpoint) || 768;
97
+ if (mobile && typeof mobile === "object" && viewportWidth < breakpoint) {
98
+ return { ...base, ...mobile };
99
+ }
100
+ return base;
101
+ }
102
+
103
+ // Where a label sits relative to an intersection. `anchor` names the
104
+ // quadrant: "br" below-right (default), "bl", "tr", "tl".
105
+ //
106
+ // Pass `box` and the anchor becomes a preference rather than an order: a
107
+ // label that would land outside the box flips to the opposite side. Without
108
+ // it, any readout on the last column or bottom row is clipped by the host's
109
+ // overflow — which is what the outermost intersections are FOR, so the
110
+ // default must not punish using them.
111
+ function readoutPosition(point, anchor = "br", gap = 8, box = null) {
112
+ const a = ["tl", "tr", "bl", "br"].includes(anchor) ? anchor : "br";
113
+ let right = a[1] === "r";
114
+ let below = a[0] === "b";
115
+
116
+ if (box) {
117
+ const w = Number(box.width) || 0;
118
+ const h = Number(box.height) || 0;
119
+ // Room for a short label on the side the anchor asks for. The exact
120
+ // text width is a render-time measurement; this only has to catch the
121
+ // case where the intersection sits ON the edge.
122
+ const room = gap + 24;
123
+ if (right && point.x + room > w) right = false;
124
+ else if (!right && point.x - room < 0) right = true;
125
+ if (below && point.y + room > h) below = false;
126
+ else if (!below && point.y - room < 0) below = true;
127
+ }
128
+
129
+ return {
130
+ x: point.x + (right ? gap : -gap),
131
+ y: point.y + (below ? gap : -gap),
132
+ textAnchor: right ? "start" : "end",
133
+ baseline: below ? "hanging" : "alphabetic",
134
+ };
135
+ }
136
+
137
+ export { gridGeometry, resolveGridOptions, readoutPosition };
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -2936,8 +2936,24 @@ function snapshotToImage(el, w, h, dpr) {
2936
2936
  return embeddedFontCss(el).catch(() => "").then((fontCss) => new Promise((resolve, reject) => {
2937
2937
  const serialized = new XMLSerializer()
2938
2938
  .serializeToString(freezeStyles(el, el.cloneNode(true)));
2939
+ // The SVG carries NO scaling at all — it is emitted at CSS size, 1:1.
2940
+ //
2941
+ // Engines do not agree about scaling foreignObject content: mobile Chrome and
2942
+ // Safari apply neither a viewBox ratio nor an ancestor transform to the HTML
2943
+ // subtree, so content laid out at `w x h` stayed `w x h` in the top-left of a
2944
+ // `w*dpr` canvas. Reading the uploaded texture back off the GPU on a dpr-3
2945
+ // phone showed exactly that: a 1101x369 texture with its ink confined to the
2946
+ // first 367x123. The same SVG looked correct in an <img> sized to 367 CSS px,
2947
+ // because there the unscaled content happened to fill the box — which is what
2948
+ // made this look like a compositing bug for so long.
2949
+ //
2950
+ // So the SVG is rasterised at the size its contents are laid out for, and the
2951
+ // dpr upscale is done by drawImage below, where the destination rectangle is
2952
+ // stated outright and no engine has an opinion. High-dpr output is a touch
2953
+ // softer than a native-resolution rasterisation would be; correct everywhere
2954
+ // beats crisp on the machines that happened to agree with us.
2939
2955
  const svg =
2940
- `<svg xmlns="http://www.w3.org/2000/svg" width="${w * dpr}" height="${h * dpr}" viewBox="0 0 ${w} ${h}">` +
2956
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` +
2941
2957
  `<foreignObject width="100%" height="100%">` +
2942
2958
  `<div xmlns="http://www.w3.org/1999/xhtml">` +
2943
2959
  (fontCss ? `<style>${fontCss}</style>` : "") +
@@ -2948,7 +2964,30 @@ function snapshotToImage(el, w, h, dpr) {
2948
2964
  // globalThis. Where they do, the DOM constructor is gone and this
2949
2965
  // promise never settles: the snapshot backend hangs with no error.
2950
2966
  const img = document.createElement("img");
2951
- img.onload = () => resolve(img);
2967
+ img.onload = () => {
2968
+ // Rasterise into a canvas of exactly the size we asked for, and hand
2969
+ // THAT to the caller.
2970
+ //
2971
+ // texImage2D takes an image's INTRINSIC size, and engines disagree about
2972
+ // what that is for an SVG: desktop Chrome honours the width/height
2973
+ // attributes (w*dpr), while mobile Chrome and Safari take it from the
2974
+ // viewBox (w). On a phone the texture therefore arrived at 1/dpr scale
2975
+ // and the effect painted its content into the top-left corner of its own
2976
+ // canvas — the headline came out about a third of its size, while every
2977
+ // measurement of the box, the SVG and the CSS looked perfectly correct.
2978
+ //
2979
+ // Drawing with an explicit destination rectangle makes the size ours
2980
+ // instead of theirs, so the texture matches the backing store on every
2981
+ // engine. This mirrors what fitImageToBox already does for the
2982
+ // sole-image fast path.
2983
+ const c = document.createElement("canvas");
2984
+ c.width = Math.max(1, Math.round(w * dpr));
2985
+ c.height = Math.max(1, Math.round(h * dpr));
2986
+ const ctx = c.getContext("2d");
2987
+ if (!ctx) { resolve(img); return; }
2988
+ ctx.drawImage(img, 0, 0, c.width, c.height);
2989
+ resolve(c);
2990
+ };
2952
2991
  img.onerror = reject;
2953
2992
  img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
2954
2993
  }));
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
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.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.3.7
2
+ * nodality v1.3.9
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.7",
3
+ "version": "1.3.9",
4
4
  "description": "A lightweight library for declarative UI elements.",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
package/schema.json CHANGED
@@ -4593,16 +4593,85 @@
4593
4593
  "Set"
4594
4594
  ],
4595
4595
  "params": [
4596
+ {
4597
+ "name": "aspectRatio",
4598
+ "description": "Box proportion held before the video loads, e.g. \"16 / 9\". Prevents the page shifting when the first frame arrives."
4599
+ },
4600
+ {
4601
+ "name": "autoplay",
4602
+ "description": "Start on its own. Implies muted and playsinline, pauses off screen and in a hidden tab, and is cancelled by prefers-reduced-motion (the poster stays)."
4603
+ },
4604
+ {
4605
+ "name": "background",
4606
+ "description": "CSS background behind the frame — visible while the poster loads."
4607
+ },
4608
+ {
4609
+ "name": "controls",
4610
+ "description": "Show the browser's playback controls. Default true — pass false for an ambient loop."
4611
+ },
4612
+ {
4613
+ "name": "decorative",
4614
+ "description": "Hide from assistive technology. For an ambient loop that repeats what the page already says in text."
4615
+ },
4616
+ {
4617
+ "name": "height",
4618
+ "description": "CSS height of the element."
4619
+ },
4596
4620
  {
4597
4621
  "name": "id",
4598
4622
  "description": "This element's DOM id. Also what a design node's `target` names, and what `parse` recovers an element by."
4599
4623
  },
4600
4624
  {
4601
- "name": "opacity"
4625
+ "name": "label",
4626
+ "description": "Accessible name, for a clip that carries meaning."
4627
+ },
4628
+ {
4629
+ "name": "lazy",
4630
+ "description": "Only play while on screen. On by default whenever `autoplay` is set; pass false to keep an autoplaying clip running off screen."
4631
+ },
4632
+ {
4633
+ "name": "loop",
4634
+ "description": "Restart when the clip ends."
4635
+ },
4636
+ {
4637
+ "name": "maxWidth",
4638
+ "description": "CSS max-width of the element."
4639
+ },
4640
+ {
4641
+ "name": "muted",
4642
+ "description": "Mute the audio track. Forced on by `autoplay`, because browsers block an unmuted autoplay; pass false only if you mean to accept that."
4643
+ },
4644
+ {
4645
+ "name": "objectFit",
4646
+ "description": "How the frame fills the element box: \"cover\", \"contain\", \"fill\", \"none\"."
4647
+ },
4648
+ {
4649
+ "name": "opacity",
4650
+ "description": "0–1."
4651
+ },
4652
+ {
4653
+ "name": "playsinline",
4654
+ "description": "Play in place on iPhone instead of taking over the screen. Implied by `autoplay`; there is no reason to refuse it on a background loop."
4655
+ },
4656
+ {
4657
+ "name": "poster",
4658
+ "description": "Image shown before the first frame decodes. Give one on any autoplaying clip: it is what holds the layout still and what reduced-motion visitors see instead of the video."
4659
+ },
4660
+ {
4661
+ "name": "preload",
4662
+ "description": "\"none\", \"metadata\" (a good default for a loop) or \"auto\"."
4602
4663
  },
4603
4664
  {
4604
4665
  "name": "radius",
4605
- "description": "Corner radius in pixels. A bare number, not a CSS length."
4666
+ "description": "Corner radius. A CSS length; a bare number is read as pixels."
4667
+ },
4668
+ {
4669
+ "name": "raster",
4670
+ "description": "Raster op nodes to run over this element."
4671
+ },
4672
+ {
4673
+ "name": "sources",
4674
+ "description": "Several encodings of the same clip: [{url, type}] or plain url strings. The browser picks the first it can play, so order best-first (AV1, then HEVC, then H.264). `type` is inferred from the extension when omitted."
4606
4675
  },
4607
4676
  {
4608
4677
  "name": "type",
@@ -4612,7 +4681,8 @@
4612
4681
  "name": "url"
4613
4682
  },
4614
4683
  {
4615
- "name": "width"
4684
+ "name": "width",
4685
+ "description": "CSS width of the element."
4616
4686
  }
4617
4687
  ]
4618
4688
  },
@@ -6285,11 +6355,100 @@
6285
6355
  "name": "width"
6286
6356
  }
6287
6357
  ]
6358
+ },
6359
+ "gridOverlay": {
6360
+ "resolved": true,
6361
+ "mapper": "mapGridOverlay",
6362
+ "components": [
6363
+ "GridOverlay"
6364
+ ],
6365
+ "params": [
6366
+ {
6367
+ "name": "breakpoint",
6368
+ "description": "Viewport width in px below which `mobile` applies. Default 768."
6369
+ },
6370
+ {
6371
+ "name": "color",
6372
+ "description": "Colour of lines, marks and readouts (drawn with currentColor). Falls back to the Theme text colour."
6373
+ },
6374
+ {
6375
+ "name": "columns",
6376
+ "description": "Columns, counted as cells (N cells draw N+1 lines). 0 draws no vertical lines. Default 4. Named `columns`, not `cols`: the library's shared `cols` means grid-template-columns verbatim."
6377
+ },
6378
+ {
6379
+ "name": "font",
6380
+ "description": "Font family of the readouts. Default a monospace stack."
6381
+ },
6382
+ {
6383
+ "name": "fontSize",
6384
+ "description": "Readout font size in px. Default 11."
6385
+ },
6386
+ {
6387
+ "name": "id",
6388
+ "description": "This element's DOM id. Also what a design node's `target` names, and what `parse` recovers an element by."
6389
+ },
6390
+ {
6391
+ "name": "inset",
6392
+ "description": "Distance of the outermost lines from the edge in px. A number, or {x, y}. Default 16."
6393
+ },
6394
+ {
6395
+ "name": "letterSpacing",
6396
+ "description": "Readout letter spacing, any CSS length. Default \"0.04em\"."
6397
+ },
6398
+ {
6399
+ "name": "lineOpacity",
6400
+ "description": "Opacity of the lines only; marks and readouts stay opaque. 0–1, default 0.35."
6401
+ },
6402
+ {
6403
+ "name": "lineWidth",
6404
+ "description": "Stroke width of the grid lines in px. Default 1."
6405
+ },
6406
+ {
6407
+ "name": "markSize",
6408
+ "description": "Size of each registration mark in px. Default 8."
6409
+ },
6410
+ {
6411
+ "name": "marks",
6412
+ "description": "Registration mark at each intersection: \"square\" (default), \"cross\" or \"none\"."
6413
+ },
6414
+ {
6415
+ "name": "mobile",
6416
+ "description": "Options that replace their desktop values below `breakpoint`, e.g. {columns: 1, rows: 4}."
6417
+ },
6418
+ {
6419
+ "name": "position",
6420
+ "description": "\"fixed\" (default) pins the grid to the viewport; \"absolute\" fills the nearest positioned ancestor."
6421
+ },
6422
+ {
6423
+ "name": "raster",
6424
+ "description": "Raster op nodes for the overlay, e.g. an offset driven by hover to bend the lines near the cursor."
6425
+ },
6426
+ {
6427
+ "name": "readouts",
6428
+ "description": "Labels beside intersections: [{id, col, row, text, anchor}]. col/row index lines from 0; anchor \"br\" (default), \"bl\", \"tr\" or \"tl\", flipped automatically when the label would fall outside. Decorative — say the same thing in real page text."
6429
+ },
6430
+ {
6431
+ "name": "rows",
6432
+ "description": "Rows across the overlay's height, counted as cells. 0 draws no horizontal lines. Default 2."
6433
+ },
6434
+ {
6435
+ "name": "type",
6436
+ "description": "The element type, which selects the mapper that builds it -- \"h1\", \"cards\", \"form\", and so on."
6437
+ },
6438
+ {
6439
+ "name": "uppercase",
6440
+ "description": "Uppercase the readout text. Default true."
6441
+ },
6442
+ {
6443
+ "name": "zIndex",
6444
+ "description": "Stacking order of the overlay. Default 2 — keep navigation above it."
6445
+ }
6446
+ ]
6288
6447
  }
6289
6448
  },
6290
6449
  "summary": {
6291
- "types": 35,
6292
- "resolved": 35,
6450
+ "types": 36,
6451
+ "resolved": 36,
6293
6452
  "unresolved": 0
6294
6453
  }
6295
6454
  }