uix(kbd): temable — 0 % → 86 %, y 13/13 en el guard sin una sola excepción

13 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 86 %.

1. **`font-family` NO se acuña**: lee `var(--style-code-font-family)`, la capa
   tipográfica, y un consumidor no re-declara el vocabulario de su capa —
   precedente inmediato, `code-block`. El `font-size` SÍ entra, y la diferencia
   está medida: su fallback era un PRIMITIVO, no la capa. `--_kbd-font-size` es
   la escotilla por instancia que el wrapper escribe desde la prop `size`
   (`kbd.svelte:37`), y el fallback pasa a ser el público.
2. **El padding y el min-width son em-relativos a propósito** — una tecla crece
   con su letra —, así que son knobs con valor `em`, no literales pendientes de
   corregir. El `line-height: 1` sí es identidad: una tecla no tiene
   interlineado.
3. **El relieve de tecla física es de la variante `surface`**: borde inferior
   más grueso + luz interior. Dos knobs propios, no adorno del borde general;
   outline y ghost son planas por identidad.

Artefactos y gates:
- Sonda antes/después = 0 diffs en 522 valores × 7 estados. Determinismo
  verificado.
- R-5.4: **13/13**, sin una sola entrada en el ledger — el primero de la tanda
  que no necesita adjudicar nada.
- censo --only 86 % · component:audit PASS · eidos-lint invalid 0, class-hooks
  0 · suite eidos sin rojos nuevos · rtl:check 0 · docs:check 0 · check por
  fichero limpio.
- README con su «Talla y tema» y tab `Tokens` en la demo (13 claves, en vivo).

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

