Files
ServerKit/frontend/src/lib/dateMath.js
T
Juan Denis 7c8519772f Add accessible form controls and field wiring
FormField now hands its control the id, aria-describedby, aria-invalid
and aria-required through context, so every settings form announces
its hint and validation without per-call wiring. IconButton gets a real
tooltip instead of title=, ProgressBar is a labelled progressbar with
tones and an indeterminate state, and Button takes `loading` without
changing width.

New controls the panel lacked: NumberField, Combobox (single, multiple,
creatable), TagInput, Calendar with DatePicker and DateRangePicker,
PinInput, Slider, RadioGroup, Stepper, TreeView, and FileDropzone with
FileUploadList. Rounding, date and file-type rules live in lib/ with
unit tests. All of them render on /style-guide/controls.
2026-10-25 23:18:24 -04:00

104 lines
3.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Calendar-day arithmetic behind Calendar / DatePicker / DateRangePicker.
//
// Every value here is a LOCAL calendar day: a Date at local midnight. A date
// picker picks a day, not an instant, so nothing in this file touches UTC —
// `new Date('2026-10-25')` would parse as UTC midnight and render as the 24th
// west of Greenwich, which is the classic date-picker off-by-one.
export function startOfDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
}
export function addDays(date, days) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
}
/** Month arithmetic that clamps the day: Jan 31 + 1 month is Feb 28/29. */
export function addMonths(date, months) {
const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), lastDay));
}
export function startOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1);
}
export function endOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
}
export function isSameDay(a, b) {
return Boolean(a && b)
&& a.getFullYear() === b.getFullYear()
&& a.getMonth() === b.getMonth()
&& a.getDate() === b.getDate();
}
export function isSameMonth(a, b) {
return Boolean(a && b) && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
}
/** Negative, zero or positive, comparing calendar days only. */
export function compareDays(a, b) {
return startOfDay(a).getTime() - startOfDay(b).getTime();
}
export function isBefore(a, b) { return compareDays(a, b) < 0; }
export function isAfter(a, b) { return compareDays(a, b) > 0; }
export function isWithinRange(day, start, end) {
if (!start || !end) return false;
return compareDays(day, start) >= 0 && compareDays(day, end) <= 0;
}
/** First day of the week containing `date`; weekStartsOn is 0 (Sun) .. 6 (Sat). */
export function startOfWeek(date, weekStartsOn = 0) {
const offset = (date.getDay() - weekStartsOn + 7) % 7;
return addDays(date, -offset);
}
export function endOfWeek(date, weekStartsOn = 0) {
return addDays(startOfWeek(date, weekStartsOn), 6);
}
/**
* The 6×7 grid shown for `month`: always six rows so the popover never
* changes height while paging through months.
*/
export function monthGrid(month, weekStartsOn = 0) {
const first = startOfWeek(startOfMonth(month), weekStartsOn);
return Array.from({ length: 6 }, (_, row) => (
Array.from({ length: 7 }, (_, col) => addDays(first, row * 7 + col))
));
}
/** Clamp a day into [min, max]; either bound may be null. */
export function clampDay(day, min, max) {
if (min && isBefore(day, min)) return startOfDay(min);
if (max && isAfter(day, max)) return startOfDay(max);
return day;
}
/** Order a picked pair so start <= end. */
export function orderRange(a, b) {
return isAfter(a, b) ? { start: b, end: a } : { start: a, end: b };
}
/** `YYYY-MM-DD` for a local day (the wire format for date-only fields). */
export function toISODate(date) {
if (!date) return '';
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${date.getFullYear()}-${month}-${day}`;
}
/** Parse `YYYY-MM-DD` as a LOCAL day; null when malformed. */
export function fromISODate(text) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text || '');
if (!match) return null;
const [, year, month, day] = match.map(Number);
const date = new Date(year, month - 1, day);
return date.getMonth() === month - 1 ? date : null;
}