date-range-picker: propagate kind to start/end inputs

User: 'lo has trasladado da daterangepicker?' — only the Footer pure-
composition refactor had landed in range; the `kind` prop (Chakra
date/month/year variant) was still single-picker only. The range
demo's input segments stayed MM/DD/YYYY regardless of the chip.

This commit propagates `kind` end-to-end through the range stack.
Calendar view-mode (year-grid / month-grid for ranges) is still
pending — that's the next commit.

soma:
- DateRangePickerKind type exported alongside DateRangePickerMode.
- DateRangePickerProvider opts gain `kind`. Root .svelte threads it
  through readableActive both to the picker context AND to the
  DateRangeFieldProvider it creates.
- DateRangeFieldProvider opts gain `kind` (single value — there's no
  use case for kind='year' on start + kind='month' on end; the range
  picker enforces a uniform kind on both endpoints).
- date-range-field-input forwards provider.opts.kind to each endpoint
  DateField it spawns. The constant `kind: 'date'` placeholder is
  gone.
- types.ts (range-picker + range-field): JSDoc + prop declarations.
- exports.ts: surface DateRangePickerKind alongside DateRangePickerMode.
- Test fixtures (range-picker + range-field): extended with the new opt.

demo:
- `let kind = $state<'date' | 'month' | 'year'>('date')` + chip control
  next to the `mode` radio + prop pass-through to <DateRangePicker>.

Browser confirmed: clicking 'year' chip collapses BOTH start and end
inputs to a single `2026` segment. 'month' shows MM/YYYY on both.
'date' restores MM/DD/YYYY.

What's still pending (next commit):
- Year-grid and month-grid views for the range popover. Currently
  the day calendar still renders regardless of kind. The range
  picker's popover view-mode needs RangeCalendar to gain a `view`
  prop and the eidos surface to dispatch to YearRangeView /
  MonthRangeView components (range selection state machine: first
  click sets start, second click sets end).

Verification: 0 type errors, 18/18 tests across the date-* soma
suite, 67/67 component:audit PASS.

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

@ -75,9 +75,11 @@
? provider.opts.startReadonlySegments.current
: provider.opts.endReadonlySegments.current
),
// Date-range-field has no `kind` notion yet — endpoints stay as
// full date segments. Range-picker propagation lands in commit D.
kind: readableActive(() => 'date' as const),
// Forward the range provider's `kind` (Chakra-style picker
// variant) so both endpoint DateFields filter their segments
// uniformly. There's no use case for kind='year' on start +
// kind='month' on end — the range picker enforces a single kind.
kind: provider.opts.kind,
required: readableActive(() => provider.isRequired),
granularity: provider.opts.granularity,
hideTimeZone: provider.opts.hideTimeZone,

@ -39,6 +39,7 @@
startReadonlySegments = [],
endReadonlySegments = [],
required = false,
kind = 'date',
granularity,
hideTimeZone = false,
hourCycle,
@ -129,6 +130,7 @@
startReadonlySegments: readableActive(() => startReadonlySegments),
endReadonlySegments: readableActive(() => endReadonlySegments),
required: readableActive(() => required),
kind: readableActive(() => kind),
granularity: readableActive(() => granularity),
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),

