160 KiB
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 componenteBackgroundrico, 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:
BackgroundNO componeBoxni 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: clipen la propia pila). Un solo modo de colocación; el layout sigue siendo deBox/Section/Card(«one axis, one primitive»). D-BG.12 queda superada por D-BG.14.- 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;:wheredeja ganar alpositionde cualquier recipe). El bloquedata-ongana el mismo gemelo:has.- Cero props nuevas en
Box(Box es layout-only por doctrina; el tratamiento vive en primitivas aparte — Surface es el precedente).- Composición, no struct: el padre que quiera renderizar el fondo expone un snippet (
background) donde el app compone<Background>…</Background>; un propbackground={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.- 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(layoutbackground) monta tres capas a mano: unBoxabsoluto para el media del app, unBoxconstyle="background: var(--color-overlay); opacity: var(--opacity-scrim)"de scrim y un<style>scoped/* justified: */para elobject-fit: coverdel<img>/<video>del app — la excepción D-BLK.2 del contrato B, más eldata-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 undata-attrque 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, ySurfacedeja 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) yreact-scroll-parallax(ParallaxBannerpor 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 |
| 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) |
| 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:
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.- 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). - 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:imagesó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;contentpisaposition) → sin archetype («Omit it for a plain display part that pulls no shared styling»). Se confirma en F0 contraARCHETYPE_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 sección» —enmendado 2026-08-18, el plan de calidad la pedía 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
<!-- 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 arenderOnContextBlocks) — 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: elpositionpropio de cualquier recipe (Dialog.Contentfixed, Affix…) gana;isolationno altera layout. Un padredisplay: contentsno 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 untransform/filter/contain: painten ancestros rompefixed). Dentro de un padre con[data-shape]/[data-shape-nest]el recorte sigue alcorner-shapeheredado (verificar en F1: Cardrounded× shapecontinuous, en Chrome real).[data-background-pause](segundo nodo raíz):position:absolute; inset-block-start/inset-inline-endsegúnLogicalPosition;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) pordom.writePropertydesdeScrollProgressy el recipe traduce concalc(). Reduce →animation:none; translate:none. - Parallax puntero: host escribe
--background-pointer-x/-y(−1..1) enpointermovecoalescido pordom.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. Sinwill-changeglobal (memoria: jitter a DPR fraccional; sólo si se mide necesidad).
Anotación 2026-08-26 — las cinco vars de runtime se llaman
--_background-*(vale para las NUEVE menciones de esta sección, tres arriba y seis abajo). El codemod de los 14 canales de valor (ley del espacio cerrado) renombró--background-{pointer-x,pointer-y,pointer-px,pointer-py,progress}a--_background-*: son estado por instancia que el envoltorio escribe, no superficie de tema, y el prefijo privado es lo que esa clase (channel, firma de los 318) significa. Este documento NO se reescribe — sus decisiones firmadas y sus medidas (D-BG.3, los −64px deprogress0, el 0,062 del wheel tick) fueron correctas con el nombre de su día. Vale para las nueve menciones de aquí abajo.
- Scrim:
[data-kind='scrim']→background: color-mix(in oklch, var(--_background-scrim-color) calc(var(--_background-scrim-opacity) * 100%), transparent)o gradienteto-t/to-b/radial;[data-blur]→backdrop-filter: blur(var(--blur-{k})). - Patrones: migración literal de
backdrop.csscon prefijo--background-pattern-*. @media (forced-colors: active) { [data-background-layer] { display:none } }(D-BG.6);@media (prefers-contrast: more)cae glow/mesh (hoy).- Cero
@keyframesno anotados, cero color crudo, cero!important, cero--eidos-*. Recipe tokens enlib/recipes/base.ts(background-*, TSC:rootlos estables;hostlos 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 comoImageconalten 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.PausecomponeToggle(foco,aria-pressed, textos por langs). Reduced-motion NO sustituye al control (es preferencia, no mecanismo). - Legibilidad:
Scrim+onson la pareja documentada; la demo mide contraste con el método del hero (píxeles pintados; el probe rgb/canvas miente conoklch()). - Reduced-motion: parallax off, vídeo sin autoplay,
animateestático;reduce='hide'oculta las capas decorativas enteras. - Reduced-data: vídeo → poster; imágenes: se respeta
srcset/sizesdel app. forced-colors: capas fuera; el contenido cae alCanvasdel UA.- Sin foco atrapado, sin roles falsos (lección TextFocus).
5.4 Rendimiento
Sólo translate/opacity en las capas; scroll-driven por CSS donde
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 |
| 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) |
| 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. Enmendada 2026-08-18: la banda de feature-split va por SECCIÓN y no por fila — una banda por fila obliga a cada Row a poseer el estado de alternancia (su índice y el de sus hermanas), y un block que coordina deja de ser un block que no posee nada; si se quisiera alternancia, el lugar es un :nth-child de la receta, decisión de eidos. PLAN-blocks-quality.md §3 col. B enmendado en el mismo acto |
| 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 | 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 |
| 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 |
| 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 |
| 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» |
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 |
| 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) |
| 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 SECCIÓN — enmienda 2026-08-18, antes «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.
- 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 bloquebackground: { 'parallax-travel': … }enlib/recipes/base.tsy corriendorecipe-css-contract:does not declare recipes for missing component CSS files→expected [ 'background' ] to deeply equal [](el guard exigecomponents/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 onceexige que ese.csslo auto-importebackground.svelte— el wrapper, también de F1. Los tokens viajan con la receta que los consume; la sonda se revirtió sin dejar rastro (git diffvacío enbase.ts).
- La parte
pausees 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-33declara «Structurally a<Button>: the same<button>carriesdata-button+data-fab». Y el archetype importa:[data-archetype='action']arrastracursor: pointer+user-select: none(archetypes.css:169-172), el anillo de foco y un suelo de 44px bajopointer: coarse(archetypes.css:293-301) — sobre undivde colocación eso es la clase de defecto quereference_archetype_item_pulls_interactive_stylingfichó. El morfo declaradefaultElement: 'button'+archetype: 'action': UN nodo, elTogglecompuesto llevando ambos markers. La colocación absoluta vivirá en el recipe sobre ese mismo nodo, como enFab. - «test happy-dom» (F0 / D-BG.7) no existe como proyecto.
vite.config.ts:73-107define dos:client(Playwright chromium,*.svelte.{test,spec}.ts) yserver(node). Los helpers con runas de$adomse prueban TODOS enclient(is-in-viewport.svelte.test.tsy sus once hermanos). Los dos tests nuevos siguen esa convención.
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:
- La regla firmada en D-BG.14 no bastaba y el constructor la amplió con
--box-position: relative(medido: los anfitriones basados en Box computabanstaticporquebox.cssdeclaraposition: var(--box-position, revert-layer)a (0,1,0), y sin@layerrevert-layercae al UA — la trampa queaffix/types.ts:19ya 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. - El bug de
Box flexexiste, 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 shorthandflex:va ANTES de los longhands, que revierten al UA cuando su var está sin poner y borran su aportación; inyectando--box-grow: 2en 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. Scrimsincolorhereda la paleta del anfitrión (por semántica CSS, no medido aún):--palette-*NO están registradasinherits:false(0@property --palette-engenerated/base.css) y la capa compartida las pone en[data-color='x']— un<Card color="teal">que hospede un<Background.Scrim>sincolorle pasa--palette-solidpor 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 decardengenerated/base.css).Patternno lo sufre porque SIEMPRE estampadata-color(defaultprimary), pero merece la misma guarda.- El audit clasifica
backgroundcomo INTERACTIVE (la partepausedeclararole: 'button'+defaultElement: 'button'; heurística decomponent-audit.ts:1471-1477) → pideapg(A-1.4) y un tratamiento de foco (R-1.5). El precedente exacto esfab(mismo caso, PASS):apgdel patrón button +## Audit exceptionsconR-1.5 exception:(el foco es delTogglecompuesto) +## Sema events(«0 eventos propios; interactivo POR COMPOSICIÓN»). Falta añadirlos. - Docs desincronizadas por la migración: el README del hero sigue diciendo
Backdropen el mapa de composición (línea 18) y en la filabackdrop(27); y la fila F1 de §7 pedía renombrar el snippetbackdrop→backgroundmientras 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.
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:
effect_update_depth_exceeded— una capa que se registraba desde un$effectescribí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ásuntracken la mutación y la decisión leída desde un componente aparte.- El control de pausa computaba
position: relativey caía 18px fuera del anfitrión: el<Button>compuesto declaraposition: relativeen[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!importantni envoltorio. Medido después:absolute, 12px/12px, dentro. - Mi
size='sm'inventado en el control: retirado, se queda el del canon (md, 36×36), que es el par que componeMediaPlayer.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
strengthdel scrim no está ordenada por peso:ghost0.30 ·scrim0.45 ·overlay0.65 ·muted0.65 (duplicado) ·subtle0.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.
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
Sectioncon 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:
- El shorthand
animationponeanimation-duration: 0s, y una línea de tiempo de progreso necesita elautoinicial para mapear su intervalo sobre el rango de scroll. Con0sla animación es instantánea y la capa no se mueve nunca. Van longhands, y el porqué queda escrito en la receta. - 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
pointerenteryobserveResize), la lectura sale del camino caliente y el fallo desaparece por forma. - 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:
attach='fixed'era un footgun: había que escribirlo en la capa Y en la pila, y olvidar el segundo dejaba la capaposition: fixedpintando 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 ponerfixedla pila estampadata-attachyclip-path: inset(0), y al volver ascrolllos retira.- Un
speednegativo invertía el bleed. El bleed por defecto ES el travel, y con travel negativo elinset-blocksalí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:-64pxcon 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.
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:
- Dentro del panel sólido del
cta, las dos tramas TINTADAS hunden el titular bajo AA:glowlleva la copia blanca de 5,18:1 a 3,06:1 ymesha 2,27:1, porque pintan un lavado claro anclado en50% 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 esrings, que irradia del mismo anclaje que usaba el glow. Las tintadas se ofrecen igual, con el número en el tipo. - Una trama tintada dentro del panel es invisible por construcción:
--_background-tintcae 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 pasacolor="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.
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}>computaba0 1 autoy 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 aflex={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 resuelvergb(0 0 0 / 0.66)=--color-overlayy el patrónoklch(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-overlay0.66 ×--opacity-scrim0.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
decorationreescrita (hijo de la Section, ocho tramas) + nota fechada; el hallazgoBox flexde 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.
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%,translate0px -8,34px→0px 58,19px, monótono y lineal, y0px 64px=4remcompletos en el extremo. Los keyframes van −travel → +travel, así que el punto medio es 0 (-0,03pxa 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: 0salió 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 usaarts/perfno 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 notsigue 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.
7.decies El cierre: escala del scrim, la rama del fallback y el paseo A–H (2026-08-18)
La escala strength deja de tomar prestada --opacity-*. Esos tokens nombran
cuán opaco es un ELEMENTO y, leídos como pesos de velo, no ordenaban: subtle
(0,80) velaba MÁS que overlay (0,65), que empataba con muted. Ahora cinco
pasos propios (--background-scrim-strength-xs…-xl) y la unión pasa a
xs|sm|md|lg|xl, con md sosteniendo el 0,45 que shipeó el hero. Medido en
Chrome tras el cambio: α 0,084 · 0,126 · 0,189 · 0,273 · 0,420, estrictamente
creciente, y el default pinta 0,189 — idéntico a antes, así que la paridad con
el hero se conserva. Era cambio de API y se hizo mientras el único consumidor era
la demo: ningún block nombra strength.
⚠️ La tabla de contraste del README estaba mal, y el problema era PEOR de lo
escrito. Decía α 0,297 / 0,429 / 0,528 y 2,10:1 para el default, calculados
sobre una tinta de α 0,66. --color-overlay resuelve hoy a
rgba(28, 25, 23, 0.42), así que los valores reales son 0,189 / 0,273 / 0,336 y
1,48:1 el default sobre foto blanca. Re-medido componiendo el velo en un
canvas y leyendo el píxel, no calculándolo. La consecuencia es un TECHO: xl
gasta la tinta entera y llega a 2,66:1; ningún paso adicional puede pasar de
ahí porque el límite es el alpha del token. Con tinta opaca, los mismos pesos dan
5,45:1 (0,65) y 9,22:1 (0,80). Sigue siendo decisión del autor y es sobre qué
ES un scrim, no sobre un número: el gap queda reescrito con estas cifras.
La rama @supports not, ejercitada de verdad. Chromium ya no puede
desactivar scroll-driven (estable, flag de runtime retirado — probados seis
candidatos, todos siguen reportando soporte), así que se ejerció en el Firefox
de Playwright, que reporta CSS.supports('animation-timeline: view()') === false
por sí mismo: la rama está VIVA ahí, sin emulación. Con el fichero de receta
real cargado, --background-progress 0 → −64px, 0,25 → −32px, 0,5 → 0px,
0,75 → +32px, 1 → +64px, con animation-name: none y cero animaciones. Son
exactamente los extremos del camino CSS medido en Chrome (−64…+64, punto medio 0):
los dos caminos concuerdan. Y en Chromium la rama no aplica y escribir la var
no mueve nada (18,65px constante), que es la exclusión mutua que el README
afirmaba sin haberla medido.
Lo que NO se pudo ejercitar y por qué: que ScrollProgress escriba la var
extremo a extremo en un motor sin soporte. Desde el shell de este entorno no hay
ruta a localhost:5173 (curl devuelve 000 con y sin sandbox; el Chromium de
Playwright llega, Firefox no), así que la app no se puede cargar en Firefox aquí.
El gate JS queda cubierto por lectura (background.svelte: CSS.supports +
prefersReducedMotion + scrollTravellers > 0) y por los tres tests de
ScrollProgress (0, 1, rango medio); el eslabón que los une, sin ejercitar.
Paseo de la checklist A–H (estaba listada como lectura de F4 y nunca
registrada como paseo). Mecánicos: audit PASS 0/0 · morfo:check (6 de 160
fallan, background no está) · eidos-lint background invalid 0, class-hooks 0
· rtl:check 0/180 · smoke del componente PASS · check sin errores en ficheros
propios · vitest eidos 434/435 (el único rojo es skin-media-player, el de
siempre). Manuales, con lo que encontraron:
- A-2.2 —
rolesólo lo declarapause. No es omisión: es el patrón de facto (surface, section, box, image, separator declaran CERO), y la única parte con semántica aquí es el botón, que sí lo lleva. - A-2.3 — las tres partes llevan
data: []. Añadida al README la excepción con su ID (A2.3 exception:, el formato que ya usachart): los attrs que estampa son de WRAPPER, y el único estado de contrato —la pausa— vive en elButton. - G-1.1 — faltaba
## Subset. Añadida:colores el conjunto COMPLETO (en decoración, restringir la paleta sería arbitrario) eintentno se acepta. - Deriva documental cazada: el README decía en TRES sitios que el control de
pausa «IS the canonical
<Toggle>» y que disparacommit-toggle. ComponeIconButton(D-BG.18) y el evento escontact-activatedebutton.ts. Eran vestigios de la era D-BG.4 que la enmienda D-BG.18 no barrió. Corregidos, más el comentario del propio morfo. - D-7.1 — 11 controles, todos producen cambio salvo
intent, cuyo único efecto es SUPRIMIRcolor(el<PalettePicker>es un control compartido de dos ejes y este componente no toma intent). Justificado, no defecto.
⚠️ Tres fantasmas en una sesión, todos del mismo patrón: leer 0,0/50% tras
un screenshot (era el write(0,0) de un pointerleave), dar por muerto el
strength leyendo backgroundColor de un scrim GRADUADO (pinta por
background-image), y creer que fade/pause no hacían nada por juzgarlos sin
la precondición que necesitan (scrim encendido, capa que se mueva). El
instrumento miente antes que el código, y aquí mintió tres veces.
7.undecies D-BG.19 — un fondo NUNCA suena (2026-08-18)
Planteada por el autor: qué pasa con un Background.Video cuyo clip trae
pista de audio, y cómo se relaciona con el interruptor de sonido del framework o
con un «no sound» de la app. La respuesta, medida en el código, era que no
estaba resuelto: estaba evitado. muted era una prop con default true, y la
razón escrita no era doctrinal sino mecánica («un clip sin mutear tiene el
autoplay rechazado en todos los navegadores»). Nada conectaba el vídeo con
arts/sound: cero referencias en todo el componente.
Lo que eso significaba con muted={false}: el clip sonaba FUERA del grafo.
sound.buses.content.setMuted(true) no lo silenciaba; no participaba del audio
focus, así que hablaba por encima de un MediaPlayer en vez de cederle el paso;
no atenuaba el bus ui; no se proyectaba a MediaSession; y el slot sound de
``—el interruptor «sin sonido» de la app, proyectado comodata-sound—
no lo alcanzaba. Un AudioContext por documento es la razón de ser de ese art, y
un fondo cantando por fuera es exactamente la fuga que existe para impedir.
De WCAG 1.4.2 se salvaba de rebote: el control de pausa lo para, por carambola.
FIRMADA (a): el fondo nunca suena. muted deja de ser prop y se escribe
true incondicionalmente, así que la API no puede expresar un fondo audible.
Tres razones, de más a menos vinculante: la doctrina propia del componente (toda
capa es aria-hidden, la decoración no es contenido, así que un clip de aquí no
puede portar significado que el audio entregue); WCAG 1.4.2 (una decoración no
puede pedir un consentimiento que nadie le dio); y la fuga de arts/sound de
arriba, que es la parte de framework. Un clip que DEBE oírse es contenido:
MediaPlayer, que ya es ciudadano de sound.media(), o montarlo por
Background.Layer y registrarlo uno mismo.
(b) rechazada —hacerlo ciudadano con sound.media(el, { focus: 'duck' }) cuando
muted === false— por más potente y por eso mismo: abre la puerta a que un fondo
compita con el contenido real, y ese territorio es de Ambient o de un reproductor.
Verificado en Chrome: con la capa en video, el.muted === true,
paused: false, currentTime avanzando y readyState: 4 — sigue reproduciéndose
—, y el control de pausa presente. ⚠️ Anotado en el código: hasAttribute('muted')
lee false mientras la propiedad es true, porque Svelte lo fija como PROPIEDAD
sin reflejarlo; no es un agujero, porque la propiedad se escribe en el mismo
efecto y dos líneas antes del play(). Ningún consumidor pasaba muted (el
HeroSite ya lo decía en prosa: «DECORATIVE — muted»), así que el cambio de API
no rompe a nadie. Gates: audit PASS 0/0 · vitest eidos 434/435 (el de siempre) ·
check sin errores propios · prettier limpio.
7.duodecies D-BG.20 — la tinta del scrim es el SUELO de la tinta en vigor (2026-08-18)
Firmada tras leer la doctrina entera (reference.md §3/§4/§25/§29/§39/§40,
gradient-finish.md §10 + D11/D12, el changelog de la escala de opacidad y del
cue scrim podado, on-solid.ts, renderOnContextBlocks). Mi recomendación
anterior no era conforme y la doctrina la corrigió en tres puntos.
El diagnóstico que sí se sostuvo: --color-overlay ES surface.backdrop, el
dim modal (MD3/Radix/Vaul según el changelog), afinado POR MODO porque una página
clara necesita menos atenuación que una oscura. Es una herramienta de ATENUACIÓN,
y el scrim la tomaba prestada como tinta de LEGIBILIDAD — la misma enfermedad que
la escala --opacity-* prestada que se arregló horas antes.
Lo que la doctrina corrigió:
- No es «una tinta opaca» elegida por mí: es el SUELO del
on. D12 ya define el contexto de tinta (on='dark'→--color-content-on-solid;on='light'→--color-content-on-solid-contrast), así que el velo es el OTRO miembro de ese par, y sinones el suelo de la tinta de la página (--color-surface-default). Tres roles EXISTENTES, ninguno inventado (§16.C), capa 4 → capa 3 (§3), sin alias (no-token-aliases). Mi token único servía sólo aon='dark'e ignoraba los otros dos casos — y conon='light'el velo era OSCURO bajo tinta oscura: el scrim combatía a su propia tinta. Ése era el defecto más grave de los dos, y no lo había visto. - El contraste se mide con el criterio del framework:
on-solid.ts(APCA |Lc| ≥ 60 ∧ WCAG ≥ 3) y §40 (AA 4,5 para texto), con la matemática de$color, no con un canvas WCAG-only como el que usé antes. --opacity-scrimsigue existiendo (0,45, rol de opacidad de ELEMENTO, lo leencharty el mesh): elmd: 0.45que shipeé por la mañana colisionaba con él por valor. Con el suelo firmado,mdpasa a 0,19 y la colisión se disuelve.
La escala, por trabajo y medida ($color sobre la peor obra de cada
contexto): xs 0,08 · sm 0,13 · md 0,19 (default) · lg 0,40 · xl
0,70. Con tinta opaca el peso ES el alpha. xl es el único paso que promete
legibilidad sobre CUALQUIER fotografía, y lo promete contra los cuatro suelos a
la vez: on='dark' sobre foto blanca 6,45:1 / Lc 85; on='light' sobre foto
negra 8,29:1 / Lc 61. 0,70 es el peso mínimo que cierra los cuatro (el más
exigente, el APCA de on='light', pedía 0,695).
Verificado en Chrome, no calculado: sin on el velo es
oklch(0.9911 0 0 / 0.19) (el suelo de la página en claro) · on='dark' →
#1c1917 / 0.19 · on='light' → blanco / 0.19 con tinta oscura encima
(el arreglo) · xl en on='light' sobre foto negra medido 8,25:1 por píxel,
donde $color predijo 8,29 · un data-color='teal' dentro de un stack
on='light' pinta teal y no el suelo (la precedencia). Y el consumidor real:
el hero en layout background (on='dark', md) pinta #1c1917 / 0.19 con
titular blanco y da 1,48 / 5,17 — idéntico a lo que daba antes en modo claro,
paridad exacta. Los heroes en modo OSCURO se aclaran de 0,297 a 0,19, que es
el modo soltando una decisión que nunca fue suya.
⚠️ La especificidad casi rompe §25. El selector de contexto a pelo llega a
(0,4,0) y ganaba a [data-color] (0,3,0): un <Background.Scrim color="teal">
dentro de un stack on='dark' pintaba el suelo en vez de teal — es decir, el
sistema de color abierto (§25) derrotado por un selector de conveniencia.
Envuelto en :where() la familia entera baja a (0,2,0): el contexto gana al
bloque base por ORDEN y pierde contra el color explícito. Medido antes y después.
Fuera de alcance, anotado: el vignette sigue consumiendo --color-overlay
(background.css), y ahí es correcto — es una trama que atenúa bordes, no un
velo de legibilidad.
Gates: audit PASS 0/0 · eidos-lint invalid 0 / class-hooks 0 · rtl:check 0/180 ·
vitest eidos 434/435 (el skin-media-player de siempre) · check con los 72
errores preexistentes y ninguno propio.
7.terdecies backdrop/ borrado (2026-08-18) — D-BG.1 completada
La orden explícita del autor llegó el 2026-08-18 («ok borra»), que es lo que D-BG.1 llevaba esperando desde que se firmó: la absorción estaba hecha desde F1, pero el borrado exigía su palabra y no la tenía.
Cinco ficheros, todos rastreados: eidos/components/backdrop/
(backdrop.css, backdrop.svelte, index.ts, types.ts) y su morfo
morfo/components/backdrop.ts. Censo previo, repetido justo antes de tocar
nada: ningún import del componente ni del morfo fuera de su propia carpeta, no
lo exporta el índice de eidos, no está en ningún índice de morfos, sin langs,
sin demo, y ningún descubrimiento por import.meta.glob que lo cargara. Borrado
con git rm, no con rm, para que el índice lo registre.
Gates después: check con los mismos 72 errores preexistentes y ninguna
mención a backdrop (el conteo de ficheros baja de 6985 a 6981) · vitest
eidos + morfo 653/654 (el rojo sigue siendo skin-media-player) ·
docs:check 0/635 · smoke 320/320 · morfo:check sin cambios en los
fallos y con 167 morfos donde había 168, y los saltados sin demo bajan de 8 a
7 — la aritmética confirma que lo que se fue era exactamente lo que se pretendía.
Lo que NO se borra: las citas. La §Baseline del README de Background conserva a
Backdrop como su baseline —ahora fechada, con la nota de que sus cuatro tramas
se migraron valor por valor y que la historia guarda el original— y las tres
menciones en docs de proceso se quedan donde están. Una fila de registro nunca es
basura: es la huella de por qué el componente se llama Background y no
Backdrop (el nombre colisionaba con el velo modal).
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+
ony miden; el hero ya midió los dos huecos dedata-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 checkverde al borrarsrc/packs/. - Naming: renombrar/borrar
Backdropexige orden explícita del autor (regla dura); hasta entoncesBackground.Patternpuede convivir conBackdropsin 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)
- 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.
- Rama propia para el trabajo (
alpha-0.1-backgroundo 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 hacergit stash/reset/checkoutde nada. Crear la rama la ordena el autor. - 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 paramorfo:check(403 porfs.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»:
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.- 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. - 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/*(contextoseen) ·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. - 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-hiddenliteral) ·pause;textspause/playcon claves IDÉNTICAS al catálogo; ningún knob visual en el morfo (regla 2026-08-15,image.ts);as const satisfies Morfo;validateMorfoen test. - Sin soma, sin pack sema,
scope: ['eidos'], 0 eventos +## Passive justificationen el README. Si en algún punto parece necesitar un evento → PARAR y preguntar (antes se leearchitecture/sema.md). BackgroundNO componeBoxni 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.Layeres una ranura. La integración conAmbient(D-BG.8) es una tarea DEL PACK, separada, después. - Recipe: tokens
background-*enlib/recipes/base.ts(TSC:root/host),generate:eidos-css; sin color crudo, sinopacityliteral (--opacity-*), sinbox-shadowliteral, sin@keyframessin/* functional: … */, sin!importantsin/* important: … */, sin--eidos-*, sinwill-change(memoria: jitter a DPR fraccional). Migrarbackdrop.cssLITERALMENTE con prefijo--background-pattern-*. - DOM sólo por
ActiveEidos.require().dom(listen,raf,measure,writeProperty,observeIntersection,observeResize,getWindow,prefersReducedMotion); lecturas de layout SIEMPRE dentro dedom.raf/dom.measure, nunca síncronas tras una escritura.CSS.supportsvíadom.getWindow(node).CSS. - Composición:
Background.Imagecompone<Image>;Background.Pausecompone<Toggle>; nada de<img>/<button>crudos. - Demo: plantilla v2 de 9 tabs, chips = uniones completas,
SemaPanelen vacío justificado, snippet con paridad; canariobutton. - README con las secciones que el audit exige (Baseline · Comparativa ≥3 ·
Decisiones · Gaps con disposición · Passive justification · Audit
exceptions) — Baseline =
Backdrop+ layoutbackgrounddel hero + semillasweb/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:
- 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.
- 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). - Diff contra doctrina (lectura del diff entero, no de resumen): morfo
sin knobs · sin Box · regla
:hasen el generador y no en el recipe · tokens con TSC · anotaciones R-4.x · ActiveDom sin globales · lecturas post-layout · Image/Toggle compuestos · nada ensrc/packs/· nada en recipes ajenos · nada borrado sin orden · sin shims. - 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 deanimation-timeline(@supportsforzado / flag de Chrome), el detectoruix.perfde reflow a 0, RTL condir="rtl"(puntero físico, scroll vertical) y reduced-motion → estático. - Encapsulación: el pack sigue fuera del canon (grep de imports); borrar
src/packs/en un worktree de prueba dejanpm run checkverde (F1+). - 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. - 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.