@lekoala/date-picker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/custom-elements.json +366 -0
  4. package/dist/date-picker.css +344 -0
  5. package/dist/date-picker.js +2693 -0
  6. package/dist/date-picker.min.css +1 -0
  7. package/dist/date-picker.min.js +17 -0
  8. package/dist/date-picker.standalone.min.js +361 -0
  9. package/dist/types/calendar-model.d.ts +34 -0
  10. package/dist/types/calendar-model.d.ts.map +1 -0
  11. package/dist/types/date-calendar.d.ts +228 -0
  12. package/dist/types/date-calendar.d.ts.map +1 -0
  13. package/dist/types/date-field.d.ts +94 -0
  14. package/dist/types/date-field.d.ts.map +1 -0
  15. package/dist/types/date-picker.d.ts +258 -0
  16. package/dist/types/date-picker.d.ts.map +1 -0
  17. package/dist/types/date-range.d.ts +89 -0
  18. package/dist/types/date-range.d.ts.map +1 -0
  19. package/dist/types/date.d.ts +61 -0
  20. package/dist/types/date.d.ts.map +1 -0
  21. package/dist/types/define.d.ts +2 -0
  22. package/dist/types/define.d.ts.map +1 -0
  23. package/dist/types/index.d.ts +9 -0
  24. package/dist/types/index.d.ts.map +1 -0
  25. package/dist/types/intl.d.ts +33 -0
  26. package/dist/types/intl.d.ts.map +1 -0
  27. package/dist/types/locales/ar.d.ts +22 -0
  28. package/dist/types/locales/ar.d.ts.map +1 -0
  29. package/dist/types/locales/de.d.ts +22 -0
  30. package/dist/types/locales/de.d.ts.map +1 -0
  31. package/dist/types/locales/en.d.ts +22 -0
  32. package/dist/types/locales/en.d.ts.map +1 -0
  33. package/dist/types/locales/es.d.ts +22 -0
  34. package/dist/types/locales/es.d.ts.map +1 -0
  35. package/dist/types/locales/fr.d.ts +22 -0
  36. package/dist/types/locales/fr.d.ts.map +1 -0
  37. package/dist/types/locales/hi.d.ts +22 -0
  38. package/dist/types/locales/hi.d.ts.map +1 -0
  39. package/dist/types/locales/id.d.ts +22 -0
  40. package/dist/types/locales/id.d.ts.map +1 -0
  41. package/dist/types/locales/it.d.ts +22 -0
  42. package/dist/types/locales/it.d.ts.map +1 -0
  43. package/dist/types/locales/ja.d.ts +22 -0
  44. package/dist/types/locales/ja.d.ts.map +1 -0
  45. package/dist/types/locales/ko.d.ts +22 -0
  46. package/dist/types/locales/ko.d.ts.map +1 -0
  47. package/dist/types/locales/nl.d.ts +22 -0
  48. package/dist/types/locales/nl.d.ts.map +1 -0
  49. package/dist/types/locales/pl.d.ts +22 -0
  50. package/dist/types/locales/pl.d.ts.map +1 -0
  51. package/dist/types/locales/pt-BR.d.ts +22 -0
  52. package/dist/types/locales/pt-BR.d.ts.map +1 -0
  53. package/dist/types/locales/pt-PT.d.ts +22 -0
  54. package/dist/types/locales/pt-PT.d.ts.map +1 -0
  55. package/dist/types/locales/ru.d.ts +22 -0
  56. package/dist/types/locales/ru.d.ts.map +1 -0
  57. package/dist/types/locales/tr.d.ts +22 -0
  58. package/dist/types/locales/tr.d.ts.map +1 -0
  59. package/dist/types/locales/zh-CN.d.ts +22 -0
  60. package/dist/types/locales/zh-CN.d.ts.map +1 -0
  61. package/dist/types/messages.d.ts +43 -0
  62. package/dist/types/messages.d.ts.map +1 -0
  63. package/dist/types/source.d.ts +28 -0
  64. package/dist/types/source.d.ts.map +1 -0
  65. package/docs/ACCESSIBILITY.md +128 -0
  66. package/docs/API.md +289 -0
  67. package/docs/ARCHITECTURE.md +183 -0
  68. package/docs/AT-CHECKLIST.md +63 -0
  69. package/docs/CALENDAR-INTEGRATION.md +70 -0
  70. package/docs/DECISIONS.md +40 -0
  71. package/docs/MIGRATION-FLATPICKR.md +92 -0
  72. package/docs/ROADMAP.md +105 -0
  73. package/docs/TESTING.md +73 -0
  74. package/docs/USE_CASES.md +128 -0
  75. package/package.json +174 -0
  76. package/src/calendar-model.js +80 -0
  77. package/src/date-calendar.js +800 -0
  78. package/src/date-field.js +203 -0
  79. package/src/date-picker.css +344 -0
  80. package/src/date-picker.js +1140 -0
  81. package/src/date-range.js +172 -0
  82. package/src/date.js +203 -0
  83. package/src/define.js +9 -0
  84. package/src/index.js +15 -0
  85. package/src/intl.js +155 -0
  86. package/src/locales/ar.js +20 -0
  87. package/src/locales/de.js +20 -0
  88. package/src/locales/en.js +20 -0
  89. package/src/locales/es.js +20 -0
  90. package/src/locales/fr.js +20 -0
  91. package/src/locales/hi.js +20 -0
  92. package/src/locales/id.js +20 -0
  93. package/src/locales/it.js +20 -0
  94. package/src/locales/ja.js +20 -0
  95. package/src/locales/ko.js +20 -0
  96. package/src/locales/nl.js +20 -0
  97. package/src/locales/pl.js +20 -0
  98. package/src/locales/pt-BR.js +20 -0
  99. package/src/locales/pt-PT.js +20 -0
  100. package/src/locales/ru.js +20 -0
  101. package/src/locales/tr.js +20 -0
  102. package/src/locales/zh-CN.js +20 -0
  103. package/src/messages.js +31 -0
  104. package/src/source.js +50 -0
