uix(field): temable — 73 % → 98 %, y sus 18 rojos estaban VIVOS

8 claves nuevas (60 → 68) y 8 literales ANOTADOS. Lo que falta es el PUENTE DE
PALETA (`_palette-text`, THM-2), que no es deuda. Global 63 %.

Field es el componente más COMPUESTO del sistema: lo que otras fichas llaman
«préstamo con dueño» se cobra aquí, así que su techo pesa más que el de nadie.

Acuñados el DISPARADOR DE CONTROL —el icono que abre el calendario, el reloj o
la muestra, y revela la contraseña: `size`, `radius`, `fg` y sus dos estados—,
el indicador `optional` (gemelo del `required`, que ya tenía token) y el lateral
de la etiqueta flotante, que es lo que CORTA el borde bajo el texto. La caja del
disparador se mide en `em` a propósito, así que el token guarda la RAZÓN
(`1.75em`) y no un píxel por talla.

OCHO LITERALES PASAN A EXCEPCIÓN FIRMADA POR IDENTIDAD, no para inflar el ratio:
cuatro `inline-size: 100%` (el campo, el control, el input y el control flotante
llenan su fila), dos `line-height: 1` (un glifo suelto no tiene interlínea) y
dos `1em` del icono dentro del disparador (el glifo ES el tamaño del texto).

LOS 20 ROJOS DEL GUARD ESTABAN VIVOS, uno por uno, y tres enseñan algo:

- `disabled-opacity` vive en el CONTROL, no en la raíz: el guard leía el nodo
  equivocado.
- `control-placeholder-fg` no leía porque el input de la demo NO TIENE
  `placeholder`: sin el atributo no existe la caja `::placeholder`, y
  `getComputedStyle(nd, '::placeholder')` devuelve entonces el estilo del
  ELEMENTO. Con un placeholder puesto, alcanza. Es genérico → §13.
- `segment-height` es un token de FAMILIA que field posee y los pickers
  consumen —en las rutas de field no lo lee nadie— y además es RESUELTO
  (declarado por `[data-field][data-size]`), así que escribirlo en `:root`
  pierde POR DISEÑO: el tema mueve la coordenada. Medido en `/time-picker`:
  desde `:root` nada, desde el host del campo 28px → 1234px, y moviendo
  `--field-control-height-md` 28px → 992px.

El resto son estado (invalid, disabled, readonly, foco), variante ghost,
orientación horizontal, la franja de segmentos, el texto de error —que sólo se
RENDERIZA con el interruptor de la demo: forzar el atributo no hace que Svelte
monte el nodo— y los dos de transición, que el guard congela.

Instrumento: el disparador de control no lo monta NINGUNA demo de field, así que
el guard mide ahora `field` sobre DOS rutas (`/field` + `/date-picker`).

Gates: sonda 0 diffs en 2.280 valores × 8 estados · equivalencia del disparador
comprobada aparte en `/date-picker` (28px = 1.75em con fuente 16px · 4px =
`--radius-sm` · misma tinta) · centinela 48/68 con las 20 adjudicadas · censo
98 % · `--names` 0 · `component:audit` PASS · `rtl:check` 0 · `docs:check` 0 ·
suite eidos con el rojo conocido ajeno · `check` sin errores propios · capturas
de reposo, invalid, disabled, etiqueta flotante y disparador.

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

