@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.
@@ -537,6 +537,24 @@ At 390px:
537
537
  Use an ellipsis only when the full name is in the slat's `aria-label` and in the readout.
538
538
  - `room: { start: "auto" }` sizes the names' room to the longest name, up to `--rhp-gutter-max` (40% of the chart).
539
539
 
540
+ ### Fit the label gutters
541
+
542
+ For a poster row containing one main plot, usually fit whichever side gutters contain labels to the text that actually occupies them, so the plot uses the remaining width.
543
+ This helps smaller screens and keeps the outer labels aligned with the caption, controls, readout and footer throughout the poster.
544
+ Treat this as a design preference and let the intended composition take priority, including deliberate spacing or different alignments.
545
+ The poster's own padding supplies the outer margins; do not add a second empty buffer past the labels unless the design calls for one.
546
+
547
+ - Prefer `room: { start: "auto", end: "auto" }` when the labels are direct edge Labels, or use "auto" only on the side that needs it.
548
+ - Labels anchored with `at` are not measured by "auto".
549
+ Measure their rendered sizes and pass reactive pixel values to the appropriate `room` sides instead of choosing fixed desktop and phone guesses.
550
+ - Measure after fonts load and when text, font size, wrapping or stacking changes, including gradual changes between breakpoints.
551
+ Keep each label's box tight to its content so an unnecessarily wide wrapper does not become empty gutter space.
552
+ - Include the clearance between a point and its label, but account for a point sitting inside the scale's end so that inset is not reserved twice.
553
+ The outermost text should reach the poster's content edge without crossing it.
554
+ - Use natural layout sizes, such as `ResizeObserver` border-box sizes, so fitting or zooming the poster does not alter its gutters.
555
+ Schedule size updates in an animation frame outside ResizeObserver delivery to avoid resize loops.
556
+ - Check both label layouts and the widths between breakpoints, then fit the poster and verify that its labels, marks and PNG export keep the same alignment.
557
+
540
558
  How many slats:
541
559
 
542
560
  - Horizontal slats of 28 to 50px; up to about 15 read at a glance, about 30 at 22px each.
@@ -30,7 +30,7 @@ rhp draws all of these; the recipe named has the technique, not the design.
30
30
  | a route or a sequence in space (stops, stages, a course, a river) | one line with its stations in order | `stacked-100` (its key is a metro line), `gantt`, `slope` |
31
31
  | a document the reader knows (a receipt, a ledger, a timetable, a scoreboard, a league table, a menu) | the chart laid out as that document, its lines the slats | `lollipop`, `waterfall`, `sparklines`, `live` |
32
32
  | a place (states, districts, seats in a hall, a pitch, a shelf) | a grid of tiles that echoes the place | `heatmap`'s composition (a Plot of rows, each a Plot of Cells) |
33
- | an instrument that measures it (a thermometer, a gauge, a ruler, a clock, a scale) | the instrument's own scale and pointer | a `Scale` drawn your own way, a Tick as the pointer, `radial-bars` |
33
+ | an instrument that measures it (a thermometer, a gauge, a ruler, a clock, a scale) | the instrument's own scale and pointer | a `Scale` drawn your own way, a Tick as the pointer, `radial-bars`, `tape` (a ruler's rules and numerals along a whole cut into lengths) |
34
34
 
35
35
  - The native form is the hero, and the numbers still have to be read: print each value on or beside its mark, since a ring, an outline or a tile is read less exactly than a bar on a shared baseline.
36
36
  - When an exact comparison is the whole point, keep bars from one baseline and let the subject show in how the marks are drawn (design.md section 2).
@@ -76,7 +76,7 @@ The headline will be its answer.
76
76
  | see a value over two categories | a grid of values (day by hour) | heatmap | `heatmap` |
77
77
  | see two groups across ordered bands | two values per band | pyramid | `pyramid` |
78
78
  | see things known by their shape at their size | one value per item, each item with an outline | silhouettes to scale | `silhouettes` |
79
- | see how a day or another cycle splits | parts that add up to the cycle | a dial | `dial` |
79
+ | see how a day or another cycle splits | parts that add up to the cycle | a dial, or a tape cut into lengths | `dial`, `tape` |
80
80
  | follow one part of a total over periods, and count it | parts per period that add up | unit stacks with the part at the foot | `unit-stack` |
81
81
  | see a schedule and its progress | tasks with start, end, progress | gantt | `gantt` |
82
82
  | follow prices with their range | open, high, low, close per day | candlestick | `candlestick` |
@@ -150,6 +150,7 @@ The interaction listed is the recipe's; interaction.md has the others.
150
150
  | `pyramid` | two sides of a population by age | 2 | an age picked lights everyone that age or older |
151
151
  | `silhouettes` | ten towers to scale, each in its own outline, on one street | 1, vertical, `shape()` per row | readout of the tower picked, its level carried to the tallest |
152
152
  | `dial` | the parts of a day around a 24-hour dial | 10 + 9 | a hand that turns to the part picked, a readout in the hub |
