@bezda/rhp-mcp 0.1.6 → 0.1.7

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.
@@ -1188,8 +1188,56 @@ export function install() {
1188
1188
  while (a.parentElement && a.parentElement !== document.body && a.parentElement !== document.documentElement) a = a.parentElement;
1189
1189
  return a;
1190
1190
  }))].map((el) => ({ el, end: el.getBoundingClientRect().bottom + scrollY }));
1191
+ // the texts around the charts, each placed in the box it floats in (see words), and the boxes with a background or
1192
+ // a border around them that hold no chart (a readout's card, a legend's panel), with their size
1193
+ words = [];
1194
+ boxes = [];
1195
+ for (const { el: root } of sections) {
1196
+ for (const el of [root, ...root.querySelectorAll("*")]) {
1197
+ if (words.length >= 600) break;
1198
+ if (el.closest(".rhp-chart") || el.closest(CONTROLS) || !visible(el) || (viewRoot && !viewRoot.contains(el))) continue;
1199
+ const text = ownText(el);
1200
+ if (text && el.getBoundingClientRect().width > 0) {
1201
+ const frame = frameOf(el);
1202
+ words.push({ ...within(el, frame), text, frame });
1203
+ }
1204
+ if (boxes.length < 60 && framed(el) && !el.querySelector(".rhp-chart") && !/fixed|absolute/.test(getComputedStyle(el).position)) {
1205
+ const r = el.getBoundingClientRect();
1206
+ const scale = scaleOf(el);
1207
+ if (r.width >= 24 && r.height >= 16 && el.textContent.trim()) boxes.push({ el, w: r.width / scale, h: r.height / scale, name: snippet(seenText(el)) });
1208
+ }
1209
+ }
1210
+ }
1191
1211
  }
1192
1212
 
