feat(motion): `motionAttrs` helper — content-domain animation surface, (b) step 1

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:

  <div {...motionAttrs('scale-fade')}>…</div>   // 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
`<Cascade.Item>` 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 `<Motion>` wrapper (out: retention → exit) + wiring the
`motion` prop into real components.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent bfac34dd4f
commit 4f15be9f1b

@ -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.** `<Cascade>` `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 `<Motion>`).
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`). `<Cascade>` `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**: `<div
{...motionAttrs('scale-fade')}>` 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 `<Cascade.Item>` 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 `<Motion>` (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

@ -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,

@ -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 `<Cascade.Item>` 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.
*
* <div {...motionAttrs('scale-fade')}>…</div> // 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 `<Motion>` 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<string, string> {
if (!preset || preset === 'none') return {}
return { 'data-animation-style': preset, 'data-state': 'open' }
}
Loading…
Cancel
Save

Powered by TurnKey Linux.