153
+ | `tape` | the parts of a day laid end to end along a tape measure cut into three 8-hour lengths | 2 (an `overlap` Plot of Bars from each part's start to its end), a Chart per length + 9 | readout of the part picked, its key lines picking too |
153
154
  | `log-line` | ten years of a price on a log scale with a line at each doubling, its falls shaded | 5 (the falls as one filled Line) + 8 + 9, log scale | a crosshair readout by pointer, tap, drag or keys |
154
155
  | `unit-stack` | monthly spending as piles of coins, one part at the foot of every pile | 2 (the picked part and the rest), units by a mask | a picker of parts |
155
156
  | `gantt` | tasks over weeks, progress, today | 2 + 8 + 9 | readout of a task and its dependencies |
@@ -1,6 +1,6 @@
1
1
  # Interaction
2
2
 
3
- How to pick the one interaction that serves a chart's story when the user names none, and how to build any interaction a user asks for.
3
+ How to choose interaction behavior and presentation for the reader's task, and how to build an interaction a user asks for.
4
4
  rhp keeps no interaction state: the app holds it in a signal, passes it to the Plot as data, and the slat shows it.
5
5
 
6
6
  Contents:
@@ -71,10 +71,14 @@ The page's CSS styles what sits around the chart (the poster, the readout, butto
71
71
 
72
72
  ## 1. Pick the interaction from the story
73
73
 
74
- When the user names an interaction, build exactly that, from the section that matches it.
75
- When they name none, find the chart's story here and add that one interaction.
74
+ When the user names an interaction, preserve the behavior they requested and use the sections below for the techniques it needs.
75
+ When they name none, decide what the reader should be able to inspect, compare or change, and what visible response helps them do it.
76
+ The table offers working examples, not fixed pairings between stories, chart types and controls.
77
+ Reuse reliable state and event handling, while choosing the controls, feedback, placement and transitions for this chart.
78
+ The same selection state can drive an inline annotation, a comparison or a detail reveal; a separate readout band is only one way to present it.
79
+ Keep a familiar pattern when it fits the task and design; difference alone is not a reason to change it.
76
80
 
77
- | The story | The interaction | Section | Recipes that have it |
81
+ | The reader's task | Example interaction | Section | Recipes demonstrating the technique |
78
82
  |---|---|---|---|
79
83
  | compare items: which is bigger, by how much | readout of the slat under the pointer, finger or focus, with the fields the bars do not show | 2 | bar, column, pyramid, donut |
80
84
  | a ranking whose order depends on the measure | sort toggle | 3 | grouped-bars, bullet, dumbbell |
@@ -104,6 +104,51 @@ Both widened when a longer label became visible, with identical widths before an
104
104
  Conditional rendering does not by itself require fixed room.
105
105
  Verify the chart's gutter geometry, including its own sizing constraints, when changing which labels are mounted.
106
106
 
107
+ ## Go without the guard when the page's CSS leaves charts alone
108
+
109
+ The guard is the part of rhp's CSS that resets, on every element rhp makes, what a page's CSS might set there, so the page cannot change a chart (api.md section 11, "What page CSS can and cannot change").
110
+ Without it, a chart's first draw is 18 to 28% faster in Chromium (1,000 bars: 34 to 24 ms), about 30% faster in WebKit, and 4 to 16% faster in Firefox.
111
+ A chart goes without it with `noCssGuard=${true}` on the Chart, and every chart on a page with `noCssGuard()` before the first one (rhp 2.1 and later).
112
+ Without it, rhp's own CSS still places and colors every block, keeps hidden slats hidden and keeps `box-sizing: border-box`, but page CSS reaches whatever rhp does not set itself (measured with page rules on every `div` and `span`):
113
+
114
+ | Page CSS | Without the guard |
115
+ |---|---|
116
+ | `margin`, `padding`, `border` | moves blocks, Labels and grid lines |
117
+ | `width`, `height`, `min-width`, `max-width`, `min-height`, `max-height` | resizes bars, slat roots and Labels |
118
+ | `font`, `line-height`, `letter-spacing`, `text-indent`, `text-transform` | changes Labels and axis numbers, and moves them |
119
+ | `::before` and `::after` with `content` (a clearfix) | adds boxes to the chart's grid, the axis, Plots and slat roots |
120
+ | `background`, `box-shadow`, `border-radius`, `outline` | drawn on rhp's elements |
121
+ | `transition` | reaches the axis numbers and edge Labels (rhp keeps its own on blocks and slat roots) |
122
+ | `animation` | runs on every element |
123
+ | `::selection` | colors selected text in Labels |
124
+ | a rule for a class a slat uses too (`.poster .note`, written for the poster's note, and a Label with `class="note"`) | reaches that element |
125
+ | `display`, `float`, `box-sizing` | moves nothing |
126
+
127
+ The browser's own styles reach a chart without the guard too, on a slat root that isn't a `div` (measured in Chromium, WebKit and Firefox): an `<svg>` root cuts off what it draws outside its box, an `<a>` is blue and underlined, a `<button>` or `<input>` has the browser's border, background and font, and a `<p>`, `<ul>`, `<h3>` or `<figure>` has margins that move it off its place.
128
+ A `div`, `li`, `span`, `section`, `label` or `table` root looks the same either way.
129
+ Set those properties in the slat's `css` (`margin: 0`, `overflow: visible`), or use a `div`.
130
+
131
+ Choose it for a page made for its charts, such as a poster or an article, or for one large chart among small ones.
132
+ Then check that the page's CSS leaves the chart alone: compare screenshots of the chart with and without it, at rest, while hovered and focused, and after a data change.
133
+ The checker reports `page-css-reaches` for a page rule that reaches an element rhp makes in a chart without the guard.
134
+ Drawn without the guard under 12 popular stylesheets, the docs site's 92 example charts and the slat gallery's 20 changed only where a slat used one of the framework's class names: Bootstrap 5's `.row` and `.mark`, Bulma's `.box`, `.tag`, `.icon` and `.progress`, Milligram's `.row`.
135
+ Tailwind (browser build and preflight), normalize.css, modern-normalize, sanitize.css, Pico, Water.css, Simple.css and new.css changed none of them.
136
+ The prop saves as much as `noCssGuard()` for its own chart, but once one chart says `noCssGuard`, the guard checks which chart each element is in (a `data-rhp-guard` mark on the charts that keep it), and those charts draw 2 to 6% slower.
137
+ So prefer `noCssGuard()` when every chart on the page can go without it.
138
+ A chart inside another chart's slat goes without the guard only when the chart around it does too.
139
+ A page that links rhp's CSS itself (`linkedCss()`) links `@bezda/rhp/rhp-no-css-guard.css`, the same without the guard, instead of `rhp.css`, and calls `noCssGuard()` too: the prop can't change a stylesheet the page links, and rhp warns.
140
+ In the html template the prop is written out, `noCssGuard=${true}`: bare, it is `""`, which is false.
141
+
142
+ ## Read a slat's position in its CSS
143
+
144
+ From rhp 2.1, each slat's `--rhp-position` is written on its root and not passed down to the elements inside, so the browser can reuse one slat's styles for the next: in Chromium a 1,000-bar chart draws about 40% faster (57 to 33 ms), a 50 by 50 heatmap 57% faster, 50 small charts 17% faster; WebKit gains about 10% on bars, and Firefox is unchanged.
145
+ Where nothing can be shared anyway (a scatter of 1,000 dots, every value changing at once), keeping the position on the root costs 1 to 2 ms (3 to 5%) in Chromium; WebKit shows no cost there, and Firefox none beyond its run-to-run noise.
146
+ The slat type's `css` still reads it on any element in the slat: rhp rewrites `var(--rhp-position)` there to a copy its roots pass down (DevTools shows it as `--rhp-row`), and in a Plot inside the slat it is the inner slat's place.
147
+ Its slats then share no styles, as in 2.0, and draw about 4% slower than in 2.0 (1,000 bars: 63 against 61 ms in Chromium).
148
+ A `style` prop or page CSS on an element inside a slat gets nothing from `var(--rhp-position)`.
149
+ Read it in the slat's `css`, or copy it on the root and read the copy inside: `style=${{ "--row": "var(--rhp-position)" }}`, then `var(--row)` (those slats then draw as slowly as before).
150
+ `inheritPosition()` before the first chart passes it down again in every chart on the page, as rhp did before 2.1, and charts draw as in 2.0 or a little slower (1,000 bars: 59 against 57 ms in Chromium).
151
+
107
152
  ## Verified example and limits
108
153
 
109
154
  The vocabulary article already uses these patterns.
@@ -57,6 +57,10 @@ Start from a recipe, never from what another library does.
57
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).
58
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
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.
60
+ 11. **The chart says `noCssGuard`, or the page calls `noCssGuard()`**: page CSS that sets margins, padding, borders or sizes on every `div` or `span` (a framework's base styles) now reaches rhp's elements and moves them, and a slat root that is a `<p>`, `<ul>`, `<h3>` or `<figure>` keeps the browser's margins (performance.md, "Go without the guard").
61
+ Take `noCssGuard` off, aim those rules at the page's own elements, or use a `div` as the slat's root.
62
+ 12. **A position read outside the slat's CSS is empty** (rhp 2.1 and later): `var(--rhp-position)` in a `style` prop on an element inside a slat gets nothing [37].
63
+ Read it in the slat type's `css`, or copy it on the slat's root (`style=${{ "--row": "var(--rhp-position)" }}`) and read `var(--row)`.
60
64
 
61
65
  ## Text sticks out, is cut off or overlaps
62
66
 
@@ -95,7 +99,7 @@ Start from a recipe, never from what another library does.
95
99
  6. **Numbers show long decimals while they move** [26]: in the JS version (`animate`) values are fractional mid-way; round what you print.
96
100
  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
101
  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.
102
+ Blocks need the same workaround only on rhp 2.0.2; 2.0.3 fixed Block style replacements.
99
103
  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
104
  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
105
  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.
@@ -116,12 +120,13 @@ Start from a recipe, never from what another library does.
116
120
 
117
121
  ## The layout jumps when the reader interacts
118
122
 
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.
123
+ The checker warns (`layout-jump`) when a control or the chart moves after one of its interactions, (`text-shift`) when a text that still says the same thing moves, and (`jitter`) when a text moves as the pointer goes along a chart that follows it (a line read day by day); these are the usual causes. It tries only some controls, so try every one yourself and compare.
120
124
 
121
125
  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.
122
126
  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
127
  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").
124
128
  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.
129
+ In a readout, tabular figures alone are not enough when the number of digits changes ("$654" to "$84,176") or a date's month names differ in width: start each part where nothing before it changes width (a grid with a column per part, or each part on its own line), or give the value a box with a `min-width` in `ch` for the longest value it can show, right-aligned. A label or a unit after a value on the same line ("8,101 tons in 2026") slides at every step of a pointer moving along the chart.
125
130
  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
131
  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
132
  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.
@@ -151,6 +156,7 @@ These are the traps it brings.
151
156
  ## Styles or colors do not apply
152
157
 
153
158
  1. **Page CSS aimed at bars, labels or the axis** [21]: it cannot change their color, background, font, border, radius, shadow, padding or transition, even with `!important`. Move those rules into the slat type's `css`.
159
+ (A chart that says `noCssGuard` lets page CSS reach what rhp does not set itself, such as fonts, borders and padding, but still not a block's color: performance.md, "Go without the guard".)
154
160
  2. **`className`** [5]: on a block it replaces rhp's class and the block vanishes. Use `class`.
155
161
  3. **The chart ignores the page's font or colors** [20]: give them in the Chart's `theme` (`font`, `ink`, `muted`, `grid`, `series`, ...).
156
162
  4. **`var(--brand)` as a color warns** [20]: put the brand colors in the theme instead.
@@ -166,6 +172,8 @@ These are the traps it brings.
166
172
  14. **A pinned readout or a menu over the chart is drawn under the bars** [36]: give it `z-index: 2` or more.
167
173
  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
174
  Give each a fallback with the app's own value: `"var(--chart-1, oklch(0.646 0.222 41.116))"` (design.md section 8).
175
+ 16. **A Label takes a color, font or margin meant for something else on the page**: the chart says `noCssGuard`, and a page rule for one of the slat's classes reaches it (the candlestick recipe's page had `.poster .note` for its footnote, and its earnings Label has `class="note"`).
176
+ Rename the slat's class, or aim the page rule at the page's own element.
169
177
 
170
178
  ## Console warnings rhp prints
171
179
 
@@ -182,6 +190,7 @@ Each is worth fixing [34]:
182
190
  | "--rhp-radius takes one length" | one length, or `--rhp-start-radius` and `--rhp-end-radius` |
183
191
  | "unknown ease" | a CSS name, four cubic-bezier numbers, or a function |
184
192
  | "linkedCss() was called, but this page doesn't link" | link `@bezda/rhp/rhp.css`, or remove `linkedCss()` |
193
+ | "noCssGuard on a Chart can't turn off the guard in the rhp.css the page links" | link `@bezda/rhp/rhp-no-css-guard.css` instead and call `noCssGuard()`, or remove the prop |
185
194
 
186
195
  ## Mistakes in the work itself
187
196
 
@@ -0,0 +1,33 @@
1
+ // Adds the poster's fit-to-screen and PNG download block to a standalone HTML page, before </body>.
2
+ // The block is the same in every recipe; this copies it from one, so the agent never reads it.
3
+ // Usage: node scripts/add-download.mjs <page.html>
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const page = process.argv[2];
10
+ if (!page || !fs.existsSync(page)) {
11
+ console.error("Usage: node scripts/add-download.mjs <page.html> (the page must exist)");
12
+ process.exit(1);
13
+ }
14
+
15
+ const html = fs.readFileSync(page, "utf8");
16
+ if (html.includes('id="poster-download-script"')) {
17
+ console.log(`${page} already has the download block.`);
18
+ process.exit(0);
19
+ }
20
+ if (!/<\/body>/i.test(html)) {
21
+ console.error(`${page} has no </body>: add the block by hand, from <style id="poster-download-styles"> to </body> of any recipe.`);
22
+ process.exit(1);
23
+ }
24
+
25
+ const recipe = fs.readFileSync(path.join(here, "../recipes/bar.html"), "utf8");
26
+ const start = recipe.indexOf('<style id="poster-download-styles">');
27
+ const end = recipe.lastIndexOf("</body>");
28
+ const block = recipe.slice(start, end);
29
+
30
+ // We insert before the last </body>, so a </body> inside a script or a comment is left alone.
31
+ const at = html.toLowerCase().lastIndexOf("</body>");
32
+ fs.writeFileSync(page, html.slice(0, at) + block + html.slice(at));
33
+ console.log(`Added the fit-to-screen and download block to ${page} (${block.length} characters). The poster needs the class "poster" for the block to find it.`);
@@ -19,14 +19,26 @@ The CLI (`rhp-mcp check`) and the MCP tool (`rhp_check`) print the same report.
19
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
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
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.
22
+ While the sweep picks, the texts around the charts that a pick rewrites are noted: that is the readout.
23
+ When the main chart draws a Line or an Area, a layer over the plot takes the pointer (the main Plot's first slat cannot be pointed at) or the main Plot has one or two slats (a line per series), the reader moves along the chart, and the sweep first **scrubs** it (`scrub()` in index.js): the mouse goes through 41 points across the plot's middle, along the scale of the plot that draws the line or the area (or of a Plot of one or two slats) and across the main Plot's slats otherwise, at every width (a finger dragging along a phone's plot shows the same readout).
24
+ After each point the layout is measured as for a pick, and so is every line of text around the charts, each text node on its own (its first line box, in the box it floats in), against the page with the pointer away: a text that still says the same thing and moved jitters, and so does one whose words changed and that kept none of its edges (its start, its end or, on one line, its middle) where it was.
25
+ When the page's text changes at three steps in four or more, the readout follows the pointer along the chart (a line read day by day), and a whole sentence that slides counts, though a pick lets one slide. When it changes less often, it changes once per mark crossed (bubbles that a layer covers), and a slide along the line counts as for a pick: only for a text that still says the same thing, and not for a sentence. A text that moves up or down (a readout's name centered beside a description that takes another line) counts either way.
26
+ The report says when no slat could be picked, rather than that nothing moved.
27
+ Then, when the main chart is taller than three quarters of the window (664px at a phone's width), the page is scrolled so that the chart's middle is in the middle of the window, and the controls of the chart's section (not its fit and download icons) and the readout are noted when all of them are out of view.
28
+ At the first width, with a mouse, the **glide** (`glide()` in index.js) then moves the pointer from a slat to its neighbor in twelve steps, for the first two slats, two in the middle and the last two, and reads after each step the page's visible text and how the slats are drawn (two hashes).
29
+ When both slats show something of their own and a step between them shows the text, or the slats, as they are with the pointer away, the readout or the lit slat flickers in the gap.
22
30
  6. **Interact** (interact.js), on a fresh page with motion on, as a reader gets it.
23
31
  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.
24
32
  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.
25
33
  At a phone's width, in a window 664px tall (what Safari shows of a page on a 390 by 844 iPhone): tap a mark of the first and the last slat, and note when the tap changed text the reader can see (wrote it, or showed it) but all of that text lies outside the window while the tapped slat is in it; a style the tap changed in view (the tapped bar lit) does not tell the reader what the text says.
26
34
  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.
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).
35
+ 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, and the texts that moved).
36
+ A step that is not a pick (a button, a range, a select, a key on a slider) is also read five times as it runs, every 200ms, so a text that jitters while Play runs the years counts even when it is back in place at the end.
37
+ A text is measured in the box it floats in when it floats (fixed, sticky or absolute: a readout pinned to the window, a tooltip that follows the pointer), else on the page, and only when its own text (its text nodes, not its children's) is the same as before: a value that changed may change its size, but the label beside it stays put.
38
+ A text that moves by as much as something that jumped is reported with that jump, not again.
28
39
  Controls go by their accessible name, slats by their Labels' text as the reader sees it (not hidden text, nor screen-reader text).
29
40
  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.
41
+ A finding whose fix the skill's references explain names that section on a line of its own (`See: pitfalls.md, "The layout jumps when the reader interacts"`, from `SEE` in findings.js), so an agent whose fix did not hold reads that section, not the whole file; the headings it names must stay as they are.
30
42
  When pointing at slats (and tapping them) changes nothing but buttons or a slider change the chart, the report says so in one line.
31
43
  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).
