@xmesh/system-design 0.0.2 → 0.0.4

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 (73) hide show
  1. package/dist/lit/components/alert/index.d.ts +1 -1
  2. package/dist/lit/components/alert/index.js +1 -0
  3. package/dist/lit/components/app-bar/index.d.ts +1 -1
  4. package/dist/lit/components/app-bar/index.js +1 -0
  5. package/dist/lit/components/artifact/index.d.ts +1 -1
  6. package/dist/lit/components/artifact/index.js +16 -2
  7. package/dist/lit/components/avatar/index.d.ts +1 -1
  8. package/dist/lit/components/avatar/index.js +1 -0
  9. package/dist/lit/components/avatar-group/index.d.ts +1 -1
  10. package/dist/lit/components/avatar-group/index.js +1 -0
  11. package/dist/lit/components/badge/index.d.ts +1 -2
  12. package/dist/lit/components/badge/index.js +1 -0
  13. package/dist/lit/components/brand-mark/index.d.ts +2 -2
  14. package/dist/lit/components/brand-mark/index.js +14 -0
  15. package/dist/lit/components/breadcrumbs/index.d.ts +1 -1
  16. package/dist/lit/components/breadcrumbs/index.js +1 -0
  17. package/dist/lit/components/bubble/index.d.ts +4 -4
  18. package/dist/lit/components/bubble/index.js +18 -0
  19. package/dist/lit/components/button/index.d.ts +1 -1
  20. package/dist/lit/components/button/index.js +1 -0
  21. package/dist/lit/components/card/index.d.ts +1 -1
  22. package/dist/lit/components/card/index.js +1 -0
  23. package/dist/lit/components/chat/index.d.ts +1 -2
  24. package/dist/lit/components/chat/index.js +16 -2
  25. package/dist/lit/components/checkbox/index.d.ts +1 -2
  26. package/dist/lit/components/checkbox/index.js +1 -0
  27. package/dist/lit/components/chip/index.d.ts +1 -1
  28. package/dist/lit/components/chip/index.js +1 -0
  29. package/dist/lit/components/chip-group/index.d.ts +1 -1
  30. package/dist/lit/components/chip-group/index.js +1 -0
  31. package/dist/lit/components/code/index.d.ts +1 -2
  32. package/dist/lit/components/code/index.js +1 -0
  33. package/dist/lit/components/composer/index.d.ts +1 -2
  34. package/dist/lit/components/composer/index.js +14 -0
  35. package/dist/lit/components/date-range/index.css +324 -0
  36. package/dist/lit/components/date-range/index.d.ts +57 -0
  37. package/dist/lit/components/date-range/index.js +702 -0
  38. package/dist/lit/components/divider/index.d.ts +1 -1
  39. package/dist/lit/components/divider/index.js +1 -0
  40. package/dist/lit/components/expansion-panel/index.d.ts +1 -2
  41. package/dist/lit/components/expansion-panel/index.js +1 -0
  42. package/dist/lit/components/grid/index.d.ts +1 -1
  43. package/dist/lit/components/grid/index.js +1 -0
  44. package/dist/lit/components/kbd/index.d.ts +1 -2
  45. package/dist/lit/components/kbd/index.js +1 -0
  46. package/dist/lit/components/list/index.d.ts +1 -1
  47. package/dist/lit/components/list/index.js +1 -0
  48. package/dist/lit/components/list-item/index.d.ts +1 -2
  49. package/dist/lit/components/list-item/index.js +1 -0
  50. package/dist/lit/components/navigation-drawer/index.d.ts +1 -2
  51. package/dist/lit/components/navigation-drawer/index.js +1 -0
  52. package/dist/lit/components/pagination/index.d.ts +1 -1
  53. package/dist/lit/components/pagination/index.js +1 -0
  54. package/dist/lit/components/popover/index.css +34 -0
  55. package/dist/lit/components/popover/index.d.ts +29 -0
  56. package/dist/lit/components/popover/index.js +204 -0
  57. package/dist/lit/components/primitives/index.d.ts +2 -2
  58. package/dist/lit/components/primitives/index.js +14 -0
  59. package/dist/lit/components/sidebar-item/index.d.ts +1 -1
  60. package/dist/lit/components/sidebar-item/index.js +13 -0
  61. package/dist/lit/components/snackbar/index.d.ts +1 -2
  62. package/dist/lit/components/snackbar/index.js +16 -2
  63. package/dist/lit/components/stack/index.d.ts +1 -1
  64. package/dist/lit/components/stack/index.js +1 -0
  65. package/dist/lit/components/table/index.d.ts +1 -2
  66. package/dist/lit/components/table/index.js +5 -9
  67. package/dist/lit/components/tabs/index.d.ts +3 -3
  68. package/dist/lit/components/tabs/index.js +3 -0
  69. package/dist/lit/components/validation/index.d.ts +1 -2
  70. package/dist/lit/components/validation/index.js +16 -1
  71. package/dist/lit/index.d.ts +2 -0
  72. package/dist/lit/index.js +2 -0
  73. package/package.json +1 -1
