diff --git a/docs/audit/theming/rating-group.md b/docs/audit/theming/rating-group.md index 01cb2e0ed..6faec9198 100644 --- a/docs/audit/theming/rating-group.md +++ b/docs/audit/theming/rating-group.md @@ -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: */` 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 `⚠`. -_(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. diff --git a/src/uix/eidos/components/rating-group/README.md b/src/uix/eidos/components/rating-group/README.md index 1d60580f4..319c3cb4a 100644 --- a/src/uix/eidos/components/rating-group/README.md +++ b/src/uix/eidos/components/rating-group/README.md @@ -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 | diff --git a/src/uix/eidos/components/rating-group/rating-group.css b/src/uix/eidos/components/rating-group/rating-group.css index efae1fb56..e6ec8cf3c 100644 --- a/src/uix/eidos/components/rating-group/rating-group.css +++ b/src/uix/eidos/components/rating-group/rating-group.css @@ -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; } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index f2657d8c1..7e08cc0f9 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -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); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index fb051e353..a92fa0722 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -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)', diff --git a/web/routes/uix/components/rating-group/+page.svelte b/web/routes/uix/components/rating-group/+page.svelte index da364363f..854fd61b0 100644 --- a/web/routes/uix/components/rating-group/+page.svelte +++ b/web/routes/uix/components/rating-group/+page.svelte @@ -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 @@ + @@ -498,6 +502,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'recipe'}

Eidos recipe