@bezda/rhp-mcp 0.1.5 → 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.
Files changed (57) hide show
  1. package/package.json +6 -1
  2. package/skill/SKILL.md +113 -22
  3. package/skill/recipes/area.html +129 -33
  4. package/skill/recipes/bar.html +129 -33
  5. package/skill/recipes/box-plot.html +129 -33
  6. package/skill/recipes/bubble.html +129 -33
  7. package/skill/recipes/bullet.html +129 -33
  8. package/skill/recipes/candlestick.html +129 -33
  9. package/skill/recipes/column.html +129 -33
  10. package/skill/recipes/dial.html +129 -33
  11. package/skill/recipes/diverging-bars.html +129 -33
  12. package/skill/recipes/donut.html +129 -33
  13. package/skill/recipes/dumbbell.html +129 -33
  14. package/skill/recipes/gantt.html +129 -33
  15. package/skill/recipes/grouped-bars.html +129 -33
  16. package/skill/recipes/heatmap.html +129 -33
  17. package/skill/recipes/histogram.html +129 -33
  18. package/skill/recipes/line.html +129 -33
  19. package/skill/recipes/live.html +129 -33
  20. package/skill/recipes/log-line.html +129 -33
  21. package/skill/recipes/lollipop.html +129 -33
  22. package/skill/recipes/multi-line.html +129 -33
  23. package/skill/recipes/pyramid.html +129 -33
  24. package/skill/recipes/race.html +129 -33
  25. package/skill/recipes/radial-bars.html +129 -33
  26. package/skill/recipes/scatter.html +129 -33
  27. package/skill/recipes/silhouettes.html +129 -33
  28. package/skill/recipes/slope.html +129 -33
  29. package/skill/recipes/sparklines.html +129 -33
  30. package/skill/recipes/stacked-100.html +129 -33
  31. package/skill/recipes/stacked-bars.html +129 -33
  32. package/skill/recipes/strip.html +129 -33
  33. package/skill/recipes/tape.html +578 -0
  34. package/skill/recipes/unit-stack.html +129 -33
  35. package/skill/recipes/violin.html +129 -33
  36. package/skill/recipes/waffle.html +129 -33
  37. package/skill/recipes/waterfall.html +129 -33
  38. package/skill/references/api.md +6 -6
  39. package/skill/references/design.md +18 -0
  40. package/skill/references/download.md +135 -35
  41. package/skill/references/forms.md +3 -2
  42. package/skill/references/interaction.md +8 -4
  43. package/skill/references/pitfalls.md +3 -2
  44. package/skill/scripts/add-download.mjs +33 -0
  45. package/src/check/README.md +29 -5
  46. package/src/check/build.js +4 -3
  47. package/src/check/colors.js +4 -2
  48. package/src/check/findings.js +99 -3
  49. package/src/check/index.js +158 -17
  50. package/src/check/inpage.js +292 -5
  51. package/src/check/interact.js +22 -4
  52. package/src/check/png.js +6 -1
  53. package/src/check/report.js +9 -2
  54. package/src/check/selection-controls.js +105 -0
  55. package/src/check/selection.js +54 -12
  56. package/src/check/source.js +21 -0
  57. package/src/server.js +4 -4
@@ -1,7 +1,8 @@
1
- // A conditional audit of an existing click selection with a temporary hover override.
2
- // No click selection or no temporary hover means there is no interaction policy to repair here.
1
+ // Conditional audits of existing selection: a control selecting chart data, or click selection with temporary hover.
2
+ // The observed behavior establishes which interaction policy applies; unrelated controls do not change it.
3
3
  // Run on a disposable page: clicks must not change the starting state of the normal interaction pass.
4
4
  import { seenPixels } from "./png.js";
5
+ import { controlSelection } from "./selection-controls.js";
5
6
 
6
7
  const SAME = 20;
7
8
  const same = (a, b) => seenPixels(a, b) < SAME;
@@ -40,7 +41,22 @@ export async function selectionBehavior(page, { events }) {
40
41
  };
41
42
  const look = async () => {
42
43
  await settle();
43
- return page.screenshot({ animations: "disabled", scale: "css" });
44
+ // Decorative canvas pixels have their own clock, unrelated to chart selection.
45
+ // Hide only inert backgrounds behind the content, never a chart or its marks.
46
+ const backgrounds = page.locator('canvas[aria-hidden="true"]');
47
+ await backgrounds.evaluateAll((canvases) => {
48
+ for (const canvas of canvases) {
49
+ const css = getComputedStyle(canvas);
50
+ if (!canvas.closest(".rhp-chart") && Number(css.zIndex) < 0 && css.pointerEvents === "none") {
51
+ canvas.setAttribute("data-rhp-selection-background", "");
52
+ }
53
+ }
54
+ });
55
+ try {
56
+ return await page.screenshot({ animations: "disabled", scale: "css", style: "canvas[data-rhp-selection-background] { visibility: hidden !important; }" });
57
+ } finally {
58
+ await backgrounds.evaluateAll((canvases) => canvases.forEach((canvas) => canvas.removeAttribute("data-rhp-selection-background")));
59
+ }
44
60
  };
