21 KiB
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 · método y protocolo: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 comovar(--_x, var(--x)); el consumidor no acuña--time-picker-{eje}para él — sería un vocabulario paralelo (README deeidos/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 guardrecipe-css-contractprohí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:
borderes 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:
paddinges 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:
1es 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-accentderiva 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-sizedebe 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-heightdebe 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-borderdebe pasar a leer este público (o desaparecer) —--time-picker-hover-trigger-border,--time-picker-trigger-border - el privado
--_time-picker-accent-trackdebe pasar a leer este público (o desaparecer) —--time-picker-hover-trigger-bg - el privado
--_time-picker-accent-textdebe pasar a leer este público (o desaparecer) —--time-picker-hover-trigger-fg - el privado
--_time-picker-label-fontdebe 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, unbackgrounden 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
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 estampadata-size✓, elclocktambién la estampa — si el popup del clock viaja por portal, sus declarations por talla llevanparts: ['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ó.