uix(card-group): temable — 0 % → 90 %, y dos privados abreviados que mueren

20 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 90 %, cero
globales y cero privados. Global 47 %, sin contrato 45 → 44.

Es un CASCO de composición y su propia cabecera ya lo decía: compone
ToggleGroup, Collapsible, Card y Button, y la receta posee «ONLY the genuinely
CardGroup-level visual». Eso acota el contrato antes de nombrar nada — el cromo
de las tarjetas es de Card, el del título de disclosure es de Button.

1. **`--_cg-pad` y `--_cg-card-radius` eran privados ABREVIADOS** (§6 r5 los
   prohíbe: `cg` por `card-group`), el mismo defecto que los `--_mp-*` de
   media-player. Pasar a públicos con el nombre completo es a la vez el
   renombrado que la regla pedía y la tokenización. La §3 de la ficha decía
   «la receta no declara privados propios»: los declaraba en su bloque raíz
   (`card-group.css:15-16`), y el escáner no los veía por buscarlos con el
   prefijo completo del componente.
2. **`--shape-outer-radius` NO se acuña: se CALCULA.** La receta lo compone
   como `card-radius + padding` para que las tarjetas aniden concéntricamente
   (§30). Un tema mueve las dos coordenadas y el radio exterior se recompone
   solo; el censo ya lo clasifica como sistema.
3. **El eje `size` es sm|md|lg, no las cinco tallas**
   (`Extract<Size, 'sm' | 'md' | 'lg'>`): seis coordenadas por talla, no diez.
   Escribir xs/xl habría sido vocabulario muerto.
4. Los tres `padding` son shorthand con una parte fija y otra variable; lo que
   varía y no es el inset del grupo es el hueco inferior →
   `title-padding-block-end` y `description-padding-block-end`.
5. El chevron es geometría propia (dos bordes girados 45°): `chevron-size`
   cubre las dos dimensiones y `chevron-width` el trazo. Su `currentColor`
   hereda del título a propósito y no es knob.
6. Los `font-size` van al BUNDLE (`--size-{k}-font-size`, alias 1:1) y no al
   primitivo crudo — misma trampa que cayó en code-block: la propuesta copia el
   CSS verbatim y hereda el incumplimiento cuando el valor de partida ya
   violaba `recipe-css-contract`. Aquí se aplicó de entrada.

Artefactos y gates:
- Sonda antes/después = 0 diffs en 2.581 valores × 7 estados. Determinismo
  verificado: dos corridas, 0 diffs.
- R-5.4: 15/20. Los cinco mudos son los del encabezado ESTÁTICO, que la demo no
  monta (0 nodos `[data-static]` contra 1 `[data-button]`, CONTADOS); forzado
  `data-static` alcanzan los cinco, y cada uno lleva su medición en el ledger.
  El hueco de demo queda registrado en next-features §13, misma clase que el
  `kind='month'|'year'` de date-range-picker.
- censo --only 90 % · component:audit PASS · eidos-lint invalid 0, class-hooks
  0 · suite eidos sin rojos nuevos (el conocido skin-media-player) · 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 354d166df1
commit c56d6544e6

