7.6 KiB
image-picker — 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-24 · Alcance: 100% — 13 de 13 knobs por token público
- Knobs de apariencia: 16 — público 13 · privado 0 · global 0 · literal 0 · sistema 3 · excepción 4 (los dos últimos, fuera del ratio)
- Contrato hoy (
lib/recipes/base.ts): 14 pública(s) —gap,preview-radius,preview-bg,preview-border,preview-aspect,toolbar-gap,toolbar-inset,button-size,button-bg,button-fg,button-border,button-radius,button-font-size,hover-button-bg - Eje
size: no · ficheros:image-picker.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 (4) — 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 | image-picker.css:45 |
[data-image-picker-canvas] > * |
inline-size |
100% |
| 2 | image-picker.css:46 |
[data-image-picker-canvas] > * |
block-size |
100% |
| 3 | image-picker.css:79 |
[data-image-picker-rotate], [data-image-picker-remove] |
line-height |
1 |
| 4 | image-picker.css:109 |
[data-image-picker-rotate]:focus-visible, [data-image-picker-remove]:focus-visible |
outline-color |
Highlight |
2. Sistema transversal (3) — 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 | image-picker.css:19 |
[data-image-picker][data-disabled] |
opacity |
var(--opacity-disabled) |
| 2 | image-picker.css:90 |
[data-image-picker-rotate]:focus-visible, [data-image-picker-remove]:focus-visible |
outline |
var(--focus-ring-width) solid var(--focus-ring-color) |
| 3 | image-picker.css:96 |
[data-image-picker-rotate][disabled], [data-image-picker-remove][disabled] |
opacity |
var(--opacity-disabled) |
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 (--image-picker-…) |
scope TSC | valor propuesto | usos |
|---|
4.2 Sin nombre mecánico (4)
- ⚠ 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 — 2: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 — 1:line-height. - foco: lo posee el sistema (
--focus-ring-*, theming §32) — no acuñar token propio — 1:outline-color.
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 — 71 % → 100 %, una clave nueva y cuatro literales firmados. Diff de computed 0 sobre 1.344 valores en 7 estados (6 nodos); R-5.4 de 13/13 a 14/14 sin una sola adjudicación; capturas 2× antes/después idénticas.
El instrumento medía UN nodo. El picker arranca VACÍO —un dropzone de
FileUpload— y todo lo que la receta pinta (preview, canvas, toolbar y los dos
botones de icono) sólo existe en el estado ready, al que no llega ningún
trigger: hay que cargar un fichero. Con el chip «Load sample image» de la demo
la sonda pasa de 1 a 6 nodos. Sonda (DEMO_VARIANTS) y centinela
(prepareWith) llevan ya el interruptor; el centinela además necesita
openMarker para que su re-apertura por token no vuelva a pulsar el chip y
remonte la vista previa a mitad de corrida.
La única clave: hover-button-bg. Era el único knob que iba a un primitivo
global a pelo (--color-surface-default). La §4 lo nombraba hover-rotate-bg
por el PRIMER selector de la regla, y miente dos veces: pinta rotate y
remove, y el contrato de este componente ya llama button-* a ese par desde que
nació. El catálogo decide — hover-{parte}-{slot} es la forma de las 60 claves
hover-* de base.ts.
No es el velo del sistema, y por eso se acuña. Los dos botones flotan SOBRE
la foto: su relleno de reposo es translúcido (color-mix(… 88%, transparent)) y
el hover lo vuelve OPACO — un cambio de RELLENO, no una capa encima. Medido
desde el píxel hacia arriba con las transiciones congeladas: el cambio cae en el
<button> (el nodo con forma, radio 6 px, srgb .988/.88 → oklch(.9911 0 0))
y ningún ancestro recibe background-image, porque el arquetipo action no
trae velo — archetypes.css sólo vela trigger, item y option. No se está
bendiciendo un duplicado, que es lo que paró el hover-* de listbox.
Los cuatro literales se firman, no se acuñan (válvula de recipe-contract §3):
el 100% doble del hijo del canvas es identidad (la <Image> ES la caja del
canvas, que ya está en inset: 0), el line-height: 1 es un botón de un solo
glifo, y el Highlight de forced-colors es la paleta del sistema operativo —
un valor de tema ahí lo sustituiría el UA, que es justo lo que ese modo hace.
Fuera del ratio por doctrina: --opacity-disabled (×2) y el par
--focus-ring-* son de la capa de sistema (theming §32).
Sin eje size y es correcto: la prop size del wrapper viaja al
ImageAdjustments embebido; este cromo de orquestación no escala.
⚠ Un defecto real, ANOTADO, no arreglado: ese hover neutro es una invención
por componente, y §38 / R-4.3 mandan que el hover neutro sea la capa
--state-*. Migrarlo cambia el default (velo translúcido en vez de relleno
opaco) y D-TH.5 lo prohíbe dentro de este eje. Queda escrito aquí y en el README
del componente. Nota para quien lo firme: el arquetipo del par es action, que
HOY no recibe velo del sistema, así que la migración no es sólo cambiar la
receta — o action gana velo en archetypes.css (mueve píxel en todos los
action del catálogo) o el par cambia de arquetipo, que es morfo.