@bezda/rhp-mcp 0.1.6 → 0.1.8
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/package.json +2 -2
- package/skill/SKILL.md +108 -18
- package/skill/recipes/tape.html +578 -0
- package/skill/references/api.md +10 -8
- package/skill/references/design.md +18 -0
- package/skill/references/forms.md +3 -2
- package/skill/references/interaction.md +8 -4
- package/skill/references/performance.md +45 -0
- package/skill/references/pitfalls.md +11 -2
- package/skill/scripts/add-download.mjs +33 -0
- package/src/check/README.md +20 -3
- package/src/check/findings.js +109 -4
- package/src/check/index.js +158 -17
- package/src/check/inpage.js +329 -6
- package/src/check/interact.js +12 -0
- package/src/check/report.js +10 -3
- package/src/check/selection.js +16 -1
- package/src/check/source.js +2 -1
- package/src/guard/rules.js +2 -2
- package/src/server.js +4 -4
package/src/check/interact.js
CHANGED
|
@@ -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");
|
package/src/check/report.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
const PASSES = [
|
|
6
6
|
[["runtime-error", "console-error", "syntax-error", "missing-import", "missing-import-map", "plain-script-tag", "interaction-error", "missing-export"], "no errors thrown"],
|
|
7
7
|
[["rhp-warning"], "no rhp warnings"],
|
|
8
|
-
[["unknown-prop", "bad-prop", "bad-scale", "dot-size", "missing-group", "plot-typo", "group-lengths", "no-component", "bare-boolean", "handler-at-render", "style-string", "class-name", "two-solids", "series-past-theme", "page-css-ignored", "block-transition", "stale-read", "static-chart"], "rhp used as documented"],
|
|
8
|
+
[["unknown-prop", "bad-prop", "bad-scale", "dot-size", "missing-group", "plot-typo", "group-lengths", "no-component", "bare-boolean", "handler-at-render", "style-string", "class-name", "two-solids", "series-past-theme", "page-css-ignored", "page-css-reaches", "block-transition", "stale-read", "static-chart"], "rhp used as documented"],
|
|
9
9
|
[["no-chart", "empty-plot", "small-plot", "collapsed-chart", "plot-outside-chart"], null],
|
|
10
10
|
[["nan-value"], "no NaN values"],
|
|
11
11
|
[["value-past-scale", "points-past-scale"], "values inside the scale"],
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
}
|
package/src/check/selection.js
CHANGED
|
@@ -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 = !
|
|
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/check/source.js
CHANGED
|
@@ -13,10 +13,11 @@ import { closest } from "../guard/rules.js";
|
|
|
13
13
|
const STANDALONE = "@bezda/rhp/standalone";
|
|
14
14
|
|
|
15
15
|
// The props of rhp's components (and Solid's Show) that take a boolean, and what "" (false) does to each
|
|
16
|
-
const BOOLEANS = { Chart: ["animate", "static"], Plot: ["overlap", "static", "keyboard", "animate"], Area: ["mirror", "smooth"], Line: ["fill", "smooth"], Show: ["keyed"] };
|
|
16
|
+
const BOOLEANS = { Chart: ["animate", "static", "noCssGuard"], Plot: ["overlap", "static", "keyboard", "animate"], Area: ["mirror", "smooth"], Line: ["fill", "smooth"], Show: ["keyed"] };
|
|
17
17
|
const OFF = {
|
|
18
18
|
overlap: "its slats don't share one band",
|
|
19
19
|
static: "the chart is not static",
|
|
20
|
+
noCssGuard: "the chart keeps the guard",
|
|
20
21
|
keyboard: "its slats take no focus",
|
|
21
22
|
animate: "the JS version stays off",
|
|
22
23
|
mirror: "the Area is not mirrored",
|
package/src/guard/rules.js
CHANGED
|
@@ -25,7 +25,7 @@ export const SETTING_VALUE = {
|
|
|
25
25
|
children: (v) => typeof v === "function",
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export const CHART_PROPS = ["scale", "orientation", "height", "aspect", "ticks", "grid", "format", "animate", "theme", "static", "cross", "crossTicks", "crossGrid", "crossFormat", "label", "id", "class", "role", "style", "ref", "children"];
|
|
28
|
+
export const CHART_PROPS = ["scale", "orientation", "height", "aspect", "ticks", "grid", "format", "animate", "theme", "static", "noCssGuard", "cross", "crossTicks", "crossGrid", "crossFormat", "label", "id", "class", "role", "style", "ref", "children"];
|
|
29
29
|
|
|
30
30
|
export const THEME_KEYS = ["series", "positive", "negative", "ink", "muted", "grid", "surface", "low", "high", "font"];
|
|
31
31
|
|
|
@@ -58,7 +58,7 @@ export const RENAMES = {
|
|
|
58
58
|
Label: { value: "at", x: "at", y: "cross", position: "side", align: "side", text: "children", label: "children" },
|
|
59
59
|
Area: { data: "points", values: "points", max: "peak", fill: "color", colour: "color", curved: "smooth" },
|
|
60
60
|
Line: { data: "points", values: "points", max: "peak", curved: "smooth", area: "fill", colour: "color" },
|
|
61
|
-
Chart: { domain: "scale", range: "scale", extent: "scale", horizontal: "orientation", vertical: "orientation", direction: "orientation", colors: "theme", palette: "theme", tickFormat: "format", ariaLabel: "label", title: "label" },
|
|
61
|
+
Chart: { domain: "scale", range: "scale", extent: "scale", horizontal: "orientation", vertical: "orientation", direction: "orientation", colors: "theme", palette: "theme", tickFormat: "format", ariaLabel: "label", title: "label", noGuard: "noCssGuard", unguarded: "noCssGuard", noCSSGuard: "noCssGuard" },
|
|
62
62
|
Poster: { headline: "title", heading: "title", subtitle: "dek", subheading: "dek", subhead: "dek", description: "dek", lede: "dek", standfirst: "dek", source: "note", footnote: "note", credit: "note", credits: "note", caption: "note", footer: "note", eyebrow: "kicker", overline: "kicker", tag: "kicker" },
|
|
63
63
|
slat: { height: "thickness", width: "thickness", size: "thickness", thick: "thickness", pitch: "thickness", padding: "room", margin: "room", gutter: "room", gutters: "room", gap: "inset", style: "css", styles: "css", class: "css", className: "css" },
|
|
64
64
|
theme: { colors: "series", palette: "series", accent: "series", text: "ink", foreground: "ink", color: "ink", background: "surface", bg: "surface", paper: "surface", gridline: "grid", gridlines: "grid", axis: "grid", fontFamily: "font", family: "font", typeface: "font" },
|
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
|
|
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 (
|
|
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
|
|
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
|
}));
|