@ -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**: **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
- **Alcance global**: **47%** — 2213 de 4678 knobs pasan por un token público del componente
- **Reparto**: público 2213 · privado 654 · global 1233 · literal 578 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 44 · **alcance < 20 %**: 38 · **alcance 100 %**: 7 · **con eje `size`**: 64
## 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 (45)
## Sin ningún token público propio (44)
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) · [`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) · [`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 | – |
| [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 |
| [link-preview](./link-preview.md) | 0% | 17 | 0 | 4 | 9 | 1 | 3 | 0 | y |
@ -238,6 +237,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [gradient-picker](./gradient-picker.md) | 88% | 27 | 23 | 0 | 2 | 1 | 1 | 25 | – |
| [chat-typing](./chat-typing.md) | 89% | 9 | 8 | 0 | 1 | 0 | 0 | 7 | – |
| [breadcrumb](./breadcrumb.md) | 90% | 23 | 18 | 2 | 0 | 0 | 3 | 28 | y |
| [card-group](./card-group.md) | 90% | 22 | 18 | 0 | 0 | 2 | 2 | 20 | – |
| [collapsible](./collapsible.md) | 90% | 12 | 9 | 0 | 1 | 0 | 2 | 11 | – |
| [tree-view](./tree-view.md) | 92% | 28 | 24 | 1 | 0 | 1 | 2 | 46 | – |
| [feed](./feed.md) | 94% | 33 | 30 | 0 | 1 | 1 | 1 | 50 | – |

@ -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 22 knobs por token público
- **Knobs de apariencia**: 24 — público 0 · privado 0 · global 18 · literal 4 · sistema 2 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`**
- **Eje `size`**: sí · **ficheros**: `card-group.css`
- **Medido**: 2026-08-21 · **Alcance**: **90%** — 18 de 20 knobs por token público
- **Knobs de apariencia**: 22 — público 18 · privado 0 · global 0 · literal 2 · sistema 2 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 20 pública(s) — `padding-sm`, `padding-md`, `padding-lg`, `grid-gap-sm`, `grid-gap-md`, `grid-gap-lg`, `padding`, `grid-gap`, `card-radius`, `title-font-weight`, `title-fg`, `title-font-family`, `title-font-size`, `title-line-height`, `title-padding-block-end`, `chevron-size`, `chevron-width`, `description-fg`, `description-font-size`, `description-padding-block-end`
- **Eje `size`**: no · **ficheros**: `card-group.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (18)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `card-group.css:38` | `[data-card-group-title][data-button]` | `padding-inline` | `var(--_cg-pad)` |
| 2 | `card-group.css:39` | `[data-card-group-title][data-button]` | `font-weight` | `var(--font-weight-semibold)` |
| 3 | `card-group.css:46` | `[data-card-group-title][data-static]` | `padding` | `var(--_cg-pad) var(--_cg-pad) var(--space-2)` |
| 4 | `card-group.css:47` | `[data-card-group-title][data-static]` | `color` | `var(--color-content-primary)` |
| 5 | `card-group.css:48` | `[data-card-group-title][data-static]` | `font-family` | `var(--style-label-font-family)` |
| 6 | `card-group.css:49` | `[data-card-group-title][data-static]` | `font-size` | `var(--font-size-md)` |
| 7 | `card-group.css:50` | `[data-card-group-title][data-static]` | `font-weight` | `var(--font-weight-semibold)` |
| 8 | `card-group.css:51` | `[data-card-group-title][data-static]` | `line-height` | `var(--leading-ui)` |
| 9 | `card-group.css:65` | `[data-card-group-title-chevron]` | `border-inline-end` | `var(--border-width-medium) solid currentColor` |
| 10 | `card-group.css:66` | `[data-card-group-title-chevron]` | `border-block-end` | `var(--border-width-medium) solid currentColor` |
| 11 | `card-group.css:79` | `[data-card-group-description]` | `padding` | `0 var(--_cg-pad) var(--space-2)` |
| 12 | `card-group.css:80` | `[data-card-group-description]` | `color` | `var(--color-content-muted)` |
| 13 | `card-group.css:81` | `[data-card-group-description]` | `font-size` | `var(--font-size-sm)` |
| 14 | `card-group.css:93` | `[data-card-group-grid]` | `gap` | `var(--space-3)` |
| 15 | `card-group.css:94` | `[data-card-group-grid]` | `padding` | `0 var(--_cg-pad) var(--_cg-pad)` |
| 16 | `card-group.css:100` | `[data-card-group] [data-card]` | `border-radius` | `var(--_cg-card-radius)` |
| 17 | `card-group.css:131` | `[data-card-group][data-size='sm'] [data-card-group-grid]` | `gap` | `var(--space-2)` |
| 18 | `card-group.css:138` | `[data-card-group][data-size='lg'] [data-card-group-grid]` | `gap` | `var(--space-4)` |
_Ninguno._
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (4)
### 1.3 Literales (2)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `card-group.css:20` | `[data-card-group]` | `inline-size` | `100%` |
| 2 | `card-group.css:61` | `[data-card-group-title-chevron]` | `inline-size` | `0.5em` |
| 3 | `card-group.css:62` | `[data-card-group-title-chevron]` | `block-size` | `0.5em` |
| 4 | `card-group.css:105` | `[data-card-group-item]` | `block-size` | `100%` |
| 1 | `card-group.css:21` | `[data-card-group]` | `inline-size` | `100%` |
| 2 | `card-group.css:107` | `[data-card-group-item]` | `block-size` | `100%` |
## 2. Sistema transversal (2) — informativo, fuera del ratio
@ -54,8 +33,8 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `card-group.css:21` | `[data-card-group]` | `border-radius` | `var(--shape-outer-radius)` |
| 2 | `card-group.css:69` | `[data-card-group-title-chevron]` | `opacity` | `var(--opacity-muted)` |
| 1 | `card-group.css:22` | `[data-card-group]` | `border-radius` | `var(--shape-outer-radius)` |
| 2 | `card-group.css:71` | `[data-card-group-title-chevron]` | `opacity` | `var(--opacity-muted)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
@ -63,9 +42,7 @@ _La receta no declara privados propios en su CSS._
## 4. Propuesta de corrección
- **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (14)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (0)
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
@ -75,27 +52,10 @@ no distingue lo que debería — se marca `⚠`.
| token (`--card-group-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `title-font-weight` | `root` | `var(--font-weight-semibold)` | 2 |
| `title-padding-inline` | `root` | `var(--_cg-pad)` | 1 |
| `title-fg` | `root` | `var(--color-content-primary)` | 1 |
| `title-font-family` | `root` | `var(--style-label-font-family)` | 1 |
| `title-font-size` | `root` | `var(--font-size-md)` | 1 |
| `title-line-height` | `root` | `var(--leading-ui)` | 1 |
| `title-chevron-width` | `root` | `0.5em` | 1 |
| `title-chevron-height` | `root` | `0.5em` | 1 |
| `description-fg` | `root` | `var(--color-content-muted)` | 1 |
| `description-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `grid-gap` | `root` | `var(--space-3)` | 1 |
| `radius` | `root` | `var(--_cg-card-radius)` | 1 |
| `grid-gap-sm` | `root` | `var(--space-2)` | 1 |
| `grid-gap-lg` | `root` | `var(--space-4)` | 1 |
### 4.2 Sin nombre mecánico (7)
### 4.2 Sin nombre mecánico (2)
- **⚠ decisión: `100%` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 2: `inline-size`, `block-size`.
- **⚠ 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)** — 3: `padding`.
- **⚠ decisión: `border-inline-end` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `border-inline-end`.
- **⚠ decisión: `border-block-end` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `border-block-end`.
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -109,6 +69,52 @@ 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 2.581
valores · 7 estados). Es un CASCO de composición —compone ToggleGroup,
Collapsible, Card y Button— y su propia cabecera lo dice: la receta posee
«ONLY the genuinely CardGroup-level visual». Eso acota el contrato antes de
nombrar nada: el cromo de las tarjetas es de `Card`, el del título es de
`Button`, y aquí sólo entra lo del grupo.
1. **`--_cg-pad` y `--_cg-card-radius` son privados ABREVIADOS** (§6 r5 los
prohíbe: `cg` por `card-group`) — el mismo defecto que los `--_mp-*` de
media-player y los `--gp-*` de gradient-picker. Los dos pasan a públicos con
el nombre completo (`padding`, `card-radius`), que es a la vez el renombrado
que la regla pide y la tokenización. **La §3 de esta ficha se equivoca** al
decir «la receta no declara privados propios»: los declara en su bloque raíz
(`card-group.css:15-16`); el escáner no los vio porque los busca con el
prefijo completo del componente.
2. **`--shape-outer-radius` NO se acuña.** Es un token del sistema de forma que
esta receta CALCULA (`calc(card-radius + padding)`) para que las tarjetas
aniden concéntricamente (§30). El censo ya lo clasifica como `system`, y así
se queda: un tema mueve las dos coordenadas y el radio exterior se recompone
solo.
3. **El eje `size` es sm/md/lg, no las cinco tallas**
(`CardGroupSize = Extract<Size, 'sm' | 'md' | 'lg'>`), así que las
coordenadas por talla son SEIS, no diez. Escribir xs/xl habría sido
vocabulario muerto.
4. **Los tres `padding` son shorthand con una parte fija y otra variable**:
`var(--_cg-pad) var(--_cg-pad) var(--space-2)` (título estático),
`0 var(--_cg-pad) var(--space-2)` (descripción) y
`0 var(--_cg-pad) var(--_cg-pad)` (grid). Lo que varía y no es el padding
del grupo es el hueco inferior → `title-padding-block-end` y
`description-padding-block-end`; el resto lo cubre `padding`.
5. **El chevron es geometría propia, no un icono compuesto**: dos bordes
girados 45°. `inline-size` y `block-size` son UN knob (`chevron-size`,
`0.5em`) y la anchura del trazo otro (`chevron-width`). El `currentColor` no
es knob: hereda del título a propósito.
6. **`title-font-weight` lo comparten las dos formas del título** (el botón de
disclosure y el encabezado estático), con el mismo valor. Un solo knob.
7. **Los `font-size` van al BUNDLE, no al primitivo** —
`var(--size-md-font-size)` y `var(--size-sm-font-size)`, alias 1:1— porque
`recipe-css-contract` lo exige y la propuesta generada copia el CSS
verbatim. Es la misma trampa que acaba de caer en `code-block`: cuando el
valor de partida ya incumple el contrato, la propuesta hereda el
incumplimiento.
8. **Fuera del contrato**: los dos `100%` (identidad de layout, D-TH.2) y el
`opacity: var(--opacity-muted)` del chevron (sistema).
Quedan **20 claves** y el alcance esperado ronda el 90 %: lo que no llega son
los dos `100%`.
<!-- veredicto:end -->

@ -659,6 +659,13 @@ 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`).
- **Hueco de demo: `card-group` no monta su encabezado ESTÁTICO.** La página
renderiza el título sólo en su forma de disclosure (`[data-button]`), así que
la mitad `[data-static]` del cromo del título —tinta, familia, tamaño,
interlineado y el hueco inferior— no se ve ni se mide: cinco tokens
adjudicados por eso. Misma clase que `date-range-picker` con
`kind='month'|'year'` (norma N-6).
Lo que `code-block` añadió (2026-08-21):
- **⚠ La demo de `code-block` NO ENSEÑA el `code-block`: el harness del sitio

@ -114,6 +114,18 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'sentinel-spinner-reduced-duration':
'behind prefers-reduced-motion; emulated reduce context → reaches (4s → 11.5s)'
},
'card-group': {
'title-fg':
'the STATIC heading form is not mounted in the demo (0 [data-static] nodes vs 1 [data-button], counted); forced → reaches (rgb(1,2,3))',
'title-font-family':
'static heading not mounted in the demo; forced → reaches (Instrument Sans → Zapfino)',
'title-font-size':
'static heading not mounted in the demo; forced → reaches (16 → 1234px)',
'title-line-height':
'static heading not mounted in the demo; forced → reaches (20px → 3.77em)',
'title-padding-block-end':
'static heading not mounted in the demo; forced → reaches (8 → 1234px)'
},
'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'

