@nodegx/mcp 0.2.3 → 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/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodegx-mcp-bundles",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": true,
5
5
  "description": "Build metadata for the bundled CLIs beside it. See build-cloud-bundle.mjs: @noodl/platform reads it."
6
6
  }
@@ -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() {