@bezda/rhp-mcp 0.1.0 → 0.1.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.
Files changed (59) hide show
  1. package/README.md +20 -5
  2. package/bin/rhp-mcp.js +5 -1
  3. package/package.json +3 -3
  4. package/skill/SKILL.md +116 -26
  5. package/skill/recipes/area.html +6 -0
  6. package/skill/recipes/bar.html +9 -1
  7. package/skill/recipes/box-plot.html +9 -1
  8. package/skill/recipes/bubble.html +6 -0
  9. package/skill/recipes/bullet.html +6 -0
  10. package/skill/recipes/candlestick.html +9 -1
  11. package/skill/recipes/column.html +18 -5
  12. package/skill/recipes/dial.html +349 -0
  13. package/skill/recipes/diverging-bars.html +6 -0
  14. package/skill/recipes/donut.html +9 -1
  15. package/skill/recipes/dumbbell.html +6 -0
  16. package/skill/recipes/gantt.html +17 -5
  17. package/skill/recipes/grouped-bars.html +6 -0
  18. package/skill/recipes/heatmap.html +9 -1
  19. package/skill/recipes/histogram.html +9 -1
  20. package/skill/recipes/line.html +7 -1
  21. package/skill/recipes/live.html +6 -0
  22. package/skill/recipes/log-line.html +493 -0
  23. package/skill/recipes/lollipop.html +6 -0
  24. package/skill/recipes/multi-line.html +10 -5
  25. package/skill/recipes/pyramid.html +9 -1
  26. package/skill/recipes/race.html +6 -0
  27. package/skill/recipes/radial-bars.html +6 -0
  28. package/skill/recipes/scatter.html +23 -4
  29. package/skill/recipes/silhouettes.html +354 -0
  30. package/skill/recipes/slope.html +23 -4
  31. package/skill/recipes/sparklines.html +7 -1
  32. package/skill/recipes/stacked-100.html +6 -0
  33. package/skill/recipes/stacked-bars.html +6 -0
  34. package/skill/recipes/strip.html +9 -1
  35. package/skill/recipes/unit-stack.html +276 -0
  36. package/skill/recipes/violin.html +6 -0
  37. package/skill/recipes/waffle.html +6 -0
  38. package/skill/recipes/waterfall.html +6 -0
  39. package/skill/references/api.md +150 -22
  40. package/skill/references/design.md +303 -326
  41. package/skill/references/environments.md +11 -4
  42. package/skill/references/forms.md +41 -4
  43. package/skill/references/interaction.md +7 -2
  44. package/skill/references/looks.md +267 -0
  45. package/skill/references/performance.md +143 -0
  46. package/skill/references/pitfalls.md +52 -7
  47. package/src/check/README.md +33 -3
  48. package/src/check/browser.js +11 -5
  49. package/src/check/findings.js +15 -1
  50. package/src/check/index.js +182 -31
  51. package/src/check/inpage.js +82 -18
  52. package/src/check/interact.js +1 -19
  53. package/src/check/network.js +168 -0
  54. package/src/check/png.js +18 -0
  55. package/src/check/report.js +4 -1
  56. package/src/check/selection.js +123 -0
  57. package/src/guard/rhp.js +4 -0
  58. package/src/guard/rules.js +1 -1
  59. package/src/server.js +9 -8
@@ -14,6 +14,7 @@ Every cause here was reproduced with rhp 2.0.1; the numbers in brackets point to
14
14
  - Values never update, or the chart jumps
15
15
  - A click, hover, tap or key does nothing, or the wrong thing
16
16
  - The layout jumps when the reader interacts
17
+ - Text and layers laid over the chart
17
18
  - Styles or colors do not apply
18
19
  - Console warnings rhp prints
19
20
  - Mistakes in the work itself
@@ -50,9 +51,12 @@ Start from a recipe, never from what another library does.
50
51
  5. **NaN, undefined or null** draw at the scale's start and print "NaN" [16]: clean or filter the data first.
51
52
  6. **Every slat got one item of a list meant for all of them**: an array given to a Plot is a column, item i for row i. To give every slat the same list (the series still shown, a set of picked names), pass a function of the row: `shown=${(d) => shown()}`.
52
53
  7. **Lists of different lengths** wrap around without an error [8]: the third slat of a 3-item list shown beside a 5-item list repeats the first values. Give every data group the same length.
