You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/docs/audit/theming/time-picker.md

250 lines
21 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# time-picker — alcance de tema: análisis y propuesta
> Generado por `node --import tsx/esm scripts/theming-census.ts --report`.
> Lo **medido** y la **propuesta** se regeneran; el **Veredicto** (§5) se conserva.
> Vista de conjunto: [README](./README.md) · método y protocolo:
> [`PLAN-theming.md`](../../process/PLAN-theming.md) §1, §2, §7.
- **Medido**: 2026-08-23 · **Alcance**: **18%** — 10 de 57 knobs por token público
- **Knobs de apariencia**: 59 — público 10 · privado 12 · global 24 · literal 11 · sistema 2 · excepción 0 _(los dos últimos, fuera del ratio)_
- **Contrato hoy** (`lib/recipes/base.ts`): 16 pública(s) — `accent-border`, `accent-track`, `accent-text`, `gap`, `clock-gap`, `clock-width`, `clock-fg`, `clock-font-family`, `clock-font-size`, `clock-line-height`, `clock-hand-hour`, `clock-hand-minute`, `clock-hand-second`, `clock-thumb-shadow`, `clock-thumb-ring`, `trigger-radius`
- **Eje `size`**: sí · **ficheros**: `time-picker.css`
## 1. Knobs fuera de alcance
### 1.1 Directo a primitivo global (24)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `time-picker.css:19` | `[data-time-picker]` | `color` | `var(--field-control-fg)` ⤴ prestado de `field` |
| 2 | `time-picker.css:20` | `[data-time-picker]` | `font-family` | `var(--field-font-family)` ⤴ prestado de `field` |
| 3 | `time-picker.css:22` | `[data-time-picker]` | `line-height` | `var(--field-control-line-height)` ⤴ prestado de `field` |
| 4 | `time-picker.css:86` | `[data-time-picker-trigger]` | `border` | `var(--field-control-border-width) solid var(--field-control-border)` ⤴ prestado de `field` |
| 5 | `time-picker.css:87` | `[data-time-picker-trigger]` | `border-radius` | `var(--field-control-radius)` ⤴ prestado de `field` |
| 6 | `time-picker.css:88` | `[data-time-picker-trigger]` | `background` | `var(--field-control-bg)` ⤴ prestado de `field` |
| 7 | `time-picker.css:114` | `[data-time-picker-trigger][data-disabled], [data-time-picker-trigger]:disabled` | `opacity` | `var(--field-disabled-opacity)` ⤴ prestado de `field` |
| 8 | `time-picker.css:123` | `[data-time-field-input] [data-time-picker-trigger]` | `inline-size` | `var(--field-segment-height)` ⤴ prestado de `field` |
| 9 | `time-picker.css:124` | `[data-time-field-input] [data-time-picker-trigger]` | `block-size` | `var(--field-segment-height)` ⤴ prestado de `field` |
| 10 | `time-picker.css:130` | `[data-time-field-input] [data-time-picker-trigger]` | `color` | `var(--field-control-fg)` ⤴ prestado de `field` |
| 11 | `time-picker.css:144` | `[data-time-field-input] [data-time-picker-trigger]:focus-visible` | `background` | `var(--field-segment-active-bg)` ⤴ prestado de `field` |
| 12 | `time-picker.css:145` | `[data-time-field-input] [data-time-picker-trigger]:focus-visible` | `color` | `var(--field-segment-active-text)` ⤴ prestado de `field` |
| 13 | `time-picker.css:160` | `[data-time-picker-clock]` | `padding` | `var(--space-2-5)` |
| 14 | `time-picker.css:204` | `[data-time-picker-clock] [data-time-picker-day-period-toggle] [data-toggle-group-item][data-state='on']:hover` | `background` | `var(--toggle-palette-solid)` ⤴ prestado de `toggle` |
| 15 | `time-picker.css:205` | `[data-time-picker-clock] [data-time-picker-day-period-toggle] [data-toggle-group-item][data-state='on']:hover` | `border-color` | `var(--toggle-palette-solid)` ⤴ prestado de `toggle` |
| 16 | `time-picker.css:222` | `.time-picker-slider-label` | `color` | `var(--color-content-secondary)` |
| 17 | `time-picker.css:223` | `.time-picker-slider-label` | `font-family` | `var(--style-label-font-family)` |
| 18 | `time-picker.css:225` | `.time-picker-slider-label` | `font-weight` | `var(--font-weight-medium)` |
| 19 | `time-picker.css:226` | `.time-picker-slider-label` | `letter-spacing` | `var(--tracking-caps)` |
| 20 | `time-picker.css:237` | `.time-picker-scale` | `padding-inline` | `calc(var(--_slider-thumb-size, 1rem) / 2)` |
| 21 | `time-picker.css:249` | `.time-picker-scale-tick` | `color` | `var(--color-content-muted)` |
| 22 | `time-picker.css:250` | `.time-picker-scale-tick` | `font-family` | `var(--font-family-mono, var(--style-label-font-family))` |
| 23 | `time-picker.css:253` | `.time-picker-scale-tick` | `font-size` | `var(--font-size-xxs)` |
| 24 | `time-picker.css:262` | `.time-picker-scale-mark` | `background` | `var(--color-border-default)` |
### 1.2 A través de un privado (12)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `time-picker.css:21` | `[data-time-picker]` | `font-size` | `var(--_time-picker-font-size)` |
| 2 | `time-picker.css:83` | `[data-time-picker-trigger]` | `inline-size` | `var(--_time-picker-height)` |
| 3 | `time-picker.css:84` | `[data-time-picker-trigger]` | `block-size` | `var(--_time-picker-height)` |
| 4 | `time-picker.css:99` | `[data-time-picker-trigger]:hover` | `border-color` | `var(--_time-picker-accent-border)` |
| 5 | `time-picker.css:100` | `[data-time-picker-trigger]:hover` | `background` | `var(--_time-picker-accent-track)` |
| 6 | `time-picker.css:101` | `[data-time-picker-trigger]:hover` | `color` | `var(--_time-picker-accent-text)` |
| 7 | `time-picker.css:105` | `[data-time-picker-trigger]:focus-visible` | `border-color` | `var(--_time-picker-accent-border)` |
| 8 | `time-picker.css:137` | `[data-time-field-input] [data-time-picker-trigger]:hover` | `background` | `var(--_time-picker-accent-track)` |
| 9 | `time-picker.css:138` | `[data-time-field-input] [data-time-picker-trigger]:hover` | `color` | `var(--_time-picker-accent-text)` |
| 10 | `time-picker.css:224` | `.time-picker-slider-label` | `font-size` | `var(--_time-picker-label-font, var(--font-size-xxs))` |
| 11 | `time-picker.css:308` | `[data-time-picker-clock] [data-slider-thumb]` | `border-color` | `var(--_time-picker-slider-accent, var(--_time-picker-accent-border))` |
| 12 | `time-picker.css:323` | `[data-time-picker-clock] [data-slider-range]` | `background` | `var(--_time-picker-slider-accent, var(--_time-picker-accent-border))` |
### 1.3 Literales (11)
| # | fichero:línea | selector | propiedad | valor |
| ---: | --- | --- | --- | --- |
| 1 | `time-picker.css:17` | `[data-time-picker]` | `inline-size` | `100%` |
| 2 | `time-picker.css:185` | `[data-time-picker-clock] [data-slider]` | `inline-size` | `100%` |
| 3 | `time-picker.css:213` | `.time-picker-slider` | `gap` | `2px` |
| 4 | `time-picker.css:214` | `.time-picker-slider` | `inline-size` | `100%` |
| 5 | `time-picker.css:227` | `.time-picker-slider-label` | `line-height` | `1` |
| 6 | `time-picker.css:233` | `.time-picker-scale` | `block-size` | `0.875rem` |
| 7 | `time-picker.css:234` | `.time-picker-scale` | `inline-size` | `100%` |
| 8 | `time-picker.css:248` | `.time-picker-scale-tick` | `gap` | `1px` |
| 9 | `time-picker.css:254` | `.time-picker-scale-tick` | `line-height` | `1` |
| 10 | `time-picker.css:260` | `.time-picker-scale-mark` | `inline-size` | `1px` |
| 11 | `time-picker.css:261` | `.time-picker-scale-mark` | `block-size` | `4px` |
### 1.4 Excepciones firmadas (0) — fuera del ratio
Literales que llevan su anotación `/* literal: <razón> */` en la propia
declaración: la válvula de recipe-contract §3, la misma que honra
`component-audit`. **Una desviación firmada no es deuda** — se listan para que la
razón se lea, no para acuñarlas.
_Ninguno._
## 2. Sistema transversal (2) — 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 | `time-picker.css:107` | `[data-time-picker-trigger]:focus-visible` | `outline` | `var(--focus-ring-width) solid var(--focus-ring-color)` |
| 2 | `time-picker.css:318` | `[data-time-picker-clock] [data-slider-thumb]: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? |
| --- | ---: | --- | --- | :-: |
| `--_time-picker-height` | 4 | `var(--field-control-height-md)`, `var(--field-control-height-xs)`, `var(--field-control-height-sm)`, `var(--field-control-height-lg)` | global | no |
| `--_time-picker-font-size` | 4 | `var(--field-control-font-size-md)`, `var(--field-control-font-size-xs)`, `var(--field-control-font-size-sm)`, `var(--field-control-font-size-lg)` | global | no |
| `--_time-picker-accent-border` | 8 | `var(--time-picker-accent-border)`, `var(--color-secondary-border)`, `var(--color-neutral-border)`, `var(--color-affirm-border)`, `var(--color-fulfill-border)`, `var(--color-risk-border)` …(+2) | global, public | no |
| `--_time-picker-accent-track` | 8 | `var(--time-picker-accent-track)`, `var(--color-secondary-track)`, `var(--color-neutral-track)`, `var(--color-affirm-track)`, `var(--color-fulfill-track)`, `var(--color-risk-track)` …(+2) | global, public | no |
| `--_time-picker-accent-text` | 8 | `var(--time-picker-accent-text)`, `var(--color-secondary-text)`, `var(--color-neutral-text)`, `var(--color-affirm-text)`, `var(--color-fulfill-text)`, `var(--color-risk-text)` …(+2) | global, public | no |
| `--_time-picker-label-font` | 4 | `var(--font-size-sm)`, `var(--font-size-xxs)`, `var(--font-size-xs)`, `var(--font-size-md)` | global | no |
| `--_time-picker-slider-accent` | 3 | `var(--time-picker-clock-hand-hour)`, `var(--time-picker-clock-hand-minute)`, `var(--time-picker-clock-hand-second)` | public | **sí** |
| `--_time-picker-slider-track` | 3 | `var(--time-picker-accent-track)`, `var(--color-secondary-track)`, `var(--color-neutral-track)` | global, public | no |
## 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** `--time-picker-{eje}` para él — sería un vocabulario paralelo (README de `eidos/components`, «Capas compartidas» regla 2).
- **Consume tokens públicos de `field`, `toggle`.** Un token prestado importa la semántica de su dueño: la corrección no es duplicarlo con prefijo propio, sino la decisión de familia que la auditoría de fase 1 dejó registrada (`theming-audit.md` §B, familia calendar).
- **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` (35)
Valor **verbatim** del CSS de hoy: el default no se mueve, sólo cambia quién
puede moverlo. Nombres derivados de recipe-contract §1 (ejes lógicos, talla
al final) y theming §6.7 (slots de color, modificador delante). Un token con
DOS valores distintos es una colisión de nombre: son dos knobs, o el nombre
no distingue lo que debería — se marca `⚠`.
| token (`--time-picker-…`) | scope TSC | valor propuesto | usos |
| --- | --- | --- | ---: |
| `hover-trigger-bg` | `root` | ⚠ `var(--time-picker-accent-track)` / `var(--color-secondary-track)` / `var(--color-neutral-track)` / `var(--color-affirm-track)` / `var(--color-fulfill-track)` / `var(--color-risk-track)` / `var(--color-threat-track)` / `var(--color-loss-track)` | 16 |
| `hover-trigger-fg` | `root` | ⚠ `var(--time-picker-accent-text)` / `var(--color-secondary-text)` / `var(--color-neutral-text)` / `var(--color-affirm-text)` / `var(--color-fulfill-text)` / `var(--color-risk-text)` / `var(--color-threat-text)` / `var(--color-loss-text)` | 16 |
| `hover-trigger-border` | `root` | ⚠ `var(--time-picker-accent-border)` / `var(--color-secondary-border)` / `var(--color-neutral-border)` / `var(--color-affirm-border)` / `var(--color-fulfill-border)` / `var(--color-risk-border)` / `var(--color-threat-border)` / `var(--color-loss-border)` | 8 |
| `trigger-border` | `root` | ⚠ `var(--time-picker-accent-border)` / `var(--color-secondary-border)` / `var(--color-neutral-border)` / `var(--color-affirm-border)` / `var(--color-fulfill-border)` / `var(--color-risk-border)` / `var(--color-threat-border)` / `var(--color-loss-border)` | 8 |
| `fg` | `root` | ⚠ `var(--field-control-fg)` / `var(--color-content-secondary)` / `var(--color-content-muted)` | 3 |
| `font-family` | `root` | ⚠ `var(--field-font-family)` / `var(--style-label-font-family)` / `var(--font-family-mono, var(--style-label-font-family))` | 3 |
| `font-size-md` | `size:md` | ⚠ `var(--field-control-font-size-md)` / `var(--font-size-sm)` | 2 |
| `font-size-xs` | `size:xs` | ⚠ `var(--field-control-font-size-xs)` / `var(--font-size-xxs)` | 2 |
| `font-size-sm` | `size:sm` | ⚠ `var(--field-control-font-size-sm)` / `var(--font-size-xs)` | 2 |
| `font-size-lg` | `size:lg` | ⚠ `var(--field-control-font-size-lg)` / `var(--font-size-md)` | 2 |
| `trigger-bg` | `root` | ⚠ `var(--field-control-bg)` / `var(--field-segment-active-bg)` | 2 |
| `trigger-fg` | `root` | ⚠ `var(--field-control-fg)` / `var(--field-segment-active-text)` | 2 |
| `gap` _(ya existe)_ | `root` | ⚠ `2px` / `1px` | 2 |
| `height` | `root` | ⚠ `0.875rem` / `4px` | 2 |
| `line-height` | `root` | `var(--field-control-line-height)` | 1 |
| `trigger-width-md` | `size:md` | `var(--field-control-height-md)` | 1 |
| `trigger-width-xs` | `size:xs` | `var(--field-control-height-xs)` | 1 |
| `trigger-width-sm` | `size:sm` | `var(--field-control-height-sm)` | 1 |
| `trigger-width-lg` | `size:lg` | `var(--field-control-height-lg)` | 1 |
| `trigger-height-md` | `size:md` | `var(--field-control-height-md)` | 1 |
| `trigger-height-xs` | `size:xs` | `var(--field-control-height-xs)` | 1 |
| `trigger-height-sm` | `size:sm` | `var(--field-control-height-sm)` | 1 |
| `trigger-height-lg` | `size:lg` | `var(--field-control-height-lg)` | 1 |
| `trigger-radius` _(ya existe)_ | `root` | `var(--field-control-radius)` | 1 |
| `disabled-trigger-opacity` | `root` | `var(--field-disabled-opacity)` | 1 |
| `trigger-width` | `root` | `var(--field-segment-height)` | 1 |
| `trigger-height` | `root` | `var(--field-segment-height)` | 1 |
| `hover-clock-bg` | `host` | `var(--toggle-palette-solid)` | 1 |
| `hover-clock-border` | `host` | `var(--toggle-palette-solid)` | 1 |
| `font-weight` | `root` | `var(--font-weight-medium)` | 1 |
| `letter-spacing` | `root` | `var(--tracking-caps)` | 1 |
| `padding-inline` | `root` | `calc(var(--_slider-thumb-size, 1rem) / 2)` | 1 |
| `font-size` | `root` | `var(--font-size-xxs)` | 1 |
| `width` | `root` | `1px` | 1 |
| `bg` | `root` | `var(--color-border-default)` | 1 |
### 4.2 Sin nombre mecánico (10)
- **⚠ 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** — 4: `inline-size`.
- **⚠ 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)** — 1: `border`.
- **⚠ decisión: `padding` es shorthand o eje físico — hay que partirlo en ejes lógicos antes de nombrarlo (recipe-contract §1, R-4.4)** — 1: `padding`.
- **⚠ decisión: `1` 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** — 2: `line-height`.
- **ya alcanzable: el privado `--_time-picker-slider-accent` deriva de un público (`var(--time-picker-clock-hand-hour)`, `var(--time-picker-clock-hand-minute)`, `var(--time-picker-clock-hand-second)`) — sin acción; el censo lo cuenta como no alcanzable por el límite del regex** — 2: `border-color`, `background`.
### 4.3 Avisos sobre los tokens propuestos (7)
- **el privado `--_time-picker-font-size` debe pasar a leer este público (o desaparecer)** — `--time-picker-font-size-md`, `--time-picker-font-size-xs`, `--time-picker-font-size-sm`, `--time-picker-font-size-lg`
- **el privado `--_time-picker-height` debe pasar a leer este público (o desaparecer)** — `--time-picker-trigger-width-md`, `--time-picker-trigger-width-xs`, `--time-picker-trigger-width-sm`, `--time-picker-trigger-width-lg`, `--time-picker-trigger-height-md`, `--time-picker-trigger-height-xs`, `--time-picker-trigger-height-sm`, `--time-picker-trigger-height-lg`
- **el privado `--_time-picker-accent-border` debe pasar a leer este público (o desaparecer)** — `--time-picker-hover-trigger-border`, `--time-picker-trigger-border`
- **el privado `--_time-picker-accent-track` debe pasar a leer este público (o desaparecer)** — `--time-picker-hover-trigger-bg`
- **el privado `--_time-picker-accent-text` debe pasar a leer este público (o desaparecer)** — `--time-picker-hover-trigger-fg`
- **el privado `--_time-picker-label-font` debe pasar a leer este público (o desaparecer)** — `--time-picker-font-size-md`, `--time-picker-font-size-xs`, `--time-picker-font-size-sm`, `--time-picker-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** — `--time-picker-padding-inline`
### 4.4 Lo que hay que comprobar a mano (PLAN-theming §1.3 · §7.4)
- [ ] **Privado que no deriva de un público** — §3 lo marca; el privado debe leer el público o desaparecer.
- [ ] **Velo o acento en el nodo equivocado** (`archetype: 'item'` en un envoltorio, un `background` en shorthand que mata la capa de estado) — se mide desde el píxel hacia arriba.
- [ ] **Doble animación** al mover un sello a una superficie con animación propia — registro de `animationstart`/`animationend`.
- [ ] **Diff de computed = 0** en reposo · hover · abierto · disabled · foco, por talla, antes y después.
- [ ] **Centinela por token nuevo**: valor imposible en el root → el nodo lo sigue. Si no, el token miente.
## 5. Veredicto
<!-- veredicto:start -->
**Revisión 2026-08-20 — verificación previa a implementación (Opus).**
Gemelo de `time-range-picker` — misma familia, mismos defectos; su ficha lleva
el detalle y ésta las diferencias.
**Análisis (§1–§3): CORRECTO en cifras** (59 · 0 · 3 · 42 · 12 · 2), con el
mismo hallazgo: los «ajenos» `--_time-field-*` **los declara este CSS con
prefijo equivocado** (`time-picker.css:8-12`) — renombrar a
`--_time-picker-*` (mecánico, `git ls-files` antes) es el paso previo a todo.
**Propuesta (§4.1): NO APTA tal cual**, por las mismas tres causas
verificadas: colisiones por estado sin parsear (`clock-bg/border` mezclan
manecillas y ticks primary/secondary/neutral; `gap`/`height` colapsan pistas
y separadores), composición (Field + Slider + Toggle + `picker-shell`: lo que
lee `--field-*`/`--slider-*` públicos se tema en la ficha del dueño;
`padding-inline: calc(var(--_slider-thumb-size,…)/2)` consume un privado DE
SLIDER → decisión del dueño), y `hover-clock-bg/border =
var(--toggle-palette-solid)` con scope `host` inválido (paleta ajena ⇒ bloque
`composition:` o nada). Diferencia con TRP: aquí el trigger además monta
sobre `--field-segment-height` (la vía segmento), que refuerza el mandato
field-composition de `theming-audit.md` §5.3-3.
**Lo acuñable aquí**: la cara del reloj (`clock-gap/width/fg/font-*/shadow`),
los acentos `accent-border/track/text` promovidos a públicos (los privados
renombrados los leen), y los `font-size-{k}` del HUD **separados por parte**
(dos escalas conviven: campo y HUD `xxs`); patrón Sidebar (coordenadas `root`
+ resuelto `host`/`size:{k}`; el root estampa `data-size` ✓, el `clock` también
la estampa — si el popup del clock viaja por portal, sus declarations por
talla llevan `parts: ['clock']`). Los 12 literales: anotar o tokenizar uno a
uno.
**Bloqueos de firma**: los mismos de TRP (field-composition · eje del thumb
de slider · D-TH.4 orden B7).
---
**EJECUTADO 2026-08-20 (parcial: lo que no depende de otra firma).** Alcance
**0 % → 18 %** · contrato **0 → 15 claves públicas**.
Hecho: renombrados los **44 privados con prefijo AJENO** — declaraba
`--_time-field-*` dentro de su propio CSS, que el censo contaba como global y
que se cruzaría con un TimeField anidado — a `--_time-picker-*`; el acento pasa
a público (`accent-border` / `-track` / `-text`); y a contrato la cara del reloj
(`clock-gap`, `clock-width`, `clock-fg`, `clock-font-*`), las tres manecillas
(`clock-hand-{hour,minute,second}`), el refuerzo del pulgar y el radio del
trigger en línea.
No hecho, y por qué: la mitad con forma de campo sigue leyendo `--field-*`
porque **compone Field**. La corrección de fondo es el mandato de composición de
Field (`theming-audit.md` §5.3-3), que sigue sin ejecutar — así que ese 82 % no
es deriva, es préstamo con dueño.
Verificación: **diff de computed = 0** sobre 580 valores en 8 estados ·
`component:audit` PASS · suite eidos sin rojos nuevos · `rtl:check` 0 ·
`docs:check` 0.
El guard de huérfanos cazó **12 tokens que declaré de más**: copié `day-period-*`
y `tick-*` del range-picker, que este componente no tiene. Retirados, salvo
`trigger-radius`, que sí tenía consumidor y se cableó.
<!-- veredicto:end -->

Powered by TurnKey Linux.