uix(search-field): temable — 71 % → 100 %, y sus 10 tokens sin medir NO mienten

Cero claves nuevas. Su 29 % de deuda eran CUATRO literales que son identidad, y
ahora van firmados. Diff de computed 0 sobre 1.216 valores en 8 estados y 5
nodos; R-5.4 de 7/17 a 17/17 SIN una sola adjudicación, dos corridas de acuerdo;
capturas 2× antes/después idénticas byte a byte.

LOS CUATRO LITERALES SON IDENTIDAD, NO KNOBS

El line-height: 1 del icono —un glifo solo no tiene interlineado, la misma
anotación que su hermana field.css—, el 100 % doble del ::before —el anillo ES la
caja del indicador, que ya mide por --_search-field-loading-size— y el
border-radius: 50 %. Ese último la §4 pedía acuñarlo como
loading-indicator-radius: sería un knob que SÓLO puede romper el componente. El
arco es un BORDE que gira; con cualquier otro valor la ruleta deja de leerse como
una. Es la clase 4 de F2-B, «el 50 % de un círculo», al pie de la letra.

DIEZ DE SUS DIECISIETE TOKENS NO SE HABÍAN MEDIDO NUNCA

El centinela leía 7/17 porque la demo no monta dos estados: el indicador de carga
sólo existe con loading (ocho tokens) y la X sólo se desvanece con el campo
VACÍO, y el escenario arranca con valor (clear-hidden-opacity). Los dos
interruptores entran como prepareWith, y EL ORDEN IMPORTA: el clic en la X tiene
que ir PRIMERO, porque la propia regla que enciende le quita después el
pointer-events al botón que la disparó. La sonda necesita el de loading por lo
mismo: 4 → 5 nodos, sin perder ninguno.

LOS DOS PRIVADOS NO SON DEUDA

--_search-field-icon-size y --_search-field-loading-size son el conmutador por
talla y sus cinco fuentes son, cada una, una clave pública. Se quedan: 58 de las
162 recetas resuelven así su eje size —incluidas las cerradas al 100 % este mes,
empty-state y banner—, y convertirlas al ámbito size: del TSC cambiaría la
especificidad de la emisión sin ganar un punto de alcance.

⚠ UN DEFAULT MEDIDO, ANOTADO, NO CAMBIADO

Las dos escalas del mismo renglón divergen en los dos últimos peldaños. Medido en
Chrome con transiciones y animación congeladas, forzando data-size sobre el nodo
real, icono → indicador: xs 14→14, sm 16→16, md 18→18, lg 20→18, xl 32→20. El
contrato lo declara de frente (loading-indicator-size-lg apunta a
--size-md-icon-size y -xl a --size-lg-icon-size), así que en xl la ruleta es DOCE
píxeles menor que la lupa que tiene al lado. No es deuda de alcance —las diez
claves alcanzan—, es diseño que huele a copia a medias. Mueve píxel: D-TH.5 lo
saca de este eje. Escrito en el veredicto §5 y en el README.

CIFRAS Y GUARDS

Censo 71 % → 100 % (15 knobs → 11: público 10, literal 4 → 0, excepción firmada
0 → 4; sistema 1, fuera del ratio). Contrato 17 claves, intacto — no hay cambio
en base.ts ni en generated/. component-audit PASS · eidos-lint invalid 0 (9
morfo-backed, 4 eidos-only) · vitest src/uix/eidos 35/36 (único rojo
skin-media-player, preexistente) · rtl:check 0 · docs:check 0 · npm run check sin
errores en ficheros de este componente. README con su sección «Talla y tema» y
pestaña Tokens en la demo (17 claves, verificada con clic de Playwright tras
networkidle).

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

