@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 +33 -8
- package/custom-elements.json +54 -0
- package/dist/date-picker.css +223 -76
- package/dist/date-picker.js +327 -90
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +237 -90
- package/dist/types/date-calendar.d.ts +3 -0
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-field.d.ts +3 -1
- package/dist/types/date-field.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +86 -5
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/intl.d.ts +9 -0
- package/dist/types/intl.d.ts.map +1 -1
- package/dist/types/time.d.ts +8 -0
- package/dist/types/time.d.ts.map +1 -0
- package/docs/ACCESSIBILITY.md +17 -9
- package/docs/API.md +68 -2
- package/docs/ARCHITECTURE.md +16 -2
- package/docs/AT-CHECKLIST.md +1 -1
- package/docs/DECISIONS.md +10 -1
- package/docs/MIGRATION-FLATPICKR.md +1 -1
- package/docs/ROADMAP.md +3 -2
- package/docs/USE_CASES.md +14 -1
- package/package.json +5 -5
- package/src/date-calendar.js +45 -13
- package/src/date-field.js +12 -4
- package/src/date-picker.css +223 -76
- package/src/date-picker.js +314 -80
- package/src/index.js +1 -0
- package/src/intl.js +35 -0
- package/src/time.js +39 -0
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.
|
|
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
|
-
|
|
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.
|
|
165
|
-
- Playwright 1.
|
|
166
|
-
- `@lekoala/floating ^0.
|
|
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.
|
|
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.
|
|
239
|
+
Deliberately out of scope for v0.2:
|
|
215
240
|
|
|
216
|
-
- time
|
|
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;
|
package/custom-elements.json
CHANGED
|
@@ -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
|
}
|
package/dist/date-picker.css
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
:where(date-calendar, date-picker) {
|
|
2
|
-
--dp-bg:
|
|
3
|
-
--dp-fg:
|
|
4
|
-
--dp-muted: color-mix(in srgb,
|
|
5
|
-
--dp-border: color-mix(in srgb,
|
|
6
|
-
--dp-hover: color-mix(in srgb,
|
|
7
|
-
--dp-accent:
|
|
8
|
-
--dp-accent-fg:
|
|
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.
|
|
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
|
-
|
|
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:
|
|
56
|
+
margin-block-end: var(--dp-header-block-gap);
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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;
|
|
114
|
-
border-spacing: 0
|
|
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.
|
|
192
|
+
block-size: 1.5rem;
|
|
128
193
|
color: var(--dp-muted);
|
|
129
194
|
font-size: 0.72rem;
|
|
130
|
-
font-weight:
|
|
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:
|
|
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:
|
|
146
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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-
|
|
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:
|
|
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.
|
|
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(
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
318
|
-
|
|
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
|
}
|