@ -712,6 +712,150 @@ podrá retirarse.
Esas variantes son `<DatePicker kind='month'>` /
`<DateRangePicker kind='year'>` etc.
## Picker patterns (contrato reutilizable)
Los pickers (`date-picker`, `date-range-picker` , y los futuros
`time-picker` , `time-range-picker` , `color-picker` ) comparten un
contrato común. Documentado aquí como referencia canónica — cualquier
picker nuevo se construye sobre este esqueleto.
### P-1 · Provider helpers: `commit() / cancel() / clear()`
Cada `*PickerProvider` expone tres métodos imperativos consumidos por
las parts del Footer:
- ** `commit()` ** — cierra el popover preservando `value.current` tal
cual. Es la confirmación normal del valor seleccionado.
- ** `cancel()` ** — revierte `value.current` al snapshot capturado en
el **OPEN edge** y cierra el popover. El snapshot se toma vía
`watch(opts.open)` cuando `open` transita de `false → true` :
```ts
private valueOnOpen: TValue | undefined = undefined;
constructor(...) {
watch(() => this.opts.open.current, (isOpen) => {
if (isOpen) this.valueOnOpen = $state.snapshot(this.opts.value.current);
});
}
cancel() {
this.opts.value.current = this.valueOnOpen;
this.opts.open.current = false;
}
```
- ** `clear()` ** — pone `value.current = undefined` . NO cierra el
popover (es una acción destructiva visible que el usuario puede
querer seguir editando). Si el consumer quiere cierre tras clear,
compone `<Picker.Close/>` en el mismo footer.
Estas tres operaciones son ortogonales: cada part Footer hace exactamente
una. No mezcles (ej. `clear` no debe cerrar; `cancel` no debe limpiar).
### P-2 · `mode: 'inline' | 'modal'` → `popover.modal`
`mode` es un opt root del provider que se propaga al `Popover.modal`
underlying:
- ** `mode='inline'` ** (default) — popover no-modal. Outside-click
cierra. Escape cierra. Body scroll libre.
- ** `mode='modal'` ** — popover modal. Outside-click NO cierra (el
usuario debe usar `<Picker.Close/>` o `<Picker.Cancel/>` ). Escape
sigue cerrando. Focus trap dentro del popover. Body scroll lock.
El consumer NO setea `popover.modal` directamente — eso es decisión
del provider:
```svelte
< Popover.Provider modal = {provider.opts.mode.current = = = ' modal ' } . . . >
```
Modal mode obliga al consumer a componer un Footer con `<Cancel/>` o
`<Close/>` para escapar; sin ellos el popover sólo cierra por Escape.
Sigue siendo el consumer responsable (no auto-injecta nada).
### P-3 · Shell composition (Provider > Input > Portal > Content > view + Footer)
Layout canónico de un picker:
```svelte
< X.Provider value = {...} open = {...} mode = 'modal' kind = 'date' >
{#snippet trigger()}
< X.Input / > {!-- field input con segments, button, etc. --}
{/snippet}
{#snippet content()}
< X.Content >
{#if kind === 'year'} < X.YearView / >
{:else if kind === 'month'} < X.MonthView / >
{:else} < X.Calendar / >
{/if}
< X.Footer >
< X.Clear / >
< X.Cancel / >
< X.Close / >
< / X.Footer >
< / X.Content >
{/snippet}
< / X.Provider >
```
- **Trigger snippet** — entry surface (input + segments, button, swatch…).
- **Content** — popover content; sólo `<X.Content>` puede ir aquí.
- **View** — `Calendar` , `YearView` , `MonthView` , `Clock` , `Picker`
(color), etc. El consumer hace branching estructural por `kind` o
variante (P-4 abajo).
- **Footer** — `<X.Footer>` es contenedor archetype='footer'. Las
parts (`Clear`, `Cancel` , `Close` , etc.) se componen dentro. Cada
part renderiza sin checks internos contra props del provider —
presencia = visibilidad (N-7).
Si quieres omitir el footer entero, no compones `<X.Footer>` . Si
quieres sólo `Close` , compones sólo `<X.Close>` dentro.
### P-4 · `kind` o equivalente como single source of truth
Cuando un picker tiene variantes de granularidad (date: day/month/year;
time: hour/minute/second), el opt `kind` (o equivalente) es el ** único
punto de configuración**. Drives:
1. **Segments del input** — filtrado a nivel del FieldProvider (soma),
no en el snippet del consumer. El provider expone una derivación
tipo `visiblePartsByKind: Set<PartName>` y `segmentContents` filtra
`allSegmentContent.arr` colapsando literal runs.
2. **View del popover** — el consumer hace branching estructural sobre
el opt (`{#if kind === 'year'} < YearView / > ` etc).
**No existen** componentes separados por variante (`MonthPicker`,
`HourPicker` , etc). Esas formas son `<X kind='month'>` ,
`<X kind='hour'>` .
### P-5 · Range state machine (para `*-range-picker` )
Para selección de rangos, el provider mantiene un estado interno
implícito (no expuesto como opt):
- **empty** (`value === undefined` o `{start: undefined, end: undefined}` )
— siguiente click setea `start` . Estado pasa a `pending` .
- **pending** (`start` definido, `end` undefined) — siguiente click
setea `end` . Si el nuevo punto < `start` , **swap automático**
(start ↔ end). Estado pasa a `complete` .
- **complete** (`start` y `end` definidos) — siguiente click reinicia:
setea `start = click` , `end = undefined` . Estado pasa a `pending` .
Cada granularidad normaliza los endpoints:
- **Year range** — start = Jan 1, end = Dec 31.
- **Month range** — start = day 1, end = último día del mes.
- **Day range** — start/end son la fecha clickada tal cual.
- **Hour range** — start = `:00` , end = `:59:59` .
Implementado en `YearView` /`MonthView`/`Calendar` (range variant). El
provider sólo expone `setValue(start, end)` ; la state machine vive
en los views.
## Cambios 2026-05-21
- **Tokens muted añadidos al contrato.** `SurfaceColorRoles` y