@ -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**: **71%** — 10 de 14 knobs por token público
- **Knobs de apariencia**: 15 — público 10 · privado 0 · global 0 · literal 4 · sistema 1 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Medido**: 2026-08-23 · **Alcance**: **100%** — 10 de 10 knobs por token público
- **Knobs de apariencia**: 11 — público 10 · privado 0 · global 0 · literal 0 · sistema 1 · excepción 4 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 17 pública(s) — `clear-hidden-opacity`, `icon-size-xs`, `icon-size-sm`, `icon-size-md`, `icon-size-lg`, `icon-size-xl`, `icon-fg`, `icon-margin-inline-end`, `loading-indicator-size-xs`, `loading-indicator-size-sm`, `loading-indicator-size-md`, `loading-indicator-size-lg`, `loading-indicator-size-xl`, `loading-indicator-fg`, `loading-indicator-track`, `loading-indicator-thickness`, `loading-indicator-duration`
- **Eje `size`**: sí · **ficheros**: `search-field.css`
@ -20,23 +20,23 @@ _Ninguno._
_Ninguno._
### 1.3 Literales (4)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `search-field.css:68` | `[data-search-field-icon]` | `line-height` | `1` |
| 2 | `search-field.css:86` | `[data-search-field-loading-indicator]::before` | `inline-size` | `100%` |
| 3 | `search-field.css:87` | `[data-search-field-loading-indicator]::before` | `block-size` | `100%` |
| 4 | `search-field.css:88` | `[data-search-field-loading-indicator]::before` | `border-radius` | `50%` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (4) — 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 | `search-field.css:68` | `[data-search-field-icon]` | `line-height` | `1` |
| 2 | `search-field.css:86` | `[data-search-field-loading-indicator]::before` | `inline-size` | `100%` |
| 3 | `search-field.css:87` | `[data-search-field-loading-indicator]::before` | `block-size` | `100%` |
| 4 | `search-field.css:88` | `[data-search-field-loading-indicator]::before` | `border-radius` | `50%` |
## 2. Sistema transversal (1) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -85,6 +85,53 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**Ejecutado 2026-08-23 — 71 % → 100 %, y CERO claves nuevas.** Diff de computed 0
sobre 1.216 valores en 8 estados (5 nodos); R-5.4 de **7/17 a 17/17 sin una sola
adjudicación**, dos corridas del mismo código de acuerdo; capturas 2×
antes/después **idénticas byte a byte** (`a7a83949…`).
**Su deuda eran CUATRO literales que son identidad**, y ahora van firmados con
`/* literal: … */` (válvula de recipe-contract §3): el `line-height: 1` del icono
—un glifo solo no tiene interlineado, la misma anotación que su hermana
`field.css`—, el `100%` doble del `::before` —el anillo ES la caja del indicador,
que ya mide por `--_search-field-loading-size`— y el `border-radius: 50%`. Este
último la §4 pedía acuñarlo como `loading-indicator-radius`: **sería un knob que
sólo puede romper el componente**. El arco es un BORDE que gira; con cualquier
otro valor la ruleta deja de leerse como una. Es la clase 4 de F2-B, «el 50 % de
un círculo», al pie de la letra.
**Diez de sus diecisiete tokens no se habían medido nunca, y ninguno miente.**
El centinela leía 7/17 porque la demo no monta dos estados: el indicador de carga
sólo existe con `loading` (ocho tokens) y la X sólo se desvanece con el campo
VACÍO, y el escenario arranca con valor (`clear-hidden-opacity`). Los dos
interruptores viven ya en el guard (`prepareWith`), y el orden importa: el clic
en la X tiene que ir PRIMERO, porque la propia regla que enciende le quita
después el `pointer-events` al botón que la disparó. La sonda necesita el
interruptor de `loading` por lo mismo: 4 → **5 nodos**, y sin perder ninguno.
**Los dos privados NO son deuda**: `--_search-field-icon-size` y
`--_search-field-loading-size` son el conmutador por talla y sus cinco fuentes
son, cada una, una clave pública. Se dejan como están: 58 de las 162 recetas
resuelven así su eje `size` —incluidas las cerradas al 100 % este mismo mes
(`empty-state`, `banner`)—, y convertirlas al ámbito `size:` del TSC cambiaría la
especificidad de la emisión sin ganar un punto de alcance.
**Casi nada de su superficie es suyo, y está bien**: el cromo de la fila (borde,
radio, fondo, altura, padding, hueco, tipografía del valor, foco, inválido,
deshabilitado) es de `field.css` por `[data-field-control]`, y el de la X es de
`button.css`. La receta lo dice en su cabecera y no realiasa NADA. Fuera del
ratio queda además `--opacity-muted` del indicador bajo
`prefers-reduced-motion`, que es capa de sistema.
⚠ **Un default medido, ANOTADO, no cambiado**: las dos escalas del mismo renglón
divergen en los dos últimos peldaños. Medido en Chrome con transiciones y
animación congeladas, forzando `data-size` sobre el nodo real —icono →
indicador—: xs 14→14, sm 16→16, md 18→18, **lg 20→18**, **xl 32→20**. El contrato
lo declara así de frente: `loading-indicator-size-lg` apunta a
`--size-md-icon-size` y `-xl` a `--size-lg-icon-size`, mientras la escala del
icono sube peldaño a peldaño. En `xl` la ruleta es DOCE píxeles menor que la lupa
que tiene al lado. No es deuda de alcance —las dos escalas son públicas y las
diez claves alcanzan—, es una decisión de diseño que huele a copia que se quedó a
medias. Cambiarla mueve píxel: D-TH.5 la saca de este eje.
<!-- veredicto:end -->

