test(uix): audit Progress native behavior

astra
dev 7 days ago
parent f086a32727
commit d97d993228

@ -369,11 +369,46 @@ docs 976/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta; fases 0–5 sin cerrar y fase 7 pendiente. Progress continúa.
### Progress — cierre neutral global (2026-10-01)
Baseline final de 43 nombres únicos: 33 positivos, diez AssertionError y cero
pendientes. La calibración inicial y el checkpoint 32/11 se preservaron;
min=max se adjudicó según WAI-ARIA como caracterización positiva de
loaded/porcentaje cero. También se preservó una ejecución intermedia con
matcher inexistente; se repitió con el matcher correcto y diez AssertionError.
Treinta y tres positivos promovidos con huella exacta; dirigida 36/36 con
los tres canarios previos. Los 43 PNG del segundo checkpoint se inspeccionaron
individualmente y archivaron/SHA: 42 a 1100×900, uno a 350×900 y uno RTL.
Las animaciones no se congelaron ni se regeneraron originales al adjudicar.
L-152 se reproduce en las cuatro partes Eidos frente a controles Soma válidos.
Q241 conserva la referencia del ID antiguo de Label. Q315 registra valor
ARIA fuera de rango e inversión; Q316, nombre obsoleto al desmontar Label;
Q317, transición circular de 180ms bajo reduce. Los cinco quedan diferidos.
Soma separa style sin ; final, por lo que Progress no amplía Q313.
El bidi de value / max en RTL es nativo; no se adjudica como defecto.
Air morfo-runtime sí tiene Progress; se concilia la ausencia de glm-5 como
específica de ese árbol. README de ambas capas y comentarios de tipos
conciliados; TS minificado idéntico y 17 fuentes previas intactas.
Cero lógica extraída; API/CSS/receta y tres pruebas anteriores conservadas.
[Ficha](./audit-uix-2026-09-19/progress-native.md) reviewed/censo 162.
Guard final PASS 1/cero errores y aviso D-1.2 de tabs de demo congelada,
triado para reconstrucción. El marcador de disposición documental F-1.4
se concilió tras la primera gate. Ésta se detuvo por cuatro bind:ref no
enlazables del harness; diagnóstico machine de 93 errores preservado.
Las sondas negativas y controles Soma permanecen en tmp; sólo se retiró
su instrumentación del helper positivo, sin cambiar aserciones ni producción.
Segunda gate integral exit 0: 601 archivos/7351 pruebas,
docs 977/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente. Meter continúa.
## Continuación segura
Progress es el siguiente candidato, con baseline nativa en curso; la
[ficha de Progress/Meter](./audit-uix-2026-09-19/progress-meter.md) aporta los
candidatos de rango para adjudicación. Skeleton cerró su baseline individual.
Meter es el siguiente candidato, con preflight preparado y baseline por ejecutar.
La [ficha de Progress/Meter](./audit-uix-2026-09-19/progress-meter.md) conserva
la historia; Progress cerró su baseline nativa con hallazgos diferidos.
TagGroup ya tenía [auditoría nativa](./audit-uix-2026-09-19/tag-group.md)
reviewed/censo 104 desde el 28 de septiembre. Releer antes las cuatro guías
obligatorias de `AGENTS.md`. Mantener el alcance neutral autorizado:
@ -411,7 +446,8 @@ La misma cautela aplica a `tmp/sticky-audit-evidence-2026-09-30/`,
`tmp/avatar-audit-evidence-2026-09-30/` y
`tmp/image-audit-evidence-2026-09-30/` y
`tmp/spinner-audit-evidence-2026-09-30/` y
`tmp/skeleton-audit-evidence-2026-09-30/`.
`tmp/skeleton-audit-evidence-2026-09-30/` y
`tmp/progress-audit-evidence-2026-10-01/`.
Para gate/Vitest, apuntar `TEMP` y `TMP` a `node_modules/.cache`, capturar
`LASTEXITCODE` inmediatamente y no mutar archivos durante la gate.
La primera gate de Code sufrió un timeout Playwright de PickerShell

@ -11,7 +11,7 @@ status: reviewed
Workspace `G:\dev\svelte\vicen`, rama `astra`, remoto `gita/astra`.
El [handoff del 30](./HANDOFF-uix-quality-2026-09-30.md) conserva el
contexto de Highlight y del commit previo `5045216d6`. Este handoff
recoge el tramo tipográfico y la continuación hasta Skeleton. Al retomar,
recoge el tramo tipográfico y la continuación hasta Progress. Al retomar,
comprobar `HEAD`, `gita/astra` y el estado local antes de seguir; no
depender de un hash escrito dentro de este documento. La fase 6 sigue
abierta; fases 0–5 no cerradas y fase 7
@ -46,6 +46,7 @@ pendiente.
| [Image](./audit-uix-2026-09-19/image-native.md) | reviewed/159 | 26 positivos, 5 `AssertionError`, 0 pendientes | 31 PNG/SHA + 1 sonda | Q15/Q16/Q311/Q312 | 598/7270 |
| [Spinner](./audit-uix-2026-09-19/spinner-native.md) | reviewed/160 | 23 positivos, 5 AssertionError, 0 pendientes | 28 PNG/SHA, 2 a 350px | Q13/Q313 | 599/7293 |
| [Skeleton](./audit-uix-2026-09-19/skeleton-native.md) | reviewed/161 | 25 positivos, 6 AssertionError, 0 pendientes | 31 PNG/SHA + 3 sondas | Q13/Q313/Q314 | 600/7318 |
| [Progress](./audit-uix-2026-09-19/progress-native.md) | reviewed/162 | 33 positivos, 10 AssertionError, 0 pendientes | 43 PNG/SHA, 1 a 350px | L-152/Q241/Q315/Q316/Q317 | 601/7351 |
Los positivos promovidos conservaron nombres/estados exactos frente a
cada baseline. Los tests y dos helpers por componente viven fuera de
@ -191,11 +192,46 @@ docs 976/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta; fases 0–5 sin cerrar y fase 7 pendiente. Progress continúa.
### Progress — cierre neutral global (2026-10-01)
Baseline final de 43 nombres únicos: 33 positivos, diez AssertionError y cero
pendientes. La calibración inicial y el checkpoint 32/11 se preservaron;
min=max se adjudicó según WAI-ARIA como caracterización positiva de
loaded/porcentaje cero. También se preservó una ejecución intermedia con
matcher inexistente; se repitió con el matcher correcto y diez AssertionError.
Treinta y tres positivos promovidos con huella exacta; dirigida 36/36 con
los tres canarios previos. Los 43 PNG del segundo checkpoint se inspeccionaron
individualmente y archivaron/SHA: 42 a 1100×900, uno a 350×900 y uno RTL.
Las animaciones no se congelaron ni se regeneraron originales al adjudicar.
L-152 se reproduce en las cuatro partes Eidos frente a controles Soma válidos.
Q241 conserva la referencia del ID antiguo de Label. Q315 registra valor
ARIA fuera de rango e inversión; Q316, nombre obsoleto al desmontar Label;
Q317, transición circular de 180ms bajo reduce. Los cinco quedan diferidos.
Soma separa style sin ; final, por lo que Progress no amplía Q313.
El bidi de value / max en RTL es nativo; no se adjudica como defecto.
Air morfo-runtime sí tiene Progress; se concilia la ausencia de glm-5 como
específica de ese árbol. README de ambas capas y comentarios de tipos
conciliados; TS minificado idéntico y 17 fuentes previas intactas.
Cero lógica extraída; API/CSS/receta y tres pruebas anteriores conservadas.
[Ficha](./audit-uix-2026-09-19/progress-native.md) reviewed/censo 162.
Guard final PASS 1/cero errores y aviso D-1.2 de tabs de demo congelada,
triado para reconstrucción. El marcador de disposición documental F-1.4
se concilió tras la primera gate. Ésta se detuvo por cuatro bind:ref no
enlazables del harness; diagnóstico machine de 93 errores preservado.
Las sondas negativas y controles Soma permanecen en tmp; sólo se retiró
su instrumentación del helper positivo, sin cambiar aserciones ni producción.
Segunda gate integral exit 0: 601 archivos/7351 pruebas,
docs 977/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente. Meter continúa.
## Continuación segura
[Progress](./audit-uix-2026-09-19/progress-meter.md) continúa con baseline nativa
en curso. Skeleton cerró Q13/Q313/Q314 como hallazgos diferidos; su runtime
permanece intacto.
[Meter](./audit-uix-2026-09-19/progress-meter.md) continúa con baseline nativa
por ejecutar y preflight preparado. Progress cerró su baseline individual
con Q315/Q316/Q317, Q241 y L-152 diferidos; su runtime permanece intacto.
TagGroup ya tenía [auditoría nativa](./audit-uix-2026-09-19/tag-group.md)
reviewed/censo 104 desde el 28 de septiembre.
Grid, Flex, AspectRatio, Separator y Wrap no abrieron un defecto nuevo;
@ -211,6 +247,7 @@ No editar manualmente `web/` ni usar reset/stash/checkout. Los cambios
locales `.claude/settings.local.json` y `.vitest-attachments/` son del
usuario y se excluyeron de los commits de este lote. La evidencia
ignorada está en `tmp/{heading,text,display,link,prose,label,box,stack,group,container,section,auto-grid,float,affix,sticky,wrap,separator,aspect-ratio,flex,grid,badge,avatar,image,spinner,skeleton}-audit-evidence-2026-09-30/`;
Progress conserva evidencia en `tmp/progress-audit-evidence-2026-10-01/`;
archivo de capturas y promoción ya se ejecutaron, así que no repetir
esas mutaciones. Para gate/Vitest, apuntar `TEMP` y `TMP` a
`node_modules/.cache`, capturar `$LASTEXITCODE` inmediatamente y no

