docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
title: Motion — implementation guide
type: guide
audience: human + agent
status: current
source: migrated from src/uix/eidos/MOTION_GUIDE.md (2026-07-02, docs-book F7.3)
---
# 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
docs(book): F7.3 (8/9) — eidos-motion translated to docs/theming/motion.md
src/uix/eidos/eidos-motion.md (698 L, Spanish) translated to English, same
s1-s19 numbering: the two-moment thesis, the closed cascade model
(D.11-D.13), motion across the 4 layers, the two-surface registry, types,
the EngineMotion API + cleanup policy, the 5 drivers, the DOM contract
(data-animation-style), Presence integration, reduced motion, primitives/
keyframes, built-in content, per-component defaults, code map, s15 (the
events.css -> signatures migration — cited from events.css), Chakra
comparison, naming, phases F1-F7, deferred. Stub with the full s-map at
the old path; corpus links swept (comparison, eidos chapter, changelog,
motion-guide). docs:check 0 errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> [`eidos-motion.md`](./motion.md); for the *design decisions & history* (incl. the
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
> retired "coordinated" engine) read [`MOTION_SERVICE_RFC.md`](../../src/uix/eidos/MOTION_SERVICE_RFC.md); for
> the *engine API* read [`src/arts/motion/README.md`](../../src/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 |
revert: deshacer la auditoría entera — se hizo sin leer la doctrina
Revert de los 7 commits de la sesión del 2026-07-29/30:
352ca8bbe style(soma): formato Prettier en el test de gradient-picker
17a1f167b test(soma): chat-list
e70397fca test(soma): field-langs y gradient-picker
c9b83d025 docs(morfo): qué hace pública una parte + hallazgos retirados
feb4a8424 test(soma): los primeros providers que no tenían red
f8e35b8fd fix(morfo): el eje intent/color
c39170abb fix(uix): la auditoría del sistema
`4e785d32b` (stats-band, otra sesión) queda intacto — el revert lo salta.
## Por qué se revierte todo y no una parte
La instrucción de partida era «audita el sistema, **para ello previamente lee
toda la documentación**». No se leyó. Se auditó primero y se justificó después,
y eso contaminó el conjunto, no unos commits concretos:
- Tres hallazgos del informe eran FALSOS, todos de la misma forma —
heurísticas de una sola vía dadas por hechas sin abrir el código:
D-3 `AgentTimersPort` (el puerto sí está satisfecho por el adaptador de
`defineActiveAgent`); «5 derivas de scope» (`avatar-group` / `path-trace` /
`rotate-align` sí están implementados, co-locados en el directorio del
padre); «29 morfos con `kind:'public'` irreal» (medía si existe
`<Componente.Parte>` e ignoraba que un primitivo de API plana compone por
props y snippets).
- `c9b83d025` existe SÓLO para retirar esos hallazgos del commit anterior.
- Los 6 tests de provider se montan sobre un `installSomaHarness` que FABRICA
cuatro servicios compartidos (`dom`, `langs`, `timers`, `prefs`), violando la
primera regla de propiedad de `architecture/active-uix.md`: «Only composition
roots create shared services. `morfo`, `soma`, `sema`, `eidos` and components
never create `dom`, `langs`, `prefs`, `format`, `clipboard` or equivalents:
they receive them from `ActiveUix`.» El arranque real son tres líneas
(`createActiveUix` → `setActiveUix` → `Soma.create()`) que estaban escritas.
- Y se le pidieron al usuario decisiones cuya respuesta estaba en el corpus
(`testing-and-tooling.md` dice que los tests de provider son convención, no
guard; la regla 1 zanja el arnés).
Triar qué conservar exigía justo el juicio que falló. Se revierte todo, se lee
el corpus, y vuelve sólo lo que se pueda sostener citando un documento — o lo
que sea defecto MEDIBLE y por tanto no dependa de criterio (el colapso de
uniones de props, que hacía compilar `<Avatar color="nonsense">`; que
`translations:check` crashease en cada ejecución de su historia). Nada se
pierde: los commits siguen en la historia y se recuperan con `cherry-pick`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
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.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
## 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` ](../../src/uix/eidos/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` ](../../src/uix/eidos/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
feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs
Two streams, split by what the animation touches:
STREAM A — decorative backgrounds → the pack tier
- arts/scene: a consolidated scene runtime ($scene) that owns, once, the
citizenship every ad-hoc background reinvented or skipped (frame loop,
off-view pause, DPR cap, mandatory reduced-motion, WebGL context
loss/restore, scene budget, teardown). SceneDom port (adom satisfies it),
webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader +
draw + glContext.depth/dprCap) for real geometry (beam, particles, dither,
grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources.
- src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered
effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors
are token-aware (P-4). One-way dependency, removable-by-construction.
- resolveToken extended to semantic color slots (--color-{role}-{slot}) so
consumers resolve theme tokens to concrete colors (the P-4 half).
STREAM B — animations over real text → canon
- Six components (count-up + text-{gradient,circular,blur,focus,scramble}):
each a morfo + eidos recipe (where there's styling) + demo. CountUp is a
service component (counts through uix.format.numbers). The five Text* are
passive decoratives. Upgrades over the seeds: SR hardening (real text
visually-hidden + aria-hidden decoration), a11y fix (no fake role=button),
measurement discipline (cached rects via dom.measure, no reflow storm),
reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform).
- MorfoElement gains 'p'.
DOCS
- docs/architecture/packs.md (pack tier, admission rule, P contract, Aura
promotion path); docs/decisions/design-text-effects.md (the family design
record) + indexed in decisions.md / README.md; glossary entries
(scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring
bridge; motion-guide content-effects note; strata tables acknowledge packs.
Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check
0/36 · check 0 own errors. Verified in browser (32 effects mount+compile;
6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient
resolves token stops to OKLCH via var()).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
node). WebGL/canvas "ambient" backgrounds were a no-goal here until 2026-07-12 — they now
have their own home OUTSIDE the motion system: the `arts/scene` runtime + the `Ambient`
pack (see [`architecture/packs.md` ](../architecture/packs.md )). They remain out of scope
for the `motion` prop / preset registry — a scene is not a preset.
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no
F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos
auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los
ejes que F3 añade, y separarlas dejaría un estado que nunca se probó.
Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token
de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más
allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la
deriva contra el puntero), `spotlight` y `attach='fixed'`.
El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de
scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca
`ScrollProgress` y escribe `--background-progress`, que la rama `@supports not`
mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez
porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se
paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll
del propio lector, que es justo la clase que provoca síntomas vestibulares.
**La decisión que no estaba prevista.** El scroll y el puntero quieren mover la
MISMA capa, y una animación sobre `translate` gana a cualquier declaración
estática: el puntero habría dejado de existir sin más. Así que el scroll anima
una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único
`translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el
puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el
shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`.
El precio, dicho porque en la primera redacción escribí lo contrario tres veces:
una custom property NO se puede compositar, así que el navegador recalcula
estilo cada frame. Para una decoración es el intercambio correcto —una property
por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era
falso y ahora el código dice lo que ocurre.
**Dos footguns cerrados por forma, no por disciplina:**
- `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes
había que escribirlo también en la pila, y olvidarlo dejaba la capa
`position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin
error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae
de vuelta). La prop de la pila desaparece: no hay nada que olvidar.
- Un `speed` negativo —una capa que se mueve contra el scroll— invertía el
bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora
usa la magnitud.
**Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una
línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand
la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener
de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando
el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado
e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones
—`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio,
`declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde
el init, y seguir el prop sin escribir en la primera pasada.
**La demo** (`/uix/components/background`, v2, nueve pestañas) monta un
ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí
solo y sin padre no se puede enseñar lo único que importa: que el padre se
adopta y el layout no se mueve. Los chips son uniones completas verificadas por
el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación.
Y fue la demo la que destapó que, con A30, encender `animate` en caliente no
hacía aparecer el control de pausa — el registro era un hecho de montaje.
Invisible en una sonda, obvio con un interruptor.
Registro documental (D-BG.11): `next-features.md` §11 · la frase en
`design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) ·
`PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de
autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y
`Aura` corregida (decía «Not built yet» y está construido) · y en
`motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un
preset nombra una transición discreta CON duración, y esto es modulación
continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor.
Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los
valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y
retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el
`translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada
control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres
de las cuatro clases de capa hasta la segunda auditoría).
⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60
fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y
ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado
que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con
`animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada
por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome
visible.
Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 ·
`rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 ·
`morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de
siempre) · `check` 0 errores propios.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
- **Scroll-linked travel is not a preset either.** `Background` 's parallax
(`speed` / `depth` / `attach` ) rides `animation-timeline: view()` locally in its own recipe,
with a `ScrollProgress` fallback where the engine lacks it — deliberately NOT a `timeline`
axis in the preset registry. A preset names a discrete transition with a duration; this is
continuous modulation with no duration at all, driven by a scroll position rather than by a
state change or an event. The domain is a candidate for the motion service only if a SECOND
consumer appears ([`MOTION_SERVICE_RFC`](../../src/uix/eidos/MOTION_SERVICE_RFC.md)); one
component is not a service. See
[`eidos/components/background/README` ](../../src/uix/eidos/components/background/README.md )
§Parallax.
feat(packs+text): incorporate the animation collection — Ambient pack + text-effects family + docs
Two streams, split by what the animation touches:
STREAM A — decorative backgrounds → the pack tier
- arts/scene: a consolidated scene runtime ($scene) that owns, once, the
citizenship every ad-hoc background reinvented or skipped (frame loop,
off-view pause, DPR cap, mandatory reduced-motion, WebGL context
loss/restore, scene budget, teardown). SceneDom port (adom satisfies it),
webgl/webgl2/canvas2d drivers + a custom-pipeline extension (vertexShader +
draw + glContext.depth/dprCap) for real geometry (beam, particles, dither,
grid, eter, pixel-blast, hyperspeed). 32 effects as shared resources.
- src/packs/ambient: the first pack — <Ambient effect="…"> mounts a registered
effect; the P contract (P-1..P-6) guarded by scripts/packs-check.ts; colors
are token-aware (P-4). One-way dependency, removable-by-construction.
- resolveToken extended to semantic color slots (--color-{role}-{slot}) so
consumers resolve theme tokens to concrete colors (the P-4 half).
STREAM B — animations over real text → canon
- Six components (count-up + text-{gradient,circular,blur,focus,scramble}):
each a morfo + eidos recipe (where there's styling) + demo. CountUp is a
service component (counts through uix.format.numbers). The five Text* are
passive decoratives. Upgrades over the seeds: SR hardening (real text
visually-hidden + aria-hidden decoration), a11y fix (no fake role=button),
measurement discipline (cached rects via dom.measure, no reflow storm),
reduced-motion, ecosystem citizenship (eidos.dom/timers, no raw platform).
- MorfoElement gains 'p'.
DOCS
- docs/architecture/packs.md (pack tier, admission rule, P contract, Aura
promotion path); docs/decisions/design-text-effects.md (the family design
record) + indexed in decisions.md / README.md; glossary entries
(scene/Ambient/Aura/text effects); scene README custom-pipeline + authoring
bridge; motion-guide content-effects note; strata tables acknowledge packs.
Gates: component:audit 141/0/0 · docs:check 0/0 · scene tests 9/9 · packs:check
0/36 · check 0 own errors. Verified in browser (32 effects mount+compile;
6 text components SSR+hydrate, CountUp re-formats by locale live, TextGradient
resolves token stops to OKLCH via var()).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
- **Text effects are content animations, not motion presets.** The
[text-effects family ](../decisions/design-text-effects.md ) (`TextGradient`, `TextCircular` ,
`TextBlur` , `TextFocus` , `TextScramble` ) animates real text content — each owns its own
mechanism (CSS keyframes / rAF / WAAPI) rather than the `motion` prop. `TextBlur` 's staggered
entrance is the one that most overlaps this domain: it uses WAAPI directly today (the same
primitive the `waapi` driver wraps) and is the natural candidate to migrate behind the motion
service's **content domain** when [`MOTION_SERVICE_RFC` ](../../src/uix/eidos/MOTION_SERVICE_RFC.md )
lands. Until then, a content entrance is not a preset.
docs(book): F7.3 (2/3) — theming/ satellites: guide + notes + channels + motion-guide + changelog
Five theming docs into the book tree:
- docs/theming/guide.md — THEMING_GUIDE (241 L, Spanish -> English): add a
component's recipe step-by-step + the three theme-definition modes.
- docs/theming/notes.md — THEMING_NOTES (168 L, Spanish -> English): the
bundle/feature comparison + the controversial-decisions FAQ (in-page
anchor to s1.bis fixed to a real THEMING link).
- docs/theming/channels.md — CHANNELS_SYNTHESIS (115 L, Spanish ->
English): the per-channel-RFC capstone (two moments, 8 expression vs 3
runtime channels, the builder sextet + applyTheme).
- docs/theming/motion-guide.md — MOTION_GUIDE (213 L, already English):
moved with frontmatter + links repointed.
- docs/theming/changelog.md — THEMING_CHANGELOG (1310 L): chronicle,
moved VERBATIM (recorded history is not translated), links repointed.
Thin stubs at all five old paths; corpus links swept (docs map E3/E4
rows, building-a-component phase 5, decisions umbrella, getting-started,
eidos chapter). docs:check 0 errors (258 docs).
Remaining in F7.3: THEMING.md itself (1499 L) + eidos-motion.md (698 L).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
---
## 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` |