1213
+ // A text is placed in the box it floats in when it floats (fixed, sticky or absolute: a readout pinned to the
1214
+ // window, a tooltip that follows the pointer), else on the page: the box may move as a whole, and only a move inside
1215
+ // it counts. Its own text is its text nodes', not its children's, so a label beside a value that changed keeps its own.
1216
+ let words = [];
1217
+ let boxes = [];
1218
+ const frameOf = (el) => {
1219
+ for (let a = el; a && a.nodeType === 1 && a !== document.body; a = a.parentElement) {
1220
+ if (/fixed|sticky|absolute/.test(getComputedStyle(a).position)) return a;
1221
+ }
1222
+ return null;
1223
+ };
1224
+ const ownText = (el) => [...el.childNodes].filter((n) => n.nodeType === 3).map((n) => n.data).join(" ").replace(/\s+/g, " ").trim();
1225
+ const scaleOf = (el) => (viewRoot?.contains(el) ? viewRoot.getBoundingClientRect().width / viewRoot.offsetWidth : 1);
1226
+ const within = (el, frame) => {
1227
+ if (!frame) return at(el);
1228
+ const r = el.getBoundingClientRect();
1229
+ const f = frame.getBoundingClientRect();
1230
+ const scale = scaleOf(el);
1231
+ return { el, x: (r.left - f.left) / scale, y: (r.top - f.top) / scale };
1232
+ };
1233
+ // a box the reader sees the edges of: a background, a border or a shadow
1234
+ const framed = (el) => {
1235
+ const s = getComputedStyle(el);
1236
+ const paint = (c) => c && c !== "transparent" && !/rgba\([^)]*,\s*0(\.0+)?\)$/.test(c);
1237
+ const border = ["Top", "Right", "Bottom", "Left"].some((side) => parseFloat(s[`border${side}Width`]) >= 1 && paint(s[`border${side}Color`]) && s[`border${side}Style`] !== "none");
1238
+ return paint(s.backgroundColor) || border || s.boxShadow !== "none";
1239
+ };
1240
+
1193
1241
  function moved(n, lit = false) {
1194
1242
 
1195
1243
  const used = window.__rhpTargets[n];
@@ -1226,6 +1274,28 @@ export function install() {
1226
1274
  const grew = Math.round(el.getBoundingClientRect().bottom + scrollY - end);
1227
1275
  if (el.isConnected && far(grew)) out.push({ what: "the end of the chart's section", dx: 0, dy: grew });
1228
1276
  }
1277
+ // lighting a slat leaves a readout's card or a legend's panel its size: one that grows or shrinks with what it says
1278
+ // shows its edges jumping
1279
+ for (const b of lit ? boxes : []) {
1280
+ if (!b.el.isConnected || !visible(b.el)) continue;
1281
+ const r = b.el.getBoundingClientRect();
1282
+ const scale = scaleOf(b.el);
1283
+ const dw = Math.round(r.width / scale - b.w);
1284
+ const dh = Math.round(r.height / scale - b.h);
1285
+ if (far(dw) || far(dh)) out.push({ what: b.name ? `the box of "${b.name}"` : "a box", dx: 0, dy: 0, dw, dh, box: true });
1286
+ }
1287
+ // a text that still says the same thing stays where it was in its box: one that moves because a value beside it got
1288
+ // wider, or because a line above it wrapped, makes the reader lose their place (text-shift)
1289
+ for (const w of words) {
1290
+ if (!w.el.isConnected || !visible(w.el) || ownText(w.el) !== w.text || frameOf(w.el) !== w.frame) continue;
1291
+ const now = within(w.el, w.frame);
1292
+ const dx = Math.round(now.x - w.x);
1293
+ const dy = Math.round(now.y - w.y);
1294
+ // (a phrase of three words or more that only slides along its line, after a name or a value that changed, is a
1295
+ // sentence that reads on: "4.1% of people on Earth speak Hindi". A label of a word or two that slides is not)
1296
+ if (!far(dy) && (w.text.match(/[\p{L}\p{N}]+/gu) ?? []).length >= 3) continue;
1297
+ if (far(dx) || far(dy)) out.push({ what: `"${snippet(w.text)}"`, dx, dy, text: true });
1298
+ }
1229
1299
 
1230
1300
  return out;
1231
1301
  }
@@ -1340,5 +1410,210 @@ export function install() {
1340
1410
  return { what: focusName(el), clip: box };
1341
1411
  }
1342
1412
 
