uix(tags-input): temable — 88 % → 90 % CERRADO, y su tinta no ha pintado nunca

Los cinco knobs fuera de alcance se clasificaron UNO A UNO antes de tocar una
línea, que es la mitad del trabajo:

- **uno era la COSTURA**: `[data-tags-input-control][data-invalid]` leía
  `var(--color-risk-border)` A PELO. Entra como
  `--tags-input-invalid-control-border`, valor verbatim, con el nombre Y el
  default de `field.invalid-control-border` — la misma parte del mismo chasis.
  NO se reusó el `risk-border` de la paleta: la cascada `[data-color]` lo
  re-enruta por instancia y se llevaría con él el estado inválido, que no es
  una elección de tono. `global` a CERO.
- **los otros cuatro son el MISMO puente THM-2**: el borde del control
  enfocado y los tres slots de la etiqueta activa leen
  `--_tags-input-palette-{border,track,text}`. Ni conmutador ni escala por
  talla. No se acuñan (CONTINUE §3.pre): un público encima dejaría que un tema
  los fijara y matara en silencio el `color=` por instancia.

**Así que 90 % ES el techo, y es doctrina bien aplicada, no deuda.** Los siete
privados dimensionales ya derivan de públicos; sus bloques `[data-size]` del
CSS SE QUEDAN porque aquí no los emite el TSC (las claves de talla son planas,
sin `scope: 'size:{k}'`) — retirarlos rompería el eje. Migrar esa escala al TSC
es cambio de mecanismo, no de alcance, y queda fuera.

Censo: 46 knobs · público 37 → 38 · global 1 → 0 · privado 4 (el puente) ·
sistema 4 (fuera del ratio) · 88 % → 90 %. Contrato 90 → 91 claves.
`--names`: 0 desviadas (67 conformes + 24 role-slot canónicas).

Diff de computed: VACÍO en las dos superficies. 2.496 valores × 8 estados en el
escenario por defecto (válido, sonda estándar, 10 nodos) y 459 valores × 5
tallas + 3 variantes + hover en el estado INVÁLIDO, con una sonda de un solo
uso porque la demo arranca válida. Captura 2× antes/después byte a byte
idéntica. No se tocó ninguna regla `:hover`, así que el paso 12 no aplica.

Centinela R-5.4: **63/91**, 28 adjudicadas, cuatro razones y todas MEDIDAS:

1. **Las 24 claves de tono — la cascada de paleta** (excepción por PATRÓN). El
   wrapper resuelve `color` y estampa `data-color` en TODA instancia, incluida
   la del default, así que gana el bloque genérico
   `[data-tags-input][data-color], [data-tags-input][data-color-custom]`, que
   se emite el ÚLTIMO a la misma (0,2,0) que los ocho por tono. Medido sobre la
   etiqueta real forzada a `data-state=active`: `--tags-input-primary-border`
   no mueve nada (el borde sigue en `oklch(0.8043 0.0918 309.69)`) mientras
   `--palette-border` en el mismo nodo lo repinta (`rgb(4, 5, 6)`); quita
   `data-color` y `--tags-input-primary-border` vuelve a pintar
   (`rgb(1, 2, 3)`).
2. **`transition-duration` / `transition-ease`** SON la transición que el guard
   congela. Medidos sin congelar: 0.12s → 4.321s y
   `cubic-bezier(0.4, 0, 0.2, 1)` → `steps(3)`.
3. **`placeholder-fg`**: soma sólo pone el atributo `placeholder` mientras el
   input está VACÍO (`isEmpty ? opts.placeholder : undefined`) y el escenario
   arranca con dos etiquetas — y `::placeholder` sobre un input SIN placeholder
   devuelve el estilo del ELEMENTO (la trampa ya medida en `field`). Vaciar el
   escenario no vale para esa corrida: borra los nodos de etiqueta, donde pinta
   un tercio del contrato. Medido de las dos formas: pulsando el propio `Clear`
   de la demo el input queda con `placeholder="Add tag"`, y forzando el
   atributo sobre el input real el token alcanza (`rgb(1, 2, 3)`; default
   `oklch(0.61 0 0)` = `--color-content-muted`).