53
- 8. **A Dot is an oval**: its `size` was a number [17]. Give a length, `size="12px"`.
54
+ 8. **A Dot is an oval**: its `size` was a number [17].
55
+ Give a length, `size="12px"`.
56
+ `ManyDots` has a different size contract: its numbers mean pixels, so `size=${12}` is a 12px point.
54
57
  9. **A vertical Plot over a horizontal chart draws 0px-tall columns**: in rhp 2.0.1, a second top-level Plot with `orientation="vertical"` in a horizontal Chart whose slat type has `room: "auto"` gets columns as wide as the chart and no height. Give the slats room in px, or put the Plot in a chart with `cross` (as the line recipe does).
55
- 10. **A scatter or line chart puts everything on one line**: the Chart needs `cross=${[min, max]}`, the Plot needs `overlap=${true}`, and each Dot needs `cross=${() => d.y}`.
58
+ 10. **A scatter or line chart puts everything on one line**: with Dot slats, the Chart needs `cross=${[min, max]}`, the Plot needs `overlap=${true}`, and each Dot needs `cross=${() => d.y}`.
59
+ The experimental [ManyDots collection](api.md#manydots-experimental-point-collection) goes directly inside a Chart with `cross` and takes its own `rows`, `at`, and `cross` accessors.
56
60
 
57
61
  ## Text sticks out, is cut off or overlaps
58
62
 
@@ -67,6 +71,12 @@ Start from a recipe, never from what another library does.
67
71
  9. **A value Label over the last column of a vertical chart sticks out**: give `room.after` half the label's width.
68
72
  10. **Text overlaps on a phone only**: test at 390px. Use a media query or a container query on the page for the poster, and `:vertical` / `:horizontal` rules for the slat.
69
73
  11. **Two words run together in a readout** ("All regions12") [35]: Solid's html template drops a space that stands alone between two tags. Write it as `${" "}`.
74
+ 12. **The first or last name under a column sticks out of the chart**: a label centered under its column with `translate: -50%` runs past the plot's ends.
75
+ Align the first one's start and the last one's end with the plot, or give `room.before` and `room.after` half its width.
76
+ 13. **Axis numbers run together at 320px**: the names' and the values' room leave a narrow plot.
77
+ Give the Chart fewer ticks when the window is narrow (`ticks=${() => (narrow() ? every(10) : every(5))}`, with `narrow` a signal from `matchMedia("(max-width: 400px)")`), shorter numbers, or less room.
78
+ 14. **Small parts lose their names**: hiding a label that does not fit is never the only fallback.
79
+ Put it outside its mark with a short leader, or in a key beside the chart in the marks' order; on a phone use short codes before dropping names (design.md section 7).
70
80
 
71
81
  ## The chart is tiny, squashed or the wrong size
72
82
 
@@ -83,9 +93,12 @@ Start from a recipe, never from what another library does.
83
93
  4. **Slats swap data when rows are removed**: give the Plot a `key` (`key="city"`).
84
94
  5. **Blocks jump to new values instead of moving** [22]: a `transition` on a block replaces rhp's own. Put your transition on an element inside the block.
85
95
  6. **Numbers show long decimals while they move** [26]: in the JS version (`animate`) values are fractional mid-way; round what you print.
86
- 7. **`animate` is on, but a value still jumps**: the JS version never moves a group given as a function of the row (`v=${(d) => ...}`). Give the changing numbers as a list (`v=${() => values()}`), a single value or a field of `rows`.
87
- 8. **A race stops at each step, or a live feed's marks stop between readings**: to play back data you have, never step through it with a timer; move a playhead on every frame, give the Plot the values interpolated between the steps around it, and keep the JS version's default 150 ms, so the marks glide and Pause leaves them between steps (interaction.md section 10). For a live feed, whose next value is not known yet, ease linear into each new reading over a little more than one interval (`ease: "linear"`, 1.5 intervals); with ease-out or a shorter duration the marks rest between readings.
88
- 9. **Marks trail behind data that changes rapidly or continuously** (a live feed, a slider or drag that drives the data, a play button, a bar chart race): in the default CSS version every new value restarts a CSS transition from wherever the mark is, so under steady change the marks always lag, and Safari can stall transitions that input events retarget. Give the Plot `animate` for the data groups that change (`animate=${["value"]}`, or `animate=${true}`), or the Chart when the scale moves with the data: rhp's JS clock carries each move into the next value. Measured with a slider changing the data every frame: the CSS version trailed the data by 10 points of 100, the JS version by 6.
96
+ 7. **Bars vanish, or a slat jumps to the first place, after the data changes** [4]: a `style` given as a string that changed (or turned to nothing) replaced the variables rhp keeps in the element's style.
97
+ Give a slat root's changing style as an object: `style=${() => ({ "--n": d.n })}`.
98
+ Blocks need the same workaround on published rhp 2.0.2; current source fixes Block style replacements for the upcoming 2.0.3 release.
99
+ 8. **`animate` is on, but a value still jumps**: the JS version never moves a group given as a function of the row (`v=${(d) => ...}`). Give the changing numbers as a list (`v=${() => values()}`), a single value or a field of `rows`.
100
+ 9. **A race stops at each step, or a live feed's marks stop between readings**: to play back data you have, never step through it with a timer; move a playhead on every frame, give the Plot the values interpolated between the steps around it, and keep the JS version's default 150 ms, so the marks glide and Pause leaves them between steps (interaction.md section 10). For a live feed, whose next value is not known yet, ease linear into each new reading over a little more than one interval (`ease: "linear"`, 1.5 intervals); with ease-out or a shorter duration the marks rest between readings.
101
+ 10. **Marks trail behind data that changes rapidly or continuously** (a live feed, a slider or drag that drives the data, a play button, a bar chart race): in the default CSS version every new value restarts a CSS transition from wherever the mark is, so under steady change the marks always lag, and Safari can stall transitions that input events retarget. Give the Plot `animate` for the data groups that change (`animate=${["value"]}`, or `animate=${true}`), or the Chart when the scale moves with the data: rhp's JS clock carries each move into the next value. Measured with a slider changing the data every frame: the CSS version trailed the data by 10 points of 100, the JS version by 6.
89
102
 
90
103
  ## A click, hover, tap or key does nothing, or the wrong thing
91
104
 
@@ -106,8 +119,34 @@ Start from a recipe, never from what another library does.
106
119
  The checker warns (`layout-jump`) when a control or the chart moves after one of its interactions; these are the usual causes. It tries only some controls, so try every one yourself and compare.
107
120
 
108
121
  1. **Text that changes with a switch moves the controls below it** (a headline or a dek per question): stack every version in one grid cell (a wrapper with `display: grid`, each version at `grid-area: 1 / 1`, `visibility: hidden` except the current one), so the cell is always as tall as the longest version.
109
- 2. **A readout that wraps on a phone pushes the chart down**: give it a `min-height` for its longest text (`min-height: 2lh`).
122
+ Align the versions to the cell's end (`align-self: end`) when the chart is below them, so a short version leaves no empty line above the chart.
123
+ 2. **A readout that wraps on a phone pushes the chart down**: give it a `min-height` for its longest text at 320px (`min-height: 2lh`, or `3lh`), counting the longest numbers and dates that gain a year ("Jan 4, 2027").
110
124
  3. **Numbers that change width shift what follows them**: use `font-variant-numeric: tabular-nums`, and give a column of numbers a fixed room; in an "auto" end room, align them across slats with `justify-self: stretch` and `justify-content: flex-end` on the label.
125
+ 4. **Every bar slides when a slat is lit**: the lit name turns bold in a `room.start` of "auto", which measures the widest name again and moves the plot.
126
+ Change only the lit name's color, or keep its bold width at rest: room in px, or the bold text drawn hidden under the name (`.name::after { content: attr(data-name); display: block; height: 0; overflow: hidden; visibility: hidden; font-weight: 700; }`, with `data-name=${() => d.name}` on the Label; measured: the bars held still at 1280px, where a plain bold name moved them 10.8px).
127
+ 5. **A button whose text changes (Play, Pause) moves its neighbors**: stack both texts in one grid cell, the hidden one `visibility: hidden`, so the button is always as wide as the longer one.
128
+
129
+ ## Text and layers laid over the chart
130
+
131
+ A bold composition sets a headline, a picture or a key over the plot's empty part.
132
+ These are the traps it brings.
133
+
134
+ 1. **A focus ring strikes through the text laid over the plot**: an outline on the slat's root runs the whole length of the plot.
135
+ Draw the ring on the mark instead: `.slat:focus-visible { outline: none; }` and `.slat:focus-visible .bar { outline: 2px solid var(--rhp-ink); outline-offset: 2px; }`, with `class="bar"` on the Bar.
136
+ A mark that wears a `shape()` or a mask shows no outline, because both cut it away: ring the slat's name instead (`.slat:focus-visible .name { outline: 2px solid var(--rhp-ink); outline-offset: 3px; }`), as `silhouettes` does.
137
+ Never take the slat's ring away without drawing another.
138
+ 2. **Gridlines run through the value labels**: a label just past a bar's end sits on the next gridline when the value is near a tick.
139
+ Give the number the paper as its background, on a span inside the Label so the Label keeps the gap rhp gives it (its own padding): `<${Label} at=${() => d.v} class="value"><span class="num">${() => d.v}</span><//>`, with `.num { margin: 0 -3px; padding: 0 3px; background: var(--rhp-surface); }` in the slat's css.
140
+ The grid is drawn under the slats, so the line stops at the text.
141
+ The theme's `surface` must be the paper (`surface: "var(--paper)"`; it is white by default), and the box suits a flat paper only: over a grain or a texture it shows as a patch.
142
+ Not next to a reference line, which stays whole (design.md section 7).
143
+ 3. **Taps on the marks pick nothing**: a layer laid over the chart (a picture, a glow, a caption box) takes every pointer event when it is drawn above rhp's plot (`z-index: 1`): at `z-index: 2` or more, or at 1 after the Plot in the page.
144
+ Give it `pointer-events: none`.
145
+ What the reader presses must be the slat or an element inside it: a mark that takes no pointer events sends the press to the chart under it, which cannot take focus, so the focused slat loses focus and the pick clears.
146
+ Give the mark `pointer-events: auto` (`donut`), or, when the chart picks by the pointer's place, put `onMouseDown=${(e) => e.preventDefault()}` on the element under the marks (`dial`).
147
+ 4. **The text laid over the plot runs into the marks with other data**: its place was set by hand (a fixed `top`, a share of the width) for these values.
148
+ Set it for your data, then check a copy whose values all sit near the largest, so the plot has no empty part, at every width, and look at the screenshots: the checker reports words over value labels, not words over bare marks.
149
+ Where the words meet a mark, set them above the chart at that width.
111
150
 
112
151
  ## Styles or colors do not apply
113
152
 
@@ -116,7 +155,7 @@ The checker warns (`layout-jump`) when a control or the chart moves after one of
116
155
  3. **The chart ignores the page's font or colors** [20]: give them in the Chart's `theme` (`font`, `ink`, `muted`, `grid`, `series`, ...).
117
156
  4. **`var(--brand)` as a color warns** [20]: put the brand colors in the theme instead.
118
157
  5. **Rows 5 and 6 take the first series color** [18]: `series()` counts six colors; with a shorter theme list call `series(n)`.
119
- 6. **A value label does not take its bar's color** [19]: set `--rhp-color` on the slat's root (and leave `color` off the Bar), or color the label itself.
158
+ 6. **A value label does not take its bar's color** [19]: set `--rhp-color` on the slat's root in a style object (and leave `color` off the Bar), or color the label itself.
120
159
  7. **The poster looks unstyled** [31]: the Poster brings no CSS. Style `.poster`, `figcaption`, `.kicker`, `.headline`, `.dek` and `.note` in the page.
121
160
  8. **Focus rings on slats do not show your style** [22]: rhp's core CSS resets `outline` on slat roots, so a page `:focus-visible` rule never reaches them. Put `.slat:focus-visible { ... }` in the slat type's `css`.
122
161
  9. **The poster's styles do not apply inside a framework component**: scoped component styles (Vue `scoped`, Svelte, Astro, Angular's emulated encapsulation) never reach elements rhp makes. Use a plain CSS file; environments.md section 14 has every framework's verified caveats.
