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 <noreply@anthropic.com>
alpha-0.1-background
dev 1 month ago
parent 466b24f34f
commit d5a27b87e7

@ -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)

@ -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.

@ -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 `<input type="text">` (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

@ -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.
<!-- veredicto:end -->

@ -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

@ -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

@ -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
`<input type="text">` 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

@ -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;

@ -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: [

@ -31,6 +31,36 @@ An input field for managing a list of tags. Supports keyboard navigation between
| `ItemDeleteTrigger` | `<button>` | Remove button for a specific tag. |
| `ClearTrigger` | `<button>` | Remove all tags. Auto-hidden when empty. |
## ARIA structure
The morfo cites the APG **layout grid**, and since 2026-08-25 it speaks it
(F-1, `docs/audit/components/tags-input.md`):
```
[data-tags-input] no role — the provider owns no cell of its own
[data-tags-input-control] role=grid, ONE row
[data-tags-input-row] role=row ← private wrapper, display: contents
[data-tags-input-item] role=gridcell ← the tag, with aria-selected
[…item-text] the tag's text
[…item-delete-trigger] role=button — operable, because a gridcell is
NOT "children presentational" (an `option` is)
[data-tags-input-input-cell] role=gridcell ← private, display: contents
[data-tags-input-input] plain text field (native textbox)
[data-tags-input-clear-trigger] role=button, outside the grid
```
`Row` and `InputCell` are `private` parts: soma renders them, the consumer
never composes them — a `grid` may own nothing but rows, and a `row` nothing
but cells. The recipe puts both in `display: contents`, so they cost zero
boxes.
The field is NOT a `combobox`: that role REQUIRES `aria-expanded` and
`aria-controls`, and there is no popup to expand. For the same reason it
carries no `aria-activedescendant` — the highlight moves real DOM focus onto
the tag's cell (the grid's own focus model, which the roving `tabindex` here
already implements). Exactly one tag reports `aria-selected: true` while
navigating, none at rest.
## Data Attributes
| Part | Attribute | Values |

@ -2,7 +2,7 @@
import { partOpts } from '../../../provider';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { TagsInputControlProvider } from '../tags-input-provider.svelte';
import { TagsInputControlProvider, TagsInputRowProvider } from '../tags-input-provider.svelte';
import type { TagsInputControlProps } from '../types';
const uid = $props.id();
@ -22,11 +22,25 @@
)
);
// The grid's single row. `grid` may own nothing but rows, so this wrapper is
// structural and private: soma renders it, the consumer never composes it.
// Plain holder for the ref — nothing reads it, and the `state` const above
// shadows the `$state` rune in this scope.
const rowId = createId(uid, 'tags-input-row');
let rowRef: HTMLElement | null = null;
const row = TagsInputRowProvider.create(
partOpts(
() => rowId,
() => rowRef,
(v) => (rowRef = v)
)
);
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>{@render children?.()}</div>
<div {...mergedProps}><div {...row.props}>{@render children?.()}</div></div>
{/if}

@ -2,7 +2,10 @@
import { partOpts } from '../../../provider';
import { mergeProps } from '../../../props';
import { createId } from '$active-uix/id';
import { TagsInputInputProvider } from '../tags-input-provider.svelte';
import {
TagsInputInputProvider,
TagsInputInputCellProvider
} from '../tags-input-provider.svelte';
import type { TagsInputInputProps } from '../types';
const uid = $props.id();
@ -22,11 +25,26 @@
)
);
// A `row` may own nothing but cells, so the editable field travels in one.
// Plain holder for the ref — nothing reads it, and the `state` const above
// shadows the `$state` rune in this scope.
const cellId = createId(uid, 'tags-input-input-cell');
let cellRef: HTMLElement | null = null;
const cell = TagsInputInputCellProvider.create(
partOpts(
() => cellId,
() => cellRef,
(v) => (cellRef = v)
)
);
const mergedProps = $derived(mergeProps(restProps, state.props));
</script>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<input {...mergedProps} />
{/if}
<div {...cell.props}>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<input {...mergedProps} />
{/if}
</div>

