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'`
-
+
-
+