@lekoala/date-picker 0.2.0 → 0.4.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.
@@ -38,13 +38,15 @@ Weekday headings use visible short labels plus a full `abbr` value.
38
38
  | Shift+PageDown | same/clamped day in next year |
39
39
  | Enter / Space | activate focused date |
40
40
 
41
- Moving focus never changes selection.
41
+ Moving focus never changes selection. Each cell that actually receives focus dispatches `datefocus`, so an application can follow keyboard navigation without reading internal DOM classes; hover never dispatches it, and assigning `focusedDate` without moving focus stays silent.
42
42
 
43
- ## Month/year controls
43
+ A range preview projected onto the band is visual only: it never contributes a range start/end to a day's accessible name, so a proposal is never announced as a committed selection.
44
+
45
+ Endpoint dragging is a pointer enhancement. Every adjustment it offers is also reachable from the keyboard: activating before or after a complete range moves the corresponding bound.
44
46
 
45
- The prototype deliberately uses native selects. They are easy to discover, keyboard-operable, and do not require nested custom popovers.
47
+ ## Month/year controls
46
48
 
47
- The year select is a moving window around the displayed year when no hard min/max exists. Navigating past the current window causes a normal re-render centered on the new year.
49
+ The month control is a native select and the year control is a native number input. They are easy to discover, keyboard-operable, and do not require nested custom popovers.
48
50
 
49
51
  ## Date picker input
50
52
 
@@ -99,19 +101,29 @@ The same resolved date state used by the grid is used for manual-entry validatio
99
101
 
100
102
  Forced colors should not rely on the normal selected-day fill tokens.
101
103
 
102
- When `forced-colors: active`, the selected day switches to a border-based treatment:
104
+ When `forced-colors: active`, the selected day and range endpoints switch to a border-based treatment:
103
105
 
104
106
  - `Canvas` background;
105
107
  - `CanvasText` foreground;
106
- - `Highlight` outer border;
107
- - an inner `CanvasText` ring.
108
+ - `2px Highlight` outer border;
109
+ - in-range days keep a `1px Highlight` underline (the range fill is lost and the browser does not know the semantics).
108
110
 
109
- Today keeps the base 1px outline — `border-color: var(--dp-accent)` force-adjusts to `CanvasText` here, so it stays a thin `CanvasText` border. Selected and today therefore remain distinct through border color, width, the inner ring and weight (selected is `700`), not through a shared system key.
110
-
111
- This avoids relying on white text over a `Highlight` fill, which can become ambiguous when the active high-contrast theme resolves `Highlight` near-white or when small white digits rasterize poorly. The disabled state is `GrayText` at full opacity.
111
+ Today keeps a thin `1px Highlight` border while selected carries the wider border, so the two stay distinct through border width. The disabled state relies on the native dimming, verified by capture. The month select hands its caret back to the browser (`appearance: auto`), since the author-drawn gradient is a non-system color.
112
112
 
113
113
  Guard: re-run the `color-contrast` review and the forced-colors screenshot bundle whenever `--dp-*` styling or day-cell rendering changes.
114
114
 
115
+ ## Visible focus
116
+
117
+ Fields (including native time companions), calendar header controls and day cells use an inner solid outline. `--dp-focus` defaults to `var(--dp-accent)` and `--dp-focus-width` defaults to `2px`; the inward offset follows the width. This replaces the previous accent border and soft halo on fields and header controls. The resting input appearance stays consumer-owned.
118
+
119
+ Focus is only ever drawn on a real box. The calendar trigger is a real button painted inside the date field's own box, so the field's focus ring naturally wraps the affordance too — no host-level ring, no pseudo-element. When the trigger itself is focused it shows a small inner ring, like the native `input[type=time]` indicator, instead of pretending the whole field is focused.
120
+
121
+ Day cells use an **inner** ring too: keyboard focus stays inside the 44px cell instead of reading as a second selected state, and it never bleeds onto a neighbouring range band. The ring is an `outline` with `outline-offset: -2px`, not an inset `box-shadow`, because forced colors computes `box-shadow` to `none` — an inset shadow would erase the keyboard indicator (including on disabled days, which must stay discoverable). `outline-color` is force-adjusted to a system color there, so the ring survives high contrast. On a cell already filled with the accent (selected, range endpoints) the ring inverts to `--dp-accent-fg` so it stays readable.
122
+
123
+ Each native time companion is an independent control and keeps its own ring; they are not joined, so rings never merge and no neighbour swallows another's focus. In `[range]`, each bound owns its trigger and its own ring, and focus follows the active bound rather than the original trigger.
124
+
125
+ Closing returns focus to the control that opened the popover (field or trigger) in single mode.
126
+
115
127
  ## Recommended manual test matrix
