@nodegx/mcp 0.2.1 → 0.2.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/kit-extract.cjs +2 -2
- package/dist/noodl-mcp.cjs +4373 -177
- package/dist/package.json +1 -1
- package/dist/render-harness/drive-steps.js +69 -5
- package/dist/render-harness/measure-from-disk.js +63 -3
- package/package.json +1 -1
package/dist/package.json
CHANGED
|
@@ -551,7 +551,10 @@ var require_src = __commonJS({
|
|
|
551
551
|
// two parts, a centred heading block and a toggle-and-price group ~260 px left of it.
|
|
552
552
|
const byTop = [...inks].sort((a, b) => a.top - b.top);
|
|
553
553
|
if (!byTop.every((k, i) => i === 0 || k.top >= byTop[i - 1].bottom - 2)) continue;
|
|
554
|
-
|
|
554
|
+
// Two or more tall parts, not one: RL1's waitlist is eyebrow, heading, line and a 290 px form card in ONE group,
|
|
555
|
+
// and "any part > 240" skipped it \u2014 Richard saw the eyebrow off the centred heading (s6). A page of sections
|
|
556
|
+
// has many tall children; a section with one card has one.
|
|
557
|
+
if (byTop.filter((k) => k.height > 240).length > 1) continue;
|
|
555
558
|
const centre = pr.left + pr.width / 2;
|
|
556
559
|
const offOf = (k) => k.left + k.width / 2 - centre;
|
|
557
560
|
const centred = byTop.filter((k) => Math.abs(offOf(k)) <= 12 && k.width < pr.width * 0.9);
|
|
@@ -564,6 +567,47 @@ var require_src = __commonJS({
|
|
|
564
567
|
}
|
|
565
568
|
}
|
|
566
569
|
|
|
570
|
+
// P112-OWN019-LETTEREDGE \u2014 words hard against the edge of the box that frames them. Richard on TL2's hero: seven
|
|
571
|
+
// outlined day chips, each letter pressed to its chip's left edge. The chip Group centred its child the way CSS
|
|
572
|
+
// would, but a Text stretches to its Group's width and sets its letters left, so the centring moved nothing. The
|
|
573
|
+
// frame is the nearest of the text's first four ancestors with a border or a fill (a page-wide band is not a frame,
|
|
574
|
+
// nor an inline highlight); the reading is the gap between the words' own ink (a Range) and the frame's inner left
|
|
575
|
+
// and right edges. Every width: a chip is a chip on a phone too. Raw numbers here; the judgement is summarise's.
|
|
576
|
+
layout.edgeTexts = [];
|
|
577
|
+
const EDGE_TEXT_MAX_GAP = 3;
|
|
578
|
+
if (typeof document.createRange === 'function' && typeof document.createTreeWalker === 'function') {
|
|
579
|
+
const shown = new Set(visible);
|
|
580
|
+
const range = document.createRange();
|
|
581
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
582
|
+
const framed = (el) => {
|
|
583
|
+
const cs = styleOf(el);
|
|
584
|
+
if (cs.display === 'inline') return false;
|
|
585
|
+
const bg = cs.backgroundColor;
|
|
586
|
+
return (bg && bg !== 'transparent' && !/rgba\\([^)]*,\\s*0\\)$/.test(bg)) || cs.backgroundImage !== 'none' ||
|
|
587
|
+
parseFloat(cs.borderLeftWidth) > 0 || parseFloat(cs.borderRightWidth) > 0;
|
|
588
|
+
};
|
|
589
|
+
const frames = new Set();
|
|
590
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
591
|
+
const text = n.textContent.trim();
|
|
592
|
+
if (!text || !shown.has(n.parentElement)) continue;
|
|
593
|
+
let frame = null;
|
|
594
|
+
for (let e = n.parentElement, i = 0; e && e !== document.body && i < 4; e = e.parentElement, i++) {
|
|
595
|
+
if (framed(e)) { frame = e; break; }
|
|
596
|
+
}
|
|
597
|
+
if (!frame || frames.has(frame)) continue;
|
|
598
|
+
const fr = frame.getBoundingClientRect();
|
|
599
|
+
if (fr.width < 16 || fr.width >= vw * 0.98) continue;
|
|
600
|
+
range.selectNodeContents(n);
|
|
601
|
+
const r = range.getBoundingClientRect();
|
|
602
|
+
if (!(r.width > 0 && r.height > 0)) continue;
|
|
603
|
+
const fcs = styleOf(frame);
|
|
604
|
+
const left = r.left - (fr.left + (parseFloat(fcs.borderLeftWidth) || 0));
|
|
605
|
+
const right = fr.right - (parseFloat(fcs.borderRightWidth) || 0) - r.right;
|
|
606
|
+
frames.add(frame);
|
|
607
|
+
if (Math.min(left, right) < EDGE_TEXT_MAX_GAP) layout.edgeTexts.push({ cls: cls(frame), text: text.slice(0, 50), left: round(left), right: round(right), frameWidth: round(fr.width), frameHeight: round(fr.height) });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
567
611
|
const contentBottom = visible.reduce((lowest, el) => {
|
|
568
612
|
const r = el.getBoundingClientRect();
|
|
569
613
|
return r.width > 0 && r.height > 0 && r.bottom > lowest ? r.bottom : lowest;
|
|
@@ -718,7 +762,12 @@ var require_src = __commonJS({
|
|
|
718
762
|
*/
|
|
719
763
|
RowGap: "row-gap",
|
|
720
764
|
/** P112-OWN019-LAPTOPGAPS — a section that centres its heading and leaves another part off that centre (L1's price). */
|
|
721
|
-
AlignmentDrift: "alignment-drift"
|
|
765
|
+
AlignmentDrift: "alignment-drift",
|
|
766
|
+
/**
|
|
767
|
+
* P112-OWN019-LETTEREDGE (Richard 2026-10-15, TL2's hero). Words within 3 px of the inner left or right edge of the
|
|
768
|
+
* box that frames them (a border or a fill) — a letter in an outlined chip set left because its Text stretched.
|
|
769
|
+
*/
|
|
770
|
+
TextAgainstEdge: "text-against-edge"
|
|
722
771
|
};
|
|
723
772
|
var SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
724
773
|
var ROW_GAP_MIN_TALLEST = 48;
|
|
@@ -939,7 +988,7 @@ var require_src = __commonJS({
|
|
|
939
988
|
});
|
|
940
989
|
}
|
|
941
990
|
if (isDesktop && v.layout) {
|
|
942
|
-
const gaps = (v.layout.rowGaps || []).filter((g) => g.tallest >= ROW_GAP_MIN_TALLEST && g.gap >= g.rowWidth * ROW_GAP_MIN_SHARE);
|
|
991
|
+
const gaps = (v.layout.rowGaps || []).filter((g) => (g.left || g.right) && g.tallest >= ROW_GAP_MIN_TALLEST && g.gap >= g.rowWidth * ROW_GAP_MIN_SHARE);
|
|
943
992
|
const seen2 = /* @__PURE__ */ new Set();
|
|
944
993
|
for (const g of gaps) {
|
|
945
994
|
const key = `${g.left}|${g.right}`;
|
|
@@ -964,6 +1013,17 @@ var require_src = __commonJS({
|
|
|
964
1013
|
});
|
|
965
1014
|
}
|
|
966
1015
|
}
|
|
1016
|
+
const edges = v.layout && v.layout.edgeTexts || [];
|
|
1017
|
+
if (edges.length) {
|
|
1018
|
+
const named = edges.slice(0, 3).map((e) => `"${e.text}" ${Math.max(0, Math.min(e.left, e.right))}px from the ${e.left <= e.right ? "left" : "right"} edge of its ${e.frameWidth}\xD7${e.frameHeight}px box`);
|
|
1019
|
+
add({
|
|
1020
|
+
code: RenderFinding.TextAgainstEdge,
|
|
1021
|
+
severity: "warning",
|
|
1022
|
+
viewport: name,
|
|
1023
|
+
message: `${plural(edges.length, "text sits", "texts sit")} hard against the edge of the outlined or filled box that frames ${edges.length === 1 ? "it" : "them"}: ${named.join("; ")}. Centring a Group does not centre a Text's letters \u2014 the Text stretches to the Group's width and sets them left \u2014 so give the Text textAlignX: center, or give the box padding. Look at this width's screenshot.`,
|
|
1024
|
+
evidence: edges.slice(0, 6)
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
967
1027
|
if (v.consoleErrors && v.consoleErrors.length) {
|
|
968
1028
|
add({
|
|
969
1029
|
code: RenderFinding.ConsoleError,
|
|
@@ -2761,7 +2821,11 @@ async function runSteps(page, steps, { outDir, stubs = routeStubs(page), network
|
|
|
2761
2821
|
} else if (verb === "watch") {
|
|
2762
2822
|
const w = step.watch && typeof step.watch === "object" ? step.watch : {};
|
|
2763
2823
|
if (w.look === void 0) throw new UsageError(`Step ${i + 1}: watch takes { look: T } or { look: [T, T, \u2026] }.`);
|
|
2764
|
-
const
|
|
2824
|
+
const unknown = Object.keys(w).filter((k) => !["look", "for_ms", "expect"].includes(k));
|
|
2825
|
+
if (unknown.length) throw new UsageError(`Step ${i + 1}: watch does not take ${unknown.map((k) => JSON.stringify(k)).join(", ")} \u2014 it takes look, for_ms and expect.`);
|
|
2826
|
+
if (step.expect !== void 0 && w.expect !== void 0) throw new UsageError(`Step ${i + 1}: give watch one expect, beside it or inside it, not both.`);
|
|
2827
|
+
const forMs = Math.min(1e4, Math.max(100, Number(step.for_ms ?? w.for_ms ?? 1500)));
|
|
2828
|
+
const expect = step.expect !== void 0 ? step.expect : w.expect;
|
|
2765
2829
|
const many = Array.isArray(w.look);
|
|
2766
2830
|
const targets = many ? w.look : [w.look];
|
|
2767
2831
|
if (targets.length === 0 || targets.length > 8) throw new UsageError(`Step ${i + 1}: watch looks at 1 to 8 targets.`);
|
|
@@ -2782,7 +2846,7 @@ async function runSteps(page, steps, { outDir, stubs = routeStubs(page), network
|
|
|
2782
2846
|
}
|
|
2783
2847
|
const shown = (list) => list.map(({ from, until, look }) => ({ from, until, look }));
|
|
2784
2848
|
row.value = many ? targets.map((t, k) => ({ target: t, states: shown(states[k]) })) : shown(states[0]);
|
|
2785
|
-
const never =
|
|
2849
|
+
const never = expect && typeof expect === "object" ? expect.never : void 0;
|
|
2786
2850
|
if (never && typeof never === "object") {
|
|
2787
2851
|
for (let k = 0; k < targets.length && row.ok; k++) {
|
|
2788
2852
|
const hit = states[k].find((st) => st.look && Object.keys(never).every((key) => matches(st.look[key], never[key])));
|
|
@@ -551,7 +551,10 @@ var require_src = __commonJS({
|
|
|
551
551
|
// two parts, a centred heading block and a toggle-and-price group ~260 px left of it.
|
|
552
552
|
const byTop = [...inks].sort((a, b) => a.top - b.top);
|
|
553
553
|
if (!byTop.every((k, i) => i === 0 || k.top >= byTop[i - 1].bottom - 2)) continue;
|
|
554
|
-
|
|
554
|
+
// Two or more tall parts, not one: RL1's waitlist is eyebrow, heading, line and a 290 px form card in ONE group,
|
|
555
|
+
// and "any part > 240" skipped it \u2014 Richard saw the eyebrow off the centred heading (s6). A page of sections
|
|
556
|
+
// has many tall children; a section with one card has one.
|
|
557
|
+
if (byTop.filter((k) => k.height > 240).length > 1) continue;
|
|
555
558
|
const centre = pr.left + pr.width / 2;
|
|
556
559
|
const offOf = (k) => k.left + k.width / 2 - centre;
|
|
557
560
|
const centred = byTop.filter((k) => Math.abs(offOf(k)) <= 12 && k.width < pr.width * 0.9);
|
|
@@ -564,6 +567,47 @@ var require_src = __commonJS({
|
|
|
564
567
|
}
|
|
565
568
|
}
|
|
566
569
|
|
|
570
|
+
// P112-OWN019-LETTEREDGE \u2014 words hard against the edge of the box that frames them. Richard on TL2's hero: seven
|
|
571
|
+
// outlined day chips, each letter pressed to its chip's left edge. The chip Group centred its child the way CSS
|
|
572
|
+
// would, but a Text stretches to its Group's width and sets its letters left, so the centring moved nothing. The
|
|
573
|
+
// frame is the nearest of the text's first four ancestors with a border or a fill (a page-wide band is not a frame,
|
|
574
|
+
// nor an inline highlight); the reading is the gap between the words' own ink (a Range) and the frame's inner left
|
|
575
|
+
// and right edges. Every width: a chip is a chip on a phone too. Raw numbers here; the judgement is summarise's.
|
|
576
|
+
layout.edgeTexts = [];
|
|
577
|
+
const EDGE_TEXT_MAX_GAP = 3;
|
|
578
|
+
if (typeof document.createRange === 'function' && typeof document.createTreeWalker === 'function') {
|
|
579
|
+
const shown = new Set(visible);
|
|
580
|
+
const range = document.createRange();
|
|
581
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
582
|
+
const framed = (el) => {
|
|
583
|
+
const cs = styleOf(el);
|
|
584
|
+
if (cs.display === 'inline') return false;
|
|
585
|
+
const bg = cs.backgroundColor;
|
|
586
|
+
return (bg && bg !== 'transparent' && !/rgba\\([^)]*,\\s*0\\)$/.test(bg)) || cs.backgroundImage !== 'none' ||
|
|
587
|
+
parseFloat(cs.borderLeftWidth) > 0 || parseFloat(cs.borderRightWidth) > 0;
|
|
588
|
+
};
|
|
589
|
+
const frames = new Set();
|
|
590
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
591
|
+
const text = n.textContent.trim();
|
|
592
|
+
if (!text || !shown.has(n.parentElement)) continue;
|
|
593
|
+
let frame = null;
|
|
594
|
+
for (let e = n.parentElement, i = 0; e && e !== document.body && i < 4; e = e.parentElement, i++) {
|
|
595
|
+
if (framed(e)) { frame = e; break; }
|
|
596
|
+
}
|
|
597
|
+
if (!frame || frames.has(frame)) continue;
|
|
598
|
+
const fr = frame.getBoundingClientRect();
|
|
599
|
+
if (fr.width < 16 || fr.width >= vw * 0.98) continue;
|
|
600
|
+
range.selectNodeContents(n);
|
|
601
|
+
const r = range.getBoundingClientRect();
|
|
602
|
+
if (!(r.width > 0 && r.height > 0)) continue;
|
|
603
|
+
const fcs = styleOf(frame);
|
|
604
|
+
const left = r.left - (fr.left + (parseFloat(fcs.borderLeftWidth) || 0));
|
|
605
|
+
const right = fr.right - (parseFloat(fcs.borderRightWidth) || 0) - r.right;
|
|
606
|
+
frames.add(frame);
|
|
607
|
+
if (Math.min(left, right) < EDGE_TEXT_MAX_GAP) layout.edgeTexts.push({ cls: cls(frame), text: text.slice(0, 50), left: round(left), right: round(right), frameWidth: round(fr.width), frameHeight: round(fr.height) });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
567
611
|
const contentBottom = visible.reduce((lowest, el) => {
|
|
568
612
|
const r = el.getBoundingClientRect();
|
|
569
613
|
return r.width > 0 && r.height > 0 && r.bottom > lowest ? r.bottom : lowest;
|
|
@@ -718,7 +762,12 @@ var require_src = __commonJS({
|
|
|
718
762
|
*/
|
|
719
763
|
RowGap: "row-gap",
|
|
720
764
|
/** P112-OWN019-LAPTOPGAPS — a section that centres its heading and leaves another part off that centre (L1's price). */
|
|
721
|
-
AlignmentDrift: "alignment-drift"
|
|
765
|
+
AlignmentDrift: "alignment-drift",
|
|
766
|
+
/**
|
|
767
|
+
* P112-OWN019-LETTEREDGE (Richard 2026-10-15, TL2's hero). Words within 3 px of the inner left or right edge of the
|
|
768
|
+
* box that frames them (a border or a fill) — a letter in an outlined chip set left because its Text stretched.
|
|
769
|
+
*/
|
|
770
|
+
TextAgainstEdge: "text-against-edge"
|
|
722
771
|
};
|
|
723
772
|
var SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
724
773
|
var ROW_GAP_MIN_TALLEST = 48;
|
|
@@ -939,7 +988,7 @@ var require_src = __commonJS({
|
|
|
939
988
|
});
|
|
940
989
|
}
|
|
941
990
|
if (isDesktop && v.layout) {
|
|
942
|
-
const gaps = (v.layout.rowGaps || []).filter((g) => g.tallest >= ROW_GAP_MIN_TALLEST && g.gap >= g.rowWidth * ROW_GAP_MIN_SHARE);
|
|
991
|
+
const gaps = (v.layout.rowGaps || []).filter((g) => (g.left || g.right) && g.tallest >= ROW_GAP_MIN_TALLEST && g.gap >= g.rowWidth * ROW_GAP_MIN_SHARE);
|
|
943
992
|
const seen2 = /* @__PURE__ */ new Set();
|
|
944
993
|
for (const g of gaps) {
|
|
945
994
|
const key = `${g.left}|${g.right}`;
|
|
@@ -964,6 +1013,17 @@ var require_src = __commonJS({
|
|
|
964
1013
|
});
|
|
965
1014
|
}
|
|
966
1015
|
}
|
|
1016
|
+
const edges = v.layout && v.layout.edgeTexts || [];
|
|
1017
|
+
if (edges.length) {
|
|
1018
|
+
const named = edges.slice(0, 3).map((e) => `"${e.text}" ${Math.max(0, Math.min(e.left, e.right))}px from the ${e.left <= e.right ? "left" : "right"} edge of its ${e.frameWidth}\xD7${e.frameHeight}px box`);
|
|
1019
|
+
add({
|
|
1020
|
+
code: RenderFinding.TextAgainstEdge,
|
|
1021
|
+
severity: "warning",
|
|
1022
|
+
viewport: name,
|
|
1023
|
+
message: `${plural(edges.length, "text sits", "texts sit")} hard against the edge of the outlined or filled box that frames ${edges.length === 1 ? "it" : "them"}: ${named.join("; ")}. Centring a Group does not centre a Text's letters \u2014 the Text stretches to the Group's width and sets them left \u2014 so give the Text textAlignX: center, or give the box padding. Look at this width's screenshot.`,
|
|
1024
|
+
evidence: edges.slice(0, 6)
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
967
1027
|
if (v.consoleErrors && v.consoleErrors.length) {
|
|
968
1028
|
add({
|
|
969
1029
|
code: RenderFinding.ConsoleError,
|
package/package.json
CHANGED