You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/eidos/eidos-motion.md

36 KiB

Eidos Motion — Diseño y Arquitectura

Estado: modelo de dos momentos — F1–F7 implementadas (2026-06-04).

Este documento describe el sistema de animaciones de Eidos. El modelo, anclado en el canon (GUIA_IMPLEMENTACION_SEMAUIX.md §4.3, §7, §11), es:

Hay DOS momentos en una interacción, y CADA UNO puede llevar animación — uno, el otro, o ambos:

  • Momento --event (data-event-*): el flourish perceptivo durante el hold de una señal de sema. Es el canal motion de la firma perceptiva (§11), definido por evento (family/intent/event). Transitorio.
  • Momento --state (data-state): la transición a/desde una condición persistente. Definido por componente (prop motion). Persistente.

Esto es lo que hace a UIX distinto de todos los frameworks actuales: los demás colapsan la animación de presence en un solo eje (Chakra: solo data-state + Presence). UIX anima los dos momentos y los integra con la firma perceptiva — más coherente y más rico.

Implementado (F1–F6): los dos momentos (keyframes + signatures + presets

  • generación + motor EngineMotion + validación + tests + demo /temas/animations); la firma migrada de events.css al registro (signatures, F2); los recipes migrados a presets con override de duración/easing por componente (F3); los drivers JS spring (física) / waapi / rect (FLIP) + Presence.motion + un overlay real (Dialog) brincando (F4); la coreografía stagger + presets Material shared-axis/fade-through (F5); el rigor de tokens — firma cruda tokenizada (holds largos slower/deliberate/emphatic/sustained), shared-axis --motion-distance-xl, easing emphasized, set [data-motion-set='expressive'] (F6); el typegen extensible de nombres de preset — registry augmentable EidosMotionPresets (F7). Roadmap F1–F7 completo. data-motion-ref y el "TSC event:* scope" quedan obsoletos (THEMING.md §13/§14, sin uso real).

Refactor (post-F5): el motor de motion es un SERVICIO. El runtime ya no vive en eidos: se reubicó a src/arts/motion (un art — artefacto de runtime puro, sin dependencias de UI ni de otros arts), expuesto como uix.motion y consumido por ambas capas — soma (Presence recibe motion: EngineMotion y llama motion.run(node, phase)) y eidos (delega vía eidos.motion + registra sus presets css en el servicio al boot). Esto disuelve el acoplamiento soma→eidos: desaparecen la prop DialogProps.runMotion y eidos.motionRunner — el bridge es ahora EngineMotion.run, que lee data-animation-style del nodo. Eidos conserva la generación de CSS (lib/render-css.ts) + los datos de presets/keyframes/firmas (lib/motion/presets/css.ts); los tipos, el motor y los drivers (spring/waapi/rect) viven en $motion. Ver src/arts/motion/README.md. (Las secciones §5/§6/§9 + el árbol de archivos abajo reflejan ya el nuevo hogar.)

TL;DR:

  • Dos momentos animables: --event (data-event-*, la firma perceptiva, signatures) y --state (data-state, la transición per-componente, presets). Uno, el otro, o ambos; componen en secuencia (sequence).
  • Un registro (EidosConfig.motion) con tres mapas: keyframes, signatures (momento-evento), presets (momento-estado).
  • data-state es de soma; data-event-* es de sema; eidos lee ambos y anima (§7: la regla es no SOBRE-ESCRIBIR el atributo ajeno, no "un eje").
  • Se usa: el momento-estado vía prop motion="scale-fade" → data-animation-style; el momento-evento es automático al disparar el evento (la firma).
  • Drivers: css (suelo, paridad Chakra) + JS (waapi/spring/rect/svelte, física/FLIP/genie/orquestación — más allá de Chakra), para cualquiera de los dos momentos.

Tabla de contenidos

  1. Tesis y posicionamiento
  2. Los dos momentos
  3. Motion en las 4 capas
  4. Arquitectura — el registro de dos superficies
  5. Los tipos
  6. La API del EngineMotion
  7. Los drivers
  8. Contrato DOM
  9. Integración con soma (Presence)
  10. Reduced motion
  11. Primitivas y keyframes
  12. Contenido inicial (signatures + presets)
  13. Defaults por componente
  14. Dónde vive el código
  15. events.css ES el momento-evento
  16. Comparación con Chakra UI v3
  17. Decisiones de naming
  18. Fases de implementación
  19. Qué queda fuera / diferido

