diff --git a/docs/audit/theming/radio-group.md b/docs/audit/theming/radio-group.md index 6f929fd38..9913205cf 100644 --- a/docs/audit/theming/radio-group.md +++ b/docs/audit/theming/radio-group.md @@ -5,39 +5,33 @@ > 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**: **77%** — 56 de 73 knobs por token público -- **Knobs de apariencia**: 75 — público 56 · privado 10 · global 7 · literal 0 · sistema 2 · excepción 0 _(los dos últimos, fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): 61 pública(s) — `gap-vertical`, `gap-horizontal`, `item-size-xs`, `item-size-sm`, `item-size-md`, `item-size-lg`, `item-size-xl`, `dot-size-xs`, `dot-size-sm`, `dot-size-md`, `dot-size-lg`, `dot-size-xl`, `item-gap`, `font-family`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `font-weight`, `line-height`, `item-label-fg`, `disabled-item-label-fg`, `indicator-radius`, `indicator-border-width`, `indicator-bg`, `indicator-border`, `hover-indicator-border`, `disabled-indicator-bg`, `disabled-indicator-border`, `checked-disabled-indicator-bg`, `dot-fg`, `disabled-dot-fg`, `transition-duration`, `transition-ease`, `disabled-opacity`, `primary-solid`, `primary-solid-hover`, `neutral-solid`, `neutral-solid-hover`, `secondary-solid`, `secondary-solid-hover`, `affirm-solid`, `affirm-solid-hover`, `label-font-family`, `label-font-size`, `label-font-weight`, `label-line-height`, `label-fg`, `segmented-track-bg`, `segmented-track-shadow`, `segmented-track-padding`, `segmented-track-radius`, `segmented-segment-padding-inline`, `segmented-segment-fg`, `hover-segmented-segment-fg`, `segmented-segment-font-weight`, `segmented-pill-shadow`, `segmented-pill-radius`, `segmented-transition-duration`, `segmented-transition-ease` · 3 privada(s) forward — `_palette-solid`, `_palette-solid-hover`, `_palette-text` -- **Eje `size`**: sí · **ficheros**: `radio-group.css` +- **Medido**: 2026-08-23 · **Alcance**: **81%** — 46 de 57 knobs por token público +- **Knobs de apariencia**: 59 — público 46 · privado 10 · global 1 · literal 0 · sistema 2 · excepción 0 _(los dos últimos, fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 69 pública(s) — `gap-vertical`, `gap-horizontal`, `item-size-xs`, `item-size-sm`, `item-size-md`, `item-size-lg`, `item-size-xl`, `dot-size-xs`, `dot-size-sm`, `dot-size-md`, `dot-size-lg`, `dot-size-xl`, `item-gap`, `font-family`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `item-size`, `dot-size`, `font-size`, `font-weight`, `line-height`, `item-label-fg`, `disabled-item-label-fg`, `invalid-item-label-fg`, `indicator-radius`, `indicator-border-width`, `indicator-bg`, `indicator-border`, `hover-indicator-border`, `disabled-indicator-bg`, `disabled-indicator-border`, `checked-disabled-indicator-bg`, `invalid-indicator-border`, `invalid-ring`, `invalid-ring-gap`, `dot-fg`, `disabled-dot-fg`, `transition-duration`, `transition-ease`, `disabled-opacity`, `primary-solid`, `primary-solid-hover`, `neutral-solid`, `neutral-solid-hover`, `secondary-solid`, `secondary-solid-hover`, `affirm-solid`, `affirm-solid-hover`, `label-font-family`, `label-font-size`, `label-font-weight`, `label-line-height`, `label-fg`, `segmented-track-bg`, `segmented-track-shadow`, `segmented-track-padding`, `segmented-track-radius`, `segmented-segment-padding-inline`, `segmented-segment-padding-block`, `segmented-segment-fg`, `hover-segmented-segment-fg`, `segmented-segment-font-weight`, `segmented-pill-shadow`, `segmented-pill-radius`, `segmented-transition-duration`, `segmented-transition-ease` · 3 privada(s) forward — `_palette-solid`, `_palette-solid-hover`, `_palette-text` +- **Eje `size`**: no · **ficheros**: `radio-group.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (7) +### 1.1 Directo a primitivo global (1) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `radio-group.css:41` | `[data-radio-group][data-invalid] [data-radio-group-row-label]` | `color` | `var(--color-threat-text)` | -| 2 | `radio-group.css:78` | `[data-radio-group-row]` | `min-block-size` | `var(--touch-target)` | -| 3 | `radio-group.css:93` | `[data-radio-group-item]` | `inline-size` | `var(--_radio-item-size)` | -| 4 | `radio-group.css:94` | `[data-radio-group-item]` | `block-size` | `var(--_radio-item-size)` | -| 5 | `radio-group.css:188` | `[data-radio-group][data-invalid] [data-radio-group-item]` | `border-color` | `var(--color-threat-element)` | -| 6 | `radio-group.css:192` | `[data-radio-group][data-invalid] [data-radio-group-item]:focus-visible` | `box-shadow` | `0 0 0 2px var(--color-surface-default), 0 0 0 4px var(--color-threat-element)` | -| 7 | `radio-group.css:313` | `[data-radio-group][data-variant='segmented'] [data-radio-group-item]` | `padding-block` | `var(--space-1)` | +| 1 | `radio-group.css:64` | `[data-radio-group-row]` | `min-block-size` | `var(--touch-target)` | ### 1.2 A través de un privado (10) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `radio-group.css:146` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked'], [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']` | `background` | `var(--_radio-group-palette-solid)` | -| 2 | `radio-group.css:147` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked'], [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']` | `border-color` | `var(--_radio-group-palette-solid)` | -| 3 | `radio-group.css:153` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly]), [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly])` | `background` | `var(--_radio-group-palette-solid-hover)` | -| 4 | `radio-group.css:154` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly]), [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly])` | `border-color` | `var(--_radio-group-palette-solid-hover)` | -| 5 | `radio-group.css:161` | `[data-radio-group][data-variant='outline'] [data-radio-group-item][data-state='checked']` | `border-color` | `var(--_radio-group-palette-solid)` | -| 6 | `radio-group.css:173` | `[data-radio-group][data-variant='ghost'] [data-radio-group-item]:hover:not([data-disabled]):not([data-readonly])` | `background` | `color-mix(in srgb, var(--_radio-group-palette-solid) 8%, transparent)` | -| 7 | `radio-group.css:178` | `[data-radio-group][data-variant='ghost'] [data-radio-group-item][data-state='checked']` | `background` | `color-mix(in srgb, var(--_radio-group-palette-solid) 18%, transparent)` | -| 8 | `radio-group.css:214` | `[data-radio-group-indicator]` | `color` | `var(--_radio-group-palette-text)` | -| 9 | `radio-group.css:355` | `[data-radio-group][data-variant='segmented'] [data-radio-group-item][data-state='checked']` | `color` | `var(--_radio-group-palette-text)` | -| 10 | `radio-group.css:372` | `[data-radio-group-selection-indicator]` | `background` | `var(--_radio-group-palette-solid)` | +| 1 | `radio-group.css:109` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked'], [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']` | `background` | `var(--_radio-group-palette-solid)` | +| 2 | `radio-group.css:110` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked'], [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']` | `border-color` | `var(--_radio-group-palette-solid)` | +| 3 | `radio-group.css:116` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly]), [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly])` | `background` | `var(--_radio-group-palette-solid-hover)` | +| 4 | `radio-group.css:117` | `[data-radio-group][data-variant='solid'] [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly]), [data-radio-group]:not([data-variant]) [data-radio-group-item][data-state='checked']:hover:not([data-disabled]):not([data-readonly])` | `border-color` | `var(--_radio-group-palette-solid-hover)` | +| 5 | `radio-group.css:124` | `[data-radio-group][data-variant='outline'] [data-radio-group-item][data-state='checked']` | `border-color` | `var(--_radio-group-palette-solid)` | +| 6 | `radio-group.css:136` | `[data-radio-group][data-variant='ghost'] [data-radio-group-item]:hover:not([data-disabled]):not([data-readonly])` | `background` | `color-mix(in srgb, var(--_radio-group-palette-solid) 8%, transparent)` | +| 7 | `radio-group.css:141` | `[data-radio-group][data-variant='ghost'] [data-radio-group-item][data-state='checked']` | `background` | `color-mix(in srgb, var(--_radio-group-palette-solid) 18%, transparent)` | +| 8 | `radio-group.css:179` | `[data-radio-group-indicator]` | `color` | `var(--_radio-group-palette-text)` | +| 9 | `radio-group.css:288` | `[data-radio-group][data-variant='segmented'] [data-radio-group-item][data-state='checked']` | `color` | `var(--_radio-group-palette-text)` | +| 10 | `radio-group.css:305` | `[data-radio-group-selection-indicator]` | `background` | `var(--_radio-group-palette-solid)` | ### 1.3 Literales (0) @@ -57,8 +51,8 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2). | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `radio-group.css:138` | `[data-radio-group-item]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | -| 2 | `radio-group.css:359` | `[data-radio-group][data-variant='segmented'] [data-radio-group-item]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | +| 1 | `radio-group.css:101` | `[data-radio-group-item]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | +| 2 | `radio-group.css:292` | `[data-radio-group][data-variant='segmented'] [data-radio-group-item]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | ## 3. Privados de la receta — ¿de dónde sale su valor? @@ -71,9 +65,8 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl ## 4. Propuesta de corrección - **Consume la capa compartida `sliding-indicator`.** Un eje que la capa posee se consume como `var(--_x, var(--x))`; el consumidor **no acuña** `--radio-group-{eje}` para él — sería un vocabulario paralelo (README de `eidos/components`, «Capas compartidas» regla 2). -- **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 @@ -85,13 +78,7 @@ no distingue lo que debería — se marca `⚠`. | --- | --- | --- | ---: | | `indicator-fg` | `root` | ⚠ `var(--radio-group-dot-fg)` / `var(--_radio-group-palette-solid)` | 4 | | `segmented-on-item-fg` | `root` | ⚠ `var(--radio-group-dot-fg)` / `var(--_radio-group-palette-solid)` | 4 | -| `invalid-row-label-fg` | `root` | `var(--color-threat-text)` | 1 | | `row-height` | `root` | `var(--touch-target)` | 1 | -| `item-width` | `root` | `var(--_radio-item-size)` | 1 | -| `item-height` | `root` | `var(--_radio-item-size)` | 1 | -| `invalid-item-border` | `root` | `var(--color-threat-element)` | 1 | -| `invalid-item-shadow` | `root` | `0 0 0 2px var(--color-surface-default), 0 0 0 4px var(--color-threat-element)` | 1 | -| `segmented-item-padding-block` | `root` | `var(--space-1)` | 1 | ### 4.2 Sin nombre mecánico (8) @@ -113,6 +100,75 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Ejecutado 2026-08-23 · 77 % → 81 % · CERRADO en su número.** + +Contrato 61 → **69 claves**. Censo: knobs 75 → 59, público 56 → 46, **global +7 → 1**, privado 10 (invariante), sistema 2. Diff de computed **0** sobre +21.423 valores en 40 estados (20 combinaciones de variante × estado × talla, +cada una con su pasada de hover) más 3.840 de la sonda estándar; capturas 2× +antes/después idénticas. Centinela **46/69**, 23 adjudicadas. + +**Qué entra (8 claves).** Tres RESUELTAS por talla en el TSC —`item-size`, +`dot-size`, `font-size`— que retiran los **diecisiete bloques `[data-size]`** +del CSS (§7.4-4) y jubilan `--_radio-item-size`, un privado con el nombre del +componente ABREVIADO (theming §6 r5; la clase `--_mp-*` / `--gp-*`) que hacía +que el ancho y el alto del ítem contaran como deuda. Cuatro de validación +—`invalid-indicator-border`, `invalid-ring`, `invalid-ring-gap`, +`invalid-item-label-fg`— donde la receta leía `--color-threat-{element,text}` a +pelo; el trío del anillo copia el vocabulario del gemelo `checkbox`, que ya lo +tenía. Y `segmented-segment-padding-block`, el hermano que le faltaba a +`-padding-inline`. + +**Por qué 81 % es el TECHO, no una deuda.** Los diez knobs restantes leen +`--_radio-group-palette-*`: el puente THM-2 (CONTINUE §3.pre) que la capa +compartida alimenta por instancia desde `[data-color]` — un público encima +dejaría que un tema lo fijara y matara el `color=` de cada instancia. El único +`global` que queda es `min-block-size: var(--touch-target)` de la fila bajo +`@media (pointer: coarse)`: **se acuñó y hubo que retirarlo**, porque +`--touch-target` no lo emite ningún generador (es una constante ergonómica +escrita a mano en `archetypes.css`, «the fingertip, NOT a theme value») y el +guard `recipe-css-contract` lo cazó como referencia FANTASMA en el acto. +recipe-contract §2 lista el touch-target entre los sistemas transversales que +una receta CONSUME y nunca acuña, así que la lectura cruda es el contrato +cumpliéndose; el censo la cuenta `global` sólo porque su lista `system` aún no +conoce el token (el pendiente de firma «extender la clase system a las capas +compartidas» del CONTINUE). + +**Tres declaraciones que NO PINTAN, medidas** (en el ledger del centinela y en +`next-features.md` §13; arreglarlas mueve píxel ⇒ D-TH.5, esperan firma): + +1. **La escala del punto nunca ha pintado.** `` escribe + `style="width: 1em; height: 1em"` — un inline gana a cualquier regla. El + punto computa **13,3281px en las cinco tallas** mientras el token dice + 5/6/7/9/11px, el valor centinela no lo mueve, y quitando el estilo inline el + MISMO token pinta 1234px. Es la clase «canal de valor inline» del trío + `carousel.item-gap*` y `drag-drop.preview-z`, pero aquí con arreglo posible: + la forma `!important` que recipe-contract §3 autoriza justo para saltarse un + estilo inline. +2. **`[data-radio-group-label][data-disabled]` no casa jamás.** Con el grupo + entero deshabilitado (`?perm.disabled=true`) la etiqueta lleva + `[id, data-radio-group-label, data-archetype]` y nada más: soma no estampa + `data-disabled` en la parte `Label` (el morfo declara `data: []`). Forzado a + mano el token alcanza, así que **la clave es sana y falta el ATRIBUTO** — + decisión de morfo (el label gemelo de `checkbox` sí lo lleva). +3. **El fondo de un radio marcado Y deshabilitado no llega.** La regla de + variante es (0,4,0) y `[data-radio-group-item][data-disabled][data-state='checked']` + sólo (0,3,0): medido con el valor centinela escrito, `solid` da el acento a + plena intensidad, `outline` y `segmented` transparente, `ghost` la mezcla al + 18 % — nunca el centinela. Y quitar `data-variant` no ayuda: el gemelo + `:not([data-variant])` también es (0,4,0). + +**Y una cuarta, de escala del SISTEMA, fuera de este eje por D-TH.5**: el +`:hover` del círculo es (0,4,0) y el tinte inválido (0,3,0), así que **pasar el +ratón por encima TAPA el borde de error**. Mueve píxel en cuatro componentes +con el mismo patrón: se anota, no se toca. + +**El instrumento**, que medía 27 de 61: la sonda estándar no ve el punto (es un +`` sin atributo del componente), ni la variante `segmented` +(doce claves), ni los estados `disabled` / `invalid`. El guard lleva ahora +`extraNodes` para el punto, un barrido de DOS ejes (`data-variant` × +`data-orientation` — el `gap` es un PAR y sólo pintaba la mitad) y las dos URL +con `?perm.disabled` / `?perm.invalid`, que la propia demo honra: once +adjudicaciones sustituidas por medición real. diff --git a/docs/next-features.md b/docs/next-features.md index 103a4e161..0a629a77c 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -1517,6 +1517,33 @@ Lo que `chart` destapó (2026-08-22): `range-calendar` renderiza un heading de texto en su lugar, mientras `DateRangePicker` importa los selectores del CALENDARIO (que llevan `data-calendar-month-select`). Hueco de DEMO: la parte existe en soma y en - eidos y ninguna superficie la monta. + eidos y ninguna superficie la monta.- **La escala del punto de `radio-group` NUNCA ha pintado** (medido + 2026-08-23). `` escribe `style="width: 1em; height: 1em"` en el glifo y + un inline gana a cualquier regla: el punto computa **13,3281px en las cinco + tallas** mientras `--radio-group-dot-size-{xs…xl}` dice 5/6/7/9/11px. Quitando + el estilo inline el MISMO token pinta. Es la clase «canal de valor inline» de + `carousel.item-gap*` y `drag-drop.preview-z`, pero con arreglo disponible: la + forma `!important` que recipe-contract §3 autoriza justo para saltarse un + estilo inline (o pasarle la talla a ``). **Mueve píxel** ⇒ + firma. Seis claves adjudicadas mientras tanto. +- **`radio-group` no tiñe la etiqueta de grupo deshabilitada**: la regla es + `[data-radio-group-label][data-disabled]` y soma **no estampa `data-disabled` + en la parte `Label`** (el morfo declara `data: []`). Medido 2026-08-23 con el + grupo entero deshabilitado: la etiqueta lleva `[id, data-radio-group-label, + data-archetype]` y nada más. El token alcanza forzando el atributo, así que + falta el ATRIBUTO, no la clave — decisión de MORFO (el label gemelo de + `checkbox` sí lo lleva). +- **Un radio marcado Y deshabilitado muestra el acento a plena intensidad** + (medido 2026-08-23): la regla de variante + `[data-radio-group][data-variant=X] [data-radio-group-item][data-state='checked']` + es (0,4,0) y `[data-radio-group-item][data-disabled][data-state='checked']` + sólo (0,3,0) — y el gemelo `:not([data-variant])` también es (0,4,0), así que + no hay instancia que escape. Subir la especificidad **mueve píxel** ⇒ firma. +- **El `:hover` de `radio-group` TAPA su tinte inválido**: hover (0,4,0) contra + invalid (0,3,0), la misma escalera que `textarea` (hover > foco > invalid) ya + registrada aquí. Es una decisión de escala del SISTEMA que mueve píxel en + varios controles de formulario con el mismo patrón; se anota, no se toca en + el eje (D-TH.5). + **Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando estorben. diff --git a/docs/process/PLAN-theming.md b/docs/process/PLAN-theming.md index e073e0893..c8317f31c 100644 --- a/docs/process/PLAN-theming.md +++ b/docs/process/PLAN-theming.md @@ -486,6 +486,25 @@ commits de corrección. ## 8. Registro +- 2026-08-23 — **`radio-group` 77 % → 81 %, y su TECHO es ése** (COMMIT), + contrato 61 → **69** claves, centinela **46/69** con 23 adjudicadas. Ocho + claves: tres RESUELTAS por talla (`item-size`, `dot-size`, `font-size`) que + retiran **diecisiete bloques `[data-size]`** y jubilan `--_radio-item-size` + (privado con el nombre ABREVIADO, clase `--_mp-*`/`--gp-*`), cuatro de + validación que iban a `--color-threat-*` a pelo (el trío del anillo con el + vocabulario del gemelo `checkbox`) y `segmented-segment-padding-block`. Diff + **0** sobre 25.263 valores en 47 estados. Los diez knobs que quedan son el + puente THM-2 de paleta: **el techo no es el 100 %**. + **`row-min-height-touch` se acuñó y hubo que RETIRARLO**: `--touch-target` no + lo emite ningún generador (constante ergonómica escrita a mano en + `archetypes.css`) y `recipe-css-contract` lo cazó como referencia FANTASMA en + el acto — recipe-contract §2 ya lo lista entre los sistemas transversales que + una receta consume y nunca acuña. **Y salieron TRES declaraciones que no + pintan** (§13, las tres mueven píxel ⇒ firma): la escala del punto no ha + pintado nunca (`` escribe `width:1em` INLINE — 13,3281px en las cinco + tallas), `[data-radio-group-label][data-disabled]` no casa jamás (soma no + estampa el atributo en la parte `Label`), y el fondo de un radio marcado Y + deshabilitado pierde (0,3,0) contra la regla de variante (0,4,0). - 2026-08-23 — **`chat-composer` 79 % → 100 %** (COMMIT), contrato 31 → **33** claves, centinela **29/33** con 4 adjudicadas. **La mitad de su contrato no la montaba la demo**: 14 muertas de 31, y once eran la barra de contexto diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 948089304..9eb04fe4d 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -1123,7 +1123,49 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'the guard freezes transitions and this token IS the transition; measured unfrozen on a real day cell -> reaches (0.12s -> 11.5s on all five properties)', 'transition-ease': 'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3))' - } + }, + 'radio-group': { + // 2026-08-23. The guard already sweeps its four variants and both + // orientations and measures the disabled / invalid stages through the + // demo's URL switches; these are what is left, each measured on the REAL + // nodes. Three of them are DEFECTS, not instrument limits, and say so. + // + // (1) The dot's whole scale — a value channel written INLINE. + 'dot-size': + 'DEAD BY INLINE STYLE, not by the demo: writes style="width: 1em; height: 1em" on the glyph, and an inline declaration beats any stylesheet rule. Measured 2026-08-23: the dot computes 13.3281px (1em of the button UA font) at EVERY step while the token says 5/6/7/9/11px, the sentinel value changes nothing, and removing the inline style makes the same token paint 1234px. The recipe scale has never painted — a pixel-moving fix (the `!important` shape recipe-contract §3 allows for exactly this, bypassing an inline style) that needs a signature; registered in next-features §13', + 'dot-size-xs': 'same inline style; the step reads 5px and the dot paints 13.3281px', + 'dot-size-sm': 'same inline style; the step reads 6px and the dot paints 13.3281px', + 'dot-size-md': 'same inline style; the step reads 7px and the dot paints 13.3281px', + 'dot-size-lg': 'same inline style; the step reads 9px and the dot paints 13.3281px', + 'dot-size-xl': 'same inline style; the step reads 11px and the dot paints 13.3281px', + // (2) A rule whose selector no runtime ever matches. + 'disabled-item-label-fg': + 'DEAD SELECTOR: it is consumed by [data-radio-group-label][data-disabled], and soma never stamps data-disabled on the Label part (the morfo declares `data: []` for it). Measured 2026-08-23 with the group disabled through ?perm.disabled=true: the label carries [id, data-radio-group-label, data-archetype] and nothing else. Forced on the label by hand -> reaches (oklch(0.2435 0 0) -> rgb(1, 2, 3)), so the token is sound and the ATTRIBUTE is missing — a morfo decision (checkbox\'s twin label does carry its own), registered in next-features §13', + // (3) A declaration the variant rules out-rank in every variant. + 'checked-disabled-indicator-bg': + 'SUPERSEDED by the variant rules: [data-radio-group][data-variant=X] [data-radio-group-item][data-state=checked] is (0,4,0) and [data-radio-group-item][data-disabled][data-state=checked] only (0,3,0). Measured 2026-08-23 on a disabled+checked item with the sentinel value written: solid oklch(0.5556 0.1829 305.86) (the full accent), outline and segmented rgba(0,0,0,0), ghost the 18% mix — never rgb(1,2,3). And removing data-variant does not help: the `:not([data-variant])` twin is (0,4,0) too. So a disabled+checked radio shows the FULL accent today; raising the specificity moves pixels -> signature, registered in next-features §13', + // Instrument limits, measured. + 'gap-horizontal': + 'the row gap is a PAIR and only the half in force paints: the demo boots `vertical`, and the guard sweeps ONE axis (data-variant, where twelve of its keys live). Switched the real demo to horizontal -> reaches (column-gap 16px -> 1234px)', + 'invalid-ring': + 'needs [data-invalid] AND :focus-visible, and the guard blurs on purpose; on a button :focus-visible only latches with the keyboard modality, so measured 2026-08-23 with a Tab followed by focus() on the real item -> reaches (the 4px ring oklch(0.9555 0.0207 13.86) -> rgb(1, 2, 3))', + 'invalid-ring-gap': 'same double ring; measured the same way -> reaches (the 2px gap oklch(0.9911 0 0) -> rgb(4, 5, 6))', + 'transition-duration': + 'the guard freezes transitions and this token IS the transition; measured unfrozen on the real item -> reaches (0.12s -> 11.5s on all four transitioned properties)', + 'transition-ease': + 'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3))', + 'segmented-transition-duration': + 'same freeze, on the segmented pair; measured unfrozen with the real variant on -> reaches (segment 0.18s -> 11.5s, and the pill\'s --indicator-duration 180ms -> 11.5s)', + 'segmented-transition-ease': + 'same freeze; measured unfrozen -> reaches (cubic-bezier(0.32, 0.72, 0, 1) -> steps(3))', + 'segmented-pill-shadow': + 'the sliding pill is a node the WRAPPER only renders under variant="segmented" (like Switch\'s default Thumb), so stamping data-variant while measuring cannot create it; measured with the demo\'s real segmented chip on -> reaches (rgba(15, 23, 42, 0.08) 0 1px 2px -> rgb(1, 2, 3) 0 0 0 7px)' + // `hover-segmented-segment-fg` had an entry here for one run and the guard + // reported it STALE on the next: it needs hover AND the segmented variant, + // which the hover pass does not sweep — yet the pointer's resting place is + // enough for the static pass to catch it. Verified alive twice before the + // entry was pruned (hovered segment: oklch(0.2435 0 0) -> rgb(1, 2, 3)). + }, }; /** @@ -1172,6 +1214,13 @@ export const SENTINEL_PATTERN_EXCEPTIONS: Array<{ pattern: /^(primary|secondary|neutral|affirm)-(solid|solid-hover)$/, reason: PALETTE_SUPERSEDED }, + { + component: 'radio-group', + pattern: /^(primary|secondary|neutral|affirm)-(solid|solid-hover)$/, + reason: + PALETTE_SUPERSEDED + + ' — the same four tones x two slots as its twin checkbox, and PRIMARY falls with them: the eidos wrapper always resolves a colour and stamps data-color, so the generic block matches on every instance and the fallback that names the component key never applies. Measured 2026-08-23 on an affirm instance: --radio-group-affirm-solid moves nothing (oklch(0.6491 0.1136 181.96) stays), --palette-solid on the same node repaints (rgb(4, 5, 6))' + }, { component: 'badge', pattern: diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index ee15d20ba..86d2fc9fd 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -244,6 +244,23 @@ 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] *' }, + // Three blind spots at once (measured 2026-08-23, 27 of 61 tokens moved + // before this): the DOT is a bare `` inside the + // indicator, so the five `dot-size-*` steps had no node in the filter; the + // whole SEGMENTED chrome (track, segment, pill — twelve keys) only paints + // under `data-variant='segmented'` and the demo boots `solid`; and the + // disabled / invalid chrome needs a STATE the stage does not wear — this + // demo reads both from the URL (`perm.disabled`, `perm.invalid`), so they + // are measured as extra surfaces instead of eleven ledger lines. + 'radio-group': { + extraNodes: "[data-radio-group-indicator] svg[data-svg='dot']", + sweepAttr: { attr: 'data-variant', values: ['solid', 'outline', 'ghost', 'segmented'] }, + urls: [ + '/uix/components/radio-group', + '/uix/components/radio-group?perm.disabled=true', + '/uix/components/radio-group?perm.invalid=true' + ] + }, // Half its contract lives in parts the default stage does not mount: the // AutoFields tree (array, item, widget) only exists in the demo's `auto` // mode, and the panel chrome only under `variant='panel'`. 18 of 54 tokens diff --git a/src/uix/eidos/components/radio-group/README.md b/src/uix/eidos/components/radio-group/README.md index 73d7b45bb..fbb8fc0c5 100644 --- a/src/uix/eidos/components/radio-group/README.md +++ b/src/uix/eidos/components/radio-group/README.md @@ -66,6 +66,73 @@ Sources: [Radix](https://www.radix-ui.com/primitives/docs/components/radio-group | `color` | `primary` \| `secondary` \| `neutral` \| `affirm` | Active palette. | | `columns` | `number` | Switches root layout to CSS grid using `--_radio-group-columns`. | +## Talla y tema + +**69 claves públicas** en `lib/recipes/base.ts` más tres privadas de paleta +(`_palette-solid`, `_palette-solid-hover`, `_palette-text` — el puente THM-2, +que no se acuña). El eje `size` se resuelve en el TSC, no en el CSS: la receta +lee **un solo nombre** por eje (`item-size`, `dot-size`, `font-size`) y el +contrato lo hace bajar por talla desde el proveedor, que es quien lleva +`data-size`. La pestaña **Tokens** de su demo las resuelve en vivo. + +| Eje | Coordenadas | Nombre resuelto | +| --- | --- | --- | +| Diámetro del círculo | `item-size-{xs…xl}` (14/16/18/22/26px) | `item-size` | +| Diámetro del punto | `dot-size-{xs…xl}` (5/6/7/9/11px) | `dot-size` | +| Texto de la fila | `font-size-{xs…xl}` = `--size-{k}-font-size` | `font-size` | + +El resto, por rol: la caja (`gap-vertical`, `gap-horizontal`, `item-gap`, +`font-family`, `font-weight`, `line-height`, `disabled-opacity`), el cromo del +círculo (`indicator-{radius,border-width,bg,border}`, `hover-indicator-border`), +la tinta (`item-label-fg`, `dot-fg`), los estados (`disabled-*`, `invalid-*`), +la etiqueta de grupo (`label-*`), la variante `segmented` +(`segmented-track-*`, `segmented-segment-*`, `segmented-pill-*`, +`segmented-transition-*`) y los ocho forwards de tono. + +Lo que entró el **2026-08-23** (77 % → 81 %): + +- **`item-size` / `dot-size` / `font-size` resueltos** — y con ellos se retiran + los **diecisiete bloques `[data-size]`** que el CSS llevaba. El primero + además jubila `--_radio-item-size`, un privado con el nombre del componente + ABREVIADO (theming §6 regla 5 lo prohíbe; la clase de `--_mp-*` y `--gp-*`): + el censo no lo reconocía como suyo y contaba el ancho y el alto del ítem como + deuda. +- **`invalid-indicator-border`, `invalid-ring`, `invalid-ring-gap`, + `invalid-item-label-fg`** — el tinte de validación iba a + `--color-threat-{element,text}` a pelo. El trío del anillo copia el + vocabulario del gemelo `checkbox`, que ya lo tenía. +- **`segmented-segment-padding-block`** — el hermano que le faltaba a + `segmented-segment-padding-inline`. + +**Lo que NO se acuña, y por qué**: el suelo táctil de la fila +(`min-block-size: var(--touch-target)` bajo `@media (pointer: coarse)`) es un +sistema transversal de recipe-contract §2, no un knob — `--touch-target` es una +constante ergonómica escrita a mano en `archetypes.css`, y darle un token de +componente la convierte en referencia FANTASMA (el guard `recipe-css-contract` +lo rechazó en el acto). Y los diez knobs que quedan fuera del alcance son los +que leen `--_radio-group-palette-*`: el puente THM-2 que la capa compartida +alimenta por instancia desde `[data-color]`. **El techo de este componente es +ese 81 %**, no el 100 %. + +**Tres declaraciones que NO pintan** (medidas el 2026-08-23, todas en el ledger +del centinela y en `next-features.md` §13 — arreglarlas mueve píxel, así que +esperan firma): + +1. **La escala del punto nunca ha pintado.** `` escribe + `style="width: 1em; height: 1em"` y un inline gana a cualquier regla: el + punto computa **13,3281px en las cinco tallas** mientras el token dice + 5/6/7/9/11px. +2. **`[data-radio-group-label][data-disabled]` no casa jamás**: soma no estampa + `data-disabled` en la parte `Label` (el morfo declara `data: []`), ni con el + grupo entero deshabilitado. +3. **El fondo de un radio marcado Y deshabilitado no llega**: la regla de + variante es (0,4,0) y `[data-radio-group-item][data-disabled][data-state='checked']` + sólo (0,3,0), así que hoy se ve el acento a plena intensidad. + +Y una cuarta, de escala del SISTEMA y por tanto fuera de este eje: el +`:hover` del círculo es (0,4,0) y el tinte inválido (0,3,0), de modo que +**pasar el ratón por encima TAPA el borde de error**. + ## Ownership - Morfo declares radiogroup roles, item states, data attrs and the diff --git a/src/uix/eidos/components/radio-group/radio-group.css b/src/uix/eidos/components/radio-group/radio-group.css index dd5b8958f..d9c82da20 100644 --- a/src/uix/eidos/components/radio-group/radio-group.css +++ b/src/uix/eidos/components/radio-group/radio-group.css @@ -38,32 +38,18 @@ } [data-radio-group][data-invalid] [data-radio-group-row-label] { - color: var(--color-threat-text); + color: var(--radio-group-invalid-item-label-fg); } +/* `--radio-group-font-size` is the size-RESOLVED name: the TSC emits it on the + * provider (which carries `data-size`) and every part reads the one name. */ [data-radio-group-row-label] { font-family: var(--radio-group-font-family); - font-size: var(--radio-group-font-size-md); + font-size: var(--radio-group-font-size); line-height: var(--radio-group-line-height); font-weight: var(--radio-group-font-weight); } -[data-radio-group][data-size='xs'] [data-radio-group-row-label] { - font-size: var(--radio-group-font-size-xs); -} - -[data-radio-group][data-size='sm'] [data-radio-group-row-label] { - font-size: var(--radio-group-font-size-sm); -} - -[data-radio-group][data-size='lg'] [data-radio-group-row-label] { - font-size: var(--radio-group-font-size-lg); -} - -[data-radio-group][data-size='xl'] [data-radio-group-row-label] { - font-size: var(--radio-group-font-size-xl); -} - /* ── Touch-target (a11y) — coarse pointers ────────────────────────────────── * On COARSE pointers (touch), the clickable label ROW (dot + text) is the 44px * tap target — the reference-grade web pattern for small markers (React Aria wraps @@ -82,7 +68,6 @@ /* ── Item ───────────────────────────────────────────────────────────────── */ [data-radio-group-item] { - --_radio-item-size: var(--radio-group-item-size-md); /* `--_radio-group-palette-*` is declared on the [data-radio-group] host * (via TSC `_palette-*` in lib/recipes/base.ts) and inherited here. */ @@ -90,8 +75,8 @@ align-items: center; justify-content: center; flex: 0 0 auto; - inline-size: var(--_radio-item-size); - block-size: var(--_radio-item-size); + inline-size: var(--radio-group-item-size); + block-size: var(--radio-group-item-size); padding: 0; border: var(--radio-group-indicator-border-width) solid var(--radio-group-indicator-border); border-radius: var(--radio-group-indicator-radius); @@ -106,28 +91,6 @@ opacity var(--radio-group-transition-duration) var(--radio-group-transition-ease); } -/* ── Sizes ──────────────────────────────────────────────────────────────── */ - -[data-radio-group][data-size='xs'] [data-radio-group-item] { - --_radio-item-size: var(--radio-group-item-size-xs); -} - -[data-radio-group][data-size='sm'] [data-radio-group-item] { - --_radio-item-size: var(--radio-group-item-size-sm); -} - -[data-radio-group][data-size='md'] [data-radio-group-item] { - --_radio-item-size: var(--radio-group-item-size-md); -} - -[data-radio-group][data-size='lg'] [data-radio-group-item] { - --_radio-item-size: var(--radio-group-item-size-lg); -} - -[data-radio-group][data-size='xl'] [data-radio-group-item] { - --_radio-item-size: var(--radio-group-item-size-xl); -} - /* ── Hover / focus ─────────────────────────────────────────────────────── */ [data-radio-group-item]:hover:not([data-disabled]):not([data-readonly]) { @@ -185,11 +148,13 @@ * because invalid signals priority. */ [data-radio-group][data-invalid] [data-radio-group-item] { - border-color: var(--color-threat-element); + border-color: var(--radio-group-invalid-indicator-border); } [data-radio-group][data-invalid] [data-radio-group-item]:focus-visible { - box-shadow: 0 0 0 2px var(--color-surface-default), 0 0 0 4px var(--color-threat-element); + box-shadow: + 0 0 0 2px var(--radio-group-invalid-ring-gap), + 0 0 0 4px var(--radio-group-invalid-ring); } /* ── Disabled / readonly ────────────────────────────────────────────────── */ @@ -215,35 +180,16 @@ pointer-events: none; } -/* SVG dot — sized via the dot tokens, scales 0→1 for the select moment. */ +/* SVG dot — sized via the size-RESOLVED dot token, scales 0→1 for the select + * moment. */ [data-radio-group-indicator] svg[data-svg='dot'] { - width: var(--radio-group-dot-size-md); - height: var(--radio-group-dot-size-md); + width: var(--radio-group-dot-size); + height: var(--radio-group-dot-size); transform: scale(0); transition: transform var(--radio-group-transition-duration) cubic-bezier(0.4, 0, 0.2, 1); } -[data-radio-group][data-size='xs'] [data-radio-group-indicator] svg[data-svg='dot'] { - width: var(--radio-group-dot-size-xs); - height: var(--radio-group-dot-size-xs); -} - -[data-radio-group][data-size='sm'] [data-radio-group-indicator] svg[data-svg='dot'] { - width: var(--radio-group-dot-size-sm); - height: var(--radio-group-dot-size-sm); -} - -[data-radio-group][data-size='lg'] [data-radio-group-indicator] svg[data-svg='dot'] { - width: var(--radio-group-dot-size-lg); - height: var(--radio-group-dot-size-lg); -} - -[data-radio-group][data-size='xl'] [data-radio-group-indicator] svg[data-svg='dot'] { - width: var(--radio-group-dot-size-xl); - height: var(--radio-group-dot-size-xl); -} - /* Scale up to full size when the parent Item is checked. */ [data-radio-group-item][data-state='checked'] [data-radio-group-indicator] svg[data-svg='dot'] { transform: scale(1); @@ -310,14 +256,14 @@ inline-size: auto; block-size: auto; flex: 0 0 auto; - padding-block: var(--space-1); + padding-block: var(--radio-group-segmented-segment-padding-block); padding-inline: var(--radio-group-segmented-segment-padding-inline); border: none; background: transparent; border-radius: var(--radio-group-segmented-pill-radius); color: var(--radio-group-segmented-segment-fg); font-family: var(--radio-group-font-family); - font-size: var(--radio-group-font-size-md); + font-size: var(--radio-group-font-size); font-weight: var(--radio-group-segmented-segment-font-weight); line-height: var(--radio-group-line-height); white-space: nowrap; @@ -325,19 +271,6 @@ var(--radio-group-segmented-transition-ease); } -[data-radio-group][data-variant='segmented'][data-size='xs'] [data-radio-group-item] { - font-size: var(--radio-group-font-size-xs); -} -[data-radio-group][data-variant='segmented'][data-size='sm'] [data-radio-group-item] { - font-size: var(--radio-group-font-size-sm); -} -[data-radio-group][data-variant='segmented'][data-size='lg'] [data-radio-group-item] { - font-size: var(--radio-group-font-size-lg); -} -[data-radio-group][data-variant='segmented'][data-size='xl'] [data-radio-group-item] { - font-size: var(--radio-group-font-size-xl); -} - [data-radio-group][data-variant='segmented'] [data-radio-group-item]:hover:not([data-disabled]):not([data-readonly]) { background: transparent; diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index c4a67bb17..266f6bbf0 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -2551,6 +2551,7 @@ --radio-group-line-height: var(--leading-ui); --radio-group-item-label-fg: var(--color-content-primary); --radio-group-disabled-item-label-fg: var(--color-content-disabled); + --radio-group-invalid-item-label-fg: var(--color-threat-text); --radio-group-indicator-radius: var(--radius-full); --radio-group-indicator-border-width: var(--border-width); --radio-group-indicator-bg: var(--color-surface-default); @@ -2559,6 +2560,9 @@ --radio-group-disabled-indicator-bg: color-mix( in srgb, var(--color-surface-overlay) 78%, var(--color-surface-default) ); --radio-group-disabled-indicator-border: color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) ); --radio-group-checked-disabled-indicator-bg: color-mix( in srgb, var(--color-content-disabled) 40%, var(--color-surface-overlay) ); + --radio-group-invalid-indicator-border: var(--color-threat-element); + --radio-group-invalid-ring: var(--color-threat-element); + --radio-group-invalid-ring-gap: var(--color-surface-default); --radio-group-dot-fg: var(--color-content-on-solid); --radio-group-disabled-dot-fg: color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) ); --radio-group-transition-duration: var(--duration-fast); @@ -2582,6 +2586,7 @@ --radio-group-segmented-track-padding: var(--space-1); --radio-group-segmented-track-radius: var(--radius-md); --radio-group-segmented-segment-padding-inline: var(--space-3); + --radio-group-segmented-segment-padding-block: var(--space-1); --radio-group-segmented-segment-fg: var(--color-content-secondary); --radio-group-hover-segmented-segment-fg: var(--color-content-primary); --radio-group-segmented-segment-font-weight: var(--font-weight-medium); @@ -6698,11 +6703,38 @@ } [data-radio-group] { + --radio-group-item-size: var(--radio-group-item-size-md); + --radio-group-dot-size: var(--radio-group-dot-size-md); + --radio-group-font-size: var(--radio-group-font-size-md); --_radio-group-palette-solid: var(--radio-group-primary-solid); --_radio-group-palette-solid-hover: var(--radio-group-primary-solid-hover); --_radio-group-palette-text: var(--radio-group-dot-fg); } +[data-radio-group][data-size='xs'] { + --radio-group-item-size: var(--radio-group-item-size-xs); + --radio-group-dot-size: var(--radio-group-dot-size-xs); + --radio-group-font-size: var(--radio-group-font-size-xs); +} + +[data-radio-group][data-size='sm'] { + --radio-group-item-size: var(--radio-group-item-size-sm); + --radio-group-dot-size: var(--radio-group-dot-size-sm); + --radio-group-font-size: var(--radio-group-font-size-sm); +} + +[data-radio-group][data-size='lg'] { + --radio-group-item-size: var(--radio-group-item-size-lg); + --radio-group-dot-size: var(--radio-group-dot-size-lg); + --radio-group-font-size: var(--radio-group-font-size-lg); +} + +[data-radio-group][data-size='xl'] { + --radio-group-item-size: var(--radio-group-item-size-xl); + --radio-group-dot-size: var(--radio-group-dot-size-xl); + --radio-group-font-size: var(--radio-group-font-size-xl); +} + [data-radio-group][data-color='secondary'] { --_radio-group-palette-solid: var(--radio-group-secondary-solid); --_radio-group-palette-solid-hover: var(--radio-group-secondary-solid-hover); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 980b8562f..9b6ed3c77 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -3899,10 +3899,59 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'font-size-md': 'var(--size-md-font-size)', 'font-size-lg': 'var(--size-lg-font-size)', 'font-size-xl': 'var(--size-xl-font-size)', + // The RESOLVED names. `data-size` is stamped on the PROVIDER, so the + // per-size cascade targets the host and every part inherits it — the CSS + // reads ONE name and the `[data-size]` blocks it used to carry are gone + // (PLAN-theming §7.4-4: leaving them alive keeps the OLD route painting + // and the computed diff reads 0 for the wrong reason). `item-size` also + // retires `--_radio-item-size`, a private with the component name + // ABBREVIATED (theming §6 rule 5 forbids it — the `--_mp-*` /`--gp-*` + // class), which is why the item's own width and height counted as + // out-of-reach knobs. + 'item-size': { + declarations: [ + { value: 'var(--radio-group-item-size-md)', scope: 'host' }, + { value: 'var(--radio-group-item-size-xs)', scope: 'size:xs' }, + { value: 'var(--radio-group-item-size-sm)', scope: 'size:sm' }, + { value: 'var(--radio-group-item-size-lg)', scope: 'size:lg' }, + { value: 'var(--radio-group-item-size-xl)', scope: 'size:xl' } + ] + }, + 'dot-size': { + declarations: [ + { value: 'var(--radio-group-dot-size-md)', scope: 'host' }, + { value: 'var(--radio-group-dot-size-xs)', scope: 'size:xs' }, + { value: 'var(--radio-group-dot-size-sm)', scope: 'size:sm' }, + { value: 'var(--radio-group-dot-size-lg)', scope: 'size:lg' }, + { value: 'var(--radio-group-dot-size-xl)', scope: 'size:xl' } + ] + }, + 'font-size': { + declarations: [ + { value: 'var(--radio-group-font-size-md)', scope: 'host' }, + { value: 'var(--radio-group-font-size-xs)', scope: 'size:xs' }, + { value: 'var(--radio-group-font-size-sm)', scope: 'size:sm' }, + { value: 'var(--radio-group-font-size-lg)', scope: 'size:lg' }, + { value: 'var(--radio-group-font-size-xl)', scope: 'size:xl' } + ] + }, 'font-weight': 'var(--font-weight-medium)', 'line-height': 'var(--leading-ui)', 'item-label-fg': 'var(--color-content-primary)', 'disabled-item-label-fg': 'var(--color-content-disabled)', + 'invalid-item-label-fg': 'var(--color-threat-text)', + // NO key for the row's coarse-pointer floor. It was minted as + // `row-min-height-touch: var(--touch-target)` and the + // `recipe-css-contract` phantom guard rejected it in the act: + // `--touch-target` is not emitted by any generator, it is a `:root` + // constant hand-written in `archetypes.css` next to the coarse rules that + // consume it — "a hard ergonomic constant, the fingertip, NOT a theme + // value". recipe-contract §2 lists the touch target among the transversal + // systems a recipe CONSUMES and never mints, so the raw read in the CSS is + // the contract being obeyed; the census counts it `global` only because + // its SYSTEM list does not know the token yet (the pending-signature item + // of CONTINUE-theming, §«extender la clase system a las capas + // compartidas»). 'indicator-radius': 'var(--radius-full)', 'indicator-border-width': 'var(--border-width)', 'indicator-bg': 'var(--color-surface-default)', @@ -3914,6 +3963,15 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'color-mix( in srgb, var(--color-border-default) 72%, var(--color-border-subtle) )', 'checked-disabled-indicator-bg': 'color-mix( in srgb, var(--color-content-disabled) 40%, var(--color-surface-overlay) )', + // Invalid wins over the active colour across every variant: the item's + // border and the double focus ring (gap + ring) that marks it. Same three + // slots, same values as the twin `checkbox` — where a pair exists, the + // vocabulary is the pair's. Here the circle's chrome is named + // `indicator-*` (radio-group calls the OUTER circle the indicator and the + // inner dot `dot-*`), so the border key follows that, not checkbox's. + 'invalid-indicator-border': 'var(--color-threat-element)', + 'invalid-ring': 'var(--color-threat-element)', + 'invalid-ring-gap': 'var(--color-surface-default)', 'dot-fg': 'var(--color-content-on-solid)', 'disabled-dot-fg': 'color-mix( in srgb, var(--color-content-on-solid) 82%, var(--color-surface-default) )', @@ -3949,6 +4007,7 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'segmented-track-padding': 'var(--space-1)', 'segmented-track-radius': 'var(--radius-md)', 'segmented-segment-padding-inline': 'var(--space-3)', + 'segmented-segment-padding-block': 'var(--space-1)', 'segmented-segment-fg': 'var(--color-content-secondary)', 'hover-segmented-segment-fg': 'var(--color-content-primary)', 'segmented-segment-font-weight': 'var(--font-weight-medium)', diff --git a/web/routes/uix/components/radio-group/+page.svelte b/web/routes/uix/components/radio-group/+page.svelte index 32d8e0003..38c42c4f5 100644 --- a/web/routes/uix/components/radio-group/+page.svelte +++ b/web/routes/uix/components/radio-group/+page.svelte @@ -13,11 +13,12 @@ import { getActiveUix } from '$active-uix'; import { page } from '$app/state'; import { colorValue, PALETTE_SCALES } from '../../lib/harness.svelte'; + import TokensPanel from '../../lib/TokensPanel.svelte'; const uix = getActiveUix(); const params = page.url.searchParams; - type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y'; + type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y'; let tab = $state('live'); let value = $state('pro'); @@ -295,6 +296,9 @@ {events.length} + @@ -588,6 +592,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'recipe'}

Eidos recipe