diff --git a/src/uix/PENDIENTES.md b/src/uix/PENDIENTES.md index b6fa56c19..2f0cbc791 100644 --- a/src/uix/PENDIENTES.md +++ b/src/uix/PENDIENTES.md @@ -13,8 +13,8 @@ | `MonthRangePicker` (Chakra-style) | **implementar** | Componente nuevo. Segments month/year + grid de meses, sin calendario de días. | | `YearRangePicker` | **implementar** | Componente nuevo. Segments year + grid de años. | | `MonthPicker` / `YearPicker` (single) | **implementar** | Variantes hermanas de los anteriores. | -| Botón Clear/Save/Close como **eidos parts** canónicos (no inline en demo) | **implementar** | Hoy viven en `picker-actions.svelte` del demo. Promover a `` + `` etc. y registrar en morfo. | -| Morfo `date-range-picker`: añadir parts Footer/Clear/Cancel/Close + eventos `commit-save` y `commit-clear-from-footer` | **implementar** | Hace falta para que el audit no marque ARIA gaps cuando el footer está activo. | +| Botón Clear/Save/Close como **eidos parts** canónicos (no inline en demo) | **hecho** 2026-05-21 | Promovidos: `` + `` consumiendo `provider.commit/cancel/clear`. Visibility owned por provider opts. `picker-actions.svelte` borrados. CSS folded into `*.css`. | +| Morfo `date-range-picker` + `date-picker`: añadir parts Footer/Clear/Cancel/Close | **hecho** 2026-05-21 | Ambos morfos declaran las 4 partes (Footer = archetype `footer` añadido al vocabulario; Clear/Cancel/Close = `trigger`). | | Tests browser del flujo completo (Playwright) | **implementar** | Abrir/cerrar, modal/inline, snapshot/cancel, clear, click-outside, Escape. | | Sema event al bloquear click cuando `mode='modal'` | **diferir** | Útil para screen reader feedback. No bloquea. | diff --git a/src/uix/eidos/components/date-picker/date-picker-cancel.svelte b/src/uix/eidos/components/date-picker/date-picker-cancel.svelte new file mode 100644 index 000000000..6144480c7 --- /dev/null +++ b/src/uix/eidos/components/date-picker/date-picker-cancel.svelte @@ -0,0 +1,40 @@ + + +{#if visible} + +{/if} diff --git a/src/uix/eidos/components/date-picker/date-picker-clear.svelte b/src/uix/eidos/components/date-picker/date-picker-clear.svelte new file mode 100644 index 000000000..4446ae0c8 --- /dev/null +++ b/src/uix/eidos/components/date-picker/date-picker-clear.svelte @@ -0,0 +1,40 @@ + + +{#if visible} + +{/if} diff --git a/src/uix/eidos/components/date-picker/date-picker-close.svelte b/src/uix/eidos/components/date-picker/date-picker-close.svelte new file mode 100644 index 000000000..5b52afc0c --- /dev/null +++ b/src/uix/eidos/components/date-picker/date-picker-close.svelte @@ -0,0 +1,44 @@ + + +{#if visible} + +{/if} diff --git a/src/uix/eidos/components/date-picker/date-picker-footer.svelte b/src/uix/eidos/components/date-picker/date-picker-footer.svelte new file mode 100644 index 000000000..0a9cbd1f0 --- /dev/null +++ b/src/uix/eidos/components/date-picker/date-picker-footer.svelte @@ -0,0 +1,27 @@ + + +{#if visible} +
+ {@render children?.()} +
+{/if} diff --git a/src/uix/eidos/components/date-picker/date-picker.css b/src/uix/eidos/components/date-picker/date-picker.css index 131e01565..d6c00fab5 100644 --- a/src/uix/eidos/components/date-picker/date-picker.css +++ b/src/uix/eidos/components/date-picker/date-picker.css @@ -185,8 +185,71 @@ border-color: transparent; } +/* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */ + +[data-date-picker-footer] { + display: flex; + align-items: center; + gap: var(--space-2); + padding-block-start: var(--space-3); + margin-block-start: var(--space-3); + border-block-start: var(--border-width) solid var(--color-border-subtle); +} + +[data-date-picker-clear], +[data-date-picker-cancel] { + margin-inline-end: auto; +} + +[data-date-picker-clear], +[data-date-picker-cancel], +[data-date-picker-close] { + inline-size: max-content; + min-block-size: var(--control-height-sm); + padding-inline: var(--space-3); + border-radius: var(--radius-md); + border: var(--border-width) solid transparent; + background: transparent; + color: var(--color-content-primary); + font: inherit; + font-size: var(--font-size-sm); + line-height: var(--leading-ui); + cursor: pointer; + transition: + background var(--duration-fast) var(--ease-default), + border-color var(--duration-fast) var(--ease-default), + color var(--duration-fast) var(--ease-default); +} + +[data-date-picker-clear]:hover, +[data-date-picker-cancel]:hover { + background: var(--color-surface-overlay); +} + +[data-date-picker-close] { + border-color: var(--color-primary-border); + background: var(--color-primary-solid); + color: var(--color-content-on-solid); +} + +[data-date-picker-close]:hover { + background: var(--color-primary-solid-hover); +} + +[data-date-picker-clear]:focus-visible, +[data-date-picker-cancel]:focus-visible, +[data-date-picker-close]:focus-visible { + outline: var(--focus-ring-width) solid var(--color-primary-border); + outline-offset: var(--focus-ring-offset); +} + @media (prefers-reduced-motion: reduce) { [data-date-picker-trigger] { transition: none; } + [data-date-picker-clear], + [data-date-picker-cancel], + [data-date-picker-close] { + transition: none; + } } diff --git a/src/uix/eidos/components/date-picker/index.ts b/src/uix/eidos/components/date-picker/index.ts index 9d19887fc..5acda8e03 100644 --- a/src/uix/eidos/components/date-picker/index.ts +++ b/src/uix/eidos/components/date-picker/index.ts @@ -2,6 +2,10 @@ import DatePickerComponent from './date-picker.svelte'; import Trigger from './date-picker-trigger.svelte'; import Content from './date-picker-content.svelte'; import Calendar from './date-picker-calendar.svelte'; +import Footer from './date-picker-footer.svelte'; +import Clear from './date-picker-clear.svelte'; +import Cancel from './date-picker-cancel.svelte'; +import Close from './date-picker-close.svelte'; import Label from '../date-field/date-field-label.svelte'; import Input from '../date-field/date-field-input.svelte'; import Segment from '../date-field/date-field-segment.svelte'; @@ -9,7 +13,6 @@ import HiddenInput from '../date-field/date-field-hidden-input.svelte'; import Anchor from '../popover/popover-anchor.svelte'; import Overlay from '../popover/popover-overlay.svelte'; import Arrow from '../popover/popover-arrow.svelte'; -import Close from '../popover/popover-close.svelte'; import Header from '../calendar/calendar-header.svelte'; import Heading from '../calendar/calendar-heading.svelte'; import PrevButton from '../calendar/calendar-prev-button.svelte'; @@ -33,6 +36,9 @@ type DatePickerNamespace = typeof DatePickerComponent & { Content: typeof Content; Arrow: typeof Arrow; Close: typeof Close; + Footer: typeof Footer; + Clear: typeof Clear; + Cancel: typeof Cancel; Label: typeof Label; Input: typeof Input; Segment: typeof Segment; @@ -61,6 +67,9 @@ DatePicker.Overlay = Overlay; DatePicker.Content = Content; DatePicker.Arrow = Arrow; DatePicker.Close = Close; +DatePicker.Footer = Footer; +DatePicker.Clear = Clear; +DatePicker.Cancel = Cancel; DatePicker.Label = Label; DatePicker.Input = Input; DatePicker.Segment = Segment; diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-cancel.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-cancel.svelte new file mode 100644 index 000000000..8916002e0 --- /dev/null +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-cancel.svelte @@ -0,0 +1,38 @@ + + +{#if visible} + +{/if} diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-clear.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-clear.svelte new file mode 100644 index 000000000..5f6e57ffa --- /dev/null +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-clear.svelte @@ -0,0 +1,38 @@ + + +{#if visible} + +{/if} diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-close.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-close.svelte new file mode 100644 index 000000000..a7ec6f561 --- /dev/null +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-close.svelte @@ -0,0 +1,40 @@ + + +{#if visible} + +{/if} diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-footer.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-footer.svelte new file mode 100644 index 000000000..53328fd1e --- /dev/null +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-footer.svelte @@ -0,0 +1,24 @@ + + +{#if visible} +
+ {@render children?.()} +
+{/if} diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker.css b/src/uix/eidos/components/date-range-picker/date-range-picker.css index c4002555b..5d54840f9 100644 --- a/src/uix/eidos/components/date-range-picker/date-range-picker.css +++ b/src/uix/eidos/components/date-range-picker/date-range-picker.css @@ -820,6 +820,64 @@ } } +/* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */ + +[data-date-range-picker-footer] { + display: flex; + align-items: center; + gap: var(--space-2); + padding-block-start: var(--space-3); + margin-block-start: var(--space-3); + border-block-start: var(--border-width) solid var(--color-border-subtle); +} + +[data-date-range-picker-clear], +[data-date-range-picker-cancel] { + margin-inline-end: auto; +} + +[data-date-range-picker-clear], +[data-date-range-picker-cancel], +[data-date-range-picker-close] { + inline-size: max-content; + min-block-size: var(--control-height-sm); + padding-inline: var(--space-3); + border-radius: var(--radius-md); + border: var(--border-width) solid transparent; + background: transparent; + color: var(--color-content-primary); + font: inherit; + font-size: var(--font-size-sm); + line-height: var(--leading-ui); + cursor: pointer; + transition: + background var(--duration-fast) var(--ease-default), + border-color var(--duration-fast) var(--ease-default), + color var(--duration-fast) var(--ease-default); +} + +[data-date-range-picker-clear]:hover, +[data-date-range-picker-cancel]:hover { + background: var(--color-surface-overlay); +} + +[data-date-range-picker-close] { + border-color: var(--color-primary-border); + background: var(--color-primary-solid); + color: var(--color-content-on-solid); +} + +[data-date-range-picker-close]:hover { + background: var(--color-primary-solid-hover); +} + +[data-date-range-picker-clear]:focus-visible, +[data-date-range-picker-cancel]:focus-visible, +[data-date-range-picker-close]:focus-visible { + outline: var(--focus-ring-width) solid var(--color-primary-border); + outline-offset: var(--focus-ring-offset); +} + @media (prefers-reduced-motion: reduce) { [data-date-range-picker-trigger], [data-date-range-field-input], @@ -828,7 +886,10 @@ [data-range-calendar-next-button], [data-range-calendar-month-select], [data-range-calendar-year-select], - [data-range-calendar-day] { + [data-range-calendar-day], + [data-date-range-picker-clear], + [data-date-range-picker-cancel], + [data-date-range-picker-close] { transition: none; } } diff --git a/src/uix/eidos/components/date-range-picker/index.ts b/src/uix/eidos/components/date-range-picker/index.ts index 4d46509d6..ed32270d1 100644 --- a/src/uix/eidos/components/date-range-picker/index.ts +++ b/src/uix/eidos/components/date-range-picker/index.ts @@ -8,7 +8,10 @@ import Segment from './date-range-picker-segment.svelte'; import Anchor from '../popover/popover-anchor.svelte'; import Overlay from '../popover/popover-overlay.svelte'; import Arrow from '../popover/popover-arrow.svelte'; -import Close from '../popover/popover-close.svelte'; +import Close from './date-range-picker-close.svelte'; +import Footer from './date-range-picker-footer.svelte'; +import Clear from './date-range-picker-clear.svelte'; +import Cancel from './date-range-picker-cancel.svelte'; import Header from './date-range-picker-header.svelte'; import Heading from './date-range-picker-heading.svelte'; import PrevButton from './date-range-picker-prev-button.svelte'; @@ -32,6 +35,9 @@ type DateRangePickerNamespace = typeof DateRangePickerComponent & { Content: typeof Content; Arrow: typeof Arrow; Close: typeof Close; + Footer: typeof Footer; + Clear: typeof Clear; + Cancel: typeof Cancel; Label: typeof Label; Input: typeof Input; Segment: typeof Segment; @@ -59,6 +65,9 @@ DateRangePicker.Overlay = Overlay; DateRangePicker.Content = Content; DateRangePicker.Arrow = Arrow; DateRangePicker.Close = Close; +DateRangePicker.Footer = Footer; +DateRangePicker.Clear = Clear; +DateRangePicker.Cancel = Cancel; DateRangePicker.Label = Label; DateRangePicker.Input = Input; DateRangePicker.Segment = Segment; diff --git a/src/uix/langs/components/date-picker.ts b/src/uix/langs/components/date-picker.ts index 3e7ca3073..49e39c746 100644 --- a/src/uix/langs/components/date-picker.ts +++ b/src/uix/langs/components/date-picker.ts @@ -19,5 +19,17 @@ export const datePickerLangs = { calendar: { es: "Calendario de fecha", en: "Date calendar" + }, + clear: { + es: 'Borrar fecha', + en: 'Clear date' + }, + cancel: { + es: 'Cancelar', + en: 'Cancel' + }, + close: { + es: 'Cerrar', + en: 'Close' } } satisfies LangNode; diff --git a/src/uix/langs/components/date-range-picker.ts b/src/uix/langs/components/date-range-picker.ts index 6454edcde..d86dff033 100644 --- a/src/uix/langs/components/date-range-picker.ts +++ b/src/uix/langs/components/date-range-picker.ts @@ -19,5 +19,17 @@ export const dateRangePickerLangs = { calendar: { es: 'Calendario de rango de fechas', en: 'Date range calendar' + }, + clear: { + es: 'Borrar rango', + en: 'Clear range' + }, + cancel: { + es: 'Cancelar', + en: 'Cancel' + }, + close: { + es: 'Cerrar', + en: 'Close' } } satisfies LangNode; diff --git a/src/uix/morfo/components/date-picker.ts b/src/uix/morfo/components/date-picker.ts index 0b4427818..461c86da6 100644 --- a/src/uix/morfo/components/date-picker.ts +++ b/src/uix/morfo/components/date-picker.ts @@ -231,6 +231,78 @@ export const datePickerMorfo = { { key: 'Tab', action: 'focus-next' }, { key: 'Shift+Tab', action: 'focus-prev' } ] + }, + { + // Optional footer rendered inside the Calendar. Visibility is owned + // by the consumer (`clearButton`/`cancelButton`/`closeButton` props + // on the root). In `mode='modal'` the Close affordance is forced on. + name: 'Footer', + kebab: 'footer', + archetype: 'footer', + kind: 'public', + defaultElement: 'div', + optional: true, + data: [{ attr: 'data-mode', values: ['inline', 'modal'], severity: 'optional' }], + aria: [] + }, + { + // Clear action — resets value to undefined, keeps the popover open. + name: 'Clear', + kebab: 'clear', + archetype: 'trigger', + kind: 'public', + defaultElement: 'button', + role: 'button', + optional: true, + data: [{ attr: 'data-action', value: v.literal('clear') }], + aria: [ + { attr: 'type', value: v.literal('button') }, + { + attr: 'aria-label', + value: v.translationRef('#?components.date-picker.clear|Clear date'), + severity: 'recommended' + } + ] + }, + { + // Cancel action — reverts to the value snapshot taken on open, closes. + name: 'Cancel', + kebab: 'cancel', + archetype: 'trigger', + kind: 'public', + defaultElement: 'button', + role: 'button', + optional: true, + data: [{ attr: 'data-action', value: v.literal('cancel') }], + aria: [ + { attr: 'type', value: v.literal('button') }, + { + attr: 'aria-label', + value: v.translationRef('#?components.date-picker.cancel|Cancel'), + severity: 'recommended' + } + ] + }, + { + // Close / Save action — commits current value and closes. In modal + // mode this is the only affordance to exit (outside-click + Escape + // are intentionally blocked). + name: 'Close', + kebab: 'close', + archetype: 'trigger', + kind: 'public', + defaultElement: 'button', + role: 'button', + optional: true, + data: [{ attr: 'data-action', value: v.literal('close') }], + aria: [ + { attr: 'type', value: v.literal('button') }, + { + attr: 'aria-label', + value: v.translationRef('#?components.date-picker.close|Close'), + severity: 'recommended' + } + ] } ] } as const satisfies Morfo; diff --git a/src/uix/morfo/components/date-range-picker.ts b/src/uix/morfo/components/date-range-picker.ts index 684b87b9c..cb802db9b 100644 --- a/src/uix/morfo/components/date-range-picker.ts +++ b/src/uix/morfo/components/date-range-picker.ts @@ -237,6 +237,78 @@ export const dateRangePickerMorfo = { { key: 'Tab', action: 'focus-next' }, { key: 'Shift+Tab', action: 'focus-prev' } ] + }, + { + // Optional footer rendered inside the Calendar. Visibility is owned + // by the consumer (`clearButton`/`cancelButton`/`closeButton` props + // on the root). In `mode='modal'` the Close affordance is forced on. + name: 'Footer', + kebab: 'footer', + archetype: 'footer', + kind: 'public', + defaultElement: 'div', + optional: true, + data: [{ attr: 'data-mode', values: ['inline', 'modal'], severity: 'optional' }], + aria: [] + }, + { + // Clear action — resets value to an empty range, keeps the popover open. + name: 'Clear', + kebab: 'clear', + archetype: 'trigger', + kind: 'public', + defaultElement: 'button', + role: 'button', + optional: true, + data: [{ attr: 'data-action', value: v.literal('clear') }], + aria: [ + { attr: 'type', value: v.literal('button') }, + { + attr: 'aria-label', + value: v.translationRef('#?components.date-range-picker.clear|Clear range'), + severity: 'recommended' + } + ] + }, + { + // Cancel action — reverts to the value snapshot taken on open, closes. + name: 'Cancel', + kebab: 'cancel', + archetype: 'trigger', + kind: 'public', + defaultElement: 'button', + role: 'button', + optional: true, + data: [{ attr: 'data-action', value: v.literal('cancel') }], + aria: [ + { attr: 'type', value: v.literal('button') }, + { + attr: 'aria-label', + value: v.translationRef('#?components.date-range-picker.cancel|Cancel'), + severity: 'recommended' + } + ] + }, + { + // Close / Save action — commits current value and closes. In modal + // mode this is the only affordance to exit (outside-click + Escape + // are intentionally blocked). + name: 'Close', + kebab: 'close', + archetype: 'trigger', + kind: 'public', + defaultElement: 'button', + role: 'button', + optional: true, + data: [{ attr: 'data-action', value: v.literal('close') }], + aria: [ + { attr: 'type', value: v.literal('button') }, + { + attr: 'aria-label', + value: v.translationRef('#?components.date-range-picker.close|Close'), + severity: 'recommended' + } + ] } ] } as const satisfies Morfo; diff --git a/src/uix/morfo/types.ts b/src/uix/morfo/types.ts index 2644766ad..85e819d3c 100644 --- a/src/uix/morfo/types.ts +++ b/src/uix/morfo/types.ts @@ -468,6 +468,7 @@ export type MorfoArchetype = | 'close' // dismiss / close button | 'action' // call-to-action button | 'header' // section header (often above content) + | 'footer' // section footer (often below content) | 'image' // -based content | 'fallback' // shown when primary content unavailable | 'arrow' // pointer arrow (tooltip / popover) @@ -501,6 +502,7 @@ export const ARCHETYPE_VOCABULARY = [ 'close', 'action', 'header', + 'footer', 'image', 'fallback', 'arrow', diff --git a/src/uix/soma/components/date-picker/exports.ts b/src/uix/soma/components/date-picker/exports.ts index 5a754f10d..383ccca8c 100644 --- a/src/uix/soma/components/date-picker/exports.ts +++ b/src/uix/soma/components/date-picker/exports.ts @@ -4,7 +4,7 @@ export { default as Trigger } from './components/date-picker-trigger.svelte'; export { default as Calendar } from './components/date-picker-calendar.svelte'; // Provider class (action helpers: commit / cancel / clear / handleDateSelect) -export { DatePickerProvider } from './date-picker-provider.svelte'; +export { DatePickerProvider, datePickerAttrs } from './date-picker-provider.svelte'; export type { DatePickerMode } from './date-picker-provider.svelte'; // Popover surface (shared via composed PopoverProvider) diff --git a/web/routes/uix/components/date-picker/+page.svelte b/web/routes/uix/components/date-picker/+page.svelte index 2aa76ac68..cfa519d64 100644 --- a/web/routes/uix/components/date-picker/+page.svelte +++ b/web/routes/uix/components/date-picker/+page.svelte @@ -15,7 +15,6 @@ import { popoverMorfo } from '@/uix/morfo/components/popover'; import { getActiveUix } from '$active-uix'; import { CalendarDate, type DateMatcher, type DateValue, type WeekStartsOn } from '$libs/days'; - import PickerActions from './picker-actions.svelte'; const uix = getActiveUix(); void uix; @@ -1022,7 +1021,11 @@ {/each} - + + + + + {/snippet} diff --git a/web/routes/uix/components/date-range-picker/+page.svelte b/web/routes/uix/components/date-range-picker/+page.svelte index e78d49f68..f2908126d 100644 --- a/web/routes/uix/components/date-range-picker/+page.svelte +++ b/web/routes/uix/components/date-range-picker/+page.svelte @@ -14,7 +14,6 @@ import { rangeCalendarMorfo } from '@/uix/morfo/components/range-calendar'; import { popoverMorfo } from '@/uix/morfo/components/popover'; import { getActiveUix } from '$active-uix'; - import PickerActions from './picker-actions.svelte'; import { CalendarDate, getLocalTimeZone, @@ -1045,7 +1044,11 @@ {/each} - + + + + + {/snippet}