uix(link-preview): temable — 0 % → 100 %, y el plano overlay que traía tipografía

33 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → **100 %**: no
queda un knob de apariencia fuera del contrato (los tres restantes son sistema
— anillo de foco y los dos del plano de profundidad que pintan la flecha).
Global 48 %, sin contrato 42 → 41, al 100 % 7 → 8.

1. **Los per-talla llevan `parts: ['content']`**, no el host: el panel está
   PORTALADO y su `data-size` vive en el content, así que una cascada colgada
   del host no lo alcanza nunca. La talla mueve sólo dimensiones físicas
   (padding, min-width, tipo); familia, peso e interlineado son constantes a
   propósito, como ya decía la receta.
2. **La flecha no se tokeniza**: su fill y su stroke consumen el plano
   `overlay` para casar siempre con el panel; acuñar `arrow-fg` rompería justo
   esa garantía. El subrayado del trigger es un `color-mix` sobre
   `currentColor` — la mezcla ES el aspecto, así que el knob es la declaración
   entera, no el 35 %. Y el fallback encadenado del color del trigger viaja
   verbatim: la coma es parte del valor, no un segundo knob.

EL HALLAZGO — el plano `overlay` impone TIPOGRAFÍA (→ next-features §12.9):

`[data-depth='overlay']` (`generated/base.css:7088`) no declara sólo fondo,
borde y sombra: también `font-family` y `line-height`. El panel estampa ese
plano para su elevación, así que la declaración del plano empata a (0,1,0) con
la de la receta y **gana por orden de carga** (la hoja generada va después del
chunk de la receta). Medido con CDP `getMatchedStylesForNode`: el par
`font-family` / `line-height` de esta receta **no había pintado nunca**.
Retirado con **0 diffs**, que es la prueba.

Afecta a los **veinte** componentes que estampan el plano. Lo que hay que
decidir —aparte, mueve píxel— es si un plano de profundidad tiene competencia
sobre la tipografía o sólo sobre la superficie. Cuarta ocurrencia de la clase
«empate resuelto por orden de carga».

CÓMO SE MIDIÓ, porque el instrumento estándar tampoco servía aquí:

El panel abre en HOVER con retardo y está portalado → la sonda genérica ve **1
nodo**, y una sonda que compara un nodo pasa en falso. Sonda dirigida (abre por
hover, captura los cinco nodos reales en las cinco tallas): determinismo
verificado y **gate antes/después = 0 diffs sobre 550 valores**, repetido tras
la retirada.

El guard R-5.4 pasó de **3/35 a 31/33** con dos extensiones: `openBy: 'hover'`
para este componente (un hover card no se abre con clic — clicar su `<a>`
navega) y `text-decoration-color` en la lista de propiedades, que faltaba pese
a estar en `KNOB_PROPS` del censo. Verificado que los otros QUINCE componentes
con ledger no se mueven ni una cifra.

Los dos tokens que siguen mudos son un conflicto ESTRUCTURAL, no un hueco de
demo: para abrir el panel hay que tener el puntero en el trigger, y el hover es
justo el estado cuya regla pisa la tinta y el subrayado de reposo — el token de
hover lee vivo en la misma corrida. Adjudicados con esa razón.

