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>
`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 |
| **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`
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 |
| **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 |
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 |
| `--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 |
| `--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 |
| `--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` 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.state.{hover,press,selected}` | `--state-*`| `8%/12%/12%` de `currentColor` (bloque `:root` de archetypes.css — **eliminado**; las reglas se quedan) |
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). |
| `--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