/** * Package-wide theme-contract guard (startsim-1f3n.3). * * A shared component may only use colors that packages/ui/theme/contract.css * guarantees — the SEMANTIC tokens (`primary`, `card`, `border`, `ring`, …). * The numeric Tailwind ramp (`bg-primary-600`, `text-primary-700`, …) is NOT * part of the contract, and Tailwind v4 emits no rule at all for a color it * has no `--color-*` entry for. That is how the Foundry control plane ended up * with an invisible "Invite member" button and a blank active-nav pill: the app * declares `--color-primary` and never declared the ramp. * * Requiring every app to also declare a ramp is the wrong fix — it makes N apps * responsible for a promise this package should not have made. So the ramp is * banned here instead, and this test keeps it out. * * Need a lighter/darker brand shade? Use an opacity modifier on the token: * bg-primary-50 -> bg-primary/10 text-primary-700 -> text-primary * hover:bg-primary-700 -> hover:bg-primary/90 */ import * as fs from 'fs' import * as path from 'path' const SRC = path.resolve(__dirname, '..') /** `bg-primary-600`, `hover:text-primary-700`, `focus:ring-primary-500`, … */ const PRIMARY_RAMP = /\b[a-z-]*-primary-\d{2,3}\b/ function sourceFiles(dir: string, acc: string[] = []): string[] { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name) if (entry.isDirectory()) { if (entry.name === '__tests__' || entry.name === 'node_modules') continue sourceFiles(full, acc) } else if (/\.tsx?$/.test(entry.name)) { acc.push(full) } } return acc } describe('packages/ui only uses colors the theme contract guarantees', () => { it('no component styles against the numeric primary ramp', () => { const offenders: string[] = [] for (const file of sourceFiles(SRC)) { fs.readFileSync(file, 'utf8') .split('\n') .forEach((line, i) => { // Prose in a doc comment may legitimately name the banned class. const code = line.replace(/^\s*(\*|\/\/).*$/, '') const hit = code.match(PRIMARY_RAMP) if (hit) offenders.push(`${path.relative(SRC, file)}:${i + 1} ${hit[0]}`) }) } expect(offenders).toEqual([]) }) })