@bezda/rhp-mcp 0.1.7 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bezda/rhp-mcp",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Make charts with rhp from any AI agent: an MCP server with rhp's guide, references and recipes, and a checker that renders a chart in a browser and reports what is wrong, with screenshots",
5
5
  "keywords": [
6
6
  "rhp",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@babel/core": "^7.29.7",
54
- "@bezda/rhp": "^2.0.5",
54
+ "@bezda/rhp": "^2.1.0",
55
55
  "@modelcontextprotocol/sdk": "^1.32.0",
56
56
  "babel-preset-solid": "^1.9.15",
57
57
  "esbuild": "^0.28.2",
@@ -43,7 +43,7 @@ Both `@bezda/rhp` and `@bezda/rhp/standalone` export rhp's whole API:
43
43
  - components: `Chart`, `Plot`, `Scale`, `Axis`, `Theme`, `Poster`
44
44
  - blocks: `Bar`, `Dot`, `Tick`, `Label`, `Cell`, `Place`, `Area`, `Line`
45
45
  - experimental point collection: [`ManyDots`](#manydots-experimental-point-collection), in rhp 2.0.3 and later
46
- - functions: `slat`, `restyle`, `linkedCss`, `shape`, `at`, `useOrientation`, `series`, `cycle`, `sortBy`, `extent`, `every`, `nice`, `stackUp`, `shares`, `running`, `summary`, `bins`, `density`, `animated`, `curve`, `drawing`
46
+ - functions: `slat`, `restyle`, `linkedCss`, `noCssGuard`, `inheritPosition`, `shape`, `at`, `useOrientation`, `series`, `cycle`, `sortBy`, `extent`, `every`, `nice`, `stackUp`, `shares`, `running`, `summary`, `bins`, `density`, `animated`, `curve`, `drawing`
47
47
  - the object `THEME` (the default theme)
48
48
 
49
49
  `@bezda/rhp/standalone` adds these from Solid, and nothing else: `render`, `html`, `createSignal`, `createMemo`, `createEffect`, `createRoot`, `createSelector`, `createComputed`, `on`, `onMount`, `onCleanup`, `batch`, `untrack`, `mergeProps`, `splitProps`, `For`, `Index`, `Show`, `Switch`, `Match`, `createStore`, `reconcile`, `produce`, `unwrap`.
@@ -169,6 +169,7 @@ export default function FruitChart() {
169
169
  | `animate` | `true` or `{ duration, ease, slide }` | off | the JS version of motion (section 12); the Plots inside take it |
170
170
  | `theme` | theme object (section 8) | `THEME` | colors and font, over those of a `<Theme>` around it |
171
171
  | `static` | boolean | `false` | for data that never changes: slats are drawn once and keep no signals; a data change draws them all again, without motion |
172
+ | `noCssGuard` | boolean | `false` | faster, and page CSS reaches the chart (performance.md) |
172
173
  | `cross` | `[min, max]` | none | a second axis across the band (scatter plots, line charts; section 7) |
173
174
  | `crossTicks`, `crossFormat` | like `ticks`, `format` | about 5, the number | the second axis |
174
175
  | `crossGrid` | boolean | `true` | `false` hides only the second axis' grid lines, keeping its numbers. |
@@ -1219,7 +1220,7 @@ Even where page CSS reaches rhp's custom properties, set them in the slat's `css
1219
1220
  | `--rhp-slide-time`, `--rhp-slide-ease` | `.3s`, `ease-in-out` | how slats slide to new places |
1220
1221
  | `--rhp-gutter-max` | `40cqw` | with `room: "auto"`, the widest a name (and the room) can be |
1221
1222
 
1222
- **Variables a slat's CSS reads**: the theme (`--rhp-ink`, `--rhp-muted`, `--rhp-grid`, `--rhp-surface`, `--rhp-series-1`…, `--rhp-positive`, `--rhp-negative`, `--rhp-low`, `--rhp-high`, `--rhp-font`), `--rhp-color` (the block's color), `--rhp-toward-end` (the direction from a Bar's start to its end: `linear-gradient(var(--rhp-toward-end), …)`), `--rhp-p` (where a Dot, Tick, Label or Place sits on the scale, 0 to 1: a Label's room to the scale's end is `calc((1 - var(--rhp-p)) * 100%)`), `--rhp-lo` and `--rhp-hi` (where a Bar, an Area or a Line starts and ends on the scale, 0 to 1, clipped to it: the donut and radial-bars recipes turn a Bar's into angles), `--rhp-min`, `--rhp-max`, a block's `--rhp-from`, `--rhp-to`, `--rhp-at`, `--rhp-value`, a slat's `--rhp-position`, and `--rhp-n` (how many places the slat's Plot has on screen).
1223
+ **Variables a slat's CSS reads**: the theme (`--rhp-ink`, `--rhp-muted`, `--rhp-grid`, `--rhp-surface`, `--rhp-series-1`…, `--rhp-positive`, `--rhp-negative`, `--rhp-low`, `--rhp-high`, `--rhp-font`), `--rhp-color` (the block's color), `--rhp-toward-end` (the direction from a Bar's start to its end: `linear-gradient(var(--rhp-toward-end), …)`), `--rhp-p` (where a Dot, Tick, Label or Place sits on the scale, 0 to 1: a Label's room to the scale's end is `calc((1 - var(--rhp-p)) * 100%)`), `--rhp-lo` and `--rhp-hi` (where a Bar, an Area or a Line starts and ends on the scale, 0 to 1, clipped to it: the donut and radial-bars recipes turn a Bar's into angles), `--rhp-min`, `--rhp-max`, a block's `--rhp-from`, `--rhp-to`, `--rhp-at`, `--rhp-value`, a slat's `--rhp-position` (its place in its Plot, 0 for the first), and `--rhp-n` (how many places the slat's Plot has on screen).
1223
1224
 
1224
1225
  **Styling the axis.** Page CSS and slat CSS cannot reach rhp's grid lines and numbers.
1225
1226
  Their colors are the theme's `grid` and `muted`, their font the theme's `font`; `ticks` and `format` choose them (`format` may return an element with a class, which page CSS styles).
@@ -1682,3 +1683,4 @@ Every one of these was found by running rhp 2.0.1.
1682
1683
  Write the space as `${" "}`, or keep it next to other text (`<b>Name</b>: <em>12</em>`).
1683
1684
  36. **A Plot sits at `z-index: 1`.**
1684
1685
  A page element that overlaps a chart (a readout pinned with `position: sticky`, a menu that opens over the chart) is drawn under the slats unless its own `z-index` is 2 or more.
1686
+ 37. **`var(--rhp-position)` in a `style` prop inside a slat is empty** (rhp 2.1 and later): read it in the slat's `css`.
@@ -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
 
@@ -152,6 +156,7 @@ These are the traps it brings.
152
156
  ## Styles or colors do not apply
153
157
 
154
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".)
155
160
  2. **`className`** [5]: on a block it replaces rhp's class and the block vanishes. Use `class`.
156
161
  3. **The chart ignores the page's font or colors** [20]: give them in the Chart's `theme` (`font`, `ink`, `muted`, `grid`, `series`, ...).
157
162
  4. **`var(--brand)` as a color warns** [20]: put the brand colors in the theme instead.
@@ -167,6 +172,8 @@ These are the traps it brings.
167
172
  14. **A pinned readout or a menu over the chart is drawn under the bars** [36]: give it `z-index: 2` or more.
168
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.
169
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.
170
177
 
171
178
  ## Console warnings rhp prints
172
179
 
@@ -183,6 +190,7 @@ Each is worth fixing [34]:
183
190
  | "--rhp-radius takes one length" | one length, or `--rhp-start-radius` and `--rhp-end-radius` |
184
191
  | "unknown ease" | a CSS name, four cubic-bezier numbers, or a function |
185
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 |
186
194
 
187
195
  ## Mistakes in the work itself
188
196
 
@@ -86,7 +86,7 @@ The tests read this table: every code in it must have a chart in `test/broken/`
86
86
  |---|---|---|---|
87
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. |
88
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. |
89
- | `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}`. |
90
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) => ...`. |
91
91
  | `style-string` | error | `style="..."` (or a string) on a Chart or a Plot, which take only a style object. | `style=${{ "pointer-events": "none" }}`. |
92
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`. |
@@ -146,6 +146,7 @@ The tests read this table: every code in it must have a chart in `test/broken/`
146
146
  |---|---|---|---|
147
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. |
148
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. |
149
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. |
150
151
 
151
152
  ### Controls and interactions
@@ -55,6 +55,7 @@ export const SEE = {
55
55
  "scrolls-away": 'interaction.md, "2. Readout on hover, focus and tap"',
56
56
  "focus-invisible": 'interaction.md, "9. Keyboard"',
57
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"',
58
59
  "series-past-theme": PITFALLS("Styles or colors do not apply"),
59
60
  "class-name": PITFALLS("Styles or colors do not apply"),
60
61
  "low-contrast": 'design.md, "4. Color"',
@@ -148,6 +149,21 @@ export function fromProbe(f, context) {
148
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.`,
149
150
  };
150
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
+ };
151
167
  case "value-past-scale": {
152
168
  const where = `${f.cross ? "the Chart's cross scale" : "the Chart's scale"} [${f.min}, ${f.max}]`;
153
169
  const count = f.count > 1 ? ` (${f.count} values)` : "";
@@ -313,7 +329,7 @@ export function fromProbe(f, context) {
313
329
  }
314
330
  }
