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

682 lines
30 KiB

feat(eidos): port range-calendar from soma + extract shared partial Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar primitive. Adds size / variant / color cascades; otherwise pass-through 14 compound parts (Provider, Header, Heading, PrevButton, NextButton, MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell, Cell, Day). ## Feature comparison | Feature | bits-ui | ark-ui | react-aria | Eidos | |----------------------|---------|--------|------------|-------| | Range start/end | ✓ | ✓ | ✓ | ✓ | | Hover preview state | ✓ | ✓ | ✓ | ✓ | | minDays / maxDays | — | — | ✓ | ✓ | | allowSingleDay | ✓ | ✓ | ✓ | ✓ | | preventDeselect | ✓ | ✓ | — | ✓ | | Multi-month display | ✓ | ✓ | ✓ | ✓ | | Week-number column | — | — | ✓ | ✓ | | Month/Year selects | — | ✓ | — | ✓ | | Disabled / readonly | ✓ | ✓ | ✓ | ✓ | | isDateUnavailable | ✓ | ✓ | ✓ | ✓ | | Holiday markers | — | — | — | ✓ | | Locale / dir | ✓ | ✓ | ✓ | ✓ | | Size cascade | — | — | — | ✓ 4 | | Variant cascade | — | — | — | ✓ 3 | | Color cascade | — | — | — | ✓ 8 | ## Range-cells partial extracted The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css` now owns ALL `[data-range-calendar-*]` selectors — the shared rules that previously lived inside `date-range-picker.css` (lines 537-834) moved here. Both consumers (the standalone `<RangeCalendar>` and `<DateRangePicker>`'s popover content) stamp the same data-attrs, so the same selectors paint without duplication. `date-range-picker.css` drops 298 lines of duplication; it keeps only the picker-specific scope overrides (`[data-date-range-picker-calendar]` with no chrome, max-content sizing, transparent bg). ## Morfo Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new wrapper appears in the eidos-layer inventory and the demo Morfo tab declares it correctly. ## Demo Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live tab exposes every range-specific prop: minDays / maxDays sliders, allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3), pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth, weekStartsOn, disabled, readonly, locale, plus size / variant / color cascades. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
<script lang="ts">
import {
RangeCalendar,
type RangeCalendarColor,
type RangeCalendarGridRowSnippetProps,
type RangeCalendarProviderSnippetProps,
type RangeCalendarSize,
type RangeCalendarVariant
} from '$uix/eidos/components/range-calendar';
import { compileMorfo } from '$uix/morfo';
import { rangeCalendarMorfo } from '@/uix/morfo/components/range-calendar';
import { getActiveUix } from '$active-uix';
import { CalendarDate, type DateRange, type DateValue, 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 MinDateMode = 'none' | 'today' | 'custom';
type MaxDateMode = 'none' | 'custom';
const uix = getActiveUix();
type EmitSignal = Parameters<NonNullable<typeof uix.events>['emit']>[0];
const initialMonth = new CalendarDate(2026, 5, 1);
const start = new CalendarDate(2026, 5, 8);
const end = new CalendarDate(2026, 5, 19);
const todayDate = new CalendarDate(2026, 5, 12);
const customMinDate = new CalendarDate(2026, 5, 5);
const customMaxDate = new CalendarDate(2026, 7, 15);
let tab = $state<Tab>('live');
let trace = $state<TraceEntry[]>([]);
let stageRef = $state<HTMLElement | null>(null);
let value = $state<DateRange>({ start, end });
let placeholder = $state<DateValue>(initialMonth);
let minDays = $state<number | undefined>(undefined);
let maxDays = $state<number | undefined>(14);
let allowSingleDay = $state(true);
let preventDeselect = $state(false);
let pagedNavigation = $state(false);
let fixedWeeks = $state(true);
let numberOfMonths = $state(1);
let showWeekNumbers = $state(false);
let disableDaysOutsideMonth = $state(false);
let disabled = $state(false);
let readonly = $state(false);
let minDateMode = $state<MinDateMode>('custom');
let maxDateMode = $state<MaxDateMode>('custom');
let weekStartsOn = $state<WeekStartControl>('1');
let locale = $state('en-US');
let size = $state<RangeCalendarSize>('md');
let variant = $state<RangeCalendarVariant>('surface');
let color = $state<RangeCalendarColor>('primary');
const compiled = compileMorfo(rangeCalendarMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const resolvedWeekStartsOn = $derived(
weekStartsOn === 'locale' ? undefined : (Number(weekStartsOn) as WeekStartsOn)
);
const selectionMinValue = $derived(
minDateMode === 'today' ? todayDate : minDateMode === 'custom' ? customMinDate : undefined
);
const selectionMaxValue = $derived(maxDateMode === 'custom' ? customMaxDate : undefined);
const minDateLabel = $derived(selectionMinValue?.toString() ?? 'none');
const maxDateLabel = $derived(selectionMaxValue?.toString() ?? 'none');
const rangeLabel = $derived.by(() => {
if (!value.start && !value.end) return 'empty';
if (value.start && !value.end) return `${value.start.toString()} → …`;
if (value.start && value.end) return `${value.start.toString()} → ${value.end.toString()}`;
return '—';
});
$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();
});
function fmtTime(at: number): string {
const d = new Date(at);
return `${String(d.getSeconds()).padStart(2, '0')}.${String(d.getMilliseconds()).padStart(3, '0')}`;
}
function emitSelectStart(signal: EmitSignal) {
void uix.events?.emit(signal);
}
const eidosSnippet = $derived(
[
"<script lang='ts'>",
" import { RangeCalendar } from '$uix/eidos/components/range-calendar';",
" import type { DateRange } from '$libs/days';",
' let value = $state<DateRange>({ start, end });',
'</' + 'script>',
'',
'<RangeCalendar',
' bind:value',
size !== 'md' && ` size="${size}"`,
variant !== 'surface' && ` variant="${variant}"`,
color !== 'primary' && ` color="${color}"`,
numberOfMonths !== 1 && ` numberOfMonths={${numberOfMonths}}`,
minDays !== undefined && ` minDays={${minDays}}`,
maxDays !== undefined && ` maxDays={${maxDays}}`,
!allowSingleDay && ' allowSingleDay={false}',
preventDeselect && ' preventDeselect',
pagedNavigation && ' pagedNavigation',
!fixedWeeks && ' fixedWeeks={false}',
showWeekNumbers && ' showWeekNumbers',
disableDaysOutsideMonth && ' disableDaysOutsideMonth',
disabled && ' disabled',
readonly && ' readonly',
'>',
' <RangeCalendar.Header>',
' <RangeCalendar.PrevButton />',
' <RangeCalendar.Heading />',
' <RangeCalendar.NextButton />',
' </RangeCalendar.Header>',
' <RangeCalendar.Grid>',
' <RangeCalendar.GridHead>',
' <RangeCalendar.GridRow>',
' {#snippet children({ weekdays })}',
' {#each weekdays as wd}<RangeCalendar.HeadCell>{wd}</RangeCalendar.HeadCell>{/each}',
' {/snippet}',
' </RangeCalendar.GridRow>',
' </RangeCalendar.GridHead>',
' <RangeCalendar.GridBody>',
' <!-- rows iterate via the months snippet prop -->',
' </RangeCalendar.GridBody>',
' </RangeCalendar.Grid>',
'</RangeCalendar>'
]
.filter(Boolean)
.join('\n')
);
function setNumberOfMonths(n: number) {
numberOfMonths = n;
}
</script>
<div data-uix-canvas-inner>
<header>
<div data-uix-eyebrow>Forms · RangeCalendar</div>
<h1 data-uix-page-title>Range calendar</h1>
<p data-uix-page-lede>
Calendar primitive for picking a date range. Soma owns the state
machine (start → preview → end), the keyboard nav, the min/max
constraints (<code>minValue</code>, <code>maxValue</code>,
<code>minDays</code>, <code>maxDays</code>) and the hover-preview
signaling. Eidos paints the cells with distinct
<code>data-range-start</code> / <code>data-range-end</code> /
<code>data-in-range</code> / <code>data-in-preview-range</code>
treatments — the shared partial at
<code>range-calendar.css</code> is consumed by both this standalone
component and by <code>DateRangePicker</code>'s popover content.
</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>sizes</span>4
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>apg</span>
<a href={rangeCalendarMorfo.apg} target="_blank" rel="noreferrer">grid ↗</a>
</span>
<span data-uix-meta-pill>
<span data-uix-meta-key>range</span> {rangeLabel}
</span>
</div>
</header>
<div data-uix-stage>
<div data-uix-stage-area bind:this={stageRef} style="padding: var(--uix-space-3); display: flex; justify-content: center;">
<RangeCalendar
bind:value
placeholder={placeholder as never}
{minDays}
{maxDays}
{allowSingleDay}
{preventDeselect}
{pagedNavigation}
{fixedWeeks}
{numberOfMonths}
{showWeekNumbers}
{disableDaysOutsideMonth}
{disabled}
{readonly}
minValue={selectionMinValue}
maxValue={selectionMaxValue}
weekStartsOn={resolvedWeekStartsOn}
{locale}
{size}
{variant}
{color}
>
{#snippet children({ months, weekdays }: RangeCalendarProviderSnippetProps)}
<RangeCalendar.Header>
<RangeCalendar.PrevButton aria-label="Previous month">‹</RangeCalendar.PrevButton>
<RangeCalendar.Heading />
<RangeCalendar.NextButton aria-label="Next month">›</RangeCalendar.NextButton>
</RangeCalendar.Header>
{#each months as month (month.value.toString())}
<RangeCalendar.Grid>
<RangeCalendar.GridHead>
<RangeCalendar.GridRow>
{#snippet children(_props: RangeCalendarGridRowSnippetProps)}
{#each weekdays as wd (wd)}
<RangeCalendar.HeadCell>{wd}</RangeCalendar.HeadCell>
{/each}
{/snippet}
</RangeCalendar.GridRow>
</RangeCalendar.GridHead>
<RangeCalendar.GridBody>
{#each month.weeks as week (week[0].toString())}
<RangeCalendar.GridRow week={week}>
{#snippet children(_props: RangeCalendarGridRowSnippetProps)}
{#each week as date (date.toString())}
<RangeCalendar.Cell {date} month={month.value}>
<RangeCalendar.Day />
</RangeCalendar.Cell>
{/each}
{/snippet}
</RangeCalendar.GridRow>
{/each}
</RangeCalendar.GridBody>
</RangeCalendar.Grid>
{/each}
{/snippet}
</RangeCalendar>
</div>
<div data-uix-stage-trace>
<span data-uix-stage-trace-key>trace</span>
{#if trace.length === 0}
<span>click a day to start; hover to preview; click second to commit</span>
{:else}
{#each trace.slice(0, 3) as entry (entry.at)}
<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>range</span> {rangeLabel}
</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>14</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>
Soma owns the range state machine, the keyboard navigation, the
min/max constraints and the hover-preview signaling. Eidos adds
variant/size/color cascades.
</p>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> range constraints
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>minValue</span>
<span data-uix-chips role="radiogroup">
{#each ['none', 'today', 'custom'] as mode (mode)}
<button data-uix-chip data-active={minDateMode === mode} onclick={() => (minDateMode = mode as MinDateMode)}>{mode}</button>
{/each}
</span>
<span data-uix-control-label-aux>{minDateLabel}</span>
</label>
<label data-uix-control>
<span data-uix-control-label>maxValue</span>
<span data-uix-chips role="radiogroup">
{#each ['none', 'custom'] as mode (mode)}
<button data-uix-chip data-active={maxDateMode === mode} onclick={() => (maxDateMode = mode as MaxDateMode)}>{mode}</button>
{/each}
</span>
<span data-uix-control-label-aux>{maxDateLabel}</span>
</label>
<label data-uix-control>
<span data-uix-control-label>minDays</span>
<input type="number" min="1" max="60" placeholder="—" bind:value={minDays} style="inline-size: 4rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label>maxDays</span>
<input type="number" min="1" max="365" placeholder="—" bind:value={maxDays} style="inline-size: 4rem;" />
</label>
<label data-uix-control>
<span data-uix-control-label>allowSingleDay</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={allowSingleDay} />
<span data-uix-switch-label>{allowSingleDay ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>preventDeselect</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={preventDeselect} />
<span data-uix-switch-label>{preventDeselect ? 'on' : 'off'}</span>
</span>
</label>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="soma">soma</span> grid layout
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>numberOfMonths</span>
<span data-uix-chips role="radiogroup">
{#each [1, 2, 3] as n (n)}
<button data-uix-chip data-active={numberOfMonths === n} onclick={() => setNumberOfMonths(n)}>{n}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>pagedNavigation</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={pagedNavigation} />
<span data-uix-switch-label>{pagedNavigation ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>fixedWeeks</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={fixedWeeks} />
<span data-uix-switch-label>{fixedWeeks ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>showWeekNumbers</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={showWeekNumbers} />
<span data-uix-switch-label>{showWeekNumbers ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>disableDaysOutsideMonth</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={disableDaysOutsideMonth} />
<span data-uix-switch-label>{disableDaysOutsideMonth ? 'on' : 'off'}</span>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>weekStartsOn</span>
<span data-uix-chips role="radiogroup">
{#each ['locale', '0', '1'] as opt (opt)}
<button data-uix-chip data-active={weekStartsOn === opt} onclick={() => (weekStartsOn = opt as WeekStartControl)}>{opt}</button>
{/each}
</span>
</label>
<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>
</div>
<div data-uix-subsection-head>
<span data-uix-layer-badge="eidos">eidos</span> visual props
</div>
<div data-uix-controls>
<label data-uix-control>
<span data-uix-control-label>variant</span>
<span data-uix-chips role="radiogroup">
{#each ['surface', 'outline', 'ghost'] as v (v)}
<button data-uix-chip data-active={variant === v} onclick={() => (variant = v as RangeCalendarVariant)}>{v}</button>
{/each}
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>size</span>
<span data-uix-chips role="radiogroup">
{#each ['xs', 'sm', 'md', 'lg'] as s (s)}
<button data-uix-chip data-active={size === s} onclick={() => (size = s as RangeCalendarSize)}>{s}</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 c (c)}
<button data-uix-chip data-active={color === c} onclick={() => (color = c as RangeCalendarColor)}>{c}</button>
{/each}
</span>
</label>
</div>
<div data-uix-code>
<div data-uix-code-head>
<span data-uix-layer-badge="eidos">eidos</span>
<span>composition · with bind:value</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>14 parts: Provider, Header, Heading, PrevButton, NextButton, MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell, Cell, Day.</p>
<div data-uix-subsection-head>Provider (RangeCalendar)</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Prop</th><th>Type</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 class="type">DateRange</td><td class="default">{`{}`}</td><td>Committed range.</td></tr>
<tr><td class="name">placeholder <span data-uix-tag data-kind="bindable">bindable</span></td><td class="type">DateValue</td><td class="default empty">today</td><td>Month currently visible.</td></tr>
<tr><td class="name">onValueChange</td><td class="type">(r: DateRange) =&gt; void</td><td class="default empty">—</td><td>Fires when end is committed.</td></tr>
<tr><td class="name">minValue · maxValue</td><td class="type">DateValue</td><td class="default empty">—</td><td>Allowed bounds.</td></tr>
<tr><td class="name">minDays · maxDays</td><td class="type">number</td><td class="default empty">—</td><td>Inclusive length constraints (days).</td></tr>
<tr><td class="name">allowSingleDay</td><td class="type">boolean</td><td class="default">true</td><td>Permit start === end.</td></tr>
<tr><td class="name">preventDeselect</td><td class="type">boolean</td><td class="default">false</td><td>Clicking the selected endpoint keeps the range.</td></tr>
<tr><td class="name">numberOfMonths</td><td class="type">number</td><td class="default">1</td><td>Visible months.</td></tr>
<tr><td class="name">pagedNavigation</td><td class="type">boolean</td><td class="default">false</td><td>Prev/Next advance by numberOfMonths.</td></tr>
<tr><td class="name">fixedWeeks</td><td class="type">boolean</td><td class="default">false</td><td>Always render 6 rows.</td></tr>
<tr><td class="name">disableDaysOutsideMonth</td><td class="type">boolean</td><td class="default">false</td><td>Skip non-current-month cells from selection.</td></tr>
<tr><td class="name">disabled · readonly</td><td class="type">boolean</td><td class="default">false</td><td>OR-merged with Field.Provider.</td></tr>
<tr><td class="name">isDateDisabled · isDateUnavailable · isDateHoliday</td><td class="type">DateMatcher</td><td class="default empty">—</td><td>Per-cell decorators.</td></tr>
<tr><td class="name">weekStartsOn · weekdayFormat · locale · dir</td><td class="type">—</td><td class="default empty">locale</td><td>Localization.</td></tr>
<tr><td class="name">size <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">'xs' | 'sm' | 'md' | 'lg'</td><td class="default">'md'</td><td>Day cells + nav buttons + selects.</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>Calendar chrome.</td></tr>
<tr><td class="name">color <span data-uix-tag data-kind="eidos">eidos</span></td><td class="type">ColorRole</td><td class="default">'primary'</td><td>End-endpoint + in-range fill + focus accent.</td></tr>
</tbody>
</table>
</div>
<div data-uix-subsection-head>Sub-parts</div>
<p data-uix-section-desc style="margin-top: 0.25rem;">
The sub-parts (<code>Header</code>, <code>Heading</code>,
<code>PrevButton</code>, <code>NextButton</code>,
<code>MonthSelect</code>, <code>YearSelect</code>,
<code>Grid</code>, <code>GridHead</code>, <code>GridBody</code>,
<code>GridRow</code>, <code>HeadCell</code>, <code>Cell</code>,
<code>Day</code>) are pass-throughs to soma — they accept the
headless props from the soma `RangeCalendarXxxProps` types
unchanged. See the morfo table below for the per-part data
contract.
</p>
</section>
{/if}
{#if tab === 'morfo'}
<section data-uix-section>
<h2 data-uix-section-title>
<span data-uix-layer-badge="morfo">morfo</span>
· declarative contract
</h2>
<p data-uix-section-desc>Source: <code>src/uix/morfo/components/range-calendar.ts</code>.</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">"{rangeCalendarMorfo.name}"</td></tr>
<tr><td class="name">kebab</td><td class="type">"{rangeCalendarMorfo.kebab}"</td></tr>
<tr><td class="name">apg</td><td><a href={rangeCalendarMorfo.apg} target="_blank" rel="noreferrer">{rangeCalendarMorfo.apg}</a></td></tr>
<tr><td class="name">scope</td><td class="default">{rangeCalendarMorfo.scope.join(', ')}</td></tr>
<tr><td class="name">parts.length</td><td class="default">{rangeCalendarMorfo.parts.length}</td></tr>
<tr><td class="name">events.length</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 (part.kebab)}
<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>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 events as action (action.name)}
{@const sem = action.semantic}
{@const intentDecl = 'intent' in sem ? sem.intent : undefined}
{@const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:${(intentDecl as { fromProp?: string }).fromProp ?? '?'}` : '—'}
<tr>
<td class="name">{action.name}</td>
<td class="type">{sem.family}</td>
<td>{sem.verb ?? '—'}</td>
<td>{sem.sequence ?? 'pre'}</td>
<td class="default">{intentStr}</td>
<td>{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>
<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>Play</th></tr></thead>
<tbody>
{#each events as action (action.name)}
{@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined}
{@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined}
<tr>
<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">{effectiveIntent ?? '—'}</td>
<td>
<button
data-uix-play
onclick={() => {
const target = (stageRef?.querySelector('[data-range-calendar]') ?? stageRef) as HTMLElement | null;
if (!target) return;
emitSelectStart({
name: action.name,
family: action.semantic.family,
target,
...(effectiveIntent ? { intent: effectiveIntent } : {})
});
}}
>▶ play</button>
</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 at <code>src/uix/eidos/components/range-calendar/range-calendar.css</code>.
This file is consumed BOTH by the standalone eidos
<code>&lt;RangeCalendar&gt;</code> and by
<code>&lt;DateRangePicker&gt;</code>'s popover content — the
same <code>[data-range-calendar-*]</code> attrs land in both
environments. The picker recipe only adds scoping overrides
(no chrome, max-content sizing, transparent background).
</p>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Selector</th><th>Source</th><th>What it paints</th></tr></thead>
<tbody>
<tr><td class="name">[data-range-calendar]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Root chrome — border + bg + padding + token scaffolding.</td></tr>
<tr><td class="name">[data-range-calendar][data-size]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Day-cell + control-button size scale (xs/sm/md/lg).</td></tr>
<tr><td class="name">[data-range-calendar][data-variant]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Surface / outline / ghost chrome.</td></tr>
<tr><td class="name">[data-range-calendar][data-color]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>End-endpoint + in-range + focus accent tokens.</td></tr>
<tr><td class="name">[data-range-calendar-day][data-range-start]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Start endpoint: affirm-family solid + left-edge stripe + semibold.</td></tr>
<tr><td class="name">[data-range-calendar-day][data-range-end]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>End endpoint: accent-family solid + right-edge stripe.</td></tr>
<tr><td class="name">[data-range-calendar-day][data-range-start][data-range-end]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Single-day range: diagonal-split gradient (start ↘ end).</td></tr>
<tr><td class="name">[data-range-calendar-cell][data-in-range]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>In-range cells get accent-track background fill on the cell.</td></tr>
<tr><td class="name">[data-range-calendar-day][data-in-preview-range]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Hover-preview state while user is choosing end (transparent + accent border).</td></tr>
<tr><td class="name">[data-range-calendar-day][data-today]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Today marker (border ring + semibold).</td></tr>
<tr><td class="name">[data-range-calendar-day][data-outside-month]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>Outside-month dimming.</td></tr>
<tr><td class="name">[data-range-calendar-day][data-disabled] · [data-unavailable]</td><td><span data-uix-tag data-kind="eidos">eidos</span></td><td>not-allowed cursor + reduced opacity (+ line-through for unavailable).</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>
Follows the APG <a href={rangeCalendarMorfo.apg} target="_blank" rel="noreferrer">grid pattern</a>
with localized labels for range-start / range-end / in-range
announced via <code>aria-label</code> additions on the relevant
day cells.
</p>
<div data-uix-subsection-head>Keyboard</div>
<div data-uix-table-wrap>
<table data-uix-table>
<thead><tr><th>Key</th><th>Action</th></tr></thead>
<tbody>
<tr><td><span data-uix-kbd>ArrowLeft</span> / <span data-uix-kbd>ArrowRight</span></td><td>Move focus ±1 day.</td></tr>
<tr><td><span data-uix-kbd>ArrowUp</span> / <span data-uix-kbd>ArrowDown</span></td><td>Move focus ±1 week.</td></tr>
<tr><td><span data-uix-kbd>PageUp</span> / <span data-uix-kbd>PageDown</span></td><td>Move focus ±1 month.</td></tr>
<tr><td><span data-uix-kbd>Shift+PageUp</span> / <span data-uix-kbd>Shift+PageDown</span></td><td>Move focus ±1 year.</td></tr>
<tr><td><span data-uix-kbd>Home</span> / <span data-uix-kbd>End</span></td><td>First / last day of the visible week.</td></tr>
<tr><td><span data-uix-kbd>Enter</span> / <span data-uix-kbd>Space</span></td><td>Commit start (first press) or end (second press).</td></tr>
<tr><td><span data-uix-kbd>Escape</span></td><td>Cancel the in-progress range; revert to last committed.</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>Attribute</th><th>Value</th></tr></thead>
<tbody>
<tr><td class="name">provider</td><td>role</td><td class="type">"application"</td></tr>
<tr><td class="name">provider</td><td>aria-label</td><td class="type">localized "Range calendar"</td></tr>
<tr><td class="name">grid</td><td>role</td><td class="type">"grid"</td></tr>
<tr><td class="name">cell</td><td>role · aria-selected</td><td class="type">"gridcell" / "true|false"</td></tr>
<tr><td class="name">day</td><td>aria-label</td><td class="type">"date — range start | range end | in range"</td></tr>
<tr><td class="name">prev-button · next-button</td><td>aria-label</td><td class="type">"Previous month" / "Next month"</td></tr>
</tbody>
</table>
</div>
</section>
{/if}
</div>

Powered by TurnKey Linux.