@ -4125,3 +4125,38 @@ exactas de PickerShell 35 y Skeleton 25. Segunda gate integral exit 0: 600 archi
docs 976/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta; fases 0–5 sin cerrar y fase 7 pendiente. Progress continúa.
### Progress — cierre neutral global (2026-10-01)
Baseline final de 43 nombres únicos: 33 positivos, diez AssertionError y cero
pendientes. La calibración inicial y el checkpoint 32/11 se preservaron;
min=max se adjudicó según WAI-ARIA como caracterización positiva de
loaded/porcentaje cero. También se preservó una ejecución intermedia con
matcher inexistente; se repitió con el matcher correcto y diez AssertionError.
Treinta y tres positivos promovidos con huella exacta; dirigida 36/36 con
los tres canarios previos. Los 43 PNG del segundo checkpoint se inspeccionaron
individualmente y archivaron/SHA: 42 a 1100×900, uno a 350×900 y uno RTL.
Las animaciones no se congelaron ni se regeneraron originales al adjudicar.
L-152 se reproduce en las cuatro partes Eidos frente a controles Soma válidos.
Q241 conserva la referencia del ID antiguo de Label. Q315 registra valor
ARIA fuera de rango e inversión; Q316, nombre obsoleto al desmontar Label;
Q317, transición circular de 180ms bajo reduce. Los cinco quedan diferidos.
Soma separa style sin ; final, por lo que Progress no amplía Q313.
El bidi de value / max en RTL es nativo; no se adjudica como defecto.
Air morfo-runtime sí tiene Progress; se concilia la ausencia de glm-5 como
específica de ese árbol. README de ambas capas y comentarios de tipos
conciliados; TS minificado idéntico y 17 fuentes previas intactas.
Cero lógica extraída; API/CSS/receta y tres pruebas anteriores conservadas.
[Ficha](./audit-uix-2026-09-19/progress-native.md) reviewed/censo 162.
Guard final PASS 1/cero errores y aviso D-1.2 de tabs de demo congelada,
triado para reconstrucción. El marcador de disposición documental F-1.4
se concilió tras la primera gate. Ésta se detuvo por cuatro bind:ref no
enlazables del harness; diagnóstico machine de 93 errores preservado.
Las sondas negativas y controles Soma permanecen en tmp; sólo se retiró
su instrumentación del helper positivo, sin cambiar aserciones ni producción.
Segunda gate integral exit 0: 601 archivos/7351 pruebas,
docs 977/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente. Meter continúa.

@ -30,11 +30,20 @@ Spinner/Skeleton Q313 con controles positivos. Skeleton añade Q314 para
text vacío de una línea sin reserva. Las fichas nativas y las secciones
finales conservan las sondas y su adjudicación diferida.
Progress añade Q315–Q317: valores ARIA fuera de rango/inversión, nombre
obsoleto tras desmontar Label y transición circular bajo reduce. Amplía
L-152 con cuatro controles Soma y Q241 con mutación del ID de Label.
min=max se reclasificó como caracterización positiva: la norma permite
igualdad; no se considera un rango invertido.
| Ampliación de cola | ID | Condición de cierre |
| ----------------------------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| Demora y sustitución de Image | Q311, Q312 | Adjudicar precedencia part/root y conservar la URL alternativa durante loading; cubrir reintentos, alternativa fallida, cambio externo y SSR. |
| Composición de estilo custom | Q313 | Separar fragmentos de composeInlineStyle conservando precedencia; caracterizar string sin ; final, record, vacío y consumidores. |
| Reserva de text de una línea | Q314 | Decidir barra única y compatibilidad de lines/snippet/keyed root; fijar reserva vacía y transiciones una/múltiples líneas antes de cambiar render. |
| Rango conocido de Progress | Q315 | Adjudicar rechazo, normalización o saturación de entradas; cubrir extremos y min>max, sin tratar min=max como infracción normativa. |
| Nombre tras desmontar Label | Q316 | Resolver vida del registro y fallback conservando precedencia externa; separar de los registros que requieren labels persistentes. |
| Transición circular reducida | Q317 | Adjudicar eliminación de transiciones bajo reduce y comprobar barra, anillo y overrides antes de cambiar CSS. |
Q34 se reprodujo con el proveedor Sticky y geometría real en Chromium.
Q35–Q36 se reprodujeron con el wrapper SkipLink montado en Chromium.
@ -3064,3 +3073,38 @@ sólo en timeout PickerShell; se amplió el plazo del canario de tres clicks
a 30000ms, misma huella positiva y dirigida conjunta 62/62. Segunda gate
exit 0: 600 archivos/7318 pruebas, docs 976/arts 25, check:gate 89 históricos/
cero nuevos src/scripts y apps/base check/build/smoke foundation 1/1 + boot 27/27.
## Progress — baseline nativa neutral (2026-10-01)
Baseline final de 43 nombres únicos: 33 positivos, diez AssertionError y cero
pendientes. La calibración inicial y el checkpoint 32/11 se preservaron;
min=max se adjudicó según WAI-ARIA como caracterización positiva de
loaded/porcentaje cero. También se preservó una ejecución intermedia con
matcher inexistente; se repitió con el matcher correcto y diez AssertionError.
Treinta y tres positivos promovidos con huella exacta; dirigida 36/36 con
los tres canarios previos. Los 43 PNG del segundo checkpoint se inspeccionaron
individualmente y archivaron/SHA: 42 a 1100×900, uno a 350×900 y uno RTL.
Las animaciones no se congelaron ni se regeneraron originales al adjudicar.
L-152 se reproduce en las cuatro partes Eidos frente a controles Soma válidos.
Q241 conserva la referencia del ID antiguo de Label. Q315 registra valor
ARIA fuera de rango e inversión; Q316, nombre obsoleto al desmontar Label;
Q317, transición circular de 180ms bajo reduce. Los cinco quedan diferidos.
Soma separa style sin ; final, por lo que Progress no amplía Q313.
El bidi de value / max en RTL es nativo; no se adjudica como defecto.
Air morfo-runtime sí tiene Progress; se concilia la ausencia de glm-5 como
específica de ese árbol. README de ambas capas y comentarios de tipos
conciliados; TS minificado idéntico y 17 fuentes previas intactas.
Cero lógica extraída; API/CSS/receta y tres pruebas anteriores conservadas.
[Ficha](./progress-native.md) reviewed/censo 162.
Guard final PASS 1/cero errores y aviso D-1.2 de tabs de demo congelada,
triado para reconstrucción. El marcador de disposición documental F-1.4
se concilió tras la primera gate. Ésta se detuvo por cuatro bind:ref no
enlazables del harness; diagnóstico machine de 93 errores preservado.
Las sondas negativas y controles Soma permanecen en tmp; sólo se retiró
su instrumentación del helper positivo, sin cambiar aserciones ni producción.
Segunda gate integral exit 0: 601 archivos/7351 pruebas,
docs 977/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente. Meter continúa.

