diff --git a/.changeset/funny-planes-smile.md b/.changeset/funny-planes-smile.md new file mode 100644 index 0000000000..fc09dd2730 --- /dev/null +++ b/.changeset/funny-planes-smile.md @@ -0,0 +1,9 @@ +--- +'@evidence-dev/core-components': major +--- + +- Timezone Fixes +- Changed the default value to today +- Year select only shows the first and last date in the data +- Disabled dates that are before or after the first and last date +- Added Today and Yesterday range presets \ No newline at end of file diff --git a/packages/ui/core-components/src/lib/atoms/inputs/date-input/DateInput.svelte b/packages/ui/core-components/src/lib/atoms/inputs/date-input/DateInput.svelte index 30a332511b..17d39b40d9 100644 --- a/packages/ui/core-components/src/lib/atoms/inputs/date-input/DateInput.svelte +++ b/packages/ui/core-components/src/lib/atoms/inputs/date-input/DateInput.svelte @@ -45,6 +45,8 @@ /** @type {boolean| string} */ export let range = false; $: range = toBoolean(range); + /** @type {boolean | string} */ + export let defaultToToday = false; let query; let errors = []; @@ -78,11 +80,9 @@ $: startString = formatDateString(start || $query?.[0].start || new Date(0)); $: endString = formatDateString(end || $query?.[0].end || new Date()); - let currentDate = dateToYYYYMMDD(new Date()); - let extraDayEndString; - $: if (endString && range) { + $: if (endString) { extraDayEndString = new Date(endString); extraDayEndString.setDate(extraDayEndString.getDate() + 1); extraDayEndString = formatDateString(extraDayEndString); @@ -151,10 +151,12 @@ loaded={loaded?.ready ?? true} {presetRanges} {defaultValue} + {defaultToToday} {range} - {currentDate} {title} {description} + {data} + {dates} /> {/if} diff --git a/packages/ui/core-components/src/lib/atoms/inputs/date-input/_DateInput.svelte b/packages/ui/core-components/src/lib/atoms/inputs/date-input/_DateInput.svelte index 4c30d94a75..030d13b5bd 100644 --- a/packages/ui/core-components/src/lib/atoms/inputs/date-input/_DateInput.svelte +++ b/packages/ui/core-components/src/lib/atoms/inputs/date-input/_DateInput.svelte @@ -6,7 +6,8 @@ startOfMonth, endOfMonth, startOfYear, - endOfYear + endOfYear, + today } from '@internationalized/date'; import { cn } from '$lib/utils.js'; import { Button } from '$lib/atoms/shadcn/button/index.js'; @@ -33,133 +34,197 @@ dateStyle: 'short' }); + const todayDate = today(getLocalTimeZone()); + /** @type {import('bits-ui').DateRange | undefined} */ let selectedDateInput = undefined; + $: referenceDate = (() => { + if (selectedDateInput && !range) { + return selectedDateInput; + } + if (selectedDateInput && selectedDateInput.end) { + return selectedDateInput.end; + } + // If today is outside the data range, use calendarEnd as reference for presets + if (calendarEnd && todayDate.compare(calendarEnd) > 0) { + return calendarEnd; + } + if (calendarStart && todayDate.compare(calendarStart) < 0) { + return calendarStart; + } + return todayDate; + })(); + /** @type {(selectedDateInput: import('bits-ui').DateRange | undefined) => void} */ export let onSelectedDateInputChange; /** @type {string} */ - export let start; + export let start = undefined; /** @type {string} */ - export let end; + export let end = undefined; export let loaded = true; /** @type {[]string] | undefined} */ - export let presetRanges; + export let presetRanges = undefined; /** @type {string] | undefined} */ - export let defaultValue; + export let defaultValue = undefined; /** @type {boolean} */ export let range = false; $: range = toBoolean(range); + /** @type {boolean} */ + export let defaultToToday = false; + $: defaultToToday = toBoolean(defaultToToday); /** @type {string} */ - export let title; + export let title = undefined; export let extraDayEndString = undefined; /** @type {string | undefined} */ export let description = undefined; + /** @type {any} */ + export let data = undefined; + /** @type {string | undefined} */ + export let dates = undefined; + + // Extract available years from data if provided + $: extractedYears = (() => { + if (!data || !dates || !data.rows) { + return undefined; + } + + const years = new Set(); + data.rows.forEach((row) => { + if (row[dates]) { + const year = new Date(row[dates]).getFullYear(); + years.add(year); + } + }); + + return Array.from(years).sort((a, b) => b - a); + })(); + + $: calendarStart = start ? YYYYMMDDToCalendar(start) : todayDate.subtract({ years: 10 }); + // Use extraDayEndString for safety measures if available, otherwise use regular end + $: calendarEnd = + extraDayEndString || end ? YYYYMMDDToCalendar(extraDayEndString || end) : todayDate; /** @type { { label: string, group: string, range: import('bits-ui').DateRange }[] } */ $: presets = [ + { + label: 'Yesterday', + group: 'Days', + range: { + start: todayDate.subtract({ days: 1 }), + end: todayDate.subtract({ days: 1 }) + } + }, + { + label: 'Today', + group: 'Days', + range: { + start: todayDate, + end: todayDate + } + }, { label: 'Last 7 Days', group: 'Days', range: { - start: calendarEnd.subtract({ days: 6 }), - end: calendarEnd + start: referenceDate.subtract({ days: 6 }), + end: referenceDate } }, { label: 'Last 30 Days', group: 'Days', range: { - start: calendarEnd.subtract({ days: 29 }), - end: calendarEnd + start: referenceDate.subtract({ days: 29 }), + end: referenceDate } }, { label: 'Last 90 Days', group: 'Days', range: { - start: calendarEnd.subtract({ days: 89 }), - end: calendarEnd + start: referenceDate.subtract({ days: 89 }), + end: referenceDate } }, { label: 'Last 365 Days', group: 'Days', range: { - start: calendarEnd.subtract({ days: 364 }), - end: calendarEnd + start: referenceDate.subtract({ days: 364 }), + end: referenceDate } }, { label: 'Last 3 Months', group: 'Months', range: { - start: startOfMonth(calendarEnd.subtract({ months: 3 })), - end: endOfMonth(calendarEnd.subtract({ months: 1 })) + start: startOfMonth(referenceDate.subtract({ months: 3 })), + end: endOfMonth(referenceDate.subtract({ months: 1 })) } }, { label: 'Last 6 Months', group: 'Months', range: { - start: startOfMonth(calendarEnd.subtract({ months: 6 })), - end: endOfMonth(calendarEnd.subtract({ months: 1 })) + start: startOfMonth(referenceDate.subtract({ months: 6 })), + end: endOfMonth(referenceDate.subtract({ months: 1 })) } }, { label: 'Last 12 Months', group: 'Months', range: { - start: startOfMonth(calendarEnd.subtract({ months: 12 })), - end: endOfMonth(calendarEnd.subtract({ months: 1 })) + start: startOfMonth(referenceDate.subtract({ months: 12 })), + end: endOfMonth(referenceDate.subtract({ months: 1 })) } }, { label: 'Last Month', group: 'Last', range: { - start: startOfMonth(calendarEnd.subtract({ months: 1 })), - end: endOfMonth(calendarEnd.subtract({ months: 1 })) + start: startOfMonth(referenceDate.subtract({ months: 1 })), + end: endOfMonth(referenceDate.subtract({ months: 1 })) } }, { label: 'Last Year', group: 'Last', range: { - start: startOfYear(calendarEnd.subtract({ years: 1 })), - end: endOfYear(calendarEnd.subtract({ years: 1 })) + start: startOfYear(referenceDate.subtract({ years: 1 })), + end: endOfYear(referenceDate.subtract({ years: 1 })) } }, { label: 'Month to Date', group: 'To Date', range: { - start: startOfMonth(calendarEnd), - end: endOfMonth(calendarEnd) + start: startOfMonth(referenceDate), + end: endOfMonth(referenceDate) } }, { label: 'Month to Today', group: 'To Date', range: { - start: startOfMonth(calendarEnd), - end: calendarEnd + start: startOfMonth(referenceDate), + end: todayDate } }, { label: 'Year to Date', group: 'To Date', range: { - start: startOfYear(calendarEnd), - end: endOfYear(calendarEnd) + start: startOfYear(referenceDate), + end: endOfYear(referenceDate) } }, { label: 'Year to Today', group: 'To Date', range: { - start: startOfYear(calendarEnd), - end: calendarEnd + start: startOfYear(referenceDate), + end: todayDate } }, { @@ -198,7 +263,51 @@ let selectedPreset; let placeholder; - $: setPlaceholderDefault(calendarEnd); + // Set default placeholder: use calendarEnd if today is beyond the data range, otherwise use today + $: { + if (calendarEnd && todayDate.compare(calendarEnd) > 0) { + // If today is after the end of the data range, open to the end date + setPlaceholderDefault(calendarEnd); + } else if (calendarStart && todayDate.compare(calendarStart) < 0) { + // If today is before the start of the data range, open to the start date + setPlaceholderDefault(calendarStart); + } else { + // Otherwise, open to today + setPlaceholderDefault(todayDate); + } + } + + $: if (typeof defaultValue === 'string' && !selectedPreset && presets.length) { + applyPreset(defaultValue); + } + + // Initialize with default value or today's date (if defaultToToday is enabled) + $: if (!selectedDateInput && !selectedPreset) { + if (defaultValue && typeof defaultValue === 'string') { + try { + const defaultDate = YYYYMMDDToCalendar(defaultValue); + selectedDateInput = defaultDate; + } catch (error) { + if (defaultToToday) { + if (calendarEnd && todayDate.compare(calendarEnd) > 0) { + selectedDateInput = calendarEnd; + } else if (calendarStart && todayDate.compare(calendarStart) < 0) { + selectedDateInput = calendarStart; + } else { + selectedDateInput = todayDate; + } + } + } + } else if (defaultToToday) { + if (calendarEnd && todayDate.compare(calendarEnd) > 0) { + selectedDateInput = calendarEnd; + } else if (calendarStart && todayDate.compare(calendarStart) < 0) { + selectedDateInput = calendarStart; + } else { + selectedDateInput = todayDate; + } + } + } // group exists check for nicely rendering group border for dropdown function groupExists(groupName) { @@ -219,23 +328,10 @@ selectedPreset = targetPreset; if (range) { selectedDateInput = targetPreset.range; + } else { + selectedDateInput = targetPreset.range.end; } - } - - $: if ( - typeof defaultValue === 'string' && - !selectedDateInput && - !selectedPreset && - presets.length - ) - applyPreset(defaultValue); - - $: calendarStart = YYYYMMDDToCalendar(start); - $: calendarEnd = YYYYMMDDToCalendar(end); - - let extraDayCalendarEnd = calendarEnd; - $: if (range) { - extraDayCalendarEnd = YYYYMMDDToCalendar(extraDayEndString); + onSelectedDateInputChange(selectedDateInput); } function updateDateRange(start, end) { @@ -244,7 +340,7 @@ if (range) { selectedDateInput = { start, end }; } else { - selectedDateInput = start; + selectedDateInput = selectedDateInput; } } $: updateDateRange(calendarStart, calendarEnd); @@ -326,7 +422,9 @@ selectedDateInput = value; }} minValue={calendarStart} - maxValue={extraDayCalendarEnd} + maxValue={calendarEnd} + defaultValue={todayDate} + availableYears={extractedYears} /> {:else} {/if} diff --git a/packages/ui/core-components/src/lib/atoms/inputs/date-input/helpers.js b/packages/ui/core-components/src/lib/atoms/inputs/date-input/helpers.js index 302f3a74e1..d912f461e9 100644 --- a/packages/ui/core-components/src/lib/atoms/inputs/date-input/helpers.js +++ b/packages/ui/core-components/src/lib/atoms/inputs/date-input/helpers.js @@ -1,7 +1,10 @@ const YYYYMMDD = /^\d{4}-\d{2}-\d{2}$/; function dateToYYYYMMDD(date) { - return date.toISOString().split('T')[0]; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; } function formatDateString(date) { diff --git a/packages/ui/core-components/src/lib/atoms/shadcn/calendar/calendar.svelte b/packages/ui/core-components/src/lib/atoms/shadcn/calendar/calendar.svelte index 04c8e9090f..ab120c5bc2 100644 --- a/packages/ui/core-components/src/lib/atoms/shadcn/calendar/calendar.svelte +++ b/packages/ui/core-components/src/lib/atoms/shadcn/calendar/calendar.svelte @@ -34,6 +34,8 @@ let minValue = undefined; /** @type {CalendarDate | undefined} */ let maxValue = undefined; + /** @type {number[] | undefined} */ + let availableYears = undefined; /** @type {string | undefined | null} */ let className = undefined; @@ -57,10 +59,23 @@ month: 'long' }); - $: yearOptions = Array.from({ length: 100 }, (_, i) => ({ - label: String(new Date().getFullYear() - i), - value: new Date().getFullYear() - i - })).filter(({ value }) => !(minValue?.year > value || value > maxValue?.year)); + // Create year options based on available years from data + $: yearOptions = (() => { + if (availableYears?.length > 0) { + return availableYears + .sort((a, b) => b - a) + .map((year) => ({ label: String(year), value: year })) + .filter(({ value }) => !(minValue?.year > value || value > maxValue?.year)); + } + if (minValue && maxValue) { + const years = []; + for (let year = maxValue.year; year >= minValue.year; year--) { + years.push({ label: String(year), value: year }); + } + return years; + } + return undefined; + })(); $: defaultYear = placeholder ? { value: placeholder.year, label: String(placeholder.year) } @@ -78,7 +93,8 @@ startValue, selectedDateInput, minValue, - maxValue + maxValue, + availableYears }; @@ -126,9 +142,37 @@ selected={defaultYear} items={yearOptions} onSelectedChange={(v) => { - if (!v || !placeholder) return; - if (v.value === placeholder?.year) return; - placeholder = placeholder.set({ year: v.value }); + if (!v || !placeholder || v.value === placeholder?.year) return; + + const newYear = v.value; + const currentMonth = placeholder.month; + + // First, try to keep the same month in the new year + const sameMonthDate = placeholder.set({ year: newYear, month: currentMonth }); + if ( + (!minValue || sameMonthDate.compare(minValue) >= 0) && + (!maxValue || sameMonthDate.compare(maxValue) <= 0) + ) { + placeholder = sameMonthDate; + return; + } + + // If the same month is not available, find the closest valid month + const isBeyondRange = maxValue && sameMonthDate.compare(maxValue) > 0; + const startMonth = isBeyondRange ? 12 : 1; + const endMonth = isBeyondRange ? 0 : 13; + const step = isBeyondRange ? -1 : 1; + + for (let month = startMonth; month !== endMonth; month += step) { + const testDate = placeholder.set({ year: newYear, month }); + if ( + (!minValue || testDate.compare(minValue) >= 0) && + (!maxValue || testDate.compare(maxValue) <= 0) + ) { + placeholder = testDate; + break; + } + } }} > - {#each yearOptions as { value, label }} - - {label} - - {/each} + {#if yearOptions && yearOptions.length > 0} + {#each yearOptions as { value, label }} + + {label} + + {/each} + {/if} diff --git a/packages/ui/core-components/src/lib/atoms/shadcn/range-calendar/range-calendar.svelte b/packages/ui/core-components/src/lib/atoms/shadcn/range-calendar/range-calendar.svelte index 0062d35ddb..c0451c810b 100644 --- a/packages/ui/core-components/src/lib/atoms/shadcn/range-calendar/range-calendar.svelte +++ b/packages/ui/core-components/src/lib/atoms/shadcn/range-calendar/range-calendar.svelte @@ -24,6 +24,8 @@ let minValue = undefined; /** @type {CalendarDate | undefined} */ let maxValue = undefined; + /** @type {number[] | undefined} */ + let availableYears = undefined; /** @type {string | undefined | null} */ let className = undefined; @@ -47,10 +49,24 @@ month: 'long' }); - $: yearOptions = Array.from({ length: 100 }, (_, i) => ({ - label: String(new Date().getFullYear() - i), - value: new Date().getFullYear() - i - })).filter(({ value }) => !(minValue?.year > value || value > maxValue?.year)); + // Create year options based on available years from data, with fallback to default range + $: yearOptions = (() => { + if (availableYears?.length > 0) { + return availableYears + .sort((a, b) => b - a) + .map((year) => ({ label: String(year), value: year })) + .filter(({ value }) => !(minValue?.year > value || value > maxValue?.year)); + } + + if (minValue && maxValue) { + const years = []; + for (let year = maxValue.year; year >= minValue.year; year--) { + years.push({ label: String(year), value: year }); + } + return years; + } + return undefined; + })(); $: defaultYear = placeholder ? { value: placeholder.year, label: String(placeholder.year) } @@ -68,7 +84,8 @@ startValue, selectedDateInput, minValue, - maxValue + maxValue, + availableYears }; @@ -116,9 +133,37 @@ selected={defaultYear} items={yearOptions} onSelectedChange={(v) => { - if (!v || !placeholder) return; - if (v.value === placeholder?.year) return; - placeholder = placeholder.set({ year: v.value }); + if (!v || !placeholder || v.value === placeholder?.year) return; + + const newYear = v.value; + const currentMonth = placeholder.month; + + // First, try to keep the same month in the new year + const sameMonthDate = placeholder.set({ year: newYear, month: currentMonth }); + if ( + (!minValue || sameMonthDate.compare(minValue) >= 0) && + (!maxValue || sameMonthDate.compare(maxValue) <= 0) + ) { + placeholder = sameMonthDate; + return; + } + + // If the same month is not available, find the closest valid month + const isBeyondRange = maxValue && sameMonthDate.compare(maxValue) > 0; + const startMonth = isBeyondRange ? 12 : 1; + const endMonth = isBeyondRange ? 0 : 13; + const step = isBeyondRange ? -1 : 1; + + for (let month = startMonth; month !== endMonth; month += step) { + const testDate = placeholder.set({ year: newYear, month }); + if ( + (!minValue || testDate.compare(minValue) >= 0) && + (!maxValue || testDate.compare(maxValue) <= 0) + ) { + placeholder = testDate; + break; + } + } }} > - {#each yearOptions as { value, label }} - - {label} - - {/each} + {#if yearOptions && yearOptions.length > 0} + {#each yearOptions as { value, label }} + + {label} + + {/each} + {/if} diff --git a/sites/docs/pages/components/inputs/date-input/index.md b/sites/docs/pages/components/inputs/date-input/index.md index 8d097affc5..8559678649 100644 --- a/sites/docs/pages/components/inputs/date-input/index.md +++ b/sites/docs/pages/components/inputs/date-input/index.md @@ -293,7 +293,13 @@ Customize "Select a Range" drop down, by including present range options. **Rang Accepts preset in string format to apply default value in Date Input picker. **Range options**: `'Last 7 Days'` `'Last 30 Days'` `'Last 90 Days'` `'Last 3 Months'` `'Last 6 Months'` `'Last 12 Months'` `'Last Month'` `'Last Year'` `'Month to Date'` `'Year to Date'` `'All Time'` - + - +