@nodegx/mcp 0.2.2 → 0.2.4
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/README.md +10 -0
- package/dist/backend/cli.js +356 -245
- package/dist/cloud-bundle.cjs +898 -18
- package/dist/kit-extract.cjs +382 -222
- package/dist/kit-extract.inputs.json +1 -0
- package/dist/node-catalog.json +45 -1
- package/dist/noodl-mcp.cjs +3832 -630
- package/dist/package.json +1 -1
- package/dist/render-harness/data/node-catalog-enriched.json +45 -1
- package/dist/render-harness/data/node-catalog.json +45 -1
- package/dist/render-harness/drive-steps.js +95 -0
- package/dist/render-harness/measure-from-disk.js +95 -0
- package/dist/render-harness/render-from-disk.js +20 -1
- package/dist/src/external/viewer/global.d.ts.keep +13 -0
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/package.json +4 -6
package/dist/package.json
CHANGED
|
@@ -37470,6 +37470,18 @@
|
|
|
37470
37470
|
"isSignal": false,
|
|
37471
37471
|
"description": "Object path inside the bucket, such as avatars/me.png. Supabase only. Leave blank to use the file's own name"
|
|
37472
37472
|
},
|
|
37473
|
+
{
|
|
37474
|
+
"name": "photoQuality",
|
|
37475
|
+
"displayName": "Photo Quality",
|
|
37476
|
+
"group": "Shrink Photos",
|
|
37477
|
+
"plug": "input",
|
|
37478
|
+
"type": {
|
|
37479
|
+
"name": "number"
|
|
37480
|
+
},
|
|
37481
|
+
"isSignal": false,
|
|
37482
|
+
"default": 80,
|
|
37483
|
+
"description": "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk"
|
|
37484
|
+
},
|
|
37473
37485
|
{
|
|
37474
37486
|
"name": "private",
|
|
37475
37487
|
"displayName": "Private",
|
|
@@ -37482,6 +37494,17 @@
|
|
|
37482
37494
|
"default": false,
|
|
37483
37495
|
"description": "Restricts the stored file to whoever uploaded it, so reading it later needs a Sign File URL node"
|
|
37484
37496
|
},
|
|
37497
|
+
{
|
|
37498
|
+
"name": "readers",
|
|
37499
|
+
"displayName": "Readers",
|
|
37500
|
+
"group": "General",
|
|
37501
|
+
"plug": "input",
|
|
37502
|
+
"type": {
|
|
37503
|
+
"name": "string"
|
|
37504
|
+
},
|
|
37505
|
+
"isSignal": false,
|
|
37506
|
+
"description": "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only"
|
|
37507
|
+
},
|
|
37485
37508
|
{
|
|
37486
37509
|
"name": "recordId",
|
|
37487
37510
|
"displayName": "Record ID (PocketBase)",
|
|
@@ -37493,6 +37516,18 @@
|
|
|
37493
37516
|
"isSignal": false,
|
|
37494
37517
|
"description": "Existing record to attach the file to. PocketBase only. Leave blank to create a new record as part of the upload"
|
|
37495
37518
|
},
|
|
37519
|
+
{
|
|
37520
|
+
"name": "shrinkTo",
|
|
37521
|
+
"displayName": "Shrink To",
|
|
37522
|
+
"group": "Shrink Photos",
|
|
37523
|
+
"plug": "input",
|
|
37524
|
+
"type": {
|
|
37525
|
+
"name": "number"
|
|
37526
|
+
},
|
|
37527
|
+
"isSignal": false,
|
|
37528
|
+
"default": 0,
|
|
37529
|
+
"description": "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked"
|
|
37530
|
+
},
|
|
37496
37531
|
{
|
|
37497
37532
|
"name": "upload",
|
|
37498
37533
|
"displayName": "Upload",
|
|
@@ -37635,6 +37670,15 @@
|
|
|
37635
37670
|
"private": [
|
|
37636
37671
|
"always: held as handed; read when the next Upload goes"
|
|
37637
37672
|
],
|
|
37673
|
+
"readers": [
|
|
37674
|
+
"always: held as handed; read when the next Upload goes — set, those roles may read the file as well as whoever uploaded it, and the file is private"
|
|
37675
|
+
],
|
|
37676
|
+
"shrinkTo": [
|
|
37677
|
+
"always: held as handed; read when the next Upload goes — above 0, a photo (not a GIF or an SVG) is drawn smaller in the browser first, its longer side at most this many pixels, and the smaller file is what goes; it goes as picked when it is already that small, when the smaller one would weigh more, or when the browser cannot read it"
|
|
37678
|
+
],
|
|
37679
|
+
"photoQuality": [
|
|
37680
|
+
"always: held as handed; read when the next Upload goes, and only when a photo is shrunk — 1 to 100 for a JPEG or WebP, anything else is 80; a PNG keeps every pixel"
|
|
37681
|
+
],
|
|
37638
37682
|
"bucket": [
|
|
37639
37683
|
"always: held as handed; read when the next Upload goes — set, the file goes to this bucket at Path (or under the file's own name)"
|
|
37640
37684
|
],
|
|
@@ -37651,7 +37695,7 @@
|
|
|
37651
37695
|
"always: held as handed; read when the next Upload goes, and only with Collection — a half-filled group is no location at all"
|
|
37652
37696
|
],
|
|
37653
37697
|
"upload": [
|
|
37654
|
-
"always: its own upload at the frame's end — two presses are two uploads — with File, Private and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
|
|
37698
|
+
"always: its own upload at the frame's end — two presses are two uploads — with File, Private, Readers and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
|
|
37655
37699
|
]
|
|
37656
37700
|
},
|
|
37657
37701
|
"enrichment": {
|
|
@@ -34883,6 +34883,18 @@
|
|
|
34883
34883
|
"isSignal": false,
|
|
34884
34884
|
"description": "Object path inside the bucket, such as avatars/me.png. Supabase only. Leave blank to use the file's own name"
|
|
34885
34885
|
},
|
|
34886
|
+
{
|
|
34887
|
+
"name": "photoQuality",
|
|
34888
|
+
"displayName": "Photo Quality",
|
|
34889
|
+
"group": "Shrink Photos",
|
|
34890
|
+
"plug": "input",
|
|
34891
|
+
"type": {
|
|
34892
|
+
"name": "number"
|
|
34893
|
+
},
|
|
34894
|
+
"isSignal": false,
|
|
34895
|
+
"default": 80,
|
|
34896
|
+
"description": "How much detail a shrunk JPEG or WebP keeps, from 1 to 100; PNG keeps every pixel and its transparency, GIF and SVG are never shrunk"
|
|
34897
|
+
},
|
|
34886
34898
|
{
|
|
34887
34899
|
"name": "private",
|
|
34888
34900
|
"displayName": "Private",
|
|
@@ -34895,6 +34907,17 @@
|
|
|
34895
34907
|
"default": false,
|
|
34896
34908
|
"description": "Restricts the stored file to whoever uploaded it, so reading it later needs a Sign File URL node"
|
|
34897
34909
|
},
|
|
34910
|
+
{
|
|
34911
|
+
"name": "readers",
|
|
34912
|
+
"displayName": "Readers",
|
|
34913
|
+
"group": "General",
|
|
34914
|
+
"plug": "input",
|
|
34915
|
+
"type": {
|
|
34916
|
+
"name": "string"
|
|
34917
|
+
},
|
|
34918
|
+
"isSignal": false,
|
|
34919
|
+
"description": "Roles that may read the file as well as whoever uploaded it, such as role:member, separated by commas; setting it makes the file private. NodeGX backends only"
|
|
34920
|
+
},
|
|
34898
34921
|
{
|
|
34899
34922
|
"name": "recordId",
|
|
34900
34923
|
"displayName": "Record ID (PocketBase)",
|
|
@@ -34906,6 +34929,18 @@
|
|
|
34906
34929
|
"isSignal": false,
|
|
34907
34930
|
"description": "Existing record to attach the file to. PocketBase only. Leave blank to create a new record as part of the upload"
|
|
34908
34931
|
},
|
|
34932
|
+
{
|
|
34933
|
+
"name": "shrinkTo",
|
|
34934
|
+
"displayName": "Shrink To",
|
|
34935
|
+
"group": "Shrink Photos",
|
|
34936
|
+
"plug": "input",
|
|
34937
|
+
"type": {
|
|
34938
|
+
"name": "number"
|
|
34939
|
+
},
|
|
34940
|
+
"isSignal": false,
|
|
34941
|
+
"default": 0,
|
|
34942
|
+
"description": "Makes a photo smaller in the browser before sending it, so its longer side is at most this many pixels; 0 sends every file as picked"
|
|
34943
|
+
},
|
|
34909
34944
|
{
|
|
34910
34945
|
"name": "upload",
|
|
34911
34946
|
"displayName": "Upload",
|
|
@@ -35048,6 +35083,15 @@
|
|
|
35048
35083
|
"private": [
|
|
35049
35084
|
"always: held as handed; read when the next Upload goes"
|
|
35050
35085
|
],
|
|
35086
|
+
"readers": [
|
|
35087
|
+
"always: held as handed; read when the next Upload goes — set, those roles may read the file as well as whoever uploaded it, and the file is private"
|
|
35088
|
+
],
|
|
35089
|
+
"shrinkTo": [
|
|
35090
|
+
"always: held as handed; read when the next Upload goes — above 0, a photo (not a GIF or an SVG) is drawn smaller in the browser first, its longer side at most this many pixels, and the smaller file is what goes; it goes as picked when it is already that small, when the smaller one would weigh more, or when the browser cannot read it"
|
|
35091
|
+
],
|
|
35092
|
+
"photoQuality": [
|
|
35093
|
+
"always: held as handed; read when the next Upload goes, and only when a photo is shrunk — 1 to 100 for a JPEG or WebP, anything else is 80; a PNG keeps every pixel"
|
|
35094
|
+
],
|
|
35051
35095
|
"bucket": [
|
|
35052
35096
|
"always: held as handed; read when the next Upload goes — set, the file goes to this bucket at Path (or under the file's own name)"
|
|
35053
35097
|
],
|
|
@@ -35064,7 +35108,7 @@
|
|
|
35064
35108
|
"always: held as handed; read when the next Upload goes, and only with Collection — a half-filled group is no location at all"
|
|
35065
35109
|
],
|
|
35066
35110
|
"upload": [
|
|
35067
|
-
"always: its own upload at the frame's end — two presses are two uploads — with File, Private and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
|
|
35111
|
+
"always: its own upload at the frame's end — two presses are two uploads — with File, Private, Readers and the File Location held then: no File, or a Backend the project does not have, is Error, Error Status Code 0 and Failure at once; otherwise the backend answers with Cloud File then Done, or Error, Error Status Code and Failure"
|
|
35068
35112
|
]
|
|
35069
35113
|
}
|
|
35070
35114
|
},
|
|
@@ -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
|
};
|
|
@@ -2030,6 +2120,11 @@ var require_render_report = __commonJS({
|
|
|
2030
2120
|
"--no-default-browser-check",
|
|
2031
2121
|
"--hide-scrollbars",
|
|
2032
2122
|
"--disable-gpu",
|
|
2123
|
+
// PARKED-THREE-S3-NOWEBGL: `--disable-gpu` alone leaves no WebGL, so a three.js scene (three-kit, garden-3d-kit)
|
|
2124
|
+
// drew its "No WebGL here" fallback to the agent. Software GL brings WebGL back with no GPU (measured 2026-10-19 in
|
|
2125
|
+
// this harness: gl false → true, the three-kit fixture's 12 meshes drawn); a page with no WebGL never touches it.
|
|
2126
|
+
"--use-angle=swiftshader",
|
|
2127
|
+
"--enable-unsafe-swiftshader",
|
|
2033
2128
|
// P112-S14-KEYCHAIN: on macOS a page that touches storage makes Chrome open the login keychain. With no
|
|
2034
2129
|
// keychain where `HOME` points (an empty HOME, a CI user) the OS shows "Keychain Not Found" on the person's
|
|
2035
2130
|
// screen and Chrome waits for the answer, so the render hangs until its 240 s kill. A throwaway profile
|
|
@@ -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
|
};
|
|
@@ -2030,6 +2120,11 @@ var require_render_report = __commonJS({
|
|
|
2030
2120
|
"--no-default-browser-check",
|
|
2031
2121
|
"--hide-scrollbars",
|
|
2032
2122
|
"--disable-gpu",
|
|
2123
|
+
// PARKED-THREE-S3-NOWEBGL: `--disable-gpu` alone leaves no WebGL, so a three.js scene (three-kit, garden-3d-kit)
|
|
2124
|
+
// drew its "No WebGL here" fallback to the agent. Software GL brings WebGL back with no GPU (measured 2026-10-19 in
|
|
2125
|
+
// this harness: gl false → true, the three-kit fixture's 12 meshes drawn); a page with no WebGL never touches it.
|
|
2126
|
+
"--use-angle=swiftshader",
|
|
2127
|
+
"--enable-unsafe-swiftshader",
|
|
2033
2128
|
// P112-S14-KEYCHAIN: on macOS a page that touches storage makes Chrome open the login keychain. With no
|
|
2034
2129
|
// keychain where `HOME` points (an empty HOME, a CI user) the OS shows "Keychain Not Found" on the person's
|
|
2035
2130
|
// screen and Chrome waits for the answer, so the render hangs until its 240 s kill. A throwaway profile
|
|
@@ -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() {
|
|
@@ -592,6 +592,19 @@ declare namespace Noodl {
|
|
|
592
592
|
onProgress?: (progress: number) => void;
|
|
593
593
|
}
|
|
594
594
|
): Promise<CloudFile>;
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* A link an image can use for a private file, the way the Sign File URL node mints one.
|
|
598
|
+
* Takes a CloudFile, a record's file (or an item of a list of them), or the stored name.
|
|
599
|
+
* Rejects for someone who could not read the file itself.
|
|
600
|
+
*
|
|
601
|
+
* Example:
|
|
602
|
+
* ```ts
|
|
603
|
+
* const link = await Noodl.Files.sign(post.photos[0])
|
|
604
|
+
* console.log(link.url, link.expiresAt)
|
|
605
|
+
* ```
|
|
606
|
+
*/
|
|
607
|
+
sign(file: CloudFile | { name: string } | string): Promise<{ url: string; kind: string; expiresAt?: string; ttlSeconds?: number }>;
|
|
595
608
|
}
|
|
596
609
|
|
|
597
610
|
/**
|