stimeo-ui 0.16.0 → 0.17.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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +8 -1
  4. data/dist/cable/index.js +363 -436
  5. data/dist/controllers/accordion_controller.js +166 -55
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -198
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +271 -127
  10. data/dist/controllers/avatar_controller.js +171 -105
  11. data/dist/controllers/breadcrumb_controller.js +0 -130
  12. data/dist/controllers/bulk_select_controller.js +262 -52
  13. data/dist/controllers/calendar_controller.js +175 -209
  14. data/dist/controllers/carousel_controller.js +461 -399
  15. data/dist/controllers/character_counter_controller.js +246 -156
  16. data/dist/controllers/checkbox_controller.js +32 -74
  17. data/dist/controllers/clipboard_controller.js +168 -147
  18. data/dist/controllers/collapsible_controller.js +301 -120
  19. data/dist/controllers/color_picker_controller.js +149 -139
  20. data/dist/controllers/combobox_controller.js +360 -133
  21. data/dist/controllers/command_palette_controller.js +970 -358
  22. data/dist/controllers/conditional_fields_controller.js +166 -124
  23. data/dist/controllers/confirm_controller.js +939 -248
  24. data/dist/controllers/context_menu_controller.js +256 -128
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +183 -148
  27. data/dist/controllers/currency_input_controller.js +134 -158
  28. data/dist/controllers/data_grid_controller.js +114 -186
  29. data/dist/controllers/date_range_picker_controller.js +288 -206
  30. data/dist/controllers/dialog_controller.js +858 -223
  31. data/dist/controllers/direct_upload_controller.js +73 -153
  32. data/dist/controllers/dirty_form_controller.js +14 -48
  33. data/dist/controllers/dismissible_controller.js +175 -21
  34. data/dist/controllers/drawer_controller.js +933 -341
  35. data/dist/controllers/dropdown_controller.js +312 -82
  36. data/dist/controllers/editable_controller.js +158 -68
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +311 -207
  39. data/dist/controllers/filter_controller.js +67 -39
  40. data/dist/controllers/flash_controller.js +172 -314
  41. data/dist/controllers/focus_controller.js +654 -240
  42. data/dist/controllers/form_field_controller.js +147 -132
  43. data/dist/controllers/form_validation_controller.js +13 -86
  44. data/dist/controllers/frame_loading_controller.js +264 -213
  45. data/dist/controllers/highlight_controller.js +107 -75
  46. data/dist/controllers/hover_card_controller.js +309 -118
  47. data/dist/controllers/idle_controller.js +267 -101
  48. data/dist/controllers/input_mask_controller.js +71 -72
  49. data/dist/controllers/intersection_controller.js +147 -106
  50. data/dist/controllers/lazy_frame_controller.js +9 -65
  51. data/dist/controllers/listbox_controller.js +306 -204
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -93
  54. data/dist/controllers/menu_controller.js +214 -156
  55. data/dist/controllers/menubar_controller.js +170 -329
  56. data/dist/controllers/meter_controller.js +151 -67
  57. data/dist/controllers/multi_select_controller.js +593 -358
  58. data/dist/controllers/navigation_menu_controller.js +124 -232
  59. data/dist/controllers/nested_form_controller.js +352 -182
  60. data/dist/controllers/network_status_controller.js +242 -54
  61. data/dist/controllers/number_input_controller.js +320 -263
  62. data/dist/controllers/optimistic_controller.js +16 -76
  63. data/dist/controllers/otp_controller.js +198 -193
  64. data/dist/controllers/overflow_indicator_controller.js +116 -113
  65. data/dist/controllers/overflow_menu_controller.js +343 -283
  66. data/dist/controllers/pagination_controller.js +295 -119
  67. data/dist/controllers/password_reveal_controller.js +389 -121
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +237 -103
  70. data/dist/controllers/pointer_drag_controller.js +185 -165
  71. data/dist/controllers/popover_controller.js +236 -97
  72. data/dist/controllers/portal_controller.js +209 -76
  73. data/dist/controllers/preview_guard_controller.js +232 -116
  74. data/dist/controllers/progress_controller.js +125 -56
  75. data/dist/controllers/radio_group_controller.js +117 -137
  76. data/dist/controllers/range_slider_controller.js +192 -105
  77. data/dist/controllers/rating_controller.js +418 -207
  78. data/dist/controllers/read_more_controller.js +134 -63
  79. data/dist/controllers/reading_progress_controller.js +147 -135
  80. data/dist/controllers/relative_time_controller.js +145 -110
  81. data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  82. data/dist/controllers/resizable_controller.js +255 -139
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -225
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -145
  87. data/dist/controllers/scrollspy_controller.js +181 -242
  88. data/dist/controllers/separator_controller.js +267 -140
  89. data/dist/controllers/sidebar_controller.js +1023 -386
  90. data/dist/controllers/skeleton_controller.js +248 -97
  91. data/dist/controllers/slider_controller.js +159 -79
  92. data/dist/controllers/smart_sticky_header_controller.js +162 -63
  93. data/dist/controllers/sortable_controller.js +0 -112
  94. data/dist/controllers/spinner_controller.js +277 -169
  95. data/dist/controllers/step_indicator_controller.js +99 -46
  96. data/dist/controllers/stepper_controller.js +114 -73
  97. data/dist/controllers/stick_to_bottom_controller.js +262 -138
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -205
  100. data/dist/controllers/switch_controller.js +33 -60
  101. data/dist/controllers/tabs_controller.js +35 -29
  102. data/dist/controllers/tags_input_controller.js +136 -145
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +130 -112
  105. data/dist/controllers/time_picker_controller.js +146 -80
  106. data/dist/controllers/toast_controller.js +186 -297
  107. data/dist/controllers/toggle_group_controller.js +95 -136
  108. data/dist/controllers/toolbar_controller.js +68 -98
  109. data/dist/controllers/tooltip_controller.js +158 -124
  110. data/dist/controllers/transition_controller.js +123 -119
  111. data/dist/controllers/tree_view_controller.js +150 -345
  112. data/dist/index.js +6370 -9346
  113. data/dist/positioning/index.js +148 -47
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,8 +1,27 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/flash_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
4
24
 
