stimeo-ui 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/controllers/announcer_controller.js.map +1 -1
  3. package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
  4. package/dist/controllers/aspect_ratio_controller.js +19 -11
  5. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  6. package/dist/controllers/avatar_controller.d.ts +36 -15
  7. package/dist/controllers/avatar_controller.js +195 -40
  8. package/dist/controllers/avatar_controller.js.map +1 -1
  9. package/dist/controllers/breadcrumb_controller.js +5 -1
  10. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  11. package/dist/controllers/carousel_controller.d.ts +12 -3
  12. package/dist/controllers/carousel_controller.js +90 -10
  13. package/dist/controllers/carousel_controller.js.map +1 -1
  14. package/dist/controllers/checkbox_controller.d.ts +31 -5
  15. package/dist/controllers/checkbox_controller.js +136 -25
  16. package/dist/controllers/checkbox_controller.js.map +1 -1
  17. package/dist/controllers/clipboard_controller.js +8 -3
  18. package/dist/controllers/clipboard_controller.js.map +1 -1
  19. package/dist/controllers/collapsible_controller.d.ts +1 -1
  20. package/dist/controllers/collapsible_controller.js +4 -1
  21. package/dist/controllers/collapsible_controller.js.map +1 -1
  22. package/dist/controllers/color_picker_controller.d.ts +9 -3
  23. package/dist/controllers/color_picker_controller.js +41 -11
  24. package/dist/controllers/color_picker_controller.js.map +1 -1
  25. package/dist/controllers/combobox_controller.js.map +1 -1
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/context_menu_controller.d.ts +1 -1
  28. package/dist/controllers/context_menu_controller.js +2 -2
  29. package/dist/controllers/context_menu_controller.js.map +1 -1
  30. package/dist/controllers/countdown_controller.d.ts +5 -3
  31. package/dist/controllers/countdown_controller.js +5 -1
  32. package/dist/controllers/countdown_controller.js.map +1 -1
  33. package/dist/controllers/date_range_picker_controller.d.ts +23 -4
  34. package/dist/controllers/date_range_picker_controller.js +162 -31
  35. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  36. package/dist/controllers/direct_upload_controller.d.ts +1 -1
  37. package/dist/controllers/direct_upload_controller.js +3 -3
  38. package/dist/controllers/direct_upload_controller.js.map +1 -1
  39. package/dist/controllers/empty_state_controller.d.ts +27 -7
  40. package/dist/controllers/empty_state_controller.js +107 -16
  41. package/dist/controllers/empty_state_controller.js.map +1 -1
  42. package/dist/controllers/file_dropzone_controller.d.ts +7 -1
  43. package/dist/controllers/file_dropzone_controller.js +26 -3
  44. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  45. package/dist/controllers/flash_controller.d.ts +25 -5
  46. package/dist/controllers/flash_controller.js +161 -21
  47. package/dist/controllers/flash_controller.js.map +1 -1
  48. package/dist/controllers/focus_controller.d.ts +2 -3
  49. package/dist/controllers/focus_controller.js.map +1 -1
  50. package/dist/controllers/form_validation_controller.js +8 -2
  51. package/dist/controllers/form_validation_controller.js.map +1 -1
  52. package/dist/controllers/frame_loading_controller.d.ts +19 -2
  53. package/dist/controllers/frame_loading_controller.js +94 -22
  54. package/dist/controllers/frame_loading_controller.js.map +1 -1
  55. package/dist/controllers/highlight_controller.d.ts +8 -4
  56. package/dist/controllers/highlight_controller.js +38 -1
  57. package/dist/controllers/highlight_controller.js.map +1 -1
  58. package/dist/controllers/hover_card_controller.d.ts +2 -2
  59. package/dist/controllers/hover_card_controller.js.map +1 -1
  60. package/dist/controllers/idle_controller.d.ts +5 -4
  61. package/dist/controllers/idle_controller.js +39 -6
  62. package/dist/controllers/idle_controller.js.map +1 -1
  63. package/dist/controllers/listbox_controller.js.map +1 -1
  64. package/dist/controllers/local_time_controller.js +2 -0
  65. package/dist/controllers/local_time_controller.js.map +1 -1
  66. package/dist/controllers/masonry_controller.d.ts +1 -1
  67. package/dist/controllers/masonry_controller.js +1 -1
  68. package/dist/controllers/masonry_controller.js.map +1 -1
  69. package/dist/controllers/menubar_controller.js +5 -3
  70. package/dist/controllers/menubar_controller.js.map +1 -1
  71. package/dist/controllers/meter_controller.js +3 -1
  72. package/dist/controllers/meter_controller.js.map +1 -1
  73. package/dist/controllers/multi_select_controller.d.ts +46 -12
  74. package/dist/controllers/multi_select_controller.js +460 -151
  75. package/dist/controllers/multi_select_controller.js.map +1 -1
  76. package/dist/controllers/network_status_controller.d.ts +15 -13
  77. package/dist/controllers/network_status_controller.js +1 -3
  78. package/dist/controllers/network_status_controller.js.map +1 -1
  79. package/dist/controllers/number_input_controller.d.ts +29 -3
  80. package/dist/controllers/number_input_controller.js +455 -64
  81. package/dist/controllers/number_input_controller.js.map +1 -1
  82. package/dist/controllers/overflow_menu_controller.js +5 -1
  83. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  84. package/dist/controllers/pagination_controller.js +38 -1
  85. package/dist/controllers/pagination_controller.js.map +1 -1
  86. package/dist/controllers/password_strength_controller.d.ts +4 -4
  87. package/dist/controllers/password_strength_controller.js +21 -3
  88. package/dist/controllers/password_strength_controller.js.map +1 -1
  89. package/dist/controllers/persist_controller.d.ts +4 -4
  90. package/dist/controllers/persist_controller.js +24 -5
  91. package/dist/controllers/persist_controller.js.map +1 -1
  92. package/dist/controllers/pointer_drag_controller.js +10 -0
  93. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  94. package/dist/controllers/popover_controller.d.ts +2 -2
  95. package/dist/controllers/popover_controller.js.map +1 -1
  96. package/dist/controllers/portal_controller.d.ts +1 -2
  97. package/dist/controllers/portal_controller.js +10 -0
  98. package/dist/controllers/portal_controller.js.map +1 -1
  99. package/dist/controllers/progress_controller.d.ts +1 -1
  100. package/dist/controllers/progress_controller.js +7 -3
  101. package/dist/controllers/progress_controller.js.map +1 -1
  102. package/dist/controllers/radio_group_controller.d.ts +41 -15
  103. package/dist/controllers/radio_group_controller.js +540 -56
  104. package/dist/controllers/radio_group_controller.js.map +1 -1
  105. package/dist/controllers/range_slider_controller.d.ts +23 -0
  106. package/dist/controllers/range_slider_controller.js +385 -94
  107. package/dist/controllers/range_slider_controller.js.map +1 -1
  108. package/dist/controllers/rating_controller.d.ts +36 -31
  109. package/dist/controllers/rating_controller.js +274 -89
  110. package/dist/controllers/rating_controller.js.map +1 -1
  111. package/dist/controllers/relative_time_controller.js +2 -0
  112. package/dist/controllers/relative_time_controller.js.map +1 -1
  113. package/dist/controllers/resizable_controller.js +33 -0
  114. package/dist/controllers/resizable_controller.js.map +1 -1
  115. package/dist/controllers/roving_controller.d.ts +4 -0
  116. package/dist/controllers/roving_controller.js +60 -5
  117. package/dist/controllers/roving_controller.js.map +1 -1
  118. package/dist/controllers/scroll_area_controller.d.ts +7 -1
  119. package/dist/controllers/scroll_area_controller.js +155 -23
  120. package/dist/controllers/scroll_area_controller.js.map +1 -1
  121. package/dist/controllers/scroll_visibility_controller.js +33 -0
  122. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  123. package/dist/controllers/separator_controller.js +13 -17
  124. package/dist/controllers/separator_controller.js.map +1 -1
  125. package/dist/controllers/skeleton_controller.d.ts +2 -2
  126. package/dist/controllers/skeleton_controller.js +71 -3
  127. package/dist/controllers/skeleton_controller.js.map +1 -1
  128. package/dist/controllers/slider_controller.d.ts +17 -1
  129. package/dist/controllers/slider_controller.js +325 -48
  130. package/dist/controllers/slider_controller.js.map +1 -1
  131. package/dist/controllers/spinner_controller.d.ts +15 -10
  132. package/dist/controllers/spinner_controller.js +18 -3
  133. package/dist/controllers/spinner_controller.js.map +1 -1
  134. package/dist/controllers/step_indicator_controller.d.ts +1 -1
  135. package/dist/controllers/step_indicator_controller.js +3 -1
  136. package/dist/controllers/step_indicator_controller.js.map +1 -1
  137. package/dist/controllers/stepper_controller.js +2 -0
  138. package/dist/controllers/stepper_controller.js.map +1 -1
  139. package/dist/controllers/switch_controller.d.ts +12 -8
  140. package/dist/controllers/switch_controller.js +162 -18
  141. package/dist/controllers/switch_controller.js.map +1 -1
  142. package/dist/controllers/tags_input_controller.d.ts +41 -11
  143. package/dist/controllers/tags_input_controller.js +356 -120
  144. package/dist/controllers/tags_input_controller.js.map +1 -1
  145. package/dist/controllers/textarea_autosize_controller.js +1 -1
  146. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  147. package/dist/controllers/time_picker_controller.d.ts +42 -10
  148. package/dist/controllers/time_picker_controller.js +296 -104
  149. package/dist/controllers/time_picker_controller.js.map +1 -1
  150. package/dist/controllers/toggle_group_controller.d.ts +39 -13
  151. package/dist/controllers/toggle_group_controller.js +378 -55
  152. package/dist/controllers/toggle_group_controller.js.map +1 -1
  153. package/dist/controllers/toolbar_controller.js +5 -3
  154. package/dist/controllers/toolbar_controller.js.map +1 -1
  155. package/dist/controllers/tooltip_controller.d.ts +2 -2
  156. package/dist/controllers/tooltip_controller.js.map +1 -1
  157. package/dist/controllers/transition_controller.d.ts +2 -3
  158. package/dist/controllers/transition_controller.js.map +1 -1
  159. package/dist/controllers/tree_view_controller.d.ts +1 -2
  160. package/dist/controllers/tree_view_controller.js +24 -4
  161. package/dist/controllers/tree_view_controller.js.map +1 -1
  162. package/dist/index.js +3811 -1048
  163. package/dist/index.js.map +1 -1
  164. package/dist/inspector/cli.d.ts +92 -4
  165. package/dist/inspector/cli.js +174 -6
  166. package/dist/inspector/cli.js.map +1 -1
  167. package/dist/inspector/cli_bin.js +234 -8
  168. package/dist/inspector/cli_bin.js.map +1 -1
  169. package/dist/inspector/examples.json +24 -24
  170. package/dist/inspector/manifest.json +573 -59
  171. package/dist/positioning/index.d.ts +2 -2
  172. package/dist/positioning/index.js.map +1 -1
  173. package/package.json +3 -3
