@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lekoala/date-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Native-first date picker and inline calendar primitives",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"typecheck": "tsc -p jsconfig.json",
|
|
144
144
|
"types": "tsc -p tsconfig.types.json",
|
|
145
145
|
"test": "bun test test/unit",
|
|
146
|
-
"test:syntax": "node --check src/date.js && node --check src/intl.js && node --check src/calendar-model.js && node --check src/source.js && node --check src/messages.js && node --check src/date-calendar.js && node --check src/date-field.js && node --check src/date-picker.js && node --check src/date-range.js && node --check src/index.js && node --check src/define.js",
|
|
146
|
+
"test:syntax": "node --check src/date.js && node --check src/intl.js && node --check src/calendar-model.js && node --check src/source.js && node --check src/messages.js && node --check src/date-calendar.js && node --check src/date-field.js && node --check src/date-picker.js && node --check src/date-range.js && node --check src/time.js && node --check src/index.js && node --check src/define.js",
|
|
147
147
|
"test:types": "tsc -p test/types/tsconfig.json",
|
|
148
148
|
"test:browser": "playwright test browser --project chromium",
|
|
149
149
|
"test:browser:accessibility": "playwright test test/browser/picker.spec.js test/browser/focus.spec.js test/browser/calendar.spec.js test/browser/form-state.spec.js test/browser/surfaces.spec.js",
|
|
@@ -162,13 +162,13 @@
|
|
|
162
162
|
"prepublishOnly": "bun run verify"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
|
-
"@biomejs/biome": "2.5.
|
|
165
|
+
"@biomejs/biome": "2.5.13",
|
|
166
166
|
"@lekoala/calendar": "0.1.0",
|
|
167
|
-
"@playwright/test": "1.
|
|
167
|
+
"@playwright/test": "1.63.0",
|
|
168
168
|
"temporal-polyfill": "^1.0.4",
|
|
169
169
|
"typescript": "7.0.2"
|
|
170
170
|
},
|
|
171
171
|
"dependencies": {
|
|
172
|
-
"@lekoala/floating": "^0.
|
|
172
|
+
"@lekoala/floating": "^0.2.0"
|
|
173
173
|
}
|
|
174
174
|
}
|
package/src/date-calendar.js
CHANGED
|
@@ -11,7 +11,14 @@ import {
|
|
|
11
11
|
todayISO,
|
|
12
12
|
} from "./date.js";
|
|
13
13
|
import { normalizeRange, rangePosition } from "./date-range.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
formatLongDate,
|
|
16
|
+
formatMonthYear,
|
|
17
|
+
monthNames,
|
|
18
|
+
monthYearOrder,
|
|
19
|
+
resolveLocale,
|
|
20
|
+
weekdayNames,
|
|
21
|
+
} from "./intl.js";
|
|
15
22
|
import { getDefaultMessages } from "./messages.js";
|
|
16
23
|
import { normalizeDateStates } from "./source.js";
|
|
17
24
|
|
|
@@ -93,6 +100,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
93
100
|
"fixed-weeks",
|
|
94
101
|
"show-week-numbers",
|
|
95
102
|
"selection",
|
|
103
|
+
"month-format",
|
|
96
104
|
];
|
|
97
105
|
|
|
98
106
|
constructor() {
|
|
@@ -237,6 +245,16 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
237
245
|
this.setAttribute("selection", value === "none" ? "none" : "single");
|
|
238
246
|
}
|
|
239
247
|
|
|
248
|
+
/** @public Visible month-name style in the month select (`long` | `short`); anything else falls back to `long`. The accessible grid heading always keeps the long month/year form. */
|
|
249
|
+
get monthFormat() {
|
|
250
|
+
return this.getAttribute("month-format") === "short" ? "short" : "long";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
set monthFormat(value) {
|
|
254
|
+
if (value === "short") this.setAttribute("month-format", "short");
|
|
255
|
+
else this.removeAttribute("month-format");
|
|
256
|
+
}
|
|
257
|
+
|
|
240
258
|
/** @public */
|
|
241
259
|
get highlightedRange() {
|
|
242
260
|
return { ...this._highlightedRange };
|
|
@@ -368,7 +386,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
368
386
|
|
|
369
387
|
/** @param {HTMLElement} element @returns {string} */
|
|
370
388
|
_focusTargetKey(element) {
|
|
371
|
-
if (element.matches(".dp-month-select, .dp-year-
|
|
389
|
+
if (element.matches(".dp-month-select, .dp-year-input") && element.id) {
|
|
372
390
|
return `#${CSS.escape(element.id)}`;
|
|
373
391
|
}
|
|
374
392
|
if (element.matches(".dp-nav[data-calendar-action]")) {
|
|
@@ -398,7 +416,8 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
398
416
|
const outsideBounds =
|
|
399
417
|
(this.min && compareDates(date, this.min) < 0) || (this.max && compareDates(date, this.max) > 0);
|
|
400
418
|
const explicitlyEnabled = state.enabled === true;
|
|
401
|
-
// min/max > state.disabled >
|
|
419
|
+
// min/max > resolved state.disabled > isDateDisabled(), unless resolved
|
|
420
|
+
// state.enabled === true lifts only isDateDisabled().
|
|
402
421
|
const disabledByRule = explicitlyEnabled ? false : Boolean(this._isDateDisabled?.(date));
|
|
403
422
|
const disabled = Boolean(outsideBounds) || Boolean(state.disabled) || disabledByRule;
|
|
404
423
|
return { ...state, disabled };
|
|
@@ -624,10 +643,6 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
624
643
|
this._setDisplay(`${this.display.slice(0, 4)}-${target.value}`);
|
|
625
644
|
return;
|
|
626
645
|
}
|
|
627
|
-
if (target instanceof HTMLSelectElement && target.matches(".dp-year-select")) {
|
|
628
|
-
this._setDisplay(`${target.value}-${this.display.slice(5, 7)}`);
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
646
|
if (target instanceof HTMLInputElement && target.matches(".dp-year-input")) {
|
|
632
647
|
const year = Number(target.value);
|
|
633
648
|
if (!Number.isInteger(year) || year < 1 || year > 9999) {
|
|
@@ -688,7 +703,7 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
688
703
|
const display = this.display;
|
|
689
704
|
const displayYear = yearOf(display);
|
|
690
705
|
const displayMonth = Number(display.slice(5, 7));
|
|
691
|
-
const names = monthNames(locale,
|
|
706
|
+
const names = monthNames(locale, this.monthFormat);
|
|
692
707
|
const weekdayShort = weekdayNames(locale, this.firstDay, "short");
|
|
693
708
|
const weekdayLong = weekdayNames(locale, this.firstDay, "long");
|
|
694
709
|
const weeks = this._weeks();
|
|
@@ -758,14 +773,22 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
758
773
|
? this._focusTargetKey(focusedElement)
|
|
759
774
|
: "";
|
|
760
775
|
|
|
776
|
+
// Visible month/year controls follow the locale order (Intl-derived, never
|
|
777
|
+
// direction-derived); the flexible header track stays under the month
|
|
778
|
+
// select via `data-year-first`.
|
|
779
|
+
const yearFirst = monthYearOrder(locale)[0] === "year";
|
|
780
|
+
this.toggleAttribute("data-year-first", yearFirst);
|
|
781
|
+
const monthControl = `<label class="dp-visually-hidden" for="${this._id}-month">${escapeHtml(this._messages.month)}</label>
|
|
782
|
+
<select id="${this._id}-month" class="dp-month-select" aria-label="${escapeHtml(this._messages.month)}">${monthOptions}</select>`;
|
|
783
|
+
const yearControl = `<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
|
|
784
|
+
<input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">`;
|
|
785
|
+
|
|
761
786
|
this._rendering = true;
|
|
762
787
|
this.innerHTML = `
|
|
763
788
|
<div class="dp-calendar-shell">
|
|
764
789
|
<div class="dp-calendar-header">
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
<label class="dp-visually-hidden" for="${this._id}-year">${escapeHtml(this._messages.year)}</label>
|
|
768
|
-
<input id="${this._id}-year" class="dp-year-input" type="number" inputmode="numeric" value="${displayYear}"${minYearAttr}${maxYearAttr} aria-label="${escapeHtml(this._messages.year)}">
|
|
790
|
+
${yearFirst ? yearControl : monthControl}
|
|
791
|
+
${yearFirst ? monthControl : yearControl}
|
|
769
792
|
<button type="button" class="dp-nav dp-prev" data-calendar-action="previous" aria-label="${escapeHtml(this._messages.previousMonth)}"${prevDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m10 4-4 4 4 4"/></svg></button>
|
|
770
793
|
<button type="button" class="dp-nav dp-next" data-calendar-action="next" aria-label="${escapeHtml(this._messages.nextMonth)}"${nextDisabled ? " disabled" : ""}><svg aria-hidden="true" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 4 4 4-4 4"/></svg></button>
|
|
771
794
|
</div>
|
|
@@ -779,7 +802,16 @@ export class DateCalendarElement extends HTMLElement {
|
|
|
779
802
|
|
|
780
803
|
if (focusKey) {
|
|
781
804
|
queueMicrotask(() => {
|
|
782
|
-
|
|
805
|
+
let selector = focusKey;
|
|
806
|
+
// A day-cell key goes stale across display changes: the old date can
|
|
807
|
+
// reappear as outside-month padding in the new month (fixed-weeks or
|
|
808
|
+
// overlapping edge weeks). Restoring it would yank keyboard focus back
|
|
809
|
+
// onto the date the user just navigated away from, so retarget day
|
|
810
|
+
// cells to the current keyboard position. Controls keep exact restore.
|
|
811
|
+
if (selector.startsWith(".dp-day[data-date=")) {
|
|
812
|
+
selector = `.dp-day[data-date="${CSS.escape(this._model.focused)}"]`;
|
|
813
|
+
}
|
|
814
|
+
const target = this.querySelector(selector);
|
|
783
815
|
if (target instanceof HTMLElement) target.focus();
|
|
784
816
|
});
|
|
785
817
|
}
|
package/src/date-field.js
CHANGED
|
@@ -117,18 +117,26 @@ export class DateFieldController {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/** Live text input: the raw text no longer matches the canonical value, so
|
|
120
|
-
* the hidden ISO must not submit it.
|
|
120
|
+
* the hidden ISO must not submit it. A text that formats back to the current
|
|
121
|
+
* canonical value (e.g. re-applied by a calendar pick) keeps the ISO value
|
|
122
|
+
* valid; invalidating in-flight commits first still drops stale async work. */
|
|
121
123
|
handleInput() {
|
|
122
124
|
const input = this.input;
|
|
123
|
-
|
|
125
|
+
const text = input.value.trim();
|
|
126
|
+
const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
|
|
124
127
|
this.dirty();
|
|
128
|
+
if (text === canonical) {
|
|
129
|
+
this._dirty = false;
|
|
130
|
+
if (this.hidden) this.hidden.value = this.canonical;
|
|
131
|
+
input.setCustomValidity("");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
this._dirty = true;
|
|
125
135
|
if (this.hidden) this.hidden.value = "";
|
|
126
|
-
const text = input.value.trim();
|
|
127
136
|
if (!text) {
|
|
128
137
|
input.setCustomValidity("");
|
|
129
138
|
return;
|
|
130
139
|
}
|
|
131
|
-
const canonical = this.canonical ? this.adapter.format(this.canonical) : "";
|
|
132
140
|
input.setCustomValidity(text === canonical ? "" : this.messages.invalidDate);
|
|
133
141
|
}
|
|
134
142
|
|
package/src/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
|
}
|