stimeo-ui 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +184 -0
  3. data/dist/controllers/aspect_ratio_controller.js +19 -11
  4. data/dist/controllers/avatar_controller.js +195 -40
  5. data/dist/controllers/breadcrumb_controller.js +5 -1
  6. data/dist/controllers/carousel_controller.js +90 -10
  7. data/dist/controllers/checkbox_controller.js +136 -25
  8. data/dist/controllers/clipboard_controller.js +8 -3
  9. data/dist/controllers/collapsible_controller.js +4 -1
  10. data/dist/controllers/color_picker_controller.js +41 -11
  11. data/dist/controllers/context_menu_controller.js +2 -2
  12. data/dist/controllers/countdown_controller.js +5 -1
  13. data/dist/controllers/date_range_picker_controller.js +162 -31
  14. data/dist/controllers/direct_upload_controller.js +3 -3
  15. data/dist/controllers/empty_state_controller.js +107 -16
  16. data/dist/controllers/file_dropzone_controller.js +26 -3
  17. data/dist/controllers/flash_controller.js +161 -21
  18. data/dist/controllers/form_validation_controller.js +8 -2
  19. data/dist/controllers/frame_loading_controller.js +94 -22
  20. data/dist/controllers/highlight_controller.js +38 -1
  21. data/dist/controllers/idle_controller.js +39 -6
  22. data/dist/controllers/local_time_controller.js +2 -0
  23. data/dist/controllers/masonry_controller.js +1 -1
  24. data/dist/controllers/menubar_controller.js +5 -3
  25. data/dist/controllers/meter_controller.js +3 -1
  26. data/dist/controllers/multi_select_controller.js +460 -151
  27. data/dist/controllers/network_status_controller.js +1 -3
  28. data/dist/controllers/number_input_controller.js +455 -64
  29. data/dist/controllers/overflow_menu_controller.js +5 -1
  30. data/dist/controllers/pagination_controller.js +38 -1
  31. data/dist/controllers/password_strength_controller.js +21 -3
  32. data/dist/controllers/persist_controller.js +24 -5
  33. data/dist/controllers/pointer_drag_controller.js +10 -0
  34. data/dist/controllers/portal_controller.js +10 -0
  35. data/dist/controllers/progress_controller.js +7 -3
  36. data/dist/controllers/radio_group_controller.js +540 -56
  37. data/dist/controllers/range_slider_controller.js +385 -94
  38. data/dist/controllers/rating_controller.js +274 -89
  39. data/dist/controllers/relative_time_controller.js +2 -0
  40. data/dist/controllers/resizable_controller.js +33 -0
  41. data/dist/controllers/roving_controller.js +60 -5
  42. data/dist/controllers/scroll_area_controller.js +155 -23
  43. data/dist/controllers/scroll_visibility_controller.js +33 -0
  44. data/dist/controllers/separator_controller.js +13 -17
  45. data/dist/controllers/skeleton_controller.js +71 -3
  46. data/dist/controllers/slider_controller.js +325 -48
  47. data/dist/controllers/spinner_controller.js +18 -3
  48. data/dist/controllers/step_indicator_controller.js +3 -1
  49. data/dist/controllers/stepper_controller.js +2 -0
  50. data/dist/controllers/switch_controller.js +162 -18
  51. data/dist/controllers/tags_input_controller.js +356 -120
  52. data/dist/controllers/textarea_autosize_controller.js +1 -1
  53. data/dist/controllers/time_picker_controller.js +296 -104
  54. data/dist/controllers/toggle_group_controller.js +378 -55
  55. data/dist/controllers/toolbar_controller.js +5 -3
  56. data/dist/controllers/tree_view_controller.js +24 -4
  57. data/dist/index.js +3811 -1048
  58. data/lib/stimeo/ui/version.rb +1 -1
  59. metadata +2 -2
@@ -2,6 +2,43 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/scroll_area_controller.ts
4
4
 
