|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
|
import {
|
|
|
|
|
COLOR_ALPHA_STEPS,
|
|
|
|
|
COLOR_SCALE_STEPS,
|
|
|
|
|
EIDOS_CONFIG_DOCUMENT_KIND,
|
|
|
|
|
EIDOS_CONFIG_DOCUMENT_VERSION,
|
|
|
|
|
createActiveEidos,
|
|
|
|
|
createThemeBaseEidosConfig,
|
|
|
|
|
defineEidosConfig,
|
|
|
|
|
EidosCssVariableError,
|
|
|
|
|
EidosConfigDocumentError,
|
|
|
|
|
EidosThemeNotFoundError,
|
|
|
|
|
EidosConfigValidationError,
|
|
|
|
|
extendEidosConfig,
|
|
|
|
|
parseEidosConfigDocument,
|
|
|
|
|
STATIC_CONTROL_HEIGHT,
|
|
|
|
|
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,
|
|
|
|
|
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 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 12 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(Object.keys(THEME_BASE_LIGHT_COLOR_SCALES)).toHaveLength(12)
|
|
|
|
|
expect(Object.keys(THEME_BASE_DARK_COLOR_SCALES)).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.opacity).toEqual(STATIC_OPACITY)
|
|
|
|
|
expect(snapshot.primitives.zIndex).toEqual(STATIC_Z_INDEX)
|
|
|
|
|
expect(snapshot.primitives.border).toEqual(STATIC_PRIMITIVES.border)
|
|
|
|
|
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',
|
|
|
|
|
'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()
|
|
|
|
|
expect(cfg.primitives.typography?.families.primary).toEqual({
|
|
|
|
|
family: 'Inter',
|
|
|
|
|
fallbacks: ['system-ui', 'sans-serif'],
|
|
|
|
|
source: 'local'
|
|
|
|
|
})
|
|
|
|
|
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('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).toEqual({ base: 'xxl', md: 'xxxl' })
|
|
|
|
|
expect(typography.styles.h1.family).toBe('display')
|
|
|
|
|
expect(typography.styles.body.family).toBe('primary')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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<string, string> }).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 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<string, string>)['thin']
|
|
|
|
|
;(cfg.primitives.border as { defaultWidth: string }).defaultWidth = 'missing-width'
|
|
|
|
|
;(cfg.primitives.opacity as Record<string, string>)['ghost'] = '0.4'
|
|
|
|
|
delete (cfg.primitives.zIndex as Record<string, number>)['toast']
|
|
|
|
|
delete (cfg.themes!['base-light'].shadow as Record<string, string>)['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 layout primitive keys', () => {
|
|
|
|
|
const cfg = structuredClone(THEME_BASE_OPTIONS)
|
|
|
|
|
|
|
|
|
|
delete (cfg.primitives.layout!.containerWidth as Record<string, string>)['md']
|
|
|
|
|
;(cfg.primitives.layout!.contentWidth as Record<string, string>)['mega'] = '96rem'
|
|
|
|
|
;(cfg.primitives.layout!.aspectRatio as Record<string, string>)['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 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 explicit alpha color scales against their physical scale', () => {
|
|
|
|
|
const cfg = structuredClone(THEME_BASE_OPTIONS)
|
|
|
|
|
const color = cfg.themes!['base-light'].color as {
|
|
|
|
|
alphaScales: Record<string, ColorScale>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
color.alphaScales = {
|
|
|
|
|
purple: colorScale('purple-alpha'),
|
|
|
|
|
ghost: colorScale('ghost-alpha')
|
|
|
|
|
}
|
|
|
|
|
delete (color.alphaScales.purple as Partial<ColorScale>)['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<ColorScale>)['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 {')
|
|
|
|
|
expect(css).toContain('--space-4: 16px;')
|
|
|
|
|
expect(css).toContain("--font-family-primary: 'Instrument Sans', system-ui, sans-serif;")
|
|
|
|
|
expect(css).toContain('--font-size-xxxl: 80px;')
|
|
|
|
|
expect(css).toContain('--control-height-xxs: 22px;')
|
|
|
|
|
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('--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).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: '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.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('--opacity-disabled: ;')
|
|
|
|
|
expect(css).toContain('--z-index-modal: ;')
|
|
|
|
|
expect(css).toContain('--border-width-thin: ;')
|
|
|
|
|
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('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;')
|
|
|
|
|
expect(css).toContain(
|
|
|
|
|
'--scale-purple-a9: color-mix(in srgb, var(--scale-purple-9) 56%, transparent);'
|
|
|
|
|
)
|
|
|
|
|
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-gray-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('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)
|
|
|
|
|
})
|
|
|
|
|
})
|