uix(card): el realce al puntero deja de ser rehén de `interactive`

El usuario preguntó por qué una tarjeta de plan de precios no responde al
puntero. Le dije que Card no exponía estado de puntero y era falso: el realce
existe desde siempre —los dos tokens están en la fundación, con reglas para
ghost, outline y solid, composición con el anillo de selección para que no
desaparezca al pasar el ratón, y anulación bajo reduced-motion—, pero todo
colgaba de un solo attr.

Y ese attr empaqueta cinco cosas: el elemento button, el evento commit-select, el
cursor, la escala de pulsación, el anillo de foco y la elevación. Una tarjeta que
ya contiene su propia llamada a la acción no puede tomar ese paquete: un button
dentro de otro es marcado inválido y las dos activaciones se pelean por el mismo
gesto. Así que la elevación era inalcanzable justo donde más se pide — planes de
precio, teasers de artículo, tarjetas de testimonio.

La elevación pasa a su propio eje. `interactive` lo implica, así que una tarjeta
clicable no cambia en nada. Lo que el eje NO trae, a propósito: ni cursor de
puntero —un puntero sobre algo que al pulsarlo no hace nada es una promesa
falsa—, ni escala de pulsación, ni anillo de foco. Eso es lo que un CONTROL le
debe al visitante.

Sin tokens nuevos: los dos que hacían falta ya estaban. Y el attr es de envoltorio
visual, no contrato — lo estampa el componente de eidos y lo guarda la mitad de
presencia, que gana su primera fila de card; no el morfo, porque declarar ahí lo
que sólo lee una capa es justo lo que la doctrina del 15 de agosto dejó dicho.
El eidos-lint lo clasifica como eidos-only, 0 inválidos.

Medido en navegador sobre estilos computados y posición real: apagado, el hover no
mueve nada ni pinta sombra —cero regresión—; puesto, translateY de −2px y sombra
de 18/48, con la tarjeta todavía div, cursor auto y su CTA intacto dentro, sin un
solo botón anidado en botón; bajo reduced-motion el desplazamiento se anula y la
sombra se queda, que es suprimir el movimiento sin perder la señal de profundidad;
y una Card interactive de verdad —cinco en la demo del canon— sigue siendo button,
con cursor de puntero y los mismos dos píxeles de elevación.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 44e4c9328e
commit d38c54fdf8

