You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/web/routes/uix/components/date-range-picker/+page.svelte

1221 lines
51 KiB

<script lang="ts">
import {
DateRangePicker,
type DateRangePickerCalendarSnippetProps,
type DateRangePickerColor,
type DateRangePickerGridRowSnippetProps,
type DateRangePickerInputSnippetProps,
type DateRangePickerSize,
type DateRangePickerVariant
} from '$uix/eidos/components/date-range-picker';
import { compileMorfo } from '$uix/morfo';
import { dateRangePickerMorfo } from '@/uix/morfo/components/date-range-picker';
import { dateRangeFieldMorfo } from '@/uix/morfo/components/date-range-field';
import { rangeCalendarMorfo } from '@/uix/morfo/components/range-calendar';
import { popoverMorfo } from '@/uix/morfo/components/popover';
import { getActiveUix } from '$active-uix';
import {
CalendarDate,
getLocalTimeZone,
type DateMatcher,
type DateRange,
type DateValue,
type EditableSegmentPart,
type WeekStartsOn
} from '$libs/days';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
type WeekStartControl = 'locale' | '0' | '1';
type SegmentLock = 'none' | 'year' | 'monthYear' | 'date';
type Preset = 'sprint' | 'single' | 'late' | 'empty';
const uix = getActiveUix();
const sprintStart = new CalendarDate(2026, 5, 20);
const sprintEnd = new CalendarDate(2026, 6, 3);
const lateStart = new CalendarDate(2026, 6, 8);
const lateEnd = new CalendarDate(2026, 6, 18);
const defaultMin = new CalendarDate(2026, 5, 1);
const defaultMax = new CalendarDate(2026, 7, 31);
const invalidMax = new CalendarDate(2026, 4, 30);
const blockedDates = [new CalendarDate(2026, 6, 12), new CalendarDate(2026, 7, 4)];
const holidays = [new CalendarDate(2026, 5, 25), new CalendarDate(2026, 6, 24)];
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
let value = $state<DateRange | undefined>({ start: sprintStart, end: sprintEnd });
let placeholder = $state<DateValue>(sprintStart);
// Popover starts closed. Consumers click the trigger icon to open.
let open = $state(false);
let startInput = $state(toDateInput(sprintStart));
let endInput = $state(toDateInput(sprintEnd));
let minInput = $state(toDateInput(defaultMin));
let maxInput = $state(toDateInput(defaultMax));
let minDaysInput = $state('2');
let maxDaysInput = $state('21');
let allowSingleDay = $state(false);
let closeOnRangeSelect = $state(false);
let mode = $state<'inline' | 'modal'>('inline');
let kind = $state<'date' | 'month' | 'year'>('date');
// Footer composition demo toggles. Visibility = whether the part is
// INCLUDED in the markup (the parts themselves render unconditionally).
let showFooter = $state(true);
let showClear = $state(true);
let showCancel = $state(true);
let showClose = $state(true);
let disableDaysOutsideMonth = $state(true);
let fixedWeeks = $state(true);
let pagedNavigation = $state(false);
let showWeekNumbers = $state(false);
let blockWeekends = $state(false);
let markBlockedUnavailable = $state(true);
let numberOfMonths = $state(2);
let weekStartsOn = $state<WeekStartControl>('1');
let locale = $state('en-US');
let dir = $state<'ltr' | 'rtl'>('ltr');
let required = $state(true);
let disabled = $state(false);
let readonly = $state(false);
let startLock = $state<SegmentLock>('none');
let endLock = $state<SegmentLock>('none');
let size = $state<DateRangePickerSize>('md');
let variant = $state<DateRangePickerVariant>('surface');
let color = $state<DateRangePickerColor>('primary');
let labelText = $state('Booking range');
let startName = $state('booking-start');
let endName = $state('booking-end');
let lastLoggedConfigError = $state('');
const compiled = compileMorfo(dateRangePickerMorfo);
const rangeCalendarCompiled = compileMorfo(rangeCalendarMorfo);
const dateRangeFieldCompiled = compileMorfo(dateRangeFieldMorfo);
const popoverCompiled = compileMorfo(popoverMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const dateRangePickerEvents = $derived([...compiled.actions.byName.values()]);
const dateRangeFieldEvents = $derived([...dateRangeFieldCompiled.actions.byName.values()]);
const rangeCalendarEvents = $derived([...rangeCalendarCompiled.actions.byName.values()]);
const popoverEvents = $derived([...popoverCompiled.actions.byName.values()]);
const eventRows = $derived([
...dateRangePickerEvents.map((action) => ({ owner: 'DateRangePicker', action })),
...dateRangeFieldEvents.map((action) => ({ owner: 'DateRangeField', action })),
...rangeCalendarEvents.map((action) => ({ owner: 'RangeCalendar', action })),
...popoverEvents.map((action) => ({ owner: 'Popover', action }))
]);
const events = $derived(eventRows.map((row) => row.action));
const minValue = $derived(parseDateInput(minInput));
const maxValue = $derived(parseDateInput(maxInput));
const minDays = $derived(parseOptionalInt(minDaysInput));
const maxDays = $derived(parseOptionalInt(maxDaysInput));
const resolvedWeekStartsOn = $derived(
weekStartsOn === 'locale' ? undefined : (Number(weekStartsOn) as WeekStartsOn)
);
const startReadonlySegments = $derived(lockToSegments(startLock));
const endReadonlySegments = $derived(lockToSegments(endLock));
const dateDisabledMatcher = $derived<DateMatcher | undefined>(
blockWeekends ? isWeekend : undefined
);
const dateUnavailableMatcher = $derived<DateMatcher | undefined>(
markBlockedUnavailable ? isBlocked : undefined
);
const boundsMessage = $derived(getBoundsMessage());
const spanMessage = $derived(getSpanConfigMessage());
const rangeMessage = $derived(getRangeMessage());
const statusMessage = $derived(boundsMessage || spanMessage || rangeMessage);
const valueLabel = $derived(formatRange(value));
const boundsLabel = $derived(`${minValue?.toString() ?? 'none'} / ${maxValue?.toString() ?? 'none'}`);
function toDateInput(date: DateValue): string {
return [
String(date.year).padStart(4, '0'),
String(date.month).padStart(2, '0'),
String(date.day).padStart(2, '0')
].join('-');
}
function parseDateInput(text: string): DateValue | undefined {
if (!text) return undefined;
const [year, month, day] = text.split('-').map(Number);
if (!year || !month || !day) return undefined;
return new CalendarDate(year, month, day);
}
function parseOptionalInt(text: string): number | undefined {
if (!text) return undefined;
const value = Number.parseInt(text, 10);
return Number.isFinite(value) && value > 0 ? value : undefined;
}
function dateCtor(date: DateValue | undefined): string {
return date ? `new CalendarDate(${date.year}, ${date.month}, ${date.day})` : 'undefined';
}
function sameDate(a: DateValue | undefined, b: DateValue): boolean {
return a?.compare(b) === 0;
}
function daysBetween(start: DateValue, end: DateValue): number {
const s = start.toDate(getLocalTimeZone()).getTime();
const e = end.toDate(getLocalTimeZone()).getTime();
return Math.abs(Math.round((e - s) / 86_400_000)) + 1;
}
function isWeekend(date: DateValue): boolean {
const day = date.toDate('UTC').getUTCDay();
return day === 0 || day === 6;
}
function isBlocked(date: DateValue): boolean {
return blockedDates.some((item) => sameDate(item, date));
}
function isHoliday(date: DateValue): boolean {
return holidays.some((item) => sameDate(item, date));
}
function lockToSegments(lock: SegmentLock): EditableSegmentPart[] {
if (lock === 'year') return ['year'];
if (lock === 'monthYear') return ['month', 'year'];
if (lock === 'date') return ['day', 'month', 'year'];
return [];
}
function inputValue(event: Event): string {
return (event.currentTarget as HTMLInputElement).value;
}
function setEndpoint(endpoint: 'start' | 'end', text: string): void {
const next = parseDateInput(text);
const current = value ?? { start: undefined, end: undefined };
value = endpoint === 'start' ? { ...current, start: next } : { ...current, end: next };
if (next) placeholder = next;
}
function setPreset(preset: Preset): void {
if (preset === 'sprint') value = { start: sprintStart, end: sprintEnd };
if (preset === 'single') value = { start: sprintStart, end: sprintStart };
if (preset === 'late') value = { start: lateStart, end: lateEnd };
if (preset === 'empty') value = { start: undefined, end: undefined };
const anchor = value?.start ?? value?.end;
if (anchor) placeholder = anchor;
}
function setBoundsPreset(preset: 'none' | 'min' | 'max' | 'window' | 'invalid'): void {
if (preset === 'none') {
minInput = '';
maxInput = '';
}
if (preset === 'min') {
minInput = toDateInput(defaultMin);
maxInput = '';
}
if (preset === 'max') {
minInput = '';
maxInput = toDateInput(defaultMax);
}
if (preset === 'window') {
minInput = toDateInput(defaultMin);
maxInput = toDateInput(defaultMax);
}
if (preset === 'invalid') {
minInput = toDateInput(defaultMin);
maxInput = toDateInput(invalidMax);
}
}
function getBoundsMessage(): string {
if (minInput && !minValue) return 'minValue is not a valid date.';
if (maxInput && !maxValue) return 'maxValue is not a valid date.';
if (minValue && maxValue && minValue.compare(maxValue) > 0) {
return 'minValue must be on or before maxValue.';
}
return '';
}
function getSpanConfigMessage(): string {
if (minDaysInput && minDays === undefined) return 'minDays must be a positive integer.';
if (maxDaysInput && maxDays === undefined) return 'maxDays must be a positive integer.';
if (minDays !== undefined && maxDays !== undefined && minDays > maxDays) {
return 'minDays must be lower than or equal to maxDays.';
}
return '';
}
function validateRange(range: DateRange): string | undefined {
if (!range.start || !range.end) return required ? 'Both endpoints are required.' : undefined;
const length = daysBetween(range.start, range.end);
if (!allowSingleDay && length === 1) return 'Single-day ranges are disabled.';
if (minDays !== undefined && length < minDays) return `Range must be at least ${minDays} days.`;
if (maxDays !== undefined && length > maxDays) return `Range must be at most ${maxDays} days.`;
return undefined;
}
function getRangeMessage(): string {
const range = value;
if (!range?.start || !range.end) return required ? 'Both endpoints are required.' : 'No range selected.';
const custom = validateRange(range);
if (custom) return custom;
if (minValue && (range.start.compare(minValue) < 0 || range.end.compare(minValue) < 0)) {
return 'Selected range starts before minValue.';
}
if (maxValue && (range.start.compare(maxValue) > 0 || range.end.compare(maxValue) > 0)) {
return 'Selected range ends after maxValue.';
}
if (dateDisabledMatcher?.(range.start) || dateDisabledMatcher?.(range.end)) {
return 'Selected range endpoint matches isDateDisabled.';
}
if (dateUnavailableMatcher?.(range.start) || dateUnavailableMatcher?.(range.end)) {
return 'Selected range endpoint matches isDateUnavailable.';
}
return 'Range is valid.';
}
function formatRange(range: DateRange | undefined): string {
if (!range?.start && !range?.end) return 'empty';
return `${range.start?.toString() ?? '...'} -> ${range.end?.toString() ?? '...'}`;
}
function formatMonthYear(date: DateValue): string {
return new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(
date.toDate(getLocalTimeZone())
);
}
function monthHeadingId(date: DateValue): string {
return `date-range-picker-month-${date.toString()}`;
}
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
$effect(() => {
startInput = value?.start ? toDateInput(value.start) : '';
endInput = value?.end ? toDateInput(value.end) : '';
});
$effect(() => {
const message = boundsMessage || spanMessage;
if (!message) {
lastLoggedConfigError = '';
return;
}
if (message === lastLoggedConfigError) return;
lastLoggedConfigError = message;
uix.logger.error('soma.date-range-picker', message, {
context: {
minValue: minValue?.toString() ?? null,
maxValue: maxValue?.toString() ?? null,
minDays,
maxDays
}
});
});
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '-',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes: true, subtree: true, attributeFilter: ['data-event'] });
return () => obs.disconnect();
});
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import * as DateRangePicker from '$soma/components/date-range-picker';",
" import { CalendarDate, type DateRange, type DateValue } from '$libs/days';",
'',
` const minValue = ${dateCtor(minValue)};`,
` const maxValue = ${dateCtor(maxValue)};`,
` let value = $state<DateRange | undefined>({ start: ${dateCtor(value?.start)}, end: ${dateCtor(value?.end)} });`,
` let placeholder = $state<DateValue>(${dateCtor(placeholder)});`,
` let open = $state(${open});`,
'</' + 'script>',
'',
'<DateRangePicker.Provider',
' bind:value',
' bind:placeholder',
' bind:open',
required && ' required',
disabled && ' disabled',
readonly && ' readonly',
minValue && ' {minValue}',
maxValue && ' {maxValue}',
minDays !== undefined && ` minDays={${minDays}}`,
maxDays !== undefined && ` maxDays={${maxDays}}`,
allowSingleDay && ' allowSingleDay',
!closeOnRangeSelect && ' closeOnRangeSelect={false}',
numberOfMonths !== 2 && ` numberOfMonths={${numberOfMonths}}`,
fixedWeeks && ' fixedWeeks',
pagedNavigation && ' pagedNavigation',
disableDaysOutsideMonth && ' disableDaysOutsideMonth',
showWeekNumbers && ' showWeekNumbers',
weekStartsOn !== 'locale' && ` weekStartsOn={${weekStartsOn}}`,
startReadonlySegments.length &&
` startReadonlySegments={${JSON.stringify(startReadonlySegments)}}`,
endReadonlySegments.length && ` endReadonlySegments={${JSON.stringify(endReadonlySegments)}}`,
blockWeekends && ' isDateDisabled={(date) => date.toDate("UTC").getUTCDay() % 6 === 0}',
markBlockedUnavailable && ' isDateUnavailable={(date) => date.day === 22}',
' weekdayFormat="short"',
'>',
` <DateRangePicker.Label>${labelText}</DateRangePicker.Label>`,
` <DateRangePicker.Input type="start" name="${startName}">...</DateRangePicker.Input>`,
` <DateRangePicker.Input type="end" name="${endName}">`,
' <!-- end segments -->',
' <DateRangePicker.Trigger />',
' </DateRangePicker.Input>',
' <DateRangePicker.Portal>',
' <DateRangePicker.Content align="end" sideOffset={10} collisionPadding={12}>',
' <DateRangePicker.Calendar>...</DateRangePicker.Calendar>',
' </DateRangePicker.Content>',
' </DateRangePicker.Portal>',
'</DateRangePicker.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { DateRangePicker } from '$uix/eidos/components/date-range-picker';",
" import { CalendarDate, type DateRange, type DateValue } from '$libs/days';",
'',
` const minValue = ${dateCtor(minValue)};`,
` const maxValue = ${dateCtor(maxValue)};`,
` let value = $state<DateRange | undefined>({ start: ${dateCtor(value?.start)}, end: ${dateCtor(value?.end)} });`,
` let placeholder = $state<DateValue>(${dateCtor(placeholder)});`,
` let open = $state(${open});`,
'</' + 'script>',
'',
'<DateRangePicker',
' bind:value',
' bind:placeholder',
' bind:open',
size !== 'md' && ` size="${size}"`,
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
required && ' required',
disabled && ' disabled',
readonly && ' readonly',
minValue && ' {minValue}',
maxValue && ' {maxValue}',
minDays !== undefined && ` minDays={${minDays}}`,
maxDays !== undefined && ` maxDays={${maxDays}}`,
allowSingleDay && ' allowSingleDay',
!closeOnRangeSelect && ' closeOnRangeSelect={false}',
numberOfMonths !== 2 && ` numberOfMonths={${numberOfMonths}}`,
fixedWeeks && ' fixedWeeks',
pagedNavigation && ' pagedNavigation',
disableDaysOutsideMonth && ' disableDaysOutsideMonth',
showWeekNumbers && ' showWeekNumbers',
weekStartsOn !== 'locale' && ` weekStartsOn={${weekStartsOn}}`,
startReadonlySegments.length &&
` startReadonlySegments={${JSON.stringify(startReadonlySegments)}}`,
endReadonlySegments.length && ` endReadonlySegments={${JSON.stringify(endReadonlySegments)}}`,
blockWeekends && ' isDateDisabled={(date) => date.toDate("UTC").getUTCDay() % 6 === 0}',
markBlockedUnavailable && ' isDateUnavailable={(date) => date.day === 22}',
' weekdayFormat="short"',
'>',
` <DateRangePicker.Label>${labelText}</DateRangePicker.Label>`,
' <div class="range-field-row">',
` <DateRangePicker.Input type="start" name="${startName}">...</DateRangePicker.Input>`,
' <span aria-hidden="true">to</span>',
` <DateRangePicker.Input type="end" name="${endName}">`,
' <!-- end segments -->',
' <DateRangePicker.Trigger />',
' </DateRangePicker.Input>',
' </div>',
' <DateRangePicker.Portal>',
' <DateRangePicker.Content align="end" sideOffset={10} collisionPadding={12}>',
' <DateRangePicker.Calendar>...</DateRangePicker.Calendar>',
' </DateRangePicker.Content>',
' </DateRangePicker.Portal>',
'</DateRangePicker>'
]
.filter(Boolean)
.join('\n')
);
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Forms - DateRangePicker</div>
<h1 data-uix-page-title>DateRangePicker</h1>
<p data-uix-page-lede>
Segmented range input with an attached range calendar popover. Soma owns the two
endpoints, min/max validation, popover state and range selection; Eidos only adds the
visual size, variant and color recipe.
</p>
<div data-uix-page-meta>
<span data-uix-meta-pill><span data-uix-meta-key>parts</span>{compiled.parts.order.length}</span>
<span data-uix-meta-pill><span data-uix-meta-key>events</span>{events.length}</span>
<span data-uix-meta-pill><span data-uix-meta-key>value</span>{valueLabel}</span>
<span data-uix-meta-pill><span data-uix-meta-key>min/max</span>{boundsLabel}</span>
<span data-uix-meta-pill><span data-uix-meta-key>months</span>{numberOfMonths}</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<div class="date-range-picker-stage">
<DateRangePicker
bind:value
bind:placeholder
bind:open
{required}
{disabled}
{readonly}
{allowSingleDay}
{closeOnRangeSelect}
{mode}
{kind}
{disableDaysOutsideMonth}
{fixedWeeks}
{pagedNavigation}
{showWeekNumbers}
{numberOfMonths}
{locale}
{dir}
{size}
{variant}
{color}
{minValue}
{maxValue}
{minDays}
{maxDays}
{startReadonlySegments}
{endReadonlySegments}
weekStartsOn={resolvedWeekStartsOn}
isDateDisabled={dateDisabledMatcher}
isDateUnavailable={dateUnavailableMatcher}
isDateHoliday={isHoliday}
validate={validateRange}
weekdayFormat="short"
calendarLabel="Booking date range"
>
<DateRangePicker.Label>{labelText}</DateRangePicker.Label>
<div class="range-field-row">
<DateRangePicker.Input type="start" name={startName} aria-label="Start date">
{#snippet children({ segments }: DateRangePickerInputSnippetProps)}
{#each segments as { part, value }, index (`start-${part}-${index}`)}
<DateRangePicker.Segment {part}>{value}</DateRangePicker.Segment>
{/each}
{/snippet}
</DateRangePicker.Input>
<span class="range-separator" aria-hidden="true">to</span>
<DateRangePicker.Input type="end" name={endName} aria-label="End date">
{#snippet children({ segments }: DateRangePickerInputSnippetProps)}
{#each segments as { part, value }, index (`end-${part}-${index}`)}
<DateRangePicker.Segment {part}>{value}</DateRangePicker.Segment>
{/each}
<DateRangePicker.Trigger />
{/snippet}
</DateRangePicker.Input>
</div>
<div class="range-picker-status" data-invalid={statusMessage !== 'Range is valid.' || undefined}>
<span>{statusMessage}</span>
</div>
<DateRangePicker.Portal>
<DateRangePicker.Content align="end" sideOffset={10} collisionPadding={12}>
{#if kind === 'year'}
<DateRangePicker.YearView />
{:else if kind === 'month'}
<DateRangePicker.MonthView />
{:else}
<DateRangePicker.Calendar>
{#snippet children(args: DateRangePickerCalendarSnippetProps)}
{@render calendarBody(args)}
{/snippet}
</DateRangePicker.Calendar>
{/if}
{@render pickerFooter()}
</DateRangePicker.Content>
</DateRangePicker.Portal>
</DateRangePicker>
</div>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>open the picker and select a range</span>
{:else}
{#each trace.slice(0, 3) as entry}
<span><span data-uix-stage-trace-event>{entry.event}</span> - {entry.family}{entry.intent ? ' - ' + entry.intent : ''}</span>
<span style="color: var(--uix-text-faint)">{fmtTime(entry.at)}</span>
{/each}
{/if}
<span style="margin-inline-start: auto;">
<span data-uix-stage-trace-key>open</span> {String(open)}
<span data-uix-stage-trace-key style="margin-inline-start: var(--uix-space-2);">range</span>
{valueLabel}
</span>
</div>
</div>
<div data-uix-tabs role="tablist">
<button data-uix-tab data-active={tab === 'live'} onclick={() => (tab = 'live')}>Live</button>
<button data-uix-tab data-active={tab === 'api'} onclick={() => (tab = 'api')}>API <span data-uix-tab-count>23</span></button>
<button data-uix-tab data-active={tab === 'morfo'} onclick={() => (tab = 'morfo')}>
<span data-uix-layer-badge="morfo">morfo</span>
<span data-uix-tab-count>{partsList.length}p - {events.length}e</span>
</button>
<button data-uix-tab data-active={tab === 'sema'} onclick={() => (tab = 'sema')}>
<span data-uix-layer-badge="sema">sema</span>
<span data-uix-tab-count>{events.length}</span>
</button>
<button data-uix-tab data-active={tab === 'recipe'} onclick={() => (tab = 'recipe')}>Recipe</button>
<button data-uix-tab data-active={tab === 'a11y'} onclick={() => (tab = 'a11y')}>A11y</button>
</div>
{#if tab === 'live'}
<section data-uix-section>
<h2 data-uix-section-title>Controls</h2>
<p data-uix-section-desc>
<span data-uix-layer-badge="soma">soma</span> owns value, min/max dates, range
length, disabled/unavailable dates and popover behavior.
<span data-uix-layer-badge="eidos">eidos</span> owns visual treatment.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props - range value and constraints
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>open <span data-uix-control-hint>bindable</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={open} />
<span data-uix-switch-label>{open ? 'open' : 'closed'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>start</span>
<input type="date" value={startInput} oninput={(event) => setEndpoint('start', inputValue(event))} />
</label>
<label data-uix-control>
<span data-uix-control-label>end</span>
<input type="date" value={endInput} oninput={(event) => setEndpoint('end', inputValue(event))} />
</label>
<label data-uix-control>
<span data-uix-control-label>range presets</span>
<span data-uix-chips role="group">
<button data-uix-chip data-active={sameDate(value?.start, sprintStart) && sameDate(value?.end, sprintEnd)} onclick={() => setPreset('sprint')}>May-Jun</button>
<button data-uix-chip data-active={sameDate(value?.start, sprintStart) && sameDate(value?.end, sprintStart)} onclick={() => setPreset('single')}>single day</button>
<button data-uix-chip data-active={sameDate(value?.start, lateStart)} onclick={() => setPreset('late')}>June range</button>
<button data-uix-chip data-active={!value?.start && !value?.end} onclick={() => setPreset('empty')}>empty</button>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>minValue <span data-uix-control-hint>{minValue?.toString() ?? 'none'}</span></span>
<input type="date" bind:value={minInput} />
</label>
<label data-uix-control>
<span data-uix-control-label>maxValue <span data-uix-control-hint>{maxValue?.toString() ?? 'none'}</span></span>
<input type="date" bind:value={maxInput} />
</label>
<label data-uix-control>
<span data-uix-control-label>min/max presets</span>
<span data-uix-chips role="radiogroup">
<button data-uix-chip data-active={!minInput && !maxInput} onclick={() => setBoundsPreset('none')}>none</button>
<button data-uix-chip data-active={!!minInput && !maxInput} onclick={() => setBoundsPreset('min')}>min only</button>
<button data-uix-chip data-active={!minInput && !!maxInput} onclick={() => setBoundsPreset('max')}>max only</button>
<button data-uix-chip data-active={minInput === toDateInput(defaultMin) && maxInput === toDateInput(defaultMax)} onclick={() => setBoundsPreset('window')}>May-Jul</button>
<button data-uix-chip data-active={maxInput === toDateInput(invalidMax)} onclick={() => setBoundsPreset('invalid')}>invalid</button>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>minDays</span>
<input type="number" min="1" bind:value={minDaysInput} />
</label>
<label data-uix-control>
<span data-uix-control-label>maxDays</span>
<input type="number" min="1" bind:value={maxDaysInput} />
</label>
<label data-uix-control>
<span data-uix-control-label>visible months</span>
<span data-uix-chips role="radiogroup">
{#each [1, 2] as count}
<button data-uix-chip data-active={numberOfMonths === count} onclick={() => (numberOfMonths = count)}>{count}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>week starts</span>
<span data-uix-chips role="radiogroup">
{#each ['locale', '0', '1'] as value}
<button data-uix-chip data-active={weekStartsOn === value} onclick={() => (weekStartsOn = value as WeekStartControl)}>{value}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>locale</span>
<select bind:value={locale}>
<option value="en-US">en-US</option>
<option value="es-ES">es-ES</option>
<option value="fr-FR">fr-FR</option>
</select>
</label>
<label data-uix-control>
<span data-uix-control-label>direction</span>
<span data-uix-chips role="radiogroup">
{#each ['ltr', 'rtl'] as value}
<button data-uix-chip data-active={dir === value} onclick={() => (dir = value as 'ltr' | 'rtl')}>{value}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>start segments</span>
<span data-uix-chips role="radiogroup">
{#each ['none', 'year', 'monthYear', 'date'] as value}
<button data-uix-chip data-active={startLock === value} onclick={() => (startLock = value as SegmentLock)}>{value}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>end segments</span>
<span data-uix-chips role="radiogroup">
{#each ['none', 'year', 'monthYear', 'date'] as value}
<button data-uix-chip data-active={endLock === value} onclick={() => (endLock = value as SegmentLock)}>{value}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>mode</span>
<span data-uix-radio>
<input type="radio" bind:group={mode} value="inline" />
<span data-uix-radio-label>inline</span>
</span>
<span data-uix-radio>
<input type="radio" bind:group={mode} value="modal" />
<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
>footer <span data-uix-control-hint>compose parts inside &lt;Footer&gt;</span></span
>
<span data-uix-switch
><input type="checkbox" bind:checked={showFooter} /><span data-uix-switch-label
>Footer</span
></span
>
<span data-uix-switch
><input type="checkbox" bind:checked={showClear} /><span data-uix-switch-label
>Clear</span
></span
>
<span data-uix-switch
><input type="checkbox" bind:checked={showCancel} /><span data-uix-switch-label
>Cancel</span
></span
>
<span data-uix-switch
><input type="checkbox" bind:checked={showClose} /><span data-uix-switch-label
>Close</span
></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>
<span data-uix-switch><input type="checkbox" bind:checked={disabled} /><span data-uix-switch-label>disabled</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={readonly} /><span data-uix-switch-label>readonly</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={allowSingleDay} /><span data-uix-switch-label>single day</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={closeOnRangeSelect} /><span data-uix-switch-label>close on select</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={disableDaysOutsideMonth} /><span data-uix-switch-label>disable outside</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={fixedWeeks} /><span data-uix-switch-label>fixed weeks</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={pagedNavigation} /><span data-uix-switch-label>paged nav</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={showWeekNumbers} /><span data-uix-switch-label>week numbers</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={blockWeekends} /><span data-uix-switch-label>block weekends</span></span>
<span data-uix-switch><input type="checkbox" bind:checked={markBlockedUnavailable} /><span data-uix-switch-label>unavailable dates</span></span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props - visual treatment
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['sm', 'md', 'lg'] as value}
<button data-uix-chip data-active={size === value} onclick={() => (size = value as DateRangePickerSize)}>{value}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['surface', 'outline', 'ghost'] as value}
<button data-uix-chip data-active={variant === value} onclick={() => (variant = value as DateRangePickerVariant)}>{value}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each ['primary', 'secondary', 'neutral', 'affirm', 'fulfill', 'risk', 'threat', 'loss'] as value}
<button data-uix-chip data-active={color === value} onclick={() => (color = value as DateRangePickerColor)}>{value}</button>
{/each}
</span>
</label>
</div>
<div data-uix-subsection-head>Demo content</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>label</span>
<input type="text" bind:value={labelText} />
</label>
<label data-uix-control>
<span data-uix-control-label>start name</span>
<input type="text" bind:value={startName} />
</label>
<label data-uix-control>
<span data-uix-control-label>end name</span>
<input type="text" bind:value={endName} />
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless - shared range, field, popover and calendar</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{somaSnippet}</code></pre>
</div>
<div data-uix-code style="margin-top: var(--uix-space-3);">
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>visual - root plus attached field, popover and range-calendar parts</span>
<span data-uix-code-lang>svelte</span>
</div>
<pre><code>{eidosSnippet}</code></pre>
</div>
</section>
{/if}
{#if tab === 'api'}
<section data-uix-section>
<h2 data-uix-section-title>API reference</h2>
<p data-uix-section-desc>
DateRangePicker is a composition over DateRangeField, Popover and RangeCalendar. Air
has no local DateRangePicker; Terra in morfo-runtime is the historical baseline.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Owner</th><th>Default</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="name">value <span data-uix-tag data-kind="bindable">bindable</span></td><td>soma</td><td class="default">undefined</td><td>Range <code>{'{ start, end }'}</code> shared by both inputs and calendar.</td></tr>
<tr><td class="name">placeholder <span data-uix-tag data-kind="bindable">bindable</span></td><td>soma</td><td class="default">today</td><td>Visible month anchor.</td></tr>
<tr><td class="name">open <span data-uix-tag data-kind="bindable">bindable</span></td><td>soma</td><td class="default">false</td><td>Popover state.</td></tr>
<tr><td class="name">minValue / maxValue</td><td>soma</td><td class="default empty">-</td><td>Selectable date bounds. Invalid pairs mark the range calendar invalid and log through UIX.</td></tr>
<tr><td class="name">minDays / maxDays / allowSingleDay</td><td>soma</td><td class="default">- / - / true</td><td>Inclusive range length constraints.</td></tr>
<tr><td class="name">isDateDisabled / isDateUnavailable / isDateHoliday</td><td>soma</td><td class="default empty">-</td><td>Per-date matchers for hard disabled, unavailable and decorative holiday dates.</td></tr>
<tr><td class="name">startReadonlySegments / endReadonlySegments</td><td>soma</td><td class="default">[]</td><td>Independent segment locks for each endpoint.</td></tr>
<tr><td class="name">numberOfMonths / fixedWeeks / pagedNavigation</td><td>soma</td><td class="default">2 / false / false</td><td>Range calendar layout and navigation behavior.</td></tr>
<tr><td class="name">closeOnRangeSelect</td><td>soma</td><td class="default">true</td><td>Close the popover when both endpoints are selected.</td></tr>
<tr><td class="name">size / variant / color</td><td>eidos</td><td class="default">md / surface / primary</td><td>Visual-only data attrs for field, trigger and calendar.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Reference comparison</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Feature</th><th>Air</th><th>Terra morfo-runtime</th><th>React Aria</th><th>Ark</th><th>Bits</th><th>UIX decision</th></tr></thead>
<tbody>
<tr><td class="name">DateRangePicker primitive</td><td>no</td><td>yes</td><td>yes</td><td>range mode</td><td>yes</td><td>Add Eidos wrapper over current Soma composition.</td></tr>
<tr><td class="name">Segmented start/end inputs</td><td>-</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Use DateRangeField parts inside the picker namespace.</td></tr>
<tr><td class="name">Min/max date controls</td><td>-</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Expose as direct props and visible demo controls.</td></tr>
<tr><td class="name">Range length constraints</td><td>-</td><td>min/max days</td><td>custom validation</td><td>range constraints</td><td>custom</td><td>First-class `minDays`, `maxDays`, `allowSingleDay`.</td></tr>
<tr><td class="name">Multiple visible months</td><td>calendar only</td><td>yes</td><td>yes</td><td>yes</td><td>yes</td><td>Top heading shows full range; each visible month gets its own title only when there are 2 months.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title><span data-uix-layer-badge="morfo">morfo</span> contract</h2>
<p data-uix-section-desc>
Source: <code>src/uix/morfo/components/date-range-picker.ts</code>. The picker
declares only the composition root, trigger and calendar bridge; DateRangeField,
RangeCalendar and Popover keep their own contracts.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Field</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">name</td><td class="type">"{dateRangePickerMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{dateRangePickerMorfo.kebab}"</td></tr>
<tr><td class="name">scope</td><td class="type">[{dateRangePickerMorfo.scope.map((s) => `"${s}"`).join(', ')}]</td></tr>
<tr><td class="name">apg</td><td class="default">composed: DateRangeField + Popover + RangeCalendar</td></tr>
<tr><td class="name">parts.length</td><td class="default">{dateRangePickerMorfo.parts.length}</td></tr>
<tr><td class="name">own events</td><td class="default">{dateRangePickerEvents.length}</td></tr>
<tr><td class="name">composed events</td><td class="default">{events.length}</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>Marker</th><th>Element</th><th>Role</th><th>Archetype</th><th>States</th><th>Optional</th></tr></thead>
<tbody>
{#each partsList as part}
<tr>
<td class="name">{part.kebab}</td>
<td><code data-uix-part-marker>[{part.marker}]</code></td>
<td class="type">&lt;{part.defaultElement}&gt;</td>
<td class="type">{part.role ?? '-'}</td>
<td class="default">{part.archetype ?? '-'}</td>
<td class="default">{part.states.length ? part.states.join(' | ') : '-'}</td>
<td class="default">{part.optional ? 'yes' : 'no'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<div data-uix-subsection-head>Composed contracts</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Contract</th><th>Parts</th><th>Events</th><th>Role in picker</th></tr></thead>
<tbody>
<tr><td class="name">DateRangeField</td><td>{dateRangeFieldCompiled.parts.order.length}</td><td>{dateRangeFieldCompiled.actions.byName.size}</td><td>Label plus start/end segmented inputs.</td></tr>
<tr><td class="name">RangeCalendar</td><td>{rangeCalendarCompiled.parts.order.length}</td><td>{rangeCalendarCompiled.actions.byName.size}</td><td>Range grid, min/max, preview and navigation.</td></tr>
<tr><td class="name">Popover</td><td>{popoverCompiled.parts.order.length}</td><td>{popoverCompiled.actions.byName.size}</td><td>Disclosure, portal and floating content.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Events declaration</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Intent</th><th>Target</th></tr></thead>
<tbody>
{#each eventRows as { owner, action }}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
<tr>
<td class="name">{owner}.{action.name}</td>
<td class="type">{action.semantic.family}</td>
<td>{action.semantic.verb ?? '-'}</td>
<td>{action.semantic.sequence ?? 'pre'}</td>
<td class="default">{typeof intentDecl === 'string' ? intentDecl : intentDecl ? 'fromProp' : '-'}</td>
<td class="type">{action.target}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section id="sema">
<h2 data-uix-section-title><span data-uix-layer-badge="sema">sema</span> events</h2>
<p data-uix-section-desc>
The public picker is a composition. Its event surface is the union of range calendar
selection/navigation events, popover disclosure events and any field commit events.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Owner</th><th>Events</th><th>Responsibility</th></tr></thead>
<tbody>
<tr><td class="name">DateRangePicker</td><td class="default">{dateRangePickerEvents.length}</td><td>Root state and composition.</td></tr>
<tr><td class="name">DateRangeField</td><td class="default">{dateRangeFieldEvents.length}</td><td>Endpoint field commits.</td></tr>
<tr><td class="name">RangeCalendar</td><td class="default">{rangeCalendarEvents.length}</td><td>Start selection, range commit, clear and calendar navigation.</td></tr>
<tr><td class="name">Popover</td><td class="default">{popoverCompiled.actions.byName.size}</td><td>Disclosure events.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Composed event declarations</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Owner</th><th>Name</th><th>Family</th><th>Verb</th><th>Sequence</th><th>Intent</th></tr></thead>
<tbody>
{#each eventRows as { owner, action }}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
<tr>
<td class="name">{owner}</td>
<td class="name">{action.name}</td>
<td class="type">{action.semantic.family}</td>
<td>{action.semantic.verb ?? '-'}</td>
<td>{action.semantic.sequence ?? 'pre'}</td>
<td class="default">{typeof intentDecl === 'string' ? intentDecl : intentDecl ? 'fromProp' : '-'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'recipe'}
<section data-uix-section>
<h2 data-uix-section-title>Eidos recipe</h2>
<p data-uix-section-desc>
Selectors live in <code>src/uix/eidos/components/date-range-picker/date-range-picker.css</code>.
The field and trigger use DateField tokens; the popup range grid mirrors Calendar
without drawing a nested card.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td class="name"><code>[data-date-range-picker]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Root provider, open and invalid states.</td></tr>
<tr><td class="name"><code>[data-date-range-picker][data-size][data-variant][data-color]</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Visual scale, treatment and intent palette.</td></tr>
<tr><td class="name"><code>[data-date-range-field-input][data-endpoint]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Start and end segmented fields.</td></tr>
<tr><td class="name"><code>[data-date-range-picker-trigger]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Calendar opener button.</td></tr>
<tr><td class="name"><code>[data-date-range-picker-calendar][data-range-calendar]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Range calendar bridge with nested chrome removed.</td></tr>
<tr><td class="name"><code>[data-range-calendar-day][data-in-range]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Range track styling.</td></tr>
<tr><td class="name"><code>[data-range-calendar-day][data-range-start]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Committed endpoint styling.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>Accessibility</h2>
<p data-uix-section-desc>
The picker combines DateRangeField's grouped segmented inputs, Popover disclosure and
RangeCalendar's application-labelled grid.
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Concern</th><th>Contract</th></tr></thead>
<tbody>
<tr><td class="name">Trigger</td><td><code>aria-haspopup="dialog"</code>, <code>aria-expanded</code> and calendar label from Morfo translations.</td></tr>
<tr><td class="name">Inputs</td><td>Each endpoint is a DateField group with editable spinbutton segments.</td></tr>
<tr><td class="name">Calendar</td><td>Range grid exposes <code>role="application"</code>, grid cells and day buttons.</td></tr>
<tr><td class="name">Keyboard</td><td>Arrow keys move by day/week, PageUp/PageDown navigates months, Enter/Space selects, Escape cancels range selection.</td></tr>
<tr><td class="name">Validation</td><td>Invalid min/max pairs are surfaced in the calendar snippet and logged through the UIX logger.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>
{#snippet calendarBody({ months, weekdays, validationMessage }: DateRangePickerCalendarSnippetProps)}
<DateRangePicker.Header>
<DateRangePicker.PrevButton aria-label="Previous month">&lt;</DateRangePicker.PrevButton>
<div class="range-header-titles" data-month-count={months.length}>
{#each months as month (`hdr-${month.value.toString()}`)}
<div class="range-month-title" id={monthHeadingId(month.value)}>
{formatMonthYear(month.value)}
</div>
{/each}
</div>
<DateRangePicker.NextButton aria-label="Next month">&gt;</DateRangePicker.NextButton>
</DateRangePicker.Header>
{#if validationMessage || boundsMessage || spanMessage}
<p class="calendar-error" role="alert">{validationMessage || boundsMessage || spanMessage}</p>
{/if}
<div class="range-months" data-month-count={months.length}>
{#each months as month (month.value.toString())}
<div class="range-month-panel"><!-- per-month title now lives in the header above -->
<DateRangePicker.Grid>
<DateRangePicker.GridHead>
<DateRangePicker.GridRow>
{#if showWeekNumbers}
<th data-range-calendar-head-cell data-range-calendar-week-number scope="col">wk</th>
{/if}
{#each weekdays as day}
<DateRangePicker.HeadCell>{day}</DateRangePicker.HeadCell>
{/each}
</DateRangePicker.GridRow>
</DateRangePicker.GridHead>
<DateRangePicker.GridBody>
{#each month.weeks as week}
<DateRangePicker.GridRow {week}>
{#snippet children({ weekNumber }: DateRangePickerGridRowSnippetProps)}
{#if showWeekNumbers}
<th data-range-calendar-head-cell data-range-calendar-week-number scope="row">
{weekNumber ?? ''}
</th>
{/if}
{#each week as date}
<DateRangePicker.Cell {date} month={month.value}>
<DateRangePicker.Day />
</DateRangePicker.Cell>
{/each}
{/snippet}
</DateRangePicker.GridRow>
{/each}
</DateRangePicker.GridBody>
</DateRangePicker.Grid>
</div>
{/each}
</div>
{/snippet}
{#snippet pickerFooter()}
{#if showFooter && (showClear || showCancel || showClose)}
<DateRangePicker.Footer>
{#if showClear}<DateRangePicker.Clear />{/if}
{#if showCancel}<DateRangePicker.Cancel />{/if}
{#if showClose}<DateRangePicker.Close />{/if}
</DateRangePicker.Footer>
{/if}
{/snippet}
<style>
.date-range-picker-stage {
inline-size: min(44rem, 100%);
}
.range-field-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) auto;
align-items: center;
gap: var(--uix-space-2);
inline-size: 100%;
}
.range-separator {
color: var(--uix-text-muted);
font-size: var(--uix-text-sm);
white-space: nowrap;
}
.range-picker-status {
display: flex;
align-items: center;
flex: 1 0 100%;
min-block-size: 1.5rem;
color: var(--uix-text-muted);
font-size: var(--uix-text-sm);
}
.range-picker-status[data-invalid='true'] {
color: var(--color-threat-text);
}
.range-months {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
justify-content: center;
gap: var(--calendar-month-gap, 36px);
max-inline-size: 100%;
overflow-x: auto;
}
.range-months[data-month-count='1'] {
grid-auto-flow: row;
}
.range-month-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--calendar-grid-gap);
min-inline-size: max-content;
}
.range-header-titles {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
gap: var(--calendar-month-gap, 36px);
flex: 1 1 auto;
min-inline-size: 0;
}
.range-month-title {
color: var(--calendar-heading-color);
font-size: var(--calendar-heading-font-size);
font-weight: var(--calendar-heading-font-weight);
text-align: center;
}
.calendar-error {
margin: 0;
max-inline-size: calc((var(--_calendar-day-size) * 7) + (var(--calendar-grid-gap) * 8));
color: var(--color-threat-text);
font-size: var(--uix-text-sm);
text-align: center;
overflow-wrap: anywhere;
}
[data-uix-control] input[type='date'],
[data-uix-control] input[type='number'] {
min-inline-size: 0;
padding: 6px var(--uix-space-3);
border: 1px solid var(--uix-line);
border-radius: var(--uix-radius-sm);
background: var(--uix-surface);
color: var(--uix-text);
font: inherit;
font-size: var(--uix-text-sm);
}
[data-uix-control] input[type='date']:focus,
[data-uix-control] input[type='number']:focus {
outline: none;
border-color: var(--uix-accent-line);
box-shadow: var(--uix-ring);
}
@media (max-width: 44rem) {
.range-field-row {
grid-template-columns: minmax(0, 1fr) auto;
}
:global(.range-field-row [data-date-range-field-input]) {
grid-column: 1 / -1;
}
.range-separator {
display: none;
}
:global(.range-field-row [data-date-range-picker-trigger]) {
grid-column: 2;
justify-self: end;
}
}
</style>

Powered by TurnKey Linux.