@@ -0,0 +1,172 @@
1
+ import { compareDates, isDate } from "./date.js";
2
+
3
+ /** @typedef {{start:string, end:string}} DateRange */
4
+
5
+ /**
6
+ * Validate and normalize a range for display.
7
+ *
8
+ * - `null` and `{ start: "", end: "" }` reset the range;
9
+ * - `start` and `end` are canonical `YYYY-MM-DD` strings;
10
+ * - `end` may be empty (range in progress);
11
+ * - the range is never reordered here: an inversion or an `end` without a
12
+ * `start` is a programming error and is rejected.
13
+ * @param {{start?:string, end?:string}|null|undefined} range
14
+ * @returns {DateRange}
15
+ */
16
+ export function normalizeRange(range) {
17
+ if (range == null) return { start: "", end: "" };
18
+ if (typeof range !== "object") throw new TypeError("highlightedRange expects { start, end }");
19
+ const { start = "", end = "" } = range;
20
+ if (start && !isDate(start)) throw new TypeError(`Invalid range start: ${start}`);
21
+ if (end && !isDate(end)) throw new TypeError(`Invalid range end: ${end}`);
22
+ if (!start && end) throw new TypeError("highlightedRange cannot define an end without a start");
23
+ if (start && end && compareDates(end, start) < 0)
24
+ throw new TypeError("highlightedRange must be ordered; start must be on or before end");
25
+ return { start, end };
26
+ }
27
+
28
+ /**
29
+ * Position of one day within a displayable range.
30
+ *
31
+ * A single-day range (`start === end`) cannot be both endpoints at once, so it
32
+ * returns `"single"`; the caller renders it as both `data-range-start` and
33
+ * `data-range-end`. A start-only range (in-progress) returns `"start"`.
34
+ * @param {string} date
35
+ * @param {DateRange|null|undefined} range
36
+ * @returns {"" | "start" | "in" | "end" | "single"}
37
+ */
38
+ export function rangePosition(date, range) {
39
+ if (!range || !isDate(date)) return "";
40
+ const { start, end } = range;
41
+ if (!start || !isDate(start)) return "";
42
+ if (start === date) return end ? (end === date ? "single" : "start") : "start";
43
+ if (end && end === date) return "end";
44
+ if (end && compareDates(date, start) >= 0 && compareDates(date, end) <= 0) return "in";
45
+ return "";
46
+ }
47
+
48
+ /**
49
+ * Pure two-bound state machine for the shared-calendar range picker. Covers
50
+ * start/end state and the active-endpoint transition rules; it knows nothing
51
+ * about DOM, popups or async availability.
52
+ *
53
+ * Interaction contract (U9):
54
+ * - opening through a field targets that bound;
55
+ * - a pick outside a complete range extends the corresponding bound;
56
+ * - otherwise activating through `end` refuses a date before `start`;
57
+ * - activating through `start` commits `start`, moves the active endpoint to
58
+ * `end` and leaves the popup open for the second selection.
59
+ *
60
+ * The business range may be temporarily incomplete or inverted; it is the
61
+ * coordinator's job to forward only a displayable range to
62
+ * `calendar.highlightedRange`.
63
+ */
64
+ export class DateRangeController {
65
+ constructor() {
66
+ /** @type {"" | "start" | "end"} */
67
+ this.activeEndpoint = "";
68
+ this.start = "";
69
+ this.end = "";
70
+ }
71
+
72
+ /** @returns {{start:string, end:string}} */
73
+ get range() {
74
+ return { start: this.start, end: this.end };
75
+ }
76
+
77
+ /** Whether both bounds exist and are ordered (start <= end). */
78
+ get complete() {
79
+ return Boolean(this.start && this.end && compareDates(this.end, this.start) >= 0);
80
+ }
81
+
82
+ /** @param {"" | "start" | "end"} endpoint */
83
+ focus(endpoint) {
84
+ this.activeEndpoint = endpoint;
85
+ }
86
+
87
+ /**
88
+ * Process one grid activation through the active endpoint.
89
+ *
90
+ * `editable(bound)` reports whether a bound can receive a user pick. It
91
+ * guards every commit and the `start -> end` transition: when the next bound
92
+ * is uneditable the returned result carries `close: true` instead of leaving
93
+ * the popup targeting a bound that can never change. `status: "complete"`
94
+ * stays reserved for an actually ordered pair.
95
+ * @param {string} date
96
+ * @param {(bound: "start" | "end") => boolean} [editable]
97
+ * @returns {{status: "pending" | "complete" | "refused", endpoint: "start" | "end", close?: boolean}}
98
+ */
99
+ activate(date, editable = () => true) {
100
+ if (!isDate(date)) throw new TypeError(`Invalid range activation: ${date}`);
101
+ if (this.complete && (compareDates(date, this.start) < 0 || compareDates(date, this.end) > 0)) {
102
+ const endpoint = compareDates(date, this.start) < 0 ? "start" : "end";
103
+ if (!editable(endpoint)) return { status: "refused", endpoint };
104
+ this[endpoint] = date;
105
+ this.activeEndpoint = endpoint;
106
+ return { status: "complete", endpoint };
107
+ }
108
+ if (this.activeEndpoint === "end") {
109
+ if (this.start && compareDates(date, this.start) < 0) {
110
+ return { status: "refused", endpoint: "end" };
111
+ }
112
+ if (!editable("end")) return { status: "refused", endpoint: "end" };
113
+ this.end = date;
114
+ return { status: this.start ? "complete" : "pending", endpoint: "end" };
115
+ }
116
+ if (!editable("start")) return { status: "refused", endpoint: "start" };
117
+ this.start = date;
118
+ if (!editable("end")) {
119
+ // The range stays incomplete but must close: the next bound cannot
120
+ // receive the complementary pick.
121
+ return { status: "pending", endpoint: "start", close: true };
122
+ }
123
+ this.activeEndpoint = "end";
124
+ return { status: "pending", endpoint: "start" };
125
+ }
126
+ }
127
+
128
+ /** @param {string} a @param {string} b */
129
+ function later(a, b) {
130
+ if (!a) return b;
131
+ if (!b) return a;
132
+ return compareDates(a, b) >= 0 ? a : b;
133
+ }
134
+
135
+ /** @param {string} a @param {string} b */
136
+ function earlier(a, b) {
137
+ if (!a) return b;
138
+ if (!b) return a;
139
+ return compareDates(a, b) <= 0 ? a : b;
140
+ }
141
+
142
+ /**
143
+ * Link two date pickers without teaching either picker about its sibling.
144
+ * Effective constraint: end >= start and start <= end.
145
+ * @param {import("./date-picker.js").DatePickerElement} start
146
+ * @param {import("./date-picker.js").DatePickerElement} end
147
+ * @returns {() => void}
148
+ */
149
+ export function linkDateRange(start, end) {
150
+ const baseStartMax = start.max;
151
+ const baseEndMin = end.min;
152
+
153
+ const sync = () => {
154
+ const startValue = typeof start.value === "string" && isDate(start.value) ? start.value : "";
155
+ const endValue = typeof end.value === "string" && isDate(end.value) ? end.value : "";
156
+ end.min = later(baseEndMin, startValue);
157
+ start.max = earlier(baseStartMax, endValue);
158
+ void start.validate();
159
+ void end.validate();
160
+ };
161
+
162
+ start.addEventListener("valuechange", sync);
163
+ end.addEventListener("valuechange", sync);
164
+ sync();
165
+
166
+ return () => {
167
+ start.removeEventListener("valuechange", sync);
168
+ end.removeEventListener("valuechange", sync);
169
+ start.max = baseStartMax;
170
+ end.min = baseEndMin;
171
+ };
172
+ }
package/src/date.js ADDED
@@ -0,0 +1,203 @@
1
+ const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
2
+ const MONTH_RE = /^(\d{4})-(\d{2})$/;
3
+
4
+ /** @param {number} year */
5
+ export function isLeapYear(year) {
6
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
7
+ }
8
+
9
+ /** @param {number} year @param {number} month */
10
+ export function daysInMonth(year, month) {
11
+ if (month === 2) return isLeapYear(year) ? 29 : 28;
12
+ return [4, 6, 9, 11].includes(month) ? 30 : 31;
13
+ }
14
+
15
+ /**
16
+ * Parse one canonical civil date.
17
+ * @param {string} value
18
+ * @returns {{year:number, month:number, day:number} | null}
19
+ */
20
+ export function parseDate(value) {
21
+ const match = DATE_RE.exec(String(value || ""));
22
+ if (!match) return null;
23
+ const year = Number(match[1]);
24
+ const month = Number(match[2]);
25
+ const day = Number(match[3]);
26
+ if (year < 1 || year > 9999 || month < 1 || month > 12) return null;
27
+ if (day < 1 || day > daysInMonth(year, month)) return null;
28
+ return { year, month, day };
29
+ }
30
+
31
+ /** @param {string} value */
32
+ export function isDate(value) {
33
+ return parseDate(value) !== null;
34
+ }
35
+
36
+ /**
37
+ * Normalize YYYY-MM or YYYY-MM-DD to YYYY-MM.
38
+ * @param {string} value
39
+ * @returns {string}
40
+ */
41
+ export function monthKey(value) {
42
+ const text = String(value || "");
43
+ const monthMatch = MONTH_RE.exec(text);
44
+ if (monthMatch) {
45
+ const month = Number(monthMatch[2]);
46
+ if (month >= 1 && month <= 12) return `${monthMatch[1]}-${String(month).padStart(2, "0")}`;
47
+ }
48
+ const parsed = parseDate(text);
49
+ if (!parsed) throw new TypeError(`Invalid civil date/month: ${value}`);
50
+ return `${String(parsed.year).padStart(4, "0")}-${String(parsed.month).padStart(2, "0")}`;
51
+ }
52
+
53
+ /** @param {number} year @param {number} month @param {number} day */
54
+ export function toISODate(year, month, day) {
55
+ const value = `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
56
+ if (!isDate(value)) throw new TypeError(`Invalid civil date: ${value}`);
57
+ return value;
58
+ }
59
+
60
+ /** @param {{year:number, month:number, day:number}} parts */
61
+ function toUTCDate(parts) {
62
+ const date = new Date(0);
63
+ date.setUTCHours(12, 0, 0, 0);
64
+ date.setUTCFullYear(parts.year, parts.month - 1, parts.day);
65
+ return date;
66
+ }
67
+
68
+ /** @param {Date} date */
69
+ function fromUTCDate(date) {
70
+ return toISODate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
71
+ }
72
+
73
+ /** @param {string} value */
74
+ export function toIntlDate(value) {
75
+ const parsed = parseDate(value);
76
+ if (!parsed) throw new TypeError(`Invalid civil date: ${value}`);
77
+ return toUTCDate(parsed);
78
+ }
79
+
80
+ /** @param {string} a @param {string} b */
81
+ export function compareDates(a, b) {
82
+ if (!isDate(a) || !isDate(b)) throw new TypeError("compareDates() expects YYYY-MM-DD values");
83
+ return a === b ? 0 : a < b ? -1 : 1;
84
+ }
85
+
86
+ /** @param {string} value @param {number} amount */
87
+ export function addDays(value, amount) {
88
+ const parsed = parseDate(value);
89
+ if (!parsed || !Number.isInteger(amount))
90
+ throw new TypeError("addDays() expects a date and integer amount");
91
+ const date = toUTCDate(parsed);
92
+ date.setUTCDate(date.getUTCDate() + amount);
93
+ return fromUTCDate(date);
94
+ }
95
+
96
+ /** @param {string} value @param {number} amount */
97
+ export function addMonths(value, amount) {
98
+ const parsed = parseDate(value);
99
+ if (!parsed || !Number.isInteger(amount))
100
+ throw new TypeError("addMonths() expects a date and integer amount");
101
+ const absoluteMonth = parsed.year * 12 + (parsed.month - 1) + amount;
102
+ const year = Math.floor(absoluteMonth / 12);
103
+ const monthIndex = ((absoluteMonth % 12) + 12) % 12;
104
+ const month = monthIndex + 1;
105
+ const day = Math.min(parsed.day, daysInMonth(year, month));
106
+ return toISODate(year, month, day);
107
+ }
108
+
109
+ /** @param {string} value @param {number} amount */
110
+ export function addYears(value, amount) {
111
+ const parsed = parseDate(value);
112
+ if (!parsed || !Number.isInteger(amount))
113
+ throw new TypeError("addYears() expects a date and integer amount");
114
+ const year = parsed.year + amount;
115
+ const day = Math.min(parsed.day, daysInMonth(year, parsed.month));
116
+ return toISODate(year, parsed.month, day);
117
+ }
118
+
119
+ /** @param {string} month @param {number} amount */
120
+ export function shiftMonth(month, amount) {
121
+ const start = `${monthKey(month)}-01`;
122
+ return monthKey(addMonths(start, amount));
123
+ }
124
+
125
+ /** @param {string} value */
126
+ export function startOfMonth(value) {
127
+ return `${monthKey(value)}-01`;
128
+ }
129
+
130
+ /** @param {string} value */
131
+ export function endOfMonth(value) {
132
+ const parsed = parseDate(`${monthKey(value)}-01`);
133
+ if (!parsed) throw new TypeError(`Invalid month: ${value}`);
134
+ return toISODate(parsed.year, parsed.month, daysInMonth(parsed.year, parsed.month));
135
+ }
136
+
137
+ /** 0=Sunday ... 6=Saturday. @param {string} value */
138
+ export function dayOfWeek(value) {
139
+ const parsed = parseDate(value);
140
+ if (!parsed) throw new TypeError(`Invalid civil date: ${value}`);
141
+ return toUTCDate(parsed).getUTCDay();
142
+ }
143
+
144
+ /** @param {string} value @param {number} firstDay */
145
+ export function startOfWeek(value, firstDay = 1) {
146
+ if (!Number.isInteger(firstDay) || firstDay < 0 || firstDay > 6)
147
+ throw new RangeError("firstDay must be 0..6");
148
+ const offset = (dayOfWeek(value) - firstDay + 7) % 7;
149
+ return addDays(value, -offset);
150
+ }
151
+
152
+ /** @param {string} value @param {number} firstDay */
153
+ export function endOfWeek(value, firstDay = 1) {
154
+ return addDays(startOfWeek(value, firstDay), 6);
155
+ }
156
+
157
+ /**
158
+ * True civil weeks covering the anchor month: 4..6 full rows, never padded.
159
+ * @param {string} value YYYY-MM or YYYY-MM-DD
160
+ * @param {{firstDay?: number}} [options]
161
+ */
162
+ export function getMonthWeeks(value, options = {}) {
163
+ const firstDay = options.firstDay ?? 1;
164
+ const start = startOfWeek(startOfMonth(value), firstDay);
165
+ const end = endOfWeek(endOfMonth(value), firstDay);
166
+ /** @type {string[][]} */
167
+ const weeks = [];
168
+ let cursor = start;
169
+ while (compareDates(cursor, end) <= 0) {
170
+ const week = [];
171
+ for (let i = 0; i < 7; i++) week.push(addDays(cursor, i));
172
+ weeks.push(week);
173
+ cursor = addDays(cursor, 7);
174
+ }
175
+ return weeks;
176
+ }
177
+
178
+ /** @param {string} value @param {string} [min] @param {string} [max] */
179
+ export function clampDate(value, min = "", max = "") {
180
+ if (!isDate(value)) throw new TypeError(`Invalid civil date: ${value}`);
181
+ if (min && compareDates(value, min) < 0) return min;
182
+ if (max && compareDates(value, max) > 0) return max;
183
+ return value;
184
+ }
185
+
186
+ /** Local civil date; no timezone conversion of the selected date model. */
187
+ export function todayISO() {
188
+ const now = new Date();
189
+ return toISODate(now.getFullYear(), now.getMonth() + 1, now.getDate());
190
+ }
191
+
192
+ /** ISO-8601 week number. @param {string} value */
193
+ export function isoWeekNumber(value) {
194
+ const parsed = parseDate(value);
195
+ if (!parsed) throw new TypeError(`Invalid civil date: ${value}`);
196
+ const date = toUTCDate(parsed);
197
+ const day = date.getUTCDay() || 7;
198
+ date.setUTCDate(date.getUTCDate() + 4 - day);
199
+ const yearStart = new Date(0);
200
+ yearStart.setUTCHours(12, 0, 0, 0);
201
+ yearStart.setUTCFullYear(date.getUTCFullYear(), 0, 1);
202
+ return Math.ceil(((date.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
203
+ }
package/src/define.js ADDED
@@ -0,0 +1,9 @@
1
+ import { DateCalendarElement } from "./date-calendar.js";
2
+ import { DatePickerElement } from "./date-picker.js";
3
+
4
+ export function defineDatePicker() {
5
+ if (!customElements.get("date-calendar")) customElements.define("date-calendar", DateCalendarElement);
6
+ if (!customElements.get("date-picker")) customElements.define("date-picker", DatePickerElement);
7
+ }
8
+
9
+ defineDatePicker();
package/src/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export { CalendarModel } from "./calendar-model.js";
2
+ export * as dates from "./date.js";
3
+ export { DateCalendarElement } from "./date-calendar.js";
4
+ export { DatePickerElement } from "./date-picker.js";
5
+ export { linkDateRange, normalizeRange, rangePosition } from "./date-range.js";
6
+ export {
7
+ createDateAdapter,
8
+ formatLongDate,
9
+ formatMonthYear,
10
+ monthNames,
11
+ resolveLocale,
12
+ weekdayNames,
13
+ } from "./intl.js";
14
+ export { DEFAULT_MESSAGES, getDefaultMessages, setDefaultMessages } from "./messages.js";
15
+ export { createFetchSource, normalizeDateStates } from "./source.js";
package/src/intl.js ADDED
@@ -0,0 +1,155 @@
1
+ import { isDate, parseDate, toIntlDate, toISODate } from "./date.js";
2
+
3
+ const BIDI = /[\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069]/g;
4
+
5
+ /** @param {string} [locale] */
6
+ export function resolveLocale(locale = "") {
7
+ if (locale) return locale;
8
+ if (typeof document !== "undefined" && document.documentElement.lang) return document.documentElement.lang;
9
+ if (typeof navigator !== "undefined" && navigator.language) return navigator.language;
10
+ return "en-US";
11
+ }
12
+
13
+ /** @param {string} locale */
14
+ function digitMap(locale) {
15
+ const formatter = new Intl.NumberFormat(locale, { useGrouping: false });
16
+ const map = new Map();
17
+ for (let i = 0; i <= 9; i++) map.set(formatter.format(i), String(i));
18
+ return map;
19
+ }
20
+
21
+ /** @param {string} text @param {string} locale */
22
+ function normalizeDigits(text, locale) {
23
+ let result = String(text || "")
24
+ .replace(BIDI, "")
25
+ .trim();
26
+ for (const [local, ascii] of digitMap(locale)) result = result.split(local).join(ascii);
27
+ return result;
28
+ }
29
+
30
+ /** @param {string} value @param {string} [locale] */
31
+ export function formatLongDate(value, locale = "") {
32
+ const resolved = resolveLocale(locale);
33
+ return new Intl.DateTimeFormat(resolved, {
34
+ calendar: "gregory",
35
+ weekday: "long",
36
+ year: "numeric",
37
+ month: "long",
38
+ day: "numeric",
39
+ timeZone: "UTC",
40
+ }).format(toIntlDate(value));
41
+ }
42
+
43
+ /** @param {string} value @param {string} [locale] */
44
+ export function formatMonthYear(value, locale = "") {
45
+ const resolved = resolveLocale(locale);
46
+ return new Intl.DateTimeFormat(resolved, {
47
+ calendar: "gregory",
48
+ year: "numeric",
49
+ month: "long",
50
+ timeZone: "UTC",
51
+ }).format(toIntlDate(`${value.slice(0, 7)}-15`));
52
+ }
53
+
54
+ /** @param {string} [locale] @param {"long"|"short"} [style] */
55
+ export function monthNames(locale = "", style = "long") {
56
+ const resolved = resolveLocale(locale);
57
+ const formatter = new Intl.DateTimeFormat(resolved, { calendar: "gregory", month: style, timeZone: "UTC" });
58
+ return Array.from({ length: 12 }, (_, index) =>
59
+ formatter.format(toIntlDate(`2026-${String(index + 1).padStart(2, "0")}-15`)),
60
+ );
61
+ }
62
+
63
+ /**
64
+ * @param {string} [locale]
65
+ * @param {number} [firstDay]
66
+ * @param {"long"|"short"|"narrow"} [style]
67
+ */
68
+ export function weekdayNames(locale = "", firstDay = 1, style = "short") {
69
+ const resolved = resolveLocale(locale);
70
+ const formatter = new Intl.DateTimeFormat(resolved, {
71
+ calendar: "gregory",
72
+ weekday: style,
73
+ timeZone: "UTC",
74
+ });
75
+ // 2026-01-04 is a Sunday.
76
+ const sunday = toIntlDate("2026-01-04");
77
+ return Array.from({ length: 7 }, (_, index) => {
78
+ const date = new Date(sunday);
79
+ date.setUTCDate(sunday.getUTCDate() + ((firstDay + index) % 7));
80
+ return formatter.format(date);
81
+ });
82
+ }
83
+
84
+ /** @param {string} value */
85
+ function escapeRegExp(value) {
86
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
87
+ }
88
+
89
+ /**
90
+ * Localized, editable numeric date adapter. The canonical value remains YYYY-MM-DD.
91
+ * @param {string} [locale]
92
+ */
93
+ export function createDateAdapter(locale = "") {
94
+ const resolved = resolveLocale(locale);
95
+ const formatter = new Intl.DateTimeFormat(resolved, {
96
+ calendar: "gregory",
97
+ year: "numeric",
98
+ month: "2-digit",
99
+ day: "2-digit",
100
+ timeZone: "UTC",
101
+ });
102
+ const sample = toIntlDate("2006-11-22");
103
+ const parts = formatter.formatToParts(sample).filter((part) => part.type !== "literal" || part.value);
104
+ const order = parts.filter((part) => ["day", "month", "year"].includes(part.type)).map((part) => part.type);
105
+ const placeholder = parts
106
+ .map((part) => {
107
+ if (part.type === "day") return "DD";
108
+ if (part.type === "month") return "MM";
109
+ if (part.type === "year") return "YYYY";
110
+ return part.value.replace(BIDI, "");
111
+ })
112
+ .join("");
113
+ const pattern = parts
114
+ .map((part) => {
115
+ if (part.type === "day" || part.type === "month") return "(\\d{1,2})";
116
+ if (part.type === "year") return "(\\d{4})";
117
+ const literal = normalizeDigits(part.value, resolved).replace(/\s+/g, " ");
118
+ return escapeRegExp(literal).replace(/\\ /g, "\\s*");
119
+ })
120
+ .join("");
121
+ const regex = new RegExp(`^\\s*${pattern}\\s*$`);
122
+
123
+ return {
124
+ locale: resolved,
125
+ placeholder,
126
+ /** @param {string} value */
127
+ format(value) {
128
+ if (!isDate(value)) return "";
129
+ return formatter.format(toIntlDate(value));
130
+ },
131
+ /** @param {string} text @returns {string} */
132
+ parse(text) {
133
+ const normalized = normalizeDigits(text, resolved);
134
+ if (isDate(normalized)) return normalized;
135
+ const match = regex.exec(normalized);
136
+ if (!match) return "";
137
+ /** @type {Record<string, number>} */
138
+ const values = {};
139
+ order.forEach((type, index) => {
140
+ values[type] = Number(match[index + 1]);
141
+ });
142
+ if (!values.year || !values.month || !values.day) return "";
143
+ try {
144
+ return toISODate(values.year, values.month, values.day);
145
+ } catch {
146
+ return "";
147
+ }
148
+ },
149
+ };
150
+ }
151
+
152
+ /** @param {string} value */
153
+ export function dateParts(value) {
154
+ return parseDate(value);
155
+ }
@@ -0,0 +1,20 @@
1
+ export default {
2
+ chooseDate: "اختيار تاريخ",
3
+ changeDate: "تغيير التاريخ",
4
+ previousMonth: "الشهر السابق",
5
+ nextMonth: "الشهر التالي",
6
+ month: "الشهر",
7
+ year: "السنة",
8
+ calendar: "اختيار تاريخ",
9
+ unavailable: "غير متاح",
10
+ invalidDate: "أدخل تاريخًا صالحًا",
11
+ unavailableDate: "هذا التاريخ غير متاح",
12
+ formatHint: "التنسيق",
13
+ week: "الأسبوع",
14
+ rangeStart: "بداية النطاق",
15
+ rangeInRange: "داخل النطاق",
16
+ rangeEnd: "نهاية النطاق",
17
+ rangeSingle: "نطاق يوم واحد",
18
+ rangeOrderStart: "يجب أن تكون البداية قبل النهاية أو مساوية لها",
19
+ rangeOrderEnd: "يجب أن تكون النهاية بعد البداية أو مساوية لها",
20
+ };
@@ -0,0 +1,20 @@
1
+ export default {
2
+ chooseDate: "Datum auswählen",
3
+ changeDate: "Datum ändern",
4
+ previousMonth: "Vorheriger Monat",
5
+ nextMonth: "Nächster Monat",
6
+ month: "Monat",
7
+ year: "Jahr",
8
+ calendar: "Datum auswählen",
9
+ unavailable: "Nicht verfügbar",
10
+ invalidDate: "Gültiges Datum eingeben",
11
+ unavailableDate: "Dieses Datum ist nicht verfügbar",
12
+ formatHint: "Format",
13
+ week: "Woche",
14
+ rangeStart: "Bereichsbeginn",
15
+ rangeInRange: "Im Bereich",
16
+ rangeEnd: "Bereichsende",
17
+ rangeSingle: "Eintägiger Bereich",
18
+ rangeOrderStart: "Start muss vor oder am Ende liegen",
19
+ rangeOrderEnd: "Ende muss nach oder am Start liegen",
20
+ };
@@ -0,0 +1,20 @@
1
+ export default {
2
+ chooseDate: "Choose date",
3
+ changeDate: "Change date",
4
+ previousMonth: "Previous month",
5
+ nextMonth: "Next month",
6
+ month: "Month",
7
+ year: "Year",
8
+ calendar: "Choose a date",
9
+ unavailable: "Unavailable",
10
+ invalidDate: "Enter a valid date",
11
+ unavailableDate: "This date is unavailable",
12
+ formatHint: "Format",
13
+ week: "Week",
14
+ rangeStart: "Range start",
15
+ rangeInRange: "Inside range",
16
+ rangeEnd: "Range end",
17
+ rangeSingle: "Range of one day",
18
+ rangeOrderStart: "Start must be on or before end",
19
+ rangeOrderEnd: "End must be on or after start",
20
+ };
@@ -0,0 +1,20 @@
1
+ export default {
2
+ chooseDate: "Elegir fecha",
3
+ changeDate: "Cambiar fecha",
4
+ previousMonth: "Mes anterior",
5
+ nextMonth: "Mes siguiente",
6
+ month: "Mes",
7
+ year: "Año",
8
+ calendar: "Elegir una fecha",
9
+ unavailable: "No disponible",
10
+ invalidDate: "Introduce una fecha válida",
11
+ unavailableDate: "Esta fecha no está disponible",
12
+ formatHint: "Formato",
13
+ week: "Semana",
14
+ rangeStart: "Inicio del rango",
15
+ rangeInRange: "Dentro del rango",
16
+ rangeEnd: "Fin del rango",
17
+ rangeSingle: "Rango de un día",
18
+ rangeOrderStart: "El inicio debe ser anterior o igual al final",
19
+ rangeOrderEnd: "El final debe ser posterior o igual al inicio",
20
+ };
@@ -0,0 +1,20 @@
1
+ export default {
2
+ chooseDate: "Choisir une date",
3
+ changeDate: "Modifier la date",
4
+ previousMonth: "Mois précédent",
5
+ nextMonth: "Mois suivant",
6
+ month: "Mois",
7
+ year: "Année",
8
+ calendar: "Choisir une date",
9
+ unavailable: "Indisponible",
10
+ invalidDate: "Saisissez une date valide",
11
+ unavailableDate: "Cette date n’est pas disponible",
12
+ formatHint: "Format",
13
+ week: "Semaine",
14
+ rangeStart: "Début de la plage",
15
+ rangeInRange: "Dans la plage",
16
+ rangeEnd: "Fin de la plage",
17
+ rangeSingle: "Plage d’un jour",
18
+ rangeOrderStart: "Le début doit être antérieur ou égal à la fin",
19
+ rangeOrderEnd: "La fin doit être postérieure ou égale au début",
20
+ };
@@ -0,0 +1,20 @@
1
+ export default {
2
+ chooseDate: "तारीख़ चुनें",
3
+ changeDate: "तारीख़ बदलें",
4
+ previousMonth: "पिछला महीना",
5
+ nextMonth: "अगला महीना",
6
+ month: "महीना",
7
+ year: "वर्ष",
8
+ calendar: "कोई तारीख़ चुनें",
9
+ unavailable: "अनुपलब्ध",
10
+ invalidDate: "एक मान्य तारीख़ दर्ज करें",
11
+ unavailableDate: "यह तारीख़ उपलब्ध नहीं है",
12
+ formatHint: "प्रारूप",
13
+ week: "सप्ताह",
14
+ rangeStart: "सीमा आरंभ",
15
+ rangeInRange: "सीमा के अंदर",
16
+ rangeEnd: "सीमा अंत",
17
+ rangeSingle: "एक दिन की सीमा",
18
+ rangeOrderStart: "प्रारंभ अंत से पहले या उसके बराबर होना चाहिए",
19
+ rangeOrderEnd: "अंत प्रारंभ के बाद या उसके बराबर होना चाहिए",
20
+ };