parent
fb5dd0919c
commit
d6f550554d
@ -0,0 +1,96 @@
|
||||
# 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 audit
|
||||
|
||||
| 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. |
|
||||
|
||||
## Reference comparison
|
||||
|
||||
| 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
|
||||
|
||||
```svelte
|
||||
<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>
|
||||
```
|
||||
|
||||
## Gaps
|
||||
|
||||
| Gap | Owner | Decision |
|
||||
| --------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Public `Calendar.WeekNumber` part | Soma | Deferred. Morfo declares the part, but Soma currently exposes week numbers through `GridRow` snippet data instead of a DOM component. |
|
||||
| Range selection | Soma | Deferred to `range-calendar` / date range components, not Eidos Calendar. |
|
||||
| Popover date picker shell | Soma/Eidos date-picker | Deferred to `date-picker`; Calendar remains inline. |
|
||||
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarCellProps, CalendarCellSnippetProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarCellProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.Cell {...rest}>
|
||||
{#snippet children(snippetProps: CalendarCellSnippetProps)}
|
||||
{@render bodyContent?.(snippetProps)}
|
||||
{/snippet}
|
||||
</Calendar.Cell>
|
||||
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarDayProps, CalendarDaySnippetProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarDayProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<Calendar.Day {...rest}>
|
||||
{#snippet children(snippetProps: CalendarDaySnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</Calendar.Day>
|
||||
{:else}
|
||||
<Calendar.Day {...rest} />
|
||||
{/if}
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarGridBodyProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarGridBodyProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.GridBody {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
</Calendar.GridBody>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarGridHeadProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarGridHeadProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.GridHead {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
</Calendar.GridHead>
|
||||
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarGridRowProps, CalendarGridRowSnippetProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarGridRowProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.GridRow {...rest}>
|
||||
{#snippet children(snippetProps: CalendarGridRowSnippetProps)}
|
||||
{@render bodyContent?.(snippetProps)}
|
||||
{/snippet}
|
||||
</Calendar.GridRow>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarGridProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarGridProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.Grid {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
</Calendar.Grid>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarHeadCellProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarHeadCellProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.HeadCell {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
</Calendar.HeadCell>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarHeaderProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarHeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.Header {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
</Calendar.Header>
|
||||
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarHeadingProps, CalendarHeadingSnippetProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarHeadingProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<Calendar.Heading {...rest}>
|
||||
{#snippet children(snippetProps: CalendarHeadingSnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</Calendar.Heading>
|
||||
{:else}
|
||||
<Calendar.Heading {...rest} />
|
||||
{/if}
|
||||
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarMonthSelectProps, CalendarMonthSelectSnippetProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarMonthSelectProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<Calendar.MonthSelect {...rest}>
|
||||
{#snippet children(snippetProps: CalendarMonthSelectSnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</Calendar.MonthSelect>
|
||||
{:else}
|
||||
<Calendar.MonthSelect {...rest} />
|
||||
{/if}
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarNextButtonProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarNextButtonProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.NextButton {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
</Calendar.NextButton>
|
||||
@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarPrevButtonProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarPrevButtonProps = $props();
|
||||
</script>
|
||||
|
||||
<Calendar.PrevButton {...rest}>
|
||||
{@render bodyContent?.()}
|
||||
</Calendar.PrevButton>
|
||||
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarYearSelectProps, CalendarYearSelectSnippetProps } from './types';
|
||||
|
||||
let { children: bodyContent, ...rest }: CalendarYearSelectProps = $props();
|
||||
</script>
|
||||
|
||||
{#if bodyContent}
|
||||
<Calendar.YearSelect {...rest}>
|
||||
{#snippet children(snippetProps: CalendarYearSelectSnippetProps)}
|
||||
{@render bodyContent(snippetProps)}
|
||||
{/snippet}
|
||||
</Calendar.YearSelect>
|
||||
{:else}
|
||||
<Calendar.YearSelect {...rest} />
|
||||
{/if}
|
||||
@ -0,0 +1,362 @@
|
||||
[data-calendar] {
|
||||
--_calendar-padding: var(--calendar-padding-md);
|
||||
--_calendar-control-size: var(--calendar-control-size-md);
|
||||
--_calendar-day-size: var(--calendar-day-size-md);
|
||||
--_calendar-font-size: var(--calendar-font-size-md);
|
||||
--_calendar-accent-solid: var(--color-primary-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-primary-solid-hover);
|
||||
--_calendar-accent-track: var(--color-primary-track);
|
||||
--_calendar-accent-border: var(--color-primary-border);
|
||||
--_calendar-accent-text: var(--color-primary-text);
|
||||
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
gap: var(--calendar-gap);
|
||||
inline-size: max-content;
|
||||
max-inline-size: 100%;
|
||||
padding: var(--_calendar-padding);
|
||||
border: var(--calendar-border-width) solid var(--calendar-border);
|
||||
border-radius: var(--calendar-radius);
|
||||
background: var(--calendar-bg);
|
||||
box-shadow: var(--calendar-shadow);
|
||||
color: var(--calendar-color);
|
||||
font-family: var(--calendar-font-family);
|
||||
font-size: var(--_calendar-font-size);
|
||||
line-height: var(--calendar-line-height);
|
||||
}
|
||||
|
||||
[data-calendar][data-size='sm'] {
|
||||
--_calendar-padding: var(--calendar-padding-sm);
|
||||
--_calendar-control-size: var(--calendar-control-size-sm);
|
||||
--_calendar-day-size: var(--calendar-day-size-sm);
|
||||
--_calendar-font-size: var(--calendar-font-size-sm);
|
||||
}
|
||||
|
||||
[data-calendar][data-size='lg'] {
|
||||
--_calendar-padding: var(--calendar-padding-lg);
|
||||
--_calendar-control-size: var(--calendar-control-size-lg);
|
||||
--_calendar-day-size: var(--calendar-day-size-lg);
|
||||
--_calendar-font-size: var(--calendar-font-size-lg);
|
||||
}
|
||||
|
||||
[data-calendar][data-variant='outline'] {
|
||||
background: var(--calendar-outline-bg);
|
||||
box-shadow: var(--calendar-outline-shadow);
|
||||
}
|
||||
|
||||
[data-calendar][data-variant='ghost'] {
|
||||
padding: 0;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-calendar][data-color='secondary'] {
|
||||
--_calendar-accent-solid: var(--color-secondary-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-secondary-solid-hover);
|
||||
--_calendar-accent-track: var(--color-secondary-track);
|
||||
--_calendar-accent-border: var(--color-secondary-border);
|
||||
--_calendar-accent-text: var(--color-secondary-text);
|
||||
}
|
||||
|
||||
[data-calendar][data-color='neutral'] {
|
||||
--_calendar-accent-solid: var(--color-neutral-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-neutral-solid-hover);
|
||||
--_calendar-accent-track: var(--color-neutral-track);
|
||||
--_calendar-accent-border: var(--color-neutral-border);
|
||||
--_calendar-accent-text: var(--color-neutral-text);
|
||||
}
|
||||
|
||||
[data-calendar][data-color='affirm'] {
|
||||
--_calendar-accent-solid: var(--color-affirm-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-affirm-solid-hover);
|
||||
--_calendar-accent-track: var(--color-affirm-track);
|
||||
--_calendar-accent-border: var(--color-affirm-border);
|
||||
--_calendar-accent-text: var(--color-affirm-text);
|
||||
}
|
||||
|
||||
[data-calendar][data-color='fulfill'] {
|
||||
--_calendar-accent-solid: var(--color-fulfill-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-fulfill-solid-hover);
|
||||
--_calendar-accent-track: var(--color-fulfill-track);
|
||||
--_calendar-accent-border: var(--color-fulfill-border);
|
||||
--_calendar-accent-text: var(--color-fulfill-text);
|
||||
}
|
||||
|
||||
[data-calendar][data-color='risk'] {
|
||||
--_calendar-accent-solid: var(--color-risk-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-risk-solid-hover);
|
||||
--_calendar-accent-track: var(--color-risk-track);
|
||||
--_calendar-accent-border: var(--color-risk-border);
|
||||
--_calendar-accent-text: var(--color-risk-text);
|
||||
}
|
||||
|
||||
[data-calendar][data-color='threat'] {
|
||||
--_calendar-accent-solid: var(--color-threat-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-threat-solid-hover);
|
||||
--_calendar-accent-track: var(--color-threat-track);
|
||||
--_calendar-accent-border: var(--color-threat-border);
|
||||
--_calendar-accent-text: var(--color-threat-text);
|
||||
}
|
||||
|
||||
[data-calendar][data-color='loss'] {
|
||||
--_calendar-accent-solid: var(--color-loss-solid);
|
||||
--_calendar-accent-solid-hover: var(--color-loss-solid-hover);
|
||||
--_calendar-accent-track: var(--color-loss-track);
|
||||
--_calendar-accent-border: var(--color-loss-border);
|
||||
--_calendar-accent-text: var(--color-loss-text);
|
||||
}
|
||||
|
||||
[data-calendar-header] {
|
||||
display: grid;
|
||||
grid-template-columns: var(--_calendar-control-size) minmax(0, 1fr) var(--_calendar-control-size);
|
||||
align-items: center;
|
||||
gap: var(--calendar-header-gap);
|
||||
inline-size: 100%;
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
[data-calendar-header-controls] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--calendar-header-gap);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
[data-calendar-heading] {
|
||||
flex: 1 1 auto;
|
||||
min-inline-size: 0;
|
||||
color: var(--calendar-heading-color);
|
||||
font-weight: var(--calendar-heading-font-weight);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-calendar-month-panel] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--calendar-grid-gap);
|
||||
min-inline-size: max-content;
|
||||
}
|
||||
|
||||
[data-calendar-month-panel] > [data-calendar-heading] {
|
||||
flex: 0 0 auto;
|
||||
font-size: var(--calendar-heading-font-size, 0.95em);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-calendar-prev-button],
|
||||
[data-calendar-next-button] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
inline-size: var(--_calendar-control-size);
|
||||
block-size: var(--_calendar-control-size);
|
||||
padding: 0;
|
||||
border: var(--calendar-control-border-width) solid var(--calendar-control-border);
|
||||
border-radius: var(--calendar-control-radius);
|
||||
background: var(--calendar-control-bg);
|
||||
color: var(--calendar-control-color);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
border-color var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
color var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
box-shadow var(--calendar-transition-duration) var(--calendar-transition-ease);
|
||||
}
|
||||
|
||||
[data-calendar-prev-button]:hover,
|
||||
[data-calendar-next-button]:hover {
|
||||
border-color: var(--calendar-control-border-hover);
|
||||
background: var(--calendar-control-bg-hover);
|
||||
color: var(--calendar-control-color-hover);
|
||||
}
|
||||
|
||||
[data-calendar-prev-button]:focus-visible,
|
||||
[data-calendar-next-button]:focus-visible,
|
||||
[data-calendar-month-select]:focus-visible,
|
||||
[data-calendar-year-select]:focus-visible,
|
||||
[data-calendar-day]:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--calendar-focus-shadow);
|
||||
}
|
||||
|
||||
[data-calendar-prev-button][data-disabled],
|
||||
[data-calendar-next-button][data-disabled],
|
||||
[data-calendar-month-select][data-disabled],
|
||||
[data-calendar-year-select][data-disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: var(--calendar-disabled-opacity);
|
||||
}
|
||||
|
||||
[data-calendar-month-select],
|
||||
[data-calendar-year-select] {
|
||||
min-inline-size: 0;
|
||||
block-size: var(--_calendar-control-size);
|
||||
padding-inline: var(--calendar-select-px);
|
||||
border: var(--calendar-control-border-width) solid var(--calendar-control-border);
|
||||
border-radius: var(--calendar-control-radius);
|
||||
background: var(--calendar-control-bg);
|
||||
color: var(--calendar-control-color);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
border-color var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
color var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
box-shadow var(--calendar-transition-duration) var(--calendar-transition-ease);
|
||||
}
|
||||
|
||||
[data-calendar-month-select] {
|
||||
inline-size: calc(var(--_calendar-day-size) * 2.35);
|
||||
}
|
||||
|
||||
[data-calendar-year-select] {
|
||||
inline-size: calc(var(--_calendar-day-size) * 2.15);
|
||||
}
|
||||
|
||||
[data-calendar-month-select]:hover,
|
||||
[data-calendar-year-select]:hover {
|
||||
border-color: var(--calendar-control-border-hover);
|
||||
background: var(--calendar-control-bg-hover);
|
||||
color: var(--calendar-control-color-hover);
|
||||
}
|
||||
|
||||
[data-calendar-grid] {
|
||||
inline-size: max-content;
|
||||
max-inline-size: 100%;
|
||||
margin-inline: auto;
|
||||
border-collapse: separate;
|
||||
border-spacing: var(--calendar-grid-gap);
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
[data-calendar-head-cell] {
|
||||
inline-size: var(--_calendar-day-size);
|
||||
block-size: var(--calendar-weekday-size);
|
||||
color: var(--calendar-weekday-color);
|
||||
font-size: var(--calendar-weekday-font-size);
|
||||
font-weight: var(--calendar-weekday-font-weight);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
[data-calendar-week-number] {
|
||||
color: var(--calendar-weekday-color);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
[data-calendar-week-number][scope='row'] {
|
||||
block-size: var(--_calendar-day-size);
|
||||
font-size: var(--calendar-weekday-font-size);
|
||||
font-weight: var(--calendar-weekday-font-weight);
|
||||
}
|
||||
|
||||
[data-calendar-cell] {
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
[data-calendar-day] {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: var(--_calendar-day-size);
|
||||
block-size: var(--_calendar-day-size);
|
||||
border: var(--calendar-day-border-width) solid var(--calendar-day-border);
|
||||
border-radius: var(--calendar-day-radius);
|
||||
background: var(--calendar-day-bg);
|
||||
color: var(--calendar-day-color);
|
||||
font: inherit;
|
||||
font-weight: var(--calendar-day-font-weight);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition:
|
||||
background var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
border-color var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
color var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
box-shadow var(--calendar-transition-duration) var(--calendar-transition-ease),
|
||||
opacity var(--calendar-transition-duration) var(--calendar-transition-ease);
|
||||
}
|
||||
|
||||
[data-calendar-day]:hover {
|
||||
border-color: var(--calendar-day-border-hover);
|
||||
background: var(--calendar-day-bg-hover);
|
||||
color: var(--calendar-day-color-hover);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-selected] {
|
||||
border-color: var(--_calendar-accent-solid);
|
||||
background: var(--_calendar-accent-solid);
|
||||
color: var(--calendar-day-selected-color);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-selected]:hover {
|
||||
border-color: var(--_calendar-accent-solid-hover);
|
||||
background: var(--_calendar-accent-solid-hover);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-focused]:not([data-selected]) {
|
||||
border-color: var(--_calendar-accent-border);
|
||||
background: var(--_calendar-accent-track);
|
||||
color: var(--_calendar-accent-text);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-today]:not([data-selected]) {
|
||||
border-color: var(--calendar-day-today-border);
|
||||
color: var(--calendar-day-today-color);
|
||||
font-weight: var(--calendar-day-today-font-weight);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-outside-month] {
|
||||
color: var(--calendar-day-outside-color);
|
||||
opacity: var(--calendar-day-outside-opacity);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-weekend]:not([data-selected]) {
|
||||
color: var(--calendar-day-weekend-color);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-holiday]:not([data-selected]) {
|
||||
box-shadow: var(--calendar-day-holiday-shadow);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-unavailable] {
|
||||
text-decoration-line: line-through;
|
||||
text-decoration-color: var(--calendar-day-unavailable-decoration);
|
||||
}
|
||||
|
||||
[data-calendar-day][data-disabled],
|
||||
[data-calendar-day][data-unavailable] {
|
||||
cursor: not-allowed;
|
||||
opacity: var(--calendar-disabled-opacity);
|
||||
}
|
||||
|
||||
[data-calendar][data-invalid] {
|
||||
border-color: var(--calendar-invalid-border);
|
||||
}
|
||||
|
||||
[data-calendar][data-disabled] {
|
||||
opacity: var(--calendar-disabled-opacity);
|
||||
}
|
||||
|
||||
[data-calendar][data-event],
|
||||
[data-calendar-day][data-event],
|
||||
[data-calendar-prev-button][data-event],
|
||||
[data-calendar-next-button][data-event],
|
||||
[data-calendar-month-select][data-event],
|
||||
[data-calendar-year-select][data-event] {
|
||||
box-shadow: var(--calendar-event-shadow);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-calendar-prev-button],
|
||||
[data-calendar-next-button],
|
||||
[data-calendar-month-select],
|
||||
[data-calendar-year-select],
|
||||
[data-calendar-day] {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { ActiveEidos } from '$uix/eidos';
|
||||
import * as Calendar from '$soma/components/calendar';
|
||||
import type { CalendarProps, CalendarProviderSnippetProps } from './types';
|
||||
|
||||
let {
|
||||
size = 'md',
|
||||
variant = 'surface',
|
||||
color = 'primary',
|
||||
value = $bindable(),
|
||||
placeholder = $bindable(),
|
||||
children: bodyContent,
|
||||
...rest
|
||||
}: CalendarProps = $props();
|
||||
|
||||
const eidos = ActiveEidos.require();
|
||||
const resolvedSize = $derived(eidos.resolve(size, 'md'));
|
||||
</script>
|
||||
|
||||
<Calendar.Provider
|
||||
{...rest}
|
||||
bind:value
|
||||
bind:placeholder
|
||||
data-size={resolvedSize}
|
||||
data-variant={variant}
|
||||
data-color={color}
|
||||
>
|
||||
{#snippet children(snippetProps: CalendarProviderSnippetProps)}
|
||||
{@render bodyContent?.(snippetProps)}
|
||||
{/snippet}
|
||||
</Calendar.Provider>
|
||||
@ -0,0 +1,76 @@
|
||||
import CalendarComponent from './calendar.svelte'
|
||||
import Header from './calendar-header.svelte'
|
||||
import Heading from './calendar-heading.svelte'
|
||||
import PrevButton from './calendar-prev-button.svelte'
|
||||
import NextButton from './calendar-next-button.svelte'
|
||||
import MonthSelect from './calendar-month-select.svelte'
|
||||
import YearSelect from './calendar-year-select.svelte'
|
||||
import Grid from './calendar-grid.svelte'
|
||||
import GridHead from './calendar-grid-head.svelte'
|
||||
import GridBody from './calendar-grid-body.svelte'
|
||||
import GridRow from './calendar-grid-row.svelte'
|
||||
import HeadCell from './calendar-head-cell.svelte'
|
||||
import Cell from './calendar-cell.svelte'
|
||||
import Day from './calendar-day.svelte'
|
||||
|
||||
type CalendarNamespace = typeof CalendarComponent & {
|
||||
Header: typeof Header
|
||||
Heading: typeof Heading
|
||||
PrevButton: typeof PrevButton
|
||||
NextButton: typeof NextButton
|
||||
MonthSelect: typeof MonthSelect
|
||||
YearSelect: typeof YearSelect
|
||||
Grid: typeof Grid
|
||||
GridHead: typeof GridHead
|
||||
GridBody: typeof GridBody
|
||||
GridRow: typeof GridRow
|
||||
HeadCell: typeof HeadCell
|
||||
Cell: typeof Cell
|
||||
Day: typeof Day
|
||||
}
|
||||
|
||||
const Calendar = CalendarComponent as CalendarNamespace
|
||||
Calendar.Header = Header
|
||||
Calendar.Heading = Heading
|
||||
Calendar.PrevButton = PrevButton
|
||||
Calendar.NextButton = NextButton
|
||||
Calendar.MonthSelect = MonthSelect
|
||||
Calendar.YearSelect = YearSelect
|
||||
Calendar.Grid = Grid
|
||||
Calendar.GridHead = GridHead
|
||||
Calendar.GridBody = GridBody
|
||||
Calendar.GridRow = GridRow
|
||||
Calendar.HeadCell = HeadCell
|
||||
Calendar.Cell = Cell
|
||||
Calendar.Day = Day
|
||||
|
||||
export { Calendar }
|
||||
|
||||
export default Calendar
|
||||
|
||||
export type {
|
||||
CalendarProps,
|
||||
CalendarHeaderProps as HeaderProps,
|
||||
CalendarHeadingProps as HeadingProps,
|
||||
CalendarPrevButtonProps as PrevButtonProps,
|
||||
CalendarNextButtonProps as NextButtonProps,
|
||||
CalendarMonthSelectProps as MonthSelectProps,
|
||||
CalendarYearSelectProps as YearSelectProps,
|
||||
CalendarGridProps as GridProps,
|
||||
CalendarGridHeadProps as GridHeadProps,
|
||||
CalendarGridBodyProps as GridBodyProps,
|
||||
CalendarGridRowProps as GridRowProps,
|
||||
CalendarHeadCellProps as HeadCellProps,
|
||||
CalendarCellProps as CellProps,
|
||||
CalendarDayProps as DayProps,
|
||||
CalendarProviderSnippetProps,
|
||||
CalendarCellSnippetProps,
|
||||
CalendarDaySnippetProps,
|
||||
CalendarHeadingSnippetProps,
|
||||
CalendarMonthSelectSnippetProps,
|
||||
CalendarYearSelectSnippetProps,
|
||||
CalendarGridRowSnippetProps,
|
||||
CalendarSize,
|
||||
CalendarVariant,
|
||||
CalendarColor
|
||||
} from './types'
|
||||
@ -0,0 +1,62 @@
|
||||
import type {
|
||||
CalendarCellProps as SomaCalendarCellProps,
|
||||
CalendarCellSnippetProps,
|
||||
CalendarDayProps as SomaCalendarDayProps,
|
||||
CalendarDaySnippetProps,
|
||||
CalendarGridBodyProps as SomaCalendarGridBodyProps,
|
||||
CalendarGridHeadProps as SomaCalendarGridHeadProps,
|
||||
CalendarGridProps as SomaCalendarGridProps,
|
||||
CalendarGridRowProps as SomaCalendarGridRowProps,
|
||||
CalendarGridRowSnippetProps,
|
||||
CalendarHeadCellProps as SomaCalendarHeadCellProps,
|
||||
CalendarHeaderProps as SomaCalendarHeaderProps,
|
||||
CalendarHeadingProps as SomaCalendarHeadingProps,
|
||||
CalendarHeadingSnippetProps,
|
||||
CalendarMonthSelectProps as SomaCalendarMonthSelectProps,
|
||||
CalendarMonthSelectSnippetProps,
|
||||
CalendarNextButtonProps as SomaCalendarNextButtonProps,
|
||||
CalendarPrevButtonProps as SomaCalendarPrevButtonProps,
|
||||
CalendarProps as SomaCalendarProps,
|
||||
CalendarProviderSnippetProps,
|
||||
CalendarYearSelectProps as SomaCalendarYearSelectProps,
|
||||
CalendarYearSelectSnippetProps
|
||||
} from '$soma/components/calendar'
|
||||
import type { DateValue } from '$libs/days'
|
||||
import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'
|
||||
|
||||
export type CalendarSize = Extract<Size, 'sm' | 'md' | 'lg'>
|
||||
export type CalendarVariant = ControlVariant
|
||||
export type CalendarColor = ColorRole
|
||||
|
||||
export type CalendarProps = SomaCalendarProps & {
|
||||
/** Visual size for day cells, nav buttons and select controls. @default 'md' */
|
||||
size?: ResponsiveProp<CalendarSize>
|
||||
/** Visual treatment of the calendar surface. @default 'surface' */
|
||||
variant?: CalendarVariant
|
||||
/** Intent palette used for selected days and focus affordances. @default 'primary' */
|
||||
color?: CalendarColor
|
||||
}
|
||||
|
||||
export type CalendarHeaderProps = SomaCalendarHeaderProps
|
||||
export type CalendarHeadingProps = SomaCalendarHeadingProps
|
||||
export type CalendarPrevButtonProps = SomaCalendarPrevButtonProps
|
||||
export type CalendarNextButtonProps = SomaCalendarNextButtonProps
|
||||
export type CalendarMonthSelectProps = SomaCalendarMonthSelectProps
|
||||
export type CalendarYearSelectProps = SomaCalendarYearSelectProps
|
||||
export type CalendarGridProps = SomaCalendarGridProps
|
||||
export type CalendarGridHeadProps = SomaCalendarGridHeadProps
|
||||
export type CalendarGridBodyProps = SomaCalendarGridBodyProps
|
||||
export type CalendarGridRowProps = SomaCalendarGridRowProps & { week?: DateValue[] }
|
||||
export type CalendarHeadCellProps = SomaCalendarHeadCellProps
|
||||
export type CalendarCellProps = SomaCalendarCellProps
|
||||
export type CalendarDayProps = SomaCalendarDayProps
|
||||
|
||||
export type {
|
||||
CalendarProviderSnippetProps,
|
||||
CalendarCellSnippetProps,
|
||||
CalendarDaySnippetProps,
|
||||
CalendarHeadingSnippetProps,
|
||||
CalendarMonthSelectSnippetProps,
|
||||
CalendarYearSelectSnippetProps,
|
||||
CalendarGridRowSnippetProps
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import { semaSelector } from '$uix/morfo'
|
||||
import { calendarMorfo } from '$uix/morfo/components/calendar'
|
||||
import { soundTuning } from '../sounds'
|
||||
import type { Sema } from '../sema-map'
|
||||
|
||||
const onPart = (
|
||||
part:
|
||||
| 'provider'
|
||||
| 'prev-button'
|
||||
| 'next-button'
|
||||
| 'month-select'
|
||||
| 'year-select'
|
||||
| 'day',
|
||||
matchers?: Parameters<typeof semaSelector<typeof calendarMorfo>>[2]
|
||||
) => semaSelector(calendarMorfo, part, matchers)
|
||||
|
||||
export const calendarSema: Sema = {
|
||||
name: 'calendar',
|
||||
cascade: [
|
||||
{
|
||||
selector: onPart('day', { eventName: 'commit-select' }),
|
||||
sound: soundTuning('form.commit.soft'),
|
||||
haptic: { kind: 'tap' }
|
||||
},
|
||||
{
|
||||
selector: onPart('day', { eventName: 'commit-unselect' }),
|
||||
sound: soundTuning('form.commit.subtle'),
|
||||
haptic: { kind: 'tap' }
|
||||
},
|
||||
...(
|
||||
[
|
||||
'provider',
|
||||
'prev-button',
|
||||
'next-button',
|
||||
'month-select',
|
||||
'year-select',
|
||||
'day'
|
||||
] as const
|
||||
).map((part) => ({
|
||||
selector: onPart(part, { eventName: 'shift-navigate' }),
|
||||
sound: soundTuning('form.commit.subtle', {
|
||||
gain: 0.18,
|
||||
contour: 'ascending',
|
||||
pitch: { op: 'add', value: -90 }
|
||||
})
|
||||
}))
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue