|
|
# 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).
|