|
|
<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) => 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) => 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) => 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>
|