fix(css): scope utilities under the view root so tailwind plugins stop outranking each other

This commit is contained in:
hh
2026-08-29 19:39:21 +02:00
parent b46dab194b
commit ab22efff7b
7 changed files with 210 additions and 5 deletions
+67
View File
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import { scopeCss } from "../scripts/scope-css.mjs";
const one = (css: string) => scopeCss(css).replace(/\s+/gu, " ").trim();
describe("scoping the built stylesheet", () => {
it("prefixes a utility with the view root, keeping the root itself in reach", () => {
expect(one(".flex{display:flex!important}")).toBe(
":is(.beaver-root, .beaver-root *).flex { display: flex !important; }"
);
});
it("outranks the same utility from another plugin's Tailwind", () => {
// Class specificity: theirs is (0,1,0); ours must be (0,2,0).
const out = one(".hidden{display:none!important}");
expect(out.startsWith(":is(.beaver-root, .beaver-root *).hidden")).toBe(
true
);
});
it("keeps pseudo-elements at the end where they are valid", () => {
expect(one(".x::placeholder{color:red}")).toBe(
":is(.beaver-root, .beaver-root *).x::placeholder { color: red; }"
);
expect(one(".y::first-letter{t:1}")).toContain(".y:first-letter");
});
it("scopes rules nested in container queries and layers", () => {
const out = one(
"@layer utilities{@container (min-width:30rem){.a{display:inline-block!important}}}"
);
// lightningcss rewrites the query into range syntax; Obsidian's Chromium
// has understood that inside @container since container queries landed.
expect(out).toContain("@container (width >= 30rem)");
expect(out).toContain(":is(.beaver-root, .beaver-root *).a");
});
it("drops a leading universal selector instead of producing `:is()*`", () => {
const out = one("*,:before,::backdrop{--tw-a:initial}");
expect(out).toContain(":is(.beaver-root, .beaver-root *),");
expect(out).toContain(":is(.beaver-root, .beaver-root *):before");
expect(out).not.toContain("*)*");
});
it("moves theme variables from :root onto the view root", () => {
expect(one(":root,:host{--spacing:.25rem}")).toBe(
".beaver-root { --spacing: .25rem; }"
);
});
it("leaves rules that already name the root alone", () => {
const reset = ".beaver-root.beaver-root button{cursor:pointer}";
expect(one(reset)).toBe(
".beaver-root.beaver-root button { cursor: pointer; }"
);
expect(one(".theme-dark .beaver-root{--note:red}")).toBe(
".theme-dark .beaver-root { --note: red; }"
);
});
it("scopes variant selectors without breaking their inner :where()", () => {
const out = one(".dark\\:bg-x:where(.theme-dark,.theme-dark *){b:1}");
expect(out).toBe(
":is(.beaver-root, .beaver-root *).dark\\:bg-x:where(.theme-dark, .theme-dark *) { b: 1; }"
);
});
});