uix(spin-field): ADJUDICADO — 67 % es su techo, y lo dice la cabecera de su propio CSS

CERO claves nuevas, un literal firmado, siete adjudicaciones medidas. Diff de
computed 0 sobre 2.400 valores en 8 estados.

EL TECHO ESTABA ESCRITO, Y ES DOCTRINA

Ocho de sus knobs leen los privados RESUELTOS del Field
(--_field-control-height, --_field-control-padding-inline, --_field-control-gap)
porque los steppers y el scrubber IGUALAN la fila del input a cualquier talla.
La cabecera del CSS lo dice con estas palabras: «never a parallel --spin-field-*
chrome that would alias the Field base». Acuñar ahí sería el antipatrón que el
fichero prohíbe por su nombre. Préstamo con dueño —se tema en la ficha de
field— y el censo lo puntúa como deuda porque no sabe distinguirlo: el mismo
artefacto que listbox (68 %) y radio-cards (61 %) ya registraron.

Los dos factores del apilado (* 0.62 para el ancho del botón, * 0.34 para el
glifo) tampoco se acuñan: viven dentro de un calc() sobre ese mismo privado de
Field, así que un token propio no cambiaría quién manda.

SIETE ADJUDICACIONES, Y UNA SORPRESA

Los tres active-control-* no responden a :active NI forzando el pseudo por CDP:
su estado es el [data-scrubbing] que soma pone en el SCRUBBER mientras se
arrastra. Forzado el atributo sobre el nodo real, los tres alcanzan. Más
focus-control-bg (pseudo forzado por CDP), control-disabled-opacity (0.48 →
0.137) y el par de transición sin congelar (0,12s → 11,5s).

ES UNA CAPA, NO UN COMPONENTE

No tiene wrapper ni ruta propia, así que el guard lo mide dentro de
number-field — declarado ahora en `urls`, para que nadie tenga que saberlo.

⚠ Y una corrida de sonda dio 5 «nodos ausentes» que no eran nada: una recarga
que no montó la talla xl. Repetida, 0 diffs y 0 ausentes contra el mismo ANTES.
Un «missing node» tampoco se cree a la primera.

VERIFICACIÓN

Sonda antes/después 0 diffs · R-5.4 13/20 con las siete adjudicadas, exit 0 ·
component:audit sin hallazgos · censo 67 % con las ocho filas de préstamo
explicadas · docs:check 0 · el ledger sigue en sus 63 hunks previos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 4da40c63a2
commit 2ebeb9b16c

@ -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%** — 3034 de 4579 knobs pasan por un token público del componente
- **Reparto**: público 3034 · privado 317 · global 827 · literal 401 · sistema transversal 509 · excepción firmada 130 _(los dos últimos, fuera del ratio)_
- **Alcance global**: **66%** — 3034 de 4578 knobs pasan por un token público del componente
- **Reparto**: público 3034 · privado 317 · global 827 · literal 400 · sistema transversal 509 · excepción firmada 131 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 27 · **con eje `size`**: 57
## Cómo se lee
@ -139,12 +139,12 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [radio-cards](./radio-cards.md) | 61% | 72 | 43 | 0 | 23 | 4 | 2 | 40 | y |
| [password-field](./password-field.md) | 62% | 35 | 21 | 2 | 2 | 9 | 1 | 30 | y |
| [clipboard](./clipboard.md) | 63% | 8 | 5 | 2 | 1 | 0 | 0 | 6 | – |
| [spin-field](./spin-field.md) | 64% | 27 | 16 | 0 | 8 | 1 | 2 | 20 | – |
| [spinner](./spinner.md) | 64% | 25 | 16 | 5 | 0 | 4 | 0 | 18 | – |
| [pin-input](./pin-input.md) | 65% | 18 | 11 | 0 | 6 | 0 | 1 | 20 | y |
| [tag-group](./tag-group.md) | 65% | 46 | 28 | 14 | 0 | 1 | 3 | 91 | y |
| [popover](./popover.md) | 67% | 51 | 32 | 4 | 12 | 0 | 3 | 46 | y |
| [dialog](./dialog.md) | 67% | 36 | 24 | 4 | 7 | 1 | 0 | 37 | y |
| [spin-field](./spin-field.md) | 67% | 26 | 16 | 0 | 8 | 0 | 2 | 20 | – |
| [image-adjustments](./image-adjustments.md) | 67% | 18 | 10 | 0 | 4 | 1 | 3 | 12 | – |
| [virtual-list](./virtual-list.md) | 67% | 13 | 8 | 0 | 0 | 4 | 1 | 12 | – |
| [separator](./separator.md) | 67% | 6 | 4 | 0 | 0 | 2 | 0 | 3 | – |

