From 05a432afc05059c9bb872e3a7594a94c9720b43c Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 21 May 2026 15:30:59 +0200 Subject: [PATCH] date-range-picker: YearRangeView + MonthRangeView (kind grids) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the kind contract for the range picker. The popover used to render the day-grid range calendar regardless of kind; now it switches to a year-grid or month-grid view that handles range selection on the appropriate granularity. New Eidos parts: - : 3×4 decade grid with prev/next decade buttons. Selection state machine (mirrors the day-grid range selection): * empty → click sets startValue (and clears endValue) * pending (start set, no end) → click sets endValue. Swap if the clicked year is before the existing start. * complete (both set) → click resets to a fresh start + clears end. Year endpoints normalise to Jan 1 (start) / Dec 31 (end). - : 3×4 month grid for the placeholder's year, with prev/next year navigation. Same state machine using `year*12 + month` as the comparison key, so ranges span multi-year boundaries cleanly. Start = day 1; end = last day of the month (computed via Date(y, m, 0).getDate()). - Localised month names via DateFormatter(locale, { month: 'short' }). Index + CSS: - index.ts: registers YearView + MonthView on the DateRangePicker namespace. - date-range-picker.css: shared grid layout (3 columns, gap-2), hover surface-overlay, selected cells get primary-solid + content-on-solid, in-range cells get color-mix(primary-solid 22%, transparent) as a stripe with border-radius:0 (start/end keep their outer corner radius). Reduced-motion respected. Demo: - {#if kind === 'year'} → ; {:else if 'month'} → ; {:else} → existing day calendar. - pickerFooter snippet rendered once at the Content level (removed the inline render at the end of calendarBody to avoid duplication). Verified in browser: 1. kind='year' chip → popover shows year grid '2020 – 2031'. 2. Click 2024 → cell marked start (purple solid). 3. Click 2027 → cell marked end, 2025-2026 marked in-range. trace shows 'range 2024-01-01 -> 2027-12-31'. Popover stays open (closeOnRangeSelect=false in the demo). 0 type errors, 67/67 component:audit PASS. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../date-range-picker-month-view.svelte | 124 ++++++++++++++++++ .../date-range-picker-year-view.svelte | 110 ++++++++++++++++ .../date-range-picker/date-range-picker.css | 109 +++++++++++++++ .../components/date-range-picker/index.ts | 6 + .../components/date-range-picker/+page.svelte | 18 ++- 5 files changed, 361 insertions(+), 6 deletions(-) create mode 100644 src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte create mode 100644 src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte new file mode 100644 index 000000000..42783afec --- /dev/null +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-month-view.svelte @@ -0,0 +1,124 @@ + + +
+
+ + {year} + +
+
+ {#each months as month (month)} + {@const c = classFor(month)} + + {/each} +
+
diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte b/src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte new file mode 100644 index 000000000..8126767db --- /dev/null +++ b/src/uix/eidos/components/date-range-picker/date-range-picker-year-view.svelte @@ -0,0 +1,110 @@ + + +
+
+ + + {decadeStart} – {decadeStart + 11} + + +
+
+ {#each years as year (year)} + {@const c = classFor(year)} + + {/each} +
+
diff --git a/src/uix/eidos/components/date-range-picker/date-range-picker.css b/src/uix/eidos/components/date-range-picker/date-range-picker.css index dba13c232..e02cfa4f2 100644 --- a/src/uix/eidos/components/date-range-picker/date-range-picker.css +++ b/src/uix/eidos/components/date-range-picker/date-range-picker.css @@ -824,6 +824,115 @@ } } +/* ── Range Year-view + Month-view (kind='year' / 'month') ────────────────── */ + +[data-date-range-picker-year-view], +[data-date-range-picker-month-view] { + display: flex; + flex-direction: column; + gap: var(--space-3); + inline-size: min(20rem, 100%); +} + +[data-date-range-picker-year-view-header], +[data-date-range-picker-month-view-header] { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); +} + +[data-date-range-picker-year-view-heading], +[data-date-range-picker-month-view-heading] { + font-weight: var(--font-weight-semibold); + color: var(--color-content-primary); +} + +[data-date-range-picker-year-view-prev], +[data-date-range-picker-year-view-next], +[data-date-range-picker-month-view-prev], +[data-date-range-picker-month-view-next] { + min-block-size: var(--control-height-sm); + min-inline-size: var(--control-height-sm); + border-radius: var(--radius-md); + border: var(--border-width) solid transparent; + background: transparent; + color: var(--color-content-primary); + cursor: pointer; + font: inherit; + font-size: var(--font-size-md); +} + +[data-date-range-picker-year-view-prev]:hover, +[data-date-range-picker-year-view-next]:hover, +[data-date-range-picker-month-view-prev]:hover, +[data-date-range-picker-month-view-next]:hover { + background: var(--color-surface-overlay); +} + +[data-date-range-picker-year-view-grid], +[data-date-range-picker-month-view-grid] { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-2); +} + +[data-date-range-picker-year-view-cell], +[data-date-range-picker-month-view-cell] { + min-block-size: var(--control-height-md); + border-radius: var(--radius-md); + border: var(--border-width) solid transparent; + background: transparent; + color: var(--color-content-primary); + cursor: pointer; + font: inherit; + font-size: var(--font-size-sm); + transition: + background var(--duration-fast) var(--ease-default), + color var(--duration-fast) var(--ease-default); +} + +[data-date-range-picker-year-view-cell]:hover, +[data-date-range-picker-month-view-cell]:hover { + background: var(--color-surface-overlay); +} + +/* Range stripe: in-range cells get a tinted background. Start and end + * cells overlay it with the solid primary fill. */ +[data-date-range-picker-year-view-cell][data-in-range], +[data-date-range-picker-month-view-cell][data-in-range] { + background: color-mix(in srgb, var(--color-primary-solid) 22%, transparent); + border-radius: 0; +} + +[data-date-range-picker-year-view-cell][data-range-start], +[data-date-range-picker-year-view-cell][data-range-end], +[data-date-range-picker-month-view-cell][data-range-start], +[data-date-range-picker-month-view-cell][data-range-end] { + background: var(--color-primary-solid); + color: var(--color-content-on-solid); + border-color: var(--color-primary-border); +} + +/* Round the open edge of the range strip on start / end cells. */ +[data-date-range-picker-year-view-cell][data-range-start], +[data-date-range-picker-month-view-cell][data-range-start] { + border-start-start-radius: var(--radius-md); + border-end-start-radius: var(--radius-md); +} + +[data-date-range-picker-year-view-cell][data-range-end], +[data-date-range-picker-month-view-cell][data-range-end] { + border-start-end-radius: var(--radius-md); + border-end-end-radius: var(--radius-md); +} + +[data-date-range-picker-year-view-cell]:focus-visible, +[data-date-range-picker-month-view-cell]:focus-visible { + outline: var(--focus-ring-width) solid var(--color-primary-border); + outline-offset: var(--focus-ring-offset); +} + /* ── Footer (Clear / Cancel / Close action row) ──────────────────────────── */ [data-date-range-picker-footer] { diff --git a/src/uix/eidos/components/date-range-picker/index.ts b/src/uix/eidos/components/date-range-picker/index.ts index ed32270d1..d1447437e 100644 --- a/src/uix/eidos/components/date-range-picker/index.ts +++ b/src/uix/eidos/components/date-range-picker/index.ts @@ -10,6 +10,8 @@ import Overlay from '../popover/popover-overlay.svelte'; import Arrow from '../popover/popover-arrow.svelte'; import Close from './date-range-picker-close.svelte'; import Footer from './date-range-picker-footer.svelte'; +import YearView from './date-range-picker-year-view.svelte'; +import MonthView from './date-range-picker-month-view.svelte'; import Clear from './date-range-picker-clear.svelte'; import Cancel from './date-range-picker-cancel.svelte'; import Header from './date-range-picker-header.svelte'; @@ -36,6 +38,8 @@ type DateRangePickerNamespace = typeof DateRangePickerComponent & { Arrow: typeof Arrow; Close: typeof Close; Footer: typeof Footer; + YearView: typeof YearView; + MonthView: typeof MonthView; Clear: typeof Clear; Cancel: typeof Cancel; Label: typeof Label; @@ -66,6 +70,8 @@ DateRangePicker.Content = Content; DateRangePicker.Arrow = Arrow; DateRangePicker.Close = Close; DateRangePicker.Footer = Footer; +DateRangePicker.YearView = YearView; +DateRangePicker.MonthView = MonthView; DateRangePicker.Clear = Clear; DateRangePicker.Cancel = Cancel; DateRangePicker.Label = Label; diff --git a/web/routes/uix/components/date-range-picker/+page.svelte b/web/routes/uix/components/date-range-picker/+page.svelte index 56f8588b4..d5be678e1 100644 --- a/web/routes/uix/components/date-range-picker/+page.svelte +++ b/web/routes/uix/components/date-range-picker/+page.svelte @@ -539,11 +539,18 @@ - - {#snippet children(args: DateRangePickerCalendarSnippetProps)} - {@render calendarBody(args)} - {/snippet} - + {#if kind === 'year'} + + {:else if kind === 'month'} + + {:else} + + {#snippet children(args: DateRangePickerCalendarSnippetProps)} + {@render calendarBody(args)} + {/snippet} + + {/if} + {@render pickerFooter()} @@ -1082,7 +1089,6 @@ {/each} - {@render pickerFooter()} {/snippet} {#snippet pickerFooter()}