feat(gradient-picker): temable — y su trigger resultó estar muerto entero

Quinto del bloque F2-A.

  censo    0 % → 88 % · global 42 %
  computed 609 valores en 8 estados: 0 diffs

**El hallazgo llegó por el centinela: 36 de 45 tokens no movían nada.** Con esa
cifra no se sigue adelante. El trigger es un POPOVER TRIGGER, y `popover.css` le
pinta el cromo entero —altura, padding inline, tipografía, color, fondo, borde,
radio, hover y foco— desde
`[data-popover-trigger]:not([data-archetype='field-trigger'])`, que gana a
`[data-gradient-picker-trigger]` en base (0,2,0 contra 0,1,0) y en los estados
(0,3,0 contra 0,2,0).

Esta receta re-declaraba TODO eso y nada pintaba: la altura, el padding, el
tamaño de letra y el radio venían de popover, y con valores distintos —12px
contra el `space-2-5` de la receta, 14px contra su `font-size-md`—. Llevaba
así desde que el trigger se volvió trigger de popover.

Así que no se tokeniza: **se retira**. Un token sobre una declaración muerta es
un token que miente, y este eje ya retiró uno por lo mismo en `table`.
Retirarlo dio **0 diffs**, que es la prueba de que estaba muerto. Un tema viste
este trigger por el contrato de POPOVER, que es composición funcionando.

Lo que sí es del picker se queda y alcanza (verificado a mano, porque el chip
usa un hook de CLASE que el centinela no ve): el `gap` de la fila, el chip del
degradado (tamaño por talla, radio, borde), las tres anchuras del panel, la
lista de paradas y la rejilla de presets.

**Correcciones al veredicto**, ambas medidas: `--gp-current-gradient` NO lo
estampa el wrapper sino SOMA (`gradient-picker-provider.svelte.ts:197`), así
que renombrarlo toca otra capa y no es de este commit; y no es un knob de tema
sino un canal de valor —el degradado que eligió el usuario—, sobre el que un
tema no tiene nada que decir.

Las tres anchuras del panel se quedan como tokens planos leídos por tres
reglas, no como cascada del TSC: el content va por PORTAL y dimensiona con
`data-picker-size` (el atributo de picker-shell), y el vocabulario de scopes no
tiene palabra para el atributo de otro componente.

Al registro (`next-features.md` §13) van cuatro incidencias nuevas: que un
componente compuesto pueda tener el cromo entero muerto sin que nada avise
—merece guard, y el instrumento ya existe—, el nombre abreviado en soma, los
dos hooks por CLASE que `eidos-lint` cuenta, y el foco opaco del trigger contra
la mezcla suave del sistema.

  audit PASS · eidos-lint 0 invalid · rtl 0 · docs 0 · check 0 en tocados ·
  suite: sólo el rojo conocido

Demo con tab Tokens: 25 filas, 0 sin computar.

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