1. Tesis y posicionamiento

Una interacción no tiene un momento animable, tiene dos, y son de naturaleza distinta:

  1. La ocurrencia perceptiva — "algo acaba de pasar" — transitoria, con hold, intent y sequence. La escribe sema como data-event-*.
  2. El cambio de estado — "ahora esto está abierto" — persistente, fuente de verdad. Lo escribe soma como data-state.

GUIA §4.3 lo deja explícito: emerge = "algo entra o sale del campo perceptivo" → es un evento, no un estado. GUIA §11 define la firma perceptiva por evento, donde motion es uno de los canales (junto a sound/color/presence/haptic): el movimiento de salida de un commit.delete + loss es "retirada/descenso"; el de signal.alert + threat es "entrada saliente". El movimiento se define por evento.

El diferenciador de UIX. Todos los frameworks actuales animan presence en un solo eje (Chakra: data-state + Presence; Radix/Ark: igual). UIX distingue los dos momentos y anima ambos, integrando el momento-evento con la firma perceptiva (sonido/haptic incluidos). Eso es lo que lo hace más coherente (un solo modelo, dos momentos nítidos) y más rico (la animación de "qué ocurrió" no se confunde con la de "en qué estado estamos").

Lo que eidos posee: el registro tipado de DATOS (EidosConfig.motion = keyframes + signatures + presets), la generación de CSS para ambas superficies, la prop motion y el attr data-animation-style. El motor de ejecución (EngineMotion) NO es de eidos: vive en $motion y se consume vía uix.motion (eidos delega + registra ahí sus presets css).

Lo que NO posee: la emisión de la señal y su firma semántica → sema; el lifecycle mount/unmount y data-state → soma Presence; los attrs sobre los que se anima (data-state, data-side, data-starting/ending-style) → declarados en morfo; la pref allow/reduce → ActivePrefs.


2. Los dos momentos

Momento --event Momento --state
Atributo data-event-* data-state
Qué es la ocurrencia perceptiva (la señal) la transición a/desde una condición persistente
Su animación el flourish de la firma (settle, pulse, retirada por intent, entrada de un present…) la transición de presencia/layout (escala, slide, crecer a la altura abierta…)
Granularidad por evento (family/intent/event) — genérica, consistente en todo el sistema por componente (prop motion)
Dueño del attr sema (lo estampa durante el hold) soma (effects)
Naturaleza transitoria (vive el hold) persistente
En el registro signatures presets
Canon §11 (firma perceptiva), §4.3 (emerge), §7.1 §7.2, §5.2 (sequence)

Cada momento puede llevar animación — uno, el otro, o ambos:

  • press (botón): solo momento-evento (contact.press → squeeze). Sin transición de estado.
  • dialog abriendo: el flourish del evento emerge.present y la transición de estado a open.
  • collapse: la transición de altura (estado) y/o el flourish del evento expand.

Componen en secuencia (GUIA §5.2): sequence: 'pre' hace que el flourish del evento corra antes de que el estado cambie (p. ej. animar la salida antes de cerrar); 'post', después (celebrar tras el resultado real). El active_architecture.md §5 lo describe en la cadena causal: la animación del evento corre durante el hold, luego el estado toma el relevo.

data-state y data-event-* no se mezclan (§7.3): la regla es que sema no sobre-escribe atributos de estado y viceversa (ownership). NO dice que solo uno pueda animarse — eidos lee ambos y anima ambos. Lo prohibido es pisar el nombre ajeno, no animar sobre los dos ejes.

Coordinación de hijos (stagger / cascada) — modelo CERRADO, RFC §D.11. Hubo un tercer eje «coordinado» (PresenceGroup / presets cascade-* sobre data-starting/ending-style); se retiró el 2026-06-19. El modelo final: la cascada NO es un sistema aparte — es el momento --state (un preset declarado) + el stagger que ya existía (índice × --motion-stagger-each, paralelo = 0, cascada = N) + una regla de foundation que escribe el índice desde estructura ([data-stagger] > *:nth-child → --motion-stagger-index / :nth-last-child → -rev, generada en lib/render-css.ts). La firma del evento (emerge → present-rise) es solo el flourish del contenedor (genérico, sobre data-event-*); el timing de los hijos es realización per-componente, no un canal de sema. Tres ejes ortogonales (sema emite · motion es el motor · eidos materializa) + el lifecycle completo: §D.11 del RFC.

