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/process/PLAN-background.md

1103 lines
96 KiB

uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
# PLAN — `Background`: el anfitrión de capas de fondo del canon (estudio + plan)
> **Estado: DECISIONES D-BG.1–11 y D-BG.13–15 FIRMADAS 2026-08-17 (D-BG.12
> superada por la 14) — NADA CONSTRUIDO.** Falta que el autor nombre la rama de
> trabajo (§10.1) y ordene el arranque del agente (F0). El estudio original,
> abajo, se conserva íntegro.
>
> ~~Estado: ESTUDIO ENTREGADO 2026-08-17 — NADA CONSTRUIDO, NADA FIRMADO.~~
> Este documento responde a la pregunta «¿conviene un componente `Background`
> rico, composable desde el resto del framework, a la altura o por encima de lo
> que hay hoy (parallax, capas, vídeo, patrones, scrim…)?». Contiene el
> veredicto razonado, la comparativa con las referencias, el inventario de lo
> que YA existe en el framework, la forma canónica propuesta, las decisiones
> que sólo el autor puede firmar (§6, D-BG) y el plan por fases con sus gates
> (§7). Es un documento de proceso: no es doctrina hasta que el autor firma.
>
> **Kickoff para sesión nueva**: _«Lee `docs/process/PLAN-background.md`; si
> las D-BG de §6 están firmadas, ejecuta la fase que toque; si no, PARA y
> preséntalas una a una.»_ Cada fase lista qué leer, qué producir y con qué
> guard se verifica.
>
> **Enmiendas de la sesión 2026-08-17 (conversación con el autor, tras el
> estudio)** — mandan sobre cualquier frase anterior de este documento:
>
> 1. **`Background` NO compone `Box` ni es una caja de flujo.** Es la PILA DE
> CAPAS que el padre renderiza como hijo (`position:absolute; inset:0;
> z-index:-1; border-radius: inherit; overflow: clip` en la propia pila). Un
> solo modo de colocación; el layout sigue siendo de `Box`/`Section`/`Card`
> («one axis, one primitive»). D-BG.12 queda superada por D-BG.14.
> 2. **El padre se convierte en anfitrión sin tocarlo**: una regla de foundation
> `:where(:has(> [data-background])) { position: relative; isolation: isolate }`
> (`:has()` ya se usa 71 veces en eidos; `:where` deja ganar al `position` de
> cualquier recipe). El bloque `data-on` gana el mismo gemelo `:has`.
> 3. **Cero props nuevas en `Box`** (Box es layout-only por doctrina; el
> tratamiento vive en primitivas aparte — Surface es el precedente).
> 4. **Composición, no struct**: el padre que quiera renderizar el fondo expone
> un snippet (`background`) donde el app compone `<Background>…</Background>`;
> un prop `background={struct}` contradice la regla compositional-not-data-driven
> del autor (OnionMenu 2026-06-21), B-5 y la regla 6 de eidos → D-BG.13.
> 5. Ejecución: **un agente Opus 5 construye; Fable supervisa** — brief en §10,
> protocolo de supervisión en §11.
---
## 0. La pregunta y la respuesta corta
**Pregunta** (usuario, 2026-08-17): estudiar el sistema ActiveUIX, su
documentación, filosofía y guías, y evaluar un componente `Background` rico,
composable en el resto de componentes, que permita las técnicas de fondo de hoy
(parallax, etc.), a la par o por encima de las referencias; evaluar idoneidad;
si conviene, plan de desarrollo.
**Respuesta corta — SÍ conviene, con una forma precisa**: no «un componente
más» sino **el anfitrión canónico de capas de fondo** que el tier `blocks` ya
está reclamando a gritos y que hoy se resuelve a mano. La evidencia es
mecánica, no de gusto:
- `src/uix/blocks/hero/hero.svelte` (layout `background`) monta **tres capas a
mano**: un `Box` absoluto para el media del app, un `Box` con
`style="background: var(--color-overlay); opacity: var(--opacity-scrim)"` de
scrim y un `<style>` scoped `/* justified: */` para el `object-fit: cover`
del `<img>/<video>` del app — la **excepción D-BLK.2** del contrato B, más el
`data-on='dark'` y las tintas explícitas puestos a mano. El README del hero
documenta dos huecos del canon que salieron de ahí. Es exactamente la regla
de admisión disparando: «si al construir un block hace falta un `data-attr`
que el CSS necesita seleccionar o una obligación a11y, esa pieza se construye
ANTES como componente canónico».
- `docs/process/PLAN-blocks-quality.md` §2 diagnosticó «no hay fondo decorativo
de sección» y resolvió `Backdrop` (glow/mesh/grid/dots) — **cuatro patrones
estáticos, una sola capa, sin README, sin demo, un consumidor** (hero). El
mismo plan deja «parallax suave» del hero como acabado que falta, y
`Surface` deja en Gaps «scrim de autoría para vívidos — componentizarlo sólo
si aparece el caso repetido». El caso ha aparecido.
- Ninguna referencia (Radix/Ark/Bits/React Aria/MUI/Chakra) tiene un
componente de fondo; Mantine (`BackgroundImage` + `Overlay`), Vuetify
(`v-parallax`) y `react-scroll-parallax` (`ParallaxBanner` por capas) son
los únicos con API de producto; el resto (Aceternity, Magic UI, shadcn.io,
React Bits) son colecciones de efectos copy-paste sin sistema. **El hueco de
«anfitrión de capas con contrato» está vacío en el sector** — y el framework
ya posee todas las piezas para llenarlo con ventaja (tokens+TSC, ActiveDom,
motion reducido, `data-on`, `Image`, `$scene`/`Ambient`, `Toggle`).
**Y con la misma precisión, lo que NO conviene**: meter en el canon los efectos
animados decorativos (aurora WebGL, beams, partículas…). Eso ya está decidido y
construido: son el **pack `Ambient`** (32 efectos sobre `$scene`) —
`docs/architecture/packs.md` + `decisions/design-text-effects.md` («backgrounds
went to the pack tier»). `Background` es el **anfitrión** (geometría, a11y,
tokens, parallax, scrim, media) donde el app monta esos efectos como una capa
más. Canon = la superficie de contrato; pack = la hoja decorativa. La doctrina
se cumple leyéndola entera: lo que fue al pack fue el EFECTO, no la ranura.
---
## 1. Lo que se ha leído (precondición cumplida)
Doctrina leída ENTERA para los ejes que toca esta pieza (arquitectura, canon,
morfo, soma §2 membresía, eidos, motion, theming, packs, blocks, guías):
`docs/README.md` · `architecture/overview.md` · `architecture/active-architecture.md`
· `CANON.md` · `architecture/morfo.md` · `architecture/soma.md` (membresía) ·
`architecture/eidos.md` · `src/uix/eidos/components/README.md` ·
`theming/motion.md` · `theming/motion-guide.md` · `src/arts/motion/README.md` ·
`theming/reference.md` (entera) · `canon/tsc.md` · `canon/recipe-contract.md` ·
`theming/gradient-finish.md` · `theming/channels.md` · `theming/notes.md` ·
`architecture/packs.md` · `architecture/blocks.md` · `building-a-component.md` ·
`guides/component-guide.md` (build contract + Before You Start §1–5) ·
`guides/completion-checklist.md` · `guides/demo-authoring.md` ·
`guides/component-audit.md` · `canon/vocabularies.md` ·
`decisions/design-text-effects.md` · `decisions.md` · `glossary.md` ·
`next-features.md` · `rfcs/rfc-depth.md` (alcance: scrim/frost/parallax) ·
`process/PLAN-blocks-quality.md` · `process/PLAN-blocks.md` (forma) ·
`process/CONTINUE-lectura-doctrina.md`.
Código leído: `eidos/components/{backdrop,surface,image,scroll-frames,motion}`
+ sus morfos · `blocks/hero/{README,hero.svelte}` · `packs/ambient/{README,
ambient.svelte}` · `arts/scene/{README,types}` · `arts/adom/README.md` ·
`eidos/lib/primitives/static.ts` (gradientes/scrim/opacity/blur) ·
`eidos/lib/themes/cristal.ts` · `web/routes/demos/heroscrolling` (semilla de
parallax) · `web/routes/demos/animations/background/*` (censo de semillas).
**No leído, y por qué no bloquea**: `architecture/sema.md` (1051 L) — la
pieza es pasiva (`scope: ['eidos']`, 0 eventos, patrón ScrollFrames/Backdrop);
si alguna decisión de §6 la hiciera emitir eventos, se lee ANTES de tocar el
morfo. `MOTION_SERVICE_RFC.md` — leído por sus síntesis en `motion.md` /
`motion-guide.md` y grepeado: **no contempla timelines de scroll ni parallax**
(sólo cita GSAP como lo que NO se reinventa) → el parallax es terreno no
cubierto por la doctrina de motion, y por eso es una decisión (D-BG.3), no una
presuposición.
---
## 2. Comparativa — qué hay hoy en el sector y dónde queda UIX
Leído en las fuentes (docs oficiales / repos), 2026-08-17.
| Capacidad | Mantine (`BackgroundImage`, `Overlay`) | Vuetify `v-parallax` | `react-scroll-parallax` (`ParallaxBanner`) | Motion/Framer (`useScroll`) | Aceternity · Magic UI · shadcn.io · React Bits | Tailwind Plus · Untitled · daisyUI | **UIX hoy** | **UIX con `Background`** |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Imagen de fondo cover con contenido encima | ✓ (`src`, `radius`) | ✓ (`src`, `height`) | ✓ (`image` por capa) | — | dentro de cada efecto | HTML + `bg-*` / `hero-overlay` | hero a mano (`<style>` justified) | ✓ `Background.Image` compone `Image` (fit/position/srcset/priority/fallback) |
| Vídeo de fondo (autoplay muted loop poster, pausa a11y) | ✗ | ✗ | ✗ | ✗ | ✗ | HTML crudo | ✗ | ✓ `Background.Video` + `Background.Pause` (WCAG 2.2.2), pausa fuera de vista/pestaña |
| Scrim / overlay (color, opacidad, gradiente, blur, fixed) | ✓ `Overlay` | ✗ | ✗ | — | ad hoc | `hero-overlay` (opaco fijo) | inline `style=` en hero | ✓ `Background.Scrim` sobre `--color-overlay` / `--opacity-*` / `--blur-*` (Cristal-compatible) |
| Capas apiladas con blend/opacidad/máscara | ✗ | ✗ | ✓ (orden = z; `expanded`) | manual | ✗ (un efecto = un wrapper) | ✗ | `Backdrop` = una capa `::before` | ✓ `Background.Layer` genérica (blend, opacity semántica, fade-mask, bleed) |
| Patrones estáticos (grid, dots, líneas, ruido, anillos, viñeta, glow, mesh, spotlight) | ✗ | ✗ | ✗ | — | ✓ (copy-paste, colores a mano) | Untitled `BackgroundPattern` (SVG), TW blobs `clip-path` | `Backdrop`: 4 sobre tokens | ✓ `Background.Pattern` (los 4 + lines/noise/rings/vignette/spotlight), **sobre tokens y modo** |
| Gradientes nombrados temables | ✗ | ✗ | ✗ | — | hex a mano | utilidades | `--gradient-{name}` (open cage) + `Surface gradient` | ✓ `Background.Gradient colors="aurora"\|[stops]` (patrón D-T3 de TextGradient) |
| Parallax por scroll (velocidad por capa) | ✗ | ✓ (una imagen) | ✓ (`speed`, easing, ranges) | ✓ (`useTransform`) | «Hero Parallax» (JS, GSAP-like) | ✗ | ✗ (semilla `heroscrolling`) | ✓ CSS scroll-driven (`animation-timeline: view()`) primero + fallback JS por ActiveDom |
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, y una animación sobre `translate` gana a cualquier declaración estática: el puntero habría dejado de existir sin más. Así que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único `translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| Parallax por puntero / spotlight | ✗ | ✗ | ✗ | ✗ | ✓ («Parallax Scroll mouse», Spotlight) | ✗ | sólo dentro de efectos WebGL del pack | ✓ `depth` por capa + `spotlight` (un listener por pila, rect cacheado, `dom.writeProperty`) |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| Efectos animados (aurora, beams, partículas, mesh en deriva…) | ✗ | ✗ | ✗ | ✗ | ✓ decenas (canvas/CSS/WebGL) | ✗ | ✓ **pack `Ambient` (32)** + `$scene` | = pack; `Background.Layer` es la ranura; `Ambient` lee el contexto (pausa/reduce) |
| Reduced-motion / reduced-data / forced-colors por construcción | ✗ | ✗ | `disabled` manual | manual | rara vez | ✗ | Backdrop: `prefers-contrast` ✓; pack: P-1 reduce ✓ | ✓ política `reduce` en el host + poster-only bajo `saveData` + capas fuera en `forced-colors` |
| Contexto de tinta para el contenido (inversión) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | `data-on` (D12, mínimo) | ✓ prop `on` en el host (D12) + scrim: la legibilidad se compone, no se improvisa |
| Disciplina de plataforma (iframe-safe, sin reflow forzado, teardown) | ✗ | ✗ | ✗ | parcial | ✗ | ✗ | ActiveDom (`listen/raf/measure/observe*`) | ✓ heredada — la misma que ScrollFrames |
| Contrato declarado (partes, `aria-hidden`, tokens con TSC, guard) | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | morfo pasivo (Backdrop/Surface/ScrollFrames) | ✓ morfo `scope:['eidos']` + `component:audit` + eidos-lint + recipe-contract |
**Lectura de la tabla.** Ninguna referencia ofrece la pieza entera; cada una
tiene un trozo. UIX iguala cada trozo con SU mecanismo (tokens, ActiveDom,
`Image`, `data-on`, pack) y supera en lo que ninguna tiene: contrato
declarado, a11y por construcción, theming vivo, y la ranura para el pack. Donde
las referencias seguirán ganando **a propósito**: amplitud de efectos exóticos
(shadcn.io lista 100) — la respuesta del framework es el tier de packs, no el
canon; y la orquestación de scroll tipo GSAP (pin + scrub + timelines) —
fuera de alcance de esta pieza (ScrollFrames ya cubre el scrub de media; el
storytelling pinned es otra iniciativa).
**Soporte de plataforma comprobado (2026-08)**: CSS scroll-driven animations
en Chrome/Edge 115+, Safari 26+ (threaded en 26.4), Firefox aún tras flag
(prioridad Interop 2026) — ~84 % global, no Baseline → **progressive
enhancement obligatoria** con `@supports (animation-timeline: view())` y
fallback JS (es lo que hace Motion/Framer: `ScrollTimeline` nativo donde hay,
JS donde no).
---
## 3. Inventario — lo que el framework YA tiene (y no se reinventa)
| Pieza | Qué aporta a `Background` | Cómo se usa |
| --- | --- | --- |
| `Backdrop` (canon, eidos-native; morfo `scope:['eidos']`, 1 parte, `data:[]`) | 4 patrones sobre tokens (glow/mesh/grid/dots), `fade` mask, `prefers-contrast: more` cae el bloom, `isolation:isolate` + `::before z:-1` | Se **absorbe** como `Background.Pattern` (D-BG.1); único consumidor: hero |
| `Surface` (Box + tratamiento; `on`, `gradient`, `rounded`) | El precedente de «lienzo temable» y de `data-on` (D12) | `Background` NO duplica: es la superficie de CAPAS; `Surface` sigue siendo el lienzo pintado por identidad. Se cierra su gap «scrim» |
| Gradientes nombrados `--gradient-{name}` (open cage; `aurora` es `background-image`-válido, los mesh con base final NO) | Capa de gradiente temable | `Background.Gradient` pinta con `background:` shorthand (válido en ambos casos) — la lección de `reference_mesh_gradient_not_background_image` |
| `Image` (fit/position responsive, srcset/sizes/fetchpriority/decoding, Fallback/Error, `data-status` de `ImageProvider`) | La imagen de fondo con ciclo de carga | `Background.Image` **compone** `<Image fit="cover">`; nunca un `<img>` crudo |
| `ScrollFrames` (progreso de scroll por `dom.listen` + `dom.raf` + `observeResize`, lecturas dentro del rAF) | El patrón JS de progreso de scroll disciplinado | Semilla del fallback de parallax; candidato a compartir un helper `$adom` (`ScrollProgress`, D-BG.7) |
| `Motion trigger="viewport"` + contexto `seen` · `Cascade` · `data-stagger` | La coreografía de entrada del contenido — no del fondo | El contenido encima sigue usando esto; `Background` publica su propio contexto (pausa/reduce) por el mismo patrón |
| `$scene` (engine con IO-pause, DPR cap, reduce obligatoria, context-loss, budget, `pause()/resume()` en el handle) + pack `Ambient` (32 efectos, P-1…P-6, re-tintado por tema) + `Aura` | Los fondos ANIMADOS ya resueltos | Se montan DENTRO de `Background.Layer` desde el app; el pack puede leer el contexto de `Background` (dirección pack→framework, legal por `packs.md` §Hard boundaries 1) |
| Tokens: `--color-overlay` (por modo), `--opacity-{scrim,overlay,muted,subtle,ghost}`, `--blur-*`, `--gradient-angle-*`, `--motion-distance-*`, `--radius-*`, `--z-index-*` | Todo lo que un scrim / frost / parallax necesita, ya temable | Consumidos por el recipe; el generador emite los `--background-*` (TSC) |
| Depth channel + `data-frost` + tema Cristal | El vidrio como cue de plano | `Background.Scrim blur` usa `--blur-*`; el fondo NO es elevación (`rfc-depth` excluye parallax 3D del canal a propósito) |
| `data-on` (foundation, D12) | Inversión mínima de tinta del subárbol | Prop `on` en el host; límites documentados (anidados/portales) — hero midió los dos huecos (`Display` ignora el contexto, `Link subtle` hereda) |
| `ActiveDom`: `listen`, `raf`, `measure`, `writeProperty`, `observeIntersection`, `observeResize`, `prefersReducedMotion`, `IsInViewport`, `IsDocumentVisible`, `ScrollState`, `ElementRect`, `getWindow` | Toda la ciudadanía DOM | Sin `window.*` crudos; `CSS.supports` vía `dom.getWindow(node)` |
| `Toggle` / `IconButton` (canon) | El control de pausa | `Background.Pause` compone `Toggle` (`pressed` = pausado); los eventos sema son de Toggle |
| `Box` (position/inset/overflow/minHeight…) | La caja anfitriona | `Background` compone `<Box>` como Backdrop/Surface (patrón Section) |
| Semillas: `web/routes/demos/heroscrolling` (parallax de columnas + giro 3D), `animations/background/{liquid-image,glass-window,…}` | Referencia comparativa (decisión D5 del plan scene: intactas, no se shippean) | Se citan en el README como baseline; nada se porta verbatim |
**Huecos reales del framework** (lo que `Background` construye o destapa):
anfitrión de capas composable · scrim/overlay canónico · imagen/vídeo cover
como capa con políticas (modo, reduce-motion, reduce-data, pausa) · parallax
por scroll y por puntero · patrones adicionales sobre tokens · política
`forced-colors` para decoración · puerto `prefersReducedData` en `$adom` (no
existe; `prefers-reduced-data` no es Baseline, `navigator.connection.saveData`
sólo Chromium) · helper `ScrollProgress` en `$adom` (no existe; ScrollFrames lo
lleva privado).
---
## 4. Idoneidad y tier — el veredicto razonado
**Regla de admisión** (`packs.md` / `blocks.md`): canon = lo que tiene
superficie de contrato que otros consumen. `Background` la tiene por tres vías:
1. **`data-*` que el CSS necesita seleccionar** — `data-background`,
`data-background-layer`, `data-background-pause`: un block NO puede escribir
CSS (B-3/B-11), así que la geometría de capas sólo puede vivir en el canon.
2. **Obligaciones a11y** — capas decorativas `aria-hidden` +
`pointer-events:none`; **pausa/parada** de movimiento automático > 5 s en
paralelo con contenido (WCAG 2.2.2) para vídeo y capas animadas;
**legibilidad del contenido sobre media** (1.4.3/1.4.11 → scrim + `on`);
reduced-motion (2.3.3) para parallax; `forced-colors`. «The moment an
artifact owns an accessibility obligation, it enters the canon»
(`design-text-effects.md`).
3. **Tokens que otros recipes/temas quieren** — `--background-scrim-*`,
`--background-parallax-travel`, `--background-pattern-*` (los de Backdrop),
temables por config (precedente state-layer §38/§40).
**Forma**: componente **eidos-native pasivo** — morfo `scope: ['eidos']`, 0
eventos, sin soma (membresía §2 de `soma.md`: no hay patrón APG ni máquina de
estados accesible; el fondo pinta y reacciona a scroll/puntero como
**modulación continua**, no como acto — el mismo criterio contract-level que
ScrollFrames («observación, no acto») y la regla «no emitir por frame»). El
único acto de usuario —pausar— pertenece al `Toggle` compuesto y a SU morfo
(`commit-toggle`). El estado `paused` es visual (`data-paused`, attr de
wrapper). Sema: `SemaPanel` en vacío justificado (D-4.1).
**Morfo mínimo (regla 2026-08-15, morfo `image.ts`)**: sólo partes + ARIA;
los knobs visuales (`data-kind`, `data-fit`, `data-blend`, `data-parallax`,
`data-attach`, `data-paused`, `data-pattern`, `data-color`, `data-strength`)
son attrs eidos-only de wrapper — ningún prop cruza la frontera de soma. Las
partes son lo que el consumidor COMPONE (`morfo.md` §renderAttrs: «if a
consumer can compose it or address it, it is a PART»):
| Parte | kebab | archetype | rol/aria | Nota |
| --- | --- | --- | --- | --- |
| Provider (host) | `provider` | `provider` | — | `Box` + `position:relative; isolation:isolate; overflow:clip` |
| Layer | `layer` | — (display puro; ver nota) | `aria-hidden="true"` (literal, como `scroll-frames.media`) | UNA parte para todas las capas; el `kind` (image/video/pattern/gradient/scrim/custom) es attr eidos-only |
| Pause | `pause` | `action` | ARIA del `Toggle` compuesto | Opcional; contenedor de colocación (patrón Fab-sobre-Button) |
> Nota archetype `layer`: `image` sólo cuando el elemento ES `<img>`; una capa
> genérica no encaja en ninguno de los 26 (`overlay` = «modal/dim backdrop» y
> arrastraría CSS de veil; `content` pisa `position`) → sin archetype
> («Omit it for a plain display part that pulls no shared styling»). Se
> confirma en F0 contra `ARCHETYPE_DESCRIPTIONS`.
**Textos** (`texts:` + `langs/components/background.ts`): `pause`
(`#?components.background.pause|Pause background`) y `play`
(`#?components.background.play|Play background`) para el `Toggle`; claves
IDÉNTICAS entre `texts` y catálogo (la trampa de chronos).
**Motion (doctrina aplicada)**: la deriva ambiental de un gradiente o el
travel de parallax NO son firmas de evento ni presets de estado → van como
`@keyframes` locales anotados `/* functional: … */` (R-4.5; precedente
TextGradient) con kill bajo reduce; el parallax por scroll con
`animation-timeline: view()` es la misma clase (maquinaria continua). La
alternativa —un eje `timeline` en el registro de presets— es una EXTENSIÓN de
contrato de `$motion`/eidos y sólo entra firmada (D-BG.3).
**Packs (doctrina aplicada)**: canon nunca importa `src/packs/`;
`Background.Layer` es una ranura; el app compone `<Ambient>` dentro; un block
NO puede (B-4) — recibe la escena del app por snippet (el hero ya tiene
`backdrop`). Un `Background.Scene effect=…` en canon importaría decoración al
canon → **rechazado** (queda registrado como alternativa desechada).
**Blocks (doctrina aplicada)**: al existir `Background`, el layout
`background` del hero deja de necesitar su `<style>` justificado y su scrim
inline; `Background.Pattern` releva a `Backdrop`; los blocks pendientes de
acabado (`cta`, `stats-band`, `testimonials`, `feature-split` «banda de fondo
por fila», `banner`, `site-footer`) componen la misma pieza. **Un block sólo
puede ser tan expresivo como el canon que compone** — ésta es la palanca.
---
## 5. Forma canónica propuesta (API, recipe, tokens, a11y, perf)
Todo lo de esta sección es PROPUESTA sujeta a §6. Convención: disciplined
option C (root + partes atadas), children siempre; sin flat-snippet API.
### 5.1 API
```svelte
<!-- El PADRE es cualquier componente o elemento que acepte hijos: Section, Card,
Dialog.Content, un Box-columna de Grid/Flex, un <li>… No se envuelve nada:
Background es un HIJO más, y el padre se vuelve anfitrión por la regla de
foundation (`:has`) sin tocar su recipe. -->
<Section>
<Background on="dark" parallax bind:paused>
<!-- capas: orden de fuente = apilamiento; todas absolute/inset 0 DENTRO de la pila, aria-hidden -->
<Background.Image src={hero} srcset sizes fetchpriority="high" fit="cover" position="center"
speed={-0.3} sources={{ dark: heroDark }} />
<Background.Video src="/loop.mp4" poster="/loop.jpg" sources={[{ src, type, media }]}
speed={-0.2} />
<Background.Gradient colors="aurora" | colors={['--color-primary-solid', '--color-tertiary-solid']}
angle="to-b" animate />
<Background.Pattern pattern="dots" color="primary" fade /> <!-- ex-Backdrop -->
<Background.Layer speed={0.15} blend="screen" opacity="muted" fade="to-b">
<Ambient effect="mesh" params={{…}} /> <!-- el pack, desde el APP -->
</Background.Layer>
<Background.Scrim strength="scrim" gradient="to-t" blur="md" />
<Background.Pause position="top-end" /> <!-- opcional: reubica el control -->
</Background>
<!-- el contenido, hermano de la pila, en flujo normal: pinta encima solo -->
<Container>…</Container>
</Section>
<!-- Un componente que quiera RENDERIZAR el fondo desde dentro (hero) expone un
snippet — composición, nunca un struct (D-BG.13): -->
<Hero>
{#snippet background()}<Background>…</Background>{/snippet}
{#snippet title()}…{/snippet}
</Hero>
```
| Superficie | Props (v1) | Notas |
| --- | --- | --- |
| `Background` (root = la PILA; NO compone `Box`) | `on?: 'light'\|'dark'` · `parallax?: boolean \| 'scroll' \| 'pointer' \| ('scroll'\|'pointer')[]` · `attach?: 'scroll'\|'fixed'` · `reduce?: 'static'\|'hide'` (política bajo reduced-motion, default `static`) · `paused?: boolean` (bindable) · `children` (las capas) + `HTMLAttributes<HTMLDivElement>` | La pila es `position:absolute; inset:0; z-index:-1; border-radius: inherit; overflow: clip` — bleed, recorte, radio y `attach='fixed'` (`clip-path: inset(0)` en la PILA, nunca en el padre) no tocan al anfitrión. El padre queda posicionado + aislado por la regla de foundation `:where(:has(> [data-background]))` (D-BG.14). `on` re-vincula la tinta del PADRE vía el gemelo `:where(:has(> [data-background][data-on='dark']))` del bloque D12 (mismos límites: anidados/portales). Publica contexto `getBackgroundContext()` → `{ paused, reduced, seen }`. Sin `rounded` (hereda del padre), sin props de layout (son del padre) |
| `Background.Layer` | `speed?: number` (parallax scroll; 0 = fijo; negativo = más lento/opuesto — semántica react-scroll-parallax) · `depth?: number` (parallax puntero) · `blend?: MixBlend` · `opacity?: 'full'\|'subtle'\|'muted'\|'ghost'` (tokens `--opacity-*`) · `fade?: 'radial'\|'to-t'\|'to-b'\|'edges'` (mask) · `bleed?: boolean` (default `true` cuando hay `speed`: margen negativo = `--background-parallax-travel` para que no asomen bordes; el `expanded` de react-scroll-parallax) · `pointer?: boolean` (opt-in `pointer-events:auto` para escenas interactivas, P-5) · `children` | La capa genérica; las demás son azúcar sobre ella con `data-kind` |
| `Background.Image` | `ImageProps` (compone `<Image fit="cover">`) + layer props + `sources?: { dark?: string; light?: string }` | Decorativa por definición: `alt=""` + `aria-hidden` (si la imagen significa algo, es contenido, no fondo). Modo resuelto por `eidos.getThemeContext()` (D-BG.5). Default `loading="eager"` + `fetchpriority="high"` sólo si el app lo pide: un fondo bajo el pliegue debe seguir lazy |
| `Background.Video` | `src` · `sources?` · `poster` · `loop=true` · `autoplay=true` · `preload='metadata'` · layer props | Siempre `muted playsinline` (política de autoplay); pausa fuera de vista (`observeIntersection`) y con pestaña oculta (`IsDocumentVisible`); reduced-motion → no autoplay (poster); reduced-data → sólo poster; `paused` global |
| `Background.Pattern` | `pattern: 'glow'\|'mesh'\|'grid'\|'dots'\|'lines'\|'noise'\|'rings'\|'vignette'\|'spotlight'` · `color?: ComponentColorProp` · `cell?` · `fade?` + layer props | Los 4 de Backdrop migran 1:1 (tokens `--backdrop-*` → `--background-pattern-*`); `noise` = `feTurbulence` en `url()` data-URI (sobrevive forced-colors → por eso D-BG.6 oculta las capas allí); `spotlight` sigue el puntero |
| `Background.Gradient` | `colors: string \| string[]` (nombre canónico `--gradient-{name}` \| lista de stops con tokens) · `angle?: GradientAngle` · `animate?: boolean` + layer props | Discriminación por FORMA (D-T3 de TextGradient); pinta con `background:` shorthand; `animate` = deriva `background-position` funcional, estática bajo reduce |
| `Background.Scrim` | `color?: ComponentColorProp` (default `--color-overlay`) · `strength?: 'scrim'\|'overlay'\|'muted'\|'subtle'\|'ghost'` (tokens) · `gradient?: 'to-t'\|'to-b'\|'radial'` · `blur?: BlurKey` (`--blur-*`, `backdrop-filter`) | Paridad Mantine `Overlay`; el frost es Cristal-compatible |
| `Background.Pause` | `position?: LogicalPosition` (default `top-end`) · props del `Toggle` compuesto | Compone `Toggle` (`pressed` ↔ `paused`, textos del morfo). Regla de render por defecto: D-BG.4. **Colocación**: NO puede vivir dentro de la pila (`z-index:-1` la dejaría bajo el contenido) → `Background` la renderiza como **segundo nodo raíz** (fragmento hermano de la pila, `position:absolute; z-index: 1` sobre el contenido del padre); si el app compone `<Background.Pause>` fuera de `<Background>` (junto al contenido), esa instancia se registra en el contexto y el default no se renderiza |
Contexto publicado (patrón `getMotionContext`): `{ paused, reduced, seen }` —
`seen` por `IsInViewport` para que capas caras arranquen al entrar (misma
razón que Motion→CountUp).
### 5.2 Recipe (`background.css`) — sólo selectores morfo-backed + attrs de wrapper
- **Foundation (generador, `render-css.ts`, junto a `renderOnContextBlocks`)** —
la adopción del anfitrión (D-BG.14):
`:where(:has(> [data-background])) { position: relative; isolation: isolate; }`
y el gemelo de tinta `:where(:has(> [data-background][data-on='dark']))` /
`…='light'` añadido como segundo selector del bloque D12 (una regla, dos
selectores — sin duplicar declaraciones). `:where` = especificidad 0: el
`position` propio de cualquier recipe (Dialog.Content `fixed`, Affix…) gana;
`isolation` no altera layout. Un padre `display: contents` no tiene caja → se
documenta como no-anfitrión.
- `[data-background]` (la pila): `position:absolute; inset:0; z-index:-1;
border-radius: inherit; overflow: clip; pointer-events: none;` —
`[data-attach='fixed']` → `clip-path: inset(0)` (la capa fija dentro queda
recortada por la pila; documentar que un `transform`/`filter`/`contain: paint`
en ancestros rompe `fixed`). Dentro de un padre con `[data-shape]` /
`[data-shape-nest]` el recorte sigue al `corner-shape` heredado (verificar en
F1: Card `rounded` × shape `continuous`, en Chrome real).
- `[data-background-pause]` (segundo nodo raíz): `position:absolute;
inset-block-start/inset-inline-end` según `LogicalPosition`; `z-index: 1`
(intra-componente, entero crudo permitido por el recipe-contract) — sobre el
contenido del padre, alcanzable por puntero y teclado.
- `[data-background-layer]`: `position:absolute; inset:0; z-index:-1;
pointer-events:none;` + `[data-bleed]` inset negativo por
`--background-parallax-travel`; `[data-blend=…]`; `[data-opacity=…]` →
`opacity: var(--opacity-{k})` (R-4.2 sin literales); `[data-fade=…]` → mask
(Backdrop hoy); `[data-pointer]` → `pointer-events:auto`.
- Parallax scroll: `[data-background][data-parallax~='scroll'] [data-background-layer][data-speed]`
→ `animation: background-parallax linear both; animation-timeline: view();
animation-range: entry 0% exit 100%;` con `@keyframes background-parallax
{ from { translate: 0 calc(var(--_background-speed) * var(--background-parallax-travel)) } to { translate: 0 calc(-1 * …) } }`
`/* functional: scroll-linked travel, not an event signature */`; bajo
`@supports not (animation-timeline: view())` el wrapper escribe
`--background-progress` (0..1) por `dom.writeProperty` desde `ScrollProgress`
y el recipe traduce con `calc()`. Reduce → `animation:none; translate:none`.
- Parallax puntero: host escribe `--background-pointer-x/-y` (−1..1) en
`pointermove` coalescido por `dom.raf`; capa → `translate:
calc(var(--background-pointer-x) * var(--_background-depth) * var(--motion-distance-xl))`.
Físico, no espeja en RTL (sigue al puntero). Reduce → 0. Sin `will-change`
global (memoria: jitter a DPR fraccional; sólo si se mide necesidad).
- Scrim: `[data-kind='scrim']` → `background: color-mix(in oklch,
var(--_background-scrim-color) calc(var(--_background-scrim-opacity) * 100%), transparent)`
o gradiente `to-t/to-b/radial`; `[data-blur]` → `backdrop-filter: blur(var(--blur-{k}))`.
- Patrones: migración literal de `backdrop.css` con prefijo `--background-pattern-*`.
- `@media (forced-colors: active) { [data-background-layer] { display:none } }`
(D-BG.6); `@media (prefers-contrast: more)` cae glow/mesh (hoy).
- Cero `@keyframes` no anotados, cero color crudo, cero `!important`, cero
`--eidos-*`. Recipe tokens en `lib/recipes/base.ts` (`background-*`, TSC:
`root` los estables; `host` los que leen `--palette-*`).
### 5.3 a11y — el contrato, no la esperanza
- Capas: `aria-hidden` (morfo) + `pointer-events:none` (recipe). Un fondo NO
es contenido; si el media significa, el app lo pone como `Image` con `alt`
en el flujo.
- **Pausa** (WCAG 2.2.2): cualquier capa que se mueva sola > 5 s (vídeo
autoplay, `Gradient animate`, escena del pack) exige control accesible;
`Background.Pause` compone `Toggle` (foco, `aria-pressed`, textos por langs).
Reduced-motion NO sustituye al control (es preferencia, no mecanismo).
- **Legibilidad**: `Scrim` + `on` son la pareja documentada; la demo mide
contraste con el método del hero (píxeles pintados; el probe rgb/canvas
miente con `oklch()`).
- Reduced-motion: parallax off, vídeo sin autoplay, `animate` estático;
`reduce='hide'` oculta las capas decorativas enteras.
- Reduced-data: vídeo → poster; imágenes: se respeta `srcset/sizes` del app.
- `forced-colors`: capas fuera; el contenido cae al `Canvas` del UA.
- Sin foco atrapado, sin roles falsos (lección TextFocus).
### 5.4 Rendimiento
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, y una animación sobre `translate` gana a cualquier declaración estática: el puntero habría dejado de existir sin más. Así que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único `translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
Sólo `translate`/`opacity` en las capas; scroll-driven por CSS donde
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
hay soporte; el fallback lee layout dentro de `dom.raf` (nunca sync tras
escritura; el detector `uix.perf` de reflow lo verifica); vídeo pausado fuera
de vista y con pestaña oculta; `preload="metadata"`; imágenes lazy por
defecto; el presupuesto de escenas lo pone `$scene`; `attach='fixed'` con
`clip-path` (no `background-attachment: fixed`, roto en iOS y caro).
---
## 6. Decisiones D-BG — para firmar UNA A UNA (nada ejecutado)
| # | Decisión | Opciones | Recomendación (arquitecto de framework de referencia) |
| --- | --- | --- | --- |
| **D-BG.1** | Nombre y destino de `Backdrop` | (a) nuevo `Background` compound que ABSORBE Backdrop → `Background.Pattern`, hero migra, `backdrop/` se BORRA (sin shim); (b) crecer `Backdrop` como compound; (c) dos componentes | **FIRMADA 2026-08-17 — (a), nombre `Background`** (no `RichBackground`: los nombres del catálogo dicen QUÉ es la pieza, nunca cómo de buena es; ergonomía de `Background.Video` / `data-background` / `--background-*`; las referencias usan el sustantivo). Motivo: «Backdrop» colisiona con el veil modal (MUI/Vuetify y el archetype `overlay` = «modal/dim backdrop»); Backdrop tiene 1 consumidor, sin README ni demo (C8 de PLAN-blocks-quality). El borrado de `backdrop/` + su morfo sigue exigiendo tu orden explícita en F1 |
| **D-BG.2** | Tier y alcance | canon eidos-native pasivo (host + capas + a11y + tokens); efectos animados siguen en el pack; `Background.Layer` = ranura; SIN `Background.Scene` en canon | **FIRMADA 2026-08-17** — canon eidos-native pasivo (`scope: ['eidos']`, partes `provider`/`layer`/`pause`, 0 eventos, sin soma ni pack sema; la pausa la emite el `Toggle` compuesto); efectos animados = pack `Ambient`; `Background.Layer` = ranura; `Background.Scene` en canon RECHAZADO. Si una capa necesitara un evento propio algún día: leer `architecture/sema.md` y replantear, no improvisar |
| **D-BG.3** | Mecanismo de parallax | (A) local al recipe: `animation-timeline: view()` + keyframes `/* functional */` + fallback JS por ActiveDom; (B) eje `timeline` en el registro de presets de `$motion`/eidos (extensión de contrato) | **FIRMADA 2026-08-17 — (A)**: CSS scroll-driven primero (`animation-timeline: view()` + `animation-range`, keyframes locales anotados `/* functional: scroll-linked travel, not an event signature */`), fallback bajo `@supports not (animation-timeline: view())` con `ScrollProgress` (D-BG.7) escribiendo `--background-progress` vía `dom.writeProperty` dentro de `dom.raf`; reduce → sin travel; token `--background-parallax-travel` (config data). Sin cambios en `$motion` ni en el registro de presets; nota en `MOTION_SERVICE_RFC` (candidato «dominio scroll», ≥2 consumidores) y en `motion-guide.md` §8 |
uix(background): el media entra en la pila, y el fondo aprende a pararse F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-BG.4** | Control de pausa | (a) `Background.Pause` se renderiza POR DEFECTO cuando hay capa autoplay/animada; componerlo explícito lo reubica y suprime el default (precedente: thumb por defecto de `Switch`); (b) sólo por composición (N-7 estricto) + warning dev si autoplay sin control | **FIRMADA 2026-08-17 — (a)**: default cuando hay capa que se mueve sola > 5 s (vídeo autoplay, `Gradient animate`, escena del pack que lo declare por contexto); `Toggle` compuesto (`pressed` ↔ `paused`, textos `pause`/`play` por langs, `aria-pressed`, teclado) como segundo nodo raíz sobre el contenido, `top-end` (`LogicalPosition`); una instancia compuesta explícitamente por el app se registra en el contexto y suprime el default; reduced-motion NO sustituye al control. Sin `*Button` boolean props. **Enmendada por D-BG.18** (la forma es `IconButton` + `aria-label` que cambia, sin `aria-pressed`) y **por D-BG.17** (la reubicación explícita viaja por el snippet `pause`, no por un hijo dentro de la pila) |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **D-BG.5** | Media según modo | `sources={{ dark }}` resuelto por `eidos.getThemeContext()` (reactivo, como el re-tintado de Ambient), NO `prefers-color-scheme` (el modo del framework no es el del SO) | **FIRMADA 2026-08-17** — `sources={{ light?, dark? }}` en `Background.Image`/`Background.Video`, `src` derivado en el wrapper por `getThemeContext().mode` (reactivo, sin remount; cae a `src` si falta la clave del modo activo). Motivo: `mode` es un source visual de `ActiveEidos` (toggle del app / pref persistida / `data-mode` local) que puede divergir del SO; `<picture media="(prefers-color-scheme)">` sólo ve el SO. Alternativa CSS (dos `<img>` + `[data-mode]`) descartada: doble descarga y DOM |
| **D-BG.6** | `forced-colors` | ocultar todas las capas decorativas (contenido sobre `Canvas`) | **FIRMADA 2026-08-17** — `@media (forced-colors: active) { [data-background-layer] { display: none } }`; el `Toggle` de pausa sigue visible (control, no decoración); se mantiene el `prefers-contrast: more` heredado de Backdrop (glow/mesh fuera, patrones estructurales se quedan); nunca `forced-color-adjust: none`. Motivo: los `url()` (imagen, vídeo, ruido data-URI) sobreviven al UA y el scrim (`background-color`) lo pinta el UA como `Canvas`, así que no protege |
| **D-BG.15** | **Fallo de carga de imagen / vídeo** — planteada por el autor 2026-08-17 («¿lo resolvía Image con su fallback?») | (a) **la pila ES el fallback**: las capas de media son transparentes hasta cargar y se ocultan al fallar; lo que haya debajo (padre, `Pattern`, `Gradient`, `Scrim`) se ve; `Background.Image` compone `<Image>` y hereda su ciclo `idle/loading/loaded/error` (`data-status`, `Fallback`/`Error` como snippets passthrough) con defaults de FONDO: `placeholder='none'` (nada de skeleton a sangre), sin icono de error (decorativo → capa transparente); `Background.Video`: `poster` nativo + estados propios (`loadeddata`/`error`/`stalled`/fuente no soportada → `data-status='error'` en la capa → el vídeo se oculta y queda el poster o la capa de debajo); (b) API de fallback propia (`fallback` snippet por capa) | **FIRMADA 2026-08-17 — (a)**: un fondo no muestra errores: degrada a lo que tiene debajo, y el orden de capas ya expresa «primero lo barato, encima el media». `Background.Image` = `<Image>` con `placeholder='none'`, sin icono de error, `Fallback`/`Error` passthrough. `Background.Video` = `poster` + `data-status` propio (`loadeddata`/`error`/`stalled`/no soportado → capa oculta). Para el vídeo NO existe contrato de estado en el framework (`ImageProvider` sólo cubre `<img>`; ScrollFrames escucha `loadedmetadata` a mano): con DOS consumidores (`ScrollFrames` + `Background.Video`) es candidato a capa compartida `soma/layers` («compose existing; flag gaps») — se evalúa en F2 y, si se extrae, entra por su propia decisión; en v1 `Background.Video` lo hace localmente vía `dom.listen` como ScrollFrames. Demo: una imagen rota y un vídeo roto |
| **D-BG.7** | Infra `$adom` | (i) helper reactivo `ScrollProgress` (patrón `IsInViewport`; fallback de parallax; ScrollFrames candidato a migrar); (ii) puerto `prefersReducedData` (`prefers-reduced-data` + `navigator.connection.saveData`, `false` en SSR) | **FIRMADA 2026-08-17 — ambos**: `src/arts/adom/scroll-progress.svelte.ts` (+ test happy-dom; progreso 0→1 de un elemento por viewport o `root`, medido en `dom.raf` sobre `listen('scroll', passive)` + `observeResize`) y `ActiveDom.prefersReducedData` (`matchMedia('(prefers-reduced-data: reduce)')` OR `navigator.connection?.saveData`, `false` en SSR, sobre `targetWindow`; + test); export en el barrel `$adom`; entrada fechada en «Backlog / Evolution decisions» del README de adom (what/why/trigger). `SceneDom`/`MotionDom` no cambian; ScrollFrames no se migra ahora (F6) |
| **D-BG.8** | Integración con el pack | `Background` publica `getBackgroundContext()` `{ paused, reduced, seen }`; `<Ambient>` lo lee opcionalmente y llama `pause()/resume()` del handle (`$scene` ya lo expone) | **FIRMADA 2026-08-17** — el canon publica `{ paused, reduced, seen, registerAnimated() }` (patrón `getMotionContext`); `<Ambient>` (pack) lo lee opcionalmente: `handle.pause()/resume()` con `paused`, respeta `reduced`, se declara animado (dispara la pausa por defecto de D-BG.4); un prop `paused` explícito en `<Ambient>` puede coexistir como override. Tarea DEL PACK, separada, después de F2 (`src/packs/ambient/ambient.svelte` + README del pack); nada en `$scene`, nada en el canon más allá del contexto; encapsulación intacta |
| **D-BG.9** | `attach='fixed'` | técnica `clip-path: inset(0)` + capa `position:fixed`; incompatibilidades documentadas | **FIRMADA 2026-08-17** — en F3: `[data-background][data-attach='fixed'] { clip-path: inset(0) }` + `[data-background-layer][data-attach='fixed'] { position: fixed; inset: 0 }` (el `overflow: clip` de la pila NO recorta fijos; el `clip-path` sí). Nunca `background-attachment: fixed`. Incompatibilidades documentadas (`transform`/`filter`/`perspective`/`contain: paint`/`will-change: transform` en un ancestro → el fijo se comporta como absoluto, sin efecto pero sin fallo); `Dialog.Content` = no-anfitrión de este modo (documentado). Sin JS, sin listeners; verificar en Chrome y Safari reales |
| **D-BG.10** | Rollout en blocks | hero `background` → `Background` (borra la excepción D-BLK.2), Backdrop→Pattern en todos; luego cta/stats-band/testimonials/feature-split/banner/site-footer | **FIRMADA 2026-08-17** — F5, sólo tras F4 PASS, en este orden: (1) hero (layout `background` compone `Background`: fuera el `<style>` justificado, el scrim inline y el `data-on` manual → `on="dark"`; `decor` → `Background.Pattern`; snippet `backdrop` → `background`; su README borra la excepción D-BLK.2) — es la prueba del listón; (2) cta · stats-band · testimonials · feature-split (banda por fila) · banner · site-footer (columna B de PLAN-blocks-quality §3); (3) página compuesta. Una entrada de `AUDIT-blocks-ledger.md` por block con medición (contraste en píxeles). El «parallax suave del mockup» del hero queda FUERA (contenido, no fondo → candidato del eje motion/scroll); los huecos `Display`/`Link` bajo `data-on` siguen como candidatos de canon aparte |
| **D-BG.11** | Registro documental | entrada nueva en `docs/next-features.md` (§11); nota aclaratoria en `design-text-effects.md` («el HOST es canon; el EFECTO es pack»); `PLAN-blocks-quality.md` Q0.3 apunta aquí | **FIRMADA 2026-08-17** — en F4, tras el PASS (nunca antes: una nota que describe futuro es la deriva de `packs.md`/`glossary.md` con Aura): (1) `next-features.md` §11 (fecha, origen, alcance, deps, enlace a este plan); (2) frase en `design-text-effects.md`: el EFECTO fue al pack, el ANFITRIÓN de capas es canon por su contrato; (3) `PLAN-blocks-quality.md` Q0.3 → sucesor; (4) `surface/README.md` §Gaps «scrim» → cerrado por `Background.Scrim`; (5) `motion-guide.md` §8 una línea + nota en `MOTION_SERVICE_RFC` (D-BG.3); (6) `glossary.md`: entrada `Background` + corregir `Aura` («Not built yet» caduco, hallazgo nº 18 del registro de lectura). Regla `authoring.md` (leerlo entero antes): enlazar, no copiar; sin conteos a mano; `docs:check` verde |
| **D-BG.12** | ~~Uso dentro de CUALQUIER componente — dos modos (envoltorio + `fill`)~~ | — | **SUPERADA el mismo día por D-BG.14** (un solo modo: hijo). Sobreviven sus tres consecuencias como alcance/gate de F1: (1) herencia de radio y **shape** dentro de superficies redondeadas/anidadas (verificar Card `rounded` × shape `continuous` en Chrome real); (2) el **bug medido** en el hero (`Box flex/grow` no crece un hijo flex, README hero 2026-07-23) lo destapa el caso columna → se verifica y, si sigue, se arregla en `Box` ANTES (canon, no en Background); (3) **coste en partes repetidas** (celdas/items/cards de una rejilla): patrones/gradientes/scrim sí; vídeo/escenas NO (una por sección; el presupuesto de `$scene` avisa) — README y demo lo dicen |
| **D-BG.13** | **Cómo llega el fondo a un componente que lo renderiza desde dentro** (hero, y cualquier otro) — planteada por el autor 2026-08-17 (`<Hero background={struct}>`) | (a) **snippet** `background` en el componente; el app compone `<Background>…</Background>` dentro y el componente lo coloca (el hero ya tiene `backdrop` → se renombra); (b) prop `background={struct}` (árbol de datos que describe capas) | **FIRMADA 2026-08-17 — (a)**: snippet `background` (hero: `backdrop` → `background` en F5); ninguna API nueva. (b) rechazada: contradice la regla compositional-not-data-driven fijada por el autor (OnionMenu 2026-06-21: «los hijos son componentes reales, nunca `root={tree}`/`items={[...]}`»), B-5 de blocks y la regla 6 de eidos; además un struct no puede nombrar un efecto del pack sin canon→pack, y cada componente tendría que poseer su mapeo struct→capas |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **D-BG.16** | **Enmienda medida de la regla de anfitrión (D-BG.14)** — destapada en F1, 2026-08-17 | (a) la regla escribe también `--box-position: relative` (resuelve dentro del mecanismo de Box sin subir especificidad; un `position` por prop, inline, sigue ganando; `Dialog.Content` conserva su `fixed` — medido); (b) subir la especificidad de la regla (rompe `Dialog.Content`/`Affix`); (c) que Box deje de usar `revert-layer` para `position` (cambio de Box con radio catálogo) | **FIRMADA 2026-08-17 — (a)**: la regla queda `:where(:has(> [data-background])) { --box-position: relative; position: relative; isolation: isolate }`. Medido antes/después en Chrome: sin la var, Section y Box-columna computaban `static` y la pila se escapaba; con ella, 5/5 anfitriones cubiertos y `Dialog.Content` conserva su `fixed`. El acoplamiento foundation → token PÚBLICO de Box es el precio declarado (precedente estructural: la foundation ya escribe `--motion-stagger-index` sobre `[data-stagger] > *`); `:where` se mantiene, así que nada sube de especificidad |
uix(background): el media entra en la pila, y el fondo aprende a pararse F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
| **D-BG.17** | **Dónde vive el control de pausa que el app compone** — destapada en F2, 2026-08-17 | (a) hijo registrador: `<Background.Pause>` dentro de la pila no pinta ahí, registra sus props y `<Background>` lo coloca; (b) **snippet** `pause` en `<Background>`, pintado como segundo nodo raíz; (c) hermano de `<Background>` con `bind:pressed` | **FIRMADA 2026-08-17 — (b)**. La pila es `z-index: -1` + `pointer-events: none` y forma su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido del anfitrión y deja de ser un control — pero registrarse en el contexto exige ser descendiente. El snippet rompe el nudo con el único mecanismo que ya tiene precedente canónico (`Toggle.icon`, `Image.fallback`, y el propio D-BG.13): `<Background pause={…}>` recibe `{ paused, toggle }` y `<Background>` lo pinta donde la geometría funciona. Suministrar el snippet ES la composición explícita, así que suprime el default por la misma forma que `Switch` elige entre `bodyContent` y su `<SwitchThumb>` — sin prop booleana, que D-BG.4 prohíbe. (a) rechazada: inventa un mecanismo nuevo (un hijo que se re-parentiza) para un problema que un snippet resuelve; (c) rechazada: obliga al app a cablear el booleano y a una prop booleana para suprimir |
| **D-BG.18** | **Enmienda de D-BG.4: la FORMA del control de pausa** — destapada al comparar con el estado del arte, 2026-08-17 | (a) `IconButton` con `aria-label` que cambia play↔pause y SIN `aria-pressed` (el precedente propio: `MediaPlayer.PlayButton`); (b) `Toggle` con `aria-pressed` y etiqueta fija; (c) lo que decía D-BG.4: `Toggle` + `aria-pressed` + etiquetas que cambian | **FIRMADA 2026-08-17 — (a)**. (c) mezcla los dos patrones que el APG separa: etiqueta FIJA con `aria-pressed`, **o** etiqueta que CAMBIA sin él — nunca ambos, porque el lector anuncia dos veces el estado y se contradicen. El framework ya eligió en `MediaPlayer.PlayButton` (`IconButton` ghost, `aria-label` derivado del estado, sin `aria-pressed`), y coherencia C0–C10 manda que dos controles del mismo acto se comporten igual. Es además lo que hacen las dos referencias con equipo de accesibilidad (Apple y Microsoft ponen un botón persistente de pausa/reproducir en la esquina de cada hero animado; ninguna librería de componentes trae fondo de vídeo, y las colecciones copy-paste que sí — shadcn.io, `next-video` — lo sirven `autoplay` SIN control). Consecuencias: el morfo describe la parte como botón compuesto, no como Toggle; el control es persistente (jamás sólo-hover) y va ANTES del contenido en orden de DOM, que es el orden que el APG fija para el control de rotación de un carrusel |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **D-BG.14** | **Colocación: hijo, sin `Box`** — decidido en conversación 2026-08-17 (autor: «componer una capa Box trastoca todo»; «¿no puede renderizar desde el padre?») | (a) `Background` = la PILA como hijo del padre; el padre se vuelve anfitrión por la regla de foundation `:where(:has(> [data-background])) { position: relative; isolation: isolate }` (+ gemelo `data-on`); pausa como segundo nodo raíz; cero props en `Box`; (b) igual pero SIN regla `:has` (el consumidor posiciona/aísla el padre a mano, estilo Mantine `Overlay`); (c) `Background` compone `Box` (el plan original) | **FIRMADA 2026-08-17 — (a)**: la pila como hijo (`absolute; inset:0; z:-1; border-radius: inherit; overflow: clip; pointer-events: none`); adopción del anfitrión por regla de FOUNDATION en el generador (`:where(:has(> [data-background])) { position: relative; isolation: isolate }`) + gemelo `data-on` como segundo selector del bloque D12 (test en `generated-css`); pausa = segundo nodo raíz; `Background` sin `BoxProps` ni `rounded`; **cero props nuevas en `Box`**. **Enmendada por D-BG.16** (la regla escribe además `--box-position: relative`; medido). `display: contents` = no-anfitrión (documentado). Verificación F1 en Chrome real: Section · Box-columna · Card rounded×shape · Dialog.Content. (b) rechazada: footgun «las capas desaparecen»; (c) rechazada: duplica la API de Box y contradice «one axis, one primitive» |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
---
## 7. Plan por fases (cada una con lectura, producto y gate)
| Fase | Producto | Leer antes | Gate |
| --- | --- | --- | --- |
| **F0 · Firma + infra** | D-BG.1–14 firmadas · `$adom` `ScrollProgress` (+ test happy-dom, patrón `is-in-viewport.svelte.test.ts`) y `prefersReducedData` (+ README adom Backlog) · morfo `background.ts` (3 partes, `texts`, cabecera con la justificación pasiva y la regla de attrs de wrapper) + `langs/components/background.ts` · recipe tokens `background-*` en `lib/recipes/base.ts` · **regla de adopción del anfitrión + gemelo `data-on` en el generador** (`render-css.ts`, junto a `renderOnContextBlocks`; test en `generated-css`/`active-eidos-config`) · `generate:eidos-css` | `morfo.md` §Authoring · `canon/tsc.md` · `recipe-contract.md` §1 · `arts/adom/README.md` · `render-css.ts` (`renderOnContextBlocks`, `renderSharedPaletteLayer`) | `validateMorfo` · `npm run translations:check` · `recipe-css-contract` · `generated-css` · `npm run check` · suites adom verdes |
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **F1 · Núcleo** | `Background` root = la pila (hijo, sin Box — D-BG.14) · `Layer` · `Pattern` (4 migrados + lines/noise/rings/vignette) · `Gradient` · `Scrim` · `on` (gemelo `:has`) · contexto · hero: `Backdrop`→`Background.Pattern` (el renombre del snippet `backdrop`→`background` NO es de esta fase: D-BG.13 lo firmó para F5) · borrado de `backdrop/` (tras tu orden) · verificación previa del `Box flex/grow` (bug hero 2026-07-23; si persiste, arreglo en `Box` primero — **persistió: arreglado en F1.5.a**) · enmienda D-BG.16 (`--box-position` en la regla de anfitrión) | `eidos/components/README.md` (7 reglas + comparativa obligatoria) · `gradient-finish.md` §10 (fronteras) · `backdrop.css` (migración literal) · `box/README.md` + `card/README.md` + `dialog/dialog.css` (qué `position` propio traen los anfitriones habituales) | `component-api-contract` · `component-visual-attrs` · `eidos-lint` invalid 0 · `rtl:check` · `blocks:check` · verificación en Chrome REAL (el pane oculto suspende rAF) de CUATRO anfitriones: `Section`, **`Box`-columna de Grid/Flex**, **Card `rounded` × shape**, **`Dialog.Content`** (portal + `position: fixed` propio) |
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
| **F2 · Media** | `Image` (compone `<Image>`, `sources` por modo, prioridad) · `Video` (políticas: IO-pause, doc-hidden, reduce-motion, reduce-data, `paused`) · `Pause` (Toggle; regla D-BG.4) · hero layout `background` refactorizado (fuera `<style>` justified + scrim inline + `data-on` manual) | `image/README.md` + `image/types.ts` · `toggle/README.md` · WCAG 2.2.2 · `hero/README.md` (huecos medidos) | audit `--only background` · `blocks:check` · contraste medido en píxeles (método hero) · teclado: el control de pausa alcanzable y anunciado · reduce-motion probado en Chrome |
| **F3 · Parallax** | scroll (`animation-timeline: view()` + fallback `ScrollProgress`→`--background-progress`) · `speed`/`bleed`/`--background-parallax-travel` · puntero (`depth`, `--background-pointer-*`) · `spotlight` · `attach='fixed'` · reduce=static | `motion.md` §2/§10 · `motion-guide.md` §8 · `theming/reference.md` §14 (draggable lift: `translate` ≠ `transform`) · MDN scroll-driven · memoria `will-change` jitter | `@supports` en ambos caminos (Chrome con/sin flag) · `uix.perf` reflowDetector 0 violaciones · 60 fps medido · RTL: puntero físico, scroll vertical |
| **F4 · Demo + README + docs** | `web/routes/uix/components/background/+page.svelte` (v2 9 tabs LOCKED; Sema en vacío justificado; chips = uniones completas) · README (Baseline: Backdrop + hero background + semillas · Comparativa ≥3 · Decisiones · Gaps · Passive justification · Audit exceptions) · `next-features.md` §11 · nota en `design-text-effects.md` · `PLAN-blocks-quality.md` Q0.3 · Surface README (gap scrim → cerrado) | `demo-authoring.md` (entera) · `completion-checklist.md` | `component:audit --only background` **PASS 0 errores** · `morfo:check` · `SMOKE_SCOPE=/uix/components/background npm run smoke` · `docs:check` |
| **F5 · Rollout tier** | cta · stats-band · testimonials · feature-split (banda por fila) · banner · site-footer con `Background.*` (columna B de PLAN-blocks-quality §3) · página compuesta | `AUDIT-blocks-ledger.md` · cada README de block | `blocks:check` · ledger actualizado · comparación al lado de la referencia (la vara de `feedback_blocks_must_surpass`) |
| **F6 · Condicionado** | `Ambient` lee el contexto (pack) · ScrollFrames migra a `ScrollProgress` · `Gradient animate` variantes · patrones open-cage por config | — | sólo con consumidor real |
Orden fijo: **F0 → F1 → F2 → F3 → F4 → F5**; F4 puede solaparse con F2/F3
(la demo se construye con el componente). Nada de F1 empieza sin D-BG.1/2
firmadas; nada de F3 sin D-BG.3; F2 depende de D-BG.4/5.
### 7.bis Hallazgos de ejecución de F0 (2026-08-17) — medidos, no supuestos
Tres cosas que el plan no podía saber hasta tocar el árbol. Se registran aquí
porque son deuda de ESTE documento, no del código.
1. **F0.4 (tokens `background-*`) es IMPOSIBLE en F0 y se ejecuta en F1.** No
es un recorte: los dos guards que el propio F0 declara como gate lo
prohíben. Medido declarando un bloque `background: { 'parallax-travel': … }`
en `lib/recipes/base.ts` y corriendo `recipe-css-contract`:
- `does not declare recipes for missing component CSS files` →
`expected [ 'background' ] to deeply equal []` (el guard exige
`components/background/background.css`, producto de F1);
- `does not leave declared public recipe variables orphaned` → el token no
lo consume nadie hasta que exista la receta.
Y un tercero espera detrás: `loads every component CSS recipe exactly once`
exige que ese `.css` lo auto-importe `background.svelte` — el wrapper,
también de F1. **Los tokens viajan con la receta que los consume**; la
sonda se revirtió sin dejar rastro (`git diff` vacío en `base.ts`).
2. **La parte `pause` es el BOTÓN, no un contenedor de colocación.** La tabla
de §4 dice «contenedor de colocación (patrón Fab-sobre-Button)», pero el
patrón que cita hace lo contrario: `morfo/components/fab.ts:31-33` declara
*«Structurally a `<Button>`: the same `<button>` carries `data-button` +
`data-fab`»*. Y el archetype importa: `[data-archetype='action']` arrastra
`cursor: pointer` + `user-select: none` (`archetypes.css:169-172`), el
anillo de foco y un suelo de 44px bajo `pointer: coarse`
(`archetypes.css:293-301`) — sobre un `div` de colocación eso es la clase
de defecto que `reference_archetype_item_pulls_interactive_styling` fichó.
El morfo declara `defaultElement: 'button'` + `archetype: 'action'`: UN
nodo, el `Toggle` compuesto llevando ambos markers. La colocación absoluta
vivirá en el recipe sobre ese mismo nodo, como en `Fab`.
3. **«test happy-dom» (F0 / D-BG.7) no existe como proyecto.**
`vite.config.ts:73-107` define dos: `client` (Playwright chromium,
`*.svelte.{test,spec}.ts`) y `server` (node). Los helpers con runas de
`$adom` se prueban TODOS en `client` (`is-in-viewport.svelte.test.ts` y sus
once hermanos). Los dos tests nuevos siguen esa convención.
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
### 7.ter Supervisión de F1 (Fable, 2026-08-17) — veredicto y correcciones
**Estado de F1 al evaluarla**: los cinco wrappers, el recipe, los tokens, el
README y la migración del hero existen y pasan los gates de contrato
(`recipe-css-contract` · `component-api-contract` · `component-visual-attrs` ·
`eidos-lint` invalid 0 · `rtl:check` · `blocks:check` · `docs:check` · `check`
0 errores propios) y la verificación en Chrome de CINCO anfitriones (Section ·
Box-columna · Card `rounded`×shape · panel `fixed` · `Dialog.Content` real).
Sin commitear. Cinco desviaciones/hallazgos, ninguna oculta — todas están en la
entrega del constructor o las destapó el supervisor al re-ejecutar:
1. **La regla firmada en D-BG.14 no bastaba y el constructor la amplió** con
`--box-position: relative` (medido: los anfitriones basados en Box
computaban `static` porque `box.css` declara `position: var(--box-position,
revert-layer)` a (0,1,0), y sin `@layer` `revert-layer` cae al UA — la
trampa que `affix/types.ts:19` ya documentaba). La ampliación es correcta y
está medida, pero es una ENMIENDA del texto firmado y acopla la foundation
a un token público de Box → **D-BG.16**, a firmar.
2. **El bug de `Box flex` existe, está diagnosticado y NO arreglado.** F1
firmaba «si persiste, arreglo en Box PRIMERO»; el constructor lo reprodujo
(`<Box flex={2}>` → `0 1 auto`, 71px de 992) y diagnosticó el mecanismo
(`box.css:268-271`: el shorthand `flex:` va ANTES de los longhands, que
revierten al UA cuando su var está sin poner y borran su aportación;
inyectando `--box-grow: 2` en el mismo nodo → `2 1 auto`, 823px), pero se
detuvo por §10.3 («cero cambios en recipes ajenos») y lo escaló. Conflicto
entre dos textos del plan; el firmado (D-BG.12→F1) manda: **se arregla en
F1.5**, con guard.
3. **`Scrim` sin `color` hereda la paleta del anfitrión** (por semántica CSS,
no medido aún): `--palette-*` NO están registradas `inherits:false` (0
`@property --palette-` en `generated/base.css`) y la capa compartida las
pone en `[data-color='x']` — un `<Card color="teal">` que hospede un
`<Background.Scrim>` sin `color` le pasa `--palette-solid` por herencia y
la tinta del velo deja de ser `--color-overlay`. Es exactamente el «nesting
gap» que THM-2 cierra con una guarda de PRESENCIA (`[data-{c}][data-color],
[data-{c}][data-color-custom]`, ver el forward de `card` en
`generated/base.css`). `Pattern` no lo sufre porque SIEMPRE estampa
`data-color` (default `primary`), pero merece la misma guarda.
4. **El audit clasifica `background` como INTERACTIVE** (la parte `pause`
declara `role: 'button'` + `defaultElement: 'button'`; heurística de
`component-audit.ts:1471-1477`) → pide `apg` (A-1.4) y un tratamiento de
foco (R-1.5). El precedente exacto es `fab` (mismo caso, PASS): `apg` del
patrón button + `## Audit exceptions` con `R-1.5 exception:` (el foco es del
`Toggle` compuesto) + `## Sema events` («0 eventos propios; interactivo POR
COMPOSICIÓN»). Falta añadirlos.
5. **Docs desincronizadas por la migración**: el README del hero sigue diciendo
`Backdrop` en el mapa de composición (línea 18) y en la fila `backdrop`
(27); y la fila F1 de §7 pedía renombrar el snippet `backdrop`→`background`
mientras D-BG.13 lo firmó para F5 — el constructor siguió el texto FIRMADO
(bien), pero la fila de §7 queda corregida aquí: **el renombrado es de F5**.
Fuera de desviación, anotado: `[data-background-layer] > :where(img, video)`
(cover-fit del media del app) entró en F1 sin estar en su fila — es el receptor
del `<style>` justificado del hero y F2 lo consume; se acepta y se registra.
uix(background): el media entra en la pila, y el fondo aprende a pararse F2 de PLAN-background.md. Entran las tres piezas que le faltaban al anfitrión: `Image` (compone el `<Image>` canónico, una fuente por modo de tema, `priority` para el caso LCP), `Video` (con las cinco políticas que deciden si suena una sola trama) y `Pause`, el control que WCAG 2.2.2 le debe al lector cuando algo se mueve solo. Ninguna de las cinco políticas del vídeo es del consumidor: fuera de vista, pestaña oculta, movimiento reducido, datos reducidos y la pausa. El elemento se gobierna con `play()`/`pause()` y no con `autoplay` porque cuatro de las cinco son estado que va y vuelve, y el atributo es un disparo único al parsear. El hero se queda SIN CSS. Su layout `background` era tres capas `Box` a mano, un scrim inline y una hoja con marcador `/* justified: */` para el cover-fit del media; las tres cosas son ya canon, así que el bloque pierde su única excepción D-BLK.2. También pierde el `data-on` manual: `<Background on="dark">` lo estampa en la pila y el gemelo de foundation se lo da al anfitrión — medido, el título y la bajada resuelven `rgb(255,255,255)` sin que el bloque lo recuerde. Tres decisiones que la ejecución obligó a tomar, todas firmadas: - **D-BG.17 — el control explícito viaja por un snippet.** La pila es `z-index: -1` con `pointer-events: none` y su propio contexto de apilamiento: cualquier control escrito DENTRO pinta detrás del contenido y deja de ser un control, pero registrarse en el contexto exige ser descendiente. El snippet `pause` rompe el nudo con el único mecanismo que ya tenía precedente (`Toggle.icon`, `Image.fallback`), y suministrarlo suprime el default igual que `Switch` elige entre su snippet y su propio thumb. - **D-BG.18 — la etiqueta CAMBIA y no hay `aria-pressed`.** El APG ofrece dos formas de nombrar un control de dos estados y hacer las dos anuncia el estado por partida doble, en dos lecturas que se contradicen. Es la forma que `MediaPlayer.PlayButton` ya usa para el mismo acto. - **D-BG.15 completa.** Un fondo no reporta su fallo: se aparta y la capa de debajo ES la composición. Pero si la capa todavía tiene algo que enseñar —un snippet `error` del app, o el póster de un vídeo— la capa SE QUEDA (`data-has-fallback`). Sin eso los snippets eran inalcanzables por construcción, y un `<video>` fallido se llevaba por delante el póster que la decisión promete conservar. Y tres defectos que sólo aparecieron al medirlos en Chrome: - **`effect_update_depth_exceeded`**: una capa que se registraba desde un `$effect` escribía el contador del padre en fase de efectos y el flush no cerraba. No era ruido — mataba el efecto raíz: el botón se pintaba y todo clic posterior en la superficie se ignoraba en silencio. La cura es la ley que el framework ya tenía escrita, A30 (`anchor-nav-provider.svelte.ts`): registrar desde el init, nunca desde un efecto reactivo. Bisecado: ni `untrack` solo ni mover la lectura a otro componente lo arreglaban. - **El control caía 18px fuera del anfitrión**: el `<Button>` compuesto declara `position: relative` en `[data-button]`, misma especificidad que un `[data-background-pause]` pelado, así que decidía el orden de hojas del bundler. Fijado a dos atributos. - **Un `<img>` pelado no se estiraba** dentro de una capa. La capa es ahora una rejilla de una celda —así cualquier hijo la llena sin que la receta escriba tamaños sobre contenido ajeno— y el media se dimensiona aparte, porque `stretch` no aplica a elementos reemplazados. Medido en Chrome real: el control por defecto y el de snippet alternan etiqueta, escriben `data-paused` y congelan de verdad la deriva; sin capa que se mueva no hay control; una imagen rota oculta su capa y el patrón de debajo sigue pintando; un vídeo roto conserva su póster; `Surface`, `<Image>` compuesto, `<img>` pelado y el backdrop real del hero llenan su capa. Queda por verificar con Chrome visible: el vídeo REPRODUCIÉNDOSE y las políticas de movimiento/datos reducidos. En un panel oculto el IntersectionObserver está suspendido y las media features no se pueden emular, así que las políticas 1 y 2 se comprobaron RETENIÉNDOLO y las otras no se dan por probadas. Dos decisiones del autor quedan abiertas en el README (§Gaps): la escala `strength` del scrim no está ordenada por peso (`subtle` 0.80 vela más que `overlay` 0.65, y `overlay` ≡ `muted`), y sobre una foto clara ningún peso llega a AA — 2.10:1 el default, 4.42:1 el más fuerte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.quater F2 ejecutada (2026-08-17) — medido en Chrome real
Entregado: `Background.Image` (compone `<Image>`, `sources` por modo, `priority`,
sin `alt` porque la capa es `aria-hidden`), `Background.Video` (las cinco
políticas), `Background.Pause` (D-BG.18), el snippet `pause` (D-BG.17), la
receta y sus dos tokens, y el layout `background` del hero refactorizado — el
bloque **se queda sin `<style>`**: su única excepción D-BLK.2 (el cover-fit) es
ya canon, y el `data-on` manual sobra porque el gemelo de foundation se lo da al
anfitrión (medido: título y bajada resuelven `rgb(255,255,255)` sin él).
**Tres defectos destapados por la verificación, los tres arreglados:**
1. **`effect_update_depth_exceeded`** — una capa que se registraba desde un
`$effect` escribía el contador del padre en fase de efectos, invalidaba al
hermano que lo lee y el flush no cerraba. No sólo hacía ruido: **mataba el
efecto raíz**, así que el botón se pintaba y todo clic posterior en la
superficie se ignoraba en silencio. Arreglado aplicando la ley que el propio
framework ya tenía escrita, **A30** (`anchor-nav-provider.svelte.ts:204`:
«register with the parent from the constructor, not a reactive effect»), más
`untrack` en la mutación y la decisión leída desde un componente aparte.
2. **El control de pausa computaba `position: relative`** y caía 18px fuera del
anfitrión: el `<Button>` compuesto declara `position: relative` en
`[data-button]`, misma (0,1,0), así que decidía el orden de hojas del
bundler. Clavado con `[data-background-pause][data-placement]` — (0,2,0), sin
`!important` ni envoltorio. Medido después: `absolute`, 12px/12px, dentro.
3. **Mi `size='sm'` inventado** en el control: retirado, se queda el del canon
(`md`, 36×36), que es el par que compone `MediaPlayer.PlayButton`.
**Comprobado en navegador**: control por defecto y de snippet alternan etiqueta
ES pausa↔reanudar, escriben `data-paused` y **congelan de verdad** la deriva
(`animation-play-state: paused`) · sin capa que se mueva no hay control · una
imagen rota oculta su capa (`data-status='error'`) y el patrón de debajo sigue
pintando · la buena carga con `object-fit: cover` a 192px · el control es
`<button type="button">`, enfocable, fuera de todo `aria-hidden` · el anfitrión
del hero es `relative` y la tinta sale a `rgb(255,255,255)`.
**DOS decisiones tuyas, medidas, sin tocar** (README §Gaps):
- **La escala de `strength` del scrim no está ordenada por peso**: `ghost` 0.30 ·
`scrim` 0.45 · `overlay` 0.65 · `muted` **0.65 (duplicado)** · `subtle`
**0.80 — el más pesado de todos, y su nombre dice lo contrario**. La causa es
que esos tokens nombran cuán opaco es un ELEMENTO, no cuánto vela un scrim.
Renombrar o renumerar es cambio de API, no arreglo de pasada.
- **Sobre una foto CLARA ningún peso llega a AA**: 2.10:1 el default, 4.42:1 el
más fuerte, con texto blanco. O entra un peso por encima de `--opacity-subtle`,
o se doctrina que la respuesta es el scrim graduado.
Gates: audit `--only background` **PASS 0/0** · eidos-lint invalid 0 ·
`blocks:check` 0/15 · `rtl:check` 0/180 · 441/442 en eidos+morfo (el fallo es el
`skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio.
Sin commitear: falta tu orden.
uix(background): el fondo se mueve con el scroll, y con lo que el lector no pidió no F3 (parallax) + F4 (demo y registro documental) + las correcciones de sus dos auditorías, en un commit porque viven en los mismos ficheros: la demo enseña los ejes que F3 añade, y separarlas dejaría un estado que nunca se probó. Cinco maneras de que una capa deje de estarse quieta: `speed` (cuánto del token de travel cubre mientras el anfitrión cruza el viewport), `bleed` (crece más allá del anfitrión para que el viaje no arrastre su propio borde), `depth` (la deriva contra el puntero), `spotlight` y `attach='fixed'`. El travel es CSS: `animation-timeline: view()` lo gobierna desde la posición de scroll, sin listener ni rAF. Sólo donde el motor no lo trae, la pila arranca `ScrollProgress` y escribe `--background-progress`, que la rama `@supports not` mete en la MISMA declaración; los dos caminos no pueden estar vivos a la vez porque el JS comprueba la condición idéntica con `CSS.supports`, y ambos se paran bajo `prefers-reduced-motion` — el parallax es movimiento atado al scroll del propio lector, que es justo la clase que provoca síntomas vestibulares. **La decisión que no estaba prevista.** El scroll y el puntero quieren mover la MISMA capa, y una animación sobre `translate` gana a cualquier declaración estática: el puntero habría dejado de existir sin más. Así que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría a saltos) y un único `translate` compone los dos términos. Medido: parallax solo → `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es `translate` y nunca el shorthand `transform`, la misma ley que sigue el lift del draggable con `scale`. El precio, dicho porque en la primera redacción escribí lo contrario tres veces: una custom property NO se puede compositar, así que el navegador recalcula estilo cada frame. Para una decoración es el intercambio correcto —una property por capa que viaja, ninguna bajo reduced motion— pero «va en el compositor» era falso y ahora el código dice lo que ocurre. **Dos footguns cerrados por forma, no por disciplina:** - `attach='fixed'` se DECLARA desde la capa y la pila se recorta sola. Antes había que escribirlo también en la pila, y olvidarlo dejaba la capa `position: fixed` pintando a sangre por todo el viewport, detrás de todo y sin error (un hijo fijo se escapa de `overflow: clip`; sólo un `clip-path` lo trae de vuelta). La prop de la pila desaparece: no hay nada que olvidar. - Un `speed` negativo —una capa que se mueve contra el scroll— invertía el bleed: la capa ENCOGÍA y enseñaba justo los bordes que el bleed tapa. Ahora usa la magnitud. **Lo que costó medición**: el shorthand `animation` pone `duration: 0s` y una línea de tiempo de progreso necesita el `auto` inicial, así que con el shorthand la capa no se movía nunca (van longhands, con el porqué escrito) · mi listener de puntero pedía un frame y no lo liberaba si el rect salía degenerado, matando el puntero para el resto de la sesión (reescrito sin frame, con el rect cacheado e invalidado por `pointerenter` y `observeResize`) · las cuatro registraciones —`animated`, `pointer`, `scroll`, `fixed`— comparten un solo sitio, `declare.svelte.ts`, donde vive la regla A30 y su segunda mitad: registrar desde el init, y seguir el prop sin escribir en la primera pasada. **La demo** (`/uix/components/background`, v2, nueve pestañas) monta un ANFITRIÓN de verdad en el escenario, porque este componente es invisible por sí solo y sin padre no se puede enseñar lo único que importa: que el padre se adopta y el layout no se mueve. Los chips son uniones completas verificadas por el TIPO (`Record<Union, 0>`): un miembro que falte es error de compilación. Y fue la demo la que destapó que, con A30, encender `animate` en caliente no hacía aparecer el control de pausa — el registro era un hecho de montaje. Invisible en una sonda, obvio con un interruptor. Registro documental (D-BG.11): `next-features.md` §11 · la frase en `design-text-effects.md` (el mismo corte canon/pack leído desde el otro lado) · `PLAN-blocks-quality.md` Q0.3 → sucesor · `surface/README.md` §Gaps «scrim de autoría» CERRADO por `Background.Scrim` · glosario con entrada `Background` y `Aura` corregida (decía «Not built yet» y está construido) · y en `motion-guide.md` §8 + el RFC: el travel ligado al scroll no es un preset —un preset nombra una transición discreta CON duración, y esto es modulación continua sin ninguna— y sólo se replantea como dominio con un segundo consumidor. Verificado en Chrome real: el puntero mueve `depth` y `spotlight` con los valores exactos y vuelven al centro al salir · `attach='fixed'` estampa y retira el recorte de la pila · el bleed aguanta el speed negativo · RTL: el `translate` del puntero se mantiene FÍSICO y el bleed en el eje de bloque · cada control de la demo cambia algo (los de `spotlight` y `depth` no llegaban a tres de las cuatro clases de capa hasta la segunda auditoría). ⚠️ SIN VERIFICAR, y no lo doy por bueno: el travel real al hacer scroll, los 60 fps y el detector de reflow. El panel del navegador va oculto con viewport 0×0 y ahí las animaciones scroll-driven declaradas en CSS no se activan — comprobado que es del ENTORNO con un caso mínimo inyectado (un `div` pelado con `animation-timeline: view()` sale inactivo mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca 68%). Necesita una pasada con Chrome visible. Gates: audit `--only background` PASS 0 errores · eidos-lint invalid 0 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS · smoke PASS · 441/442 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.quinquies F4 ejecutada (2026-08-18)
**La demo** (`web/routes/uix/components/background/+page.svelte`) sigue el v2 de
`demo-authoring.md`: las 9 pestañas, el harness compartido sin inventar paneles,
y la entrada en el nav bajo **Layout**, junto a `Section` — su anfitrión
canónico (ni `Surface` ni `Backdrop` tenían demo, así que no había precedente).
Dos cosas que este componente obliga a hacer distinto, y ambas son el punto:
- **El escenario muestra un ANFITRIÓN de verdad** (una `Section` con copia
encima), no el componente aislado. Background es invisible por sí solo: sin
anfitrión la vista previa sería un rectángulo vacío y lo que hay que enseñar
—que el padre se ADOPTA y el layout no se mueve— sería intestable.
- **Los chips son uniones completas por el TIPO, no a mano.** Cada fila se
deriva de un `Record<Union, 0>`: un miembro que falte es error de compilación
y uno inventado también, así que una fila de chips no puede derivar en
silencio del tipo que enseña.
**Un defecto que sólo la demo podía destapar, arreglado.** Con A30 el registro
de «esta capa se mueve» era un hecho de MONTAJE, así que cambiar `animate` en
caliente no hacía aparecer el control de pausa — invisible en una sonda, obvio
en cuanto hay un interruptor. `background-gradient.svelte` registra ahora al
init Y sigue el prop después, con un efecto que **no escribe nada en su primera
pasada**: el flush de montaje sigue limpio y un cambio posterior es una
escritura suelta en un grafo ya asentado. Medido en Chrome: el control aparece
al encender `animate`, sin `effect_update_depth_exceeded`.
**Las entradas documentales de D-BG.11**: (1) `next-features.md` §11 · (2) la
frase en `decisions/design-text-effects.md` (el mismo corte canon/pack leído
desde el otro lado) · (3) `PLAN-blocks-quality.md` Q0.3 → sucesor · (4)
`surface/README.md` §Gaps «scrim de autoría» → **CERRADO** por
`Background.Scrim` · (6) `glossary.md`: entrada `Background` **y** `Aura`
corregida — decía «Not built yet» y está construido, con sus familias
`delegate`/`sustain`.
**(5) queda DIFERIDA a F3 a propósito**: la nota de `motion-guide.md` §8 y la de
`MOTION_SERVICE_RFC` describen el parallax (D-BG.3), que no existe todavía. La
propia D-BG.11 lo prohíbe — «una nota que describe futuro es la deriva de
`packs.md`/`glossary.md` con Aura», que es exactamente el error que esta fase
acaba de corregir en el glosario. Se escriben cuando el parallax aterrice.
Gates: `component:audit --only background` **PASS 0 errores** (2 warnings
`D-1.5`/`D-4.3` idénticos a los del canario `button` — el harness v2 encapsula
el `MutationObserver` y el emit, así que el grep literal del audit no los ve) ·
`morfo:check` **PASS background** · `SMOKE_SCOPE=/uix/components/background`
**PASS** · `docs:check` **0 errores, 0 warnings** en 633 docs.
### 7.sexies F3 ejecutada (2026-08-18) — parallax, puntero, `attach`
Entregado sobre D-BG.3 (A) y D-BG.9, sin tocar `$motion` ni el registro de
presets: `speed` · `bleed` · `depth` · `spotlight` · `attach='fixed'`, el
fallback `--background-progress`, y los tokens `--background-parallax-travel` /
`--background-spotlight-*`. La demo expone los cuatro ejes.
**La decisión de diseño que no estaba prevista.** Dos ejes quieren mover la
MISMA capa —el scroll y el puntero— y una animación sobre `translate` gana a
cualquier declaración estática: el puntero habría dejado de existir sin más. Así
que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría
a saltos) y un único `translate` compone los dos términos. Medido: parallax solo
→ `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es
`translate`, nunca el shorthand `transform`, que es la misma ley que sigue el
lift del draggable con `scale`.
**Tres cosas que costaron medición:**
1. El shorthand `animation` pone `animation-duration: 0s`, y una línea de tiempo
de progreso necesita el `auto` inicial para mapear su intervalo sobre el
rango de scroll. Con `0s` la animación es instantánea y la capa no se mueve
nunca. Van longhands, y el porqué queda escrito en la receta.
2. Mi listener de puntero pedía un frame y, si el rect salía degenerado, **no lo
liberaba nunca**: un anfitrión sin caja en el primer movimiento (transición,
cambio de display) mataba el puntero para siempre. Reescrito sin frame: el
rect se CACHEA (invalidado por `pointerenter` y `observeResize`), la lectura
sale del camino caliente y el fallo desaparece por forma.
3. Las cuatro registraciones (`animated`/`pointer`/`scroll`/`fixed`) comparten
un solo sitio (`declare.svelte.ts`) con la regla A30 y su segunda mitad
—seguir el prop sin escribir en la primera pasada—, en vez de una copia del
baile por fichero.
**Verificado en Chrome real**: el puntero mueve `depth` y `spotlight` con los
valores exactos y ambos vuelven al centro al salir · `attach='fixed'` da
`position: fixed` en la capa y `clip-path: inset(0)` en la pila, y una pila sin
`attach` conserva su `overflow: clip` sin tocar · **RTL**: el `translate` del
puntero se mantiene FÍSICO (no espeja, que es lo correcto para un dispositivo
físico) y el bleed vive en el eje de bloque · la demo estampa `data-parallax`,
`--_background-speed`, `animation-timeline: view()` y el bleed al cambiar los
chips, con paridad en el snippet.
⚠️ **Lo que NO pude verificar, y por qué**: el panel del navegador va oculto y
con viewport 0×0, así que **las animaciones scroll-driven declaradas en CSS no
se activan** ahí — comprobado que es del ENTORNO y no del código con un caso
mínimo inyectado (un `div` pelado con `animation-timeline: view()` también sale
inactivo, mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca
68%). Quedan por medir con Chrome visible: el travel real al hacer scroll, los
60 fps, el detector de reflow, y la rama `@supports not` (Chrome la soporta, así
que no se puede ejercitar aquí). Lo verificable —la composición de los dos ejes,
el puntero, `attach`, RTL— está medido arriba.
Gates: audit **PASS 0 errores** · eidos-lint invalid 0 · `rtl:check` 0/180 ·
`docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS ·
`smoke` PASS · 441/442 (el fallo es el `skin-media-player` de siempre) ·
`check` 0 errores propios · prettier limpio.
### 7.septies F3.5 — correcciones de la segunda auditoría (2026-08-18)
Nueve hallazgos, dos de diseño. Ninguno cambia el mecanismo firmado; cambian
quién declara qué, y que lo escrito sea verdad.
**Diseño:**
1. **`attach='fixed'` era un footgun**: había que escribirlo en la capa Y en la
pila, y olvidar el segundo dejaba la capa `position: fixed` **pintando a
sangre por todo el viewport**, detrás de todo, sin error. Ahora la capa lo
DECLARA (`registerFixed`) y la pila se recorta sola — la prop de la pila
desaparece, así que no hay nada que olvidar. La forma CSS de D-BG.9 no
cambia; cambia quién pone el atributo. Medido: al poner `fixed` la pila
estampa `data-attach` y `clip-path: inset(0)`, y al volver a `scroll` los
retira.
2. **Un `speed` negativo invertía el bleed.** El bleed por defecto ES el travel,
y con travel negativo el `inset-block` salía positivo: la capa ENCOGÍA y
enseñaba justo los bordes que el bleed existe para tapar. Ahora usa la
magnitud (`max(t, -t)`). Medido: `-64px` con speed 1 y con speed −1.
**Verdad de lo escrito** (lo más importante de esta pasada): afirmé tres veces
—README, wrapper y plan— que el travel «va en el compositor / nada en el hilo
principal». **Es falso**: animar una custom property REGISTRADA no se puede
compositar, el navegador recalcula estilo cada frame. Es el precio de componer
los dos ejes en un `translate`, y para una decoración es el intercambio correcto
— pero había que decirlo, no lo contrario. Corregidas las tres frases y el
comentario del `will-change`, que se apoyaba en la premisa falsa. También decía
que el puntero iba «coalescido en un frame» cuando yo mismo había quitado ese
frame, y que las registraciones compartían un sitio cuando gradient y video
seguían con su copia — ahora **sí** lo comparten las cuatro.
**Menores:** el fallback JS se gatea por reduced-motion (el CSS ya se paraba
solo, el JS seguía midiendo) · el spotlight gana `--background-spotlight-color`
con guarda de paleta, como el scrim · documentados los cuatro límites (`speed`
muere bajo `attach='fixed'`; el `clip-path` cuadra las esquinas de una capa fija
en un anfitrión redondeado; `Dialog.Content` no es anfitrión de ese modo; el
fallback mide contra la ventana y `view()` contra el scroller más cercano) y las
vars de runtime `--background-pointer-*` / `--background-progress`.
**Demo:** `spotlight` y `depth` llegan ya a las cuatro clases de capa (antes el
interruptor **no hacía nada** con gradiente, imagen y vídeo), entra el control
`bleed`, las tablas API y Recipe dejan de mentir —fila de ejes compartidos y los
selectores de parallax/attach— y el snippet `pause` pierde el `onclick` que el
componente pisaba y un `aria-pressed` inerte.
Gates: audit **PASS 0** · eidos-lint invalid 0 · rtl 0/180 · docs 0/0 en 634 ·
blocks 0/18 · smoke PASS · 441/442 · `check` 0 propios · prettier limpio.
blocks(background): seis fondos, y el que parecía mejor era el que no se podía leer F5 de PLAN-background.md: los seis blocks de D-BG.10 (2) reciben `Background.*`. El hero cayó en F2 y la página compuesta ya existía — no había que construirla, había que verificarla con la decoración puesta. La forma es uniforme y tiene un solo desvío: cada block gana un prop `decor` que monta `<Background.Pattern>` como HIJA de su `Section`, la misma composición que el hero ya probó. El desvío es `cta`, donde la decoración va dentro del PANEL, que es donde está el ojo. **Dos hallazgos que sólo la medición podía dar.** Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular bajo AA: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a **2,27:1**. Pintan un lavado claro anclado en `50% 0%` — exactamente donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen, y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1 contra el panel. Por eso el default es `rings`, que además irradia del mismo anclaje superior que usaba el glow. Las tintadas se ofrecen igual: el app decide, pero ya con el número delante, en el tipo y en el README. Y una trama tintada dentro del panel era invisible por construcción: `--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que pinta el panel — un halo del color del panel sobre el color del panel. El block pasa ahora `color="var(--color-content-on-solid)"`, el token que ya usa para su propia copia; medido después, el tinte resuelve a `#ffffff`. **Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band` `grid`, porque una banda de cifras se lee como medida y la rejilla es la trama que lo dice sin decorar por decorar · `testimonials` y `feature-split` `glow`. `site-footer` y `banner` reciben la capacidad APAGADA: la columna B del plan de calidad no pide acabado en ninguno de los dos, y encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió. **Un desvío de alcance, declarado.** La columna B pedía «`Backdrop` alterno» POR FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría a cada `Row` a poseer el estado de alternancia, y eso es coordinación — un block que coordina deja de ser un block que no posee nada. Medido en Chrome sobre la página compuesta real: cinco pilas, los cinco anfitriones adoptados (`position: relative` + `isolation: isolate`) sin que ningún block los posicione; la copia mide 15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y 12,37:1 con `grid`/`dots`. Ledger: seis filas nuevas, A-100…A-105, cada una con su mecanismo y su evidencia. Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 docs · 434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.octies F5 ejecutada (2026-08-18) — rollout al tier
Los seis blocks de D-BG.10 (2) reciben `Background.*`. El (1) —el hero— aterrizó
en F2, y el (3) —la página compuesta— **ya existía** (`/blocks/landing`): no
había que construirla, había que verificarla con la decoración puesta, y eso es
lo que se hizo.
**La forma, uniforme y con un solo desvío**: cada block gana un prop `decor` que
monta `<Background.Pattern>` como HIJA de su `Section` — la misma que el hero ya
probó. El desvío es `cta`, donde la decoración va dentro del PANEL (`Surface`),
que es donde está el ojo.
**Dos hallazgos que sólo la medición podía dar, y que habría shippeado a ciegas:**
1. **Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular
bajo AA**: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a
**2,27:1**, porque pintan un lavado claro anclado en `50% 0%` — exactamente
donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen,
y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1
contra el panel. El default es `rings`, que irradia del mismo anclaje que
usaba el glow. Las tintadas se ofrecen igual, con el número en el tipo.
2. **Una trama tintada dentro del panel es invisible por construcción**:
`--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que
pinta el panel — un halo del color del panel sobre el color del panel. El
block pasa `color="var(--color-content-on-solid)"`, el token que ya usa para
su copia; medido después, el tinte resuelve a `#ffffff`.
**Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band`
`grid` (una banda de cifras se lee como medida) · `testimonials` y
`feature-split` `glow`. `site-footer` y `banner` reciben la capacidad **APAGADA**:
la columna B del plan de calidad no pide acabado en ninguno de los dos, y
encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió.
**Un desvío de alcance, declarado**: la columna B pedía «`Backdrop` alterno» POR
FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría
a cada `Row` a poseer el estado de alternancia, que es coordinación — y un block
que coordina deja de ser un block que no posee nada.
**Medido en Chrome sobre la página compuesta real** (`/blocks/landing`): cinco
pilas, los cinco anfitriones adoptados (`position: relative` + `isolation:
isolate`) sin que ningún block los posicione; la copia en modo claro mide
15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en
todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y
12,37:1 con `grid`/`dots`.
Ledger: seis filas nuevas, **A-100…A-105**, con su mecanismo y su evidencia.
Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 ·
434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores
propios · prettier limpio.
uix(background): la pila se cuelga del anfitrión, y el anfitrión ni se entera El núcleo del componente (F1 de PLAN-background.md): `Background` es la PILA, no un envoltorio — se renderiza como HIJO de la superficie que viste y se ancla detrás de su contenido. El padre se vuelve anfitrión por una regla de foundation, `:where(:has(> [data-background]))`, así que ninguna receta se parchea y ningún consumidor reestructura su layout. Con la pila entran sus capas: `Layer` (la ranura del pack), `Pattern` (las 4 de Backdrop más lines, noise, rings y vignette), `Gradient` y `Scrim`, el contexto de pausa y el `on` que reata la tinta del contenido de arriba. Dos cosas medidas en Chrome real que el plan no preveía: - La regla de anfitrión escribía `position` y no llegaba (D-BG.16). `box.css` declara `position: var(--box-position, revert-layer)` en `[data-box]`, que es (0,1,0) y gana a un `:where()`; y sin `@layer` en la hoja, `revert-layer` devuelve la propiedad al valor del UA. Una Section o una columna de Grid computaban `static` y la pila se escapaba al ancestro posicionado más cercano. La regla escribe ahora también `--box-position: relative`: se resuelve DENTRO del mecanismo de Box en vez de sobre-especificarlo, así que un `position` por prop —que llega inline— sigue ganando y `Dialog.Content` conserva su `fixed`. 5/5 anfitriones cubiertos. - El prop `flex` de Box no crecía a un hijo flex — el hallazgo que el README del hero dejó anotado el 2026-07-23 sin causa. Es el mismo `revert-layer`: `box.css` ponía el shorthand `flex:` al lado de los tres longhands, y el shorthand borraba aquel de los tres cuya var estuviera sin poner. `<Box flex={2}>` computaba `0 1 auto`. El wrapper expande ahora el shorthand con la gramática de CSS (`expandFlexShorthand`, pinneada en test) y el shorthand desaparece de la receta; medido en una fila de 600px: `flex={1}`→146px, `flex={2}`→292px, `grow` explícito sigue mandando. Las capas no heredan la paleta del anfitrión: `--palette-*` se hereda, así que un `Scrim` dentro de un `<Card color="teal">` habría pintado un velo teal en vez de un velo. Guarda de PRESENCIA, la misma de THM-2 un nivel más abajo — el scrim resuelve `--color-overlay` y el patrón `--color-primary-solid` salvo que la capa lleve su propio `color`. El hero cambia `Backdrop` por `Background.Pattern` como hijo de su Section y gana las ocho tramas. Su layout `background` sigue con las capas a mano hasta F2, cuando existan `Background.Image` / `.Video`; el renombre del snippet es de F5 por D-BG.13. Gates: audit `--only background` PASS 0/0 · eidos-lint invalid 0 · recipe-css-contract · generated-css · blocks:check 0/15 · rtl:check 0/180 · 504/505 en eidos+morfo+adom (el fallo es el `skin-media-player` de siempre) · `check` 0 errores propios. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
### F1.5 — correcciones de supervisión (para el agente, ANTES del commit de F1)
| # | Qué | Cómo (exacto) | Gate |
| --- | --- | --- | --- |
| F1.5.a | **Box `flex`** (canon, `eidos/components/box`) | El wrapper expande el shorthand: `flex` prop → `--box-grow` / `--box-shrink` / `--box-basis` (número `n` → `n 1 0%`; `'none'` → `0 0 auto`; `'auto'` → `1 1 auto`; string de 1–3 tokens → asignación CSS estándar); `box.css` deja de declarar el shorthand `flex:` y conserva los tres longhands. Un test browser en `box` que monte `<Box display="flex">` con hijos `flex={1}`/`flex={2}` y afirme `computedStyle.flexGrow` y anchos (discriminante: hoy falla). README de Box: nota fechada. Verificar que Surface/Section/Container (componen Box) no cambian de aspecto: `check` + suite eidos + un vistazo a `/uix/components/{box,flex,surface}` | suite eidos verde · test nuevo rojo→verde · `check` 0 errores propios |
| F1.5.b | **Guarda de presencia de paleta** en `background.css` | `--_background-tint` y `--_background-scrim-ink` se resuelven desde `--palette-solid` SÓLO bajo `[data-background-layer][data-color], [data-background-layer][data-color-custom]`; el default (`--color-primary-solid` / `--background-scrim-color`) en la regla base. Medir en Chrome: `<Card color="teal">` + `<Background.Scrim/>` → `--_background-scrim-ink` == `--color-overlay` (hoy: teal) | `eidos-lint` invalid 0 · medición en navegador anotada |
| F1.5.c | **Audit** | morfo `background.ts`: `apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/button/'` (el `Toggle` compuesto es un botón; precedente `fab.ts:25`); README: `## Audit exceptions` (`R-1.5 exception:` foco del `Toggle` compuesto; `E-2.2` NO aplica: la receta se auto-importa) + `## Sema events` (0 propios; interactivo por composición) | `component-audit --only background` → 0 errores (queda el `Demo —` hasta F4) |
| F1.5.d | **README del hero** | Fila `decoration` → `Background.Pattern` (hijo de `Section`, no envoltorio; patrones `glow·mesh·grid·dots·lines·noise·rings·vignette·none`); fila `backdrop` intacta (F2 la refactoriza); nota fechada en Decisiones | `blocks:check` verde |
| F1.5.e | **Plan** | Fila F1 de §7: quitar «snippet backdrop→background» (es F5, D-BG.13); D-BG.14: añadir la enmienda `--box-position` si el autor firma D-BG.16 | `docs:check` |
Orden: a → b → c → d → e; después los gates de F1 completos otra vez y el
commit de F1 (sólo lo propio) por orden del autor.
**Ejecutada 2026-08-17 — a·b·c·d·e hechas, medidas en Chrome real** (sonda
`/uix/__probe-f15`, creada y borrada; el layout de `/uix` es el que carga el CSS
de componentes, el raíz no):
- **a — `Box flex`, ARREGLADO.** Antes: `<Box flex={2}>` computaba `0 1 auto` y
71 px de 992. Ahora, en una fila de 600 px: `flex={1}` → `1 1 0%` **146 px** ·
`flex={2}` → `2 1 0%` **292 px** (exactamente el doble) · `Surface flex={1}`
(compone Box) crece igual, 146 px · `flex="none"` → `0 0 auto` ·
`flex="0 0 120px"` → `0 0 120px`, 120 px · `grow={3}` junto a `flex={1}` →
`3 1 0%`, o sea el prop explícito sigue ganando. Suma: 146+146+292+2 gaps = 600.
- **b — guarda de paleta, CONFIRMADA.** Dentro de `<Card color="teal">`
(`--palette-solid` = teal): el scrim resuelve `rgb(0 0 0 / 0.66)` =
`--color-overlay` y el patrón `oklch(0.5556 0.1829 305.86)` =
`--color-primary-solid` — ninguno hereda el teal del anfitrión. Con
`<Background.Scrim color="teal">` sí resuelve teal: la adhesión sigue siendo
explícita. De paso, la adopción del anfitrión (D-BG.16) se re-midió en los dos
Cards: `position: relative` + `isolation: isolate`.
- **No es defecto**: el scrim pinta alfa efectiva **0.297** (`--color-overlay`
0.66 × `--opacity-scrim` 0.45). Es EXACTAMENTE lo que pintaba el scrim inline
del hero que sustituye, así que la migración es fiel; queda dicho en el README
que los pesos son relativos a una tinta ya translúcida.
- **c** — `component-audit --only background` → **PASS, 0 errores, 0 warnings**.
- **d** — README del hero: fila `decoration` reescrita (hijo de la Section, ocho
tramas) + nota fechada; el hallazgo `Box flex` de 2026-07-23 queda marcado
«Resuelto 2026-08-17» con la causa.
- **e** — fila F1 de §7 corregida (el renombre del snippet es F5) y D-BG.14
enlazada con su enmienda D-BG.16.
uix(background): la línea de tiempo la capturaba un overflow, y el puntero se iba con el scroll El travel llevaba dos días escrito y sin medir, porque el panel oculto nunca activa una animación scroll-driven declarada en CSS. Con el Chrome del autor delante se pudo medir, y lo que apareció no fue una confirmación: fueron dos defectos que ninguna sonda había estado en posición de ver. La puerta es `document.visibilityState` — con la pestaña oculta la `ViewTimeline` existe, con `source` y `subject` correctos y `playState: 'running'`, y `currentTime` es `null` para siempre; ahí ni un `await requestAnimationFrame` resuelve. El primero estaba en la demo y el límite es del componente. `[data-uix-stage]` declaraba `overflow: hidden`, y eso es un scroll container aunque no pueda scrollear nunca: `view()` se ancló al stage y el progreso quedó clavado en 52,63% en toda posición de scroll, sin error, sin aviso y con un `translate` de aspecto perfectamente razonable. Con `overflow: clip` —recorta igual, respeta el radio, y no es scroll container— el travel aparece: progreso 43,5% → 95,5%, `translate` `0px -8,34px` → `0px 58,19px`, monótono, y los `4rem` completos de `--background-parallax-travel` en el extremo. Entra en el README como quinto límite porque el caso real no es un harness de demos: es el `overflow-x: hidden` con el que cualquier landing contiene su decoración, que convierte al elemento en scroll container en los DOS ejes. El segundo era del componente. El rect del anfitrión se cacheaba en coordenadas de viewport y sólo lo invalidaban `pointerenter` y un resize; el scroll mueve el anfitrión sin disparar ninguno de los dos. Medido con ratón real: un tick de rueda sobre un anfitrión de 288px dejaba `--background-pointer-y` 0,77 fuera de sitio —los 111px de scroll sobre media altura, a la centésima—, o sea fuera del rango −1…1 que la variable promete, con el spotlight despegado del cursor unos 115px y sin recuperarse hasta salir y volver a entrar. Cacheada la caja en coordenadas de DOCUMENTO y normalizando contra `pageX`/`pageY`, que en un evento real ya traen el scroll incorporado: error 0 en los dos movimientos, halo a 1px del píxel pedido, sin lectura de layout en el camino caliente y sin el listener de scroll que este componente existe para no tener. Queda dicho el residuo: un scroller anidado vuelve a desviar, porque `pageY` no lo ve, y se autocura al reentrar. Y lo que era el encargo, medido: 60 fps con mediana de 16,7ms, p95 17,0, máximo 17,1 y 0 de 200 frames por encima de 20ms con el travel vivo —la base con `speed: 0` salió peor, que es ruido de entorno—; y 0ms de reflow forzado en esos mismos 200 frames moviendo los DOS ejes, con el observador de Long Animation Frames que usa `arts/perf`. El cero está validado por mutación: un thrash deliberado de 65ms en la misma página se reporta con 43ms forzados y el script atribuido, porque un instrumento que no ve nada y un cero real se leen igual. Sigue pendiente la rama `@supports not`, que sólo se ejercita en Firefox. Las 174 demos comparten ese stage, así que el cambio se auditó: `affix` idéntico y `sticky` idéntico —cuatro pasadas alternando `clip` y `hidden`, porque la primera miente—, y `anchor-nav` cambia a mejor: su rail `position: sticky` es hermano del scroller interno, con `hidden` no se pegaba nunca y se escapaba por arriba perdiendo media lista, y ahora se mantiene a la vista. Ninguna demo tocada. Dos avisos para quien vuelva. Un `PointerEvent` construido reporta `pageY === clientY`, sin sumar el scroll, así que los eventos sintéticos sirven para DESCUBRIR este defecto pero no para verificarlo: con ellos el arreglo bueno mide −1,389 donde el ratón real da −0,008. Y leer las variables después de un screenshot puede devolver el `write(0, 0)` de un `pointerleave` en vez de la medición; el valor que vale es el del último `pointermove`, trazado. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2 months ago
### 7.nonies La verificación con Chrome VISIBLE (2026-08-18) — y los dos defectos que destapó
Lo que F3 dejó sin medir porque el panel oculto no activa una animación
scroll-driven declarada en CSS. Hecho con el Chrome real del autor, pestaña al
frente. **`document.visibilityState` es la puerta**: con la pestaña oculta la
`ViewTimeline` existe, con `source` y `subject` correctos y `playState:
'running'`, y `currentTime` es `null` para siempre; en cuanto la pestaña se ve,
marca. Un `await requestAnimationFrame` en esa pestaña ni resuelve (CDP timeout
a 45s), así que ninguna medición por frames es posible ahí.
**Medido (los tres pendientes):**
- **El travel es real.** Anfitrión de 288px, `speed: 1`, scroll de página:
progreso 43,5% → 95,5%, `translate` `0px -8,34px` → `0px 58,19px`, monótono y
lineal, y `0px 64px` = `4rem` completos en el extremo. Los keyframes van
−travel → +travel, así que el punto medio es 0 (`-0,03px` a 49,98%) y la
amplitud pico a pico es el doble del token. Las cifras de §7.sexies (`0px 30px`,
`20px 10px`) NO eran falsas: probaban la COMPOSICIÓN, y la aritmética de ahora
las ratifica (52,6% → −64 + 128×0,526 = 3,37px, medido 3,37px).
- **60 fps sin un frame caído.** 200 frames de scroll continuo con el travel
vivo: mediana 16,7ms · p95 17,0 · **max 17,1** · 0 frames > 20ms. La base con
`speed: 0` salió PEOR (max 63,6ms, 6 > 20ms): ruido de entorno, no del
componente.
- **0 violaciones de reflow.** Mismos 200 frames moviendo LOS DOS ejes (scroll +
puntero con `depth`): el observador de Long Animation Frames que usa
`arts/perf` no reportó ni un frame largo, 0ms forzados. **Instrumento validado
por mutación** — un thrash deliberado de 65ms en la misma página sí se reporta
(43ms forzados, script atribuido); sin esa prueba el cero sería el de un guard
ciego.
- **`@supports not` sigue pendiente**: Chrome soporta scroll-driven, así que la
rama del fallback sólo se ejercita en Firefox. No verificable en esta sesión.
**Defecto 1 — un ancestro `overflow: hidden` mata el travel en silencio.**
`view()` se ancla al scroll container más cercano, y `hidden` lo es aunque no
pueda scrollear nunca. La demo de este componente lo sufría: `[data-uix-stage]`
(`web/routes/uix/uix.css`) declaraba `overflow: hidden`, la timeline se ancló al
stage y el progreso quedó clavado en 52,63% en TODA posición de scroll — sin
error, con un `translate` de aspecto plausible. Arreglado con `overflow: clip`,
que recorta igual, respeta el radio y no es scroll container: `timeline.source`
pasa a ser el `scrollingElement` y el travel aparece, con las cifras de arriba.
El límite es del COMPONENTE, no de la demo (un envoltorio con `overflow-x:
hidden` para contener decoración es el patrón más común de una landing, y hace
scroll container en ambos ejes), así que entra en el README como **quinto
límite**. Efecto colateral verificado en las 174 demos que comparten el stage:
`affix` idéntico (`position: fixed`, A/B `identical: true`), `sticky` idéntico
(4 pasadas alternando `clip`/`hidden`: `top 13`, `stuck ""` las cuatro — el
sujeto tiene su scroller interno ANTES del stage, así que el stage no
participaba); `anchor-nav` sí cambia y **a mejor**: su rail `position: sticky` es
HERMANO del scroller interno, con `hidden` no se pegaba nunca y se escapaba por
arriba perdiendo media lista, con `clip` se mantiene a la vista (offset 47px) y
se ve el item activo. Ninguna demo se toca.
**Defecto 2 — el eje del puntero se desviaba exactamente lo scrolleado.** El
rect del anfitrión se cacheaba en coordenadas de VIEWPORT y sólo se invalidaba
con `pointerenter` y `observeResize`; el scroll mueve el anfitrión sin disparar
ninguno de los dos. Medido con ratón real: un tick de rueda sobre un anfitrión
de 288px dejó `--background-pointer-y` en 0,062 donde la geometría pedía 0,831 —
error 0,769, que es 111px de scroll sobre media altura (0,771) a la centésima —
sacando el valor del rango −1…1 que las vars prometen, y despegando el
`spotlight` del cursor unos 115px a la vista. No se recuperaba hasta salir y
volver a entrar. Arreglado cacheando la caja en coordenadas de DOCUMENTO y
normalizando contra `pageX`/`pageY`, que en un evento REAL ya traen el scroll:
cero lecturas de layout en el camino caliente y cero listeners nuevos, que es la
propiedad que este componente defiende. Verificado con ratón real: error **0** en
los dos movimientos (−0,008 y 0,833 contra su geometría), secuencia
`pointerenter > pointermove > pointermove` sin `leave` de por medio, y el centro
del halo del spotlight a **1px** del píxel pedido. Residuo documentado: un
scroller ANIDADO vuelve a desviar (`pageY` no lo ve) y se autocura al reentrar.
⚠️ **Un `PointerEvent` sintético no puede verificar esto**: un evento construido
reporta `pageY === clientY`, sin sumar el scroll, así que el handler nuevo mide
mal por culpa del instrumento y parece roto (medido: −1,389 donde el ratón real
da −0,008). Descubrí el defecto con eventos sintéticos, pero sólo el ratón real
lo verifica. Segundo fantasma de la sesión: el primero fue leer `0,0` / `50%`
tras un `computer{screenshot}`, que es el `write(0,0)` del `pointerleave`, no una
medición.
---
uix(background): el contrato del fondo existe antes que su primer píxel F0 de `docs/process/PLAN-background.md`: la infraestructura que las fases siguientes consumen, sin una sola regla de pintura todavía. - **morfo `background`** (`scope: ['eidos']`, 0 eventos): tres partes — la pila, la capa (`aria-hidden`) y la pausa. Ningún knob visual: no hay soma que cruzar, así que cada eje de pintura es attr de wrapper (la llamada que `image.ts` ya registró en su bloque comentado). La pausa es el BOTÓN compuesto, no un contenedor de colocación: el patrón de `fab.ts` — el mismo `<button>` lleva los dos markers — porque `[data-archetype='action']` paga cursor, anillo de foco y 44px de suelo táctil, y eso no se le cuelga a un div. Textos `pause`/`play` con las claves idénticas al catálogo (la trampa de chronos). - **foundation**: `:where(:has(> [data-background]))` adopta a CUALQUIER padre como anfitrión — posicionado y aislado — sin tocarle la receta. Un componente no puede estilar a su padre, y la alternativa (que el consumidor recuerde `position: relative`) es el footgun de las referencias: las capas desaparecen y nada dice por qué. Especificidad 0 a propósito: el `position` propio de un `Dialog.Content` sigue ganando. El bloque D12 gana el gemelo `:has` para que el `on` de un fondo re-entinte al padre que lo hospeda. - **`$adom`** gana los dos puertos que un fondo necesita y nadie tenía: `ScrollProgress` (el 0→1 que `view()` recorre, hasta ahora privado dentro de ScrollFrames) y `prefersReducedData` (la query estándar O el `saveData` de Chromium — el gemelo de PESO del reduced-motion, honrado no descargando, nunca escondiendo contenido). Dos defectos propios, cazados por revisión adversaria y corregidos aquí: la primera medición de `ScrollProgress` era síncrona dentro del `$effect` (la regla 5 de timing — ahora se difiere al frame que el helper ya tenía), y el stub de `matchMedia` ignoraba la query, así que la implementación podía pedir `prefers-reduced-motion` con la suite entera en verde. Medido por mutación: ahora mata 4 de 6. Los tokens `--background-*` NO entran todavía: declararlos sin su receta rompe tres guards de `recipe-css-contract` (medido con sonda, revertida). Viajan con el CSS que los consume, en F1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 months ago
---
## 8. Riesgos y cómo se acotan
- **Sobre-alcance**: la pieza podría crecer hacia GSAP. Frontera escrita: el
fondo NO orquesta contenido (pin/scrub/timelines fuera; ScrollFrames sigue
siendo el scrub de media).
- **Deuda de contraste**: un fondo hace fácil poner texto ilegible. La demo y
el README ENSEÑAN scrim+`on` y miden; el hero ya midió los dos huecos de
`data-on` (Display/Link) — candidatos de canon aparte, no de esta pieza.
- **Firefox sin scroll-driven**: el fallback JS existe desde F3; nunca «no
hay parallax en Firefox».
- **`will-change`/compositing**: no se añade a ciegas (memoria de jitter);
sólo si una medición lo pide.
- **Pack ↔ canon**: la integración va por contexto opcional (pack lee); el
canon jamás importa el pack — `npm run check` verde al borrar `src/packs/`.
- **Naming**: renombrar/borrar `Backdrop` exige orden explícita del autor
(regla dura); hasta entonces `Background.Pattern` puede convivir con
`Backdrop` sin shim y el hero migra en F1.
---
## 9. Fuentes externas consultadas (2026-08-17)
Mantine `BackgroundImage` / `Overlay` (docs) · Vuetify `v-parallax` (**no verificado en sesión**: la página devolvió sólo el título y el JSON de la API dio 429; la columna de la tabla §2 va por conocimiento previo — `src` + altura, una sola imagen) · react-scroll-parallax
(`ParallaxBanner` layers, props) · Motion/Framer `useScroll`/`useTransform` (+ ScrollTimeline
nativo con fallback) · Aceternity UI (backgrounds, parallax, spotlight) · Magic UI (Backgrounds)
· shadcn.io/background (100 fondos canvas/CSS) · Chakra v3 `Bleed`/layout helpers · daisyUI
`hero`/`hero-overlay` · MDN «CSS scroll-driven animations» + soporte 2026 (Chrome/Edge 115+,
Safari 26+, Firefox flag/Interop 2026, ~84 %) · WCAG 2.2.2 / 2.3.3 / 1.4.3.
---
## 10. Brief de ejecución para el agente constructor (Opus 5)
> El agente lo ejecuta; **Fable supervisa** (§11). El brief es autosuficiente:
> el agente NO descubre la doctrina por arqueología — la lee en el orden de
> abajo. Todo lo que no esté aquí ni en los docs enlazados es una PREGUNTA al
> supervisor, nunca una decisión propia.
### 10.1 Precondiciones (no arranca sin ellas)
1. **D-BG.1–14 (§6) FIRMADAS por el autor** — una fila sin «FIRMADA» bloquea la
fase que dependa de ella (F1 ← D-BG.1/2/13/14 · F2 ← D-BG.4/5/6 · F3 ←
D-BG.3/7/9 · F5 ← D-BG.10). El agente empieza por la fase más baja cuyas
decisiones estén firmadas y PARA en la primera que no.
2. **Rama propia** para el trabajo (`alpha-0.1-background` o la que el autor
nombre): el árbol actual (`alpha-0.1-dir-prefs`) lleva cambios sin commitear
ajenos a esta pieza (`cta`, docs, `scripts/__scratch-*`, `web/routes/alpha/`)
— **no tocarlos, no incluirlos, no hacer `git stash`/`reset`/`checkout` de
nada**. Crear la rama la ordena el autor.
3. Dev server: el que ya corra en el árbol (`npm run dev`); no reiniciarlo
(`reference_dev_server_restart_orphans_browser`), no arrancar otro en un
worktree para `morfo:check` (403 por `fs.allow`).
### 10.2 Lectura obligatoria (el «paquete mínimo» de `component-audit.md` §0, más los ejes de esta pieza)
Orden y TODO entero, no «la parte relevante»:
1. `docs/building-a-component.md` → `docs/guides/component-guide.md` (Build
contract + Before You Start §1–5) → `docs/guides/completion-checklist.md` →
`docs/guides/demo-authoring.md` (LOCKED) → `docs/guides/component-audit.md`
→ `docs/CANON.md` → `docs/architecture/morfo.md` + `soma.md` (§2 membresía)
→ `docs/canon/vocabularies.md` → `src/uix/eidos/components/README.md`.
2. Ejes de la pieza: `docs/theming/reference.md` (§3, §6, §25, §39, §40) ·
`docs/canon/tsc.md` · `docs/canon/recipe-contract.md` ·
`docs/theming/gradient-finish.md` (§10 fronteras, D8, D12) ·
`docs/theming/motion.md` + `motion-guide.md` (§8 límites) ·
`docs/architecture/packs.md` · `docs/architecture/blocks.md` ·
`docs/decisions/design-text-effects.md` · `src/arts/adom/README.md`.
3. Código de referencia (los precedentes que se imitan, no se reinventan):
`eidos/components/backdrop/*` (se absorbe) · `surface/*` (Box+tratamiento,
`on`) · `image/*` (se compone) · `scroll-frames/*` (progreso de scroll por
ActiveDom) · `motion/*` (contexto `seen`) · `toggle/*` (se compone para la
pausa) · `blocks/hero/{README.md,hero.svelte}` (el consumidor que motiva
todo) · `packs/ambient/ambient.svelte` + `arts/scene/{README.md,types.ts}`
(el pack que se montará DENTRO) · `eidos/lib/render-css.ts`
(`renderOnContextBlocks`) · `eidos/lib/primitives/static.ts` (gradientes,
scrim/opacity/blur) · `morfo/components/{surface,scroll-frames,image}.ts`.
4. Este plan entero, incluidas las enmiendas de cabecera.
### 10.3 Reglas duras del agente (además de `CLAUDE.md`)
- **Nunca commitear ni pushear**; nunca borrar ficheros (incluido
`eidos/components/backdrop/`) sin orden escrita del autor transmitida por el
supervisor. Nunca `--no-verify`.
- **Morfo-first** y morfo MÍNIMO: partes `provider` (la pila) · `layer`
(`aria-hidden` literal) · `pause`; `texts` `pause`/`play` con claves
IDÉNTICAS al catálogo; **ningún knob visual en el morfo** (regla 2026-08-15,
`image.ts`); `as const satisfies Morfo`; `validateMorfo` en test.
- **Sin soma**, sin pack sema, `scope: ['eidos']`, 0 eventos + `## Passive
justification` en el README. Si en algún punto parece necesitar un evento →
PARAR y preguntar (antes se lee `architecture/sema.md`).
- **`Background` NO compone `Box`** ni acepta props de layout; es un hijo del
padre; el anfitrión se adopta por la regla de foundation `:where(:has(>
[data-background]))` emitida por el generador (no por el recipe). La pausa
es un segundo nodo raíz.
- **Cero props nuevas en `Box`**, cero cambios en recipes ajenos (Card,
Section, Dialog…) para «hacer sitio»: si un anfitrión no funciona, se
reporta con medición, no se parchea desde fuera.
- **Canon nunca importa `src/packs/`**; `Background.Layer` es una ranura. La
integración con `Ambient` (D-BG.8) es una tarea DEL PACK, separada, después.
- Recipe: tokens `background-*` en `lib/recipes/base.ts` (TSC: `root` /
`host`), `generate:eidos-css`; sin color crudo, sin `opacity` literal
(`--opacity-*`), sin `box-shadow` literal, sin `@keyframes` sin
`/* functional: … */`, sin `!important` sin `/* important: … */`, sin
`--eidos-*`, sin `will-change` (memoria: jitter a DPR fraccional). Migrar
`backdrop.css` LITERALMENTE con prefijo `--background-pattern-*`.
- DOM sólo por `ActiveEidos.require().dom` (`listen`, `raf`, `measure`,
`writeProperty`, `observeIntersection`, `observeResize`, `getWindow`,
`prefersReducedMotion`); lecturas de layout SIEMPRE dentro de `dom.raf`/
`dom.measure`, nunca síncronas tras una escritura. `CSS.supports` vía
`dom.getWindow(node).CSS`.
- Composición: `Background.Image` compone `<Image>`; `Background.Pause`
compone `<Toggle>`; nada de `<img>`/`<button>` crudos.
- Demo: plantilla v2 de 9 tabs, chips = uniones completas, `SemaPanel` en
vacío justificado, snippet con paridad; canario `button`.
- README con las secciones que el audit exige (Baseline · Comparativa ≥3 ·
Decisiones · Gaps con disposición · Passive justification · Audit
exceptions) — Baseline = `Backdrop` + layout `background` del hero + semillas
`web/routes/demos/{heroscrolling,animations/background}` (referencia, no se
portan).
- Comentarios en inglés; tabs; comillas simples; sin `console.log`; sin
español nuevo en código.
- **Ante cualquier contradicción entre docs, o entre docs y código, o ante
una decisión no cubierta por §6: PARAR, escribirla con cita
(`fichero:línea`) y devolverla al supervisor.** No inventar campos, tipos,
tokens ni mecanismos.
### 10.4 Entregable por fase (lo que el supervisor recibe)
Al cerrar cada fase el agente entrega, en su mensaje final: (a) lista de
ficheros creados/modificados; (b) salida LITERAL de cada gate de la fase
(comando + resultado); (c) qué verificó en navegador y cómo (ruta, qué midió,
valores) — para F1+ una captura por anfitrión (Section · Box-columna · Card
rounded×shape · Dialog.Content) y para F2 la medición de contraste con el
método del hero (píxeles pintados, no `rgb()` parseado a mano); (d) dudas y
contradicciones encontradas, con cita; (e) lo que quedó fuera y por qué. Sin
adjetivos: números y rutas.
### 10.5 Prompt de arranque (para el `Agent`, `model: opus`)
> «Lee ENTERO `docs/process/PLAN-background.md` (incluidas las enmiendas de
> cabecera y §10) y después, en ese orden, toda la lectura de §10.2. Comprueba
> en §6 qué D-BG están FIRMADAS. Ejecuta SOLO la fase más baja de §7 cuyas
> decisiones estén firmadas, respetando §10.3, y PARA al terminarla entregando
> §10.4. No commitees, no borres, no toques ficheros ajenos a la pieza. Si
> algo no está decidido o contradice la doctrina, para y devuélvelo con cita.»
---
## 11. Protocolo de supervisión (Fable)
Por cada fase entregada, en este orden y sin saltarse ninguno:
1. **Firma**: comprobar que la fase sólo usó decisiones FIRMADAS; si el agente
decidió algo por su cuenta → se retira antes de revisar nada más.
2. **Gates, re-ejecutados por el supervisor** (no se acepta el pegado del
agente): `npm run check` (filtrado a los paths de la pieza + hero) ·
`npx vitest run src/uix/eidos` (recipe-css-contract, component-api-contract,
component-visual-attrs, generated-css, gradient-finish-guard) ·
`npx vitest run src/arts/adom` (F0) · `node --import tsx/esm
scripts/component-audit.ts --only background` (F4: PASS 0 errores) ·
`node scripts/eidos-lint.ts background` (invalid 0) · `npm run rtl:check` ·
`npm run blocks:check` (F1+, hero) · `npm run translations:check` ·
`npm run docs:check` (F4) · `morfo:check` + `SMOKE_SCOPE=/uix/components/background npm run smoke` (F4, con dev server).
3. **Diff contra doctrina** (lectura del diff entero, no de resumen): morfo
sin knobs · sin Box · regla `:has` en el generador y no en el recipe ·
tokens con TSC · anotaciones R-4.x · ActiveDom sin globales · lecturas
post-layout · Image/Toggle compuestos · nada en `src/packs/` · nada en
recipes ajenos · nada borrado sin orden · sin shims.
4. **Navegador REAL** (Chrome; el pane oculto suspende rAF y miente): los
cuatro anfitriones de F1; en F2 el vídeo (autoplay muted, pausa por
teclado, `aria-pressed`, pausa fuera de vista y con pestaña oculta,
reduced-motion emulado → poster, reduced-data → poster) y el contraste en
píxeles; en F3 el parallax con y sin soporte de `animation-timeline`
(`@supports` forzado / flag de Chrome), el detector `uix.perf` de reflow a
0, RTL con `dir="rtl"` (puntero físico, scroll vertical) y reduced-motion →
estático.
5. **Encapsulación**: el pack sigue fuera del canon (grep de imports); borrar
`src/packs/` en un worktree de prueba deja `npm run check` verde (F1+).
6. **Informe al autor**: 3–6 líneas — qué entregó el agente, qué verifiqué,
qué falló (con `fichero:línea`), qué decide él. Si hay decisión nueva → las
5 preguntas, UNA por mensaje, y PARAR.
7. Sólo tras el «ok» del autor: siguiente fase al agente. Los commits los
ordena el autor; el supervisor los prepara (staged verificado, mensaje) y
no los ejecuta sin orden.

Powered by TurnKey Linux.