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
@@ -22,21 +22,42 @@ function actionSource(source) {
22
22
  };
23
23
  }
24
24
 
25
+ function isAriaDisabled(element) {
26
+ for (let current = element; current; current = current.parentElement) {
27
+ if (current.getAttribute("aria-disabled") === "true") return true;
28
+ }
29
+ return false;
30
+ }
31
+
25
32
  function isReservedArrowChord(event, allow = []) {
26
33
  if (!event.key.startsWith("Arrow")) return false;
27
34
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
28
35
  }
36
+ function hasModifierChord(event) {
37
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
38
+ }
39
+
40
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
41
+ if (element.getAttribute(name) === value) return false;
42
+ if (value === null) element.removeAttribute(name);
43
+ else element.setAttribute(name, value);
44
+ return true;
45
+ }
29
46
 
30
47
  function sharedRegistry(key, create) {
31
48
  const symbol = Symbol.for(key);
32
49
  const scope = globalThis;
33
50
  const existing = scope[symbol];
34
51
  if (existing !== void 0) return existing;
35
- const registry = create();
36
- Object.defineProperty(globalThis, symbol, { value: registry });
37
- return registry;
52
+ const registry2 = create();
53
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
54
+ return registry2;
38
55
  }
39
56
 
57
+ var registry = sharedRegistry(
58
+ "stimeo-ui.attribute-lease.registry.v1",
59
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
60
+ );
40
61
  function isAttributeValue(value) {
41
62
  return value === null || typeof value === "string";
42
63
  }
@@ -53,24 +74,46 @@ function readLeaseRecord(element, record) {
53
74
  return void 0;
54
75
  }
55
76
  }
