nodality 1.2.11 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/bin/mcp-server.mjs +45 -0
  2. package/dist/animator.cjs.js +1 -1
  3. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  4. package/dist/animator.esm.js +1 -1
  5. package/dist/animator.esm.js.LICENSE.txt +1 -1
  6. package/dist/audionew.cjs.js +1 -1
  7. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  8. package/dist/audionew.esm.js +1 -1
  9. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  10. package/dist/base.cjs.js +1 -1
  11. package/dist/base.cjs.js.LICENSE.txt +1 -1
  12. package/dist/base.esm.js +1 -1
  13. package/dist/base.esm.js.LICENSE.txt +1 -1
  14. package/dist/beta-desktop-bar.cjs.js +1 -1
  15. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  16. package/dist/beta-desktop-bar.esm.js +1 -1
  17. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  18. package/dist/beta-mobile-bar.cjs.js +1 -1
  19. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  20. package/dist/beta-mobile-bar.esm.js +1 -1
  21. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  22. package/dist/bundle.umd.js +1 -1
  23. package/dist/bundle.umd.js.LICENSE.txt +1 -1
  24. package/dist/button.cjs.js +1 -1
  25. package/dist/button.cjs.js.LICENSE.txt +1 -1
  26. package/dist/button.esm.js +1 -1
  27. package/dist/button.esm.js.LICENSE.txt +1 -1
  28. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  29. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  30. package/dist/center.cjs.js +1 -1
  31. package/dist/center.cjs.js.LICENSE.txt +1 -1
  32. package/dist/center.esm.js +1 -1
  33. package/dist/center.esm.js.LICENSE.txt +1 -1
  34. package/dist/checkbox.cjs.js +1 -1
  35. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  36. package/dist/checkbox.esm.js +1 -1
  37. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  38. package/dist/code.cjs.js +1 -1
  39. package/dist/code.cjs.js.LICENSE.txt +1 -1
  40. package/dist/code.esm.js +1 -1
  41. package/dist/code.esm.js.LICENSE.txt +1 -1
  42. package/dist/container.cjs.js +1 -1
  43. package/dist/container.cjs.js.LICENSE.txt +1 -1
  44. package/dist/container.esm.js +1 -1
  45. package/dist/container.esm.js.LICENSE.txt +1 -1
  46. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  47. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  48. package/dist/designer.cjs.js +1 -1
  49. package/dist/designer.cjs.js.LICENSE.txt +1 -1
  50. package/dist/designer.esm.js +1 -1
  51. package/dist/designer.esm.js.LICENSE.txt +1 -1
  52. package/dist/element-mapper.cjs.js +1 -1
  53. package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
  54. package/dist/element-mapper.esm.js +1 -1
  55. package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
  56. package/dist/finalresult.esm.js +1 -1
  57. package/dist/finalresult.esm.js.LICENSE.txt +1 -1
  58. package/dist/flex-card.cjs.js +1 -1
  59. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  60. package/dist/flex-card.esm.js +1 -1
  61. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  62. package/dist/flex-grid.cjs.js +1 -1
  63. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  64. package/dist/flex-grid.esm.js +1 -1
  65. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  66. package/dist/flex-row.cjs.js +1 -1
  67. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  68. package/dist/flex-row.esm.js +1 -1
  69. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  70. package/dist/floating-input.cjs.js +1 -1
  71. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  72. package/dist/floating-input.esm.js +1 -1
  73. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  74. package/dist/free.cjs.js +1 -1
  75. package/dist/free.cjs.js.LICENSE.txt +1 -1
  76. package/dist/free.esm.js +1 -1
  77. package/dist/free.esm.js.LICENSE.txt +1 -1
  78. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  79. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  80. package/dist/image-picker.cjs.js +1 -1
  81. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  82. package/dist/image-picker.esm.js +1 -1
  83. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  84. package/dist/image.cjs.js +1 -1
  85. package/dist/image.cjs.js.LICENSE.txt +1 -1
  86. package/dist/image.esm.js +1 -1
  87. package/dist/image.esm.js.LICENSE.txt +1 -1
  88. package/dist/index.cjs.js +1 -1
  89. package/dist/index.cjs.js.LICENSE.txt +1 -1
  90. package/dist/index.d.ts +3 -2
  91. package/dist/index.esm.js +1 -1
  92. package/dist/index.esm.js.LICENSE.txt +1 -1
  93. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  94. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  95. package/dist/link-getter.cjs.js +1 -1
  96. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  97. package/dist/link-getter.esm.js +1 -1
  98. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  99. package/dist/link.cjs.js +1 -1
  100. package/dist/link.cjs.js.LICENSE.txt +1 -1
  101. package/dist/link.esm.js +1 -1
  102. package/dist/link.esm.js.LICENSE.txt +1 -1
  103. package/dist/meta-adder.cjs.js +1 -1
  104. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  105. package/dist/meta-adder.esm.js +1 -1
  106. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  107. package/dist/modal-2025.cjs.js +1 -1
  108. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  109. package/dist/modal-2025.esm.js +1 -1
  110. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  111. package/dist/multiswitcher.cjs.js +1 -1
  112. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  113. package/dist/multiswitcher.esm.js +1 -1
  114. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  115. package/dist/new-nav-bar.cjs.js +1 -1
  116. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  117. package/dist/new-nav-bar.esm.js +1 -1
  118. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  119. package/dist/picker.cjs.js +1 -1
  120. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  121. package/dist/picker.esm.js +1 -1
  122. package/dist/picker.esm.js.LICENSE.txt +1 -1
  123. package/dist/progress.cjs.js +1 -1
  124. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  125. package/dist/progress.esm.js +1 -1
  126. package/dist/progress.esm.js.LICENSE.txt +1 -1
  127. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  128. package/dist/radio.esm.js.LICENSE.txt +1 -1
  129. package/dist/range.cjs.js.LICENSE.txt +1 -1
  130. package/dist/range.esm.js.LICENSE.txt +1 -1
  131. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  132. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  133. package/dist/side-bar.cjs.js +1 -1
  134. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  135. package/dist/side-bar.esm.js +1 -1
  136. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  137. package/dist/side-nav-bar.cjs.js +1 -1
  138. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  139. package/dist/side-nav-bar.esm.js +1 -1
  140. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  141. package/dist/simple-bar.cjs.js +1 -1
  142. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  143. package/dist/simple-bar.esm.js +1 -1
  144. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  145. package/dist/slider-2025.cjs.js +1 -1
  146. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  147. package/dist/slider-2025.esm.js +1 -1
  148. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  149. package/dist/spacer.cjs.js +1 -1
  150. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  151. package/dist/spacer.esm.js +1 -1
  152. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  153. package/dist/stack.cjs.js +1 -1
  154. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  155. package/dist/stack.esm.js +1 -1
  156. package/dist/stack.esm.js.LICENSE.txt +1 -1
  157. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  158. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  159. package/dist/table.cjs.js +1 -1
  160. package/dist/table.cjs.js.LICENSE.txt +1 -1
  161. package/dist/table.esm.js +1 -1
  162. package/dist/table.esm.js.LICENSE.txt +1 -1
  163. package/dist/text-field.cjs.js +1 -1
  164. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  165. package/dist/text-field.esm.js +1 -1
  166. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  167. package/dist/text.cjs.js +1 -1
  168. package/dist/text.cjs.js.LICENSE.txt +1 -1
  169. package/dist/text.esm.js +1 -1
  170. package/dist/text.esm.js.LICENSE.txt +1 -1
  171. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  172. package/dist/theme.esm.js.LICENSE.txt +1 -1
  173. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  174. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  175. package/dist/ulist.cjs.js +1 -1
  176. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  177. package/dist/ulist.esm.js +1 -1
  178. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  179. package/dist/video.cjs.js +1 -1
  180. package/dist/video.cjs.js.LICENSE.txt +1 -1
  181. package/dist/video.esm.js +1 -1
  182. package/dist/video.esm.js.LICENSE.txt +1 -1
  183. package/dist/wrap.cjs.js +1 -1
  184. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  185. package/dist/wrap.esm.js +1 -1
  186. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  187. package/dist/zoom-card.cjs.js +1 -1
  188. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  189. package/dist/zoom-card.esm.js +1 -1
  190. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  191. package/layout/animator.js +15 -2
  192. package/layout/audio.js +1 -1
  193. package/layout/audionew.js +1 -1
  194. package/layout/base.js +1 -1
  195. package/layout/beta-desktop-bar.js +1 -1
  196. package/layout/beta-mobile-bar.js +1 -1
  197. package/layout/button.js +1 -1
  198. package/layout/center.js +1 -1
  199. package/layout/checkbox.js +1 -1
  200. package/layout/circle.js +1 -1
  201. package/layout/code.js +1 -1
  202. package/layout/container.js +1 -1
  203. package/layout/dropdown-2025.js +10 -3
  204. package/layout/flex-card.js +1 -1
  205. package/layout/flex-grid.js +1 -1
  206. package/layout/flex-row.js +1 -1
  207. package/layout/form-components/custom.js +1 -1
  208. package/layout/form-components/data-list.js +1 -1
  209. package/layout/form-components/floating-input.js +14 -1
  210. package/layout/form-components/form-all.js +1 -1
  211. package/layout/form-components/form.js +1 -1
  212. package/layout/form-components/image-picker.js +5 -3
  213. package/layout/form-components/picker.js +1 -1
  214. package/layout/form-components/radio.js +1 -1
  215. package/layout/form-components/radiogroup.js +1 -1
  216. package/layout/form-components/range.js +1 -1
  217. package/layout/free.js +1 -1
  218. package/layout/grid-switcher.js +1 -1
  219. package/layout/grid.js +1 -1
  220. package/layout/horizontal-scroller.js +1 -1
  221. package/layout/image.js +15 -2
  222. package/layout/index.js +1 -1
  223. package/layout/link.js +1 -1
  224. package/layout/list.js +1 -1
  225. package/layout/meta-adder.js +1 -1
  226. package/layout/modal-2025.js +1 -1
  227. package/layout/morph.js +1 -1
  228. package/layout/multiswitcher.js +1 -1
  229. package/layout/nav-bar.js +1 -1
  230. package/layout/nav-factor/custom-div.js +1 -1
  231. package/layout/new-nav-bar.js +1 -1
  232. package/layout/polygon.js +1 -1
  233. package/layout/prerender-site.js +1 -1
  234. package/layout/prerender.js +1 -1
  235. package/layout/progress.js +1 -1
  236. package/layout/row.js +1 -1
  237. package/layout/scroll-video.js +1 -1
  238. package/layout/side-bar.js +11 -3
  239. package/layout/side-nav-bar.js +16 -3
  240. package/layout/simple-bar.js +1 -1
  241. package/layout/slider-2025.js +1 -1
  242. package/layout/spacer.js +1 -1
  243. package/layout/stack.js +1 -1
  244. package/layout/svg.js +1 -1
  245. package/layout/switcher.js +1 -1
  246. package/layout/table.js +1 -1
  247. package/layout/text-field.js +16 -1
  248. package/layout/text.js +28 -1
  249. package/layout/ulist.js +1 -1
  250. package/layout/video.js +1 -1
  251. package/layout/wrap.js +1 -1
  252. package/layout/zoom-card.js +1 -1
  253. package/lib/agent-surface.js +1 -1
  254. package/lib/card-getter.js +1 -1
  255. package/lib/check-page.js +239 -0
  256. package/lib/codegen.js +1 -1
  257. package/lib/data.js +1 -1
  258. package/lib/designer.js +64 -2
  259. package/lib/element-mapper.js +113 -23
  260. package/lib/element-params.generated.js +2 -2
  261. package/lib/keyframe-animation.js +1 -1
  262. package/lib/link-getter.js +1 -1
  263. package/lib/morph-node.js +8 -1
  264. package/lib/normalize-spec.js +134 -0
  265. package/lib/parse-html.js +1 -1
  266. package/lib/raster-inspect.js +1 -1
  267. package/lib/raster-ops.js +1 -1
  268. package/lib/raster-presets.js +1 -1
  269. package/lib/resolve-refs.js +121 -0
  270. package/lib/scroll-video.js +1 -1
  271. package/lib/seo.js +1 -1
  272. package/lib/stacker.js +1 -1
  273. package/lib/suggest.js +1 -1
  274. package/lib/theme.js +1 -1
  275. package/lib/transform-anim.js +1 -1
  276. package/lib/transition.js +1 -1
  277. package/lib/validate-nodes.js +60 -3
  278. package/lib/webmcp-adapter.js +1 -1
  279. package/package.json +3 -1
  280. package/schema.json +211 -12
  281. package/skills/nodality/SKILL.md +50 -0
