fix(css,preview): scope utilities under the view root, preview with real boards and foreign css

This commit is contained in:
hh committed 2026-08-29 19:39:19 +02:00
1 parent 0a7742eb5e
commit 8c971e431a
10 files changed
+458 -14

No files matched your search

+48
View File
@@ -0,0 +1,48 @@
// node scripts/css.mjs [--minify] [--watch]
//
// Tailwind writes to a scratch file; the scoped, optionally minified result
// is what lands in styles.css. See scope-css.mjs for why the extra step.
import { spawn, spawnSync } from "node:child_process";
import { readFileSync, watch, writeFileSync } from "node:fs";
import { join } from "node:path";
import { scopeCss } from "./scope-css.mjs";
const root = new URL("..", import.meta.url).pathname;
const args = new Set(process.argv.slice(2));
const minify = args.has("--minify");
const watching = args.has("--watch");
const input = join(root, "src/tailwind.css");
const scratch = join(root, ".tailwind.css");
const output = join(root, "styles.css");
const cli = join(root, "node_modules/.bin/tailwindcss");
function finish() {
const built = readFileSync(scratch, "utf8");
writeFileSync(output, scopeCss(built, { minify }));
console.log(`css: wrote styles.css (${minify ? "minified, " : ""}scoped)`);
}
if (!watching) {
const run = spawnSync(cli, ["-i", input, "-o", scratch], { stdio: "inherit" });
if (run.status !== 0) process.exit(run.status ?? 1);
finish();
} else {
writeFileSync(scratch, "");
const child = spawn(cli, ["-i", input, "-o", scratch, "--watch"], {
stdio: "inherit",
});
let timer = null;
watch(scratch, () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
try {
finish();
} catch (error) {
console.error("css:", error);
}
}, 60);
});
child.on("exit", (code) => process.exit(code ?? 0));
}
+71
View File
@@ -0,0 +1,71 @@
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(.bcal-root, .bcal-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 `.bcal-root`.
*
* Theme variables move from `:root, :host` onto `.bcal-root` for the same
* reason: a later `:root{--spacing:…}` from someone else must not resize us.
*/
const ROOT = "bcal-root";
const SCOPE = {
type: "pseudo-class",
kind: "is",
selectors: [
[{ type: "class", name: ROOT }],
[
{ type: "class", name: ROOT },
{ type: "combinator", value: "descendant" },
{ type: "universal" },
],
],
};
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 [{ type: "class", name: ROOT }];
// `: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({
filename: "styles.css",
code: Buffer.from(code),
minify,
visitor: { Selector: scopeSelector },
});
return result.code.toString();
}