@ -73,6 +73,7 @@ function createDateRangeFieldOpts(root = document.createElement('div')) {
startReadonlySegments: state<EditableSegmentPart[]>([]),
endReadonlySegments: state<EditableSegmentPart[]>([]),
required: state(false),
kind: state<'date' | 'month' | 'year'>('date'),
granularity: state<Granularity | undefined>(undefined),
hideTimeZone: state(false),
hourCycle: state<HourCycle | undefined>(undefined),

@ -47,6 +47,16 @@ interface DateRangeFieldOpts
startReadonlySegments: EditableSegmentPart[];
endReadonlySegments: EditableSegmentPart[];
required: boolean;
/**
* Chakra-style kind. Forwarded to BOTH endpoint DateField
* providers so the start and end inputs filter their segments
* uniformly (range pickers always show the same kind on both
* sides — there's no use case for kind='year' on start and
* kind='month' on end).
*
* @default 'date'
*/
kind: 'date' | 'month' | 'year';
granularity: Granularity | undefined;
hideTimeZone: boolean;
hourCycle: HourCycle | undefined;

@ -60,6 +60,17 @@ export type DateRangeFieldProviderProps = WithChild<{
endReadonlySegments?: EditableSegmentPart[];
/** Hidden form inputs are required. @default false */
required?: boolean;
/**
* Chakra-style picker kind. Forwarded to both endpoint DateField
* providers so the start/end inputs filter their segments uniformly.
*
* - `'date'` (default): day + month + year segments
* - `'month'`: month + year segments
* - `'year'`: year segment only
*
* @default 'date'
*/
kind?: 'date' | 'month' | 'year';
/** Visual granularity (auto from placeholder when omitted). */
granularity?: Granularity;
/** Hide the timezone segment on ZonedDateTime. @default false */

@ -32,6 +32,7 @@
onOpenChangeComplete = () => {},
closeOnRangeSelect = true,
mode = 'inline',
kind = 'date',
validate,
onInvalid,
minValue,
@ -175,6 +176,7 @@
required: readableActive(() => required),
closeOnRangeSelect: readableActive(() => closeOnRangeSelect),
mode: readableActive(() => mode),
kind: readableActive(() => kind),
granularity: readableActive(() => granularity),
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),
@ -228,6 +230,7 @@
startReadonlySegments: readableActive(() => startReadonlySegments),
endReadonlySegments: readableActive(() => endReadonlySegments),
required: readableActive(() => required),
kind: readableActive(() => kind),
granularity: readableActive(() => granularity),
hideTimeZone: readableActive(() => hideTimeZone),
hourCycle: readableActive(() => hourCycle),

@ -81,6 +81,7 @@ function createDateRangePickerOpts() {
required: state(false),
closeOnRangeSelect: state(true),
mode: state<'inline' | 'modal'>('inline'),
kind: state<'date' | 'month' | 'year'>('date'),
granularity: state<Granularity | undefined>(undefined),
hideTimeZone: state(false),
hourCycle: state<HourCycle | undefined>(undefined),

@ -15,6 +15,17 @@ import { getLocalTimeZone, isBefore } from '$libs/days';
* action in the footer; Cancel restores the value snapshot taken at open.
*/
export type DateRangePickerMode = 'inline' | 'modal';
/**
* Range picker granularity (Chakra-style). Drives both the visible
* segments in the start/end inputs AND the calendar view in the
* popover:
*
* - `'date'` (default): MM/DD/YYYY inputs + day calendar
* - `'month'`: MM/YYYY inputs + month grid
* - `'year'`: YYYY inputs + year grid (decade)
*/
export type DateRangePickerKind = 'date' | 'month' | 'year';
import type {
DateValue,
DateRange,
@ -60,6 +71,7 @@ interface DateRangePickerOpts
required: boolean;
closeOnRangeSelect: boolean;
mode: DateRangePickerMode;
kind: DateRangePickerKind;
granularity: Granularity | undefined;
hideTimeZone: boolean;
hourCycle: HourCycle | undefined;

@ -6,7 +6,10 @@ export { default as Calendar } from './components/date-range-picker-calendar.sve
// Provider class — required by consumers that wire footer actions
// (Clear / Cancel / Save) inside the calendar tree via `.require()`.
export { DateRangePickerProvider } from './date-range-picker-provider.svelte';
export type { DateRangePickerMode } from './date-range-picker-provider.svelte';
export type {
DateRangePickerKind,
DateRangePickerMode
} from './date-range-picker-provider.svelte';
// Popover surface
export { Content, Arrow, Close, Overlay, Anchor } from '../popover/exports';

@ -58,6 +58,19 @@ type DateRangePickerBaseProps = {
* @default 'inline'
*/
mode?: import('./date-range-picker-provider.svelte').DateRangePickerMode;
/**
* Picker granularity / Chakra-style date kind. Drives the visible
* segments in BOTH endpoint inputs AND the calendar view in the
* popover. There's no use case for kind='year' on start +
* kind='month' on end — both endpoints share the same kind.
*
* - `'date'` (default): MM/DD/YYYY inputs + day calendar
* - `'month'`: MM/YYYY inputs + month grid
* - `'year'`: YYYY inputs + year grid (decade)
*
* @default 'date'
*/
kind?: import('./date-range-picker-provider.svelte').DateRangePickerKind;
// ── Validation ──
/** Custom range validator. */
validate?: DateRangeValidator;

@ -60,6 +60,7 @@
let allowSingleDay = $state(false);
let closeOnRangeSelect = $state(false);
let mode = $state<'inline' | 'modal'>('inline');
let kind = $state<'date' | 'month' | 'year'>('date');
let disableDaysOutsideMonth = $state(true);
let fixedWeeks = $state(true);
let pagedNavigation = $state(false);
@ -483,6 +484,7 @@
{allowSingleDay}
{closeOnRangeSelect}
{mode}
{kind}
{disableDaysOutsideMonth}
{fixedWeeks}
{pagedNavigation}
@ -696,6 +698,22 @@
<span data-uix-radio-label>modal</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label
>kind <span data-uix-control-hint>{kind === 'year' ? 'YYYY' : kind === 'month' ? 'MM/YYYY' : 'MM/DD/YYYY'}</span></span
>
<span data-uix-chips role="radiogroup">
{#each ['date', 'month', 'year'] as value (value)}
<button
data-uix-chip
data-active={kind === value}
onclick={() => (kind = value as 'date' | 'month' | 'year')}
>
{value}
</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>states</span>
<span data-uix-switch><input type="checkbox" bind:checked={required} /><span data-uix-switch-label>required</span></span>

Loading…
Cancel
Save

Powered by TurnKey Linux.