diff --git a/docs/audit/theming/tags-input.md b/docs/audit/theming/tags-input.md index 456f5809c..50d0f907a 100644 --- a/docs/audit/theming/tags-input.md +++ b/docs/audit/theming/tags-input.md @@ -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 `⚠`. -_(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**. diff --git a/docs/next-features.md b/docs/next-features.md index 4a58de9d5..d14d58894 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -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 diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 87a6fb08c..3e93dba0a 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -31,6 +31,19 @@ export const SENTINEL_EXCEPTIONS: Record> = { // 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)$/, diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts index 4e9b34fe6..bbb33abe9 100644 --- a/scripts/theming-sentinel.ts +++ b/scripts/theming-sentinel.ts @@ -248,6 +248,19 @@ const COMPONENT_OVERRIDES: Record< // (an ``) 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 diff --git a/src/uix/eidos/components/tags-input/README.md b/src/uix/eidos/components/tags-input/README.md index 101a585ee..4d140b2ae 100644 --- a/src/uix/eidos/components/tags-input/README.md +++ b/src/uix/eidos/components/tags-input/README.md @@ -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 | diff --git a/src/uix/eidos/components/tags-input/tags-input.css b/src/uix/eidos/components/tags-input/tags-input.css index 8d5c633bc..b478bdfaa 100644 --- a/src/uix/eidos/components/tags-input/tags-input.css +++ b/src/uix/eidos/components/tags-input/tags-input.css @@ -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] { diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index c60ae9e71..f19f5bb6d 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -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; diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 44c6025b6..0eeeb007d 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -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', diff --git a/web/routes/uix/components/tags-input/+page.svelte b/web/routes/uix/components/tags-input/+page.svelte index 203b70588..42d8f5040 100644 --- a/web/routes/uix/components/tags-input/+page.svelte +++ b/web/routes/uix/components/tags-input/+page.svelte @@ -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 @@ {events.length} + @@ -577,6 +581,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'recipe'}

Eidos recipe