diff --git a/docs/audit/theming/README.md b/docs/audit/theming/README.md index 5cbb3a68a..8efac2994 100644 --- a/docs/audit/theming/README.md +++ b/docs/audit/theming/README.md @@ -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 | diff --git a/docs/audit/theming/emoji-picker.md b/docs/audit/theming/emoji-picker.md index 1ffb672d2..3c9dc05d7 100644 --- a/docs/audit/theming/emoji-picker.md +++ b/docs/audit/theming/emoji-picker.md @@ -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: */` 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 `⚠`. -_(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 `` +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. diff --git a/docs/next-features.md b/docs/next-features.md index 6f6579bb5..2fdb809ac 100644 --- a/docs/next-features.md +++ b/docs/next-features.md @@ -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 diff --git a/scripts/__shot.ts b/scripts/__shot.ts index 17ece6b9f..4f2f5d757 100644 --- a/scripts/__shot.ts +++ b/scripts/__shot.ts @@ -1,9 +1,39 @@ +/** + * __shot — the 2× capture of PLAN-theming §7.2.6 / §7.4.13. + * + * node scripts/__shot.ts [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: [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); diff --git a/scripts/theming-sentinel-exceptions.ts b/scripts/theming-sentinel-exceptions.ts index d8ca6e72e..273804440 100644 --- a/scripts/theming-sentinel-exceptions.ts +++ b/scripts/theming-sentinel-exceptions.ts @@ -77,6 +77,29 @@ export const SENTINEL_EXCEPTIONS: Record> = { '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 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 diff --git a/src/uix/eidos/components/emoji-picker/README.md b/src/uix/eidos/components/emoji-picker/README.md index 6b6cca803..8ffbac41f 100644 --- a/src/uix/eidos/components/emoji-picker/README.md +++ b/src/uix/eidos/components/emoji-picker/README.md @@ -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 diff --git a/src/uix/eidos/components/emoji-picker/emoji-picker.css b/src/uix/eidos/components/emoji-picker/emoji-picker.css index 601f571d9..0d658fb5a 100644 --- a/src/uix/eidos/components/emoji-picker/emoji-picker.css +++ b/src/uix/eidos/components/emoji-picker/emoji-picker.css @@ -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; } diff --git a/src/uix/eidos/generated/base.css b/src/uix/eidos/generated/base.css index df64a9517..430cb2b87 100644 --- a/src/uix/eidos/generated/base.css +++ b/src/uix/eidos/generated/base.css @@ -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); diff --git a/src/uix/eidos/lib/recipes/base.ts b/src/uix/eidos/lib/recipes/base.ts index 6d6a0a336..c0803fdeb 100644 --- a/src/uix/eidos/lib/recipes/base.ts +++ b/src/uix/eidos/lib/recipes/base.ts @@ -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)', diff --git a/web/routes/uix/components/emoji-picker/+page.svelte b/web/routes/uix/components/emoji-picker/+page.svelte index 4bbbe01ba..c8c9e38bf 100644 --- a/web/routes/uix/components/emoji-picker/+page.svelte +++ b/web/routes/uix/components/emoji-picker/+page.svelte @@ -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('live'); // ── Picker state (app-land) ────────────────────────────────────────────── @@ -139,6 +140,9 @@ + @@ -252,6 +256,10 @@ {/if} + {#if tab === 'tokens'} + + {/if} + {#if tab === 'a11y'}

Accessibility