diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 8ff697dc6..bec7257cf 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,8 +10,8 @@ - **Medido**: 2026-08-23 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/` - **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**? -- **Alcance global**: **66%** — 3019 de 4601 knobs pasan por un token público del componente -- **Reparto**: público 3019 · privado 318 · global 844 · literal 420 · sistema transversal 509 · excepción firmada 111 _(los dos últimos, fuera del ratio)_ +- **Alcance global**: **66%** — 3025 de 4598 knobs pasan por un token público del componente +- **Reparto**: público 3025 · privado 318 · global 838 · literal 417 · sistema transversal 509 · excepción firmada 114 _(los dos últimos, fuera del ratio)_ - **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 26 · **con eje `size`**: 57 ## Cómo se lee @@ -134,7 +134,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [toggle-group](./toggle-group.md) | 50% | 2 | 1 | 0 | 1 | 0 | 0 | 1 | – | | [sidebar](./sidebar.md) | 55% | 45 | 23 | 0 | 14 | 5 | 3 | 50 | – | | [audio-player](./audio-player.md) | 56% | 18 | 10 | 0 | 8 | 0 | 0 | 8 | – | -| [background](./background.md) | 57% | 26 | 13 | 1 | 6 | 3 | 3 | 31 | – | | [s-text-virtual-list](./s-text-virtual-list.md) | 58% | 12 | 7 | 0 | 3 | 2 | 0 | 9 | – | | [badge](./badge.md) | 59% | 22 | 13 | 6 | 1 | 2 | 0 | 65 | y | | [drawer](./drawer.md) | 60% | 74 | 43 | 8 | 16 | 5 | 2 | 54 | y | @@ -218,6 +217,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [color-field](./color-field.md) | 93% | 15 | 14 | 0 | 1 | 0 | 0 | 26 | y | | [scroll-area](./scroll-area.md) | 94% | 17 | 15 | 0 | 0 | 1 | 1 | 13 | – | | [tree-grid](./tree-grid.md) | 95% | 42 | 37 | 1 | 0 | 1 | 3 | 47 | – | +| [background](./background.md) | 95% | 23 | 19 | 1 | 0 | 0 | 3 | 37 | – | | [picker-shell](./picker-shell.md) | 95% | 21 | 20 | 0 | 1 | 0 | 0 | 31 | y | | [nav-tree](./nav-tree.md) | 96% | 24 | 22 | 0 | 0 | 1 | 1 | 18 | – | | [image](./image.md) | 96% | 23 | 22 | 1 | 0 | 0 | 0 | 23 | y | diff --git a/docs/audit/theming/background.md b/docs/audit/theming/background.md index 4707f1fac..e43b366b2 100644 --- a/docs/audit/theming/background.md +++ b/docs/audit/theming/background.md @@ -5,46 +5,39 @@ > Vista de conjunto: [README](./README.md) · método y protocolo: > [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7. -- **Medido**: 2026-08-23 · **Alcance**: **57%** — 13 de 23 knobs por token público -- **Knobs de apariencia**: 26 — público 13 · privado 1 · global 6 · literal 3 · sistema 3 · excepción 0 _(los dos últimos, fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 31 pública(s) — `pattern-rule`, `pattern-cell`, `pattern-dot-size`, `pattern-glow-size`, `pattern-glow-at`, `pattern-glow-strength`, `pattern-mesh-opacity`, `pattern-noise-opacity`, `pattern-vignette-strength`, `pattern-lines-width`, `pattern-lines-gap`, `pattern-lines-angle`, `pattern-rings-width`, `pattern-rings-gap`, `fade-size`, `fade-at`, `scrim-fg`, `scrim-fg-over-dark`, `scrim-fg-over-light`, `scrim-strength-xs`, `scrim-strength-sm`, `scrim-strength-md`, `scrim-strength-lg`, `scrim-strength-xl`, `gradient-drift-duration`, `pause-offset`, `pause-z`, `parallax-travel`, `spotlight-size`, `spotlight-strength`, `spotlight-fg` +- **Medido**: 2026-08-23 · **Alcance**: **95%** — 19 de 20 knobs por token público +- **Knobs de apariencia**: 23 — público 19 · privado 1 · global 0 · literal 0 · sistema 3 · excepción 3 _(los dos últimos, fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 37 pública(s) — `pattern-rule`, `pattern-cell`, `pattern-dot-size`, `pattern-glow-size`, `pattern-glow-at`, `pattern-glow-strength`, `pattern-mesh-image`, `pattern-mesh-opacity`, `pattern-noise-opacity`, `pattern-vignette-strength`, `pattern-lines-width`, `pattern-lines-gap`, `pattern-lines-angle`, `pattern-rings-width`, `pattern-rings-gap`, `fade-size`, `fade-at`, `scrim-fg`, `scrim-fg-over-dark`, `scrim-fg-over-light`, `scrim-blur-sm`, `scrim-blur-md`, `scrim-blur-lg`, `scrim-blur-xl`, `scrim-blur-xxl`, `scrim-strength-xs`, `scrim-strength-sm`, `scrim-strength-md`, `scrim-strength-lg`, `scrim-strength-xl`, `gradient-drift-duration`, `pause-offset`, `pause-z`, `parallax-travel`, `spotlight-size`, `spotlight-strength`, `spotlight-fg` - **Eje `size`**: no · **ficheros**: `background.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (6) +### 1.1 Directo a primitivo global (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `background.css:192` | `[data-background-layer][data-pattern='mesh']` | `background-image` | `var(--gradient-aurora)` | -| 2 | `background.css:379` | `[data-background-layer][data-kind='scrim'][data-blur='sm']` | `backdrop-filter` | `blur(var(--blur-sm))` | -| 3 | `background.css:382` | `[data-background-layer][data-kind='scrim'][data-blur='md']` | `backdrop-filter` | `blur(var(--blur-md))` | -| 4 | `background.css:385` | `[data-background-layer][data-kind='scrim'][data-blur='lg']` | `backdrop-filter` | `blur(var(--blur-lg))` | -| 5 | `background.css:388` | `[data-background-layer][data-kind='scrim'][data-blur='xl']` | `backdrop-filter` | `blur(var(--blur-xl))` | -| 6 | `background.css:391` | `[data-background-layer][data-kind='scrim'][data-blur='xxl']` | `backdrop-filter` | `blur(var(--blur-xxl))` | +_Ninguno._ ### 1.2 A través de un privado (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `background.css:264` | `[data-background-layer][data-kind='gradient']` | `background` | `var(--_background-gradient-image, transparent)` | +| 1 | `background.css:265` | `[data-background-layer][data-kind='gradient']` | `background` | `var(--_background-gradient-image, transparent)` | -### 1.3 Literales (3) +### 1.3 Literales (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `background.css:76` | `[data-background-layer] :where(img, video)` | `inline-size` | `100%` | -| 2 | `background.css:77` | `[data-background-layer] :where(img, video)` | `block-size` | `100%` | -| 3 | `background.css:239` | `[data-background-layer][data-pattern='noise']` | `background-image` | `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` | +_Ninguno._ -### 1.4 Excepciones firmadas (0) — fuera del ratio +### 1.4 Excepciones firmadas (3) — fuera del ratio Literales que llevan su anotación `/* literal: */` en la propia declaración: la válvula de recipe-contract §3, la misma que honra `component-audit`. **Una desviación firmada no es deuda** — se listan para que la razón se lea, no para acuñarlas. -_Ninguno._ +| # | fichero:línea | selector | propiedad | valor | +| ---: | --- | --- | --- | --- | +| 1 | `background.css:76` | `[data-background-layer] :where(img, video)` | `inline-size` | `100%` | +| 2 | `background.css:77` | `[data-background-layer] :where(img, video)` | `block-size` | `100%` | +| 3 | `background.css:240` | `[data-background-layer][data-pattern='noise']` | `background-image` | `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` | ## 2. Sistema transversal (3) — informativo, fuera del ratio Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). @@ -73,7 +66,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl ## 4. Propuesta de corrección -### 4.1 Tokens a declarar en `lib/recipes/base.ts` (2) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1) Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla @@ -83,8 +76,7 @@ no distingue lo que debería — se marca `⚠`. | token (`--background-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `layer-backdrop-filter` | `root` | ⚠ `blur(var(--blur-sm))` / `blur(var(--blur-md))` / `blur(var(--blur-lg))` / `blur(var(--blur-xl))` / `blur(var(--blur-xxl))` | 5 | -| `layer-bg-image` | `root` | ⚠ `var(--gradient-aurora)` / `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` | 2 | +| `layer-bg-image` | `root` | `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E")` | 1 | ### 4.2 Sin nombre mecánico (3) @@ -103,6 +95,75 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**EJECUTADO 2026-08-23 — 57 % → 95 %.** Seis claves nuevas (31 → 37), tres +literales firmados, un privado adjudicado y **19 tokens preexistentes medidos +uno a uno**: el componente no había pasado por R-5.4. + +**Las seis claves son la MISMA costura, dos veces.** El patrón `mesh` leía +`var(--gradient-aurora)` y los cinco pasos de escarcha leían `var(--blur-*)`, +los dos a pelo: el VALOR es del sistema, el KNOB es del componente. Con +`scrim-blur-*` hay además una razón dura — `blur` es familia MÉTRICA del eje +`scaling` (theming §23), así que un literal ahí sería ciego al zoom global. + +**Tres literales firmados.** El `100%` de `img` / `video` es identidad (una capa +de medio ES la caja de su capa). Y el **grano del ruido no se acuña**: es un +`feTurbulence` en un data-URI, la textura ES la técnica —el único patrón que el +UA no elimina bajo `forced-colors`— y re-escaparlo para meterlo en el contrato +cambiaría el valor serializado sin cambiar la imagen. Su mando sigue siendo +`pattern-noise-opacity`. + +**El 5 % que queda es el privado, y es canal de valor.** +`--_background-gradient-image` lo escribe `background-gradient.svelte` EN LÍNEA +desde la prop `colors`. Un público encima no lo alcanzaría —el inline gana— y +mentiría: es el caso de `--gp-current-gradient` del gradient-picker y del +`preview-z` de drag-drop que la revisión de F2-B retiró. + +**Una sospecha mía, refutada midiendo.** La línea del mesh hacía exactamente lo +que un apunte de referencia da por roto: un gradiente MESH consumido por el +longhand `background-image` (su color base final no es una capa de imagen +válida, así que computa `none`). Medido aquí: `--gradient-aurora` **pinta** —su +serialización empieza por un `radial-gradient` y el navegador la acepta—, así +que no hay defecto. La nota vale para un mesh serializado con color base +final, no para este token. + +**El guard midió 3 de 37 y ahora mide 18, con un campo nuevo.** Las partes de +este componente son CAPAS OPT-IN independientes (velo, foco, pausa…), cada una +tras su control, y sólo un patrón se renderiza a la vez: sobre el escenario por +defecto casi todo el contrato no tiene nodo que pintar. `prepareWith` (nuevo: +enciende TODOS los controles que se le den, al contrario que `openWith`, que +para en el primero) más el barrido de `data-pattern` lo llevan a 18. + +**Y una lección del propio guard: montar MÁS puede medir MENOS.** Encender +además el foco puntual, la velocidad de paralaje y la profundidad hizo que la +corrida bajase de **17 a 9**: los tres repintan el `background-image` y el +`translate` de la misma capa donde se medían los patrones. `prepareWith` se dejó +en lo que no tapa. + +**Las 19 adjudicaciones, todas medidas forzando el atributo sobre la capa REAL** +(nunca sobre un nodo fabricado): las dos del mask (`fade-*`, que el guard no +puede ver porque no fotografía `mask-image` — hueco de instrumento, §13), las +dos del contexto de tinta (`on='dark'` / `on='light'`), los ocho pasos de las +dos escalas (blur 4→33, 8→33, 12→33, 24→33 px; strength alpha 0.08 · 0.13 · 0.4 +· 0.7 → 0.9), el `gradient-drift-duration` (24s → 42s, en una capa `gradient` +que EXCLUYE a la de patrón), los tres del foco puntual (40% → 42px…, y +`spotlight-fg` sólo cuando la capa NO lleva `data-color`: con él manda +`--palette-solid`, por diseño), el `parallax-travel` (sólo entra por las +keyframes ligadas al scroll, que el escenario medido no tiene) y los dos de la +pausa (la demo no monta ese control: cero nodos en la página). + +**Un aviso para quien mida aquí: los chips de esta demo se aplican con RETRASO.** +Una comparación por chips capturó cada estado con el patrón ANTERIOR —el estado +«mesh» guardaba el glow— y dio 0 diffs por estar desfasada igual en las dos +corridas. La prueba buena fue la de EQUIVALENCIA, determinista: cada token nuevo +resuelve al mismo valor que el primitivo que sustituyó +(`pattern-mesh-image` = `--gradient-aurora`; `scrim-blur-{k}` = `--blur-{k}` = +4/8/12/16/24 px), y lo pintado al forzar el atributo es idéntico (el mesh pinta +el aurora, el ruido pinta su data-URI intacto). + +**Verificación**: sonda estándar antes/después **0 diffs** (384 valores · 7 +estados) · equivalencia token↔primitivo exacta en las seis claves · R-5.4 18/37 +con las 19 restantes adjudicadas y medidas, cero STALE · `component:audit` PASS +· censo 95 % · `eidos-lint` 3 morfo-backed / 63 eidos-only / **0 invalid, 0 +class-hooks** · `docs:check` 0. diff --git a/docs/next-features.md b/docs/next-features.md index 41413c4cf..103b0df82 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -1282,6 +1282,25 @@ Lo que `chart` destapó (2026-08-22): waveform, chronos, los pickers) puede pasarle primitivos crudos sin que el censo ni `recipe-css-contract` lo vean. Medir cuántas hay antes de decidir si D-TH.2 se ensancha. +- **El guard no fotografía `mask-image`** (medido 2026-08-23 en `background`). + Sus dos tokens de desvanecido (`fade-size`, `fade-at`) alimentan un + `mask-image` radial y leen muertos aunque están vivos — medidos a mano, el + mask cambia (`at 50% 35%` → `at 11% 50%`). Añadir `maskImage` a la lista de + propiedades del guard es una línea, pero toca el instrumento de TODOS: entra + con su re-verificación de los componentes con ledger. +- **Montar MÁS capas puede medir MENOS** (medido 2026-08-23 en `background`). + Al enseñar al guard a encender las capas opt-in del componente, añadir el foco + puntual, la velocidad de paralaje y la profundidad hizo BAJAR la corrida de 17 + a 9 tokens: los tres repintan el `background-image` y el `translate` de la + misma capa donde se miden los patrones. La regla operativa: `prepareWith` sólo + debe encender lo que NO tapa lo ya medido, y eso hay que comprobarlo corriendo + el guard antes y después de cada control que se añada. +- **Los chips de la demo de `background` se aplican con RETRASO.** Una captura + por chips guardó cada estado con el patrón ANTERIOR (el estado «mesh» tenía el + glow) y dio 0 diffs por estar desfasada IGUAL en las dos corridas — un gate + verde sobre dos medidas equivocadas. Donde la demo no coopere, la prueba buena + es la de EQUIVALENCIA (el token nuevo resuelve al mismo valor que el primitivo + que sustituyó) más forzar el atributo en el DOM, las dos deterministas. - **Un panel de HOVER se cierra por el propio guard.** `navigation-menu` abre con `pointerenter` y `pointerleave` programa el cierre; el guard abre con clic y luego aparca el puntero en (0,0), que es exactamente el gesto que lo diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 8a5242a2b..eeb379cb5 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -18,6 +18,54 @@ */ export const SENTINEL_EXCEPTIONS: Record> = { + background: { + // 2026-08-23. Its parts are independent OPT-IN layers and only ONE pattern + // renders at a time, so the default stage moved 3 of 37 tokens. The guard + // now switches the scrim on and sweeps `data-pattern` (18/37); what is + // left is measured here, forcing the attribute on the REAL layer — never + // on a node built by hand. + // + // The mask: the guard does not read `mask-image`, so both read dead while + // being perfectly alive (instrument gap, next-features §13). + 'fade-size': 'the guard does not snapshot mask-image; measured by hand → reaches (the radial mask changes)', + 'fade-at': + 'the guard does not snapshot mask-image; measured by hand → reaches (at 50% 35% → at 11% 50%)', + // The ink context: only under `on='dark'` / `on='light'`. + 'scrim-fg-over-dark': "only under on='dark'; forced → reaches (the veil colour changes)", + 'scrim-fg-over-light': "only under on='light'; forced → reaches (the veil colour changes)", + // Two scales where only the step in force paints (the `section` / `qr-code` + // class): every step measured one by one on the real scrim layer. + 'scrim-blur-sm': 'only the step in force paints; forced data-blur=sm → reaches (4px → 33px)', + 'scrim-blur-md': 'only the step in force paints; forced data-blur=md → reaches (8px → 33px)', + 'scrim-blur-lg': 'only the step in force paints; forced data-blur=lg → reaches (12px → 33px)', + 'scrim-blur-xxl': 'only the step in force paints; forced data-blur=xxl → reaches (24px → 33px)', + 'scrim-strength-xs': + 'only the step in force paints; forced data-strength=xs → reaches (alpha 0.08 → 0.9)', + 'scrim-strength-sm': + 'only the step in force paints; forced data-strength=sm → reaches (alpha 0.13 → 0.9)', + 'scrim-strength-lg': + 'only the step in force paints; forced data-strength=lg → reaches (alpha 0.4 → 0.9)', + 'scrim-strength-xl': + 'only the step in force paints; forced data-strength=xl → reaches (alpha 0.7 → 0.9)', + // A gradient layer EXCLUDES a pattern layer, so the sweep cannot have both. + 'gradient-drift-duration': + 'the gradient layer is exclusive with the pattern one the sweep needs; forced data-kind=gradient → reaches (24s → 42s)', + // The spotlight is off on the default stage and, once on, it repaints the + // pattern layer it shares a node with — measured on its own pass. + 'spotlight-size': 'spotlight is off in the demo; forced data-spotlight → reaches (40% 40% → 42px)', + 'spotlight-strength': + 'spotlight is off in the demo; forced data-spotlight → reaches (the mix stop moves)', + 'spotlight-fg': + "spotlight is off AND the demo layer carries data-color, where --palette-solid wins by design; forced data-spotlight with no data-color → reaches", + // Structural, not conditional: the travel only enters through the + // scroll-linked `background-parallax` keyframes, which need a scroll + // timeline the measured stage does not have. + 'parallax-travel': + 'only enters via the scroll-linked keyframes (--_background-parallax-offset); the measured stage has no scroll timeline', + // No node at all: the demo never mounts the pause control. + 'pause-offset': 'the pause control is not mounted in the demo (0 nodes on the page)', + 'pause-z': 'the pause control is not mounted in the demo (0 nodes on the page)' + }, 'color-field': { // 2026-08-23. Thirteen tokens, none of them a lie: the demo boots with a // value, in `md`, editable, with three formats allowed — so every state diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index a12ce2a8b..dfe8da60f 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -139,6 +139,16 @@ const COMPONENT_OVERRIDES: Record< urls?: string[]; /** Nodes to measure that carry NO `data-{c}-*` attr (prose styles bare HTML). */ extraNodes?: string; + /** + * Demo controls to switch ON before measuring — ALL of them, unlike + * `openWith`, which stops at the first that works because it opens ONE + * surface. A component whose parts are independent OPT-IN layers + * (background: scrim, spotlight, pause… each behind its own control) + * measured 3 of 37 tokens on the default stage: what the demo does not + * mount has no node to paint. Failures are ignored — a control that is + * not there is not an error, it is one layer this route cannot show. + */ + prepareWith?: string[]; /** * How to tell the surface is ALREADY open, when it is not * `[data-{c}-content]`. Without it the re-open guard that runs before every @@ -199,6 +209,25 @@ const COMPONENT_OVERRIDES: Record< // pointer parking the guard does after a click (`mouse.move(0, 0)`) shut it // MID-RUN — two runs of the same code disagreed on one token. 'navigation-menu': { openBy: 'hover', extraNodes: '[data-navigation-menu-content] a' }, + // Its parts are independent OPT-IN layers, each behind its own demo control, + // and only ONE pattern renders at a time: on the default stage the guard saw + // a single `glow` layer and 3 of 37 tokens moved. Switch the extra layers on, + // then sweep the pattern axis. + background: { + // ONLY the layers that do not COVER what is already being measured: + // switching the spotlight, the parallax speed and the pointer depth on + // repaints the pattern layer's own `background-image` and its transform, + // and the run went BACKWARDS (17 → 9 tokens). A guard that mounts more + // can measure less. + prepareWith: [ + "[data-uix-control]:has([data-uix-control-label]:text-matches('^scrim')) input[type=checkbox]", + "[data-uix-control]:has([data-uix-control-label]:text-matches('^blur')) [data-uix-chip]:text-is('xl')" + ], + sweepAttr: { + attr: 'data-pattern', + values: ['glow', 'mesh', 'grid', 'dots', 'noise', 'vignette', 'lines', 'rings'] + } + }, // Same class: its chrome is an embedded Slider the recipe re-tints, and the // playhead IS that slider's thumb — `data-waveform*` matched 4 nodes and none // of them was it. @@ -393,6 +422,17 @@ async function main() { // A surface that boots OPEN must not be clicked SHUT: onion-menu renders its // sectors from the start, and the opening click toggled the whole recipe out // of the document (12 of its 20 tokens read dead, 2026-08-23). + for (const sel of override.prepareWith ?? []) { + const el = page.locator(sel).first(); + if (!(await el.count())) continue; + try { + await el.click({ timeout: 1500 }); + await page.waitForTimeout(350); + } catch { + /* one layer this route cannot show */ + } + } + const alreadyOpen = override.openMarker ? (await page.locator(override.openMarker).count()) > 0 : false; diff --git a/src/uix/eidos/components/background/README.md b/src/uix/eidos/components/background/README.md index 026362fb8..a660532f3 100644 --- a/src/uix/eidos/components/background/README.md +++ b/src/uix/eidos/components/background/README.md @@ -298,6 +298,37 @@ never attached.** Parallax is motion tied to the reader's own scrolling, which is the exact class that triggers vestibular symptoms. The layer stays where it is: `reduce="static"`, not `hide`. +## Talla y tema + +37 claves públicas en `lib/recipes/base.ts` (bloque `background`). El componente +no tiene eje `size`: sus escalas propias son **la fuerza del velo** +(`scrim-strength-{xs…xl}`) y **la escarcha** (`scrim-blur-{sm…xxl}`), cada una +resuelta por su atributo (`data-strength`, `data-blur`). + +Lo que entró el **2026-08-23** (57 % → 95 %), y por qué: + +- **`pattern-mesh-image`** — el patrón `mesh` leía `var(--gradient-aurora)` a + pelo. La costura: el VALOR es del catálogo de gradientes, el KNOB es de este + componente, así que un tema puede dar otro mesh sin mover el aurora en todas + partes. Verificado: el token resuelve al mismo valor que el primitivo. +- **`scrim-blur-{sm,md,lg,xl,xxl}`** — los cinco pasos leían `var(--blur-*)` a + pelo. Misma costura, y con una razón extra: `blur` es familia MÉTRICA del eje + `scaling` (theming §23), así que un literal ahí sería ciego al zoom global. + Medido paso a paso: 4 · 8 · 12 · 16 · 24 px, idénticos antes y después. + +**Lo que NO se acuña**, y queda firmado en su declaración: + +- **El grano del ruido** es un `feTurbulence` en un data-URI: la textura ES la + técnica —el único patrón que el UA no elimina bajo `forced-colors`— y + re-escaparlo para meterlo en el contrato cambiaría el valor serializado sin + cambiar la imagen. `pattern-noise-opacity` sigue siendo su mando. +- **El `100%` de `img` / `video`** es identidad: una capa de medio ES la caja de + su capa. +- **`--_background-gradient-image` es CANAL DE VALOR, no superficie de tema**: + lo escribe `background-gradient.svelte` en línea, desde la prop `colors`. Un + público encima no lo alcanzaría (el inline gana) y mentiría — el mismo caso + que `--gp-current-gradient` del gradient-picker. + ## Preferences | Preference | What happens | diff --git a/src/uix/eidos/components/background/background.css b/src/uix/eidos/components/background/background.css index 14fc947f9..50a21a8ee 100644 --- a/src/uix/eidos/components/background/background.css +++ b/src/uix/eidos/components/background/background.css @@ -73,8 +73,8 @@ * of the app's own. */ [data-background-layer] :where(img, video) { display: block; - inline-size: 100%; - block-size: 100%; + inline-size: 100%; /* literal: identity — a media layer IS its layer's box */ + block-size: 100%; /* literal: identity — a media layer IS its layer's box */ object-fit: cover; } @@ -189,7 +189,7 @@ } [data-background-layer][data-pattern='mesh'] { - background-image: var(--gradient-aurora); + background-image: var(--background-pattern-mesh-image); opacity: var(--background-pattern-mesh-opacity); } @@ -236,7 +236,8 @@ only pattern the UA does NOT strip under forced colors — which is why the forced-colors block below drops every layer rather than trusting it. */ [data-background-layer][data-pattern='noise'] { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E"); + /* prettier-ignore */ + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E"); /* literal: the grain IS the technique — an inline feTurbulence, the one pattern the UA does not strip under forced colors; re-escaping it into the contract would change the serialised value, not the picture */ opacity: var(--background-pattern-noise-opacity); } @@ -376,19 +377,19 @@ } [data-background-layer][data-kind='scrim'][data-blur='sm'] { - backdrop-filter: blur(var(--blur-sm)); + backdrop-filter: blur(var(--background-scrim-blur-sm)); } [data-background-layer][data-kind='scrim'][data-blur='md'] { - backdrop-filter: blur(var(--blur-md)); + backdrop-filter: blur(var(--background-scrim-blur-md)); } [data-background-layer][data-kind='scrim'][data-blur='lg'] { - backdrop-filter: blur(var(--blur-lg)); + backdrop-filter: blur(var(--background-scrim-blur-lg)); } [data-background-layer][data-kind='scrim'][data-blur='xl'] { - backdrop-filter: blur(var(--blur-xl)); + backdrop-filter: blur(var(--background-scrim-blur-xl)); } [data-background-layer][data-kind='scrim'][data-blur='xxl'] { - backdrop-filter: blur(var(--blur-xxl)); + backdrop-filter: blur(var(--background-scrim-blur-xxl)); } /* ── Parallax, pointer depth, spotlight ────────────────────────────────────── diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 17852e7f6..1cfb9af65 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -3511,6 +3511,7 @@ --background-pattern-glow-size: 60% 55%; --background-pattern-glow-at: 50% 0%; --background-pattern-glow-strength: 28%; + --background-pattern-mesh-image: var(--gradient-aurora); --background-pattern-mesh-opacity: var(--opacity-scrim); --background-pattern-noise-opacity: var(--opacity-ghost); --background-pattern-vignette-strength: 38%; @@ -3524,6 +3525,11 @@ --background-scrim-fg: var(--color-surface-default); --background-scrim-fg-over-dark: var(--color-content-on-solid-contrast); --background-scrim-fg-over-light: var(--color-content-on-solid); + --background-scrim-blur-sm: var(--blur-sm); + --background-scrim-blur-md: var(--blur-md); + --background-scrim-blur-lg: var(--blur-lg); + --background-scrim-blur-xl: var(--blur-xl); + --background-scrim-blur-xxl: var(--blur-xxl); --background-scrim-strength-xs: 0.08; --background-scrim-strength-sm: 0.13; --background-scrim-strength-md: 0.19; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 59e532a10..7714c008a 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -5462,6 +5462,14 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'pattern-glow-strength': '28%', // A mesh is a veil at scrim weight — the same 0.45 Backdrop shipped, now // tied to the semantic scale instead of a literal (R-4.2). + // The mesh pattern IS a system gradient — the seam: the VALUE belongs to + // the gradient catalogue, the KNOB belongs to this component, so a theme + // can give the mesh another image without moving `--gradient-aurora` + // everywhere it is used. (Measured 2026-08-23: it paints as a longhand + // `background-image` here — this token's serialisation starts with a + // radial-gradient, so the mesh-through-`background-image` trap does not + // apply to it.) + 'pattern-mesh-image': 'var(--gradient-aurora)', 'pattern-mesh-opacity': 'var(--opacity-scrim)', 'pattern-noise-opacity': 'var(--opacity-ghost)', 'pattern-vignette-strength': '38%', @@ -5495,6 +5503,16 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ // legibility over ANY photograph: it clears BOTH framework floors in BOTH // contexts (on-solid's APCA ≥ 60 ∧ WCAG ≥ 3, and §40's AA 4.5 for text) — // measured `on='dark'` 6.45:1 / Lc 85, `on='light'` 8.29:1 / Lc 61. + // The frost behind the scrim, one step per `blur` value. Same seam as the + // mesh above: the scale is the system's (`--blur-*`), the knob is the + // component's — a theme can soften the scrim's frost without touching + // every blur in the app. `blur` is a METRIC family of the `scaling` axis + // (theming §23), so these must stay tokens, never literals. + 'scrim-blur-sm': 'var(--blur-sm)', + 'scrim-blur-md': 'var(--blur-md)', + 'scrim-blur-lg': 'var(--blur-lg)', + 'scrim-blur-xl': 'var(--blur-xl)', + 'scrim-blur-xxl': 'var(--blur-xxl)', 'scrim-strength-xs': '0.08', 'scrim-strength-sm': '0.13', 'scrim-strength-md': '0.19', diff --git a/web/routes/uix/components/background/+page.svelte b/web/routes/uix/components/background/+page.svelte index d27508661..468c61ec9 100644 --- a/web/routes/uix/components/background/+page.svelte +++ b/web/routes/uix/components/background/+page.svelte @@ -31,6 +31,7 @@ import { compileMorfo } from '$uix/morfo'; import { backgroundMorfo } from '@/uix/morfo/components/background'; import { getActiveUix } from '$active-uix'; + import TokensPanel from '../../lib/TokensPanel.svelte'; import PalettePicker from '../../lib/PalettePicker.svelte'; import SystemAxes from '../../lib/SystemAxes.svelte'; @@ -49,6 +50,7 @@ | 'api' | 'morfo' | 'recipe' + | 'tokens' | 'a11y'; let tab = $state('live'); @@ -354,6 +356,9 @@ + @@ -886,6 +891,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'recipe'}

Eidos recipe