stimeo-ui 0.17.0 → 0.18.1

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 +136 -2
  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 +290 -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 +66 -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 +200 -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 +434 -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 +272 -95
  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 +255 -24
  67. data/dist/controllers/popover_controller.js +91 -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 +2515 -1315
  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
@@ -157,6 +157,9 @@ var MicrotaskCoalescer = class {
157
157
  this.#queued = false;
158
158
  this.#generation += 1;
159
159
  }
160
+ get pending() {
161
+ return this.#queued;
162
+ }
160
163
  schedule() {
161
164
  if (!this.#active || this.#queued) return;
162
165
  this.#queued = true;
@@ -187,6 +190,9 @@ var MorphRenderWatcher = class {
187
190
  schedule() {
188
191
  this.#pass.schedule();
189
192
  }
193
+ get pending() {
194
+ return this.#pass.pending;
195
+ }
190
196
  disconnect() {
191
197
  this.#listeners.dispose();
192
198
  this.#pass.cancel();
@@ -35,6 +35,11 @@ function canTakeFocus(element) {
35
35
  if (element.disabled) return false;
36
36
  return !inheritsFieldsetDisabled(element);
37
37
  }
38
+ function deepActiveElement(ownerDocument) {
39
+ let active = ownerDocument.activeElement;
40
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
41
+ return active;
42
+ }
38
43
 
39
44
  var LayoutObserver = class {
40
45
  #callback;
@@ -118,6 +123,9 @@ var MicrotaskCoalescer = class {
118
123
  this.#queued = false;
119
124
  this.#generation += 1;
120
125
  }
126
+ get pending() {
127
+ return this.#queued;
128
+ }
121
129
  schedule() {
122
130
  if (!this.#active || this.#queued) return;
123
131
  this.#queued = true;
@@ -148,6 +156,9 @@ var MorphRenderWatcher = class {
148
156
  schedule() {
149
157
  this.#pass.schedule();
150
158
  }
159
+ get pending() {
160
+ return this.#pass.pending;
161
+ }
151
162
  disconnect() {
152
163
  this.#listeners.dispose();
153
164
  this.#pass.cancel();
@@ -201,6 +212,94 @@ var NumberValueReader = class {
201
212
  }
202
213
  };
203
214
 
215
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
216
+ if (element.getAttribute(name) === value) return false;
217
+ if (value === null) element.removeAttribute(name);
218
+ else element.setAttribute(name, value);
219
+ return true;
220
+ }
221
+
222
+ var OwnedText = class {
223
+ #attribute;
224
+ #channel;
225
+ #guard;
226
+ #writes = /* @__PURE__ */ new WeakMap();
227
+ constructor(attribute, channel = "text", guard = () => true) {
228
+ this.#attribute = attribute;
229
+ this.#channel = channel;
230
+ this.#guard = guard;
231
+ }
232
+ hasRecord(element) {
233
+ return element.hasAttribute(this.#attribute);
234
+ }
235
+ read(element, evidence = {}) {
236
+ if (evidence.source === "live") return this.#writes.get(element) ?? null;
237
+ const raw = element.getAttribute(this.#attribute);
238
+ if (raw === null) return null;
239
+ try {
240
+ const record = JSON.parse(raw);
241
+ if (!Array.isArray(record) || record.length !== 3 || record[0] !== this.#channel || typeof record[1] !== "string" || typeof record[2] !== "string")
242
+ return null;
243
+ return { original: record[1], written: record[2] };
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+ owns(element, evidence = {}) {
249
+ return this.#ownedRecord(element, evidence) !== null;
250
+ }
251
+ #ownedRecord(element, evidence) {
252
+ const record = this.read(element, evidence);
253
+ if (record === null || !this.#guard(element)) return null;
254
+ if (this.#channel === "text" && element.firstElementChild !== null) return null;
255
+ const current = this.#current(element);
256
+ return current === record.written || evidence.source === "live" && evidence.allowOriginal === true && current === record.original ? record : null;
257
+ }
258
+ write(element, written, original, force = false) {
259
+ const current = this.#current(element);
260
+ if (current === null) return;
261
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
262
+ if (force || current !== written) this.#put(element, written);
263
+ this.remember(element, written, before);
264
+ }
265
+ remember(element, written, original) {
266
+ const current = this.#current(element);
267
+ if (current === null) return;
268
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
269
+ this.#writes.set(element, { original: before, written });
270
+ setAttributeIfChanged(
271
+ element,
272
+ this.#attribute,
273
+ JSON.stringify([this.#channel, before, written])
274
+ );
275
+ }
276
+ clear(element, evidence = {}) {
277
+ if (!this.owns(element, evidence)) return false;
278
+ if (this.#current(element) !== "") this.#put(element, "");
279
+ this.forget(element);
280
+ return true;
281
+ }
282
+ restore(element, evidence = {}) {
283
+ const record = this.#ownedRecord(element, evidence);
284
+ if (record === null) return false;
285
+ if (this.#current(element) !== record.original) this.#put(element, record.original);
286
+ this.forget(element);
287
+ return true;
288
+ }
289
+ forget(element) {
290
+ setAttributeIfChanged(element, this.#attribute, null);
291
+ this.#writes.delete(element);
292
+ }
293
+ #current(element) {
294
+ if (this.#channel === "text") return element.textContent ?? "";
295
+ return element instanceof HTMLInputElement ? element.value : null;
296
+ }
297
+ #put(element, text) {
298
+ if (this.#channel === "text") element.textContent = text;
299
+ else if (element instanceof HTMLInputElement) element.value = text;
300
+ }
301
+ };
302
+
204
303
  var TimerRegistry = class {
205
304
  ids = /* @__PURE__ */ new Set();
206
305
  clear(id) {
@@ -261,6 +360,9 @@ function sharedRegistry(key, create) {
261
360
  return registry;
262
361
  }
263
362
 
363
+ function holdsFocus(element) {
364
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
365
+ }
264
366
  var TabindexLoan = class _TabindexLoan {
265
367
  static #registry = sharedRegistry(
266
368
  "stimeo-ui.tabindex-loan.registry.v1",
@@ -268,19 +370,22 @@ var TabindexLoan = class _TabindexLoan {
268
370
  );
269
371
  #value;
270
372
  #record;
373
+ #preserveFocus;
271
374
  #lent = /* @__PURE__ */ new Set();
272
- constructor(value, owner) {
375
+ constructor(value, owner, options = {}) {
273
376
  this.#value = value;
274
377
  this.#record = `data-${owner}-tabindex-loan`;
378
+ this.#preserveFocus = options.preserveFocus === true;
275
379
  _TabindexLoan.#registry.names.add(this.#record);
276
380
  }
277
- lend(element) {
381
+ lend(element, options = {}) {
278
382
  const stack = this.#stack(element);
279
383
  const current = element.getAttribute("tabindex");
280
384
  const held = stack.find((hold) => hold.loan === this) ?? stack.find(
281
385
  (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
282
386
  );
283
387
  const linked = stack.at(-1)?.value === current;
388
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
284
389
  if (!held && current !== null && !linked) return;
285
390
  this.#lent.add(element);
286
391
  if (held) held.loan = this;
@@ -290,19 +395,28 @@ var TabindexLoan = class _TabindexLoan {
290
395
  element.setAttribute("tabindex", this.#value);
291
396
  }
292
397
  }
293
- returnAll() {
294
- for (const element of this.#lent) {
295
- const stack = this.#stack(element);
296
- const hold = stack.find((held) => held.loan === this);
297
- 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;
298
408
  }
299
- 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);
300
414
  }
301
415
  reclaim(element) {
302
416
  if (element.getAttribute(this.#record) !== this.#value) return;
303
417
  const stack = this.#stack(element);
304
418
  const hold = stack.find((held) => held.loan === null && held.record === this.#record);
305
- if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
419
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
306
420
  }
307
421
  reclaimWithin(root) {
308
422
  for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
@@ -326,26 +440,26 @@ var TabindexLoan = class _TabindexLoan {
326
440
  for (const [index, hold] of holds.entries()) hold.linked = index > 0;
327
441
  return holds;
328
442
  }
329
- #release(element, stack, hold) {
443
+ #release(element, stack, hold, preserveFocus = false) {
330
444
  const current = element.getAttribute("tabindex");
331
445
  const index = stack.indexOf(hold);
332
446
  const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
447
+ if (owns && preserveFocus && holdsFocus(element)) return false;
333
448
  stack.splice(index, 1);
334
449
  const above = stack[index];
335
450
  if (above) above.linked &&= hold.linked;
336
451
  this.#mark(element, stack);
337
- if (!owns) return;
452
+ if (!owns) return true;
338
453
  const below = hold.linked ? stack[index - 1] : void 0;
339
454
  if (!below) element.removeAttribute("tabindex");
340
455
  else if (below.value !== current) element.setAttribute("tabindex", below.value);
456
+ return true;
341
457
  }
342
458
  #mark(element, stack) {
343
459
  const records = /* @__PURE__ */ new Map([[this.#record, null]]);
344
460
  for (const hold of stack) records.set(hold.record, hold.value);
345
461
  for (const [name, recorded] of records) {
346
- if (element.getAttribute(name) === recorded) continue;
347
- if (recorded === null) element.removeAttribute(name);
348
- else element.setAttribute(name, recorded);
462
+ setAttributeIfChanged(element, name, recorded);
349
463
  }
350
464
  }
351
465
  };
@@ -360,6 +474,11 @@ var MENU_TARGET = "data-stimeo--menu-target";
360
474
  var BOOKKEEPING = [BANKED, INDEX, SAVED_ROLE, SAVED_TABINDEX, SAVED_MENU_TARGET];
361
475
  var OWNED_LABEL = "owns-label";
362
476
  var OverflowMenuController = class _OverflowMenuController extends Controller {
477
+ #labelText = new OwnedText(
478
+ this.#attr(OWNED_LABEL),
479
+ "text",
480
+ (trigger) => this.#carriesOnlyText(trigger)
481
+ );
363
482
  #numbers = new NumberValueReader();
364
483
  #attr(name) {
365
484
  return `data-${this.identifier}-${name}`;
@@ -643,17 +762,13 @@ var OverflowMenuController = class _OverflowMenuController extends Controller {
643
762
  this.#releaseLabel(trigger);
644
763
  return;
645
764
  }
646
- if (owned && trigger.textContent === label) return;
647
- trigger.textContent = label;
648
- trigger.setAttribute(this.#attr(OWNED_LABEL), label);
765
+ this.#labelText.write(trigger, label);
649
766
  }
650
767
  #ownsLabel(trigger) {
651
- return trigger.getAttribute(this.#attr(OWNED_LABEL)) === trigger.textContent && this.#carriesOnlyText(trigger);
768
+ return this.#labelText.owns(trigger);
652
769
  }
653
770
  #releaseLabel(trigger) {
654
- if (!this.#ownsLabel(trigger)) return;
655
- trigger.textContent = "";
656
- trigger.removeAttribute(this.#attr(OWNED_LABEL));
771
+ this.#labelText.clear(trigger);
657
772
  }
658
773
  #isBareTrigger(trigger) {
659
774
  return (trigger.textContent ?? "").trim() === "" && this.#carriesOnlyText(trigger);
@@ -49,6 +49,11 @@ function canTakeFocus(element) {
49
49
  if (element.disabled) return false;
50
50
  return !inheritsFieldsetDisabled(element);
51
51
  }
52
+ function deepActiveElement(ownerDocument) {
53
+ let active = ownerDocument.activeElement;
54
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
55
+ return active;
56
+ }
52
57
 
53
58
  var ListenerSet = class {
54
59
  #abort = new AbortController();
@@ -77,6 +82,9 @@ var MicrotaskCoalescer = class {
77
82
  this.#queued = false;
78
83
  this.#generation += 1;
79
84
  }
85
+ get pending() {
86
+ return this.#queued;
87
+ }
80
88
  schedule() {
81
89
  if (!this.#active || this.#queued) return;
82
90
  this.#queued = true;
@@ -107,6 +115,9 @@ var MorphRenderWatcher = class {
107
115
  schedule() {
108
116
  this.#pass.schedule();
109
117
  }
118
+ get pending() {
119
+ return this.#pass.pending;
120
+ }
110
121
  disconnect() {
111
122
  this.#listeners.dispose();
112
123
  this.#pass.cancel();
@@ -169,6 +180,13 @@ var NumberValueReader = class {
169
180
  }
170
181
  };
171
182
 
183
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
184
+ if (element.getAttribute(name) === value) return false;
185
+ if (value === null) element.removeAttribute(name);
186
+ else element.setAttribute(name, value);
187
+ return true;
188
+ }
189
+
172
190
  function queryOpenDescendants(root, selector) {
173
191
  const matches = [];
174
192
  const roots = [root];
@@ -195,6 +213,9 @@ function sharedRegistry(key, create) {
195
213
  return registry;
196
214
  }
197
215
 
216
+ function holdsFocus(element) {
217
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
218
+ }
198
219
  var TabindexLoan = class _TabindexLoan {
199
220
  static #registry = sharedRegistry(
200
221
  "stimeo-ui.tabindex-loan.registry.v1",
@@ -202,19 +223,22 @@ var TabindexLoan = class _TabindexLoan {
202
223
  );
203
224
  #value;
204
225
  #record;
226
+ #preserveFocus;
205
227
  #lent = /* @__PURE__ */ new Set();
206
- constructor(value, owner) {
228
+ constructor(value, owner, options = {}) {
207
229
  this.#value = value;
208
230
  this.#record = `data-${owner}-tabindex-loan`;
231
+ this.#preserveFocus = options.preserveFocus === true;
209
232
  _TabindexLoan.#registry.names.add(this.#record);
210
233
  }
211
- lend(element) {
234
+ lend(element, options = {}) {
212
235
  const stack = this.#stack(element);
213
236
  const current = element.getAttribute("tabindex");
214
237
  const held = stack.find((hold) => hold.loan === this) ?? stack.find(
215
238
  (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
216
239
  );
217
240
  const linked = stack.at(-1)?.value === current;
241
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
218
242
  if (!held && current !== null && !linked) return;
219
243
  this.#lent.add(element);
220
244
  if (held) held.loan = this;
@@ -224,19 +248,28 @@ var TabindexLoan = class _TabindexLoan {
224
248
  element.setAttribute("tabindex", this.#value);
225
249
  }
226
250
  }
227
- returnAll() {
228
- for (const element of this.#lent) {
229
- const stack = this.#stack(element);
230
- const hold = stack.find((held) => held.loan === this);
231
- if (hold) this.#release(element, stack, hold);
251
+ return(element) {
252
+ const stack = this.#stack(element);
253
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
254
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
255
+ );
256
+ if (!hold) return false;
257
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
258
+ hold.loan = this;
259
+ this.#lent.add(element);
260
+ return false;
232
261
  }
233
- this.#lent.clear();
262
+ this.#lent.delete(element);
263
+ return true;
264
+ }
265
+ returnAll() {
266
+ for (const element of Array.from(this.#lent)) this.return(element);
234
267
  }
235
268
  reclaim(element) {
236
269
  if (element.getAttribute(this.#record) !== this.#value) return;
237
270
  const stack = this.#stack(element);
238
271
  const hold = stack.find((held) => held.loan === null && held.record === this.#record);
239
- if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
272
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
240
273
  }
241
274
  reclaimWithin(root) {
242
275
  for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
@@ -260,26 +293,26 @@ var TabindexLoan = class _TabindexLoan {
260
293
  for (const [index, hold] of holds.entries()) hold.linked = index > 0;
261
294
  return holds;
262
295
  }
263
- #release(element, stack, hold) {
296
+ #release(element, stack, hold, preserveFocus = false) {
264
297
  const current = element.getAttribute("tabindex");
265
298
  const index = stack.indexOf(hold);
266
299
  const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
300
+ if (owns && preserveFocus && holdsFocus(element)) return false;
267
301
  stack.splice(index, 1);
268
302
  const above = stack[index];
269
303
  if (above) above.linked &&= hold.linked;
270
304
  this.#mark(element, stack);
271
- if (!owns) return;
305
+ if (!owns) return true;
272
306
  const below = hold.linked ? stack[index - 1] : void 0;
273
307
  if (!below) element.removeAttribute("tabindex");
274
308
  else if (below.value !== current) element.setAttribute("tabindex", below.value);
309
+ return true;
275
310
  }
276
311
  #mark(element, stack) {
277
312
  const records = /* @__PURE__ */ new Map([[this.#record, null]]);
278
313
  for (const hold of stack) records.set(hold.record, hold.value);
279
314
  for (const [name, recorded] of records) {
280
- if (element.getAttribute(name) === recorded) continue;
281
- if (recorded === null) element.removeAttribute(name);
282
- else element.setAttribute(name, recorded);
315
+ setAttributeIfChanged(element, name, recorded);
283
316
  }
284
317
  }
285
318
  };
@@ -1,15 +1,26 @@
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
+
3
10
  function sharedRegistry(key, create) {
4
11
  const symbol = Symbol.for(key);
5
12
  const scope = globalThis;
6
13
  const existing = scope[symbol];
7
14
  if (existing !== void 0) return existing;
8
- const registry = create();
9
- Object.defineProperty(globalThis, symbol, { value: registry });
10
- return registry;
15
+ const registry2 = create();
16
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
17
+ return registry2;
11
18
  }
12
19
 
20
+ var registry = sharedRegistry(
21
+ "stimeo-ui.attribute-lease.registry.v1",
22
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
23
+ );
13
24
  function isAttributeValue(value) {
14
25
  return value === null || typeof value === "string";
15
26
  }
@@ -26,24 +37,46 @@ function readLeaseRecord(element, record) {
26
37
  return void 0;
27
38
  }
28
39
  }
29
- var AttributeLease = class _AttributeLease {
30
- static #registry = sharedRegistry(
31
- "stimeo-ui.attribute-lease.registry.v1",
32
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
- );
40
+ function attributeStack(element, attribute) {
41
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
42
+ registry.stacks.set(element, byAttribute);
43
+ const known = byAttribute.get(attribute);
44
+ if (known) return known;
45
+ const found = [];
46
+ for (const record of registry.names.get(attribute) ?? []) {
47
+ const read = readLeaseRecord(element, record);
48
+ if (read) found.push({ ...read, record });
49
+ }
50
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
51
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
52
+ lease: null,
53
+ record,
54
+ written,
55
+ original,
56
+ linked: linked && index > 0
57
+ }));
58
+ byAttribute.set(attribute, stack);
59
+ return stack;
60
+ }
61
+ function findHold(stack, lease, record) {
62
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
63
+ }
64
+ var AttributeLease = class {
34
65
  #attribute;
35
66
  #record;
36
67
  #base;
68
+ #preserveAuthor;
37
69
  #held = /* @__PURE__ */ new Set();
38
70
  constructor(attribute, owner, options = {}) {
39
71
  this.#attribute = attribute;
40
72
  this.#record = `data-${owner}-${attribute}-lease`;
41
73
  this.#base = options.base === true;
42
- const names = _AttributeLease.#registry.names;
74
+ this.#preserveAuthor = options.preserveAuthor === true;
75
+ const names = registry.names;
43
76
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
77
  }
45
78
  write(element, value) {
46
- const stack = this.#stack(element);
79
+ const stack = attributeStack(element, this.#attribute);
47
80
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
81
  this.#held.add(element);
49
82
  const shows = !this.#covered(element, stack, hold);
@@ -53,8 +86,8 @@ var AttributeLease = class _AttributeLease {
53
86
  }
54
87
  return(element) {
55
88
  const held = this.#held.delete(element);
56
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
89
+ const known = registry.stacks.get(element)?.has(this.#attribute);
90
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
58
91
  const hold = this.#take(stack);
59
92
  if (!hold) return false;
60
93
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -72,31 +105,8 @@ var AttributeLease = class _AttributeLease {
72
105
  returnAll() {
73
106
  for (const element of Array.from(this.#held)) this.return(element);
74
107
  }
75
- #stack(element) {
76
- const stacks = _AttributeLease.#registry.stacks;
77
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
- stacks.set(element, byAttribute);
79
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
- byAttribute.set(this.#attribute, stack);
81
- return stack;
82
- }
83
- #seed(element) {
84
- const found = [];
85
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
- const read = readLeaseRecord(element, record);
87
- if (read) found.push({ ...read, record });
88
- }
89
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
- lease: null,
92
- record,
93
- written,
94
- original,
95
- linked: linked && index > 0
96
- }));
97
- }
98
108
  #take(stack) {
99
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
109
+ const hold = findHold(stack, this, this.#record);
100
110
  if (hold) hold.lease = this;
101
111
  return hold;
102
112
  }
@@ -105,7 +115,7 @@ var AttributeLease = class _AttributeLease {
105
115
  const top = stack.at(-1);
106
116
  const bottom = stack[0];
107
117
  const over = top?.written === current ? top : void 0;
108
- const from = this.#base ? bottom : over;
118
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
109
119
  const hold = {
110
120
  lease: this,
111
121
  record: this.#record,
@@ -135,15 +145,11 @@ var AttributeLease = class _AttributeLease {
135
145
  );
136
146
  });
137
147
  for (const [name, recorded] of records) {
138
- if (element.getAttribute(name) === recorded) continue;
139
- if (recorded === null) element.removeAttribute(name);
140
- else element.setAttribute(name, recorded);
148
+ setAttributeIfChanged(element, name, recorded);
141
149
  }
142
150
  }
143
151
  #reflect(element, value) {
144
- if (element.getAttribute(this.#attribute) === value) return;
145
- if (value === null) element.removeAttribute(this.#attribute);
146
- else element.setAttribute(this.#attribute, value);
152
+ setAttributeIfChanged(element, this.#attribute, value);
147
153
  }
148
154
  };
149
155
 
@@ -278,9 +284,8 @@ var StateRegions = class _StateRegions {
278
284
  const below = hold.linked ? stack[index - 1] : void 0;
279
285
  if (below) {
280
286
  if (region.hidden !== below.written) region.hidden = below.written;
281
- } else if (region.getAttribute("hidden") !== hold.authored) {
282
- if (hold.authored === null) region.removeAttribute("hidden");
283
- else region.setAttribute("hidden", hold.authored);
287
+ } else {
288
+ setAttributeIfChanged(region, "hidden", hold.authored);
284
289
  }
285
290
  }
286
291
  this.#mark(region, stack);
@@ -355,9 +360,7 @@ var StateRegions = class _StateRegions {
355
360
  );
356
361
  });
357
362
  for (const [name, recorded] of records) {
358
- if (region.getAttribute(name) === recorded) continue;
359
- if (recorded === null) region.removeAttribute(name);
360
- else region.setAttribute(name, recorded);
363
+ setAttributeIfChanged(region, name, recorded);
361
364
  }
362
365
  }
363
366
  #recorded(region, name) {
@@ -1,5 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ function fillTemplate(template, values) {
4
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
5
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
6
+ return replacement === void 0 ? match : String(replacement);
7
+ });
8
+ }
9
+
3
10
  function announce(message, options = {}) {
4
11
  const text = message.trim();
5
12
  if (text.length === 0) return;
@@ -9,12 +16,6 @@ function announce(message, options = {}) {
9
16
  })
10
17
  );
11
18
  }
12
- function fillTemplate(template, values) {
13
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
14
- const replacement = values[name];
15
- return replacement === void 0 ? match : String(replacement);
16
- });
17
- }
18
19
 
19
20
  var NUMBER_BOUNDS = {
20
21
  nonNegative: { finite: true, min: 0 }};
@@ -69,6 +70,9 @@ var MicrotaskCoalescer = class {
69
70
  this.#queued = false;
70
71
  this.#generation += 1;
71
72
  }
73
+ get pending() {
74
+ return this.#queued;
75
+ }
72
76
  schedule() {
73
77
  if (!this.#active || this.#queued) return;
74
78
  this.#queued = true;
@@ -99,6 +103,9 @@ var MorphRenderWatcher = class {
99
103
  schedule() {
100
104
  this.#pass.schedule();
101
105
  }
106
+ get pending() {
107
+ return this.#pass.pending;
108
+ }
102
109
  disconnect() {
103
110
  this.#listeners.dispose();
104
111
  this.#pass.cancel();