diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 6d828966e..4216df56a 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%** — 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 | – | diff --git a/docs/audit/theming/spin-field.md b/docs/audit/theming/spin-field.md index 5c61f1d87..525d26b3f 100644 --- a/docs/audit/theming/spin-field.md +++ b/docs/audit/theming/spin-field.md @@ -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: */` 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 `⚠`. -_(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. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 76acba803..a72cf8f92 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -77,6 +77,24 @@ export const SENTINEL_EXCEPTIONS: Record> = { '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. diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index b572dcf48..b79a028a7 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -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. diff --git a/src/uix/eidos/components/spin-field/README.md b/src/uix/eidos/components/spin-field/README.md index 638fe5bca..146981c4c 100644 --- a/src/uix/eidos/components/spin-field/README.md +++ b/src/uix/eidos/components/spin-field/README.md @@ -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 diff --git a/src/uix/eidos/components/spin-field/spin-field.css b/src/uix/eidos/components/spin-field/spin-field.css index 948c235c5..f2cba97f2 100644 --- a/src/uix/eidos/components/spin-field/spin-field.css +++ b/src/uix/eidos/components/spin-field/spin-field.css @@ -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] {