Gates: censo --only 100 % · 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 «Talla y
tema» y tab `Tokens` en la demo (33 claves, en vivo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 67b4c810c7
commit 7a0fc2eadb

@ -10,9 +10,9 @@
- **Medido**: 2026-08-21 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/` - **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**? - **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **48%** — 2245 de 4678 knobs pasan por un token público del componente - **Alcance global**: **48%** — 2257 de 4676 knobs pasan por un token público del componente
- **Reparto**: público 2245 · privado 642 · global 1213 · literal 578 · sistema transversal 509 _(fuera del ratio)_ - **Reparto**: público 2257 · privado 638 · global 1204 · literal 577 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 42 · **alcance < 20 %**: 36 · **alcance 100 %**: 7 · **con eje `size`**: 63 - **Sin token público propio**: 41 · **alcance < 20 %**: 35 · **alcance 100 %**: 8 · **con eje `size`**: 62
## Cómo se lee ## 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 | | 19 | [timeline](./timeline.md) | 24 | 51% | 49 | 45 |
| 20 | [avatar](./avatar.md) | 23 | 36% | 36 | 0 | | 20 | [avatar](./avatar.md) | 23 | 36% | 36 | 0 |
## Sin ningún token público propio (42) ## Sin ningún token público propio (41)
Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward
(`_palette-*`), que no son contrato: un tema no puede nombrarlos. (`_palette-*`), que no son contrato: un tema no puede nombrarlos.
[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`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) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md) [`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md)
## Tabla completa (162 recetas, por alcance ascendente) ## 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 | – | | [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 | – | | [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 | – | | [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – |
| [link-preview](./link-preview.md) | 0% | 17 | 0 | 4 | 9 | 1 | 3 | 0 | y |
| [drag-drop](./drag-drop.md) | 0% | 16 | 0 | 3 | 6 | 0 | 7 | 1 | – | | [drag-drop](./drag-drop.md) | 0% | 16 | 0 | 3 | 6 | 0 | 7 | 1 | – |
| [kbd](./kbd.md) | 0% | 14 | 0 | 1 | 9 | 4 | 0 | 0 | – | | [kbd](./kbd.md) | 0% | 14 | 0 | 1 | 9 | 4 | 0 | 0 | – |
| [link](./link.md) | 0% | 14 | 0 | 4 | 2 | 5 | 3 | 0 | – | | [link](./link.md) | 0% | 14 | 0 | 4 | 2 | 5 | 3 | 0 | – |
@ -249,6 +248,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [text](./text.md) | — | 24 | 0 | 0 | 0 | 0 | 24 | 0 | – | | [text](./text.md) | — | 24 | 0 | 0 | 0 | 0 | 24 | 0 | – |
| [chat-list](./chat-list.md) | 100% | 18 | 18 | 0 | 0 | 0 | 0 | 18 | – | | [chat-list](./chat-list.md) | 100% | 18 | 18 | 0 | 0 | 0 | 0 | 18 | – |
| [splitter](./splitter.md) | 100% | 18 | 17 | 0 | 0 | 0 | 1 | 15 | – | | [splitter](./splitter.md) | 100% | 18 | 17 | 0 | 0 | 0 | 1 | 15 | – |
| [link-preview](./link-preview.md) | 100% | 15 | 12 | 0 | 0 | 0 | 3 | 33 | – |
| [box](./box.md) | 100% | 9 | 9 | 0 | 0 | 0 | 0 | 43 | – | | [box](./box.md) | 100% | 9 | 9 | 0 | 0 | 0 | 0 | 43 | – |
| [display](./display.md) | — | 6 | 0 | 0 | 0 | 0 | 6 | 0 | – | | [display](./display.md) | — | 6 | 0 | 0 | 0 | 0 | 6 | 0 | – |
| [barcode](./barcode.md) | 100% | 5 | 5 | 0 | 0 | 0 | 0 | 7 | – | | [barcode](./barcode.md) | 100% | 5 | 5 | 0 | 0 | 0 | 0 | 7 | – |

@ -5,41 +5,24 @@
> Vista de conjunto: [README](./README.md) · método y protocolo: > Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7. > [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-20 · **Alcance**: **0%** — 0 de 14 knobs por token público - **Medido**: 2026-08-21 · **Alcance**: **100%** — 12 de 12 knobs por token público
- **Knobs de apariencia**: 17 — público 0 · privado 4 · global 9 · literal 1 · sistema 3 _(fuera del ratio)_ - **Knobs de apariencia**: 15 — público 12 · privado 0 · global 0 · literal 0 · sistema 3 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`** - **Contrato hoy** (`lib/recipes/base.ts`): 33 pública(s) — `padding-block-xs`, `padding-block-sm`, `padding-block-md`, `padding-block-lg`, `padding-block-xl`, `padding-inline-xs`, `padding-inline-sm`, `padding-inline-md`, `padding-inline-lg`, `padding-inline-xl`, `font-size-xs`, `font-size-sm`, `font-size-md`, `font-size-lg`, `font-size-xl`, `min-width-xs`, `min-width-sm`, `min-width-md`, `min-width-lg`, `min-width-xl`, `padding-block`, `padding-inline`, `font-size`, `min-width`, `max-width`, `gap`, `radius`, `fg`, `font-weight`, `trigger-radius`, `trigger-fg`, `hover-trigger-fg`, `trigger-underline`
- **Eje `size`**: sí · **ficheros**: `link-preview.css` - **Eje `size`**: no · **ficheros**: `link-preview.css`
## 1. Knobs fuera de alcance ## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (9) ### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor | _Ninguno._
| ---: | --- | --- | --- | --- |
| 1 | `link-preview.css:21` | `[data-link-preview-trigger]` | `color` | `var(--color-primary-text, var(--color-content-primary))` |
| 2 | `link-preview.css:27` | `[data-link-preview-trigger]` | `border-radius` | `var(--radius-sm)` |
| 3 | `link-preview.css:36` | `[data-link-preview-trigger]:hover, [data-link-preview-trigger][data-state='open']` | `color` | `var(--color-primary-solid, var(--color-content-primary))` |
| 4 | `link-preview.css:59` | `[data-link-preview-content]` | `gap` | `var(--space-2)` |
| 5 | `link-preview.css:66` | `[data-link-preview-content]` | `border-radius` | `var(--radius-md)` |
| 6 | `link-preview.css:67` | `[data-link-preview-content]` | `color` | `var(--color-content-primary)` |
| 7 | `link-preview.css:68` | `[data-link-preview-content]` | `font-family` | `var(--style-label-font-family)` |
| 8 | `link-preview.css:70` | `[data-link-preview-content]` | `font-weight` | `var(--font-weight-regular)` |
| 9 | `link-preview.css:71` | `[data-link-preview-content]` | `line-height` | `var(--leading-normal)` |
### 1.2 A través de un privado (4)
| # | fichero:línea | selector | propiedad | valor | ### 1.2 A través de un privado (0)
| ---: | --- | --- | --- | --- |
| 1 | `link-preview.css:60` | `[data-link-preview-content]` | `min-inline-size` | `var(--_link-preview-min-width)` |
| 2 | `link-preview.css:62` | `[data-link-preview-content]` | `padding-block` | `var(--_link-preview-padding-block)` |
| 3 | `link-preview.css:63` | `[data-link-preview-content]` | `padding-inline` | `var(--_link-preview-padding-inline)` |
| 4 | `link-preview.css:69` | `[data-link-preview-content]` | `font-size` | `var(--_link-preview-font-size)` |
### 1.3 Literales (1) _Ninguno._
| # | fichero:línea | selector | propiedad | valor | ### 1.3 Literales (0)
| ---: | --- | --- | --- | --- |
| 1 | `link-preview.css:25` | `[data-link-preview-trigger]` | `text-decoration-color` | `color-mix(in srgb, currentColor 35%, transparent)` | _Ninguno._
## 2. Sistema transversal (3) — informativo, fuera del ratio ## 2. Sistema transversal (3) — informativo, fuera del ratio
@ -48,74 +31,16 @@ Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor | | # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- | | ---: | --- | --- | --- | --- |
| 1 | `link-preview.css:41` | `[data-link-preview-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` | | 1 | `link-preview.css:41` | `[data-link-preview-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 2 | `link-preview.css:131` | `[data-link-preview-arrow] polygon` | `fill` | `var(--depth-overlay-surface)` | | 2 | `link-preview.css:99` | `[data-link-preview-arrow] polygon` | `fill` | `var(--depth-overlay-surface)` |
| 3 | `link-preview.css:136` | `[data-link-preview-arrow] path` | `stroke` | `var(--depth-overlay-border)` | | 3 | `link-preview.css:104` | `[data-link-preview-arrow] path` | `stroke` | `var(--depth-overlay-border)` |
## 3. Privados de la receta — ¿de dónde sale su valor? ## 3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? | _La receta no declara privados propios en su CSS._
| --- | ---: | --- | --- | :-: |
| `--_link-preview-padding-block` | 6 | `var(--space-3)`, `var(--space-2)`, `var(--space-4)`, `var(--space-5)` | global | no |
| `--_link-preview-padding-inline` | 6 | `var(--space-4)`, `var(--space-3)`, `var(--space-5)`, `var(--space-6)` | global | no |
| `--_link-preview-font-size` | 6 | `var(--font-size-sm)`, `var(--font-size-xs)`, `var(--font-size-md)` | global | no |
| `--_link-preview-min-width` | 6 | `14rem`, `11rem`, `12rem`, `18rem`, `22rem` | literal | no |
| `--_link-preview-max-width` | 1 | `min(28rem, calc(100vw - var(--space-4)))` | global | no |
## 4. Propuesta de corrección ## 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). _Nada que proponer: no hay knobs fuera de alcance._
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (34)
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
al final) y theming §6.7 (slots de color, modificador delante). Un token con
DOS valores distintos es una colisión de nombre: son dos knobs, o el nombre
no distingue lo que debería — se marca `⚠`.
| token (`--link-preview-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `trigger-fg` | `root` | `var(--color-primary-text, var(--color-content-primary))` | 1 |
| `trigger-underline` | `root` | `color-mix(in srgb, currentColor 35%, transparent)` | 1 |
| `trigger-radius` | `root` | `var(--radius-sm)` | 1 |
| `hover-open-trigger-fg` | `root` | `var(--color-primary-solid, var(--color-content-primary))` | 1 |
| `content-gap` | `root` | `var(--space-2)` | 1 |
| `content-width` | `root` | `14rem` | 1 |
| `content-width-xs` | `size:xs` | `11rem` | 1 |
| `content-width-sm` | `size:sm` | `12rem` | 1 |
| `content-width-md` | `size:md` | `14rem` | 1 |
| `content-width-lg` | `size:lg` | `18rem` | 1 |
| `content-width-xl` | `size:xl` | `22rem` | 1 |
| `content-padding-block` | `root` | `var(--space-3)` | 1 |
| `content-padding-block-xs` | `size:xs` | `var(--space-2)` | 1 |
| `content-padding-block-sm` | `size:sm` | `var(--space-2)` | 1 |
| `content-padding-block-md` | `size:md` | `var(--space-3)` | 1 |
| `content-padding-block-lg` | `size:lg` | `var(--space-4)` | 1 |
| `content-padding-block-xl` | `size:xl` | `var(--space-5)` | 1 |
| `content-padding-inline` | `root` | `var(--space-4)` | 1 |
| `content-padding-inline-xs` | `size:xs` | `var(--space-3)` | 1 |
| `content-padding-inline-sm` | `size:sm` | `var(--space-3)` | 1 |
| `content-padding-inline-md` | `size:md` | `var(--space-4)` | 1 |
| `content-padding-inline-lg` | `size:lg` | `var(--space-5)` | 1 |
| `content-padding-inline-xl` | `size:xl` | `var(--space-6)` | 1 |
| `content-radius` | `root` | `var(--radius-md)` | 1 |
| `content-fg` | `root` | `var(--color-content-primary)` | 1 |
| `content-font-family` | `root` | `var(--style-label-font-family)` | 1 |
| `content-font-size` | `root` | `var(--font-size-sm)` | 1 |
| `content-font-size-xs` | `size:xs` | `var(--font-size-xs)` | 1 |
| `content-font-size-sm` | `size:sm` | `var(--font-size-sm)` | 1 |
| `content-font-size-md` | `size:md` | `var(--font-size-sm)` | 1 |
| `content-font-size-lg` | `size:lg` | `var(--font-size-md)` | 1 |
| `content-font-size-xl` | `size:xl` | `var(--font-size-md)` | 1 |
| `content-font-weight` | `root` | `var(--font-weight-regular)` | 1 |
| `content-line-height` | `root` | `var(--leading-normal)` | 1 |
### 4.3 Avisos sobre los tokens propuestos (4)
- **el privado `--_link-preview-min-width` debe pasar a leer este público (o desaparecer)** — `--link-preview-content-width`, `--link-preview-content-width-xs`, `--link-preview-content-width-sm`, `--link-preview-content-width-md`, `--link-preview-content-width-lg`, `--link-preview-content-width-xl`
- **el privado `--_link-preview-padding-block` debe pasar a leer este público (o desaparecer)** — `--link-preview-content-padding-block`, `--link-preview-content-padding-block-xs`, `--link-preview-content-padding-block-sm`, `--link-preview-content-padding-block-md`, `--link-preview-content-padding-block-lg`, `--link-preview-content-padding-block-xl`
- **el privado `--_link-preview-padding-inline` debe pasar a leer este público (o desaparecer)** — `--link-preview-content-padding-inline`, `--link-preview-content-padding-inline-xs`, `--link-preview-content-padding-inline-sm`, `--link-preview-content-padding-inline-md`, `--link-preview-content-padding-inline-lg`, `--link-preview-content-padding-inline-xl`
- **el privado `--_link-preview-font-size` debe pasar a leer este público (o desaparecer)** — `--link-preview-content-font-size`, `--link-preview-content-font-size-xs`, `--link-preview-content-font-size-sm`, `--link-preview-content-font-size-md`, `--link-preview-content-font-size-lg`, `--link-preview-content-font-size-xl`
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4) ### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -129,6 +54,30 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start --> <!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_ **Medido 2026-08-21.** Sonda dirigida (550 valores × 5 tallas, 0 diffs entre
dos corridas): la genérica ve **1 nodo** porque el panel está PORTALADO y abre
en HOVER con retardo, y el `data-size` vive en el CONTENT, no en el trigger.
1. **Los per-talla van con `parts: ['content']`**, no al host: los bloques son
`[data-link-preview-content][data-size='X']` y el content está portalado —
una cascada en el host no lo alcanza. Cuatro ejes por talla
(`padding-block`, `padding-inline`, `font-size`, `min-width`), veinte
coordenadas. La propia receta ya avisa de que la talla mueve **sólo
dimensiones físicas**: familia, peso e interlineado son constantes a
propósito, así que ésos son knobs planos.
2. **`--_link-preview-max-width` no se declara por talla** (es
`min(28rem, calc(100vw - var(--space-4)))` en las cinco): un knob plano, no
una quinta coordenada.
3. **El color del trigger lleva FALLBACK encadenado**
(`var(--color-primary-text, var(--color-content-primary))`) — la coma es
parte del valor, no un segundo knob. Se tokeniza verbatim, fallback
incluido, y lo mismo el del hover.
4. **La flecha NO se tokeniza**: su `fill` y su `stroke` consumen el plano
`overlay` del sistema de profundidad (`--depth-overlay-surface` / `-border`)
para casar siempre con el panel, y el censo ya los clasifica como sistema.
Acuñar `arrow-fg` rompería justo la garantía que esa regla da.
5. **El `text-decoration-color` del reposo es un `color-mix` sobre
`currentColor`** (35 %): la mezcla es construcción — el subrayado tenue de
un enlace —, así que el knob es la declaración entera, no el porcentaje.
<!-- veredicto:end --> <!-- veredicto:end -->

@ -454,6 +454,26 @@ el mismo nodo entra en una relación que nunca se escribió.
`hover-row-bg` SÍ es un token legítimo — al revés que en `grid-list`. La `hover-row-bg` SÍ es un token legítimo — al revés que en `grid-list`. La
misma regla CSS cubre los dos nodos. misma regla CSS cubre los dos nodos.
9. **El plano de profundidad `overlay` impone TIPOGRAFÍA, no sólo elevación**
— medido con CDP el 2026-08-21 al tokenizar `link-preview`.
`[data-depth='overlay']` (`generated/base.css:7088`) declara
`font-family: var(--style-label-font-family)` y
`line-height: var(--leading-ui)` junto al fondo, el borde y la sombra. Un
componente que estampe el plano para su elevación —**veinte lo hacen**—
hereda además esa tipografía, y su propia declaración empata a **(0,1,0)**
y **pierde por orden de carga** (la hoja generada va después del chunk de la
receta). En `link-preview` el par `font-family` / `line-height` de la receta
no había pintado nunca; retirarlo dio 0 diffs, que es la prueba. Cuarta
ocurrencia de la clase «empate resuelto por orden de carga» (§12.3, y las
dos del calendario y de `tree-grid`).
Lo que hay que decidir: si un PLANO DE PROFUNDIDAD tiene competencia sobre
la tipografía o sólo sobre la superficie. Si sólo sobre la superficie, esas
dos líneas salen del plano y cada componente recupera las suyas; si el plano
manda, entonces ningún overlay del catálogo debe declarar familia ni
interlineado y hay que barrer los que lo intentan. Mueve píxel en los veinte
en cuanto se toque.
### Qué habría que decidir ### Qué habría que decidir
En una frase: **quién manda cuando el arquetipo y la receta pintan el mismo En una frase: **quién manda cuando el arquetipo y la receta pintan el mismo

