diff --git a/scripts/scope-css.mjs b/scripts/scope-css.mjs index 0d4a77a..a2df6fb 100644 --- a/scripts/scope-css.mjs +++ b/scripts/scope-css.mjs @@ -59,9 +59,26 @@ function scopeSelector(selector) { if (mentions(selector, ROOT)) { return selector; } - // `*` has to open a compound, and :is() takes that place now. - const rest = selector[0]?.type === "universal" ? selector.slice(1) : selector; - return [SCOPE, ...rest]; + // The scope goes on the subject — the last compound — not the first: + // `.theme-dark .hue-chip` must keep matching with `.theme-dark` on body, + // outside the root. + let at = 0; + for (let i = 0; i < selector.length; i += 1) { + if (selector[i].type === "combinator") { + at = i + 1; + } + } + const head = selector.slice(0, at); + const subject = selector.slice(at); + // `*` and type selectors must open a compound; :is() goes in their place + // or right after them. + if (subject[0]?.type === "universal") { + return [...head, SCOPE, ...subject.slice(1)]; + } + if (subject[0]?.type === "type") { + return [...head, subject[0], SCOPE, ...subject.slice(1)]; + } + return [...head, SCOPE, ...subject]; } /** Rewrites a built stylesheet so its rules only reach the plugin's own DOM. */ diff --git a/tests/scope-css.test.ts b/tests/scope-css.test.ts index 0084b1e..43605d1 100644 --- a/tests/scope-css.test.ts +++ b/tests/scope-css.test.ts @@ -48,6 +48,18 @@ describe("scoping the built stylesheet", () => { ); }); + it("scopes the subject, so an ancestor outside the root still matches", () => { + expect(one(".theme-dark .hue-chip{c:1}")).toBe( + ".theme-dark :is(.beaver-root, .beaver-root *).hue-chip { c: 1; }" + ); + expect(one(".a > button{c:2}")).toBe( + ".a > button:is(.beaver-root, .beaver-root *) { c: 2; }" + ); + expect(one(".a *{c:3}")).toBe( + ".a :is(.beaver-root, .beaver-root *) { c: 3; }" + ); + }); + it("leaves rules that already name the root alone", () => { const reset = ".beaver-root.beaver-root button{cursor:pointer}"; expect(one(reset)).toBe(