From d5a27b87e741d9ff3bec1bb4822a8fdf3238a866 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 25 Aug 2026 04:09:13 +0200 Subject: [PATCH] fix(morfo)!: tags-input habla UN patron ARIA - layout grid, cierra F-1 El AX hablaba tres dialectos rotos a la vez (medido por CDP): dos listbox anidados (las opciones eran hijas del Control; el Provider no tenia ninguna), button DENTRO de option (rol hoja: el nombre se fundia - "svelte Eliminar etiqueta" como una sola cosa) y un combobox sin aria-expanded/aria-controls apuntando activedescendant a etiquetas commiteadas. F-1, adjudicado 2026-07-07; el registro de ayer era una re-deteccion parcial. Estructura B (firmada): Control = grid de UNA fila - etiquetas = gridcell (conservan el aria-selected -> stateRef de 2ce43db56; gridcell lo admite) - el boton de borrar CONVIVE en la celda de su etiqueta ("a cell contains one widget... link, button" - lo que lo prohibia era option, Children Presentational) - el input en celda propia como TEXTBOX nativo (combobox EXIGE controls/expanded y no hay popup) - el Provider pierde su listbox. Dos partes privadas nuevas (Row, InputCell) con display:contents: grid solo posee filas, row solo celdas. - Teclado: la navegacion estaba MUERTA (ArrowLeft x2 avanzaba 0) y ahora camina; el resalte mueve foco DOM - la CONDICION de retirar el activedescendant (su destino debe ser descendiente y era hermano). Tabulacion sigue en UNA parada. - aria-orientation RETIRADO (no es propiedad de grid; Chrome no la expone, medido): se conserva como comportamiento, no como atributo. - Pixel CERO: sonda 2.496 valores por identidad de nodo, 0 diffs, geometria al centesimo; capturas byte-identicas. CSS: 7 lineas display:contents. - Gates: morfo 222/222 - soma tags-input - attr-audit con el techo APRETADO 1->0 - eidos 443+1 ajeno - morfo:check PASA - check 0 propios - docs 0/0. - Registro: F-1 CERRADO (ficha de componente, nota fechada) - el item 12.5 del tono MOVIDO de 13 a 12 con sus dos cifras corregidas ((0,4,0); gana el BLOQUE de archetypes) - abiertos: tag-group filas sin celdas (el "precedente" refutado midiendo: CERO gridcell), search-field F-1, y la regla del censo de citas APG - demo: 3 filas de su tabla a11y actualizadas para no mentir sobre los roles nuevos. - Trampa nueva anotada: `const state = Provider.create()` SOMBREA la runa $state (compila y revienta en render); un error SSR no llega al navegador - se caza navegando por el router desde otra pagina. Co-Authored-By: Claude Fable 5 --- docs/audit/components/_cierre.md | 7 + docs/audit/components/_system.md | 2 +- docs/audit/components/tags-input.md | 40 +++++- docs/audit/theming/tags-input.md | 47 +++++-- docs/next-features.md | 126 +++++++++++------- docs/process/CONTINUE-theming.md | 23 +++- src/uix/eidos/components/tags-input/README.md | 24 +++- .../components/tags-input/tags-input.css | 8 ++ src/uix/morfo/components/tags-input.ts | 71 ++++++++-- src/uix/soma/components/tags-input/README.md | 30 +++++ .../components/tags-input-control.svelte | 18 ++- .../components/tags-input-input.svelte | 30 ++++- .../tags-input-provider.svelte.test.ts | 30 ++++- .../tags-input/tags-input-provider.svelte.ts | 94 ++++++++++--- src/uix/soma/soma-attr-audit.test.ts | 7 +- .../uix/components/tags-input/+page.svelte | 6 +- 16 files changed, 445 insertions(+), 118 deletions(-) diff --git a/docs/audit/components/_cierre.md b/docs/audit/components/_cierre.md index 0739ebee9..4867cf236 100644 --- a/docs/audit/components/_cierre.md +++ b/docs/audit/components/_cierre.md @@ -62,6 +62,13 @@ Ejecución de fixes: 2026-07-08 → 2026-07-11. Rama `alpha-0.1-sec-dom`. reciben el invalid de rango) — gap compartido documentado. - class-hooks preexistentes fuera del codemod S1: color-picker(2) · time-picker(8) · time-range-picker(12). +- **`tags-input` F-1 — CERRADO el 2026-08-25**, mes y medio después del + programa, por la firma «el árbol ARIA de `tags-input` habla UN patrón»: se + eligió UNO (el layout grid que su `apg` ya citaba) y se retiró el rol + duplicado Provider/Control y el `combobox` sin popup. Detalle fechado en su + [ficha](./tags-input.md). Deja vivo el censo de citas APG erróneas que la + ficha alimenta: `search-field` F-1 (misma clase) y `tag-group` (mismo `apg`, + filas SIN celdas) — ver [`_system.md`](./_system.md). ## Fuera del programa (dueños nuevos) diff --git a/docs/audit/components/_system.md b/docs/audit/components/_system.md index 6eefced19..f38a219c6 100644 --- a/docs/audit/components/_system.md +++ b/docs/audit/components/_system.md @@ -32,7 +32,7 @@ catálogo). Formato: origen (fichas) · hallazgo · propuesta. - **Duplicación de alturas (SYS-6)**: date/time/color/search/password/mask-field (6). - **Packs sema (SYS-3) — censo CORREGIDO por la re-auditoría (pickers/calendar)**: trío FIELDS roto (date ✓ / time ✗ / color-field ✗) PERO los pickers de media TODOS con pack ✓ (color-picker, image-picker, gradient-picker, gradient-builder) · **command RETIRADO de la lista** (family-default razonado con libro in-place: cap.22 §8 anti-"celebrate before time", precedente Button — tercer default legítimo documentado con field-langs y button) · sin pack con semántica rica CONFIRMADOS: **range-calendar (el mejor candidato: start affirm → range FULFILL → reset neutral ya declarados)**, month/year-grid, listbox, clipboard, drag-drop, media-player, onion-menu/menu-dial, carousel, table · **tercera categoría nueva: deuda DECLARADA** (Picker genérico :18-19 — fijar plazo a los "yet" en el veredicto) · pendiente re-verificar al llegar a sus familias: listbox/clipboard/drag-drop/etc. - ~~Demo (D-7.4 chip drift · D-1.2 plantilla · D-1.5/D-4.3/D-3.1 cableado)~~ — **FUERA DEL ALCANCE de esta auditoría** (corrección de alcance 2026-07-07: solo las capas del componente). Los datos siguen en tmp/component-audit.md como backlog de demos aparte; no computan en fichas ni en el checkpoint. -- **APG ausente (A-1.4)**: natural-time-picker, gradient-picker, gradient-builder, card (variante interactiva), drag-drop, media-player, virtual-list, metrics → y **regla nueva necesaria**: cómo satisfacer "sin patrón APG oficial" (media-player, drag-drop) — ¿`apg: 'none — rationale'`? +- **APG ausente (A-1.4)**: natural-time-picker, gradient-picker, gradient-builder, card (variante interactiva), drag-drop, media-player, virtual-list, metrics → y **regla nueva necesaria**: cómo satisfacer "sin patrón APG oficial" (media-player, drag-drop) — ¿`apg: 'none — rationale'`? **Actualización 2026-08-25**: la cara gemela —el `apg` PRESENTE pero MENTIROSO— tiene ya su primer miembro resuelto (`tags-input` F-1: se implementó el layout grid que citaba, en vez de corregir la cita) y dos miembros vivos medidos: `search-field` F-1 (`apg: combobox` sin popup) y **`tag-group`**, que declara el mismo `apg: grid` que `tags-input` y renderiza `role=row` SIN ninguna celda (`morfo/components/tag-group.ts:64` `:137` `:163` — cero `gridcell` declarados) más un `Label` colgando del `grid` que no es `row`. La regla que falta es UNA para las dos caras: qué se hace cuando no hay patrón, y qué se hace cuando el declarado no describe lo implementado. - **Gramática de eventos (A-3.6/A-3.7)**: media-player ×4 nombres fuera de `{familia}-{verbo}` · float-panel 28 mutaciones/6 eventos (el mayor infra-declarado). - **Interactivo con 0 eventos (A-3.1) — censo FINAL tras chequeos de delegación**: **pin-input = el único caso desnudo** (0 eventos + comentario :7-9 que promete un "commit-fill" imposible: sema ni está en scope — contradicción in-file). EXONERADOS: date-range-field/time-range-field (delegación real a los singles embebidos; les falta solo declararla — `expression: 'delegated'`) · natural-time-picker (header :4-19 documenta la composición entera; su hueco transaccional es deuda del Picker genérico) · **Picker genérico = tercera categoría: deuda DECLARADA** (:18-19 "candidate commit-* events for a later sema pass" — Accept/Cancel/Clear sin contrato, reconocido; UN fix ahí repara ntp + los 5 pickers de la migración deferValue). Afinar A-3.1: leer expression/scope/header antes de flaguear; añadir el Picker genérico al universo del script. - **Colores crudos (R-2.1)**: natural-time-picker ×9 (cielos physically-fixed SIN anotación) · tipografía literal (R-2.7): field-langs, timeline ×2. diff --git a/docs/audit/components/tags-input.md b/docs/audit/components/tags-input.md index e647c87cb..65edca4fb 100644 --- a/docs/audit/components/tags-input.md +++ b/docs/audit/components/tags-input.md @@ -15,7 +15,7 @@ | 5 | Tokens | ✓ | Sin duplicación de alturas; chips vía recipe | | 6 | Sema | ✓ | Pack ✓ + eventos ricos | | 7 | Animaciones | ✓ | Sin keyframes | -| 8 | Contrato morfo | **HALLAZGO ARIA** | **F-1**: `apg: grid` (:9) pero Provider `role='listbox'` (:73) **Y** Control `role='listbox'` (:88) **Y** Input `role='combobox'` + `aria-autocomplete='list'` + `activedescendant` (:104-113) — tres patrones mezclados, y **dos listbox anidados** (Provider⊃Control con el mismo rol) es ARIA inválida; además combobox exige popup que no existe (misma clase que search-field F-1) | +| 8 | Contrato morfo | **F-1 CERRADO 2026-08-25** | **F-1**: `apg: grid` (:9) pero Provider `role='listbox'` (:73) **Y** Control `role='listbox'` (:88) **Y** Input `role='combobox'` + `aria-autocomplete='list'` + `activedescendant` (:104-113) — tres patrones mezclados, y **dos listbox anidados** (Provider⊃Control con el mismo rol) es ARIA inválida; además combobox exige popup que no existe (misma clase que search-field F-1). **Cerrado** por la firma «el árbol ARIA de `tags-input` habla UN patrón» — ver la nota fechada bajo «Hallazgos y propuestas» | | 9 | A11y | condicionada a F-1 | Textos remove/clear localizados ✓; activedescendant hacia item ✓ — pero el árbol de roles necesita el veredicto de F-1 antes de validar el conjunto | | 10 | Theming | ✓ | Focus o7 (input+chips) → censo SYS-5; state-hover 1 tier neutro ✓ | | 11 | Composición interna | verificar | Types NO mencionan OR-merge con Field (a diferencia de sus hermanos) — ¿participa de Field.Provider? verificar provider en fase de fixes; si no, gap de familia | @@ -28,14 +28,48 @@ | ID | Hallazgo | Propuesta | |---|---|---| -| **F-1** | Árbol ARIA mezclado: grid citado, listbox ×2 anidados, combobox sin popup | Sesión de patrón en fase de fixes: elegir UNO (propuesta: grid layout para la colección de pills — como su APG cita — O group+listbox único; el combobox solo si se añaden sugerencias); eliminar el rol duplicado Provider/Control | +| ~~**F-1**~~ | ~~Árbol ARIA mezclado: grid citado, listbox ×2 anidados, combobox sin popup~~ | **CERRADO 2026-08-25** — se eligió el layout grid que el `apg` ya citaba (ver nota abajo) | | **F-2** | `validate` booleano + `onValueInvalid` divergen de la familia (validador-mensaje + `onInvalid`) | Veredicto _naming: una firma de validador (propuesta: la que devuelve mensaje, con razones tipadas como color-field) y un nombre de callback | | **F-3** | Verbo `set-add` vs `remove` asimétrico | Revisar contra verbos del CANON en el veredicto de vocabulario | | **F-4** | Participación en Field sin documentar (¿existe?) | Verificación dirigida; si falta, sumarlo al pass SYS-6 | +### F-1 — CERRADO el 2026-08-25 + +Firma «el árbol ARIA de `tags-input` habla UN patrón», estructura **B**. Se +eligió UNO: el **layout grid** que el `apg` ya citaba — porque es el patrón que +existe justamente para esto («a layout grid can be used to group a set of +interactive elements», APG), y el botón de borrar por etiqueta es un +interactivo que un `listbox` no puede alojar (`option` es *Children +Presentational: true*, y el nombre computado lo probaba: `"svelte Eliminar +etiqueta"` — el botón se había fundido con el nombre de la opción). + +Las tres caras, cerradas juntas: + +1. **El rol duplicado** — el Provider pierde `role='listbox'` (medido: no + contenía ninguna opción propia; las dos son hijas DIRECTAS del Control) y el + Control pasa a `role='grid'`, de UNA fila. `aria-orientation` se va con el + listbox: no es propiedad soportada de `grid`, y Chrome la ignora (medido). +2. **La etiqueta** pasa a `role='gridcell'` — que es *Children Presentational: + false*, así que el botón de borrar deja de fundirse, y que **admite + `aria-selected`**, así que el `stateRef('active')` de la firma del 2026-08-24 + sobrevive sin tocarse. No necesita celda propia para el botón: APG contempla + el widget dentro de la celda. +3. **El `combobox`** deja de serlo: sus dos propiedades REQUERIDAS son + `aria-expanded` y `aria-controls`, y no llevaba ninguna porque no hay popup. + Queda un `` (textbox nativo) dentro de su propio + `gridcell` — un `row` no puede poseer nada que no sea una celda. Con el rol + se retiró el `aria-activedescendant`, que apuntaba a una etiqueta HERMANA + (su destino debe ser descendiente del nodo que lo lleva), y el resalte pasó + a mover foco DOM real sobre la celda: el modelo del propio grid, y el que la + `tabindex` roving de soma ya preparaba sin que nadie la usara. + +Estructura: `grid > row > gridcell ×N + gridcell > textbox`. Dos envoltorios +privados (`row`, `input-cell`) con `display: contents` — geometría idéntica al +píxel y **0 diffs sobre 2496 valores computados**. + ## Escalan al sistema -_naming (validadores ×3 firmas · onInvalid vs onValueInvalid · identidad de items por índice) · F-1 alimenta el censo de citas APG erróneas (con search-field) · SYS-5. +_naming (validadores ×3 firmas · onInvalid vs onValueInvalid · identidad de items por índice) · **F-1 CERRADO 2026-08-25** — el censo de citas APG erróneas sigue vivo con `search-field` (mismo «combobox sin popup») y ahora con `tag-group`, que declara el mismo `apg` y tiene filas SIN celdas · SYS-5. ## Veredictos diff --git a/docs/audit/theming/tags-input.md b/docs/audit/theming/tags-input.md index 5f3a5d200..cc859cac5 100644 --- a/docs/audit/theming/tags-input.md +++ b/docs/audit/theming/tags-input.md @@ -196,13 +196,44 @@ por parte; nadie inventa selección. Medido de nuevo, entero: de 2.504**, todos `background-image` de las dos etiquetas en los siete pases sin puntero: el velo de estado saliendo del reposo, nada más. - **Corrección de la atribución de arriba**: los tres slots `*-text` del tono - NO llevaban la segunda causa por `aria-selected`. Lo que enmascara la tinta - de la etiqueta activa es `[data-archetype='item'][data-highlighted]` - **(0,3,0)**, que sigue ganando a `[data-tags-input-item][data-state='active']` - (0,2,0) **después** de la firma. Re-medido sobre la etiqueta navegada por - TECLADO: sin `aria-selected` la tinta no se mueve; sin `data-highlighted` - aparece el tono (`oklch(0.5168 0.1733 305.88)`). La medida vieja forzaba - `data-state=active` a mano, sin `data-highlighted`, y por eso señaló al - atributo equivocado. El hueco sigue abierto en next-features §13. + NO llevaban la segunda causa por `aria-selected`. Re-medido sobre la etiqueta + navegada por TECLADO: sin `aria-selected` la tinta no se mueve; sin + `data-highlighted` aparece el tono (`oklch(0.5168 0.1733 305.88)`). La medida + vieja forzaba `data-state=active` a mano, sin `data-highlighted`, y por eso + señaló al atributo equivocado. + + > **Enmienda del 2026-08-25** (medida con `CSS.getMatchedStylesForNode`, la + > especificidad que dice Chrome): la cifra correcta es **(0,4,0)**, no + > (0,3,0) — cuatro componentes: `[data-archetype='item']`, + > `[data-highlighted]`, `:not([data-state='checked'])`, + > `:not([data-state='on'])`. Contra los (0,2,0) de la receta el hueco es de + > DOS peldaños, no de uno. Y la atribución fina también: los dos selectores + > —el de `data-highlighted` y el de `aria-selected`— viven en el **MISMO + > bloque** (`archetypes.css:245-257`, una sola declaración con diez + > selectores) y desde la firma de `aria-selected` **los dos casan** la + > etiqueta activa. «Es `data-highlighted`, no `aria-selected`» es una + > distinción sin diferencia: **el ganador es el BLOQUE**, y retirar + > cualquiera de los dos no mueve nada porque el otro sostiene la misma regla. + > La conclusión medida (la tinta no se mueve) era correcta; el mecanismo + > escrito, no. El hueco es de la capa del SISTEMA y su decisión vive en + > next-features **§12** («Qué habría que decidir», `:516`), donde este punto + > se registró como cuarta ocurrencia de §12.5 — ya no en §13. + +### Nota del 2026-08-25 — la migración al layout grid NO toca el píxel + +Firma «el árbol ARIA de `tags-input` habla UN patrón» (F-1 de la ficha de +componente). Cambia roles y retira `aria-orientation` / `aria-autocomplete` / +`aria-activedescendant`; **ningún CSS del ecosistema selecciona por `role`** +(8 `[role=` en todo eidos, ninguno de este componente) y el `aria-selected` que +`archetypes.css:251` SÍ selecciona se conserva, porque `gridcell` lo admite. +Medido con la sonda antes/después: **0 diffs sobre 2496 valores computados en 8 +pases**, y geometría idéntica al centésimo de píxel. + +Lo único que cambia en el censo de la sonda son **dos nodos NUEVOS** +—`[data-tags-input-row]` y `[data-tags-input-input-cell]`, los dos envoltorios +estructurales del grid— que la receta pone en `display: contents`: no pintan +nada y no traen claves. La sonda los cuenta (10 → 12 nodos en reposo), así que +un `MISSING NODE` en el pase `hover` de un diff antiguo es sólo el +re-numerado de índices de ese pase, no una pérdida. diff --git a/docs/next-features.md b/docs/next-features.md index 054119a24..567288244 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -508,10 +508,31 @@ Lo que `tags-input` añadió (2026-08-24): razón medida. > **SUPERADO EL 2026-08-25** (firma B-a2, PLAN §8): el morfo ata ya > `aria-selected` a `v.stateRef('active')`, así que `item-fg` PINTA y su - > adjudicación se retiró. De los dos tokens, el segundo —la tinta de tono de - > la etiqueta activa— sigue enmascarado, pero por `data-highlighted`, no por - > `aria-selected`: la atribución de este párrafo era errónea y la corrección - > medida está al final de §13. + > adjudicación se retiró. + > + > **CUARTA OCURRENCIA de §12.5** —contando `grid-list`, que el párrafo de + > arriba enumera aparte como par arquetipo-`item`— **y con el mecanismo + > corregido** (medido + > 2026-08-25 sobre la etiqueta real navegada por teclado, con + > `CSS.getMatchedStylesForNode`). De los dos tokens sigue enmascarada la + > tinta de TONO de la etiqueta activa, y **el que gana no es un selector: es + > el BLOQUE** `archetypes.css:245-257` — UNA sola declaración con diez + > selectores, de los que DOS casan la etiqueta activa + > (`[data-archetype='item'][data-highlighted]:not(…):not(…)` y + > `[data-archetype='item'][aria-selected='true']:not(…):not(…)`), los dos a + > **(0,4,0)** —no (0,3,0)— contra los (0,2,0) de la receta: dos peldaños, no + > uno. «Es `data-highlighted`, no `aria-selected`» es una distinción sin + > diferencia: retirar cualquiera de los dos no mueve la tinta porque el otro + > sostiene la MISMA regla. Coste de píxel de cederla: UN nodo, UN estado + > —`oklch(0.2435 0 0)` neutro → `oklch(0.5168 0.1733 305.88)`, el tono— y el + > velo no se pierde al soltar `color`: `--state-hover` es + > `color-mix(in srgb, currentColor 8%, transparent)`, así que se tiñe con el + > tono. Pertenece a la capa del SISTEMA (el bloque es acento + > cross-component, y el hover es del sistema, nunca una invención por + > componente), así que **no es una firma de `tags-input`**: su decisión es la + > que ya está redactada en «Qué habría que decidir» de esta misma §12 + > (`docs/next-features.md:516`). La migración al grid del 2026-08-25 la dejó + > intacta — medido, 0 diffs sobre 2496 valores computados. ### Qué habría que decidir @@ -1991,53 +2012,56 @@ Lo que `chart` destapó (2026-08-22): entre filas (`row-gap` 12/16/20/24/28 px). **Mueve píxel en xs/sm/lg/xl** ⇒ firma. Es independiente de la cascada: existía ganase quien ganase. -Lo que dejó ABIERTO la firma `aria-selected` de `tags-input` (2026-08-25): - -- **⚠ El morfo declara el patrón `grid` y lo implementa como `listbox`/`option`.** - `morfo/components/tags-input.ts:10` declara - `apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/grid/'` mientras sus partes - llevan `role: 'listbox'` (Provider y Control) y `role: 'option'` (Item). Su - hermano `tag-group` declara **el mismo `apg`** (`tag-group.ts:9`) y sí lo - implementa: `role: 'grid'` (`:64`), `role: 'row'` en el item (`:137`), - `states: ['selected','unselected']` y `aria-selected` sobre un estado real - (`:148`). En el patrón grid la etiqueta es `row`/`gridcell` y `aria-selected` - es OPCIONAL — sólo tiene sentido con selección de verdad, que `tags-input` - no tiene ni ha tenido (soma sólo navega y borra: `highlightedIndex`, sin - `selected`, sin valor seleccionado, sin API). Migrar el patrón es la opción - **B-b** del expediente y es la continuación correcta a largo plazo, pero - arrastra el role de la etiqueta Y el de sus dos contenedores ⇒ firma aparte. - Hoy `role=option` conserva su estado propio y lo dice con la verdad; lo que - queda es que el `apg` declarado no describe lo implementado. -- **⚠ `Provider` y `Control` llevan LOS DOS `role='listbox'`, anidados.** - `tags-input.ts:70` y `:84`. El árbol AX lo confirma: **dos `listbox` anidados - con `multiselectable: false`**, y el externo no contiene ninguna opción - propia. Un lector encuentra dos listas donde hay una. Es independiente del - punto anterior (se puede cerrar sin migrar de patrón, retirando uno de los - dos), y cualquier pase §12 sobre este morfo se lo va a topar. -- **La tinta de TONO de la etiqueta activa sigue enmascarada — §12.5, y con la - causa CORREGIDA.** El párrafo de §12 fechado el 2026-08-24 se lo atribuía a - `aria-selected`; medido de nuevo el 2026-08-25 sobre la etiqueta real - **navegada por teclado**, el que gana es - `[data-archetype='item'][data-highlighted]:not([data-state='checked']):not([data-state='on'])` - a **(0,3,0)**, por encima de `[data-tags-input-item][data-state='active']` - a (0,2,0). Quitar `aria-selected` no mueve la tinta (sigue - `oklch(0.2435 0 0)`); quitar además `data-highlighted` es lo que deja pasar - el tono (`oklch(0.5168 0.1733 305.88)`). La medida vieja forzaba - `data-state=active` a mano, SIN `data-highlighted`, y por eso señaló al - atributo equivocado — la clase «el instrumento miente primero». Consecuencia: - los ocho `*-text` del tono siguen adjudicados por PATRÓN en el ledger de - R-5.4, ahora con la razón correcta, y la decisión sigue siendo la misma de - §12.5 (quién manda cuando el arquetipo y la receta pintan el mismo eje). - Alineado con la doctrina: que el RESALTE lo pinte el sistema es lo correcto; - lo que está sin decidir es si el tono del componente debe poder asomar por - debajo. -- **Observado al paso, NO es de theming**: desde el input, `ArrowLeft` llama a - `highlightLast()` (`tags-input-provider.svelte.ts:381`) y **el foco no sale - del input**, así que pulsarlo dos veces vuelve a resaltar la última etiqueta - en vez de caminar hacia atrás. `highlightPrev/Next` sí caminan, pero sólo los - recibe el item cuando ya tiene el foco, y nadie se lo da salvo el clic - (`:531`). Es de soma, es anterior a esta firma y no la afecta: el contrato AX - medido —exactamente una seleccionada mientras se navega— se cumple igual. +Lo que dejó ABIERTO la firma `aria-selected` de `tags-input` (2026-08-25) — +**CERRADO el 2026-08-25** por la firma «el árbol ARIA de `tags-input` habla UN +patrón» (estructura B): + +- Los dos primeros puntos —el `apg: grid` implementado como `listbox`/`option` + y los dos `listbox` anidados— no eran dos hallazgos: eran **las dos primeras + caras de F-1**, ya adjudicado como UNA decisión el 2026-07-07 + (`docs/audit/components/tags-input.md:18` `:31`, «elegir UNO … eliminar el + rol duplicado Provider/Control»). Se cerraron JUNTOS con la tercera cara que + aquel registro no recogía —el `combobox` sin popup— porque cerrar dos deja el + árbol mezclado igual. Resultado: el Control es el `grid` de UNA fila, la + etiqueta es `gridcell` (que admite `aria-selected`, así que la firma anterior + sobrevive intacta), el Provider se queda sin role y el campo es un `textbox` + en su propia celda. Píxel medido: **0 diffs sobre 2496 valores computados**. +- El punto del foco que no salía del input **era la condición** para poder + retirar el `aria-activedescendant` (su destino debe ser descendiente del nodo + que lo lleva, y una etiqueta nunca lo fue), así que se cerró con él: el + resalte mueve ahora foco DOM real sobre la celda — el modelo del propio grid. +- El punto de la tinta de tono **no era una firma de `tags-input`**: es de la + capa del sistema y se movió a §12 como cuarta ocurrencia de §12.5, con la + cifra corregida a (0,4,0) y la atribución al BLOQUE, que es donde vive su + decisión. + +Lo que ABRE la firma «el árbol ARIA de `tags-input` habla UN patrón» (2026-08-25): + +- **⚠ `tag-group` implementa los NOMBRES del patrón grid, no su ESTRUCTURA.** + Declara el MISMO `apg` (`morfo/components/tag-group.ts:9`) y sus roles son + `grid` (`:64`) y `row` (`:137` el item, `:163` el link), pero **no declara + NINGUNA parte con `role: 'gridcell'`** y el DOM renderizado lo confirma + (medido en `/uix/components/tag-group`): filas con un `button` colgando + directamente, y un `data-tag-group-label` que es hijo del `grid` sin ser + `row`. WAI-ARIA: `row` exige al menos un `cell` / `gridcell` / + `columnheader` / `rowheader`, y `grid` no puede poseer nada que no sea + `row`/`rowgroup`. Es el «precedente vivo» que el registro anterior citaba + como prueba de que el patrón ya estaba implementado: **no lo es**. Y ahora + los dos hermanos con el mismo `apg` tienen estructuras distintas, lo cual es + una decisión consciente pendiente, no un accidente. +- **⚠ `search-field` F-1 sigue abierto y es la MISMA clase** que la tercera + cara que acaba de cerrarse: `apg: combobox` sin popup + (`docs/audit/components/search-field.md:18` `:31`). El precedente ya existe + —un `combobox` sin `aria-expanded`/`aria-controls` no es un `combobox`, y el + campo desnudo es la respuesta— pero su cita APG sigue apuntando a un patrón + que el componente no implementa. +- **⚠ El censo de citas APG erróneas sigue SIN REGLA.** La propia ficha lo + nombra (`docs/audit/components/tags-input.md:38`: «F-1 alimenta el censo de + citas APG erróneas (con `search-field`)») y `docs/audit/components/_system.md:35` + deja la pregunta escrita: cómo satisfacer «sin patrón APG oficial» + (media-player, drag-drop) — ¿`apg: 'none — rationale'`? Con `tags-input` + cerrado el censo tiene ya un miembro RESUELTO al que mirar, y ocho + componentes sin `apg` esperando la forma. - **El codemod de nombres NO SABE LEER una IIFE, y aborta sobre `avatar`** (medido 2026-08-24 por el verificador V3′, re-verificado al ejecutar la firma diff --git a/docs/process/CONTINUE-theming.md b/docs/process/CONTINUE-theming.md index be22b5f64..82f58123e 100644 --- a/docs/process/CONTINUE-theming.md +++ b/docs/process/CONTINUE-theming.md @@ -176,10 +176,25 @@ guarda `active-eidos-config.test.ts` · ledger `scripts/theming-sentinel-excepti 212,10 en los dos) y ahora son 224,58 → 212,10. Diff de sonda: **14 valores de 2.504, todos `background-image` de las dos etiquetas en los siete pases sin puntero** — el velo de estado saliendo del reposo, y nada - más. Detalle en §8 del PLAN. Lo que NO arregla (abierto en next-features - §13): la tinta de tono de la etiqueta activa, enmascarada por - `[data-archetype='item'][data-highlighted]` (0,3,0) — **no** por - `aria-selected`, atribución que esta firma corrige midiendo. + más. Detalle en §8 del PLAN. Lo que NO arregla: la tinta de tono de la + etiqueta activa. + **Enmienda del 2026-08-25**: esa tinta ya no está abierta en §13 sino en + **next-features §12**, como cuarta ocurrencia de §12.5 y con su decisión ya + redactada (`docs/next-features.md:516`) — es de la capa del SISTEMA, no del + componente. Y las dos cifras de esta entrada estaban mal: la especificidad + es **(0,4,0)**, no (0,3,0), y el ganador no es el selector de + `data-highlighted` sino **el BLOQUE** `archetypes.css:245-257`, cuyos dos + selectores —`data-highlighted` y `aria-selected`— casan LOS DOS la + etiqueta activa desde esta misma firma; retirar uno no mueve nada. + Los dos `listbox` anidados que esta entrada describe **ya no existen**: la + firma «el árbol ARIA de `tags-input` habla UN patrón» (2026-08-25, F-1 de + la ficha de componente) los sustituyó por el layout grid del `apg` + —`grid > row > gridcell`, el campo un textbox en su celda— sin mover un + píxel: **0 diffs sobre 2496 valores computados** en los 8 pases de la + sonda, y ningún CSS del ecosistema selecciona por `role`. No es theming y + no toca el ledger de claves; el centinela no se re-corrió en esa firma + (su script está en obras en otra sesión), pero no hay valor computado que + se haya movido. 6. ~~**El chip de añadir reacción de `chat-message`** lo pinta `popover.css`: los chips de reacción son píldoras redondas y el de añadir un cuadrado gris.~~ — **✅ FIRMADA Y EJECUTADA 2026-08-25** (opción b, «la ley del diff --git a/src/uix/eidos/components/tags-input/README.md b/src/uix/eidos/components/tags-input/README.md index 484100cf0..96eb1daa1 100644 --- a/src/uix/eidos/components/tags-input/README.md +++ b/src/uix/eidos/components/tags-input/README.md @@ -57,12 +57,24 @@ token lee VIVO, su entrada del ledger se RETIRA y la medida pasa a **71/91** etiqueta real: escribir `--tags-input-item-fg` en `:root` la repinta (`oklch(0.2435 0 0)` → `rgb(1, 2, 3)`) y vuelve al retirarlo. -Lo que esa firma NO arregla, y queda registrado en next-features §13: la tinta -de TONO de la etiqueta resaltada sigue enmascarada, pero por -`[data-archetype='item'][data-highlighted]` (0,3,0), que gana a -`[data-tags-input-item][data-state='active']` (0,2,0) — medido quitando los -atributos uno a uno sobre la etiqueta real: sin `aria-selected` no cambia nada; -sin `data-highlighted` el tono aparece (`oklch(0.5168 0.1733 305.88)`). +Lo que esa firma NO arregla, y queda registrado en next-features **§12** (cuarta +ocurrencia de §12.5, con su decisión ya redactada en «Qué habría que decidir»): +la tinta de TONO de la etiqueta resaltada sigue enmascarada. El ganador es el +**BLOQUE** `archetypes.css:245-257` a **(0,4,0)** —una sola declaración cuyos +selectores de `data-highlighted` y de `aria-selected` casan LOS DOS la etiqueta +activa— contra los (0,2,0) de `[data-tags-input-item][data-state='active']`: +por eso quitar cualquiera de los dos atributos por separado no mueve la tinta y +sólo quitar el `color` del bloque deja pasar el tono +(`oklch(0.5168 0.1733 305.88)`). Es de la capa del sistema, no del componente. + +**El árbol ARIA habla ya UN patrón (firma 2026-08-25, F-1).** El Control es el +layout grid que el `apg` citaba, de UNA fila; la etiqueta es un `gridcell` +(admite `aria-selected`, así que lo de arriba sigue igual, y su botón de borrar +deja de fundirse en el nombre); el Provider se queda sin role y el campo es un +`` en su propia celda, sin `combobox` —no hay popup— ni +`aria-activedescendant`. La receta sólo suma `display: contents` para los dos +envoltorios estructurales (`[data-tags-input-row]`, +`[data-tags-input-input-cell]`): **0 diffs sobre 2496 valores computados**. ## Baseline diff --git a/src/uix/eidos/components/tags-input/tags-input.css b/src/uix/eidos/components/tags-input/tags-input.css index b478bdfaa..db403aaeb 100644 --- a/src/uix/eidos/components/tags-input/tags-input.css +++ b/src/uix/eidos/components/tags-input/tags-input.css @@ -79,6 +79,14 @@ box-shadow var(--tags-input-transition-duration) var(--tags-input-transition-ease); } +/* The grid's row and the editable field's cell are ARIA structure, not layout: + * `display: contents` keeps the tags and the input direct flex children of the + * control, so the migration to the layout grid costs zero boxes. */ +[data-tags-input-row], +[data-tags-input-input-cell] { + display: contents; +} + [data-tags-input][data-variant='outline'] [data-tags-input-control] { background: transparent; box-shadow: none; diff --git a/src/uix/morfo/components/tags-input.ts b/src/uix/morfo/components/tags-input.ts index cec89eb87..d83be08eb 100644 --- a/src/uix/morfo/components/tags-input.ts +++ b/src/uix/morfo/components/tags-input.ts @@ -65,12 +65,18 @@ export const tagsInputMorfo = { ], parts: [ { + // FIRMA 2026-08-25 — the provider carries NO role. It used to be a + // second `listbox` wrapping the control's `listbox`: two nested + // listboxes with the same role, and the provider owned no option of + // its own (measured — every item is a direct child of the control; + // only the control and the clear trigger hang here). `aria-orientation` + // left with the role: it is supported on `listbox`, not on the plain + // generic this now is. name: 'Provider', kebab: 'provider', archetype: 'provider', kind: 'public', defaultElement: 'div', - role: 'listbox', optional: false, data: [ { attr: 'data-empty', value: v.propRef('empty'), severity: 'optional' }, @@ -78,14 +84,20 @@ export const tagsInputMorfo = { { attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' }, { attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' } ], - aria: [{ attr: 'aria-orientation', value: v.literal('horizontal') }] + aria: [] }, { + // FIRMA 2026-08-25 — the control IS the layout grid the `apg` cites, + // with ONE row. `aria-orientation` is NOT a supported property of + // `grid` (it belongs to the `select` family) and Chrome drops it from + // the AX node — measured; the horizontal reading survives as the + // grid's own Left/Right-walks-cells behaviour, which is what APG + // gives a single-row layout grid. name: 'Control', kebab: 'control', kind: 'public', defaultElement: 'div', - role: 'listbox', + role: 'grid', optional: false, data: [ { attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' }, @@ -93,15 +105,52 @@ export const tagsInputMorfo = { { attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' }, { attr: 'data-focus', value: v.propRef('focus'), severity: 'optional' } ], - aria: [{ attr: 'aria-orientation', value: v.literal('horizontal') }] + aria: [] + }, + { + // The single row of the layout grid. `grid` may own nothing but + // `row` / `rowgroup`, and `row` nothing but cells — so the row is a + // structural wrapper soma renders inside the control, never composed + // by the consumer. `display: contents` in the recipe keeps it out of + // the box tree (measured: identical geometry). + name: 'Row', + kebab: 'row', + kind: 'private', + defaultElement: 'div', + role: 'row', + optional: false, + data: [], + aria: [] + }, + { + // The input's own cell. A `row` may own nothing but cells, so the + // editable field gets one — same structural wrapper treatment as the + // row, same `display: contents`. + name: 'InputCell', + kebab: 'input-cell', + kind: 'private', + defaultElement: 'div', + role: 'gridcell', + optional: false, + data: [], + aria: [] }, { + // FIRMA 2026-08-25 — a plain text field (native `textbox`), no + // explicit role. It used to claim `role='combobox'`, whose two + // REQUIRED properties are `aria-expanded` and `aria-controls`: it + // carried neither, because there is no popup to expand or control — + // `aria-activedescendant` pointed at the tags ALREADY COMMITTED, a + // sibling collection, not at a suggestion list. That reference left + // with the role: the target of `aria-activedescendant` must be a + // descendant (or `aria-owns`-ed) of the element carrying it, and a + // tag never was one. The active tag is now announced by real DOM + // focus on its cell — the grid's own model. name: 'Input', kebab: 'input', archetype: 'input', kind: 'public', defaultElement: 'input', - role: 'combobox', optional: false, data: [ { attr: 'data-disabled', severity: 'optional' }, @@ -109,8 +158,6 @@ export const tagsInputMorfo = { { attr: 'data-invalid', severity: 'optional' } ], aria: [ - { attr: 'aria-autocomplete', value: v.literal('list') }, - { attr: 'aria-activedescendant', value: v.partRef('item'), severity: 'optional' }, { attr: 'aria-invalid', value: v.literal('true'), @@ -131,7 +178,15 @@ export const tagsInputMorfo = { archetype: 'item', kind: 'public', defaultElement: 'div', - role: 'option', + // FIRMA 2026-08-25 — a tag is a CELL of the layout grid, not an + // `option`. `option` is `Children Presentational: true`: the delete + // button inside it was folded into the option's own name + // ("svelte Eliminar etiqueta") and stopped existing as an operable + // button. `gridcell` is `Children Presentational: false` — which is + // the reason APG gives for layout grids: they "group a set of + // interactive elements". `gridcell` also supports `aria-selected`, + // so the stateRef below survives the migration untouched. + role: 'gridcell', optional: false, states: ['active', 'inactive'], data: [ diff --git a/src/uix/soma/components/tags-input/README.md b/src/uix/soma/components/tags-input/README.md index 16c80948a..bb110f62c 100644 --- a/src/uix/soma/components/tags-input/README.md +++ b/src/uix/soma/components/tags-input/README.md @@ -31,6 +31,36 @@ An input field for managing a list of tags. Supports keyboard navigation between | `ItemDeleteTrigger` | `