From 2cecc0b3b7456e624f44b874c0d58112cefb0b17 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 23 May 2026 19:16:49 +0200 Subject: [PATCH] feat(eidos): port range-calendar from soma + extract shared partial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standalone eidos `` 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 `` and ``'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) --- .../date-range-picker/date-range-picker.css | 298 -------- .../eidos/components/range-calendar/index.ts | 101 +++ .../range-calendar/range-calendar-cell.svelte | 12 + .../range-calendar/range-calendar-day.svelte | 16 + .../range-calendar-grid-body.svelte | 10 + .../range-calendar-grid-head.svelte | 10 + .../range-calendar-grid-row.svelte | 12 + .../range-calendar/range-calendar-grid.svelte | 10 + .../range-calendar-head-cell.svelte | 10 + .../range-calendar-header.svelte | 10 + .../range-calendar-heading.svelte | 16 + .../range-calendar-month-select.svelte | 19 + .../range-calendar-next-button.svelte | 10 + .../range-calendar-prev-button.svelte | 10 + .../range-calendar-year-select.svelte | 19 + .../range-calendar/range-calendar.css | 479 ++++++++++++ .../range-calendar/range-calendar.svelte | 36 + .../eidos/components/range-calendar/types.ts | 83 +++ src/uix/eidos/index.css | 1 + src/uix/morfo/components/range-calendar.ts | 2 +- web/routes/uix/+layout@.svelte | 1 + .../components/range-calendar/+page.svelte | 681 ++++++++++++++++++ 22 files changed, 1547 insertions(+), 299 deletions(-) create mode 100644 src/uix/eidos/components/range-calendar/index.ts create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-cell.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-day.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-grid-body.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-grid-head.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-grid-row.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-grid.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-head-cell.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-header.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-heading.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-month-select.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-next-button.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-prev-button.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar-year-select.svelte create mode 100644 src/uix/eidos/components/range-calendar/range-calendar.css create mode 100644 src/uix/eidos/components/range-calendar/range-calendar.svelte create mode 100644 src/uix/eidos/components/range-calendar/types.ts create mode 100644 web/routes/uix/components/range-calendar/+page.svelte diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker.css b/src/uix/eidos/components/date-range-picker/date-range-picker.css index 5c03b233c..97fd38e03 100644 --- a/src/uix/eidos/components/date-range-picker/date-range-picker.css +++ b/src/uix/eidos/components/date-range-picker/date-range-picker.css @@ -534,304 +534,6 @@ border-color: transparent; } -[data-range-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-range-calendar-header-controls] { - display: flex; - align-items: center; - justify-content: center; - gap: var(--calendar-header-gap); - min-inline-size: 0; -} - -[data-range-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-range-calendar-prev-button], -[data-range-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-range-calendar-prev-button]:hover, -[data-range-calendar-next-button]:hover { - border-color: var(--calendar-control-border-hover); - background: var(--calendar-control-bg-hover); - color: var(--calendar-control-color-hover); -} - -[data-range-calendar-prev-button]:focus-visible, -[data-range-calendar-next-button]:focus-visible, -[data-range-calendar-month-select]:focus-visible, -[data-range-calendar-year-select]:focus-visible, -[data-range-calendar-day]:focus-visible { - outline: none; - box-shadow: var(--calendar-focus-shadow); -} - -[data-range-calendar-prev-button][data-disabled], -[data-range-calendar-next-button][data-disabled], -[data-range-calendar-month-select][data-disabled], -[data-range-calendar-year-select][data-disabled] { - cursor: not-allowed; - opacity: var(--calendar-disabled-opacity); -} - -[data-range-calendar-month-select], -[data-range-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-range-calendar-month-select] { - min-inline-size: var(--calendar-select-min-width); - inline-size: calc(var(--_calendar-day-size) * 2.35); -} - -[data-range-calendar-year-select] { - min-inline-size: var(--calendar-select-min-width); - inline-size: calc(var(--_calendar-day-size) * 2.15); -} - -[data-range-calendar-month-select]:hover, -[data-range-calendar-year-select]:hover { - border-color: var(--calendar-control-border-hover); - background: var(--calendar-control-bg-hover); - color: var(--calendar-control-color-hover); -} - -[data-range-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-range-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-range-calendar-week-number] { - color: var(--calendar-weekday-color); - font-variant-numeric: tabular-nums; -} - -[data-range-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-range-calendar-cell] { - padding: 0; - border-radius: var(--calendar-day-radius); - text-align: center; - vertical-align: middle; -} - -[data-range-calendar-cell][data-in-range], -[data-range-calendar-cell][data-in-preview-range] { - background: var(--_calendar-accent-track); -} - -[data-range-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-range-calendar-day]:hover { - border-color: var(--calendar-day-border-hover); - background: var(--calendar-day-bg-hover); - color: var(--calendar-day-color-hover); -} - -[data-range-calendar-day][data-selected] { - border-color: var(--_calendar-range-end-border); - background: var(--_calendar-range-end-solid); - color: var(--calendar-day-selected-color); -} - -[data-range-calendar-day][data-range-start] { - /* Start endpoint: solid background + small left-edge accent stripe so it - reads as the *opening* of the range, never blending with the end. */ - border-color: var(--_calendar-range-start-border); - background: var(--_calendar-range-start-solid); - color: var(--_calendar-range-start-text); - box-shadow: inset 2px 0 0 0 var(--_calendar-range-start-border); - font-weight: var(--font-weight-semibold); -} - -[data-range-calendar-day][data-range-end] { - /* End endpoint: solid background + small right-edge accent stripe. */ - border-color: var(--_calendar-range-end-border); - background: var(--_calendar-range-end-solid); - color: var(--_calendar-range-end-text); - box-shadow: inset -2px 0 0 0 var(--_calendar-range-end-border); - font-weight: var(--font-weight-semibold); -} - -[data-range-calendar-day][data-range-start][data-range-end] { - border-color: var(--_calendar-range-end-border); - background: linear-gradient( - 135deg, - var(--_calendar-range-start-solid) 0 50%, - var(--_calendar-range-end-solid) 50% 100% - ); - color: var(--calendar-day-selected-color); -} - -[data-range-calendar-day][data-selected]:hover { - border-color: var(--_calendar-range-end-solid-hover); - background: var(--_calendar-range-end-solid-hover); -} - -[data-range-calendar-day][data-range-start]:hover { - border-color: var(--_calendar-range-start-solid-hover); - background: var(--_calendar-range-start-solid-hover); -} - -[data-range-calendar-day][data-range-end]:hover { - border-color: var(--_calendar-range-end-solid-hover); - background: var(--_calendar-range-end-solid-hover); -} - -[data-range-calendar-day][data-range-start][data-range-end]:hover { - background: linear-gradient( - 135deg, - var(--_calendar-range-start-solid-hover) 0 50%, - var(--_calendar-range-end-solid-hover) 50% 100% - ); -} - -[data-range-calendar-day][data-in-range]:not([data-selected]) { - border-color: transparent; - background: transparent; - color: var(--_calendar-accent-text); -} - -[data-range-calendar-day][data-preview-start], -[data-range-calendar-day][data-preview-end], -[data-range-calendar-day][data-in-preview-range]:not([data-selected]) { - border-color: var(--_calendar-accent-border); - background: transparent; - color: var(--_calendar-accent-text); -} - -[data-range-calendar-day][data-focused]:not([data-selected]) { - border-color: var(--_calendar-accent-border); - background: var(--_calendar-accent-track); - color: var(--_calendar-accent-text); -} - -[data-range-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-range-calendar-day][data-outside-month] { - color: var(--calendar-day-outside-color); - opacity: var(--calendar-day-outside-opacity); -} - -[data-range-calendar-day][data-weekend]:not([data-selected]) { - color: var(--calendar-day-weekend-color); -} - -[data-range-calendar-day][data-holiday]:not([data-selected]) { - box-shadow: var(--calendar-day-holiday-shadow); -} - -[data-range-calendar-day][data-unavailable] { - text-decoration-line: line-through; - text-decoration-color: var(--calendar-day-unavailable-decoration); -} - -[data-range-calendar-day][data-disabled], -[data-range-calendar-day][data-unavailable] { - cursor: not-allowed; - opacity: var(--calendar-disabled-opacity); -} - -[data-range-calendar][data-invalid] { - border-color: var(--calendar-invalid-border); -} - -[data-range-calendar][data-disabled] { - opacity: var(--calendar-disabled-opacity); -} - -[data-range-calendar][data-event], -[data-range-calendar-day][data-event], -[data-range-calendar-prev-button][data-event], -[data-range-calendar-next-button][data-event], -[data-range-calendar-month-select][data-event], -[data-range-calendar-year-select][data-event] { - box-shadow: var(--calendar-event-shadow); -} @media (max-width: 42rem) { [data-date-range-field-input] { diff --git a/src/uix/eidos/components/range-calendar/index.ts b/src/uix/eidos/components/range-calendar/index.ts new file mode 100644 index 000000000..6224bdada --- /dev/null +++ b/src/uix/eidos/components/range-calendar/index.ts @@ -0,0 +1,101 @@ +// RangeCalendar — eidos compound API. +// +// import { RangeCalendar } from '$uix/eidos/components/range-calendar'; +// +// +// +// +// +// +// +// +// +// +// {#snippet children({ weekdays })} +// {#each weekdays as wd} +// {wd} +// {/each} +// {/snippet} +// +// +// +// ... +// +// +// +import RangeCalendarComponent from './range-calendar.svelte'; +import Header from './range-calendar-header.svelte'; +import Heading from './range-calendar-heading.svelte'; +import PrevButton from './range-calendar-prev-button.svelte'; +import NextButton from './range-calendar-next-button.svelte'; +import MonthSelect from './range-calendar-month-select.svelte'; +import YearSelect from './range-calendar-year-select.svelte'; +import Grid from './range-calendar-grid.svelte'; +import GridHead from './range-calendar-grid-head.svelte'; +import GridBody from './range-calendar-grid-body.svelte'; +import GridRow from './range-calendar-grid-row.svelte'; +import HeadCell from './range-calendar-head-cell.svelte'; +import Cell from './range-calendar-cell.svelte'; +import Day from './range-calendar-day.svelte'; + +type RangeCalendarNamespace = typeof RangeCalendarComponent & { + 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 RangeCalendar = RangeCalendarComponent as RangeCalendarNamespace; +RangeCalendar.Header = Header; +RangeCalendar.Heading = Heading; +RangeCalendar.PrevButton = PrevButton; +RangeCalendar.NextButton = NextButton; +RangeCalendar.MonthSelect = MonthSelect; +RangeCalendar.YearSelect = YearSelect; +RangeCalendar.Grid = Grid; +RangeCalendar.GridHead = GridHead; +RangeCalendar.GridBody = GridBody; +RangeCalendar.GridRow = GridRow; +RangeCalendar.HeadCell = HeadCell; +RangeCalendar.Cell = Cell; +RangeCalendar.Day = Day; + +export { RangeCalendar }; + +export default RangeCalendar; + +export type { + RangeCalendarProps, + RangeCalendarHeaderProps as HeaderProps, + RangeCalendarHeadingProps as HeadingProps, + RangeCalendarPrevButtonProps as PrevButtonProps, + RangeCalendarNextButtonProps as NextButtonProps, + RangeCalendarMonthSelectProps as MonthSelectProps, + RangeCalendarYearSelectProps as YearSelectProps, + RangeCalendarGridProps as GridProps, + RangeCalendarGridHeadProps as GridHeadProps, + RangeCalendarGridBodyProps as GridBodyProps, + RangeCalendarGridRowProps as GridRowProps, + RangeCalendarHeadCellProps as HeadCellProps, + RangeCalendarCellProps as CellProps, + RangeCalendarDayProps as DayProps, + RangeCalendarProviderSnippetProps, + RangeCalendarCellSnippetProps, + RangeCalendarDaySnippetProps, + RangeCalendarHeadingSnippetProps, + RangeCalendarMonthSelectSnippetProps, + RangeCalendarYearSelectSnippetProps, + RangeCalendarGridRowSnippetProps, + RangeCalendarSize, + RangeCalendarVariant, + RangeCalendarColor +} from './types'; diff --git a/src/uix/eidos/components/range-calendar/range-calendar-cell.svelte b/src/uix/eidos/components/range-calendar/range-calendar-cell.svelte new file mode 100644 index 000000000..bccf931d5 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-cell.svelte @@ -0,0 +1,12 @@ + + + + {#snippet children(snippetProps: RangeCalendarCellSnippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-day.svelte b/src/uix/eidos/components/range-calendar/range-calendar-day.svelte new file mode 100644 index 000000000..4bab97fff --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-day.svelte @@ -0,0 +1,16 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: RangeCalendarDaySnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/range-calendar/range-calendar-grid-body.svelte b/src/uix/eidos/components/range-calendar/range-calendar-grid-body.svelte new file mode 100644 index 000000000..6d71281fa --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-grid-body.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-grid-head.svelte b/src/uix/eidos/components/range-calendar/range-calendar-grid-head.svelte new file mode 100644 index 000000000..a739e908b --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-grid-head.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-grid-row.svelte b/src/uix/eidos/components/range-calendar/range-calendar-grid-row.svelte new file mode 100644 index 000000000..bfa650272 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-grid-row.svelte @@ -0,0 +1,12 @@ + + + + {#snippet children(snippetProps: RangeCalendarGridRowSnippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-grid.svelte b/src/uix/eidos/components/range-calendar/range-calendar-grid.svelte new file mode 100644 index 000000000..1806f5e46 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-grid.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-head-cell.svelte b/src/uix/eidos/components/range-calendar/range-calendar-head-cell.svelte new file mode 100644 index 000000000..145abfc14 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-head-cell.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-header.svelte b/src/uix/eidos/components/range-calendar/range-calendar-header.svelte new file mode 100644 index 000000000..d73e20b50 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-header.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-heading.svelte b/src/uix/eidos/components/range-calendar/range-calendar-heading.svelte new file mode 100644 index 000000000..c20677a66 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-heading.svelte @@ -0,0 +1,16 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: RangeCalendarHeadingSnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/range-calendar/range-calendar-month-select.svelte b/src/uix/eidos/components/range-calendar/range-calendar-month-select.svelte new file mode 100644 index 000000000..127b29879 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-month-select.svelte @@ -0,0 +1,19 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: RangeCalendarMonthSelectSnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/range-calendar/range-calendar-next-button.svelte b/src/uix/eidos/components/range-calendar/range-calendar-next-button.svelte new file mode 100644 index 000000000..0b8832c5c --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-next-button.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-prev-button.svelte b/src/uix/eidos/components/range-calendar/range-calendar-prev-button.svelte new file mode 100644 index 000000000..739abf7c6 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-prev-button.svelte @@ -0,0 +1,10 @@ + + + + {@render bodyContent?.()} + diff --git a/src/uix/eidos/components/range-calendar/range-calendar-year-select.svelte b/src/uix/eidos/components/range-calendar/range-calendar-year-select.svelte new file mode 100644 index 000000000..b9fc19be4 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar-year-select.svelte @@ -0,0 +1,19 @@ + + +{#if bodyContent} + + {#snippet children(snippetProps: RangeCalendarYearSelectSnippetProps)} + {@render bodyContent(snippetProps)} + {/snippet} + +{:else} + +{/if} diff --git a/src/uix/eidos/components/range-calendar/range-calendar.css b/src/uix/eidos/components/range-calendar/range-calendar.css new file mode 100644 index 000000000..bac655ccf --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar.css @@ -0,0 +1,479 @@ +/* + * Range Calendar recipe. + * + * [data-range-calendar] → root container + * [data-range-calendar-header] → heading row + * [data-range-calendar-heading] → month/year label + * [data-range-calendar-prev-button] → previous-month nav + * [data-range-calendar-next-button] → next-month nav + * [data-range-calendar-month-select] → month picker dropdown + * [data-range-calendar-year-select] → year picker dropdown + * [data-range-calendar-grid] → 7-column day grid + * [data-range-calendar-head-cell] → weekday label + * [data-range-calendar-cell] → day-cell container (+ in-range bg) + * [data-range-calendar-day] → individual day button + * + * Range-specific data-attrs (lit by soma): + * - [data-range-start] first endpoint + * - [data-range-end] second endpoint + * - [data-in-range] between endpoints + * - [data-preview-start] hover preview anchor + * - [data-preview-end] hover preview anchor + * - [data-in-preview-range] between hover anchor and pivot + * + * Cross-component reuse: + * This file is consumed BOTH by the standalone eidos `` and + * by `` (which renders the soma RangeCalendar inside a + * popover). Both stamp the same `[data-range-calendar-*]` attrs, so the + * same selectors paint without duplication. + */ + +/* ── Root token scaffolding ──────────────────────────────────────────── */ +[data-range-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); + /* Start endpoint defaults to `affirm` semantic (range opens). End + endpoint follows the `data-color` cascade. */ + --_calendar-range-start-solid: var(--color-affirm-solid); + --_calendar-range-start-solid-hover: var(--color-affirm-solid-hover); + --_calendar-range-start-border: var(--color-affirm-border); + --_calendar-range-start-text: var(--color-affirm-contrast); + --_calendar-range-end-solid: var(--_calendar-accent-solid); + --_calendar-range-end-solid-hover: var(--_calendar-accent-solid-hover); + --_calendar-range-end-border: var(--_calendar-accent-solid); + --_calendar-range-end-text: var(--calendar-day-selected-color); + + 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-range-calendar][data-size='xs'] { + --_calendar-padding: var(--calendar-padding-xs); + --_calendar-control-size: var(--calendar-control-size-xs); + --_calendar-day-size: var(--calendar-day-size-xs); + --_calendar-font-size: var(--calendar-font-size-xs); +} + +[data-range-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-range-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-range-calendar][data-variant='outline'] { + background: var(--calendar-outline-bg); + box-shadow: var(--calendar-outline-shadow); +} + +[data-range-calendar][data-variant='ghost'] { + padding: 0; + border-color: transparent; + background: transparent; + box-shadow: none; +} + +/* ── Color cascades ─────────────────────────────────────────────────── */ +[data-range-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-range-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-range-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); + /* When the end accent IS `affirm`, swap the start endpoint to + secondary so the two endpoints stay visually distinct. */ + --_calendar-range-start-solid: var(--color-secondary-solid); + --_calendar-range-start-solid-hover: var(--color-secondary-solid-hover); + --_calendar-range-start-border: var(--color-secondary-border); + --_calendar-range-start-text: var(--color-secondary-contrast); +} + +[data-range-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); + --_calendar-range-start-solid: var(--color-secondary-solid); + --_calendar-range-start-solid-hover: var(--color-secondary-solid-hover); + --_calendar-range-start-border: var(--color-secondary-border); + --_calendar-range-start-text: var(--color-secondary-contrast); +} + +[data-range-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-range-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-range-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); +} + +/* ── Header + nav controls ──────────────────────────────────────────── */ +[data-range-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-range-calendar-header-controls] { + display: flex; + align-items: center; + justify-content: center; + gap: var(--calendar-header-gap); + min-inline-size: 0; +} + +[data-range-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-range-calendar-prev-button], +[data-range-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-range-calendar-prev-button]:hover, +[data-range-calendar-next-button]:hover { + border-color: var(--calendar-control-border-hover); + background: var(--calendar-control-bg-hover); + color: var(--calendar-control-color-hover); +} + +[data-range-calendar-prev-button]:focus-visible, +[data-range-calendar-next-button]:focus-visible, +[data-range-calendar-month-select]:focus-visible, +[data-range-calendar-year-select]:focus-visible, +[data-range-calendar-day]:focus-visible { + outline: none; + box-shadow: var(--calendar-focus-shadow); +} + +[data-range-calendar-prev-button][data-disabled], +[data-range-calendar-next-button][data-disabled], +[data-range-calendar-month-select][data-disabled], +[data-range-calendar-year-select][data-disabled] { + cursor: not-allowed; + opacity: var(--calendar-disabled-opacity); +} + +[data-range-calendar-month-select], +[data-range-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-range-calendar-month-select] { + min-inline-size: var(--calendar-select-min-width); + inline-size: calc(var(--_calendar-day-size) * 2.35); +} + +[data-range-calendar-year-select] { + min-inline-size: var(--calendar-select-min-width); + inline-size: calc(var(--_calendar-day-size) * 2.15); +} + +[data-range-calendar-month-select]:hover, +[data-range-calendar-year-select]:hover { + border-color: var(--calendar-control-border-hover); + background: var(--calendar-control-bg-hover); + color: var(--calendar-control-color-hover); +} + +/* ── Grid ───────────────────────────────────────────────────────────── */ +[data-range-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-range-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-range-calendar-week-number] { + color: var(--calendar-weekday-color); + font-variant-numeric: tabular-nums; +} + +[data-range-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-range-calendar-cell] { + padding: 0; + border-radius: var(--calendar-day-radius); + text-align: center; + vertical-align: middle; +} + +[data-range-calendar-cell][data-in-range], +[data-range-calendar-cell][data-in-preview-range] { + background: var(--_calendar-accent-track); +} + +/* ── Day cells + range-specific states ──────────────────────────────── */ +[data-range-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-range-calendar-day]:hover { + border-color: var(--calendar-day-border-hover); + background: var(--calendar-day-bg-hover); + color: var(--calendar-day-color-hover); +} + +[data-range-calendar-day][data-selected] { + border-color: var(--_calendar-range-end-border); + background: var(--_calendar-range-end-solid); + color: var(--calendar-day-selected-color); +} + +[data-range-calendar-day][data-range-start] { + /* Start endpoint: solid background + left-edge accent stripe so it reads + as the *opening* of the range, never blending with the end. */ + border-color: var(--_calendar-range-start-border); + background: var(--_calendar-range-start-solid); + color: var(--_calendar-range-start-text); + box-shadow: inset 2px 0 0 0 var(--_calendar-range-start-border); + font-weight: var(--font-weight-semibold); +} + +[data-range-calendar-day][data-range-end] { + /* End endpoint: solid background + right-edge accent stripe. */ + border-color: var(--_calendar-range-end-border); + background: var(--_calendar-range-end-solid); + color: var(--_calendar-range-end-text); + box-shadow: inset -2px 0 0 0 var(--_calendar-range-end-border); + font-weight: var(--font-weight-semibold); +} + +[data-range-calendar-day][data-range-start][data-range-end] { + border-color: var(--_calendar-range-end-border); + background: linear-gradient( + 135deg, + var(--_calendar-range-start-solid) 0 50%, + var(--_calendar-range-end-solid) 50% 100% + ); + color: var(--calendar-day-selected-color); +} + +[data-range-calendar-day][data-selected]:hover { + border-color: var(--_calendar-range-end-solid-hover); + background: var(--_calendar-range-end-solid-hover); +} + +[data-range-calendar-day][data-range-start]:hover { + border-color: var(--_calendar-range-start-solid-hover); + background: var(--_calendar-range-start-solid-hover); +} + +[data-range-calendar-day][data-range-end]:hover { + border-color: var(--_calendar-range-end-solid-hover); + background: var(--_calendar-range-end-solid-hover); +} + +[data-range-calendar-day][data-range-start][data-range-end]:hover { + background: linear-gradient( + 135deg, + var(--_calendar-range-start-solid-hover) 0 50%, + var(--_calendar-range-end-solid-hover) 50% 100% + ); +} + +[data-range-calendar-day][data-in-range]:not([data-selected]) { + border-color: transparent; + background: transparent; + color: var(--_calendar-accent-text); +} + +[data-range-calendar-day][data-preview-start], +[data-range-calendar-day][data-preview-end], +[data-range-calendar-day][data-in-preview-range]:not([data-selected]) { + border-color: var(--_calendar-accent-border); + background: transparent; + color: var(--_calendar-accent-text); +} + +[data-range-calendar-day][data-focused]:not([data-selected]) { + border-color: var(--_calendar-accent-border); + background: var(--_calendar-accent-track); + color: var(--_calendar-accent-text); +} + +[data-range-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-range-calendar-day][data-outside-month] { + color: var(--calendar-day-outside-color); + opacity: var(--calendar-day-outside-opacity); +} + +[data-range-calendar-day][data-weekend]:not([data-selected]) { + color: var(--calendar-day-weekend-color); +} + +[data-range-calendar-day][data-holiday]:not([data-selected]) { + box-shadow: var(--calendar-day-holiday-shadow); +} + +[data-range-calendar-day][data-unavailable] { + text-decoration-line: line-through; + text-decoration-color: var(--calendar-day-unavailable-decoration); +} + +[data-range-calendar-day][data-disabled], +[data-range-calendar-day][data-unavailable] { + cursor: not-allowed; + opacity: var(--calendar-disabled-opacity); +} + +[data-range-calendar][data-invalid] { + border-color: var(--calendar-invalid-border); +} + +[data-range-calendar][data-disabled] { + opacity: var(--calendar-disabled-opacity); +} + +[data-range-calendar][data-event], +[data-range-calendar-day][data-event], +[data-range-calendar-prev-button][data-event], +[data-range-calendar-next-button][data-event], +[data-range-calendar-month-select][data-event], +[data-range-calendar-year-select][data-event] { + box-shadow: var(--calendar-event-shadow); +} + +@media (prefers-reduced-motion: reduce) { + [data-range-calendar-prev-button], + [data-range-calendar-next-button], + [data-range-calendar-month-select], + [data-range-calendar-year-select], + [data-range-calendar-day] { + transition: none; + } +} diff --git a/src/uix/eidos/components/range-calendar/range-calendar.svelte b/src/uix/eidos/components/range-calendar/range-calendar.svelte new file mode 100644 index 000000000..78979bebf --- /dev/null +++ b/src/uix/eidos/components/range-calendar/range-calendar.svelte @@ -0,0 +1,36 @@ + + + + {#snippet children(snippetProps: RangeCalendarProviderSnippetProps)} + {@render bodyContent?.(snippetProps)} + {/snippet} + diff --git a/src/uix/eidos/components/range-calendar/types.ts b/src/uix/eidos/components/range-calendar/types.ts new file mode 100644 index 000000000..b2898e8b2 --- /dev/null +++ b/src/uix/eidos/components/range-calendar/types.ts @@ -0,0 +1,83 @@ +import type { + RangeCalendarCellProps as SomaRangeCalendarCellProps, + RangeCalendarCellSnippetProps, + RangeCalendarDayProps as SomaRangeCalendarDayProps, + RangeCalendarDaySnippetProps, + RangeCalendarGridBodyProps as SomaRangeCalendarGridBodyProps, + RangeCalendarGridHeadProps as SomaRangeCalendarGridHeadProps, + RangeCalendarGridProps as SomaRangeCalendarGridProps, + RangeCalendarGridRowProps as SomaRangeCalendarGridRowProps, + RangeCalendarGridRowSnippetProps, + RangeCalendarHeadCellProps as SomaRangeCalendarHeadCellProps, + RangeCalendarHeaderProps as SomaRangeCalendarHeaderProps, + RangeCalendarHeadingProps as SomaRangeCalendarHeadingProps, + RangeCalendarHeadingSnippetProps, + RangeCalendarMonthSelectProps as SomaRangeCalendarMonthSelectProps, + RangeCalendarMonthSelectSnippetProps, + RangeCalendarNextButtonProps as SomaRangeCalendarNextButtonProps, + RangeCalendarPrevButtonProps as SomaRangeCalendarPrevButtonProps, + RangeCalendarProps as SomaRangeCalendarProps, + RangeCalendarProviderSnippetProps, + RangeCalendarYearSelectProps as SomaRangeCalendarYearSelectProps, + RangeCalendarYearSelectSnippetProps +} from '$soma/components/range-calendar'; +import type { DateValue } from '$libs/days'; +import type { ColorRole, ControlVariant, ResponsiveProp, Size } from '$uix/eidos/lib/types'; + +/** + * RangeCalendar visual size — four-step subset of the canonical scale, + * mirrors `CalendarSize` so RangeCalendar reads identically to Calendar + * for the visual consumer. + */ +export type RangeCalendarSize = Extract; + +/** + * Visual treatment of the range-calendar surface. Mirrors `CalendarVariant`. + * + * - `surface` (default) — bordered card chrome with default background. + * - `outline` — bordered surface with the outline-variant background token. + * - `ghost` — no chrome, transparent surface (used when consumed inside + * a picker popover that already paints its own chrome). + */ +export type RangeCalendarVariant = ControlVariant; + +/** + * Color cascade. End endpoint follows this accent; start endpoint defaults + * to the `affirm` family (range opens), except when the end accent itself + * is `affirm` or `fulfill` — those swap the start to `secondary` so the + * two endpoints stay visually distinct (see range-calendar.css). + */ +export type RangeCalendarColor = ColorRole; + +export type RangeCalendarProps = SomaRangeCalendarProps & { + /** Visual size for day cells, nav buttons and select controls. @default 'md' */ + size?: ResponsiveProp; + /** Visual treatment of the calendar surface. @default 'surface' */ + variant?: RangeCalendarVariant; + /** Intent palette used for end endpoint, in-range fill and focus affordances. @default 'primary' */ + color?: RangeCalendarColor; +}; + +export type RangeCalendarHeaderProps = SomaRangeCalendarHeaderProps; +export type RangeCalendarHeadingProps = SomaRangeCalendarHeadingProps; +export type RangeCalendarPrevButtonProps = SomaRangeCalendarPrevButtonProps; +export type RangeCalendarNextButtonProps = SomaRangeCalendarNextButtonProps; +export type RangeCalendarMonthSelectProps = SomaRangeCalendarMonthSelectProps; +export type RangeCalendarYearSelectProps = SomaRangeCalendarYearSelectProps; +export type RangeCalendarGridProps = SomaRangeCalendarGridProps; +export type RangeCalendarGridHeadProps = SomaRangeCalendarGridHeadProps; +export type RangeCalendarGridBodyProps = SomaRangeCalendarGridBodyProps; +export type RangeCalendarGridRowProps = SomaRangeCalendarGridRowProps & { week?: DateValue[] }; +export type RangeCalendarHeadCellProps = SomaRangeCalendarHeadCellProps; +export type RangeCalendarCellProps = SomaRangeCalendarCellProps; +export type RangeCalendarDayProps = SomaRangeCalendarDayProps; + +export type { + RangeCalendarProviderSnippetProps, + RangeCalendarCellSnippetProps, + RangeCalendarDaySnippetProps, + RangeCalendarHeadingSnippetProps, + RangeCalendarMonthSelectSnippetProps, + RangeCalendarYearSelectSnippetProps, + RangeCalendarGridRowSnippetProps +}; diff --git a/src/uix/eidos/index.css b/src/uix/eidos/index.css index 6df8584dc..9f9424406 100644 --- a/src/uix/eidos/index.css +++ b/src/uix/eidos/index.css @@ -130,6 +130,7 @@ @import './components/select/select.css'; @import './components/combobox/combobox.css'; @import './components/calendar/calendar.css'; +@import './components/range-calendar/range-calendar.css'; @import './components/month-grid/month-grid.css'; @import './components/year-grid/year-grid.css'; @import './components/date-field/date-field.css'; diff --git a/src/uix/morfo/components/range-calendar.ts b/src/uix/morfo/components/range-calendar.ts index 17d8a2109..2a1dcbb88 100644 --- a/src/uix/morfo/components/range-calendar.ts +++ b/src/uix/morfo/components/range-calendar.ts @@ -4,7 +4,7 @@ import { v } from '../types'; export const rangeCalendarMorfo = { name: 'RangeCalendar', kebab: 'range-calendar', - scope: ['soma', 'sema'], + scope: ['soma', 'sema', 'eidos'], apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/grid/', texts: { label: '#?components.range-calendar.label|Range calendar', diff --git a/web/routes/uix/+layout@.svelte b/web/routes/uix/+layout@.svelte index a7821ce53..27c32a3e3 100644 --- a/web/routes/uix/+layout@.svelte +++ b/web/routes/uix/+layout@.svelte @@ -151,6 +151,7 @@ { slug: '/uix/components/listbox', label: 'Listbox' }, { slug: '/uix/components/grid-list', label: 'Grid list' }, { slug: '/uix/components/calendar', label: 'Calendar' }, + { slug: '/uix/components/range-calendar', label: 'Range calendar' }, { slug: '/uix/components/month-grid', label: 'Month grid' }, { slug: '/uix/components/year-grid', label: 'Year grid' }, { slug: '/uix/components/date-field', label: 'Date field' }, diff --git a/web/routes/uix/components/range-calendar/+page.svelte b/web/routes/uix/components/range-calendar/+page.svelte new file mode 100644 index 000000000..d23e6624e --- /dev/null +++ b/web/routes/uix/components/range-calendar/+page.svelte @@ -0,0 +1,681 @@ + + +
+
+
Forms · RangeCalendar
+

Range calendar

+

+ Calendar primitive for picking a date range. Soma owns the state + machine (start → preview → end), the keyboard nav, the min/max + constraints (minValue, maxValue, + minDays, maxDays) and the hover-preview + signaling. Eidos paints the cells with distinct + data-range-start / data-range-end / + data-in-range / data-in-preview-range + treatments — the shared partial at + range-calendar.css is consumed by both this standalone + component and by DateRangePicker's popover content. +

+
+ + parts{compiled.parts.order.length} + + + events{events.length} + + + sizes4 + + + apg + grid ↗ + + + range {rangeLabel} + +
+
+ +
+
+ + {#snippet children({ months, weekdays }: RangeCalendarProviderSnippetProps)} + + ‹ + + › + + {#each months as month (month.value.toString())} + + + + {#snippet children(_props: RangeCalendarGridRowSnippetProps)} + {#each weekdays as wd (wd)} + {wd} + {/each} + {/snippet} + + + + {#each month.weeks as week (week[0].toString())} + + {#snippet children(_props: RangeCalendarGridRowSnippetProps)} + {#each week as date (date.toString())} + + + + {/each} + {/snippet} + + {/each} + + + {/each} + {/snippet} + +
+
+ trace + {#if trace.length === 0} + click a day to start; hover to preview; click second to commit + {:else} + {#each trace.slice(0, 3) as entry (entry.at)} + {entry.event} · {entry.family}{entry.intent ? ' · ' + entry.intent : ''} + {fmtTime(entry.at)} + {/each} + {/if} + + range {rangeLabel} + +
+
+ +
+ + + + + + +
+ + {#if tab === 'live'} +
+

Controls

+

+ Soma owns the range state machine, the keyboard navigation, the + min/max constraints and the hover-preview signaling. Eidos adds + variant/size/color cascades. +

+ +
+ soma range constraints +
+
+ + + + + + +
+ +
+ soma grid layout +
+
+ + + + + + + + +
+ +
+ eidos visual props +
+
+ + + +
+ +
+
+ eidos + composition · with bind:value + svelte +
+
{eidosSnippet}
+
+
+ {/if} + + {#if tab === 'api'} +
+

API reference

+

14 parts: Provider, Header, Heading, PrevButton, NextButton, MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell, Cell, Day.

+ +
Provider (RangeCalendar)
+
+ + + + + + + + + + + + + + + + + + + + + +
PropTypeDefaultDescription
value bindableDateRange{`{}`}Committed range.
placeholder bindableDateValuetodayMonth currently visible.
onValueChange(r: DateRange) => void—Fires when end is committed.
minValue · maxValueDateValue—Allowed bounds.
minDays · maxDaysnumber—Inclusive length constraints (days).
allowSingleDaybooleantruePermit start === end.
preventDeselectbooleanfalseClicking the selected endpoint keeps the range.
numberOfMonthsnumber1Visible months.
pagedNavigationbooleanfalsePrev/Next advance by numberOfMonths.
fixedWeeksbooleanfalseAlways render 6 rows.
disableDaysOutsideMonthbooleanfalseSkip non-current-month cells from selection.
disabled · readonlybooleanfalseOR-merged with Field.Provider.
isDateDisabled · isDateUnavailable · isDateHolidayDateMatcher—Per-cell decorators.
weekStartsOn · weekdayFormat · locale · dir—localeLocalization.
size eidos'xs' | 'sm' | 'md' | 'lg''md'Day cells + nav buttons + selects.
variant eidos'surface' | 'outline' | 'ghost''surface'Calendar chrome.
color eidosColorRole'primary'End-endpoint + in-range fill + focus accent.
+
+ +
Sub-parts
+

+ The sub-parts (Header, Heading, + PrevButton, NextButton, + MonthSelect, YearSelect, + Grid, GridHead, GridBody, + GridRow, HeadCell, Cell, + Day) 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. +

+
+ {/if} + + {#if tab === 'morfo'} +
+

+ morfo + · declarative contract +

+

Source: src/uix/morfo/components/range-calendar.ts.

+ +
+ + + + + + + + + + +
FieldValue
name"{rangeCalendarMorfo.name}"
kebab"{rangeCalendarMorfo.kebab}"
apg{rangeCalendarMorfo.apg}
scope{rangeCalendarMorfo.scope.join(', ')}
parts.length{rangeCalendarMorfo.parts.length}
events.length{events.length}
+
+ +
Parts
+
+ + + + {#each partsList as part (part.kebab)} + + + + + + + + + + {/each} + +
PartMarkerElementRoleArchetypeStatesOptional
{part.kebab}[{part.marker}]<{part.defaultElement}>{part.role ?? '—'}{part.archetype ?? '—'}{part.states.length ? part.states.join(' | ') : '—'}{part.optional ? 'yes' : 'no'}
+
+ +
Events declaration
+
+ + + + {#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 ?? '?'}` : '—'} + + + + + + + + + {/each} + +
namefamilyverbsequenceintenttarget
{action.name}{sem.family}{sem.verb ?? '—'}{sem.sequence ?? 'pre'}{intentStr}{action.target}
+
+
+ {/if} + + {#if tab === 'sema'} +
+

+ sema + · events +

+
+ + + + {#each events as action (action.name)} + {@const intentDecl = 'intent' in action.semantic ? action.semantic.intent : undefined} + {@const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined} + + + + + + + + + {/each} + +
NameFamilyVerbSequenceIntentPlay
{action.name}{action.semantic.family}{action.semantic.verb ?? '—'}{action.semantic.sequence ?? 'pre'}{effectiveIntent ?? '—'} + +
+
+
+ {/if} + + {#if tab === 'recipe'} +
+

Eidos recipe

+

+ Selectors at src/uix/eidos/components/range-calendar/range-calendar.css. + This file is consumed BOTH by the standalone eidos + <RangeCalendar> and by + <DateRangePicker>'s popover content — the + same [data-range-calendar-*] attrs land in both + environments. The picker recipe only adds scoping overrides + (no chrome, max-content sizing, transparent background). +

+
+ + + + + + + + + + + + + + + + +
SelectorSourceWhat it paints
[data-range-calendar]eidosRoot chrome — border + bg + padding + token scaffolding.
[data-range-calendar][data-size]eidosDay-cell + control-button size scale (xs/sm/md/lg).
[data-range-calendar][data-variant]eidosSurface / outline / ghost chrome.
[data-range-calendar][data-color]eidosEnd-endpoint + in-range + focus accent tokens.
[data-range-calendar-day][data-range-start]eidosStart endpoint: affirm-family solid + left-edge stripe + semibold.
[data-range-calendar-day][data-range-end]eidosEnd endpoint: accent-family solid + right-edge stripe.
[data-range-calendar-day][data-range-start][data-range-end]eidosSingle-day range: diagonal-split gradient (start ↘ end).
[data-range-calendar-cell][data-in-range]eidosIn-range cells get accent-track background fill on the cell.
[data-range-calendar-day][data-in-preview-range]eidosHover-preview state while user is choosing end (transparent + accent border).
[data-range-calendar-day][data-today]eidosToday marker (border ring + semibold).
[data-range-calendar-day][data-outside-month]eidosOutside-month dimming.
[data-range-calendar-day][data-disabled] · [data-unavailable]eidosnot-allowed cursor + reduced opacity (+ line-through for unavailable).
+
+
+ {/if} + + {#if tab === 'a11y'} +
+

Accessibility

+

+ Follows the APG grid pattern + with localized labels for range-start / range-end / in-range + announced via aria-label additions on the relevant + day cells. +

+ +
Keyboard
+
+ + + + + + + + + + + +
KeyAction
ArrowLeft / ArrowRightMove focus ±1 day.
ArrowUp / ArrowDownMove focus ±1 week.
PageUp / PageDownMove focus ±1 month.
Shift+PageUp / Shift+PageDownMove focus ±1 year.
Home / EndFirst / last day of the visible week.
Enter / SpaceCommit start (first press) or end (second press).
EscapeCancel the in-progress range; revert to last committed.
+
+ +
ARIA contract
+
+ + + + + + + + + + +
PartAttributeValue
providerrole"application"
provideraria-labellocalized "Range calendar"
gridrole"grid"
cellrole · aria-selected"gridcell" / "true|false"
dayaria-label"date — range start | range end | in range"
prev-button · next-buttonaria-label"Previous month" / "Next month"
+
+
+ {/if} +