@lekoala/date-picker 0.1.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.
Files changed (104) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +236 -0
  3. package/custom-elements.json +366 -0
  4. package/dist/date-picker.css +344 -0
  5. package/dist/date-picker.js +2693 -0
  6. package/dist/date-picker.min.css +1 -0
  7. package/dist/date-picker.min.js +17 -0
  8. package/dist/date-picker.standalone.min.js +361 -0
  9. package/dist/types/calendar-model.d.ts +34 -0
  10. package/dist/types/calendar-model.d.ts.map +1 -0
  11. package/dist/types/date-calendar.d.ts +228 -0
  12. package/dist/types/date-calendar.d.ts.map +1 -0
  13. package/dist/types/date-field.d.ts +94 -0
  14. package/dist/types/date-field.d.ts.map +1 -0
  15. package/dist/types/date-picker.d.ts +258 -0
  16. package/dist/types/date-picker.d.ts.map +1 -0
  17. package/dist/types/date-range.d.ts +89 -0
  18. package/dist/types/date-range.d.ts.map +1 -0
  19. package/dist/types/date.d.ts +61 -0
  20. package/dist/types/date.d.ts.map +1 -0
  21. package/dist/types/define.d.ts +2 -0
  22. package/dist/types/define.d.ts.map +1 -0
  23. package/dist/types/index.d.ts +9 -0
  24. package/dist/types/index.d.ts.map +1 -0
  25. package/dist/types/intl.d.ts +33 -0
  26. package/dist/types/intl.d.ts.map +1 -0
  27. package/dist/types/locales/ar.d.ts +22 -0
  28. package/dist/types/locales/ar.d.ts.map +1 -0
  29. package/dist/types/locales/de.d.ts +22 -0
  30. package/dist/types/locales/de.d.ts.map +1 -0
  31. package/dist/types/locales/en.d.ts +22 -0
  32. package/dist/types/locales/en.d.ts.map +1 -0
  33. package/dist/types/locales/es.d.ts +22 -0
  34. package/dist/types/locales/es.d.ts.map +1 -0
  35. package/dist/types/locales/fr.d.ts +22 -0
  36. package/dist/types/locales/fr.d.ts.map +1 -0
  37. package/dist/types/locales/hi.d.ts +22 -0
  38. package/dist/types/locales/hi.d.ts.map +1 -0
  39. package/dist/types/locales/id.d.ts +22 -0
  40. package/dist/types/locales/id.d.ts.map +1 -0
  41. package/dist/types/locales/it.d.ts +22 -0
  42. package/dist/types/locales/it.d.ts.map +1 -0
  43. package/dist/types/locales/ja.d.ts +22 -0
  44. package/dist/types/locales/ja.d.ts.map +1 -0
  45. package/dist/types/locales/ko.d.ts +22 -0
  46. package/dist/types/locales/ko.d.ts.map +1 -0
  47. package/dist/types/locales/nl.d.ts +22 -0
  48. package/dist/types/locales/nl.d.ts.map +1 -0
  49. package/dist/types/locales/pl.d.ts +22 -0
  50. package/dist/types/locales/pl.d.ts.map +1 -0
  51. package/dist/types/locales/pt-BR.d.ts +22 -0
  52. package/dist/types/locales/pt-BR.d.ts.map +1 -0
  53. package/dist/types/locales/pt-PT.d.ts +22 -0
  54. package/dist/types/locales/pt-PT.d.ts.map +1 -0
  55. package/dist/types/locales/ru.d.ts +22 -0
  56. package/dist/types/locales/ru.d.ts.map +1 -0
  57. package/dist/types/locales/tr.d.ts +22 -0
  58. package/dist/types/locales/tr.d.ts.map +1 -0
  59. package/dist/types/locales/zh-CN.d.ts +22 -0
  60. package/dist/types/locales/zh-CN.d.ts.map +1 -0
  61. package/dist/types/messages.d.ts +43 -0
  62. package/dist/types/messages.d.ts.map +1 -0
  63. package/dist/types/source.d.ts +28 -0
  64. package/dist/types/source.d.ts.map +1 -0
  65. package/docs/ACCESSIBILITY.md +128 -0
  66. package/docs/API.md +289 -0
  67. package/docs/ARCHITECTURE.md +183 -0
  68. package/docs/AT-CHECKLIST.md +63 -0
  69. package/docs/CALENDAR-INTEGRATION.md +70 -0
  70. package/docs/DECISIONS.md +40 -0
  71. package/docs/MIGRATION-FLATPICKR.md +92 -0
  72. package/docs/ROADMAP.md +105 -0
  73. package/docs/TESTING.md +73 -0
  74. package/docs/USE_CASES.md +128 -0
  75. package/package.json +174 -0
  76. package/src/calendar-model.js +80 -0
  77. package/src/date-calendar.js +800 -0
  78. package/src/date-field.js +203 -0
  79. package/src/date-picker.css +344 -0
  80. package/src/date-picker.js +1140 -0
  81. package/src/date-range.js +172 -0
  82. package/src/date.js +203 -0
  83. package/src/define.js +9 -0
  84. package/src/index.js +15 -0
  85. package/src/intl.js +155 -0
  86. package/src/locales/ar.js +20 -0
  87. package/src/locales/de.js +20 -0
  88. package/src/locales/en.js +20 -0
  89. package/src/locales/es.js +20 -0
  90. package/src/locales/fr.js +20 -0
  91. package/src/locales/hi.js +20 -0
  92. package/src/locales/id.js +20 -0
  93. package/src/locales/it.js +20 -0
  94. package/src/locales/ja.js +20 -0
  95. package/src/locales/ko.js +20 -0
  96. package/src/locales/nl.js +20 -0
  97. package/src/locales/pl.js +20 -0
  98. package/src/locales/pt-BR.js +20 -0
  99. package/src/locales/pt-PT.js +20 -0
  100. package/src/locales/ru.js +20 -0
  101. package/src/locales/tr.js +20 -0
  102. package/src/locales/zh-CN.js +20 -0
  103. package/src/messages.js +31 -0
  104. package/src/source.js +50 -0