@ -33,18 +33,18 @@ Permite que un token cambie SU VALOR durante una señal:
```ts
recipes.toast = {
// Color base — scope 'host'
'bg': {
value: 'var(--color-surface-raised)',
scope: 'host'
},
// Override durante señal de announce — el toast cambia su bg
// mientras dura la señal perceptual
'bg-during-announce': {
value: 'var(--color-primary-element)',
scope: 'event:announce'
}
// Color base — scope 'host'
bg: {
value: 'var(--color-surface-raised)',
scope: 'host'
},
// Override durante señal de announce — el toast cambia su bg
// mientras dura la señal perceptual
'bg-during-announce': {
value: 'var(--color-primary-element)',
scope: 'event:announce'
}
};
```
@ -52,10 +52,10 @@ CSS generado:
```css
[data-toast] {
--toast-bg: var(--color-surface-raised);
--toast-bg: var(--color-surface-raised);
}
[data-toast][data-event='announce'] {
--toast-bg-during-announce: var(--color-primary-element);
--toast-bg-during-announce: var(--color-primary-element);
}
```
@ -63,10 +63,10 @@ El recipe usa el token apropiado:
```css
[data-toast] {
background: var(--toast-bg);
background: var(--toast-bg);
}
[data-toast][data-event='announce'] {
background: var(--toast-bg-during-announce);
background: var(--toast-bg-during-announce);
}
```
@ -83,8 +83,8 @@ Eidos lee `data-motion` (la pref global proyectada por `ActivePrefs`):
```css
[data-motion='reduce'] [data-event][data-event-phase='active'] {
animation-duration: 1ms;
transition-duration: 1ms;
animation-duration: 1ms;
transition-duration: 1ms;
}
```
@ -238,10 +238,10 @@ Radix Themes. Diseño completo en [`SCALING_RFC.md`](../../src/uix/eidos/SCALING
Son **dos ejes ortogonales** que se multiplican:
| Eje | Atributo | Qué mueve | Tipografía |
| --- | --- | --- | --- |
| **Densidad** | `data-density` (`compact` / `comfortable` / `spacious`) | ritmo de layout (`space`) + altura de controles (`control-height`) | **NO** — el cuerpo de texto queda fijo |
| **Scaling** | `data-scaling` (`90` / `95` / `100` / `105` / `110`) | **zoom global**: `space` + `control-height` + `font-size` + `icon-size` | **SÍ** — escala el cuerpo de texto |
| Eje | Atributo | Qué mueve | Tipografía |
| ------------ | ------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------- |
| **Densidad** | `data-density` (`compact` / `comfortable` / `spacious`) | ritmo de layout (`space`) + altura de controles (`control-height`) | **NO** — el cuerpo de texto queda fijo |
| **Scaling** | `data-scaling` (`90` / `95` / `100` / `105` / `110`) | **zoom global**: `space` + `control-height` + `font-size` + `icon-size` | **SÍ** — escala el cuerpo de texto |
Densidad = "más/menos aire entre cosas, controles más bajos, mismo
texto". Scaling = "agranda/encoge **todo** proporcionalmente", igual que
@ -308,10 +308,10 @@ Simétrica a `density`:
```ts
createActiveEidos({
scaling: '110', // estático
// o reactivo:
scalingSource: { get: () => prefs.scaling, onChange: (fn) => prefs.subscribe(fn) }
})
scaling: '110', // estático
// o reactivo:
scalingSource: { get: () => prefs.scaling, onChange: (fn) => prefs.subscribe(fn) }
});
```
`ActiveEidos` escribe `data-scaling` en el target junto a `data-theme` /
@ -413,11 +413,11 @@ de alias + **override por componente**. Lo único propio es que los **intents**
### 25.1 — Las tres capas
| Capa | Qué es | Cómo se define |
| --- | --- | --- |
| **Paleta** | librería de escalas de 12 pasos | `--scale-{name}-{step}` (+ alpha `--scale-{name}-a{step}`) · directamente usable · **diseñable por el tema** |
| **Roles** (jerarquía) | `primary` · `secondary` · `tertiary` | **alias explícito** a una escala (decisión de marca · obligatorio) |
| **Intents** | `neutral` + `affirm`/`fulfill`/`risk`/`threat`/`loss` | **auto-derivados** de la paleta por convención del libro · identidad = step 9 · slots derivan normal · override opcional |
| Capa | Qué es | Cómo se define |
| --------------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| **Paleta** | librería de escalas de 12 pasos | `--scale-{name}-{step}` (+ alpha `--scale-{name}-a{step}`) · directamente usable · **diseñable por el tema** |
| **Roles** (jerarquía) | `primary` · `secondary` · `tertiary` | **alias explícito** a una escala (decisión de marca · obligatorio) |
| **Intents** | `neutral` + `affirm`/`fulfill`/`risk`/`threat`/`loss` | **auto-derivados** de la paleta por convención del libro · identidad = step 9 · slots derivan normal · override opcional |
Los componentes consumen la capa semántica (`--color-{role}-{slot}`) y pueden
**override** su color a cualquier escala vía la prop `color` / `data-color`.
@ -441,10 +441,10 @@ Los componentes consumen la capa semántica (`--color-{role}-{slot}`) y pueden
dos formas para el mismo valor crearían ambigüedad sobre cuál es la canónica.
- **Diseñable**: la paleta la trae el tema (dominio del diseñador). El framework
envía una paleta por defecto de **33 escalas** (en `lib/themes/color-scales.ts`
+ `base.ts`). Muchas se sembraron desde Radix Colors —un buen punto de partida—
pero **la paleta es NUESTRA, sin perseguir paridad con nadie**. Un tema la
reemplaza/amplía; un color de marca se añade como **una escala** (autorada o
generada), nunca como un valor inline suelto.
- `base.ts`). Muchas se sembraron desde Radix Colors —un buen punto de partida—
pero **la paleta es NUESTRA, sin perseguir paridad con nadie**. Un tema la
reemplaza/amplía; un color de marca se añade como **una escala** (autorada o
generada), nunca como un valor inline suelto.
#### Regla de pertenencia — por qué 33 y no un número mágico
@ -479,14 +479,14 @@ azul, que "resiste nombres"), `chartreuse` (pega con `lime`).
La puerta 3 vale lo que el generador más flojo. Hay **tres** generadores —
autorado, `generatePalette` (paramétrico, `lib/generate-palette.ts`) y
`deriveScheme` (M3 runtime, `$color`)— y cualquiera puede escupir dos familias
confusables sin que una puerta de *entrada* lo frene (medido: `generatePalette` a
confusables sin que una puerta de _entrada_ lo frene (medido: `generatePalette` a
`tone +0.12` funde `yellow` = `lime`, ΔE **0.0**). Por eso la garantía es un **test
sobre la salida** — [`lib/palette-invariant.test.ts`](../../src/uix/eidos/lib/palette-invariant.test.ts) — que:
- corre sobre **los tres** generadores;
- usa dos suelos honestos: **sólido** (step 9) ≥ 0.02 (el confusable que importa,
el acento que usan los componentes) e **idéntico** ≥ 0.002 en cualquier step;
- chequea **varios steps** (3 · 9 · 11): tints y texto colisionan *peor* que el
- chequea **varios steps** (3 · 9 · 11): tints y texto colisionan _peor_ que el
sólido (`green`↔`jade` cae a **0.003** en el step 3), así que medir solo el 9
subestima la confusabilidad;
- **exceptúa `monochrome`**: colapsar la jerarquía a una tinta es intencional
@ -515,8 +515,8 @@ slots (p. ej. un primario monocromo con `slots: { contrast: '1' }`).
### 25.4 — Intents auto-derivados (convención del libro)
- Los 6 intents tienen color canónico definido en el libro *Diseñando lo que
ocurre*. La convención `INTENT → escala` vive en `CANONICAL_INTENT_SCALES`
- Los 6 intents tienen color canónico definido en el libro _Diseñando lo que
ocurre_. La convención `INTENT → escala` vive en `CANONICAL_INTENT_SCALES`
(`lib/config-types.ts`):
`neutral→gray · affirm→teal · fulfill→green · risk→amber · threat→red · loss→plum`.
- Un intent **omitido** del mapa de roles **auto-deriva** de la paleta por esa
@ -581,7 +581,7 @@ instancia. Equivalente a `<Button color="grass">` de Radix.
El RFC §21 proponía declarar un intent como un solo hex (`{ anchor }`) y derivar
los slots inline con `color-mix()`. Se **descartó**: Radix no lo hace (genera una
*escala* desde un hex y la aliasa), y con una **paleta rica** el problema que lo
_escala_ desde un hex y la aliasa), y con una **paleta rica** el problema que lo
motivaba (autorar 12 pasos a mano para `loss` → el bug de loss=azul) **desaparece
solo**: `loss` simplemente aliasa la escala `plum`, que ya existe en la paleta.
El modelo final es **paleta rica + alias / auto-derivación**, no ancla.
@ -603,27 +603,28 @@ clase. Un app re-tematiza desde UN color de marca con una llamada, sin tocar el
```ts
const result = eidos.applyColorScheme('#8e4ec6', {
variant: 'tonal', // 'tonal' | 'vibrant' | 'monochrome'
temper: 0.12, // cohesión de intents (mantiene hue)
overrides: { tertiary: '#3e63dd' } // fija un rol; el resto deriva del seed
})
eidos.clearColorScheme() // revierte a los primitives del tema
variant: 'tonal', // 'tonal' | 'vibrant' | 'monochrome'
temper: 0.12, // cohesión de intents (mantiene hue)
overrides: { tertiary: '#3e63dd' } // fija un rol; el resto deriva del seed
});
eidos.clearColorScheme(); // revierte a los primitives del tema
```
**Qué hace**: compone el motor `uix.color` — `deriveScheme` (Material 3 → jerarquía
+ neutral) → `generateScale` (12 pasos por rol) → APCA on-solid → alpha
compositing-inverse — en un override de la **capa de binding** `--primitive-{role}-*`
(+ `--color-{role}-contrast`). Override del binding **reproyecta** cada
`--color-{role}-{slot}` y el chrome neutral (surface/content/border) aguas abajo. La
**paleta de 33 escalas** y los slots NO se tocan.
- neutral) → `generateScale` (12 pasos por rol) → APCA on-solid → alpha
compositing-inverse — en un override de la **capa de binding** `--primitive-{role}-*`
(+ `--color-{role}-contrast`). Override del binding **reproyecta** cada
`--color-{role}-{slot}` y el chrome neutral (surface/content/border) aguas abajo. La
**paleta de 33 escalas** y los slots NO se tocan.
**Capas** (matemática pura → composición pura → aplicación DOM):
| Pieza | Dónde | Qué |
| --- | --- | --- |
| matemática | `arts/color` (`$color`) | `deriveScheme` / `generateScale` / `temper` / APCA / alpha — pura, isomórfica |
| composición | `eidos/lib/build-scheme.ts` | `buildScheme(seed, opts)` → `{ variables, roles }` — pura, testeable |
| runtime | `ActiveEidos.applyColorScheme` | resuelve donantes + background del tema activo, escribe el bloque de estilo, **sigue light/dark** |
| Pieza | Dónde | Qué |
| ----------- | ------------------------------ | ------------------------------------------------------------------------------------------------- |
| matemática | `arts/color` (`$color`) | `deriveScheme` / `generateScale` / `temper` / APCA / alpha — pura, isomórfica |
| composición | `eidos/lib/build-scheme.ts` | `buildScheme(seed, opts)` → `{ variables, roles }` — pura, testeable |
| runtime | `ActiveEidos.applyColorScheme` | resuelve donantes + background del tema activo, escribe el bloque de estilo, **sigue light/dark** |
**Sigue el modo**: las curvas-donantes + el background salen del tema activo, así que
el esquema se **re-deriva en cada `apply()`** (cambio de modo → ramp light vs dark). El
@ -633,7 +634,8 @@ cascada.
**Override por rol** + **temper** = doctrina de §25.4 / RFC §6.2: la jerarquía deriva
(override per-rol opcional), los intents **mantienen su hue** y solo afinan
temperatura. `applyColorScheme` devuelve `BuildSchemeResult` (steps hex + `stepsOklch`
+ solid / on-solid / pinned por rol) para introspección de UI.
- solid / on-solid / pinned por rol) para introspección de UI.
**Wide-gamut**: el bloque apila **hex fallback + `oklch()`** por paso (vía
`schemeDeclarations`), y `generateScale` retiene el OKLCH raw sin clamp — un seed
@ -652,8 +654,8 @@ navegador lo soporta (Chrome 111+ / Safari 15.4+ / Firefox 113+).
```css
:root {
--scale-purple-9: #8e4ec6; /* fallback sRGB */
--scale-purple-9: oklch(0.5556 0.1829 305.86); /* gana -> gamut del display */
--scale-purple-9: #8e4ec6; /* fallback sRGB */
--scale-purple-9: oklch(0.5556 0.1829 305.86); /* gana -> gamut del display */
}
```
@ -684,7 +686,10 @@ box-shadow, así que el foco **desaparecía**. Fix: la foundation emite siempre
```css
@media (forced-colors: active) {
:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
:focus-visible {
outline: 2px solid Highlight;
outline-offset: 2px;
}
}
```
@ -703,7 +708,7 @@ query) y estrictamente MÁS fuerte, así que no puede regresar el look por defec
`renderPrefersContrastBlock` en `render-css.ts`.
**Ramp de bordes** — el slot de rol `border` pasó de **step 6 → step 7**. En la escala
funcional de Radix el 6 es un *separador sutil* y el 7 es el *UI element border*; el 6
funcional de Radix el 6 es un _separador sutil_ y el 7 es el _UI element border_; el 6
se leía lavado en bordes reales (outline / surface / controles). `element` / `hover` /
`active` (3 / 4 / 5) se mantienen (canónicos de Radix para component-bg).
`DEFAULT_COLOR_ROLE_SLOT_STEPS`. Verificado en navegador (checkbox + token
@ -715,7 +720,7 @@ La profundidad es un **canal unificado y eventful**, no tres sistemas sueltos (s
superficie + z). Guía canónica: `DEPTH_ENGINE_RFC.md`. **Dos momentos**:
- **Estado** — `data-depth='{plane}'` aplica un **plano en reposo** (`flush · raised ·
overlay · modal · recessed`) que cohere superficie + sombra + z. Los tokens
overlay · modal · recessed`) que cohere superficie + sombra + z. Los tokens
`--depth-{plane}-{cue}` **componen los primitivos existentes** (`--color-surface-*`,
`--shadow-*`, `--z-index-*`), así que la mezcla es mode-aware gratis. La regla
`[data-depth]` aplica las señales aditivas seguras (`box-shadow` = gota `shadow` + rim-light
@ -724,7 +729,7 @@ superficie + z). Guía canónica: `DEPTH_ENGINE_RFC.md`. **Dos momentos**:
invisible sobre superficies claras (manda la gota), señal de elevación sobre oscuras — la
respuesta mode-adaptive a "la sombra miente en dark".
- **Evento** — al **emerger** la sombra crece desde plano → la de reposo (sube); al
**presionar** se aplana (recede). Vive en la *firma* (`present-rise` / `press-squeeze`
**presionar** se aplana (recede). Vive en la _firma_ (`present-rise` / `press-squeeze`
sobre `data-event-*`), coordinado con motion + sound + haptic desde **un solo evento**.
Generic: un elemento `flush` (sin sombra) = no-op. Degrada con `prefers-reduced-motion`.
@ -749,7 +754,7 @@ unificaría también la z) queda como opción futura.
> canónico): los overlays **estampan el atributo** (`data-depth="overlay"` en
> select · dropdown · context-menu · popover · link-preview · nav-menu ·
> card-group…), y el plano pinta el **bundle de APARIENCIA**: `background`
> (si declara surface), **`border`** (cue nuevo — modelo *bordered elevation*,
> (si declara surface), **`border`** (cue nuevo — modelo _bordered elevation_,
> el bando Radix/shadcn frente al tonal de M3: en light el borde separa
> planos con sombras sutiles; en dark, donde la sombra miente, borde+halo
> llevan el límite), `box-shadow` (gota+halo) y el **baseline tipográfico
@ -757,8 +762,8 @@ unificaría también la z) queda como opción futura.
> re-wrap de Radix en portales; es baseline, no estilo de contenido: el
> adopter fija su radius/font/color encima y gana por orden de cascada, sin
> doble borde; ojo: el borde del plano es real y suma 1px a la caja).
> **z JAMÁS se pinta** — doctrina: *el plano pinta, el posicionador
> posiciona* (el positioner de soma espeja el z computado; los portalados
> **z JAMÁS se pinta** — doctrina: _el plano pinta, el posicionador
> posiciona_ (el positioner de soma espeja el z computado; los portalados
> viven en la banda plana `--z-index-overlay-*` de §35 precisamente porque
> el ladder no puede ordenarlos). `--depth-{plane}-z` queda expuesto como
> introspección/escape (0 consumidores — jaula abierta). Guarda:
@ -786,18 +791,18 @@ la translucidez era idéntica en toda elevación — un error: no acompañaba a
**Tier de sombra interior** (`--shadow-inset-*`, 2026-06-15): la escala de sombra
gana un tier **inset** mode-aware, distinto de las sombras de gota (exteriores) y
de los *inset-rings* (anillo nítido `inset 0 0 0 Npx`, otro eje):
de los _inset-rings_ (anillo nítido `inset 0 0 0 Npx`, otro eje):
| Token | Light | Dark |
|---|---|---|
| Token | Light | Dark |
| ----------------------- | -------------------------------------- | ----------------------------------- |
| `--shadow-inset-subtle` | `inset 0 1px 2px rgb(15 23 42 / 0.08)` | `inset 0 1px 2px rgb(0 0 0 / 0.30)` |
| `--shadow-inset-deep` | `inset 0 2px 4px rgb(15 23 42 / 0.12)` | `inset 0 2px 4px rgb(0 0 0 / 0.45)` |
| `--shadow-inset-deep` | `inset 0 2px 4px rgb(15 23 42 / 0.12)` | `inset 0 2px 4px rgb(0 0 0 / 0.45)` |
El plano `recessed` lo consume (`--depth-recessed-shadow: var(--shadow-inset-subtle)`),
sustituyendo el `color-mix(neutral-contrast …)` inline previo — que en dark daba un
borde claro (embossado) en vez de un hundido; ahora es mode-correcto (inset oscuro en
ambos modos). Referencias: Tailwind `inset-shadow-{2xs,xs,sm}`, Bootstrap `shadow-inset`,
Chakra `inner`. Disponible además para estados *pressed* / wells.
Chakra `inner`. Disponible además para estados _pressed_ / wells.
**Inset-ring** (`--ring-inset-width`, 2026-06-15): eje hermano pero **distinto** —
un anillo interior **nítido** (no difuminado), como el `inset-ring` de Tailwind
@ -817,14 +822,14 @@ marcas laterales de un solo lado (range-calendar `inset ±2px 0 0 0`) **no** son
**primitivo** (`STATIC_BLUR` → `lib/primitives/static.ts`), no un px disperso.
Valores alineados a Tailwind, escalados por `--scaling` como `--icon-size-*`:
| Token | px | = Tailwind |
|---|---|---|
| `--blur-none` | 0 | — |
| `--blur-sm` | 4 | xs |
| `--blur-md` | 8 | sm |
| `--blur-lg` | 12 | md |
| `--blur-xl` | 16 | lg |
| `--blur-xxl` | 24 | xl |
| Token | px | = Tailwind |
| ------------- | --- | ---------- |
| `--blur-none` | 0 | — |
| `--blur-sm` | 4 | xs |
| `--blur-md` | 8 | sm |
| `--blur-lg` | 12 | md |
| `--blur-xl` | 16 | lg |
| `--blur-xxl` | 24 | xl |
Dos modelos de referencia: **Tailwind** (escala numérica cruda) y **Apple** (materiales
semánticos `ultraThin…thick` que acoplan blur+translucidez). Eidos toma el de Tailwind como
@ -838,6 +843,7 @@ inventa px de blur a mano.
**Gradientes themeables** (`--gradient-angle-*` + `gradients`, 2026-06-15): eje de
dos capas, espejo de Tailwind (que declara **0 gradientes nombrados** — solo
maquinaria):
- **Direcciones** (`--gradient-angle-*`): las 8 brújulas de Tailwind como ángulos CSS
(`to-t 0deg · to-tr 45 · to-r 90 · to-br 135 · to-b 180 · to-bl 225 · to-l 270 · to-tl 315`).
- **Nombrados** (`gradients` config → `--gradient-*`): **extensibles** (jaula abierta:
@ -898,10 +904,11 @@ solo es posible generándolo). Opt-in: un ancestro con `data-container` activa
**Opacidad — escala coordinada de dos capas** (`--opacity-*`, 2026-06-15): mismo
patrón dual que la sombra (numérico + semántico).
- **Numérico** (`--opacity-{0,5,…,100}`, Tailwind step-5): granularidad fina para
interfaces etéreas / cristal (capas translúcidas en el tramo bajo).
- **Semántico** (los roles que consumen los recipes): `ghost 0.3 · disabled 0.4 ·
scrim 0.45 · muted 0.65 · overlay 0.65 · subtle 0.8 · press 0.85 · hover 0.9 · full 1`.
scrim 0.45 · muted 0.65 · overlay 0.65 · subtle 0.8 · press 0.85 · hover 0.9 · full 1`.
`disabled = 0.4` (estándar moderno ≈ Material 38%).
**Unificación**: el estado `disabled` se renderizaba con ~10 valores distintos
@ -934,10 +941,10 @@ es la corrección óptica intrínseca de cada paso.
> escala.** Decisión de diseño (usuario): los 12 styles declaraban
> `letterSpacing: '0'`, anulando sin registro el tracking óptico por-tamaño
> de la Fase 3 en TODA la superficie de contenido (heading/display/text/
> caption…). Pauta vigente: *un named style solo declara
> caption…). Pauta vigente: _un named style solo declara
> `lineHeight`/`letterSpacing` cuando DIVERGE deliberadamente de la métrica
> de su tamaño (anotado inline); coincidencia = omitir (hereda el token de
> la escala — fuente óptica única).* Aplicado: 12 trackings a herencia
> la escala — fuente óptica única)._ Aplicado: 12 trackings a herencia
> (hero/h1/h2 aprietan −0.02/−0.015/−0.01em; h6/caption aflojan +0.005em),
> 3 lineHeights redundantes fuera (hero, h1, code), 9 divergencias de
> leading anotadas. Guarda en `active-eidos-config.test.ts` (re-declarar la
@ -965,12 +972,12 @@ arco + estático; la continuidad solo existía en Apple, atada a plataforma).
círculos/píldoras) y **progresivo**: degrada al arco de `border-radius` donde no hay
`corner-shape` (Chromium 2025+).
- **Armonía anidada** — `[data-shape-nest]` deriva `border-radius: max(0px,
var(--shape-outer-radius) − var(--shape-nest-gap))`: el hijo queda concéntrico al padre (que
var(--shape-outer-radius) − var(--shape-nest-gap))`: el hijo queda concéntrico al padre (que
expone su radio en `--shape-outer-radius`). **El concéntrico de 4 esquinas requiere radios
finitos**: a `full` (9999px) el radio se recorta a ½ de la dimensión menor *de cada elemento*, así
finitos**: a `full` (9999px) el radio se recorta a ½ de la dimensión menor _de cada elemento_, así
que un hijo de proporción distinta no puede serlo en las 4. Pero **sí en las superiores**
(`radio_card − gap`) si las inferiores quedan rectas — la geometría del reproductor iOS. El demo
`/temas/forma` lo mide (`ResizeObserver`, porque el cap es valor *usado* no legible en CSS) y lo
`/temas/forma` lo mide (`ResizeObserver`, porque el cap es valor _usado_ no legible en CSS) y lo
aplica al top de la carátula.
- **Eventful (dos momentos)** — la forma en reposo (`data-shape`) + el **morph** al pulsar: la
firma `press-squeeze` cuadra la esquina un instante (`--shape-smoothing` 2→3→2, registrado con
@ -1009,7 +1016,7 @@ Los sistemas **estructurales** (a diferencia de los expresivos) son **solo-estad
escenario, no el suceso. Guía canónica: `STRUCTURE_ENGINE_RFC.md`. Tres ejes ortogonales:
- **Densidad** — `[data-density='compact'|'comfortable'|'spacious']` reescala el layout (`space`
+ `control-height`) sin tocar la legibilidad del texto. 3 niveles × 2 ejes (config-driven).
- `control-height`) sin tocar la legibilidad del texto. 3 niveles × 2 ejes (config-driven).
- **Scaling** — `[data-scaling='90'..'110']` es el zoom global (incluye tipografía; paridad
Radix), compone con densidad.
- **Espacio (el ritmo)** — `--space-{key}` se emite como
@ -1032,7 +1039,7 @@ compartido), capstone del cuarteto→quinteto de builders.
> **Actualización (2026-07-07, auditoría C)**: `applyTheme(seed)` /
> `clearTheme()` **existen** (`ActiveEidos`; `{ color?, type?, depth?,
> shape?, space? }` en un solo write gestionado y atómico — §29-gradientes
shape?, space? }` en un solo write gestionado y atómico — §29-gradientes
> 06-27 ya lo citaba, y `channels.md` lo documenta como capstone). Este
> "pendiente" quedó stale al aterrizar el sexteto.
@ -1060,6 +1067,7 @@ marco** al editar (anillo interior + exterior), inconsistente entre componentes.
```
Con `inner-width: 0` el anillo interior es invisible → **un solo marco exterior**.
- El anillo del foundation `[data-archetype]:focus-visible` **excluye los elementos internos
de campo** (`:not(input):not(textarea):not(select):not([data-archetype='segment'])`): su
foco lo muestra el control que los envuelve (`archetypes.css`).
@ -1079,10 +1087,10 @@ uso** (solo la consumía el anillo interior ad-hoc) y se retiró. Quedan registr
se quiere reintroducir que `css-field` / `number-field` tiñan su foco por `data-color` (como
hacen date/time/color-field con sus segmentos):
| Componente | Tokens retirados | Cascada |
| --- | --- | --- |
| `css-field` | `--_css-field-accent-border` · `--_css-field-accent-track` · `--_css-field-accent-text` | `[data-css-field][data-color='…']` × 8 (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) |
| `number-field` | `--_number-field-accent-border` · `--_number-field-accent-track` · `--_number-field-accent-text` | `[data-number-field][data-color='…']` × 8 |
| Componente | Tokens retirados | Cascada |
| -------------- | ------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `css-field` | `--_css-field-accent-border` · `--_css-field-accent-track` · `--_css-field-accent-text` | `[data-css-field][data-color='…']` × 8 (primary/secondary/neutral/affirm/fulfill/risk/threat/loss) |
| `number-field` | `--_number-field-accent-border` · `--_number-field-accent-track` · `--_number-field-accent-text` | `[data-number-field][data-color='…']` × 8 |
Para reinstaurarlos: re-declarar el trío en el bloque base + la cascada `data-color`, y
consumir `accent-border` en el anillo del campo. **Hoy** ambos usan el `--focus-ring-color`
@ -1153,10 +1161,10 @@ etiqueta con su `aria-label` (morfo), así que `content` en un pseudo-elemento e
Cuatro tokens, dos por layout (viven en el recipe **compartido** `spin-field` — ver §34):
| Token | Default | Layout |
| --- | --- | --- |
| `--spin-field-control-increment-glyph` | `'+'` | split |
| `--spin-field-control-decrement-glyph` | `'−'` (`\2212`) | split |
| Token | Default | Layout |
| ---------------------------------------------- | --------------- | ------- |
| `--spin-field-control-increment-glyph` | `'+'` | split |
| `--spin-field-control-decrement-glyph` | `'−'` (`\2212`) | split |
| `--spin-field-control-increment-glyph-stacked` | `'▲'` (`\25B2`) | stacked |
| `--spin-field-control-decrement-glyph-stacked` | `'▼'` (`\25BC`) | stacked |
@ -1175,7 +1183,7 @@ flechas verticales `▲`/`▼`. Un único par no puede tener ambos defaults a la
`number-field` y `css-field` son **el mismo visual** (campo con borde + input + botones
increment/decrement + scrubber, layouts split/stacked, sizes/variants/colors, glifos);
solo difieren en el *modelo de valor* (soma: número vs valor CSS). Tener dos recipes +
solo difieren en el _modelo de valor_ (soma: número vs valor CSS). Tener dos recipes +
dos CSS clonados causaba **drift**: refinar uno (botones cuadrados, flush, divisor) dejaba
el otro con el look viejo. La respuesta canónica no es duplicar — es **compartir
estructuralmente**, el mismo patrón que `toggle-group` reusa `toggle`.
@ -1199,7 +1207,7 @@ uno scopeando el token al `data-` del componente — `[data-number-field] { --sp
**Resultado**: una sola fuente del visual del stepper-field. Un fix se aplica a los dos (y a
cualquier futuro spin-field) sin posibilidad de drift. `date`/`time`/`color-field` son
*segmentados* (sin steppers) — comparten solo la *superficie* del campo, lo que sería un
_segmentados_ (sin steppers) — comparten solo la _superficie_ del campo, lo que sería un
refactor aparte.
---
@ -1208,23 +1216,23 @@ refactor aparte.
Sprint de auditoría que llevó las escalas del theming a paridad con las referencias
(Tailwind · Material 3 · Apple HIG · Bootstrap · Radix) y, sobre todo, **forzó su
consumo**: la tesis de la auditoría es que *una escala canónica que los componentes
no consumen (la bypassan con literales) deriva en N variantes del mismo valor*. Cada
consumo**: la tesis de la auditoría es que _una escala canónica que los componentes
no consumen (la bypassan con literales) deriva en N variantes del mismo valor_. Cada
eje es ahora **retunable por tema** (igual que size/sombra/superficie) y los recipes
**consumen el token, nunca un literal**. Detalle por-eje en el addendum de §29; tokens
en la tabla de §6.
| Eje | Token(s) | Canon | Decisión clave |
|---|---|---|---|
| **Blur** | `--blur-{none,sm,md,lg,xl,xxl}` | 0/4/8/12/16/24 (Tailwind) | numérico crudo; los planos de depth lo consumen (`--depth-*-blur`) |
| **Inner-shadow** | `--shadow-inset-{subtle,deep}` | mode-aware (light slate / dark negro) | lo usa el plano `recessed`; ≠ inset-ring |
| **Inset-ring** | `--ring-inset-width` + `--ring-inset-color` | `inset 0 0 0 var(width) var(color)` **en el punto de uso** | un token único pre-resuelto es imposible (CSS hornea el `var()` anidado en `:root`) |
| **Gradientes** | `--gradient-angle-*` + `gradients`→`--gradient-*` | 8 direcciones + nombrados role-composed | Tailwind ship 0 nombrados → solo `shimmer`; los funcionales (HSV, conic, líneas) NO son de tema |
| **Breakpoints** | `--breakpoint-{sm..xxl}`, `EidosConfig.breakpoints` | **fuente = `ActiveDom`** (runtime, dev-settable) | `ActiveEidos` threadea `dom.breakpoints` al generador; los `@media` dejan de congelarse |
| **Container queries** | recipe key `container` → `@container` + `[data-container]` | px literal **generado** (CSS prohíbe `var()` en `@container`) | jaula abierta, 0 consumidores hoy |
| **Opacidad** | `--opacity-{0..100}` + semánticos | dual numérico + semántico; `disabled 0.4` (≈ Material 38%) | unificado (~10 valores de disabled → 1); numérico fino = glass-friendly |
| **Border-width** | `--border-width-{none,thin,medium,thick,heavy}` | **lineal Bootstrap** 0/1/2/3/4 (única ref con el 3px real) | **todos** los anchos crudos tokenizados (~35 ficheros) |
| **Tracking** | `--tracking-{…,caps,widest}` | + `caps 0.04em` (MAYÚSCULAS) + `widest 0.1em` | 11 `letter-spacing` crudos migrados; el óptico por-tamaño NO |
| Eje | Token(s) | Canon | Decisión clave |
| --------------------- | ---------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| **Blur** | `--blur-{none,sm,md,lg,xl,xxl}` | 0/4/8/12/16/24 (Tailwind) | numérico crudo; los planos de depth lo consumen (`--depth-*-blur`) |
| **Inner-shadow** | `--shadow-inset-{subtle,deep}` | mode-aware (light slate / dark negro) | lo usa el plano `recessed`; ≠ inset-ring |
| **Inset-ring** | `--ring-inset-width` + `--ring-inset-color` | `inset 0 0 0 var(width) var(color)` **en el punto de uso** | un token único pre-resuelto es imposible (CSS hornea el `var()` anidado en `:root`) |
| **Gradientes** | `--gradient-angle-*` + `gradients`→`--gradient-*` | 8 direcciones + nombrados role-composed | Tailwind ship 0 nombrados → solo `shimmer`; los funcionales (HSV, conic, líneas) NO son de tema |
| **Breakpoints** | `--breakpoint-{sm..xxl}`, `EidosConfig.breakpoints` | **fuente = `ActiveDom`** (runtime, dev-settable) | `ActiveEidos` threadea `dom.breakpoints` al generador; los `@media` dejan de congelarse |
| **Container queries** | recipe key `container` → `@container` + `[data-container]` | px literal **generado** (CSS prohíbe `var()` en `@container`) | jaula abierta, 0 consumidores hoy |
| **Opacidad** | `--opacity-{0..100}` + semánticos | dual numérico + semántico; `disabled 0.4` (≈ Material 38%) | unificado (~10 valores de disabled → 1); numérico fino = glass-friendly |
| **Border-width** | `--border-width-{none,thin,medium,thick,heavy}` | **lineal Bootstrap** 0/1/2/3/4 (única ref con el 3px real) | **todos** los anchos crudos tokenizados (~35 ficheros) |
| **Tracking** | `--tracking-{…,caps,widest}` | + `caps 0.04em` (MAYÚSCULAS) + `widest 0.1em` | 11 `letter-spacing` crudos migrados; el óptico por-tamaño NO |
**Incidente registrado**: una reescritura masiva por PowerShell (`WriteAllText`)
corrompió 11 ficheros (`o→p`); recuperados con `git checkout` + rehechos con la
@ -1238,7 +1246,7 @@ px/rem (cierra el hueco del guard solo-CSS). Arreglados los últimos hardcodes
(`toggle`, `avatar` → `--font-size-*`, valores preservados). El `icon-size` de
`password-field` desde `--control-height-*` es **correcto** (es el tamaño del botón
reveal, no del glifo) — falso positivo de la auditoría. **Deferido**: el refactor a
*consumir* el bundle del arquetipo (en vez de re-declarar el mapeo) — grande, con
_consumir_ el bundle del arquetipo (en vez de re-declarar el mapeo) — grande, con
edge-cases (fuentes semánticas por-parte, sistema `--text-N` de accordion) + edición
en paralelo; el guard es lo que impide la deriva mientras tanto.
@ -1275,7 +1283,7 @@ literal que iguala un paso de escala DEBE consumir el token; nada de "intenciona
deliberadamente sub-`fast`, NO transiciones de interacción; la **banda de
interacción** de 5 pasos (`instant..slow`) no tiene sitio para ellos (la
escala completa de duraciones suma las 4 largas de F6 — `slower·deliberate·
emphatic·sustained`, 9 claves; `motion.md`).
emphatic·sustained`, 9 claves; `motion.md`).
- **Border / ring width** — los anchos **únicos** de borde/ring que igualaban un
paso (`2px`=`medium`, `3px`=`thick`, `1px`=`thin`) se tokenizaron a
`var(--border-width-*)` (avatar border + badge + carve, drawer drag-ring, slider
@ -1299,7 +1307,7 @@ literal que iguala un paso de escala DEBE consumir el token; nada de "intenciona
> fantasma (`var(--layout-container-width-xl, 80rem)`), **sombreando la
> primitiva** para todos los consumidores (`maxWidth='xl'` ≡ `'xxl'`). Ahora
> la recipe posee su propio nombre — `--container-max-width:
> var(--container-width-xl)` — y `container.css` lo consume. Regla registrada:
var(--container-width-xl)` — y `container.css` lo consume. Regla registrada:
> **una clave de recipe jamás re-emite el nombre de un token de foundation**.
## 36. Gap canónico trigger→panel — offset token-driven (2026-06-22)
@ -1327,10 +1335,10 @@ eso cada flotante hardcodeaba su gap (0/4/6/8, incoherente). Canon:
default cae al token. Los 5 pickers default `sideOffset=undefined` para seguir el
token del panel (componen `PopoverContent`).
| Arquetipo | Componentes | Gap |
|---|---|---|
| `menu` (gap pequeño) | dropdown · context · sub-menus · menubar · select · navigation-menu | `--floating-gap-menu` (`var(--space-1)`) |
| `panel` | popover · combobox · link-preview · 5 pickers (vía `PopoverContent`) | `--floating-gap-panel` (`--space-1-5`) |
| Arquetipo | Componentes | Gap |
| -------------------- | -------------------------------------------------------------------- | ---------------------------------------- |
| `menu` (gap pequeño) | dropdown · context · sub-menus · menubar · select · navigation-menu | `--floating-gap-menu` (`var(--space-1)`) |
| `panel` | popover · combobox · link-preview · 5 pickers (vía `PopoverContent`) | `--floating-gap-panel` (`--space-1-5`) |
Fuera: `command` (dialog/inline, no anclado a trigger), `onion-menu` (radial). Para
retunear el gap de un tema: override `--floating-gap-menu` / `--floating-gap-panel`.
@ -1480,14 +1488,14 @@ Auditoría de theming, ítem A.9. Dos decisiones de usuario ejecutadas juntas:
**segunda gramática** para conceptos que ya tenían nombre canónico. Mueren
todos, con migración value-preserving de sus consumidores:
| Alias muerto | Canónico | Refs migradas |
| --- | --- | --- |
| `--font-ui` (cadena de TRES nombres: ui → style-label → family-primary) | `var(--style-label-font-family)` — la capa de roles ES los named styles; nueva validación: `styles.label.family` es requisito | 94 |
| `--font-mono` / `--font-code` | `--font-family-mono` | 64 / 0 |
| `--font-sans` / `--font-serif` / `--font-prose` / `--font-heading` / `--font-display` | `--font-family-{primary·secondary·display}` | 9 |
| `--text-{1..6}-{size,lh,ls}` (escala numérica paralela) | `--font-size-{k}` etc. — y al migrar, los consumidores de control cayeron bajo la regla §5 y ahora consumen la **coordenada del bundle** `--size-{k}-font-size` (el alias EVADÍA ese guard, igual que `px/py` evadía R-4.4) | 7 |
| `--color-focus-ring` (par duplicado con ambos nombres vivos: 10 vs 36) | `--focus-ring-color` (contratado + mayoritario + familia cohesiva) | 13 |
| `--radius-xs` (¡step inexistente!), `--control-height-2xs`, `--font-size-{base,2xl,3xl}`, `--font-weight-normal`, `--motion-spin-duration` | `--radius-sm` · `xxs` · `md/xxl/xxxl` · `regular` · (0 consumidores) | 3 |
| Alias muerto | Canónico | Refs migradas |
| ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `--font-ui` (cadena de TRES nombres: ui → style-label → family-primary) | `var(--style-label-font-family)` — la capa de roles ES los named styles; nueva validación: `styles.label.family` es requisito | 94 |
| `--font-mono` / `--font-code` | `--font-family-mono` | 64 / 0 |
| `--font-sans` / `--font-serif` / `--font-prose` / `--font-heading` / `--font-display` | `--font-family-{primary·secondary·display}` | 9 |
| `--text-{1..6}-{size,lh,ls}` (escala numérica paralela) | `--font-size-{k}` etc. — y al migrar, los consumidores de control cayeron bajo la regla §5 y ahora consumen la **coordenada del bundle** `--size-{k}-font-size` (el alias EVADÍA ese guard, igual que `px/py` evadía R-4.4) | 7 |
| `--color-focus-ring` (par duplicado con ambos nombres vivos: 10 vs 36) | `--focus-ring-color` (contratado + mayoritario + familia cohesiva) | 13 |
| `--radius-xs` (¡step inexistente!), `--control-height-2xs`, `--font-size-{base,2xl,3xl}`, `--font-weight-normal`, `--motion-spin-duration` | `--radius-sm` · `xxs` · `md/xxl/xxxl` · `regular` · (0 consumidores) | 3 |
Matiz web-app: `web/routes/layout.css` usaba `--font-sans`/`--font-mono`/
`--radius-xs` como **vocabulario local** (Geist para el chrome del dev-site);
@ -1540,13 +1548,13 @@ contrato (`setCssVariables` lo rechazaba) y fuera de la validación.
Promovidos a datos del config, con los valores embarcados **verbatim**
(cero cambio visual):
| Config nuevo | Token | Valor (antes cocido en) |
| --- | --- | --- |
| `primitives.state.{hover,press,selected}` | `--state-*` | `8%/12%/12%` de `currentColor` (bloque `:root` de archetypes.css — **eliminado**; las reglas se quedan) |
| `primitives.radiusFactor` | `--radius-factor` | `1` (render-css) |
| `primitives.radiusDefault` | `--radius-default` | `'md'` (render-css; Decisión 2) |
| `primitives.border.insetRingWidth` | `--ring-inset-width` | `'medium'` (render-css; §29) |
| `primitives.floating.{gapMenu,gapPanel}` | `--floating-gap-*` | `var(--space-1)` / `var(--space-1-5)` (render-css; §36) |
| Config nuevo | Token | Valor (antes cocido en) |
| ----------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------- |
| `primitives.state.{hover,press,selected}` | `--state-*` | `8%/12%/12%` de `currentColor` (bloque `:root` de archetypes.css — **eliminado**; las reglas se quedan) |
| `primitives.radiusFactor` | `--radius-factor` | `1` (render-css) |
| `primitives.radiusDefault` | `--radius-default` | `'md'` (render-css; Decisión 2) |
| `primitives.border.insetRingWidth` | `--ring-inset-width` | `'medium'` (render-css; §29) |
| `primitives.floating.{gapMenu,gapPanel}` | `--floating-gap-*` | `var(--space-1)` / `var(--space-1-5)` (render-css; §36) |
El MECANISMO no se toca en ningún caso: el velo sigue siendo `currentColor`
(mode-correct gratis — misma razón por la que M3 no re-declara sus state
@ -1569,15 +1577,15 @@ tematizable — y aquí además es config→emisión→contrato→runtime de una
Auditoría de theming, bloque B. **La mecánica** (la clase entera): un token de
recipe emitido en `:root` cuyo valor referencia un privado `--_*` declarado
solo en el CSS del componente se computa EN `:root`, donde el privado no
existe → congela *guaranteed-invalid* → los descendientes heredan el
existe → congela _guaranteed-invalid_ → los descendientes heredan el
congelado (re-declarar el privado más abajo no re-evalúa nada). El TSC no lo
cazaba: su inferencia de dependencias solo ve referencias públicas.
| Fantasma | Desde | Veredicto + fix |
| --- | --- | --- |
| `--calendar-day-holiday-shadow` + `--calendar-event-shadow` — las marcas de festivo (subrayado 2px en acento) y evento (anillo) **nunca pintaron** | su nacimiento | "Las features deben existir" → `scope: 'host'` (dato TSC puro): computan en `[data-calendar]` donde vive `--_calendar-accent-border` y sus 8 retunes por color. **Pintan por primera vez** (verificado en vivo). Los préstamos cross-component (range-calendar 79 tokens, month/year-grid 65, drp 38) siguen como estaban → veredicto de diseño registrado: **la familia calendar se formaliza como capa compartida/arquetipo** (chronos beberá de ella) — `docs/next-features.md`. |
| `--{date,time,color}-field-segment-height` — el alto encajado del segmento computó `auto` siempre | su nacimiento | Veredicto de diseño del usuario: la altura sale del **eje size a nivel familia field** (`--field-control-height-{k}`, que cada x-field hoy re-duplica) — spec registrada y **mandatada**: los componentes deben incorporar el wrapper Field (`[data-field][data-size]`; sondado en vivo: hoy los segmentos no viven dentro). Mientras: los 3 tokens rotos y sus 4 consumos **eliminados** (computaban `auto`; cero cambio visual, verificado). |
| `image-adjustments` `value-color: var(--color-content-tertiary)` — slot **inexistente** (mezcló el namespace content con el nombre del ROL); el read-out heredó su color desde `7da7285d` (2026-06-11) | 2026-06-11 | Corrección del usuario: "tertiary era un color de ACENTO" → `var(--color-tertiary-text)` (slot de texto del rol jerárquico). El read-out pinta el acento terciario (verificado en vivo). |
| Fantasma | Desde | Veredicto + fix |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--calendar-day-holiday-shadow` + `--calendar-event-shadow` — las marcas de festivo (subrayado 2px en acento) y evento (anillo) **nunca pintaron** | su nacimiento | "Las features deben existir" → `scope: 'host'` (dato TSC puro): computan en `[data-calendar]` donde vive `--_calendar-accent-border` y sus 8 retunes por color. **Pintan por primera vez** (verificado en vivo). Los préstamos cross-component (range-calendar 79 tokens, month/year-grid 65, drp 38) siguen como estaban → veredicto de diseño registrado: **la familia calendar se formaliza como capa compartida/arquetipo** (chronos beberá de ella) — `docs/next-features.md`. |
| `--{date,time,color}-field-segment-height` — el alto encajado del segmento computó `auto` siempre | su nacimiento | Veredicto de diseño del usuario: la altura sale del **eje size a nivel familia field** (`--field-control-height-{k}`, que cada x-field hoy re-duplica) — spec registrada y **mandatada**: los componentes deben incorporar el wrapper Field (`[data-field][data-size]`; sondado en vivo: hoy los segmentos no viven dentro). Mientras: los 3 tokens rotos y sus 4 consumos **eliminados** (computaban `auto`; cero cambio visual, verificado). |
| `image-adjustments` `value-color: var(--color-content-tertiary)` — slot **inexistente** (mezcló el namespace content con el nombre del ROL); el read-out heredó su color desde `7da7285d` (2026-06-11) | 2026-06-11 | Corrección del usuario: "tertiary era un color de ACENTO" → `var(--color-tertiary-text)` (slot de texto del rol jerárquico). El read-out pinta el acento terciario (verificado en vivo). |
**Guards que matan la clase** (`recipe-css-contract.test.ts`):
**G1** — un valor de recipe que referencia `--_*` debe declarar scope que lo
@ -1618,45 +1626,46 @@ margen hacia el blanco en media paleta. Forma final: ambos stops mezclan hacia
el lado de sombra de la tinta (blanca → `#000`, extremo fuerte abajo; oscura →
`#fff`, extremo fuerte arriba), con ancla + ángulo resueltos por color×modo
por el MISMO flip del slot `contrast` (`--color-{role}-finish-anchor/-angle`
+ líneas por escala en la capa THM-2) — el contraste solo puede MEJORAR sobre
el emparejamiento base: seguridad constructiva, dial sin topes (0 regresiones
hasta 40%). Guard ejecutable: `gradient-finish-guard.test.ts` (no-regresión +
set flat-fail clavado: cyan/orange, deuda preexistente del on-solid). **Badge**
se suma al gate v1 (paleta privada; sin `solid-hover` → el extremo profundo
cae a `solid`). **Override por tema/modo del lift** (mismo día):
`ThemeDefinition.gradientFinish.lift` re-emite el dial en el bloque de tema
(misma especificidad, después en la cascada → el tema gana; claro y oscuro
pueden llevar intensidades distintas, `0%` lo apaga por tema) — test en
`active-eidos-config.test.ts`. v1.x COMPLETA. **v1.5 `spread` shipped (mismo
día, D10)**: `gradient="spread"` = rotación de matiz ±`--gradient-finish-spread`
(default `30`, SIN unidad — el canal `h` de relative color es `<number>`:
`30deg` computa `none`, medido en Chromium) a L/C constantes, con la mezcla
débil del ancla (lift/3) en ambos stops — medido: la rotación pura rompía
grass ±4°/gold ±27° (L de OKLCH ≠ luminancia); anclada: 0 regresiones ≤±45°,
clavado en el guard (4/4). ≈Plano en grises (C≈0), documentado. **v2.1
`Surface` shipped (mismo día)**: la primitiva del lienzo temable — Box +
tratamiento (Box sigue layout-only por doctrina; Surface compone `<Box>`
patrón Section y estampa `data-surface` + color/variant/gradient/rounded);
recipe palette-tint espejo de Card sin chrome (`_palette-*` 5×8, THM-2:
roles + 33 escalas gratis), variantes `soft`/`solid`, ambos kinds del
acabado, morfo declarativo patrón Box (`scope:['eidos']`, 0 eventos
justificados), audit 145/145. El hero deja de ser escape-hatch. **v2.2 (mismo día) — EL
PLAN COMPLETO**: **named finishes** (D11: `gradientFinish.named` opta
gradientes del open cage como acabado CON tinta autorada obligatoria →
`--gradient-{name}-ink`, viajando como var `--_{c}-finish-ink` que el slice
solid de cada recipe consume con su contrast de fallback — auditoría
2026-07-15: el override directo de `--_{c}-fg` empataba (0,3,0) con el slice
de Surface y dejaba el ganador al orden de hojas, no-contrato → indirección
D7, gana por existir, nunca por especificidad; nombre no declarado degrada a
la rampa, medido; la base sigue siendo el solid de la identidad → `aurora` shipped como blobs de rol con
alfa SIN color base final, `background-image`-válido por arquitectura;
validación numérica solo posible para gradientes de modelo — diferida y
documentada) + **`on`/`data-on` mínimo** (D12: re-binding de
`--color-content-*`/`--color-border-default` para el subárbol, verificado en
vivo — muted computa la tinta del contexto al 64%; límites por construcción:
anidados y portales excluidos; forced-colors → `CanvasText`; sin
`color-scheme` a propósito). Guard 5/5. Capítulo del libro:
`docs/theming/gradient-finish.md` (registro de decisiones D1–D12).
- líneas por escala en la capa THM-2) — el contraste solo puede MEJORAR sobre
el emparejamiento base: seguridad constructiva, dial sin topes (0 regresiones
hasta 40%). Guard ejecutable: `gradient-finish-guard.test.ts` (no-regresión +
set flat-fail clavado: cyan/orange, deuda preexistente del on-solid). **Badge**
se suma al gate v1 (paleta privada; sin `solid-hover` → el extremo profundo
cae a `solid`). **Override por tema/modo del lift** (mismo día):
`ThemeDefinition.gradientFinish.lift` re-emite el dial en el bloque de tema
(misma especificidad, después en la cascada → el tema gana; claro y oscuro
pueden llevar intensidades distintas, `0%` lo apaga por tema) — test en
`active-eidos-config.test.ts`. v1.x COMPLETA. **v1.5 `spread` shipped (mismo
día, D10)**: `gradient="spread"` = rotación de matiz ±`--gradient-finish-spread`
(default `30`, SIN unidad — el canal `h` de relative color es `<number>`:
`30deg` computa `none`, medido en Chromium) a L/C constantes, con la mezcla
débil del ancla (lift/3) en ambos stops — medido: la rotación pura rompía
grass ±4°/gold ±27° (L de OKLCH ≠ luminancia); anclada: 0 regresiones ≤±45°,
clavado en el guard (4/4). ≈Plano en grises (C≈0), documentado. **v2.1
`Surface` shipped (mismo día)**: la primitiva del lienzo temable — Box +
tratamiento (Box sigue layout-only por doctrina; Surface compone `<Box>`
patrón Section y estampa `data-surface` + color/variant/gradient/rounded);
recipe palette-tint espejo de Card sin chrome (`_palette-*` 5×8, THM-2:
roles + 33 escalas gratis), variantes `soft`/`solid`, ambos kinds del
acabado, morfo declarativo patrón Box (`scope:['eidos']`, 0 eventos
justificados), audit 145/145. El hero deja de ser escape-hatch. **v2.2 (mismo día) — EL
PLAN COMPLETO**: **named finishes** (D11: `gradientFinish.named` opta
gradientes del open cage como acabado CON tinta autorada obligatoria →
`--gradient-{name}-ink`, viajando como var `--_{c}-finish-ink` que el slice
solid de cada recipe consume con su contrast de fallback — auditoría
2026-07-15: el override directo de `--_{c}-fg` empataba (0,3,0) con el slice
de Surface y dejaba el ganador al orden de hojas, no-contrato → indirección
D7, gana por existir, nunca por especificidad; nombre no declarado degrada a
la rampa, medido; la base sigue siendo el solid de la identidad → `aurora` shipped como blobs de rol con
alfa SIN color base final, `background-image`-válido por arquitectura;
validación numérica solo posible para gradientes de modelo — diferida y
documentada) + **`on`/`data-on` mínimo** (D12: re-binding de
`--color-content-*`/`--color-border-default` para el subárbol, verificado en
vivo — muted computa la tinta del contexto al 64%; límites por construcción:
anidados y portales excluidos; forced-colors → `CanvasText`; sin
`color-scheme` a propósito). Guard 5/5. Capítulo del libro:
`docs/theming/gradient-finish.md` (registro de decisiones D1–D12).
---
@ -1719,6 +1728,7 @@ se cumplen en las 33 escalas × 2 modos. Herramienta reutilizable:
reutiliza la matemática de `$color`).
**Drift medido + veredictos del usuario:**
- **Texto sobre fondos**: `text·11` (Radix "low-contrast text") queda marginal
sub-4.5:1 en ~11 escalas turbias en modo claro (bronze/orange/teal/gold/…,
peor sobre `element·3`); `textStrong·12` pasa 4.5 en TODAS. **Veredicto:
@ -1737,11 +1747,11 @@ reutiliza la matemática de `$color`).
(`color.focus.ring` = `primary·8 @ ~50%` translúcido, `innerWidth: 0`) mide
sub-3:1 (1.4–1.9:1 vs superficies; ~1.1 sobre un control solid del mismo hue).
PERO es un **eje de config** (`primitives.focusRing` {offset,width,innerWidth}
+ `color.focus.ring`, emitido una vez, §32): endurecerlo (color opaco,
`innerWidth>0`, offset) es una decisión de valores-por-defecto por config, NO
código. El modelo sigue siendo UN `outline` (§32 lo canonizó; el box-shadow
doble-anillo se retiró — sobrevive a HCM, sin flicker de segment-fields). Se
mantienen los defaults por decisión del usuario; queda registrado.
- `color.focus.ring`, emitido una vez, §32): endurecerlo (color opaco,
`innerWidth>0`, offset) es una decisión de valores-por-defecto por config, NO
código. El modelo sigue siendo UN `outline` (§32 lo canonizó; el box-shadow
doble-anillo se retiró — sobrevive a HCM, sin flicker de segment-fields). Se
mantienen los defaults por decisión del usuario; queda registrado.
Stage 2 (generador by-construction que resuelve la luminancia de cada step para
satisfacer la tabla) sigue registrado en `next-features.md §1`, gated en la
@ -1752,12 +1762,12 @@ migración base→seeds (`rfc-color-engine §9`). Doctrina standing:
Ejecución del plan [`process/contrast-stage2-plan-2026-07.md`](../process/contrast-stage2-plan-2026-07.md).
Se cerraron las 6 decisiones del usuario (D1 banda `text·11` blanda ≈APCA 60 ·
D2 *medido-pasa* · D3 flip-only · D4 post-pass opt-in · D5 runtime-first · D6
D2 _medido-pasa_ · D3 flip-only · D4 post-pass opt-in · D5 runtime-first · D6
tabla-como-datos ya) y luego la **ejecución refutó la premisa** del solver.
**Hallazgo (medido, no teórico):** el morph de plantilla **no COMPUTA** el
contraste del texto — lo **HEREDA**. Los steps 11/12 copian la curva-L del
donante *verbatim* y el contraste está dominado por L (el gamut-mapping baja
donante _verbatim_ y el contraste está dominado por L (el gamut-mapping baja
croma a L fija ⇒ el contraste es casi **invariante al croma**); el anclaje exacto
solo toca el step 9. Por tanto, TODA escala generada desde una librería de
donantes §40-compliant hereda los suelos de texto ratificados **por
@ -1770,6 +1780,7 @@ entradas realistas → **descartado por especulativo** (CLAUDE.md: nada
especulativo).
**Lo que sí entró:**
- La tabla de pares ratificada (§40) como **datos compartidos** —
`$color` → `CONTRAST_PAIRS` (`arts/color/contrast-contract.ts`): floor duro
`text-strong·12`, banda blanda `text·11` (tope relacional a text-strong, sin
@ -1802,7 +1813,7 @@ layout anidados = cinco veces el padding.
**Corrección**: las 44 variables se registran en `box.css` con
`@property { syntax: '*'; inherits: false }` y sin valor inicial. Así cada una
queda *garantizada-inválida* salvo que la ponga el propio elemento, y las
queda _garantizada-inválida_ salvo que la ponga el propio elemento, y las
cadenas `var(--box-…, revert-layer)` resuelven lo que su autor escribió: los
props de ESE Box y, si no, el cascade normal.
@ -1874,6 +1885,47 @@ había anotado en el handoff de blocks— es una causa raíz sin buscar.
---
**Última revisión**: 2026-07-24 (§48 herencia en `Flex`/`Grid`). Si algo en
## 49. El realce al puntero de `Card` deja de ser rehén de `interactive` (2026-08-17)
**Incidente**: una tarjeta de plan de `pricing` no respondía al puntero, y no
había forma de que lo hiciera. El realce existía —`--card-hover-lift` y
`--card-hover-shadow` están en la fundación desde siempre, con reglas para
`ghost` / `outline` / `solid`, composición con `data-selected` y anulación bajo
`prefers-reduced-motion`— pero **todo colgaba de `[data-interactive]`**.
**Causa**: `interactive` empaqueta cinco cosas: el elemento `<button>`, el evento
`commit-select`, el `cursor: pointer`, la escala de pulsación, el anillo de foco
**y** la elevación. Una tarjeta que ya contiene su propia llamada a la acción no
puede tomar ese paquete: un `<button>` dentro de otro es marcado inválido y las
dos activaciones se pelean por el mismo gesto. Así que la elevación era
inalcanzable justo donde más se pide — planes de precio, teasers de artículo,
tarjetas de testimonio.
**Corrección**: la elevación pasa a su propio eje, `data-lift`, y las tres reglas
de hover (más las dos de `selected` + hover y la de reduced-motion) cuelgan de él.
`interactive` estampa `data-lift` también, así que una tarjeta clicable no cambia
en nada. Lo que el eje NO trae, a propósito: ni `cursor: pointer` —un puntero
sobre algo que al pulsarlo no hace nada es una promesa falsa—, ni escala de
pulsación, ni anillo de foco. Eso es lo que un CONTROL le debe al visitante.
**Sin tokens nuevos**: los dos que hacían falta ya existían. El attr es
visual-wrapper, no contrato: lo estampa el componente de eidos y lo guarda la
mitad de PRESENCIA (`component-visual-attrs.test.ts`, que gana su fila de `card`
— la primera), no el morfo. Es la doctrina del autor del 2026-08-15 aplicada:
declarar en morfo lo que sólo lee una capa no tiene sentido.
**Radio verificado** (Playwright, sobre píxeles y estilos computados):
`lift` apagado → hover no mueve nada (0px, sin sombra), cero regresión ·
`lift` puesto → `translateY(-2px)` + `0 18px 48px`, con la tarjeta todavía
`<div>`, `cursor: auto` y su CTA intacto dentro (0 botones anidados en botón) ·
bajo `prefers-reduced-motion` el desplazamiento se anula y la sombra se queda —el
movimiento se suprime, la señal de profundidad no— · una `Card interactive` real
(demo del canon, 5 de ellas) sigue siendo `<button>`, con `cursor: pointer` y los
mismos 2px de elevación. `vitest src/uix/eidos` 420/421: el único rojo es
`skin-media-player`, ajeno y anterior.
---
**Última revisión**: 2026-08-17 (§49 el eje `lift` de `Card`). Si algo en
este doc no coincide con el código, el código gana — pero abre un issue para que
actualicemos el doc.

@ -16,6 +16,10 @@ const VISUAL_ATTRS = [
['avatar/avatar-image.svelte', ['data-status']],
['avatar/avatar-fallback.svelte', ['data-status']],
['breadcrumb/breadcrumb.svelte', ['data-size']],
[
'card/card.svelte',
['data-size', 'data-variant', 'data-color', 'data-rounded', 'data-shape', 'data-lift']
],
['calendar/calendar.svelte', ['data-size', 'data-variant', 'data-color']],
['date-picker/date-picker.svelte', ['data-size', 'data-variant', 'data-color']],
['date-picker/date-picker-trigger.svelte', ['data-size', 'data-variant', 'data-color']],

@ -31,24 +31,50 @@ squircle ✓ y piloto del patrón `data-motion-state` (R-4.5).
<!-- Card seleccionable en un grid -->
<Card interactive bind:selected onSelect={pick} motion="lift">…</Card>
<!-- Card que RESPONDE al puntero sin ser un control (lleva su propio CTA dentro) -->
<Card lift>…<Card.Footer><Button>Elegir</Button></Card.Footer></Card>
```
- Partes: `Header`, `Title` (`as` h1–h6), `Description`, `Body`, `Footer`.
- Props: `variant` (`ChipVariant`), `size` (`xs`–`xl`, responsive), `color`
+ `colorCustom`, `rounded` + `shape`, **`interactive`** (el provider pasa
a superficie accionable), `selected` (estado pintado por la receta vía
`data-state`), `disabled`, `motion` (preset), `noEmerge` (desactiva el
fade-in de montaje), `onSelect(event)`.
- `colorCustom`, `rounded` + `shape`, **`interactive`** (el provider pasa
a superficie accionable), **`lift`** (solo el realce al puntero, ver abajo),
`selected` (estado pintado por la receta vía `data-state`), `disabled`,
`motion` (preset), `noEmerge` (desactiva el fade-in de montaje),
`onSelect(event)`.
### `lift` — el realce sin el rol (2026-08-17)
`interactive` empaqueta cinco cosas: el elemento `<button>`, el evento
`commit-select`, el cursor, la escala de pulsación, el anillo de foco **y** la
elevación al puntero. Una tarjeta que ya contiene su propia llamada a la acción
no puede tomar ese paquete —un `<button>` dentro de otro es marcado inválido y
las dos activaciones compiten por el mismo gesto—, así que la elevación era
inalcanzable justo donde más se pide: un plan de precios, el teaser de un
artículo, una tarjeta de testimonio.
`lift` da **solo la elevación**: `translateY(--card-hover-lift)` +
`--card-hover-shadow`, medidos en −2px y `0 18px 48px`. `interactive` lo implica,
así que una tarjeta clicable no cambia en nada. Lo que NO trae, a propósito: ni
`cursor: pointer` (un puntero sobre algo que al pulsarlo no hace nada es una
promesa falsa), ni escala de pulsación, ni anillo de foco — eso es lo que un
CONTROL le debe al visitante. Bajo `prefers-reduced-motion` el desplazamiento se
anula y la sombra se queda: se suprime el movimiento, no la señal de profundidad.
El attr es visual-wrapper (`data-lift`), no contrato: lo estampa este componente
y lo guarda `component-visual-attrs.test.ts`. Detalle en `theming/changelog.md`
§49.
## Comparativa
| Capacidad | UIX | shadcn | Chakra | Radix Themes | Mantine |
| --- | --- | --- | --- | --- | --- |
| Anatomía header/title/description/body/footer | ✓ | ✓ | ✓ | Children | ✓ (Section) |
| Variante interactiva/seleccionable | ✓ (`interactive`+`selected`) | Manual | No | `asChild` | No |
| Evento perceptivo del select | ✓ (`commit-select`) | No | No | No | No |
| Motion de superficie (mount/hover/press/selected) | ✓ (receta + preset) | No | No | No | No |
| Radio/esquina desacoplados (squircle Tier B) | ✓ | No | No | `radius` | `radius` |
| Capacidad | UIX | shadcn | Chakra | Radix Themes | Mantine |
| ------------------------------------------------- | ---------------------------- | ------ | ------ | ------------ | ----------- |
| Anatomía header/title/description/body/footer | ✓ | ✓ | ✓ | Children | ✓ (Section) |
| Variante interactiva/seleccionable | ✓ (`interactive`+`selected`) | Manual | No | `asChild` | No |
| Evento perceptivo del select | ✓ (`commit-select`) | No | No | No | No |
| Motion de superficie (mount/hover/press/selected) | ✓ (receta + preset) | No | No | No | No |
| Radio/esquina desacoplados (squircle Tier B) | ✓ | No | No | `radius` | `radius` |
Referencias: [shadcn Card](https://ui.shadcn.com/docs/components/card) ·
[Chakra Card](https://chakra-ui.com/docs/components/card) ·
@ -74,11 +100,11 @@ Referencias: [shadcn Card](https://ui.shadcn.com/docs/components/card) ·
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Gap | Disposición | Detalle |
| --------------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------- |
| Cardinalidad de selección (radio/checkbox de cards) | **descartar** | Ya existe: `CardGroup` + `ToggleGroup` poseen la cardinalidad (decisión 2026-06-27) — Card suelto no re-modela grupo. |
| Slots de media dedicados (cover/aspect) | **diferir** | `Header` + `AspectRatio` compuestos lo cubren; parte propia espera caso. |
| Tests del wrapper eidos | **diferir** | Pasada SYS-2. |
| Slots de media dedicados (cover/aspect) | **diferir** | `Header` + `AspectRatio` compuestos lo cubren; parte propia espera caso. |
| Tests del wrapper eidos | **diferir** | Pasada SYS-2. |
## Referencias

@ -6,7 +6,8 @@
* - data-color: primary | secondary | neutral | affirm | fulfill | risk | threat | loss
* - data-size: xs | sm | md | lg | xl
* - data-rounded: sm | md | lg
* - data-interactive (boolean) — promotes to <button> + hover/press/focus
* - data-interactive (boolean) — promotes to <button> + press/focus chrome
* - data-lift (boolean) — hover elevation; implied by data-interactive
*
* Plus three state attrs:
* - data-selected — pinned by consumer; recipe paints accent ring
@ -130,9 +131,12 @@
/* ── Rounded ─────────────────────────────────────────────────────────── */
[data-card][data-rounded='sm'] { --_card-radius: var(--card-radius-sm); }
[data-card][data-rounded='lg'] { --_card-radius: var(--card-radius-lg); }
[data-card][data-rounded='sm'] {
--_card-radius: var(--card-radius-sm);
}
[data-card][data-rounded='lg'] {
--_card-radius: var(--card-radius-lg);
}
/* Custom CSS colour: the wrapper stamps `data-color-custom` + the shared
* `--color-custom` seed, and the recipe's palette forward derives every
@ -179,16 +183,22 @@
-webkit-tap-highlight-color: transparent;
}
[data-card][data-interactive]:hover:not([data-disabled]) {
/* The LIFT is its own axis. `interactive` stamps `data-lift` too, so a clickable
* card is unchanged; but a card that already holds its own call to action (a
* pricing plan, an article teaser) can take the elevation WITHOUT becoming a
* `<button>`: nesting one inside another is invalid markup and makes two
* activations compete for one gesture. Cursor, press-scale and focus ring stay
* under `interactive` — they are what a CONTROL owes the visitor. */
[data-card][data-lift]:hover:not([data-disabled]) {
transform: translateY(var(--card-hover-lift));
box-shadow: var(--card-hover-shadow);
}
[data-card][data-interactive][data-variant='ghost']:hover:not([data-disabled]) {
[data-card][data-lift][data-variant='ghost']:hover:not([data-disabled]) {
background: var(--_card-palette-track);
}
[data-card][data-interactive][data-variant='outline']:hover:not([data-disabled]) {
[data-card][data-lift][data-variant='outline']:hover:not([data-disabled]) {
background: var(--_card-palette-track);
}
@ -198,7 +208,16 @@
* would press-scale the whole card. Pressing a control inside the card must not
* move the card. (Plain content presses still scale: only interactive descendants
* suppress it, so `:active` on the card's own surface is unaffected.) */
[data-card][data-interactive]:active:not([data-disabled]):not(:has(button:active, a:active, input:active, select:active, textarea:active, [role='button']:active)) {
[data-card][data-interactive]:active:not([data-disabled]):not(
:has(
button:active,
a:active,
input:active,
select:active,
textarea:active,
[role='button']:active
)
) {
transform: scale(var(--card-press-scale));
transition-duration: var(--card-press-duration);
}
@ -223,12 +242,12 @@
/* Selected + hovered: `box-shadow` is a single property, so the interactive
* hover shadow would otherwise REPLACE the selection ring (the ring disappears
* on hover). Compose both — ring first, hover shadow second. */
[data-card][data-selected][data-interactive]:hover:not([data-disabled]) {
[data-card][data-selected][data-lift]:hover:not([data-disabled]) {
box-shadow:
0 0 0 var(--card-selected-ring-width) var(--_card-palette-solid),
var(--card-hover-shadow);
}
[data-card][data-selected][data-variant='solid'][data-interactive]:hover:not([data-disabled]) {
[data-card][data-selected][data-variant='solid'][data-lift]:hover:not([data-disabled]) {
box-shadow:
0 0 0 var(--card-selected-ring-width) var(--_card-palette-contrast),
var(--card-hover-shadow);
@ -311,7 +330,7 @@
[data-card]:not([data-no-emerge]) {
animation: none;
}
[data-card][data-interactive]:hover:not([data-disabled]) {
[data-card][data-lift]:hover:not([data-disabled]) {
transform: none;
}
[data-card][data-interactive]:active:not([data-disabled]) {

@ -29,6 +29,7 @@
rounded = 'md',
shape,
interactive = false,
lift = false,
selected = false,
disabled = false,
noEmerge = false,
@ -135,6 +136,7 @@
data-rounded={resolvedRounded}
data-shape={shape}
data-interactive={interactive ? '' : undefined}
data-lift={lift || interactive ? '' : undefined}
data-selected={selected ? '' : undefined}
data-disabled={disabled ? '' : undefined}
data-state={selected ? 'selected' : 'idle'}

@ -106,6 +106,24 @@ export type CardProps = Pick<
* @default false
*/
interactive?: boolean;
/**
* Let the card LIFT under the pointer — the hover elevation only, without
* becoming a control.
*
* `interactive` bundles four things: the `<button>` element, the
* `commit-select` event, the focus ring and the press scale, plus this
* elevation. A card that already contains its own call to action cannot take
* that bundle — a `<button>` inside a `<button>` is invalid markup and the
* two activations fight over one gesture — which is why a pricing plan card
* had no pointer response at all. So the elevation is available on its own.
*
* What it does NOT bring, deliberately: no `cursor: pointer` (a pointer over
* something that does nothing when pressed is a false promise), no press
* scale, no focus ring. Those belong to a control. `interactive` still
* implies the lift, so nothing changes for a clickable card.
* @default false
*/
lift?: boolean;
/**
* Mark the card as selected — adds `data-selected` and (when
* interactive) `aria-pressed="true"`. Composes with `interactive`

Loading…
Cancel
Save

Powered by TurnKey Linux.