@ -10,8 +10,8 @@
- **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**: **63%** — 2896 de 4628 knobs pasan por un token público del componente
- **Reparto**: público 2896 · privado 338 · global 899 · literal 495 · sistema transversal 509 · excepción firmada 54 _(los dos últimos, fuera del ratio)_
- **Alcance global**: **63%** — 2905 de 4620 knobs pasan por un token público del componente
- **Reparto**: público 2905 · privado 338 · global 892 · literal 485 · sistema transversal 509 · excepción firmada 62 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 14 · **alcance 100 %**: 17 · **con eje `size`**: 58
## Cómo se lee
@ -85,14 +85,14 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma
| 10 | [color-picker](./color-picker.md) | 20 | 75% | 82 | 71 |
| 11 | [navigation-menu](./navigation-menu.md) | 19 | 44% | 38 | 26 |
| 12 | [sidebar](./sidebar.md) | 19 | 55% | 45 | 50 |
| 13 | [field](./field.md) | 18 | 73% | 72 | 60 |
| 14 | [mockup](./mockup.md) | 18 | 38% | 29 | 0 |
| 15 | [drawer](./drawer.md) | 17 | 72% | 62 | 54 |
| 16 | [onion-menu](./onion-menu.md) | 17 | 29% | 27 | 12 |
| 17 | [radio-group](./radio-group.md) | 17 | 77% | 75 | 61 |
| 18 | [popover](./popover.md) | 16 | 67% | 51 | 46 |
| 19 | [form](./form.md) | 15 | 71% | 52 | 54 |
| 20 | [tag-group](./tag-group.md) | 15 | 65% | 46 | 91 |
| 13 | [mockup](./mockup.md) | 18 | 38% | 29 | 0 |
| 14 | [drawer](./drawer.md) | 17 | 72% | 62 | 54 |
| 15 | [onion-menu](./onion-menu.md) | 17 | 29% | 27 | 12 |
| 16 | [radio-group](./radio-group.md) | 17 | 77% | 75 | 61 |
| 17 | [popover](./popover.md) | 16 | 67% | 51 | 46 |
| 18 | [form](./form.md) | 15 | 71% | 52 | 54 |
| 19 | [tag-group](./tag-group.md) | 15 | 65% | 46 | 91 |
| 20 | [file-upload](./file-upload.md) | 14 | 76% | 61 | 98 |
## Sin ningún token público propio (24)
@ -182,7 +182,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [drawer](./drawer.md) | 72% | 62 | 43 | 8 | 4 | 5 | 2 | 54 | y |
| [dialog](./dialog.md) | 73% | 33 | 24 | 4 | 4 | 1 | 0 | 37 | y |
| [virtual-grid](./virtual-grid.md) | 73% | 12 | 8 | 0 | 0 | 3 | 1 | 12 | – |
| [field](./field.md) | 73% | 72 | 49 | 1 | 7 | 10 | 5 | 60 | y |
| [color-picker](./color-picker.md) | 75% | 82 | 59 | 7 | 2 | 11 | 3 | 71 | y |
| [editable](./editable.md) | 75% | 44 | 30 | 7 | 1 | 2 | 4 | 56 | y |
| [avatar](./avatar.md) | 75% | 36 | 27 | 3 | 0 | 6 | 0 | 0 | y |
@ -237,6 +236,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [slider](./slider.md) | 97% | 33 | 31 | 0 | 0 | 1 | 1 | 39 | y |
| [toast](./toast.md) | 98% | 42 | 41 | 0 | 0 | 1 | 0 | 85 | – |
| [command](./command.md) | 98% | 52 | 49 | 0 | 0 | 1 | 2 | 57 | – |
| [field](./field.md) | 98% | 64 | 58 | 1 | 0 | 0 | 5 | 68 | y |
| [heading](./heading.md) | — | 36 | 0 | 0 | 0 | 0 | 36 | 0 | – |
| [s-text](./s-text.md) | 100% | 30 | 2 | 0 | 0 | 0 | 28 | 3 | – |
| [pagination](./pagination.md) | 100% | 26 | 24 | 0 | 0 | 0 | 2 | 34 | y |

