@aquera/nile-elements 2.0.18 → 2.0.20

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 (138) hide show
  1. package/README.md +10 -0
  2. package/demo/index.js +0 -1
  3. package/dist/index.cjs.js +1 -1
  4. package/dist/index.esm.js +1 -1
  5. package/dist/index.js +1275 -490
  6. package/dist/nile-badge/index.cjs.js +1 -1
  7. package/dist/nile-badge/index.esm.js +1 -1
  8. package/dist/nile-badge/nile-badge.cjs.js +1 -1
  9. package/dist/nile-badge/nile-badge.cjs.js.map +1 -1
  10. package/dist/nile-badge/nile-badge.css.cjs.js +1 -1
  11. package/dist/nile-badge/nile-badge.css.cjs.js.map +1 -1
  12. package/dist/nile-badge/nile-badge.css.esm.js +24 -3
  13. package/dist/nile-badge/nile-badge.esm.js +5 -3
  14. package/dist/nile-breadcrumb/nile-breadcrumb.cjs.js +1 -1
  15. package/dist/nile-breadcrumb/nile-breadcrumb.cjs.js.map +1 -1
  16. package/dist/nile-breadcrumb/nile-breadcrumb.esm.js +2 -2
  17. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js +1 -1
  18. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.cjs.js.map +1 -1
  19. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.cjs.js +1 -1
  20. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.cjs.js.map +1 -1
  21. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.css.esm.js +14 -2
  22. package/dist/nile-breadcrumb-item/nile-breadcrumb-item.esm.js +4 -4
  23. package/dist/nile-calendar/index.cjs.js +1 -1
  24. package/dist/nile-calendar/index.esm.js +1 -1
  25. package/dist/nile-calendar/nile-calendar.cjs.js +2 -1
  26. package/dist/nile-calendar/nile-calendar.cjs.js.map +1 -1
  27. package/dist/nile-calendar/nile-calendar.css.cjs.js +1 -1
  28. package/dist/nile-calendar/nile-calendar.css.cjs.js.map +1 -1
  29. package/dist/nile-calendar/nile-calendar.css.esm.js +357 -2
  30. package/dist/nile-calendar/nile-calendar.esm.js +365 -41
  31. package/dist/nile-calendar/timezones.cjs.js +1 -1
  32. package/dist/nile-calendar/timezones.cjs.js.map +1 -1
  33. package/dist/nile-calendar/timezones.esm.js +1 -1
  34. package/dist/nile-date-time-picker/index.cjs.js +2 -0
  35. package/dist/nile-date-time-picker/index.cjs.js.map +1 -0
  36. package/dist/nile-date-time-picker/index.esm.js +1 -0
  37. package/dist/nile-date-time-picker/nile-date-time-picker.cjs.js +2 -0
  38. package/dist/nile-date-time-picker/nile-date-time-picker.cjs.js.map +1 -0
  39. package/dist/nile-date-time-picker/nile-date-time-picker.css.cjs.js +2 -0
  40. package/dist/nile-date-time-picker/nile-date-time-picker.css.cjs.js.map +1 -0
  41. package/dist/nile-date-time-picker/nile-date-time-picker.css.esm.js +13 -0
  42. package/dist/nile-date-time-picker/nile-date-time-picker.esm.js +53 -0
  43. package/dist/nile-select/index.cjs.js +1 -1
  44. package/dist/nile-select/index.esm.js +1 -1
  45. package/dist/nile-select/nile-select.cjs.js +2 -2
  46. package/dist/nile-select/nile-select.cjs.js.map +1 -1
  47. package/dist/nile-select/nile-select.esm.js +1 -1
  48. package/dist/nile-select/virtual-scroll-helper.cjs.js +1 -1
  49. package/dist/nile-select/virtual-scroll-helper.cjs.js.map +1 -1
  50. package/dist/nile-select/virtual-scroll-helper.esm.js +48 -47
  51. package/dist/nile-virtual-select/nile-virtual-select.cjs.js +4 -4
  52. package/dist/nile-virtual-select/nile-virtual-select.cjs.js.map +1 -1
  53. package/dist/nile-virtual-select/nile-virtual-select.esm.js +2 -2
  54. package/dist/src/index.d.ts +1 -0
  55. package/dist/src/index.js +1 -0
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/nile-badge/nile-badge.css.js +24 -3
  58. package/dist/src/nile-badge/nile-badge.css.js.map +1 -1
  59. package/dist/src/nile-badge/nile-badge.d.ts +7 -4
  60. package/dist/src/nile-badge/nile-badge.js +22 -5
  61. package/dist/src/nile-badge/nile-badge.js.map +1 -1
  62. package/dist/src/nile-badge/nile-badge.test.js +88 -0
  63. package/dist/src/nile-badge/nile-badge.test.js.map +1 -1
  64. package/dist/src/nile-breadcrumb/nile-breadcrumb.d.ts +7 -0
  65. package/dist/src/nile-breadcrumb/nile-breadcrumb.js +24 -1
  66. package/dist/src/nile-breadcrumb/nile-breadcrumb.js.map +1 -1
  67. package/dist/src/nile-breadcrumb/nile-breadcrumb.test.js +51 -0
  68. package/dist/src/nile-breadcrumb/nile-breadcrumb.test.js.map +1 -1
  69. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.css.js +12 -0
  70. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.css.js.map +1 -1
  71. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.d.ts +5 -0
  72. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js +9 -0
  73. package/dist/src/nile-breadcrumb-item/nile-breadcrumb-item.js.map +1 -1
  74. package/dist/src/nile-calendar/nile-calendar.css.js +355 -0
  75. package/dist/src/nile-calendar/nile-calendar.css.js.map +1 -1
  76. package/dist/src/nile-calendar/nile-calendar.d.ts +175 -0
  77. package/dist/src/nile-calendar/nile-calendar.js +1726 -26
  78. package/dist/src/nile-calendar/nile-calendar.js.map +1 -1
  79. package/dist/src/nile-calendar/nile-calendar.test.d.ts +1 -0
  80. package/dist/src/nile-calendar/nile-calendar.test.js +3580 -0
  81. package/dist/src/nile-calendar/nile-calendar.test.js.map +1 -1
  82. package/dist/src/nile-calendar/timezones.d.ts +21 -0
  83. package/dist/src/nile-calendar/timezones.js +49 -0
  84. package/dist/src/nile-calendar/timezones.js.map +1 -1
  85. package/dist/src/nile-date-time-picker/index.d.ts +1 -0
  86. package/dist/src/nile-date-time-picker/index.js +2 -0
  87. package/dist/src/nile-date-time-picker/index.js.map +1 -0
  88. package/dist/src/nile-date-time-picker/nile-date-time-picker.css.d.ts +11 -0
  89. package/dist/src/nile-date-time-picker/nile-date-time-picker.css.js +49 -0
  90. package/dist/src/nile-date-time-picker/nile-date-time-picker.css.js.map +1 -0
  91. package/dist/src/nile-date-time-picker/nile-date-time-picker.d.ts +173 -0
  92. package/dist/src/nile-date-time-picker/nile-date-time-picker.js +1270 -0
  93. package/dist/src/nile-date-time-picker/nile-date-time-picker.js.map +1 -0
  94. package/dist/src/nile-date-time-picker/nile-date-time-picker.test.d.ts +143 -0
  95. package/dist/src/nile-date-time-picker/nile-date-time-picker.test.js +7277 -0
  96. package/dist/src/nile-date-time-picker/nile-date-time-picker.test.js.map +1 -0
  97. package/dist/src/nile-select/nile-select.d.ts +1 -0
  98. package/dist/src/nile-select/nile-select.js +16 -1
  99. package/dist/src/nile-select/nile-select.js.map +1 -1
  100. package/dist/src/nile-select/nile-select.test.js +430 -5
  101. package/dist/src/nile-select/nile-select.test.js.map +1 -1
  102. package/dist/src/nile-select/virtual-scroll-helper.js +7 -4
  103. package/dist/src/nile-select/virtual-scroll-helper.js.map +1 -1
  104. package/dist/src/nile-virtual-select/nile-virtual-select.d.ts +3 -1
  105. package/dist/src/nile-virtual-select/nile-virtual-select.js +14 -6
  106. package/dist/src/nile-virtual-select/nile-virtual-select.js.map +1 -1
  107. package/dist/src/nile-virtual-select/nile-virtual-select.test.js +115 -1
  108. package/dist/src/nile-virtual-select/nile-virtual-select.test.js.map +1 -1
  109. package/dist/src/version.js +1 -1
  110. package/dist/src/version.js.map +1 -1
  111. package/dist/tsconfig.tsbuildinfo +1 -1
  112. package/dist/utilities/animation-registry.cjs.js +1 -1
  113. package/dist/utilities/animation-registry.cjs.js.map +1 -1
  114. package/dist/utilities/animation-registry.esm.js +1 -1
  115. package/package.json +3 -1
  116. package/scripts/coverage-report.mjs +96 -0
  117. package/src/index.ts +1 -0
  118. package/src/nile-badge/nile-badge.css.ts +24 -3
  119. package/src/nile-badge/nile-badge.test.ts +102 -0
  120. package/src/nile-badge/nile-badge.ts +28 -5
  121. package/src/nile-breadcrumb/nile-breadcrumb.test.ts +57 -0
  122. package/src/nile-breadcrumb/nile-breadcrumb.ts +19 -0
  123. package/src/nile-breadcrumb-item/nile-breadcrumb-item.css.ts +12 -0
  124. package/src/nile-breadcrumb-item/nile-breadcrumb-item.ts +7 -0
  125. package/src/nile-calendar/nile-calendar.css.ts +355 -0
  126. package/src/nile-calendar/nile-calendar.test.ts +3793 -0
  127. package/src/nile-calendar/nile-calendar.ts +1760 -28
  128. package/src/nile-calendar/timezones.ts +57 -0
  129. package/src/nile-date-time-picker/index.ts +1 -0
  130. package/src/nile-date-time-picker/nile-date-time-picker.css.ts +54 -0
  131. package/src/nile-date-time-picker/nile-date-time-picker.test.ts +7886 -0
  132. package/src/nile-date-time-picker/nile-date-time-picker.ts +1153 -0
  133. package/src/nile-select/nile-select.test.ts +560 -5
  134. package/src/nile-select/nile-select.ts +21 -1
  135. package/src/nile-select/virtual-scroll-helper.ts +7 -4
  136. package/src/nile-virtual-select/nile-virtual-select.test.ts +139 -1
  137. package/src/nile-virtual-select/nile-virtual-select.ts +17 -6
  138. package/vscode-html-custom-data.json +200 -20
@@ -12,11 +12,148 @@ import { classMap } from 'lit/directives/class-map.js';
12
12
  import { watch } from '../internal/watch';
13
13
  import NileElement from '../internal/nile-element';
14
14
  import { ClearMode } from './nile-calendar.enum';