@@ -0,0 +1,183 @@
1
+ # Architecture
2
+
3
+ ## Goal
4
+
5
+ The package should remain much smaller than a scheduling calendar while covering the cases where a normal date field stops being enough.
6
+
7
+ The key architectural rule is that **the calendar is the primitive and the picker is a composition**.
8
+
9
+ ```text
10
+ civil date helpers
11
+ ↓
12
+ CalendarModel
13
+ ↓
14
+ <date-calendar> ── source / date state / day decoration
15
+ ↓
16
+ <date-picker> ─── input / parse+format / Popover / floating
17
+ ↓
18
+ linkDateRange() or application navigation
19
+ ```
20
+
21
+ ## 1. Civil dates are the canonical model
22
+
23
+ Public values are `YYYY-MM-DD` strings. Public months are `YYYY-MM` strings.
24
+
25
+ A selected date is not an instant. The core therefore does not expose a JavaScript `Date`, epoch milliseconds, UTC conversion, timezone or `Temporal` object as the canonical API.
26
+
27
+ `Date` is used internally only as a browser primitive for safe UTC calendar arithmetic and `Intl.DateTimeFormat` presentation.
28
+
29
+ This keeps form values predictable and avoids accidental midnight/timezone conversions.
30
+
31
+ ## 2. Four distinct concepts
32
+
33
+ The component must never collapse these concepts:
34
+
35
+ ```text
36
+ display = month currently rendered
37
+ focus = current keyboard target
38
+ activate = date explicitly actioned by click / Enter / Space
39
+ value = selected date
40
+ ```
41
+
42
+ Examples:
43
+
44
+ - changing the month select changes `display`, not `value`;
45
+ - pressing ArrowRight changes `focus`, not `value`;
46
+ - pressing Enter emits `dateactivate`;
47
+ - in `selection="single"`, accepted activation also updates `value`;
48
+ - in `selection="none"`, activation is left entirely to the consumer.
49
+
50
+ This is what lets the same `<date-calendar>` power both a date picker and the mini-calendar beside `@lekoala/calendar`.
51
+
52
+ `highlightedRange` is a separate, strictly explanatory concept: it paints a band over rendered cells (`data-range-start` / `data-in-range` / `data-range-end`) without touching `display`, `focusedDate`, activation or `value`. The picker, an inline calendar or an app-owned controller can attach a range for display; the band is validated by `normalizeRange()` (ordered, never reordered) so the presentation never represents a range the model would contest. The business range (which may be temporarily inverted or in progress) stays in whichever controller owns it and is only forwarded to `highlightedRange` when it is displayable.
53
+
54
+ ## 3. True month math vs presentation
55
+
56
+ `dates.getMonthWeeks()` returns the true civil weeks that cover a month: 4, 5 or 6 full rows.
57
+
58
+ It does not know whether the UI wants a stable height.
59
+
60
+ `<date-calendar fixed-weeks>` pads the rendered grid to six rows. That choice belongs to presentation, not date math.
61
+
62
+ This mirrors the existing `@lekoala/calendar` direction: surrounding UI can consume the same true month primitive and decide how to display it.
63
+
64
+ ## 4. DOM ownership
65
+
66
+ `<date-calendar>` owns the grid structure, focus semantics and interactive cells. A consumer cannot replace the day `td`.
67
+
68
+ Two extension seams are intentionally narrow:
69
+
70
+ ### `dateState(date, sourceState)`
71
+
72
+ Returns state/metadata for one day. The core reads only generic fields such as:
73
+
74
+ - `disabled`;
75
+ - `description`.
76
+
77
+ Everything else remains application metadata.
78
+
79
+ ### `renderDay(date, state)`
80
+
81
+ Returns a `Node`, `DocumentFragment`, string or nothing. The result is appended inside a component-owned, non-interactive, `aria-hidden` container. That container name is an implementation detail: consumers style the nodes they return and keep their own class names.
82
+
83
+ Accessible meaning belongs in `state.description`, not in purely visual dots/badges.
84
+
85
+ This prevents rich decorations from breaking grid semantics or keyboard behavior.
86
+
87
+ ## 5. Constraints have one resolution path
88
+
89
+ `calendar.getDateState(date)` composes:
90
+
91
+ 1. remote/source state;
92
+ 2. `dateState()` application state;
93
+ 3. `min` / `max`;
94
+ 4. `isDateDisabled()`.
95
+
96
+ `enabled: true` is the explicit exception seam: it can reopen a date disabled by generic/source rules, but never bypasses hard `min`/`max` bounds.
97
+
98
+ Both click activation and typed picker validation use this resolved state.
99
+
100
+ There should not be one "disabled" implementation for the grid and another for manual text input.
101
+
102
+ ## 6. Async source contract
103
+
104
+ A source is either a function or an object with `load()`:
105
+
106
+ ```js
107
+ load({ start, end }, { signal })
108
+ ```
109
+
110
+ The range is inclusive and covers the cells actually rendered for that month. With `fixed-weeks`, this can extend beyond the month itself.
111
+
112
+ Navigation cancels superseded loads using `AbortController`. Results are normalized to a map keyed by date.
113
+
114
+ A load reports three outcomes: `loaded`, `cancelled` and `failed`. A cancelled or failed load never authorizes a date: `ensureDate()` returns `false` and both cell activation and typed picker validation refuse the date until a later load confirms it. `dateloaderror` keeps observing the failure; the loader error is never swallowed silently on the validation path.
115
+
116
+ Supported response shapes:
117
+
118
+ ```js
119
+ { dates: { "2026-09-10": {...} } }
120
+ { "2026-09-10": {...} }
121
+ [{ date: "2026-09-10", ...state }]
122
+ ```
123
+
124
+ No server URL, query parameter names or backend schema are hard-coded into the core. `createFetchSource()` is only a convenience adapter.
125
+
126
+ ## 7. Picker composition
127
+
128
+ `<date-picker>` enhances a direct child text input.
129
+
130
+ The visible input owns:
131
+
132
+ - label association;
133
+ - focus;
134
+ - typing;
135
+ - required state;
136
+ - validation UI;
137
+ - the localized display string.
138
+
139
+ A hidden input receives the original `name` and owns canonical form submission. This avoids Flatpickr's "pretty format means no editable input" tradeoff while preserving ordinary form submission.
140
+
141
+ The picker creates a `<date-calendar selection="none">` in a native manual popover. On `dateactivate`, it commits the canonical value, formats the visible input and closes the popup.
142
+
143
+ ## 8. Popup responsibilities
144
+
145
+ Native Popover owns the top layer. `@lekoala/floating` owns coordinates and geometry updates.
146
+
147
+ The picker does not implement:
148
+
149
+ - custom portals;
150
+ - dropdown parent options;
151
+ - manual clipping calculations;
152
+ - z-index escalation APIs.
153
+
154
+ The popup remains in authored DOM, preserving inherited locale/theme/density context.
155
+
156
+ ## 9. Range relationships
157
+
158
+ Two product shapes cover the range cases, and both keep range rules outside the classes:
159
+
160
+ `<date-picker range>` shares **one** calendar surface between two editable fields. Each input is owned by a `DateFieldController` (parsing, hidden ISO, commit/stale protection); the pair is driven by `DateRangeController` in `date-range.js`, which owns the active-endpoint transitions and the start/end state. `DatePickerElement` remains a shell: it coordinates DOM/popup/focus only. `value` is unavailable in range mode and `range` is the atomic pair API; inverted or incomplete business ranges are never forwarded to `highlightedRange`, which stays presentation-only.
161
+
162
+ `linkDateRange(start, end)` keeps the external relationship for two visually separate pickers:
163
+
164
+ ```text
165
+ end.min = max(authorMin, start.value)
166
+ start.max = min(authorMax, end.value)
167
+ ```
168
+
169
+ It does not auto-correct values. If a new start date makes the current end invalid, the end input becomes invalid and the user decides what value replaces it.
170
+
171
+ ## 10. Keep the class small
172
+
173
+ If `DateCalendarElement` or `DatePickerElement` starts absorbing pure rules, move them out.
174
+
175
+ Expected homes:
176
+
177
+ - date arithmetic → `date.js`;
178
+ - locale parsing/formatting → `intl.js`;
179
+ - state transitions → `calendar-model.js`;
180
+ - payload normalization/fetch adapter → `source.js`;
181
+ - start/end relationship → `date-range.js`.
182
+
183
+ Do not add a plugin system to solve a one-off rendering need.
@@ -0,0 +1,63 @@
1
+ # Manual accessibility test pass
2
+
3
+ Automated Playwright covers interaction invariants; this checklist now distinguishes what the repo can re-check from what only a human with a real screen reader, keyboard or visual review can confirm.
4
+
5
+ Release decision (0.1.0): the human reader/OS pairings below are **deferred past 0.1** — the automated portion is the 0.1 gate, and the [Recommended manual test matrix](ACCESSIBILITY.md#recommended-manual-test-matrix) plus the Record table stay open until the passes are actually run and logged.
6
+
7
+ ## Legend
8
+
9
+ - `[auto]`: can be verified in-repo with Playwright and DOM assertions.
10
+ - `[manual]`: requires a human AT or visual pass.
11
+ - `[split]`: behavior can be checked automatically here, but announcement or perception still needs a human pass. Leave these unchecked until the manual portion is done.
12
+
13
+ ## Setup
14
+
15
+ - Engines: Chrome/Narrator (Windows), Firefox/NVDA, macOS/VoiceOver at least once.
16
+ - Contrast: run `color-contrast` review in DevTools on the demo plus a forced-colors spot check.
17
+ - Page under test: `demo/index.html` (uses `fr-BE` labels) and `test/fixtures/form-state.html`.
18
+
19
+ ## Keyboard only
20
+
21
+ - [x] [auto] Tab reaches the input, the popup button and the month/year selects as native controls.
22
+ - [x] [auto] Inside an open popup, Tab enters the grid on exactly one `tabindex="0"` cell.
23
+ - [x] [auto] Arrow keys move by day, ArrowUp/ArrowDown by week; no date is selected while moving.
24
+ - [x] [auto] Home/End move to the first/last cell of the week.
25
+ - [x] [auto] PageUp/PageDown move month; Shift+PageUp/PageDown move year; focus follows across a month boundary.
26
+ - [x] [auto] Enter/Space activates and, in `selection="single"`, selects.
27
+ - [ ] [split] Disabled days stay focusable via arrow keys but never select; reason is announced (behavior covered automatically; announcement still manual).
28
+ - [x] [auto] Escape in the picker closes the popup and restores focus to the input; in a `<dialog>`, the second Escape closes the dialog.
29
+ - [x] [split] Month/year selects and prev/next buttons work without a pointer (control behavior covered automatically; manual keyboard/AT pass still open).
30
+ - [x] [manual] Links/callback-seam cases (U8): all demo cards operate without a pointing device.
31
+
32
+ ## Screen reader
33
+
34
+ - [ ] [manual] The grid is announced as a table with full weekday names (`abbr` expands).
35
+ - [ ] [split] `aria-current="date"` marks today; `aria-selected` marks the selection (attributes present in DOM; announcement still manual).
36
+ - [ ] [split] Each cell's accessible name includes the full date and, when present, the `description` (ARIA label content is present in DOM; announcement still manual).
37
+ - [ ] [manual] Disabled cells announce "unavailable" distinctly.
38
+ - [ ] [split] The picker input announces its role (combobox), the popup trigger, and the format hint (attributes wired in DOM; announcement still manual).
39
+ - [ ] [split] The popup announces as a dialog with a label, and `aria-modal` stays absent (role/label/absence are wired in DOM; announcement still manual).
40
+ - [ ] [manual] The mini calendar + `calendar-view` demo announces both parts; activating a day announces the agenda navigation.
41
+ - [ ] [manual] Month/year navigation and source loading announcements are not clipped or repeated twice.
42
+
43
+ ## Form & lifecycle
44
+
45
+ - [x] [auto] Typing a valid localized date updates the field validation state (no stale invalid message).
46
+ - [x] [split] Typing an impossible date produces an announced invalid message (native invalid state/message covered automatically; announcement still manual).
47
+ - [x] [manual] Forced-colors mode keeps selected/today/disabled distinguishable — including a selected day when `Highlight` resolves near-white (no white-on-white from `--dp-accent`; spot-checked with the forced-colors screenshot bundle).
48
+
49
+ ## Automated pass
50
+
51
+ - Date: 2026-09-06.
52
+ - Engines: Chromium, Firefox, WebKit.
53
+ - Browser command run: `bun run test:browser:accessibility`.
54
+ - Result: 124 browser tests passed and 2 were skipped across the configured desktop projects; the forced-colors style assertion is Chromium-only.
55
+ - Forced-colors spot check: `bun run shot:forced-colors:important` reviewed manually after the selected-day forced-colors fix.
56
+
57
+ ## Record
58
+
59
+ Run once per supported screen reader pairing, then add the date, the pair (OS + reader + browser), and any failures as issues. Still open for 0.1.0.
60
+
61
+ | Date | OS / reader / browser | Result | Regression issues |
62
+ | --- | --- | --- | --- |
63
+ | | | ✅ / ❌ | |
@@ -0,0 +1,70 @@
1
+ # Integration with @lekoala/calendar
2
+
3
+ The mini calendar is intentionally a consumer of the date-picker package, not a feature duplicated inside the scheduling engine.
4
+
5
+ The live integration lives in `demo/index.html` (card 6): a real `<calendar-view view="month">` is wired to the mini calendar. `@lekoala/calendar` (and its `temporal-polyfill` peer) are dev dependencies used by the demo only.
6
+
7
+ `@lekoala/calendar` already exposes the right navigation seam:
8
+
9
+ ```js
10
+ calendar.gotoDate("2026-09-10");
11
+ ```
12
+
13
+ A minimal sidebar navigator therefore looks like this:
14
+
15
+ ```html
16
+ <date-calendar id="mini"
17
+ selection="none"
18
+ fixed-weeks
19
+ show-week-numbers></date-calendar>
20
+
21
+ <calendar-view id="agenda"></calendar-view>
22
+ ```
23
+
24
+ ```js
25
+ const mini = document.querySelector("#mini");
26
+ const agenda = document.querySelector("#agenda");
27
+
28
+ mini.display = agenda.date.toString().slice(0, 7);
29
+ mini.focusedDate = agenda.date.toString();
30
+
31
+ mini.addEventListener("dateactivate", (event) => {
32
+ agenda.gotoDate(event.detail.date);
33
+ });
34
+
35
+ agenda.addEventListener("calendar:datechange", (event) => {
36
+ const date = event.detail.date.toString();
37
+ mini.display = date.slice(0, 7);
38
+ mini.focusedDate = date;
39
+ });
40
+ ```
41
+
42
+ Notice what is missing: the mini calendar does not need a selected value. The agenda anchor is application/calendar state, not form selection state.
43
+
44
+ ## Active week / availability decoration
45
+
46
+ Application state can decorate the mini calendar without changing its navigation semantics:
47
+
48
+ ```js
49
+ mini.dateState = (date, sourceState) => ({
50
+ ...sourceState,
51
+ description: isBookable(date)
52
+ ? "Bookable; activate to navigate"
53
+ : "Closed; activate to navigate"
54
+ });
55
+
56
+ mini.renderDay = (date, state) => state.available ? "•" : "";
57
+ ```
58
+
59
+ A closed day can still navigate an administrative agenda. Do not set `disabled` merely because booking is unavailable unless activation itself is forbidden for this particular calendar.
60
+
61
+ That distinction mirrors the showcase rule: navigation policy and booking policy are different concerns.
62
+
63
+ ## Shared month math
64
+
65
+ Both packages expose month-week derivation. They stay **deliberately duplicated**, each owning its own contract:
66
+
67
+ - `@lekoala/calendar` owns Temporal-based math (`Temporal.PlainDate`).
68
+ - `@lekoala/date-picker` owns civil string math (`dates.getMonthWeeks()`, `YYYY-MM-DD`).
69
+
70
+ This keeps the two packages dependency-free from each other: a circular dependency between a picker and a scheduler would cost more than a few duplicated lines. `dates.getMonthWeeks()` keeps the scheduler's important contract (full civil weeks, configurable first day, 4–6 rows, no forced padding) so both stay interchangeable for the mini-calendar seam. A future shared `@lekoala/date-math` package is possible, but only as a separate proposal; it must not introduce a circular dependency.
@@ -0,0 +1,40 @@
1
+ # Decisions
2
+
3
+ Frozen contracts for the `0.1.0` surface. A change to a decision below is a breaking change and needs a proposal, not an opportunistic edit.
4
+
5
+ ## D1 — Calendar events are flat, verb-named
6
+
7
+ Never merged with the `value` concept (`dateactivate` is activation, not `value`).
8
+
9
+ - `date-calendar`: `dateactivate`, `datechange`, `dateinvalid`, `displaychange`, `dateloadstart`, `dateloadend`, `dateloaderror`.
10
+ - `date-picker`: `valuechange`, `open`, `close`.
11
+
12
+ No `dp:` namespace prefix on element events, even though `@lekoala/combobox` namespaces its events (`combobox:*`).
13
+
14
+ Rationale: the flat names describe a distinct concept (`dateactivate`), never collide with native input events, and are already wired through the docs, demos and tests. A namespace is not worth the churn for this package.
15
+
16
+ ## D2 — `selection="none"` is the final spelling
17
+
18
+ The navigation-only calendar keeps `selection="none"`.
19
+
20
+ `selection` remains a value attribute (`single` | `none`); it is how the mini calendar speaks the navigation contract: accepted activation dispatches `dateactivate`, `value` is untouched.
21
+
22
+ ## D3 — CSS: `--dp-*` is public, `.dp-*` is not
23
+
24
+ > `--dp-*` is the public styling namespace. `.dp-*` identifies component-owned DOM but is not a public styling API unless explicitly documented.
25
+
26
+ - The namespace prefix is `dp`, final. There are no `--lko-dp-*` or `.lko-dp-*` classes. A shared `--lko-*` token space is reserved for tokens actually shared between LeKoala packages, not as a per-component prefix.
27
+ - The `--dp-*` custom properties (background, foreground, accent, border, radius, day-size, gap, …) are the **public customization surface**. They are listed in `custom-elements.json` and are stable.
28
+ - Every `.dp-*` class is **internal DOM** and may change, move or disappear without a semver bump. This includes `.dp-day`, `.dp-grid`, `.dp-picker-panel` and the day-extra container.
29
+ - `renderDay()` is a public extension seam, but the name of the container its output is placed into is **not** promised. Consumers style their own returned nodes and assign their own class names.
30
+ - Demos, tests and internal code may reference `.dp-*` classes; their use there does not turn those classes into public API.
31
+
32
+ Context: DOM-backed styling hooks become API the moment they are documented — every internal rename then breaks consumers. We prefer to expose hooks (`--dp-*`, `renderDay()`, events) and keep the rendered anatomy free to evolve.
33
+
34
+ ## D4 — `<date-picker>` opens on focus; `open` is read-only
35
+
36
+ - The picker opens its popover when the input receives focus (`open-on-focus`, default `true`; set `open-on-focus="false"` to opt out).
37
+ - Opening on focus **never steals keyboard focus**: the user can start typing immediately. The grid takes focus only on ArrowDown, on the calendar trigger, or after the user moves the pointer into the popup.
38
+ - `show()` / `hide()` are the control surface. `open` is a **read-only getter** reflecting the popover. There is no `open` attribute: the popover element is the single machine of truth and a second state machine would only drift.
39
+ - `readonly` mirrors native semantics: the field is non-editable, the value is still submitted, and the picker cannot change it — the calendar trigger is disabled. `disabled` additionally unsubmits the field. `readonly` is never used to mean "no typing, but calendar selection allowed".
40
+ - Clearing the editable input clears the canonical value (the component renders no internal clear button). Form resets follow the native `defaultValue` contract. This matches the Open UI datepicker `clear` concept: clearing is a text-editing action, not a component-owned affordance.
@@ -0,0 +1,92 @@
1
+ # Flatpickr / Formidable migration map
2
+
3
+ This prototype was scoped against the existing `FlatpickrInput` wrapper rather than against Flatpickr's full feature list.
4
+
5
+ ## Directly covered
6
+
7
+ | Existing need | New direction |
8
+ |---------------------------|------------------------------------------------------------|
9
+ | canonical `Y-m-d` value | `date-picker.value` / hidden form value `YYYY-MM-DD` |
10
+ | localized pretty display | `Intl` numeric adapter while staying editable |
11
+ | `minDate` / `maxDate` | `min` / `max` |
12
+ | disable callback | `isDateDisabled(date)` |
13
+ | dynamic enable/disable | source state + `dateState()` |
14
+ | `configUrl` on navigation | abortable `source.load({start,end}, {signal})` |
15
+ | bars/events on dates | source metadata + `renderDay()` + accessible `description` |
16
+ | inline calendar | `<date-calendar>` |
17
+ | previous/next month | calendar methods/buttons |
18
+ | direct month/year choice | native month/year selects |
19
+ | start/end relation | `linkDateRange(start, end)` |
20
+ | locale | `Intl` locale + small control-message dictionaries |
21
+ | value-changed hook | `valuechange` / native input+change events |
22
+
23
+ ## The display-format change is intentional
24
+
25
+ The old wrapper has to choose between a rich localized display and manual editing because Flatpickr cannot reliably parse arbitrary long localized output.
26
+
27
+ The new default is deliberately less decorative and more useful:
28
+
29
+ ```text
30
+ fr-BE: 06/09/2026
31
+ en-US: 09/06/2026
32
+ ```
33
+
34
+ The pattern is generated with `Intl.DateTimeFormat(...).formatToParts()`, so the same adapter can format and parse it.
35
+
36
+ Long dates such as `6 septembre 2026` remain presentation content, not the editing format of the field.
37
+
38
+ ## Dynamic source payload
39
+
40
+ Legacy payload:
41
+
42
+ ```json
43
+ {
44
+ "enable": ["2025-06-07"],
45
+ "events": [
46
+ "2025-06-07",
47
+ { "date": "2025-06-09", "class": "bg-danger" }
48
+ ]
49
+ }
50
+ ```
51
+
52
+ Suggested new payload:
53
+
54
+ ```json
55
+ {
56
+ "dates": {
57
+ "2025-06-07": {
58
+ "disabled": false,
59
+ "description": "Available"
60
+ },
61
+ "2025-06-09": {
62
+ "description": "Limited availability",
63
+ "status": "danger"
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ The backend returns meaning, not CSS class names. The consumer decides how `status` is rendered.
70
+
71
+ ## Explicit enable-overrides-disable
72
+
73
+ If an application currently relies on a remote `enable` list overriding a generic disabled rule (for example: weekends disabled except one exceptional Saturday), implement that policy in one `dateState` resolver:
74
+
75
+ ```js
76
+ calendar.isDateDisabled = (date) => isWeekend(date);
77
+ // Source state { enabled: true } is an explicit exception to the generic
78
+ // weekend rule. Hard min/max bounds still win.
79
+ ```
80
+
81
+ The prototype promotes this legacy behavior into a documented generic field: `enabled: true` reopens a date disabled by `isDateDisabled()` or ordinary source state, but never bypasses hard `min`/`max` bounds.
82
+
83
+ ## Not migrated into this component
84
+
85
+ The existing wrapper also exposes Flatpickr features that should not be carried into the date picker core:
86
+
87
+ - `enableTime`;
88
+ - `noCalendar` / time-only;
89
+ - confirm-date plugin;
90
+ - month-as-value selection plugin.
91
+
92
+ Those are separate controls/use cases. Adding them here would immediately mix civil-date selection with time and alternate value models.
@@ -0,0 +1,105 @@
1
+ # Roadmap
2
+
3
+ ## P0 — prove the contracts
4
+
5
+ - [x] canonical civil-date helpers;
6
+ - [x] true month weeks;
7
+ - [x] `value / display / focused` split;
8
+ - [x] inline `<date-calendar>`;
9
+ - [x] navigation-only selection mode;
10
+ - [x] month/year controls;
11
+ - [x] APG-style grid keyboard interaction;
12
+ - [x] localized editable adapter;
13
+ - [x] `<date-picker>` Popover + floating composition;
14
+ - [x] source contract with AbortController;
15
+ - [x] day state + safe decoration seam;
16
+ - [x] start/end linking primitive;
17
+ - [x] unit/browser test skeleton;
18
+ - [x] demos and architecture docs.
19
+
20
+ ## P1 — hardened before 0.1 (shipped in 0.1.0)
21
+
22
+ - [x] run TypeScript 7/Biome/Bun checks in the actual repo;
23
+ - [x] run Playwright on all engines;
24
+ - [x] manual AT checklist and record ([docs/AT-CHECKLIST.md](AT-CHECKLIST.md)); the automated portion is the 0.1 gate, the human reader/OS rows track as a post-0.1 recommendation;
25
+ - [x] add a real `@lekoala/calendar` mini-calendar integration demo;
26
+ - [x] validate Popover behavior inside `<dialog>` and nested app surfaces;
27
+ - [x] test form reset, disabled toggles and dynamic name/form ownership;
28
+ - [x] decide exact event naming — flat `dateactivate` etc. ([DECISIONS.md](DECISIONS.md), D1);
29
+ - [x] decide whether `selection="none"` is the final public spelling — yes (D2);
30
+ - [x] decide public styling tokens/class names — `--dp-*` public, `.dp-*` internal (D3);
31
+ - [x] decide the shared-range picker shape — `<date-picker range>` + `linkDateRange` (U9/U5);
32
+ - [x] decide the popup modality on touch surfaces — stays non-modal in 0.1 ([ACCESSIBILITY.md](ACCESSIBILITY.md));
33
+ - [x] add custom-elements manifest once names stabilize — `custom-elements.json`;
34
+ - [x] add package/generated-artifact checks like `@lekoala/combobox`;
35
+ - [x] commit `bun.lock` and freeze installs in CI.
36
+
37
+ ## P2 — real-world migration
38
+
39
+ Use Formidable/Flatpickr cases as migration fixtures:
40
+
41
+ - remote enable/disable payload;
42
+ - date markers/events → metadata/decoration;
43
+ - French/Spanish locale → shipped locale message dictionaries (`ar`, `de`, `en`, `es`, `fr`, `hi`, `id`, `it`, `ja`, `ko`, `nl`, `pl`, `pt-BR`, `pt-PT`, `ru`, `tr`, `zh-CN`);
44
+ - initial canonical values;
45
+ - inline calendar;
46
+ - linked start/end fields;
47
+ - month navigation source refresh.
48
+
49
+ ## Explicitly deferred
50
+
51
+ These should require a separate proposal, not opportunistic growth:
52
+
53
+ - time picker / datetime picker;
54
+ - single-grid visual **selection** interaction (the band itself is core presentation via `calendar.highlightedRange`);
55
+ - month-as-value picker;
56
+ - multi-date;
57
+ - recurrence;
58
+ - timezone-aware values;
59
+ - natural-language parsing;
60
+ - plugin registry;
61
+ - multiple visible months;
62
+ - swipe gestures;
63
+ - full calendar/scheduler features.
64
+
65
+ ## Open UI nomenclature reference
66
+
67
+ Cross-check our coverage against the Open UI datepicker research: <https://open-ui.org/components/datepicker.research/> — a cross-design-system vocabulary inventory for date pickers (2023, no longer maintained). It is a term checklist, not a behavior specification; normative behavior stays with the WAI-ARIA APG grid/dialog patterns and the HTML spec. Use these terms as-is in proposals instead of re-defining them.
68
+
69
+ | Open UI concept | Status | Note |
70
+ | --- | --- | --- |
71
+ | `open` | core | `show()` / `hide()` with a read-only `open` getter (D4). |
72
+ | `navigation` | core | month/year selects plus prev/next buttons (P0). |
73
+ | `selected` | core | `aria-selected` mapped from `value`. |
74
+ | `disabled` | core | native disabled state on the component and its controls. |
75
+ | `disabled dates` | core | `min` / `max`, `isDateDisabled`, source state (P0). |
76
+ | `required` | core | forwarded to the visible native input. |
77
+ | `manual input` | core | editable native field with localized parsing (U1). |
78
+ | `format` | core | `intl.js`; format hint exposed via `aria-describedby`. |
79
+ | `error` | core | distinct `setCustomValidity` messages for invalid vs unavailable dates. |
80
+ | `readonly` / `disabled` | core | native semantics kept separate (D4). |
81
+ | `default` | core | form reset via `defaultValue` (D4). |
82
+ | `clear` | contract boundary | no internal clear button; clearing happens by editing the text input (D4). |
83
+ | `custom cells` | contract boundary | `renderDay()` only; cells stay component-owned (D3). |
84
+ | `weeknumber` | contract boundary | `show-week-numbers` (U3). |
85
+ | `custom weeks` | no | only the `first-day` attribute; no custom week rendering. |
86
+ | `range` | core | `<date-picker range>` two-field shared surface (U9); `linkDateRange()` stays external (U5). |
87
+ | `range selected` | deferred | selection interaction needs its own proposal; `highlightedRange` covers band presentation only. |
88
+ | `range presets` | deferred | application-owned. |
89
+ | `date-time picker` | deferred | time/datetime is explicitly out of scope. |
90
+ | `side by side` | deferred | maps to "multiple visible months". |
91
+ | `footer` | deferred | not part of the grid contract. |
92
+ | `shape`, `size`, `compact`, `subtle`, `light`, `borderless`, `no icon` | app-owned | styling via public `--dp-*` tokens (D3), not a component API. |
93
+
94
+ ## Related prior art: Duet Date Picker
95
+
96
+ <https://github.com/duetds/date-picker> — a Web Components date picker by the Duet Design System (MIT, Stencil, ~10 kb). The repository was **archived in 2024**; treat it as a source of observed behaviors, not maintained API reference. It validates several of our choices and highlights points we must decide:
97
+
98
+ - `isDateDisabled` (same seam name as ours), hidden-input ISO submission, `formatLongDate`-style cell labels ("17 November 2020"), month/year announced on change, and a real `table[role="grid"]` with a single roving tab stop — all mirrored in this package.
99
+ - **Differs from us on the canonical model**: its `dateAdapter` and events work with `Date` objects (`valueAsDate`). That is the exact `Date`-object tradeoff our canonical `YYYY-MM-DD` strings avoid.
100
+ - **Differs on modality**: it ships a modal dialog with overlay and an explicit close button, while our popup intentionally stays non-modal (`aria-modal` absent). `docs/ACCESSIBILITY.md` still holds the open question of going modal on small/touch surfaces.
101
+ - **Tab wrap-around**: Duet cycles focus back to the first focusable element inside the dialog. Our non-modal popup lets Tab leave the picker — decided, documented in `docs/ACCESSIBILITY.md`.
102
+ - **Opening focus target**: Duet moves focus to the first (month) select on open; we focus the grid — a deliberate divergence (ArrowDown / trigger button enter the grid).
103
+ - **Deferred alignments**: Duet's touch gestures and modified mobile interface map to our explicit "swipe gestures" deferral.
104
+
105
+ Resolved without further work: popover flipping near an edge is handled by `@lekoala/floating`, so no `direction`-style prop is needed; the open-popover `Tab` contract is decide-and-document only (see above), not code.
@@ -0,0 +1,73 @@
1
+ # Testing
2
+
3
+ ## Fast loop
4
+
5
+ ```bash
6
+ bun run test:syntax
7
+ bun run typecheck
8
+ bun run test
9
+ ```
10
+
11
+ Pure date math, locale parsing and model transitions belong in `test/unit` and must remain DOM-free.
12
+
13
+ ## Browser loop
14
+
15
+ ```bash
16
+ bun run test:browser
17
+ ```
18
+
19
+ Chromium covers the fast interaction loop. Before release:
20
+
21
+ ```bash
22
+ bun run test:browser:all
23
+ ```
24
+
25
+ runs Chromium, Firefox and WebKit.
26
+
27
+ For the accessibility-adjacent browser subset used by the AT checklist:
28
+
29
+ ```bash
30
+ bun run test:browser:accessibility
31
+ ```
32
+
33
+ This runs the focused picker/calendar/focus/form/surface specs across the configured desktop projects, including the forced-colors regression check in `calendar.spec.js`.
34
+ The forced-colors style assertion follows the Chromium capture pipeline, so it is intentionally skipped on Firefox and WebKit.
35
+
36
+ ## What belongs in browser tests
37
+
38
+ Browser tests should cover behavior that cannot be trusted to a DOM shim:
39
+
40
+ - Popover open/close;
41
+ - focus movement and roving tabindex;
42
+ - real keyboard events;
43
+ - month/year selects;
44
+ - form submission shape;
45
+ - floating placement staying in the viewport;
46
+ - outside pointer close;
47
+ - source cancellation;
48
+ - range validation;
49
+ - forced-colors/zoom smoke cases.
50
+
51
+ ## Core invariants
52
+
53
+ Keep explicit tests for these regressions:
54
+
55
+ 1. `value`, `display` and `focusedDate` are independent.
56
+ 2. Month/year navigation never selects.
57
+ 3. Arrow navigation never selects.
58
+ 4. `dateactivate` is the only user activation seam.
59
+ 5. `selection="none"` never mutates `value`.
60
+ 6. `getMonthWeeks()` never pads.
61
+ 7. `fixed-weeks` renders 42 day cells.
62
+ 8. Outside-month cells can activate.
63
+ 9. Source navigation aborts stale loads.
64
+ 10. Typed and clicked dates resolve the same disabled state.
65
+ 11. Start/end linkage never silently changes sibling values.
66
+ 12. Exactly one grid cell has `tabindex="0"`.
67
+ 13. A failed or cancelled source load never authorizes activation or validation.
68
+
69
+ ## CI
70
+
71
+ The included workflow uses Bun 1.4.2 and runs static checks/unit tests before Playwright.
72
+
73
+ `bun.lock` is committed. Keep installs frozen in CI (`bun install --frozen-lockfile`) so dependency resolution stays deterministic. Regenerate the lock with `bun install` when the manifest changes and commit it in the same change.