diff --git a/web/src/components/sections/BuildPanel.module.css b/web/src/components/sections/BuildPanel.module.css index a6992ac5b..085c81a8e 100644 --- a/web/src/components/sections/BuildPanel.module.css +++ b/web/src/components/sections/BuildPanel.module.css @@ -170,7 +170,10 @@ text-transform: uppercase; } -.otherPaths { +/* Through .buildPanel, the same element as .panel-body: the global + `.panel-body p` (0,1,1) beat the bare class and set this line at 20px with + a 28px bottom margin. */ +.buildPanel .otherPaths { margin: 16px 0 0; color: var(--pf-ink-muted); font-size: var(--pf-fs-body); diff --git a/web/src/components/sections/PatternPanel.module.css b/web/src/components/sections/PatternPanel.module.css index f47c0b9ef..c99363c1e 100644 --- a/web/src/components/sections/PatternPanel.module.css +++ b/web/src/components/sections/PatternPanel.module.css @@ -280,7 +280,11 @@ line-height: 1.5; } -.advancedNote { +/* .panel-body p is a global rule at (0,1,1), 20px (17px on a phone) with a + 28px bottom margin, and it outranks a bare module class. Both notes are + scoped through .body to win it. At 1440 they were 20px under an 18px title, + and the To hardware block ran 572px. */ +.body .advancedNote { margin: 14px 0 0; color: var(--pf-ink-muted); font-size: var(--pf-fs-row-d); @@ -341,7 +345,7 @@ border-color: var(--pf-cream-2); } -.hardwareNote { +.body .hardwareNote { margin: 0; color: var(--pf-cream); opacity: 0.75; diff --git a/web/src/components/sections/PatternPanel.tsx b/web/src/components/sections/PatternPanel.tsx index e62baf492..402b542de 100644 --- a/web/src/components/sections/PatternPanel.tsx +++ b/web/src/components/sections/PatternPanel.tsx @@ -156,7 +156,7 @@ export default function PatternPanel({ content, isActive }: PatternPanelProps) {

{content.title}

{content.subtitle}

-
+
{content.meta && content.meta.length > 0 && (
Details diff --git a/web/src/components/sections/panelNotes.test.ts b/web/src/components/sections/panelNotes.test.ts new file mode 100644 index 000000000..c8733e8ec --- /dev/null +++ b/web/src/components/sections/panelNotes.test.ts @@ -0,0 +1,57 @@ +// jsdom applies none of the CSS modules, so the rules are read from the files. +// The global `.panel-body p` (app/globals.css) is (0,1,1): 20px, 17px under +// 900px, and a 28px bottom margin. A paragraph styled by one module class loses +// to it. On 24 October, at 1440, the Pattern tab's advanced note and both To +// hardware notes were 20px instead of 14px under an 18px title, the To hardware +// block ran 572px, and the Build tab's other-paths line was 20px instead of 15px. + +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +const read = (name: string) => readFileSync(path.resolve(__dirname, name), "utf8"); + +/** Every selector in the file, comma lists split, comments dropped. */ +function selectors(css: string): string[] { + const bare = css.replace(/\/\*[\s\S]*?\*\//g, ""); + const found: string[] = []; + for (const m of bare.matchAll(/([^{}]+)\{/g)) { + for (const s of m[1].split(",")) found.push(s.trim().replace(/\s+/g, " ")); + } + return found; +} + +// Each one renders inside a `.panel-body`. +const panels = [ + "PatternPanel", + "BuildPanel", + "InsidePanel", + "CasePicker", + "InsideGlobe/BuildCard", +]; + +describe("paragraphs styled by a module class under .panel-body", () => { + for (const panel of panels) { + const tsx = read(`${panel}.tsx`); + const rules = selectors(read(`${panel}.module.css`)); + const names = [...new Set([...tsx.matchAll(/

m[1]))]; + + for (const name of names) { + it(`${panel} .${name} is scoped through a second class`, () => { + expect(rules, `a bare .${name} loses to .panel-body p`).not.toContain(`.${name}`); + const scoped = rules.find((s) => new RegExp(`^\\.(\\w+) \\.${name}$`).test(s)); + expect(scoped, `no ".scope .${name}" rule`).toBeDefined(); + const scope = scoped!.split(" ")[0].slice(1); + expect(tsx, `.${scope} is not on an element in ${panel}.tsx`).toContain(`styles.${scope}`); + }); + } + } + + it("finds the four notes this guards", () => { + const all = panels.flatMap((panel) => + [...read(`${panel}.tsx`).matchAll(/

m[1]), + ); + expect(all).toEqual(expect.arrayContaining(["advancedNote", "hardwareNote", "otherPaths"])); + expect(all.filter((n) => n === "hardwareNote")).toHaveLength(2); + }); +});