@lekoala/date-picker 0.3.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Small, native-first date picker and inline calendar primitives.
4
4
 
5
- > v0.2: the interaction contract, event names, public styling tokens and the component API are frozen — see [docs/DECISIONS.md](docs/DECISIONS.md). Internal DOM and class names are `.dp-*` implementation details and stay private.
5
+ > The interaction contract, event names, public styling tokens and the component API are frozen — see [docs/DECISIONS.md](docs/DECISIONS.md). Internal DOM and class names are `.dp-*` implementation details and stay private.
6
6
 
7
7
  This project fills the gap between a normal editable date field and a full scheduling calendar. A plain date input remains the right answer for simple CMS/editing forms. This component becomes useful when a date has context: availability, disabled days, annotations, remote state, start/end relationships, or a mini calendar that navigates another view.
8
8
 
@@ -164,6 +164,8 @@ The picker popup uses native Popover for the top layer and `@lekoala/floating` f
164
164
 
165
165
  The calendar trigger is a real button painted inside the field's own box (like the native `input[type=time]` indicator): the field reserves its place with `padding-inline-end`, keeps its own border and background, and its focus ring wraps the trigger too. The trigger draws no chrome of its own and shows a small inner ring when focused.
166
166
 
167
+ Fields, header controls and days share an inner outline that survives forced colors. Customize its color with `--dp-focus` (default `var(--dp-accent)`) and thickness with `--dp-focus-width` (default `2px`) on `date-picker` and `date-calendar`. The trigger keeps a `-5px` inset around its icon; selected days and range endpoints use `--dp-accent-fg` for contrast. Fields and header controls now show this solid outline instead of the previous soft halo.
168
+
167
169
  When the popup opens on the field's focus, grid navigation is reachable via `ArrowDown` (or the trigger button) so the focus-restore/Escape flow stays predictable. Closing returns focus to the control that opened the popover: the field when it opened by focus, the trigger when it was activated. In range mode focus follows the active bound instead, because a first pick moves the workflow to the other bound.
168
170
 
169
171
  See [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md).
