@kalyx/core 1.0.0-rc.7 → 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 +246 -0
- package/README.md +6 -2
- package/dist/index.cjs +118 -251
- package/dist/index.d.cts +31 -20
- package/dist/index.d.ts +31 -20
- package/dist/index.js +118 -258
- package/package.json +5 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,251 @@
|
|
|
1
1
|
# @kalyx/core
|
|
2
2
|
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 5b6c37f: Extract `@kalyx/adapter-date-fns` and make `@kalyx/core` neutral
|
|
8
|
+
|
|
9
|
+
Step 1 + 2 of the four-step adapter-extraction plan (see `.claude/skills/adapter-extraction.md`). After this change, `@kalyx/core` no longer depends on `date-fns` or `date-fns-tz`; it ships only the platform-agnostic date logic (`getCalendarDays`, `isDateDisabled`, timezone helpers, labels, the `DateAdapter` contract). The DateFnsAdapter implementation now lives in its own publishable package so dayjs / luxon / Temporal adapters can be added later without forcing every Kalyx user to bundle two date libraries.
|
|
10
|
+
|
|
11
|
+
### What changed
|
|
12
|
+
- **`@kalyx/core`** — `DateFnsAdapter` is no longer exported and `date-fns` / `date-fns-tz` are no longer listed as dependencies. `utils/timezone.ts` was the lone leak and uses native `new Date(string)` now (every caller already routes through `normalizeISO` or `DateAdapter.parse`, so the input subset is fully spec-defined).
|
|
13
|
+
- **`@kalyx/adapter-date-fns`** — new package with the full `DateFnsAdapter` implementation moved verbatim. Same UTC semantics, same timezone-aware paths, same 35 adapter tests.
|
|
14
|
+
- **`@kalyx/react`** — imports `DateFnsAdapter` from `@kalyx/adapter-date-fns` now. The default adapter is still wired up automatically — anyone using `import { DatePicker } from '@kalyx/react'` keeps the previous behaviour with zero changes. The adapter package is a direct dependency so consumers installing just `@kalyx/react` continue to get a working default.
|
|
15
|
+
|
|
16
|
+
### Migration
|
|
17
|
+
|
|
18
|
+
If you imported `DateFnsAdapter` directly from `@kalyx/core`:
|
|
19
|
+
|
|
20
|
+
```diff
|
|
21
|
+
- import { DateFnsAdapter } from '@kalyx/core';
|
|
22
|
+
+ import { DateFnsAdapter } from '@kalyx/adapter-date-fns';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`@kalyx/react` consumers don't need to change anything — the adapter is still re-exported from `@kalyx/react`.
|
|
26
|
+
|
|
27
|
+
### Next (separate PR)
|
|
28
|
+
|
|
29
|
+
The `/headless` entry point (`@kalyx/react/headless`) that lets dayjs/luxon users tree-shake date-fns out is a follow-up. The component Roots still default to the date-fns adapter inline; the entry split requires moving that fallback out of each Root and into the entry boundary.
|
|
30
|
+
|
|
31
|
+
- ca7180e: chore: v1.0 milestone — API freeze.
|
|
32
|
+
|
|
33
|
+
Kalyx v1.0 declares the public API stable. This is a milestone release bundling the v0.5 surface additions (MonthPicker, YearPicker, WeekPicker, DatePicker.Presets, `onOpenChange`/`onCalendarNavigate` event callbacks) with an explicit commitment to semantic versioning going forward.
|
|
34
|
+
|
|
35
|
+
### What v1.0 commits to
|
|
36
|
+
- **Public API surface** — exports from `@kalyx/react` and `@kalyx/core` listed in their `index.ts` files. Any breaking change requires a major bump.
|
|
37
|
+
- **Compositional structure** — Root + subcomponent names (`DatePicker.Input`, `DatePicker.Calendar`, …) are stable. Removal or renaming requires a major bump.
|
|
38
|
+
- **Value semantics** — ISO 8601 UTC strings for single dates, `DateRange` `{start, end}` for ranges. `displayTimezone` behavior (civil-midnight-in-tz for date selection) is stable.
|
|
39
|
+
- **Accessibility contracts** — role/aria-\* attributes emitted by each component are stable.
|
|
40
|
+
|
|
41
|
+
### What v1.0 does NOT freeze
|
|
42
|
+
- Internal implementation details (non-exported functions, component file layout).
|
|
43
|
+
- CSS class name strings on elements — no classes are applied by default; only when a consumer passes them via `classNames` props.
|
|
44
|
+
- Error message text.
|
|
45
|
+
- Peer dependency version ranges (may expand to cover new React majors).
|
|
46
|
+
|
|
47
|
+
### Breaking changes vs 0.4.x
|
|
48
|
+
|
|
49
|
+
None. v1.0 is API-compatible with 0.4.x — existing code continues to work. The major bump communicates stability commitment, not breakage.
|
|
50
|
+
|
|
51
|
+
### Minor Changes
|
|
52
|
+
|
|
53
|
+
- 0eca2e8: Two new `DatePicker.Calendar` / `RangePicker.Calendar` props plus an ISO-week utility:
|
|
54
|
+
- **`showWeekNumber`** — render an ISO 8601 week-number column (1–53) on the left of the grid. The column uses `<th scope="row" aria-hidden="true">` so it doesn't participate in the WAI-ARIA grid data region; keyboard navigation across date cells is unchanged. New className slots: `weekNumberHeader`, `weekNumber`.
|
|
55
|
+
- **`fixedWeeks`** — when true, always render 6 rows (42 cells) regardless of the month. Useful for popover layouts that need a stable height across month navigation.
|
|
56
|
+
|
|
57
|
+
Both also accepted on `CalendarOptions` (the `getCalendarDays` core util gains `fixedWeeks`).
|
|
58
|
+
|
|
59
|
+
New core export: **`getISOWeekNumber(iso)`** — pure UTC computation, no date-fns dep. Anchored to the Thursday of the week (so the same week always returns the same number regardless of `weekStartsOn`).
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<DatePicker value={date} onChange={setDate}>
|
|
63
|
+
<DatePicker.Input />
|
|
64
|
+
<DatePicker.Popover>
|
|
65
|
+
<DatePicker.Calendar showWeekNumber fixedWeeks />
|
|
66
|
+
</DatePicker.Popover>
|
|
67
|
+
</DatePicker>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Bundle impact: +0.46 KB ESM gzip (13.96 → 14.42 KB). Still well under the 15 KB ceiling.
|
|
71
|
+
|
|
72
|
+
- d62c84e: `DisabledRule` gains a programmatic `filter` variant — pass any predicate `(iso: ISODateString) => boolean` to disable arbitrary days that don't fit the declarative `before` / `after` / `dayOfWeek` / `date` rules.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
const holidays = new Set(['2026-01-01T00:00:00.000Z', '2026-12-25T00:00:00.000Z']);
|
|
76
|
+
|
|
77
|
+
<DatePicker
|
|
78
|
+
disabled={[
|
|
79
|
+
{ dayOfWeek: [0, 6] }, // weekends
|
|
80
|
+
{ filter: (iso) => holidays.has(iso) }, // holidays
|
|
81
|
+
]}
|
|
82
|
+
>
|
|
83
|
+
…
|
|
84
|
+
</DatePicker>;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The new variant slots into the existing `isDateDisabled` evaluation (short-circuits on first match) and works with keyboard-navigation disabled-skip in `DatePicker.Calendar` / `RangePicker.Calendar` with no further changes. Equivalent to `react-datepicker`'s `filterDate` prop and MUI X DatePicker's `shouldDisableDate`. Bundle impact: 0 KB (still 13.96 KB ESM gzip).
|
|
88
|
+
|
|
89
|
+
- 4629384: chore(oss): unify node engines to >=20 and add public repository metadata
|
|
90
|
+
- `@kalyx/react` and `@kalyx/core` now require Node `>=20`, matching the root workspace and CI. This was the de-facto requirement; only the published manifests still claimed `>=18`.
|
|
91
|
+
- Root `package.json` now exposes `homepage`, `repository`, and `bugs` so `npm info` and the npm registry page link back to the GitHub repo.
|
|
92
|
+
- `.github/PULL_REQUEST_TEMPLATE.md` bundle ceiling updated `15KB → 16 KB` to match the post-rc.8 limit advertised in README and CI.
|
|
93
|
+
- `.gitignore` ignores `.codegraph/`, `.serena/`, and `.tmp-*/` (MCP server caches and worktree scratchpads).
|
|
94
|
+
|
|
95
|
+
- c8a6609: fix(rangepicker): announce next selection target and final range to screen readers
|
|
96
|
+
|
|
97
|
+
`<RangePicker.Calendar>` now announces context-aware messages through its existing `role="status"` live region:
|
|
98
|
+
- After the first click (start), it announces `<formatted-date>. Now select end date.` so screen-reader users know the next click commits the other endpoint.
|
|
99
|
+
- After the second click (end), it announces `Range selected: <start> – <end>` instead of just the bare date — matching the swap-if-before behaviour so the announcement always reflects what was committed.
|
|
100
|
+
- Week-mode commits now share the same `Range selected: ...` prefix for consistency.
|
|
101
|
+
|
|
102
|
+
The two new strings are wired through `RangePickerLabels.selectingEnd` and `RangePickerLabels.rangeSelected` with English defaults, and they are fully overridable via the existing `labels` prop for i18n. `@kalyx/core` gets a `minor` bump because `RangePickerLabels` gained required fields (with defaults supplied by `DEFAULT_RANGEPICKER_LABELS`); any consumer constructing a literal `RangePickerLabels` from scratch will need to add the two keys.
|
|
103
|
+
|
|
104
|
+
### Patch Changes
|
|
105
|
+
|
|
106
|
+
- 19ac1c0: fix(core): allow `generateMinutes` step values up to 60
|
|
107
|
+
|
|
108
|
+
`generateMinutes(step)` rejected any step above 30, which prevented legitimate cases like `step=45` (quarter-and-three-quarters past the hour) and `step=60` (on-the-hour only). The slot-generation loop already works for any 1–60 integer, so the upper bound is now 60 with the same error message format. Steps `0`, `61+`, and negative values still throw. No callers in `@kalyx/react` relied on the previous narrower bound.
|
|
109
|
+
|
|
110
|
+
- 3587b13: Remove unused English-hardcoded weekday utilities from `utils/date.ts`:
|
|
111
|
+
- `WEEKDAY_LABELS` (constant)
|
|
112
|
+
- `getOrderedWeekdays()` (function)
|
|
113
|
+
|
|
114
|
+
Both were internal exports (never exposed via `@kalyx/core` public `index.ts`) and had no consumers anywhere in the workspace. They were superseded by the locale-aware `getWeekdayNames(locale, weekStartsOn)` in `utils/locale.ts`, which uses `Intl.DateTimeFormat` to produce the same shape with multi-language support.
|
|
115
|
+
|
|
116
|
+
No public API surface changed.
|
|
117
|
+
|
|
118
|
+
- abc56ac: Security: pin transitive `fast-uri` to `>=3.1.2` and `@babel/plugin-transform-modules-systemjs` to `>=7.29.4` via `pnpm.overrides`.
|
|
119
|
+
|
|
120
|
+
Resolves three Code Scanning alerts on `pnpm-lock.yaml`:
|
|
121
|
+
- `fast-uri@3.1.0` — [GHSA-v39h-62p7-jpjc](https://osv.dev/GHSA-v39h-62p7-jpjc) (CVE-2026-6322), first patched in `3.1.2`.
|
|
122
|
+
- `fast-uri@3.1.0` — [GHSA-q3j6-qgpj-74h6](https://osv.dev/GHSA-q3j6-qgpj-74h6) (CVE-2026-6321), first patched in `3.1.1`.
|
|
123
|
+
- `@babel/plugin-transform-modules-systemjs@7.29.0` — [GHSA-fv7c-fp4j-7gwp](https://osv.dev/GHSA-fv7c-fp4j-7gwp) (CVE-2026-44728), first patched in `7.29.4` on the 7.x line.
|
|
124
|
+
|
|
125
|
+
All three packages are transitive build-time dependencies (ajv → fast-uri, Babel preset-env → systemjs plugin); no public API impact.
|
|
126
|
+
|
|
127
|
+
- aadb512: Security: pin transitive `postcss` to `>=8.5.10` via `pnpm.overrides`.
|
|
128
|
+
|
|
129
|
+
Two `postcss` versions in `pnpm-lock.yaml` (`8.4.31` from a `postcss-load-config` chain and `8.5.9` from the `tsup` chain) were affected by [GHSA-qx2v-qp2m-jg93](https://osv.dev/GHSA-qx2v-qp2m-jg93) (CVSS 6.1 — improper newline handling that lets crafted input bypass quote escapes). Both are now resolved to `8.5.10`+. The OSV scanner workflow (which auto-creates issues #23 / #24 / #27) now reports zero advisories.
|
|
130
|
+
|
|
131
|
+
- 0556886: fix(core): validate inputs to `to12Hour` and `to24Hour`
|
|
132
|
+
|
|
133
|
+
`to12Hour(hours24)` and `to24Hour(hours12, period)` are public exports from `@kalyx/core` but had no input validation. The previous silent arithmetic mapped invalid inputs onto plausible-looking but wrong outputs and hid caller bugs:
|
|
134
|
+
- `to12Hour(24)` returned `{ hours12: 12, period: 'PM' }` (because `24 % 12 = 0` → mapped to 12)
|
|
135
|
+
- `to12Hour(-1)` returned `{ hours12: -1, period: 'AM' }`
|
|
136
|
+
- `to24Hour(13, 'PM')` returned `25`
|
|
137
|
+
- `to24Hour(0, 'AM')` returned `0` (but `0` is not a valid 12-hour clock value — midnight is `12 AM`)
|
|
138
|
+
|
|
139
|
+
Both functions now throw `RangeError` with a clear message when the input is outside its valid integer range (`[0, 23]` for `to12Hour`, `[1, 12]` for `to24Hour`). `Number.isInteger` guards non-integers and `NaN`. No `@kalyx/react` callers ever passed invalid values, so the internal contracts are unchanged; only direct `@kalyx/core` users who relied on the silent-wrong behaviour see the new exception.
|
|
140
|
+
|
|
141
|
+
- df97687: P1 audit follow-ups for v1.0-rc:
|
|
142
|
+
- **SSR hydration safety in 4 commit/drilldown grids** — `DatePicker.MonthGrid`, `DatePicker.YearGrid`, `MonthPicker.Grid`, and `YearPicker.Grid` previously called `adapter.today()` directly inside their render bodies, producing a server/client clock-mismatch hydration warning across day boundaries (and intermittently wrong "today" highlights in tz-different SSR setups). Today is now snapshotted via `useState(null)` + post-mount `useEffect`, so the server output and the first client render agree, and the highlight settles on the first effect tick.
|
|
143
|
+
- **`AmPmToggle` now follows the WAI-ARIA radiogroup pattern** — Arrow / Home / End / Space / Enter move and commit selection between AM and PM, and `tabIndex` is roving (only the checked radio is in the tab order). Previously both buttons were tabbable and arrow keys were ignored.
|
|
144
|
+
- **`DatePicker.Preset` / `RangePicker.Preset` now use `aria-pressed`** instead of `role="option"` + `aria-selected`. `role="option"` is invalid outside `role="listbox"` / `role="combobox"`, so axe was flagging the previous markup. Active state still appears on `data-active` for CSS targeting.
|
|
145
|
+
- **`RangePicker.Calendar` no longer advertises `aria-multiselectable="true"`** — a date range is one selection (two endpoints), not a multi-select grid.
|
|
146
|
+
- **Test stability** — `useRangePicker` `respects disabled rules` test pinned to April 2026 via `defaultValue` so the calendar grid contains the expected weekend day regardless of the system clock (was failing once the clock crossed into May).
|
|
147
|
+
- **`labels.ts` test coverage** — first unit tests for the default-label exports.
|
|
148
|
+
|
|
149
|
+
Behavioral notes for users (none of these are breaking for code that follows the documented `data-*` styling contract):
|
|
150
|
+
- If you targeted Preset buttons via `[aria-selected="true"]` in CSS, switch to `[aria-pressed="true"]` or `[data-active]`.
|
|
151
|
+
- If you targeted the range grid via `[aria-multiselectable]`, that attribute is gone; use `[role="grid"]` on the calendar root instead.
|
|
152
|
+
|
|
153
|
+
- 21f3c1f: Resolve v1.0-rc release-blocking defects (P0):
|
|
154
|
+
- **`"use client"` directive** — bundle is now marked as a React Server Component client boundary via tsup banner. Next.js App Router consumers no longer have to wrap each import.
|
|
155
|
+
- **Stable `today()`/`now()` initialization** — `viewMonth`/`focusedDate` `useState` calls in `DatePicker`/`RangePicker`/`DateTimePicker` Roots now use lazy initializers, so the adapter isn't called on every render.
|
|
156
|
+
- **`@kalyx/core` version sync** — bumped from `1.0.0-rc.0` to `1.0.0-rc.1` to match `@kalyx/react`.
|
|
157
|
+
- **`@kalyx/core` package contents** — `LICENSE` and `CHANGELOG.md` are now included in the npm tarball (`files` field).
|
|
158
|
+
- **Form auto-submit blocked when calendar open** — pressing Enter inside `DatePicker.Input`/`RangePicker.Input`/`DateTimePicker.Input` while the popover is open no longer submits the surrounding `<form>`.
|
|
159
|
+
- **`aria-haspopup="dialog"` on Trigger** — completes the WAI-ARIA combobox/dialog pattern.
|
|
160
|
+
- **Disabled cells skipped during keyboard navigation** — Calendar arrow keys / PageUp/Down / Home / End now step over disabled days and stop only when no enabled day is reachable.
|
|
161
|
+
|
|
162
|
+
- 3228533: P1 v1.0-rc API/a11y/docs improvements:
|
|
163
|
+
- **Popover focus-out close** — `usePopover` now closes the popover when focus leaves the floating layer and the reference element (Tab through). Matches the Radix/Ark dismissable layer pattern.
|
|
164
|
+
- **`name` prop + hidden form input** — `DatePicker.Input` accepts a `name` prop. When set, a hidden `<input type="hidden">` is rendered alongside the visible input so the value participates in native form submission and integrates with `react-hook-form` Controller-less flows.
|
|
165
|
+
- **IME composition handling** — `DatePicker.Input` now defers parsing during IME composition (`compositionstart` / `compositionend`). Previously, partial Korean / Japanese / Chinese input was repeatedly re-parsed and the user's text disappeared.
|
|
166
|
+
- **README parity** — Korean README now has the "Styling with Tailwind CSS" and "Using data attributes" sections that were missing. Version table and bundle-size claim corrected to `v1.0.0-rc.1` / `11.57 KB`.
|
|
167
|
+
- **Package metadata** — `peerDependenciesMeta`, `engines.node`, and `publishConfig.provenance` added to both `@kalyx/react` and `@kalyx/core`.
|
|
168
|
+
- **`@kalyx/react` description** — corrected from "under 10 KB gzipped" (false claim) to "≤12 KB gzipped".
|
|
169
|
+
|
|
170
|
+
- b6129ed: P2 polish for v1.0-rc:
|
|
171
|
+
- **Calendar grid `aria-rowindex` / `aria-colindex` / `aria-rowcount` / `aria-colcount`** — `DatePicker.Calendar` and `RangePicker.Calendar` now expose grid coordinates so screenreaders announce position ("row 3 of 6, column 4 of 7") during keyboard navigation.
|
|
172
|
+
- **`displayName` on all `forwardRef` components** — `DatePicker.Input`, `DatePicker.Trigger`, `RangePicker.Input`, `TimePicker.Input`, `DateTimePicker.Input` now render with their public dot-notation name in React DevTools.
|
|
173
|
+
- **JSDoc on `DatePicker.Input` and `DatePicker.Trigger`** — public API surface for the most-used components has explanatory docstrings.
|
|
174
|
+
- **`addYears` leap-day regression tests** — locked the date-fns clamp behavior (2024-02-29 + 1y → 2025-02-28, not March 1).
|
|
175
|
+
- **DST fall-back ambiguous-hour regression test** — captures the current behavior of `setTimeInTimezone` for 2026-11-01 01:30 America/New_York so silent drift surfaces as a test failure.
|
|
176
|
+
- **Test count claim corrected** — root and core `CLAUDE.md` previously claimed "1,000+ unit tests"; actual count is ~140 in core, 374 across the workspace.
|
|
177
|
+
|
|
178
|
+
## 1.0.0-rc.13
|
|
179
|
+
|
|
180
|
+
### Major Changes
|
|
181
|
+
|
|
182
|
+
- 5b6c37f: Extract `@kalyx/adapter-date-fns` and make `@kalyx/core` neutral
|
|
183
|
+
|
|
184
|
+
Step 1 + 2 of the four-step adapter-extraction plan (see `.claude/skills/adapter-extraction.md`). After this change, `@kalyx/core` no longer depends on `date-fns` or `date-fns-tz`; it ships only the platform-agnostic date logic (`getCalendarDays`, `isDateDisabled`, timezone helpers, labels, the `DateAdapter` contract). The DateFnsAdapter implementation now lives in its own publishable package so dayjs / luxon / Temporal adapters can be added later without forcing every Kalyx user to bundle two date libraries.
|
|
185
|
+
|
|
186
|
+
### What changed
|
|
187
|
+
- **`@kalyx/core`** — `DateFnsAdapter` is no longer exported and `date-fns` / `date-fns-tz` are no longer listed as dependencies. `utils/timezone.ts` was the lone leak and uses native `new Date(string)` now (every caller already routes through `normalizeISO` or `DateAdapter.parse`, so the input subset is fully spec-defined).
|
|
188
|
+
- **`@kalyx/adapter-date-fns`** — new package with the full `DateFnsAdapter` implementation moved verbatim. Same UTC semantics, same timezone-aware paths, same 35 adapter tests.
|
|
189
|
+
- **`@kalyx/react`** — imports `DateFnsAdapter` from `@kalyx/adapter-date-fns` now. The default adapter is still wired up automatically — anyone using `import { DatePicker } from '@kalyx/react'` keeps the previous behaviour with zero changes. The adapter package is a direct dependency so consumers installing just `@kalyx/react` continue to get a working default.
|
|
190
|
+
|
|
191
|
+
### Migration
|
|
192
|
+
|
|
193
|
+
If you imported `DateFnsAdapter` directly from `@kalyx/core`:
|
|
194
|
+
|
|
195
|
+
```diff
|
|
196
|
+
- import { DateFnsAdapter } from '@kalyx/core';
|
|
197
|
+
+ import { DateFnsAdapter } from '@kalyx/adapter-date-fns';
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`@kalyx/react` consumers don't need to change anything — the adapter is still re-exported from `@kalyx/react`.
|
|
201
|
+
|
|
202
|
+
### Next (separate PR)
|
|
203
|
+
|
|
204
|
+
The `/headless` entry point (`@kalyx/react/headless`) that lets dayjs/luxon users tree-shake date-fns out is a follow-up. The component Roots still default to the date-fns adapter inline; the entry split requires moving that fallback out of each Root and into the entry boundary.
|
|
205
|
+
|
|
206
|
+
## 1.0.0-rc.12
|
|
207
|
+
|
|
208
|
+
### Patch Changes
|
|
209
|
+
|
|
210
|
+
- 0556886: fix(core): validate inputs to `to12Hour` and `to24Hour`
|
|
211
|
+
|
|
212
|
+
`to12Hour(hours24)` and `to24Hour(hours12, period)` are public exports from `@kalyx/core` but had no input validation. The previous silent arithmetic mapped invalid inputs onto plausible-looking but wrong outputs and hid caller bugs:
|
|
213
|
+
- `to12Hour(24)` returned `{ hours12: 12, period: 'PM' }` (because `24 % 12 = 0` → mapped to 12)
|
|
214
|
+
- `to12Hour(-1)` returned `{ hours12: -1, period: 'AM' }`
|
|
215
|
+
- `to24Hour(13, 'PM')` returned `25`
|
|
216
|
+
- `to24Hour(0, 'AM')` returned `0` (but `0` is not a valid 12-hour clock value — midnight is `12 AM`)
|
|
217
|
+
|
|
218
|
+
Both functions now throw `RangeError` with a clear message when the input is outside its valid integer range (`[0, 23]` for `to12Hour`, `[1, 12]` for `to24Hour`). `Number.isInteger` guards non-integers and `NaN`. No `@kalyx/react` callers ever passed invalid values, so the internal contracts are unchanged; only direct `@kalyx/core` users who relied on the silent-wrong behaviour see the new exception.
|
|
219
|
+
|
|
220
|
+
## 1.0.0-rc.11
|
|
221
|
+
|
|
222
|
+
### Minor Changes
|
|
223
|
+
|
|
224
|
+
- c8a6609: fix(rangepicker): announce next selection target and final range to screen readers
|
|
225
|
+
|
|
226
|
+
`<RangePicker.Calendar>` now announces context-aware messages through its existing `role="status"` live region:
|
|
227
|
+
- After the first click (start), it announces `<formatted-date>. Now select end date.` so screen-reader users know the next click commits the other endpoint.
|
|
228
|
+
- After the second click (end), it announces `Range selected: <start> – <end>` instead of just the bare date — matching the swap-if-before behaviour so the announcement always reflects what was committed.
|
|
229
|
+
- Week-mode commits now share the same `Range selected: ...` prefix for consistency.
|
|
230
|
+
|
|
231
|
+
The two new strings are wired through `RangePickerLabels.selectingEnd` and `RangePickerLabels.rangeSelected` with English defaults, and they are fully overridable via the existing `labels` prop for i18n. `@kalyx/core` gets a `minor` bump because `RangePickerLabels` gained required fields (with defaults supplied by `DEFAULT_RANGEPICKER_LABELS`); any consumer constructing a literal `RangePickerLabels` from scratch will need to add the two keys.
|
|
232
|
+
|
|
233
|
+
### Patch Changes
|
|
234
|
+
|
|
235
|
+
- 19ac1c0: fix(core): allow `generateMinutes` step values up to 60
|
|
236
|
+
|
|
237
|
+
`generateMinutes(step)` rejected any step above 30, which prevented legitimate cases like `step=45` (quarter-and-three-quarters past the hour) and `step=60` (on-the-hour only). The slot-generation loop already works for any 1–60 integer, so the upper bound is now 60 with the same error message format. Steps `0`, `61+`, and negative values still throw. No callers in `@kalyx/react` relied on the previous narrower bound.
|
|
238
|
+
|
|
239
|
+
## 1.0.0-rc.10
|
|
240
|
+
|
|
241
|
+
### Minor Changes
|
|
242
|
+
|
|
243
|
+
- 4629384: chore(oss): unify node engines to >=20 and add public repository metadata
|
|
244
|
+
- `@kalyx/react` and `@kalyx/core` now require Node `>=20`, matching the root workspace and CI. This was the de-facto requirement; only the published manifests still claimed `>=18`.
|
|
245
|
+
- Root `package.json` now exposes `homepage`, `repository`, and `bugs` so `npm info` and the npm registry page link back to the GitHub repo.
|
|
246
|
+
- `.github/PULL_REQUEST_TEMPLATE.md` bundle ceiling updated `15KB → 16 KB` to match the post-rc.8 limit advertised in README and CI.
|
|
247
|
+
- `.gitignore` ignores `.codegraph/`, `.serena/`, and `.tmp-*/` (MCP server caches and worktree scratchpads).
|
|
248
|
+
|
|
3
249
|
## 1.0.0-rc.7
|
|
4
250
|
|
|
5
251
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -35,11 +35,15 @@ import type {
|
|
|
35
35
|
|
|
36
36
|
### Adapter
|
|
37
37
|
|
|
38
|
+
`@kalyx/core` defines the `DateAdapter` interface but ships no implementation — the package is date-library-agnostic. Install a separate adapter package:
|
|
39
|
+
|
|
38
40
|
```ts
|
|
39
|
-
import { DateFnsAdapter } from '@kalyx/
|
|
40
|
-
// UTC-safe
|
|
41
|
+
import { DateFnsAdapter } from '@kalyx/adapter-date-fns';
|
|
42
|
+
// UTC-safe adapter built on date-fns v4.
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
Bring your own adapter by implementing the `DateAdapter` interface from `@kalyx/core` against any date library (dayjs, luxon, Temporal, etc.).
|
|
46
|
+
|
|
43
47
|
### Calendar utilities
|
|
44
48
|
|
|
45
49
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -24,7 +24,6 @@ __export(index_exports, {
|
|
|
24
24
|
DEFAULT_DATETIMEPICKER_LABELS: () => DEFAULT_DATETIMEPICKER_LABELS,
|
|
25
25
|
DEFAULT_RANGEPICKER_LABELS: () => DEFAULT_RANGEPICKER_LABELS,
|
|
26
26
|
DEFAULT_TIMEPICKER_LABELS: () => DEFAULT_TIMEPICKER_LABELS,
|
|
27
|
-
DateFnsAdapter: () => DateFnsAdapter,
|
|
28
27
|
civilMidnightFromUtcDay: () => civilMidnightFromUtcDay,
|
|
29
28
|
formatFullDate: () => formatFullDate,
|
|
30
29
|
formatInTimezone: () => formatInTimezone,
|
|
@@ -57,249 +56,6 @@ __export(index_exports, {
|
|
|
57
56
|
});
|
|
58
57
|
module.exports = __toCommonJS(index_exports);
|
|
59
58
|
|
|
60
|
-
// src/adapters/date-fns.ts
|
|
61
|
-
var import_date_fns2 = require("date-fns");
|
|
62
|
-
|
|
63
|
-
// src/utils/timezone.ts
|
|
64
|
-
var import_date_fns = require("date-fns");
|
|
65
|
-
var formatterCache = /* @__PURE__ */ new Map();
|
|
66
|
-
function getCachedPartsFormatter(timeZone) {
|
|
67
|
-
const key = `parts:${timeZone}`;
|
|
68
|
-
let fmt = formatterCache.get(key);
|
|
69
|
-
if (!fmt) {
|
|
70
|
-
fmt = new Intl.DateTimeFormat("en-US", {
|
|
71
|
-
timeZone,
|
|
72
|
-
year: "numeric",
|
|
73
|
-
month: "2-digit",
|
|
74
|
-
day: "2-digit",
|
|
75
|
-
hour: "2-digit",
|
|
76
|
-
minute: "2-digit",
|
|
77
|
-
second: "2-digit",
|
|
78
|
-
hourCycle: "h23"
|
|
79
|
-
});
|
|
80
|
-
formatterCache.set(key, fmt);
|
|
81
|
-
}
|
|
82
|
-
return fmt;
|
|
83
|
-
}
|
|
84
|
-
function partsInTimezone(utc, timeZone) {
|
|
85
|
-
const dtf = getCachedPartsFormatter(timeZone);
|
|
86
|
-
const parts = Object.fromEntries(dtf.formatToParts(utc).map((p) => [p.type, p.value]));
|
|
87
|
-
return {
|
|
88
|
-
year: Number(parts.year),
|
|
89
|
-
month: Number(parts.month),
|
|
90
|
-
day: Number(parts.day),
|
|
91
|
-
// Some locales/engines return '24' instead of '0' at midnight
|
|
92
|
-
hour: parts.hour === "24" ? 0 : Number(parts.hour),
|
|
93
|
-
minute: Number(parts.minute),
|
|
94
|
-
second: Number(parts.second)
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
function formatInTimezone(iso, formatStr, timeZone) {
|
|
98
|
-
const p = partsInTimezone((0, import_date_fns.parseISO)(iso), timeZone);
|
|
99
|
-
const tokens = {
|
|
100
|
-
yyyy: String(p.year),
|
|
101
|
-
MM: String(p.month).padStart(2, "0"),
|
|
102
|
-
dd: String(p.day).padStart(2, "0"),
|
|
103
|
-
HH: String(p.hour).padStart(2, "0"),
|
|
104
|
-
mm: String(p.minute).padStart(2, "0"),
|
|
105
|
-
ss: String(p.second).padStart(2, "0")
|
|
106
|
-
};
|
|
107
|
-
let result = formatStr;
|
|
108
|
-
for (const [token, value] of Object.entries(tokens).sort((a, b) => b[0].length - a[0].length)) {
|
|
109
|
-
result = result.split(token).join(value);
|
|
110
|
-
}
|
|
111
|
-
return result;
|
|
112
|
-
}
|
|
113
|
-
function getTimezoneOffsetMinutes(iso, timeZone) {
|
|
114
|
-
const utc = (0, import_date_fns.parseISO)(iso);
|
|
115
|
-
const p = partsInTimezone(utc, timeZone);
|
|
116
|
-
const asUtcEpoch = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
117
|
-
return Math.round((asUtcEpoch - utc.getTime()) / 6e4);
|
|
118
|
-
}
|
|
119
|
-
function startOfDayInTimezone(iso, timeZone) {
|
|
120
|
-
const utc = (0, import_date_fns.parseISO)(iso);
|
|
121
|
-
const p = partsInTimezone(utc, timeZone);
|
|
122
|
-
const civilMidnightUtc = Date.UTC(p.year, p.month - 1, p.day, 0, 0, 0);
|
|
123
|
-
const midnightProbe = new Date(civilMidnightUtc).toISOString();
|
|
124
|
-
const offsetMinutes = getTimezoneOffsetMinutes(midnightProbe, timeZone);
|
|
125
|
-
return new Date(civilMidnightUtc - offsetMinutes * 6e4).toISOString();
|
|
126
|
-
}
|
|
127
|
-
function isSameDayInTimezone(a, b, timeZone) {
|
|
128
|
-
const pa = partsInTimezone((0, import_date_fns.parseISO)(a), timeZone);
|
|
129
|
-
const pb = partsInTimezone((0, import_date_fns.parseISO)(b), timeZone);
|
|
130
|
-
return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
|
|
131
|
-
}
|
|
132
|
-
function todayInTimezone(timeZone) {
|
|
133
|
-
return startOfDayInTimezone((/* @__PURE__ */ new Date()).toISOString(), timeZone);
|
|
134
|
-
}
|
|
135
|
-
function civilMidnightFromUtcDay(gridUtcIso, timeZone) {
|
|
136
|
-
const utc = (0, import_date_fns.parseISO)(gridUtcIso);
|
|
137
|
-
const probe = new Date(
|
|
138
|
-
Date.UTC(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate(), 12, 0, 0)
|
|
139
|
-
).toISOString();
|
|
140
|
-
return startOfDayInTimezone(probe, timeZone);
|
|
141
|
-
}
|
|
142
|
-
function getTimeInTimezone(iso, timeZone) {
|
|
143
|
-
const p = partsInTimezone((0, import_date_fns.parseISO)(iso), timeZone);
|
|
144
|
-
return { hours: p.hour, minutes: p.minute, seconds: p.second };
|
|
145
|
-
}
|
|
146
|
-
function setTimeInTimezone(iso, partial, timeZone) {
|
|
147
|
-
const p = partsInTimezone((0, import_date_fns.parseISO)(iso), timeZone);
|
|
148
|
-
const targetHours = partial.hours ?? p.hour;
|
|
149
|
-
const targetMinutes = partial.minutes ?? p.minute;
|
|
150
|
-
const targetSeconds = partial.seconds ?? p.second;
|
|
151
|
-
const civilEpoch = Date.UTC(
|
|
152
|
-
p.year,
|
|
153
|
-
p.month - 1,
|
|
154
|
-
p.day,
|
|
155
|
-
targetHours,
|
|
156
|
-
targetMinutes,
|
|
157
|
-
targetSeconds
|
|
158
|
-
);
|
|
159
|
-
const probe1 = new Date(civilEpoch).toISOString();
|
|
160
|
-
const offset1 = getTimezoneOffsetMinutes(probe1, timeZone);
|
|
161
|
-
const realEpoch1 = civilEpoch - offset1 * 6e4;
|
|
162
|
-
const probe2 = new Date(realEpoch1).toISOString();
|
|
163
|
-
const offset2 = getTimezoneOffsetMinutes(probe2, timeZone);
|
|
164
|
-
const realEpoch2 = civilEpoch - offset2 * 6e4;
|
|
165
|
-
return new Date(realEpoch2).toISOString();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// src/adapters/date-fns.ts
|
|
169
|
-
function toDate(iso) {
|
|
170
|
-
return (0, import_date_fns2.parseISO)(iso);
|
|
171
|
-
}
|
|
172
|
-
function toISO(date) {
|
|
173
|
-
return date.toISOString();
|
|
174
|
-
}
|
|
175
|
-
function normalize(value) {
|
|
176
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
177
|
-
return `${value}T00:00:00.000Z`;
|
|
178
|
-
}
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
function utcStartOfDay(d) {
|
|
182
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
183
|
-
}
|
|
184
|
-
function utcStartOfMonth(d) {
|
|
185
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
|
|
186
|
-
}
|
|
187
|
-
function utcEndOfMonth(d) {
|
|
188
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0, 23, 59, 59, 999));
|
|
189
|
-
}
|
|
190
|
-
function utcStartOfWeek(d, weekStartsOn) {
|
|
191
|
-
const day = d.getUTCDay();
|
|
192
|
-
const diff = (day - weekStartsOn + 7) % 7;
|
|
193
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - diff));
|
|
194
|
-
}
|
|
195
|
-
function utcEndOfWeek(d, weekStartsOn) {
|
|
196
|
-
const start = utcStartOfWeek(d, weekStartsOn);
|
|
197
|
-
return new Date(
|
|
198
|
-
Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate() + 6, 23, 59, 59, 999)
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
var DateFnsAdapter = {
|
|
202
|
-
parse(value) {
|
|
203
|
-
if (!value) return "";
|
|
204
|
-
return normalize(value);
|
|
205
|
-
},
|
|
206
|
-
format(iso, formatStr, timezone) {
|
|
207
|
-
if (timezone) {
|
|
208
|
-
return formatInTimezone(iso, formatStr, timezone);
|
|
209
|
-
}
|
|
210
|
-
const d = toDate(iso);
|
|
211
|
-
const tokens = {
|
|
212
|
-
yyyy: String(d.getUTCFullYear()),
|
|
213
|
-
MM: String(d.getUTCMonth() + 1).padStart(2, "0"),
|
|
214
|
-
dd: String(d.getUTCDate()).padStart(2, "0"),
|
|
215
|
-
HH: String(d.getUTCHours()).padStart(2, "0"),
|
|
216
|
-
mm: String(d.getUTCMinutes()).padStart(2, "0"),
|
|
217
|
-
ss: String(d.getUTCSeconds()).padStart(2, "0"),
|
|
218
|
-
M: String(d.getUTCMonth() + 1),
|
|
219
|
-
d: String(d.getUTCDate())
|
|
220
|
-
};
|
|
221
|
-
let result = formatStr;
|
|
222
|
-
for (const [token, value] of Object.entries(tokens).sort((a, b) => b[0].length - a[0].length)) {
|
|
223
|
-
result = result.split(token).join(value);
|
|
224
|
-
}
|
|
225
|
-
return result;
|
|
226
|
-
},
|
|
227
|
-
addDays(iso, n) {
|
|
228
|
-
return toISO((0, import_date_fns2.addDays)(toDate(iso), n));
|
|
229
|
-
},
|
|
230
|
-
addMonths(iso, n) {
|
|
231
|
-
return toISO((0, import_date_fns2.addMonths)(toDate(iso), n));
|
|
232
|
-
},
|
|
233
|
-
addYears(iso, n) {
|
|
234
|
-
return toISO((0, import_date_fns2.addYears)(toDate(iso), n));
|
|
235
|
-
},
|
|
236
|
-
isBefore(a, b) {
|
|
237
|
-
return (0, import_date_fns2.isBefore)(toDate(a), toDate(b));
|
|
238
|
-
},
|
|
239
|
-
isAfter(a, b) {
|
|
240
|
-
return (0, import_date_fns2.isAfter)(toDate(a), toDate(b));
|
|
241
|
-
},
|
|
242
|
-
isSameDay(a, b, timezone) {
|
|
243
|
-
if (!a || !b) return false;
|
|
244
|
-
if (timezone) {
|
|
245
|
-
return isSameDayInTimezone(a, b, timezone);
|
|
246
|
-
}
|
|
247
|
-
const da = toDate(a);
|
|
248
|
-
const db = toDate(b);
|
|
249
|
-
return da.getUTCFullYear() === db.getUTCFullYear() && da.getUTCMonth() === db.getUTCMonth() && da.getUTCDate() === db.getUTCDate();
|
|
250
|
-
},
|
|
251
|
-
isSameMonth(a, b) {
|
|
252
|
-
const da = toDate(a);
|
|
253
|
-
const db = toDate(b);
|
|
254
|
-
return da.getUTCFullYear() === db.getUTCFullYear() && da.getUTCMonth() === db.getUTCMonth();
|
|
255
|
-
},
|
|
256
|
-
startOfDay(iso, timezone) {
|
|
257
|
-
if (timezone) {
|
|
258
|
-
return startOfDayInTimezone(iso, timezone);
|
|
259
|
-
}
|
|
260
|
-
return toISO(utcStartOfDay(toDate(iso)));
|
|
261
|
-
},
|
|
262
|
-
startOfMonth(iso) {
|
|
263
|
-
return toISO(utcStartOfMonth(toDate(iso)));
|
|
264
|
-
},
|
|
265
|
-
endOfMonth(iso) {
|
|
266
|
-
return toISO(utcEndOfMonth(toDate(iso)));
|
|
267
|
-
},
|
|
268
|
-
startOfWeek(iso, weekStartsOn = 0) {
|
|
269
|
-
return toISO(utcStartOfWeek(toDate(iso), weekStartsOn));
|
|
270
|
-
},
|
|
271
|
-
endOfWeek(iso, weekStartsOn = 0) {
|
|
272
|
-
return toISO(utcEndOfWeek(toDate(iso), weekStartsOn));
|
|
273
|
-
},
|
|
274
|
-
now() {
|
|
275
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
276
|
-
},
|
|
277
|
-
today(timezone) {
|
|
278
|
-
if (timezone) {
|
|
279
|
-
return todayInTimezone(timezone);
|
|
280
|
-
}
|
|
281
|
-
return toISO(utcStartOfDay(/* @__PURE__ */ new Date()));
|
|
282
|
-
},
|
|
283
|
-
isValid(value) {
|
|
284
|
-
if (!value) return false;
|
|
285
|
-
const normalized = normalize(value);
|
|
286
|
-
const date = (0, import_date_fns2.parseISO)(normalized);
|
|
287
|
-
return (0, import_date_fns2.isValid)(date);
|
|
288
|
-
},
|
|
289
|
-
getYear(iso) {
|
|
290
|
-
return toDate(iso).getUTCFullYear();
|
|
291
|
-
},
|
|
292
|
-
getMonth(iso) {
|
|
293
|
-
return toDate(iso).getUTCMonth();
|
|
294
|
-
},
|
|
295
|
-
getDate(iso) {
|
|
296
|
-
return toDate(iso).getUTCDate();
|
|
297
|
-
},
|
|
298
|
-
getDay(iso) {
|
|
299
|
-
return toDate(iso).getUTCDay();
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
|
|
303
59
|
// src/utils/calendar.ts
|
|
304
60
|
function getCalendarDays(monthISO, adapter, options = {}) {
|
|
305
61
|
const {
|
|
@@ -477,12 +233,18 @@ function formatTimeString(time, withSeconds = false) {
|
|
|
477
233
|
return `${hh}:${mm}`;
|
|
478
234
|
}
|
|
479
235
|
function to12Hour(hours24) {
|
|
236
|
+
if (!Number.isInteger(hours24) || hours24 < 0 || hours24 > 23) {
|
|
237
|
+
throw new RangeError(`[to12Hour] hours24 must be an integer in [0, 23], got ${hours24}`);
|
|
238
|
+
}
|
|
480
239
|
const period = hours24 >= 12 ? "PM" : "AM";
|
|
481
240
|
let hours12 = hours24 % 12;
|
|
482
241
|
if (hours12 === 0) hours12 = 12;
|
|
483
242
|
return { hours12, period };
|
|
484
243
|
}
|
|
485
244
|
function to24Hour(hours12, period) {
|
|
245
|
+
if (!Number.isInteger(hours12) || hours12 < 1 || hours12 > 12) {
|
|
246
|
+
throw new RangeError(`[to24Hour] hours12 must be an integer in [1, 12], got ${hours12}`);
|
|
247
|
+
}
|
|
486
248
|
if (period === "AM") {
|
|
487
249
|
return hours12 === 12 ? 0 : hours12;
|
|
488
250
|
}
|
|
@@ -495,8 +257,8 @@ function generateHours(format = "24h") {
|
|
|
495
257
|
return Array.from({ length: 24 }, (_, i) => i);
|
|
496
258
|
}
|
|
497
259
|
function generateMinutes(step = 1) {
|
|
498
|
-
if (step < 1 || step >
|
|
499
|
-
throw new Error(`[generateMinutes] step must be between 1 and
|
|
260
|
+
if (step < 1 || step > 60) {
|
|
261
|
+
throw new Error(`[generateMinutes] step must be between 1 and 60, got ${step}`);
|
|
500
262
|
}
|
|
501
263
|
const result = [];
|
|
502
264
|
for (let i = 0; i < 60; i += step) {
|
|
@@ -522,13 +284,13 @@ function formatTimeFromISO(iso, format) {
|
|
|
522
284
|
}
|
|
523
285
|
|
|
524
286
|
// src/utils/locale.ts
|
|
525
|
-
var
|
|
287
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
526
288
|
function getCachedFormatter(locale, options) {
|
|
527
289
|
const key = `${locale}:${JSON.stringify(options)}`;
|
|
528
|
-
let fmt =
|
|
290
|
+
let fmt = formatterCache.get(key);
|
|
529
291
|
if (!fmt) {
|
|
530
292
|
fmt = new Intl.DateTimeFormat(locale, options);
|
|
531
|
-
|
|
293
|
+
formatterCache.set(key, fmt);
|
|
532
294
|
}
|
|
533
295
|
return fmt;
|
|
534
296
|
}
|
|
@@ -573,6 +335,110 @@ function formatFullDate(iso, locale = "en-US") {
|
|
|
573
335
|
}).format(date);
|
|
574
336
|
}
|
|
575
337
|
|
|
338
|
+
// src/utils/timezone.ts
|
|
339
|
+
var formatterCache2 = /* @__PURE__ */ new Map();
|
|
340
|
+
function getCachedPartsFormatter(timeZone) {
|
|
341
|
+
const key = `parts:${timeZone}`;
|
|
342
|
+
let fmt = formatterCache2.get(key);
|
|
343
|
+
if (!fmt) {
|
|
344
|
+
fmt = new Intl.DateTimeFormat("en-US", {
|
|
345
|
+
timeZone,
|
|
346
|
+
year: "numeric",
|
|
347
|
+
month: "2-digit",
|
|
348
|
+
day: "2-digit",
|
|
349
|
+
hour: "2-digit",
|
|
350
|
+
minute: "2-digit",
|
|
351
|
+
second: "2-digit",
|
|
352
|
+
hourCycle: "h23"
|
|
353
|
+
});
|
|
354
|
+
formatterCache2.set(key, fmt);
|
|
355
|
+
}
|
|
356
|
+
return fmt;
|
|
357
|
+
}
|
|
358
|
+
function partsInTimezone(utc, timeZone) {
|
|
359
|
+
const dtf = getCachedPartsFormatter(timeZone);
|
|
360
|
+
const parts = Object.fromEntries(dtf.formatToParts(utc).map((p) => [p.type, p.value]));
|
|
361
|
+
return {
|
|
362
|
+
year: Number(parts.year),
|
|
363
|
+
month: Number(parts.month),
|
|
364
|
+
day: Number(parts.day),
|
|
365
|
+
// Some locales/engines return '24' instead of '0' at midnight
|
|
366
|
+
hour: parts.hour === "24" ? 0 : Number(parts.hour),
|
|
367
|
+
minute: Number(parts.minute),
|
|
368
|
+
second: Number(parts.second)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function formatInTimezone(iso, formatStr, timeZone) {
|
|
372
|
+
const p = partsInTimezone(new Date(iso), timeZone);
|
|
373
|
+
const tokens = {
|
|
374
|
+
yyyy: String(p.year),
|
|
375
|
+
MM: String(p.month).padStart(2, "0"),
|
|
376
|
+
dd: String(p.day).padStart(2, "0"),
|
|
377
|
+
HH: String(p.hour).padStart(2, "0"),
|
|
378
|
+
mm: String(p.minute).padStart(2, "0"),
|
|
379
|
+
ss: String(p.second).padStart(2, "0")
|
|
380
|
+
};
|
|
381
|
+
let result = formatStr;
|
|
382
|
+
for (const [token, value] of Object.entries(tokens).sort((a, b) => b[0].length - a[0].length)) {
|
|
383
|
+
result = result.split(token).join(value);
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
function getTimezoneOffsetMinutes(iso, timeZone) {
|
|
388
|
+
const utc = new Date(iso);
|
|
389
|
+
const p = partsInTimezone(utc, timeZone);
|
|
390
|
+
const asUtcEpoch = Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second);
|
|
391
|
+
return Math.round((asUtcEpoch - utc.getTime()) / 6e4);
|
|
392
|
+
}
|
|
393
|
+
function startOfDayInTimezone(iso, timeZone) {
|
|
394
|
+
const utc = new Date(iso);
|
|
395
|
+
const p = partsInTimezone(utc, timeZone);
|
|
396
|
+
const civilMidnightUtc = Date.UTC(p.year, p.month - 1, p.day, 0, 0, 0);
|
|
397
|
+
const midnightProbe = new Date(civilMidnightUtc).toISOString();
|
|
398
|
+
const offsetMinutes = getTimezoneOffsetMinutes(midnightProbe, timeZone);
|
|
399
|
+
return new Date(civilMidnightUtc - offsetMinutes * 6e4).toISOString();
|
|
400
|
+
}
|
|
401
|
+
function isSameDayInTimezone(a, b, timeZone) {
|
|
402
|
+
const pa = partsInTimezone(new Date(a), timeZone);
|
|
403
|
+
const pb = partsInTimezone(new Date(b), timeZone);
|
|
404
|
+
return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
|
|
405
|
+
}
|
|
406
|
+
function todayInTimezone(timeZone) {
|
|
407
|
+
return startOfDayInTimezone((/* @__PURE__ */ new Date()).toISOString(), timeZone);
|
|
408
|
+
}
|
|
409
|
+
function civilMidnightFromUtcDay(gridUtcIso, timeZone) {
|
|
410
|
+
const utc = new Date(gridUtcIso);
|
|
411
|
+
const probe = new Date(
|
|
412
|
+
Date.UTC(utc.getUTCFullYear(), utc.getUTCMonth(), utc.getUTCDate(), 12, 0, 0)
|
|
413
|
+
).toISOString();
|
|
414
|
+
return startOfDayInTimezone(probe, timeZone);
|
|
415
|
+
}
|
|
416
|
+
function getTimeInTimezone(iso, timeZone) {
|
|
417
|
+
const p = partsInTimezone(new Date(iso), timeZone);
|
|
418
|
+
return { hours: p.hour, minutes: p.minute, seconds: p.second };
|
|
419
|
+
}
|
|
420
|
+
function setTimeInTimezone(iso, partial, timeZone) {
|
|
421
|
+
const p = partsInTimezone(new Date(iso), timeZone);
|
|
422
|
+
const targetHours = partial.hours ?? p.hour;
|
|
423
|
+
const targetMinutes = partial.minutes ?? p.minute;
|
|
424
|
+
const targetSeconds = partial.seconds ?? p.second;
|
|
425
|
+
const civilEpoch = Date.UTC(
|
|
426
|
+
p.year,
|
|
427
|
+
p.month - 1,
|
|
428
|
+
p.day,
|
|
429
|
+
targetHours,
|
|
430
|
+
targetMinutes,
|
|
431
|
+
targetSeconds
|
|
432
|
+
);
|
|
433
|
+
const probe1 = new Date(civilEpoch).toISOString();
|
|
434
|
+
const offset1 = getTimezoneOffsetMinutes(probe1, timeZone);
|
|
435
|
+
const realEpoch1 = civilEpoch - offset1 * 6e4;
|
|
436
|
+
const probe2 = new Date(realEpoch1).toISOString();
|
|
437
|
+
const offset2 = getTimezoneOffsetMinutes(probe2, timeZone);
|
|
438
|
+
const realEpoch2 = civilEpoch - offset2 * 6e4;
|
|
439
|
+
return new Date(realEpoch2).toISOString();
|
|
440
|
+
}
|
|
441
|
+
|
|
576
442
|
// src/utils/labels.ts
|
|
577
443
|
var DEFAULT_DATEPICKER_LABELS = {
|
|
578
444
|
triggerOpen: "Open calendar",
|
|
@@ -590,7 +456,9 @@ var DEFAULT_RANGEPICKER_LABELS = {
|
|
|
590
456
|
popoverLabel: "Choose date range",
|
|
591
457
|
startInput: "Start date",
|
|
592
458
|
endInput: "End date",
|
|
593
|
-
presetsGroup: "Date range presets"
|
|
459
|
+
presetsGroup: "Date range presets",
|
|
460
|
+
selectingEnd: "Now select end date",
|
|
461
|
+
rangeSelected: "Range selected"
|
|
594
462
|
};
|
|
595
463
|
var DEFAULT_TIMEPICKER_LABELS = {
|
|
596
464
|
timeInput: "Time",
|
|
@@ -611,7 +479,6 @@ var DEFAULT_DATETIMEPICKER_LABELS = {
|
|
|
611
479
|
DEFAULT_DATETIMEPICKER_LABELS,
|
|
612
480
|
DEFAULT_RANGEPICKER_LABELS,
|
|
613
481
|
DEFAULT_TIMEPICKER_LABELS,
|
|
614
|
-
DateFnsAdapter,
|
|
615
482
|
civilMidnightFromUtcDay,
|
|
616
483
|
formatFullDate,
|
|
617
484
|
formatInTimezone,
|
package/dist/index.d.cts
CHANGED
|
@@ -119,21 +119,6 @@ interface CalendarOptions {
|
|
|
119
119
|
fixedWeeks?: boolean;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
* DateAdapter implementation backed by date-fns.
|
|
124
|
-
* All operations run in UTC to avoid timezone interference.
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* ```ts
|
|
128
|
-
* import { DateFnsAdapter } from '@kalyx/core';
|
|
129
|
-
*
|
|
130
|
-
* DateFnsAdapter.format('2026-01-15T00:00:00.000Z', 'yyyy-MM-dd'); // "2026-01-15"
|
|
131
|
-
* DateFnsAdapter.addDays('2026-01-15T00:00:00.000Z', 7); // "2026-01-22T..."
|
|
132
|
-
* DateFnsAdapter.isSameDay('2026-01-15T00:00:00.000Z', '2026-01-15T23:59:59.000Z'); // true
|
|
133
|
-
* ```
|
|
134
|
-
*/
|
|
135
|
-
declare const DateFnsAdapter: DateAdapter;
|
|
136
|
-
|
|
137
122
|
/**
|
|
138
123
|
* Builds the calendar grid for the given month.
|
|
139
124
|
* Returns a 2D array (`CalendarGrid`) organized by week.
|
|
@@ -218,7 +203,13 @@ declare function parseTimeString(input: string): TimeValue | null;
|
|
|
218
203
|
declare function formatTimeString(time: TimeValue, withSeconds?: boolean): string;
|
|
219
204
|
/**
|
|
220
205
|
* Converts a 24-hour value to 12-hour form.
|
|
221
|
-
*
|
|
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.
|
|
222
213
|
*/
|
|
223
214
|
declare function to12Hour(hours24: number): {
|
|
224
215
|
hours12: number;
|
|
@@ -226,6 +217,10 @@ declare function to12Hour(hours24: number): {
|
|
|
226
217
|
};
|
|
227
218
|
/**
|
|
228
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'`.
|
|
229
224
|
*/
|
|
230
225
|
declare function to24Hour(hours12: number, period: 'AM' | 'PM'): number;
|
|
231
226
|
/**
|
|
@@ -234,9 +229,15 @@ declare function to24Hour(hours12: number, period: 'AM' | 'PM'): number;
|
|
|
234
229
|
declare function generateHours(format?: '12h' | '24h'): number[];
|
|
235
230
|
/**
|
|
236
231
|
* Builds a minutes list at the given step.
|
|
237
|
-
*
|
|
238
|
-
* step=
|
|
239
|
-
* 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.
|
|
240
241
|
*/
|
|
241
242
|
declare function generateMinutes(step?: number): number[];
|
|
242
243
|
/**
|
|
@@ -399,6 +400,16 @@ interface RangePickerLabels extends DatePickerLabels {
|
|
|
399
400
|
startInput: string;
|
|
400
401
|
endInput: string;
|
|
401
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;
|
|
402
413
|
}
|
|
403
414
|
interface TimePickerLabels {
|
|
404
415
|
timeInput: string;
|
|
@@ -416,4 +427,4 @@ declare const DEFAULT_RANGEPICKER_LABELS: RangePickerLabels;
|
|
|
416
427
|
declare const DEFAULT_TIMEPICKER_LABELS: TimePickerLabels;
|
|
417
428
|
declare const DEFAULT_DATETIMEPICKER_LABELS: DateTimePickerLabels;
|
|
418
429
|
|
|
419
|
-
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.d.ts
CHANGED
|
@@ -119,21 +119,6 @@ interface CalendarOptions {
|
|
|
119
119
|
fixedWeeks?: boolean;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
/**
|
|
123
|
-
* DateAdapter implementation backed by date-fns.
|
|
124
|
-
* All operations run in UTC to avoid timezone interference.
|
|
125
|
-
*
|
|
126
|
-
* @example
|
|
127
|
-
* ```ts
|
|
128
|
-
* import { DateFnsAdapter } from '@kalyx/core';
|
|
129
|
-
*
|
|
130
|
-
* DateFnsAdapter.format('2026-01-15T00:00:00.000Z', 'yyyy-MM-dd'); // "2026-01-15"
|
|
131
|
-
* DateFnsAdapter.addDays('2026-01-15T00:00:00.000Z', 7); // "2026-01-22T..."
|
|
132
|
-
* DateFnsAdapter.isSameDay('2026-01-15T00:00:00.000Z', '2026-01-15T23:59:59.000Z'); // true
|
|
133
|
-
* ```
|
|
134
|
-
*/
|
|
135
|
-
declare const DateFnsAdapter: DateAdapter;
|
|
136
|
-
|
|
137
122
|
/**
|
|
138
123
|
* Builds the calendar grid for the given month.
|
|
139
124
|
* Returns a 2D array (`CalendarGrid`) organized by week.
|
|
@@ -218,7 +203,13 @@ declare function parseTimeString(input: string): TimeValue | null;
|
|
|
218
203
|
declare function formatTimeString(time: TimeValue, withSeconds?: boolean): string;
|
|
219
204
|
/**
|
|
220
205
|
* Converts a 24-hour value to 12-hour form.
|
|
221
|
-
*
|
|
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.
|
|
222
213
|
*/
|
|
223
214
|
declare function to12Hour(hours24: number): {
|
|
224
215
|
hours12: number;
|
|
@@ -226,6 +217,10 @@ declare function to12Hour(hours24: number): {
|
|
|
226
217
|
};
|
|
227
218
|
/**
|
|
228
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'`.
|
|
229
224
|
*/
|
|
230
225
|
declare function to24Hour(hours12: number, period: 'AM' | 'PM'): number;
|
|
231
226
|
/**
|
|
@@ -234,9 +229,15 @@ declare function to24Hour(hours12: number, period: 'AM' | 'PM'): number;
|
|
|
234
229
|
declare function generateHours(format?: '12h' | '24h'): number[];
|
|
235
230
|
/**
|
|
236
231
|
* Builds a minutes list at the given step.
|
|
237
|
-
*
|
|
238
|
-
* step=
|
|
239
|
-
* 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.
|
|
240
241
|
*/
|
|
241
242
|
declare function generateMinutes(step?: number): number[];
|
|
242
243
|
/**
|
|
@@ -399,6 +400,16 @@ interface RangePickerLabels extends DatePickerLabels {
|
|
|
399
400
|
startInput: string;
|
|
400
401
|
endInput: string;
|
|
401
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;
|
|
402
413
|
}
|
|
403
414
|
interface TimePickerLabels {
|
|
404
415
|
timeInput: string;
|
|
@@ -416,4 +427,4 @@ declare const DEFAULT_RANGEPICKER_LABELS: RangePickerLabels;
|
|
|
416
427
|
declare const DEFAULT_TIMEPICKER_LABELS: TimePickerLabels;
|
|
417
428
|
declare const DEFAULT_DATETIMEPICKER_LABELS: DateTimePickerLabels;
|
|
418
429
|
|
|
419
|
-
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 {
|
|
@@ -426,12 +175,18 @@ function formatTimeString(time, withSeconds = false) {
|
|
|
426
175
|
return `${hh}:${mm}`;
|
|
427
176
|
}
|
|
428
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
|
+
}
|
|
429
181
|
const period = hours24 >= 12 ? "PM" : "AM";
|
|
430
182
|
let hours12 = hours24 % 12;
|
|
431
183
|
if (hours12 === 0) hours12 = 12;
|
|
432
184
|
return { hours12, period };
|
|
433
185
|
}
|
|
434
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
|
+
}
|
|
435
190
|
if (period === "AM") {
|
|
436
191
|
return hours12 === 12 ? 0 : hours12;
|
|
437
192
|
}
|
|
@@ -444,8 +199,8 @@ function generateHours(format = "24h") {
|
|
|
444
199
|
return Array.from({ length: 24 }, (_, i) => i);
|
|
445
200
|
}
|
|
446
201
|
function generateMinutes(step = 1) {
|
|
447
|
-
if (step < 1 || step >
|
|
448
|
-
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}`);
|
|
449
204
|
}
|
|
450
205
|
const result = [];
|
|
451
206
|
for (let i = 0; i < 60; i += step) {
|
|
@@ -471,13 +226,13 @@ function formatTimeFromISO(iso, format) {
|
|
|
471
226
|
}
|
|
472
227
|
|
|
473
228
|
// src/utils/locale.ts
|
|
474
|
-
var
|
|
229
|
+
var formatterCache = /* @__PURE__ */ new Map();
|
|
475
230
|
function getCachedFormatter(locale, options) {
|
|
476
231
|
const key = `${locale}:${JSON.stringify(options)}`;
|
|
477
|
-
let fmt =
|
|
232
|
+
let fmt = formatterCache.get(key);
|
|
478
233
|
if (!fmt) {
|
|
479
234
|
fmt = new Intl.DateTimeFormat(locale, options);
|
|
480
|
-
|
|
235
|
+
formatterCache.set(key, fmt);
|
|
481
236
|
}
|
|
482
237
|
return fmt;
|
|
483
238
|
}
|
|
@@ -522,6 +277,110 @@ function formatFullDate(iso, locale = "en-US") {
|
|
|
522
277
|
}).format(date);
|
|
523
278
|
}
|
|
524
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
|
+
|
|
525
384
|
// src/utils/labels.ts
|
|
526
385
|
var DEFAULT_DATEPICKER_LABELS = {
|
|
527
386
|
triggerOpen: "Open calendar",
|
|
@@ -539,7 +398,9 @@ var DEFAULT_RANGEPICKER_LABELS = {
|
|
|
539
398
|
popoverLabel: "Choose date range",
|
|
540
399
|
startInput: "Start date",
|
|
541
400
|
endInput: "End date",
|
|
542
|
-
presetsGroup: "Date range presets"
|
|
401
|
+
presetsGroup: "Date range presets",
|
|
402
|
+
selectingEnd: "Now select end date",
|
|
403
|
+
rangeSelected: "Range selected"
|
|
543
404
|
};
|
|
544
405
|
var DEFAULT_TIMEPICKER_LABELS = {
|
|
545
406
|
timeInput: "Time",
|
|
@@ -559,7 +420,6 @@ export {
|
|
|
559
420
|
DEFAULT_DATETIMEPICKER_LABELS,
|
|
560
421
|
DEFAULT_RANGEPICKER_LABELS,
|
|
561
422
|
DEFAULT_TIMEPICKER_LABELS,
|
|
562
|
-
DateFnsAdapter,
|
|
563
423
|
civilMidnightFromUtcDay,
|
|
564
424
|
formatFullDate,
|
|
565
425
|
formatInTimezone,
|
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",
|