From 1f8ce293bb3234790b1986f5d9f8f950a21dc5c4 Mon Sep 17 00:00:00 2001 From: dev Date: Tue, 25 Aug 2026 01:03:53 +0200 Subject: [PATCH] fix(theming)!: avatar - el contrato dice la verdad: 4 claves, cero desviadas Las ultimas CUATRO claves fuera de gramatica de las 4.558 publicas del catalogo, resueltas cada una por lo que ES (no eran cuatro iguales): - `badge-color-custom-contrast` -> `badge-fg-custom-contrast`: la unica TINTA real y alcanzable. Nombre derivado DE la gramatica D-TH.6 (la ranura se sustituye EN SU SITIO; `custom` no es interactivo ni rol, va detras) con el precedente firmado scrim-fg-over-dark - y es la salida literal del clasificador que consume R-5.3. Renombre a mano (el codemod ABORTA sobre la IIFE de avatar - deuda registrada en 13): value-preserving, verificado con sonda dirigida (la clave vieja movia antes y no despues; la nueva al reves). - `ring-color-custom` y `badge-color-custom` RETIRADAS del contrato publico: canales de valor INALCANZABLES (puerta data-*-custom y escritura inline nacen de la misma expresion - mentiras bajo R-5.4). Mecanismo: canal privado de envoltorio `--_avatar-*-color-custom` + respaldo currentColor (el patron de las 5 privadas-de-envoltorio del parque, 5/5 sin declaracion en receta), NO borrado (el CSS las lee) NI privada plana (cero precedente). Ley 8.bis re-aplicada: cero lectores fuera de avatar, re-barrido. - `group-carve-color` EXENTA en NAME_EXEMPT con razon medida: alimenta box-shadow (geometria del carve, no tinta; alcanza en 59 instancias) - `fg` seria gramatica correcta con semantica peor. Medido: sonda 7.4 0/576 - pixel visible IDENTICO (anillo ambar, insignia violeta, tinta white) e instancia imposible identica (currentColor preserva el default retirado) - Tokens 88 -> 86 EN VIVO con el nombre nuevo (captura revisada por el supervisor) - censo --names: DESVIADAS 0 EN 0 COMPONENTES (4.556 claves) - centinela exit 0 - lint 0 - docs-check 0/0 - vitest 442+1 ajeno - check 0 atribuibles. Hallazgo de instrumento NUEVO, adjudicado y registrado en 13: el chooser del centinela elige el valor por NOMBRE y no conoce `fg` MEDIAL (su prueba de tinta es `fg$`): las 5 claves que la propia D-TH.6 fabrica (badge-fg-custom- contrast, bubble-fg-in/-out, scrim-fg-over-dark/-light) reciben 1234px, invalido para color - y el veredicto depende de si el respaldo coincide por azar (bubble-fg-* leen vivas POR ACCIDENTE). Arreglarlo obliga a re-verificar el ledger - firma de instrumento aparte. Co-Authored-By: Claude Fable 5 --- docs/audit/theming/avatar.md | 46 ++++++++++-- docs/next-features.md | 52 +++++++++++++ docs/process/CONTINUE-theming.md | 32 +++++++- docs/process/PLAN-theming.md | 74 +++++++++++++++++++ scripts/theming-census.ts | 10 ++- scripts/theming-sentinel-exceptions.ts | 6 +- .../components/avatar/avatar-badge.svelte | 5 +- src/uix/eidos/components/avatar/avatar.css | 25 ++++--- src/uix/eidos/components/avatar/avatar.svelte | 5 +- src/uix/eidos/generated/base.css | 4 +- src/uix/eidos/lib/recipes/base.ts | 21 ++++-- 11 files changed, 247 insertions(+), 33 deletions(-) diff --git a/docs/audit/theming/avatar.md b/docs/audit/theming/avatar.md index 333c8d4fc..e134f4ef2 100644 --- a/docs/audit/theming/avatar.md +++ b/docs/audit/theming/avatar.md @@ -141,11 +141,18 @@ significaba nada. `:nth-child()` — lo dice el propio comentario del CSS. Retirada de los dos sitios: el `+3` del grupo sigue exactamente donde estaba. - **el respaldo `, white`** de `--_avatar-badge-fg`: el contrato ya declara - `--avatar-badge-color-custom-contrast: white`, así que el respaldo era - inalcanzable y sólo podía envejecer contra su token (la clase del `, 1.4` - contra `--font-line-height-sm`). Comprobado en la rama custom: la tinta sigue + `--avatar-badge-color-custom-contrast: white` (desde 2026-08-25, + `--avatar-badge-fg-custom-contrast`), así que el respaldo era inalcanzable y + sólo podía envejecer contra su token (la clase del `, 1.4` contra + `--font-line-height-sm`). Comprobado en la rama custom: la tinta sigue computando `rgb(255, 255, 255)`. +> **Nota 2026-08-25**: las tablas generadas de §1–§4 de esta ficha siguen +> nombrando `--avatar-badge-color-custom` y `--avatar-ring-color-custom` como +> públicas. Es fotografía vieja: la firma A-c las retiró del contrato ese día +> (ver §5). Se corrigen solas en la próxima regeneración del censo; no se +> regeneró aquí para no mezclar en esta firma un diff que no es suyo. + **Seis literales firmados** (salen del ratio, clase `exception`): los cuatro `100 %` de `Image` y `Fallback` son IDENTIDAD —la parte ES la superficie del avatar, no una talla propia— y los dos `line-height: 1` mantienen el glifo @@ -165,14 +172,37 @@ paso `xxl`, que **el barrido de tallas del guard no alcanza** (para en `xl` — mismo límite que `metrics` ya registró); `radius-none` y `ring-width-sm` son «sólo el paso en vigor pinta»; `group-overlap-xxl` junta las dos cosas. +**Una SEXTA adjudicación desde 2026-08-25**, y de otra clase: no es el escenario +sino el INSTRUMENTO. `badge-fg-custom-contrast` alcanza perfectamente (desde +`:root`, `white` → `rgb(4,5,6)`), pero `sentinelFor()` elige el valor de sonda +por el NOMBRE de la clave y su prueba de tinta es `fg$` — la cadena lleva `-bg-` +pero no `-fg-`, así que un `fg` MEDIAL cae al default `1234px`, inválido para +`color:`. Reproducido bajo las condiciones del propio guard: con `1234px` no +mueve en ningún combo, con `rgb(1, 2, 3)` mueve en `solid`. Leía VIVA hasta el +renombre, cuando el nombre dejó de casar `color`. Son cinco claves así en el +catálogo y las cinco las fabrica D-TH.6 (next-features §13). + **Defectos reales que NO se arreglan aquí:** -1. **R-5.3, cuatro claves fuera de la gramática, PREEXISTENTES**: +1. ~~**R-5.3, cuatro claves fuera de la gramática, PREEXISTENTES**: `ring-color-custom`, `badge-color-custom`, `badge-color-custom-contrast` (y - la cuarta que el audit trunca). La tinta es `fg`, no `color`. Son los - canales de valor que el envoltorio escribe INLINE, así que renombrarlos - cambia el contrato público de tres escotillas — decisión, no ejecución: - queda listado, el audit sigue en NEEDS-WORK por esto y sólo por esto. + la cuarta que el audit trunca). La tinta es `fg`, no `color`.~~ — **FIRMADA + Y EJECUTADA 2026-08-25** (opción A-c; la cuarta que el audit truncaba es + `group-carve-color`). Eran las **únicas 4 desviaciones de 4.558 claves + públicas** del catálogo, y no eran la misma cosa: **una se renombró** + (`badge-color-custom-contrast` → `badge-fg-custom-contrast`, la única que es + tinta de verdad y la única de las tres `*-custom*` que alcanza desde `:root` + — medido `white` → `rgb(4,5,6)`); **dos se retiraron del contrato** + (`ring-color-custom` y `badge-color-custom` son canal de valor: la puerta y + el inline salen de la misma expresión, así que ningún tema puede ganarles; + pasan a privadas `--_avatar-{ring,badge}-color-custom` leídas con + `var(…, currentColor)`, la forma que usan las 5 privadas de envoltorio del + parque y la adjudicación que `tabs` hizo con su indicador); y **una quedó + EXENTA** (`group-carve-color` alimenta `box-shadow` — la geometría del + carve, no tinta; `group-carve-fg` sería gramática correcta y semántica + peor). Contrato **88 → 86** («Tokens 86» en vivo), censo `--names` **4 → 0 + en todo el catálogo**, sonda **0 diffs sobre 576 valores**, **0 píxeles**. + Detalle en PLAN-theming §8. 2. El barrido de tallas del centinela **no llega a `xxl`**; añadirlo dejaría STALE las seis excepciones que `metrics` escribió por lo mismo, así que se respeta el precedente y se adjudica. diff --git a/docs/next-features.md b/docs/next-features.md index 1533d4fea..ed184ab06 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -1578,6 +1578,15 @@ Lo que `chart` destapó (2026-08-22): viajan JUNTAS en el envoltorio. Son canal de valor filtrado al contrato; su hermana `badge-color-custom-contrast` sí vive desde `:root`.)* + *(Actualización 2026-08-25 — **los dos de `avatar` ya NO están en el + contrato**, así que en el contrato quedan **DOS**, los dos `overlay-opacity`. + La firma A-c retiró `ring-color-custom` y `badge-color-custom` a privadas + `--_avatar-{ring,badge}-color-custom`: el canal sigue funcionando igual y el + falso positivo del guard deja de contar como público que miente, porque ya no + hay público. **No arregla el guard** —el mecanismo sigue intacto y `dialog` / + `drawer` lo siguen sufriendo—, sólo saca a `avatar` de la lista. Detalle en + PLAN-theming §8.)* + Los otros cuatro (`emoji-picker.columns`, `text-focus.border-color` / `-glow-color`, `text-gradient.duration`) no están en ningún contrato, así que sólo son canal. @@ -1971,5 +1980,48 @@ Lo que dejó ABIERTO la firma `aria-selected` de `tags-input` (2026-08-25): (`: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. +- **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 + el 2026-08-25). `scripts/__names-codemod.ts` localiza el bloque de un + componente con `blockRange()`, que busca una apertura `nombre: {` + (`/^\t'?([a-z0-9-]+)'?: \{/gm`); el de `avatar` es + `avatar: ((): RecipeTokenMap => {`, así que no casa y el `--dry` responde + `muro 1 · colisiones: avatar: SIN BLOQUE` → `ABORTA: un muro está en rojo`. + Es **exactamente la misma ceguera** que `f68bac4a6` corrigió en el censo y en + el centinela («los dos lectores leen ya la IIFE»), pero al codemod nadie se la + enseñó — y `avatar` era el ÚNICO componente que lo necesitaba, porque era el + único con desviaciones de nombre. Consecuencia real: la firma A-c del + 2026-08-25 se ejecutó **A MANO**, con verificación por grep. Coste, no + bloqueo, y hoy sin urgencia (el censo `--names` da 0 desviadas, así que no + queda cola para el codemod); pero si vuelve a haber cola, esto se arregla + ANTES — son unas líneas, con el mismo dedentado que censo y centinela ya + tienen. +- **El centinela elige el VALOR de sonda por el NOMBRE de la clave, y D-TH.6 + fabrica nombres que ese chequeo no reconoce** (hallazgo propio del ejecutor + E-avatar, 2026-08-25). `sentinelFor()` (`theming-sentinel.ts:692`) es una + cadena de regex sobre el nombre; su prueba de tinta es + `/color|bg$|fg$|…|-bg-|…/` — lleva `-bg-` para el `bg` MEDIAL pero **no lleva + `-fg-`**. Así que toda clave con `fg` medial cae al default `1234px`, que es + inválido para `color:` y **no mueve nada**: el guard la da por muerta. Son + **cinco** en el catálogo y las cinco nacen de la gramática firmada + (`{parte}-fg-{calificador}`): `avatar.badge-fg-custom-contrast`, + `chat-message.bubble-fg-in` / `-out`, `background.scrim-fg-over-dark` / + `-over-light`. Reproducido bajo las condiciones del propio guard (mismo + conjunto de 55 nodos, mismo barrido de `data-variant`, mismo forzado de + talla) sobre la insignia custom real: con `1234px` no mueve en **ningún** + combo; con `rgb(1, 2, 3)` mueve en `solid` (`rgb(255,255,255)` → + `rgb(1,2,3)`). Es la imagen especular del caso que el propio comentario del + chooser ya registra (`chart.slice-stroke-width`, una DIMENSIÓN que cazaba una + palabra de color), y es **peor que un falso negativo limpio**: el veredicto + depende de si el valor de respaldo coincide por casualidad con el original — + bajo el barrido de `avatar` coincide y lee muerta, en `chat-message` no + coincide y `bubble-fg-in`/`-out` leen VIVAS **por accidente**. Además deja + incompleta la razón escrita de `scrim-fg-over-*` en el ledger, que atribuye la + ceguera sólo a la puerta `on='dark'`. **Arreglo**: añadir `-fg-` a la cadena, + junto a `-bg-`. **No se hizo aquí** porque tocar el chooser obliga a + re-verificar el ledger entero (la misma condición que el cambio de la + congelación); mientras tanto, `avatar.badge-fg-custom-contrast` va adjudicada + en el ledger con la razón medida. + **Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando estorben. diff --git a/docs/process/CONTINUE-theming.md b/docs/process/CONTINUE-theming.md index 6264eb7c0..58b64a0d7 100644 --- a/docs/process/CONTINUE-theming.md +++ b/docs/process/CONTINUE-theming.md @@ -133,8 +133,36 @@ guarda `active-eidos-config.test.ts` · ledger `scripts/theming-sentinel-excepti `chat-composer` recupera 6 valores —el textarea salía a **0.4³ = 0.064** de alpha en el tema POR DEFECTO, 925 px— y su entrada del ledger se RETIRA. 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. + 4. ~~**Las cuatro claves de `avatar` fuera de gramática**~~ — **✅ FIRMADA Y + EJECUTADA 2026-08-25** (opción A-c): las cuatro **no eran la misma cosa**, + y la medida las separó en tres tratos. **UNA se renombra** — + `badge-color-custom-contrast` → **`badge-fg-custom-contrast`**, la única + tinta de verdad (alimenta `color:`) y alcanzable desde `:root` (medido: + `white` → `rgb(4,5,6)`); es la salida literal del clasificador firmado, que + deja el calificador contextual DETRÁS de la ranura como en el precedente + `scrim-color-on-dark` → `scrim-fg-over-dark`. **DOS se retiran del + contrato** — `ring-color-custom` y `badge-color-custom` son **canal de + valor**: la puerta `data-*-custom` y la escritura inline nacen de la MISMA + expresión del envoltorio, así que no existe ni puede existir instancia en + la que un tema les gane. Pasan a privadas `--_avatar-{ring,badge}-color-custom` + escritas por el envoltorio y leídas con `var(…, currentColor)` — la forma + que `background`, `carousel`, `checkbox-group`, `file-upload` y + `gradient-builder` ya usan (5 de 5 privadas de envoltorio NO se declaran en + la receta), y la adjudicación que `tabs` hizo con su indicador. **UNA queda + EXENTA** — `group-carve-color` alimenta `box-shadow`: es la GEOMETRÍA del + carve, no tinta, y `group-carve-fg` sería gramática correcta con semántica + peor. Contrato público **88 → 86** (medido en vivo: la pestaña se llama ya + «Tokens 86»), censo `--names` **4 desviadas → 0 en TODO el catálogo** + (4556 claves, exentas 13 → 14). Sonda §7.4: **0 diffs sobre 576 valores**, + y el píxel de los nodos con puerta idéntico (anillo ámbar + `rgb(245,158,11)`, insignia violeta `rgb(124,58,237)`, tinta `white`), + incluida la «instancia imposible». **0 píxeles**, como firmaba la opción. + Abrió dos deudas de INSTRUMENTO, las dos en next-features §13: el codemod + de nombres **aborta sobre `avatar`** (`blockRange()` no lee la IIFE, la + misma ceguera que `f68bac4a6` arregló en censo y centinela), y el centinela + **elige el valor de sonda por el NOMBRE** (`fg$`, sin `-fg-`), así que las + cinco claves del catálogo con `fg` MEDIAL reciben `1234px`. Detalle en §8 + del PLAN. 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` diff --git a/docs/process/PLAN-theming.md b/docs/process/PLAN-theming.md index c24160605..84746ea93 100644 --- a/docs/process/PLAN-theming.md +++ b/docs/process/PLAN-theming.md @@ -521,6 +521,80 @@ excepción abre la mano para MEDIR, no para contabilizar. ## 8. Registro +- 2026-08-25 — **FIRMA: las cuatro claves de `avatar` fuera de gramática dicen + ya la verdad** (opción A-c). Las cuatro eran las **únicas 4 desviaciones de + 4.558 claves públicas** del catálogo, y la medida demostró que **no son la + misma cosa**: son tres tratos distintos. + **(1) Una se RENOMBRA.** `badge-color-custom-contrast` → + **`badge-fg-custom-contrast`**. Es la única que es tinta de verdad + (`avatar.css` la lleva a `--_avatar-badge-fg` → `color:`) y la única de las + tres `*-custom*` que **alcanza desde `:root`** (medido en la insignia real: + `white` → `rgb(4,5,6)`, restaurado). La derivación no es de gusto: es la + salida literal del clasificador firmado (`classifyName`), que consume R-5.3 y + que ejecutó el codemod de 196 claves. `custom` **no** promociona al frente + porque no está en `UNIVERSAL_MODIFIERS` (D-TH.6: «delante lo interactivo, + detrás lo dimensional y contextual») y **no** es rol/paleta —no está en + `ROLES`, y el morfo lo prueba: `data-color` es un eje enumerado aparte y la + vía custom es una PUERTA distinta, ausente por diseño cuando se pasa un rol—. + El precedente exacto es la propia acta: `scrim-color-on-dark` → + `scrim-fg-over-dark` cambió la ranura **en su sitio** y dejó el calificador + detrás; `bubble-fg-in`/`-out` repiten la forma. El candidato de sonido natural + `badge-custom-fg` se descarta por eso. + **(2) Dos se RETIRAN del contrato.** `ring-color-custom` y + `badge-color-custom` son **canal de valor, no knobs**: la puerta + `data-*-custom` y la escritura inline del token salen de la MISMA expresión + del envoltorio (`avatar.svelte:93`+`:125`, `avatar-badge.svelte:36`+`:48`), + así que viajan juntas y **no existe ni puede existir** instancia en la que un + tema le gane al inline (re-medido: escribir cualquiera de las dos en `:root` + no mueve nada, ni antes ni después). El MECANISMO elegido es el de la casa, + no el de `group-max`: aquel se borró de los dos sitios porque **no lo leía + nadie**, mientras que aquí el CSS sí los lee, así que borrarlos rompería la + función. Se retiran de la RECETA y el canal pasa a privado — + `--_avatar-ring-color-custom` / `--_avatar-badge-color-custom`, escrito por el + envoltorio y leído con `var(…, currentColor)`. Es la forma que ya usan **5 de + 5** privadas de envoltorio del parque (`background-gradient-image`, + `carousel-vertical-block-size`, `checkbox-group-columns`, + `file-upload-progress-value`, `gradient-builder-stop-fill`): **ninguna** se + declara en la receta y todas llevan el respaldo en el `var()`. Y es la + adjudicación que `tabs` firmó para su indicador («un público encima sería un + knob que un tema puede fijar»). El respaldo `currentColor` **no** reintroduce + el antipatrón del `, white` que esta misma ficha retiró: aquel duplicaba el + default de un token del contrato y podía envejecer contra él; éste es la única + declaración que queda del default. Verificado sobre la **instancia imposible** + (quitar el inline dejando la puerta): anillo `oklch(0.5032 0 0)` e insignia + `rgb(255,255,255)` **idénticos** antes y después. + **(3) Una queda EXENTA con razón escrita.** `group-carve-color` alimenta + `box-shadow` (`avatar.css`): es el **filo que recorta la silueta** contra el + avatar anterior, geometría y no tinta, y alcanza de verdad — medido en las + **59** instancias de `/uix/components/avatar-group` (`oklch(0.9911 0 0)` → + `rgb(1,2,3)`, restaurado). `group-carve-fg` sería gramática correcta y + semántica peor, así que entra en `NAME_EXEMPT` como cuarta entrada, la primera + que no es un falso amigo sino una medida. + **Cifras.** Contrato público **88 → 86** filas, comprobado EN VIVO: la pestaña + del demo se llama ya «Tokens 86», trae `--avatar-badge-fg-custom-contrast` + con `white` → `white` y **ninguna** privada se ha colado. Censo `--names`: + **4 desviadas en 1 componente → 0 en 0**, es decir **CERO de catálogo** sobre + 4.556 claves (conformes 4.084 → 4.085, exentas 13 → 14). Diff de + `generated/base.css`: **un solo hunk**, 3 bajas y 1 alta, exactamente el + renombre y las dos retiradas. Sonda §7.4 antes/después: **0 diffs sobre 576 + valores computados**; el píxel de los nodos con puerta, idéntico (anillo + `rgb(245,158,11) 0 0 0 2px`, insignia `rgb(124,58,237)`, tinta `white`). + **0 píxeles**, que es lo que la opción firmaba. + **Dos deudas de INSTRUMENTO que esta firma destapa** (las dos a next-features + §13, ninguna arreglada aquí porque tocan herramienta compartida): el **codemod + de nombres aborta sobre `avatar`** —`blockRange()` busca `nombre: {` y el + bloque es una IIFE, la misma ceguera que `f68bac4a6` corrigió en censo y + centinela—, y el **centinela elige el valor de sonda por el NOMBRE de la + clave**: su prueba de tinta es `fg$` y la cadena lleva `-bg-` pero **no** + `-fg-`, así que toda clave con `fg` MEDIAL recibe `1234px`, inválido para + `color:`. Son **cinco** en el catálogo, y D-TH.6 fabrica esa forma: + `badge-fg-custom-contrast`, `bubble-fg-in`, `bubble-fg-out`, + `scrim-fg-over-dark`, `scrim-fg-over-light`. Reproducido bajo las condiciones + del propio guard (mismo conjunto de 55 nodos, mismo barrido de `data-variant`, + mismo forzado de talla): con `1234px` no mueve en ningún combo, con + `rgb(1,2,3)` mueve en `solid`. Por eso la clave renombrada entra en el ledger + con su razón medida — y por eso el guard queda en **exit 0**, 80/86 con 6 + adjudicadas. - 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') }` → diff --git a/scripts/theming-census.ts b/scripts/theming-census.ts index 9ed302765..0ab81e12b 100644 --- a/scripts/theming-census.ts +++ b/scripts/theming-census.ts @@ -1345,11 +1345,19 @@ const VARIANT_TAILS = /(^|-)(solid|surface|soft|outline|ghost|subtle|element)-(h * The three false-friend groups the author signed as KEEPING their `color` * segment (2026-08-20 acta): the system's focus-ring family, colour as a NOUN, * and `stop-color` as a PART name. + * + * The fourth (2026-08-25 acta) is of a different kind: not a false friend but a + * measured one. The classifier decides by the FORM of the name; `group-carve-color` + * is the one key whose DESTINATION contradicts it. */ const NAME_EXEMPT: Array<{ re: RegExp; why: string }> = [ { re: /focus-ring-color$/, why: 'espeja la familia del SISTEMA --focus-ring-*' }, { re: /^orb-color-/, why: 'color como sustantivo del orbe, no slot' }, - { re: /^stop-color(-|$)/, why: 'stop-color ES la parte (gradient-builder-stop-color.svelte)' } + { re: /^stop-color(-|$)/, why: 'stop-color ES la parte (gradient-builder-stop-color.svelte)' }, + { + re: /^group-carve-color$/, + why: 'alimenta box-shadow (la GEOMETRÍA del carve, no tinta): avatar.css `[data-carve-out] { box-shadow: 0 0 0 var(--avatar-group-carve-width) var(--avatar-group-carve-color) }`. Alcanza — medido en las 59 instancias de /uix/components/avatar-group (oklch(0.9911 0 0) → rgb(1,2,3), restaurado). `group-carve-fg` sería gramática correcta y semántica PEOR: nombraría tinta lo que es el filo que recorta la silueta contra el avatar anterior' + } ]; /** diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index 2d2013973..a73ac2f8b 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -94,7 +94,11 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'ring-width-sm': 'only the step in force paints (the demo boots ringWidth=md); forced data-ring-width=sm -> reaches (halo 1.5px -> 17px)', 'group-overlap-xxl': - 'same xxl step, on AvatarGroup; forced data-size=xxl on the real group -> reaches (margin-inline-start -33.6px -> 37px)' + 'same xxl step, on AvatarGroup; forced data-size=xxl on the real group -> reaches (margin-inline-start -33.6px -> 37px)', + // 2026-08-25, added with the D-TH.6 rename. This one is NOT about the + // stage: it is the guard picking the wrong probe VALUE. + 'badge-fg-custom-contrast': + 'INSTRUMENT, not the token. `sentinelFor` chooses the probe value by regex on the key NAME, and its ink test is `fg$` — `fg` as the FINAL segment — while D-TH.6 leaves the slot MEDIAL here; the chain carries `-bg-` but has no `-fg-`. So the key gets the `1234px` default, which is invalid for `color:` and moves nothing. Measured under the conditions the guard itself uses (same node set of 55, same data-variant sweep, same size forcing) on the real custom badge of /uix/components/avatar: with `1234px` the snapshot does not move on any combo; with `rgb(1, 2, 3)` it moves on `solid` (rgb(255,255,255) -> rgb(1,2,3)). From `:root` alone it also reaches (white -> rgb(4,5,6)). It read alive until 2026-08-25 under its old name `badge-color-custom-contrast`, which matched `color`. Same class as `bubble-fg-in` / `bubble-fg-out` and `scrim-fg-over-dark` / `scrim-fg-over-light` — the five keys of the catalogue whose `fg` is medial (next-features §13)' }, editable: { // 2026-08-23, revised 2026-08-24. The transition pair IS the transition the diff --git a/src/uix/eidos/components/avatar/avatar-badge.svelte b/src/uix/eidos/components/avatar/avatar-badge.svelte index 981574339..b22cb59fd 100644 --- a/src/uix/eidos/components/avatar/avatar-badge.svelte +++ b/src/uix/eidos/components/avatar/avatar-badge.svelte @@ -33,7 +33,10 @@ const customColor = $derived(isCanonicalColor ? undefined : color); const inlineStyle = $derived( - [customColor && `--avatar-badge-color-custom: ${customColor};`, styleProp] + // Private on purpose: this carries THIS badge's value, it is not a + // theming knob (the gate and this write are one expression, so no theme + // could ever reach a public key here). + [customColor && `--_avatar-badge-color-custom: ${customColor};`, styleProp] .filter(Boolean) .join(' ') || undefined ); diff --git a/src/uix/eidos/components/avatar/avatar.css b/src/uix/eidos/components/avatar/avatar.css index cc9e96c24..99f5c2a24 100644 --- a/src/uix/eidos/components/avatar/avatar.css +++ b/src/uix/eidos/components/avatar/avatar.css @@ -188,9 +188,12 @@ [data-avatar][data-ring-color='loss'] { --_avatar-ring-color: var(--avatar-loss-solid-bg); } -/* Custom ring color — set inline by the Svelte file. */ +/* Custom ring color — a VALUE CHANNEL the Svelte file writes inline, private + * on purpose: the gate and the inline write come from one expression, so a + * public key here could never be reached from a theme (retired 2026-08-25). + * The `currentColor` fallback is the default the contract used to declare. */ [data-avatar][data-ring-color-custom] { - --_avatar-ring-color: var(--avatar-ring-color-custom); + --_avatar-ring-color: var(--_avatar-ring-color-custom, currentColor); } /* ── Badge ──────────────────────────────────────────────────────────── */ @@ -280,26 +283,28 @@ /* Badge (variant × color) cascade lives in the recipe via TSC v2.2 — * see file header for details. */ -/* Custom badge color — value comes from inline style. */ +/* Custom badge color — a VALUE CHANNEL the Svelte file writes inline, private + * for the same reason as the ring's above. The `currentColor` fallback is the + * default the contract used to declare (retired 2026-08-25). */ [data-avatar-badge][data-color-custom] { - --_avatar-badge-bg: var(--avatar-badge-color-custom); + --_avatar-badge-bg: var(--_avatar-badge-color-custom, currentColor); /* No hand-written fallback: the contract already declares - * `--avatar-badge-color-custom-contrast: white`, so `, white` here was dead + * `--avatar-badge-fg-custom-contrast: white`, so `, white` here was dead * code that could only age against it. */ - --_avatar-badge-fg: var(--avatar-badge-color-custom-contrast); + --_avatar-badge-fg: var(--avatar-badge-fg-custom-contrast); } [data-avatar-badge][data-variant='soft'][data-color-custom] { --_avatar-badge-bg: color-mix( in srgb, - var(--avatar-badge-color-custom) 18%, + var(--_avatar-badge-color-custom, currentColor) 18%, transparent ); - --_avatar-badge-fg: var(--avatar-badge-color-custom); + --_avatar-badge-fg: var(--_avatar-badge-color-custom, currentColor); } [data-avatar-badge][data-variant='outline'][data-color-custom] { --_avatar-badge-bg: var(--color-surface-default); - --_avatar-badge-fg: var(--avatar-badge-color-custom); - --_avatar-badge-border: var(--avatar-badge-color-custom); + --_avatar-badge-fg: var(--_avatar-badge-color-custom, currentColor); + --_avatar-badge-border: var(--_avatar-badge-color-custom, currentColor); } /* ── AvatarGroup ────────────────────────────────────────────────────── */ diff --git a/src/uix/eidos/components/avatar/avatar.svelte b/src/uix/eidos/components/avatar/avatar.svelte index 23098fcfc..9a9f408fc 100644 --- a/src/uix/eidos/components/avatar/avatar.svelte +++ b/src/uix/eidos/components/avatar/avatar.svelte @@ -60,7 +60,8 @@ // CSS value → `data-color-custom` + the shared `--color-custom` seed, which // the recipe's palette forward derives every `--_avatar-palette-*` slot from // (the generic path every component uses). The RING is a SEPARATE colour - // channel below — it keeps its own `--avatar-ring-color-custom` seed. + // channel below — it keeps its own `--_avatar-ring-color-custom` seed, which + // is PRIVATE: it carries this instance's value, it is not a theming knob. const colorAttrs = $derived(resolveComponentColor(resolvedColor)); // Ring resolves the boolean shortcut → 'solid'. @@ -90,7 +91,7 @@ [ style, colorAttrs.customStyle, - customRingColor && `--avatar-ring-color-custom: ${customRingColor};` + customRingColor && `--_avatar-ring-color-custom: ${customRingColor};` ] .filter(Boolean) .join(' ') || undefined diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index e4aa39ba5..206b0ab0b 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -638,10 +638,8 @@ --avatar-ring-width-sm: 1.5px; --avatar-ring-width-md: 2px; --avatar-ring-width-lg: 3px; - --avatar-ring-color-custom: currentColor; --avatar-badge-border-width: var(--border-width-medium); - --avatar-badge-color-custom: currentColor; - --avatar-badge-color-custom-contrast: white; + --avatar-badge-fg-custom-contrast: white; --avatar-group-gap-xs: var(--space-1); --avatar-group-gap-sm: var(--space-2); --avatar-group-gap-md: var(--space-3); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index af03749fc..6276b1e4c 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -220,13 +220,24 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({ 'ring-width-lg': '3px', // ── Custom colour ───────────────────────────────────────────────── // The main fill routes through the shared `--color-custom` seed - // (`resolveComponentColor` + the palette forward), so only the ring - // keeps its own custom channel, set inline by the Svelte file. - 'ring-color-custom': 'currentColor', + // (`resolveComponentColor` + the palette forward). The ring and the + // badge keep a custom channel of their own, but it is a VALUE + // CHANNEL, not a theming surface: the wrapper stamps the + // `data-*-custom` gate and writes the token inline from the SAME + // expression, so the two travel together and no `:root` a theme + // writes can ever win against the inline (measured 2026-08-23 and + // again 2026-08-25). A public key nothing can reach is a knob that + // lies, so both moved to `--_avatar-{ring,badge}-color-custom`, + // declared nowhere and carried by the wrapper — the same shape + // `background`, `carousel`, `checkbox-group`, `file-upload` and + // `gradient-builder` already use, and the adjudication `tabs` made + // for its indicator. Retired 2026-08-25 (88 → 86 contract rows). // ── Badge ───────────────────────────────────────────────────────── 'badge-border-width': 'var(--border-width-medium)', - 'badge-color-custom': 'currentColor', - 'badge-color-custom-contrast': 'white', + // The ink that rides ON that custom fill, in the solid variant. This + // one IS reachable from `:root` (measured: white → rgb(4,5,6)), so it + // stays public — and speaks D-TH.6: the ink slot is `fg`. + 'badge-fg-custom-contrast': 'white', // ── AvatarGroup ────────────────────────────────────────────────── 'group-gap-xs': 'var(--space-1)', 'group-gap-sm': 'var(--space-2)',