@@ -179,10 +181,10 @@ If a previous `bun run dev` is still holding port `4859`, clear it with `bun run
179
181
 
180
182
  Tooling is aligned with the current LeKoala component repos:
181
183
 
182
- - Bun 1.4.2
184
+ - Bun 1.4.3
183
185
  - TypeScript 7.0.2 (`checkJs` + declaration emit)
184
- - Biome 2.5.13
185
- - Playwright 1.63.0
186
+ - Biome 2.5.15
187
+ - Playwright 1.64.0
186
188
  - `@lekoala/floating ^0.2.0`
187
189
 
188
190
  Useful commands:
@@ -214,7 +216,7 @@ The default ESM exports point at `src/`, matching the native-first development s
214
216
 
215
217
  ## Scope
216
218
 
217
- Included in v0.2:
219
+ Included:
218
220
 
219
221
  - inline single-date selection;
220
222
  - mini-calendar/navigation-only use;
@@ -236,7 +238,7 @@ Included in v0.2:
236
238
  - ISO week numbers;
237
239
  - keyboard and focus contracts.
238
240
 
239
- Deliberately out of scope for v0.2:
241
+ Deliberately out of scope:
240
242
 
241
243
  - custom time-picker UI and combined datetime values (only native `input[type=time]` companions are supported);
242
244
  - timezones;
@@ -139,6 +139,11 @@
139
139
  "kind": "method",
140
140
  "name": "focusGrid"
141
141
  },
142
+ {
143
+ "kind": "method",
144
+ "name": "activateDate",
145
+ "description": "Run one explicit activation: availability check, then the cancelable `dateactivate` seam, then selection. Click, keyboard and any other input device share this single path, so an application that cancels `dateactivate` cannot be bypassed by a new interaction."
146
+ },
142
147
  {
143
148
  "kind": "method",
144
149
  "name": "render"
@@ -151,6 +156,9 @@
151
156
  {
152
157
  "name": "datechange"
153
158
  },
159
+ {
160
+ "name": "datefocus"
161
+ },
154
162
  {
155
163
  "name": "dateinvalid"
156
164
  },
@@ -189,6 +197,12 @@
189
197
  {
190
198
  "name": "--dp-accent-fg"
191
199
  },
200
+ {
201
+ "name": "--dp-focus"
202
+ },
203
+ {
204
+ "name": "--dp-focus-width"
205
+ },
192
206
  {
193
207
  "name": "--dp-radius"
194
208
  },
@@ -375,6 +389,12 @@
375
389
  {
376
390
  "name": "--dp-accent-fg"
377
391
  },
392
+ {
393
+ "name": "--dp-focus"
394
+ },
395
+ {
396
+ "name": "--dp-focus-width"
397
+ },
378
398
  {
379
399
  "name": "--dp-radius"
380
400
  },
@@ -6,6 +6,8 @@
6
6
  --dp-hover: color-mix(in srgb, var(--dp-fg) 5%, var(--dp-bg));
7
7
  --dp-accent: #2563eb;
8
8
  --dp-accent-fg: #fff;
9
+ --dp-focus: var(--dp-accent);
10
+ --dp-focus-width: 2px;
9
11
  --dp-radius: 0.5rem;
10
12
  --dp-day-size: 2.75rem;
11
13
  --dp-gap: 0.375rem;
@@ -137,14 +139,6 @@ html[dir="rtl"] .dp-month-select {
137
139
  pointer-events: none;
138
140
  }
139
141
 
140
- /* Inherited RTL mirrors the header layout via the grid; mirror the nav glyphs
141
- so previous/next keep their visual sense. Actions stay chronological: no
142
- button swap, no keyboard remapping. */
143
- [dir="rtl"] .dp-nav svg,
144
- html[dir="rtl"] .dp-nav svg {
145
- transform: scaleX(-1);
146
- }
147
-
148
142
  .dp-calendar-header :where(button, select, input):disabled,
149
143
  .dp-picker-button:disabled {
150
144
  cursor: not-allowed;
@@ -155,12 +149,13 @@ html[dir="rtl"] .dp-nav svg {
155
149
  background: var(--dp-hover);
156
150
  }
157
151
 
158
- /* Shared control focus: accent border + light halo. Day cells keep their
159
- dedicated outline ring below; the picker trigger has its own inner ring. */
160
- .dp-calendar-header :where(button, select, input):focus-visible {
161
- outline: none;
162
- border-color: var(--dp-accent);
163
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dp-accent) 22%, transparent);
152
+ /* Inner outlines survive forced colors and keep focus inside each control.
153
+ The picker trigger uses a tighter inset around its icon below. */
154
+ .dp-calendar-header :where(button, select, input):focus-visible,
155
+ date-picker > input:not([type="hidden"]):focus-visible,
156
+ .dp-day:focus-visible {
157
+ outline: var(--dp-focus-width) solid var(--dp-focus);
158
+ outline-offset: calc(var(--dp-focus-width) * -1);
164
159
  }
165
160
 
166
161
  .dp-day:hover:not([data-disabled="true"]):not([data-selected="true"]):not([data-range-start="true"]):not(
@@ -205,15 +200,6 @@ html[dir="rtl"] .dp-nav svg {
205
200
  font-size: 0.875rem;
206
201
  }
207
202
 
208
- /* Inner ring: keyboard focus stays inside the cell instead of reading as a
209
- second selected state or bleeding onto a neighbouring range band. It is an
210
- `outline` (not an inset box-shadow) because forced colors computes
211
- box-shadow to `none`, which would erase the keyboard indicator entirely. */
212
- .dp-day:focus-visible {
213
- outline: 2px solid var(--dp-accent);
214
- outline-offset: -2px;
215
- }
216
-
217
203
  /* On an accent-filled cell the ring inverts so it stays readable. */
218
204
  .dp-day:is([data-selected="true"], [data-range-start="true"], [data-range-end="true"]):focus-visible {
219
205
  outline-color: var(--dp-accent-fg);
@@ -275,6 +261,28 @@ html[dir="rtl"] .dp-nav svg {
275
261
  border-end-end-radius: var(--dp-day-radius);
276
262
  }
277
263
 
264
+ /* Projected band: the range the next commit would produce. It reads as one
265
+ step short of the committed band, so a hovered, keyboard or dragged preview
266
+ is never mistaken for the selection itself. */
267
+ [data-range-preview] .dp-day:is([data-range-start="true"], [data-range-end="true"]) {
268
+ background: color-mix(in srgb, var(--dp-accent) 58%, var(--dp-bg));
269
+ }
270
+
271
+ [data-range-preview] .dp-day[data-in-range="true"] {
272
+ background: color-mix(in srgb, var(--dp-accent) 11%, var(--dp-bg));
273
+ }
274
+
275
+ /* Endpoint handles are draggable only while the committed range is complete
276
+ and its two ends sit on different days. */
277
+ [data-range-drag] .dp-day:is([data-range-start="true"], [data-range-end="true"]) {
278
+ cursor: grab;
279
+ }
280
+
281
+ [data-range-drag="active"],
282
+ [data-range-drag="active"] .dp-day {
283
+ cursor: grabbing;
284
+ }
285
+
278
286
  .dp-day-number {
279
287
  display: block;
280
288
  font-variant-numeric: tabular-nums;
@@ -320,14 +328,6 @@ date-picker {
320
328
  max-inline-size: 100%;
321
329
  }
322
330
 
323
- /* Visible focus is normalized to --dp-accent so the component holds together
324
- without a consumer stylesheet; the field's own look stays consumer-owned. */
325
- date-picker > input:not([type="hidden"]):focus-visible {
326
- border-color: var(--dp-accent);
327
- outline: none;
328
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--dp-accent) 20%, transparent);
329
- }
330
-
331
331
  /* Every authored control is a grid item. */
332
332
  date-picker > input:not([type="hidden"]) {
333
333
  min-inline-size: 0;
@@ -368,7 +368,7 @@ date-picker > input:not([type="hidden"]):not([type="time"]) {
368
368
  field already shows its own ring through :focus-visible). Same radius as
369
369
  everywhere else: no bespoke shape. */
370
370
  .dp-picker-button:focus-visible {
371
- outline: 2px solid var(--dp-accent);
371
+ outline: var(--dp-focus-width) solid var(--dp-focus);
372
372
  outline-offset: -5px;
373
373
  border-radius: var(--dp-radius);
374
374
  }