You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/audit/theming/button.md

176 lines
12 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# button — 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](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-24 · **Alcance**: **91%** — 20 de 22 knobs por token público
- **Knobs de apariencia**: 23 — público 20 · privado 2 · global 0 · literal 0 · sistema 1 · excepción 7 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 106 pública(s) — `height-xs`, `height-sm`, `height-md`, `height-lg`, `height-xl`, `padding-inline-xs`, `padding-inline-sm`, `padding-inline-md`, `padding-inline-lg`, `padding-inline-xl`, `gap-xs`, `gap-sm`, `gap-md`, `gap-lg`, `gap-xl`, `font-family`, `line-height`, `letter-spacing`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `font-weight-xs`, `font-weight-sm`, `font-weight-md`, `font-weight-lg`, `font-weight-xl`, `icon-size-xs`, `icon-size-sm`, `icon-size-md`, `icon-size-lg`, `icon-size-xl`, `radius-sm`, `radius-md`, `radius-lg`, `radius-xl`, `radius-full`, `border-width`, `shadow`, `transition-duration`, `transition-ease`, `disabled-opacity`, `primary-track`, `primary-element`, `primary-border`, `primary-solid`, `primary-solid-hover`, `primary-text`, `primary-contrast`, `secondary-track`, `secondary-element`, `secondary-border`, `secondary-solid`, `secondary-solid-hover`, `secondary-text`, `secondary-contrast`, `neutral-track`, `neutral-element`, `neutral-border`, `neutral-solid`, `neutral-solid-hover`, `neutral-text`, `neutral-contrast`, `affirm-track`, `affirm-element`, `affirm-border`, `affirm-solid`, `affirm-solid-hover`, `affirm-text`, `affirm-contrast`, `fulfill-track`, `fulfill-element`, `fulfill-border`, `fulfill-solid`, `fulfill-solid-hover`, `fulfill-text`, `fulfill-contrast`, `risk-track`, `risk-element`, `risk-border`, `risk-solid`, `risk-solid-hover`, `risk-text`, `risk-contrast`, `threat-track`, `threat-element`, `threat-border`, `threat-solid`, `threat-solid-hover`, `threat-text`, `threat-contrast`, `loss-track`, `loss-element`, `loss-border`, `loss-solid`, `loss-solid-hover`, `loss-text`, `loss-contrast`, `palette-track`, `palette-element`, `palette-border`, `palette-solid`, `palette-solid-hover`, `palette-text`, `palette-contrast`
- **Eje `size`**: sí · **ficheros**: `button.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (0)
_Ninguno._
### 1.2 A través de un privado (2)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `button.css:100` | `[data-button][data-gradient][data-variant='solid']` | `background-image` | `var(--_button-fill-finish)` |
| 2 | `button.css:103` | `[data-button][data-gradient][data-variant='solid']:hover:not([data-disabled]):not([data-loading])` | `background-image` | `var(--_button-fill-finish)` |
### 1.3 Literales (0)
_Ninguno._
### 1.4 Excepciones firmadas (7) — 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 | `button.css:63` | `[data-button]` | `inline-size` | `fit-content` |
| 2 | `button.css:64` | `[data-button]` | `block-size` | `fit-content` |
| 3 | `button.css:244` | `[data-button][data-block]` | `inline-size` | `100%` |
| 4 | `button.css:262` | `[data-button][data-icon-only] [data-button-body]` | `inline-size` | `1px` |
| 5 | `button.css:263` | `[data-button][data-icon-only] [data-button-body]` | `block-size` | `1px` |
| 6 | `button.css:309` | `[data-button-sr-only]` | `inline-size` | `1px` |
| 7 | `button.css:310` | `[data-button-sr-only]` | `block-size` | `1px` |
## 2. Sistema transversal (1) — 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 | `button.css:121` | `[data-button]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_button-height` | 6 | `var(--button-height-md)`, `var(--button-height-xs)`, `var(--button-height-sm)`, `var(--button-height-lg)`, `var(--button-height-xl)` | public | **sí** |
| `--_button-padding-inline` | 6 | `var(--button-padding-inline-md)`, `var(--button-padding-inline-xs)`, `var(--button-padding-inline-sm)`, `var(--button-padding-inline-lg)`, `var(--button-padding-inline-xl)` | public | **sí** |
| `--_button-gap` | 6 | `var(--button-gap-md)`, `var(--button-gap-xs)`, `var(--button-gap-sm)`, `var(--button-gap-lg)`, `var(--button-gap-xl)` | public | **sí** |
| `--_button-radius` | 6 | `var(--button-radius-md)`, `var(--button-radius-sm)`, `var(--button-radius-lg)`, `var(--button-radius-xl)`, `var(--button-radius-full)` | public | **sí** |
| `--_button-font-size` | 6 | `var(--button-font-size-md)`, `var(--button-font-size-xs)`, `var(--button-font-size-sm)`, `var(--button-font-size-lg)`, `var(--button-font-size-xl)` | public | **sí** |
| `--_button-font-weight` | 6 | `var(--button-font-weight-md)`, `var(--button-font-weight-xs)`, `var(--button-font-weight-sm)`, `var(--button-font-weight-lg)`, `var(--button-font-weight-xl)` | public | **sí** |
| `--_button-icon-size` | 6 | `var(--button-icon-size-md)`, `var(--button-icon-size-xs)`, `var(--button-icon-size-sm)`, `var(--button-icon-size-lg)`, `var(--button-icon-size-xl)` | public | **sí** |
| `--_button-bg` | 7 | `var(--button-palette-solid)`, `var(--button-palette-track)`, `transparent` | exception, public | no |
| `--_button-fg` | 7 | `var(--_button-finish-ink, var(--button-palette-contrast))`, `var(--button-palette-text)` | public | **sí** |
| `--_button-border` | 7 | `var(--button-palette-solid)`, `transparent`, `var(--button-palette-border)` | exception, public | no |
| `--_button-hover-bg` | 7 | `var(--button-palette-solid-hover)`, `var(--button-palette-element)`, `var(--button-palette-track)`, `transparent` | exception, public | no |
| `--_button-hover-border` | 7 | `var(--button-palette-solid-hover)`, `transparent`, `var(--button-palette-border)` | exception, public | no |
Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inline del wrapper): `--_button-fill-finish`, `--_button-finish-ink`.
## 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 guard `recipe-css-contract` prohíbe el primitivo).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (6)
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 (`--button-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `width` | `root` | `fit-content` | 1 |
| `height` | `root` | `fit-content` | 1 |
| `body-width` | `root` | `1px` | 1 |
| `body-height` | `root` | `1px` | 1 |
| `sr-only-width` | `root` | `1px` | 1 |
| `sr-only-height` | `root` | `1px` | 1 |
### 4.2 Sin nombre mecánico (3)
- **⚠ decisión: el privado que alimenta este knob no se declara en el CSS (viene de `base.ts` o de un estilo inline) — hay que resolverlo antes de nombrarlo** — 2: `background-image`.
- **⚠ 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, un `background` en 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
<!-- veredicto:start -->
**EJECUTADO 2026-08-23 — 55 % → 91 %**, UNA clave nueva y 17 declaraciones
anotadas. Y al medirlo salió lo más gordo del eje hasta hoy: **56 de sus 106
claves públicas no pintan nada**, y no por deuda de este componente.
Lo acuñado es una sola cosa, la que faltaba: **el paso `full` de la escala de
radio**. `[data-rounded='full']` leía el primitivo global a pelo, así que la
forma que más se retoca —la píldora— era el único escalón fuera del contrato.
**Diecisiete declaraciones pasan a excepción firmada, y son de dos clases**:
- **Identidad** (7): `fit-content` × 2 (un botón ES su contenido), el `100 %`
de `[data-block]` (que ES «a todo el ancho») y los cuatro `1px` de las dos
cajas sr-only.
- **Ausencia** (10): los `transparent` de `soft`, `outline`, `ghost` y `plain`.
La variante ES la ausencia de cromo; ponerle fondo a un `ghost` es pedir un
`soft`. Diez claves de `transparent` habrían sido el antipatrón que la propia
doctrina avisa (agrupa por VALOR).
Para que eso cuente, **el censo aprende la válvula un piso más abajo**: un
PRIVADO cuyo valor es un literal CON su razón escrita es una ausencia firmada,
no deuda. Sin eso, cuatro privados marcaban «no deriva» y seis knobs leían
inalcanzables mientras TODAS las demás ramas del mismo privado leen un
público.
**⚠⚠ EL HALLAZGO: la cascada de paleta ANULA los tonos del componente.** El
último bloque del forward, `[data-button][data-color]`, resuelve
`--button-palette-*` desde el `--palette-*` GLOBAL —que `[data-color='{tono}']`
llena desde `--color-{tono}-*`— y gana a las reglas por tono
`[data-button][data-color='{tono}']` por ORDEN, con la misma especificidad
(0,2,0). Medido sobre `data-color='risk'`: `--button-risk-solid` no mueve nada
ni en `:root` ni en el nodo, mientras `--palette-solid` y `--color-risk-solid`
en el nodo repintan. Las siete `primary-*` son el RESPALDO de esa misma regla y
sí actúan en una instancia SIN `data-color`.
No es de este componente: **49 recetas emiten el mismo forward genérico** y el
catálogo tiene **419 claves de tono**. Medido igual en `badge` y `callout`. El
arreglo cabe en el orden de emisión —sacar la regla genérica ANTES de las de
tono— y no movería un píxel (el valor por defecto de `--button-risk-solid` ES
`var(--color-risk-solid)`), pero cambia el contrato de la jaula del color: un
ancestro que inyecte `--palette-*` dejaría de pisar un tono semántico. Es
firma. → §13.
**El techo restante (2 filas) es canal de valor**: `--_button-fill-finish`, el
acabado de degradado que el generador deriva por instancia. Su dial de tema
existe y es del sistema (`--gradient-finish-lift`).
**Los 23 rojos restantes, medidos**: la escala de iconos pide un `<svg>` de
verdad (la demo no lleva icono), la de radio es una prop OPT-IN que la demo no
usa, `transition-*` es el punto ciego del congelado, `disabled-opacity` pide el
estado, y las cuatro `palette-*` son nombres RESUELTOS: escribirlas en `:root`
pierde por diseño (desde el nodo alcanzan).
**Un error de método, anotado porque casi cuela**: anclé la clave nueva en la
primera línea `'radius-xl'` del fichero y aterrizó en **`meter`**. Lo delató el
navegador (`--button-radius-full` sin definir ⇒ `border-radius` a 0). **Una
inserción se ancla en el BLOQUE del componente, nunca en una línea de clave.**
Verificación: **diff de computed = 0** (416 valores, 7 estados; la demo monta UN
botón) · equivalencia del paso `full` comprobada aparte (9999px antes y después,
y el token lo mueve) · centinela **34/106**, todo lo demás adjudicado por
escrito · censo 91 % · `component:audit` PASS · suite eidos con el rojo conocido
ajeno · `rtl:check` 0 · `docs:check` 0 · `check` sin errores propios · captura de
las seis variantes + píldora + tono `risk`.
<!-- veredicto:end -->

Powered by TurnKey Linux.