diff --git a/docs/README.md b/docs/README.md index 8d8dd61b5..182847f31 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 | diff --git a/src/uix/eidos/MOTION_GUIDE.md b/src/uix/eidos/MOTION_GUIDE.md new file mode 100644 index 000000000..dce57ba7e --- /dev/null +++ b/src/uix/eidos/MOTION_GUIDE.md @@ -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` / +`` 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 + … +
…
+ … +``` + +- 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=""`; 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 `` — enter on mount, exit on removal (it retains the node for the preset's +declared duration, then unmounts): +```svelte +{#if show} +
…
+{/if} +``` +No extra node wanted (inline content, enter only)? Spread `motionAttrs` on your own element: +```svelte +… +``` +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 +… +``` +`spin`/`pulse`/`bounce` rotate/fade/translate the element; `ping` scales+fades a ring (put it +on the ring child, with `transform-box: fill-box`). `` 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 `` — 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 +
+ {#each items as item}
{item}
{/each} +
+``` +Use `` (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: ``. + +### 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 `` (`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`/`` 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 ``). +- **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` / `` / `` | `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` | diff --git a/src/uix/eidos/README.md b/src/uix/eidos/README.md index 272892945..9302b6772 100644 --- a/src/uix/eidos/README.md +++ b/src/uix/eidos/README.md @@ -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 (``), 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í: diff --git a/src/uix/eidos/eidos-motion.md b/src/uix/eidos/eidos-motion.md index a8f35a5d7..f596273de 100644 --- a/src/uix/eidos/eidos-motion.md +++ b/src/uix/eidos/eidos-motion.md @@ -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` / `` / 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 ``), 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