diff --git a/continue.md b/continue.md index e100b517e..6b529f775 100644 --- a/continue.md +++ b/continue.md @@ -28,10 +28,14 @@ Actualizacion Eidos ecosystem 2026-05-16: CSS kebab-case antes de generar selectores o custom properties desde config. - Claves authorables de tipografia (`families`, `weights`, `styles`) deben generar sufijos CSS validos; `typography.sizes` permanece canonico. +- Motion e icon dejan de depender solo de tipos TS: sus key sets canonicos + (`DURATION_KEYS`, `EASE_KEYS`, `MOTION_DISTANCE_KEYS`, + `MOTION_SCALE_KEYS`, `ICON_STROKE_WIDTH_KEYS`) se exportan y validan en + runtime antes de generar CSS. - Validado: - - `npx vitest run src/uix/eidos/active-eidos-config.test.ts` -> 29 tests OK. + - `npx vitest run src/uix/eidos/active-eidos-config.test.ts` -> 30 tests OK. - `npx vitest run src/uix/eidos/active-eidos.test.ts` -> 19 tests OK. - - `npx vitest run src/uix/eidos` -> 7 archivos, 69 tests OK. + - `npx vitest run src/uix/eidos` -> 7 archivos, 70 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 fb5eb592a..6c1bd0e3c 100644 --- a/src/uix/eidos/active-eidos-config.test.ts +++ b/src/uix/eidos/active-eidos-config.test.ts @@ -497,6 +497,31 @@ describe('ActiveEidos config', () => { ).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) diff --git a/src/uix/eidos/index.ts b/src/uix/eidos/index.ts index 0f14af5a4..508471207 100644 --- a/src/uix/eidos/index.ts +++ b/src/uix/eidos/index.ts @@ -178,7 +178,12 @@ export { CONTENT_WIDTH_KEYS, DENSITIES, DENSITY_KEYS, + DURATION_KEYS, + EASE_KEYS, HIERARCHY_COLOR_ROLES, + ICON_STROKE_WIDTH_KEYS, + MOTION_DISTANCE_KEYS, + MOTION_SCALE_KEYS, OPACITY_KEYS, SHADOW_KEYS, SHADOW_PHYSICAL_KEYS, diff --git a/src/uix/eidos/lib/config-types.ts b/src/uix/eidos/lib/config-types.ts index 9f6bfd1d2..2fa04b92c 100644 --- a/src/uix/eidos/lib/config-types.ts +++ b/src/uix/eidos/lib/config-types.ts @@ -199,10 +199,16 @@ export type RecipeTokenValue = string | number; export type RecipeTokenMap = Readonly>; export type RecipeTokenSet = Readonly>; -export type DurationKey = 'instant' | 'fast' | 'normal' | 'moderate' | 'slow'; -export type EaseKey = 'default' | 'out' | 'in' | 'spring' | 'alert' | 'symmetric'; -export type MotionDistanceKey = 'xs' | 'sm' | 'md' | 'lg'; -export type MotionScaleKey = 'enter' | 'press'; +export const DURATION_KEYS = ['instant', 'fast', 'normal', 'moderate', 'slow'] as const; +export const EASE_KEYS = ['default', 'out', 'in', 'spring', 'alert', 'symmetric'] as const; +export const MOTION_DISTANCE_KEYS = ['xs', 'sm', 'md', 'lg'] as const; +export const MOTION_SCALE_KEYS = ['enter', 'press'] as const; +export const ICON_STROKE_WIDTH_KEYS = ['sm', 'md', 'lg'] as const; + +export type DurationKey = (typeof DURATION_KEYS)[number]; +export type EaseKey = (typeof EASE_KEYS)[number]; +export type MotionDistanceKey = (typeof MOTION_DISTANCE_KEYS)[number]; +export type MotionScaleKey = (typeof MOTION_SCALE_KEYS)[number]; export interface MotionPrimitiveSet { readonly duration: Record; @@ -213,7 +219,7 @@ export interface MotionPrimitiveSet { } export type IconSizeKey = Exclude; -export type IconStrokeWidthKey = 'sm' | 'md' | 'lg'; +export type IconStrokeWidthKey = (typeof ICON_STROKE_WIDTH_KEYS)[number]; export interface IconPrimitiveSet { readonly size: Record; diff --git a/src/uix/eidos/lib/config.ts b/src/uix/eidos/lib/config.ts index 1a7067f47..c0a434a45 100644 --- a/src/uix/eidos/lib/config.ts +++ b/src/uix/eidos/lib/config.ts @@ -7,6 +7,11 @@ import { CONTAINER_WIDTH_KEYS, CONTENT_WIDTH_KEYS, DENSITY_KEYS, + DURATION_KEYS, + EASE_KEYS, + ICON_STROKE_WIDTH_KEYS, + MOTION_DISTANCE_KEYS, + MOTION_SCALE_KEYS, OPACITY_KEYS, SHADOW_KEYS, SIZE_PRIMITIVE_KEYS, @@ -45,6 +50,11 @@ const BORDER_STYLE_KEY_SET = new Set(BORDER_STYLE_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 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) const OPACITY_KEY_SET = new Set(OPACITY_KEYS) const SHADOW_KEY_SET = new Set(SHADOW_KEYS) const SIZE_PRIMITIVE_KEY_SET = new Set(SIZE_PRIMITIVE_KEYS) @@ -96,6 +106,8 @@ export function validateEidosConfig(options: EidosConfig): EidosValidationReport validateBorderPrimitives(options, issues) validateLayoutPrimitives(options, issues) validateDensityPrimitives(options, issues) + validateMotionPrimitives(options, issues) + validateIconPrimitives(options, issues) validateRecipeTokens(options, issues) validateRecordPrimitive( 'primitives.opacity', @@ -629,6 +641,55 @@ function validateDensityPrimitives(options: EidosConfig, issues: EidosValidation ) } +function validateMotionPrimitives(options: EidosConfig, issues: EidosValidationIssue[]): void { + const motion = options.primitives.motion + if (!motion) return + + validateRecordPrimitive( + 'primitives.motion.duration', + motion.duration, + DURATION_KEYS, + DURATION_KEY_SET, + issues + ) + validateRecordPrimitive('primitives.motion.ease', motion.ease, EASE_KEYS, EASE_KEY_SET, issues) + validateRecordPrimitive( + 'primitives.motion.distance', + motion.distance, + MOTION_DISTANCE_KEYS, + MOTION_DISTANCE_KEY_SET, + issues + ) + validateRecordPrimitive( + 'primitives.motion.scale', + motion.scale, + MOTION_SCALE_KEYS, + MOTION_SCALE_KEY_SET, + issues + ) + validateNonEmptyCssValue('primitives.motion.stagger', motion.stagger, issues) +} + +function validateIconPrimitives(options: EidosConfig, issues: EidosValidationIssue[]): void { + const icon = options.primitives.icon + if (!icon) return + + validateRecordPrimitive( + 'primitives.icon.size', + icon.size, + SIZE_PRIMITIVE_KEYS, + SIZE_PRIMITIVE_KEY_SET, + issues + ) + validateRecordPrimitive( + 'primitives.icon.strokeWidth', + icon.strokeWidth, + ICON_STROKE_WIDTH_KEYS, + ICON_STROKE_WIDTH_KEY_SET, + issues + ) +} + function validateRecipeTokens(options: EidosConfig, issues: EidosValidationIssue[]): void { for (const [component, tokens] of Object.entries(options.recipes ?? {})) { if (!isCssTokenSegment(component)) {