@agencecinq/calendar 1.0.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 ADDED
@@ -0,0 +1,187 @@
1
+ # @agencecinq/calendar
2
+
3
+ [![](https://img.shields.io/npm/v/@agencecinq/calendar)](https://www.npmjs.com/package/@agencecinq/calendar)
4
+
5
+ > Accessible date / range picker as a lightweight Web Component (`<cinq-calendar>`).
6
+
7
+ Markup and CSS are yours — the package fills the grid and handles selection.
8
+ Aligned with the
9
+ [WAI-ARIA Authoring Practices date picker grid](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/).
10
+
11
+ Inspired by [`@19h47/calendar`](https://github.com/19h47/19h47-calendar).
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pnpm add @agencecinq/calendar
17
+ ```
18
+
19
+ ## Markup
20
+
21
+ Required hooks:
22
+
23
+ | Selector | Role |
24
+ | -------- | ---- |
25
+ | `<cinq-calendar>` | Host element |
26
+ | `.js-previous` / `.js-next` | Month navigation (optional) |
27
+ | `.js-title` | Month / year label; click advances to the next month (same as `.js-next`) |
28
+ | `.js-days` | Weekday headers row |
29
+ | `.js-body` | Day cells |
30
+ | `.js-day` | Day button (generated) |
31
+
32
+ ```html
33
+ <cinq-calendar locale="fr" deselect>
34
+ <header>
35
+ <button type="button" class="js-previous" aria-label="Previous month">Previous</button>
36
+ <button type="button" class="js-title" id="calendar-label" aria-live="polite"></button>
37
+ <button type="button" class="js-next" aria-label="Next month">Next</button>
38
+ </header>
39
+ <table role="grid" aria-labelledby="calendar-label">
40
+ <thead>
41
+ <tr class="js-days"></tr>
42
+ </thead>
43
+ <tbody class="js-body"></tbody>
44
+ </table>
45
+ </cinq-calendar>
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```js
51
+ import "@agencecinq/calendar";
52
+ import { EVENTS } from "@agencecinq/utils";
53
+
54
+ const el = document.querySelector("cinq-calendar");
55
+
56
+ el.addEventListener(EVENTS.CALENDAR_CHANGE, ({ detail }) => {
57
+ // detail.values → string[] (`YYYY-MM-DD`)
58
+ console.log(detail.values);
59
+ });
60
+ ```
61
+
62
+ Importing the package registers the custom element. The calendar mounts on
63
+ `connectedCallback` — no manual `init()` is required.
64
+
65
+ > **HTML is the source of truth.** Provide `role="grid"`, `aria-labelledby`,
66
+ > `aria-live` on the title, and `aria-label` on nav buttons yourself.
67
+
68
+ ### Single date
69
+
70
+ ```html
71
+ <cinq-calendar single deselect>…</cinq-calendar>
72
+ ```
73
+
74
+ ### Date range
75
+
76
+ ```html
77
+ <cinq-calendar single="false">…</cinq-calendar>
78
+ ```
79
+
80
+ ### Locale & week start
81
+
82
+ `locale` drives labels via `Intl`. Week start defaults from the locale
83
+ (`weekInfo`); pass `first-day` to override.
84
+
85
+ ```html
86
+ <cinq-calendar locale="fr">…</cinq-calendar>
87
+ <!-- fr → week starts Monday -->
88
+
89
+ <cinq-calendar locale="en" first-day="1">…</cinq-calendar>
90
+ <!-- force Monday despite en -->
91
+ ```
92
+
93
+ Update at runtime:
94
+
95
+ ```js
96
+ import { getWeekStart } from "@agencecinq/calendar";
97
+
98
+ el.options.locale = "ja";
99
+ el.options.firstDay = getWeekStart("ja");
100
+ el.render();
101
+ ```
102
+
103
+ ### Custom labels
104
+
105
+ ```js
106
+ el.options.days = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"];
107
+ el.options.months = [
108
+ "Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
109
+ "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre",
110
+ ];
111
+ el.render();
112
+ ```
113
+
114
+ ### Cell hook
115
+
116
+ Override `renderInner` to enrich or rebuild each day cell. Keep `.js-day` and
117
+ `data-day`.
118
+
119
+ ```js
120
+ el.renderInner = (inner, date) => {
121
+ const button = inner.querySelector(".js-day");
122
+ if (!button) return;
123
+
124
+ button.classList.add("MyDay");
125
+ button.innerHTML = `<time datetime="${date.toISOString().slice(0, 10)}">${date.getDate()}</time>`;
126
+ };
127
+ ```
128
+
129
+ ## Attributes
130
+
131
+ | Attribute | Description |
132
+ | --------- | ----------- |
133
+ | `locale` / `data-locale` | BCP 47 locale for title and weekday/month labels |
134
+ | `single` | Single date (`true`, default) vs range (`false`) |
135
+ | `deselect` | Allow clearing the selection in single mode |
136
+ | `allow-past` | Allow selecting past dates |
137
+ | `first-day` | Week start (`0`=Sun … `6`=Sat); defaults from locale |
138
+ | `button-class` | Extra classes on day buttons |
139
+ | `name` | Forwarded in `calendar:change` detail |
140
+ | `data-month` | Initial month (`0`–`11`) |
141
+ | `data-year` | Initial year |
142
+ | `data-picked-dates` | JSON array of `YYYY-MM-DD` days to preselect |
143
+
144
+ ## Keyboard (focus in the grid)
145
+
146
+ | Key | Action |
147
+ | --- | ------ |
148
+ | Arrow keys | Move by day / week (crosses months) |
149
+ | Home / End | First / last day of the week |
150
+ | Page Up / Page Down | Previous / next month |
151
+ | Shift + Page Up / Down | Previous / next year |
152
+ | Enter / Space | Select the focused day |
153
+
154
+ ## Events
155
+
156
+ Dispatched on the host `<cinq-calendar>` (bubble). Constants live on
157
+ `@agencecinq/utils` `EVENTS`:
158
+
159
+ | Event | Constant | Detail |
160
+ | ----- | -------- | ------ |
161
+ | `calendar:change` | `CALENDAR_CHANGE` | `{ values: string[], name?: string }` |
162
+
163
+ ## API
164
+
165
+ | Method / property | Description |
166
+ | ----------------- | ----------- |
167
+ | `options` | Runtime options object |
168
+ | `picked` | Selected days as `YYYY-MM-DD` |
169
+ | `current` | Viewed `{ month, year, day }` |
170
+ | `render()` | Rebuild the grid |
171
+ | `move(deltaMonths)` | Navigate by months |
172
+ | `destroy()` | Detach listeners and clear the grid |
173
+ | `renderInner(inner, date)` | Per-cell hook (override) |
174
+
175
+ Exported helpers: `getWeekStart`, `toDayString`, `fromDayString`.
176
+
177
+ ## Build setup
178
+
179
+ ```bash
180
+ pnpm -C packages/calendar build
181
+ ```
182
+
183
+ ## Acknowledgments
184
+
185
+ - [`@19h47/calendar`](https://github.com/19h47/19h47-calendar) — original implementation
186
+ - [WAI-ARIA APG — Date Picker Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/)
187
+ - Litepicker
@@ -0,0 +1,51 @@
1
+ import { Current, Options } from './types.js';
2
+ /**
3
+ * Date / range picker Web Component.
4
+ *
5
+ * Markup and CSS are yours — the package fills the grid and handles selection.
6
+ * Follows the WAI-ARIA APG date grid practices.
7
+ *
8
+ * @see https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/
9
+ * @see https://github.com/19h47/19h47-calendar
10
+ */
11
+ export declare class Calendar extends HTMLElement {
12
+ today: Date;
13
+ day: string;
14
+ options: Options;
15
+ current: Current;
16
+ private keyboard;
17
+ $body: HTMLTableSectionElement | null;
18
+ $title: HTMLElement | null;
19
+ $next: HTMLButtonElement | null;
20
+ $previous: HTMLButtonElement | null;
21
+ /** Selected days as `YYYY-MM-DD`. */
22
+ picked: string[];
23
+ connectedCallback(): void;
24
+ disconnectedCallback(): void;
25
+ private readOptions;
26
+ init(): void;
27
+ destroy(): void;
28
+ move(deltaMonths: number, { focus }?: {
29
+ focus?: boolean | undefined;
30
+ }): void;
31
+ persistPicked(): void;
32
+ setDaySelected($el: HTMLElement, selected: boolean): void;
33
+ handleClick: (event: MouseEvent) => void;
34
+ /** Paint in-between days while choosing the range end (mouse or keyboard). */
35
+ previewRange(to: string): void;
36
+ handleMousemove: (event: MouseEvent) => void;
37
+ getMonthName(month: number): string;
38
+ getWeekdays(): string[];
39
+ renderDays(): void;
40
+ renderHeader(month: number, year: number): void;
41
+ renderCalendar(month: number, year: number, { focus }?: {
42
+ focus?: boolean | undefined;
43
+ }): void;
44
+ reset(): void;
45
+ render({ focus }?: {
46
+ focus?: boolean | undefined;
47
+ }): void;
48
+ /** Override to enrich or rebuild each day cell. Keep `.js-day` and `data-day`. */
49
+ renderInner(_inner: HTMLElement, _date: Date): void;
50
+ }
51
+ //# sourceMappingURL=calendar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"calendar.d.ts","sourceRoot":"","sources":["../src/calendar.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAgB,OAAO,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAC;AAkD/E;;;;;;;;GAQG;AACH,qBAAa,QAAS,SAAQ,WAAW;IACvC,KAAK,EAAE,IAAI,CAAc;IACzB,GAAG,SAAM;IACT,OAAO,EAAE,OAAO,CAQd;IACF,OAAO,EAAE,OAAO,CAAoC;IAEpD,OAAO,CAAC,QAAQ,CAAyB;IAEzC,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAQ;IAC7C,MAAM,EAAE,WAAW,GAAG,IAAI,CAAQ;IAClC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAQ;IACvC,SAAS,EAAE,iBAAiB,GAAG,IAAI,CAAQ;IAE3C,qCAAqC;IACrC,MAAM,EAAE,MAAM,EAAE,CAAM;IAEtB,iBAAiB,IAAI,IAAI;IAgCzB,oBAAoB,IAAI,IAAI;IAS5B,OAAO,CAAC,WAAW;IAuBnB,IAAI,IAAI,IAAI;IAqBZ,OAAO,IAAI,IAAI;IAef,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,EAAE,KAAY,EAAE;;KAAK;IAW/C,aAAa;IAWb,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO;IAWlD,WAAW,GAAI,OAAO,UAAU,UAsD9B;IAEF,8EAA8E;IAC9E,YAAY,CAAC,EAAE,EAAE,MAAM;IAkDvB,eAAe,GAAI,OAAO,UAAU,UAalC;IAEF,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAOnC,WAAW,IAAI,MAAM,EAAE;IAMvB,UAAU;IAsBV,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAqBxC,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,KAAa,EAAE;;KAAK;IAoFlE,KAAK;IAML,MAAM,CAAC,EAAE,KAAa,EAAE;;KAAK;IAO7B,kFAAkF;IAClF,WAAW,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI;CAC7C"}
@@ -0,0 +1,5 @@
1
+ import { Calendar } from './calendar.js';
2
+ export { Calendar };
3
+ export type { ChangeDetail, Current, Options, StateClasses } from './types.js';
4
+ export { getWeekStart, toDayString, fromDayString } from './utils.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,CAAC;AACpB,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,340 @@
1
+ //#region ../utils/dist/index.js
2
+ var e = {
3
+ DRAWER_CLOSE: "drawer-close",
4
+ DRAWER_OPEN: "drawer-open",
5
+ DRAWER_TOGGLE: "drawer-toggle",
6
+ MODAL_CLOSE: "modal-close",
7
+ MODAL_OPEN: "modal-open",
8
+ MODAL_TOGGLE: "modal-toggle",
9
+ SPINBUTTON_CHANGE: "spinbutton-change",
10
+ DISCLOSURE_BUTTON_OPEN: "disclosure-button:open",
11
+ DISCLOSURE_BUTTON_CLOSE: "disclosure-button:close",
12
+ SWITCH_ACTIVATE: "switch:activate",
13
+ SWITCH_DEACTIVATE: "switch:deactivate",
14
+ ACCORDION_PANEL_OPEN: "accordion-panel:open",
15
+ ACCORDION_PANEL_CLOSE: "accordion-panel:close",
16
+ COMBOBOX_LOADING: "combobox:loading",
17
+ COMBOBOX_LOADED: "combobox:loaded",
18
+ COMBOBOX_UPDATE: "combobox:update",
19
+ COMBOBOX_SUBMIT: "combobox:submit",
20
+ COMBOBOX_EMPTY: "combobox:empty",
21
+ WINDOWSPLITTER_CHANGE: "windowsplitter:change",
22
+ CALENDAR_CHANGE: "calendar:change",
23
+ TAB_BEFORE_ACTIVATE: "tab-before-activate",
24
+ TAB_ACTIVATE: "tab-activate",
25
+ TAB_DELETE: "tab-delete",
26
+ CART_BEFORE_ADD: "cart-before-add",
27
+ CART_BEFORE_UPDATE: "cart-before-update",
28
+ CART_UPDATE: "cart-update",
29
+ VARIANT_CHANGE: "variant-change"
30
+ }, t = (e, t, n, r = {}) => {
31
+ let { bubbles: i = !0, cancelable: a = !0 } = r;
32
+ return e.dispatchEvent(new CustomEvent(t, {
33
+ bubbles: i,
34
+ cancelable: a,
35
+ detail: n
36
+ }));
37
+ }, n = (e, t) => {
38
+ if (e == null || e === "") return t;
39
+ let n = Number(e);
40
+ return Number.isFinite(n) ? n : t;
41
+ }, r = (e, t = !1) => e == null ? t : e !== "false" && e !== "0", i = (e, t) => {
42
+ let n = null, r = null, i = () => {
43
+ r && e(...r), n = null;
44
+ };
45
+ return (...e) => {
46
+ r = e, n ||= setTimeout(i, t);
47
+ };
48
+ }, a = document.documentElement, { body: o } = document;
49
+ a.hasAttribute("data-debug");
50
+ var s = {
51
+ x: 0,
52
+ y: 0
53
+ };
54
+ window.addEventListener("pointermove", i(({ x: e, y: t }) => {
55
+ s.x = e, s.y = t;
56
+ }, 100), { passive: !0 }), window.matchMedia("(width >= 64rem)"), window.matchMedia("(min-width: 1280px)"), window.matchMedia("(min-width: 1440px)"), window.matchMedia("(min-width: 1920px)");
57
+ //#endregion
58
+ //#region src/utils.ts
59
+ function c(e) {
60
+ return `${e.getFullYear()}-${String(e.getMonth() + 1).padStart(2, "0")}-${String(e.getDate()).padStart(2, "0")}`;
61
+ }
62
+ function l(e) {
63
+ let [t, n, r] = e.split("-").map(Number);
64
+ return new Date(t, n - 1, r);
65
+ }
66
+ function u(e, t, n) {
67
+ return e <= n && e >= t;
68
+ }
69
+ function d(e, t) {
70
+ return 32 - new Date(e, t, 32).getDate();
71
+ }
72
+ function f(e, t, n = 0) {
73
+ return (new Date(t, e).getDay() - n + 7) % 7;
74
+ }
75
+ function p(e) {
76
+ try {
77
+ let t = new Intl.Locale(e).getWeekInfo?.();
78
+ if (t) return t.firstDay === 7 ? 0 : t.firstDay;
79
+ } catch {}
80
+ return 0;
81
+ }
82
+ function m(e, t = "short") {
83
+ return Array.from({ length: 7 }, (n, r) => new Date(2020, 0, 5 + r).toLocaleDateString(e, { weekday: t }));
84
+ }
85
+ function h(e, t) {
86
+ return new Date(2020, t, 1).toLocaleDateString(e, { month: "long" });
87
+ }
88
+ //#endregion
89
+ //#region src/keyboard.ts
90
+ var g = class {
91
+ host;
92
+ constructor(e) {
93
+ this.host = e;
94
+ }
95
+ setFocusDay(e, { focus: t = !0 } = {}) {
96
+ this.host.$body?.querySelectorAll(".js-day").forEach((e) => {
97
+ e.tabIndex = -1;
98
+ }), e.tabIndex = 0, this.host.current.day = e.getAttribute("data-day"), t && e.focus(), this.host.current.day && this.host.previewRange(this.host.current.day);
99
+ }
100
+ dayInView() {
101
+ let { current: e } = this.host, t = e.day ? l(e.day).getDate() : 1, n = d(e.year, e.month);
102
+ return c(new Date(e.year, e.month, Math.min(t, n)));
103
+ }
104
+ focusDayByOffset(e, t) {
105
+ let n = l(e);
106
+ n.setDate(n.getDate() + t);
107
+ let r = c(n);
108
+ this.host.current.day = r, this.host.current.year = n.getFullYear(), this.host.current.month = n.getMonth();
109
+ let i = this.host.$body?.querySelector(`[data-day="${r}"]`);
110
+ if (i) {
111
+ this.setFocusDay(i);
112
+ return;
113
+ }
114
+ this.host.render({ focus: !0 });
115
+ }
116
+ sync({ focus: e = !1 } = {}) {
117
+ if (!this.host.$body) return;
118
+ let t = Array.from(this.host.$body.querySelectorAll(".js-day"));
119
+ if (!t.length) return;
120
+ let n = (e) => t.find((t) => t.getAttribute("data-day") === e), r = this.host.current.day && (n(this.host.current.day) || n(this.dayInView())) || t.find((e) => this.host.picked.includes(e.getAttribute("data-day") || "")) || n(this.host.day) || t[0];
121
+ this.setFocusDay(r, { focus: e });
122
+ }
123
+ handleKeydown = (e) => {
124
+ let t = e.target.closest(".js-day");
125
+ if (!t) return;
126
+ let n = t.getAttribute("data-day"), r = (l(n).getDay() - this.host.options.firstDay + 7) % 7, { key: i, code: a } = e, o = () => {
127
+ t.getAttribute("aria-disabled") !== "true" && (e.preventDefault(), t.click());
128
+ }, s = {
129
+ ArrowLeft: () => {
130
+ e.preventDefault(), this.focusDayByOffset(n, -1);
131
+ },
132
+ ArrowRight: () => {
133
+ e.preventDefault(), this.focusDayByOffset(n, 1);
134
+ },
135
+ ArrowUp: () => {
136
+ e.preventDefault(), this.focusDayByOffset(n, -7);
137
+ },
138
+ ArrowDown: () => {
139
+ e.preventDefault(), this.focusDayByOffset(n, 7);
140
+ },
141
+ Home: () => {
142
+ e.preventDefault(), this.focusDayByOffset(n, -r);
143
+ },
144
+ End: () => {
145
+ e.preventDefault(), this.focusDayByOffset(n, 6 - r);
146
+ },
147
+ PageUp: () => {
148
+ e.preventDefault(), this.host.move(e.shiftKey ? -12 : -1);
149
+ },
150
+ PageDown: () => {
151
+ e.preventDefault(), this.host.move(e.shiftKey ? 12 : 1);
152
+ },
153
+ Enter: o,
154
+ " ": o,
155
+ default: () => !1
156
+ };
157
+ return (s[i || a] || s.default)();
158
+ };
159
+ }, _ = {
160
+ active: "active",
161
+ range: "range",
162
+ start: "start",
163
+ end: "end"
164
+ }, v = (e, t, n, { disabled: r, selected: i, current: a, tabIndex: o }) => `
165
+ <button
166
+ type="button"
167
+ class="js-day${n ? ` ${n}` : ""}"
168
+ data-day="${e}"
169
+ tabindex="${o}"
170
+ ${r ? "aria-disabled=\"true\"" : ""}
171
+ ${i ? "aria-selected=\"true\"" : ""}
172
+ ${a ? "aria-current=\"date\"" : ""}
173
+ >
174
+ ${t}
175
+ </button>
176
+ `, y = () => document.documentElement.getAttribute("lang") || "en", b = class extends HTMLElement {
177
+ today = /* @__PURE__ */ new Date();
178
+ day = "";
179
+ options = {
180
+ single: !0,
181
+ firstDay: 0,
182
+ stateClasses: { ..._ },
183
+ locale: "en",
184
+ buttonClass: "",
185
+ deselect: !1,
186
+ allowPast: !1
187
+ };
188
+ current = {
189
+ month: 0,
190
+ year: 0,
191
+ day: null
192
+ };
193
+ keyboard = null;
194
+ $body = null;
195
+ $title = null;
196
+ $next = null;
197
+ $previous = null;
198
+ picked = [];
199
+ connectedCallback() {
200
+ let e = /* @__PURE__ */ new Date();
201
+ if (this.today = new Date(e.getFullYear(), e.getMonth(), e.getDate()), this.day = c(this.today), this.options = this.readOptions(), this.current = {
202
+ month: n(this.getAttribute("data-month"), this.today.getMonth()),
203
+ year: n(this.getAttribute("data-year"), this.today.getFullYear()),
204
+ day: null
205
+ }, this.$title = this.querySelector(".js-title"), this.$body = this.querySelector(".js-body"), this.$next = this.querySelector(".js-next"), this.$previous = this.querySelector(".js-previous"), !this.$body) throw Error("Calendar: .js-body element not found");
206
+ this.keyboard = new g(this), this.init();
207
+ }
208
+ disconnectedCallback() {
209
+ this.destroy(), this.keyboard = null, this.$body = null, this.$title = null, this.$next = null, this.$previous = null;
210
+ }
211
+ readOptions() {
212
+ let e = this.getAttribute("locale") || this.getAttribute("data-locale") || y(), t = this.getAttribute("first-day");
213
+ return {
214
+ single: r(this.getAttribute("single"), !0),
215
+ firstDay: t != null && t !== "" ? n(t, p(e)) : p(e),
216
+ stateClasses: { ..._ },
217
+ locale: e,
218
+ buttonClass: this.getAttribute("button-class") || "",
219
+ deselect: r(this.getAttribute("deselect"), !1),
220
+ allowPast: r(this.getAttribute("allow-past"), !1),
221
+ name: this.getAttribute("name") || void 0
222
+ };
223
+ }
224
+ init() {
225
+ try {
226
+ this.picked = JSON.parse(this.getAttribute("data-picked-dates") || "[]");
227
+ } catch {
228
+ this.picked = [];
229
+ }
230
+ this.render(), this.addEventListener("click", this.handleClick), this.$body && this.keyboard && (this.$body.addEventListener("keydown", this.keyboard.handleKeydown, !1), this.options.single || this.$body.addEventListener("mousemove", this.handleMousemove, !1));
231
+ }
232
+ destroy() {
233
+ this.removeEventListener("click", this.handleClick), this.$body && this.keyboard && (this.$body.removeEventListener("keydown", this.keyboard.handleKeydown, !1), this.$body.removeEventListener("mousemove", this.handleMousemove, !1)), this.reset();
234
+ }
235
+ move(e, { focus: t = !0 } = {}) {
236
+ let n = new Date(this.current.year, this.current.month + e, 1);
237
+ this.current.year = n.getFullYear(), this.current.month = n.getMonth(), this.render({ focus: t });
238
+ }
239
+ persistPicked() {
240
+ this.setAttribute("data-picked-dates", JSON.stringify(this.picked));
241
+ let n = {
242
+ values: this.picked,
243
+ name: this.options.name
244
+ };
245
+ t(this, e.CALENDAR_CHANGE, n, { cancelable: !1 });
246
+ }
247
+ setDaySelected(e, t) {
248
+ if (t) {
249
+ e.classList.add(this.options.stateClasses.active), e.setAttribute("aria-selected", "true");
250
+ return;
251
+ }
252
+ e.classList.remove(this.options.stateClasses.active), e.removeAttribute("aria-selected");
253
+ }
254
+ handleClick = (e) => {
255
+ let t = e.target;
256
+ if (t.closest(".js-next") || t.closest(".js-title")) return this.move(1, { focus: !1 });
257
+ if (t.closest(".js-previous")) return this.move(-1, { focus: !1 });
258
+ if (t = t.closest(".js-day"), !t || t.getAttribute("aria-disabled") === "true") return;
259
+ let n = t.getAttribute("data-day");
260
+ return this.keyboard?.setFocusDay(t, { focus: !1 }), this.options.single ? t.classList.contains(this.options.stateClasses.active) && this.options.deselect ? (this.picked = [], this.setDaySelected(t, !1), this.persistPicked()) : (this.picked.forEach((e) => {
261
+ let t = this.$body?.querySelector(`[data-day="${e}"]`);
262
+ t && this.setDaySelected(t, !1);
263
+ }), this.picked = [n], this.setDaySelected(t, !0), this.persistPicked()) : (1 < this.picked.length && (this.$body?.querySelectorAll(".js-day").forEach((e) => {
264
+ e.classList.remove(this.options.stateClasses.range), this.setDaySelected(e, !1);
265
+ }), this.picked = [], this.setAttribute("data-picked-dates", JSON.stringify(this.picked))), this.picked.push(n), this.picked.sort(), this.setDaySelected(t, !0), this.persistPicked());
266
+ };
267
+ previewRange(e) {
268
+ if (this.options.single || this.picked.length !== 1 || !this.$body) return;
269
+ let t = this.$body.querySelector(`[data-day="${e}"]`);
270
+ if (!t || t.getAttribute("aria-disabled") === "true") return;
271
+ let n = this.$body.querySelectorAll(".js-day"), r = this.$body.querySelector(`[data-day="${this.picked[0]}"]`), i = !1, a = this.picked[0], o = e;
272
+ a > o && (i = !0, o = this.picked[0], a = e), n.forEach((e) => {
273
+ let t = e.getAttribute("data-day");
274
+ e.classList.remove(this.options.stateClasses.range, this.options.stateClasses.end, this.options.stateClasses.start), u(t, a, o) && e.classList.add(this.options.stateClasses.range);
275
+ }), r?.classList.add(this.options.stateClasses.start), t.classList.add(this.options.stateClasses.end), i && (r?.classList.add(this.options.stateClasses.end), r?.classList.remove(this.options.stateClasses.start), t.classList.add(this.options.stateClasses.start), t.classList.remove(this.options.stateClasses.end));
276
+ }
277
+ handleMousemove = (e) => {
278
+ let t = e.target.closest(".js-day");
279
+ if (!t) return;
280
+ let n = t.getAttribute("data-day");
281
+ n && this.previewRange(n);
282
+ };
283
+ getMonthName(e) {
284
+ return this.options.months?.[e] ?? h(this.options.locale, e);
285
+ }
286
+ getWeekdays() {
287
+ return this.options.days ?? m(this.options.locale, "short");
288
+ }
289
+ renderDays() {
290
+ let e = this.querySelector(".js-days");
291
+ if (!e) return;
292
+ let t = this.getWeekdays(), n = m(this.options.locale, "long");
293
+ e.innerHTML = "";
294
+ for (let r = 0; r < 7; r += 1) {
295
+ let i = (this.options.firstDay + r) % 7, a = document.createElement("th");
296
+ a.scope = "col", a.abbr = n[i], a.textContent = t[i], e.appendChild(a);
297
+ }
298
+ }
299
+ renderHeader(e, t) {
300
+ this.$title && (this.$title.textContent = new Date(t, e, 1).toLocaleDateString(this.options.locale, {
301
+ month: "long",
302
+ year: "numeric"
303
+ })), this.$previous?.setAttribute("data-content", this.getMonthName(0 > e - 1 ? 11 : e - 1)), this.$next?.setAttribute("data-content", this.getMonthName(11 < e + 1 ? 0 : e + 1));
304
+ }
305
+ renderCalendar(e, t, { focus: n = !1 } = {}) {
306
+ if (!this.$body) return;
307
+ let r = this.options.buttonClass ?? "", i = 1;
308
+ for (let n = 0; 6 >= n; n += 1) {
309
+ let a = document.createElement("tr");
310
+ for (let o = this.options.firstDay; o < 7 + this.options.firstDay; o += 1) {
311
+ let s = new Date(t, e, i), l = document.createElement("td"), p = document.createElement("div");
312
+ if (n === 0 && o < this.options.firstDay + f(e, t, this.options.firstDay)) a.appendChild(l);
313
+ else if (i > d(t, e)) break;
314
+ else {
315
+ let e = c(s), t = e === this.day, n = this.options.allowPast || e >= this.day, o = this.picked.includes(e);
316
+ p.innerHTML = v(e, i, r, {
317
+ disabled: !n,
318
+ selected: o,
319
+ current: t,
320
+ tabIndex: -1
321
+ });
322
+ let d = p.querySelector("button");
323
+ o && (d.classList.add(this.options.stateClasses.active), e === this.picked[0] && d.classList.add(this.options.stateClasses.start), this.picked.length > 1 && e === this.picked[this.picked.length - 1] && d.classList.add(this.options.stateClasses.end)), !this.options.single && this.picked.length === 2 && u(e, this.picked[0], this.picked[1]) && d.classList.add(this.options.stateClasses.range), this.renderInner(p, s), l.appendChild(p), a.appendChild(l), i += 1;
324
+ }
325
+ }
326
+ a.childNodes.length && this.$body.appendChild(a);
327
+ }
328
+ this.keyboard?.sync({ focus: n });
329
+ }
330
+ reset() {
331
+ this.$body && (this.$body.innerHTML = "");
332
+ }
333
+ render({ focus: e = !1 } = {}) {
334
+ this.reset(), this.renderDays(), this.renderHeader(this.current.month, this.current.year), this.renderCalendar(this.current.month, this.current.year, { focus: e });
335
+ }
336
+ renderInner(e, t) {}
337
+ };
338
+ customElements.get("cinq-calendar") || customElements.define("cinq-calendar", b);
339
+ //#endregion
340
+ export { b as Calendar, l as fromDayString, p as getWeekStart, c as toDayString };
@@ -0,0 +1,17 @@
1
+ import { Calendar } from './calendar.js';
2
+ /** APG day-grid focus: roving tabindex + keyboard navigation. */
3
+ export default class Keyboard {
4
+ host: Calendar;
5
+ constructor(host: Calendar);
6
+ setFocusDay($button: HTMLButtonElement, { focus }?: {
7
+ focus?: boolean | undefined;
8
+ }): void;
9
+ /** Same day-of-month in the viewed month, or last day if it does not exist (APG). */
10
+ dayInView(): string;
11
+ focusDayByOffset(fromDay: string, dayDelta: number): void;
12
+ sync({ focus }?: {
13
+ focus?: boolean | undefined;
14
+ }): void;
15
+ handleKeydown: (event: KeyboardEvent) => false | void;
16
+ }
17
+ //# sourceMappingURL=keyboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../src/keyboard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,iEAAiE;AACjE,MAAM,CAAC,OAAO,OAAO,QAAQ;IAC3B,IAAI,EAAE,QAAQ,CAAC;gBAEH,IAAI,EAAE,QAAQ;IAI1B,WAAW,CAAC,OAAO,EAAE,iBAAiB,EAAE,EAAE,KAAY,EAAE;;KAAK;IAiB7D,qFAAqF;IACrF,SAAS,IAAI,MAAM;IAYnB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAqBlD,IAAI,CAAC,EAAE,KAAa,EAAE;;KAAK;IA4B3B,aAAa,GAAI,OAAO,aAAa,kBA4DnC;CACH"}
@@ -0,0 +1,29 @@
1
+ export interface StateClasses {
2
+ active: string;
3
+ range: string;
4
+ start: string;
5
+ end: string;
6
+ }
7
+ export interface Options {
8
+ single?: boolean;
9
+ firstDay: number;
10
+ stateClasses: StateClasses;
11
+ locale: string;
12
+ buttonClass?: string;
13
+ months?: string[];
14
+ days?: string[];
15
+ deselect?: boolean;
16
+ name?: string;
17
+ allowPast?: boolean;
18
+ }
19
+ export interface Current {
20
+ month: number;
21
+ year: number;
22
+ /** Navigation day as `YYYY-MM-DD` (may be outside the viewed month until render). */
23
+ day: string | null;
24
+ }
25
+ export interface ChangeDetail {
26
+ values: string[];
27
+ name?: string;
28
+ }
29
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,OAAO;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,YAAY,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf"}
@@ -0,0 +1,24 @@
1
+ /** Local calendar day as `YYYY-MM-DD`. */
2
+ export declare function toDayString(date: Date): string;
3
+ /** Parse a local `YYYY-MM-DD` day into a Date at local midnight. */
4
+ export declare function fromDayString(day: string): Date;
5
+ /** Inclusive `YYYY-MM-DD` range check. */
6
+ export declare function isBetween(check: string, from: string, to: string): boolean;
7
+ /** @see https://dzone.com/articles/determining-number-days-month */
8
+ export declare function getDaysInMonth(year: number, month: number): number;
9
+ /**
10
+ * How many weekdays precede the 1st of the month, given a week start
11
+ * (`0`=Sun … `6`=Sat).
12
+ *
13
+ * @see https://stackoverflow.com/a/33508649/5091221
14
+ */
15
+ export declare function getLeadingDays(month: number, year: number, weekStart?: number): number;
16
+ /**
17
+ * Week start for a locale as JS `getDay()` (0=Sun … 6=Sat).
18
+ * Falls back to Sunday when `Intl.Locale.getWeekInfo` is unavailable.
19
+ */
20
+ export declare function getWeekStart(locale: string): number;
21
+ /** Sunday-first weekday labels (Jan 5, 2020 was a Sunday). */
22
+ export declare function getIntlWeekdays(locale: string, weekday?: Intl.DateTimeFormatOptions["weekday"]): string[];
23
+ export declare function getIntlMonth(locale: string, month: number): string;
24
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAM9C;AAED,oEAAoE;AACpE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAI/C;AAED,0CAA0C;AAC1C,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,SAAS,SAAI,GACZ,MAAM,CAGR;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAcnD;AAED,8DAA8D;AAC9D,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,IAAI,CAAC,qBAAqB,CAAC,SAAS,CAAW,GACvD,MAAM,EAAE,CAIV;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIlE"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@agencecinq/calendar",
3
+ "version": "1.0.0",
4
+ "description": "Accessible date / range picker as a lightweight Web Component.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Jérémy Levron <jeremy@agencecinq.com> (https://agencecinq.com)",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/agencecinq/shopify.git",
11
+ "directory": "packages/calendar"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "main": "./dist/index.js",
20
+ "module": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "peerDependencies": {
26
+ "@agencecinq/utils": "*"
27
+ },
28
+ "devDependencies": {
29
+ "vite": "^8.1.4",
30
+ "vite-plugin-dts": "^5.0.3",
31
+ "@agencecinq/utils": "5.2.1"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "vite build",
38
+ "dev": "vite build --watch"
39
+ }
40
+ }