@lekoala/date-picker 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Small, native-first date picker and inline calendar primitives.
4
4
 
5
- > v0.1: 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
+ > 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.
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
 
@@ -77,6 +77,24 @@ const cleanup = linkDateRange(startPicker, endPicker);
77
77
 
78
78
  The relationship applies `end >= start` and `start <= end` as effective bounds. Neither picker knows about its sibling.
79
79
 
80
+ ## Date + time
81
+
82
+ A date pairs with one or two native time companions. The picker owns the civil date; the time inputs own their civil time values:
83
+
84
+ ```html
85
+ <date-picker>
86
+ <input name="appointment[date]">
87
+ <input type="time" data-time-start name="appointment[time]">
88
+ </date-picker>
89
+ ```
90
+
91
+ ```text
92
+ appointment[date] = 2026-09-10
93
+ appointment[time] = 09:30
94
+ ```
95
+
96
+ The application/server combines these civil values and applies the relevant timezone when a datetime/instant is needed. The picker never constructs a combined datetime value.
97
+
80
98
  ## Availability and per-day metadata
81
99
 
82
100
  The calendar accepts a source function or `{ load() }` object with the same abortable shape used elsewhere in LeKoala components:
@@ -144,7 +162,9 @@ The calendar follows the WAI-ARIA date-picker/grid interaction model:
144
162
 
145
163
  The picker popup uses native Popover for the top layer and `@lekoala/floating` for geometry. It has `role="dialog"`, but it is intentionally **not** marked `aria-modal` because the current implementation does not inert the rest of the document.
146
164
 
147
- 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.
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
+
167
+ 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.
148
168
 
149
169
  See [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md).
150
170
 
@@ -161,9 +181,9 @@ Tooling is aligned with the current LeKoala component repos:
161
181
 
162
182
  - Bun 1.4.2
163
183
  - TypeScript 7.0.2 (`checkJs` + declaration emit)
164
- - Biome 2.5.11
165
- - Playwright 1.62.1
166
- - `@lekoala/floating ^0.1.1`
184
+ - Biome 2.5.13
185
+ - Playwright 1.63.0
186
+ - `@lekoala/floating ^0.2.0`
167
187
 
168
188
  Useful commands:
169
189
 
@@ -194,7 +214,7 @@ The default ESM exports point at `src/`, matching the native-first development s
194
214
 
195
215
  ## Scope
196
216
 
197
- Included in v0.1:
217
+ Included in v0.2:
198
218
 
199
219
  - inline single-date selection;
200
220
  - mini-calendar/navigation-only use;
@@ -205,15 +225,20 @@ Included in v0.1:
205
225
  - presentation-only range band (`highlightedRange`);
206
226
  - shared-calendar two-field range picker (`<date-picker range>`);
207
227
  - linked start/end pickers (`linkDateRange`);
228
+ - native time companions (one or two `input[type=time]` beside the date);
229
+ - `Intl` month/year control order + `month-format="short"`;
230
+ - RTL through the inherited `dir` (no `isRTL` option);
231
+ - readable keyboard focus on disabled/selected days;
232
+ - compact-consumer CSS tokens (`--dp-header-control-size`, `--dp-day-radius`, …);
208
233
  - async per-day state with cancellation;
209
234
  - custom day decorations that cannot replace the accessible cell;
210
235
  - fixed six-row presentation as an opt-in;
211
236
  - ISO week numbers;
212
237
  - keyboard and focus contracts.
213
238
 
214
- Deliberately out of scope for v0.1:
239
+ Deliberately out of scope for v0.2:
215
240
 
216
- - time or datetime picking;
241
+ - custom time-picker UI and combined datetime values (only native `input[type=time]` companions are supported);
217
242
  - timezones;
218
243
  - in-place single-calendar range *selection interaction* (use the two-field picker);
219
244
  - multi-date selection;
@@ -37,6 +37,9 @@
37
37
  },
38
38
  {
39
39
  "name": "selection"
40
+ },
41
+ {
42
+ "name": "month-format"
40
43
  }
41
44
  ],
42
45
  "members": [
@@ -72,6 +75,10 @@
72
75
  "kind": "getter",
73
76
  "name": "selection"
74
77
  },
