From 2e708adae856ae1ae00f9dbc01357b660b6aa42b Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 23 Aug 2026 21:34:17 +0200 Subject: [PATCH] =?UTF-8?q?uix(progress):=20temable=20=E2=80=94=2069=20%?= =?UTF-8?q?=20=E2=86=92=2087=20%,=20y=20el=20instrumento=20med=C3=ADa=20UN?= =?UTF-8?q?A=20de=20sesenta=20caras?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/audit/theming/README.md | 6 +- docs/audit/theming/progress.md | 112 ++++++++++++++---- docs/process/PLAN-theming.md | 16 +++ scripts/theming-sentinel-exceptions.ts | 12 ++ scripts/theming-sentinel.ts | 9 ++ src/uix/eidos/components/progress/README.md | 42 +++++++ .../eidos/components/progress/progress.css | 21 ++-- src/uix/eidos/generated/base.css | 2 + src/uix/eidos/lib/recipes/base.ts | 6 + .../uix/components/progress/+page.svelte | 10 +- 10 files changed, 201 insertions(+), 35 deletions(-) diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 8b3c37a80..f97d3ee15 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,8 +10,8 @@ - **Medido**: 2026-08-23 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/` - **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**? -- **Alcance global**: **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 | – | diff --git a/docs/audit/theming/progress.md b/docs/audit/theming/progress.md index ce092a8bc..aab11ec20 100644 --- a/docs/audit/theming/progress.md +++ b/docs/audit/theming/progress.md @@ -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: */` 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 `⚠`. -_(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: */` 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. diff --git a/docs/process/PLAN-theming.md b/docs/process/PLAN-theming.md index 02354aeb2..7fda494fb 100644 --- a/docs/process/PLAN-theming.md +++ b/docs/process/PLAN-theming.md @@ -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 diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index a7b52a397..c84fd74e8 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -155,6 +155,18 @@ export const SENTINEL_EXCEPTIONS: Record> = { '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 diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index 9592176fe..338f6dcd9 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -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: [ diff --git a/src/uix/eidos/components/progress/README.md b/src/uix/eidos/components/progress/README.md index 58cde505d..5b69d06f3 100644 --- a/src/uix/eidos/components/progress/README.md +++ b/src/uix/eidos/components/progress/README.md @@ -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: */` 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 diff --git a/src/uix/eidos/components/progress/progress.css b/src/uix/eidos/components/progress/progress.css index 19591f464..4bbb55f43 100644 --- a/src/uix/eidos/components/progress/progress.css +++ b/src/uix/eidos/components/progress/progress.css @@ -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); } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index 7d2be9e31..891e4e84b 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -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); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 5ee275f28..f81a0d70f 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -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)', diff --git a/web/routes/uix/components/progress/+page.svelte b/web/routes/uix/components/progress/+page.svelte index 2f7aea6c0..74a4cd3a7 100644 --- a/web/routes/uix/components/progress/+page.svelte +++ b/web/routes/uix/components/progress/+page.svelte @@ -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 @@ + @@ -457,6 +461,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'recipe'}

Eidos recipe