@ -114,6 +114,12 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'sentinel-spinner-reduced-duration': 'sentinel-spinner-reduced-duration':
'behind prefers-reduced-motion; emulated reduce context → reaches (4s → 11.5s)' 'behind prefers-reduced-motion; emulated reduce context → reaches (4s → 11.5s)'
}, },
'link-preview': {
'trigger-fg':
'the panel opens on HOVER, so measuring it REQUIRES the pointer on the trigger — and hover is exactly the state whose rule overrides the resting ink. Structural, not a demo gap: the hover token (`hover-trigger-fg`) reads live in the same run',
'trigger-underline':
'same structural conflict: the hover rule sets `text-decoration-color: currentColor` over the resting mix, and the panel cannot be opened without hovering'
},
'picker-shell': { 'picker-shell': {
'section-gap-xs': 'section-gap-xs':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)', 'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',

@ -73,6 +73,7 @@ const PROPS = [
'maxInlineSize', 'maxInlineSize',
'boxShadow', 'boxShadow',
'opacity', 'opacity',
'textDecorationColor',
'outlineColor', 'outlineColor',
'outlineWidth', 'outlineWidth',
'outlineOffset', 'outlineOffset',
@ -94,11 +95,18 @@ const PROPS = [
* measured inside a host picker, behind that picker's popover. Without this the * measured inside a host picker, behind that picker's popover. Without this the
* guard reported 0/31 and would have needed 31 false "exceptions". * guard reported 0/31 and would have needed 31 false "exceptions".
*/ */
const COMPONENT_OVERRIDES: Record<string, { attrPrefix?: string; openWith?: string[] }> = { const COMPONENT_OVERRIDES: Record<
string,
{ attrPrefix?: string; openWith?: string[]; openBy?: 'click' | 'hover' }
> = {
'picker-shell': { 'picker-shell': {
attrPrefix: 'data-picker', attrPrefix: 'data-picker',
openWith: ['[data-uix-stage-area] [data-popover-trigger]'] openWith: ['[data-uix-stage-area] [data-popover-trigger]']
} },
// A hover card opens on POINTER-OVER, not on click: clicking its trigger
// (an `<a>`) navigates instead of revealing the panel, so the portaled
// content never enters the document and 32 of its 35 tokens read dead.
'link-preview': { openBy: 'hover' }
}; };
function sentinelFor(key: string): string { function sentinelFor(key: string): string {
@ -168,7 +176,8 @@ async function main() {
const el = page.locator(sel).first(); const el = page.locator(sel).first();
if (await el.count()) { if (await el.count()) {
try { try {
await el.click({ timeout: 1500 }); if (override.openBy === 'hover') await el.hover({ timeout: 1500 });
else await el.click({ timeout: 1500 });
await page.waitForTimeout(400); await page.waitForTimeout(400);
break; break;
} catch { } catch {
@ -176,15 +185,17 @@ async function main() {
} }
} }
} }
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); if (override.openBy !== 'hover')
// ...and PARK THE POINTER. blur() drops the focus but Playwright leaves the await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.());
// ...and PARK THE POINTER (never for a hover-opened panel: moving the cursor
// away is exactly what dismisses it). blur() drops the focus but Playwright leaves the
// cursor where it clicked, so `:hover` keeps matching — and a hover rule // cursor where it clicked, so `:hover` keeps matching — and a hover rule
// usually outweighs the rest / focus / invalid ones it shares a node with // usually outweighs the rest / focus / invalid ones it shares a node with
// (measured on textarea 2026-08-21: hover (0,4,0) beats focus (0,3,0) beats // (measured on textarea 2026-08-21: hover (0,4,0) beats focus (0,3,0) beats
// invalid (0,2,0) beats rest (0,1,0), so THREE rest-state tokens read dead). // invalid (0,2,0) beats rest (0,1,0), so THREE rest-state tokens read dead).
// Same class as the click-focus false negative above, and the half that fix // Same class as the click-focus false negative above, and the half that fix
// left behind. The per-token hover pass re-hovers on purpose further down. // left behind. The per-token hover pass re-hovers on purpose further down.
await page.mouse.move(0, 0); if (override.openBy !== 'hover') await page.mouse.move(0, 0);
const dead: string[] = []; const dead: string[] = [];
const live: string[] = []; const live: string[] = [];
@ -205,10 +216,13 @@ async function main() {
const el = page.locator(sel).first(); const el = page.locator(sel).first();
if (await el.count()) { if (await el.count()) {
try { try {
await el.click({ timeout: 1000 }); if (override.openBy === 'hover') await el.hover({ timeout: 1000 });
else await el.click({ timeout: 1000 });
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.()); if (override.openBy !== 'hover') {
await page.mouse.move(0, 0); await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur?.());
await page.mouse.move(0, 0);
}
return; return;
} catch { } catch {
/* keep trying */ /* keep trying */

@ -62,6 +62,54 @@ Referencias: [Bits UI Link Preview](https://bits-ui.com/docs/components/link-pre
- **Sin re-declarar overlay**: open/close/posicionamiento son del Popover - **Sin re-declarar overlay**: open/close/posicionamiento son del Popover
compuesto; este morfo solo aporta identidad + partes. compuesto; este morfo solo aporta identidad + partes.
## Talla y tema
**Guard R-5.4** (`npm run theming:sentinel -- link-preview <url>`): 31/33
tokens mueven un computed en vivo; los dos que callan están adjudicados con su
razón medida en `scripts/theming-sentinel-exceptions.ts`. Corrido 2026-08-21.
Contrato en `lib/recipes/base.ts > link-preview` (33 claves, entrada NUEVA).
Alcance del censo: **100 %** — no queda un knob de apariencia fuera del
contrato; los tres restantes son sistema (anillo de foco y los dos del plano de
profundidad que pintan la flecha).
**Los per-talla van con `parts: ['content']`**, no al host: el panel está
PORTALADO y su `data-size` vive en el content, así que una cascada colgada del
host no lo alcanza jamás. La talla mueve **sólo dimensiones físicas** —padding,
min-width y tamaño de letra—; familia, peso e interlineado son constantes a
propósito, como decía ya la receta.
**La flecha no se tokeniza**: su `fill` y su `stroke` consumen el plano
`overlay` para casar siempre con el panel, y acuñar `arrow-fg` rompería justo
esa garantía. El subrayado del trigger es un `color-mix` sobre `currentColor`:
la mezcla ES el aspecto, así que el knob es la declaración entera.
### ⚠ El plano `overlay` traía la tipografía puesta
Medido con CDP el 2026-08-21: `[data-depth='overlay']` no declara sólo fondo,
borde y sombra — también `font-family` y `line-height`. El panel estampa ese
plano para su elevación, así que la declaración del plano empata a (0,1,0) con
la de esta receta y **gana por orden de carga** (la hoja generada va después).
El par `font-family` / `line-height` de la receta **no había pintado nunca**;
se retiró y dio 0 diffs, que es la prueba. Afecta a los veinte componentes que
estampan el plano, y decidir si un plano de profundidad manda sobre la
tipografía es aparte: `next-features.md` §12, punto 9.
### Para medirlo hace falta sonda dirigida
El panel abre en **hover** con retardo y está portalado, así que la sonda
genérica ve **1 nodo** — y una sonda que compara un nodo pasa en falso. La
verificación de este commit usó sonda dirigida (abre por hover, captura los
cinco nodos reales en las cinco tallas): **550 valores, 0 diffs**. El guard
R-5.4 ganó `openBy: 'hover'` para este componente (y `text-decoration-color`
en su lista de propiedades); los otros quince con ledger, verificados sin
cambio.
Los dos tokens mudos son un conflicto ESTRUCTURAL, no un hueco de demo: para
abrir el panel hay que tener el puntero en el trigger, y el hover es justo el
estado cuya regla pisa la tinta y el subrayado de reposo. El token de hover
(`hover-trigger-fg`) sí lee vivo en la misma corrida.
## Gaps ## Gaps
| Gap | Disposición | Detalle | | Gap | Disposición | Detalle |

@ -18,13 +18,13 @@
/* ── Trigger ─────────────────────────────────────────────────────────── */ /* ── Trigger ─────────────────────────────────────────────────────────── */
[data-link-preview-trigger] { [data-link-preview-trigger] {
color: var(--color-primary-text, var(--color-content-primary)); color: var(--link-preview-trigger-fg);
text-decoration-line: underline; text-decoration-line: underline;
text-decoration-thickness: 1px; text-decoration-thickness: 1px;
text-underline-offset: 0.18em; text-underline-offset: 0.18em;
text-decoration-color: color-mix(in srgb, currentColor 35%, transparent); text-decoration-color: var(--link-preview-trigger-underline);
text-decoration-skip-ink: auto; text-decoration-skip-ink: auto;
border-radius: var(--radius-sm); border-radius: var(--link-preview-trigger-radius);
outline: none; outline: none;
transition: transition:
color var(--duration-fast) var(--ease-default), color var(--duration-fast) var(--ease-default),
@ -33,7 +33,7 @@
[data-link-preview-trigger]:hover, [data-link-preview-trigger]:hover,
[data-link-preview-trigger][data-state='open'] { [data-link-preview-trigger][data-state='open'] {
color: var(--color-primary-solid, var(--color-content-primary)); color: var(--link-preview-hover-trigger-fg);
text-decoration-color: currentColor; text-decoration-color: currentColor;
} }
@ -49,63 +49,31 @@
* size and intrinsic type are independent concerns. * size and intrinsic type are independent concerns.
*/ */
[data-link-preview-content] { [data-link-preview-content] {
--_link-preview-padding-block: var(--space-3); /* The per-size coordinates are EMITTED by the TSC onto this part — the panel
--_link-preview-padding-inline: var(--space-4); is portaled, so they cannot hang off the host. The old `[data-size]` blocks
--_link-preview-font-size: var(--font-size-sm); are gone: they would shadow the new tokens. */
--_link-preview-min-width: 14rem;
--_link-preview-max-width: min(28rem, calc(100vw - var(--space-4)));
display: grid; display: grid;
gap: var(--space-2); gap: var(--link-preview-gap);
min-inline-size: var(--_link-preview-min-width); min-inline-size: var(--link-preview-min-width);
max-inline-size: var(--_link-preview-max-width); max-inline-size: var(--link-preview-max-width);
padding-block: var(--_link-preview-padding-block); padding-block: var(--link-preview-padding-block);
padding-inline: var(--_link-preview-padding-inline); padding-inline: var(--link-preview-padding-inline);
/* Elevation — surface · border · shadow — from the `overlay` plane via /* Elevation — surface · border · shadow — from the `overlay` plane via
`data-depth` (A1, Decisión 8). Radius / color stay the panel's own. */ `data-depth` (A1, Decisión 8). Radius / color stay the panel's own. */
border-radius: var(--radius-md); border-radius: var(--link-preview-radius);
color: var(--color-content-primary); color: var(--link-preview-fg);
font-family: var(--style-label-font-family); /* NO `font-family` / `line-height` here: the panel carries
font-size: var(--_link-preview-font-size); `data-depth='overlay'` for its elevation, and that plane declares BOTH
font-weight: var(--font-weight-regular); (generated/base.css) at the same (0,1,0) as this rule — the generated sheet
line-height: var(--leading-normal); loads later, so it wins. Measured 2026-08-21 with CDP matched-rules: the
recipe's own pair had never painted. Retiring them gave 0 diffs, which is
the proof. See next-features §12. */
font-size: var(--link-preview-font-size);
font-weight: var(--link-preview-font-weight);
outline: none; outline: none;
} }
[data-link-preview-content][data-size='xs'] { /* Size coordinates are emitted by the TSC on [data-link-preview-content]. */
--_link-preview-padding-block: var(--space-2);
--_link-preview-padding-inline: var(--space-3);
--_link-preview-font-size: var(--font-size-xs);
--_link-preview-min-width: 11rem;
}
[data-link-preview-content][data-size='sm'] {
--_link-preview-padding-block: var(--space-2);
--_link-preview-padding-inline: var(--space-3);
--_link-preview-font-size: var(--font-size-sm);
--_link-preview-min-width: 12rem;
}
[data-link-preview-content][data-size='md'] {
--_link-preview-padding-block: var(--space-3);
--_link-preview-padding-inline: var(--space-4);
--_link-preview-font-size: var(--font-size-sm);
--_link-preview-min-width: 14rem;
}
[data-link-preview-content][data-size='lg'] {
--_link-preview-padding-block: var(--space-4);
--_link-preview-padding-inline: var(--space-5);
--_link-preview-font-size: var(--font-size-md);
--_link-preview-min-width: 18rem;
}
[data-link-preview-content][data-size='xl'] {
--_link-preview-padding-block: var(--space-5);
--_link-preview-padding-inline: var(--space-6);
--_link-preview-font-size: var(--font-size-md);
--_link-preview-min-width: 22rem;
}
/* The floating wrapper is `position: fixed` but un-stacked. Lift it /* The floating wrapper is `position: fixed` but un-stacked. Lift it
* above the page chrome so the panel actually paints over content. */ * above the page chrome so the panel actually paints over content. */

@ -4016,6 +4016,35 @@
--splitter-grip-cross: 2px; --splitter-grip-cross: 2px;
--splitter-transition-duration: var(--duration-fast); --splitter-transition-duration: var(--duration-fast);
--splitter-transition-ease: var(--ease-default); --splitter-transition-ease: var(--ease-default);
--link-preview-padding-block-xs: var(--space-2);
--link-preview-padding-block-sm: var(--space-2);
--link-preview-padding-block-md: var(--space-3);
--link-preview-padding-block-lg: var(--space-4);
--link-preview-padding-block-xl: var(--space-5);
--link-preview-padding-inline-xs: var(--space-3);
--link-preview-padding-inline-sm: var(--space-3);
--link-preview-padding-inline-md: var(--space-4);
--link-preview-padding-inline-lg: var(--space-5);
--link-preview-padding-inline-xl: var(--space-6);
--link-preview-font-size-xs: var(--size-xs-font-size);
--link-preview-font-size-sm: var(--size-sm-font-size);
--link-preview-font-size-md: var(--size-sm-font-size);
--link-preview-font-size-lg: var(--size-md-font-size);
--link-preview-font-size-xl: var(--size-md-font-size);
--link-preview-min-width-xs: 11rem;
--link-preview-min-width-sm: 12rem;
--link-preview-min-width-md: 14rem;
--link-preview-min-width-lg: 18rem;
--link-preview-min-width-xl: 22rem;
--link-preview-max-width: min(28rem, calc(100vw - var(--space-4)));
--link-preview-gap: var(--space-2);
--link-preview-radius: var(--radius-md);
--link-preview-fg: var(--color-content-primary);
--link-preview-font-weight: var(--font-weight-regular);
--link-preview-trigger-radius: var(--radius-sm);
--link-preview-trigger-fg: var(--color-primary-text, var(--color-content-primary));
--link-preview-hover-trigger-fg: var(--color-primary-solid, var(--color-content-primary));
--link-preview-trigger-underline: color-mix(in srgb, currentColor 35%, transparent);
--picker-shell-section-gap-xs: var(--space-2); --picker-shell-section-gap-xs: var(--space-2);
--picker-shell-section-gap-sm: var(--space-3); --picker-shell-section-gap-sm: var(--space-3);
--picker-shell-section-gap-md: var(--space-3-5); --picker-shell-section-gap-md: var(--space-3-5);
@ -6860,6 +6889,41 @@
--_callout-palette-text: var(--palette-text, var(--callout-primary-text)); --_callout-palette-text: var(--palette-text, var(--callout-primary-text));
} }
[data-link-preview-content] {
--link-preview-padding-block: var(--link-preview-padding-block-md);
--link-preview-padding-inline: var(--link-preview-padding-inline-md);
--link-preview-font-size: var(--link-preview-font-size-md);
--link-preview-min-width: var(--link-preview-min-width-md);
}
[data-link-preview-content][data-size='xs'] {
--link-preview-padding-block: var(--link-preview-padding-block-xs);
--link-preview-padding-inline: var(--link-preview-padding-inline-xs);
--link-preview-font-size: var(--link-preview-font-size-xs);
--link-preview-min-width: var(--link-preview-min-width-xs);
}
[data-link-preview-content][data-size='sm'] {
--link-preview-padding-block: var(--link-preview-padding-block-sm);
--link-preview-padding-inline: var(--link-preview-padding-inline-sm);
--link-preview-font-size: var(--link-preview-font-size-sm);
--link-preview-min-width: var(--link-preview-min-width-sm);
}
[data-link-preview-content][data-size='lg'] {
--link-preview-padding-block: var(--link-preview-padding-block-lg);
--link-preview-padding-inline: var(--link-preview-padding-inline-lg);
--link-preview-font-size: var(--link-preview-font-size-lg);
--link-preview-min-width: var(--link-preview-min-width-lg);
}
[data-link-preview-content][data-size='xl'] {
--link-preview-padding-block: var(--link-preview-padding-block-xl);
--link-preview-padding-inline: var(--link-preview-padding-inline-xl);
--link-preview-font-size: var(--link-preview-font-size-xl);
--link-preview-min-width: var(--link-preview-min-width-xl);
}
[data-card-group] { [data-card-group] {
--card-group-padding: var(--card-group-padding-md); --card-group-padding: var(--card-group-padding-md);
--card-group-grid-gap: var(--card-group-grid-gap-md); --card-group-grid-gap: var(--card-group-grid-gap-md);

@ -6434,6 +6434,98 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// floating layer mirrors this computed z onto the positioner wrapper). The // floating layer mirrors this computed z onto the positioner wrapper). The
// band is deliberately NOT the global `--z-index-*` ladder — see // band is deliberately NOT the global `--z-index-*` ladder — see
// STATIC_Z_INDEX_OVERLAY. // STATIC_Z_INDEX_OVERLAY.
// LinkPreview's panel is PORTALED and its `data-size` sits on the CONTENT,
// not on the trigger — hence `parts: ['content']` on every resolved name; a
// cascade on the host would never reach it. Size moves PHYSICAL dimensions
// only: family, weight and line-height are constant across sizes on purpose
// (the recipe says so), so those are flat.
'link-preview': {
'padding-block-xs': 'var(--space-2)',
'padding-block-sm': 'var(--space-2)',
'padding-block-md': 'var(--space-3)',
'padding-block-lg': 'var(--space-4)',
'padding-block-xl': 'var(--space-5)',
'padding-inline-xs': 'var(--space-3)',
'padding-inline-sm': 'var(--space-3)',
'padding-inline-md': 'var(--space-4)',
'padding-inline-lg': 'var(--space-5)',
'padding-inline-xl': 'var(--space-6)',
'font-size-xs': 'var(--size-xs-font-size)',
'font-size-sm': 'var(--size-sm-font-size)',
'font-size-md': 'var(--size-sm-font-size)',
'font-size-lg': 'var(--size-md-font-size)',
'font-size-xl': 'var(--size-md-font-size)',
'min-width-xs': '11rem',
'min-width-sm': '12rem',
'min-width-md': '14rem',
'min-width-lg': '18rem',
'min-width-xl': '22rem',
'padding-block': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-padding-block-md)', scope: 'host' },
{ value: 'var(--link-preview-padding-block-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-padding-block-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-padding-block-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-padding-block-xl)', scope: 'size:xl' }
]
},
'padding-inline': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-padding-inline-md)', scope: 'host' },
{ value: 'var(--link-preview-padding-inline-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-padding-inline-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-padding-inline-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-padding-inline-xl)', scope: 'size:xl' }
]
},
'font-size': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-font-size-md)', scope: 'host' },
{ value: 'var(--link-preview-font-size-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-font-size-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-font-size-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-font-size-xl)', scope: 'size:xl' }
]
},
'min-width': {
parts: ['content'],
declarations: [
{ value: 'var(--link-preview-min-width-md)', scope: 'host' },
{ value: 'var(--link-preview-min-width-xs)', scope: 'size:xs' },
{ value: 'var(--link-preview-min-width-sm)', scope: 'size:sm' },
{ value: 'var(--link-preview-min-width-lg)', scope: 'size:lg' },
{ value: 'var(--link-preview-min-width-xl)', scope: 'size:xl' }
]
},
// Flat: one value across the five sizes.
'max-width': 'min(28rem, calc(100vw - var(--space-4)))',
gap: 'var(--space-2)',
radius: 'var(--radius-md)',
fg: 'var(--color-content-primary)',
'font-weight': 'var(--font-weight-regular)',
// NO `font-family` / `line-height`: the panel's `data-depth='overlay'`
// declares both and wins the (0,1,0) tie by load order, so the recipe's own
// pair was dead. Retired 2026-08-21 with 0 diffs — that typography is the
// depth plane's to theme, for all 20 components that stamp the plane.
// Trigger. The chained fallback is PART of the value — the comma is not a
// second knob — so it travels verbatim.
'trigger-radius': 'var(--radius-sm)',
'trigger-fg': 'var(--color-primary-text, var(--color-content-primary))',
'hover-trigger-fg': 'var(--color-primary-solid, var(--color-content-primary))',
// The resting underline: a faint mix of the ink itself. The mix IS the
// look, so the knob is the whole declaration, not the 35 %.
'trigger-underline': 'color-mix(in srgb, currentColor 35%, transparent)'
// NO arrow tokens: its fill/stroke consume the depth system's `overlay`
// plane so the caret always matches the panel (A1, like popover). Coining
// `arrow-fg` would break exactly the guarantee that rule provides.
},
// PickerShell is a COMPONENT the composite pickers import (date, color, // PickerShell is a COMPONENT the composite pickers import (date, color,
// chronos), not a shared CSS layer — there is no layer hook and no borrowed // chronos), not a shared CSS layer — there is no layer hook and no borrowed
// vocabulary. // vocabulary.

@ -6,10 +6,11 @@
import { compileMorfo } from '$uix/morfo'; import { compileMorfo } from '$uix/morfo';
import { linkPreviewMorfo } from '@/uix/morfo/components/link-preview'; import { linkPreviewMorfo } from '@/uix/morfo/components/link-preview';
import { getActiveUix } from '$active-uix'; import { getActiveUix } from '$active-uix';
import TokensPanel from '../../lib/TokensPanel.svelte';
const uix = getActiveUix(); const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y'; type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'tokens' | 'a11y';
let tab = $state<Tab>('live'); let tab = $state<Tab>('live');
type Side = 'top' | 'right' | 'bottom' | 'left'; type Side = 'top' | 'right' | 'bottom' | 'left';
@ -222,6 +223,9 @@
<span data-uix-tab-count>{events.length}</span> <span data-uix-tab-count>{events.length}</span>
</button> </button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button> <button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>33</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button> <button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div> </div>
@ -573,6 +577,10 @@
</section> </section>
{/if} {/if}
{#if tab === 'tokens'}
<TokensPanel component="link-preview" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'a11y'} {#if tab === 'a11y'}
<section data-uix-section> <section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2> <h2 data-uix-section-title>Accessibility</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.