@ -5,24 +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-22 · **Alcance**: **73%** — 49 de 67 knobs por token público
- **Knobs de apariencia**: 72 — público 49 · privado 1 · global 7 · literal 10 · sistema 5 · excepción 1 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 60 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `font-family`, `label-gap`, `label-font-weight`, `label-line-height`, `label-fg`, `invalid-label-fg`, `disabled-label-fg`, `floating-label-top`, `layout-column-gap`, `required-fg`, `control-height-xs`, `control-height-sm`, `control-height-md`, `control-height-lg`, `control-height-xl`, `segment-height`, `control-padding-inline-xs`, `control-padding-inline-sm`, `control-padding-inline-md`, `control-padding-inline-lg`, `control-padding-inline-xl`, `control-gap-xs`, `control-gap-sm`, `control-gap-md`, `control-gap-lg`, `control-gap-xl`, `control-font-size-xs`, `control-font-size-sm`, `control-font-size-md`, `control-font-size-lg`, `control-font-size-xl`, `control-line-height`, `control-font-weight`, `control-radius`, `control-border-width`, `control-border`, `hover-control-border`, `focus-control-border`, `invalid-control-border`, `control-bg`, `control-ghost-border`, `control-ghost-bg`, `control-bg-readonly`, `disabled-control-bg`, `control-fg`, `control-placeholder-fg`, `affix-fg`, `segment-active-bg`, `segment-active-text`, `message-line-height`, `helper-fg`, `error-fg`, `transition-duration`, `transition-ease`, `disabled-opacity` · 1 privada(s) forward — `_palette-text`
- **Medido**: 2026-08-22 · **Alcance**: **98%** — 58 de 59 knobs por token público
- **Knobs de apariencia**: 64 — público 58 · privado 1 · global 0 · literal 0 · sistema 5 · excepción 9 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 68 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `font-family`, `label-gap`, `label-font-weight`, `label-line-height`, `label-fg`, `invalid-label-fg`, `disabled-label-fg`, `floating-label-top`, `floating-label-padding-inline`, `layout-column-gap`, `required-fg`, `optional-fg`, `optional-font-weight`, `control-height-xs`, `control-height-sm`, `control-height-md`, `control-height-lg`, `control-height-xl`, `segment-height`, `control-padding-inline-xs`, `control-padding-inline-sm`, `control-padding-inline-md`, `control-padding-inline-lg`, `control-padding-inline-xl`, `control-gap-xs`, `control-gap-sm`, `control-gap-md`, `control-gap-lg`, `control-gap-xl`, `control-font-size-xs`, `control-font-size-sm`, `control-font-size-md`, `control-font-size-lg`, `control-font-size-xl`, `control-line-height`, `control-font-weight`, `control-radius`, `control-border-width`, `control-border`, `hover-control-border`, `focus-control-border`, `invalid-control-border`, `control-bg`, `control-ghost-border`, `control-ghost-bg`, `control-bg-readonly`, `disabled-control-bg`, `control-fg`, `control-placeholder-fg`, `affix-fg`, `control-trigger-size`, `control-trigger-radius`, `control-trigger-fg`, `hover-control-trigger-fg`, `focus-control-trigger-fg`, `segment-active-bg`, `segment-active-text`, `message-line-height`, `helper-fg`, `error-fg`, `transition-duration`, `transition-ease`, `disabled-opacity` · 1 privada(s) forward — `_palette-text`
- **Eje `size`**: sí · **ficheros**: `field-control-trigger.css`, `field-segment-state.css`, `field.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (7)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `field-control-trigger.css:34` | `[data-field-control-trigger]` | `border-radius` | `var(--radius-sm)` |
| 2 | `field-control-trigger.css:36` | `[data-field-control-trigger]` | `color` | `var(--color-content-secondary)` |
| 3 | `field-control-trigger.css:51` | `[data-field-control-trigger]:hover:not([data-disabled]):not(:disabled)` | `color` | `var(--color-content-primary)` |
| 4 | `field-control-trigger.css:57` | `[data-field-control-trigger]:focus-visible` | `color` | `var(--color-content-primary)` |
| 5 | `field.css:143` | `[data-field-optional-indicator]` | `color` | `var(--color-content-muted)` |
| 6 | `field.css:145` | `[data-field-optional-indicator]` | `font-weight` | `var(--font-weight-regular)` |
| 7 | `field.css:284` | `[data-field][data-floating-label] > [data-field-label]` | `padding-inline` | `var(--space-1)` |
_Ninguno._
### 1.2 A través de un privado (1)
@ -30,22 +22,11 @@
| ---: | --- | --- | --- | --- |
| 1 | `field.css:320` | `[data-field][data-floating-label]:focus-within > [data-field-label]` | `color` | `var(--_field-palette-text)` |
### 1.3 Literales (10)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `field-control-trigger.css:30` | `[data-field-control-trigger]` | `inline-size` | `1.75em` |
| 2 | `field-control-trigger.css:31` | `[data-field-control-trigger]` | `block-size` | `1.75em` |
| 3 | `field-control-trigger.css:45` | `[data-field-control-trigger] :is(svg, [data-icon])` | `inline-size` | `1em` |
| 4 | `field-control-trigger.css:46` | `[data-field-control-trigger] :is(svg, [data-icon])` | `block-size` | `1em` |
| 5 | `field.css:13` | `[data-field]` | `inline-size` | `100%` |
| 6 | `field.css:139` | `[data-field-required-indicator]` | `line-height` | `1` |
| 7 | `field.css:146` | `[data-field-optional-indicator]` | `line-height` | `1` |
| 8 | `field.css:159` | `[data-field-control]` | `inline-size` | `100%` |
| 9 | `field.css:208` | `[data-field-input]` | `inline-size` | `100%` |
| 10 | `field.css:299` | `[data-field][data-floating-label] > [data-field-control]` | `inline-size` | `100%` |
### 1.4 Excepciones firmadas (1) — fuera del ratio
_Ninguno._
### 1.4 Excepciones firmadas (9) — 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
@ -54,7 +35,15 @@ razón se lea, no para acuñarlas.
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `field.css:144` | `[data-field-optional-indicator]` | `font-size` | `0.85em` |
| 1 | `field-control-trigger.css:45` | `[data-field-control-trigger] :is(svg, [data-icon])` | `inline-size` | `1em` |
| 2 | `field-control-trigger.css:46` | `[data-field-control-trigger] :is(svg, [data-icon])` | `block-size` | `1em` |
| 3 | `field.css:13` | `[data-field]` | `inline-size` | `100%` |
| 4 | `field.css:139` | `[data-field-required-indicator]` | `line-height` | `1` |
| 5 | `field.css:144` | `[data-field-optional-indicator]` | `font-size` | `0.85em` |
| 6 | `field.css:146` | `[data-field-optional-indicator]` | `line-height` | `1` |
| 7 | `field.css:159` | `[data-field-control]` | `inline-size` | `100%` |
| 8 | `field.css:208` | `[data-field-input]` | `inline-size` | `100%` |
| 9 | `field.css:299` | `[data-field][data-floating-label] > [data-field-control]` | `inline-size` | `100%` |
## 2. Sistema transversal (5) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -85,7 +74,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl
- **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (9)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (3)
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
@ -95,15 +84,9 @@ no distingue lo que debería — se marca `⚠`.
| token (`--field-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `control-trigger-width` | `root` | ⚠ `1.75em` / `1em` | 2 |
| `control-trigger-height` | `root` | ⚠ `1.75em` / `1em` | 2 |
| `control-trigger-fg` | `root` | ⚠ `var(--color-content-secondary)` / `var(--color-content-primary)` | 2 |
| `control-trigger-radius` | `root` | `var(--radius-sm)` | 1 |
| `hover-control-trigger-fg` | `root` | `var(--color-content-primary)` | 1 |
| `optional-indicator-fg` | `root` | `var(--color-content-muted)` | 1 |
| `control-trigger-width` | `root` | `1em` | 1 |
| `control-trigger-height` | `root` | `1em` | 1 |
| `optional-indicator-font-size` | `root` | `0.85em` | 1 |
| `optional-indicator-font-weight` | `root` | `var(--font-weight-regular)` | 1 |
| `label-padding-inline` | `root` | `var(--space-1)` | 1 |
### 4.2 Sin nombre mecánico (7)
@ -123,6 +106,68 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-23 — 73 % → 98 %**, 8 claves nuevas (60 → 68) y 8 literales
ANOTADOS. El 2 % que falta es el PUENTE DE PALETA (`_palette-text`, THM-2), que
no es deuda. Y el guard, que arrastraba **18 rojos**, queda en verde con sus
veinte adjudicaciones escritas.
Es el componente más COMPUESTO del sistema: lo que otras fichas llaman
«préstamo con dueño» se cobra aquí. Por eso el techo importa más que en otros.
Lo acuñado:
- **El disparador de control** (`field-control-trigger.css`, el icono que abre
el calendario / el reloj / la muestra, o revela la contraseña): `size`,
`radius`, `fg` y los dos estados (`hover-` y `focus-`). Su caja se mide en
`em` a propósito —la afordancia crece con la fuente del campo—, así que el
token guarda la RAZÓN (`1.75em`), no un píxel por talla.
- **El indicador `optional`**, gemelo del `required` que ya tenía token: tinta y
peso.
- **El lateral de la etiqueta flotante** (`floating-label-padding-inline`): es
lo que CORTA el borde bajo el texto, así que un tema que mueva el padding
mueve el corte.
**Ocho literales pasan a excepción firmada, y no por inflar el ratio**: cuatro
`inline-size: 100%` (el campo, el control, el input y el control flotante llenan
su fila), dos `line-height: 1` (un glifo suelto no tiene interlínea) y dos `1em`
del glifo dentro del disparador (el icono ES el tamaño del texto). Ninguno es
una perilla: son identidades. Llevan su `/* literal: … */` con la razón, que es
la válvula de recipe-contract §3.
**Los 20 rojos del guard estaban VIVOS, uno por uno** — y tres enseñan algo:
- **`disabled-opacity` vive en el CONTROL, no en la raíz**: el guard leía el
nodo equivocado.
- **`control-placeholder-fg` no leía porque el input de la demo NO TIENE
`placeholder`**: sin atributo no hay caja `::placeholder`, y
`getComputedStyle(nd, '::placeholder')` devuelve entonces el estilo del
ELEMENTO. Con un placeholder puesto, alcanza. Es genérico: cualquier token de
placeholder leerá muerto sobre un input vacío.
- **`segment-height` es un token de FAMILIA que field posee y los pickers
consumen**: en las rutas de field no lo lee nadie. Y además es RESUELTO
(declarado por `[data-field][data-size]`), así que una escritura en `:root`
pierde POR DISEÑO — el tema mueve la coordenada. Medido en `/time-picker`:
desde `:root` nada, desde el host del campo 28px → 1234px, y moviendo
`--field-control-height-md` 28px → 992px.
El resto son estado (`invalid`, `disabled`, `readonly`, foco), variante
(`ghost`), orientación (`horizontal`), la franja de segmentos (medida en
`/date-field` con el segmento pulsado), el texto de error —que sólo se RENDERIZA
con el interruptor de la demo: forzar el atributo no hace que Svelte monte el
nodo— y los dos tokens de transición, que el guard no puede medir porque los
congela.
**Instrumento**: el disparador de control no lo monta NINGUNA demo de field
(`/field` lleva inputs planos y los segmentados llevan segmentos), así que sus
cuatro tokens leían muertos; el guard mide ahora `field` sobre DOS rutas
(`/field` + `/date-picker`).
Verificación: **diff de computed = 0** sobre 2.280 valores en 8 estados ·
equivalencia del disparador comprobada aparte en `/date-picker` (28px = 1.75em
con fuente 16px · 4px = `--radius-sm` · misma tinta) · centinela **48/68** con
las 20 adjudicadas · censo 98 % · `--names` 0 desviadas · `component:audit`
PASS · `rtl:check` 0 · `docs:check` 0 · suite eidos con el rojo conocido ajeno ·
`check` sin errores propios · capturas de reposo, invalid, disabled, etiqueta
flotante (en reposo y con foco) y disparador en hover.
<!-- veredicto:end -->