@ -139,14 +139,19 @@ describe('TagsInputProvider', () => {
expect(result.provider.isEmpty).toBe(false);
expect(result.provider.isAtMax).toBe(false);
// FIRMA 2026-08-25 — the field is a plain textbox: no `role`, no
// `aria-autocomplete`, no `aria-activedescendant`. It never had the two
// properties `combobox` REQUIRES (aria-expanded / aria-controls) because
// there is no popup.
expect(result.input.props).toMatchObject({
id: 'tags-input',
type: 'text',
role: 'combobox',
'aria-autocomplete': 'list',
value: '',
placeholder: undefined
});
expect(result.input.props).not.toHaveProperty('role');
expect(result.input.props).not.toHaveProperty('aria-autocomplete');
expect(result.input.props).not.toHaveProperty('aria-activedescendant');
expect(result.provider.addTag(' gamma ')).toBe(true);
expect(opts.value.current).toEqual(['alpha', 'beta', 'gamma']);
@ -162,10 +167,14 @@ describe('TagsInputProvider', () => {
expect(result.provider.isAtMax).toBe(true);
await tick();
expect(root.getAttribute('role')).toBe('listbox');
expect(root.getAttribute('aria-orientation')).toBe('horizontal');
// FIRMA 2026-08-25 — the provider carries no role (it owned no option of
// its own) and the control IS the layout grid the `apg` cites.
// `aria-orientation` went with the listbox: `grid` does not support it.
expect(root.hasAttribute('role')).toBe(false);
expect(root.hasAttribute('aria-orientation')).toBe(false);
expect(root.hasAttribute('data-empty')).toBe(false);
expect(controlEl.getAttribute('role')).toBe('listbox');
expect(controlEl.getAttribute('role')).toBe('grid');
expect(controlEl.hasAttribute('aria-orientation')).toBe(false);
expect(controlEl.getAttribute('data-focus')).toBe('');
cleanup();
@ -274,7 +283,9 @@ describe('TagsInputProvider', () => {
expect(focusSpy).toHaveBeenLastCalledWith(secondEl);
expect(result.second.props).toMatchObject({
id: 'tag-beta',
role: 'option',
// FIRMA 2026-08-25 — a tag is a CELL of the control's layout grid.
// `gridcell` supports aria-selected, so the stateRef below is intact.
role: 'gridcell',
// FIRMA 2026-08-24 — aria-selected follows the roving highlight
// (morfo `stateRef('active')` → the per-part `active` state soma
// registers). The HIGHLIGHTED tag is the only one that reports true.
@ -287,7 +298,7 @@ describe('TagsInputProvider', () => {
// made impossible: before the firma both tags said 'true' at rest.
expect(result.first.props).toMatchObject({
id: 'tag-alpha',
role: 'option',
role: 'gridcell',
'aria-selected': 'false',
tabindex: -1,
'data-state': 'inactive'
@ -301,6 +312,11 @@ describe('TagsInputProvider', () => {
result.second.onkeydown(left as never);
expect(left.preventDefault).toHaveBeenCalledOnce();
expect(result.provider.highlightedIndex).toBe(0);
// FIRMA 2026-08-25 — the highlight MOVES DOM focus onto the cell. Before
// it only changed the index, so the Item's own key plan never fired from
// the keyboard (focus stayed on the input) and the roving tabindex was a
// preparation nobody used.
expect(focusSpy).toHaveBeenLastCalledWith(firstEl);
const escape = keyEvent('Escape', firstEl);
result.first.onkeydown(escape as never);

@ -234,6 +234,17 @@ export class TagsInputProvider {
// ── Highlight navigation ─────────────────────────────────────────────────
/**
* Move DOM focus onto the highlighted tag's cell. The grid's focus model is
* the roving one the `tabindex` below already prepares: without this move
* the highlight travelled but focus never left the input, so the Item's own
* key plan (Arrow/Home/End/Backspace/Escape) only ever fired after a MOUSE
* click and the highlight could not advance past the first hop.
*/
private focusHighlighted() {
this.soma.dom.focus(this.getItemElement(this.highlightedIndex) ?? null);
}
highlightPrev() {
const count = this.opts.value.current.length;
if (count === 0) return;
@ -243,6 +254,7 @@ export class TagsInputProvider {
this.soma.dom.focus(this.inputRef.current);
} else {
this.highlightedIndex--;
this.focusHighlighted();
}
}
@ -254,12 +266,14 @@ export class TagsInputProvider {
this.soma.dom.focus(this.inputRef.current);
} else {
this.highlightedIndex++;
this.focusHighlighted();
}
}
highlightFirst() {
if (this.opts.value.current.length > 0) {
this.highlightedIndex = 0;
this.focusHighlighted();
}
}
@ -267,6 +281,7 @@ export class TagsInputProvider {
const count = this.opts.value.current.length;
if (count > 0) {
this.highlightedIndex = count - 1;
this.focusHighlighted();
}
}
@ -318,6 +333,63 @@ export class TagsInputControlProvider {
);
}
// ── Row ──────────────────────────────────────────────────────────────────────
interface TagsInputRowOpts extends WithRefOpts {}
/**
* The grid's single row. A structural wrapper the Control renders around its
* own children — never composed by the consumer.
*/
export class TagsInputRowProvider {
readonly opts: TagsInputRowOpts;
readonly runtimePart: SomaRuntimePart<typeof tagsInputMorfo, 'row'>;
static create(opts: TagsInputRowOpts) {
return new TagsInputRowProvider(opts);
}
readonly provider: TagsInputProvider;
private constructor(opts: TagsInputRowOpts) {
this.opts = opts;
this.provider = TagsInputProvider.require();
this.runtimePart = this.provider.runtime.part('row', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly props = $derived.by(() => this.runtimePart.assert(this.runtimePart.renderProps()));
}
// ── InputCell ────────────────────────────────────────────────────────────────
interface TagsInputInputCellOpts extends WithRefOpts {}
/** The cell that owns the editable field. Rendered by the Input part. */
export class TagsInputInputCellProvider {
readonly opts: TagsInputInputCellOpts;
readonly runtimePart: SomaRuntimePart<typeof tagsInputMorfo, 'input-cell'>;
static create(opts: TagsInputInputCellOpts) {
return new TagsInputInputCellProvider(opts);
}
readonly provider: TagsInputProvider;
private constructor(opts: TagsInputInputCellOpts) {
this.opts = opts;
this.provider = TagsInputProvider.require();
this.runtimePart = this.provider.runtime.part('input-cell', {
id: opts.id,
ref: opts.ref,
owner: this
});
}
readonly props = $derived.by(() => this.runtimePart.assert(this.runtimePart.renderProps()));
}
// ── Input ────────────────────────────────────────────────────────────────────
interface TagsInputInputOpts extends WithRefOpts {}
@ -407,28 +479,14 @@ export class TagsInputInputProvider {
this.provider.clearHighlight();
};
/** ID of the currently highlighted item for aria-activedescendant. */
private get highlightedItemId(): string | undefined {
const idx = this.provider.highlightedIndex;
if (idx < 0) return undefined;
// Items register in DOM order — find the item element by index
const control = this.provider.opts.ref?.current;
if (!control) return undefined;
const items = control.querySelectorAll(`[${attrs.item}]`);
return items[idx]?.id || undefined;
}
readonly props = $derived.by(() =>
this.runtimePart.assert({
// Morfo-declared (role=combobox, aria-autocomplete, aria-invalid via
// the provider-level `invalid` source) come from renderProps.
// Morfo-declared (aria-invalid via the provider-level `invalid`
// source) comes from renderProps.
...this.runtimePart.renderProps(),
// Soma-only: native input attrs the morfo doesn't model + the
// computed active-descendant id (morfo's partRef('item') can't pick
// the highlighted instance) + state-derived data-* (morfo declares
// them valueless) + handlers.
// Soma-only: native input attrs the morfo doesn't model +
// state-derived data-* (morfo declares them valueless) + handlers.
type: 'text' as const,
'aria-activedescendant': this.highlightedItemId,
value: this.provider.opts.inputValue.current,
placeholder: this.provider.isEmpty
? this.provider.opts.placeholder.current || undefined

@ -29,7 +29,12 @@ const MIGRATED_CEILINGS: Record<string, number> = {
'search-field': 1, // input aria-label — computed, yields to Field.Label's labelledby
'drag-drop': 3,
stepper: 4,
'tags-input': 1,
// Was 1 — the input's hand-written `aria-activedescendant`, the one value the
// morfo could not express (partRef('item') cannot pick the highlighted
// instance). FIRMA 2026-08-25 retired the attribute with the `combobox` role
// that legitimised it, so the residue is 0. The ceiling only fails by EXCESS:
// leaving it at 1 would have passed in green while permitting one more.
'tags-input': 0,
'color-field': 4,
'password-field': 4,
'link-preview': 0,

@ -617,9 +617,9 @@
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr><td class="name">Root</td><td>Provider and control expose horizontal listbox semantics.</td></tr>
<tr><td class="name">Input</td><td>Input is a combobox with autocomplete and active descendant support.</td></tr>
<tr><td class="name">Items</td><td>Tags are options with active/inactive state and delete triggers.</td></tr>
<tr><td class="name">Root</td><td>The control is a single-row layout grid (APG); the provider carries no role.</td></tr>
<tr><td class="name">Input</td><td>Plain text field in its own cell — no combobox, because there is no popup.</td></tr>
<tr><td class="name">Items</td><td>Tags are grid cells with active/inactive state; their delete button stays operable.</td></tr>
<tr><td class="name">Keyboard</td><td>Enter adds; Backspace deletes previous when input is empty; arrows navigate tags.</td></tr>
<tr><td class="name">Readonly</td><td>Readonly blocks add/remove while keeping existing tags readable.</td></tr>
</tbody>

Loading…
Cancel
Save

Powered by TurnKey Linux.