uix(emoji-picker): temable — 61 % → 100 %, y el cromo del trigger NO era suyo

Siete claves nuevas, TRES RETIRADAS por muertas, tres literales firmados y cinco
adjudicaciones medidas. Contrato 17 → 21. Diff de computed 0 sobre 6.080 valores
en 8 estados (176 nodos con el panel abierto), capturas 2× idénticas.

LOS NOMBRES LOS DIO EL CATÁLOGO

`open-trigger-fg` (accordion lo nombra igual), grid-padding-inline +
grid-padding-block-end (el eje lógico completo, como card-group / combobox /
command) y las tres del marcador de tono, la familia
timeline.marker-{size,radius,bg}. Se llama tone-marker-* y no marker-* porque el
pie lleva además un selector de TONO: dentro del propio componente, `marker` a
secas sería ambiguo.

TRES CLAVES RETIRADAS — EL CROMO DE REPOSO DEL TRIGGER NO ES SUYO

El trigger lleva también data-popover-trigger, y
[data-popover-trigger]:not([data-archetype='field-trigger']) es (0,2,0) contra
el (0,1,0) de esta receta: el radio, la tinta y el tamaño de letra los pinta
popover.css. Medido: mover --emoji-picker-trigger-{fg,radius,font-size} no
cambiaba NADA. Retiradas con sus declaraciones, 0 diffs — la prueba de que
estaban muertas. Es el defecto que gradient-picker documentó (36 de sus 45
tokens) y la misma respuesta. Sobreviven dos por razones distintas:
trigger-size, porque popover dimensiona con `height` y no con el par lógico, y
open-trigger-fg, porque su selector añade [data-state='open'] y gana por orden.
trigger-fg no llegó a existir: se acuñó y se midió muerta en el mismo pase.

CINCO ADJUDICACIONES, TODAS SOBRE NODOS REALES

Las tres del marcador de tono (la demo monta CERO celdas data-tone-capable;
forzado el atributo sobre una celda real, el ::after alcanza: 4px → 37px,
9999px → 37px, y el fondo), preview-name-fg (los hijos de la vista previa son
<span> desnudos, sin atributo del componente, fuera del conjunto medido) y
open-trigger-fg.

⚠ open-trigger-fg OSCILA ENTRE CORRIDAS: 17/21, 16/21, 16/21 con el mismo
código. Medido vivo CUATRO veces —la última replicando paso a paso el staticPass
del propio guard, que movió a la primera talla—, así que el token no miente: lo
que falla es el instrumento. La causa: el panel se cierra en algún punto de la
pasada y, durante la animación de SALIDA, el content sigue en el DOM mientras el
trigger ya perdió data-state='open', que es lo que la regla selecciona;
reopen() mira el CONTENT, cree que está abierto y no reabre. Probé un openMarker
sobre el trigger y NO basta (el clic de reapertura cae en esa misma ventana), así
que lo retiré en vez de dejar un override que no arregla. El arreglo real es que
reopen() espere a la CONDICIÓN en vez de dormir un número fijo — §13. Su entrada
de ledger saldrá STALE en las corridas donde alcanza, y está escrito en la razón
para que nadie la borre por eso.

Y EL INSTRUMENTO DE CAPTURA ESTABA ROTO DESDE SIEMPRE

__shot.ts tenía el puerto 5173 CABLEADO, así que cualquier sesión en otro puerto
moría con ERR_EMPTY_RESPONSE. Es probablemente por lo que los pasos 6 y 13 del
protocolo se saltaron en 33 componentes sin que nadie lo notara. Ahora toma la
URL como argumento —como la sonda y el guard— y un --open opcional para los
componentes cuya superficie sólo existe abierta. Estrenado aquí: capturas antes
y después, miradas y idénticas.

VERIFICACIÓN

Sonda antes/después 0 diffs (6.080 valores · 8 estados) · R-5.4 17/21 con las
cinco adjudicadas, exit 0 · component:audit PASS · censo --only 100 % ·
eidos-lint 17 morfo-backed / 0 invalid / 0 class-hooks · rtl:check 0 ·
docs:check 0 · suite eidos 439/440 (el rojo es skin-media-player, el conocido) ·
el ledger sigue en sus 63 hunks de prettier preexistentes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent 0c29c544b1
commit 863c6993c8