@ -1149,5 +1149,13 @@ Lo que `chart` destapó (2026-08-22):
cola por esto; aquí no se acuña un `hover-trigger-bg` porque fosilizaría la
invención (doctrina: el hover es del SISTEMA).
- **⚠ Un token de `::placeholder` lee MUERTO sobre un input sin `placeholder`**
(medido en `field`, 2026-08-23). Sin el atributo no existe la caja del
pseudo-elemento, y `getComputedStyle(nd, '::placeholder')` devuelve entonces el
estilo del ELEMENTO — que no sigue al token. Poniendo un placeholder, alcanza
(`oklch(0.61 0 0)` → `rgb(1,2,3)`). Es genérico: cualquier componente con un
token de placeholder lo sufrirá si su demo arranca con el input vacío. El guard
podría sembrar un placeholder antes de esa medida.
**Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando
estorben.

@ -455,6 +455,43 @@ de commit. Sin artefacto, el paso no se ha hecho.
## 8. Registro
- 2026-08-23 — **`field` 73 % → 98 %** (8 claves + 8 literales ANOTADOS) **y sus
18 rojos, medidos**. Global **63 %**. El 2 % que falta es el puente de paleta
(THM-2), que no es deuda.
Es el componente más COMPUESTO del sistema: lo que otras fichas llaman
«préstamo con dueño» se cobra aquí, así que su techo pesa más que el de nadie.
Acuñados el **disparador de control** —el icono que abre el calendario, el
reloj o la muestra, y revela la contraseña: `size`, `radius`, `fg` y sus dos
estados—, el indicador `optional` (gemelo del `required`, que ya tenía token)
y el **lateral de la etiqueta flotante**, que es lo que CORTA el borde bajo el
texto. La caja del disparador se mide en `em` a propósito, así que el token
guarda la RAZÓN (`1.75em`), no un píxel por talla.
**Ocho literales pasan a excepción firmada por IDENTIDAD, no por inflar el
ratio**: cuatro `inline-size: 100%`, dos `line-height: 1` de un glifo suelto y
dos `1em` del icono dentro del disparador (el glifo ES el tamaño del texto).
**Los 20 rojos del guard estaban VIVOS**, y tres enseñan algo:
`disabled-opacity` vive en el CONTROL y el guard leía la raíz;
**`control-placeholder-fg` no leía porque el input de la demo NO TIENE
`placeholder`** —sin atributo no hay caja `::placeholder` y
`getComputedStyle(nd, '::placeholder')` devuelve el estilo del ELEMENTO—; y
`segment-height` es un token de FAMILIA que field posee y los pickers
consumen, además de RESUELTO: desde `:root` no se mueve por diseño (medido en
`/time-picker`: desde el host 28px → 1234px, moviendo la coordenada
`--field-control-height-md` 28px → 992px).
**Instrumento**: el disparador de control no lo monta NINGUNA demo de field,
así que el guard mide ahora `field` sobre DOS rutas (`/field` + `/date-picker`).
Gates: sonda **0 diffs** en 2.280 valores × 8 estados · equivalencia del
disparador comprobada aparte · centinela 48/68 con las 20 adjudicadas · censo
98 % · `--names` 0 · audit PASS · rtl 0 · docs 0 · suite eidos con el rojo
conocido ajeno · `check` sin errores propios · capturas de reposo, invalid,
disabled, etiqueta flotante y disparador.
- 2026-08-23 — **`menubar` 15 % → 88 %** (30 claves, 5 → 35). Global **62 % →
63 %**. Las tres filas que quedan fuera NO son deuda de este eje.