@ -138,7 +138,12 @@ const DEMO_VARIANTS: Record<string, string[]> = {
// It boots EMPTY (a FileUpload dropzone): the preview, the canvas, the
// toolbar and the two icon buttons — every part the recipe paints — only
// exist in the `ready` state. Without the sample chip the probe saw 1 node.
'image-picker': ['[data-uix-chip]:text-is("Load sample image")']
'image-picker': ['[data-uix-chip]:text-is("Load sample image")'],
// Its loading indicator — the part that carries EIGHT of its seventeen
// tokens — renders only while `loading`, and the demo boots it off: 4 nodes.
'search-field': [
'[data-uix-control]:has([data-uix-control-label]:text-is("loading")) input[type=checkbox]'
]
};
async function main() {

@ -417,6 +417,18 @@ const COMPONENT_OVERRIDES: Record<
'image-picker': {
prepareWith: ['[data-uix-chip]:text-is("Load sample image")'],
openMarker: '[data-image-picker-preview]'
},
// Two states the default stage does not show, and NINE of its seventeen
// tokens live in them: the loading indicator renders only while `loading`
// (eight tokens), and the clear affordance only fades while the field is
// EMPTY — and the demo boots with a value. Clearing it is what empties it,
// and the click has to come FIRST: the same rule then drops
// `pointer-events` on the very button that fired it.
'search-field': {
prepareWith: [
'[data-search-field-clear-trigger]',
'[data-uix-control]:has([data-uix-control-label]:text-is("loading")) input[type=checkbox]'
]
}
};

@ -38,6 +38,42 @@ Fuentes externas consultadas:
- Eidos no fuerza iconos. El consumidor puede renderizar una lupa o una X en
`ClearTrigger`; la receta define marco visual, `size`, `variant` y `color`.
## Talla y tema
**17 claves públicas** en `lib/recipes/base.ts`, y sólo las de los AFIJOS de
búsqueda: el cromo de la fila —borde, radio, fondo, altura, padding, hueco y la
tipografía del valor— es de `field.css` (`[data-field-control]`), y el de la X es
de `button.css`; esta receta no los realiasa. Lo suyo es el icono
(`icon-size-{xs…xl}`, `icon-fg`, `icon-margin-inline-end`), el indicador de carga
(`loading-indicator-size-{xs…xl}`, `-fg`, `-track`, `-thickness`, `-duration`) y
la desaparición de la X cuando no hay nada que borrar (`clear-hidden-opacity`).
La pestaña **Tokens** de su demo las resuelve en vivo.
Lo que entró el **2026-08-23** (71 % → 100 %): **cero claves nuevas**. Su 29 % de
deuda eran CUATRO literales que son identidad, y ahora van firmados con
`/* literal: … */` —la válvula de recipe-contract §3—: el `line-height: 1` del
icono (un solo glifo no tiene interlineado), el `100%` doble del `::before` (el
anillo ES la caja del indicador, que ya mide por `--_search-field-loading-size`)
y el `border-radius: 50%`, que es la definición del spinner: el arco es un borde
que gira, y con cualquier otro valor deja de leerse como uno. La §4 pedía acuñar
ese 50 % como `loading-indicator-radius`; sería un knob que sólo puede romperlo.
**Los dos privados se quedan**, y no son deuda: `--_search-field-icon-size` y
`--_search-field-loading-size` son el conmutador por talla, y sus cinco fuentes
son las cinco claves públicas de cada escala — el censo los cuenta alcanzables
por eso.
**Fuera del ratio por doctrina**: `--opacity-muted` del indicador en
`prefers-reduced-motion` es de la capa de sistema.
⚠ **Un default medido, ANOTADO, no cambiado**: las dos escalas del mismo renglón
divergen en los dos últimos peldaños. Medido en Chrome con las transiciones
congeladas, icono → indicador: xs 14→14, sm 16→16, md 18→18, **lg 20→18**, **xl
32→20**. El contrato lo dice a las claras (`loading-indicator-size-lg` apunta a
`--size-md-icon-size` y `-xl` a `--size-lg-icon-size`), así que en `xl` la ruleta
es doce píxeles menor que la lupa que tiene al lado. Cambiarlo mueve píxel:
D-TH.5 lo saca de este eje.
## Gaps
| Gap | Disposición | Detalle |

@ -65,7 +65,7 @@
margin-inline-end: var(--search-field-icon-margin-inline-end);
color: var(--search-field-icon-fg);
font-size: var(--_search-field-icon-size);
line-height: 1;
line-height: 1; /* literal: a lone glyph owns no leading — identity, not a knob */
pointer-events: none;
}
@ -83,9 +83,9 @@
[data-search-field-loading-indicator]::before {
content: '';
display: block;
inline-size: 100%;
block-size: 100%;
border-radius: 50%;
inline-size: 100%; /* literal: identity — the ring IS the indicator's box, already sized by --_search-field-loading-size */
block-size: 100%; /* literal: identity — the ring IS the indicator's box, already sized by --_search-field-loading-size */
border-radius: 50%; /* literal: identity — a spinner IS a circle; the arc is a rotating border and any other value stops reading as one */
border: var(--search-field-loading-indicator-thickness) solid var(--search-field-loading-indicator-track);
border-top-color: currentColor;
animation: search-field-spin var(--search-field-loading-indicator-duration) linear infinite;

@ -11,8 +11,9 @@
import { compileMorfo } from '$uix/morfo';
import { searchFieldMorfo } from '@/uix/morfo/components/search-field';
import { getActiveUix } from '$active-uix';
import TokensPanel from '../../lib/TokensPanel.svelte';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
const uix = getActiveUix();
@ -418,6 +419,9 @@
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}
>Recipe</button
>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>17</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -688,6 +692,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="search-field" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.