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

699 lines
38 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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.)
> **Extensión de USO — 3 dominios (2026-06-21).** Este documento describe el MOTOR, que tiene
> **dos momentos** (event/state). A nivel de USO el prop `motion` cubre **tres** dominios:
> **event** (la firma) · **state** (la transición per-componente) · **content** (el tercero:
> contenido que entra/sale/loop, vía `motionAttrs` / `<Motion>` / loops `spin`/`pulse`/… — una
> capa de USO **sobre** la maquinaria de state-presets, NO un tercer momento del motor). El
> state-domain dedicado (`data-motion-state`, piloto `<Card>`), la cascada container-driven en
> ambas direcciones y el afford `[data-debug-stagger]` son posteriores a F7. **Para la guía
> orientada a tareas** (recipes · catálogo de presets · loops · state-domain · staggered cascade ·
> reduced-motion · debug) → [`MOTION_GUIDE.md`](./MOTION_GUIDE.md). Para decisiones e historia
> (incl. el motor "coordinado" retirado en el Plan A) → [`MOTION_SERVICE_RFC.md`](./MOTION_SERVICE_RFC.md).
**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](#1-tesis-y-posicionamiento)
2. [Los dos momentos](#2-los-dos-momentos)
3. [Motion en las 4 capas](#3-motion-en-las-4-capas)
4. [Arquitectura — el registro de dos superficies](#4-arquitectura--el-registro-de-dos-superficies)
5. [Los tipos](#5-los-tipos)
6. [La API del `EngineMotion`](#6-la-api-del-enginemotion)
7. [Los drivers](#7-los-drivers)
8. [Contrato DOM](#8-contrato-dom)
9. [Integración con soma (`Presence`)](#9-integración-con-soma-presence)
10. [Reduced motion](#10-reduced-motion)
11. [Primitivas y keyframes](#11-primitivas-y-keyframes)
12. [Contenido inicial (signatures + presets)](#12-contenido-inicial-signatures--presets)
13. [Defaults por componente](#13-defaults-por-componente)
14. [Dónde vive el código](#14-dónde-vive-el-código)
15. [`events.css` ES el momento-evento](#15-eventscss-es-el-momento-evento)
16. [Comparación con Chakra UI v3](#16-comparación-con-chakra-ui-v3)
17. [Decisiones de naming](#17-decisiones-de-naming)
18. [Fases de implementación](#18-fases-de-implementación)
19. [Qué queda fuera / diferido](#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](./MOTION_SERVICE_RFC.md).
> **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](./MOTION_SERVICE_RFC.md).
> **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; el modo opt-in `[data-debug-stagger]` lo materializa (un badge `::after` por
> hijo con su índice, vía un `counter` que espeja `:nth-child - 1`).
---
## 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):
```ts
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 // declarado; hoy NO se auto-aplica
// (el driver spring honra ctx.reduced él mismo)
}
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.
```ts
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 `Animation`s 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):
```css
[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`:
```css
[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`](./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. El primer **built-in JS por nombre** es `spring-pop` (`driver:'spring'`,
`presets/js.ts` → `BUILTIN_JS_PRESETS`), registrado directo en `ActiveEidos` — no
vía el `EidosConfig.motion.presets` serializable, que no puede portar sus funciones
`MotionRun` (`structuredClone` falla). Demos adicionales registran `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-21 — cierre del dominio del prop `motion` (§D.12):
**dominio estado** (atributo dedicado `data-motion-state` + preset énfasis
`select-pop`, piloto `<Card>`), **loops de contenido** (`spin`/`pulse`/`ping`/`bounce`,
reglas un-gated infinitas) + fix del exit de `<Motion>` (`tick` no-op), afford de
debug `[data-debug-stagger]`, y primer **built-in JS por nombre** `spring-pop`
(driver `spring` estrenado, registrado en `ActiveEidos`; demostrado sobre
`Popover.Content motion="spring-pop"`). Demo de loops+lifecycle en
`web/routes/demos/motion`. Previo (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.