fix(morfo)!: tags-input - aria-selected sigue al resalte, no a un literal

`v.literal('true')` era el UNICO aria-selected incondicional de los 18 morfos
que declaran el atributo: el AX tree entregaba DOS option selected:true dentro
de un listbox multiselectable:false (mentira de plataforma), archetypes.css
(0,4,0) clavaba el velo de estado EN REPOSO (la etiqueta vivia en su propio
hover: reposo y hover byte-identicos) y `--tags-input-item-fg` no habia
pintado nunca.

Ahora `v.stateRef('active')` - el estado que soma YA deriva de
highlightedIndex, registrado como fuente por parte (states: { active }).
Precedente en casa: tag-group/listbox/tabs. `severity: 'optional'` NO hace
falta (inerte: solo la leen los checks de data-*, verificado en compile.ts).

Medido (ejecutor E-tags, heredando y VERIFICANDO la edicion de un ejecutor
interrumpido - dos claims sin medir corregidos, uno de atribucion: el tono de
la activa lo enmascara [data-highlighted] (0,3,0), no este atributo):
- Sonda: 14 diffs / 2.504 valores, UNA propiedad (backgroundImage), las dos
  etiquetas x 7 pases sin puntero - el velo saliendo del reposo. Hover: 0
  diffs = la afordancia recuperada (reposo 224,58 / hover 212,10; antes
  212,10 = 212,10). Capturas antes/despues revisadas por el supervisor.
- AX: reposo false,false - ArrowLeft false,TRUE - Escape false,false.
  Exactamente una navegando, cero en reposo.
- Centinela 63/91 -> 71/91 (revive item-fg; medida directa oklch(0.2435) ->
  rgb(1,2,3)), exit 0, 0 sin adjudicar, 0 STALE, ceguera-a-patrones validada
  a mano (17 cubiertas, todas muertas).
- Gates: morfo 222/222 - soma tags-input 3/3 - eidos 442+1 ajeno - check 0
  atribuibles - docs-check 0/0 - eidos-lint 0 invalid.

Quedan ABIERTOS y registrados en next-features 13 (sin decidir aqui): el
morfo declara apg:grid pero implementa listbox/option - Provider y Control
llevan ambos role=listbox anidados - la tinta de tono de la activa la
enmascara [data-highlighted] (12.5) - migracion a grid (B-b) como
continuacion. Y una observacion de soma al paso: ArrowLeft desde el input no
saca el foco (:381).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent df3a4d054b
commit 2ce43db563

@ -173,4 +173,36 @@ AUSENCIA (`null`) de `data-focus`, `data-invalid` y `data-disabled` lo
devuelve, y con ello `invalid-control-border` y `disabled-opacity`: **60/91 →
63/91 sin adjudicar una clave de más**.
### Nota del 2026-08-25 — `item-fg` YA PINTA: la firma `aria-selected` ejecutada
Lo que arriba queda anotado como «toca morfo ⇒ se mide, se registra y se
sigue» se firmó y se ejecutó (opción **B-a2**, PLAN §8). El morfo ata
`aria-selected` al estado que soma ya deriva de `highlightedIndex`
(`v.literal('true')` → **`v.stateRef('active')`**), y soma registra la fuente
por parte; nadie inventa selección. Medido de nuevo, entero:
- **`item-fg` alcanza**: `oklch(0.2435 0 0)` → `rgb(1, 2, 3)` escribiendo el
token en `:root` sobre la etiqueta real, y vuelve al retirarlo. **El
centinela pasa a 71/91 con 20 adjudicadas** (era 63/91 con 28: siete claves
de tono las devolvió la firma B′ y la octava es ésta), exit 0, **cero sin
adjudicar y cero STALE**. Su entrada del ledger se RETIRA.
- **A11y**: el árbol AX daba **dos `option` con `selected: true` dentro de dos
`listbox` con `multiselectable: false`**; ahora **0 en reposo, exactamente 1
navegando y 0 tras `Escape`**.
- **Píxel, y es el que faltaba**: con el literal, reposo y hover de la misma
etiqueta salían **byte a byte idénticos** (media RGB 212,10 y 212,10, puntero
real a 2×) — la etiqueta vivía permanentemente en su propio hover. Ahora
**224,58 en reposo, 212,10 en hover**. El diff de la sonda son **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, 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.
<!-- veredicto:end -->