78
+ {
79
+ "kind": "getter",
80
+ "name": "monthFormat"
81
+ },
75
82
  {
76
83
  "kind": "getter",
77
84
  "name": "highlightedRange"
@@ -190,6 +197,24 @@
190
197
  },
191
198
  {
192
199
  "name": "--dp-gap"
200
+ },
201
+ {
202
+ "name": "--dp-header-control-size"
203
+ },
204
+ {
205
+ "name": "--dp-header-block-gap"
206
+ },
207
+ {
208
+ "name": "--dp-row-gap"
209
+ },
210
+ {
211
+ "name": "--dp-picker-button-size"
212
+ },
213
+ {
214
+ "name": "--dp-year-inline-size"
215
+ },
216
+ {
217
+ "name": "--dp-day-radius"
193
218
  }
194
219
  ]
195
220
  }
@@ -228,6 +253,9 @@
228
253
  },
229
254
  {
230
255
  "name": "open-on-focus"
256
+ },
257
+ {
258
+ "name": "month-format"
231
259
  }
232
260
  ],
233
261
  "members": [
@@ -251,6 +279,10 @@
251
279
  "kind": "getter",
252
280
  "name": "locale"
253
281
  },
282
+ {
283
+ "kind": "getter",
284
+ "name": "monthFormat"
285
+ },
254
286
  {
255
287
  "kind": "getter",
256
288
  "name": "min"
@@ -287,6 +319,10 @@
287
319
  "kind": "getter",
288
320
  "name": "isDateDisabled"
289
321
  },
322
+ {
323
+ "kind": "getter",
324
+ "name": "coordinateSpace"
325
+ },
290
326
  {
291
327
  "kind": "method",
292
328
  "name": "validate"
@@ -347,6 +383,24 @@
347
383
  },
348
384
  {
349
385
  "name": "--dp-gap"
386
+ },
387
+ {
388
+ "name": "--dp-header-control-size"
389
+ },
390
+ {
391
+ "name": "--dp-header-block-gap"
392
+ },
393
+ {
394
+ "name": "--dp-row-gap"
395
+ },
396
+ {
397
+ "name": "--dp-picker-button-size"
398
+ },
399
+ {
400
+ "name": "--dp-year-inline-size"
401
+ },
402
+ {
403
+ "name": "--dp-day-radius"
350
404
  }
351
405
  ]
352
406
  }
@@ -1,14 +1,20 @@
1
1
  :where(date-calendar, date-picker) {
2
- --dp-bg: Canvas;
3
- --dp-fg: CanvasText;
4
- --dp-muted: color-mix(in srgb, CanvasText 58%, Canvas);
5
- --dp-border: color-mix(in srgb, CanvasText 22%, Canvas);
6
- --dp-hover: color-mix(in srgb, CanvasText 7%, Canvas);
7
- --dp-accent: Highlight;
8
- --dp-accent-fg: HighlightText;
2
+ --dp-bg: #ffffff;
3
+ --dp-fg: #1f2328;
4
+ --dp-muted: color-mix(in srgb, var(--dp-fg) 58%, var(--dp-bg));
5
+ --dp-border: color-mix(in srgb, var(--dp-fg) 16%, var(--dp-bg));
6
+ --dp-hover: color-mix(in srgb, var(--dp-fg) 5%, var(--dp-bg));
7
+ --dp-accent: #2563eb;
8
+ --dp-accent-fg: #fff;
9
9
  --dp-radius: 0.5rem;
10
10
  --dp-day-size: 2.75rem;
11
- --dp-gap: 0.25rem;
11
+ --dp-gap: 0.375rem;
12
+ --dp-header-control-size: 2.25rem;
13
+ --dp-header-block-gap: 0.75rem;
14
+ --dp-row-gap: 0.375rem;
15
+ --dp-picker-button-size: 2.5rem;
16
+ --dp-year-inline-size: 5.5rem;
17
+ --dp-day-radius: calc(var(--dp-radius) * 0.75);
12
18
  color: var(--dp-fg);
13
19
  font: inherit;
14
20
  }