@@ -125,6 +164,8 @@ The checker warns (`layout-jump`) when a control or the chart moves after one of
125
164
  12. **A transition on the slat's root does nothing** [22]: rhp keeps the root's own transition (its slide). Transition an element inside a block instead.
126
165
  13. **A rule in the slat's CSS matches nothing**: the class is on a different element than you think. Classes you put on blocks go on the block's own element (`.rhp-bar.bar`); `:horizontal` and `:vertical` go on the slat or a block, not on an element inside a block.
127
166
  14. **A pinned readout or a menu over the chart is drawn under the bars** [36]: give it `z-index: 2` or more.
167
+ 15. **The marks are invisible when the component is shown on its own**: its theme reads the app's variables with no fallback (`series: ["var(--chart-1)", …]`), so on a page without the app's CSS (the checker's page, a test, Storybook) every mark is transparent.
168
+ Give each a fallback with the app's own value: `"var(--chart-1, oklch(0.646 0.222 41.116))"` (design.md section 8).
128
169
 
129
170
  ## Console warnings rhp prints
130
171
 
@@ -150,6 +191,10 @@ These are not rhp errors, but they decide whether the user gets what they asked
150
191
  - **Invented numbers presented as fact**: data you supplied names its source and year, or the note says "Illustrative data".
151
192
  - **A series the user did not ask for**: what is left when their parts do not reach 100% is a quiet, unlabeled remainder explained in the note, not a new series with its own color and key entry; the brief lists it under Asked as a deviation.
152
193
  - **A headline that is not true for the data**: recompute every number in the headline, the dek and the note from the final data ("four in ten" was false for 52%).
194
+ - **Text computed from data that turns false on other data**: "rose ∞%" from a zero first week, "as many as" for a change from none, "held steady" because the first and last values match after a peak, "led for eight years" counted only inside the chart's window.
195
+ SKILL.md step 6 has the rules; step 8's hardest data finds them.
196
+ - **Mixed releases**: real yearly figures taken partly from first estimates and partly from later revisions can reverse a move between two years.
197
+ Take each year from the latest release that covers it.
153
198
  - **A real name on invented numbers**: never put a real country, city, company or person on illustrative data; use real figures with their source, or plain invented names ("Riverton", "a monsoon city", "Team A") and say "invented" in the note.
154
199
  - **A magazine poster dropped into an app**: inside an existing app, use the app's fonts and colors (design.md).
155
200
  - **Hover-only information**: everything a hover shows also shows on focus and on tap.
@@ -15,6 +15,10 @@ The CLI (`rhp-mcp check`) and the MCP tool (`rhp_check`) print the same report.
15
15
  5. **Probe** (inpage.js runs in the page; colors.js reads screenshots): layout, text, controls, rhp's CSS, colors.
16
16
  The report's screenshot is taken first, before any probe hides or restyles anything; then the screenshots the colors are read from.
17
17
  A slat's own transition on a block is read last, with motion asked for again (with reduced motion rhp turns every block's transition off).