4. **`item-fg` — y ésta NO es del instrumento, es del COMPONENTE.** El morfo
   sella `aria-selected='true'` como LITERAL en CADA etiqueta, así que la regla
   de `archetypes.css`
   `[data-archetype='item'][aria-selected='true']:not([data-state='checked']):not([data-state='on'])`
   **(0,4,0)** gana siempre a `[data-tags-input-item]` **(0,1,0)**: la tinta de
   la etiqueta es la del arquetipo y su velo de estado se pinta en TODAS las
   etiquetas en reposo. Medido: con el atributo tal como se envía el token no
   mueve nada (`oklch(0.2435 0 0)`, la del arquetipo); con `aria-selected=false`
   sobre la etiqueta real, la misma escritura alcanza (`rgb(1, 2, 3)`). Los
   tres slots `*-text` del tono llevan esta SEGUNDA causa encima de la primera.
   Es la clase de `table.selected-row-fg` y la tinta de `listbox`. Toca morfo Y
   mueve píxel ⇒ se mide, se REGISTRA (next-features §12, con su cara de a11y:
   un lector anuncia todas las etiquetas como seleccionadas) y se sigue.

Lo que el instrumento aprendió: **el guard ABRE haciendo clic en el control**,
lo que estampa `data-focus`, y la regla de foco re-tinta el borde a la misma
(0,2,0) pero más tarde — así que `control-border`, un token perfectamente vivo,
leía muerto. Es la clase de `command.input-border`, otra vez, y la cura es
barrer la AUSENCIA (`null`) de `data-focus`, `data-invalid` y `data-disabled`:
**60/91 → 63/91 sin adjudicar una clave de más**, y de paso reviven
`invalid-control-border` y `disabled-opacity`. Es una entrada por componente:
no toca a nadie más. `fab` re-verificado tras el cambio del guard: 11/12, sin
regresión ni STALE.

Guards: censo `--only tags-input` 90 % (global 68 %, no baja) · `--names` 0
desviadas · `component-audit --only tags-input` PASS · `eidos-lint tags-input`
0 invalid (22 morfo-backed, 6 eidos-only) · `vitest run src/uix/eidos` 35/36
ficheros, el único rojo es el conocido `skin-media-player` en `lint.test.ts` ·
`rtl:check` 0 · `docs:check` 0 · `npm run check` COMPLETED, cero errores en los
ficheros tocados (los 72 globales son de otras sesiones) · prettier limpio
sobre el contenido normalizado a LF.

README con su sección «Talla y tema», pestaña `Tokens` en la demo verificada en
navegador (91 claves, cero errores de página) y veredicto §5 escrito en la
ficha.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent bcb958e5dc
commit 92180c390c