@@ -0,0 +1,702 @@
1
+ /*
2
+ date-range/index.ts — <xm-date-range>
3
+
4
+ A field-styled trigger that opens an anchored popover holding a vertical preset
5
+ rail + an accessible calendar grid for selecting a date range. Emits ISO
6
+ yyyy-mm-dd strings.
7
+
8
+ Composes the xm-overlay foundation (Story 1.4) for anchored positioning,
9
+ top-layer stacking, Esc dismissal, and focus-restore — it does NOT hand-roll
10
+ z-index, anchoring math, or the close-on-Esc path. The overlay runs in the
11
+ `menu` tier and is driven through its PUBLIC API only (mode / tier / placement /
12
+ .anchor / .opener / show / hide / xm-overlay-close); we never reach into its
13
+ shadow root (AD-12). Because the overlay's non-modal popover is
14
+ `popover="manual"` (no native light-dismiss), outside-click dismissal is owned
15
+ here, and the panel keeps its own Tab focus-cycle (role="dialog").
16
+
17
+ Properties:
18
+ from string | null ISO yyyy-mm-dd start (controlled)
19
+ to string | null ISO yyyy-mm-dd end (controlled)
20
+ placeholder string trigger placeholder (default "Date range")
21
+ open boolean reflected open state (also a preview hook)
22
+
23
+ Events (AD-8 / AD-8a, tier (b)):
24
+ xm-date-range-change detail { from, to } — ISO yyyy-mm-dd strings.
25
+ Presets emit IMMEDIATELY and close; the calendar emits only on explicit
26
+ Apply; Clear emits { null, null }. bubbles:true, composed:true.
27
+
28
+ Behaviour:
29
+ - Presets (Today / Last 7 days / Last 30 days / This month) apply on click,
30
+ emit, and close. "Custom" just reveals the calendar (no emit).
31
+ - Calendar: click an endpoint, then a second to complete the range; Apply
32
+ commits. Clear resets.
33
+
34
+ A11y (critical):
35
+ - panel role="dialog" + aria-labelledby; initial focus = current start
36
+ endpoint (or today if unset); on close focus → trigger (via overlay.opener).
37
+ - calendar role="grid", roving tabindex (one day tabindex=0, rest -1),
38
+ aria-current="date" on today, aria-selected on chosen days, each day's
39
+ accessible name is the FULL date (e.g. "Tuesday, April 14, 2026").
40
+ - PageUp/PageDown change month and announce the new month/year via a polite
41
+ live region inside the component.
42
+ - preset rail role="radiogroup" with aria-checked.
43
+ - invalid range (from>to): role="alert" message + Apply aria-disabled
44
+ (still focusable, NOT hard-disabled).
45
+
46
+ Shadow DOM. Depends on xm-overlay + tokens (AD-12). Lit is a bare `import`.
47
+ */
48
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
49
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
50
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
51
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
52
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
53
+ };
54
+ import { LitElement, html, svg, nothing } from "lit";
55
+ import { customElement, property, state, query } from "lit/decorators.js";
56
+ const DR_CSS = new URL("../date-range/index.css", import.meta.url).href;
57
+ let drSeq = 0;
58
+ const PRESETS = [
59
+ { id: "today", label: "Today" },
60
+ { id: "last7", label: "Last 7 days" },
61
+ { id: "last30", label: "Last 30 days" },
62
+ { id: "month", label: "This month" },
63
+ { id: "custom", label: "Custom" },
64
+ ];
65
+ const WEEKDAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
66
+ const CAL_ICON = () => svg `
67
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
68
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
69
+ stroke-linejoin="round" aria-hidden="true">
70
+ <rect x="3" y="4" width="18" height="18" rx="2" />
71
+ <path d="M16 2v4M8 2v4M3 10h18" />
72
+ </svg>
73
+ `;
74
+ const CHEVRON_LEFT = () => svg `
75
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
76
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
77
+ stroke-linejoin="round" aria-hidden="true">
78
+ <polyline points="15 18 9 12 15 6" />
79
+ </svg>
80
+ `;
81
+ const CHEVRON_RIGHT = () => svg `
82
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none"
83
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
84
+ stroke-linejoin="round" aria-hidden="true">
85
+ <polyline points="9 18 15 12 9 6" />
86
+ </svg>
87
+ `;
88
+ const ALERT_ICON = () => svg `
89
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
90
+ stroke="currentColor" stroke-width="1.8" stroke-linecap="round"
91
+ stroke-linejoin="round" aria-hidden="true">
92
+ <path d="M12 9v4M12 17h.01" />
93
+ <path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
94
+ </svg>
95
+ `;
96
+ /* ---------- date utilities (all local, no Intl-day-arithmetic) ---------- */
97
+ function pad(n) {
98
+ return n < 10 ? `0${n}` : `${n}`;
99
+ }
100
+ function toISO(d) {
101
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
102
+ }
103
+ function fromISO(s) {
104
+ if (!s)
105
+ return null;
106
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
107
+ if (!m)
108
+ return null;
109
+ const y = Number(m[1]);
110
+ const mo = Number(m[2]) - 1;
111
+ const da = Number(m[3]);
112
+ const d = new Date(y, mo, da);
113
+ if (Number.isNaN(d.getTime()))
114
+ return null;
115
+ // new Date normalizes overflow (Feb 30 → Mar 2), so a structurally-valid but
116
+ // non-existent date would silently round-trip out as a different ISO string.
117
+ // Reject it by checking the components survived the round-trip.
118
+ if (d.getFullYear() !== y || d.getMonth() !== mo || d.getDate() !== da) {
119
+ return null;
120
+ }
121
+ return d;
122
+ }
123
+ function sameDay(a, b) {
124
+ return (a.getFullYear() === b.getFullYear() &&
125
+ a.getMonth() === b.getMonth() &&
126
+ a.getDate() === b.getDate());
127
+ }
128
+ function startOfDay(d) {
129
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate());
130
+ }
131
+ function addDays(d, n) {
132
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n);
133
+ }
134
+ const MONTH_FMT = new Intl.DateTimeFormat(undefined, {
135
+ month: "long",
136
+ year: "numeric",
137
+ });
138
+ const FULL_FMT = new Intl.DateTimeFormat(undefined, {
139
+ weekday: "long",
140
+ month: "long",
141
+ day: "numeric",
142
+ year: "numeric",
143
+ });
144
+ const SHORT_FMT = new Intl.DateTimeFormat(undefined, {
145
+ month: "short",
146
+ day: "numeric",
147
+ year: "numeric",
148
+ });
149
+ let XmDateRange = class XmDateRange extends LitElement {
150
+ constructor() {
151
+ super(...arguments);
152
+ this.from = null;
153
+ this.to = null;
154
+ this.placeholder = "Date range";
155
+ this.open = false;
156
+ this._id = `xm-date-range-${++drSeq}`;
157
+ // Draft selection inside the calendar (not committed until Apply).
158
+ this._draftFrom = null;
159
+ this._draftTo = null;
160
+ // The month currently displayed in the calendar (first of month).
161
+ this._view = startOfDay(new Date());
162
+ // The day that owns roving tabindex=0.
163
+ this._focusDay = startOfDay(new Date());
164
+ this._liveMsg = "";
165
+ this._activePreset = null;
166
+ // Guards re-entrancy between our hide() and the overlay's xm-overlay-close.
167
+ this._closing = false;
168
+ this._onDocPointer = (e) => {
169
+ if (!this.open)
170
+ return;
171
+ if (!e.composedPath().includes(this))
172
+ this._close(false);
173
+ };
174
+ this._onOverlayClose = () => {
175
+ // Overlay self-dismissed (Esc). It already restored focus to the trigger
176
+ // via .opener; just sync our state.
177
+ if (this.open && !this._closing) {
178
+ this.open = false;
179
+ }
180
+ };
181
+ }
182
+ connectedCallback() {
183
+ super.connectedCallback();
184
+ document.addEventListener("pointerdown", this._onDocPointer, true);
185
+ }
186
+ disconnectedCallback() {
187
+ super.disconnectedCallback();
188
+ document.removeEventListener("pointerdown", this._onDocPointer, true);
189
+ }
190
+ /* ---------- open / close ---------- */
191
+ _initDraft() {
192
+ const start = fromISO(this.from);
193
+ const end = fromISO(this.to);
194
+ this._draftFrom = start;
195
+ this._draftTo = end;
196
+ const anchor = start ?? startOfDay(new Date());
197
+ this._view = new Date(anchor.getFullYear(), anchor.getMonth(), 1);
198
+ this._focusDay = anchor;
199
+ this._activePreset = null;
200
+ this._liveMsg = "";
201
+ }
202
+ _toggle() {
203
+ if (this.open)
204
+ this._close(false);
205
+ else
206
+ this.open = true;
207
+ }
208
+ _close(restoreFocus) {
209
+ if (!this.open)
210
+ return;
211
+ this._closing = true;
212
+ this.open = false;
213
+ const ov = this._overlay;
214
+ if (ov?.open)
215
+ ov.hide("api");
216
+ this._closing = false;
217
+ if (restoreFocus)
218
+ requestAnimationFrame(() => this._trigger?.focus());
219
+ }
220
+ _focusRovingDay() {
221
+ const el = this._panel?.querySelector('.date-range__day[tabindex="0"]');
222
+ el?.focus();
223
+ }
224
+ /* ---------- presets ---------- */
225
+ _applyPreset(id) {
226
+ if (id === "custom") {
227
+ this._activePreset = "custom";
228
+ return;
229
+ }
230
+ const today = startOfDay(new Date());
231
+ let from;
232
+ let to;
233
+ switch (id) {
234
+ case "today":
235
+ from = today;
236
+ to = today;
237
+ break;
238
+ case "last7":
239
+ from = addDays(today, -6);
240
+ to = today;
241
+ break;
242
+ case "last30":
243
+ from = addDays(today, -29);
244
+ to = today;
245
+ break;
246
+ case "month":
247
+ from = new Date(today.getFullYear(), today.getMonth(), 1);
248
+ to = today;
249
+ break;
250
+ default:
251
+ return;
252
+ }
253
+ this._emit(from, to);
254
+ this._close(true);
255
+ }
256
+ /* ---------- calendar selection ---------- */
257
+ _pickDay(day) {
258
+ this._activePreset = "custom";
259
+ if (!this._draftFrom || (this._draftFrom && this._draftTo)) {
260
+ // start a fresh range
261
+ this._draftFrom = day;
262
+ this._draftTo = null;
263
+ this._liveMsg = `Start date ${FULL_FMT.format(day)} selected, choose end date`;
264
+ }
265
+ else {
266
+ // completing the range; order them
267
+ if (day < this._draftFrom) {
268
+ this._draftTo = this._draftFrom;
269
+ this._draftFrom = day;
270
+ }
271
+ else {
272
+ this._draftTo = day;
273
+ }
274
+ this._liveMsg = `Range ${FULL_FMT.format(this._draftFrom)} to ${FULL_FMT.format(this._draftTo)} selected`;
275
+ }
276
+ this._focusDay = day;
277
+ // Picking a muted leading/trailing cell scrolls the calendar to that month
278
+ // so the header + roving tabindex don't strand on an out-of-month day.
279
+ if (day.getMonth() !== this._view.getMonth() ||
280
+ day.getFullYear() !== this._view.getFullYear()) {
281
+ this._view = new Date(day.getFullYear(), day.getMonth(), 1);
282
+ }
283
+ }
284
+ get _invalid() {
285
+ return !!(this._draftFrom &&
286
+ this._draftTo &&
287
+ this._draftFrom > this._draftTo);
288
+ }
289
+ _apply() {
290
+ if (!this._draftFrom || !this._draftTo || this._invalid)
291
+ return;
292
+ this._emit(this._draftFrom, this._draftTo);
293
+ this._close(true);
294
+ }
295
+ _clear() {
296
+ this._draftFrom = null;
297
+ this._draftTo = null;
298
+ this._activePreset = null;
299
+ // Only emit when there was actually a committed range to clear, so a Clear
300
+ // on an already-empty picker doesn't fire a no-op change.
301
+ if (this.from !== null || this.to !== null) {
302
+ this._emit(null, null);
303
+ }
304
+ this._close(true);
305
+ }
306
+ _emit(from, to) {
307
+ const detail = {
308
+ from: from ? toISO(from) : null,
309
+ to: to ? toISO(to) : null,
310
+ };
311
+ this.from = detail.from;
312
+ this.to = detail.to;
313
+ this.dispatchEvent(new CustomEvent("xm-date-range-change", {
314
+ detail,
315
+ bubbles: true,
316
+ composed: true,
317
+ }));
318
+ }
319
+ /* ---------- month nav ---------- */
320
+ _shiftMonth(delta) {
321
+ this._view = new Date(this._view.getFullYear(), this._view.getMonth() + delta, 1);
322
+ this._liveMsg = MONTH_FMT.format(this._view);
323
+ }
324
+ /* ---------- keyboard (roving grid + Tab-cycle + PageUp/Down) ---------- */
325
+ _onGridKey(e) {
326
+ let next = null;
327
+ switch (e.key) {
328
+ case "ArrowRight":
329
+ next = addDays(this._focusDay, 1);
330
+ break;
331
+ case "ArrowLeft":
332
+ next = addDays(this._focusDay, -1);
333
+ break;
334
+ case "ArrowDown":
335
+ next = addDays(this._focusDay, 7);
336
+ break;
337
+ case "ArrowUp":
338
+ next = addDays(this._focusDay, -7);
339
+ break;
340
+ case "Home":
341
+ next = addDays(this._focusDay, -((this._focusDay.getDay() + 6) % 7));
342
+ break;
343
+ case "End":
344
+ next = addDays(this._focusDay, 6 - ((this._focusDay.getDay() + 6) % 7));
345
+ break;
346
+ case "PageUp":
347
+ e.preventDefault();
348
+ this._shiftMonth(-1);
349
+ next = new Date(this._view.getFullYear(), this._view.getMonth(), Math.min(this._focusDay.getDate(), this._daysInMonth(this._view)));
350
+ break;
351
+ case "PageDown":
352
+ e.preventDefault();
353
+ this._shiftMonth(1);
354
+ next = new Date(this._view.getFullYear(), this._view.getMonth(), Math.min(this._focusDay.getDate(), this._daysInMonth(this._view)));
355
+ break;
356
+ // Enter / Space are handled by the focused day's native <button>
357
+ // (activation → @click → _pickDay). Intercepting them here too would
358
+ // double-fire on Space (button activates on keyup, after this keydown).
359
+ default:
360
+ return;
361
+ }
362
+ if (next) {
363
+ e.preventDefault();
364
+ this._focusDay = next;
365
+ if (next.getMonth() !== this._view.getMonth() ||
366
+ next.getFullYear() !== this._view.getFullYear()) {
367
+ this._view = new Date(next.getFullYear(), next.getMonth(), 1);
368
+ }
369
+ requestAnimationFrame(() => this._focusRovingDay());
370
+ }
371
+ }
372
+ _panelFocusables() {
373
+ if (!this._panel)
374
+ return [];
375
+ const sel = 'button:not([disabled]),[role="radio"],[tabindex]:not([tabindex="-1"])';
376
+ return Array.from(this._panel.querySelectorAll(sel)).filter((el) => el.getClientRects().length > 0);
377
+ }
378
+ // Esc is owned by the overlay (innermost-only). Here we only keep Tab inside
379
+ // the role="dialog" panel — the non-modal popover does not trap natively.
380
+ _onPanelKey(e) {
381
+ if (e.key !== "Tab")
382
+ return;
383
+ const items = this._panelFocusables();
384
+ if (items.length === 0) {
385
+ e.preventDefault();
386
+ return;
387
+ }
388
+ const first = items[0];
389
+ const last = items[items.length - 1];
390
+ const active = this.shadowRoot?.activeElement;
391
+ if (e.shiftKey && (active === first || active === this._panel)) {
392
+ e.preventDefault();
393
+ last.focus();
394
+ }
395
+ else if (!e.shiftKey && active === last) {
396
+ e.preventDefault();
397
+ first.focus();
398
+ }
399
+ }
400
+ _daysInMonth(view) {
401
+ return new Date(view.getFullYear(), view.getMonth() + 1, 0).getDate();
402
+ }
403
+ /* ---------- overlay reconcile ---------- */
404
+ willUpdate(changed) {
405
+ // Initialise the draft from the committed from/to whenever we open — covers
406
+ // both trigger-open and an externally set `open` (stories / preview hook).
407
+ if (changed.has("open") && this.open)
408
+ this._initDraft();
409
+ }
410
+ updated(changed) {
411
+ if (changed.has("open")) {
412
+ const ov = this._overlay;
413
+ const trigger = this._trigger;
414
+ if (this.open && ov && trigger) {
415
+ ov.anchor = trigger;
416
+ ov.opener = trigger;
417
+ ov.show();
418
+ requestAnimationFrame(() => {
419
+ if (this.open)
420
+ this._focusRovingDay();
421
+ });
422
+ }
423
+ else if (!this.open && ov?.open) {
424
+ ov.hide("api");
425
+ }
426
+ }
427
+ }
428
+ /* ---------- render ---------- */
429
+ render() {
430
+ const labelId = `${this._id}-title`;
431
+ const triggerCls = `date-range__trigger${this.open ? " date-range__trigger--open" : ""}`;
432
+ return html `
433
+ <link rel="stylesheet" href="${DR_CSS}" />
434
+ <style>
435
+ :host { display: inline-block; }
436
+ :host([hidden]) { display: none; }
437
+ </style>
438
+ <button
439
+ type="button"
440
+ class="${triggerCls}"
441
+ aria-haspopup="dialog"
442
+ aria-expanded="${this.open ? "true" : "false"}"
443
+ @click=${() => this._toggle()}
444
+ >
445
+ <span class="date-range__icon">${CAL_ICON()}</span>
446
+ ${this._renderTriggerValue()}
447
+ </button>
448
+
449
+ <xm-overlay
450
+ mode="non-modal"
451
+ tier="menu"
452
+ placement="bottom-start"
453
+ label="Date range"
454
+ @xm-overlay-close=${this._onOverlayClose}
455
+ >
456
+ ${this.open ? this._renderPanel(labelId) : nothing}
457
+ </xm-overlay>
458
+ `;
459
+ }
460
+ _renderTriggerValue() {
461
+ const start = fromISO(this.from);
462
+ const end = fromISO(this.to);
463
+ if (start && end) {
464
+ const label = `${SHORT_FMT.format(start)} – ${SHORT_FMT.format(end)}`;
465
+ return html `<span class="date-range__value">${label}</span>`;
466
+ }
467
+ return html `<span class="date-range__value date-range__value--placeholder"
468
+ >${this.placeholder}</span
469
+ >`;
470
+ }
471
+ _renderPanel(labelId) {
472
+ return html `
473
+ <div
474
+ class="date-range__panel"
475
+ role="dialog"
476
+ aria-labelledby="${labelId}"
477
+ @keydown=${(e) => this._onPanelKey(e)}
478
+ >
479
+ <span id="${labelId}" class="date-range__sr-only">Select a date range</span>
480
+ <div class="date-range__layout">
481
+ ${this._renderRail()} ${this._renderCalendar()}
482
+ </div>
483
+ ${this._invalid
484
+ ? html `<div
485
+ class="date-range__alert"
486
+ role="alert"
487
+ id="${labelId}-alert"
488
+ >
489
+ <span class="date-range__alert-icon">${ALERT_ICON()}</span>
490
+ <span>End date is before start date</span>
491
+ </div>`
492
+ : nothing}
493
+ ${this._renderFooter(labelId)}
494
+ <span class="date-range__sr-only" aria-live="polite"
495
+ >${this._liveMsg}</span
496
+ >
497
+ </div>
498
+ `;
499
+ }
500
+ _renderRail() {
501
+ return html `
502
+ <div
503
+ class="date-range__rail"
504
+ role="radiogroup"
505
+ aria-label="Date range presets"
506
+ >
507
+ ${PRESETS.map((p) => {
508
+ const active = this._activePreset === p.id;
509
+ const cls = `date-range__preset${active ? " date-range__preset--active" : ""}`;
510
+ return html `<button
511
+ type="button"
512
+ class="${cls}"
513
+ role="radio"
514
+ aria-checked="${active ? "true" : "false"}"
515
+ @click=${() => this._applyPreset(p.id)}
516
+ >
517
+ ${p.label}
518
+ </button>`;
519
+ })}
520
+ </div>
521
+ `;
522
+ }
523
+ _renderCalendar() {
524
+ return html `
525
+ <div class="date-range__calendar">
526
+ <div class="date-range__cal-head">
527
+ <button
528
+ type="button"
529
+ class="date-range__nav"
530
+ aria-label="Previous month"
531
+ @click=${() => this._shiftMonth(-1)}
532
+ >
533
+ ${CHEVRON_LEFT()}
534
+ </button>
535
+ <span class="date-range__month" aria-hidden="true"
536
+ >${MONTH_FMT.format(this._view)}</span
537
+ >
538
+ <button
539
+ type="button"
540
+ class="date-range__nav"
541
+ aria-label="Next month"
542
+ @click=${() => this._shiftMonth(1)}
543
+ >
544
+ ${CHEVRON_RIGHT()}
545
+ </button>
546
+ </div>
547
+ <div
548
+ class="date-range__grid"
549
+ role="grid"
550
+ aria-label="${MONTH_FMT.format(this._view)}"
551
+ @keydown=${(e) => this._onGridKey(e)}
552
+ >
553
+ <div class="date-range__weekdays" role="row">
554
+ ${WEEKDAYS.map((w) => html `<span
555
+ class="date-range__weekday"
556
+ role="columnheader"
557
+ aria-hidden="true"
558
+ >${w}</span
559
+ >`)}
560
+ </div>
561
+ ${this._renderWeeks()}
562
+ </div>
563
+ </div>
564
+ `;
565
+ }
566
+ _renderWeeks() {
567
+ const year = this._view.getFullYear();
568
+ const month = this._view.getMonth();
569
+ const first = new Date(year, month, 1);
570
+ // Monday-first offset (JS getDay: 0=Sun).
571
+ const lead = (first.getDay() + 6) % 7;
572
+ const gridStart = addDays(first, -lead);
573
+ const today = startOfDay(new Date());
574
+ const weeks = [];
575
+ // Six role="row" weeks of seven gridcells — the ARIA grid requires rows
576
+ // between the grid and its cells (the row wrappers are display:contents so
577
+ // the 7-column CSS grid is unaffected).
578
+ for (let w = 0; w < 6; w++) {
579
+ const cells = [];
580
+ for (let d = 0; d < 7; d++) {
581
+ const day = addDays(gridStart, w * 7 + d);
582
+ cells.push(this._renderDay(day, month, today));
583
+ }
584
+ weeks.push(html `<div class="date-range__week" role="row">${cells}</div>`);
585
+ }
586
+ return weeks;
587
+ }
588
+ _renderDay(day, viewMonth, today) {
589
+ const inMonth = day.getMonth() === viewMonth;
590
+ const isToday = sameDay(day, today);
591
+ const isFocus = sameDay(day, this._focusDay);
592
+ const start = this._draftFrom;
593
+ const end = this._draftTo;
594
+ const isStart = start && sameDay(day, start);
595
+ const isEnd = end && sameDay(day, end);
596
+ const selected = !!(isStart || isEnd);
597
+ const inRange = !!(start && end && day > start && day < end);
598
+ const cellCls = [
599
+ "date-range__cell",
600
+ inRange && "date-range__cell--in-range",
601
+ isStart && end && "date-range__cell--start",
602
+ isEnd && start && "date-range__cell--end",
603
+ ]
604
+ .filter(Boolean)
605
+ .join(" ");
606
+ const dayCls = [
607
+ "date-range__day",
608
+ !inMonth && "date-range__day--muted",
609
+ isToday && "date-range__day--today",
610
+ selected && "date-range__day--selected",
611
+ ]
612
+ .filter(Boolean)
613
+ .join(" ");
614
+ return html `
615
+ <div
616
+ class="${cellCls}"
617
+ role="gridcell"
618
+ aria-selected="${selected ? "true" : "false"}"
619
+ >
620
+ <button
621
+ type="button"
622
+ class="${dayCls}"
623
+ tabindex="${isFocus ? "0" : "-1"}"
624
+ aria-label="${FULL_FMT.format(day)}"
625
+ aria-current="${isToday ? "date" : nothing}"
626
+ @click=${() => this._pickDay(day)}
627
+ >
628
+ ${day.getDate()}
629
+ </button>
630
+ </div>
631
+ `;
632
+ }
633
+ _renderFooter(labelId) {
634
+ const canApply = !!(this._draftFrom && this._draftTo) && !this._invalid;
635
+ return html `
636
+ <div class="date-range__footer">
637
+ <button
638
+ type="button"
639
+ class="date-range__btn"
640
+ @click=${() => this._clear()}
641
+ >
642
+ Clear
643
+ </button>
644
+ <button
645
+ type="button"
646
+ class="date-range__btn date-range__btn--apply"
647
+ aria-disabled="${canApply ? "false" : "true"}"
648
+ aria-describedby="${this._invalid ? `${labelId}-alert` : nothing}"
649
+ @click=${() => this._apply()}
650
+ >
651
+ Apply
652
+ </button>
653
+ </div>
654
+ `;
655
+ }
656
+ };
657
+ __decorate([
658
+ property({ type: String })
659
+ ], XmDateRange.prototype, "from", void 0);
660
+ __decorate([
661
+ property({ type: String })
662
+ ], XmDateRange.prototype, "to", void 0);
663
+ __decorate([
664
+ property({ type: String })
665
+ ], XmDateRange.prototype, "placeholder", void 0);
666
+ __decorate([
667
+ property({ type: Boolean, reflect: true })
668
+ ], XmDateRange.prototype, "open", void 0);
669
+ __decorate([
670
+ state()
671
+ ], XmDateRange.prototype, "_id", void 0);
672
+ __decorate([
673
+ state()
674
+ ], XmDateRange.prototype, "_draftFrom", void 0);
675
+ __decorate([
676
+ state()
677
+ ], XmDateRange.prototype, "_draftTo", void 0);
678
+ __decorate([
679
+ state()
680
+ ], XmDateRange.prototype, "_view", void 0);
681
+ __decorate([
682
+ state()
683
+ ], XmDateRange.prototype, "_focusDay", void 0);
684
+ __decorate([
685
+ state()
686
+ ], XmDateRange.prototype, "_liveMsg", void 0);
687
+ __decorate([
688
+ state()
689
+ ], XmDateRange.prototype, "_activePreset", void 0);
690
+ __decorate([
691
+ query("xm-overlay")
692
+ ], XmDateRange.prototype, "_overlay", void 0);
693
+ __decorate([
694
+ query(".date-range__trigger")
695
+ ], XmDateRange.prototype, "_trigger", void 0);
696
+ __decorate([
697
+ query(".date-range__panel")
698
+ ], XmDateRange.prototype, "_panel", void 0);
699
+ XmDateRange = __decorate([
700
+ customElement("xm-date-range")
701
+ ], XmDateRange);
702
+ export { XmDateRange };