docs(eidos): add 'Picker patterns' canonical contract section

Documents P-1..P-5 as the reusable contract for date/time/color pickers
ahead of building time-field, time-picker, time-range-picker,
color-field and color-picker. Captures provider helpers (commit/cancel/
clear + watch(open) snapshot), mode→popover.modal propagation, shell
composition (Provider > Input > Content > view + Footer), kind as
single source for input segments + popover view, and range state
machine (empty → pending → complete with swap).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
active-uix
dev 5 months ago
parent 72ff90b71f
commit ec878fd2ce

@ -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

Loading…
Cancel
Save

Powered by TurnKey Linux.