1343
- window.__rhpProbe = { settle, calm, resized: () => [resizes, mutations], measure, transitions, colorItems, hide, show, contentBox, targets, sweepTargets, breakpoints, pointAt, place, moved, record, stop, unseen, focused, focusStop };
1413
+ // Three pairs of neighboring slats of the main Plot (the first two, two in the middle, the last two), for glide() in
1414
+ // index.js
1415
+ function neighbors() {
1416
+
1417
+ const slats = mainSlats();
1418
+ if (slats.length < 3) return [];
1419
+ const list = (window.__rhpTargets ??= []);
1420
+ const one = (i) => ({ index: i + 1, of: slats.length, name: snippet([...slats[i].querySelectorAll(".rhp-label")].map(seenText).find((x) => x) ?? ""), target: list.push(slats[i]) - 1 });
1421
+
1422
+ return [...new Set([0, Math.floor((slats.length - 1) / 2), slats.length - 2])].map((i) => [one(i), one(i + 1)]);
1423
+ }
1424
+
1425
+ // The middle of a slat's largest mark (a target), in the window: where its own pick shows, for the glide. A slat with
1426
+ // marks of its own (a box and its outlier dots) may show several things; its largest mark shows the slat's own.
1427
+ function markCenter(n, scroll = true) {
1428
+
1429
+ const slat = window.__rhpTargets[n];
1430
+ if (scroll) slat.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
1431
+ const area = (r) => r.width * r.height;
1432
+ const mark = [...slat.querySelectorAll(MARKS)].filter(visible).map((m) => m.getBoundingClientRect()).filter((r) => area(r) > 0).sort((a, b) => area(b) - area(a))[0];
1433
+ if (!mark) return null;
1434
+
1435
+ return { x: mark.left + mark.width / 2, y: mark.top + mark.height / 2 };
1436
+ }
1437
+
1438
+ // What the reader sees of a pick, as two short hashes: the page's visible text, and how the main Plot's slats are
1439
+ // drawn (the classes and inline styles of everything in them)
1440
+ function state() {
1441
+
1442
+ const hash = (s) => {
1443
+ let h = 5381;
1444
+ for (let i = 0; i < s.length; i++) {
1445
+ h = (h * 33 + s.charCodeAt(i)) | 0;
1446
+ }
1447
+ return h;
1448
+ };
1449
+ const look = mainSlats().map((s) => [s, ...s.querySelectorAll("*")].map((e) => (e.getAttribute("class") ?? "") + (e.getAttribute("style") ?? "")).join("")).join("|");
1450
+
1451
+ return { text: hash(document.body.innerText), look: hash(look) };
1452
+ }
1453
+
1454
+ // The elements around the charts whose text a pick changes (the readout), noted while the sweep picks every slat
1455
+ let watcher = null;
1456
+ const readout = new Set();
1457
+ function watch(on) {
1458
+
1459
+ watcher?.disconnect();
1460
+ watcher = null;
1461
+ if (!on) return readout.size;
1462
+ watcher = new MutationObserver((list) => {
1463
+ for (const m of list) {
1464
+ const el = m.type === "characterData" ? m.target.parentElement : m.target;
1465
+ if (el?.nodeType === 1 && el !== document.body && !el.closest(".rhp-chart")) readout.add(el);
1466
+ }
1467
+ });
1468
+ watcher.observe(document.body, { subtree: true, childList: true, characterData: true });
1469
+ return 0;
1470
+ }
1471
+
1472
+ // While the main chart is taller than three quarters of the window (height: a phone's is shorter than the page's),
1473
+ // scrolled so that its middle is in the middle of the window: its controls (in its section, outside its slats, not
1474
+ // the poster's fit and download icons) and its readout (what the sweep's picks wrote), when all of them are out of
1475
+ // view. null when they are in view, or there are none.
1476
+ function scrollsAway(height = innerHeight) {
1477
+
1478
+ const area = (el) => el.getBoundingClientRect().width * el.getBoundingClientRect().height;
1479
+ const chart = all().filter((c) => visible(c) && !c.parentElement?.closest(".rhp-chart")).sort((a, b) => area(b) - area(a))[0];
1480
+ if (!chart || chart.getBoundingClientRect().height < height * 0.75) return null;
1481
+ let section = chart;
1482
+ while (section.parentElement && section.parentElement !== document.body && section.parentElement !== document.documentElement) section = section.parentElement;
1483
+ const utility = /\b(fit|download|save|export|png|svg|share|print|full ?screen|restore|enlarge|shrink)\b/i;
1484
+ const controls = [...section.querySelectorAll(CONTROLS)].filter((el) => visible(el) && !el.closest(".rhp-chart") && el.getBoundingClientRect().width > 0 && !utility.test(nameOf(el) || el.textContent));
1485
+ const said = [...readout].filter((el) => el.isConnected && visible(el) && section.contains(el) && seenText(el));
1486
+ if (!controls.length && !said.length) return null;
1487
+
1488
+ const y = scrollY;
1489
+ const r = chart.getBoundingClientRect();
1490
+ scrollTo({ left: scrollX, top: scrollY + r.top + r.height / 2 - height / 2, behavior: "instant" });
1491
+ const inView = (el) => {
1492
+ const b = el.getBoundingClientRect();
1493
+ return b.width * b.height > 0 && b.bottom > 0 && b.top < height && b.right > 0 && b.left < innerWidth;
1494
+ };
1495
+ const out = {
1496
+ controls: controls.some(inView) ? [] : controls.slice(0, 3).map((el) => focusName(el)),
1497
+ readout: said.length && !said.some(inView) ? snippet(seenText(said[0])) : null,
1498
+ };
1499
+ scrollTo({ left: scrollX, top: y, behavior: "instant" });
1500
+
1501
+ return out.controls.length || out.readout ? out : null;
1502
+ }
1503
+
1504
+ // The points the scrub moves the pointer through (see scrub() in index.js), when the reader moves along the chart:
1505
+ // the main chart draws a line or an area (read day by day along its scale), or force (the sweep cannot point at its
1506
+ // slats, or there are one or two). steps points through the middle of the plot, from just inside one end to just
1507
+ // inside the other, with the plot scrolled to the middle of the window: along the scale of the plot that draws the
1508
+ // line or the area, else along the main Plot's scale when it has one or two slats, else across its slats (columns
1509
+ // side by side, or rows one under the other). Points outside the window are left out. { points, target: the plot,
1510
+ // for moved() }
1511
+ function scrubPath(force = false, steps = 40) {
1512
+
1513
+ const slats = mainSlats();
1514
+ const main = slats[0]?.parentElement;
1515
+ const chart = main?.closest(".rhp-chart");
1516
+ const mark = chart ? [...chart.querySelectorAll(".rhp-line, .rhp-area")].find(visible) : null;
1517
+ if (!main || (!mark && !force)) return { points: [] };
1518
+ const plot = mark?.closest(".rhp-plot") ?? main;
1519
+ plot.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
1520
+ const r = plot.getBoundingClientRect();
1521
+ const vertical = plot.dataset.rhpO === "v";
1522
+ const across = mark || slats.length <= 2 ? !vertical : vertical;
1523
+ const points = [];
1524
+ for (let k = 0; k <= steps; k++) {
1525
+ const f = 0.02 + (0.96 * k) / steps;
1526
+ const p = across ? { x: r.left + r.width * f, y: r.top + r.height / 2 } : { x: r.left + r.width / 2, y: r.top + r.height * f };
1527
+ if (p.x >= 0 && p.y >= 0 && p.x < innerWidth && p.y < innerHeight) points.push(p);
1528
+ }
1529
+
1530
+ return { points, target: (window.__rhpTargets ??= []).push(plot) - 1 };
1531
+ }
1532
+
1533
+ // The lines of text around the charts (outside them and their controls), for the scrub: each text node's first line
1534
+ // box, in the box it floats in (see frameOf), keyed by where the node sits in the page so that one written again
1535
+ // counts as the same. placeLines() notes them before the pointer moves; slides() then says which moved: a text that
1536
+ // says the same thing and moved, or one whose words changed and that kept none of its edges (its start, its end or,
1537
+ // on one line, its middle) where it was. A number at the start of its line may grow; a label after it that slides as
1538
+ // it grows jitters under the reader's eyes at every step, and so does a whole sentence, on a chart whose readout
1539
+ // changes at every step (a line read day by day). Over separate marks the readout changes once per mark, as a pick
1540
+ // does, and only what a pick counts counts (see moved()): what is strict is left out there, a slide along its line of
1541
+ // a text whose words changed, or of a sentence of three words or more. A text that moves up or down jumps, whatever
1542
+ // it says and however often the readout changes. A text first seen after placeLines() is measured against where it
1543
+ // was first seen.
1544
+ let lines = new Map();
1545
+ const keyOf = (node) => {
1546
+ const path = [];
1547
+ for (let n = node; n && n !== document.body && n.parentNode; n = n.parentNode) path.push([...n.parentNode.childNodes].indexOf(n));
1548
+ return path.join(".");
1549
+ };
1550
+ const textNodes = () => {
1551
+ const out = [];
1552
+ for (const { el: root } of sections) {
1553
+ if (!root.isConnected) continue;
1554
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
1555
+ for (let node = walker.nextNode(); node && out.length < 800; node = walker.nextNode()) {
1556
+ const el = node.parentElement;
1557
+ if (!el || !node.data.trim() || el.closest(".rhp-chart") || el.closest(CONTROLS) || !visible(el) || (viewRoot && !viewRoot.contains(el))) continue;
1558
+ out.push(node);
1559
+ }
1560
+ }
1561
+ return out;
1562
+ };
1563
+ const lineOf = (node) => {
1564
+ const range = document.createRange();
1565
+ range.selectNodeContents(node);
1566
+ const rects = [...range.getClientRects()].filter((q) => q.width > 0 && q.height > 0);
1567
+ if (!rects.length) return null;
1568
+ const el = node.parentElement;
1569
+ const frame = frameOf(el);
1570
+ const f = frame ? frame.getBoundingClientRect() : { left: -scrollX, top: -scrollY };
1571
+ const scale = scaleOf(el);
1572
+ const left = (rects[0].left - f.left) / scale;
1573
+ const right = (rects.at(-1).right - f.left) / scale;
1574
+ // (the line it is part of: its nearest block, which a finding names when it holds more than this text)
1575
+ let block = el;
1576
+ while (block.parentElement && /^(inline|contents)$/.test(getComputedStyle(block).display)) block = block.parentElement;
1577
+ return { text: node.data.replace(/\s+/g, " ").trim(), frame, left, right, mid: (left + right) / 2, top: (rects[0].top - f.top) / scale, one: rects.length === 1, line: snippet(seenText(block)) };
1578
+ };
1579
+ function placeLines() {
1580
+
1581
+ lines = new Map();
1582
+ for (const node of textNodes()) {
1583
+ const line = lineOf(node);
1584
+ if (line) lines.set(keyOf(node), line);
1585
+ }
1586
+ return lines.size;
1587
+ }
1588
+ function slides() {
1589
+
1590
+ const out = [];
1591
+ for (const node of textNodes()) {
1592
+ const now = lineOf(node);
1593
+ if (!now) continue;
1594
+ const key = keyOf(node);
1595
+ const was = lines.get(key);
1596
+ if (!was) {
1597
+ lines.set(key, now);
1598
+ continue;
1599
+ }
1600
+ if (was.frame !== now.frame) continue;
1601
+ const by = (k) => Math.round(now[k] - was[k]);
1602
+ const dy = by("top");
1603
+ let dx = by("left");
1604
+ // (a line that moves up or down jumps whatever it says: only a slide along the line can be strict)
1605
+ const strict = Math.abs(dy) <= 2 && (now.text !== was.text || (was.text.match(/[\p{L}\p{N}]+/gu) ?? []).length >= 3);
1606
+ if (now.text !== was.text) {
1607
+ const edges = [by("left"), by("right"), ...(now.one && was.one ? [by("mid")] : [])];
1608
+ dx = edges.some((v) => Math.abs(v) <= 2) ? 0 : edges.reduce((a, b) => (Math.abs(b) < Math.abs(a) ? b : a));
1609
+ }
1610
+ // (a comma or a dot moves with the words around it, and is not named apart from them)
1611
+ if ((Math.abs(dx) > 2 || Math.abs(dy) > 2) && /[\p{L}\p{N}]/u.test(was.text)) out.push({ what: `"${snippet(was.text)}"`, line: was.line !== snippet(was.text) ? was.line : null, dx, dy, text: true, strict });
1612
+ if (out.length >= 40) break;
1613
+ }
1614
+
1615
+ return out;
1616
+ }
1617
+
1618
+ window.__rhpProbe = { settle, calm, resized: () => [resizes, mutations], measure, transitions, colorItems, hide, show, contentBox, targets, sweepTargets, breakpoints, pointAt, place, moved, record, stop, unseen, focused, focusStop, neighbors, markCenter, state, watch, scrollsAway, scrubPath, placeLines, slides };
1344
1619
  }