56
- var AttributeLease = class _AttributeLease {
57
- static #registry = sharedRegistry(
58
- "stimeo-ui.attribute-lease.registry.v1",
59
- () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
60
- );
77
+ function attributeStack(element, attribute) {
78
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
79
+ registry.stacks.set(element, byAttribute);
80
+ const known = byAttribute.get(attribute);
81
+ if (known) return known;
82
+ const found = [];
83
+ for (const record of registry.names.get(attribute) ?? []) {
84
+ const read = readLeaseRecord(element, record);
85
+ if (read) found.push({ ...read, record });
86
+ }
87
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
88
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
89
+ lease: null,
90
+ record,
91
+ written,
92
+ original,
93
+ linked: linked && index > 0
94
+ }));
95
+ byAttribute.set(attribute, stack);
96
+ return stack;
97
+ }
98
+ function findHold(stack, lease, record) {
99
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
100
+ }
101
+ var AttributeLease = class {
61
102
  #attribute;
62
103
  #record;
63
104
  #base;
105
+ #preserveAuthor;
64
106
  #held = /* @__PURE__ */ new Set();
65
107
  constructor(attribute, owner, options = {}) {
66
108
  this.#attribute = attribute;
67
109
  this.#record = `data-${owner}-${attribute}-lease`;
68
110
  this.#base = options.base === true;
69
- const names = _AttributeLease.#registry.names;
111
+ this.#preserveAuthor = options.preserveAuthor === true;
112
+ const names = registry.names;
70
113
  names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
71
114
  }
72
115
  write(element, value) {
73
- const stack = this.#stack(element);
116
+ const stack = attributeStack(element, this.#attribute);
74
117
  const hold = this.#take(stack) ?? this.#acquire(element, stack);
75
118
  this.#held.add(element);
76
119
  const shows = !this.#covered(element, stack, hold);
@@ -80,8 +123,8 @@ var AttributeLease = class _AttributeLease {
80
123
  }
81
124
  return(element) {
82
125
  const held = this.#held.delete(element);
83
- const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
84
- const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
126
+ const known = registry.stacks.get(element)?.has(this.#attribute);
127
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
85
128
  const hold = this.#take(stack);
86
129
  if (!hold) return false;
87
130
  const carries = element.getAttribute(this.#attribute) === hold.written;
@@ -99,31 +142,8 @@ var AttributeLease = class _AttributeLease {
99
142
  returnAll() {
100
143
  for (const element of Array.from(this.#held)) this.return(element);
101
144
  }
102
- #stack(element) {
103
- const stacks = _AttributeLease.#registry.stacks;
104
- const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
105
- stacks.set(element, byAttribute);
106
- const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
107
- byAttribute.set(this.#attribute, stack);
108
- return stack;
109
- }
110
- #seed(element) {
111
- const found = [];
112
- for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
113
- const read = readLeaseRecord(element, record);
114
- if (read) found.push({ ...read, record });
115
- }
116
- const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
117
- return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
118
- lease: null,
119
- record,
120
- written,
121
- original,
122
- linked: linked && index > 0
123
- }));
124
- }
125
145
  #take(stack) {
126
- const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
146
+ const hold = findHold(stack, this, this.#record);
127
147
  if (hold) hold.lease = this;
128
148
  return hold;
129
149
  }
@@ -132,7 +152,7 @@ var AttributeLease = class _AttributeLease {
132
152
  const top = stack.at(-1);
133
153
  const bottom = stack[0];
134
154
  const over = top?.written === current ? top : void 0;
135
- const from = this.#base ? bottom : over;
155
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
136
156
  const hold = {
137
157
  lease: this,
138
158
  record: this.#record,
@@ -162,15 +182,23 @@ var AttributeLease = class _AttributeLease {
162
182
  );
163
183
  });
164
184
  for (const [name, recorded] of records) {
165
- if (element.getAttribute(name) === recorded) continue;
166
- if (recorded === null) element.removeAttribute(name);
167
- else element.setAttribute(name, recorded);
185
+ setAttributeIfChanged(element, name, recorded);
168
186
  }
169
187
  }
170
188
  #reflect(element, value) {
171
- if (element.getAttribute(this.#attribute) === value) return;
172
- if (value === null) element.removeAttribute(this.#attribute);
173
- else element.setAttribute(this.#attribute, value);
189
+ setAttributeIfChanged(element, this.#attribute, value);
190
+ }
191
+ };
192
+
193
+ var ClickVerdicts = class {
194
+ #verdicts = /* @__PURE__ */ new WeakMap();
195
+ record(event, entries, refused) {
196
+ this.#verdicts.set(event, { entries, refused });
197
+ }
198
+ for(event, entry) {
199
+ const verdict = event && this.#verdicts.get(event);
200
+ if (!verdict?.entries.includes(entry)) return void 0;
201
+ return verdict.refused;
174
202
  }
175
203
  };
176
204
 
@@ -188,25 +216,25 @@ var EscapeLayer = class _EscapeLayer {
188
216
  #active = null;
189
217
  activate(ownerDocument = document, options) {
190
218
  this.deactivate();
191
- let registry = _EscapeLayer.#registries.get(ownerDocument);
192
- if (!registry) {
193
- registry = _EscapeLayer.#createRegistry();
194
- _EscapeLayer.#registries.set(ownerDocument, registry);
195
- ownerDocument.addEventListener("keydown", registry.onKeydown);
219
+ let registry2 = _EscapeLayer.#registries.get(ownerDocument);
220
+ if (!registry2) {
221
+ registry2 = _EscapeLayer.#createRegistry();
222
+ _EscapeLayer.#registries.set(ownerDocument, registry2);
223
+ ownerDocument.addEventListener("keydown", registry2.onKeydown);
196
224
  }
197
225
  const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
198
- registry.stack.push(entry);
226
+ registry2.stack.push(entry);
199
227
  this.#active = { document: ownerDocument, entry };
200
228
  }
201
229
  deactivate() {
202
230
  const active = this.#active;
203
231
  if (!active) return;
204
- const registry = _EscapeLayer.#registries.get(active.document);
205
- if (registry) {
206
- const index = registry.stack.lastIndexOf(active.entry);
207
- if (index >= 0) registry.stack.splice(index, 1);
208
- if (registry.stack.length === 0) {
209
- active.document.removeEventListener("keydown", registry.onKeydown);
232
+ const registry2 = _EscapeLayer.#registries.get(active.document);
233
+ if (registry2) {
234
+ const index = registry2.stack.lastIndexOf(active.entry);
235
+ if (index >= 0) registry2.stack.splice(index, 1);
236
+ if (registry2.stack.length === 0) {
237
+ active.document.removeEventListener("keydown", registry2.onKeydown);
210
238
  _EscapeLayer.#registries.delete(active.document);
211
239
  }
212
240
  }
@@ -215,22 +243,22 @@ var EscapeLayer = class _EscapeLayer {
215
243
  get ownsEscape() {
216
244
  const active = this.#active;
217
245
  if (!active) return false;
218
- const registry = _EscapeLayer.#registries.get(active.document);
219
- if (!registry) return false;
220
- return _EscapeLayer.#resolveOwner(registry.stack) === active.entry;
246
+ const registry2 = _EscapeLayer.#registries.get(active.document);
247
+ if (!registry2) return false;
248
+ return _EscapeLayer.#resolveOwner(registry2.stack) === active.entry;
221
249
  }
222
250
  static #createRegistry() {
223
- const registry = {
251
+ const registry2 = {
224
252
  stack: [],
225
253
  onKeydown: (event) => {
226
254
  if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
227
- const owner = _EscapeLayer.#resolveOwner(registry.stack);
255
+ const owner = _EscapeLayer.#resolveOwner(registry2.stack);
228
256
  if (!owner) return;
229
257
  event.preventDefault();
230
258
  owner.onDismiss();
231
259
  }
232
260
  };
233
- return registry;
261
+ return registry2;
234
262
  }
235
263
  static #resolveOwner(stack) {
236
264
  for (let index = stack.length - 1; index >= 0; index--) {
@@ -243,6 +271,29 @@ var EscapeLayer = class _EscapeLayer {
243
271
  }
244
272
  };
245
273
 
274
+ function ownerIndex(candidates, node) {
275
+ if (!(node instanceof Node)) return -1;
276
+ return candidates.findIndex((candidate) => candidate.contains(node));
277
+ }
278
+ function ownerOf(candidates, node) {
279
+ return candidates[ownerIndex(candidates, node)] ?? null;
280
+ }
281
+ function ownersOf(candidates, node) {
282
+ if (!(node instanceof Node)) return [];
283
+ return candidates.filter((candidate) => candidate.contains(node));
284
+ }
285
+
286
+ var ListenerSet = class {
287
+ #abort = new AbortController();
288
+ add(target, type, handler, options) {
289
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
290
+ }
291
+ dispose() {
292
+ this.#abort.abort();
293
+ this.#abort = new AbortController();
294
+ }
295
+ };
296
+
246
297
  var TimerRegistry = class {
247
298
  ids = /* @__PURE__ */ new Set();
248
299
  clear(id) {
@@ -289,11 +340,13 @@ var ContextMenuController = class _ContextMenuController extends Controller {
289
340
  #menu = null;
290
341
  #connected = false;
291
342
  #reporting = false;
343
+ #clickVerdicts = new ClickVerdicts();
344
+ #listeners = new ListenerSet();
292
345
  connect() {
293
346
  this.#closeMenu("api");
294
- this.element.addEventListener("click", this.#onItemClickCapture, true);
295
- document.addEventListener("click", this.#onOutsidePointer, true);
296
- document.addEventListener("contextmenu", this.#onOutsidePointer, true);
347
+ this.#listeners.add(this.element, "click", this.#onItemClickCapture, { capture: true });
348
+ this.#listeners.add(document, "click", this.#onOutsidePointer, { capture: true });
349
+ this.#listeners.add(document, "contextmenu", this.#onOutsidePointer, { capture: true });
297
350
  this.#connected = true;
298
351
  this.#reporting = true;
299
352
  }
@@ -302,9 +355,7 @@ var ContextMenuController = class _ContextMenuController extends Controller {
302
355
  this.#reporting = false;
303
356
  this.#timers.clearAll();
304
357
  this.#escapeLayer.deactivate();
305
- this.element.removeEventListener("click", this.#onItemClickCapture, true);
306
- document.removeEventListener("click", this.#onOutsidePointer, true);
307
- document.removeEventListener("contextmenu", this.#onOutsidePointer, true);
358
+ this.#listeners.dispose();
308
359
  }
309
360
  regionTargetConnected() {
310
361
  if (this.#connected) this.#reflectRegion();
@@ -342,6 +393,7 @@ var ContextMenuController = class _ContextMenuController extends Controller {
342
393
  onItemKeydown(event) {
343
394
  if (event.defaultPrevented) return;
344
395
  if (isReservedArrowChord(event)) return;
396
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
345
397
  const items = this.#navigableItems;
346
398
  const currentIndex = items.indexOf(event.currentTarget);
347
399
  switch (event.key) {
@@ -373,9 +425,18 @@ var ContextMenuController = class _ContextMenuController extends Controller {
373
425
  break;
374
426
  }
375
427
  }
376
- activate() {
428
+ activate(event) {
429
+ const item = ownerOf(this.itemTargets, event?.target);
430
+ if (item !== null && this.#refuses(item, event)) return;
377
431
  this.#closeAndRestore("select");
378
432
  }
433
+ #refuses(item, event) {
434
+ const recorded = this.#clickVerdicts.for(event, item);
435
+ if (recorded !== void 0) return recorded;
436
+ return ownersOf(this.itemTargets, event?.target).some(
437
+ (owner) => this.#isActivationDisabled(owner)
438
+ );
439
+ }
379
440
  #openAt(x, y, reason) {
380
441
  if (!this.hasMenuTarget) return;
381
442
  this.#timers.clearAll();
@@ -435,12 +496,13 @@ var ContextMenuController = class _ContextMenuController extends Controller {
435
496
  if (this.#isOpen && !this.element.contains(event.target)) this.#closeMenu("outside");
436
497
  };
437
498
  #onItemClickCapture = (event) => {
438
- const target = event.target;
439
- if (!(target instanceof Node)) return;
440
- const disabled = this.itemTargets.some(
441
- (item) => item.getAttribute("aria-disabled") === "true" && item.contains(target)
499
+ const items = ownersOf(this.itemTargets, event.target);
500
+ this.#clickVerdicts.record(
501
+ event,
502
+ items,
503
+ items.some((item) => this.#isActivationDisabled(item))
442
504
  );
443
- if (!disabled) return;
505
+ if (!items.some((item) => isAriaDisabled(item))) return;
444
506
  event.preventDefault();
445
507
  event.stopImmediatePropagation();
446
508
  };
@@ -451,6 +513,9 @@ var ContextMenuController = class _ContextMenuController extends Controller {
451
513
  if (item.hasAttribute("hidden")) return false;
452
514
  return !item.disabled;
453
515
  }
516
+ #isActivationDisabled(item) {
517
+ return !this.#isNavigable(item) || isAriaDisabled(item);
518
+ }
454
519
  get #isOpen() {
455
520
  return this.hasMenuTarget && !this.menuTarget.hidden;
456
521
  }
@@ -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 ListenerSet = class {
20
21
  #abort = new AbortController();
@@ -43,6 +44,9 @@ var MicrotaskCoalescer = class {
43
44
  this.#queued = false;
44
45
  this.#generation += 1;
45
46
  }
47
+ get pending() {
48
+ return this.#queued;
49
+ }
46
50
  schedule() {
47
51
  if (!this.#active || this.#queued) return;
48
52
  this.#queued = true;
@@ -73,6 +77,9 @@ var MorphRenderWatcher = class {
73
77
  schedule() {
74
78
  this.#pass.schedule();
75
79
  }
80
+ get pending() {
81
+ return this.#pass.pending;
82
+ }
76
83
  disconnect() {
77
84
  this.#listeners.dispose();
78
85
  this.#pass.cancel();
@@ -127,6 +134,94 @@ var NumberValueReader = class {
127
134
  }
128
135
  };
129
136
 
137
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
138
+ if (element.getAttribute(name) === value) return false;
139
+ if (value === null) element.removeAttribute(name);
140
+ else element.setAttribute(name, value);
141
+ return true;
142
+ }
143
+
144
+ var OwnedText = class {
145
+ #attribute;
146
+ #channel;
147
+ #guard;
148
+ #writes = /* @__PURE__ */ new WeakMap();
149
+ constructor(attribute, channel = "text", guard = () => true) {
150
+ this.#attribute = attribute;
151
+ this.#channel = channel;
152
+ this.#guard = guard;
153
+ }
154
+ hasRecord(element) {
155
+ return element.hasAttribute(this.#attribute);
156
+ }
157
+ read(element, evidence = {}) {
158
+ if (evidence.source === "live") return this.#writes.get(element) ?? null;
159
+ const raw = element.getAttribute(this.#attribute);
160
+ if (raw === null) return null;
161
+ try {
162
+ const record = JSON.parse(raw);
163
+ if (!Array.isArray(record) || record.length !== 3 || record[0] !== this.#channel || typeof record[1] !== "string" || typeof record[2] !== "string")
164
+ return null;
165
+ return { original: record[1], written: record[2] };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ owns(element, evidence = {}) {
171
+ return this.#ownedRecord(element, evidence) !== null;
172
+ }
173
+ #ownedRecord(element, evidence) {
174
+ const record = this.read(element, evidence);
175
+ if (record === null || !this.#guard(element)) return null;
176
+ if (this.#channel === "text" && element.firstElementChild !== null) return null;
177
+ const current = this.#current(element);
178
+ return current === record.written || evidence.source === "live" && evidence.allowOriginal === true && current === record.original ? record : null;
179
+ }
180
+ write(element, written, original, force = false) {
181
+ const current = this.#current(element);
182
+ if (current === null) return;
183
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
184
+ if (force || current !== written) this.#put(element, written);
185
+ this.remember(element, written, before);
186
+ }
187
+ remember(element, written, original) {
188
+ const current = this.#current(element);
189
+ if (current === null) return;
190
+ const before = original ?? this.#writes.get(element)?.original ?? this.read(element)?.original ?? current;
191
+ this.#writes.set(element, { original: before, written });
192
+ setAttributeIfChanged(
193
+ element,
194
+ this.#attribute,
195
+ JSON.stringify([this.#channel, before, written])
196
+ );
197
+ }
198
+ clear(element, evidence = {}) {
199
+ if (!this.owns(element, evidence)) return false;
200
+ if (this.#current(element) !== "") this.#put(element, "");
201
+ this.forget(element);
202
+ return true;
203
+ }
204
+ restore(element, evidence = {}) {
205
+ const record = this.#ownedRecord(element, evidence);
206
+ if (record === null) return false;
207
+ if (this.#current(element) !== record.original) this.#put(element, record.original);
208
+ this.forget(element);
209
+ return true;
210
+ }
211
+ forget(element) {
212
+ setAttributeIfChanged(element, this.#attribute, null);
213
+ this.#writes.delete(element);
214
+ }
215
+ #current(element) {
216
+ if (this.#channel === "text") return element.textContent ?? "";
217
+ return element instanceof HTMLInputElement ? element.value : null;
218
+ }
219
+ #put(element, text) {
220
+ if (this.#channel === "text") element.textContent = text;
221
+ else if (element instanceof HTMLInputElement) element.value = text;
222
+ }
223
+ };
224
+
130
225
  var TimerRegistry = class {
131
226
  ids = /* @__PURE__ */ new Set();
132
227
  clear(id) {
@@ -189,7 +284,7 @@ var CountdownController = class _CountdownController extends Controller {
189
284
  #renderedAmount = 0;
190
285
  #phase = "paused";
191
286
  #valuesDirty = false;
192
- #statusWrites = /* @__PURE__ */ new WeakMap();
287
+ #statusText = new OwnedText(this.#ownedStatus);
193
288
  connect() {
194
289
  this.#connected = true;
195
290
  this.#resync.observe(this.element);
@@ -245,8 +340,7 @@ var CountdownController = class _CountdownController extends Controller {
245
340
  }
246
341
  statusTargetDisconnected(status) {
247
342
  if (!this.statusTargets.includes(status) && this.#ownsStatus(status)) {
248
- status.removeAttribute(this.#ownedStatus);
249
- this.#statusWrites.delete(status);
343
+ this.#statusText.forget(status);
250
344
  }
251
345
  this.#resync.schedule();
252
346
  }
@@ -366,25 +460,19 @@ var CountdownController = class _CountdownController extends Controller {
366
460
  this.#writeStatus(status, label);
367
461
  }
368
462
  #ownsStatus(status) {
369
- const held = this.#statusWrites.get(status);
370
- if (!status.hasAttribute(this.#ownedStatus) && held && status.firstElementChild === null && (status.textContent === held.text || status.textContent === held.original))
371
- return true;
372
- return status.getAttribute(this.#ownedStatus) === status.textContent && status.firstElementChild === null;
463
+ return this.#statusText.owns(status) || !this.#statusText.hasRecord(status) && this.#statusText.owns(status, { source: "live", allowOriginal: true });
373
464
  }
374
465
  #isEmpty(status) {
375
466
  return status.textContent === "" && status.firstElementChild === null;
376
467
  }
377
468
  #writeStatus(status, text) {
378
- const original = this.#statusWrites.get(status)?.original ?? status.textContent ?? "";
379
- this.#statusWrites.set(status, { original, text });
380
- if (status.textContent !== text) status.textContent = text;
381
- if (status.getAttribute(this.#ownedStatus) !== text)
382
- status.setAttribute(this.#ownedStatus, text);
469
+ this.#statusText.write(status, text);
383
470
  }
384
471
  #releaseStatus(status) {
385
- status.textContent = "";
386
- status.removeAttribute(this.#ownedStatus);
387
- this.#statusWrites.delete(status);
472
+ this.#statusText.clear(
473
+ status,
474
+ this.#statusText.hasRecord(status) ? {} : { source: "live", allowOriginal: true }
475
+ );
388
476
  }
389
477
  #announceCompletion() {
390
478
  announce(fillTemplate(this.announceTextValue, {}));