docs(motion): implementation guide + reconcile the doc family

- New MOTION_GUIDE.md — the task-oriented developer front door: the `motion` prop, the three
  USE domains (event/state/content), the full preset catalog, recipes (animate in/out, loop,
  state transitions, staggered list in&out, springs, custom presets), reduced-motion, theming
  tokens, [data-debug-stagger], and constraints. Links the model (eidos-motion) + the engine
  (arts/motion) + history (RFC).
- eidos-motion.md: note the 3-domain USE framing over the engine's 2 moments (the content
  domain is a usage pattern over the state machinery); fix the `fallback` overclaim — it is
  NOT auto-applied (the spring honours ctx.reduced itself, per the audit fix).
- docs/README.md (corpus map) + eidos/README.md: motion was undiscoverable — add the guide to
  E4 Guides + the "I want to…" table + the eidos Motion section.

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

@ -97,6 +97,7 @@ invented vocabulary (morfo, archetype, hold, TSC, …) one line each.
| [`src/uix/COMPONENT_COMPLETION_CHECKLIST.md`](../src/uix/COMPONENT_COMPLETION_CHECKLIST.md) | Decide when a component is *done* (machine-audited) |
| [`eidos/THEMING.md`](../src/uix/eidos/THEMING.md) · [`THEMING_GUIDE.md`](../src/uix/eidos/THEMING_GUIDE.md) | Theming reference (E1) + the add-component / define-theme how-tos (E4) |
| [`eidos/components/README.md`](../src/uix/eidos/components/README.md) | The eidos component pattern |
| [`eidos/MOTION_GUIDE.md`](../src/uix/eidos/MOTION_GUIDE.md) | Animate it — the `motion` prop, the preset catalog, loops, stagger, reduced-motion (links the model + the motion RFC) |
| [`web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md`](../web/routes/uix/lib/DEMO_AUTHORING_GUIDE.md) | Author an interactive demo page |
### E5 — Module reference
@ -116,6 +117,7 @@ and the server-authoritative engines in `src/svrs/`.
| Build a new component | `soma/COMPONENT_GUIDE.md` |
| Know if a component is finished | `src/uix/COMPONENT_COMPLETION_CHECKLIST.md` (`npm run component:audit`) |
| Theme it / add a token | `eidos/THEMING.md` + `eidos/TSC.md` |
| Animate it (motion · loops · stagger · reduced-motion) | [`eidos/MOTION_GUIDE.md`](../src/uix/eidos/MOTION_GUIDE.md) |
| Understand why a decision was made | `docs/decisions.md` → the relevant RFC / `LIBRO_VARIACIONES` |
| Use a runtime artifact (auth, cache, http, …) | `arts/README.md` + `src/arts/{name}/README.md` |
| **Write or edit documentation** | [`docs/authoring.md`](./authoring.md) — the authoring rules |