5
- // src/utils/announce.ts
6
25
  function announce(message, options = {}) {
7
26
  const text = message.trim();
8
27
  if (text.length === 0) return;
@@ -13,36 +32,6 @@ function announce(message, options = {}) {
13
32
  );
14
33
  }
15
34
 
16
- // src/utils/before_cache_reset.ts
17
- var BeforeCacheReset = class _BeforeCacheReset {
18
- /** Every subscribed instance, iterated by the one shared document listener. */
19
- static #subscribers = /* @__PURE__ */ new Set();
20
- /** The shared listener; installed while at least one instance is subscribed. */
21
- static #onBeforeCache = () => {
22
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
23
- };
24
- #rewind;
25
- /** @param rewind - the pass that returns this controller's state to its initial form. */
26
- constructor(rewind) {
27
- this.#rewind = rewind;
28
- }
29
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
30
- activate() {
31
- const first = _BeforeCacheReset.#subscribers.size === 0;
32
- _BeforeCacheReset.#subscribers.add(this);
33
- if (first) {
34
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
35
- }
36
- }
37
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
38
- deactivate() {
39
- _BeforeCacheReset.#subscribers.delete(this);
40
- if (_BeforeCacheReset.#subscribers.size > 0) return;
41
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
42
- }
43
- };
44
-
45
- // src/utils/event_owner.ts
46
35
  function ownerIndex(candidates, node) {
47
36
  if (!(node instanceof Node)) return -1;
48
37
  return candidates.findIndex((candidate) => candidate.contains(node));
@@ -51,43 +40,26 @@ function ownerOf(candidates, node) {
51
40
  return candidates[ownerIndex(candidates, node)] ?? null;
52
41
  }
53
42
 
54
- // src/utils/safe_timeout.ts
43
+ var MAX_TIMER_DELAY_MS = 2147483647;
44
+
55
45
  var TimerRegistry = class {
56
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
57
46
  ids = /* @__PURE__ */ new Set();
58
- /**
59
- * Cancels a single tracked timer.
60
- *
61
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
62
- * registry), so callers can clear defensively without guarding.
63
- */
64
47
  clear(id) {
65
48
  if (this.ids.delete(id)) {
66
49
  this.cancel(id);
67
50
  }
68
51
  }
69
- /**
70
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
71
- * to guarantee no timer outlives the element.
72
- */
73
52
  clearAll() {
74
53
  for (const id of this.ids) {
75
54
  this.cancel(id);
76
55
  }
77
56
  this.ids.clear();
78
57
  }
79
- /** Number of timers currently tracked (pending). */
80
58
  get size() {
81
59
  return this.ids.size;
82
60
  }
83
61
  };
84
62
  var SafeTimeout = class extends TimerRegistry {
85
- /**
86
- * Schedules `callback` after `delay` ms and returns the timer id.
87
- *
88
- * The id is removed from the registry automatically when the timeout fires,
89
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
90
- */
91
63
  set(callback, delay) {
92
64
  const id = this.schedule(() => {
93
65
  this.ids.delete(id);
@@ -104,16 +76,9 @@ var SafeTimeout = class extends TimerRegistry {
104
76
  }
105
77
  };
106
78
 
107
- // src/utils/keyed_timers.ts
108
79
  var KeyedTimers = class {
109
80
  #timers = new SafeTimeout();
110
- /** The pending timer of each key; an entry lives exactly as long as its timer. */
111
81
  #pending = /* @__PURE__ */ new Map();
112
- /**
113
- * Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
114
- * pending. The entry is dropped before the callback runs, so the callback sees
115
- * the key unarmed and may arm it again for the next round.
116
- */
117
82
  set(key, callback, delay) {
118
83
  this.clear(key);
119
84
  const id = this.#timers.set(() => {
@@ -122,76 +87,46 @@ var KeyedTimers = class {
122
87
  }, delay);
123
88
  this.#pending.set(key, id);
124
89
  }
125
- /**
126
- * Cancels `key`'s pending timer, if it has one.
127
- *
128
- * A timer id is a positive integer, so `-1` stands for "nothing pending" and
129
- * the registry ignores an id it does not own — the unarmed case needs no
130
- * branch of its own, and no other key's timer can be reached from here.
131
- */
132
90
  clear(key) {
133
91
  this.#timers.clear(this.#pending.get(key) ?? -1);
134
92
  this.#pending.delete(key);
135
93
  }
136
- /**
137
- * Cancels every pending timer and forgets every key. Call this from a
138
- * controller's `disconnect()` so no timer, and no entry, outlives the element.
139
- */
140
94
  clearAll() {
141
95
  this.#timers.clearAll();
142
96
  this.#pending.clear();
143
97
  }
144
- /** Whether `key` has a timer pending. */
145
98
  has(key) {
146
99
  return this.#pending.has(key);
147
100
  }
148
101
  };
149
102
 
150
- // src/utils/listener_set.ts
151
103
  var ListenerSet = class {
152
- /** The generation every `add` joins until the next `dispose()`. */
153
104
  #abort = new AbortController();
154
- /**
155
- * Attaches `handler` to the open generation, exactly as the caller spelled it.
156
- *
157
- * The set supplies the signal, so `options` carries everything else the DOM
158
- * accepts — `capture` included, which has to match at release time and no
159
- * longer has a second place to drift from.
160
- */
161
105
  add(target, type, handler, options) {
162
106
  target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
163
107
  }
164
- /**
165
- * Releases every listener of the open generation, synchronously, and opens the
166
- * next one. Idempotent, and safe before anything has been added.
167
- */
168
108
  dispose() {
169
109
  this.#abort.abort();
170
110
  this.#abort = new AbortController();
171
111
  }
172
112
  };
173
113
 
174
- // src/utils/microtask_coalescer.ts
175
114
  var MicrotaskCoalescer = class {
176
115
  #run;
177
116
  #queued = false;
178
117
  #active = false;
179
118
  #generation = 0;
180
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
181
119
  constructor(run) {
182
120
  this.#run = run;
183
121
  }
184
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
185
122
  activate() {
186
123
  this.#active = true;
187
124
  }
188
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
189
125
  cancel() {
190
126
  this.#active = false;
191
127
  this.#queued = false;
192
128
  this.#generation += 1;
193
129
  }
194
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
195
130
  schedule() {
196
131
  if (!this.#active || this.#queued) return;
197
132
  this.#queued = true;
@@ -204,27 +139,85 @@ var MicrotaskCoalescer = class {
204
139
  }
205
140
  };
206
141
 
207
- // src/utils/pausable_timers.ts
142
+ var MorphRenderWatcher = class {
143
+ #listeners = new ListenerSet();
144
+ #pass;
145
+ #element = null;
146
+ #onMorph = () => this.#pass.schedule();
147
+ constructor(run) {
148
+ this.#pass = new MicrotaskCoalescer(run);
149
+ }
150
+ observe(element) {
151
+ if (this.#element === element) return;
152
+ this.disconnect();
153
+ this.#element = element;
154
+ this.#pass.activate();
155
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
156
+ }
157
+ schedule() {
158
+ this.#pass.schedule();
159
+ }
160
+ disconnect() {
161
+ this.#listeners.dispose();
162
+ this.#pass.cancel();
163
+ this.#element = null;
164
+ }
165
+ };
166
+
167
+ var NUMBER_BOUNDS = {
168
+ finite: { finite: true },
169
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS }};
170
+ function matchesNumberBounds(value, bounds) {
171
+ if (!Number.isFinite(value)) {
172
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
173
+ if (direction === null) return false;
174
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
175
+ }
176
+ if (bounds.min !== void 0 && value < bounds.min) return false;
177
+ if (bounds.max !== void 0 && value > bounds.max) return false;
178
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
179
+ if (bounds.integer && !Number.isInteger(value)) return false;
180
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
181
+ return true;
182
+ }
183
+
184
+ function readNumber(raw, fallback, bounds) {
185
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
186
+ }
187
+
188
+ var NumberValueReader = class {
189
+ #lastRejected = /* @__PURE__ */ new Map();
190
+ read(owner, name, raw, fallback, bounds) {
191
+ const resolved = readNumber(raw, fallback, bounds);
192
+ if (matchesNumberBounds(raw, bounds)) {
193
+ this.#lastRejected.delete(name);
194
+ return resolved;
195
+ }
196
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
197
+ const literal = owner.element.getAttribute(attribute);
198
+ if (literal === null) {
199
+ this.#lastRejected.delete(name);
200
+ return resolved;
201
+ }
202
+ if (this.#lastRejected.get(name) !== literal) {
203
+ this.#lastRejected.set(name, literal);
204
+ console.warn(
205
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
206
+ );
207
+ }
208
+ return resolved;
209
+ }
210
+ };
211
+
208
212
  var PausableTimers = class {
209
213
  #timers = new KeyedTimers();
210
- /** Every key armed or held; an entry goes as its timer fires, or with the key. */
211
214
  #entries = /* @__PURE__ */ new Map();
212
- /**
213
- * Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.
214
- * A held key stays held and banks `delay` for its resume.
215
- */
216
215
  set(key, callback, delay) {
217
216
  const reasons = this.#entries.get(key)?.reasons ?? /* @__PURE__ */ new Set();
218
217
  const entry = { callback, startedAt: 0, remaining: delay, reasons };
219
218
  this.#entries.set(key, entry);
220
219
  if (reasons.size === 0) this.#arm(key, entry, callback);
221
220
  }
222
- /**
223
- * Holds `key` for `reason` and banks the time left, with a floor of one
224
- * millisecond. A key with no timer is held all the same. Reports whether this
225
- * call is the one that stopped a running timer, so the caller can write its
226
- * held-state hook exactly once.
227
- */
228
221
  pause(key, reason) {
229
222
  let entry = this.#entries.get(key);
230
223
  if (entry === void 0) {
@@ -237,11 +230,6 @@ var PausableTimers = class {
237
230
  entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));
238
231
  return true;
239
232
  }
240
- /**
241
- * Releases `reason` on `key`, arming the banked time again once no reason is
242
- * left; a key released with no timer is forgotten. Reports whether this call
243
- * is the one that started the timer again.
244
- */
245
233
  resume(key, reason) {
246
234
  const entry = this.#entries.get(key);
247
235
  if (entry === void 0) return false;
@@ -255,38 +243,20 @@ var PausableTimers = class {
255
243
  this.#arm(key, entry, callback);
256
244
  return true;
257
245
  }
258
- /** Whether any reason holds `key`, with or without a timer. */
259
246
  isHeld(key) {
260
247
  return (this.#entries.get(key)?.reasons.size ?? 0) > 0;
261
248
  }
262
- /** Whether `key` is armed or held — that is, whether this registry drives it at all. */
263
249
  tracks(key) {
264
250
  return this.#entries.has(key);
265
251
  }
266
- /**
267
- * Cancels `key`'s timer and keeps its hold, so a release arms nothing; a key
268
- * nothing holds is forgotten.
269
- */
270
- disarm(key) {
271
- this.#timers.clear(key);
272
- const entry = this.#entries.get(key);
273
- if (entry?.reasons.size) entry.callback = null;
274
- else this.#entries.delete(key);
275
- }
276
- /** Cancels `key`'s timer and drops its hold. */
277
252
  clear(key) {
278
253
  this.#timers.clear(key);
279
254
  this.#entries.delete(key);
280
255
  }
281
- /**
282
- * Cancels every timer and forgets every key. Call this from a controller's
283
- * `disconnect()` so neither a timer nor a hold outlives the element.
284
- */
285
256
  clearAll() {
286
257
  this.#timers.clearAll();
287
258
  this.#entries.clear();
288
259
  }
289
- /** Starts `entry`'s banked time running for `key`, and drops it as `callback` fires. */
290
260
  #arm(key, entry, callback) {
291
261
  entry.startedAt = Date.now();
292
262
  this.#timers.set(
@@ -300,12 +270,10 @@ var PausableTimers = class {
300
270
  }
301
271
  };
302
272
 
303
- // src/utils/target_selector.ts
304
273
  function targetSelector(identifier, name) {
305
274
  return `[data-${identifier}-target~="${name}"]`;
306
275
  }
307
276
 
308
- // src/utils/transition_completion.ts
309
277
  function timeMs(value) {
310
278
  const trimmed = value.trim();
311
279
  const amount = Number.parseFloat(trimmed);
@@ -339,11 +307,11 @@ function maxTransitionTotalMs(style) {
339
307
  return maxTotalMs(transitionTimings(style));
340
308
  }
341
309
 
342
- // src/controllers/flash_controller.ts
343
310
  var ASSERTIVE_TYPES = /* @__PURE__ */ new Set(["alert", "error"]);
344
311
  var MESSAGE_PART = "message";
345
- var FlashController = class extends Controller {
346
- /** Selects the message parts, in the namespace this controller is registered under. */
312
+ var SHOWN_ATTRIBUTE = "shown";
313
+ var FlashController = class _FlashController extends Controller {
314
+ #numbers = new NumberValueReader();
347
315
  get #messageSelector() {
348
316
  return targetSelector(this.identifier, MESSAGE_PART);
349
317
  }
@@ -353,54 +321,29 @@ var FlashController = class extends Controller {
353
321
  pauseOnHover: { type: Boolean, default: true },
354
322
  max: { type: Number, default: 0 }
355
323
  };
324
+ static valueConstraints = {
325
+ duration: NUMBER_BOUNDS.timer,
326
+ max: NUMBER_BOUNDS.finite
327
+ };
356
328
  static actions = ["dismiss"];
357
329
  static events = ["show", "dismiss", "reconcile"];
358
- /** Removal timers for the leaving transition; the auto-dismiss ones live below. */
359
330
  #timers = new SafeTimeout();
360
- /**
361
- * Per-message auto-dismiss held open while the message is hovered or focused, and
362
- * the one record of which messages hover or focus holds, which the cap reads.
363
- */
364
331
  #dismiss = new PausableTimers();
365
- /**
366
- * Auto-dismiss of the messages taken on while `pauseOnHover` is off. It runs on
367
- * whatever holds the message; the hold itself still keeps the cap away.
368
- */
369
332
  #fixedDismiss = new KeyedTimers();
370
- /** Applies the cap again once the event that released a hold has run its course. */
371
- #reapply = new MicrotaskCoalescer(() => this.#reapplyCap());
372
- /** Messages the pointer or focus moved into since the cap was last applied again. */
333
+ #reapply = new MorphRenderWatcher(() => this.#reapplyCap());
373
334
  #spared = [];
374
- /** The next pointer movement, listened for while a hover hold waits on it. */
375
335
  #pointer = new ListenerSet();
376
- /** Messages whose hover hold waits for the next pointer movement to be confirmed. */
377
336
  #awaitingPointer = /* @__PURE__ */ new Set();
378
337
  #observer = null;
379
- /** Whether the controller is between `connect()` and `disconnect()`. */
380
338
  #connected = false;
381
- /**
382
- * The `max` the stack was last held to, or `null` before the first time. `connect()`
383
- * applies the cap only when `max` is not that one, so a reconnect of the same instance
384
- * leaves the stack as it was unless `max` changed while it was away.
385
- */
339
+ #arrivals = /* @__PURE__ */ new Set();
386
340
  #appliedMax = null;
387
- /** Messages already processed, in insertion order, to enforce `max` and avoid double work. */
388
341
  #order = [];
389
- /**
390
- * Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}
391
- * releases the bookkeeping above *before* the transition wait, so for that window the
392
- * element is in the DOM but in neither collection — without this set a re-scan would
393
- * read it as a brand-new flash and show it a second time.
394
- */
395
342
  #leaving = /* @__PURE__ */ new Set();
396
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
343
+ get #shownAttribute() {
344
+ return `data-${this.identifier}-${SHOWN_ATTRIBUTE}`;
345
+ }
397
346
  #onEnter = (event) => this.#pause(event.currentTarget, event.type === "focusin" ? "focus" : "hover");
398
- /**
399
- * Releases hover or focus on a message. Focus moving between two of the message's
400
- * own controls is no release, since `focusout` names a control still inside it; and
401
- * the message the pointer or focus is moving into is spared when the release
402
- * applies the cap again, because its own hold only arrives after this event.
403
- */
404
347
  #onLeave = (event) => {
405
348
  const message = event.currentTarget;
406
349
  const next = ownerOf(this.#order, event.relatedTarget);
@@ -409,25 +352,28 @@ var FlashController = class extends Controller {
409
352
  };
410
353
  connect() {
411
354
  this.#connected = true;
412
- this.#reapply.activate();
355
+ const current = this.messageTargets;
356
+ for (const item of this.#arrivals) {
357
+ if (!current.includes(item)) this.#arrivals.delete(item);
358
+ }
359
+ this.#discardInherited();
360
+ this.#reapply.observe(this.element);
413
361
  for (const message of this.messageTargets) {
414
362
  if (this.#owns(message)) this.#process(message, true);
415
363
  }
416
- if (!Object.is(this.#appliedMax, this.maxValue)) {
417
- this.#appliedMax = this.maxValue;
364
+ if (!Object.is(this.#appliedMax, this.#safeMax)) {
365
+ this.#appliedMax = this.#safeMax;
418
366
  this.#enforceMax();
419
367
  }
420
368
  this.#syncObservation();
421
- this.#beforeCache.activate();
422
369
  }
423
370
  disconnect() {
424
371
  this.#connected = false;
425
- this.#beforeCache.deactivate();
426
372
  this.#stopObserving();
427
373
  this.#timers.clearAll();
428
374
  this.#dismiss.clearAll();
429
375
  this.#fixedDismiss.clearAll();
430
- this.#reapply.cancel();
376
+ this.#reapply.disconnect();
431
377
  this.#spared = [];
432
378
  this.#pointer.dispose();
433
379
  this.#awaitingPointer.clear();
@@ -435,66 +381,30 @@ var FlashController = class extends Controller {
435
381
  this.#order.length = 0;
436
382
  this.#leaving.clear();
437
383
  }
438
- /**
439
- * Takes the managed flashes out of the page just before Turbo freezes it, so a
440
- * restored snapshot carries no notification the visitor has already received: the
441
- * fresh `connect()` there reads a leftover flash as a brand-new one and announces it
442
- * a second time. A message that never auto-dismisses (`duration: 0`) is one of these
443
- * too — that value governs the timer, not what belongs in a cached page. Removal
444
- * only: `dismiss` reports a dismissal, and freezing the page is not one.
445
- */
446
- #rewindForCache() {
447
- const removed = this.#order.length;
448
- for (const message of [...this.#order]) {
384
+ #discardInherited() {
385
+ let removed = 0;
386
+ for (const message of this.messageTargets) {
387
+ if (!message.hasAttribute(this.#shownAttribute) || this.#arrivals.has(message)) continue;
388
+ if (!this.#owns(message)) continue;
449
389
  message.remove();
450
- this.#forget(message);
390
+ removed += 1;
451
391
  }
452
- for (const message of this.#leaving) message.remove();
453
- this.#leaving.clear();
454
392
  if (removed > 0) this.dispatch("reconcile", { detail: { removed } });
455
393
  }
456
- /** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */
457
394
  regionTargetConnected() {
458
395
  this.#resync();
459
396
  }
460
- /** Releases the observation when the `region` element leaves the target set. */
461
397
  regionTargetDisconnected() {
462
398
  this.#resync();
463
399
  }
464
- /**
465
- * Holds the stack to a changed `max`: lowering it below the count dismisses the
466
- * oldest messages hover and focus do not hold, with reason `limit`, as an arrival
467
- * past the cap does, while raising it or setting `0` dismisses nothing. Stimulus can
468
- * also call this ahead of `connect()` — for an attribute that changed while the
469
- * controller was away, and on every connect for an undeclared `max`, with its default;
470
- * `connect()` decides then.
471
- */
472
400
  maxValueChanged() {
473
401
  if (!this.#connected) return;
474
- this.#appliedMax = this.maxValue;
402
+ this.#appliedMax = this.#safeMax;
475
403
  this.#enforceMax();
476
404
  }
477
- /**
478
- * Whether this controller owns `message`. Ownership is the current `region`'s
479
- * subtree: a message target anywhere else in the controller's scope is the
480
- * consumer's, and so is one in a region that has gone away. The initial scan, a
481
- * re-scan after a `region` swap, and a departure from the target set all resolve
482
- * ownership through this one test; the observation gets it structurally, by watching
483
- * that subtree and nothing else.
484
- */
485
405
  #owns(message) {
486
406
  return this.hasRegionTarget && this.regionTarget.contains(message);
487
407
  }
488
- /**
489
- * Releases a message that left the target set (a Turbo Stream `remove`, the consumer
490
- * detaching the node, or a morph that rewrote the target attribute in place): it
491
- * stops occupying a `max` slot, and both its pending auto-dismiss and an already
492
- * scheduled removal are cancelled. A move *within* the region keeps all of them —
493
- * which is why the element must still be a message to be treated as one: ownership
494
- * alone reads an in-place attribute rewrite as a move, and a node outside the target
495
- * set belongs to the consumer, so nothing here may dismiss it. A move may still have
496
- * ended a hold, so those are read again.
497
- */
498
408
  messageTargetDisconnected(message) {
499
409
  if (!this.#connected) return;
500
410
  const moved = this.#owns(message) && message.matches(this.#messageSelector);
@@ -503,15 +413,9 @@ var FlashController = class extends Controller {
503
413
  return;
504
414
  }
505
415
  this.#forget(message);
416
+ this.#arrivals.delete(message);
506
417
  this.#leaving.delete(message);
507
418
  }
508
- /**
509
- * Re-points the observation after a `region` swap and picks up the messages the
510
- * new element brought with it (dynamic inserts, so their own `role` announces
511
- * them). The `#connected` guard is load-bearing: Stimulus runs target callbacks
512
- * for the initial markup *before* `connect()` and again during teardown *after*
513
- * `disconnect()`, and re-observing there would outlive the controller.
514
- */
515
419
  #resync() {
516
420
  if (!this.#connected) return;
517
421
  this.#syncObservation();
@@ -519,11 +423,6 @@ var FlashController = class extends Controller {
519
423
  if (this.#owns(message)) this.#process(message, false);
520
424
  }
521
425
  }
522
- /**
523
- * Points the mutation observation at the current `region` target, re-resolved on
524
- * every sync rather than captured at connect, so an element swapped in at runtime
525
- * is observed instead of the detached original.
526
- */
527
426
  #syncObservation() {
528
427
  this.#stopObserving();
529
428
  if (!this.hasRegionTarget || typeof MutationObserver === "undefined") return;
@@ -534,11 +433,6 @@ var FlashController = class extends Controller {
534
433
  this.#observer?.disconnect();
535
434
  this.#observer = null;
536
435
  }
537
- /**
538
- * Hover and focus listeners, bound and unbound as a pair so the two sides stay in
539
- * sync. Every message gets them, whatever `pauseOnHover` says, because the cap reads
540
- * the holds they record; unbinding is idempotent (a no-op when nothing was bound).
541
- */
542
436
  #bindPause(message) {
543
437
  message.addEventListener("mouseenter", this.#onEnter);
544
438
  message.addEventListener("mouseleave", this.#onLeave);
@@ -551,13 +445,14 @@ var FlashController = class extends Controller {
551
445
  message.removeEventListener("focusin", this.#onEnter);
552
446
  message.removeEventListener("focusout", this.#onLeave);
553
447
  }
554
- /** Dismisses the flash whose close control fired the event. */
555
- dismiss(event) {
556
- const target = event.currentTarget || event.target;
557
- const message = target?.closest(this.#messageSelector);
558
- if (message) this.#beginDismiss(message, "user");
448
+ dismiss(input) {
449
+ const source = actionSource(input);
450
+ if (source.origin && source.origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
451
+ return;
452
+ const message = (source.origin ?? source.host)?.closest(this.#messageSelector);
453
+ if (message && this.messageTargets.includes(message))
454
+ this.#beginDismiss(message, source.reason);
559
455
  }
560
- /** Processes messages added after connect (Turbo Stream); their own role announces them. */
561
456
  #onMutations(mutations) {
562
457
  for (const mutation of mutations) {
563
458
  for (const node of mutation.addedNodes) {
@@ -569,14 +464,6 @@ var FlashController = class extends Controller {
569
464
  }
570
465
  }
571
466
  }
572
- /**
573
- * Applies role/state, wires pause listeners, takes the holds the message already has,
574
- * and schedules auto-dismiss. A message `connect()` takes on (`atConnect`) is bridged
575
- * to the Announcer and meets no cap here, `connect()` holding the stack to it; a later
576
- * insert announces through its own role and meets the cap as it arrives. Idempotent
577
- * per message, and a node that has left the region by the time its insertion is
578
- * reported is not taken on.
579
- */
580
467
  #process(message, atConnect) {
581
468
  if (this.#order.includes(message) || this.#leaving.has(message) || !this.#owns(message)) {
582
469
  return;
@@ -587,110 +474,74 @@ var FlashController = class extends Controller {
587
474
  message.setAttribute("role", assertive ? "alert" : "status");
588
475
  }
589
476
  message.setAttribute("data-flash-state", "visible");
477
+ message.setAttribute(this.#shownAttribute, "");
478
+ const known = this.#arrivals.has(message);
590
479
  this.#order.push(message);
480
+ this.#arrivals.add(message);
591
481
  this.#bindPause(message);
592
482
  this.#readHolds(message);
593
- const text = message.textContent?.trim() ?? "";
594
- this.dispatch("show", { target: message, detail: { type, message: text } });
595
- if (atConnect) announce(text, { assertive });
483
+ if (!known) {
484
+ const text = message.textContent?.trim() ?? "";
485
+ this.dispatch("show", { target: message, detail: { type, message: text } });
486
+ if (atConnect) announce(text, { assertive });
487
+ }
596
488
  this.#startTimer(message);
597
489
  if (!atConnect) this.#enforceMax([message]);
598
490
  }
599
- /**
600
- * Dismisses the oldest messages with reason `limit` while more than `max` are shown
601
- * (0 or less, or not a finite number, means no cap), passing over every message hover
602
- * or focus holds and those in `spared` — the message just taken on, or the ones the
603
- * pointer or focus moved into. Only messages still in the region count or go: one a
604
- * script has just taken out stays on the stack until its removal is reported. With
605
- * nothing else left to go the stack stays over the cap. The loop walks a copy and
606
- * re-reads the count on every step, because a `dismiss` listener may already have
607
- * changed both.
608
- *
609
- * @stimeoRenderRoot
610
- */
611
491
  #enforceMax(spared = []) {
612
- if (!Number.isFinite(this.maxValue) || this.maxValue <= 0) return;
613
- for (const message of [...this.#order]) {
614
- if (this.#shownCount() <= this.maxValue) return;
492
+ if (this.#safeMax <= 0) return;
493
+ for (const message of [...this.#arrivals]) {
494
+ if (this.#shownCount() <= this.#safeMax) return;
615
495
  if (this.#owns(message) && !spared.includes(message) && !this.#dismiss.isHeld(message)) {
616
496
  this.#beginDismiss(message, "limit");
617
497
  }
618
498
  }
619
499
  }
620
- /** How many messages the stack shows: the ones taken on that are still in the region. */
621
500
  #shownCount() {
622
501
  return this.#order.filter((message) => this.#owns(message)).length;
623
502
  }
624
- /**
625
- * Arms a message's auto-dismiss; a non-positive `duration` means it never expires.
626
- * The timer waits out hover and focus when `pauseOnHover` is on, and runs on
627
- * regardless when it is off.
628
- *
629
- * @stimeoRuntimeOnly `duration` is the delay of the one dismissal timer this call arms,
630
- * and `pauseOnHover` whether hover and focus hold that timer.
631
- */
632
503
  #startTimer(message) {
633
- if (this.durationValue <= 0) return;
504
+ if (this.#safeDuration <= 0) return;
634
505
  const dismiss = () => this.#beginDismiss(message, "timeout");
635
- if (this.pauseOnHoverValue) this.#dismiss.set(message, dismiss, this.durationValue);
636
- else this.#fixedDismiss.set(message, dismiss, this.durationValue);
637
- }
638
- /**
639
- * Records hover or focus on a message. Hover and focus are independent reasons:
640
- * the registry banks the time left on the first of them and waits for the last
641
- * (WCAG 2.2 2.2.1), and the cap passes over the message while either is held.
642
- */
506
+ if (this.pauseOnHoverValue) this.#dismiss.set(message, dismiss, this.#safeDuration);
507
+ else this.#fixedDismiss.set(message, dismiss, this.#safeDuration);
508
+ }
643
509
  #pause(message, reason) {
644
510
  this.#dismiss.pause(message, reason);
645
511
  }
646
- /**
647
- * Releases one reason, resuming the banked time once no reason is left. Releasing
648
- * the last one applies the cap again, sparing `spare`.
649
- */
650
512
  #resume(message, reason, spare) {
651
513
  const held = this.#dismiss.isHeld(message);
652
514
  this.#dismiss.resume(message, reason);
653
515
  if (held && !this.#dismiss.isHeld(message)) this.#capLater(spare);
654
516
  }
655
- /**
656
- * Applies the cap again once the current event is over, sparing `spare`. A hold can
657
- * end in a `focusout` the engine fires while it is itself taking the node out for a
658
- * script's move or removal, and dismissing the message inside that event would pull
659
- * the node from under the caller's own call.
660
- */
661
517
  #capLater(spare) {
662
518
  if (spare) this.#spared.push(spare);
663
519
  this.#reapply.schedule();
664
520
  }
665
- /** Applies the cap again, sparing every message gathered since the last pass. */
666
521
  #reapplyCap() {
522
+ for (const message of [...this.#order, ...this.#leaving]) {
523
+ if (!this.#owns(message)) continue;
524
+ message.setAttribute("data-flash-state", this.#leaving.has(message) ? "leaving" : "visible");
525
+ message.setAttribute(this.#shownAttribute, "");
526
+ if (!message.hasAttribute("role")) {
527
+ const assertive = ASSERTIVE_TYPES.has(message.getAttribute("data-flash-type") ?? "");
528
+ message.setAttribute("role", assertive ? "alert" : "status");
529
+ }
530
+ }
667
531
  const spared = this.#spared;
668
532
  this.#spared = [];
669
533
  this.#enforceMax(spared);
670
534
  }
671
- /**
672
- * Reads the holds of a message that moved within the region. Focus is read at once:
673
- * an ordinary move ends it with a `focusout` and `moveBefore` keeps it. Where the
674
- * pointer is, nothing says until it moves again: no `mouseleave` reaches a moved node,
675
- * and `:hover` can answer from before the move. A message still held waits for that
676
- * movement.
677
- */
678
535
  #afterMove(message) {
679
536
  if (!message.contains(document.activeElement)) this.#resume(message, "focus", null);
680
537
  if (this.#dismiss.isHeld(message)) this.#awaitPointer(message);
681
538
  }
682
- /**
683
- * Takes the holds a message already has as it is taken on: focus inside it, and the
684
- * pointer over it by `:hover`. That answer can date from before the message came here,
685
- * so a hover hold taken on it waits for the next pointer movement to be confirmed.
686
- */
687
539
  #readHolds(message) {
688
540
  if (message.contains(document.activeElement)) this.#pause(message, "focus");
689
541
  if (!message.matches(":hover")) return;
690
542
  this.#pause(message, "hover");
691
543
  this.#awaitPointer(message);
692
544
  }
693
- /** Leaves `message`'s hover hold to be confirmed or let go on the next pointer movement. */
694
545
  #awaitPointer(message) {
695
546
  this.#awaitingPointer.add(message);
696
547
  this.#pointer.add(document, "pointermove", this.#onPointerMove, {
@@ -698,10 +549,6 @@ var FlashController = class extends Controller {
698
549
  passive: true
699
550
  });
700
551
  }
701
- /**
702
- * Reads `:hover` once the pointer has moved, when the answer is current, and lets go of
703
- * the hover hold of every waiting message the pointer is not over. Listens once.
704
- */
705
552
  #onPointerMove = () => {
706
553
  this.#pointer.dispose();
707
554
  const waiting = [...this.#awaitingPointer];
@@ -710,11 +557,6 @@ var FlashController = class extends Controller {
710
557
  if (!message.matches(":hover")) this.#resume(message, "hover", null);
711
558
  }
712
559
  };
713
- /**
714
- * Releases every per-message resource: timers, hold, stacking slot, pause listeners,
715
- * and the wait on the pointer. A message something still held counts as released: the
716
- * cap is applied again.
717
- */
718
560
  #forget(message) {
719
561
  if (this.#dismiss.isHeld(message)) this.#capLater(null);
720
562
  this.#dismiss.clear(message);
@@ -725,7 +567,6 @@ var FlashController = class extends Controller {
725
567
  this.#awaitingPointer.delete(message);
726
568
  if (this.#awaitingPointer.size === 0) this.#pointer.dispose();
727
569
  }
728
- /** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */
729
570
  #beginDismiss(message, reason) {
730
571
  if (!this.#order.includes(message)) return;
731
572
  this.#forget(message);
@@ -734,6 +575,7 @@ var FlashController = class extends Controller {
734
575
  const finalize = () => {
735
576
  if (!this.#leaving.delete(message)) return;
736
577
  message.remove();
578
+ this.#arrivals.delete(message);
737
579
  this.dispatch("dismiss", { detail: { element: message, reason } });
738
580
  };
739
581
  const transition = this.#transitionMs(message);
@@ -743,13 +585,29 @@ var FlashController = class extends Controller {
743
585
  finalize();
744
586
  }
745
587
  }
746
- /** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */
747
588
  #transitionMs(el) {
748
589
  if (typeof window.getComputedStyle !== "function") return 0;
749
590
  return maxTransitionTotalMs(window.getComputedStyle(el));
750
591
  }
592
+ get #safeDuration() {
593
+ return this.#numbers.read(
594
+ this,
595
+ "duration",
596
+ this.durationValue,
597
+ _FlashController.values.duration.default,
598
+ _FlashController.valueConstraints.duration
599
+ );
600
+ }
601
+ get #safeMax() {
602
+ return this.#numbers.read(
603
+ this,
604
+ "max",
605
+ this.maxValue,
606
+ _FlashController.values.max.default,
607
+ _FlashController.valueConstraints.max
608
+ );
609
+ }
751
610
  };
752
611
 
753
612
  export { FlashController };
754
- //# sourceMappingURL=flash_controller.js.map
755
613
  //# sourceMappingURL=flash_controller.js.map