From b48a6c48bf5618a98a12616514478b43ac0eb913 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 16 May 2026 23:26:06 +0200 Subject: [PATCH] Validate Eidos semantic color groups --- continue.md | 7 +- src/uix/eidos/active-eidos-config.test.ts | 25 ++++++ src/uix/eidos/index.ts | 4 + src/uix/eidos/lib/config-types.ts | 4 + src/uix/eidos/lib/config.ts | 99 +++++++++++++++++++++++ 5 files changed, 137 insertions(+), 2 deletions(-) diff --git a/continue.md b/continue.md index f8559f00d..b8b85b120 100644 --- a/continue.md +++ b/continue.md @@ -35,10 +35,13 @@ Actualizacion Eidos ecosystem 2026-05-16: - Primitivas estaticas base reforzadas: `space` valida sufijos CSS abiertos, `controlHeight` y `radius` usan key sets canonicos, y `focusRing` valida valores no vacios. +- Grupos semanticos de color (`surface`, `content`, `border`, `focus`) quedan + cerrados con key sets canonicos y valores no vacios antes de generar tokens + de theme. - Validado: - - `npx vitest run src/uix/eidos/active-eidos-config.test.ts` -> 31 tests OK. + - `npx vitest run src/uix/eidos/active-eidos-config.test.ts` -> 32 tests OK. - `npx vitest run src/uix/eidos/active-eidos.test.ts` -> 19 tests OK. - - `npx vitest run src/uix/eidos` -> 7 archivos, 71 tests OK. + - `npx vitest run src/uix/eidos` -> 7 archivos, 72 tests OK. - `npm run check` -> 0 errores, 0 warnings. Actualizacion permutation runner 2026-05-16: diff --git a/src/uix/eidos/active-eidos-config.test.ts b/src/uix/eidos/active-eidos-config.test.ts index 522d1e461..7d36ca609 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -605,6 +605,31 @@ describe('ActiveEidos config', () => { ).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 { diff --git a/src/uix/eidos/index.ts b/src/uix/eidos/index.ts index 8592f16d4..25bad2993 100644 --- a/src/uix/eidos/index.ts +++ b/src/uix/eidos/index.ts @@ -171,15 +171,18 @@ export { BORDER_STYLE_KEYS, BORDER_WIDTH_KEYS, COLOR_ALPHA_STEPS, + BORDER_COLOR_KEYS, COLOR_ROLE_SLOTS, COLOR_ROLES, COLOR_SCALE_STEPS, CONTAINER_WIDTH_KEYS, + CONTENT_COLOR_KEYS, CONTENT_WIDTH_KEYS, DENSITIES, DENSITY_KEYS, DURATION_KEYS, EASE_KEYS, + FOCUS_COLOR_KEYS, HIERARCHY_COLOR_ROLES, ICON_STROKE_WIDTH_KEYS, MOTION_DISTANCE_KEYS, @@ -191,6 +194,7 @@ export { SHADOW_SEMANTIC_KEYS, SIZE_PRIMITIVE_KEYS, SIZES, + SURFACE_COLOR_KEYS, TEXT_SIZES, Z_INDEX_KEYS } from './lib/config-types'; diff --git a/src/uix/eidos/lib/config-types.ts b/src/uix/eidos/lib/config-types.ts index 19dd62ac4..1a9ccaa6f 100644 --- a/src/uix/eidos/lib/config-types.ts +++ b/src/uix/eidos/lib/config-types.ts @@ -67,6 +67,7 @@ export interface SurfaceColorRoles { readonly overlay: string; readonly backdrop: string; } +export const SURFACE_COLOR_KEYS = ['default', 'raised', 'overlay', 'backdrop'] as const; export interface ContentColorRoles { readonly primary: string; @@ -74,17 +75,20 @@ export interface ContentColorRoles { readonly disabled: string; readonly onSolid: string; } +export const CONTENT_COLOR_KEYS = ['primary', 'secondary', 'disabled', 'onSolid'] as const; export interface BorderColorRoles { readonly subtle: string; readonly default: string; readonly strong: string; } +export const BORDER_COLOR_KEYS = ['subtle', 'default', 'strong'] as const; export interface FocusColorRoles { readonly ring: string; readonly ringError: string; } +export const FOCUS_COLOR_KEYS = ['ring', 'ringError'] as const; export interface ColorSemanticSet { readonly roles: ColorRoleMap; diff --git a/src/uix/eidos/lib/config.ts b/src/uix/eidos/lib/config.ts index 2f70d6776..4575d18b7 100644 --- a/src/uix/eidos/lib/config.ts +++ b/src/uix/eidos/lib/config.ts @@ -1,14 +1,17 @@ import { ASPECT_RATIO_KEYS, + BORDER_COLOR_KEYS, BORDER_STYLE_KEYS, BORDER_WIDTH_KEYS, COLOR_ROLES, COLOR_SCALE_STEPS, CONTAINER_WIDTH_KEYS, + CONTENT_COLOR_KEYS, CONTENT_WIDTH_KEYS, DENSITY_KEYS, DURATION_KEYS, EASE_KEYS, + FOCUS_COLOR_KEYS, ICON_STROKE_WIDTH_KEYS, MOTION_DISTANCE_KEYS, MOTION_SCALE_KEYS, @@ -16,9 +19,11 @@ import { RADIUS_KEYS, SHADOW_KEYS, SIZE_PRIMITIVE_KEYS, + SURFACE_COLOR_KEYS, TEXT_SIZES, Z_INDEX_KEYS, type BorderPrimitiveSet, + type ColorSemanticSet, type ColorRole, type ColorRoleDefinition, type ColorScale, @@ -48,11 +53,14 @@ const COLOR_ROLE_SET = new Set(COLOR_ROLES) const ASPECT_RATIO_KEY_SET = new Set(ASPECT_RATIO_KEYS) const BORDER_WIDTH_KEY_SET = new Set(BORDER_WIDTH_KEYS) const BORDER_STYLE_KEY_SET = new Set(BORDER_STYLE_KEYS) +const BORDER_COLOR_KEY_SET = new Set(BORDER_COLOR_KEYS) +const CONTENT_COLOR_KEY_SET = new Set(CONTENT_COLOR_KEYS) const CONTAINER_WIDTH_KEY_SET = new Set(CONTAINER_WIDTH_KEYS) const CONTENT_WIDTH_KEY_SET = new Set(CONTENT_WIDTH_KEYS) const DENSITY_KEY_SET = new Set(DENSITY_KEYS) const DURATION_KEY_SET = new Set(DURATION_KEYS) const EASE_KEY_SET = new Set(EASE_KEYS) +const FOCUS_COLOR_KEY_SET = new Set(FOCUS_COLOR_KEYS) const ICON_STROKE_WIDTH_KEY_SET = new Set(ICON_STROKE_WIDTH_KEYS) const MOTION_DISTANCE_KEY_SET = new Set(MOTION_DISTANCE_KEYS) const MOTION_SCALE_KEY_SET = new Set(MOTION_SCALE_KEYS) @@ -60,6 +68,7 @@ const OPACITY_KEY_SET = new Set(OPACITY_KEYS) const RADIUS_KEY_SET = new Set(RADIUS_KEYS) const SHADOW_KEY_SET = new Set(SHADOW_KEYS) const SIZE_PRIMITIVE_KEY_SET = new Set(SIZE_PRIMITIVE_KEYS) +const SURFACE_COLOR_KEY_SET = new Set(SURFACE_COLOR_KEYS) const TEXT_SIZE_SET = new Set(TEXT_SIZES) const Z_INDEX_KEY_SET = new Set(Z_INDEX_KEYS) const RESPONSIVE_KEY_SET = new Set(['base', 'sm', 'md', 'lg', 'xl', 'xxl']) @@ -103,6 +112,7 @@ export function validateEidosConfig(options: EidosConfig): EidosValidationReport validateThemeKeys(options, issues) validateColorScales(options, issues) validateColorRoles(options, issues) + validateColorSemanticGroups(options, issues) validateTypography(options, issues) validateSizePrimitives(options, issues) validateStaticPrimitives(options, issues) @@ -323,6 +333,95 @@ function validateColorRoles(options: EidosConfig, issues: EidosValidationIssue[] } } +function validateColorSemanticGroups(options: EidosConfig, issues: EidosValidationIssue[]): void { + validateColorSemanticSet('semantics.color', options.semantics.color, issues) + + for (const [themeId, theme] of Object.entries(options.themes ?? {})) { + if (!theme.color) continue + validateColorSemanticSet(`themes.${themeId}.color`, theme.color, issues) + } +} + +function validateColorSemanticSet( + path: string, + color: Partial< + Pick + >, + issues: EidosValidationIssue[] +): void { + validateColorSemanticGroup( + `${path}.surface`, + color.surface, + SURFACE_COLOR_KEYS, + SURFACE_COLOR_KEY_SET, + issues + ) + validateColorSemanticGroup( + `${path}.content`, + color.content, + CONTENT_COLOR_KEYS, + CONTENT_COLOR_KEY_SET, + issues + ) + validateColorSemanticGroup( + `${path}.border`, + color.border, + BORDER_COLOR_KEYS, + BORDER_COLOR_KEY_SET, + issues + ) + validateColorSemanticGroup( + `${path}.focus`, + color.focus, + FOCUS_COLOR_KEYS, + FOCUS_COLOR_KEY_SET, + issues + ) +} + +function validateColorSemanticGroup( + path: string, + group: unknown, + keys: readonly string[], + keySet: Set, + issues: EidosValidationIssue[] +): void { + if (group === undefined) return + if (!isPlainRecord(group)) { + issues.push({ + path, + message: 'color semantic group must be a plain object' + }) + return + } + + for (const key of keys) { + if (!hasOwn(group, key)) { + issues.push({ + path: `${path}.${key}`, + message: 'color semantic groups must define every canonical key' + }) + continue + } + + const value = group[key] + if (typeof value !== 'string' || value.trim() === '') { + issues.push({ + path: `${path}.${key}`, + message: 'color semantic CSS value cannot be empty' + }) + } + } + + for (const key of Object.keys(group)) { + if (keySet.has(key)) continue + issues.push({ + path: `${path}.${key}`, + message: 'unknown color semantic key' + }) + } +} + function validateColorScale( path: string, name: string,