16 KiB
avatar — alcance de tema: análisis y propuesta
Generado por
node --import tsx/esm scripts/theming-census.ts --report. Lo medido y la propuesta se regeneran; el Veredicto (§5) se conserva. Vista de conjunto: README · método y protocolo:PLAN-theming.md§1, §2, §7.
- Medido: 2026-08-25 · Alcance: 100% — 24 de 24 knobs por token público
- Knobs de apariencia: 24 — público 24 · privado 0 · global 0 · literal 0 · sistema 0 · excepción 12 · estructural 0 · puente 0 · canal 0 (los cinco últimos, fuera del ratio)
- Contrato hoy (
lib/recipes/base.ts): 86 pública(s) —size-xs,size-sm,size-md,size-lg,size-xl,size-xxl,font-family,font-size-xs,font-size-sm,font-size-md,font-size-lg,font-size-xl,font-size-xxl,font-weight,radius-full,radius-md,radius-sm,radius-none,border-width,primary-solid-bg,primary-solid-fg,primary-soft-bg,primary-soft-fg,primary-outline-border,primary-outline-fg,secondary-solid-bg,secondary-solid-fg,secondary-soft-bg,secondary-soft-fg,secondary-outline-border,secondary-outline-fg,neutral-solid-bg,neutral-solid-fg,neutral-soft-bg,neutral-soft-fg,neutral-outline-border,neutral-outline-fg,affirm-solid-bg,affirm-solid-fg,affirm-soft-bg,affirm-soft-fg,affirm-outline-border,affirm-outline-fg,fulfill-solid-bg,fulfill-solid-fg,fulfill-soft-bg,fulfill-soft-fg,fulfill-outline-border,fulfill-outline-fg,risk-solid-bg,risk-solid-fg,risk-soft-bg,risk-soft-fg,risk-outline-border,risk-outline-fg,threat-solid-bg,threat-solid-fg,threat-soft-bg,threat-soft-fg,threat-outline-border,threat-outline-fg,loss-solid-bg,loss-solid-fg,loss-soft-bg,loss-soft-fg,loss-outline-border,loss-outline-fg,ring-width-sm,ring-width-md,ring-width-lg,badge-border-width,badge-fg-custom-contrast,group-gap-xs,group-gap-sm,group-gap-md,group-gap-lg,group-overlap-xs,group-overlap-sm,group-overlap-md,group-overlap-lg,group-overlap-xl,group-overlap-xxl,group-carve-width,group-carve-color,group-reverse-z-max,overflow-font-weight· 11 privada(s) forward —_palette-solid,_palette-surface,_palette-contrast,_palette-text,_palette-border,_bg,_fg,_border,_badge-bg,_badge-fg,_badge-border - Eje
size: sí · ficheros:avatar.css
1. Knobs fuera de alcance
1.1 Directo a primitivo global (0)
Ninguno.
1.2 A través de un privado (0)
Ninguno.
1.3 Literales (0)
Ninguno.
1.4 Excepciones firmadas (12) — fuera del ratio
Literales que llevan su anotación /* literal: <razón> */ en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
component-audit. Una desviación firmada no es deuda — se listan para que la
razón se lea, no para acuñarlas.
| # | fichero:línea | selector | propiedad | valor |
|---|---|---|---|---|
| 1 | avatar.css:45 |
[data-avatar] |
background |
var(--_avatar-bg) |
| 2 | avatar.css:48 |
[data-avatar] |
color |
var(--_avatar-fg) |
| 3 | avatar.css:107 |
[data-avatar-image] |
inline-size |
100% |
| 4 | avatar.css:108 |
[data-avatar-image] |
block-size |
100% |
| 5 | avatar.css:126 |
[data-avatar-fallback] |
inline-size |
100% |
| 6 | avatar.css:127 |
[data-avatar-fallback] |
block-size |
100% |
| 7 | avatar.css:132 |
[data-avatar-fallback] |
line-height |
1 |
| 8 | avatar.css:171 |
[data-avatar][data-ring='solid'] |
box-shadow |
0 0 0 var(--_avatar-ring-width) var(--_avatar-ring-color) |
| 9 | avatar.css:176 |
[data-avatar][data-ring='soft'] |
box-shadow |
0 0 0 var(--_avatar-ring-width) color-mix(in srgb, var(--_avatar-ring-color) 30%, transparent), 0 0 0 calc(var(--_avatar-ring-width) * 2.2) color-mix(in srgb, var(--_avatar-ring-color) 10%, transparent) |
| 10 | avatar.css:249 |
[data-avatar-badge] |
background |
var(--_avatar-badge-bg) |
| 11 | avatar.css:252 |
[data-avatar-badge] |
color |
var(--_avatar-badge-fg) |
| 12 | avatar.css:258 |
[data-avatar-badge] |
line-height |
1 |
2. Sistema transversal (0) — informativo, fuera del ratio
Un tema los alcanza a nivel de sistema, por diseño (recipe-contract §2).
Ninguno.
3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
|---|---|---|---|---|
--_avatar-size |
7 | var(--avatar-size-md), var(--avatar-size-xs), var(--avatar-size-sm), var(--avatar-size-lg), var(--avatar-size-xl), var(--avatar-size-xxl) |
public | sí |
--_avatar-font-size |
7 | var(--avatar-font-size-md), var(--avatar-font-size-xs), var(--avatar-font-size-sm), var(--avatar-font-size-lg), var(--avatar-font-size-xl), var(--avatar-font-size-xxl) |
public | sí |
--_avatar-radius |
5 | var(--avatar-radius-full), var(--avatar-radius-md), var(--avatar-radius-sm), var(--avatar-radius-none) |
public | sí |
--_avatar-ring-color |
10 | var(--avatar-neutral-solid-bg), var(--avatar-primary-solid-bg), var(--avatar-secondary-solid-bg), var(--avatar-affirm-solid-bg), var(--avatar-fulfill-solid-bg), var(--avatar-risk-solid-bg) …(+3) |
private, public | no |
--_avatar-ring-width |
4 | var(--avatar-ring-width-md), var(--avatar-ring-width-sm), var(--avatar-ring-width-lg) |
public | sí |
--_avatar-badge-size |
1 | calc(var(--_avatar-size) * 0.3) |
private | no |
--_avatar-badge-dot-size |
1 | calc(var(--_avatar-size) * 0.26) |
private | no |
--_avatar-badge-flip |
2 | 1, -1 |
literal | no |
--_avatar-badge-bg |
3 | var(--_avatar-badge-color-custom, currentColor), color-mix( in srgb, var(--_avatar-badge-color-custom, currentColor) 18%, transparent ), var(--color-surface-default) |
global, private | no |
--_avatar-badge-fg |
3 | var(--avatar-badge-fg-custom-contrast), var(--_avatar-badge-color-custom, currentColor) |
private, public | no |
--_avatar-badge-border |
1 | var(--_avatar-badge-color-custom, currentColor) |
private | no |
--_avatar-group-overlap |
7 | var(--avatar-group-overlap-md), var(--avatar-group-overlap-xs), var(--avatar-group-overlap-sm), var(--avatar-group-overlap-lg), var(--avatar-group-overlap-xl), var(--avatar-group-overlap-xxl) |
public | sí |
Consumidos y no declarados en el CSS (vienen de base.ts o de un estilo inline del wrapper): --_avatar-badge-color-custom, --_avatar-bg, --_avatar-border, --_avatar-fg, --_avatar-ring-color-custom.
4. Propuesta de corrección
- Tiene eje
size: los tokens dimensionales van por talla ({part}-{eje}-{k}) apuntando al bundle--size-{k}-*, nunca al primitivo crudo (theming §5; el guardrecipe-css-contractprohíbe el primitivo).
4.1 Tokens a declarar en lib/recipes/base.ts (5)
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
al final) y theming §6.7 (slots de color, modificador delante). Un token con
DOS valores distintos es una colisión de nombre: son dos knobs, o el nombre
no distingue lo que debería — se marca ⚠.
token (--avatar-…) |
scope TSC | valor propuesto | usos |
|---|---|---|---|
shadow |
root |
⚠ 0 0 0 var(--_avatar-ring-width) var(--_avatar-ring-color) / 0 0 0 var(--_avatar-ring-width) color-mix(in srgb, var(--_avatar-ring-color) 30%, transparent), 0 0 0 calc(var(--_avatar-ring-width) * 2.2) color-mix(in srgb, var(--_avatar-ring-color) 10%, transparent) |
2 |
bg |
root |
var(--_avatar-bg) |
1 |
fg |
root |
var(--_avatar-fg) |
1 |
badge-bg |
root |
var(--_avatar-badge-bg) |
1 |
badge-fg |
root |
var(--_avatar-badge-fg) |
1 |
4.2 Sin nombre mecánico (6)
- ⚠ decisión:
100%es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar — 4:inline-size,block-size. - ⚠ decisión:
1es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar — 2:line-height.
4.3 Avisos sobre los tokens propuestos (1)
- ⚠ decisión: el valor es una expresión — el token puede llevar la expresión entera o sólo su término variable —
--avatar-shadow
4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
- Privado que no deriva de un público — §3 lo marca; el privado debe leer el público o desaparecer.
- Velo o acento en el nodo equivocado (
archetype: 'item'en un envoltorio, unbackgrounden shorthand que mata la capa de estado) — se mide desde el píxel hacia arriba. - Doble animación al mover un sello a una superficie con animación propia — registro de
animationstart/animationend. - Diff de computed = 0 en reposo · hover · abierto · disabled · foco, por talla, antes y después.
- Centinela por token nuevo: valor imposible en el root → el nodo lo sigue. Si no, el token miente.
5. Veredicto
Ejecutado 2026-08-23 — 75 % → 90 %, contrato 88 claves, centinela 83/88 (5
adjudicadas). Diff de computed 0 en tres bases (demo de avatar con
insignia + anillo: 576 valores · la misma en modo fallback: 384 · demo de
AvatarGroup: 3.072) y las dos capturas 2× idénticas al byte.
Lo primero, porque contaminaba todo lo demás: su contrato existía y ningún
instrumento lo veía. La entrada avatar de base.ts es la ÚNICA construida
por una IIFE (un helper local genera sus 24 ámbitos compuestos), así que su mapa
vive un tabulador más adentro, en el return {. El censo la leía como «sin
entrada en base.ts» —84 claves invisibles— y el centinela moría con no recipe block for avatar: el componente no se podía medir. Los dos lectores
leen ya la IIFE (dedentan el return). Sin eso, el gate de este componente no
significaba nada.
Lo que se acuñó (6) y por qué:
group-overlap-{xs..xxl}— la escala de solape deAvatarGroup. Había UNA clave,--avatar-group-overlap, y la receta la re-declaraba en seis bloques[data-size]: sentada en el elemento, ganaba siempre al:rootdonde escribe un tema. Medido: desde el asiento del tema,37pxno movía el margen ni un píxel; escrita sobre el nodo, sí. Es un falso positivo del centinela (escribe el token también sobre cada nodo del componente, y para una propiedad personalizada que la receta re-declara EN EL ELEMENTO ese inline sí gana), y queda anotado como límite del instrumento. Ahora el paso viaja por--_avatar-group-overlapy los seis alcanzan desde:root(−8,4 · −11,2 · −14 · −16,8 · −22,4 · −33,6 px → 37 px, uno a uno).
Lo que se retiró (2 declaraciones muertas, diff 0 las dos):
group-max— el envoltorio escribía--avatar-group-maxINLINE y la receta declaraba su default99; no lo leía nadie. El tope se aplica condata-has-max+:nth-child(n + M)porque una variable no entra en:nth-child()— lo dice el propio comentario del CSS. Retirada de los dos sitios: el+3del grupo sigue exactamente donde estaba.- el respaldo
, whitede--_avatar-badge-fg: el contrato ya declara--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.4contra--font-line-height-sm). Comprobado en la rama custom: la tinta sigue computandorgb(255, 255, 255).
Nota 2026-08-25: las tablas generadas de §1–§4 de esta ficha siguen nombrando
--avatar-badge-color-customy--avatar-ring-color-customcomo 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
centrado por la caja flex; con interlineado se descentra.
Lo que se queda fuera, y por qué (los 3 privados, el techo real es 90 %):
--_avatar-bg, --_avatar-fg y --_avatar-badge-bg son un CONMUTADOR —
cambian de fuente con la variante (solid · soft · outline) y su valor sale
del forward de paleta THM-2 que la capa de color alimenta por instancia desde
[data-color]. Un público encima dejaría que un tema los fijara y matara el
color= de cada avatar (la razón de §3.pre del handoff). Aplanarlos obligaría
además a duplicar cada regla por tono: 24 combinaciones.
Las cinco adjudicaciones del centinela (todas medidas a mano sobre el nodo
REAL, con transiciones congeladas): size-xxl y font-size-xxl viven en el
paso xxl, que el barrido de tallas del guard no alcanza (para en xl — el
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í:
R-5.3, cuatro claves fuera de la gramática, PREEXISTENTES:— FIRMADA Y EJECUTADA 2026-08-25 (opción A-c; la cuarta que el audit truncaba esring-color-custom,badge-color-custom,badge-color-custom-contrast(y la cuarta que el audit trunca). La tinta esfg, nocolor.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— medidowhite→rgb(4,5,6)); dos se retiraron del contrato (ring-color-customybadge-color-customson 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-customleídas convar(…, currentColor), la forma que usan las 5 privadas de envoltorio del parque y la adjudicación quetabshizo con su indicador); y una quedó EXENTA (group-carve-coloralimentabox-shadow— la geometría del carve, no tinta;group-carve-fgsería gramática correcta y semántica peor). Contrato 88 → 86 («Tokens 86» en vivo), censo--names4 → 0 en todo el catálogo, sonda 0 diffs sobre 576 valores, 0 píxeles. Detalle en PLAN-theming §8.- El barrido de tallas del centinela no llega a
xxl; añadirlo dejaría STALE las seis excepciones quemetricsescribió por lo mismo, así que se respeta el precedente y se adjudica.