nodality 1.0.221 → 1.1.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 (279) hide show
  1. package/API.md +227 -0
  2. package/bin/nodality.js +64 -0
  3. package/dist/animator.cjs.js +1 -1
  4. package/dist/animator.cjs.js.LICENSE.txt +1 -1
  5. package/dist/animator.esm.js +1 -1
  6. package/dist/animator.esm.js.LICENSE.txt +1 -1
  7. package/dist/audionew.cjs.js +1 -1
  8. package/dist/audionew.cjs.js.LICENSE.txt +1 -1
  9. package/dist/audionew.esm.js +1 -1
  10. package/dist/audionew.esm.js.LICENSE.txt +1 -1
  11. package/dist/base.cjs.js +1 -1
  12. package/dist/base.cjs.js.LICENSE.txt +1 -1
  13. package/dist/base.esm.js +1 -1
  14. package/dist/base.esm.js.LICENSE.txt +1 -1
  15. package/dist/beta-desktop-bar.cjs.js +1 -1
  16. package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
  17. package/dist/beta-desktop-bar.esm.js +1 -1
  18. package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
  19. package/dist/beta-mobile-bar.cjs.js +1 -1
  20. package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
  21. package/dist/beta-mobile-bar.esm.js +1 -1
  22. package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
  23. package/dist/bundle.umd.js +1 -1
  24. package/dist/bundle.umd.js.LICENSE.txt +131 -1
  25. package/dist/button.cjs.js +1 -1
  26. package/dist/button.cjs.js.LICENSE.txt +1 -1
  27. package/dist/button.esm.js +1 -1
  28. package/dist/button.esm.js.LICENSE.txt +1 -1
  29. package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
  30. package/dist/card-getter.esm.js.LICENSE.txt +1 -1
  31. package/dist/center.cjs.js +1 -1
  32. package/dist/center.cjs.js.LICENSE.txt +1 -1
  33. package/dist/center.esm.js +1 -1
  34. package/dist/center.esm.js.LICENSE.txt +1 -1
  35. package/dist/checkbox.cjs.js +1 -1
  36. package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
  37. package/dist/checkbox.esm.js +1 -1
  38. package/dist/checkbox.esm.js.LICENSE.txt +1 -1
  39. package/dist/code.cjs.js +1 -1
  40. package/dist/code.cjs.js.LICENSE.txt +1 -1
  41. package/dist/code.esm.js +1 -1
  42. package/dist/code.esm.js.LICENSE.txt +1 -1
  43. package/dist/container.cjs.js +1 -1
  44. package/dist/container.cjs.js.LICENSE.txt +1 -1
  45. package/dist/container.esm.js +1 -1
  46. package/dist/container.esm.js.LICENSE.txt +1 -1
  47. package/dist/data-list.cjs.js +1 -1
  48. package/dist/data-list.cjs.js.LICENSE.txt +1 -1
  49. package/dist/data-list.esm.js +1 -1
  50. package/dist/data-list.esm.js.LICENSE.txt +1 -1
  51. package/dist/designer.cjs.js +1 -1
  52. package/dist/designer.cjs.js.LICENSE.txt +97 -1
  53. package/dist/designer.esm.js +1 -1
  54. package/dist/designer.esm.js.LICENSE.txt +97 -1
  55. package/dist/element-mapper.cjs.js +1 -1
  56. package/dist/element-mapper.cjs.js.LICENSE.txt +17 -1
  57. package/dist/element-mapper.esm.js +1 -1
  58. package/dist/element-mapper.esm.js.LICENSE.txt +17 -1
  59. package/dist/finalresult.esm.js +1 -1
  60. package/dist/finalresult.esm.js.LICENSE.txt +158 -1
  61. package/dist/flex-card.cjs.js +1 -1
  62. package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
  63. package/dist/flex-card.esm.js +1 -1
  64. package/dist/flex-card.esm.js.LICENSE.txt +1 -1
  65. package/dist/flex-grid.cjs.js +1 -1
  66. package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
  67. package/dist/flex-grid.esm.js +1 -1
  68. package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
  69. package/dist/flex-row.cjs.js +1 -1
  70. package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
  71. package/dist/flex-row.esm.js +1 -1
  72. package/dist/flex-row.esm.js.LICENSE.txt +1 -1
  73. package/dist/floating-input.cjs.js +1 -1
  74. package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
  75. package/dist/floating-input.esm.js +1 -1
  76. package/dist/floating-input.esm.js.LICENSE.txt +1 -1
  77. package/dist/free.cjs.js +1 -1
  78. package/dist/free.cjs.js.LICENSE.txt +1 -1
  79. package/dist/free.esm.js +1 -1
  80. package/dist/free.esm.js.LICENSE.txt +1 -1
  81. package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
  82. package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
  83. package/dist/image-picker.cjs.js +1 -1
  84. package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
  85. package/dist/image-picker.esm.js +1 -1
  86. package/dist/image-picker.esm.js.LICENSE.txt +1 -1
  87. package/dist/image.cjs.js +1 -1
  88. package/dist/image.cjs.js.LICENSE.txt +1 -1
  89. package/dist/image.esm.js +1 -1
  90. package/dist/image.esm.js.LICENSE.txt +1 -1
  91. package/dist/index.cjs.js +1 -1
  92. package/dist/index.cjs.js.LICENSE.txt +158 -1
  93. package/dist/index.d.ts +962 -0
  94. package/dist/index.esm.js +1 -1
  95. package/dist/index.esm.js.LICENSE.txt +158 -1
  96. package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
  97. package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
  98. package/dist/link-getter.cjs.js +1 -1
  99. package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
  100. package/dist/link-getter.esm.js +1 -1
  101. package/dist/link-getter.esm.js.LICENSE.txt +1 -1
  102. package/dist/link.cjs.js +1 -1
  103. package/dist/link.cjs.js.LICENSE.txt +1 -1
  104. package/dist/link.esm.js +1 -1
  105. package/dist/link.esm.js.LICENSE.txt +1 -1
  106. package/dist/meta-adder.cjs.js +1 -1
  107. package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
  108. package/dist/meta-adder.esm.js +1 -1
  109. package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
  110. package/dist/modal-2025.cjs.js +1 -1
  111. package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
  112. package/dist/modal-2025.esm.js +1 -1
  113. package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
  114. package/dist/multiswitcher.cjs.js +1 -1
  115. package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
  116. package/dist/multiswitcher.esm.js +1 -1
  117. package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
  118. package/dist/new-nav-bar.cjs.js +1 -1
  119. package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
  120. package/dist/new-nav-bar.esm.js +1 -1
  121. package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
  122. package/dist/picker.cjs.js +1 -1
  123. package/dist/picker.cjs.js.LICENSE.txt +1 -1
  124. package/dist/picker.esm.js +1 -1
  125. package/dist/picker.esm.js.LICENSE.txt +1 -1
  126. package/dist/progress.cjs.js +1 -1
  127. package/dist/progress.cjs.js.LICENSE.txt +1 -1
  128. package/dist/progress.esm.js +1 -1
  129. package/dist/progress.esm.js.LICENSE.txt +1 -1
  130. package/dist/radio.cjs.js +1 -1
  131. package/dist/radio.cjs.js.LICENSE.txt +1 -1
  132. package/dist/radio.esm.js +1 -1
  133. package/dist/radio.esm.js.LICENSE.txt +1 -1
  134. package/dist/range.cjs.js +1 -1
  135. package/dist/range.cjs.js.LICENSE.txt +1 -1
  136. package/dist/range.esm.js +1 -1
  137. package/dist/range.esm.js.LICENSE.txt +1 -1
  138. package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
  139. package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
  140. package/dist/side-bar.cjs.js +1 -1
  141. package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
  142. package/dist/side-bar.esm.js +1 -1
  143. package/dist/side-bar.esm.js.LICENSE.txt +1 -1
  144. package/dist/side-nav-bar.cjs.js +1 -1
  145. package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
  146. package/dist/side-nav-bar.esm.js +1 -1
  147. package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
  148. package/dist/simple-bar.cjs.js +1 -1
  149. package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
  150. package/dist/simple-bar.esm.js +1 -1
  151. package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
  152. package/dist/slider-2025.cjs.js +1 -1
  153. package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
  154. package/dist/slider-2025.esm.js +1 -1
  155. package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
  156. package/dist/spacer.cjs.js +1 -1
  157. package/dist/spacer.cjs.js.LICENSE.txt +1 -1
  158. package/dist/spacer.esm.js +1 -1
  159. package/dist/spacer.esm.js.LICENSE.txt +1 -1
  160. package/dist/stack.cjs.js +1 -1
  161. package/dist/stack.cjs.js.LICENSE.txt +1 -1
  162. package/dist/stack.esm.js +1 -1
  163. package/dist/stack.esm.js.LICENSE.txt +1 -1
  164. package/dist/stacker.cjs.js.LICENSE.txt +1 -1
  165. package/dist/stacker.esm.js.LICENSE.txt +1 -1
  166. package/dist/table.cjs.js +1 -1
  167. package/dist/table.cjs.js.LICENSE.txt +1 -1
  168. package/dist/table.esm.js +1 -1
  169. package/dist/table.esm.js.LICENSE.txt +1 -1
  170. package/dist/text-field.cjs.js +1 -1
  171. package/dist/text-field.cjs.js.LICENSE.txt +1 -1
  172. package/dist/text-field.esm.js +1 -1
  173. package/dist/text-field.esm.js.LICENSE.txt +1 -1
  174. package/dist/text.cjs.js +1 -1
  175. package/dist/text.cjs.js.LICENSE.txt +1 -1
  176. package/dist/text.esm.js +1 -1
  177. package/dist/text.esm.js.LICENSE.txt +1 -1
  178. package/dist/theme.cjs.js.LICENSE.txt +1 -1
  179. package/dist/theme.esm.js.LICENSE.txt +1 -1
  180. package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
  181. package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
  182. package/dist/ulist.cjs.js +1 -1
  183. package/dist/ulist.cjs.js.LICENSE.txt +1 -1
  184. package/dist/ulist.esm.js +1 -1
  185. package/dist/ulist.esm.js.LICENSE.txt +1 -1
  186. package/dist/video.cjs.js +1 -1
  187. package/dist/video.cjs.js.LICENSE.txt +1 -1
  188. package/dist/video.esm.js +1 -1
  189. package/dist/video.esm.js.LICENSE.txt +1 -1
  190. package/dist/wrap.cjs.js +1 -1
  191. package/dist/wrap.cjs.js.LICENSE.txt +1 -1
  192. package/dist/wrap.esm.js +1 -1
  193. package/dist/wrap.esm.js.LICENSE.txt +1 -1
  194. package/dist/zoom-card.cjs.js +1 -1
  195. package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
  196. package/dist/zoom-card.esm.js +1 -1
  197. package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
  198. package/examples/custom-raster-op.js +174 -0
  199. package/layout/animator.js +30 -2
  200. package/layout/audio.js +1 -1
  201. package/layout/audionew.js +1 -1
  202. package/layout/base.js +1 -1
  203. package/layout/beta-desktop-bar.js +1 -1
  204. package/layout/beta-mobile-bar.js +3 -2
  205. package/layout/button.js +1 -1
  206. package/layout/center.js +1 -1
  207. package/layout/checkbox.js +2 -2
  208. package/layout/circle.js +3 -2
  209. package/layout/code.js +3 -2
  210. package/layout/container.js +3 -4
  211. package/layout/dropdown-2025.js +1 -1
  212. package/layout/flex-card.js +1 -1
  213. package/layout/flex-grid.js +1 -1
  214. package/layout/flex-row.js +3 -2
  215. package/layout/form-components/custom.js +1 -1
  216. package/layout/form-components/data-list.js +2 -2
  217. package/layout/form-components/floating-input.js +2 -2
  218. package/layout/form-components/form-all.js +2 -2
  219. package/layout/form-components/form.js +1 -1
  220. package/layout/form-components/image-picker.js +1 -1
  221. package/layout/form-components/picker.js +2 -2
  222. package/layout/form-components/radio.js +2 -2
  223. package/layout/form-components/radiogroup.js +2 -2
  224. package/layout/form-components/range.js +2 -2
  225. package/layout/free.js +3 -4
  226. package/layout/grid-switcher.js +1 -1
  227. package/layout/grid.js +1 -1
  228. package/layout/horizontal-scroller.js +1 -1
  229. package/layout/image.js +1 -1
  230. package/layout/index.js +1 -1
  231. package/layout/link.js +5 -5
  232. package/layout/list.js +3 -4
  233. package/layout/meta-adder.js +1 -1
  234. package/layout/modal-2025.js +1 -1
  235. package/layout/morph.js +1003 -0
  236. package/layout/multiswitcher.js +11 -3
  237. package/layout/nav-bar.js +2 -3
  238. package/layout/nav-factor/custom-div.js +2 -2
  239. package/layout/new-nav-bar.js +11 -4
  240. package/layout/polygon.js +3 -4
  241. package/layout/prerender-site.js +1 -1
  242. package/layout/prerender.js +1 -1
  243. package/layout/progress.js +1 -1
  244. package/layout/row.js +1 -1
  245. package/layout/scroll-video.js +1 -1
  246. package/layout/side-bar.js +1 -1
  247. package/layout/side-nav-bar.js +1 -1
  248. package/layout/simple-bar.js +1 -1
  249. package/layout/slider-2025.js +1 -1
  250. package/layout/spacer.js +1 -1
  251. package/layout/stack.js +1 -1
  252. package/layout/svg.js +3 -4
  253. package/layout/switcher.js +1 -1
  254. package/layout/table.js +1 -1
  255. package/layout/text-field.js +1 -1
  256. package/layout/text.js +5 -5
  257. package/layout/ulist.js +1 -1
  258. package/layout/video.js +1 -1
  259. package/layout/wrap.js +1 -1
  260. package/layout/zoom-card.js +1 -1
  261. package/lib/card-getter.js +1 -1
  262. package/lib/codegen.js +62 -0
  263. package/lib/data.js +37 -1
  264. package/lib/designer.js +18 -1
  265. package/lib/element-mapper.js +351 -247
  266. package/lib/keyframe-animation.js +1 -1
  267. package/lib/link-getter.js +1 -1
  268. package/lib/morph-node.js +424 -0
  269. package/lib/raster-inspect.js +409 -0
  270. package/lib/raster-ops.js +1980 -81
  271. package/lib/raster-presets.js +329 -0
  272. package/lib/scroll-video.js +1 -1
  273. package/lib/seo.js +28 -1
  274. package/lib/stacker.js +1 -1
  275. package/lib/suggest.js +66 -0
  276. package/lib/theme.js +1 -1
  277. package/lib/transform-anim.js +1 -1
  278. package/lib/transition.js +205 -0
  279. package/package.json +20 -7
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * nodality v1.0.221
2
+ * nodality v1.1.0
3
3
  * (c) 2026 Filip Vabrousek