@@ -18,6 +18,35 @@ function isReservedArrowChord(event, allow = []) {
18
18
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
19
  }
20
20
 
21
+ // src/utils/before_cache_reset.ts
22
+ var BeforeCacheReset = class _BeforeCacheReset {
23
+ /** Every subscribed instance, iterated by the one shared document listener. */
24
+ static #subscribers = /* @__PURE__ */ new Set();
25
+ /** The shared listener; installed while at least one instance is subscribed. */
26
+ static #onBeforeCache = () => {
27
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
28
+ };
29
+ #rewind;
30
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
31
+ constructor(rewind) {
32
+ this.#rewind = rewind;
33
+ }
34
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
35
+ activate() {
36
+ const first = _BeforeCacheReset.#subscribers.size === 0;
37
+ _BeforeCacheReset.#subscribers.add(this);
38
+ if (first) {
39
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
40
+ }
41
+ }
42
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
43
+ deactivate() {
44
+ _BeforeCacheReset.#subscribers.delete(this);
45
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
46
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
47
+ }
48
+ };
49
+
21
50
  // src/utils/dates.ts
22
51
  function toISODateString(date) {
23
52
  const year = date.getFullYear();
@@ -138,16 +167,34 @@ var SafeTimeout = class extends TimerRegistry {
138
167
  }
139
168
  };