@ -587,5 +587,30 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
},
menubar: {
'disabled-trigger-fg': 'no disabled entry in the demo bar; forced data-disabled 2026-08-23 -> reaches (oklch(0.61 0 0) -> rgb(1,2,3))'
},
field: {
// Measured one by one 2026-08-23 on /field, /date-field, /date-picker and
// /time-picker. The 20 are alive; every one needed a state, a variant, a
// route or an instrument the guard does not reach on its own.
'invalid-label-fg': 'only under [data-invalid] and the demo boots valid; forced -> reaches (oklch(0.5855 0.1743 42.74) -> rgb(1,2,3))',
'disabled-label-fg': 'only under [data-disabled]; forced -> reaches (oklch(0.7921 0 0) -> rgb(1,2,3))',
'invalid-control-border': 'only under [data-invalid]; forced -> reaches (oklch(0.8059 0.1123 59.96) -> rgb(1,2,3))',
'disabled-control-bg': 'only under [data-disabled]; forced -> reaches (oklch(0.9551 0 0) -> rgb(1,2,3))',
'control-bg-readonly': 'only under [data-readonly]; forced -> reaches (oklch(0.9551 0 0) -> rgb(1,2,3))',
'disabled-opacity': 'it lives on the CONTROL, not on the field root, so the guard read the wrong node; forced data-disabled and read on the control -> reaches (0.4 -> 0.137)',
'control-ghost-border': 'the ghost variant and the demo boots surface; forced data-variant=ghost -> reaches (transparent -> rgb(1,2,3))',
'control-ghost-bg': 'same ghost variant; forced -> reaches (transparent -> rgb(1,2,3))',
'layout-column-gap': 'only in the horizontal orientation; forced data-orientation=horizontal -> reaches (16px -> 1234px)',
'focus-control-border': 'only under :focus-within, and the guard blurs ON PURPOSE (the click-focus poisoning fix); focused from the keyboard -> reaches (the focus ring colour -> rgb(1,2,3))',
'focus-control-trigger-fg': 'only under :focus-visible on the picker trigger, which the guard blurs; focused from the keyboard on /date-picker -> reaches (oklch(0.2435 0 0) -> rgb(1,2,3))',
'floating-label-top': 'floatingLabel is a demo switch that boots OFF, and the label must also be floated (focused or filled); switch on + focus -> reaches (0px -> 1234px)',
'floating-label-padding-inline': 'same switch; turned on -> reaches (4px -> 1234px)',
'error-fg': 'the error text only RENDERS when the field is invalid, and it is the demo switch that mounts it - forcing the data-invalid attribute does not make Svelte render the node; switch on -> reaches (oklch(0.5855 0.1743 42.74) -> rgb(1,2,3))',
'control-placeholder-fg': 'the ::placeholder box exists only when the input HAS a placeholder attribute, and the demo input has none: getComputedStyle then returns the ELEMENT style and the token reads dead. With a placeholder set -> reaches (oklch(0.61 0 0) -> rgb(1,2,3))',
'segment-active-bg': 'the ACTIVE segment being edited: no segment on the field route and none focused; measured on /date-field with the segment clicked -> reaches (the 40% mix -> rgb(1,2,3))',
'segment-active-text': 'same segment; measured on /date-field -> reaches (oklch(0.5168 0.1733 305.88) -> rgb(1,2,3))',
'segment-height': 'a FAMILY token that field OWNS and the pickers CONSUME - nothing on the field routes reads it - and it is RESOLVED (declared per [data-field][data-size]), so a :root write loses BY DESIGN: a theme moves the coordinate. Measured on /time-picker: from :root nothing, from the field host 28px -> 1234px, and moving --field-control-height-md 28px -> 992px',
'transition-duration': 'the guard freezes transitions to measure and this token IS the transition; measured unfrozen -> reaches (0.12s -> 11.5s)',
'transition-ease': 'same instrument limit; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(7))'
}
};