@ -10,9 +10,9 @@
- **Medido**: 2026-08-23 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/`
- **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **66%** — 3029 de 4587 knobs pasan por un token público del componente
- **Reparto**: público 3029 · privado 317 · global 835 · literal 406 · sistema transversal 509 · excepción firmada 125 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 26 · **con eje `size`**: 57
- **Alcance global**: **66%** — 3034 de 4581 knobs pasan por un token público del componente
- **Reparto**: público 3034 · privado 317 · global 827 · literal 403 · sistema transversal 509 · excepción firmada 128 _(los dos últimos, fuera del ratio)_
- **Sin token público propio**: 24 · **alcance < 20 %**: 12 · **alcance 100 %**: 27 · **con eje `size`**: 57
## Cómo se lee
@ -136,7 +136,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [audio-player](./audio-player.md) | 56% | 18 | 10 | 0 | 8 | 0 | 0 | 8 | – |
| [drawer](./drawer.md) | 60% | 74 | 43 | 8 | 16 | 5 | 2 | 54 | y |
| [mark](./mark.md) | 60% | 5 | 3 | 2 | 0 | 0 | 0 | 3 | – |
| [emoji-picker](./emoji-picker.md) | 61% | 32 | 17 | 0 | 8 | 3 | 4 | 17 | – |
| [radio-cards](./radio-cards.md) | 61% | 72 | 43 | 0 | 23 | 4 | 2 | 40 | y |
| [password-field](./password-field.md) | 62% | 35 | 21 | 2 | 2 | 9 | 1 | 30 | y |
| [clipboard](./clipboard.md) | 63% | 8 | 5 | 2 | 1 | 0 | 0 | 6 | – |
@ -230,6 +229,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [navigation-menu](./navigation-menu.md) | 100% | 37 | 33 | 0 | 0 | 0 | 4 | 44 | – |
| [heading](./heading.md) | — | 36 | 0 | 0 | 0 | 0 | 36 | 0 | – |
| [s-text](./s-text.md) | 100% | 30 | 2 | 0 | 0 | 0 | 28 | 3 | – |
| [emoji-picker](./emoji-picker.md) | 100% | 26 | 22 | 0 | 0 | 0 | 4 | 21 | – |
| [pagination](./pagination.md) | 100% | 26 | 24 | 0 | 0 | 0 | 2 | 34 | y |
| [text](./text.md) | — | 24 | 0 | 0 | 0 | 0 | 24 | 0 | – |
| [breadcrumb](./breadcrumb.md) | 100% | 23 | 20 | 0 | 0 | 0 | 3 | 28 | y |

@ -5,56 +5,47 @@
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-23 · **Alcance**: **61%** — 17 de 28 knobs por token público
- **Knobs de apariencia**: 32 — público 17 · privado 0 · global 8 · literal 3 · sistema 4 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 17 pública(s) — `trigger-size`, `trigger-radius`, `trigger-font-size`, `panel-width`, `panel-gap`, `header-gap`, `grid-gap`, `grid-max-block-size`, `option-radius`, `option-font-size`, `footer-gap`, `footer-border-fg`, `preview-gap`, `preview-glyph-size`, `preview-name-font-size`, `preview-name-fg`, `preview-min-block-size`
- **Medido**: 2026-08-23 · **Alcance**: **100%** — 22 de 22 knobs por token público
- **Knobs de apariencia**: 26 — público 22 · privado 0 · global 0 · literal 0 · sistema 4 · excepción 3 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 21 pública(s) — `trigger-size`, `open-trigger-fg`, `panel-width`, `panel-gap`, `header-gap`, `grid-gap`, `grid-max-block-size`, `grid-padding-inline`, `grid-padding-block-end`, `option-radius`, `option-font-size`, `tone-marker-size`, `tone-marker-radius`, `tone-marker-bg`, `footer-gap`, `footer-border-fg`, `preview-gap`, `preview-glyph-size`, `preview-name-font-size`, `preview-name-fg`, `preview-min-block-size`
- **Eje `size`**: no · **ficheros**: `emoji-picker.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (8)
### 1.1 Directo a primitivo global (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `emoji-picker.css:29` | `[data-emoji-picker-trigger]` | `color` | `var(--color-content-secondary)` |
| 2 | `emoji-picker.css:45` | `[data-emoji-picker-trigger][data-state='open']` | `color` | `var(--color-content-primary)` |
| 3 | `emoji-picker.css:95` | `[data-emoji-picker-content] [data-emoji-picker-grid]` | `padding-inline` | `var(--space-0-5)` |
| 4 | `emoji-picker.css:96` | `[data-emoji-picker-content] [data-emoji-picker-grid]` | `padding-block-end` | `var(--space-0-5)` |
| 5 | `emoji-picker.css:124` | `[data-emoji-picker-content] [data-emoji-picker-option][data-tone-capable]::after` | `inline-size` | `var(--space-1)` |
| 6 | `emoji-picker.css:125` | `[data-emoji-picker-content] [data-emoji-picker-option][data-tone-capable]::after` | `block-size` | `var(--space-1)` |
| 7 | `emoji-picker.css:126` | `[data-emoji-picker-content] [data-emoji-picker-option][data-tone-capable]::after` | `border-radius` | `var(--radius-full)` |
| 8 | `emoji-picker.css:127` | `[data-emoji-picker-content] [data-emoji-picker-option][data-tone-capable]::after` | `background` | `color-mix(in srgb, var(--color-content-muted) 50%, transparent)` |
_Ninguno._
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (3)
### 1.3 Literales (0)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `emoji-picker.css:62` | `[data-emoji-picker-content]` | `inline-size` | `100%` |
| 2 | `emoji-picker.css:109` | `[data-emoji-picker-content] [data-emoji-picker-option]` | `line-height` | `1` |
| 3 | `emoji-picker.css:153` | `[data-emoji-picker-preview] > :first-child` | `line-height` | `1` |
_Ninguno._
### 1.4 Excepciones firmadas (0) — fuera del ratio
### 1.4 Excepciones firmadas (3) — fuera del ratio
Literales que llevan su anotación `/* literal: <razón> */` en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
`component-audit`. **Una desviación firmada no es deuda** — se listan para que la
razón se lea, no para acuñarlas.
_Ninguno._
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `emoji-picker.css:64` | `[data-emoji-picker-content]` | `inline-size` | `100%` |
| 2 | `emoji-picker.css:111` | `[data-emoji-picker-content] [data-emoji-picker-option]` | `line-height` | `1` |
| 3 | `emoji-picker.css:155` | `[data-emoji-picker-preview] > :first-child` | `line-height` | `1` |
## 2. Sistema transversal (4) — informativo, fuera del ratio
Un tema los alcanza **a nivel de sistema**, por diseño (recipe-contract §2).
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `emoji-picker.css:36` | `[data-emoji-picker-trigger]:hover:not([data-disabled])` | `background-image` | `linear-gradient(var(--state-hover), var(--state-hover))` |
| 2 | `emoji-picker.css:40` | `[data-emoji-picker-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 3 | `emoji-picker.css:46` | `[data-emoji-picker-trigger][data-state='open']` | `background-image` | `linear-gradient(var(--state-hover), var(--state-hover))` |
| 4 | `emoji-picker.css:51` | `[data-emoji-picker-trigger][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
| 1 | `emoji-picker.css:38` | `[data-emoji-picker-trigger]:hover:not([data-disabled])` | `background-image` | `linear-gradient(var(--state-hover), var(--state-hover))` |
| 2 | `emoji-picker.css:42` | `[data-emoji-picker-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 3 | `emoji-picker.css:48` | `[data-emoji-picker-trigger][data-state='open']` | `background-image` | `linear-gradient(var(--state-hover), var(--state-hover))` |
| 4 | `emoji-picker.css:53` | `[data-emoji-picker-trigger][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
@ -62,7 +53,7 @@ _La receta no declara privados propios en su CSS._
## 4. Propuesta de corrección
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (8)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (0)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
@ -72,14 +63,6 @@ no distingue lo que debería — se marca `⚠`.
| token (`--emoji-picker-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `trigger-fg` | `root` | `var(--color-content-secondary)` | 1 |
| `open-trigger-fg` | `root` | `var(--color-content-primary)` | 1 |
| `content-padding-inline` | `root` | `var(--space-0-5)` | 1 |
| `content-padding-block` | `root` | `var(--space-0-5)` | 1 |
| `content-width` | `root` | `var(--space-1)` | 1 |
| `content-height` | `root` | `var(--space-1)` | 1 |
| `content-radius` | `root` | `var(--radius-full)` | 1 |
| `content-bg` | `root` | `color-mix(in srgb, var(--color-content-muted) 50%, transparent)` | 1 |
### 4.2 Sin nombre mecánico (3)
@ -98,6 +81,60 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**EJECUTADO 2026-08-23 — 61 % → 100 %.** Siete claves nuevas, **tres retiradas
por muertas**, tres literales firmados y cinco adjudicaciones medidas. Contrato
17 → 21. Diff de computed **0 sobre 6.080 valores en 8 estados** (176 nodos con
el panel abierto), capturas 2× antes/después idénticas.
**Las siete, con el nombre que dio el catálogo**: `open-trigger-fg`
(`accordion` lo nombra igual), `grid-padding-inline` + `grid-padding-block-end`
(el eje lógico completo, como en `card-group` / `combobox` / `command`) y las
tres del marcador de tono, la familia `timeline.marker-{size,radius,bg}`. Se
llama `tone-marker-*` y no `marker-*` porque el pie lleva además un selector de
TONO: `marker` a secas sería ambiguo dentro del propio componente.
**TRES CLAVES RETIRADAS — el cromo de reposo del trigger no es suyo.** El
trigger lleva también `data-popover-trigger`, y
`[data-popover-trigger]:not([data-archetype='field-trigger'])` es **(0,2,0)**
contra el (0,1,0) de esta receta: el radio, la tinta y el tamaño de letra los
pinta `popover.css`. Medido: mover `--emoji-picker-trigger-{fg,radius,font-size}`
no cambiaba NADA. Retiradas con sus declaraciones, **0 diffs**, que es la prueba
de que estaban muertas. Es el defecto que `gradient-picker` documentó (36 de sus
45 tokens) y la misma respuesta. Sobreviven dos, y por razones distintas:
`trigger-size` porque popover dimensiona con `height` y no con el par lógico, y
`open-trigger-fg` porque su selector añade `[data-state='open']` y gana por
orden. **`trigger-fg` no llegó a existir**: se acuñó y se midió muerta en el
mismo pase.
**Cinco adjudicaciones, todas medidas forzando sobre nodos reales**: las tres
del marcador de tono (la demo monta **cero** celdas `data-tone-capable`; forzado
el atributo sobre una celda real, el `::after` alcanza — 4px → 37px, 9999px →
37px, y el fondo), `preview-name-fg` (los hijos de la vista previa son `<span>`
desnudos, sin atributo del componente, así que quedan fuera del conjunto
medido) y `open-trigger-fg`, que es el caso interesante.
⚠ **`open-trigger-fg` OSCILA entre corridas**: 17/21, 16/21, 16/21 con el mismo
código. Medido vivo **cuatro veces** —la última replicando paso a paso el
`staticPass` del propio guard, que movió a la primera talla—, así que el token
no miente: lo que falla es el instrumento. La causa: el panel se cierra en algún
punto de la pasada y, durante la animación de SALIDA, el content sigue en el DOM
mientras el trigger ya perdió `data-state='open'` — que es lo que la regla
selecciona; `reopen()` mira el CONTENT, cree que está abierto y no reabre. Probé
un `openMarker` sobre el trigger y **no basta** (el clic de reapertura cae en esa
misma ventana), así que lo retiré en vez de dejar un override que no arregla. El
arreglo real es que `reopen()` espere a la CONDICIÓN en vez de dormir un número
fijo — §13. Su entrada de ledger saldrá **STALE** en las corridas donde alcanza,
y eso está escrito en la propia razón para que nadie la borre por ello.
**Y el instrumento de captura estaba roto desde siempre**: `__shot.ts` tenía el
puerto 5173 cableado, así que cualquier sesión en otro puerto moría con
`ERR_EMPTY_RESPONSE`. Es probablemente por lo que los pasos 6 y 13 del protocolo
se saltaron en 33 componentes. Ahora toma la URL como argumento y un `--open`
opcional para los componentes cuya superficie sólo existe abierta.
**Verificación**: sonda antes/después **0 diffs** (6.080 valores · 8 estados) ·
R-5.4 17/21 con las cinco adjudicadas, exit 0 · `component:audit` PASS · censo
**100 %** · `eidos-lint` 17 morfo-backed / **0 invalid, 0 class-hooks** ·
`rtl:check` 0 · `docs:check` 0 · capturas 2× antes y después, miradas.
<!-- veredicto:end -->

@ -1307,6 +1307,18 @@ Lo que `chart` destapó (2026-08-22):
tiene css, svelte, index y types y nada más). Es el cuarto canónico sin ruta de
demo y además sin documento: cuando se le escriba, le toca su «Talla y tema»
con las 8 claves que tiene desde `83a897b92`.
- **`reopen()` mira el CONTENT, y el estado vive en el TRIGGER** (medido
2026-08-23 en `emoji-picker`). Su `open-trigger-fg` oscila corrida a corrida
(17/21 · 16/21 · 16/21 con el mismo código): el panel se cierra en algún punto
de la pasada y, durante la animación de SALIDA, el content sigue en el DOM
mientras el trigger ya perdió `data-state='open'` — que es lo que la regla
selecciona. `reopen()` ve el content, cree que está abierto y no reabre.
Probado un `openMarker` sobre el trigger: **no basta**, porque el clic de
reapertura cae dentro de esa misma ventana y sus 250 ms de espera no llegan.
El arreglo es que `reopen()` **espere a la condición** (`waitForSelector` del
marcador tras el clic) en vez de dormir un número fijo. Mientras tanto, su
entrada de ledger sale STALE en las corridas donde alcanza: está escrito en la
propia razón para que nadie la borre por eso.
- **R-5.3 no guarda el vocabulario DIMENSIONAL** (medido 2026-08-23 en `badge`).
Cubre la ranura de tinta (`fg` vs `color`) y la posición del modificador, pero
no que una clave POR TALLA se llame `…-height-{k}` y no

@ -1,9 +1,39 @@
/**
* __shot — the 2× capture of PLAN-theming §7.2.6 / §7.4.13.
*
* node scripts/__shot.ts <component> <out.png> [url] [--open[=selector]]
*
* The URL is an ARGUMENT, not a constant: the port lived hard-coded at 5173
* here while the probe and the guard already took it, so every call from a
* 5180 session died with ERR_EMPTY_RESPONSE — which is why the step was easy
* to skip (measured 2026-08-23). `--open` clicks the component's trigger
* first, for the components whose surface only exists open.
*/
import { chromium } from 'playwright';
const [component, out] = process.argv.slice(2);
const args = process.argv.slice(2);
const flags = args.filter((a) => a.startsWith('--'));
const [component, out, urlArg] = args.filter((a) => !a.startsWith('--'));
if (!component || !out) throw new Error('usage: <component> <out.png> [url] [--open[=selector]]');
const url = urlArg ?? `http://localhost:5180/uix/components/${component}`;
const openFlag = flags.find((f) => f.startsWith('--open'));
const b = await chromium.launch();
const p = await b.newPage({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 2 });
await p.goto(`http://localhost:5173/uix/components/${component}`, { waitUntil: 'networkidle' });
try {
await p.goto(url, { waitUntil: 'networkidle', timeout: 20000 });
} catch {
await p.goto(url, { waitUntil: 'load', timeout: 20000 });
}
await p.waitForTimeout(900);
if (openFlag) {
const sel = openFlag.includes('=') ? openFlag.split('=').slice(1).join('=') : `[data-${component}-trigger]`;
const el = p.locator(sel).first();
if (await el.count()) {
await el.click({ timeout: 4000 }).catch(() => {});
await p.waitForTimeout(700);
}
}
const stage = p.locator('[data-uix-stage]').first();
await ((await stage.count()) ? stage : p.locator('body')).screenshot({ path: out });
console.log('shot ->', out);

@ -77,6 +77,29 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'radius-sm': 'only the step in force paints; forced data-rounded=sm → reaches (4px → 37px)',
'radius-lg': 'only the step in force paints; forced data-rounded=lg → reaches (10px → 37px)'
},
'emoji-picker': {
// 2026-08-23. Four keys of the PORTALED panel. The guard writes the
// sentinel on :root and on the host, and :root does cross the portal
// (verified) — what it cannot do is measure a node the demo never mounts
// or one that carries no attribute of the component.
'tone-marker-size':
'the corner dot lives on [data-tone-capable]::after and the demo mounts ZERO tone-capable cells; forced the attribute on a real option -> reaches (4px -> 37px)',
'tone-marker-radius': 'same pseudo, same absent state; forced -> reaches (9999px -> 37px)',
'tone-marker-bg': 'same pseudo, same absent state; forced -> reaches (-> rgb(1,2,3))',
'preview-name-fg':
'the preview children are bare <span>s stamped inline, with no data-emoji-picker-* attr, so they are outside the measured set; read on the last child -> reaches (oklch(0.5032 0 0) -> rgb(1,2,3))',
// NON-DETERMINISTIC, and that is the reason: three runs of the same code
// gave 17/21, 16/21, 16/21 — the panel closes somewhere in the pass and the
// trigger loses [data-state=open], which is what this rule selects on.
// Measured alive FOUR times, the last one replicating the guard's own
// staticPass step by step (moved on the first size): oklch(0.2435 0 0) ->
// rgb(1,2,3), from :root and from the host. An openMarker on the trigger was
// tried and did NOT fix it (still oscillates), so the cause is the reopen
// timing, not the marker. EXPECT THIS ENTRY TO BE FLAGGED STALE on the runs
// where it does reach — do not delete it on that alone. next-features 13.
'open-trigger-fg':
'oscillates run to run (17/21 vs 16/21 on the same code); measured alive four times, including a step-by-step replica of the guard staticPass -> reaches (oklch(0.2435 0 0) -> rgb(1,2,3))'
},
'color-field': {
// 2026-08-23. Thirteen tokens, none of them a lie: the demo boots with a
// value, in `md`, editable, with three formats allowed — so every state

@ -45,14 +45,40 @@ No Air baseline — `emoji-picker` is a new component (chat block v2).
S1 — never classes-as-hooks). The preview glyph + name are stamped inline and
styled via structural child selectors under `[data-emoji-picker-preview]`.
## Tokens (`--emoji-picker-*`)
Declared in `lib/recipes/base.ts`: trigger (`trigger-size/radius/font-size`),
panel (`panel-width/gap`, `header-gap`), grid (`grid-gap/max-block-size`), option
(`option-radius/font-size` — the cell size comes from `--emoji-picker-columns`,
set inline, × the panel width), footer (`footer-gap/border-color`) and preview
(`preview-gap/glyph-size/name-font-size/name-color/min-block-size`). No local
keyframes; logical axes.
## Talla y tema
**21 claves públicas** en `lib/recipes/base.ts`. Sin eje `size`: el tamaño de la
celda sale de `--emoji-picker-columns` (inline) × la anchura del panel. La
pestaña **Tokens** de su demo las lista y las resuelve sobre el escenario.
| Grupo | Claves |
| --- | --- |
| trigger | `trigger-size` · `open-trigger-fg` |
| panel | `panel-width` · `panel-gap` · `header-gap` |
| rejilla | `grid-gap` · `grid-max-block-size` · **`grid-padding-inline`** · **`grid-padding-block-end`** |
| celda | `option-radius` · `option-font-size` |
| marcador de tono | **`tone-marker-size`** · **`tone-marker-radius`** · **`tone-marker-bg`** |
| pie | `footer-gap` · `footer-border-fg` |
| vista previa | `preview-gap` · `preview-glyph-size` · `preview-name-font-size` · `preview-name-fg` · `preview-min-block-size` |
Las **siete en negrita** entraron el 2026-08-23. El marcador de tono es el punto
de esquina de las celdas con variantes de piel (referencia: emoji-mart); se llama
`tone-marker-*` y no `marker-*` porque el pie lleva además un selector de TONO,
y la familia del catálogo es `timeline.marker-{size,radius,bg}`.
⚠ **El cromo de reposo del trigger NO es de este componente, y sus tres claves se
retiraron** (2026-08-23). El trigger lleva también `data-popover-trigger`, y
`[data-popover-trigger]:not([data-archetype='field-trigger'])` es (0,2,0) contra
el (0,1,0) de esta receta: el radio, la tinta y el tamaño de letra los pinta
`popover.css`. Las tres declaraciones que vivían aquí **no movían nada** —
medido, y retirarlas dio 0 diffs, que es la prueba. Es el mismo defecto que
`gradient-picker` documentó (36 de sus 45 tokens). Sobreviven `trigger-size`
(popover dimensiona con `height`, no con el par lógico) y `open-trigger-fg`
(su selector añade `[data-state='open']` y gana por orden).
Los tres literales que quedan son IDENTIDAD, firmados en su declaración: el
panel ES la anchura del popover, y las dos cajas de glifo (`line-height: 1`) no
llevan interlineado.
## Comparativa

@ -24,10 +24,12 @@
block-size: var(--emoji-picker-trigger-size);
padding: 0;
border: 0;
border-radius: var(--emoji-picker-trigger-radius);
/* NO radius / colour / font-size here: this trigger also carries
`data-popover-trigger`, whose rule is (0,2,0) against this one's (0,1,0)
and paints all three. The declarations that lived here moved NOTHING
(measured 2026-08-23) — the same defect `gradient-picker` documented. The
OPEN ink below does win: its selector adds `[data-state='open']`. */
background: transparent;
color: var(--color-content-secondary);
font-size: var(--emoji-picker-trigger-font-size);
cursor: pointer;
transition: background var(--duration-fast) var(--ease-default);
}
@ -42,7 +44,7 @@
}
[data-emoji-picker-trigger][data-state='open'] {
color: var(--color-content-primary);
color: var(--emoji-picker-open-trigger-fg);
background-image: linear-gradient(var(--state-hover), var(--state-hover));
}
@ -59,7 +61,7 @@
gap: var(--emoji-picker-panel-gap);
/* The Popover owns the width (via its `width` prop); the panel fills it so
nothing overflows the popover box. */
inline-size: 100%;
inline-size: 100%; /* literal: identity — the panel IS the popover's width */
min-inline-size: 0;
}
@ -92,8 +94,8 @@
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
padding-inline: var(--space-0-5);
padding-block-end: var(--space-0-5);
padding-inline: var(--emoji-picker-grid-padding-inline);
padding-block-end: var(--emoji-picker-grid-padding-block-end);
}
/* ── Option (double-registered Command.Item → role=option) ──────────────── */
@ -106,7 +108,7 @@
min-inline-size: 0;
border-radius: var(--emoji-picker-option-radius);
font-size: var(--emoji-picker-option-font-size);
line-height: 1;
line-height: 1; /* literal: identity — a glyph cell, the type sets no leading */
cursor: pointer;
user-select: none;
}
@ -121,10 +123,10 @@
position: absolute;
inset-block-end: var(--space-0-5);
inset-inline-end: var(--space-0-5);
inline-size: var(--space-1);
block-size: var(--space-1);
border-radius: var(--radius-full);
background: color-mix(in srgb, var(--color-content-muted) 50%, transparent);
inline-size: var(--emoji-picker-tone-marker-size);
block-size: var(--emoji-picker-tone-marker-size);
border-radius: var(--emoji-picker-tone-marker-radius);
background: var(--emoji-picker-tone-marker-bg);
}
/* ── Footer (preview + skin-tone selector) ──────────────────────────────── */
@ -150,7 +152,7 @@
/* Preview children (stamped inline): the large glyph then the truncated name. */
[data-emoji-picker-preview] > :first-child {
font-size: var(--emoji-picker-preview-glyph-size);
line-height: 1;
line-height: 1; /* literal: identity — the preview glyph, the type sets no leading */
flex: none;
}

