stimeo-ui 0.17.0 → 0.18.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +106 -1
  3. data/README.md +19 -10
  4. data/dist/cable/index.js +22 -14
  5. data/dist/controllers/accordion_controller.js +14 -6
  6. data/dist/controllers/alert_dialog_controller.js +145 -118
  7. data/dist/controllers/announcer_controller.js +113 -17
  8. data/dist/controllers/auto_submit_controller.js +185 -60
  9. data/dist/controllers/avatar_controller.js +55 -43
  10. data/dist/controllers/bulk_select_controller.js +64 -50
  11. data/dist/controllers/calendar_controller.js +55 -53
  12. data/dist/controllers/carousel_controller.js +124 -86
  13. data/dist/controllers/character_counter_controller.js +291 -95
  14. data/dist/controllers/checkbox_controller.js +6 -0
  15. data/dist/controllers/clipboard_controller.js +118 -27
  16. data/dist/controllers/collapsible_controller.js +52 -49
  17. data/dist/controllers/color_picker_controller.js +29 -7
  18. data/dist/controllers/combobox_controller.js +210 -71
  19. data/dist/controllers/command_palette_controller.js +289 -140
  20. data/dist/controllers/conditional_fields_controller.js +145 -58
  21. data/dist/controllers/confirm_controller.js +145 -118
  22. data/dist/controllers/context_menu_controller.js +138 -73
  23. data/dist/controllers/countdown_controller.js +109 -21
  24. data/dist/controllers/currency_input_controller.js +446 -72
  25. data/dist/controllers/data_grid_controller.js +134 -28
  26. data/dist/controllers/date_range_picker_controller.js +93 -72
  27. data/dist/controllers/dialog_controller.js +145 -118
  28. data/dist/controllers/direct_upload_controller.js +24 -14
  29. data/dist/controllers/dirty_form_controller.js +68 -31
  30. data/dist/controllers/dismissible_controller.js +64 -46
  31. data/dist/controllers/drawer_controller.js +145 -118
  32. data/dist/controllers/dropdown_controller.js +76 -63
  33. data/dist/controllers/editable_controller.js +199 -62
  34. data/dist/controllers/empty_state_controller.js +13 -6
  35. data/dist/controllers/file_dropzone_controller.js +55 -48
  36. data/dist/controllers/filter_controller.js +9 -2
  37. data/dist/controllers/flash_controller.js +66 -33
  38. data/dist/controllers/focus_controller.js +39 -14
  39. data/dist/controllers/form_field_controller.js +52 -43
  40. data/dist/controllers/form_validation_controller.js +18 -11
  41. data/dist/controllers/frame_loading_controller.js +55 -48
  42. data/dist/controllers/hover_card_controller.js +67 -61
  43. data/dist/controllers/idle_controller.js +10 -6
  44. data/dist/controllers/input_mask_controller.js +411 -24
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +15 -3
  47. data/dist/controllers/listbox_controller.js +260 -79
  48. data/dist/controllers/local_time_controller.js +48 -13
  49. data/dist/controllers/masonry_controller.js +11 -4
  50. data/dist/controllers/menu_controller.js +138 -82
  51. data/dist/controllers/menubar_controller.js +65 -19
  52. data/dist/controllers/meter_controller.js +270 -24
  53. data/dist/controllers/multi_select_controller.js +251 -94
  54. data/dist/controllers/navigation_menu_controller.js +67 -19
  55. data/dist/controllers/nested_form_controller.js +98 -64
  56. data/dist/controllers/network_status_controller.js +70 -52
  57. data/dist/controllers/number_input_controller.js +344 -94
  58. data/dist/controllers/optimistic_controller.js +29 -11
  59. data/dist/controllers/otp_controller.js +502 -104
  60. data/dist/controllers/overflow_indicator_controller.js +6 -0
  61. data/dist/controllers/overflow_menu_controller.js +136 -21
  62. data/dist/controllers/pagination_controller.js +47 -14
  63. data/dist/controllers/password_reveal_controller.js +52 -49
  64. data/dist/controllers/password_strength_controller.js +13 -6
  65. data/dist/controllers/persist_controller.js +48 -128
  66. data/dist/controllers/pointer_drag_controller.js +249 -23
  67. data/dist/controllers/popover_controller.js +82 -65
  68. data/dist/controllers/portal_controller.js +6 -0
  69. data/dist/controllers/preview_guard_controller.js +14 -3
  70. data/dist/controllers/progress_controller.js +271 -27
  71. data/dist/controllers/radio_group_controller.js +167 -80
  72. data/dist/controllers/range_slider_controller.js +27 -7
  73. data/dist/controllers/rating_controller.js +93 -57
  74. data/dist/controllers/read_more_controller.js +10 -6
  75. data/dist/controllers/reading_progress_controller.js +22 -5
  76. data/dist/controllers/relative_time_controller.js +75 -11
  77. data/dist/controllers/reset_on_restore_controller.js +6 -0
  78. data/dist/controllers/resizable_controller.js +55 -22
  79. data/dist/controllers/roving_controller.js +10 -4
  80. data/dist/controllers/scroll_area_controller.js +129 -88
  81. data/dist/controllers/scroll_restore_controller.js +14 -2
  82. data/dist/controllers/scroll_visibility_controller.js +93 -60
  83. data/dist/controllers/scrollspy_controller.js +11 -5
  84. data/dist/controllers/separator_controller.js +59 -43
  85. data/dist/controllers/sidebar_controller.js +150 -122
  86. data/dist/controllers/skeleton_controller.js +55 -48
  87. data/dist/controllers/slider_controller.js +27 -6
  88. data/dist/controllers/smart_sticky_header_controller.js +13 -5
  89. data/dist/controllers/sortable_controller.js +27 -14
  90. data/dist/controllers/spinner_controller.js +55 -48
  91. data/dist/controllers/step_indicator_controller.js +6 -0
  92. data/dist/controllers/stepper_controller.js +6 -0
  93. data/dist/controllers/stick_to_bottom_controller.js +19 -8
  94. data/dist/controllers/sticky_observer_controller.js +49 -43
  95. data/dist/controllers/submit_once_controller.js +145 -85
  96. data/dist/controllers/switch_controller.js +172 -28
  97. data/dist/controllers/tabs_controller.js +6 -1
  98. data/dist/controllers/tags_input_controller.js +157 -28
  99. data/dist/controllers/textarea_autosize_controller.js +40 -19
  100. data/dist/controllers/theme_controller.js +10 -4
  101. data/dist/controllers/time_picker_controller.js +192 -54
  102. data/dist/controllers/toast_controller.js +58 -27
  103. data/dist/controllers/toggle_group_controller.js +158 -56
  104. data/dist/controllers/toolbar_controller.js +150 -21
  105. data/dist/controllers/tree_view_controller.js +134 -25
  106. data/dist/index.js +2451 -1312
  107. data/dist/positioning/index.js +6 -0
  108. data/lib/generators/stimeo/install/install_generator.rb +7 -2
  109. data/lib/generators/stimeo/install/templates/stimeo.js +19 -6
  110. data/lib/stimeo/ui/version.rb +1 -1
  111. metadata +2 -2
