Add full shell, replaceable Omics adapter and bilingual interaction
Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
|
||||
const root = fileURLToPath(new URL("../src/", import.meta.url));
|
||||
const files = (directory) => fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) =>
|
||||
entry.isDirectory() ? files(path.join(directory, entry.name)) : [path.join(directory, entry.name)],
|
||||
);
|
||||
const source = (file) => ts.createSourceFile(file, fs.readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
||||
const catalog = new Map();
|
||||
const problems = [];
|
||||
const slots = (value) => [...value.matchAll(/\{([A-Za-z][A-Za-z0-9_]*)\}/g)].map((match) => match[1]).sort().join(",");
|
||||
|
||||
for (const file of files(path.join(root, "i18n/locales"))) {
|
||||
if (!/it.*\.ts$/.test(file)) continue;
|
||||
function visit(node) {
|
||||
if (ts.isPropertyAssignment(node) && ts.isStringLiteral(node.name) && ts.isStringLiteral(node.initializer)) {
|
||||
const key = node.name.text;
|
||||
const value = node.initializer.text;
|
||||
if (slots(key) !== slots(value)) problems.push(`${path.relative(root, file)}: interpolation mismatch for ${key}`);
|
||||
// Shared gettext messages may appear in multiple feature catalogues.
|
||||
// Their meaning and translation must agree regardless of merge order.
|
||||
if (catalog.has(key) && catalog.get(key) !== value) problems.push(`${path.relative(root, file)}: conflicting translation for ${key}`);
|
||||
catalog.set(key, value);
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(source(file));
|
||||
}
|
||||
|
||||
let references = 0;
|
||||
for (const file of files(root)) {
|
||||
if (!/\.tsx?$/.test(file) || /(?:\.test\.|\/test\/|\/i18n\/|prototype)/i.test(file)) continue;
|
||||
const tree = source(file);
|
||||
function visit(node) {
|
||||
if (ts.isCallExpression(node) && /^(t|translate)$/.test(node.expression.getText(tree))) {
|
||||
const arg = node.arguments[0];
|
||||
if (arg && ts.isStringLiteral(arg)) {
|
||||
references++;
|
||||
if (!catalog.has(arg.text)) {
|
||||
const line = tree.getLineAndCharacterOfPosition(arg.getStart(tree)).line + 1;
|
||||
problems.push(`${path.relative(root, file)}:${line}: missing Italian message ${JSON.stringify(arg.text)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(tree);
|
||||
}
|
||||
|
||||
console.log(`${catalog.size} Italian messages; ${references} static translation references checked.`);
|
||||
if (problems.length) {
|
||||
console.error(problems.join("\n"));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import postcss from "postcss";
|
||||
|
||||
const mount = ":where(#root, [data-base-ui-portal])";
|
||||
|
||||
/** Compile our Tailwind base into ordinary selectors, without native @scope. */
|
||||
export default function scopedBase() {
|
||||
return {
|
||||
postcssPlugin: "thoth-scoped-base",
|
||||
OnceExit(root) {
|
||||
root.walkAtRules("thoth-base", boundary => {
|
||||
boundary.walkRules(rule => {
|
||||
// Keyframe selectors are animation offsets, not document selectors.
|
||||
if (rule.parent.type === "atrule" && /keyframes$/.test(rule.parent.name)) return;
|
||||
rule.selector = postcss.list.comma(rule.selector).map(selector => {
|
||||
// Authored tokens already target the mount, including host dark ancestors.
|
||||
if (selector.includes("#root") || selector.includes("[data-base-ui-portal]")) return selector;
|
||||
if (["html", ":host", "body", ":root"].includes(selector)) return mount;
|
||||
if (selector === "*") return `${mount}, ${mount} *`;
|
||||
if (/^::?(?:before|after|backdrop)$/.test(selector)) return `${mount}${selector}, ${mount} ${selector}`;
|
||||
return `${mount} ${selector}`;
|
||||
}).join(", ");
|
||||
});
|
||||
boundary.replaceWith(...boundary.nodes);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import postcss from "postcss";
|
||||
import tailwindcss from "tailwindcss";
|
||||
|
||||
test("compiled application tokens are usable without native CSS scope support", async () => {
|
||||
const source = await readFile(new URL("../src/index.css", import.meta.url), "utf8");
|
||||
const config = (await import("../postcss.config.js")).default;
|
||||
const plugins = Array.isArray(config.plugins) ? config.plugins : [tailwindcss()];
|
||||
const result = await postcss(plugins).process(source, { from: "src/index.css" });
|
||||
let tokens = 0;
|
||||
result.root.walkDecls("--background", declaration => {
|
||||
tokens++;
|
||||
let parent = declaration.parent;
|
||||
while (parent.type !== "root") {
|
||||
assert.notEqual(parent.name, "scope", "native @scope hides the theme on unsupported browsers");
|
||||
assert.notEqual(parent.name, "thoth-base", "the build must expand the scoped reset");
|
||||
parent = parent.parent;
|
||||
}
|
||||
});
|
||||
assert.ok(tokens >= 2, "light and dark root tokens must be emitted");
|
||||
let gutter = false;
|
||||
result.root.walkDecls("--thot-shell-gutter", () => { gutter = true; });
|
||||
assert.ok(gutter, "full shell gutter must survive Tailwind content pruning");
|
||||
let nestedGridTheme = false;
|
||||
result.root.walkDecls("--ag-background-color", declaration => {
|
||||
const selector = declaration.parent.selector;
|
||||
if (selector.includes(".thot-database-grid") && selector.includes(".ag-theme-alpine")) nestedGridTheme = true;
|
||||
});
|
||||
assert.ok(nestedGridTheme, "AG Grid nested theme roots must receive shell colors directly");
|
||||
result.root.walkDecls("box-sizing", declaration => {
|
||||
assert.match(declaration.parent.selector, /#root|data-base-ui-portal/, "reset must not change the host portal");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user