@ -172,6 +172,11 @@ 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] *' },
// The shared control trigger (the icon affordance a picker drops inside the
// field) is mounted by NO field demo: /field has plain inputs and the
// segmented fields have segments, not triggers. It lives on the pickers, so
// its four tokens read dead on the field route alone.
field: { urls: ['/uix/components/field', '/uix/components/date-picker'] },
// The bar drops TWO different surfaces and only one carries its public knobs:
// the first entry opens a role=menu Content (chrome owned by dropdown-menu),
// while the role=dialog Panel — z, min-width, max-height, padding, radius,

@ -27,13 +27,13 @@
justify-content: center;
flex: 0 0 auto;
margin-inline-start: auto;
inline-size: 1.75em;
block-size: 1.75em;
inline-size: var(--field-control-trigger-size);
block-size: var(--field-control-trigger-size);
padding: 0;
border: none;
border-radius: var(--radius-sm);
border-radius: var(--field-control-trigger-radius);
background: transparent;
color: var(--color-content-secondary);
color: var(--field-control-trigger-fg);
cursor: pointer;
font: inherit;
transition:
@ -42,19 +42,19 @@
}
[data-field-control-trigger] :is(svg, [data-icon]) {
inline-size: 1em;
block-size: 1em;
inline-size: 1em; /* literal: the glyph IS the text size — identity, not a knob */
block-size: 1em; /* literal: the glyph IS the text size — identity, not a knob */
}
[data-field-control-trigger]:hover:not([data-disabled]):not(:disabled) {
background-image: linear-gradient(var(--state-hover), var(--state-hover));
color: var(--color-content-primary);
color: var(--field-hover-control-trigger-fg);
}
[data-field-control-trigger]:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: 0;
color: var(--color-content-primary);
color: var(--field-focus-control-trigger-fg);
}
[data-field-control-trigger][data-disabled],

