diff --git a/docs/process/HANDOFF-uix-quality-2026-09-30.md b/docs/process/HANDOFF-uix-quality-2026-09-30.md index 638be0d88..341cea446 100644 --- a/docs/process/HANDOFF-uix-quality-2026-09-30.md +++ b/docs/process/HANDOFF-uix-quality-2026-09-30.md @@ -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 diff --git a/docs/process/HANDOFF-uix-quality-2026-10-01.md b/docs/process/HANDOFF-uix-quality-2026-10-01.md index a0cb217ce..231a835ca 100644 --- a/docs/process/HANDOFF-uix-quality-2026-10-01.md +++ b/docs/process/HANDOFF-uix-quality-2026-10-01.md @@ -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 diff --git a/docs/process/PLAN-uix-quality-2026-09-19.md b/docs/process/PLAN-uix-quality-2026-09-19.md index de03fd1b6..be5c9d25d 100644 --- a/docs/process/PLAN-uix-quality-2026-09-19.md +++ b/docs/process/PLAN-uix-quality-2026-09-19.md @@ -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. diff --git a/docs/process/audit-uix-2026-09-19/findings-register.md b/docs/process/audit-uix-2026-09-19/findings-register.md index 22a3d6515..90ab8bef4 100644 --- a/docs/process/audit-uix-2026-09-19/findings-register.md +++ b/docs/process/audit-uix-2026-09-19/findings-register.md @@ -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. diff --git a/docs/process/audit-uix-2026-09-19/progress-meter.md b/docs/process/audit-uix-2026-09-19/progress-meter.md index 781015672..3405fb3ab 100644 --- a/docs/process/audit-uix-2026-09-19/progress-meter.md +++ b/docs/process/audit-uix-2026-09-19/progress-meter.md @@ -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. diff --git a/docs/process/audit-uix-2026-09-19/progress-native.md b/docs/process/audit-uix-2026-09-19/progress-native.md new file mode 100644 index 000000000..82a9475d3 --- /dev/null +++ b/docs/process/audit-uix-2026-09-19/progress-native.md @@ -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. diff --git a/src/uix/eidos/components/progress/README.md b/src/uix/eidos/components/progress/README.md index 96b4f0ee3..9df05a471 100644 --- a/src/uix/eidos/components/progress/README.md +++ b/src/uix/eidos/components/progress/README.md @@ -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. diff --git a/src/uix/eidos/components/progress/progress-native.svelte.test.ts b/src/uix/eidos/components/progress/progress-native.svelte.test.ts new file mode 100644 index 000000000..1da283142 --- /dev/null +++ b/src/uix/eidos/components/progress/progress-native.svelte.test.ts @@ -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[0]>[0]) => + getComputedStyle(view.indicator()!); +const fill = (view: Parameters[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 } + )); diff --git a/src/uix/soma/components/progress/README.md b/src/uix/soma/components/progress/README.md index 20ef26bb9..9dc6ef9b0 100644 --- a/src/uix/soma/components/progress/README.md +++ b/src/uix/soma/components/progress/README.md @@ -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 diff --git a/src/uix/soma/components/progress/types.ts b/src/uix/soma/components/progress/types.ts index f52e4b637..a7ed1238c 100644 --- a/src/uix/soma/components/progress/types.ts +++ b/src/uix/soma/components/progress/types.ts @@ -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 `
` with the full ARIA valuenow/min/max - * triplet. Supports indeterminate mode (`value=null`) for unknown-duration + * Renders `
` 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; diff --git a/src/uix/test/progress/progress-audit-harness.svelte b/src/uix/test/progress/progress-audit-harness.svelte new file mode 100644 index 000000000..bae87abb2 --- /dev/null +++ b/src/uix/test/progress/progress-audit-harness.svelte @@ -0,0 +1,126 @@ + + + + +{#snippet customValue(info: ProgressValueTextSnippetProps)} + {info.value}|{info.min}|{info.max}|{info.state}|{info.percentage}|{info.valueText ?? + 'none'} +{/snippet} +
+
+ {#if initial.externalLabel}

External task

{/if} + + {#if showLabel}Import account{/if} + {#if initial.valuePart !== false}{/if} + {#if initial.indicatorPart !== false}{/if} + +
+
diff --git a/src/uix/test/progress/progress-audit-view.ts b/src/uix/test/progress/progress-audit-view.ts new file mode 100644 index 000000000..5147e4cae --- /dev/null +++ b/src/uix/test/progress/progress-audit-view.ts @@ -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((resolve) => + requestAnimationFrame(() => requestAnimationFrame(() => resolve())) + ); + return { + uix, + target, + controls: () => controls, + root: () => target.querySelector('#audit-progress')!, + label: () => target.querySelector('[data-progress-label]'), + valueText: () => target.querySelector('#audit-progress-value'), + indicator: () => target.querySelector('#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; +export async function visit(fn: (view: View) => Promise, initial: Config = {}) { + await page.viewport(initial.viewportWidth ?? 1100, 900); + const view = renderProgress(initial); + try { + await view.ready(); + await fn(view); + } finally { + await view.dispose(); + } +}