Validate Eidos semantic color groups

active-uix
dev 5 months ago
parent cfee72bfc8
commit b48a6c48bf

@ -35,10 +35,13 @@ Actualizacion Eidos ecosystem 2026-05-16:
- Primitivas estaticas base reforzadas: `space` valida sufijos CSS abiertos, - Primitivas estaticas base reforzadas: `space` valida sufijos CSS abiertos,
`controlHeight` y `radius` usan key sets canonicos, y `focusRing` valida `controlHeight` y `radius` usan key sets canonicos, y `focusRing` valida
valores no vacios. 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: - 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/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. - `npm run check` -> 0 errores, 0 warnings.
Actualizacion permutation runner 2026-05-16: Actualizacion permutation runner 2026-05-16:

@ -605,6 +605,31 @@ describe('ActiveEidos config', () => {
).toBe(true) ).toBe(true)
}) })
it('validates semantic color groups before rendering theme CSS', () => {
const cfg = structuredClone(THEME_BASE_OPTIONS)
;(cfg.semantics.color as unknown as Record<string, unknown>).surface = {
default: 'Canvas'
}
;(cfg.themes!['base-light'].color!.content as unknown as Record<string, string>).disabled =
''
;(cfg.themes!['base-light'].color!.border as unknown as Record<string, string>).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', () => { it('validates explicit alpha color scales against their physical scale', () => {
const cfg = structuredClone(THEME_BASE_OPTIONS) const cfg = structuredClone(THEME_BASE_OPTIONS)
const color = cfg.themes!['base-light'].color as { const color = cfg.themes!['base-light'].color as {

@ -171,15 +171,18 @@ export {
BORDER_STYLE_KEYS, BORDER_STYLE_KEYS,
BORDER_WIDTH_KEYS, BORDER_WIDTH_KEYS,
COLOR_ALPHA_STEPS, COLOR_ALPHA_STEPS,
BORDER_COLOR_KEYS,
COLOR_ROLE_SLOTS, COLOR_ROLE_SLOTS,
COLOR_ROLES, COLOR_ROLES,
COLOR_SCALE_STEPS, COLOR_SCALE_STEPS,
CONTAINER_WIDTH_KEYS, CONTAINER_WIDTH_KEYS,
CONTENT_COLOR_KEYS,
CONTENT_WIDTH_KEYS, CONTENT_WIDTH_KEYS,
DENSITIES, DENSITIES,
DENSITY_KEYS, DENSITY_KEYS,
DURATION_KEYS, DURATION_KEYS,
EASE_KEYS, EASE_KEYS,
FOCUS_COLOR_KEYS,
HIERARCHY_COLOR_ROLES, HIERARCHY_COLOR_ROLES,
ICON_STROKE_WIDTH_KEYS, ICON_STROKE_WIDTH_KEYS,
MOTION_DISTANCE_KEYS, MOTION_DISTANCE_KEYS,
@ -191,6 +194,7 @@ export {
SHADOW_SEMANTIC_KEYS, SHADOW_SEMANTIC_KEYS,
SIZE_PRIMITIVE_KEYS, SIZE_PRIMITIVE_KEYS,
SIZES, SIZES,
SURFACE_COLOR_KEYS,
TEXT_SIZES, TEXT_SIZES,
Z_INDEX_KEYS Z_INDEX_KEYS
} from './lib/config-types'; } from './lib/config-types';

@ -67,6 +67,7 @@ export interface SurfaceColorRoles {
readonly overlay: string; readonly overlay: string;
readonly backdrop: string; readonly backdrop: string;
} }
export const SURFACE_COLOR_KEYS = ['default', 'raised', 'overlay', 'backdrop'] as const;
export interface ContentColorRoles { export interface ContentColorRoles {
readonly primary: string; readonly primary: string;
@ -74,17 +75,20 @@ export interface ContentColorRoles {
readonly disabled: string; readonly disabled: string;
readonly onSolid: string; readonly onSolid: string;
} }
export const CONTENT_COLOR_KEYS = ['primary', 'secondary', 'disabled', 'onSolid'] as const;
export interface BorderColorRoles { export interface BorderColorRoles {
readonly subtle: string; readonly subtle: string;
readonly default: string; readonly default: string;
readonly strong: string; readonly strong: string;
} }
export const BORDER_COLOR_KEYS = ['subtle', 'default', 'strong'] as const;
export interface FocusColorRoles { export interface FocusColorRoles {
readonly ring: string; readonly ring: string;
readonly ringError: string; readonly ringError: string;
} }
export const FOCUS_COLOR_KEYS = ['ring', 'ringError'] as const;
export interface ColorSemanticSet { export interface ColorSemanticSet {
readonly roles: ColorRoleMap; readonly roles: ColorRoleMap;

@ -1,14 +1,17 @@
import { import {
ASPECT_RATIO_KEYS, ASPECT_RATIO_KEYS,
BORDER_COLOR_KEYS,
BORDER_STYLE_KEYS, BORDER_STYLE_KEYS,
BORDER_WIDTH_KEYS, BORDER_WIDTH_KEYS,
COLOR_ROLES, COLOR_ROLES,
COLOR_SCALE_STEPS, COLOR_SCALE_STEPS,
CONTAINER_WIDTH_KEYS, CONTAINER_WIDTH_KEYS,
CONTENT_COLOR_KEYS,
CONTENT_WIDTH_KEYS, CONTENT_WIDTH_KEYS,
DENSITY_KEYS, DENSITY_KEYS,
DURATION_KEYS, DURATION_KEYS,
EASE_KEYS, EASE_KEYS,
FOCUS_COLOR_KEYS,
ICON_STROKE_WIDTH_KEYS, ICON_STROKE_WIDTH_KEYS,
MOTION_DISTANCE_KEYS, MOTION_DISTANCE_KEYS,
MOTION_SCALE_KEYS, MOTION_SCALE_KEYS,
@ -16,9 +19,11 @@ import {
RADIUS_KEYS, RADIUS_KEYS,
SHADOW_KEYS, SHADOW_KEYS,
SIZE_PRIMITIVE_KEYS, SIZE_PRIMITIVE_KEYS,
SURFACE_COLOR_KEYS,
TEXT_SIZES, TEXT_SIZES,
Z_INDEX_KEYS, Z_INDEX_KEYS,
type BorderPrimitiveSet, type BorderPrimitiveSet,
type ColorSemanticSet,
type ColorRole, type ColorRole,
type ColorRoleDefinition, type ColorRoleDefinition,
type ColorScale, type ColorScale,
@ -48,11 +53,14 @@ const COLOR_ROLE_SET = new Set<string>(COLOR_ROLES)
const ASPECT_RATIO_KEY_SET = new Set<string>(ASPECT_RATIO_KEYS) const ASPECT_RATIO_KEY_SET = new Set<string>(ASPECT_RATIO_KEYS)
const BORDER_WIDTH_KEY_SET = new Set<string>(BORDER_WIDTH_KEYS) const BORDER_WIDTH_KEY_SET = new Set<string>(BORDER_WIDTH_KEYS)
const BORDER_STYLE_KEY_SET = new Set<string>(BORDER_STYLE_KEYS) const BORDER_STYLE_KEY_SET = new Set<string>(BORDER_STYLE_KEYS)
const BORDER_COLOR_KEY_SET = new Set<string>(BORDER_COLOR_KEYS)
const CONTENT_COLOR_KEY_SET = new Set<string>(CONTENT_COLOR_KEYS)
const CONTAINER_WIDTH_KEY_SET = new Set<string>(CONTAINER_WIDTH_KEYS) const CONTAINER_WIDTH_KEY_SET = new Set<string>(CONTAINER_WIDTH_KEYS)
const CONTENT_WIDTH_KEY_SET = new Set<string>(CONTENT_WIDTH_KEYS) const CONTENT_WIDTH_KEY_SET = new Set<string>(CONTENT_WIDTH_KEYS)
const DENSITY_KEY_SET = new Set<string>(DENSITY_KEYS) const DENSITY_KEY_SET = new Set<string>(DENSITY_KEYS)
const DURATION_KEY_SET = new Set<string>(DURATION_KEYS) const DURATION_KEY_SET = new Set<string>(DURATION_KEYS)
const EASE_KEY_SET = new Set<string>(EASE_KEYS) const EASE_KEY_SET = new Set<string>(EASE_KEYS)
const FOCUS_COLOR_KEY_SET = new Set<string>(FOCUS_COLOR_KEYS)
const ICON_STROKE_WIDTH_KEY_SET = new Set<string>(ICON_STROKE_WIDTH_KEYS) const ICON_STROKE_WIDTH_KEY_SET = new Set<string>(ICON_STROKE_WIDTH_KEYS)
const MOTION_DISTANCE_KEY_SET = new Set<string>(MOTION_DISTANCE_KEYS) const MOTION_DISTANCE_KEY_SET = new Set<string>(MOTION_DISTANCE_KEYS)
const MOTION_SCALE_KEY_SET = new Set<string>(MOTION_SCALE_KEYS) const MOTION_SCALE_KEY_SET = new Set<string>(MOTION_SCALE_KEYS)
@ -60,6 +68,7 @@ const OPACITY_KEY_SET = new Set<string>(OPACITY_KEYS)
const RADIUS_KEY_SET = new Set<string>(RADIUS_KEYS) const RADIUS_KEY_SET = new Set<string>(RADIUS_KEYS)
const SHADOW_KEY_SET = new Set<string>(SHADOW_KEYS) const SHADOW_KEY_SET = new Set<string>(SHADOW_KEYS)
const SIZE_PRIMITIVE_KEY_SET = new Set<string>(SIZE_PRIMITIVE_KEYS) const SIZE_PRIMITIVE_KEY_SET = new Set<string>(SIZE_PRIMITIVE_KEYS)
const SURFACE_COLOR_KEY_SET = new Set<string>(SURFACE_COLOR_KEYS)
const TEXT_SIZE_SET = new Set<string>(TEXT_SIZES) const TEXT_SIZE_SET = new Set<string>(TEXT_SIZES)
const Z_INDEX_KEY_SET = new Set<string>(Z_INDEX_KEYS) const Z_INDEX_KEY_SET = new Set<string>(Z_INDEX_KEYS)
const RESPONSIVE_KEY_SET = new Set(['base', 'sm', 'md', 'lg', 'xl', 'xxl']) 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) validateThemeKeys(options, issues)
validateColorScales(options, issues) validateColorScales(options, issues)
validateColorRoles(options, issues) validateColorRoles(options, issues)
validateColorSemanticGroups(options, issues)
validateTypography(options, issues) validateTypography(options, issues)
validateSizePrimitives(options, issues) validateSizePrimitives(options, issues)
validateStaticPrimitives(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<ColorSemanticSet, 'surface' | 'content' | 'border' | 'focus'>
>,
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<string>,
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( function validateColorScale(
path: string, path: string,
name: string, name: string,

Loading…
Cancel
Save

Powered by TurnKey Linux.