@ -67,3 +67,38 @@ mantenimiento. Los wrappers visuales son suficientemente pequeños.
- Sin cambios de producción, CSS, pruebas, API, DOM ni árbol `web/`; ahorro de
código de producción: **0 líneas**. Esta auditoría evita una extracción sin
beneficio demostrado y deja dos discrepancias de documentación corregidas.
## Progress — baseline nativa neutral (2026-10-01)
Baseline final de 43 nombres únicos: 33 positivos, diez AssertionError y cero
pendientes. La calibración inicial y el checkpoint 32/11 se preservaron;
min=max se adjudicó según WAI-ARIA como caracterización positiva de
loaded/porcentaje cero. También se preservó una ejecución intermedia con
matcher inexistente; se repitió con el matcher correcto y diez AssertionError.
Treinta y tres positivos promovidos con huella exacta; dirigida 36/36 con
los tres canarios previos. Los 43 PNG del segundo checkpoint se inspeccionaron
individualmente y archivaron/SHA: 42 a 1100×900, uno a 350×900 y uno RTL.
Las animaciones no se congelaron ni se regeneraron originales al adjudicar.
L-152 se reproduce en las cuatro partes Eidos frente a controles Soma válidos.
Q241 conserva la referencia del ID antiguo de Label. Q315 registra valor
ARIA fuera de rango e inversión; Q316, nombre obsoleto al desmontar Label;
Q317, transición circular de 180ms bajo reduce. Los cinco quedan diferidos.
Soma separa style sin ; final, por lo que Progress no amplía Q313.
El bidi de value / max en RTL es nativo; no se adjudica como defecto.
Air morfo-runtime sí tiene Progress; se concilia la ausencia de glm-5 como
específica de ese árbol. README de ambas capas y comentarios de tipos
conciliados; TS minificado idéntico y 17 fuentes previas intactas.
Cero lógica extraída; API/CSS/receta y tres pruebas anteriores conservadas.
[Ficha](./progress-native.md) reviewed/censo 162.
Guard final PASS 1/cero errores y aviso D-1.2 de tabs de demo congelada,
triado para reconstrucción. El marcador de disposición documental F-1.4
se concilió tras la primera gate. Ésta se detuvo por cuatro bind:ref no
enlazables del harness; diagnóstico machine de 93 errores preservado.
Las sondas negativas y controles Soma permanecen en tmp; sólo se retiró
su instrumentación del helper positivo, sin cambiar aserciones ni producción.
Segunda gate integral exit 0: 601 archivos/7351 pruebas,
docs 977/arts 25, check:gate 89 históricos/cero nuevos src/scripts y
apps/base check/build/smoke foundation 1/1 + boot 27/27.
Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente. Meter continúa.

