uix(progress): temable — 69 % → 87 %, y el instrumento medía UNA de sesenta caras

Dos claves acuñadas, seis literales FIRMADOS y cuatro knobs adjudicados por
escrito. El default no se mueve: diff de computed 0 sobre 60 configuraciones y
capturas 2x idénticas byte a byte.

Censo: 69 % → 87 % (35 knobs → 30; público 24 → 26, global 3 → 2, literal 6 → 0,
excepción firmada 0 → 6). Contrato 41 → 43 claves. Global 67 % (no baja).

QUÉ ENTRA (2 claves)

  row-gap (--space-2) + column-gap (--space-3). Era el ÚNICO knob de apariencia
  que iba a un primitivo a pelo, y se parte en dos porque el atajo llevaba DOS
  valores distintos: el hueco de fila separa la línea de texto de la barra, el
  de columna separa el rótulo del valor. Un gap único habría nombrado un knob
  para dos. El catálogo tiene las dos palabras (row-gap x10, column-gap x4) y el
  molde exacto es chart.legend-row-gap / -column-gap.

QUÉ SE FIRMA EN VEZ DE ACUÑARSE (6 literales, fuera del ratio)

  Cuatro 100 % y dos max-content: la geometría de la barra, no una decisión de
  tema — llena la fila que le dan, o se encoge a su propio grosor / diámetro.
  Cada uno con su anotación en la declaración (válvula de recipe-contract §3).

QUÉ SE RECHAZA DE LA PROPUESTA §4 (dos de sus tres filas)

  indicator-radius — el --radius-full del anillo. Su GEMELO meter ya dejó el
  veredicto escrito EN EL CSS: es lo que hace circular a shape='circular', y un
  token dejaría que un tema des-redondease una forma que el consumidor pidió por
  su nombre. Identidad, no knob; se copia el comentario del gemelo. Son los dos
  únicos global que quedan, y el censo no puede sacarlos del ratio: la válvula de
  la anotación sólo reclasifica la clase literal, nunca un global (mismo techo
  que meter).

  width: max-content — la §4 fundía en UN nombre dos nodos distintos (la raíz
  vertical y la raíz circular) y los nombraba por el primer selector. Es el
  literal de identidad de arriba.

EL TECHO HONESTO ES 87 %, NO 100 %

  Los dos knobs private que quedan son el CANAL DE VALOR:
  --_progress-value-pct lo escribe soma en el estilo inline del provider en cada
  render (0-100 saturado) y la receta lo lee para el ancho del relleno lineal y
  el alto del vertical. Un público encima mentiría — un tema no puede fijar el
  progreso de una tarea. Misma clase que el z-index inline de drag-drop.

GUARD R-5.4: 40/43, TRES ADJUDICADOS CON SU MEDIDA

  indicator-bg-loaded — sólo pinta bajo [data-state='loaded'], y la corrida
  mantiene indeterminate encendido para alcanzar los tokens del barrido; forzado
  sobre el nodo real: oklch(0.6406 0.1329 157.68) -> rgb(1,2,3).
  transition-duration / -ease — los congela el guard por diseño; pasada sin
  congelar: 0.18s -> 11.5s y cubic-bezier(0.4, 0, 0.2, 1) -> steps(7).

EL INSTRUMENTO MEDÍA UNA DE SESENTA CARAS

  La sonda estándar ve 4 nodos — que son TODAS las partes del morfo — pero una
  sola configuración: lineal, horizontal, loading, md. Medio contrato (el anillo,
  el eje vertical, el barrido indeterminado, la tinta de completado) queda fuera
  del diff, y el gate pasa en verde sin haberlo mirado. El guard lo dijo antes:
  22 de 41 en la página por defecto. Con sweepAttr data-shape — el interruptor
  que su gemelo meter YA tenía — sube a 34; con el interruptor indeterminate de
  la demo, a 37; con el chip vertical, a 38. Cada control se comprobó por
  separado, antes y después, porque montar más puede medir menos: aquí ninguno
  tapó nada.

  Y una trampa NUEVA, medida aquí: una sonda que CACHEA los nodos y luego mide en
  bucle da 8.352 falsos diffs si el HMR reemplaza el subárbol a mitad de corrida.
  getComputedStyle sobre un nodo DESACOPLADO devuelve la cadena vacía en TODAS
  las propiedades, y el diff lee cada vacío como un cambio. Se arregla
  re-consultando los nodos en cada instantánea, y se detecta haciendo fallar la
  sonda ante un computed vacío. Pariente de hmr-stale-tab-phantom-findings.

