diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 3870f4802..576b7b288 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -10,9 +10,9 @@ - **Medido**: 2026-08-21 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/` - **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**? -- **Alcance global**: **46%** — 2174 de 4680 knobs pasan por un token público del componente -- **Reparto**: público 2174 · privado 654 · global 1272 · literal 580 · sistema transversal 509 _(fuera del ratio)_ -- **Sin token público propio**: 46 · **alcance < 20 %**: 40 · **alcance 100 %**: 7 · **con eje `size`**: 65 +- **Alcance global**: **47%** — 2195 de 4680 knobs pasan por un token público del componente +- **Reparto**: público 2195 · privado 654 · global 1251 · literal 580 · sistema transversal 509 _(fuera del ratio)_ +- **Sin token público propio**: 45 · **alcance < 20 %**: 39 · **alcance 100 %**: 7 · **con eje `size`**: 65 ## Cómo se lee @@ -85,18 +85,18 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma | 13 | [popover](./popover.md) | 28 | 42% | 51 | 36 | | 14 | [radio-cards](./radio-cards.md) | 28 | 60% | 72 | 39 | | 15 | [drawer](./drawer.md) | 27 | 55% | 62 | 44 | -| 16 | [code-block](./code-block.md) | 26 | 0% | 26 | 0 | -| 17 | [mockup](./mockup.md) | 25 | 14% | 29 | 0 | -| 18 | [aura](./aura.md) | 24 | 31% | 35 | 24 | -| 19 | [password-field](./password-field.md) | 24 | 31% | 36 | 23 | -| 20 | [timeline](./timeline.md) | 24 | 51% | 49 | 45 | +| 16 | [mockup](./mockup.md) | 25 | 14% | 29 | 0 | +| 17 | [aura](./aura.md) | 24 | 31% | 35 | 24 | +| 18 | [password-field](./password-field.md) | 24 | 31% | 36 | 23 | +| 19 | [timeline](./timeline.md) | 24 | 51% | 49 | 45 | +| 20 | [avatar](./avatar.md) | 23 | 36% | 36 | 0 | -## Sin ningún token público propio (46) +## Sin ningún token público propio (45) Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward (`_palette-*`), que no son contrato: un tema no puede nombrarlos. -[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`card-group`](./card-group.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`code-block`](./code-block.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) +[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`card-group`](./card-group.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) ## Tabla completa (162 recetas, por alcance ascendente) @@ -108,7 +108,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [month-grid](./month-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – | | [year-grid](./year-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – | | [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – | -| [code-block](./code-block.md) | 0% | 26 | 0 | 2 | 24 | 0 | 0 | 0 | – | | [card-group](./card-group.md) | 0% | 24 | 0 | 0 | 18 | 4 | 2 | 0 | y | | [spinner](./spinner.md) | 0% | 23 | 0 | 15 | 4 | 4 | 0 | 0 | y | | [picker-shell](./picker-shell.md) | 0% | 21 | 0 | 4 | 17 | 0 | 0 | 0 | y | @@ -226,6 +225,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component | [chat-composer](./chat-composer.md) | 79% | 44 | 31 | 0 | 3 | 5 | 5 | 31 | – | | [textarea](./textarea.md) | 80% | 28 | 20 | 2 | 0 | 3 | 3 | 37 | – | | [gradient-builder](./gradient-builder.md) | 80% | 60 | 45 | 7 | 1 | 3 | 4 | 72 | – | +| [code-block](./code-block.md) | 81% | 26 | 21 | 2 | 3 | 0 | 0 | 20 | – | | [chat-message](./chat-message.md) | 82% | 84 | 63 | 2 | 6 | 6 | 7 | 75 | y | | [fab](./fab.md) | 82% | 17 | 14 | 0 | 2 | 1 | 0 | 10 | – | | [toast](./toast.md) | 83% | 42 | 35 | 6 | 0 | 1 | 0 | 85 | – | diff --git a/docs/audit/theming/code-block.md b/docs/audit/theming/code-block.md index 6b2ab60e3..4c1c0bde0 100644 --- a/docs/audit/theming/code-block.md +++ b/docs/audit/theming/code-block.md @@ -5,48 +5,27 @@ > Vista de conjunto: [README](./README.md) · método y protocolo: > [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7. -- **Medido**: 2026-08-20 · **Alcance**: **0%** — 0 de 26 knobs por token público -- **Knobs de apariencia**: 26 — público 0 · privado 2 · global 24 · literal 0 · sistema 0 _(fuera del ratio)_ -- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`** +- **Medido**: 2026-08-21 · **Alcance**: **81%** — 21 de 26 knobs por token público +- **Knobs de apariencia**: 26 — público 21 · privado 2 · global 3 · literal 0 · sistema 0 _(fuera del ratio)_ +- **Contrato hoy** (`lib/recipes/base.ts`): 20 pública(s) — `radius`, `fg`, `border-width`, `border`, `surface-bg`, `header-gap`, `header-padding-inline`, `header-padding-block`, `header-bg`, `title-font-family`, `title-font-size`, `title-fg`, `lang-font-family`, `lang-font-size`, `lang-fg`, `lang-letter-spacing`, `copy-gap`, `copy-font-size`, `copy-fg`, `pre-padding` - **Eje `size`**: no · **ficheros**: `code-block.css` ## 1. Knobs fuera de alcance -### 1.1 Directo a primitivo global (24) +### 1.1 Directo a primitivo global (3) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `code-block.css:14` | `[data-code-block]` | `border-radius` | `var(--radius-md)` | -| 2 | `code-block.css:16` | `[data-code-block]` | `color` | `var(--color-content-primary)` | -| 3 | `code-block.css:20` | `[data-code-block][data-variant='surface']` | `background` | `var(--color-neutral-element)` | -| 4 | `code-block.css:21` | `[data-code-block][data-variant='surface']` | `border` | `var(--border-width) solid var(--color-neutral-border)` | -| 5 | `code-block.css:26` | `[data-code-block][data-variant='outline']` | `border` | `var(--border-width) solid var(--color-neutral-border)` | -| 6 | `code-block.css:37` | `[data-code-block-header]` | `gap` | `var(--space-2)` | -| 7 | `code-block.css:38` | `[data-code-block-header]` | `padding-inline` | `var(--space-3)` | -| 8 | `code-block.css:39` | `[data-code-block-header]` | `padding-block` | `var(--space-1)` | -| 9 | `code-block.css:40` | `[data-code-block-header]` | `border-bottom` | `var(--border-width) solid var(--color-neutral-border)` | -| 10 | `code-block.css:41` | `[data-code-block-header]` | `background` | `var(--color-neutral-track)` | -| 11 | `code-block.css:52` | `[data-code-block-title]` | `font-family` | `var(--font-family-mono)` | -| 12 | `code-block.css:53` | `[data-code-block-title]` | `font-size` | `var(--font-size-xs)` | -| 13 | `code-block.css:54` | `[data-code-block-title]` | `color` | `var(--color-content-secondary)` | -| 14 | `code-block.css:61` | `[data-code-block-lang]` | `font-family` | `var(--font-family-mono)` | -| 15 | `code-block.css:62` | `[data-code-block-lang]` | `font-size` | `var(--font-size-xxs)` | -| 16 | `code-block.css:63` | `[data-code-block-lang]` | `color` | `var(--color-content-muted)` | -| 17 | `code-block.css:65` | `[data-code-block-lang]` | `letter-spacing` | `var(--tracking-caps)` | -| 18 | `code-block.css:82` | `[data-code-block-copy]` | `gap` | `var(--space-1)` | -| 19 | `code-block.css:84` | `[data-code-block-copy]` | `font-size` | `var(--font-size-xs)` | -| 20 | `code-block.css:85` | `[data-code-block-copy]` | `color` | `var(--color-affirm-text)` | -| 21 | `code-block.css:90` | `[data-code-block-pre]` | `padding` | `var(--space-3)` | -| 22 | `code-block.css:97` | `[data-code-block-code]` | `font-family` | `var(--style-code-font-family)` | -| 23 | `code-block.css:99` | `[data-code-block-code]` | `font-weight` | `var(--style-code-font-weight)` | -| 24 | `code-block.css:101` | `[data-code-block-code]` | `letter-spacing` | `var(--style-code-letter-spacing)` | +| 1 | `code-block.css:102` | `[data-code-block-code]` | `font-family` | `var(--style-code-font-family)` | +| 2 | `code-block.css:104` | `[data-code-block-code]` | `font-weight` | `var(--style-code-font-weight)` | +| 3 | `code-block.css:106` | `[data-code-block-code]` | `letter-spacing` | `var(--style-code-letter-spacing)` | ### 1.2 A través de un privado (2) | # | fichero:línea | selector | propiedad | valor | | ---: | --- | --- | --- | --- | -| 1 | `code-block.css:98` | `[data-code-block-code]` | `font-size` | `var(--_code-block-font-size, var(--style-code-font-size))` | -| 2 | `code-block.css:100` | `[data-code-block-code]` | `line-height` | `var(--_code-block-line-height, var(--style-code-line-height))` | +| 1 | `code-block.css:103` | `[data-code-block-code]` | `font-size` | `var(--_code-block-font-size, var(--style-code-font-size))` | +| 2 | `code-block.css:105` | `[data-code-block-code]` | `line-height` | `var(--_code-block-line-height, var(--style-code-line-height))` | ### 1.3 Literales (0) @@ -66,7 +45,7 @@ Consumidos y **no declarados en el CSS** (vienen de `base.ts` o de un estilo inl ## 4. Propuesta de corrección -### 4.1 Tokens a declarar en `lib/recipes/base.ts` (20) +### 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 @@ -76,32 +55,12 @@ no distingue lo que debería — se marca `⚠`. | token (`--code-block-…`) | scope TSC | valor propuesto | usos | | --- | --- | --- | ---: | -| `radius` | `root` | `var(--radius-md)` | 1 | -| `fg` | `root` | `var(--color-content-primary)` | 1 | -| `surface-bg` | `root` | `var(--color-neutral-element)` | 1 | -| `header-gap` | `root` | `var(--space-2)` | 1 | -| `header-padding-inline` | `root` | `var(--space-3)` | 1 | -| `header-padding-block` | `root` | `var(--space-1)` | 1 | -| `header-bg` | `root` | `var(--color-neutral-track)` | 1 | -| `title-font-family` | `root` | `var(--font-family-mono)` | 1 | -| `title-font-size` | `root` | `var(--font-size-xs)` | 1 | -| `title-fg` | `root` | `var(--color-content-secondary)` | 1 | -| `lang-font-family` | `root` | `var(--font-family-mono)` | 1 | -| `lang-font-size` | `root` | `var(--font-size-xxs)` | 1 | -| `lang-fg` | `root` | `var(--color-content-muted)` | 1 | -| `lang-letter-spacing` | `root` | `var(--tracking-caps)` | 1 | -| `copy-gap` | `root` | `var(--space-1)` | 1 | -| `copy-font-size` | `root` | `var(--font-size-xs)` | 1 | -| `copy-fg` | `root` | `var(--color-affirm-text)` | 1 | | `code-font-family` | `root` | `var(--style-code-font-family)` | 1 | | `code-font-weight` | `root` | `var(--style-code-font-weight)` | 1 | | `code-letter-spacing` | `root` | `var(--style-code-letter-spacing)` | 1 | -### 4.2 Sin nombre mecánico (6) +### 4.2 Sin nombre mecánico (2) -- **⚠ decisión: `border` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 2: `border`. -- **⚠ decisión: `border-bottom` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `border-bottom`. -- **⚠ decisión: `padding` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `padding`. - **⚠ 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: `font-size`, `line-height`. ### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) @@ -116,6 +75,58 @@ no distingue lo que debería — se marca `⚠`. -_(pendiente — lo escribe el autor; se conserva al regenerar)_ +**Medido 2026-08-21** (sonda ×2 sobre el mismo código = 0 diffs en 1.305 +valores · 7 estados). El handoff lo llamaba «el más mecánico de los que +quedan»: **no lo es**. Sus 26 knobs se parten en dos grupos con doctrinas +distintas, y la §4 propone acuñar los cinco del segundo, que es justo lo que la +regla de capas prohíbe. + +1. **El bloque `[data-code-block-code]` consume la CAPA tipográfica + `--style-code-*`, y no debe acuñar nada.** Sus cinco declaraciones + (`font-family`, `font-size`, `font-weight`, `line-height`, + `letter-spacing`) leen `var(--style-code-…)`, y las dos que pasan por + privado lo hacen con la forma canónica de la escotilla por instancia: + `var(--_code-block-font-size, var(--style-code-font-size))`, donde el + privado lo escribe el WRAPPER desde la prop `size` + (`code-block.svelte:50-51`, sólo si se pasa) y el nombre de estilo ES la + superficie de tema. Es literalmente el patrón que D-TH.2-b describió para + `heading` / `text` / `code`. Acuñar `code-font-family` y compañía, como pide + la §4.1, sería el vocabulario paralelo que la regla 2 de capas compartidas + prohíbe: **no se acuñan**. +2. **Consecuencia en la métrica, y una decisión que NO es mía.** El censo + cuenta esas cinco como `global` porque `code-block` no está en + `TYPOGRAPHIC_PRIMITIVES` (`theming-census.ts:153`) — es un componente + COMPUESTO que contiene un primitivo tipográfico, no uno de ellos. Así que el + alcance se queda en **~81 %** en vez del ~100 % que da el trabajo hecho. Es + exactamente el techo que el registro ya tiene abierto: «extender la clase + `system` del censo a las capas compartidas», **pendiente de firma**. La + métrica penaliza hacer lo correcto, otra vez, y no se toca de oficio. +3. **Los shorthand se parten**: los dos `border` (surface / outline) y el + `border-bottom` de la cabecera comparten anchura y color → un + `border-width` + un `border`; el `padding` uniforme del `
` →
+ `pre-padding` (precedente `command.viewport-padding`). El `background:
+ transparent` de outline/ghost y el `border: 0` de ghost son IDENTIDAD de
+ variante, no knobs.
+4. **No hay eje `size` que cascadear.** El wrapper escribe la talla como
+ ESTILO INLINE (`--_code-block-font-size`) y sólo cuando la prop llega; sin
+ ella manda la capa. No hay `data-size` en el CSS y no hay cascada TSC que
+ emitir — la ficha acierta al decir «Eje size: no».
+5. **`copy-fg` es del INDICADOR, no del botón.** El comentario de la receta lo
+ dice: el cromo del trigger viene de `button.css`, y esta regla sólo coloca y
+ tiñe el texto transitorio «Copied». El nombre se queda, pero conviene no
+ leerlo como el color del botón de copiar.
+
+6. **Los tres `font-size` van al BUNDLE, no al primitivo crudo** — y esto lo
+ cazó un guard, no yo: la §4.1 propone `var(--font-size-xs)` /
+ `var(--font-size-xxs)` verbatim, y `recipe-css-contract` («recipes consume
+ the size bundle, not the raw size-coordinate primitives») falló con los tres
+ (`title-font-size`, `lang-font-size`, `copy-font-size`). Corregidos a
+ `var(--size-{k}-font-size)`; los alias son 1:1, así que el cambio preserva
+ el valor — 0 diffs de computed tras aplicarlo. Vale como aviso general: la
+ propuesta generada copia el valor del CSS **verbatim**, y cuando ese valor
+ era ya una violación del contrato, la copia la hereda.
+
+Quedan **20 claves**. Nada más de la propuesta cambia: los nombres del chasis,
+cabecera, título, lengua y copia son correctos tal como los deriva la §4.1.
diff --git a/docs/next-features.md b/docs/next-features.md
index 037e79242..5168cade8 100644
--- a/docs/next-features.md
+++ b/docs/next-features.md
@@ -659,5 +659,38 @@ Lo que `textarea` añadió (2026-08-21):
los diez componentes con ledger: ningún otro cambia, un único STALE
(`command.input-placeholder-fg`, por el arreglo del `::placeholder`).
+Lo que `code-block` añadió (2026-08-21):
+
+- **⚠ La demo de `code-block` NO ENSEÑA el `code-block`: el harness del sitio
+ de documentación le pinta el `` por encima.**
+ `[data-uix-docs] pre` (`web/routes/uix/uix.css:801`) pesa **(0,1,1)** y gana
+ a `[data-code-block-pre]` **(0,1,0)** — y `[data-uix-docs]` es el shell de
+ TODAS las páginas, así que la regla alcanza cualquier demo. Medidas **siete
+ propiedades pisadas**: `padding` (16px 20px del harness contra los 12px de la
+ receta), `border` (1px sólido contra ninguno), `border-radius` (8px contra
+ 0), `background-color` (gris contra transparente), `font-family`,
+ `font-size` (13px contra 14px) y `line-height` (18.2px contra 20.3px). El
+ fondo, el borde y el radio que se ven en esa página son del sitio, no del
+ componente. Es la clase «la demo puede tapar el componente» (precedente
+ `proof-of-human`), pero aquí son siete propiedades y le toca justo al
+ componente cuya razón de ser es mostrar código. Por eso `pre-padding` lee
+ muerto en el guard y está adjudicado con esa medición.
+
+ Acotar la regla del harness es tocar el CSS del SITIO, que sirve a las ~162
+ páginas y a los bloques de código de las fichas: no entra en el commit de un
+ componente. Decisión aparte.
+
+- **⚠ El guard de claves duplicadas del handoff INSPECCIONA EL VACÍO.** El
+ comando documentado en `CONTINUE-theming.md` («Trampas que costaron un commit
+ cada una») es
+ `grep -oE "^ '?[a-z0-9-]+'?: \{" src/uix/eidos/lib/recipes/base.ts | …`,
+ y **`grep -E` no interpreta ` ` como tabulador** — lo lee como una `t`
+ literal, así que el patrón no casa NADA: 0 coincidencias sobre 130 bloques
+ reales. El guard salía siempre «vacío = OK» sin haber mirado nada, que es la
+ definición del antipatrón que el propio proyecto tiene registrado. Con un tab
+ real en el patrón encuentra los 130 (comprobado: cero duplicados en el árbol,
+ así que no hay daño que reparar). El handoff queda corregido con la versión
+ que sí mira.
+
**Deps**: ninguna. Son mejoras del instrumental del eje, ejecutables cuando
estorben.
diff --git a/docs/process/CONTINUE-theming.md b/docs/process/CONTINUE-theming.md
index 27c4a3d37..e6a2e8b50 100644
--- a/docs/process/CONTINUE-theming.md
+++ b/docs/process/CONTINUE-theming.md
@@ -734,9 +734,18 @@ solo uso (verificaciones dirigidas de ayer); no se commitean.
## Trampas que costaron un commit cada una (no repetirlas)
- **Clave duplicada en `recipes/base.ts`**: añadir un bloque que ya existía lo
- descarta **en silencio** (gana la segunda). Guard antes de generar:
- `grep -oE "^\t'?[a-z0-9-]+'?: \{" src/uix/eidos/lib/recipes/base.ts | sed "s/[\t':{ ]//g" | sort | uniq -d`
- → debe salir vacío.
+ descarta **en silencio** (gana la segunda). Guard antes de generar — y que
+ **cuente cuántos bloques ve**: la versión con `\t` que vivió aquí hasta el
+ 2026-08-21 no casaba NINGUNO (`grep -E` lee `\t` como una `t` literal) y salía
+ «vacío = OK» sin haber mirado nada — el antipatrón del guard que inspecciona el
+ vacío, en el propio handoff.
+
+ ```bash
+ node -e "const t=require('fs').readFileSync('src/uix/eidos/lib/recipes/base.ts','utf8').replace(/\r\n/g,'\n');const k=[...t.matchAll(/^\t'?([a-z0-9-]+)'?: \{/gm)].map(m=>m[1]);const d=k.filter((x,i)=>k.indexOf(x)!==i);console.log('bloques',k.length,'| duplicados',d.length?d:'ninguno')"
+ ```
+
+ Debe imprimir los ~130 bloques y `duplicados ninguno`. **Si dice 0 bloques el
+ guard está roto**, no es que el fichero esté limpio.
- **Dejar vivos los bloques `[data-…][data-size='…']` del CSS** al emitir la
cascada por el TSC: pisan los tokens nuevos con los valores viejos, y el diff
de computed da 0 **porque la ruta vieja sigue mandando**. Lo cazó el centinela.
diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts
index aa2b48804..f6a57b77d 100644
--- a/scripts/theming-sentinel-exceptions.ts
+++ b/scripts/theming-sentinel-exceptions.ts
@@ -114,6 +114,10 @@ export const SENTINEL_EXCEPTIONS: Record> = {
'sentinel-spinner-reduced-duration':
'behind prefers-reduced-motion; emulated reduce context → reaches (4s → 11.5s)'
},
+ 'code-block': {
+ 'pre-padding':
+ 'the DEMO HARNESS outweighs the component: `[data-uix-docs] pre` (uix.css:801, 0-1-1) beats `[data-code-block-pre]` (0-1-0) and paints padding/border/radius/bg/font on the . Forced with !important → reaches (16px 20px → 12px). Seven properties measured, registered in next-features §13'
+ },
'tree-view': {
'disabled-row-fg':
'no disabled row in the demo (0 mounted, counted); forced data-disabled → reaches (oklch(0.61 0 0) → rgb(1,2,3))'
diff --git a/src/uix/eidos/components/code-block/README.md b/src/uix/eidos/components/code-block/README.md
index 04ffbcfcd..7563fbf2a 100644
--- a/src/uix/eidos/components/code-block/README.md
+++ b/src/uix/eidos/components/code-block/README.md
@@ -83,6 +83,55 @@ donde copy debe disparar sema (e.g. fulfilment of a tutorial step), el
consumer compone CodeBlock dentro de un componente con sema (Form
step, Stepper item) que posee el verbo `commit-fulfill`.
+## Talla y tema
+
+**Guard R-5.4** (`npm run theming:sentinel -- code-block `): 19/20 tokens
+mueven un computed en vivo; el que calla está adjudicado con su porqué medido
+en `scripts/theming-sentinel-exceptions.ts`. Corrido 2026-08-21.
+
+Contrato en `lib/recipes/base.ts > code-block` (20 claves, entrada NUEVA — no
+tenía). Alcance del censo: **81 %** (era 0 %).
+
+**El nodo del código NO acuña nada, y es lo correcto.**
+`[data-code-block-code]` consume la capa tipográfica `--style-code-*` en sus
+cinco declaraciones, y las dos que pasan por privado lo hacen con la forma
+canónica de la escotilla por instancia —
+`var(--_code-block-font-size, var(--style-code-font-size))` — donde el privado
+lo escribe el wrapper desde la prop `size` (sólo si se pasa) y el nombre de
+estilo ES la superficie de tema. Es el patrón que D-TH.2-b fijó para `heading`
+/ `text` / `code`. Acuñar `--code-block-code-font-*` sería el vocabulario
+paralelo que la regla 2 de capas compartidas prohíbe. Todo lo que hay en el
+contrato es el CASCO alrededor.
+
+Consecuencia en la métrica: el censo cuenta esas cinco como `global` porque
+`code-block` no está en `TYPOGRAPHIC_PRIMITIVES` — es un componente compuesto
+que CONTIENE un primitivo tipográfico, no uno de ellos. De ahí el 81 % en vez
+del ~100 % que da el trabajo hecho. Extender la clase `system` del censo a las
+capas está pendiente de firma (`next-features.md` §13); la métrica penaliza
+hacer lo correcto, y no se fuerza de oficio.
+
+**No hay eje `size` que cascadear**: el wrapper escribe la talla como estilo
+INLINE y sólo cuando la prop llega; sin ella manda la capa. No hay `data-size`
+en el CSS. Los `background: transparent` de outline/ghost y el `border: 0` de
+ghost son identidad de variante, no knobs. `copy-fg` tiñe el indicador
+transitorio «Copied», no el botón: el cromo del trigger viene de `button.css`.
+
+### ⚠ La demo NO enseña este componente — el harness le pinta el ``
+
+Medido el 2026-08-21: `[data-uix-docs] pre` (`web/routes/uix/uix.css:801`) pesa
+**(0,1,1)** contra los **(0,1,0)** de `[data-code-block-pre]`, y `[data-uix-docs]`
+es el shell de TODAS las páginas del sitio. Siete propiedades del `` las
+pone el harness y no la receta: `padding` (16px 20px contra 12px), `border`
+(1px sólido contra ninguno), `border-radius` (8px contra 0),
+`background-color` (gris contra transparente), `font-family`, `font-size`
+(13px contra 14px) y `line-height` (18.2px contra 20.3px). El fondo, el borde y
+el radio que se ven en esa página **son del sitio**. Por eso `pre-padding` lee
+muerto en el guard, con esa medición escrita en el ledger.
+
+Acotar la regla del harness toca el CSS del SITIO, que sirve a ~162 páginas y a
+los bloques de código de las fichas: es decisión aparte, registrada en
+`next-features.md` §13.
+
## Gaps
| Gap | Disposición | Detalle |
diff --git a/src/uix/eidos/components/code-block/code-block.css b/src/uix/eidos/components/code-block/code-block.css
index 1dbb6c104..432e11958 100644
--- a/src/uix/eidos/components/code-block/code-block.css
+++ b/src/uix/eidos/components/code-block/code-block.css
@@ -11,19 +11,19 @@
position: relative;
display: flex;
flex-direction: column;
- border-radius: var(--radius-md);
+ border-radius: var(--code-block-radius);
overflow: hidden;
- color: var(--color-content-primary);
+ color: var(--code-block-fg);
}
[data-code-block][data-variant='surface'] {
- background: var(--color-neutral-element);
- border: var(--border-width) solid var(--color-neutral-border);
+ background: var(--code-block-surface-bg);
+ border: var(--code-block-border-width) solid var(--code-block-border);
}
[data-code-block][data-variant='outline'] {
background: transparent;
- border: var(--border-width) solid var(--color-neutral-border);
+ border: var(--code-block-border-width) solid var(--code-block-border);
}
[data-code-block][data-variant='ghost'] {
@@ -34,35 +34,35 @@
[data-code-block-header] {
display: flex;
align-items: center;
- gap: var(--space-2);
- padding-inline: var(--space-3);
- padding-block: var(--space-1);
- border-bottom: var(--border-width) solid var(--color-neutral-border);
- background: var(--color-neutral-track);
+ gap: var(--code-block-header-gap);
+ padding-inline: var(--code-block-header-padding-inline);
+ padding-block: var(--code-block-header-padding-block);
+ border-bottom: var(--code-block-border-width) solid var(--code-block-border);
+ background: var(--code-block-header-bg);
}
[data-code-block][data-variant='ghost'] [data-code-block-header] {
background: transparent;
- border-bottom-color: var(--color-neutral-border);
+ border-bottom-color: var(--code-block-border);
}
[data-code-block-title] {
flex: 1 1 auto;
min-inline-size: 0;
- font-family: var(--font-family-mono);
- font-size: var(--font-size-xs);
- color: var(--color-content-secondary);
+ font-family: var(--code-block-title-font-family);
+ font-size: var(--code-block-title-font-size);
+ color: var(--code-block-title-fg);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
[data-code-block-lang] {
- font-family: var(--font-family-mono);
- font-size: var(--font-size-xxs);
- color: var(--color-content-muted);
+ font-family: var(--code-block-lang-font-family);
+ font-size: var(--code-block-lang-font-size);
+ color: var(--code-block-lang-fg);
text-transform: lowercase;
- letter-spacing: var(--tracking-caps);
+ letter-spacing: var(--code-block-lang-letter-spacing);
white-space: nowrap;
}
@@ -79,19 +79,24 @@
[data-code-block-copy] {
display: inline-flex;
align-items: center;
- gap: var(--space-1);
+ gap: var(--code-block-copy-gap);
flex: 0 0 auto;
- font-size: var(--font-size-xs);
- color: var(--color-affirm-text);
+ font-size: var(--code-block-copy-font-size);
+ color: var(--code-block-copy-fg);
}
[data-code-block-pre] {
margin: 0;
- padding: var(--space-3);
+ padding: var(--code-block-pre-padding);
overflow-x: auto;
tab-size: 2;
}
+/* The code node consumes the `--style-code-*` TYPOGRAPHIC LAYER, and coins
+ * nothing of its own: a layer's vocabulary is not re-declared by its consumer
+ * (shared-layer rule 2). `--_code-block-*` in front is the per-INSTANCE escape
+ * hatch the wrapper writes from the `size` prop — the named style is the
+ * theming surface, exactly the D-TH.2-b shape. */
[data-code-block-code] {
display: block;
font-family: var(--style-code-font-family);
diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css
index b03b0360a..de089d280 100644
--- a/src/uix/eidos/generated/base.css
+++ b/src/uix/eidos/generated/base.css
@@ -4000,6 +4000,26 @@
--splitter-grip-cross: 2px;
--splitter-transition-duration: var(--duration-fast);
--splitter-transition-ease: var(--ease-default);
+ --code-block-radius: var(--radius-md);
+ --code-block-fg: var(--color-content-primary);
+ --code-block-border-width: var(--border-width);
+ --code-block-border: var(--color-neutral-border);
+ --code-block-surface-bg: var(--color-neutral-element);
+ --code-block-header-gap: var(--space-2);
+ --code-block-header-padding-inline: var(--space-3);
+ --code-block-header-padding-block: var(--space-1);
+ --code-block-header-bg: var(--color-neutral-track);
+ --code-block-title-font-family: var(--font-family-mono);
+ --code-block-title-font-size: var(--size-xs-font-size);
+ --code-block-title-fg: var(--color-content-secondary);
+ --code-block-lang-font-family: var(--font-family-mono);
+ --code-block-lang-font-size: var(--size-xxs-font-size);
+ --code-block-lang-fg: var(--color-content-muted);
+ --code-block-lang-letter-spacing: var(--tracking-caps);
+ --code-block-copy-gap: var(--space-1);
+ --code-block-copy-font-size: var(--size-xs-font-size);
+ --code-block-copy-fg: var(--color-affirm-text);
+ --code-block-pre-padding: var(--space-3);
--combobox-content-z: var(--z-index-overlay-floating);
--combobox-control-height-xs: var(--size-xs-control-height);
--combobox-control-height-sm: var(--size-sm-control-height);
diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts
index d452db446..60a4b8219 100644
--- a/src/uix/eidos/lib/recipes/base.ts
+++ b/src/uix/eidos/lib/recipes/base.ts
@@ -6378,6 +6378,42 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// floating layer mirrors this computed z onto the positioner wrapper). The
// band is deliberately NOT the global `--z-index-*` ladder — see
// STATIC_Z_INDEX_OVERLAY.
+ // CodeBlock. The `[data-code-block-code]` node is deliberately ABSENT from
+ // this contract: it consumes the `--style-code-*` typographic layer (with the
+ // wrapper's per-instance escape hatch `--_code-block-font-size` in front),
+ // and a layer's vocabulary is not re-coined by its consumer (shared-layer
+ // rule 2, the D-TH.2-b reasoning). Everything here is the SHELL around it.
+ 'code-block': {
+ radius: 'var(--radius-md)',
+ fg: 'var(--color-content-primary)',
+ // Shared by the surface/outline borders and the header's bottom rule.
+ // Ghost's `transparent` / `border: 0` are variant IDENTITY, not knobs.
+ 'border-width': 'var(--border-width)',
+ border: 'var(--color-neutral-border)',
+ 'surface-bg': 'var(--color-neutral-element)',
+
+ // Header strip.
+ 'header-gap': 'var(--space-2)',
+ 'header-padding-inline': 'var(--space-3)',
+ 'header-padding-block': 'var(--space-1)',
+ 'header-bg': 'var(--color-neutral-track)',
+ 'title-font-family': 'var(--font-family-mono)',
+ 'title-font-size': 'var(--size-xs-font-size)',
+ 'title-fg': 'var(--color-content-secondary)',
+ 'lang-font-family': 'var(--font-family-mono)',
+ 'lang-font-size': 'var(--size-xxs-font-size)',
+ 'lang-fg': 'var(--color-content-muted)',
+ 'lang-letter-spacing': 'var(--tracking-caps)',
+
+ // Copy slot. `copy-fg` tints the transient "Copied" INDICATOR, not the
+ // trigger: the trigger's chrome comes from button.css.
+ 'copy-gap': 'var(--space-1)',
+ 'copy-font-size': 'var(--size-xs-font-size)',
+ 'copy-fg': 'var(--color-affirm-text)',
+
+ // The gutter around the code.
+ 'pre-padding': 'var(--space-3)'
+ },
combobox: {
'content-z': 'var(--z-index-overlay-floating)',
// ── Size axis (theme-reach 2026-08-20) ───────────────────────────────
diff --git a/web/routes/uix/components/code-block/+page.svelte b/web/routes/uix/components/code-block/+page.svelte
index 3857e4142..2eea8f352 100644
--- a/web/routes/uix/components/code-block/+page.svelte
+++ b/web/routes/uix/components/code-block/+page.svelte
@@ -6,8 +6,9 @@
} from '$uix/eidos/components/code-block';
import { compileMorfo } from '$uix/morfo';
import { codeBlockMorfo } from '@/uix/morfo/components/code-block';
+ import TokensPanel from '../../lib/TokensPanel.svelte';
- type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
+ type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
let tab = $state('live');
let trace = $state([]);
@@ -147,6 +148,9 @@
+
@@ -350,6 +354,10 @@
{/if}
+ {#if tab === 'tokens'}
+
+ {/if}
+
{#if tab === 'a11y'}
Accessibility