/** * rtl-check — the mechanical guard of the build contract's RTL row * (docs/guides/component-guide.md). Until now that row's guard was * "rule (LIVE)", i.e. human review, and it is how the Slider defect of * `013ceac57` reached production: three vertical rules paired a logical * `inset-inline-start` with a physical `translateX`, and nothing in CI looked * at CSS declarations — `eidos-lint` classifies selectors. * * RTL-1 a physical inline-axis displacement (`transform: translateX/ * translate/translate3d`, or the independent `translate` property) in * a block that also anchors on `inset-inline*` / `margin-inline*`. * The anchor flips with the writing direction, the displacement does * not: correct in LTR, a full element-width off-axis in RTL. * * RTL-2 inside a `:dir()` block, one logical face turned off and the other * repainted. The property had ALREADY mirrored, so relocating it * cancels the mirror and parks it opposite the siblings left alone. * This is the shape that survived the `[dir='rtl']` → `:dir(rtl)` * migration untouched, because that pass rewrote selectors and never * read a body — a mechanical migration inherits what it translates. * * Both rules, their deliberate exclusions and their escape hatches * (`rtl-physical:` / `rtl-mirror:`) live in `src/uix/eidos/rtl-lint.ts`. */ import { readdirSync, readFileSync, statSync } from 'node:fs'; import { join, relative } from 'node:path'; import { lintRtlGeometry, lintRtlMirror } from '../src/uix/eidos/rtl-lint'; const REPO = process.cwd(); const EIDOS_DIR = join(REPO, 'src', 'uix', 'eidos'); function walk(dir: string, out: string[] = []): string[] { for (const entry of readdirSync(dir)) { const path = join(dir, entry); if (statSync(path).isDirectory()) walk(path, out); else if (entry.endsWith('.css')) out.push(path); } return out; } function main(): void { const files = walk(EIDOS_DIR); let errors = 0; for (const file of files) { const rel = relative(REPO, file).replace(/\\/g, '/'); const css = readFileSync(file, 'utf8'); for (const f of lintRtlGeometry(css)) { errors++; console.error( `ERROR [RTL-1] ${rel}:${f.line} — physical "${f.translate}" against logical ` + `${f.anchors.map((a) => `"${a}"`).join(' + ')} in ${f.selector}` ); } for (const f of lintRtlMirror(css)) { errors++; console.error( `ERROR [RTL-2] ${rel}:${f.line} — double flip: "${f.neutral}" turns a face off and ` + `"${f.payload}" repaints the other, but ${f.family} had already mirrored ` + `in ${f.selector}` ); } } if (errors > 0) { console.error( `\nrtl-check: ${errors} error(s) across ${files.length} files\n` + 'RTL-1: express the offset logically (margin-inline-start: calc(…)), go fully\n' + 'physical (left/right), or mark the line /* rtl-physical: */ if meant.\n' + 'RTL-2: DELETE the rule and check the base declaration — it had already mirrored.' ); process.exit(1); } console.log(`rtl-check: 0 error(s) across ${files.length} files`); } main();