ARTEFACTOS

  Sonda estándar: 4 nodos, 800 valores x 7 estados -> 0 diffs.
  Sonda de variantes (2 formas x 2 orientaciones x 3 estados x 5 tallas, elemento
  y ::before): 17.280 valores -> 0 diffs.
  Capturas 2x antes/después, lineal y circular: idénticas byte a byte.
  Censo --only progress 87 % · component:audit PASS 1/1 · eidos-lint 0 invalid
  (13 morfo-backed, 12 eidos-only) · vitest src/uix/eidos 439/440 (el único rojo
  es el conocido skin-media-player en lint.test.ts) · rtl:check 0 · docs:check 0
  · npm run check sin errores atribuibles a progress · prettier limpio en lo
  tocado (progress.css estaba limpio en HEAD y sigue limpio; los dos ficheros del
  centinela ya venían sucios de antes y mis líneas no añaden nada).

  README de eidos con su sección «Talla y tema» y pestaña Tokens en la demo,
  verificada en el navegador: 43 filas resueltas en vivo, 0 errores de consola.

NOTA DE ÁRBOL COMPARTIDO

  Otra sesión trabaja sobre form en los mismos ficheros (base.ts,
  generated/base.css, los dos del centinela y el índice de fichas). Lo indexado
  se construyó a mano desde HEAD + sólo mis hunks, así que el commit no arrastra
  su trabajo en curso; docs/audit/theming/README.md lleva sólo el delta de
  progress y conserva la fila de form tal como está en HEAD.

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

@ -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**: **67%** — 3047 de 4577 knobs pasan por un token público del componente
- **Reparto**: público 3047 · privado 317 · global 814 · literal 399 · sistema transversal 509 · excepción firmada 132 _(los dos últimos, fuera del ratio)_
- **Alcance global**: **67%** — 3049 de 4572 knobs pasan por un token público del componente
- **Reparto**: público 3049 · privado 317 · global 813 · literal 393 · sistema transversal 509 · excepción firmada 138 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 28 · **con eje `size`**: 57
## Cómo se lee
@ -147,7 +147,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [virtual-list](./virtual-list.md) | 67% | 13 | 8 | 0 | 0 | 4 | 1 | 12 | – |
| [separator](./separator.md) | 67% | 6 | 4 | 0 | 0 | 2 | 0 | 3 | – |
| [listbox](./listbox.md) | 68% | 37 | 23 | 9 | 1 | 1 | 3 | 21 | – |
| [progress](./progress.md) | 69% | 35 | 24 | 2 | 3 | 6 | 0 | 41 | y |
| [color-swatch](./color-swatch.md) | 70% | 10 | 7 | 0 | 3 | 0 | 0 | 12 | y |
| [s-text-virtual-list](./s-text-virtual-list.md) | 70% | 10 | 7 | 0 | 3 | 0 | 0 | 9 | – |
| [form](./form.md) | 71% | 52 | 36 | 8 | 5 | 2 | 1 | 54 | y |
@ -191,6 +190,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [waveform](./waveform.md) | 86% | 7 | 6 | 0 | 1 | 0 | 0 | 11 | y |
| [table](./table.md) | 86% | 46 | 37 | 3 | 0 | 3 | 3 | 45 | – |
| [context-menu](./context-menu.md) | 87% | 33 | 26 | 0 | 1 | 3 | 3 | 28 | – |
| [progress](./progress.md) | 87% | 30 | 26 | 2 | 2 | 0 | 0 | 43 | y |
| [kbd](./kbd.md) | 87% | 15 | 13 | 0 | 1 | 1 | 0 | 13 | – |
| [fab](./fab.md) | 88% | 16 | 14 | 0 | 2 | 0 | 0 | 10 | – |
| [announce](./announce.md) | 88% | 8 | 7 | 0 | 1 | 0 | 0 | 9 | – |