@@ -30,17 +30,31 @@ function isReservedArrowChord(event, allow = []) {
30
30
  if (!event.key.startsWith("Arrow")) return false;
31
31
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
32
32
  }
33
+ function hasModifierChord(event) {
34
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
35
+ }
36
+
37
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
38
+ if (element.getAttribute(name) === value) return false;
39
+ if (value === null) element.removeAttribute(name);
40
+ else element.setAttribute(name, value);
41
+ return true;
42
+ }
33
43
 
34
44
  function sharedRegistry(key, create) {
35
45
  const symbol = Symbol.for(key);
36
46
  const scope = globalThis;
37
47
  const existing = scope[symbol];
38
48
  if (existing !== void 0) return existing;
39
- const registry = create();
40
- Object.defineProperty(globalThis, symbol, { value: registry });
41
- return registry;
49
+ const registry2 = create();
50
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
51
+ return registry2;
42
52
  }
43
53
 
54
+ var registry = sharedRegistry(
55
+ "stimeo-ui.attribute-lease.registry.v1",
56
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
57
+ );
44
58
  function isAttributeValue(value) {
45
59
  return value === null || typeof value === "string";
46
60
  }
@@ -57,24 +71,46 @@ function readLeaseRecord(element, record) {
57
71
  return void 0;
58
72
  }