@ -1053,15 +1053,19 @@
--chat-typing-dot-gap: var(--space-0-5);
--chat-typing-dot-fg: var(--color-content-muted);
--emoji-picker-trigger-size: var(--size-md-control-height);
--emoji-picker-trigger-radius: var(--radius-md);
--emoji-picker-trigger-font-size: var(--size-md-font-size);
--emoji-picker-open-trigger-fg: var(--color-content-primary);
--emoji-picker-panel-width: 23rem;
--emoji-picker-panel-gap: var(--space-2);
--emoji-picker-header-gap: var(--space-2);
--emoji-picker-grid-gap: var(--space-0-5);
--emoji-picker-grid-max-block-size: min(50vh, 17rem);
--emoji-picker-grid-padding-inline: var(--space-0-5);
--emoji-picker-grid-padding-block-end: var(--space-0-5);
--emoji-picker-option-radius: var(--radius-md);
--emoji-picker-option-font-size: var(--size-lg-font-size);
--emoji-picker-tone-marker-size: var(--space-1);
--emoji-picker-tone-marker-radius: var(--radius-full);
--emoji-picker-tone-marker-bg: color-mix(in srgb, var(--color-content-muted) 50%, transparent);
--emoji-picker-footer-gap: var(--space-2);
--emoji-picker-footer-border-fg: var(--color-neutral-separator);
--emoji-picker-preview-gap: var(--space-2);