@ -169,6 +169,44 @@ Item (ToggleGroup.Item button when selectable).
`mode` enum + min/max trio. `whenFull` governs the at-cap strategy; the mode
is derived from the ceiling.
## Talla y tema
**Guard R-5.4** (`npm run theming:sentinel -- card-group <url>`): 15/20 tokens
mueven un computed en vivo; los cinco que callan son los del encabezado
ESTÁTICO, que la demo no monta (0 nodos `[data-static]` contra 1
`[data-button]`, contados), y están adjudicados uno a uno con su medición en
`scripts/theming-sentinel-exceptions.ts`. Corrido 2026-08-21.
Contrato en `lib/recipes/base.ts > card-group` (20 claves, entrada NUEVA).
Alcance del censo: **90 %** (era 0 %), con cero globales y cero privados.
**Es un CASCO de composición**, y eso acota el contrato antes de nombrar nada:
compone ToggleGroup, Collapsible, Card y Button, así que el cromo de las
tarjetas es de `Card`, el del título de disclosure es de `Button`, y aquí sólo
entra lo del grupo.
**El eje `size` es sm|md|lg**, no las cinco tallas
(`CardGroupSize = Extract<Size, 'sm' | 'md' | 'lg'>`): seis coordenadas, no
diez. Escribir xs/xl habría sido vocabulario muerto.
**`--shape-outer-radius` no es un knob: se CALCULA.** La receta lo compone como
`card-radius + padding` para que las tarjetas aniden concéntricamente (§30), y
el censo ya lo clasifica como sistema. Un tema mueve las dos coordenadas y el
radio exterior se recompone solo — por eso `card-radius` y `padding` son
públicos y el resultado no.
**Dos privados abreviados murieron aquí**: `--_cg-pad` y `--_cg-card-radius`
violaban §6 r5 (`cg` por `card-group`), el mismo defecto que los `--_mp-*` de
media-player. Pasar a públicos con el nombre completo es a la vez el renombrado
que la regla pedía y la tokenización. (La §3 de la ficha generada decía que la
receta no declaraba privados: los declaraba en su bloque raíz, y el escáner no
los vio por buscarlos con el prefijo completo.)
El chevron es geometría propia — dos bordes girados 45° —, así que
`chevron-size` cubre las dos dimensiones y `chevron-width` el trazo; su
`currentColor` hereda del título a propósito y no es knob. Los dos `100%` son
identidad de layout (D-TH.2) y el `opacity` del chevron es sistema.
## Gaps
- **`disabled` doesn't reach the toggle.** The props table promises "selection

