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-field/+page.svelte

1009 lines
43 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<script lang="ts">
import {
DateRangeField,
type DateRangeFieldSize,
type DateRangeFieldVariant,
type DateRangeFieldColor
} from '$uix/eidos/components/date-range-field';
import { compileMorfo } from '$uix/morfo';
import { dateRangeFieldMorfo } from '@/uix/morfo/components/date-range-field';
import { getActiveUix } from '$active-uix';
import {
CalendarDate,
CalendarDateTime,
ZonedDateTime,
type DateValue,
type DateRange,
type EditableSegmentPart,
type Granularity,
type HourCycle
} from '$libs/days';
const uix = getActiveUix();
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event: string; family: string; intent?: string; at: number };
type Direction = 'ltr' | 'rtl';
type ValueProfile = 'date' | 'datetime' | 'zoned';
type Kind = 'day' | 'month' | 'year';
// ── Sample anchors (one per value profile) ───────────────────────────
const startDate = new CalendarDate(2026, 5, 19);
const endDate = new CalendarDate(2026, 6, 3);
const startDateTime = new CalendarDateTime(2026, 5, 19, 13, 45, 0);
const endDateTime = new CalendarDateTime(2026, 6, 3, 9, 30, 0);
const startZoned = new ZonedDateTime(2026, 5, 19, 'Europe/Madrid', 7_200_000, 13, 45, 0);
const endZoned = new ZonedDateTime(2026, 6, 3, 'Europe/Madrid', 7_200_000, 9, 30, 0);
const minDate = new CalendarDate(2026, 1, 1);
const maxDate = new CalendarDate(2026, 12, 31);
// ── Tab state ────────────────────────────────────────────────────────
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
// ── Live state — every public prop wired ─────────────────────────────
let valueProfile = $state<ValueProfile>('date');
let kind = $state<Kind>('day');
let granularity = $state<Granularity>('day');
let value = $state<DateRange>({ start: startDate, end: endDate });
let placeholder = $state<DateValue>(startDate);
let nameStart = $state('startDate');
let nameEnd = $state('endDate');
let required = $state(false);
let disabled = $state(false);
let readonly = $state(false);
let bounds = $state(false);
let readonlyStartYear = $state(false);
let readonlyStartMonth = $state(false);
let readonlyStartDay = $state(false);
let readonlyEndYear = $state(false);
let readonlyEndMonth = $state(false);
let readonlyEndDay = $state(false);
let locale = $state('en-US');
let dir = $state<Direction>('ltr');
let hourCycle = $state<HourCycle>(24);
let hideTimeZone = $state(false);
let size = $state<DateRangeFieldSize>('md');
let variant = $state<DateRangeFieldVariant>('surface');
let color = $state<DateRangeFieldColor>('primary');
const sizes: DateRangeFieldSize[] = ['xs', 'sm', 'md', 'lg', 'xl'];
const variants: DateRangeFieldVariant[] = ['surface', 'outline', 'ghost'];
const colors: DateRangeFieldColor[] = ['primary', 'secondary', 'neutral', 'affirm', 'risk', 'threat'];
const kinds: Kind[] = ['day', 'month', 'year'];
const granularities: Granularity[] = ['day', 'hour', 'minute', 'second'];
const hourCycles: HourCycle[] = [12, 24];
const directions: Direction[] = ['ltr', 'rtl'];
const valueProfiles: ValueProfile[] = ['date', 'datetime', 'zoned'];
const locales = ['en-US', 'en-GB', 'es-ES', 'de-DE', 'fr-FR', 'ja-JP'];
// ── Derived state ────────────────────────────────────────────────────
const startReadonlySegments = $derived<EditableSegmentPart[]>(
[
...(readonlyStartYear ? (['year'] as const) : []),
...(readonlyStartMonth ? (['month'] as const) : []),
...(readonlyStartDay ? (['day'] as const) : [])
]
);
const endReadonlySegments = $derived<EditableSegmentPart[]>(
[
...(readonlyEndYear ? (['year'] as const) : []),
...(readonlyEndMonth ? (['month'] as const) : []),
...(readonlyEndDay ? (['day'] as const) : [])
]
);
const valueLabel = $derived(
`{ start: ${value.start?.toString() ?? '—'}, end: ${value.end?.toString() ?? '—'} }`
);
const showsTimeSegments = $derived(granularity !== 'day');
function valueFor(profile: ValueProfile): DateRange {
if (profile === 'date') return { start: startDate, end: endDate };
if (profile === 'datetime') return { start: startDateTime, end: endDateTime };
return { start: startZoned, end: endZoned };
}
function placeholderFor(profile: ValueProfile): DateValue {
if (profile === 'date') return startDate;
if (profile === 'datetime') return startDateTime;
return startZoned;
}
function setProfile(next: ValueProfile): void {
valueProfile = next;
if (next === 'date') granularity = 'day';
if (next !== 'date' && granularity === 'day') granularity = 'minute';
value = valueFor(next);
placeholder = placeholderFor(next);
}
function setGranularity(next: Granularity): void {
granularity = next;
if (next === 'day' && valueProfile !== 'date') setProfile('date');
if (next !== 'day' && valueProfile === 'date') setProfile('datetime');
}
function clearRange(): void {
value = { start: undefined, end: undefined };
}
function resetRange(): void {
value = valueFor(valueProfile);
}
// ── Trace observer ───────────────────────────────────────────────────
$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();
});
// ── Morfo introspection ──────────────────────────────────────────────
const compiled = compileMorfo(dateRangeFieldMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const rawMorfo = dateRangeFieldMorfo as unknown as {
name: string;
kebab: string;
scope: readonly string[];
apg?: string;
parts: ReadonlyArray<{
name: string;
kebab: string;
archetype?: string;
kind: string;
defaultElement?: string;
role?: string;
optional?: boolean;
states?: readonly string[];
data?: ReadonlyArray<{ attr: string; values?: readonly string[]; severity?: string }>;
aria?: ReadonlyArray<{
attr: string;
value?: { kind: string };
condition?: { when: string; prop?: string; part?: string };
severity?: string;
}>;
keyboard?: ReadonlyArray<{ key: string; action: string }>;
}>;
events?: ReadonlyArray<{
name: string;
semantic: {
family: string;
verb?: string;
target?: unknown;
intent?: unknown;
sequence?: string;
};
}>;
};
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
function valueLiteral(profile: ValueProfile, end: boolean): string {
if (profile === 'date') {
const d = end ? endDate : startDate;
return `new CalendarDate(${d.year}, ${d.month}, ${d.day})`;
}
if (profile === 'datetime') {
const d = end ? endDateTime : startDateTime;
return `new CalendarDateTime(${d.year}, ${d.month}, ${d.day}, ${d.hour}, ${d.minute}, ${d.second})`;
}
const d = end ? endZoned : startZoned;
return `new ZonedDateTime(${d.year}, ${d.month}, ${d.day}, '${d.timeZone}', ${d.offset}, ${d.hour}, ${d.minute}, ${d.second})`;
}
// ── Snippets ─────────────────────────────────────────────────────────
const somaSnippet = $derived(
[
"<script lang='ts'>",
" import * as DateRangeField from '$soma/components/date-range-field';",
" import { type DateRange } from '$libs/days';",
' let value = $state<DateRange>({',
` start: ${valueLiteral(valueProfile, false)},`,
` end: ${valueLiteral(valueProfile, true)}`,
' });',
'</' + 'script>',
'',
'<DateRangeField.Provider',
' bind:value',
kind !== 'day' && ` kind="${kind}"`,
locale !== 'en-US' && ` locale="${locale}"`,
dir !== 'ltr' && ` dir="${dir}"`,
hourCycle !== 24 && ` hourCycle={${hourCycle}}`,
hideTimeZone && ' hideTimeZone',
disabled && ' disabled',
readonly && ' readonly',
required && ' required',
startReadonlySegments.length > 0 &&
` startReadonlySegments={[${startReadonlySegments.map((s) => `'${s}'`).join(', ')}]}`,
endReadonlySegments.length > 0 &&
` endReadonlySegments={[${endReadonlySegments.map((s) => `'${s}'`).join(', ')}]}`,
bounds && ` minValue={new CalendarDate(${minDate.year}, ${minDate.month}, ${minDate.day})}`,
bounds && ` maxValue={new CalendarDate(${maxDate.year}, ${maxDate.month}, ${maxDate.day})}`,
'>',
' <DateRangeField.Label>Period</DateRangeField.Label>',
` <DateRangeField.Input type="start" name="${nameStart}">`,
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateField.Segment part={seg.part}>{seg.value}</DateField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
` <DateRangeField.Input type="end" name="${nameEnd}">`,
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateField.Segment part={seg.part}>{seg.value}</DateField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
'</DateRangeField.Provider>'
]
.filter(Boolean)
.join('\n')
);
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { DateRangeField } from '$uix/eidos/components/date-range-field';",
" import { type DateRange } from '$libs/days';",
' let value = $state<DateRange>({',
` start: ${valueLiteral(valueProfile, false)},`,
` end: ${valueLiteral(valueProfile, true)}`,
' });',
'</' + 'script>',
'',
'<DateRangeField',
' bind:value',
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
size !== 'md' && ` size="${size}"`,
kind !== 'day' && ` kind="${kind}"`,
locale !== 'en-US' && ` locale="${locale}"`,
dir !== 'ltr' && ` dir="${dir}"`,
hourCycle !== 24 && ` hourCycle={${hourCycle}}`,
hideTimeZone && ' hideTimeZone',
disabled && ' disabled',
readonly && ' readonly',
required && ' required',
startReadonlySegments.length > 0 &&
` startReadonlySegments={[${startReadonlySegments.map((s) => `'${s}'`).join(', ')}]}`,
endReadonlySegments.length > 0 &&
` endReadonlySegments={[${endReadonlySegments.map((s) => `'${s}'`).join(', ')}]}`,
bounds && ` minValue={new CalendarDate(${minDate.year}, ${minDate.month}, ${minDate.day})}`,
bounds && ` maxValue={new CalendarDate(${maxDate.year}, ${maxDate.month}, ${maxDate.day})}`,
'>',
' <DateRangeField.Label>Period</DateRangeField.Label>',
` <DateRangeField.Input type="start" name="${nameStart}">`,
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
` <DateRangeField.Input type="end" name="${nameEnd}">`,
' {#snippet children({ segments })}',
' {#each segments as seg, i (i)}',
' <DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>',
' {/each}',
' {/snippet}',
' </DateRangeField.Input>',
'</DateRangeField>'
]
.filter(Boolean)
.join('\n')
);
// ── Reference-library comparison rows ────────────────────────────────
const refRows = [
{ lib: 'Bits UI', dr: '✓', kindFilter: '—', perEndpointReadonly: '✓', sharedValidator: '✓', autoBridge: '✓' },
{ lib: 'Ark UI', dr: '✓', kindFilter: '—', perEndpointReadonly: '—', sharedValidator: '✓', autoBridge: '✓' },
{ lib: 'react-aria', dr: '✓', kindFilter: '—', perEndpointReadonly: '—', sharedValidator: '✓', autoBridge: '✓' },
{ lib: 'Chakra v3', dr: '—', kindFilter: '—', perEndpointReadonly: '—', sharedValidator: '—', autoBridge: '—' },
{ lib: 'Eidos (this)', dr: '✓', kindFilter: '✓ (date/month/year)', perEndpointReadonly: '✓', sharedValidator: '✓', autoBridge: '✓' }
];
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Form · DateRangeField</div>
<h1 data-uix-page-title>DateRangeField</h1>
<p data-uix-page-lede>
Two-endpoint date entry, segment-by-segment. Mirrors
<code>DateField</code> × 2 with shared validation
(<code>minValue</code> / <code>maxValue</code> / <code>validate</code>)
and a single <code>Label</code> naming the whole range.
<span data-uix-layer-badge="soma">soma</span> scopes a
<code>DateFieldProvider</code> per endpoint internally; eidos
overlays <code>data-date-field</code> on the root so the entire
<code>date-field.css</code> cascade (chrome, variant, size,
color) applies inside the range without duplicating recipe rules.
</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>profile</span>{valueProfile}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>kind</span>{kind}
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>locale</span>{locale}
</span>
<span data-uix-meta-pill>
<a
href={rawMorfo.apg}
target="_blank"
rel="noopener"
style="color: inherit; text-decoration: none;"
>
<span data-uix-meta-key>apg</span>spinbutton
</a>
</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef}>
<DateRangeField
bind:value
bind:placeholder
{size}
{variant}
{color}
{kind}
{disabled}
{readonly}
{required}
{startReadonlySegments}
{endReadonlySegments}
{granularity}
{hourCycle}
{hideTimeZone}
{locale}
{dir}
minValue={bounds ? minDate : undefined}
maxValue={bounds ? maxDate : undefined}
>
<DateRangeField.Label>Period</DateRangeField.Label>
<DateRangeField.Input type="start" name={nameStart}>
{#snippet children({ segments })}
{#each segments as seg, i (i)}
<DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
<DateRangeField.Input type="end" name={nameEnd}>
{#snippet children({ segments })}
{#each segments as seg, i (i)}
<DateRangeField.Segment part={seg.part}>{seg.value}</DateRangeField.Segment>
{/each}
{/snippet}
</DateRangeField.Input>
</DateRangeField>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>focus a segment and press arrows / digits to fire commits</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>value</span>
<code>{valueLabel}</code>
</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>4</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>
Every public prop wired. Switch the value profile, change
granularity, flip individual segments to readonly per
endpoint, swap locale / dir / hour-cycle — the live preview
re-renders and both snippets (soma + eidos) update.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · value shape
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>value profile <span data-uix-control-hint>changes DateValue subclass</span></span>
<span data-uix-chips role="radiogroup">
{#each valueProfiles as opt}
<button data-uix-chip data-active={valueProfile === opt} onclick={() => setProfile(opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>kind <span data-uix-control-hint>segment set</span></span>
<span data-uix-chips role="radiogroup">
{#each kinds as opt}
<button data-uix-chip data-active={kind === opt} onclick={() => (kind = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>granularity</span>
<span data-uix-chips role="radiogroup">
{#each granularities as opt}
<button
data-uix-chip
data-active={granularity === opt}
disabled={valueProfile === 'date' && opt !== 'day'}
onclick={() => setGranularity(opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>hourCycle</span>
<span data-uix-chips role="radiogroup">
{#each hourCycles as opt}
<button data-uix-chip data-active={hourCycle === opt} disabled={!showsTimeSegments} onclick={() => (hourCycle = opt)}>{opt}h</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>hideTimeZone</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={hideTimeZone} disabled={valueProfile !== 'zoned'} />
<span data-uix-switch-label>{hideTimeZone ? 'hidden' : 'shown'}</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · locale + direction
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>locale</span>
<span data-uix-chips role="radiogroup">
{#each locales as opt}
<button data-uix-chip data-active={locale === opt} onclick={() => (locale = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>dir</span>
<span data-uix-chips role="radiogroup">
{#each directions as opt}
<button data-uix-chip data-active={dir === opt} onclick={() => (dir = opt)}>{opt}</button>
{/each}
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · flags
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>disabled</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={disabled} />
<span data-uix-switch-label>{disabled ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>readonly</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonly} />
<span data-uix-switch-label>{readonly ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>required</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={required} />
<span data-uix-switch-label>{required ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>min/max bounds <span data-uix-control-hint>{minDate.toString()} … {maxDate.toString()}</span></span>
<span data-uix-switch>
<input type="checkbox" bind:checked={bounds} />
<span data-uix-switch-label>{bounds ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>name (start)</span>
<input type="text" bind:value={nameStart} />
</label>
<label data-uix-control>
<span data-uix-control-label>name (end)</span>
<input type="text" bind:value={nameEnd} />
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> props · per-endpoint readonly segments
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>start · year</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonlyStartYear} />
<span data-uix-switch-label>{readonlyStartYear ? 'locked' : 'editable'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>start · month</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonlyStartMonth} />
<span data-uix-switch-label>{readonlyStartMonth ? 'locked' : 'editable'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>start · day</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonlyStartDay} disabled={kind !== 'day'} />
<span data-uix-switch-label>{readonlyStartDay ? 'locked' : 'editable'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>end · year</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonlyEndYear} />
<span data-uix-switch-label>{readonlyEndYear ? 'locked' : 'editable'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>end · month</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonlyEndMonth} />
<span data-uix-switch-label>{readonlyEndMonth ? 'locked' : 'editable'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>end · day</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={readonlyEndDay} disabled={kind !== 'day'} />
<span data-uix-switch-label>{readonlyEndDay ? 'locked' : 'editable'}</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> props · chrome
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each sizes as opt}
<button data-uix-chip data-active={size === opt} onclick={() => (size = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each variants as opt}
<button data-uix-chip data-active={variant === opt} onclick={() => (variant = opt)}>{opt}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>color</span>
<span data-uix-chips role="radiogroup">
{#each colors as opt}
<button data-uix-chip data-active={color === opt} onclick={() => (color = opt)}>{opt}</button>
{/each}
</span>
</label>
</div>
<div data-uix-subsection-head>Actions</div>
<div data-uix-controls>
<button data-uix-chip onclick={resetRange}>Reset value</button>
<button data-uix-chip onclick={clearRange}>Clear value</button>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="soma">soma</span>
<span>headless · DateFieldProvider scoped per endpoint</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 · size + variant + color cascade</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>
<div data-uix-subsection-head>DateRangeField (root)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">value <span data-uix-tag data-kind="soma">soma</span></td><td class="type">DateRange</td><td class="default empty">—</td><td>Bindable <code>{`{ start, end }`}</code>. Each endpoint is a <code>DateValue</code>.</td></tr>
<tr><td class="name">placeholder <span data-uix-tag data-kind="soma">soma</span></td><td class="type">DateValue</td><td class="default empty">—</td><td>Drives the segment format for both endpoints.</td></tr>
<tr><td class="name">onValueChange <span data-uix-tag data-kind="soma">soma</span></td><td class="type">(v: DateRange) =&gt; void</td><td class="default empty">—</td><td>Fired when both endpoints resolve to a valid range.</td></tr>
<tr><td class="name">onStartValueChange <span data-uix-tag data-kind="soma">soma</span></td><td class="type">(v: DateValue) =&gt; void</td><td class="default empty">—</td><td>Per-endpoint hook.</td></tr>
<tr><td class="name">onEndValueChange <span data-uix-tag data-kind="soma">soma</span></td><td class="type">(v: DateValue) =&gt; void</td><td class="default empty">—</td><td>Per-endpoint hook.</td></tr>
<tr><td class="name">validate <span data-uix-tag data-kind="soma">soma</span></td><td class="type">DateRangeValidator</td><td class="default empty">—</td><td>Custom validator. Return a string error message to mark invalid.</td></tr>
<tr><td class="name">onInvalid <span data-uix-tag data-kind="soma">soma</span></td><td class="type">DateOnInvalid</td><td class="default empty">—</td><td>Called when validation fails.</td></tr>
<tr><td class="name">minValue <span data-uix-tag data-kind="soma">soma</span></td><td class="type">DateValue</td><td class="default empty">—</td><td>Earliest acceptable date for both endpoints.</td></tr>
<tr><td class="name">maxValue <span data-uix-tag data-kind="soma">soma</span></td><td class="type">DateValue</td><td class="default empty">—</td><td>Latest acceptable date for both endpoints.</td></tr>
<tr><td class="name">kind <span data-uix-tag data-kind="soma">soma</span></td><td class="type">'day' | 'month' | 'year'</td><td class="default">'day'</td><td>Segment filter (Chakra-style). Both endpoints share the same kind.</td></tr>
<tr><td class="name">granularity <span data-uix-tag data-kind="soma">soma</span></td><td class="type">'day' | 'hour' | 'minute' | 'second'</td><td class="default">auto (from placeholder)</td><td>Visual granularity. Adds time segments for <code>hour+</code>.</td></tr>
<tr><td class="name">hourCycle <span data-uix-tag data-kind="soma">soma</span></td><td class="type">12 | 24</td><td class="default">locale-derived</td><td>12-hour or 24-hour clock.</td></tr>
<tr><td class="name">hideTimeZone <span data-uix-tag data-kind="soma">soma</span></td><td class="type">boolean</td><td class="default">false</td><td>Hide the timezone segment on <code>ZonedDateTime</code>.</td></tr>
<tr><td class="name">locale <span data-uix-tag data-kind="soma">soma</span></td><td class="type">string</td><td class="default">soma config</td><td>BCP 47 locale tag.</td></tr>
<tr><td class="name">dir <span data-uix-tag data-kind="soma">soma</span></td><td class="type">'ltr' | 'rtl'</td><td class="default">soma prefs</td><td>Text direction.</td></tr>
<tr><td class="name">disabled <span data-uix-tag data-kind="soma">soma</span></td><td class="type">boolean</td><td class="default">false</td><td>Disables both inputs.</td></tr>
<tr><td class="name">readonly <span data-uix-tag data-kind="soma">soma</span></td><td class="type">boolean</td><td class="default">false</td><td>Segments focusable, selection blocked.</td></tr>
<tr><td class="name">required <span data-uix-tag data-kind="soma">soma</span></td><td class="type">boolean</td><td class="default">false</td><td>Hidden form inputs are required.</td></tr>
<tr><td class="name">startReadonlySegments <span data-uix-tag data-kind="soma">soma</span></td><td class="type">EditableSegmentPart[]</td><td class="default">[]</td><td>Per-segment lock for the start endpoint.</td></tr>
<tr><td class="name">endReadonlySegments <span data-uix-tag data-kind="soma">soma</span></td><td class="type">EditableSegmentPart[]</td><td class="default">[]</td><td>Per-segment lock for the end endpoint.</td></tr>
<tr><td class="name">errorMessageId <span data-uix-tag data-kind="soma">soma</span></td><td class="type">string</td><td class="default empty">—</td><td>External error element id (linked via <code>aria-describedby</code>).</td></tr>
<tr><td class="name">variant <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'surface' | 'outline' | 'ghost'</td><td class="default">'surface'</td><td>Mirrors DateField vocabulary.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'primary' | 'secondary' | 'neutral' | 'affirm' | 'risk' | 'threat'</td><td class="default">'primary'</td><td>Active-segment tint.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'xs' | 'sm' | 'md' | 'lg' | 'xl' (responsive)</td><td class="default">'md'</td><td>Cascaded to both inputs + segments.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>DateRangeField.Label</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">id</td><td class="type">string</td><td>Auto-generated. Used by both <code>Input</code>s as <code>aria-labelledby</code>.</td></tr>
<tr><td class="name">children</td><td class="type">Snippet</td><td>Label text.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>DateRangeField.Input</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Type</th><th>Default</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">type</td><td class="type">'start' | 'end'</td><td class="default empty">— required</td><td>Which endpoint this input drives.</td></tr>
<tr><td class="name">name</td><td class="type">string</td><td class="default empty">—</td><td>Form field name for the hidden ISO-string input.</td></tr>
<tr><td class="name">aria-label</td><td class="type">string</td><td class="default empty">—</td><td>Prefer pairing with <code>DateRangeField.Label</code>; only used when label is absent.</td></tr>
<tr><td class="name">aria-labelledby</td><td class="type">string</td><td class="default empty">—</td><td>External labelling element id.</td></tr>
</tbody>
</table>
<p data-uix-section-desc>
Snippet args: <code>{`{ segments: Array<{ part, value }> }`}</code>.
Key the iteration by index <code>(i)</code> — <code>seg.part</code>
repeats for separator literals.
</p>
</div>
<div data-uix-subsection-head>DateRangeField.Segment</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Name</th><th>Type</th><th>Notes</th></tr></thead>
<tbody>
<tr><td class="name">part</td><td class="type">SegmentPart</td><td>Re-export of <code>DateField.Segment</code>. Each <code>Input</code> scopes its own DateField context so segment resolution lands on the right endpoint.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Reference parity</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead>
<tr><th>Library</th><th>DateRangeField</th><th>kind filter</th><th>per-endpoint readonly segs</th><th>shared validator</th><th>auto-bridges to DateField</th></tr>
</thead>
<tbody>
{#each refRows as row}
<tr>
<td class="name">{row.lib}</td>
<td>{row.dr}</td>
<td>{row.kindFilter}</td>
<td>{row.perEndpointReadonly}</td>
<td>{row.sharedValidator}</td>
<td>{row.autoBridge}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>Morfo contract</h2>
<div data-uix-table-wrap>
<table data-uix-table>
<tbody>
<tr><td class="name">name</td><td>{rawMorfo.name}</td></tr>
<tr><td class="name">kebab</td><td><code>{rawMorfo.kebab}</code></td></tr>
<tr><td class="name">scope</td><td>{rawMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">apg</td><td><a href={rawMorfo.apg} target="_blank" rel="noopener">{rawMorfo.apg}</a></td></tr>
<tr><td class="name">parts</td><td>{rawMorfo.parts.length}</td></tr>
<tr><td class="name">events</td><td>{rawMorfo.events?.length ?? 0}</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Parts overview</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead>
<tr><th>Kebab</th><th>Element</th><th>Role</th><th>Archetype</th><th>Kind</th><th>States</th><th>Optional</th></tr>
</thead>
<tbody>
{#each rawMorfo.parts as part}
<tr>
<td class="name"><code>{part.kebab}</code></td>
<td>{part.defaultElement ?? '—'}</td>
<td>{part.role ?? '—'}</td>
<td>{part.archetype ?? '—'}</td>
<td>{part.kind}</td>
<td>{part.states?.join(', ') ?? '—'}</td>
<td>{part.optional ? '✓' : '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{#each rawMorfo.parts as part}
{#if (part.data && part.data.length > 0) || (part.aria && part.aria.length > 0) || (part.keyboard && part.keyboard.length > 0)}
<div data-uix-subsection-head>{part.name} <code>({part.kebab})</code></div>
{#if part.data && part.data.length > 0}
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>data-attr</th><th>values</th><th>severity</th></tr></thead>
<tbody>
{#each part.data as d}
<tr>
<td class="name"><code>{d.attr}</code></td>
<td>{d.values?.join(' / ') ?? '—'}</td>
<td>{d.severity ?? 'required'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if part.aria && part.aria.length > 0}
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>aria-attr</th><th>value source</th><th>condition</th><th>severity</th></tr></thead>
<tbody>
{#each part.aria as a}
<tr>
<td class="name"><code>{a.attr}</code></td>
<td>{a.value?.kind ?? '—'}</td>
<td>{a.condition ? `${a.condition.when}:${a.condition.prop ?? a.condition.part ?? ''}` : '—'}</td>
<td>{a.severity ?? 'required'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{#if part.keyboard && part.keyboard.length > 0}
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>key</th><th>action</th></tr></thead>
<tbody>
{#each part.keyboard as k}
<tr>
<td><span data-uix-kbd>{k.key === ' ' ? 'Space' : k.key}</span></td>
<td>{k.action}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
{/if}
{/each}
{#if rawMorfo.events && rawMorfo.events.length > 0}
<div data-uix-subsection-head>Events</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead>
<tr><th>name</th><th>family</th><th>verb</th><th>sequence</th></tr>
</thead>
<tbody>
{#each rawMorfo.events as ev}
<tr>
<td class="name">{ev.name}</td>
<td>{ev.semantic.family}</td>
<td>{ev.semantic.verb ?? '—'}</td>
<td>{ev.semantic.sequence ?? 'pre'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
{/if}
{#if tab === 'sema'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="sema">sema</span> · perceptual events
</h2>
<p data-uix-section-desc>
DateRangeField morfo declares <strong>{events.length} events of its own</strong>.
The range provider is passive at this contract layer — sema
signals come from the two endpoint DateField runtimes scoped
inside each <code>Input</code>. See the
<a href="/uix/components/date-field" style="color: inherit;">DateField demo</a>'s
Sema tab for the full event vocabulary (segment commits, value
changes, validation signals).
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Source</th><th>Events delegated</th></tr></thead>
<tbody>
<tr>
<td>date-range-field morfo</td>
<td>0 (passive)</td>
</tr>
<tr>
<td>date-field runtime · start endpoint</td>
<td>Forwarded by soma's <code>DateFieldProvider.create</code> inside the start <code>Input</code>.</td>
</tr>
<tr>
<td>date-field runtime · end endpoint</td>
<td>Forwarded by soma's <code>DateFieldProvider.create</code> inside the end <code>Input</code>.</td>
</tr>
</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>
<code>date-range-field.css</code> is layout-only. Each
endpoint <code>Input</code> carries both
<code>data-date-range-field-input</code> AND
<code>data-date-field-input</code> on the same element (soma
overlays the markers). The eidos wrapper also overlays
<code>data-date-field</code> on the range root, so the entire
<code>date-field.css</code> cascade (chrome, variant, size,
color tokens) applies inside the range with zero duplication.
The range recipe contributes only:
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Owner</th><th>Purpose</th></tr></thead>
<tbody>
<tr><td class="name"><code>[data-date-range-field]</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>3-column grid: <code>1fr · auto · 1fr</code> with label spanning the full first row.</td></tr>
<tr><td class="name"><code>[data-date-range-field-label]</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Label sits above both inputs, spans the entire grid row.</td></tr>
<tr><td class="name"><code>[data-date-range-field-input][data-endpoint='start']</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Placed in column 1, row 2.</td></tr>
<tr><td class="name"><code>[data-date-range-field-input][data-endpoint='end']</code></td><td><span data-uix-tag data-kind="morfo">morfo</span></td><td>Placed in column 3, row 2.</td></tr>
<tr><td class="name"><code>[data-date-range-field]:has(…)::before</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Em-dash separator rendered in column 2, row 2 (only when both endpoints are present).</td></tr>
<tr><td class="name"><code>[data-date-range-field][data-disabled]</code></td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Opacity reduction + pointer-events lock.</td></tr>
</tbody>
</table>
</div>
<p data-uix-section-desc>
All input chrome (border, padding, gap, focus ring, segment
active-tint, variant cascade) comes from
<code>date-field.css</code> — no recipe duplication.
</p>
</section>
{/if}
{#if tab === 'a11y'}
<section data-uix-section>
<h2 data-uix-section-title>A11y contract</h2>
<p data-uix-section-desc>
Each segment is a WAI-ARIA <a
href="https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/"
target="_blank"
rel="noopener">Spinbutton</a> — focusable, arrow-key
stepping, paste distribution. The two endpoints behave
identically; the only inter-endpoint behaviour is the
shared validation (range-level <code>validate</code>,
<code>minValue</code>, <code>maxValue</code> apply to the
resolved range).
</p>
<div data-uix-subsection-head>Keyboard (per segment)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Key</th><th>Effect</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>ArrowUp</span></td><td>Increment segment value (clamps to min/max).</td></tr>
<tr><td><span data-uix-kbd>ArrowDown</span></td><td>Decrement segment value.</td></tr>
<tr><td><span data-uix-kbd>ArrowLeft</span> / <span data-uix-kbd>ArrowRight</span></td><td>Move focus to previous / next segment within the active <code>Input</code>.</td></tr>
<tr><td><span data-uix-kbd>Tab</span> / <span data-uix-kbd>Shift+Tab</span></td><td>Move focus across endpoints (start → end and back).</td></tr>
<tr><td>typeahead</td><td>Direct numeric entry; auto-advances when the segment is full.</td></tr>
<tr><td><span data-uix-kbd>Backspace</span></td><td>Clear current segment.</td></tr>
<tr><td><span data-uix-kbd>Delete</span></td><td>Clear current segment.</td></tr>
<tr><td>paste</td><td>Distribute pasted ISO-string across the segments of the focused endpoint.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>ARIA contract</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Part</th><th>role</th><th>aria-*</th></tr></thead>
<tbody>
<tr><td>Provider</td><td>group</td><td><code>aria-labelledby</code> → Label id when present</td></tr>
<tr><td>Label</td><td>—</td><td>Used as the accessible name for both Inputs.</td></tr>
<tr><td>Input</td><td>group</td><td><code>aria-labelledby</code> chains Label + endpoint role.</td></tr>
<tr><td>Segment</td><td>spinbutton</td><td><code>aria-valuenow</code>, <code>aria-valuemin</code>, <code>aria-valuemax</code>, <code>aria-label</code>, <code>aria-readonly</code> when in <code>{`{start,end}ReadonlySegments`}</code>.</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.