uix(text-circular): temable — 83 % → 100 %, declarando lo que ya consumía

6 claves en una entrada NUEVA de `recipes/base.ts`. Censo 83 % → **100 %**.

Ya consumía CINCO públicos «de facto» (`var(--text-circular-x, <default>)`)
pero **sin declarar ninguno**: un tema no los veía en `getRecipeTokens()` ni el
panel de la demo los listaba. Tokenizar ha sido sobre todo DECLARAR lo que ya
se consumía, más el `radius` que faltaba.

- **Aquí declarar es seguro, al revés que en `knob`**: no hay privado por talla
  detrás del fallback, así que el público no puede matar una escala.
- `color: currentColor` es correcto como token: una custom property no resuelve
  `currentColor` hasta el punto de uso, así que sigue tomando la tinta del nodo
  y no la del `:root` — verificado con el diff.
- `char-transition` guarda una transición COMPLETA
  (`all 0.5s cubic-bezier(0, 0, 0, 1)`): el gesto del carácter al girar,
  verbatim, no cuatro ejes sueltos.

EL HALLAZGO (→ next-features §13): **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 token, y un token vivo parecería muerto— pero
deja ciego al guard ante `char-transition`. Medido sin congelar: alcanza
(0.5s cubic-bezier → 9.5s linear). Si aparecen más tokens de esta clase
—transición, animación, easing— merecen un pase propio sin congelación, como
el que ya tiene el hover.

