@nodegx/mcp 0.2.0 → 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/backend/cli.js +182 -182
- package/dist/cloud-bundle.cjs +747 -745
- package/dist/kit-extract.cjs +228 -228
- package/dist/kit-extract.inputs.json +113 -113
- package/dist/noodl-mcp.cjs +6073 -1090
- package/dist/package.json +1 -1
- package/dist/render-harness/data/node-catalog-enriched.json +242 -9
- package/dist/render-harness/drive-steps.js +222 -18
- package/dist/render-harness/measure-from-disk.js +172 -2
- package/dist/render-harness/render-from-disk.js +136 -136
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/dist/starter-assets/noodl_modules/inter/Inter-Bold.woff2 +0 -0
- package/dist/starter-assets/noodl_modules/inter/Inter-Medium.woff2 +0 -0
- package/dist/starter-assets/noodl_modules/inter/Inter-Regular.woff2 +0 -0
- package/dist/starter-assets/noodl_modules/inter/Inter-SemiBold.woff2 +0 -0
- package/dist/starter-assets/noodl_modules/inter/LICENSE.txt +93 -0
- package/dist/starter-assets/noodl_modules/inter/manifest.json +7 -0
- package/dist/starter-assets/noodl_modules/inter/styles.css +49 -0
- package/dist/starter-assets/noodl_modules/lucide-icons/lucide.woff2 +0 -0
- package/dist/starter-assets/noodl_modules/lucide-icons/manifest.json +229 -0
- package/dist/starter-assets/noodl_modules/lucide-icons/styles.css +2007 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/LICENCES.json +445 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/animal-cat.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/animal-dog.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/animal-horse.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/animal-sheep.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/avatar-1.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/avatar-2.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/avatar-3.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/avatar-4.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/avatar-5.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/avatar-6.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/food-bakery.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/food-board.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/food-bread.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/food-carrots.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/food-grocer.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/food-market.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/food-plate.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-aurora.svg +28 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-canyon.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-city-dusk.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-coast.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-dune.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-forest-fog.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-red-rock.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-ridge.svg +28 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-shore.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/ground-skyline.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/manifest.json +4 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/people-cafe.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/people-coffee-shop.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/people-desk.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/people-market.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/people-meeting.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/portrait.svg +11 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/texture-brick.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/texture-coffee.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/texture-earth.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/texture-pebbles.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/texture-snow.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/texture-soil.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/tile-1.svg +17 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/tile-2.svg +17 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/tile-3.svg +17 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-baker.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-carpenter.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-chef.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-hands-clay.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-leather-bench.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-machine-shop.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-potter.webp +0 -0
- package/dist/starter-assets/noodl_modules/starter-imagery/work-welder.webp +0 -0
- package/package.json +4 -3
|
@@ -492,6 +492,122 @@ var require_src = __commonJS({
|
|
|
492
492
|
// viewport height with content laid out past it is clipped; a page that is
|
|
493
493
|
// simply short has nothing past it. That difference is the whole of
|
|
494
494
|
// \`clipped-page\`, and neither number alone can tell them apart.
|
|
495
|
+
// P112-OWN019-LAPTOPGAPS (R39) \u2014 where the INK is, not where the boxes are. Richard on RL1's laptop page: a
|
|
496
|
+
// feature number in a Columns "1 4" row had a fifth of the page to itself, ~215 px of nothing before its title;
|
|
497
|
+
// L1's pricing toggle and price sat ~260 px left of their centred heading. Every box there is well-formed \u2014 the
|
|
498
|
+
// number's column box abuts the title's \u2014 so the measure is each child's ink: the union of its shown text runs
|
|
499
|
+
// (a Range, so a full-width text block counts only as far as its words reach) and its painted boxes. Desktop only:
|
|
500
|
+
// that is where a row has room to open a hole. Raw numbers here; the judgement is summarise's.
|
|
501
|
+
const layout = { rowGaps: [], drifts: [] };
|
|
502
|
+
// (The API guard is for the fake DOMs the unit suites hand this expression; every browser has both.)
|
|
503
|
+
if (vw >= 1024 && typeof document.createRange === 'function' && typeof document.createTreeWalker === 'function') {
|
|
504
|
+
const shown = new Set(visible);
|
|
505
|
+
const textInk = [];
|
|
506
|
+
const range = document.createRange();
|
|
507
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
508
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
509
|
+
if (!n.textContent.trim() || !shown.has(n.parentElement)) continue;
|
|
510
|
+
range.selectNodeContents(n);
|
|
511
|
+
const r = range.getBoundingClientRect();
|
|
512
|
+
if (r.width > 0 && r.height > 0) textInk.push({ node: n, r });
|
|
513
|
+
}
|
|
514
|
+
const PAINTED_TAGS = ['IMG', 'SVG', 'CANVAS', 'VIDEO', 'INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'svg'];
|
|
515
|
+
const paintedInk = visible
|
|
516
|
+
.filter((el) => {
|
|
517
|
+
if (PAINTED_TAGS.includes(el.tagName)) return true;
|
|
518
|
+
const cs = styleOf(el);
|
|
519
|
+
const bg = cs.backgroundColor;
|
|
520
|
+
return (bg && bg !== 'transparent' && !/rgba\\([^)]*,\\s*0\\)$/.test(bg)) || cs.backgroundImage !== 'none' || parseFloat(cs.borderTopWidth) > 0;
|
|
521
|
+
})
|
|
522
|
+
.map((el) => ({ el, r: el.getBoundingClientRect() }))
|
|
523
|
+
.filter((p) => p.r.width > 0 && p.r.height > 0 && p.r.width < vw * 0.98);
|
|
524
|
+
const inkOf = (el) => {
|
|
525
|
+
let l = Infinity, t = Infinity, rr = -Infinity, b = -Infinity, text = '';
|
|
526
|
+
const take = (r) => { l = Math.min(l, r.left); t = Math.min(t, r.top); rr = Math.max(rr, r.right); b = Math.max(b, r.bottom); };
|
|
527
|
+
for (const x of textInk) if (el.contains(x.node)) { take(x.r); if (!text) text = x.node.textContent.trim().slice(0, 50); }
|
|
528
|
+
for (const p of paintedInk) if (el === p.el || el.contains(p.el)) take(p.r);
|
|
529
|
+
return l === Infinity ? null : { left: l, top: t, right: rr, bottom: b, width: rr - l, height: b - t, text };
|
|
530
|
+
};
|
|
531
|
+
const ROW_GAP_MIN_PX = 160;
|
|
532
|
+
for (const parent of visible) {
|
|
533
|
+
const kids = [...parent.children].filter((k) => shown.has(k));
|
|
534
|
+
if (kids.length < 2) continue;
|
|
535
|
+
const pr = parent.getBoundingClientRect();
|
|
536
|
+
if (pr.width < 600) continue;
|
|
537
|
+
const inks = kids.map(inkOf).filter(Boolean);
|
|
538
|
+
if (inks.length < 2) continue;
|
|
539
|
+
// Side by side: consecutive by left edge, overlapping vertically, the second starting after the first ends.
|
|
540
|
+
const byLeft = [...inks].sort((a, b) => a.left - b.left);
|
|
541
|
+
for (let i = 1; i < byLeft.length; i++) {
|
|
542
|
+
const a = byLeft[i - 1], b = byLeft[i];
|
|
543
|
+
const overlap = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
|
|
544
|
+
const gap = b.left - a.right;
|
|
545
|
+
if (overlap <= 0 || gap < ROW_GAP_MIN_PX) continue;
|
|
546
|
+
layout.rowGaps.push({ cls: cls(parent), gap: round(gap), rowWidth: round(pr.width), tallest: round(Math.max(a.height, b.height)), left: a.text, right: b.text });
|
|
547
|
+
}
|
|
548
|
+
// Stacked, each starting below the one before, and the parts of ONE section: every part under 240 px tall (a
|
|
549
|
+
// page whose children are whole sections compared section against section \u2014 measured on L1 and GL1, s5). The
|
|
550
|
+
// section's axis is set by a centred part among its first two (its heading, usually); L1's pricing is just
|
|
551
|
+
// two parts, a centred heading block and a toggle-and-price group ~260 px left of it.
|
|
552
|
+
const byTop = [...inks].sort((a, b) => a.top - b.top);
|
|
553
|
+
if (!byTop.every((k, i) => i === 0 || k.top >= byTop[i - 1].bottom - 2)) continue;
|
|
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;
|
|
558
|
+
const centre = pr.left + pr.width / 2;
|
|
559
|
+
const offOf = (k) => k.left + k.width / 2 - centre;
|
|
560
|
+
const centred = byTop.filter((k) => Math.abs(offOf(k)) <= 12 && k.width < pr.width * 0.9);
|
|
561
|
+
if (!centred.some((k) => byTop.indexOf(k) < 2)) continue;
|
|
562
|
+
for (const k of byTop) {
|
|
563
|
+
if (Math.abs(offOf(k)) > 40 && k.width < pr.width * 0.8) {
|
|
564
|
+
layout.drifts.push({ cls: cls(parent), offBy: round(offOf(k)), sectionWidth: round(pr.width), text: k.text, centredText: centred[0].text });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
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
|
+
|
|
495
611
|
const contentBottom = visible.reduce((lowest, el) => {
|
|
496
612
|
const r = el.getBoundingClientRect();
|
|
497
613
|
return r.width > 0 && r.height > 0 && r.bottom > lowest ? r.bottom : lowest;
|
|
@@ -499,6 +615,7 @@ var require_src = __commonJS({
|
|
|
499
615
|
|
|
500
616
|
return {
|
|
501
617
|
lists,
|
|
618
|
+
layout,
|
|
502
619
|
// FLD-012 AC3 \u2014 the SIZE of the population every rule below is computed over, and the number of
|
|
503
620
|
// elements the visibility correction took out of it. Reported directly because a rule's finding
|
|
504
621
|
// count can move for several reasons at once, and 'the fix changed what is measured' is a
|
|
@@ -637,9 +754,24 @@ var require_src = __commonJS({
|
|
|
637
754
|
*/
|
|
638
755
|
SingleGround: "single-ground",
|
|
639
756
|
NoImagery: "no-imagery",
|
|
640
|
-
NoDisplayType: "no-display-type"
|
|
757
|
+
NoDisplayType: "no-display-type",
|
|
758
|
+
/**
|
|
759
|
+
* P112-OWN019-LAPTOPGAPS (R39, Richard 2026-10-14). On a laptop, two things side by side in one row with a hole
|
|
760
|
+
* between their ink — RL1's feature number given a fifth of the page by a Columns "1 4" row, L1's text and card
|
|
761
|
+
* pushed to the row's two edges. A warning: a hole can be meant, and the screenshots are the arbiter.
|
|
762
|
+
*/
|
|
763
|
+
RowGap: "row-gap",
|
|
764
|
+
/** P112-OWN019-LAPTOPGAPS — a section that centres its heading and leaves another part off that centre (L1's price). */
|
|
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"
|
|
641
771
|
};
|
|
642
772
|
var SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
773
|
+
var ROW_GAP_MIN_TALLEST = 48;
|
|
774
|
+
var ROW_GAP_MIN_SHARE = 0.12;
|
|
643
775
|
var POVERTY_FINDINGS = [RenderFinding.SingleGround, RenderFinding.NoImagery, RenderFinding.NoDisplayType];
|
|
644
776
|
function isPovertyFinding(finding) {
|
|
645
777
|
return Boolean(finding) && POVERTY_FINDINGS.indexOf(finding.code) !== -1;
|
|
@@ -855,6 +987,43 @@ var require_src = __commonJS({
|
|
|
855
987
|
evidence: v.grounds.values
|
|
856
988
|
});
|
|
857
989
|
}
|
|
990
|
+
if (isDesktop && v.layout) {
|
|
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);
|
|
992
|
+
const seen2 = /* @__PURE__ */ new Set();
|
|
993
|
+
for (const g of gaps) {
|
|
994
|
+
const key = `${g.left}|${g.right}`;
|
|
995
|
+
if (seen2.has(key) || seen2.size >= 3) continue;
|
|
996
|
+
seen2.add(key);
|
|
997
|
+
add({
|
|
998
|
+
code: RenderFinding.RowGap,
|
|
999
|
+
severity: "warning",
|
|
1000
|
+
viewport: name,
|
|
1001
|
+
message: `"${g.left}" and "${g.right}" sit side by side with ${g.gap}px of empty space between them (${Math.round(g.gap / g.rowWidth * 100)}% of the ${g.rowWidth}px row). A label or number beside text reads as one thing at its own width with a small gap \u2014 a row Group, the label at its natural width and the text growing \u2014 not a column given a share of the page (Columns widths are proportions); two parts pushed to a wide row's edges leave the same hole: give the row a max width and a gap. Look at this width's screenshot.`,
|
|
1002
|
+
evidence: g
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const drifts = (v.layout.drifts || []).slice(0, 3);
|
|
1006
|
+
for (const d of drifts) {
|
|
1007
|
+
add({
|
|
1008
|
+
code: RenderFinding.AlignmentDrift,
|
|
1009
|
+
severity: "warning",
|
|
1010
|
+
viewport: name,
|
|
1011
|
+
message: `This section centres "${d.centredText}", but "${d.text}" sits ${Math.abs(d.offBy)}px ${d.offBy < 0 ? "left" : "right"} of that centre. One section, one alignment: centre the rest too, or align everything to the same left edge. Look at this width's screenshot.`,
|
|
1012
|
+
evidence: d
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
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
|
+
}
|
|
858
1027
|
if (v.consoleErrors && v.consoleErrors.length) {
|
|
859
1028
|
add({
|
|
860
1029
|
code: RenderFinding.ConsoleError,
|
|
@@ -964,6 +1133,7 @@ var require_render_report = __commonJS({
|
|
|
964
1133
|
var WS_MODULE = HARNESS_PATHS.WS_MODULE.path;
|
|
965
1134
|
var {
|
|
966
1135
|
DEFAULT_VIEWPORTS,
|
|
1136
|
+
NAMED_VIEWPORTS,
|
|
967
1137
|
DESKTOP_WIDTH,
|
|
968
1138
|
RenderFinding,
|
|
969
1139
|
SEVERITY_ORDER,
|
|
@@ -1499,7 +1669,7 @@ var require_render_report = __commonJS({
|
|
|
1499
1669
|
}
|
|
1500
1670
|
function parseViewports2(spec) {
|
|
1501
1671
|
if (!spec) return DEFAULT_VIEWPORTS;
|
|
1502
|
-
const known = new Map(
|
|
1672
|
+
const known = new Map(NAMED_VIEWPORTS.map((v) => [v.name, v]));
|
|
1503
1673
|
return spec.split(",").map((token) => {
|
|
1504
1674
|
const trimmed = token.trim();
|
|
1505
1675
|
if (known.has(trimmed)) return known.get(trimmed);
|