uix(text-focus): temable — 63 % → 100 %, y un primitivo FANTASMA que pintaba por accidente

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 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent c5923c8e0a
commit 9c9c0c8652

@ -10,9 +10,9 @@
- **Medido**: 2026-08-22 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/` - **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**? - **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 - **Alcance global**: **56%** — 2634 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)_ - **Reparto**: público 2634 · privado 338 · global 1145 · literal 561 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 26 · **alcance < 20 %**: 19 · **alcance 100 %**: 15 · **con eje `size`**: 58 - **Sin token público propio**: 25 · **alcance < 20 %**: 19 · **alcance 100 %**: 16 · **con eje `size`**: 58
## Cómo se lee ## 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 | | 19 | [float-panel](./float-panel.md) | 20 | 59% | 53 | 28 |
| 20 | [field](./field.md) | 19 | 72% | 73 | 60 | | 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 Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward
(`_palette-*`), que no son contrato: un tema no puede nombrarlos. (`_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) ## 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 | – | | [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 | | [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 | – | | [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 | – | | [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 | | [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 | | [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 | | [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 | – | | [box](./box.md) | 100% | 9 | 9 | 0 | 0 | 0 | 0 | 43 | – |
| [label](./label.md) | 100% | 8 | 2 | 0 | 0 | 0 | 6 | 2 | – | | [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 | – | | [display](./display.md) | — | 6 | 0 | 0 | 0 | 0 | 6 | 0 | – |
| [text-circular](./text-circular.md) | 100% | 6 | 6 | 0 | 0 | 0 | 0 | 6 | – | | [text-circular](./text-circular.md) | 100% | 6 | 6 | 0 | 0 | 0 | 0 | 6 | – |
| [barcode](./barcode.md) | 100% | 5 | 5 | 0 | 0 | 0 | 0 | 7 | – | | [barcode](./barcode.md) | 100% | 5 | 5 | 0 | 0 | 0 | 0 | 7 | – |

@ -5,29 +5,24 @@
> Vista de conjunto: [README](./README.md) · método y protocolo: > Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7. > [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-20 · **Alcance**: **63%** — 5 de 8 knobs por token público - **Medido**: 2026-08-22 · **Alcance**: **100%** — 8 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)_ - **Knobs de apariencia**: 8 — público 8 · privado 0 · global 0 · literal 0 · sistema 0 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`** - **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` - **Eje `size`**: no · **ficheros**: `text-focus.css`
## 1. Knobs fuera de alcance ## 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 | _Ninguno._
| ---: | --- | --- | --- | --- |
| 1 | `text-focus.css:42` | `[data-text-focus-corner]` | `border-radius` | `var(--radius-xs, 3px)` |
### 1.2 A través de un privado (0) ### 1.2 A través de un privado (0)
_Ninguno._ _Ninguno._
### 1.3 Literales (2) ### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor | _Ninguno._
| ---: | --- | --- | --- | --- |
| 1 | `text-focus.css:39` | `[data-text-focus-corner]` | `width` | `1rem` |
| 2 | `text-focus.css:40` | `[data-text-focus-corner]` | `height` | `1rem` |
## 2. Sistema transversal (0) — informativo, fuera del ratio ## 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. Propuesta de corrección
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1) _Nada que proponer: no hay knobs fuera de alcance._
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`.
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) ### 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 `⚠`.
<!-- veredicto:start --> <!-- veredicto:start -->
_(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.
<!-- veredicto:end --> <!-- veredicto:end -->

@ -744,6 +744,27 @@ Lo que `textarea` añadió (2026-08-21):
un componente monta ANTES de comparar**. (Memoria un componente monta ANTES de comparar**. (Memoria
`worktree-devserver-fsallow`; vuelve a morder cada vez.) `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 - **⚠ 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 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 (una propiedad transicionada devuelve su valor INICIAL justo tras escribir el

@ -73,6 +73,8 @@ const PROPS = [
'maxInlineSize', 'maxInlineSize',
'boxShadow', 'boxShadow',
'opacity', 'opacity',
'filter',
'backdropFilter',
'textDecorationColor', 'textDecorationColor',
'textDecorationThickness', 'textDecorationThickness',
'textUnderlineOffset', 'textUnderlineOffset',

@ -75,6 +75,37 @@ Referencias externas: react-bits TrueFocus
- **No emite eventos sema**: el foco viajero es ambiental; el hover manual - **No emite eventos sema**: el foco viajero es ambiental; el hover manual
elige, no compromete. elige, no compromete.
## Talla y tema
**Guard R-5.4** (`npm run theming:sentinel -- text-focus <url>`): **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 ## Gaps
- Navegación por teclado del foco (flechas): **diferir** — exigiría un - Navegación por teclado del foco (flechas): **diferir** — exigiría un

@ -16,13 +16,13 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: var(--text-focus-gap, var(--space-4)); gap: var(--text-focus-gap);
} }
[data-text-focus-word] { [data-text-focus-word] {
position: relative; position: relative;
font-weight: var(--text-focus-word-weight, 900); font-weight: var(--text-focus-word-weight);
font-size: var(--text-focus-word-size, var(--font-size-4xl, 3rem)); font-size: var(--text-focus-word-size);
cursor: pointer; cursor: pointer;
} }
@ -36,11 +36,11 @@
[data-text-focus-corner] { [data-text-focus-corner] {
position: absolute; position: absolute;
width: 1rem; width: var(--text-focus-corner-size);
height: 1rem; height: var(--text-focus-corner-size);
border: 3px solid var(--text-focus-border-color, var(--color-primary-solid)); border: var(--text-focus-corner-width) solid var(--text-focus-border-color);
border-radius: var(--radius-xs, 3px); border-radius: var(--text-focus-corner-radius);
filter: drop-shadow(0 0 4px var(--text-focus-glow-color, var(--color-primary-solid))); filter: drop-shadow(0 0 4px var(--text-focus-glow-color));
} }
[data-text-focus-corner='tl'] { [data-text-focus-corner='tl'] {
top: -0.625rem; top: -0.625rem;

@ -4094,6 +4094,14 @@
--skin-media-player-scale-xl: 1.7; --skin-media-player-scale-xl: 1.7;
--skin-media-player-lamp-opacity: 0.18; --skin-media-player-lamp-opacity: 0.18;
--skin-media-player-key-radius: var(--radius-sm); --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-size: 12.5rem;
--text-circular-radius: var(--radius-full); --text-circular-radius: var(--radius-full);
--text-circular-weight: 900; --text-circular-weight: 900;

@ -6643,6 +6643,27 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// per-size private hides behind the fallback, so the public cannot kill a // per-size private hides behind the fallback, so the public cannot kill a
// scale. `color: currentColor` stays correct as a token — a custom property // scale. `color: currentColor` stays correct as a token — a custom property
// does not resolve `currentColor` until the point of use. // 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': { 'text-circular': {
size: '12.5rem', size: '12.5rem',
radius: 'var(--radius-full)', radius: 'var(--radius-full)',

Loading…
Cancel
Save

Powered by TurnKey Linux.