@ -10,9 +10,9 @@
- **Medido**: 2026-08-20 · **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**: **41%** — 2003 de 4866 knobs pasan por un token público del componente
- **Reparto**: público 2003 · privado 731 · global 1539 · literal 593 · sistema transversal 337 _(fuera del ratio)_
- **Sin token público propio**: 53 · **alcance < 20 %**: 47 · **alcance 100 %**: 7 · **con eje `size`**: 75
- **Alcance global**: **42%** — 2026 de 4857 knobs pasan por un token público del componente
- **Reparto**: público 2026 · privado 724 · global 1518 · literal 589 · sistema transversal 335 _(fuera del ratio)_
- **Sin token público propio**: 52 · **alcance < 20 %**: 46 · **alcance 100 %**: 7 · **con eje `size`**: 74
## Cómo se lee
@ -82,21 +82,21 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma
| 10 | [time-picker](./time-picker.md) | 47 | 18% | 59 | 16 |
| 11 | [color-picker](./color-picker.md) | 43 | 46% | 82 | 54 |
| 12 | [field-langs](./field-langs.md) | 39 | 0% | 41 | 0 |
| 13 | [gradient-picker](./gradient-picker.md) | 35 | 0% | 38 | 0 |
| 14 | [listbox](./listbox.md) | 35 | 0% | 38 | 0 |
| 15 | [field](./field.md) | 33 | 51% | 73 | 60 |
| 16 | [feed](./feed.md) | 32 | 0% | 33 | 0 |
| 17 | [carousel](./carousel.md) | 31 | 0% | 36 | 0 |
| 18 | [date-range-picker](./date-range-picker.md) | 30 | 40% | 53 | 22 |
| 19 | [natural-time-picker](./natural-time-picker.md) | 30 | 61% | 77 | 62 |
| 20 | [popover](./popover.md) | 28 | 42% | 51 | 36 |
| 13 | [listbox](./listbox.md) | 35 | 0% | 38 | 0 |
| 14 | [field](./field.md) | 33 | 51% | 73 | 60 |
| 15 | [feed](./feed.md) | 32 | 0% | 33 | 0 |
| 16 | [carousel](./carousel.md) | 31 | 0% | 36 | 0 |
| 17 | [date-range-picker](./date-range-picker.md) | 30 | 40% | 53 | 22 |
| 18 | [natural-time-picker](./natural-time-picker.md) | 30 | 61% | 77 | 62 |
| 19 | [popover](./popover.md) | 28 | 42% | 51 | 36 |
| 20 | [radio-cards](./radio-cards.md) | 28 | 60% | 72 | 39 |
## Sin ningún token público propio (53)
## Sin ningún token público propio (52)
Ni una clave pública en `lib/recipes/base.ts` — alguno tiene privados forward
(`_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) · [`card-group`](./card-group.md) · [`carousel`](./carousel.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`code-block`](./code-block.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`feed`](./feed.md) · [`field-langs`](./field-langs.md) · [`gradient-picker`](./gradient-picker.md) · [`grid-list`](./grid-list.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`listbox`](./listbox.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) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.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) · [`textarea`](./textarea.md) · [`tree-view`](./tree-view.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) · [`card-group`](./card-group.md) · [`carousel`](./carousel.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`code-block`](./code-block.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`feed`](./feed.md) · [`field-langs`](./field-langs.md) · [`grid-list`](./grid-list.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`listbox`](./listbox.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) · [`picker-shell`](./picker-shell.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`spinner`](./spinner.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) · [`textarea`](./textarea.md) · [`tree-view`](./tree-view.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)
@ -108,7 +108,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [month-grid](./month-grid.md) | 0% | 61 | 0 | 13 | 40 | 5 | 3 | 0 | y |
| [year-grid](./year-grid.md) | 0% | 61 | 0 | 13 | 40 | 5 | 3 | 0 | y |
| [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – |
| [gradient-picker](./gradient-picker.md) | 0% | 38 | 0 | 7 | 23 | 5 | 3 | 0 | y |
| [listbox](./listbox.md) | 0% | 38 | 0 | 9 | 23 | 3 | 3 | 0 | – |
| [carousel](./carousel.md) | 0% | 36 | 0 | 14 | 12 | 5 | 5 | 0 | y |
| [feed](./feed.md) | 0% | 33 | 0 | 13 | 16 | 3 | 1 | 0 | y |
@ -238,6 +237,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [table](./table.md) | 86% | 46 | 37 | 3 | 0 | 3 | 3 | 45 | – |
| [media-player](./media-player.md) | 88% | 49 | 43 | 0 | 0 | 6 | 0 | 61 | – |
| [tabs](./tabs.md) | 88% | 76 | 66 | 6 | 2 | 1 | 1 | 79 | y |
| [gradient-picker](./gradient-picker.md) | 88% | 27 | 23 | 0 | 2 | 1 | 1 | 25 | – |
| [chat-typing](./chat-typing.md) | 89% | 9 | 8 | 0 | 1 | 0 | 0 | 7 | – |
| [breadcrumb](./breadcrumb.md) | 90% | 23 | 18 | 2 | 0 | 0 | 3 | 28 | y |
| [collapsible](./collapsible.md) | 90% | 12 | 9 | 0 | 1 | 0 | 2 | 11 | – |

@ -5,88 +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-20 · **Alcance**: **0%** — 0 de 35 knobs por token público
- **Knobs de apariencia**: 38 — público 0 · privado 7 · global 23 · literal 5 · sistema 3 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`**
- **Eje `size`**: sí · **ficheros**: `gradient-picker.css`
- **Medido**: 2026-08-20 · **Alcance**: **88%** — 23 de 26 knobs por token público
- **Knobs de apariencia**: 27 — público 23 · privado 0 · global 2 · literal 1 · sistema 1 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 25 pública(s) — `chip-size-sm`, `chip-size-md`, `chip-size-lg`, `chip-size`, `gap`, `trigger-gap`, `border-width`, `chip-radius`, `chip-border`, `content-width-sm`, `content-width-md`, `content-width-lg`, `stoplist-max-block`, `stoplist-scrollbar-size`, `stoplist-scrollbar-inset`, `presets-panel-width`, `presets-gap`, `presets-min-column`, `preset-height`, `preset-radius`, `preset-border`, `hover-preset-border`, `selected-preset-border`, `selected-preset-outline-offset`, `actions-gap`
- **Eje `size`**: no · **ficheros**: `gradient-picker.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (23)
### 1.1 Directo a primitivo global (2)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `gradient-picker.css:21` | `[data-gradient-picker]` | `gap` | `var(--space-1-5)` |
| 2 | `gradient-picker.css:43` | `[data-gradient-picker-trigger]` | `gap` | `var(--space-2)` |
| 3 | `gradient-picker.css:47` | `[data-gradient-picker-trigger]` | `color` | `var(--color-content-primary)` |
| 4 | `gradient-picker.css:48` | `[data-gradient-picker-trigger]` | `background` | `var(--color-surface-default)` |
| 5 | `gradient-picker.css:49` | `[data-gradient-picker-trigger]` | `border` | `var(--border-width) solid var(--color-border-default)` |
| 6 | `gradient-picker.css:50` | `[data-gradient-picker-trigger]` | `border-radius` | `var(--radius-md)` |
| 7 | `gradient-picker.css:57` | `[data-gradient-picker-trigger]:hover` | `background` | `var(--color-surface-overlay)` |
| 8 | `gradient-picker.css:58` | `[data-gradient-picker-trigger]:hover` | `border-color` | `var(--color-border-strong)` |
| 9 | `gradient-picker.css:61` | `[data-gradient-picker-trigger]:focus-visible` | `outline` | `2px solid var(--color-primary-solid)` |
| 10 | `gradient-picker.css:73` | `.gradient-picker-trigger-swatch` | `border-radius` | `var(--radius-sm)` |
| 11 | `gradient-picker.css:74` | `.gradient-picker-trigger-swatch` | `border` | `var(--border-width) solid var(--color-border-subtle)` |
| 12 | `gradient-picker.css:75` | `.gradient-picker-trigger-swatch` | `background` | `var(--gp-current-gradient, transparent)` |
| 13 | `gradient-picker.css:85` | `[data-gradient-picker-value-swatch]` | `border-radius` | `var(--radius-sm)` |
| 14 | `gradient-picker.css:86` | `[data-gradient-picker-value-swatch]` | `border` | `var(--border-width) solid var(--color-border-subtle)` |
| 15 | `gradient-picker.css:87` | `[data-gradient-picker-value-swatch]` | `background` | `var(--gp-current-gradient, transparent)` |
| 16 | `gradient-picker.css:125` | `[data-gradient-picker-stoplist-scroll] [data-scroll-area-viewport]` | `padding-inline-end` | `var(--space-3)` |
| 17 | `gradient-picker.css:136` | `[data-gradient-picker-presets]` | `gap` | `var(--space-2)` |
| 18 | `gradient-picker.css:141` | `[data-gradient-picker-preset]` | `block-size` | `calc(32px * var(--scaling, 1))` |
| 19 | `gradient-picker.css:143` | `[data-gradient-picker-preset]` | `border` | `var(--border-width) solid var(--color-border-subtle)` |
| 20 | `gradient-picker.css:144` | `[data-gradient-picker-preset]` | `border-radius` | `var(--radius-md)` |
| 21 | `gradient-picker.css:148` | `[data-gradient-picker-preset]:hover` | `border-color` | `var(--color-border-strong)` |
| 22 | `gradient-picker.css:155` | `[data-gradient-picker-preset][data-selected]` | `border-color` | `var(--color-primary-border)` |
| 23 | `gradient-picker.css:164` | `[data-gradient-picker-actions-row]` | `gap` | `var(--space-2)` |
### 1.2 A través de un privado (7)
| 1 | `gradient-picker.css:52` | `.gradient-picker-trigger-swatch` | `background` | `var(--gp-current-gradient, transparent)` |
| 2 | `gradient-picker.css:64` | `[data-gradient-picker-value-swatch]` | `background` | `var(--gp-current-gradient, transparent)` |
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `gradient-picker.css:44` | `[data-gradient-picker-trigger]` | `block-size` | `var(--_gradient-picker-trigger-height)` |
| 2 | `gradient-picker.css:45` | `[data-gradient-picker-trigger]` | `padding-inline` | `var(--_gradient-picker-trigger-padding-inline)` |
| 3 | `gradient-picker.css:46` | `[data-gradient-picker-trigger]` | `font-size` | `var(--_gradient-picker-trigger-font-size)` |
| 4 | `gradient-picker.css:71` | `.gradient-picker-trigger-swatch` | `inline-size` | `var(--_gradient-picker-chip-size)` |
| 5 | `gradient-picker.css:72` | `.gradient-picker-trigger-swatch` | `block-size` | `var(--_gradient-picker-chip-size)` |
| 6 | `gradient-picker.css:83` | `[data-gradient-picker-value-swatch]` | `inline-size` | `var(--_gradient-picker-chip-size)` |
| 7 | `gradient-picker.css:84` | `[data-gradient-picker-value-swatch]` | `block-size` | `var(--_gradient-picker-chip-size)` |
### 1.2 A través de un privado (0)
_Ninguno._
### 1.3 Literales (5)
### 1.3 Literales (1)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `gradient-picker.css:93` | `[data-gradient-picker-content]` | `inline-size` | `20rem` |
| 2 | `gradient-picker.css:96` | `[data-gradient-picker-content][data-picker-size='sm']` | `inline-size` | `18rem` |
| 3 | `gradient-picker.css:99` | `[data-gradient-picker-content][data-picker-size='lg']` | `inline-size` | `22rem` |
| 4 | `gradient-picker.css:130` | `[data-gradient-picker-presets-panel]` | `inline-size` | `15rem` |
| 5 | `gradient-picker.css:140` | `[data-gradient-picker-preset]` | `inline-size` | `100%` |
| 1 | `gradient-picker.css:117` | `[data-gradient-picker-preset]` | `inline-size` | `100%` |
## 2. Sistema transversal (3) — informativo, fuera del ratio
## 2. Sistema transversal (1) — 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 | `gradient-picker.css:65` | `[data-gradient-picker-trigger][data-disabled]` | `opacity` | `var(--opacity-disabled)` |
| 2 | `gradient-picker.css:151` | `[data-gradient-picker-preset]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 3 | `gradient-picker.css:156` | `[data-gradient-picker-preset][data-selected]` | `outline` | `var(--focus-ring-width) solid var(--color-primary-border)` |
| 1 | `gradient-picker.css:128` | `[data-gradient-picker-preset]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
## 3. Privados de la receta — ¿de dónde sale su valor?
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_gradient-picker-trigger-height` | 3 | `var(--control-height-md)`, `var(--control-height-sm)`, `var(--control-height-lg)` | global | no |
| `--_gradient-picker-trigger-padding-inline` | 3 | `var(--space-2-5)`, `var(--space-2)`, `var(--space-3)` | global | no |
| `--_gradient-picker-trigger-font-size` | 3 | `var(--font-size-md)`, `var(--font-size-sm)`, `var(--font-size-lg)` | global | no |
| `--_gradient-picker-chip-size` | 3 | `calc(var(--control-height-md) - var(--space-2))`, `calc(var(--control-height-sm) - var(--space-2))`, `calc(var(--control-height-lg) - var(--space-2))` | global | no |
_La receta no declara privados propios en su CSS._
## 4. Propuesta de corrección
- **Consume la capa compartida `picker-shell`.** Un eje que la capa posee se consume como `var(--_x, var(--x))`; el consumidor **no acuña** `--gradient-picker-{eje}` para él — sería un vocabulario paralelo (README de `eidos/components`, «Capas compartidas» regla 2).
- **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).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (41)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (2)
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
@ -96,61 +55,12 @@ no distingue lo que debería — se marca `⚠`.
| token (`--gradient-picker-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `content-width` | `root` | ⚠ `20rem` / `18rem` / `22rem` | 3 |
| `gap` | `root` | `var(--space-1-5)` | 1 |
| `trigger-gap` | `root` | `var(--space-2)` | 1 |
| `trigger-height-md` | `size:md` | `var(--control-height-md)` | 1 |
| `trigger-height-sm` | `size:sm` | `var(--control-height-sm)` | 1 |
| `trigger-height-lg` | `size:lg` | `var(--control-height-lg)` | 1 |
| `trigger-padding-inline-md` | `size:md` | `var(--space-2-5)` | 1 |
| `trigger-padding-inline-sm` | `size:sm` | `var(--space-2)` | 1 |
| `trigger-padding-inline-lg` | `size:lg` | `var(--space-3)` | 1 |
| `trigger-font-size-md` | `size:md` | `var(--font-size-md)` | 1 |
| `trigger-font-size-sm` | `size:sm` | `var(--font-size-sm)` | 1 |
| `trigger-font-size-lg` | `size:lg` | `var(--font-size-lg)` | 1 |
| `trigger-fg` | `root` | `var(--color-content-primary)` | 1 |
| `trigger-bg` | `root` | `var(--color-surface-default)` | 1 |
| `trigger-radius` | `root` | `var(--radius-md)` | 1 |
| `hover-trigger-bg` | `root` | `var(--color-surface-overlay)` | 1 |
| `hover-trigger-border` | `root` | `var(--color-border-strong)` | 1 |
| `width-md` | `size:md` | `calc(var(--control-height-md) - var(--space-2))` | 1 |
| `width-sm` | `size:sm` | `calc(var(--control-height-sm) - var(--space-2))` | 1 |
| `width-lg` | `size:lg` | `calc(var(--control-height-lg) - var(--space-2))` | 1 |
| `height-md` | `size:md` | `calc(var(--control-height-md) - var(--space-2))` | 1 |
| `height-sm` | `size:sm` | `calc(var(--control-height-sm) - var(--space-2))` | 1 |
| `height-lg` | `size:lg` | `calc(var(--control-height-lg) - var(--space-2))` | 1 |
| `radius` | `root` | `var(--radius-sm)` | 1 |
| `bg` | `root` | `var(--gp-current-gradient, transparent)` | 1 |
| `value-swatch-width-md` | `size:md` | `calc(var(--control-height-md) - var(--space-2))` | 1 |
| `value-swatch-width-sm` | `size:sm` | `calc(var(--control-height-sm) - var(--space-2))` | 1 |
| `value-swatch-width-lg` | `size:lg` | `calc(var(--control-height-lg) - var(--space-2))` | 1 |
| `value-swatch-height-md` | `size:md` | `calc(var(--control-height-md) - var(--space-2))` | 1 |
| `value-swatch-height-sm` | `size:sm` | `calc(var(--control-height-sm) - var(--space-2))` | 1 |
| `value-swatch-height-lg` | `size:lg` | `calc(var(--control-height-lg) - var(--space-2))` | 1 |
| `value-swatch-radius` | `root` | `var(--radius-sm)` | 1 |
| `value-swatch-bg` | `root` | `var(--gp-current-gradient, transparent)` | 1 |
| `stoplist-scroll-padding-inline` | `root` | `var(--space-3)` | 1 |
| `presets-panel-width` | `root` | `15rem` | 1 |
| `presets-gap` | `root` | `var(--space-2)` | 1 |
| `preset-height` | `root` | `calc(32px * var(--scaling, 1))` | 1 |
| `preset-radius` | `root` | `var(--radius-md)` | 1 |
| `hover-preset-border` | `root` | `var(--color-border-strong)` | 1 |
| `selected-preset-border` | `root` | `var(--color-primary-border)` | 1 |
| `actions-row-gap` | `root` | `var(--space-2)` | 1 |
### 4.2 Sin nombre mecánico (6)
- **⚠ decisión: `border` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 4: `border`.
- **foco: lo posee el sistema (`--focus-ring-*`, theming §32) — no acuñar token propio** — 1: `outline`.
- **⚠ decisión: `100%` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 1: `inline-size`.
### 4.3 Avisos sobre los tokens propuestos (5)
### 4.2 Sin nombre mecánico (1)
- **el privado `--_gradient-picker-trigger-height` debe pasar a leer este público (o desaparecer)** — `--gradient-picker-trigger-height-md`, `--gradient-picker-trigger-height-sm`, `--gradient-picker-trigger-height-lg`
- **el privado `--_gradient-picker-trigger-padding-inline` debe pasar a leer este público (o desaparecer)** — `--gradient-picker-trigger-padding-inline-md`, `--gradient-picker-trigger-padding-inline-sm`, `--gradient-picker-trigger-padding-inline-lg`
- **el privado `--_gradient-picker-trigger-font-size` debe pasar a leer este público (o desaparecer)** — `--gradient-picker-trigger-font-size-md`, `--gradient-picker-trigger-font-size-sm`, `--gradient-picker-trigger-font-size-lg`
- **⚠ decisión: el valor es una expresión — el token puede llevar la expresión entera o sólo su término variable · el privado `--_gradient-picker-chip-size` debe pasar a leer este público (o desaparecer)** — `--gradient-picker-width-md`, `--gradient-picker-width-sm`, `--gradient-picker-width-lg`, `--gradient-picker-height-md`, `--gradient-picker-height-sm`, `--gradient-picker-height-lg`, `--gradient-picker-value-swatch-width-md`, `--gradient-picker-value-swatch-width-sm`, `--gradient-picker-value-swatch-width-lg`, `--gradient-picker-value-swatch-height-md`, `--gradient-picker-value-swatch-height-sm`, `--gradient-picker-value-swatch-height-lg`
- **⚠ decisión: el valor es una expresión — el token puede llevar la expresión entera o sólo su término variable** — `--gradient-picker-preset-height`
- **⚠ decisión: `100%` es un valor identidad o geometría de layout, no un knob de tema — el perímetro de «knob» es D-TH.2, sin firmar** — 1: `inline-size`.
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)

@ -457,6 +457,27 @@ existiera. Se anotan porque el eje entero se apoya en esas mediciones.
`date-range-picker`.
- **`date-range-picker` no expone `kind='month'` / `'year'` en su demo**
(norma N-6), así que la mitad de su API no se ve ni se mide.
- **Un componente COMPUESTO puede tener su cromo entero muerto sin que nada
avise.** `gradient-picker` re-declaraba altura, padding, tipografía, color,
fondo, borde, radio, hover y foco de su trigger, y NADA de ello pintaba:
`popover.css` gana con `[data-popover-trigger]:not([data-archetype='field-trigger'])`
(0,2,0 contra 0,1,0). Lo cazó el centinela —36 de 45 tokens sin efecto— y
retirarlo dio 0 diffs. **Merece un guard**: una declaración de receta que
ninguna cascada puede ganar es deuda muerta, y hoy sólo aparece si alguien
pasa el centinela por ese componente. El instrumento para detectarlo ya
existe (`__theming-sentinel.ts`); falta convertirlo en regla.
- **`--gp-current-gradient` es un nombre ABREVIADO en SOMA.** Lo estampa
`gradient-picker-provider.svelte.ts` (no el wrapper de eidos, como decía la
ficha), y viola theming §6 r5. No es knob de tema —es un canal de valor— pero
el nombre es deuda; renombrarlo toca soma, otra capa.
- **Hooks por CLASE en una receta de eidos.** `gradient-picker` usa
`.gradient-picker-trigger-swatch` y `.gradient-picker-trigger-label` en vez
de `data-*`; `eidos-lint` los cuenta (`class-hooks: 2`) y el centinela no los
ve, porque filtra nodos por atributo.
- **El foco del trigger de `gradient-picker` es opaco** (`--color-primary-solid`)
mientras el resto del catálogo lleva la mezcla suave del sistema
(`--focus-ring-color`, 52 %). Quedó sin tocar porque alinearlo mueve píxel —
y de hecho quedó sin token, porque la regla entera está muerta bajo popover.
- **La demo del bloque F2-A ya enseña los tokens** (`TokensPanel`), pero los
~155 componentes sin contrato siguen sin superficie donde verlos hasta que se
tokenicen.

@ -31,30 +31,31 @@ sus dos consumidores reales).
```
`<GradientPicker.Content />` renders a default body — the GradientBuilder editor
+ the `Clear / Cancel / Save` footer — so it works out of the box. Pass children
to the Content to customise it.
- the `Clear / Cancel / Save` footer — so it works out of the box. Pass children
to the Content to customise it.
## Parts
| Part | Composes | Notes |
| ---------------------------- | ---------------------- | ---------------------------------------------------- |
| `Provider` | soma Provider | Sets a size/variant visual context. |
| `Trigger` | soma Trigger + Popover | Field-shaped pill with the gradient chip. |
| `ValueSwatch` | soma ValueSwatch | Standalone gradient chip (paints `--gp-current-gradient`). |
| `Content` | Popover + PickerShell | The floating editor: GradientBuilder + footer. |
| `Footer` / `Clear` / `Cancel` / `Close` | PickerShell | The shared footer actions (read `pickerShellContext`).|
| `Portal` / `Anchor` / `Overlay` / `Arrow` | Popover | Re-exports. |
| Part | Composes | Notes |
| ----------------------------------------- | ---------------------- | ---------------------------------------------------------- |
| `Provider` | soma Provider | Sets a size/variant visual context. |
| `Trigger` | soma Trigger + Popover | Field-shaped pill with the gradient chip. |
| `ValueSwatch` | soma ValueSwatch | Standalone gradient chip (paints `--gp-current-gradient`). |
| `Content` | Popover + PickerShell | The floating editor: GradientBuilder + footer. |
| `Footer` / `Clear` / `Cancel` / `Close` | PickerShell | The shared footer actions (read `pickerShellContext`). |
| `Portal` / `Anchor` / `Overlay` / `Arrow` | Popover | Re-exports. |
## Props
| Prop | Type | Default | Description |
| --------- | ----------------------------------- | ----------- | ---------------------------------------- |
| `value` | `Gradient` | 2-stop | The committed gradient. Bindable. |
| `open` | `boolean` | `false` | Popover open state. Bindable. |
| `mode` | `'inline' \| 'modal'` | `'inline'` | Footer layout (Clear/Cancel vs Cancel/Save). |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Trigger / popover / editor size. |
| `variant` | `'surface' \| 'outline' \| 'ghost'` | `'surface'` | Trigger framing. |
| `disabled`| `boolean` | `false` | Disables the trigger. |
| Prop | Type | Default | Description |
| ---------- | ----------------------------------- | ----------- | -------------------------------------------- |
| `value` | `Gradient` | 2-stop | The committed gradient. Bindable. |
| `open` | `boolean` | `false` | Popover open state. Bindable. |
| `mode` | `'inline' \| 'modal'` | `'inline'` | Footer layout (Clear/Cancel vs Cancel/Save). |
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Trigger / popover / editor size. |
| `variant` | `'surface' \| 'outline' \| 'ghost'` | `'surface'` | Trigger framing. |
| `disabled` | `boolean` | `false` | Disables the trigger. |
## The value bridge (the one subtle bit)
@ -73,13 +74,13 @@ standalone swatch paint `--gp-current-gradient` (stamped inline by soma).
## Comparativa
| Capacidad | UIX | Figma (fill popover) | Libs web de color con tab gradiente |
| --- | --- | --- | --- |
| Form control con valor comprometido (`bind:value`) | **✓** | No (edición in-place) | Parcial (sin transacción) |
| Footer transaccional (Clear/Cancel/Save + revert) | ✓ (PickerShell) | No | No |
| Editor accesible por teclado dentro | ✓ (builder apg slider) | No | No |
| Chip de valor vivo en el trigger | ✓ (`--gp-current-gradient`) | ✓ | Parcial |
| Overlay delegado al sistema | ✓ (Popover §2) | Propio | Propio |
| Capacidad | UIX | Figma (fill popover) | Libs web de color con tab gradiente |
| -------------------------------------------------- | --------------------------- | --------------------- | ----------------------------------- |
| Form control con valor comprometido (`bind:value`) | **✓** | No (edición in-place) | Parcial (sin transacción) |
| Footer transaccional (Clear/Cancel/Save + revert) | ✓ (PickerShell) | No | No |
| Editor accesible por teclado dentro | ✓ (builder apg slider) | No | No |
| Chip de valor vivo en el trigger | ✓ (`--gp-current-gradient`) | ✓ | Parcial |
| Overlay delegado al sistema | ✓ (Popover §2) | Propio | Propio |
Referencias: [`../gradient-builder/README.md`](../gradient-builder/README.md)
(el editor y su research) · patrón §2 en [`../date-picker/`](../date-picker/).
@ -99,13 +100,53 @@ Referencias: [`../gradient-builder/README.md`](../gradient-builder/README.md)
resets externos bajan por `$effect` con guard — sin bucle.
- **Sin prop `color`** (como el builder).
## Talla y tema
Contrato en `lib/recipes/base.ts > gradient-picker` (25 claves). Alcance del
censo: **88 %** (era 0 % antes del 2026-08-21), sin privados.
### El trigger NO se tematiza desde aquí, y no es un olvido
Medido con centinela sobre cada knob: el trigger es un **popover trigger**, y
`popover.css` le pinta el cromo entero —altura, padding inline, tipografía,
color, fondo, borde, radio, hover y foco— desde
`[data-popover-trigger]:not([data-archetype='field-trigger'])`, que pesa más
que cualquier cosa que este fichero diga sobre `[data-gradient-picker-trigger]`
(0,2,0 contra 0,1,0; 0,3,0 contra 0,2,0 en los estados).
Esta receta re-declaraba todo eso y **nada de ello pintaba**: la altura, el
padding, el tamaño de letra y el radio venían de popover, y con valores
distintos (12px contra el `space-2-5` de la receta, 14px contra su
`font-size-md`). Era código muerto, así que se retiró en vez de tokenizarlo —
un token sobre una declaración muerta es un token que miente, y retirarlo dio
**0 diffs** en 609 valores, que es la prueba de que estaba muerto.
**Un tema viste este trigger por el contrato de POPOVER** (`--popover-trigger-*`).
Es composición funcionando: el trigger ES un popover trigger. Lo que sí es
propio del picker —el `gap` de la fila y el chip del degradado— vive aquí y
alcanza (verificado a mano: el chip usa un hook de CLASE y el centinela no lo
ve).
### Lo demás
Las tres anchuras del panel (`content-width-{sm,md,lg}`) se quedan como tokens
planos leídos por tres reglas, no como cascada del TSC: el content viaja por
PORTAL y dimensiona con `data-picker-size` —el atributo de `picker-shell`—, y
el vocabulario de scopes no tiene palabra para el atributo de otro componente.
**El relleno del chip no es un knob**: soma estampa el degradado del usuario
inline (`--gp-current-gradient`, un canal de valor), y un tema no tiene nada
que decir sobre lo que el usuario eligió. Que ese nombre esté abreviado
—contra theming §6 r5— es deuda de SOMA, no de aquí; anotada en
`next-features.md` §13.
## Gaps
| Gap | Disposición | Detalle |
| --- | --- | --- |
| Persistencia de presets guardados | **descartar** (nivel componente) | Como ColorPicker: los presets/save son del CONSUMIDOR; el panel de presets ya compone. |
| Migración a `PickerProvider<TValue>` (deferValue genérico) | **diferir** | Task #31 de la memoria — pasada conjunta de los pickers POST audit-fix. |
| Demo v2 (D-1.5/D-3.1 de la máquina) | **implementar** | Pase de demos de esta iniciativa (ver sesión S9). |
| Gap | Disposición | Detalle |
| ---------------------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------- |
| Persistencia de presets guardados | **descartar** (nivel componente) | Como ColorPicker: los presets/save son del CONSUMIDOR; el panel de presets ya compone. |
| Migración a `PickerProvider<TValue>` (deferValue genérico) | **diferir** | Task #31 de la memoria — pasada conjunta de los pickers POST audit-fix. |
| Demo v2 (D-1.5/D-3.1 de la máquina) | **implementar** | Pase de demos de esta iniciativa (ver sesión S9). |
## Notes

@ -1,77 +1,54 @@
/* GradientPicker — visual layer.
*
* Self-contained recipe (gradient-picker is on the active dev track, so it
* does NOT register tokens in recipes/base.ts yet). The trigger is a
* Contract in `recipes/base.ts > gradient-picker` (2026-08-20). The trigger is a
* field-shaped pill carrying a gradient preview chip; the popover content +
* editor layout reuse the shared Popover + PickerShell + GradientBuilder
* recipes. Internal `--_gradient-picker-*` tokens derive from global tokens.
* recipes; every knob of its own is a public `--gradient-picker-*`.
*
* Soma stamps `--gp-current-gradient` inline on the trigger + value swatch, so
* the chip paints purely from CSS — soma owns the value, eidos owns the paint.
*/
[data-gradient-picker] {
--_gradient-picker-trigger-height: var(--control-height-md);
--_gradient-picker-trigger-padding-inline: var(--space-2-5);
--_gradient-picker-trigger-font-size: var(--font-size-md);
--_gradient-picker-chip-size: calc(var(--control-height-md) - var(--space-2));
/* The per-size cascade is emitted by the TSC from `recipes/base.ts`; writing
`[data-size]` blocks here too would paint the OLD values over the new
tokens and the computed diff would read 0 through the old path. */
display: inline-flex;
flex-direction: column;
gap: var(--space-1-5);
gap: var(--gradient-picker-gap);
min-inline-size: 0;
}
[data-gradient-picker][data-size='sm'] {
--_gradient-picker-trigger-height: var(--control-height-sm);
--_gradient-picker-trigger-padding-inline: var(--space-2);
--_gradient-picker-trigger-font-size: var(--font-size-sm);
--_gradient-picker-chip-size: calc(var(--control-height-sm) - var(--space-2));
}
[data-gradient-picker][data-size='lg'] {
--_gradient-picker-trigger-height: var(--control-height-lg);
--_gradient-picker-trigger-padding-inline: var(--space-3);
--_gradient-picker-trigger-font-size: var(--font-size-lg);
--_gradient-picker-chip-size: calc(var(--control-height-lg) - var(--space-2));
}
/* ── Trigger — field-shaped pill with a gradient chip ────────────────────── */
/* ── Trigger ───────────────────────────────────────────────────────────────
* The trigger IS a popover trigger, and `popover.css` paints its whole
* chrome — height, inline padding, font-size, colour, background, border,
* radius, hover and focus — from `[data-popover-trigger]:not([data-archetype='field-trigger'])`,
* which outweighs anything this file can say about `[data-gradient-picker-trigger]`
* (0,2,0 vs 0,1,0; 0,3,0 vs 0,2,0 on the states).
*
* This recipe used to re-declare all of it. Measured 2026-08-20 with a
* sentinel on every knob: NONE of it painted — height, padding, font-size and
* radius all came from popover, and the values differed (12px vs the recipe's
* space-2-5, 14px vs its font-size-md). It was dead code, so it is gone rather
* than tokenised: a token over a dead declaration is a token that lies.
*
* A theme dresses this trigger through the POPOVER contract, which is
* composition working as designed. What is genuinely the picker's own — the
* row gap and the gradient chip — stays here.
*/
[data-gradient-picker-trigger] {
display: inline-flex;
align-items: center;
gap: var(--space-2);
block-size: var(--_gradient-picker-trigger-height);
padding-inline: var(--_gradient-picker-trigger-padding-inline);
font-size: var(--_gradient-picker-trigger-font-size);
color: var(--color-content-primary);
background: var(--color-surface-default);
border: var(--border-width) solid var(--color-border-default);
border-radius: var(--radius-md);
gap: var(--gradient-picker-trigger-gap);
cursor: pointer;
transition:
background-color var(--duration-fast) var(--ease-default),
border-color var(--duration-fast) var(--ease-default);
}
[data-gradient-picker-trigger]:hover {
background: var(--color-surface-overlay);
border-color: var(--color-border-strong);
}
[data-gradient-picker-trigger]:focus-visible {
outline: 2px solid var(--color-primary-solid);
outline-offset: 1px;
}
[data-gradient-picker-trigger][data-disabled] {
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
.gradient-picker-trigger-swatch {
flex: none;
inline-size: var(--_gradient-picker-chip-size);
block-size: var(--_gradient-picker-chip-size);
border-radius: var(--radius-sm);
border: var(--border-width) solid var(--color-border-subtle);
inline-size: var(--gradient-picker-chip-size);
block-size: var(--gradient-picker-chip-size);
border-radius: var(--gradient-picker-chip-radius);
border: var(--gradient-picker-border-width) solid var(--gradient-picker-chip-border);
background: var(--gp-current-gradient, transparent);
}
.gradient-picker-trigger-label {
@ -80,23 +57,23 @@
/* ── Standalone value swatch ─────────────────────────────────────────────── */
[data-gradient-picker-value-swatch] {
inline-size: var(--_gradient-picker-chip-size);
block-size: var(--_gradient-picker-chip-size);
border-radius: var(--radius-sm);
border: var(--border-width) solid var(--color-border-subtle);
inline-size: var(--gradient-picker-chip-size);
block-size: var(--gradient-picker-chip-size);
border-radius: var(--gradient-picker-chip-radius);
border: var(--gradient-picker-border-width) solid var(--gradient-picker-chip-border);
background: var(--gp-current-gradient, transparent);
}
/* ── Content — same width as the ColorPicker (the editor + a three-item footer
* fit; the add-to-presets action lives in the body, not the footer) ────────── */
[data-gradient-picker-content] {
inline-size: 20rem;
inline-size: var(--gradient-picker-content-width-md);
}
[data-gradient-picker-content][data-picker-size='sm'] {
inline-size: 18rem;
inline-size: var(--gradient-picker-content-width-sm);
}
[data-gradient-picker-content][data-picker-size='lg'] {
inline-size: 22rem;
inline-size: var(--gradient-picker-content-width-lg);
}
/* Let the shell + body shrink to the popover width instead of forcing the
@ -114,54 +91,54 @@
/* ── Stops list — content-sizes up to a max, then scrolls (ScrollArea autosize),
* so a long list doesn't grow the popover ──────────────────────────────────── */
[data-gradient-picker-stoplist-scroll] {
--scroll-area-auto-max-block: calc(132px * var(--scaling, 1));
--scroll-area-auto-max-block: var(--gradient-picker-stoplist-max-block);
/* A slim scrollbar track (thinner than the sm default), no inner padding. */
--scroll-area-scrollbar-size: var(--space-1-5);
--scroll-area-scrollbar-size: var(--gradient-picker-stoplist-scrollbar-size);
--scroll-area-scrollbar-padding: 0;
}
/* Reserve the scrollbar's width PLUS a gap so the stop rows keep clear of it —
the list content stops short of the overlay track with breathing room. */
[data-gradient-picker-stoplist-scroll] [data-scroll-area-viewport] {
padding-inline-end: var(--space-3);
padding-inline-end: var(--gradient-picker-stoplist-scrollbar-inset);
}
/* ── Presets panel (opens from the Layers button) + its swatch grid ─────────── */
[data-gradient-picker-presets-panel] {
inline-size: 15rem;
max-inline-size: min(15rem, calc(100vw - var(--space-4)));
inline-size: var(--gradient-picker-presets-panel-width);
max-inline-size: min(var(--gradient-picker-presets-panel-width), calc(100vw - var(--space-4)));
}
[data-gradient-picker-presets] {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
gap: var(--space-2);
grid-template-columns: repeat(auto-fill, minmax(var(--gradient-picker-presets-min-column), 1fr));
gap: var(--gradient-picker-presets-gap);
align-content: start;
}
[data-gradient-picker-preset] {
inline-size: 100%;
block-size: calc(32px * var(--scaling, 1));
block-size: var(--gradient-picker-preset-height);
padding: 0;
border: var(--border-width) solid var(--color-border-subtle);
border-radius: var(--radius-md);
border: var(--gradient-picker-border-width) solid var(--gradient-picker-preset-border);
border-radius: var(--gradient-picker-preset-radius);
cursor: pointer;
}
[data-gradient-picker-preset]:hover {
border-color: var(--color-border-strong);
border-color: var(--gradient-picker-hover-preset-border);
}
[data-gradient-picker-preset]:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
[data-gradient-picker-preset][data-selected] {
border-color: var(--color-primary-border);
outline: var(--focus-ring-width) solid var(--color-primary-border);
outline-offset: 2px;
border-color: var(--gradient-picker-selected-preset-border);
outline: var(--focus-ring-width) solid var(--gradient-picker-selected-preset-border);
outline-offset: var(--gradient-picker-selected-preset-outline-offset);
}
/* ── Actions row below the stops — [ Añadir degradado (fills) · Layers ] ────── */
[data-gradient-picker-actions-row] {
display: flex;
align-items: center;
gap: var(--space-2);
gap: var(--gradient-picker-actions-gap);
}
[data-gradient-picker-save-action] {
flex: 1 1 auto;

@ -1924,6 +1924,30 @@
--gradient-builder-hover-preset-border: var(--color-border-strong);
--gradient-builder-selected-preset-border: var(--color-primary-border);
--gradient-builder-selected-preset-outline: var(--color-primary-border);
--gradient-picker-chip-size-sm: calc(var(--size-sm-control-height) - var(--space-2));
--gradient-picker-chip-size-md: calc(var(--size-md-control-height) - var(--space-2));
--gradient-picker-chip-size-lg: calc(var(--size-lg-control-height) - var(--space-2));
--gradient-picker-gap: var(--space-1-5);
--gradient-picker-trigger-gap: var(--space-2);
--gradient-picker-border-width: var(--border-width);
--gradient-picker-chip-radius: var(--radius-sm);
--gradient-picker-chip-border: var(--color-border-subtle);
--gradient-picker-content-width-sm: 18rem;
--gradient-picker-content-width-md: 20rem;
--gradient-picker-content-width-lg: 22rem;
--gradient-picker-stoplist-max-block: calc(132px * var(--scaling, 1));
--gradient-picker-stoplist-scrollbar-size: var(--space-1-5);
--gradient-picker-stoplist-scrollbar-inset: var(--space-3);
--gradient-picker-presets-panel-width: 15rem;
--gradient-picker-presets-gap: var(--space-2);
--gradient-picker-presets-min-column: 48px;
--gradient-picker-preset-height: calc(32px * var(--scaling, 1));
--gradient-picker-preset-radius: var(--radius-md);
--gradient-picker-preset-border: var(--color-border-subtle);
--gradient-picker-hover-preset-border: var(--color-border-strong);
--gradient-picker-selected-preset-border: var(--color-primary-border);
--gradient-picker-selected-preset-outline-offset: 2px;
--gradient-picker-actions-gap: var(--space-2);
--proof-of-human-min-height-sm: calc(var(--size-sm-control-height) * 3);
--proof-of-human-min-height-md: calc(var(--size-md-control-height) * 3);
--proof-of-human-min-height-lg: calc(var(--size-lg-control-height) * 3);
@ -5469,6 +5493,18 @@
--gradient-builder-padding-inline: var(--gradient-builder-padding-inline-lg);
}
[data-gradient-picker] {
--gradient-picker-chip-size: var(--gradient-picker-chip-size-md);
}
[data-gradient-picker][data-size='sm'] {
--gradient-picker-chip-size: var(--gradient-picker-chip-size-sm);
}
[data-gradient-picker][data-size='lg'] {
--gradient-picker-chip-size: var(--gradient-picker-chip-size-lg);
}
[data-clipboard] {
--_clipboard-palette-solid: var(--color-primary-solid);
--_clipboard-palette-element: var(--color-primary-element);

@ -2527,6 +2527,70 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
'selected-preset-border': 'var(--color-primary-border)',
'selected-preset-outline': 'var(--color-primary-border)'
},
// GradientPicker. Composes picker-shell + Popover + GradientBuilder, so it
// mints nothing those layers own — only its own trigger pill, the gradient
// chip, the panel widths and the presets grid.
'gradient-picker': {
// Per-size coordinates (sm | md | lg). Height and type are 1:1 with the
// bundle; the trigger's inline padding is its OWN rhythm (md and lg sit
// tighter than the bundle, so forcing it would change the default).
// The chip is the trigger's height minus its vertical breathing room, so
// it tracks the control instead of carrying a size of its own.
'chip-size-sm': 'calc(var(--size-sm-control-height) - var(--space-2))',
'chip-size-md': 'calc(var(--size-md-control-height) - var(--space-2))',
'chip-size-lg': 'calc(var(--size-lg-control-height) - var(--space-2))',
// The RESOLVED names. `data-size` is stamped on the root and the trigger
// hangs from it, so `host` scope is enough.
'chip-size': {
declarations: [
{ value: 'var(--gradient-picker-chip-size-md)', scope: 'host' },
{ value: 'var(--gradient-picker-chip-size-sm)', scope: 'size:sm' },
{ value: 'var(--gradient-picker-chip-size-lg)', scope: 'size:lg' }
]
},
// Root rhythm.
gap: 'var(--space-1-5)',
// Trigger — a field-shaped pill.
'trigger-gap': 'var(--space-2)',
'border-width': 'var(--border-width)',
// The gradient chip. Its FILL is not a knob: soma stamps the user's own
// gradient inline (`--gp-current-gradient`, a value channel), and a theme
// has no business overriding what the user picked.
'chip-radius': 'var(--radius-sm)',
'chip-border': 'var(--color-border-subtle)',
// Panel widths. The content is PORTALLED and sizes off `data-picker-size`
// (picker-shell's attr), not `data-size`, so these stay flat tokens read
// by three rules instead of a TSC size cascade - the scope vocabulary has
// no word for another component's attribute.
'content-width-sm': '18rem',
'content-width-md': '20rem',
'content-width-lg': '22rem',
// Stop list — content-sized up to a max, then scrolls.
'stoplist-max-block': 'calc(132px * var(--scaling, 1))',
'stoplist-scrollbar-size': 'var(--space-1-5)',
'stoplist-scrollbar-inset': 'var(--space-3)',
// Presets panel + its swatch grid.
'presets-panel-width': '15rem',
'presets-gap': 'var(--space-2)',
'presets-min-column': '48px',
'preset-height': 'calc(32px * var(--scaling, 1))',
'preset-radius': 'var(--radius-md)',
'preset-border': 'var(--color-border-subtle)',
'hover-preset-border': 'var(--color-border-strong)',
'selected-preset-border': 'var(--color-primary-border)',
'selected-preset-outline-offset': '2px',
// Actions row under the stops.
'actions-gap': 'var(--space-2)'
},
clipboard: {
'_palette-solid': { value: 'var(--color-primary-solid)', scope: 'host' },
'_palette-element': { value: 'var(--color-primary-element)', scope: 'host' }

@ -13,6 +13,7 @@
import { compileMorfo } from '$uix/morfo';
import { gradientPickerMorfo } from '@/uix/morfo/components/gradient-picker';
import { getActiveUix } from '$active-uix';
import TokensPanel from '../../lib/TokensPanel.svelte';
import SystemAxes from '../../lib/SystemAxes.svelte';
import MotionPanel from '../../lib/MotionPanel.svelte';
@ -21,7 +22,17 @@
const uix = getActiveUix();
type Tab = 'live' | 'system' | 'motion' | 'sema' | 'services' | 'api' | 'morfo' | 'recipe' | 'a11y';
type Tab =
| 'live'
| 'system'
| 'motion'
| 'sema'
| 'services'
| 'api'
| 'morfo'
| 'recipe'
| 'tokens'
| 'a11y';
let tab = $state<Tab>('live');
const sizes: GradientPickerSize[] = ['sm', 'md', 'lg'];
@ -156,10 +167,10 @@
<div data-uix-eyebrow>Controls · GradientPicker</div>
<h1 data-uix-page-title>GradientPicker</h1>
<p data-uix-page-lede>
The <a href="/uix/components/gradient-builder">GradientBuilder</a> wrapped in a Popover —
a field-shaped trigger carrying a live gradient chip opens the editor, with a
Clear / Cancel / Save footer. An identity wrapper over the composed Popover (the Popover is
the dialog); Cancel reverts to the value the popover opened with.
The <a href="/uix/components/gradient-builder">GradientBuilder</a> wrapped in a Popover — a field-shaped
trigger carrying a live gradient chip opens the editor, with a Clear / Cancel / Save footer. An
identity wrapper over the composed Popover (the Popover is the dialog); Cancel reverts to the value
the popover opened with.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill><span data-uix-meta-key>parts</span>{partsList.length}</span>
@ -181,7 +192,18 @@
{...dir ? { dir } : {}}
style={stageStyle}
>
<GradientPicker bind:value bind:open {size} {variant} {mode} {deferValue} {disabled} {presets} {maxPresets} {dir}>
<GradientPicker
bind:value
bind:open
{size}
{variant}
{mode}
{deferValue}
{disabled}
{presets}
{maxPresets}
{dir}
>
<GradientPicker.Trigger />
<GradientPicker.Portal>
<GradientPicker.Content />
@ -194,10 +216,16 @@
<span>open the picker, edit, then Save / Cancel</span>
{:else}
{#each trace.entries.slice(0, 3) as entry}
<span><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent ? ' · ' + entry.intent : ''}</span>
<span
><span data-uix-stage-trace-event>{entry.event}</span> · {entry.family}{entry.intent
? ' · ' + entry.intent
: ''}</span
>
{/each}
{/if}
<span style="margin-inline-start: auto;"><span data-uix-stage-trace-key>open</span> {open ? 'yes' : 'no'}</span>
<span style="margin-inline-start: auto;"
><span data-uix-stage-trace-key>open</span> {open ? 'yes' : 'no'}</span
>
</div>
</div>
@ -207,16 +235,27 @@
<button data-uix-tab data-active={tab === 'system'} onclick={() => (tab = 'system')}>
<span data-uix-layer-badge="eidos">eidos</span> System
</button>
<button data-uix-tab data-active={tab === 'motion'} onclick={() => (tab = 'motion')}>Motion</button>
<button data-uix-tab data-active={tab === 'motion'} onclick={() => (tab = 'motion')}
>Motion</button
>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span><span data-uix-tab-count>{actions.length}</span>
</button>
<button data-uix-tab data-active={tab === 'services'} onclick={() => (tab = 'services')}>Services</button>
<button data-uix-tab data-active={tab === 'services'} onclick={() => (tab = 'services')}
>Services</button
>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API</button>
<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 · {actions.length}e</span>
<span data-uix-layer-badge="morfo">morfo</span><span data-uix-tab-count
>{partsList.length}p · {actions.length}e</span
>
</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>25</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
@ -224,8 +263,8 @@
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
The component API. Like GradientBuilder it has no <code>color</code> prop. Foundation
axes live in the <button data-uix-linklike onclick={() => (tab = 'system')}>System</button> tab.
The component API. Like GradientBuilder it has no <code>color</code> prop. Foundation axes
live in the <button data-uix-linklike onclick={() => (tab = 'system')}>System</button> tab.
</p>
<div data-uix-subsection-head>
@ -241,10 +280,14 @@
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>variant <span data-uix-control-hint>trigger framing</span></span>
<span data-uix-control-label
>variant <span data-uix-control-hint>trigger framing</span></span
>
<span data-uix-chips role="radiogroup">
{#each variants as v}
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v)}>{v}</button>
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v)}
>{v}</button
>
{/each}
</span>
</label>
@ -258,29 +301,50 @@
<span data-uix-control-label>mode <span data-uix-control-hint>footer layout</span></span>
<span data-uix-chips role="radiogroup">
{#each ['inline', 'modal'] as m}
<button data-uix-chip data-active={mode === m} onclick={() => (mode = m as typeof mode)}>{m}</button>
<button
data-uix-chip
data-active={mode === m}
onclick={() => (mode = m as typeof mode)}>{m}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>disabled</span>
<span data-uix-switch><input type="checkbox" bind:checked={disabled} /><span data-uix-switch-label>{disabled ? 'on' : 'off'}</span></span>
<span data-uix-switch
><input type="checkbox" bind:checked={disabled} /><span data-uix-switch-label
>{disabled ? 'on' : 'off'}</span
></span
>
</label>
<label data-uix-control>
<span data-uix-control-label>deferValue <span data-uix-control-hint>draft until Listo</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={deferValue} /><span data-uix-switch-label>{deferValue ? 'on' : 'off'}</span></span>
<span data-uix-control-label
>deferValue <span data-uix-control-hint>draft until Listo</span></span
>
<span data-uix-switch
><input type="checkbox" bind:checked={deferValue} /><span data-uix-switch-label
>{deferValue ? 'on' : 'off'}</span
></span
>
</label>
<label data-uix-control>
<span data-uix-control-label>maxPresets <span data-uix-control-hint>saved cap</span></span>
<span data-uix-control-label>maxPresets <span data-uix-control-hint>saved cap</span></span
>
<span data-uix-chips role="radiogroup">
{#each [1, 5, 10] as n}
<button data-uix-chip data-active={maxPresets === n} onclick={() => (maxPresets = n)}>{n}</button>
<button data-uix-chip data-active={maxPresets === n} onclick={() => (maxPresets = n)}
>{n}</button
>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>open</span>
<span data-uix-switch><input type="checkbox" bind:checked={open} /><span data-uix-switch-label>{open ? 'on' : 'off'}</span></span>
<span data-uix-switch
><input type="checkbox" bind:checked={open} /><span data-uix-switch-label
>{open ? 'on' : 'off'}</span
></span
>
</label>
</div>
@ -293,7 +357,9 @@
</div>
<pre><code>background: {css};</code></pre>
</div>
<div style={`block-size: 3rem; border-radius: var(--uix-radius-md, 8px); border: 1px solid var(--color-border-subtle); background: ${css};`}></div>
<div
style={`block-size: 3rem; border-radius: var(--uix-radius-md, 8px); border: 1px solid var(--color-border-subtle); background: ${css};`}
></div>
<div data-uix-code>
<div data-uix-code-head>
@ -313,7 +379,13 @@
Foundation knobs applied to the stage. Open the picker and switch mode to preview the
popover in dark.
</p>
<SystemAxes bind:density bind:scaling bind:mode={stageMode} bind:dir={dirMode} bind:borderWidth />
<SystemAxes
bind:density
bind:scaling
bind:mode={stageMode}
bind:dir={dirMode}
bind:borderWidth
/>
</section>
{/if}
@ -324,7 +396,9 @@
The picker's motion is the composed Popover's enter / exit (the emerge firma) plus the
editor's direct-manipulation feedback. The catalog teaches the system presets.
</p>
<MotionPanel note="The popover open/close is owned by the composed Popover (emerge family). The editor inside is pure direct manipulation." />
<MotionPanel
note="The popover open/close is owned by the composed Popover (emerge family). The editor inside is pure direct manipulation."
/>
</section>
{/if}
@ -334,9 +408,14 @@
<p data-uix-section-desc>
The picker declares one value event — <code>commit-reset</code> (the Clear action). The
editing firma (handle / commit) lives on the embedded GradientBuilder; the open / close
firma lives on the composed Popover. Turn on <strong>Semantics → Sound</strong>, then ▶ play.
firma lives on the composed Popover. Turn on <strong>Semantics → Sound</strong>, then ▶
play.
</p>
<SemaPanel {actions} {uix} getTarget={() => stageRef?.querySelector('[data-gradient-picker]') ?? stageRef} />
<SemaPanel
{actions}
{uix}
getTarget={() => stageRef?.querySelector('[data-gradient-picker]') ?? stageRef}
/>
</section>
{/if}
@ -344,17 +423,21 @@
<section data-uix-section>
<h2 data-uix-section-title>Services</h2>
<p data-uix-section-desc>
GradientPicker consumes <strong>langs</strong> (i18n): the trigger / content labels +
the footer Clear / Cancel / Save (from the shared picker-shell). Switch language in the
topbar to see them follow.
GradientPicker consumes <strong>langs</strong> (i18n): the trigger / content labels + the footer
Clear / Cancel / Save (from the shared picker-shell). Switch language in the topbar to see them
follow.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Text key</th><th>Default</th><th>Service</th></tr></thead>
<tbody>
<tr><td class="name">label</td><td class="type">Gradient picker</td><td>langs</td></tr>
<tr><td class="name">trigger</td><td class="type">Open gradient editor</td><td>langs</td></tr>
<tr><td class="name">content</td><td class="type">Gradient editor</td><td>langs</td></tr>
<tr
><td class="name">trigger</td><td class="type">Open gradient editor</td><td>langs</td
></tr
>
<tr><td class="name">content</td><td class="type">Gradient editor</td><td>langs</td></tr
>
</tbody>
</table>
</div>
@ -368,16 +451,53 @@
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">value</td><td class="type">Gradient</td><td class="default">2-stop linear</td><td>The committed gradient. Bindable.</td></tr>
<tr><td class="name">open</td><td class="type">boolean</td><td class="default">false</td><td>Popover open state. Bindable.</td></tr>
<tr><td class="name">mode</td><td class="type">'inline' | 'modal'</td><td class="default">'inline'</td><td>Footer layout (Clear/Cancel vs Cancel/Save).</td></tr>
<tr><td class="name">deferValue</td><td class="type">boolean</td><td class="default">false</td><td>Buffer edits in a draft until Listo; other closes discard.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'sm' | 'md' | 'lg'</td><td class="default">'md'</td><td>Trigger / popover / editor size.</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'surface' | 'outline' | 'ghost'</td><td class="default">'surface'</td><td>Trigger framing.</td></tr>
<tr><td class="name">onValueChange</td><td class="type">(g: Gradient) =&gt; void</td><td class="default empty">—</td><td>Fires on every edit (live; deferValue off).</td></tr>
<tr><td class="name">presets <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">Gradient[]</td><td class="default empty">—</td><td>Defined → the popover splits into Editor + Predefinidos tabs.</td></tr>
<tr><td class="name">maxPresets</td><td class="type">number</td><td class="default">10</td><td>Cap on user-saved presets — "Añadir degradado" disables at the cap.</td></tr>
</tbody>
<tr
><td class="name">value</td><td class="type">Gradient</td><td class="default"
>2-stop linear</td
><td>The committed gradient. Bindable.</td></tr
>
<tr
><td class="name">open</td><td class="type">boolean</td><td class="default">false</td
><td>Popover open state. Bindable.</td></tr
>
<tr
><td class="name">mode</td><td class="type">'inline' | 'modal'</td><td class="default"
>'inline'</td
><td>Footer layout (Clear/Cancel vs Cancel/Save).</td></tr
>
<tr
><td class="name">deferValue</td><td class="type">boolean</td><td class="default"
>false</td
><td>Buffer edits in a draft until Listo; other closes discard.</td></tr
>
<tr
><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">'sm' | 'md' | 'lg'</td
><td class="default">'md'</td><td>Trigger / popover / editor size.</td></tr
>
<tr
><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">'surface' | 'outline' | 'ghost'</td
><td class="default">'surface'</td><td>Trigger framing.</td></tr
>
<tr
><td class="name">onValueChange</td><td class="type">(g: Gradient) =&gt; void</td><td
class="default empty">—</td
><td>Fires on every edit (live; deferValue off).</td></tr
>
<tr
><td class="name">presets <span data-uix-tag data-kind="eidos">eidos</span></td><td
class="type">Gradient[]</td
><td class="default empty">—</td><td
>Defined → the popover splits into Editor + Predefinidos tabs.</td
></tr
>
<tr
><td class="name">maxPresets</td><td class="type">number</td><td class="default"
>10</td
><td>Cap on user-saved presets — "Añadir degradado" disables at the cap.</td></tr
>
</tbody>
</table>
</div>
</section>
@ -385,13 +505,24 @@
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title><span data-uix-layer-badge="morfo">morfo</span> · declarative contract</h2>
<p data-uix-section-desc>Source: <code>src/uix/morfo/components/gradient-picker.ts</code> · <code>expression: 'delegated'</code> (the Popover is the dialog).</p>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span> · declarative contract
</h2>
<p data-uix-section-desc>
Source: <code>src/uix/morfo/components/gradient-picker.ts</code> ·
<code>expression: 'delegated'</code> (the Popover is the dialog).
</p>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th>Optional</th></tr></thead>
<thead
><tr
><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th
>Optional</th
></tr
></thead
>
<tbody>
{#each partsList as part}
<tr>
@ -410,7 +541,10 @@
<div data-uix-subsection-head>Events</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>target</th></tr></thead>
<thead
><tr><th>name</th><th>family</th><th>verb</th><th>sequence</th><th>target</th></tr
></thead
>
<tbody>
{#each actions as action}
{@const sem = action.semantic}
@ -432,8 +566,8 @@
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Selectors at <code>src/uix/eidos/components/gradient-picker/gradient-picker.css</code>.
The trigger is a field-shaped pill carrying a gradient chip (<code>--gp-current-gradient</code>,
Selectors at <code>src/uix/eidos/components/gradient-picker/gradient-picker.css</code>. The
trigger is a field-shaped pill carrying a gradient chip (<code>--gp-current-gradient</code>,
stamped inline by soma); the popover content + editor reuse the Popover + PickerShell +
GradientBuilder recipes.
</p>
@ -441,30 +575,58 @@
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th></tr></thead>
<tbody>
<tr><td class="name">[data-gradient-picker]</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">[data-gradient-picker][data-size='X'] / [data-variant='X']</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-gradient-picker-trigger][data-state='open']</td><td><span data-uix-tag data-kind="soma">morfo</span></td></tr>
<tr><td class="name">.gradient-picker-trigger-swatch</td><td><span data-uix-tag data-kind="eidos">eidos</span></td></tr>
<tr><td class="name">[data-gradient-picker-content]</td><td><span data-uix-tag data-kind="soma">morfo</span> + Popover</td></tr>
<tr
><td class="name">[data-gradient-picker]</td><td
><span data-uix-tag data-kind="soma">morfo</span></td
></tr
>
<tr
><td class="name">[data-gradient-picker][data-size='X'] / [data-variant='X']</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
></tr
>
<tr
><td class="name">[data-gradient-picker-trigger][data-state='open']</td><td
><span data-uix-tag data-kind="soma">morfo</span></td
></tr
>
<tr
><td class="name">.gradient-picker-trigger-swatch</td><td
><span data-uix-tag data-kind="eidos">eidos</span></td
></tr
>
<tr
><td class="name">[data-gradient-picker-content]</td><td
><span data-uix-tag data-kind="soma">morfo</span> + Popover</td
></tr
>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'tokens'}
<TokensPanel component="gradient-picker" stage={stageRef ?? undefined} />
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
The trigger is <code>aria-haspopup="dialog"</code> + <code>aria-expanded</code>; the
Popover provides the <code>role="dialog"</code>, focus trap and Escape-to-close. Inside,
the GradientBuilder's keyboard-accessible stops apply.
The trigger is <code>aria-haspopup="dialog"</code> + <code>aria-expanded</code>; the Popover
provides the <code>role="dialog"</code>, focus trap and Escape-to-close. Inside, the
GradientBuilder's keyboard-accessible stops apply.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Key</th><th>Action</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span></td><td>Open the editor (on the trigger)</td></tr>
<tr
><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span></td><td
>Open the editor (on the trigger)</td
></tr
>
<tr><td><span data-uix-kbd>Escape</span></td><td>Close the popover</td></tr>
<tr><td><span data-uix-kbd>Tab</span></td><td>Move focus within the editor</td></tr>
</tbody>

Loading…
Cancel
Save

Powered by TurnKey Linux.