@bezda/rhp-mcp 0.1.7 → 0.1.9

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 (50) hide show
  1. package/package.json +2 -2
  2. package/skill/SKILL.md +1 -1
  3. package/skill/recipes/area.html +349 -138
  4. package/skill/recipes/bar.html +349 -138
  5. package/skill/recipes/box-plot.html +349 -138
  6. package/skill/recipes/bubble.html +349 -138
  7. package/skill/recipes/bullet.html +349 -138
  8. package/skill/recipes/candlestick.html +349 -138
  9. package/skill/recipes/column.html +349 -138
  10. package/skill/recipes/dial.html +349 -138
  11. package/skill/recipes/diverging-bars.html +349 -138
  12. package/skill/recipes/donut.html +349 -138
  13. package/skill/recipes/dumbbell.html +349 -138
  14. package/skill/recipes/gantt.html +349 -138
  15. package/skill/recipes/grouped-bars.html +349 -138
  16. package/skill/recipes/heatmap.html +349 -138
  17. package/skill/recipes/histogram.html +349 -138
  18. package/skill/recipes/line.html +349 -138
  19. package/skill/recipes/live.html +349 -138
  20. package/skill/recipes/log-line.html +349 -138
  21. package/skill/recipes/lollipop.html +349 -138
  22. package/skill/recipes/multi-line.html +349 -138
  23. package/skill/recipes/pyramid.html +349 -138
  24. package/skill/recipes/race.html +363 -139
  25. package/skill/recipes/radial-bars.html +349 -138
  26. package/skill/recipes/scatter.html +349 -138
  27. package/skill/recipes/silhouettes.html +349 -138
  28. package/skill/recipes/slope.html +349 -138
  29. package/skill/recipes/sparklines.html +349 -138
  30. package/skill/recipes/stacked-100.html +349 -138
  31. package/skill/recipes/stacked-bars.html +349 -138
  32. package/skill/recipes/strip.html +349 -138
  33. package/skill/recipes/tape.html +415 -165
  34. package/skill/recipes/unit-stack.html +349 -138
  35. package/skill/recipes/violin.html +349 -138
  36. package/skill/recipes/waffle.html +349 -138
  37. package/skill/recipes/waterfall.html +349 -138
  38. package/skill/references/api.md +4 -2
  39. package/skill/references/design.md +13 -0
  40. package/skill/references/download.md +372 -140
  41. package/skill/references/environments.md +1 -1
  42. package/skill/references/interaction.md +2 -0
  43. package/skill/references/performance.md +45 -0
  44. package/skill/references/pitfalls.md +9 -0
  45. package/src/check/README.md +2 -1
  46. package/src/check/findings.js +17 -1
  47. package/src/check/inpage.js +53 -5
  48. package/src/check/report.js +1 -1
  49. package/src/check/source.js +2 -1
  50. package/src/guard/rules.js +2 -2
@@ -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
 
@@ -123,6 +127,7 @@ The checker warns (`layout-jump`) when a control or the chart moves after one of
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.
125
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.
130
+ Tip: when the font's tabular figures do not suit the poster (a slashed or dotted zero), keep its own digits and set each in an equal-width slot (design.md, section 5); a right-aligned number that changes, such as a large date, needs the same.
126
131
  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.
127
132
  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).
128
133
  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.
@@ -152,6 +157,7 @@ These are the traps it brings.
152
157
  ## Styles or colors do not apply
153
158
 
154
159
  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`.
160
+ (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
161
  2. **`className`** [5]: on a block it replaces rhp's class and the block vanishes. Use `class`.
156
162
  3. **The chart ignores the page's font or colors** [20]: give them in the Chart's `theme` (`font`, `ink`, `muted`, `grid`, `series`, ...).
157
163
  4. **`var(--brand)` as a color warns** [20]: put the brand colors in the theme instead.
@@ -167,6 +173,8 @@ These are the traps it brings.
167
173
  14. **A pinned readout or a menu over the chart is drawn under the bars** [36]: give it `z-index: 2` or more.
168
174
  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
175
  Give each a fallback with the app's own value: `"var(--chart-1, oklch(0.646 0.222 41.116))"` (design.md section 8).
176
+ 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"`).
177
+ Rename the slat's class, or aim the page rule at the page's own element.
170
178
 
171
179
  ## Console warnings rhp prints
172
180
 
@@ -183,6 +191,7 @@ Each is worth fixing [34]:
183
191
  | "--rhp-radius takes one length" | one length, or `--rhp-start-radius` and `--rhp-end-radius` |
184
192
  | "unknown ease" | a CSS name, four cubic-bezier numbers, or a function |
185
193
  | "linkedCss() was called, but this page doesn't link" | link `@bezda/rhp/rhp.css`, or remove `linkedCss()` |
194
+ | "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
195
 
187
196
  ## Mistakes in the work itself
188
197
 
@@ -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" },