@ -10,7 +10,7 @@
display: flex;
flex-direction: column;
gap: var(--_field-gap);
inline-size: 100%;
inline-size: 100%; /* literal: fills the row it is in — identity, not a knob */
min-inline-size: 0;
font-family: var(--field-font-family);
color: var(--field-control-fg);
@ -136,14 +136,14 @@
[data-field-required-indicator] {
color: var(--field-required-fg);
line-height: 1;
line-height: 1; /* literal: a lone glyph owns no leading — identity, not a knob */
}
[data-field-optional-indicator] {
color: var(--color-content-muted);
color: var(--field-optional-fg);
font-size: 0.85em; /* literal: helper text proportional to the control context */
font-weight: var(--font-weight-regular);
line-height: 1;
font-weight: var(--field-optional-font-weight);
line-height: 1; /* literal: a lone glyph owns no leading — identity, not a knob */
margin-inline-start: var(--space-1);
}
@ -156,7 +156,7 @@
flex-direction: row;
align-items: center;
gap: var(--_field-control-gap);
inline-size: 100%;
inline-size: 100%; /* literal: fills the row it is in — identity, not a knob */
min-inline-size: 0;
min-block-size: var(--_field-control-height);
padding-inline: var(--_field-control-padding-inline);
@ -205,7 +205,7 @@
[data-field-input] {
flex: 1 1 auto;
inline-size: 100%;
inline-size: 100%; /* literal: fills the row it is in — identity, not a knob */
min-inline-size: 0;
border: 0;
background: transparent;
@ -281,7 +281,7 @@
overflow: hidden;
white-space: nowrap;
pointer-events: none;
padding-inline: var(--space-1);
padding-inline: var(--field-floating-label-padding-inline);
font-size: var(--_field-control-font-size);
color: var(--field-control-placeholder-fg);
background: transparent;
@ -296,7 +296,7 @@
/* The control fills the field; helper/error stack below it. */
[data-field][data-floating-label] > [data-field-control] {
inline-size: 100%;
inline-size: 100%; /* literal: fills the row it is in — identity, not a knob */
}
/* Floated: focused OR filled → sit on the top border, shrunk. The filled test

@ -1169,8 +1169,11 @@
--field-invalid-label-fg: var(--color-risk-text);
--field-disabled-label-fg: var(--color-content-disabled);
--field-floating-label-top: 0px;
--field-floating-label-padding-inline: var(--space-1);
--field-layout-column-gap: var(--space-4);
--field-required-fg: var(--color-risk-text);
--field-optional-fg: var(--color-content-muted);
--field-optional-font-weight: var(--font-weight-regular);
--field-control-height-xs: var(--size-xs-control-height);
--field-control-height-sm: var(--size-sm-control-height);
--field-control-height-md: var(--size-md-control-height);
@ -1207,6 +1210,11 @@
--field-control-fg: var(--color-content-primary);
--field-control-placeholder-fg: var(--color-content-muted);
--field-affix-fg: var(--color-content-secondary);
--field-control-trigger-size: 1.75em;
--field-control-trigger-radius: var(--radius-sm);
--field-control-trigger-fg: var(--color-content-secondary);
--field-hover-control-trigger-fg: var(--color-content-primary);
--field-focus-control-trigger-fg: var(--color-content-primary);
--field-segment-active-bg: color-mix(in srgb, var(--color-primary-border) 40%, transparent);
--field-segment-active-text: var(--color-primary-text);
--field-message-line-height: var(--leading-ui);

@ -1145,8 +1145,15 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// theme can retarget it — negative to float fully above, or a positive
// offset to float top-inside (filled style) — without touching the recipe.
'floating-label-top': '0px',
// Side breathing room of the floated label: it sits ON the control's
// border, so the padding is what CUTS the border under the text.
'floating-label-padding-inline': 'var(--space-1)',
'layout-column-gap': 'var(--space-4)',
'required-fg': 'var(--color-risk-text)',
// The optional marker is the required marker's twin: muted, and a weight
// step below the label it trails.
'optional-fg': 'var(--color-content-muted)',
'optional-font-weight': 'var(--font-weight-regular)',
'control-height-xs': 'var(--size-xs-control-height)',
'control-height-sm': 'var(--size-sm-control-height)',
'control-height-md': 'var(--size-md-control-height)',
@ -1204,6 +1211,16 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'control-fg': 'var(--color-content-primary)',
'control-placeholder-fg': 'var(--color-content-muted)',
'affix-fg': 'var(--color-content-secondary)',
// The icon affordance inside the control (date/time/colour pickers,
// password reveal). ONE flush treatment for every field: no border, no
// shadow, the system's neutral hover tint — never the field's accent.
// Its box is sized from the field font, so the em ratio travels with the
// size axis instead of being re-declared per step.
'control-trigger-size': '1.75em',
'control-trigger-radius': 'var(--radius-sm)',
'control-trigger-fg': 'var(--color-content-secondary)',
'hover-control-trigger-fg': 'var(--color-content-primary)',
'focus-control-trigger-fg': 'var(--color-content-primary)',
// Shared field-family segment highlight (single source, theme-retunable
// from one place): the date/time/color ACTIVE segment being edited.
// `segment-active-bg` bakes its own transparency (color-mix), so a

Loading…
Cancel
Save

Powered by TurnKey Linux.