315
331
 
316
- 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"];
317
333
 
318
334
  // What to do about an error the page threw
319
335
  export function fixForError(message, format) {
@@ -654,16 +654,29 @@ export function install() {
654
654
  if (past.size) add({ code: "series-past-theme", keys: [...past.keys()].sort((a, b) => a - b), count: [...past.values()].reduce((a, b) => a + b, 0), have });
655
655
  }
656
656
 
657
- // Page CSS that reaches into a chart and sets what rhp's own CSS sets there: it loses, so it changes nothing
657
+ // Page CSS that reaches into a chart and sets what rhp's own CSS sets there: it loses, so it changes nothing. rhp's
658
+ // rules are the ones it adopts and, on a page that links rhp.css (linkedCss), the ones in rhp's layers there.
659
+ const pageSheets = [...document.styleSheets].filter((s) => !s.ownerNode?.matches?.("[data-rhp-layers], [data-rhp-server]"));
660
+ const pageRules = styleRules(pageSheets);
661
+ const rhpRules = [...own, ...pageRules.filter((r) => /^rhp(\.|$)/.test(r.layer))];
658
662
  const byProp = new Map();
659
- for (const r of own) {
663
+ for (const r of rhpRules) {
660
664
  for (const p of r.props) {
661
665
  if (!byProp.has(p)) byProp.set(p, []);
662
666
  byProp.get(p).push(r.selector);
663
667
  }
664
668
  }
665
- const pageSheets = [...document.styleSheets].filter((s) => !s.ownerNode?.matches?.("[data-rhp-layers], [data-rhp-server]"));
666
- for (const r of styleRules(pageSheets)) {
669
+ // A chart without the guard (noCssGuard): page CSS reaches what rhp doesn't set itself on the elements rhp makes.
670
+ // The guard is the rhp rule with display: revert; once a chart says noCssGuard it applies only to charts that don't,
671
+ // and with noCssGuard() there is none.
672
+ const guardRule = rhpRules.find((r) => r.rule.style.getPropertyValue("display") === "revert" && r.selector.includes(".rhp-body"));
673
+ const RHP_MADE = ".rhp-body, .rhp-axis, .rhp-gridline, .rhp-gridline > span, .rhp-plot, .rhp-plot > *, .rhp-bar, .rhp-dot, .rhp-tick, .rhp-label, .rhp-cell, .rhp-place, .rhp-area, .rhp-area path, .rhp-line, .rhp-line path";
674
+ const unguarded = (el) => matches(el, RHP_MADE) && (!guardRule || !matches(el, guardRule.selector));
675
+ // what the guard resets (display, float and box-sizing move nothing there), and values that change nothing
676
+ const GUARDED = /^(margin|padding|border|outline|background|box-shadow|overflow|min-width|min-height|max-width|max-height|width$|height$|vertical-align|transition|animation|text-decoration|user-select|touch-action|font|line-height|letter-spacing|word-spacing|text-transform|text-indent|text-align|text-shadow|color$|white-space|text-wrap|word-break|overflow-wrap|hyphens|line-break)/;
677
+ const harmless = (v) => /^(0|0px|0%|0s|none|auto|visible|baseline|normal|initial|unset|inherit|revert|revert-layer|transparent|currentcolor|rgba\(0, 0, 0, 0\))$/i.test(v.trim());
678
+ const family = (p) => p.match(/^(background|border|margin|padding|font|outline|text-decoration|transition|animation|inset|overflow|grid|flex|columns?|list-style)/)?.[1] ?? p;
679
+ for (const r of pageRules) {
667
680
  if (/^rhp(\.|$)/.test(r.layer) || !r.props.length) continue;
668
681
  let matched = [];
669
682
  try {
@@ -673,16 +686,51 @@ export function install() {
673
686
  }
674
687
  const reached = matched.filter((el) => el.closest(".rhp-chart")).slice(0, 60);
675
688
  if (!reached.length) continue;
689
+ const loose = reached.filter(unguarded);
690
+ const live = r.props.filter((p) => GUARDED.test(p) && !harmless(r.rule.style.getPropertyValue(p)) && loose.some((el) => !(byProp.get(p) ?? []).some((sel) => matches(el, sel))));
691
+ if (live.length) {
692
+ const first = loose.find((el) => live.some((p) => !(byProp.get(p) ?? []).some((sel) => matches(el, sel))));
693
+ add({ code: "page-css-reaches", selector: snippet(r.selector), props: [...new Set(live.map(family))], what: first.matches(".rhp-gridline > span") ? "an axis number" : describe(first), count: loose.length });
694
+ }
676
695
  // only a rule aimed at the chart: one that names rhp's classes, or one with a class, an id or an attribute on its
677
696
  // subject that reaches only elements in charts (a reset such as svg { display: block } is not aimed at it)
678
697
  const subjects = r.selector.split(/,(?![^(]*\))/).map((x) => x.trim().split(/\s*[\s>+~]\s*(?![^(]*\))/).pop());
679
698
  if (!/\.rhp-/.test(r.selector) && !(reached.length === matched.length && subjects.every((x) => /[.#[]/.test(x)))) continue;
680
699
  const dead = r.props.filter((p) => byProp.has(p) && reached.every((el) => byProp.get(p).some((sel) => matches(el, sel))));
681
700
  if (!dead.length) continue;
682
- const family = (p) => p.match(/^(background|border|margin|padding|font|outline|text-decoration|transition|animation|inset|overflow|grid|flex|columns?|list-style)/)?.[1] ?? p;
683
701
  add({ code: "page-css-ignored", selector: snippet(r.selector), props: [...new Set(dead.map(family))], what: describe(reached[0]), count: reached.length });
684
702
  }
685
703
 
704
+ // The browser's own styles for a slat root's element (a button's border, a p's margins, an svg's overflow), which the
705
+ // guard resets: in a chart without it they stay. What no rule (rhp's, a slat's, the page's) and no style attribute
706
+ // sets on the root, and differs from what the guard gives, comes from the browser.
707
+ const setByRule = (el, p) => el.style.getPropertyValue(p) !== "" || (byProp.get(p) ?? []).some((sel) => matches(el, sel))
708
+ || pageRules.some((r) => r.props.includes(p) && matches(el, r.selector));
709
+ const defaults = new Map();
710
+ for (const el of document.querySelectorAll(".rhp-chart .rhp-plot > *")) {
711
+ if (!unguarded(el)) continue;
712
+ const s = getComputedStyle(el);
713
+ const up = getComputedStyle(el.parentElement);
714
+ const sides = ["top", "right", "bottom", "left"];
715
+ const off = [
716
+ ...sides.map((d) => [`margin-${d}`, s.getPropertyValue(`margin-${d}`) !== "0px"]),
717
+ ...sides.map((d) => [`padding-${d}`, s.getPropertyValue(`padding-${d}`) !== "0px"]),
718
+ ...sides.map((d) => [`border-${d}-width`, s.getPropertyValue(`border-${d}-style`) !== "none" && s.getPropertyValue(`border-${d}-width`) !== "0px"]),
719
+ ["text-decoration-line", s.getPropertyValue("text-decoration-line") !== "none"],
720
+ ["background-color", s.getPropertyValue("background-color") !== "rgba(0, 0, 0, 0)"],
721
+ ["overflow-x", el.localName === "svg" && s.getPropertyValue("overflow-x") !== "visible"],
722
+ ...["font-size", "font-weight", "font-family", "color"].map((p) => [p, s.getPropertyValue(p) !== up.getPropertyValue(p)]),
723
+ ].filter(([p, differs]) => differs && !setByRule(el, p)).map(([p]) => family(p.replace(/^border-\w+-width$/, "border").replace(/^overflow-x$/, "overflow")));
724
+ if (!off.length) continue;
725
+ const seen = defaults.get(el.localName) ?? { props: new Set(), count: 0, el };
726
+ off.forEach((p) => seen.props.add(p));
727
+ seen.count++;
728
+ defaults.set(el.localName, seen);
729
+ }
730
+ for (const [tag, d] of defaults) {
731
+ add({ code: "page-css-reaches", browser: tag, props: [...d.props], what: describe(d.el), count: d.count });
732
+ }
733
+
686
734
  // Text too small to read
687
735
  let tiny = null;
688
736
  let tinyCount = 0;
@@ -5,7 +5,7 @@
5
5
  const PASSES = [
6
6
  [["runtime-error", "console-error", "syntax-error", "missing-import", "missing-import-map", "plain-script-tag", "interaction-error", "missing-export"], "no errors thrown"],
7
7
  [["rhp-warning"], "no rhp warnings"],
8
- [["unknown-prop", "bad-prop", "bad-scale", "dot-size", "missing-group", "plot-typo", "group-lengths", "no-component", "bare-boolean", "handler-at-render", "style-string", "class-name", "two-solids", "series-past-theme", "page-css-ignored", "block-transition", "stale-read", "static-chart"], "rhp used as documented"],
8
+ [["unknown-prop", "bad-prop", "bad-scale", "dot-size", "missing-group", "plot-typo", "group-lengths", "no-component", "bare-boolean", "handler-at-render", "style-string", "class-name", "two-solids", "series-past-theme", "page-css-ignored", "page-css-reaches", "block-transition", "stale-read", "static-chart"], "rhp used as documented"],
9
9
  [["no-chart", "empty-plot", "small-plot", "collapsed-chart", "plot-outside-chart"], null],
10
10
  [["nan-value"], "no NaN values"],
11
11
  [["value-past-scale", "points-past-scale"], "values inside the scale"],
@@ -13,10 +13,11 @@ import { closest } from "../guard/rules.js";
13
13
  const STANDALONE = "@bezda/rhp/standalone";
14
14
 
15
15
  // The props of rhp's components (and Solid's Show) that take a boolean, and what "" (false) does to each
16
- const BOOLEANS = { Chart: ["animate", "static"], Plot: ["overlap", "static", "keyboard", "animate"], Area: ["mirror", "smooth"], Line: ["fill", "smooth"], Show: ["keyed"] };
16
+ const BOOLEANS = { Chart: ["animate", "static", "noCssGuard"], Plot: ["overlap", "static", "keyboard", "animate"], Area: ["mirror", "smooth"], Line: ["fill", "smooth"], Show: ["keyed"] };
17
17
  const OFF = {
18
18
  overlap: "its slats don't share one band",
19
19
  static: "the chart is not static",
20
+ noCssGuard: "the chart keeps the guard",
20
21
  keyboard: "its slats take no focus",
21
22
  animate: "the JS version stays off",
22
23
  mirror: "the Area is not mirrored",
@@ -25,7 +25,7 @@ export const SETTING_VALUE = {
25
25
  children: (v) => typeof v === "function",
26
26
  };
27
27
 
28
- export const CHART_PROPS = ["scale", "orientation", "height", "aspect", "ticks", "grid", "format", "animate", "theme", "static", "cross", "crossTicks", "crossGrid", "crossFormat", "label", "id", "class", "role", "style", "ref", "children"];
28
+ export const CHART_PROPS = ["scale", "orientation", "height", "aspect", "ticks", "grid", "format", "animate", "theme", "static", "noCssGuard", "cross", "crossTicks", "crossGrid", "crossFormat", "label", "id", "class", "role", "style", "ref", "children"];
29
29
 
30
30
  export const THEME_KEYS = ["series", "positive", "negative", "ink", "muted", "grid", "surface", "low", "high", "font"];
31
31
 
@@ -58,7 +58,7 @@ export const RENAMES = {
58
58
  Label: { value: "at", x: "at", y: "cross", position: "side", align: "side", text: "children", label: "children" },
59
59
  Area: { data: "points", values: "points", max: "peak", fill: "color", colour: "color", curved: "smooth" },
60
60
  Line: { data: "points", values: "points", max: "peak", curved: "smooth", area: "fill", colour: "color" },
61
- Chart: { domain: "scale", range: "scale", extent: "scale", horizontal: "orientation", vertical: "orientation", direction: "orientation", colors: "theme", palette: "theme", tickFormat: "format", ariaLabel: "label", title: "label" },
61
+ Chart: { domain: "scale", range: "scale", extent: "scale", horizontal: "orientation", vertical: "orientation", direction: "orientation", colors: "theme", palette: "theme", tickFormat: "format", ariaLabel: "label", title: "label", noGuard: "noCssGuard", unguarded: "noCssGuard", noCSSGuard: "noCssGuard" },
62
62
  Poster: { headline: "title", heading: "title", subtitle: "dek", subheading: "dek", subhead: "dek", description: "dek", lede: "dek", standfirst: "dek", source: "note", footnote: "note", credit: "note", credits: "note", caption: "note", footer: "note", eyebrow: "kicker", overline: "kicker", tag: "kicker" },
63
63
  slat: { height: "thickness", width: "thickness", size: "thickness", thick: "thickness", pitch: "thickness", padding: "room", margin: "room", gutter: "room", gutters: "room", gap: "inset", style: "css", styles: "css", class: "css", className: "css" },
64
64
  theme: { colors: "series", palette: "series", accent: "series", text: "ink", foreground: "ink", color: "ink", background: "surface", bg: "surface", paper: "surface", gridline: "grid", gridlines: "grid", axis: "grid", fontFamily: "font", family: "font", typeface: "font" },