nodality 1.2.9 → 1.2.11

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 (205) hide show
  1. package/bin/mcp-server.mjs +86 -0
  2. package/bin/nodality.js +12 -0
  3. package/bin/schema-cli.mjs +72 -0
  4. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  5. package/dist/animator.esm.js.LICENSE.txt +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.LICENSE.txt +1 -1
  9. package/dist/base.esm.js.LICENSE.txt +1 -1
  10. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  11. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  12. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  13. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  14. package/dist/bundle.umd.js +1 -1
  15. package/dist/bundle.umd.js.LICENSE.txt +1 -28
  16. package/dist/button.cjs.js.LICENSE.txt +1 -1
  17. package/dist/button.esm.js.LICENSE.txt +1 -1
  18. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  19. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  20. package/dist/center.cjs.js.LICENSE.txt +1 -1
  21. package/dist/center.esm.js.LICENSE.txt +1 -1
  22. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  23. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  24. package/dist/code.cjs.js.LICENSE.txt +1 -1
  25. package/dist/code.esm.js.LICENSE.txt +1 -1
  26. package/dist/container.cjs.js.LICENSE.txt +1 -1
  27. package/dist/container.esm.js.LICENSE.txt +1 -1
  28. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  29. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  30. package/dist/designer.cjs.js +1 -1
  31. package/dist/designer.cjs.js.LICENSE.txt +1 -28
  32. package/dist/designer.esm.js +1 -1
  33. package/dist/designer.esm.js.LICENSE.txt +1 -28
  34. package/dist/element-mapper.cjs.js +1 -1
  35. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  36. package/dist/element-mapper.esm.js +1 -1
  37. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  38. package/dist/finalresult.esm.js +1 -1
  39. package/dist/finalresult.esm.js.LICENSE.txt +1 -28
  40. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  41. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  42. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  43. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  44. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  45. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  46. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  47. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  48. package/dist/free.cjs.js.LICENSE.txt +1 -1
  49. package/dist/free.esm.js.LICENSE.txt +1 -1
  50. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  51. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  52. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  53. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  54. package/dist/image.cjs.js.LICENSE.txt +1 -1
  55. package/dist/image.esm.js.LICENSE.txt +1 -1
  56. package/dist/index.cjs.js +1 -1
  57. package/dist/index.cjs.js.LICENSE.txt +1 -28
  58. package/dist/index.d.ts +1 -1
  59. package/dist/index.esm.js +1 -1
  60. package/dist/index.esm.js.LICENSE.txt +1 -28
  61. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  62. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  63. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  64. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  65. package/dist/link.cjs.js.LICENSE.txt +1 -1
  66. package/dist/link.esm.js.LICENSE.txt +1 -1
  67. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  68. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  69. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  70. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  71. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  72. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  73. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  74. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  75. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  76. package/dist/picker.esm.js.LICENSE.txt +1 -1
  77. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  78. package/dist/progress.esm.js.LICENSE.txt +1 -1
  79. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  80. package/dist/radio.esm.js.LICENSE.txt +1 -1
  81. package/dist/range.cjs.js.LICENSE.txt +1 -1
  82. package/dist/range.esm.js.LICENSE.txt +1 -1
  83. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  84. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  85. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  86. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  87. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  88. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  89. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  90. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  91. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  92. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  93. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  94. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  95. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  96. package/dist/stack.esm.js.LICENSE.txt +1 -1
  97. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  98. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  99. package/dist/table.cjs.js.LICENSE.txt +1 -1
  100. package/dist/table.esm.js.LICENSE.txt +1 -1
  101. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  102. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  103. package/dist/text.cjs.js.LICENSE.txt +1 -1
  104. package/dist/text.esm.js.LICENSE.txt +1 -1
  105. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  106. package/dist/theme.esm.js.LICENSE.txt +1 -1
  107. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  108. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  109. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  110. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  111. package/dist/video.cjs.js.LICENSE.txt +1 -1
  112. package/dist/video.esm.js.LICENSE.txt +1 -1
  113. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  114. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  115. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  116. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  117. package/layout/animator.js +1 -1
  118. package/layout/audio.js +1 -1
  119. package/layout/audionew.js +1 -1
  120. package/layout/base.js +1 -1
  121. package/layout/beta-desktop-bar.js +1 -1
  122. package/layout/beta-mobile-bar.js +1 -1
  123. package/layout/button.js +1 -1
  124. package/layout/center.js +1 -1
  125. package/layout/checkbox.js +1 -1
  126. package/layout/circle.js +1 -1
  127. package/layout/code.js +1 -1
  128. package/layout/container.js +1 -1
  129. package/layout/dropdown-2025.js +1 -1
  130. package/layout/flex-card.js +1 -1
  131. package/layout/flex-grid.js +1 -1
  132. package/layout/flex-row.js +1 -1
  133. package/layout/form-components/custom.js +1 -1
  134. package/layout/form-components/data-list.js +1 -1
  135. package/layout/form-components/floating-input.js +1 -1
  136. package/layout/form-components/form-all.js +1 -1
  137. package/layout/form-components/form.js +1 -1
  138. package/layout/form-components/image-picker.js +1 -1
  139. package/layout/form-components/picker.js +1 -1
  140. package/layout/form-components/radio.js +1 -1
  141. package/layout/form-components/radiogroup.js +1 -1
  142. package/layout/form-components/range.js +1 -1
  143. package/layout/free.js +1 -1
  144. package/layout/grid-switcher.js +1 -1
  145. package/layout/grid.js +1 -1
  146. package/layout/horizontal-scroller.js +1 -1
  147. package/layout/image.js +1 -1
  148. package/layout/index.js +1 -1
  149. package/layout/link.js +1 -1
  150. package/layout/list.js +1 -1
  151. package/layout/meta-adder.js +1 -1
  152. package/layout/modal-2025.js +1 -1
  153. package/layout/morph.js +1 -1
  154. package/layout/multiswitcher.js +1 -1
  155. package/layout/nav-bar.js +1 -1
  156. package/layout/nav-factor/custom-div.js +1 -1
  157. package/layout/new-nav-bar.js +1 -1
  158. package/layout/polygon.js +1 -1
  159. package/layout/prerender-site.js +1 -1
  160. package/layout/prerender.js +1 -1
  161. package/layout/progress.js +1 -1
  162. package/layout/row.js +1 -1
  163. package/layout/scroll-video.js +1 -1
  164. package/layout/side-bar.js +1 -1
  165. package/layout/side-nav-bar.js +1 -1
  166. package/layout/simple-bar.js +1 -1
  167. package/layout/slider-2025.js +1 -1
  168. package/layout/spacer.js +1 -1
  169. package/layout/stack.js +1 -1
  170. package/layout/svg.js +1 -1
  171. package/layout/switcher.js +1 -1
  172. package/layout/table.js +1 -1
  173. package/layout/text-field.js +1 -1
  174. package/layout/text.js +1 -1
  175. package/layout/ulist.js +1 -1
  176. package/layout/video.js +1 -1
  177. package/layout/wrap.js +1 -1
  178. package/layout/zoom-card.js +1 -1
  179. package/lib/agent-surface.js +1 -1
  180. package/lib/card-getter.js +1 -1
  181. package/lib/codegen.js +1 -1
  182. package/lib/data.js +1 -1
  183. package/lib/designer.js +21 -1
  184. package/lib/element-mapper.js +173 -16
  185. package/lib/element-params.generated.js +11 -0
  186. package/lib/keyframe-animation.js +1 -1
  187. package/lib/link-getter.js +1 -1
  188. package/lib/morph-node.js +1 -1
  189. package/lib/parse-html.js +151 -0
  190. package/lib/raster-inspect.js +1 -1
  191. package/lib/raster-ops.js +1 -1
  192. package/lib/raster-presets.js +1 -1
  193. package/lib/scroll-video.js +1 -1
  194. package/lib/seo.js +1 -1
  195. package/lib/stacker.js +1 -1
  196. package/lib/suggest.js +1 -1
  197. package/lib/theme.js +1 -1
  198. package/lib/transform-anim.js +1 -1
  199. package/lib/transition.js +1 -1
  200. package/lib/validate-nodes.js +112 -2
  201. package/lib/webmcp-adapter.js +1 -1
  202. package/package.json +5 -2
  203. package/schema.json +7953 -0
  204. package/scripts/generate-schema.mjs +251 -0
  205. package/skills/nodality/SKILL.md +71 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.9
