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/docs/theming/motion.md

818 lines
43 KiB

---
title: Eidos Motion — Design and Architecture
type: reference
audience: human + agent
authority: E1/E3 — the two-moment motion model and the engine architecture
status: current
source: migrated from src/uix/eidos/eidos-motion.md (2026-07-02, docs-book F7.3)
---
# Eidos Motion — Design and Architecture
> **Status: the two-moment model — F1–F7 implemented (2026-06-04).**
>
> This document describes Eidos's animation system. The model, anchored in
> the canon (the historical seed `GUIA_IMPLEMENTACION_SEMAUIX.md` §4.3, §7,
> §11), is:
>
> **An interaction has TWO moments, and EACH may carry animation — one, the
> other, or both:**
>
> - **The `--event` moment** (`data-event-*`): the perceptual flourish during
> a sema signal's *hold*. It is the `motion` channel of the **perceptual
> signature** (§11), defined **per event** (`family`/`intent`/`event`).
> Transient.
> - **The `--state` moment** (`data-state`): the **transition** to/from a
> persistent condition. Defined **per component** (the `motion` prop).
> Persistent.
>
> This is what makes UIX different from **every** current framework: the
> others collapse presence animation onto a single axis (Chakra: only
> `data-state` + `Presence`). UIX animates both moments and integrates them
> with the perceptual signature — more coherent and richer.
>
> **Implemented (F1–F6):** the two moments (keyframes + `signatures` +
> `presets` + generation + the `EngineMotion` engine + validation + tests +
> the `/temas/animations` demo); the **signature migrated** from `events.css`
> into the registry (`signatures`, F2); the **recipes migrated** to presets
> with per-component duration/easing overrides (F3); the **JS drivers**
> `spring` (physics) / `waapi` / `rect` (FLIP) + `Presence.motion` + a real
> overlay (Dialog) bouncing (F4); **choreography** — stagger + the Material
> `shared-axis`/`fade-through` presets (F5); **token rigor** — the raw
> signature tokenized (long perceptual holds `slower`/`deliberate`/
> `emphatic`/`sustained`), shared-axis on `--motion-distance-xl`, the
> `emphasized` easing, the `[data-motion-set='expressive']` set (F6); the
> **extensible typegen** of preset names — the augmentable registry
> `EidosMotionPresets` (F7). **Roadmap F1–F7 complete.** `data-motion-ref`
> and the "TSC `event:*` scope" are obsolete (THEMING §13/§14, no real use).
>
> **Refactor (post-F5): the motion engine is a SERVICE.** The runtime no
> longer lives in eidos: it was relocated to **`src/arts/motion`** (an *art*
> — a pure runtime artifact with no UI or cross-art dependencies), exposed as
> **`uix.motion`** and consumed by **both** layers — soma (`Presence`
> receives `motion: EngineMotion` and calls `motion.run(node, phase)`) and
> eidos (delegates via `eidos.motion` + registers its `css` presets in the
> service at boot). This **dissolves the soma→eidos coupling**: the
> `DialogProps.runMotion` prop and `eidos.motionRunner` disappear — the
> *bridge* is now `EngineMotion.run`, which reads the node's
> `data-animation-style`. Eidos keeps the **CSS generation**
> (`lib/render-css.ts`) + the presets/keyframes/signatures **DATA**
> (`lib/motion/presets/css.ts`); the types, the engine and the drivers
> (`spring`/`waapi`/`rect`) live in `$motion`. See
> [`src/arts/motion/README.md`](../../src/arts/motion/README.md). (Sections
> §5/§6/§9 + the file tree below already reflect the new home.)
> **USAGE extension — 3 domains (2026-06-21).** This document describes the
> ENGINE, which has **two moments** (event/state). At the USAGE level the
> `motion` prop covers **three** domains: **event** (the signature) ·
> **state** (the per-component transition) · **content** (the third: content
> entering/leaving/looping, via `motionAttrs` / `<Motion>` / the
> `spin`/`pulse`/… loops — a USAGE layer **on top of** the state-preset
> machinery, NOT a third engine moment). The dedicated state-domain
> (`data-motion-state`, `<Card>` pilot), the container-driven cascade in both
> directions and the `[data-debug-stagger]` affordance are post-F7. **For the
> task-oriented guide** (recipes · preset catalog · loops · state-domain ·
> staggered cascade · reduced-motion · debug) →
> [`motion-guide.md`](./motion-guide.md). For decisions and history (incl.
> the "coordinated" engine retired in Plan A) →
> [`MOTION_SERVICE_RFC.md`](../../src/uix/eidos/MOTION_SERVICE_RFC.md).
**TL;DR**:
- Two animatable moments: **`--event`** (`data-event-*`, the perceptual
signature, `signatures`) and **`--state`** (`data-state`, the
per-component transition, `presets`). One, the other, or both; they
compose in sequence (`sequence`).
- One registry (`EidosConfig.motion`) with three maps: `keyframes`,
`signatures` (the event-moment), `presets` (the state-moment).
- `data-state` belongs to **soma**; `data-event-*` to **sema**; **eidos
reads both and animates** (§7: the rule is not to OVERWRITE the other
layer's attribute, not "one axis only").
- Usage: the state-moment via the prop `motion="scale-fade"` →
`data-animation-style`; the event-moment is automatic when the event fires
(the signature).
- Drivers: `css` (the floor, Chakra parity) + JS (`waapi`/`spring`/`rect`/
`svelte` — physics/FLIP/genie/orchestration, beyond Chakra), for either
moment.
---
## Table of contents
1. [Thesis and positioning](#1-thesis-and-positioning)
2. [The two moments](#2-the-two-moments)
3. [Motion across the 4 layers](#3-motion-across-the-4-layers)
4. [Architecture — the two-surface registry](#4-architecture--the-two-surface-registry)
5. [The types](#5-the-types)
6. [The `EngineMotion` API](#6-the-enginemotion-api)
7. [The drivers](#7-the-drivers)
8. [The DOM contract](#8-the-dom-contract)
9. [Soma integration (`Presence`)](#9-soma-integration-presence)
10. [Reduced motion](#10-reduced-motion)
11. [Primitives and keyframes](#11-primitives-and-keyframes)
12. [Initial content (signatures + presets)](#12-initial-content-signatures--presets)
13. [Per-component defaults](#13-per-component-defaults)
14. [Where the code lives](#14-where-the-code-lives)
15. [The event-moment: from `events.css` to `signatures`](#15-the-event-moment-from-eventscss-to-signatures)
16. [Comparison with Chakra UI v3](#16-comparison-with-chakra-ui-v3)
17. [Naming decisions](#17-naming-decisions)
18. [Implementation phases](#18-implementation-phases)
19. [Out of scope / deferred](#19-out-of-scope--deferred)
---
## 1. Thesis and positioning
An interaction does not have **one** animatable moment; it has **two**, and
they are of different natures:
1. The **perceptual occurrence** — "something just happened" — transient,
with a `hold`, an `intent` and a `sequence`. **Sema** writes it as
`data-event-*`.
2. The **state change** — "this is now open" — persistent, the source of
truth. **Soma** writes it as `data-state`.
The canon makes it explicit (GUIA §4.3): `emerge` = "something enters or
leaves the perceptual field" → it is an **event**, not a state. GUIA §11
defines the **per-event perceptual signature**, where `motion` is one of the
channels (alongside sound/color/presence/haptic): the exit movement of a
`commit.delete + loss` is "withdrawal/descent"; that of a
`signal.alert + threat` is "protruding entrance". Movement is defined **per
event**.
> **UIX's differentiator.** Every current framework animates presence on
> **one axis** (Chakra: `data-state` + `Presence`; Radix/Ark: same). UIX
> distinguishes the two moments and **animates both**, integrating the
> event-moment with the perceptual signature (sound/haptic included). That
> is what makes it more coherent (one model, two sharp moments) and richer
> (the animation of "what happened" is never confused with "what state we
> are in").
**What eidos owns**: the typed DATA registry (`EidosConfig.motion` =
`keyframes` + `signatures` + `presets`), the CSS generation for both
surfaces, the `motion` prop and the `data-animation-style` attr. The
**execution engine** (`EngineMotion`) is NOT eidos's: it lives in `$motion`
and is consumed via `uix.motion` (eidos delegates + registers its `css`
presets there).
**What it does NOT own**: emitting the signal and its semantic signature →
**sema**; the mount/unmount lifecycle and `data-state` → **soma
`Presence`**; the attrs animated over (`data-state`, `data-side`,
`data-starting/ending-style`) → declared in **morfo**; the `allow`/`reduce`
pref → **`ActivePrefs`**.
---
## 2. The two moments
| | The **`--event`** moment | The **`--state`** moment |
|---|---|---|
| **Attribute** | `data-event-*` | `data-state` |
| **What it is** | the perceptual occurrence (the signal) | the transition to/from a persistent condition |
| **Its animation** | the **signature's flourish** (settle, pulse, intent-tinted withdrawal, a `present`'s entrance…) | the **presence/layout transition** (scale, slide, grow to the open height…) |
| **Granularity** | **per event** (`family`/`intent`/`event`) — generic, consistent system-wide | **per component** (the `motion` prop) |
| **Attr owner** | sema (stamps it during the `hold`) | soma (effects) |
| **Nature** | transient (lives the `hold`) | persistent |
| **In the registry** | `signatures` | `presets` |
| **Canon** | §11 (perceptual signature), §4.3 (emerge), §7.1 | §7.2, §5.2 (`sequence`) |
**Each moment may carry animation — one, the other, or both:**
- `press` (a button): only the **event** moment (`contact.press` → squeeze).
No state transition.
- a `dialog` opening: the **event** flourish of `emerge.present` **and** the
**state** transition to `open`.
- a `collapse`: the height transition (**state**) **and/or** the `expand`
**event** flourish.
**They compose in sequence** (GUIA §5.2): `sequence: 'pre'` runs the event's
flourish **before** the state changes (e.g. animate the exit before
closing); `'post'`, after (celebrate after the real result). The
architecture chapter describes it in the causal chain
([`active-architecture.md`](../architecture/active-architecture.md) §5): the
event's animation runs during the `hold`, then the state takes over.
**`data-state` and `data-event-*` never mix** (§7.3): the rule is that
**sema does not overwrite** state attributes and vice versa (ownership). It
does NOT say only one may animate — **eidos reads both and animates both**.
What is forbidden is stepping on the other's name, not animating over both
axes.
test(motion): pin firma-vs-stagger cascade precedence (F4-D motion decision) Workflow analysis (state map + 4 dispositions + adversarial stress) resolved the direction dispute: the stagger/coordinated preset winning the (0,3,0) tie over the generic commit-affirm firma is the DOCTRINE-CORRECT outcome (RFC B.2 — the coordinated owns the visual axis, the generic firma is silenced). The earlier "flip to firma" reading was backwards; that flip contradicts B.2 and its channels:[] safety net is dormant; @layer sinks as a front-line fix (a semantic ownership question answered with a global cascade tool + a reduced-motion !important-inversion a11y risk). No shipped component shows a defect (dropdown select feedback is sound+haptic only; the item is fading out at select). The only real issue: the correct outcome rides on an ACCIDENTAL source-order tiebreak (the explicit neutralization was retired), undocumented + untested. Execute the proportionate fix: - Regression test (motion.test.ts) pinning the (0,3,0)==(0,3,0) tie + the signatures-before-presets emission order + the absence of a (0,4,0) [data-event^='commit-select'] signature — a HAZARD MONITOR, so a generator reorder or a widened signature fails loudly, not silently. - Load-bearing comment at the render-css emission order. - Doctrine note in motion.md: a known-fragile accident, not a contract; the structural endpoint ("una firma por evento") + @layer are deferred/tracked. Zero behavior change; base.css unchanged. Tests 28/28; npm run check src/ = 9 baseline, 0 new. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3 months ago
> **When both write `animation` on ONE node (KNOWN-FRAGILE).** Eidos animates
> both axes, but a firma and a `--state`/stagger preset both set the single
> `animation` shorthand — only one applies. On a shared node (a `DropdownMenu`
> item that staggers on open/close AND receives a `commit-select` event) both
> selectors compute `(0,3,0)`, so **CSS source order** decides: the generator
> emits all `signatures` before all `presets`, so the later stagger wins and the
> generic firma is masked — directionally the intended outcome (the coordinated
> owns the visual axis, RFC B.2), but produced **accidentally by emission order**,
> NOT by an opt-out (the `channels:[]` / `expression:'none'` source-silence is
> dormant; the old `animation: none !important` neutralization is retired). No
> shipped component shows a defect (e.g. dropdown select feedback is sound+haptic
> only; the masked pulse is the generic fallback and the item is fading out at
> select). This is a **provisional accident, not a contract** — pinned by
> `motion.test.ts` ("pins the firma-vs-stagger cascade precedence") so a generator
> reorder or a future `(0,4,0)` `[data-event^='…']` signature fails loudly. The
> structural fix ("una firma por evento") is
> [RFC §D.4-B](../../src/uix/eidos/MOTION_SERVICE_RFC.md). `@layer` (order-
> independent precedence) is a deferred, tracked end-state — triggered only when a
> coordinated component gains a visual commit channel, `(0,4,0)` signatures ship,
> or `@layer` is adopted for other reasons.
> **Child coordination (stagger / cascade) — CLOSED model, RFC §D.11.**
> There was a third "coordinated" axis (`PresenceGroup` / `cascade-*` presets
> over `data-starting/ending-style`); it was **retired** on 2026-06-19. The
> final model: the cascade is NOT a separate system — it is the **`--state`
> moment** (a declared preset) + the **stagger that already existed**
> (`index × --motion-stagger-each`, parallel = 0, cascade = N) + **one
> foundation rule** that writes the index from structure
> (`[data-stagger] > *:nth-child → --motion-stagger-index` /
> `:nth-last-child → -rev`, generated in `lib/render-css.ts`). The event's
> signature (`emerge` → `present-rise`) is only the **container's** flourish
> (generic, over `data-event-*`); the children's timing is **per-component
> realization**, not a sema channel. Three orthogonal axes (sema emits ·
> motion is the engine · eidos materializes) + the full lifecycle:
> [RFC §D.11](../../src/uix/eidos/MOTION_SERVICE_RFC.md).
> **The UNIVERSAL `motion` prop — one selector, three domains (design
> framing, [RFC §D.12]).** After retiring the coordinated axis, ONE system
> remains → **a single `motion` prop** (today `<Cascade>` still uses
> `animation`; it gets unified). The goal: **any** component (not just
> overlays) can receive `motion="X"` from a type-safe registered catalog.
> What the prop means is decided by the **discriminant** "does the animation
> realize a perceptual event?": **event** → the signature rules, the prop is
> an override/violation (types/lint); **state** → it selects the
> state-preset (`data-state`); **content** (no event) → the prop is the
> **primary** path. The sound/haptic coupling applies only to the event
> domain — content ones stay in clean parity. Detail + plan (a)+(b):
> [RFC §D.12](../../src/uix/eidos/MOTION_SERVICE_RFC.md).
> **Structural constraints of the CSS model — [RFC §D.13].** The contracts
> the declarative approach demands: (1) **structure** — `[data-stagger]` ↔
> DIRECT children; `:nth-child` ignores comments/`{#if}` (hardened), but an
> intermediate wrapper element breaks the count → a cascading grouper
> becomes its own scope (`inherits:false` isolates the index);
> `display:contents` without re-scoping = a dead zone. (2) **exit** —
> unit-exit via `Presence` (retain + await; the parent's `opacity` drags the
> children) works today; per-child staggered-exit requires lifecycle JS
> (`PresenceGroup`, deferred) → the container-driven rule is ENTER-ONLY;
> **never raw `{#if}`** on an animated surface. (3) **debug** — the index is
> `inherits:false` (no ancestor can stomp it); everything lives typed in
> Computed; the opt-in `[data-debug-stagger]` mode materializes it (an
> `::after` badge per child with its index, via a counter mirroring
> `:nth-child - 1`).
---
## 3. Motion across the 4 layers
| Layer | What it contributes | Moment |
|---|---|---|
| **Morfo** | Declares the attrs: `data-state` + states (`open`/`closed`), the **events** (`emerge`/`commit`/`signal` + `sequence`/`persistence`/`intent`), `data-side`/`data-align`, `data-starting/ending-style`. | both |
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **Sema** | Stamps `data-event-*` (`family`/`intent`/`direction`/`phase`/`id`) during the `hold`; resolves the signature (sound/haptic are runtime channels; **`motion`/`color`/`presence` are materialized by eidos** reading `data-event-*`). | `--event` |
| **Soma** | Writes `data-state` via effects; `Presence` keeps the node during the exit and awaits the animation (`getAnimations()` + `Promise.all(finished)`). Fires the event with its `sequence`. | `--state` (+ fires the event) |
| **Eidos** | `keyframes` + `signatures` (event-moment, over `data-event-*`) + `presets` (state-moment, over `data-state`) + **CSS generation**; delegates the JS engine to **`uix.motion`** (the service) and registers its `css` presets there at boot. **Reads both axes and animates.** | both |
Reduced-motion is already wired: `MotionEffective = 'allow' | 'reduce'`
feat(motion)!: una fuente de motion, y los cuatro ejes visuales SIEMPRE en prefs — eidos no lee medios, nadie en el árbol uix pregunta al SO, ningún esquema del app deja a eidos sin motor (P2 #5) La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas: A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion: reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52 selectores `[data-motion='reduce']` («with or without the JS projection»). Dos fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a `[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones, 0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus contra un `style:transition` inline; events.css empate (0,3,0) con las firmas generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 / 5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una veintena de variantes más específicas que ganaban al media hoy pierden — y bajo la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion` tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events, float-panel). B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena (`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion` (el media en crudo). El puerto correcto YA EXISTÍA sin consumidores: `MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`, precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic` (`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos `MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface `HapticChannelDom` entero (un puerto que contesta política es la puerta por donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo; standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS motores de escena por superficie DOM (aura-indicator, pack Ambient) que se habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO — reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la fuente en el montaje (paridad con la lectura del media que sustituye). `ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta el ENTORNO de prefs y nada más. Soma (adenda firmada «a en B»): el último lector de política que preguntaba al SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒ `channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como `dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin `motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs» (standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96 sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma` CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit` real; el tipo hizo su trabajo en el código de producción y era ciego justo donde se suponía que bastaba). Test real nuevo `soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()` lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales; mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba `requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97 (`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62. C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs site: texto invisible en oscuro). Cuatro layouts congelados componen su propio esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a `mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma: `uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }` — el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks, onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo para attach con prefs ajenas; docs de attach: el app compone `uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒ `data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2 rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto. `web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29): muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura `mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador: SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta `oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper, tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve `light` y la hidratación corrige (previo). Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0 discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests (motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 · soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados, 93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 + la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial (la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos — check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK, arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus independientes por lote (informes en el handoff). Constructores + adversariales Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes») era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados tras releer su diff entero (ley de la casa). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
(`libs/motion`), resolved ONCE by prefs (`resolveMotion` folds the OS hint
under the user's intent), projected as `data-motion` by
`ActivePrefsDomProjection`, and handed to every JS runtime as a `MotionSource`
(`createMotionSourceFromPrefs`). The OS hint itself is tracked by
`ReducedMotionTracker` (`arts/adom`) and exposed on
`ActiveDom.prefersReducedMotion.matches` — it feeds the prefs ENVIRONMENT, and
nothing else reads it to decide.
---
## 4. Architecture — the two-surface registry
```
EidosConfig.motion
├── keyframes: { 'fade-in': {...}, 'scale-in': {...}, ... } registered @keyframes
│
├── signatures: { ← the --event MOMENT (the signature, generic per event)
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
│ 'emerge-present': { family:'emerge', event:'emerge-present', keyframes:['fade-in'], ... },
│ 'commit-settle': { family:'commit', keyframes:['settle'], ... },
│ 'announce-threat':{ family:'signal', intent:'threat', keyframes:['pulse'], ... }
│ } → generates [data-event-*][data-event-phase='active'] rules
│
└── presets: { ← the --state MOMENT (per-component transition)
'scale-fade': { driver:'css', enter:{...}, exit:{...} },
'slide-fade': { driver:'css', enter:{ bySide }, exit:{ bySide } },
'genie': { driver:'rect', enter, exit } (P3, JS)
} → generates [data-animation-style][data-state] rules
│
▼
uix.motion : EngineMotion (service · arts/motion — resolves, runs JS drivers, honors reduce)
▲ eidos.motion delegates here + registers the css presets at boot
│
▼
Eidos reads data-event-* (signature) + data-state (transition) → animates
```
- **`signatures`** is the generic signature: a `commit` settles the same way
system-wide; an `emerge.present` enters the same way. Sema's packs
(`sema/components/*.ts`) fine-tune a concrete component's signature. It is
what `events.css` used to do by hand (§15).
- **`presets`** is the per-component transition, chosen with the `motion`
prop.
- Theme/app can add or override in both maps.
---
## 5. The types
`$motion` (`src/arts/motion/types.ts`) — relocated to an *art*.
`duration`/`ease` are `string` (decoupled from `DurationKey`/`EaseKey`:
eidos resolves the tokens in its generation layer; the art doesn't know the
scale):
```ts
type ReducePolicy = 'instant' | 'opacity-only' | 'none'
type MotionSide = 'top' | 'right' | 'bottom' | 'left'
type KeyframeName = string // key into motion.keyframes
type MotionPresetName = string // the `motion` prop's value; 'none' disables
// One CSS phase: composed keyframes + tokens + side-awareness.
interface CssPhase {
keyframes: KeyframeName | KeyframeName[] // comma-composed: ['scale-in','fade-in']
duration?: string // token key ('moderate'…) or raw ('600ms')
ease?: string
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
transformOrigin?: string // e.g. 'var(--_floating-transform-origin)'
bySide?: Partial<Record<MotionSide, KeyframeName | KeyframeName[]>>
}
// ── The --event moment: the perceptual signature (generic per event) ──
interface EventSignature {
family?: string // data-event-family ('emerge' | 'commit' | 'signal' | …)
intent?: string // data-event-intent (valenced families)
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
event?: string | string[] // data-event name(s)/prefix(es) (['emerge-present','emerge-open'], …)
direction?: string // data-event-direction ('forward' | 'backward') — sense of travel,
// decided per emit. A REFINEMENT, never a matcher on its own.
keyframes: KeyframeName | KeyframeName[]
duration?: string // token key OR raw hold ('600ms', outside the scale)
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
ease?: EaseKey
fill?: 'none' | 'forwards' | 'backwards' | 'both'
reduce?: ReducePolicy
}
// ── The --state moment: per-component preset (the `motion` prop) ──
interface CssStatePreset {
driver: 'css'
enter?: CssPhase // [data-state='open']
exit?: CssPhase // [data-state='closed']
reduce?: ReducePolicy
}
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
interface JsStatePreset { // implemented (waapi/spring/rect/svelte)
driver: 'waapi' | 'spring' | 'rect' | 'svelte'
enter?: MotionRun; exit?: MotionRun
requires?: ('sourceRect' | 'targetRect' | 'placement')[]
reduce?: ReducePolicy
fallback?: CssStatePreset // declared; NOT auto-applied today
// (the spring driver honors ctx.reduced itself)
}
type StatePreset = CssStatePreset | JsStatePreset
// ── The registry ──
interface MotionConfig {
keyframes?: Record<KeyframeName, KeyframeStops>
signatures?: Record<string, EventSignature> // the --event moment
presets?: Record<string, StatePreset> // the --state moment
}
```
Structural decision: **the event-moment is generic per event**
(`signatures`, by `family`/`intent`), not packaged per preset — because the
signature (§11) is defined per event and must be consistent across
components. Per-component fine-tuning of the event-moment goes in **sema's
packs**. The `motion` prop only chooses the **state preset**.
---
## 6. The `EngineMotion` API
Lives in `$motion` (the `uix.motion` service; `eidos.motion` and
`soma.motion` expose it — the same instance). Registration + the declarative
CSS path + **JS driver execution**. The caller passes `MotionRunOptions`
(`dom`, `reduced`, `side`, `sourceRect`/`targetRect`, `duration`/`ease`) for
the drivers that need them.
```ts
interface EngineMotion {
register(name: string, preset: StatePreset): void
resolve(name: string): StatePreset | undefined
has(name: string): boolean
list(): string[]
// css: declarative (settled handle); js: builds the MotionContext, runs the
// driver, normalizes the return into a handle and tracks it per element.
enter(el: HTMLElement, name: string, opts?: MotionRunOptions): MotionHandle
exit(el: HTMLElement, name: string, opts?: MotionRunOptions): MotionHandle
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's bridge (replaces the old `runner`): reads the node's
// `data-animation-style` and delegates to enter/exit (css → settled handle;
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
// js → runs the driver).
run(el: HTMLElement, phase: 'enter' | 'exit', opts?: MotionRunOptions): MotionHandle
cancel(el: HTMLElement): void // cancels active JS handles
pending(el: HTMLElement): Promise<void> // combined finished (for Presence)
dispose(): void // cancels everything (service cleanup)
}
```
- A **`css` state preset** is declarative: the wrapper sets
`data-animation-style` and the `[data-animation-style][data-state]` CSS
rule + soma's `Presence` do the rest. The runtime doesn't "run" it.
- A **JS state preset** runs the driver (`spring`/`waapi`/`rect`/`svelte`),
normalizes the return (`Animation` | `Animation[]` | `MotionHandle`) into
one handle, tracks it per element (`cancel`/`pending`), and honors the
`ReducePolicy`.
- The **event-moment** (signatures) does not go through `enter/exit`: it is
generated CSS reacting to `data-event-*`.
### 6.1 — Cleanup policy (WAAPI / spring)
`fill: forwards` leaves `Animation`s dangling in `getAnimations()` → it
would break `Presence`'s exit wait. Discipline: rest states live in the
`[data-state]` CSS; `waapi` presets animate **without `fill: forwards`** and
the runtime `cancel()`s on finish; for rest states not expressible in CSS
(measured FLIP), `commitStyles()` + `cancel()`.
---
## 7. The drivers
They apply to **either moment** (a signature or a state preset).
| Driver | Substrate | For | Beyond CSS |
|---|---|---|---|
| **`css`** | `@keyframes` + tokens (generated) | fade / scale / slide / collapse / the signature | — (the Chakra floor) |
| **`waapi`** | `el.animate()` | runtime-computed keyframes | measured distance/size, cancelable |
| **`spring`** | a semi-implicit Euler integrator (RAF via `ActiveDom`) | overshoot/settle, drag, snap-back | real physics — what no cubic-bezier expresses |
| **`rect`** | rect measurement (via `ActiveDom`) → WAAPI | FLIP, shared-element, genie | animating between two real positions |
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
| **`svelte`** (opt-in) | `transition:` / `animate:flip` | `{#each}` list reorders | Svelte's declarative |
**Bundle**: `css` (generated) is the floor. `spring` is a **self-contained**
integrator (no dependency — one independent spring per property, stepped by
`ctx.dom.requestFrame`); `waapi` wraps `el.animate` (a browser API, already
visible to `getAnimations()`); `rect` (FLIP) composes measurement + WAAPI.
The helpers live in `lib/motion/presets/js.ts` (`spring()` / `waapi()` /
`rect()`). `svelte` (opt-in) is reserved for `animate:flip` in `{#each}`. An
external adapter (`motion-one`) is opt-in.
**The `svelte` nuance**: `{#if}` transitions would fight soma's `Presence`;
the core path is WAAPI/spring (composes with `getAnimations`). `svelte` is
reserved for `animate:flip` in `{#each}` (Svelte owns that lifecycle).
**`rect` batching**: FLIP with N elements does its pass in two phases over a
single `requestFrame` (measure all sources → mutate → measure all targets →
animate). `ActiveDom` already provides `requestFrame`; the batching belongs
to the driver, not to `ActiveDom`.
---
## 8. The DOM contract
**The `--event` moment** (sema writes it during the hold; eidos reacts):
```css
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
[data-event='emerge-present'][data-event-phase='active'] { animation: fade-in …; }
[data-event-family='commit'][data-event-phase='active'] { animation: settle …; }
[data-event-family='signal'][data-event-intent='threat'][data-event-phase='active'] { animation: pulse …; }
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
[data-event-family='shift'][data-event-direction='forward'][data-event-phase='active'] { animation: shift-cross-forward …; }
```
feat(morfo,sema,eidos): todo evento dice de que familia es, y el cruce por fin se ve El framework llamaba a la misma cosa de dos maneras: `open` pelado en ocho componentes y `emerge-open` en tres. No era estetica — un preset de movimiento engancha el nombre con `^=`, asi que el dialecto pelado no casaba con ninguna firma y simplemente no animaba, sin romper una sola prueba. Los 40 nombres sin prefijo pasan a `{familia}-{verbo}[-{matiz}]`: 256 eventos, 256 con prefijo, 0 ambiguos. El plan decia 36 y decia `handle-drag-start`; eran 40, y el canon (c25) dice que esos verbos son `pick` y `drop` — `handle-pick` y `handle-drop` ya existian en 10 y 4 componentes. `validateMorfo` cierra la puerta: un `events[].name` que no empiece por su familia ahora lanza. Visto fallar antes con un nombre pelado inyectado. Lo que el renombrado destapo, y va aqui tambien: - La receta del splitter enganchaba `commit-resize`, muerto desde `bd2e40366`. No casaba desde mayo y nadie chillo. Reescrita por FAMILIA, como slider y knob, y `eidos-lint` valida ahora el VALOR de `data-event*` contra el catalogo de morfos — el guard que lo habria cazado en su dia. - La familia `shift` era muda en el canal visual, contra su propia doctrina (c27: el cruce debe percibirse; c34 tipifica el «shift invisible»). Su mapa ya describia la firma que le faltaba y su sonido por defecto es `slide`. Ahora tiene firma direccional: sexto atributo del sello (`data-event-direction`, `forward`|`backward`, por emision) y deslizamiento de 320ms RTL-safe por `:dir()`. Medido: LTR -30px/+30px, RTL los invierte. - El sello de `shift-navigate` pasa del BOTON al `grid` en los cuatro calendarios. Medido: el boton recibia `contact-activate` y 8,5 ms despues —media trama— el `shift-navigate` pisaba la misma ranura y el `press-squeeze` moria sin pintar un fotograma. Una superficie, una ranura (A-36). - 101 contradicciones docs<->morfo adjudicadas con evidencia (git log, docs de decision, el componente vivo). Las docs desfasadas, corregidas; los nueve DEFECTOS de codigo obsoleto quedan abiertos y sin tocar. - `SoundDirection` -> `SoundContour`: era un contorno de tono, no un sentido, y habia tres cosas distintas deletreadas «direction». check en su linea base con 0 errores nuevos por diferencia de conjuntos · docs:check 0/0 · eidos-lint invalid 0 · el censo y las escenas de navegador medidas con raton real y rAF vivo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
`data-event-direction` (`forward` | `backward`) is the sixth attr of the stamp
and the only one decided PER EMIT rather than declared in the morfo: the event
name already says `shift-enter-mode` vs `shift-exit-mode`, so the attr carries
only the sense of travel. The `shift` crossing is the first firma to read it —
its keyframes travel the inline axis via `--motion-shift-sign` (+1 `:dir(ltr)` /
−1 `:dir(rtl)`), never a physical `translateX`.
**The `--state` moment** (soma writes it; eidos reacts). The wrapper sets
`data-animation-style` (the `motion` prop); the rest state comes from the
recipe over the same `data-state`:
```css
[data-animation-style='scale-fade'][data-state='open'] { animation: scale-in, fade-in …; }
[data-animation-style='scale-fade'][data-state='closed'] { animation: scale-out, fade-out …; }
[data-animation-style='slide-fade'][data-side='top'][data-state='open'] { animation: slide-from-bottom, fade-in …; }
```
Public prop: `motion?: MotionPresetName | 'none'` (per-component default;
`'none'` disables). Placement-aware: it reads `data-side`/`data-align`
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
(morfo) + the `--_floating-transform-origin` alias each floating host's recipe
declares from its own `--_{c}-floating-transform-origin` channel (§15).
**NOT `data-motion`**: that name already has two owners (the
`allow`/`reduce` pref + NavMenu's direction). The engine's attr is
`data-animation-style` (mirroring Chakra's `animationStyle`). See §17.
---
## 9. Soma integration (`Presence`)
`soma/layers/presence.svelte.ts` already does the lifecycle: on close it
keeps the node, awaits `node.getAnimations()` + `Promise.all(finished)`
(`runId` guard, `enabled` flag), and unmounts. CSS animations (both moments)
and `el.animate()` ones (JS drivers) appear in `getAnimations()` → **they
are awaited with no new plumbing**.
**`Presence.motion`** (F4 · the service refactor) — the hook for drivers NOT
in `getAnimations()` (the `spring`, pure RAF): `Presence` receives
`motion: EngineMotion` and calls **`motion.run(node, phase)`** on
enter/exit; `run` starts the JS animation and returns a `MotionHandle`,
whose `finished` `Presence` awaits ALONGSIDE `getAnimations()`. For a `css`
preset (or a node without `data-animation-style`), `run` returns an
already-settled handle → the declarative path is unchanged. Soma **does NOT
import eidos**: it consumes the service via `soma.motion` (= `uix.motion`),
the same instance as `eidos.motion`. Wired in Dialog's provider
(`contentPresence`/`overlayPresence` receive `motion: this.soma.motion`);
the same pattern fits any overlay — just pass `motion` to its `Presence`,
**no prop and no coupling** (the old `DialogProps.runMotion` /
`eidos.motionRunner` disappeared in the refactor).
**Child orchestration** (pending, being redesigned): it hangs off the
event's signature (sema) materialized by eidos, not off a parallel engine —
see Appendix D of
[`MOTION_SERVICE_RFC.md`](../../src/uix/eidos/MOTION_SERVICE_RFC.md).
### 9.1 — SSR / hydration
Overlays mount on open, client-side; motion fires on transitions, not on
first mount (a mounted-open element suppresses the enter). No in-flight
SSR→JS handoff; the CSS `fallback` covers no-JS.
### 9.2 — Exit + a11y: `inert` (soma's)
During the exit the node stays mounted: it must be `inert`/`aria-hidden` and
unfocusable. The morfo's `focus.return` already returns focus; `inert`
during the exit window is a `Presence`/`Dismissal` improvement in soma (the
pattern is already used in `color-field`).
---
## 10. Reduced motion
feat(motion)!: una fuente de motion, y los cuatro ejes visuales SIEMPRE en prefs — eidos no lee medios, nadie en el árbol uix pregunta al SO, ningún esquema del app deja a eidos sin motor (P2 #5) La preferencia efectiva de motion la decide prefs UNA vez (`resolveMotion`: la intención `allow|reduce` gana al hint del SO, `system` deriva) y la proyección la estampa en `<html data-motion>` — desde e3c0899dd antes del primer pintado. Aun así el árbol la re-derivaba por su cuenta leyendo el SO en crudo en dos capas: A) CSS de eidos (§61 del changelog): 65 at-rules `@media (prefers-reduced-motion: reduce)` en 63 ficheros + 29 emitidas por el generador, conviviendo con 14 + 52 selectores `[data-motion='reduce']` («with or without the JS projection»). Dos fuentes ⇒ un `allow` explícito no llegaba al CSS. Cada bloque pasa a `[data-motion='reduce'] SEL` con la MISMA declaración y orden (paridad a máquina: 63 ficheros, 65 bloques, 89 reglas, 178 pares selector-declaraciones, 0 discrepancias); los 3 ficheros con gemelo idéntico se unifican; ningún bloque usaba `:root`. Los 4 `!important` dentro de bloques migrados QUEDAN con su rival nombrado (navigation-menu ×2 contra el swap direccional (0,3,0); text-focus contra un `style:transition` inline; events.css empate (0,3,0) con las firmas generadas en otra hoja). Generador: los 4 emisores dejan de emitir el media, ink marks gana su gemelo; `generated/base.css` 29 → 0 media, `--nombres` 8034 / 5751 únicos INTACTOS. GUARD nuevo `reduced-motion-media.test.ts` (at-rule, no prosa; anti-vacío > 60 ficheros; mordido por mutación). Cambio de veredicto de cascada MEDIDO en Chrome real por el adversarial: el prefijo suma (0,1,0), una veintena de variantes más específicas que ganaban al media hoy pierden — y bajo la media del SO la forma de HEAD NO paraba el anillo del spinner. `data-motion` tiene TRES dueños (reduce · navigation-menu from/to · tabs fade|slide), valores disjuntos, forma ancestro obligatoria. Prosa que afirmaba «both» barrida por la AFIRMACIÓN (tooltip.css, card.css, recipes/base.ts, skin-media-player, events, float-panel). B) JS (§62): el motor de motion (`engine-motion.ts:131`), el motor de escena (`engine-scene.ts:99`), la háptica de sema (`chans/haptic.ts:129`) y OCHO lecturas en componentes de eidos preguntaban a `ActiveDom.prefersReducedMotion` (el media en crudo). El puerto correcto YA EXISTÍA sin consumidores: `MotionSource = Source<MotionEffective>` (`$libs/motion`). La fuente nace en prefs (`createMotionSourceFromPrefs`, `src/arts/prefs/motion-source.ts`, precedente `createLocaleSourceFromPrefs`) y la construyen UNA vez las tres raíces: `createActiveUix`, `attachActiveUix` (fallbacks desde `app.prefs`) y las fábricas `defineEngineMotion` / `defineEngineScene` / `defineEngineSemantic` (`coreDependencies: ['prefs']`, forma de format y langs). MUEREN de los puertos `MotionDom.prefersReducedMotion`, `SceneDom.prefersReducedMotion` y el interface `HapticChannelDom` entero (un puerto que contesta política es la puerta por donde vuelve el defecto); `MotionRunOptions.reduced` se queda como PIN por ejecución. `ActiveEidos.reducedMotion` con dos puertas: con prefs el efectivo; standalone sigue al SO con acta (sin primer motor no hay segundo, §60). Los ocho sitios de componentes leen `eidos.reducedMotion`. Hallazgo del lote: DOS motores de escena por superficie DOM (aura-indicator, pack Ambient) que se habrían quedado ciegos a la policy `reduce` obligatoria (P-1) EN SILENCIO — reciben la fuente vía `eidos.reducedMotion`. Deuda nombrada: escena lee la fuente en el montaje (paridad con la lectura del media que sustituye). `ActiveDom.prefersReducedMotion` no se toca: es un hecho del SO que alimenta el ENTORNO de prefs y nada más. Soma (adenda firmada «a en B»): el último lector de política que preguntaba al SO, `soma/runtime.svelte.ts:1280` (migración a11y del morfo: `'state'` ⇒ `channels: []` por S5, `'text'` ⇒ región viva, `'focus'` ⇒ foco), lee la MISMA fuente: `SomaRuntimeBaseSources.motion: MotionSource` OBLIGATORIO como `dom` (un runtime que no puede responder «¿reducido?» no puede honrar S5 — lo garantiza el tipo), llenado por `Soma.runtime()` desde `uix.prefs` (esquema sin `motion` ⇒ permitir, como los motores). La decisión mayor (b) — que el MOTOR aplique `'state'` en su pasada de reducción y que un `emit()` directo reciba tratamiento a11y — queda ABIERTA como fila §3.9 de CONTINUE-sema-audit.md, a ejecutar junto a D-full. `ActiveEidos.reducedMotion` distingue «sin prefs» (standalone ⇒ SO, §60) de «prefs sin ranura motion» (⇒ permitir, como los motores): las dos mitades de una UI ya no discrepan bajo un esquema sin la dimensión. Hallazgo de la adenda: la bolsa `sources` se construye en 96 sitios (3 de producción — menu-dial, metrics, onion-menu — que reciben la fuente vía `eidos.reducedMotion`, y 93 harnesses cuyo `as unknown as Soma` CEGABA la comprobación de miembros: 60 tests rojos hasta declarar el `Omit` real; el tipo hizo su trabajo en el código de producción y era ciego justo donde se suponía que bastaba). Test real nuevo `soma/test/reduced-motion-source.svelte.test.ts` (chromium: `Soma.create()` lee contexto Svelte) con `createActiveUix` + `EngineSemantic` reales; mutación (el trigger vuelve al dom) ROJA 3/3. Adversarial dirigido de la adenda: siete defectos cerrados — `src/uix/contracts.ts` declaraba `requires: ['dom']` para la bolsa de soma y el test no lo asertaba (un guard que no inspecciona nada pasa) → `['dom', 'motion']` + aserto; la bolsa #97 (`bag-census.test.ts`) sin `motion` bajo un casteo; el camino `'text'` —el ÚNICO que declaran 9 morfos de producción— sin test (añadido); dos snippets de docs que ya no compilaban (component-guide A1, morfo.md); cifras del §62. C) La raíz GARANTIZA los cuatro ejes visuales (cazado por el autor en el docs site: texto invisible en oscuro). Cuatro layouts congelados componen su propio esquema de prefs SIN `mode/theme/density/scaling`; un esquema del app SUSTITUÍA al de la raíz entero y eidos «degradaba defensivamente» a `mode = 'light'` CONSTANTE ignorando el SO, mientras el shell estampaba su wrapper en oscuro: tinta de tema claro sobre superficies oscuras. Forma: `uixVisualPrefsDimensions()` (pura, en `prefs-schema.ts`; el boot compila la misma) y `createActiveUix` fusiona SIEMPRE `{ ...ejesVisuales, ...esquemaDelApp }` — el app puede REDEFINIR un eje, nunca omitirlo; sin un solo cast. Eidos deja de degradar en silencio: `createPrefsPreferenceSource(prefs, fallbacks, onMissing)` avisa por `uix.logger.warn` por cada ranura ausente (queda solo para attach con prefs ajenas; docs de attach: el app compone `uixVisualPrefsDimensions()`). Tests reales (esquema sin ejes + SO oscuro ⇒ `data-mode="dark"`; app que redefine `theme` gana; prefs ajenas ⇒ 4 avisos por el logger REAL); el viejo test «degrades to the fallbacks» estaba verde POR COINCIDENCIA (defaults = fallbacks) y se sustituye; mutación (sin fusión) 2 rojos y restaurada. Boot regenerado (15134 B), delta cero ×5 intacto. `web/routes/uix/+layout@.svelte` (descongelado por orden del autor, +14/−29): muere el `modeSource` muerto y el `$state` local; el shell LEE la ranura `mode` y el toggle escribe `uix.prefs.setIntent('mode', …)`; migración única de `uix-docs-theme` al sobre de prefs (clave borrada). Ledger 95 → 93 (la entrada de ese fichero desaparece). VERIFICADO EN CHROME por el coordinador: SO oscuro sin clave ⇒ `<html data-mode="dark">`, wrapper dark, tinta `oklch(0.95)`; clave vieja `dark` ⇒ intent `dark`; toggle mueve html, wrapper, tinta e intent en los dos sentidos. Nombrado, no arreglado: el boot compila siempre el esquema por defecto (un app que REDEFINE un eje resuelve distinto que el boot; hoy nadie en web/ usa el boot) · los otros tres layouts congelados recuperan los ejes pero su wrapper sigue en `$state` local · SSR del docs sirve `light` y la hidratación corrige (previo). Verificación: A — vitest eidos+value-channels 43/483, eidos:lint 0, paridad 0 discrepancias, mutación del guard 3/3 y 4/4 (adversarial), prettier solo avisos preexistentes; B — vitest del scope 90/990 y, con la adenda, 242/2395; +26 tests (motor 5 · escena 2 · háptica 2 · raíz real jsdom 7 · ActiveEidos 7 · fábrica 1 · soma 3 — cifras verificadas una a una por el adversarial), 11 dobles re-firmados, 93 harnesses de soma tipados con el `Omit` real, cuatro mutaciones ROJAS (3/2/3 + la de soma 3/3) y restauradas por sha256, más la mutación de TIPO del adversarial (la fuente devuelve la intención ⇒ 2 errores nuevos en src/: el puerto es gate de tipo, no prosa); C — vitest active-uix+eidos+prefs 57/591, boot 8/8 con delta cero, mutación 2 rojos, verificación en Chrome por el coordinador; todos — check src/ 0 (ledger 95 → 93: MENGUA), check:gate OK, docs:check OK, arts:check OK, packs:check OK. Suite completa 457/5336 verde (adversarial A). Adversariales Opus independientes por lote (informes en el handoff). Constructores + adversariales Opus 5; la sesión coordina. ⚠ Lección: la cuenta del brief de A («94 en fuentes») era un fallo de medida del coordinador (`grep -rh | grep -v generated` filtra LÍNEAS con esa palabra, no el directorio); el constructor la re-midió porque el brief lo exigía. ⚠ Dos constructores cortados por límite de sesión y reanudados tras releer su diff entero (ley de la casa). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 weeks ago
- **CSS** (both moments): `[data-motion='reduce'] …` rules per policy
(`instant` → `animation: none`; `opacity-only` → fade only; `none` →
untouched) — and **only** those. Eidos does not read media: the effective
preference is `prefs`' (`resolveMotion` folds the OS hint, and an explicit
`allow` overrules it) and arrives stamped on `<html>`. A `@media
(prefers-reduced-motion)` in a recipe would be a second source, still
suppressing the motion of a user who asked to keep it. Guarded by
`src/uix/eidos/reduced-motion-media.test.ts`.
- **Runtime (JS drivers)**: `MotionContext.reduced` from the EFFECTIVE
preference — a `MotionSource` over `prefs.motion`
(`createMotionSourceFromPrefs`), injected into `EngineMotion` / `EngineScene`
/ sema's haptic channel by the composition root. The same ONE source as the
CSS above: the engines no longer read `ActiveDom.prefersReducedMotion`, which
is the OS hint, not the answer. `MotionRunOptions.reduced` stays as a per-run
PIN and wins over the source. The `ReducePolicy` applies before running.
- Morfo declares `a11ySemantic.reducedMotionFallback` per event; soma
silences the signal when it applies. The engine is coherent with that
decision.
---
## 11. Primitives and keyframes
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
Existing tokens (`STATIC_MOTION`, generated, read by css and js):
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
| Token | Values |
|---|---|
| `--duration-{k}` | `instant 0` · `fast 120` · `normal 180` · `moderate 240` · `slow 320` ms |
| `--ease-{k}` | `default` · `out` · `in` · `spring` · `alert` · `symmetric` |
| `--motion-distance-{k}` | `xs 2` · `sm 4` · `md 8` · `lg 16` px |
| `--motion-scale-{k}` | `enter 0.985` · `press 0.97` · `through 0.92` (fade-through) · `lift 1.02` (drag pickup — the only `>1`, see §Draggable surfaces in THEMING) |
feat(eidos): el ritmo de revelado sube a config — primitives.motion.staggerViewport, y su regla emigra a la foundation Firma opción 1 del autor (cableado EidosConfig.motion → CSS, la última entrada de PENDING_PRIVATE_RENAME). Ejecutada con el esquema completo: expediente + dos constructores en serie + adversarial, todos Opus en ultra, coordinación y supervisión de la sesión. El gancho --motion-stagger-each-default era FICCIÓN: nadie lo declaraba y su único consumo (motion.css:42) resolvía siempre al literal 70ms. Ahora: primitives.motion.staggerViewport: '70ms' (tipo + default + validación + contrato + emisión --motion-stagger-viewport en :root, molde floating-gap verbatim) y la regla [data-stagger] > [data-animation-trigger='viewport'] EMIGRA a renderMotionBlocks — donde vive la máquina entera del stagger. Con ello motion.css queda en sus dos gates de opacity (su veredicto structural, más verdadero), la entrada del registro cayó STALE POR MECÁNICA y el registro, a cero, se DESMONTA con acta (un guard que inspecciona el vacío es un falso verde); la historia completa cierra en canon/recipe-contract.md. E14 (70→20ms) queda APLAZADA con nombre: firma de diseño propia, mueve 8 blocks. Adversarial 9/9 CONFIRMADO: neutralidad término a término contra la línea base histórica en Chrome real con el IntersectionObserver disparando (0/0.07/0.14/0.21/0.28/0.35s en /blocks/team/preview) - override 999ms reescribe la cascada - nombre muerto inerte (5s en :root, cero efecto) - <Cascade> intacto con control positivo quirúrgico (solo el hijo estampado toma el token) - cero empates de hoja posibles (tres escritores en .css, especificidad decide) - el guard del espacio cerrado muerde por mutación - 126 tests, censo 73%/66/1152-0-0, docs 0/0. Sus reservas, con acta en changelog §57: el contrato gana DOS filas (--motion-stagger-each entra como (derived) al emitirse — clase preexistente de index/index-rev) - gates divergentes latentes - path del contrato sin pin. Correcciones del adversarial incluidas: dos citas muertas del ledger de blocks anotadas, el presente falso de component-audit.ts, y el absoluto de canon matizado. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| `--motion-stagger` | `20ms` — the stagger STEP a menu ripples at (`dropdown-menu`, `context-menu` set `--motion-stagger-each` from it) |
| `--motion-stagger-viewport` | `70ms` — the reveal rhythm of a viewport animator inside `[data-stagger]`; the foundation writes it into `--motion-stagger-each` on that selector alone (changelog §57) |
**Override vars (runtime, NOT theme tokens)** — a component sets them on its
element; the preset reads them with a token fallback:
| Var | For | Notes |
|---|---|---|
| `--motion-duration-{enter,exit}` | per-component timing (F3) | `@property inherits:false` |
| `--motion-ease-{enter,exit}` | per-component curve (F3) | `@property inherits:false` |
| `--motion-slide-leave` | `slide-full`'s extra travel for inset panels (drawer) | default `0px` |
| `--height` / `--collapsed-height` | the `collapse`'s measured height (soma writes it) | — |
feat(eidos): el ritmo de revelado sube a config — primitives.motion.staggerViewport, y su regla emigra a la foundation Firma opción 1 del autor (cableado EidosConfig.motion → CSS, la última entrada de PENDING_PRIVATE_RENAME). Ejecutada con el esquema completo: expediente + dos constructores en serie + adversarial, todos Opus en ultra, coordinación y supervisión de la sesión. El gancho --motion-stagger-each-default era FICCIÓN: nadie lo declaraba y su único consumo (motion.css:42) resolvía siempre al literal 70ms. Ahora: primitives.motion.staggerViewport: '70ms' (tipo + default + validación + contrato + emisión --motion-stagger-viewport en :root, molde floating-gap verbatim) y la regla [data-stagger] > [data-animation-trigger='viewport'] EMIGRA a renderMotionBlocks — donde vive la máquina entera del stagger. Con ello motion.css queda en sus dos gates de opacity (su veredicto structural, más verdadero), la entrada del registro cayó STALE POR MECÁNICA y el registro, a cero, se DESMONTA con acta (un guard que inspecciona el vacío es un falso verde); la historia completa cierra en canon/recipe-contract.md. E14 (70→20ms) queda APLAZADA con nombre: firma de diseño propia, mueve 8 blocks. Adversarial 9/9 CONFIRMADO: neutralidad término a término contra la línea base histórica en Chrome real con el IntersectionObserver disparando (0/0.07/0.14/0.21/0.28/0.35s en /blocks/team/preview) - override 999ms reescribe la cascada - nombre muerto inerte (5s en :root, cero efecto) - <Cascade> intacto con control positivo quirúrgico (solo el hijo estampado toma el token) - cero empates de hoja posibles (tres escritores en .css, especificidad decide) - el guard del espacio cerrado muerde por mutación - 126 tests, censo 73%/66/1152-0-0, docs 0/0. Sus reservas, con acta en changelog §57: el contrato gana DOS filas (--motion-stagger-each entra como (derived) al emitirse — clase preexistente de index/index-rev) - gates divergentes latentes - path del contrato sin pin. Correcciones del adversarial incluidas: dos citas muertas del ledger de blocks anotadas, el presente falso de component-audit.ts, y el absoluto de canon matizado. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| `--motion-stagger-each` | the stagger rhythm (on the container, inherits) | default `0ms` → **parallel**; N → cascade. One exception, and it is foundation, not a component: `[data-stagger] > [data-animation-trigger='viewport']` gets `var(--motion-stagger-viewport)` (the theme token above), so a section reveals on rhythm with no per-item delay. `<Cascade>` keeps its `0` |
| `--motion-stagger-index` / `-index-rev` | the per-item index **from structure** — `[data-stagger] > *:nth-child` (fwd, enter) / `:nth-last-child` (rev, exit); generated in `render-css.ts`, **nobody writes it** | `@property inherits:false`, `<integer>` |
Keyframes (`EidosConfig.motion.keyframes`, parity with Chakra's
`theme.keyframes`): individual `translate`/`scale` (they compose without
stomping each other); parameterized by CSS var for dynamic sizes/distances
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
(`var(--height)`, `var(--collapsed-height,0)`). Built-in: `fade-in/out`,
`scale-in/out`, `slide-from/to-{side}[-full]`, `expand/collapse-height`; the
**signature** (`announce-pulse-*`, `commit-settle`, `press-squeeze`,
`dismiss-fade`, `present-rise`); **Material**
(`slide-axis-{x,y}-{in,out}`, `scale-from-92`).
---
## 12. Initial content (signatures + presets)
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
**`presets` (the `--state` moment)** — built-in (`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
| Name | Notes |
|---|---|
refactor(soma)!: §15 — la geometría del posicionador es canal del ANFITRIÓN Firma A del autor (§14 literal, cuarta aplicación de «la capa es la pluma, no la dueña»): FloatingContentOpts gana `component` (kebab del morfo, nunca literal — 11 hilos de creación en 9 anfitriones) y las medidas por instancia del posicionador aterrizan como `--_{host}-floating-{transform-origin, available-width,available-height,anchor-width,anchor-height}`. El sexto nombre que el expediente no contaba (`--floating-native-offset`, rama nativa) queda canal INTERNO de capa (`--_floating-native-offset`: lo escribe la capa en su propio wrapper, lo leen solo sus reglas foundation). El lector COMPARTIDO (preset scale-fade de motion) lee el alias neutro `--_floating-transform-origin` que cada receta anfitriona declara desde su canal — cableado sliding-indicator, 9 declaraciones. getFloatingContentCSSVars (export muerto, next-features §13.ii) MUERE con la firma. Matiz que la ejecución destapó: el content de MENUBAR compone DropdownMenu — su canal es --_dropdown-menu-floating-* y el alias de menubar se acota al PANEL (el canal sigue al componente que POSEE la composición flotante); split-button y palabras leen el canal ajeno de su composición en el punto de composición, anotado en cada sitio. Verificado: suites 96+60+78 en verde - eidos-lint x10 con 0 invalid - reach-floor 5/5 (ledger 0 NEW / 0 STALE) - component:audit 162 PASS / 4 NEEDS-WORK idéntico - check 0 atribuibles POR FICHERO - dinámica CDP 9/9 anfitriones: canal vivo con px por instancia, los nombres VIEJOS computan VACÍO en todos, positivo exacto (tooltip max-block-size 525.033px == canal 525.0326px; combobox ídem; panel de menubar anchor-width 886px = la barra; sidebar popout ENGANCHADO 720px/35px), negativo fijando los viejos en :root = nada se mueve. Trampa reconfirmada: el popout SIN ANCLAR da undefinedpx — comprobar que la superficie estaba abierta. Viaja con atribución declarada (acuerdo por canal con la sesión P0 vicen-42, precedente 1cb07c1e1): los tres hunks de fase C en docs/architecture/soma-architecture.md (runtime.part sin syncAttrs, partProps = render bag, el párrafo del imperativo) son suyos. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 month ago
| `fade` · `scale-fade` | enter+exit; scale-fade with the `--_floating-transform-origin` alias (§15) |
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
| `slide-fade` | side-aware (`data-side`): slide + scale 0.985 + fade |
| `slide-full` | edge drawer, inset-aware (`--motion-slide-leave`) |
| `collapse` | measured height (`var(--height)`) |
| `shared-axis-x/y` · `fade-through` | Material 3 (F5) |
The **JS drivers** (`spring`/`waapi`/`rect`, `presets/js.ts`) are registered
per app/demo. The first **named built-in JS** preset is `spring-pop`
(`driver:'spring'`, `presets/js.ts` → `BUILTIN_JS_PRESETS`), registered
directly on `ActiveEidos` — not via the serializable
`EidosConfig.motion.presets`, which cannot carry its `MotionRun` functions
(`structuredClone` fails). Demos additionally register `panel-spring`,
`dialog-spring`, `flip`.
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
**`signatures` (the `--event` moment)** — built-in, migrated from
`events.css` (F2, §15): `present`/`dismiss`, `commit` (+ the
`fulfill`/`affirm`/`threat` intents), `press` (`contact`), `announce`
(+ 5 intents). Generic per `family`/`intent`/`event`.
---
## 13. Per-component defaults
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
The per-component **state-moment** (the `motion` prop, the wrapper's
default):
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
| Component | Part | Default `motion` |
|---|---|---|
| Dialog | overlay / content | `fade` / `scale-fade` |
| Popover · Tooltip · Dropdown · Select · Menubar · ContextMenu | content | `slide-fade` |
| Drawer | content | `slide-full` |
| Accordion · Collapsible | content | `collapse` |
| Toast | item | `slide-fade` |
The **event-moment** is automatic: when the event fires (e.g. `commit`,
`emerge.present`), the corresponding `signatures` entry reacts — no prop.
---
## 14. Where the code lives
```
arts/motion/ ← THE SERVICE (a pure art: no UI or cross-art deps)
types.ts ✓ MotionDom (structural port), MotionConfig, EventSignature, StatePreset, MotionHandle…
engine-motion.ts ✓ createEngineMotion → EngineMotion (register/resolve/enter/exit/run/cancel/pending/dispose)
drivers.ts ✓ JS drivers: spring() (physics) / waapi() / rect() (FLIP)
index.ts ✓ the `$motion` barrel (named re-exports) + README.md
arts/active-app/service-factories/motion.ts ✓ defineEngineMotion (factory; coreDep 'dom')
eidos/lib/motion/presets/css.ts ✓ DATA: keyframes + state presets + signatures (BUILTIN_*) + Material (F5)
eidos/lib/config-types.ts ✓ EidosConfig.motion ($motion's MotionConfig type)
eidos/lib/render-css.ts ✓ GENERATES CSS: presets ([data-state]) + signatures ([data-event-*]) + overrides + stagger
eidos/lib/config.ts ✓ validates presets (css + js) + signatures
eidos/lib/themes/base.ts ✓ built-in DATA (keyframes + signatures + presets)
eidos/active-eidos.svelte.ts ✓ eidos.motion → delegates to uix.motion + registers the css presets at boot
eidos/components/dialog/ ✓ wrapper: motion prop → data-animation-style (no runMotion)
active-uix/active-uix.svelte.ts ✓ uix.motion : EngineMotion (createActiveUix creates it; attach reads it from the app)
soma/core/soma.svelte.ts ✓ soma.motion → uix.motion
soma/layers/presence.svelte.ts ✓ `motion: EngineMotion` opt → motion.run(node, phase) (JS gating) + tests
```
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
(`✓` done. Relocated to `arts/motion` in the service refactor. Pending:
passing `motion` to more overlays (drawer/popover/…); named built-in JS
drivers; F6/F7.)
---
## 15. The event-moment: from `events.css` to `signatures`
`events.css` WAS "the hand-implemented `--event` surface". In **F2 it
migrated to `signatures`**: the 9 keyframes + 12 reactions (announce pulse
per intent, commit settle, dismiss fade, press squeeze, present rise) now
live in `EidosConfig.motion.{keyframes,signatures}` (`presets/css.ts` →
`BUILTIN_SIGNATURES`) and are generated into `generated/base.css` —
theme-extensible. `events.css` was reduced to two global concerns: the
compositor hint (`will-change`) and the reduced-motion cap (the signature's
`motion` is silenced under reduce, but sound + haptic keep communicating —
the cross-modal advantage). `archetypes.css` (4 baseline hover/focus
transitions) stays as the transversal micro-interaction.
---
## 16. Comparison with Chakra UI v3
| Capability | Eidos | Chakra v3 |
|---|---|---|
| The `--state` moment (`data-state` + named presets) | ✅ `presets` + the `motion` prop | ✅ `animationStyle` + `data-state` |
| `Presence` (hold-through-exit) | ✅ soma (`getAnimations`, covers transitions) | ✅ (`animationend`) |
| keyframes registry + tokens | ✅ | ✅ |
| placement-aware | ✅ `data-side` | ✅ `data-placement` |
| **The `--event` moment (perceptual signature)** | ✅ `signatures` + sema (intent/hold/sequence) | ❌ **doesn't exist** |
| **Both moments integrated** | ✅ | ❌ (only `--state`) |
| physics / FLIP / orchestration (JS drivers) | ✅ `spring` / `rect` / `waapi` | ❌ |
The **`--event` moment integrated with the perceptual signature** (and with
sound/haptic) is what no current framework has. Chakra animates the state;
UIX animates the state **and** the occurrence, keeping them distinct.
---
## 17. Naming decisions
- The state-moment attr: **`data-animation-style`** (NOT `data-motion`,
which is reserved for the reduce-motion pref `allow`/`reduce`).
- The event-moment attrs: the `data-event-*` sema already stamps.
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
- **Cleanup (F7)** ✓: NavMenu's directional `data-motion` (a planned rename)
**does not exist** in the code — moot. `data-motion-ref` and the "TSC
`event:*` scope" are discarded (no real use).
---
## 18. Implementation phases
The "beat every framework" roadmap. **F1–F6 implemented:**
- **F1 — The two-moment foundation** ✓: the types (`MotionConfig`
{keyframes, signatures, presets}, `EventSignature`, `StatePreset`) +
generation (both surfaces) + the `EngineMotion` engine (the `uix.motion`
service, relocated to `$motion` in the refactor) + validation + tests.
- **F2 — The cross-modal signature** ✓ (beats SwiftUI): `events.css`
migrated to `signatures` (§15); the signature's `motion` + sound + haptic
come from the SAME event (a coupling SwiftUI's `.sensoryFeedback` leaves
loose).
- **F3 — Polished presence** ✓ (matches Base UI): Dialog/Popover/Drawer/
Accordion migrated to presets; **per-component duration + easing**
overrides (`--motion-duration/ease-{enter,exit}` + `@property
inherits:false`), inset-aware slide (`--motion-slide-leave`), the
measured-height bridge (`--height`). Zero timing regression.
- **F4 — The mechanical JS engine** ✓ (matches Framer): the `spring` (real
physics) / `waapi` / `rect` (FLIP) drivers; `Presence.motion` →
`motion.run(node, phase)` (the engine as the `uix.motion` service,
consumed by soma and eidos **with no coupling**); a real Dialog bouncing.
- **F5 — Choreography** ✓ (matches Material 3): declarative stagger
(`--motion-stagger-{index,each}`), the `shared-axis-x/y` + `fade-through`
presets; container-transform via the `rect` driver.
- **F6 — Token rigor** ✓ (matches Carbon): the raw signature **tokenized** —
feat(audit): component re-audit + fixes P0–P5 (P4 complete, P5 in progress) Re-audit of the whole component catalog at pilot depth (91 fichas + the checkpoint verdicts in docs/audit/components/) and the executed fix packages. - P0–P3: doctrine (sema participation, CANON §3 intent, component-guide §5 + API naming, §32 outline), guards (morfo-vocabulary-check S10/S11d, component-audit A-3.1/A-1.4/S9, eidos-lint classHooks), morfo contracts (role=application removed ×4, aria-selected off the Day, drp translationRef, field data-state prune, pin-input commit-set, media-player renames), 13 new sema packs + 12 morfos family-default → pack. - P4 (N1–N10) COMPLETE: selectionMode (select/combobox/toggle-group/calendar); onValueCommit terminal-callback norm (pin-input/search/password/textarea + date/time/color-field add); typed validation reason + onInvalid (tags-input, css-field); index/onIndexChange (carousel); deselectable; openDelay/ groupSkipDelay; allowCustomValue; defaultValue prune. - P5 (in progress): S1 class-hooks codemod (22 hooks → data-attrs across cropper/button/image-picker/toggle/image-adjustments); S5 outline remnant in field.css; depth pass (data-depth=overlay on tooltip/toast/float-panel); touch-rows (--touch-target token, decoupled ::before hit-slop, area-not-visual, 44 AAA). S6 (Field composition) + S8 (calendar-surface) pending. Verification: errors-outside-(alpha|words|palabras|chronos) == 59 (baseline); morfo-vocabulary-check exit 0; eidos-lint class-hooks/invalid 0 on touched components; per-component vitest suites green. Handoff + remaining plan: docs/process/continue-audit-fixes-2026-07.md Excluded (broken by the N1 rename, left broken per user decision, not staged): words/**, palabras/**, chronos, web/routes/alpha/**. Reconciliation pending: the touch-rows ::before for checkbox/switch reverses changelog §37's earlier pseudo-element rejection (WCAG-overlap) that had routed markers to a labeled-row/Field task — flagged for the user in the handoff. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 months ago
the duration scale gains the long stretch (`slower` 400 · `deliberate` 600 ·
`emphatic` 800 · `sustained` 1000ms). *(Corrected 2026-07-06: F6 originally
set the neutral/affirm/fulfill announces to `deliberate` claiming alignment
"with the book's holds-by-intent" — that aligned with the DRIFTED runtime
table, not the book. Per the book's regions (TABLA 32.2): neutral/affirm =
`moderate` ("breve"), fulfill = `slower` ("breve-media"), and the long end
(`emphatic`/`sustained`) belongs to risk/threat. The hold never truncates a
signature — the visual channel awaits the expression, capped by
`MAX_EXPRESSION_WAIT_MS`; see decisions/book-deviations.md D.12.)*
Shared-axis travels the canonical
`--motion-distance-xl` (30px); fade-through uses `--motion-scale-through`
(0.92); `press`/`commit` snap to the existing scale (sub-perceptible). The
**`emphasized`** easing (M3 emphasized-decelerate). **Productive/
expressive sets** (Carbon): `primitives.motion.expressive` emits a
`[data-motion-set='expressive']` scope remapping the easing — productive
is the default. Distance-scaled duration stays a **pairing convention**
(distance token ↔ duration token), not a runtime formula (no CSS consumer
today — the `rect` driver would do it by measurement if a consumer asks).
- **F7 — Extensibility + typegen** ✓ (beyond everyone): the `--state`
moment's preset names are **type-safe + app-extensible** via an
augmentable registry — `EidosMotionPresets` (mirroring
`SemaChannelSignatures`), with `MotionPresetName = keyof
EidosMotionPresets | 'none' | (string & {})`. An app adds type-safe
presets with `declare module '$uix/eidos' { interface EidosMotionPresets
{ … } }`: the `motion` prop autocompletes them and a typo is a compile
error. The engine (`$motion`/`uix.motion`) stays `string` (open at
runtime) — the registry is compile-time ergonomics over the props; a test
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
pins the built-in set against it. NavMenu's directional `data-motion` no
longer exists (the rename is moot). `transition` with named groups stays
**deferred** (no consumer; `getAnimations({ subtree: true })` would cover
it).
**Roadmap F1–F7 complete.** Every phase kept **zero regression** for anyone
not using `motion` or firing new events.
---
## 19. Out of scope / deferred
- A `motion-one` adapter or another external JS engine (opt-in, never a base
dependency).
- Exotic drivers (complex timelines) — only with a real consumer.
- Widening durations to 7 steps (Chakra-style) — only if 5 falls short.
---
**Revision notes**: 2026-06-21 — closure of the `motion` prop's domain
(§D.12): the **state domain** (the dedicated `data-motion-state` attr + the
`select-pop` emphasis preset, `<Card>` pilot), **content loops**
(`spin`/`pulse`/`ping`/`bounce`, un-gated infinite rules) + the `<Motion>`
exit fix (`tick` no-op), the `[data-debug-stagger]` debug affordance, and
the first **named built-in JS** preset `spring-pop` (the `spring` driver's
debut, registered on `ActiveEidos`; demonstrated on
`Popover.Content motion="spring-pop"`). Loops+lifecycle demo at
`web/routes/demos/motion`. Earlier (2026-06-04): F1–F7 implemented + the
engine relocated to `arts/motion` (the `uix.motion` service).
If the code diverges, the code wins; open an issue. References: the
historical seed `GUIA_IMPLEMENTACION_SEMAUIX.md` (§4.3, §7, §11 — the
event/state/signature model),
[`active-architecture.md`](../architecture/active-architecture.md) §5/§6
(the causal chain, the attrs), THEMING (tokens),
[`soma-architecture.md`](../architecture/soma-architecture.md) (Presence),
[`sema.md`](../architecture/sema.md) (signature/signals),
`arts/adom/README.md` (reduced-motion).

Powered by TurnKey Linux.