@ -0,0 +1,106 @@
---
title: UIX — Progress, rango y presentación nativa
type: notes
audience: human + agent
authority: process — auditoría de componente, no contrato nuevo
status: reviewed
---
# Progress — continuación nativa
Fecha: 2026-10-01. Alcance neutral autorizado; web fuera. Cuatro guías
obligatorias, Morfo/Soma, cuatro wrappers, tipos, receta/CSS, README,
langs, tres tests previos y Air leídos. La ficha de familia del 23
conserva los candidatos de rango; no se corrige runtime.
| Aspecto | Radix Primitives/Themes | Bits UI | Ark UI | React Aria | Decisión UIX |
| ------- | ----------------------- | -------------------------- | --------------------------- | ---------------------------- | --------------------------------- |
| Valor | null/max | null/min/max | null/min/max | isIndeterminate/custom scale | Medir rango y conservar null. |
| Partes | Root/Indicator | Root + composición externa | Label/Track/Range/ValueText | Label/render props | Conservar cuatro partes. |
| Forma | Themes lineal | CSS consumidor | Lineal/circular | SVG consumidor | Medir CSS propio de barra/anillo. |
| Talla | Themes 1/2/3 responsive | CSS consumidor | CSS variables | CSS consumidor | Medir xs…xl y responsive. |
| Nombre | ARIA del consumidor | Referencia explícita | Label/translations | Label/aria-label | Medir nombres y locale. |
Referencias primarias:
[Radix](https://www.radix-ui.com/primitives/docs/components/progress),
[Themes](https://www.radix-ui.com/themes/docs/components/progress),
[Bits](https://bits-ui.com/docs/components/progress),
[Ark linear](https://ark-ui.com/docs/components/progress-linear),
[Ark circular](https://ark-ui.com/docs/components/progress-circular),
[React Aria](https://react-aria.adobe.com/ProgressBar) y
[APG rangos](https://www.w3.org/WAI/ARIA/apg/practices/range-related-properties/).
Arquitectura: pasivo, cero eventos/Sema; consumidor posee progreso y feedback.
Soma clasifica null/valor frente a max, porcentaje y nombres; Eidos añade
size/shape y receta. No se fusiona con Meter. Cero lógica extraída.
Air morfo-runtime sí contiene Progress lineal y circular SVG, con
showValue/formatter, sm/md/lg y color; la ausencia citada de glm-5 era
de ese árbol, no de toda la historia. No se portan capacidades de Air.
Calibración inicial: 26 positivos/16 fallas; cinco expectativas omitían
el espacio inicial de ValueText, y la sexta positiva reveló transición
circular de 180ms bajo reduce. Se conservaron JSON/fuentes, 36 capturas
manuales y screenshots automáticos en calibration-original, sin certificar
su revisión visual íntegra. Segundo checkpoint 32 positivos/11 fallas,
43 nombres únicos, originales archivados/SHA e inspeccionados.
Una negativa min=max imponía desigualdad estricta: WAI-ARIA admite
igualdad. Su registro y captura se preservan; el caso se reclasifica
como positivo de loaded/porcentaje cero, sin cambio de producción.
[Norma aria-valuemax](https://www.w3.org/TR/wai-aria-1.2/#aria-valuemax).
Baseline final: 33 positivos, diez AssertionError, cero pendientes y 43
nombres únicos. Una ejecución intermedia de adjudicación usó el matcher
inexistente toBeGreaterThanOrEqualTo; se preservó en adjudication-invalid-matcher
y se repitió con toBeGreaterThanOrEqual. No se acepta ese Error como defecto.
La nueva corrida no reescribe los originales visuales.
Los positivos miden atributos/locale/nombres internos y externos, null,
valores conocidos, escala propia, completion color, cinco tallas lineales
2/4/6/8/12px y circulares 32/40/48/56/64px con inset 3/4/6/8/10px;
orientación vertical de 160px, ValueText/snippet/override, composición
opcional, estilo consumidor sin ; final, mutación sin remontar, RTL y
responsive a 350px. Unknown anima segmento de 35% a 1,1s en barra y rota
anillo a 1,1s. Reduce detiene animaciones; el anillo conserva transición
de fondo 180ms por especificidad CSS. ValueText conserva su espacio inicial.
DOM/CSS no certifican resultado AT.
Visuales: 43 originales del segundo checkpoint inspeccionados
individualmente; 42 de 1100×900, uno de 350×900 y uno RTL. Animaciones no congeladas.
La barra RTL llena desde el borde derecho; el string value / max sigue
bidi nativo, sin afirmar política de formato numérico aislado. La geometría
lineal/circular y reducción de movimiento se conserva. Rango saturado
visualmente pero texto/ARIA mantienen valor original; invertido produce
loaded/0% visible. Igualdad de límites es caso extremo válido por norma,
no violación ARIA. Sin certificación touch/dark/contraste/otros motores.
| ID | Evidencia | Corrección posterior |
| ----- | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| L-152 | Ref de root/Label/ValueText/Indicator nula; cuatro controles Soma reciben los nodos. | Reenviar bindables sólo tras adjudicación compatible por familia. |
| Q241 | Label id muta en DOM, aria-labelledby conserva audit-progress-label. | Registrar IDs reactivos sin variar precedencia externa. |
| Q315 | aria-valuenow 150/-10 frente a rango 0–100 pese a CSS 100/0%; min 100/max 50 invertidos con loaded/0%. | Adjudicar rechazo/normalización/clamp de entradas y semántica del rango antes de corregir. |
| Q316 | Tras desmontar Label, root conserva su ID inexistente y suprime default genérico. | Adjudicar vida del registro/nombre de fallback; conservar labels cuando su contrato requiera persistencia. |
| Q317 | Reduce circular detiene animación pero root del Indicator y su agujero conservan transición 0,18s; regla circular tiene mayor especificidad. | Adjudicar reducción de transiciones y cubrir barra/anillo/override antes de variar CSS. |
Hallazgos sin reparación. README de ambas capas y comentarios de tipos
conciliados; salida TypeScript minificada idéntica y 17 fuentes previas
intactas, incluidos los tres canarios del provider. Treinta y tres positivos
promovidos con nombres/estados/diagnósticos exactos y dos helpers externos.
Dirigida 36/36. Guard PASS 1, cero errores y un aviso D-1.2 de tabs de demo
congelada, triado para reconstrucción. Gate integral completada. Censo 162 reviewed;
fase 6 abierta.
La primera gate se detuvo en check:gate por cuatro bind:ref declarados
no enlazables en los wrappers Eidos del harness. El diagnóstico machine
se preserva: 93 errores, los 89 históricos y estos cuatro. Las sondas
negativas con sus controles Soma permanecen en tmp; se retiran sólo
esas bindings y su instrumentación del helper promovido, que cubre
positivos. No se cambia producción ni ninguna aserción positiva.
## Cierre de verificación
Segunda gate integral exit 0: 601 archivos/7351 pruebas, docs 977/arts 25,
check:gate 89 históricos/cero nuevos src/scripts y apps/base
check/build/smoke foundation 1/1 + boot 27/27.
La dirigida repetida mantiene 36/36 y huella exacta tras retirar del harness
positivo la instrumentación negativa de ref, conservada en tmp.
Guard final PASS 1/cero errores/un aviso D-1.2, triado.
Censo 162 reviewed. Meter es el siguiente candidato; fase 6 abierta.

@ -1,6 +1,6 @@
# Eidos Progress
Fecha de revision: 2026-05-17.
Fecha de revisión: 2026-10-01 (historia de tema conservada).
## Talla y tema
@ -44,66 +44,83 @@ 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
## Baseline nativa (2026-10-01)
La auditoría actual conserva el contrato Morfo/Soma/Eidos y los tres tests
previos. Air `glm-5:src/uix/air/components` no contenía Progress; el árbol
histórico morfo-runtime sí contiene Progress lineal y circular SVG, con
showValue/formatter, sm/md/lg y color. Esa superficie no se porta aquí.
Baseline final: 33 positivos, diez sondas negativas AssertionError, cero
pendientes y 43 nombres únicos. Se preservaron las calibraciones y la
adjudicación normativa de min=max, que WAI-ARIA permite. Los 43 PNG originales
del segundo checkpoint se inspeccionaron individualmente y archivaron/SHA:
42 a 1100×900, uno a 350×900 y uno RTL. No se congelaron animaciones.
[Ficha y evidencia](../../../../../docs/process/audit-uix-2026-09-19/progress-native.md).
## Comparativa estructural
| Referencia | Superficie consultada | Decisión UIX |
| ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| [Radix Primitives](https://www.radix-ui.com/primitives/docs/components/progress) / [Themes](https://www.radix-ui.com/themes/docs/components/progress) | Root/Indicator, value/null/max; Themes aporta barra y tallas responsive. | Conservar min, Label/ValueText y receta propia. |
| [Bits UI](https://bits-ui.com/docs/components/progress) | Root, value/null/min/max y composición mediante child/children; fill externo. | No atribuirle una parte Indicator; conservar las cuatro partes UIX. |
| [Ark linear](https://ark-ui.com/docs/components/progress-linear) / [circular](https://ark-ui.com/docs/components/progress-circular) | Label/ValueText/Track/Range y variantes de geometría. | Provider conserva semántica; Indicator pinta track/fill o anillo. |
| [React Aria ProgressBar](https://react-aria.adobe.com/ProgressBar) | Label, valueLabel/valueText, escala, render props e isIndeterminate. | Conservar valueText y snippet actuales; no importar API ni formato. |
La comparativa describe composición; no certifica paridad ni resultados AT.
## Contrato observado
- Provider DIV con role progressbar; Label y ValueText SPAN, Indicator DIV.
Las tres partes son opcionales. El root conserva semántica sin ellas.
- value=null omite aria-valuenow y resuelve indeterminate. Un valueText
explícito no vacío puede conservar aria-valuetext y el texto visible.
- Soma resuelve loading/loaded frente a max, escala respecto de min/max,
nombre y texto. El porcentaje CSS se satura entre 0 y 100, mientras
aria-valuenow y texto conservan el valor original. min=max produce 0%
y loaded cuando value=max; min>max también produce 0%, pero su rango ARIA
está invertido. Validación/normalización quedan diferidas.
- Label interno suprime el nombre genérico; aria-labelledby externo tiene
precedencia. El fallback traducido es Loading/Cargando. Mutación de ID y
desmontaje del Label conservan una referencia obsoleta: Q241/Q316.
- ValueText usa el texto de Soma o un snippet con valor, límites, estado,
porcentaje y texto. El render ordinario conserva un espacio inicial.
- Eidos resuelve size=xs…xl y shape=linear/circular, también responsive.
Barras de 2/4/6/8/12px; anillos de 32/40/48/56/64px con inset
3/4/6/8/10px. La orientación vertical utiliza 160px por defecto.
- El barrido indeterminado lineal ocupa 35% y dura 1,1s; el anillo rota a
1,1s. data-motion=reduce detiene animaciones y conserva geometría;
en circular quedan transiciones de fondo de 180ms (Q317).
- El fill lineal RTL parte del borde derecho. El texto value / max sigue
el bidi nativo; no se afirma una política de formato aislado.
- El style consumidor sin punto y coma final conserva overrides y canal
porcentual: la composición Soma separa fragmentos. No reproduce Q313.
- Los cuatro bind:ref Eidos no alcanzan al consumidor, frente a controles
Soma que sí reciben los nodos (L-152). Svelte-check declara esas bindings
no enlazables; las sondas negativas quedan en evidencia temporal.
El harness permanente conserva sólo los positivos. No se cambia ese comportamiento.
Air no tenia `progress` en `glm-5:src/uix/air/components`; la referencia local
es el contrato Morfo/Soma actual.
## Comparativa
| Referencia | Superficie relevante | Estado Eidos |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Air `glm-5` | No existe `progress` en Air. | Sin baseline Air. |
| Soma actual | `Provider`, `Label`, `ValueText`, `Indicator`; determinate/indeterminate, `min/max/value`, `valueText`, orientacion, label interno o externo. | Cubierto por passthrough. Eidos anade `size`, `shape` y receta visual. |
| Radix Progress | `Root`, `Indicator`, `value`, `max`, `getValueLabel`, indeterminate con `value=null`, data attrs y CSS vars. | Cubierto. Radix no tiene `min`, `Label` ni `ValueText` como partes. |
| Ark UI Progress | Linear progress con `Root`, `Label`, `Track`, `Range`, `ValueText`, `View`; `orientation`, translations y value text. | Cubierto en lineal salvo separar `Track/Range`: UIX usa `Provider/Indicator`, donde `Indicator` pinta el fill. |
| Bits UI Progress | `Root`, `Indicator`, `value`, `max`, `getValueLabel`, `valueLabel`, indeterminate, children snippet. | Cubierto. Label/value visibles quedan como partes canonicas en UIX. |
| React Aria ProgressBar | Label, value label, indeterminate, min/max/value, accessible progressbar. | Cubierto en el caso lineal. |
Fuentes externas consultadas:
## Decisiones
- Radix Progress: https://www.radix-ui.com/primitives/docs/components/progress
- Ark UI Progress: https://ark-ui.com/docs/components/progress
- Bits UI Progress: https://bits-ui.com/docs/components/progress
- React Aria ProgressBar: https://react-spectrum.adobe.com/react-aria/ProgressBar.html
Soma posee estado/ARIA y Eidos sólo presentación. shape no modifica
semántica; orientation sí viaja por Morfo/Soma. No se crea Track adicional
ni se fusiona con Meter. La fórmula compartida no justifica extracción
con ahorro neto: cero lógica extraída.
## Decisiones
## Passive justification
- No se crea `Progress.Track`. El root con `data-progress` es el contenedor
semantico; `Progress.Indicator` pinta el track/fill lineal o el ring
circular. Crear otra parte duplicaria contenedor sin aportar estado.
- `Progress.ValueText` no escribe estado propio. Renderiza el texto resuelto por
Soma, que es el mismo que `aria-valuetext`.
- `orientation` pertenece a Morfo/Soma porque cambia `data-orientation`; Eidos
solo consume el atributo para la receta horizontal/vertical.
- `shape` es visual y vive en Eidos: `linear` mantiene barra, `circular`
renderiza ring con el mismo porcentaje de Soma. No se modela en Morfo porque
no cambia ARIA ni estado headless.
- `size` y `shape` son props visuales de Eidos. El color queda en tokens de
recipe (`--progress-indicator-bg`, `--progress-indicator-bg-loaded`), no como
estado funcional.
## Eventos Sema
Progress declara 0 eventos semanticos de forma deliberada. Representa estado de
tarea o carga, no una accion directa del usuario. La decision de anunciar un
avance, completar una tarea o advertir de un bloqueo pertenece al flujo de
producto que controla el valor, no al primitive visual.
Pasivo, cero eventos Sema. El consumidor posee valor, carga y feedback.
El componente representa estado de tarea; no incorpora interacción ni
anuncios perceptivos propios al cambiar el valor.
## Gaps
| Gap | Disposición | Detalle |
| --------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------- |
| API extendida sobre las referencias externas | **diferir** | Sólo si llega un caso de uso real. Mantener la superficie estable. |
| Cobertura adicional de variantes visuales | **diferir** | El recipe cubre sm/md/lg + solid/outline/ghost. Más variantes requieren caso concreto. |
| Documentación per-prop exhaustiva | **implementar** | Cuando se cierre el ciclo de remediación de cada componente. |
| Tests browser-level del flujo completo (Playwright) | **implementar** | Cobertura visual + interacciones. Se hace en una pasada conjunta de tests. |
## Passive justification
Disposición: **diferir**. L-152, Q241, Q315 (rango), Q316 (desmontaje del Label) y Q317 (transición
circular bajo reduce) quedan registrados para corrección posterior
compatible. API, wrappers, CSS y receta permanecen intactos.
La cobertura nativa no certifica AT, touch, tema oscuro, contraste ni otros motores.
Componente passive por diseño: no gestiona estado mutable propio,
no responde a teclado más allá del foco del navegador, no emite
eventos sema propios. El feedback perceptivo correspondiente al
cambio que rodea al componente (validación, progreso, transición)
pertenece a quien orquesta ese cambio — Form / Field / Toast /
Dialog — no al componente visual.
Cierre: dirigida 36/36 con huella exacta; guard PASS 1/cero errores y aviso D-1.2
de demo congelada, triado. Segunda gate integral exit 0: 601 archivos/7351 pruebas.
Docs 977/arts 25 limpios, cero nuevos src/scripts y apps/base check/build/smoke
foundation 1/1 + boot 27/27.

@ -0,0 +1,321 @@
import { expect, it, vi } from 'vitest';
import { page } from 'vitest/browser';
import { flushSync } from 'svelte';
import { visit } from '$uix/test/progress/progress-audit-view';
const css = (view: Parameters<Parameters<typeof visit>[0]>[0]) =>
getComputedStyle(view.indicator()!);
const fill = (view: Parameters<Parameters<typeof visit>[0]>[0]) =>
getComputedStyle(view.indicator()!, '::before');
it('native default Progress omits the unknown value and animates a linear track', () =>
visit(async (view) => {
expect(view.root().tagName).toBe('DIV');
expect(view.root().getAttribute('role')).toBe('progressbar');
expect(view.root().getAttribute('aria-valuemin')).toBe('0');
expect(view.root().getAttribute('aria-valuemax')).toBe('100');
expect(view.root().hasAttribute('aria-valuenow')).toBe(false);
expect(view.root().hasAttribute('aria-valuetext')).toBe(false);
expect(view.root().getAttribute('aria-label')).toBe('Loading');
expect(view.root().getAttribute('data-state')).toBe('indeterminate');
expect(view.root().getAttribute('data-size')).toBe('md');
expect(view.root().getAttribute('data-shape')).toBe('linear');
expect(view.root().getAttribute('data-orientation')).toBe('horizontal');
expect(view.root().classList.contains('audit-progress')).toBe(true);
expect(view.root().getAttribute('data-probe')).toBe('progress');
expect(view.root().title).toBe('Audit Progress');
expect(view.valueText()!.textContent).toBe(' ');
expect(view.indicator()!.getAttribute('data-state')).toBe('indeterminate');
expect(css(view).height).toBe('6px');
expect(fill(view).animationName).toBe('progress-indeterminate');
expect(fill(view).animationDuration).toBe('1.1s');
expect(fill(view).width).toBe('147px');
await view.capture('default');
}));
it('native determinate Progress publishes the value and scaled fill', () =>
visit(
async (view) => {
expect(view.root().getAttribute('aria-valuenow')).toBe('30');
expect(view.root().getAttribute('aria-valuetext')).toBe('30 / 100');
expect(view.valueText()!.textContent).toBe(' 30 / 100');
expect(view.root().getAttribute('data-state')).toBe('loading');
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('30');
expect(fill(view).width).toBe('126px');
expect(fill(view).animationName).toBe('none');
await view.capture('determinate');
},
{ value: 30 }
));
it('native custom scale subtracts min before computing the fill percentage', () =>
visit(
async (view) => {
expect(view.root().getAttribute('aria-valuemin')).toBe('20');
expect(view.root().getAttribute('aria-valuemax')).toBe('60');
expect(view.root().getAttribute('aria-valuenow')).toBe('30');
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('25');
expect(fill(view).width).toBe('105px');
await view.capture('scale');
},
{ value: 30, min: 20, max: 60 }
));
it('native completed Progress uses the loaded state and completion color', () =>
visit(
async (view) => {
expect(view.root().getAttribute('data-state')).toBe('loaded');
expect(view.indicator()!.getAttribute('data-state')).toBe('loaded');
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('100');
expect(fill(view).width).toBe('420px');
expect(fill(view).backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
const completed = fill(view).backgroundColor;
flushSync(() => view.controls().value(50));
await vi.waitFor(() => expect(fill(view).backgroundColor).not.toBe(completed));
expect(view.root().getAttribute('data-state')).toBe('loading');
await view.capture('completion-to-loading');
},
{ value: 100, reduce: true }
));
for (const [size, height, diameter, thickness] of [
['xs', 2, 32, 3],
['sm', 4, 40, 4],
['md', 6, 48, 6],
['lg', 8, 56, 8],
['xl', 12, 64, 10]
] as const) {
it('native linear ' + size + ' has ' + height + 'px track thickness', () =>
visit(
async (view) => {
expect(view.indicator()!.getBoundingClientRect().width).toBe(420);
expect(view.indicator()!.getBoundingClientRect().height).toBe(height);
expect(fill(view).width).toBe('210px');
await view.capture('linear-' + size);
},
{ value: 50, size }
)
);
it(
'native circular ' + size + ' reserves ' + diameter + 'px with ' + thickness + 'px inset',
() =>
visit(
async (view) => {
expect(view.indicator()!.getBoundingClientRect().width).toBe(diameter);
expect(view.indicator()!.getBoundingClientRect().height).toBe(diameter);
expect(fill(view).top).toBe(thickness + 'px');
expect(css(view).backgroundImage).toContain('conic-gradient');
expect(css(view).backgroundImage).toContain('50%');
expect(fill(view).animationName).toBe('none');
await view.capture('circular-' + size);
},
{ value: 50, size, shape: 'circular' }
)
);
}
it('native consumer aria label overrides the default translation', () =>
visit(
async (view) => {
expect(view.root().getAttribute('aria-label')).toBe('Downloading account');
await view.capture('custom-label');
},
{ ariaLabel: 'Downloading account' }
));
it('native internal Label names Progress and suppresses its generic label', () =>
visit(
async (view) => {
expect(view.root().getAttribute('aria-labelledby')).toBe('audit-progress-label');
expect(view.root().hasAttribute('aria-label')).toBe(false);
expect(view.label()!.tagName).toBe('SPAN');
expect(view.label()!.textContent).toBe('Import account');
await view.capture('internal-label');
},
{ value: 42, label: true }
));
it('native external labelledby takes precedence over the internal Label', () =>
visit(
async (view) => {
expect(view.root().getAttribute('aria-labelledby')).toBe('audit-progress-external');
expect(view.root().hasAttribute('aria-label')).toBe(false);
expect(view.target.querySelector('#audit-progress-external')!.textContent).toBe(
'External task'
);
await view.capture('external-label');
},
{ value: 42, label: true, externalLabel: true }
));
it('native default Progress label follows the active Spanish locale', () =>
visit(async (view) => {
view.uix.langs.setLocale('es');
await vi.waitFor(() => expect(view.root().getAttribute('aria-label')).toBe('Cargando'));
await view.capture('locale-es');
}));
it('native valueText override supplies both visible and ARIA value text', () =>
visit(
async (view) => {
expect(view.root().getAttribute('aria-valuetext')).toBe('25 of 100 MB');
expect(view.valueText()!.textContent).toBe(' 25 of 100 MB');
await view.capture('value-text');
},
{ value: 25, valueText: '25 of 100 MB' }
));
it('native ValueText snippet receives value range state percentage and text', () =>
visit(
async (view) => {
expect(view.valueText()!.textContent!.trim()).toBe('30|20|60|loading|25|30 / 60');
flushSync(() => view.controls().value(60));
expect(view.valueText()!.textContent!.trim()).toBe('60|20|60|loaded|100|60 / 60');
await view.capture('value-snippet');
},
{ value: 30, min: 20, max: 60, valueSnippet: true }
));
it('native indeterminate Progress can retain an explicit value text', () =>
visit(
async (view) => {
expect(view.root().hasAttribute('aria-valuenow')).toBe(false);
expect(view.root().getAttribute('aria-valuetext')).toBe('Waiting for server');
expect(view.valueText()!.textContent).toBe(' Waiting for server');
await view.capture('unknown-text');
},
{ value: null, valueText: 'Waiting for server' }
));
it('native vertical Progress fills from the bottom of a 160px track', () =>
visit(
async (view) => {
expect(view.indicator()!.getBoundingClientRect().width).toBe(6);
expect(view.indicator()!.getBoundingClientRect().height).toBe(160);
expect(fill(view).height).toBe('40px');
expect(fill(view).bottom).toBe('0px');
expect(fill(view).width).toBe('6px');
await view.capture('vertical');
},
{ value: 25, orientation: 'vertical' }
));
it('native vertical unknown Progress uses the vertical sweep', () =>
visit(
async (view) => {
expect(fill(view).animationName).toBe('progress-indeterminate-vertical');
expect(fill(view).height).toBe('56px');
expect(fill(view).width).toBe('6px');
await view.capture('vertical-unknown');
},
{ orientation: 'vertical' }
));
it('native circular unknown Progress rotates the ring rather than its hole', () =>
visit(
async (view) => {
expect(css(view).animationName).toBe('progress-ring-indeterminate');
expect(css(view).animationDuration).toBe('1.1s');
expect(css(view).backgroundImage).toContain('conic-gradient');
expect(fill(view).animationName).toBe('none');
expect(fill(view).transform).toBe('none');
await view.capture('ring-unknown');
},
{ shape: 'circular' }
));
for (const mode of ['linear', 'vertical', 'circular'] as const) {
it('native reduce stops ' + mode + ' animation and retains track geometry', () =>
visit(
async (view) => {
expect(css(view).animationName).toBe('none');
expect(fill(view).animationName).toBe('none');
expect(fill(view).transform).toBe('none');
expect(css(view).transitionDuration).toBe(mode === 'circular' ? '0.18s' : '0s');
expect(fill(view).transitionDuration).toBe(mode === 'circular' ? '0.18s' : '0s');
expect(view.indicator()!.getBoundingClientRect().height).toBe(
mode === 'circular' ? 48 : mode === 'vertical' ? 160 : 6
);
await view.capture('reduce-' + mode);
},
{
shape: mode === 'circular' ? 'circular' : 'linear',
orientation: mode === 'vertical' ? 'vertical' : 'horizontal',
reduce: true
}
)
);
}
it('native consumer CSS fragments and recipe overrides coexist with the value channel', () =>
visit(
async (view) => {
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('40');
expect(css(view).height).toBe('10px');
expect(fill(view).backgroundColor).toBe('rgb(153, 17, 0)');
expect(getComputedStyle(view.root()).outlineWidth).toBe('2px');
await view.capture('style');
},
{
value: 40,
style: '--progress-height-md:10px;--progress-indicator-bg:#991100;outline:2px solid #123456'
}
));
it('native Progress value scale shape and size react on the same root', () =>
visit(
async (view) => {
const root = view.root();
flushSync(() => {
view.controls().value(40);
view.controls().min(20);
view.controls().max(60);
view.controls().shape('circular');
view.controls().size('lg');
view.controls().valueText('Halfway');
});
expect(view.root()).toBe(root);
expect(root.style.getPropertyValue('--_progress-value-pct')).toBe('50');
expect(root.getAttribute('aria-valuenow')).toBe('40');
expect(view.valueText()!.textContent).toBe(' Halfway');
expect(view.indicator()!.getBoundingClientRect().height).toBe(56);
flushSync(() => view.controls().value(null));
expect(root.getAttribute('data-state')).toBe('indeterminate');
expect(root.hasAttribute('aria-valuenow')).toBe(false);
await view.capture('reactive');
},
{ value: 10, reduce: true }
));
it('native RTL determinate Progress anchors its fill to the logical start', () =>
visit(
async (view) => {
expect(getComputedStyle(view.root()).direction).toBe('rtl');
expect(fill(view).right).toBe('0px');
expect(fill(view).width).toBe('105px');
await view.capture('rtl');
},
{ value: 25, label: true, dir: 'rtl' }
));
it('native responsive Progress resolves circular xs below the md breakpoint', () =>
visit(
async (view) => {
expect(view.root().getAttribute('data-shape')).toBe('linear');
expect(view.root().getAttribute('data-size')).toBe('lg');
await page.viewport(350, 900);
await vi.waitFor(() => expect(view.root().getAttribute('data-shape')).toBe('circular'));
expect(view.root().getAttribute('data-size')).toBe('xs');
expect(view.indicator()!.getBoundingClientRect().width).toBe(32);
expect(view.indicator()!.getBoundingClientRect().height).toBe(32);
await view.capture('responsive');
},
{ value: 25, size: { base: 'xs', md: 'lg' }, shape: { base: 'circular', md: 'linear' } }
));
it('native Progress remains a named semantic root without optional parts', () =>
visit(
async (view) => {
expect(view.root().getAttribute('role')).toBe('progressbar');
expect(view.root().getAttribute('aria-label')).toBe('Loading');
expect(view.root().getAttribute('aria-valuenow')).toBe('40');
expect(view.indicator()).toBeNull();
expect(view.valueText()).toBeNull();
expect(view.label()).toBeNull();
await view.capture('no-parts');
},
{ value: 40, indicatorPart: false, valuePart: false }
));
it('native Progress exposes an ordered ARIA range for 100 to 100', () =>
visit(
async (view) => {
expect(view.root().getAttribute('aria-valuemin')).toBe('100');
expect(view.root().getAttribute('aria-valuemax')).toBe('100');
expect(view.root().getAttribute('aria-valuenow')).toBe('100');
expect(view.root().style.getPropertyValue('--_progress-value-pct')).toBe('0');
expect(view.root().getAttribute('data-state')).toBe('loaded');
expect(getComputedStyle(view.indicator()!, '::before').width).toBe('0px');
await view.capture('negative-scale-100');
},
{ value: 100, min: 100, max: 100, reduce: true }
));

@ -1,6 +1,6 @@
# Progress
A `role="progressbar"` that shows the completion of a known-duration task. Supports determinate (numeric) and indeterminate (`value=null`) modes. The decorative Indicator is driven by the `--_progress-value-pct` CSS custom property.
A `role="progressbar"` that shows the completion of a task. Supports determinate (numeric) and indeterminate (`value=null`) modes. The decorative Indicator is driven by the `--_progress-value-pct` CSS custom property.
For a value rendered inside a known range (disk usage, score), use `Meter` instead.
@ -27,16 +27,16 @@ For a value rendered inside a known range (disk usage, score), use `Meter` inste
### `Provider`
| Prop | Type | Default | Description |
| ----------------- | ---------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------- |
| `id` | `string` | auto | DOM id. |
| `value` | `number \| null` | `null` | Current progress. `null` → indeterminate (no ARIA value, `data-state='indeterminate'`). |
| `min` | `number` | `0` | Lower bound. |
| `max` | `number` | `100` | Upper bound. |
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Data/CSS orientation. |
| `valueText` | `string` | `${value} / ${max}` | Override for `aria-valuetext`. Provide when the percentage would be misleading (e.g. `"75 of 100 MB"`). |
| `aria-label` | `string` | translated `Loading` | Accessible name. Ignored when `aria-labelledby` is set. |
| `aria-labelledby` | `string` | — | External label. |
| Prop | Type | Default | Description |
| ----------------- | ---------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `id` | `string` | auto | DOM id. |
| `value` | `number \| null` | `null` | Current progress. `null` → indeterminate (no aria-valuenow, `data-state='indeterminate'`). |
| `min` | `number` | `0` | Lower bound. |
| `max` | `number` | `100` | Upper bound. |
| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Data/CSS orientation. |
| `valueText` | `string` | `${value} / ${max}` | Non-empty override for `aria-valuetext`. Provide when the percentage would be misleading (e.g. `"75 of 100 MB"`). |
| `aria-label` | `string` | translated `Loading` | Consumer name; the generic default is suppressed by a resolved label. |
| `aria-labelledby` | `string` | — | External label. |
## ARIA
@ -46,11 +46,14 @@ For a value rendered inside a known range (disk usage, score), use `Meter` inste
| Provider | `aria-valuemin` | Value of `min` |
| Provider | `aria-valuemax` | Value of `max` |
| Provider | `aria-valuenow` | Value of `value` (omitted when indeterminate) |
| Provider | `aria-valuetext` | Resolved value text (omitted when indeterminate) |
| Provider | `aria-valuetext` | Explicit non-empty text, or known value / max |
| Provider | `aria-label` | Translated default or override when no label exists |
| Provider | `aria-labelledby` | Explicit external id or internal `Label` id |
Indeterminate mode omits `aria-valuenow` / `aria-valuetext`, matching the WAI-ARIA spec (unknown value).
Indeterminate mode omits `aria-valuenow`. Explicit non-empty `valueText` can remain as
`aria-valuetext` and visible text. An empty override falls back to the ordinary resolution.
External `aria-labelledby` wins over the internal Label. A supplied `aria-label` may remain
in the DOM; accessible name precedence belongs to the platform.
## Data Attributes
@ -73,6 +76,12 @@ State resolution:
- `value >= max` → `loaded`
- otherwise → `loading`
Known values outside the range remain raw in ARIA and text; only the CSS percentage
is clamped. When max <= min, the percentage is zero. WAI-ARIA permits min=max;
this implementation classifies value=max as loaded while leaving the fill empty.
An inverted range violates the required ordering and needs contract adjudication.
[WAI-ARIA aria-valuemax](https://www.w3.org/TR/wai-aria-1.2/#aria-valuemax).
## CSS Variables
| Variable | Part | Description |
@ -81,22 +90,29 @@ State resolution:
## Comparison
| Feature | Soma | Radix | Ark UI | bits-ui | react-aria |
| ------------------------------------ | :--: | :---: | :----: | :-----: | :--------: |
| `role="progressbar"` | ✅ | ✅ | ✅ | ✅ | ✅ |
| Indeterminate (`value=null`) | ✅ | ✅ | ✅ | ✅ | ✅ |
| `aria-valuetext` override | ✅ | ✅ | ✅ | ✅ | ✅ |
| `--_progress-value-pct` CSS var | ✅ | ⚠️¹ | ✅ | ✅ | ❌ |
| `data-state` reflects loading/loaded | ✅ | ✅ | ✅ | ✅ | ❌ |
| Translated default `aria-label` | ✅ | ❌ | ❌ | ❌ | ❌ |
| `min` configurable | ✅ | ❌² | ✅ | ✅ | ✅ |
| Label / value text parts | ✅ | ❌ | ✅ | ❌³ | ✅ |
| Vertical orientation | ✅ | ❌ | ✅ | ❌ | ⚠️⁴ |
¹ Radix uses `--radix-progress-indicator-max-width`; same shape, different name.
² Radix hardcodes `min=0`.
³ Bits documents external label/value markup around the root, not built-in parts.
⁴ React Aria supports custom circular/vertical renderings through your own markup.
| Primary reference | Documented structure | UIX decision |
| ----------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [Radix Progress](https://www.radix-ui.com/primitives/docs/components/progress) | Root/Indicator, value/null/max and consumer label. | Keep configurable min and optional Label/ValueText. |
| [Bits Progress](https://bits-ui.com/docs/components/progress) | Root with min/max/value and child/children composition; external fill. | Keep UIX parts; do not invent a Bits Indicator or shared CSS variable. |
| [Ark linear](https://ark-ui.com/docs/components/progress-linear) / [circular](https://ark-ui.com/docs/components/progress-circular) | Label/ValueText/Track/Range and geometry parts. | Preserve Provider/Indicator ownership. |
| [React Aria ProgressBar](https://react-aria.adobe.com/ProgressBar) | Label, formatted value, custom scale and render props. | Keep the existing valueText and ValueText snippet. |
These are structural references, not a parity or assistive technology guarantee.
## Native audit limits (2026-10-01)
The final baseline has 33 passing cases and ten failing AssertionError probes;
the three existing provider tests remain unchanged. Four Eidos ref probes
fail while direct Soma controls receive their nodes (L-152). Out-of-range
known values remain raw in ARIA/text despite clamped CSS (Q315).
Label ID mutation keeps the old reference (Q241); Label unmount leaves a
dangling aria-labelledby and suppresses the default name (Q316).
The Eidos circular recipe retains its 180ms background transition under
reduced motion despite stopping animation (Q317).
No runtime correction or common provider extraction was made.
[Native audit](../../../../../docs/process/audit-uix-2026-09-19/progress-native.md)
preserves calibration, normative adjudication, screenshots and deferred decisions.
## Usage

@ -1,7 +1,7 @@
import type { Orientation, WithChild, Without } from '../../types';
import type { PrimitiveDivAttributes } from '../../types';
/** Progress state derived from value + min + max. */
/** Progress state derived from null or the known value relative to max. */
export type ProgressState = 'indeterminate' | 'loading' | 'loaded';
// ── Root provider ──────────────────────────────────────────────────────────
@ -9,11 +9,11 @@ export type ProgressState = 'indeterminate' | 'loading' | 'loaded';
/**
* Props for the root `Progress.Provider`.
*
* Renders `<div role="progressbar">` with the full ARIA valuenow/min/max
* triplet. Supports indeterminate mode (`value=null`) for unknown-duration
* Renders `<div role="progressbar">` with ARIA min/max and a known valuenow.
* Indeterminate mode (`value=null`) omits valuenow for unknown-duration
* operations like initial data fetches.
*
* The `Indicator` sub-part is a scaled fill bar, positioned via the
* The optional `Indicator` sub-part is a scaled fill, positioned via the
* `--_progress-value-pct` CSS variable (0–100).
*/
export type ProgressProps = WithChild<{
@ -22,8 +22,7 @@ export type ProgressProps = WithChild<{
/**
* Current progress value. `null` renders the bar in indeterminate mode
* (the Indicator is still present but has no meaningful width, and
* `data-state='indeterminate'`).
* (`aria-valuenow` is omitted and `data-state='indeterminate'`).
* @default null
*/
value?: number | null;

@ -0,0 +1,126 @@
<script module lang="ts">
import type { ProgressProps } from '$uix/eidos/components/progress';
import type { ValueTextSnippetProps as ProgressValueTextSnippetProps } from '$soma/components/progress';
export type Config = {
value?: number | null;
min?: number;
max?: number;
size?: ProgressProps['size'];
shape?: ProgressProps['shape'];
orientation?: ProgressProps['orientation'];
valueText?: string;
label?: boolean;
externalLabel?: boolean;
ariaLabel?: string;
valuePart?: boolean;
indicatorPart?: boolean;
valueSnippet?: boolean;
style?: string;
dir?: 'ltr' | 'rtl';
reduce?: boolean;
viewportWidth?: number;
};
export type Controls = {
value: (next: number | null) => void;
min: (next: number) => void;
max: (next: number) => void;
size: (next: ProgressProps['size']) => void;
shape: (next: ProgressProps['shape']) => void;
orientation: (next: ProgressProps['orientation']) => void;
valueText: (next: string | undefined) => void;
label: (next: boolean) => void;
labelId: (next: string) => void;
};
</script>
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { setActiveUix, type ActiveUix } from '$active-uix';
import { Soma } from '$soma/core/soma.svelte';
import { ActiveEidos } from '$uix/eidos';
import Progress from '$uix/eidos/components/progress';
let {
uix,
initial = {},
onControls
}: {
uix: ActiveUix;
initial?: Config;
onControls: (controls: Controls) => void;
} = $props();
// svelte-ignore state_referenced_locally
setActiveUix(uix);
Soma.create();
const eidos = ActiveEidos.create({ applyDom: false });
onDestroy(() => eidos.dispose());
// svelte-ignore state_referenced_locally
let value = $state(initial.value);
// svelte-ignore state_referenced_locally
let min = $state(initial.min);
// svelte-ignore state_referenced_locally
let max = $state(initial.max);
// svelte-ignore state_referenced_locally
let size = $state(initial.size);
// svelte-ignore state_referenced_locally
let shape = $state(initial.shape);
// svelte-ignore state_referenced_locally
let orientation = $state(initial.orientation);
// svelte-ignore state_referenced_locally
let valueText = $state(initial.valueText);
// svelte-ignore state_referenced_locally
let showLabel = $state(initial.label ?? false);
let labelId = $state('audit-progress-label');
onMount(() =>
onControls({
value: (next) => (value = next),
min: (next) => (min = next),
max: (next) => (max = next),
size: (next) => (size = next),
shape: (next) => (shape = next),
orientation: (next) => (orientation = next),
valueText: (next) => (valueText = next),
label: (next) => (showLabel = next),
labelId: (next) => (labelId = next)
})
);
</script>
{#snippet customValue(info: ProgressValueTextSnippetProps)}
<span data-audit-value-snippet=""
>{info.value}|{info.min}|{info.max}|{info.state}|{info.percentage}|{info.valueText ??
'none'}</span
>
{/snippet}
<div
id="audit-progress-stage"
dir={initial.dir ?? 'ltr'}
data-motion={initial.reduce ? 'reduce' : undefined}
style="padding:24px;font:16px Arial,sans-serif"
>
<div style="max-inline-size:420px">
{#if initial.externalLabel}<p id="audit-progress-external">External task</p>{/if}
<Progress
id="audit-progress"
class="audit-progress"
data-probe="progress"
title="Audit Progress"
{value}
{min}
{max}
{size}
{shape}
{orientation}
{valueText}
aria-label={initial.ariaLabel}
aria-labelledby={initial.externalLabel ? 'audit-progress-external' : undefined}
style={initial.style}
>
{#if showLabel}<Progress.Label id={labelId}>Import account</Progress.Label>{/if}
{#if initial.valuePart !== false}<Progress.ValueText
id="audit-progress-value"
children={initial.valueSnippet ? customValue : undefined}
/>{/if}
{#if initial.indicatorPart !== false}<Progress.Indicator id="audit-progress-indicator" />{/if}
</Progress>
</div>
</div>

@ -0,0 +1,56 @@
import { mount, unmount, flushSync } from 'svelte';
import { page } from 'vitest/browser';
import { createActiveUix } from '$active-uix';
import '$uix/eidos/index.css';
import Harness, { type Config, type Controls } from './progress-audit-harness.svelte';
export function renderProgress(initial: Config = {}) {
const uix = createActiveUix({
langs: { schema: {}, defaultLocale: 'en' },
prefs: { storage: false },
projectPrefs: false
});
const target = document.createElement('div');
document.body.append(target);
let controls!: Controls;
const component = mount(Harness, {
target,
props: { uix, initial, onControls: (value) => (controls = value) }
});
flushSync();
const ready = async () =>
new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
);
return {
uix,
target,
controls: () => controls,
root: () => target.querySelector<HTMLElement>('#audit-progress')!,
label: () => target.querySelector<HTMLElement>('[data-progress-label]'),
valueText: () => target.querySelector<HTMLElement>('#audit-progress-value'),
indicator: () => target.querySelector<HTMLElement>('#audit-progress-indicator'),
ready,
async capture(name: string) {
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
await ready();
await page.screenshot({ path: 'tmp/progress-' + name + '.png' });
}
},
async dispose() {
await unmount(component);
uix.dispose();
target.remove();
}
};
}
export type View = ReturnType<typeof renderProgress>;
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
await page.viewport(initial.viewportWidth ?? 1100, 900);
const view = renderProgress(initial);
try {
await view.ready();
await fn(view);
} finally {
await view.dispose();
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.