@@ -0,0 +1,121 @@
1
+ /*!
2
+ * nodality v1.3.0
3
+ * (c) 2026 Filip Vabrousek
4
+ * License: MIT
5
+ */
6
+
7
+ /**
8
+ * Tier 3 of AGENTIC-FIRST-PLAN.md §10 — declare shared structure once.
9
+ *
10
+ * MEASURED FIRST, as the plan asked. Chrome taken from a real site: a nav and
11
+ * a footer are 184 tokens, and **61% of every token in that site is the same
12
+ * nav and footer**. Referencing rather than repeating them is 1.98x at ten
13
+ * pages and 2.10x at twenty — and a NET LOSS at one page, which is why this is
14
+ * opt-in and why nothing changes for a caller who never uses it.
15
+ *
16
+ * The reason it is needed is not that the data format is verbose. The site
17
+ * used for that measurement does not repeat itself: it imports `renderNav`
18
+ * from a shared module and calls it. That is the imperative layer, where reuse
19
+ * is just a function. An element array cannot call a function, so an agent
20
+ * authoring as data pays for the chrome once per page, and the only way out is
21
+ * to drop into code — which is what property 1, "total", exists to prevent.
22
+ * A human never feels this and an agent always does.
23
+ *
24
+ * new Des()
25
+ * .defs({ nav: { type: "nav", items: [...] } })
26
+ * .add([{ $ref: "nav" }, { type: "h1", text: "Page" }])
27
+ *
28
+ * The definitions are ordinary data, so they serialise, ship in one file, and
29
+ * are imported by every page's entry — the sharing is a plain import of a
30
+ * plain object, not a new authoring mode.
31
+ */
32
+
33
+ /** The key that marks a reference. `$`-prefixed so it cannot collide with a
34
+ * parameter name: the schema's vocabulary is all bare identifiers. */
35
+ export const REF_KEY = "$ref";
36
+
37
+ /** True for `{ $ref: "name" }` and nothing else. */
38
+ export const isRef = (v) =>
39
+ !!v && typeof v === "object" && !Array.isArray(v) && typeof v[REF_KEY] === "string";
40
+
41
+ /** Every name referenced anywhere in a spec, with the path that reached it. */
42
+ export function collectRefs(elements, at = "elements") {
43
+ const found = [];
44
+ const walk = (node, path) => {
45
+ if (Array.isArray(node)) return node.forEach((n, i) => walk(n, `${path}[${i}]`));
46
+ if (!node || typeof node !== "object") return;
47
+ if (isRef(node)) { found.push({ name: node[REF_KEY], path }); return; }
48
+ for (const key of ["children", "items"]) {
49
+ if (node[key] !== undefined) walk(node[key], `${path}.${key}`);
50
+ }
51
+ };
52
+ walk(elements, at);
53
+ return found;
54
+ }
55
+
56
+ /**
57
+ * Replace every `{ $ref: name }` with a deep copy of `defs[name]`.
58
+ *
59
+ * A copy, not the definition itself: two pages referencing one nav must not
60
+ * share an object the mapper then mutates — rendering an `a` already adds
61
+ * `font` and `pad` to the descriptor it is given, so a shared definition would
62
+ * accumulate the leavings of every page that used it.
63
+ *
64
+ * An unresolvable reference is left in place rather than dropped or guessed.
65
+ * The validator reports it as DANGLING_REF with a did-you-mean; silently
66
+ * removing it would make a missing nav look like a page that never had one.
67
+ */
68
+ export function resolveRefs(elements, defs, seen = new Set()) {
69
+ if (!defs || typeof defs !== "object") return elements;
70
+
71
+ const expand = (node) => {
72
+ if (Array.isArray(node)) return node.map(expand);
73
+ if (!node || typeof node !== "object") return node;
74
+
75
+ if (isRef(node)) {
76
+ const name = node[REF_KEY];
77
+ if (!Object.prototype.hasOwnProperty.call(defs, name)) return node;
78
+ if (seen.has(name)) {
79
+ // A definition that reaches itself. Returning the ref rather
80
+ // than recursing means a cycle is a reported error instead of
81
+ // a stack overflow at render time.
82
+ return node;
83
+ }
84
+ const next = new Set(seen).add(name);
85
+ const base = resolveRefs([defs[name]], defs, next)[0];
86
+
87
+ // Keys written BESIDE the reference override the definition.
88
+ //
89
+ // Without this they were silently dropped — `{ $ref: "nav", id:
90
+ // "topnav" }` expanded to the bare definition and the id went
91
+ // nowhere, which is precisely the declared-but-ignored failure
92
+ // this project spent six stages removing. Having built a new one,
93
+ // the choice was to reject the shape or to honour it; honouring it
94
+ // is what makes a shared fragment reusable at all, since the whole
95
+ // point of sharing a nav is that each page marks a different entry.
96
+ //
97
+ // Shallow, deliberately: a deep merge would make it unclear
98
+ // whether `items` replaces the definition's or extends it. It
99
+ // replaces.
100
+ const { [REF_KEY]: _drop, ...overrides } = node;
101
+ return Object.keys(overrides).length
102
+ ? { ...(base && typeof base === "object" ? base : {}), ...overrides }
103
+ : base;
104
+ }
105
+
106
+ const out = { ...node };
107
+ for (const key of ["children", "items"]) {
108
+ if (out[key] !== undefined) out[key] = expand(out[key]);
109
+ }
110
+ return out;
111
+ };
112
+
113
+ return Array.isArray(elements) ? elements.map(expand) : elements;
114
+ }
115
+
116
+ /** Names defined but never referenced — dead weight in a shared file. */
117
+ export function unusedDefs(elements, defs) {
118
+ if (!defs) return [];
119
+ const used = new Set(collectRefs(elements).map((r) => r.name));
120
+ return Object.keys(defs).filter((n) => !used.has(n));
121
+ }
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.0
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.11
2
+ * nodality v1.3.0
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.11
2
+ * nodality v1.3.0
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.11
2
+ * nodality v1.3.0
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.11
2
+ * nodality v1.3.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.0
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.11
2
+ * nodality v1.3.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -30,6 +30,8 @@ import {
30
30
  FRAMEWORK_DOC, EASING_NAMES,
31
31
  } from "./raster-ops.js";
