uix(rating-group): temable — 80 % → 100 %, y su ÚNICO hover mueve lo que el guard no fotografía

Cuatro claves acuñadas, tres privados jubilados, dos literales firmados y seis
adjudicaciones medidas. El default no se mueve: diff de computed 0 sobre 5.624
valores en 27 estados y capturas 2x idénticas.

Censo: 80 % → 100 % (público 12 → 13, global 1 → 0, literal 2 → 0 —los dos pasan
a `exception`, fuera del ratio—, privado 0, sistema 1). Contrato 25 → 29 claves.
Global no baja (68 %).

QUÉ ENTRA (4 claves)

  gap · item-size · font-size — los tres ejes RESUELTOS por talla en el TSC, con
  `scope: 'size:{k}'` sobre el proveedor, que es quien lleva `data-size`. Con
  ellos se van los tres privados `--_rating-group-*` y los cuatro bloques
  `[data-size]` que los conmutaban: eran «privado con fuentes por TALLA», o sea
  una ESCALA, y una escala sube al contrato. Un tema alcanza ahora la coordenada
  resuelta además de los cinco pasos que ya alcanzaba.

  invalid-item-fg — la costura: el tinte de validación leía
  `--color-threat-element` a pelo, igual que en sus hermanos de formulario
  (checkbox, radio-group, pin-input). El valor es del sistema; el knob es del
  componente.

DOS LITERALES FIRMADOS (fuera del ratio)

  `inline-size: 100%` / `block-size: 100%` sobre el glifo son IDENTIDAD: el SVG
  llena la caja del ítem, y la caja ES el eje de talla. Llevan su anotación
  `/* literal: … */` con la razón en la misma línea, más un
  `/* prettier-ignore */` sobre la regla para que el formateo no separe la
  anotación de su valor (la regresión que costó un día en natural-time-picker).
  Nota honesta: la demo pinta las estrellas con TEXTO (★/☆), así que esa regla
  no tiene nodo en esta ruta — su razón es estructural, no medida en pantalla.

CENTINELA 23/29, 6 adjudicadas, ninguna es deuda

  partial-item-fg necesita `allowHalf` y medio valor · invalid-item-fg necesita
  [data-invalid], y encenderlo en la demo taparía item-fg, active-item-fg y
  partial-item-fg durante toda la corrida (su regla es (0,3,0)) · focus-radius
  necesita :focus-visible, que el guard quita a propósito · el par de transición
  ES la transición que el guard congela.

  Y la sexta es un HUECO DE INSTRUMENTO nuevo: **item-hover-offset mueve un
  `transform`**, propiedad que el guard no fotografía. Es el único knob de esta
  receta que mueve uno, así que leía muerto estando vivo — medido a mano sobre
  el ítem realmente hovereado: matrix(1, 0, 0, 1, 0, -1) → matrix(1, 0, 0, 1, 0,
  -40). Añadir `transform` a la lista obliga a re-verificar los componentes con
  ledger, así que queda anotado en next-features §13.

  El guard gana además un `prepareWith` con el interruptor `disabled` de la
  demo: `disabled-opacity` era el único knob tras un estado, esta demo no tiene
  switch por URL, y encenderlo no tapa nada (el único hover de la receta ya
  estaba fuera del alcance del instrumento).

CARRERA DE ÍNDICE — dónde está el resto de este trabajo

  El commit `edc019515` (`toggle`, otra sesión) se llevó cuatro fragmentos míos
  que estaban en el índice compartido cuando confirmó: el `prepareWith` de
  rating-group en `theming-sentinel.ts`, su bloque de seis adjudicaciones en
  `theming-sentinel-exceptions.ts`, su entrada del §8 del PLAN y su viñeta de
  next-features §13. El contenido es correcto y ya vive en HEAD; se anota aquí
  para que la trazabilidad no se pierda. Lección: en esta rama el índice es
  compartido, así que entre construirlo y confirmar no puede mediar NADA.