Gates: sonda antes/después = 0 diffs en 5.916 valores × 7 estados · censo
--only 100 % · R-5.4 5/6 con el mudo adjudicado · rtl:check 0 · docs:check 0 ·
check por fichero limpio · ledger verificado con import real (27 componentes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent bfaae722b9
commit c5923c8e0a

@ -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%** — 2630 de 4678 knobs pasan por un token público del componente
- **Reparto**: público 2630 · privado 338 · global 1147 · literal 563 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 27 · **alcance < 20 %**: 19 · **alcance 100 %**: 14 · **con eje `size`**: 58
- **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
## 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 (27)
## Sin ningún token público propio (26)
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-circular`](./text-circular.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-focus`](./text-focus.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)
@ -210,7 +210,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [toggle](./toggle.md) | 81% | 28 | 22 | 1 | 1 | 3 | 1 | 119 | y |
| [fab](./fab.md) | 82% | 17 | 14 | 0 | 2 | 1 | 0 | 10 | – |
| [select](./select.md) | 83% | 72 | 54 | 5 | 3 | 3 | 7 | 101 | y |
| [text-circular](./text-circular.md) | 83% | 6 | 5 | 0 | 1 | 0 | 0 | 0 | – |
| [calendar](./calendar.md) | 84% | 71 | 57 | 7 | 0 | 4 | 3 | 74 | – |
| [chat-message](./chat-message.md) | 84% | 84 | 65 | 0 | 6 | 6 | 7 | 75 | y |
| [grid-list](./grid-list.md) | 85% | 31 | 22 | 2 | 1 | 1 | 5 | 38 | – |
@ -251,6 +250,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [box](./box.md) | 100% | 9 | 9 | 0 | 0 | 0 | 0 | 43 | – |
| [label](./label.md) | 100% | 8 | 2 | 0 | 0 | 0 | 6 | 2 | – |
| [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 | – |
| [flex](./flex.md) | 100% | 2 | 2 | 0 | 0 | 0 | 0 | 7 | – |
| [grid](./grid.md) | 100% | 2 | 2 | 0 | 0 | 0 | 0 | 13 | – |

@ -5,18 +5,16 @@
> 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**: **83%** — 5 de 6 knobs por token público
- **Knobs de apariencia**: 6 — público 5 · privado 0 · global 1 · literal 0 · sistema 0 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`**
- **Medido**: 2026-08-22 · **Alcance**: **100%** — 6 de 6 knobs por token público
- **Knobs de apariencia**: 6 — público 6 · privado 0 · global 0 · literal 0 · sistema 0 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 6 pública(s) — `size`, `radius`, `weight`, `color`, `font-size`, `char-transition`
- **Eje `size`**: no · **ficheros**: `text-circular.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-circular.css:17` | `[data-text-circular]` | `border-radius` | `var(--radius-full, 9999px)` |
_Ninguno._
### 1.2 A través de un privado (0)
@ -38,17 +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-circular-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `radius` | `root` | `var(--radius-full, 9999px)` | 1 |
_Nada que proponer: no hay knobs fuera de alcance._
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -62,6 +50,22 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**Medido 2026-08-22.** Ya tenía **cinco públicos «de facto»** —consumidos como
`var(--text-circular-x, <default>)`— pero **ninguno declarado en el contrato**,
así que un tema no los ve en `getRecipeTokens()` ni el panel de la demo los
lista. Tokenizar es sobre todo DECLARAR lo que ya se consume, más el único
knob que faltaba.
1. **A diferencia de `knob`, aquí declarar es seguro**: no hay privado por
talla detrás del fallback, así que el público no puede matar una escala. El
fallback simplemente deja de usarse y el valor por defecto pasa al contrato,
que es donde un tema lo busca.
2. **El que faltaba es el `radius`** (`var(--radius-full, 9999px)`): el círculo.
3. **`color` con valor `currentColor` es correcto como token**: una custom
property no resuelve `currentColor` hasta el punto de uso, así que sigue
tomando la tinta del nodo y no la del `:root` — verificado con el diff.
4. **`char-transition` guarda una transición COMPLETA**
(`all 0.5s cubic-bezier(0, 0, 0, 1)`): es el gesto del carácter al girar, y
se conserva verbatim.
<!-- veredicto:end -->

@ -744,6 +744,15 @@ Lo que `textarea` añadió (2026-08-21):
un componente monta ANTES de comparar**. (Memoria
`worktree-devserver-fsallow`; vuelve a morder cada vez.)
- **⚠ 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
token, y un token vivo parecía muerto), pero deja ciego al guard ante
`text-circular.char-transition`, que guarda una transición completa. Medido
sin congelar: alcanza. Si aparecen más tokens de este tipo —transición,
animación, easing— merecen un pase propio sin congelación, como el que ya
tiene el hover.
- **⚠ El valor centinela GRANDE no prueba nada contra una propiedad capada.**
`skin-media-player` calcula su ancho como `min(100%, base × scale)`: el
`1234px` del guard se capa al contenedor y los siete tokens de tamaño leen

@ -314,6 +314,10 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'disabled-row-fg':
'no disabled row in the demo (0 mounted, counted); forced data-disabled → reaches (oklch(0.61 0 0) → rgb(1,2,3))'
},
'text-circular': {
'char-transition':
"the guard FREEZES transitions on purpose (a transitioned property returns its starting value), which is exactly what hides a token whose whole job is the transition. Measured without the freeze 2026-08-22 -> reaches (0.5s cubic-bezier -> 9.5s linear)"
},
textarea: {
'focus-input-border':
'consumed only under [data-focused], which the guard blurs away ON PURPOSE (the click-focus poisoning fix); focused by hand with the pointer parked → reaches (rgb(4,5,6))',

@ -74,6 +74,34 @@ Referencias externas: react-bits CircularText
- **No emite eventos sema**: el hover modula un loop ambiental; no hay
commit/emerge que percibir.
## Talla y tema
**Guard R-5.4** (`npm run theming:sentinel -- text-circular <url>`): 5/6 en
vivo; el que calla está adjudicado con su medición. Corrido 2026-08-22.
Contrato en `lib/recipes/base.ts > text-circular` (6 claves, entrada NUEVA).
Alcance del censo: **100 %** (era 83 %).
Ya consumía **cinco públicos «de facto»** —`var(--text-circular-x, <default>)`—
pero **sin declarar ninguno**, así que un tema no los veía en
`getRecipeTokens()` ni el panel de la demo los listaba. Tokenizar ha sido sobre
todo DECLARAR lo que ya se consumía, más el `radius` que faltaba.
**Aquí declarar es seguro, a diferencia de `knob`**: no hay privado por talla
detrás del fallback, así que el público no puede matar una escala.
`color: currentColor` es correcto como token — una custom property no resuelve
`currentColor` hasta el punto de uso, así que sigue tomando la tinta del nodo y
no la del `:root` (verificado con el diff). Y `char-transition` guarda una
transición COMPLETA (`all 0.5s cubic-bezier(0, 0, 0, 1)`): es el gesto del
carácter al girar, conservado verbatim.
**El guard no ve `char-transition` y es por diseño**: congela las transiciones
—porque una propiedad transicionada devuelve su valor inicial y un token vivo
parecería muerto—, que es exactamente lo que esconde un token cuyo trabajo ES
la transición. Medido sin congelar, alcanza (0.5s → 9.5s). Registrado en
`next-features.md` §13.
## Gaps
- Radio/curvatura configurables (elipse, arco parcial): **diferir** — el

@ -12,11 +12,11 @@
[data-text-circular] {
position: relative;
width: var(--text-circular-size, 12.5rem);
height: var(--text-circular-size, 12.5rem);
border-radius: var(--radius-full, 9999px);
font-weight: var(--text-circular-weight, 900);
color: var(--text-circular-color, currentColor);
width: var(--text-circular-size);
height: var(--text-circular-size);
border-radius: var(--text-circular-radius);
font-weight: var(--text-circular-weight);
color: var(--text-circular-color);
text-align: center;
cursor: pointer;
transform-origin: center;
@ -26,8 +26,8 @@
display: inline-block;
position: absolute;
inset: 0;
font-size: var(--text-circular-font-size, 1.5rem);
transition: var(--text-circular-char-transition, all 0.5s cubic-bezier(0, 0, 0, 1));
font-size: var(--text-circular-font-size);
transition: var(--text-circular-char-transition);
}
@media (prefers-reduced-motion: reduce) {

@ -4094,6 +4094,12 @@
--skin-media-player-scale-xl: 1.7;
--skin-media-player-lamp-opacity: 0.18;
--skin-media-player-key-radius: var(--radius-sm);
--text-circular-size: 12.5rem;
--text-circular-radius: var(--radius-full);
--text-circular-weight: 900;
--text-circular-color: currentColor;
--text-circular-font-size: 1.5rem;
--text-circular-char-transition: all 0.5s cubic-bezier(0, 0, 0, 1);
--knob-gap-sm: var(--size-sm-gap);
--knob-gap-md: var(--size-md-gap);
--knob-gap-lg: var(--size-lg-gap);

@ -6637,6 +6637,21 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'lamp-opacity': '0.18',
'key-radius': 'var(--radius-sm)'
},
// TextCircular already consumed five publics through
// `var(--text-circular-x, <default>)` without declaring any of them, so a
// theme could not see them. Declaring is safe here (unlike `knob`): no
// 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.
'text-circular': {
size: '12.5rem',
radius: 'var(--radius-full)',
weight: '900',
color: 'currentColor',
'font-size': '1.5rem',
// The character's turn, kept verbatim: it is a gesture, not four axes.
'char-transition': 'all 0.5s cubic-bezier(0, 0, 0, 1)'
},
knob: {
'gap-sm': 'var(--size-sm-gap)',
'gap-md': 'var(--size-md-gap)',

Loading…
Cancel
Save

Powered by TurnKey Linux.