32
44
 
@@ -74,7 +86,7 @@ The tests read this table: every code in it must have a chart in `test/broken/`
74
86
  |---|---|---|---|
75
87
  | `missing-export` | error | A name imported from `@bezda/rhp/standalone` that it doesn't export, so the module fails to load. | What to use instead (a function returning the template for `Dynamic`, `render()` into the other element for `Portal`...), or the closest export. |
76
88
  | `two-solids` | error | `@bezda/rhp/standalone` and `solid-js` imported in the same code: two copies of Solid, and the signals of one are not tracked by the other. | Import everything from the standalone module. |
77
- | `bare-boolean` | error | `overlap`, `static`, `keyboard`, `animate`, `mirror`, `smooth`, `fill` or `keyed` written bare on a component in an html template: it passes `""`, which is false. | `name=${true}`. |
89
+ | `bare-boolean` | error | `overlap`, `static`, `keyboard`, `animate`, `noCssGuard`, `mirror`, `smooth`, `fill` or `keyed` written bare on a component in an html template: it passes `""`, which is false. | `name=${true}`. |
78
90
  | `handler-at-render` | error | An `on...` prop on a component in an html template given a function with no parameter (inline, or a function named in the file): it is read as a value, runs at render and is never attached. | Give the handler a parameter: `(e) => ...`. |
