@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,128 @@
1
+ # Use cases
2
+
3
+ These cases define the product more strongly than a feature checklist. A proposed API change should be tested against them before being accepted.
4
+
5
+ ## U1 — Editable localized date field
6
+
7
+ A user can type `06/09/2026` in `fr-BE`, open the calendar, select the same date visually, and submit `2026-09-06` to the server.
8
+
9
+ Acceptance:
10
+
11
+ - input remains editable;
12
+ - label still targets the real input;
13
+ - required/custom validity still works;
14
+ - display formatting does not leak into submitted data.
15
+
16
+ ## U2 — Inline selector
17
+
18
+ An inline calendar owns one selected date.
19
+
20
+ Acceptance:
21
+
22
+ - month/year navigation does not change the value;
23
+ - arrow-key focus does not change the value;
24
+ - click/Enter/Space activates and selects;
25
+ - today, focused and selected are visually/semantically distinct.
26
+
27
+ ## U3 — Mini calendar navigator
28
+
29
+ An inline `selection="none"` calendar navigates a separate agenda/calendar.
30
+
31
+ Acceptance:
32
+
33
+ - there may be no selected value at all;
34
+ - changing month/year only changes what the mini calendar displays;
35
+ - activating a date dispatches `dateactivate` and the application calls `calendarView.gotoDate(date)`;
36
+ - outside-month days remain activatable;
37
+ - six-row height is available as presentation, not baked into month math;
38
+ - ISO week numbers are available.
39
+
40
+ ## U4 — Constrained dates
41
+
42
+ Example: only the next 90 days, weekdays only.
43
+
44
+ Acceptance:
45
+
46
+ - min/max affect both activation and typed validation;
47
+ - custom disabled days can still receive keyboard focus so the reason can be announced;
48
+ - disabled activation never changes the value;
49
+ - previous/next controls stop at bounded months.
50
+
51
+ ## U5 — Linked start/end dates
52
+
53
+ Two fields represent a range.
54
+
55
+ Acceptance:
56
+
57
+ - end cannot validate before start;
58
+ - start cannot validate after end;
59
+ - changing one side revalidates the other;
60
+ - the component never silently rewrites the sibling value;
61
+ - each picker remains independently usable and replaceable.
62
+
63
+ ## U6 — Remote availability
64
+
65
+ Changing the displayed month loads per-day state from a backend.
66
+
67
+ Acceptance:
68
+
69
+ - load receives the actual rendered range;
70
+ - superseded navigation aborts the previous request;
71
+ - state can disable a day or describe it;
72
+ - loading logic is independent of URL/backend format;
73
+ - a typed date can call `ensureDate()` before validation.
74
+
75
+ ## U7 — Morning / afternoon indicators
76
+
77
+ A date can carry richer state such as morning/afternoon availability.
78
+
79
+ Acceptance:
80
+
81
+ - the business fields remain opaque to core logic;
82
+ - visual badges/dots live inside the owned day cell;
83
+ - accessible meaning is supplied through `description`;
84
+ - custom rendering cannot replace focus/ARIA semantics.
85
+
86
+ ## U8 — Keyboard-only operation
87
+
88
+ Everything above works without a pointing device.
89
+
90
+ Acceptance:
91
+
92
+ - one grid cell in the Tab sequence;
93
+ - arrows, Home/End, PageUp/PageDown, Shift+PageUp/PageDown work;
94
+ - changing display follows keyboard focus when focus crosses a month;
95
+ - activation requires Enter/Space;
96
+ - Escape closes the picker and restores a sensible focus target;
97
+ - month/year selects remain normal native controls.
98
+
99
+ ## U9 — Two fields, one shared calendar
100
+
101
+ A form has two editable date fields (arrival/departure) that share a single calendar surface inside one picker popup.
102
+
103
+ Acceptance:
104
+
105
+ - each field stays independently editable and localized, with its own hidden ISO value for submission;
106
+ - opening from a bound targets that bound: selecting a first date commits it, moves the active endpoint to the other bound without closing, and a second selection commits and closes;
107
+ - once an ordered range is complete, activating before it extends the start and activating after it extends the end, regardless of the opening field; the opposite bound stays unchanged and readonly/disabled bounds remain protected;
108
+ - a preloaded range shows the band on open and is never reordered;
109
+ - month/year navigation and arrow-key focus never mutate either bound;
110
+ - an inverted range (start after end) keeps both entered values and surfaces an order error on the bound that was just modified; changing either side revalidates both;
111
+ - form reset restores both fields in one resync;
112
+ - a stale async availability response arriving after the active endpoint changed never commits a value or closes the popup;
113
+ - only the bounds are validated; per-night availability stays application-owned.
114
+
115
+ ## Not a use case for this package
116
+
117
+ Do not grow the API to cover these without a separate design:
118
+
119
+ - time slots;
120
+ - duration picking;
121
+ - timezone conversion;
122
+ - appointment recurrence;
123
+ - resource scheduling;
124
+ - drag/drop;
125
+ - event rendering;
126
+ - multi-day scheduler views.
127
+
128
+ Those belong to `@lekoala/calendar` or application logic.
package/package.json ADDED
@@ -0,0 +1,174 @@
1
+ {
2
+ "name": "@lekoala/date-picker",
3
+ "version": "0.1.0",
4
+ "description": "Native-first date picker and inline calendar primitives",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "./src/define.js",
8
+ "./src/date-picker.css",
9
+ "./dist/date-picker.js",
10
+ "./dist/date-picker.min.js",
11
+ "./dist/date-picker.standalone.min.js",
12
+ "./dist/date-picker.css",
13
+ "./dist/date-picker.min.css"
14
+ ],
15
+ "license": "MIT",
16
+ "customElements": "custom-elements.json",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/lekoala/date-picker.git"
20
+ },
21
+ "homepage": "https://github.com/lekoala/date-picker",
22
+ "bugs": "https://github.com/lekoala/date-picker/issues",
23
+ "packageManager": "bun@1.4.2",
24
+ "keywords": [
25
+ "date-picker",
26
+ "calendar",
27
+ "datepicker",
28
+ "popover",
29
+ "web-component",
30
+ "accessibility"
31
+ ],
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "docs",
36
+ "custom-elements.json",
37
+ "LICENSE",
38
+ "README.md"
39
+ ],
40
+ "exports": {
41
+ ".": {
42
+ "types": "./dist/types/index.d.ts",
43
+ "import": "./src/index.js"
44
+ },
45
+ "./define": {
46
+ "types": "./dist/types/define.d.ts",
47
+ "import": "./src/define.js"
48
+ },
49
+ "./calendar": {
50
+ "types": "./dist/types/date-calendar.d.ts",
51
+ "import": "./src/date-calendar.js"
52
+ },
53
+ "./date-picker.css": "./src/date-picker.css",
54
+ "./locales/ar": {
55
+ "types": "./dist/types/locales/ar.d.ts",
56
+ "import": "./src/locales/ar.js"
57
+ },
58
+ "./locales/de": {
59
+ "types": "./dist/types/locales/de.d.ts",
60
+ "import": "./src/locales/de.js"
61
+ },
62
+ "./locales/en": {
63
+ "types": "./dist/types/locales/en.d.ts",
64
+ "import": "./src/locales/en.js"
65
+ },
66
+ "./locales/es": {
67
+ "types": "./dist/types/locales/es.d.ts",
68
+ "import": "./src/locales/es.js"
69
+ },
70
+ "./locales/fr": {
71
+ "types": "./dist/types/locales/fr.d.ts",
72
+ "import": "./src/locales/fr.js"
73
+ },
74
+ "./locales/hi": {
75
+ "types": "./dist/types/locales/hi.d.ts",
76
+ "import": "./src/locales/hi.js"
77
+ },
78
+ "./locales/id": {
79
+ "types": "./dist/types/locales/id.d.ts",
80
+ "import": "./src/locales/id.js"
81
+ },
82
+ "./locales/it": {
83
+ "types": "./dist/types/locales/it.d.ts",
84
+ "import": "./src/locales/it.js"
85
+ },
86
+ "./locales/ja": {
87
+ "types": "./dist/types/locales/ja.d.ts",
88
+ "import": "./src/locales/ja.js"
89
+ },
90
+ "./locales/ko": {
91
+ "types": "./dist/types/locales/ko.d.ts",
92
+ "import": "./src/locales/ko.js"
93
+ },
94
+ "./locales/nl": {
95
+ "types": "./dist/types/locales/nl.d.ts",
96
+ "import": "./src/locales/nl.js"
97
+ },
98
+ "./locales/pl": {
99
+ "types": "./dist/types/locales/pl.d.ts",
100
+ "import": "./src/locales/pl.js"
101
+ },
102
+ "./locales/pt-BR": {
103
+ "types": "./dist/types/locales/pt-BR.d.ts",
104
+ "import": "./src/locales/pt-BR.js"
105
+ },
106
+ "./locales/pt-PT": {
107
+ "types": "./dist/types/locales/pt-PT.d.ts",
108
+ "import": "./src/locales/pt-PT.js"
109
+ },
110
+ "./locales/ru": {
111
+ "types": "./dist/types/locales/ru.d.ts",
112
+ "import": "./src/locales/ru.js"
113
+ },
114
+ "./locales/tr": {
115
+ "types": "./dist/types/locales/tr.d.ts",
116
+ "import": "./src/locales/tr.js"
117
+ },
118
+ "./locales/zh-CN": {
119
+ "types": "./dist/types/locales/zh-CN.d.ts",
120
+ "import": "./src/locales/zh-CN.js"
121
+ },
122
+ "./standalone": "./dist/date-picker.standalone.min.js",
123
+ "./dist/date-picker.js": "./dist/date-picker.js",
124
+ "./dist/date-picker.min.js": "./dist/date-picker.min.js",
125
+ "./dist/date-picker.standalone.min.js": "./dist/date-picker.standalone.min.js",
126
+ "./dist/date-picker.css": "./dist/date-picker.css",
127
+ "./dist/date-picker.min.css": "./dist/date-picker.min.css"
128
+ },
129
+ "publishConfig": {
130
+ "access": "public"
131
+ },
132
+ "scripts": {
133
+ "dev": "node test/server.js",
134
+ "dev:kill": "node scripts/kill-port.mjs",
135
+ "dev:restart": "node scripts/kill-port.mjs && node test/server.js",
136
+ "build:bundle": "bun scripts/build.js",
137
+ "build:types": "bun run types",
138
+ "build:manifest": "bun scripts/custom-elements.js",
139
+ "build": "bun run build:bundle && bun run build:types && bun run build:manifest",
140
+ "sync": "bun run build",
141
+ "lint": "biome check .",
142
+ "format": "biome format --write .",
143
+ "typecheck": "tsc -p jsconfig.json",
144
+ "types": "tsc -p tsconfig.types.json",
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",
147
+ "test:types": "tsc -p test/types/tsconfig.json",
148
+ "test:browser": "playwright test browser --project chromium",
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",
150
+ "test:browser:headed": "playwright test browser --project chromium --headed",
151
+ "test:browser:all": "playwright test browser",
152
+ "test:dist": "playwright test dist --project chromium",
153
+ "shot:forced-colors": "bun scripts/forced-colors-shot.js",
154
+ "shot:forced-colors:important": "bun scripts/forced-colors-shot.js /demo/index.html --scenario important --out-dir tmp/forced-colors-important",
155
+ "shot:forced-colors:important:dark": "bun scripts/forced-colors-shot.js /demo/index.html --scheme dark --scenario important --out-dir tmp/forced-colors-important-dark",
156
+ "check": "bun run test:syntax && bun run lint && bun run typecheck && bun run test",
157
+ "verify": "bun run check && bun run sync && bun run test:types && bun run check:generated && bun run check:package",
158
+ "check:all": "bun run verify && bun run test:browser:all && bun run test:dist",
159
+ "check:generated": "bun scripts/check-generated.js",
160
+ "check:package": "bun scripts/check-package.js",
161
+ "prepack": "bun run sync",
162
+ "prepublishOnly": "bun run verify"
163
+ },
164
+ "devDependencies": {
165
+ "@biomejs/biome": "2.5.11",
166
+ "@lekoala/calendar": "0.1.0",
167
+ "@playwright/test": "1.62.1",
168
+ "temporal-polyfill": "^1.0.4",
169
+ "typescript": "7.0.2"
170
+ },
171
+ "dependencies": {
172
+ "@lekoala/floating": "^0.1.1"
173
+ }
174
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ addDays,
3
+ addMonths,
4
+ addYears,
5
+ endOfWeek,
6
+ isDate,
7
+ monthKey,
8
+ shiftMonth,
9
+ startOfWeek,
10
+ todayISO,
11
+ } from "./date.js";
12
+
13
+ export class CalendarModel {
14
+ /**
15
+ * @param {{value?:string, display?:string, focused?:string, firstDay?:number}} [options]
16
+ */
17
+ constructor(options = {}) {
18
+ const today = todayISO();
19
+ this.firstDay = options.firstDay ?? 1;
20
+ this.value = options.value && isDate(options.value) ? options.value : "";
21
+ this.focused = options.focused && isDate(options.focused) ? options.focused : this.value || today;
22
+ this.display = options.display ? monthKey(options.display) : monthKey(this.focused);
23
+ }
24
+
25
+ /** @param {string} value */
26
+ setValue(value) {
27
+ if (value && !isDate(value)) throw new TypeError(`Invalid value: ${value}`);
28
+ this.value = value || "";
29
+ return this.value;
30
+ }
31
+
32
+ /** @param {string} value */
33
+ setDisplay(value) {
34
+ const target = monthKey(value);
35
+ const day = Number(this.focused.slice(8, 10)) || 1;
36
+ const next = addMonths(`${target}-01`, 0);
37
+ const candidate = addDays(next, Math.max(0, day - 1));
38
+ this.focused = monthKey(candidate) === target ? candidate : addDays(`${shiftMonth(target, 1)}-01`, -1);
39
+ this.display = target;
40
+ return this.display;
41
+ }
42
+
43
+ /** @param {string} value @param {{follow?:boolean}} [options] */
44
+ setFocused(value, options = {}) {
45
+ if (!isDate(value)) throw new TypeError(`Invalid focused date: ${value}`);
46
+ this.focused = value;
47
+ if (options.follow !== false) this.display = monthKey(value);
48
+ return this.focused;
49
+ }
50
+
51
+ /** @param {number} amount */
52
+ moveFocusDays(amount) {
53
+ return this.setFocused(addDays(this.focused, amount));
54
+ }
55
+
56
+ /** @param {number} amount */
57
+ moveFocusMonths(amount) {
58
+ return this.setFocused(addMonths(this.focused, amount));
59
+ }
60
+
61
+ /** @param {number} amount */
62
+ moveFocusYears(amount) {
63
+ return this.setFocused(addYears(this.focused, amount));
64
+ }
65
+
66
+ focusWeekStart() {
67
+ return this.setFocused(startOfWeek(this.focused, this.firstDay));
68
+ }
69
+
70
+ focusWeekEnd() {
71
+ return this.setFocused(endOfWeek(this.focused, this.firstDay));
72
+ }
73
+
74
+ /** Header/month-select navigation. Value is intentionally untouched. @param {number} amount */
75
+ navigateMonth(amount) {
76
+ const target = shiftMonth(this.display, amount);
77
+ this.setDisplay(target);
78
+ return this.display;
79
+ }
80
+ }