@@ -39,42 +45,85 @@ date-calendar {
39
45
  .dp-calendar-header {
40
46
  display: grid;
41
47
  grid-template-columns: minmax(0, 1fr) auto auto auto;
42
- align-items: center;
48
+ /* Stretch (not center): once a UA natural height exceeds
49
+ --dp-header-control-size, the row grows to the tallest control and the
50
+ others stretch to match, so month/year/nav stay equal. */
51
+ align-items: stretch;
52
+ /* Uniform horizontal rhythm: the four header controls are self-evident from
53
+ their kind and position, so no group gets an extra semantic gap. The air
54
+ belongs to the vertical zones (header -> weekdays -> dates). */
43
55
  gap: var(--dp-gap);
44
- margin-block-end: 0.4rem;
56
+ margin-block-end: var(--dp-header-block-gap);
45
57
  }
46
58
 
47
- .dp-calendar-header .dp-prev {
48
- margin-inline-start: calc(var(--dp-gap) * 2.5);
59
+ /* Locale year-first order (Intl-derived, direction-independent): the flexible
60
+ track stays under the month select so the fixed-width year input never
61
+ stretches. Visual mirroring itself is left to the inherited `dir`. */
62
+ date-calendar[data-year-first] .dp-calendar-header {
63
+ grid-template-columns: auto minmax(0, 1fr) auto auto;
49
64
  }
50
65
 
51
66
  .dp-calendar-header :where(button, select, input) {
52
- min-block-size: 2.25rem;
67
+ min-block-size: var(--dp-header-control-size);
68
+ box-sizing: border-box;
69
+ /* Shared vertical metrics: UA default padding/line-height differ per control
70
+ (up to ~13px apart), which only shows once --dp-header-control-size drops
71
+ below content size. */
72
+ padding-block: 0.15rem;
53
73
  border: 1px solid var(--dp-border);
54
74
  border-radius: calc(var(--dp-radius) * 0.75);
55
75
  background: var(--dp-bg);
56
76
  color: var(--dp-fg);
57
77
  font: inherit;
78
+ line-height: 1.2;
58
79
  }
59
80
 
60
- .dp-calendar-header select,
61
81
  .dp-calendar-header input {
62
82
  min-inline-size: 0;
63
83
  padding-inline: 0.45rem;
64
84
  }
65
85
 
86
+ /* Author-drawn caret so the month select matches the nav controls without a
87
+ hardcoded SVG color (currentColor keeps theming). Equal outer heights come
88
+ from the header grid's stretch alignment: the row sizes to the tallest
89
+ control and the others stretch to match. */
66
90
  .dp-month-select {
67
91
  inline-size: 100%;
92
+ min-inline-size: 0;
93
+ /* Long month names in a narrow track (compact consumers, long locales)
94
+ would paint under the caret: truncate with an ellipsis instead. */
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
97
+ appearance: none;
98
+ padding-inline: 0.45rem 2rem;
99
+ background-image:
100
+ linear-gradient(45deg, transparent 50%, currentColor 50%),
101
+ linear-gradient(135deg, currentColor 50%, transparent 50%);
102
+ background-position:
103
+ calc(100% - 0.9rem) 50%,
104
+ calc(100% - 0.65rem) 50%;
105
+ background-size: 0.3rem 0.3rem;
106
+ background-repeat: no-repeat;
107
+ }
108
+
109
+ /* Inherited RTL mirrors the header layout via the grid. The logical
110
+ `padding-inline` already flips, so only the physically-positioned caret has
111
+ to follow it to the other side. */
112
+ [dir="rtl"] .dp-month-select,
113
+ html[dir="rtl"] .dp-month-select {
114
+ background-position:
115
+ 0.9rem 50%,
116
+ 0.65rem 50%;
68
117
  }
69
118
 
70
119
  .dp-year-input {
71
- inline-size: 5.5rem;
120
+ inline-size: var(--dp-year-inline-size);
72
121
  }
73
122
 
74
123
  .dp-nav {
75
124
  display: grid;
76
125
  place-items: center;
77
- inline-size: 2.25rem;
126
+ inline-size: var(--dp-header-control-size);
78
127
  padding: 0;
79
128
  font-size: 1.35rem;
80
129
  line-height: 1;
@@ -88,6 +137,14 @@ date-calendar {
88
137
  pointer-events: none;
89
138
  }
90
139
 
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
+
91
148
  .dp-calendar-header :where(button, select, input):disabled,
92
149
  .dp-picker-button:disabled {
93
150
  cursor: not-allowed;
@@ -98,6 +155,14 @@ date-calendar {
98
155
  background: var(--dp-hover);
99
156
  }
100
157
 
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);
164
+ }
165
+
101
166
  .dp-day:hover:not([data-disabled="true"]):not([data-selected="true"]):not([data-range-start="true"]):not(
102
167
  [data-range-end="true"]
103
168
  ) {
@@ -110,8 +175,8 @@ date-calendar {
110
175
 
111
176
  .dp-grid {
112
177
  border-collapse: separate;
113
- /* --dp-gap scopes to the header grid; day-cell spacing stays fixed. */
114
- border-spacing: 0 0.25rem;
178
+ /* --dp-gap scopes to the header grid; vertical week rhythm is --dp-row-gap. */
179
+ border-spacing: 0 var(--dp-row-gap);
115
180
  table-layout: fixed;
116
181
  }
117
182
 
@@ -124,26 +189,41 @@ date-calendar {
124
189
  }
125
190
 
126
191
  .dp-grid thead th {
127
- block-size: 1.6rem;
192
+ block-size: 1.5rem;
128
193
  color: var(--dp-muted);
129
194
  font-size: 0.72rem;
130
- font-weight: 600;
195
+ font-weight: 500;
131
196
  }
132
197
 
133
198
  .dp-day {
134
199
  position: relative;
135
200
  border: 1px solid transparent;
136
- border-radius: calc(var(--dp-radius) * 0.75);
201
+ border-radius: var(--dp-day-radius);
137
202
  cursor: pointer;
138
203
  outline: none;
139
204
  vertical-align: middle;
140
205
  font-size: 0.875rem;
141
206
  }
142
207
 
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. */
143
212
  .dp-day:focus-visible {
144
213
  outline: 2px solid var(--dp-accent);
145
- outline-offset: 1px;
146
- z-index: 1;
214
+ outline-offset: -2px;
215
+ }
216
+
217
+ /* On an accent-filled cell the ring inverts so it stays readable. */
218
+ .dp-day:is([data-selected="true"], [data-range-start="true"], [data-range-end="true"]):focus-visible {
219
+ outline-color: var(--dp-accent-fg);
220
+ }
221
+
222
+ /* Base tint for days outside the displayed month. It stays before the
223
+ selection states so selected/range endpoints keep their accent-contrast
224
+ text on the accent fill (equal specificity, source order decides). */
225
+ .dp-day[data-outside-month="true"] {
226
+ color: var(--dp-muted);
147
227
  }
148
228
 
149
229
  .dp-day[data-selected="true"] {
@@ -151,14 +231,13 @@ date-calendar {
151
231
  color: var(--dp-accent-fg);
152
232
  }
153
233
 
154
- .dp-day[data-today="true"]:not([data-selected="true"]) {
234
+ /* Today never competes with a stronger state: a selected day or a range
235
+ endpoint keeps its own treatment, in normal rendering and in
236
+ forced-colors alike. */
237
+ .dp-day[data-today="true"]:not([data-selected="true"], [data-range-start="true"], [data-range-end="true"]) {
155
238
  border-color: var(--dp-accent);
156
239
  }
157
240
 
158
- .dp-day[data-outside-month="true"] {
159
- color: var(--dp-muted);
160
- }
161
-
162
241
  .dp-day[data-disabled="true"] {
163
242
  cursor: not-allowed;
164
243
  opacity: 0.42;
@@ -187,13 +266,13 @@ date-calendar {
187
266
  }
188
267
 
189
268
  .dp-day[data-in-range="true"]:first-of-type {
190
- border-start-start-radius: var(--dp-radius);
191
- border-end-start-radius: var(--dp-radius);
269
+ border-start-start-radius: var(--dp-day-radius);
270
+ border-end-start-radius: var(--dp-day-radius);
192
271
  }
193
272
 
194
273
  .dp-day[data-in-range="true"]:last-of-type {
195
- border-start-end-radius: var(--dp-radius);
196
- border-end-end-radius: var(--dp-radius);
274
+ border-start-end-radius: var(--dp-day-radius);
275
+ border-end-end-radius: var(--dp-day-radius);
197
276
  }
198
277
 
199
278
  .dp-day-number {
@@ -201,6 +280,17 @@ date-calendar {
201
280
  font-variant-numeric: tabular-nums;
202
281
  }
203
282
 
283
+ /* A focused disabled day stays discoverable by keyboard: restore full opacity
284
+ so the focus outline renders at full strength, and mark the number with the
285
+ muted tone (same language as outside-month days) instead of the wash-out. */
286
+ .dp-day[data-disabled="true"]:focus-visible {
287
+ opacity: 1;
288
+ }
289
+
290
+ .dp-day[data-disabled="true"]:focus-visible .dp-day-number {
291
+ color: var(--dp-muted);
292
+ }
293
+
204
294
  .dp-day-extra {
205
295
  position: absolute;
206
296
  inset-inline: 2px;
@@ -225,18 +315,95 @@ date-calendar {
225
315
  date-picker {
226
316
  position: relative;
227
317
  display: inline-grid;
228
- grid-template-columns: minmax(0, 1fr) auto;
318
+ grid-template-columns: minmax(0, 1fr);
229
319
  align-items: stretch;
230
320
  max-inline-size: 100%;
231
321
  }
232
322
 
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
+ /* Every authored control is a grid item. */
233
332
  date-picker > input:not([type="hidden"]) {
234
- grid-column: 1;
235
333
  min-inline-size: 0;
236
334
  }
237
335
 
238
- date-picker[range] > input:not([type="hidden"]) {
336
+ /* Date fields reserve room for the trigger, which is painted inside the
337
+ field's own box: the field is the real outer box, so its focus ring wraps
338
+ the affordance too. `grid-row` must stay explicit, otherwise the trigger
339
+ (placed with a definite row) claims the row and pushes the field down. */
340
+ date-picker > input:not([type="hidden"]):not([type="time"]) {
239
341
  grid-column: 1;
342
+ grid-row: 1;
343
+ box-sizing: border-box;
344
+ inline-size: 100%;
345
+ padding-inline-end: calc(var(--dp-picker-button-size) + 0.5rem);
346
+ }
347
+
348
+ /* The trigger is a real button overlaid on the field's inline-end, like the
349
+ native `input[type=time]` indicator: no chrome of its own, transparent, so
350
+ the authored field background and border stay visible underneath. */
351
+ .dp-picker-button {
352
+ display: grid;
353
+ place-items: center;
354
+ grid-column: 1;
355
+ grid-row: 1;
356
+ justify-self: end;
357
+ align-self: stretch;
358
+ inline-size: var(--dp-picker-button-size);
359
+ min-inline-size: 0;
360
+ border: 0;
361
+ background: transparent;
362
+ color: var(--dp-fg);
363
+ font: inherit;
364
+ cursor: pointer;
365
+ }
366
+
367
+ /* Like the native time indicator: the ring wraps the icon, not the field (the
368
+ field already shows its own ring through :focus-visible). Same radius as
369
+ everywhere else: no bespoke shape. */
370
+ .dp-picker-button:focus-visible {
371
+ outline: 2px solid var(--dp-accent);
372
+ outline-offset: -5px;
373
+ border-radius: var(--dp-radius);
374
+ }
375
+
376
+ /* Optional native time companions share the date row; authored inputs stay
377
+ consumer-styled, the picker only reserves their grid cells. Time tracks are
378
+ flexible (never `auto`) so a wide UA time control cannot squeeze the date.
379
+ Each companion keeps its own box and its own gap: nothing is joined, so
380
+ focus rings never merge and no neighbour swallows another's focus. */
381
+ date-picker:has(> [data-time-start], > [data-time-end]) {
382
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
383
+ column-gap: var(--dp-gap);
384
+ }
385
+
386
+ date-picker:has(> [data-time-start]):has(> [data-time-end]) {
387
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1fr);
388
+ }
389
+
390
+ date-picker > input[data-time-start] {
391
+ grid-column: 2;
392
+ grid-row: 1;
393
+ min-inline-size: 0;
394
+ }
395
+
396
+ date-picker > input[data-time-end] {
397
+ grid-column: 3;
398
+ grid-row: 1;
399
+ min-inline-size: 0;
400
+ }
401
+
402
+ /* Range: two stacked fields, each owning its trigger, sharing one popup and
403
+ one calendar. No trigger spans both bounds, so each bound keeps a local
404
+ affordance and a local focus target. */
405
+ date-picker[range] {
406
+ row-gap: var(--dp-gap);
240
407
  }
241
408
 
242
409
  date-picker[range] > input[data-range-start] {
@@ -247,35 +414,26 @@ date-picker[range] > input[data-range-end] {
247
414
  grid-row: 2;
248
415
  }
249
416
 
250
- .dp-picker-button {
251
- grid-column: 2;
252
- min-inline-size: 2.5rem;
253
- border: 1px solid var(--dp-border);
254
- border-inline-start: 0;
255
- border-start-start-radius: 0;
256
- border-end-start-radius: 0;
257
- border-start-end-radius: var(--dp-radius);
258
- border-end-end-radius: var(--dp-radius);
259
- background: var(--dp-bg);
260
- color: var(--dp-fg);
261
- font: inherit;
262
- cursor: pointer;
417
+ date-picker[range] > .dp-picker-button[data-endpoint="start"] {
418
+ grid-row: 1;
263
419
  }
264
420
 
265
- date-picker[range] > .dp-picker-button {
266
- grid-row: 1 / span 2;
421
+ date-picker[range] > .dp-picker-button[data-endpoint="end"] {
422
+ grid-row: 2;
267
423
  }
268
424
 
269
425
  .dp-picker-panel[popover] {
426
+ /* Safe baseline. show() switches to absolute only when the consumer
427
+ * explicitly selects document coordinate space. */
270
428
  position: fixed;
271
429
  inset: auto;
272
430
  margin: 0;
273
- padding: 0.65rem;
431
+ padding: 0.875rem 1rem;
274
432
  border: 1px solid var(--dp-border);
275
433
  border-radius: var(--dp-radius);
276
434
  background: var(--dp-bg);
277
435
  color: var(--dp-fg);
278
- box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 0.16);
436
+ box-shadow: 0 0.5rem 1.5rem rgb(15 23 42 / 0.12);
279
437
  max-block-size: var(--available-height, 90svh);
280
438
  max-inline-size: calc(100vi - 1rem);
281
439
  overflow: auto;
@@ -286,19 +444,22 @@ date-picker[range] > .dp-picker-button {
286
444
  }
287
445
 
288
446
  @media (forced-colors: active) {
289
- .dp-day[data-selected="true"] {
290
- border: 2px solid Highlight;
291
- background-color: Canvas;
292
- color: CanvasText;
293
- font-weight: 700;
447
+ /* The author-drawn caret is a gradient and disappears here: hand the
448
+ control back to the browser instead of trying to rebuild it. */
449
+ .dp-month-select,
450
+ [dir="rtl"] .dp-month-select {
451
+ appearance: auto;
452
+ background-image: none;
453
+ padding-inline: 0.45rem;
294
454
  }
295
455
 
296
- .dp-day[data-range-start="true"],
297
- .dp-day[data-range-end="true"] {
456
+ /* Fills vanish here, so selection and endpoints fall back to a 2px
457
+ Highlight border. The in-range underline is the only band marker left:
458
+ the range fill is lost and the browser does not know the semantics. */
459
+ .dp-day:is([data-selected="true"], [data-range-start="true"], [data-range-end="true"]) {
298
460
  border: 2px solid Highlight;
299
461
  background-color: Canvas;
300
462
  color: CanvasText;
301
- font-weight: 700;
302
463
  }
303
464
 
304
465
  .dp-day[data-in-range="true"] {
@@ -314,15 +475,8 @@ date-picker[range] > .dp-picker-button {
314
475
  background-color: Highlight;
315
476
  }
316
477
 
317
- .dp-day[data-selected="true"]::before {
318
- content: "";
319
- position: absolute;
320
- inset: 3px;
321
- border: 1px solid CanvasText;
322
- border-radius: calc(var(--dp-radius) * 0.5);
323
- pointer-events: none;
324
- }
325
-
478
+ /* Pin the today marker: the deterministic accent is a non-system color
479
+ and would otherwise remap unpredictably here. */
326
480
  .dp-day[data-today="true"] {
327
481
  border-color: Highlight;
328
482
  }
@@ -331,11 +485,4 @@ date-picker[range] > .dp-picker-button {
331
485
  color: CanvasText;
332
486
  forced-color-adjust: none;
333
487
  }
334
-
335
- .dp-day[data-disabled="true"],
336
- .dp-calendar-header :where(button, select, input):disabled,
337
- .dp-picker-button:disabled {
338
- opacity: 1;
339
- color: GrayText;
340
- }
341
488
  }