45
61
  const away = async () => {
46
62
  await page.mouse.move(-1, -1);
@@ -51,10 +67,32 @@ export async function selectionBehavior(page, { events }) {
51
67
  // Use reachable marks, including marks inside a slat, from the main Plot.
52
68
  // Keep the window still throughout the sequence so scrolling and sticky readouts cannot mimic a state change.
53
69
  const slats = await page.evaluate(() => window.__rhpProbe.sweepTargets(12));
54
- if (slats.length < 2) return audit;
70
+ if (!slats.length) return audit;
55
71
  await page.evaluate((n) => window.__rhpProbe.pointAt(n), slats[0].target);
56
72
  await away();
57
73
  const position = await page.evaluate(() => [scrollX, scrollY]);
74
+ const off = await page.evaluate(beside, slats[0].target);
75
+ if (!off) return audit;
76
+ doing = "checking selection changed by a control";
77
+ const controlled = await controlSelection(page, { slats, off, look, away, settle });
78
+ findings.push(...controlled.findings);
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;
95
+ if (slats.length < 2) return audit;
58
96
  const candidates = [];
59
97
  for (const s of slats) {
60
98
  const point = await pointAt(s);
@@ -64,8 +102,6 @@ export async function selectionBehavior(page, { events }) {
64
102
  if (candidates.length === 3) break;
65
103
  }
66
104
  if (candidates.length < 2) return audit;
67
- const off = await page.evaluate(beside, candidates[0].target);
68
- if (!off) return audit;
69
105
  const rest = await look();
70
106
  if (!same(rest, await look())) return audit;
71
107
  // At most three candidates, so an initial meaningful default on the first mark does not hide click selection.
@@ -98,14 +134,20 @@ export async function selectionBehavior(page, { events }) {
98
134
  const selected = await look();
99
135
  const hoverBroken = !same(selected, offChart);
100
136
  // This click is inside the poster, outside every chart and control.
101
- doing = `click beside the chart after selecting ${name(chosen)}`;
102
- await page.mouse.click(off.x, off.y);
103
- await away();
104
- const reset = await look();
105
- const resetBroken = !same(rest, reset);
137
+ let reset = selected;
138
+ let resetBroken = false;
139
+ // A proven selection control establishes a persistent choice, rather than the page's initial default.
140
+ // The control audit already checks that a background click leaves that choice intact.
141
+ if (!controlled.tested.length) {
142
+ doing = `click beside the chart after selecting ${name(chosen)}`;
143
+ await page.mouse.click(off.x, off.y);
144
+ await away();
145
+ reset = await look();
146
+ resetBroken = !quiet(rest, reset);
147
+ }
106
148
  const end = await page.evaluate(() => [scrollX, scrollY]);
107
149
  if (position.some((v, i) => v !== end[i]) || !same(reset, await look())) return audit;
108
- audit.tested = { selected: name(chosen), preview: name(preview), hoverRestoresSelection: !hoverBroken, clickRestoresInitial: !resetBroken };
150
+ audit.tested = { ...audit.tested, selected: name(chosen), preview: name(preview), hoverRestoresSelection: !hoverBroken, ...(!controlled.tested.length ? { clickRestoresInitial: !resetBroken } : {}) };
109
151
  if (hoverBroken) findings.push({ code: "selection-hover-stays", target: name(preview), selected: name(chosen), what: off.what });
110
152
  if (resetBroken) findings.push({ code: "selection-reset", target: name(chosen), what: off.what });
111
153
  return audit;
@@ -74,6 +74,27 @@ function walk(node, visit) {
74
74
  }
75
75
  }
76
76
 
77
+ // Literal module dependencies, excluding strings, comments and object properties that look like imports.
78
+ export function moduleSpecifiers(code, file) {
79
+
80
+ let ast;
81
+ try {
82
+ ast = parseSync(code, { sourceType: "module", configFile: false, babelrc: false, filename: file });
83
+ } catch {
84
+ return []; // The caller reports syntax errors separately.
85
+ }
86
+ const imports = [];
87
+ walk(ast.program, (node) => {
88
+ const source = /^(ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration|ImportExpression)$/.test(node.type)
89
+ ? node.source
90
+ : node.type === "CallExpression" && node.callee.type === "Import" ? node.arguments[0] : null;
91
+ const value = source?.type === "StringLiteral" ? source.value
92
+ : source?.type === "TemplateLiteral" && source.expressions.length === 0 ? source.quasis[0].value.cooked : null;
93
+ if (value != null) imports.push({ source: value, line: node.loc.start.line });
94
+ });
95
+ return imports;
96
+ }
97
+
77
98
  const isSpace = (c) => c === " " || c === "\n" || c === "\t" || c === "\r" || c === "\f";
78
99
 
79
100
  // The attributes in an html template, in order: { tag, name, value, at } with tag { component, expr, name }, value
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 PNG download icon 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
  }));