|
|
# PLAN — `Background`: el anfitrión de capas de fondo del canon (estudio + plan)
|
|
|
|
|
|
> **Estado: DECISIONES D-BG.1–11 y D-BG.13–15 FIRMADAS 2026-08-17 (D-BG.12
|
|
|
> superada por la 14) — NADA CONSTRUIDO.** Falta que el autor nombre la rama de
|
|
|
> trabajo (§10.1) y ordene el arranque del agente (F0). El estudio original,
|
|
|
> abajo, se conserva íntegro.
|
|
|
>
|
|
|
> ~~Estado: ESTUDIO ENTREGADO 2026-08-17 — NADA CONSTRUIDO, NADA FIRMADO.~~
|
|
|
> Este documento responde a la pregunta «¿conviene un componente `Background`
|
|
|
> rico, composable desde el resto del framework, a la altura o por encima de lo
|
|
|
> que hay hoy (parallax, capas, vídeo, patrones, scrim…)?». Contiene el
|
|
|
> veredicto razonado, la comparativa con las referencias, el inventario de lo
|
|
|
> que YA existe en el framework, la forma canónica propuesta, las decisiones
|
|
|
> que sólo el autor puede firmar (§6, D-BG) y el plan por fases con sus gates
|
|
|
> (§7). Es un documento de proceso: no es doctrina hasta que el autor firma.
|
|
|
>
|
|
|
> **Kickoff para sesión nueva**: _«Lee `docs/process/PLAN-background.md`; si
|
|
|
> las D-BG de §6 están firmadas, ejecuta la fase que toque; si no, PARA y
|
|
|
> preséntalas una a una.»_ Cada fase lista qué leer, qué producir y con qué
|
|
|
> guard se verifica.
|
|
|
>
|
|
|
> **Enmiendas de la sesión 2026-08-17 (conversación con el autor, tras el
|
|
|
> estudio)** — mandan sobre cualquier frase anterior de este documento:
|
|
|
>
|
|
|
> 1. **`Background` NO compone `Box` ni es una caja de flujo.** Es la PILA DE
|
|
|
> CAPAS que el padre renderiza como hijo (`position:absolute; inset:0;
|
|
|
> z-index:-1; border-radius: inherit; overflow: clip` en la propia pila). Un
|
|
|
> solo modo de colocación; el layout sigue siendo de `Box`/`Section`/`Card`
|
|
|
> («one axis, one primitive»). D-BG.12 queda superada por D-BG.14.
|
|
|
> 2. **El padre se convierte en anfitrión sin tocarlo**: una regla de foundation
|
|
|
> `:where(:has(> [data-background])) { position: relative; isolation: isolate }`
|
|
|
> (`:has()` ya se usa 71 veces en eidos; `:where` deja ganar al `position` de
|
|
|
> cualquier recipe). El bloque `data-on` gana el mismo gemelo `:has`.
|
|
|
> 3. **Cero props nuevas en `Box`** (Box es layout-only por doctrina; el
|
|
|
> tratamiento vive en primitivas aparte — Surface es el precedente).
|
|
|
> 4. **Composición, no struct**: el padre que quiera renderizar el fondo expone
|
|
|
> un snippet (`background`) donde el app compone `<Background>…</Background>`;
|
|
|
> un prop `background={struct}` contradice la regla compositional-not-data-driven
|
|
|
> del autor (OnionMenu 2026-06-21), B-5 y la regla 6 de eidos → D-BG.13.
|
|
|
> 5. Ejecución: **un agente Opus 5 construye; Fable supervisa** — brief en §10,
|
|
|
> protocolo de supervisión en §11.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 0. La pregunta y la respuesta corta
|
|
|
|
|
|
**Pregunta** (usuario, 2026-08-17): estudiar el sistema ActiveUIX, su
|
|
|
documentación, filosofía y guías, y evaluar un componente `Background` rico,
|
|
|
composable en el resto de componentes, que permita las técnicas de fondo de hoy
|
|
|
(parallax, etc.), a la par o por encima de las referencias; evaluar idoneidad;
|
|
|
si conviene, plan de desarrollo.
|
|
|
|
|
|
**Respuesta corta — SÍ conviene, con una forma precisa**: no «un componente
|
|
|
más» sino **el anfitrión canónico de capas de fondo** que el tier `blocks` ya
|
|
|
está reclamando a gritos y que hoy se resuelve a mano. La evidencia es
|
|
|
mecánica, no de gusto:
|
|
|
|
|
|
- `src/uix/blocks/hero/hero.svelte` (layout `background`) monta **tres capas a
|
|
|
mano**: un `Box` absoluto para el media del app, un `Box` con
|
|
|
`style="background: var(--color-overlay); opacity: var(--opacity-scrim)"` de
|
|
|
scrim y un `<style>` scoped `/* justified: */` para el `object-fit: cover`
|
|
|
del `<img>/<video>` del app — la **excepción D-BLK.2** del contrato B, más el
|
|
|
`data-on='dark'` y las tintas explícitas puestos a mano. El README del hero
|
|
|
documenta dos huecos del canon que salieron de ahí. Es exactamente la regla
|
|
|
de admisión disparando: «si al construir un block hace falta un `data-attr`
|
|
|
que el CSS necesita seleccionar o una obligación a11y, esa pieza se construye
|
|
|
ANTES como componente canónico».
|
|
|
- `docs/process/PLAN-blocks-quality.md` §2 diagnosticó «no hay fondo decorativo
|
|
|
de sección» y resolvió `Backdrop` (glow/mesh/grid/dots) — **cuatro patrones
|
|
|
estáticos, una sola capa, sin README, sin demo, un consumidor** (hero). El
|
|
|
mismo plan deja «parallax suave» del hero como acabado que falta, y
|
|
|
`Surface` deja en Gaps «scrim de autoría para vívidos — componentizarlo sólo
|
|
|
si aparece el caso repetido». El caso ha aparecido.
|
|
|
- Ninguna referencia (Radix/Ark/Bits/React Aria/MUI/Chakra) tiene un
|
|
|
componente de fondo; Mantine (`BackgroundImage` + `Overlay`), Vuetify
|
|
|
(`v-parallax`) y `react-scroll-parallax` (`ParallaxBanner` por capas) son
|
|
|
los únicos con API de producto; el resto (Aceternity, Magic UI, shadcn.io,
|
|
|
React Bits) son colecciones de efectos copy-paste sin sistema. **El hueco de
|
|
|
«anfitrión de capas con contrato» está vacío en el sector** — y el framework
|
|
|
ya posee todas las piezas para llenarlo con ventaja (tokens+TSC, ActiveDom,
|
|
|
motion reducido, `data-on`, `Image`, `$scene`/`Ambient`, `Toggle`).
|
|
|
|
|
|
**Y con la misma precisión, lo que NO conviene**: meter en el canon los efectos
|
|
|
animados decorativos (aurora WebGL, beams, partículas…). Eso ya está decidido y
|
|
|
construido: son el **pack `Ambient`** (32 efectos sobre `$scene`) —
|
|
|
`docs/architecture/packs.md` + `decisions/design-text-effects.md` («backgrounds
|
|
|
went to the pack tier»). `Background` es el **anfitrión** (geometría, a11y,
|
|
|
tokens, parallax, scrim, media) donde el app monta esos efectos como una capa
|
|
|
más. Canon = la superficie de contrato; pack = la hoja decorativa. La doctrina
|
|
|
se cumple leyéndola entera: lo que fue al pack fue el EFECTO, no la ranura.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 1. Lo que se ha leído (precondición cumplida)
|
|
|
|
|
|
Doctrina leída ENTERA para los ejes que toca esta pieza (arquitectura, canon,
|
|
|
morfo, soma §2 membresía, eidos, motion, theming, packs, blocks, guías):
|
|
|
|
|
|
`docs/README.md` · `architecture/overview.md` · `architecture/active-architecture.md`
|
|
|
· `CANON.md` · `architecture/morfo.md` · `architecture/soma.md` (membresía) ·
|
|
|
`architecture/eidos.md` · `src/uix/eidos/components/README.md` ·
|
|
|
`theming/motion.md` · `theming/motion-guide.md` · `src/arts/motion/README.md` ·
|
|
|
`theming/reference.md` (entera) · `canon/tsc.md` · `canon/recipe-contract.md` ·
|
|
|
`theming/gradient-finish.md` · `theming/channels.md` · `theming/notes.md` ·
|
|
|
`architecture/packs.md` · `architecture/blocks.md` · `building-a-component.md` ·
|
|
|
`guides/component-guide.md` (build contract + Before You Start §1–5) ·
|
|
|
`guides/completion-checklist.md` · `guides/demo-authoring.md` ·
|
|
|
`guides/component-audit.md` · `canon/vocabularies.md` ·
|
|
|
`decisions/design-text-effects.md` · `decisions.md` · `glossary.md` ·
|
|
|
`next-features.md` · `rfcs/rfc-depth.md` (alcance: scrim/frost/parallax) ·
|
|
|
`process/PLAN-blocks-quality.md` · `process/PLAN-blocks.md` (forma) ·
|
|
|
`process/CONTINUE-lectura-doctrina.md`.
|
|
|
|
|
|
Código leído: `eidos/components/{backdrop,surface,image,scroll-frames,motion}`
|
|
|
+ sus morfos · `blocks/hero/{README,hero.svelte}` · `packs/ambient/{README,
|
|
|
ambient.svelte}` · `arts/scene/{README,types}` · `arts/adom/README.md` ·
|
|
|
`eidos/lib/primitives/static.ts` (gradientes/scrim/opacity/blur) ·
|
|
|
`eidos/lib/themes/cristal.ts` · `web/routes/demos/heroscrolling` (semilla de
|
|
|
parallax) · `web/routes/demos/animations/background/*` (censo de semillas).
|
|
|
|
|
|
**No leído, y por qué no bloquea**: `architecture/sema.md` (1051 L) — la
|
|
|
pieza es pasiva (`scope: ['eidos']`, 0 eventos, patrón ScrollFrames/Backdrop);
|
|
|
si alguna decisión de §6 la hiciera emitir eventos, se lee ANTES de tocar el
|
|
|
morfo. `MOTION_SERVICE_RFC.md` — leído por sus síntesis en `motion.md` /
|
|
|
`motion-guide.md` y grepeado: **no contempla timelines de scroll ni parallax**
|
|
|
(sólo cita GSAP como lo que NO se reinventa) → el parallax es terreno no
|
|
|
cubierto por la doctrina de motion, y por eso es una decisión (D-BG.3), no una
|
|
|
presuposición.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 2. Comparativa — qué hay hoy en el sector y dónde queda UIX
|
|
|
|
|
|
Leído en las fuentes (docs oficiales / repos), 2026-08-17.
|
|
|
|
|
|
| Capacidad | Mantine (`BackgroundImage`, `Overlay`) | Vuetify `v-parallax` | `react-scroll-parallax` (`ParallaxBanner`) | Motion/Framer (`useScroll`) | Aceternity · Magic UI · shadcn.io · React Bits | Tailwind Plus · Untitled · daisyUI | **UIX hoy** | **UIX con `Background`** |
|
|
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
|
| Imagen de fondo cover con contenido encima | ✓ (`src`, `radius`) | ✓ (`src`, `height`) | ✓ (`image` por capa) | — | dentro de cada efecto | HTML + `bg-*` / `hero-overlay` | hero a mano (`<style>` justified) | ✓ `Background.Image` compone `Image` (fit/position/srcset/priority/fallback) |
|
|
|
| Vídeo de fondo (autoplay muted loop poster, pausa a11y) | ✗ | ✗ | ✗ | ✗ | ✗ | HTML crudo | ✗ | ✓ `Background.Video` + `Background.Pause` (WCAG 2.2.2), pausa fuera de vista/pestaña |
|
|
|
| Scrim / overlay (color, opacidad, gradiente, blur, fixed) | ✓ `Overlay` | ✗ | ✗ | — | ad hoc | `hero-overlay` (opaco fijo) | inline `style=` en hero | ✓ `Background.Scrim` sobre `--color-overlay` / `--opacity-*` / `--blur-*` (Cristal-compatible) |
|
|
|
| Capas apiladas con blend/opacidad/máscara | ✗ | ✗ | ✓ (orden = z; `expanded`) | manual | ✗ (un efecto = un wrapper) | ✗ | `Backdrop` = una capa `::before` | ✓ `Background.Layer` genérica (blend, opacity semántica, fade-mask, bleed) |
|
|
|
| Patrones estáticos (grid, dots, líneas, ruido, anillos, viñeta, glow, mesh, spotlight) | ✗ | ✗ | ✗ | — | ✓ (copy-paste, colores a mano) | Untitled `BackgroundPattern` (SVG), TW blobs `clip-path` | `Backdrop`: 4 sobre tokens | ✓ `Background.Pattern` (los 4 + lines/noise/rings/vignette/spotlight), **sobre tokens y modo** |
|
|
|
| Gradientes nombrados temables | ✗ | ✗ | ✗ | — | hex a mano | utilidades | `--gradient-{name}` (open cage) + `Surface gradient` | ✓ `Background.Gradient colors="aurora"\|[stops]` (patrón D-T3 de TextGradient) |
|
|
|
| Parallax por scroll (velocidad por capa) | ✗ | ✓ (una imagen) | ✓ (`speed`, easing, ranges) | ✓ (`useTransform`) | «Hero Parallax» (JS, GSAP-like) | ✗ | ✗ (semilla `heroscrolling`) | ✓ CSS scroll-driven (`animation-timeline: view()`) primero + fallback JS por ActiveDom |
|
|
|
| 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:
|
|
|
|
|
|
1. **`data-*` que el CSS necesita seleccionar** — `data-background`,
|
|
|
`data-background-layer`, `data-background-pause`: un block NO puede escribir
|
|
|
CSS (B-3/B-11), así que la geometría de capas sólo puede vivir en el canon.
|
|
|
2. **Obligaciones a11y** — capas decorativas `aria-hidden` +
|
|
|
`pointer-events:none`; **pausa/parada** de movimiento automático > 5 s en
|
|
|
paralelo con contenido (WCAG 2.2.2) para vídeo y capas animadas;
|
|
|
**legibilidad del contenido sobre media** (1.4.3/1.4.11 → scrim + `on`);
|
|
|
reduced-motion (2.3.3) para parallax; `forced-colors`. «The moment an
|
|
|
artifact owns an accessibility obligation, it enters the canon»
|
|
|
(`design-text-effects.md`).
|
|
|
3. **Tokens que otros recipes/temas quieren** — `--background-scrim-*`,
|
|
|
`--background-parallax-travel`, `--background-pattern-*` (los de Backdrop),
|
|
|
temables por config (precedente state-layer §38/§40).
|
|
|
|
|
|
**Forma**: componente **eidos-native pasivo** — morfo `scope: ['eidos']`, 0
|
|
|
eventos, sin soma (membresía §2 de `soma.md`: no hay patrón APG ni máquina de
|
|
|
estados accesible; el fondo pinta y reacciona a scroll/puntero como
|
|
|
**modulación continua**, no como acto — el mismo criterio contract-level que
|
|
|
ScrollFrames («observación, no acto») y la regla «no emitir por frame»). El
|
|
|
único acto de usuario —pausar— pertenece al `Toggle` compuesto y a SU morfo
|
|
|
(`commit-toggle`). El estado `paused` es visual (`data-paused`, attr de
|
|
|
wrapper). Sema: `SemaPanel` en vacío justificado (D-4.1).
|
|
|
|
|
|
**Morfo mínimo (regla 2026-08-15, morfo `image.ts`)**: sólo partes + ARIA;
|
|
|
los knobs visuales (`data-kind`, `data-fit`, `data-blend`, `data-parallax`,
|
|
|
`data-attach`, `data-paused`, `data-pattern`, `data-color`, `data-strength`)
|
|
|
son attrs eidos-only de wrapper — ningún prop cruza la frontera de soma. Las
|
|
|
partes son lo que el consumidor COMPONE (`morfo.md` §renderAttrs: «if a
|
|
|
consumer can compose it or address it, it is a PART»):
|
|
|
|
|
|
| Parte | kebab | archetype | rol/aria | Nota |
|
|
|
| --- | --- | --- | --- | --- |
|
|
|
| Provider (host) | `provider` | `provider` | — | `Box` + `position:relative; isolation:isolate; overflow:clip` |
|
|
|
| Layer | `layer` | — (display puro; ver nota) | `aria-hidden="true"` (literal, como `scroll-frames.media`) | UNA parte para todas las capas; el `kind` (image/video/pattern/gradient/scrim/custom) es attr eidos-only |
|
|
|
| Pause | `pause` | `action` | ARIA del `Toggle` compuesto | Opcional; contenedor de colocación (patrón Fab-sobre-Button) |
|
|
|
|
|
|
> Nota archetype `layer`: `image` sólo cuando el elemento ES `<img>`; una capa
|
|
|
> genérica no encaja en ninguno de los 26 (`overlay` = «modal/dim backdrop» y
|
|
|
> arrastraría CSS de veil; `content` pisa `position`) → sin archetype
|
|
|
> («Omit it for a plain display part that pulls no shared styling»). Se
|
|
|
> confirma en F0 contra `ARCHETYPE_DESCRIPTIONS`.
|
|
|
|
|
|
**Textos** (`texts:` + `langs/components/background.ts`): `pause`
|
|
|
(`#?components.background.pause|Pause background`) y `play`
|
|
|
(`#?components.background.play|Play background`) para el `Toggle`; claves
|
|
|
IDÉNTICAS entre `texts` y catálogo (la trampa de chronos).
|
|
|
|
|
|
**Motion (doctrina aplicada)**: la deriva ambiental de un gradiente o el
|
|
|
travel de parallax NO son firmas de evento ni presets de estado → van como
|
|
|
`@keyframes` locales anotados `/* functional: … */` (R-4.5; precedente
|
|
|
TextGradient) con kill bajo reduce; el parallax por scroll con
|
|
|
`animation-timeline: view()` es la misma clase (maquinaria continua). La
|
|
|
alternativa —un eje `timeline` en el registro de presets— es una EXTENSIÓN de
|
|
|
contrato de `$motion`/eidos y sólo entra firmada (D-BG.3).
|
|
|
|
|
|
**Packs (doctrina aplicada)**: canon nunca importa `src/packs/`;
|
|
|
`Background.Layer` es una ranura; el app compone `<Ambient>` dentro; un block
|
|
|
NO puede (B-4) — recibe la escena del app por snippet (el hero ya tiene
|
|
|
`backdrop`). Un `Background.Scene effect=…` en canon importaría decoración al
|
|
|
canon → **rechazado** (queda registrado como alternativa desechada).
|
|
|
|
|
|
**Blocks (doctrina aplicada)**: al existir `Background`, el layout
|
|
|
`background` del hero deja de necesitar su `<style>` justificado y su scrim
|
|
|
inline; `Background.Pattern` releva a `Backdrop`; los blocks pendientes de
|
|
|
acabado (`cta`, `stats-band`, `testimonials`, `feature-split` «banda de fondo
|
|
|
por 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
|
|
|
|
|
|
```svelte
|
|
|
<!-- El PADRE es cualquier componente o elemento que acepte hijos: Section, Card,
|
|
|
Dialog.Content, un Box-columna de Grid/Flex, un <li>… No se envuelve nada:
|
|
|
Background es un HIJO más, y el padre se vuelve anfitrión por la regla de
|
|
|
foundation (`:has`) sin tocar su recipe. -->
|
|
|
<Section>
|
|
|
<Background on="dark" parallax bind:paused>
|
|
|
<!-- capas: orden de fuente = apilamiento; todas absolute/inset 0 DENTRO de la pila, aria-hidden -->
|
|
|
<Background.Image src={hero} srcset sizes fetchpriority="high" fit="cover" position="center"
|
|
|
speed={-0.3} sources={{ dark: heroDark }} />
|
|
|
<Background.Video src="/loop.mp4" poster="/loop.jpg" sources={[{ src, type, media }]}
|
|
|
speed={-0.2} />
|
|
|
<Background.Gradient colors="aurora" | colors={['--color-primary-solid', '--color-tertiary-solid']}
|
|
|
angle="to-b" animate />
|
|
|
<Background.Pattern pattern="dots" color="primary" fade /> <!-- ex-Backdrop -->
|
|
|
<Background.Layer speed={0.15} blend="screen" opacity="muted" fade="to-b">
|
|
|
<Ambient effect="mesh" params={{…}} /> <!-- el pack, desde el APP -->
|
|
|
</Background.Layer>
|
|
|
<Background.Scrim strength="scrim" gradient="to-t" blur="md" />
|
|
|
<Background.Pause position="top-end" /> <!-- opcional: reubica el control -->
|
|
|
</Background>
|
|
|
|
|
|
<!-- el contenido, hermano de la pila, en flujo normal: pinta encima solo -->
|
|
|
<Container>…</Container>
|
|
|
</Section>
|
|
|
|
|
|
<!-- Un componente que quiera RENDERIZAR el fondo desde dentro (hero) expone un
|
|
|
snippet — composición, nunca un struct (D-BG.13): -->
|
|
|
<Hero>
|
|
|
{#snippet background()}<Background>…</Background>{/snippet}
|
|
|
{#snippet title()}…{/snippet}
|
|
|
</Hero>
|
|
|
```
|
|
|
|
|
|
| Superficie | Props (v1) | Notas |
|
|
|
| --- | --- | --- |
|
|
|
| `Background` (root = la PILA; NO compone `Box`) | `on?: 'light'\|'dark'` · `parallax?: boolean \| 'scroll' \| 'pointer' \| ('scroll'\|'pointer')[]` · `attach?: 'scroll'\|'fixed'` · `reduce?: 'static'\|'hide'` (política bajo reduced-motion, default `static`) · `paused?: boolean` (bindable) · `children` (las capas) + `HTMLAttributes<HTMLDivElement>` | La pila es `position:absolute; inset:0; z-index:-1; border-radius: inherit; overflow: clip` — bleed, recorte, radio y `attach='fixed'` (`clip-path: inset(0)` en la PILA, nunca en el padre) no tocan al anfitrión. El padre queda posicionado + aislado por la regla de foundation `:where(:has(> [data-background]))` (D-BG.14). `on` re-vincula la tinta del PADRE vía el gemelo `:where(:has(> [data-background][data-on='dark']))` del bloque D12 (mismos límites: anidados/portales). Publica contexto `getBackgroundContext()` → `{ paused, reduced, seen }`. Sin `rounded` (hereda del padre), sin props de layout (son del padre) |
|
|
|
| `Background.Layer` | `speed?: number` (parallax scroll; 0 = fijo; negativo = más lento/opuesto — semántica react-scroll-parallax) · `depth?: number` (parallax puntero) · `blend?: MixBlend` · `opacity?: 'full'\|'subtle'\|'muted'\|'ghost'` (tokens `--opacity-*`) · `fade?: 'radial'\|'to-t'\|'to-b'\|'edges'` (mask) · `bleed?: boolean` (default `true` cuando hay `speed`: margen negativo = `--background-parallax-travel` para que no asomen bordes; el `expanded` de react-scroll-parallax) · `pointer?: boolean` (opt-in `pointer-events:auto` para escenas interactivas, P-5) · `children` | La capa genérica; las demás son azúcar sobre ella con `data-kind` |
|
|
|
| `Background.Image` | `ImageProps` (compone `<Image fit="cover">`) + layer props + `sources?: { dark?: string; light?: string }` | Decorativa por definición: `alt=""` + `aria-hidden` (si la imagen significa algo, es contenido, no fondo). Modo resuelto por `eidos.getThemeContext()` (D-BG.5). Default `loading="eager"` + `fetchpriority="high"` sólo si el app lo pide: un fondo bajo el pliegue debe seguir lazy |
|
|
|
| `Background.Video` | `src` · `sources?` · `poster` · `loop=true` · `autoplay=true` · `preload='metadata'` · layer props | Siempre `muted playsinline` (política de autoplay); pausa fuera de vista (`observeIntersection`) y con pestaña oculta (`IsDocumentVisible`); reduced-motion → no autoplay (poster); reduced-data → sólo poster; `paused` global |
|
|
|
| `Background.Pattern` | `pattern: 'glow'\|'mesh'\|'grid'\|'dots'\|'lines'\|'noise'\|'rings'\|'vignette'\|'spotlight'` · `color?: ComponentColorProp` · `cell?` · `fade?` + layer props | Los 4 de Backdrop migran 1:1 (tokens `--backdrop-*` → `--background-pattern-*`); `noise` = `feTurbulence` en `url()` data-URI (sobrevive forced-colors → por eso D-BG.6 oculta las capas allí); `spotlight` sigue el puntero |
|
|
|
| `Background.Gradient` | `colors: string \| string[]` (nombre canónico `--gradient-{name}` \| lista de stops con tokens) · `angle?: GradientAngle` · `animate?: boolean` + layer props | Discriminación por FORMA (D-T3 de TextGradient); pinta con `background:` shorthand; `animate` = deriva `background-position` funcional, estática bajo reduce |
|
|
|
| `Background.Scrim` | `color?: ComponentColorProp` (default `--color-overlay`) · `strength?: 'scrim'\|'overlay'\|'muted'\|'subtle'\|'ghost'` (tokens) · `gradient?: 'to-t'\|'to-b'\|'radial'` · `blur?: BlurKey` (`--blur-*`, `backdrop-filter`) | Paridad Mantine `Overlay`; el frost es Cristal-compatible |
|
|
|
| `Background.Pause` | `position?: LogicalPosition` (default `top-end`) · props del `Toggle` compuesto | Compone `Toggle` (`pressed` ↔ `paused`, textos del morfo). Regla de render por defecto: D-BG.4. **Colocación**: NO puede vivir dentro de la pila (`z-index:-1` la dejaría bajo el contenido) → `Background` la renderiza como **segundo nodo raíz** (fragmento hermano de la pila, `position:absolute; z-index: 1` sobre el contenido del padre); si el app compone `<Background.Pause>` fuera de `<Background>` (junto al contenido), esa instancia se registra en el contexto y el default no se renderiza |
|
|
|
|
|
|
Contexto publicado (patrón `getMotionContext`): `{ paused, reduced, seen }` —
|
|
|
`seen` por `IsInViewport` para que capas caras arranquen al entrar (misma
|
|
|
razón que Motion→CountUp).
|
|
|
|
|
|
### 5.2 Recipe (`background.css`) — sólo selectores morfo-backed + attrs de wrapper
|
|
|
|
|
|
- **Foundation (generador, `render-css.ts`, junto a `renderOnContextBlocks`)** —
|
|
|
la adopción del anfitrión (D-BG.14):
|
|
|
`:where(:has(> [data-background])) { position: relative; isolation: isolate; }`
|
|
|
y el gemelo de tinta `:where(:has(> [data-background][data-on='dark']))` /
|
|
|
`…='light'` añadido como segundo selector del bloque D12 (una regla, dos
|
|
|
selectores — sin duplicar declaraciones). `:where` = especificidad 0: el
|
|
|
`position` propio de cualquier recipe (Dialog.Content `fixed`, Affix…) gana;
|
|
|
`isolation` no altera layout. Un padre `display: contents` no tiene caja → se
|
|
|
documenta como no-anfitrión.
|
|
|
- `[data-background]` (la pila): `position:absolute; inset:0; z-index:-1;
|
|
|
border-radius: inherit; overflow: clip; pointer-events: none;` —
|
|
|
`[data-attach='fixed']` → `clip-path: inset(0)` (la capa fija dentro queda
|
|
|
recortada por la pila; documentar que un `transform`/`filter`/`contain: paint`
|
|
|
en ancestros rompe `fixed`). Dentro de un padre con `[data-shape]` /
|
|
|
`[data-shape-nest]` el recorte sigue al `corner-shape` heredado (verificar en
|
|
|
F1: Card `rounded` × shape `continuous`, en Chrome real).
|
|
|
- `[data-background-pause]` (segundo nodo raíz): `position:absolute;
|
|
|
inset-block-start/inset-inline-end` según `LogicalPosition`; `z-index: 1`
|
|
|
(intra-componente, entero crudo permitido por el recipe-contract) — sobre el
|
|
|
contenido del padre, alcanzable por puntero y teclado.
|
|
|
- `[data-background-layer]`: `position:absolute; inset:0; z-index:-1;
|
|
|
pointer-events:none;` + `[data-bleed]` inset negativo por
|
|
|
`--background-parallax-travel`; `[data-blend=…]`; `[data-opacity=…]` →
|
|
|
`opacity: var(--opacity-{k})` (R-4.2 sin literales); `[data-fade=…]` → mask
|
|
|
(Backdrop hoy); `[data-pointer]` → `pointer-events:auto`.
|
|
|
- Parallax scroll: `[data-background][data-parallax~='scroll'] [data-background-layer][data-speed]`
|
|
|
→ `animation: background-parallax linear both; animation-timeline: view();
|
|
|
animation-range: entry 0% exit 100%;` con `@keyframes background-parallax
|
|
|
{ from { translate: 0 calc(var(--_background-speed) * var(--background-parallax-travel)) } to { translate: 0 calc(-1 * …) } }`
|
|
|
`/* functional: scroll-linked travel, not an event signature */`; bajo
|
|
|
`@supports not (animation-timeline: view())` el wrapper escribe
|
|
|
`--background-progress` (0..1) por `dom.writeProperty` desde `ScrollProgress`
|
|
|
y el recipe traduce con `calc()`. Reduce → `animation:none; translate:none`.
|
|
|
- Parallax puntero: host escribe `--background-pointer-x/-y` (−1..1) en
|
|
|
`pointermove` coalescido por `dom.raf`; capa → `translate:
|
|
|
calc(var(--background-pointer-x) * var(--_background-depth) * var(--motion-distance-xl))`.
|
|
|
Físico, no espeja en RTL (sigue al puntero). Reduce → 0. Sin `will-change`
|
|
|
global (memoria: jitter a DPR fraccional; sólo si se mide necesidad).
|
|
|
- Scrim: `[data-kind='scrim']` → `background: color-mix(in oklch,
|
|
|
var(--_background-scrim-color) calc(var(--_background-scrim-opacity) * 100%), transparent)`
|
|
|
o gradiente `to-t/to-b/radial`; `[data-blur]` → `backdrop-filter: blur(var(--blur-{k}))`.
|
|
|
- Patrones: migración literal de `backdrop.css` con prefijo `--background-pattern-*`.
|
|
|
- `@media (forced-colors: active) { [data-background-layer] { display:none } }`
|
|
|
(D-BG.6); `@media (prefers-contrast: more)` cae glow/mesh (hoy).
|
|
|
- Cero `@keyframes` no anotados, cero color crudo, cero `!important`, cero
|
|
|
`--eidos-*`. Recipe tokens en `lib/recipes/base.ts` (`background-*`, TSC:
|
|
|
`root` los estables; `host` los que leen `--palette-*`).
|
|
|
|
|
|
### 5.3 a11y — el contrato, no la esperanza
|
|
|
|
|
|
- Capas: `aria-hidden` (morfo) + `pointer-events:none` (recipe). Un fondo NO
|
|
|
es contenido; si el media significa, el app lo pone como `Image` con `alt`
|
|
|
en el flujo.
|
|
|
- **Pausa** (WCAG 2.2.2): cualquier capa que se mueva sola > 5 s (vídeo
|
|
|
autoplay, `Gradient animate`, escena del pack) exige control accesible;
|
|
|
`Background.Pause` compone `Toggle` (foco, `aria-pressed`, textos por langs).
|
|
|
Reduced-motion NO sustituye al control (es preferencia, no mecanismo).
|
|
|
- **Legibilidad**: `Scrim` + `on` son la pareja documentada; la demo mide
|
|
|
contraste con el método del hero (píxeles pintados; el probe rgb/canvas
|
|
|
miente con `oklch()`).
|
|
|
- Reduced-motion: parallax off, vídeo sin autoplay, `animate` estático;
|
|
|
`reduce='hide'` oculta las capas decorativas enteras.
|
|
|
- Reduced-data: vídeo → poster; imágenes: se respeta `srcset/sizes` del app.
|
|
|
- `forced-colors`: capas fuera; el contenido cae al `Canvas` del UA.
|
|
|
- Sin foco atrapado, sin roles falsos (lección TextFocus).
|
|
|
|
|
|
### 5.4 Rendimiento
|
|
|
|
|
|
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** | ~~Uso dentro de CUALQUIER componente — dos modos (envoltorio + `fill`)~~ | — | **SUPERADA el mismo día por D-BG.14** (un solo modo: hijo). Sobreviven sus tres consecuencias como alcance/gate de F1: (1) herencia de radio y **shape** dentro de superficies redondeadas/anidadas (verificar Card `rounded` × shape `continuous` en Chrome real); (2) el **bug medido** en el hero (`Box flex/grow` no crece un hijo flex, README hero 2026-07-23) lo destapa el caso columna → se verifica y, si sigue, se arregla en `Box` ANTES (canon, no en Background); (3) **coste en partes repetidas** (celdas/items/cards de una rejilla): patrones/gradientes/scrim sí; vídeo/escenas NO (una por sección; el presupuesto de `$scene` avisa) — README y demo lo dicen |
|
|
|
| **D-BG.13** | **Cómo llega el fondo a un componente que lo renderiza desde dentro** (hero, y cualquier otro) — planteada por el autor 2026-08-17 (`<Hero background={struct}>`) | (a) **snippet** `background` en el componente; el app compone `<Background>…</Background>` dentro y el componente lo coloca (el hero ya tiene `backdrop` → se renombra); (b) prop `background={struct}` (árbol de datos que describe capas) | **FIRMADA 2026-08-17 — (a)**: snippet `background` (hero: `backdrop` → `background` en F5); ninguna API nueva. (b) rechazada: contradice la regla compositional-not-data-driven fijada por el autor (OnionMenu 2026-06-21: «los hijos son componentes reales, nunca `root={tree}`/`items={[...]}`»), B-5 de blocks y la regla 6 de eidos; además un struct no puede nombrar un efecto del pack sin canon→pack, y cada componente tendría que poseer su mapeo struct→capas |
|
|
|
| **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.
|
|
|
|
|
|
1. **F0.4 (tokens `background-*`) es IMPOSIBLE en F0 y se ejecuta en F1.** No
|
|
|
es un recorte: los dos guards que el propio F0 declara como gate lo
|
|
|
prohíben. Medido declarando un bloque `background: { 'parallax-travel': … }`
|
|
|
en `lib/recipes/base.ts` y corriendo `recipe-css-contract`:
|
|
|
- `does not declare recipes for missing component CSS files` →
|
|
|
`expected [ 'background' ] to deeply equal []` (el guard exige
|
|
|
`components/background/background.css`, producto de F1);
|
|
|
- `does not leave declared public recipe variables orphaned` → el token no
|
|
|
lo consume nadie hasta que exista la receta.
|
|
|
Y un tercero espera detrás: `loads every component CSS recipe exactly once`
|
|
|
exige que ese `.css` lo auto-importe `background.svelte` — el wrapper,
|
|
|
también de F1. **Los tokens viajan con la receta que los consume**; la
|
|
|
sonda se revirtió sin dejar rastro (`git diff` vacío en `base.ts`).
|
|
|
2. **La parte `pause` es el BOTÓN, no un contenedor de colocación.** La tabla
|
|
|
de §4 dice «contenedor de colocación (patrón Fab-sobre-Button)», pero el
|
|
|
patrón que cita hace lo contrario: `morfo/components/fab.ts:31-33` declara
|
|
|
*«Structurally a `<Button>`: the same `<button>` carries `data-button` +
|
|
|
`data-fab`»*. Y el archetype importa: `[data-archetype='action']` arrastra
|
|
|
`cursor: pointer` + `user-select: none` (`archetypes.css:169-172`), el
|
|
|
anillo de foco y un suelo de 44px bajo `pointer: coarse`
|
|
|
(`archetypes.css:293-301`) — sobre un `div` de colocación eso es la clase
|
|
|
de defecto que `reference_archetype_item_pulls_interactive_styling` fichó.
|
|
|
El morfo declara `defaultElement: 'button'` + `archetype: 'action'`: UN
|
|
|
nodo, el `Toggle` compuesto llevando ambos markers. La colocación absoluta
|
|
|
vivirá en el recipe sobre ese mismo nodo, como en `Fab`.
|
|
|
3. **«test happy-dom» (F0 / D-BG.7) no existe como proyecto.**
|
|
|
`vite.config.ts:73-107` define dos: `client` (Playwright chromium,
|
|
|
`*.svelte.{test,spec}.ts`) y `server` (node). Los helpers con runas de
|
|
|
`$adom` se prueban TODOS en `client` (`is-in-viewport.svelte.test.ts` y sus
|
|
|
once hermanos). Los dos tests nuevos siguen esa convención.
|
|
|
|
|
|
### 7.ter Supervisión de F1 (Fable, 2026-08-17) — veredicto y correcciones
|
|
|
|
|
|
**Estado de F1 al evaluarla**: los cinco wrappers, el recipe, los tokens, el
|
|
|
README y la migración del hero existen y pasan los gates de contrato
|
|
|
(`recipe-css-contract` · `component-api-contract` · `component-visual-attrs` ·
|
|
|
`eidos-lint` invalid 0 · `rtl:check` · `blocks:check` · `docs:check` · `check`
|
|
|
0 errores propios) y la verificación en Chrome de CINCO anfitriones (Section ·
|
|
|
Box-columna · Card `rounded`×shape · panel `fixed` · `Dialog.Content` real).
|
|
|
Sin commitear. Cinco desviaciones/hallazgos, ninguna oculta — todas están en la
|
|
|
entrega del constructor o las destapó el supervisor al re-ejecutar:
|
|
|
|
|
|
1. **La regla firmada en D-BG.14 no bastaba y el constructor la amplió** con
|
|
|
`--box-position: relative` (medido: los anfitriones basados en Box
|
|
|
computaban `static` porque `box.css` declara `position: var(--box-position,
|
|
|
revert-layer)` a (0,1,0), y sin `@layer` `revert-layer` cae al UA — la
|
|
|
trampa que `affix/types.ts:19` ya documentaba). La ampliación es correcta y
|
|
|
está medida, pero es una ENMIENDA del texto firmado y acopla la foundation
|
|
|
a un token público de Box → **D-BG.16**, a firmar.
|
|
|
2. **El bug de `Box flex` existe, está diagnosticado y NO arreglado.** F1
|
|
|
firmaba «si persiste, arreglo en Box PRIMERO»; el constructor lo reprodujo
|
|
|
(`<Box flex={2}>` → `0 1 auto`, 71px de 992) y diagnosticó el mecanismo
|
|
|
(`box.css:268-271`: el shorthand `flex:` va ANTES de los longhands, que
|
|
|
revierten al UA cuando su var está sin poner y borran su aportación;
|
|
|
inyectando `--box-grow: 2` en el mismo nodo → `2 1 auto`, 823px), pero se
|
|
|
detuvo por §10.3 («cero cambios en recipes ajenos») y lo escaló. Conflicto
|
|
|
entre dos textos del plan; el firmado (D-BG.12→F1) manda: **se arregla en
|
|
|
F1.5**, con guard.
|
|
|
3. **`Scrim` sin `color` hereda la paleta del anfitrión** (por semántica CSS,
|
|
|
no medido aún): `--palette-*` NO están registradas `inherits:false` (0
|
|
|
`@property --palette-` en `generated/base.css`) y la capa compartida las
|
|
|
pone en `[data-color='x']` — un `<Card color="teal">` que hospede un
|
|
|
`<Background.Scrim>` sin `color` le pasa `--palette-solid` por herencia y
|
|
|
la tinta del velo deja de ser `--color-overlay`. Es exactamente el «nesting
|
|
|
gap» que THM-2 cierra con una guarda de PRESENCIA (`[data-{c}][data-color],
|
|
|
[data-{c}][data-color-custom]`, ver el forward de `card` en
|
|
|
`generated/base.css`). `Pattern` no lo sufre porque SIEMPRE estampa
|
|
|
`data-color` (default `primary`), pero merece la misma guarda.
|
|
|
4. **El audit clasifica `background` como INTERACTIVE** (la parte `pause`
|
|
|
declara `role: 'button'` + `defaultElement: 'button'`; heurística de
|
|
|
`component-audit.ts:1471-1477`) → pide `apg` (A-1.4) y un tratamiento de
|
|
|
foco (R-1.5). El precedente exacto es `fab` (mismo caso, PASS): `apg` del
|
|
|
patrón button + `## Audit exceptions` con `R-1.5 exception:` (el foco es del
|
|
|
`Toggle` compuesto) + `## Sema events` («0 eventos propios; interactivo POR
|
|
|
COMPOSICIÓN»). Falta añadirlos.
|
|
|
5. **Docs desincronizadas por la migración**: el README del hero sigue diciendo
|
|
|
`Backdrop` en el mapa de composición (línea 18) y en la fila `backdrop`
|
|
|
(27); y la fila F1 de §7 pedía renombrar el snippet `backdrop`→`background`
|
|
|
mientras D-BG.13 lo firmó para F5 — el constructor siguió el texto FIRMADO
|
|
|
(bien), pero la fila de §7 queda corregida aquí: **el renombrado es de F5**.
|
|
|
|
|
|
Fuera de desviación, anotado: `[data-background-layer] > :where(img, video)`
|
|
|
(cover-fit del media del app) entró en F1 sin estar en su fila — es el receptor
|
|
|
del `<style>` justificado del hero y F2 lo consume; se acepta y se registra.
|
|
|
|
|
|
### 7.quater F2 ejecutada (2026-08-17) — medido en Chrome real
|
|
|
|
|
|
Entregado: `Background.Image` (compone `<Image>`, `sources` por modo, `priority`,
|
|
|
sin `alt` porque la capa es `aria-hidden`), `Background.Video` (las cinco
|
|
|
políticas), `Background.Pause` (D-BG.18), el snippet `pause` (D-BG.17), la
|
|
|
receta y sus dos tokens, y el layout `background` del hero refactorizado — el
|
|
|
bloque **se queda sin `<style>`**: su única excepción D-BLK.2 (el cover-fit) es
|
|
|
ya canon, y el `data-on` manual sobra porque el gemelo de foundation se lo da al
|
|
|
anfitrión (medido: título y bajada resuelven `rgb(255,255,255)` sin él).
|
|
|
|
|
|
**Tres defectos destapados por la verificación, los tres arreglados:**
|
|
|
|
|
|
1. **`effect_update_depth_exceeded`** — una capa que se registraba desde un
|
|
|
`$effect` escribía el contador del padre en fase de efectos, invalidaba al
|
|
|
hermano que lo lee y el flush no cerraba. No sólo hacía ruido: **mataba el
|
|
|
efecto raíz**, así que el botón se pintaba y todo clic posterior en la
|
|
|
superficie se ignoraba en silencio. Arreglado aplicando la ley que el propio
|
|
|
framework ya tenía escrita, **A30** (`anchor-nav-provider.svelte.ts:204`:
|
|
|
«register with the parent from the constructor, not a reactive effect»), más
|
|
|
`untrack` en la mutación y la decisión leída desde un componente aparte.
|
|
|
2. **El control de pausa computaba `position: relative`** y caía 18px fuera del
|
|
|
anfitrión: el `<Button>` compuesto declara `position: relative` en
|
|
|
`[data-button]`, misma (0,1,0), así que decidía el orden de hojas del
|
|
|
bundler. Clavado con `[data-background-pause][data-placement]` — (0,2,0), sin
|
|
|
`!important` ni envoltorio. Medido después: `absolute`, 12px/12px, dentro.
|
|
|
3. **Mi `size='sm'` inventado** en el control: retirado, se queda el del canon
|
|
|
(`md`, 36×36), que es el par que compone `MediaPlayer.PlayButton`.
|
|
|
|
|
|
**Comprobado en navegador**: control por defecto y de snippet alternan etiqueta
|
|
|
ES pausa↔reanudar, escriben `data-paused` y **congelan de verdad** la deriva
|
|
|
(`animation-play-state: paused`) · sin capa que se mueva no hay control · una
|
|
|
imagen rota oculta su capa (`data-status='error'`) y el patrón de debajo sigue
|
|
|
pintando · la buena carga con `object-fit: cover` a 192px · el control es
|
|
|
`<button type="button">`, enfocable, fuera de todo `aria-hidden` · el anfitrión
|
|
|
del hero es `relative` y la tinta sale a `rgb(255,255,255)`.
|
|
|
|
|
|
**DOS decisiones tuyas, medidas, sin tocar** (README §Gaps):
|
|
|
|
|
|
- **La escala de `strength` del scrim no está ordenada por peso**: `ghost` 0.30 ·
|
|
|
`scrim` 0.45 · `overlay` 0.65 · `muted` **0.65 (duplicado)** · `subtle`
|
|
|
**0.80 — el más pesado de todos, y su nombre dice lo contrario**. La causa es
|
|
|
que esos tokens nombran cuán opaco es un ELEMENTO, no cuánto vela un scrim.
|
|
|
Renombrar o renumerar es cambio de API, no arreglo de pasada.
|
|
|
- **Sobre una foto CLARA ningún peso llega a AA**: 2.10:1 el default, 4.42:1 el
|
|
|
más fuerte, con texto blanco. O entra un peso por encima de `--opacity-subtle`,
|
|
|
o se doctrina que la respuesta es el scrim graduado.
|
|
|
|
|
|
Gates: audit `--only background` **PASS 0/0** · eidos-lint invalid 0 ·
|
|
|
`blocks:check` 0/15 · `rtl:check` 0/180 · 441/442 en eidos+morfo (el fallo es el
|
|
|
`skin-media-player` de siempre) · `check` 0 errores propios · prettier limpio.
|
|
|
Sin commitear: falta tu orden.
|
|
|
|
|
|
### 7.quinquies F4 ejecutada (2026-08-18)
|
|
|
|
|
|
**La demo** (`web/routes/uix/components/background/+page.svelte`) sigue el v2 de
|
|
|
`demo-authoring.md`: las 9 pestañas, el harness compartido sin inventar paneles,
|
|
|
y la entrada en el nav bajo **Layout**, junto a `Section` — su anfitrión
|
|
|
canónico (ni `Surface` ni `Backdrop` tenían demo, así que no había precedente).
|
|
|
|
|
|
Dos cosas que este componente obliga a hacer distinto, y ambas son el punto:
|
|
|
|
|
|
- **El escenario muestra un ANFITRIÓN de verdad** (una `Section` con copia
|
|
|
encima), no el componente aislado. Background es invisible por sí solo: sin
|
|
|
anfitrión la vista previa sería un rectángulo vacío y lo que hay que enseñar
|
|
|
—que el padre se ADOPTA y el layout no se mueve— sería intestable.
|
|
|
- **Los chips son uniones completas por el TIPO, no a mano.** Cada fila se
|
|
|
deriva de un `Record<Union, 0>`: un miembro que falte es error de compilación
|
|
|
y uno inventado también, así que una fila de chips no puede derivar en
|
|
|
silencio del tipo que enseña.
|
|
|
|
|
|
**Un defecto que sólo la demo podía destapar, arreglado.** Con A30 el registro
|
|
|
de «esta capa se mueve» era un hecho de MONTAJE, así que cambiar `animate` en
|
|
|
caliente no hacía aparecer el control de pausa — invisible en una sonda, obvio
|
|
|
en cuanto hay un interruptor. `background-gradient.svelte` registra ahora al
|
|
|
init Y sigue el prop después, con un efecto que **no escribe nada en su primera
|
|
|
pasada**: el flush de montaje sigue limpio y un cambio posterior es una
|
|
|
escritura suelta en un grafo ya asentado. Medido en Chrome: el control aparece
|
|
|
al encender `animate`, sin `effect_update_depth_exceeded`.
|
|
|
|
|
|
**Las entradas documentales de D-BG.11**: (1) `next-features.md` §11 · (2) la
|
|
|
frase en `decisions/design-text-effects.md` (el mismo corte canon/pack leído
|
|
|
desde el otro lado) · (3) `PLAN-blocks-quality.md` Q0.3 → sucesor · (4)
|
|
|
`surface/README.md` §Gaps «scrim de autoría» → **CERRADO** por
|
|
|
`Background.Scrim` · (6) `glossary.md`: entrada `Background` **y** `Aura`
|
|
|
corregida — decía «Not built yet» y está construido, con sus familias
|
|
|
`delegate`/`sustain`.
|
|
|
|
|
|
**(5) queda DIFERIDA a F3 a propósito**: la nota de `motion-guide.md` §8 y la de
|
|
|
`MOTION_SERVICE_RFC` describen el parallax (D-BG.3), que no existe todavía. La
|
|
|
propia D-BG.11 lo prohíbe — «una nota que describe futuro es la deriva de
|
|
|
`packs.md`/`glossary.md` con Aura», que es exactamente el error que esta fase
|
|
|
acaba de corregir en el glosario. Se escriben cuando el parallax aterrice.
|
|
|
|
|
|
Gates: `component:audit --only background` **PASS 0 errores** (2 warnings
|
|
|
`D-1.5`/`D-4.3` idénticos a los del canario `button` — el harness v2 encapsula
|
|
|
el `MutationObserver` y el emit, así que el grep literal del audit no los ve) ·
|
|
|
`morfo:check` **PASS background** · `SMOKE_SCOPE=/uix/components/background`
|
|
|
**PASS** · `docs:check` **0 errores, 0 warnings** en 633 docs.
|
|
|
|
|
|
### 7.sexies F3 ejecutada (2026-08-18) — parallax, puntero, `attach`
|
|
|
|
|
|
Entregado sobre D-BG.3 (A) y D-BG.9, sin tocar `$motion` ni el registro de
|
|
|
presets: `speed` · `bleed` · `depth` · `spotlight` · `attach='fixed'`, el
|
|
|
fallback `--background-progress`, y los tokens `--background-parallax-travel` /
|
|
|
`--background-spotlight-*`. La demo expone los cuatro ejes.
|
|
|
|
|
|
**La decisión de diseño que no estaba prevista.** Dos ejes quieren mover la
|
|
|
MISMA capa —el scroll y el puntero— y una animación sobre `translate` gana a
|
|
|
cualquier declaración estática: el puntero habría dejado de existir sin más. Así
|
|
|
que el scroll anima una custom property REGISTRADA (`@property`, o interpolaría
|
|
|
a saltos) y un único `translate` compone los dos términos. Medido: parallax solo
|
|
|
→ `0px 30px`; con el puntero arriba-derecha y `depth: 20px` → `20px 10px`. Es
|
|
|
`translate`, nunca el shorthand `transform`, que es la misma ley que sigue el
|
|
|
lift del draggable con `scale`.
|
|
|
|
|
|
**Tres cosas que costaron medición:**
|
|
|
|
|
|
1. El shorthand `animation` pone `animation-duration: 0s`, y una línea de tiempo
|
|
|
de progreso necesita el `auto` inicial para mapear su intervalo sobre el
|
|
|
rango de scroll. Con `0s` la animación es instantánea y la capa no se mueve
|
|
|
nunca. Van longhands, y el porqué queda escrito en la receta.
|
|
|
2. Mi listener de puntero pedía un frame y, si el rect salía degenerado, **no lo
|
|
|
liberaba nunca**: un anfitrión sin caja en el primer movimiento (transición,
|
|
|
cambio de display) mataba el puntero para siempre. Reescrito sin frame: el
|
|
|
rect se CACHEA (invalidado por `pointerenter` y `observeResize`), la lectura
|
|
|
sale del camino caliente y el fallo desaparece por forma.
|
|
|
3. Las cuatro registraciones (`animated`/`pointer`/`scroll`/`fixed`) comparten
|
|
|
un solo sitio (`declare.svelte.ts`) con la regla A30 y su segunda mitad
|
|
|
—seguir el prop sin escribir en la primera pasada—, en vez de una copia del
|
|
|
baile por fichero.
|
|
|
|
|
|
**Verificado en Chrome real**: el puntero mueve `depth` y `spotlight` con los
|
|
|
valores exactos y ambos vuelven al centro al salir · `attach='fixed'` da
|
|
|
`position: fixed` en la capa y `clip-path: inset(0)` en la pila, y una pila sin
|
|
|
`attach` conserva su `overflow: clip` sin tocar · **RTL**: el `translate` del
|
|
|
puntero se mantiene FÍSICO (no espeja, que es lo correcto para un dispositivo
|
|
|
físico) y el bleed vive en el eje de bloque · la demo estampa `data-parallax`,
|
|
|
`--_background-speed`, `animation-timeline: view()` y el bleed al cambiar los
|
|
|
chips, con paridad en el snippet.
|
|
|
|
|
|
⚠️ **Lo que NO pude verificar, y por qué**: el panel del navegador va oculto y
|
|
|
con viewport 0×0, así que **las animaciones scroll-driven declaradas en CSS no
|
|
|
se activan** ahí — comprobado que es del ENTORNO y no del código con un caso
|
|
|
mínimo inyectado (un `div` pelado con `animation-timeline: view()` también sale
|
|
|
inactivo, mientras una `ViewTimeline` creada por API sobre el mismo sujeto marca
|
|
|
68%). Quedan por medir con Chrome visible: el travel real al hacer scroll, los
|
|
|
60 fps, el detector de reflow, y la rama `@supports not` (Chrome la soporta, así
|
|
|
que no se puede ejercitar aquí). Lo verificable —la composición de los dos ejes,
|
|
|
el puntero, `attach`, RTL— está medido arriba.
|
|
|
|
|
|
Gates: audit **PASS 0 errores** · eidos-lint invalid 0 · `rtl:check` 0/180 ·
|
|
|
`docs:check` 0/0 en 634 docs · `blocks:check` 0/18 · `morfo:check` PASS ·
|
|
|
`smoke` PASS · 441/442 (el fallo es el `skin-media-player` de siempre) ·
|
|
|
`check` 0 errores propios · prettier limpio.
|
|
|
|
|
|
### 7.septies F3.5 — correcciones de la segunda auditoría (2026-08-18)
|
|
|
|
|
|
Nueve hallazgos, dos de diseño. Ninguno cambia el mecanismo firmado; cambian
|
|
|
quién declara qué, y que lo escrito sea verdad.
|
|
|
|
|
|
**Diseño:**
|
|
|
|
|
|
1. **`attach='fixed'` era un footgun**: había que escribirlo en la capa Y en la
|
|
|
pila, y olvidar el segundo dejaba la capa `position: fixed` **pintando a
|
|
|
sangre por todo el viewport**, detrás de todo, sin error. Ahora la capa lo
|
|
|
DECLARA (`registerFixed`) y la pila se recorta sola — la prop de la pila
|
|
|
desaparece, así que no hay nada que olvidar. La forma CSS de D-BG.9 no
|
|
|
cambia; cambia quién pone el atributo. Medido: al poner `fixed` la pila
|
|
|
estampa `data-attach` y `clip-path: inset(0)`, y al volver a `scroll` los
|
|
|
retira.
|
|
|
2. **Un `speed` negativo invertía el bleed.** El bleed por defecto ES el travel,
|
|
|
y con travel negativo el `inset-block` salía positivo: la capa ENCOGÍA y
|
|
|
enseñaba justo los bordes que el bleed existe para tapar. Ahora usa la
|
|
|
magnitud (`max(t, -t)`). Medido: `-64px` con speed 1 y con speed −1.
|
|
|
|
|
|
**Verdad de lo escrito** (lo más importante de esta pasada): afirmé tres veces
|
|
|
—README, wrapper y plan— que el travel «va en el compositor / nada en el hilo
|
|
|
principal». **Es falso**: animar una custom property REGISTRADA no se puede
|
|
|
compositar, el navegador recalcula estilo cada frame. Es el precio de componer
|
|
|
los dos ejes en un `translate`, y para una decoración es el intercambio correcto
|
|
|
— pero había que decirlo, no lo contrario. Corregidas las tres frases y el
|
|
|
comentario del `will-change`, que se apoyaba en la premisa falsa. También decía
|
|
|
que el puntero iba «coalescido en un frame» cuando yo mismo había quitado ese
|
|
|
frame, y que las registraciones compartían un sitio cuando gradient y video
|
|
|
seguían con su copia — ahora **sí** lo comparten las cuatro.
|
|
|
|
|
|
**Menores:** el fallback JS se gatea por reduced-motion (el CSS ya se paraba
|
|
|
solo, el JS seguía midiendo) · el spotlight gana `--background-spotlight-color`
|
|
|
con guarda de paleta, como el scrim · documentados los cuatro límites (`speed`
|
|
|
muere bajo `attach='fixed'`; el `clip-path` cuadra las esquinas de una capa fija
|
|
|
en un anfitrión redondeado; `Dialog.Content` no es anfitrión de ese modo; el
|
|
|
fallback mide contra la ventana y `view()` contra el scroller más cercano) y las
|
|
|
vars de runtime `--background-pointer-*` / `--background-progress`.
|
|
|
|
|
|
**Demo:** `spotlight` y `depth` llegan ya a las cuatro clases de capa (antes el
|
|
|
interruptor **no hacía nada** con gradiente, imagen y vídeo), entra el control
|
|
|
`bleed`, las tablas API y Recipe dejan de mentir —fila de ejes compartidos y los
|
|
|
selectores de parallax/attach— y el snippet `pause` pierde el `onclick` que el
|
|
|
componente pisaba y un `aria-pressed` inerte.
|
|
|
|
|
|
Gates: audit **PASS 0** · eidos-lint invalid 0 · rtl 0/180 · docs 0/0 en 634 ·
|
|
|
blocks 0/18 · smoke PASS · 441/442 · `check` 0 propios · prettier limpio.
|
|
|
|
|
|
### 7.octies F5 ejecutada (2026-08-18) — rollout al tier
|
|
|
|
|
|
Los seis blocks de D-BG.10 (2) reciben `Background.*`. El (1) —el hero— aterrizó
|
|
|
en F2, y el (3) —la página compuesta— **ya existía** (`/blocks/landing`): no
|
|
|
había que construirla, había que verificarla con la decoración puesta, y eso es
|
|
|
lo que se hizo.
|
|
|
|
|
|
**La forma, uniforme y con un solo desvío**: cada block gana un prop `decor` que
|
|
|
monta `<Background.Pattern>` como HIJA de su `Section` — la misma que el hero ya
|
|
|
probó. El desvío es `cta`, donde la decoración va dentro del PANEL (`Surface`),
|
|
|
que es donde está el ojo.
|
|
|
|
|
|
**Dos hallazgos que sólo la medición podía dar, y que habría shippeado a ciegas:**
|
|
|
|
|
|
1. **Dentro del panel sólido del `cta`, las dos tramas TINTADAS hunden el titular
|
|
|
bajo AA**: `glow` lleva la copia blanca de 5,18:1 a **3,06:1** y `mesh` a
|
|
|
**2,27:1**, porque pintan un lavado claro anclado en `50% 0%` — exactamente
|
|
|
donde se asienta el titular. Las de línea toman la tinta de regla, oscurecen,
|
|
|
y la copia sube a **14,35:1** mientras la textura sigue leyéndose a 3,19:1
|
|
|
contra el panel. El default es `rings`, que irradia del mismo anclaje que
|
|
|
usaba el glow. Las tintadas se ofrecen igual, con el número en el tipo.
|
|
|
2. **Una trama tintada dentro del panel es invisible por construcción**:
|
|
|
`--_background-tint` cae en `--color-primary-solid`, que es EL MISMO valor que
|
|
|
pinta el panel — un halo del color del panel sobre el color del panel. El
|
|
|
block pasa `color="var(--color-content-on-solid)"`, el token que ya usa para
|
|
|
su copia; medido después, el tinte resuelve a `#ffffff`.
|
|
|
|
|
|
**Los defaults, y por qué dos están apagados.** `cta` `rings` · `stats-band`
|
|
|
`grid` (una banda de cifras se lee como medida) · `testimonials` y
|
|
|
`feature-split` `glow`. `site-footer` y `banner` reciben la capacidad **APAGADA**:
|
|
|
la columna B del plan de calidad no pide acabado en ninguno de los dos, y
|
|
|
encenderlo por nuestra cuenta sería inventar un aspecto que nadie pidió.
|
|
|
|
|
|
**Un desvío de alcance, declarado**: la columna B pedía «`Backdrop` alterno» POR
|
|
|
FILA en `feature-split`; se resuelve en la SECCIÓN. Una banda por fila obligaría
|
|
|
a cada `Row` a poseer el estado de alternancia, que es coordinación — y un block
|
|
|
que coordina deja de ser un block que no posee nada.
|
|
|
|
|
|
**Medido en Chrome sobre la página compuesta real** (`/blocks/landing`): cinco
|
|
|
pilas, los cinco anfitriones adoptados (`position: relative` + `isolation:
|
|
|
isolate`) sin que ningún block los posicione; la copia en modo claro mide
|
|
|
15,88:1 sobre las secciones decoradas y 5,18:1 sobre el panel del cta — AA en
|
|
|
todas. Sobre lienzo oscuro, medido aparte: 17,06 → 13,23:1 con `glow` y
|
|
|
12,37:1 con `grid`/`dots`.
|
|
|
|
|
|
Ledger: seis filas nuevas, **A-100…A-105**, con su mecanismo y su evidencia.
|
|
|
|
|
|
Gates: `blocks:check` 0/18 · `rtl:check` 0/180 · `docs:check` 0/0 en 634 ·
|
|
|
434/435 (el fallo es el `skin-media-player` de siempre) · `check` 0 errores
|
|
|
propios · prettier limpio.
|
|
|
|
|
|
### F1.5 — correcciones de supervisión (para el agente, ANTES del commit de F1)
|
|
|
|
|
|
| # | Qué | Cómo (exacto) | Gate |
|
|
|
| --- | --- | --- | --- |
|
|
|
| F1.5.a | **Box `flex`** (canon, `eidos/components/box`) | El wrapper expande el shorthand: `flex` prop → `--box-grow` / `--box-shrink` / `--box-basis` (número `n` → `n 1 0%`; `'none'` → `0 0 auto`; `'auto'` → `1 1 auto`; string de 1–3 tokens → asignación CSS estándar); `box.css` deja de declarar el shorthand `flex:` y conserva los tres longhands. Un test browser en `box` que monte `<Box display="flex">` con hijos `flex={1}`/`flex={2}` y afirme `computedStyle.flexGrow` y anchos (discriminante: hoy falla). README de Box: nota fechada. Verificar que Surface/Section/Container (componen Box) no cambian de aspecto: `check` + suite eidos + un vistazo a `/uix/components/{box,flex,surface}` | suite eidos verde · test nuevo rojo→verde · `check` 0 errores propios |
|
|
|
| F1.5.b | **Guarda de presencia de paleta** en `background.css` | `--_background-tint` y `--_background-scrim-ink` se resuelven desde `--palette-solid` SÓLO bajo `[data-background-layer][data-color], [data-background-layer][data-color-custom]`; el default (`--color-primary-solid` / `--background-scrim-color`) en la regla base. Medir en Chrome: `<Card color="teal">` + `<Background.Scrim/>` → `--_background-scrim-ink` == `--color-overlay` (hoy: teal) | `eidos-lint` invalid 0 · medición en navegador anotada |
|
|
|
| F1.5.c | **Audit** | morfo `background.ts`: `apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/button/'` (el `Toggle` compuesto es un botón; precedente `fab.ts:25`); README: `## Audit exceptions` (`R-1.5 exception:` foco del `Toggle` compuesto; `E-2.2` NO aplica: la receta se auto-importa) + `## Sema events` (0 propios; interactivo por composición) | `component-audit --only background` → 0 errores (queda el `Demo —` hasta F4) |
|
|
|
| F1.5.d | **README del hero** | Fila `decoration` → `Background.Pattern` (hijo de `Section`, no envoltorio; patrones `glow·mesh·grid·dots·lines·noise·rings·vignette·none`); fila `backdrop` intacta (F2 la refactoriza); nota fechada en Decisiones | `blocks:check` verde |
|
|
|
| F1.5.e | **Plan** | Fila F1 de §7: quitar «snippet backdrop→background» (es F5, D-BG.13); D-BG.14: añadir la enmienda `--box-position` si el autor firma D-BG.16 | `docs:check` |
|
|
|
|
|
|
Orden: a → b → c → d → e; después los gates de F1 completos otra vez y el
|
|
|
commit de F1 (sólo lo propio) por orden del autor.
|
|
|
|
|
|
**Ejecutada 2026-08-17 — a·b·c·d·e hechas, medidas en Chrome real** (sonda
|
|
|
`/uix/__probe-f15`, creada y borrada; el layout de `/uix` es el que carga el CSS
|
|
|
de componentes, el raíz no):
|
|
|
|
|
|
- **a — `Box flex`, ARREGLADO.** Antes: `<Box flex={2}>` computaba `0 1 auto` y
|
|
|
71 px de 992. Ahora, en una fila de 600 px: `flex={1}` → `1 1 0%` **146 px** ·
|
|
|
`flex={2}` → `2 1 0%` **292 px** (exactamente el doble) · `Surface flex={1}`
|
|
|
(compone Box) crece igual, 146 px · `flex="none"` → `0 0 auto` ·
|
|
|
`flex="0 0 120px"` → `0 0 120px`, 120 px · `grow={3}` junto a `flex={1}` →
|
|
|
`3 1 0%`, o sea el prop explícito sigue ganando. Suma: 146+146+292+2 gaps = 600.
|
|
|
- **b — guarda de paleta, CONFIRMADA.** Dentro de `<Card color="teal">`
|
|
|
(`--palette-solid` = teal): el scrim resuelve `rgb(0 0 0 / 0.66)` =
|
|
|
`--color-overlay` y el patrón `oklch(0.5556 0.1829 305.86)` =
|
|
|
`--color-primary-solid` — ninguno hereda el teal del anfitrión. Con
|
|
|
`<Background.Scrim color="teal">` sí resuelve teal: la adhesión sigue siendo
|
|
|
explícita. De paso, la adopción del anfitrión (D-BG.16) se re-midió en los dos
|
|
|
Cards: `position: relative` + `isolation: isolate`.
|
|
|
- **No es defecto**: el scrim pinta alfa efectiva **0.297** (`--color-overlay`
|
|
|
0.66 × `--opacity-scrim` 0.45). Es EXACTAMENTE lo que pintaba el scrim inline
|
|
|
del hero que sustituye, así que la migración es fiel; queda dicho en el README
|
|
|
que los pesos son relativos a una tinta ya translúcida.
|
|
|
- **c** — `component-audit --only background` → **PASS, 0 errores, 0 warnings**.
|
|
|
- **d** — README del hero: fila `decoration` reescrita (hijo de la Section, ocho
|
|
|
tramas) + nota fechada; el hallazgo `Box flex` de 2026-07-23 queda marcado
|
|
|
«Resuelto 2026-08-17» con la causa.
|
|
|
- **e** — fila F1 de §7 corregida (el renombre del snippet es F5) y D-BG.14
|
|
|
enlazada con su enmienda D-BG.16.
|
|
|
|
|
|
### 7.nonies La verificación con Chrome VISIBLE (2026-08-18) — y los dos defectos que destapó
|
|
|
|
|
|
Lo que F3 dejó sin medir porque el panel oculto no activa una animación
|
|
|
scroll-driven declarada en CSS. Hecho con el Chrome real del autor, pestaña al
|
|
|
frente. **`document.visibilityState` es la puerta**: con la pestaña oculta la
|
|
|
`ViewTimeline` existe, con `source` y `subject` correctos y `playState:
|
|
|
'running'`, y `currentTime` es `null` para siempre; en cuanto la pestaña se ve,
|
|
|
marca. Un `await requestAnimationFrame` en esa pestaña ni resuelve (CDP timeout
|
|
|
a 45s), así que ninguna medición por frames es posible ahí.
|
|
|
|
|
|
**Medido (los tres pendientes):**
|
|
|
|
|
|
- **El travel es real.** Anfitrión de 288px, `speed: 1`, scroll de página:
|
|
|
progreso 43,5% → 95,5%, `translate` `0px -8,34px` → `0px 58,19px`, monótono y
|
|
|
lineal, y `0px 64px` = `4rem` completos en el extremo. Los keyframes van
|
|
|
−travel → +travel, así que el punto medio es 0 (`-0,03px` a 49,98%) y la
|
|
|
amplitud pico a pico es el doble del token. Las cifras de §7.sexies (`0px 30px`,
|
|
|
`20px 10px`) NO eran falsas: probaban la COMPOSICIÓN, y la aritmética de ahora
|
|
|
las ratifica (52,6% → −64 + 128×0,526 = 3,37px, medido 3,37px).
|
|
|
- **60 fps sin un frame caído.** 200 frames de scroll continuo con el travel
|
|
|
vivo: mediana 16,7ms · p95 17,0 · **max 17,1** · 0 frames > 20ms. La base con
|
|
|
`speed: 0` salió PEOR (max 63,6ms, 6 > 20ms): ruido de entorno, no del
|
|
|
componente.
|
|
|
- **0 violaciones de reflow.** Mismos 200 frames moviendo LOS DOS ejes (scroll +
|
|
|
puntero con `depth`): el observador de Long Animation Frames que usa
|
|
|
`arts/perf` no reportó ni un frame largo, 0ms forzados. **Instrumento validado
|
|
|
por mutación** — un thrash deliberado de 65ms en la misma página sí se reporta
|
|
|
(43ms forzados, script atribuido); sin esa prueba el cero sería el de un guard
|
|
|
ciego.
|
|
|
- **`@supports not` sigue pendiente**: Chrome soporta scroll-driven, así que la
|
|
|
rama del fallback sólo se ejercita en Firefox. No verificable en esta sesión.
|
|
|
|
|
|
**Defecto 1 — un ancestro `overflow: hidden` mata el travel en silencio.**
|
|
|
`view()` se ancla al scroll container más cercano, y `hidden` lo es aunque no
|
|
|
pueda scrollear nunca. La demo de este componente lo sufría: `[data-uix-stage]`
|
|
|
(`web/routes/uix/uix.css`) declaraba `overflow: hidden`, la timeline se ancló al
|
|
|
stage y el progreso quedó clavado en 52,63% en TODA posición de scroll — sin
|
|
|
error, con un `translate` de aspecto plausible. Arreglado con `overflow: clip`,
|
|
|
que recorta igual, respeta el radio y no es scroll container: `timeline.source`
|
|
|
pasa a ser el `scrollingElement` y el travel aparece, con las cifras de arriba.
|
|
|
El límite es del COMPONENTE, no de la demo (un envoltorio con `overflow-x:
|
|
|
hidden` para contener decoración es el patrón más común de una landing, y hace
|
|
|
scroll container en ambos ejes), así que entra en el README como **quinto
|
|
|
límite**. Efecto colateral verificado en las 174 demos que comparten el stage:
|
|
|
`affix` idéntico (`position: fixed`, A/B `identical: true`), `sticky` idéntico
|
|
|
(4 pasadas alternando `clip`/`hidden`: `top 13`, `stuck ""` las cuatro — el
|
|
|
sujeto tiene su scroller interno ANTES del stage, así que el stage no
|
|
|
participaba); `anchor-nav` sí cambia y **a mejor**: su rail `position: sticky` es
|
|
|
HERMANO del scroller interno, con `hidden` no se pegaba nunca y se escapaba por
|
|
|
arriba perdiendo media lista, con `clip` se mantiene a la vista (offset 47px) y
|
|
|
se ve el item activo. Ninguna demo se toca.
|
|
|
|
|
|
**Defecto 2 — el eje del puntero se desviaba exactamente lo scrolleado.** El
|
|
|
rect del anfitrión se cacheaba en coordenadas de VIEWPORT y sólo se invalidaba
|
|
|
con `pointerenter` y `observeResize`; el scroll mueve el anfitrión sin disparar
|
|
|
ninguno de los dos. Medido con ratón real: un tick de rueda sobre un anfitrión
|
|
|
de 288px dejó `--background-pointer-y` en 0,062 donde la geometría pedía 0,831 —
|
|
|
error 0,769, que es 111px de scroll sobre media altura (0,771) a la centésima —
|
|
|
sacando el valor del rango −1…1 que las vars prometen, y despegando el
|
|
|
`spotlight` del cursor unos 115px a la vista. No se recuperaba hasta salir y
|
|
|
volver a entrar. Arreglado cacheando la caja en coordenadas de DOCUMENTO y
|
|
|
normalizando contra `pageX`/`pageY`, que en un evento REAL ya traen el scroll:
|
|
|
cero lecturas de layout en el camino caliente y cero listeners nuevos, que es la
|
|
|
propiedad que este componente defiende. Verificado con ratón real: error **0** en
|
|
|
los dos movimientos (−0,008 y 0,833 contra su geometría), secuencia
|
|
|
`pointerenter > pointermove > pointermove` sin `leave` de por medio, y el centro
|
|
|
del halo del spotlight a **1px** del píxel pedido. Residuo documentado: un
|
|
|
scroller ANIDADO vuelve a desviar (`pageY` no lo ve) y se autocura al reentrar.
|
|
|
|
|
|
⚠️ **Un `PointerEvent` sintético no puede verificar esto**: un evento construido
|
|
|
reporta `pageY === clientY`, sin sumar el scroll, así que el handler nuevo mide
|
|
|
mal por culpa del instrumento y parece roto (medido: −1,389 donde el ratón real
|
|
|
da −0,008). Descubrí el defecto con eventos sintéticos, pero sólo el ratón real
|
|
|
lo verifica. Segundo fantasma de la sesión: el primero fue leer `0,0` / `50%`
|
|
|
tras un `computer{screenshot}`, que es el `write(0,0)` del `pointerleave`, no una
|
|
|
medición.
|
|
|
|
|
|
---
|
|
|
|
|
|
### 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** — `role` sólo lo declara `pause`. 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 usa `chart`): los attrs que estampa
|
|
|
son de WRAPPER, y el único estado de contrato —la pausa— vive en el `Button`.
|
|
|
- **G-1.1** — faltaba `## Subset`. Añadida: `color` es el conjunto COMPLETO (en
|
|
|
decoración, restringir la paleta sería arbitrario) e `intent` no se acepta.
|
|
|
- **Deriva documental cazada**: el README decía en TRES sitios que el control de
|
|
|
pausa «IS the canonical `<Toggle>`» y que dispara `commit-toggle`. Compone
|
|
|
`IconButton` (D-BG.18) y el evento es `contact-activate` de `button.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 SUPRIMIR `color` (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 como `data-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ó:**
|
|
|
|
|
|
1. **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 sin `on` es 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 a `on='dark'` e ignoraba los otros dos casos — y con `on='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.
|
|
|
2. **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.
|
|
|
3. **`--opacity-scrim` sigue existiendo** (0,45, rol de opacidad de ELEMENTO, lo
|
|
|
leen `chart` y el mesh): el `md: 0.45` que shipeé por la mañana colisionaba con
|
|
|
él por valor. Con el suelo firmado, `md` pasa 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+`on` y miden; el hero ya midió los dos huecos de
|
|
|
`data-on` (Display/Link) — candidatos de canon aparte, no de esta pieza.
|
|
|
- **Firefox sin scroll-driven**: el fallback JS existe desde F3; nunca «no
|
|
|
hay parallax en Firefox».
|
|
|
- **`will-change`/compositing**: no se añade a ciegas (memoria de jitter);
|
|
|
sólo si una medición lo pide.
|
|
|
- **Pack ↔ canon**: la integración va por contexto opcional (pack lee); el
|
|
|
canon jamás importa el pack — `npm run check` verde al borrar `src/packs/`.
|
|
|
- **Naming**: renombrar/borrar `Backdrop` exige orden explícita del autor
|
|
|
(regla dura); hasta entonces `Background.Pattern` puede convivir con
|
|
|
`Backdrop` sin shim y el hero migra en F1.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 9. Fuentes externas consultadas (2026-08-17)
|
|
|
|
|
|
Mantine `BackgroundImage` / `Overlay` (docs) · Vuetify `v-parallax` (**no verificado en sesión**: la página devolvió sólo el título y el JSON de la API dio 429; la columna de la tabla §2 va por conocimiento previo — `src` + altura, una sola imagen) · react-scroll-parallax
|
|
|
(`ParallaxBanner` layers, props) · Motion/Framer `useScroll`/`useTransform` (+ ScrollTimeline
|
|
|
nativo con fallback) · Aceternity UI (backgrounds, parallax, spotlight) · Magic UI (Backgrounds)
|
|
|
· shadcn.io/background (100 fondos canvas/CSS) · Chakra v3 `Bleed`/layout helpers · daisyUI
|
|
|
`hero`/`hero-overlay` · MDN «CSS scroll-driven animations» + soporte 2026 (Chrome/Edge 115+,
|
|
|
Safari 26+, Firefox flag/Interop 2026, ~84 %) · WCAG 2.2.2 / 2.3.3 / 1.4.3.
|
|
|
|
|
|
---
|
|
|
|
|
|
## 10. Brief de ejecución para el agente constructor (Opus 5)
|
|
|
|
|
|
> El agente lo ejecuta; **Fable supervisa** (§11). El brief es autosuficiente:
|
|
|
> el agente NO descubre la doctrina por arqueología — la lee en el orden de
|
|
|
> abajo. Todo lo que no esté aquí ni en los docs enlazados es una PREGUNTA al
|
|
|
> supervisor, nunca una decisión propia.
|
|
|
|
|
|
### 10.1 Precondiciones (no arranca sin ellas)
|
|
|
|
|
|
1. **D-BG.1–14 (§6) FIRMADAS por el autor** — una fila sin «FIRMADA» bloquea la
|
|
|
fase que dependa de ella (F1 ← D-BG.1/2/13/14 · F2 ← D-BG.4/5/6 · F3 ←
|
|
|
D-BG.3/7/9 · F5 ← D-BG.10). El agente empieza por la fase más baja cuyas
|
|
|
decisiones estén firmadas y PARA en la primera que no.
|
|
|
2. **Rama propia** para el trabajo (`alpha-0.1-background` o la que el autor
|
|
|
nombre): el árbol actual (`alpha-0.1-dir-prefs`) lleva cambios sin commitear
|
|
|
ajenos a esta pieza (`cta`, docs, `scripts/__scratch-*`, `web/routes/alpha/`)
|
|
|
— **no tocarlos, no incluirlos, no hacer `git stash`/`reset`/`checkout` de
|
|
|
nada**. Crear la rama la ordena el autor.
|
|
|
3. Dev server: el que ya corra en el árbol (`npm run dev`); no reiniciarlo
|
|
|
(`reference_dev_server_restart_orphans_browser`), no arrancar otro en un
|
|
|
worktree para `morfo:check` (403 por `fs.allow`).
|
|
|
|
|
|
### 10.2 Lectura obligatoria (el «paquete mínimo» de `component-audit.md` §0, más los ejes de esta pieza)
|
|
|
|
|
|
Orden y TODO entero, no «la parte relevante»:
|
|
|
|
|
|
1. `docs/building-a-component.md` → `docs/guides/component-guide.md` (Build
|
|
|
contract + Before You Start §1–5) → `docs/guides/completion-checklist.md` →
|
|
|
`docs/guides/demo-authoring.md` (LOCKED) → `docs/guides/component-audit.md`
|
|
|
→ `docs/CANON.md` → `docs/architecture/morfo.md` + `soma.md` (§2 membresía)
|
|
|
→ `docs/canon/vocabularies.md` → `src/uix/eidos/components/README.md`.
|
|
|
2. Ejes de la pieza: `docs/theming/reference.md` (§3, §6, §25, §39, §40) ·
|
|
|
`docs/canon/tsc.md` · `docs/canon/recipe-contract.md` ·
|
|
|
`docs/theming/gradient-finish.md` (§10 fronteras, D8, D12) ·
|
|
|
`docs/theming/motion.md` + `motion-guide.md` (§8 límites) ·
|
|
|
`docs/architecture/packs.md` · `docs/architecture/blocks.md` ·
|
|
|
`docs/decisions/design-text-effects.md` · `src/arts/adom/README.md`.
|
|
|
3. Código de referencia (los precedentes que se imitan, no se reinventan):
|
|
|
`eidos/components/backdrop/*` (se absorbe) · `surface/*` (Box+tratamiento,
|
|
|
`on`) · `image/*` (se compone) · `scroll-frames/*` (progreso de scroll por
|
|
|
ActiveDom) · `motion/*` (contexto `seen`) · `toggle/*` (se compone para la
|
|
|
pausa) · `blocks/hero/{README.md,hero.svelte}` (el consumidor que motiva
|
|
|
todo) · `packs/ambient/ambient.svelte` + `arts/scene/{README.md,types.ts}`
|
|
|
(el pack que se montará DENTRO) · `eidos/lib/render-css.ts`
|
|
|
(`renderOnContextBlocks`) · `eidos/lib/primitives/static.ts` (gradientes,
|
|
|
scrim/opacity/blur) · `morfo/components/{surface,scroll-frames,image}.ts`.
|
|
|
4. Este plan entero, incluidas las enmiendas de cabecera.
|
|
|
|
|
|
### 10.3 Reglas duras del agente (además de `CLAUDE.md`)
|
|
|
|
|
|
- **Nunca commitear ni pushear**; nunca borrar ficheros (incluido
|
|
|
`eidos/components/backdrop/`) sin orden escrita del autor transmitida por el
|
|
|
supervisor. Nunca `--no-verify`.
|
|
|
- **Morfo-first** y morfo MÍNIMO: partes `provider` (la pila) · `layer`
|
|
|
(`aria-hidden` literal) · `pause`; `texts` `pause`/`play` con claves
|
|
|
IDÉNTICAS al catálogo; **ningún knob visual en el morfo** (regla 2026-08-15,
|
|
|
`image.ts`); `as const satisfies Morfo`; `validateMorfo` en test.
|
|
|
- **Sin soma**, sin pack sema, `scope: ['eidos']`, 0 eventos + `## Passive
|
|
|
justification` en el README. Si en algún punto parece necesitar un evento →
|
|
|
PARAR y preguntar (antes se lee `architecture/sema.md`).
|
|
|
- **`Background` NO compone `Box`** ni acepta props de layout; es un hijo del
|
|
|
padre; el anfitrión se adopta por la regla de foundation `:where(:has(>
|
|
|
[data-background]))` emitida por el generador (no por el recipe). La pausa
|
|
|
es un segundo nodo raíz.
|
|
|
- **Cero props nuevas en `Box`**, cero cambios en recipes ajenos (Card,
|
|
|
Section, Dialog…) para «hacer sitio»: si un anfitrión no funciona, se
|
|
|
reporta con medición, no se parchea desde fuera.
|
|
|
- **Canon nunca importa `src/packs/`**; `Background.Layer` es una ranura. La
|
|
|
integración con `Ambient` (D-BG.8) es una tarea DEL PACK, separada, después.
|
|
|
- Recipe: tokens `background-*` en `lib/recipes/base.ts` (TSC: `root` /
|
|
|
`host`), `generate:eidos-css`; sin color crudo, sin `opacity` literal
|
|
|
(`--opacity-*`), sin `box-shadow` literal, sin `@keyframes` sin
|
|
|
`/* functional: … */`, sin `!important` sin `/* important: … */`, sin
|
|
|
`--eidos-*`, sin `will-change` (memoria: jitter a DPR fraccional). Migrar
|
|
|
`backdrop.css` LITERALMENTE con prefijo `--background-pattern-*`.
|
|
|
- DOM sólo por `ActiveEidos.require().dom` (`listen`, `raf`, `measure`,
|
|
|
`writeProperty`, `observeIntersection`, `observeResize`, `getWindow`,
|
|
|
`prefersReducedMotion`); lecturas de layout SIEMPRE dentro de `dom.raf`/
|
|
|
`dom.measure`, nunca síncronas tras una escritura. `CSS.supports` vía
|
|
|
`dom.getWindow(node).CSS`.
|
|
|
- Composición: `Background.Image` compone `<Image>`; `Background.Pause`
|
|
|
compone `<Toggle>`; nada de `<img>`/`<button>` crudos.
|
|
|
- Demo: plantilla v2 de 9 tabs, chips = uniones completas, `SemaPanel` en
|
|
|
vacío justificado, snippet con paridad; canario `button`.
|
|
|
- README con las secciones que el audit exige (Baseline · Comparativa ≥3 ·
|
|
|
Decisiones · Gaps con disposición · Passive justification · Audit
|
|
|
exceptions) — Baseline = `Backdrop` + layout `background` del hero + semillas
|
|
|
`web/routes/demos/{heroscrolling,animations/background}` (referencia, no se
|
|
|
portan).
|
|
|
- Comentarios en inglés; tabs; comillas simples; sin `console.log`; sin
|
|
|
español nuevo en código.
|
|
|
- **Ante cualquier contradicción entre docs, o entre docs y código, o ante
|
|
|
una decisión no cubierta por §6: PARAR, escribirla con cita
|
|
|
(`fichero:línea`) y devolverla al supervisor.** No inventar campos, tipos,
|
|
|
tokens ni mecanismos.
|
|
|
|
|
|
### 10.4 Entregable por fase (lo que el supervisor recibe)
|
|
|
|
|
|
Al cerrar cada fase el agente entrega, en su mensaje final: (a) lista de
|
|
|
ficheros creados/modificados; (b) salida LITERAL de cada gate de la fase
|
|
|
(comando + resultado); (c) qué verificó en navegador y cómo (ruta, qué midió,
|
|
|
valores) — para F1+ una captura por anfitrión (Section · Box-columna · Card
|
|
|
rounded×shape · Dialog.Content) y para F2 la medición de contraste con el
|
|
|
método del hero (píxeles pintados, no `rgb()` parseado a mano); (d) dudas y
|
|
|
contradicciones encontradas, con cita; (e) lo que quedó fuera y por qué. Sin
|
|
|
adjetivos: números y rutas.
|
|
|
|
|
|
### 10.5 Prompt de arranque (para el `Agent`, `model: opus`)
|
|
|
|
|
|
> «Lee ENTERO `docs/process/PLAN-background.md` (incluidas las enmiendas de
|
|
|
> cabecera y §10) y después, en ese orden, toda la lectura de §10.2. Comprueba
|
|
|
> en §6 qué D-BG están FIRMADAS. Ejecuta SOLO la fase más baja de §7 cuyas
|
|
|
> decisiones estén firmadas, respetando §10.3, y PARA al terminarla entregando
|
|
|
> §10.4. No commitees, no borres, no toques ficheros ajenos a la pieza. Si
|
|
|
> algo no está decidido o contradice la doctrina, para y devuélvelo con cita.»
|
|
|
|
|
|
---
|
|
|
|
|
|
## 11. Protocolo de supervisión (Fable)
|
|
|
|
|
|
Por cada fase entregada, en este orden y sin saltarse ninguno:
|
|
|
|
|
|
1. **Firma**: comprobar que la fase sólo usó decisiones FIRMADAS; si el agente
|
|
|
decidió algo por su cuenta → se retira antes de revisar nada más.
|
|
|
2. **Gates, re-ejecutados por el supervisor** (no se acepta el pegado del
|
|
|
agente): `npm run check` (filtrado a los paths de la pieza + hero) ·
|
|
|
`npx vitest run src/uix/eidos` (recipe-css-contract, component-api-contract,
|
|
|
component-visual-attrs, generated-css, gradient-finish-guard) ·
|
|
|
`npx vitest run src/arts/adom` (F0) · `node --import tsx/esm
|
|
|
scripts/component-audit.ts --only background` (F4: PASS 0 errores) ·
|
|
|
`node scripts/eidos-lint.ts background` (invalid 0) · `npm run rtl:check` ·
|
|
|
`npm run blocks:check` (F1+, hero) · `npm run translations:check` ·
|
|
|
`npm run docs:check` (F4) · `morfo:check` + `SMOKE_SCOPE=/uix/components/background npm run smoke` (F4, con dev server).
|
|
|
3. **Diff contra doctrina** (lectura del diff entero, no de resumen): morfo
|
|
|
sin knobs · sin Box · regla `:has` en el generador y no en el recipe ·
|
|
|
tokens con TSC · anotaciones R-4.x · ActiveDom sin globales · lecturas
|
|
|
post-layout · Image/Toggle compuestos · nada en `src/packs/` · nada en
|
|
|
recipes ajenos · nada borrado sin orden · sin shims.
|
|
|
4. **Navegador REAL** (Chrome; el pane oculto suspende rAF y miente): los
|
|
|
cuatro anfitriones de F1; en F2 el vídeo (autoplay muted, pausa por
|
|
|
teclado, `aria-pressed`, pausa fuera de vista y con pestaña oculta,
|
|
|
reduced-motion emulado → poster, reduced-data → poster) y el contraste en
|
|
|
píxeles; en F3 el parallax con y sin soporte de `animation-timeline`
|
|
|
(`@supports` forzado / flag de Chrome), el detector `uix.perf` de reflow a
|
|
|
0, RTL con `dir="rtl"` (puntero físico, scroll vertical) y reduced-motion →
|
|
|
estático.
|
|
|
5. **Encapsulación**: el pack sigue fuera del canon (grep de imports); borrar
|
|
|
`src/packs/` en un worktree de prueba deja `npm run check` verde (F1+).
|
|
|
6. **Informe al autor**: 3–6 líneas — qué entregó el agente, qué verifiqué,
|
|
|
qué falló (con `fichero:línea`), qué decide él. Si hay decisión nueva → las
|
|
|
5 preguntas, UNA por mensaje, y PARAR.
|
|
|
7. Sólo tras el «ok» del autor: siguiente fase al agente. Los commits los
|
|
|
ordena el autor; el supervisor los prepara (staged verificado, mensaje) y
|
|
|
no los ejecuta sin orden.
|