79
91
  | `style-string` | error | `style="..."` (or a string) on a Chart or a Plot, which take only a style object. | `style=${{ "pointer-events": "none" }}`. |
80
92
  | `class-name` | error | `className` in an html template: on a block it replaces rhp's class and the block is not drawn, elsewhere it sets nothing. | `class`. |
@@ -134,6 +146,7 @@ The tests read this table: every code in it must have a chart in `test/broken/`
134
146
  |---|---|---|---|
135
147
  | `series-past-theme` | error | A block's color is `series-n` with no `--rhp-series-n` in its chart, so it falls back to `series-1`. | `series(n)` for a theme of n colors, or more theme colors. |
136
148
  | `block-transition` | warning | A slat's CSS sets a transition on a block (or on an Area's or a Line's path) that drops rhp's own (left and width, bottom and height, a Cell's background-color, a path's d), so the block jumps to new values. Not in the JS version, not with `transition: none`, and not when the slat's CSS places the block itself (a radial layout). Read with motion on. | A transition on an element inside the block, or rhp's properties named too. |
149
+ | `page-css-reaches` | warning | A chart goes without the guard (`noCssGuard`), and a page rule reaches an element rhp makes in it and sets what the guard would have reset there (margin, padding, border, sizes, fonts and text styles, colors, outlines, transitions...), where rhp's own rules don't win. A class name the page and a slat share does it too, and so do the browser's own styles for a slat root that isn't a `div` (a button's border, a `p`'s margins, an `svg`'s overflow). | Aim the rule at the page's own elements, rename the slat's class, use a `div` root, or take `noCssGuard` off. |
137
150
  | `page-css-ignored` | warning | A page rule aimed at a chart (it names an `.rhp-` class, or its subject has a class, an id or an attribute and it reaches only elements in charts) sets properties that rhp's own rules set on every element it reaches. rhp's are `!important` in its layers, so the page's change nothing. | The slat type's `css`, the block's `color`, the Chart's theme. |
138
151
 
139
152
  ### Controls and interactions
@@ -141,8 +154,12 @@ The tests read this table: every code in it must have a chart in `test/broken/`
141
154
  | Code | Level | What triggers it | Fix it gives |
142
155
  |---|---|---|---|
143
156
  | `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. |
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. |
157
+ | `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), or it changes the size of a box around the charts that has a background, a border or a shadow and holds no chart (a readout's card that shrinks to its text, a legend's panel), 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 floats (fixed or absolute: a tooltip) is left out. | A box that keeps its size: a readout on one line with an ellipsis, or a min-height for its longest text; a card or a panel with a fixed width, or a min-width and min-height for its longest content; buttons that keep their width; names that keep their weight and size when lit, or room in px. |
158
+ | `text-shift` | warning | An interaction of the pass, a pick of the sweep, or a moment while a control runs (Play), moves a text around the charts whose own text did not change, by more than 2px, inside the box it floats in or on the page: a label after a value that got wider, a unit after a number in proportional figures, a readout's lines that change order, a legend that reflows, a column that resizes to its widest value. One finding per width, each text at the most it moved; a text that moved with a layout jump is that jump. | Numbers in tabular figures in a column of their own or with a min-width in ch, right-aligned; labels in their own column (a grid), not after a value on one line; lines in a fixed order; a min-height for the readout's longest text. |
159
+ | `jitter` | warning | While the scrub moves the pointer along the chart, a text around the charts moves by more than 2px against the page at rest, inside the box it floats in or on the page: a label or a unit after a value that changed width ("8,101 tons in 2026", where " in 2026" slides), a phrase after a date whose month names differ in width, a sentence whose values change, a line that wraps on some days and not others. One finding per width, each text at the most it moved, named with the line it is part of; a text that moved with a layout jump is that jump, and a text found here is left out of the width's text-shift. | Start each part of the readout where nothing before it changes width: a grid with a column per part, or each part on a line of its own. Values in tabular figures with a min-width in ch for the longest value they can show, right-aligned when they end a column; the readout on its lines whatever it says (nowrap, or a min-height). |
160
+ | `flicker` | warning | With a mouse, between two neighboring slats that each show something of their own, a step of the glide shows the page's text, or the slats, as they are with the pointer away: the pick clears in the gap between two bars and comes back, so the readout or the lit slat blinks as the reader moves along the chart. | Keep the pick while the pointer is between slats: change it only on another slat, clear it only when the pointer leaves the chart or the poster. |
145
161
  | `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). |
162
+ | `scrolls-away` | info | The main chart is taller than three quarters of the window (664px at a phone's width), and with its middle in the middle of the window, all of its section's controls (not its fit and download icons), or all of its readout (the texts a pick rewrites), are out of view: the reader has to scroll back to change the year or to read the values. A note: keeping them in view is usually better, not always. | Consider `position: sticky; top: env(safe-area-inset-top, 0px)` on the row that holds them, with a background and a z-index over the plot's 1, compact on a phone, and `scroll-margin-top` on the slats so a picked slat is never under it. |
146
163
  | `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). |
