fix(css): scope utilities under the view root so tailwind plugins stop outranking each other
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { transform } from "lightningcss";
|
||||
|
||||
/**
|
||||
* Obsidian loads every plugin's stylesheet into one document, and Tailwind
|
||||
* puts its utilities into a cascade layer called `utilities` in each of them.
|
||||
* Layers with the same name merge, so another Tailwind plugin's
|
||||
* `.hidden{display:none!important}` and this plugin's
|
||||
* `@container … .\@min-\[30rem\]\:inline-block{…!important}` end up in the
|
||||
* same layer with the same specificity, and whichever plugin loaded last
|
||||
* wins. Prefixing every selector with `:is(.beaver-root, .beaver-root *)` adds a
|
||||
* class of specificity that no unscoped utility can match, whatever the load
|
||||
* order, while still reaching the view root itself and the portal layer,
|
||||
* both of which carry `.beaver-root`.
|
||||
*
|
||||
* Theme variables move from `:root, :host` onto `.beaver-root` for the same
|
||||
* reason: a later `:root{--spacing:…}` from someone else must not resize us.
|
||||
*/
|
||||
|
||||
const ROOT = "beaver-root";
|
||||
|
||||
const SCOPE = {
|
||||
kind: "is",
|
||||
selectors: [
|
||||
[{ name: ROOT, type: "class" }],
|
||||
[
|
||||
{ name: ROOT, type: "class" },
|
||||
{ type: "combinator", value: "descendant" },
|
||||
{ type: "universal" },
|
||||
],
|
||||
],
|
||||
type: "pseudo-class",
|
||||
};
|
||||
|
||||
function isPseudo(selector, kind) {
|
||||
return (
|
||||
selector.length === 1 &&
|
||||
selector[0].type === "pseudo-class" &&
|
||||
selector[0].kind === kind
|
||||
);
|
||||
}
|
||||
|
||||
function mentions(selector, name) {
|
||||
for (const part of selector) {
|
||||
if (part.type === "class" && part.name === name) {
|
||||
return true;
|
||||
}
|
||||
if (part.selectors?.some((inner) => mentions(inner, name))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function scopeSelector(selector) {
|
||||
if (isPseudo(selector, "root")) {
|
||||
return [{ name: ROOT, type: "class" }];
|
||||
}
|
||||
// `:root, :host` is one rule; the root alone carries it, so :host goes.
|
||||
if (isPseudo(selector, "host")) {
|
||||
return [];
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
||||
/** Rewrites a built stylesheet so its rules only reach the plugin's own DOM. */
|
||||
export function scopeCss(code, { minify = false } = {}) {
|
||||
// The selector visitor, not the rule one: handing a whole rule back makes
|
||||
// lightningcss re-read its declarations, and some of Tailwind's it cannot.
|
||||
const result = transform({
|
||||
code: Buffer.from(code),
|
||||
filename: "styles.css",
|
||||
minify,
|
||||
visitor: { Selector: scopeSelector },
|
||||
});
|
||||
return result.code.toString();
|
||||
}
|
||||
Reference in New Issue
Block a user