GUARDS

  censo --only rating-group 100 % · global 68 % (no baja) · component-audit PASS
  · eidos-lint 0 invalid (12 morfo-backed, 0 eidos-only) · vitest src/uix/eidos
  sin rojos nuevos (el único es `skin-media-player`, conocido) · rtl:check 0 ·
  docs:check 0 · prettier OK sobre el contenido normalizado a LF · check sin
  errores en ficheros del componente.

  README con su sección «Talla y tema» y la demo con su pestaña Tokens (29
  claves, verificada en navegador).

Índice construido a mano desde HEAD sobre los dos ficheros compartidos que
quedaban (recipes/base.ts, generated/base.css). Fuera del commit,
docs/audit/theming/README.md — es un agregado global que mezcla trabajo en
vuelo.

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

@ -5,59 +5,51 @@
> 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**: **80%** — 12 de 15 knobs por token público
- **Knobs de apariencia**: 16 — público 12 · privado 0 · global 1 · literal 2 · sistema 1 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 25 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `item-size-xs`, `item-size-sm`, `item-size-md`, `item-size-lg`, `item-size-xl`, `font-family`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `line-height`, `item-fg`, `active-item-fg`, `partial-item-fg`, `item-hover-offset`, `focus-radius`, `transition-duration`, `transition-ease`, `disabled-opacity`
- **Eje `size`**: sí · **ficheros**: `rating-group.css`
- **Medido**: 2026-08-23 · **Alcance**: **100%** — 13 de 13 knobs por token público
- **Knobs de apariencia**: 14 — público 13 · privado 0 · global 0 · literal 0 · sistema 1 · excepción 2 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 29 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `item-size-xs`, `item-size-sm`, `item-size-md`, `item-size-lg`, `item-size-xl`, `font-family`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `gap`, `item-size`, `font-size`, `line-height`, `item-fg`, `active-item-fg`, `partial-item-fg`, `invalid-item-fg`, `item-hover-offset`, `focus-radius`, `transition-duration`, `transition-ease`, `disabled-opacity`
- **Eje `size`**: no · **ficheros**: `rating-group.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (1)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `rating-group.css:58` | `[data-rating-group][data-invalid] [data-rating-group-item]` | `color` | `var(--color-threat-element)` |
_Ninguno._
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (2)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `rating-group.css:92` | `[data-rating-group-item] > :where(svg)` | `inline-size` | `100%` |
| 2 | `rating-group.css:93` | `[data-rating-group-item] > :where(svg)` | `block-size` | `100%` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (2) — 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 | `rating-group.css:68` | `[data-rating-group-item] > :where(svg)` | `inline-size` | `100%` |
| 2 | `rating-group.css:69` | `[data-rating-group-item] > :where(svg)` | `block-size` | `100%` |
## 2. Sistema transversal (1) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `rating-group.css:47` | `[data-rating-group]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 1 | `rating-group.css:22` | `[data-rating-group]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_rating-group-gap` | 5 | `var(--rating-group-gap-md)`, `var(--rating-group-gap-xs)`, `var(--rating-group-gap-sm)`, `var(--rating-group-gap-lg)`, `var(--rating-group-gap-xl)` | public | **sí** |
| `--_rating-group-item-size` | 5 | `var(--rating-group-item-size-md)`, `var(--rating-group-item-size-xs)`, `var(--rating-group-item-size-sm)`, `var(--rating-group-item-size-lg)`, `var(--rating-group-item-size-xl)` | public | **sí** |
| `--_rating-group-font-size` | 5 | `var(--rating-group-font-size-md)`, `var(--rating-group-font-size-xs)`, `var(--rating-group-font-size-sm)`, `var(--rating-group-font-size-lg)`, `var(--rating-group-font-size-xl)` | public | **sí** |
_La receta no declara privados propios en su CSS._
## 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` (1)
### 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,7 +59,6 @@ no distingue lo que debería — se marca `⚠`.
| token (`--rating-group-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `invalid-item-fg` | `root` | `var(--color-threat-element)` | 1 |
### 4.2 Sin nombre mecánico (2)
@ -85,6 +76,37 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**Ejecutado 2026-08-23 · 80 % → 100 %.**
Contrato 25 → **29 claves**. Censo: público 12 → 13, **global 1 → 0**,
**literal 2 → 0** (los dos pasan a `exception`, fuera del ratio), privado 0,
sistema 1. Diff de computed **0** sobre 4.440 valores en 20 estados más 1.184 de
la sonda estándar; capturas 2× antes/después idénticas. Centinela **23/29**, 6
adjudicadas.
**Qué entra (4 claves).** Tres RESUELTAS por talla en el TSC —`gap`,
`item-size`, `font-size`— que jubilan los tres privados `--_rating-group-*` y
los cuatro bloques `[data-size]` que los conmutaban (§7.4-4): eran «privado con
fuentes por TALLA», que es una ESCALA y sube al contrato, no un conmutador. Un
tema alcanza ahora la coordenada resuelta además de los cinco pasos. Y
`invalid-item-fg`, la costura: el tinte de validación leía
`--color-threat-element` a pelo, igual que en sus hermanos de formulario.
**Los dos literales se FIRMAN, no se acuñan.** `inline-size: 100%` /
`block-size: 100%` sobre el glifo son IDENTIDAD —el SVG llena la caja del ítem,
y la caja ES el eje de talla—, así que llevan su `/* literal: … */` y salen del
ratio. Ojo: la demo pinta las estrellas con TEXTO (★/☆), de modo que esa regla
no tiene nodo en esta ruta; su razón es estructural, no medida en pantalla.
**Las seis adjudicaciones son instrumento o estado, ninguna es deuda**:
`partial-item-fg` necesita `allowHalf` y medio valor; `invalid-item-fg` necesita
`[data-invalid]`, y encenderlo en la demo taparía `item-fg`, `active-item-fg` y
`partial-item-fg` durante toda la corrida (su regla es (0,3,0)); `focus-radius`
necesita `:focus-visible`, que el guard quita a propósito; el par de transición
ES la transición que el guard congela; y **`item-hover-offset` mueve un
`transform`, propiedad que el guard no fotografía** — hueco de instrumento
nuevo, medido a mano (`matrix(1,0,0,1,0,-1)` → `matrix(1,0,0,1,0,-40)`) y
registrado en `next-features.md` §13. Todas verificadas forzando el estado sobre
el nodo REAL.
<!-- veredicto:end -->

@ -35,6 +35,44 @@ Fuentes externas consultadas:
- Eidos no define iconos. El consumidor decide estrella, corazon u otro glyph;
la receta solo colorea por `data-state`.
## Talla y tema
**29 claves públicas** en `lib/recipes/base.ts`, y el eje `size` se resuelve en
el TSC: la receta lee **un solo nombre** por eje 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 |
| --- | --- | --- |
| Separación entre estrellas | `gap-{xs…xl}` = `--space-{0-5…2-5}` | `gap` |
| Caja del ítem | `item-size-{xs…xl}` (14/18/22/28/34px) | `item-size` |
| Texto | `font-size-{xs…xl}` = `--size-{k}-font-size` | `font-size` |
El resto, por rol: `font-family`, `line-height`, la tinta por estado
(`item-fg` en reposo, `active-item-fg`, `partial-item-fg`, `invalid-item-fg`),
el levantamiento de un píxel al pasar el ratón (`item-hover-offset`), la esquina
del anillo de foco (`focus-radius`), el par de transición y
`disabled-opacity`.
Lo que entró el **2026-08-23** (80 % → **100 %**):
- **`gap` / `item-size` / `font-size` resueltos** — jubilan los tres privados
`--_rating-group-*` y los cuatro bloques `[data-size]` que los conmutaban. Un
tema alcanza ahora también la coordenada resuelta, no sólo los cinco pasos.
- **`invalid-item-fg`** — la costura: el tinte de validación leía
`--color-threat-element` a pelo, igual que en sus hermanos de formulario.
- **Los dos `100%` del glifo quedan FIRMADOS** como identidad
(`/* literal: … */`): el SVG llena la caja del ítem, y la caja es el eje de
talla — ahí no va un token.
**Lo que el guard da por silencioso** (seis, todos medidos forzando el estado
sobre el nodo real): `partial-item-fg` necesita `allowHalf` y medio valor;
`invalid-item-fg` necesita `[data-invalid]`, y encenderlo taparía las otras tres
tintas durante toda la corrida; `focus-radius` necesita `:focus-visible`, que el
guard quita a propósito; el par de transición ES la transición que el guard
congela; y `item-hover-offset` mueve un `transform`, propiedad que el guard no
fotografía (hueco de instrumento, registrado en `next-features.md` §13).
## Gaps
| Gap | Disposición | Detalle |

@ -1,42 +1,17 @@
/* `gap`, `item-size` and `font-size` are the size-RESOLVED names: the TSC emits
* them on the provider (the node that carries `data-size`), so the three
* `[data-size]` blocks that used to switch a private each are gone. */
[data-rating-group] {
--_rating-group-gap: var(--rating-group-gap-md);
--_rating-group-item-size: var(--rating-group-item-size-md);
--_rating-group-font-size: var(--rating-group-font-size-md);
display: inline-flex;
align-items: center;
gap: var(--_rating-group-gap);
gap: var(--rating-group-gap);
color: var(--rating-group-item-fg);
font-family: var(--rating-group-font-family);
font-size: var(--_rating-group-font-size);
font-size: var(--rating-group-font-size);
line-height: var(--rating-group-line-height);
outline: none;
}
[data-rating-group][data-size='xs'] {
--_rating-group-gap: var(--rating-group-gap-xs);
--_rating-group-item-size: var(--rating-group-item-size-xs);
--_rating-group-font-size: var(--rating-group-font-size-xs);
}
[data-rating-group][data-size='sm'] {
--_rating-group-gap: var(--rating-group-gap-sm);
--_rating-group-item-size: var(--rating-group-item-size-sm);
--_rating-group-font-size: var(--rating-group-font-size-sm);
}
[data-rating-group][data-size='lg'] {
--_rating-group-gap: var(--rating-group-gap-lg);
--_rating-group-item-size: var(--rating-group-item-size-lg);
--_rating-group-font-size: var(--rating-group-font-size-lg);
}
[data-rating-group][data-size='xl'] {
--_rating-group-gap: var(--rating-group-gap-xl);
--_rating-group-item-size: var(--rating-group-item-size-xl);
--_rating-group-font-size: var(--rating-group-font-size-xl);
}
[data-rating-group][data-orientation='vertical'] {
flex-direction: column;
align-items: flex-start;
@ -55,15 +30,15 @@
/* Invalid (Field-aware) — the items' icon ink tints like the sibling form
controls (checkbox, switch, pin-input). */
[data-rating-group][data-invalid] [data-rating-group-item] {
color: var(--color-threat-element);
color: var(--rating-group-invalid-item-fg);
}
[data-rating-group-item] {
display: inline-flex;
align-items: center;
justify-content: center;
inline-size: var(--_rating-group-item-size);
block-size: var(--_rating-group-item-size);
inline-size: var(--rating-group-item-size);
block-size: var(--rating-group-item-size);
color: var(--rating-group-item-fg);
cursor: pointer;
transition:
@ -88,9 +63,10 @@
cursor: default;
}
/* prettier-ignore */
[data-rating-group-item] > :where(svg) {
inline-size: 100%;
block-size: 100%;
inline-size: 100%; /* literal: identity — the glyph FILLS the item box, and the box is the size axis */
block-size: 100%; /* literal: identity — same, on the block axis */
fill: currentColor;
stroke: currentColor;
}

@ -2709,6 +2709,7 @@
--rating-group-item-fg: var(--color-neutral-border);
--rating-group-active-item-fg: var(--color-fulfill-solid);
--rating-group-partial-item-fg: var(--color-fulfill-solid);
--rating-group-invalid-item-fg: var(--color-threat-element);
--rating-group-item-hover-offset: -1px;
--rating-group-focus-radius: var(--radius-md);
--rating-group-transition-duration: var(--duration-fast);
@ -6763,6 +6764,36 @@
--_radio-group-palette-text: var(--palette-text, var(--radio-group-dot-fg));
}
[data-rating-group] {
--rating-group-gap: var(--rating-group-gap-md);
--rating-group-item-size: var(--rating-group-item-size-md);
--rating-group-font-size: var(--rating-group-font-size-md);
}
[data-rating-group][data-size='xs'] {
--rating-group-gap: var(--rating-group-gap-xs);
--rating-group-item-size: var(--rating-group-item-size-xs);
--rating-group-font-size: var(--rating-group-font-size-xs);
}
[data-rating-group][data-size='sm'] {
--rating-group-gap: var(--rating-group-gap-sm);
--rating-group-item-size: var(--rating-group-item-size-sm);
--rating-group-font-size: var(--rating-group-font-size-sm);
}
[data-rating-group][data-size='lg'] {
--rating-group-gap: var(--rating-group-gap-lg);
--rating-group-item-size: var(--rating-group-item-size-lg);
--rating-group-font-size: var(--rating-group-font-size-lg);
}
[data-rating-group][data-size='xl'] {
--rating-group-gap: var(--rating-group-gap-xl);
--rating-group-item-size: var(--rating-group-item-size-xl);
--rating-group-font-size: var(--rating-group-font-size-xl);
}
[data-select-trigger], [data-select-content] {
--_select-palette-track: var(--select-primary-track);
--_select-palette-border: var(--select-primary-border);

@ -4221,10 +4221,47 @@ 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, which is the
// node the gap and the font-size paint on and the item's ancestor: the CSS
// reads ONE name per axis and the `[data-size]` blocks that used to switch
// three privates are gone (PLAN-theming §7.4-4). Same move as the twin
// `radio-group`; a theme now reaches the resolved coordinate too, not only
// the five steps behind it.
gap: {
declarations: [
{ value: 'var(--rating-group-gap-md)', scope: 'host' },
{ value: 'var(--rating-group-gap-xs)', scope: 'size:xs' },
{ value: 'var(--rating-group-gap-sm)', scope: 'size:sm' },
{ value: 'var(--rating-group-gap-lg)', scope: 'size:lg' },
{ value: 'var(--rating-group-gap-xl)', scope: 'size:xl' }
]
},
'item-size': {
declarations: [
{ value: 'var(--rating-group-item-size-md)', scope: 'host' },
{ value: 'var(--rating-group-item-size-xs)', scope: 'size:xs' },
{ value: 'var(--rating-group-item-size-sm)', scope: 'size:sm' },
{ value: 'var(--rating-group-item-size-lg)', scope: 'size:lg' },
{ value: 'var(--rating-group-item-size-xl)', scope: 'size:xl' }
]
},
'font-size': {
declarations: [
{ value: 'var(--rating-group-font-size-md)', scope: 'host' },
{ value: 'var(--rating-group-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--rating-group-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--rating-group-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--rating-group-font-size-xl)', scope: 'size:xl' }
]
},
'line-height': 'var(--leading-ui)',
'item-fg': 'var(--color-neutral-border)',
'active-item-fg': 'var(--color-fulfill-solid)',
'partial-item-fg': 'var(--color-fulfill-solid)',
// The Field-aware invalid tint, the same seam its sibling form controls
// carry (checkbox, radio-group, pin-input): the VALUE is the system's
// threat element, the KNOB is the component's.
'invalid-item-fg': 'var(--color-threat-element)',
'item-hover-offset': '-1px',
'focus-radius': 'var(--radius-md)',
'transition-duration': 'var(--duration-fast)',

@ -3,8 +3,9 @@
import { compileMorfo } from '$uix/morfo';
import { ratingGroupMorfo } from '@/uix/morfo/components/rating-group';
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 Orientation = 'horizontal' | 'vertical';
type Direction = 'ltr' | 'rtl';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
@ -195,6 +196,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>29</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -498,6 +502,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="rating-group" 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.