@@ -94,9 +94,21 @@ export async function interact(page, { events, capture, shoot, taps = false }) {
94
94
  window.__rhpProbe.record(keep);
95
95
  }, {keep:texts,view});
96
96
  if (!error) await attempt(act);
97
+ // a control that sets the page moving (Play) is read as it moves too: a text that jitters while the values run
98
+ // may be back in place at the end
99
+ const passing = [];
100
+ for (let k = 0; !lit && !error && k < 5; k++) {
101
+ await page.waitForTimeout(200);
102
+ passing.push(...(await page.evaluate((n) => window.__rhpProbe.moved(n, false), used).catch(() => [])).filter((m) => m.text));
103
+ }
97
104
  await page.evaluate(() => window.__rhpProbe.settle(1500)).catch(() => {});
98
105
  const seen = await page.evaluate(() => window.__rhpProbe.stop()).catch(() => null);
99
106
  const moved = await page.evaluate(([n, l]) => window.__rhpProbe.moved(n, l), [used, lit]).catch(() => []);
107
+ for (const m of passing) {
108
+ const was = moved.find((x) => x.what === m.what);
109
+ if (!was) moved.push(m);
110
+ else if (Math.abs(m.dx) + Math.abs(m.dy) > Math.abs(was.dx) + Math.abs(was.dy)) Object.assign(was, m);
111
+ }
100
112
  // a value a slat read once, and that this interaction changed, is shown stale (the guard reads them again)