32
32
  import { presetNames, presetInfo } from "./raster-presets.js";
33
+ import { collectRefs, isRef, resolveRefs } from "./resolve-refs.js";
34
+ import { normalizeSpec, aliasConflicts, ALIASES } from "./normalize-spec.js";
33
35
  import { ELEMENT_TYPES } from "./element-mapper.js";
34
36
  import { ELEMENT_PARAM_NAMES } from "./element-params.generated.js";
35
37
  import { didYouMean, suggest, levenshtein } from "./suggest.js";
@@ -161,7 +163,7 @@ export const CONTENT_SLOT = {
161
163
  row: "children", form: "children", wrap: "children",
162
164
  };
163
165
 
164
- export function validateNodes(nodes, elements) {
166
+ export function validateNodes(nodes, elements, defs) {
165
167
  const errors = [];
166
168
  const push = (code, path, got, suggestions, valid) => {
167
169
  errors.push({
@@ -219,6 +221,42 @@ export function validateNodes(nodes, elements) {
219
221
  // element whose `type` names nothing is not a silent no-op but a THROW
220
222
  // from the mapper at render time, which reaches a generator as a stack
221
223
  // trace rather than as a report it can repair from.
224
+ // Tier 3: a reference with no definition. Reported here as well as
225
+ // refused at render, because a report is repairable in one turn and a
226
+ // throw is not — the Stage 3 contract applied to the newest way of being
227
+ // wrong. `defs` is optional: without it references are left alone rather
228
+ // than all reported missing, since a caller may validate a page before
229
+ // deciding what to share.
230
+ if (defs && typeof defs === "object" && Array.isArray(elements)) {
231
+ const defined = Object.keys(defs);
232
+ for (const { name, path } of collectRefs(elements)) {
233
+ if (!Object.prototype.hasOwnProperty.call(defs, name)) {
234
+ push("DANGLING_REF", path, name, suggest(name, defined), defined);
235
+ }
236
+ }
237
+ }
238
+
239
+ // Walk the RESOLVED tree when definitions are available. A reference
240
+ // with keys beside it is a real element once merged, and only the merged
241
+ // form can be checked: `{ $ref: "nav", itms: [] }` is a typo in an
242
+ // override, and validating the unexpanded reference would miss it —
243
+ // a silent no-op inside the mechanism built to remove silent no-ops.
244
+ let toWalk = (defs && typeof defs === "object" && Array.isArray(elements))
245
+ ? resolveRefs(elements, defs)
246
+ : elements;
247
+
248
+ // An alias and its canonical name both present, disagreeing: one of them
249
+ // is going to be ignored. The author hears it here rather than finding it
250
+ // in the page.
251
+ for (const c of aliasConflicts(Array.isArray(elements) ? elements : [])) {
252
+ push("CONFLICTING_ALIAS", c.path, c.alias, [c.canonical],
253
+ [`"${c.alias}" is accepted as "${c.canonical}"; give one of them, not both`]);
254
+ }
255
+
256
+ // Walk the normalised tree, so `{type:"img", src:"..."}` is checked as the
257
+ // element it actually becomes rather than reported as an unknown parameter.
258
+ if (Array.isArray(toWalk)) toWalk = normalizeSpec(toWalk);
259
+
222
260
  if (idsKnown) {
223
261
  const walkEls = (list, at) => {
224
262
  if (!Array.isArray(list)) return;
@@ -239,6 +277,12 @@ export function validateNodes(nodes, elements) {
239
277
  push("BAD_ELEMENT", p, el, [], ["{ type: \"<element type>\", ... }"]);
240
278
  return;
241
279
  }
280
+ // A reference is not an element and has no `type` by design.
281
+ // Its validity is whether the name resolves, which the
282
+ // DANGLING_REF pass above decides; reporting MISSING_FIELD
283
+ // here rejected every correct `$ref` there is.
284
+ if (isRef(el)) return;
285
+
242
286
  if (typeof el.type !== "string") {
243
287
  push("MISSING_FIELD", `${p}.type`, el.type, [],
244
288
  ["an element type, e.g. \"h1\", \"wrap\", \"nav\""]);
@@ -252,6 +296,19 @@ export function validateNodes(nodes, elements) {
252
296
  // removed for `cards`, surviving one slot over. It is not a
253
297
  // typo, so near-miss detection cannot reach it.
254
298
  const wants = CONTENT_SLOT[el.type];
299
+ if (wants) {
300
+ // A content slot that is present but not a list. The
301
+ // mapper tests `Array.isArray(...) && length`, so
302
+ // `items: {…}` and `items: "x"` fall through to the
303
+ // placeholders — content declared, content ignored, no
304
+ // error. Same family as WRONG_CONTENT_SLOT: the key is
305
+ // right and the value cannot carry anything.
306
+ const given = el[wants];
307
+ if (given !== undefined && given !== null && !Array.isArray(given)) {
308
+ push("BAD_CONTENT_SHAPE", `${p}.${wants}`, given, [],
309
+ [`an array — "${el.type}" reads \`${wants}\` as a list`]);
310
+ }
311
+ }
255
312
  if (wants) {
256
313
  const other = wants === "items" ? "children" : "items";
257
314
  const filled = (v) => Array.isArray(v) ? v.length > 0 : v != null;
@@ -327,7 +384,7 @@ export function validateNodes(nodes, elements) {
327
384
  }
328
385
  });
329
386
  };
330
- walkEls(elements, "elements");
387
+ walkEls(toWalk, "elements");
331
388
  }
332
389
 
333
390
  nodes.forEach((node, i) => {
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.2.11
2
+ * nodality v1.3.0
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.2.11",
3
+ "version": "1.3.0",
4
4
  "description": "A lightweight library for declarative UI elements.",
5
5
  "main": "dist/index.cjs.js",
6
6
  "module": "dist/index.esm.js",
@@ -33,6 +33,8 @@
33
33
  "./raster": "./lib/raster-ops.js",
34
34
  "./validate": "./lib/validate-nodes.js",
35
35
  "./parse": "./lib/parse-html.js",
36
+ "./refs": "./lib/resolve-refs.js",
37
+ "./normalize": "./lib/normalize-spec.js",
36
38
  "./inspect": "./lib/raster-inspect.js",
37
39
  "./examples/*": "./examples/*.js",
38
40
  "./transition": "./lib/transition.js",