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

18 KiB

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 · método y protocolo: PLAN-theming.md §1, §2, §7.

  • Medido: 2026-08-22 · 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

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.

Powered by TurnKey Linux.