@ -999,8 +999,19 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'emoji-picker': {
// ── Trigger (icon button that opens the picker) ───────────────────
'trigger-size': 'var(--size-md-control-height)',
'trigger-radius': 'var(--radius-md)',
'trigger-font-size': 'var(--size-md-font-size)',
// The trigger's OPEN ink. Its rest twin does NOT exist, and neither do the
// radius and the font size that lived here until 2026-08-23: the trigger
// also carries `data-popover-trigger`, and
// `[data-popover-trigger]:not([data-archetype='field-trigger'])` is (0,2,0)
// against this recipe's (0,1,0) — it paints the whole rest chrome. Measured:
// moving `--emoji-picker-trigger-{fg,radius,font-size}` changed NOTHING,
// three declarations that believed they painted. Retired with their CSS,
// 0 diffs — the same defect `gradient-picker` found (36 of its 45 tokens)
// and the same answer. `open-trigger-fg` survives because its selector is
// `[…-trigger][data-state='open']`, (0,2,0), and wins by order; and
// `trigger-size` survives because popover sizes with `height`, not with the
// logical pair.
'open-trigger-fg': 'var(--color-content-primary)',
// ── Panel (portaled; Popover provides surface + depth). The picker owns
// its width as a component token (color-picker precedent: a concrete rem
// sized to ~9 emoji columns), passed to the Popover's `width`. ─────────
@ -1011,9 +1022,19 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'grid-gap': 'var(--space-0-5)',
'grid-max-block-size': 'min(50vh, 17rem)',
// ── Option (one emoji cell — hover/selected from archetype 'item') ─
// The grid's own inset: the scroll container needs room so the focus ring
// of the first and last cells is not clipped by `overflow-y: auto`.
'grid-padding-inline': 'var(--space-0-5)',
'grid-padding-block-end': 'var(--space-0-5)',
'option-radius': 'var(--radius-md)',
'option-font-size': 'var(--size-lg-font-size)',
// ── Footer (preview + skin-tone selector) ─────────────────────────
// The corner dot on a tone-capable cell (reference: emoji-mart). Named
// `tone-marker-*` and not `marker-*` because the footer carries a skin-TONE
// selector too; the family is `timeline.marker-{size,radius,bg}`.
'tone-marker-size': 'var(--space-1)',
'tone-marker-radius': 'var(--radius-full)',
'tone-marker-bg': 'color-mix(in srgb, var(--color-content-muted) 50%, transparent)',
'footer-gap': 'var(--space-2)',
'footer-border-fg': 'var(--color-neutral-separator)',
'preview-gap': 'var(--space-2)',

@ -15,8 +15,9 @@
import { emojiPickerMorfo } from '@/uix/morfo/components/emoji-picker';
import SystemAxes from '../../lib/SystemAxes.svelte';
import TokensPanel from '../../lib/TokensPanel.svelte';
type Tab = 'live' | 'system' | 'api' | 'morfo' | 'a11y';
type Tab = 'live' | 'system' | 'api' | 'morfo' | 'tokens' | 'a11y';
let tab = $state<Tab>('live');
// ── Picker state (app-land) ──────────────────────────────────────────────
@ -139,6 +140,9 @@
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span><span data-uix-tab-count>{partsList.length}p</span>
</button>
<button data-uix-tab data-active={tab === 'tokens'} onclick={() => (tab = 'tokens')}>
Tokens <span data-uix-tab-count>21</span>
</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -252,6 +256,10 @@
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="emoji-picker" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>

Loading…
Cancel
Save

Powered by TurnKey Linux.