diff --git a/packages/time/src/calendar/date-core.ts b/packages/time/src/calendar/date-core.ts index a4dc8092..2bf416f8 100644 --- a/packages/time/src/calendar/date-core.ts +++ b/packages/time/src/calendar/date-core.ts @@ -54,12 +54,25 @@ export interface DateCoreOptions { viewMode: ViewMode /** Optional locale for date formatting. Uses a BCP 47 language tag. */ locale?: Intl.UnicodeBCP47LocaleIdentifier + /** + * Optional first day of the week (ISO: 1=Mon … 7=Sun). Overrides the + * locale-derived first day across all views when provided. + */ + weekStartsOn?: number /** Optional time zone specification. */ timeZone?: Temporal.TimeZoneLike /** Optional calendar system to be used. */ calendar?: Temporal.CalendarLike /** Optional range of dates to be used. */ range?: DateRange + /** + * When `true`, the month view always spans a fixed 6 week-rows (the maximum + * any month can occupy) instead of the natural 4/5/6, padding with leading + * days of the following month. Keeps the grid height stable across months. + * Only affects the `month` view mode; ignored for week/day/workWeek. + * @default false + */ + fixedWeeks?: boolean /** Optional date formatter. */ dateFormatter?: Intl.DateTimeFormat /** Optional time formatter. */ @@ -70,9 +83,12 @@ export interface DateCoreOptions { export interface ParsedDateCoreOptions extends Omit< Required, - 'range' | 'dateFormatter' | 'timeFormatter' | 'dateTimeFormatter' + 'range' | 'fixedWeeks' | 'weekStartsOn' | 'dateFormatter' | 'timeFormatter' | 'dateTimeFormatter' > { range: ParsedDateRange + fixedWeeks?: boolean + /** ISO 1=Mon … 7=Sun, or undefined to derive from the locale. */ + weekStartsOn?: number } export abstract class DateCore { @@ -153,6 +169,7 @@ export abstract class DateCore { return getFirstDayOfWeek( this.store.state.currentPeriod.toString(), this.options.locale, + this.options.weekStartsOn, ) } @@ -194,6 +211,21 @@ export abstract class DateCore { 7) % 7 end = lastDayOfMonth.add({ days: 6 - lastDayOfMonthWeekDay }) + // Pad short months up to a fixed 6 week-rows so the grid never shifts. + // Only extends (never truncates), so multi-month spans (>6 weeks) are + // left untouched. + if (this.options.fixedWeeks) { + const FIXED_WEEKS = 6 + // start and end are both week-boundary aligned, so the span is a whole + // number of weeks; round defensively so a fractional value can never + // reach Temporal's integer-only `add`. + const spanWeeks = Math.round( + (start.until(end, { largestUnit: 'day' }).days + 1) / 7, + ) + if (spanWeeks < FIXED_WEEKS) { + end = end.add({ weeks: FIXED_WEEKS - spanWeeks }) + } + } break } case 'week': { diff --git a/packages/time/src/utils/weekUtils.ts b/packages/time/src/utils/weekUtils.ts index 7bd05cc5..f8cef44d 100644 --- a/packages/time/src/utils/weekUtils.ts +++ b/packages/time/src/utils/weekUtils.ts @@ -13,15 +13,18 @@ export function getFirstDayOfMonth(yearMonth: string): Temporal.PlainDate { } /** - * Get the first day of the week for a given date string and locale + * Get the first day of the week for a given date string and locale. + * + * `weekStartsOn` (ISO: 1=Mon … 7=Sun) overrides the locale-derived first day + * when provided; otherwise the locale's own convention is used. */ export function getFirstDayOfWeek( dateString: string, locale: string, + weekStartsOn?: number, ): Temporal.PlainDate { const date = Temporal.PlainDate.from(dateString) - const weekInfo = getWeekInfo(locale) - const firstDayOfWeek = weekInfo.firstDay + const firstDayOfWeek = weekStartsOn ?? getWeekInfo(locale).firstDay const dayOfWeek = date.dayOfWeek const daysToSubtract = (dayOfWeek - firstDayOfWeek + 7) % 7