nodality 1.2.11 → 1.3.1
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.
- package/bin/mcp-server.mjs +45 -0
- package/dist/animator.cjs.js +1 -1
- package/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.esm.js +1 -1
- package/dist/beta-mobile-bar.esm.js.LICENSE.txt +1 -1
- package/dist/bundle.umd.js +1 -1
- package/dist/bundle.umd.js.LICENSE.txt +1 -1
- package/dist/button.cjs.js +1 -1
- package/dist/button.cjs.js.LICENSE.txt +1 -1
- package/dist/button.esm.js +1 -1
- package/dist/button.esm.js.LICENSE.txt +1 -1
- package/dist/card-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/card-getter.esm.js.LICENSE.txt +1 -1
- package/dist/center.cjs.js +1 -1
- package/dist/center.cjs.js.LICENSE.txt +1 -1
- package/dist/center.esm.js +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js +1 -1
- package/dist/container.cjs.js.LICENSE.txt +1 -1
- package/dist/container.esm.js +1 -1
- package/dist/container.esm.js.LICENSE.txt +1 -1
- package/dist/data-list.cjs.js.LICENSE.txt +1 -1
- package/dist/data-list.esm.js.LICENSE.txt +1 -1
- package/dist/designer.cjs.js +1 -1
- package/dist/designer.cjs.js.LICENSE.txt +1 -1
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +1 -1
- package/dist/element-mapper.cjs.js +1 -1
- package/dist/element-mapper.cjs.js.LICENSE.txt +1 -1
- package/dist/element-mapper.esm.js +1 -1
- package/dist/element-mapper.esm.js.LICENSE.txt +1 -1
- package/dist/finalresult.esm.js +1 -1
- package/dist/finalresult.esm.js.LICENSE.txt +1 -1
- package/dist/flex-card.cjs.js +1 -1
- package/dist/flex-card.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-row.esm.js +1 -1
- package/dist/flex-row.esm.js.LICENSE.txt +1 -1
- package/dist/floating-input.cjs.js +1 -1
- package/dist/floating-input.cjs.js.LICENSE.txt +1 -1
- package/dist/floating-input.esm.js +1 -1
- package/dist/floating-input.esm.js.LICENSE.txt +1 -1
- package/dist/free.cjs.js +1 -1
- package/dist/free.cjs.js.LICENSE.txt +1 -1
- package/dist/free.esm.js +1 -1
- package/dist/free.esm.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.cjs.js.LICENSE.txt +1 -1
- package/dist/horizontal-scroller.esm.js.LICENSE.txt +1 -1
- package/dist/image-picker.cjs.js +1 -1
- package/dist/image-picker.cjs.js.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js +1 -1
- package/dist/image.cjs.js.LICENSE.txt +1 -1
- package/dist/image.esm.js +1 -1
- package/dist/image.esm.js.LICENSE.txt +1 -1
- package/dist/index.cjs.js +1 -1
- package/dist/index.cjs.js.LICENSE.txt +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.cjs.js.LICENSE.txt +1 -1
- package/dist/keyframe-animation.esm.js.LICENSE.txt +1 -1
- package/dist/link-getter.cjs.js +1 -1
- package/dist/link-getter.cjs.js.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.esm.js +1 -1
- package/dist/new-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/picker.cjs.js +1 -1
- package/dist/picker.cjs.js.LICENSE.txt +1 -1
- package/dist/picker.esm.js +1 -1
- package/dist/picker.esm.js.LICENSE.txt +1 -1
- package/dist/progress.cjs.js +1 -1
- package/dist/progress.cjs.js.LICENSE.txt +1 -1
- package/dist/progress.esm.js +1 -1
- package/dist/progress.esm.js.LICENSE.txt +1 -1
- package/dist/radio.cjs.js.LICENSE.txt +1 -1
- package/dist/radio.esm.js.LICENSE.txt +1 -1
- package/dist/range.cjs.js.LICENSE.txt +1 -1
- package/dist/range.esm.js.LICENSE.txt +1 -1
- package/dist/scroll-video.cjs.js.LICENSE.txt +1 -1
- package/dist/scroll-video.esm.js.LICENSE.txt +1 -1
- package/dist/side-bar.cjs.js +1 -1
- package/dist/side-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +1 -1
- package/dist/stack.esm.js +1 -1
- package/dist/stack.esm.js.LICENSE.txt +1 -1
- package/dist/stacker.cjs.js.LICENSE.txt +1 -1
- package/dist/stacker.esm.js.LICENSE.txt +1 -1
- package/dist/table.cjs.js +1 -1
- package/dist/table.cjs.js.LICENSE.txt +1 -1
- package/dist/table.esm.js +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js +1 -1
- package/dist/text.cjs.js.LICENSE.txt +1 -1
- package/dist/text.esm.js +1 -1
- package/dist/text.esm.js.LICENSE.txt +1 -1
- package/dist/theme.cjs.js.LICENSE.txt +1 -1
- package/dist/theme.esm.js.LICENSE.txt +1 -1
- package/dist/transform-anim.cjs.js.LICENSE.txt +1 -1
- package/dist/transform-anim.esm.js.LICENSE.txt +1 -1
- package/dist/ulist.cjs.js +1 -1
- package/dist/ulist.cjs.js.LICENSE.txt +1 -1
- package/dist/ulist.esm.js +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +15 -2
- package/layout/audio.js +1 -1
- package/layout/audionew.js +1 -1
- package/layout/base.js +1 -1
- package/layout/beta-desktop-bar.js +1 -1
- package/layout/beta-mobile-bar.js +1 -1
- package/layout/button.js +1 -1
- package/layout/center.js +1 -1
- package/layout/checkbox.js +1 -1
- package/layout/circle.js +1 -1
- package/layout/code.js +1 -1
- package/layout/container.js +1 -1
- package/layout/dropdown-2025.js +10 -3
- package/layout/flex-card.js +1 -1
- package/layout/flex-grid.js +1 -1
- package/layout/flex-row.js +1 -1
- package/layout/form-components/custom.js +1 -1
- package/layout/form-components/data-list.js +1 -1
- package/layout/form-components/floating-input.js +23 -4
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +5 -3
- package/layout/form-components/picker.js +1 -1
- package/layout/form-components/radio.js +1 -1
- package/layout/form-components/radiogroup.js +1 -1
- package/layout/form-components/range.js +1 -1
- package/layout/free.js +1 -1
- package/layout/grid-switcher.js +1 -1
- package/layout/grid.js +1 -1
- package/layout/horizontal-scroller.js +1 -1
- package/layout/image.js +15 -2
- package/layout/index.js +1 -1
- package/layout/link.js +1 -1
- package/layout/list.js +1 -1
- package/layout/meta-adder.js +1 -1
- package/layout/modal-2025.js +1 -1
- package/layout/morph.js +1 -1
- package/layout/multiswitcher.js +1 -1
- package/layout/nav-bar.js +1 -1
- package/layout/nav-factor/custom-div.js +1 -1
- package/layout/new-nav-bar.js +1 -1
- package/layout/polygon.js +1 -1
- package/layout/prerender-site.js +1 -1
- package/layout/prerender.js +1 -1
- package/layout/progress.js +1 -1
- package/layout/row.js +1 -1
- package/layout/scroll-video.js +1 -1
- package/layout/side-bar.js +11 -3
- package/layout/side-nav-bar.js +16 -3
- package/layout/simple-bar.js +1 -1
- package/layout/slider-2025.js +1 -1
- package/layout/spacer.js +1 -1
- package/layout/stack.js +1 -1
- package/layout/svg.js +1 -1
- package/layout/switcher.js +1 -1
- package/layout/table.js +1 -1
- package/layout/text-field.js +16 -1
- package/layout/text.js +28 -1
- package/layout/ulist.js +1 -1
- package/layout/video.js +1 -1
- package/layout/wrap.js +1 -1
- package/layout/zoom-card.js +1 -1
- package/lib/agent-surface.js +1 -1
- package/lib/card-getter.js +1 -1
- package/lib/check-page.js +348 -0
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +64 -2
- package/lib/element-mapper.js +121 -23
- package/lib/element-params.generated.js +2 -2
- package/lib/keyframe-animation.js +1 -1
- package/lib/link-getter.js +1 -1
- package/lib/morph-node.js +8 -1
- package/lib/normalize-spec.js +134 -0
- package/lib/parse-html.js +1 -1
- package/lib/raster-inspect.js +1 -1
- package/lib/raster-ops.js +1 -1
- package/lib/raster-presets.js +1 -1
- package/lib/resolve-refs.js +121 -0
- package/lib/scroll-video.js +1 -1
- package/lib/seo.js +1 -1
- package/lib/stacker.js +1 -1
- package/lib/suggest.js +1 -1
- package/lib/theme.js +1 -1
- package/lib/transform-anim.js +1 -1
- package/lib/transition.js +1 -1
- package/lib/validate-nodes.js +111 -3
- package/lib/webmcp-adapter.js +1 -1
- package/package.json +3 -1
- package/schema.json +973 -12
- package/scripts/generate-schema.mjs +9 -1
- package/skills/nodality/SKILL.md +50 -0
package/lib/element-mapper.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.1
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -88,6 +88,17 @@ const ELEMENT_TYPES = [
|
|
|
88
88
|
// ignoring it was the defect Stage 4 fixes - throwing on it would be a worse
|
|
89
89
|
// one. Missing fields are filled rather than left undefined, because the
|
|
90
90
|
// mapper derives an id from the title and a missing title threw.
|
|
91
|
+
// One id, however it was spelled. `#hero` and `hero` name the same element,
|
|
92
|
+
// and until now which one worked depended on the subsystem: raster targeting
|
|
93
|
+
// matched by exact string, morph-node stripped the hash, and chainReact
|
|
94
|
+
// required it. Both spellings are accepted everywhere now, and neither is made
|
|
95
|
+
// canonical — normalising only at the point of comparison means every existing
|
|
96
|
+
// page keeps working whichever it used.
|
|
97
|
+
const sameId = (a, b) => {
|
|
98
|
+
const bare = (v) => typeof v === "string" && v.charAt(0) === "#" ? v.slice(1) : v;
|
|
99
|
+
return bare(a) === bare(b);
|
|
100
|
+
};
|
|
101
|
+
|
|
91
102
|
const navEntry = (i) =>
|
|
92
103
|
(typeof i === "string" || typeof i === "number")
|
|
93
104
|
? { title: String(i), link: "#e" }
|
|
@@ -202,8 +213,12 @@ class ElementMapper { // 22:09:58 04/11/2024
|
|
|
202
213
|
font: "Arial"
|
|
203
214
|
},
|
|
204
215
|
headStyle: {
|
|
216
|
+
// White on #ff6d22 measured 2.81:1 against a 4.5:1
|
|
217
|
+
// requirement — a header that reads as styled and is not
|
|
218
|
+
// legible. #c2410c is 5.18:1, and is where the card title
|
|
219
|
+
// moved to, so the palette stays one palette.
|
|
205
220
|
color: "white",
|
|
206
|
-
background: "#
|
|
221
|
+
background: "#c2410c"
|
|
207
222
|
},
|
|
208
223
|
border: "2px solid black",
|
|
209
224
|
center: true,
|
|
@@ -546,6 +561,21 @@ animation: {
|
|
|
546
561
|
return this.attachRaster(new FloatingInput()
|
|
547
562
|
.set({
|
|
548
563
|
type: el.inputType ?? "input",
|
|
564
|
+
// FloatingInput calls it `title`; every generator and every
|
|
565
|
+
// UI kit calls it `label`, and both solvers in the eval
|
|
566
|
+
// wrote `label`. The field it produced then had no
|
|
567
|
+
// accessible name at all — a placeholder is not one, since
|
|
568
|
+
// it disappears the moment anyone types.
|
|
569
|
+
//
|
|
570
|
+
// `text` joined them after Tier 7. A model wrote
|
|
571
|
+
// `{type:"labelInput", text:"Name"}` and the field rendered
|
|
572
|
+
// the literal string "undefined" as its label, its
|
|
573
|
+
// placeholder and its accessible name, while validate_nodes
|
|
574
|
+
// reported ok — the worst available outcome for a spelling
|
|
575
|
+
// this natural. Tier 6's rule applies: take what the world
|
|
576
|
+
// already writes. `title` still wins, then `label`.
|
|
577
|
+
...(el.title === undefined && (el.label !== undefined || el.text !== undefined)
|
|
578
|
+
? { title: el.label !== undefined ? el.label : el.text } : {}),
|
|
549
579
|
...this.elOpts(el),
|
|
550
580
|
}), el, obj.customOptions);
|
|
551
581
|
}
|
|
@@ -578,6 +608,13 @@ animation: {
|
|
|
578
608
|
|
|
579
609
|
|
|
580
610
|
static dropdown(obj){
|
|
611
|
+
// A label for the trigger, which is what makes every declared option
|
|
612
|
+
// selectable. Without one the first item becomes the label and is
|
|
613
|
+
// spent — `items: ["CZE","POL","DEU"]` offered two. `label` is the
|
|
614
|
+
// name a generator reaches for (it is what <select> and every UI kit
|
|
615
|
+
// call it); `title` is what the component calls it, so both are taken.
|
|
616
|
+
const ddLabel = obj.el.label ?? obj.el.title;
|
|
617
|
+
|
|
581
618
|
if (!obj.el.items){
|
|
582
619
|
obj.el.items = ["Flower", "Car", "Maseratti"];
|
|
583
620
|
}
|
|
@@ -604,6 +641,9 @@ animation: {
|
|
|
604
641
|
// `raster:` option here would be silently dropped.
|
|
605
642
|
const dd = new Dropdown()
|
|
606
643
|
.set({
|
|
644
|
+
// Supplied only when there is one, so a dropdown without a label
|
|
645
|
+
// keeps the shape it always had, byte for byte.
|
|
646
|
+
...(ddLabel === undefined ? {} : { title: ddLabel }),
|
|
607
647
|
behaviour: "click", // click otherwise
|
|
608
648
|
//width: "120px",
|
|
609
649
|
|
|
@@ -623,7 +663,7 @@ animation: {
|
|
|
623
663
|
new Text("Option 3"),*/
|
|
624
664
|
|
|
625
665
|
|
|
626
|
-
new Text(obj.el.items[0]).set({
|
|
666
|
+
...(ddLabel !== undefined ? [] : [new Text(obj.el.items[0]).set({
|
|
627
667
|
font: "Arial",
|
|
628
668
|
fluidc: "S6",
|
|
629
669
|
weight: "bold",
|
|
@@ -635,10 +675,12 @@ animation: {
|
|
|
635
675
|
},
|
|
636
676
|
pad: [{l: 10, r: 10}],
|
|
637
677
|
|
|
638
|
-
}),
|
|
678
|
+
})]),
|
|
639
679
|
|
|
640
680
|
new Wrapper().set({border: "1px solid green", background: "#1abc9c", radius: "0.7rem", socenter: true})
|
|
641
|
-
|
|
681
|
+
// Every item is an option when the trigger carries its own label;
|
|
682
|
+
// otherwise the first is spent on the label, as before.
|
|
683
|
+
.add(ddLabel === undefined ? items.slice(1) : items
|
|
642
684
|
|
|
643
685
|
/* new Text("Option 1o").set({font: "Arial", pad: [{a: 10}],}),
|
|
644
686
|
new Text("Option 2").set({font: "Arial", pad: [{a: 10}],}),
|
|
@@ -719,8 +761,10 @@ alert("PP")
|
|
|
719
761
|
const navItems = Array.isArray(obj.el?.items) && obj.el.items.length
|
|
720
762
|
? obj.el.items.map(navEntry)
|
|
721
763
|
: null;
|
|
764
|
+
// `pad` gives the link a hit area: unpadded these measured 43x18,
|
|
765
|
+
// under the 24px WCAG 2.2 minimum in the direction a thumb needs.
|
|
722
766
|
const navLinks = () => navItems.map((i) =>
|
|
723
|
-
new Link().set({ text: i.title, url: i.link }));
|
|
767
|
+
new Link().set({ text: i.title, url: i.link, pad: [{ tb: 8, lr: 4 }] }));
|
|
724
768
|
|
|
725
769
|
let links = items.map((i, o) => new Link(`"${i.title}"`)
|
|
726
770
|
.set({
|
|
@@ -1114,6 +1158,12 @@ new Wrapper().set({
|
|
|
1114
1158
|
|
|
1115
1159
|
// Phase S4, as protoNav above: `items` carries {title, link}; absent,
|
|
1116
1160
|
// the placeholders are used exactly as before.
|
|
1161
|
+
// Whether the caller supplied anything, kept separate from `items`
|
|
1162
|
+
// itself: the placeholders carry a `link` of their own that was never
|
|
1163
|
+
// rendered, so honouring it now would change output that has to stay
|
|
1164
|
+
// byte-identical. Supplied entries use their link; placeholders keep
|
|
1165
|
+
// the hardcoded url they have always rendered.
|
|
1166
|
+
const suppliedNav = Array.isArray(obj.el?.items) && obj.el.items.length > 0;
|
|
1117
1167
|
let items = Array.isArray(obj.el?.items) && obj.el.items.length
|
|
1118
1168
|
? obj.el.items.map(navEntry)
|
|
1119
1169
|
: [
|
|
@@ -1139,7 +1189,11 @@ new Wrapper().set({
|
|
|
1139
1189
|
text: i.title,
|
|
1140
1190
|
link: i.link,
|
|
1141
1191
|
isNavA: true,
|
|
1142
|
-
|
|
1192
|
+
// The caller's link, not a hardcoded one. `link:` is not read by
|
|
1193
|
+
// Link — `url` is what becomes the href — so declaring a link here
|
|
1194
|
+
// used to render a fixed value regardless, which is the silent
|
|
1195
|
+
// no-op this whole plan exists to remove.
|
|
1196
|
+
url: suppliedNav ? (i.link || "#e") : "#e",
|
|
1143
1197
|
id: "#" + i.title.toLowerCase(),
|
|
1144
1198
|
font: "Arial",
|
|
1145
1199
|
pad: [{a:20}],
|
|
@@ -1240,7 +1294,11 @@ new Wrapper().set({
|
|
|
1240
1294
|
text: i.title,
|
|
1241
1295
|
link: i.link,
|
|
1242
1296
|
isNavA: true,
|
|
1243
|
-
|
|
1297
|
+
// The caller's link, not a hardcoded one. `link:` is not read by
|
|
1298
|
+
// Link — `url` is what becomes the href — so declaring a link here
|
|
1299
|
+
// used to render a fixed value regardless, which is the silent
|
|
1300
|
+
// no-op this whole plan exists to remove.
|
|
1301
|
+
url: suppliedNav ? (i.link || "#myURL") : "#myURL",
|
|
1244
1302
|
id: "#" + i.title.toLowerCase(),
|
|
1245
1303
|
font: "Arial",
|
|
1246
1304
|
pad: [{a:20}],
|
|
@@ -1347,7 +1405,11 @@ if (obj.el.dropdown){
|
|
|
1347
1405
|
radius: "1rem",
|
|
1348
1406
|
isSide: true,
|
|
1349
1407
|
background: "#ecf0f1",
|
|
1350
|
-
|
|
1408
|
+
// Both hamburger states measured under 3:1 on white at 26px:
|
|
1409
|
+
// #e67e22 was 2.48:1 and #1abc9c 2.16:1. Same hues, dark enough
|
|
1410
|
+
// to be seen — and #c2410c is the orange the cards and the table
|
|
1411
|
+
// header moved to, so one palette rather than three.
|
|
1412
|
+
hamColour: {opened: "#0f766e", closed: "#c2410c"},
|
|
1351
1413
|
mobileSize: "1.2em",
|
|
1352
1414
|
fixed: true,
|
|
1353
1415
|
tags: {open: "sidebar:open", close: "sidebar:closed"}
|
|
@@ -1518,6 +1580,12 @@ let elo = new Polygon({ id: "hex" })
|
|
|
1518
1580
|
class: el.class,
|
|
1519
1581
|
color: el.color,
|
|
1520
1582
|
size: this.getElType(el.type), // update 23/07/2025
|
|
1583
|
+
//@ tag: Heading level to render, independent of the type's size.
|
|
1584
|
+
// Without this the option exists on the component and is not
|
|
1585
|
+
// reachable from a spec, which for a data-first format means it
|
|
1586
|
+
// does not exist. The card template uses it; an author writing
|
|
1587
|
+
// {type:"h5", tag:"h2"} must get the same lever.
|
|
1588
|
+
tag: el.tag,
|
|
1521
1589
|
font: el.font ?? "Arial",
|
|
1522
1590
|
// index: obj.i + "",
|
|
1523
1591
|
// keySet: {key: "border", value: "3px solid green"},
|
|
@@ -1678,15 +1746,12 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1678
1746
|
})
|
|
1679
1747
|
.items([
|
|
1680
1748
|
new Image(item.img).set({isFull: true, url: item.img}),
|
|
1681
|
-
// #
|
|
1682
|
-
|
|
1683
|
-
// this block lives inside a template literal and is evaluated as
|
|
1684
|
-
// source, so module scope is not visible either.
|
|
1685
|
-
new Text(item.title).set({ size: "S5", color: "#f97316" }),
|
|
1749
|
+
// #c2410c is 5.18:1 on white; h2 keeps the outline intact.
|
|
1750
|
+
new Text(item.title).set({ size: "S5", tag: "h2", color: "#c2410c" }),
|
|
1686
1751
|
new Link("Link").set({
|
|
1687
1752
|
text: item.title,
|
|
1688
1753
|
url: item.link,
|
|
1689
|
-
background: "#
|
|
1754
|
+
background: "#1d6fe0",
|
|
1690
1755
|
pad: [{ lr: "0.5rem", tb: "1rem" }],
|
|
1691
1756
|
radius: "0.4rem",
|
|
1692
1757
|
color: "white",
|
|
@@ -1694,6 +1759,28 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1694
1759
|
})
|
|
1695
1760
|
])`;
|
|
1696
1761
|
|
|
1762
|
+
// WHY THE CARD'S COLOURS AND HEADING LEVEL ARE WHAT THEY ARE.
|
|
1763
|
+
//
|
|
1764
|
+
// Kept OUT of the template above on purpose. Everything inside that string
|
|
1765
|
+
// is source the developer receives, so a paragraph of rationale there is a
|
|
1766
|
+
// paragraph in every generated page: fourteen lines of commentary about
|
|
1767
|
+
// contrast ratios added 700 characters to the emitted output before this
|
|
1768
|
+
// was noticed. Moving it here — and taking the older four-line note with
|
|
1769
|
+
// it — left the emission 235 characters SMALLER than before this work.
|
|
1770
|
+
//
|
|
1771
|
+
// The title was #f97316, chosen over the CSS keyword orange because that
|
|
1772
|
+
// reads yellow beside modern palettes. A considered choice that measured
|
|
1773
|
+
// 2.80:1 on white against a 3:1 requirement and missed it by a margin
|
|
1774
|
+
// nobody catches by looking. #c2410c is 5.18:1 and clears both the large-
|
|
1775
|
+
// and small-text thresholds whatever size the title renders at. The link
|
|
1776
|
+
// background moved from #3498db (white on it: 3.15:1) to #1d6fe0 (4.77:1),
|
|
1777
|
+
// already this library's blue elsewhere.
|
|
1778
|
+
//
|
|
1779
|
+
// The tag option decouples the document outline from the type scale: the
|
|
1780
|
+
// title stays S5-sized and stops being an h5, which made every page with
|
|
1781
|
+
// an h1 above a card grid skip h2, h3 and h4 at once. h2, because a card
|
|
1782
|
+
// grid is normally a section of a page rather than a subsection of one;
|
|
1783
|
+
// pass tag to nest it deeper.
|
|
1697
1784
|
// zoom card block (string template)
|
|
1698
1785
|
const zoomCard = `
|
|
1699
1786
|
new ZoomCard()
|
|
@@ -1705,11 +1792,11 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1705
1792
|
useBrightness: true
|
|
1706
1793
|
})
|
|
1707
1794
|
.items([
|
|
1708
|
-
new Text(item.title).set({ fluidc: "S6", color: "#
|
|
1795
|
+
new Text(item.title).set({ fluidc: "S6", tag: "h2", color: "#c2410c" }),
|
|
1709
1796
|
new Link("Link").set({
|
|
1710
1797
|
text: item.title,
|
|
1711
1798
|
url: item.link,
|
|
1712
|
-
background: "#
|
|
1799
|
+
background: "#1d6fe0",
|
|
1713
1800
|
pad: [{ lr: "0.5rem", tb: "1rem" }],
|
|
1714
1801
|
radius: "0.4rem",
|
|
1715
1802
|
color: "white"
|
|
@@ -1806,12 +1893,23 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1806
1893
|
const shorthand = items.filter((it) => !isSpecList(it));
|
|
1807
1894
|
const specLists = items.filter(isSpecList);
|
|
1808
1895
|
|
|
1896
|
+
// Alt text, forwarded only when some entry carries it. The template
|
|
1897
|
+
// is emitted SOURCE, so anything added here is paid by every generated
|
|
1898
|
+
// page — including the no-items path, whose output is pinned byte for
|
|
1899
|
+
// byte. Widening the template only when the data needs it keeps that
|
|
1900
|
+
// pin intact and still lets the terse form describe its images, which
|
|
1901
|
+
// matters because the terse form is the one a generator reaches for.
|
|
1902
|
+
const altTemplate = (tpl) =>
|
|
1903
|
+
shorthand.some((it) => it && it.alt !== undefined)
|
|
1904
|
+
? tpl.replace("url: item.img}", "url: item.img, alt: item.alt}")
|
|
1905
|
+
: tpl;
|
|
1906
|
+
|
|
1809
1907
|
// Every entry is shorthand: keep the `.map()` form, so the emitted
|
|
1810
1908
|
// code stays as compact as the hand-written equivalent and the
|
|
1811
1909
|
// template is still paid once.
|
|
1812
1910
|
if (specLists.length === 0) {
|
|
1813
1911
|
return `${JSON.stringify(shorthand, null, 2).split("\n").map((l, i) => i === 0 ? l : " " + l).join("\n")}.map(item =>
|
|
1814
|
-
${cardTemplate}
|
|
1912
|
+
${altTemplate(cardTemplate)}
|
|
1815
1913
|
)`;
|
|
1816
1914
|
}
|
|
1817
1915
|
|
|
@@ -1852,9 +1950,9 @@ const blast = this.filtero("blast", el.id, obj.customOptions);
|
|
|
1852
1950
|
return `new Card()
|
|
1853
1951
|
.set({ width: "300px", height: "700px", radius: "0.7rem", mar: { sides: ["all"], value: "0.8rem" } })
|
|
1854
1952
|
.items([
|
|
1855
|
-
new Image(${s(item.img)}).set({isFull: true, url: ${s(item.img)}}),
|
|
1856
|
-
new Text(${s(item.title)}).set({ size: "S5", color: "#
|
|
1857
|
-
new Link("Link").set({ text: ${s(item.title)}, url: ${s(item.link)}, background: "#
|
|
1953
|
+
new Image(${s(item.img)}).set({isFull: true, url: ${s(item.img)}${item.alt === undefined ? "" : `, alt: ${s(item.alt)}`}}),
|
|
1954
|
+
new Text(${s(item.title)}).set({ size: "S5", tag: "h2", color: "#c2410c" }),
|
|
1955
|
+
new Link("Link").set({ text: ${s(item.title)}, url: ${s(item.link)}, background: "#1d6fe0", pad: [{ lr: "0.5rem", tb: "1rem" }], radius: "0.4rem", color: "white" })
|
|
1858
1956
|
])`;
|
|
1859
1957
|
}
|
|
1860
1958
|
|
|
@@ -2065,7 +2163,7 @@ static form(obj){
|
|
|
2065
2163
|
.filter(l => l.op.name === name)
|
|
2066
2164
|
.filter(l => {
|
|
2067
2165
|
if (l.target) {
|
|
2068
|
-
return l.target.
|
|
2166
|
+
return l.target.some((t) => sameId(t, id));
|
|
2069
2167
|
} else {
|
|
2070
2168
|
return true;
|
|
2071
2169
|
}
|
|
@@ -2116,7 +2214,7 @@ static form(obj){
|
|
|
2116
2214
|
static filteroRaster(id, customOptions) {
|
|
2117
2215
|
const list = (customOptions || []).filter((l) =>
|
|
2118
2216
|
l && typeof l.op === "string" && RASTER_OP_NAMES.includes(l.op) &&
|
|
2119
|
-
(!l.target || (id && l.target.
|
|
2217
|
+
(!l.target || (id && l.target.some((t) => sameId(t, id)))));
|
|
2120
2218
|
return list.length > 0 ? list : undefined;
|
|
2121
2219
|
}
|
|
2122
2220
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.1
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
// Every parameter name any element type reads, recovered from source.
|
|
9
9
|
// Used for near-miss typo detection in validate-nodes.js. A drift test
|
|
10
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"];
|
|
11
|
+
export const ELEMENT_PARAM_NAMES = ["absolute","accept","action","align","alignIts","alignTo","aligns","alt","animate","animation","area","arrayMargin","arrayPadding","as","at","auto","backTo","background","backgroundCard","backgroundColor","backgroundOp","behaviour","blast","blastData","block","bold","border","borderObj","borderObja","borderRadius","brand","breakWord","breakpoints","cellAlign","cellPadding","center","centerColumn","centerSelf","centered","checkedBackgroundColor","child","children","clampc","class","clipPath","code","colat","color","column","contentWidth","cursor","customAlign","customJustify","customStyle","data","decoding","diameter","disp","dropdown","em","exact","fetchPriority","filter","filtera","fitContent","fixMobileTap","fixed","flex","flexCenter","flexDir","fluid","fluidc","font","frame","ga","gap","gpos","gradient","hamColour","hamburgerColour","headStyle","height","hideField","hideOverflow","hover","icon","id","img","index","initLetter","inline","inpad","inputType","isBackground","isFull","isHidden","isLink","isNavA","italic","items","justifo","justify","keySet","kind","label","layout","left","length","link","loading","map","mar","margin","marginOp","maxHeight","maxWidth","mboth","method","minHeight","mobile","multiple","name","new","nowrap","objectFit","objectPosition","offcanvas","okayColor","onScroll","onTap","opacity","overflow","owrap","pad","padding","placeholder","position","positions","preffersId","radius","ranges","raster","react","removeDecoration","required","resmar","respad","responsive","resprop","rounded","scale","sequence","shadow","sides","simpleBorder","simpleCenter","size","sizes","span","srcset","sticky","stretch","stroke","style","stype","svg","tag","tags","templateCols","text","theme","tint","title","toColumn","top","transform","transition","type","url","useBrightness","value","vtn","weight","width","wrap","x","y","zIndex"];
|
package/lib/link-getter.js
CHANGED
package/lib/morph-node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.1
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -142,6 +142,13 @@ const norm = (t) => String(t || "").replace(/\s+/g, " ").trim().toLowerCase();
|
|
|
142
142
|
const bareId = (id) =>
|
|
143
143
|
typeof id === "string" && id.startsWith("#") ? id.slice(1) : id;
|
|
144
144
|
|
|
145
|
+
// The DOM id a component actually carries, which is not always the bare one:
|
|
146
|
+
// nav and sideNav emit `id="#docs"`, hash included, because animator.js used
|
|
147
|
+
// to require the hash to match. Both spellings are now accepted on both sides,
|
|
148
|
+
// and this is the lookup half — an element is found whether its id was written
|
|
149
|
+
// with the hash or without.
|
|
150
|
+
export const idMatches = (a, b) => bareId(a) === bareId(b);
|
|
151
|
+
|
|
145
152
|
/**
|
|
146
153
|
* A morph node, in one shape: a list of EDGES.
|
|
147
154
|
*
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.3.1
|
|
3
|
+
* (c) 2026 Filip Vabrousek
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tier 6 of AGENTIC-FIRST-PLAN.md §10 — accept what the world already writes.
|
|
9
|
+
*
|
|
10
|
+
* MEASURED FIRST. Running claude-sonnet-5 over the 24-brief eval scored 17/24
|
|
11
|
+
* against 23/24 for a hand-written solver, and **four of the seven failures
|
|
12
|
+
* were one mistake wearing different clothes**: the model reached for the name
|
|
13
|
+
* HTML uses and the library wanted another.
|
|
14
|
+
*
|
|
15
|
+
* src -> url an <img> has src. Two briefs lost to this.
|
|
16
|
+
* href -> url an <a> has href.
|
|
17
|
+
* options -> items a <select> has options.
|
|
18
|
+
*
|
|
19
|
+
* These are not typos, so the did-you-mean built in Stage 3 cannot reach them:
|
|
20
|
+
* `src` is three edits from `url`. And a model writing `src` for an image is
|
|
21
|
+
* not confused — it is applying the most widely attested convention there is.
|
|
22
|
+
* The divergence bought nothing and cost three briefs in twenty-four.
|
|
23
|
+
*
|
|
24
|
+
* The same measurement found one more failure of the same shape rather than
|
|
25
|
+
* the same name: a table's rows written as arrays, header row first, which is
|
|
26
|
+
* how every CSV and every markdown table on earth is laid out. That is handled
|
|
27
|
+
* here too — see `normalizeShapes`.
|
|
28
|
+
*
|
|
29
|
+
* Purely additive. A spec that uses the canonical names and shapes is
|
|
30
|
+
* untouched, so no existing page can observe this. Where both are present the canonical name
|
|
31
|
+
* wins and the collision is REPORTED rather than silently resolved, because
|
|
32
|
+
* quietly dropping a key the author wrote is the exact failure this project
|
|
33
|
+
* has spent every stage removing.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** alias -> canonical. Deliberately short: each entry has to earn its place by
|
|
37
|
+
* being what a generator actually writes, not by being plausible. */
|
|
38
|
+
export const ALIASES = Object.freeze({
|
|
39
|
+
src: "url",
|
|
40
|
+
href: "url",
|
|
41
|
+
options: "items",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const CANONICAL = new Set(Object.values(ALIASES));
|
|
45
|
+
|
|
46
|
+
/** Walk children and items as well, so nested specs get the same treatment. */
|
|
47
|
+
const walk = (node, fn) => {
|
|
48
|
+
if (Array.isArray(node)) return node.map((n) => walk(n, fn));
|
|
49
|
+
if (!node || typeof node !== "object") return node;
|
|
50
|
+
const out = fn({ ...node });
|
|
51
|
+
for (const key of ["children", "items"]) {
|
|
52
|
+
if (out[key] !== undefined) out[key] = walk(out[key], fn);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Rewrite every alias to its canonical name. */
|
|
58
|
+
export function normalizeAliases(elements) {
|
|
59
|
+
return walk(elements, (node) => {
|
|
60
|
+
for (const [alias, canonical] of Object.entries(ALIASES)) {
|
|
61
|
+
if (!Object.prototype.hasOwnProperty.call(node, alias)) continue;
|
|
62
|
+
// Canonical wins. `aliasConflicts` is what tells the author.
|
|
63
|
+
if (!Object.prototype.hasOwnProperty.call(node, canonical)) {
|
|
64
|
+
node[canonical] = node[alias];
|
|
65
|
+
}
|
|
66
|
+
delete node[alias];
|
|
67
|
+
}
|
|
68
|
+
return node;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Places where an alias and its canonical name are both present with
|
|
74
|
+
* DIFFERENT values — one of them is going to be ignored, and the author should
|
|
75
|
+
* hear it from the validator rather than discover it in the page.
|
|
76
|
+
*/
|
|
77
|
+
export function aliasConflicts(elements, at = "elements") {
|
|
78
|
+
const found = [];
|
|
79
|
+
const visit = (node, path) => {
|
|
80
|
+
if (Array.isArray(node)) return node.forEach((n, i) => visit(n, `${path}[${i}]`));
|
|
81
|
+
if (!node || typeof node !== "object") return;
|
|
82
|
+
for (const [alias, canonical] of Object.entries(ALIASES)) {
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(node, alias) &&
|
|
84
|
+
Object.prototype.hasOwnProperty.call(node, canonical) &&
|
|
85
|
+
JSON.stringify(node[alias]) !== JSON.stringify(node[canonical])) {
|
|
86
|
+
found.push({ path: `${path}.${alias}`, alias, canonical });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const key of ["children", "items"]) {
|
|
90
|
+
if (node[key] !== undefined) visit(node[key], `${path}.${key}`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
visit(elements, at);
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Every name this module accepts, for the validator's vocabulary. */
|
|
98
|
+
export const ALIAS_NAMES = Object.freeze(Object.keys(ALIASES));
|
|
99
|
+
export const isCanonical = (name) => CANONICAL.has(name);
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Shapes that are not the library's but are what everyone writes.
|
|
104
|
+
*
|
|
105
|
+
* A table's rows as ARRAYS, header row first:
|
|
106
|
+
*
|
|
107
|
+
* items: [["date","race"], ["29/08","Krusnoman"]]
|
|
108
|
+
* -> items: [{ date: "29/08", race: "Krusnoman" }]
|
|
109
|
+
*
|
|
110
|
+
* The model in the Tier 2b run wrote exactly this, and it is the layout of
|
|
111
|
+
* every CSV and every markdown table there is. Header-first is not a guess:
|
|
112
|
+
* a table whose first row is data has no column names at all, and the mapper
|
|
113
|
+
* needs them to build the head.
|
|
114
|
+
*
|
|
115
|
+
* Narrow on purpose — `table` only, and only when EVERY entry is an array.
|
|
116
|
+
* A mixed list is left alone rather than half-converted.
|
|
117
|
+
*/
|
|
118
|
+
export function normalizeShapes(elements) {
|
|
119
|
+
return walk(elements, (node) => {
|
|
120
|
+
if (node.type !== "table" || !Array.isArray(node.items) || node.items.length < 2) return node;
|
|
121
|
+
if (!node.items.every((r) => Array.isArray(r))) return node;
|
|
122
|
+
|
|
123
|
+
const [head, ...rows] = node.items;
|
|
124
|
+
if (!head.every((h) => typeof h === "string" || typeof h === "number")) return node;
|
|
125
|
+
node.items = rows.map((row) =>
|
|
126
|
+
Object.fromEntries(head.map((key, i) => [String(key), row[i]])));
|
|
127
|
+
return node;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Both passes, in the order the caller wants: names first, then shapes. */
|
|
132
|
+
export function normalizeSpec(elements) {
|
|
133
|
+
return normalizeShapes(normalizeAliases(elements));
|
|
134
|
+
}
|
package/lib/parse-html.js
CHANGED
package/lib/raster-inspect.js
CHANGED
package/lib/raster-ops.js
CHANGED
package/lib/raster-presets.js
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.3.1
|
|
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
|
+
}
|
package/lib/scroll-video.js
CHANGED