@kalyx/core 0.2.0 → 0.3.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/README.md CHANGED
@@ -1,17 +1,85 @@
1
1
  # @kalyx/core
2
2
 
3
- > Platform-agnostic date logic for Kalyx. Types, adapters, and utilities.
3
+ > Platform-independent date logic powering [Kalyx](https://github.com/jiji-hoon96/kalyx). Types, adapters, and UTC-safe utilities.
4
4
 
5
- This package is used internally by `@kalyx/react`. Most users should install `@kalyx/react` directly.
5
+ [![npm](https://img.shields.io/npm/v/@kalyx/core?color=5b4fe1)](https://www.npmjs.com/package/@kalyx/core)
6
+ [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/jiji-hoon96/kalyx/blob/main/LICENSE)
7
+
8
+ Most users should install [`@kalyx/react`](https://www.npmjs.com/package/@kalyx/react) directly — it re-exports what you need. Install `@kalyx/core` only if you're building your own picker layer or a custom platform adapter.
9
+
10
+ **📚 Full docs:** [kalyx-docs.vercel.app/docs/api/core](https://kalyx-docs.vercel.app/docs/api/core)
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pnpm add @kalyx/core
16
+ ```
6
17
 
7
18
  ## What's inside
8
19
 
9
- - **Types** — `ISODateString`, `DateRange`, `DateAdapter`, `CalendarDay`, `TimeValue`, etc.
10
- - **DateFnsAdapter** — UTC-based date-fns adapter implementing `DateAdapter` interface
11
- - **Calendar utils** — `getCalendarDays`, `isDateDisabled`
12
- - **Time utils** — `setTime`, `getTime`, `parseTimeString`, `to12Hour`, `to24Hour`, `generateHours`, `generateMinutes`
13
- - **Date utils** — `normalizeISO`, `parseInputValue`
20
+ ### Types
21
+
22
+ ```ts
23
+ import type {
24
+ ISODateString,
25
+ DisabledRule,
26
+ DateRange,
27
+ CalendarDay,
28
+ CalendarGrid,
29
+ WeekStartsOn,
30
+ CalendarOptions,
31
+ DateAdapter,
32
+ TimeValue,
33
+ } from '@kalyx/core';
34
+ ```
35
+
36
+ ### Adapter
37
+
38
+ ```ts
39
+ import { DateFnsAdapter } from '@kalyx/core';
40
+ // UTC-safe default adapter, built on date-fns v4.
41
+ ```
42
+
43
+ ### Calendar utilities
44
+
45
+ ```ts
46
+ import { getCalendarDays, isDateDisabled, minDate, maxDate } from '@kalyx/core';
47
+ ```
48
+
49
+ ### Date helpers
50
+
51
+ ```ts
52
+ import { normalizeISO, parseInputValue } from '@kalyx/core';
53
+ ```
54
+
55
+ ### Time helpers
56
+
57
+ ```ts
58
+ import {
59
+ setTime, getTime,
60
+ parseTimeString, formatTimeString, formatTimeFromISO,
61
+ to12Hour, to24Hour,
62
+ generateHours, generateMinutes,
63
+ isSameTime,
64
+ } from '@kalyx/core';
65
+ ```
66
+
67
+ ### Locale helpers
68
+
69
+ ```ts
70
+ import {
71
+ getMonthName, formatMonthYear,
72
+ getWeekdayNames, formatFullDate,
73
+ } from '@kalyx/core';
74
+ ```
75
+
76
+ ## Principles
77
+
78
+ - **All dates are ISO 8601 UTC strings** — never `Date` objects.
79
+ - **UTC-only arithmetic** — uses `getUTC*` methods, never local-timezone variants.
80
+ - **Adapter abstraction** — swap date engines by implementing `DateAdapter`.
81
+ - **Pure functions** — zero side effects, fully testable.
14
82
 
15
83
  ## License
16
84
 
17
- MIT
85
+ [MIT](https://github.com/jiji-hoon96/kalyx/blob/main/LICENSE)
package/dist/index.cjs CHANGED
@@ -20,8 +20,13 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ DEFAULT_DATEPICKER_LABELS: () => DEFAULT_DATEPICKER_LABELS,
24
+ DEFAULT_DATETIMEPICKER_LABELS: () => DEFAULT_DATETIMEPICKER_LABELS,
25
+ DEFAULT_RANGEPICKER_LABELS: () => DEFAULT_RANGEPICKER_LABELS,
26
+ DEFAULT_TIMEPICKER_LABELS: () => DEFAULT_TIMEPICKER_LABELS,
23
27
  DateFnsAdapter: () => DateFnsAdapter,
24
28
  formatFullDate: () => formatFullDate,
29
+ formatInTimezone: () => formatInTimezone,
25
30
  formatMonthYear: () => formatMonthYear,
26
31
  formatTimeFromISO: () => formatTimeFromISO,
27
32
  formatTimeString: () => formatTimeString,
@@ -30,8 +35,10 @@ __export(index_exports, {
30
35
  getCalendarDays: () => getCalendarDays,
31
36
  getMonthName: () => getMonthName,
32
37
  getTime: () => getTime,
38
+ getTimezoneOffsetMinutes: () => getTimezoneOffsetMinutes,
33
39
  getWeekdayNames: () => getWeekdayNames,
34
40
  isDateDisabled: () => isDateDisabled,
41
+ isSameDayInTimezone: () => isSameDayInTimezone,
35
42
  isSameTime: () => isSameTime,
36
43
  maxDate: () => maxDate,
37
44
  minDate: () => minDate,
@@ -39,8 +46,10 @@ __export(index_exports, {
39
46
  parseInputValue: () => parseInputValue,
40
47
  parseTimeString: () => parseTimeString,
41
48
  setTime: () => setTime,
49
+ startOfDayInTimezone: () => startOfDayInTimezone,
42
50
  to12Hour: () => to12Hour,
43
- to24Hour: () => to24Hour
51
+ to24Hour: () => to24Hour,
52
+ todayInTimezone: () => todayInTimezone
44
53
  });
45
54
  module.exports = __toCommonJS(index_exports);
46
55
 
@@ -275,14 +284,14 @@ function maxDate(a, b, adapter) {
275
284
 
276
285
  // src/utils/date.ts
277
286
  var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
278
- var ISO_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
287
+ var ISO_DATETIME_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
279
288
  function normalizeISO(value) {
280
289
  if (!value) return "";
281
290
  if (ISO_DATETIME_REGEX.test(value)) return value;
282
291
  if (ISO_DATE_REGEX.test(value)) return `${value}T00:00:00.000Z`;
283
292
  return value;
284
293
  }
285
- function parseInputValue(input, format, adapter) {
294
+ function parseInputValue(input, adapter) {
286
295
  if (!input.trim()) return null;
287
296
  const cleaned = input.replace(/\//g, "-").trim();
288
297
  if (ISO_DATE_REGEX.test(cleaned)) {
@@ -357,8 +366,8 @@ function generateHours(format = "24h") {
357
366
  return Array.from({ length: 24 }, (_, i) => i);
358
367
  }
359
368
  function generateMinutes(step = 1) {
360
- if (step < 1 || step > 60) {
361
- throw new Error(`[generateMinutes] step must be between 1 and 60, got ${step}`);
369
+ if (step < 1 || step > 30) {
370
+ throw new Error(`[generateMinutes] step must be between 1 and 30, got ${step}`);
362
371
  }
363
372
  const result = [];
364
373
  for (let i = 0; i < 60; i += step) {
@@ -369,7 +378,7 @@ function generateMinutes(step = 1) {
369
378
  function isSameTime(a, b) {
370
379
  return a.hours === b.hours && a.minutes === b.minutes && a.seconds === b.seconds;
371
380
  }
372
- function formatTimeFromISO(iso, format, _adapter) {
381
+ function formatTimeFromISO(iso, format) {
373
382
  const time = getTime(iso);
374
383
  if (format === "h:mm a" || format === "h:mm:ss a") {
375
384
  const { hours12, period } = to12Hour(time.hours);
@@ -384,30 +393,35 @@ function formatTimeFromISO(iso, format, _adapter) {
384
393
  }
385
394
 
386
395
  // src/utils/locale.ts
396
+ var formatterCache = /* @__PURE__ */ new Map();
397
+ function getCachedFormatter(locale, options) {
398
+ const key = `${locale}:${JSON.stringify(options)}`;
399
+ let fmt = formatterCache.get(key);
400
+ if (!fmt) {
401
+ fmt = new Intl.DateTimeFormat(locale, options);
402
+ formatterCache.set(key, fmt);
403
+ }
404
+ return fmt;
405
+ }
406
+ var REFERENCE_YEAR = 2026;
387
407
  function getMonthName(month, locale = "en-US") {
388
- const date = new Date(Date.UTC(2026, month, 1));
389
- return new Intl.DateTimeFormat(locale, { month: "long", timeZone: "UTC" }).format(date);
408
+ const date = new Date(Date.UTC(REFERENCE_YEAR, month, 1));
409
+ return getCachedFormatter(locale, { month: "long", timeZone: "UTC" }).format(date);
390
410
  }
391
411
  function formatMonthYear(year, month, locale = "en-US") {
392
412
  const date = new Date(Date.UTC(year, month, 1));
393
- return new Intl.DateTimeFormat(locale, {
413
+ return getCachedFormatter(locale, {
394
414
  year: "numeric",
395
415
  month: "long",
396
416
  timeZone: "UTC"
397
417
  }).format(date);
398
418
  }
399
419
  function getWeekdayNames(locale = "en-US", weekStartsOn = 0) {
400
- const shortFormatter = new Intl.DateTimeFormat(locale, {
401
- weekday: "short",
402
- timeZone: "UTC"
403
- });
404
- const fullFormatter = new Intl.DateTimeFormat(locale, {
405
- weekday: "long",
406
- timeZone: "UTC"
407
- });
420
+ const shortFormatter = getCachedFormatter(locale, { weekday: "short", timeZone: "UTC" });
421
+ const fullFormatter = getCachedFormatter(locale, { weekday: "long", timeZone: "UTC" });
408
422
  const days = [];
409
423
  for (let i = 0; i < 7; i++) {
410
- const date = new Date(Date.UTC(2026, 0, 4 + i));
424
+ const date = new Date(Date.UTC(REFERENCE_YEAR, 0, 4 + i));
411
425
  days.push({
412
426
  short: shortFormatter.format(date),
413
427
  full: fullFormatter.format(date)
@@ -421,7 +435,7 @@ function getWeekdayNames(locale = "en-US", weekStartsOn = 0) {
421
435
  }
422
436
  function formatFullDate(iso, locale = "en-US") {
423
437
  const date = new Date(iso);
424
- return new Intl.DateTimeFormat(locale, {
438
+ return getCachedFormatter(locale, {
425
439
  year: "numeric",
426
440
  month: "long",
427
441
  day: "numeric",
@@ -429,10 +443,123 @@ function formatFullDate(iso, locale = "en-US") {
429
443
  timeZone: "UTC"
430
444
  }).format(date);
431
445
  }
446
+
447
+ // src/utils/timezone.ts
448
+ var import_date_fns2 = require("date-fns");
449
+ var formatterCache2 = /* @__PURE__ */ new Map();
450
+ function getCachedPartsFormatter(timeZone) {
451
+ const key = `parts:${timeZone}`;
452
+ let fmt = formatterCache2.get(key);
453
+ if (!fmt) {
454
+ fmt = new Intl.DateTimeFormat("en-US", {
455
+ timeZone,
456
+ year: "numeric",
457
+ month: "2-digit",
458
+ day: "2-digit",
459
+ hour: "2-digit",
460
+ minute: "2-digit",
461
+ second: "2-digit",
462
+ hourCycle: "h23"
463
+ });
464
+ formatterCache2.set(key, fmt);
465
+ }
466
+ return fmt;
467
+ }
468
+ function partsInTimezone(utc, timeZone) {
469
+ const dtf = getCachedPartsFormatter(timeZone);
470
+ const parts = Object.fromEntries(
471
+ dtf.formatToParts(utc).map((p) => [p.type, p.value])
472
+ );
473
+ return {
474
+ year: Number(parts.year),
475
+ month: Number(parts.month),
476
+ day: Number(parts.day),
477
+ // Some locales/engines return '24' instead of '0' at midnight
478
+ hour: parts.hour === "24" ? 0 : Number(parts.hour),
479
+ minute: Number(parts.minute),
480
+ second: Number(parts.second)
481
+ };
482
+ }
483
+ function formatInTimezone(iso, formatStr, timeZone) {
484
+ const p = partsInTimezone((0, import_date_fns2.parseISO)(iso), timeZone);
485
+ const tokens = {
486
+ yyyy: String(p.year),
487
+ MM: String(p.month).padStart(2, "0"),
488
+ dd: String(p.day).padStart(2, "0"),
489
+ HH: String(p.hour).padStart(2, "0"),
490
+ mm: String(p.minute).padStart(2, "0"),
491
+ ss: String(p.second).padStart(2, "0")
492
+ };
493
+ let result = formatStr;
494
+ for (const [token, value] of Object.entries(tokens).sort((a, b) => b[0].length - a[0].length)) {
495
+ result = result.split(token).join(value);
496
+ }
497
+ return result;
498
+ }
499
+ function getTimezoneOffsetMinutes(iso, timeZone) {
500
+ const utc = (0, import_date_fns2.parseISO)(iso);
501
+ const p = partsInTimezone(utc, timeZone);
502
+ const asUtcEpoch = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
503
+ return Math.round((asUtcEpoch - utc.getTime()) / 6e4);
504
+ }
505
+ function startOfDayInTimezone(iso, timeZone) {
506
+ const utc = (0, import_date_fns2.parseISO)(iso);
507
+ const p = partsInTimezone(utc, timeZone);
508
+ const civilMidnightUtc = Date.UTC(p.year, p.month - 1, p.day, 0, 0, 0);
509
+ const midnightProbe = new Date(civilMidnightUtc).toISOString();
510
+ const offsetMinutes = getTimezoneOffsetMinutes(midnightProbe, timeZone);
511
+ return new Date(civilMidnightUtc - offsetMinutes * 6e4).toISOString();
512
+ }
513
+ function isSameDayInTimezone(a, b, timeZone) {
514
+ const pa = partsInTimezone((0, import_date_fns2.parseISO)(a), timeZone);
515
+ const pb = partsInTimezone((0, import_date_fns2.parseISO)(b), timeZone);
516
+ return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
517
+ }
518
+ function todayInTimezone(timeZone) {
519
+ return startOfDayInTimezone((/* @__PURE__ */ new Date()).toISOString(), timeZone);
520
+ }
521
+
522
+ // src/utils/labels.ts
523
+ var DEFAULT_DATEPICKER_LABELS = {
524
+ triggerOpen: "Open calendar",
525
+ triggerClose: "Close calendar",
526
+ popoverLabel: "Choose date",
527
+ prevMonth: "Previous month",
528
+ nextMonth: "Next month",
529
+ prevYear: "Previous year",
530
+ nextYear: "Next year",
531
+ prevDecade: "Previous decade",
532
+ nextDecade: "Next decade"
533
+ };
534
+ var DEFAULT_RANGEPICKER_LABELS = {
535
+ ...DEFAULT_DATEPICKER_LABELS,
536
+ popoverLabel: "Choose date range",
537
+ startInput: "Start date",
538
+ endInput: "End date",
539
+ presetsGroup: "Date range presets"
540
+ };
541
+ var DEFAULT_TIMEPICKER_LABELS = {
542
+ timeInput: "Time",
543
+ hourList: "Hour",
544
+ minuteList: "Minute",
545
+ amPmToggle: "AM/PM",
546
+ hourOption: (hour) => `${hour} hours`,
547
+ minuteOption: (minute) => `${minute} minutes`
548
+ };
549
+ var DEFAULT_DATETIMEPICKER_LABELS = {
550
+ ...DEFAULT_DATEPICKER_LABELS,
551
+ ...DEFAULT_TIMEPICKER_LABELS,
552
+ dateTimeInput: "Date and time"
553
+ };
432
554
  // Annotate the CommonJS export names for ESM import in node:
433
555
  0 && (module.exports = {
556
+ DEFAULT_DATEPICKER_LABELS,
557
+ DEFAULT_DATETIMEPICKER_LABELS,
558
+ DEFAULT_RANGEPICKER_LABELS,
559
+ DEFAULT_TIMEPICKER_LABELS,
434
560
  DateFnsAdapter,
435
561
  formatFullDate,
562
+ formatInTimezone,
436
563
  formatMonthYear,
437
564
  formatTimeFromISO,
438
565
  formatTimeString,
@@ -441,8 +568,10 @@ function formatFullDate(iso, locale = "en-US") {
441
568
  getCalendarDays,
442
569
  getMonthName,
443
570
  getTime,
571
+ getTimezoneOffsetMinutes,
444
572
  getWeekdayNames,
445
573
  isDateDisabled,
574
+ isSameDayInTimezone,
446
575
  isSameTime,
447
576
  maxDate,
448
577
  minDate,
@@ -450,6 +579,8 @@ function formatFullDate(iso, locale = "en-US") {
450
579
  parseInputValue,
451
580
  parseTimeString,
452
581
  setTime,
582
+ startOfDayInTimezone,
453
583
  to12Hour,
454
- to24Hour
584
+ to24Hour,
585
+ todayInTimezone
455
586
  });