Eidos Calendar
Visual wrapper for Soma Calendar. Calendar follows the compound Eidos surface:
Calendar is the public root and parts hang from it (Calendar.Day,
Calendar.Grid, etc.). There is no public Provider export.
Baseline
| Source |
Relevant coverage |
Decision |
Air morfo-runtime:src/uix/air/components/calendar |
Full visual shell over Terra: root size, header, heading, prev/next, month/year selects, grid, cell/day states; direct Air sound on selection/navigation. |
Keep the full part coverage and size. Do not copy direct sound; selection/navigation move to Morfo/Sema. |
| Soma current |
Single/multiple value, placeholder navigation, min/max, disabled/unavailable/holiday matchers, fixed weeks, multi-month, week numbers, locale, RTL and live announcements. |
Eidos wraps every public Soma part and adds only size, variant, color. |
| Morfo current |
15 declared parts including WeekNumber contract, grid APG, day/cell state data. |
Add semantic events because Calendar is interactive. WeekNumber remains contract-only until Soma exposes a component. |
| Sema current |
No calendar pack before this migration. |
Add calendarSema for select/unselect and navigation. |
Comparativa
| Capability |
Air |
React Aria |
Ark UI |
Bits UI |
shadcn-svelte |
UIX decision |
| Single date |
yes |
yes |
yes |
yes |
yes |
Soma owns. |
| Multiple dates |
inherited Terra |
RangeCalendar separate in React Aria; non-contiguous supported by API docs |
selectionMode="multiple" |
type="multiple" |
via Bits Calendar |
Soma supports type="multiple". |
| Multiple months |
yes |
visibleDuration |
numOfMonths |
numberOfMonths |
via Bits Calendar |
Soma supports numberOfMonths; Eidos styles without assuming layout. |
| Fixed weeks |
yes |
display option |
fixedWeeks |
fixedWeeks |
via Bits Calendar |
Soma supports. |
| Month/year select |
yes |
documented as custom dropdowns |
documented example |
public MonthSelect/YearSelect |
generated wrappers around Bits |
Eidos exposes MonthSelect and YearSelect. |
| Min/max/unavailable |
yes |
minValue, maxValue, isDateUnavailable |
min, max, isDateUnavailable |
min/max/unavailable/disabled |
via Bits Calendar |
Soma supports selection and view bounds; invalid pairs surface as invalid + logger error. |
| Week numbers |
no Air-specific part |
not a first-class Calendar part |
showWeekNumbers |
documented separately through composition |
not primary API |
Soma exposes showWeekNumbers snippet data; visual support is included. |
| Holiday/weekend flags |
Air styled some date states only |
custom render state |
custom machine state possible |
custom render state possible |
via custom code |
Soma emits data-holiday and data-weekend; Eidos styles both. |
| Semantic feedback |
Air called interaction sounds in wrapper |
library-specific announcements |
callbacks |
callbacks |
none in wrapper |
Morfo/Sema owns commit-select, commit-unselect, shift-navigate. |
Reference docs checked: React Aria Calendar, Ark UI Date Picker, Bits UI
Calendar, shadcn-svelte Calendar, Radix Primitives components list and WAI-ARIA
APG Date Picker Dialog/Grid.
Morfo / Sema
| Event |
Family |
Target |
Runtime source |
Sema default |
commit-select |
commit.select |
day |
CalendarProvider.select(date, target) after value changes |
soft form commit + tap |
commit-unselect |
commit.remove |
day |
same, when a date is removed/cleared |
subtle form commit + tap |
shift-navigate |
shift.navigate |
provider fallback, actual button/select/day target when known |
prevPage, nextPage, setMonth, setYear, keyboard month boundary |
subtle ascending navigation sound |
Calendar is not passive: a 0-event contract would hide committed selection and
visible-range navigation from the semantic layer. This migration closes that gap
before adding the Eidos recipe.
Eidos API
<script lang="ts">
import { Calendar } from '$uix/eidos/components/calendar';
import { CalendarDate, type DateValue } from '$libs/days';
const placeholder = new CalendarDate(2026, 5, 1);
let value = $state<DateValue | undefined>(placeholder);
</script>
<Calendar type="single" bind:value {placeholder} size="md" variant="surface" color="primary">
{#snippet children({ months, weekdays })}
<Calendar.Header>
<Calendar.PrevButton>Previous</Calendar.PrevButton>
<Calendar.Heading />
<Calendar.NextButton>Next</Calendar.NextButton>
</Calendar.Header>
{#each months as month (month.value.toString())}
<Calendar.Grid>
<Calendar.GridHead>
<Calendar.GridRow>
{#each weekdays as day}
<Calendar.HeadCell>{day}</Calendar.HeadCell>
{/each}
</Calendar.GridRow>
</Calendar.GridHead>
<Calendar.GridBody>
{#each month.weeks as week}
<Calendar.GridRow>
{#each week as date}
<Calendar.Cell {date} month={month.value}>
<Calendar.Day />
</Calendar.Cell>
{/each}
</Calendar.GridRow>
{/each}
</Calendar.GridBody>
</Calendar.Grid>
{/each}
{/snippet}
</Calendar>
Decisiones
- No se expone
Calendar.Provider en Eidos. El componente raíz
monta el provider internamente y estampa data-size / data-variant
/ data-color. Los consumidores manipulan estado vía props
(bind:value, bind:placeholder).
commit-select lleva intent: 'affirm' porque seleccionar una
fecha es un compromiso evaluativamente positivo (suave). Un commit
destructivo correspondería a commit-unselect con intent: 'neutral'.
shift-navigate cubre todo el movimiento de mes/año (prev/next,
selects, atajos de teclado). No se subdivide por dirección — la
perceptiva de Sema se modula con target y sequence, no con
variantes de verbo.
- Las flechas de teclado son focus moves puros: mueven el día
enfocado pero no mutan
value. Por eso el morfo declara 10
keyboards y sólo 3 eventos: Enter/Space (mutación) → commit-select,
los 8 restantes no necesitan evento propio.
data-readonly es estado, no atributo de morfo input: el grid
marca el aria-readonly y la receta lo tinta con tono atenuado pero
permite seguir leyendo y navegando con teclado.
Gaps
| Gap |
Disposición |
Detalle |
Public Calendar.WeekNumber part |
diferir |
Morfo declara el part, pero Soma expone los números de semana a través del snippet data de GridRow en vez de un componente DOM. Requiere refactor en Soma antes de exponer en Eidos. |
| Range selection |
descartar |
Vive en range-calendar / componentes de range, no en Calendar. |
| Popover date picker shell |
descartar |
Vive en date-picker; Calendar es inline por diseño. |
| Modal + footer (clear / cancel / close) compartido con pickers |
diferir |
Calendar inline no lo necesita; los pickers lo aportan en su capa. Si llega un caso de Calendar como contenido de Drawer, se reevalúa. |
| Año/mes con scroll vertical estilo iOS |
diferir |
Patrón nuevo, no cubierto por ninguna referencia que mantengamos. Sin caso de uso real. |