18
+ Before that, the **sweep** (`sweep()` in index.js): every slat of the main Plot (up to 32, spread evenly) is picked in turn, by a tap at a phone's width and the mouse elsewhere, and what moved outside the plot is noted (as below).
19
+ It runs at each width checked, and on a page of its own at each of the page's own breakpoints between that width and the next narrower one (both sides of each `min-width` or `max-width` of its media queries, up to six per width), where a layout made only for the widths checked jumps: a readout that takes another line for one slat, just past the width where the poster's padding changes.
20
+ With reduced motion a pick shows at once, so two frames after each are enough: the sweep takes a few seconds per width.
21
+ Widths between those (a jump only from 322 to 338px) are not swept; a readout that keeps its height by design (one line, or the same number of lines whatever it says) has none.
18
22
  6. **Interact** (interact.js), on a fresh page with motion on, as a reader gets it.
19
23
  At the first width: point at a mark of the first, middle and last slat of the largest chart (in its first Plot that takes the pointer), and of the slat whose edge Label is widest when it is wider than theirs (lit, a name that gets bolder can make room "auto" wider); click each button and toggle, change each range and select, press an arrow key on a slider (`role=slider` first, else a range), and press Tab and the arrow keys when slats take focus.
20
24
  Then Tab through the page (up to 8 stops), and compare a screenshot of each stop's box (with 12px around it for an outline) focused and blurred: under 20 pixels that change (by more than 24 in all three channels) is focus that shows nothing.