@ -506,6 +506,12 @@ Lo que `tags-input` añadió (2026-08-24):
píxel (las etiquetas perderían el velo en reposo) ⇒ no se toca desde el eje
de theming; los dos tokens quedan adjudicados en el ledger de R-5.4 con esa
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.
### Qué habría que decidir
@ -1917,6 +1923,53 @@ 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.
**Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando
estorben.

@ -135,9 +135,23 @@ guarda `active-eidos-config.test.ts` · ledger `scripts/theming-sentinel-excepti
Detalle en §8 del PLAN.
4. **Las cuatro claves de `avatar` fuera de gramática** (`*-color-custom*`,
la ranura de tinta es `fg`). Son contrato público: renombrar es romper.
5. **`tags-input`** sella `aria-selected='true'` como LITERAL en cada etiqueta,
así que `archetypes.css` le fija la tinta en reposo y su token no ha
pintado nunca. Tiene cara de defecto de a11y.
5. ~~**`tags-input`** sella `aria-selected='true'` como LITERAL en cada
etiqueta~~ — **✅ FIRMADA Y EJECUTADA 2026-08-25** (opción B-a2): el morfo
ata `aria-selected` al estado que soma YA deriva de `highlightedIndex`
(`v.literal('true')` → `v.stateRef('active')`), una línea. Era el **único
`aria-selected` incondicional del catálogo**. El árbol AX pasa de **dos
`option` con `selected: true` dentro de dos `listbox` con
`multiselectable: false`** a **cero en reposo y exactamente uno navegando**
(medido por CDP). `--tags-input-item-fg` **revive** —el centinela sube a
**71/91** y su entrada del ledger se RETIRA— y la etiqueta **recupera la
afordancia de hover**: reposo y hover eran byte a byte IDÉNTICOS (media RGB
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.
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.

@ -521,6 +521,59 @@ excepción abre la mano para MEDIR, no para contabilizar.
## 8. Registro
- 2026-08-25 — **FIRMA: `tags-input` deja de sellar `aria-selected` como
LITERAL** (opción B-a2). Una línea de morfo:
`{ attr: 'aria-selected', value: v.literal('true') }` →
`v.stateRef('active')`, el estado que soma **ya** deriva de
`highlightedIndex` — soma sólo registra la fuente por parte
(`states: { active: () => this.isHighlighted }` en `TagsInputItemProvider`),
así que `renderProps()` lo resuelve desde el morfo en vez de re-declararlo.
Era el **único `aria-selected` incondicional del catálogo**: 19
declaraciones en 18 morfos, 8 por `v.stateRef`, 9 por `v.propRef` y las dos
de `tree-view` con `condition`. **Sin `severity`**, como los dos precedentes
más cercanos (`listbox:129`, `role=option`; `tabs:96`, el mismo estado
`active`); `tag-group` la lleva pero es INERTE — `compile.ts:583` la guarda y
nadie la lee, `morfo-check.ts:154` y `permutation-check.ts:97` sólo miran la
de los `data-*`. El modo compilado es `aria-boolean`
(`compile.ts:577`/`:702`), que emite SIEMPRE `'true'` o `'false'`: nunca
ausencia.
**Lo que arregla, medido.** (1) **A11y de raíz**: el árbol AX por CDP
entregaba **dos `option` con `selected: true` dentro de dos `listbox` con
`multiselectable: false`** — contradicción en la plataforma, no sólo en el
atributo; ahora da **0 seleccionadas en reposo, exactamente 1 navegando con
el teclado y 0 tras `Escape`**. (2) **Revive `--tags-input-item-fg`**, que no
había pintado nunca: escrito en `:root` sobre la etiqueta real,
`oklch(0.2435 0 0)` → `rgb(1, 2, 3)` (y vuelve al retirarlo). El centinela
pasa de **63/91 con 28 adjudicadas** (2026-08-24) a **71/91 con 20**, exit 0,
**cero sin adjudicar y cero STALE** — 7 de las 8 nuevas vivas son de la firma
B′ y la octava es `item-fg`, cuya entrada del ledger se RETIRA. (3)
**Devuelve la afordancia de hover**, que no existía: con el literal, reposo y
hover de la misma etiqueta eran **byte a byte IDÉNTICOS** (media RGB 212,10 y
212,10, puntero real, 2×); ahora **224,58 en reposo y 212,10 en hover**.
**Diff de sonda: 14 valores de 2.504**, y los catorce son el mismo:
`background-image` de las DOS etiquetas en los siete pases sin puntero
(`rest`, las cinco tallas y `open`), del velo `linear-gradient(…/0.08)` a
`none`. El pase `hover` sale con **0 diferencias** — el velo del hover era y
sigue siendo el mismo, y ésa es justo la razón de que el reposo no se
distinguiera. Ninguna otra propiedad, ningún nodo de más ni de menos.
Reproducido a mano sobre CÓDIGO IDÉNTICO (re-estampando `aria-selected='true'`
en el árbol vivo): los cuatro números vuelven.
**Una atribución CORREGIDA de la ficha anterior**: lo que enmascara la tinta
de TONO de la etiqueta activa **no es `aria-selected`** sino
`[data-archetype='item'][data-highlighted]` **(0,3,0)**, que sigue ganando a
`[data-tags-input-item][data-state='active']` (0,2,0). Re-medido sobre la
etiqueta real navegada por TECLADO: quitar `aria-selected` no cambia nada
(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 lo atribuía a
`aria-selected` porque forzaba `data-state=active` a mano, sin
`data-highlighted`. El ledger recoge la corrección; **el hueco sigue abierto
en next-features §13**, junto con los otros dos que la firma destapa (el
`apg: grid` que el morfo declara e implementa como listbox/option, y los dos
`role='listbox'` anidados de Provider y Control). Cinco ficheros:
morfo, provider, su test, el README de la receta y el ledger. Gates: `morfo`
entero 222/222, `soma/components/tags-input` 3/3, `eidos` 442/443 (el rojo
ajeno conocido, `lint.test.ts` / `skin-media-player`).
- 2026-08-24 — **FIRMA: el bloque de acción de `form` apuntaba a las partes
EQUIVOCADAS.** `form.css:85-190` metía cuatro nodos en el mismo grupo:
`[data-form-submit]`, `[data-form-reset]` y los dos botones del array de

@ -63,12 +63,17 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
lift: 'the guard does not snapshot `translate` (the `transform` limit of background / rating-group / card on the sibling property) and its static pass never hovers, while this rule is `[data-fab]:hover:not([data-disabled]):not(:active)`. Measured by hand with a real pointer: set --fab-lift=1234px BEFORE hovering and the FAB computes `translate: 0px -1234px` at t+300ms -> reaches. Note the self-cancel that makes it unmeasurable even with a hover pass: a 1234px lift carries the node out from under the cursor, :hover drops and it settles back to none by t+900ms'
},
'tags-input': {
// 2026-08-24. Four reasons, all measured on the real nodes with a real
// 2026-08-24. THREE reasons, all measured on the real nodes with a real
// pointer. Of the twenty-four tone keys the old palette defect buried,
// SEVEN came back alive with firma B′; the seventeen still dead ride the
// two PATTERN entries below, and neither is the old supersession.
'item-fg':
"the ARCHETYPE ink always wins: the morfo stamps aria-selected='true' as a LITERAL on EVERY tag, so archetypes.css's `[data-archetype='item'][aria-selected='true']:not([data-state='checked']):not([data-state='on'])` (0,4,0) out-ranks the recipe's `[data-tags-input-item]` (0,1,0) on every item, at rest and always. Measured 2026-08-24: with the attribute as shipped the token moves nothing (the ink stays oklch(0.2435 0 0) = --color-content-primary, the archetype's); set aria-selected=false on the real tag and the same write reaches (rgb(1, 2, 3)). Same class as table.selected-row-fg and the listbox ink (next-features §12); the aria literal is a morfo decision and is registered, not touched from this axis",
// `item-fg` was the fourth and is GONE (2026-08-25): the morfo stopped
// stamping aria-selected='true' as a literal on every tag (FIRMA B-a2 —
// it now follows the roving highlight), so the token MOVES — measured on
// the real tag, oklch(0.2435 0 0) -> rgb(1, 2, 3) writing it on :root,
// and back when the write is withdrawn. With the entry still here the
// guard's own STALE detector would call it in (`live.filter(k => k in
// ledger)`). Run of 2026-08-25: 71/91, 20 adjudicated, 0 unadjudicated.
'placeholder-fg':
"the input carries a `placeholder` attribute only while it is EMPTY (soma: `isEmpty ? opts.placeholder : undefined`) and the stage boots with two tags, so ::placeholder returns the ELEMENT's style — the trap already measured on field. Emptying the stage is not an option for this run: it deletes every item node, which is where a third of the contract paints. Measured both ways 2026-08-24: clearing the tags through the demo's own Clear trigger gives the input placeholder=\"Add tag\", and forcing the attribute on the real input makes the token reach (rgb(1, 2, 3); its default is oklch(0.61 0 0) = --color-content-muted)",
'transition-duration':
@ -1577,7 +1582,7 @@ export const SENTINEL_PATTERN_EXCEPTIONS: Array<{
pattern: /^(secondary|neutral|affirm|fulfill|risk|threat|loss)-(track|text)$/,
reason:
TONE_UNREACHED +
' — the three slots share ONE rule, [data-tags-input-item][data-state=active], and the guard sweeps data-focus / data-invalid / data-disabled for this component, never data-state; `border` reads alive because the CONTROL paints it under data-focus, which IS swept. Measured 2026-08-24 forcing data-state=active on the real tag with the tone stamped: --tags-input-secondary-track repaints it (oklch(0.9913 0.0013 286.38) -> rgb(1, 2, 3)) and --tags-input-secondary-text the ink. The eight `-text` keys carry a SECOND cause on top: the archetype ink beats the recipe on every tag (see the item-fg entry above)'
' — the three slots share ONE rule, [data-tags-input-item][data-state=active], and the guard sweeps data-focus / data-invalid / data-disabled for this component, never data-state; `border` reads alive because the CONTROL paints it under data-focus, which IS swept. Measured 2026-08-24 forcing data-state=active on the real tag with the tone stamped: --tags-input-secondary-track repaints it (oklch(0.9913 0.0013 286.38) -> rgb(1, 2, 3)) and --tags-input-secondary-text the ink. The eight `-text` keys carry a SECOND cause on top, and it SURVIVES the aria-selected firma of 2026-08-25: what masks the ink of the highlighted tag is [data-archetype=item][data-highlighted]:not([data-state=checked]):not([data-state=on]) at (0,3,0), which still out-ranks the recipe rule [data-tags-input-item][data-state=active] at (0,2,0). Re-measured on the real tag driven by the KEYBOARD: stripping aria-selected alone changes nothing (the ink stays oklch(0.2435 0 0)); stripping data-highlighted too is what lets the tone through (oklch(0.5168 0.1733 305.88)). The old wording blamed aria-selected through the retired item-fg entry, and that attribution was wrong'
},
{
component: 'toast',

@ -36,16 +36,33 @@ control enfocado y los tres slots de la etiqueta activa. Un público encima
dejaría que un tema los fijara y matara en silencio el `color=` de cada
instancia.
**Lo que el guard da por silencioso** (28 de 91, todas medidas): las 24 claves
de tono, anuladas por la cascada de paleta (el bloque genérico
`[data-tags-input][data-color]` se emite el ÚLTIMO a la misma especificidad);
`transition-duration` y `transition-ease`, que SON la transición que el guard
congela; `placeholder-fg`, porque soma sólo pone el atributo `placeholder`
mientras el input está VACÍO y el escenario arranca con dos etiquetas; y
`item-fg` — que no es límite del instrumento sino del componente: el morfo
sella `aria-selected='true'` como LITERAL en cada etiqueta, así que la tinta de
`archetypes.css` (0,4,0) gana siempre a la de la receta (0,1,0). Registrado
como incidencia; el literal del morfo no se toca desde este eje.
**Lo que el guard da por silencioso** (20 de 91, todas medidas): 17 de las 24
claves de tono, anuladas por la cascada de paleta (el bloque genérico
`[data-tags-input][data-color]` se emite el ÚLTIMO a la misma especificidad; la
firma B′ devolvió siete a la vida); `transition-duration` y `transition-ease`,
que SON la transición que el guard congela; y `placeholder-fg`, porque soma sólo
pone el atributo `placeholder` mientras el input está VACÍO y el escenario
arranca con dos etiquetas.
**`item-fg` ya PINTA (firma 2026-08-25).** Era la cuarta silenciosa y no era
límite del instrumento sino del componente: el morfo sellaba
`aria-selected='true'` como LITERAL en cada etiqueta, así que la tinta de
`archetypes.css` (0,4,0) ganaba siempre a la de la receta (0,1,0) —en reposo y
siempre— y además dejaba a la etiqueta sin respuesta al puntero (reposo y hover
salían idénticos byte a byte). El morfo ata ahora `aria-selected` al resalte que
soma ya deriva (`stateRef('active')`), así que en reposo no hay ninguna
seleccionada y al navegar hay exactamente una. El centinela lo confirma: el
token lee VIVO, su entrada del ledger se RETIRA y la medida pasa a **71/91**
(20 adjudicadas, cero sin adjudicar, cero STALE). Medido aparte sobre la
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)`).
## Baseline

@ -141,7 +141,20 @@ export const tagsInputMorfo = {
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-selected', value: v.literal('true') },
// FIRMA 2026-08-24 — the selection state FOLLOWS the roving
// highlight. It used to be `v.literal('true')`, the only
// UNCONDITIONAL literal `aria-selected` in the catalogue
// (tree-view's two carry a `condition`): the AX tree handed a
// screen reader EVERY tag as `selected: true` inside a listbox
// reporting `multiselectable: false`, and the archetype's selected
// chrome (`archetypes.css`, (0,4,0)) pinned ink and state veil on
// every tag AT REST — so `--tags-input-item-fg` had never painted
// and a resting tag had no hover affordance left (rest and hover
// were byte-identical). `active` is the state soma already derives
// from `highlightedIndex`, so exactly one tag reports selected
// while the user navigates and none at rest. Precedent:
// tag-group / listbox / tabs.
{ attr: 'aria-selected', value: v.stateRef('active') },
{
attr: 'aria-disabled',
value: v.literal('true'),

@ -275,13 +275,23 @@ describe('TagsInputProvider', () => {
expect(result.second.props).toMatchObject({
id: 'tag-beta',
role: 'option',
// renderProps emits the morfo's literal ARIA value as the canonical
// ARIA STRING (DOM-identical to the pre-migration boolean).
// 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.
'aria-selected': 'true',
tabindex: 0,
'data-state': 'active',
'data-highlighted': ''
});
// …and every other tag reports FALSE. This is the assertion the literal
// made impossible: before the firma both tags said 'true' at rest.
expect(result.first.props).toMatchObject({
id: 'tag-alpha',
role: 'option',
'aria-selected': 'false',
tabindex: -1,
'data-state': 'inactive'
});
expect(result.text.props).toMatchObject({
id: 'tag-beta-text',
'data-state': 'active'

@ -485,7 +485,12 @@ export class TagsInputItemProvider {
// Per-part source: the item's effective disabled (item-local OR
// provider-level) so the morfo's `aria-disabled` condition resolves
// via renderProps instead of being re-declared here.
props: { disabled: () => this.isDisabled }
props: { disabled: () => this.isDisabled },
// Per-part state source for the morfo's `aria-selected` (stateRef
// 'active', FIRMA 2026-08-24). The roving highlight is per-item, so it
// is registered per-instance here → `renderProps()` resolves
// aria-selected from the morfo instead of soma re-declaring it.
states: { active: () => this.isHighlighted }
});
}
@ -530,8 +535,9 @@ export class TagsInputItemProvider {
readonly props = $derived.by(() =>
this.runtimePart.assert({
// role=option, aria-selected, aria-disabled (from the per-part
// `disabled` source) are morfo-declared → renderProps.
// role=option, aria-selected (from the per-part `active` state) and
// aria-disabled (from the per-part `disabled` source) are
// morfo-declared → renderProps.
...this.runtimePart.renderProps(),
// Soma-only: roving tabindex + state-derived data-* (morfo declares
// data-value/data-state/data-highlighted/data-disabled valueless) +

Loading…
Cancel
Save

Powered by TurnKey Linux.