116
128
 
117
129
  Beyond what the automated suite covers, the following pairings should be exercised before a broad adoption of 0.1. Results are recorded in [AT-CHECKLIST.md](AT-CHECKLIST.md); the human rows there are still open:
package/docs/API.md CHANGED
@@ -31,7 +31,7 @@ Presentation-only range band. It never changes `value`, `selection` or `focusedD
31
31
  calendar.isDateDisabled = (date) => boolean;
32
32
  calendar.dateState = (date, sourceState) => ({
33
33
  disabled,
34
- enabled, // true = explicit exception to generic disabled rules
34
+ enabled, // true = lifts isDateDisabled() only; never min/max or resolved disabled
35
35
  description,
36
36
  ...applicationMetadata
37
37
  });
@@ -62,6 +62,7 @@ calendar.nextMonth();
62
62
  calendar.focusDate("2026-09-10");
63
63
  calendar.focusGrid();
64
64
  calendar.getDateState("2026-09-10");
65
+ await calendar.activateDate("2026-09-10");
65
66
  await calendar.ensureDate("2026-09-10");
66
67
  await calendar.refreshSource();
67
68
  calendar.render();
@@ -87,6 +88,28 @@ calendar.addEventListener("dateactivate", (event) => {
87
88
  });
88
89
  ```
89
90
 
91
+ `activateDate(date)` runs that same path on demand: availability check, then
92
+ the cancelable event, then selection. Click, keyboard and the range endpoint
93
+ drag all go through it, so an application that cancels `dateactivate` cannot be
94
+ bypassed by a different input device.
95
+
96
+ #### `datefocus`
97
+
98
+ Fired when a date cell actually receives focus, with `{ date, state }`. Arrow,
99
+ Home, End, PageUp and PageDown all emit one through the cell that took focus.
100
+
101
+ ```js
102
+ calendar.addEventListener("datefocus", (event) => {
103
+ console.log(event.detail.date);
104
+ });
105
+ ```
106
+
107
+ It observes navigation and nothing else: a disabled day still reports it (grid
108
+ navigation stays discoverable), hover never does, and assigning `focusedDate`
109
+ without moving DOM focus stays silent. The three verbs are distinct —
110
+ `datefocus` observes, `dateactivate` requests an activation, `datechange`
111
+ observes a selection.
112
+
90
113
  #### `datechange`
91
114
 
92
115
  Fired when `selection="single"` changes `value`.
@@ -152,6 +175,23 @@ The direct child text input is required. On enhancement its original `name` move
152
175
  - `isDateDisabled`;
153
176
  - `messages`.
154
177
 
178
+ ### Popup positioning
179
+
180
+ - `coordinateSpace` — JS-only, `"viewport"` by default.
181
+
182
+ ```text
183
+ "viewport"
184
+ panel uses position: fixed
185
+ general/default integration
186
+
187
+ "document"
188
+ panel uses position: absolute
189
+ explicit optimization when the application knows the picker
190
+ reference moves with root-page scrolling
191
+ ```
192
+
193
+ `document` follows the `@lekoala/floating` consumer contract: the picker panel is a top-layer popover, so its absolute positioning resolves against the initial containing block; the application remains responsible for knowing that the `<date-picker>` reference itself moves with the root document. Changing `coordinateSpace` while open configures the next opening, never the current one. See the `@lekoala/floating` documentation for the coordinate-space doctrine.
194
+
155
195
  ### Read-only properties
