/** * Theme-token guard for the team/settings surfaces (startsim-1f3n.1). * * These components ship default Tailwind classes. The only colors an app is * guaranteed to have are the semantic tokens from packages/ui/theme/contract.css * (--primary, --card, --border, --muted-foreground, …). Apps do NOT all define a * numeric palette ramp: apps/foundry defines `--color-primary` with no * `--color-primary-600`, so in Tailwind v4 `bg-primary-600` generates no rule at * all — the Invite member button rendered as white text on a white card, i.e. * invisible. * * Hard-coded `gray-*` / `bg-white` are the same class of bug one step quieter: * the classes exist, but they ignore the app's theme and are unreadable in dark * mode. Shared components must be theme-driven, not palette-driven. */ import { INVITE_DIALOG_DEFAULTS } from '../invite-member-dialog-default-class-names' import { MEMBERS_TABLE_DEFAULTS } from '../members-table-default-class-names' import { ROLE_SELECTOR_DEFAULTS } from '../role-selector-default-class-names' import { PENDING_INVITE_DEFAULTS } from '../pending-invitation-callout-default-class-names' import { LEAVE_DIALOG_DEFAULTS } from '../leave-team-dialog-default-class-names' import { ORG_SWITCHER_DEFAULTS } from '../org-switcher-default-class-names' import { DOMAIN_CLAIM_DEFAULTS } from '../domain-claim-card-default-class-names' import { TEAM_SETTINGS_PAGE_DEFAULTS } from '../pages/team-settings-page-default-class-names' import { DOMAINS_SETTINGS_PAGE_DEFAULTS } from '../pages/domains-settings-page-default-class-names' const MAPS: Record> = { INVITE_DIALOG_DEFAULTS, MEMBERS_TABLE_DEFAULTS, ROLE_SELECTOR_DEFAULTS, PENDING_INVITE_DEFAULTS, LEAVE_DIALOG_DEFAULTS, ORG_SWITCHER_DEFAULTS, DOMAIN_CLAIM_DEFAULTS, TEAM_SETTINGS_PAGE_DEFAULTS, DOMAINS_SETTINGS_PAGE_DEFAULTS, } /** `bg-primary-600`, `text-primary-700`, `focus:border-primary-500`, … */ const PRIMARY_RAMP = /(^|:)[a-z-]*-primary-\d{2,3}\b/ /** `text-gray-500`, `bg-white`, `divide-gray-100`, … — theme-blind surfaces. */ const RAW_SURFACE = /(^|:)(bg|text|border|divide|ring)-(gray|slate|zinc|neutral|stone)-\d{2,3}\b|(^|:)bg-white\b/ function offenders(map: Record, pattern: RegExp): string[] { return Object.entries(map) .flatMap(([slot, value]) => String(value) .split(/\s+/) .filter((cls) => pattern.test(cls)) .map((cls) => `${slot}: ${cls}`), ) } describe('team surfaces are theme-driven', () => { it.each(Object.keys(MAPS))( '%s uses no numeric primary ramp (undefined in token-only apps like foundry)', (name) => { expect(offenders(MAPS[name], PRIMARY_RAMP)).toEqual([]) }, ) it.each(Object.keys(MAPS))('%s uses no theme-blind gray/white surfaces', (name) => { expect(offenders(MAPS[name], RAW_SURFACE)).toEqual([]) }) it('the invite trigger is a filled primary button that is visible on a card', () => { expect(INVITE_DIALOG_DEFAULTS.trigger).toContain('bg-primary') expect(INVITE_DIALOG_DEFAULTS.trigger).toContain('text-primary-foreground') }) it('dialog panels sit on the card surface, not hard-coded white', () => { expect(INVITE_DIALOG_DEFAULTS.panel).toContain('bg-card') expect(LEAVE_DIALOG_DEFAULTS.panel).toContain('bg-card') }) })