@ -5,47 +5,46 @@
> 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**: **69%** — 24 de 35 knobs por token público
- **Knobs de apariencia**: 35 — público 24 · privado 2 · global 3 · literal 6 · sistema 0 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 41 pública(s) — `height-xs`, `height-sm`, `height-md`, `height-lg`, `height-xl`, `vertical-height`, `radius-xs`, `radius-sm`, `radius-md`, `radius-lg`, `radius-xl`, `ring-size-xs`, `ring-size-sm`, `ring-size-md`, `ring-size-lg`, `ring-size-xl`, `ring-thickness-xs`, `ring-thickness-sm`, `ring-thickness-md`, `ring-thickness-lg`, `ring-thickness-xl`, `label-font-family`, `label-font-size`, `label-font-weight`, `label-line-height`, `label-fg`, `value-text-font-family`, `value-text-font-size`, `value-text-font-weight`, `value-text-line-height`, `value-text-fg`, `track-bg`, `ring-track-bg`, `ring-center-bg`, `indicator-bg`, `indicator-bg-loaded`, `transition-duration`, `transition-ease`, `indeterminate-width`, `indeterminate-duration`, `indeterminate-ease`
- **Medido**: 2026-08-23 · **Alcance**: **87%** — 26 de 30 knobs por token público
- **Knobs de apariencia**: 30 — público 26 · privado 2 · global 2 · literal 0 · sistema 0 · excepción 6 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 43 pública(s) — `height-xs`, `height-sm`, `height-md`, `height-lg`, `height-xl`, `vertical-height`, `row-gap`, `column-gap`, `radius-xs`, `radius-sm`, `radius-md`, `radius-lg`, `radius-xl`, `ring-size-xs`, `ring-size-sm`, `ring-size-md`, `ring-size-lg`, `ring-size-xl`, `ring-thickness-xs`, `ring-thickness-sm`, `ring-thickness-md`, `ring-thickness-lg`, `ring-thickness-xl`, `label-font-family`, `label-font-size`, `label-font-weight`, `label-line-height`, `label-fg`, `value-text-font-family`, `value-text-font-size`, `value-text-font-weight`, `value-text-line-height`, `value-text-fg`, `track-bg`, `ring-track-bg`, `ring-center-bg`, `indicator-bg`, `indicator-bg-loaded`, `transition-duration`, `transition-ease`, `indeterminate-width`, `indeterminate-duration`, `indeterminate-ease`
- **Eje `size`**: sí · **ficheros**: `progress.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (3)
### 1.1 Directo a primitivo global (2)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `progress.css:11` | `[data-progress]` | `gap` | `var(--space-2) var(--space-3)` |
| 2 | `progress.css:122` | `[data-progress][data-shape='circular'] [data-progress-indicator]` | `border-radius` | `var(--radius-full)` |
| 3 | `progress.css:134` | `[data-progress][data-shape='circular'] [data-progress-indicator]::before` | `border-radius` | `var(--radius-full)` |
| 1 | `progress.css:127` | `[data-progress][data-shape='circular'] [data-progress-indicator]` | `border-radius` | `var(--radius-full)` |
| 2 | `progress.css:139` | `[data-progress][data-shape='circular'] [data-progress-indicator]::before` | `border-radius` | `var(--radius-full)` |
### 1.2 A través de un privado (2)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `progress.css:66` | `[data-progress-indicator]::before` | `inline-size` | `calc(var(--_progress-value-pct, 0) * 1%)` |
| 2 | `progress.css:82` | `[data-progress][data-orientation='vertical'] [data-progress-indicator]::before` | `block-size` | `calc(var(--_progress-value-pct, 0) * 1%)` |
| 1 | `progress.css:67` | `[data-progress-indicator]::before` | `inline-size` | `calc(var(--_progress-value-pct, 0) * 1%)` |
| 2 | `progress.css:83` | `[data-progress][data-orientation='vertical'] [data-progress-indicator]::before` | `block-size` | `calc(var(--_progress-value-pct, 0) * 1%)` |
### 1.3 Literales (6)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `progress.css:12` | `[data-progress]` | `inline-size` | `100%` |
| 2 | `progress.css:44` | `[data-progress][data-orientation='vertical']` | `inline-size` | `max-content` |
| 3 | `progress.css:54` | `[data-progress-indicator]` | `inline-size` | `100%` |
| 4 | `progress.css:81` | `[data-progress][data-orientation='vertical'] [data-progress-indicator]::before` | `inline-size` | `100%` |
| 5 | `progress.css:102` | `[data-progress][data-orientation='vertical'][data-state='indeterminate'] [data-progress-indicator]::before` | `inline-size` | `100%` |
| 6 | `progress.css:107` | `[data-progress][data-shape='circular']` | `inline-size` | `max-content` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (6) — 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 | `progress.css:13` | `[data-progress]` | `inline-size` | `100%` |
| 2 | `progress.css:45` | `[data-progress][data-orientation='vertical']` | `inline-size` | `max-content` |
| 3 | `progress.css:55` | `[data-progress-indicator]` | `inline-size` | `100%` |
| 4 | `progress.css:82` | `[data-progress][data-orientation='vertical'] [data-progress-indicator]::before` | `inline-size` | `100%` |
| 5 | `progress.css:103` | `[data-progress][data-orientation='vertical'][data-state='indeterminate'] [data-progress-indicator]::before` | `inline-size` | `100%` |
| 6 | `progress.css:108` | `[data-progress][data-shape='circular']` | `inline-size` | `max-content` |
## 2. Sistema transversal (0) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
@ -68,7 +67,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl
- **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` (3)
### 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
@ -80,7 +79,6 @@ no distingue lo que debería — se marca `⚠`.
| --- | --- | --- | ---: |
| `width` | `root` | `max-content` | 2 |
| `indicator-radius` | `root` | `var(--radius-full)` | 2 |
| `gap` | `root` | `var(--space-2) var(--space-3)` | 1 |
### 4.2 Sin nombre mecánico (6)
@ -99,6 +97,74 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**Ejecutado 2026-08-23 — 69 % → 87 %.** Dos claves acuñadas, seis literales
firmados, cuatro knobs adjudicados. Diff de computed **0** sobre 60
configuraciones (2 formas × 2 orientaciones × 3 estados × 5 tallas) y capturas
2× **idénticas byte a byte**. Guard R-5.4: **40 de 43** tokens mueven un
computed; los tres callados, medidos y escritos en el ledger.
**Lo que entra (2 claves, el ÚNICO knob que iba a un primitivo a pelo):**
- `row-gap` (`--space-2`) y `column-gap` (`--space-3`). Eran un `gap` en
atajo de DOS valores distintos, y por eso se parte: el hueco de fila separa
la línea de texto de la barra, el de columna separa el rótulo del valor. Un
`gap` único habría nombrado un knob para dos. El catálogo tiene las dos
palabras (`row-gap` ×10, `column-gap` ×4) y el molde exacto es
`chart.legend-row-gap` / `-column-gap`.
**Lo que se FIRMA en vez de acuñarse (6 literales, fuera del ratio):** los
cuatro `100%` y los dos `max-content` son la geometría de la barra, no una
decisión de tema — llena la fila que le dan (`100%`) o se encoge a su propio
grosor / diámetro (`max-content`). Llevan su `/* literal: <razón> */` en la
declaración; válvula de recipe-contract §3.
**Lo que se RECHAZA de la propuesta §4 (dos de las tres filas):**
1. `indicator-radius` — el `--radius-full` del anillo. Su gemelo `meter` ya
dejó el veredicto escrito EN EL CSS: es lo que hace circular a
`shape='circular'`, y un token dejaría que un tema des-redondease una forma
que el consumidor pidió POR SU NOMBRE. **Identidad, no knob.** Se copia el
comentario del gemelo. Son los dos únicos `global` que quedan, y el censo no
puede sacarlos del ratio: la válvula `/* literal: */` sólo reclasifica la
clase `literal`, nunca un `global` (mismo techo que `meter`).
2. `width: max-content` — la §4 fundía en UN nombre dos nodos distintos (la
raíz vertical y la raíz circular) y además lo llamaba por el primer selector,
no por lo que pinta. Es el literal de identidad de arriba.
**Los dos knobs `private` que quedan son el CANAL DE VALOR, no deuda.**
`--_progress-value-pct` lo escribe **soma** en el estilo inline del provider en
cada render (`progress-provider.svelte.ts`, 0–100 saturado); la receta lo lee
para el ancho del relleno lineal y el alto del vertical. Un público encima
mentiría: un tema no puede fijar el progreso de una tarea. Es la misma clase que
el `z-index` inline de `drag-drop` y el gap de `carousel`. Con ellos y con los
dos `--radius-full` de identidad, **el techo honesto de este componente es
87 %**, no el 100 %.
**Lo que el guard da por callado (3 de 43, todos medidos sobre el nodo real):**
- `indicator-bg-loaded` — sólo pinta bajo `[data-state='loaded']`, y la corrida
mantiene `indeterminate` encendido para alcanzar los tokens del barrido:
forzado → `oklch(0.6406 0.1329 157.68)` → `rgb(1,2,3)`.
- `transition-duration` / `transition-ease` — los congela el propio guard por
diseño; pasada SIN congelar → `0.18s` → `11.5s` y
`cubic-bezier(0.4, 0, 0.2, 1)` → `steps(7)`. Mismo par que en `meter`.
**Lo que enseñó el instrumento.** La sonda estándar mide 4 nodos — que son
TODAS las partes del morfo — pero una sola configuración: lineal, horizontal,
`loading`, `md`. Medio contrato (el anillo, el eje vertical, el barrido
indeterminado, la tinta de completado) no entra en ese diff, y el gate habría
pasado en verde sin haberlo mirado. El guard lo dijo antes: **22 de 41** en la
página por defecto. Con `sweepAttr: data-shape` (el interruptor que su gemelo
`meter` ya tenía) sube a 34; encendiendo el interruptor `indeterminate` de la
demo, a 37; y el chip `vertical`, a 38 — **cada control comprobado por separado,
antes y después, porque montar más puede medir menos**. Aquí no tapó ninguno.
**Y una trampa nueva del instrumento, medida aquí**: una sonda que CACHEA los
nodos y luego mide en bucle da 8.352 falsos diffs si el HMR reemplaza el
subárbol a mitad de corrida — `getComputedStyle` sobre un nodo DESACOPLADO
devuelve la cadena VACÍA en todas las propiedades, y el diff lee cada vacío como
un cambio. Se arregla re-consultando los nodos en cada instantánea, y se detecta
haciendo fallar la sonda ante un computed vacío. Es pariente de
`hmr-stale-tab-phantom-findings`, con otra cara.
<!-- veredicto:end -->