156
196
 
157
197
  ```js
@@ -231,6 +271,8 @@ Optional native time companions (one or both). They are direct children, fixed a
231
271
 
232
272
  Two editable fields share one calendar popup. The mode is fixed at connect time (`range` attribute + both marked direct child inputs); switching single/range at runtime is not supported in V1.
233
273
 
274
+ Each field gets its own calendar trigger, inserted right after it (`start`, then its trigger, `end`, then its trigger), so DOM order matches visual/tab order. Both triggers point at the same popup; activating the other bound's trigger while it is open switches the active bound and keeps it open.
275
+
234
276
  ### Strict API
235
277
 
236
278
  - `range` — atomic get/set of `{ start, end }` (canonical `YYYY-MM-DD`). The setter validates through `normalizeRange` (ISO-only, ordered; inverted throws) and applies both bounds, the visible fields and both hidden fields **before** one `rangechange` event — no intermediate new-start/old-end state is observable.
@@ -253,8 +295,12 @@ The business range may be temporarily incomplete or inverted; `calendar.highligh
253
295
  ### Interaction
254
296
 
255
297
  - When an ordered range is complete, picking before its start updates only the start; picking after its end updates only the end, regardless of the opening field. The popup closes after this update. An uneditable target bound refuses the activation.
256
- - Otherwise, opening through a field targets that bound (`activeEndpoint`). Picking from `start` commits it, moves the endpoint to `end` without closing, and a second pick commits and closes. Opening from `end` refuses a date before `start` (`dateinvalid`, nothing changes).
257
- - The shared trigger opens the last active bound, else `start`; it never auto-switches to a `readonly`/`disabled` bound.
298
+ - Otherwise, opening through a field targets that bound (`activeEndpoint`). Picking from `start` commits it, moves the endpoint to `end` without closing, and a second pick commits and closes.
299
+ - **Calendar interaction never produces an inverted range.** While a range is being created, a second pick before the anchor is sorted rather than refused: `10` then `15` gives `10 -> 15`, `10` then `5` gives `5 -> 10`, and `10` then `10` a one-day range. That transition moves **both** bounds at once and is applied in a single write — one `rangechange`, with synthetic `input`/`change` on each bound that moved, and no intermediate inverted pair ever observable. Typed text keeps the opposite contract and may be temporarily inverted (see [DECISIONS D6](DECISIONS.md)).
300
+ - Changing one bound of an already complete pair keeps that bound's identity: moving `end` before `start` is refused (`dateinvalid`, nothing changes) instead of rewriting the other field.
301
+ - **Preview.** While a range is being created, hovering a day or moving the keyboard focus projects the range the next pick would commit onto `calendar.highlightedRange`, and the pick commits exactly what was projected. The projection is visual only: no `rangechange`, no field write, no validation and no source request. It reads the month already loaded, so a day known to be unavailable promises nothing; the real availability check still runs on commit. Pointer hover never moves `focusedDate`, which stays the keyboard target. Leaving the grid, Escape and closing the popup restore the committed band.
302
+ - **Endpoint drag.** On a complete, ordered range whose two ends sit on different days, each endpoint can be dragged with a mouse or pen (pointer events plus capture, armed only once the pointer travels). A handle never crosses the other one — the candidate is clamped to it, equality allowed — an unavailable cell is not a drop target, and `pointercancel` restores the committed band. The drop runs the same projection and the same `ensureDate` + cancelable `dateactivate` path as a click, emits one `rangechange`, and keeps the popup open: a drag adjusts a range, it does not validate it. A press without travel stays a plain click. Touch keeps native scrolling and stays a plain tap.
303
+ - Each bound owns its trigger (`input[data-range-start]` then its trigger, `input[data-range-end]` then its trigger); the two triggers share **one** popup, calendar, source and state. Pressing the other bound's trigger while the popup is open switches the active bound and keeps it open instead of toggling it shut. A trigger never opens a `readonly`/`disabled` bound.
258
304
  - Typing stays independent per field. Moving `start` past `end` keeps both values and flags the *modified* bound with `rangeOrderStart`; fixing either side revalidates both. Parse/source errors are never cleared by cross-bound revalidation.
