@codecademy/gamut
68.2.268.2.3-alpha.93a7da.0
dist/DatePicker/DatePickerCalendar/Calendar/utils/dateGrid.js+
dist/DatePicker/DatePickerCalendar/Calendar/utils/dateGrid.jsNew file+188
Index: package/dist/DatePicker/DatePickerCalendar/Calendar/utils/dateGrid.js
===================================================================
--- package/dist/DatePicker/DatePickerCalendar/Calendar/utils/dateGrid.js
+++ package/dist/DatePicker/DatePickerCalendar/Calendar/utils/dateGrid.js
@@ -0,0 +1,188 @@
+/**
+ * Builds a grid of days for a calendar month using native Date and Intl.
+ * Each row has 7 cells; leading/trailing cells may be null (padding from adjacent months).
+ */
+
+const DAYS_PER_WEEK = 7;
+
+/**
+ * Normalize to start of day in local time for comparison.
+ */
+export const normalizeDate = date => {
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
+};
+
+/**
+ * Number of empty cells before the 1st of the month, for a grid whose first column is
+ * `firstWeekday` (ISO: 1 = Monday … 7 = Sunday from `Intl.Locale#getWeekInfo`).
+ */
+export const getWeekdayOffsetInGrid = ({
+ date,
+ firstWeekday
+}) => {
+ const dayOfWeek = date.getDay();
+ const iso = dayOfWeek === 0 ? 7 : dayOfWeek;
+ return (iso - firstWeekday + 14) % 7;
+};
+export const getFirstOfMonth = date => {
+ return new Date(date.getFullYear(), date.getMonth(), 1);
+};
+
+/**
+ * Returns an array of weeks for the given month. Each week is an array of 7 items:
+ * each item is either a Date (that day) or null (padding from previous/next month).
+ *
+ * @param year - Full year (e.g. 2026)
+ * @param month - Month 0-11 (0 = January)
+ * @param firstWeekday - First day of the week for the calendar row (ISO 1–7, from `getWeekInfo().firstDay`)
+ */
+export const getMonthGrid = ({
+ year,
+ month,
+ firstWeekday
+}) => {
+ const first = getFirstOfMonth(new Date(year, month, 1));
+ const last = new Date(year, month + 1, 0);
+ const firstDayOfWeek = getWeekdayOffsetInGrid({
+ date: first,
+ firstWeekday
+ });
+ const daysInMonth = last.getDate();
+ const weeks = [];
+ let currentWeek = [];
+ for (let i = 0; i < firstDayOfWeek; i += 1) {
+ currentWeek.push(null);
+ }
+ for (let day = 1; day <= daysInMonth; day += 1) {
+ currentWeek.push(new Date(year, month, day));
+ if (currentWeek.length === DAYS_PER_WEEK) {
+ weeks.push(currentWeek);
+ currentWeek = [];
+ }
+ }
+ if (currentWeek.length > 0) {
+ while (currentWeek.length < DAYS_PER_WEEK) {
+ currentWeek.push(null);
+ }
+ weeks.push(currentWeek);
+ }
+ return weeks;
+};
+
+/**
+ * Check if two dates are the same calendar day (ignoring time).
+ */
+export const isSameDay = (a, b) => {
+ if (a === null || b === null) return false;
+ return normalizeDate(a) === normalizeDate(b);
+};
+
+/**
+ * Calendar-ordered local-midnight instants for two possibly unordered `Date` values.
+ * Matches the bounds used by {@link isDateInRange} (and range selection).
+ */
+export const getOrderedCalendarEndpoints = ({
+ startDate,
+ endDate
+}) => {
+ const normalizedStartDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate());
+ const normalizedEndDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate());
+ return normalizedStartDate <= normalizedEndDate ? {
+ low: normalizedStartDate,
+ high: normalizedEndDate
+ } : {
+ low: normalizedEndDate,
+ high: normalizedStartDate
+ };
+};
+
+/**
+ * Check if `date` is between `startDate` and `endDate` (exclusive), ignoring time.
+ */
+export const isDateInRange = ({
+ date,
+ startDate,
+ endDate
+}) => {
+ if (startDate === null) return false;
+ const endBound = endDate ?? startDate;
+ const {
+ low,
+ high
+ } = getOrderedCalendarEndpoints({
+ startDate,
+ endDate: endBound
+ });
+ const normalizedDate = normalizeDate(date);
+ return normalizedDate > normalizeDate(low) && normalizedDate < normalizeDate(high);
+};
+
+/**
+ * Build a `shouldDisableDate` that disables each listed calendar day (time-of-day ignored).
+ *
+ * @example
+ * ```tsx
+ * <DatePicker
+ * mode="single"
+ * selectedDate={null}
+ * onSelected={() => {}}
+ * shouldDisableDate={matchDisabledDates([new Date(2026, 3, 14)])}
+ * />
+ * ```
+ */
+export const matchDisabledDates = (dates = []) => date => dates.some(d => isSameDay(date, d));
+
+/** True when `shouldDisableDate` returns true for this calendar day. */
+export const isDateDisabled = ({
+ date,
+ shouldDisableDate
+}) => Boolean(shouldDisableDate?.(date));
+
+/** One visible day in the month grid with its row (for Home/End and keyboard nav). */
+
+/** Flat list of dates in grid order (row-major, non-null only) with row index for Home/End */
+export const getDatesWithRow = weeks => {
+ const result = [];
+ weeks.forEach((week, rowIndex) => {
+ week.forEach(date => {
+ if (date !== null) result.push({
+ date,
+ rowIndex
+ });
+ });
+ });
+ return result;
+};
+
+/** Add `n` months to the given date. */
+export const addMonths = ({
+ date,
+ n
+}) => new Date(date.getFullYear(), date.getMonth() + n, 1);
+
+/**
+ * True if `date` falls in the left visible month, or—when `showSecondMonth`—in the
+ * month shown in the second column. Used to avoid shifting the visible month pair when
+ * the committed date is already on screen (e.g. a click in the right-hand month).
+ */
+export const isDateWithinVisibleMonths = ({
+ date,
+ startOfLeftVisibleMonth,
+ showSecondMonth
+}) => {
+ const year = date.getFullYear();
+ const month = date.getMonth();
+ const leftYear = startOfLeftVisibleMonth.getFullYear();
+ const leftMonth = startOfLeftVisibleMonth.getMonth();
+ if (year === leftYear && month === leftMonth) return true;
+ if (showSecondMonth) {
+ const rightMonthStart = addMonths({
+ date: startOfLeftVisibleMonth,
+ n: 1
+ });
+ const rightYear = rightMonthStart.getFullYear();
+ const rightMonth = rightMonthStart.getMonth();
+ return year === rightYear && month === rightMonth;
+ }
+ return false;
+};
\ No newline at end of file