@ -5,18 +5,16 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-23 · **Alcance**: **88%** — 37 de 42 knobs por token público
- **Knobs de apariencia**: 46 — público 37 · privado 4 · global 1 · literal 0 · sistema 4 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 90 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `control-min-height-xs`, `control-min-height-sm`, `control-min-height-md`, `control-min-height-lg`, `control-min-height-xl`, `control-padding-inline-xs`, `control-padding-inline-sm`, `control-padding-inline-md`, `control-padding-inline-lg`, `control-padding-inline-xl`, `control-padding-block-xs`, `control-padding-block-sm`, `control-padding-block-md`, `control-padding-block-lg`, `control-padding-block-xl`, `item-height-xs`, `item-height-sm`, `item-height-md`, `item-height-lg`, `item-height-xl`, `item-padding-inline-xs`, `item-padding-inline-sm`, `item-padding-inline-md`, `item-padding-inline-lg`, `item-padding-inline-xl`, `font-family`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `line-height`, `fg`, `item-gap`, `control-radius`, `control-border-width`, `control-border`, `hover-control-border`, `control-bg`, `control-shadow`, `input-min-inline-size`, `placeholder-fg`, `item-radius`, `item-border-width`, `item-border`, `item-bg`, `item-fg`, `action-gap`, `action-padding-inline`, `action-radius`, `action-border-width`, `action-border`, `hover-action-border`, `action-bg`, `action-fg`, `hover-action-fg`, `delete-size`, `delete-radius`, `transition-duration`, `transition-ease`, `disabled-opacity`, `primary-text`, `secondary-text`, `neutral-text`, `affirm-text`, `fulfill-track`, `fulfill-border`, `fulfill-text`, `risk-text`, `threat-text`, `loss-track`, `loss-border`, `loss-text`, `primary-track`, `primary-border`, `secondary-track`, `secondary-border`, `neutral-track`, `neutral-border`, `affirm-track`, `affirm-border`, `risk-track`, `risk-border`, `threat-track`, `threat-border` · 3 privada(s) forward — `_palette-track`, `_palette-border`, `_palette-text`
- **Medido**: 2026-08-24 · **Alcance**: **90%** — 38 de 42 knobs por token público
- **Knobs de apariencia**: 46 — público 38 · privado 4 · global 0 · literal 0 · sistema 4 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 91 pública(s) — `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `control-min-height-xs`, `control-min-height-sm`, `control-min-height-md`, `control-min-height-lg`, `control-min-height-xl`, `control-padding-inline-xs`, `control-padding-inline-sm`, `control-padding-inline-md`, `control-padding-inline-lg`, `control-padding-inline-xl`, `control-padding-block-xs`, `control-padding-block-sm`, `control-padding-block-md`, `control-padding-block-lg`, `control-padding-block-xl`, `item-height-xs`, `item-height-sm`, `item-height-md`, `item-height-lg`, `item-height-xl`, `item-padding-inline-xs`, `item-padding-inline-sm`, `item-padding-inline-md`, `item-padding-inline-lg`, `item-padding-inline-xl`, `font-family`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `line-height`, `fg`, `item-gap`, `control-radius`, `control-border-width`, `control-border`, `hover-control-border`, `invalid-control-border`, `control-bg`, `control-shadow`, `input-min-inline-size`, `placeholder-fg`, `item-radius`, `item-border-width`, `item-border`, `item-bg`, `item-fg`, `action-gap`, `action-padding-inline`, `action-radius`, `action-border-width`, `action-border`, `hover-action-border`, `action-bg`, `action-fg`, `hover-action-fg`, `delete-size`, `delete-radius`, `transition-duration`, `transition-ease`, `disabled-opacity`, `primary-text`, `secondary-text`, `neutral-text`, `affirm-text`, `fulfill-track`, `fulfill-border`, `fulfill-text`, `risk-text`, `threat-text`, `loss-track`, `loss-border`, `loss-text`, `primary-track`, `primary-border`, `secondary-track`, `secondary-border`, `neutral-track`, `neutral-border`, `affirm-track`, `affirm-border`, `risk-track`, `risk-border`, `threat-track`, `threat-border` · 3 privada(s) forward — `_palette-track`, `_palette-border`, `_palette-text`
- **Eje `size`**: sí · **ficheros**: `tags-input.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (1)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `tags-input.css:105` | `[data-tags-input-control][data-invalid]` | `border-color` | `var(--color-risk-border)` |
_Ninguno._
### 1.2 A través de un privado (4)
@ -68,7 +66,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl
- **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (0)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
@ -78,7 +76,6 @@ no distingue lo que debería — se marca `⚠`.
| token (`--tags-input-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `invalid-control-border` | `root` | `var(--color-risk-border)` | 1 |
### 4.2 Sin nombre mecánico (4)
@ -96,6 +93,84 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**Medido y CERRADO el 2026-08-24 en su techo: 88 % → 90 %** (90 → 91 claves de
contrato, centinela **63/91** con 28 adjudicadas).
**Los cinco knobs fuera de alcance se clasificaron UNO A UNO antes de tocar una
línea**, que es la mitad del trabajo:
1. **`[data-tags-input-control][data-invalid] { border-color }`** leía
`var(--color-risk-border)` A PELO — el único `global`. Es la COSTURA del
PLAN §2-A y entra como **`--tags-input-invalid-control-border`**, con el
nombre y el default de `field.invalid-control-border`, que tinta la misma
parte del mismo chasis. **No** se reusó el `risk-border` de la paleta: la
cascada `[data-color]` lo re-enruta por instancia y se llevaría con él el
estado inválido, que no es una elección de tono.
2–5. **Los cuatro privados son el MISMO puente THM-2**: el borde del control
enfocado (`--_tags-input-palette-border`) y los tres slots de la etiqueta
activa (`-border`, `-track`, `-text`). Ni conmutador ni escala por talla:
los alimenta el forward `_palette-*` de `base.ts` por `[data-color]`.
**No se acuñan** (CONTINUE §3.pre): un público encima dejaría que un tema
los fijara y matara en silencio el `color=` de cada instancia.
**Así que 90 % ES el techo, y es doctrina bien aplicada, no deuda.** Los siete
privados dimensionales (`--_tags-input-{gap,control-min-height,…}`) ya derivan
de públicos y el censo los cuenta alcanzables; sus bloques `[data-size]` del
CSS **se quedan** — aquí NO los emite el TSC (las claves de talla son planas,
sin `scope: 'size:{k}'`), así que retirarlos rompería el eje. Migrar esa escala
al TSC es un cambio de mecanismo, no de alcance: queda fuera de este commit.
**Diff de computed = 0** en las dos superficies: 2.496 valores × 8 estados en
el escenario por defecto (válido) y 459 valores × 5 tallas + 3 variantes +
hover en el estado **inválido**, medido con una sonda de un solo uso porque la
demo arranca válida. Captura 2× antes/después **byte a byte idéntica**. No se
tocó ninguna regla `:hover`, así que el paso 12 no aplica.
**Centinela: 63/91, y las 28 adjudicaciones son cuatro razones, todas
medidas.**
- **Las 24 claves de tono — la cascada de paleta** (excepción por PATRÓN). El
wrapper resuelve `color` y estampa `data-color` en TODA instancia, incluida
la del default, así que gana el bloque genérico
`[data-tags-input][data-color], [data-tags-input][data-color-custom]`, que se
emite el ÚLTIMO a la misma (0,2,0) que los ocho por tono. Medido sobre la
etiqueta real forzada a `data-state=active`: `--tags-input-primary-border` no
mueve nada (el borde sigue en `oklch(0.8043 0.0918 309.69)`) mientras
`--palette-border` en el mismo nodo lo repinta (`rgb(4, 5, 6)`); quita
`data-color` y `--tags-input-primary-border` vuelve a pintar (`rgb(1, 2, 3)`).
- **`transition-duration` / `transition-ease`** SON la transición que el guard
congela. Medidos sin congelar: `0.12s → 4.321s` y
`cubic-bezier(0.4, 0, 0.2, 1) → steps(3)`.
- **`placeholder-fg`**: soma sólo pone el atributo `placeholder` mientras el
input está VACÍO (`isEmpty ? opts.placeholder : undefined`) y el escenario
arranca con dos etiquetas — y `::placeholder` sobre un input sin placeholder
devuelve el estilo del ELEMENTO (la trampa ya medida en `field`). Vaciar el
escenario no es opción para esa corrida: borra los nodos de etiqueta, donde
pinta un tercio del contrato. Medido de las dos formas: pulsando el propio
`Clear` de la demo el input queda con `placeholder="Add tag"`, y forzando el
atributo sobre el input real el token alcanza (`rgb(1, 2, 3)`; su default es
`oklch(0.61 0 0)` = `--color-content-muted`).
- **`item-fg` — y ésta NO es del instrumento, es del componente.** El morfo
sella **`aria-selected='true'` como LITERAL en CADA etiqueta**
(`morfo/components/tags-input.ts`), así que la regla de `archetypes.css`
`[data-archetype='item'][aria-selected='true']:not([data-state='checked']):not([data-state='on'])`
**(0,4,0)** gana siempre a `[data-tags-input-item]` **(0,1,0)**: la tinta de
la etiqueta es la del arquetipo, y su velo de estado se pinta en TODAS las
etiquetas en reposo. Medido: con el atributo tal como se envía el token no
mueve nada (la tinta sigue en `oklch(0.2435 0 0)`, la del arquetipo); con
`aria-selected=false` sobre la etiqueta real, la misma escritura alcanza
(`rgb(1, 2, 3)`). Es la clase de `table.selected-row-fg` y la tinta de
`listbox` (next-features §12). Por el mismo camino, los tres slots `*-text`
del tono llevan una SEGUNDA causa encima de la cascada de paleta. **Toca
morfo ⇒ se mide, se registra y se sigue** (§7.8): queda anotado en
next-features §12, no se arregla desde este eje.
**Lo que el instrumento aprendió**: el guard ABRE haciendo clic en el control,
lo que estampa `data-focus`, y la regla de foco re-tinta el borde a la misma
(0,2,0) pero más tarde — así que `control-border`, un token perfectamente vivo,
leía muerto. Es la clase de `command.input-border`, otra vez. Barrer la
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**.
<!-- veredicto:end -->

@ -483,6 +483,30 @@ el mismo nodo entra en una relación que nunca se escribió.
interlineado y hay que barrer los que lo intentan. Mueve píxel en los veinte
en cuanto se toque.
Lo que `tags-input` añadió (2026-08-24):
- **⚠ El morfo sella `aria-selected='true'` como LITERAL en CADA etiqueta**
(`morfo/components/tags-input.ts`, `v.literal('true')` sin condición), y eso
arrastra el cromo de SELECCIÓN del arquetipo a todas las etiquetas en
reposo. Con el atributo puesto, la regla de `archetypes.css`
`[data-archetype='item'][aria-selected='true']:not([data-state='checked']):not([data-state='on'])`
pesa **(0,4,0)** contra los **(0,1,0)** de `[data-tags-input-item]` y los
**(0,2,0)** de `[data-tags-input-item][data-state='active']`, así que gana
dos veces: fija `color: var(--color-content-primary)` —el token
`--tags-input-item-fg` **no ha pintado nunca**, y el
`--_tags-input-palette-text` del estado activo tampoco— y pinta el velo
`background-image` en todas. Medido 2026-08-24 sobre la etiqueta real con las
transiciones congeladas: escribir el token no mueve nada
(`oklch(0.2435 0 0)`, la tinta del arquetipo); poner `aria-selected=false`
sobre el mismo nodo y la misma escritura alcanza (`rgb(1, 2, 3)`). Es la
tercera ocurrencia de §12.5 (tras `table.selected-row-fg` y la tinta de
`listbox`) y la segunda del par arquetipo-`item` con `aria-selected`, tras
`grid-list`. Tiene además una cara de **a11y**: un lector de pantalla
anuncia las siete etiquetas como seleccionadas. Arreglarlo es morfo Y mueve
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.
### Qué habría que decidir
En una frase: **quién manda cuando el arquetipo y la receta pintan el mismo

@ -31,6 +31,19 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
// cancels itself (`:hover` true at t+50ms, false by t+300ms).
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
// pointer; the twenty-four tone keys are the palette cascade and ride the
// PATTERN entry below.
'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",
'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':
'the guard freezes transitions and this token IS the transition; measured unfrozen -> reaches (0.12s -> 4.321s on the three transitioned properties of the control)',
'transition-ease':
'same freeze; measured unfrozen -> reaches (cubic-bezier(0.4, 0, 0.2, 1) -> steps(3))'
},
avatar: {
// 2026-08-23. Five steps of scales the measured stage does not wear, all
// forced on the REAL nodes with transitions frozen. Two of them sit on the
@ -1391,6 +1404,13 @@ export const SENTINEL_PATTERN_EXCEPTIONS: Array<{
PALETTE_SUPERSEDED +
' — the eight tones x five slots, 40 of its 98 keys. Measured 2026-08-23 on the real stage card with transitions frozen: with data-color=primary, --card-primary-track moves NOTHING (the surface stays oklch(0.9932 0.0034 325.6)) while --palette-track on the same node repaints it (rgb(1, 2, 3)); strip data-color and --card-neutral-track paints again (oklch(0.9911 0 0) -> rgb(1, 2, 3)). The wrapper stamps data-color on every instance, so the named fallback never applies'
},
{
component: 'tags-input',
pattern: /^(primary|secondary|neutral|affirm|fulfill|risk|threat|loss)-(track|border|text)$/,
reason:
PALETTE_SUPERSEDED +
' — the eight tones x three slots, 24 of its 91 keys. Measured 2026-08-24 on the real tag forced into data-state=active with transitions frozen: with data-color=primary (the wrapper resolves `color` and stamps it on EVERY instance, default included) --tags-input-primary-border moves NOTHING (the tag border stays oklch(0.8043 0.0918 309.69)) while --palette-border on the same node repaints it (rgb(4, 5, 6)); strip data-color and --tags-input-primary-border paints again (rgb(1, 2, 3)). The generic `[data-tags-input][data-color], [data-tags-input][data-color-custom]` block is emitted LAST at the same (0,2,0) as the eight per-tone blocks. The three `*-text` slots carry a SECOND cause on top: even unsuperseded they cannot tint the item, because the archetype ink beats them (see the item-fg entry above)'
},
{
component: 'select',
pattern: /^(primary|secondary|neutral|affirm|fulfill|risk|threat|loss)-(track|border|text)$/,

@ -248,6 +248,19 @@ const COMPONENT_OVERRIDES: Record<
// (an `<a>`) navigates instead of revealing the panel, so the portaled
// content never enters the document and 32 of its 35 tokens read dead.
'link-preview': { openBy: 'hover' },
// Three states the stage never wears at once, and the guard's own opening
// creates the third: it CLICKS the control, which stamps `data-focus`, and
// the focus rule re-tints the border at the same (0,2,0) but later — so the
// resting `control-border` read dead on a token that is perfectly alive
// (the `command.input-border` class, measured again here 2026-08-24).
// Sweeping the absence (`null`) of each is what measures the resting form.
'tags-input': {
sweepAttr: [
{ attr: 'data-focus', values: [null, ''] },
{ attr: 'data-invalid', values: [null, ''] },
{ attr: 'data-disabled', values: [null, ''] }
]
},
// Two axes, and the guard knew NEITHER of them (3/12 without this entry).
// (1) The FAB's size axis is `data-fab-size`, not `data-size` — it is an
// eidos-only visual attr with its own scale off the control ladder, so the

@ -4,6 +4,49 @@ Eidos `TagsInput` viste el primitive headless de Soma para crear y editar una li
de etiquetas desde un input. La rama anterior tenia el equivalente en
`terra/tags-input`, no en `air/components`.
## Talla y tema
**91 claves públicas** en `lib/recipes/base.ts` (más los tres forwards privados
`_palette-*`, que son el puente THM-2 y NO se acuñan). La pestaña **Tokens** de
su demo las resuelve en vivo.
| eje | claves | resuelto por |
| --- | --- | --- |
| ritmo por talla | `gap-{xs…xl}` · `control-min-height-{xs…xl}` · `control-padding-inline-{xs…xl}` · `control-padding-block-{xs…xl}` · `item-height-{xs…xl}` · `item-padding-inline-{xs…xl}` · `font-size-{xs…xl}` | `[data-size]` → privado `--_tags-input-*` |
| tipografía | `font-family` · `line-height` · `fg` | — |
| control | `control-radius` · `control-border-width` · `control-border` · `hover-control-border` · **`invalid-control-border`** · `control-bg` · `control-shadow` | estado en el propio nodo |
| input | `input-min-inline-size` · `placeholder-fg` | — |
| etiqueta | `item-gap` · `item-radius` · `item-border-width` · `item-border` · `item-bg` · `item-fg` | — |
| acciones | `action-gap` · `action-padding-inline` · `action-radius` · `action-border-width` · `action-border` · `hover-action-border` · `action-bg` · `action-fg` · `hover-action-fg` · `delete-size` · `delete-radius` | — |
| movimiento y estado | `transition-duration` · `transition-ease` · `disabled-opacity` | — |
| tono | `{primary…loss}-{track,border,text}` (8 × 3) | `[data-color]` vía el puente THM-2 |
Lo que entró el **2026-08-24** (88 % → **90 %, y ése es su techo**): UNA clave,
`invalid-control-border`, que la receta leía como `var(--color-risk-border)` a
pelo. Es la COSTURA del PLAN §2-A —el valor sigue siendo del sistema, el knob
pasa a ser del componente— y toma el nombre y el default de
`field.invalid-control-border`, que tinta la misma parte del mismo chasis. NO
se reusó el `risk-border` de la paleta: ese lo re-enruta la cascada
`[data-color]` por instancia y se llevaría el estado inválido con él.
**El techo es 90 %, y no es deuda.** Los cuatro knobs que quedan fuera de
alcance leen `--_tags-input-palette-{border,track,text}`, el puente THM-2 que
la capa compartida alimenta por instancia desde `[data-color]`: el borde del
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.
## Baseline
| Fuente | Superficie | Estado Eidos |

@ -102,7 +102,7 @@
}
[data-tags-input-control][data-invalid] {
border-color: var(--color-risk-border);
border-color: var(--tags-input-invalid-control-border);
}
[data-tags-input-input] {

@ -3220,6 +3220,7 @@
--tags-input-control-border-width: var(--border-width);
--tags-input-control-border: var(--color-border-default);
--tags-input-hover-control-border: var(--color-border-strong);
--tags-input-invalid-control-border: var(--color-risk-border);
--tags-input-control-bg: var(--color-surface-default);
--tags-input-control-shadow: var(--shadow-subtle);
--tags-input-input-min-inline-size: 8ch;

@ -5068,6 +5068,13 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'control-border-width': 'var(--border-width)',
'control-border': 'var(--color-border-default)',
'hover-control-border': 'var(--color-border-strong)',
// The invalid tint the control's own rule reads. It was `var(--color-risk-border)`
// straight from the CSS — the seam of PLAN §2-A: the value stays the system's,
// the knob becomes the component's. Same NAME and same default as
// `field.invalid-control-border`, which tints the same part of the same
// chassis; NOT the palette's `risk-border`, which the `[data-color]` cascade
// re-routes per instance and would take the invalid state with it.
'invalid-control-border': 'var(--color-risk-border)',
'control-bg': 'var(--color-surface-default)',
'control-shadow': 'var(--shadow-subtle)',
'input-min-inline-size': '8ch',

@ -9,6 +9,7 @@
import { compileMorfo } from '$uix/morfo';
import { tagsInputMorfo } from '@/uix/morfo/components/tags-input';
import type { Intent } from '$uix/intent';
import TokensPanel from '../../lib/TokensPanel.svelte';
import { getActiveUix } from '$active-uix';
import { page } from '$app/state';
import { colorValue, PALETTE_SCALES } from '../../lib/harness.svelte';
@ -27,7 +28,7 @@
...PALETTE_SCALES
] as const;
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y';
type BlurBehavior = 'add' | 'clear';
const uix = getActiveUix();
@ -258,6 +259,9 @@
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>91</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -577,6 +581,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="tags-input" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.