101
113
  await page.evaluate(() => globalThis.__rhpCheck?.recheck?.()).catch(() => {});
102
114
  const thrown = events.slice(before).filter((e) => e.kind === "pageerror" || e.kind === "console-error");
@@ -44,6 +44,7 @@ export function report(r) {
44
44
  list.forEach((f, i) => {
45
45
  lines.push(`${i + 1}. [${f.code}] ${f.message}${widthsOf(f)}`);
46
46
  if (f.fix) lines.push(` Fix: ${f.fix}`);
47
+ if (f.see) lines.push(` See: ${f.see}`);
47
48
  });
48
49
  };
49
50
 
@@ -88,9 +89,15 @@ export function report(r) {
88
89
  if (quiet.includes(x.kind)) continue;
89
90
  lines.push(`- ${x.kind} ${x.target}${x.width !== r.interactedAt ? ` at ${x.width}px` : ""}: ${x.how}${x.error ? `, and it threw: ${x.error}` : ""}`);
90
91
  }
91
- // the sweep: one line, every width at which every slat was picked in turn
92
+ // the sweep: a line for the pointer moved along the chart, where it was, and one for every slat picked in turn, at
93
+ // every width at which it was (none when no slat could be reached: nothing was picked)
92
94
  const swept = (r.swept ?? []).filter((x) => x.slats);
