import { describe, expect, it } from 'vitest' import { renderThemeCss } from './lib/render-css' import { COLOR_ALPHA_STEPS, COLOR_SCALE_STEPS, EIDOS_CONFIG_DOCUMENT_KIND, EIDOS_CONFIG_DOCUMENT_VERSION, createActiveEidos, createThemeBaseEidosConfig, defineEidosConfig, EidosCssVariableError, EidosConfigDocumentError, EidosThemeNotFoundError, EidosConfigValidationError, extendEidosConfig, parseEidosConfigFromJson, parseEidosConfigDocument, STATIC_CONTROL_HEIGHT, STATIC_DENSITY, STATIC_LAYOUT, STATIC_OPACITY, STATIC_PRIMITIVES, STATIC_RADIUS, STATIC_SIZE, STATIC_SPACE, STATIC_TYPOGRAPHY, STATIC_Z_INDEX, SHADOW_KEYS, SIZE_PRIMITIVE_KEYS, TEXT_SIZES, THEME_BASE_DARK_COLOR_SCALES, THEME_BASE_DARK_SHADOW, THEME_BASE_LIGHT_COLOR_SCALES, THEME_BASE_LIGHT_COLOR_SEMANTICS, THEME_BASE_OPTIONS, THEME_BASE_RECIPE_TOKENS, type ColorScale, type EidosConfig, type EidosConfigDocument, type EidosConfigPatch } from '.' function createEidos(config: EidosConfig) { return createActiveEidos({ config: config, applyDom: false, themeSource: 'config' }) } function createThemeBaseEidos(patch?: EidosConfigPatch) { return createActiveEidos({ themeBase: patch, applyDom: false, themeSource: 'config' }) } function createEidosFromDocument(document: unknown) { return createActiveEidos({ config: document as EidosConfigDocument, applyDom: false, themeSource: 'config' }) } function createEidosFromJson(json: string) { return createEidosFromDocument(parseEidosConfigDocument(json)) } function cssVariableDeclarations(css: string): Set { return new Set([...css.matchAll(/--[a-z0-9_-]+(?=\s*:)/g)].map(([name]) => name)) } function colorScale(seed: string): ColorScale { return Object.fromEntries( COLOR_SCALE_STEPS.map((step) => [step, `${seed}-${step}`]) ) as ColorScale } function options(): EidosConfig { return { primitives: { color: { scales: { gray: colorScale('gray'), violet: colorScale('violet'), slate: colorScale('slate'), teal: colorScale('teal'), green: colorScale('green'), orange: colorScale('orange'), red: colorScale('red'), purple: colorScale('purple') } } }, semantics: { color: { roles: { primary: 'violet', secondary: 'slate', tertiary: 'gray', neutral: 'gray', affirm: 'teal', fulfill: 'green', risk: 'orange', threat: 'red', loss: 'purple' } } } } } describe('ActiveEidos config', () => { it('accepts the canonical 3 hierarchy + 6 intent color roles', () => { const eidos = createEidos(options()) expect(eidos.validate()).toEqual({ ok: true, issues: [] }) expect(eidos.listColorRoles()).toEqual([ 'primary', 'secondary', 'tertiary', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss' ]) }) it('ships a base theme pair with 31 physical color scales of 12 steps', () => { const eidos = createThemeBaseEidos() expect(eidos.validate()).toEqual({ ok: true, issues: [] }) expect(eidos.listThemes()).toEqual(['base-light', 'base-dark']) expect(eidos.listRecipes()).toContain('tooltip') expect(eidos.getRecipeTokens('tooltip')).toEqual(THEME_BASE_RECIPE_TOKENS.tooltip) expect(eidos.getRecipeTokens('missing')).toBeUndefined() const tooltipTokens = eidos.getRecipeTokens('tooltip') as Record tooltipTokens['content-z'] = '1' expect(eidos.getRecipeTokens('tooltip')?.['content-z']).toBe('76') // Palette library at Radix parity: 12 original + 19 added Radix scales. expect(Object.keys(THEME_BASE_LIGHT_COLOR_SCALES)).toHaveLength(31) expect(Object.keys(THEME_BASE_DARK_COLOR_SCALES)).toHaveLength(31) for (const scale of [ ...Object.values(THEME_BASE_LIGHT_COLOR_SCALES), ...Object.values(THEME_BASE_DARK_COLOR_SCALES) ]) { expect(Object.keys(scale)).toHaveLength(12) } expect(eidos.getColorRoleScale('primary', 'base-light')).toEqual( THEME_BASE_LIGHT_COLOR_SCALES.purple ) expect(eidos.getTheme('base-light')?.color?.surface).toEqual( THEME_BASE_LIGHT_COLOR_SEMANTICS.surface ) expect(eidos.getTheme('base-dark')?.shadow).toEqual(THEME_BASE_DARK_SHADOW) }) it('ships the canonical static primitives in the base options', () => { const eidos = createThemeBaseEidos() const snapshot = eidos.snapshot() expect(snapshot.primitives.space).toEqual(STATIC_SPACE) expect(snapshot.primitives.controlHeight).toEqual(STATIC_CONTROL_HEIGHT) expect(snapshot.primitives.radius).toEqual(STATIC_RADIUS) expect(snapshot.primitives.size).toEqual(STATIC_SIZE) expect(snapshot.primitives.typography).toEqual(STATIC_TYPOGRAPHY) expect(snapshot.primitives.icon).toEqual(STATIC_PRIMITIVES.icon) expect(snapshot.primitives.layout).toEqual(STATIC_LAYOUT) expect(snapshot.primitives.density).toEqual(STATIC_DENSITY) expect(snapshot.primitives.opacity).toEqual(STATIC_OPACITY) expect(snapshot.primitives.zIndex).toEqual(STATIC_Z_INDEX) expect(snapshot.primitives.border).toEqual(STATIC_PRIMITIVES.border) expect(snapshot.recipes).toEqual(THEME_BASE_RECIPE_TOKENS) expect(snapshot.recipes?.tooltip?.['content-z']).toBe('76') expect(Object.keys(STATIC_SIZE)).toEqual([...SIZE_PRIMITIVE_KEYS]) expect(new Set(Object.keys(THEME_BASE_DARK_SHADOW))).toEqual(new Set(SHADOW_KEYS)) expect(Object.keys(STATIC_SPACE)).toEqual([ '0', '1', '2', '3', '4', '5', '6', '7', '8', '10', '12', '16', '0-5', '1-5', '2-5', '3-5', '4-5', '5-5' ]) }) it('extends theme base options without mutating the base constants', () => { const patch = { primitives: { space: { '4': '18px', '20': '80px' }, typography: { families: { primary: { family: 'Inter' } } } }, semantics: { color: { roles: { primary: { scale: 'blue', slots: { solid: '10' } } } } }, themes: { 'base-light': { color: { surface: { default: 'Canvas' } } } } } satisfies EidosConfigPatch const cfg = createThemeBaseEidosConfig(patch) expect(cfg.primitives.space?.['4']).toBe('18px') expect(cfg.primitives.space?.['20']).toBe('80px') expect(THEME_BASE_OPTIONS.primitives.space?.['4']).toBe('16px') expect(THEME_BASE_OPTIONS.primitives.space?.['20']).toBeUndefined() // The patch overrides only `family`; fallbacks/source/faces deep-merge from the // base (a real font swap would also override `faces` with its own files). const mergedPrimary = cfg.primitives.typography?.families.primary expect(mergedPrimary?.family).toBe('Inter') expect(mergedPrimary?.fallbacks).toEqual(['system-ui', 'sans-serif']) expect(mergedPrimary?.source).toBe('local') expect(mergedPrimary?.faces?.length).toBe(3) // Base constant not mutated. expect(THEME_BASE_OPTIONS.primitives.typography?.families.primary.family).toBe( 'Instrument Sans' ) expect(cfg.semantics.color.roles.primary).toEqual({ scale: 'blue', slots: { solid: '10' } }) expect(cfg.themes?.['base-light'].color?.surface?.default).toBe('Canvas') expect(cfg.themes?.['base-light'].color?.surface?.raised).toBe( THEME_BASE_LIGHT_COLOR_SEMANTICS.surface?.raised ) }) it('creates a theme base config from a partial patch', () => { const eidos = createThemeBaseEidos({ semantics: { color: { roles: { primary: 'blue' } } } }) expect(eidos.validate()).toEqual({ ok: true, issues: [] }) expect(eidos.getColorRoleScale('primary', 'base-light')).toEqual( THEME_BASE_LIGHT_COLOR_SCALES.blue ) }) it('clones authored options before storing them in ActiveEidos', () => { const cfg = defineEidosConfig(options()) const extended = extendEidosConfig(cfg, { semantics: { color: { roles: { primary: 'blue' } } } }) expect(cfg.semantics.color.roles.primary).toBe('violet') expect(extended.semantics.color.roles.primary).toBe('blue') }) it('serializes a versioned document for persistence', () => { const eidos = createThemeBaseEidos() const document = eidos.toDocument() const json = eidos.serialize() const parsed = parseEidosConfigDocument(json) expect(document.kind).toBe(EIDOS_CONFIG_DOCUMENT_KIND) expect(document.version).toBe(EIDOS_CONFIG_DOCUMENT_VERSION) expect(parsed.options).toEqual(eidos.snapshot()) expect(createEidosFromDocument(document).snapshot()).toEqual(eidos.snapshot()) expect(createEidosFromJson(json).snapshot()).toEqual(eidos.snapshot()) ;(document.options.semantics.color.roles as { primary: string }).primary = 'blue' expect(eidos.snapshot().semantics.color.roles.primary).toBe('purple') }) it('rejects unsupported persisted document versions', () => { const document = createThemeBaseEidos().toDocument() const nextVersion = { ...document, version: 2 } expect(() => createEidosFromDocument(nextVersion)).toThrow(EidosConfigDocumentError) expect(() => parseEidosConfigDocument('{')).toThrow(EidosConfigDocumentError) }) it('rejects persisted documents with invalid Eidos config options', () => { const invalid = structuredClone(createThemeBaseEidos().toDocument()) as EidosConfigDocument ;(invalid.options.semantics.color.roles as Record).primary = 'missing-scale' expect(() => createEidosFromDocument(invalid)).toThrow(EidosConfigValidationError) expect(() => parseEidosConfigFromJson(JSON.stringify(invalid))).toThrow( EidosConfigValidationError ) }) it('reports malformed persisted config options as validation issues', () => { const malformed = { kind: EIDOS_CONFIG_DOCUMENT_KIND, version: EIDOS_CONFIG_DOCUMENT_VERSION, options: {} } try { createEidosFromDocument(malformed) throw new Error('expected malformed config to fail') } catch (error) { expect(error).toBeInstanceOf(EidosConfigValidationError) expect((error as EidosConfigValidationError).issues.map((issue) => issue.path)).toEqual( expect.arrayContaining(['primitives', 'semantics.color.roles']) ) } expect(() => parseEidosConfigFromJson(JSON.stringify(malformed))).toThrow( EidosConfigValidationError ) }) it('reports malformed optional config groups before deep validation', () => { const malformed = structuredClone(createThemeBaseEidos().toDocument()) ;(malformed.options.primitives as unknown as Record).typography = 'bad' ;(malformed.options.primitives as unknown as Record).shadow = 'bad' ;(malformed.options.themes!['base-light'] as unknown as Record).color = 'bad' ;(malformed.options.recipes as unknown as Record).tooltip = 'bad' try { createEidosFromDocument(malformed) throw new Error('expected malformed optional groups to fail') } catch (error) { expect(error).toBeInstanceOf(EidosConfigValidationError) expect((error as EidosConfigValidationError).issues.map((issue) => issue.path)).toEqual( expect.arrayContaining([ 'primitives.typography', 'primitives.shadow', 'themes.base-light.color', 'recipes.tooltip' ]) ) } }) it('reports malformed typography primitive groups without throwing', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) const typography = cfg.primitives.typography as unknown as Record delete typography.sizes typography.families = 'bad' typography.weights = null typography.styles = ['bad'] const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.map((issue) => issue.path)).toEqual( expect.arrayContaining([ 'primitives.typography.sizes', 'primitives.typography.families', 'primitives.typography.weights', 'primitives.typography.styles' ]) ) }) it('reports incomplete primitive values without throwing', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) delete (cfg.primitives.size!.md as unknown as Record).paddingInline delete (cfg.primitives.typography!.sizes.md as unknown as Record).lineHeight ;(cfg.themes!['base-light'].color!.scales!.purple as unknown as Record< string, unknown >)['9'] = null const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.map((issue) => issue.path)).toEqual( expect.arrayContaining([ 'primitives.size.md.paddingInline', 'primitives.typography.sizes.md.lineHeight', 'themes.base-light.color.scales.purple.9' ]) ) }) it('ships typography families separately from typography styles', () => { const typography = createThemeBaseEidos().snapshot().primitives.typography! expect(Object.keys(typography.families)).toEqual(['primary', 'secondary', 'mono', 'display']) expect(Object.keys(typography.sizes)).toEqual([...TEXT_SIZES]) expect(typography.styles.hero.size).toBe('xxxl') expect(typography.styles.h1.family).toBe('display') expect(typography.styles.body.family).toBe('primary') }) it('generates @font-face blocks from the typography config, deduped by font', () => { const css = createThemeBaseEidos().renderStaticCss() expect(css).toContain('@font-face') expect(css).toContain("font-family: 'Instrument Sans';") expect(css).toContain("src: url('/fonts/air/Lora-Bold.ttf') format('truetype');") expect(css).toContain('font-style: italic;') // Lora is shared by `secondary` + `display` → its 8 faces emit once, not 16. expect((css.match(/font-family: 'Lora';/g) ?? []).length).toBe(8) }) it('emits variable-font output from family axes (weight range + optical sizing)', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) const t = cfg.primitives.typography! // Declare `primary` as a variable font: one face spanning the wght axis + an opsz // axis (enables optical sizing). The face omits an explicit weight on purpose so the // range is derived from the axis. ;(t.families as Record).primary = { ...t.families.primary, axes: { wght: [100, 900], opsz: [8, 144] }, faces: [ { sources: [{ url: '/fonts/variable/InstrumentSans.woff2', format: 'woff2-variations' }] } ] } const css = createEidos(cfg).renderStaticCss() // wght axis → @font-face weight range (a single face spans 100–900) expect(css).toContain('font-weight: 100 900;') // opsz axis → optical sizing enabled in :root expect(css).toContain('font-optical-sizing: auto') }) it('emits the Phase 3 typography scales + optical tracking', () => { const css = createThemeBaseEidos().renderStaticCss() expect(css).toContain('--tracking-tight: -0.02em;') expect(css).toContain('--leading-normal: 1.5;') expect(css).toContain('--font-feature-tabular: "tnum" 1, "lnum" 1;') expect(css).toContain('--measure-normal: 66ch;') // optical tracking baked into the size tokens (display negative, small positive) expect(css).toContain('--font-letter-spacing-xxxl: -0.02em;') expect(css).toContain('--font-letter-spacing-xxs: 0.01em;') }) it('validates typography style references', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) const typography = cfg.primitives.typography! ;(typography.styles.body as { family: string }).family = 'missing-family' ;(typography.styles.code as { weight: string }).weight = 'heavy' ;(typography.styles.h1 as { size: string }).size = 'huge' ;(typography.styles.hero as { size: Record }).size = { tablet: 'xxl' } const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'primitives.typography.styles.body.family') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.typography.styles.code.weight') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.typography.styles.h1.size') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.typography.styles.hero.size.tablet') ).toBe(true) }) it('validates typography keys that become CSS custom properties', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) const typography = cfg.primitives.typography! ;(typography.families as Record)['Bad Family'] = { family: 'Bad' } ;(typography.sizes as Record)['mega'] = { size: '96px', lineHeight: '1', letterSpacing: '0' } ;(typography.weights as Record)['semi bold'] = 600 ;(typography.styles as Record)['Hero Display'] = { size: 'xxl' } const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'primitives.typography.families.Bad Family') ).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.typography.sizes.mega')).toBe( true ) expect( report.issues.some((issue) => issue.path === 'primitives.typography.weights.semi bold') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.typography.styles.Hero Display') ).toBe(true) }) it('validates size primitive references', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) const size = cfg.primitives.size! ;(size.md as { controlHeight: string }).controlHeight = 'missing-height' ;(size.md as { fontSize: string }).fontSize = 'huge' ;(size.md as { iconSize: string }).iconSize = 'missing-icon' ;(size.md as { radius: string }).radius = 'missing-radius' ;(size.md as { gap: string }).gap = '' const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.path === 'primitives.size.md.controlHeight')).toBe( true ) expect(report.issues.some((issue) => issue.path === 'primitives.size.md.fontSize')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.size.md.iconSize')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.size.md.radius')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.size.md.gap')).toBe(true) }) it('validates border, opacity, z-index and shadow primitive keys', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) delete (cfg.primitives.border!.width as Record)['thin'] ;(cfg.primitives.border as { defaultWidth: string }).defaultWidth = 'missing-width' ;(cfg.primitives.opacity as Record)['ghost'] = '0.4' delete (cfg.primitives.zIndex as Record)['toast'] delete (cfg.themes!['base-light'].shadow as Record)['6'] const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.path === 'primitives.border.width.thin')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.border.defaultWidth')).toBe( true ) expect(report.issues.some((issue) => issue.path === 'primitives.opacity.ghost')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.zIndex.toast')).toBe(true) expect(report.issues.some((issue) => issue.path === 'themes.base-light.shadow.6')).toBe(true) }) it('validates static primitive keys and focus ring values', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) ;(cfg.primitives.space as Record)['bad space'] = '4px' delete (cfg.primitives.controlHeight as Record)['md'] ;(cfg.primitives.radius as Record)['huge'] = '32px' ;(cfg.primitives.focusRing as { width: string }).width = '' const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.path === 'primitives.space.bad space')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.controlHeight.md')).toBe( true ) expect(report.issues.some((issue) => issue.path === 'primitives.radius.huge')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.focusRing.width')).toBe(true) }) it('validates layout primitive keys', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) delete (cfg.primitives.layout!.containerWidth as Record)['md'] ;(cfg.primitives.layout!.contentWidth as Record)['mega'] = '96rem' ;(cfg.primitives.layout!.aspectRatio as Record)['video'] = '' ;(cfg.primitives.layout as { containerPaddingInline: string }).containerPaddingInline = '' const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'primitives.layout.containerWidth.md') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.layout.contentWidth.mega') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.layout.aspectRatio.video') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.layout.containerPaddingInline') ).toBe(true) }) it('validates density primitive keys', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) ;(cfg.primitives.density!.spaceScale as Record)['dense'] = '0.7' ;(cfg.primitives.density!.controlScale as Record)['spacious'] = '' const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'primitives.density.spaceScale.dense') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.density.controlScale.spacious') ).toBe(true) }) it('validates motion and icon primitive keys', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) delete (cfg.primitives.motion!.duration as Record)['fast'] ;(cfg.primitives.motion!.ease as Record)['bounce'] = 'ease-out' ;(cfg.primitives.motion as { stagger: string }).stagger = '' delete (cfg.primitives.icon!.size as Record)['md'] ;(cfg.primitives.icon!.strokeWidth as Record)['hairline'] = 1 const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.path === 'primitives.motion.duration.fast')).toBe( true ) expect(report.issues.some((issue) => issue.path === 'primitives.motion.ease.bounce')).toBe( true ) expect(report.issues.some((issue) => issue.path === 'primitives.motion.stagger')).toBe(true) expect(report.issues.some((issue) => issue.path === 'primitives.icon.size.md')).toBe(true) expect( report.issues.some((issue) => issue.path === 'primitives.icon.strokeWidth.hairline') ).toBe(true) }) it('validates recipe token keys and values', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) ;(cfg.recipes as Record>).BadRecipe = { 'ok-token': '1px' } ;(cfg.recipes!.tooltip as Record).BadToken = '1px' ;(cfg.recipes!.tooltip as Record)['empty-token'] = '' const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.path === 'recipes.BadRecipe')).toBe(true) expect(report.issues.some((issue) => issue.path === 'recipes.tooltip.BadToken')).toBe(true) expect(report.issues.some((issue) => issue.path === 'recipes.tooltip.empty-token')).toBe(true) }) it('rejects config CSS values that contain declaration delimiters', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) ;(cfg.recipes!.tooltip as Record)['content-bg'] = 'red; color: transparent' ;(cfg.themes!['base-light'].color!.surface as unknown as Record).default = 'Canvas { color: red }' const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.path === 'recipes.tooltip.content-bg')).toBe(true) expect( report.issues.some((issue) => issue.path === 'themes.base-light.color.surface.default') ).toBe(true) }) it('validates role mappings against every configured theme', () => { const cfg: EidosConfig = { ...THEME_BASE_OPTIONS, themes: { ...THEME_BASE_OPTIONS.themes, brand: { color: { scales: { gray: colorScale('gray') } } } } } const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.path === 'themes.brand.color.scales.purple')).toBe( true ) }) it('validates semantic color role slot overrides', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) ;(cfg.semantics.color.roles as unknown as Record).primary = { scale: 'purple', slots: { solid: '13', madeUp: '9' } } const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'semantics.color.roles.primary.slots.solid') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'semantics.color.roles.primary.slots.madeUp') ).toBe(true) }) it('validates CSS-facing theme and color scale keys', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) ;(cfg.primitives as { color: { scales: Record } }).color = { scales: { 'bad scale': colorScale('bad-scale') } } ;(cfg.themes as Record)['bad theme'] = cfg.themes!['base-light'] ;(cfg.themes!['base-light'].color!.scales as Record)['BadScale'] = colorScale('bad-scale') const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'primitives.color.scales.bad scale') ).toBe(true) expect(report.issues.some((issue) => issue.path === 'themes.bad theme')).toBe(true) expect( report.issues.some((issue) => issue.path === 'themes.base-light.color.scales.BadScale') ).toBe(true) }) it('validates semantic color groups before rendering theme CSS', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) ;(cfg.semantics.color as unknown as Record).surface = { default: 'Canvas' } ;(cfg.themes!['base-light'].color!.content as unknown as Record).disabled = '' ;(cfg.themes!['base-light'].color!.border as unknown as Record).ghost = 'transparent' const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'semantics.color.surface.raised') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'themes.base-light.color.content.disabled') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'themes.base-light.color.border.ghost') ).toBe(true) }) it('validates explicit alpha color scales against their physical scale', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) const color = cfg.themes!['base-light'].color as { alphaScales: Record } color.alphaScales = { purple: colorScale('purple-alpha'), ghost: colorScale('ghost-alpha') } delete (color.alphaScales.purple as Partial)['12'] const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect( report.issues.some((issue) => issue.path === 'themes.base-light.color.alphaScales.purple.12') ).toBe(true) expect( report.issues.some((issue) => issue.path === 'themes.base-light.color.alphaScales.ghost') ).toBe(true) }) it('throws with the validation report when asserted', () => { const cfg = options() delete (cfg.primitives.color!.scales.red as Partial)['12'] const eidos = createEidos(cfg) expect(() => eidos.assertValid()).toThrow(EidosConfigValidationError) }) it('renders static primitive CSS without persisting it', () => { const css = createThemeBaseEidos().renderStaticCss() expect(css).toContain(':root {') // Spacing + control heights scale by BOTH density and the global scaling // zoom; font-size scales by scaling only. At comfortable + scaling=100 all // factors are 1, so rendered sizes are unchanged — the tokens must carry // the vars so [data-density]/[data-scaling] can rebind them. expect(css).toContain('--space-4: calc(16px * var(--density-space-scale) * var(--scaling));') expect(css).toContain("--font-family-primary: 'Instrument Sans', system-ui, sans-serif;") expect(css).toContain( '--font-size-xxxl: calc(clamp(2.5rem, calc(1rem + 5vw), 5rem) * var(--scaling));' ) expect(css).toContain( '--control-height-xxs: calc(22px * var(--density-control-scale) * var(--scaling));' ) expect(css).toContain('--border-width-thin: 1px;') expect(css).toContain('--border-width: var(--border-width-thin);') expect(css).toContain('--border-style: var(--border-style-solid);') expect(css).toContain('--container-width-md: 688px;') expect(css).toContain('--container-padding-inline: var(--space-4);') expect(css).toContain('--content-width-prose: 65ch;') expect(css).toContain('--aspect-ratio-video: 16 / 9;') expect(css).toContain('--density-compact-space-scale: 0.84;') expect(css).toContain('--density-space-scale: var(--density-comfortable-space-scale);') expect(css).toContain("[data-density='compact'] {") // The compact block REBINDS the per-axis scalars that drive rescaling. // Combined with `--space-4: calc(16px * var(--density-space-scale) * …)` and // the 0.84 compact scalar, this is the chain that makes a compact `--space-4` // compute to 13.44px (regression guard for the density-inert bug). expect(css).toContain('--density-space-scale: var(--density-compact-space-scale);') expect(css).toContain('--density-control-scale: var(--density-compact-control-scale);') expect(css).toContain('--density-compact-control-scale: 0.9;') // Global scaling (zoom) axis — Radix parity, orthogonal to density. expect(css).toContain('--scaling-110: 1.1;') expect(css).toContain('--scaling: var(--scaling-100);') expect(css).toContain("[data-scaling='110'] {") expect(css).toContain('--opacity-disabled: 0.48;') expect(css).toContain('--z-index-modal: 700;') expect(css).toContain('--size-md-control-height: var(--control-height-md);') expect(css).toContain('--size-md-font-size: var(--font-size-sm);') expect(css).toContain('--size-md-icon-size: var(--icon-size-md);') expect(css).toContain('--size-md-padding-inline: var(--space-3);') expect(css).toContain('--tooltip-content-z: 76;') expect(css).toContain('--dialog-overlay-bg: var(--color-overlay);') expect(css).toContain('--toggle-height-md: var(--control-height-md);') expect(css).not.toContain('--size-full-control-height') }) it('renders the CSS custom-property contract for external themes', () => { const eidos = createThemeBaseEidos() const contract = eidos.getCssContract() const css = eidos.renderContractCss({ staticSelector: ':root', themeSelector: "[data-theme='external']" }) expect(contract.static).toContainEqual({ name: 'size-md-control-height', cssVar: '--size-md-control-height', scope: 'static', category: 'size', path: 'primitives.size.md.controlHeight' }) expect(contract.static).toContainEqual({ name: 'size-md-font-line-height', cssVar: '--size-md-font-line-height', scope: 'static', category: 'size', path: `primitives.typography.sizes.${STATIC_SIZE.md.fontSize}.lineHeight` }) expect(contract.static).toContainEqual({ name: 'size-md-font-letter-spacing', cssVar: '--size-md-font-letter-spacing', scope: 'static', category: 'size', path: `primitives.typography.sizes.${STATIC_SIZE.md.fontSize}.letterSpacing` }) expect(contract.static).toContainEqual({ name: 'container-width-md', cssVar: '--container-width-md', scope: 'static', category: 'layout.container-width', path: 'primitives.layout.containerWidth.md' }) expect(contract.static).toContainEqual({ name: 'aspect-ratio-video', cssVar: '--aspect-ratio-video', scope: 'static', category: 'layout.aspect-ratio', path: 'primitives.layout.aspectRatio.video' }) expect(contract.static).toContainEqual({ name: 'density-compact-space-scale', cssVar: '--density-compact-space-scale', scope: 'static', category: 'density.space-scale', path: 'primitives.density.spaceScale.compact' }) expect(contract.static).toContainEqual({ name: 'density-space-scale', cssVar: '--density-space-scale', scope: 'static', category: 'density.active', path: 'primitives.density.spaceScale' }) expect(contract.static).toContainEqual({ name: 'tooltip-content-z', cssVar: '--tooltip-content-z', scope: 'static', category: 'recipe.tooltip', path: 'recipes.tooltip.content-z' }) expect(contract.theme).toContainEqual({ name: 'color-primary-solid', cssVar: '--color-primary-solid', scope: 'theme', category: 'color.role', path: 'semantics.color.roles.primary.slots.solid' }) expect(contract.theme).toContainEqual({ name: 'primitive-primary-a9', cssVar: '--primitive-primary-a9', scope: 'theme', category: 'color.primitive.alpha', path: 'semantics.color.roles.primary.alpha.9' }) expect(contract.static.some((token) => token.name === 'size-full-control-height')).toBe(false) expect(css).toContain(':root {') expect(css).toContain("[data-theme='external'] {") expect(css).toContain('--space-4: ;') expect(css).toContain('--font-size-xxxl: ;') expect(css).toContain('--size-md-control-height: ;') expect(css).toContain('--container-width-md: ;') expect(css).toContain('--content-width-prose: ;') expect(css).toContain('--aspect-ratio-video: ;') expect(css).toContain('--density-compact-space-scale: ;') expect(css).toContain('--density-space-scale: ;') expect(css).toContain('--opacity-disabled: ;') expect(css).toContain('--z-index-modal: ;') expect(css).toContain('--border-width-thin: ;') expect(css).toContain('--tooltip-content-z: ;') expect(css).toContain('--scale-purple-9: ;') expect(css).toContain('--scale-purple-a9: ;') expect(css).toContain('--primitive-primary-9: ;') expect(css).toContain('--primitive-primary-a9: ;') expect(css).toContain('--color-primary-solid-hover: ;') expect(css).toContain('--color-surface-default: ;') expect(css).toContain('--focus-ring-color-error: ;') expect(css).toContain('--shadow-overlay: ;') expect(css).toContain('--shadow-6: ;') expect(css).not.toContain('--size-full-control-height') }) it('keeps the generated CSS renderers aligned with the CSS contract', () => { const eidos = createThemeBaseEidos() const contract = eidos.getCssContract() const staticDeclarations = cssVariableDeclarations(eidos.renderStaticCss()) const themeDeclarations = cssVariableDeclarations(eidos.renderThemeCss('base-light')) for (const token of contract.static) { expect(staticDeclarations.has(token.cssVar), token.cssVar).toBe(true) } for (const token of contract.theme) { expect(themeDeclarations.has(token.cssVar), token.cssVar).toBe(true) } }) it('renders contract-aware runtime CSS variables', () => { const eidos = createThemeBaseEidos() const css = eidos.renderCssVariables( { '--color-primary-solid': 'rebeccapurple', 'size-md-control-height': '40px' }, { selector: ':root' } ) expect(css).toContain(':root {') expect(css).toContain('--color-primary-solid: rebeccapurple;') expect(css).toContain('--size-md-control-height: 40px;') expect(() => eidos.renderCssVariables({ '--unknown-token': '1px' })).toThrow( EidosCssVariableError ) expect(eidos.renderCssVariables({ '--app-local-token': '1px' }, { strict: false })).toContain( '--app-local-token: 1px;' ) }) it('renders themed color CSS from physical palettes and semantic roles', () => { const css = createThemeBaseEidos().renderThemeCss('base-light') expect(css).toContain(':root {') expect(css).toContain('--scale-purple-9: #8e4ec6;') // Alpha is GENERATED as the compositing inverse of the solid step over the // scale's background (white for light scales): rgb(92 0 173 / 0.6941) over // white reproduces purple-9 (#8e4ec6). (audit P1-1) expect(css).toContain('--scale-purple-a9: rgb(92 0 173 / 0.6941);') expect(css).toContain('--primitive-primary-9: var(--scale-purple-9);') expect(css).toContain('--primitive-primary-a9: var(--scale-purple-a9);') expect(css).toContain('--primitive-tertiary-9: var(--scale-indigo-9);') expect(css).toContain('--color-primary-solid: var(--primitive-primary-9);') expect(css).toContain('--color-primary-solid-hover: var(--primitive-primary-10);') expect(css).toContain('--color-surface-default: var(--primitive-neutral-1);') expect(css).toContain('--focus-ring-color: color-mix(in srgb, var(--primitive-primary-8)') expect(css).toContain('--shadow-3: 0 8px 24px rgb(15 23 42 / 0.12);') expect(css).toContain('--shadow-raised: var(--shadow-3);') }) it('resolves the per-role contrast slot to on-solid by default and to an explicit step when overridden', () => { const css = createThemeBaseEidos().renderThemeCss('base-light') // Default `contrast` → the theme's on-solid ink (white-on-solid that // inverts per mode), NOT the illegible step-12 fill text. Regression guard // for the contrast-illegibility fix (THEMING §20.2). expect(css).toContain( '--color-primary-contrast: var(--color-content-on-solid, var(--primitive-primary-12));' ) // `neutral` overrides `slots.contrast: '12'` because gray-9 is a mid-tone // solid: the contrast ink must invert per mode (near-black light / // near-white dark) to stay legible — AA in both modes (audit P0-2). expect(css).toContain('--color-neutral-contrast: var(--primitive-neutral-12);') }) it('renders dark-theme color CSS with the dark palette and role tokens', () => { const css = createThemeBaseEidos().renderThemeCss('base-dark') // The dark palette is genuinely dark-specific: gray-9 is darker than light // (#8d8d8d) and the text ink gray-12 inverts to near-white (light is #202020). expect(css).toContain('--scale-gray-9: #6e6e6e;') expect(css).toContain('--scale-gray-12: #eeeeee;') // Role tokens derive from the dark palette; the neutral contrast override // holds in dark too (var ref resolves to ~#eee — the P0-2 fix). expect(css).toContain('--primitive-neutral-9: var(--scale-gray-9);') expect(css).toContain('--color-neutral-contrast: var(--primitive-neutral-12);') }) it('rejects a malformed hex color value in a scale step', () => { const report = createThemeBaseEidos({ themes: { 'base-light': { color: { scales: { purple: { ...THEME_BASE_LIGHT_COLOR_SCALES.purple, '9': '#ggg' } } } } } }).validate() expect(report.ok).toBe(false) expect(report.issues.some((issue) => issue.message.includes('invalid hex color'))).toBe(true) }) it('rejects a theme whose effective surface colors are undefined', () => { const cfg = structuredClone(THEME_BASE_OPTIONS) // base-light defines surface; the global semantics.color does not. Removing // it from the theme leaves the effective surface set undefined → the render // would emit dangling `var(--color-surface-*)`. Validation must catch it. delete (cfg.themes!['base-light'].color as unknown as Record).surface const report = createEidos(cfg).validate() expect(report.ok).toBe(false) expect(report.issues.map((issue) => issue.path)).toEqual( expect.arrayContaining(['themes.base-light.color.surface']) ) }) it('emits TSC v2.2 multi-part and cross-recipe composition selectors', () => { const css = createThemeBaseEidos().renderStaticCss() // Multi-part scope: a `parts: ['trigger','content']` token emits a // comma-joined selector targeting both parts (select's content portals out // of the trigger's subtree, so its color must scope to both). (audit B4) expect(css).toContain( "[data-select-trigger][data-color='affirm'], [data-select-content][data-color='affirm']" ) // Cross-recipe composition: toggle-group overrides toggle's palette tokens, // scoped to its own cascade + a descendant target selector. expect(css).toContain("[data-toggle-group][data-color='primary'] [data-toggle-group-item]") }) it('renders raw theme CSS with a selector derived only from the theme id', () => { const css = renderThemeCss(THEME_BASE_OPTIONS, 'base-light') expect(css).toContain("[data-theme='base-light'] {") expect(css).not.toContain(':root') }) it('renders explicit alpha color scales when a theme provides them', () => { const eidos = createThemeBaseEidos({ themes: { 'base-light': { color: { alphaScales: { purple: colorScale('purple-alpha') } } } } }) const css = eidos.renderThemeCss('base-light') expect(COLOR_ALPHA_STEPS).toEqual(COLOR_SCALE_STEPS) expect(css).toContain('--scale-purple-a4: purple-alpha-4;') expect(css).toContain('--primitive-primary-a4: var(--scale-purple-a4);') }) it('throws when rendering a missing theme', () => { const eidos = createThemeBaseEidos() expect(() => eidos.renderThemeCss('missing')).toThrow(EidosThemeNotFoundError) }) })