15
+ import { TIMEZONES, isValidTimezone, isClockTime, utcFromFields, localDateFromFields, formatYear4, DEFAULT_START_YEAR, DEFAULT_END_YEAR } from './timezones';
16
+ import { setAnimation } from '../utilities/animation-registry';
17
+ /* one exact moment on the timeline Eg: 2026-03-01T10:00:00Z, …+05:30 */
18
+ const INSTANT_VALUE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(?:\.\d{1,3})?(?:Z|[+-]\d{2}:?\d{2})$/;
19
+ /** an instant plus the zone it was picked in. Eg: 2026-03-01T10:00:00.000[GMT+05:30 Asia/Kolkata] */
20
+ const BRACKETED_VALUE_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?\[GMT([+-])(\d{2}):(\d{2})\s+([^\]]+)\]$/;
21
+ export function instantFromValueString(raw) {
22
+ if (typeof raw !== 'string')
23
+ return null;
24
+ const trimmed = raw.trim();
25
+ if (!INSTANT_VALUE_RE.test(trimmed))
26
+ return null;
27
+ const ms = new Date(trimmed).getTime();
28
+ return isNaN(ms) ? null : ms;
29
+ }
30
+ /** digits on a clock face, 2026-03-01T10:00:00 */
31
+ const OFFSETLESS_VALUE_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?$/;
32
+ /** Digits as written, with no zone conversion. */
33
+ export function wallClockFromOffsetLess(raw) {
34
+ if (typeof raw !== 'string')
35
+ return null;
36
+ const m = OFFSETLESS_VALUE_RE.exec(raw.trim());
37
+ if (!m)
38
+ return null;
39
+ const year = Number(m[1]);
40
+ const month0 = Number(m[2]) - 1;
41
+ const day = Number(m[3]);
42
+ if (month0 < 0 || month0 > 11 || day < 1)
43
+ return null;
44
+ if (!isClockTime(Number(m[4]), Number(m[5]), Number(m[6] ?? 0), Number(m[7] ?? 0)))
45
+ return null;
46
+ const lastDay = new Date(utcFromFields(year, month0 + 1, 0)).getUTCDate();
47
+ const t = utcFromFields(year, month0, Math.min(day, lastDay), Number(m[4]), Number(m[5]), Number(m[6] ?? 0), Number(m[7] ?? 0));
48
+ if (isNaN(t))
49
+ return null;
50
+ const u = new Date(t);
51
+ return {
52
+ year: u.getUTCFullYear(),
53
+ month: u.getUTCMonth() + 1,
54
+ day: u.getUTCDate(),
55
+ hours: u.getUTCHours(),
56
+ minutes: u.getUTCMinutes(),
57
+ seconds: u.getUTCSeconds(),
58
+ ms: u.getUTCMilliseconds(),
59
+ };
60
+ }
61
+ export function wallClockInZone(epochMs, zone) {
62
+ const formatter = new Intl.DateTimeFormat('en-US', {
63
+ timeZone: zone,
64
+ hourCycle: 'h23',
65
+ year: 'numeric', month: '2-digit', day: '2-digit',
66
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
67
+ });
68
+ const parts = {};
69
+ for (const part of formatter.formatToParts(new Date(epochMs))) {
70
+ if (part.type !== 'literal')
71
+ parts[part.type] = Number(part.value);
72
+ }
73
+ return {
74
+ year: parts.year,
75
+ month: parts.month,
76
+ day: parts.day,
77
+ hours: parts.hour === 24 ? 0 : parts.hour,
78
+ minutes: parts.minute,
79
+ seconds: parts.second,
80
+ ms: ((epochMs % 1000) + 1000) % 1000,
81
+ };
82
+ }
83
+ export function instantFromBracketedValue(raw) {
84
+ if (typeof raw !== 'string')
85
+ return null;
86
+ const m = BRACKETED_VALUE_RE.exec(raw.trim());
87
+ if (!m)
88
+ return null;
89
+ const [, y, mo, d, h, mi, s, ms, sign, oh, om, rawZone] = m;
90
+ const zone = rawZone.trim();
91
+ if (!isValidTimezone(zone))
92
+ return null;
93
+ const wall = {
94
+ year: Number(y), month: Number(mo), day: Number(d),
95
+ hours: Number(h), minutes: Number(mi), seconds: Number(s), ms: Number(ms ?? 0),
96
+ };
97
+ // ISO end-of-day: exactly 24:00:00(.000) is the next day's 00:00:00; any other hour 24 or above is not a time
98
+ const endOfDay = wall.hours === 24;
99
+ if (wall.hours > 24 || (endOfDay && (wall.minutes !== 0 || wall.seconds !== 0 || wall.ms !== 0)))
100
+ return null;
101
+ const offsetMs = (sign === '-' ? -1 : 1) * (Number(oh) * 3600000 + Number(om) * 60000);
102
+ const wallMs = utcFromFields(wall.year, wall.month - 1, wall.day, wall.hours, wall.minutes, wall.seconds, wall.ms);
103
+ const candidate = wallMs - offsetMs;
104
+ const readBack = wallClockInZone(candidate, zone);
105
+ // the digits the zone must read back: the written ones, or the rolled-over next day for 24:00:00
106
+ const expected = endOfDay ? wallClockInZone(wallMs, 'UTC') : wall;
107
+ const digitsAgree = readBack.year === expected.year && readBack.month === expected.month &&
108
+ readBack.day === expected.day && readBack.hours === expected.hours &&
109
+ readBack.minutes === expected.minutes && readBack.seconds === expected.seconds &&
110
+ readBack.ms === expected.ms;
111
+ if (!digitsAgree)
112
+ return null;
113
+ return { epochMs: candidate, timezone: zone };
114
+ }
15
115
  /**
16
116
  * Nile icon component.
17
117
  *
18
118
  * @tag nile-calendar
19
119
  *
120
+ * @csspart calendar-container - The outer container.
121
+ * @csspart calendar-switcher - The Absolute / Relative tab switcher.
122
+ * @csspart button-container - The default variant's button row.
123
+ * @csspart calendar-wrapper - Wraps the month grid.
124
+ * @csspart trigger - The month and year dropdown triggers.
125
+ * @csspart calendar-day-N - A day cell, where N is the day of the month.
126
+ * @csspart current-date-dot - The marker on today's date.
127
+ *
128
+ * Parts that exist only in `variant="date-time-picker"`:
129
+ * @csspart calendar-config - The row holding the type switcher.
130
+ * @csspart calendar - One month's header and grid.
131
+ * @csspart calendar-header - The month header row.
132
+ * @csspart calendar-header-prev - The previous-month icon.
133
+ * @csspart calendar-header-next - The next-month icon.
134
+ * @csspart calendar-header-center - The month and year area.
135
+ * @csspart days-container - The grid of day cells.
136
+ * @csspart day-name - A weekday label (Sun, Mon, ...).
137
+ * @csspart calendar-day-label - The number inside a day cell.
138
+ * @csspart tp-chevron - The expand / collapse control.
139
+ * @csspart tp-scroll-area - The scrolling area under the grid.
140
+ * @csspart tp-scroll-content - The content inside the scrolling area.
141
+ * @csspart tp-time-fields - The row of time inputs.
142
+ * @csspart tp-time-field - One time input (hours, minutes, seconds, milliseconds).
143
+ * @csspart tp-stepper - The up/down stepper inside a time input.
144
+ * @csspart tp-stepper-btn - Either stepper button.
145
+ * @csspart tp-stepper-btn-up - The increase button.
146
+ * @csspart tp-stepper-btn-down - The decrease button.
147
+ * @csspart tp-timezone - The timezone section.
148
+ * @csspart tp-timezone-header - The timezone label row.
149
+ * @csspart tp-timezone-label - The "Timezone" label.
150
+ * @csspart tp-timezone-local-link - The "Use local timezone" link.
151
+ * @csspart tp-timezone-select - The timezone select.
152
+ * @csspart tp-footer - The footer.
153
+ * @csspart tp-footer-actions - The Reset and Apply group.
154
+ * @csspart tp-set-to-now-button - The "Set to now" button.
155
+ * @csspart clear-button - The Reset button.
156
+ * @csspart apply-button - The Apply button.
20
157
  */
