nodality 1.3.0 → 1.3.2
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/dist/animator.cjs.js.LICENSE.txt +1 -1
- package/dist/animator.esm.js.LICENSE.txt +1 -1
- package/dist/audionew.cjs.js.LICENSE.txt +1 -1
- package/dist/audionew.esm.js.LICENSE.txt +1 -1
- package/dist/base.cjs.js.LICENSE.txt +1 -1
- package/dist/base.esm.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/beta-desktop-bar.esm.js.LICENSE.txt +1 -1
- package/dist/beta-mobile-bar.cjs.js.LICENSE.txt +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.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/center.esm.js.LICENSE.txt +1 -1
- package/dist/checkbox.cjs.js.LICENSE.txt +1 -1
- package/dist/checkbox.esm.js.LICENSE.txt +1 -1
- package/dist/code.cjs.js.LICENSE.txt +1 -1
- package/dist/code.esm.js.LICENSE.txt +1 -1
- package/dist/container.cjs.js.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/flex-card.esm.js.LICENSE.txt +1 -1
- package/dist/flex-grid.cjs.js.LICENSE.txt +1 -1
- package/dist/flex-grid.esm.js.LICENSE.txt +1 -1
- package/dist/flex-row.cjs.js.LICENSE.txt +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.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/image-picker.esm.js.LICENSE.txt +1 -1
- package/dist/image.cjs.js.LICENSE.txt +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 +1 -1
- 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.LICENSE.txt +1 -1
- package/dist/link-getter.esm.js.LICENSE.txt +1 -1
- package/dist/link.cjs.js.LICENSE.txt +1 -1
- package/dist/link.esm.js.LICENSE.txt +1 -1
- package/dist/meta-adder.cjs.js.LICENSE.txt +1 -1
- package/dist/meta-adder.esm.js.LICENSE.txt +1 -1
- package/dist/modal-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/modal-2025.esm.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.cjs.js.LICENSE.txt +1 -1
- package/dist/multiswitcher.esm.js.LICENSE.txt +1 -1
- package/dist/new-nav-bar.cjs.js.LICENSE.txt +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.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/side-bar.esm.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/side-nav-bar.esm.js.LICENSE.txt +1 -1
- package/dist/simple-bar.cjs.js.LICENSE.txt +1 -1
- package/dist/simple-bar.esm.js.LICENSE.txt +1 -1
- package/dist/slider-2025.cjs.js.LICENSE.txt +1 -1
- package/dist/slider-2025.esm.js.LICENSE.txt +1 -1
- package/dist/spacer.cjs.js.LICENSE.txt +1 -1
- package/dist/spacer.esm.js.LICENSE.txt +1 -1
- package/dist/stack.cjs.js.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/table.esm.js.LICENSE.txt +1 -1
- package/dist/text-field.cjs.js.LICENSE.txt +1 -1
- package/dist/text-field.esm.js.LICENSE.txt +1 -1
- package/dist/text.cjs.js.LICENSE.txt +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.LICENSE.txt +1 -1
- package/dist/ulist.esm.js.LICENSE.txt +1 -1
- package/dist/video.cjs.js.LICENSE.txt +1 -1
- package/dist/video.esm.js.LICENSE.txt +1 -1
- package/dist/wrap.cjs.js.LICENSE.txt +1 -1
- package/dist/wrap.esm.js.LICENSE.txt +1 -1
- package/dist/zoom-card.cjs.js.LICENSE.txt +1 -1
- package/dist/zoom-card.esm.js.LICENSE.txt +1 -1
- package/layout/animator.js +1 -1
- 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 +1 -1
- 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 +12 -6
- package/layout/form-components/form-all.js +1 -1
- package/layout/form-components/form.js +1 -1
- package/layout/form-components/image-picker.js +1 -1
- package/layout/form-components/picker.js +37 -2
- 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 +1 -1
- 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 +1 -1
- package/layout/side-nav-bar.js +1 -1
- 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 +1 -1
- package/layout/text.js +1 -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 +173 -24
- package/lib/codegen.js +1 -1
- package/lib/data.js +1 -1
- package/lib/designer.js +1 -1
- package/lib/element-mapper.js +11 -3
- 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 +1 -1
- package/lib/normalize-spec.js +1 -1
- 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 +1 -1
- 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 +52 -1
- package/lib/webmcp-adapter.js +71 -6
- package/package.json +1 -1
- package/schema.json +769 -0
- package/scripts/generate-schema.mjs +9 -1
package/lib/check-page.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.2
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -45,18 +45,85 @@ export const DEFAULT_VIEWPORTS = [
|
|
|
45
45
|
*/
|
|
46
46
|
const IN_PAGE = () => {
|
|
47
47
|
const out = [];
|
|
48
|
-
const add = (code, path, got, detail) => out.push({ code, path, got, detail });
|
|
49
48
|
|
|
50
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* A PAGE-UNIQUE selector, so a report can be acted on.
|
|
51
|
+
*
|
|
52
|
+
* The first version was parent-relative and returned `input` for any input
|
|
53
|
+
* that was the only one under its own div. A form with three unlabelled
|
|
54
|
+
* fields produced the string "input" three times, the merge at the bottom
|
|
55
|
+
* of this file keyed on it, and three findings collapsed into one row
|
|
56
|
+
* reading `viewport: "mobile, mobile, mobile"`. The agent was told neither
|
|
57
|
+
* which control nor that there were three — Tier 7 watched a model fail to
|
|
58
|
+
* repair exactly that. A path walks to the nearest id, or to the body.
|
|
59
|
+
*/
|
|
51
60
|
const sel = (el) => {
|
|
52
61
|
if (!el || !el.tagName) return "?";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
const seg = (n) => {
|
|
63
|
+
const tag = n.tagName.toLowerCase();
|
|
64
|
+
const sibs = n.parentElement
|
|
65
|
+
? [...n.parentElement.children].filter((x) => x.tagName === n.tagName) : [];
|
|
66
|
+
return sibs.length > 1 ? `${tag}:nth-of-type(${sibs.indexOf(n) + 1})` : tag;
|
|
67
|
+
};
|
|
68
|
+
const parts = [];
|
|
69
|
+
for (let n = el; n && n !== document.body; n = n.parentElement) {
|
|
70
|
+
if (n.id) { parts.unshift(`#${CSS.escape(n.id)}`); break; }
|
|
71
|
+
parts.unshift(seg(n));
|
|
72
|
+
}
|
|
73
|
+
return parts.join(" > ") || el.tagName.toLowerCase();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The SPEC node an element came from, when the page carries annotation.
|
|
78
|
+
* A selector says where in the DOM; this says which descriptor to edit,
|
|
79
|
+
* which is the thing an agent repairing a spec actually needs. Free when
|
|
80
|
+
* the annotation is there, null when it is not — it is opt-in, and the
|
|
81
|
+
* attribute name is spelled out here because this function crosses into
|
|
82
|
+
* the browser as a string and cannot import `SPEC_ATTR`. A test pins the
|
|
83
|
+
* two together.
|
|
84
|
+
*/
|
|
85
|
+
const nod = (el) => {
|
|
86
|
+
for (let n = el; n && n !== document.body; n = n.parentElement) {
|
|
87
|
+
const v = n.getAttribute && n.getAttribute("data-nod");
|
|
88
|
+
if (v) return v;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// `target` is the element itself, so a finding can carry both. A string is
|
|
94
|
+
// passed for findings about the document rather than about an element.
|
|
95
|
+
const add = (code, target, got, detail) => {
|
|
96
|
+
const isEl = !!(target && target.tagName);
|
|
97
|
+
out.push({
|
|
98
|
+
code, path: isEl ? sel(target) : String(target), got, detail,
|
|
99
|
+
nod: isEl ? nod(target) : null,
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A name assistive technology can announce.
|
|
105
|
+
*
|
|
106
|
+
* Was `aria-label` alone, which is only one of the four ways to give one
|
|
107
|
+
* and the least used. A control correctly labelled by a `<label for>` —
|
|
108
|
+
* the ordinary HTML way — was reported as unnamed, and a false finding
|
|
109
|
+
* sends a repair at something that was never broken, which this file
|
|
110
|
+
* argues elsewhere is worse than a miss.
|
|
111
|
+
*/
|
|
112
|
+
const hasName = (el) => {
|
|
113
|
+
if ((el.getAttribute("aria-label") || "").trim()) return true;
|
|
114
|
+
if ((el.getAttribute("title") || "").trim()) return true;
|
|
115
|
+
const by = (el.getAttribute("aria-labelledby") || "").trim();
|
|
116
|
+
if (by && by.split(/\s+/).some((id) => {
|
|
117
|
+
const t = document.getElementById(id);
|
|
118
|
+
return t && (t.textContent || "").trim();
|
|
119
|
+
})) return true;
|
|
120
|
+
if (el.id) {
|
|
121
|
+
const l = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
|
|
122
|
+
if (l && (l.textContent || "").trim()) return true;
|
|
123
|
+
}
|
|
124
|
+
const wrap = el.closest && el.closest("label");
|
|
125
|
+
if (wrap && (wrap.textContent || "").trim()) return true;
|
|
126
|
+
return false;
|
|
60
127
|
};
|
|
61
128
|
|
|
62
129
|
const all = [...document.body.querySelectorAll("*")];
|
|
@@ -78,7 +145,7 @@ const IN_PAGE = () => {
|
|
|
78
145
|
// 2. An element sticking out past the right edge. Left/top are not
|
|
79
146
|
// checked: off-canvas menus and slide-in panels park there legitimately.
|
|
80
147
|
if (r.right > vw + 1 && cs.position !== "fixed") {
|
|
81
|
-
add("ELEMENT_OVERFLOWS_VIEWPORT",
|
|
148
|
+
add("ELEMENT_OVERFLOWS_VIEWPORT", el, Math.round(r.right),
|
|
82
149
|
`extends ${Math.round(r.right - vw)}px past the ${vw}px viewport`);
|
|
83
150
|
}
|
|
84
151
|
|
|
@@ -87,7 +154,7 @@ const IN_PAGE = () => {
|
|
|
87
154
|
if ((cs.overflow === "hidden" || cs.overflowY === "hidden") &&
|
|
88
155
|
el.scrollHeight > el.clientHeight + 2 && el.clientHeight > 0 &&
|
|
89
156
|
(el.textContent || "").trim().length > 0) {
|
|
90
|
-
add("CONTENT_CLIPPED",
|
|
157
|
+
add("CONTENT_CLIPPED", el, el.scrollHeight,
|
|
91
158
|
`content is ${el.scrollHeight}px in a ${el.clientHeight}px box with overflow hidden`);
|
|
92
159
|
}
|
|
93
160
|
|
|
@@ -95,24 +162,38 @@ const IN_PAGE = () => {
|
|
|
95
162
|
// too, which is what this library's cards emit.
|
|
96
163
|
const tag = el.tagName.toLowerCase();
|
|
97
164
|
if (tag === "img" && !el.hasAttribute("alt")) {
|
|
98
|
-
add("IMAGE_WITHOUT_ALT",
|
|
165
|
+
add("IMAGE_WITHOUT_ALT", el, el.getAttribute("src") || "", "no alt attribute");
|
|
99
166
|
}
|
|
100
167
|
if (el.getAttribute("role") === "img" && !el.getAttribute("aria-label")) {
|
|
101
|
-
add("IMAGE_WITHOUT_ALT",
|
|
168
|
+
add("IMAGE_WITHOUT_ALT", el, "role=img", "role=img with no aria-label");
|
|
102
169
|
}
|
|
103
170
|
|
|
104
171
|
// 5. A target too small to hit. 24px is the WCAG 2.2 AA minimum.
|
|
172
|
+
//
|
|
173
|
+
// `select` and `textarea` were both missing from this set, so neither
|
|
174
|
+
// check below ever ran on one. A picker could not be given an
|
|
175
|
+
// accessible name at all — `label` reached the component and nothing
|
|
176
|
+
// read it — and no report said so, because the checker was not
|
|
177
|
+
// looking. Two blind spots that hid each other.
|
|
105
178
|
const interactive = tag === "a" || tag === "button" ||
|
|
106
|
-
(tag === "input" && el.type !== "hidden") ||
|
|
179
|
+
(tag === "input" && el.type !== "hidden") ||
|
|
180
|
+
tag === "select" || tag === "textarea" ||
|
|
181
|
+
el.getAttribute("role") === "button";
|
|
107
182
|
if (interactive && r.width > 0 && (r.width < 24 || r.height < 24)) {
|
|
108
|
-
add("TAP_TARGET_TOO_SMALL",
|
|
183
|
+
add("TAP_TARGET_TOO_SMALL", el, `${Math.round(r.width)}x${Math.round(r.height)}`,
|
|
109
184
|
"below the 24x24 minimum");
|
|
110
185
|
}
|
|
111
186
|
|
|
112
187
|
// 6. A control with nothing to announce.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
188
|
+
//
|
|
189
|
+
// A <select>'s own text is its OPTIONS and a <textarea>'s is its
|
|
190
|
+
// VALUE. Neither names the control, so the textContent shortcut that
|
|
191
|
+
// clears a button by its caption must not clear these.
|
|
192
|
+
const ownText = (tag === "select" || tag === "textarea")
|
|
193
|
+
? "" : (el.textContent || "").trim();
|
|
194
|
+
if (interactive && !ownText && !hasName(el) &&
|
|
195
|
+
!el.querySelector("img[alt]:not([alt=''])")) {
|
|
196
|
+
add("CONTROL_WITHOUT_LABEL", el, tag, "no text and no accessible name");
|
|
116
197
|
}
|
|
117
198
|
}
|
|
118
199
|
|
|
@@ -123,7 +204,7 @@ const IN_PAGE = () => {
|
|
|
123
204
|
for (const h of heads) {
|
|
124
205
|
const lvl = Number(h.tagName[1]);
|
|
125
206
|
if (prev && lvl > prev + 1) {
|
|
126
|
-
add("HEADING_LEVEL_SKIPPED",
|
|
207
|
+
add("HEADING_LEVEL_SKIPPED", h, h.tagName,
|
|
127
208
|
`h${prev} is followed by ${h.tagName.toLowerCase()}`);
|
|
128
209
|
}
|
|
129
210
|
prev = lvl;
|
|
@@ -163,19 +244,83 @@ const IN_PAGE = () => {
|
|
|
163
244
|
const large = size >= 24 || (size >= 18.66 && Number(cs.fontWeight) >= 700);
|
|
164
245
|
const need = large ? 3 : 4.5;
|
|
165
246
|
if (ratio < need) {
|
|
166
|
-
add("LOW_CONTRAST",
|
|
247
|
+
add("LOW_CONTRAST", el, `${ratio.toFixed(2)}:1`,
|
|
167
248
|
`needs ${need}:1 at ${Math.round(size)}px`);
|
|
168
249
|
}
|
|
169
250
|
}
|
|
170
251
|
return out;
|
|
171
252
|
};
|
|
172
253
|
|
|
254
|
+
/**
|
|
255
|
+
* How to repair each finding, in THIS library's vocabulary.
|
|
256
|
+
*
|
|
257
|
+
* `suggestions` and `valid` are in the finding shape because `validate_nodes`
|
|
258
|
+
* puts repairs there, and check_page left both empty on every finding it has
|
|
259
|
+
* ever emitted — a field that is always blank is a field an agent learns to
|
|
260
|
+
* ignore. Tier 7 measured the cost. Told only "54x23, below the 24x24
|
|
261
|
+
* minimum", the strong model guessed `keySet: {key, value}` and was right, the
|
|
262
|
+
* weaker one invented `keySet: {"--tap-target-size": "44px"}` and was wrong,
|
|
263
|
+
* and the page stayed broken through the repair turn. Naming the element was
|
|
264
|
+
* not enough; the report has to name the edit.
|
|
265
|
+
*
|
|
266
|
+
* Kept deliberately short. These are the repair that fits the overwhelming
|
|
267
|
+
* majority of cases, not a treatment of every way a page can be wrong — an
|
|
268
|
+
* agent that needs more has `npx nodality schema <type>`.
|
|
269
|
+
*/
|
|
270
|
+
const REPAIRS = {
|
|
271
|
+
TAP_TARGET_TOO_SMALL: {
|
|
272
|
+
suggestions: [`keySet: { key: "min-height", value: "44px" }`,
|
|
273
|
+
`keySet: { key: "min-width", value: "44px" }`,
|
|
274
|
+
`pad: [{ a: 12 }]`],
|
|
275
|
+
valid: ["at least 24x24 CSS pixels, per WCAG 2.2 AA target size (minimum)"],
|
|
276
|
+
},
|
|
277
|
+
CONTROL_WITHOUT_LABEL: {
|
|
278
|
+
suggestions: [`label: "<what the field is for>" (input, labelInput, picker)`,
|
|
279
|
+
`text: "<what the control does>" (button, a)`],
|
|
280
|
+
valid: ["every control needs a name assistive technology can announce; " +
|
|
281
|
+
"a placeholder is not one"],
|
|
282
|
+
},
|
|
283
|
+
IMAGE_WITHOUT_ALT: {
|
|
284
|
+
suggestions: [`alt: "<what the image shows>"`,
|
|
285
|
+
`alt: "" if it is decorative and repeats nearby text`],
|
|
286
|
+
valid: ["img takes `alt`; a background image takes it too and becomes aria-label"],
|
|
287
|
+
},
|
|
288
|
+
LOW_CONTRAST: {
|
|
289
|
+
suggestions: [`color: "<a darker or lighter text colour>"`,
|
|
290
|
+
`background: "<a background with more separation>"`],
|
|
291
|
+
valid: ["4.5:1 for body text, 3:1 at 24px or at 18.66px bold"],
|
|
292
|
+
},
|
|
293
|
+
HEADING_LEVEL_SKIPPED: {
|
|
294
|
+
suggestions: [`tag: "h<the next level down>"`],
|
|
295
|
+
valid: ["`tag` sets the heading level independently of the size scale, " +
|
|
296
|
+
"so the outline can be correct without changing the design"],
|
|
297
|
+
},
|
|
298
|
+
ELEMENT_OVERFLOWS_VIEWPORT: {
|
|
299
|
+
suggestions: [`maxWidth: "100%"`, `width: "100%"`, `breakWord: true`],
|
|
300
|
+
valid: ["nothing may extend past the right edge of the viewport"],
|
|
301
|
+
},
|
|
302
|
+
HORIZONTAL_OVERFLOW: {
|
|
303
|
+
suggestions: [`maxWidth: "100%" on whichever element the other findings name`],
|
|
304
|
+
valid: ["the document may not be wider than the viewport"],
|
|
305
|
+
},
|
|
306
|
+
CONTENT_CLIPPED: {
|
|
307
|
+
suggestions: [`height: "auto"`, `overflow: "visible"`],
|
|
308
|
+
valid: ["a box with `overflow: hidden` must be tall enough for its text"],
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
173
312
|
/**
|
|
174
313
|
* Check rendered HTML and return a report in the shape everything else here
|
|
175
314
|
* returns. `html` is a full document or a fragment; a fragment is wrapped.
|
|
176
315
|
*/
|
|
177
316
|
export async function checkPage(html, opts = {}) {
|
|
178
317
|
const viewports = opts.viewports || DEFAULT_VIEWPORTS;
|
|
318
|
+
// `load` is right for a self-contained page, which is what this library
|
|
319
|
+
// emits. A page whose styling arrives over the network — a CDN stylesheet,
|
|
320
|
+
// a webfont — has not been laid out yet at `load`, and measuring it there
|
|
321
|
+
// reports the unstyled document. Callers checking such a page pass
|
|
322
|
+
// "networkidle".
|
|
323
|
+
const waitUntil = opts.waitUntil || "load";
|
|
179
324
|
let chromium;
|
|
180
325
|
try {
|
|
181
326
|
({ chromium } = await import("playwright"));
|
|
@@ -208,16 +353,20 @@ export async function checkPage(html, opts = {}) {
|
|
|
208
353
|
for (const vp of viewports) {
|
|
209
354
|
const page = await browser.newPage({ viewport: { width: vp.width, height: vp.height } });
|
|
210
355
|
try {
|
|
211
|
-
await page.setContent(doc, { waitUntil
|
|
356
|
+
await page.setContent(doc, { waitUntil });
|
|
212
357
|
const found = await page.evaluate(IN_PAGE);
|
|
213
358
|
for (const f of found) {
|
|
214
359
|
errors.push({
|
|
215
360
|
code: f.code,
|
|
216
361
|
path: f.path,
|
|
217
362
|
got: f.got,
|
|
218
|
-
suggestions: [],
|
|
219
|
-
valid: [],
|
|
363
|
+
suggestions: REPAIRS[f.code]?.suggestions ?? [],
|
|
364
|
+
valid: REPAIRS[f.code]?.valid ?? [],
|
|
220
365
|
detail: f.detail,
|
|
366
|
+
// The spec node, when the page was annotated. Null
|
|
367
|
+
// otherwise, rather than absent, so the field's
|
|
368
|
+
// meaning does not depend on whether it is there.
|
|
369
|
+
nod: f.nod ?? null,
|
|
221
370
|
viewport: vp.name,
|
|
222
371
|
});
|
|
223
372
|
}
|
package/lib/codegen.js
CHANGED
package/lib/data.js
CHANGED
package/lib/designer.js
CHANGED
package/lib/element-mapper.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.2
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -566,8 +566,16 @@ animation: {
|
|
|
566
566
|
// wrote `label`. The field it produced then had no
|
|
567
567
|
// accessible name at all — a placeholder is not one, since
|
|
568
568
|
// it disappears the moment anyone types.
|
|
569
|
-
|
|
570
|
-
|
|
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 } : {}),
|
|
571
579
|
...this.elOpts(el),
|
|
572
580
|
}), el, obj.customOptions);
|
|
573
581
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.2
|
|
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","alt","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","tag","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
package/lib/normalize-spec.js
CHANGED
package/lib/parse-html.js
CHANGED
package/lib/raster-inspect.js
CHANGED
package/lib/raster-ops.js
CHANGED
package/lib/raster-presets.js
CHANGED
package/lib/resolve-refs.js
CHANGED
package/lib/scroll-video.js
CHANGED
package/lib/seo.js
CHANGED
package/lib/stacker.js
CHANGED
package/lib/suggest.js
CHANGED
package/lib/theme.js
CHANGED
package/lib/transform-anim.js
CHANGED
package/lib/transition.js
CHANGED
package/lib/validate-nodes.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* nodality v1.3.
|
|
2
|
+
* nodality v1.3.2
|
|
3
3
|
* (c) 2026 Filip Vabrousek
|
|
4
4
|
* License: MIT
|
|
5
5
|
*/
|
|
@@ -163,6 +163,15 @@ export const CONTENT_SLOT = {
|
|
|
163
163
|
row: "children", form: "children", wrap: "children",
|
|
164
164
|
};
|
|
165
165
|
|
|
166
|
+
// Of the types that read `items`, which read the entries as ELEMENT SPECS
|
|
167
|
+
// rather than as data. `ulist` maps every object entry through the mapper;
|
|
168
|
+
// `cards`, `nav`, `sideNav` and `table` read theirs as data — nav entries are
|
|
169
|
+
// `{title, link}`, table entries are rows — and walking those as elements
|
|
170
|
+
// would report every correct page. Pinned against the mapper by a test rather
|
|
171
|
+
// than trusted, because a hand-kept table beside a mapper is exactly what
|
|
172
|
+
// drifted before 1.2.8.
|
|
173
|
+
export const SPEC_ITEMS = new Set(["ulist"]);
|
|
174
|
+
|
|
166
175
|
export function validateNodes(nodes, elements, defs) {
|
|
167
176
|
const errors = [];
|
|
168
177
|
const push = (code, path, got, suggestions, valid) => {
|
|
@@ -382,6 +391,48 @@ export function validateNodes(nodes, elements, defs) {
|
|
|
382
391
|
if (Array.isArray(entry)) walkEls(entry, `${p}.items[${k}]`);
|
|
383
392
|
});
|
|
384
393
|
}
|
|
394
|
+
|
|
395
|
+
// Phase S7: an object entry with no `type`, in a slot whose
|
|
396
|
+
// entries ARE elements. `{type:"ulist", items:[{text:"Fast"}]}`
|
|
397
|
+
// validated clean and then threw from deep in the mapper —
|
|
398
|
+
// `Unknown element type "undefined"` — which is the Stage 3
|
|
399
|
+
// contract exactly inverted: validate-then-render handed the
|
|
400
|
+
// agent a stack trace in the one place the tool exists to
|
|
401
|
+
// prevent it.
|
|
402
|
+
//
|
|
403
|
+
// Tier 7 measured what that cost. The throw names every valid
|
|
404
|
+
// type and not one word about WHICH entry, so it is the same
|
|
405
|
+
// string for every fault of this shape on any page; the weaker
|
|
406
|
+
// of the two models repaired none of them and returned a
|
|
407
|
+
// byte-identical spec. The suggestion below therefore hands
|
|
408
|
+
// back the replacement rather than describing it.
|
|
409
|
+
//
|
|
410
|
+
// A string entry is the shorthand and stays valid.
|
|
411
|
+
if (SPEC_ITEMS.has(el.type) && Array.isArray(el.items)) {
|
|
412
|
+
el.items.forEach((entry, k) => {
|
|
413
|
+
const at = `${p}.items[${k}]`;
|
|
414
|
+
if (typeof entry === "string" || typeof entry === "number") return;
|
|
415
|
+
if (Array.isArray(entry)) return;
|
|
416
|
+
if (!entry || typeof entry !== "object") {
|
|
417
|
+
push("BAD_ELEMENT", at, entry, [],
|
|
418
|
+
["a string, or { type: \"<element type>\", ... }"]);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (isRef(entry)) return;
|
|
422
|
+
if (typeof entry.type !== "string") {
|
|
423
|
+
const text = ["text", "title", "label"]
|
|
424
|
+
.map((k2) => entry[k2]).find((v) => typeof v === "string");
|
|
425
|
+
push("MISSING_FIELD", `${at}.type`, entry.type,
|
|
426
|
+
text === undefined ? []
|
|
427
|
+
: [JSON.stringify(text),
|
|
428
|
+
`{ "type": "p", "text": ${JSON.stringify(text)} }`],
|
|
429
|
+
[`"${el.type}" reads each item as an element; a plain string is the shorthand`]);
|
|
430
|
+
} else if (!ELEMENT_TYPES.includes(entry.type)) {
|
|
431
|
+
push("UNKNOWN_ELEMENT_TYPE", `${at}.type`, entry.type,
|
|
432
|
+
suggest(entry.type, ELEMENT_TYPES), ELEMENT_TYPES);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
}
|
|
385
436
|
});
|
|
386
437
|
};
|
|
387
438
|
walkEls(toWalk, "elements");
|