@ -0,0 +1,213 @@
# Motion — implementation guide
> **The developer front door for animating UIX.** Task-oriented: pick the recipe that
> matches what you're animating. For the *model & engine architecture* read
> [`eidos-motion.md`](./eidos-motion.md); for the *design decisions & history* (incl. the
> retired "coordinated" engine) read [`MOTION_SERVICE_RFC.md`](./MOTION_SERVICE_RFC.md); for
> the *engine API* read [`src/arts/motion/README.md`](../../arts/motion/README.md).
Animation in UIX is **declarative CSS by default** (no JS engine in the hot path) and **one
selector** — the `motion` prop, which maps to a `data-animation-style` attribute the generated
foundation CSS reacts to. A small physics engine (`uix.motion`) runs the JS drivers (`spring`)
when a preset asks for them. Every preset is **reduced-motion-aware for free**.
---
## 1. The model in a minute — three domains
The `motion` prop exists on (almost) anything, but it does NOT mean the same thing everywhere.
The discriminant is one question:
> **Does the animation realize a perceptual EVENT** — something *appears* / is *committed* /
> *claims attention* / is *still in progress*?
| Domain | Triggered by | Owned by | `motion` is… |
| --- | --- | --- | --- |
| **event** | `data-event-*` (during a sema signal's hold) | morfo (declares it) + sema (the firma: + sound/haptic) | an explicit **override** of the event's signature (or a violation if it should *be* an event) |
| **state** | `data-state` (`open`/`closed`, `selected`…) | the state-preset (recipe) | **selects** which preset reacts to the state |
| **content** | nothing — it runs on mount / forever | the prop, directly | the **primary** trigger (no event, no state to compose with) |
A spinner is **event** (`sustain` — "a live process"); a pulsing logo is **content** (it
communicates nothing operational). Same SVG, different domain — the question separates them.
The **content domain is a usage pattern over the state-preset machinery**: `motionAttrs` /
`<Motion>` set a *constant* presentation `data-state="open"` so the existing enter rule plays.
So at the engine level there are still **two moments** (event-signatures + state-presets, see
`eidos-motion.md`); "three domains" is the *usage* framing.
---
## 2. The `motion` selector
```svelte
<Popover.Content motion="scale-fade"> … </Popover.Content> <!-- a component prop -->
<div {...motionAttrs('fade')}> … </div> <!-- a bare element -->
<Motion motion="spring-pop"> … </Motion> <!-- a content wrapper -->
```
- The value is a **registered preset name**. Built-ins autocomplete + type-check via the
augmentable `EidosMotionPresets` registry (`lib/motion/registry.ts`); an app adds names by
declaration-merging it. `'none'` (or `undefined`) disables — the explicit opt-out.
- Under the hood the wrapper writes `data-animation-style="<preset>"`; the generated CSS
(`generated/base.css`) animates on it. The engine only gets involved for JS presets.
---
## 3. The preset catalog
| Preset | Kind | For | Notes |
| --- | --- | --- | --- |
| `fade` | enter/exit | the safe default | opacity only — reduced-motion-safe by nature |
| `scale-fade` | enter/exit | popovers, dialogs, content | scale + fade; `transform-origin` aware |
| `slide-fade` | enter/exit | anchored panels (menus, tooltips) | side-aware via `data-side` |
| `slide-full` | enter/exit | drawers / sheets | full slide by edge (inset-aware) |
| `collapse` | enter/exit | accordions / disclosure | animates a measured `var(--height)` |
| `shared-axis-x` / `-y` | enter/exit | spatial navigation (Material) | directional slide + fade |
| `fade-through` | enter/exit | unrelated content swap (Material) | scale-up + fade |
| `select-pop` | **emphasis** (state domain) | a "becoming selected" confirmation | scale pop, **no opacity** (stays visible) |
| `spin` · `pulse` · `ping` · `bounce` | **loop** (content domain, infinite) | loaders · skeletons · notifications · cues | run forever while present; themeable per loop |
| `spring-pop` | **JS** (`spring` driver) | a physical bounce on an overlay | runs via `uix.motion`, not CSS |
Presence presets (`fade`/`scale-fade`/…) go **0→1** — they appear/disappear. **Never** use one
in the *state* domain (a still-visible element would flash from invisible); use an **emphasis**
preset (`select-pop`). Loops never enter/exit — they animate continuously.
---
## 4. Recipes — "how do I…"
### Animate content in & out (mount / unmount)
Wrap it in `<Motion>` — enter on mount, exit on removal (it retains the node for the preset's
declared duration, then unmounts):
```svelte
{#if show}
<Motion motion="scale-fade"><div class="card">…</div></Motion>
{/if}
```
No extra node wanted (inline content, enter only)? Spread `motionAttrs` on your own element:
```svelte
<span {...motionAttrs('fade')}>…</span>
```
See [`components/motion/README.md`](./components/motion/README.md).
### Loop something forever
Use a loop preset on any element (loops are un-gated — they need no enter/exit lifecycle):
```svelte
<svg {...motionAttrs('spin')} style="transform-origin:center">…</svg>
```
`spin`/`pulse`/`bounce` rotate/fade/translate the element; `ping` scales+fades a ring (put it
on the ring child, with `transform-box: fill-box`). `<Motion motion="spin">` also works —
it unmounts promptly (a loop has no exit).
### Animate a stateful component's transition
A component with its OWN `data-state` machine (Card `selected`/`idle`, Switch `on`/`off`) drives
a preset via a dedicated `data-motion-state`, so the preset fires on its transition *without*
clobbering the semantic `data-state`. The pilot is `<Card motion="select-pop">` — it pops once
each time it becomes selected (animate-on, snap-off; mount-guarded). To wire a new one: keep
your `data-state`, emit `data-animation-style` + `data-motion-state="open"` when the active
transition happens (see `components/card/card.svelte`).
### Stagger a list IN and OUT
The container-driven cascade: mark the container `[data-stagger]`, give items a preset, set the
rhythm. Children inherit the container's `data-state` — ENTER cascades in (first item first),
EXIT cascades out in **reverse** (last leaves first):
```svelte
<div data-stagger data-state={open ? 'open' : 'closed'} style="--motion-stagger-each: 65ms">
{#each items as item}<div data-animation-style="slide-fade">{item}</div>{/each}
</div>
```
Use `<Cascade {open} motion="slide-fade">` (see [`components/cascade/README.md`](./components/cascade/README.md))
for the explicit wrapper, which manages the open/closed state + retention for you. **Exit
caveat:** the container must stay in the DOM during the stagger window — give it a longer exit
or retain+unmount after `N × each + exit`. This is the §D.13.2 *pragmatic CSS bridge* for
**bounded** lists; the "parent waits for every child" version needs JS lifecycle (deferred).
### An overlay's enter/exit
Automatic. Dialog / Drawer / Popover / … animate via soma's `Presence` + the component's `motion`
prop. You only pick the preset: `<Dialog.Content motion="scale-fade">`.
### A physical spring
Use the built-in `spring-pop`, or author your own JS preset. JS presets hold functions, so they
register **directly** in `ActiveEidos` (not the serializable `EidosConfig.motion` config):
```ts
import { spring } from '$motion'
uix.motion.register('pop', {
driver: 'spring',
enter: spring({ values: { scale: [0.8, 1], opacity: [0, 1] }, stiffness: 300, damping: 14 }),
reduce: 'opacity-only'
})
// + declaration-merge `EidosMotionPresets` to make `motion="pop"` type-check.
```
### Add a custom CSS preset (theme / app)
CSS presets are plain serializable data — add them to the config:
```ts
defineActiveEidos({ motion: {
keyframes: { 'my-in': { from: { opacity: '0' }, to: { opacity: '1' } } },
presets: { 'my-fade': { driver: 'css', enter: { keyframes: 'my-in' }, reduce: 'none' } }
}})
```
---
## 5. Reduced motion — free
A developer gets reduced-motion handling **without writing anything**:
- The pref (`motion: system | allow | reduce`) folds the OS `prefers-reduced-motion` and is
projected as `data-motion="reduce"` on `<html>` (`arts/prefs`).
- Every CSS preset declares a `reduce` policy (`none` / `opacity-only` / `instant`); the
generator emits `[data-motion='reduce'] …` overrides **and** a `@media (prefers-reduced-motion:
reduce)` block, so it works with or without the JS projection.
- **Loops stop** under reduce (the element rests in its static frame).
- **JS springs honor it**: `spring()` snaps to the target and settles — no physics — when
`ctx.reduced` (a spring IS the overshoot curve, so under reduce there must be none).
- Content via `motionAttrs`/`<Motion>` is reduced-motion-safe for free (the overrides key on
`data-state`, which it already sets).
---
## 6. Theming & tokens
Retint timing without touching a component:
| Token | Controls |
| --- | --- |
| `--duration-{fast,moderate,slow,…,sustained}` | the canonical duration scale |
| `--motion-loop-{spin,pulse,ping,bounce}` | each loop's period (declared in `:root`) |
| `--motion-stagger-each` | the stagger rhythm (set on the `[data-stagger]` container) |
| `--motion-duration-{enter,exit}` · `--motion-ease-{enter,exit}` | per-component override on an animated element (falls back to the preset's token) |
| `[data-motion-set='expressive']` | remaps the easing curves (Carbon productive ↔ expressive) |
---
## 7. Debugging
Set **`[data-debug-stagger]`** on a stagger container: every animated child gets a corner badge
with its `:nth-child - 1` index (the CSS analog of `UIX_DEBUG_MOTION`). Opt-in → zero cost
without the attr. The stagger index (`--motion-stagger-index`), `animation-delay` and
`animation-name` are also all inspectable in DevTools' Computed pane (`@property`-typed).
---
## 8. Constraints & gotchas
- **One `data-animation-style` per element.** You can't loop AND enter/exit the *same* element
with different presets — use nested elements (the demo loops an inner SVG inside a `<Motion>`).
- **The content domain reuses the state machinery** — `motionAttrs` stamps `data-state="open"`.
Harmless for loops (their rule is un-gated), but don't read it as semantic state.
- **No-goals (deliberate):** layout animations / shared-layout (the `rect` driver measures ONE
node), and WebGL "ambient" backgrounds (`demos/animations/background/*` — a separate axis).
---
## 9. Where the code lives
| Concern | Path |
| --- | --- |
| Engine (registry, run, drivers) | `src/arts/motion/` (`$motion`) |
| Preset/keyframe data + the registry | `src/uix/eidos/lib/motion/` |
| CSS generation (presets, loops, stagger, debug, reduce) | `src/uix/eidos/lib/render-css.ts` → `generated/base.css` |
| `motionAttrs` / `<Motion>` / `<Cascade>` | `src/uix/eidos/lib/motion/motion-attrs.ts`, `components/{motion,cascade}/` |
| Presence (lifecycle + awaits JS motion) | `src/uix/soma/layers/presence.svelte.ts` |
| Reduced-motion projection | `src/arts/prefs/dom-projection.ts`, `arts/adom` |

@ -519,14 +519,18 @@ layers, naming conventions, bundle purge, tokens de motion, comparación
con referencias, anti-patterns, FAQ) vive en
[`src/uix/eidos/THEMING.md`](./THEMING.md). Ese es el reference canónico.
## Motion — ver `eidos-motion.md`
El sistema de animación (modelo de **dos momentos** `--event`/`--state`,
`keyframes` + `signatures` + `presets`, generación de CSS, drivers JS, sets
productive/expressive y el typegen de nombres de preset) vive en
[`eidos-motion.md`](./eidos-motion.md). El **motor** (`EngineMotion`) es un
servicio reubicado a `arts/motion` (`uix.motion`); Eidos delega vía
`eidos.motion` y registra ahí sus presets `css`. F1–F7 implementadas.
## Motion — guía en `MOTION_GUIDE.md`, modelo en `eidos-motion.md`
- **Cómo animar (front door, orientado a tareas)** → [`MOTION_GUIDE.md`](./MOTION_GUIDE.md):
el prop `motion`, los 3 dominios de uso (event/state/**content**), el catálogo de presets,
loops (`spin`/`pulse`/…), state-domain (`<Card>`), staggered cascade, reduced-motion, theming
y debug.
- **Modelo y arquitectura del motor** (dos momentos `--event`/`--state`, `keyframes` +
`signatures` + `presets`, generación de CSS, drivers JS, sets productive/expressive, typegen) →
[`eidos-motion.md`](./eidos-motion.md). El **motor** (`EngineMotion`) es un servicio en
`arts/motion` (`uix.motion`); Eidos delega vía `eidos.motion` y registra ahí sus presets `css`.
- **Decisiones e historia** (incl. el motor "coordinado" retirado en el Plan A) →
[`MOTION_SERVICE_RFC.md`](./MOTION_SERVICE_RFC.md).
Resumen rápido de lo que cubre, para no duplicar aquí:

@ -49,6 +49,17 @@
> `src/arts/motion/README.md`. (Las secciones §5/§6/§9 + el árbol de archivos
> abajo reflejan ya el nuevo hogar.)
> **Extensión de USO — 3 dominios (2026-06-21).** Este documento describe el MOTOR, que tiene
> **dos momentos** (event/state). A nivel de USO el prop `motion` cubre **tres** dominios:
> **event** (la firma) · **state** (la transición per-componente) · **content** (el tercero:
> contenido que entra/sale/loop, vía `motionAttrs` / `<Motion>` / loops `spin`/`pulse`/… — una
> capa de USO **sobre** la maquinaria de state-presets, NO un tercer momento del motor). El
> state-domain dedicado (`data-motion-state`, piloto `<Card>`), la cascada container-driven en
> ambas direcciones y el afford `[data-debug-stagger]` son posteriores a F7. **Para la guía
> orientada a tareas** (recipes · catálogo de presets · loops · state-domain · staggered cascade ·
> reduced-motion · debug) → [`MOTION_GUIDE.md`](./MOTION_GUIDE.md). Para decisiones e historia
> (incl. el motor "coordinado" retirado en el Plan A) → [`MOTION_SERVICE_RFC.md`](./MOTION_SERVICE_RFC.md).
**TL;DR**:
- Dos momentos animables: **`--event`** (`data-event-*`, la firma perceptiva,
@ -287,7 +298,8 @@ interface JsStatePreset { // implementado (waapi/spring/rect/
enter?: MotionRun; exit?: MotionRun
requires?: ('sourceRect' | 'targetRect' | 'placement')[]
reduce?: ReducePolicy
fallback?: CssStatePreset // progressive enhancement
fallback?: CssStatePreset // declarado; hoy NO se auto-aplica
// (el driver spring honra ctx.reduced él mismo)
}
type StatePreset = CssStatePreset | JsStatePreset

Loading…
Cancel
Save

Powered by TurnKey Linux.