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/MOTION_SERVICE_RFC.md

1418 lines
100 KiB

# RFC — Servicio de motion de UIX (coordinación de presencia cross-layer)
refactor(motion): retire parallel orchestration motor (Plan A) + verified model + Fase 1 prototype Remove the parallel motion-coordination service per the redesign: animation is a channel of the EVENT's firma (sema owns it across all channels), not a parallel axis. Code is gone; the RFC stays as historical record with a retirement banner. - morfo: drop MorfoPart.animation + its schema/compile/types/exports - arts/motion: drop CoordinatedPreset / MotionConfig.coordinated - eidos: drop BUILTIN_COORDINATED_PRESETS / renderCoordinatedPresetRules / the animation:none neutralization + registry block; regen generated/base.css - soma: Presence reduced to a single-surface island; delete presence-group / dom-cascade / coordination; dropdown-menu loses the cascade wiring - delete Reveal / Rail (morfo + soma + demos) Docs: MOTION_SERVICE_RFC gains the retirement banner + §D.8 (retirada) + §D.9 (verified model: morfo->soma->sema->eidos pipeline + the two hard rules — soma never writes a visual --var; data-event-* is a single-target stamp, not a bus) + §D.10 (Fase 1 prototype). Fix stale "5 canales" claim in GUIA §11; eidos-motion + dropdown README aligned. Fase 1 prototype (web/routes/temas/animations/panel-cascade): validates the model end to end — panel->cards cascade (enter/exit), dynamic removal with Svelte out: retention, nested cascade — all via :nth-child + custom-property inheritance, with zero JS visual writes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
> ## ⚠️ RETIRADA — diseño implementado y luego ELIMINADO (2026-06-19, «Plan A»)
>
> **El servicio de orquestación paralelo que diseña el cuerpo de este RFC (§4–§9 +
> Apéndices B/C: `MorfoPart.animation`, la prop `animation` ruteada, `PresenceGroup`,
> `DomCascade`, los presets coordinados `cascade-*` / `MotionConfig.coordinated`, la
> neutralización `animation: none !important`, y sus consumidores `Reveal` / `Rail`)
> se implementó, se validó… y se ELIMINÓ por completo.** El código ya no existe; este
> documento queda como **registro histórico** del diseño y su razonamiento.
>
> **Por qué (corrección del usuario — Apéndice D.2).** La animación NO es un eje
> paralelo: es **un canal de la firma del EVENTO**, y la firma la posee **sema en
> TODOS sus canales** (motion incluido). Modelar el motion coordinado como un motor
> propio —fuera del evento— producía DOS firmas para el mismo canal que convivían
> neutralizándose (Grieta 1, D.1). El libro pide UNA firma por evento, coordinada
> alrededor del evento.
>
> **El reparto correcto (hacia donde se reconstruye):**
> - **sema** lanza el evento: proyecta `data-event-*` + sonido/haptic + el hold.
> - **eidos** materializa el canal motion + la coordinación visual **reaccionando a
> `data-event-*`** (único dueño de lo visual; `arts/motion` sobrevive como _island_
> de progressive-enhancement para lo que CSS no puede — spring / FLIP).
> - **soma** posee el lifecycle (retener / esperar / desmontar): el `Presence` island,
> que se conserva.
> - La coordinación padre↔hijo **sigue siendo necesaria**, pero **cuelga del evento de
> sema**, no de un motor paralelo.
>
> **Qué se conservó:** `arts/motion` (engine spring/waapi/rect), el `Presence` island
> (`soma/layers/presence.svelte.ts`, reducido a una sola superficie), los **state-presets**
> (`motion` prop → `data-animation-style` sobre `data-state`: `scale-fade`, `slide-fade`,
> Material shared-axis/fade-through), las **firmas sema** visuales (`present-rise` /
> `dismiss-fade`) y el **stagger Material** (`--motion-stagger-{index,each}`). Modelo
> vigente: [`eidos-motion.md`](./eidos-motion.md) (dos momentos `--event` / `--state`).
>
> **El modelo hacia adelante** está en el **[Apéndice D](#apéndice-d--re-análisis-2026-06-16-grietas-y-principios-del-re-diseño)**
> (re-análisis 2026-06-16): principio rector, discriminante de los tres dominios, plan.
> La retirada de 2026-06-19 ejecutó de golpe la limpieza que D.6 preveía incremental
> (pasos 3 + 5): dejar el terreno limpio antes de reconstruir sobre el evento.
>
> ---
>
> Hermano de `eidos-motion.md` (que **extiende**, no contradice) y de los RFC de engine
> (`COLOR_ENGINE_RFC.md`, `DEPTH_ENGINE_RFC.md`, …). A diferencia de aquéllos —engines
> **visuales** de eidos— éste es un **servicio cross-layer** (morfo + soma + eidos + arts):
> de ahí `_SERVICE_` y no `_ENGINE_`.
>
> **Qué resuelve.** Hoy el motion de UIX anima **superficies aisladas**: `EngineMotion` es
> por-nodo, `Presence` coordina una sola superficie, y la coreografía entre superficies
> (incluida la de los hijos) se **delega a la app**. Este RFC diseña que el desarrollador
> pueda **aplicar una animación a cualquier componente sin nada más** —el sistema ya sabe
> sobre qué superficie— y que las superficies se **coordinen** (secuencia / paralelo /
> stagger, padre↔hijo).
>
> **La línea que ordena todo el documento es state ↔ visual:**
>
> - **morfo** declara la _estructura_ (qué partes son superficies animables y sus
> dependencias de coordinación);
> - **soma** coordina la _presencia / lifecycle_ del árbol (mount / unmount / await /
> cancel) — **nunca el cómo-se-ve**;
> - **eidos** posee el _QUÉ visual_ y los _valores expresivos_ (keyframes, easing, stagger-ms);
> - **arts/motion** ejecuta primitivas **por-nodo** (no orquesta, no gana jerarquía).
>
refactor(motion): retire parallel orchestration motor (Plan A) + verified model + Fase 1 prototype Remove the parallel motion-coordination service per the redesign: animation is a channel of the EVENT's firma (sema owns it across all channels), not a parallel axis. Code is gone; the RFC stays as historical record with a retirement banner. - morfo: drop MorfoPart.animation + its schema/compile/types/exports - arts/motion: drop CoordinatedPreset / MotionConfig.coordinated - eidos: drop BUILTIN_COORDINATED_PRESETS / renderCoordinatedPresetRules / the animation:none neutralization + registry block; regen generated/base.css - soma: Presence reduced to a single-surface island; delete presence-group / dom-cascade / coordination; dropdown-menu loses the cascade wiring - delete Reveal / Rail (morfo + soma + demos) Docs: MOTION_SERVICE_RFC gains the retirement banner + §D.8 (retirada) + §D.9 (verified model: morfo->soma->sema->eidos pipeline + the two hard rules — soma never writes a visual --var; data-event-* is a single-target stamp, not a bus) + §D.10 (Fase 1 prototype). Fix stale "5 canales" claim in GUIA §11; eidos-motion + dropdown README aligned. Fase 1 prototype (web/routes/temas/animations/panel-cascade): validates the model end to end — panel->cards cascade (enter/exit), dynamic removal with Svelte out: retention, nested cascade — all via :nth-child + custom-property inheritance, with zero JS visual writes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
> **Estado: RETIRADO (2026-06-19).** El texto a continuación (§0–§15, Apéndices A–C)
> documenta el diseño **tal como llegó a implementarse** —M1–M4 + M5/M6 + M9, con
> `PresenceGroup` / `DomCascade` / presets coordinados y los consumidores `Reveal` /
> `Rail` / `dropdown-menu`— y que se **eliminó por completo**. Ver la nota de retirada
> al inicio y el [Apéndice D](#apéndice-d--re-análisis-2026-06-16-grietas-y-principios-del-re-diseño)
> para el modelo hacia adelante. Se conserva como registro histórico.
## Tabla de contenidos
- [0. Tesis](#0-tesis)
- [1. El estudio — referentes y dónde topan](#1-el-estudio--referentes-y-dónde-topan)
- [2. No-goals explícitos](#2-no-goals-explícitos)
- [3. Dónde está UIX hoy (estado verificado + gap)](#3-dónde-está-uix-hoy-estado-verificado--gap)
- [4. Superficie animable declarada en morfo (solo estructura)](#4-superficie-animable-declarada-en-morfo-solo-estructura)
- [5. Variants separables + naming (`animation` ↔ `data-animation-style`)](#5-variants-separables--naming-animation--data-animation-style)
- [6. Routing automático vs choreography opt-in](#6-routing-automático-vs-choreography-opt-in)
- [7. Coordinación de presencia — el `PresenceGroup`](#7-coordinación-de-presencia--el-presencegroup)
- [7.1. El árbol de presencia es lógico (context), NO DOM](#71-el-árbol-de-presencia-es-lógico-context-no-dom)
- [8. Salida, interrupción y reversa (el caso difícil)](#8-salida-interrupción-y-reversa-el-caso-difícil)
- [9. Motor: agregación por el `PresenceGroup`, no `subtree:true` ciego](#9-motor-agregación-por-el-presencegroup-no-subtreetrue-ciego)
- [10. Reduced-motion · firma semántica · SSR/hydration](#10-reduced-motion--firma-semántica--ssrhydration)
- [11. Integración de los efectos WIP](#11-integración-de-los-efectos-wip)
- [12. Encaje con las 4 capas + qué NO hace](#12-encaje-con-las-4-capas--qué-no-hace)
- [13. Validación — prototipo exit-heavy](#13-validación--prototipo-exit-heavy)
- [14. Impacto sobre el código actual + migración](#14-impacto-sobre-el-código-actual--migración)
- [15. Roadmap de implementación por fases](#15-roadmap-de-implementación-por-fases)
- [Apéndice A — decisiones resueltas vs abiertas](#apéndice-a--decisiones-resueltas-vs-abiertas)
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
- [Apéndice B — Convivencia con la firma sema (los tres sistemas visuales)](#apéndice-b--convivencia-con-la-firma-sema-los-tres-sistemas-visuales)
- [Apéndice C — M9: el modo children-DOM (la segunda coordinación)](#apéndice-c--m9-el-modo-children-dom-la-segunda-coordinación)
---
## 0. Tesis
> **Una animación se _aplica_, no se _envuelve_. La coreografía se _declara_ en el contrato,
> no se _escribe_ en cada call-site. Y la coordinación entre superficies es lifecycle
> (state), no pintura (visual).**
Hoy, para animar de forma coordinada hay que envolver elementos a mano (Framer:
`<motion.div>` por nodo + `<AnimatePresence>`) o escribir timelines imperativas (GSAP). UIX
ya tiene un contrato declarativo cross-layer (**morfo**) y un lifecycle de presencia headless
(**`Presence`**, en soma). La apuesta de este servicio es **proyectar la coreografía sobre
ese contrato**: el componente declara _qué partes son superficies animables y cómo se
coordinan_, soma generaliza `Presence` de una superficie a un **árbol de presencia**, y el
desarrollador solo **nombra** la animación. El sistema sabe **dónde** aplicarla (la superficie
está declarada) y **cuándo** (la coordinación está declarada).
Tres propiedades que ningún referente reúne, y que NO incluyen "coreografía declarativa" a
secas (Framer ya la tiene):
- **(a)** la coreografía vive en el **contrato (morfo)**, una vez por componente, no en el
JSX de cada uso;
- **(b)** **enrutado automático a la superficie** — Framer no lo tiene porque allí envuelves
manualmente;
- **(c)** **composición con la firma semántica** (sema): la misma `commit + fulfill` que
dispara sound/haptic/visual coordina también la entrada de los hijos.
---
## 1. El estudio — referentes y dónde topan
| Framework | Modelo | Coreografía padre↔hijo | Límite estructural |
| ---------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **Framer Motion** | `variants` declarativos + `<AnimatePresence>` para exit | ✅ `when: 'beforeChildren'/'afterChildren'`, `staggerChildren`, `delayChildren` | la coreografía vive en el **JSX de la app** (cada call-site la cablea); el target se **envuelve a mano** (`<motion.div>`); atado a React |
| **GSAP** | timelines imperativas (`tl.to(...).to(..., '<')`) | ✅ manual, control total | **imperativo**: el guion se escribe a mano; ninguna relación con el contrato del componente |
| **Svelte transitions** | directivas `in:`/`out:`/`transition:`, `animate:flip` | ⚠️ parcial (`flip` por lista; sin `when`/stagger jerárquico) | por-elemento; sin coordinación de árbol con dependencias |
| **AutoAnimate** | `autoAnimate(el)` zero-config sobre cambios de children | ❌ (anima cambios, no coreografía dirigida) | una sola caja; sin secuencia/paralelo declarado |
| **Chakra (Ark)** | `data-state` + `Presence` (un eje) | ❌ | colapsa la animación de presence en un solo eje; sin firma ni árbol |
**El límite común** —incluso Framer, el más fino— es que la coreografía es **algo que el
consumidor escribe en cada uso** y el target es **algo que el consumidor envuelve**. Nadie la
trata como **propiedad declarada del componente** ni la **compone con la semántica del
evento**. Ahí está el hueco (a)(b)(c) de §0.
> **Honestidad obligatoria (§1 no sobreafirma):** Framer Motion **ya hace** coreografía
> declarativa padre↔hijo. Nuestro `children: { enter, exit }` (§4) es ~1:1 con su `when`. Reclamar
> "coreografía declarativa" como novedad sería falso y haría perder credibilidad al resto.
> Lo nuevo es **dónde vive** (el contrato, no el call-site), el **enrutado** y la
> **composición con sema**.
---
## 2. No-goals explícitos
Declarar el alcance evita que un revisor lo pida y que el servicio se desenfoque.
1. **Layout animations / shared layout / reordenación automática** — **no-goal a propósito.**
El "shared layout" de Framer (un elemento que parece viajar entre dos posiciones de
layout, `layoutId`) NO entra. Matiz técnico: el driver `rect` (FLIP) de `arts/motion`
mide **un solo nodo** (`el.getBoundingClientRect()` antes/después) — sirve para que **una**
superficie absorba su propio salto de layout, **no** para transiciones de elemento
compartido entre dos árboles. El RFC corta esa expectativa de raíz.
2. **Fondos / efectos WebGL** (`web/routes/demos/animations/background/*`: aurora, galaxy,
particles, liquid-image, …) — **fuera del servicio.** No son superficies enter/exit; son
efectos ambientales full-canvas. Pertenecen a otro eje (un art / componentes "ambient"
tipo `<Background variant="aurora">`), que se diseñará por separado.
3. **Reinventar GSAP** — el servicio **no** construye timelines imperativas de propósito
general. El JS coordina **presencia** (timing de mount/unmount/await), no compone
tweens arbitrarios.
Lo que **sí** es goal: los **text-effects** WIP (blur/count/scrambled/…) entran como
candidatos a _variants de contenido_ (§11), porque sí son animación de contenido al entrar.
---
## 3. Dónde está UIX hoy (estado verificado + gap)
Verificado leyendo el código, no por memoria:
- **El motor es por-nodo.** `EngineMotion.run(el, phase)` lee el `data-animation-style` de
_ese_ nodo, resuelve el preset y lo corre; `pending(el)` / `cancel(el)` se indexan por
elemento (`Map<HTMLElement, Set<MotionHandle>>`). No conoce árbol, padres ni hijos.
→ `src/arts/motion/engine-motion.ts`.
- **`Presence` es una isla por superficie.** Coordina una sola superficie (`opts.ref.current`)
y espera `node.getAnimations()` **sin `{ subtree: true }`**. Un Dialog o un ContextMenu
crean **varios `new Presence` independientes** (overlay, content) que coinciden en el tiempo
por casualidad, no por coordinación. → `src/uix/soma/layers/presence.svelte.ts`.
- **La coreografía se delega a la app.** El propio código de los presets Material shared-axis
lo dice: `// Per-element here; an app triggers both sides together for the "shared" effect.`
→ `src/uix/eidos/lib/motion/presets/css.ts`.
- **La selección del variant vive en eidos.** El momento-estado se aplica vía prop
`motion="scale-fade"` → atributo `data-animation-style`. → `eidos-motion.md` (TL;DR).
- **morfo no declara nada de animación.** `MorfoPart` declara `data` / `aria` / `keyboard` /
`states` / `archetype` / `parts`, pero **ningún concepto de "superficie animable"** ni de
coordinación. Lo más cercano es `events[].semantic.sequence` (`pre|coincident|post`), que
ordena el momento-evento vs el momento-estado **del mismo componente**, no entre superficies.
→ `src/uix/morfo/types.ts`.
**El gap, en una frase:** UIX anima superficies sueltas; no hay un árbol de presencia que las
coordine. Este RFC añade ese árbol **en soma** (donde ya vive el lifecycle de presencia),
declarado **en morfo** y vestido **en eidos**.
---
## 4. Superficie animable declarada en morfo (solo estructura)
Una `MorfoPart` gana un campo opcional. **Solo lleva estructura** — nada visual ni expresivo:
```ts
// src/uix/morfo/types.ts — añadido a MorfoPart
animation?: {
/** Esta part es una superficie animable: tiene lifecycle de presencia coordinable. */
surface?: boolean
/** Dependencia de coordinación con las superficies hijas (opt-in: declararlo activa la coreografía). */
children?: {
/**
* Relación temporal del padre con sus hijos, INDEPENDIENTE por fase. Cada valor:
* - 'before' → el padre actúa ANTES (enter: entra y luego suelta a los hijos;
* exit: se va antes que ellos)
* - 'after' → el padre actúa DESPUÉS (exit: RETIENE su DOM hasta que los hijos
* terminen de salir — §8.1)
* - 'together' → padre e hijos en paralelo (default de la fase ausente)
*/
enter?: 'before' | 'after' | 'together'
exit?: 'before' | 'after' | 'together'
}
}
```
**Por qué `enter`/`exit` por separado y no un `when` simétrico.** Un único `when` ata las dos
fases a la misma relación, y el caso más común —un **contenedor que bracketea a sus hijos**—
**no es simétrico**: entra ANTES que ellos (`enter: 'before'`) y sale DESPUÉS (`exit: 'after'`,
reteniendo su DOM hasta que terminen). El prototipo (§13) lo destapó: con un `when` simétrico el
bracket era inexpresable. Separar las fases lo hace declarable sin coste — la fase ausente cae
a `'together'`.
**Por qué van en morfo y el stagger-ms NO.** `enter`/`exit` tienen **consecuencia de
lifecycle**: `exit: 'after'` obliga a soma a **retener el DOM del padre** hasta que los hijos
terminen su salida (§8.1). Eso es state, lo necesita soma → es estructura → morfo. En cambio el
**valor** del stagger (80 ms), el easing, los keyframes y el orden visual son **expresivos** →
viven en eidos (tokens/recipe), nunca en morfo. La regla 2-de-3 lo confirma: el campo lo
consumen **soma** (lifecycle) y **eidos** (lo viste) — dos capas → justificado en el contrato.
> **Nota sobre `order`.** El _orden_ del escalonado (forward/reverse) es expresivo y vive en
> eidos. Soma no lo necesita: el escalonado lo pinta el CSS (§7). Lo que soma observa es el
> **orden de registración** (§7.1), que el RFC fija = document order.
El compilador (`compile.ts`) traduce este campo a un _plan de coordinación_ que el
`PresenceGroup` consume; el validador (`schema.ts`) comprueba que `children` solo aparezca en
parts con `surface: true` y que las partes referenciadas existan.
---
## 5. Variants separables + naming (`animation` ↔ `data-animation-style`)
**El objetivo de DX:** el dev escribe `animation="scale-fade"` en **cualquier** componente
animable y el sistema lo **enruta** a la superficie declarada (la part con `surface: true`),
sin que el dev sepa qué part es. "El sistema ya sabe dónde."
Esto convierte un variant en **separable y reutilizable**: `scale-fade` no pertenece al
Dialog; pertenece al catálogo y se aplica a cualquier superficie. Es el momento-estado de
`eidos-motion.md`, elevado de "prop de un wrapper concreto" a "prop transversal enrutado".
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
**Decisión M5 (TOMADA): `animation` y `motion` quedan SEPARADAS por rol.**
La versión original de este RFC recomendaba aquí _unificar_ (`animation` como cara DX y `motion`
como alias deprecable), asumiendo que ambas nombraban el MISMO eje (el momento-estado elevado a
transversal). **El hallazgo M6 lo invalidó** (el «hallazgo gemelo» de más abajo + Apéndice B):
`motion` y `animation` gobiernan sistemas visuales **distintos** — un state-preset (`data-state`,
dispara al MONTAR) metido en un coordinado ROMPE la cascada. Es **distinción, no duplicación**; y
lo "caro" era la duplicación, no la distinción. Se mantienen separadas porque el **nombre de la
prop comunica el sistema** a nivel de propiedad, sin leer docs:
| Prop | Sistema | Reacciona a | Capa dueña | Catálogo type-safe |
| --------------- | ----------------- | ------------------------------ | ---------- | ---------------------------------------- |
| **`motion`** | momento `--state` | `[data-state]` | eidos | `EidosMotionPresets` (`scale-fade`…) |
| **`animation`** | coordinado | `[data-starting/ending-style]` | soma | `MotionCoordinatedPresets` (`cascade-*`) |
Ambas emiten `data-animation-style` (el contrato de bajo nivel del motor, que **se conserva**); lo
que difiere es A QUÉ reacciona el CSS y QUIÉN la tipa. El **type-safety de `animation`** vive en un
registry augmentable `MotionCoordinatedPresets` en **`$motion`** (la capa compartida, para que soma
lo tipe sin importar eidos — una capa inferior no depende de una superior); **eidos lo puebla** con
sus `cascade-*` por declaration-merging — espejo exacto de `EidosMotionPresets`. Esto **cierra** la
línea abierta del Apéndice A ("destino de `motion`": se queda, con rol propio).
> **Estado — incremento M5 (hecho, sobre `Reveal`).** El enrutado existe: `<Reveal.Provider
animation="X">` se nombra una vez en la raíz y el provider lo enruta a las parts `surface: true`
> del morfo COMPILADO (`routeAnimation(kebab)` consulta el set de superficies derivado de
> `compileMorfo`; una part no-superficie —el Trigger— recibe `undefined`, nunca se mis-targetea).
> Los wrappers Panel/Item emiten `data-animation-style` con el valor enrutado. Unit-tested.
>
> **Hallazgo — el enrutado da el QUÉ, no el CUÁNDO.** Los presets BUILT-IN de eidos (`scale-fade`,
> `slide-fade`…) reaccionan a `data-state` (disparan al MONTAR) → NO componen con superficies
> COORDINADAS, donde el cuándo lo gobierna el `data-starting-style`/release del `PresenceGroup`: un
> preset data-state dispararía todo al montar y rompería la cascada. Por eso la demo reacciona a
> `[data-animation-style='X'][data-starting-style]` (el attr da el QUÉ; los attrs del Presence dan el
> CUÁNDO). Reconciliar la LIBRERÍA de presets con superficies coordinadas (un preset/variante que
> reaccione a `data-starting/ending-style`) está **HECHO (M6)**: la librería de eidos gana
> `MotionConfig.coordinated` — presets `cascade-slide`/`-fade`/`-scale` que eidos genera como
> transición (off-state + stagger reversible) sobre los attrs del Presence, no `data-state`. El motor
> nunca los corre (CSS puro). Un coordinado hace `animation="cascade-slide"` sin CSS propio.
> **Hallazgo gemelo — la FIRMA sema genérica también choca.** El mismo patrón con el otro sistema
> visual de eidos: al disparar `runtime.trigger('open'/'close')`, sema estampa `data-event-*` y eidos
> corre la firma genérica `emerge` (`present-rise`/`dismiss-fade … forwards`, `generated/base.css`)
> ENCIMA de la cascada coordinada. En `Reveal` se vio como "otra animación" al cerrar con el botón
> (que va por `runtime.trigger`) ausente al usar el reversa (que pone `open` directo). **Doctrina: un
> componente de movimiento COORDINADO silencia la señal sema genérica** — sus eventos declaran
> `semantic.channels: []` (el engine corta antes de estampar, `sema/engine.ts`) y `expression: 'none'`:
> su percepción ES la coordinación, no la firma. Generaliza el hueco §5: la cascada coordinada
> (data-starting/ending-style) es un tercer sistema visual que NO compone con los otros dos de eidos
> (presets data-state · firmas data-event) — un coordinado debe optar fuera de ambos.
---
## 6. Routing automático vs choreography opt-in
Lo que parecía una decisión binaria ("¿coordinación automática u opt-in?") se disuelve
separando **dos cosas distintas**:
- **Routing a superficie = automático / zero-config.** El dev nunca cablea superficies:
`animation="..."` se enruta solo a la part `surface: true`. Es el corazón del "sin nada más".
- **Coreografía padre↔hijo = opt-in por construcción.** Solo ocurre si la part **declara**
`children` en morfo (§4). Un componente que no lo declara nunca arrastra a sus hijos.
El beneficio combinado: coherencia con "sin nada más" **sin** el riesgo de que un padre
empiece a **esperar misteriosamente** a una superficie anidada profunda que el autor no quería
coordinar. La coordinación es explícita en el contrato; el routing es implícito en el uso.
---
## 7. Coordinación de presencia — el `PresenceGroup`
`Presence` hoy gestiona el lifecycle de **una** superficie: monta, marca
`data-starting-style`/`data-ending-style`, espera las animaciones, desmonta. El servicio lo
**generaliza a un árbol** con una clase hermana — **`PresenceGroup`** (el nombre es
deliberado: coordina _presencia_, state; no _motion_, visual):
```ts
// src/uix/soma/layers/presence-group.svelte.ts (nuevo)
// Presence generalizado de 1 superficie a un árbol. Pure lifecycle/state — NO visual.
class PresenceGroup {
static create(structure: AnimationStructure): PresenceGroup; // root → set in context
static get(): PresenceGroup | undefined; // children discover it
/** A surface (or a nested group) cedes its "when" to this group. */
register(member: PresenceMember): () => void; // returns deregister
/** Coordinate the tree for a phase: ordered mount/unmount/await + aggregate `finished`. */
play(phase: 'enter' | 'exit'): Promise<void>;
/** Propagate cancellation to every registered member (interrupt/reverse). */
cancel(): void;
}
```
`Presence` gana una opción `group?` (descubierta por context). Si hay grupo, la superficie
**cede el timing**: en vez de arrancar su `motion.run` por su cuenta en `handleOpen`, se
**registra** y espera la señal del grupo (`release(phase)`). Si no hay grupo, `Presence`
**degrada al comportamiento de hoy** (isla autónoma) — backward-compat total.
**Qué coordina el grupo y qué NO.** El grupo decide _cuándo_ existe / monta / espera / cancela
cada superficie (state). El **cómo-se-ve** sigue siendo de eidos: el **stagger visual lo pinta
el CSS** (`animation-delay` escalonado por índice, como hoy). Soma **solo monta y espera el
`finished` agregado** — **nunca calcula un milisegundo**. Esta es la línea state/visual hecha
mecanismo: el grupo no conoce el valor del stagger; lo pone el delay CSS, y el grupo solo
agrega los `finished` (que ya incluyen ese delay).
**Las dos direcciones** (consecuencia de `children.when`):
```
play('enter'):
when 'before' → corre la superficie del padre; al (o durante) su animación, libera a los hijos
when 'together' → libera a todos a la vez (el stagger CSS los escalona visualmente)
when 'after' → libera a los hijos; cuando su finished agrega, corre la del padre
play('exit'):
when 'after' → emite exit a los hijos, RETIENE el DOM del padre, agrega su finished,
y solo entonces corre la salida del padre y libera la retención (§8.1)
```
**Modelo de coordinación — DECISIÓN (recomendación: híbrido declarativo).** Tres opciones:
| Modelo | Cómo | Veredicto |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| **Híbrido declarativo** _(recomendado)_ | morfo declara la estructura; soma coordina el timing y agrega `finished`; **el stagger/escalonado lo pinta CSS** (eidos); el motor ejecuta por-nodo. El JS solo coordina presencia. | respeta la línea state/visual; no reinventa GSAP; reusa `Presence` |
| **Variants propagados (Framer)** | el padre propaga su fase a los hijos por context con `staggerChildren`/`when` en el árbol de componentes | mete lógica de coordinación en el árbol de componentes; más cerca de soma que de morfo; el timing visual se filtra a JS |
| **Timeline / grafo explícito** | un orquestador construye una timeline con offsets/dependencias y la ejecuta vía WAAPI groups / scheduler | máximo control; pero maquinaria JS pesada y riesgo de reinventar GSAP / alejarse de lo declarativo |
El híbrido es el único que mantiene a soma fuera de los valores visuales.
### 7.1. El árbol de presencia es lógico (context), NO DOM
La pregunta clave: _¿cómo se gestiona el árbol si entre un padre animable y sus hijos
animables hay componentes sin animación? Los padres tienen "saltos" en el árbol._
**Respuesta: el árbol que se coordina es el de superficies animables proyectado sobre el
`context` de Svelte, no el árbol DOM.** Y el context **no depende de adyacencia DOM**, así que
los saltos se resuelven solos:
- Un ancestro animable pone un `PresenceGroup` en context.
- Los componentes **sin animación** (wrappers de layout, `{#if}`, una `<ul>` tonta) son
**transparentes**: no crean grupo ni se registran. El context del grupo **fluye a través de
ellos** sin que lo toquen.
- El siguiente descendiente animable —a la profundidad DOM que sea— hace `PresenceGroup.get()`,
encuentra el grupo del **ancestro animable más cercano** y se **registra**. El padre conoce
a sus hijos animables **por registración, no por proximidad DOM**.
- Un intermedio que **sí** es animable crea su propio grupo y **corta el descenso** (caja
negra, §9) — única frontera del árbol lógico.
Implicaciones que el resto del RFC hereda:
1. **El count no sale del DOM** (los descendientes cuelgan a profundidad arbitraria) → §8.2.
2. **El orden del stagger = orden de registración** (= montaje = document order), modulado por
el `order` visual de eidos; **nunca la profundidad DOM**.
3. **La retención en exit aguanta el salto**: retener el **nodo del ancestro** retiene
transitivamente su subárbol entero (wrappers incluidos) → §8.1 no se rompe.
4. **Des-registración abrupta** (caso de borde): un wrapper intermedio con su propio `{#if}`
que se desmonta deja a sus hijos animables sin exit coordinado → el grupo debe tolerar un
**set dinámico** de registros (aparecen/desaparecen) y **no colgar** su promesa por un
miembro que se fue (liga con el timeout anti-deadlock de §8.2).
---
## 8. Salida, interrupción y reversa (el caso difícil)
La entrada en cascada (`enter` + stagger) es trivial; todo framework la borda. El estrés real
—donde los sistemas mueren— está en la **salida coordinada** (`after`) y en la **interrupción**
(un `enter` a medias que pasa a `exit`). Esta sección combina el lifecycle de soma con el
contrato de morfo para resolverlo.
### 8.1. Retención del DOM en exit coordinado
Cuando un padre declara `exit: 'after'`, el nodo padre **no puede desmontarse** hasta que
todos los hijos orquestados terminen su animación de salida.
**Mecanismo:** el `PresenceGroup` del padre **retiene el bloque en el DOM** (vía el
`shouldRender` de `Presence` / Svelte). Al dispararse `play('exit')`, emite la señal de salida
a sus hijos registrados, **recolecta sus `finished` agregados**, y **solo cuando todas
resuelven** ejecuta su propia animación de salida; al terminar, **libera la retención** y el
nodo se desmonta. El **escalonado visual** de la salida lo pone el CSS (eidos,
`animation-delay`); soma solo **espera** el agregado. Coherente con la línea state/visual: el
grupo nunca decide cuánto se escalona, solo cuándo desmontar.
### 8.2. Cierre del conjunto de registración (el problema async)
En render condicional o diferido, el padre **no puede** esperar solo "las superficies que se
hayan registrado en este tick": podría asumir que no hay hijos antes de que terminen de
montar. La **bala de plata es el contrato** — pero la **fuente del count se bifurca** (porque
con saltos, §7.1, el DOM no sirve para contar):
- **Cardinalidad fija** (parts nombradas: `overlay` + `content`) → el count exacto lo da
**morfo** (sabe cuántas superficies-hijas declaró). El padre **bloquea** hasta que
`registros == count`, con un **timeout de seguridad anti-deadlock** (si un hijo se excluye
lógicamente del render, no se cuelga para siempre).
- **Colecciones** (part `item` repetida N veces — el caso del prototipo §13) → morfo declara
el **tipo** de superficie hija, **no el número** de instancias (el count es runtime). El
count lo aporta el **provider de la colección** (conoce su `items.length`) o un _commit_
explícito cuando la colección termina de montar.
> Esta bifurcación es directa consecuencia de §7.1: si el árbol es lógico y hay saltos,
> contar por estructura DOM es imposible; el count tiene que venir de una fuente que lo
> conozca declarativamente (morfo) o de runtime (provider de la colección).
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
### 8.3. Interrupción y reversa (máquina de estados) — IMPLEMENTADO (M4 + M6 handoff JS)
Un usuario abre y cierra un Dialog rápido. El sistema debe transicionar de un `enter`
incompleto a un `exit` (o de vuelta) sin saltos ni huérfanos.
**Lo que M4 entrega — corrección estructural:**
- **Token de generación de grupo.** `PresenceGroup` gana un `generation` — el análogo a nivel
de árbol del `runId` de `Presence`. Cada `requestEnter`/`requestExit`/`cancel` lo incrementa;
el `playEnter`/`playExit` agendado captura su valor y **se aborta en cada `await`** cuando
queda obsoleto. El punto crítico: un `playExit` superado por una reapertura **NO ejecuta su
bucle de unmount** → la superficie que se reabre no se desmonta-y-remonta (sin flash). Es el
bug central que el árbol tenía sin guarda (el unmount incondicional de §8.1).
- **Propagación de cancelación.** En un flip (un `requestExit` con un enter en vuelo, o
viceversa) el grupo llama `member.cancel()` a cada superficie, que propaga
`motion.cancel(node)` por-nodo de `arts/motion` — así un `spring`/WAAPI en vuelo no queda
**apilado** con el de la fase inversa (`run()` del motor es aditivo: no cancela el previo).
`Presence.cancel()` ya hacía `cleanup()` (bump del `runId` → la cola async queda inerte);
M4 le añade el `motion.cancel(node)`.
- **`finished` resuelve en cancel.** `toHandle` (motor) ahora **traga el rechazo** de
`Animation.cancel()` (WAAPI rechaza su `finished` con `AbortError`): sin esto, la reversa
filtraría un _unhandled rejection_ por `track`/`pending`. Unifica todos los drivers a la
semántica "stop-in-place, resolve" del `spring`.
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
**Reversa fluida — el estado por sustrato:**
- **CSS transiciones (los coordinados `cascade-*`):** fluida **sin esfuerzo**. Una `transition`
interpola desde el valor computado actual al invertir el target → el prototipo
`/temas/animations/presence-group` y los presets coordinados (que usan `transition`, NO
`@keyframes`) revierten desde la posición actual gratis (el "caso (1)" de abajo, implícito). Es el
sustrato del servicio de coordinación → **su reversa ya es fluida**.
- **JS (`spring`) — HECHO (M6):** el motor ahora **expone el estado del driver**
(`MotionHandle.peek()` → posición + velocidad por propiedad, sólo física) y **lo reinyecta** en la
fase inversa vía `MotionContext.handoff`: el `spring` arranca cada propiedad desde el
`handoff[prop]` (x, v) en vez del `from` declarado, así la reversa **continúa desde la
posición/velocidad actual** sin saltar ni reiniciar el overshoot. El _handoff_ vive en el engine
(`cancel` captura el `peek`; `run` lo consume vía `takeHandoff`, que **además** cancela un run en
vuelo si el caller no pre-canceló — cubre el camino _island_ de un overlay-spring y elimina el
apilamiento). `waapi`/`rect` no exponen velocidad → reinicio limpio (sin `peek`). Tests en
`engine-motion.test.ts`.
- **CSS `@keyframes` — pendiente (decisión por preset):** un `@keyframes` de salida **salta** a su
`from` al invertir. "Sin saltos" exige una de tres políticas: (1) keyframes interrumpibles
(`from` ≈ reposo); (2) leer el computed value y reinyectarlo como `from` vía WAAPI; (3) aceptar el
salto donde sea imperceptible (fades). Aplica a los state-presets que usan `@keyframes`
(`scale-fade`…), NO a los coordinados (transiciones). Sin consumidor que lo fuerce hoy.
Rigor sostenido: M4 garantiza la **corrección** (sin unmount espurio, sin animaciones apiladas, sin
rejection filtrado); M6 añade la **continuidad fina del path JS** (handoff con velocidad). El único
cabo es la política de `@keyframes`, acotada a los state-presets y diferida hasta que un preset la
necesite.
---
## 9. Motor: agregación por el `PresenceGroup`, no `subtree:true` ciego
Tentación natural: darle al motor `getAnimations({ subtree: true })` para esperar el árbol.
**Se rechaza**, por dos razones:
1. **Rompe la agnosticidad del motor.** `subtree: true` ES jerarquía; el motor dejaría de ser
por-nodo y agnóstico de componentes — justo lo que lo mantiene un _art_ puro reusable.
2. **Captura animaciones ajenas.** `subtree: true` devuelve **todas** las animaciones de los
descendientes: un hover de un hijo, una transición incidental, **un `PresenceGroup` anidado
independiente**. Agregar su `finished` sin filtrar haría esperar a animaciones que no son de
esta coreografía, o acoplarse a un grupo hijo que debería ser autónomo.
**La solución: la espera del árbol la hace el `PresenceGroup` (soma), no el motor.** El grupo
agrega los `finished` de las superficies / grupos **registrados en él** — su lista de miembros,
no un barrido DOM. El **scoping sale gratis de la topología de registro** (§7.1): un grupo
anidado se registra en el padre como **caja negra** (su `play()`/`finished` representa todo su
subárbol) o decide ser independiente y no registrarse; el padre **no desciende** al DOM. Cada
superficie-hoja resuelve su propia espera con lo que `Presence` ya usa: `node.getAnimations()`
(de **su** nodo, no subtree) + `motion.pending(node)`.
**Resultado:** `arts/motion` queda **agnóstico de verdad** — `engine-motion.ts` y `drivers.ts`
no se reescriben; el motor sigue ejecutando primitivas por-nodo. La inteligencia de "quién y
cuándo" es 100% de soma.
---
## 10. Reduced-motion · firma semántica · SSR/hydration
- **Reduced-motion.** Ya existe `ReducePolicy` (`instant`/`opacity-only`/`none`) por preset y
la proyección `data-motion='reduce'` (+ `@media (prefers-reduced-motion: reduce)`). La
coordinación lo respeta: bajo reduced-motion el grupo puede **colapsar la cascada a
instantánea** (todas las superficies resuelven su `finished` de inmediato) — la presencia se
coordina igual, sin el realce temporal. La firma cross-modal (sound/haptic) sigue
comunicando (ventaja del libro: ningún canal agota la semántica).
- **Firma semántica (la composición que nadie más tiene).** El momento-evento (`data-event-*`,
la firma de sema) y el momento-estado (la transición coordinada) **componen**: un
`commit + fulfill` puede coreografiar su _settle_ con la entrada de los hijos. El
`PresenceGroup` ordena el lifecycle; la firma la sigue disparando sema sobre el mismo evento.
El servicio **no toca** el engine de sema ni los `data-event-*` — solo se apoya en que ya
están ahí.
- **SSR / hydration.** La coordinación **degrada sin JS**: el estado base de toda superficie es
**presente/visible**. Si el `PresenceGroup` no corre (SSR, hydration pendiente, JS
deshabilitado), el contenido **nunca se bloquea ni se oculta** — la coreografía es **realce
aditivo**, jamás un prerrequisito para ver el contenido. El `cancel(el)` por-nodo se propaga
a `group.cancel()` sin dejar superficies en estado intermedio si el árbol se desmonta a
media animación.
---
## 11. Integración de los efectos WIP
La carpeta `web/routes/demos/animations/` (40 efectos, sin trackear) **no es una sola cosa**:
- **text-effects** (blur, count, scrambled, gradient, focus, circular) → **candidatos a
_variants de contenido_** de eidos. Son animación de contenido al entrar (WAAPI + stagger
por letra/palabra), encajan como presets aplicables a una superficie de texto. Entran en el
catálogo de variants (§5), no como demos sueltas.
- **fondos WebGL** (aurora, galaxy, particles, liquid-image, …) → **no-goal** (§2). Se
apartan a su propio eje ("ambient"); **no se mezclan** con el servicio de presencia.
La integración de los text-effects es una fase tardía del roadmap (§15): primero el núcleo de
coordinación, luego ampliar el catálogo.
---
## 12. Encaje con las 4 capas + qué NO hace
| Capa | Aporta al servicio | Línea que NO cruza |
| --------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
| **morfo** | declara la **estructura** (`animation: { surface, children: { enter, exit } }`) | no lleva valores visuales/expresivos |
| **soma** | coordina la **presencia** del árbol (`PresenceGroup`, `Presence` generalizado) | **nunca** decide el cómo-se-ve ni calcula un ms |
| **eidos** | el **QUÉ visual** + valores expresivos (keyframes, easing, stagger-ms, order) | no controla mount/unmount (lo lee, no lo manda) |
| **arts/motion** | ejecuta primitivas **por-nodo** (`run`/`pending`/`cancel`) | no orquesta, no gana jerarquía |
**Regla 2-de-3:** el campo morfo `animation` lo consumen soma (lifecycle) + eidos (visual) →
justificado en el contrato.
**Qué NO hace el servicio:** no reinventa GSAP (sin timelines imperativas de propósito
general); no mete visualidad en soma (el JS solo coordina presencia); no mete comportamiento en
eidos (eidos lee el DOM, no controla el mount). La palabra "orquestar" se reparte por la línea
state/visual: la coordinación-de-presencia es de soma; la coordinación-visual-expresiva
(stagger CSS) es de eidos.
---
## 13. Validación — prototipo exit-heavy
El prototipo **no** valida el caso fácil (enter con stagger). Valida lo difícil:
- **Primario — una lista que hace stagger de SALIDA antes de que el contenedor se vaya**
(`exit: 'after'`, **colección**). Ejercita de golpe: retención del DOM (§8.1), cierre
de registración **por el provider de la colección** (§8.2, el caso runtime-count), agregación
de `finished` (§9), y **interrupción** (abrir/cerrar a media animación → §8.3). El escalonado
es CSS (eidos); soma coordina la presencia.
- **Secundario — Dialog `overlay→content` secuenciado** (`when`, **cardinalidad fija**):
valida el camino del count-desde-morfo y la coordinación de dos superficies nombradas.
**Hogar del prototipo:** la ruta `/temas/animations` (la demo del sistema de motion de UIX, la
misma que cita `eidos-motion.md`) — **no** `web/routes/demos/animations/` (los efectos WebGL,
no-goal §2).
Criterio de éxito del prototipo: la salida coordinada se completa sin desmontar el padre antes
de tiempo; una interrupción no deja superficies montadas "colgadas"; bajo reduced-motion el
contenido aparece/desaparece sin bloquearse.
**Verificación ejecutable** (scripts reales del repo): `npm run check` (tipos, 0 errores) ·
`npm run morfo:check` (DOM real vs contrato morfo — valida que `MorfoPart.animation` se
emite/consume bien) · `npm run perm:check` (re-valida el morfo a través de transiciones de
estado — clave para enter↔exit↔interrupción) · `npm run smoke` (runtime/hydration, con
`npm run dev` levantado) · `npm run test` (vitest).
---
## 14. Impacto sobre el código actual + migración
**Todo es aditivo y opt-in.** Un componente sin la nueva declaración funciona **exactamente
como hoy**. No hay big-bang sobre los ~25 componentes existentes.
| Capa | Cambio | Backward-compat |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| **arts/motion** | **casi intacto** — la coordinación vive en soma (§9); único cambio (M4): `toHandle` resuelve `finished` en cancel (no rechaza) para que la reversa no filtre un _unhandled rejection_; `drivers.ts` sin tocar | ✅ |
| **morfo** | `types.ts`: nuevo campo **opcional** `MorfoPart.animation` (solo estructura); `schema.ts`/`compile.ts` lo validan/compilan; los morfos lo declaran **incrementalmente** | ✅ (opcional ⇒ morfos actuales válidos) |
| **soma (epicentro)** | `presence.svelte.ts` **modificado** (descubre `group?`, cede timing, `cancel`→`motion.cancel`, **degrada sin grupo**); `presence-group.ts` **nuevo** (coordinación + token de generación M4); providers tocados incrementalmente | ✅ (sin grupo = comportamiento actual) |
| **eidos** | **gana los valores expresivos del stagger** (tokens/recipe) + keyframes para reversa interrumpible (§8.3); naming `animation`↔`motion` (§5) decide cuánto tocan los wrappers (estratificar = mínimo) | ◐ depende de §5 |
| **sema** | **no se toca** — la firma se compone, su engine y los `data-event-*` no cambian | ✅ |
| **langs/format/prefs/adom** | **no se tocan** | ✅ |
**Pilotos de migración:** Dialog (`overlay`+`content`, cardinalidad fija) + una lista/menú con
items (colección, stagger de salida = el prototipo §13). Los dos casos canónicos.
---
## 15. Roadmap de implementación por fases
> Cada fase es independiente, verificable (`npm run check` · `npm run test` · `npm run morfo:check`
> · `npm run perm:check` · `npm run smoke` + el prototipo en `/temas/animations`) y no rompe lo
> anterior. Aprobado el RFC, cada fase se planifica por separado.
- ✅ **M1 — Contrato (hecho).** `MorfoPart.animation` en `morfo/types.ts` + `schema.ts` +
`compile.ts`. El campo `children` se separó en `{ enter?, exit? }` (no un `when` simétrico) al
materializar el prototipo — un contenedor que bracketea a sus hijos no es simétrico (§4).
- ✅ **M2 — `PresenceGroup` + `Presence.group` (hecho).** Clase nueva en `soma/layers/presence-group.ts`
(sin runes, unit-testable) + `Presence` cede timing por context y degrada sin grupo.
- ◐ **M3 — Exit coordinado (hecho) + cierre de registración (diferido).** §8.1 (retención DOM)
implementado; §8.2 (count fija/colección + timeout anti-deadlock) **sigue diferido** al primer
consumidor que necesite una barrera de cardinalidad fija.
- ✅ **M4 — Interrupción / reversa + cancel de grupo (hecho).** §8.3: token de generación
(supersede la fase obsoleta → sin unmount espurio), propagación de `motion.cancel(node)` en el
flip, y `toHandle` resuelve `finished` en cancel (sin _unhandled rejection_). La reversa fluida
desde la posición actual (JS y `@keyframes`) queda para **M6**. _Verificación:_ tests de
`presence-group`/`presence`/`engine-motion` + prototipo `/temas/animations/presence-group`.
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
- ✅ **M5 — DX `animation` enrutado + naming (hecho).** Prop transversal `animation` →
enrutado a las superficies `surface: true` del morfo compilado (`routeAnimation`), emitido como
`data-animation-style` por los wrappers de superficie. **Generalizado**: el wiring (PresenceGroup +
routing + auto-stagger) se extrajo al helper `soma/layers/coordination.ts` (`Coordination` +
`CoordinatedSurface`), validado por DOS consumidores — `Reveal` (raíz virtual + Panel owner,
bracket) y `Rail` (raíz=owner, `together`, sin eventos). Un componente coordinado nuevo es ahora un
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
puñado de líneas. **Naming resuelto (§5): `animation` y `motion` SEPARADAS por rol** — no se
unifican (tras M6 son sistemas distintos); `animation` gana type-safety vía el registry
augmentable `MotionCoordinatedPresets` en `$motion` (eidos lo puebla con `cascade-*`), espejo de
`EidosMotionPresets`. El gating data-state↔data-starting-style se resolvió en M6.
- ◐ **M6 — Stagger expresivo + presets coordinados (hechos) + reversa interrumpible (pendiente).**
(1) Stagger: tokens canónicos `--motion-stagger-index` (por ítem, **auto-derivado del orden de
registro del grupo** vía `PresenceGroup.childIndex`) × `--motion-stagger-each` (ritmo, heredado).
(2) **Presets coordinados**: `MotionConfig.coordinated` + `CoordinatedPreset` (en `$motion`); eidos
genera (render-css.ts) la transición + off-state + stagger reversible sobre `data-starting/ending-style`;
built-ins `cascade-slide`/`-fade`/`-scale` (presets/css.ts), en `generated/base.css`. Un coordinado
hace `animation="cascade-slide"` sin CSS de animación propio — cierra el hallazgo §5. Generation-test
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
en `eidos/motion.test.ts`. La demo `/temas/animations/compuesto` consume ya los presets de la librería
en un compuesto real (un `Rail` de `<Button>`s; el selector cambia `animation` en vivo) — ver
**Apéndice B.4**; `reveal`/`rail` conservan hand-CSS a propósito como ejemplo _custom_. **Reversa fluida
(§8.3): los coordinados (transiciones CSS) ya revierten desde la posición actual; el handoff JS con
velocidad (`spring`) está HECHO — `MotionHandle.peek()` + `MotionContext.handoff`, orquestado en el
engine (`takeHandoff`). Único cabo: la política de `@keyframes` interrumpibles para los state-presets
(acotada, diferida).**
- **M7 — Reduced-motion + SSR/hydration hardening** (§10).
- **M8 — text-effects como variants de contenido** (§11).
- **M9 — Segunda coordinación: el modo children-DOM** — HECHO (F1/F1b/F1c/F2, commit `d797a0ce`).
El `DomCascade` propaga el lifecycle del owner sobre ítems DOM descubiertos por selector;
cableado en el `dropdown-menu` real (cascade de filas, exit-heavy con `pending`, + 4 fixes de
convivencia). Detalle completo en el **Apéndice C**. Pendiente F3: submenús (`sub-content` como
segundo owner). El otro piloto (Dialog) usa el modo registered-`Presence` y queda para después.
> **Validación end-to-end — primer consumidor real (`Reveal`).** Antes de M5/M6 se cableó un
> componente REAL como primer consumidor del contrato: `Reveal` (disclosure list — morfo
> `src/uix/morfo/components/reveal.ts`, soma `src/uix/soma/components/reveal/`). Su Panel declara
> `animation: { surface: true, children: { enter: 'before', exit: 'after' } }`; el provider **lee
> ese `children` del morfo COMPILADO** (`compileMorfo(...).parts.byKebab.get('panel').animation`)
> para construir el `PresenceGroup` — el contrato es lo que dirige la coordinación, no hand-wiring.
> Verificado a velocidad real en `/temas/animations/reveal`: enter en cascada (panel, luego ítems)
> y exit-heavy (ítems salen primero, panel RETIENE su DOM ~550 ms, luego el panel, luego unmount).
> Cierra el hueco que el motor M1–M4 tenía: **dejaba de ser código sin usar**. Se eligió un
> componente nuevo y aislado (no retrofit de Dialog/dropdown-menu) para validar el stack sin riesgo
> de regresión en un componente con focus-trap / roving-focus / items por DOM-vivo.
---
## Apéndice A — decisiones resueltas vs abiertas
**Resueltas (con el usuario):**
- **Reparto state/visual** — morfo=estructura · soma=presencia/lifecycle · eidos=QUÉ visual +
valores expresivos · arts/motion=ejecución por-nodo. (§0, §4, §7, §12)
- **morfo declara / soma coordina** (no "soma orquesta animaciones": eso sonaba a invasión
visual; el lifecycle de presencia ya es soma por doctrina — `Presence`). (§7)
- **Routing automático + choreography opt-in-por-declaración.** (§6)
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
- **Naming: `animation` y `motion` separadas por rol** (M5) — NO se unifican; tras M6 son sistemas distintos (`--state`/eidos vs coordinado/soma), cada una con su registry type-safe. El nombre de la prop comunica el sistema. (§5)
- **Layout animations = no-goal**; `rect` mide un solo nodo. (§2)
- **Scoping = topología de registro**, no `subtree:true` ciego. (§9)
- **Cierre de registración** — fija=morfo / colección=provider. (§8.2)
- **Reversa interrumpible** — M4 entrega la corrección estructural (sin unmount espurio ni motion apilado); la fluidez _desde la posición actual_ (JS y `@keyframes`) es M6. (§8.3)
- **Árbol lógico, no DOM**; no-animables transparentes; set dinámico de registros. (§7.1)
**Abiertas (a fijar durante la implementación):**
- **Modelo de coordinación** — recomendación: híbrido declarativo (§7); confirmar contra los
pilotos en M2–M3.
- **Shape exacto** del plan de coordinación que emite `compile.ts` y de `PresenceMember`.
- **Política de late-joiners** (un hijo que monta tras cerrarse la ventana de registro: ¿se une
a la coreografía en curso o arranca autónomo?).
- **Política de keyframes interrumpibles** (cuál de las tres vías de §8.3 por preset).
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
---
## Apéndice B — Convivencia con la firma sema (los tres sistemas visuales)
> **Guía de composición.** Consolida cómo el motion coordinado de este servicio **convive** con
> los otros dos sistemas visuales de eidos. Es material de _cómo se componen_ (con diagrama y un
> ejemplo con tokens); el _por qué_ está en §5 (el hallazgo) y §10 (la firma cross-modal). Para el
> modelo base de **dos** momentos (`--event` / `--state`) ver
> [`eidos-motion.md`](./eidos-motion.md); este apéndice añade el **tercer eje** —el coordinado— y
> la regla que evita que pelee con la firma.
### B.1 — Los tres sistemas visuales
eidos reacciona en CSS a **tres** familias de atributos distintas, sobre el **mismo nodo**. Las
tres las emite `lib/render-css.ts` con funciones hermanas:
| Sistema | Selector | Lo escribe | Generador / mapa | Qué expresa |
| -------------------------------- | --------------------------------------------------- | -------------------------------- | -------------------------------------- | ------------------------------------------------- |
| **Firma** (momento `--event`) | `[data-event-family][data-event-phase='active']` | sema (durante el hold) | `renderSignatureRules` (`signatures`) | _qué SIGNIFICA_ el acto (commit/threat/contact…) |
| **State-preset** (`--state`) | `[data-animation-style][data-state]` | soma (effects) | `renderCssPresetRules` (`presets`) | la transición a/desde un estado persistente |
| **Coordinado** (este servicio) | `[data-animation-style][data-starting/ending-style]`| soma (`PresenceGroup`/`Presence`)| `renderCoordinatedPresetRules` (`coordinated`) | _cómo APARECE/SALE_ una superficie en el árbol |
Son **ejes ortogonales de atributo**: sólo entran en conflicto cuando dos animan la **misma
propiedad** (`transform`/`opacity`) del **mismo nodo a la vez**. La firma sema y el motion
coordinado son, además, **dos sistemas de feedback que responden a preguntas distintas** del
mismo evento — _qué significa_ vs _cómo aparece_:
```
EVENTO ( open · close · activate )
│
┌───────────────────────┴───────────────────────┐
SEMA · ¿qué SIGNIFICA? MOTION · ¿cómo APARECE?
familia / intent soma · PresenceGroup
sound · haptic (canales runtime) (lifecycle del árbol)
estampa data-event-* pone data-starting/ending-style
│ │
└───────────────────────┬───────────────────────┘
▼
EIDOS · capas visuales (CSS, mismo nodo)
┌─────────────────────────┬─────────────────────────┬─────────────────────────┐
│ firma [data-event-*] │ state [data-state] │ coordinado │
│ momento --event │ momento --state │ [data-starting/ │
│ (signatures) │ (presets) │ ending-style] │
│ ✕ MUDA en coordinados │ transición de estado │ (coordinated) │
│ (channels: []) │ │ ✓ gana el eje visual │
└─────────────────────────┴─────────────────────────┴─────────────────────────┘
▼
DOM · una sola animación visual
```
### B.2 — La regla: el coordinado gana el eje visual, la firma genérica se silencia
Si un componente coordinado deja salir además su **firma genérica**, la familia `emerge`
materializa su `present-rise`/`dismiss-fade` — _otra_ transición de `transform`/`opacity` sobre
el **mismo nodo** a la vez que la cascada coordinada. Dos animaciones peleando por la misma
propiedad: el bug que pegó el prototipo ("aparece otra animación con un ligero desplazamiento").
La doctrina: un componente coordinado **silencia su firma genérica** pero **mantiene el evento en
la capa sema**. En `reveal.ts` son tres piezas:
```ts
scope: ['soma', 'sema'], // el evento SIGUE en sema (telemetría / a11y / cascade per-app)
expression: 'none', // declara: no materializo firma perceptual por defecto
events: [{ name: 'open', semantic: { family: 'emerge', /* … */ channels: [] } }]
```
`channels: []` corta en seco en el engine (`sema/engine.ts` — el early-return antes de proyectar):
sin `data-event-*` estampado → la capa de firma queda muda → sólo corre el coordinado. La
percepción del componente **es** la cascada, no una firma genérica encima.
**No es exclusión, es composición selectiva.** Tres modos, todos válidos:
| Modo | Ejemplo | Feedback |
| -------------------------- | ------------------------------------- | ------------------------------------------------------------ |
| **(a) sema sola** | Button, Checkbox | la firma (`data-event-*`); sin coordinado |
feat(motion): modo (c) — composición sema + motion (opt-out visual en eidos) Un componente coordinado puede componer la cascada (motion) con sonido/háptico (sema) en el mismo evento. El cabo: el meta-canal visual de sema estampa data-event-* siempre que el evento tenga algún canal (channels != []), así que channels:['sound'] arrastraba la firma visual genérica (present-rise), que pelea con la cascada. Solución EN MOTION (sema intacto): eidos neutraliza su PROPIA firma visual sobre las superficies coordinadas. render-css.ts > renderCoordinatedPresetRules emite `[data-animation-style='cascade-X'][data-event-phase='active'] { animation: none !important }`. La firma usa `animation` -> muere; la cascada es `transition` -> sobrevive. Aditivo: inerte hasta que un coordinado dispare un evento sema. Ejemplo: Reveal pasa a modo (c) — open y close declaran channels:['sound'] (emerge suena, pitch 600) + expression:'family-default'; suena al abrir y al cerrar a la vez que la cascada, sin pelea visual. Rail queda como modo (b) puro. Layout de /temas/animations con events:{ sound: true }; la demo /reveal explica el modo (c). Honestidad: la versión inicial del RFC Apéndice B afirmaba que el modo (c) componía "gratis" — falso (verificado en engine.ts/visual.ts). Corregido: el Apéndice B documenta ahora el acoplamiento real y el opt-out en motion. Tests: eidos/motion 25/25 (neutralización) · morfo 70/70 · morfo:vocabulary limpio. Audio verificado en navegador (suena al abrir y cerrar). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
| **(b) coordinado solo** | Rail | la cascada; firma genérica silenciada (`channels: []`) |
| **(c) ambos compuestos** | Reveal (`open` con `channels:['sound']`) | la cascada (visual) + sonido `emerge` — el opt-out visual lo da eidos (ver nota) |
El conflicto está en el **eje visual**, y silenciar `channels: []` lo elimina apagando TODO. El
modo (c) —componer la cascada con sound/haptic— tiene un matiz **verificado en el engine**: el
meta-canal visual estampa `data-event-*` **siempre que el evento tenga algún canal** (`channels ≠
[]`), sin mirar _cuál_. Así que un coordinado con `channels: ['sound']` obtiene el sonido **y** la
firma visual genérica (`present-rise`) — que pelearía con la cascada. **La solución vive en MOTION,
no en sema** (sema queda intacto): eidos emite, junto a cada preset coordinado, una regla que
**neutraliza su propia firma visual** sobre la superficie —
`[data-animation-style='cascade-X'][data-event-phase='active'] { animation: none !important }`
(`render-css.ts` → `renderCoordinatedPresetRules`). La firma usa `animation` → muere; la cascada es
`transition` → sobrevive. Es **aditivo** (inerte hasta que un coordinado dispare un evento sema) y
coherente con la doctrina: **eidos es el dueño del eje visual**, así que el opt-out de SU firma vive
en eidos. Resultado: un coordinado declara `channels: ['sound']` y compone sonido + cascada, **sin
tocar el engine de sema** (§10 sigue valiendo). Test en `eidos/motion.test.ts`.
feat(motion): M5 naming type-safe + reversa fluida JS + demo de presets + docs de convivencia Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado). - M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6 son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos; `animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana type-safety vía el registry augmentable `MotionCoordinatedPresets` + `CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale` por declaration-merging — espejo de `EidosMotionPresets`. - Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()` (posición + velocidad por propiedad) y la reinyecta en la fase inversa vía `MotionContext.handoff`: el `spring` continúa desde la posición/velocidad actual en vez de reiniciar desde el `from`. Orquestado enteramente en el engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un run en vuelo cuando el caller no pre-cancela): cubre el camino island (overlay-spring) y elimina un apilamiento preexistente. La reversa de los coordinados (transiciones CSS) ya era fluida. - Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la página); el selector cambia `animation` en vivo y cada Button compone su press (firma sema) con la cascada (motion coordinado). - Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma `--event` / state-preset `--state` / coordinado, con diagrama + ejemplo Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y cross-link del tercer eje desde eidos-motion.md. Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad). svelte-check: 0 errores en mis archivos. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
### B.3 — Ejemplo con tokens: `Button` (modo a) y su composición (modo c)
`Button` es el caso limpio del **modo (a)**: su morfo declara `expression: 'family-default'` y un
único evento `contact-activate` (`family: 'contact'`, **sin intent**) — su feedback es la **firma
de la familia `contact`**, no motion coordinado. Los "tokens" viven en **dos planos** que no se
pisan:
| Plano | Chrome / estado (locales) | Firma perceptual (global) |
| ------------------------------ | ---------------------------------------------------------- | ----------------------------------------------------- |
| **Dónde** | `button.css` + recipe `--button-*` | `BUILTIN_SIGNATURES` → `generated/base.css` |
| **Tokens** | `--button-palette-solid`, `--button-height-md`, `--button-transition-*` | `press-squeeze` + `--duration-fast` + `--ease-default` |
| **Reacciona a** | `data-variant` / `data-size` / `data-color` / `:hover` / `:active` | `data-event-family='contact'` (el hold de sema) |
| **Alcance** | **per-instancia** (cada button elige variant/size/color) | **canon global** (un press se siente igual en todo el sistema) |
El recorrido del press (el plano de la firma):
```
click → soma: runtime.trigger('contact-activate') (button-provider.svelte.ts)
→ engine sema estampa data-event-family="contact" data-event-phase="active" durante el hold
→ la firma 'press' matchea → animation: press-squeeze var(--duration-fast) var(--ease-default)
(press-squeeze: scale + shadow se aplana + esquina se cuadra)
→ + sound · haptic (canales runtime de contact, si on) → fin del hold → reposo
```
Reparto honesto: **el chrome lo tokeniza el componente; la firma es canon** (el libro pide que un
`contact` se sienta igual en todo el sistema, así que NO hay un `--button-press-duration` por
instancia — la firma vive en `--duration-fast` global). El tinte de intención (`data-color`) es un
eje aparte que NO carga el press: el press es genérico `contact` sin intent (libro cap. 22 §11).
**Modo (c) — el mismo Button componiendo con la cascada.** Metido en un `Rail` que aparece
coordinado, tiene **los dos ejes a la vez, con sus propios tokens, sin pelearse**:
```svelte
<Rail.Provider animation="cascade-slide">
<Rail.Item><Button variant="solid">Guardar</Button></Rail.Item>
</Rail.Provider>
```
- **entrada del item** → `cascade-slide`, eje `[data-starting-style]`, tokens `--motion-cascade-*` _(coordinado)_
- **press del button** → `press-squeeze`, eje `[data-event-*]`, tokens `--duration-fast`/`--ease-default` _(firma)_
Son **dos nodos** (el `Rail.Item` que se anima vs el `<Button>` dentro) y **dos momentos**
(aparecer ≠ pulsar): por eso componen sin conflicto.
### B.4 — Demos de referencia
- [`/temas/animations/compuesto`](../../../web/routes/temas/animations/compuesto/+page.svelte) — la
cara **"librería"**: un `Rail` de `<Button>`s que usa los presets coordinados **predefinidos** de
eidos (`cascade-slide/-fade/-scale`) con **cero CSS de animación** en la página; el selector
cambia `animation` en vivo, y cada Button compone su press (modo c).
- [`/temas/animations/reveal`](../../../web/routes/temas/animations/reveal/+page.svelte) ·
[`/rail`](../../../web/routes/temas/animations/rail/+page.svelte) — la cara **"custom"**: hand-CSS
para presets propios (`cascade-drop`/`-blur`), demuestra cómo extender más allá de la librería.
- [`/temas/animations/presence-group`](../../../web/routes/temas/animations/presence-group/+page.svelte)
— el mecanismo sintético (`Presence` + `PresenceGroup` construidos a mano).
---
## Apéndice C — M9: el modo children-DOM (la segunda coordinación)
> **Estado: IMPLEMENTADO y verificado en navegador real (commit `d797a0ce`).** F1 (esqueleto +
> contrato + `DomCascade`), F1b (demo aislado), F1c (exit-heavy con `pending`), F2 (el
> `dropdown-menu` real). Cubre el segundo de los dos modos de coordinación del servicio. F3
> (submenús) queda fuera.
### C.1 — Por qué un segundo modo
El `PresenceGroup` (§7) coordina **superficies-`Presence` registradas**: cada hijo animable es un
`Presence` que se registra en el grupo por context (Reveal/Rail). Pero un **menú o lista real** no
tiene esa forma: tiene **UNA** superficie `Presence` (el _content_/overlay que monta como unidad) y
sus **ítems son DOM plano** que el runtime no envuelve — se descubren por selector (`getItems`/
`querySelectorAll`), markup arbitrario del consumidor. No hay nada que registrar en un grupo, ni un
sitio de binding Svelte donde poner `--motion-stagger-index`.
**3 grietas que el menú destapó** (una por capa):
1. **registro** — no hay un `Presence` por ítem que registrar en un grupo.
2. **index per-ítem** — el preset de eidos hace `calc(var(--motion-stagger-index) * …)`; en
Reveal/Rail lo pone un binding Svelte (`style:--motion-stagger-index`), inaplicable a ítems DOM
arbitrarios del consumidor.
3. **escritura de CSS-var por elemento** — `ActiveDom` no tenía un método para escribir una
custom property en UN elemento (`apply` = attrs/data-\*; `writeStyle` = `<style>` global; la
doctrina prohíbe `el.style` directo). Se añadió `writeProperty`/`removeProperty` a `$adom`
(el usuario aprobó extender adom para esto — sema NO se toca, adom SÍ).
### C.2 — El diseño: el owner propaga su lifecycle a los ítems DOM
En vez de registrar ítems, el **owner `Presence` PROPAGA su lifecycle** sobre cada ítem DOM,
**reutilizando el preset coordinado de eidos SIN cambios**. Mientras el owner está
`data-starting/ending-style`, el propagador escribe en cada ítem los mismos attrs que un child-
`Presence` habría producido — `data-animation-style` (el preset ruteado) + el `data-starting/
ending-style` ESPEJADO del owner + `--motion-stagger-index`/`-count` por orden DOM — de modo que
`[data-animation-style='cascade-X'][data-starting-style]` matchea y la cascada corre. El owner
`Presence` ya posee el TIMING (starting → next-frame → transición); el propagador solo espeja.
**`DomCascade`** (`src/uix/soma/layers/dom-cascade.svelte.ts`) es ese propagador:
```ts
new DomCascade({
dom, // ActiveDom
animationStyle, // Active<string|undefined> — el preset ruteado; undefined ⇒ inerte
ownerTransitionAttrs, // Active<…> = Presence.transitionAttrs del owner
items // () => readonly HTMLElement[] — los nodos a cascadear, en orden DOM
})
```
- `sync/clear` son PUROS sobre `opts.dom` (testeables sin componente, server-test con un mock).
- `watch()` corre el `$effect` reactivo (de ahí el `.svelte.ts`): 3 ramas por prioridad —
`items===0` (`clear`) · `ending` (espeja el exit) · `appeared && style` (`beginEnter`). El
`$effect` de lógica **NO tiene cleanup** (un `clear()` por re-run cortaba el enter a media
transición); el teardown vive en un 2º `$effect` sin-deps.
- **`beginEnter`** conduce el enter de filas recién montadas: estas montan en el ON-state, así que
estampar el off-state con la transición ACTIVA haría que el opacity **interpole** hacia 0 (y,
soltado un frame después, apenas se mueva). En su lugar estampa el off-state con `transition:
none` (SALTO instantáneo) → fuerza un reflow → restaura la transición y suelta
`data-starting-style` → las filas hacen _ease in_ con el stagger. (El `Presence` no sufre esto:
su `data-starting-style` está en el render de montaje, que nunca transiciona.)
- **`pending()`** (F1c) — el cabo del §9: el owner `getAnimations()` NO ve las transiciones de los
ítems (son descendientes DOM, no el nodo owner; el motor sigue sin `{subtree:true}`). `pending()`
agrega los `finished` de los ítems en UNA promesa (diferida 2 frames para que las transiciones de
salida ya hayan registrado), que el owner `Presence` espera vía `PresenceOptions.pending` →
retiene el subárbol hasta que la cascada de salida settlea. Usa
`Promise.all(finishers.map(f => f.then(noop, noop)))` (settle, no `Promise.all` crudo) para que
una transición cancelada aislada no colapse la espera.
**El contrato (morfo).** La parte owner declara
`animation: { surface: true, staggerChildren: '<item-kebab>' }` (`MorfoPart.animation.staggerChildren`,
validado en `schema.ts`: requiere `surface:true`, mutuamente excluyente con `children`). Es el
PRIMER consumidor real de `staggerChildren`. El valor nombra la item-part líder; **el provider
resuelve los nodos DOM** (puede ampliar a todas las filas visibles — ver C.3).
### C.3 — F2: el `dropdown-menu` real + los fixes de convivencia
El owner es el `content` (ya era un `Presence` isla); los ítems son sus filas, descubiertas por un
`getCascadeRows` (TODAS las filas visibles — item/checkbox/radio/sub-trigger + separators +
group-headings, incl. disabled, scoped al content excluyendo submenús). Prop pública opt-in
`animation?: CoordinatedPresetName` ruteada al `DomCascade`; `undefined` ⇒ el menú se comporta como
siempre. El `pending` se cablea reenviándolo por `createFloatingShellRoot` (los otros 6 floating
consumers quedan byte-idénticos). **El menú destapó CUATRO colisiones**, cada una con su fix —
todas son del componente, no del motor, y aditivas:
1. **Colisión de `transition`.** Las filas declaran `transition: background, color` (hover); el
preset coordinado anima con `transition: opacity, transform` — MISMA propiedad shorthand, y el
CSS de componente carga DESPUÉS del preset generado → la regla de la fila gana y BORRA la
cascada. Fix: la fila base usa **longhands** (`transition-property/duration/timing`, sin
`transition-delay` — el shorthand lo reseteaba a 0s y mataba el stagger) + una regla
`[data-…-item][data-animation-style]` (specificity 0,2,0) que cede `transition` al preset.
2. **Opacidad de disabled.** Un `[data-disabled]` tiene `opacity` dim al mismo specificity que el
off-state → ganaba y fijaba la fila, que no podía desvanecerse. Fix: gatear el opacity dim con
`:not([data-starting-style]):not([data-ending-style])` para que el off-state (0) se vea durante
la cascada.
3. **La firma `dismiss-fade` del PANEL.** El content tiene su propia `CSSAnimation` `dismiss-fade`
(firma sema del evento `close`) que desvanece TODO el panel en ~240ms → con él se van las filas
que cascadeaban dentro («cierra de una»). La neutralización de firma del preset
(`[data-animation-style][data-event-phase='active'] { animation: none !important }`) SOLO cubre
las filas (tienen `data-animation-style`), NO el panel. Fix: el provider estampa `data-cascade`
en el content (vía `$effect` + `dom.apply`) sii hay `animation` ruteado, y eidos neutraliza
`[data-dropdown-menu-content][data-cascade] { animation: none !important }` +
`[…][data-cascade][data-state='closed'][data-ending-style] { opacity: 1 }` (evita el colapso
instantáneo del `[data-state='closed']{opacity:0}` durante el exit). El panel se queda quieto y
desmonta solo cuando `pending` settlea.
4. **El toggle no cerraba** (preexistente). El `Dismissal` cierra en `pointerdown` + el `onclick`
del trigger reabre. Fix: el provider excluye el trigger del dismissal
(`runtime.partRef('trigger')` + `contains` en `onInteractOutside`).
### C.4 — Doctrina / lecciones (children-DOM)
- **El espejado pasivo del `transitionStatus` del owner solo captura el enter si los hijos existen
durante la fase `starting`** (efímera, 1 rAF). Hijos descubiertos por DOM/selector montan tarde
→ el cascade debe CONDUCIR su propio enter (`beginEnter`), no espejar.
- **Un elemento que monta en on-state no puede recibir el off-state con la transición activa** —
interpola en vez de saltar. Hay que SUPRIMIR la transición (`transition:none` + reflow) al
estampar el off-state del ENTER, y restaurarla al soltar. El EXIT es lo contrario (quiere
interpolar) → transición activa.
- **La neutralización de firma sema del preset solo alcanza a quien lleva `data-animation-style`.**
El owner (panel) la necesita por separado (un marcador propio: `data-cascade`), o su firma
`dismiss-fade`/keyframe colapsa el panel antes de que las filas terminen.
- **Un `$effect` que escribe DOM con cleanup `clear()` se auto-interrumpe en cada re-run.** Separar
lógica (sin cleanup) de teardown (effect sin-deps).
- **Diagnóstico:** con el preview MCP roto y estados transitorios, un sampler de `opacity`/
`getAnimations()` frame-a-frame en navegador headless real (Playwright) fue lo decisivo; los
observadores/snapshots y los `console.log` capturados ayudaron pero no bastaron.
### C.5 — Demos
- [`/temas/animations/dom-cascade`](../../../web/routes/temas/animations/dom-cascade/+page.svelte) —
el modo children-DOM AISLADO (owner `Presence` + ítems DOM estáticos + `DomCascade.watch()`),
validó F1b (enter) y F1c (exit-heavy con `pending`) antes del menú.
- [`/temas/animations/dropdown-menu`](../../../web/routes/temas/animations/dropdown-menu/+page.svelte)
— el `<DropdownMenu>` REAL con la prop `animation` + selector de preset + ritmo; convive con
focus-trap / dismissal / submenús.
---
## Apéndice D — Re-análisis 2026-06-16: grietas y principios del re-diseño
> **Estado.** Sesión de **análisis y diseño, SIN cambios de código.** Tras una lectura íntegra del libro
> fundacional [`docs/Disenando_lo_que_ocurre_v2_3.md`](../../../docs/Disenando_lo_que_ocurre_v2_3.md), el
> servicio de motion entra en **re-diseño**. Nada de lo de abajo está implementado: es el diagnóstico y los
> principios acordados con el usuario, base del plan a ejecutar. El cuerpo del RFC (§1–§15, Apéndices A–C)
> documenta lo que HOY existe; este apéndice documenta hacia dónde —y por qué— pivota.
### D.1 — Las dos grietas
**Grieta 1 — dos firmas para el canal motion del mismo evento.** El motion coordinado (cascada, vía
`data-starting/ending-style`) y la firma sema visual (vía `data-event-*`) son DOS realizaciones del canal
motion del MISMO evento, y conviven NEUTRALIZÁNDOSE (`[data-animation-style][data-event-phase='active'] {
animation: none !important }`, `lib/render-css.ts:813`). El libro pide UNA firma por evento, coordinada
(su Apéndice B; cap. 20: «los canales se coordinan **alrededor de un evento**, no entre sí en abstracto»).
**F1 (Reveal/Rail, modo c) está comprometida igual que F2 (dropdown)** — la neutralización es sistemática
en F1 (el preset la emite para las superficies con `data-animation-style`) y manual en F2 (`data-cascade`
porque el panel-owner no lleva ese attr). La mecánica de presencia del `PresenceGroup` (registro por
contexto, retención DOM, agregación de `finished`, generación/cancel) es **sólida**; lo comprometido es la
**coexistencia con la firma sema**.
**Origen de la Grieta 1:** la animación se modeló como **eje PARALELO** (`parts[].animation.surface` + prop
`animation` + `data-animation-style` ruteado), desacoplada del evento al que pertenece. De ahí salen las dos
firmas. Si la animación viviera DENTRO del evento, habría una sola.
### D.2 — Principio rector (corrección del usuario): la animación vive en el EVENTO
- **La animación es un canal de la firma del EVENTO. Se declara en el evento que la define**, no en una
prop/eje paralelo (paralelo al Apéndice A del libro para el intent: «la semántica se declara; el runtime
no adivina»).
- **Simple vs compuesto** (distinción que el análisis previo NO hacía):
- _Simple_ (botón, toggle): un evento, su firma. El motion ES la firma.
- _Compuesto_ (menú, lista): el **contenedor** declara el evento (`emerge.open`) y **su firma incluye cómo
se presentan los hijos** (la cascada). Los hijos **participan en el evento del padre**; **no declaran
firma propia para aparecer**.
- **Por evento, no por componente:** la opción del menú **no** tiene firma para _aparecer_ (eso es
`emerge.open` del menú) pero **sí** la tiene para _su_ acto evaluable, `commit.select`. Misma opción, dos
cosas distintas.
- **La prop `animation` cuando hay un evento que debería portar la animación es override-explícito o
violación** (tipos/lint), no la vía primaria.
### D.3 — El discriminante (caso «SVG animado») y los tres dominios
Pregunta única que decide DÓNDE vive una animación:
> **¿La animación realiza un evento perceptivo (comunica un cambio del contexto operativo: algo apareció /
> se fijó / reclama atención / sigue en curso)?**
- **Sí** → firma del evento (morfo). La prop `animation` = override / violación.
- **No** → **animación de contenido / intrínseca** → la prop `animation` es la **vía primaria y legítima**.
No hay evento que la porte.
El **SVG animado** (logo que late, ilustración ambiental, loop decorativo) es el caso paradigmático del
segundo: no es `emerge`/`commit`/`signal`, es presentación. Por tanto **la prop `animation` tiene un dominio
propio legítimo**, y «colapsar los tres sistemas en uno» (lo que decía mi plan inicial) era **incorrecto**.
Hay **tres dominios** que hay que **delimitar** (no colapsar):
| Dominio | Disparador | Dónde se declara | Ejemplo |
| --- | --- | --- | --- |
| **Evento** (firma sema) | `data-event-*` | en el evento (morfo) | aparición del menú = `emerge.open`; spinner = `sustain.progress`; checkmark = `commit.complete` |
| **Estado** | `data-state` | recipe del componente | hamburguesa ↔ X (open/closed) |
| **Contenido / intrínseco** | prop `animation` | prop DX | SVG que late, ilustración animada |
Matiz: un spinner «solo gira» pero **comunica** «proceso vivo» → firma de `sustain`, no contenido. El logo
que late no comunica nada operativo → contenido. Mismo medio (SVG), dominios distintos.
### D.4 — Decisiones de base del re-diseño (pendientes de aprobar/prototipar)
- **A — Lifecycle: Svelte nativo, no custom.** Apoyar presencia/coordinación en `{#if}` +
`transition:…|global` + `introstart`/`introend`/`outrostart`/`outroend` + la retención de DOM nativa, con
**eidos como dueño del visual** vía un shim mínimo. NO se obviaron por incompatibilidad (son compatibles
con Svelte 5); se construyó sobre `Presence` heredado sin re-evaluar el camino nativo. Se come `DomCascade`
entero, media maquinaria de `PresenceGroup` (retención DOM + `pending` + doble-rAF), **elimina el bug
ease-vs-jump** (Svelte aplica el estado inicial antes del paint) y **regala el cleanup-on-abrupt-unmount**
(D.5). Coste a diseñar: el shim state/visual (no meter duración/easing en JS) y la coordinación
motion↔sonido sin neutralizar.
- **B — Una firma por evento, no neutralización** (D.2): la animación-de-evento se declara en el evento; se
retira `animation: none !important` + los `channels:[]`/`expression:'none'` defensivos.
- **Descubrimiento por registro-de-contexto, no DOM-search ni ids.** Lo que ya hacen Reveal/Rail
(`PresenceGroup.register` + `childIndex`). El menú es el único que cayó al `querySelectorAll`
(`getCascadeRows`) + propagación imperativa — fue atajo. La propuesta del usuario `parent-animation="id"`
es mejor que el DOM-search, pero el **contexto de Svelte la supera** (suscripción jerárquica sin ids). El
**roving-focus** se queda con consulta-DOM donde el orden visual estricto lo exija.
### D.5 — Edge-cases (pregunta de Gemini) y qué se conserva
- **Cleanup en desmontaje abrupto (router) — gap real.** El único teardown de `Presence` desregistra del
grupo (`presence.svelte.ts:92`), NO llama `cancel()`/`motion.cancel(node)`. Un spring en vuelo sigue
corriendo sobre un nodo detached hasta converger solo (trabajo desperdiciado + retención temporal; no fuga
permanente — `track().finally` limpia el Map). La interrupción COORDINADA (reversa/`cancel`/`dispose`) sí
está limpia (AbortController + `safeCancel` + handoff de velocidad). **Svelte nativo resuelve el caso
abrupto de fábrica** → refuerza A.
- **Política de interrupción:** el mecanismo (handoff de velocidad, `peek`/`takeHandoff`) ya existe; falta
**declararla como contrato** (no es edge-case nuevo: es otra cara de la Grieta 1 — press-mientras-cierra
cruza `--event` y `--state`).
- **SSR/CLS:** el diseño ya mitiga (estado base = presente/visible; motion solo en transiciones, no en el
primer montaje); falta el contrato explícito `data-initial-state` (M7).
- **Se conserva:** `arts/motion` (spring con handoff, FLIP) para lo que CSS no puede — el _progressive
enhancement_ legítimo; el contrato morfo; la doctrina state/visual.
### D.6 — Plan en fases (a reformular sobre D.2/D.3, aún sin aprobar)
0. **Prototipo descartable** del cascade del menú con Svelte-nativo + registro-por-contexto + firma unificada
(motion+sonido sin neutralizar). Caso más difícil (owner + hijos-DOM + portal + roving-focus). Criterios
binarios medibles en navegador real (Playwright). NO producción. Valida A y B a la vez.
1. Unificar la firma (la animación-de-evento se declara en el evento) — primero Reveal/Rail.
2. **Delimitar los tres dominios** (D.3) — NO «colapsar a uno»: declarar por componente qué gobierna cada
animación; prop `animation` reservada a contenido, prohibida (tipos/lint) donde haya evento que la porte.
DevX: tipado estricto + flag `UIX_DEBUG_MOTION`.
3. Migrar el menú a producción (retirar `DomCascade`).
4. Edge-cases (D.5).
5. Limpieza (retirar lo que el nativo reemplace; el spring-JS de Dialog probablemente sobrevive como island).
**Fuera de alcance:** `words`/`palabras`, `chronos`, `time-picker`, y el **engine de sema** (sema no se
toca; la coexistencia se resuelve en motion/eidos). Todo aditivo / opt-in.
### D.7 — Fase 0 validada (prototipo Svelte-nativo)
Prototipo descartable en [`/temas/animations/menu-native`](../../../web/routes/temas/animations/menu-native/+page.svelte)
(`{#if}` + `transition:…|global` + un shim genérico que lee tokens de eidos). Valida la **Decisión A**
(Svelte nativo) en el caso difícil (menú) + los edge-cases, **medido en navegador real**:
- **Entrada** escalonada **sin ease-vs-jump** (los ítems montan en off-state; midpoints de opacidad
`[86, 149, 193, 236, 320, 361] ms`).
- **Salida** con stagger **invertido** (`[510, …, 236] ms`) + **exit-heavy nativo**: el `{#if}` retiene el
panel hasta que el último ítem termina (~553 ms) — **sin `pending()` / `getAnimations()` / `DomCascade`**.
- **Adición dinámica** a una lista abierta → entrada **individual** (delay 0). El **contenedor arbitra** con
los flags `opening`/`closing`: durante la ráfaga de apertura/cierre = cascada (índice × ritmo); una
mutación suelta = delay 0. (Confirma la distinción «evento-del-contenedor vs evento-del-ítem» de §D.3.)
- **Roll-up del panel** = firma de cierre **opcional** del panel (`when: { exit: 'after' }`): los ítems
salen, **luego** el panel colapsa su `height` (con retardo ajustable). El panel tiene firma de salida
**propia**, coordinada con la de los hijos — no solo «seguir el flujo».
**Toda la maquinaria de F2** (`DomCascade` / `pending` / `beginEnter` / doble-rAF) **desaparece**, sustituida
por `{#if}` + `transition:…|global` + el shim. Decisión A confirmada en el caso más difícil.
**Hallazgos para producción:** (1) `grid-template-rows` **no anima** por la vía de Svelte/WAAPI (no crea
animación) → colapso por `height` medido (un elemento) o `interpolate-size: allow-keywords` (CSS). (2) Chrome
real computa layout aunque la pestaña esté oculta; el preview headless no (`getBoundingClientRect` → 0).
**Pendiente de la Fase 0** (mejor sobre el componente real / con `uix` montado): el shim toggle-de-attrs
(visual 100 % en eidos reutilizando el preset coordinado), el sonido sin neutralización (prueba de la
Grieta 1), y el registro-por-contexto real (ítems como componentes, no `{#each}`).
refactor(motion): retire parallel orchestration motor (Plan A) + verified model + Fase 1 prototype Remove the parallel motion-coordination service per the redesign: animation is a channel of the EVENT's firma (sema owns it across all channels), not a parallel axis. Code is gone; the RFC stays as historical record with a retirement banner. - morfo: drop MorfoPart.animation + its schema/compile/types/exports - arts/motion: drop CoordinatedPreset / MotionConfig.coordinated - eidos: drop BUILTIN_COORDINATED_PRESETS / renderCoordinatedPresetRules / the animation:none neutralization + registry block; regen generated/base.css - soma: Presence reduced to a single-surface island; delete presence-group / dom-cascade / coordination; dropdown-menu loses the cascade wiring - delete Reveal / Rail (morfo + soma + demos) Docs: MOTION_SERVICE_RFC gains the retirement banner + §D.8 (retirada) + §D.9 (verified model: morfo->soma->sema->eidos pipeline + the two hard rules — soma never writes a visual --var; data-event-* is a single-target stamp, not a bus) + §D.10 (Fase 1 prototype). Fix stale "5 canales" claim in GUIA §11; eidos-motion + dropdown README aligned. Fase 1 prototype (web/routes/temas/animations/panel-cascade): validates the model end to end — panel->cards cascade (enter/exit), dynamic removal with Svelte out: retention, nested cascade — all via :nth-child + custom-property inheritance, with zero JS visual writes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4 months ago
> El prototipo de la Fase 0 (`/temas/animations/menu-native`) se **eliminó** en la retirada D.8 — había
> cumplido su función de validar las Decisiones A y B. Su demo hermana de dos momentos
> (`/temas/animations`) se conserva (state-presets + firma sema, no coordinación).
### D.8 — La retirada ejecutada (2026-06-19, «Plan A»)
En lugar de la limpieza incremental de D.6 (pasos 3 + 5), el usuario optó por **retirar de golpe todo el
motor de orquestación paralelo** y dejar el terreno limpio antes de reconstruir la coordinación como canal
de la firma del evento (D.2). La retirada es **funcional y verificada** — `npm run check` no añade ningún
error (los pre-existentes son de otros tracks: icon-button, spin-field, scroll-area, palabras); las suites
de morfo / motion / soma-layers quedan verdes.
**Eliminado (archivos borrados):**
- `soma/layers/presence-group.ts` (+ test) · `soma/layers/dom-cascade.svelte.ts` (+ test) ·
`soma/layers/coordination.ts`.
- `soma/components/reveal/` · `soma/components/rail/` · `morfo/components/reveal.ts` ·
`morfo/components/rail.ts`.
- Demos `web/routes/temas/animations/{presence-group,rail,compuesto,reveal,dom-cascade,dropdown-menu,menu-native}/`.
**Eliminado (código recortado):**
- **morfo**: `MorfoPart.animation` + `MorfoAnimation` / `MorfoAnimationChildren` (types) · su schema +
invariante · `compilePartAnimation` / `CompiledPartAnimation` (compile) · re-exports.
- **arts/motion**: `CoordinatedPreset` / `CoordinatedPresetName` / `MotionCoordinatedPresets` ·
`MotionConfig.coordinated`.
- **eidos**: `BUILTIN_COORDINATED_PRESETS` (`cascade-*`) · `renderCoordinatedPresetRules` + su llamada ·
la neutralización `animation: none !important` · el bloque `declare module '$motion'` del registry ·
`coordinated: …` en `themes/base` · las reglas `[data-cascade]` + `[data-animation-style]` del cascade en
`dropdown-menu.css`. `generated/base.css` regenerado.
- **soma**: el camino agrupado de `Presence` (`group`/`groupRole`/`pending`/`PresenceMember`/`release`/
`unmount`/`cancel`/registro) → `presence.svelte.ts` reducido a **island de una superficie**; `FloatingShell`
pierde `pending`; el `dropdown-menu` provider pierde la prop `animation` / `data-cascade` / `getCascadeRows`.
**Conservado:** `arts/motion` (engine spring/waapi/rect) · el `Presence` island · los state-presets
(`scale-fade`/`slide-fade`/Material) · las firmas sema visuales (`present-rise`/`dismiss-fade`) · el stagger
Material (`--motion-stagger-{index,each}`) · la doctrina state/visual · el contrato morfo (menos `animation`).
**Próximo (sin implementar):** reconstruir la coordinación padre↔hijo como **canal de la firma del evento**
(D.2) — sema lanza el evento, eidos materializa motion + coordinación reaccionando a `data-event-*`, soma
posee el lifecycle (island). El prototipo Svelte-nativo (D.7) validó el _cómo_ visual; falta cablearlo al
evento de sema.
### D.9 — Modelo verificado (auditoría 2026-06-19): pipeline + las dos reglas duras
Tras la retirada, una auditoría de 8 agentes (5 lectores de ground-truth + 3 críticos adversariales) sobre
`src/uix/sema/*` + el libro fijó el modelo y cazó varios errores de framing. **El pipeline correcto, verbo
por dueño:**
```
morfo DEFINE el contrato semántico del evento (family/intent/verb/target/sequence/hold/a11y) + estructura (parts)
↓
soma DISPARA origina el evento (runtime.trigger) y envía a sema la carga semántica (engine.emit{target,name,family,intent…})
│ — el evento es de SOMA; sema es ORNAMENTAL: sin engine, trigger corre igual (prewrite+handler+effects)
↓
sema RESUELVE+PUBLICA resuelve la firma (cascada de 5 capas → SOLO sound/haptic) y, por el META-canal visual,
│ estampa data-event-* en UN target vía adom (dom.apply) + abre/cierra el HOLD
│ — en paralelo, la misma emisión realiza sound/haptic (canales reales, NO por adom)
↓
eidos REACCIONA CSS reaccionando a data-event-* (durante el hold) + data-state/data-intent
↓
MOTION (y presence/depth/shape/color) — el stagger/índice lo calcula EIDOS en CSS (sibling-index/nth-child)
```
**Las dos reglas duras (violarlas resucita bugs ya muertos):**
1. **Soma NUNCA escribe una `--var` visual.** El índice/ritmo del stagger es animación → es de eidos, que lo
computa en CSS desde la estructura (`sibling-index()` / `:nth-child`, como ya hacen `spinner.css` /
`avatar.css`). El `DomCascade` borrado escribía `--motion-stagger-index` desde soma — **esa** era la
violación de raíz, no solo el "motor paralelo". Soma aporta estructura (hijos en el DOM) + `data-state`;
nada visual.
2. **`data-event-*` es un sello de UN `signal.target`, NO un bus de difusión.** El projector estampa un solo
elemento; el resolver resuelve solo sound/haptic/hold. **No hay fan-out por hijo dirigido por el evento.**
La cascada de los hijos NO sale de `data-event-*` — sale de eidos leyendo estructura + `data-state`.
**Errores de framing que la auditoría corrigió (a no repetir):**
- "sema emite el evento" → soma lo ORIGINA; sema lo procesa (`runtime.svelte.ts:696`; sin engine soma sigue).
- "el engine no ejecuta nada" → no realiza modalidad, pero RESUELVE la firma + posee el await del hold + la
persistencia (`engine.ts:198-271`).
- "eidos es un canal par del de sonido" → error de categoría: eidos NO está en `SemaChannelSignatures` (solo
sound+haptic) ni recibe dispatch; es una **capa/realm** que reacciona a los tokens. Nunca registrar un
`EidosChannel` ni meter motion/color en `SemaChannelSignatures`.
- "`data-event-*` son tokens visuales" → son tokens **semánticos compartidos**: los lee la cascada de sema
(sound/haptic) Y el CSS de eidos (`stamp.ts:5-11`).
- el canal `visual` es **META**: no realiza modalidad — solo estampa `data-event-*` + temporiza el hold
(`engine.ts:243-248`, `sema/README.md:43`).
**Doc corregida:** `GUIA_IMPLEMENTACION_SEMAUIX.md` §11 decía "5 canales canónicos (motion, sound, color,
presence, haptic)" — stale; reescrita al split real (sema: sound+haptic+meta-visual · eidos: los visuales).
**Consecuencia para la Fase 1 (panel + N cards):** la cascada NO se conduce estampando un `data-event-*` rico
en el contenedor. El `emerge.open` del panel cae en UN target (su flourish + sonido). La entrada escalonada
de las cards es el momento `--state` + stagger, que **eidos** calcula en CSS desde la estructura; soma solo
aporta los hijos + `data-state`. El caso dinámico (ráfaga de apertura vs alta suelta) se distingue por
`data-state` (dominio de soma), no por una `--var` que soma escriba.
### D.10 — Prototipo de Fase 1 validado (2026-06-19)
Prototipo descartable en `web/routes/temas/animations/panel-cascade/` (`+page.svelte` + `panel-cascade.css`):
un contenedor coordinador + N `<Card>` reales. **Valida el modelo en los tres casos difíciles**, con el
reparto verificado y **cero `--var` visual escrita por JS** (confirmado por valores computados frame-a-frame):
- **Cascada externa** — entrada (`:nth-child` → `--row`, delay = row × ritmo) + salida en cierre de panel
(`:nth-last-child` → `--row-rev`, reverse). El índice lo computa eidos desde la estructura.
- **Borrado dinámico con retención** — al quitar cards, **soma retiene** el nodo (Svelte `out:`, leyendo la
duración de la regla de eidos) y marca `data-leaving` (flag de **estado**); **eidos pinta** la salida
(`[data-leaving]` → card-fall). Suelto → `--row-rev 0` → inmediato; en bloque → cascada inversa. Sin
`data-phase`: el `:nth-last-child` da los dos casos.
- **Anidamiento** — contenido interno de cada card cascadea componiendo el **`--row` heredado** (índice
externo de su card) + su **`--inner-row` propio** (`:nth-child` scopeado a su lista), secuenciado tras
asentar la card. Compone por **herencia de custom properties** → escala a profundidad arbitraria sin
maquinaria.
**Hallazgos para la implementación de producción:**
- **El CSS de eidos DEBE vivir en `.css` plano**, no en `<style>` de Svelte: Svelte **poda** los selectores
que dependen de atributos puestos solo en runtime (`[data-leaving]`) por detección de CSS-no-usado. Un
recipe real ya ships así, luego no es problema en producción — pero un prototipo en `<style>` falla.
- **El índice por nth-child tiene cap** (enumerado 1–16, como `spinner.css`/`avatar.css`). Para colecciones
sin tope: el wrapper de **eidos** se auto-indexa (eidos escribe su propia var, NO soma), o `sibling-index()`
cuando madure el soporte.
- **La salida en bloque de una colección dinámica es el borde** donde el CSS-puro-desde-estructura topa (el
nodo saliente ocupa layout + `nth-last-child` se desplaza al quitar hermanos). El usuario lo validó visualmente
OK en este caso, pero la versión glitch-free pide que el **contenedor orqueste** la retirada como unidad
(lifecycle de soma, lo que hacía el `pending` borrado — sin tocar lo visual). Pendiente para producción.
### D.11 — El modelo final: tres ejes ortogonales + el lifecycle completo (2026-06-19)
Tras un re-análisis de fondo (workflow sobre código + canon) y correcciones duras del usuario, **este es el
modelo cerrado**. Supersede los matices de D.2/D.9 donde difieran; D.8 (retirada) y D.10 (prototipo) siguen
vigentes como historia.
#### D.11.0 — Tres ejes ORTOGONALES (el error de raíz era confundirlos)
El sistema NO es «una forma de animar». Son **tres cosas separables** que no hay que mezclar:
| Eje | Qué es | Qué hace EXACTAMENTE |
| --- | --- | --- |
| **sema** (semántico) | el «qué ocurrió» | **SOLO emite `data-event-*`** en el DOM (+ corre sus 2 canales runtime sound/haptic). **NO es animación.** |
| **motion** (`arts/motion`, el SERVICIO) | el **MOTOR** de animación | corre los drivers JS (`spring`/`waapi`/`rect`/`svelte`) + el path CSS declarativo + handoff de velocidad. Motor full, par/superior a Framer. |
| **eidos** (visual) | la materialización | **LEE `data-event-*` (+ `data-state`) y reacciona**: genera el CSS, decide estilo/animación, consume el servicio motion para lo JS. Único dueño visual. |
Y los dos que orquestan alrededor: **morfo** DECLARA (semántico + estructura, cero visual); **soma** DISPARA
(`runtime.trigger`) + ESTADO (`data-state`) + LIFECYCLE (presence/retención), cero var visual.
**La regla mental:** sema = el QUÉ (emite el token) · motion = el MOTOR (corre lo JS) · eidos = el CÓMO-SE-VE
(lee el token y materializa). Mezclar «sema» con «la capacidad de animar», o comparar el suelo-CSS contra el
motor entero de otro framework, es el error a no repetir.
#### D.11.1 — El lifecycle completo de un evento
```
1. DECLARACIÓN (estática, en código)
morfo: el evento (family/intent/verb/target/sequence/hold) + estructura (parts, data-state, data-stagger)
eidos: la realización — presets/signatures/keyframes en EidosConfig.motion
(derivados a generated/base.css + registrados en uix.motion)
2. TRIGGER (runtime, ORIGEN)
soma: runtime.trigger('open') ← origina desde interacción/lifecycle (sin engine sigue: sema es ornamental)
+ escribe data-state (en el contenedor y, propagado, en los items)
3. EMISIÓN (sema PROCESA)
sema: engine.emit → resuelve la firma (cascada 5 capas → SOLO sound/haptic)
→ el META-canal visual ESTAMPA data-event-* en signal.target (UN elemento)
→ abre el HOLD; en paralelo sound/haptic se realizan
4. REACCIÓN (eidos LEE, durante el hold)
eidos: el CSS del componente reacciona a data-event-* (+ data-state) → tres salidas posibles (D.11.2)
5. CLEANUP
sema: al cerrar el hold, des-estampa data-event-* (transient). Persistente → soma lo limpia (clearTarget)
6. PRESENCE (lifecycle de salida)
soma: Presence retiene el nodo durante la salida (await getAnimations / out:), luego desmonta.
La retención es soma; el visual, eidos.
```
#### D.11.2 — La reacción de eidos: una escalera de realización
Leído el `data-event-*` (+ `data-state`), el CSS del componente decide CÓMO se realiza. **No es siempre una
animación** — hay una escalera de cuatro escalones, de menos a más:
```
snap → transition → animation (keyframe / preset) → JS (servicio motion)
```
- **(a) snap — estilo estático.** CSS puro, sin interpolar (la firma en el canal color):
`[data-field][data-event-intent='threat'] { border-color: var(--color-threat-border) }`. Cambia de golpe.
- **(b) transition — estilo INTERPOLADO.** Una `transition` CSS interpola la propiedad cuando su valor cambia:
`transition: border-color var(--duration-fast)`. El borde rojo **entra suave** en vez de saltar. Es el suelo de
las micro-interacciones (hover/focus, `archetypes.css`) y de cambios de estado de UNA propiedad. La corre el
navegador.
- **(c) animation — keyframe / preset.** La firma (`--event`) o el preset (`--state`: enter/exit con forma +
stagger, fill `backwards`). Declarativa; **el navegador la corre**; motion la tiene en registry + Presence la
espera, pero no la ejecuta.
- **(d) JS — el servicio `motion`.** Física (`spring`), FLIP (`rect`), reversa interrumpible. La ejecuta
`uix.motion.run`, disparada por **soma/Presence** leyendo `data-animation-style` + tokens. El CSS NO llama a JS.
Frontera clave: **declarar (CSS) ≠ ejecutar.** El navegador corre (a)/(b)/(c); el servicio motion corre (d),
disparado por soma. El CSS DECLARA qué hace motion; no lo invoca. **Lifecycle:** `Presence` espera tanto los
`CSSAnimation` como los `CSSTransition` vía `getAnimations()` al salir — el servicio no gestiona las `transition`,
pero el lifecycle sí las contempla.
#### D.11.3 — Firma vs realización (el seam, canon §10/§15)
Una firma conserva IDENTIDAD bajo variación; los valores concretos son realización:
- **GENÉRICO (firma, del evento):** la identidad — «un `emerge` aparece» (flourish del contenedor, signature
`present-rise`) y «los hijos de un contenedor que emerge aparecen JUNTOS, por orden estructural».
- **ELÁSTICO (realización, per-componente):** los valores — el preset (keyframe), la duración, el ritmo
(paralelo/cascada), el easing. Un menú a 40ms y un toast a 0ms (paralelo) realizan la MISMA firma distinto.
→ El timing NO es un parámetro genérico de firma (eso resucitaría la Grieta 1). Es **realización per-componente**,
en su estilo de eidos.
#### D.11.4 — La cascada/stagger: el sistema EXISTENTE + una regla
La cascada NO es un sistema nuevo. Es el preset declarado + el stagger que ya existía + **UNA regla de foundation**:
- **El índice (foundation, generado por `render-css.ts`):** `[data-stagger] > *:nth-child(N) { --motion-stagger-index: N-1 }`
(forward) + `:nth-last-child → --motion-stagger-index-rev` (reverse). **Nadie lo escribe; sale de la
estructura.** (El `DomCascade` borrado lo escribía desde soma = la violación raíz.)
- **El delay (en CADA preset, ya estaba):** enter `calc(var(--motion-stagger-index) * var(--motion-stagger-each))`;
exit usa el reverse. **Paralelo = `--motion-stagger-each: 0`; cascada = N.** No hay regla de «tipo»: el tipo
lineal es el VALOR del token, en el estilo del componente.
- **La animación:** el preset que el item lleva en `data-animation-style` (declarado en EidosConfig.motion).
Keyframe + duración + reduced-motion: del preset, gratis.
**Dos dominios para usarlo:** (1) **explícito** — el componente `<Cascade>` (orquestador fino, sin recipe ni
`--cascade-*`): marca `[data-stagger]`, refleja `open`→`data-state`, pasa el preset a los items por contexto;
(2) **event-driven** — un componente real declara en SU morfo el evento `emerge` + `data-stagger` en el
contenedor; sus items llevan el preset + `data-state`. La aparición sale del evento declarado, sin wrapper.
#### D.11.5 — El servicio `motion` es el motor (par/superior a Framer)
`EngineMotion` (`arts/motion` · `uix.motion`): drivers `spring` (integrador de Euler semi-implícito:
overshoot/settle/drag/snap), `waapi`, `rect` (FLIP/genie, medido + batcheado), `svelte`, `css` (suelo
declarativo). **Handoff de velocidad** (`peek`/`takeHandoff`): la reversa continúa desde posición + velocidad
actuales (el caso difícil de la interrupción). **Puerto DOM inyectable** (`MotionDom`) → iframe/popup/SSR-test
(Framer asume `document` global). En capacidad de motor, **par o superior**. Madurez pendiente: el catálogo de
presets-JS-por-nombre está a medio poblar (los CSS vienen de serie; los JS se registran por app).
El `spring` es además el **equivalente INTERRUMPIBLE de una `transition` CSS** (D.11.2.b): una `transition`
**salta** al revertir a media interpolación; el `spring` **continúa desde la velocidad actual** (el handoff). Por
eso la frontera práctica: interpolación simple no-interrumpible → `transition` CSS (eidos, browser); física o
reversa interrumpible → el driver JS del servicio.
#### D.11.6 — Qué se corrigió (vs el primer intento de esta sesión)
El primer intento de productización hizo un `<Cascade>` con recipe propio + un namespace `--cascade-*`
(`index`/`each`/`base`/`pass`) + keyframes propios. **Era reinventar** el `--motion-stagger-*` + los state-presets
que ya existían (+ le faltaba reduced-motion + un `getComputedStyle` crudo violando ActiveDom). Corregido:
**retirado el `--cascade-*` entero**; la cascada monta sobre el preset declarado + el índice de foundation;
`<Cascade>` quedó como orquestador fino del dominio explícito (consume el sistema, no lo duplica). Verificado en
navegador + `npm run check`/`motion.test`/eidos-lint limpios.
---
> **Fuentes.** Canon semántico: [`docs/CANON.md`](../../../docs/CANON.md). Arquitectura de
> capas: [`active_architecture.md`](../active_architecture.md). Motion actual (que este RFC
> extiende): [`eidos-motion.md`](./eidos-motion.md). Motor: `src/arts/motion/README.md`.
> Lifecycle de presencia: `src/uix/soma/layers/presence.svelte.ts`.

Powered by TurnKey Linux.