2
+ * nodality v1.2.11
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -83,6 +83,16 @@ const ELEMENT_TYPES = [
83
83
  "simple", "copy", "wrap", "circle", "polygon", "code", "table", "ulist",
84
84
  ];
85
85
 
86
+ // A nav entry is {title, link}. A bare string is accepted as the title: that
87
+ // is the shape callers were already passing while `items` was ignored, and
88
+ // ignoring it was the defect Stage 4 fixes - throwing on it would be a worse
89
+ // one. Missing fields are filled rather than left undefined, because the
90
+ // mapper derives an id from the title and a missing title threw.
91
+ const navEntry = (i) =>
92
+ (typeof i === "string" || typeof i === "number")
93
+ ? { title: String(i), link: "#e" }
94
+ : { ...i, title: String(i?.title ?? ""), link: i?.link ?? "#e" };
95
+
86
96
  class ElementMapper { // 22:09:58 04/11/2024
87
97
  static mapType(obj) {
88
98
  // console.log("LOBJ");
@@ -172,12 +182,19 @@ class ElementMapper { // 22:09:58 04/11/2024
172
182
 
173
183
  let el = obj.el;
174
184
 
175
- return new Table()
176
- .add([
185
+ // Phase S4. A table's rows are DATA, not element specs, so `items` takes
186
+ // row objects and the columns follow from their keys. Without items the
187
+ // three placeholder rows are emitted exactly as before — the same
188
+ // non-breaking rule as cards and row.
189
+ const rows = Array.isArray(el.items) && el.items.length
190
+ ? el.items
191
+ : [
177
192
  { abbr: "AUIUI/AK9PT", name: "Pokročilé mobilní technologie", grade: "A", date: "09.01.2024" },
178
193
  { abbr: "AUIUI/AK9PT", name: "Pokročilé mobilní technologie", grade: "A", date: "09.01.2024" },
179
194
  { abbr: "AUIUI/AK9PT", name: "Pokročilé mobilní technologie", grade: "A", date: "09.01.2024" }
180
- ])
195
+ ];
196
+ return new Table()
197
+ .add(rows)
181
198
  .set({
182
199
  cellPadding: "0.3em",
183
200
  cellAlign: "center",
@@ -231,6 +248,13 @@ class ElementMapper { // 22:09:58 04/11/2024
231
248
  raster: this.filteroRaster(el.id, obj.customOptions),
232
249
  }).items([
233
250
 
251
+ // Phase S4. `children` come from the element, mapped like any other
252
+ // subtree — the fix mapWrap got in P3 and mapGrid in S1. Absent, the
253
+ // three placeholders below are emitted exactly as before, so every
254
+ // existing page is unaffected.
255
+ ...(Array.isArray(el.children) && el.children.length
256
+ ? el.children.map((child) => this.mapType({ ...obj, el: child }))
257
+ : [
234
258
  new Text("This").set({
235
259
  size: "S3"
236
260
  }),
@@ -244,7 +268,7 @@ new Text("row.").set({
244
268
  size: "S3",
245
269
  text: "Hello"
246
270
  })
247
-
271
+ ])
248
272
  ])
249
273
 
250
274
 
@@ -675,12 +699,29 @@ alert("PP")
675
699
  // console.log(ft);
676
700
 
677
701
 
678
- let items = [
702
+ // Phase S4. Nav entries are DATA — {title, link} — so `items`
703
+ // carries them. Absent, the three placeholders are used exactly as
704
+ // before, which is what every existing page renders today.
705
+ let items = Array.isArray(obj.el?.items) && obj.el.items.length
706
+ ? obj.el.items.map(navEntry)
707
+ : [
679
708
  {title: "Fire", link: "@e"},
680
709
  {title: "and", link: "@e"},
681
710
  {title: "smoke", link: "@e"}
682
711
  ];
683
712
 
713
+ // The `links` built below feed the UINavBar block that is commented
714
+ // out further down, so they are not what a page renders. The live nav
715
+ // is the Switcher: two views, each carrying its own hardcoded link
716
+ // list. `navItems` is what drives those. It is null when the caller
717
+ // supplied nothing, which leaves the literals - and therefore every
718
+ // existing page - exactly as they were.
719
+ const navItems = Array.isArray(obj.el?.items) && obj.el.items.length
720
+ ? obj.el.items.map(navEntry)
721
+ : null;
722
+ const navLinks = () => navItems.map((i) =>
723
+ new Link().set({ text: i.title, url: i.link }));
724
+
684
725
  let links = items.map((i, o) => new Link(`"${i.title}"`)
685
726
  .set({
686
727
  fluidc: "S6",
@@ -844,7 +885,9 @@ if (obj.el.dropdown){
844
885
  radius: "1rem",
845
886
  }).add([
846
887
 
847
- new Link().set({text: "About", url: "#a"}),//.render()
888
+ ...(navItems
889
+ ? navLinks()
890
+ : [new Link().set({text: "About", url: "#a"})]),//.render()
848
891
 
849
892
 
850
893
 
@@ -901,8 +944,13 @@ if (obj.el.dropdown){
901
944
 
902
945
 
903
946
 
947
+ // Supplied entries are emitted above, ahead of the dropdown, so
948
+ // these two trailing placeholders drop out rather than appending
949
+ // themselves to the end of the caller's nav.
950
+ ...(navItems ? [] : [
904
951
  new Link("B").set({text: "About", url: "#b"}),//.render()
905
952
  new Link("C").set({text: "Contact", url: "https://www.abcnews.com"})//.render()
953
+ ])
906
954
  ])//.addNavItem(new Link("C").set({}).render())
907
955
  },
908
956
 
@@ -1013,9 +1061,11 @@ new Wrapper().set({
1013
1061
 
1014
1062
 
1015
1063
 
1064
+ ...(navItems ? navLinks() : [
1016
1065
  new Link().set({ text: "About", url: "#a"}),//.render()
1017
1066
  new Link().set({ text: "Services", url: "#a"}),//.render()
1018
1067
  new Link().set({ text: "Contact", url: "#a"})//.render()
1068
+ ]),
1019
1069
  // new Link("C").set({})//.render()
1020
1070
  ])
1021
1071
  /*.addNavItem(
@@ -1062,7 +1112,11 @@ new Wrapper().set({
1062
1112
 
1063
1113
 
1064
1114
 
1065
- let items = [
1115
+ // Phase S4, as protoNav above: `items` carries {title, link}; absent,
1116
+ // the placeholders are used exactly as before.
1117
+ let items = Array.isArray(obj.el?.items) && obj.el.items.length
1118
+ ? obj.el.items.map(navEntry)
1119
+ : [
1066
1120
  {title: "Home", link: "@e"},
1067
1121
  {title: "Projects", link: "@e"},
1068
1122
  {title: "Services", link: "@e"}
@@ -1246,10 +1300,17 @@ if (obj.el.dropdown){
1246
1300
  // pad: {sides: ["all"], value: "1rem"}
1247
1301
  }),
1248
1302
 
1249
- obj.el.animation ? animatedLinks[0] : links[0],
1250
- obj.el.animation ? animatedLinks[1] : links[1],
1251
- obj.el.animation ? animatedLinks[2] : links[2],
1252
- (obj.el.animation && obj.el.dropdown) ? animatedLinks[3] : new Text("").set({}).excludeFromCode(),
1303
+ // Phase S4. These were three fixed slots, which assumed exactly
1304
+ // three nav entries. Once `items` could supply any number,
1305
+ // links[1] and links[2] were `undefined` and Wrapper.add called
1306
+ // toCode() on them a throw, not a silent no-op. One slot per
1307
+ // item instead; with the default three the emitted list is
1308
+ // unchanged, and the dropdown slot after it still indexes the
1309
+ // entry the pushes above appended.
1310
+ ...(obj.el.animation ? animatedLinks : links).slice(0, items.length).filter(Boolean),
1311
+ (obj.el.animation && obj.el.dropdown)
1312
+ ? animatedLinks[items.length]
1313
+ : new Text("").set({}).excludeFromCode(),
1253
1314
 
1254
1315
  new Text("Company, 2025")
1255
1316
  .set({ // no ID, no animation
@@ -1690,15 +1751,111 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
1690
1751
  }
1691
1752
  })
1692
1753
  .items(
1693
- [
1754
+ ${this.gridItemsSource(obj, cardTemplate)}
1755
+ )
1756
+ `;
1757
+ }
1758
+
1759
+ /**
1760
+ * The `.items(...)` argument for a `cards` grid.
1761
+ *
1762
+ * Phase S1. Until now this was a hardcoded array of three space cards
1763
+ * inside a template literal, so `{type:"cards"}` rendered the same
1764
+ * Starship/Saturn V/Shuttle grid whatever the caller declared — an
1765
+ * element type with no way to carry content. `items` supplies it.
1766
+ *
1767
+ * Three shapes, chosen per entry so a mixed array works:
1768
+ *
1769
+ * absent the three placeholder cards, EXACTLY as before.
1770
+ * This path emits a byte-identical string to the
1771
+ * pre-S1 mapper and is covered by a regression test;
1772
+ * the change ships behind no flag because nothing
1773
+ * that works today can observe it.
1774
+ *
1775
+ * {img,title,link} shorthand. The same card template as before, over
1776
+ * the caller's data instead of the placeholders.
1777
+ * Kept because it is what the published benchmark
1778
+ * and Table 3 of the PeerJ submission already write.
1779
+ *
1780
+ * [ {type:…}, … ] nested specs: a card whose children are ordinary
1781
+ * element descriptors, mapped through `mapType` like
1782
+ * any other subtree and emitted with `toCode()`.
1783
+ * This is the shape that makes the format TOTAL —
1784
+ * any element can appear inside a card without a new
1785
+ * schema per composite.
1786
+ *
1787
+ * Data in, code out: every branch still emits `new Card().set({...})`
1788
+ * source the developer owns. The template became parameterised; it did
1789
+ * not become an interpreter.
1790
+ */
1791
+ static gridItemsSource(obj, cardTemplate) {
1792
+ const items = obj.el.items;
1793
+
1794
+ // No items: the pre-S1 string, character for character.
1795
+ if (!Array.isArray(items) || items.length === 0) {
1796
+ return `[
1694
1797
  { img: "https://upload.wikimedia.org/wikipedia/commons/3/3a/Starship_S20.jpg", title: "Starship", link: "#ship" },
1695
1798
  { img: "https://upload.wikimedia.org/wikipedia/commons/1/16/Apollo_11_Launch_-_GPN-2000-000630.jpg", title: "Saturn V", link: "#saturn" },
1696
1799
  { img: "https://upload.wikimedia.org/wikipedia/commons/d/d6/STS120LaunchHiRes-edit1.jpg", title: "Shuttle", link: "#shuttle" }
1697
1800
  ].map(item =>
1698
1801
  ${cardTemplate}
1699
- )
1700
- )
1701
- `;
1802
+ )`;
1803
+ }
1804
+
1805
+ const isSpecList = (it) => Array.isArray(it);
1806
+ const shorthand = items.filter((it) => !isSpecList(it));
1807
+ const specLists = items.filter(isSpecList);
1808
+
1809
+ // Every entry is shorthand: keep the `.map()` form, so the emitted
1810
+ // code stays as compact as the hand-written equivalent and the
1811
+ // template is still paid once.
1812
+ if (specLists.length === 0) {
1813
+ return `${JSON.stringify(shorthand, null, 2).split("\n").map((l, i) => i === 0 ? l : " " + l).join("\n")}.map(item =>
1814
+ ${cardTemplate}
1815
+ )`;
1816
+ }
1817
+
1818
+ // At least one entry is a spec list, so the cards differ and a single
1819
+ // template cannot serve them. Emit each card explicitly.
1820
+ const cards = items.map((it) => isSpecList(it)
1821
+ ? this.cardFromSpecs(obj, it)
1822
+ : this.cardFromShorthand(it));
1823
+ return `[
1824
+ ${cards.join(",\n ")}
1825
+ ]`;
1826
+ }
1827
+
1828
+ /** A card whose children are element specs, mapped like any other subtree. */
1829
+ static cardFromSpecs(obj, specs) {
1830
+ const kids = specs.map((child) => {
1831
+ const mapped = this.mapType({ ...obj, el: child });
1832
+ // mapType returns an instance for most types and SOURCE for the
1833
+ // few that emit source themselves (grid, copy, wrap-as-source).
1834
+ // A string is already code; an instance knows how to become code.
1835
+ if (typeof mapped === "string") return mapped.trim();
1836
+ const code = mapped && typeof mapped.toCode === "function"
1837
+ ? [].concat(mapped.toCode()).join("")
1838
+ : "";
1839
+ return code.trim();
1840
+ }).filter(Boolean);
1841
+
1842
+ return `new Card()
1843
+ .set({ width: "300px", height: "700px", radius: "0.7rem", mar: { sides: ["all"], value: "0.8rem" } })
1844
+ .items([
1845
+ ${kids.join(",\n ")}
1846
+ ])`;
1847
+ }
1848
+
1849
+ /** A card built from {img, title, link} shorthand, matching the template. */
1850
+ static cardFromShorthand(item) {
1851
+ const s = (v) => JSON.stringify(v ?? "");
1852
+ return `new Card()
1853
+ .set({ width: "300px", height: "700px", radius: "0.7rem", mar: { sides: ["all"], value: "0.8rem" } })
1854
+ .items([
1855
+ new Image(${s(item.img)}).set({isFull: true, url: ${s(item.img)}}),
1856
+ new Text(${s(item.title)}).set({ size: "S5", color: "#f97316" }),
1857
+ new Link("Link").set({ text: ${s(item.title)}, url: ${s(item.link)}, background: "#3498db", pad: [{ lr: "0.5rem", tb: "1rem" }], radius: "0.4rem", color: "white" })
1858
+ ])`;
1702
1859
  }
1703
1860
 
1704
1861
 
@@ -0,0 +1,11 @@
1
+ /*!
2
+ * nodality v1.2.11
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ // GENERATED by scripts/generate-schema.mjs — do not edit.
8
+ // Every parameter name any element type reads, recovered from source.
9
+ // Used for near-miss typo detection in validate-nodes.js. A drift test
10
+ // regenerates this and fails if it differs, so it cannot rot silently.
11
+ export const ELEMENT_PARAM_NAMES = ["absolute","accept","action","align","alignIts","animate","animation","area","arrayMargin","arrayPadding","at","auto","backTo","background","backgroundCard","backgroundColor","backgroundOp","behaviour","blast","blastData","block","bold","border","borderObj","borderObja","borderRadius","brand","breakWord","breakpoints","cellAlign","cellPadding","center","centerColumn","centered","checkedBackgroundColor","children","clampc","class","clipPath","code","color","column","contentWidth","cursor","customAlign","customJustify","customStyle","data","diameter","disp","dropdown","em","exact","filter","filtera","fixMobileTap","fixed","flex","flexCenter","flexDir","fluid","fluidc","font","frame","ga","gap","gpos","gradient","hamColour","hamburgerColour","headStyle","height","hideField","hover","icon","id","img","index","initLetter","inline","inpad","inputType","isFull","isHidden","isNavA","italic","items","keySet","kind","label","layout","left","length","link","map","mar","margin","marginOp","maxHeight","maxWidth","mboth","method","mobile","multiple","name","new","nowrap","offcanvas","okayColor","onScroll","onTap","opacity","overflow","pad","padding","placeholder","position","positions","preffersId","radius","ranges","raster","react","removeDecoration","required","resmar","respad","responsive","resprop","rounded","scale","sequence","shadow","sides","simpleBorder","simpleCenter","size","span","sticky","stretch","stroke","style","svg","tags","templateCols","text","theme","tint","title","top","transform","transition","type","url","useBrightness","value","vtn","weight","width","x","y","zIndex"];
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.9
2
+ * nodality v1.2.11
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.9
2
+ * nodality v1.2.11
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.2.9
2
+ * nodality v1.2.11
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -0,0 +1,151 @@
1
+ /*!
2
+ * nodality v1.2.11
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ /**
8
+ * Stage 5 of AGENTIC-FIRST-PLAN.md — the round-trip.
9
+ *
10
+ * parse(html) -> spec such that render(parse(html)) === html
11
+ *
12
+ * The plan expected this to be partial at first, and measurement decided
13
+ * exactly where the partial line falls.
14
+ *
15
+ * WHAT THE MEASUREMENT SAID. Rendering all 35 declared types in isolation and
16
+ * comparing their DOM signatures: 13 of them — `cards`, `nav`, `sideNav`,
17
+ * `row`, `dropdown`, `radio`, `labelInput`, `filePicker`, `checkbox`, `wrap`,
18
+ * `circle`, `polygon`, `table` — produce a bare `<div>` with no class and no
19
+ * attributes. Nothing in the output says which one produced it. Looking two
20
+ * levels deeper separates 24 of 30, but only for the placeholder renders it
21
+ * was calibrated on: a `row` holding an `h2` and a `wrap` holding an `h2` are
22
+ * the same shape, so a signature table built from empty composites does not
23
+ * match a page with content in it. Structural recognition of a composite is
24
+ * therefore guesswork, and a parser that guesses wrong hands an agent a spec
25
+ * that renders a different page.
26
+ *
27
+ * SO THERE ARE TWO TIERS, and they are not presented as one.
28
+ *
29
+ * exact The page carries its own spec: `set({annotate: true})` writes
30
+ * each element's descriptor onto the node it produced. parse
31
+ * reads it back. Round-trip is exact for every type, including
32
+ * every style option, because nothing is being inferred.
33
+ *
34
+ * structural No annotation: recover what the tag alone determines —
35
+ * headings, paragraphs, links, images, lists. These are the
36
+ * leaf types, which is the case the paper measures, and their
37
+ * tag is not shared with anything ambiguous.
38
+ *
39
+ * Annotation is OPT-IN and off by default. It writes attributes into the
40
+ * output, and the whole project rests on existing pages rendering byte for
41
+ * byte as they always did.
42
+ *
43
+ * The 1:1 correspondence between descriptors and mounted children that
44
+ * `annotate` relies on is not a new assumption: morph nodes already resolve
45
+ * `from`/`to` against the rendered children by position, for the same reason
46
+ * (components do not all carry their id into the DOM). It was re-measured for
47
+ * this stage across mixed leaf-and-composite specs.
48
+ */
49
+
50
+ import { validateNodes } from "./validate-nodes.js";
51
+
52
+ const HEADINGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
53
+
54
+ /** The attribute a page uses to carry its own descriptors. */
55
+ export const SPEC_ATTR = "data-nod";
56
+
57
+ /**
58
+ * Write each descriptor onto the child it produced. Returns how many were
59
+ * written, which is what the caller needs to know if the two ever diverge.
60
+ */
61
+ export function annotateRoundTrip(host, elements) {
62
+ if (!host || !Array.isArray(elements)) return 0;
63
+ const kids = Array.from(host.children || []);
64
+ let written = 0;
65
+ for (let i = 0; i < kids.length && i < elements.length; i++) {
66
+ try {
67
+ kids[i].setAttribute(SPEC_ATTR, JSON.stringify(elements[i]));
68
+ written++;
69
+ } catch (e) { /* a node that refuses attributes is skipped, not fatal */ }
70
+ }
71
+ return written;
72
+ }
73
+
74
+ /**
75
+ * Recover what the tag alone determines. Deliberately narrow: returning a
76
+ * confident guess for a composite would be worse than returning nothing,
77
+ * because the caller re-renders whatever comes back.
78
+ */
79
+ export function inferNode(node) {
80
+ const tag = (node.tagName || "").toLowerCase();
81
+ if (HEADINGS.has(tag)) return { type: tag, text: node.textContent };
82
+ if (tag === "p") return { type: "p", text: node.textContent };
83
+ if (tag === "a") {
84
+ return { type: "a", text: node.textContent, url: node.getAttribute("href") || "" };
85
+ }
86
+ if (tag === "img") return { type: "img", url: node.getAttribute("src") || "" };
87
+ if (tag === "ul") {
88
+ return { type: "ulist", items: Array.from(node.children).map((c) => c.textContent) };
89
+ }
90
+ return null;
91
+ }
92
+
93
+ /** One node to one descriptor: the carried spec if there is one, else inference. */
94
+ export function parseNode(node) {
95
+ if (!node || node.nodeType !== 1) return null;
96
+ const carried = node.getAttribute && node.getAttribute(SPEC_ATTR);
97
+ if (carried) {
98
+ try { return JSON.parse(carried); }
99
+ catch (e) { /* a corrupted attribute falls through to inference */ }
100
+ }
101
+ return inferNode(node);
102
+ }
103
+
104
+ /**
105
+ * parse(html) -> spec. `doc` is only needed off-browser, where the caller
106
+ * supplies a document; the library's own globals are shadowed by the bridge,
107
+ * so this builds its container through createElement rather than a
108
+ * constructor.
109
+ */
110
+ export function parseHTML(html, doc) {
111
+ const d = doc || (typeof document !== "undefined" ? document : null);
112
+ if (!d) throw new Error("[nodality] parseHTML needs a document");
113
+ const box = d.createElement("div");
114
+ box.innerHTML = String(html == null ? "" : html);
115
+ return Array.from(box.children).map(parseNode).filter(Boolean);
116
+ }
117
+
118
+ /**
119
+ * What parse could NOT recover, so a caller can tell a partial read from a
120
+ * complete one instead of discovering it at render time.
121
+ */
122
+ export function parseReport(html, doc) {
123
+ const d = doc || (typeof document !== "undefined" ? document : null);
124
+ if (!d) throw new Error("[nodality] parseReport needs a document");
125
+ const box = d.createElement("div");
126
+ box.innerHTML = String(html == null ? "" : html);
127
+ const kids = Array.from(box.children);
128
+ const spec = [], unrecovered = [];
129
+ kids.forEach((node, i) => {
130
+ const one = parseNode(node);
131
+ if (one) spec.push(one);
132
+ else unrecovered.push({ index: i, tag: (node.tagName || "").toLowerCase() });
133
+ });
134
+ // What parse returns is UNTRUSTED. A `data-nod` attribute is just text in
135
+ // a document: hand-edited, served by someone else, or written by a version
136
+ // of the library that is not this one. The caller's next move is to render
137
+ // it, so the recovered spec is checked here rather than after it has
138
+ // already become a page. This is also the first place Stage 3 and Stage 5
139
+ // compose — the validator that makes a spec repairable is the one that
140
+ // makes a parsed spec safe.
141
+ const check = validateNodes([], spec);
142
+
143
+ return {
144
+ ok: unrecovered.length === 0 && check.ok,
145
+ exact: kids.every((n) => n.getAttribute && n.getAttribute(SPEC_ATTR)),
146
+ total: kids.length,
147
+ spec,
148
+ unrecovered,
149
+ errors: check.errors,
150
+ };
151
+ }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.9
2
+ * nodality v1.2.11
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.2.9
2
+ * nodality v1.2.11
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.9
2
+ * nodality v1.2.11
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.9
2
+ * nodality v1.2.11
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.2.9
2
+ * nodality v1.2.11
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.2.9
2
+ * nodality v1.2.11
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.2.9
2
+ * nodality v1.2.11
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.2.9
2
+ * nodality v1.2.11
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.9
2
+ * nodality v1.2.11
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.2.9
2
+ * nodality v1.2.11
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */