@nodegx/mcp 0.2.3 → 0.2.5
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/backend/cli.js +39 -12
- package/dist/kit-extract.cjs +41 -14
- package/dist/noodl-mcp.cjs +219682 -213106
- package/dist/package.json +1 -1
- package/dist/render-harness/drive-steps.js +90 -0
- package/dist/render-harness/measure-from-disk.js +90 -0
- package/dist/render-harness/render-from-disk.js +20 -1
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/package.json +4 -6
package/dist/package.json
CHANGED
|
@@ -174,6 +174,52 @@ var require_src = __commonJS({
|
|
|
174
174
|
.filter((x) => x.box > 0 && x.width > x.box + 1)
|
|
175
175
|
.map((x) => ({ text: x.el.textContent.trim().slice(0, 80), width: round(x.width), parentWidth: round(x.box), cls: cls(x.el) }));
|
|
176
176
|
|
|
177
|
+
// P112-OWN020-OFFSYSTEM \u2014 the controls, measured against the project's type scale. A control nobody styled draws
|
|
178
|
+
// the runtime's own look (a Radio Button is a 32px circle with a 2px black ring; a native select or button keeps
|
|
179
|
+
// the browser's 13.33px text), and that is the "chunky" control beside a page on tokens. A radio or checkbox input
|
|
180
|
+
// is opacity 0, so its box is read on the first ancestor a person can see. The scale is the project's --text-*
|
|
181
|
+
// tokens, read off :root; the fake DOMs in the tests have no getPropertyValue, so it is guarded.
|
|
182
|
+
const pxOf = (value) => {
|
|
183
|
+
const s = String(value || '').trim();
|
|
184
|
+
if (!s) return NaN;
|
|
185
|
+
if (s.slice(-3) === 'rem') return parseFloat(s) * (parseFloat(styleOf(document.documentElement).fontSize) || 16);
|
|
186
|
+
return parseFloat(s);
|
|
187
|
+
};
|
|
188
|
+
const textScale = [];
|
|
189
|
+
const rootStyle = styleOf(document.documentElement);
|
|
190
|
+
if (rootStyle && typeof rootStyle.getPropertyValue === 'function') {
|
|
191
|
+
for (const step of ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl']) {
|
|
192
|
+
const px = pxOf(rootStyle.getPropertyValue('--text-' + step));
|
|
193
|
+
if (px > 0) textScale.push(Math.round(px * 100) / 100);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const controlKind = (el) => {
|
|
197
|
+
const type = String(el.type || '').toLowerCase();
|
|
198
|
+
if (el.tagName === 'INPUT' && type === 'radio') return 'Radio Button';
|
|
199
|
+
if (el.tagName === 'INPUT' && type === 'checkbox') return 'Checkbox';
|
|
200
|
+
if (el.tagName === 'SELECT') return 'Dropdown';
|
|
201
|
+
if (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && ['', 'text', 'email', 'password', 'search', 'number', 'tel', 'url'].indexOf(type) !== -1)) return 'Text Input';
|
|
202
|
+
if (el.tagName === 'BUTTON') return 'Button';
|
|
203
|
+
return null;
|
|
204
|
+
};
|
|
205
|
+
const controls = [];
|
|
206
|
+
for (const el of all) {
|
|
207
|
+
const kind = controlKind(el);
|
|
208
|
+
if (!kind) continue;
|
|
209
|
+
if (kind === 'Radio Button' || kind === 'Checkbox') {
|
|
210
|
+
let shown = el;
|
|
211
|
+
for (let hops = 0; shown && hops < 3 && (parseFloat(styleOf(shown).opacity) < 0.05 || shown.getBoundingClientRect().width < 2); hops++) shown = shown.parentElement;
|
|
212
|
+
if (!shown || shown.offsetParent === null) continue;
|
|
213
|
+
const r = shown.getBoundingClientRect();
|
|
214
|
+
let label = null;
|
|
215
|
+
if (el.id && typeof document.querySelector === 'function') label = document.querySelector('label[for="' + el.id + '"]');
|
|
216
|
+
controls.push({ kind, box: round(Math.max(r.width, r.height)), textSize: label ? pxOf(styleOf(label).fontSize) : NaN });
|
|
217
|
+
} else {
|
|
218
|
+
if (visible.indexOf(el) === -1) continue;
|
|
219
|
+
controls.push({ kind, box: 0, textSize: pxOf(styleOf(el).fontSize) });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
// AWP-004 \u2014 what is on screen, as opposed to what is in the DOM. The page is
|
|
178
224
|
// never scrolled when this runs, so a rect is already a document position.
|
|
179
225
|
// Kimi K3's storefront was 83 texts in the DOM and about three on screen, and
|
|
@@ -632,6 +678,7 @@ var require_src = __commonJS({
|
|
|
632
678
|
overflowingCount: overflowing.length,
|
|
633
679
|
textsWiderThanBox: textsWiderThanBox.slice(0, 10),
|
|
634
680
|
textsWiderThanBoxCount: textsWiderThanBox.length,
|
|
681
|
+
controls: { textScale, items: controls.slice(0, 60), count: controls.length },
|
|
635
682
|
text: {
|
|
636
683
|
elements: textEls.length,
|
|
637
684
|
onScreen: textEls.filter(onScreen).length,
|
|
@@ -721,6 +768,13 @@ var require_src = __commonJS({
|
|
|
721
768
|
* page-level finding stayed quiet because the page itself kept its width.
|
|
722
769
|
*/
|
|
723
770
|
TextWiderThanItsBox: "text-wider-than-its-box",
|
|
771
|
+
/**
|
|
772
|
+
* P112-OWN020-OFFSYSTEM — a control drawn at the runtime's own size beside a page on its token scale: a radio or
|
|
773
|
+
* checkbox box much bigger than its label, or control text off the project's --text-* scale (a native select or
|
|
774
|
+
* button keeps the browser's 13.33px). Mistral (SP1) and DeepSeek (OpenTune) both shipped one — the "chunky"
|
|
775
|
+
* Sort-by circles — because a control placed in passing is the one an agent never styles.
|
|
776
|
+
*/
|
|
777
|
+
ControlOffScale: "control-off-scale",
|
|
724
778
|
SingleColumnGrid: "single-column-grid",
|
|
725
779
|
MinimumLayoutWidth: "minimum-layout-width",
|
|
726
780
|
HorizontalOverflow: "horizontal-overflow",
|
|
@@ -789,9 +843,30 @@ var require_src = __commonJS({
|
|
|
789
843
|
const named = widest.cls ? `${widest.tag.toLowerCase()}.${widest.cls.split(/\s+/)[0]}` : widest.tag.toLowerCase();
|
|
790
844
|
return `${named} at ${widest.width}px`;
|
|
791
845
|
}
|
|
846
|
+
function controlsOffScale(controls) {
|
|
847
|
+
const scale = controls.textScale || [];
|
|
848
|
+
const onScale = (px) => scale.length === 0 || !(px > 0) || scale.some((s) => Math.abs(s - px) < 0.6);
|
|
849
|
+
const byKind = {};
|
|
850
|
+
for (const c of controls.items || []) {
|
|
851
|
+
const oversized = (c.kind === "Radio Button" || c.kind === "Checkbox") && c.box > Math.max(20, 1.5 * (c.textSize > 0 ? c.textSize : 0));
|
|
852
|
+
const offText = !onScale(c.textSize);
|
|
853
|
+
if (!oversized && !offText) continue;
|
|
854
|
+
const k = byKind[c.kind] || (byKind[c.kind] = { kind: c.kind, count: 0, box: c.box, textSize: c.textSize, oversized, offText });
|
|
855
|
+
k.count += 1;
|
|
856
|
+
}
|
|
857
|
+
const fmt = (n) => String(Math.round(n * 100) / 100);
|
|
858
|
+
return Object.keys(byKind).map((kind) => {
|
|
859
|
+
const k = byKind[kind];
|
|
860
|
+
const parts = [];
|
|
861
|
+
if (k.oversized) parts.push(fmt(k.box) + "px box" + (k.textSize > 0 ? " beside a " + fmt(k.textSize) + "px label" : ""));
|
|
862
|
+
if (k.offText) parts.push(fmt(k.textSize) + "px text, off the type scale (" + scale.map(fmt).join("/") + "px)");
|
|
863
|
+
return { kind, count: k.count, box: k.box, textSize: k.textSize, sentence: kind + " x" + k.count + ": " + parts.join(", ") };
|
|
864
|
+
});
|
|
865
|
+
}
|
|
792
866
|
function summarise(viewports, diagnosis, overridden = {}) {
|
|
793
867
|
const findings = [];
|
|
794
868
|
const add = (f) => findings.push(f);
|
|
869
|
+
let controlsJudged = false;
|
|
795
870
|
for (const [name, v] of Object.entries(viewports)) {
|
|
796
871
|
if (!v || v.error) continue;
|
|
797
872
|
const isDesktop = v.requested.width >= DESKTOP_WIDTH;
|
|
@@ -940,6 +1015,19 @@ var require_src = __commonJS({
|
|
|
940
1015
|
evidence: v.textsWiderThanBox
|
|
941
1016
|
});
|
|
942
1017
|
}
|
|
1018
|
+
if (!controlsJudged && v.controls && v.controls.count > 0) {
|
|
1019
|
+
controlsJudged = true;
|
|
1020
|
+
const off = controlsOffScale(v.controls);
|
|
1021
|
+
if (off.length > 0) {
|
|
1022
|
+
add({
|
|
1023
|
+
code: RenderFinding.ControlOffScale,
|
|
1024
|
+
severity: "warning",
|
|
1025
|
+
viewport: name,
|
|
1026
|
+
message: "Controls drawn at a size the rest of the page is not: " + off.map((o) => o.sentence).join("; ") + `. A control nobody styled keeps the runtime's own look. Give the type one look for the whole app with set_element_style (find_tools group "theme"), or set_style_preset to add on-system styles for every control; a node that sets the parameter itself keeps its own value.`,
|
|
1027
|
+
evidence: off.map((o) => ({ kind: o.kind, count: o.count, box: o.box, textSize: o.textSize, textScale: v.controls.textScale }))
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
943
1031
|
if (v.text.elements >= 10) {
|
|
944
1032
|
const weights = Object.keys(v.text.fontWeights);
|
|
945
1033
|
if (weights.length <= 1 || v.text.distinctFontSizes <= 2) {
|
|
@@ -1110,6 +1198,8 @@ var require_src = __commonJS({
|
|
|
1110
1198
|
plural,
|
|
1111
1199
|
widestOffender,
|
|
1112
1200
|
describeSites,
|
|
1201
|
+
// P112-OWN020-OFFSYSTEM — exported so its spec grades the judgement on readings, not only through a fake DOM.
|
|
1202
|
+
controlsOffScale,
|
|
1113
1203
|
TEXT_BEARING_PORTS,
|
|
1114
1204
|
placeholderStringsFromCatalog
|
|
1115
1205
|
};
|
|
@@ -174,6 +174,52 @@ var require_src = __commonJS({
|
|
|
174
174
|
.filter((x) => x.box > 0 && x.width > x.box + 1)
|
|
175
175
|
.map((x) => ({ text: x.el.textContent.trim().slice(0, 80), width: round(x.width), parentWidth: round(x.box), cls: cls(x.el) }));
|
|
176
176
|
|
|
177
|
+
// P112-OWN020-OFFSYSTEM \u2014 the controls, measured against the project's type scale. A control nobody styled draws
|
|
178
|
+
// the runtime's own look (a Radio Button is a 32px circle with a 2px black ring; a native select or button keeps
|
|
179
|
+
// the browser's 13.33px text), and that is the "chunky" control beside a page on tokens. A radio or checkbox input
|
|
180
|
+
// is opacity 0, so its box is read on the first ancestor a person can see. The scale is the project's --text-*
|
|
181
|
+
// tokens, read off :root; the fake DOMs in the tests have no getPropertyValue, so it is guarded.
|
|
182
|
+
const pxOf = (value) => {
|
|
183
|
+
const s = String(value || '').trim();
|
|
184
|
+
if (!s) return NaN;
|
|
185
|
+
if (s.slice(-3) === 'rem') return parseFloat(s) * (parseFloat(styleOf(document.documentElement).fontSize) || 16);
|
|
186
|
+
return parseFloat(s);
|
|
187
|
+
};
|
|
188
|
+
const textScale = [];
|
|
189
|
+
const rootStyle = styleOf(document.documentElement);
|
|
190
|
+
if (rootStyle && typeof rootStyle.getPropertyValue === 'function') {
|
|
191
|
+
for (const step of ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl']) {
|
|
192
|
+
const px = pxOf(rootStyle.getPropertyValue('--text-' + step));
|
|
193
|
+
if (px > 0) textScale.push(Math.round(px * 100) / 100);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const controlKind = (el) => {
|
|
197
|
+
const type = String(el.type || '').toLowerCase();
|
|
198
|
+
if (el.tagName === 'INPUT' && type === 'radio') return 'Radio Button';
|
|
199
|
+
if (el.tagName === 'INPUT' && type === 'checkbox') return 'Checkbox';
|
|
200
|
+
if (el.tagName === 'SELECT') return 'Dropdown';
|
|
201
|
+
if (el.tagName === 'TEXTAREA' || (el.tagName === 'INPUT' && ['', 'text', 'email', 'password', 'search', 'number', 'tel', 'url'].indexOf(type) !== -1)) return 'Text Input';
|
|
202
|
+
if (el.tagName === 'BUTTON') return 'Button';
|
|
203
|
+
return null;
|
|
204
|
+
};
|
|
205
|
+
const controls = [];
|
|
206
|
+
for (const el of all) {
|
|
207
|
+
const kind = controlKind(el);
|
|
208
|
+
if (!kind) continue;
|
|
209
|
+
if (kind === 'Radio Button' || kind === 'Checkbox') {
|
|
210
|
+
let shown = el;
|
|
211
|
+
for (let hops = 0; shown && hops < 3 && (parseFloat(styleOf(shown).opacity) < 0.05 || shown.getBoundingClientRect().width < 2); hops++) shown = shown.parentElement;
|
|
212
|
+
if (!shown || shown.offsetParent === null) continue;
|
|
213
|
+
const r = shown.getBoundingClientRect();
|
|
214
|
+
let label = null;
|
|
215
|
+
if (el.id && typeof document.querySelector === 'function') label = document.querySelector('label[for="' + el.id + '"]');
|
|
216
|
+
controls.push({ kind, box: round(Math.max(r.width, r.height)), textSize: label ? pxOf(styleOf(label).fontSize) : NaN });
|
|
217
|
+
} else {
|
|
218
|
+
if (visible.indexOf(el) === -1) continue;
|
|
219
|
+
controls.push({ kind, box: 0, textSize: pxOf(styleOf(el).fontSize) });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
// AWP-004 \u2014 what is on screen, as opposed to what is in the DOM. The page is
|
|
178
224
|
// never scrolled when this runs, so a rect is already a document position.
|
|
179
225
|
// Kimi K3's storefront was 83 texts in the DOM and about three on screen, and
|
|
@@ -632,6 +678,7 @@ var require_src = __commonJS({
|
|
|
632
678
|
overflowingCount: overflowing.length,
|
|
633
679
|
textsWiderThanBox: textsWiderThanBox.slice(0, 10),
|
|
634
680
|
textsWiderThanBoxCount: textsWiderThanBox.length,
|
|
681
|
+
controls: { textScale, items: controls.slice(0, 60), count: controls.length },
|
|
635
682
|
text: {
|
|
636
683
|
elements: textEls.length,
|
|
637
684
|
onScreen: textEls.filter(onScreen).length,
|
|
@@ -721,6 +768,13 @@ var require_src = __commonJS({
|
|
|
721
768
|
* page-level finding stayed quiet because the page itself kept its width.
|
|
722
769
|
*/
|
|
723
770
|
TextWiderThanItsBox: "text-wider-than-its-box",
|
|
771
|
+
/**
|
|
772
|
+
* P112-OWN020-OFFSYSTEM — a control drawn at the runtime's own size beside a page on its token scale: a radio or
|
|
773
|
+
* checkbox box much bigger than its label, or control text off the project's --text-* scale (a native select or
|
|
774
|
+
* button keeps the browser's 13.33px). Mistral (SP1) and DeepSeek (OpenTune) both shipped one — the "chunky"
|
|
775
|
+
* Sort-by circles — because a control placed in passing is the one an agent never styles.
|
|
776
|
+
*/
|
|
777
|
+
ControlOffScale: "control-off-scale",
|
|
724
778
|
SingleColumnGrid: "single-column-grid",
|
|
725
779
|
MinimumLayoutWidth: "minimum-layout-width",
|
|
726
780
|
HorizontalOverflow: "horizontal-overflow",
|
|
@@ -789,9 +843,30 @@ var require_src = __commonJS({
|
|
|
789
843
|
const named = widest.cls ? `${widest.tag.toLowerCase()}.${widest.cls.split(/\s+/)[0]}` : widest.tag.toLowerCase();
|
|
790
844
|
return `${named} at ${widest.width}px`;
|
|
791
845
|
}
|
|
846
|
+
function controlsOffScale(controls) {
|
|
847
|
+
const scale = controls.textScale || [];
|
|
848
|
+
const onScale = (px) => scale.length === 0 || !(px > 0) || scale.some((s) => Math.abs(s - px) < 0.6);
|
|
849
|
+
const byKind = {};
|
|
850
|
+
for (const c of controls.items || []) {
|
|
851
|
+
const oversized = (c.kind === "Radio Button" || c.kind === "Checkbox") && c.box > Math.max(20, 1.5 * (c.textSize > 0 ? c.textSize : 0));
|
|
852
|
+
const offText = !onScale(c.textSize);
|
|
853
|
+
if (!oversized && !offText) continue;
|
|
854
|
+
const k = byKind[c.kind] || (byKind[c.kind] = { kind: c.kind, count: 0, box: c.box, textSize: c.textSize, oversized, offText });
|
|
855
|
+
k.count += 1;
|
|
856
|
+
}
|
|
857
|
+
const fmt = (n) => String(Math.round(n * 100) / 100);
|
|
858
|
+
return Object.keys(byKind).map((kind) => {
|
|
859
|
+
const k = byKind[kind];
|
|
860
|
+
const parts = [];
|
|
861
|
+
if (k.oversized) parts.push(fmt(k.box) + "px box" + (k.textSize > 0 ? " beside a " + fmt(k.textSize) + "px label" : ""));
|
|
862
|
+
if (k.offText) parts.push(fmt(k.textSize) + "px text, off the type scale (" + scale.map(fmt).join("/") + "px)");
|
|
863
|
+
return { kind, count: k.count, box: k.box, textSize: k.textSize, sentence: kind + " x" + k.count + ": " + parts.join(", ") };
|
|
864
|
+
});
|
|
865
|
+
}
|
|
792
866
|
function summarise(viewports, diagnosis, overridden = {}) {
|
|
793
867
|
const findings = [];
|
|
794
868
|
const add = (f) => findings.push(f);
|
|
869
|
+
let controlsJudged = false;
|
|
795
870
|
for (const [name, v] of Object.entries(viewports)) {
|
|
796
871
|
if (!v || v.error) continue;
|
|
797
872
|
const isDesktop = v.requested.width >= DESKTOP_WIDTH;
|
|
@@ -940,6 +1015,19 @@ var require_src = __commonJS({
|
|
|
940
1015
|
evidence: v.textsWiderThanBox
|
|
941
1016
|
});
|
|
942
1017
|
}
|
|
1018
|
+
if (!controlsJudged && v.controls && v.controls.count > 0) {
|
|
1019
|
+
controlsJudged = true;
|
|
1020
|
+
const off = controlsOffScale(v.controls);
|
|
1021
|
+
if (off.length > 0) {
|
|
1022
|
+
add({
|
|
1023
|
+
code: RenderFinding.ControlOffScale,
|
|
1024
|
+
severity: "warning",
|
|
1025
|
+
viewport: name,
|
|
1026
|
+
message: "Controls drawn at a size the rest of the page is not: " + off.map((o) => o.sentence).join("; ") + `. A control nobody styled keeps the runtime's own look. Give the type one look for the whole app with set_element_style (find_tools group "theme"), or set_style_preset to add on-system styles for every control; a node that sets the parameter itself keeps its own value.`,
|
|
1027
|
+
evidence: off.map((o) => ({ kind: o.kind, count: o.count, box: o.box, textSize: o.textSize, textScale: v.controls.textScale }))
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
943
1031
|
if (v.text.elements >= 10) {
|
|
944
1032
|
const weights = Object.keys(v.text.fontWeights);
|
|
945
1033
|
if (weights.length <= 1 || v.text.distinctFontSizes <= 2) {
|
|
@@ -1110,6 +1198,8 @@ var require_src = __commonJS({
|
|
|
1110
1198
|
plural,
|
|
1111
1199
|
widestOffender,
|
|
1112
1200
|
describeSites,
|
|
1201
|
+
// P112-OWN020-OFFSYSTEM — exported so its spec grades the judgement on readings, not only through a fake DOM.
|
|
1202
|
+
controlsOffScale,
|
|
1113
1203
|
TEXT_BEARING_PORTS,
|
|
1114
1204
|
placeholderStringsFromCatalog
|
|
1115
1205
|
};
|
|
@@ -7018,6 +7018,24 @@ function readScriptFiles() {
|
|
|
7018
7018
|
walk(root);
|
|
7019
7019
|
return scripts;
|
|
7020
7020
|
}
|
|
7021
|
+
function readVariants() {
|
|
7022
|
+
const file = path.join(PROJECT, "nodegx.styles.json");
|
|
7023
|
+
if (!fs.existsSync(file)) return [];
|
|
7024
|
+
let styles;
|
|
7025
|
+
try {
|
|
7026
|
+
styles = readJSON(file);
|
|
7027
|
+
} catch {
|
|
7028
|
+
return [];
|
|
7029
|
+
}
|
|
7030
|
+
return (Array.isArray(styles.variants) ? styles.variants : []).filter((v) => v && typeof v.typename === "string").map((v) => ({
|
|
7031
|
+
...typeof v.name === "string" && v.name !== "" ? { name: v.name } : {},
|
|
7032
|
+
typename: v.typename,
|
|
7033
|
+
parameters: v.parameters || {},
|
|
7034
|
+
stateParameters: v.stateParameters || v.stateParamaters || {},
|
|
7035
|
+
stateTransitions: v.stateTransitions || {},
|
|
7036
|
+
defaultStateTransitions: v.defaultStateTransitions || {}
|
|
7037
|
+
}));
|
|
7038
|
+
}
|
|
7021
7039
|
function buildProjectData() {
|
|
7022
7040
|
const project = readJSON(path.join(PROJECT, "nodegx.project.json"));
|
|
7023
7041
|
const componentsDir = path.join(PROJECT, project.structure?.componentsDir || "components");
|
|
@@ -7132,7 +7150,8 @@ function buildProjectData() {
|
|
|
7132
7150
|
componentIndex: {},
|
|
7133
7151
|
rootComponent,
|
|
7134
7152
|
routerIndex: { routers, pages },
|
|
7135
|
-
scripts: readScriptFiles()
|
|
7153
|
+
scripts: readScriptFiles(),
|
|
7154
|
+
variants: readVariants()
|
|
7136
7155
|
};
|
|
7137
7156
|
}
|
|
7138
7157
|
function projectOverrideDecls() {
|