@@ -22,13 +26,21 @@ The CLI (`rhp-mcp check`) and the MCP tool (`rhp_check`) print the same report.
22
26
  A point on a mark is one that reaches its slat: on its marks, inside an SVG shape in it (`isPointInFill`, for a donut's wedge that fills a small part of its box), or at a Label's center. When none does and the slat takes the pointer, something covers it, and the step says "could not reach slat N" and what covers it instead of pointing at a corner of a box.
23
27
  After each step the guard reads again what the slats read once (stale values, static charts), and the pass notes what moved on the page (the layout jumps).
24
28
  Controls go by their accessible name, slats by their Labels' text as the reader sees it (not hidden text, nor screen-reader text).
25
- 7. **Report** (findings.js turns probe results into findings, report.js writes the text): errors first, then warnings, then notes, what passed, the interactions and the screenshots.
29
+ 7. **Report** (findings.js turns probe results into findings, report.js writes the text): errors first, then warnings, then notes, what passed, the interactions (with one line for the sweep: how many slats, and every width it picked them at) and the screenshots.
26
30
  When pointing at slats (and tapping them) changes nothing but buttons or a slider change the chart, the report says so in one line.
27
31
  When nothing changes and the page has no controls, it says "no interaction found (fine when none was asked for)" in one line, instead of a line per slat (slats that others cover, in an overlap Plot with no interaction, are left out of it, as long as one slat was reached).
28
32
 
29
33
  ### Screenshots, and the page they move
30
34
 
31
35
  A full-page screenshot of a page taller than the window makes Chromium lay the page out at another size for a moment (1 by 1 px, after most captures), so the page's resize, ResizeObserver and matchMedia handlers run: a chart whose orientation follows the width turns and turns back.
36
+ The checker waits up to ten seconds for fonts, finite entry animations and two unchanged DOM frames, and checks the result of that wait.
37
+ `settleTimeout` overrides this budget in `check()` and the MCP tool.
38
+ The CLI exposes the same setting as `--settle-timeout <ms>`.
39
+ The CLI and MCP accept 100 to 30,000ms; the MCP's overall 50-second deadline still applies.
40
+ An unfinished wait produces `page-unsettled` instead of layout or color conclusions from a moving page.
41
+ That holds for the page that is measured, with reduced motion asked for, where a live feed or a race changes in steps and comes to rest between them.
42
+ The interaction pass runs with motion on, as a reader gets the page, and there a page that moves on its own never comes to rest: it waits up to three seconds for fonts and entry animations, half a second after each screenshot, and is tried as it moves (`clean-moving.html`).
43
+ Geometry and text colors are read after the initial screenshots have settled, so responsive capture handlers cannot leave stale coordinates in the color probes.
32
44
  So every screenshot goes through `capture()` (index.js), which lets the page come to rest again when the capture made it change (`calm()` in inpage.js), and `hide()` and `show()` put back only the style properties they set, never a whole `style` attribute saved before (that undid what the page did meanwhile and left such a chart drawn wrong).
33
45
 
34
46
  ## Levels
@@ -76,7 +88,7 @@ The tests read this table: every code in it must have a chart in `test/broken/`
76
88
  | `interaction-error` | error | A step of the interaction pass threw or logged an error. | As for `runtime-error`. |
77
89
  | `rhp-warning` | error | rhp printed one of its `rhp:` warnings. | Do what the warning says. |
78
90
  | `request-failed` | error | A file next to the chart (or in the bundle) failed to load. | Fix the path, or add the file. |
79
- | `remote-failed` | info | A remote file (fonts, a CDN) could not be fetched; the check goes on without it. | Check the URL; offline, fonts fall back. |
91
+ | `remote-failed` | info | A remote file (fonts, a CDN) could not be fetched, or the page asked for something the checker does not fetch (a request that sends data, an address that is not public); the check goes on without it. | Check the URL; offline, fonts fall back. Load data and assets with plain GET requests from public URLs, or from files next to the chart. |
80
92
 
81
93
  ### How rhp is used (the guard)
82
94
 
@@ -100,6 +112,7 @@ The tests read this table: every code in it must have a chart in `test/broken/`
100
112
  |---|---|---|---|
101
113
  | `no-chart` | error | No `.rhp-chart` in the page (not reported when the code can't load, since that reason is reported instead). | Render into an element the page has, or fix the errors above. |
102
114
  | `empty-plot` | error | A Plot with no slats shown (an empty overlap Plot is fine: a readout drawn only while pointing). | Give the Plot its data. |
115
+ | `page-unsettled` | error | Fonts, finite animations or DOM changes did not settle within the measurement budget. | Finish entry animations, honor reduced motion for continuous updates, or increase `settleTimeout` for a slow machine. |
103
116
  | `small-plot` | error | A plot under 40px along its value axis, or slats under 2px thick (3px slats are fine: a histogram's many bins, a line chart's slats for pointing). | A height or an aspect, a thickness, or fewer rows. |
104
117
  | `collapsed-chart` | error | A chart whose plot is under 2px wide: it sits in a flex row, an inline-block or a float that shrinks it to its room. | `flex: 1; min-width: 0`, or a box with a width. |
105
118
  | `plot-outside-chart` | error | A `.rhp-plot` with no `.rhp-chart` around it: no scale and none of rhp's CSS. | Put the Plot in a Chart. |
@@ -128,10 +141,27 @@ The tests read this table: every code in it must have a chart in `test/broken/`
128
141
  | Code | Level | What triggers it | Fix it gives |
129
142
  |---|---|---|---|
130
143
  | `unnamed-control` | warning | A button, input, select or control with a role (slider, switch, tab...) that has no accessible name: no text, aria-label, aria-labelledby or label, so a screen reader says only what kind of control it is. | Visible text, an aria-label on an icon button, or a `<label>` around an input or a select. |
131
- | `layout-jump` | warning | An interaction of the pass moves a control other than the one it used, or a chart's top, by more than 2px (a readout that takes a second line, a button whose label changes its width); or, while it lights a slat (pointing, a tap, the keyboard), it moves an edge of the plot by more than 2px (a lit name that gets bolder makes room "auto" wider, and every bar slides). A control in a slat that moves with its slat (rows sorted or removed) is left out, and so are fixed and sticky elements. | A box that keeps its size: a readout on one line with an ellipsis, or a min-height for its longest text; buttons that keep their width; names that keep their weight and size when lit, or room in px. |
144
+ | `layout-jump` | warning | An interaction of the pass, or a pick of the sweep, moves a control other than the one it used, or a chart's top, by more than 2px (a readout that takes a second line, a button whose label changes its width); or, while it lights a slat (pointing, a tap, the keyboard), it moves an edge of the plot (a lit name that gets bolder makes room "auto" wider, and every bar slides) or the end of the chart's section (its outermost box in the page: a readout after the chart that takes a second line), by more than 2px. A control in a slat that moves with its slat (rows sorted or removed) is left out, and so are fixed and sticky elements. | A box that keeps its size: a readout on one line with an ellipsis, or a min-height for its longest text; buttons that keep their width; names that keep their weight and size when lit, or room in px. |
132
145
  | `out-of-view` | warning | At a phone's width, in a window 664px tall, a tap on a slat that is in the window changes text the reader can see, and all of it lies outside the window (above, below or beside it), even when the tap lights the slat in view: a readout above a tall chart, when the last slat is tapped. | A readout next to the slat (in the slat, or in a Label of it), or one kept in view (`position: sticky`, with a z-index over the plot's 1). |
133
146
  | `focus-invisible` | warning | Tab reaches a slat or a control, and a screenshot of its box (with 12px around it) focused and blurred differ in under 20 pixels: `outline: none` with nothing in its place. | Keep the focus ring, or a `:focus-visible` style that shows in its place (on a slat, in the slat type's css). |
134
147
 
148
+ ### Conditional selection repair
149
+
150
+ The shared checker runs this audit after creation, through both `rhp_check` and the CLI (`npx -y @bezda/rhp-mcp check <file>`).
151
+ It does not require either click selection or hover, and these checks do not choose an interaction for the chart.
152
+ On an isolated page, it first verifies that a click persists after the pointer leaves the poster, another slat's hover overrides it, and leaving the poster restores the clicked selection.
153
+ Only that observed combination qualifies.
154
+ It then checks a pointer move and a click on noninteractive text or margin inside the same poster, outside every chart and control.
155
+ The initial appearance is the reset reference, preserving an existing default without guessing which subject should be selected.
156
+ Moving pages, unreachable marks and sequences that scroll are inconclusive rather than findings.
157
+ The audit samples up to three reachable slats of the main Plot at the first checked width.
158
+ It never changes the source; its conditional fix is returned to the authoring agent through the skill's existing check-and-fix step, with explicit user interaction instructions taking precedence.
159
+
160
+ | Code | Level | What triggers it | Fix it gives |
161
+ |---|---|---|---|
162
+ | `selection-hover-stays` | warning | Verified click selection with a temporary hover override: moving off the chart but staying in the poster does not restore the clicked selection, although leaving the poster does. | Clear only the hover preview on leaving the chart area; preserve the clicked selection, unless the user explicitly asked for different behavior. |
163
+ | `selection-reset` | warning | The same verified pattern: clicking noninteractive poster space outside the chart fails to restore the initial appearance. | Clear the selection if no default existed, or restore the chart's meaningful default; preserve explicitly requested behavior and other controls. |
164
+
135
165
  ### Colors (read from screenshots)
136
166
 
137
167
  | Code | Level | What triggers it | Fix it gives |
@@ -77,12 +77,18 @@ export async function launch() {
77
77
 
78
78
  const failures = [];
79
79
 
80
+ // Chromium's own sandbox on (Playwright leaves it off): the page runs code the checker did not write. Where it
81
+ // cannot start (some Linux containers, or running as root), the same browser runs without it. WebRTC sends nothing
82
+ // outside a proxy, so it cannot reach the network around the check's router.
83
+ const args = ["--hide-scrollbars", "--disable-lcd-text", "--force-webrtc-ip-handling-policy=disable_non_proxied_udp"];
80
84
  for (const t of tries) {
81
- try {
82
- const browser = await chromium.launch({ headless: true, ...t.options, args: ["--hide-scrollbars", "--disable-lcd-text"] });
83
- return { browser, name: `${t.name} ${browser.version()}` };
84
- } catch (e) {
85
- failures.push(`${t.name}: ${String(e.message ?? e).split("\n")[0]}`);
85
+ for (const chromiumSandbox of [true, false]) {
86
+ try {
87
+ const browser = await chromium.launch({ headless: true, chromiumSandbox, ...t.options, args });
88
+ return { browser, name: `${t.name} ${browser.version()}`, sandboxed: chromiumSandbox };
89
+ } catch (e) {
90
+ if (!chromiumSandbox) failures.push(`${t.name}: ${String(e.message ?? e).split("\n")[0]}`);
91
+ }
86
92
  }
87
93
  }
88
94
 
@@ -4,7 +4,7 @@
4
4
  const q = (text) => `"${text}"`;
5
5
 
6
6
  // What an interaction is, said as something a reader does ("clicking button "Q2"")
7
- const DOING = { hover: "pointing at", tap: "tapping", click: "clicking", input: "setting", select: "setting", key: "pressing" };
7
+ const DOING = { hover: "pointing at", tap: "tapping", click: "clicking", input: "setting", select: "setting", key: "pressing", pick: "picking" };
8
8
 
9
9
  // How far something moved: "88px down", "12px right"
10
10
  const shift = ({ dx, dy }) => [dy && `${Math.abs(dy)}px ${dy > 0 ? "down" : "up"}`, dx && `${Math.abs(dx)}px ${dx > 0 ? "right" : "left"}`].filter(Boolean).join(" and ");
@@ -198,6 +198,20 @@ export function fromProbe(f, context) {
198
198
  message: `Tab reaches ${f.what}${f.more ? ` and ${f.more} more` : ""}, but nothing on screen shows the focus there: a keyboard reader cannot see where it is.`,
199
199
  fix: "Show focus: take off outline: none, or put a :focus-visible style that shows in its place (outline: 2px solid in the ink color, outline-offset: 2px; on a slat, in the slat type's css).",
200
200
  };
201
+ case "selection-hover-stays":
202
+ return {
203
+ level: "warning",
204
+ code: f.code,
205
+ message: `After clicking ${f.selected}, hovering ${f.target} overrides that selection, but moving beside the chart (${f.what}) does not give the selected item back. Leaving the poster does.`,
206
+ fix: "This chart already has click selection with a temporary hover override. Unless the user explicitly requested a different behavior, end only the hover preview when the pointer leaves the chart area, including when it moves onto the poster's readout, text or margin, and show the existing click selection again. Keep hover and selection separate; do not clear the selection on pointerleave, add a new interaction, or change a different interaction style.",
207
+ };
208
+ case "selection-reset":
209
+ return {
210
+ level: "warning",
211
+ code: f.code,
212
+ message: `This chart has click selection with a temporary hover override. After selecting ${f.target}, clicking beside the chart (${f.what}), inside the poster, does not restore its initial state.`,
213
+ fix: "Unless the user explicitly requested persistent selection or another reset behavior, make a click or tap inside this poster but outside its chart area reset this existing selection and its hover preview. Restore the meaningful default selection the chart already had (such as the analysis subject); if it had none, clear all selections. Preserve that default rather than inventing one, and leave controls, links, other charts, and keyboard interactions alone. Do not add click selection or hover to a chart that lacks them. Apply the fix in the chart source and rerun the checker; the checker does not rewrite the file.",
214
+ };
201
215
  default:
202
216
  return { level: "error", code: f.code, message: JSON.stringify(f), fix: "" };
203
217
  }