147
164
 
148
165
  ### Conditional selection repair
@@ -4,11 +4,66 @@
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", pick: "picking" };
7
+ const DOING = { hover: "pointing at", tap: "tapping", click: "clicking", input: "setting", select: "setting", key: "pressing", pick: "picking", scrub: "moving the pointer along" };
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 ");
11
11
 
12
+ // How a box changed size: "grows 18px taller", "shrinks 6px narrower"
13
+ const grows = ({ dw, dh }) => [dh && `${dh > 0 ? "grows" : "shrinks"} ${Math.abs(dh)}px ${dh > 0 ? "taller" : "shorter"}`, dw && `${dw > 0 ? "grows" : "shrinks"} ${Math.abs(dw)}px ${dw > 0 ? "wider" : "narrower"}`].filter(Boolean).join(" and ");
14
+
15
+ // The section of the skill's references that explains a finding's fix, for an agent whose fix did not hold: the
16
+ // report names it ("See: ..."), so the agent reads that section, not the whole file
17
+ const PITFALLS = (title) => `pitfalls.md, "${title}"`;
18
+ export const SEE = {
19
+ "two-solids": PITFALLS("Mistakes that come from other libraries"),
20
+ "cdn-version": PITFALLS("Mistakes that come from other libraries"),
21
+ "no-chart": PITFALLS("Nothing draws, or the chart is empty"),
22
+ "empty-plot": PITFALLS("Nothing draws, or the chart is empty"),
23
+ "missing-import-map": PITFALLS("Nothing draws, or the chart is empty"),
24
+ "plain-script-tag": PITFALLS("Nothing draws, or the chart is empty"),
25
+ "missing-export": PITFALLS("Nothing draws, or the chart is empty"),
26
+ "bad-scale": PITFALLS("Bars, dots or labels in the wrong place"),
27
+ "value-past-scale": PITFALLS("Bars, dots or labels in the wrong place"),
28
+ "points-past-scale": PITFALLS("Bars, dots or labels in the wrong place"),
29
+ "nan-value": PITFALLS("Bars, dots or labels in the wrong place"),
30
+ "dot-size": PITFALLS("Bars, dots or labels in the wrong place"),
31
+ "sticks-out": PITFALLS("Text sticks out, is cut off or overlaps"),
32
+ "text-cut-off": PITFALLS("Text sticks out, is cut off or overlaps"),
33
+ "text-overlap": PITFALLS("Text sticks out, is cut off or overlaps"),
34
+ "text-outside-poster": PITFALLS("Text sticks out, is cut off or overlaps"),
35
+ "page-scrolls-sideways": PITFALLS("Text sticks out, is cut off or overlaps"),
36
+ "tiny-text": PITFALLS("Text sticks out, is cut off or overlaps"),
37
+ "small-plot": PITFALLS("The chart is tiny, squashed or the wrong size"),
38
+ "collapsed-chart": PITFALLS("The chart is tiny, squashed or the wrong size"),
39
+ "cramped": PITFALLS("The chart is tiny, squashed or the wrong size"),
40
+ "plot-outside-chart": PITFALLS("The chart is tiny, squashed or the wrong size"),
41
+ "stale-read": PITFALLS("Values never update, or the chart jumps"),
42
+ "static-chart": PITFALLS("Values never update, or the chart jumps"),
43
+ "style-string": PITFALLS("Values never update, or the chart jumps"),
44
+ "block-transition": PITFALLS("Values never update, or the chart jumps"),
45
+ "bare-boolean": PITFALLS("A click, hover, tap or key does nothing, or the wrong thing"),
46
+ "handler-at-render": PITFALLS("A click, hover, tap or key does nothing, or the wrong thing"),
47
+ "flicker": 'interaction.md, "2. Readout on hover, focus and tap"',
48
+ "selection-reset": 'interaction.md, "2. Readout on hover, focus and tap"',
49
+ "selection-hover-stays": 'interaction.md, "2. Readout on hover, focus and tap"',
50
+ "selection-control-reset": 'interaction.md, "2. Readout on hover, focus and tap"',
51
+ "layout-jump": PITFALLS("The layout jumps when the reader interacts"),
52
+ "text-shift": PITFALLS("The layout jumps when the reader interacts"),
53
+ "jitter": PITFALLS("The layout jumps when the reader interacts"),
54
+ "out-of-view": 'interaction.md, "2. Readout on hover, focus and tap"',
55
+ "scrolls-away": 'interaction.md, "2. Readout on hover, focus and tap"',
56
+ "focus-invisible": 'interaction.md, "9. Keyboard"',
57
+ "page-css-ignored": PITFALLS("Styles or colors do not apply"),
58
+ "page-css-reaches": 'performance.md, "Go without the guard when the page\'s CSS leaves charts alone"',
59
+ "series-past-theme": PITFALLS("Styles or colors do not apply"),
60
+ "class-name": PITFALLS("Styles or colors do not apply"),
61
+ "low-contrast": 'design.md, "4. Color"',
62
+ "faint-marks": 'design.md, "4. Color"',
63
+ "colorblind": 'design.md, "4. Color"',
64
+ "rhp-warning": PITFALLS("Console warnings rhp prints"),
65
+ };
66
+
12
67
  // A finding from what measure() saw in the page
