diff --git a/src/uix/eidos/README.md b/src/uix/eidos/README.md index a88edb7be..3d78b867d 100644 --- a/src/uix/eidos/README.md +++ b/src/uix/eidos/README.md @@ -712,6 +712,150 @@ podrá retirarse. Esas variantes son `` / `` 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 `` 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 `` o ``). 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 + +``` + +Modal mode obliga al consumer a componer un Footer con `` o +`` 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 + + {#snippet trigger()} + {!-- field input con segments, button, etc. --} + {/snippet} + + {#snippet content()} + + {#if kind === 'year'} + {:else if kind === 'month'} + {:else} + {/if} + + + + + + + + {/snippet} + +``` + +- **Trigger snippet** — entry surface (input + segments, button, swatch…). +- **Content** — popover content; sólo `` puede ir aquí. +- **View** — `Calendar`, `YearView`, `MonthView`, `Clock`, `Picker` + (color), etc. El consumer hace branching estructural por `kind` o + variante (P-4 abajo). +- **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 ``. Si +quieres sólo `Close`, compones sólo `` 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` y `segmentContents` filtra + `allSegmentContent.arr` colapsando literal runs. +2. **View del popover** — el consumer hace branching estructural sobre + el opt (`{#if kind === 'year'} ` etc). + +**No existen** componentes separados por variante (`MonthPicker`, +`HourPicker`, etc). Esas formas son ``, +``. + +### 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