259
305
  - A stale availability response resolving after the active endpoint changed (or after a newer selection) never commits; a field focus that switches the endpoint invalidates in-flight activations.
260
306
 
@@ -278,6 +324,29 @@ rangePosition("2026-09-12", range); // "start" | "in" | "end" | "single" | ""
278
324
 
279
325
  `normalizeRange` validates an ordered displayable range and never reorders: an inversion or an `end` without a `start` throws. `rangePosition` classifies one day within a displayable range (see [`highlightedRange`](#highlightedrange)).
280
326
 
327
+ ## `DateRangeController`
328
+
329
+ The pure two-bound state machine behind `date-picker[range]`. It owns the
330
+ selection rules and knows nothing about DOM, popups or async availability.
331
+
332
+ ```js
333
+ const model = new DateRangeController();
334
+ model.focus("start");
335
+ model.activate("2026-09-10"); // { status: "pending", endpoint: "start", … }
336
+ model.previewRange("2026-09-05"); // { start: "2026-09-05", end: "2026-09-10" }
337
+ model.activate("2026-09-05"); // changedEndpoints: ["start", "end"]
338
+ ```
339
+
340
+ - `project(date, editable?)` / `projectEndpoint(date, endpoint, editable?)` describe a transition without applying it;
341
+ - `activate(date, editable?)` / `moveEndpoint(date, endpoint, editable?)` apply one;
342
+ - `previewRange(date, endpoint?, editable?)` returns the displayable band a candidate would produce, or `null` when there is nothing to promise.
343
+
344
+ A transition is `{ status, endpoint, changedEndpoints, range, activeEndpoint, close? }`.
345
+ `range` is the whole resulting pair, because one interaction can move both
346
+ bounds; `changedEndpoints` names the ones that actually move. Every rule exists
347
+ once, as a projection, and the previews read the same projections the commit
348
+ applies.
349
+
281
350
  ## `createDateAdapter(locale)`
282
351
 
283
352
  Locale-aware editable numeric adapter:
@@ -93,7 +93,10 @@ This prevents rich decorations from breaking grid semantics or keyboard behavior
93
93
  3. `min` / `max`;
94
94
  4. `isDateDisabled()`.
95
95
 
96
- `enabled: true` is the explicit exception seam: it can reopen a date disabled by generic/source rules, but never bypasses hard `min`/`max` bounds.
96
+ `enabled: true` is the explicit exception seam: when the resolved state carries
97
+ `enabled === true`, it lifts `isDateDisabled()` only. It never bypasses
98
+ `min`/`max` or the resolved `state.disabled` (source state merged with
99
+ `dateState()` output, so `dateState()` can already replace a source `disabled`).
97
100
 
98
101
  Both click activation and typed picker validation use this resolved state.
99
102
 
@@ -144,6 +147,17 @@ The picker creates a `<date-calendar selection="none">` in a native manual popov
144
147
 
145
148
  Native Popover owns the top layer. `@lekoala/floating` owns coordinates and geometry updates.
146
149
 
150
+ ### Coordinate space
151
+
152
+ `picker.coordinateSpace` (`"viewport"` default, `"document"` explicit opt-in, JS-only, no attribute) is frozen once per opening in `show()`; `panel.style.position` is coupled:
153
+
154
+ ```text
155
+ viewport (default) → fixed
156
+ document → absolute
157
+ ```
158
+
159
+ No layout inference is performed. Coordinate-space choice is consumer policy; `@lekoala/floating` owns only coordinate math and update tracking.
160
+
147
161
  The picker does not implement:
148
162
 
149
163
  - custom portals;
@@ -157,7 +171,7 @@ The popup remains in authored DOM, preserving inherited locale/theme/density con
157
171
 
158
172
  Two product shapes cover the range cases, and both keep range rules outside the classes:
159
173
 
160
- `<date-picker range>` shares **one** calendar surface between two editable fields. Each input is owned by a `DateFieldController` (parsing, hidden ISO, commit/stale protection); the pair is driven by `DateRangeController` in `date-range.js`, which owns the active-endpoint transitions and the start/end state. `DatePickerElement` remains a shell: it coordinates DOM/popup/focus only. `value` is unavailable in range mode and `range` is the atomic pair API; inverted or incomplete business ranges are never forwarded to `highlightedRange`, which stays presentation-only.
174
+ `<date-picker range>` shares **one** calendar surface between two editable fields. Each input is owned by a `DateFieldController` (parsing, hidden ISO, commit/stale protection); the pair is driven by `DateRangeController` in `date-range.js`, which owns the active-endpoint transitions and the start/end state. The shell owns one calendar trigger per bound (each inserted after its own field, both pointing at the same popup) and switches the active endpoint without closing it. `DatePickerElement` remains a shell: it coordinates DOM/popup/focus only. `value` is unavailable in range mode and `range` is the atomic pair API; inverted or incomplete business ranges are never forwarded to `highlightedRange`, which stays presentation-only.
161
175
 
162
176
  `linkDateRange(start, end)` keeps the external relationship for two visually separate pickers:
163
177
 
@@ -44,7 +44,7 @@ Release decision (0.1.0): the human reader/OS pairings below are **deferred past
44
44
 
45
45
  - [x] [auto] Typing a valid localized date updates the field validation state (no stale invalid message).
46
46
  - [x] [split] Typing an impossible date produces an announced invalid message (native invalid state/message covered automatically; announcement still manual).
47
- - [x] [manual] Forced-colors mode keeps selected/today/disabled distinguishable — including a selected day when `Highlight` resolves near-white (no white-on-white from `--dp-accent`; spot-checked with the forced-colors screenshot bundle).
47
+ - [ ] [manual] Forced-colors mode keeps selected/today/disabled distinguishable with the simplified border-based treatment (re-check after the neutral-palette + rhythm pass; spot-check with the forced-colors screenshot bundle).
48
48
 
49
49
  ## Automated pass
50
50
 
package/docs/DECISIONS.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # Decisions
2
2
 
3
- Frozen contracts for the `0.1.0` surface. A change to a decision below is a breaking change and needs a proposal, not an opportunistic edit.
3
+ Frozen contracts for the public surface. A change to a decision below is a breaking change and needs a proposal, not an opportunistic edit.
4
4
 
5
5
  ## D1 — Calendar events are flat, verb-named
6
6
 
7
7
  Never merged with the `value` concept (`dateactivate` is activation, not `value`).
8
8
 
9
- - `date-calendar`: `dateactivate`, `datechange`, `dateinvalid`, `displaychange`, `dateloadstart`, `dateloadend`, `dateloaderror`.
10
- - `date-picker`: `valuechange`, `open`, `close`.
9
+ - `date-calendar`: `dateactivate`, `datefocus`, `datechange`, `dateinvalid`, `displaychange`, `dateloadstart`, `dateloadend`, `dateloaderror`.
10
+ - `date-picker`: `valuechange`, `open`, `close`, plus `rangechange` in range mode.
11
+
12
+ The observation verbs stay distinct: `datefocus` observes navigation, `dateactivate` requests an activation (and is cancelable), `datechange` observes a selection. `datefocus` is named after the user event rather than after the `focusedDate` setter, so a programmatic assignment that moves no DOM focus never announces one.
11
13
 
12
14
  No `dp:` namespace prefix on element events, even though `@lekoala/combobox` namespaces its events (`combobox:*`).
13
15
 
@@ -35,6 +37,8 @@ Context: DOM-backed styling hooks become API the moment they are documented —
35
37
 
36
38
  - The picker opens its popover when the input receives focus (`open-on-focus`, default `true`; set `open-on-focus="false"` to opt out).
37
39
  - Opening on focus **never steals keyboard focus**: the user can start typing immediately. The grid takes focus only on ArrowDown, on the calendar trigger, or after the user moves the pointer into the popup.
40
+ - Closing returns focus to the control that opened the popover (field or trigger) — the usual popover convention. Range mode is the exception: focus follows the active bound, because a first pick moves the workflow to the other bound.
41
+ - The calendar trigger is overlaid inside the field's own box, like the native `input[type=time]` indicator. The field reserves its place (`padding-inline-end`), keeps its border/background, and owns the focus ring for the pair; the trigger is a real button with an inner ring of its own. A range bound owns its own trigger; the two share one popup, calendar, source and state.
38
42
  - `show()` / `hide()` are the control surface. `open` is a **read-only getter** reflecting the popover. There is no `open` attribute: the popover element is the single machine of truth and a second state machine would only drift.
39
43
  - `readonly` mirrors native semantics: the field is non-editable, the value is still submitted, and the picker cannot change it — the calendar trigger is disabled. `disabled` additionally unsubmits the field. `readonly` is never used to mean "no typing, but calendar selection allowed".
40
44
  - Clearing the editable input clears the canonical value (the component renders no internal clear button). Form resets follow the native `defaultValue` contract. This matches the Open UI datepicker `clear` concept: clearing is a text-editing action, not a component-owned affordance.
@@ -44,4 +48,15 @@ Context: DOM-backed styling hooks become API the moment they are documented —
44
48
  - Time fields are authored native `input[type=time]` marked `[data-time-start]` / `[data-time-end]`; `name`, `value`, `required`, `min`, `max`, `step` and submission stay native. There is no hidden time input and no time field controller.
45
49
  - `picker.value` / `picker.range` remain date-only (`YYYY-MM-DD`); the picker never constructs a datetime, a timestamp or a timezone-bearing value.
46
50
  - The picker only coordinates from/to order (`start <= end`, equality allowed); duration rules stay application-owned.
47
- - Custom time UI and slot picking stay outside the package.
51
+ - Custom time UI and slot picking stay outside the package.
52
+
53
+ ## D6 — Calendar interaction never produces an inverted range
54
+
55
+ Pointing and typing are different contracts, deliberately.
56
+
57
+ - **Interaction** (click, keyboard, endpoint drag) always yields an ordered range. While a range is being created, the second selection is sorted against the anchor: `10` then `5` commits `5 -> 10`, and `10` then `10` a one-day range. No `dateinvalid` is fired merely because the second point is earlier.
58
+ - **Text entry** may be temporarily inverted. Typing `start` past `end` keeps both values and reports `rangeOrderStart` / `rangeOrderEnd` on the modified bound, because a half-typed pair is a normal editing state.
59
+ - An explicit change to **one bound of an already complete pair** keeps that bound's identity: moving `end` before `start` is refused rather than silently rewriting the other field.
60
+ - Because one interaction can move both bounds at once, a transition describes the whole resulting pair (`{ status, endpoint, changedEndpoints, range, activeEndpoint }`) and is applied in a single write: one `rangechange`, with synthetic `input` / `change` only on the bounds that actually moved.
61
+ - Hover, keyboard focus and endpoint drag read the **same projection** (`previewRange()`) that the commit applies, so what the band promises is what the next selection commits. A preview writes nothing: no `rangechange`, no field write, no validation, no source request.
62
+ - Click, keyboard and drop all end in that one range machine and in the same `ensureDate` plus cancelable `dateactivate` path, so an application veto cannot be bypassed by choosing another input device.
@@ -1,6 +1,6 @@
1
1
  # Flatpickr / Formidable migration map
2
2
 
3
- This prototype was scoped against the existing `FlatpickrInput` wrapper rather than against Flatpickr's full feature list.
3
+ This project was scoped against the existing `FlatpickrInput` wrapper rather than against Flatpickr's full feature list.
4
4
 
5
5
  ## Directly covered
6
6
 
@@ -78,7 +78,7 @@ calendar.isDateDisabled = (date) => isWeekend(date);
78
78
  // weekend rule. Hard min/max bounds still win.
79
79
  ```
80
80
 
81
- The prototype promotes this legacy behavior into a documented generic field: `enabled: true` reopens a date disabled by `isDateDisabled()` or ordinary source state, but never bypasses hard `min`/`max` bounds.
81
+ The prototype promotes this legacy behavior into a documented generic field: `enabled: true` reopens a date disabled by `isDateDisabled()`, but never bypasses hard `min`/`max` bounds or a resolved `state.disabled`.
82
82
 
83
83
  ## Not migrated into this component
84
84
 
package/docs/TESTING.md CHANGED
@@ -68,6 +68,6 @@ Keep explicit tests for these regressions:
68
68
 
69
69
  ## CI
70
70
 
71
- The included workflow uses Bun 1.4.2 and runs static checks/unit tests before Playwright.
71
+ The included workflow uses Bun 1.4.3 and runs static checks/unit tests before Playwright.
72
72
 
73
73
  `bun.lock` is committed. Keep installs frozen in CI (`bun install --frozen-lockfile`) so dependency resolution stays deterministic. Regenerate the lock with `bun install` when the manifest changes and commit it in the same change.
package/docs/USE_CASES.md CHANGED
@@ -111,6 +111,9 @@ Acceptance:
111
111
  - an inverted range (start after end) keeps both entered values and surfaces an order error on the bound that was just modified; changing either side revalidates both;
112
112
  - form reset restores both fields in one resync;
113
113
  - a stale async availability response arriving after the active endpoint changed never commits a value or closes the popup;
114
+ - calendar interaction never produces an inverted range: while a range is being created, a second selection before the first is sorted (`10` then `5` commits `5 -> 10`) in one atomic update; an explicit change to one bound of an already complete pair keeps that bound's identity instead;
115
+ - pointer hover and keyboard navigation project the range the next selection would commit onto the band, without changing any value, and a selection commits exactly what was projected;
116
+ - the endpoints of a complete range can be dragged to adjust it, through the same projection, the same availability check and the same cancelable activation as a click;
114
117
  - only the bounds are validated; per-night availability stays application-owned.
115
118
 
116
119
  ## U10 — Date with native time companions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lekoala/date-picker",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Native-first date picker and inline calendar primitives",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "homepage": "https://github.com/lekoala/date-picker",
22
22
  "bugs": "https://github.com/lekoala/date-picker/issues",
23
- "packageManager": "bun@1.4.2",
23
+ "packageManager": "bun@1.4.3",
24
24
  "keywords": [
25
25
  "date-picker",
26
26
  "calendar",
@@ -162,13 +162,13 @@
162
162
  "prepublishOnly": "bun run verify"
163
163
  },
164
164
  "devDependencies": {
165
- "@biomejs/biome": "2.5.11",
166
- "@lekoala/calendar": "0.1.0",
167
- "@playwright/test": "1.62.1",
168
- "temporal-polyfill": "^1.0.4",
165
+ "@biomejs/biome": "2.5.15",
166
+ "@lekoala/calendar": "0.2.0",
167
+ "@playwright/test": "1.64.0",
168
+ "temporal-polyfill": "^1.0.5",
169
169
  "typescript": "7.0.2"
170
170
  },
171
171
  "dependencies": {
172
- "@lekoala/floating": "^0.1.1"
172
+ "@lekoala/floating": "^0.2.0"
173
173
  }
174
174
  }