13
68
  export function fromProbe(f, context) {
14
69
 
@@ -94,6 +149,21 @@ export function fromProbe(f, context) {
94
149
  fix: `Style what is in a chart in the slat type's css (slat({ css: \`.bar { ... }\` }, fn))${colors ? ", and give colors with the block's color prop or the Chart's theme" : ""}${font ? ", and the font with the theme's font" : ""}. Page CSS can only set opacity, cursor, filter, transform, z-index and pointer-events there.`,
95
150
  };
96
151
  }
152
+ case "page-css-reaches":
153
+ if (f.browser) {
154
+ return {
155
+ level: "warning",
156
+ code: f.code,
157
+ message: `The chart goes without the guard (noCssGuard), and ${f.count > 1 ? `${f.count} slat roots` : "a slat root"} (<${f.browser}>) keep${f.count > 1 ? "" : "s"} the browser's own ${f.props.join(", ")} for that element${f.browser === "svg" ? ": what it draws outside its box is cut off" : ""}.`,
158
+ fix: `Use a div as the slat's root, or set ${f.props.join(", ")} in the slat type's css, or take noCssGuard off.`,
159
+ };
160
+ }
161
+ return {
162
+ level: "warning",
163
+ code: f.code,
164
+ message: `The chart goes without the guard (noCssGuard), and the page's CSS rule ${f.selector} sets ${f.props.join(", ")} on ${f.what}${f.count > 1 ? ` and ${f.count - 1} more` : ""} in it.`,
165
+ fix: "Aim that rule at the page's own elements, or give the slat a class name the page doesn't use, or take noCssGuard off: without the guard, page CSS reaches what rhp doesn't set itself.",
166
+ };
97
167
  case "value-past-scale": {
98
168
  const where = `${f.cross ? "the Chart's cross scale" : "the Chart's scale"} [${f.min}, ${f.max}]`;
99
169
  const count = f.count > 1 ? ` (${f.count} values)` : "";
@@ -173,7 +243,7 @@ export function fromProbe(f, context) {
173
243
  fix: "Give it visible text, an aria-label (aria-label=\"Play\" on an icon button), or a <label> around an input or a select.",
174
244
  };
175
245
  case "layout-jump": {
176
- const list = f.moved.slice(0, 3).map((m) => `${m.what} moves ${shift(m)}`).join(", ");
246
+ const list = f.moved.slice(0, 3).map((m) => (m.box ? `${m.what} ${grows(m)}` : `${m.what} moves ${shift(m)}`)).join(", ");
177
247
  const plot = f.moved.some((m) => m.plot);
178
248
  return {
179
249
  level: "warning",
@@ -181,7 +251,42 @@ export function fromProbe(f, context) {
181
251
  message: `The layout jumps when ${DOING[f.kind] ?? f.kind} ${f.target}: ${list}${f.moved.length > 3 ? ` and ${f.moved.length - 3} more` : ""}${f.more ? ` (${f.more} more interaction${f.more > 1 ? "s do" : " does"} the same)` : ""}, so what the reader goes to next is no longer where it was.`,
182
252
  fix: plot
183
253
  ? "Keep the plot still while a slat is lit: a lit name that gets wider (bolder, larger) makes room: \"auto\" measure the names again, so keep the names' weight and size and change only their color, or give the room in px (room: { start: 140 })."
184
- : "Keep what the interaction changes in a box that keeps its size: a readout or a headline on one line (white-space: nowrap; overflow: hidden; text-overflow: ellipsis) or with a min-height for its longest text, and buttons that keep their width when their label or weight changes.",
254
+ : "Keep what the interaction changes in a box that keeps its size: a readout or a headline on one line (white-space: nowrap; overflow: hidden; text-overflow: ellipsis) or with a min-height for its longest text, a readout's card or a legend's panel with a fixed width or a min-width and min-height for its longest content, and buttons that keep their width when their label or weight changes.",
255
+ };
256
+ }
257
+ case "text-shift": {
258
+ const list = f.moved.slice(0, 3).map((m) => `${m.what} moves ${shift(m)}`).join(", ");
259
+ return {
260
+ level: "warning",
261
+ code: f.code,
262
+ message: `Text that says the same thing moves when ${DOING[f.kind] ?? f.kind} ${f.target}: ${list}${f.moved.length > 3 ? ` and ${f.moved.length - 3} more` : ""}${f.more ? ` (${f.more} more interaction${f.more > 1 ? "s" : ""} or pick${f.more > 1 ? "s" : ""} move text too)` : ""}. Labels and values that slide as the numbers change make the reader lose their place.`,
263
+ fix: "Give what changes a place that keeps its size, so the text around it stays put: numbers in tabular figures (font-variant-numeric: tabular-nums) in a column of their own or with a min-width in ch, right-aligned; a label in its own column (a grid), not after a value on the same line; a readout's lines in a fixed order; and a min-height for the readout's longest text.",
264
+ };
265
+ }
266
+ case "jitter": {
267
+ const how = (m) => [m.dx && `slides up to ${Math.abs(m.dx)}px ${m.dx > 0 ? "right" : "left"}`, m.dy && `moves up to ${Math.abs(m.dy)}px ${m.dy > 0 ? "down" : "up"}`].filter(Boolean).join(" and ");
268
+ const list = f.moved.slice(0, 3).map((m) => `${m.what}${m.line ? ` (in "${m.line}")` : ""} ${how(m)}`).join(", ");
269
+ return {
270
+ level: "warning",
271
+ code: f.code,
272
+ message: `The readout jitters as the pointer moves along ${f.target}: ${list}${f.moved.length > 3 ? ` and ${f.moved.length - 3} more` : ""}. A text that keeps none of its edges in place while the values change, or moves while it says the same thing, shakes under the reader's eyes at every step along the chart.`,
273
+ fix: "Start each part of the readout where nothing before it changes width: a grid with a column per part, or each part on a line of its own, never a label after a value on the same line. Give each value a box that keeps its size: tabular figures (font-variant-numeric: tabular-nums) with a min-width in ch for the longest value it can show (and the longest date), right-aligned when it ends a column. Keep the readout on its lines whatever it says: white-space: nowrap, or a min-height for its longest text at the narrowest width.",
274
+ };
275
+ }
276
+ case "flicker":
277
+ return {
278
+ level: "warning",
279
+ code: f.code,
280
+ message: `Moving the mouse from ${f.from} to ${f.to}, ${f.readout ? "the readout goes back to what it shows at rest" : "the lit slat goes back to how it looks at rest"} for a moment between them${f.more ? ` (and between ${f.more} more pair${f.more > 1 ? "s" : ""} of neighbors)` : ""}: it flickers as the reader moves along the chart.`,
281
+ fix: "Keep the pick while the pointer is between slats: change it only when the pointer reaches another slat (on a pointer move over no slat, keep the pick), and clear it only when the pointer leaves the chart or the poster, as the readout example in SKILL.md does.",
282
+ };
283
+ case "scrolls-away": {
284
+ const parts = [f.controls.length && `its controls (${f.controls.join(", ")})`, f.readout && `its readout ("${f.readout}")`].filter(Boolean);
285
+ return {
286
+ level: "info",
287
+ code: f.code,
288
+ message: `With the middle of the chart in the middle of the ${f.phone ? "phone's screen" : "window"}, ${parts.join(" and ")} ${parts.length > 1 || f.controls.length ? "are" : "is"} scrolled out of view: a reader going through the chart has to scroll back to ${f.controls.length ? "use them" : "read it"}.`,
289
+ fix: "Consider keeping them in view as the reader scrolls (often better, not always): position: sticky; top: env(safe-area-inset-top, 0px) on the row that holds them, with a background and a z-index over the plot's 1, compact on a phone (one line, the readout under the controls), and scroll-margin-top on the slats so a picked slat is never under it. Leave them where they are when the design needs it.",
185
290
  };
186
291
  }
187
292
  case "out-of-view":
@@ -224,7 +329,7 @@ export function fromProbe(f, context) {
224
329
  }
225
330
  }
226
331
 
227
- const STANDALONE = ["render", "html", "createSignal", "createMemo", "createEffect", "createRoot", "onMount", "onCleanup", "batch", "untrack", "For", "Index", "Show", "createStore", "reconcile", "produce", "unwrap", "Plot", "Scale", "Chart", "Theme", "Axis", "at", "useOrientation", "THEME", "series", "slat", "restyle", "linkedCss", "shape", "Bar", "Dot", "Tick", "Label", "Cell", "Place", "Area", "Line", "Poster", "sortBy", "cycle", "every", "extent", "nice", "stackUp", "shares", "running", "summary", "bins", "density", "animated", "curve", "drawing"];
332
+ const STANDALONE = ["render", "html", "createSignal", "createMemo", "createEffect", "createRoot", "onMount", "onCleanup", "batch", "untrack", "For", "Index", "Show", "createStore", "reconcile", "produce", "unwrap", "Plot", "Scale", "Chart", "Theme", "Axis", "at", "useOrientation", "THEME", "series", "slat", "restyle", "linkedCss", "noCssGuard", "inheritPosition", "shape", "Bar", "Dot", "Tick", "Label", "Cell", "Place", "Area", "Line", "Poster", "sortBy", "cycle", "every", "extent", "nice", "stackUp", "shares", "running", "summary", "bins", "density", "animated", "curve", "drawing"];
228
333
 
229
334
  // What to do about an error the page threw
230
335
  export function fixForError(message, format) {