@lekoala/date-picker 0.1.0 → 0.2.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 +30 -5
- package/custom-elements.json +44 -0
- package/dist/date-picker.css +114 -18
- package/dist/date-picker.js +176 -19
- package/dist/date-picker.min.css +1 -1
- package/dist/date-picker.min.js +13 -13
- package/dist/date-picker.standalone.min.js +128 -32
- package/dist/types/calendar-model.d.ts.map +1 -1
- package/dist/types/date-calendar.d.ts +3 -0
- package/dist/types/date-calendar.d.ts.map +1 -1
- package/dist/types/date-picker.d.ts +24 -0
- package/dist/types/date-picker.d.ts.map +1 -1
- package/dist/types/date.d.ts +6 -0
- package/dist/types/date.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/API.md +52 -2
- package/docs/CALENDAR-INTEGRATION.md +12 -6
- package/docs/DECISIONS.md +8 -1
- package/docs/ROADMAP.md +3 -2
- package/docs/USE_CASES.md +14 -1
- package/package.json +2 -2
- package/src/calendar-model.js +2 -1
- package/src/date-calendar.js +44 -8
- package/src/date-picker.css +114 -18
- package/src/date-picker.js +108 -5
- package/src/date.js +13 -3
- package/src/index.js +1 -0
- package/src/intl.js +38 -2
- 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
|
|
|
@@ -10,12 +10,14 @@ This project fills the gap between a normal editable date field and a full sched
|
|
|
10
10
|
|
|
11
11
|
### `<date-calendar>`
|
|
12
12
|
|
|
13
|
-
An inline calendar with real month/year navigation, keyboard support and three
|
|
13
|
+
An inline calendar with real month/year navigation, keyboard support and three distinct pieces of state:
|
|
14
14
|
|
|
15
15
|
- `display` — the month being rendered (`YYYY-MM`);
|
|
16
16
|
- `focusedDate` — the roving keyboard target (`YYYY-MM-DD`);
|
|
17
17
|
- `value` — the selected date (`YYYY-MM-DD`, optional).
|
|
18
18
|
|
|
19
|
+
`display` and `focusedDate` are coupled during navigation/display: changing the displayed month re-clamps the focused day into that month, because the roving-tabbable grid cell must stay visible. `value` and activation stay independent of both.
|
|
20
|
+
|
|
19
21
|
User activation is exposed separately as `dateactivate`. This is what makes the same calendar useful as a selector **and** as a mini navigator.
|
|
20
22
|
|
|
21
23
|
```html
|
|
@@ -75,6 +77,24 @@ const cleanup = linkDateRange(startPicker, endPicker);
|
|
|
75
77
|
|
|
76
78
|
The relationship applies `end >= start` and `start <= end` as effective bounds. Neither picker knows about its sibling.
|
|
77
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
|
+
|
|
78
98
|
## Availability and per-day metadata
|
|
79
99
|
|
|
80
100
|
The calendar accepts a source function or `{ load() }` object with the same abortable shape used elsewhere in LeKoala components:
|
|
@@ -192,7 +212,7 @@ The default ESM exports point at `src/`, matching the native-first development s
|
|
|
192
212
|
|
|
193
213
|
## Scope
|
|
194
214
|
|
|
195
|
-
Included in v0.
|
|
215
|
+
Included in v0.2:
|
|
196
216
|
|
|
197
217
|
- inline single-date selection;
|
|
198
218
|
- mini-calendar/navigation-only use;
|
|
@@ -203,15 +223,20 @@ Included in v0.1:
|
|
|
203
223
|
- presentation-only range band (`highlightedRange`);
|
|
204
224
|
- shared-calendar two-field range picker (`<date-picker range>`);
|
|
205
225
|
- linked start/end pickers (`linkDateRange`);
|
|
226
|
+
- native time companions (one or two `input[type=time]` beside the date);
|
|
227
|
+
- `Intl` month/year control order + `month-format="short"`;
|
|
228
|
+
- RTL through the inherited `dir` (no `isRTL` option);
|
|
229
|
+
- readable keyboard focus on disabled/selected days;
|
|
230
|
+
- compact-consumer CSS tokens (`--dp-header-control-size`, `--dp-day-radius`, …);
|
|
206
231
|
- async per-day state with cancellation;
|
|
207
232
|
- custom day decorations that cannot replace the accessible cell;
|
|
208
233
|
- fixed six-row presentation as an opt-in;
|
|
209
234
|
- ISO week numbers;
|
|
210
235
|
- keyboard and focus contracts.
|
|
211
236
|
|
|
212
|
-
Deliberately out of scope for v0.
|
|
237
|
+
Deliberately out of scope for v0.2:
|
|
213
238
|
|
|
214
|
-
- time
|
|
239
|
+
- custom time-picker UI and combined datetime values (only native `input[type=time]` companions are supported);
|
|
215
240
|
- timezones;
|
|
216
241
|
- in-place single-calendar range *selection interaction* (use the two-field picker);
|
|
217
242
|
- 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,21 @@
|
|
|
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-nav-gap"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "--dp-year-inline-size"
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
"name": "--dp-day-radius"
|
|
193
215
|
}
|
|
194
216
|
]
|
|
195
217
|
}
|
|
@@ -228,6 +250,9 @@
|
|
|
228
250
|
},
|
|
229
251
|
{
|
|
230
252
|
"name": "open-on-focus"
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"name": "month-format"
|
|
231
256
|
}
|
|
232
257
|
],
|
|
233
258
|
"members": [
|
|
@@ -251,6 +276,10 @@
|
|
|
251
276
|
"kind": "getter",
|
|
252
277
|
"name": "locale"
|
|
253
278
|
},
|
|
279
|
+
{
|
|
280
|
+
"kind": "getter",
|
|
281
|
+
"name": "monthFormat"
|
|
282
|
+
},
|
|
254
283
|
{
|
|
255
284
|
"kind": "getter",
|
|
256
285
|
"name": "min"
|
|
@@ -347,6 +376,21 @@
|
|
|
347
376
|
},
|
|
348
377
|
{
|
|
349
378
|
"name": "--dp-gap"
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"name": "--dp-header-control-size"
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
"name": "--dp-header-block-gap"
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"name": "--dp-nav-gap"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
"name": "--dp-year-inline-size"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"name": "--dp-day-radius"
|
|
350
394
|
}
|
|
351
395
|
]
|
|
352
396
|
}
|
package/dist/date-picker.css
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
--dp-radius: 0.5rem;
|
|
10
10
|
--dp-day-size: 2.75rem;
|
|
11
11
|
--dp-gap: 0.25rem;
|
|
12
|
+
--dp-header-control-size: 2.25rem;
|
|
13
|
+
--dp-header-block-gap: 0.4rem;
|
|
14
|
+
--dp-nav-gap: calc(var(--dp-gap) * 2.5);
|
|
15
|
+
--dp-year-inline-size: 5.5rem;
|
|
16
|
+
--dp-day-radius: calc(var(--dp-radius) * 0.75);
|
|
12
17
|
color: var(--dp-fg);
|
|
13
18
|
font: inherit;
|
|
14
19
|
}
|
|
@@ -39,22 +44,46 @@ date-calendar {
|
|
|
39
44
|
.dp-calendar-header {
|
|
40
45
|
display: grid;
|
|
41
46
|
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
|
42
|
-
|
|
47
|
+
/* Stretch (not center): once a UA natural height exceeds
|
|
48
|
+
--dp-header-control-size, the row grows to the tallest control and the
|
|
49
|
+
others stretch to match, so month/year/nav stay equal. */
|
|
50
|
+
align-items: stretch;
|
|
43
51
|
gap: var(--dp-gap);
|
|
44
|
-
margin-block-end:
|
|
52
|
+
margin-block-end: var(--dp-header-block-gap);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Locale year-first order (Intl-derived, direction-independent): the flexible
|
|
56
|
+
track stays under the month select so the fixed-width year input never
|
|
57
|
+
stretches. Visual mirroring itself is left to the inherited `dir`. */
|
|
58
|
+
date-calendar[data-year-first] .dp-calendar-header {
|
|
59
|
+
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
45
60
|
}
|
|
46
61
|
|
|
47
62
|
.dp-calendar-header .dp-prev {
|
|
48
|
-
margin-inline-start:
|
|
63
|
+
margin-inline-start: var(--dp-nav-gap);
|
|
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;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The select menulist carries UA-internal leading that author padding only
|
|
82
|
+
adds to (and it ignores line-height), so it gets zero author padding.
|
|
83
|
+
Equal outer heights come from the header grid's stretch alignment: the row
|
|
84
|
+
sizes to the tallest control and the others stretch to match. */
|
|
85
|
+
.dp-calendar-header select {
|
|
86
|
+
padding-block: 0;
|
|
58
87
|
}
|
|
59
88
|
|
|
60
89
|
.dp-calendar-header select,
|
|
@@ -65,16 +94,20 @@ date-calendar {
|
|
|
65
94
|
|
|
66
95
|
.dp-month-select {
|
|
67
96
|
inline-size: 100%;
|
|
97
|
+
/* Long month names in a narrow track (compact consumers, long locales)
|
|
98
|
+
would paint under the native caret: truncate with an ellipsis instead. */
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
text-overflow: ellipsis;
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
.dp-year-input {
|
|
71
|
-
inline-size:
|
|
104
|
+
inline-size: var(--dp-year-inline-size);
|
|
72
105
|
}
|
|
73
106
|
|
|
74
107
|
.dp-nav {
|
|
75
108
|
display: grid;
|
|
76
109
|
place-items: center;
|
|
77
|
-
inline-size:
|
|
110
|
+
inline-size: var(--dp-header-control-size);
|
|
78
111
|
padding: 0;
|
|
79
112
|
font-size: 1.35rem;
|
|
80
113
|
line-height: 1;
|
|
@@ -88,6 +121,14 @@ date-calendar {
|
|
|
88
121
|
pointer-events: none;
|
|
89
122
|
}
|
|
90
123
|
|
|
124
|
+
/* Inherited RTL mirrors the header layout via the grid; mirror the nav glyphs
|
|
125
|
+
so previous/next keep their visual sense. Actions stay chronological: no
|
|
126
|
+
button swap, no keyboard remapping. */
|
|
127
|
+
[dir="rtl"] .dp-nav svg,
|
|
128
|
+
html[dir="rtl"] .dp-nav svg {
|
|
129
|
+
transform: scaleX(-1);
|
|
130
|
+
}
|
|
131
|
+
|
|
91
132
|
.dp-calendar-header :where(button, select, input):disabled,
|
|
92
133
|
.dp-picker-button:disabled {
|
|
93
134
|
cursor: not-allowed;
|
|
@@ -104,12 +145,9 @@ date-calendar {
|
|
|
104
145
|
background: var(--dp-hover);
|
|
105
146
|
}
|
|
106
147
|
|
|
107
|
-
.dp-calendar-heading
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
font-weight: 600;
|
|
111
|
-
text-align: center;
|
|
112
|
-
}
|
|
148
|
+
/* .dp-calendar-heading is always visually hidden: it names the grid via
|
|
149
|
+
aria-labelledby and stays the aria-live announcement channel while the
|
|
150
|
+
month/year selects render the same info visually. */
|
|
113
151
|
|
|
114
152
|
.dp-grid {
|
|
115
153
|
border-collapse: separate;
|
|
@@ -136,7 +174,7 @@ date-calendar {
|
|
|
136
174
|
.dp-day {
|
|
137
175
|
position: relative;
|
|
138
176
|
border: 1px solid transparent;
|
|
139
|
-
border-radius:
|
|
177
|
+
border-radius: var(--dp-day-radius);
|
|
140
178
|
cursor: pointer;
|
|
141
179
|
outline: none;
|
|
142
180
|
vertical-align: middle;
|
|
@@ -145,10 +183,21 @@ date-calendar {
|
|
|
145
183
|
|
|
146
184
|
.dp-day:focus-visible {
|
|
147
185
|
outline: 2px solid var(--dp-accent);
|
|
148
|
-
outline-offset:
|
|
186
|
+
outline-offset: 2px;
|
|
187
|
+
/* Separation gap between the cell fill and the ring: invisible on plain
|
|
188
|
+
days, but keeps the ring readable on accent-filled days (selected, range
|
|
189
|
+
endpoints) where an accent-on-accent outline would disappear. */
|
|
190
|
+
box-shadow: 0 0 0 2px var(--dp-bg);
|
|
149
191
|
z-index: 1;
|
|
150
192
|
}
|
|
151
193
|
|
|
194
|
+
/* Base tint for days outside the displayed month. It stays before the
|
|
195
|
+
selection states so selected/range endpoints keep their accent-contrast
|
|
196
|
+
text on the accent fill (equal specificity, source order decides). */
|
|
197
|
+
.dp-day[data-outside-month="true"] {
|
|
198
|
+
color: var(--dp-muted);
|
|
199
|
+
}
|
|
200
|
+
|
|
152
201
|
.dp-day[data-selected="true"] {
|
|
153
202
|
background: var(--dp-accent);
|
|
154
203
|
color: var(--dp-accent-fg);
|
|
@@ -158,10 +207,6 @@ date-calendar {
|
|
|
158
207
|
border-color: var(--dp-accent);
|
|
159
208
|
}
|
|
160
209
|
|
|
161
|
-
.dp-day[data-outside-month="true"] {
|
|
162
|
-
color: var(--dp-muted);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
210
|
.dp-day[data-disabled="true"] {
|
|
166
211
|
cursor: not-allowed;
|
|
167
212
|
opacity: 0.42;
|
|
@@ -204,6 +249,17 @@ date-calendar {
|
|
|
204
249
|
font-variant-numeric: tabular-nums;
|
|
205
250
|
}
|
|
206
251
|
|
|
252
|
+
/* A focused disabled day stays discoverable by keyboard: restore full opacity
|
|
253
|
+
so the focus outline renders at full strength, and mark the number with the
|
|
254
|
+
muted tone (same language as outside-month days) instead of the wash-out. */
|
|
255
|
+
.dp-day[data-disabled="true"]:focus-visible {
|
|
256
|
+
opacity: 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.dp-day[data-disabled="true"]:focus-visible .dp-day-number {
|
|
260
|
+
color: var(--dp-muted);
|
|
261
|
+
}
|
|
262
|
+
|
|
207
263
|
.dp-day-extra {
|
|
208
264
|
position: absolute;
|
|
209
265
|
inset-inline: 2px;
|
|
@@ -238,16 +294,56 @@ date-picker > input:not([type="hidden"]) {
|
|
|
238
294
|
min-inline-size: 0;
|
|
239
295
|
}
|
|
240
296
|
|
|
297
|
+
/* Optional native time companions share the date row; authored inputs stay
|
|
298
|
+
consumer-styled, the picker only reserves their grid cells. Time tracks are
|
|
299
|
+
flexible (never `auto`) so a wide UA time control cannot squeeze the date. */
|
|
300
|
+
date-picker:has(> [data-time-start], > [data-time-end]) {
|
|
301
|
+
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
date-picker:has(> [data-time-start]):has(> [data-time-end]) {
|
|
305
|
+
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr) minmax(0, 1fr);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
date-picker > input[data-time-start] {
|
|
309
|
+
grid-column: 3;
|
|
310
|
+
grid-row: 1;
|
|
311
|
+
min-inline-size: 0;
|
|
312
|
+
inline-size: 100%;
|
|
313
|
+
margin-inline-start: var(--dp-gap);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
date-picker > input[data-time-end] {
|
|
317
|
+
grid-column: 4;
|
|
318
|
+
grid-row: 1;
|
|
319
|
+
min-inline-size: 0;
|
|
320
|
+
inline-size: 100%;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* From/to read as one joined control: square the inner corners, but only
|
|
324
|
+
when both companions are present so a lone time keeps its own radius. */
|
|
325
|
+
date-picker:has(> [data-time-start]):has(> [data-time-end]) > input[data-time-start] {
|
|
326
|
+
border-start-end-radius: 0;
|
|
327
|
+
border-end-end-radius: 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
date-picker:has(> [data-time-start]):has(> [data-time-end]) > input[data-time-end] {
|
|
331
|
+
border-start-start-radius: 0;
|
|
332
|
+
border-end-start-radius: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
241
335
|
date-picker[range] > input:not([type="hidden"]) {
|
|
242
336
|
grid-column: 1;
|
|
243
337
|
}
|
|
244
338
|
|
|
245
339
|
date-picker[range] > input[data-range-start] {
|
|
246
340
|
grid-row: 1;
|
|
341
|
+
border-end-start-radius: 0;
|
|
247
342
|
}
|
|
248
343
|
|
|
249
344
|
date-picker[range] > input[data-range-end] {
|
|
250
345
|
grid-row: 2;
|
|
346
|
+
border-start-start-radius: 0;
|
|
251
347
|
}
|
|
252
348
|
|
|
253
349
|
.dp-picker-button {
|