@kalyx/core 1.0.0-rc.6 → 1.0.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.
- package/CHANGELOG.md +286 -0
- package/README.md +6 -2
- package/dist/index.cjs +133 -253
- package/dist/index.d.cts +51 -21
- package/dist/index.d.ts +51 -21
- package/dist/index.js +132 -260
- package/package.json +5 -6
package/dist/index.d.ts
CHANGED
|
@@ -15,7 +15,8 @@ type ISODateString = string;
|
|
|
15
15
|
* { before: '2026-01-01T00:00:00.000Z' }, // disable before Jan 1
|
|
16
16
|
* { after: '2026-12-31T00:00:00.000Z' }, // disable after Dec 31
|
|
17
17
|
* { date: '2026-06-15T00:00:00.000Z' }, // disable a specific date
|
|
18
|
-
* { dayOfWeek: [0, 6] },
|
|
18
|
+
* { dayOfWeek: [0, 6] }, // disable weekends
|
|
19
|
+
* { filter: (iso) => holidays.has(iso) }, // programmatic filter
|
|
19
20
|
* ];
|
|
20
21
|
* ```
|
|
21
22
|
*/
|
|
@@ -27,6 +28,8 @@ type DisabledRule = {
|
|
|
27
28
|
after: ISODateString;
|
|
28
29
|
} | {
|
|
29
30
|
dayOfWeek: number[];
|
|
31
|
+
} | {
|
|
32
|
+
filter: (iso: ISODateString) => boolean;
|
|
30
33
|
};
|
|
31
34
|
/** Date range (RangePicker) */
|
|
32
35
|
interface DateRange {
|
|
@@ -108,23 +111,14 @@ interface CalendarOptions {
|
|
|
108
111
|
* midnight form while the grid iterates in UTC.
|
|
109
112
|
*/
|
|
110
113
|
timezone?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Always emit exactly six weeks (42 days). The default (`false`) emits 4–6 weeks
|
|
116
|
+
* depending on the month, breaking after the last week containing a current-month day.
|
|
117
|
+
* Setting `true` is useful for layouts that need a fixed-height grid.
|
|
118
|
+
*/
|
|
119
|
+
fixedWeeks?: boolean;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
/**
|
|
114
|
-
* DateAdapter implementation backed by date-fns.
|
|
115
|
-
* All operations run in UTC to avoid timezone interference.
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* ```ts
|
|
119
|
-
* import { DateFnsAdapter } from '@kalyx/core';
|
|
120
|
-
*
|
|
121
|
-
* DateFnsAdapter.format('2026-01-15T00:00:00.000Z', 'yyyy-MM-dd'); // "2026-01-15"
|
|
122
|
-
* DateFnsAdapter.addDays('2026-01-15T00:00:00.000Z', 7); // "2026-01-22T..."
|
|
123
|
-
* DateFnsAdapter.isSameDay('2026-01-15T00:00:00.000Z', '2026-01-15T23:59:59.000Z'); // true
|
|
124
|
-
* ```
|
|
125
|
-
*/
|
|
126
|
-
declare const DateFnsAdapter: DateAdapter;
|
|
127
|
-
|
|
128
122
|
/**
|
|
129
123
|
* Builds the calendar grid for the given month.
|
|
130
124
|
* Returns a 2D array (`CalendarGrid`) organized by week.
|
|
@@ -146,6 +140,16 @@ declare const DateFnsAdapter: DateAdapter;
|
|
|
146
140
|
* ```
|
|
147
141
|
*/
|
|
148
142
|
declare function getCalendarDays(monthISO: string, adapter: DateAdapter, options?: CalendarOptions): CalendarGrid;
|
|
143
|
+
/**
|
|
144
|
+
* Returns the ISO 8601 week number (1-53) of the given date. ISO weeks start on Monday;
|
|
145
|
+
* week 1 is the week containing the year's first Thursday. The computation works in UTC
|
|
146
|
+
* to match the calendar grid's iteration semantics.
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* getISOWeekNumber('2026-01-01T00:00:00.000Z'); // 1 (Jan 1 2026 is a Thursday → ISO week 1)
|
|
150
|
+
* getISOWeekNumber('2026-12-31T00:00:00.000Z'); // 53
|
|
151
|
+
*/
|
|
152
|
+
declare function getISOWeekNumber(iso: ISODateString): number;
|
|
149
153
|
/**
|
|
150
154
|
* Checks whether the given date matches any disable rule.
|
|
151
155
|
*/
|
|
@@ -199,7 +203,13 @@ declare function parseTimeString(input: string): TimeValue | null;
|
|
|
199
203
|
declare function formatTimeString(time: TimeValue, withSeconds?: boolean): string;
|
|
200
204
|
/**
|
|
201
205
|
* Converts a 24-hour value to 12-hour form.
|
|
202
|
-
*
|
|
206
|
+
*
|
|
207
|
+
* - `0` → `{ hours12: 12, period: 'AM' }` (midnight)
|
|
208
|
+
* - `12` → `{ hours12: 12, period: 'PM' }` (noon)
|
|
209
|
+
*
|
|
210
|
+
* Throws if `hours24` isn't an integer in `[0, 23]`. The previous silent
|
|
211
|
+
* modulo behaviour mapped invalid inputs (e.g. `24`, `25`, `-1`) onto
|
|
212
|
+
* arbitrary valid-looking outputs, which masked caller bugs.
|
|
203
213
|
*/
|
|
204
214
|
declare function to12Hour(hours24: number): {
|
|
205
215
|
hours12: number;
|
|
@@ -207,6 +217,10 @@ declare function to12Hour(hours24: number): {
|
|
|
207
217
|
};
|
|
208
218
|
/**
|
|
209
219
|
* Converts a 12-hour value to 24-hour form.
|
|
220
|
+
*
|
|
221
|
+
* Throws if `hours12` isn't an integer in `[1, 12]`. The previous silent
|
|
222
|
+
* arithmetic produced out-of-range outputs (e.g. `to24Hour(13, 'PM')` → `25`).
|
|
223
|
+
* `period` is constrained at the type level to `'AM' | 'PM'`.
|
|
210
224
|
*/
|
|
211
225
|
declare function to24Hour(hours12: number, period: 'AM' | 'PM'): number;
|
|
212
226
|
/**
|
|
@@ -215,9 +229,15 @@ declare function to24Hour(hours12: number, period: 'AM' | 'PM'): number;
|
|
|
215
229
|
declare function generateHours(format?: '12h' | '24h'): number[];
|
|
216
230
|
/**
|
|
217
231
|
* Builds a minutes list at the given step.
|
|
218
|
-
*
|
|
219
|
-
* step=
|
|
220
|
-
* step=
|
|
232
|
+
*
|
|
233
|
+
* - `step=1` → `[0, 1, 2, ..., 59]`
|
|
234
|
+
* - `step=15` → `[0, 15, 30, 45]`
|
|
235
|
+
* - `step=5` → `[0, 5, 10, ..., 55]`
|
|
236
|
+
* - `step=45` → `[0, 45]`
|
|
237
|
+
* - `step=60` → `[0]` (on-the-hour only)
|
|
238
|
+
*
|
|
239
|
+
* Steps above 60 are rejected because they always collapse to `[0]` — useful UX
|
|
240
|
+
* is impossible past that point.
|
|
221
241
|
*/
|
|
222
242
|
declare function generateMinutes(step?: number): number[];
|
|
223
243
|
/**
|
|
@@ -380,6 +400,16 @@ interface RangePickerLabels extends DatePickerLabels {
|
|
|
380
400
|
startInput: string;
|
|
381
401
|
endInput: string;
|
|
382
402
|
presetsGroup: string;
|
|
403
|
+
/**
|
|
404
|
+
* Screen-reader prompt appended after the start date is picked, telling the user
|
|
405
|
+
* the next click commits the end of the range.
|
|
406
|
+
*/
|
|
407
|
+
selectingEnd: string;
|
|
408
|
+
/**
|
|
409
|
+
* Screen-reader prefix announced when both endpoints are committed, e.g.
|
|
410
|
+
* `"Range selected: Jan 5, 2026 – Jan 12, 2026"`.
|
|
411
|
+
*/
|
|
412
|
+
rangeSelected: string;
|
|
383
413
|
}
|
|
384
414
|
interface TimePickerLabels {
|
|
385
415
|
timeInput: string;
|
|
@@ -397,4 +427,4 @@ declare const DEFAULT_RANGEPICKER_LABELS: RangePickerLabels;
|
|
|
397
427
|
declare const DEFAULT_TIMEPICKER_LABELS: TimePickerLabels;
|
|
398
428
|
declare const DEFAULT_DATETIMEPICKER_LABELS: DateTimePickerLabels;
|
|
399
429
|
|
|
400
|
-
export { type CalendarDay, type CalendarGrid, type CalendarOptions, type CalendarWeek, DEFAULT_DATEPICKER_LABELS, DEFAULT_DATETIMEPICKER_LABELS, DEFAULT_RANGEPICKER_LABELS, DEFAULT_TIMEPICKER_LABELS, type DateAdapter,
|
|
430
|
+
export { type CalendarDay, type CalendarGrid, type CalendarOptions, type CalendarWeek, DEFAULT_DATEPICKER_LABELS, DEFAULT_DATETIMEPICKER_LABELS, DEFAULT_RANGEPICKER_LABELS, DEFAULT_TIMEPICKER_LABELS, type DateAdapter, type DatePickerLabels, type DateRange, type DateTimePickerLabels, type DisabledRule, type ISODateString, type RangePickerLabels, type TimePickerLabels, type TimeValue, type WeekStartsOn, type WeekdayInfo, civilMidnightFromUtcDay, formatFullDate, formatInTimezone, formatMonthYear, formatTimeFromISO, formatTimeString, generateHours, generateMinutes, getCalendarDays, getISOWeekNumber, getMonthName, getTime, getTimeInTimezone, getTimezoneOffsetMinutes, getWeekdayNames, isDateDisabled, isSameDayInTimezone, isSameTime, maxDate, minDate, normalizeISO, parseInputValue, parseTimeString, setTime, setTimeInTimezone, startOfDayInTimezone, to12Hour, to24Hour, todayInTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,254 +1,3 @@
|
|
|
1
|
-
// src/adapters/date-fns.ts
|
|
2
|
-
import {
|
|
3
|
-
parseISO as parseISO2,
|
|
4
|
-
addDays as dfAddDays,
|
|
5
|
-
addMonths as dfAddMonths,
|
|
6
|
-
addYears as dfAddYears,
|
|
7
|
-
isBefore as dfIsBefore,
|
|
8
|
-
isAfter as dfIsAfter,
|
|
9
|
-
isValid as dfIsValid
|
|
10
|
-
} from "date-fns";
|
|
11
|
-
|
|
12
|
-
// src/utils/timezone.ts
|
|
13
|
-
import { parseISO } from "date-fns";
|
|
14
|
-
var formatterCache = /* @__PURE__ */ new Map();
|
|
15
|
-
function getCachedPartsFormatter(timeZone) {
|
|
16
|
-
const key = `parts:${timeZone}`;
|
|
17
|
-
let fmt = formatterCache.get(key);
|
|
18
|
-
if (!fmt) {
|
|
19
|
-
fmt = new Intl.DateTimeFormat("en-US", {
|
|
20
|
-
timeZone,
|
|
21
|
-
year: "numeric",
|
|
22
|
-
month: "2-digit",
|
|
23
|
-
day: "2-digit",
|
|
24
|
-
hour: "2-digit",
|
|
25
|
-
minute: "2-digit",
|
|
26
|
-
second: "2-digit",
|
|
27
|
-
hourCycle: "h23"
|
|
28
|
-
});
|
|
29
|
-
formatterCache.set(key, fmt);
|
|
30
|
-
}
|
|
31
|
-
return fmt;
|
|
32
|
-
}
|
|
33
|
-
function partsInTimezone(utc, timeZone) {
|
|
34
|
-
const dtf = getCachedPartsFormatter(timeZone);
|
|
35
|
-
const parts = Object.fromEntries(dtf.formatToParts(utc).map((p) => [p.type, p.value]));
|
|
36
|
-
return {
|
|
37
|
-
year: Number(parts.year),
|
|
38
|
-
month: Number(parts.month),
|
|
39
|
-
day: Number(parts.day),
|
|
40
|
-
// Some locales/engines return '24' instead of '0' at midnight
|
|
41
|
-
hour: parts.hour === "24" ? 0 : Number(parts.hour),
|
|
42
|
-
minute: Number(parts.minute),
|
|
43
|
-
second: Number(parts.second)
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
function formatInTimezone(iso, formatStr, timeZone) {
|
|
47
|
-
const p = partsInTimezone(parseISO(iso), timeZone);
|
|
48
|
-
const tokens = {
|
|
49
|
-
yyyy: String(p.year),
|
|
50
|
-
MM: String(p.month).padStart(2, "0"),
|
|
51
|
-
dd: String(p.day).padStart(2, "0"),
|
|
52
|
-
HH: String(p.hour).padStart(2, "0"),
|
|
53
|
-
mm: String(p.minute).padStart(2, "0"),
|
|
54
|
-
ss: String(p.second).padStart(2, "0")
|
|
55
|
-
};
|
|
56
|
-
let result = formatStr;
|
|
57
|
-
for (const [token, value] of Object.entries(tokens).sort((a, b) => b[0].length - a[0].length)) {
|
|
58
|
-
result = result.split(token).join(value);
|
|
59
|
-
}
|
|
60
|
-
return result;
|
|
61
|
-
}
|
|
62
|
-
function getTimezoneOffsetMinutes(iso, timeZone) {
|
|
63
|
-
const utc = parseISO(iso);
|
|
64
|
-
const p = partsInTimezone(utc, timeZone);
|
|
65
|
-
const asUtcEpoch = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
66
|
-
return Math.round((asUtcEpoch - utc.getTime()) / 6e4);
|
|
67
|
-
}
|
|
68
|
-
function startOfDayInTimezone(iso, timeZone) {
|
|
69
|
-
const utc = parseISO(iso);
|
|
70
|
-
const p = partsInTimezone(utc, timeZone);
|
|
71
|
-
const civilMidnightUtc = Date.UTC(p.year, p.month - 1, p.day, 0, 0, 0);
|
|
72
|
-
const midnightProbe = new Date(civilMidnightUtc).toISOString();
|
|
73
|
-
const offsetMinutes = getTimezoneOffsetMinutes(midnightProbe, timeZone);
|
|
74
|
-
return new Date(civilMidnightUtc - offsetMinutes * 6e4).toISOString();
|
|
75
|
-
}
|
|
76
|
-
function isSameDayInTimezone(a, b, timeZone) {
|
|
77
|
-
const pa = partsInTimezone(parseISO(a), timeZone);
|
|
78
|
-
const pb = partsInTimezone(parseISO(b), timeZone);
|
|
79
|
-
return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
|
|
80
|
-
}
|
|
81
|
-
function todayInTimezone(timeZone) {
|
|
82
|
-
return startOfDayInTimezone((/* @__PURE__ */ new Date()).toISOString(), timeZone);
|
|
83
|
-
}
|
|
84
|
-
function civilMidnightFromUtcDay(gridUtcIso, timeZone) {
|
|
85
|
-
const utc = parseISO(gridUtcIso);
|
|
86
|
-
const probe = new Date(
|
|
87
|
-
Date.UTC(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate(), 12, 0, 0)
|
|
88
|
-
).toISOString();
|
|
89
|
-
return startOfDayInTimezone(probe, timeZone);
|
|
90
|
-
}
|
|
91
|
-
function getTimeInTimezone(iso, timeZone) {
|
|
92
|
-
const p = partsInTimezone(parseISO(iso), timeZone);
|
|
93
|
-
return { hours: p.hour, minutes: p.minute, seconds: p.second };
|
|
94
|
-
}
|
|
95
|
-
function setTimeInTimezone(iso, partial, timeZone) {
|
|
96
|
-
const p = partsInTimezone(parseISO(iso), timeZone);
|
|
97
|
-
const targetHours = partial.hours ?? p.hour;
|
|
98
|
-
const targetMinutes = partial.minutes ?? p.minute;
|
|
99
|
-
const targetSeconds = partial.seconds ?? p.second;
|
|
100
|
-
const civilEpoch = Date.UTC(
|
|
101
|
-
p.year,
|
|
102
|
-
p.month - 1,
|
|
103
|
-
p.day,
|
|
104
|
-
targetHours,
|
|
105
|
-
targetMinutes,
|
|
106
|
-
targetSeconds
|
|
107
|
-
);
|
|
108
|
-
const probe1 = new Date(civilEpoch).toISOString();
|
|
109
|
-
const offset1 = getTimezoneOffsetMinutes(probe1, timeZone);
|
|
110
|
-
const realEpoch1 = civilEpoch - offset1 * 6e4;
|
|
111
|
-
const probe2 = new Date(realEpoch1).toISOString();
|
|
112
|
-
const offset2 = getTimezoneOffsetMinutes(probe2, timeZone);
|
|
113
|
-
const realEpoch2 = civilEpoch - offset2 * 6e4;
|
|
114
|
-
return new Date(realEpoch2).toISOString();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// src/adapters/date-fns.ts
|
|
118
|
-
function toDate(iso) {
|
|
119
|
-
return parseISO2(iso);
|
|
120
|
-
}
|
|
121
|
-
function toISO(date) {
|
|
122
|
-
return date.toISOString();
|
|
123
|
-
}
|
|
124
|
-
function normalize(value) {
|
|
125
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
126
|
-
return `${value}T00:00:00.000Z`;
|
|
127
|
-
}
|
|
128
|
-
return value;
|
|
129
|
-
}
|
|
130
|
-
function utcStartOfDay(d) {
|
|
131
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
132
|
-
}
|
|
133
|
-
function utcStartOfMonth(d) {
|
|
134
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
|
|
135
|
-
}
|
|
136
|
-
function utcEndOfMonth(d) {
|
|
137
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0, 23, 59, 59, 999));
|
|
138
|
-
}
|
|
139
|
-
function utcStartOfWeek(d, weekStartsOn) {
|
|
140
|
-
const day = d.getUTCDay();
|
|
141
|
-
const diff = (day - weekStartsOn + 7) % 7;
|
|
142
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - diff));
|
|
143
|
-
}
|
|
144
|
-
function utcEndOfWeek(d, weekStartsOn) {
|
|
145
|
-
const start = utcStartOfWeek(d, weekStartsOn);
|
|
146
|
-
return new Date(
|
|
147
|
-
Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate() + 6, 23, 59, 59, 999)
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
var DateFnsAdapter = {
|
|
151
|
-
parse(value) {
|
|
152
|
-
if (!value) return "";
|
|
153
|
-
return normalize(value);
|
|
154
|
-
},
|
|
155
|
-
format(iso, formatStr, timezone) {
|
|
156
|
-
if (timezone) {
|
|
157
|
-
return formatInTimezone(iso, formatStr, timezone);
|
|
158
|
-
}
|
|
159
|
-
const d = toDate(iso);
|
|
160
|
-
const tokens = {
|
|
161
|
-
yyyy: String(d.getUTCFullYear()),
|
|
162
|
-
MM: String(d.getUTCMonth() + 1).padStart(2, "0"),
|
|
163
|
-
dd: String(d.getUTCDate()).padStart(2, "0"),
|
|
164
|
-
HH: String(d.getUTCHours()).padStart(2, "0"),
|
|
165
|
-
mm: String(d.getUTCMinutes()).padStart(2, "0"),
|
|
166
|
-
ss: String(d.getUTCSeconds()).padStart(2, "0"),
|
|
167
|
-
M: String(d.getUTCMonth() + 1),
|
|
168
|
-
d: String(d.getUTCDate())
|
|
169
|
-
};
|
|
170
|
-
let result = formatStr;
|
|
171
|
-
for (const [token, value] of Object.entries(tokens).sort((a, b) => b[0].length - a[0].length)) {
|
|
172
|
-
result = result.split(token).join(value);
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
175
|
-
},
|
|
176
|
-
addDays(iso, n) {
|
|
177
|
-
return toISO(dfAddDays(toDate(iso), n));
|
|
178
|
-
},
|
|
179
|
-
addMonths(iso, n) {
|
|
180
|
-
return toISO(dfAddMonths(toDate(iso), n));
|
|
181
|
-
},
|
|
182
|
-
addYears(iso, n) {
|
|
183
|
-
return toISO(dfAddYears(toDate(iso), n));
|
|
184
|
-
},
|
|
185
|
-
isBefore(a, b) {
|
|
186
|
-
return dfIsBefore(toDate(a), toDate(b));
|
|
187
|
-
},
|
|
188
|
-
isAfter(a, b) {
|
|
189
|
-
return dfIsAfter(toDate(a), toDate(b));
|
|
190
|
-
},
|
|
191
|
-
isSameDay(a, b, timezone) {
|
|
192
|
-
if (!a || !b) return false;
|
|
193
|
-
if (timezone) {
|
|
194
|
-
return isSameDayInTimezone(a, b, timezone);
|
|
195
|
-
}
|
|
196
|
-
const da = toDate(a);
|
|
197
|
-
const db = toDate(b);
|
|
198
|
-
return da.getUTCFullYear() === db.getUTCFullYear() && da.getUTCMonth() === db.getUTCMonth() && da.getUTCDate() === db.getUTCDate();
|
|
199
|
-
},
|
|
200
|
-
isSameMonth(a, b) {
|
|
201
|
-
const da = toDate(a);
|
|
202
|
-
const db = toDate(b);
|
|
203
|
-
return da.getUTCFullYear() === db.getUTCFullYear() && da.getUTCMonth() === db.getUTCMonth();
|
|
204
|
-
},
|
|
205
|
-
startOfDay(iso, timezone) {
|
|
206
|
-
if (timezone) {
|
|
207
|
-
return startOfDayInTimezone(iso, timezone);
|
|
208
|
-
}
|
|
209
|
-
return toISO(utcStartOfDay(toDate(iso)));
|
|
210
|
-
},
|
|
211
|
-
startOfMonth(iso) {
|
|
212
|
-
return toISO(utcStartOfMonth(toDate(iso)));
|
|
213
|
-
},
|
|
214
|
-
endOfMonth(iso) {
|
|
215
|
-
return toISO(utcEndOfMonth(toDate(iso)));
|
|
216
|
-
},
|
|
217
|
-
startOfWeek(iso, weekStartsOn = 0) {
|
|
218
|
-
return toISO(utcStartOfWeek(toDate(iso), weekStartsOn));
|
|
219
|
-
},
|
|
220
|
-
endOfWeek(iso, weekStartsOn = 0) {
|
|
221
|
-
return toISO(utcEndOfWeek(toDate(iso), weekStartsOn));
|
|
222
|
-
},
|
|
223
|
-
now() {
|
|
224
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
225
|
-
},
|
|
226
|
-
today(timezone) {
|
|
227
|
-
if (timezone) {
|
|
228
|
-
return todayInTimezone(timezone);
|
|
229
|
-
}
|
|
230
|
-
return toISO(utcStartOfDay(/* @__PURE__ */ new Date()));
|
|
231
|
-
},
|
|
232
|
-
isValid(value) {
|
|
233
|
-
if (!value) return false;
|
|
234
|
-
const normalized = normalize(value);
|
|
235
|
-
const date = parseISO2(normalized);
|
|
236
|
-
return dfIsValid(date);
|
|
237
|
-
},
|
|
238
|
-
getYear(iso) {
|
|
239
|
-
return toDate(iso).getUTCFullYear();
|
|
240
|
-
},
|
|
241
|
-
getMonth(iso) {
|
|
242
|
-
return toDate(iso).getUTCMonth();
|
|
243
|
-
},
|
|
244
|
-
getDate(iso) {
|
|
245
|
-
return toDate(iso).getUTCDate();
|
|
246
|
-
},
|
|
247
|
-
getDay(iso) {
|
|
248
|
-
return toDate(iso).getUTCDay();
|
|
249
|
-
}
|
|
250
|
-
};
|
|
251
|
-
|
|
252
1
|
// src/utils/calendar.ts
|
|
253
2
|
function getCalendarDays(monthISO, adapter, options = {}) {
|
|
254
3
|
const {
|
|
@@ -259,7 +8,8 @@ function getCalendarDays(monthISO, adapter, options = {}) {
|
|
|
259
8
|
disabled = [],
|
|
260
9
|
range,
|
|
261
10
|
rangeHover,
|
|
262
|
-
timezone
|
|
11
|
+
timezone,
|
|
12
|
+
fixedWeeks = false
|
|
263
13
|
} = options;
|
|
264
14
|
const todayISO = today ?? adapter.today(timezone);
|
|
265
15
|
const monthStart = adapter.startOfMonth(monthISO);
|
|
@@ -289,12 +39,20 @@ function getCalendarDays(monthISO, adapter, options = {}) {
|
|
|
289
39
|
current = adapter.addDays(current, 1);
|
|
290
40
|
}
|
|
291
41
|
weeks.push(days);
|
|
292
|
-
if (!adapter.isSameMonth(current, monthISO) && week >= 3) {
|
|
42
|
+
if (!fixedWeeks && !adapter.isSameMonth(current, monthISO) && week >= 3) {
|
|
293
43
|
break;
|
|
294
44
|
}
|
|
295
45
|
}
|
|
296
46
|
return weeks;
|
|
297
47
|
}
|
|
48
|
+
function getISOWeekNumber(iso) {
|
|
49
|
+
const d = new Date(iso);
|
|
50
|
+
const utc = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
51
|
+
const isoDay = utc.getUTCDay() || 7;
|
|
52
|
+
utc.setUTCDate(utc.getUTCDate() + 4 - isoDay);
|
|
53
|
+
const yearStart = new Date(Date.UTC(utc.getUTCFullYear(), 0, 1));
|
|
54
|
+
return Math.ceil(((utc.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
|
|
55
|
+
}
|
|
298
56
|
function normalizeRangeForDisplay(range, hover, adapter, _timezone) {
|
|
299
57
|
if (!range) return { start: null, end: null };
|
|
300
58
|
const { start, end } = range;
|
|
@@ -338,6 +96,8 @@ function isDateDisabled(iso, rules, adapter) {
|
|
|
338
96
|
if (adapter.isAfter(iso, rule.after)) return true;
|
|
339
97
|
} else if ("dayOfWeek" in rule) {
|
|
340
98
|
if (rule.dayOfWeek.includes(adapter.getDay(iso))) return true;
|
|
99
|
+
} else if ("filter" in rule) {
|
|
100
|
+
if (rule.filter(iso)) return true;
|
|
341
101
|
}
|
|
342
102
|
}
|
|
343
103
|
return false;
|
|
@@ -415,12 +175,18 @@ function formatTimeString(time, withSeconds = false) {
|
|
|
415
175
|
return `${hh}:${mm}`;
|
|
416
176
|
}
|
|
417
177
|
function to12Hour(hours24) {
|
|
178
|
+
if (!Number.isInteger(hours24) || hours24 < 0 || hours24 > 23) {
|
|
179
|
+
throw new RangeError(`[to12Hour] hours24 must be an integer in [0, 23], got ${hours24}`);
|
|
180
|
+
}
|
|
418
181
|
const period = hours24 >= 12 ? "PM" : "AM";
|
|
419
182
|
let hours12 = hours24 % 12;
|
|
420
183
|
if (hours12 === 0) hours12 = 12;
|
|
421
184
|
return { hours12, period };
|
|
422
185
|
}
|
|
423
186
|
function to24Hour(hours12, period) {
|
|
187
|
+
if (!Number.isInteger(hours12) || hours12 < 1 || hours12 > 12) {
|
|
188
|
+
throw new RangeError(`[to24Hour] hours12 must be an integer in [1, 12], got ${hours12}`);
|
|
189
|
+
}
|
|
424
190
|
if (period === "AM") {
|
|
425
191
|
return hours12 === 12 ? 0 : hours12;
|
|
426
192
|
}
|
|
@@ -433,8 +199,8 @@ function generateHours(format = "24h") {
|
|
|
433
199
|
return Array.from({ length: 24 }, (_, i) => i);
|
|
434
200
|
}
|
|
435
201
|
function generateMinutes(step = 1) {
|
|
436
|
-
if (step < 1 || step >
|
|
437
|
-
throw new Error(`[generateMinutes] step must be between 1 and
|
|
202
|
+
if (step < 1 || step > 60) {
|
|
203
|
+
throw new Error(`[generateMinutes] step must be between 1 and 60, got ${step}`);
|
|
438
204
|
}
|
|
439
205
|
const result = [];
|
|
440
206
|
for (let i = 0; i < 60; i += step) {
|
|
@@ -460,13 +226,13 @@ function formatTimeFromISO(iso, format) {
|
|
|
460
226
|
}
|
|
461
227
|
|
|
462
228
|
// src/utils/locale.ts
|
|
463
|
-
var
|
|
229
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
464
230
|
function getCachedFormatter(locale, options) {
|
|
465
231
|
const key = `${locale}:${JSON.stringify(options)}`;
|
|
466
|
-
let fmt =
|
|
232
|
+
let fmt = formatterCache.get(key);
|
|
467
233
|
if (!fmt) {
|
|
468
234
|
fmt = new Intl.DateTimeFormat(locale, options);
|
|
469
|
-
|
|
235
|
+
formatterCache.set(key, fmt);
|
|
470
236
|
}
|
|
471
237
|
return fmt;
|
|
472
238
|
}
|
|
@@ -511,6 +277,110 @@ function formatFullDate(iso, locale = "en-US") {
|
|
|
511
277
|
}).format(date);
|
|
512
278
|
}
|
|
513
279
|
|
|
280
|
+
// src/utils/timezone.ts
|
|
281
|
+
var formatterCache2 = /* @__PURE__ */ new Map();
|
|
282
|
+
function getCachedPartsFormatter(timeZone) {
|
|
283
|
+
const key = `parts:${timeZone}`;
|
|
284
|
+
let fmt = formatterCache2.get(key);
|
|
285
|
+
if (!fmt) {
|
|
286
|
+
fmt = new Intl.DateTimeFormat("en-US", {
|
|
287
|
+
timeZone,
|
|
288
|
+
year: "numeric",
|
|
289
|
+
month: "2-digit",
|
|
290
|
+
day: "2-digit",
|
|
291
|
+
hour: "2-digit",
|
|
292
|
+
minute: "2-digit",
|
|
293
|
+
second: "2-digit",
|
|
294
|
+
hourCycle: "h23"
|
|
295
|
+
});
|
|
296
|
+
formatterCache2.set(key, fmt);
|
|
297
|
+
}
|
|
298
|
+
return fmt;
|
|
299
|
+
}
|
|
300
|
+
function partsInTimezone(utc, timeZone) {
|
|
301
|
+
const dtf = getCachedPartsFormatter(timeZone);
|
|
302
|
+
const parts = Object.fromEntries(dtf.formatToParts(utc).map((p) => [p.type, p.value]));
|
|
303
|
+
return {
|
|
304
|
+
year: Number(parts.year),
|
|
305
|
+
month: Number(parts.month),
|
|
306
|
+
day: Number(parts.day),
|
|
307
|
+
// Some locales/engines return '24' instead of '0' at midnight
|
|
308
|
+
hour: parts.hour === "24" ? 0 : Number(parts.hour),
|
|
309
|
+
minute: Number(parts.minute),
|
|
310
|
+
second: Number(parts.second)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function formatInTimezone(iso, formatStr, timeZone) {
|
|
314
|
+
const p = partsInTimezone(new Date(iso), timeZone);
|
|
315
|
+
const tokens = {
|
|
316
|
+
yyyy: String(p.year),
|
|
317
|
+
MM: String(p.month).padStart(2, "0"),
|
|
318
|
+
dd: String(p.day).padStart(2, "0"),
|
|
319
|
+
HH: String(p.hour).padStart(2, "0"),
|
|
320
|
+
mm: String(p.minute).padStart(2, "0"),
|
|
321
|
+
ss: String(p.second).padStart(2, "0")
|
|
322
|
+
};
|
|
323
|
+
let result = formatStr;
|
|
324
|
+
for (const [token, value] of Object.entries(tokens).sort((a, b) => b[0].length - a[0].length)) {
|
|
325
|
+
result = result.split(token).join(value);
|
|
326
|
+
}
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
function getTimezoneOffsetMinutes(iso, timeZone) {
|
|
330
|
+
const utc = new Date(iso);
|
|
331
|
+
const p = partsInTimezone(utc, timeZone);
|
|
332
|
+
const asUtcEpoch = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
333
|
+
return Math.round((asUtcEpoch - utc.getTime()) / 6e4);
|
|
334
|
+
}
|
|
335
|
+
function startOfDayInTimezone(iso, timeZone) {
|
|
336
|
+
const utc = new Date(iso);
|
|
337
|
+
const p = partsInTimezone(utc, timeZone);
|
|
338
|
+
const civilMidnightUtc = Date.UTC(p.year, p.month - 1, p.day, 0, 0, 0);
|
|
339
|
+
const midnightProbe = new Date(civilMidnightUtc).toISOString();
|
|
340
|
+
const offsetMinutes = getTimezoneOffsetMinutes(midnightProbe, timeZone);
|
|
341
|
+
return new Date(civilMidnightUtc - offsetMinutes * 6e4).toISOString();
|
|
342
|
+
}
|
|
343
|
+
function isSameDayInTimezone(a, b, timeZone) {
|
|
344
|
+
const pa = partsInTimezone(new Date(a), timeZone);
|
|
345
|
+
const pb = partsInTimezone(new Date(b), timeZone);
|
|
346
|
+
return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
|
|
347
|
+
}
|
|
348
|
+
function todayInTimezone(timeZone) {
|
|
349
|
+
return startOfDayInTimezone((/* @__PURE__ */ new Date()).toISOString(), timeZone);
|
|
350
|
+
}
|
|
351
|
+
function civilMidnightFromUtcDay(gridUtcIso, timeZone) {
|
|
352
|
+
const utc = new Date(gridUtcIso);
|
|
353
|
+
const probe = new Date(
|
|
354
|
+
Date.UTC(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate(), 12, 0, 0)
|
|
355
|
+
).toISOString();
|
|
356
|
+
return startOfDayInTimezone(probe, timeZone);
|
|
357
|
+
}
|
|
358
|
+
function getTimeInTimezone(iso, timeZone) {
|
|
359
|
+
const p = partsInTimezone(new Date(iso), timeZone);
|
|
360
|
+
return { hours: p.hour, minutes: p.minute, seconds: p.second };
|
|
361
|
+
}
|
|
362
|
+
function setTimeInTimezone(iso, partial, timeZone) {
|
|
363
|
+
const p = partsInTimezone(new Date(iso), timeZone);
|
|
364
|
+
const targetHours = partial.hours ?? p.hour;
|
|
365
|
+
const targetMinutes = partial.minutes ?? p.minute;
|
|
366
|
+
const targetSeconds = partial.seconds ?? p.second;
|
|
367
|
+
const civilEpoch = Date.UTC(
|
|
368
|
+
p.year,
|
|
369
|
+
p.month - 1,
|
|
370
|
+
p.day,
|
|
371
|
+
targetHours,
|
|
372
|
+
targetMinutes,
|
|
373
|
+
targetSeconds
|
|
374
|
+
);
|
|
375
|
+
const probe1 = new Date(civilEpoch).toISOString();
|
|
376
|
+
const offset1 = getTimezoneOffsetMinutes(probe1, timeZone);
|
|
377
|
+
const realEpoch1 = civilEpoch - offset1 * 6e4;
|
|
378
|
+
const probe2 = new Date(realEpoch1).toISOString();
|
|
379
|
+
const offset2 = getTimezoneOffsetMinutes(probe2, timeZone);
|
|
380
|
+
const realEpoch2 = civilEpoch - offset2 * 6e4;
|
|
381
|
+
return new Date(realEpoch2).toISOString();
|
|
382
|
+
}
|
|
383
|
+
|
|
514
384
|
// src/utils/labels.ts
|
|
515
385
|
var DEFAULT_DATEPICKER_LABELS = {
|
|
516
386
|
triggerOpen: "Open calendar",
|
|
@@ -528,7 +398,9 @@ var DEFAULT_RANGEPICKER_LABELS = {
|
|
|
528
398
|
popoverLabel: "Choose date range",
|
|
529
399
|
startInput: "Start date",
|
|
530
400
|
endInput: "End date",
|
|
531
|
-
presetsGroup: "Date range presets"
|
|
401
|
+
presetsGroup: "Date range presets",
|
|
402
|
+
selectingEnd: "Now select end date",
|
|
403
|
+
rangeSelected: "Range selected"
|
|
532
404
|
};
|
|
533
405
|
var DEFAULT_TIMEPICKER_LABELS = {
|
|
534
406
|
timeInput: "Time",
|
|
@@ -548,7 +420,6 @@ export {
|
|
|
548
420
|
DEFAULT_DATETIMEPICKER_LABELS,
|
|
549
421
|
DEFAULT_RANGEPICKER_LABELS,
|
|
550
422
|
DEFAULT_TIMEPICKER_LABELS,
|
|
551
|
-
DateFnsAdapter,
|
|
552
423
|
civilMidnightFromUtcDay,
|
|
553
424
|
formatFullDate,
|
|
554
425
|
formatInTimezone,
|
|
@@ -558,6 +429,7 @@ export {
|
|
|
558
429
|
generateHours,
|
|
559
430
|
generateMinutes,
|
|
560
431
|
getCalendarDays,
|
|
432
|
+
getISOWeekNumber,
|
|
561
433
|
getMonthName,
|
|
562
434
|
getTime,
|
|
563
435
|
getTimeInTimezone,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kalyx/core",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Kalyx core — platform-agnostic date logic, IANA timezone helpers, and the DateAdapter contract used by @kalyx/react",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "jiji-hoon96",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"dst",
|
|
23
23
|
"iso8601",
|
|
24
24
|
"utc",
|
|
25
|
-
"
|
|
25
|
+
"adapter"
|
|
26
26
|
],
|
|
27
27
|
"type": "module",
|
|
28
28
|
"sideEffects": false,
|
|
@@ -46,12 +46,11 @@
|
|
|
46
46
|
"CHANGELOG.md",
|
|
47
47
|
"LICENSE"
|
|
48
48
|
],
|
|
49
|
-
"
|
|
50
|
-
"date-fns": "
|
|
51
|
-
"date-fns-tz": "^3.0.0"
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@kalyx/adapter-date-fns": "1.0.0"
|
|
52
51
|
},
|
|
53
52
|
"engines": {
|
|
54
|
-
"node": ">=
|
|
53
|
+
"node": ">=20.0.0"
|
|
55
54
|
},
|
|
56
55
|
"publishConfig": {
|
|
57
56
|
"access": "public",
|