@ -515,6 +515,22 @@ commits de corrección.
animaciones (ningún commit toca `animation`); contrato sin duplicados ni
huérfanos.
- 2026-08-23 — **`progress` 69 % → 87 %** (COMMIT), 2 claves. `row-gap` +
`column-gap`: el `gap` en atajo tenía DOS valores distintos, así que es un knob
por eje (molde `chart.legend-row-gap`). **Seis literales de identidad
firmados** (cuatro `100%`, dos `max-content`: llena la fila que le dan o se
encoge a su propio grosor). **Rechazadas dos de las tres filas de la §4**: el
`--radius-full` del anillo es lo que HACE circular a `shape='circular'` — su
gemelo `meter` ya lo dejó escrito en el CSS, y se copia el comentario. Los dos
`private` que quedan son el CANAL DE VALOR (`--_progress-value-pct`, inline de
soma), así que **su techo honesto es 87 %**. R-5.4 40/43 con `sweepAttr:
data-shape` + los controles `indeterminate` y `vertical` de la demo (22/41 sin
ellos); tres adjudicados. Diff 0 sobre **60 configuraciones** (2 formas × 2
orientaciones × 3 estados × 5 tallas, 17.280 valores) y capturas 2×
**idénticas byte a byte**. ⚠ **una sonda que CACHEA los nodos da 8.352 falsos
diffs si el HMR reemplaza el subárbol**: `getComputedStyle` de un nodo
desacoplado devuelve la cadena vacía en TODAS las propiedades.
- 2026-08-23 — **`cropper` 60 % → 94 %** (`f03ee5af4`), 2 claves. El cromo del
botón de zoom (radio + `font-size`, costura). **Siete literales de IDENTIDAD**
firmados: cuatro `100%`, dos `50%` (lo que HACE redonda una selección y un

@ -155,6 +155,18 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'transition-ease':
'same freeze; unfrozen pass -> reaches (cubic-bezier(0.4,0,0.2,1) -> steps(7))'
},
progress: {
// 2026-08-23. The guard now sweeps `data-shape` (like its twin meter) and
// switches the demo's indeterminate + vertical controls on, which takes it
// from 22/41 to 40/43. What is left needs a state the demo cannot hold at
// the same time as the ones already forced, or a property the guard freezes.
'indicator-bg-loaded':
"only under [data-state='loaded'], and the run holds `indeterminate` to reach the sweep tokens; forced on the real root -> reaches (oklch(0.6406 0.1329 157.68) -> rgb(1,2,3))",
'transition-duration':
'frozen by the guard by design; unfrozen pass -> reaches (0.18s -> 11.5s)',
'transition-ease':
'same freeze; unfrozen pass -> reaches (cubic-bezier(0.4,0,0.2,1) -> steps(7))'
},
'emoji-picker': {
// 2026-08-23. Four keys of the PORTALED panel. The guard writes the
// sentinel on :root and on the host, and :root does cross the portal

@ -207,6 +207,15 @@ const COMPONENT_OVERRIDES: Record<
// its track and centre) and the demo boots `linear`, so 14 of its 28 tokens
// had no node to paint.
meter: { sweepAttr: { attr: 'data-shape', values: ['linear', 'circular'] } },
// Same split as its twin: half the contract is the RING (12 size / thickness
// steps plus its track and its centre) and the demo boots `linear`.
progress: {
prepareWith: [
"[data-uix-control]:has([data-uix-control-label]:text-is('indeterminate')) input[type=checkbox]",
"[data-uix-control]:has([data-uix-control-label]:text-is('orientation')) [data-uix-chip]:text-is('vertical')"
],
sweepAttr: { attr: 'data-shape', values: ['linear', 'circular'] }
},
// Its dot / icon / remove parts are OPT-IN switches, all off by default.
badge: {
prepareWith: [

@ -2,6 +2,48 @@
Fecha de revision: 2026-05-17.
## Talla y tema
**43 claves públicas** en `lib/recipes/base.ts`: la barra por talla
(`height-{xs…xl}`, `radius-{xs…xl}`), el anillo por talla
(`ring-size-{xs…xl}`, `ring-thickness-{xs…xl}`), la altura del eje vertical
(`vertical-height`), los dos huecos de la rejilla raíz
(**`row-gap`**, **`column-gap`**), la tipografía de sus dos textos
(`label-*`, `value-text-*`, cinco claves cada uno), las tintas
(`track-bg`, `ring-track-bg`, `ring-center-bg`, `indicator-bg`,
`indicator-bg-loaded`), la transición (`transition-duration`,
`transition-ease`) y el barrido indeterminado
(`indeterminate-{width,duration,ease}`). La pestaña **Tokens** de su demo las
resuelve en vivo.
Lo que entró el **2026-08-23** (69 % → 87 %):
- **`row-gap` + `column-gap`** — el único knob de apariencia que iba a un
primitivo a pelo (`gap: var(--space-2) var(--space-3)`). Se parte en dos
porque sus dos valores DIFIEREN: el hueco de fila separa la línea de texto de
la barra, el de columna separa el rótulo del valor. Un `gap` único habría
nombrado un knob para dos (molde `chart.legend-row-gap` / `-column-gap`).
- **Los seis literales quedan FIRMADOS**, no acuñados: `100%` y `max-content`
son la geometría de la barra —llena la fila que le dan, o se encoge a su
propio grosor—, no una decisión de tema. Llevan su
`/* literal: <razón> */` en la declaración (válvula de recipe-contract §3).
**Lo que NO se acuña, y por qué** (mismo veredicto que su gemelo `meter`):
- **El `--radius-full` del anillo.** Es lo que hace circular a
`shape='circular'`: un `--progress-ring-radius` dejaría que un tema
des-redondease una forma que el consumidor pidió POR SU NOMBRE. Identidad, no
knob. Son los dos únicos knobs `global` que le quedan.
- **`--_progress-value-pct`.** Es el canal de VALOR: lo escribe soma en el
estilo inline del provider en cada render (`0–100`, saturado). Un público
encima mentiría — un tema no puede fijar el progreso de una tarea.
**Lo que el guard da por silencioso** (tres de 43, todos medidos):
`indicator-bg-loaded` sólo pinta bajo `[data-state='loaded']`, y la corrida
mantiene `indeterminate` para alcanzar los tokens del barrido; los dos de
transición los congela el propio guard por diseño y se miden en una pasada sin
congelar.
## Baseline
Air no tenia `progress` en `glm-5:src/uix/air/components`; la referencia local

@ -8,8 +8,9 @@
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-2) var(--space-3);
inline-size: 100%;
row-gap: var(--progress-row-gap);
column-gap: var(--progress-column-gap);
inline-size: 100%; /* literal: identity — a linear bar IS its container's width */
}
[data-progress][data-size='xs'] {
@ -41,7 +42,7 @@
}
[data-progress][data-orientation='vertical'] {
inline-size: max-content;
inline-size: max-content; /* literal: identity — it is its own thickness wide */
grid-template-columns: auto;
justify-items: center;
}
@ -51,7 +52,7 @@
position: relative;
overflow: hidden;
grid-column: 1 / -1;
inline-size: 100%;
inline-size: 100%; /* literal: identity — the track spans the row it is given */
block-size: var(--_progress-height);
border-radius: var(--_progress-radius);
background: var(--progress-track-bg);
@ -78,7 +79,7 @@
[data-progress][data-orientation='vertical'] [data-progress-indicator]::before {
inset-block: auto 0;
inline-size: 100%;
inline-size: 100%; /* literal: identity — the fill spans the track's thickness */
block-size: calc(var(--_progress-value-pct, 0) * 1%);
}
@ -99,12 +100,12 @@
[data-progress][data-orientation='vertical'][data-state='indeterminate']
[data-progress-indicator]::before {
block-size: var(--progress-indeterminate-width);
inline-size: 100%;
inline-size: 100%; /* literal: identity — same span, sweeping the other axis */
animation-name: progress-indeterminate-vertical;
}
[data-progress][data-shape='circular'] {
inline-size: max-content;
inline-size: max-content; /* literal: identity — the ring is its own diameter wide */
grid-template-columns: auto;
justify-items: center;
justify-self: center;
@ -119,6 +120,10 @@
[data-progress][data-shape='circular'] [data-progress-indicator] {
inline-size: var(--_progress-ring-size);
block-size: var(--_progress-ring-size);
/* NOT a recipe token: `--radius-full` IS what makes `shape='circular'`
circular. A `--progress-ring-radius` would let a theme un-circle a shape
the consumer asked for BY NAME — identity, not a knob. Same reading, and
the same wording, as its twin `meter`. */
border-radius: var(--radius-full);
background: conic-gradient(
var(--_progress-indicator-bg) calc(var(--_progress-value-pct, 0) * 1%),
@ -131,7 +136,7 @@
inset: var(--_progress-ring-thickness);
inline-size: auto;
block-size: auto;
border-radius: var(--radius-full);
border-radius: var(--radius-full); /* same identity: the hole is round because the ring is */
background: var(--progress-ring-center-bg);
transition: background var(--progress-transition-duration) var(--progress-transition-ease);
}

@ -2470,6 +2470,8 @@
--progress-height-lg: 8px;
--progress-height-xl: 12px;
--progress-vertical-height: 10rem;
--progress-row-gap: var(--space-2);
--progress-column-gap: var(--space-3);
--progress-radius-xs: var(--radius-full);
--progress-radius-sm: var(--radius-full);
--progress-radius-md: var(--radius-full);

@ -3797,6 +3797,12 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'height-lg': '8px',
'height-xl': '12px',
'vertical-height': '10rem',
// The two axes of the root grid, split because their values DIFFER: the
// row gap separates the text row from the bar, the column gap separates
// the label from the value text. A single `gap` shorthand would name one
// knob for two (the `chart.legend-row-gap` / `-column-gap` pair).
'row-gap': 'var(--space-2)',
'column-gap': 'var(--space-3)',
'radius-xs': 'var(--radius-full)',
'radius-sm': 'var(--radius-full)',
'radius-md': 'var(--radius-full)',

@ -2,8 +2,9 @@
import { Progress, type ProgressShape, type ProgressSize } from '$uix/eidos/components/progress';
import { compileMorfo } from '$uix/morfo';
import { progressMorfo } from '@/uix/morfo/components/progress';
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 TraceEntry = { event: string; family: string; intent?: string; at: number };
@ -176,6 +177,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>43</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -457,6 +461,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="progress" 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.