Prop motion UNIVERSAL — un selector, tres dominios (encuadre de diseño, [RFC §D.12]). Tras retirar el coordinado, queda UN sistema → un solo prop motion (hoy <Cascade> aún usa animation; se unifica). La meta: cualquier componente (no solo overlays) puede recibir motion="X" de un catálogo registrado type-safe. Qué significa el prop lo decide el discriminante «¿la animación realiza un evento perceptivo?»: evento → la firma manda, el prop es override/violación (tipos/lint); estado → selecciona el state-preset (data-state); contenido (sin evento) → el prop es la vía primaria. El acoplamiento con sonido/háptico solo aplica al dominio evento — los de contenido quedan en paridad limpia. Detalle + plan (a)+(b): §D.12 del RFC.

Restricciones estructurales del modelo CSS — [RFC §D.13]. Los contratos que el declarativo exige: (1) estructura — [data-stagger] ↔ hijos DIRECTOS; :nth-child ignora comentarios/{#if} (blindado), pero un wrapper-elemento intermedio rompe el conteo → el agrupador que cascadea se vuelve su propio scope (inherits:false aísla el índice); display:contents sin re-scope = dead-zone. (2) exit — unit-exit por Presence (retiene + espera, el opacity del padre arrastra a los hijos) funciona hoy; el staggered-exit por-hijo exige JS de lifecycle (PresenceGroup, diferido) → la regla container-driven es ENTER-ONLY; nunca {#if} crudo en superficie animada. (3) debug — el índice es inherits:false (no lo pisa un ancestro); todo vive tipado en Computed; pendiente un modo [data-debug-stagger] que lo materialice.


3. Motion en las 4 capas

Capa Qué aporta Momento
Morfo Declara los attrs: data-state + estados (open/closed), los eventos (emerge/commit/signal + sequence/persistence/intent), data-side/data-align, data-starting/ending-style. ambos
Sema Estampa data-event-* (family/intent/phase/id) durante el hold; resuelve la firma (sound/haptic son canales runtime; motion/color/presence los materializa eidos leyendo data-event-*). --event
Soma Escribe data-state vía effects; Presence mantiene el nodo durante la salida y espera la animación (getAnimations() + Promise.all(finished)). Dispara el evento con su sequence. --state (+ dispara el evento)
Eidos keyframes + signatures (momento-evento, sobre data-event-*) + presets (momento-estado, sobre data-state) + generación de CSS; delega el motor JS a uix.motion (servicio) y registra sus presets css ahí al boot. Lee ambos ejes y anima. ambos

Reduced-motion ya está cableado: MotionEffective = 'allow' \| 'reduce' (libs/motion), proyectado como data-motion por ActivePrefsDomProjection, trackeado por ReducedMotionTracker (arts/adom), expuesto en ActiveDom.prefersReducedMotion.matches.


4. Arquitectura — el registro de dos superficies

EidosConfig.motion
├── keyframes:  { 'fade-in': {...}, 'scale-in': {...}, ... }   @keyframes registrados
│
├── signatures: {            ← MOMENTO --event (la firma, genérica por evento)
│     'emerge-present': { family:'emerge', event:'present', keyframes:['fade-in'], ... },
│     'commit-settle':  { family:'commit', keyframes:['settle'], ... },
│     'announce-threat':{ family:'signal', intent:'threat', keyframes:['pulse'], ... }
│   }                         → genera reglas [data-event-*][data-event-phase='active']
│
└── presets: {               ← MOMENTO --state (transición per-componente)
      'scale-fade': { driver:'css', enter:{...}, exit:{...} },
      'slide-fade': { driver:'css', enter:{ bySide }, exit:{ bySide } },
      'genie':      { driver:'rect', enter, exit }              (P3, JS)
    }                         → genera reglas [data-animation-style][data-state]
        │
        ▼
  uix.motion : EngineMotion  (servicio · arts/motion — resuelve, corre drivers JS, respeta reduce)
        ▲  eidos.motion delega aquí + registra los presets css al boot
        │
        ▼
  Eidos lee data-event-* (firma)  +  data-state (transición)  →  anima
  • signatures es la firma genérica: un commit asienta igual en todo el sistema; un emerge.present entra igual. Los packs de sema (sema/components/*.ts) afinan la firma de un componente concreto. Es lo que hoy hace events.css a mano (§15).
  • presets es la transición per-componente, elegida con la prop motion.
  • Theme/app pueden añadir u override en ambos mapas.

5. Los tipos

$motion (src/arts/motion/types.ts) — reubicado a un art. duration/ease son string (desacoplados de DurationKey/EaseKey: eidos resuelve los tokens en su capa de generación, el art no conoce la escala):

type ReducePolicy = 'instant' | 'opacity-only' | 'none'
type MotionSide   = 'top' | 'right' | 'bottom' | 'left'
type KeyframeName = string                 // clave en motion.keyframes
type MotionPresetName = string             // valor de la prop `motion`; 'none' desactiva

// Una fase CSS: keyframes compuestos + tokens + side-awareness.
interface CssPhase {
  keyframes: KeyframeName | KeyframeName[]  // coma-compuestos: ['scale-in','fade-in']
  duration?: string                         // token key ('moderate'…) o crudo ('600ms')
  ease?: string
  transformOrigin?: string                  // p. ej. 'var(--floating-transform-origin)'
  bySide?: Partial<Record<MotionSide, KeyframeName | KeyframeName[]>>
}

// ── Momento --event: la firma perceptiva (genérica por evento) ──
interface EventSignature {
  family?: string      // data-event-family ('emerge' | 'commit' | 'signal' | …)
  intent?: string      // data-event-intent (familias valenced)
  event?:  string | string[]  // data-event nombre(s)/prefijo(s) (['present','open'], …)
  keyframes: KeyframeName | KeyframeName[]
  duration?: string   // token key O hold crudo ('600ms', fuera de escala)
  ease?: EaseKey
  fill?: 'none' | 'forwards' | 'backwards' | 'both'
  reduce?: ReducePolicy
}

// ── Momento --state: preset per-componente (prop `motion`) ──
interface CssStatePreset {
  driver: 'css'
  enter?: CssPhase     // [data-state='open']
  exit?:  CssPhase     // [data-state='closed']
  reduce?: ReducePolicy
}
interface JsStatePreset {                    // implementado (waapi/spring/rect/svelte)
  driver: 'waapi' | 'spring' | 'rect' | 'svelte'
  enter?: MotionRun; exit?: MotionRun
  requires?: ('sourceRect' | 'targetRect' | 'placement')[]
  reduce?: ReducePolicy
  fallback?: CssStatePreset                  // progressive enhancement
}
type StatePreset = CssStatePreset | JsStatePreset

// ── El registro ──
interface MotionConfig {
  keyframes?:  Record<KeyframeName, KeyframeStops>
  signatures?: Record<string, EventSignature>   // momento --event
  presets?:    Record<string, StatePreset>        // momento --state
}

Decisión estructural: el momento-evento es genérico por evento (signatures, por family/intent), no empaquetado por preset — porque la firma (§11) se define por evento y debe ser consistente entre componentes. El ajuste fino per-componente del momento-evento va en los packs de sema. La prop motion solo elige el preset de estado.


6. La API del EngineMotion

Vive en $motion (servicio uix.motion; eidos.motion y soma.motion lo exponen — la misma instancia). Registro + path declarativo CSS + ejecución de drivers JS. El caller pasa MotionRunOptions (dom, reduced, side, sourceRect/targetRect, duration/ease) para los drivers que la necesiten.

interface EngineMotion {
  register(name: string, preset: StatePreset): void
  resolve(name: string): StatePreset | undefined
  has(name: string): boolean
  list(): string[]
  // css: declarativo (handle settled); js: construye el MotionContext, corre el
  // driver, normaliza el retorno a un handle y lo trackea por elemento.
  enter(el: HTMLElement, name: string, opts?: MotionRunOptions): MotionHandle
  exit(el: HTMLElement, name: string, opts?: MotionRunOptions): MotionHandle
  // El bridge de Presence (reemplaza al viejo `runner`): lee `data-animation-style`
  // del nodo y delega a enter/exit (css → handle settled; js → corre el driver).
  run(el: HTMLElement, phase: 'enter' | 'exit', opts?: MotionRunOptions): MotionHandle
  cancel(el: HTMLElement): void                         // cancela handles JS activos
  pending(el: HTMLElement): Promise<void>               // finished combinado (para Presence)
  dispose(): void                                       // cancela todo (cleanup del servicio)
}
  • State preset css — declarativo: el wrapper pone data-animation-style y la regla CSS [data-animation-style][data-state] + la Presence de soma hacen el resto. El runtime no lo "corre".
  • State preset JS — corre el driver (spring/waapi/rect/svelte), normaliza el retorno (Animation | Animation[] | MotionHandle) a un handle único, lo trackea por elemento (cancel/pending), respeta la ReducePolicy.
  • El momento-evento (signatures) no pasa por enter/exit: es CSS generado que reacciona a data-event-*.

6.1 — Política de cleanup (WAAPI / spring)

fill: forwards deja Animations colgando en getAnimations() → rompería la espera de exit de Presence. Disciplina: el reposo vive en el CSS de [data-state]; los presets waapi animan sin fill: forwards y el runtime hace cancel() al terminar; para reposos no expresables en CSS (FLIP medido), commitStyles() + cancel().


7. Los drivers

Aplican a cualquiera de los dos momentos (un signature o un state preset).

Driver Sustrato Para qué Más allá de CSS
css @keyframes + tokens (generado) fade / scale / slide / collapse / firma — (suelo Chakra)
waapi el.animate() keyframes computados en runtime distancia/tamaño medidos, cancelable
spring integrador semi-implícito de Euler (RAF vía ActiveDom) overshoot/settle, drag, snap-back física real — lo que ninguna cubic-bezier expresa
rect medición de rects (vía ActiveDom) → WAAPI FLIP, shared-element, genie animar entre dos posiciones reales
svelte (opt-in) transition: / animate:flip reorder de listas {#each} declarativo de Svelte

Bundle: css (generado) es el suelo. spring es un integrador self-contained (sin dependencia — un muelle independiente por propiedad, stepeado por ctx.dom.requestFrame); waapi envuelve el.animate (API del browser, ya visible a getAnimations()); rect (FLIP) compone medición + WAAPI. Los helpers viven en lib/motion/presets/js.ts (spring() / waapi() / rect()). svelte (opt-in) se reserva para animate:flip en {#each}. Adapter externo (motion-one) opt-in.

Matiz svelte: las transiciones de {#if} pelearían con la Presence de soma; el camino core es WAAPI/spring (compone con getAnimations). svelte se reserva para animate:flip en {#each} (Svelte dueño del lifecycle).

Batching rect: FLIP con N elementos hace su pasada en dos fases sobre un único requestFrame (medir todos los source → mutar → medir todos los target → animar). ActiveDom ya da requestFrame; el batching es del driver, no de ActiveDom.


8. Contrato DOM

Momento --event (lo escribe sema durante el hold; eidos reacciona):

[data-event='present'][data-event-phase='active'] { animation: fade-in …; }
[data-event-family='commit'][data-event-phase='active'] { animation: settle …; }
[data-event-family='signal'][data-event-intent='threat'][data-event-phase='active'] { animation: pulse …; }

Momento --state (lo escribe soma; eidos reacciona). El wrapper pone data-animation-style (la prop motion); el reposo lo da el recipe sobre el mismo data-state:

[data-animation-style='scale-fade'][data-state='open']   { animation: scale-in, fade-in …; }
[data-animation-style='scale-fade'][data-state='closed'] { animation: scale-out, fade-out …; }
[data-animation-style='slide-fade'][data-side='top'][data-state='open'] { animation: slide-from-bottom, fade-in …; }

Prop pública: motion?: MotionPresetName | 'none' (default por componente; 'none' desactiva). Placement-aware: lee data-side/data-align (morfo) + --floating-transform-origin (soma floating).

NO data-motion: ya tiene dos dueños (la pref allow/reduce + la dirección de NavMenu). El attr del motor es data-animation-style (espejo del animationStyle de Chakra). Ver §17.


9. Integración con soma (Presence)

soma/layers/presence.svelte.ts ya hace el lifecycle: al cerrar mantiene el nodo, espera node.getAnimations() + Promise.all(finished) (guard de runId, flag enabled), y desmonta. Las CSS animations (ambos momentos) y las de el.animate() (drivers JS) aparecen en getAnimations() → se esperan sin plumbing nuevo.

Presence.motion (F4 · refactor a servicio) — el hook para drivers que NO están en getAnimations() (el spring, RAF puro): Presence recibe motion: EngineMotion y llama motion.run(node, phase) al entrar/salir; run arranca la animación JS y devuelve un MotionHandle, cuyo finished Presence espera ALONGSIDE getAnimations(). Para un preset css (o un nodo sin data-animation-style), run devuelve un handle ya settled → path declarativo sin cambio. soma NO importa eidos: consume el servicio vía soma.motion (= uix.motion), la misma instancia que eidos.motion. Cableado en el provider de Dialog (contentPresence/overlayPresence reciben motion: this.soma.motion); el mismo patrón vale para cualquier overlay — basta pasar motion a su Presence, sin prop ni acoplamiento (el viejo DialogProps.runMotion / eidos.motionRunner desapareció en el refactor).

Orquestación de hijos (pendiente, en re-diseño): cuelga de la firma del evento (sema) materializada por eidos, no de un motor paralelo — ver Apéndice D del MOTION_SERVICE_RFC.md.

9.1 — SSR / hidratación

Los overlays montan al abrir en cliente; motion dispara en transiciones, no en el primer montaje (un elemento montado-abierto suprime el enter). Sin handoff SSR→JS en vuelo; el fallback CSS cubre sin-JS.

9.2 — Exit + a11y: inert (de soma)

Durante el exit el nodo sigue montado: debe ser inert/aria-hidden y no foco. El focus.return del morfo ya devuelve el foco; el inert durante la ventana de exit es mejora de Presence/Dismissal de soma (ya usa el patrón en color-field).


10. Reduced motion

  • CSS (ambos momentos): reglas [data-motion='reduce'] … + @media (prefers-reduced-motion) por política (instant → animation: none; opacity-only → solo fade; none → íntegro).
  • Runtime (drivers JS): MotionContext.reduced de ActiveDom.prefersReducedMotion; aplica la ReducePolicy antes de correr.
  • Morfo declara a11ySemantic.reducedMotionFallback por evento; soma silencia la señal cuando aplica. El motor es coherente con esa decisión.

11. Primitivas y keyframes

Tokens existentes (STATIC_MOTION, generados, leídos por css y js):

Token Valores
--duration-{k} instant 0 · fast 120 · normal 180 · moderate 240 · slow 320 ms
--ease-{k} default · out · in · spring · alert · symmetric
--motion-distance-{k} xs 2 · sm 4 · md 8 · lg 16 px
--motion-scale-{k} enter 0.985 · press 0.97 · through 0.92 (fade-through) · lift 1.02 (pickup de drag — el único >1, ver §Draggable surfaces en THEMING)

Vars de override (runtime, NO tokens de theme) — un componente las pone en su elemento; el preset las lee con fallback al token:

Var Para Notas
--motion-duration-{enter,exit} timing por componente (F3) @property inherits:false
--motion-ease-{enter,exit} curva por componente (F3) @property inherits:false
--motion-slide-leave extra de slide-full para paneles inset (drawer) default 0px
--height / --collapsed-height altura medida del collapse (soma la escribe) —
--motion-stagger-each ritmo del stagger (en el contenedor, hereda) default 0ms → paralelo; N → cascada
--motion-stagger-index / -index-rev índice por ítem desde estructura — [data-stagger] > *:nth-child (fwd, enter) / :nth-last-child (rev, exit); generado en render-css.ts, nadie lo escribe @property inherits:false, <integer>

Keyframes (EidosConfig.motion.keyframes, paridad theme.keyframes de Chakra): translate/scale individuales (componen sin pisarse); parametrizados por CSS var para tamaños/distancias dinámicas (var(--height), var(--collapsed-height,0)). Built-in: fade-in/out, scale-in/out, slide-from/to-{side}[-full], expand/collapse-height; la firma (announce-pulse-*, commit-settle, press-squeeze, dismiss-fade, present-rise); Material (slide-axis-{x,y}-{in,out}, scale-from-92).


12. Contenido inicial (signatures + presets)

presets (momento --state) — built-in (css):

Nombre Notas
fade · scale-fade enter+exit; scale-fade con --floating-transform-origin
slide-fade side-aware (data-side): slide + scale 0.985 + fade
slide-full drawer por borde, inset-aware (--motion-slide-leave)
collapse altura medida (var(--height))
shared-axis-x/y · fade-through Material 3 (F5)

Los drivers JS (spring/waapi/rect, presets/js.ts) se registran por app/demo (aún no hay built-ins JS por nombre; el demo registra panel-spring, dialog-spring, flip).

signatures (momento --event) — built-in, migradas de events.css (F2, §15): present/dismiss, commit (+ intents fulfill/affirm/threat), press (contact), announce (+ 5 intents). Genéricas por family/intent/event.


13. Defaults por componente

El momento-estado por componente (prop motion, default del wrapper):

Componente Parte Default motion
Dialog overlay / content fade / scale-fade
Popover · Tooltip · Dropdown · Select · Menubar · ContextMenu content slide-fade
Drawer content slide-full
Accordion · Collapsible content collapse
Toast item slide-fade

El momento-evento es automático: al disparar el evento (p. ej. commit, emerge.present), la firma signatures correspondiente reacciona — sin prop.


14. Dónde vive el código

arts/motion/                  ← EL SERVICIO (art puro: sin deps de UI ni de otros arts)
  types.ts            ✓  MotionDom (port estructural), MotionConfig, EventSignature, StatePreset, MotionHandle…
  engine-motion.ts    ✓  createEngineMotion → EngineMotion (register/resolve/enter/exit/run/cancel/pending/dispose)
  drivers.ts          ✓  drivers JS: spring() (física) / waapi() / rect() (FLIP)
  index.ts            ✓  barrel `$motion` (re-exports nombrados) + README.md
arts/active-app/service-factories/motion.ts  ✓  defineEngineMotion (factory; coreDep 'dom')

eidos/lib/motion/presets/css.ts  ✓  DATOS: keyframes + state presets + signatures (BUILTIN_*) + Material (F5)
eidos/lib/config-types.ts   ✓  EidosConfig.motion (tipo MotionConfig de $motion)
eidos/lib/render-css.ts     ✓  GENERA CSS: presets ([data-state]) + signatures ([data-event-*]) + overrides + stagger
eidos/lib/config.ts         ✓  valida presets (css + js) + signatures
eidos/lib/themes/base.ts    ✓  built-ins DATA (keyframes + signatures + presets)
eidos/active-eidos.svelte.ts  ✓  eidos.motion → delega a uix.motion + registra los presets css al boot
eidos/components/dialog/    ✓  wrapper: prop motion → data-animation-style (sin runMotion)

active-uix/active-uix.svelte.ts  ✓  uix.motion : EngineMotion (createActiveUix lo crea; attach lo lee del app)
soma/core/soma.svelte.ts    ✓  soma.motion → uix.motion
soma/layers/presence.svelte.ts  ✓  opt `motion: EngineMotion` → motion.run(node, phase) (gating JS) + tests

(✓ hecho. Reubicado a arts/motion en el refactor a servicio. Pendiente: pasar motion a más overlays (drawer/popover/…); drivers JS built-in por nombre; F6/F7.)


15. events.css ES el momento-evento

events.css ERA "la superficie --event implementada a mano". En F2 se migró a signatures: las 9 keyframes + 12 reacciones (announce pulse por intent, commit settle, dismiss fade, press squeeze, present rise) viven ahora en EidosConfig.motion.{keyframes,signatures} (presets/css.ts → BUILTIN_SIGNATURES) y se generan a generated/base.css — theme-extensible. events.css quedó reducido a dos concerns globales: el hint de compositor (will-change) y el cap de reduced-motion (el motion de la firma se silencia bajo reduce, pero sonido + haptic siguen comunicando — la ventaja cross-modal). archetypes.css (4 transitions baseline de hover/focus) se queda como micro-interacción transversal.


16. Comparación con Chakra UI v3

Capacidad Eidos Chakra v3
Momento --state (data-state + presets nombrados) ✅ presets + prop motion ✅ animationStyle + data-state
Presence (hold-through-exit) ✅ soma (getAnimations, cubre transitions) ✅ (animationend)
keyframes registry + tokens ✅ ✅
placement-aware ✅ data-side ✅ data-placement
Momento --event (firma perceptiva) ✅ signatures + sema (intent/hold/sequence) ❌ no existe
Los dos momentos integrados ✅ ❌ (solo --state)
física / FLIP / orquestación (drivers JS) ✅ spring / rect / waapi ❌

El momento --event integrado con la firma perceptiva (y con sonido/haptic) es lo que ningún framework actual tiene. Chakra anima el estado; UIX anima el estado y la ocurrencia, distinguiéndolos.


17. Decisiones de naming

  • Attr del momento-estado: data-animation-style (NO data-motion, que está reservado para la pref de reduce-motion allow/reduce).
  • Attrs del momento-evento: los data-event-* que sema ya estampa.
  • Limpieza (F7) ✓: el data-motion direccional de NavMenu (un rename planeado) no existe en el código — moot. data-motion-ref y el "TSC event:* scope" se descartan (sin uso real).

18. Fases de implementación

Roadmap «superar a todos los frameworks». F1–F6 implementadas:

  • F1 — Fundación de dos momentos ✓: tipos (MotionConfig {keyframes, signatures, presets}, EventSignature, StatePreset) + generación (ambas superficies) + motor EngineMotion (servicio uix.motion, reubicado a $motion en el refactor) + validación + tests.

  • F2 — Firma cross-modal ✓ (supera SwiftUI): events.css migrado a signatures (§15); el motion de la firma + sonido + haptic salen del MISMO evento (acoplamiento que el .sensoryFeedback de SwiftUI deja suelto).

  • F3 — Presence pulida ✓ (iguala Base UI): Dialog/Popover/Drawer/Accordion migrados a presets; override de duración + easing por componente (--motion-duration/ease-{enter,exit} + @property inherits:false), slide inset-aware (--motion-slide-leave), bridge de altura medida (--height). Cero regresión de timing.

  • F4 — Motor mecánico JS ✓ (iguala Framer): drivers spring (física real) / waapi / rect (FLIP); Presence.motion → motion.run(node, phase) (el motor como servicio uix.motion, consumido por soma y eidos sin acoplamiento); Dialog real brincando.

  • F5 — Coreografía ✓ (iguala Material 3): stagger declarativo (--motion-stagger-{index,each}), presets shared-axis-x/y + fade-through; container-transform vía el driver rect.

  • F6 — Rigor de tokens ✓ (iguala Carbon): la firma cruda tokenizada — la escala de duración gana el tramo largo de holds perceptivos (slower 400 · deliberate 600 · emphatic 800 · sustained 1000ms); los pulsos announce escalan por severidad de intent (deliberate→emphatic→sustained, alineado con los holds-by-intent del libro). Shared-axis viaja el --motion-distance-xl (30px) canónico; fade-through usa --motion-scale-through (0.92); press/commit snapean a la escala existente (sub-perceptible). Easing emphasized (M3 emphasized-decelerate). Sets productive/expressive (Carbon): primitives.motion.expressive emite un scope [data-motion-set='expressive'] que remapea el easing — productive es el default. La duración-escalada-por- distancia queda como convención de emparejamiento (distance token ↔ duration token), no fórmula runtime (sin consumidor CSS hoy — el driver rect la haría por medición si un consumidor lo pide).

  • F7 — Extensibilidad + typegen ✓ (más allá de todos): los nombres de preset del momento---state son type-safe + app-extensibles vía un registry augmentable — EidosMotionPresets (espejo de SemaChannelSignatures), con MotionPresetName = keyof EidosMotionPresets | 'none' | (string & {}). Una app añade presets type-safe con declare module '$uix/eidos' { interface EidosMotionPresets { … } }: la prop motion los autocompleta y un typo es error de compilación. El motor ($motion/uix.motion) queda string (abierto en runtime) — el registry es una ergonomía de compile-time sobre las props; un test fija el set built-in contra él. El data-motion direccional de NavMenu ya no existe (rename moot). transition con grupos nombrados queda diferido (sin consumidor; getAnimations({ subtree: true }) lo cubriría).

Roadmap F1–F7 completo. Cada fase mantuvo cero regresión para quien no usa motion ni dispara eventos nuevos.


19. Qué queda fuera / diferido

  • Adapter motion-one u otro motor JS externo (opt-in, nunca dependencia base).
  • Drivers exóticos (timelines complejos) — solo con consumer real.
  • Ampliar durations a 7 pasos (estilo Chakra) — solo si 5 se queda corto.

Última revisión: 2026-06-04 — F1–F7 implementadas (dos momentos + firma cross-modal + presence pulida + motor mecánico JS + coreografía + rigor de tokens

  • typegen extensible) + motor reubicado a arts/motion (servicio uix.motion). Si el código diverge, el código gana; abre un issue. Referencias: GUIA_IMPLEMENTACION_SEMAUIX.md (§4.3, §7, §11 — el modelo evento/estado/firma), active_architecture.md §5/§6 (cadena causal, attrs), THEMING.md (tokens), soma/SOMA_ARCHITECTURE.md (Presence), sema/README.md (firma/señales), arts/adom/README.md (reduced-motion).

Powered by TurnKey Linux.