From efb20a9c5fa95898b5a535011a9a8b3e8599d8cf Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 20 Jun 2026 21:22:58 +0200 Subject: [PATCH] =?UTF-8?q?docs(motion):=20RFC=20=C2=A7D.12=20(universal?= =?UTF-8?q?=20`motion`=20prop,=203=20domains)=20+=20=C2=A7D.13=20(structur?= =?UTF-8?q?al=20constraints)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the design framing agreed this session, BEFORE implementation: §D.12 — One universal `motion` prop on any component; the discriminant ("does the animation realize a perceptual event?") decides the domain: event (firma, morfo+sema, the prop is override/violation) / state (data-state preset) / content (prop is primary). Supersedes §5's M5/M6 "motion vs animation separate" — its justification (the coordinated `cascade-*` system) was retired in Plan A. Honest positioning: content/state = parity with Framer/SwiftUI; event = above (motion coupled with sound/haptic from one evaluated signal). Caveat: the JS engine (spring/handoff) is built but dormant — the "above" on the engine axis needs a first spring preset. §D.13 — The contracts the CSS model demands + failure modes, separating what the mechanics already guard from what's deferred: - structure: `:nth-child` ignores comments/`{#if}` (guarded); intermediate ELEMENT wrapper breaks the count (the contract); a cascading wrapper becomes its own scope (`inherits:false` isolates the index); `display:contents` = known dead-zone. - exit: animated surface exits via Presence/`out:`, NEVER raw `{#if}`; unit-exit works today (parent opacity carries the children); per-child staggered exit needs JS lifecycle (PresenceGroup, deferred) → the container-driven rule is enter-only. - debug: the index is `inherits:false` (no ancestor can step on it); a `[data-debug-stagger]` mode to surface index/delay/name is pending. Pointer added in eidos-motion.md. No code changed — design record only. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/uix/eidos/MOTION_SERVICE_RFC.md | 129 ++++++++++++++++++++++++++++ src/uix/eidos/eidos-motion.md | 19 ++++ 2 files changed, 148 insertions(+) diff --git a/src/uix/eidos/MOTION_SERVICE_RFC.md b/src/uix/eidos/MOTION_SERVICE_RFC.md index ca88d48e6..9386d41c6 100644 --- a/src/uix/eidos/MOTION_SERVICE_RFC.md +++ b/src/uix/eidos/MOTION_SERVICE_RFC.md @@ -246,6 +246,11 @@ parts con `surface: true` y que las partes referenciadas existan. ## 5. Variants separables + naming (`animation` ↔ `data-animation-style`) +> **SUPERSEDED por §D.12.** La decisión M5/M6 de abajo («`motion` y `animation` separadas por rol») +> asumía el sistema **coordinado** (`MotionCoordinatedPresets`/`cascade-*`), **retirado en el Plan A** +> (§D.8). Sin él queda UN solo sistema → **un solo prop `motion`** (§D.12). El objetivo de DX y de +> separabilidad de abajo SIGUE vigente; lo obsoleto es la dualidad de nombres. + **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." @@ -1435,6 +1440,130 @@ que ya existían (+ le faltaba reduced-motion + un `getComputedStyle` crudo viol `` 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. +#### D.12 — El prop `motion` UNIVERSAL: un selector, tres dominios + +> **Encuadre de diseño (acordado 2026-06-20, aún SIN implementar).** Supersede el §5 del cuerpo +> (decisión M5/M6 «`motion` y `animation` separadas por rol») — que asumía el sistema **coordinado** +> (`MotionCoordinatedPresets`/`cascade-*`/`data-starting-ending-style`) **retirado en el Plan A** (§D.8). +> Sin el coordinado ya no hay dos sistemas visuales que justifiquen dos nombres: queda UNO (state-presets +> sobre `data-animation-style`). Esta sección fija qué se construye. + +**La tesis — un solo prop `motion: MotionPresetName`, en CUALQUIER componente.** Hoy el selector existe +pero (1) tiene dos nombres (`motion` en overlays, `animation` en ``) — vestigio del coordinado +muerto — y (2) solo lo consumen los componentes cableados (overlays + cascade + menús). La meta: **un +nombre (`motion`)** + **universalidad** (un Button, un Card, un Badge pueden recibir `motion="X"` y +materializarlo). El registro (`EidosMotionPresets` + `EidosConfig.motion.presets`) ya es universal y +type-safe (autorar→registrar→autocompletar en todo prop); falta que el **prop y la consumición** lo sean. + +**Pero `motion` NO significa lo mismo en todo componente — el DISCRIMINANTE decide el dominio.** +La pregunta única (la misma de los 3 dominios, [`eidos-motion.md`] / memoria del rediseño): + +> ¿La animación realiza un EVENTO perceptivo —algo **aparece** / se **fija** / **reclama atención** / **sigue en curso**? + +| Dominio | Quién la dispara | Quién la posee | El prop `motion` es… | +| -------------- | ----------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------- | +| **evento** | `data-event-*` (durante el hold de sema) | morfo (la declara) + sema (firma: +sonido/háptico) | **override explícito** o **violación** (si esquiva la capa semántica) | +| **estado** | `data-state` (open/closed, checked…) | el state-preset (recipe) | **selecciona** qué preset reacciona al estado | +| **contenido** | nada — corre sola (loop / on-mount) | el prop, directo | **vía PRIMARIA** — no hay evento ni estado con qué componerse | + +Canon del ejemplo: un **spinner** es evento (`sustain` — "proceso vivo"); un **logo que late** es contenido +(no comunica nada operativo). Mismo SVG, dominios distintos; la pregunta los separa. + +**El guard (override vs violación).** Cuando un componente DECLARA un evento que debe portar la animación, +`motion="X"` es **override** si el diseñador cambia conscientemente la firma de ese evento (permitido, +explícito), o **violación** si usa el prop para animar algo que DEBERÍA declararse como evento (lo cazan +tipos/lint — el componente declara qué animaciones pertenecen a su evento). **Sin evento no hay nada que +violar: el prop es primario.** Es la regla "prop = override/violación cuando hay evento que la porte" del +rediseño, materializada. + +**Por qué esto pone a UIX conceptualmente por delante (honesto, no hype).** En el eje "animación nombrada, +reutilizable, sobre cualquier superficie" hay **paridad** con Framer (`variants`) y SwiftUI (`.transition()`). +Lo distintivo NO es el selector — es que **el mismo prop uniforme sabe en qué dominio está**: +- contenido / estado → paridad limpia (prop directo + registro type-safe); +- **evento → por encima**: la animación sale coordinada con sonido + háptico de la misma señal evaluada + (lo que SwiftUI deja DESCONECTADO en `.sensoryFeedback`; lo que Framer no tiene capa para hacer); +- y un **discriminante principista** decide cuál, en vez de dejarlo al criterio del call-site. + +Nadie más traza esta línea —evento-acoplado-a-percepción vs contenido-decorativo— **bajo un selector único**. +La universalidad de (a)+(b) NO diluye el acoplamiento metiendo los componentes-sin-evento en el mismo saco: +los pone en el dominio **contenido**, ya previsto y legítimo. El prop es uno; los dominios, tres; la frontera, nuestra. + +**El asterisco (no omitido).** Esto es superioridad **conceptual** (el encuadre). En el eje **motor** (físicas +`spring`, reversa interrumpible con handoff de velocidad), Framer demuestra más HOY porque el motor JS de UIX +existe pero está **dormido** (todos los presets shipped son `driver: 'css'`). El "por delante" también en ese +eje exige **estrenar el driver `spring`** con un primer preset JS — trabajo aparte de (a)+(b). + +**Plan de implementación (a)+(b) — acotado a eidos, NO el `PresenceGroup` pesado.** +- **(a) Unificar el nombre.** `` `animation` → `motion`; confirmar que ningún otro wrapper use + `animation`. El catálogo ya es `MotionPresetName`. +- **(b) Universalizar.** Un mecanismo uniforme para que cualquier componente acepte `motion` y emita + `data-animation-style` (un helper/acción que los wrappers compongan, o un wrapper-primitivo ``). + Decidir cómo dispara el dominio **contenido** (sin `data-state`): un attr/clase que corra el keyframe + on-mount o en loop, porque hoy los presets reaccionan SOLO a `data-state` (montaje). +- **(c) El guard.** Tipos/lint: un componente con evento-que-posee-la-animación marca el prop como override; + los demás, primario. +- Cierra §6 (routing) en su parte vigente: el enrutado a superficie sigue siendo la idea (el dev nombra, el + sistema sabe dónde), pero sin el eje coordinado separado que §6 contemplaba. + +#### D.13 — Restricciones estructurales y modos de fallo (los contratos que el modelo CSS exige) + +> **Encuadre de diseño (acordado 2026-06-20; D.13.1 ya vive en código, D.13.2/.3 con piezas DIFERIDAS).** +> El modelo declarativo (§D.11/§D.12) mueve a HTML/CSS lo que el motor paralelo retirado resolvía por +> fuerza bruta en JS. Eso exige **contratos estrictos**. Esta sección los fija y nombra los modos de fallo, +> separando lo que la mecánica **ya blinda** de lo **diferido** — para que las grietas se descubran en el +> contrato, no en producción. + +**D.13.1 — Contrato de estructura (`:nth-child` ↔ hijos directos).** + +- **Blindado por spec:** `:nth-child` cuenta SOLO elementos — ignora texto y comentarios. Los anclas `{#if}` + de Svelte (nodos-comentario) y el whitespace **no desincronizan** el índice. El renderizado condicional de + un *elemento* reindexa a los hermanos siguientes, lo cual es **correcto** (el escalonado refleja el set + realmente renderizado). +- **El contrato:** la cascada vincula un contenedor `[data-stagger]` con sus **hijos DIRECTOS** que llevan + `data-animation-style`. El componente **posee** esa estructura (como `