From 9c9c0c8652edeb2af9f23f3cb6540bd25d4fc042 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 22 Aug 2026 04:12:57 +0200 Subject: [PATCH] =?UTF-8?q?uix(text-focus):=20temable=20=E2=80=94=2063=20%?= =?UTF-8?q?=20=E2=86=92=20100=20%,=20y=20un=20primitivo=20FANTASMA=20que?= =?UTF-8?q?=20pintaba=20por=20accidente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8 claves en una entrada NUEVA de `recipes/base.ts`. Censo 63 % → **100 %**. Mismo patrón que text-circular: cinco públicos «de facto» consumidos con fallback y ninguno declarado, más tres que faltaban — los tres en la ESQUINA del marco de foco. **La esquina es una escuadra dibujada con bordes**: 1rem × 1rem con dos de sus cuatro lados a `0` según la posición. El tamaño es un knob, el grosor del trazo otro, el radio el tercero. Los desplazamientos `-0.625rem` NO son knobs: colocan la escuadra fuera del marco y se derivan del tamaño y el trazo. `border-color` y `glow-color` son el mismo acento en dos capas y se declaran aparte a propósito: un tema puede querer el halo de otro tono que el borde. EL HALLAZGO — **`--radius-xs` NO EXISTE** (→ next-features §13): La escala de radios del sistema es `sm | md | lg | xl | xxl` (+ none, full, default), sin `xs`. Esta receta escribía `border-radius: var(--radius-xs, 3px)` y **pintaba el fallback**, así que nadie lo notó nunca. Al tokenizarlo tal cual, la variable quedó vacía, la declaración inválida y las cuatro esquinas CUADRADAS: **48 diffs** — exactamente para lo que existe el gate. El contrato guarda `3px` verbatim, el valor que de verdad pintaba, y el diff vuelve a 0. Lo que abre: **un `var(--fantasma, fallback)` es una referencia muerta que funciona por accidente**, y el guard G2 que el plan menciona («referencias-fantasma contra el contrato derivado») o no existe o no cubre los fallbacks. Barrer el catálogo buscando `var(--…, …)` cuyo primer nombre no esté en el contrato derivado es un pase mecánico que se paga solo. Y un arreglo de instrumento: **`filter` y `backdrop-filter` faltaban en la lista de propiedades del guard**. `glow-color` vive dentro de un `drop-shadow()` y leía «no effect» estando vivo — `KNOB_PROPS` del censo sí las tenía, así que el censo contaba el knob y el guard no podía verlo. Con ellas, text-focus pasa a **8/8 sin una sola excepción**. Verificados los 30 componentes con ledger: cero regresiones, cero STALE. (De paso se retiró una adjudicación que había escrito con una razón equivocada antes de medir: el token sí movía el filter.) Gates: sonda antes/después = 0 diffs en 1.769 valores × 7 estados · censo --only 100 % · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por fichero limpio. Co-Authored-By: Claude Fable 5 --- docs/audit/theming/README.md | 12 ++--- docs/audit/theming/text-focus.md | 52 ++++++++----------- docs/next-features.md | 21 ++++++++ scripts/theming-sentinel.ts | 2 + src/uix/eidos/components/text-focus/README.md | 31 +++++++++++ .../components/text-focus/text-focus.css | 16 +++--- src/uix/eidos/generated/base.css | 8 +++ src/uix/eidos/lib/recipes/base.ts | 21 ++++++++ 8 files changed, 120 insertions(+), 43 deletions(-) diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index dc4809247..53db1394a 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,9 +10,9 @@ - **Medido**: 2026-08-22 · **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**: **56%** — 2631 de 4678 knobs pasan por un token público del componente -- **Reparto**: público 2631 · privado 338 · global 1146 · literal 563 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 26 · **alcance < 20 %**: 19 · **alcance 100 %**: 15 · **con eje `size`**: 58 +- **Alcance global**: **56%** — 2634 de 4678 knobs pasan por un token público del componente +- **Reparto**: público 2634 · privado 338 · global 1145 · literal 561 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 25 · **alcance < 20 %**: 19 · **alcance 100 %**: 16 · **con eje `size`**: 58 ## Cómo se lee @@ -91,12 +91,12 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma | 19 | [float-panel](./float-panel.md) | 20 | 59% | 53 | 28 | | 20 | [field](./field.md) | 19 | 72% | 73 | 60 | -## Sin ningún token público propio (26) +## Sin ningún token público propio (25) Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward (`_palette-*`), que no son contrato: un tema no puede nombrarlos. -[`alert-dialog`](./alert-dialog.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`cascade`](./cascade.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`mask-field`](./mask-field.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`range-calendar`](./range-calendar.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) +[`alert-dialog`](./alert-dialog.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`cascade`](./cascade.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`mask-field`](./mask-field.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`range-calendar`](./range-calendar.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) ## Tabla completa (162 recetas, por alcance ascendente) @@ -171,7 +171,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [emoji-picker](./emoji-picker.md) | 61% | 32 | 17 | 0 | 8 | 3 | 4 | 17 | – | | [pin-input](./pin-input.md) | 61% | 19 | 11 | 0 | 6 | 1 | 1 | 20 | y | | [clipboard](./clipboard.md) | 63% | 8 | 5 | 2 | 1 | 0 | 0 | 6 | – | -| [text-focus](./text-focus.md) | 63% | 8 | 5 | 0 | 1 | 2 | 0 | 0 | – | | [spin-field](./spin-field.md) | 64% | 27 | 16 | 0 | 8 | 1 | 2 | 20 | – | | [meter](./meter.md) | 64% | 14 | 9 | 1 | 2 | 2 | 0 | 28 | y | | [tag-group](./tag-group.md) | 65% | 46 | 28 | 14 | 0 | 1 | 3 | 91 | y | @@ -249,6 +248,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [time-field](./time-field.md) | 100% | 11 | 11 | 0 | 0 | 0 | 0 | 21 | y | | [box](./box.md) | 100% | 9 | 9 | 0 | 0 | 0 | 0 | 43 | – | | [label](./label.md) | 100% | 8 | 2 | 0 | 0 | 0 | 6 | 2 | – | +| [text-focus](./text-focus.md) | 100% | 8 | 8 | 0 | 0 | 0 | 0 | 8 | – | | [display](./display.md) | — | 6 | 0 | 0 | 0 | 0 | 6 | 0 | – | | [text-circular](./text-circular.md) | 100% | 6 | 6 | 0 | 0 | 0 | 0 | 6 | – | | [barcode](./barcode.md) | 100% | 5 | 5 | 0 | 0 | 0 | 0 | 7 | – | diff --git a/docs/audit/theming/text-focus.md b/docs/audit/theming/text-focus.md index 1120f2f42..4047bca32 100644 --- a/docs/audit/theming/text-focus.md +++ b/docs/audit/theming/text-focus.md @@ -5,29 +5,24 @@ > Vista de conjunto: [README](./README.md) · método y protocolo: > [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7. -- **Medido**: 2026-08-20 · **Alcance**: **63%** — 5 de 8 knobs por token público -- **Knobs de apariencia**: 8 — público 5 · privado 0 · global 1 · literal 2 · sistema 0 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`** +- **Medido**: 2026-08-22 · **Alcance**: **100%** — 8 de 8 knobs por token público +- **Knobs de apariencia**: 8 — público 8 · privado 0 · global 0 · literal 0 · sistema 0 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 8 pública(s) — `gap`, `word-weight`, `word-size`, `corner-size`, `corner-width`, `corner-radius`, `border-color`, `glow-color` - **Eje `size`**: no · **ficheros**: `text-focus.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (1) +### 1.1 Directo a primitivo global (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `text-focus.css:42` | `[data-text-focus-corner]` | `border-radius` | `var(--radius-xs, 3px)` | +_Ninguno._ ### 1.2 A través de un privado (0) _Ninguno._ -### 1.3 Literales (2) +### 1.3 Literales (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `text-focus.css:39` | `[data-text-focus-corner]` | `width` | `1rem` | -| 2 | `text-focus.css:40` | `[data-text-focus-corner]` | `height` | `1rem` | +_Ninguno._ ## 2. Sistema transversal (0) — informativo, fuera del ratio @@ -41,22 +36,7 @@ _La receta no declara privados propios en su CSS._ ## 4. Propuesta de corrección -### 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 -al final) y theming §6.7 (slots de color, modificador delante). Un token con -DOS valores distintos es una colisión de nombre: son dos knobs, o el nombre -no distingue lo que debería — se marca `⚠`. - -| token (`--text-focus-…`) | scope TSC | valor propuesto | usos | -| --- | --- | --- | ---: | -| `corner-radius` | `root` | `var(--radius-xs, 3px)` | 1 | - -### 4.2 Sin nombre mecánico (2) - -- **⚠ decisión: `width` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `width`. -- **⚠ decisión: `height` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `height`. +_Nada que proponer: no hay knobs fuera de alcance._ ### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) @@ -70,6 +50,20 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Medido 2026-08-22.** Mismo patrón que `text-circular`: cinco públicos «de +facto» consumidos con fallback y **ninguno declarado**, más tres knobs que sí +faltaban, todos en la ESQUINA del marco de foco. + +1. **La esquina es una escuadra dibujada con bordes**: `1rem × 1rem` con dos de + sus cuatro lados a `0` según la posición (`tl`, `tr`, `bl`, `br`). El tamaño + es UN knob (`corner-size`, las dos dimensiones), el grosor del trazo otro + (`corner-width`, hoy `3px` a pelo) y el radio el tercero. +2. **Los desplazamientos `-0.625rem` de cada esquina NO son knobs**: colocan la + escuadra justo fuera del marco y su valor se deriva del tamaño y el trazo — + son construcción de la forma, como las proporciones del spinner. +3. **`border-color` y `glow-color` son el mismo acento en dos capas** (el trazo + y su resplandor via `drop-shadow`), y los dos ya leen el mismo default. Se + declaran por separado a propósito: un tema puede querer el halo de otro tono + que el borde. diff --git a/docs/next-features.md b/docs/next-features.md index 4b374bfce..aed0bfab5 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -744,6 +744,27 @@ Lo que `textarea` añadió (2026-08-21): un componente monta ANTES de comparar**. (Memoria `worktree-devserver-fsallow`; vuelve a morder cada vez.) +- **⚠ `--radius-xs` NO EXISTE, y una receta lo referenciaba.** La escala de + radios del sistema es `sm | md | lg | xl | xxl` (+ `none`, `full`, + `default`), sin `xs`. `text-focus` escribía + `border-radius: var(--radius-xs, 3px)`: **pintaba el fallback**, así que + nadie lo notó — hasta que tokenizarlo sin fallback dejó la variable vacía, la + declaración inválida y las cuatro esquinas cuadradas (**48 diffs**, medidos + 2026-08-22). Corregido a `3px` verbatim, el valor que de verdad pintaba. + + Lo que esto dice del catálogo: **un `var(--fantasma, fallback)` es una + referencia muerta que funciona por accidente**, y el guard G2 que el plan + menciona («referencias-fantasma contra el contrato derivado») o no existe o + no cubre los fallbacks. Barrer el catálogo buscando `var(--…, …)` cuyo primer + nombre no esté en el contrato derivado es un pase mecánico que se paga solo. + +- **⚠ `filter` y `backdrop-filter` faltaban en la lista de propiedades del + guard** — arreglado 2026-08-22. `text-focus.glow-color` vive dentro de un + `drop-shadow()` y leía «no effect» estando vivo; `KNOB_PROPS` del censo sí + las tenía, así que el censo contaba el knob y el guard no podía verlo. + Verificados los 30 componentes con ledger tras el cambio: cero regresiones, + cero STALE. + - **⚠ El guard CONGELA las transiciones, y eso esconde los tokens cuyo trabajo ES la transición.** La congelación existe por una razón buena y documentada (una propiedad transicionada devuelve su valor INICIAL justo tras escribir el diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index b79b550b2..1df30225b 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -73,6 +73,8 @@ const PROPS = [ 'maxInlineSize', 'boxShadow', 'opacity', + 'filter', + 'backdropFilter', 'textDecorationColor', 'textDecorationThickness', 'textUnderlineOffset', diff --git a/src/uix/eidos/components/text-focus/README.md b/src/uix/eidos/components/text-focus/README.md index a26c24a71..55b905457 100644 --- a/src/uix/eidos/components/text-focus/README.md +++ b/src/uix/eidos/components/text-focus/README.md @@ -75,6 +75,37 @@ Referencias externas: react-bits TrueFocus - **No emite eventos sema**: el foco viajero es ambiental; el hover manual elige, no compromete. +## Talla y tema + +**Guard R-5.4** (`npm run theming:sentinel -- text-focus `): **8/8** tokens +mueven un computed en vivo, sin una sola excepción. Corrido 2026-08-22. + +Contrato en `lib/recipes/base.ts > text-focus` (8 claves, entrada NUEVA). +Alcance del censo: **100 %** (era 63 %). + +Ya consumía cinco públicos «de facto» con fallback y **sin declarar ninguno**; +los otros tres faltaban, y están todos en la ESQUINA del marco de foco. + +**La esquina es una escuadra dibujada con bordes**: `1rem × 1rem` con dos de +sus cuatro lados a `0` según la posición (`tl`, `tr`, `bl`, `br`). El tamaño es +un knob, el grosor del trazo otro y el radio el tercero. Los desplazamientos +`-0.625rem` **no** son knobs: colocan la escuadra justo fuera del marco y se +derivan del tamaño y el trazo. + +`border-color` y `glow-color` son el mismo acento en dos capas —el trazo y su +resplandor vía `drop-shadow`— y se declaran por separado a propósito: un tema +puede querer el halo de otro tono que el borde. + +### ⚠ `--radius-xs` no existe, y esta receta lo referenciaba + +La escala de radios del sistema es `sm | md | lg | xl | xxl`, sin `xs`. El CSS +escribía `var(--radius-xs, 3px)` y **pintaba el fallback**, así que nadie lo +notó. Tokenizarlo tal cual dejó la variable vacía, la declaración inválida y +las cuatro esquinas CUADRADAS — 48 diffs, que es exactamente para lo que sirve +el gate. El contrato guarda `3px` verbatim, el valor que de verdad pintaba. +Registrado en `next-features.md` §13 junto con la pregunta que abre: cuántos +`var(--fantasma, fallback)` más hay en el catálogo. + ## Gaps - Navegación por teclado del foco (flechas): **diferir** — exigiría un diff --git a/src/uix/eidos/components/text-focus/text-focus.css b/src/uix/eidos/components/text-focus/text-focus.css index 3b5416520..9ff3316fb 100644 --- a/src/uix/eidos/components/text-focus/text-focus.css +++ b/src/uix/eidos/components/text-focus/text-focus.css @@ -16,13 +16,13 @@ flex-wrap: wrap; justify-content: center; align-items: center; - gap: var(--text-focus-gap, var(--space-4)); + gap: var(--text-focus-gap); } [data-text-focus-word] { position: relative; - font-weight: var(--text-focus-word-weight, 900); - font-size: var(--text-focus-word-size, var(--font-size-4xl, 3rem)); + font-weight: var(--text-focus-word-weight); + font-size: var(--text-focus-word-size); cursor: pointer; } @@ -36,11 +36,11 @@ [data-text-focus-corner] { position: absolute; - width: 1rem; - height: 1rem; - border: 3px solid var(--text-focus-border-color, var(--color-primary-solid)); - border-radius: var(--radius-xs, 3px); - filter: drop-shadow(0 0 4px var(--text-focus-glow-color, var(--color-primary-solid))); + width: var(--text-focus-corner-size); + height: var(--text-focus-corner-size); + border: var(--text-focus-corner-width) solid var(--text-focus-border-color); + border-radius: var(--text-focus-corner-radius); + filter: drop-shadow(0 0 4px var(--text-focus-glow-color)); } [data-text-focus-corner='tl'] { top: -0.625rem; diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 13f0fd025..9744b151a 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -4094,6 +4094,14 @@ --skin-media-player-scale-xl: 1.7; --skin-media-player-lamp-opacity: 0.18; --skin-media-player-key-radius: var(--radius-sm); + --text-focus-gap: var(--space-4); + --text-focus-word-weight: 900; + --text-focus-word-size: var(--font-size-4xl, 3rem); + --text-focus-corner-size: 1rem; + --text-focus-corner-width: 3px; + --text-focus-corner-radius: 3px; + --text-focus-border-color: var(--color-primary-solid); + --text-focus-glow-color: var(--color-primary-solid); --text-circular-size: 12.5rem; --text-circular-radius: var(--radius-full); --text-circular-weight: 900; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index aabe6b43b..0b060c544 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -6643,6 +6643,27 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ // per-size private hides behind the fallback, so the public cannot kill a // scale. `color: currentColor` stays correct as a token — a custom property // does not resolve `currentColor` until the point of use. + // TextFocus, same shape as text-circular: five publics were consumed through a + // fallback and none of them declared. The corner is a BRACKET drawn with + // borders — two of its four sides are zeroed per position — so its size is one + // knob, its stroke another. The `-0.625rem` offsets are NOT knobs: they place + // the bracket just outside the frame and follow from size + stroke. + 'text-focus': { + gap: 'var(--space-4)', + 'word-weight': '900', + 'word-size': 'var(--font-size-4xl, 3rem)', + 'corner-size': '1rem', + 'corner-width': '3px', + // 3px verbatim, NOT `var(--radius-xs)`: that primitive DOES NOT EXIST. + // The recipe wrote `var(--radius-xs, 3px)` and the fallback was what + // painted — tokenizing the phantom name turned the declaration invalid and + // the corners square (48 diffs, measured 2026-08-22). + 'corner-radius': '3px', + // The stroke and its halo: same accent, two layers, declared apart so a + // theme can tint the glow differently from the border. + 'border-color': 'var(--color-primary-solid)', + 'glow-color': 'var(--color-primary-solid)' + }, 'text-circular': { size: '12.5rem', radius: 'var(--radius-full)',