@kalyx/react 1.0.0-rc.9 → 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 CHANGED
@@ -1,5 +1,546 @@
1
1
  # @kalyx/react
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - ca7180e: chore: v1.0 milestone — API freeze.
8
+
9
+ 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.
10
+
11
+ ### What v1.0 commits to
12
+ - **Public API surface** — exports from `@kalyx/react` and `@kalyx/core` listed in their `index.ts` files. Any breaking change requires a major bump.
13
+ - **Compositional structure** — Root + subcomponent names (`DatePicker.Input`, `DatePicker.Calendar`, …) are stable. Removal or renaming requires a major bump.
14
+ - **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.
15
+ - **Accessibility contracts** — role/aria-\* attributes emitted by each component are stable.
16
+
17
+ ### What v1.0 does NOT freeze
18
+ - Internal implementation details (non-exported functions, component file layout).
19
+ - CSS class name strings on elements — no classes are applied by default; only when a consumer passes them via `classNames` props.
20
+ - Error message text.
21
+ - Peer dependency version ranges (may expand to cover new React majors).
22
+
23
+ ### Breaking changes vs 0.4.x
24
+
25
+ None. v1.0 is API-compatible with 0.4.x — existing code continues to work. The major bump communicates stability commitment, not breakage.
26
+
27
+ ### Minor Changes
28
+
29
+ - 5b6c37f: Extract `@kalyx/adapter-date-fns` and make `@kalyx/core` neutral
30
+
31
+ 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.
32
+
33
+ ### What changed
34
+ - **`@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).
35
+ - **`@kalyx/adapter-date-fns`** — new package with the full `DateFnsAdapter` implementation moved verbatim. Same UTC semantics, same timezone-aware paths, same 35 adapter tests.
36
+ - **`@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.
37
+
38
+ ### Migration
39
+
40
+ If you imported `DateFnsAdapter` directly from `@kalyx/core`:
41
+
42
+ ```diff
43
+ - import { DateFnsAdapter } from '@kalyx/core';
44
+ + import { DateFnsAdapter } from '@kalyx/adapter-date-fns';
45
+ ```
46
+
47
+ `@kalyx/react` consumers don't need to change anything — the adapter is still re-exported from `@kalyx/react`.
48
+
49
+ ### Next (separate PR)
50
+
51
+ 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.
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
+ - 3db8444: feat: add `DatePicker.Presets` and `DatePicker.Preset` for single-date quick selection.
73
+
74
+ Mirrors the existing `RangePicker.Presets` API. Pass a predefined `value` key (`today`, `tomorrow`, `yesterday`, `startOfMonth`, `endOfMonth`, `startOfYear`) or a direct ISO via `date`.
75
+
76
+ ```tsx
77
+ <DatePicker value={date} onChange={setDate}>
78
+ <DatePicker.Input />
79
+ <DatePicker.Popover>
80
+ <DatePicker.Presets>
81
+ <DatePicker.Preset value="today">Today</DatePicker.Preset>
82
+ <DatePicker.Preset value="tomorrow">Tomorrow</DatePicker.Preset>
83
+ <DatePicker.Preset date="2026-12-25T00:00:00.000Z">Christmas</DatePicker.Preset>
84
+ </DatePicker.Presets>
85
+ <DatePicker.Calendar />
86
+ </DatePicker.Popover>
87
+ </DatePicker>
88
+ ```
89
+
90
+ - Active preset is marked `aria-selected="true"` when its resolved date matches the current value (timezone-aware).
91
+ - Clicking a preset commits and closes the popover.
92
+ - `displayTimezone` is honored when resolving "today"-relative presets.
93
+
94
+ - 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.
95
+
96
+ ```tsx
97
+ const holidays = new Set(['2026-01-01T00:00:00.000Z', '2026-12-25T00:00:00.000Z']);
98
+
99
+ <DatePicker
100
+ disabled={[
101
+ { dayOfWeek: [0, 6] }, // weekends
102
+ { filter: (iso) => holidays.has(iso) }, // holidays
103
+ ]}
104
+ >
105
+
106
+ </DatePicker>;
107
+ ```
108
+
109
+ 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).
110
+
111
+ - 56e1ce9: feat: add `onOpenChange` and `onCalendarNavigate` callbacks on `DatePicker`, `RangePicker`, and `DateTimePicker` Root components.
112
+ - `onOpenChange(isOpen: boolean)` fires whenever the popover opens or closes (regardless of trigger — click, keyboard, outside click, selection).
113
+ - `onCalendarNavigate(viewMonth: ISODateString)` fires when the calendar view moves to a different month. The emitted value is the first day of the newly-visible month in UTC.
114
+
115
+ Neither callback fires on initial mount. `TimePicker` does not expose these callbacks since it has no popover or calendar.
116
+
117
+ - 6fc7c59: feat: add `MonthPicker` — a headless month selector.
118
+
119
+ `MonthPicker` stores the selected month as the first day of that month in UTC-ISO form (e.g., `"2026-04-01T00:00:00.000Z"`). It reuses `DatePicker` infrastructure (Input, Trigger, Popover), so the only new primitive is `MonthPicker.Grid`, a 12-month commit grid with year navigation.
120
+
121
+ ```tsx
122
+ <MonthPicker value={month} onChange={setMonth}>
123
+ <MonthPicker.Input placeholder="Pick a month" />
124
+ <MonthPicker.Popover>
125
+ <MonthPicker.Grid />
126
+ </MonthPicker.Popover>
127
+ </MonthPicker>
128
+ ```
129
+
130
+ - Default `displayFormat` is `"yyyy-MM"`.
131
+ - `displayTimezone` is supported (commits map to civil midnight of month-start in the target zone).
132
+ - Month selection highlighting is timezone-aware — the grid reflects the month of the current value even when stored in zone-adjusted UTC form.
133
+ - Primary UX is click-to-select; full `yyyy-MM-dd` typed input still works via the inherited Input behavior.
134
+
135
+ - 4629384: chore(oss): unify node engines to >=20 and add public repository metadata
136
+ - `@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`.
137
+ - Root `package.json` now exposes `homepage`, `repository`, and `bugs` so `npm info` and the npm registry page link back to the GitHub repo.
138
+ - `.github/PULL_REQUEST_TEMPLATE.md` bundle ceiling updated `15KB → 16 KB` to match the post-rc.8 limit advertised in README and CI.
139
+ - `.gitignore` ignores `.codegraph/`, `.serena/`, and `.tmp-*/` (MCP server caches and worktree scratchpads).
140
+
141
+ - 44b3fa6: Add `@kalyx/react/headless` entry for adapter-explicit usage. Default `@kalyx/react` entry continues to auto-inject the date-fns adapter — no breaking change. Use the headless entry to opt out of the bundled date-fns and provide your own adapter (dayjs, luxon, custom). See [Adapters guide](https://kalyx-docs.vercel.app/docs/guides/adapters).
142
+ - 0d3b845: `TimePicker.Root` gains a programmatic **`filterTime`** prop — `(hours: number, minutes: number) => boolean` returning `true` for any slot that should be unselectable. Equivalent to `react-datepicker`'s `filterTime` and MUI X's `shouldDisableTime`, covering use cases the static `step` prop can't (business-hours-only, lunch breaks, blackout slots, per-day variations).
143
+
144
+ ```tsx
145
+ <TimePicker
146
+ value={time}
147
+ onChange={setTime}
148
+ step={15}
149
+ // Business hours only: 09:00–11:45 and 13:00–17:45 (no lunch slot)
150
+ filterTime={(h, m) => h < 9 || h >= 18 || h === 12}
151
+ >
152
+ <TimePicker.Input />
153
+ <TimePicker.HourList />
154
+ <TimePicker.MinuteList />
155
+ </TimePicker>
156
+ ```
157
+
158
+ Behavior:
159
+ - **`MinuteList`** — minutes for which `filterTime(currentHour, minute)` returns `true` get `aria-disabled="true"` and reject click/Enter.
160
+ - **`HourList`** — an hour is marked `aria-disabled="true"` only when `filterTime` returns `true` for **every** step minute within it. Hours with at least one open minute remain selectable.
161
+ - 12-hour mode — the predicate always receives 24-hour values (`0`–`23`) regardless of the picker's display format.
162
+
163
+ **Note**: `DateTimePicker` does not yet wire this through — combine `DatePicker.Root` + `TimePicker.Root` manually if you need both date and time-slot filtering in the same picker.
164
+
165
+ Bundle ceiling raised 15 → 16 KB (PR #N follows the 12→13→14→15 cadence — each raise tied to a documented feature; CLAUDE.md §2 records the chain). Measured 15.01 KB ESM / 15.16 KB CJS at this commit, ~4× smaller than react-datepicker.
166
+
167
+ - 6fdf8fe: feat: add `WeekPicker` — a headless week selector.
168
+
169
+ `WeekPicker` stores the selected week as a `DateRange` covering all seven days (based on `weekStartsOn`). Unlike `RangePicker`, a single click on any day selects the entire week containing that day.
170
+
171
+ ```tsx
172
+ <WeekPicker value={week} onChange={setWeek} weekStartsOn={1}>
173
+ <WeekPicker.Input part="start" />
174
+ <WeekPicker.Input part="end" />
175
+ <WeekPicker.Popover>
176
+ <WeekPicker.Calendar />
177
+ </WeekPicker.Popover>
178
+ </WeekPicker>
179
+ ```
180
+
181
+ - Reuses `RangePicker` Root / Input / Popover; only `WeekPicker.Calendar` is new.
182
+ - `weekStartsOn` (0=Sunday, 1=Monday) controls which seven days constitute a week.
183
+ - Enter / Space on the focused day commits the full week containing it.
184
+ - `displayTimezone`, `disabled` rules, and all other RangePicker props are supported.
185
+
186
+ - 6fc7c59: feat: add `YearPicker` — a headless year selector.
187
+
188
+ `YearPicker` stores the selected year as Jan 1 of that year in UTC-ISO form (e.g., `"2026-01-01T00:00:00.000Z"`). It reuses `DatePicker` infrastructure (Input, Trigger, Popover) and exposes `YearPicker.Grid`, a 12-year decade commit grid with decade navigation.
189
+
190
+ ```tsx
191
+ <YearPicker value={year} onChange={setYear}>
192
+ <YearPicker.Input placeholder="Pick a year" />
193
+ <YearPicker.Popover>
194
+ <YearPicker.Grid />
195
+ </YearPicker.Popover>
196
+ </YearPicker>
197
+ ```
198
+
199
+ - Default `displayFormat` is `"yyyy"`.
200
+ - `displayTimezone` is supported with timezone-aware year highlighting.
201
+ - Primary UX is click-to-select; full `yyyy-MM-dd` typed input still works via the inherited Input behavior.
202
+
203
+ ### Patch Changes
204
+
205
+ - 63fb80a: fix(datetimepicker): close composition-API gap and expose missing public types
206
+ - `<DateTimePicker.Root>` now accepts `withSeconds` and `filterTime` props (was silently hard-coded to `withSeconds: false`, `filterTime: undefined`)
207
+ - `currentTime` no longer calls `DateFnsAdapter.today()` during render when `value` is null — eliminates the UTC-midnight hydration mismatch risk
208
+ - Public API now re-exports `CalendarWeek`, `CalendarGrid`, `CalendarOptions`, `WeekStartsOn`, `WeekdayInfo`, every `{Picker}Labels` type, and the four `DEFAULT_*_LABELS` runtime constants per CLAUDE.md §6
209
+
210
+ - 1ca818c: fix(react): prevent WeekPicker from mutating RangePicker.Calendar
211
+
212
+ `WeekPicker` previously called `Object.assign(RangePickerRoot, { ..., Calendar: WeekPickerCalendar })`, which mutates the shared `RangePickerRoot` function object. Because `RangePicker.Calendar` is attached to the same object (via the earlier `Object.assign` in `RangePicker/index.ts`), importing `WeekPicker` would overwrite `RangePicker.Calendar` with `WeekPickerCalendar`.
213
+
214
+ Users of `RangePicker` would then see week-selection behavior (single click commits a full week and closes the popover) instead of the documented two-click range flow — even without importing `WeekPicker` directly, because both pickers share the module graph.
215
+
216
+ Added an internal `WeekPickerRoot` wrapper that the `Object.assign` target now uses, preserving `RangePickerRoot.Calendar` intact.
217
+
218
+ Caught by the `RangePicker › select range in start-date -> end-date order` Playwright test; all existing behavior is restored.
219
+
220
+ - 19ac1c0: fix(core): allow `generateMinutes` step values up to 60
221
+
222
+ `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.
223
+
224
+ - eafc3c1: fix(input): drop stale typed text when the parent re-sets value externally
225
+
226
+ `<DatePicker.Input>` and `<TimePicker.Input>` keep half-typed text in a local `inputText` state while the user is editing — without it, parse-failed input would vanish on every keystroke. The state was reset only when the user committed via blur/Enter, which left a real gap:
227
+
228
+ If the value changed from anywhere else (parent re-rendered with a new `value`, a calendar click, a `Preset`, a custom-Hook `setRange`, an HourList option), the Input kept rendering the user's stale text. Source-of-truth and visible value diverged silently.
229
+
230
+ A `useEffect` keyed on `ctx.value` now resets `inputText` whenever the source-of-truth changes. The Input goes back to formatting the new value normally. For DatePicker the reset is skipped while an IME composition is in flight (Korean/Japanese/Chinese), so an in-flight character is never wiped mid-stroke.
231
+
232
+ Impact: `DatePicker`, `MonthPicker`, `YearPicker` (the last two reuse `DatePickerInput`), and `TimePicker` all get the fix. `RangePicker`/`WeekPicker`/`DateTimePicker` Inputs are read-only or non-editable and already track context directly, so they were never affected.
233
+
234
+ - 23bc187: docs(i18n): translate Korean intro and stop bumping demo apps on every patch
235
+ - `apps/docs-site/i18n/ko/.../intro.md` is now fully translated to Korean. Previously the file lived in the `i18n/ko/` tree but the body was the verbatim English copy, so Korean docs visitors saw English content on the landing page.
236
+ - `.changeset/config.json` `ignore` now includes the two demo workspaces (`@kalyx/docs`, `docs-site`). Changesets used to bump them on every release because they depend on `@kalyx/react`, polluting their CHANGELOG with `Updated dependencies` entries and adding noise to release PRs. Demo apps aren't published — they don't need versioning.
237
+
238
+ - 3afb15b: Fix popover styling regression that broke documentation live previews.
239
+ - `DatePicker.Popover` and `RangePicker.Popover` now merge user-provided `style` props _under_ Floating UI's positioning instead of being overwritten by it. Previously, passing `style={{...}}` to a Popover stripped away `position: absolute`, `top`, `left`, and `transform`, causing the popover to render as a static block at full container width.
240
+ - The popover is now hidden until Floating UI computes its position, eliminating an unpositioned first-frame flash on every open.
241
+ - The shared `usePopover` hook also wires the floating element's reference synchronously in the ref callback, so positioning is resolved before paint in most cases.
242
+
243
+ - c8a6609: fix(rangepicker): announce next selection target and final range to screen readers
244
+
245
+ `<RangePicker.Calendar>` now announces context-aware messages through its existing `role="status"` live region:
246
+ - 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.
247
+ - 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.
248
+ - Week-mode commits now share the same `Range selected: ...` prefix for consistency.
249
+
250
+ 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.
251
+
252
+ - 3587b13: Replace deprecated `MutableRefObject<T>` with `RefObject<T>` in context types.
253
+
254
+ `@types/react@19` marks `MutableRefObject` as deprecated (`Use 'RefObject' instead`). In React 19 `RefObject<T>` is itself mutable, so the swap is type-equivalent for the existing `referenceRef` usage in `DatePickerContext` and `RangePickerContext`.
255
+
256
+ No runtime change. No public API surface change.
257
+
258
+ - abc56ac: Security: pin transitive `fast-uri` to `>=3.1.2` and `@babel/plugin-transform-modules-systemjs` to `>=7.29.4` via `pnpm.overrides`.
259
+
260
+ Resolves three Code Scanning alerts on `pnpm-lock.yaml`:
261
+ - `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`.
262
+ - `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`.
263
+ - `@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.
264
+
265
+ All three packages are transitive build-time dependencies (ajv → fast-uri, Babel preset-env → systemjs plugin); no public API impact.
266
+
267
+ - aadb512: Security: pin transitive `postcss` to `>=8.5.10` via `pnpm.overrides`.
268
+
269
+ 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.
270
+
271
+ - e5bd203: test(ssr): cover controlled value at a DST boundary with displayTimezone across all 7 pickers
272
+
273
+ The existing `renderToString` smoke tests only exercised the default `value=null` (or generic non-DST) path. They missed the highest-risk hydration scenario: a controlled value rendered on a DST transition day (2026-03-08 US Eastern spring-forward) while `displayTimezone="America/New_York"` forces the calendar/highlighting/time rows to map UTC ↔ civil time across the seam.
274
+
275
+ Each picker now has one new determinism test inside its `SSR safety` describe that renders the same tree twice via `renderToString` and asserts byte-identical output. Any accidental clock-read or non-deterministic `Intl` path during render would surface as a string diff.
276
+ - `DatePicker` — 2026-03-08 day cell + popover + calendar
277
+ - `RangePicker` — range straddling the DST seam
278
+ - `TimePicker` — value at 02:00 EST → 03:00 EDT
279
+ - `DateTimePicker` — full date + time tree (highest hydration surface)
280
+ - `MonthPicker` — March 2026 month grid
281
+ - `YearPicker` — 2026 decade grid
282
+ - `WeekPicker` — week containing the spring-forward day
283
+
284
+ No production code changed; the suite goes from 314 → 321 picker tests and locks the current SSR-deterministic behaviour against future regressions.
285
+
286
+ - 1a77283: docs: clarify `TimePicker.filterTime` polarity. The predicate returns `true` to mark a slot **unselectable** — same polarity as MUI X's `shouldDisableTime`, and the **inverse** of react-datepicker's `filterTime` (which returns `true` to _keep_ a slot). Earlier JSDoc/changelog called it "equivalent to react-datepicker's `filterTime`", which is misleading because the polarity is reversed; react-datepicker migrators must invert their predicate. No runtime behavior change — JSDoc, the published package description (≤16 KB), and docs only.
287
+ - 4178a92: fix(timepicker, rangepicker): hydration-safe time fallback and memoized preset resolution
288
+ - `<TimePicker.Root>` no longer calls `DateFnsAdapter.today()` during render when `value` is null. The displayed `currentTime` now falls back to a stable `{ hours: 0, minutes: 0, seconds: 0 }` and `today()` is resolved at event time inside `setTime`. Removes the UTC-midnight SSR/CSR hydration mismatch risk.
289
+ - `<RangePicker.Preset>` memoizes the resolved preset range. Previously `resolvePreset` (and `adapter.today()`) ran twice per render per preset — once in the click handler and once in the `isActive` getter — turning a 5-preset row into 10 `today()` allocations per render. No behavioral change.
290
+
291
+ - b40080d: Internal: sync the `tsup` onSuccess bundle-size budget from `13 KB` to `15 KB` so the per-build warning matches the actual CI gate (`scripts/check-bundle-size.js`, `pr-check.yml`, `release.yml`). No runtime change; the published artifact is byte-identical.
292
+
293
+ This was a leftover from the 13 → 14 → 15 KB ceiling raises during RC (PR #46 / PR #48); only the tsup-side TARGET_KB was missed during those bumps, so local `pnpm build` printed a spurious `⚠️` even though CI passed.
294
+
295
+ - df97687: P1 audit follow-ups for v1.0-rc:
296
+ - **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.
297
+ - **`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.
298
+ - **`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.
299
+ - **`RangePicker.Calendar` no longer advertises `aria-multiselectable="true"`** — a date range is one selection (two endpoints), not a multi-select grid.
300
+ - **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).
301
+ - **`labels.ts` test coverage** — first unit tests for the default-label exports.
302
+
303
+ Behavioral notes for users (none of these are breaking for code that follows the documented `data-*` styling contract):
304
+ - If you targeted Preset buttons via `[aria-selected="true"]` in CSS, switch to `[aria-pressed="true"]` or `[data-active]`.
305
+ - If you targeted the range grid via `[aria-multiselectable]`, that attribute is gone; use `[role="grid"]` on the calendar root instead.
306
+
307
+ - 21f3c1f: Resolve v1.0-rc release-blocking defects (P0):
308
+ - **`"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.
309
+ - **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.
310
+ - **`@kalyx/core` version sync** — bumped from `1.0.0-rc.0` to `1.0.0-rc.1` to match `@kalyx/react`.
311
+ - **`@kalyx/core` package contents** — `LICENSE` and `CHANGELOG.md` are now included in the npm tarball (`files` field).
312
+ - **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>`.
313
+ - **`aria-haspopup="dialog"` on Trigger** — completes the WAI-ARIA combobox/dialog pattern.
314
+ - **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.
315
+
316
+ - 733c0a1: Follow-up to the v1.0-rc audit series:
317
+ - **Fix WebKit Enter regression** — `DatePicker.Input` now commits the calendar's currently focused day on Enter when the popover is open and no text was typed. WebKit doesn't always shift focus from the input to the day button on `input.click()`, so the previous behavior (preventDefault but no commit) left the popover dangling. Other browsers also benefit when the user presses Enter immediately after opening.
318
+ - **Consolidate parsing path** — extract the parse-and-commit logic into a single `commitText` helper used by `onChange`, `onBlur`, `onCompositionEnd`, and Enter. Removes ~60 bytes of duplicated logic.
319
+ - **Bundle target raised to 13 KB** — the v1.0-rc accessibility / API additions (IME composition, popover focus-out, hidden form input, `aria-rowindex`/`colindex`, `displayName`, keyboard skip-disabled) accumulated to 12.07 KB gzip; the 12 KB ceiling was 0.07 KB tight. README, docs, `tsup.config.ts`, and `scripts/check-bundle-size.js` updated to ≤13 KB. Still ~3× smaller than `react-datepicker`.
320
+
321
+ - 3228533: P1 v1.0-rc API/a11y/docs improvements:
322
+ - **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.
323
+ - **`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.
324
+ - **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.
325
+ - **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`.
326
+ - **Package metadata** — `peerDependenciesMeta`, `engines.node`, and `publishConfig.provenance` added to both `@kalyx/react` and `@kalyx/core`.
327
+ - **`@kalyx/react` description** — corrected from "under 10 KB gzipped" (false claim) to "≤12 KB gzipped".
328
+
329
+ - e8519d0: Performance: memoize hot paths to avoid wasted recomputation:
330
+ - `DatePicker.Calendar` and `RangePicker.Calendar` now `useMemo` their `getCalendarDays` and `getWeekdayNames` results. Previously the 42-cell grid and 7 weekday tuples were rebuilt every parent re-render even when none of the inputs changed.
331
+ - `TimePicker.HourList` and `TimePicker.MinuteList` `useMemo` their `generateHours(format)` / `generateMinutes(step)` arrays so the listbox identity is stable across renders.
332
+ - `usePopover` middleware (`offset` / `flip` / `shift`) is now hoisted to a module-level constant, eliminating Floating UI's repeated middleware-array reconciliation.
333
+
334
+ - b6129ed: P2 polish for v1.0-rc:
335
+ - **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.
336
+ - **`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.
337
+ - **JSDoc on `DatePicker.Input` and `DatePicker.Trigger`** — public API surface for the most-used components has explanatory docstrings.
338
+ - **`addYears` leap-day regression tests** — locked the date-fns clamp behavior (2024-02-29 + 1y → 2025-02-28, not March 1).
339
+ - **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.
340
+ - **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.
341
+
342
+ - 9f3cf9b: WAI-ARIA grid keyboard navigation for the four 3×4 picker grids
343
+ (`DatePicker.MonthGrid`, `DatePicker.YearGrid`, `MonthPicker.Grid`,
344
+ `YearPicker.Grid`).
345
+
346
+ Before, these grids declared `role="grid"` but had no key handler — keyboard
347
+ users could not select a month or year, in violation of CLAUDE.md §7.
348
+
349
+ Now each grid implements:
350
+ - **Arrow keys** — ±1 column / ±3 rows, clamped to grid bounds.
351
+ - **Home / End** — first / last cell of the current row.
352
+ - **PageUp / PageDown** — previous / next year (or decade for year grids).
353
+ - **Enter / Space** — commit the focused cell (drilldown grids switch view via
354
+ `onSelect`; commit grids close the popover via `ctx.selectDate`).
355
+ - **Roving tabIndex** — only the focused cell has `tabIndex=0`; the
356
+ `data-focused` attribute follows.
357
+ - **Auto-refocus** — DOM focus moves with `focusedIndex` so PageUp/Down lands
358
+ the user back on the same column position. Cells use stable index keys so
359
+ the buttons persist across page nav.
360
+
361
+ Component-level integration tests added per CLAUDE.md §7 across `DatePicker`,
362
+ `RangePicker`, `DateTimePicker`, and `WeekPicker`: leap-year (Feb 29 2024)
363
+ click commit, `before`/`after` rule click block, `dayOfWeek` rule click block
364
+ plus visual `aria-disabled`, and keyboard ArrowLeft skip-disabled.
365
+
366
+ **Bundle target raised to 14 KB** — full grid keyboard nav (state + handlers
367
+ - auto-refocus) added ~1.4 KB gzip across the four grids. Measured 12.85 KB
368
+ ESM / 13.64 KB CJS at this point. README, docs, `scripts/check-bundle-size.js`,
369
+ PR template, and CI gate updated to ≤14 KB.
370
+
371
+ **Internal:** new shared `useGridState` hook in
372
+ `packages/react/src/components/_shared/grid-keyboard.ts` (not exported from
373
+ the package public API) consolidates keyboard handling and roving-focus
374
+ state across all four grids.
375
+
376
+ - 9b19df4: `MonthPicker.Grid` and `YearPicker.Grid` now respect `before` / `after`
377
+ disabled rules — months/years that fall entirely outside the allowed range
378
+ are rendered with the `disabled` HTML attribute, `aria-disabled="true"`, the
379
+ new `monthDisabled` / `yearDisabled` className slots, and are skipped during
380
+ keyboard navigation.
381
+
382
+ This was deliberately deferred from PR #46 to keep that bundle under 14 KB;
383
+ it lands now with a 14 → 15 KB ceiling bump.
384
+
385
+ Behavioral details:
386
+ - A month is "fully disabled" only when every day in it is excluded by a
387
+ `before` or `after` rule. `date` and `dayOfWeek` rules can never disable a
388
+ whole month, so they remain a per-day concern.
389
+ - A year follows the same rule against `[Jan 1 00:00:00, Dec 31 23:59:59.999]`.
390
+ - Click and keyboard `Enter` / `Space` on a disabled cell are no-ops.
391
+ - Initial focus and post-PageUp/PageDown focus both re-anchor to the first
392
+ enabled cell when the natural target is itself disabled. (A `disabled`
393
+ HTML button can't receive DOM focus, so without the re-anchor the user
394
+ would silently lose keyboard navigation.)
395
+
396
+ **Internal:** `useGridState` regains its optional `disabledFlags` parameter
397
+ plus a focus re-anchor effect; `isRangeFullyDisabled` is reintroduced as an
398
+ internal helper. Neither is exposed in the package public API.
399
+
400
+ **Bundle target:** raised 14 → 15 KB (measured 13.96 KB ESM / 14.21 KB CJS).
401
+ Same precedent as the 12 → 13 KB and 13 → 14 KB bumps when prior feature
402
+ work landed. Updated `scripts/check-bundle-size.js`, `pr-check.yml`, READMEs,
403
+ CLAUDE.md, PR template, and `check-bundle.md`.
404
+
405
+ - Updated dependencies [5b6c37f]
406
+ - Updated dependencies [0eca2e8]
407
+ - Updated dependencies [d62c84e]
408
+ - Updated dependencies [19ac1c0]
409
+ - Updated dependencies [4629384]
410
+ - Updated dependencies [c8a6609]
411
+ - Updated dependencies [3587b13]
412
+ - Updated dependencies [abc56ac]
413
+ - Updated dependencies [aadb512]
414
+ - Updated dependencies [0556886]
415
+ - Updated dependencies [ca7180e]
416
+ - Updated dependencies [df97687]
417
+ - Updated dependencies [21f3c1f]
418
+ - Updated dependencies [3228533]
419
+ - Updated dependencies [b6129ed]
420
+ - @kalyx/core@1.0.0
421
+ - @kalyx/adapter-date-fns@1.0.0
422
+
423
+ ## 1.0.0-rc.14
424
+
425
+ ### Minor Changes
426
+
427
+ - 44b3fa6: Add `@kalyx/react/headless` entry for adapter-explicit usage. Default `@kalyx/react` entry continues to auto-inject the date-fns adapter — no breaking change. Use the headless entry to opt out of the bundled date-fns and provide your own adapter (dayjs, luxon, custom). See [Adapters guide](https://kalyx-docs.vercel.app/docs/guides/adapters).
428
+
429
+ ## 1.0.0-rc.13
430
+
431
+ ### Minor Changes
432
+
433
+ - 5b6c37f: Extract `@kalyx/adapter-date-fns` and make `@kalyx/core` neutral
434
+
435
+ 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.
436
+
437
+ ### What changed
438
+ - **`@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).
439
+ - **`@kalyx/adapter-date-fns`** — new package with the full `DateFnsAdapter` implementation moved verbatim. Same UTC semantics, same timezone-aware paths, same 35 adapter tests.
440
+ - **`@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.
441
+
442
+ ### Migration
443
+
444
+ If you imported `DateFnsAdapter` directly from `@kalyx/core`:
445
+
446
+ ```diff
447
+ - import { DateFnsAdapter } from '@kalyx/core';
448
+ + import { DateFnsAdapter } from '@kalyx/adapter-date-fns';
449
+ ```
450
+
451
+ `@kalyx/react` consumers don't need to change anything — the adapter is still re-exported from `@kalyx/react`.
452
+
453
+ ### Next (separate PR)
454
+
455
+ 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.
456
+
457
+ ### Patch Changes
458
+
459
+ - Updated dependencies [5b6c37f]
460
+ - @kalyx/core@1.0.0-rc.13
461
+ - @kalyx/adapter-date-fns@1.0.0-rc.1
462
+
463
+ ## 1.0.0-rc.12
464
+
465
+ ### Patch Changes
466
+
467
+ - e5bd203: test(ssr): cover controlled value at a DST boundary with displayTimezone across all 7 pickers
468
+
469
+ The existing `renderToString` smoke tests only exercised the default `value=null` (or generic non-DST) path. They missed the highest-risk hydration scenario: a controlled value rendered on a DST transition day (2026-03-08 US Eastern spring-forward) while `displayTimezone="America/New_York"` forces the calendar/highlighting/time rows to map UTC ↔ civil time across the seam.
470
+
471
+ Each picker now has one new determinism test inside its `SSR safety` describe that renders the same tree twice via `renderToString` and asserts byte-identical output. Any accidental clock-read or non-deterministic `Intl` path during render would surface as a string diff.
472
+ - `DatePicker` — 2026-03-08 day cell + popover + calendar
473
+ - `RangePicker` — range straddling the DST seam
474
+ - `TimePicker` — value at 02:00 EST → 03:00 EDT
475
+ - `DateTimePicker` — full date + time tree (highest hydration surface)
476
+ - `MonthPicker` — March 2026 month grid
477
+ - `YearPicker` — 2026 decade grid
478
+ - `WeekPicker` — week containing the spring-forward day
479
+
480
+ No production code changed; the suite goes from 314 → 321 picker tests and locks the current SSR-deterministic behaviour against future regressions.
481
+
482
+ - Updated dependencies [0556886]
483
+ - @kalyx/core@1.0.0-rc.12
484
+
485
+ ## 1.0.0-rc.11
486
+
487
+ ### Patch Changes
488
+
489
+ - 19ac1c0: fix(core): allow `generateMinutes` step values up to 60
490
+
491
+ `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.
492
+
493
+ - eafc3c1: fix(input): drop stale typed text when the parent re-sets value externally
494
+
495
+ `<DatePicker.Input>` and `<TimePicker.Input>` keep half-typed text in a local `inputText` state while the user is editing — without it, parse-failed input would vanish on every keystroke. The state was reset only when the user committed via blur/Enter, which left a real gap:
496
+
497
+ If the value changed from anywhere else (parent re-rendered with a new `value`, a calendar click, a `Preset`, a custom-Hook `setRange`, an HourList option), the Input kept rendering the user's stale text. Source-of-truth and visible value diverged silently.
498
+
499
+ A `useEffect` keyed on `ctx.value` now resets `inputText` whenever the source-of-truth changes. The Input goes back to formatting the new value normally. For DatePicker the reset is skipped while an IME composition is in flight (Korean/Japanese/Chinese), so an in-flight character is never wiped mid-stroke.
500
+
501
+ Impact: `DatePicker`, `MonthPicker`, `YearPicker` (the last two reuse `DatePickerInput`), and `TimePicker` all get the fix. `RangePicker`/`WeekPicker`/`DateTimePicker` Inputs are read-only or non-editable and already track context directly, so they were never affected.
502
+
503
+ - 23bc187: docs(i18n): translate Korean intro and stop bumping demo apps on every patch
504
+ - `apps/docs-site/i18n/ko/.../intro.md` is now fully translated to Korean. Previously the file lived in the `i18n/ko/` tree but the body was the verbatim English copy, so Korean docs visitors saw English content on the landing page.
505
+ - `.changeset/config.json` `ignore` now includes the two demo workspaces (`@kalyx/docs`, `docs-site`). Changesets used to bump them on every release because they depend on `@kalyx/react`, polluting their CHANGELOG with `Updated dependencies` entries and adding noise to release PRs. Demo apps aren't published — they don't need versioning.
506
+
507
+ - c8a6609: fix(rangepicker): announce next selection target and final range to screen readers
508
+
509
+ `<RangePicker.Calendar>` now announces context-aware messages through its existing `role="status"` live region:
510
+ - 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.
511
+ - 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.
512
+ - Week-mode commits now share the same `Range selected: ...` prefix for consistency.
513
+
514
+ 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.
515
+
516
+ - Updated dependencies [19ac1c0]
517
+ - Updated dependencies [c8a6609]
518
+ - @kalyx/core@1.0.0-rc.11
519
+
520
+ ## 1.0.0-rc.10
521
+
522
+ ### Minor Changes
523
+
524
+ - 4629384: chore(oss): unify node engines to >=20 and add public repository metadata
525
+ - `@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`.
526
+ - Root `package.json` now exposes `homepage`, `repository`, and `bugs` so `npm info` and the npm registry page link back to the GitHub repo.
527
+ - `.github/PULL_REQUEST_TEMPLATE.md` bundle ceiling updated `15KB → 16 KB` to match the post-rc.8 limit advertised in README and CI.
528
+ - `.gitignore` ignores `.codegraph/`, `.serena/`, and `.tmp-*/` (MCP server caches and worktree scratchpads).
529
+
530
+ ### Patch Changes
531
+
532
+ - 63fb80a: fix(datetimepicker): close composition-API gap and expose missing public types
533
+ - `<DateTimePicker.Root>` now accepts `withSeconds` and `filterTime` props (was silently hard-coded to `withSeconds: false`, `filterTime: undefined`)
534
+ - `currentTime` no longer calls `DateFnsAdapter.today()` during render when `value` is null — eliminates the UTC-midnight hydration mismatch risk
535
+ - Public API now re-exports `CalendarWeek`, `CalendarGrid`, `CalendarOptions`, `WeekStartsOn`, `WeekdayInfo`, every `{Picker}Labels` type, and the four `DEFAULT_*_LABELS` runtime constants per CLAUDE.md §6
536
+
537
+ - 4178a92: fix(timepicker, rangepicker): hydration-safe time fallback and memoized preset resolution
538
+ - `<TimePicker.Root>` no longer calls `DateFnsAdapter.today()` during render when `value` is null. The displayed `currentTime` now falls back to a stable `{ hours: 0, minutes: 0, seconds: 0 }` and `today()` is resolved at event time inside `setTime`. Removes the UTC-midnight SSR/CSR hydration mismatch risk.
539
+ - `<RangePicker.Preset>` memoizes the resolved preset range. Previously `resolvePreset` (and `adapter.today()`) ran twice per render per preset — once in the click handler and once in the `isActive` getter — turning a 5-preset row into 10 `today()` allocations per render. No behavioral change.
540
+
541
+ - Updated dependencies [4629384]
542
+ - @kalyx/core@1.0.0-rc.10
543
+
3
544
  ## 1.0.0-rc.9
4
545
 
5
546
  ### Patch Changes
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # @kalyx/react
2
2
 
3
- > The headless React DatePicker, finally complete. Zero CSS · SSR-safe · ~14 KB gzip (≤ 15 KB ceiling).
3
+ > The headless React DatePicker, finally complete. Zero CSS · SSR-safe · ~15.63 KB gzip (≤ 16 KB ceiling).
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@kalyx/react?color=5b4fe1)](https://www.npmjs.com/package/@kalyx/react)
6
- [![Bundle](https://img.shields.io/badge/gzip-13.60KB-brightgreen)](https://kalyx-docs.vercel.app/docs/api/react#bundle-size)
6
+ [![Bundle](https://img.shields.io/badge/gzip-15.63KB-brightgreen)](https://kalyx-docs.vercel.app/docs/api/react#bundle-size)
7
7
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue)](https://www.typescriptlang.org/)
8
8
  [![License](https://img.shields.io/badge/license-MIT-green)](https://github.com/jiji-hoon96/kalyx/blob/main/LICENSE)
9
9
 
@@ -87,6 +87,21 @@ Every sub-component forwards `className`, `style`, and `ref`, and accepts a `cla
87
87
 
88
88
  Full recipes: [Tailwind](https://kalyx-docs.vercel.app/docs/recipes/tailwind), [shadcn/ui](https://kalyx-docs.vercel.app/docs/recipes/shadcn), [React Hook Form](https://kalyx-docs.vercel.app/docs/recipes/react-hook-form).
89
89
 
90
+ ## Bring your own adapter
91
+
92
+ Already shipping `dayjs`, `luxon`, or `Temporal`? Skip the bundled `date-fns` and import from `@kalyx/react/headless` instead — same component surface, no auto-installed adapter:
93
+
94
+ ```tsx
95
+ import { DatePicker } from '@kalyx/react/headless';
96
+ import { DayjsAdapter } from './my-dayjs-adapter'; // your DateAdapter
97
+
98
+ <DatePicker adapter={DayjsAdapter} value={iso} onChange={setIso}>
99
+ <DatePicker.Calendar />
100
+ </DatePicker>
101
+ ```
102
+
103
+ If you forget the `adapter` prop, the Root throws a clear error telling you exactly what's missing. The full how-to (interface, dayjs reference implementation, edge cases) is in the [adapters guide](https://kalyx-docs.vercel.app/docs/guides/adapters).
104
+
90
105
  ## Documentation
91
106
 
92
107
  - [Introduction](https://kalyx-docs.vercel.app/docs/intro)