Three fixes from the motion-ecosystem audit:
- `spring()` honours `ctx.reduced`: snap to the target and settle instead of running
the physics. The engine only short-circuits `reduce: 'instant'`, the driver ignored
`ctx.reduced`, and Presence never applied the declared CSS `fallback` — so spring-pop
played the full bounce under `prefers-reduced-motion`.
- `<Motion>` leave(): a loop preset is infinite — detect `animation-iteration-count:
infinite` and unmount promptly instead of holding the node for one loop iteration
(1–2s, still looping) on removal.
- Loop durations are now canonical `:root` tokens (`--motion-loop-{name}`), declared
like the `--duration-*` scale instead of living only as a `var()` fallback.
Tests: engine-motion (spring reduced) + motion (loop tokens); check 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the content domain of the universal `motion` prop (RFC §D.12/§D.13):
- State domain: a dedicated `data-motion-state` attribute lets a component with
its OWN data-state machine (Card selected/idle) drive a preset on its transition
without clobbering its semantic `data-state`. Enter-only (animate-on, snap-off).
New emphasis preset `select-pop` (no opacity-from-0, for still-visible elements).
- Loops (content domain, infinite): Tailwind's canonical `spin`/`pulse`/`ping`/
`bounce`, emitted as un-gated `[data-animation-style='X'] { animation: … infinite }`
rules. Themeable per loop via `--motion-loop-{name}`; reduced motion stops them.
- Staggered-exit bridge: the container-driven cascade now covers exit too —
`[data-stagger][data-state='closed'] > …` cascades children OUT in reverse
(`--motion-stagger-index-rev`, last leaves first). The container is retained for
the stagger window then unmounts (pragmatic CSS bridge for bounded lists; the
parent-waits-for-children version still needs the retired PresenceGroup).
- `[data-debug-stagger]`: opt-in dev affordance — a CSS counter badge per child
showing its `:nth-child - 1` index (the analog of UIX_DEBUG_MOTION).
- Fix `<Motion>` exit: a `{ duration }`-only Svelte transition never completes (the
node sticks at the exit frame, a re-show reuses the dead node); add a `tick` no-op
so it times the unmount.
Generated base.css regenerated; motion.test 27/27; docs (eidos-motion + RFC) updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ergonomic companion to `motionAttrs`: wrap any content to animate it IN and
OUT. `<Motion motion="scale-fade">…</Motion>` plays the preset's enter on mount
and its exit on removal (Svelte `out:` flips to `data-state='closed'` + retains
the node for the eidos-declared duration, like `<Cascade.Item>`). Rides the
existing state-presets — no new mechanism, no JS engine.
- morfo `motion.ts` (scope ['eidos']): one Provider part declaring `data-state`.
- eidos `motion/`: component (spreads `motionAttrs` + `out:` retention) + types +
index + README (incl. the `<Motion>` vs `motionAttrs` table + the content-domain
boundary).
- badge demo: a `<Motion>` show/hide example next to the prop showcase.
Caught + fixed a real collision: `data-motion` is already the reduced-motion prefs
attr (`[data-motion='reduce']`), so the wrapper emits NO part-presence marker (a
prefs `closest('[data-motion]')` would have matched it) — only the functional
`data-animation-style` + `data-state` from `motionAttrs`.
Verified at runtime: enter (data-state=open → `scale-in, fade-in`, no data-motion)
and exit (Hide → retained with data-state=closed → `scale-out, fade-out`). `check`
clean for the morfo + component + demo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>