uix(code-block): temable — 0 % → 81 %, y la demo que no enseña el componente

20 claves en una entrada NUEVA de `recipes/base.ts` (no tenía). Censo 0 % →
81 %. Global 46 % → 47 %, sin contrato 46 → 45.

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.

1. **El nodo del código NO acuña nada, y la §4 pedía que lo hiciera.**
   `[data-code-block-code]` consume la capa tipográfica `--style-code-*` en sus
   cinco declaraciones, y las dos que pasan por privado usan la forma canónica
   de la escotilla por instancia
   (`var(--_code-block-font-size, var(--style-code-font-size))`, escrita por el
   wrapper desde la prop `size` y sólo si llega). Es el patrón que D-TH.2-b
   fijó para heading/text/code. Acuñar `code-font-family` y compañía sería el
   vocabulario paralelo que la regla 2 de capas prohíbe. Todo lo que entra al
   contrato es el CASCO alrededor.
2. **Los tres `font-size` iban al primitivo crudo, y lo cazó un guard.** La
   propuesta copia el valor del CSS verbatim, y ese valor ya violaba
   `recipe-css-contract` («recipes consume the size bundle, not the raw
   size-coordinate primitives»): `title/lang/copy-font-size` corregidos a
   `var(--size-{k}-font-size)`. Los alias son 1:1 → 0 diffs tras aplicarlo.
   Aviso general: cuando el CSS de partida ya incumple el contrato, la
   propuesta generada hereda el incumplimiento.
3. Shorthand partidos: los dos `border` y el `border-bottom` de la cabecera
   comparten anchura y color → `border-width` + `border`; el `padding` del
   `<pre>` → `pre-padding`. Los `transparent` de outline/ghost y el `border: 0`
   de ghost son identidad de variante.

El 19 % que falta son esas cinco de la capa, que el censo cuenta como `global`
porque `code-block` no está en `TYPOGRAPHIC_PRIMITIVES` — es un compuesto que
CONTIENE un primitivo tipográfico, no uno de ellos. Extender la clase `system`
del censo a las capas sigue pendiente de tu firma; la métrica penaliza hacer lo
correcto y no se fuerza de oficio.

DOS HALLAZGOS QUE NO SE ARREGLAN AQUÍ (→ next-features §13):

- **La demo de `code-block` no enseña el `code-block`.** `[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. Siete propiedades del `<pre>` las pinta el harness y no la receta:
  padding (16px 20px contra 12px), border (1px contra ninguno), radius (8px
  contra 0), background (gris contra transparente), font-family, font-size
  (13 contra 14) y line-height (18.2 contra 20.3). El fondo, el borde y el
  radio que se ven ahí son del sitio. Por eso `pre-padding` lee muerto en el
  guard, adjudicado con esa medición. Acotar la regla toca el CSS del SITIO,
  que sirve a ~162 páginas: decisión aparte.
- **El guard de claves duplicadas del handoff inspeccionaba el VACÍO.** El
  comando documentado usa `grep -oE "^\t…"`, y `grep -E` lee `\t` como una `t`
  literal: 0 coincidencias sobre 130 bloques reales, así que salía «vacío = OK»
  sin mirar nada — el antipatrón que el proyecto tiene registrado, dentro del
  propio handoff. Comprobado con un contador fiable: cero duplicados en el
  árbol, así que no hay daño en los commits que confiaron en él. CONTINUE
  queda corregido con una versión que CUENTA los bloques y avisa de que «0
  bloques» significa guard roto, no fichero limpio.

Artefactos y gates:
- Sonda antes/después = 0 diffs en 1.305 valores × 7 estados (dos veces: tras
  la tokenización y tras la corrección del bundle). Determinismo verificado.
- R-5.4: 19/20; `pre-padding` adjudicado con las siete propiedades medidas.
- censo --only 81 % · component:audit PASS · eidos-lint invalid 0, class-hooks
  0 · suite eidos de vuelta al único rojo conocido (skin-media-player) tras
  corregir el bundle · rtl:check 0 · docs:check 0 · check por fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (20 claves, en vivo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent dd188f1255
commit 354d166df1

@ -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 | – |

@ -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 `⚠`.
<!-- veredicto:start -->
_(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>` →
`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.
<!-- veredicto:end -->

@ -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 `<pre>` 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.

@ -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.

@ -114,6 +114,10 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'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 <pre>. 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))'

@ -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 <url>`): 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 `<pre>`
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 `<pre>` 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 |

@ -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);

@ -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);

@ -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 <pre> gutter around the code.
'pre-padding': 'var(--space-3)'
},
combobox: {
'content-z': 'var(--z-index-overlay-floating)',
// ── Size axis (theme-reach 2026-08-20) ───────────────────────────────

@ -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<Tab>('live');
let trace = $state<TraceEntry[]>([]);
@ -147,6 +148,9 @@
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}
>Recipe</button
>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>20</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -350,6 +354,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="code-block" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.