@ -11,9 +11,10 @@
*/
[data-card-group] {
--_cg-pad: var(--space-4);
--_cg-card-radius: var(--radius-md);
--shape-outer-radius: calc(var(--_cg-card-radius) + var(--_cg-pad));
/* The outer radius is COMPUTED, not a knob: a theme moves `card-radius` and
`padding` and the concentric nesting (§30) recomposes itself. The two
per-size coordinates are emitted by the TSC on this host. */
--shape-outer-radius: calc(var(--card-group-card-radius) + var(--card-group-padding));
display: block;
box-sizing: border-box;
@ -30,25 +31,26 @@
/* ── Title (disclosure trigger = system Button, ghost) ───────────────────────
* Button owns ALL chrome (bg / border / vertical padding / font / cursor /
* hover / focus ring / press). The recipe keeps ONLY the header-specific
* layout: full-bleed horizontal padding aligned to the grid (`--_cg-pad`) and
* layout: full-bleed horizontal padding aligned to the grid (`--card-group-padding`) and
* the chevron pushed to the trailing edge via space-between. */
[data-card-group-title][data-button] {
justify-content: space-between;
padding-inline: var(--_cg-pad);
font-weight: var(--font-weight-semibold);
padding-inline: var(--card-group-padding);
font-weight: var(--card-group-title-font-weight);
text-align: start;
}
/* Static (non-toggleable) heading — no button, plain header type. */
[data-card-group-title][data-static] {
display: block;
padding: var(--_cg-pad) var(--_cg-pad) var(--space-2);
color: var(--color-content-primary);
font-family: var(--style-label-font-family);
font-size: var(--font-size-md);
font-weight: var(--font-weight-semibold);
line-height: var(--leading-ui);
padding: var(--card-group-padding) var(--card-group-padding)
var(--card-group-title-padding-block-end);
color: var(--card-group-title-fg);
font-family: var(--card-group-title-font-family);
font-size: var(--card-group-title-font-size);
font-weight: var(--card-group-title-font-weight);
line-height: var(--card-group-title-line-height);
}
[data-card-group-title] [data-card-group-title-text] {
@ -58,12 +60,12 @@
/* Chevron — down at rest, up when the Collapsible is open. */
[data-card-group-title-chevron] {
inline-size: 0.5em;
block-size: 0.5em;
inline-size: var(--card-group-chevron-size);
block-size: var(--card-group-chevron-size);
flex: 0 0 auto;
margin-block-start: -0.15em;
border-inline-end: var(--border-width-medium) solid currentColor;
border-block-end: var(--border-width-medium) solid currentColor;
border-inline-end: var(--card-group-chevron-width) solid currentColor;
border-block-end: var(--card-group-chevron-width) solid currentColor;
transform: rotate(45deg);
transition: transform var(--duration-fast) var(--ease-out);
opacity: var(--opacity-muted);
@ -76,9 +78,9 @@
[data-card-group-description] {
margin: 0;
padding: 0 var(--_cg-pad) var(--space-2);
color: var(--color-content-muted);
font-size: var(--font-size-sm);
padding: 0 var(--card-group-padding) var(--card-group-description-padding-block-end);
color: var(--card-group-description-fg);
font-size: var(--card-group-description-font-size);
}
/* ── Content region + grid ─────────────────────────────────────────────────── */
@ -90,14 +92,14 @@
[data-card-group-grid] {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
gap: var(--space-3);
padding: 0 var(--_cg-pad) var(--_cg-pad);
gap: var(--card-group-grid-gap);
padding: 0 var(--card-group-padding) var(--card-group-padding);
}
/* ── Concentric cards (§30: inner = outer − inset) ─────────────────────────── */
[data-card-group] [data-card] {
border-radius: var(--_cg-card-radius);
border-radius: var(--card-group-card-radius);
}
/* The item stretches to fill its grid cell so cards in a row match height. */
@ -124,16 +126,6 @@
/* ── Size scale ────────────────────────────────────────────────────────────── */
[data-card-group][data-size='sm'] {
--_cg-pad: var(--space-3);
}
[data-card-group][data-size='sm'] [data-card-group-grid] {
gap: var(--space-2);
}
[data-card-group][data-size='lg'] {
--_cg-pad: var(--space-5);
}
[data-card-group][data-size='lg'] [data-card-group-grid] {
gap: var(--space-4);
}
/* Size coordinates (`padding`, `grid-gap`) are EMITTED by the TSC on the host
* from the recipe contract — sm | md | lg, the component's whole axis. Keeping
* the old `[data-size]` blocks here would shadow the new tokens. */

@ -4000,6 +4000,24 @@
--splitter-grip-cross: 2px;
--splitter-transition-duration: var(--duration-fast);
--splitter-transition-ease: var(--ease-default);
--card-group-padding-sm: var(--space-3);
--card-group-padding-md: var(--space-4);
--card-group-padding-lg: var(--space-5);
--card-group-grid-gap-sm: var(--space-2);
--card-group-grid-gap-md: var(--space-3);
--card-group-grid-gap-lg: var(--space-4);
--card-group-card-radius: var(--radius-md);
--card-group-title-font-weight: var(--font-weight-semibold);
--card-group-title-fg: var(--color-content-primary);
--card-group-title-font-family: var(--style-label-font-family);
--card-group-title-font-size: var(--size-md-font-size);
--card-group-title-line-height: var(--leading-ui);
--card-group-title-padding-block-end: var(--space-2);
--card-group-chevron-size: 0.5em;
--card-group-chevron-width: var(--border-width-medium);
--card-group-description-fg: var(--color-content-muted);
--card-group-description-font-size: var(--size-sm-font-size);
--card-group-description-padding-block-end: var(--space-2);
--code-block-radius: var(--radius-md);
--code-block-fg: var(--color-content-primary);
--code-block-border-width: var(--border-width);
@ -6773,6 +6791,21 @@
--_callout-palette-text: var(--palette-text, var(--callout-primary-text));
}
[data-card-group] {
--card-group-padding: var(--card-group-padding-md);
--card-group-grid-gap: var(--card-group-grid-gap-md);
}
[data-card-group][data-size='sm'] {
--card-group-padding: var(--card-group-padding-sm);
--card-group-grid-gap: var(--card-group-grid-gap-sm);
}
[data-card-group][data-size='lg'] {
--card-group-padding: var(--card-group-padding-lg);
--card-group-grid-gap: var(--card-group-grid-gap-lg);
}
[data-combobox-control], [data-combobox-input] {
--combobox-control-height: var(--combobox-control-height-md);
--combobox-control-font-size: var(--combobox-control-font-size-md);

@ -6378,6 +6378,59 @@ 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.
// CardGroup is a COMPOSITION SHELL (ToggleGroup + Collapsible + Card +
// Button): the cards' chrome belongs to Card's recipe and the title's to
// Button's. Only group-level visual lives here. The size axis is sm|md|lg —
// `CardGroupSize = Extract<Size, 'sm' | 'md' | 'lg'>` — so there are no
// xs/xl coordinates to write.
'card-group': {
'padding-sm': 'var(--space-3)',
'padding-md': 'var(--space-4)',
'padding-lg': 'var(--space-5)',
'grid-gap-sm': 'var(--space-2)',
'grid-gap-md': 'var(--space-3)',
'grid-gap-lg': 'var(--space-4)',
padding: {
declarations: [
{ value: 'var(--card-group-padding-md)', scope: 'host' },
{ value: 'var(--card-group-padding-sm)', scope: 'size:sm' },
{ value: 'var(--card-group-padding-lg)', scope: 'size:lg' }
]
},
'grid-gap': {
declarations: [
{ value: 'var(--card-group-grid-gap-md)', scope: 'host' },
{ value: 'var(--card-group-grid-gap-sm)', scope: 'size:sm' },
{ value: 'var(--card-group-grid-gap-lg)', scope: 'size:lg' }
]
},
// The card corner the group nests concentrically against. The recipe
// COMPUTES `--shape-outer-radius` from this plus `padding` (§30), so a
// theme moves the two coordinates and the outer radius recomposes itself —
// that system token is not re-coined here.
'card-radius': 'var(--radius-md)',
// Title — both forms (the disclosure Button and the static heading) share
// the weight. Everything else about the button is Button's recipe.
'title-font-weight': 'var(--font-weight-semibold)',
'title-fg': 'var(--color-content-primary)',
'title-font-family': 'var(--style-label-font-family)',
'title-font-size': 'var(--size-md-font-size)',
'title-line-height': 'var(--leading-ui)',
// The gap under the header, the one part of its padding shorthand that is
// not the group's own inset.
'title-padding-block-end': 'var(--space-2)',
// Chevron — own geometry (two borders rotated 45°), not a composed icon.
// Its colour is `currentColor` on purpose: it inherits from the title.
'chevron-size': '0.5em',
'chevron-width': 'var(--border-width-medium)',
'description-fg': 'var(--color-content-muted)',
'description-font-size': 'var(--size-sm-font-size)',
'description-padding-block-end': 'var(--space-2)'
},
// 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),

@ -6,8 +6,9 @@
} from '$uix/eidos/components/card-group';
import { Card } from '$uix/eidos/components/card';
import { UNLIMITED, isSelectable, type SelectionLimit } from '$libs/selection';
import TokensPanel from '../../lib/TokensPanel.svelte';
type Tab = 'live' | 'api' | 'composition' | 'a11y';
type Tab = 'live' | 'api' | 'composition' | 'tokens' | 'a11y';
let tab = $state<Tab>('live');
// ── Live state ──────────────────────────────────────────────────────────
@ -246,6 +247,9 @@
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API</button>
<button data-uix-tab data-active={tab === 'composition'} onclick={() => (tab = 'composition')}>Composition</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>
@ -432,6 +436,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="card-group" 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.