21
158
  let NileCalendar = class NileCalendar extends NileElement {
22
159
  constructor() {
@@ -25,6 +162,7 @@ let NileCalendar = class NileCalendar extends NileElement {
25
162
  this.allowedDates = JSON.stringify({});
26
163
  this.range = false;
27
164
  this.hideTypes = false;
165
+ this.showTypes = false;
28
166
  this.doubleClickUnselect = false;
29
167
  this.allowClear = false;
30
168
  this.type = 'absolute';
@@ -33,6 +171,7 @@ let NileCalendar = class NileCalendar extends NileElement {
33
171
  this.showMonthDropdown = false;
34
172
  this.showYearDropdown = false;
35
173
  this.dateFormat = 'MM/DD/YYYY';
174
+ this.structuredInput = false;
36
175
  this.startDate = null;
37
176
  this.endDate = null;
38
177
  this.isSelectingStart = true;
@@ -41,6 +180,145 @@ let NileCalendar = class NileCalendar extends NileElement {
41
180
  this.currentYear = new Date().getFullYear();
42
181
  this.allowedDatesLocal = null;
43
182
  this.disabled = false;
183
+ this.variant = 'default';
184
+ this.timezone = 'UTC';
185
+ /** the timezone the consumer configured; Reset restores this (internal changes never overwrite it) */
186
+ this.configuredTimezone = 'UTC';
187
+ /** the zone the component itself is assigning, so updated() can tell it from a consumer change */
188
+ this.internalTimezone = null;
189
+ this.hideTimeFields = [];
190
+ this.expanded = false;
191
+ this.tpLockedHeight = null;
192
+ this.tpHours = null;
193
+ this.tpMinutes = null;
194
+ this.tpSeconds = null;
195
+ this.tpMilliseconds = null;
196
+ this.tpFocusedTimeField = null;
197
+ /** the day the keyboard last moved to; owns the grid's single tab stop (roving tabindex) */
198
+ this.tpFocusDate = null;
199
+ this.tpEditingText = '';
200
+ this.seedInstant = null;
201
+ this.seedFieldsKey = null;
202
+ this.seedZone = null;
203
+ /** the `.base--date-time-picker` element the observer currently watches */
204
+ this.tpObservedBase = null;
205
+ this.maskValuesMap = new WeakMap();
206
+ this.boundMaskInputs = [];
207
+ this.onMaskKeydown = (e) => {
208
+ if (this.disabled || e.ctrlKey || e.metaKey || e.altKey)
209
+ return;
210
+ const nav = ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End',
211
+ 'Tab', 'Enter', 'Shift', 'Control', 'Meta', 'Alt', 'CapsLock'];
212
+ if (nav.includes(e.key))
213
+ return;
214
+ const nileInput = e.currentTarget;
215
+ const native = nileInput?.input;
216
+ if (!nileInput || !native)
217
+ return;
218
+ const kind = nileInput.classList.contains('time-input') ? 'time' : 'date';
219
+ const layout = this.maskLayoutFor(kind);
220
+ const values = this.maskValuesFor(native, layout);
221
+ const { slotPos } = this.computeMaskDisplay(values, layout);
222
+ const selStart = native.selectionStart ?? 0;
223
+ const selEnd = native.selectionEnd ?? selStart;
224
+ const writeMask = (display, caret) => {
225
+ native.value = display;
226
+ nileInput.value = display;
227
+ try {
228
+ native.setSelectionRange(caret, caret);
229
+ }
230
+ catch { /* noop */ }
231
+ };
232
+ const caretAfter = (target) => {
233
+ const after = this.computeMaskDisplay(values, layout);
234
+ const np = after.slotPos[target + 1];
235
+ if (np !== undefined && np !== -1)
236
+ return np;
237
+ return after.display.length;
238
+ };
239
+ if (e.key >= '0' && e.key <= '9') {
240
+ e.preventDefault();
241
+ let target = slotPos.findIndex(p => p !== -1 && p >= selStart);
242
+ if (target === -1)
243
+ target = values.findIndex(v => v === ''); // append at the first gap
244
+ if (target === -1)
245
+ return; // every slot full and caret past the end
246
+ values[target] = e.key;
247
+ this.clampMaskSegment(values, layout, target);
248
+ writeMask(this.computeMaskDisplay(values, layout).display, caretAfter(target));
249
+ return;
250
+ }
251
+ if (e.key === 'Backspace') {
252
+ e.preventDefault();
253
+ if (selStart !== selEnd) {
254
+ for (let i = 0; i < values.length; i++) {
255
+ if (slotPos[i] !== -1 && slotPos[i] >= selStart && slotPos[i] < selEnd)
256
+ values[i] = '';
257
+ }
258
+ writeMask(this.computeMaskDisplay(values, layout).display, selStart);
259
+ return;
260
+ }
261
+ let target = -1;
262
+ for (let i = 0; i < values.length; i++) {
263
+ if (values[i] !== '' && slotPos[i] !== -1 && slotPos[i] < selStart)
264
+ target = i;
265
+ }
266
+ if (target === -1)
267
+ return;
268
+ values[target] = '';
269
+ const after = this.computeMaskDisplay(values, layout);
270
+ writeMask(after.display, after.slotPos[target] !== -1 ? after.slotPos[target] : Math.max(0, selStart - 1));
271
+ return;
272
+ }
273
+ if (e.key === 'Delete') {
274
+ e.preventDefault();
275
+ let target = -1;
276
+ for (let i = 0; i < values.length; i++) {
277
+ if (values[i] !== '' && slotPos[i] !== -1 && slotPos[i] >= selStart) {
278
+ target = i;
279
+ break;
280
+ }
281
+ }
282
+ if (target === -1)
283
+ return;
284
+ values[target] = '';
285
+ const after = this.computeMaskDisplay(values, layout);
286
+ writeMask(after.display, after.slotPos[target] !== -1 ? after.slotPos[target] : selStart);
287
+ return;
288
+ }
289
+ if (e.key.length === 1)
290
+ e.preventDefault(); // block any other printable key
291
+ };
292
+ this.onMaskBlur = (e) => {
293
+ const nileInput = e.currentTarget;
294
+ const native = nileInput?.input;
295
+ if (!nileInput || !native)
296
+ return;
297
+ const kind = nileInput.classList.contains('time-input') ? 'time' : 'date';
298
+ const layout = this.maskLayoutFor(kind);
299
+ const values = this.maskValuesFor(native, layout);
300
+ if (!values.some(v => v !== '')) {
301
+ native.value = '';
302
+ nileInput.value = '';
303
+ nileInput.emit?.('nile-change', { value: '' });
304
+ return;
305
+ }
306
+ // left-pad any partially-typed segment, then clamp each into range
307
+ const segStarts = [...new Set(layout.slots.map(s => s.segStart))];
308
+ for (const start of segStarts) {
309
+ const idxs = layout.slots.map((s, i) => (s.segStart === start ? i : -1)).filter(i => i !== -1);
310
+ const digits = idxs.map(i => values[i]).filter(c => c !== '');
311
+ if (digits.length > 0 && digits.length < idxs.length) {
312
+ const padded = digits.join('').padStart(idxs.length, '0');
313
+ idxs.forEach((i, k) => { values[i] = padded[k]; });
314
+ }
315
+ }
316
+ segStarts.forEach(start => this.clampMaskSegment(values, layout, start));
317
+ const { display } = this.computeMaskDisplay(values, layout);
318
+ native.value = display;
319
+ nileInput.value = display;
320
+ nileInput.emit?.('nile-change', { value: display });
321
+ };
44
322
  }
45
323
  /**
46
324
  * The styles for NileCalendar
@@ -49,42 +327,182 @@ let NileCalendar = class NileCalendar extends NileElement {
49
327
  static get styles() {
50
328
  return [styles];
51
329
  }
330
+ setTimezoneInternally(tz) {
331
+ if (tz !== this.timezone)
332
+ this.internalTimezone = tz;
333
+ this.timezone = tz;
334
+ }
335
+ /** every field shown, minus the keys in hideTimeFields (a non-array value counts as an empty list) */
336
+ get resolvedTimeFields() {
337
+ const merged = {
338
+ hours: true,
339
+ minutes: true,
340
+ seconds: true,
341
+ milliseconds: true,
342
+ timezone: true,
343
+ };
344
+ const hidden = Array.isArray(this.hideTimeFields) ? this.hideTimeFields : [];
345
+ for (const key of hidden)
346
+ merged[key] = false;
347
+ return merged;
348
+ }
349
+ get tpFewFieldsNoTz() {
350
+ const f = this.resolvedTimeFields;
351
+ if (f.timezone !== false)
352
+ return false;
353
+ const timeCount = ['hours', 'minutes', 'seconds', 'milliseconds'].filter(k => f[k] !== false).length;
354
+ return timeCount <= 2;
355
+ }
52
356
  connectedCallback() {
53
357
  super.connectedCallback();
358
+ // before initializeValue: a zoned/bracketed value seeds the zone internally and must not become the configured one
359
+ if (!this.hasUpdated)
360
+ this.configuredTimezone = this.timezone;
54
361
  this.initializeValue();
362
+ // firstUpdated runs once; a re-attach (DOM move) must restore the structuredInput listeners disconnectedCallback removed (no-op unless structuredInput)
363
+ if (this.hasUpdated)
364
+ this.bindStructuredInput();
365
+ // disconnectedCallback dropped the observer; an already-rendered picker panel needs it back (updated() won't run on a bare re-attach)
366
+ if (this.hasUpdated && this.variant === 'date-time-picker')
367
+ this.observeCollapsedBase();
55
368
  this.emit('nile-init');
56
369
  }
57
370
  disconnectedCallback() {
58
371
  super.disconnectedCallback();
372
+ this.releasePanelObserver();
373
+ this.unbindStructuredInput();
59
374
  this.emit('nile-destroy');
60
375
  }
376
+ firstUpdated() {
377
+ this.bindStructuredInput();
378
+ }
61
379
  updated(changedProperties) {
62
380
  super.updated(changedProperties);
381
+ if (changedProperties.has('timezone')) {
382
+ if (this.timezone !== this.internalTimezone)
383
+ this.configuredTimezone = this.timezone;
384
+ this.internalTimezone = null;
385
+ }
386
+ if (changedProperties.has('timezone') &&
387
+ !changedProperties.has('valueAttribute') &&
388
+ this.seedInstant !== null) {
389
+ if (this.seedZone !== null) {
390
+ if (this.timezone !== this.seedZone)
391
+ this.clearInstantSeed();
392
+ }
393
+ else if (this.seedFieldsKey === this.instantSeedFieldsKey()) {
394
+ if (isValidTimezone(this.timezone)) {
395
+ this.seedFromInstant(this.seedInstant, this.timezone);
396
+ }
397
+ else {
398
+ this.clearInstantSeed();
399
+ this.initializeValue();
400
+ }
401
+ }
402
+ else {
403
+ this.clearInstantSeed();
404
+ }
405
+ }
406
+ if (changedProperties.has('structuredInput') ||
407
+ changedProperties.has('variant') ||
408
+ changedProperties.has('range') ||
409
+ changedProperties.has('showManualInputs') ||
410
+ changedProperties.has('type') ||
411
+ changedProperties.has('dateFormat')) {
412
+ this.bindStructuredInput();
413
+ }
63
414
  if (changedProperties.has('valueAttribute')) {
64
415
  if (!this.valueAttribute) {
65
416
  this.value = null;
66
417
  this.startDate = null;
67
418
  this.endDate = null;
68
419
  this.isSelectingStart = true;
420
+ if (this.variant === 'date-time-picker')
421
+ this.resetTimeUnits();
69
422
  }
70
- const date = new Date(this.valueAttribute || '');
423
+ const date = this.dateFromValueString(this.valueAttribute || '');
71
424
  if (!isNaN(date.getTime())) {
72
- const offset = date.getTimezoneOffset();
73
- this.value = new Date(date.getTime() - offset * 60 * 1000);
425
+ this.value = date;
74
426
  this.currentMonth = this.value.getMonth();
75
427
  this.currentYear = this.value.getFullYear();
76
428
  }
77
429
  this.initializeValue();
78
430
  }
431
+ if (this.variant === 'date-time-picker') {
432
+ this.applyMonthYearDropdownAnimation();
433
+ this.captureCollapsedHeight();
434
+ }
435
+ else {
436
+ this.releasePanelObserver(); // the default variant has no picker panel to watch (a variant switch removed it)
437
+ }
438
+ }
439
+ releasePanelObserver() {
440
+ this.tpResizeObserver?.disconnect();
441
+ this.tpResizeObserver = undefined;
442
+ this.tpObservedBase = null;
443
+ }
444
+ /** (re)points the observer at the rendered panel; a no-op while it already watches that element */
445
+ observeCollapsedBase() {
446
+ const base = this.shadowRoot?.querySelector('.base--date-time-picker');
447
+ if (!base) {
448
+ this.releasePanelObserver();
449
+ return null;
450
+ }
451
+ if (this.tpObservedBase !== base) {
452
+ this.tpResizeObserver?.disconnect();
453
+ this.tpResizeObserver = new ResizeObserver(() => this.measurePanel(base));
454
+ this.tpResizeObserver.observe(base);
455
+ this.tpObservedBase = base;
456
+ }
457
+ return base;
458
+ }
459
+ captureCollapsedHeight() {
460
+ const base = this.observeCollapsedBase();
461
+ if (base)
462
+ this.measurePanel(base);
463
+ }
464
+ measurePanel(base) {
465
+ if (this.expanded)
466
+ return;
467
+ const h = `${base.offsetHeight}px`;
468
+ if (h !== this.tpLockedHeight)
469
+ this.tpLockedHeight = h;
470
+ }
471
+ applyMonthYearDropdownAnimation() {
472
+ const apply = (root) => {
473
+ root?.querySelectorAll('.month-dropdown, .year-dropdown').forEach(dd => {
474
+ setAnimation(dd, 'dropdown.show', {
475
+ keyframes: [
476
+ { opacity: 0, scale: 0.9, translate: '0 -14px' },
477
+ { opacity: 1, scale: 1, translate: '0 0' },
478
+ ],
479
+ options: { duration: 280, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' },
480
+ });
481
+ setAnimation(dd, 'dropdown.hide', {
482
+ keyframes: [
483
+ { opacity: 1, scale: 1, translate: '0 0' },
484
+ { opacity: 0, scale: 0.9, translate: '0 -14px' },
485
+ ],
486
+ options: { duration: 160, easing: 'ease' },
487
+ });
488
+ });
489
+ };
490
+ apply(this.shadowRoot);
491
+ const inner = this.shadowRoot?.querySelector('.tp-inner-calendar');
492
+ if (inner) {
493
+ const innerApply = () => apply(inner.shadowRoot);
494
+ if (inner.updateComplete)
495
+ inner.updateComplete.then(innerApply);
496
+ else
497
+ innerApply();
498
+ }
79
499
  }
80
500
  get monthNames() {
81
501
  return Array.from({ length: 12 }, (_, i) => new Date(0, i).toLocaleString('default', { month: 'long' }));
82
502
  }
83
503
  get yearOptions() {
84
- const fallbackStart = 2000;
85
- const fallbackEnd = 2050;
86
- const start = this.startYear ?? fallbackStart;
87
- const end = this.endYear ?? fallbackEnd;
504
+ const start = this.startYear ?? DEFAULT_START_YEAR;
505
+ const end = this.endYear ?? DEFAULT_END_YEAR;
88
506
  if (start > end)
89
507
  return [];
90
508
  return Array.from({ length: end - start + 1 }, (_, i) => start + i);
@@ -97,6 +515,42 @@ let NileCalendar = class NileCalendar extends NileElement {
97
515
  this.currentYear = year;
98
516
  this.emit('nile-year-change', { year });
99
517
  }
518
+ /** brings the currently-selected month/year option into view when a dropdown opens */
519
+ scrollSelectedIntoView(selector) {
520
+ const selected = this.shadowRoot?.querySelector(selector);
521
+ if (!selected)
522
+ return;
523
+ const container = selected.closest('.month-scroll-wrapper, .year-scroll-wrapper');
524
+ if (!container) {
525
+ selected.scrollIntoView({ block: 'center' });
526
+ return;
527
+ }
528
+ const target = container.scrollTop +
529
+ (selected.getBoundingClientRect().top -
530
+ container.getBoundingClientRect().top) -
531
+ (container.clientHeight - selected.clientHeight) / 2;
532
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
533
+ if (prefersReducedMotion) {
534
+ container.scrollTop = target;
535
+ return;
536
+ }
537
+ this.smoothScrollTo(container, target, 450);
538
+ }
539
+ smoothScrollTo(el, to, duration) {
540
+ const start = el.scrollTop;
541
+ const change = to - start;
542
+ if (change === 0)
543
+ return;
544
+ const startTime = performance.now();
545
+ const ease = (t) => t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
546
+ const step = (now) => {
547
+ const t = Math.min(1, (now - startTime) / duration);
548
+ el.scrollTop = start + change * ease(t);
549
+ if (t < 1)
550
+ requestAnimationFrame(step);
551
+ };
552
+ requestAnimationFrame(step);
553
+ }
100
554
  checkValidAllowedDate() {
101
555
  let newDateRange;
102
556
  try {
@@ -119,15 +573,25 @@ let NileCalendar = class NileCalendar extends NileElement {
119
573
  this.allowedDatesLocal = newDateRange;
120
574
  }
121
575
  }
576
+ /**
577
+ * The year range month navigation enforces. An explicit `startYear` / `endYear` always wins; the date-time-picker
578
+ * variant falls back to the dropdown's 2000-2050 for an unset side, while the default calendar leaves it open as published.
579
+ */
580
+ get minYear() {
581
+ return this.variant === 'date-time-picker' ? this.startYear ?? DEFAULT_START_YEAR : this.startYear;
582
+ }
583
+ get maxYear() {
584
+ return this.variant === 'date-time-picker' ? this.endYear ?? DEFAULT_END_YEAR : this.endYear;
585
+ }
122
586
  isPrevDisabled() {
123
587
  return (this.currentMonth === 0 &&
124
- this.startYear !== undefined &&
125
- this.currentYear <= this.startYear);
588
+ this.minYear !== undefined &&
589
+ this.currentYear <= this.minYear);
126
590
  }
127
591
  isNextDisabled() {
128
592
  return (this.currentMonth === 11 &&
129
- this.endYear !== undefined &&
130
- this.currentYear >= this.endYear);
593
+ this.maxYear !== undefined &&
594
+ this.currentYear >= this.maxYear);
131
595
  }
132
596
  formatDate(date) {
133
597
  if (!date)
@@ -136,7 +600,8 @@ let NileCalendar = class NileCalendar extends NileElement {
136
600
  const mm = String(date.getMonth() + 1).padStart(2, '0');
137
601
  const dd = String(date.getDate()).padStart(2, '0');
138
602
  let formatted = this.dateFormat;
139
- formatted = formatted.replace(/YYYY/g, String(yyyy));
603
+ // the date-time-picker variant pads years 0-99 to four digits; the default calendar prints the year as published
604
+ formatted = formatted.replace(/YYYY/g, this.variant === 'date-time-picker' ? formatYear4(yyyy) : String(yyyy));
140
605
  formatted = formatted.replace(/MM/g, mm);
141
606
  formatted = formatted.replace(/DD/g, dd);
142
607
  formatted = formatted.replace(/YY/g, String(yyyy).slice(-2));
@@ -154,10 +619,17 @@ let NileCalendar = class NileCalendar extends NileElement {
154
619
  const match = input.match(regex);
155
620
  if (!match || !match.groups)
156
621
  return null;
157
- const year = match.groups.year.length === 2 ? Number(`20${match.groups.year}`) : Number(match.groups.year);
622
+ const dtp = this.variant === 'date-time-picker';
623
+ const typedYear = match.groups.year;
624
+ let year = typedYear.length === 2 ? Number(`20${typedYear}`) : Number(typedYear);
625
+ // date-time-picker only: an existing bound keeps its century while the visible two digits are unchanged; with no existing bound a typed YY is 2000-2099 (the default calendar reads it as published)
626
+ if (dtp && typedYear.length === 2 && existingDate && Math.abs(existingDate.getFullYear()) % 100 === Number(typedYear)) {
627
+ year = existingDate.getFullYear();
628
+ }
158
629
  const month = Number(match.groups.month) - 1;
159
630
  const day = Number(match.groups.day);
160
- const date = new Date(year, month, day);
631
+ // new Date(y, ...) remaps years 0-99 to 1900-1999 as published; only the variant keeps the literal year
632
+ const date = dtp ? localDateFromFields(year, month, day) : new Date(year, month, day);
161
633
  if (existingDate) {
162
634
  date.setHours(existingDate.getHours(), existingDate.getMinutes(), existingDate.getSeconds());
163
635
  }
@@ -188,10 +660,131 @@ let NileCalendar = class NileCalendar extends NileElement {
188
660
  this.requestUpdate();
189
661
  }
190
662
  }
663
+ unbindStructuredInput() {
664
+ for (const inp of this.boundMaskInputs) {
665
+ inp.removeEventListener('keydown', this.onMaskKeydown, true);
666
+ inp.removeEventListener('focusout', this.onMaskBlur);
667
+ }
668
+ this.boundMaskInputs = [];
669
+ }
670
+ bindStructuredInput() {
671
+ this.unbindStructuredInput();
672
+ if (!this.structuredInput)
673
+ return;
674
+ const inputs = this.shadowRoot?.querySelectorAll('.calender-input nile-input');
675
+ inputs?.forEach(inp => {
676
+ inp.addEventListener('keydown', this.onMaskKeydown, true);
677
+ inp.addEventListener('focusout', this.onMaskBlur);
678
+ this.boundMaskInputs.push(inp);
679
+ });
680
+ }
681
+ dateMaskSegments() {
682
+ const spec = (token) => {
683
+ switch (token) {
684
+ case 'YYYY': return { kind: 'year', len: 4, min: 0, max: 9999 };
685
+ case 'YY': return { kind: 'year', len: 2, min: 0, max: 99 };
686
+ case 'MM': return { kind: 'month', len: 2, min: 1, max: 12 };
687
+ default: return { kind: 'day', len: 2, min: 1, max: 31 }; // DD
688
+ }
689
+ };
690
+ const fmt = this.dateFormat || 'MM/DD/YYYY';
691
+ const re = /YYYY|YY|MM|DD/g;
692
+ const found = [];
693
+ let m;
694
+ while ((m = re.exec(fmt)) !== null)
695
+ found.push({ token: m[0], index: m.index });
696
+ return found.map((f, i) => {
697
+ const sep = fmt.slice(f.index + f.token.length, i + 1 < found.length ? found[i + 1].index : fmt.length);
698
+ return { ...spec(f.token), after: i < found.length - 1 ? sep : '' };
699
+ });
700
+ }
701
+ timeMaskSegments() {
702
+ return [
703
+ { kind: 'hour', len: 2, min: 0, max: 23, after: ':' },
704
+ { kind: 'minute', len: 2, min: 0, max: 59, after: ':' },
705
+ { kind: 'second', len: 2, min: 0, max: 59, after: '' },
706
+ ];
707
+ }
708
+ buildMaskLayout(segs) {
709
+ const tokens = [];
710
+ const slots = [];
711
+ for (const seg of segs) {
712
+ const segStart = slots.length;
713
+ for (let k = 0; k < seg.len; k++) {
714
+ tokens.push({ slot: slots.length });
715
+ slots.push({ kind: seg.kind, min: seg.min, max: seg.max, len: seg.len, segStart });
716
+ }
717
+ for (const ch of seg.after)
718
+ tokens.push({ lit: ch });
719
+ }
720
+ return { tokens, slots, length: tokens.length };
721
+ }
722
+ maskLayoutFor(kind) {
723
+ return this.buildMaskLayout(kind === 'time' ? this.timeMaskSegments() : this.dateMaskSegments());
724
+ }
725
+ computeMaskDisplay(values, layout) {
726
+ let lastFilled = -1;
727
+ values.forEach((v, i) => { if (v !== '')
728
+ lastFilled = i; });
729
+ let s = '';
730
+ const slotPos = new Array(layout.slots.length).fill(-1);
731
+ for (const t of layout.tokens) {
732
+ if ('slot' in t) {
733
+ if (t.slot > lastFilled)
734
+ break; // stop at the trailing empties
735
+ slotPos[t.slot] = s.length;
736
+ if (values[t.slot])
737
+ s += values[t.slot];
738
+ }
739
+ else {
740
+ s += t.lit;
741
+ }
742
+ }
743
+ return { display: lastFilled === -1 ? '' : s, slotPos, lastFilled };
744
+ }
745
+ initMaskValues(text, layout) {
746
+ const digits = (text || '').replace(/\D/g, '').slice(0, layout.slots.length);
747
+ return layout.slots.map((_s, i) => digits[i] ?? '');
748
+ }
749
+ /** clamp a filled segment into range (year is left unclamped) */
750
+ clampMaskSegment(values, layout, slotIndex) {
751
+ const seg = layout.slots[slotIndex];
752
+ if (seg.kind === 'year')
753
+ return;
754
+ const segSlots = [];
755
+ layout.slots.forEach((s, i) => { if (s.segStart === seg.segStart)
756
+ segSlots.push(i); });
757
+ const chars = segSlots.map(i => values[i]);
758
+ if (chars.some(c => c === ''))
759
+ return; // segment not full yet
760
+ const joined = chars.join('');
761
+ const n = Number(joined);
762
+ let corrected = null;
763
+ if (n > seg.max)
764
+ corrected = String(Number(joined[0])).padStart(seg.len, '0');
765
+ else if (n < seg.min)
766
+ corrected = String(seg.min).padStart(seg.len, '0');
767
+ if (corrected !== null)
768
+ segSlots.forEach((s, k) => { values[s] = corrected[k]; });
769
+ }
770
+ maskValuesFor(native, layout) {
771
+ let values = this.maskValuesMap.get(native);
772
+ if (!values || values.length !== layout.slots.length) {
773
+ values = this.initMaskValues(native.value, layout);
774
+ }
775
+ else if (this.computeMaskDisplay(values, layout).display !== native.value) {
776
+ values = this.initMaskValues(native.value, layout);
777
+ }
778
+ this.maskValuesMap.set(native, values);
779
+ return values;
780
+ }
191
781
  /**
192
782
  * Render method
193
783
  */
194
784
  render() {
785
+ if (this.variant === 'date-time-picker') {
786
+ return this.renderDateTimePicker();
787
+ }
195
788
  return html `
196
789
  <div
197
790
  class=${classMap({
@@ -357,7 +950,37 @@ let NileCalendar = class NileCalendar extends NileElement {
357
950
  </div>
358
951
  </div>
359
952
 
360
- ${this.range ? html `
953
+ ${this.variant === 'date-time-picker' ? html `
954
+ <div>
955
+ <div class="unit-input-container">
956
+ <nile-input
957
+ class="manual-input duration-input"
958
+ ?disabled="${this.disabled}"
959
+ label="Duration"
960
+ inputmode="numeric"
961
+ type="text"
962
+ value="${this.selectedValue}"
963
+ @nile-input="${this.handleDurationChange}"
964
+ @nile-change="${this.handleDurationChange}"
965
+ placeholder="Enter Value"
966
+ ></nile-input>
967
+
968
+ <nile-select
969
+ class="manual-input time-input"
970
+ ?disabled="${this.disabled}"
971
+ label="Unit of time"
972
+ value="${this.selectedUnit}"
973
+ @nile-change="${this.handleUnitChange}"
974
+ >
975
+ ${this.hideDurationFields?.includes('minutes') ? '' : html `<nile-option value="minutes">Minutes</nile-option>`}
976
+ ${this.hideDurationFields?.includes('hours') ? '' : html `<nile-option value="hours">Hours</nile-option>`}
977
+ ${this.hideDurationFields?.includes('days') ? '' : html `<nile-option value="days">Days</nile-option>`}
978
+ ${this.hideDurationFields?.includes('weeks') ? '' : html `<nile-option value="weeks">Weeks</nile-option>`}
979
+ ${this.hideDurationFields?.includes('months') ? '' : html `<nile-option value="months">Months</nile-option>`}
980
+ </nile-select>
981
+ </div>
982
+ </div>
983
+ ` : this.range ? html `
361
984
  <div>
362
985
  <div class="unit-input-container">
363
986
  <nile-input
@@ -602,13 +1225,19 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
602
1225
  if (!this.allowedDatesLocal) {
603
1226
  return true;
604
1227
  }
605
- const dateToCheck = new Date(Date.UTC(year, month, day));
1228
+ // Date.UTC remaps years 0-99 to 1900-1999 as published; only the date-time-picker variant keeps the literal year
1229
+ const dateToCheck = new Date(this.variant === 'date-time-picker' ? utcFromFields(year, month, day) : Date.UTC(year, month, day));
606
1230
  const startDate = this.getUTCDate(this.allowedDatesLocal.startDate);
607
1231
  const endDate = this.getUTCDate(this.allowedDatesLocal.endDate);
608
1232
  endDate.setUTCHours(23, 59, 59, 999);
609
1233
  const isWithinRange = dateToCheck >= startDate && dateToCheck <= endDate;
610
1234
  return isWithinRange;
611
1235
  }
1236
+ dayCellLabel(day, month, year) {
1237
+ return localDateFromFields(year, month, day, 12).toLocaleDateString('default', {
1238
+ weekday: 'long', day: 'numeric', month: 'long', year: 'numeric',
1239
+ });
1240
+ }
612
1241
  clearDate(mode) {
613
1242
  const clearType = mode ??
614
1243
  (this.range
@@ -616,6 +1245,7 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
616
1245
  ? ClearMode.Relative
617
1246
  : ClearMode.Range
618
1247
  : ClearMode.Single);
1248
+ this.clearInstantSeed();
619
1249
  if (clearType === ClearMode.Single) {
620
1250
  this.value = null;
621
1251
  }
@@ -638,7 +1268,8 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
638
1268
  selectDate(day, month, year) {
639
1269
  if (this.disabled)
640
1270
  return;
641
- const selectedDate = new Date(year, month, day);
1271
+ // new Date(y, ...) remaps years 0-99 to 1900-1999 as published; only the date-time-picker variant keeps the literal year
1272
+ const selectedDate = this.variant === 'date-time-picker' ? localDateFromFields(year, month, day) : new Date(year, month, day);
642
1273
  if (this.range) {
643
1274
  if (this.startDate && this.endDate) {
644
1275
  this.startDate = null;
@@ -678,14 +1309,20 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
678
1309
  * Function to be called on initialization to set all other properties
679
1310
  */
680
1311
  initializeValue() {
1312
+ this.clearInstantSeed();
681
1313
  if (this.range) {
682
1314
  try {
683
1315
  const rangeValue = JSON.parse(this.valueAttribute || '');
684
- this.startDate = new Date(rangeValue.startDate);
685
- this.endDate = new Date(rangeValue.endDate);
1316
+ this.startDate = this.rangeBoundFromValue(rangeValue.startDate, false);
1317
+ this.endDate = this.rangeBoundFromValue(rangeValue.endDate, true);
686
1318
  // Convert to local time
687
1319
  this.startDate = new Date(this.startDate.getTime());
688
1320
  this.endDate = new Date(this.endDate.getTime());
1321
+ // date-time-picker variant only: open on the seeded range's start month (the default variant keeps its month)
1322
+ if (this.variant === 'date-time-picker' && this.startDate && !isNaN(this.startDate.getTime())) {
1323
+ this.currentMonth = this.startDate.getMonth();
1324
+ this.currentYear = this.startDate.getFullYear();
1325
+ }
689
1326
  this.value = null;
690
1327
  }
691
1328
  catch (e) {
@@ -693,9 +1330,58 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
693
1330
  }
694
1331
  }
695
1332
  else {
1333
+ if (this.variant === 'date-time-picker' && this.trySeedFromInstantValue()) {
1334
+ return;
1335
+ }
1336
+ const zoned = this.variant === 'date-time-picker'
1337
+ ? this.parseZonedValue(this.valueAttribute)
1338
+ : null;
1339
+ if (zoned) {
1340
+ this.setTimezoneInternally(zoned.timezone);
1341
+ const w = zoned.wall;
1342
+ this.value = localDateFromFields(w.year, w.month - 1, w.day, 12, 0, 0, 0);
1343
+ this.currentMonth = w.month - 1;
1344
+ this.currentYear = w.year;
1345
+ this.tpHours = w.hours;
1346
+ this.tpMinutes = w.minutes;
1347
+ this.tpSeconds = w.seconds;
1348
+ this.tpMilliseconds = w.ms;
1349
+ return;
1350
+ }
1351
+ if (this.variant === 'date-time-picker') {
1352
+ const wall = wallClockFromOffsetLess(this.valueAttribute);
1353
+ if (wall) {
1354
+ this.value = localDateFromFields(wall.year, wall.month - 1, wall.day, 12, 0, 0, 0);
1355
+ this.currentMonth = wall.month - 1;
1356
+ this.currentYear = wall.year;
1357
+ this.tpHours = wall.hours;
1358
+ this.tpMinutes = wall.minutes;
1359
+ this.tpSeconds = wall.seconds;
1360
+ this.tpMilliseconds = wall.ms;
1361
+ return;
1362
+ }
1363
+ }
1364
+ if (this.variant === 'date-time-picker') {
1365
+ const preset = this.parsePresetValue(this.valueAttribute);
1366
+ if (preset) {
1367
+ this.value = preset;
1368
+ this.currentMonth = preset.getMonth();
1369
+ this.currentYear = preset.getFullYear();
1370
+ // seed the time-picker spinners from the preset value's wall-clock time
1371
+ this.tpHours = preset.getHours();
1372
+ this.tpMinutes = preset.getMinutes();
1373
+ this.tpSeconds = preset.getSeconds();
1374
+ this.tpMilliseconds = preset.getMilliseconds();
1375
+ }
1376
+ else if (this.valueAttribute) {
1377
+ // an unparseable value must not leave the previous selection showing (the empty-value path above already clears it)
1378
+ this.value = null;
1379
+ this.resetTimeUnits();
1380
+ }
1381
+ return;
1382
+ }
696
1383
  if (this.valueAttribute) {
697
- let date = new Date(this.valueAttribute);
698
- date = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
1384
+ const date = this.dateFromValueString(this.valueAttribute);
699
1385
  if (!isNaN(date.getTime())) {
700
1386
  this.value = date;
701
1387
  this.currentMonth = this.value.getMonth();
@@ -704,11 +1390,148 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
704
1390
  }
705
1391
  }
706
1392
  }
1393
+ /** Forget any retained instant, in every category. */
1394
+ clearInstantSeed() {
1395
+ this.seedInstant = null;
1396
+ this.seedFieldsKey = null;
1397
+ this.seedZone = null;
1398
+ }
1399
+ instantSeedFieldsKey() {
1400
+ const v = this.value;
1401
+ if (!v)
1402
+ return null;
1403
+ return (`${v.getFullYear()}-${v.getMonth()}-${v.getDate()}|` +
1404
+ `${this.tpHours}:${this.tpMinutes}:${this.tpSeconds}.${this.tpMilliseconds}`);
1405
+ }
1406
+ seedFromInstant(epochMs, zone) {
1407
+ const w = wallClockInZone(epochMs, zone);
1408
+ this.value = localDateFromFields(w.year, w.month - 1, w.day, 12, 0, 0, 0);
1409
+ this.currentMonth = w.month - 1;
1410
+ this.currentYear = w.year;
1411
+ this.tpHours = w.hours;
1412
+ this.tpMinutes = w.minutes;
1413
+ this.tpSeconds = w.seconds;
1414
+ this.tpMilliseconds = w.ms;
1415
+ this.seedInstant = epochMs;
1416
+ this.seedFieldsKey = this.instantSeedFieldsKey();
1417
+ }
1418
+ trySeedFromInstantValue() {
1419
+ const epochMs = instantFromValueString(this.valueAttribute);
1420
+ if (epochMs !== null) {
1421
+ if (!isValidTimezone(this.timezone))
1422
+ return false;
1423
+ this.seedFromInstant(epochMs, this.timezone);
1424
+ this.seedZone = null;
1425
+ return true;
1426
+ }
1427
+ const bracketed = instantFromBracketedValue(this.valueAttribute);
1428
+ if (bracketed) {
1429
+ this.setTimezoneInternally(bracketed.timezone);
1430
+ this.seedFromInstant(bracketed.epochMs, bracketed.timezone);
1431
+ this.seedZone = bracketed.timezone;
1432
+ return true;
1433
+ }
1434
+ return false;
1435
+ }
1436
+ /**
1437
+ * Seeds `value` from a string with the published conversion: `new Date(raw)` shifted by the host offset. Every variant
1438
+ * keeps it, because the default calendar must behave exactly as the published component; the date-time-picker variant
1439
+ * re-derives its value from its own parsers in `initializeValue`.
1440
+ */
1441
+ dateFromValueString(raw) {
1442
+ const parsed = new Date(raw);
1443
+ return new Date(parsed.getTime() - parsed.getTimezoneOffset() * 60000);
1444
+ }
1445
+ /**
1446
+ * One bound of a `range` value. In the date-time-picker variant `YYYY-MM-DD` is a calendar day, not a UTC instant: it
1447
+ * lands on that local day, at 00:00:00.000 for the start and 23:59:59.999 for the end (what clicking the day produces).
1448
+ * Every other string, and every bound of the default calendar, goes through `new Date(raw)` exactly as published.
1449
+ */
1450
+ rangeBoundFromValue(raw, isEnd) {
1451
+ if (this.variant === 'date-time-picker' && typeof raw === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(raw)) {
1452
+ const day = this.parsePresetValue(raw);
1453
+ if (day) {
1454
+ if (isEnd)
1455
+ day.setHours(23, 59, 59, 999);
1456
+ return day;
1457
+ }
1458
+ }
1459
+ return new Date(raw);
1460
+ }
1461
+ parsePresetValue(raw) {
1462
+ if (!raw)
1463
+ return null;
1464
+ const dateOnly = /^\d{4}-\d{2}-\d{2}$/.exec(raw);
1465
+ if (dateOnly) {
1466
+ const year = Number(raw.slice(0, 4));
1467
+ const month = Number(raw.slice(5, 7)) - 1;
1468
+ const day = Number(raw.slice(8, 10));
1469
+ if (month < 0 || month > 11 || day < 1)
1470
+ return null;
1471
+ const lastDay = localDateFromFields(year, month + 1, 0).getDate();
1472
+ const date = localDateFromFields(year, month, Math.min(day, lastDay));
1473
+ return isNaN(date.getTime()) ? null : date;
1474
+ }
1475
+ // only an instant with an unusable zone or an undocumented string gets here; the documented formats are parsed before this
1476
+ const date = new Date(raw);
1477
+ return isNaN(date.getTime()) ? null : date;
1478
+ }
1479
+ parseZonedValue(raw) {
1480
+ if (!raw)
1481
+ return null;
1482
+ const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{3}))?\[GMT[+-]\d{2}:\d{2}\s+([^\]]+)\]$/.exec(raw.trim());
1483
+ if (!m)
1484
+ return null;
1485
+ const [, y, mo, d, h, mi, s, ms, rawZone] = m;
1486
+ const month0 = Number(mo) - 1;
1487
+ const hours = Number(h);
1488
+ const minutes = Number(mi);
1489
+ const seconds = Number(s);
1490
+ if (month0 < 0 || month0 > 11 || hours > 23 || minutes > 59 || seconds > 59) {
1491
+ return null;
1492
+ }
1493
+ const day = Number(d);
1494
+ if (day < 1)
1495
+ return null;
1496
+ // clamp instead of letting an out-of-range day roll into the next month
1497
+ const lastDay = localDateFromFields(Number(y), month0 + 1, 0).getDate();
1498
+ const wall = {
1499
+ year: Number(y),
1500
+ month: month0 + 1,
1501
+ day: Math.min(day, lastDay),
1502
+ hours,
1503
+ minutes,
1504
+ seconds,
1505
+ ms: Number(ms ?? 0),
1506
+ };
1507
+ const zone = rawZone.trim();
1508
+ const timezone = isValidTimezone(zone) ? zone : 'UTC';
1509
+ return { wall, timezone };
1510
+ }
707
1511
  /**
708
1512
  * Function to handle relative date selection
709
1513
  */
710
1514
  handleDurationChange(event) {
711
1515
  event.stopPropagation();
1516
+ if (this.variant === 'date-time-picker') {
1517
+ const input = event.target;
1518
+ const cleaned = String(event.detail?.value ?? input?.value ?? '').replace(/\D/g, '');
1519
+ if (input && input.value !== cleaned)
1520
+ input.value = cleaned;
1521
+ const duration = Number(cleaned);
1522
+ if (!duration) {
1523
+ this.selectedValue = undefined;
1524
+ this.startDate = null;
1525
+ this.endDate = null;
1526
+ this.requestUpdate();
1527
+ return;
1528
+ }
1529
+ this.selectedValue = duration;
1530
+ if (this.selectedUnit && this.selectedValue) {
1531
+ this.handleTimeValueClick(this.selectedUnit, this.selectedValue, event);
1532
+ }
1533
+ return;
1534
+ }
712
1535
  event.detail.value = event.detail.value.replace(/e/gi, "");
713
1536
  const duration = Number(event.detail.value);
714
1537
  if (!duration) {
@@ -735,12 +1558,22 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
735
1558
  createRelativePeriod(unit, value) {
736
1559
  const endTime = new Date();
737
1560
  const startTime = new Date();
1561
+ // only the date-time-picker variant uses exact elapsed time, month-end clamping and the too-large-duration guard; the
1562
+ // default calendar keeps the published wall-clock arithmetic (and its behaviour for an overflowing duration)
1563
+ const dtp = this.variant === 'date-time-picker';
738
1564
  switch (unit) {
739
1565
  case 'minutes':
740
- startTime.setMinutes(startTime.getMinutes() - value);
1566
+ // elapsed duration: wall-clock arithmetic (setMinutes) is off by an hour across a host DST change
1567
+ if (dtp)
1568
+ startTime.setTime(endTime.getTime() - value * 60000);
1569
+ else
1570
+ startTime.setMinutes(startTime.getMinutes() - value);
741
1571
  break;
742
1572
  case 'hours':
743
- startTime.setHours(startTime.getHours() - value);
1573
+ if (dtp)
1574
+ startTime.setTime(endTime.getTime() - value * 3600000);
1575
+ else
1576
+ startTime.setHours(startTime.getHours() - value);
744
1577
  break;
745
1578
  case 'days':
746
1579
  startTime.setDate(startTime.getDate() - value);
@@ -749,9 +1582,25 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
749
1582
  startTime.setDate(startTime.getDate() - 7 * value); // Subtract weeks as days
750
1583
  break;
751
1584
  case 'months':
752
- startTime.setMonth(startTime.getMonth() - value);
1585
+ if (dtp) {
1586
+ // clamp to the target month's last day: setMonth alone rolls 31 Mar - 1 month into 3 Mar
1587
+ const day = startTime.getDate();
1588
+ startTime.setDate(1);
1589
+ startTime.setMonth(startTime.getMonth() - value);
1590
+ const lastDay = localDateFromFields(startTime.getFullYear(), startTime.getMonth() + 1, 0).getDate();
1591
+ startTime.setDate(Math.min(day, lastDay));
1592
+ }
1593
+ else {
1594
+ startTime.setMonth(startTime.getMonth() - value);
1595
+ }
753
1596
  break;
754
1597
  }
1598
+ // a duration too large for a Date leaves an Invalid Date; storing it would keep Apply enabled and make consumers throw (RangeError) when they format it, so in the variant it is no selection
1599
+ if (dtp && (isNaN(startTime.getTime()) || isNaN(endTime.getTime()))) {
1600
+ this.startDate = null;
1601
+ this.endDate = null;
1602
+ return { startDate: null, endDate: null };
1603
+ }
755
1604
  this.startDate = new Date(startTime.getTime());
756
1605
  this.endDate = new Date(endTime.getTime());
757
1606
  return {
@@ -823,7 +1672,7 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
823
1672
  return;
824
1673
  if (this.currentMonth === 11) {
825
1674
  const newYear = this.currentYear + 1;
826
- if (this.endYear !== undefined && newYear > this.endYear)
1675
+ if (this.maxYear !== undefined && newYear > this.maxYear)
827
1676
  return;
828
1677
  this.currentMonth = 0;
829
1678
  this.currentYear = newYear;
@@ -837,7 +1686,7 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
837
1686
  return;
838
1687
  if (this.currentMonth === 0) {
839
1688
  const newYear = this.currentYear - 1;
840
- if (this.startYear !== undefined && newYear < this.startYear)
1689
+ if (this.minYear !== undefined && newYear < this.minYear)
841
1690
  return;
842
1691
  this.currentMonth = 11;
843
1692
  this.currentYear = newYear;
@@ -858,9 +1707,15 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
858
1707
  if (this.disabled)
859
1708
  return;
860
1709
  this.type = event.detail.value;
1710
+ if (this.variant === 'date-time-picker' && this.type !== 'absolute')
1711
+ this.expanded = false;
861
1712
  this.emit('nile-type-change', { value: this.type });
862
1713
  }
863
1714
  getUTCDate(dateStr) {
1715
+ // Date.UTC remaps years 0-99 to 1900-1999 as published; only the date-time-picker variant keeps the literal year
1716
+ if (this.variant === 'date-time-picker') {
1717
+ return new Date(utcFromFields(Number(dateStr.slice(0, 4)), Number(dateStr.slice(5, 7)) - 1, Number(dateStr.slice(8, 10))));
1718
+ }
864
1719
  return new Date(Date.UTC(dateStr.slice(0, 4), dateStr.slice(5, 7) - 1, dateStr.slice(8, 10)));
865
1720
  }
866
1721
  emitChangedData(data) {
@@ -871,6 +1726,809 @@ ${this.showMonthDropdown || this.showYearDropdown ? html `
871
1726
  const daysInMonth = new Date(year, month + 1, 0).getDate();
872
1727
  return Array.from({ length: daysInMonth }, (_, i) => i + 1);
873
1728
  }
1729
+ renderDateTimePicker() {
1730
+ // the collapsed height is held while the month grid is expanded (the measured value reaches the stylesheet as a custom property)
1731
+ const heightLocked = this.type === 'absolute' && this.expanded && !!this.tpLockedHeight;
1732
+ return html `
1733
+ <div
1734
+ class=${classMap({
1735
+ base: true,
1736
+ 'base--date-time-picker': true,
1737
+ 'base--tp-expanded': this.expanded,
1738
+ 'base--tp-fewfields': this.tpFewFieldsNoTz,
1739
+ 'base--tp-height-locked': heightLocked,
1740
+ })}
1741
+ style=${heightLocked ? `--nile-calendar-tp-locked-height:${this.tpLockedHeight}` : nothing}
1742
+ part="calendar-container"
1743
+ >
1744
+ <div class=${classMap({
1745
+ 'calendar-config': true,
1746
+ hidden: !this.showTypes,
1747
+ })} part="calendar-config">
1748
+ <div class="calendar-switcher" part="calendar-switcher">
1749
+ <nile-tab-group centered @nile-tab-show="${this.onTypeChange}" value="${this.type}">
1750
+ <nile-tab slot="nav" panel="absolute" tabindex="${this.disabled ? '-1' : '0'}">Absolute</nile-tab>
1751
+ <nile-tab slot="nav" panel="relative" tabindex="${this.disabled ? '-1' : '0'}">Relative</nile-tab>
1752
+ </nile-tab-group>
1753
+ </div>
1754
+ </div>
1755
+
1756
+ ${this.type === 'relative' ? this.renderRelativeCalendar() : this.renderDateTimePickerAbsolute()}
1757
+
1758
+ <div
1759
+ class=${classMap({
1760
+ 'tp-footer': true,
1761
+ 'tp-footer--relative': this.type === 'relative',
1762
+ })}
1763
+ part="tp-footer"
1764
+ >
1765
+ ${this.type === 'absolute' ? html `
1766
+ <nile-button
1767
+ id="tp-set-to-now"
1768
+ class="tp-set-to-now-button"
1769
+ part="tp-set-to-now-button"
1770
+ variant="secondary"
1771
+ ?disabled="${this.disabled}"
1772
+ @click="${this.handleSetToNow}"
1773
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
1774
+ this.handleSetToNow(); }}"
1775
+ >Set to now <nile-glyph name="ng-clock" size="16" method="stroke" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : nothing}></nile-glyph></nile-button>
1776
+ <nile-lite-tooltip
1777
+ for="tp-set-to-now"
1778
+ content="Sets the time to the current time"
1779
+ size="small"
1780
+ ></nile-lite-tooltip>
1781
+ ` : nothing}
1782
+ <div class="tp-footer-actions" part="tp-footer-actions">
1783
+ <nile-button
1784
+ class="clear-button"
1785
+ part="clear-button"
1786
+ variant="secondary"
1787
+ ?disabled="${this.disabled || !this.hasSelection()}"
1788
+ @click="${this.handleReset}"
1789
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
1790
+ this.handleReset(); }}"
1791
+ >Reset</nile-button>
1792
+ <nile-button
1793
+ class="apply-button"
1794
+ part="apply-button"
1795
+ ?disabled="${this.disabled || !this.hasSelection()}"
1796
+ @click="${this.handleApply}"
1797
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
1798
+ this.handleApply(); }}"
1799
+ >Apply</nile-button>
1800
+ </div>
1801
+ </div>
1802
+ </div>
1803
+ `;
1804
+ }
1805
+ renderDateTimePickerAbsolute() {
1806
+ return html `
1807
+ <div class="calendar-wrapper" part="calendar-wrapper">
1808
+ <div class="calendar-container">
1809
+ ${this.expanded
1810
+ ? this.renderExpandedCalendar()
1811
+ : this.renderCollapsedMonth(this.currentYear, this.currentMonth)}
1812
+ </div>
1813
+ </div>
1814
+
1815
+ <div
1816
+ class=${classMap({
1817
+ 'tp-chevron': true,
1818
+ 'tp-chevron--expanded': this.expanded,
1819
+ })}
1820
+ part="tp-chevron"
1821
+ role="button"
1822
+ tabindex="${this.disabled ? '-1' : '0'}"
1823
+ aria-expanded="${this.expanded}"
1824
+ aria-label="${this.expanded ? 'Collapse calendar' : 'Expand calendar'}"
1825
+ @click="${this.toggleExpanded}"
1826
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
1827
+ this.toggleExpanded(); }}"
1828
+ >
1829
+ <nile-icon name="arrowdropdown" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'}></nile-icon>
1830
+ </div>
1831
+
1832
+ <div class="tp-scroll-area" part="tp-scroll-area">
1833
+ <div class="tp-scroll-content" part="tp-scroll-content">
1834
+ ${this.renderTimeFields()}
1835
+ ${this.resolvedTimeFields.timezone ? this.renderTimezone() : ''}
1836
+ </div>
1837
+ </div>
1838
+ `;
1839
+ }
1840
+ renderExpandedCalendar() {
1841
+ return this.renderVariantMonth(this.currentYear, this.currentMonth, this.getFullMonthDays(this.currentYear, this.currentMonth));
1842
+ }
1843
+ renderCollapsedMonth(year, month) {
1844
+ return this.renderVariantMonth(year, month, this.getCollapsedWindowDays(year, month));
1845
+ }
1846
+ renderVariantMonth(year, month, cells) {
1847
+ const isSelectedDate = (day, month, year, isCurrentMonth) => {
1848
+ if (!isCurrentMonth)
1849
+ return '';
1850
+ if (this.value) {
1851
+ const isSelected = day === this.value.getDate() &&
1852
+ month === this.value.getMonth() &&
1853
+ year === this.value.getFullYear();
1854
+ if (isSelected)
1855
+ return 'selected-date';
1856
+ }
1857
+ return '';
1858
+ };
1859
+ const isCurrentDate = (day, month, year) => {
1860
+ const today = new Date();
1861
+ return (day === today.getDate() &&
1862
+ month + 1 === today.getMonth() + 1 &&
1863
+ year === today.getFullYear());
1864
+ };
1865
+ const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1866
+ // roving tabindex: one reachable day (the keyboard's last day, else the selection, else today, else the first allowed day)
1867
+ const key = (c) => `${c.year}-${c.month}-${c.day}`;
1868
+ const reachable = cells.filter(c => c.inMonth && this.isAllowedDate(c.day, c.month, c.year));
1869
+ const wanted = [
1870
+ this.tpFocusDate && this.tpFocusDate,
1871
+ this.value && { year: this.value.getFullYear(), month: this.value.getMonth(), day: this.value.getDate() },
1872
+ (() => { const t = new Date(); return { year: t.getFullYear(), month: t.getMonth(), day: t.getDate() }; })(),
1873
+ ];
1874
+ let tabStop = null;
1875
+ for (const w of wanted) {
1876
+ if (w && reachable.some(c => key(c) === key(w))) {
1877
+ tabStop = key(w);
1878
+ break;
1879
+ }
1880
+ }
1881
+ if (tabStop === null && reachable.length)
1882
+ tabStop = key(reachable[0]);
1883
+ return html `
1884
+ <div class="calendar" part="calendar">
1885
+ <div class="calendar-header" part="calendar-header">
1886
+ <nile-icon
1887
+ class="calendar-header__month-navigation"
1888
+ part="calendar-header-prev"
1889
+ name="var(--nile-icon-arrow-left, var(--ng-icon-chevron-left))"
1890
+ method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
1891
+ color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'}
1892
+ role="button"
1893
+ aria-label="Previous month"
1894
+ aria-disabled="${this.disabled || this.isPrevDisabled()}"
1895
+ tabindex="${this.disabled || this.isPrevDisabled() ? '-1' : '0'}"
1896
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
1897
+ this.prevMonth(); }}"
1898
+ @click="${this.prevMonth}"
1899
+ >
1900
+ </nile-icon>
1901
+ <div class="calendar-header__center" part="calendar-header-center">
1902
+
1903
+ ${this.showMonthDropdown || this.showYearDropdown ? html `
1904
+ ${this.showMonthDropdown ? html `
1905
+ <nile-dropdown ?disabled="${this.disabled}" class="month-dropdown"
1906
+ @nile-after-show="${() => this.scrollSelectedIntoView('.month-menu-item[selected]')}">
1907
+ <span
1908
+ slot="trigger"
1909
+ class="calendar-header__trigger month-trigger"
1910
+ role="button"
1911
+ part="trigger"
1912
+ tabindex="${this.disabled ? '-1' : '0'}"
1913
+ @keydown="${(e) => {
1914
+ if (e.key === 'Enter' || e.key === ' ') {
1915
+ e.preventDefault();
1916
+ e.currentTarget.click();
1917
+ }
1918
+ }}"
1919
+ >
1920
+ ${new Date(year, month).toLocaleString('default', { month: 'long' })}
1921
+ <nile-icon name="arrowdropdown" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'} class="calendar-header__caret"></nile-icon>
1922
+ </span>
1923
+ <nile-menu>
1924
+ <div class="month-scroll-wrapper">
1925
+ ${this.monthNames.map((m, idx) => html `
1926
+ <nile-menu-item
1927
+ class="month-menu-item"
1928
+ ?selected="${idx === month}"
1929
+ @click="${() => this.onMonthSelected(idx)}"
1930
+ >
1931
+ ${m}
1932
+ </nile-menu-item>
1933
+ `)}
1934
+ </div>
1935
+ </nile-menu>
1936
+ </nile-dropdown>
1937
+ ` : html `
1938
+ <span
1939
+ >${new Date(year, month).toLocaleString('default', { month: 'long' })}</span
1940
+ >
1941
+ `}
1942
+ ${this.showYearDropdown ? html `
1943
+ <nile-dropdown ?disabled="${this.disabled}" class="year-dropdown"
1944
+ @nile-after-show="${() => this.scrollSelectedIntoView('.year-menu-item[selected]')}">
1945
+ <span
1946
+ slot="trigger"
1947
+ class="calendar-header__trigger"
1948
+ role="button"
1949
+ part="trigger"
1950
+ tabindex="${this.disabled ? '-1' : '0'}"
1951
+ @keydown="${(e) => {
1952
+ if (e.key === 'Enter' || e.key === ' ') {
1953
+ e.preventDefault();
1954
+ e.currentTarget.click();
1955
+ }
1956
+ }}"
1957
+ >
1958
+ ${year}
1959
+ <nile-icon name="arrowdropdown" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'} class="calendar-header__caret"></nile-icon>
1960
+ </span>
1961
+ <nile-menu>
1962
+ <div class="year-scroll-wrapper">
1963
+ ${this.yearOptions.map(y => html `
1964
+ <nile-menu-item
1965
+ class="year-menu-item"
1966
+ ?selected="${y === year}"
1967
+ @click="${() => this.onYearSelected(y)}"
1968
+ >
1969
+ ${y}
1970
+ </nile-menu-item>
1971
+ `)}
1972
+ </div>
1973
+ </nile-menu>
1974
+ </nile-dropdown>
1975
+ ` : html `
1976
+ <span>${year}</span>
1977
+ `}
1978
+ ` : html `
1979
+ <span
1980
+ >${new Date(year, month).toLocaleString('default', {
1981
+ month: 'long',
1982
+ })}
1983
+ ${year}</span
1984
+ >
1985
+ `}
1986
+ </div>
1987
+
1988
+ <nile-icon
1989
+ class="calendar-header__month-navigation"
1990
+ part="calendar-header-next"
1991
+ name="var(--nile-icon-arrow-right, var(--ng-icon-chevron-right))"
1992
+ method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
1993
+ color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : 'var(--nile-colors-dark-900, var(--ng-colors-text-primary-900))'}
1994
+ role="button"
1995
+ aria-label="Next month"
1996
+ aria-disabled="${this.disabled || this.isNextDisabled()}"
1997
+ tabindex="${this.disabled || this.isNextDisabled() ? '-1' : '0'}"
1998
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
1999
+ this.nextMonth(); }}"
2000
+ @click="${this.nextMonth}"
2001
+ >
2002
+ </nile-icon>
2003
+ </div>
2004
+
2005
+ <div class="days_container" part="days-container">
2006
+ ${dayNames.map(day => html `<div class="day_name" part="day-name">${day}</div>`)}
2007
+ ${cells.map(cell => {
2008
+ const isCurrentMonth = cell.inMonth;
2009
+ const datePlacement = isSelectedDate(cell.day, cell.month, cell.year, isCurrentMonth);
2010
+ const classMapObj = {
2011
+ 'day_date': true,
2012
+ 'not-allowed': !this.isAllowedDate(cell.day, cell.month, cell.year),
2013
+ 'filler': !isCurrentMonth,
2014
+ };
2015
+ if (datePlacement)
2016
+ classMapObj[datePlacement] = true;
2017
+ const cellAllowed = this.isAllowedDate(cell.day, cell.month, cell.year);
2018
+ const cellToday = isCurrentDate(cell.day, cell.month, cell.year) && isCurrentMonth;
2019
+ return html `
2020
+ <div
2021
+ part="calendar-day-${cell.day}"
2022
+ class=${classMap(classMapObj)}
2023
+ role=${isCurrentMonth ? 'button' : nothing}
2024
+ aria-hidden=${isCurrentMonth ? nothing : 'true'}
2025
+ aria-label=${isCurrentMonth ? this.dayCellLabel(cell.day, cell.month, cell.year) : nothing}
2026
+ aria-disabled=${isCurrentMonth && (!cellAllowed || this.disabled) ? 'true' : nothing}
2027
+ aria-pressed=${isCurrentMonth ? String(!!datePlacement) : nothing}
2028
+ aria-current=${cellToday ? 'date' : nothing}
2029
+ data-tp-date="${isCurrentMonth ? key(cell) : nothing}"
2030
+ tabindex="${isCurrentMonth && !this.disabled && key(cell) === tabStop ? '0' : '-1'}"
2031
+ @keydown="${(e) => {
2032
+ if (!isCurrentMonth)
2033
+ return;
2034
+ if (e.key === 'Enter' || e.key === ' ') {
2035
+ e.preventDefault(); // Space on a role=button div would otherwise scroll the page
2036
+ this.tpFocusDate = { year: cell.year, month: cell.month, day: cell.day };
2037
+ this.tpSelectDate(cell.day, cell.month, cell.year);
2038
+ return;
2039
+ }
2040
+ this.handleDayNavigationKey(e, cell);
2041
+ }}"
2042
+ @click="${() => { if (isCurrentMonth) {
2043
+ this.tpFocusDate = { year: cell.year, month: cell.month, day: cell.day };
2044
+ this.tpSelectDate(cell.day, cell.month, cell.year);
2045
+ } }}"
2046
+ >
2047
+ <span class="day-label" part="calendar-day-label">
2048
+ ${cell.day}
2049
+ ${isCurrentDate(cell.day, cell.month, cell.year) && isCurrentMonth ? html `<div class="current__date__dot" part="current-date-dot"></div>` : nothing}
2050
+ </span>
2051
+ </div>`;
2052
+ })}
2053
+ </div>
2054
+ </div>
2055
+ `;
2056
+ }
2057
+ getCollapsedWindowDays(year, month) {
2058
+ const today = new Date();
2059
+ const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month;
2060
+ const selectionInMonth = this.value instanceof Date &&
2061
+ this.value.getFullYear() === year &&
2062
+ this.value.getMonth() === month;
2063
+ const centerOn = (dayOfMonth) => {
2064
+ const anchor = localDateFromFields(year, month, dayOfMonth);
2065
+ const s = new Date(anchor);
2066
+ s.setDate(anchor.getDate() - anchor.getDay() - 7);
2067
+ return s;
2068
+ };
2069
+ const start = selectionInMonth
2070
+ ? centerOn(this.value.getDate())
2071
+ : isCurrentMonth
2072
+ ? centerOn(today.getDate())
2073
+ : (() => {
2074
+ const first = localDateFromFields(year, month, 1);
2075
+ const s = new Date(first);
2076
+ s.setDate(1 - first.getDay());
2077
+ return s;
2078
+ })();
2079
+ return Array.from({ length: 21 }, (_, i) => {
2080
+ const d = new Date(start);
2081
+ d.setDate(start.getDate() + i);
2082
+ return {
2083
+ day: d.getDate(),
2084
+ month: d.getMonth(),
2085
+ year: d.getFullYear(),
2086
+ inMonth: d.getMonth() === month && d.getFullYear() === year,
2087
+ };
2088
+ });
2089
+ }
2090
+ getFullMonthDays(year, month) {
2091
+ const first = localDateFromFields(year, month, 1);
2092
+ const leading = first.getDay();
2093
+ const daysInMonth = localDateFromFields(year, month + 1, 0).getDate();
2094
+ const totalCells = Math.ceil((leading + daysInMonth) / 7) * 7;
2095
+ const start = localDateFromFields(year, month, 1 - leading);
2096
+ return Array.from({ length: totalCells }, (_, i) => {
2097
+ const d = new Date(start);
2098
+ d.setDate(start.getDate() + i);
2099
+ return {
2100
+ day: d.getDate(),
2101
+ month: d.getMonth(),
2102
+ year: d.getFullYear(),
2103
+ inMonth: d.getMonth() === month && d.getFullYear() === year,
2104
+ };
2105
+ });
2106
+ }
2107
+ /**
2108
+ * Arrow keys move by a day / week, Home and End to the week's first and last day, PageUp and PageDown by a month
2109
+ * (Shift: a year). The target is clamped to the month's length and to the year range; a day that the collapsed
2110
+ * three-week window does not show expands the calendar to reveal it.
2111
+ */
2112
+ handleDayNavigationKey(e, cell) {
2113
+ if (this.disabled || e.ctrlKey || e.metaKey || e.altKey)
2114
+ return;
2115
+ const from = localDateFromFields(cell.year, cell.month, cell.day);
2116
+ let target = null;
2117
+ switch (e.key) {
2118
+ case 'ArrowLeft':
2119
+ target = localDateFromFields(cell.year, cell.month, cell.day - 1);
2120
+ break;
2121
+ case 'ArrowRight':
2122
+ target = localDateFromFields(cell.year, cell.month, cell.day + 1);
2123
+ break;
2124
+ case 'ArrowUp':
2125
+ target = localDateFromFields(cell.year, cell.month, cell.day - 7);
2126
+ break;
2127
+ case 'ArrowDown':
2128
+ target = localDateFromFields(cell.year, cell.month, cell.day + 7);
2129
+ break;
2130
+ case 'Home':
2131
+ target = localDateFromFields(cell.year, cell.month, cell.day - from.getDay());
2132
+ break;
2133
+ case 'End':
2134
+ target = localDateFromFields(cell.year, cell.month, cell.day + (6 - from.getDay()));
2135
+ break;
2136
+ case 'PageUp':
2137
+ case 'PageDown': {
2138
+ const step = (e.key === 'PageUp' ? -1 : 1) * (e.shiftKey ? 12 : 1);
2139
+ const month = cell.month + step;
2140
+ const last = localDateFromFields(cell.year, month + 1, 0).getDate();
2141
+ target = localDateFromFields(cell.year, month, Math.min(cell.day, last));
2142
+ break;
2143
+ }
2144
+ default: return;
2145
+ }
2146
+ e.preventDefault();
2147
+ this.moveDayFocus(target);
2148
+ }
2149
+ moveDayFocus(target) {
2150
+ const year = target.getFullYear();
2151
+ const month = target.getMonth();
2152
+ const lo = this.minYear;
2153
+ const hi = this.maxYear;
2154
+ if ((lo !== undefined && year < lo) || (hi !== undefined && year > hi))
2155
+ return;
2156
+ this.tpFocusDate = { year, month, day: target.getDate() };
2157
+ this.currentYear = year;
2158
+ this.currentMonth = month;
2159
+ const sel = `[data-tp-date="${year}-${month}-${target.getDate()}"]`;
2160
+ const focusCell = () => this.shadowRoot?.querySelector(sel)?.focus();
2161
+ this.updateComplete.then(() => {
2162
+ if (this.shadowRoot?.querySelector(sel)) {
2163
+ focusCell();
2164
+ return;
2165
+ }
2166
+ // not in the collapsed window: reveal the whole month
2167
+ this.expanded = true;
2168
+ this.updateComplete.then(focusCell).catch(() => { });
2169
+ }).catch(() => { });
2170
+ }
2171
+ tpSelectDate(day, month, year) {
2172
+ if (this.disabled)
2173
+ return;
2174
+ if (!this.isAllowedDate(day, month, year))
2175
+ return;
2176
+ const selectedDate = localDateFromFields(year, month, day);
2177
+ // compare the calendar day only: seeded values are stored at noon, a clicked day at midnight
2178
+ const sameDay = !!this.value &&
2179
+ this.value.getFullYear() === year &&
2180
+ this.value.getMonth() === month &&
2181
+ this.value.getDate() === day;
2182
+ if (sameDay && this.doubleClickUnselect) {
2183
+ this.handleReset();
2184
+ return;
2185
+ }
2186
+ if (!sameDay) {
2187
+ this.resetTimeUnits();
2188
+ }
2189
+ this.value = selectedDate;
2190
+ }
2191
+ resetTimeUnits() {
2192
+ this.tpHours = null;
2193
+ this.tpMinutes = null;
2194
+ this.tpSeconds = null;
2195
+ this.tpMilliseconds = null;
2196
+ this.tpEditingText = '';
2197
+ }
2198
+ renderTimeFields() {
2199
+ const fields = this.resolvedTimeFields;
2200
+ return html `
2201
+ <div class=${classMap({
2202
+ 'tp-time-fields': true,
2203
+ 'tp-time-fields--stacked': this.tpFewFieldsNoTz,
2204
+ })} part="tp-time-fields">
2205
+ ${fields.hours ? this.renderTimeField('Hours (24H)', 'HH', 23, this.tpHours, v => { this.tpHours = v; }) : ''}
2206
+ ${fields.minutes ? this.renderTimeField('Minutes', 'MM', 59, this.tpMinutes, v => { this.tpMinutes = v; }) : ''}
2207
+ ${fields.seconds ? this.renderTimeField('Seconds', 'SS', 59, this.tpSeconds, v => { this.tpSeconds = v; }) : ''}
2208
+ ${fields.milliseconds ? this.renderTimeField('Milliseconds', 'MS', 999, this.tpMilliseconds, v => { this.tpMilliseconds = v; }) : ''}
2209
+ </div>
2210
+ `;
2211
+ }
2212
+ renderTimeField(label, placeholder, max, value, setter) {
2213
+ const maxDigits = String(max).length;
2214
+ const normalize = (raw) => {
2215
+ let digits = (raw || '').replace(/\D/g, '');
2216
+ while (digits.length > maxDigits && digits[0] === '0')
2217
+ digits = digits.slice(1);
2218
+ if (digits.length > maxDigits)
2219
+ digits = digits.slice(0, maxDigits);
2220
+ if (digits === '')
2221
+ return { text: '', value: null };
2222
+ const num = Number(digits);
2223
+ if (num > max) {
2224
+ const first = Number(digits[0]);
2225
+ return { text: String(first).padStart(maxDigits, '0'), value: first };
2226
+ }
2227
+ return { text: digits, value: num };
2228
+ };
2229
+ const apply = (e) => {
2230
+ e.stopPropagation();
2231
+ const input = e.target;
2232
+ const { text, value: next } = normalize(input.value);
2233
+ setter(next);
2234
+ this.tpEditingText = text;
2235
+ if (input.value !== text)
2236
+ input.value = text;
2237
+ };
2238
+ const step = (delta, focusEl) => {
2239
+ if (this.disabled)
2240
+ return;
2241
+ // A stepper click keeps DOM focus on whichever field was focused before, so move it here — the
2242
+ // shared editing text must be attributed to the right input.
2243
+ if (focusEl) {
2244
+ const input = focusEl.closest('nile-input');
2245
+ // `nile-input.focus()` dereferences its inner <input>, which does not exist until the input has rendered
2246
+ // (a calendar in a closed, not-yet-attached panel never renders it). Nothing to focus then; still step.
2247
+ if (input?.input)
2248
+ input.focus();
2249
+ }
2250
+ const span = max + 1;
2251
+ const next = value === null
2252
+ ? (delta === 1 ? 0 : max)
2253
+ : ((value + delta) % span + span) % span;
2254
+ setter(next);
2255
+ this.tpFocusedTimeField = label;
2256
+ this.tpEditingText = String(next).padStart(maxDigits, '0');
2257
+ };
2258
+ const focused = this.tpFocusedTimeField === label;
2259
+ const displayValue = focused
2260
+ ? this.tpEditingText
2261
+ : value !== null || this.value
2262
+ ? String(value ?? 0).padStart(maxDigits, '0')
2263
+ : '';
2264
+ return html `
2265
+ <nile-input
2266
+ class="tp-time-field"
2267
+ part="tp-time-field"
2268
+ type="text"
2269
+ inputmode="numeric"
2270
+ label="${label}"
2271
+ placeholder="${placeholder}"
2272
+ ?disabled="${this.disabled}"
2273
+ value="${displayValue}"
2274
+ @nile-change="${apply}"
2275
+ @nile-input="${apply}"
2276
+ @keydown="${(e) => {
2277
+ if (e.key === 'ArrowUp') {
2278
+ e.preventDefault();
2279
+ step(1);
2280
+ }
2281
+ else if (e.key === 'ArrowDown') {
2282
+ e.preventDefault();
2283
+ step(-1);
2284
+ }
2285
+ }}"
2286
+ @nile-focus="${(e) => {
2287
+ e.stopPropagation();
2288
+ this.tpFocusedTimeField = label;
2289
+ this.tpEditingText = '';
2290
+ }}"
2291
+ @nile-blur="${(e) => { e.stopPropagation(); if (this.tpFocusedTimeField === label)
2292
+ this.tpFocusedTimeField = null; }}"
2293
+ >
2294
+ <div
2295
+ slot="suffix"
2296
+ class="tp-stepper"
2297
+ part="tp-stepper"
2298
+ @mousedown="${(e) => e.preventDefault()}"
2299
+ >
2300
+ <div
2301
+ class="tp-stepper-btn tp-stepper-btn--up"
2302
+ part="tp-stepper-btn tp-stepper-btn-up"
2303
+ role="button"
2304
+ tabindex="${this.disabled ? '-1' : '0'}"
2305
+ aria-label="Increase ${label}"
2306
+ @click="${(e) => step(1, e.currentTarget)}"
2307
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
2308
+ step(1, e.currentTarget); }}"
2309
+ >
2310
+ <nile-icon name="arrowdropdown" size="16" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : nothing}></nile-icon>
2311
+ </div>
2312
+ <div
2313
+ class="tp-stepper-btn"
2314
+ part="tp-stepper-btn tp-stepper-btn-down"
2315
+ role="button"
2316
+ tabindex="${this.disabled ? '-1' : '0'}"
2317
+ aria-label="Decrease ${label}"
2318
+ @click="${(e) => step(-1, e.currentTarget)}"
2319
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ')
2320
+ step(-1, e.currentTarget); }}"
2321
+ >
2322
+ <nile-icon name="arrowdropdown" size="16" color=${this.disabled ? 'var(--nile-colors-dark-500, var(--ng-colors-fg-disabled))' : nothing}></nile-icon>
2323
+ </div>
2324
+ </div>
2325
+ </nile-input>
2326
+ `;
2327
+ }
2328
+ get timezoneOptions() {
2329
+ const zones = typeof Intl.supportedValuesOf === 'function'
2330
+ ? Intl.supportedValuesOf('timeZone')
2331
+ : TIMEZONES;
2332
+ return zones.includes('UTC') ? zones : ['UTC', ...zones];
2333
+ }
2334
+ renderTimezone() {
2335
+ return html `
2336
+ <div class="tp-timezone" part="tp-timezone">
2337
+ <div class="tp-timezone-header" part="tp-timezone-header">
2338
+ <span class="tp-timezone-label" part="tp-timezone-label">Timezone</span>
2339
+ <span
2340
+ class="tp-timezone-local-link"
2341
+ part="tp-timezone-local-link"
2342
+ role="button"
2343
+ tabindex="${this.disabled ? '-1' : '0'}"
2344
+ @click="${this.handleUseLocalTimezone}"
2345
+ @keydown="${(e) => { if (e.key === 'Enter' || e.key === ' ') {
2346
+ e.preventDefault();
2347
+ this.handleUseLocalTimezone();
2348
+ } }}"
2349
+ >Use local timezone</span>
2350
+ </div>
2351
+ <nile-select
2352
+ class="tp-timezone-select"
2353
+ part="tp-timezone-select"
2354
+ hoist
2355
+ placement="top"
2356
+ ?disabled="${this.disabled}"
2357
+ searchEnabled
2358
+ placeholder="Select timezone"
2359
+ .value="${this.timezone}"
2360
+ @nile-change="${this.handleTimezoneChange}"
2361
+ >
2362
+ ${this.timezoneOptions.map(tz => html `<nile-option value="${tz}">${tz}</nile-option>`)}
2363
+ </nile-select>
2364
+ </div>
2365
+ `;
2366
+ }
2367
+ handleTimezoneChange(event) {
2368
+ event.stopPropagation();
2369
+ const selected = (event.detail && (event.detail.value ?? event.detail)) ||
2370
+ event.target?.value;
2371
+ if (typeof selected === 'string' && selected) {
2372
+ this.setTimezoneInternally(selected);
2373
+ }
2374
+ }
2375
+ handleUseLocalTimezone() {
2376
+ if (this.disabled)
2377
+ return;
2378
+ const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
2379
+ if (local) {
2380
+ this.setTimezoneInternally(local);
2381
+ }
2382
+ }
2383
+ toggleExpanded() {
2384
+ if (this.disabled)
2385
+ return;
2386
+ this.expanded = !this.expanded;
2387
+ }
2388
+ composeLocalTimezoneValue() {
2389
+ if (!this.value)
2390
+ return null;
2391
+ if (this.seedInstant !== null &&
2392
+ this.seedFieldsKey === this.instantSeedFieldsKey() &&
2393
+ (this.seedZone === null || this.seedZone === this.timezone)) {
2394
+ return new Date(this.seedInstant);
2395
+ }
2396
+ return this.wallClockToInstant(this.value.getFullYear(), this.value.getMonth(), this.value.getDate(), this.tpHours ?? 0, this.tpMinutes ?? 0, this.tpSeconds ?? 0, this.tpMilliseconds ?? 0);
2397
+ }
2398
+ composeValueISO(epochMs) {
2399
+ const zone = this.timezone || 'UTC';
2400
+ const p = (n, len = 2) => String(n).padStart(len, '0');
2401
+ const ms = ((epochMs % 1000) + 1000) % 1000;
2402
+ let y, mo, d, h, mi, s;
2403
+ let offMs;
2404
+ try {
2405
+ const dtf = new Intl.DateTimeFormat('en-US', {
2406
+ timeZone: zone,
2407
+ hourCycle: 'h23',
2408
+ year: 'numeric', month: '2-digit', day: '2-digit',
2409
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
2410
+ });
2411
+ const map = {};
2412
+ for (const part of dtf.formatToParts(new Date(epochMs))) {
2413
+ if (part.type !== 'literal')
2414
+ map[part.type] = Number(part.value);
2415
+ }
2416
+ y = map.year;
2417
+ mo = map.month;
2418
+ d = map.day;
2419
+ h = map.hour;
2420
+ mi = map.minute;
2421
+ s = map.second;
2422
+ offMs = this.zoneOffsetMs(epochMs - ms);
2423
+ }
2424
+ catch {
2425
+ const local = new Date(epochMs);
2426
+ y = local.getFullYear();
2427
+ mo = local.getMonth() + 1;
2428
+ d = local.getDate();
2429
+ h = local.getHours();
2430
+ mi = local.getMinutes();
2431
+ s = local.getSeconds();
2432
+ offMs = 0;
2433
+ }
2434
+ // the bracket only carries HH:mm; historical sub-minute offsets (LMT) round
2435
+ offMs = Math.round(offMs / 60000) * 60000;
2436
+ const sign = offMs >= 0 ? '+' : '-';
2437
+ const abs = Math.abs(offMs);
2438
+ const off = `${sign}${p(Math.floor(abs / 3600000))}:${p(Math.floor((abs % 3600000) / 60000))}`;
2439
+ return (`${p(y, 4)}-${p(mo)}-${p(d)}T${p(h)}:${p(mi)}:${p(s)}.${p(ms, 3)}` +
2440
+ `[GMT${off} ${zone}]`);
2441
+ }
2442
+ wallClockToInstant(y, monthIndex, day, h, m, s, ms) {
2443
+ const guess = utcFromFields(y, monthIndex, day, h, m, s, 0);
2444
+ try {
2445
+ const offset1 = this.zoneOffsetMs(guess);
2446
+ let instant = guess - offset1;
2447
+ const offset2 = this.zoneOffsetMs(instant);
2448
+ if (offset2 !== offset1)
2449
+ instant = guess - offset2;
2450
+ return new Date(instant + ms);
2451
+ }
2452
+ catch {
2453
+ const composed = new Date(this.value.getTime());
2454
+ composed.setHours(h, m, s, ms);
2455
+ return composed;
2456
+ }
2457
+ }
2458
+ zoneOffsetMs(utcMs) {
2459
+ const dtf = new Intl.DateTimeFormat('en-US', {
2460
+ timeZone: this.timezone,
2461
+ hourCycle: 'h23',
2462
+ year: 'numeric', month: '2-digit', day: '2-digit',
2463
+ hour: '2-digit', minute: '2-digit', second: '2-digit',
2464
+ });
2465
+ const map = {};
2466
+ for (const p of dtf.formatToParts(new Date(utcMs))) {
2467
+ if (p.type !== 'literal')
2468
+ map[p.type] = Number(p.value);
2469
+ }
2470
+ const asUTC = utcFromFields(map.year, map.month - 1, map.day, map.hour, map.minute, map.second);
2471
+ return asUTC - utcMs;
2472
+ }
2473
+ hasSelection() {
2474
+ if (this.type === 'relative') {
2475
+ return Boolean(this.startDate && this.endDate);
2476
+ }
2477
+ return Boolean(this.value);
2478
+ }
2479
+ handleSetToNow() {
2480
+ if (this.disabled)
2481
+ return;
2482
+ const now = new Date();
2483
+ this.tpHours = now.getHours();
2484
+ this.tpMinutes = now.getMinutes();
2485
+ this.tpSeconds = now.getSeconds();
2486
+ this.tpMilliseconds = now.getMilliseconds();
2487
+ this.value = new Date(now.getFullYear(), now.getMonth(), now.getDate());
2488
+ this.currentMonth = now.getMonth();
2489
+ this.currentYear = now.getFullYear();
2490
+ const local = Intl.DateTimeFormat().resolvedOptions().timeZone;
2491
+ if (local) {
2492
+ this.setTimezoneInternally(local);
2493
+ }
2494
+ }
2495
+ handleApply() {
2496
+ if (this.disabled || !this.hasSelection())
2497
+ return;
2498
+ if (this.type === 'relative') {
2499
+ this.emitDateTimePickerChange({
2500
+ startDate: this.startDate,
2501
+ endDate: this.endDate,
2502
+ timezone: 'UTC',
2503
+ });
2504
+ return;
2505
+ }
2506
+ const localTimezoneValue = this.composeLocalTimezoneValue();
2507
+ const unixEpoch = localTimezoneValue ? localTimezoneValue.getTime() : null;
2508
+ this.emitDateTimePickerChange({
2509
+ value: unixEpoch === null ? null : this.composeValueISO(unixEpoch),
2510
+ localTimezoneValue,
2511
+ unixEpoch,
2512
+ timezone: this.timezone,
2513
+ });
2514
+ }
2515
+ handleReset() {
2516
+ if (this.disabled)
2517
+ return;
2518
+ this.clearInstantSeed();
2519
+ this.value = null;
2520
+ this.startDate = null;
2521
+ this.endDate = null;
2522
+ this.selectedUnit = undefined;
2523
+ this.selectedValue = undefined;
2524
+ this.resetTimeUnits();
2525
+ this.setTimezoneInternally(this.configuredTimezone);
2526
+ this.requestUpdate();
2527
+ this.emit('nile-clear');
2528
+ }
2529
+ emitDateTimePickerChange(data) {
2530
+ this.emit('nile-change', data);
2531
+ }
874
2532
  };
