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 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 2ce43db563
commit 1f8ce293bb

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

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

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

@ -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') }` →

@ -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'
}
];
/**

@ -94,7 +94,11 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'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

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

@ -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 ────────────────────────────────────────────────────── */

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

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

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

Loading…
Cancel
Save

Powered by TurnKey Linux.