@ -5,8 +5,8 @@
> 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**: **64%** — 16 de 25 knobs por token público
- **Knobs de apariencia**: 27 — público 16 · privado 0 · global 8 · literal 1 · sistema 2 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Medido**: 2026-08-23 · **Alcance**: **67%** — 16 de 24 knobs por token público
- **Knobs de apariencia**: 26 — público 16 · privado 0 · global 8 · literal 0 · sistema 2 · excepción 1 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 20 pública(s) — `border`, `control-radius`, `control-border-width`, `control-border`, `control-divider`, `hover-control-border`, `active-control-border`, `control-bg`, `focus-control-bg`, `active-control-bg`, `control-fg`, `hover-control-fg`, `active-control-fg`, `control-increment-glyph`, `control-decrement-glyph`, `control-increment-glyph-stacked`, `control-decrement-glyph-stacked`, `control-disabled-opacity`, `transition-duration`, `transition-ease`
- **Eje `size`**: no · **ficheros**: `spin-field.css`
@ -29,20 +29,20 @@
_Ninguno._
### 1.3 Literales (1)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `spin-field.css:180` | `[data-spin-field][data-steppers='stacked'] [data-spin-field-increment-trigger], [data-spin-field][data-steppers='stacked'] [data-spin-field-decrement-trigger]` | `line-height` | `1` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (1) — fuera del ratio
Literales que llevan su anotación `/* literal: <razón> */` 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 | `spin-field.css:180` | `[data-spin-field][data-steppers='stacked'] [data-spin-field-increment-trigger], [data-spin-field][data-steppers='stacked'] [data-spin-field-decrement-trigger]` | `line-height` | `1` |
## 2. Sistema transversal (2) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -96,6 +96,43 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**ADJUDICADO 2026-08-23 — 64 % → 67 %, y ése es su techo.** CERO claves nuevas,
un literal firmado, siete adjudicaciones medidas. Diff de computed 0 sobre 2.400
valores en 8 estados.
**El techo estaba escrito en la cabecera del propio CSS, y es doctrina.** Ocho de
sus knobs leen los privados RESUELTOS del Field (`--_field-control-height`,
`--_field-control-padding-inline`, `--_field-control-gap`) porque los steppers y
el scrubber **igualan** la fila del input a cualquier talla. La cabecera lo dice
con estas palabras: «never a parallel `--spin-field-*` chrome that would alias
the Field base». Acuñar ahí sería el antipatrón que el fichero prohíbe por su
nombre. Es préstamo con dueño —se tema en la ficha de `field`— y el censo lo
puntúa como deuda porque no sabe distinguirlo: el artefacto que `listbox` (68 %)
y `radio-cards` (61 %) ya registraron.
**Los dos factores del apilado tampoco se acuñan** (`* 0.62` para el ancho del
botón, `* 0.34` para el glifo): viven dentro de un `calc()` sobre ese mismo
privado de Field, así que un token propio no cambiaría quién manda. Y el
`line-height: 1` es identidad, firmado.
**Las siete adjudicaciones, y una sorpresa entre ellas.** Los tres
`active-control-*` no responden a `:active` **ni forzando el pseudo por CDP**:
su estado es el `[data-scrubbing]` que soma pone en el SCRUBBER mientras se
arrastra. Forzado el atributo sobre el nodo real, los tres alcanzan. Más
`focus-control-bg` (forzado `:focus-visible`, que el guard blurea a propósito),
`control-disabled-opacity` (0.48 → 0.137) y el par de transición (medido sin
congelar: 0,12s → 11,5s).
**Es una CAPA, no un componente**: no tiene wrapper ni ruta propia, así que el
guard lo mide dentro de `number-field` — declarado ahora en `urls`, para que
nadie tenga que saberlo.
⚠ **Y una corrida de sonda dio 5 «nodos ausentes» que no eran nada**: una
recarga que no montó la talla `xl`. Repetida, 0 diffs y 0 ausentes contra el
mismo ANTES. Un «missing node» tampoco se cree a la primera.
**Verificación**: sonda antes/después **0 diffs** (2.400 valores · 8 estados · 5
nodos) · R-5.4 **13/20** con las siete adjudicadas, exit 0 · censo 67 % con las
ocho filas de préstamo explicadas · captura del componente anfitrión.
<!-- veredicto:end -->

