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/components/motion/README.md

6.3 KiB

Motion

A single-element content animator (RFC §D.12 — the content domain). Wrap any content you want to animate in and out; it rides the existing state-presets + reduced-motion — no new mechanism, no JS engine. Same motion selector as the overlays and <Cascade>.

<script>
  import { Motion } from '$uix/eidos/components/motion'
  let show = $state(true)
</script>

{#if show}
  <Motion motion="scale-fade">
    <div class="card">…</div>
  </Motion>
{/if}

motion is any registered preset (fade / scale-fade / slide-fade / …); 'none' disables. The keyframe / duration / easing / reduced-motion all come from the preset.

Baseline

No Air baseline — new component (2026-06, RFC §D.12). The prior repo pattern was hand-spread data-animation-style + ad-hoc exit setTimeouts in demos; Motion packages that lifecycle once.

Comparativa

Reference Their shape Difference
Framer Motion <motion.div> / AnimatePresence JS animation engine; exit needs AnimatePresence bookkeeping Motion runs NO JS animation — it stamps the state attr and retains the node; keyframes/duration/reduced-motion live in eidos CSS
Svelte transition: directives Per-element JS/CSS transitions authored inline at each call site Motion consumes the REGISTERED preset catalog — one vocabulary, themable, reduced-motion handled centrally
Radix / Ark presence Presence utilities tied to their overlay state machines Motion is the free-standing content case (no state machine, no semantic event) — overlays here already have their own presence via soma
Motion One Imperative WAAPI tweens Same boundary: eidos owns the visual; the wrapper owns only lifecycle

Passive justification

Zero events by design: Motion animates content that merely appears or disappears — no interaction, no semantic occurrence (the book's two-moment model puts eventful animation in the FIRMA, which components declare themselves). scope: ['eidos'], events: [] in the morfo.

Audit exceptions

  • E-2.2 exception: Motion ships no motion.css on purpose — it has no visual of its own; the enter/exit rules it rides are the state-presets in generated/base.css (declared in EidosConfig.motion), and adding a recipe here would fork them (see Decisiones).

How it works

Phase Mechanism
enter (on mount) motionAttrs(motion) puts data-animation-style + a presentation data-state="open" on the element → the EXISTING [data-animation-style][data-state='open'] enter rule plays once. Flash-free (attrs at insertion, backwards fill).
exit (on removal) Svelte out: flips the node to data-state='closed' (the existing exit rule plays) and retains it for the eidos-declared duration (read via the ActiveDom getComputedStyle), then unmounts.

The wrapper writes no visual var and runs no JS animation — it only sets the state attr and holds the node during exit. Eidos owns the keyframes.

The reveal, published to the subtree

trigger="viewport" makes the entrance a moment, and descendants sometimes have to synchronise with it. Motion publishes it by context:

<script>
  import { getMotionContext } from '$uix/eidos/components/motion'
  // `undefined` outside a <Motion> — then there is no reveal to wait for.
  const motion = getMotionContext()
</script>

<CountUp to={12500} startWhen={motion?.seen ?? true} />

seen is false while a viewport animator waits and true from the start for every other trigger. The precedent is <Cascade>, which already published open to its items; this is the same seam on the single-element animator.

Why it is a seam and not an observer. Its only other trace is data-animation-pending in the DOM, so a consumer that needed the moment had to watch the wrapper from outside — which is what the blocks contract (B-6) forbids, and what left stats-band's counters running behind opacity: 0 (ledger A-68 / A-93). An IntersectionObserver does not look at opacity, so two animators with different thresholds disagree silently; the context is what lets the inner one defer to the outer one instead of guessing.

<Motion> vs motionAttrs

Use Extra node Exit
<Motion> wrap content to animate in/out yes (a real box — needed for transform/opacity) ✓ (out: retention)
motionAttrs(preset) spread onto your OWN element no — (enter only; you manage exit)

Reach for motionAttrs when an extra wrapper would disrupt layout (inline content) and you only need the entrance; reach for <Motion> when you want the full in/out lifecycle.

Domain boundary

Motion is the content domain only — content that simply appears/disappears, with no semantic event and no state machine. Do not wrap:

  • a stateful component (its own data-state: a Card's selected/idle, a checkbox's checked) — Motion's presentation data-state would clash; that component selects its preset via its own motion prop tied to its own state.
  • an event surface (an overlay's emerge, a commit) — the firma owns the motion; the prop there is an override, not a wrapper.

Props

Prop Type Default Notes
motion MotionPresetName 'fade' The preset played on mount (enter) and removal (exit). 'none' disables.

Plus any HTMLAttributes<HTMLDivElement>.

Decisiones

  • No recipe, no morfo events. Motion is structural lifecycle + a preset selector; the morfo (scope: ['eidos']) declares only the data-state of its single part. It reuses the motion registry + the state-presets — it adds no keyframes of its own.
  • Reuses data-state, not a bespoke trigger. The content domain fires via a constant presentation data-state="open" (the <Cascade.Item> pattern), so the existing enter/exit + reduced-motion rules apply verbatim and the generator stays untouched (RFC §D.12).
  • A real box, not display: contents. The transform/opacity need a layout box. For zero-extra-node, use motionAttrs on your own element.

Gaps

  • No demo page yet (deferred to the demo/docs web phase) — disposition: implementar.
  • Enter-only motionAttrs leaves exit to the caller by design — disposition: descartar (the full lifecycle IS <Motion>).

Powered by TurnKey Linux.