10 KiB
chat-composer — 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% — 34 de 34 knobs por token público
- Knobs de apariencia: 39 — público 34 · privado 0 · global 0 · literal 0 · sistema 5 · excepción 1 · estructural 0 · puente 0 · canal 0 (los cinco últimos, fuera del ratio)
- Contrato hoy (
lib/recipes/base.ts): 33 pública(s) —gap,bg,border,border-width,focus-border,radius,padding-inline,padding-block,input-font-size,input-line-height,input-fg,input-placeholder-fg,input-padding-inline,input-padding-block,context-bg,context-accent,context-accent-size,context-font-size,context-fg,context-radius,context-padding-inline,context-padding-block,context-gap,context-close-size,context-close-radius,attachments-gap,send-size,send-bg,send-fg,send-radius,transition-duration,transition-ease,disabled-opacity - Eje
size: no · ficheros:chat-composer.css
1. Knobs fuera de alcance
1.1 Directo a primitivo global (0)
Ninguno — los tres que había (los dos ejes y el radio del chip de descarte) están cosidos en dos claves: la clave es del componente, el valor sigue siendo el del sistema.
Ninguno.
1.2 A través de un privado (0)
Ninguno.
1.3 Literales (0)
Ninguno.
1.4 Excepciones firmadas (1) — fuera del ratio
Y cuatro declaraciones RETIRADAS por muertas (no son excepción: dejaron de
existir): [data-chat-composer-context-close] > svg (1em en los dos ejes) y
[data-chat-composer-send] > svg (1.1em). Los dos glifos son Icon
compuestos y el Icon escribe style="width: var(--icon-size-{k})" EN LÍNEA:
ningún selector gana a un estilo en línea. Medido — cierre 14 px
(--icon-size-xs) contra los 13,3 px de 1em; envío 16 px (--icon-size-sm)
contra los 14,7 px de 1.1em. Retirarlas da 0 diffs.
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 | chat-composer.css:123 |
[data-chat-composer-input] |
inline-size |
100% |
2. Sistema transversal (5) — informativo, fuera del ratio
Un tema los alcanza a nivel de sistema, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
|---|---|---|---|---|
| 1 | chat-composer.css:78 |
[data-chat-composer-context-close]:hover |
background-image |
linear-gradient(var(--state-hover), var(--state-hover)) |
| 2 | chat-composer.css:82 |
[data-chat-composer-context-close]:active |
background-image |
linear-gradient(var(--state-press), var(--state-press)) |
| 3 | chat-composer.css:160 |
[data-chat-composer-send]:hover |
background-image |
linear-gradient(var(--state-hover), var(--state-hover)) |
| 4 | chat-composer.css:164 |
[data-chat-composer-send]:active |
background-image |
linear-gradient(var(--state-press), var(--state-press)) |
| 5 | chat-composer.css:177 |
[data-chat-composer-context-close]:focus-visible, [data-chat-composer-send]:focus-visible |
outline |
var(--focus-ring-width) solid var(--focus-ring-color) |
3. Privados de la receta — ¿de dónde sale su valor?
La receta no declara privados propios en su CSS.
4. Propuesta de corrección
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
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 (--chat-composer-…) |
scope TSC | valor propuesto | usos |
|---|
4.2 Sin nombre mecánico (1)
- ⚠ 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 — 1:inline-size.
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 — 79 % → 100 %. Contrato 31 → 33 claves; censo
público 34 · global 0 · literal 0 · sistema 5 · excepción 1. Centinela
29/33, 4 adjudicadas. component-audit PASS · eidos-lint 0 invalid ·
rtl:check 0 · docs:check 0 · check sin errores del componente.
La mitad de su contrato no la montaba la demo. El guard leía 14 muertas de
31, y once eran la barra de contexto (context-*, opt-in tras un chip) más la
bandeja de adjuntos (attachments-gap, opt-in tras un botón). Con las dos
encendidas por prepareWith, 29/33 sin tocar el código. El resto es lo de
siempre: enciende sólo lo que no tapa y cuenta los nodos (9 aquí, con los dos
glifos).
Lo acuñado son DOS claves, no cuatro. El generador partía el chip de
descarte en width + height y marcaba conflicto porque metía en el mismo
saco la caja (26 px) y su glifo (1em). Son dos cosas distintas: la caja es un
control cuadrado —una clave, context-close-size, como send-size en esta
misma receta— y el glifo no es un knob de tema.
Cuatro declaraciones RETIRADAS por muertas. Los dos > svg (el 1em del
cierre y el 1.1em del envío) no pueden ganar: el Icon compuesto emite
style="width: var(--icon-size-{k})" INLINE. Medido: 14 px contra 13,3 px, y
16 px contra 14,7 px. Retirarlas da 0 diffs. Es la misma lección que chat-log
la misma tarde, con dos glifos en vez de uno.
Y una colisión ENTRE RECETAS que no es del instrumento:
--chat-composer-disabled-opacity no alcanza porque file-upload barre
DESCENDIENTES — [data-file-upload] [data-disabled] pesa (0,2,0), lo mismo que
[data-chat-composer-send][data-disabled], y su receta se emite MÁS TARDE.
El composer vive dentro de un FileUpload en su composición DOCUMENTADA (su
propio README y su demo), así que pierde siempre. Fuera del envoltorio alcanza
(0.4 → 0.123 en el send y en el shell). Los dos defaults son
--opacity-disabled, así que la colisión es invisible hasta que un tema mueva
uno. No se arregla aquí (tocar file-upload es cascadear): se adjudica.
Las otras tres adjudicaciones son hábitos del guard: focus-border (sólo
bajo :focus-within, y el guard blurea tras abrir; enfocado el textarea REAL,
color(srgb …/0.48) → rgb(1, 2, 3)) y el par de la transición, que ES la
transición que el guard congela (0,12 s → 4,321 s sin congelar).
El diff de computed NO salió limpio del todo, y la causa está medida: 2-4
valores de [data-chat-composer-attach] blockSize, que flota entre 30 px y
36 px. Flota igual corriendo dos veces el MISMO código (control: after vs
after2, 2 diffs). La causa: en una recarga de cada ocho, el Button size="sm" variant="ghost" que la demo compone en esa ranura computa 36 px y fondo
oklch(0.5556 0.1829 305.86) —el sólido primario— con sus data-size y
data-variant puestos: las reglas de talla/variante de button llegan tarde
en el dev server. Un nodo que esta receta no dimensiona, y una carrera de CSS
del servidor de desarrollo. Los 2.204 valores restantes, idénticos.
Lo que queda fuera y por qué: una identidad firmada (§1.4) y cinco knobs de sistema transversal (§2). El componente está CERRADO en su número.
Nota 2026-08-24 — la adjudicación de disabled-opacity de aquí arriba era
FALSA, y la firma la retira. file-upload acotó su barrido a sus cinco partes
propias, así que la colisión ya no existe. Pero además la cuenta que la
justificaba estaba incompleta: «invisible hasta que un tema mueva uno» es cierto
para las dos reglas PROPIAS del composer —[data-chat-composer][data-disabled]
y [data-chat-composer-send][data-disabled] empataban a 0.4 y ahí se quedaban—
y falso para el textarea, que esa lectura no miró. [data-textarea] y
[data-textarea-input] no declaran atenuación propia (y esta ficha ya
declara el wrapper «celda neutra»), así que el barrido les imponía 0.4 dos
veces más sobre el 0.4 legítimo del provider. La opacidad multiplica:
0.4³ = 0.064 de alpha efectiva en el TEMA POR DEFECTO, seis veces más
apagado de lo diseñado, con el placeholder casi ilegible — 925 de 25.568 px,
3,6 % de la superficie, sin tocar un tema.
Recuperado, medido: exactamente 6 de 25 valores del estado disabled
—[data-textarea] opacity 0.4 → 1, alpha 0.16 → 0.4 y cursor default → auto;
[data-textarea-input] opacity 0.4 → 1 y alpha 0.064 → 0.4; y el cursor
default → auto del provider del composer—. Nada más: send y el provider
siguen en 0.4 porque ambos defaults son --opacity-disabled; lo que recuperan es
que su --chat-composer-disabled-opacity vuelve a alcanzar. Centinela
29 → 30/33 y la entrada del ledger sale como STALE (retirada de
theming-sentinel-exceptions.ts). La sonda estándar da 0 diffs reales — es
CIEGA a esto: no tiene paso disabled y su filtro de nodos no incluye
[data-textarea]; los 4 crudos son otra vez el blockSize flotante del
attach ya documentado arriba, reproducido sobre código idéntico.