5
+ // src/utils/attribute_lease.ts
6
+ var AttributeLease = class {
7
+ #attribute;
8
+ #records = /* @__PURE__ */ new Map();
9
+ /** @param attribute - The attribute whose temporary values this lease owns. */
10
+ constructor(attribute) {
11
+ this.#attribute = attribute;
12
+ }
13
+ /** Writes or removes the leased attribute while preserving its authored value. */
14
+ write(element, value) {
15
+ const existing = this.#records.get(element);
16
+ if (existing) {
17
+ existing.written = value;
18
+ } else {
19
+ this.#records.set(element, {
20
+ original: element.getAttribute(this.#attribute),
21
+ written: value
22
+ });
23
+ }
24
+ if (value === null) element.removeAttribute(this.#attribute);
25
+ else element.setAttribute(this.#attribute, value);
26
+ }
27
+ /** Returns one lease without overwriting a value subsequently authored by a consumer. */
28
+ return(element) {
29
+ const record = this.#records.get(element);
30
+ if (!record) return;
31
+ this.#records.delete(element);
32
+ if (element.getAttribute(this.#attribute) !== record.written) return;
33
+ if (record.original === null) element.removeAttribute(this.#attribute);
34
+ else element.setAttribute(this.#attribute, record.original);
35
+ }
36
+ /** Returns every outstanding lease using the same ownership check as {@link return}. */
37
+ returnAll() {
38
+ for (const element of Array.from(this.#records.keys())) this.return(element);
39
+ }
40
+ };
41
+
5
42
  // src/utils/layout_observer.ts
6
43
  var LayoutObserver = class {
7
44
  #callback;
@@ -72,10 +109,74 @@ function logicalScrollMetrics(element, horizontal) {
72
109
  return { position: Math.min(max, Math.max(0, position)), max };
73
110
  }
74
111
 
112
+ // src/utils/microtask_coalescer.ts
113
+ var MicrotaskCoalescer = class {
114
+ #run;
115
+ #queued = false;
116
+ #active = false;
117
+ #generation = 0;
118
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
119
+ constructor(run) {
120
+ this.#run = run;
121
+ }
122
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
123
+ activate() {
124
+ this.#active = true;
125
+ }
126
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
127
+ cancel() {
128
+ this.#active = false;
129
+ this.#queued = false;
130
+ this.#generation += 1;
131
+ }
132
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
133
+ schedule() {
134
+ if (!this.#active || this.#queued) return;
135
+ this.#queued = true;
136
+ const generation = this.#generation;
137
+ queueMicrotask(() => {
138
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
139
+ this.#queued = false;
140
+ this.#run();
141
+ });
142
+ }
143
+ };
144
+
145
+ // src/utils/before_cache_reset.ts
146
+ var BeforeCacheReset = class _BeforeCacheReset {
147
+ /** Every subscribed instance, iterated by the one shared document listener. */
148
+ static #subscribers = /* @__PURE__ */ new Set();
149
+ /** The shared listener; installed while at least one instance is subscribed. */
150
+ static #onBeforeCache = () => {
151
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
152
+ };
153
+ #rewind;
154
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
155
+ constructor(rewind) {
156
+ this.#rewind = rewind;
157
+ }
158
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
159
+ activate() {
160
+ const first = _BeforeCacheReset.#subscribers.size === 0;
161
+ _BeforeCacheReset.#subscribers.add(this);
162
+ if (first) {
163
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
164
+ }
165
+ }
166
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
167
+ deactivate() {
168
+ _BeforeCacheReset.#subscribers.delete(this);
169
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
170
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
171
+ }
172
+ };
173
+
75
174
  // src/utils/tabindex_loan.ts
76
175
  var TabindexLoan = class {
77
176
  #value;
78
177
  #lent = /* @__PURE__ */ new Set();
178
+ /** Returns live loans before Turbo can copy them into its page snapshot. */
179
+ #beforeCache = new BeforeCacheReset(() => this.returnAll());
79
180
  /**
80
181
  * @param value - the `tabindex` to lend. `"-1"` (the default) is
81
182
  * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
@@ -89,6 +190,7 @@ var TabindexLoan = class {
89
190
  if (element.hasAttribute("tabindex")) return;
90
191
  element.setAttribute("tabindex", this.#value);
91
192
  this.#lent.add(element);
193
+ this.#beforeCache.activate();
92
194
  }
93
195
  /** Takes back every loan whose value is still the one that was lent. */
94
196
  returnAll() {
@@ -96,6 +198,7 @@ var TabindexLoan = class {
96
198
  if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
97
199
  }
98
200
  this.#lent.clear();
201
+ this.#beforeCache.deactivate();
99
202
  }
100
203
  };
101
204
 
@@ -117,29 +220,56 @@ var ScrollAreaController = class extends Controller {
117
220
  };
118
221
  static events = ["reach"];
119
222
  #layout = new LayoutObserver(() => this.#update());
223
+ /** Current element receiving scroll, resize, mutation, and keyboard-reach behavior. */
224
+ #viewport = null;
225
+ /** Guards target callbacks before connect and after disconnect. */
226
+ #connected = false;
227
+ /** Collapses target replacement callbacks into one final-DOM rebind. */
228
+ #rebind = new MicrotaskCoalescer(() => this.#syncViewport());
120
229
  /** Re-checks the tab stop when the viewport's focusable content comes or goes. */
121
230
  #content = null;
122
231
  /** Last edge reported via `reach`, so the event fires once per arrival. */
123
232
  #lastEdge = null;
124
233
  /** Whether this controller added `tabindex`, so teardown only removes its own. */
125
234
  #tabindex = new TabindexLoan("0");
126
- /** Whether this controller added `role="region"`, for symmetric teardown. */
127
- #addedRole = false;
235
+ /** Temporarily owns a derived `role="region"` without losing an authored replacement. */
236
+ #role = new AttributeLease("role");
128
237
  #onScroll = () => {
129
238
  this.#update();
130
239
  };
131
240
  connect() {
132
- if (!this.hasViewportTarget) return;
133
- this.viewportTarget.addEventListener("scroll", this.#onScroll, { passive: true });
134
- this.#layout.observe(this.viewportTarget);
241
+ this.#connected = true;
242
+ this.#rebind.activate();
135
243
  this.#layout.observeViewport();
244
+ this.#syncViewport();
245
+ }
246
+ /** Schedules a complete observer/listener rebind for a runtime viewport target. */
247
+ viewportTargetConnected() {
248
+ this.#rebind.schedule();
249
+ }
250
+ /** Schedules cleanup or replacement binding after a viewport leaves. */
251
+ viewportTargetDisconnected() {
252
+ this.#rebind.schedule();
253
+ }
254
+ /** Rebinds every viewport-owned resource against the final target in this mutation batch. */
255
+ #syncViewport() {
256
+ if (!this.#connected) return;
257
+ const next = this.hasViewportTarget ? this.viewportTarget : null;
258
+ if (next === this.#viewport) {
259
+ this.#update();
260
+ return;
261
+ }
262
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
263
+ this.#viewport = next;
264
+ if (!next) return;
265
+ next.addEventListener("scroll", this.#onScroll, { passive: true });
266
+ this.#layout.observe(next);
136
267
  if (typeof MutationObserver !== "undefined") {
137
268
  this.#content = new MutationObserver(() => {
138
- if (!this.hasViewportTarget) return;
139
- const vp = this.viewportTarget;
140
- this.#syncKeyboardReach(vp, this.#syncOverflow(vp));
269
+ if (this.#viewport !== next) return;
270
+ this.#syncKeyboardReach(next, this.#syncOverflow(next));
141
271
  });
142
- this.#content.observe(this.viewportTarget, {
272
+ this.#content.observe(next, {
143
273
  subtree: true,
144
274
  childList: true,
145
275
  attributes: true
@@ -148,24 +278,31 @@ var ScrollAreaController = class extends Controller {
148
278
  this.#update();
149
279
  }
150
280
  disconnect() {
151
- if (this.hasViewportTarget) {
152
- this.viewportTarget.removeEventListener("scroll", this.#onScroll);
153
- this.#clearAddedAttributes(this.viewportTarget);
154
- }
281
+ this.#connected = false;
282
+ this.#rebind.cancel();
283
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
155
284
  this.#layout.disconnect();
285
+ this.#lastEdge = null;
286
+ }
287
+ /** Releases every resource and derived attribute owned by one former viewport. */
288
+ #unbindViewport(viewport) {
289
+ viewport.removeEventListener("scroll", this.#onScroll);
290
+ this.#layout.unobserve(viewport);
156
291
  this.#content?.disconnect();
157
292
  this.#content = null;
293
+ this.#clearAddedAttributes(viewport);
294
+ if (this.#viewport === viewport) this.#viewport = null;
158
295
  this.#lastEdge = null;
159
296
  }
160
297
  /** Re-measures overflow and scroll position and reflects the state hooks. */
161
298
  #update() {
162
- if (!this.hasViewportTarget) return;
163
- const vp = this.viewportTarget;
299
+ const vp = this.#viewport;
300
+ if (!vp) return;
164
301
  const overflowing = this.#syncOverflow(vp);
165
302
  this.#syncKeyboardReach(vp, overflowing);
166
303
  const { position, progress } = this.#measurePosition(vp);
167
304
  this.element.setAttribute("data-scroll", position);
168
- this.element.style.setProperty("--stimeo-scroll-progress", String(progress));
305
+ this.element.style.setProperty("--stimeo--scroll-progress", String(progress));
169
306
  const edge = position === "start" ? "start" : position === "end" ? "end" : null;
170
307
  if (overflowing && edge && edge !== this.#lastEdge) {
171
308
  this.#lastEdge = edge;
@@ -174,7 +311,6 @@ var ScrollAreaController = class extends Controller {
174
311
  this.#lastEdge = null;
175
312
  }
176
313
  }
177
- /** Whether the viewport can scroll on the configured axis. */
178
314
  /**
179
315
  * Measures overflow and reflects the `data-overflow` hook.
180
316
  *
@@ -219,8 +355,7 @@ var ScrollAreaController = class extends Controller {
219
355
  if (wantsTabindex) {
220
356
  this.#tabindex.lend(vp);
221
357
  if (!vp.hasAttribute("role") && this.#hasAccessibleName(vp)) {
222
- vp.setAttribute("role", "region");
223
- this.#addedRole = true;
358
+ this.#role.write(vp, "region");
224
359
  }
225
360
  } else {
226
361
  this.#clearAddedAttributes(vp);
@@ -229,10 +364,7 @@ var ScrollAreaController = class extends Controller {
229
364
  /** Removes (and resets the flags for) only the attributes this controller added. */
230
365
  #clearAddedAttributes(vp) {
231
366
  this.#tabindex.returnAll();
232
- if (this.#addedRole) {
233
- vp.removeAttribute("role");
234
- this.#addedRole = false;
235
- }
367
+ this.#role.return(vp);
236
368
  }
237
369
  /**
238
370
  * Whether the viewport owns something the user can Tab to *right now*.
@@ -7,10 +7,41 @@ function prefersReducedMotion() {
7
7
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8
8
  }
9
9
 
10
+ // src/utils/before_cache_reset.ts
11
+ var BeforeCacheReset = class _BeforeCacheReset {
12
+ /** Every subscribed instance, iterated by the one shared document listener. */
13
+ static #subscribers = /* @__PURE__ */ new Set();
14
+ /** The shared listener; installed while at least one instance is subscribed. */
15
+ static #onBeforeCache = () => {
16
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
17
+ };
18
+ #rewind;
19
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
20
+ constructor(rewind) {
21
+ this.#rewind = rewind;
22
+ }
23
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
24
+ activate() {
25
+ const first = _BeforeCacheReset.#subscribers.size === 0;
26
+ _BeforeCacheReset.#subscribers.add(this);
27
+ if (first) {
28
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
29
+ }
30
+ }
31
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
32
+ deactivate() {
33
+ _BeforeCacheReset.#subscribers.delete(this);
34
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
35
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
36
+ }
37
+ };
38
+
10
39
  // src/utils/tabindex_loan.ts
11
40
  var TabindexLoan = class {
12
41
  #value;
13
42
  #lent = /* @__PURE__ */ new Set();
43
+ /** Returns live loans before Turbo can copy them into its page snapshot. */
44
+ #beforeCache = new BeforeCacheReset(() => this.returnAll());
14
45
  /**
15
46
  * @param value - the `tabindex` to lend. `"-1"` (the default) is
16
47
  * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
@@ -24,6 +55,7 @@ var TabindexLoan = class {
24
55
  if (element.hasAttribute("tabindex")) return;
25
56
  element.setAttribute("tabindex", this.#value);
26
57
  this.#lent.add(element);
58
+ this.#beforeCache.activate();
27
59
  }
28
60
  /** Takes back every loan whose value is still the one that was lent. */
29
61
  returnAll() {
@@ -31,6 +63,7 @@ var TabindexLoan = class {
31
63
  if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
32
64
  }
33
65
  this.#lent.clear();
66
+ this.#beforeCache.deactivate();
34
67
  }
35
68
  };
36
69
 
@@ -8,6 +8,13 @@ function isReservedArrowChord(event, allow = []) {
8
8
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
9
  }
10
10
 
11
+ // src/utils/default_attribute.ts
12
+ function setDefaultAttribute(element, name, value) {
13
+ if (element.hasAttribute(name)) return false;
14
+ element.setAttribute(name, value);
15
+ return true;
16
+ }
17
+
11
18
  // src/controllers/separator_controller.ts
12
19
  var SeparatorController = class extends Controller {
13
20
  static values = {
@@ -18,19 +25,13 @@ var SeparatorController = class extends Controller {
18
25
  static actions = ["onKeydown"];
19
26
  static events = ["change"];
20
27
  connect() {
21
- if (!this.element.hasAttribute("role")) {
22
- this.element.setAttribute("role", "separator");
23
- }
24
- if (!this.element.hasAttribute("aria-orientation")) {
25
- this.element.setAttribute("aria-orientation", this.orientationValue);
26
- }
28
+ setDefaultAttribute(this.element, "role", "separator");
29
+ setDefaultAttribute(this.element, "aria-orientation", this.orientationValue);
27
30
  if (this.focusableValue) {
28
- if (!this.element.hasAttribute("tabindex")) {
29
- this.element.setAttribute("tabindex", "0");
30
- }
31
- this.#setDefault("aria-valuemin", "0");
32
- this.#setDefault("aria-valuemax", "100");
33
- this.#setDefault("aria-valuenow", String(this.#clamp(this.#value)));
31
+ setDefaultAttribute(this.element, "tabindex", "0");
32
+ setDefaultAttribute(this.element, "aria-valuemin", "0");
33
+ setDefaultAttribute(this.element, "aria-valuemax", "100");
34
+ setDefaultAttribute(this.element, "aria-valuenow", String(this.#clamp(this.#value)));
34
35
  }
35
36
  }
36
37
  /** Adjusts the value on arrow / Home / End keys (focusable variant only). */
@@ -84,11 +85,6 @@ var SeparatorController = class extends Controller {
84
85
  const parsed = Number.parseFloat(this.element.getAttribute(name) ?? "");
85
86
  return Number.isNaN(parsed) ? fallback : parsed;
86
87
  }
87
- #setDefault(name, value) {
88
- if (!this.element.hasAttribute(name)) {
89
- this.element.setAttribute(name, value);
90
- }
91
- }
92
88
  };
93
89
 
94
90
  export { SeparatorController };
@@ -19,6 +19,61 @@ function fillTemplate(template, values) {
19
19
  });
20
20
  }
21
21
 
22
+ // src/utils/detach_gate.ts
23
+ var DetachGate = class _DetachGate {
24
+ /** Set while a probe is queued, waiting for a reconnect to cancel it. */
25
+ #pending = false;
26
+ /**
27
+ * True while a probe is queued — the last disconnect was ambiguous and no
28
+ * reconnect has cancelled it yet. Read it from `connect()` to tell the
29
+ * reconnect half of an in-page move from a first connect: a controller whose
30
+ * initialisation restarts a measurement (a min-duration floor, an elapsed
31
+ * counter) must skip it for the move, where nothing actually restarted.
32
+ */
33
+ get pending() {
34
+ return this.#pending;
35
+ }
36
+ /**
37
+ * True when the disconnect is definitely a real detach — the element left
38
+ * the document, or `data-controller` no longer lists the identifier. False
39
+ * means ambiguous (in-page move or observed-root exit), NOT "alive".
40
+ */
41
+ static isDetached(host) {
42
+ if (!host.element.isConnected) return true;
43
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
44
+ return !tokens.includes(host.identifier);
45
+ }
46
+ /**
47
+ * Call from `disconnect()`: runs `teardown` synchronously on a definite
48
+ * detach (fast path), otherwise defers it one microtask — a reconnect
49
+ * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
50
+ * One microtask is the whole probe window: Stimulus reconnects a moved
51
+ * element within the same mutation batch, before the checkpoint drains.
52
+ */
53
+ disconnected(host, teardown) {
54
+ if (_DetachGate.isDetached(host)) {
55
+ this.#pending = false;
56
+ teardown();
57
+ return;
58
+ }
59
+ this.#pending = true;
60
+ queueMicrotask(() => {
61
+ if (!this.#pending) return;
62
+ this.#pending = false;
63
+ teardown();
64
+ });
65
+ }
66
+ /**
67
+ * Disarms a pending probe. Call from `connect()` (the reconnect that proves
68
+ * an in-page move) and from the head of any teardown path not routed through
69
+ * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
70
+ * never run the teardown a second time.
71
+ */
72
+ cancel() {
73
+ this.#pending = false;
74
+ }
75
+ };
76
+
22
77
  // src/utils/min_duration_floor.ts
23
78
  var MinDurationFloor = class {
24
79
  #timers;
@@ -131,14 +186,16 @@ var SkeletonController = class extends Controller {
131
186
  static events = ["ready"];
132
187
  #timers = new SafeTimeout();
133
188
  #floor = new MinDurationFloor(this.#timers);
189
+ #gate = new DetachGate();
134
190
  connect() {
135
- if (this.#state !== "ready") {
191
+ const moved = this.#gate.pending;
192
+ this.#gate.cancel();
193
+ if (!moved && this.#state !== "ready") {
136
194
  this.#enterLoading();
137
195
  }
138
196
  }
139
197
  disconnect() {
140
- this.#timers.clearAll();
141
- this.#floor.cancel();
198
+ this.#gate.disconnected(this, () => this.#teardown());
142
199
  }
143
200
  /** Swaps to the real content. Honors `minDuration` to prevent a flash. */
144
201
  ready() {
@@ -167,6 +224,17 @@ var SkeletonController = class extends Controller {
167
224
  this.dispatch("ready", { detail: {} });
168
225
  announce(fillTemplate(this.announceReadyTextValue, {}));
169
226
  }
227
+ /**
228
+ * Drops the held reveal on a real detach. The markup keeps whatever it last
229
+ * held: an element on its way out of the document has no reader left, and one
230
+ * whose `data-controller` dropped the identifier no longer resolves its own
231
+ * targets, so the rollback could only ever be partial.
232
+ */
233
+ #teardown() {
234
+ this.#gate.cancel();
235
+ this.#timers.clearAll();
236
+ this.#floor.cancel();
237
+ }
170
238
  /** Current lifecycle phase as reflected on `data-state`. */
171
239
  get #state() {
172
240
  return this.element.getAttribute("data-state") ?? "loading";