diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index ecabf449a..8ff697dc6 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**: **65%** — 3014 de 4602 knobs pasan por un token público del componente -- **Reparto**: público 3014 · privado 318 · global 845 · literal 425 · sistema transversal 509 · excepción firmada 110 _(los dos últimos, fuera del ratio)_ +- **Alcance global**: **66%** — 3019 de 4601 knobs pasan por un token público del componente +- **Reparto**: público 3019 · privado 318 · global 844 · literal 420 · sistema transversal 509 · excepción firmada 111 _(los dos últimos, fuera del ratio)_ - **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 26 · **con eje `size`**: 57 ## Cómo se lee @@ -134,7 +134,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [toggle-group](./toggle-group.md) | 50% | 2 | 1 | 0 | 1 | 0 | 0 | 1 | – | | [sidebar](./sidebar.md) | 55% | 45 | 23 | 0 | 14 | 5 | 3 | 50 | – | | [audio-player](./audio-player.md) | 56% | 18 | 10 | 0 | 8 | 0 | 0 | 8 | – | -| [color-field](./color-field.md) | 56% | 16 | 9 | 0 | 2 | 5 | 0 | 22 | y | | [background](./background.md) | 57% | 26 | 13 | 1 | 6 | 3 | 3 | 31 | – | | [s-text-virtual-list](./s-text-virtual-list.md) | 58% | 12 | 7 | 0 | 3 | 2 | 0 | 9 | – | | [badge](./badge.md) | 59% | 22 | 13 | 6 | 1 | 2 | 0 | 65 | y | @@ -216,6 +215,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [button](./button.md) | 91% | 23 | 20 | 2 | 0 | 0 | 1 | 106 | y | | [tree-view](./tree-view.md) | 92% | 28 | 24 | 1 | 0 | 1 | 2 | 46 | – | | [gradient-builder](./gradient-builder.md) | 93% | 60 | 52 | 0 | 1 | 3 | 4 | 72 | – | +| [color-field](./color-field.md) | 93% | 15 | 14 | 0 | 1 | 0 | 0 | 26 | y | | [scroll-area](./scroll-area.md) | 94% | 17 | 15 | 0 | 0 | 1 | 1 | 13 | – | | [tree-grid](./tree-grid.md) | 95% | 42 | 37 | 1 | 0 | 1 | 3 | 47 | – | | [picker-shell](./picker-shell.md) | 95% | 21 | 20 | 0 | 1 | 0 | 0 | 31 | y | diff --git a/docs/audit/theming/color-field.md b/docs/audit/theming/color-field.md index 7e610a2da..6ac232f88 100644 --- a/docs/audit/theming/color-field.md +++ b/docs/audit/theming/color-field.md @@ -5,42 +5,37 @@ > 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**: **56%** — 9 de 16 knobs por token público -- **Knobs de apariencia**: 16 — público 9 · privado 0 · global 2 · literal 5 · sistema 0 · excepción 0 _(los dos últimos, fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 22 pública(s) — `segment-padding-inline-xs`, `segment-padding-inline-sm`, `segment-padding-inline-md`, `segment-padding-inline-lg`, `segment-padding-inline-xl`, `placeholder-fg`, `literal-fg`, `segment-radius`, `segment-fg`, `segment-invalid-fg`, `segment-readonly-bg`, `segment-readonly-fg`, `segment-readonly-decoration`, `swatch-size-xs`, `swatch-size-sm`, `swatch-size-md`, `swatch-size-lg`, `swatch-size-xl`, `format-select-fg`, `transition-duration`, `transition-ease`, `disabled-opacity` +- **Medido**: 2026-08-23 · **Alcance**: **93%** — 14 de 15 knobs por token público +- **Knobs de apariencia**: 15 — público 14 · privado 0 · global 1 · literal 0 · sistema 0 · excepción 1 _(los dos últimos, fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 26 pública(s) — `segment-padding-inline-xs`, `segment-padding-inline-sm`, `segment-padding-inline-md`, `segment-padding-inline-lg`, `segment-padding-inline-xl`, `placeholder-fg`, `literal-fg`, `segment-radius`, `segment-fg`, `segment-invalid-fg`, `segment-readonly-bg`, `segment-readonly-fg`, `segment-readonly-decoration`, `swatch-size-xs`, `swatch-size-sm`, `swatch-size-md`, `swatch-size-lg`, `swatch-size-xl`, `format-select-fg`, `transition-duration`, `transition-ease`, `disabled-opacity`, `separator-bg`, `separator-thickness`, `segment-min-width`, `hex-segment-min-width` - **Eje `size`**: sí · **ficheros**: `color-field.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (2) +### 1.1 Directo a primitivo global (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `color-field.css:82` | `[data-color-field-segment][data-segment='literal']` | `background` | `var(--color-border-default)` | -| 2 | `color-field.css:122` | `[data-color-field-swatch][data-color-swatch]` | `border-radius` | `var(--field-control-radius)` ⤴ prestado de `field` | +| 1 | `color-field.css:122` | `[data-color-field-swatch][data-color-swatch]` | `border-radius` | `var(--field-control-radius)` ⤴ prestado de `field` | ### 1.2 A través de un privado (0) _Ninguno._ -### 1.3 Literales (5) +### 1.3 Literales (0) -| # | fichero:línea | selector | propiedad | valor | -| ---: | --- | --- | --- | --- | -| 1 | `color-field.css:59` | `[data-color-field-segment]` | `min-inline-size` | `3ch` | -| 2 | `color-field.css:76` | `[data-color-field-segment][data-segment='literal']` | `inline-size` | `1px` | -| 3 | `color-field.css:77` | `[data-color-field-segment][data-segment='literal']` | `min-inline-size` | `1px` | -| 4 | `color-field.css:111` | `[data-color-field-segment][data-segment='hex']` | `min-inline-size` | `6ch` | -| 5 | `color-field.css:158` | `[data-color-field-format-select][data-select]` | `inline-size` | `fit-content` | +_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 | `color-field.css:158` | `[data-color-field-format-select][data-select]` | `inline-size` | `fit-content` | ## 2. Sistema transversal (0) — informativo, fuera del ratio Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). @@ -61,7 +56,7 @@ _Ninguno._ - **Consume tokens públicos de `field`.** Un token prestado importa la semántica de su dueño: la corrección no es duplicarlo con prefijo propio, sino la decisión de familia que la auditoría de fase 1 dejó registrada (`theming-audit.md` §B, familia calendar). - **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` (4) +### 4.1 Tokens a declarar en `lib/recipes/base.ts` (2) 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 @@ -71,8 +66,6 @@ no distingue lo que debería — se marca `⚠`. | token (`--color-field-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `segment-width` | `root` | ⚠ `3ch` / `1px` / `6ch` | 4 | -| `segment-bg` | `root` | `var(--color-border-default)` | 1 | | `swatch-radius` | `root` | `var(--field-control-radius)` | 1 | | `format-select-width` | `root` | `fit-content` | 1 | @@ -88,6 +81,63 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**EJECUTADO 2026-08-23 — 56 % → 93 %.** Cuatro claves nuevas (22 → 26), un +literal firmado, un préstamo adjudicado **y TRECE tokens preexistentes medidos +uno a uno**: el componente nunca había pasado por R-5.4. Diff de computed **0 +sobre 2.336 valores en 8 estados**. + +**Las cuatro claves.** El componente es un Field y no realiasa nada suyo, así +que lo que faltaba era pequeño y muy concreto: + +- **`separator-bg` + `separator-thickness`** — los separadores de canal («,», + « / ») se pintan como líneas verticales de un pelo, y ni su tinta + (`--color-border-default`) ni su grosor (`1px`) eran alcanzables. El `1px` ES + `--border-width`: es un filete, y `dropdown-menu.separator-thickness` lee la + misma familia. El mismo nodo renderizado como GLIFO («#», «%») ya tenía su + `literal-fg` — una parte, dos tratamientos, y ahora los dos con mando. +- **`segment-min-width` (3ch) + `hex-segment-min-width` (6ch)** — el ancho fijo + por número de caracteres, que impide el reflujo al teclear (99 → 100). Los + gemelos `date-field` y `time-field` cargan el mismo hecho DENTRO de un + `calc(2ch + 2 * var(--…-segment-padding-inline))`, así que el suyo ya viajaba + con un token y el censo lo daba por alcanzable; aquí iba a pelo. Precedentes + de unidad `ch` en el catálogo: `editable.area-min-inline-size` (12ch) y + `tags-input.input-min-inline-size` (8ch). + +**El 7 % que queda es una fila, y es doctrina.** El chip de color lee +`var(--field-control-radius)` porque IGUALA el radio del control — el comentario +del CSS lo dice: «so the chip tracks the control». Qué DECIDE ese token: el +radio del control de campo. Un `swatch-radius` propio daría un segundo mando +para una decisión ajena y permitiría desincronizar el chip del campo que lo +contiene. Préstamo con dueño, se tema en la ficha de `field` — la misma lectura +que el `--slider-disabled-opacity` de `waveform`, el mismo día. + +**Y el `fit-content` del conmutador es identidad**: el trigger se mide POR su +contenido, no contra una longitud. Firmado con su anotación, fuera del ratio. + +**Las trece adjudicaciones, todas PROBADAS forzando sobre nodos reales.** La +demo arranca con valor, en `md`, editable y con tres formatos: cada estado y +cada paso de talla que no monta no tiene nodo que pintar. Seis de estado +(`placeholder`, `invalid`, los tres de `readonly`, `disabled`), cuatro de la +escala del chip (14 → 77, 16 → 77, 20 → 77, 32 → 77 px), la etiqueta del formato +BLOQUEADO —que sólo existe con un único formato permitido, así que se midió +desmarcando `rgb` y `hsl` **con los chips de la propia demo**, no fabricando el +nodo— y las dos de transición. + +**Las dos de transición costaron dos diagnósticos falsos, y el segundo es nuevo.** +El primero es la ley ya escrita: el guard congela `transition` para hacer +medible todo lo demás, así que los tokens que SON la transición no se pueden +mover bajo él. El segundo fue mío: midiéndolas a mano leí `0s` y sospeché del +`@media (prefers-reduced-motion: reduce)` del final del fichero — lo comprobé +corriendo con `reducedMotion: 'no-preference'` y **seguía dando 0s**, porque el +nodo que había cogido (`querySelector('[data-color-field-segment]')`) es el +PRIMER segmento, que es un `literal` y declara `transition: none` de su propia +cosecha. Sobre un segmento editable: `0.12s → 11.5s` y +`cubic-bezier(0.4,0,0.2,1) → steps(7)`. **Mide el nodo que pinta, no el primero +que casa el selector.** + +**Verificación**: sonda antes/después **0 diffs** (2.336 valores · 8 estados · 10 +nodos) · R-5.4 13/26 con las trece restantes adjudicadas y medidas, cero STALE · +`component:audit` PASS · censo 93 % · `eidos-lint` 14 morfo-backed / 14 +eidos-only / **0 invalid, 0 class-hooks**. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index da5605c1e..8a5242a2b 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -18,6 +18,43 @@ */ export const SENTINEL_EXCEPTIONS: Record> = { + 'color-field': { + // 2026-08-23. Thirteen tokens, none of them a lie: the demo boots with a + // value, in `md`, editable, with three formats allowed — so every state + // and every size step but the mounted one has no node to paint. All + // forced on the REAL nodes (never a fabricated one), transitions frozen. + 'placeholder-fg': + 'no empty field in the demo; forced data-placeholder → reaches (oklch(0.61 0 0) → rgb(1,2,3))', + 'segment-invalid-fg': + 'invalid is off in the demo; forced data-invalid → reaches (oklch(0.5855 0.1743 42.74) → rgb(1,2,3))', + 'segment-readonly-bg': + 'readonly is off in the demo; forced data-readonly → reaches (oklch(0.931 0 0) → rgb(1,2,3))', + 'segment-readonly-fg': + 'readonly is off in the demo; forced data-readonly → reaches (oklch(0.5032 0 0) → rgb(1,2,3))', + 'segment-readonly-decoration': + 'readonly is off in the demo; forced data-readonly → reaches (underline dotted → overline wavy)', + 'disabled-opacity': + 'disabled is off in the demo; forced data-disabled → reaches (0.4 → 0.123)', + // The swatch scale: only the step in force paints. Forced each size on the + // real root and measured the swatch — every step reaches. + 'swatch-size-xs': 'only the size in force paints; forced data-size=xs → reaches (14px → 77px)', + 'swatch-size-sm': 'only the size in force paints; forced data-size=sm → reaches (16px → 77px)', + 'swatch-size-lg': 'only the size in force paints; forced data-size=lg → reaches (20px → 77px)', + 'swatch-size-xl': 'only the size in force paints; forced data-size=xl → reaches (32px → 77px)', + // The locked-format LABEL only renders when a single format is allowed + // (`isLocked`). Driven through the demo's own chips — not a hand-made node. + 'format-select-fg': + 'the static label only renders with ONE allowed format; unchecked rgb+hsl in the demo → reaches (oklch(0.5032 0 0) → rgb(1,2,3))', + // The known law: the guard freezes `transition` to make everything else + // measurable, so the tokens that ARE the transition can never move under + // it. Measured in a separate unfrozen pass, on an EDITABLE segment — the + // first segment is a `literal`, which declares `transition: none` of its + // own and reads 0s whatever the token says. + 'transition-duration': + 'frozen by the guard by design; unfrozen pass on the hex segment → reaches (0.12s → 11.5s)', + 'transition-ease': + 'frozen by the guard by design; unfrozen pass on the hex segment → reaches (cubic-bezier(0.4,0,0.2,1) → steps(7))' + }, 'navigation-menu': { // 2026-08-23. The rule is `[…-trigger][data-disabled], […-link][data-disabled]` // and the demo mounts neither — measured with transitions FROZEN, because diff --git a/src/uix/eidos/components/color-field/README.md b/src/uix/eidos/components/color-field/README.md index 0bcc9b952..42e90a8fb 100644 --- a/src/uix/eidos/components/color-field/README.md +++ b/src/uix/eidos/components/color-field/README.md @@ -33,6 +33,37 @@ Color Picker, Bits UI Color Picker. Partes públicas: `Label`, `Input`, `Segment`, `FormatSelect`, `HiddenInput`. +## Talla y tema + +**color-field IS-A Field**: la caja, la altura, el relleno, la tipografía y el +anillo de foco los posee `field.css`, y esta receta NO los realiasa. Su contrato +cubre sólo lo suyo — el segmento, el divisor, el chip y el conmutador de formato +— con 26 claves en `lib/recipes/base.ts`. + +| Token (`--color-field-…`) | Qué fija | +| ----------------------------------------------- | ------------------------------------------- | +| `segment-padding-inline-{xs…xl}` | relleno por talla del segmento | +| `segment-min-width` · `hex-segment-min-width` | el ancho FIJO por nº de caracteres (3ch / 6ch) | +| `segment-radius` · `segment-fg` | forma y tinta del segmento | +| `separator-bg` · `separator-thickness` | la línea divisoria entre canales | +| `placeholder-fg` · `literal-fg` | hueco y glifos de formato («#», «%») | +| `segment-invalid-fg` | segmento inválido | +| `segment-readonly-{bg,fg,decoration}` | segmento de sólo lectura | +| `swatch-size-{xs…xl}` | el chip de color por talla | +| `format-select-fg` | la etiqueta estática del formato bloqueado | +| `transition-{duration,ease}` · `disabled-opacity` | movimiento y apagado | + +**Lo que NO acuña, y por qué** (2026-08-23): el chip lee +`var(--field-control-radius)` porque IGUALA el radio del control —«so the chip +tracks the control»—, y esa decisión es de Field: un `swatch-radius` propio +permitiría desincronizarlos. Préstamo con dueño, se tema en la ficha de `field`. +Y el `fit-content` del conmutador es identidad (el trigger se mide POR su +contenido, no contra una longitud), firmado con su anotación. + +**El divisor**: los separadores de canal («,», « / ») se pintan como líneas +verticales de un pelo y los glifos de formato («#», «%») como texto — la misma +parte, dos tratamientos. Por eso hay `separator-*` y `literal-fg`. + ## Comparativa | Capacidad | Soma/Eidos | Ark UI | Chakra UI | Bits UI | diff --git a/src/uix/eidos/components/color-field/color-field.css b/src/uix/eidos/components/color-field/color-field.css index 045769116..fbe9c891f 100644 --- a/src/uix/eidos/components/color-field/color-field.css +++ b/src/uix/eidos/components/color-field/color-field.css @@ -56,7 +56,7 @@ /* Channels (RGB 0-255 / HSL 0-360 / 0-100 / alpha) are at most 3 digits → fixed 3ch so they don't reflow 99 → 100. Hex (6 chars) overridden below. Font-relative + tabular figures (shared field-segment-state layer). */ - min-inline-size: 3ch; + min-inline-size: var(--color-field-segment-min-width); padding-inline: 0; border-radius: var(--color-field-segment-radius); color: var(--color-field-segment-fg); @@ -73,13 +73,13 @@ VERTICAL DIVIDER LINES between the editable segments. The format's PREFIX / SUFFIX glyphs ("#", "%") keep their text — see the override below. */ [data-color-field-segment][data-segment='literal'] { - inline-size: 1px; - min-inline-size: 1px; + inline-size: var(--color-field-separator-thickness); + min-inline-size: var(--color-field-separator-thickness); align-self: stretch; padding-inline: 0; margin-inline: var(--space-1); color: transparent; - background: var(--color-border-default); + background: var(--color-field-separator-bg); border-radius: 0; pointer-events: none; user-select: none; @@ -108,7 +108,7 @@ /* Hex is always 6 chars (RRGGBB) — wider than the 3ch channel default. */ [data-color-field-segment][data-segment='hex'] { - min-inline-size: 6ch; + min-inline-size: var(--color-field-hex-segment-min-width); } /* Leading value swatch (opt-in via ) — composes the @@ -155,7 +155,7 @@ pin it to the inline end — a standalone