4
4
  * License: MIT
5
5
  */
@@ -7,6 +7,7 @@
7
7
  // CORE
8
8
  import { Animator } from "../layout/animator.js";
9
9
  import { RASTER_OP_NAMES } from "../lib/raster-ops.js";
10
+ import { didYouMean } from "../lib/suggest.js";
10
11
  import { Base } from "../layout/base.js";
11
12
  import { Text } from "../layout/text.js";
12
13
  import { Image } from "../layout/image.js";
@@ -71,6 +72,17 @@ import { Polygon } from "../layout/polygon.js";
71
72
  import { Circle } from "../layout/circle.js";
72
73
 
73
74
 
75
+ // The closed set of `type:` values mapType understands, in the order it
76
+ // tests them. Kept beside the dispatch so a new branch without a new entry
77
+ // is visible in review — the vocabulary and the router should not drift.
78
+ const ELEMENT_TYPES = [
79
+ "h1", "h2", "h3", "h4", "h5", "h6", "p",
80
+ "img", "a", "cards", "free", "nav", "sideNav", "row", "dropdown",
81
+ "radio", "input", "labelInput", "filePicker", "picker", "video",
82
+ "audio", "multiswitcher", "button", "form", "checkbox", "stack",
83
+ "simple", "copy", "wrap", "circle", "polygon", "code", "table", "ulist",
84
+ ];
85
+
74
86
  class ElementMapper { // 22:09:58 04/11/2024
75
87
  static mapType(obj) {
76
88
  // console.log("LOBJ");
@@ -146,6 +158,13 @@ class ElementMapper { // 22:09:58 04/11/2024
146
158
  } else if (obj.el.type === "ulist"){
147
159
  return this.mapUList(obj);
148
160
  }
161
+
162
+ // Everything above returned. Falling through used to return
163
+ // `undefined`, which the caller then treated as an element —
164
+ // surfacing much later, and far from the cause, as
165
+ // "Cannot read properties of undefined (reading 'toCode')".
166
+ // A closed vocabulary should say so at the point of the typo.
167
+ throw new Error("[nodality] " + didYouMean(obj.el.type, ELEMENT_TYPES, "element type"));
149
168
  }
150
169
 
151
170
  static mapTable(obj){
@@ -265,6 +284,12 @@ new Text("row.").set({
265
284
  const customOptions = obj.customOptions;
266
285
  const ft = customOptions.filter(l => l.op === "copy")[0];
267
286
  const count = ft?.count ?? 3;
287
+ // Any OTHER raster nodes aimed at this element. `copy` itself is
288
+ // consumed above to build the wheel, so it is excluded — leaving it in
289
+ // would attach a GPU copy pass on top of the DOM copies this mapper
290
+ // already emits, and the element would be duplicated twice.
291
+ const copyRaster = (this.filteroRaster(obj.el.id, customOptions) || [])
292
+ .filter((n) => n !== ft);
268
293
 
269
294
  const minSize = 300; // size of whole wheel
270
295
  const cardSize = 30; // width/height of text wrapper
@@ -326,7 +351,12 @@ animation: {
326
351
  position: "absolute",
327
352
  id: "#first",
328
353
  scale: 0.3,
329
- ${ft?.animation ? animation : ""}
354
+ ${ft?.animation ? animation : ""}${
355
+ // Wrapper routes its options through commonMethods(), so a raster
356
+ // chain serialised here reaches rasterize() when the emitted source
357
+ // runs. Plain data, so JSON.stringify round-trips it.
358
+ copyRaster.length ? `,\n raster: ${JSON.stringify(copyRaster)}` : ""
359
+ }
330
360
  })
331
361
  .add(
332
362
  Array.from({ length: ${count} }).map((_, i) => {
@@ -385,101 +415,139 @@ animation: {
385
415
  // referenced an undefined `radius`, so any call was a guaranteed
386
416
  // ReferenceError. Kept in git history if the ring layout is wanted back.
387
417
 
388
- static button(){
389
- return new Button("Submit")
418
+ // Took NO arguments at all, so `{type: "button", text: "Buy now"}`
419
+ // rendered a hardcoded "Submit" with a demo onTap that logged to the
420
+ // console and a keySet forcing a 3px green border on every button in
421
+ // every page. The label, the handler and the styling are the caller's.
422
+ static button(obj){
423
+ const el = obj.el;
424
+ return new Button(el.text ?? "Button")
390
425
  .set({
391
426
  fluidc: "S6",
392
- color: "white",
393
- background: "#1abc9c",
394
- arrpad: {sides: ["all"], value: ".3rem"},
395
- radius: ".3rem",
396
- onTap: () => { console.log("[nodality] demo button tapped"); },
397
- keySet: {key: "border", value: "3px solid green"}
427
+ ...this.elOpts(el),
428
+ raster: this.filteroRaster(el.id, obj.customOptions),
398
429
  });
399
430
  }
400
431
 
401
- static multiswitcher(){
402
-
403
-
404
- let r = new Switcher()
405
- .set({breakpoints:[
406
- { at: "0px", view: new Text("First").set({}) },
432
+ static multiswitcher(obj){
407
433
 
408
- { at: "700px", view: new Text("Nice").set({}) },
409
-
410
- { at: "800px", view: new Text("Best").set({}) },
411
- ]}); // undefined must be passed to render method
412
-
413
- // console.log("ROD");
414
- // console.log(r.toCode().join(""));
415
-
416
- // return new Text("MS").set({});
417
434
 
418
- return r;
435
+ // Ignored obj: every switcher showed First/Nice/Best. Each
436
+ // breakpoint's `view` is an ELEMENT SPEC now, mapped like any
437
+ // other subtree, so a switcher can hold real content.
438
+ //
439
+ // { type: "multiswitcher", breakpoints: [
440
+ // { at: "0px", view: { type: "h2", text: "Small" } },
441
+ // { at: "700px", view: { type: "h1", text: "Large" } } ]}
442
+ //
443
+ // Switcher DOES extend Animator, so it has rasterize(); its set()
444
+ // just does not accept a `raster:` key, which is why the chain is
445
+ // attached directly rather than passed through.
446
+ const bps = Array.isArray(obj.el.breakpoints) ? obj.el.breakpoints : null;
447
+ if (!bps || !bps.length) {
448
+ throw new Error('[nodality] a "multiswitcher" element needs a non-empty ' +
449
+ "`breakpoints` array of { at, view }.");
450
+ }
451
+ return new Switcher().set({
452
+ id: obj.el.id,
453
+ area: obj.el.area,
454
+ breakpoints: bps.map((bp) => ({
455
+ at: bp.at,
456
+ // Already a component (someone built it by hand) or a spec.
457
+ view: bp.view && typeof bp.view.render === "function"
458
+ ? bp.view
459
+ : this.mapType({ ...obj, el: bp.view }),
460
+ })),
461
+ });
419
462
  }
420
463
 
421
-
422
- static video(){
423
- return new Video("https://www.w3schools.com/html/mov_bbb.mp4").set({});
464
+ // REMOVED: the old multiswitcher body — a hardcoded
465
+ // First/Nice/Best fixture that ignored obj entirely. Kept in git
466
+ // history; the version above takes its views from the element.
467
+
468
+
469
+ // Hardcoded to a w3schools Big Buck Bunny URL. A missing `url` now
470
+ // says so: silently playing someone else's sample reel is worse than
471
+ // an error, because it looks like it worked.
472
+ static video(obj){
473
+ const el = obj.el;
474
+ if (!el.url) throw new Error('[nodality] a "video" element needs a `url`.');
475
+ // Video defines its own set() and never calls commonMethods(), so
476
+ // `raster:` there would be a dead key.
477
+ return this.attachRaster(
478
+ new Video(el.url).set(this.elOpts(el)), el, obj.customOptions);
424
479
  }
425
480
 
426
- static audio(){
427
- return new Audio("rouska.mp3").set({background: "#1abc9c"});
481
+ // This one did not merely ignore its input, it THREW: Audio defines
482
+ // no set() and Animator has none to inherit, so the old
483
+ // `.set({background})` was a TypeError on every `{type: "audio"}`.
484
+ // Options go to the constructor; styling belongs to the element.
485
+ static audio(obj){
486
+ const el = obj.el;
487
+ if (!el.url) throw new Error('[nodality] an "audio" element needs a `url`.');
488
+ return this.attachRaster(new Audio(el.url), el, obj.customOptions);
428
489
  }
429
490
 
491
+ // Ignored obj: every radio group asked Male/Female/Other, and
492
+ // `multiple: true` on a RADIO group was wrong on its face — that is a
493
+ // checkbox. Defaults to single-select now, as radios are.
494
+ //
495
+ // RadioGroup is not an Animator (`class RadioGroup /*extends
496
+ // Animator*/`), so it has no rasterize() and cannot carry raster ops.
430
497
  static radio(obj){
431
- // alert("PP")
498
+ const el = obj.el;
432
499
  return new RadioGroup()
433
500
  .set({
434
- items: ["Male", "Female", "Other"],
435
- multiple: true // wrnóng
501
+ items: Array.isArray(el.items) ? el.items : [],
502
+ multiple: el.multiple ?? false,
503
+ ...this.elOpts(el),
436
504
  });
437
505
  }
438
506
 
507
+ // Ignored obj: every input on every page read "Enter swimming time".
508
+ // `inputType` rather than `type`, because `type` is already the
509
+ // ELEMENT type ("input") — see elOpts.
439
510
  static input(obj){
440
- // alert("PP")
441
- return new TextField().set({
442
- type: "text",
443
- placeholder: "Enter swimming time",
444
- arrayMargin: {sides: ["all"], value: "1rem"}
445
- });
511
+ const el = obj.el;
512
+ return new TextField().set({
513
+ type: el.inputType ?? "text",
514
+ ...this.elOpts(el),
515
+ raster: this.filteroRaster(el.id, obj.customOptions),
516
+ });
446
517
  }
447
518
 
519
+ // Ignored obj: every labelled input was titled "Your name".
448
520
  static floatInput(obj){
449
- // alert("PP")
450
- return new FloatingInput()
521
+ const el = obj.el;
522
+ return this.attachRaster(new FloatingInput()
451
523
  .set({
452
- title: "Your name",
453
- type: "input"
454
- });
524
+ type: el.inputType ?? "input",
525
+ ...this.elOpts(el),
526
+ }), el, obj.customOptions);
455
527
  }
456
528
 
529
+ // Ignored obj, including a hardcoded `id: "A"` — two file pickers
530
+ // on one page produced duplicate DOM ids.
457
531
  static filePicker(obj){
458
- // alert("PP")
459
- return new FilePickera()
532
+ const el = obj.el;
533
+ return this.attachRaster(new FilePickera()
460
534
  .set({
461
- id: "A",
462
- title: "Add profile picture",
463
- background: "#3498DB",
464
- color: "white",
465
- font: "Arial",
466
- radius: "3rem",
467
- accept: "application/pdf"
468
- });
535
+ title: el.title ?? "Choose a file",
536
+ ...this.elOpts(el),
537
+ }), el, obj.customOptions);
469
538
  }
470
539
 
540
+ // Ignored obj: every picker offered Tesla and Audi.
471
541
  static picker(obj){
472
-
473
- let items = [["none", "select a car---"], ["tesla", "Tesla"], ["audi", "Audi"]];
474
-
475
-
476
- return new Picker()
542
+ const el = obj.el;
543
+ return new Picker()
477
544
  .set({
478
- items: items,
479
- font: "Arial",
545
+ items: Array.isArray(el.items) ? el.items : [],
480
546
  arrayPadding: ({sides: ["all"], value: "0.5rem"}),
481
- rounded: true
482
- })
547
+ rounded: true,
548
+ ...this.elOpts(el),
549
+ raster: this.filteroRaster(el.id, obj.customOptions),
550
+ });
483
551
  }
484
552
 
485
553
 
@@ -506,8 +574,12 @@ animation: {
506
574
 
507
575
  }));
508
576
 
509
- return new Dropdown()
510
- .set({
577
+ // Bound rather than returned inline, so the raster chain can be
578
+ // attached below. Dropdown defines its own set() which handles
579
+ // only its own keys and never calls commonMethods(), so a
580
+ // `raster:` option here would be silently dropped.
581
+ const dd = new Dropdown()
582
+ .set({
511
583
  behaviour: "click", // click otherwise
512
584
  //width: "120px",
513
585
 
@@ -543,13 +615,18 @@ animation: {
543
615
 
544
616
  new Wrapper().set({border: "1px solid green", background: "#1abc9c", radius: "0.7rem", socenter: true})
545
617
  .add(items.slice(1)
546
-
618
+
547
619
  /* new Text("Option 1o").set({font: "Arial", pad: [{a: 10}],}),
548
620
  new Text("Option 2").set({font: "Arial", pad: [{a: 10}],}),
549
621
  new Text("Option 3").set({font: "Arial", pad: [{a: 10}],}),
550
622
  */
551
623
  )
552
- ])
624
+ ]);
625
+
626
+ const ddRaster = this.filteroRaster(obj.el.id, obj.customOptions);
627
+ ddRaster && dd.rasterize(ddRaster);
628
+
629
+ return dd;
553
630
  }
554
631
 
555
632
  /* static row(obj){
@@ -755,7 +832,7 @@ if (obj.el.dropdown){
755
832
 
756
833
 
757
834
 
758
- return new Switcher()
835
+ const navBar = new Switcher()
759
836
  .set({
760
837
  breakpoints: [ // 172800 almost
761
838
  {
@@ -950,7 +1027,19 @@ new Wrapper().set({
950
1027
  // { at: "700px", view: Object.assign(document.createElement("h1"), { textContent: "Medium View" }) },
951
1028
  // { at: "800px", view: Object.assign(document.createElement("h1"), { textContent: "Large View" }) },
952
1029
  ],
953
- })
1030
+ });
1031
+
1032
+ // A raster chain aimed at this element reaches the component here.
1033
+ // Switcher DOES extend Animator (layout/multiswitcher.js) and so it
1034
+ // has rasterize() — the comment that said otherwise was wrong, and
1035
+ // the real gap was simply that protoNav never forwarded the chain.
1036
+ // Its own set() destructures {breakpoints, id, area}, so a `raster:`
1037
+ // key would be dropped; attachRaster calls rasterize() directly.
1038
+ //
1039
+ // Nothing changes for a nav with no raster nodes aimed at it:
1040
+ // filteroRaster returns undefined and attachRaster is a no-op, so
1041
+ // what {type:"nav"} produces today is untouched.
1042
+ return this.attachRaster(navBar, obj.el, customOptions);
954
1043
 
955
1044
  // return new Text("A").set({})
956
1045
  }
@@ -1208,6 +1297,19 @@ if (obj.el.dropdown){
1208
1297
  // console.log("PPPP");
1209
1298
  // console.log(obj.el.offcanvas);
1210
1299
  // console.log(rt.toCode());
1300
+
1301
+ // Raster ops, like every other rasterable element.
1302
+ //
1303
+ // NOT `raster:` in the options above: SideNav configures itself
1304
+ // through `setup()`, which builds `this.res` directly and never
1305
+ // reaches Animator.commonMethods() — where `obj.raster &&
1306
+ // this.rasterize(...)` lives. A `raster` key there would be
1307
+ // accepted, ignored, and report nothing, which is the exact bug
1308
+ // this is fixing. SideNav extends Animator, so the inherited
1309
+ // attach point is called directly instead.
1310
+ const rtRaster = this.filteroRaster(obj.el.id, obj.customOptions);
1311
+ rtRaster && rt.rasterize(rtRaster);
1312
+
1211
1313
  return rt;
1212
1314
 
1213
1315
  // return new Text("A").set({})
@@ -1276,6 +1378,12 @@ if (obj.el.dropdown){
1276
1378
  })
1277
1379
  ]);
1278
1380
 
1381
+ // Same wiring, same reason as sideNav: Free defines its own
1382
+ // set(), which handles only its own keys and never calls
1383
+ // commonMethods(), so a `raster:` option would be dead.
1384
+ const elaRaster = this.filteroRaster(obj.el.id, obj.customOptions);
1385
+ elaRaster && ela.rasterize(elaRaster);
1386
+
1279
1387
  return ela;
1280
1388
  // console.log("ETC");
1281
1389
  // console.log(ela.toCode());
@@ -1362,11 +1470,12 @@ let elo = new Polygon({ id: "hex" })
1362
1470
  transform: this.filtero("transform", el.id, obj.customOptions),
1363
1471
  filtera: this.filtero("filter", el.id, obj.customOptions),
1364
1472
  raster: this.filteroRaster(el.id, obj.customOptions),
1365
- resprop: el.resprop ? [
1366
- { breakpoint: "1400px", background: "orange", width: "200px", height: "200px" },
1367
- { breakpoint: "900px", background: "green", height: "100px", width: "100px" }
1368
- ] : null//this.filtero("resprop", el.id, obj.customOptions),
1369
- // copy: this.filtero("copy", el.id, obj.customOptions),
1473
+ // The caller's own breakpoints. This used to REPLACE them
1474
+ // with a two-breakpoint orange/green demo fixture whenever
1475
+ // `resprop` was merely present — so asking for responsive
1476
+ // styling got you someone else's debugging boxes, and the
1477
+ // option appeared to work while doing something unrelated.
1478
+ resprop: el.resprop ?? null,
1370
1479
  });
1371
1480
  }
1372
1481
 
@@ -1432,6 +1541,13 @@ let elo = new Polygon({ id: "hex" })
1432
1541
  re["gradient"] = this.filtero("gradient", obj.el.id, obj.customOptions),
1433
1542
  re["blast"] = this.filtero("blast", obj.el.id, obj.customOptions),
1434
1543
  re["backgroundOp"] = this.filtero("background", obj.el.id, obj.customOptions),//customOptions.filter(l => l.op.name === "background")[0];
1544
+ // Raster ops. Every CSS-level op above was forwarded and
1545
+ // this was not, so a raster node targeting a link was
1546
+ // accepted, matched nothing, and silently did nothing —
1547
+ // the same class of dead option phase P3 removed from
1548
+ // mapWrap. Link takes `raster` through commonMethods like
1549
+ // every other component; only the wiring was missing.
1550
+ re["raster"] = this.filteroRaster(obj.el.id, obj.customOptions);
1435
1551
  re["pad"] = [{ "a": 10 }];
1436
1552
 
1437
1553
  if (bst.length > 0) {
@@ -1469,6 +1585,9 @@ const animation = this.filtero("animation", el.id, obj.customOptions);
1469
1585
  const shadow = this.filtero("shadow", el.id, obj.customOptions);
1470
1586
  const marginOp = this.filtero("margin", el.id, obj.customOptions);
1471
1587
  const filtera = this.filtero("filter", el.id, obj.customOptions);
1588
+ // Raster nodes carry `op` as a plain string, so `filtero`'s `l.op.name`
1589
+ // matching never sees them — they need their own selector.
1590
+ const gridRaster = this.filteroRaster(el.id, obj.customOptions);
1472
1591
  const blast = this.filtero("blast", el.id, obj.customOptions);
1473
1592
 
1474
1593
 
@@ -1556,6 +1675,14 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
1556
1675
  filtera !== undefined ? `,\n filtera: ${JSON.stringify(filtera)}` : ""
1557
1676
  }${
1558
1677
  blast !== undefined ? `,\n blast: ${JSON.stringify(blast)}` : ""
1678
+ }${
1679
+ // Raster ops, emitted like every other op above. This mapper
1680
+ // returns SOURCE rather than an instance, so the chain is
1681
+ // serialised into the generated `.set({...})` instead of being
1682
+ // attached to an object. FlexGrid routes its options through
1683
+ // commonMethods(), so the emitted code reaches rasterize().
1684
+ // Raster nodes are plain data, so JSON.stringify round-trips them.
1685
+ gridRaster !== undefined ? `,\n raster: ${JSON.stringify(gridRaster)}` : ""
1559
1686
  }
1560
1687
  })
1561
1688
  .items(
@@ -1575,162 +1702,93 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
1575
1702
  // commented out for a long time and the body referenced undefined
1576
1703
  // `rta`, `el` and `i`, so wiring it back up would have thrown.
1577
1704
 
1705
+ // Ignored obj: every checkbox was named "acceptTerms" and labelled
1706
+ // "Check it out!". `label` accepts a plain string — the component wants a
1707
+ // component, so a string is wrapped here rather than making every caller
1708
+ // construct a Text.
1578
1709
  static checkbox(obj){
1579
- return new Checkbox().set({
1580
- name: "acceptTerms",
1581
- label: new Text("Check it out!").set({size: "S6", color: "#1abc9c", font: "Arial"}),
1582
- mar: "10px",
1583
- // size: "100px",
1584
- // customStyle: true,
1585
- // checkedBackgroundColor: "#1abc9c",
1586
- // clipPath: "polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)"
1587
- });
1588
-
1710
+ const el = obj.el;
1711
+ const label = typeof el.label === "string"
1712
+ ? new Text(el.label).set({size: "S6"})
1713
+ : el.label;
1714
+ return this.attachRaster(new Checkbox().set({
1715
+ ...this.elOpts(el, ["label"]),
1716
+ ...(label !== undefined ? {label} : {}),
1717
+ }), el, obj.customOptions);
1589
1718
  }
1590
1719
 
1720
+ // Ignored obj: every stack rendered the same photo and "Samuel Suresh".
1721
+ // Children come from the element now, mapped like any other subtree —
1722
+ // the same fix mapWrap got in phase P3.
1591
1723
  static stack(obj){
1592
- return new Stack()
1593
- .set({})
1594
- // .setup({})
1595
- .add([
1596
- new Image().set({url: "https://pbs.twimg.com/media/DwYvbCBVAAEKY_R.jpg"}),
1597
-
1598
-
1599
-
1600
- new Text("Samuel Suresh")
1601
- .set({
1602
- color: "#00ae56",
1603
- font: "SF Pro Display",
1604
- fluidc: "S2",
1605
- pad: {sides: ["top", "left"], value: 20}
1606
- }),
1607
- ]);
1608
-
1724
+ const el = obj.el;
1725
+ const st = new Stack().set(this.elOpts(el));
1726
+ const kids = Array.isArray(el.children) ? el.children : null;
1727
+ if (kids) st.add(kids.map((child) => this.mapType({ ...obj, el: child })));
1728
+ return this.attachRaster(st, el, obj.customOptions);
1609
1729
  }
1610
1730
 
1611
- static form(obj){
1612
-
1613
- // php -S localhost:8000
1614
- // open http://localhost:8000/designertest/1-pagesample.html
1615
- return new Form()
1616
- .set({action:"file.php"}) // 12:45:53 Nice! 21/04/2025
1617
- .add([
1618
- new FloatingInput().set({
1619
- title: "Your name",
1620
- type: "input",
1621
- name: "name"
1622
- }),
1623
-
1624
- new FloatingInput().set({
1625
- title: "Your email",
1626
- type: "input",
1627
- name: "email",
1628
- color: "#3498db",
1629
- font: "Arial",
1630
- mar: [{"a": "1rem"}]
1631
- }),
1632
-
1633
- new RadioGroup()
1634
- .set({
1635
- name: "gender",
1636
- items: ["Male", "Female", "Other"],
1637
- multiple: false,
1638
- tint: "#1abc9c",
1639
- font: "Arial",
1640
- exact: "3rem"
1641
- }),
1642
-
1643
- new Picker()
1644
- .set({
1645
- name: "car",
1646
- items: [
1647
- ["none", "select a car---"], // Placeholder option
1648
- ["tesla", "Tesla"], // Tesla as a selectable option
1649
- ["audi", "Audi"] // Audi as a selectable option
1650
- ],
1651
- font: "Arial",
1652
- arrayPadding: ({sides: ["all"], value: "0.5rem"}),
1653
- radius: "1rem",
1654
- background: "#1abc9c"
1655
- }),
1656
-
1657
- new FilePickera()
1658
- .set({
1659
- id: "A",
1660
- name: "image",
1661
- title: "Add profile picture",
1662
- background: "#3498DB",
1663
- color: "white",
1664
- font: "Arial",
1665
- radius: "3rem",
1666
- // accept: "application/pdf"
1667
- }),
1668
-
1669
- // datalist, range, filepicker, picker
1670
-
1671
- // add radio and read it in PHP
1672
-
1673
- new Button("Submit form").set({ // 174915 If I dont serve it, it downloads the php file
1674
- type: "submit",
1675
- background: "#3498db",
1676
- color: "white",
1677
- weight: "bold",
1678
- radius: "1rem",
1679
- pad: [{"a": "1rem"}], // add
1680
- mar: [{"b": "1rem"}] // add
1681
- })
1682
- ]);
1731
+ // Ignored obj: every form posted to "file.php" and contained the same
1732
+ // five demo fields (a name, an email, a gender radio, a car picker and a
1733
+ // profile-picture upload), whatever the caller asked for. Fields come
1734
+ // from `children` now, mapped like any other subtree.
1735
+ //
1736
+ // { type: "form", action: "/subscribe", method: "post", children: [
1737
+ // { type: "labelInput", title: "Your email", name: "email" },
1738
+ // { type: "button", text: "Subscribe" } ]}
1739
+ //
1740
+ // Form is not an Animator (`class Form {`), so it has no rasterize() and
1741
+ // cannot carry raster ops.
1742
+ static form(obj){
1743
+ const el = obj.el;
1744
+ const form = new Form().set({
1745
+ action: el.action ?? "",
1746
+ ...(el.method !== undefined ? { method: el.method } : {}),
1747
+ });
1748
+ const kids = Array.isArray(el.children) ? el.children : null;
1749
+ if (kids) form.add(kids.map((child) => this.mapType({ ...obj, el: child })));
1750
+ return form;
1683
1751
  }
1684
1752
 
1685
- static simple(obj){
1686
- // Chain the methods of the Simple class
1687
- return `new AreaSwitcher()
1688
- .set({ gap: "10px", height: "700px" })
1689
- .react([
1690
- {
1691
- at: "0", // Default size
1692
- color: "red",
1693
- template: [
1694
- "aaaaaa",
1695
- "bbbbbb",
1696
- "cccccc",
1697
- "dddddd",
1698
- "eeeeee",
1699
- ],
1700
- },
1701
- {
1702
- at: "768", // Medium screen size
1703
- color: "blue",
1704
- template: [
1705
- "aa bbb",
1706
- "aa bbb",
1707
- "cc ddd",
1708
- "ee ddd",
1709
- ],
1710
- },
1711
- {
1712
- at: "1024", // Large screen size
1713
- color: "green",
1714
- template: [
1715
- "aaa bbb",
1716
- "ccc ddd",
1717
- "eee ddd",
1718
- ],
1719
- },
1720
- ])
1753
+ // REMOVED: the old form body — a hardcoded five-field demo. Kept in
1754
+ // git history; the version above builds from `children`.
1755
+ // Ignored obj: every "simple" element emitted the same AreaSwitcher —
1756
+ // three fixed breakpoints of a/b/c/d/e templates and five "Hello A…E"
1757
+ // texts. The regions and the children are the caller's now.
1758
+ //
1759
+ // { type: "simple", gap: "10px", height: "700px",
1760
+ // react: [{ at: "0", template: ["aa", "bb"] },
1761
+ // { at: "768", template: ["ab"] }],
1762
+ // children: [{ type: "h1", text: "A" }, { type: "p", text: "B" }] }
1763
+ //
1764
+ // Emits SOURCE rather than an instance, like mapGrid and mapCopy, so
1765
+ // children are mapped and then serialised through their own toCode().
1766
+ static simple(obj){
1767
+ const el = obj.el;
1768
+ const react = Array.isArray(el.react) ? el.react : [];
1769
+ if (!react.length) {
1770
+ throw new Error('[nodality] a "simple" element needs a non-empty `react` ' +
1771
+ "array of { at, template }.");
1772
+ }
1773
+ const kids = Array.isArray(el.children) ? el.children : [];
1774
+ const kidSrc = kids
1775
+ .map((child) => {
1776
+ const code = this.mapType({ ...obj, el: child }).toCode();
1777
+ return Array.isArray(code) ? code.join("") : String(code);
1778
+ })
1779
+ .join(",\n ");
1780
+
1781
+ return `new AreaSwitcher()
1782
+ .set(${JSON.stringify({
1783
+ gap: el.gap ?? "10px",
1784
+ height: el.height ?? "700px",
1785
+ ...(el.id !== undefined ? { id: el.id } : {}),
1786
+ })})
1787
+ .react(${JSON.stringify(react, null, 2)})
1721
1788
  .add([
1722
- new Text("Hello A").set({size: "S1"}),
1723
- new Text("Hello B").set({size: "S1"}),
1724
- new Text("Hello C").set({size: "S1"}),
1725
- new Text("Hello D").set({size: "S1"}),
1726
- new Text("Hello E").set({size: "S1"})
1789
+ ${kidSrc}
1727
1790
  ])`;
1728
-
1729
-
1730
- // return new Simple().set({}).react([]);
1731
- }
1732
-
1733
-
1791
+ }
1734
1792
 
1735
1793
  static mapWrap(obj) {
1736
1794
  let el = obj.el;
@@ -1740,17 +1798,20 @@ return `new AreaSwitcher()
1740
1798
 
1741
1799
  let kind = obj.el.kind ?? "";
1742
1800
 
1743
-
1744
-
1745
- return new Wrapper(kind)
1801
+ // The caller's own options, then the node-derived ones. Everything
1802
+ // the element declares reaches the Wrapper — this used to forward a
1803
+ // fixed allowlist (id/class/color/size/font/keySet + the op slots)
1804
+ // and drop the rest, so a `wrap` element could not carry a width, a
1805
+ // background, or a grid.
1806
+ //
1807
+ // `size` is NOT defaulted here any more. It used to be
1808
+ // `getElType(el.type)`, which for type "wrap" produced the string
1809
+ // "Srap" — getElType slices the digit off h1…h6 and has nothing to
1810
+ // slice on a word. A caller-supplied `size` still passes through.
1811
+ const wrapper = new Wrapper(kind)
1746
1812
  .set({
1747
- id: el.id,
1748
- class: el.class,
1749
- color: el.color,
1750
- size: this.getElType(el.type), // update 23/07/2025
1813
+ ...el,
1751
1814
  font: el.font ?? "Arial",
1752
- keySet: el.keySet,
1753
- // keySet: {key: "border", value: "3px solid green"},
1754
1815
  stroke: this.filtero("blast", el.id, obj.customOptions),
1755
1816
  gradient: this.filtero("gradient", el.id, obj.customOptions),
1756
1817
  animation: this.filtero("animation", el.id, obj.customOptions),
@@ -1758,12 +1819,18 @@ return `new AreaSwitcher()
1758
1819
  transform: this.filtero("transform", el.id, obj.customOptions),
1759
1820
  filtera: this.filtero("filter", el.id, obj.customOptions),
1760
1821
  raster: this.filteroRaster(el.id, obj.customOptions),
1761
- }).add([
1762
- new Text("Hello").set({}),
1763
- new Text("Hello").set({}),
1764
- new Text("Hello").set({}),
1822
+ });
1765
1823
 
1766
- ]);
1824
+ // Children come from the element. There used to be three
1825
+ // `new Text("Hello")` here unconditionally, so every `wrap` in
1826
+ // every page rendered the same placeholder content and no page
1827
+ // could put anything of its own inside one.
1828
+ const kids = Array.isArray(el.children) ? el.children : null;
1829
+ if (kids) {
1830
+ wrapper.add(kids.map((child) =>
1831
+ this.mapType({ ...obj, el: child })));
1832
+ }
1833
+ return wrapper;
1767
1834
  }
1768
1835
 
1769
1836
 
@@ -1778,15 +1845,13 @@ return `new AreaSwitcher()
1778
1845
 
1779
1846
 
1780
1847
 
1781
- return new UList(kind)
1848
+ // Same two fixes as mapWrap: the element's own options reach the
1849
+ // component, and `size` is not defaulted to the "Srap"-shaped
1850
+ // output of getElType on a non-heading type.
1851
+ const list = new UList(kind)
1782
1852
  .set({
1783
- id: el.id,
1784
- class: el.class,
1785
- color: el.color,
1786
- size: this.getElType(el.type), // update 23/07/2025
1853
+ ...el,
1787
1854
  font: el.font ?? "Arial",
1788
- keySet: el.keySet,
1789
- // keySet: {key: "border", value: "3px solid green"},
1790
1855
  stroke: this.filtero("blast", el.id, obj.customOptions),
1791
1856
  gradient: this.filtero("gradient", el.id, obj.customOptions),
1792
1857
  animation: this.filtero("animation", el.id, obj.customOptions),
@@ -1794,11 +1859,17 @@ return `new AreaSwitcher()
1794
1859
  transform: this.filtero("transform", el.id, obj.customOptions),
1795
1860
  filtera: this.filtero("filter", el.id, obj.customOptions),
1796
1861
  raster: this.filteroRaster(el.id, obj.customOptions),
1797
- }).setItems([
1798
- new Text("First").set({}),
1799
- new Text("Second").set({}),
1800
- new Text("Third").set({})
1801
- ]);
1862
+ });
1863
+
1864
+ // `items` are the list's own; the hardcoded First/Second/Third
1865
+ // placeholders they replace made every ulist identical.
1866
+ const items = Array.isArray(el.items) ? el.items : null;
1867
+ if (items) {
1868
+ list.setItems(items.map((item) => (typeof item === "string"
1869
+ ? new Text(item).set({})
1870
+ : this.mapType({ ...obj, el: item }))));
1871
+ }
1872
+ return list;
1802
1873
  }
1803
1874
 
1804
1875
 
@@ -1847,6 +1918,39 @@ return `new AreaSwitcher()
1847
1918
  // invisible to filtero's `l.op.name` matching and never collide with
1848
1919
  // the CSS-level ops. Returns undefined when nothing applies so the
1849
1920
  // property is dropped from the generated code.
1921
+ /**
1922
+ * The element's own options, minus the keys that belong to the mapper
1923
+ * rather than to the component.
1924
+ *
1925
+ * `type` is always dropped: it is the ELEMENT type ("input"), not an
1926
+ * `<input type>`, and spreading it into a field component would set
1927
+ * type="input" on the DOM node. Components that want an HTML type read
1928
+ * `inputType` instead.
1929
+ */
1930
+ static elOpts(el, drop = []) {
1931
+ const skip = new Set(["type", "children", "items", "breakpoints",
1932
+ "inputType", "text", "url", ...drop]);
1933
+ const out = {};
1934
+ for (const k of Object.keys(el)) if (!skip.has(k)) out[k] = el[k];
1935
+ return out;
1936
+ }
1937
+
1938
+ /**
1939
+ * Attach a raster chain to a component whose own `set()` bypasses
1940
+ * Animator.commonMethods() — where `obj.raster && this.rasterize(...)`
1941
+ * lives. Passing `raster:` to those would be a silently dropped key.
1942
+ *
1943
+ * Returns the instance so it can be used inline. Components that are
1944
+ * not Animators at all (Form, RadioGroup, Switcher) have no
1945
+ * `rasterize`; they are skipped rather than crashed, and the gap is
1946
+ * recorded in the element-mapper tests.
1947
+ */
1948
+ static attachRaster(inst, el, customOptions) {
1949
+ const chain = this.filteroRaster(el.id, customOptions);
1950
+ if (chain && inst && typeof inst.rasterize === "function") inst.rasterize(chain);
1951
+ return inst;
1952
+ }
1953
+
1850
1954
  static filteroRaster(id, customOptions) {
1851
1955
  const list = (customOptions || []).filter((l) =>
1852
1956
  l && typeof l.op === "string" && RASTER_OP_NAMES.includes(l.op) &&