feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< script lang = "ts" >
import {
RangeCalendar,
type RangeCalendarColor,
type RangeCalendarGridRowSnippetProps,
type RangeCalendarProviderSnippetProps,
type RangeCalendarSize,
type RangeCalendarVariant
} from '$uix/eidos/components/range-calendar';
import { compileMorfo } from '$uix/morfo';
import { rangeCalendarMorfo } from '@/uix/morfo/components/range-calendar';
import { getActiveUix } from '$active-uix';
import { CalendarDate , type DateRange , type DateValue , type WeekStartsOn } from '$libs/days';
type Tab = 'live' | 'api' | 'morfo' | 'sema' | 'recipe' | 'a11y';
type TraceEntry = { event : string ; family : string ; intent? : string ; at : number } ;
type WeekStartControl = 'locale' | '0' | '1';
const uix = getActiveUix();
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
// Fixed anchor dates so the demo is deterministic across reloads. Same
// pattern as the date-range-picker demo (which this view mirrors), with
// presets letting consumers see the picker's behaviour under common
// real-world configurations.
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const initialMonth = new CalendarDate(2026, 5, 1);
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const sprintStart = new CalendarDate(2026, 5, 8);
const sprintEnd = new CalendarDate(2026, 5, 19);
const defaultMin = new CalendarDate(2026, 5, 1);
const defaultMax = new CalendarDate(2026, 7, 31);
const invalidMax = new CalendarDate(2026, 4, 30);
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let tab = $state< Tab > ('live');
let trace = $state< TraceEntry [ ] > ([]);
let stageRef = $state< HTMLElement | null > (null);
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let value = $state< DateRange > ({ start : sprintStart , end : sprintEnd } );
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let placeholder = $state< DateValue > (initialMonth);
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
// Bounds + span constraints — text inputs are the cleanest UX (no
// hand-rolled "today | custom" modes). Each text becomes a DateValue
// via parseDateInput; presets reset both at once.
let minInput = $state(toDateInput(defaultMin));
let maxInput = $state(toDateInput(defaultMax));
let minDaysInput = $state('');
let maxDaysInput = $state('14');
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
let allowSingleDay = $state(true);
let preventDeselect = $state(false);
let pagedNavigation = $state(false);
let fixedWeeks = $state(true);
let numberOfMonths = $state(1);
let showWeekNumbers = $state(false);
let disableDaysOutsideMonth = $state(false);
let disabled = $state(false);
let readonly = $state(false);
let weekStartsOn = $state< WeekStartControl > ('1');
let locale = $state('en-US');
let size = $state< RangeCalendarSize > ('md');
let variant = $state< RangeCalendarVariant > ('surface');
let color = $state< RangeCalendarColor > ('primary');
const compiled = compileMorfo(rangeCalendarMorfo);
const partsList = $derived([...compiled.parts.byKebab.values()]);
const events = $derived([...compiled.actions.byName.values()]);
const resolvedWeekStartsOn = $derived(
weekStartsOn === 'locale' ? undefined : (Number(weekStartsOn) as WeekStartsOn)
);
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const selectionMinValue = $derived(parseDateInput(minInput));
const selectionMaxValue = $derived(parseDateInput(maxInput));
const minDays = $derived(parseOptionalInt(minDaysInput));
const maxDays = $derived(parseOptionalInt(maxDaysInput));
function toDateInput(date: DateValue): string {
return [
String(date.year).padStart(4, '0'),
String(date.month).padStart(2, '0'),
String(date.day).padStart(2, '0')
].join('-');
}
function parseDateInput(text: string): DateValue | undefined {
if (!text) return undefined;
const [year, month, day] = text.split('-').map(Number);
if (!year || !month || !day) return undefined;
return new CalendarDate(year, month, day);
}
function parseOptionalInt(text: string): number | undefined {
if (!text) return undefined;
const value = Number.parseInt(text, 10);
return Number.isFinite(value) & & value > 0 ? value : undefined;
}
function setBoundsPreset(preset: 'none' | 'min' | 'max' | 'window' | 'invalid'): void {
if (preset === 'none') {
minInput = '';
maxInput = '';
} else if (preset === 'min') {
minInput = toDateInput(defaultMin);
maxInput = '';
} else if (preset === 'max') {
minInput = '';
maxInput = toDateInput(defaultMax);
} else if (preset === 'window') {
minInput = toDateInput(defaultMin);
maxInput = toDateInput(defaultMax);
} else if (preset === 'invalid') {
minInput = toDateInput(defaultMin);
maxInput = toDateInput(invalidMax);
}
}
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
const rangeLabel = $derived.by(() => {
if (!value.start & & !value.end) return 'empty';
if (value.start && !value.end) return `${ value . start . toString ()} → …`;
if (value.start && value.end) return `${ value . start . toString ()} → ${ value . end . toString ()} `;
return '—';
});
$effect(() => {
const el = stageRef;
if (!el) return;
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.attributeName !== 'data-event') continue;
const target = m.target as Element;
const ev = target.getAttribute('data-event');
if (!ev) continue;
trace = [
{
event: ev,
family: target.getAttribute('data-event-family') ?? '—',
intent: target.getAttribute('data-event-intent') ?? undefined,
at: Date.now()
},
...trace
].slice(0, 6);
}
});
obs.observe(el, { attributes : true , subtree : true , attributeFilter : [ 'data-event' ] } );
return () => obs.disconnect();
});
function fmtTime(at: number): string {
const d = new Date(at);
return `${ String ( d . getSeconds ()). padStart ( 2 , '0' )} .${ String ( d . getMilliseconds ()). padStart ( 3 , '0' )} `;
}
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
type EmitSignal = Parameters< NonNullable < typeof uix . events > ['emit']>[0];
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
function emitSelectStart(signal: EmitSignal) {
void uix.events?.emit(signal);
}
const eidosSnippet = $derived(
[
"< script lang = 'ts' > " ,
" import { RangeCalendar } from '$uix/eidos/components/range-calendar';",
" import type { DateRange } from '$libs/days';",
' let value = $state< DateRange > ({ start , end } );',
'< /' + 'script>',
'',
'< RangeCalendar ' ,
' bind:value',
size !== 'md' && ` size="${ size } "`,
variant !== 'surface' && ` variant="${ variant } "`,
color !== 'primary' && ` color="${ color } "`,
numberOfMonths !== 1 && ` numberOfMonths={ $ { numberOfMonths }} `,
minDays !== undefined && ` minDays={ $ { minDays }} `,
maxDays !== undefined && ` maxDays={ $ { maxDays }} `,
!allowSingleDay && ' allowSingleDay={ false } ',
preventDeselect & & ' preventDeselect',
pagedNavigation & & ' pagedNavigation',
!fixedWeeks && ' fixedWeeks={ false } ',
showWeekNumbers & & ' showWeekNumbers',
disableDaysOutsideMonth & & ' disableDaysOutsideMonth',
disabled & & ' disabled',
readonly & & ' readonly',
'>',
' < RangeCalendar.Header > ',
' < RangeCalendar.PrevButton / > ',
' < RangeCalendar.Heading / > ',
' < RangeCalendar.NextButton / > ',
' < / RangeCalendar.Header > ',
' < RangeCalendar.Grid > ',
' < RangeCalendar.GridHead > ',
' < RangeCalendar.GridRow > ',
' { # snippet children ({ weekdays })} ',
' { #each weekdays as wd } < RangeCalendar.HeadCell > { wd } </ RangeCalendar.HeadCell > { /each } ',
' { /snippet } ',
' < / RangeCalendar.GridRow > ',
' < / RangeCalendar.GridHead > ',
' < RangeCalendar.GridBody > ',
' <!-- rows iterate via the months snippet prop --> ',
' < / RangeCalendar.GridBody > ',
' < / RangeCalendar.Grid > ',
'< / RangeCalendar > '
]
.filter(Boolean)
.join('\n')
);
function setNumberOfMonths(n: number) {
numberOfMonths = n;
}
< / script >
< div data-uix-canvas-inner >
< header >
< div data-uix-eyebrow > Forms · RangeCalendar< / div >
< h1 data-uix-page-title > Range calendar< / h1 >
< p data-uix-page-lede >
Calendar primitive for picking a date range. Soma owns the state
machine (start → preview → end), the keyboard nav, the min/max
constraints (< code > minValue< / code > , < code > maxValue< / code > ,
< code > minDays< / code > , < code > maxDays< / code > ) and the hover-preview
signaling. Eidos paints the cells with distinct
< code > data-range-start< / code > / < code > data-range-end< / code > /
< code > data-in-range< / code > / < code > data-in-preview-range< / code >
treatments — the shared partial at
< code > range-calendar.css< / code > is consumed by both this standalone
component and by < code > DateRangePicker< / code > 's popover content.
< / p >
< div data-uix-page-meta >
< span data-uix-meta-pill >
< span data-uix-meta-key > parts</ span > { compiled . parts . order . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > events</ span > { events . length }
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > sizes< / span > 4
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > apg< / span >
< a href = { rangeCalendarMorfo . apg } target="_blank" rel = "noreferrer" > grid ↗</ a >
< / span >
< span data-uix-meta-pill >
< span data-uix-meta-key > range</ span > { rangeLabel }
< / span >
< / div >
< / header >
< div data-uix-stage >
< div data-uix-stage-area bind:this = { stageRef } style="padding: var ( --uix-space-3 ); display: flex ; justify-content: center ;" >
< RangeCalendar
bind:value
placeholder={ placeholder as never }
{ minDays }
{ maxDays }
{ allowSingleDay }
{ preventDeselect }
{ pagedNavigation }
{ fixedWeeks }
{ numberOfMonths }
{ showWeekNumbers }
{ disableDaysOutsideMonth }
{ disabled }
{ readonly }
minValue={ selectionMinValue }
maxValue={ selectionMaxValue }
weekStartsOn={ resolvedWeekStartsOn }
{ locale }
{ size }
{ variant }
{ color }
>
{ # snippet children ({ months , weekdays } : RangeCalendarProviderSnippetProps )}
< RangeCalendar.Header >
< RangeCalendar.PrevButton aria-label = "Previous month" > ‹ < / RangeCalendar.PrevButton >
< RangeCalendar.Heading / >
< RangeCalendar.NextButton aria-label = "Next month" > › < / RangeCalendar.NextButton >
< / RangeCalendar.Header >
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
<!-- Multi - month layout: grids sit side - by - side horizontally via
`grid-auto-flow: column` (mirrors the DateRangePicker demo).
When numberOfMonths=1 this collapses to a single grid track. -->
< div class = "range-months" data-month-count = { months . length } >
{ #each months as month ( month . value . toString ())}
< div class = "range-month-panel" >
< RangeCalendar.Grid >
< RangeCalendar.GridHead >
< RangeCalendar.GridRow >
{ # snippet children ( _props : RangeCalendarGridRowSnippetProps )}
{ #each weekdays as wd , i ( i )}
< RangeCalendar.HeadCell > { wd } </ RangeCalendar.HeadCell >
{ /each }
{ /snippet }
< / RangeCalendar.GridRow >
< / RangeCalendar.GridHead >
< RangeCalendar.GridBody >
{ #each month . weeks as week ( week [ 0 ]. toString ())}
< RangeCalendar.GridRow week = { week } >
{ # snippet children ( _props : RangeCalendarGridRowSnippetProps )}
{ #each week as date ( date . toString ())}
< RangeCalendar.Cell { date } month = { month . value } >
< RangeCalendar.Day / >
< / RangeCalendar.Cell >
{ /each }
{ /snippet }
< / RangeCalendar.GridRow >
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ /each }
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< / RangeCalendar.GridBody >
< / RangeCalendar.Grid >
< / div >
{ /each }
< / div >
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
{ /snippet }
< / RangeCalendar >
< / div >
< div data-uix-stage-trace >
< span data-uix-stage-trace-key > trace< / span >
{ #if trace . length === 0 }
< span > click a day to start; hover to preview; click second to commit< / span >
{ : else }
{ #each trace . slice ( 0 , 3 ) as entry ( entry . at )}
< span >< span data-uix-stage-trace-event > { entry . event } </ span > · { entry . family }{ entry . intent ? ' · ' + entry . intent : '' } </ span >
< span style = "color: var(--uix-text-faint)" > { fmtTime ( entry . at )} </ span >
{ /each }
{ /if }
< span style = "margin-inline-start: auto;" >
< span data-uix-stage-trace-key > range</ span > { rangeLabel }
< / span >
< / div >
< / div >
< div data-uix-tabs role = "tablist" >
< button data-uix-tab data-active = { tab === 'live' } onclick= {() => ( tab = 'live' )} > Live</ button >
< button data-uix-tab data-active = { tab === 'api' } onclick= {() => ( tab = 'api' )} >
API < span data-uix-tab-count > 14< / span >
< / button >
< button data-uix-tab data-active = { tab === 'morfo' } onclick= {() => ( tab = 'morfo' )} >
< span data-uix-layer-badge = "morfo" > morfo< / span >
< span data-uix-tab-count > { partsList . length } p · { events . length } e</ span >
< / button >
< button data-uix-tab data-active = { tab === 'sema' } onclick= {() => ( tab = 'sema' )} >
< span data-uix-layer-badge = "sema" > sema< / span >
< span data-uix-tab-count > { events . length } </ span >
< / button >
< button data-uix-tab data-active = { tab === 'recipe' } onclick= {() => ( tab = 'recipe' )} > Recipe</ button >
< button data-uix-tab data-active = { tab === 'a11y' } onclick= {() => ( tab = 'a11y' )} > A11y</ button >
< / div >
{ #if tab === 'live' }
< section data-uix-section >
< h2 data-uix-section-title > Controls< / h2 >
< p data-uix-section-desc >
Soma owns the range state machine, the keyboard navigation, the
min/max constraints and the hover-preview signaling. Eidos adds
variant/size/color cascades.
< / p >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > range constraints
< / div >
< div data-uix-controls >
< label data-uix-control >
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< span data-uix-control-label > minValue < span data-uix-control-hint > { selectionMinValue ? . toString () ?? 'none' } </ span ></ span >
< input type = "date" bind:value = { minInput } / >
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< / label >
< label data-uix-control >
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< span data-uix-control-label > maxValue < span data-uix-control-hint > { selectionMaxValue ? . toString () ?? 'none' } </ span ></ span >
< input type = "date" bind:value = { maxInput } / >
< / label >
< label data-uix-control >
< span data-uix-control-label > min/max presets< / span >
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< span data-uix-chips role = "radiogroup" >
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< button data-uix-chip data-active = { ! minInput && ! maxInput } onclick= {() => setBoundsPreset ( 'none' )} > none</ button >
< button data-uix-chip data-active = { !! minInput && ! maxInput } onclick= {() => setBoundsPreset ( 'min' )} > min only</ button >
< button data-uix-chip data-active = { ! minInput && !! maxInput } onclick= {() => setBoundsPreset ( 'max' )} > max only</ button >
< button data-uix-chip data-active = { minInput === toDateInput ( defaultMin ) && maxInput === toDateInput ( defaultMax )} onclick= {() => setBoundsPreset ( 'window' )} > May-Jul</ button >
< button data-uix-chip data-active = { maxInput === toDateInput ( invalidMax )} onclick= {() => setBoundsPreset ( 'invalid' )} > invalid</ button >
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > minDays < span data-uix-control-hint > { minDays ?? 'none' } </ span ></ span >
< span data-uix-chips role = "radiogroup" >
< button data-uix-chip data-active = { minDaysInput === '' } onclick= {() => ( minDaysInput = '' )} > none</ button >
{ #each [ '2' , '3' , '7' , '14' ] as n ( n )}
< button data-uix-chip data-active = { minDaysInput === n } onclick= {() => ( minDaysInput = n )} > { n } </ button >
{ /each }
< / span >
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< input type = "number" min = "1" placeholder = "—" bind:value = { minDaysInput } style="inline-size: 4rem ;" />
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< / label >
< label data-uix-control >
< span data-uix-control-label > maxDays < span data-uix-control-hint > { maxDays ?? 'none' } </ span ></ span >
< span data-uix-chips role = "radiogroup" >
< button data-uix-chip data-active = { maxDaysInput === '' } onclick= {() => ( maxDaysInput = '' )} > none</ button >
{ #each [ '7' , '14' , '30' , '90' ] as n ( n )}
< button data-uix-chip data-active = { maxDaysInput === n } onclick= {() => ( maxDaysInput = n )} > { n } </ button >
{ /each }
< / span >
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< input type = "number" min = "1" placeholder = "—" bind:value = { maxDaysInput } style="inline-size: 4rem ;" />
feat(eidos): port range-calendar from soma + extract shared partial
Standalone eidos `<RangeCalendar>` wrapper over the soma range-calendar
primitive. Adds size / variant / color cascades; otherwise pass-through
14 compound parts (Provider, Header, Heading, PrevButton, NextButton,
MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell,
Cell, Day).
## Feature comparison
| Feature | bits-ui | ark-ui | react-aria | Eidos |
|----------------------|---------|--------|------------|-------|
| Range start/end | ✓ | ✓ | ✓ | ✓ |
| Hover preview state | ✓ | ✓ | ✓ | ✓ |
| minDays / maxDays | — | — | ✓ | ✓ |
| allowSingleDay | ✓ | ✓ | ✓ | ✓ |
| preventDeselect | ✓ | ✓ | — | ✓ |
| Multi-month display | ✓ | ✓ | ✓ | ✓ |
| Week-number column | — | — | ✓ | ✓ |
| Month/Year selects | — | ✓ | — | ✓ |
| Disabled / readonly | ✓ | ✓ | ✓ | ✓ |
| isDateUnavailable | ✓ | ✓ | ✓ | ✓ |
| Holiday markers | — | — | — | ✓ |
| Locale / dir | ✓ | ✓ | ✓ | ✓ |
| Size cascade | — | — | — | ✓ 4 |
| Variant cascade | — | — | — | ✓ 3 |
| Color cascade | — | — | — | ✓ 8 |
## Range-cells partial extracted
The recipe at `src/uix/eidos/components/range-calendar/range-calendar.css`
now owns ALL `[data-range-calendar-*]` selectors — the shared rules
that previously lived inside `date-range-picker.css` (lines 537-834)
moved here. Both consumers (the standalone `<RangeCalendar>` and
`<DateRangePicker>`'s popover content) stamp the same data-attrs, so the
same selectors paint without duplication.
`date-range-picker.css` drops 298 lines of duplication; it keeps only
the picker-specific scope overrides (`[data-date-range-picker-calendar]`
with no chrome, max-content sizing, transparent bg).
## Morfo
Extended `rangeCalendarMorfo.scope` to include `'eidos'` so the new
wrapper appears in the eidos-layer inventory and the demo Morfo tab
declares it correctly.
## Demo
Canonical 6-tab depth (~600L) at `/uix/components/range-calendar`. Live
tab exposes every range-specific prop: minDays / maxDays sliders,
allowSingleDay, preventDeselect, numberOfMonths chips (1 / 2 / 3),
pagedNavigation, fixedWeeks, showWeekNumbers, disableDaysOutsideMonth,
weekStartsOn, disabled, readonly, locale, plus size / variant / color
cascades.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< / label >
< label data-uix-control >
< span data-uix-control-label > allowSingleDay< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { allowSingleDay } / >
< span data-uix-switch-label > { allowSingleDay ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > preventDeselect< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { preventDeselect } / >
< span data-uix-switch-label > { preventDeselect ? 'on' : 'off' } </ span >
< / span >
< / label >
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "soma" > soma< / span > grid layout
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > numberOfMonths< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 1 , 2 , 3 ] as n ( n )}
< button data-uix-chip data-active = { numberOfMonths === n } onclick= {() => setNumberOfMonths ( n )} > { n } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > pagedNavigation< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { pagedNavigation } / >
< span data-uix-switch-label > { pagedNavigation ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > fixedWeeks< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { fixedWeeks } / >
< span data-uix-switch-label > { fixedWeeks ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > showWeekNumbers< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { showWeekNumbers } / >
< span data-uix-switch-label > { showWeekNumbers ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > disableDaysOutsideMonth< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { disableDaysOutsideMonth } / >
< span data-uix-switch-label > { disableDaysOutsideMonth ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > weekStartsOn< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'locale' , '0' , '1' ] as opt ( opt )}
< button data-uix-chip data-active = { weekStartsOn === opt } onclick= {() => ( weekStartsOn = opt as WeekStartControl )} > { opt } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > disabled< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { disabled } / >
< span data-uix-switch-label > { disabled ? 'on' : 'off' } </ span >
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > readonly< / span >
< span data-uix-switch >
< input type = "checkbox" bind:checked = { readonly } / >
< span data-uix-switch-label > { readonly ? 'on' : 'off' } </ span >
< / span >
< / label >
< / div >
< div data-uix-subsection-head >
< span data-uix-layer-badge = "eidos" > eidos< / span > visual props
< / div >
< div data-uix-controls >
< label data-uix-control >
< span data-uix-control-label > variant< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'surface' , 'outline' , 'ghost' ] as v ( v )}
< button data-uix-chip data-active = { variant === v } onclick= {() => ( variant = v as RangeCalendarVariant )} > { v } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > size< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'xs' , 'sm' , 'md' , 'lg' ] as s ( s )}
< button data-uix-chip data-active = { size === s } onclick= {() => ( size = s as RangeCalendarSize )} > { s } </ button >
{ /each }
< / span >
< / label >
< label data-uix-control >
< span data-uix-control-label > color< / span >
< span data-uix-chips role = "radiogroup" >
{ #each [ 'primary' , 'secondary' , 'neutral' , 'affirm' , 'fulfill' , 'risk' , 'threat' , 'loss' ] as c ( c )}
< button data-uix-chip data-active = { color === c } onclick= {() => ( color = c as RangeCalendarColor )} > { c } </ button >
{ /each }
< / span >
< / label >
< / div >
< div data-uix-code >
< div data-uix-code-head >
< span data-uix-layer-badge = "eidos" > eidos< / span >
< span > composition · with bind:value< / span >
< span data-uix-code-lang > svelte< / span >
< / div >
< pre >< code > { eidosSnippet } </ code ></ pre >
< / div >
< / section >
{ /if }
{ #if tab === 'api' }
< section data-uix-section >
< h2 data-uix-section-title > API reference< / h2 >
< p data-uix-section-desc > 14 parts: Provider, Header, Heading, PrevButton, NextButton, MonthSelect, YearSelect, Grid, GridHead, GridBody, GridRow, HeadCell, Cell, Day.< / p >
< div data-uix-subsection-head > Provider (RangeCalendar)< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Prop< / th > < th > Type< / th > < th > Default< / th > < th > Description< / th > < / tr > < / thead >
< tbody >
< tr >< td class = "name" > value < span data-uix-tag data-kind = "bindable" > bindable</ span ></ td >< td class = "type" > DateRange</ td >< td class = "default" > { `{}` } </ td >< td > Committed range.</ td ></ tr >
< tr > < td class = "name" > placeholder < span data-uix-tag data-kind = "bindable" > bindable< / span > < / td > < td class = "type" > DateValue< / td > < td class = "default empty" > today< / td > < td > Month currently visible.< / td > < / tr >
< tr > < td class = "name" > onValueChange< / td > < td class = "type" > (r: DateRange) => void< / td > < td class = "default empty" > —< / td > < td > Fires when end is committed.< / td > < / tr >
< tr > < td class = "name" > minValue · maxValue< / td > < td class = "type" > DateValue< / td > < td class = "default empty" > —< / td > < td > Allowed bounds.< / td > < / tr >
< tr > < td class = "name" > minDays · maxDays< / td > < td class = "type" > number< / td > < td class = "default empty" > —< / td > < td > Inclusive length constraints (days).< / td > < / tr >
< tr > < td class = "name" > allowSingleDay< / td > < td class = "type" > boolean< / td > < td class = "default" > true< / td > < td > Permit start === end.< / td > < / tr >
< tr > < td class = "name" > preventDeselect< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Clicking the selected endpoint keeps the range.< / td > < / tr >
< tr > < td class = "name" > numberOfMonths< / td > < td class = "type" > number< / td > < td class = "default" > 1< / td > < td > Visible months.< / td > < / tr >
< tr > < td class = "name" > pagedNavigation< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Prev/Next advance by numberOfMonths.< / td > < / tr >
< tr > < td class = "name" > fixedWeeks< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Always render 6 rows.< / td > < / tr >
< tr > < td class = "name" > disableDaysOutsideMonth< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > Skip non-current-month cells from selection.< / td > < / tr >
< tr > < td class = "name" > disabled · readonly< / td > < td class = "type" > boolean< / td > < td class = "default" > false< / td > < td > OR-merged with Field.Provider.< / td > < / tr >
< tr > < td class = "name" > isDateDisabled · isDateUnavailable · isDateHoliday< / td > < td class = "type" > DateMatcher< / td > < td class = "default empty" > —< / td > < td > Per-cell decorators.< / td > < / tr >
< tr > < td class = "name" > weekStartsOn · weekdayFormat · locale · dir< / td > < td class = "type" > —< / td > < td class = "default empty" > locale< / td > < td > Localization.< / td > < / tr >
< tr > < td class = "name" > size < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > 'xs' | 'sm' | 'md' | 'lg'< / td > < td class = "default" > 'md'< / td > < td > Day cells + nav buttons + selects.< / td > < / tr >
< tr > < td class = "name" > variant < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > 'surface' | 'outline' | 'ghost'< / td > < td class = "default" > 'surface'< / td > < td > Calendar chrome.< / td > < / tr >
< tr > < td class = "name" > color < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td class = "type" > ColorRole< / td > < td class = "default" > 'primary'< / td > < td > End-endpoint + in-range fill + focus accent.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Sub-parts< / div >
< p data-uix-section-desc style = "margin-top: 0.25rem;" >
The sub-parts (< code > Header< / code > , < code > Heading< / code > ,
< code > PrevButton< / code > , < code > NextButton< / code > ,
< code > MonthSelect< / code > , < code > YearSelect< / code > ,
< code > Grid< / code > , < code > GridHead< / code > , < code > GridBody< / code > ,
< code > GridRow< / code > , < code > HeadCell< / code > , < code > Cell< / code > ,
< code > Day< / code > ) are pass-throughs to soma — they accept the
headless props from the soma `RangeCalendarXxxProps` types
unchanged. See the morfo table below for the per-part data
contract.
< / p >
< / section >
{ /if }
{ #if tab === 'morfo' }
< section data-uix-section >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "morfo" > morfo< / span >
· declarative contract
< / h2 >
< p data-uix-section-desc > Source: < code > src/uix/morfo/components/range-calendar.ts< / code > .< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Field< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr >< td class = "name" > name</ td >< td class = "type" > "{ rangeCalendarMorfo . name } "</ td ></ tr >
< tr >< td class = "name" > kebab</ td >< td class = "type" > "{ rangeCalendarMorfo . kebab } "</ td ></ tr >
< tr >< td class = "name" > apg</ td >< td >< a href = { rangeCalendarMorfo . apg } target="_blank" rel = "noreferrer" > { rangeCalendarMorfo . apg } </ a ></ td ></ tr >
< tr >< td class = "name" > scope</ td >< td class = "default" > { rangeCalendarMorfo . scope . join ( ', ' )} </ td ></ tr >
< tr >< td class = "name" > parts.length</ td >< td class = "default" > { rangeCalendarMorfo . parts . length } </ td ></ tr >
< tr >< td class = "name" > events.length</ td >< td class = "default" > { events . length } </ td ></ tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Parts< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Part< / th > < th > Marker< / th > < th > Element< / th > < th > Role< / th > < th > Archetype< / th > < th > States< / th > < th > Optional< / th > < / tr > < / thead >
< tbody >
{ #each partsList as part ( part . kebab )}
< tr >
< td class = "name" > { part . kebab } </ td >
< td >< code data-uix-part-marker > [{ part . marker } ]</ code ></ td >
< td class = "type" > < { part . defaultElement } > </ td >
< td class = "type" > { part . role ?? '—' } </ td >
< td class = "default" > { part . archetype ?? '—' } </ td >
< td class = "default" > { part . states . length ? part . states . join ( ' | ' ) : '—' } </ td >
< td class = "default" > { part . optional ? 'yes' : 'no' } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > Events declaration< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > name< / th > < th > family< / th > < th > verb< / th > < th > sequence< / th > < th > intent< / th > < th > target< / th > < / tr > < / thead >
< tbody >
{ #each events as action ( action . name )}
{ @const sem = action . semantic }
{ @const intentDecl = 'intent' in sem ? sem.intent : undefined }
{ @const intentStr = typeof intentDecl === 'string' ? intentDecl : intentDecl ? `fromProp:$ {( intentDecl as { fromProp? : string }). fromProp ?? '?' } ` : '—' }
< tr >
< td class = "name" > { action . name } </ td >
< td class = "type" > { sem . family } </ td >
< td > { sem . verb ?? '—' } </ td >
< td > { sem . sequence ?? 'pre' } </ td >
< td class = "default" > { intentStr } </ td >
< td > { action . target } </ td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'sema' }
< section data-uix-section id = "sema" >
< h2 data-uix-section-title >
< span data-uix-layer-badge = "sema" > sema< / span >
· events
< / h2 >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Name< / th > < th > Family< / th > < th > Verb< / th > < th > Sequence< / th > < th > Intent< / th > < th > Play< / th > < / tr > < / thead >
< tbody >
{ #each events as action ( action . name )}
{ @const intentDecl = 'intent' in action . semantic ? action.semantic.intent : undefined }
{ @const effectiveIntent = typeof intentDecl === 'string' ? intentDecl : undefined }
< tr >
< td class = "name" > { action . name } </ td >
< td class = "type" > { action . semantic . family } </ td >
< td > { action . semantic . verb ?? '—' } </ td >
< td > { action . semantic . sequence ?? 'pre' } </ td >
< td class = "default" > { effectiveIntent ?? '—' } </ td >
< td >
< button
data-uix-play
onclick={() => {
const target = (stageRef?.querySelector('[data-range-calendar]') ?? stageRef) as HTMLElement | null;
if (!target) return;
emitSelectStart({
name: action.name,
family: action.semantic.family,
target,
...(effectiveIntent ? { intent : effectiveIntent } : {} )
});
}}
>▶ play< / button >
< / td >
< / tr >
{ /each }
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'recipe' }
< section data-uix-section >
< h2 data-uix-section-title > Eidos recipe< / h2 >
< p data-uix-section-desc >
Selectors at < code > src/uix/eidos/components/range-calendar/range-calendar.css< / code > .
This file is consumed BOTH by the standalone eidos
< code > < RangeCalendar> < / code > and by
< code > < DateRangePicker> < / code > 's popover content — the
same < code > [data-range-calendar-*]< / code > attrs land in both
environments. The picker recipe only adds scoping overrides
(no chrome, max-content sizing, transparent background).
< / p >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Selector< / th > < th > Source< / th > < th > What it paints< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > [data-range-calendar]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Root chrome — border + bg + padding + token scaffolding.< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar][data-size]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Day-cell + control-button size scale (xs/sm/md/lg).< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar][data-variant]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Surface / outline / ghost chrome.< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar][data-color]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > End-endpoint + in-range + focus accent tokens.< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-day][data-range-start]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Start endpoint: affirm-family solid + left-edge stripe + semibold.< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-day][data-range-end]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > End endpoint: accent-family solid + right-edge stripe.< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-day][data-range-start][data-range-end]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Single-day range: diagonal-split gradient (start ↘ end).< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-cell][data-in-range]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > In-range cells get accent-track background fill on the cell.< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-day][data-in-preview-range]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Hover-preview state while user is choosing end (transparent + accent border).< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-day][data-today]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Today marker (border ring + semibold).< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-day][data-outside-month]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > Outside-month dimming.< / td > < / tr >
< tr > < td class = "name" > [data-range-calendar-day][data-disabled] · [data-unavailable]< / td > < td > < span data-uix-tag data-kind = "eidos" > eidos< / span > < / td > < td > not-allowed cursor + reduced opacity (+ line-through for unavailable).< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
{ #if tab === 'a11y' }
< section data-uix-section >
< h2 data-uix-section-title > Accessibility< / h2 >
< p data-uix-section-desc >
Follows the APG < a href = { rangeCalendarMorfo . apg } target="_blank" rel = "noreferrer" > grid pattern</ a >
with localized labels for range-start / range-end / in-range
announced via < code > aria-label< / code > additions on the relevant
day cells.
< / p >
< div data-uix-subsection-head > Keyboard< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Key< / th > < th > Action< / th > < / tr > < / thead >
< tbody >
< tr > < td > < span data-uix-kbd > ArrowLeft< / span > / < span data-uix-kbd > ArrowRight< / span > < / td > < td > Move focus ±1 day.< / td > < / tr >
< tr > < td > < span data-uix-kbd > ArrowUp< / span > / < span data-uix-kbd > ArrowDown< / span > < / td > < td > Move focus ±1 week.< / td > < / tr >
< tr > < td > < span data-uix-kbd > PageUp< / span > / < span data-uix-kbd > PageDown< / span > < / td > < td > Move focus ±1 month.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Shift+PageUp< / span > / < span data-uix-kbd > Shift+PageDown< / span > < / td > < td > Move focus ±1 year.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Home< / span > / < span data-uix-kbd > End< / span > < / td > < td > First / last day of the visible week.< / td > < / tr >
< tr > < td > < span data-uix-kbd > Enter< / span > / < span data-uix-kbd > Space< / span > < / td > < td > Commit start (first press) or end (second press).< / td > < / tr >
< tr > < td > < span data-uix-kbd > Escape< / span > < / td > < td > Cancel the in-progress range; revert to last committed.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div data-uix-subsection-head > ARIA contract< / div >
< div data-uix-table-wrap >
< table data-uix-table >
< thead > < tr > < th > Part< / th > < th > Attribute< / th > < th > Value< / th > < / tr > < / thead >
< tbody >
< tr > < td class = "name" > provider< / td > < td > role< / td > < td class = "type" > "application"< / td > < / tr >
< tr > < td class = "name" > provider< / td > < td > aria-label< / td > < td class = "type" > localized "Range calendar"< / td > < / tr >
< tr > < td class = "name" > grid< / td > < td > role< / td > < td class = "type" > "grid"< / td > < / tr >
< tr > < td class = "name" > cell< / td > < td > role · aria-selected< / td > < td class = "type" > "gridcell" / "true|false"< / td > < / tr >
< tr > < td class = "name" > day< / td > < td > aria-label< / td > < td class = "type" > "date — range start | range end | in range"< / td > < / tr >
< tr > < td class = "name" > prev-button · next-button< / td > < td > aria-label< / td > < td class = "type" > "Previous month" / "Next month"< / td > < / tr >
< / tbody >
< / table >
< / div >
< / section >
{ /if }
< / div >
fix(carousel/range-calendar/time-range-field): visual bugs across all three
Real visual audit caught three classes of bugs that the prior commits
missed because they were never opened in the browser.
## Carousel
### Vertical orientation overflow
Recipe gave the root no block-size in vertical, so items spilled. Plus
the soma `viewportSize` derivation only ran on ref / orientation-prop
changes, not on layout reshape — so flipping horizontal → vertical
kept reading the old clientWidth and the translate maths used the wrong
slide size (observed -214 instead of -156 after a flip).
Fix:
- Recipe: `[data-carousel][data-orientation='vertical']` gets explicit
`block-size`, and the viewport / item-group / item each get `100%`
along the block axis. Default vertical size lives on
`--_carousel-vertical-block-size` (16rem), overridable via the new
eidos `verticalBlockSize` prop or the custom property directly.
- Soma: replaced the synchronous `clientWidth/Height` reads with
`viewportClientWidth/Height` `$state` mirrors populated by a
ResizeObserver on the viewport. Layout-only changes (orientation
flip, container resize, font scaling) now propagate to translate
calc on the next reactive tick.
## RangeCalendar
### Multi-month stacked vertically
`numberOfMonths > 1` rendered grids stacked in a column because the
eidos `[data-range-calendar]` recipe is `inline-flex; column`. Copied
the side-by-side grid layout from the existing date-range-picker demo:
`.range-months { display: grid; grid-auto-flow: column; }` with
`grid-auto-flow: row` collapse for `data-month-count='1'`.
### Min/max controls were nonsense
The prior demo had a custom "today | custom" mode that conflated
fixtures with real today, and required two separate enum states.
Rewrote the controls to mirror the date-range-picker demo exactly:
two `<input type="date">` bindings on string state, parsed by
`parseDateInput`, plus a preset chip row (none / min only / max only /
window / invalid).
### Weekday key duplicate
The narrow-format weekday strings repeat letters ('T' for Tuesday and
Thursday, 'S' for Saturday and Sunday), so keying by `wd` raised
`each_key_duplicate`. Changed to key by index.
## TimeRangeField
### Start endpoint rendered no segments
Keying the segments `#each` by `seg.part` collapsed when segment
parts repeat. Copied the canonical key shape from the time-range-picker
demo: `(${part}-${i})`. Verified both endpoints now render their
four segments (`09:30` — `17:45` for the default fixture).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 months ago
< style >
/*
* Multi-month layout — mirrors the date-range-picker demo
* (web/routes/uix/components/date-range-picker/+page.svelte). Side-by-side
* via `grid-auto-flow: column` so the grids never stack vertically; the
* single-month case collapses to one row.
*/
.range-months {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
justify-content: center;
gap: var(--calendar-month-gap, 36px);
max-inline-size: 100%;
overflow-x: auto;
}
.range-months[data-month-count='1'] {
grid-auto-flow: row;
}
.range-month-panel {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--calendar-grid-gap);
min-inline-size: max-content;
}
< / style >