nodality 1.2.10 → 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.
- package/bin/mcp-server.mjs +131 -0
- package/bin/schema-cli.mjs +55 -11
- 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 -28
- 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 -28
- package/dist/designer.esm.js +1 -1
- package/dist/designer.esm.js.LICENSE.txt +1 -28
- 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 -28
- 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 -28
- package/dist/index.d.ts +3 -2
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.LICENSE.txt +1 -28
- 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 +14 -1
- 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 +239 -0
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +64 -2
- package/lib/element-mapper.js +113 -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 +14 -2
- 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 +99 -3
- package/lib/webmcp-adapter.js +1 -1
- package/package.json +6 -2
- package/schema.json +8152 -0
- package/scripts/generate-schema.mjs +251 -0
- package/skills/nodality/SKILL.md +121 -1
package/layout/multiswitcher.js
CHANGED
package/layout/nav-bar.js
CHANGED
package/layout/new-nav-bar.js
CHANGED
package/layout/polygon.js
CHANGED
package/layout/prerender-site.js
CHANGED
package/layout/prerender.js
CHANGED
package/layout/progress.js
CHANGED
package/layout/row.js
CHANGED
package/layout/scroll-video.js
CHANGED
package/layout/side-bar.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -52,8 +52,16 @@ class SideBar extends Animator{
|
|
|
52
52
|
btn.style.background = "white";
|
|
53
53
|
btn.style.opacity = 0.80;
|
|
54
54
|
btn.style.border = "none";
|
|
55
|
-
btn.width =
|
|
56
|
-
|
|
55
|
+
// `btn.width = ...` sets a PROPERTY that <button> does not have, so
|
|
56
|
+
// this element had no size at all: 0x0 in the browser, impossible to
|
|
57
|
+
// hit and invisible to the checker's tap-target rule until it was
|
|
58
|
+
// measured. Style, and at least the 24px WCAG 2.2 minimum.
|
|
59
|
+
btn.style.width = "2rem";
|
|
60
|
+
btn.style.height = "2rem";
|
|
61
|
+
btn.style.minWidth = "24px";
|
|
62
|
+
btn.style.minHeight = "24px";
|
|
63
|
+
// A control with no text is a control with nothing to announce.
|
|
64
|
+
if (!btn.getAttribute("aria-label")) btn.setAttribute("aria-label", "Toggle navigation");
|
|
57
65
|
btn.style.borderRadius = "50%";
|
|
58
66
|
btn.style.zIndex = 2;
|
|
59
67
|
|
package/layout/side-nav-bar.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -140,8 +140,16 @@ class SideNav extends Animator {
|
|
|
140
140
|
|
|
141
141
|
createToggleButton(color) {
|
|
142
142
|
const button = document.createElement("button");
|
|
143
|
-
|
|
143
|
+
// textContent, not innerText: jsdom has no innerText setter and the SSG
|
|
144
|
+
// prerenders through jsdom, so this glyph never reached a
|
|
145
|
+
// prerendered page — the toggle shipped as an empty button.
|
|
146
|
+
button.textContent = "☰";
|
|
144
147
|
button.style.padding = "10px 20px";
|
|
148
|
+
// WCAG 2.2 AA asks for 24x24. These measured 40x20: wide enough,
|
|
149
|
+
// and two pixels short in the direction that matters on a phone.
|
|
150
|
+
button.style.minWidth = "24px";
|
|
151
|
+
button.style.minHeight = "24px";
|
|
152
|
+
button.style.lineHeight = "1";
|
|
145
153
|
button.style.backgroundColor = "transparent";
|
|
146
154
|
button.style.color = color ?? "#3498db";
|
|
147
155
|
button.style.border = "none";
|
|
@@ -158,9 +166,14 @@ class SideNav extends Animator {
|
|
|
158
166
|
|
|
159
167
|
createCloseButton(color) {
|
|
160
168
|
const button = document.createElement("button");
|
|
161
|
-
button.
|
|
169
|
+
button.textContent = "×";
|
|
162
170
|
button.style.fontSize = "1.6rem";
|
|
163
171
|
button.style.padding = "10px 20px";
|
|
172
|
+
// WCAG 2.2 AA asks for 24x24. These measured 40x20: wide enough,
|
|
173
|
+
// and two pixels short in the direction that matters on a phone.
|
|
174
|
+
button.style.minWidth = "24px";
|
|
175
|
+
button.style.minHeight = "24px";
|
|
176
|
+
button.style.lineHeight = "1";
|
|
164
177
|
button.style.backgroundColor = "transparent";
|
|
165
178
|
button.style.color = color ?? "black";
|
|
166
179
|
button.style.border = "none";
|
package/layout/simple-bar.js
CHANGED
package/layout/slider-2025.js
CHANGED
package/layout/spacer.js
CHANGED
package/layout/stack.js
CHANGED
package/layout/svg.js
CHANGED
package/layout/switcher.js
CHANGED
package/layout/table.js
CHANGED
package/layout/text-field.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -44,6 +44,21 @@ class TextField extends Animator {
|
|
|
44
44
|
this.res.setAttribute("type", obj.type ?? "text");
|
|
45
45
|
//@ placeholder: Placeholder text shown while the field is empty.
|
|
46
46
|
obj.placeholder && (this.res.placeholder = obj.placeholder);
|
|
47
|
+
|
|
48
|
+
// width:100% plus horizontal padding is 100% PLUS the padding under
|
|
49
|
+
// the default content-box, so a full-width field pushed the page 20px
|
|
50
|
+
// wider than the viewport — the overflow measured exactly the padding.
|
|
51
|
+
this.res.style.boxSizing = "border-box";
|
|
52
|
+
|
|
53
|
+
//@ label: Accessible name for the field.
|
|
54
|
+
// A placeholder is NOT an accessible name: it disappears the moment
|
|
55
|
+
// anyone types, and assistive technology is not required to announce
|
|
56
|
+
// it. Take a real label where there is one and fall back to the
|
|
57
|
+
// placeholder, which is better than nothing at all.
|
|
58
|
+
if (!this.res.getAttribute("aria-label")) {
|
|
59
|
+
const name = obj.label ?? obj.title ?? obj.placeholder;
|
|
60
|
+
name && this.res.setAttribute("aria-label", String(name));
|
|
61
|
+
}
|
|
47
62
|
//@ name: Form field name. What a submitted form actually SENDS.
|
|
48
63
|
//@ id: DOM id on the input.
|
|
49
64
|
//@ required: Marks the field required for native validation.
|
package/layout/text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.
|
|
2
|
+
* nodality v1.3.0
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -228,6 +228,27 @@ this.res.style.position = "absolute";
|
|
|
228
228
|
this.fluid(obj.fluid);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
//@ tag: Heading level to render, independent of `size`/`fluid`.
|
|
232
|
+
// Size and semantics used to be the same decision: `size: "S5"`
|
|
233
|
+
// produced an <h5> and there was no way to have one without the
|
|
234
|
+
// other. That is fine for a person choosing both at once and wrong
|
|
235
|
+
// for a generated page, where the document outline is structure and
|
|
236
|
+
// the type scale is style. A card title wants to LOOK like a card
|
|
237
|
+
// title and SIT at h3, and before this it could not.
|
|
238
|
+
//
|
|
239
|
+
// Additive: absent `tag`, the element is exactly what size chose.
|
|
240
|
+
if (obj.tag && /^(h[1-6]|p)$/.test(String(obj.tag))) {
|
|
241
|
+
const want = String(obj.tag).toLowerCase();
|
|
242
|
+
if (this.res && this.res.tagName.toLowerCase() !== want) {
|
|
243
|
+
const swapped = document.createElement(want);
|
|
244
|
+
for (const a of Array.from(this.res.attributes)) {
|
|
245
|
+
swapped.setAttribute(a.name, a.value);
|
|
246
|
+
}
|
|
247
|
+
while (this.res.firstChild) swapped.appendChild(this.res.firstChild);
|
|
248
|
+
this.res = swapped;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
231
252
|
obj.fluidc && (this.elCSS.push(`font-family: ${obj.font}; \n`));
|
|
232
253
|
obj.align && (this.res.style.textAlign = obj.align);
|
|
233
254
|
|
|
@@ -348,6 +369,12 @@ addIcon(obj){
|
|
|
348
369
|
img.style.marginLeft = "10px";
|
|
349
370
|
img.style.height = "auto";
|
|
350
371
|
img.setAttribute("src", obj.url);
|
|
372
|
+
//@ alt: Description for an icon that carries meaning; omit for decoration.
|
|
373
|
+
// An <img> with no alt at all is an unlabelled image to a screen reader,
|
|
374
|
+
// which is what the nav's dropdown chevron was. Empty alt is the correct
|
|
375
|
+
// answer for decoration: it says "skip this", rather than leaving the
|
|
376
|
+
// reader to announce a URL.
|
|
377
|
+
img.setAttribute("alt", obj.alt === undefined ? "" : String(obj.alt));
|
|
351
378
|
this.res.appendChild(img);
|
|
352
379
|
return this;
|
|
353
380
|
}
|
package/layout/ulist.js
CHANGED
package/layout/video.js
CHANGED
package/layout/wrap.js
CHANGED
package/layout/zoom-card.js
CHANGED
package/lib/agent-surface.js
CHANGED
package/lib/card-getter.js
CHANGED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* nodality v1.3.0
|
|
3
|
+
* (c) 2026 Filip Vabrousek
|
|
4
|
+
* License: MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tier 1 of AGENTIC-FIRST-PLAN.md §10 — what a page actually does.
|
|
9
|
+
*
|
|
10
|
+
* Everything else in this library checks the INPUT. `validate_nodes` checks
|
|
11
|
+
* vocabulary, the schema says what a type accepts, the parser says what a page
|
|
12
|
+
* was made from. None of them can tell an agent that the page it just produced
|
|
13
|
+
* overflows its viewport, clips its own text, or is unreadable. The loop is
|
|
14
|
+
* write-only: a spec can be perfect and the page still wrong.
|
|
15
|
+
*
|
|
16
|
+
* This closes it. The checks need REAL LAYOUT — geometry, computed styles,
|
|
17
|
+
* stacking — which jsdom does not have, so this drives a browser through
|
|
18
|
+
* Playwright. Playwright is not a dependency of this package, for the same
|
|
19
|
+
* reason jsdom is not: it is a build-time concern and bundling it would put a
|
|
20
|
+
* large download into every install that never renders anything. Its absence
|
|
21
|
+
* arrives as a report, never as a stack trace.
|
|
22
|
+
*
|
|
23
|
+
* The report shape is deliberately the one `validate_nodes` returns —
|
|
24
|
+
* `{ ok, errors: [{ code, path, got, suggestions, valid }] }` — because an
|
|
25
|
+
* agent should parse one thing. `path` is a CSS selector rather than a spec
|
|
26
|
+
* path: these are facts about the rendered page, and the selector is what
|
|
27
|
+
* locates them in it.
|
|
28
|
+
*
|
|
29
|
+
* WHAT IT DOES NOT DO. It does not judge whether a design is good. Every check
|
|
30
|
+
* here is a fact that can be measured and that a reasonable page does not
|
|
31
|
+
* exhibit — text outside its box, a target too small to hit, a heading level
|
|
32
|
+
* skipped. Taste is not in scope and should not be faked.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Viewports every page is checked at unless the caller says otherwise. */
|
|
36
|
+
export const DEFAULT_VIEWPORTS = [
|
|
37
|
+
{ name: "mobile", width: 390, height: 844 },
|
|
38
|
+
{ name: "desktop", width: 1280, height: 900 },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The checks, as a function evaluated inside the page. Written as one string
|
|
43
|
+
* so it crosses the browser boundary intact; everything it needs is in scope
|
|
44
|
+
* there and nothing from this module is.
|
|
45
|
+
*/
|
|
46
|
+
const IN_PAGE = () => {
|
|
47
|
+
const out = [];
|
|
48
|
+
const add = (code, path, got, detail) => out.push({ code, path, got, detail });
|
|
49
|
+
|
|
50
|
+
/** A short, stable selector for a node, so a report can be acted on. */
|
|
51
|
+
const sel = (el) => {
|
|
52
|
+
if (!el || !el.tagName) return "?";
|
|
53
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
54
|
+
const tag = el.tagName.toLowerCase();
|
|
55
|
+
const cls = (el.getAttribute("class") || "").trim().split(/\s+/).filter(Boolean)[0];
|
|
56
|
+
const base = cls ? `${tag}.${CSS.escape(cls)}` : tag;
|
|
57
|
+
const sibs = el.parentElement
|
|
58
|
+
? [...el.parentElement.children].filter((n) => n.tagName === el.tagName) : [];
|
|
59
|
+
return sibs.length > 1 ? `${base}:nth-of-type(${sibs.indexOf(el) + 1})` : base;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const all = [...document.body.querySelectorAll("*")];
|
|
63
|
+
const vw = window.innerWidth;
|
|
64
|
+
|
|
65
|
+
// 1. The page scrolls sideways. Almost never intended, and the single most
|
|
66
|
+
// common way a generated layout is visibly broken on a phone.
|
|
67
|
+
if (document.documentElement.scrollWidth > vw + 1) {
|
|
68
|
+
add("HORIZONTAL_OVERFLOW", "html", document.documentElement.scrollWidth,
|
|
69
|
+
`the document is ${document.documentElement.scrollWidth}px wide in a ${vw}px viewport`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const el of all) {
|
|
73
|
+
const r = el.getBoundingClientRect();
|
|
74
|
+
if (!r.width && !r.height) continue;
|
|
75
|
+
const cs = getComputedStyle(el);
|
|
76
|
+
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
|
77
|
+
|
|
78
|
+
// 2. An element sticking out past the right edge. Left/top are not
|
|
79
|
+
// checked: off-canvas menus and slide-in panels park there legitimately.
|
|
80
|
+
if (r.right > vw + 1 && cs.position !== "fixed") {
|
|
81
|
+
add("ELEMENT_OVERFLOWS_VIEWPORT", sel(el), Math.round(r.right),
|
|
82
|
+
`extends ${Math.round(r.right - vw)}px past the ${vw}px viewport`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 3. Content taller than its own box, with the overflow hidden — the
|
|
86
|
+
// signature of clipped text, which reads as missing content.
|
|
87
|
+
if ((cs.overflow === "hidden" || cs.overflowY === "hidden") &&
|
|
88
|
+
el.scrollHeight > el.clientHeight + 2 && el.clientHeight > 0 &&
|
|
89
|
+
(el.textContent || "").trim().length > 0) {
|
|
90
|
+
add("CONTENT_CLIPPED", sel(el), el.scrollHeight,
|
|
91
|
+
`content is ${el.scrollHeight}px in a ${el.clientHeight}px box with overflow hidden`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 4. An image nobody can describe. Covers the background-image form
|
|
95
|
+
// too, which is what this library's cards emit.
|
|
96
|
+
const tag = el.tagName.toLowerCase();
|
|
97
|
+
if (tag === "img" && !el.hasAttribute("alt")) {
|
|
98
|
+
add("IMAGE_WITHOUT_ALT", sel(el), el.getAttribute("src") || "", "no alt attribute");
|
|
99
|
+
}
|
|
100
|
+
if (el.getAttribute("role") === "img" && !el.getAttribute("aria-label")) {
|
|
101
|
+
add("IMAGE_WITHOUT_ALT", sel(el), "role=img", "role=img with no aria-label");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 5. A target too small to hit. 24px is the WCAG 2.2 AA minimum.
|
|
105
|
+
const interactive = tag === "a" || tag === "button" ||
|
|
106
|
+
(tag === "input" && el.type !== "hidden") || el.getAttribute("role") === "button";
|
|
107
|
+
if (interactive && r.width > 0 && (r.width < 24 || r.height < 24)) {
|
|
108
|
+
add("TAP_TARGET_TOO_SMALL", sel(el), `${Math.round(r.width)}x${Math.round(r.height)}`,
|
|
109
|
+
"below the 24x24 minimum");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 6. A control with nothing to announce.
|
|
113
|
+
if (interactive && !(el.textContent || "").trim() &&
|
|
114
|
+
!el.getAttribute("aria-label") && !el.querySelector("img[alt]:not([alt=''])")) {
|
|
115
|
+
add("CONTROL_WITHOUT_LABEL", sel(el), tag, "no text and no accessible name");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 7. Heading levels that skip. A generated page is often read by machines
|
|
120
|
+
// before people, and the outline is the structure they read.
|
|
121
|
+
const heads = all.filter((e) => /^H[1-6]$/.test(e.tagName));
|
|
122
|
+
let prev = 0;
|
|
123
|
+
for (const h of heads) {
|
|
124
|
+
const lvl = Number(h.tagName[1]);
|
|
125
|
+
if (prev && lvl > prev + 1) {
|
|
126
|
+
add("HEADING_LEVEL_SKIPPED", sel(h), h.tagName,
|
|
127
|
+
`h${prev} is followed by ${h.tagName.toLowerCase()}`);
|
|
128
|
+
}
|
|
129
|
+
prev = lvl;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 8. Text nobody can read. Only where a background colour is actually
|
|
133
|
+
// resolvable — guessing through an image would produce false alarms,
|
|
134
|
+
// and a false alarm here costs more than a miss.
|
|
135
|
+
const lum = (c) => {
|
|
136
|
+
const m = c.match(/[\d.]+/g);
|
|
137
|
+
if (!m || m.length < 3) return null;
|
|
138
|
+
if (m.length > 3 && Number(m[3]) === 0) return null;
|
|
139
|
+
const [r, g, b] = m.slice(0, 3).map((v) => {
|
|
140
|
+
const s = Number(v) / 255;
|
|
141
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
142
|
+
});
|
|
143
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
144
|
+
};
|
|
145
|
+
const bgOf = (el) => {
|
|
146
|
+
for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
|
|
147
|
+
const cs = getComputedStyle(n);
|
|
148
|
+
if (cs.backgroundImage && cs.backgroundImage !== "none") return null;
|
|
149
|
+
const l = lum(cs.backgroundColor);
|
|
150
|
+
if (l !== null) return l;
|
|
151
|
+
}
|
|
152
|
+
return lum(getComputedStyle(document.body).backgroundColor);
|
|
153
|
+
};
|
|
154
|
+
for (const el of all) {
|
|
155
|
+
const own = [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim());
|
|
156
|
+
if (!own) continue;
|
|
157
|
+
const cs = getComputedStyle(el);
|
|
158
|
+
if (cs.display === "none" || cs.visibility === "hidden") continue;
|
|
159
|
+
const fg = lum(cs.color), bg = bgOf(el);
|
|
160
|
+
if (fg === null || bg === null) continue;
|
|
161
|
+
const ratio = (Math.max(fg, bg) + 0.05) / (Math.min(fg, bg) + 0.05);
|
|
162
|
+
const size = parseFloat(cs.fontSize) || 16;
|
|
163
|
+
const large = size >= 24 || (size >= 18.66 && Number(cs.fontWeight) >= 700);
|
|
164
|
+
const need = large ? 3 : 4.5;
|
|
165
|
+
if (ratio < need) {
|
|
166
|
+
add("LOW_CONTRAST", sel(el), `${ratio.toFixed(2)}:1`,
|
|
167
|
+
`needs ${need}:1 at ${Math.round(size)}px`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check rendered HTML and return a report in the shape everything else here
|
|
175
|
+
* returns. `html` is a full document or a fragment; a fragment is wrapped.
|
|
176
|
+
*/
|
|
177
|
+
export async function checkPage(html, opts = {}) {
|
|
178
|
+
const viewports = opts.viewports || DEFAULT_VIEWPORTS;
|
|
179
|
+
let chromium;
|
|
180
|
+
try {
|
|
181
|
+
({ chromium } = await import("playwright"));
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// The same arrangement `preview` uses for jsdom: a missing build-time
|
|
184
|
+
// package is a report, not a crash, and it names what to install.
|
|
185
|
+
return {
|
|
186
|
+
ok: false,
|
|
187
|
+
errors: [{
|
|
188
|
+
code: "MISSING_PEER_DEPENDENCY",
|
|
189
|
+
path: "check_page",
|
|
190
|
+
got: "playwright",
|
|
191
|
+
suggestions: ["npm install --save-dev playwright", "npx playwright install chromium"],
|
|
192
|
+
valid: [],
|
|
193
|
+
detail: "checking a page needs real layout — geometry and computed " +
|
|
194
|
+
"styles — which only a browser has. The library does not bundle " +
|
|
195
|
+
"one: it is needed for checking, not for rendering, and bundling " +
|
|
196
|
+
"it would put a large download into every install.",
|
|
197
|
+
}],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const doc = /<html[\s>]/i.test(html)
|
|
202
|
+
? html
|
|
203
|
+
: `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`;
|
|
204
|
+
|
|
205
|
+
const browser = await chromium.launch();
|
|
206
|
+
const errors = [];
|
|
207
|
+
try {
|
|
208
|
+
for (const vp of viewports) {
|
|
209
|
+
const page = await browser.newPage({ viewport: { width: vp.width, height: vp.height } });
|
|
210
|
+
try {
|
|
211
|
+
await page.setContent(doc, { waitUntil: "load" });
|
|
212
|
+
const found = await page.evaluate(IN_PAGE);
|
|
213
|
+
for (const f of found) {
|
|
214
|
+
errors.push({
|
|
215
|
+
code: f.code,
|
|
216
|
+
path: f.path,
|
|
217
|
+
got: f.got,
|
|
218
|
+
suggestions: [],
|
|
219
|
+
valid: [],
|
|
220
|
+
detail: f.detail,
|
|
221
|
+
viewport: vp.name,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} finally { await page.close(); }
|
|
225
|
+
}
|
|
226
|
+
} finally { await browser.close(); }
|
|
227
|
+
|
|
228
|
+
// The same finding at two viewports is one problem, reported once, with
|
|
229
|
+
// the widths it was seen at. An agent repairing a page should see a list
|
|
230
|
+
// of problems, not a list of measurements.
|
|
231
|
+
const merged = new Map();
|
|
232
|
+
for (const e of errors) {
|
|
233
|
+
const key = `${e.code}|${e.path}|${e.detail}`;
|
|
234
|
+
if (merged.has(key)) merged.get(key).viewport += `, ${e.viewport}`;
|
|
235
|
+
else merged.set(key, { ...e });
|
|
236
|
+
}
|
|
237
|
+
const list = [...merged.values()];
|
|
238
|
+
return { ok: list.length === 0, errors: list, viewports: viewports.map((v) => v.name) };
|
|
239
|
+
}
|
package/lib/codegen.js
CHANGED
package/lib/data.js
CHANGED