uix(picker-shell): temable — 0 % → 76 %, y el componente que el instrumento no veía

31 claves en una entrada NUEVA de `recipes/base.ts`. Censo 0 % → 76 %. Global
48 %, sin contrato 43 → 42.

La cola avisaba «es CAPA de los pickers: mirar antes qué posee y qué presta».
Mirarlo cambió tres cosas del encargo:

1. **NO es una capa compartida: es un COMPONENTE.** A diferencia de
   list-surface o calendar-surface no hay hook de capa ni vocabulario prestado
   — color-picker, date-picker y chronos importan sus `.svelte` directamente.
   No toca `LAYER_VOCABULARY` y el censo ya lo puntuaba bien.
2. **Su eje de talla no es `data-size`, es `data-picker-size` sobre
   `[data-popover-content]`**, un ancestro PORTALADO. El TSC no puede emitir esa
   cascada (`scope: 'size:xs'` generaría `[data-picker-shell][data-size='xs']`,
   que no casa nunca), así que las coordenadas por talla quedan como públicos
   PLANOS y la receta las consume desde sus propios selectores. Es el único
   componente del eje donde el patrón de nombres resueltos no aplica; la regla
   base es el default `sm` y `md` sólo re-declara lo que cambia. La fila de
   tiempo lleva un TERCER eje, `data-size` sobre su propia clase.
3. **El instrumental del eje estaba CIEGO aquí, y la sonda genérica devolvió 0
   nodos** — el antipatrón de comparar cero valores y pasar. Dos causas a la
   vez: `picker-shell` no tiene demo propia (404, se mide dentro de un picker
   anfitrión y detrás de su popover), y sus partes se llaman
   `data-picker-header` / `-body` / `-footer` en vez de
   `data-picker-shell-{parte}` — genéricas A PROPÓSITO, dice la receta, «so
   every picker gets the same visual contract for free» —, así que todo filtro
   por prefijo del componente ve sólo la raíz.

CÓMO SE VERIFICÓ, ya que el instrumento estándar no servía:
- Sonda DIRIGIDA (abre el popover del date-picker, captura los seis nodos
  reales en las cuatro picker-sizes): determinismo comprobado (576 valores, 0
  diffs entre dos corridas del mismo código) y **gate antes/después = 0 diffs**.
- Los knobs que la demo no monta —el header entero y la fila de tiempo— se
  midieron FORZANDO el nodo: los quince mueven su propiedad, y el font-size del
  header se comprobó talla a talla (12/14/16/16px → 102px, cada uno en su
  picker-size).
- El guard R-5.4 pasó de **0/31 a 6/31** al ganar un `COMPONENT_OVERRIDES`
  (prefijo de atributo + selector de apertura) acotado a este componente.
  Verificado que los otros CATORCE con ledger no se mueven ni una cifra.
  Adjudicar 31 excepciones sin ese arreglo habría sido escribir una mentira en
  el ledger; los 25 que siguen mudos llevan cada uno su razón medida.

Header y footer comparten la regla que los separa del cuerpo → UN par de knobs
(`border-width` + `border`), no dos.

Sin tab `Tokens`: este componente no tiene página donde ponerlo. Registrado en
next-features §13 junto con la pregunta de fondo — si el precio de los nombres
genéricos (un contrato visual común para todos los pickers) compensa la ceguera
del instrumental — y con el eje portalado como caso de prueba si el TSC llega a
ganar un scope configurable.

Gates: censo --only 76 % · suite eidos sin rojos nuevos (el conocido
skin-media-player) · rtl:check 0 · docs:check 0 · check por fichero limpio ·
`component:audit` no lo cubre (eidos-only, sin morfo) y `eidos-lint` tampoco.

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

