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 canalmotionde 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 (propmotion). 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 deevents.cssal registro (signatures, F2); los recipes migrados a presets con override de duración/easing por componente (F3); los drivers JSspring(física) /waapi/rect(FLIP) +Presence.motion+ un overlay real (Dialog) brincando (F4); la coreografía stagger + presets Materialshared-axis/fade-through(F5); el rigor de tokens — firma cruda tokenizada (holds largosslower/deliberate/emphatic/sustained), shared-axis--motion-distance-xl, easingemphasized, set[data-motion-set='expressive'](F6); el typegen extensible de nombres de preset — registry augmentableEidosMotionPresets(F7). Roadmap F1–F7 completo.data-motion-refy el "TSCevent:*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 comouix.motiony consumido por ambas capas — soma (Presencerecibemotion: EngineMotiony llamamotion.run(node, phase)) y eidos (delega víaeidos.motion+ registra sus presetscssen el servicio al boot). Esto disuelve el acoplamiento soma→eidos: desaparecen la propDialogProps.runMotionyeidos.motionRunner— el bridge es ahoraEngineMotion.run, que leedata-animation-styledel 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. Versrc/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-statees 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
- Tesis y posicionamiento
- Los dos momentos
- Motion en las 4 capas
- Arquitectura — el registro de dos superficies
- Los tipos
- La API del
EngineMotion - Los drivers
- Contrato DOM
- Integración con soma (
Presence) - Reduced motion
- Primitivas y keyframes
- Contenido inicial (signatures + presets)
- Defaults por componente
- Dónde vive el código
events.cssES el momento-evento- Comparación con Chakra UI v3
- Decisiones de naming
- Fases de implementación
- Qué queda fuera / diferido
1. Tesis y posicionamiento
Una interacción no tiene un momento animable, tiene dos, y son de naturaleza distinta:
- La ocurrencia perceptiva — "algo acaba de pasar" — transitoria, con
hold,intentysequence. La escribe sema comodata-event-*. - 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.dialogabriendo: el flourish del eventoemerge.presenty la transición de estado aopen.collapse: la transición de altura (estado) y/o el flourish del eventoexpand.
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/ presetscascade-*sobredata-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 enlib/render-css.ts). La firma del evento (emerge→present-rise) es solo el flourish del contenedor (genérico, sobredata-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
motionUNIVERSAL — un selector, tres dominios (encuadre de diseño, [RFC §D.12]). Tras retirar el coordinado, queda UN sistema → un solo propmotion(hoy<Cascade>aún usaanimation; se unifica). La meta: cualquier componente (no solo overlays) puede recibirmotion="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-childignora comentarios/{#if}(blindado), pero un wrapper-elemento intermedio rompe el conteo → el agrupador que cascadea se vuelve su propio scope (inherits:falseaísla el índice);display:contentssin re-scope = dead-zone. (2) exit — unit-exit porPresence(retiene + espera, elopacitydel 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 esinherits: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
signatureses la firma genérica: uncommitasienta igual en todo el sistema; unemerge.presententra igual. Los packs de sema (sema/components/*.ts) afinan la firma de un componente concreto. Es lo que hoy haceevents.cssa mano (§15).presetses la transición per-componente, elegida con la propmotion.- 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 ponedata-animation-styley la regla CSS[data-animation-style][data-state]+ laPresencede 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 laReducePolicy. - El momento-evento (signatures) no pasa por
enter/exit: es CSS generado que reacciona adata-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.reduceddeActiveDom.prefersReducedMotion; aplica laReducePolicyantes de correr. - Morfo declara
a11ySemantic.reducedMotionFallbackpor 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(NOdata-motion, que está reservado para la pref de reduce-motionallow/reduce). - Attrs del momento-evento: los
data-event-*que sema ya estampa. - Limpieza (F7) ✓: el
data-motiondireccional de NavMenu (un rename planeado) no existe en el código — moot.data-motion-refy el "TSCevent:*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) + motorEngineMotion(serviciouix.motion, reubicado a$motionen el refactor) + validación + tests. -
F2 — Firma cross-modal ✓ (supera SwiftUI):
events.cssmigrado asignatures(§15); elmotionde la firma + sonido + haptic salen del MISMO evento (acoplamiento que el.sensoryFeedbackde 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 serviciouix.motion, consumido por soma y eidos sin acoplamiento); Dialog real brincando. -
F5 — Coreografía ✓ (iguala Material 3): stagger declarativo (
--motion-stagger-{index,each}), presetsshared-axis-x/y+fade-through; container-transform vía el driverrect. -
F6 — Rigor de tokens ✓ (iguala Carbon): la firma cruda tokenizada — la escala de duración gana el tramo largo de holds perceptivos (
slower400 ·deliberate600 ·emphatic800 ·sustained1000ms); los pulsosannounceescalan 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/commitsnapean a la escala existente (sub-perceptible). Easingemphasized(M3 emphasized-decelerate). Sets productive/expressive (Carbon):primitives.motion.expressiveemite 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 driverrectla haría por medición si un consumidor lo pide). -
F7 — Extensibilidad + typegen ✓ (más allá de todos): los nombres de preset del momento-
--stateson type-safe + app-extensibles vía un registry augmentable —EidosMotionPresets(espejo deSemaChannelSignatures), conMotionPresetName = keyof EidosMotionPresets | 'none' | (string & {}). Una app añade presets type-safe condeclare module '$uix/eidos' { interface EidosMotionPresets { … } }: la propmotionlos autocompleta y un typo es error de compilación. El motor ($motion/uix.motion) quedastring(abierto en runtime) — el registry es una ergonomía de compile-time sobre las props; un test fija el set built-in contra él. Eldata-motiondireccional de NavMenu ya no existe (rename moot).transitioncon 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-oneu 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(serviciouix.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).