59
73
  }
60
- var AttributeLease = class _AttributeLease {
61
- static #registry = sharedRegistry(
62
- "stimeo-ui.attribute-lease.registry.v1",
63
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
64
- );
74
+ function attributeStack(element, attribute) {
75
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
76
+ registry.stacks.set(element, byAttribute);
77
+ const known = byAttribute.get(attribute);
78
+ if (known) return known;
79
+ const found = [];
80
+ for (const record of registry.names.get(attribute) ?? []) {
81
+ const read = readLeaseRecord(element, record);
82
+ if (read) found.push({ ...read, record });
83
+ }
84
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
85
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
86
+ lease: null,
87
+ record,
88
+ written,
89
+ original,
90
+ linked: linked && index > 0
91
+ }));
92
+ byAttribute.set(attribute, stack);
93
+ return stack;
94
+ }
95
+ function findHold(stack, lease, record) {
96
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
97
+ }
98
+ var AttributeLease = class {
65
99
  #attribute;
66
100
  #record;
67
101
  #base;
102
+ #preserveAuthor;
68
103
  #held = /* @__PURE__ */ new Set();
69
104
  constructor(attribute, owner, options = {}) {
70
105
  this.#attribute = attribute;
71
106
  this.#record = `data-${owner}-${attribute}-lease`;
72
107
  this.#base = options.base === true;
73
- const names = _AttributeLease.#registry.names;
108
+ this.#preserveAuthor = options.preserveAuthor === true;
109
+ const names = registry.names;
74
110
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
75
111
  }
76
112
  write(element, value) {
77
- const stack = this.#stack(element);
113
+ const stack = attributeStack(element, this.#attribute);
78
114
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
79
115
  this.#held.add(element);
80
116
  const shows = !this.#covered(element, stack, hold);
@@ -84,8 +120,8 @@ var AttributeLease = class _AttributeLease {
84
120
  }
85
121
  return(element) {
86
122
  const held = this.#held.delete(element);
87
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
88
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
123
+ const known = registry.stacks.get(element)?.has(this.#attribute);
124
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
89
125
  const hold = this.#take(stack);
90
126
  if (!hold) return false;
91
127
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -103,31 +139,8 @@ var AttributeLease = class _AttributeLease {
103
139
  returnAll() {
104
140
  for (const element of Array.from(this.#held)) this.return(element);
105
141
  }
106
- #stack(element) {
107
- const stacks = _AttributeLease.#registry.stacks;
108
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
109
- stacks.set(element, byAttribute);
110
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
111
- byAttribute.set(this.#attribute, stack);
112
- return stack;
113
- }
114
- #seed(element) {
115
- const found = [];
116
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
117
- const read = readLeaseRecord(element, record);
118
- if (read) found.push({ ...read, record });
119
- }
120
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
121
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
122
- lease: null,
123
- record,
124
- written,
125
- original,
126
- linked: linked && index > 0
127
- }));
128
- }
129
142
  #take(stack) {
130
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
143
+ const hold = findHold(stack, this, this.#record);
131
144
  if (hold) hold.lease = this;
132
145
  return hold;
133
146
  }
@@ -136,7 +149,7 @@ var AttributeLease = class _AttributeLease {
136
149
  const top = stack.at(-1);
137
150
  const bottom = stack[0];
138
151
  const over = top?.written === current ? top : void 0;
139
- const from = this.#base ? bottom : over;
152
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
140
153
  const hold = {
141
154
  lease: this,
142
155
  record: this.#record,
@@ -166,15 +179,11 @@ var AttributeLease = class _AttributeLease {
166
179
  );
167
180
  });
168
181
  for (const [name, recorded] of records) {
169
- if (element.getAttribute(name) === recorded) continue;
170
- if (recorded === null) element.removeAttribute(name);
171
- else element.setAttribute(name, recorded);
182
+ setAttributeIfChanged(element, name, recorded);
172
183
  }
173
184
  }
174
185
  #reflect(element, value) {
175
- if (element.getAttribute(this.#attribute) === value) return;
176
- if (value === null) element.removeAttribute(this.#attribute);
177
- else element.setAttribute(this.#attribute, value);
186
+ setAttributeIfChanged(element, this.#attribute, value);
178
187
  }
179
188
  };
180
189
 
@@ -214,6 +223,9 @@ var MicrotaskCoalescer = class {
214
223
  this.#queued = false;
215
224
  this.#generation += 1;
216
225
  }
226
+ get pending() {
227
+ return this.#queued;
228
+ }
217
229
  schedule() {
218
230
  if (!this.#active || this.#queued) return;
219
231
  this.#queued = true;
@@ -244,6 +256,9 @@ var MorphRenderWatcher = class {
244
256
  schedule() {
245
257
  this.#pass.schedule();
246
258
  }
259
+ get pending() {
260
+ return this.#pass.pending;
261
+ }
247
262
  disconnect() {
248
263
  this.#listeners.dispose();
249
264
  this.#pass.cancel();
@@ -323,6 +338,11 @@ var RovingTabindex = class {
323
338
  if (focus) items[index]?.focus();
324
339
  }
325
340
  };
341
+ function deepActiveElement(ownerDocument) {
342
+ let active = ownerDocument.activeElement;
343
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
344
+ return active;
345
+ }
326
346
 
327
347
  function queryOpenDescendants(root, selector) {
328
348
  const matches = [];
@@ -340,6 +360,9 @@ function queryOpenDescendants(root, selector) {
340
360
  return matches;
341
361
  }
342
362
 
363
+ function holdsFocus(element) {
364
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
365
+ }
343
366
  var TabindexLoan = class _TabindexLoan {
344
367
  static #registry = sharedRegistry(
345
368
  "stimeo-ui.tabindex-loan.registry.v1",
@@ -347,19 +370,22 @@ var TabindexLoan = class _TabindexLoan {
347
370
  );
348
371
  #value;
349
372
  #record;
373
+ #preserveFocus;
350
374
  #lent = /* @__PURE__ */ new Set();
351
- constructor(value, owner) {
375
+ constructor(value, owner, options = {}) {
352
376
  this.#value = value;
353
377
  this.#record = `data-${owner}-tabindex-loan`;
378
+ this.#preserveFocus = options.preserveFocus === true;
354
379
  _TabindexLoan.#registry.names.add(this.#record);
355
380
  }
356
- lend(element) {
381
+ lend(element, options = {}) {
357
382
  const stack = this.#stack(element);
358
383
  const current = element.getAttribute("tabindex");
359
384
  const held = stack.find((hold) => hold.loan === this) ?? stack.find(
360
385
  (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
361
386
  );
362
387
  const linked = stack.at(-1)?.value === current;
388
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
363
389
  if (!held && current !== null && !linked) return;
364
390
  this.#lent.add(element);
365
391
  if (held) held.loan = this;
@@ -369,19 +395,28 @@ var TabindexLoan = class _TabindexLoan {
369
395
  element.setAttribute("tabindex", this.#value);
370
396
  }
371
397
  }
372
- returnAll() {
373
- for (const element of this.#lent) {
374
- const stack = this.#stack(element);
375
- const hold = stack.find((held) => held.loan === this);
376
- if (hold) this.#release(element, stack, hold);
398
+ return(element) {
399
+ const stack = this.#stack(element);
400
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
401
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
402
+ );
403
+ if (!hold) return false;
404
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
405
+ hold.loan = this;
406
+ this.#lent.add(element);
407
+ return false;
377
408
  }
378
- this.#lent.clear();
409
+ this.#lent.delete(element);
410
+ return true;
411
+ }
412
+ returnAll() {
413
+ for (const element of Array.from(this.#lent)) this.return(element);
379
414
  }
380
415
  reclaim(element) {
381
416
  if (element.getAttribute(this.#record) !== this.#value) return;
382
417
  const stack = this.#stack(element);
383
418
  const hold = stack.find((held) => held.loan === null && held.record === this.#record);
384
- if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
419
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
385
420
  }
386
421
  reclaimWithin(root) {
387
422
  for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
@@ -405,26 +440,26 @@ var TabindexLoan = class _TabindexLoan {
405
440
  for (const [index, hold] of holds.entries()) hold.linked = index > 0;
406
441
  return holds;
407
442
  }
408
- #release(element, stack, hold) {
443
+ #release(element, stack, hold, preserveFocus = false) {
409
444
  const current = element.getAttribute("tabindex");
410
445
  const index = stack.indexOf(hold);
411
446
  const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
447
+ if (owns && preserveFocus && holdsFocus(element)) return false;
412
448
  stack.splice(index, 1);
413
449
  const above = stack[index];
414
450
  if (above) above.linked &&= hold.linked;
415
451
  this.#mark(element, stack);
416
- if (!owns) return;
452
+ if (!owns) return true;
417
453
  const below = hold.linked ? stack[index - 1] : void 0;
418
454
  if (!below) element.removeAttribute("tabindex");
419
455
  else if (below.value !== current) element.setAttribute("tabindex", below.value);
456
+ return true;
420
457
  }
421
458
  #mark(element, stack) {
422
459
  const records = /* @__PURE__ */ new Map([[this.#record, null]]);
423
460
  for (const hold of stack) records.set(hold.record, hold.value);
424
461
  for (const [name, recorded] of records) {
425
- if (element.getAttribute(name) === recorded) continue;
426
- if (recorded === null) element.removeAttribute(name);
427
- else element.setAttribute(name, recorded);
462
+ setAttributeIfChanged(element, name, recorded);
428
463
  }
429
464
  }
430
465
  };
@@ -529,6 +564,7 @@ var RatingController = class _RatingController extends Controller {
529
564
  }
530
565
  onKeydown(event) {
531
566
  if (event.defaultPrevented || isReservedArrowChord(event) || this.readonlyValue) return;
567
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
532
568
  const current = this.#normalize(this.#safeValue);
533
569
  let next = null;
534
570
  const rtl = isRtl(this.element);
@@ -107,6 +107,13 @@ function stateReasonFor(event) {
107
107
  return "user";
108
108
  }
109
109
 
110
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
111
+ if (element.getAttribute(name) === value) return false;
112
+ if (value === null) element.removeAttribute(name);
113
+ else element.setAttribute(name, value);
114
+ return true;
115
+ }
116
+
110
117
  function sharedRegistry(key, create) {
111
118
  const symbol = Symbol.for(key);
112
119
  const scope = globalThis;
@@ -161,9 +168,8 @@ var StateRegions = class _StateRegions {
161
168
  const below = hold.linked ? stack[index - 1] : void 0;
162
169
  if (below) {
163
170
  if (region.hidden !== below.written) region.hidden = below.written;
164
- } else if (region.getAttribute("hidden") !== hold.authored) {
165
- if (hold.authored === null) region.removeAttribute("hidden");
166
- else region.setAttribute("hidden", hold.authored);
171
+ } else {
172
+ setAttributeIfChanged(region, "hidden", hold.authored);
167
173
  }
168
174
  }
169
175
  this.#mark(region, stack);
@@ -238,9 +244,7 @@ var StateRegions = class _StateRegions {
238
244
  );
239
245
  });
240
246
  for (const [name, recorded] of records) {
241
- if (region.getAttribute(name) === recorded) continue;
242
- if (recorded === null) region.removeAttribute(name);
243
- else region.setAttribute(name, recorded);
247
+ setAttributeIfChanged(region, name, recorded);
244
248
  }
245
249
  }
246
250
  #recorded(region, name) {
@@ -71,6 +71,24 @@ var LayoutObserver = class {
71
71
  }
72
72
  };
73
73
 
74
+ var ListenerSet = class {
75
+ #abort = new AbortController();
76
+ add(target, type, handler, options) {
77
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
78
+ }
79
+ dispose() {
80
+ this.#abort.abort();
81
+ this.#abort = new AbortController();
82
+ }
83
+ };
84
+
85
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
86
+ if (element.getAttribute(name) === value) return false;
87
+ if (value === null) element.removeAttribute(name);
88
+ else element.setAttribute(name, value);
89
+ return true;
90
+ }
91
+
74
92
  function sharedRegistry(key, create) {
75
93
  const symbol = Symbol.for(key);
76
94
  const scope = globalThis;
@@ -210,9 +228,7 @@ var StylePropertyLease = class _StylePropertyLease {
210
228
  );
211
229
  });
212
230
  for (const [name, recorded] of records) {
213
- if (element.getAttribute(name) === recorded) continue;
214
- if (recorded === null) element.removeAttribute(name);
215
- else element.setAttribute(name, recorded);
231
+ setAttributeIfChanged(element, name, recorded);
216
232
  }
217
233
  }
218
234
  #carries(element, hold) {
@@ -238,10 +254,11 @@ var ReadingProgressController = class extends Controller {
238
254
  #progress = -1;
239
255
  #baselined = false;
240
256
  #onScroll = () => this.#frames.schedule(() => this.#measure());
257
+ #listeners = new ListenerSet();
241
258
  connect() {
242
259
  this.#progress = -1;
243
260
  this.#baselined = false;
244
- window.addEventListener("scroll", this.#onScroll, { passive: true, capture: true });
261
+ this.#listeners.add(window, "scroll", this.#onScroll, { passive: true, capture: true });
245
262
  this.#layout.observe(this.element);
246
263
  this.#layout.observeViewport();
247
264
  this.#measure();
@@ -251,7 +268,7 @@ var ReadingProgressController = class extends Controller {
251
268
  });
252
269
  }
253
270
  disconnect() {
254
- window.removeEventListener("scroll", this.#onScroll, { capture: true });
271
+ this.#listeners.dispose();
255
272
  this.#layout.disconnect();
256
273
  this.#frames.cancel();
257
274
  this.#lease.return(this.element);
@@ -1,5 +1,52 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
4
+ if (element.getAttribute(name) === value) return false;
5
+ if (value === null) element.removeAttribute(name);
6
+ else element.setAttribute(name, value);
7
+ return true;
8
+ }
9
+
10
+ var DATETIME = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
11
+ function readDatetime(element) {
12
+ const raw = element.getAttribute("datetime");
13
+ if (raw === null) return null;
14
+ const match = DATETIME.exec(raw.trim());
15
+ if (match === null) return null;
16
+ const [
17
+ ,
18
+ yearText,
19
+ monthText,
20
+ dayText,
21
+ hourText = "00",
22
+ minuteText = "00",
23
+ secondText = "00",
24
+ fraction = "",
25
+ zone = "Z"
26
+ ] = match;
27
+ const year = Number(yearText);
28
+ const month = Number(monthText);
29
+ const day = Number(dayText);
30
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
31
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
32
+ if (month < 1 || month > 12 || day < 1 || day > (days[month - 1] ?? 0)) return null;
33
+ const hour = Number(hourText);
34
+ const minute = Number(minuteText);
35
+ const second = Number(secondText);
36
+ if (hour === 24 && (minute !== 0 || second !== 0 || /[1-9]/.test(fraction))) return null;
37
+ let offset = zone;
38
+ if (zone !== "Z") {
39
+ const digits = zone.slice(1).replace(":", "");
40
+ const offsetHour = digits.slice(0, 2);
41
+ const offsetMinute = digits.slice(2);
42
+ offset = `${zone[0]}${offsetHour}:${offsetMinute}`;
43
+ }
44
+ const milliseconds = fraction.slice(0, 3).padEnd(3, "0");
45
+ const canonical = `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}.${milliseconds}${offset}`;
46
+ const instant = Date.parse(canonical);
47
+ return Number.isNaN(instant) ? null : instant;
48
+ }
49
+
3
50
  var formatters = /* @__PURE__ */ new WeakMap();
4
51
  function intlFormatter(factory, locale, options, fallbackLocale) {
5
52
  const cache = cacheFor(factory);
@@ -59,6 +106,9 @@ var MicrotaskCoalescer = class {
59
106
  this.#queued = false;
60
107
  this.#generation += 1;
61
108
  }
109
+ get pending() {
110
+ return this.#queued;
111
+ }
62
112
  schedule() {
63
113
  if (!this.#active || this.#queued) return;
64
114
  this.#queued = true;
@@ -89,6 +139,9 @@ var MorphRenderWatcher = class {
89
139
  schedule() {
90
140
  this.#pass.schedule();
91
141
  }
142
+ get pending() {
143
+ return this.#pass.pending;
144
+ }
92
145
  disconnect() {
93
146
  this.#listeners.dispose();
94
147
  this.#pass.cancel();
@@ -202,6 +255,12 @@ var RelativeTimeController = class _RelativeTimeController extends Controller {
202
255
  };
203
256
  #timers = new SafeTimeout();
204
257
  #resync = new MorphRenderWatcher(() => {
258
+ const target = readDatetime(this.element) ?? Number.NaN;
259
+ if (!Object.is(target, this.#targetMs)) {
260
+ this.#targetMs = target;
261
+ this.#adoptAuthoredText();
262
+ this.#valuesDirty = true;
263
+ }
205
264
  if (this.#valuesDirty) {
206
265
  this.#valuesDirty = false;
207
266
  this.#resyncToValues();
@@ -210,25 +269,32 @@ var RelativeTimeController = class _RelativeTimeController extends Controller {
210
269
  }
211
270
  });
212
271
  #valuesDirty = false;
272
+ #datetimeWatch = new MutationObserver(() => this.#resync.schedule());
213
273
  #targetMs = Number.NaN;
214
274
  #absoluteText = "";
215
275
  connect() {
276
+ this.#timers.clearAll();
216
277
  this.#valuesDirty = false;
217
278
  this.#resync.observe(this.element);
218
- if (this.element.getAttribute("data-state") !== "relative") {
219
- this.#absoluteText = (this.element.textContent ?? "").trim();
220
- } else {
221
- const recorded = this.#recordedText();
222
- if (recorded !== null) this.#absoluteText = recorded;
223
- }
224
- this.#targetMs = Date.parse(this.element.getAttribute("datetime") ?? "");
279
+ this.#datetimeWatch.observe(this.element, { attributeFilter: ["datetime"] });
280
+ this.#adoptAuthoredText();
281
+ this.#targetMs = readDatetime(this.element) ?? Number.NaN;
225
282
  if (Number.isNaN(this.#targetMs)) return;
226
283
  this.#schedule();
227
284
  }
228
285
  disconnect() {
229
286
  this.#resync.disconnect();
287
+ this.#datetimeWatch.disconnect();
230
288
  this.#timers.clearAll();
231
289
  }
290
+ #adoptAuthoredText() {
291
+ if (this.element.getAttribute("data-state") !== "relative") {
292
+ this.#absoluteText = (this.element.textContent ?? "").trim();
293
+ } else {
294
+ const recorded = this.#recordedText();
295
+ if (recorded !== null) this.#absoluteText = recorded;
296
+ }
297
+ }
232
298
  get #textRecord() {
233
299
  return `data-${this.identifier}-${TEXT_RECORD}`;
234
300
  }
@@ -253,8 +319,8 @@ var RelativeTimeController = class _RelativeTimeController extends Controller {
253
319
  this.#resync.schedule();
254
320
  }
255
321
  #resyncToValues() {
256
- if (Number.isNaN(this.#targetMs)) return;
257
322
  this.#timers.clearAll();
323
+ if (Number.isNaN(this.#targetMs)) return;
258
324
  this.#schedule();
259
325
  }
260
326
  #schedule() {
@@ -282,9 +348,7 @@ var RelativeTimeController = class _RelativeTimeController extends Controller {
282
348
  this.element.textContent = formatter.format(value, scale.unit);
283
349
  this.element.setAttribute("data-state", "relative");
284
350
  const record = JSON.stringify(this.#absoluteText);
285
- if (this.element.getAttribute(this.#textRecord) !== record) {
286
- this.element.setAttribute(this.#textRecord, record);
287
- }
351
+ setAttributeIfChanged(this.element, this.#textRecord, record);
288
352
  return nextDelay;
289
353
  }
290
354
  get #formatter() {
@@ -46,6 +46,9 @@ var MicrotaskCoalescer = class {
46
46
  this.#queued = false;
47
47
  this.#generation += 1;
48
48
  }
49
+ get pending() {
50
+ return this.#queued;
51
+ }
49
52
  schedule() {
50
53
  if (!this.#active || this.#queued) return;
51
54
  this.#queued = true;
@@ -76,6 +79,9 @@ var MorphRenderWatcher = class {
76
79
  schedule() {
77
80
  this.#pass.schedule();
78
81
  }
82
+ get pending() {
83
+ return this.#pass.pending;
84
+ }
79
85
  disconnect() {
80
86
  this.#listeners.dispose();
81
87
  this.#pass.cancel();