From 4f15be9f1bbd457405036bf5e10c10678fc73845 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 20 Jun 2026 21:47:44 +0200 Subject: [PATCH] =?UTF-8?q?feat(motion):=20`motionAttrs`=20helper=20?= =?UTF-8?q?=E2=80=94=20content-domain=20animation=20surface,=20(b)=20step?= =?UTF-8?q?=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The universal selector for the CONTENT domain (RFC §D.12). `motionAttrs(preset)` returns the attrs that make ANY element play a registered preset's enter on mount:
…
// fades + scales in on mount Resolves the open "how does content fire" decision — and reverses the §D.12 plan's guess. Instead of a new `data-motion-play` trigger (which would need a generator change + a base.css regen, currently entangled with an unrelated `press`/chronos WIP), it reuses a constant presentation `data-state="open"` — the same mechanism `` already uses for content. The event/state/content line is about who OWNS the animation (firma / state-machine / the prop directly), not about whether the CSS trigger is `data-state`: content is "always shown" with no machine to flip it. Reuses the enter/exit + reduced-motion rules verbatim; `'none'`/ undefined opts out; flash-free when spread in markup (`backwards` fill). Verified in-browser: an element with the produced attrs gets `animation-name: scale-in, fade-in`, enter progresses 0%(op0,scale .985)→50%(.84,.998)→100%(1,1); `'none'` returns no attrs. `check` clean for the new file + export. Step 2 (pending): a `` wrapper (out: retention → exit) + wiring the `motion` prop into real components. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/uix/eidos/MOTION_SERVICE_RFC.md | 21 +++++++++++++++------ src/uix/eidos/index.ts | 3 +++ src/uix/eidos/lib/motion/motion-attrs.ts | 24 ++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 src/uix/eidos/lib/motion/motion-attrs.ts diff --git a/src/uix/eidos/MOTION_SERVICE_RFC.md b/src/uix/eidos/MOTION_SERVICE_RFC.md index 9386d41c6..659935826 100644 --- a/src/uix/eidos/MOTION_SERVICE_RFC.md +++ b/src/uix/eidos/MOTION_SERVICE_RFC.md @@ -1494,12 +1494,21 @@ existe pero está **dormido** (todos los presets shipped son `driver: 'css'`). E 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). +- **(a) Unificar el nombre — ✅ HECHO** (commit `bfac34dd`). `` `animation` → `motion` (prop + + `CascadeContext`); ningún otro wrapper usaba `animation`. El catálogo es `MotionPresetName`. +- **(b) Universalizar — paso 1 HECHO.** El helper `motionAttrs(preset)` (exportado de `$uix/eidos`, + `lib/motion/motion-attrs.ts`) hace de cualquier elemento una **superficie de contenido**: `
` juega el ENTER del preset al montar. + - **Decisión de cómo dispara el contenido — RESUELTA, y al revés de lo que esta sección preveía.** El plan + decía «sin `data-state`» (un trigger nuevo). Se reusa **un `data-state="open"` de presentación constante** + — el MISMO patrón que `` ya usa para contenido — porque (1) reusa la regla de enter/exit + + reduced-motion sin tocar el generador, (2) da el exit gratis, (3) es patrón establecido. La línea + evento/estado/contenido es de **PROPIETARIO de la animación** (firma / máquina de estado / la prop + directa), NO de si el trigger CSS es `data-state`: el contenido es "siempre mostrado" (un `data-state` + constante), sin máquina que lo conmute. Flash-free al spreadear en markup (attrs en la inserción, `backwards` + fill). `'none'`/undefined → sin attrs (opt-out explícito). + - **Paso 2 (pendiente):** un wrapper `` (con `out:` retención → exit en desmontaje, + su morfo) y + cablear el prop `motion` en componentes reales. Loops (contenido infinito) = variante posterior. - **(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 diff --git a/src/uix/eidos/index.ts b/src/uix/eidos/index.ts index f7ac2592b..9a848b4ba 100644 --- a/src/uix/eidos/index.ts +++ b/src/uix/eidos/index.ts @@ -199,6 +199,9 @@ export type { // Momento-`--state` preset-name registry (F7 typegen) — app-extensible via // `declare module '$uix/eidos' { interface EidosMotionPresets { … } }`. export type { EidosMotionPresets, MotionPresetName } from './lib/motion/registry'; +// `motionAttrs(preset)` — make any element a content-domain animation surface +// (plays the preset's enter on mount via a presentation `data-state`). RFC §D.12. +export { motionAttrs } from './lib/motion/motion-attrs'; export { ASPECT_RATIO_KEYS, BORDER_STYLE_KEYS, diff --git a/src/uix/eidos/lib/motion/motion-attrs.ts b/src/uix/eidos/lib/motion/motion-attrs.ts new file mode 100644 index 000000000..326420ee7 --- /dev/null +++ b/src/uix/eidos/lib/motion/motion-attrs.ts @@ -0,0 +1,24 @@ +import type { MotionPresetName } from './registry' + +/** + * Make ANY element a CONTENT-domain animation surface (RFC §D.12 / §D.13): spread + * these attrs and it plays the `preset`'s ENTER on mount. Reuses the existing + * state-preset enter rule via a constant presentation `data-state="open"` — the + * same mechanism `` uses for content — so there is NO new trigger, + * no generator change, no JS. Flash-free when spread in markup: the attrs are + * present at insertion and the preset's `backwards` fill holds the from-state + * before the first paint. + * + *
…
// fades + scales in on mount + * + * `'none'` / `undefined` → no attrs (the explicit opt-out). Exit on unmount needs + * a retention primitive (`out:` / `Presence`) + flipping `data-state` to + * `'closed'`; that is the `` wrapper's job, not a bare attr spread. + * + * For a STATEFUL component (its own `data-state` machine) or an EVENT firma, do + * NOT spread this — those own the trigger; this is the content domain only. + */ +export function motionAttrs(preset: MotionPresetName | undefined): Record { + if (!preset || preset === 'none') return {} + return { 'data-animation-style': preset, 'data-state': 'open' } +}