93
- if (swept.length) lines.push(`- every slat picked in turn (${swept[0].slats}) at ${swept.map((x) => `${x.width}px`).join(", ")}: ${swept.some((x) => x.jumps) ? "the layout jumped (see the warnings)" : "nothing else moved"}`);
95
+ const at = (list) => list.map((x) => `${x.width}px`).join(", ");
96
+ const scrubbed = swept.filter((x) => x.scrubbed);
97
+ if (scrubbed.length) lines.push(`- pointer moved along the chart (${scrubbed[0].scrubbed.points} steps) at ${at(scrubbed)}: ${scrubbed.some((x) => x.scrubbed.moved) ? "the layout or the readout moved (see the warnings)" : "nothing else moved"}`);
98
+ const picked = swept.filter((x) => x.reached ?? x.slats);
99
+ if (picked.length) lines.push(`- every slat picked in turn (${picked[0].reached < picked[0].slats ? `${picked[0].reached} of ${picked[0].slats} reached` : picked[0].slats}) at ${at(picked)}: ${picked.some((x) => x.jumps) ? "the layout jumped (see the warnings)" : "nothing else moved"}`);
100
+ else if (swept.length) lines.push(`- no slat picked in turn: something covers the slats' marks at ${at(swept)}`);
94
101
  } else if (r.charts?.length && r.interactedAt) {
95
102
  lines.push("", "Interactions: nothing to interact with (no slats to point at, no buttons, ranges or selects).");
96
103
  }
