diff --git a/src/libs/days/queries.ts b/src/libs/days/queries.ts index cee6e3b64..528a62609 100644 --- a/src/libs/days/queries.ts +++ b/src/libs/days/queries.ts @@ -72,3 +72,27 @@ export function isDiasZonedDateTime(value: unknown): value is ZonedDateTime { } export { isDiasZonedDateTime as isZonedDateTime }; + +/** + * Returns the first year of the decade containing `year` (e.g. 2026 → + * 2020, 1999 → 1990). Pure number math, no DateValue allocation. + * + * Used by year-grid range pickers to anchor a 10-year window for + * decade-based pagination. + */ +export function startOfDecadeYear(year: number): number { + return year - (year % 10); +} + +/** + * Encodes a `{ year, month }` pair into a strictly-ordered integer key + * (`year * 12 + (month - 1)`) so range comparisons can be done with + * plain `<` / `>` instead of cascading conditionals. Pure math, no + * DateValue allocation. + * + * Used by month-grid range pickers to classify months as start / end / + * in-range across year boundaries. + */ +export function monthOrdinal(year: number, month: number): number { + return year * 12 + (month - 1); +} diff --git a/src/uix/eidos/components/date-picker/date-picker-month-view.svelte b/src/uix/eidos/components/date-picker/date-picker-month-view.svelte index 7a4afca1e..fd385d03f 100644 --- a/src/uix/eidos/components/date-picker/date-picker-month-view.svelte +++ b/src/uix/eidos/components/date-picker/date-picker-month-view.svelte @@ -1,76 +1,21 @@ - - {#snippet children({ months }: MonthGridProviderSnippetProps)} - - < - - > - - - {#each months as month (month)} - - {/each} - - {/snippet} - + diff --git a/src/uix/eidos/components/date-picker/date-picker-year-view.svelte b/src/uix/eidos/components/date-picker/date-picker-year-view.svelte index ace8a90ca..5ebfbc369 100644 --- a/src/uix/eidos/components/date-picker/date-picker-year-view.svelte +++ b/src/uix/eidos/components/date-picker/date-picker-year-view.svelte @@ -1,75 +1,21 @@ - - {#snippet children({ years }: YearGridProviderSnippetProps)} - - < - - > - - - {#each years as year (year)} - - {/each} - - {/snippet} - + diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte index 42783afec..102565a0f 100644 --- a/src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte @@ -1,124 +1,16 @@ -
-
- - {year} - -
-
- {#each months as month (month)} - {@const c = classFor(month)} - - {/each} -
-
+ diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte index 8126767db..706411efe 100644 --- a/src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte @@ -1,110 +1,16 @@ -
-
- - - {decadeStart} – {decadeStart + 11} - - -
-
- {#each years as year (year)} - {@const c = classFor(year)} - - {/each} -
-
+ diff --git a/src/uix/morfo/components/date-picker.ts b/src/uix/morfo/components/date-picker.ts index c8739a7a8..c3a156b8c 100644 --- a/src/uix/morfo/components/date-picker.ts +++ b/src/uix/morfo/components/date-picker.ts @@ -232,6 +232,30 @@ export const datePickerMorfo = { { key: 'Tab', action: 'focus-next' }, { key: 'Shift+Tab', action: 'focus-prev' } ] + }, + { + // Month-view bridge for `kind='month'`. Owns the bridge from + // CalendarDate ↔ MonthValue + handleDateSelect. Visual chrome + // (header/grid composition) is the wrapped MonthGrid's job. + name: 'MonthView', + kebab: 'month-view', + archetype: 'group', + kind: 'public', + defaultElement: 'div', + optional: true, + data: [], + aria: [] + }, + { + // Year-view bridge for `kind='year'`. Symmetric to MonthView. + name: 'YearView', + kebab: 'year-view', + archetype: 'group', + kind: 'public', + defaultElement: 'div', + optional: true, + data: [], + aria: [] } // Footer / Clear / Cancel / Close — declared by the shared // `picker-shell` (Eidos ``). diff --git a/src/uix/morfo/components/date-range-picker.ts b/src/uix/morfo/components/date-range-picker.ts index 5de58498d..92dd9aadf 100644 --- a/src/uix/morfo/components/date-range-picker.ts +++ b/src/uix/morfo/components/date-range-picker.ts @@ -237,6 +237,45 @@ export const dateRangePickerMorfo = { { key: 'Tab', action: 'focus-next' }, { key: 'Shift+Tab', action: 'focus-prev' } ] + }, + { + // Month-view range bridge for `kind='month'`. Owns the range + // selection state machine (empty / pending / complete) + year + // stepper. Visual chrome painted by eidos via the snippet. + name: 'MonthView', + kebab: 'month-view', + archetype: 'group', + kind: 'public', + defaultElement: 'div', + role: 'grid', + optional: true, + data: [], + aria: [ + { + attr: 'aria-label', + value: v.literal('Select month range'), + severity: 'optional' + } + ] + }, + { + // Year-view range bridge for `kind='year'`. Symmetric to the + // MonthView but with decade pagination. + name: 'YearView', + kebab: 'year-view', + archetype: 'group', + kind: 'public', + defaultElement: 'div', + role: 'grid', + optional: true, + data: [], + aria: [ + { + attr: 'aria-label', + value: v.literal('Select year range'), + severity: 'optional' + } + ] } // Footer / Clear / Cancel / Close — chrome shared via picker-shell; // not declared here. Eidos ` + /** + * Soma `` — month-grid view for `kind='month'`. + * + * Bridges DatePicker's CalendarDate state to MonthGrid's `{year, month}` + * value: reads the picker's current value/placeholder/locale/disabled/ + * readonly, exposes them via MonthGrid, and on month-select converts + * back to a CalendarDate (day=1) + fires `handleDateSelect()` so the + * picker's commit pipeline runs. + * + * Internal composition: renders the default `<` Heading `>` header + + * 3x4 grid. Consumers can override the body via the `children` snippet + * (receives the live `months` array from MonthGrid). + */ + import type { Snippet } from 'svelte'; + import { CalendarDate } from '$libs/days'; + import { DatePickerProvider } from '../date-picker-provider.svelte'; + import * as MonthGrid from '../../month-grid/exports'; + import type { + MonthValue, + MonthPlaceholder, + MonthGridProviderSnippetProps, + MonthGridProps + } from '../../month-grid/types'; + + type Props = Omit< + MonthGridProps, + 'value' | 'placeholder' | 'onValueChange' | 'onPlaceholderChange' | 'children' + > & { + children?: Snippet<[MonthGridProviderSnippetProps]>; + }; + + let { children, ...rest }: Props = $props(); + + const provider = DatePickerProvider.require(); + + const anchor = $derived( + provider.opts.value.current ?? provider.opts.placeholder.current + ); + + const monthValue = $derived( + provider.opts.value.current + ? { + year: provider.opts.value.current.year, + month: provider.opts.value.current.month + } + : undefined + ); + const monthPlaceholder = $derived({ year: anchor.year }); + + function onValueChange(next: MonthValue | undefined): void { + if (!next) return; + const date = new CalendarDate(next.year, next.month, 1); + provider.opts.value.current = date; + provider.opts.placeholder.current = date; + provider.handleDateSelect(); + } + + function onPlaceholderChange(next: MonthPlaceholder): void { + const placeholder = provider.opts.placeholder.current; + provider.opts.placeholder.current = new CalendarDate( + next.year, + placeholder.month, + placeholder.day + ); + } + + + + {#snippet children(snippetProps: MonthGridProviderSnippetProps)} + {#if children} + {@render children(snippetProps)} + {:else} + + < + + > + + + {#each snippetProps.months as month (month)} + + {/each} + + {/if} + {/snippet} + diff --git a/src/uix/soma/components/date-picker/components/date-picker-year-view.svelte b/src/uix/soma/components/date-picker/components/date-picker-year-view.svelte new file mode 100644 index 000000000..bc5827d15 --- /dev/null +++ b/src/uix/soma/components/date-picker/components/date-picker-year-view.svelte @@ -0,0 +1,93 @@ + + + + {#snippet children(snippetProps: YearGridProviderSnippetProps)} + {#if children} + {@render children(snippetProps)} + {:else} + + < + + > + + + {#each snippetProps.years as year (year)} + + {/each} + + {/if} + {/snippet} + diff --git a/src/uix/soma/components/date-picker/exports.ts b/src/uix/soma/components/date-picker/exports.ts index e12475284..efb78ccc7 100644 --- a/src/uix/soma/components/date-picker/exports.ts +++ b/src/uix/soma/components/date-picker/exports.ts @@ -2,6 +2,8 @@ export { default as Provider } from './components/date-picker.svelte'; export { default as Trigger } from './components/date-picker-trigger.svelte'; export { default as Calendar } from './components/date-picker-calendar.svelte'; +export { default as MonthView } from './components/date-picker-month-view.svelte'; +export { default as YearView } from './components/date-picker-year-view.svelte'; // Provider class (action helpers: commit / cancel / clear / handleDateSelect) export { DatePickerProvider, datePickerAttrs } from './internals'; diff --git a/src/uix/soma/components/date-range-picker/components/date-range-picker-month-view.svelte b/src/uix/soma/components/date-range-picker/components/date-range-picker-month-view.svelte new file mode 100644 index 000000000..9dae9c210 --- /dev/null +++ b/src/uix/soma/components/date-range-picker/components/date-range-picker-month-view.svelte @@ -0,0 +1,143 @@ + + +
+ {#if children} + {@render children({ year, months, classify, selectMonth, stepYear })} + {:else} +
+ + {year} + +
+
+ {#each months as month (month)} + {@const c = classify(month)} + + {/each} +
+ {/if} +
diff --git a/src/uix/soma/components/date-range-picker/components/date-range-picker-year-view.svelte b/src/uix/soma/components/date-range-picker/components/date-range-picker-year-view.svelte new file mode 100644 index 000000000..8e2081487 --- /dev/null +++ b/src/uix/soma/components/date-range-picker/components/date-range-picker-year-view.svelte @@ -0,0 +1,141 @@ + + +
+ {#if children} + {@render children({ decadeStart, years, classify, selectYear, stepDecade })} + {:else} +
+ + + {decadeStart} – {decadeStart + 11} + + +
+
+ {#each years as yearN (yearN)} + {@const c = classify(yearN)} + + {/each} +
+ {/if} +
diff --git a/src/uix/soma/components/date-range-picker/exports.ts b/src/uix/soma/components/date-range-picker/exports.ts index b267cddd7..263eb3b54 100644 --- a/src/uix/soma/components/date-range-picker/exports.ts +++ b/src/uix/soma/components/date-range-picker/exports.ts @@ -2,6 +2,8 @@ export { default as Provider } from './components/date-range-picker.svelte'; export { default as Trigger } from './components/date-range-picker-trigger.svelte'; export { default as Calendar } from './components/date-range-picker-calendar.svelte'; +export { default as MonthView } from './components/date-range-picker-month-view.svelte'; +export { default as YearView } from './components/date-range-picker-year-view.svelte'; // Provider class — required by consumers that wire footer actions // (Clear / Cancel / Save) inside the calendar tree via `.require()`.