diff --git a/src/uix/soma/components/date-picker/components/date-picker.svelte b/src/uix/soma/components/date-picker/components/date-picker.svelte index 2394fde43..e3e5acad4 100644 --- a/src/uix/soma/components/date-picker/components/date-picker.svelte +++ b/src/uix/soma/components/date-picker/components/date-picker.svelte @@ -24,6 +24,7 @@ onOpenChangeComplete = () => {}, closeOnDateSelect = true, mode = 'inline', + kind = 'date', clearButton = false, cancelButton = false, closeButton = false, @@ -138,6 +139,7 @@ required: readableActive(() => required), closeOnDateSelect: readableActive(() => closeOnDateSelect), mode: readableActive(() => mode), + kind: readableActive(() => kind), clearButton: readableActive(() => clearButton), cancelButton: readableActive(() => cancelButton), closeButton: readableActive(() => closeButton), @@ -196,7 +198,9 @@ errorMessageId: readableActive(() => errorMessageId) }); - const mergedProps = $derived(mergeProps(restProps, state.props)); + const mergedProps = $derived( + mergeProps(restProps, state.props, { 'data-kind': kind } as Record) + ); {#if child} diff --git a/src/uix/soma/components/date-picker/date-picker-provider.svelte.test.ts b/src/uix/soma/components/date-picker/date-picker-provider.svelte.test.ts index 5d9c77563..7ee0d1ca6 100644 --- a/src/uix/soma/components/date-picker/date-picker-provider.svelte.test.ts +++ b/src/uix/soma/components/date-picker/date-picker-provider.svelte.test.ts @@ -20,7 +20,11 @@ import { Soma } from '$soma/core/soma.svelte'; import { createSomaRuntime, type SomaRuntimeSources } from '$soma/runtime.svelte'; import type { Direction, OnChangeFn } from '../../types'; -import { DatePickerProvider, type DatePickerMode } from './date-picker-provider.svelte'; +import { + DatePickerProvider, + type DatePickerKind, + type DatePickerMode +} from './date-picker-provider.svelte'; function withEffectRoot(fn: () => T): { result: T; cleanup: () => void } { let result!: T; @@ -73,6 +77,7 @@ function createDatePickerOpts() { required: state(false), closeOnDateSelect: state(true), mode: state('inline'), + kind: state('date'), clearButton: state(false), cancelButton: state(false), closeButton: state(false), diff --git a/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts b/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts index 0c6d55dcf..6c4538505 100644 --- a/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts +++ b/src/uix/soma/components/date-picker/date-picker-provider.svelte.ts @@ -26,6 +26,21 @@ import type { */ export type DatePickerMode = 'inline' | 'modal'; +/** + * Picker granularity / type of date (Chakra-style). Drives both the + * visible segments in the input AND the calendar view in the popover: + * + * - `'date'` (default): MM/DD/YYYY input + day grid in popover + * - `'month'`: MM/YYYY input + month grid in popover + * - `'year'`: YYYY input + year grid (decade) in popover + * + * The contract is consumer-driven for now — the picker exposes `kind`, + * the consumer's segment snippet filters what to render, and the + * calendar honours `data-kind` for view switching (forthcoming + * commits B + C wire the grid renderers). + */ +export type DatePickerKind = 'date' | 'month' | 'year'; + // ── Contract ───────────────────────────────────────────────────────────────── import { datePickerMorfo } from '../../../morfo/components/date-picker'; @@ -58,6 +73,7 @@ interface DatePickerOpts required: boolean; closeOnDateSelect: boolean; mode: DatePickerMode; + kind: DatePickerKind; clearButton: boolean; cancelButton: boolean; closeButton: boolean; diff --git a/src/uix/soma/components/date-picker/exports.ts b/src/uix/soma/components/date-picker/exports.ts index 383ccca8c..d6affbc01 100644 --- a/src/uix/soma/components/date-picker/exports.ts +++ b/src/uix/soma/components/date-picker/exports.ts @@ -5,7 +5,7 @@ export { default as Calendar } from './components/date-picker-calendar.svelte'; // Provider class (action helpers: commit / cancel / clear / handleDateSelect) export { DatePickerProvider, datePickerAttrs } from './date-picker-provider.svelte'; -export type { DatePickerMode } from './date-picker-provider.svelte'; +export type { DatePickerKind, DatePickerMode } from './date-picker-provider.svelte'; // Popover surface (shared via composed PopoverProvider) export { diff --git a/src/uix/soma/components/date-picker/types.ts b/src/uix/soma/components/date-picker/types.ts index 7e578c85b..7bd40ce8c 100644 --- a/src/uix/soma/components/date-picker/types.ts +++ b/src/uix/soma/components/date-picker/types.ts @@ -63,6 +63,17 @@ type DatePickerBaseProps = { * @default 'inline' */ mode?: import('./date-picker-provider.svelte').DatePickerMode; + /** + * Picker granularity / Chakra-style date kind. Drives the visible + * segments in the input AND the calendar view in the popover. + * + * - `'date'` (default): MM/DD/YYYY input + day grid + * - `'month'`: MM/YYYY input + month grid + * - `'year'`: YYYY input + year grid (decade) + * + * @default 'date' + */ + kind?: import('./date-picker-provider.svelte').DatePickerKind; /** * Whether the footer renders a Clear button (resets value, keeps popover * open). @default false diff --git a/web/routes/uix/components/date-picker/+page.svelte b/web/routes/uix/components/date-picker/+page.svelte index c7f91c850..cff6de532 100644 --- a/web/routes/uix/components/date-picker/+page.svelte +++ b/web/routes/uix/components/date-picker/+page.svelte @@ -22,8 +22,6 @@ type WeekStartsOn } from '$libs/days'; - type SegmentLock = 'none' | 'year' | 'monthYear' | 'date'; - const uix = getActiveUix(); void uix; @@ -58,10 +56,10 @@ let readonly = $state(false); let closeOnDateSelect = $state(false); let mode = $state<'inline' | 'modal'>('inline'); + let kind = $state<'date' | 'month' | 'year'>('date'); let clearButton = $state(true); let cancelButton = $state(true); let closeButton = $state(true); - let segmentLock = $state('none'); let disableDaysOutsideMonth = $state(true); let fixedWeeks = $state(false); let pagedNavigation = $state(false); @@ -100,14 +98,17 @@ const resolvedWeekStartsOn = $derived( weekStartsOn === 'locale' ? undefined : (Number(weekStartsOn) as WeekStartsOn) ); - const readonlySegments = $derived( - segmentLock === 'year' - ? ['year'] - : segmentLock === 'monthYear' - ? ['month', 'year'] - : segmentLock === 'date' - ? ['day', 'month', 'year'] - : [] + /** + * Chakra-style kind → set of date-segment parts to show. The chip + * `kind='year'` hides month + day from the input; `kind='month'` + * hides only day. `'date'` shows the full triplet (default). + */ + const visibleDateParts = $derived>( + kind === 'year' + ? new Set(['year']) + : kind === 'month' + ? new Set(['month', 'year']) + : new Set(['day', 'month', 'year']) ); const dateDisabledMatcher = $derived( blockWeekends ? isWeekend : undefined @@ -148,6 +149,39 @@ return day === 0 || day === 6; } + /** + * Filter the input segments by the configured `kind`. Keeps only the + * date parts in `visibleParts` plus the literals (separators) that sit + * between two kept parts. Used by the snippet to render `YYYY` / + * `MM/YYYY` / `MM/DD/YYYY` depending on the picker kind. + */ + function filterByKind( + segments: readonly S[], + visibleParts: Set + ): S[] { + const kept: S[] = []; + let lastKeptWasDate = false; + for (const seg of segments) { + if (seg.part === 'literal') { + // Defer adding until we know the next date part is kept. + if (lastKeptWasDate) kept.push(seg); + } else if (visibleParts.has(seg.part as EditableSegmentPart)) { + kept.push(seg); + lastKeptWasDate = true; + continue; + } else { + continue; + } + if (seg.part !== 'literal') lastKeptWasDate = true; + } + // Drop trailing literal if any (e.g. when last kept was a literal + // that came after the last kept date part). + while (kept.length && kept[kept.length - 1].part === 'literal') kept.pop(); + // Drop leading literal too. + while (kept.length && kept[0].part === 'literal') kept.shift(); + return kept; + } + function isBooked(date: DateValue): boolean { return bookedDates.some((item) => sameDate(item, date)); } @@ -282,7 +316,7 @@ clearButton && ' clearButton', cancelButton && ' cancelButton', closeButton && ' closeButton', - readonlySegments.length && ` readonlySegments={${JSON.stringify(readonlySegments)}}`, + kind !== 'date' && ` kind="${kind}"`, fixedWeeks && ' fixedWeeks', pagedNavigation && ' pagedNavigation', preventDeselect && ' preventDeselect', @@ -357,7 +391,7 @@ clearButton && ' clearButton', cancelButton && ' cancelButton', closeButton && ' closeButton', - readonlySegments.length && ` readonlySegments={${JSON.stringify(readonlySegments)}}`, + kind !== 'date' && ` kind="${kind}"`, fixedWeeks && ' fixedWeeks', pagedNavigation && ' pagedNavigation', preventDeselect && ' preventDeselect', @@ -453,10 +487,10 @@ {variant} {color} {mode} + {kind} {clearButton} {cancelButton} {closeButton} - {readonlySegments} minValue={minValue} maxValue={maxValue} weekStartsOn={resolvedWeekStartsOn} @@ -468,7 +502,7 @@ {labelText} {#snippet children({ segments }: DatePickerInputSnippetProps)} - {#each segments as { part, value }, index (`${part}-${index}`)} + {#each filterByKind(segments, visibleDateParts) as { part, value }, index (`${part}-${index}`)} {value} {/each} @@ -642,13 +676,15 @@ close