@@ -77,6 +77,21 @@ export async function selectionBehavior(page, { events }) {
77
77
  const controlled = await controlSelection(page, { slats, off, look, away, settle });
78
78
  findings.push(...controlled.findings);
79
79
  if (controlled.tested.length) audit.tested = { controls: controlled.tested };
80
+ // The control audit leaves focus on the last control it tried. Its focus ring is not the chart's state,
81
+ // and a click beside the chart takes it away, so the reset would differ from the rest by that ring alone.
82
+ await page.evaluate(() => document.activeElement?.blur?.());
83
+ // The reset is the chart's state and its readout, not the controls' own look: a control keeps a focus ring, a hover
84
+ // or a pressed state of its own after the clicks, so the reset leaves out the controls' boxes (as the control audit
85
+ // does), with room for a ring around them
86
+ const controls = await page.evaluate((target) => {
87
+ const poster = window.__rhpTargets[target]?.closest(".poster, figure") ?? document.body;
88
+ return [...poster.querySelectorAll('button, input, select, textarea, summary, [role="button"], [role="slider"], [role="switch"], [role="tab"]')]
89
+ .filter((el) => !el.closest(".rhp-chart"))
90
+ .map((el) => el.getBoundingClientRect())
91
+ .filter((r) => r.width > 0 && r.height > 0)
92
+ .map((r) => ({ left: r.left - 8, right: r.right + 8, top: r.top - 8, bottom: r.bottom + 8 }));
93
+ }, slats[0].target);
94
+ const quiet = (a, b) => seenPixels(a, b, controls) < SAME;
80
95
  if (slats.length < 2) return audit;
81
96
  const candidates = [];
82
97
  for (const s of slats) {
@@ -128,7 +143,7 @@ export async function selectionBehavior(page, { events }) {
128
143
  await page.mouse.click(off.x, off.y);
129
144
  await away();
130
145
  reset = await look();
131
- resetBroken = !same(rest, reset);
146
+ resetBroken = !quiet(rest, reset);
132
147
  }
133
148
  const end = await page.evaluate(() => [scrollX, scrollY]);
134
149
  if (position.some((v, i) => v !== end[i]) || !same(reset, await look())) return audit;
package/src/server.js CHANGED
@@ -21,7 +21,7 @@ const LIMIT = 50;
21
21
  const MAX_WIDTH = 1280;
22
22
  const MAX_HEIGHT = 2000;
23
23
 
24
- const INSTRUCTIONS = `rhp 2 (@bezda/rhp) makes charts out of HTML elements and CSS with SolidJS. It came out in September 2026, after your training data, so what you remember about rhp is wrong or belongs to the unrelated rhp 1. Before you write or change any rhp code, call rhp_guide and follow its workflow. Take each chart's technique from the closest recipe (rhp_recipe) and design the chart for its own subject, and look up every prop you are unsure of with rhp_reference instead of guessing. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`;
24
+ const INSTRUCTIONS = `rhp 2 (@bezda/rhp) makes charts out of HTML elements and CSS with SolidJS. It came out in September 2026, after your training data, so what you remember about rhp is wrong or belongs to the unrelated rhp 1. Before you write or change any rhp code, call rhp_guide and follow its workflow. Take each chart's technique from the closest recipe (rhp_recipe) and design the chart for its own subject. Choose interaction behavior and presentation for the reader's task and the intended design; familiar patterns are welcome when they fit. Look up every prop you are unsure of with rhp_reference instead of guessing. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`;
25
25
 
26
26
  const text = (body) => ({ content: [{ type: "text", text: body }] });
27
27
  const failure = (body) => ({ content: [{ type: "text", text: body }], isError: true });
@@ -185,7 +185,7 @@ server.registerTool("rhp_guide", {
185
185
 
186
186
  server.registerTool("rhp_reference", {
187
187
  title: "rhp reference",
188
- description: "Returns one of rhp's references by name: api (every component, prop, helper and CSS variable, exactly), design (art direction from the subject, the editorial poster, color, type, layout), looks (eight looks written out in full, as examples), interaction (which interaction for which story, with code), download (the fit-to-screen and PNG download icons for standalone poster pages), environments (plain HTML, Solid, React, Next.js, Vue, Svelte, Angular, Astro), forms (the subject's own form, which chart for which data and story), pitfalls (mistakes and their fixes). Look props up here instead of guessing. A long reference comes back as its list of sections: call again with section to get the one you need.",
188
+ description: "Returns one of rhp's references by name: api (every component, prop, helper and CSS variable, exactly), design (art direction from the subject, the editorial poster, color, type, layout), looks (eight looks written out in full, as examples), interaction (choosing behavior and presentation for the reader's task, with implementation examples), download (the fit-to-screen and PNG download icons for standalone poster pages), environments (plain HTML, Solid, React, Next.js, Vue, Svelte, Angular, Astro), forms (the subject's own form, which chart for which data and story), pitfalls (mistakes and their fixes). Look props up here instead of guessing. A long reference comes back as its list of sections: call again with section to get the one you need.",
189
189
  inputSchema: {
190
190
  name: z.string().describe("The reference: api, design, environments, forms, interaction, looks or pitfalls"),
191
191
  section: z.string().optional().describe('One "##" section of the reference, by its number or title, for example "4" or "Plot"'),
@@ -200,7 +200,7 @@ server.registerTool("rhp_reference", {
200
200
 
201
201
  server.registerTool("rhp_recipe", {
202
202
  title: "rhp recipe",
203
- description: 'Lists rhp\'s recipes (call it with no type), or returns one recipe\'s complete HTML file (type, for example "bar"). A recipe is a tested chart page that shows the technique for one kind of chart: take its technique (how its Plots, slats and blocks are composed, its interaction, its phone rules) and leave its design, which was made for its own subject.',
203
+ description: 'Lists rhp\'s recipes (call it with no type), or returns one recipe\'s complete HTML file (type, for example "bar"). A recipe is a tested chart page demonstrating composition, state and event handling, accessibility and responsive techniques. Choose the chart\'s interaction behavior and presentation for the reader\'s task and intended design. Familiar patterns are welcome when they fit; the recipe\'s controls, feedback, layout and transitions are choices to reconsider.',
204
204
  inputSchema: {
205
205
  type: z.string().optional().describe('The recipe, for example "bar", "line" or "donut"; leave it out for the list'),
206
206
  },
@@ -239,7 +239,7 @@ server.registerPrompt("chart", {
239
239
  role: "user",
240
240
  content: {
241
241
  type: "text",
242
- text: `Make this chart with rhp (@bezda/rhp): ${request}\n\nFirst call rhp_guide and follow its workflow: write the brief, take the technique from the closest recipe (rhp_recipe), design the chart for its own subject, and look up props with rhp_reference instead of guessing. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`,
242
+ text: `Make this chart with rhp (@bezda/rhp): ${request}\n\nFirst call rhp_guide and follow its workflow: write the brief, take the technique from the closest recipe (rhp_recipe), design the chart for its own subject, and look up props with rhp_reference instead of guessing. Choose interaction behavior and presentation for the reader's task and intended design, keeping familiar patterns when they fit. After every edit, call rhp_check with the chart file's absolute path, fix what it reports and look at its screenshots, until it reports no errors and no warnings.`,
243
243
  },
244
244
  }],
245
245
  }));