diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 4216df56a..349befabf 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,9 +10,9 @@ - **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 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 +- **Alcance global**: **66%** — 3040 de 4578 knobs pasan por un token público del componente +- **Reparto**: público 3040 · privado 317 · global 821 · 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 %**: 28 · **con eje `size`**: 57 ## Cómo se lee @@ -140,7 +140,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [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 | – | | [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 | @@ -234,6 +233,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [text](./text.md) | — | 24 | 0 | 0 | 0 | 0 | 24 | 0 | – | | [breadcrumb](./breadcrumb.md) | 100% | 23 | 20 | 0 | 0 | 0 | 3 | 28 | y | | [chat-list](./chat-list.md) | 100% | 18 | 18 | 0 | 0 | 0 | 0 | 18 | – | +| [pin-input](./pin-input.md) | 100% | 18 | 17 | 0 | 0 | 0 | 1 | 22 | y | | [splitter](./splitter.md) | 100% | 18 | 17 | 0 | 0 | 0 | 1 | 15 | – | | [empty-state](./empty-state.md) | 100% | 17 | 17 | 0 | 0 | 0 | 0 | 26 | y | | [link-preview](./link-preview.md) | 100% | 17 | 14 | 0 | 0 | 0 | 3 | 35 | – | diff --git a/docs/audit/theming/pin-input.md b/docs/audit/theming/pin-input.md index 66c2163ac..c6ef253d2 100644 --- a/docs/audit/theming/pin-input.md +++ b/docs/audit/theming/pin-input.md @@ -5,23 +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-23 · **Alcance**: **65%** — 11 de 17 knobs por token público -- **Knobs de apariencia**: 18 — público 11 · privado 0 · global 6 · literal 0 · sistema 1 · excepción 1 _(los dos últimos, fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 20 pública(s) — `gap`, `disabled-opacity`, `cell-size-sm`, `cell-size-md`, `cell-size-lg`, `cell-size-xl`, `cell-font-size-sm`, `cell-font-size-md`, `cell-font-size-lg`, `cell-font-size-xl`, `cell-font-weight`, `cell-radius`, `cell-border-width`, `cell-border-fg`, `cell-bg`, `cell-filled-bg`, `cell-filled-border`, `cell-active-border`, `cell-invalid-border`, `cell-readonly-bg` +- **Medido**: 2026-08-23 · **Alcance**: **100%** — 17 de 17 knobs por token público +- **Knobs de apariencia**: 18 — público 17 · privado 0 · global 0 · literal 0 · sistema 1 · excepción 1 _(los dos últimos, fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 22 pública(s) — `gap`, `disabled-opacity`, `cell-size-sm`, `cell-size-md`, `cell-size-lg`, `cell-size-xl`, `cell-font-size-sm`, `cell-font-size-md`, `cell-font-size-lg`, `cell-font-size-xl`, `font-family`, `cell-fg`, `cell-font-weight`, `cell-radius`, `cell-border-width`, `cell-border-fg`, `cell-bg`, `cell-filled-bg`, `cell-filled-border`, `cell-active-border`, `cell-invalid-border`, `cell-readonly-bg` - **Eje `size`**: sí · **ficheros**: `pin-input.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (6) +### 1.1 Directo a primitivo global (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `pin-input.css:23` | `[data-pin-input]` | `font-family` | `var(--style-label-font-family, var(--style-label-font-family))` | -| 2 | `pin-input.css:41` | `[data-pin-input-cell]` | `inline-size` | `var(--_pin-cell-size)` | -| 3 | `pin-input.css:42` | `[data-pin-input-cell]` | `block-size` | `var(--_pin-cell-size)` | -| 4 | `pin-input.css:45` | `[data-pin-input-cell]` | `border-radius` | `var(--_pin-cell-radius)` | -| 5 | `pin-input.css:47` | `[data-pin-input-cell]` | `color` | `var(--color-content-primary)` | -| 6 | `pin-input.css:48` | `[data-pin-input-cell]` | `font-size` | `var(--_pin-cell-font-size)` | +_Ninguno._ ### 1.2 A través de un privado (0) @@ -51,13 +44,17 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). ## 3. Privados de la receta — ¿de dónde sale su valor? -_La receta no declara privados propios en su CSS._ +| privado | declaraciones | valor(es) | origen | ¿deriva de un público? | +| --- | ---: | --- | --- | :-: | +| `--_pin-input-cell-size` | 4 | `var(--pin-input-cell-size-md)`, `var(--pin-input-cell-size-sm)`, `var(--pin-input-cell-size-lg)`, `var(--pin-input-cell-size-xl)` | public | **sí** | +| `--_pin-input-cell-font-size` | 4 | `var(--pin-input-cell-font-size-md)`, `var(--pin-input-cell-font-size-sm)`, `var(--pin-input-cell-font-size-lg)`, `var(--pin-input-cell-font-size-xl)` | public | **sí** | +| `--_pin-input-cell-radius` | 1 | `var(--pin-input-cell-radius)` | public | **sí** | ## 4. Propuesta de corrección - **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` (6) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (0) 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 @@ -67,12 +64,6 @@ no distingue lo que debería — se marca `⚠`. | token (`--pin-input-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `font-family` | `root` | `var(--style-label-font-family, var(--style-label-font-family))` | 1 | -| `cell-width` | `root` | `var(--_pin-cell-size)` | 1 | -| `cell-height` | `root` | `var(--_pin-cell-size)` | 1 | -| `cell-radius` _(ya existe)_ | `root` | `var(--_pin-cell-radius)` | 1 | -| `cell-fg` | `root` | `var(--color-content-primary)` | 1 | -| `cell-font-size` | `root` | `var(--_pin-cell-font-size)` | 1 | ### 4.2 Sin nombre mecánico (1) @@ -90,6 +81,41 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**EJECUTADO 2026-08-23 — 65 % → 100 %.** Dos claves nuevas (20 → 22), cuatro +privados renombrados, **veintidós respaldos retirados** y seis adjudicaciones +medidas. Diff de computed **0 sobre 1.856 valores en 8 estados**. + +**Su 35 % de deuda eran tres defectos, ninguno de diseño.** + +1. **Un prefijo ABREVIADO.** Los privados se llamaban `--_pin-cell-*` dentro de + un componente llamado `pin-input`: el censo no podía reconocerlos como suyos, + así que los cuatro knobs que los leen (tamaño, radio y tipo de la celda) + contaban como deuda aunque DERIVAN de públicos. Renombrados a + `--_pin-input-cell-*` — el cuarto caso de esta clase tras `color-picker`, + `metrics` y `time-picker`. +2. **Un respaldo a SÍ MISMO**: `font-family` leía + `var(--style-label-font-family, var(--style-label-font-family))`, que es un + no-op con forma de defecto. Ahora es la costura: `--pin-input-font-family`. +3. **La tinta del dígito** iba a `--color-content-primary` a pelo → `cell-fg`. + +**Y los veintidós respaldos escritos a mano.** Cada `var(--pin-input-x, )` +duplicaba lo que el contrato ya declara en `:root`, así que eran código muerto — +retirarlos dio **0 diffs**, que es la prueba. Pero uno **mentía**: +`disabled-opacity` respaldaba `0.55` cuando `--opacity-disabled` vale **0.4**. +Es la ley de los `, 1.4` contra un `--font-line-height-sm` de 1.45 que este mes +salió en dos componentes, aquí con más deriva. **Un respaldo no es una red de +seguridad: es una segunda fuente que nadie actualiza.** + +**Las seis adjudicaciones, y dónde vive cada estado.** La demo muestra un pin +vacío, editable y válido, así que los seis tokens de estado no tenían nodo. +Forzados sobre los nodos reales, los seis alcanzan — con un detalle que costó una +pasada: `filled` y `active` son atributos de la CELDA, pero `invalid` y +`readonly` se estampan en el CONTENEDOR y cascadean a las celdas (soma los +OR-fusiona con el Field que las envuelve). Forzarlos en la celda no hace nada. + +**Verificación**: sonda antes/después **0 diffs** (1.856 valores · 8 estados · 8 +nodos) · R-5.4 **16/22** con las seis adjudicadas, exit 0 · `component:audit` +PASS · censo **100 %** · `eidos-lint` 8 morfo-backed / **0 invalid, 0 +class-hooks** · capturas 2× antes y después. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index a72cf8f92..e78df458f 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)' }, + 'pin-input': { + // 2026-08-23. Six STATE tokens: the demo shows an empty, editable, valid + // pin. Forced on the real nodes — note WHERE: `filled` and `active` are + // per-CELL attributes, but `invalid` and `readonly` are stamped on the + // CONTAINER and cascade down (forcing them on the cell does nothing). + 'cell-filled-bg': + 'no filled cell in the demo; forced data-filled -> reaches (oklch(0.9588 0.0249 311.71) -> rgb(1,2,3))', + 'cell-filled-border': + 'same state; forced -> reaches (oklch(0.8043 0.0918 309.69) -> rgb(1,2,3))', + 'cell-active-border': + 'the cell under the caret; forced data-active -> reaches (oklch(0.5556 0.1829 305.86) -> rgb(1,2,3))', + 'cell-invalid-border': + 'stamped on the CONTAINER (soma OR-merges it with the enclosing Field); forced there -> reaches (oklch(0.6256 0.1933 23.03) -> rgb(1,2,3))', + 'cell-readonly-bg': + 'container attribute too; forced there -> reaches (oklch(0.9551 0 0) -> rgb(1,2,3))', + 'disabled-opacity': + 'no disabled pin in the demo; forced data-disabled -> reaches (0.4 -> 0.137)' + }, '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. diff --git a/src/uix/eidos/components/pin-input/README.md b/src/uix/eidos/components/pin-input/README.md index f360c1f1c..9a7e576ab 100644 --- a/src/uix/eidos/components/pin-input/README.md +++ b/src/uix/eidos/components/pin-input/README.md @@ -69,6 +69,39 @@ The component IS interactive from the user's perspective. "Passive" here is a contract-layer classification, not a UX one — same logic as `` (delegates events to the underlying primitive). +## Talla y tema + +**22 claves públicas** en `lib/recipes/base.ts`: la caja (`gap`, +**`font-family`**, `disabled-opacity`), la celda por talla +(`cell-size-{sm…xl}`, `cell-font-size-{sm…xl}`) y su cromo +(`cell-radius`, `cell-border-width`, `cell-border-fg`, `cell-bg`, +**`cell-fg`**, `cell-font-weight`) más los cuatro estados +(`cell-filled-{bg,border}`, `cell-active-border`, `cell-invalid-border`, +`cell-readonly-bg`). La pestaña **Tokens** de su demo las resuelve en vivo. + +Lo que entró el **2026-08-23** (65 % → 100 %): + +- **`font-family`** — la costura. La receta leía + `var(--style-label-font-family, var(--style-label-font-family))`: un respaldo + A SÍ MISMO, que es un no-op con forma de defecto. +- **`cell-fg`** — la tinta del dígito, que iba a `--color-content-primary` a pelo. +- **Los privados dejan de usar un prefijo ABREVIADO**: eran `--_pin-cell-*` + dentro de un componente llamado `pin-input`, así que el censo no podía + reconocerlos como suyos y contaba sus cuatro knobs como deuda. Ahora son + `--_pin-input-cell-*` (mismo caso que `color-picker`, `metrics` y + `time-picker`). +- **Fuera los veintidós respaldos escritos a mano.** Cada `var(--pin-input-x, …)` + duplicaba el valor que el contrato ya declara en `:root`, así que eran código + muerto — y **uno mentía**: `disabled-opacity` respaldaba `0.55` cuando + `--opacity-disabled` vale `0.4`. Es la ley de los `, 1.4` del mismo mes, con + más deriva. Retirados: **0 diffs**, la prueba de que el token siempre llegaba. + +**Lo que el guard da por silencioso**: los seis tokens de ESTADO, porque la demo +muestra un pin vacío, editable y válido. Todos medidos forzando el atributo +sobre el nodo real — y ojo a DÓNDE: `filled` y `active` son de la CELDA, pero +`invalid` y `readonly` se estampan en el CONTENEDOR y cascadean (forzarlos en la +celda no hace nada). + ## Baseline WAI-ARIA Textbox pattern: diff --git a/src/uix/eidos/components/pin-input/pin-input.css b/src/uix/eidos/components/pin-input/pin-input.css index 07cdaf7e9..09a5ed1ca 100644 --- a/src/uix/eidos/components/pin-input/pin-input.css +++ b/src/uix/eidos/components/pin-input/pin-input.css @@ -19,34 +19,34 @@ [data-pin-input] { display: inline-flex; align-items: center; - gap: var(--pin-input-gap, var(--space-2)); - font-family: var(--style-label-font-family, var(--style-label-font-family)); + gap: var(--pin-input-gap); + font-family: var(--pin-input-font-family); } [data-pin-input][data-disabled] { - opacity: var(--pin-input-disabled-opacity, 0.55); + opacity: var(--pin-input-disabled-opacity); pointer-events: none; } /* ── Cell ─────────────────────────────────────────────────────────── */ [data-pin-input-cell] { - --_pin-cell-size: var(--pin-input-cell-size-md, 2.5rem); - --_pin-cell-font-size: var(--pin-input-cell-font-size-md, var(--font-size-lg)); - --_pin-cell-radius: var(--pin-input-cell-radius, var(--radius-md)); + --_pin-input-cell-size: var(--pin-input-cell-size-md); + --_pin-input-cell-font-size: var(--pin-input-cell-font-size-md); + --_pin-input-cell-radius: var(--pin-input-cell-radius); display: inline-flex; align-items: center; justify-content: center; - inline-size: var(--_pin-cell-size); - block-size: var(--_pin-cell-size); - border: var(--pin-input-cell-border-width, 1px) solid - var(--pin-input-cell-border-fg, var(--color-border-default)); - border-radius: var(--_pin-cell-radius); - background: var(--pin-input-cell-bg, var(--color-surface-default)); - color: var(--color-content-primary); - font-size: var(--_pin-cell-font-size); - font-weight: var(--pin-input-cell-font-weight, 500); + inline-size: var(--_pin-input-cell-size); + block-size: var(--_pin-input-cell-size); + border: var(--pin-input-cell-border-width) solid + var(--pin-input-cell-border-fg); + border-radius: var(--_pin-input-cell-radius); + background: var(--pin-input-cell-bg); + color: var(--pin-input-cell-fg); + font-size: var(--_pin-input-cell-font-size); + font-weight: var(--pin-input-cell-font-weight); font-variant-numeric: tabular-nums; line-height: 1; /* literal: cells are single-glyph, line-height collapse */ user-select: none; @@ -57,48 +57,48 @@ } [data-pin-input-cell][data-filled] { - background: var(--pin-input-cell-filled-bg, var(--color-primary-element)); - border-color: var(--pin-input-cell-filled-border, var(--color-primary-border)); + background: var(--pin-input-cell-filled-bg); + border-color: var(--pin-input-cell-filled-border); } [data-pin-input-cell][data-active] { - border-color: var(--pin-input-cell-active-border, var(--color-primary-solid)); + border-color: var(--pin-input-cell-active-border); /* Focus ring as a flush outline (no offset → no gap) — survives forced-colors. */ outline: var(--focus-ring-width) solid var(--focus-ring-color); outline-offset: 0; } [data-pin-input-cell][data-disabled] { - opacity: var(--pin-input-disabled-opacity, 0.55); + opacity: var(--pin-input-disabled-opacity); } /* Invalid (Field-aware). Soma OR-merges `invalid` with the enclosing * Field's state; both surface as `data-invalid` on the container which * we cascade to cells. */ [data-pin-input][data-invalid] [data-pin-input-cell] { - border-color: var(--pin-input-cell-invalid-border, var(--color-threat-solid)); + border-color: var(--pin-input-cell-invalid-border); } /* Read-only — cells keep their value visible but read as non-editable * (muted surface, no text cursor), mirroring the date-field segment. */ [data-pin-input][data-readonly] [data-pin-input-cell] { - background: var(--pin-input-cell-readonly-bg, var(--color-surface-muted)); + background: var(--pin-input-cell-readonly-bg); cursor: default; } /* ── Size cascade ─────────────────────────────────────────────────── */ [data-pin-input][data-size='sm'] [data-pin-input-cell] { - --_pin-cell-size: var(--pin-input-cell-size-sm, 2rem); - --_pin-cell-font-size: var(--pin-input-cell-font-size-sm, var(--font-size-md)); + --_pin-input-cell-size: var(--pin-input-cell-size-sm); + --_pin-input-cell-font-size: var(--pin-input-cell-font-size-sm); } [data-pin-input][data-size='lg'] [data-pin-input-cell] { - --_pin-cell-size: var(--pin-input-cell-size-lg, 3rem); - --_pin-cell-font-size: var(--pin-input-cell-font-size-lg, var(--font-size-xl)); + --_pin-input-cell-size: var(--pin-input-cell-size-lg); + --_pin-input-cell-font-size: var(--pin-input-cell-font-size-lg); } [data-pin-input][data-size='xl'] [data-pin-input-cell] { - --_pin-cell-size: var(--pin-input-cell-size-xl, 3.5rem); - --_pin-cell-font-size: var(--pin-input-cell-font-size-xl, var(--font-size-xxl)); + --_pin-input-cell-size: var(--pin-input-cell-size-xl); + --_pin-input-cell-font-size: var(--pin-input-cell-font-size-xl); } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 430cb2b87..a2915cb1b 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -4269,6 +4269,8 @@ --pin-input-cell-font-size-md: var(--size-lg-font-size); --pin-input-cell-font-size-lg: var(--size-xl-font-size); --pin-input-cell-font-size-xl: var(--size-xxl-font-size); + --pin-input-font-family: var(--style-label-font-family); + --pin-input-cell-fg: var(--color-content-primary); --pin-input-cell-font-weight: 500; --pin-input-cell-radius: var(--radius-md); --pin-input-cell-border-width: var(--border-width); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index c0803fdeb..115ff5f22 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -6929,6 +6929,13 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'cell-font-size-md': 'var(--size-lg-font-size)', 'cell-font-size-lg': 'var(--size-xl-font-size)', 'cell-font-size-xl': 'var(--size-xxl-font-size)', + // The face is the named typographic layer's; the KNOB is this component's + // (the seam), so a theme can give the code cells another face without + // moving `label` across the app. It used to read + // `var(--style-label-font-family, var(--style-label-font-family))` — a + // fallback to ITSELF, which is a no-op wearing the shape of a default. + 'font-family': 'var(--style-label-font-family)', + 'cell-fg': 'var(--color-content-primary)', 'cell-font-weight': '500', 'cell-radius': 'var(--radius-md)', 'cell-border-width': 'var(--border-width)', diff --git a/web/routes/uix/components/pin-input/+page.svelte b/web/routes/uix/components/pin-input/+page.svelte index f31d18e2f..076e216e2 100644 --- a/web/routes/uix/components/pin-input/+page.svelte +++ b/web/routes/uix/components/pin-input/+page.svelte @@ -7,8 +7,9 @@ import { Field } from '$uix/eidos/components/field'; import { compileMorfo } from '$uix/morfo'; import { pinInputMorfo } from '@/uix/morfo/components/pin-input'; + import TokensPanel from '../../lib/TokensPanel.svelte'; - type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y'; + type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y'; let tab = $state('live'); const sizes: PinInputSize[] = ['sm', 'md', 'lg', 'xl']; @@ -300,6 +301,9 @@ {events.length} + @@ -737,6 +741,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'recipe'}

Eidos recipe