@ -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**: **48%** — 2265 de 4676 knobs pasan por un token público del componente
- **Reparto**: público 2265 · privado 636 · global 1198 · literal 577 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 41 · **alcance < 20 %**: 34 · **alcance 100 %**: 8 · **con eje `size`**: 62
- **Alcance global**: **49%** — 2277 de 4676 knobs pasan por un token público del componente
- **Reparto**: público 2277 · privado 635 · global 1190 · literal 574 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 40 · **alcance < 20 %**: 33 · **alcance 100 %**: 8 · **con eje `size`**: 62
## Cómo se lee
@ -91,12 +91,12 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma
| 19 | [timeline](./timeline.md) | 24 | 51% | 49 | 45 |
| 20 | [avatar](./avatar.md) | 23 | 36% | 36 | 0 |
## Sin ningún token público propio (41)
## Sin ningún token público propio (40)
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) · [`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) · [`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) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.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) · [`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) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.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) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.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 | – |
| [kbd](./kbd.md) | 0% | 14 | 0 | 1 | 9 | 4 | 0 | 0 | – |
| [link](./link.md) | 0% | 14 | 0 | 4 | 2 | 5 | 3 | 0 | – |
| [code](./code.md) | 0% | 13 | 0 | 0 | 3 | 4 | 6 | 0 | – |
| [virtual-list](./virtual-list.md) | 0% | 13 | 0 | 2 | 6 | 4 | 1 | 0 | y |
@ -229,6 +228,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [text-circular](./text-circular.md) | 83% | 6 | 5 | 0 | 1 | 0 | 0 | 0 | – |
| [calendar](./calendar.md) | 84% | 71 | 57 | 7 | 0 | 4 | 3 | 74 | – |
| [grid-list](./grid-list.md) | 85% | 31 | 22 | 2 | 1 | 1 | 5 | 38 | – |
| [kbd](./kbd.md) | 86% | 14 | 12 | 0 | 1 | 1 | 0 | 13 | – |
| [table](./table.md) | 86% | 46 | 37 | 3 | 0 | 3 | 3 | 45 | – |
| [media-player](./media-player.md) | 88% | 49 | 43 | 0 | 0 | 6 | 0 | 61 | – |
| [tabs](./tabs.md) | 88% | 76 | 66 | 6 | 2 | 1 | 1 | 79 | y |

@ -5,41 +5,28 @@
> 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 14 knobs por token público
- **Knobs de apariencia**: 14 — público 0 · privado 1 · global 9 · literal 4 · sistema 0 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`**
- **Medido**: 2026-08-21 · **Alcance**: **86%** — 12 de 14 knobs por token público
- **Knobs de apariencia**: 14 — público 12 · privado 0 · global 1 · literal 1 · sistema 0 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 13 pública(s) — `font-size`, `font-weight`, `letter-spacing`, `fg`, `radius`, `padding-inline`, `padding-block`, `min-width`, `border-width`, `border`, `surface-bg`, `surface-border-block-end-width`, `surface-shadow`
- **Eje `size`**: no · **ficheros**: `kbd.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (9)
### 1.1 Directo a primitivo global (1)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `kbd.css:16` | `[data-kbd]` | `font-family` | `var(--style-code-font-family)` |
| 2 | `kbd.css:18` | `[data-kbd]` | `font-weight` | `var(--font-weight-medium)` |
| 3 | `kbd.css:20` | `[data-kbd]` | `color` | `var(--color-content-primary)` |
| 4 | `kbd.css:23` | `[data-kbd]` | `border-radius` | `var(--radius-sm)` |
| 5 | `kbd.css:30` | `[data-kbd]` | `letter-spacing` | `var(--tracking-wide)` |
| 6 | `kbd.css:34` | `[data-kbd][data-variant='surface']` | `background` | `var(--color-neutral-element)` |
| 7 | `kbd.css:35` | `[data-kbd][data-variant='surface']` | `border` | `var(--border-width) solid var(--color-neutral-border)` |
| 8 | `kbd.css:37` | `[data-kbd][data-variant='surface']` | `box-shadow` | `0 1px 0 var(--color-neutral-border) inset` |
| 9 | `kbd.css:42` | `[data-kbd][data-variant='outline']` | `border` | `var(--border-width) solid var(--color-neutral-border)` |
### 1.2 A través de un privado (1)
### 1.2 A través de un privado (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `kbd.css:17` | `[data-kbd]` | `font-size` | `var(--_kbd-font-size, var(--font-size-xs))` |
_Ninguno._
### 1.3 Literales (4)
### 1.3 Literales (1)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `kbd.css:19` | `[data-kbd]` | `line-height` | `1` |
| 2 | `kbd.css:24` | `[data-kbd]` | `padding-inline` | `0.4em` |
| 3 | `kbd.css:25` | `[data-kbd]` | `padding-block` | `0.18em` |
| 4 | `kbd.css:26` | `[data-kbd]` | `min-inline-size` | `1.5em` |
## 2. Sistema transversal (0) — informativo, fuera del ratio
@ -55,7 +42,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` (10)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1)
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
@ -66,21 +53,10 @@ no distingue lo que debería — se marca `⚠`.
| token (`--kbd-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `font-family` | `root` | `var(--style-code-font-family)` | 1 |
| `font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `fg` | `root` | `var(--color-content-primary)` | 1 |
| `radius` | `root` | `var(--radius-sm)` | 1 |
| `padding-inline` | `root` | `0.4em` | 1 |
| `padding-block` | `root` | `0.18em` | 1 |
| `width` | `root` | `1.5em` | 1 |
| `letter-spacing` | `root` | `var(--tracking-wide)` | 1 |
| `surface-bg` | `root` | `var(--color-neutral-element)` | 1 |
| `surface-shadow` | `root` | `0 1px 0 var(--color-neutral-border) inset` | 1 |
### 4.2 Sin nombre mecánico (4)
- **⚠ 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** — 1: `font-size`.
### 4.2 Sin nombre mecánico (1)
- **⚠ 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** — 1: `line-height`.
- **⚠ 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`.
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -94,6 +70,23 @@ 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 = 0 diffs en 522 valores · 7 estados).
Componente pequeño y limpio; dos decisiones:
1. **`font-family` NO se acuña**: lee `var(--style-code-font-family)`, la capa
tipográfica, y un consumidor no re-declara el vocabulario de su capa
(precedente inmediato: `code-block`, mismo día). El `--_kbd-font-size` sí
entra, porque su FALLBACK es un primitivo, no la capa: el privado es la
escotilla por instancia que el wrapper escribe desde la prop `size`
(`kbd.svelte:37`) y el fallback pasa a ser el público.
2. **`padding-inline: 0.4em`, `padding-block: 0.18em` y `min-inline-size: 1.5em`
son em-relativos a propósito** — la tecla crece con su letra —, así que son
knobs con valor `em`, no literales a corregir. El `line-height: 1` sí es
identidad (una tecla no tiene interlineado) y se queda fuera.
3. La variante `surface` añade el relieve de tecla física: borde inferior más
grueso + sombra interior. Son dos knobs propios
(`surface-border-block-end-width`, `surface-shadow`), no adorno del borde
general. Los `transparent` / `border: 0` de outline y ghost son identidad de
variante.
<!-- veredicto:end -->

@ -59,6 +59,32 @@ Kbd declara 0 eventos. Visual-only — el shortcut event lives on
the consumer (a Toolbar button, a global key handler), no en el
primitive de label.
## Talla y tema
**Guard R-5.4** (`npm run theming:sentinel -- kbd <url>`): **13/13** tokens
mueven un computed en vivo — sin una sola excepción en el ledger. Corrido
2026-08-21.
Contrato en `lib/recipes/base.ts > kbd` (13 claves, entrada NUEVA). Alcance del
censo: **86 %** (era 0 %).
**`font-family` no se acuña**: lee `var(--style-code-font-family)`, la capa
tipográfica, y un consumidor no re-declara el vocabulario de su capa (mismo
criterio que `code-block`). El `font-size` sí entra, porque su FALLBACK era un
primitivo y no la capa: `--_kbd-font-size` es la escotilla por instancia que el
wrapper escribe desde la prop `size`, y el fallback pasa a ser el público.
**El padding y el min-width son em-relativos a propósito** — una tecla crece
con su letra —, así que son knobs con valor `em`, no literales pendientes de
corrección. El `line-height: 1` sí es identidad (una tecla no tiene
interlineado) y queda fuera del contrato, igual que los `transparent` /
`border: 0` de las variantes outline y ghost.
**El relieve de tecla física es de la variante `surface`**: borde inferior más
grueso (`surface-border-block-end-width`) y luz interior (`surface-shadow`).
Son knobs propios, no adorno del borde general — outline y ghost son planas por
identidad.
## Gaps
| Gap | Disposición | Detalle |

@ -14,32 +14,32 @@
align-items: center;
justify-content: center;
font-family: var(--style-code-font-family);
font-size: var(--_kbd-font-size, var(--font-size-xs));
font-weight: var(--font-weight-medium);
font-size: var(--_kbd-font-size, var(--kbd-font-size));
font-weight: var(--kbd-font-weight);
line-height: 1;
color: var(--color-content-primary);
color: var(--kbd-fg);
background: transparent;
border: 0;
border-radius: var(--radius-sm);
padding-inline: 0.4em;
padding-block: 0.18em;
min-inline-size: 1.5em;
border-radius: var(--kbd-radius);
padding-inline: var(--kbd-padding-inline);
padding-block: var(--kbd-padding-block);
min-inline-size: var(--kbd-min-width);
vertical-align: baseline;
user-select: none;
white-space: nowrap;
letter-spacing: var(--tracking-wide);
letter-spacing: var(--kbd-letter-spacing);
}
[data-kbd][data-variant='surface'] {
background: var(--color-neutral-element);
border: var(--border-width) solid var(--color-neutral-border);
border-bottom-width: var(--border-width-medium);
box-shadow: 0 1px 0 var(--color-neutral-border) inset;
background: var(--kbd-surface-bg);
border: var(--kbd-border-width) solid var(--kbd-border);
border-bottom-width: var(--kbd-surface-border-block-end-width);
box-shadow: var(--kbd-surface-shadow);
}
[data-kbd][data-variant='outline'] {
background: transparent;
border: var(--border-width) solid var(--color-neutral-border);
border: var(--kbd-border-width) solid var(--kbd-border);
}
[data-kbd][data-variant='ghost'] {

@ -4016,6 +4016,19 @@
--splitter-grip-cross: 2px;
--splitter-transition-duration: var(--duration-fast);
--splitter-transition-ease: var(--ease-default);
--kbd-font-size: var(--size-xs-font-size);
--kbd-font-weight: var(--font-weight-medium);
--kbd-letter-spacing: var(--tracking-wide);
--kbd-fg: var(--color-content-primary);
--kbd-radius: var(--radius-sm);
--kbd-padding-inline: 0.4em;
--kbd-padding-block: 0.18em;
--kbd-min-width: 1.5em;
--kbd-border-width: var(--border-width);
--kbd-border: var(--color-neutral-border);
--kbd-surface-bg: var(--color-neutral-element);
--kbd-surface-border-block-end-width: var(--border-width-medium);
--kbd-surface-shadow: 0 1px 0 var(--color-neutral-border) inset;
--link-preview-padding-block-xs: var(--space-2);
--link-preview-padding-block-sm: var(--space-2);
--link-preview-padding-block-md: var(--space-3);

@ -6434,6 +6434,28 @@ 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.
// Kbd. `font-family` is NOT here: it reads `--style-code-font-family`, the
// typographic layer, and a consumer does not re-coin its layer's vocabulary
// (same call as code-block). The em-relative padding and min-width ARE knobs:
// a key cap grows with its letter, on purpose.
kbd: {
'font-size': 'var(--size-xs-font-size)',
'font-weight': 'var(--font-weight-medium)',
'letter-spacing': 'var(--tracking-wide)',
fg: 'var(--color-content-primary)',
radius: 'var(--radius-sm)',
'padding-inline': '0.4em',
'padding-block': '0.18em',
'min-width': '1.5em',
// Shared by the surface and outline variants.
'border-width': 'var(--border-width)',
border: 'var(--color-neutral-border)',
'surface-bg': 'var(--color-neutral-element)',
// The physical key-cap relief: a heavier bottom edge plus an inner
// highlight. Surface only — outline and ghost are flat by identity.
'surface-border-block-end-width': 'var(--border-width-medium)',
'surface-shadow': '0 1px 0 var(--color-neutral-border) inset'
},
// LinkPreview's panel is PORTALED and its `data-size` sits on the CONTENT,
// not on the trigger — hence `parts: ['content']` on every resolved name; a
// cascade on the host would never reach it. Size moves PHYSICAL dimensions

@ -2,8 +2,9 @@
import { Kbd, type KbdVariant, type KbdSize } from '$uix/eidos/components/kbd';
import { compileMorfo } from '$uix/morfo';
import { kbdMorfo } from '@/uix/morfo/components/kbd';
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[]>([]);
@ -133,6 +134,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>13</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -377,6 +381,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="kbd" 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.