140
169
 
170
+ // src/utils/string_list.ts
171
+ function parseStringList(raw, fallback = []) {
172
+ const text = raw.trim();
173
+ if (text.length === 0) return [...fallback];
174
+ let parsed;
175
+ try {
176
+ parsed = JSON.parse(text);
177
+ } catch {
178
+ return [...fallback];
179
+ }
180
+ if (!Array.isArray(parsed)) return [...fallback];
181
+ return parsed.filter((entry) => typeof entry === "string");
182
+ }
183
+
141
184
  // src/controllers/date_range_picker_controller.ts
142
185
  var GRID_SIZE = 42;
143
186
  var DateRangePickerController = class extends Controller {
144
187
  static targets = ["grid", "monthLabel", "cell", "status", "startField", "endField"];
145
188
  static values = {
146
189
  min: { type: String, default: "" },
147
- max: { type: String, default: "" }
190
+ max: { type: String, default: "" },
191
+ // A JSON list read through `parseStringList` rather than Stimulus's `Array`
192
+ // type: that reader throws out of the value observer before any callback
193
+ // runs, so one malformed attribute would stop the picker from connecting.
194
+ disabledDates: { type: String, default: "" }
148
195
  };
149
196
  static actions = ["applyPreset", "next", "onKeydown", "prev", "previewTo", "selectDate"];
150
- static events = ["change"];
197
+ static events = ["change", "monthchange"];
151
198
  /** The month currently rendered, as `YYYY-MM`. */
152
199
  #viewMonth = "";
153
200
  /** The confirmed range endpoints (ISO), or "" when unset. */
@@ -161,7 +208,12 @@ var DateRangePickerController = class extends Controller {
161
208
  #focusedDate = /* @__PURE__ */ new Date();
162
209
  /** Deferred focus after an async month transition (cancelled on teardown). */
163
210
  #focusTimer = new SafeTimeout();
164
- /** Seeds the range from any pre-filled hidden fields and renders the grid. */
211
+ /** The declared unavailable dates, indexed for the per-cell paint lookup. */
212
+ #disabledDates = /* @__PURE__ */ new Set();
213
+ /** Rewinds an unfinished selection before Turbo snapshots the page. */
214
+ #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
215
+ /** Last painted month, or `null` until the initial paint has settled. */
216
+ #announcedMonth = null;
165
217
  /**
166
218
  * Collapses a morph that swaps render inputs into one repaint, and refuses the
167
219
  * pass Stimulus delivers before `connect()`.
@@ -169,10 +221,17 @@ var DateRangePickerController = class extends Controller {
169
221
  #repaint = new MicrotaskCoalescer(() => {
170
222
  this.#render();
171
223
  });
224
+ /** Seeds and normalizes the range from optional hidden fields, then paints the grid. */
172
225
  connect() {
173
226
  this.#repaint.activate();
174
- this.#startDate = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : "";
175
- this.#endDate = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : "";
227
+ this.#beforeCache.activate();
228
+ const authoredStart = this.hasStartFieldTarget ? normalizeISO(this.startFieldTarget.value) : "";
229
+ const authoredEnd = this.hasEndFieldTarget ? normalizeISO(this.endFieldTarget.value) : "";
230
+ [this.#startDate, this.#endDate] = orderRange(authoredStart, authoredEnd);
231
+ this.#pendingStart = "";
232
+ this.#previewDate = "";
233
+ this.#announcedMonth = null;
234
+ this.#commitFields();
176
235
  const anchor = parseISODateString(this.#startDate) ?? this.#clampToBounds(/* @__PURE__ */ new Date()) ?? /* @__PURE__ */ new Date();
177
236
  this.#focusedDate = anchor;
178
237
  this.#viewMonth = toISOMonthString(anchor);
@@ -182,6 +241,7 @@ var DateRangePickerController = class extends Controller {
182
241
  /** Cancels any pending deferred focus so it never fires on a detached element. */
183
242
  disconnect() {
184
243
  this.#repaint.cancel();
244
+ this.#beforeCache.deactivate();
185
245
  this.#focusTimer.clearAll();
186
246
  }
187
247
  /** Repaints when application code (or a Turbo morph) changes `min` at runtime. */
@@ -192,6 +252,11 @@ var DateRangePickerController = class extends Controller {
192
252
  maxValueChanged() {
193
253
  this.#repaint.schedule();
194
254
  }
255
+ /** Re-indexes the unavailable dates and repaints when the declared set changes. */
256
+ disabledDatesValueChanged() {
257
+ this.#disabledDates = new Set(parseStringList(this.disabledDatesValue));
258
+ this.#repaint.schedule();
259
+ }
195
260
  /** Navigates to the previous month. */
196
261
  prev(event) {
197
262
  event?.preventDefault();
@@ -207,7 +272,7 @@ var DateRangePickerController = class extends Controller {
207
272
  const cell = this.#cellFrom(event.target);
208
273
  if (!cell) return;
209
274
  const date = cell.getAttribute("data-date");
210
- if (!date || cell.getAttribute("aria-disabled") === "true") return;
275
+ if (!date || !this.#isSelectable(date)) return;
211
276
  this.#choose(date);
212
277
  }
213
278
  /** Previews the range up to a hovered/focused cell while selecting. */
@@ -216,28 +281,27 @@ var DateRangePickerController = class extends Controller {
216
281
  if (!cell) return;
217
282
  const date = cell.getAttribute("data-date");
218
283
  if (!date) return;
284
+ let shouldRender = false;
219
285
  if (event.type.startsWith("focus")) {
220
286
  const parsed = parseISODateString(date);
221
287
  if (parsed) this.#focusedDate = parsed;
222
- if (!this.#pendingStart && cell.getAttribute("tabindex") !== "0") {
223
- this.#render();
224
- }
288
+ shouldRender = cell.getAttribute("tabindex") !== "0";
225
289
  }
226
- if (this.#pendingStart) {
290
+ if (this.#pendingStart && this.#isSelectable(date) && this.#previewDate !== date) {
227
291
  this.#previewDate = date;
228
- this.#render();
292
+ shouldRender = true;
229
293
  }
294
+ if (shouldRender) this.#render();
230
295
  }
231
296
  /** Applies a named preset (`today` / `last7` / `last30` / `thisMonth`). */
232
297
  applyPreset(event) {
233
298
  const button = event.target?.closest("[data-range]");
234
- const name = button?.getAttribute("data-range");
235
- if (!name) return;
236
- const range = computePreset(name);
299
+ const range = computePreset(button?.getAttribute("data-range") ?? "");
237
300
  if (!range) return;
238
- const start = this.#clampISO(range.start);
239
- const end = this.#clampISO(range.end);
240
- if (!start || !end) return;
301
+ const intersection = this.#intersectRange(range);
302
+ if (!intersection) return;
303
+ const { start, end } = intersection;
304
+ if (!this.#isSelectable(start) || !this.#isSelectable(end)) return;
241
305
  this.#startDate = start;
242
306
  this.#endDate = end;
243
307
  this.#pendingStart = "";
@@ -255,12 +319,12 @@ var DateRangePickerController = class extends Controller {
255
319
  if (isReservedArrowChord(event)) return;
256
320
  const cell = this.#cellFrom(event.target);
257
321
  if (!cell) return;
258
- const dateStr = cell.getAttribute("data-date");
259
- const date = dateStr ? parseISODateString(dateStr) : null;
322
+ const dateStr = cell.getAttribute("data-date") ?? "";
323
+ const date = parseISODateString(dateStr);
260
324
  if (!date) return;
261
325
  if (event.key === "Enter" || event.key === " ") {
262
326
  event.preventDefault();
263
- if (dateStr && cell.getAttribute("aria-disabled") !== "true") this.#choose(dateStr);
327
+ if (this.#isSelectable(dateStr)) this.#choose(dateStr);
264
328
  return;
265
329
  }
266
330
  if (event.key === "Escape") {
@@ -293,10 +357,10 @@ var DateRangePickerController = class extends Controller {
293
357
  next = addDays(date, 6 - date.getDay());
294
358
  break;
295
359
  case "PageUp":
296
- next = shiftMonthClamped(date, -1);
360
+ next = event.shiftKey ? shiftYearClamped(date, -1) : shiftMonthClamped(date, -1);
297
361
  break;
298
362
  case "PageDown":
299
- next = shiftMonthClamped(date, 1);
363
+ next = event.shiftKey ? shiftYearClamped(date, 1) : shiftMonthClamped(date, 1);
300
364
  break;
301
365
  default:
302
366
  return;
@@ -327,12 +391,14 @@ var DateRangePickerController = class extends Controller {
327
391
  /** Moves roving focus to `date`, transitioning the month when needed. */
328
392
  #moveFocusTo(date) {
329
393
  this.#focusedDate = date;
330
- if (this.#pendingStart) this.#previewDate = toISODateString(date);
331
- this.#transitionTo(toISOMonthString(date), toISODateString(date));
394
+ const iso = toISODateString(date);
395
+ if (this.#pendingStart && this.#isSelectable(iso)) this.#previewDate = iso;
396
+ this.#transitionTo(toISOMonthString(date), iso);
332
397
  }
333
398
  /** Renders `month`, then focuses the cell for `dateStr` (deferred if async). */
334
399
  #transitionTo(month, dateStr) {
335
400
  const isTransition = month !== this.#viewMonth;
401
+ this.#focusTimer.clearAll();
336
402
  this.#viewMonth = month;
337
403
  this.#render();
338
404
  const focusCell = () => {
@@ -355,22 +421,28 @@ var DateRangePickerController = class extends Controller {
355
421
  this.#focusedDate = target;
356
422
  this.#render();
357
423
  }
358
- /** Builds the six-week grid and binds range/roving/disabled state per cell. */
424
+ /**
425
+ * Builds the six-week grid and binds range/roving/disabled state per cell.
426
+ *
427
+ * @stimeoRenderRoot
428
+ */
359
429
  #render() {
360
430
  const info = parseISOMonthString(this.#viewMonth);
361
431
  if (!info) return;
362
432
  const { year, month } = info;
433
+ if (this.#previewDate && !this.#isSelectable(this.#previewDate)) this.#previewDate = "";
363
434
  if (this.hasMonthLabelTarget) {
364
435
  const lang = document.documentElement.lang || "en";
365
- const formatter = new Intl.DateTimeFormat(lang, { month: "long", year: "numeric" });
436
+ const formatter = monthFormatter(lang);
366
437
  this.monthLabelTarget.textContent = formatter.format(new Date(year, month - 1, 1));
367
438
  }
368
439
  const [rangeStart, rangeEnd] = this.#visualRange();
369
440
  const days = gridDays(year, month);
370
441
  const focusedStr = toISODateString(this.#focusedDate);
371
442
  const todayStr = toISODateString(/* @__PURE__ */ new Date());
443
+ const cells = this.cellTargets;
372
444
  for (let i = 0; i < GRID_SIZE; i++) {
373
- const el = this.cellTargets[i];
445
+ const el = cells[i];
374
446
  const date = days[i];
375
447
  if (!el || !date) continue;
376
448
  const iso = toISODateString(date);
@@ -379,8 +451,8 @@ var DateRangePickerController = class extends Controller {
379
451
  el.setAttribute("data-outside", String(date.getMonth() !== month - 1));
380
452
  el.setAttribute("data-today", String(iso === todayStr));
381
453
  el.setAttribute("tabindex", iso === focusedStr ? "0" : "-1");
382
- if (this.#outOfBounds(iso)) el.setAttribute("aria-disabled", "true");
383
- else el.removeAttribute("aria-disabled");
454
+ if (this.#isSelectable(iso)) el.removeAttribute("aria-disabled");
455
+ else el.setAttribute("aria-disabled", "true");
384
456
  const isStart = !!rangeStart && iso === rangeStart;
385
457
  const isEnd = !!rangeEnd && iso === rangeEnd && rangeEnd !== rangeStart;
386
458
  const inside = !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;
@@ -394,6 +466,16 @@ var DateRangePickerController = class extends Controller {
394
466
  )
395
467
  );
396
468
  }
469
+ for (const extra of cells.slice(GRID_SIZE)) extra.setAttribute("tabindex", "-1");
470
+ if (!cells.some((cell) => cell.getAttribute("tabindex") === "0")) {
471
+ const fallback = cells.find((cell) => cell.getAttribute("data-outside") === "false") ?? cells[0];
472
+ fallback?.setAttribute("tabindex", "0");
473
+ }
474
+ const previous = this.#announcedMonth;
475
+ this.#announcedMonth = this.#viewMonth;
476
+ if (previous !== null && previous !== this.#viewMonth) {
477
+ this.dispatch("monthchange", { detail: { month: this.#viewMonth } });
478
+ }
397
479
  }
398
480
  /** The ordered [start, end] pair to paint: the preview while selecting, else confirmed. */
399
481
  #visualRange() {
@@ -414,15 +496,25 @@ var DateRangePickerController = class extends Controller {
414
496
  this.statusTarget.textContent = `${this.#startDate} \u2013 ${this.#endDate}`;
415
497
  }
416
498
  }
499
+ /**
500
+ * True when `iso` may be chosen as a range endpoint.
501
+ *
502
+ * The single availability question in the controller: the grid paint, the
503
+ * preview, and both commit paths ask it, so a cell's `aria-disabled` and what
504
+ * a click on that cell does are the same decision rather than two that have to
505
+ * be kept in step.
506
+ */
507
+ #isSelectable(iso) {
508
+ return !this.#outOfBounds(iso) && !this.#disabledDates.has(iso);
509
+ }
417
510
  /** True when `iso` falls outside the `[min, max]` bounds. */
418
511
  #outOfBounds(iso) {
419
512
  if (this.minValue && iso < this.minValue) return true;
420
513
  if (this.maxValue && iso > this.maxValue) return true;
421
514
  return false;
422
515
  }
423
- /** Clamps an ISO date string into `[min, max]`, or "" when unparseable. */
516
+ /** Clamps a generated ISO date string into `[min, max]`. */
424
517
  #clampISO(iso) {
425
- if (!iso) return "";
426
518
  if (this.minValue && iso < this.minValue) return this.minValue;
427
519
  if (this.maxValue && iso > this.maxValue) return this.maxValue;
428
520
  return iso;
@@ -432,6 +524,27 @@ var DateRangePickerController = class extends Controller {
432
524
  const clamped = this.#clampISO(toISODateString(date));
433
525
  return parseISODateString(clamped);
434
526
  }
527
+ /**
528
+ * Intersects a preset with `[min, max]`, rejecting a disjoint interval.
529
+ *
530
+ * Only the bounds narrow a preset here: `disabledDates` excludes single days,
531
+ * not sub-intervals, so it cannot shrink one to a still-contiguous range. A
532
+ * day it excludes is still refused as an endpoint — the caller checks that —
533
+ * but a preset that merely spans one keeps its span.
534
+ */
535
+ #intersectRange(range) {
536
+ const start = this.minValue && range.start < this.minValue ? this.minValue : range.start;
537
+ const end = this.maxValue && range.end > this.maxValue ? this.maxValue : range.end;
538
+ return start <= end ? { start, end } : null;
539
+ }
540
+ /** Removes provisional range state before Turbo freezes a cached snapshot. */
541
+ #rewindForCache() {
542
+ this.#focusTimer.clearAll();
543
+ if (!this.#pendingStart && !this.#previewDate) return;
544
+ this.#pendingStart = "";
545
+ this.#previewDate = "";
546
+ this.#render();
547
+ }
435
548
  /** Resolves the cell element from an event target, or null. */
436
549
  #cellFrom(target) {
437
550
  return target?.closest(
@@ -450,6 +563,12 @@ function shiftMonthClamped(date, delta) {
450
563
  target.setDate(Math.min(date.getDate(), lastDay));
451
564
  return target;
452
565
  }
566
+ function shiftYearClamped(date, delta) {
567
+ const target = new Date(date.getFullYear() + delta, date.getMonth(), 1);
568
+ const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
569
+ target.setDate(Math.min(date.getDate(), lastDay));
570
+ return target;
571
+ }
453
572
  function gridDays(year, month) {
454
573
  const first = new Date(year, month - 1, 1);
455
574
  const start = new Date(first);
@@ -466,6 +585,18 @@ function normalizeISO(value) {
466
585
  const date = parseISODateString(value.trim());
467
586
  return date ? toISODateString(date) : "";
468
587
  }
588
+ function orderRange(start, end) {
589
+ return start && end && end < start ? [end, start] : [start, end];
590
+ }
591
+ function monthFormatter(locale) {
592
+ const options = { month: "long", year: "numeric" };
593
+ try {
594
+ return new Intl.DateTimeFormat(locale, options);
595
+ } catch (error) {
596
+ if (!(error instanceof RangeError)) throw error;
597
+ return new Intl.DateTimeFormat("en", options);
598
+ }
599
+ }
469
600
  function computePreset(name) {
470
601
  const today = /* @__PURE__ */ new Date();
471
602
  const todayStr = toISODateString(today);