test(uix): audit CardGroup native behavior

astra
dev 1 week ago
parent b93e13c7d1
commit 83e00360a6

@ -6,17 +6,18 @@ authority: process — checkpoint de continuidad, no canon
status: reviewed
---
# Retomar la fase 6 UIX desde CardGroup
# Continuidad de fase 6 UIX tras CardGroup
Workspace G:\dev\svelte\vicen, rama astra, destino autorizado gita/astra.
El usuario pidió cerrar el lote de Card y preparar este handoff para empezar
en otra sesión por el tamaño del contexto. Esta sesión termina tras publicar
Card: **CardGroup no tiene baseline ni modificaciones**. Al retomar, leer este
documento y comprobar HEAD, gita/astra y estado local; el hash del propio
cierre se obtiene con git log, sin introducir una referencia autorreferente.
Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente. El censo de fichas
reviewed es 168; no equivale a cobertura de 168 componentes del catálogo.
El usuario pidió «lee el handoff ultimo y continua» y esta sesión auditó CardGroup.
Base HEAD/gita/astra b93e13c7d, Card publicado. CardGroup tiene ficha reviewed,
baseline y promoción verificadas y segunda gate integral pasada. Confirmar
publicación con git log, ack remoto y push-exit.txt.
Comprobar git log, gita/astra y estado local al retomar; el hash del cierre se
obtiene sin introducir una referencia autorreferente.
Fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente. El censo de continuidad
reviewed es 169; no equivale a cobertura del catálogo.
La continuidad sigue siendo una auditoría neutral: no arreglar los defectos
diferidos dentro de este lote ni cerrar globalmente la fase 6.
@ -88,74 +89,54 @@ y retries conservados. Es un fallo observado en suite completa, sin causa
demostrada; no modificar Tooltip, aserciones, timeout ni hooks en la auditoría
neutral. Result: gate/retry 606/7520; EmptyState: 605/7483.
## Próximo lote: CardGroup
Sólo hubo lecturas y preparación en memoria durante la gate de Card.
**No hay baseline, PNG, fingerprint, promoción ni nueva ficha CardGroup.**
Las observaciones siguientes son pistas de fuente, no hallazgos nativos
confirmados ni IDs nuevos. La memoria de herramientas de la sesión anterior
no debe ser requisito para continuar.
Antes de crear/modificar cualquier componente o helper de auditoría, volver
a leer completas las cuatro guías obligatorias:
1. docs/guides/component-audit.md.
2. docs/guides/demo-authoring.md.
3. src/uix/eidos/components/README.md.
4. docs/building-a-component.md.
Leer src/uix/eidos/components/card-group/ completo (root, Title, Description,
Content, Item, context, types, index, CSS, README), Morfo en
src/uix/morfo/components/card-group.ts, Langs en
src/uix/langs/components/card-group.ts y receta card-group en
src/uix/eidos/lib/recipes/base.ts (20 claves públicas).
Completar Soma ToggleGroup y Collapsible, sus proveedores, libs/selection,
Sema y los helpers/roots reales antes de diseñar las pruebas.
Cuatro referencias primarias leídas en esta sesión para preparar la matriz:
- [Radix Themes RadioCards](https://www.radix-ui.com/themes/docs/components/radio-cards):
single choice, root basado en RadioGroup, columns/size y disabled.
- [Chakra CheckboxCard](https://chakra-ui.com/docs/components/checkbox-card):
composición CheckboxGroup, HiddenInput, label/description/indicator y fieldset/legend.
- [Ark ToggleGroup](https://ark-ui.com/docs/components/toggle-group):
valor array, multiple/deselectable, roving focus, orientation y teclado.
- [Bits ToggleGroup](https://bits-ui.com/docs/components/toggle-group):
type single/multiple, valor acorde, roving focus/loop y composición child/ref.
Reverificar en la nueva sesión y añadir APG Disclosure para el eje toggleable.
No dar por existente un «Chakra CardGroup» por el comparador del README.
Crear la tabla Comparativa y las Decisiones propias antes de la primera
baseline. La búsqueda de Air en morfo-runtime sólo encontró card-sim del
iconlib, ningún componente Card/CardGroup; comprobar la rama al retomar.
Ownership observado: Morfo CardGroup scope eidos, cero eventos propios.
La selección pertenece a ToggleGroup (commit-toggle/block), el disclosure
a Collapsible (emerge-expand/collapse). Contexto de grupo sólo getters;
CardGroup.Item usa identidad/receta Card. Langs sí contiene el label y está
registrado, aunque los wrappers no lo consumen.
Pistas que requieren controles nativos y adjudicación antes de registrar:
- Root pasa disabled a ToggleGroup.Provider, pero no a Collapsible cuando
toggleable. El README reconoce el hueco; contrastar composición real antes
de atribuirlo al wrapper Collapsible.
- Title y Content propagan rest en rama estática; la rama toggleable parece
omitirlo. Comparar atributos públicos con contraparte estática/HTML válida.
- Title estático es DIV; no asumir heading. Un id consumidor puede sustituir
titleId, mientras la raíz conserva aria-labelledby por defecto; distinguir
responsabilidad del consumidor y referencias automáticas.
- Item escribe su data-disabled local después de props de ToggleGroup.Item.
Comprobar disabled heredado frente a local, DOM nativo/ARIA y atenuación
visual, sin confundir filtro con opacity.
- minChildWidth explícito usa minmax(MIN,1fr); el fallback CSS sí limita el
mínimo al 100%. Medir el ancho estrecho con control CSS/HTML válido.
- Item no importa card.css. Una harness que importe Card puede ocultar una
dependencia de carga; comprobar Eidos index.css y aislar la prueba bare
para que otras importaciones no contaminen la conclusión.
- Revisar README: ejemplo antiguo select frente a enabledSelections,
claims fechadas de theming y nombre antiguo de block-shake. No reparar
runtime ni cambiar API como parte de conciliación documental.
## CardGroup cerrado
[Ficha reviewed](./audit-uix-2026-09-19/card-group-native.md). Baseline única:
**25 positivos/23 fallos, ocho AssertionError/quince TypeError, 48 nombres únicos,
cero pendientes**. Selector original asumía ID consumidor conservado y explica
catorce TypeError de selección, Tab y TypeError disabled; no certifican bugs
de interacción. Sólo los 25 originales passing promovidos, huella exacta.
48 PNG inspeccionados individualmente/SHA: 46 a 1100×900, dos a 350×900 y uno RTL.
Originales intactos, sin freeze/recaptura/calibración. Evidencia local ignorada:
tmp/card-group-audit-evidence-2026-10-01/.
Additional 22/23, geometry 1/3 y motion-control 0/2 permanecen en tmp.
Selection/foco con data-value pasa; las expectativas originales none/300ms
incorrectas se conservan y no promueven. El grupo sí mantiene scale-in/fade-in
frente a fade-in válido del control directo reducido, duración real 240ms.
Q329–Q336 y Q313 diferidos; TitleID custom requiere root labelledby correspondiente,
sin nuevo ID de defecto. Sólo README de producción conciliado, cero extracción.
Fuentes ejecutables y API intactas: 57 comparadas con b93e13c7d; cinco fuentes
originales y los 48 PNG/SHA verificados. Dirigida **86/86**.
Guard **PASS/cero errores/tres avisos frozen**: D-1.2/D-1.3/D-4.3 triados.
Gate integral exit 0 en el segundo intento; censo de continuidad 169. Consultar ficha para adjudicación
y límites; no reparar runtime en esta tanda.
Primera gate exit 1: 607 archivos passing/uno fallido, 7586 pruebas passing/una
fallida en Toast focus-pause, línea 282 (Item null/foco exterior). Dirigida
Toast 34/34 sin cambios de código, fixtures, aserciones ni tiempos. Logs/códigos
original y dirigida conservados separados; retry integral exit 0.
Segunda gate integral **exit 0: 608 archivos/7587 pruebas**, docs 985/arts 25.
check:gate conserva 89 históricos/cero nuevos src/scripts; apps/base check/build
y smoke foundation 1/1 + boot 27/27 pasan, sin pageerror, console.error ni CSP.
Guard CardGroup PASS/cero errores/tres avisos frozen triados. Censo de continuidad
169 reviewed; fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente.
Primer fallo Toast y dirigida 34/34 intactos; retry sin cambios de código,
fixtures, aserciones ni tiempos. Logs y códigos de cada intento separados.
Publicación: comprobar git log, ack remoto y push-exit.txt; no PR solicitado.
## Próximo candidato: ButtonGroup
El censo actual no tiene ficha propia ButtonGroup; Group ya lo ejercita como
control vertical, lo que no equivale a una auditoría propia. No hay baseline,
PNG ni modificaciones de ButtonGroup en este lote. Antes de crear helpers,
leer completas docs/guides/component-audit.md, docs/guides/demo-authoring.md,
src/uix/eidos/components/README.md y docs/building-a-component.md, su corpus
y owners/raíces reales,
completar la matriz de cuatro referencias aplicables y las decisiones propias.
Mantener selección neutral y el guard del componente; no reutilizar passing de
Group como baseline de ButtonGroup.
## Disciplina y operaciones
@ -175,13 +156,15 @@ Pistas que requieren controles nativos y adjudicación antes de registrar:
negativos de boot no implican fallo de gate: comprobar código final.
- Promociones/cierres ya ejecutados son no idempotentes: no repetir
promote-neutral.mjs, reconcile-docs.mjs, prepare-gate.mjs ni cierres de
lotes anteriores. verify-neutral.mjs es repetible, pero escribe diagnóstico
lotes anteriores. También están ejecutados prepare-additional.mjs,
promote-neutral.mjs, reconcile-readme.mjs, reconcile-progress.mjs y close-neutral.mjs
de CardGroup; no repetirlos. verify-neutral.mjs es repetible, pero escribe diagnóstico
en tmp y por tanto tampoco se ejecuta durante una gate viva.
- Evidencia ignorada reciente:
tmp/{progress,meter,banner,callout,empty-state,result,card}-audit-evidence-2026-10-01/.
tmp/{progress,meter,banner,callout,empty-state,result,card,card-group}-audit-evidence-2026-10-01/.
Los handoffs previos enumeran el resto. Evidencia ignored permanece local;
otro checkout necesita conservarla explícitamente.
- Para cerrar, component:audit --only card debe decir PASS y cero errores
- Para cerrar, component:audit --only card-group debe decir PASS y cero errores
(exit 0 aislado no basta), docs:check limpio, diff --check y stage explícito.
Conservar firma/hooks y publicar gita/astra con hook integral.
- La siguiente sesión debe recibir una instrucción del usuario para retomar;

@ -4305,3 +4305,48 @@ Result publicado en b889d0daa: primer hook falló en Tooltip forceMount,
dirigida sin cambios 50/50 y retry integral 606 archivos/7520 pruebas,
exit 0 con ack remoto. Logs originales/retry conservados; no se modifica
Tooltip, ninguna aserción ni hooks.
### CardGroup — baseline y conciliación neutral (2026-10-01)
[Ficha](./audit-uix-2026-09-19/card-group-native.md) reviewed. Baseline única:
25 positivos/23 fallos (ocho AssertionError/quince TypeError), 48 nombres únicos,
cero pendientes. El selector original asumía conservado el ID de Item seleccionable:
catorce TypeError de selección, una aserción Tab y un TypeError disabled no
acreditan defectos de interacción. Fuentes/log/JSON/primer diagnóstico intactos.
Sólo los 25 passing originales promovidos; no se recalibran ni recapturan.
48 originales inspeccionados individualmente/archivados/SHA: 46 a 1100×900,
dos a 350×900 y uno RTL. Additional separado 22/23: control de selección/foco
con data-value y cuatro controles positivos; cuatro variantes repetidas no
se cuentan como originales. Geometry 1/3 confirma overflow; motion-control 0/2
compara fade-in directo con scale-in/fade-in del grupo y luego falla por una
duración supuesta de 300ms frente a 240ms. Expectativas none/300ms incorrectas
se conservan, sin atribuir defecto al fade válido ni promover sondas extra.
Q329–Q336 difieren disabled disclosure, passthrough toggleable, ID seleccionable,
atenuación heredada, mínimo explícito estrecho, carga de receta Card, altura de
Item pasivo y reduced cascade. Q313 amplía concatenación sin terminador.
Naming al personalizar TitleID requiere root aria-labelledby correspondiente,
documentado como obligación del consumidor, sin nuevo defecto inventado.
Sólo README de producción conciliado; wrappers/tipos/Morfo/Langs/CSS/receta/API
intactos, cero extracción. Guard PASS/cero errores/tres avisos frozen triados:
D-1.2 tabs, D-1.3 compileMorfo ausente y D-4.3 Play sin emit; web intacto.
Dirigida 86/86; huella exacta de los 25 positivos, cinco fuentes originales,
57 fuentes previas y 48 originales/SHA/dimensiones intactos. Gate integral
exit 0 en el segundo intento; censo de continuidad 169 reviewed. Fase 6 abierta, fases 0–5 sin cerrar, fase 7
pendiente. AT/touch/oscuro/contraste/SSR/otros motores no acreditados.
Primera gate exit 1: 607 archivos passing/uno fallido, 7586 pruebas passing/una
fallida en Toast focus-pause, línea 282 (Item null/foco exterior). Dirigida
Toast 34/34 sin cambios de código, fixtures, aserciones ni tiempos. Logs/códigos
original y dirigida conservados separados; retry integral exit 0.
Segunda gate integral **exit 0: 608 archivos/7587 pruebas**, docs 985/arts 25.
check:gate conserva 89 históricos/cero nuevos src/scripts; apps/base check/build
y smoke foundation 1/1 + boot 27/27 pasan, sin pageerror, console.error ni CSP.
Guard CardGroup PASS/cero errores/tres avisos frozen triados. Censo de continuidad
169 reviewed; fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente.
Primer fallo Toast y dirigida 34/34 intactos; retry sin cambios de código,
fixtures, aserciones ni tiempos. Logs y códigos de cada intento separados.
Publicación: comprobar git log, ack remoto y push-exit.txt; no PR solicitado.

@ -0,0 +1,145 @@
---
title: UIX — CardGroup, selección y disclosure por composición
type: notes
audience: human + agent
authority: process — auditoría neutral, no contrato nuevo
status: reviewed
---
# CardGroup — continuación nativa
Fecha: 2026-10-01. Alcance autorizado por «lee el handoff ultimo y continua».
Las cuatro guías obligatorias se leyeron completas antes de escribir helpers.
Base HEAD/gita/astra b93e13c7d, Card publicado. Evidencia local ignorada en
tmp/card-group-audit-evidence-2026-10-01/. No se modifica web.
## Comparación y propiedad
| Aspecto | [Radix RadioCards](https://www.radix-ui.com/themes/docs/components/radio-cards) | [Chakra CheckboxCard](https://chakra-ui.com/docs/components/checkbox-card) | [Ark ToggleGroup](https://ark-ui.com/docs/components/toggle-group) | [Bits ToggleGroup](https://bits-ui.com/docs/components/toggle-group) | Decisión UIX |
| ---------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Selección | RadioGroup single | CheckboxGroup multiple | Array, multiple/deselectable | Single string/multiple array | Conservar enabledSelections y array; no añadir inputs ni semántica radio. |
| Anatomía | Root/Item | HiddenInput, Control, Content, Label, Description, Indicator | Root/Item | Root/Item, child/ref | Cinco wrappers; presencia controla partes. |
| Grid/talla | Responsive columns, tres tallas | sm/md/lg | Consumidor | Consumidor | Group sm/md/lg; minChildWidth precede columns. |
| Teclado/disabled | RadioGroup/Item disabled | Checkbox disabled | Roving, loop, orientation | Roving, loop, orientation | ToggleGroup nativo; contrastar local/heredado sin nueva máquina. |
Fuentes primarias verificadas de nuevo, con
[APG Disclosure](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) para
button/expanded/Enter/Space. No se afirma un supuesto Chakra CardGroup.
Air morfo-runtime sólo contiene iconos card, no CardGroup.
Leídos cinco wrappers, contexto, types/index/CSS/README, Morfo/Langs y receta
de 20 claves, helpers de color/style/layout, ToggleGroup y Collapsible con sus
providers/partes/tipos/Morfo/Sema, selección pura y raíces reales. Harness
createActiveUix/Soma.create/ActiveEidos.create; cero servicios simulados.
Morfo CardGroup scope eidos, cero eventos propios justificados: ToggleGroup
contact pre/commit-toggle post y commit-block risk al Item; Collapsible emerge
post a Content, Title Button. Contexto sólo getters; Item añade identidad Card.
Langs label registrado pero no consumido. Consumidor posee copy/nombre,
acciones válidas en Items pasivos y desmontaje. No ref bindable propio.
## Primera ejecución y límites del instrumento
**Baseline única: 25 positivos, 23 fallos, 48 nombres únicos, cero pendientes.**
Ocho AssertionError y quince TypeError. Catorce TypeError de selección y la
aserción Tab se originan en un selector que asumía conservado el ID consumidor;
el wrapper seleccionable lo reemplaza. La sonda disabled heredado sufre el
mismo TypeError. No se presentan esos fallos como defectos de click/foco.
La responsabilidad del instrumento queda explícita. Fuentes/JSON/log/huella
y primer diagnóstico originales completos conservados sin reescribir.
Sólo los 25 originales positivos se promueven con la misma huella de fullName,
status y failureMessages. Cubren default/partes, depth, sm/md/lg, cuatro variantes
con receta Card explícita, radios concéntricos, columnas/minimum reactivos y
responsive, disclosure click/Enter/Space, atributos estáticos/copy,
nombres explícitos, vacío, tokens, fallback350, acción pasiva y desmontaje.
Passing certifica sus aserciones; no convierte el overflow visible en correcto.
**48 PNG originales inspeccionados individualmente/archivados/SHA: 46 a
1100×900, dos a 350×900 y uno RTL.** Bare aislado sin Card/receta antes de
capturar; después carga card.css como control en ese mismo caso. Ninguna
recaptura, freeze ni animación neutralizada. Screenshots automáticos de fallo
fuera del censo manual; notas individuales en image-inspection.json.
Pasivos soft pálidos; solid teal saturado/blanco, outline borde teal y ghost
sólo tinta. Raised sombra, recessed gris/inset, flush sin cromo. sm permite
tres columnas a820, md/lg dos; responsive vuelve a tres. Fallback350 envuelve
prosa y cabe horizontalmente. Pasivos con receta sobresalen debajo del grupo
y tapan After; toggleable los recorta. Selectables button border-box caben.
Local disabled atenúa B, heredado deja todas a plena fuerza. RTL revierte
columnas/alineación. Bare concatena strong/span sin padding. Disclosure cerrado
conserva Title/Description y oculta grid; teclado muestra foco y una llamada.
Empty/removal no inventan copy. Las imágenes originales fallidas de selección
no certifican la interacción que el selector impidió ejecutar.
## Sondas adicionales separadas
Selección con data-value: additional.json **22 positivos/una AssertionError**,
23 nombres. Cuatro variantes se repiten por el filtro del helper; no se suman
como originales nuevos ni se promueven. Confirma single replace/clear/callbacks,
external value/ring, cap/reject al target exacto, replace-oldest, required floor,
Enter/Space, roving/skip/Home/End/loop, RTL, Tab, disabled funcional, external
open/selection retention y cascade. Cuatro controles separan inherited paint,
ID, style terminado y naming explícito.
La aserción reduce esperaba animationName none y recibe scale-in/fade-in.
geometry.json conserva **un positivo/dos AssertionError**: geometría confirma
overflow; controles reduced esperaban none pero el preset reducido válido
mantiene fade-in. No acredita que todo fade sea un defecto. motion-control.json
conserva **dos AssertionError**: comparaciones de nombres pasan (control fade-in
frente a grupo scale-in/fade-in), luego falla la expectativa incorrecta de
300ms frente a **240ms reales**. Fuentes/aserciones intactas por ejecución, sin
recalibración/promoción ni capturas manuales nuevas. Q336 se adjudica al scale
que conserva el grupo frente al control y los selectores generados; no al fade
válido ni al valor temporal supuesto.
## Hallazgos y disposición
| ID | Evidencia nativa | Disposición |
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Q329 | disabled root no llega a disclosure; Title cierra mientras peer Collapsible disabled está disabled. | Diferir forwarding del root; Eidos Collapsible sí propaga rest/disabled. |
| Q330 | Title/Content toggleable pierden atributos consumidor que estáticos conservan. | Diferir passthrough de ramas con composición. |
| Q331 | Item seleccionable cambia audit-cg-a por ID del provider; data-probe/data-value sí llegan. | Diferir precedencia compatible con registro/foco; no atribuir fallos del selector a interacción. |
| Q332 | Item inherited native disabled true, sin data-disabled/filter none; peer mantiene marker y local B atenúa. | Diferir effective disabled visual. |
| Q333 | Minimum400/viewport350: scrollWidth416/clientWidth300; control clamped cabe. | Diferir límite explícito estrecho. |
| Q334 | Bare Item block/padding0; misma instancia tras cargar card.css flex/padding20. | Diferir dependencia de receta de composición. |
| Q335 | Pasivo content-box: grid bottom339.5, item365.5, grupo340.5; control auto-height item555.5/grid563.5. | Diferir block-size100% y box sizing preservando equal-height. |
| Q336 | Scale-fade parent mantiene scale-in/fade-in240ms bajo reduce; preset directo open sólo fade-in; passive sin estado/selectable on-off no casan kill. | Diferir reducción de cascada compartida. |
| Q313 | Style padding7 sin ; absorbe semilla custom; control terminado conserva semilla/padding7. | Extender concatenación compartida. |
Sonda TitleID: sustituir el ID estático deja la referencia automática sin destino;
root aria-labelledby explícito al mismo ID resuelve. Se documenta obligación
del consumidor al personalizar la referencia; no se introduce otro ID de defecto
ni se promete seguimiento de overrides. Title DIV sin heading queda decisión
de diseño diferida, no defecto nuevo de selección.
## Conciliación y cierre
Sólo README de producción conciliado: comparación primaria, disabled/attrs/IDs/
naming, carga de receta, block-shake, contact/ownership, altura/reduced y límites.
Índice conserva comentario select obsoleto señalado en README. Wrappers/tipos/
Morfo/Langs/CSS/receta/API intactos; cero extracción, defectos diferidos sin reparar.
Guard previo PASS/cero errores/tres avisos: D-1.2 tabs, D-1.3 compileMorfo ausente
y D-4.3 Play sin emit, todos de demo frozen. No se edita web para resolverlos.
Dirigida CardGroup/Card/ToggleGroup/Collapsible **86/86**, huella exacta de los
25 positivos; cinco fuentes originales, 57 fuentes ejecutables previas frente
a b93e13c7d y 48 originales/SHA/dimensiones verificados intactos.
Gate integral cerrada en el segundo intento; censo de continuidad 169 reviewed. Fase 6 abierta; fases 0–5 sin cerrar, fase 7 pendiente.
AT/touch/oscuro/contraste/SSR/otros motores no acreditados.
Primera gate integral exit 1: 607 archivos passing/uno fallido, 7586 pruebas
passing/una fallida. Toast `native item focus pauses auto-dismiss until native
blur`, línea 282: Item ya null frente a foco en el botón exterior. Dirigida
Toast **34/34** sin cambiar producción, fixtures, aserciones ni tiempos.
gate.log/gate-exit.txt y toast-directed.log/JSON/exit conservados separados.
Retry integral completo exit 0; no se atribuye una causa no demostrada al fallo.
Segunda gate integral **exit 0: 608 archivos/7587 pruebas**, docs 985/arts 25.
check:gate conserva 89 históricos/cero nuevos src/scripts; apps/base check/build
y smoke foundation 1/1 + boot 27/27 pasan, sin pageerror, console.error ni CSP.
Guard CardGroup PASS/cero errores/tres avisos frozen triados. Censo de continuidad
169 reviewed; fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente.
Primer fallo Toast y dirigida 34/34 intactos; retry sin cambios de código,
fixtures, aserciones ni tiempos. Logs y códigos de cada intento separados.
Publicación: comprobar git log, ack remoto y push-exit.txt; no PR solicitado.

@ -52,21 +52,33 @@ exterior). Amplía Q313 con style sin terminador. Los 32 positivos promovidos
conservan huella exacta; 39 originales inspeccionados/SHA. Gate integral exit 0:
604 archivos/7450 pruebas; censo 165 reviewed.
| 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/Meter | Q315 | Adjudicar rechazo, normalización o saturación de entradas; cubrir ambos consumidores, 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. |
| Paleta tertiary de Banner | Q318 | Adjudicar completar receta o restringir tipo; cubrir las cuatro variantes, responsive y retintado antes de cambiar producción. |
| Nombre externo de Callout | Q319 | Adjudicar precedencia de aria-labelledby explícito, roles y ciclo de Title; mantener el nombre automático válido. |
| ID público de Title | Q320 | Respetar o restringir id con compatibilidad explícita; comprobar montaje, actualización y registro. |
| Unicidad de Titles | Q321 | Adjudicar múltiples títulos, IDs y registro/cleanup; cubrir peers y anidamiento. |
| Icon propio frente a anidado | Q322 | Limitar selectores a las partes propias y comprobar combinaciones de Icon exterior/interior y RTL. |
| ARIA explícita de Actions | Q323 | Adjudicar precedencia de role/aria-label frente a label; preservar grupos nombrados y default plain, cubrir mutaciones. |
| Contención de media libre | Q324 | Decidir responsabilidad de parte/consumidor y sizing, preservar slots libres y alt; probar ilustraciones grandes y viewport estrecho. |
| 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/Meter | Q315 | Adjudicar rechazo, normalización o saturación de entradas; cubrir ambos consumidores, 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. |
| Paleta tertiary de Banner | Q318 | Adjudicar completar receta o restringir tipo; cubrir las cuatro variantes, responsive y retintado antes de cambiar producción. |
| Nombre externo de Callout | Q319 | Adjudicar precedencia de aria-labelledby explícito, roles y ciclo de Title; mantener el nombre automático válido. |
| ID público de Title | Q320 | Respetar o restringir id con compatibilidad explícita; comprobar montaje, actualización y registro. |
| Unicidad de Titles | Q321 | Adjudicar múltiples títulos, IDs y registro/cleanup; cubrir peers y anidamiento. |
| Icon propio frente a anidado | Q322 | Limitar selectores a las partes propias y comprobar combinaciones de Icon exterior/interior y RTL. |
| ARIA explícita de Actions | Q323 | Adjudicar precedencia de role/aria-label frente a label; preservar grupos nombrados y default plain, cubrir mutaciones. |
| Contención de media libre | Q324 | Decidir responsabilidad de parte/consumidor y sizing, preservar slots libres y alt; probar ilustraciones grandes y viewport estrecho. |
| Disclosure disabled de CardGroup | Q329 | Adjudicar forwarding de disabled al disclosure conservando selección y control externo; cubrir Title click/Enter/Space frente a Collapsible directo. |
| Atributos toggleable de partes | Q330 | Conservar passthrough de Title/Content con precedencia explícita y paridad estática; probar atributos, handlers y referencias. |
| ID seleccionable de CardGroup.Item | Q331 | Resolver ID consumidor frente al generado manteniendo registro, foco, ARIA y actualización reactiva. |
| Atenuación disabled heredada | Q332 | Proyectar disabled efectivo al marcador visual sin alterar bloqueo nativo; cubrir root/local y combinaciones reactivas. |
| Minimum estrecho de CardGroup | Q333 | Decidir clamp del mínimo explícito a viewport estrecho manteniendo intención, columnas y cambios responsive. |
| Carga de receta Card compuesta | Q334 | Adjudicar import de receta o dependencia explícita; comprobar consumidor aislado sin Card/foundation y chunks antes de cambiar carga. |
| Altura de Item pasivo | Q335 | Corregir contención de border/padding manteniendo equal-height y variantes; medir filas, disclosure, selección y contenido heterogéneo. |
| Cascada bajo reduced motion | Q336 | Adjudicar eliminación de escala para hijos sin estado open; cubrir pasivos, on/off seleccionables y consumidores del generador compartido. |
CardGroup añade Q329–Q336 y amplía Q313. Los controles nativos y los errores
del instrumento se distinguen en la [ficha](./card-group-native.md); las
correcciones siguen diferidas hasta después de la auditoría.
Q34 se reprodujo con el proveedor Sticky y geometría real en Chromium.
Q35–Q36 se reprodujeron con el wrapper SkipLink montado en Chromium.
@ -3294,3 +3306,60 @@ Tooltip, ninguna aserción ni hooks.
| Q327 | selected true→false deja aria-pressed null e idle; control HTML conserva false. | Diferir continuidad toggle sin convertir botón ordinario en toggle. |
| Q328 | Description/Footer soft anidados bajo solid tienen tinta alpha .8/.18; peer mide oklch(0.5032 0 0)/oklch(0.931 0 0). | Diferir alcance del owner cercano de reglas solid. |
| Q313 | Style sin ; absorbe semilla/inline-size y pierde padding; controles terminados conservan semilla/seis dimensiones. | Extender concatenación compartida sin parche local. |
### CardGroup — baseline y conciliación neutral (2026-10-01)
[Ficha](./card-group-native.md) reviewed. Baseline única:
25 positivos/23 fallos (ocho AssertionError/quince TypeError), 48 nombres únicos,
cero pendientes. El selector original asumía conservado el ID de Item seleccionable:
catorce TypeError de selección, una aserción Tab y un TypeError disabled no
acreditan defectos de interacción. Fuentes/log/JSON/primer diagnóstico intactos.
Sólo los 25 passing originales promovidos; no se recalibran ni recapturan.
48 originales inspeccionados individualmente/archivados/SHA: 46 a 1100×900,
dos a 350×900 y uno RTL. Additional separado 22/23: control de selección/foco
con data-value y cuatro controles positivos; cuatro variantes repetidas no
se cuentan como originales. Geometry 1/3 confirma overflow; motion-control 0/2
compara fade-in directo con scale-in/fade-in del grupo y luego falla por una
duración supuesta de 300ms frente a 240ms. Expectativas none/300ms incorrectas
se conservan, sin atribuir defecto al fade válido ni promover sondas extra.
Q329–Q336 difieren disabled disclosure, passthrough toggleable, ID seleccionable,
atenuación heredada, mínimo explícito estrecho, carga de receta Card, altura de
Item pasivo y reduced cascade. Q313 amplía concatenación sin terminador.
Naming al personalizar TitleID requiere root aria-labelledby correspondiente,
documentado como obligación del consumidor, sin nuevo defecto inventado.
Sólo README de producción conciliado; wrappers/tipos/Morfo/Langs/CSS/receta/API
intactos, cero extracción. Guard PASS/cero errores/tres avisos frozen triados:
D-1.2 tabs, D-1.3 compileMorfo ausente y D-4.3 Play sin emit; web intacto.
Dirigida 86/86; huella exacta de los 25 positivos, cinco fuentes originales,
57 fuentes previas y 48 originales/SHA/dimensiones intactos. Gate integral
exit 0 en el segundo intento; censo de continuidad 169 reviewed. Fase 6 abierta, fases 0–5 sin cerrar, fase 7
pendiente. AT/touch/oscuro/contraste/SSR/otros motores no acreditados.
Primera gate exit 1: 607 archivos passing/uno fallido, 7586 pruebas passing/una
fallida en Toast focus-pause, línea 282 (Item null/foco exterior). Dirigida
Toast 34/34 sin cambios de código, fixtures, aserciones ni tiempos. Logs/códigos
original y dirigida conservados separados; retry integral exit 0.
| ID | Evidencia nativa | Disposición |
| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Q329 | disabled root no llega a disclosure; Title cierra mientras peer Collapsible disabled está disabled. | Diferir forwarding del root; Eidos Collapsible sí propaga rest/disabled. |
| Q330 | Title/Content toggleable pierden atributos consumidor que estáticos conservan. | Diferir passthrough de ramas con composición. |
| Q331 | Item seleccionable cambia audit-cg-a por ID del provider; data-probe/data-value sí llegan. | Diferir precedencia compatible con registro/foco; no atribuir fallos del selector a interacción. |
| Q332 | Item inherited native disabled true, sin data-disabled/filter none; peer mantiene marker y local B atenúa. | Diferir effective disabled visual. |
| Q333 | Minimum400/viewport350: scrollWidth416/clientWidth300; control clamped cabe. | Diferir límite explícito estrecho. |
| Q334 | Bare Item block/padding0; misma instancia tras cargar card.css flex/padding20. | Diferir dependencia de receta de composición. |
| Q335 | Pasivo content-box: grid bottom339.5, item365.5, grupo340.5; control auto-height item555.5/grid563.5. | Diferir block-size100% y box sizing preservando equal-height. |
| Q336 | Scale-fade parent mantiene scale-in/fade-in240ms bajo reduce; preset directo open sólo fade-in; passive sin estado/selectable on-off no casan kill. | Diferir reducción de cascada compartida. |
| Q313 | Style padding7 sin ; absorbe semilla custom; control terminado conserva semilla/padding7. | Extender concatenación compartida. |
Segunda gate integral **exit 0: 608 archivos/7587 pruebas**, docs 985/arts 25.
check:gate conserva 89 históricos/cero nuevos src/scripts; apps/base check/build
y smoke foundation 1/1 + boot 27/27 pasan, sin pageerror, console.error ni CSP.
Guard CardGroup PASS/cero errores/tres avisos frozen triados. Censo de continuidad
169 reviewed; fase 6 abierta, fases 0–5 sin cerrar y fase 7 pendiente.
Primer fallo Toast y dirigida 34/34 intactos; retry sin cambios de código,
fixtures, aserciones ni tiempos. Logs y códigos de cada intento separados.
Publicación: comprobar git log, ack remoto y push-exit.txt; no PR solicitado.

@ -63,19 +63,27 @@ visible to the acceptance machinery (clean-room 2026-07-10, MOR-4).
</CardGroup>
```
Item carries Card identity but does not import card.css; the foundation
entrypoint also omits this code-split recipe. Load it explicitly or through
the Card import used above. Bare passive Items have block layout/padding 0;
the same instance with card.css has flex layout/padding 20 at md.
Selectable Items are native buttons: use phrasing content and keep independent
controls in passive Items. Langs label is registered but not consumed;
consumer copy and accessible names are explicit.
The item IS the card — compose the Card **sub-parts** (`Card.Header` /
`Card.Title` / `Card.Body` / `Card.Footer` / `Card.Description`) or any content
inside `CardGroup.Item`, not a wrapping `<Card>`.
## Parts
| Part | Marker | Notes |
| ------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `CardGroup` (root) | `data-card-group` | The `ToggleGroup.Provider` (`role=group`) when selectable, else a plain `role=group` div. Carries `data-depth` / `data-size`. |
| `.Title` | `data-card-group-title` | Toggleable → `Collapsible.Trigger` rendered as the system `<Button>` (ghost) + chevron. Not toggleable → a static heading. |
| `.Description` | `data-card-group-description` | Secondary prose under the title. |
| `.Content` | `data-card-group-content` | Toggleable → `Collapsible.Content` (collapses via `hidden`); holds the `[data-card-group-grid]` (the `[data-stagger]` cascade container). |
| `.Item` | `data-card-group-item` + `data-card` | The card. Selectable → also a `ToggleGroup.Item` (`aria-pressed`, roving, sema). |
| Part | Marker | Notes |
| ------------------ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CardGroup` (root) | `data-card-group` | The `ToggleGroup.Provider` (`role=group`) when selectable, else a plain `role=group` div. Carries `data-depth` / `data-size`. |
| `.Title` | `data-card-group-title` | Toggleable → `Collapsible.Trigger` rendered as the system `<Button>` (ghost) + chevron. Not toggleable → a static DIV without heading semantics. |
| `.Description` | `data-card-group-description` | Secondary prose under the title. |
| `.Content` | `data-card-group-content` | Toggleable → `Collapsible.Content` (collapses via `hidden`); holds the `[data-card-group-grid]` (the `[data-stagger]` cascade container). |
| `.Item` | `data-card-group-item` + `data-card` | The card. Selectable → also a `ToggleGroup.Item` (`aria-pressed`, roving, sema). |
## Props (root)
@ -87,7 +95,7 @@ inside `CardGroup.Item`, not a wrapping `<Card>`.
| `value` | `string[]` (bindable) | `[]` | Selected item values (0–1 in single mode). |
| `onValueChange` | `(value: string[]) => void` | — | Selection callback. |
| `whenFull` | `'reject' \| 'replace-oldest'` | `'reject'` | At a multi cap (`max > 1`): reject the pick (fires `commit-block`) or drop the oldest. |
| `disabled` | `boolean` | `false` | Disable selection + toggle for the whole group. |
| `disabled` | `boolean` | `false` | Disable selection. Currently does not disable disclosure. |
| `depth` | `'raised' \| 'recessed' \| 'flush'` | `'raised'` | Canonical elevation (`data-depth`). |
| `size` | `ResponsiveProp<'sm' \| 'md' \| 'lg'>` | `'md'` | Padding / gap / title scale — **propagated to the cards**. |
| `minChildWidth` | `ResponsiveProp<number \| string>` | — | Fluid grid `repeat(auto-fill, minmax(MIN, 1fr))`. |
@ -96,7 +104,16 @@ inside `CardGroup.Item`, not a wrapping `<Card>`.
| `staggerEach` | `number` | `60` | Stagger rhythm (ms) for the cascade. `0` = parallel. |
`aria-label` / `aria-labelledby` are forwarded; absent an explicit value the
group is labelled by the Title.
group is labelled by the generated Title ID. Static Title is a DIV.
When overriding its ID, also provide the matching root aria-labelledby;
when omitting it, provide an explicit name. Description is not automatically
aria-describedby. External open/value assignments do not call callbacks.
Static parts preserve consumer attributes. Toggleable Title/Content drop their
rest attributes. Selectable Item preserves data/class but provider ID overrides
consumer ID. Inherited disabled preserves native disabled but loses the Item's
data-disabled and Card dim. A custom colour with string style requires a
terminating semicolon under the current shared style composer.
## Props (`.Item`)
@ -112,11 +129,11 @@ group is labelled by the Title.
CardGroup declares **no own events** — its perceptual surface is composed:
| Event | From | Family · verb · intent | When |
| ----------------------------------- | ----------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `emerge-expand` / `emerge-collapse` | Collapsible | `emerge` · expand/collapse | Title toggles the group. |
| `commit-toggle` | ToggleGroup | `commit` · toggle · neutral | A card is selected / unselected. |
| `commit-block` | ToggleGroup | `commit` · block · risk | A pick is rejected by the cardinality limit (at-max with `whenFull: 'reject'`, or below a required min). The card shakes (`card-group-block`) over the risk-tinted sound/haptic. |
| Event | From | Family · verb · intent | When |
| ----------------------------------- | ----------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `emerge-expand` / `emerge-collapse` | Collapsible | `emerge` · expand/collapse | Title toggles the group. |
| `commit-toggle` | ToggleGroup | `commit` · toggle · neutral | A card is selected / unselected. |
| `commit-block` | ToggleGroup | `commit` · block · risk | A pick is rejected by the cardinality limit (at-max with `whenFull: 'reject'`, or below a required min). The card shakes (`block-shake`) over the risk-tinted sound/haptic. |
## Accessibility
@ -150,13 +167,13 @@ every Item is [data-card] by identity.`
## Comparativa
| Library | Closest equivalent | Difference |
| ------------------ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| chakra-ui | `<CardGroup>` | Layout-only (shared size/variant). UIX adds disclosure (Collapsible) + selection (ToggleGroup). |
| ant design | `<Card.Grid>` | Static grid cells. UIX is a collapsible, selectable group of full cards. |
| radix / ark / bits | — | No CardGroup primitive; selection = a Radio/Toggle group, disclosure = Collapsible — exactly what UIX composes. |
| react-aria | `GridList` / `ListBox` | Selectable collections; cards are app-styled options. UIX promotes the pattern with the Card recipe reused via structural identity. |
| native HTML | `<fieldset><legend>` / `<details>` | The title-on-trigger + collapse idiom, here with a responsive card grid + coordinated motion. |
| Reference | Closest equivalent | UIX decision |
| --------------------------------------------------------------------------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| [Radix Themes](https://www.radix-ui.com/themes/docs/components/radio-cards) | RadioCards over RadioGroup, single choice and responsive columns | Keep ToggleGroup selection; no radio semantics added. |
| [Chakra](https://chakra-ui.com/docs/components/checkbox-card) | CheckboxCard + CheckboxGroup and fieldset/legend | Keep button Items and explicit names; no hidden input/form submission added. |
| [Ark](https://ark-ui.com/docs/components/toggle-group) | Array value, multiple/deselectable, roving and orientation | Reuse ToggleGroup cardinality and keyboard. |
| [Bits](https://bits-ui.com/docs/components/toggle-group) | Single/multiple value types, child/ref, roving/loop | Preserve UIX array API and composition. |
| [APG](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) | Disclosure button, expanded and Enter/Space | Collapsible owns these actions and relationships. |
## Decisiones
@ -178,54 +195,61 @@ every Item is [data-card] by identity.`
## Talla y tema
**Guard R-5.4** (`npm run theming:sentinel -- card-group <url>`): 15/20 tokens
mueven un computed en vivo; los cinco que callan son los del encabezado
ESTÁTICO, que la demo no monta (0 nodos `[data-static]` contra 1
`[data-button]`, contados), y están adjudicados uno a uno con su medición en
`scripts/theming-sentinel-exceptions.ts`. Corrido 2026-08-21.
Contrato en `lib/recipes/base.ts > card-group` (20 claves, entrada NUEVA).
Alcance del censo: **90 %** (era 0 %), con cero globales y cero privados.
The recipe in lib/recipes/base.ts has 20 public keys. Group size deliberately
narrows Size to sm/md/lg; grid density and Item size follow it. The outer radius
is computed as card-radius + padding, and the group rule owns Item radius above
its rounded stamp. Chrome comes from the foundation depth channel. Title Button
owns its own paint; static Title/Description consume group typography tokens.
Historical sentinel/census results are not fresh coverage of this audit.
**Es un CASCO de composición**, y eso acota el contrato antes de nombrar nada:
compone ToggleGroup, Collapsible, Card y Button, así que el cromo de las
tarjetas es de `Card`, el del título de disclosure es de `Button`, y aquí sólo
entra lo del grupo.
R-5.1 exception: inline-size: 100% and block-size: 100% are layout identities; passive Item height has a measured overflow gap below.
**El eje `size` es sm|md|lg**, no las cinco tallas
(`CardGroupSize = Extract<Size, 'sm' | 'md' | 'lg'>`): seis coordenadas, no
diez. Escribir xs/xl habría sido vocabulario muerto.
The CSS fallback clamps its minimum to the container width. Explicit
minChildWidth wins over columns and currently does not clamp its minimum.
**`--shape-outer-radius` no es un knob: se CALCULA.** La receta lo compone como
`card-radius + padding` para que las tarjetas aniden concéntricamente (§30), y
el censo ya lo clasifica como sistema. Un tema mueve las dos coordenadas y el
radio exterior se recompone solo — por eso `card-radius` y `padding` son
públicos y el resultado no.
## Native audit
**Dos privados abreviados murieron aquí**: `--_cg-pad` y `--_cg-card-radius`
violaban §6 r5 (`cg` por `card-group`), el mismo defecto que los `--_mp-*` de
media-player. Pasar a públicos con el nombre completo es a la vez el renombrado
que la regla pedía y la tokenización. (La §3 de la ficha generada decía que la
receta no declaraba privados: los declaraba en su bloque raíz, y el escáner no
los vio por buscarlos con el prefijo completo.)
The [audit](../../../../../docs/process/audit-uix-2026-09-19/card-group-native.md)
preserves the first run: 25 positive cases, 23 failures, 48 original captures.
Permanent tests contain only the 25 original positives. Additional selection,
keyboard and diagnostic probes stay temporary; they do not replace the baseline.
Passing a specific assertion does not certify the overflow visible in its PNG.
El chevron es geometría propia — dos bordes girados 45° —, así que
`chevron-size` cubre las dos dimensiones y `chevron-width` el trazo; su
`currentColor` hereda del título a propósito y no es knob. Los dos `100%` son
identidad de layout (D-TH.2) y el `opacity` del chevron es sistema.
CardGroup adds no runtime or perceptual pack. ToggleGroup also emits contact-press
pre on the clicked Item; commit-toggle post queues behind that contact, while
commit-block is a risk rejection on the exact Item. Collapsible emerge is post
on Content and updates open first. Hidden closes immediately; a transient stamp
does not supply exit presence. Context getters carry only visual configuration.
`R-5.1 exception: los dos `inline-size: 100%`/`block-size: 100%` son identidad de layout (perímetro de knob, D-TH.2).`
Executable wrappers/types/Morfo/Langs/CSS/recipe/index remain intact in this
neutral audit; no extraction or deferred runtime repair. The obsolete select
example in the index comment does not describe the current enabledSelections API.
## Gaps
- **`disabled` doesn't reach the toggle.** The props table promises "selection
- toggle for the whole group", but the wrapper wires `disabled` only into
`ToggleGroup.Provider` — the composed `Collapsible` never receives it (its
soma DOES support it, `soma/components/collapsible/types.ts`), and the eidos
`<Collapsible>` wrapper doesn't forward it either. → **implementar**:
forward `disabled` through the Collapsible chain (eidos wrapper → soma), or
correct the props-table promise.
- **Static Title has no heading semantics.** The non-toggleable form is a
plain `div` (no `role=heading` / `aria-level`); reference groups use a real
heading. → **diferir**: design decision (an `as` prop vs a declared role)
alongside the other surface titles.
- **Q329 — disabled disclosure:** root omits disabled when composing Collapsible;
a real disabled Collapsible peer remains disabled. → **diferir** root forwarding;
the Eidos Collapsible wrapper already forwards rest/disabled.
- **Q330 — Title/Content rest:** toggleable branches omit consumer attributes
that static branches preserve. → **diferir** compatible forwarding.
- **Q331 — Item ID:** selectable provider ID overrides consumer ID while data/class
survive. → **diferir** precedence compatible with registration and focus.
- **Q332 — inherited disabled paint:** Item removes inherited data-disabled,
although native disabled survives. → **diferir** effective visual stamp.
- **Q333 — explicit minimum:** minimum 400px overflows a 350px viewport while
clamped HTML control fits. → **diferir** minimum-width policy.
- **Q334 — recipe loading:** bare Items do not load Card painting. → **diferir**
composition recipe dependency; document explicit loading meanwhile.
- **Q335 — passive row height:** content-box Item with block-size 100% extends
below the group and overlaps following content; auto-height peer fits.
→ **diferir** row geometry preserving the equal-height intent.
- **Q336 — reduced cascade:** parent scale-fade retains scale-in/fade-in at 240ms
under reduce; direct reduced preset retains only fade-in. → **diferir** shared
cascade reduction for passive and ToggleGroup on/off states.
- **Q313 — style concatenation:** unterminated string absorbs custom palette seed;
terminated control preserves it. → **diferir** shared repair.
- **Static heading semantics:** Title is a DIV with no heading API. → **diferir**
design alongside other visual surface titles.
Native Chromium checks do not certify screen-reader output, touch, dark-mode
contrast, SSR or other engines. Frozen demo warnings are triaged in the audit.

@ -0,0 +1,237 @@
import { expect, it } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { flushSync } from 'svelte';
import '$uix/eidos/components/card/card.css';
import { visit, type View } from '$uix/test/card-group/card-group-audit-view';
const css = (node: Element) => getComputedStyle(node);
const output = (v: View, key: string) => v.node('#audit-cg-' + key).textContent;
it('native default CardGroup labels a passive grid through its static DIV title', () =>
visit(async (v) => {
expect(v.root().tagName).toBe('DIV');
expect(v.root().getAttribute('role')).toBe('group');
expect(v.root().getAttribute('aria-labelledby')).toBe(v.part('title').id);
expect(v.part('title').tagName).toBe('DIV');
expect(v.part('title').hasAttribute('role')).toBe(false);
expect(v.item('a').tagName).toBe('DIV');
expect(css(v.part('grid')).display).toBe('grid');
expect(css(v.item('a')).display).toBe('flex');
expect(v.root().querySelectorAll('[data-card-group-item]').length).toBe(3);
await v.capture('default');
}));
for (const depth of ['raised', 'recessed', 'flush'] as const) {
it('native CardGroup depth ' + depth + ' resolves foundation chrome', () =>
visit(
async (v) => {
expect(v.root().getAttribute('data-depth')).toBe(depth);
if (depth === 'flush') expect(css(v.root()).boxShadow).toBe('none');
else expect(css(v.root()).boxShadow).not.toBe('none');
await v.capture('depth-' + depth);
},
{ depth }
)
);
}
for (const [size, padding, gap, cardPadding] of [
['sm', 12, 8, 16],
['md', 16, 12, 20],
['lg', 20, 16, 24]
] as const) {
it('native CardGroup size ' + size + ' drives grid and card density', () =>
visit(
async (v) => {
expect(v.root().getAttribute('data-size')).toBe(size);
expect(css(v.part('grid')).paddingLeft).toBe(padding + 'px');
expect(css(v.part('grid')).gap).toBe(gap + 'px');
expect(v.item('a').getAttribute('data-size')).toBe(size);
expect(css(v.item('a')).paddingLeft).toBe(cardPadding + 'px');
await v.capture('size-' + size);
},
{ size }
)
);
}
for (const variant of ['soft', 'solid', 'outline', 'ghost'] as const) {
it('native CardGroup Item ' + variant + ' reuses the explicitly loaded Card recipe', () =>
visit(
async (v) => {
expect(v.item('a').hasAttribute('data-card')).toBe(true);
expect(v.item('a').getAttribute('data-variant')).toBe(variant);
expect(v.item('a').getAttribute('data-color')).toBe('teal');
if (variant === 'ghost') expect(css(v.item('a')).backgroundColor).toBe('rgba(0, 0, 0, 0)');
else expect(css(v.item('a')).backgroundColor).not.toBe('rgba(0, 0, 0, 0)');
await v.capture('variant-' + variant);
},
{ variant, color: 'teal' }
)
);
}
it('native CardGroup concentric radius owns Item geometry above its rounded axis', () =>
visit(
async (v) => {
expect(css(v.item('a')).borderTopLeftRadius).toBe('13px');
expect(css(v.root()).borderTopLeftRadius).toBe('32px');
await v.capture('concentric');
},
{ rounded: 'lg', style: '--card-group-card-radius:13px;--card-group-padding-md:19px;' }
));
it('native CardGroup numeric columns and minChildWidth precedence update without remounting', () =>
visit(
async (v) => {
const root = v.root();
expect(v.part('grid').style.gridTemplateColumns).toBe('repeat(3, minmax(0px, 1fr))');
flushSync(() => v.controls().minChildWidth(260));
expect(v.part('grid').style.gridTemplateColumns).toBe(
'repeat(auto-fill, minmax(260px, 1fr))'
);
flushSync(() => {
v.controls().minChildWidth(undefined);
v.controls().columns(2);
});
expect(v.root()).toBe(root);
expect(v.part('grid').style.gridTemplateColumns).toBe('repeat(2, minmax(0px, 1fr))');
await v.capture('layout-reactive');
},
{ columns: 3 }
));
it('native CardGroup responsive density and columns follow viewport roundtrip', () =>
visit(
async (v) => {
await expect.poll(() => v.root().getAttribute('data-size')).toBe('lg');
await page.viewport(600, 900);
await expect.poll(() => v.root().getAttribute('data-size')).toBe('sm');
expect(v.part('grid').style.gridTemplateColumns).toBe('repeat(1, minmax(0px, 1fr))');
await page.viewport(1100, 900);
await expect.poll(() => v.root().getAttribute('data-size')).toBe('lg');
expect(v.part('grid').style.gridTemplateColumns).toBe('repeat(3, minmax(0px, 1fr))');
await v.capture('responsive');
},
{ size: { base: 'sm', md: 'lg' }, columns: { base: 1, md: 3 } }
));
it('native toggleable CardGroup connects disclosure button to mounted hidden content', () =>
visit(
async (v) => {
const title = v.part('title');
const content = v.part('content');
expect(title.tagName).toBe('BUTTON');
expect(title.hasAttribute('data-button')).toBe(true);
expect(title.hasAttribute('data-collapsible-trigger')).toBe(true);
expect(title.getAttribute('aria-controls')).toBe(content.id);
expect(content.getAttribute('aria-labelledby')).toBe(title.id);
await userEvent.click(title);
await expect.poll(() => title.getAttribute('aria-expanded')).toBe('false');
expect(content.hidden).toBe(true);
expect(css(content).display).toBe('none');
expect(v.item('a').isConnected).toBe(true);
expect(output(v, 'open-calls')).toBe('1');
await v.capture('disclosure-close');
},
{ toggleable: true }
));
for (const [key, label] of [
['{Enter}', 'enter'],
[' ', 'space']
] as const) {
it('native CardGroup disclosure ' + label + ' opens once and stamps its content target', () =>
visit(
async (v) => {
v.part('title').focus();
await userEvent.keyboard(key);
await expect.poll(() => v.part('title').getAttribute('aria-expanded')).toBe('true');
expect(v.part('content').hidden).toBe(false);
expect(output(v, 'open-calls')).toBe('1');
await expect
.poll(() =>
v.stamps.some((s) => s.event === 'emerge-expand' && s.node === v.part('content'))
)
.toBe(true);
await v.capture('disclosure-' + label);
},
{ toggleable: true, open: false }
)
);
}
it('native CardGroup static parts preserve consumer classes data attrs and title copy updates', () =>
visit(async (v) => {
expect(v.root().className).toBe('audit-group');
for (const part of ['title', 'description', 'content'])
expect(v.part(part).getAttribute('data-probe')).toBe(part);
expect(v.item('a').className).toBe('audit-item');
flushSync(() => v.controls().copy('Updated plans'));
expect(v.part('title').textContent).toBe('Updated plans');
await v.capture('static-passthrough');
}));
for (const [label, labelledby] of [
['Explicit plans', undefined],
[undefined, 'audit-cg-external']
] as const) {
it(
'native CardGroup explicit name ' +
(label ? 'label' : 'labelledby') +
' supports omitted Title',
() =>
visit(
async (v) => {
expect(v.root().querySelector('[data-card-group-title]')).toBeNull();
if (label) {
expect(v.root().getAttribute('aria-label')).toBe(label);
expect(v.root().hasAttribute('aria-labelledby')).toBe(false);
} else expect(v.root().getAttribute('aria-labelledby')).toBe(labelledby);
await v.capture(label ? 'explicit-label' : 'explicit-labelledby');
},
{ title: false, label, labelledby }
)
);
}
it('native empty CardGroup omits Items without adding fallback content', () =>
visit(
async (v) => {
expect(v.part('grid').children.length).toBe(0);
expect(v.part('grid').textContent?.trim()).toBe('');
await v.capture('empty');
},
{ empty: true }
));
it('native CardGroup public tokens tune title description grid and concentric geometry', () =>
visit(
async (v) => {
expect(css(v.part('grid')).gap).toBe('23px');
expect(css(v.part('title')).fontSize).toBe('25px');
expect(css(v.part('description')).fontSize).toBe('18px');
expect(css(v.item('a')).borderTopLeftRadius).toBe('15px');
await v.capture('tokens');
},
{
style:
'--card-group-grid-gap-md:23px;--card-group-title-font-size:25px;--card-group-description-font-size:18px;--card-group-card-radius:15px;'
}
));
it('native CardGroup fallback grid fits its 350px viewport with wrapped card copy', () =>
visit(
async (v) => {
expect(v.root().getBoundingClientRect().right).toBeLessThanOrEqual(350);
expect(v.item('a').getBoundingClientRect().right).toBeLessThanOrEqual(
v.root().getBoundingClientRect().right
);
expect(v.part('grid').scrollWidth).toBeLessThanOrEqual(v.part('grid').clientWidth);
await v.capture('narrow-fallback');
},
{ viewportWidth: 350 }
));
it('native passive CardGroup composes an independent consumer action inside an Item', () =>
visit(
async (v) => {
await userEvent.click(v.node('#audit-cg-action'));
expect(output(v, 'actions')).toBe('1');
expect(v.value()).toEqual([]);
expect(v.item('a').tagName).toBe('DIV');
await v.capture('passive-action');
},
{ action: true }
));
it('native CardGroup consumer removal preserves neighboring controls', () =>
visit(async (v) => {
flushSync(() => v.controls().remove());
expect(v.target.querySelector('#audit-cg')).toBeNull();
expect(v.node('#audit-cg-before').isConnected).toBe(true);
await v.capture('removed');
}));

@ -0,0 +1,181 @@
<script module lang="ts">
import type { CardGroupProps, CardGroupItemProps } from '$uix/eidos/components/card-group';
export type Config = {
toggleable?: boolean;
open?: boolean;
enabledSelections?: CardGroupProps['enabledSelections'];
value?: string[];
whenFull?: CardGroupProps['whenFull'];
disabled?: boolean;
size?: CardGroupProps['size'];
depth?: CardGroupProps['depth'];
columns?: CardGroupProps['columns'];
minChildWidth?: CardGroupProps['minChildWidth'];
motion?: CardGroupProps['motion'];
staggerEach?: number;
variant?: CardGroupItemProps['variant'];
color?: CardGroupItemProps['color'];
rounded?: CardGroupItemProps['rounded'];
itemDisabled?: boolean;
itemStyle?: string;
style?: string;
titleId?: string;
title?: boolean;
description?: boolean;
content?: boolean;
empty?: boolean;
label?: string;
labelledby?: string;
dir?: 'ltr' | 'rtl';
reduce?: boolean;
viewportWidth?: number;
action?: boolean;
};
export type Controls = {
open: (v: boolean) => void;
value: (v: string[]) => void;
disabled: (v: boolean) => void;
size: (v: CardGroupProps['size']) => void;
columns: (v: CardGroupProps['columns']) => void;
minChildWidth: (v: CardGroupProps['minChildWidth']) => void;
limit: (v: CardGroupProps['enabledSelections']) => void;
copy: (v: string) => void;
remove: () => void;
};
</script>
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { setActiveUix, type ActiveUix } from '$active-uix';
import { Soma } from '$soma/core/soma.svelte';
import { ActiveEidos } from '$uix/eidos';
import CardGroup from '$uix/eidos/components/card-group';
let {
uix,
initial = {},
onControls
}: { uix: ActiveUix; initial?: Config; onControls: (v: 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 open = $state(initial.open ?? true);
// svelte-ignore state_referenced_locally
let value = $state(initial.value ?? []);
// svelte-ignore state_referenced_locally
let disabled = $state(initial.disabled ?? false);
// svelte-ignore state_referenced_locally
let size = $state(initial.size);
// svelte-ignore state_referenced_locally
let columns = $state(initial.columns);
// svelte-ignore state_referenced_locally
let minChildWidth = $state(initial.minChildWidth);
// svelte-ignore state_referenced_locally
let limit = $state(initial.enabledSelections);
let copy = $state('Workspace plans');
let visible = $state(true);
let selectionCalls = $state(0);
let openCalls = $state(0);
let actions = $state(0);
onMount(() =>
onControls({
open: (v) => (open = v),
value: (v) => (value = v),
disabled: (v) => (disabled = v),
size: (v) => (size = v),
columns: (v) => (columns = v),
minChildWidth: (v) => (minChildWidth = v),
limit: (v) => (limit = v),
copy: (v) => (copy = v),
remove: () => (visible = false)
})
);
</script>
<div
id="audit-cg-stage"
dir={initial.dir ?? 'ltr'}
data-motion={initial.reduce ? 'reduce' : undefined}
style="padding:24px;font:16px Arial,sans-serif"
>
<div style="max-inline-size:820px">
<button id="audit-cg-before" type="button">Before</button>
<span id="audit-cg-external">External plans</span>
{#if visible}
<CardGroup
id="audit-cg"
class="audit-group"
data-probe="group"
toggleable={initial.toggleable}
bind:open
bind:value
{disabled}
enabledSelections={limit}
whenFull={initial.whenFull}
onValueChange={() => (selectionCalls += 1)}
onOpenChange={() => (openCalls += 1)}
{size}
depth={initial.depth}
{columns}
{minChildWidth}
motion={initial.motion}
staggerEach={initial.staggerEach}
style={initial.style}
aria-label={initial.label}
aria-labelledby={initial.labelledby}
dir={initial.dir}
>
{#if initial.title !== false}<CardGroup.Title
{...initial.titleId ? { id: initial.titleId } : {}}
class="audit-title"
data-probe="title"
title="Consumer title">{copy}</CardGroup.Title
>{/if}
{#if initial.description !== false}<CardGroup.Description
id="audit-cg-description"
class="audit-description"
data-probe="description"
>Choose a workspace plan. Selection and disclosure are independent.</CardGroup.Description
>{/if}
{#if initial.content !== false}<CardGroup.Content
id="audit-cg-content"
class="audit-content"
data-probe="content"
>
{#if !initial.empty}
{#each ['a', 'b', 'c'] as key}
<CardGroup.Item
value={key}
id={'audit-cg-' + key}
class="audit-item"
data-probe={key}
disabled={key === 'b' && initial.itemDisabled}
variant={initial.variant}
color={initial.color}
rounded={initial.rounded}
style={initial.itemStyle}
>
<strong>Plan {key.toUpperCase()}</strong><span
>Projects and shared tools for your team.</span
>
{#if key === 'a' && initial.action}<button
id="audit-cg-action"
type="button"
onclick={() => (actions += 1)}>Details</button
>{/if}
</CardGroup.Item>
{/each}
{/if}
</CardGroup.Content>{/if}
</CardGroup>
{/if}
<button id="audit-cg-after" type="button">After</button>
<output id="audit-cg-value">{JSON.stringify(value)}</output>
<output id="audit-cg-open">{String(open)}</output>
<output id="audit-cg-selection-calls">{selectionCalls}</output>
<output id="audit-cg-open-calls">{openCalls}</output>
<output id="audit-cg-actions">{actions}</output>
</div>
</div>

@ -0,0 +1,87 @@
import { mount, unmount, flushSync } from 'svelte';
import { expect } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { createActiveUix } from '$active-uix';
import '$uix/eidos/index.css';
import Harness, { type Config, type Controls } from './card-group-audit-harness.svelte';
export function renderCardGroup(initial: Config = {}) {
const uix = createActiveUix({
langs: { schema: {}, defaultLocale: 'en' },
prefs: { storage: false },
projectPrefs: false
});
const target = document.createElement('div');
document.body.append(target);
const stamps: { node: Element; event: string; family: string | null }[] = [];
const observer = new MutationObserver((records) => {
for (const record of records) {
const node = record.target as Element;
const event = node.getAttribute('data-event');
if (event) stamps.push({ node, event, family: node.getAttribute('data-event-family') });
}
});
observer.observe(target, { subtree: true, attributes: true, attributeFilter: ['data-event'] });
let controls!: Controls;
let captured = false;
const component = mount(Harness, {
target,
props: { uix, initial, onControls: (v) => (controls = v) }
});
flushSync();
const node = (selector: string) => target.querySelector<HTMLElement>(selector)!;
const ready = async () => {
await new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
);
for (const item of target.querySelectorAll<HTMLElement>('[data-card-group-item]')) {
if (!item.closest('[hidden]'))
await expect.poll(() => Number(getComputedStyle(item).opacity)).toBe(1);
}
};
return {
uix,
target,
stamps,
node,
controls: () => controls,
root: () => node('#audit-cg'),
part: (name: string) => node('[data-card-group-' + name + ']'),
item: (key: string) => node('#audit-cg-' + key) as HTMLButtonElement,
value: () => JSON.parse(node('#audit-cg-value').textContent!) as string[],
ready,
hasCaptured: () => captured,
async capture(name: string) {
if (import.meta.env.VITE_UIX_AUDIT_SCREENSHOTS === '1') {
await ready();
if (captured) throw new Error('Duplicate manual capture');
await page.screenshot({ path: 'tmp/card-group-' + name + '.png' });
captured = true;
}
},
async dispose() {
observer.disconnect();
await unmount(component);
uix.dispose();
target.remove();
}
};
}
export type View = ReturnType<typeof renderCardGroup>;
export async function visit(fn: (view: View) => Promise<void>, initial: Config = {}) {
await page.viewport(initial.viewportWidth ?? 1100, 900);
const view = renderCardGroup(initial);
try {
await userEvent.hover(view.node('#audit-cg-before'));
await view.ready();
await fn(view);
} catch (error) {
if (!view.hasCaptured())
await view.capture(
'unexpected-' +
(expect.getState().currentTestName ?? 'case').replaceAll(/[^a-zA-Z0-9-]+/g, '-')
);
throw error;
} finally {
await view.dispose();
}
}
Loading…
Cancel
Save

Powered by TurnKey Linux.