875
2533
  __decorate([
876
2534
  property({ type: String, attribute: 'value' })
@@ -884,6 +2542,9 @@ __decorate([
884
2542
  __decorate([
885
2543
  property({ type: Boolean, attribute: true, reflect: true })
886
2544
  ], NileCalendar.prototype, "hideTypes", void 0);
2545
+ __decorate([
2546
+ property({ type: Boolean, attribute: true, reflect: true })
2547
+ ], NileCalendar.prototype, "showTypes", void 0);
887
2548
  __decorate([
888
2549
  property({ type: Boolean, reflect: true })
889
2550
  ], NileCalendar.prototype, "doubleClickUnselect", void 0);
@@ -920,6 +2581,9 @@ __decorate([
920
2581
  __decorate([
921
2582
  property({ type: String, reflect: true, attribute: true })
922
2583
  ], NileCalendar.prototype, "dateFormat", void 0);
2584
+ __decorate([
2585
+ property({ type: Boolean, reflect: true })
2586
+ ], NileCalendar.prototype, "structuredInput", void 0);
923
2587
  __decorate([
924
2588
  state()
925
2589
  ], NileCalendar.prototype, "startDate", void 0);
@@ -947,6 +2611,42 @@ __decorate([
947
2611
  __decorate([
948
2612
  property({ type: Boolean, attribute: true, reflect: true })
949
2613
  ], NileCalendar.prototype, "disabled", void 0);
2614
+ __decorate([
2615
+ property({ type: String, attribute: true })
2616
+ ], NileCalendar.prototype, "variant", void 0);
2617
+ __decorate([
2618
+ property({ type: String, attribute: true })
2619
+ ], NileCalendar.prototype, "timezone", void 0);
2620
+ __decorate([
2621
+ property({ type: Array, attribute: 'hide-time-fields' })
2622
+ ], NileCalendar.prototype, "hideTimeFields", void 0);
2623
+ __decorate([
2624
+ state()
2625
+ ], NileCalendar.prototype, "expanded", void 0);
2626
+ __decorate([
2627
+ state()
2628
+ ], NileCalendar.prototype, "tpLockedHeight", void 0);
2629
+ __decorate([
2630
+ state()
2631
+ ], NileCalendar.prototype, "tpHours", void 0);
2632
+ __decorate([
2633
+ state()
2634
+ ], NileCalendar.prototype, "tpMinutes", void 0);
2635
+ __decorate([
2636
+ state()
2637
+ ], NileCalendar.prototype, "tpSeconds", void 0);
2638
+ __decorate([
2639
+ state()
2640
+ ], NileCalendar.prototype, "tpMilliseconds", void 0);
2641
+ __decorate([
2642
+ state()
2643
+ ], NileCalendar.prototype, "tpFocusedTimeField", void 0);
2644
+ __decorate([
2645
+ state()
2646
+ ], NileCalendar.prototype, "tpFocusDate", void 0);
2647
+ __decorate([
2648
+ state()
2649
+ ], NileCalendar.prototype, "tpEditingText", void 0);
950
2650
  __decorate([
951
2651
  watch('allowedDates')
952
2652
  ], NileCalendar.prototype, "checkValidAllowedDate", null);