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/combobox.md

236 lines
18 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.

# combobox — 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-23 · **Alcance**: **85%** — 76 de 89 knobs por token público
- **Knobs de apariencia**: 97 — público 76 · privado 4 · global 3 · literal 6 · sistema 8 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 106 pública(s) — `content-z`, `control-height-xs`, `control-height-sm`, `control-height-md`, `control-height-lg`, `control-height-xl`, `control-font-size-xs`, `control-font-size-sm`, `control-font-size-md`, `control-font-size-lg`, `control-font-size-xl`, `control-gap-xs`, `control-gap-sm`, `control-gap-md`, `control-gap-lg`, `control-gap-xl`, `control-padding-inline-xs`, `control-padding-inline-sm`, `control-padding-inline-md`, `control-padding-inline-lg`, `control-padding-inline-xl`, `content-padding-block-xs`, `content-padding-block-sm`, `content-padding-block-md`, `content-padding-block-lg`, `content-padding-block-xl`, `content-padding-inline-xs`, `content-padding-inline-sm`, `content-padding-inline-md`, `content-padding-inline-lg`, `content-padding-inline-xl`, `content-font-size-xs`, `content-font-size-sm`, `content-font-size-md`, `content-font-size-lg`, `content-font-size-xl`, `control-height`, `control-font-size`, `control-gap`, `control-padding-inline`, `content-padding-block`, `content-padding-inline`, `content-font-size`, `gap`, `font-family`, `fg`, `border-width`, `control-radius`, `control-bg`, `control-border`, `control-shadow`, `control-fg`, `control-line-height`, `disabled-control-bg`, `disabled-control-border`, `disabled-control-fg`, `placeholder-fg`, `disabled-input-fg`, `trigger-size`, `trigger-radius`, `trigger-fg`, `indicator-size`, `content-min-width`, `content-max-width`, `content-max-height`, `content-radius`, `content-fg`, `content-font-family`, `content-line-height`, `viewport-gap`, `viewport-padding-block-end`, `scrollbar-inset`, `group-gap`, `group-padding-block`, `group-heading-padding-block`, `group-heading-padding-inline`, `group-heading-bg`, `group-heading-fg`, `group-heading-font-family`, `group-heading-font-size`, `group-heading-font-weight`, `group-heading-line-height`, `group-heading-letter-spacing`, `separator-thickness`, `separator-margin-block`, `separator-bg`, `item-gap`, `item-padding-inline`, `item-fg`, `disabled-item-fg`, `item-indicator-size`, `selected-tags-gap`, `selected-tags-margin-block-end`, `selected-tag-gap`, `selected-tag-padding-inline-start`, `selected-tag-padding-inline-end`, `selected-tag-inset`, `selected-tag-radius`, `selected-tag-bg`, `selected-tag-border`, `selected-tag-fg`, `selected-tag-line-height`, `selected-tag-font-size-offset`, `selected-tag-remove-size`, `selected-tag-remove-margin-inline-start`, `selected-tag-remove-radius` · 3 privada(s) forward — `_palette-track`, `_palette-border`, `_palette-text`
- **Eje `size`**: no · **ficheros**: `combobox.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (3)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `combobox.css:118` | `[data-combobox-trigger]:hover:not(:disabled):not([data-disabled])` | `background` | `var(--color-surface-raised)` |
| 2 | `combobox.css:119` | `[data-combobox-trigger]:hover:not(:disabled):not([data-disabled])` | `color` | `var(--color-content-primary)` |
| 3 | `combobox.css:415` | `[data-combobox-selected-tag-remove]:hover` | `background` | `var(--color-surface-overlay)` |
### 1.2 A través de un privado (4)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `combobox.css:51` | `[data-combobox-control]:has([data-combobox-input]:focus-visible), [data-combobox-control]:has([data-combobox-trigger]:focus-visible), [data-combobox-control]:has([data-state='open'])` | `border-color` | `var(--_combobox-palette-border)` |
| 2 | `combobox.css:157` | `[data-combobox-trigger][data-state='open'] [data-combobox-indicator]` | `color` | `var(--_combobox-palette-text)` |
| 3 | `combobox.css:296` | `[data-combobox-item][data-state='checked']` | `background` | `var(--_combobox-palette-track)` |
| 4 | `combobox.css:297` | `[data-combobox-item][data-state='checked']` | `color` | `var(--_combobox-palette-text)` |
### 1.3 Literales (6)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `combobox.css:4` | `[data-combobox]` | `inline-size` | `100%` |
| 2 | `combobox.css:20` | `[data-combobox-control]` | `inline-size` | `100%` |
| 3 | `combobox.css:68` | `[data-combobox-input]` | `inline-size` | `100%` |
| 4 | `combobox.css:321` | `[data-combobox-item][data-state='checked'] [data-combobox-item-indicator]` | `opacity` | `1` |
| 5 | `combobox.css:416` | `[data-combobox-selected-tag-remove]:hover` | `opacity` | `1` |
| 6 | `combobox.css:422` | `[data-combobox-selected-tag-remove]:focus-visible` | `opacity` | `1` |
### 1.4 Excepciones firmadas (0) — 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.
_Ninguno._
## 2. Sistema transversal (8) — 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 | `combobox.css:53` | `[data-combobox-control]:has([data-combobox-input]:focus-visible), [data-combobox-control]:has([data-combobox-trigger]:focus-visible), [data-combobox-control]:has([data-state='open'])` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 2 | `combobox.css:123` | `[data-combobox-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 3 | `combobox.css:331` | `[data-combobox-arrow] polygon` | `fill` | `var(--depth-overlay-surface)` |
| 4 | `combobox.css:336` | `[data-combobox-arrow] path` | `stroke` | `var(--depth-overlay-border)` |
| 5 | `combobox.css:393` | `[data-combobox-selected-tag][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
| 6 | `combobox.css:407` | `[data-combobox-selected-tag-remove]` | `opacity` | `var(--opacity-muted)` |
| 7 | `combobox.css:420` | `[data-combobox-selected-tag-remove]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--color-primary-border)` |
| 8 | `combobox.css:428` | `[data-combobox-selected-tag-remove][data-disabled], [data-combobox-selected-tag-remove]:disabled` | `opacity` | `var(--opacity-disabled)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_combobox-control-height` | 1 | `var(--combobox-control-height)` | public | **sí** |
| `--_combobox-control-padding-inline` | 1 | `var(--combobox-control-padding-inline)` | public | **sí** |
| `--_combobox-control-gap` | 1 | `var(--combobox-control-gap)` | public | **sí** |
| `--_combobox-font-size` | 2 | `var(--combobox-control-font-size)`, `var(--combobox-content-font-size)` | public | **sí** |
| `--_combobox-content-padding-block` | 1 | `var(--combobox-content-padding-block)` | public | **sí** |
| `--_combobox-content-padding-inline` | 1 | `var(--combobox-content-padding-inline)` | public | **sí** |
| `--_combobox-content-width` | 1 | `var(--_combobox-match-anchor-width, var(--combobox-content-min-width))` | public | **sí** |
Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inline del wrapper): `--_combobox-match-anchor-width`, `--_combobox-palette-border`, `--_combobox-palette-text`, `--_combobox-palette-track`.
## 4. Propuesta de corrección
- **Consume la capa compartida `list-surface`.** Un eje que la capa posee se consume como `var(--_x, var(--x))`; el consumidor **no acuña** `--combobox-{eje}` para él — sería un vocabulario paralelo (README de `eidos/components`, «Capas compartidas» regla 2).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (3)
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 (`--combobox-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `hover-trigger-bg` | `root` | `var(--color-surface-raised)` | 1 |
| `hover-trigger-fg` | `root` | `var(--color-content-primary)` | 1 |
| `hover-selected-tag-remove-bg` | `root` | `var(--color-surface-overlay)` | 1 |
### 4.2 Sin nombre mecánico (10)
- **⚠ 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** — 3: `inline-size`.
- **⚠ 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** — 4: `border-color`, `color`, `background`.
- **⚠ decisión: `1` 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** — 3: `opacity`.
### 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 -->
**Revisión 2026-08-20 — verificación previa a implementación (Opus).**
**Análisis (§1–§3): CORRECTO.** Cifras reproducidas (96 · 0 · 15 · 67 · 6 · 8);
filas muestreadas contra `combobox.css` ✓. §3 acierta: los 7 privados propios
son todos `global` (ninguno deriva de un público) y los 4 de paleta vienen del
forward THM-2 en `base.ts`.
**Propuesta (§4.1): APTA CON CINCO CORRECCIONES**, verificadas contra el CSS y
el patrón firmado (Sidebar F3, `recipes/base.ts:4735`):
1. **La forma por talla no es `size:{k}` a secas.** El patrón firmado es DOS
piezas: coordenadas por talla en scope estable (`control-height-{k}`,
`root`) **+ un nombre RESUELTO** (`control-height`) con `declarations`
— default **`host`** (= md) y overrides `size:xs..xl` — que es lo ÚNICO que
la receta consume. La tabla propone sólo la mitad (y con `size:md` como si
md necesitara selector). El root sí estampa `data-size`
(`combobox.svelte:45`), pero el **content viaja por portal**: sus
declarations por talla necesitan `parts: ['content']` (precedente `select`,
tsc.md §multi-part). Control/input/trigger cuelgan del root → sin `parts`.
2. **Valores por talla: bundle donde casa, verbatim donde desvía.** Medido
contra `--size-{k}-*` emitido: `control-height-{k}`, `*-font-size-{k}` y
`control-gap-{k}` casan 1:1 → el valor correcto es la coordenada del bundle
(`var(--size-{k}-control-height)` …), que computa idéntico y es lo que
`recipe-css-contract` exige (el primitivo crudo está prohibido en esas
coordenadas). `control-padding-inline-{k}` DESVÍA en xs/sm/lg/xl
(space-2/2-5/3-5/4 vs bundle 1-5/2/4/5) → valor verbatim de hoy, desviación
visible; adoptar el bundle sería cambio visual = decisión aparte.
3. **DOS filas romperían el default (value-changing, NO copiar):**
`selected-tag-height-{k}` propone `var(--control-height-{k})` pero el CSS
real es `calc(var(--_combobox-control-height) - var(--space-2))`
(`combobox.css:427`) — perdería el −8px; y `selected-tag-font-size-{k}`
propone `var(--font-size-{k})` pero el real es
`calc(var(--_combobox-font-size) - 1px)` (`:432`, con anotación
`/* literal: optical */` — candidata a excepción R-5). Corrección: el tag
consume el público resuelto DENTRO de su expresión
(`calc(var(--combobox-control-height) - var(--space-2))`), no un token
plano.
4. **`content-width` ⚠ se resuelve con el precedente `select`**: acuñar
`content-min-width: 14rem` (público, `root`) y el privado pasa a
`var(--_combobox-match-anchor-width, var(--combobox-content-min-width))` —
idéntico a `--_select-content-width`. El `max-inline-size: min(28rem, …)`
pide su par `content-max-width: 28rem`. El `calc(100vw - var(--space-4))`
se queda en la receta (guarda de viewport, no knob).
5. **Los hovers neutros NO se tokenizan: son la capa de estado.**
`hover-trigger-bg/fg` (`:hover → var(--color-surface-raised)`) y
`hover-selected-tag-remove-bg` (`var(--color-surface-overlay)`) son el
hover bespoke que §38 deprecó — la corrección del eje D es migrarlos a
`--state-hover` sobre el nodo CON forma, no darles nombre propio. Eso mueve
el píxel (leve) ⇒ **decisión firmada** antes de tocarlos, como en
nav-menu; mientras no se firme, se quedan como están y NO entran en el
contrato.
Resto de §4.1 (colores/espacios one-off del control, input, group-heading,
separator, item-indicator, selected-tags): **válida tal cual** — nombres en
vocabulario, scope `root`, valores verbatim. §4.2: los 3 `border` y el
`padding` shorthand se parten en ejes lógicos (mecánico, mismo computed);
los `100%`/`opacity:1` quedan fuera del knob (D-TH.2). Los 4 knobs de paleta
(`open-control-border`, `open-trigger-fg`, checked item) se QUEDAN en el
patrón THM-2 (forward privado + shared palette layer); promoverlos a público
`palette-*` es decisión, no corrección.
**Instrucción para el implementador**: bloque `combobox` en `base.ts` con el
patrón Sidebar (coordenadas + resueltos por eje: `control-height`,
`control-font-size`, `control-gap`, `control-padding-inline`,
`content-padding-block`, `content-padding-inline`, `content-font-size`); los
privados de la receta pasan a leer el público resuelto o mueren; regenerar
(`npm run generate:eidos-css`), y el protocolo §7 entero (sonda antes/después
por talla y estado — reposo·hover·abierto·disabled·foco —, centinela por token
nuevo, censo `--only combobox` al 100 % o excepciones escritas, diff de
computed = 0). Ojo §7.2: la sonda del content abre el popup ANTES de medir.
**Bloqueos de firma**: hover→state-layer (punto 5) · adopción del bundle en
`control-padding-inline` (punto 2) · D-TH.2/D-TH.6 siguen sin firmar.
---
**EJECUTADO 2026-08-20.** Alcance **0 % → 76 %** · contrato **1 → 106 claves
públicas**. Fuera quedan los 3 hovers bespoke (esperan la firma del punto 5), 6
literales de layout (`100%`, `opacity: 1`) y 12 knobs que pasan por privados que
AHORA derivan de públicos.
Se implementaron las cinco correcciones. Dos cosas sólo aparecieron al ejecutar:
- **Los resueltos del control se emiten con `parts: ['control', 'input']`**, no
en el root: el `data-size` que manda vive en el control, y el input suelto lo
necesita fuera de él. Los del panel, con `parts: ['content']` por el portal.
- **El centinela cazó un fallo REAL de la primera pasada**: había dejado vivos
los ocho bloques `[data-…][data-size='…']` de la receta, que pisaban los
tokens nuevos con los valores viejos. El diff de computed daba 0 justamente
porque la ruta vieja seguía mandando — sin centinela habría pasado por bueno.
Verificación (artefactos):
- **Diff de computed = 0**: 1.566 valores en **8 estados** (reposo · 5 tallas ·
panel ABIERTO · hover), sobre los 6 nodos en reposo y los 13 con el panel
desplegado.
- **Centinela**: 67/106 automático. Verificados a mano: `content-z`
(80 → 4321; vive en `[data-floating-wrapper]`, sin attr del componente),
`placeholder-fg`, `disabled-input-fg`, `trigger-fg`, `disabled-control-bg`,
`disabled-control-fg`, y los del panel abierto por teclado (`content-fg`,
`content-radius`, `item-gap`, `group-heading-bg`, `viewport-gap`). Los 7
nombres RESUELTOS no se mueven por diseño — el tema mueve la coordenada.
`control-border`, `disabled-control-border` e `item-fg` los pisa la paleta en
estado abierto / checked. Los 14 `selected-tag-*`, los 3 `separator-*` y
`scrollbar-inset` **no son verificables en esta demo** (no monta modo
múltiple, ni separadores, ni scroll): se comprobó su consumidor textual en la
receta, y así queda dicho en vez de darlos por buenos.
- **`content-font-family` y `content-line-height` son inertes**:
`[data-depth='overlay']` declara lo mismo con igual especificidad y gana por
orden (la tipografía de portal del Build contract). Anotado en el README.
- Guards: `component:audit` PASS · `eidos-lint` 0 invalid · suite eidos sin
rojos nuevos · `rtl:check` 0 · `docs:check` 0. El guard del bundle obligó a
apuntar `indicator-size` / `item-indicator-size` a `--size-sm-icon-size`.
**Instrumento**: la sonda congela `transition` (una propiedad transicionada lee
su valor INICIAL justo tras la escritura, y un token vivo parecía muerto) pero
**no** `animation` — Presence espera `animationend` para montar, y congelarla
impide que el panel se abra.
<!-- veredicto:end -->

Powered by TurnKey Linux.