diff --git a/docs/audit/theming/collapsible.md b/docs/audit/theming/collapsible.md index a1db347ae..985ad9b77 100644 --- a/docs/audit/theming/collapsible.md +++ b/docs/audit/theming/collapsible.md @@ -73,6 +73,43 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Ejecutado 2026-08-24 · 90 % → 100 %** (12 knobs · 10 públicos · 2 sistema +fuera del ratio). Contrato **11 → 12** claves. Diff de computed **VACÍO** (736 +valores, 8 estados, 3 nodos: las tres partes del morfo, cerrada y abierta). +Centinela **10/12**, 2 adjudicadas. + +**Lo que entró — una clave, la COSTURA.** `trigger-gap` es el único knob que la +receta leía a pelo (`var(--space-2)`): el valor es del sistema y el knob no era +de nadie, así que un tema no podía apretar esta cabecera sin mover el paso de +espaciado en toda la app (PLAN §2-A). El nombre lo decide el catálogo, no la +propuesta: `trigger-gap` ya existe **ocho veces** en `base.ts`, cinco de ellas +con exactamente `var(--space-2)`. + +**Lo que NO se acuña, y por qué.** Los dos knobs restantes son **sistema +transversal**, no deuda: el velo de hover es +`linear-gradient(var(--state-hover), var(--state-hover))` —la capa de +`archetypes.css`, nunca una invención por componente— y el anillo de foco es +`--focus-ring-*`. Y **no hay clave de motion que acuñar**: la firma de +apertura/cierre es el par de eventos `emerge-expand` / `emerge-collapse` que +materializa `EidosConfig.motion.signatures`; la receta no declara `@keyframes` +(recipe-contract §2, R-4.5), así que el componente que «anima su apertura» no +tiene ni un knob de animación propio. + +**Las dos silenciosas del guard, medidas.** `transition-duration` y +`transition-ease` **SON** la transición que el centinela congela para poder leer +todo lo demás — la clase que `color-picker` documentó el 2026-08-23. Medidas sin +congelar sobre el encabezado real: `0.18s, 0.18s → 4.321s, 4.321s` y +`cubic-bezier(0.4, 0, 0.2, 1) ×2 → steps(3) ×2` (dos entradas porque el trigger +transiciona `background` y `color`). Al ledger con esa razón. + +**Y una que NO hizo falta adjudicar.** `disabled-opacity` leía muerta porque el +escenario no lleva el estado. Encender el interruptor `disabled` para toda la +corrida se habría llevado por delante el tinte de hover (`pointer-events: none` +en el provider — la trampa de «montar más mide menos»), así que el estado se +mide como **superficie extra** por la URL de la demo (`?perm.disabled=true`, +precedente `radio-group`): 8/11 → 10/12 con la clave de hover viva en la +primera URL. + +**Veredicto: CERRADO al 100 %.** No queda knob fuera del alcance. diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index feb32c290..5406cd86c 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -586,6 +586,16 @@ export const SENTINEL_EXCEPTIONS: Record> = { 'indicator-font-weight': 'the “Copied!” indicator only exists for a moment after a copy (0 nodes at rest, counted); forced → reaches (500 → 123)' }, + collapsible: { + // 2026-08-24. Its ONLY two silences, and both are the freeze the guard + // needs to read everything else. Measured unfrozen on the real trigger, + // which transitions `background` and `color` (two entries in the computed + // list, so the write shows up twice). + 'transition-duration': + 'the guard freezes transitions and this token IS the transition; measured unfrozen on [data-collapsible-trigger] -> reaches (0.18s, 0.18s -> 4.321s, 4.321s)', + 'transition-ease': + 'same freeze, same node -> reaches (cubic-bezier(0.4, 0, 0.2, 1) x2 -> steps(3) x2)' + }, code: { radius: 'the DEMO HARNESS wins: `[data-uix-docs] code` (uix.css, 0-1-1) beats the recipe base rule `[data-code]` (0-1-0) and sets border-radius/padding/font on the inline code — confirmed with CDP matched-rules. The variant rules (0,2,0) DO win, which is why only this base-rule token reads dead. Same harness rule that hides the
 of code-block (next-features §13)',
diff --git a/scripts/theming-sentinel.ts b/scripts/theming-sentinel.ts
index f8e771276..d5aad8cc9 100644
--- a/scripts/theming-sentinel.ts
+++ b/scripts/theming-sentinel.ts
@@ -235,6 +235,15 @@ const COMPONENT_OVERRIDES: Record<
 	// Clicking is NOT an option either: its handler moves the focus to the
 	// destination region, so the pill would vanish on the way in.
 	'skip-link': { openBy: 'focus', openWith: ['[data-skip-link]'] },
+	// Its opacity floor is the only knob behind a state, and switching the demo's
+	// `disabled` toggle ON for the whole run would take the hover tint with it
+	// (`pointer-events: none` on the provider — the "mounting more can measure
+	// less" trap). The demo reads the state from the URL, so the disabled chrome
+	// is measured as an EXTRA SURFACE instead of a ledger line (the radio-group
+	// precedent): 8/11 -> 9/11, with the hover key still alive on the first URL.
+	collapsible: {
+		urls: ['/uix/components/collapsible', '/uix/components/collapsible?perm.disabled=true']
+	},
 	// A hover card opens on POINTER-OVER, not on click: clicking its trigger
 	// (an ``) navigates instead of revealing the panel, so the portaled
 	// content never enters the document and 32 of its 35 tokens read dead.
diff --git a/src/uix/eidos/components/collapsible/README.md b/src/uix/eidos/components/collapsible/README.md
index 969588ed0..5c0be47f0 100644
--- a/src/uix/eidos/components/collapsible/README.md
+++ b/src/uix/eidos/components/collapsible/README.md
@@ -55,6 +55,44 @@ Partes publicas: `Trigger`, `Content`.
   debe subir primero a Soma/Morfo porque requiere comportamiento real:
   dimension colapsada, foco/inert en el area oculta y contrato de datos.
 
+## Talla y tema
+
+**Sin eje `size`** (decisión de arriba: el disclosure no expone escala visual),
+así que el contrato es plano: **12 claves públicas** en `lib/recipes/base.ts`,
+derivadas aquí del contrato vivo — la pestaña **Tokens** de su demo las resuelve
+en el navegador.
+
+| clave (`--collapsible-…`) | qué mueve                                                   | default                          |
+| ------------------------- | ----------------------------------------------------------- | -------------------------------- |
+| `trigger-font-family`     | tipografía del encabezado                                   | `var(--style-label-font-family)` |
+| `trigger-font-size`       | "                                                           | `var(--size-sm-font-size)`       |
+| `trigger-font-weight`     | "                                                           | `var(--font-weight-medium)`      |
+| `trigger-fg`              | tinta del encabezado en reposo                              | `var(--color-content-primary)`   |
+| `hover-trigger-fg`        | tinta del encabezado bajo el puntero                        | `var(--color-content-primary)`   |
+| `trigger-radius`          | radio del encabezado                                        | `var(--radius-sm)`               |
+| `trigger-padding`         | caja del encabezado                                         | `var(--space-2) var(--space-3)`  |
+| `trigger-gap`             | separación entre la etiqueta y lo que se componga a su lado | `var(--space-2)`                 |
+| `content-padding`         | caja del cuerpo                                             | `var(--space-3)`                 |
+| `transition-duration`     | duración del fundido de fondo y tinta                       | `var(--duration-normal)`         |
+| `transition-ease`         | su curva                                                    | `var(--ease-default)`            |
+| `disabled-opacity`        | atenuación del conjunto deshabilitado                       | `var(--opacity-disabled)`        |
+
+Lo que entró el **2026-08-24** (90 % → 100 %): **`trigger-gap`**, la costura.
+El `gap` del encabezado leía `var(--space-2)` a pelo, así que el valor era del
+sistema y el knob no era de nadie — un tema no podía apretar esta cabecera sin
+mover el paso de espaciado en toda la app.
+
+**Lo que NO es del componente**: el velo de hover y el anillo de foco son la
+capa del sistema (`--state-hover`, `--focus-ring-*`), y la firma de
+apertura/cierre es el par de eventos `emerge-expand` / `emerge-collapse` que
+`EidosConfig.motion.signatures` materializa — **no hay `@keyframes` en la
+receta**, y por eso no hay clave de motion propia que acuñar.
+
+**Lo que el guard da por silencioso**: `transition-duration` y `transition-ease`,
+porque el centinela CONGELA las transiciones para poder leer todo lo demás.
+Medidos sin congelar sobre el encabezado real (`0.18s → 4.321s`,
+`cubic-bezier(0.4, 0, 0.2, 1) → steps(3)`), con su razón escrita en el ledger.
+
 ## Referencias
 
 - Air anterior: sin componente Collapsible en `glm-5:src/uix/air/components`;
diff --git a/src/uix/eidos/components/collapsible/collapsible.css b/src/uix/eidos/components/collapsible/collapsible.css
index 7645521e5..70aa93dbb 100644
--- a/src/uix/eidos/components/collapsible/collapsible.css
+++ b/src/uix/eidos/components/collapsible/collapsible.css
@@ -14,7 +14,7 @@
 [data-collapsible-trigger] {
 	display: inline-flex;
 	align-items: center;
-	gap: var(--space-2);
+	gap: var(--collapsible-trigger-gap);
 	padding: var(--collapsible-trigger-padding);
 	border: none;
 	border-radius: var(--collapsible-trigger-radius);
diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css
index 8b33cfd8b..029a4f16a 100644
--- a/src/uix/eidos/generated/base.css
+++ b/src/uix/eidos/generated/base.css
@@ -1138,6 +1138,7 @@
 	--collapsible-hover-trigger-fg: var(--color-content-primary);
 	--collapsible-trigger-radius: var(--radius-sm);
 	--collapsible-trigger-padding: var(--space-2) var(--space-3);
+	--collapsible-trigger-gap: var(--space-2);
 	--collapsible-content-padding: var(--space-3);
 	--collapsible-transition-duration: var(--duration-normal);
 	--collapsible-transition-ease: var(--ease-default);
diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts
index 472ac3bbd..162a10176 100644
--- a/src/uix/eidos/lib/recipes/base.ts
+++ b/src/uix/eidos/lib/recipes/base.ts
@@ -1153,6 +1153,12 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
 		'hover-trigger-fg': 'var(--color-content-primary)',
 		'trigger-radius': 'var(--radius-sm)',
 		'trigger-padding': 'var(--space-2) var(--space-3)',
+		// Label ↔ whatever the consumer composes beside it (a chevron, a count).
+		// The VALUE is the system's spacing step; the KNOB is the component's, so
+		// a theme can tighten this disclosure head without moving `--space-2`
+		// app-wide (PLAN §2-A, the seam). Five sibling recipes already spell the
+		// same name with the same step.
+		'trigger-gap': 'var(--space-2)',
 		'content-padding': 'var(--space-3)',
 		'transition-duration': 'var(--duration-normal)',
 		'transition-ease': 'var(--ease-default)',
diff --git a/web/routes/uix/components/collapsible/+page.svelte b/web/routes/uix/components/collapsible/+page.svelte
index bb880a993..64dbaa923 100644
--- a/web/routes/uix/components/collapsible/+page.svelte
+++ b/web/routes/uix/components/collapsible/+page.svelte
@@ -1,5 +1,6 @@