@ -77,6 +77,24 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'radius-sm': 'only the step in force paints; forced data-rounded=sm → reaches (4px → 37px)',
'radius-lg': 'only the step in force paints; forced data-rounded=lg → reaches (10px → 37px)'
},
'spin-field': {
// 2026-08-23. Measured inside number-field (this is a shared LAYER with no
// route of its own), transitions frozen except for the last pair.
'active-control-border':
"the ACTIVE state is the scrubber's [data-scrubbing], not :active; forced on the real scrubber -> reaches (oklch(0.8043 0.0918 309.69) -> rgb(1,2,3))",
'active-control-bg':
'same drag state; forced -> reaches (oklch(0.9932 0.0034 325.6) -> rgb(1,2,3))',
'active-control-fg':
'same drag state; forced -> reaches (oklch(0.5168 0.1733 305.88) -> rgb(1,2,3))',
'focus-control-bg':
'only under :focus-visible, which the guard blurs away on purpose; forced the pseudo via CDP -> reaches (oklch(0.931 0 0) -> rgb(1,2,3))',
'control-disabled-opacity':
'no disabled stepper in the demo; forced data-disabled -> reaches (0.48 -> 0.137)',
'transition-duration':
'frozen by the guard by design; unfrozen pass -> reaches (0.12s -> 11.5s)',
'transition-ease':
'same freeze; unfrozen pass -> reaches (cubic-bezier(0.4,0,0.2,1) -> steps(7))'
},
meter: {
// 2026-08-23. The demo sits on the OPTIMUM zone, so the two off-zone inks
// have no node to paint; forced on the real root, transitions frozen.

@ -199,6 +199,10 @@ const COMPONENT_OVERRIDES: Record<
// no attribute of its own: without this the guard measures ONE node (the root)
// and 27 of its 37 tokens read dead. Measured 2026-08-22.
prose: { extraNodes: '[data-prose] *' },
// A shared LAYER, not a component: no wrapper, no route of its own. It paints
// the stepper affordances of NumberField / CssField, so it is measured inside
// one of them.
'spin-field': { urls: ['/uix/components/number-field'] },
// Half of it is the CIRCULAR shape (the ring: 12 size / thickness steps plus
// its track and centre) and the demo boots `linear`, so 14 of its 28 tokens
// had no node to paint.

@ -40,6 +40,37 @@ field adds.
the control padding and clip to the rounded corners) and `gap: 0` (steppers
butt flush; the input carries the text spacing).
## Talla y tema
**20 claves públicas** en `lib/recipes/base.ts` (bloque `spin-field`): el cromo
de los steppers y del scrubber (`control-{radius,border-width,border,divider,bg,
fg}` con sus estados `hover-` / `active-` / `focus-`), los cuatro glifos
(`control-{increment,decrement}-glyph{,-stacked}`), el `border` del divisor,
`control-disabled-opacity` y el par de transición. **Sin eje `size` propio**: la
talla la pone el Field que lo contiene.
**Su techo es el 67 %, y está escrito en la cabecera del propio CSS.** Ocho
declaraciones leen los privados RESUELTOS del Field —`--_field-control-height`,
`--_field-control-padding-inline`, `--_field-control-gap`— y eso es exactamente
lo que la doctrina de esta capa manda: los steppers y el scrubber **igualan** la
fila del input a cualquier talla, y acuñar `--spin-field-*` para ello sería
«a parallel `--spin-field-*` chrome that would alias the Field base», que la
cabecera prohíbe por su nombre. Préstamo con dueño: se tema en la ficha de
`field`. El censo lo puntúa como deuda porque no puede distinguirlo — es el
artefacto que `listbox` y `radio-cards` ya registraron.
Los dos factores del layout apilado (`* 0.62` para el ancho del botón y `* 0.34`
para el glifo) tampoco se acuñan: viven dentro de un `calc()` sobre ese mismo
privado de Field, así que un token propio no cambiaría quién manda. Y el
`line-height: 1` del botón de glifo es identidad, firmado en su declaración.
**Lo que el guard da por silencioso** (`theming-sentinel-exceptions.ts`, todo
medido dentro de `number-field`, que es donde esta capa se puede ver): los tres
`active-control-*` —cuyo estado NO es `:active` sino el `[data-scrubbing]` del
scrubber—, `focus-control-bg` (forzado el pseudo por CDP),
`control-disabled-opacity` (0.48 → 0.137) y el par de transición, que el guard
congela por diseño.
## Loading
Foundation `@import` in `index.css` (there is no `spin-field.svelte` to

@ -177,7 +177,7 @@
/* Glyph scales with the field height (≈ a third), so it stays inside the
half-height cell at every size. */
font-size: calc(var(--_field-control-height) * 0.34);
line-height: 1;
line-height: 1; /* literal: identity — a glyph-only cell, the type sets no leading */
}
[data-spin-field][data-steppers='stacked'] [data-spin-field-increment-trigger] {

Loading…
Cancel
Save

Powered by TurnKey Linux.