@ -10,9 +10,9 @@
- **Medido**: 2026-08-21 · **162 recetas** con CSS + **8 componentes sin receta** = 170 fichas, el árbol entero de `eidos/components/`
- **La pregunta**: ¿cuánto de la apariencia de cada componente puede cambiar un tema **sin tocar el sistema ni la receta**?
- **Alcance global**: **48%** — 2229 de 4678 knobs pasan por un token público del componente
- **Reparto**: público 2229 · privado 642 · global 1229 · literal 578 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 43 · **alcance < 20 %**: 37 · **alcance 100 %**: 7 · **con eje `size`**: 63
- **Alcance global**: **48%** — 2245 de 4678 knobs pasan por un token público del componente
- **Reparto**: público 2245 · privado 642 · global 1213 · literal 578 · sistema transversal 509 _(fuera del ratio)_
- **Sin token público propio**: 42 · **alcance < 20 %**: 36 · **alcance 100 %**: 7 · **con eje `size`**: 63
## Cómo se lee
@ -91,12 +91,12 @@ theming §6.7 lo pone delante (`hover-bg`). Las fichas proponen la forma
| 19 | [timeline](./timeline.md) | 24 | 51% | 49 | 45 |
| 20 | [avatar](./avatar.md) | 23 | 36% | 36 | 0 |
## Sin ningún token público propio (43)
## Sin ningún token público propio (42)
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) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`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) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md)
[`alert-dialog`](./alert-dialog.md) · [`announce`](./announce.md) · [`aspect-ratio`](./aspect-ratio.md) · [`auto-grid`](./auto-grid.md) · [`avatar`](./avatar.md) · [`cascade`](./cascade.md) · [`clipboard`](./clipboard.md) · [`code`](./code.md) · [`css-field`](./css-field.md) · [`date-picker`](./date-picker.md) · [`display`](./display.md) · [`field-langs`](./field-langs.md) · [`heading`](./heading.md) · [`highlight`](./highlight.md) · [`kbd`](./kbd.md) · [`knob`](./knob.md) · [`label`](./label.md) · [`link`](./link.md) · [`link-preview`](./link-preview.md) · [`mark`](./mark.md) · [`mask-field`](./mask-field.md) · [`menu-dial`](./menu-dial.md) · [`mockup`](./mockup.md) · [`month-grid`](./month-grid.md) · [`motion`](./motion.md) · [`number-field`](./number-field.md) · [`palabras`](./palabras.md) · [`range-calendar`](./range-calendar.md) · [`scroll-frames`](./scroll-frames.md) · [`skeleton`](./skeleton.md) · [`skin-media-player`](./skin-media-player.md) · [`stack`](./stack.md) · [`text`](./text.md) · [`text-blur`](./text-blur.md) · [`text-circular`](./text-circular.md) · [`text-focus`](./text-focus.md) · [`text-gradient`](./text-gradient.md) · [`text-scramble`](./text-scramble.md) · [`virtual-grid`](./virtual-grid.md) · [`virtual-list`](./virtual-list.md) · [`wrap`](./wrap.md) · [`year-grid`](./year-grid.md)
## Tabla completa (162 recetas, por alcance ascendente)
@ -108,7 +108,6 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [month-grid](./month-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – |
| [year-grid](./year-grid.md) | 0% | 61 | 0 | 7 | 0 | 5 | 49 | 0 | – |
| [field-langs](./field-langs.md) | 0% | 41 | 0 | 0 | 31 | 8 | 2 | 0 | – |
| [picker-shell](./picker-shell.md) | 0% | 21 | 0 | 4 | 17 | 0 | 0 | 0 | y |
| [link-preview](./link-preview.md) | 0% | 17 | 0 | 4 | 9 | 1 | 3 | 0 | y |
| [drag-drop](./drag-drop.md) | 0% | 16 | 0 | 3 | 6 | 0 | 7 | 1 | – |
| [kbd](./kbd.md) | 0% | 14 | 0 | 1 | 9 | 4 | 0 | 0 | – |
@ -216,6 +215,7 @@ La columna «contrato» cuenta las claves **públicas** del bloque del component
| [skip-link](./skip-link.md) | 71% | 8 | 5 | 0 | 0 | 2 | 1 | 8 | – |
| [date-field](./date-field.md) | 73% | 11 | 8 | 3 | 0 | 0 | 0 | 21 | y |
| [time-field](./time-field.md) | 73% | 11 | 8 | 3 | 0 | 0 | 0 | 21 | y |
| [picker-shell](./picker-shell.md) | 76% | 21 | 16 | 4 | 1 | 0 | 0 | 31 | y |
| [combobox](./combobox.md) | 76% | 97 | 68 | 12 | 3 | 6 | 8 | 106 | – |
| [accordion](./accordion.md) | 77% | 65 | 49 | 10 | 0 | 5 | 1 | 76 | y |
| [carousel](./carousel.md) | 77% | 35 | 23 | 2 | 0 | 5 | 5 | 27 | – |

@ -5,34 +5,18 @@
> 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 21 knobs por token público
- **Knobs de apariencia**: 21 — público 0 · privado 4 · global 17 · literal 0 · sistema 0 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): **sin entrada en `base.ts`**
- **Medido**: 2026-08-21 · **Alcance**: **76%** — 16 de 21 knobs por token público
- **Knobs de apariencia**: 21 — público 16 · privado 4 · global 1 · literal 0 · sistema 0 _(fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 31 pública(s) — `section-gap-xs`, `section-gap-sm`, `section-gap-md`, `section-gap-lg`, `row-spacing-xs`, `row-spacing-sm`, `row-spacing-lg`, `footer-gap-xs`, `footer-gap-sm`, `footer-gap-lg`, `header-font-size-xs`, `header-font-size-sm`, `header-font-size-md`, `header-font-size-lg`, `border-width`, `border`, `header-gap`, `header-fg`, `header-font-weight`, `header-line-height`, `body-gap`, `time-row-gap`, `time-row-padding-block-start`, `time-row-padding-inline`, `time-row-border-width`, `time-row-border`, `time-label-fg`, `time-label-font-size-xs`, `time-label-font-size-sm`, `time-label-font-size-md`, `time-label-font-size-lg`
- **Eje `size`**: sí · **ficheros**: `picker-shell.css`, `picker-time-row.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (17)
### 1.1 Directo a primitivo global (1)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `picker-shell.css:32` | `[data-picker-header]` | `gap` | `var(--space-2)` |
| 2 | `picker-shell.css:34` | `[data-picker-header]` | `border-block-end` | `var(--border-width) solid var(--color-border-subtle)` |
| 3 | `picker-shell.css:35` | `[data-picker-header]` | `color` | `var(--color-content-primary)` |
| 4 | `picker-shell.css:36` | `[data-picker-header]` | `font-size` | `var(--font-size-sm)` |
| 5 | `picker-shell.css:37` | `[data-picker-header]` | `font-weight` | `var(--font-weight-medium)` |
| 6 | `picker-shell.css:38` | `[data-picker-header]` | `line-height` | `var(--leading-ui)` |
| 7 | `picker-shell.css:44` | `[data-picker-body]` | `gap` | `var(--space-3)` |
| 8 | `picker-shell.css:61` | `[data-picker-footer]` | `border-block-start` | `var(--border-width) solid var(--color-border-subtle)` |
| 9 | `picker-shell.css:89` | `[data-popover-content][data-picker-size='xs'] [data-picker-header]` | `font-size` | `var(--font-size-xs)` |
| 10 | `picker-shell.css:100` | `[data-popover-content][data-picker-size='md'] [data-picker-header]` | `font-size` | `var(--font-size-md)` |
| 11 | `picker-shell.css:108` | `[data-popover-content][data-picker-size='lg'] [data-picker-header]` | `font-size` | `var(--font-size-md)` |
| 12 | `picker-time-row.css:9` | `.picker-time-row` | `gap` | `var(--space-3)` |
| 13 | `picker-time-row.css:10` | `.picker-time-row` | `padding-block-start` | `var(--space-2)` |
| 14 | `picker-time-row.css:11` | `.picker-time-row` | `padding-inline` | `var(--space-2)` |
| 15 | `picker-time-row.css:12` | `.picker-time-row` | `border-block-start` | `var(--border-width, 1px) solid var(--color-neutral-border)` |
| 16 | `picker-time-row.css:32` | `.picker-time-label` | `color` | `var(--color-content-secondary)` |
| 17 | `picker-time-row.css:33` | `.picker-time-label` | `font-size` | `var(--_picker-time-label-font)` |
| 1 | `picker-time-row.css:34` | `.picker-time-label` | `font-size` | `var(--_picker-time-label-font)` |
### 1.2 A través de un privado (4)
@ -57,15 +41,15 @@ _Ninguno._
| privado | declaraciones | valor(es) | origen | ¿deriva de un público? |
| --- | ---: | --- | --- | :-: |
| `--_picker-shell-section-gap` | 4 | `var(--space-3)`, `var(--space-2)`, `var(--space-3-5)`, `var(--space-4)` | global | no |
| `--_picker-shell-row-spacing` | 6 | `var(--space-3)`, `var(--space-2)`, `var(--space-4)` | global | no |
| `--_picker-shell-gap` | 3 | `var(--space-2)`, `var(--space-1-5)`, `var(--space-2-5)` | global | no |
| `--_picker-shell-section-gap` | 4 | `var(--picker-shell-section-gap-sm)`, `var(--picker-shell-section-gap-xs)`, `var(--picker-shell-section-gap-md)`, `var(--picker-shell-section-gap-lg)` | public | **sí** |
| `--_picker-shell-row-spacing` | 6 | `var(--picker-shell-row-spacing-sm)`, `var(--picker-shell-row-spacing-xs)`, `var(--picker-shell-row-spacing-lg)` | public | **sí** |
| `--_picker-shell-gap` | 3 | `var(--picker-shell-footer-gap-sm)`, `var(--picker-shell-footer-gap-xs)`, `var(--picker-shell-footer-gap-lg)` | public | **sí** |
## 4. Propuesta de corrección
- **Tiene eje `size`**: los tokens dimensionales van por talla (`{part}-{eje}-{k}`) apuntando al bundle `--size-{k}-*`, nunca al primitivo crudo (theming §5; el guard `recipe-css-contract` prohíbe el primitivo).
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (7)
### 4.1 Tokens a declarar en `lib/recipes/base.ts` (1)
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
@ -75,24 +59,13 @@ no distingue lo que debería — se marca `⚠`.
| token (`--picker-shell-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `padding-block` | `root` | ⚠ `var(--space-3)` / `var(--space-2)` / `var(--space-4)` | 13 |
| `gap` | `root` | ⚠ `var(--space-3)` / `var(--space-2)` / `var(--space-3-5)` / `var(--space-4)` / `var(--space-1-5)` / `var(--space-2-5)` | 10 |
| `font-size` | `root` | ⚠ `var(--font-size-sm)` / `var(--font-size-xs)` / `var(--font-size-md)` / `var(--_picker-time-label-font)` | 5 |
| `fg` | `root` | ⚠ `var(--color-content-primary)` / `var(--color-content-secondary)` | 2 |
| `font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `line-height` | `root` | `var(--leading-ui)` | 1 |
| `padding-inline` | `root` | `var(--space-2)` | 1 |
| `font-size` | `root` | `var(--_picker-time-label-font)` | 1 |
### 4.2 Sin nombre mecánico (3)
### 4.2 Sin nombre mecánico (4)
- **⚠ decisión: `border-block-end` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `border-block-end`.
- **⚠ decisión: `border-block-start` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 2: `border-block-start`.
### 4.3 Avisos sobre los tokens propuestos (3)
- **el privado `--_picker-shell-section-gap` debe pasar a leer este público (o desaparecer)** — `--picker-shell-gap`
- **el privado `--_picker-shell-row-spacing` debe pasar a leer este público (o desaparecer)** — `--picker-shell-padding-block`
- **el privado `--_picker-shell-gap` debe pasar a leer este público (o desaparecer)** — `--picker-shell-gap`
- **ya alcanzable: el privado `--_picker-shell-section-gap` deriva de un público (`var(--picker-shell-section-gap-sm)`, `var(--picker-shell-section-gap-xs)`, `var(--picker-shell-section-gap-md)`, `var(--picker-shell-section-gap-lg)`) — sin acción; el censo lo cuenta como no alcanzable por el límite del regex** — 1: `gap`.
- **ya alcanzable: el privado `--_picker-shell-row-spacing` deriva de un público (`var(--picker-shell-row-spacing-sm)`, `var(--picker-shell-row-spacing-xs)`, `var(--picker-shell-row-spacing-lg)`) — sin acción; el censo lo cuenta como no alcanzable por el límite del regex** — 2: `padding-block-end`, `padding-block-start`.
- **ya alcanzable: el privado `--_picker-shell-gap` deriva de un público (`var(--picker-shell-footer-gap-sm)`, `var(--picker-shell-footer-gap-xs)`, `var(--picker-shell-footer-gap-lg)`) — sin acción; el censo lo cuenta como no alcanzable por el límite del regex** — 1: `gap`.
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
@ -106,6 +79,44 @@ no distingue lo que debería — se marca `⚠`.
<!-- veredicto:start -->
_(pendiente — lo escribe el autor; se conserva al regenerar)_
**Medido 2026-08-21.** La cola avisaba «es CAPA de los pickers: mirar antes qué
posee y qué presta», y mirarlo cambió tres cosas del encargo.
1. **NO es una capa compartida al estilo `list-surface` / `calendar-surface`:
es un COMPONENTE** cuyos `.svelte` importan directamente color-picker,
date-picker y chronos (`import PickerShellRoot from
'../picker-shell/picker-shell.svelte'`). No hay hook de capa ni vocabulario
prestado, así que **no toca `LAYER_VOCABULARY`** y el censo lo puntúa bien.
Se tokeniza como un componente normal, y sus consumidores lo COMPONEN.
2. **Su eje de talla NO es `data-size`: es `data-picker-size` sobre
`[data-popover-content]`**, un ancestro PORTALADO. Los selectores son
`[data-popover-content][data-picker-size='X'] [data-picker-shell]`, y el
TSC no puede emitirlos (su `scope: 'size:xs'` genera
`[data-picker-shell][data-size='xs']`, que no existe). **Consecuencia: las
coordenadas por talla se declaran como públicos PLANOS y la receta las
consume desde sus propios selectores** — el único componente del eje donde
el patrón de nombres resueltos del TSC no aplica. El default de la regla
base es `sm`, y `md` sólo re-declara lo que cambia.
3. **El instrumento del eje NO SIRVE aquí, y por dos razones a la vez.** La
sonda genérica dio **0 nodos** (el antipatrón registrado: comparar cero
valores y pasar): no sabe abrir el popover de un picker, y filtra por
`data-picker-shell*` cuando las partes se llaman `data-picker-header` /
`-body` / `-footer` / `-clear` / `-cancel` / `-close` — nombres genéricos
**a propósito**, dice la receta, «so every picker gets the same visual
contract for free», pero que rompen `data-{component}-{kebab}` y con ello
todo el instrumental. Verificado con sonda dirigida: abriendo el popover del
`date-picker` aparecen 6 nodos y `data-picker-size='md'`.
4. **`picker-time-row.css` usa hooks por CLASE** (`.picker-time-row`,
`.picker-time-label`), que `eidos-lint` cuenta como `class-hooks` y ningún
instrumento que filtre por atributo puede ver — misma deuda que
`gradient-picker` (§13). Y lleva un TERCER eje: `[data-size]` sobre la
propia clase, con default md y xs/sm/lg re-declarando.
5. **Header y footer comparten anchura y color de borde** (`--border-width` +
`--color-border-subtle`), así que es UN par de knobs, no dos.
**Lo que la demo no monta**: el `date-picker` no renderiza `[data-picker-header]`
ni la fila de tiempo, así que esos knobs se verifican forzando el nodo, no
observándolo. Queda registrado: `picker-shell` no tiene ruta de demo propia
(404) y se mide prestado.
<!-- veredicto:end -->

@ -666,6 +666,28 @@ Lo que `textarea` añadió (2026-08-21):
adjudicados por eso. Misma clase que `date-range-picker` con
`kind='month'|'year'` (norma N-6).
Lo que `picker-shell` añadió (2026-08-21):
- **⚠ `picker-shell` no tiene ruta de demo propia** (`/uix/components/picker-shell`
da 404) y sus partes se llaman `data-picker-header` / `-body` / `-footer` en
vez de `data-picker-shell-{parte}` — genéricas **a propósito**, dice su
receta, «so every picker gets the same visual contract for free». La suma de
las dos cosas deja al instrumental ciego: la sonda genérica devolvió **0
nodos**, que es el antipatrón de comparar cero valores y pasar. Se tokenizó
con sonda dirigida (abre el popover del `date-picker`, captura los seis nodos
reales en las cuatro `data-picker-size`: 576 valores, 0 diffs) y el guard
R-5.4 ganó un `COMPONENT_OVERRIDES` para poder abrirlo y filtrarlo — acotado
a este componente, con los otros catorce verificados sin cambio. Lo de fondo
sigue abierto: **una demo propia**, y decidir si el precio de los nombres
genéricos (un contrato visual común para todos los pickers) vale la ceguera
del instrumental.
- **El eje de talla de `picker-shell` es `data-picker-size` sobre un ancestro
PORTALADO**, así que el TSC no puede emitir su cascada (`scope: 'size:xs'`
generaría `[data-picker-shell][data-size='xs']`, que no casa nunca). Es el
único componente del eje cuyas coordenadas por talla quedan como públicos
planos que la receta consume desde sus propios selectores. Si algún día el
TSC gana un eje de scope configurable, éste es su caso de prueba.
Lo que `code-block` añadió (2026-08-21):
- **⚠ La demo de `code-block` NO ENSEÑA el `code-block`: el harness del sitio

@ -114,6 +114,58 @@ export const SENTINEL_EXCEPTIONS: Record<string, Record<string, string>> = {
'sentinel-spinner-reduced-duration':
'behind prefers-reduced-motion; emulated reduce context → reaches (4s → 11.5s)'
},
'picker-shell': {
'section-gap-xs':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',
'section-gap-sm':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',
'section-gap-lg':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',
'row-spacing-xs':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',
'row-spacing-lg':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',
'footer-gap-xs':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',
'footer-gap-lg':
'its size axis is `data-picker-size` on the PORTALED [data-popover-content], not `data-size` on itself, and the guard only cycles `data-size`; the demo popover sits at md. Cycled by hand across the four picker-sizes 2026-08-21 → reaches (section/row/footer spacing follows the picker size)',
'header-font-size-xs':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (12/14/16/16px → 102px, each in its own picker-size)',
'header-font-size-sm':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (12/14/16/16px → 102px, each in its own picker-size)',
'header-font-size-md':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (12/14/16/16px → 102px, each in its own picker-size)',
'header-font-size-lg':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (12/14/16/16px → 102px, each in its own picker-size)',
'header-gap':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (gap 8→1234px, fg → rgb(4,5,6), weight 500→123, line-height 20→60.32px)',
'header-fg':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (gap 8→1234px, fg → rgb(4,5,6), weight 500→123, line-height 20→60.32px)',
'header-font-weight':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (gap 8→1234px, fg → rgb(4,5,6), weight 500→123, line-height 20→60.32px)',
'header-line-height':
'the date-picker demo renders no [data-picker-header] (0 nodes, counted) — picker-shell has no demo route of its own (404) and is measured inside a host picker; forced → reaches (gap 8→1234px, fg → rgb(4,5,6), weight 500→123, line-height 20→60.32px)',
'time-row-gap':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-row-padding-block-start':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-row-padding-inline':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-row-border-width':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-row-border':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-label-fg':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-label-font-size-xs':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-label-font-size-sm':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-label-font-size-md':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)',
'time-label-font-size-lg':
'the time row is CLASS-hooked (.picker-time-row) and the date-picker demo does not render it; the guard filters nodes by attribute, so it cannot see it either way (§13 class-hook debt). Forced → reaches (gap/padding → 1234px, border 1→9px + rgb(7,8,9), label → rgb(10,11,12) / 99px)'
},
spinner: {
'bar-radius':
"the demo mounts the RING variant only (0 bars, 0 dots, counted); forced variant='bars' → reaches (1234px)",

@ -84,6 +84,23 @@ const PROPS = [
'animationDuration'
];
/**
* Components whose DOM does not follow `data-{component}-{part}`, so neither the
* node filter nor the open step can find them by convention.
*
* `picker-shell` names its parts GENERICALLY on purpose — `data-picker-header`
* / `-body` / `-footer` — "so every picker gets the same visual contract for
* free" (its recipe says so), and it has no demo route of its own: it is
* measured inside a host picker, behind that picker's popover. Without this the
* guard reported 0/31 and would have needed 31 false "exceptions".
*/
const COMPONENT_OVERRIDES: Record<string, { attrPrefix?: string; openWith?: string[] }> = {
'picker-shell': {
attrPrefix: 'data-picker',
openWith: ['[data-uix-stage-area] [data-popover-trigger]']
}
};
function sentinelFor(key: string): string {
if (/z$/.test(key)) return '4321';
if (/font-family/.test(key)) return 'Zapfino, cursive';
@ -104,6 +121,8 @@ async function main() {
const [component, urlArg] = process.argv.slice(2);
if (!component) throw new Error('usage: <component> [url]');
const url = urlArg ?? `http://localhost:5173/uix/components/${component}`;
const override = COMPONENT_OVERRIDES[component] ?? {};
const attrPrefix = override.attrPrefix ?? `data-${component}`;
const contract = readFileSync(resolve('src/uix/eidos/lib/recipes/base.ts'), 'utf8').replace(
/\r\n/g,
'\n'
@ -141,6 +160,7 @@ async function main() {
// negative). Popovers dismiss on outside pointerdown, not on blur, so the
// open state survives.
for (const sel of [
...(override.openWith ?? []),
`[data-${component}-trigger]`,
`[data-${component}-input]`,
`[data-${component}-stop]`
@ -173,9 +193,15 @@ async function main() {
// so the pointer parking below is not belt-and-braces — without it the
// cursor sits on the input for the whole run.
const reopen = async () => {
const open = await page.locator(`[data-${component}-content]`).count();
const open = await page.locator(
override.attrPrefix ? `[data-${component}]` : `[data-${component}-content]`
).count();
if (open) return;
for (const sel of [`[data-${component}-trigger]`, `[data-${component}-input]`]) {
for (const sel of [
...(override.openWith ?? []),
`[data-${component}-trigger]`,
`[data-${component}-input]`
]) {
const el = page.locator(sel).first();
if (await el.count()) {
try {
@ -193,10 +219,10 @@ async function main() {
const staticPass = (key: string, value: string) =>
page.evaluate(
([kebab, token, val, props]) => {
([kebab, token, val, props, prefix]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`))
[...n.attributes].some((a) => a.name.startsWith(prefix))
);
// Write on :root AND on every component node: resolved names are
// declared per part (root/host), and a portaled panel never sees the
@ -239,7 +265,7 @@ async function main() {
sized.forEach((n, i) => (original[i] ? n.setAttribute('data-size', original[i]!) : null));
return moved;
},
[component, key, value, PROPS] as const
[component, key, value, PROPS, attrPrefix] as const
);
// Hover pass — a `hover-*` token can only move a computed value while some
@ -247,19 +273,19 @@ async function main() {
// hover-preset-border were false negatives without it).
const hoverPass = async (key: string, value: string) => {
const count = await page.evaluate(
(kebab) =>
(prefix) =>
[...document.querySelectorAll('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`))
[...n.attributes].some((a) => a.name.startsWith(prefix))
).length,
component
attrPrefix
);
for (let i = 0; i < Math.min(count, 30); i++) {
const handle = await page.evaluateHandle(
([kebab, idx]) =>
([prefix, idx]) =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`))
[...n.attributes].some((a) => a.name.startsWith(prefix))
)[idx as number] ?? null,
[component, i] as const
[attrPrefix, i] as const
);
const el = handle.asElement();
if (!el) continue;
@ -269,10 +295,10 @@ async function main() {
continue;
}
const moved = await page.evaluate(
([kebab, token, val, props, idx]) => {
([prefix, token, val, props, idx, kebab]) => {
const all = () =>
[...document.querySelectorAll<HTMLElement>('*')].filter((n) =>
[...n.attributes].some((a) => a.name.startsWith(`data-${kebab}`))
[...n.attributes].some((a) => a.name.startsWith(prefix))
);
const node = all()[idx as number];
if (!node) return false;
@ -288,7 +314,7 @@ async function main() {
h.style.removeProperty(`--${kebab}-${token}`);
return before !== after;
},
[component, key, value, PROPS, i] as const
[attrPrefix, key, value, PROPS, i, component] as const
);
if (moved) return true;
}

@ -49,3 +49,55 @@ MyPicker.Clear = Clear;
## Closes
audit-codex-full-1.md P1 #5 — "picker-shell existe pero no esta cerrado como pieza de sistema". Closed as INTERNAL primitive per the audit's own recommendation: *"Si es interna, no deberia salir como demo publica ni Morfo publico incompleto."*
## Talla y tema
**Guard R-5.4** (`npm run theming:sentinel -- picker-shell <url de un picker>`):
6/31 tokens mueven un computed en la demo del `date-picker`; los 25 restantes
están adjudicados uno a uno con su medición en
`scripts/theming-sentinel-exceptions.ts`. Corrido 2026-08-21.
Contrato en `lib/recipes/base.ts > picker-shell` (31 claves, entrada NUEVA).
Alcance del censo: **76 %** (era 0 %).
**No es una capa compartida: es un COMPONENTE.** A diferencia de
`list-surface` o `calendar-surface`, no hay hook de capa ni vocabulario
prestado — color-picker, date-picker y chronos importan sus `.svelte`
directamente. Se tokeniza como un componente normal y sus consumidores lo
COMPONEN.
**Su eje de talla no es `data-size`.** Es `data-picker-size`, estampado sobre
`[data-popover-content]` — un ancestro PORTALADO —, y los selectores son
`[data-popover-content][data-picker-size='X'] [data-picker-shell]`. El TSC no
puede emitirlos: su `scope: 'size:xs'` genera `[data-picker-shell][data-size='xs']`,
que no existe nunca. **Por eso las coordenadas por talla son públicos PLANOS y
la receta las consume desde sus propios selectores** — el único componente del
eje donde el patrón de nombres resueltos no aplica. La regla base es el default
`sm`; `md` sólo re-declara lo que cambia. La fila de tiempo lleva un TERCER
eje, `data-size` sobre su propia clase, con default md.
Header y footer comparten la regla que los separa del cuerpo, así que es UN par
de knobs (`border-width` + `border`), no dos.
### ⚠ El instrumental del eje no ve este componente por convención
Sus partes se llaman `data-picker-header` / `-body` / `-footer` / `-clear` /
`-cancel` / `-close` — genéricas **a propósito**, «so every picker gets the same
visual contract for free», dice la receta — en vez de
`data-picker-shell-{parte}`. Con eso, todo instrumento que filtre por el
prefijo del componente ve sólo la raíz. Y `picker-shell` **no tiene ruta de
demo propia** (404): se mide prestado, dentro de un picker anfitrión y detrás
de su popover.
La sonda genérica devolvía **0 nodos** — el antipatrón de comparar cero valores
y pasar. La verificación de este commit se hizo con sonda dirigida (abre el
popover del `date-picker`, captura los seis nodos reales en las cuatro
picker-sizes): **576 valores, 0 diffs**, y los knobs que la demo no monta
(header y fila de tiempo) se midieron forzando el nodo. El guard R-5.4 ganó un
`COMPONENT_OVERRIDES` acotado a este componente para poder abrirlo y filtrarlo
— verificado que los otros catorce con ledger no se mueven.
`picker-time-row.css` usa hooks por CLASE (`.picker-time-row`,
`.picker-time-label`), deuda preexistente que `eidos-lint` cuenta y ningún
filtro por atributo puede ver (`next-features.md` §13, misma clase que
`gradient-picker`).

@ -13,7 +13,7 @@
/* Outer column wrapper. Size-scaling vars default to `sm`; host
* pickers re-tune via `[data-popover-content][data-picker-size='X']`
* scopes below. */
--_picker-shell-section-gap: var(--space-3);
--_picker-shell-section-gap: var(--picker-shell-section-gap-sm);
display: flex;
flex-direction: column;
@ -25,23 +25,23 @@
/* Mirror the Footer's bottom border by emitting a bottom-border +
* spacing block. Reuses the same `--_picker-shell-row-spacing` token
* so xs/sm/md/lg scale symmetrically with the action row below. */
--_picker-shell-row-spacing: var(--space-3);
--_picker-shell-row-spacing: var(--picker-shell-row-spacing-sm);
display: flex;
align-items: center;
gap: var(--space-2);
gap: var(--picker-shell-header-gap);
padding-block-end: var(--_picker-shell-row-spacing);
border-block-end: var(--border-width) solid var(--color-border-subtle);
color: var(--color-content-primary);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
line-height: var(--leading-ui);
border-block-end: var(--picker-shell-border-width) solid var(--picker-shell-border);
color: var(--picker-shell-header-fg);
font-size: var(--picker-shell-header-font-size-sm);
font-weight: var(--picker-shell-header-font-weight);
line-height: var(--picker-shell-header-line-height);
}
[data-picker-body] {
display: flex;
flex-direction: column;
gap: var(--space-3);
gap: var(--picker-shell-body-gap);
flex: 1 1 auto;
min-block-size: 0;
}
@ -51,14 +51,14 @@
* match `sm`; host picker scopes these via
* `[data-popover-content][data-picker-size='X']` so the row spacing
* scales with the picker. */
--_picker-shell-gap: var(--space-2);
--_picker-shell-row-spacing: var(--space-3);
--_picker-shell-gap: var(--picker-shell-footer-gap-sm);
--_picker-shell-row-spacing: var(--picker-shell-row-spacing-sm);
display: flex;
align-items: center;
gap: var(--_picker-shell-gap);
padding-block-start: var(--_picker-shell-row-spacing);
border-block-start: var(--border-width) solid var(--color-border-subtle);
border-block-start: var(--picker-shell-border-width) solid var(--picker-shell-border);
}
/* Standalone Footer (legacy composition without Root) keeps its own
@ -82,32 +82,32 @@
* spacing. The Footer buttons stay xs regardless of picker size because
* footer actions must NOT visually compete with the picker body. */
[data-popover-content][data-picker-size='xs'] [data-picker-shell] {
--_picker-shell-section-gap: var(--space-2);
--_picker-shell-section-gap: var(--picker-shell-section-gap-xs);
}
[data-popover-content][data-picker-size='xs'] [data-picker-header] {
--_picker-shell-row-spacing: var(--space-2);
font-size: var(--font-size-xs);
--_picker-shell-row-spacing: var(--picker-shell-row-spacing-xs);
font-size: var(--picker-shell-header-font-size-xs);
}
[data-popover-content][data-picker-size='xs'] [data-picker-footer] {
--_picker-shell-gap: var(--space-1-5);
--_picker-shell-row-spacing: var(--space-2);
--_picker-shell-gap: var(--picker-shell-footer-gap-xs);
--_picker-shell-row-spacing: var(--picker-shell-row-spacing-xs);
}
[data-popover-content][data-picker-size='md'] [data-picker-shell] {
--_picker-shell-section-gap: var(--space-3-5);
--_picker-shell-section-gap: var(--picker-shell-section-gap-md);
}
[data-popover-content][data-picker-size='md'] [data-picker-header] {
font-size: var(--font-size-md);
font-size: var(--picker-shell-header-font-size-md);
}
[data-popover-content][data-picker-size='lg'] [data-picker-shell] {
--_picker-shell-section-gap: var(--space-4);
--_picker-shell-section-gap: var(--picker-shell-section-gap-lg);
}
[data-popover-content][data-picker-size='lg'] [data-picker-header] {
--_picker-shell-row-spacing: var(--space-4);
font-size: var(--font-size-md);
--_picker-shell-row-spacing: var(--picker-shell-row-spacing-lg);
font-size: var(--picker-shell-header-font-size-lg);
}
[data-popover-content][data-picker-size='lg'] [data-picker-footer] {
--_picker-shell-gap: var(--space-2-5);
--_picker-shell-row-spacing: var(--space-4);
--_picker-shell-gap: var(--picker-shell-footer-gap-lg);
--_picker-shell-row-spacing: var(--picker-shell-row-spacing-lg);
}

@ -6,30 +6,31 @@
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-3);
padding-block-start: var(--space-2);
padding-inline: var(--space-2);
border-block-start: var(--border-width, 1px) solid var(--color-neutral-border);
gap: var(--picker-shell-time-row-gap);
padding-block-start: var(--picker-shell-time-row-padding-block-start);
padding-inline: var(--picker-shell-time-row-padding-inline);
border-block-start: var(--picker-shell-time-row-border-width) solid
var(--picker-shell-time-row-border);
/* Caption follows the canonical label scale (one step below the control),
keyed off the picker size. Mirrors recipes/base.ts label-font-size-*. */
--_picker-time-label-font: var(--font-size-sm);
--_picker-time-label-font: var(--picker-shell-time-label-font-size-md);
}
.picker-time-row[data-size='xs'] {
--_picker-time-label-font: var(--font-size-xxs);
--_picker-time-label-font: var(--picker-shell-time-label-font-size-xs);
}
.picker-time-row[data-size='sm'] {
--_picker-time-label-font: var(--font-size-xs);
--_picker-time-label-font: var(--picker-shell-time-label-font-size-sm);
}
.picker-time-row[data-size='lg'] {
--_picker-time-label-font: var(--font-size-md);
--_picker-time-label-font: var(--picker-shell-time-label-font-size-lg);
}
.picker-time-label {
flex: 0 0 auto;
color: var(--color-content-secondary);
color: var(--picker-shell-time-label-fg);
font-size: var(--_picker-time-label-font);
}

@ -4016,6 +4016,37 @@
--splitter-grip-cross: 2px;
--splitter-transition-duration: var(--duration-fast);
--splitter-transition-ease: var(--ease-default);
--picker-shell-section-gap-xs: var(--space-2);
--picker-shell-section-gap-sm: var(--space-3);
--picker-shell-section-gap-md: var(--space-3-5);
--picker-shell-section-gap-lg: var(--space-4);
--picker-shell-row-spacing-xs: var(--space-2);
--picker-shell-row-spacing-sm: var(--space-3);
--picker-shell-row-spacing-lg: var(--space-4);
--picker-shell-footer-gap-xs: var(--space-1-5);
--picker-shell-footer-gap-sm: var(--space-2);
--picker-shell-footer-gap-lg: var(--space-2-5);
--picker-shell-header-font-size-xs: var(--size-xs-font-size);
--picker-shell-header-font-size-sm: var(--size-sm-font-size);
--picker-shell-header-font-size-md: var(--size-md-font-size);
--picker-shell-header-font-size-lg: var(--size-md-font-size);
--picker-shell-border-width: var(--border-width);
--picker-shell-border: var(--color-border-subtle);
--picker-shell-header-gap: var(--space-2);
--picker-shell-header-fg: var(--color-content-primary);
--picker-shell-header-font-weight: var(--font-weight-medium);
--picker-shell-header-line-height: var(--leading-ui);
--picker-shell-body-gap: var(--space-3);
--picker-shell-time-row-gap: var(--space-3);
--picker-shell-time-row-padding-block-start: var(--space-2);
--picker-shell-time-row-padding-inline: var(--space-2);
--picker-shell-time-row-border-width: var(--border-width);
--picker-shell-time-row-border: var(--color-neutral-border);
--picker-shell-time-label-fg: var(--color-content-secondary);
--picker-shell-time-label-font-size-xs: var(--size-xxs-font-size);
--picker-shell-time-label-font-size-sm: var(--size-xs-font-size);
--picker-shell-time-label-font-size-md: var(--size-sm-font-size);
--picker-shell-time-label-font-size-lg: var(--size-md-font-size);
--card-group-padding-sm: var(--space-3);
--card-group-padding-md: var(--space-4);
--card-group-padding-lg: var(--space-5);

@ -6434,6 +6434,59 @@ export const THEME_BASE_RECIPE_TOKENS = defineRecipes({
// floating layer mirrors this computed z onto the positioner wrapper). The
// band is deliberately NOT the global `--z-index-*` ladder — see
// STATIC_Z_INDEX_OVERLAY.
// PickerShell is a COMPONENT the composite pickers import (date, color,
// chronos), not a shared CSS layer — there is no layer hook and no borrowed
// vocabulary.
//
// Its size axis is `data-picker-size` on `[data-popover-content]`, a PORTALED
// ancestor — not `data-size` on itself. The TSC's `size:` scope would emit
// `[data-picker-shell][data-size='x']`, which never matches, so these are
// FLAT public coordinates and the recipe reads them from its own
// `[data-popover-content][data-picker-size='X']` selectors. The base rule is
// the `sm` default; `md` only re-declares what actually changes.
'picker-shell': {
'section-gap-xs': 'var(--space-2)',
'section-gap-sm': 'var(--space-3)',
'section-gap-md': 'var(--space-3-5)',
'section-gap-lg': 'var(--space-4)',
// Header's bottom inset and footer's top inset, one number so the two rows
// stay symmetric around the body.
'row-spacing-xs': 'var(--space-2)',
'row-spacing-sm': 'var(--space-3)',
'row-spacing-lg': 'var(--space-4)',
'footer-gap-xs': 'var(--space-1-5)',
'footer-gap-sm': 'var(--space-2)',
'footer-gap-lg': 'var(--space-2-5)',
'header-font-size-xs': 'var(--size-xs-font-size)',
'header-font-size-sm': 'var(--size-sm-font-size)',
'header-font-size-md': 'var(--size-md-font-size)',
'header-font-size-lg': 'var(--size-md-font-size)',
// Header and footer share the rule that separates them from the body: one
// width, one colour.
'border-width': 'var(--border-width)',
border: 'var(--color-border-subtle)',
'header-gap': 'var(--space-2)',
'header-fg': 'var(--color-content-primary)',
'header-font-weight': 'var(--font-weight-medium)',
'header-line-height': 'var(--leading-ui)',
'body-gap': 'var(--space-3)',
// The time row of the composite date+time pickers. It hangs off CLASS
// hooks (`.picker-time-row`) and carries a THIRD axis — `data-size` on the
// class itself, default md — so its coordinates are flat too. The class
// hooks are pre-existing debt (next-features §13, same as gradient-picker).
'time-row-gap': 'var(--space-3)',
'time-row-padding-block-start': 'var(--space-2)',
'time-row-padding-inline': 'var(--space-2)',
'time-row-border-width': 'var(--border-width)',
'time-row-border': 'var(--color-neutral-border)',
'time-label-fg': 'var(--color-content-secondary)',
'time-label-font-size-xs': 'var(--size-xxs-font-size)',
'time-label-font-size-sm': 'var(--size-xs-font-size)',
'time-label-font-size-md': 'var(--size-sm-font-size)',
'time-label-font-size-lg': 'var(--size-md-font-size)'
},
// CardGroup is a COMPOSITION SHELL (ToggleGroup + Collapsible + Card +
// Button): the cards' chrome belongs to Card's recipe and the title's to
// Button's. Only group-level visual lives here. The size axis is sm|md|lg —

Loading…
Cancel
Save

Powered by TurnKey Linux.