stimeo-ui 0.15.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 +209 -0
  3. data/README.md +128 -1
  4. data/dist/cable/index.js +392 -417
  5. data/dist/controllers/accordion_controller.js +228 -28
  6. data/dist/controllers/alert_dialog_controller.js +862 -221
  7. data/dist/controllers/announcer_controller.js +135 -191
  8. data/dist/controllers/aspect_ratio_controller.js +0 -10
  9. data/dist/controllers/auto_submit_controller.js +295 -110
  10. data/dist/controllers/avatar_controller.js +172 -106
  11. data/dist/controllers/breadcrumb_controller.js +0 -129
  12. data/dist/controllers/bulk_select_controller.js +264 -53
  13. data/dist/controllers/calendar_controller.js +346 -180
  14. data/dist/controllers/carousel_controller.js +587 -300
  15. data/dist/controllers/character_counter_controller.js +271 -143
  16. data/dist/controllers/checkbox_controller.js +77 -75
  17. data/dist/controllers/clipboard_controller.js +187 -108
  18. data/dist/controllers/collapsible_controller.js +374 -108
  19. data/dist/controllers/color_picker_controller.js +183 -127
  20. data/dist/controllers/combobox_controller.js +426 -109
  21. data/dist/controllers/command_palette_controller.js +994 -350
  22. data/dist/controllers/conditional_fields_controller.js +212 -125
  23. data/dist/controllers/confirm_controller.js +939 -245
  24. data/dist/controllers/context_menu_controller.js +275 -127
  25. data/dist/controllers/count_up_controller.js +73 -21
  26. data/dist/controllers/countdown_controller.js +235 -97
  27. data/dist/controllers/currency_input_controller.js +241 -118
  28. data/dist/controllers/data_grid_controller.js +229 -135
  29. data/dist/controllers/date_range_picker_controller.js +359 -165
  30. data/dist/controllers/dialog_controller.js +887 -225
  31. data/dist/controllers/direct_upload_controller.js +80 -142
  32. data/dist/controllers/dirty_form_controller.js +26 -47
  33. data/dist/controllers/dismissible_controller.js +175 -20
  34. data/dist/controllers/drawer_controller.js +963 -336
  35. data/dist/controllers/dropdown_controller.js +342 -85
  36. data/dist/controllers/editable_controller.js +182 -58
  37. data/dist/controllers/empty_state_controller.js +67 -53
  38. data/dist/controllers/file_dropzone_controller.js +426 -229
  39. data/dist/controllers/filter_controller.js +78 -36
  40. data/dist/controllers/flash_controller.js +328 -211
  41. data/dist/controllers/focus_controller.js +654 -239
  42. data/dist/controllers/form_field_controller.js +153 -136
  43. data/dist/controllers/form_validation_controller.js +29 -96
  44. data/dist/controllers/frame_loading_controller.js +263 -198
  45. data/dist/controllers/highlight_controller.js +119 -70
  46. data/dist/controllers/hover_card_controller.js +335 -118
  47. data/dist/controllers/idle_controller.js +316 -65
  48. data/dist/controllers/input_mask_controller.js +116 -61
  49. data/dist/controllers/intersection_controller.js +147 -103
  50. data/dist/controllers/lazy_frame_controller.js +9 -56
  51. data/dist/controllers/listbox_controller.js +413 -153
  52. data/dist/controllers/local_time_controller.js +54 -64
  53. data/dist/controllers/masonry_controller.js +70 -95
  54. data/dist/controllers/menu_controller.js +254 -167
  55. data/dist/controllers/menubar_controller.js +206 -331
  56. data/dist/controllers/meter_controller.js +159 -71
  57. data/dist/controllers/multi_select_controller.js +797 -388
  58. data/dist/controllers/navigation_menu_controller.js +156 -231
  59. data/dist/controllers/nested_form_controller.js +373 -174
  60. data/dist/controllers/network_status_controller.js +241 -45
  61. data/dist/controllers/number_input_controller.js +387 -227
  62. data/dist/controllers/optimistic_controller.js +28 -75
  63. data/dist/controllers/otp_controller.js +344 -196
  64. data/dist/controllers/overflow_indicator_controller.js +167 -93
  65. data/dist/controllers/overflow_menu_controller.js +408 -277
  66. data/dist/controllers/pagination_controller.js +342 -120
  67. data/dist/controllers/password_reveal_controller.js +424 -99
  68. data/dist/controllers/password_strength_controller.js +99 -153
  69. data/dist/controllers/persist_controller.js +252 -96
  70. data/dist/controllers/pointer_drag_controller.js +263 -164
  71. data/dist/controllers/popover_controller.js +267 -94
  72. data/dist/controllers/portal_controller.js +209 -72
  73. data/dist/controllers/preview_guard_controller.js +243 -112
  74. data/dist/controllers/progress_controller.js +132 -59
  75. data/dist/controllers/radio_group_controller.js +138 -133
  76. data/dist/controllers/range_slider_controller.js +245 -112
  77. data/dist/controllers/rating_controller.js +429 -194
  78. data/dist/controllers/read_more_controller.js +185 -47
  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 +294 -118
  83. data/dist/controllers/roving_controller.js +39 -84
  84. data/dist/controllers/scroll_area_controller.js +428 -221
  85. data/dist/controllers/scroll_restore_controller.js +0 -53
  86. data/dist/controllers/scroll_visibility_controller.js +395 -146
  87. data/dist/controllers/scrollspy_controller.js +181 -236
  88. data/dist/controllers/separator_controller.js +288 -132
  89. data/dist/controllers/sidebar_controller.js +1071 -375
  90. data/dist/controllers/skeleton_controller.js +248 -92
  91. data/dist/controllers/slider_controller.js +211 -96
  92. data/dist/controllers/smart_sticky_header_controller.js +163 -54
  93. data/dist/controllers/sortable_controller.js +13 -110
  94. data/dist/controllers/spinner_controller.js +277 -161
  95. data/dist/controllers/step_indicator_controller.js +107 -53
  96. data/dist/controllers/stepper_controller.js +171 -48
  97. data/dist/controllers/stick_to_bottom_controller.js +310 -116
  98. data/dist/controllers/sticky_observer_controller.js +214 -49
  99. data/dist/controllers/submit_once_controller.js +294 -192
  100. data/dist/controllers/switch_controller.js +107 -43
  101. data/dist/controllers/tabs_controller.js +50 -25
  102. data/dist/controllers/tags_input_controller.js +290 -149
  103. data/dist/controllers/textarea_autosize_controller.js +71 -58
  104. data/dist/controllers/theme_controller.js +148 -80
  105. data/dist/controllers/time_picker_controller.js +162 -81
  106. data/dist/controllers/toast_controller.js +365 -193
  107. data/dist/controllers/toggle_group_controller.js +194 -99
  108. data/dist/controllers/toolbar_controller.js +92 -90
  109. data/dist/controllers/tooltip_controller.js +185 -125
  110. data/dist/controllers/transition_controller.js +123 -115
  111. data/dist/controllers/tree_view_controller.js +248 -290
  112. data/dist/index.js +8649 -8572
  113. data/dist/positioning/index.js +148 -45
  114. data/lib/stimeo/ui/version.rb +1 -1
  115. metadata +3 -3
@@ -1,78 +1,152 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/scroll_area_controller.ts
3
+ function sharedRegistry(key, create) {
4
+ const symbol = Symbol.for(key);
5
+ const scope = globalThis;
6
+ const existing = scope[symbol];
7
+ if (existing !== void 0) return existing;
8
+ const registry = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry });
10
+ return registry;
11
+ }
4
12
 
5
- // src/utils/attribute_lease.ts
6
- var AttributeLease = class {
13
+ function isAttributeValue(value) {
14
+ return value === null || typeof value === "string";
15
+ }
16
+ function readLeaseRecord(element, record) {
17
+ try {
18
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
19
+ if (!Array.isArray(parsed)) return void 0;
20
+ const [original, written, ...rest] = parsed;
21
+ const linked = rest[0] !== false;
22
+ const beneath = linked ? rest : rest.slice(1);
23
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
24
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ var AttributeLease = class _AttributeLease {
30
+ static #registry = sharedRegistry(
31
+ "stimeo-ui.attribute-lease.registry.v1",
32
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
+ );
7
34
  #attribute;
8
- #records = /* @__PURE__ */ new Map();
9
- /** @param attribute - The attribute whose temporary values this lease owns. */
10
- constructor(attribute) {
35
+ #record;
36
+ #base;
37
+ #held = /* @__PURE__ */ new Set();
38
+ constructor(attribute, owner, options = {}) {
11
39
  this.#attribute = attribute;
40
+ this.#record = `data-${owner}-${attribute}-lease`;
41
+ this.#base = options.base === true;
42
+ const names = _AttributeLease.#registry.names;
43
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
12
44
  }
13
- /** Writes or removes the leased attribute while preserving its authored value. */
14
45
  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
- this.#reflect(element, value);
46
+ const stack = this.#stack(element);
47
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
+ this.#held.add(element);
49
+ const shows = !this.#covered(element, stack, hold);
50
+ hold.written = value;
51
+ this.#mark(element, stack);
52
+ if (shows) this.#reflect(element, value);
25
53
  }
26
- /** Returns one lease without overwriting a value subsequently authored by a consumer. */
27
54
  return(element) {
28
- const record = this.#records.get(element);
29
- if (!record) return;
30
- this.#records.delete(element);
31
- const stillOwned = element.getAttribute(this.#attribute) === record.written;
32
- if (stillOwned) this.#reflect(element, record.original);
55
+ const held = this.#held.delete(element);
56
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
58
+ const hold = this.#take(stack);
59
+ if (!hold) return false;
60
+ const carries = element.getAttribute(this.#attribute) === hold.written;
61
+ const owns = carries && !this.#covered(element, stack, hold);
62
+ const index = stack.indexOf(hold);
63
+ stack.splice(index, 1);
64
+ const above = stack[index];
65
+ if (above) above.linked &&= hold.linked;
66
+ this.#mark(element, stack);
67
+ if (!owns) return true;
68
+ const below = hold.linked ? stack[index - 1] : void 0;
69
+ this.#reflect(element, below ? below.written : hold.original);
70
+ return true;
71
+ }
72
+ returnAll() {
73
+ for (const element of Array.from(this.#held)) this.return(element);
74
+ }
75
+ #stack(element) {
76
+ const stacks = _AttributeLease.#registry.stacks;
77
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
+ stacks.set(element, byAttribute);
79
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
+ byAttribute.set(this.#attribute, stack);
81
+ return stack;
82
+ }
83
+ #seed(element) {
84
+ const found = [];
85
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
+ const read = readLeaseRecord(element, record);
87
+ if (read) found.push({ ...read, record });
88
+ }
89
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
+ lease: null,
92
+ record,
93
+ written,
94
+ original,
95
+ linked: linked && index > 0
96
+ }));
97
+ }
98
+ #take(stack) {
99
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
100
+ if (hold) hold.lease = this;
101
+ return hold;
102
+ }
103
+ #acquire(element, stack) {
104
+ const current = element.getAttribute(this.#attribute);
105
+ const top = stack.at(-1);
106
+ const bottom = stack[0];
107
+ const over = top?.written === current ? top : void 0;
108
+ const from = this.#base ? bottom : over;
109
+ const hold = {
110
+ lease: this,
111
+ record: this.#record,
112
+ written: current,
113
+ original: from ? from.original : current,
114
+ linked: !this.#base && over !== void 0
115
+ };
116
+ if (this.#base && bottom) {
117
+ bottom.linked = true;
118
+ stack.unshift(hold);
119
+ } else stack.push(hold);
120
+ return hold;
121
+ }
122
+ #covered(element, stack, hold) {
123
+ const current = element.getAttribute(this.#attribute);
124
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
125
+ }
126
+ #mark(element, stack) {
127
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
128
+ stack.forEach((hold, index) => {
129
+ const names = stack.slice(0, index).map((under) => under.record);
130
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
131
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
132
+ records.set(
133
+ hold.record,
134
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
135
+ );
136
+ });
137
+ for (const [name, recorded] of records) {
138
+ if (element.getAttribute(name) === recorded) continue;
139
+ if (recorded === null) element.removeAttribute(name);
140
+ else element.setAttribute(name, recorded);
141
+ }
33
142
  }
34
- /** Reflects only a real value transition, avoiding self-triggered mutation work. */
35
143
  #reflect(element, value) {
36
144
  if (element.getAttribute(this.#attribute) === value) return;
37
145
  if (value === null) element.removeAttribute(this.#attribute);
38
146
  else element.setAttribute(this.#attribute, value);
39
147
  }
40
- /** Returns every outstanding lease using the same ownership check as {@link return}. */
41
- returnAll() {
42
- for (const element of Array.from(this.#records.keys())) this.return(element);
43
- }
44
148
  };
45
149
 
46
- // src/utils/before_cache_reset.ts
47
- var BeforeCacheReset = class _BeforeCacheReset {
48
- /** Every subscribed instance, iterated by the one shared document listener. */
49
- static #subscribers = /* @__PURE__ */ new Set();
50
- /** The shared listener; installed while at least one instance is subscribed. */
51
- static #onBeforeCache = () => {
52
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
53
- };
54
- #rewind;
55
- /** @param rewind - the pass that returns this controller's state to its initial form. */
56
- constructor(rewind) {
57
- this.#rewind = rewind;
58
- }
59
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
60
- activate() {
61
- const first = _BeforeCacheReset.#subscribers.size === 0;
62
- _BeforeCacheReset.#subscribers.add(this);
63
- if (first) {
64
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
65
- }
66
- }
67
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
68
- deactivate() {
69
- _BeforeCacheReset.#subscribers.delete(this);
70
- if (_BeforeCacheReset.#subscribers.size > 0) return;
71
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
72
- }
73
- };
74
-
75
- // src/utils/focus_candidate.ts
76
150
  function inheritsFieldsetDisabled(control) {
77
151
  let fieldset = control.closest("fieldset[disabled]");
78
152
  while (fieldset) {
@@ -82,8 +156,19 @@ function inheritsFieldsetDisabled(control) {
82
156
  }
83
157
  return false;
84
158
  }
159
+ function unslotted(element) {
160
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
161
+ }
162
+ function flatTreeParent(element) {
163
+ if (unslotted(element)) return null;
164
+ const parent = element.assignedSlot ?? element.parentNode;
165
+ if (parent instanceof ShadowRoot) return parent.host;
166
+ return parent instanceof Element ? parent : null;
167
+ }
85
168
  function canTakeFocus(element) {
86
- if (element.closest("[hidden], [inert]")) return false;
169
+ for (let node = element; node; node = flatTreeParent(node)) {
170
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
171
+ }
87
172
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
88
173
  if (!("disabled" in element)) return true;
89
174
  if (element.disabled) return false;
@@ -150,14 +235,8 @@ function hasTabStop(root) {
150
235
  return firstTabStop(root) !== null;
151
236
  }
152
237
 
153
- // src/utils/frame_coalescer.ts
154
238
  var FrameCoalescer = class {
155
239
  #frame = null;
156
- /**
157
- * Runs `run` on the next frame, unless a frame is already pending — the first
158
- * request of a burst wins and the rest are dropped. The pending frame is
159
- * released before `run`, so `run` may request the next one.
160
- */
161
240
  schedule(run) {
162
241
  if (this.#frame !== null) return;
163
242
  this.#frame = requestAnimationFrame(() => {
@@ -165,14 +244,6 @@ var FrameCoalescer = class {
165
244
  run();
166
245
  });
167
246
  }
168
- /**
169
- * Drops the pending frame, and reaches the platform only when there is one.
170
- *
171
- * There is no handle value that stands for "nothing pending":
172
- * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
173
- * arrives as a large positive number that the same allocator can hand out, and
174
- * an idle cancel would then drop a frame belonging to someone else.
175
- */
176
247
  cancel() {
177
248
  if (this.#frame === null) return;
178
249
  cancelAnimationFrame(this.#frame);
@@ -180,18 +251,15 @@ var FrameCoalescer = class {
180
251
  }
181
252
  };
182
253
 
183
- // src/utils/layout_observer.ts
184
254
  var LayoutObserver = class {
185
255
  #callback;
186
256
  #resizeObserverFactory;
187
257
  #resizeObserver = null;
188
258
  #observingViewport = false;
189
259
  #loadContainer = null;
190
- /** Stable bound handler so add/removeEventListener target the same reference. */
191
260
  #handleViewportResize = () => {
192
261
  this.#callback();
193
262
  };
194
- /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
195
263
  #handleDescendantLoad = () => {
196
264
  this.#callback();
197
265
  };
@@ -199,11 +267,6 @@ var LayoutObserver = class {
199
267
  this.#callback = callback;
200
268
  this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
201
269
  }
202
- /**
203
- * Starts observing an element's size. Repeated calls observe additional
204
- * elements through the same shared observer. No-ops when no
205
- * `ResizeObserver` implementation is available.
206
- */
207
270
  observe(element) {
208
271
  if (!this.#resizeObserverFactory) return;
209
272
  if (!this.#resizeObserver) {
@@ -213,47 +276,28 @@ var LayoutObserver = class {
213
276
  }
214
277
  this.#resizeObserver.observe(element);
215
278
  }
216
- /** Stops observing a single element while leaving any others in place. */
217
279
  unobserve(element) {
218
280
  this.#resizeObserver?.unobserve(element);
219
281
  }
220
- /** Starts observing viewport resizes. Idempotent: the listener is added once. */
221
282
  observeViewport() {
222
283
  if (this.#observingViewport) return;
223
284
  this.#observingViewport = true;
224
285
  window.addEventListener("resize", this.#handleViewportResize);
225
286
  }
226
- /** Stops observing viewport resizes without affecting element observation. */
227
287
  unobserveViewport() {
228
288
  if (!this.#observingViewport) return;
229
289
  this.#observingViewport = false;
230
290
  window.removeEventListener("resize", this.#handleViewportResize);
231
291
  }
232
- /**
233
- * Starts reporting a `load` from anywhere inside `container` — an image or a
234
- * frame settling changes the box it sits in, and it measures as zero high until
235
- * then. `load` does not bubble, so the subscription is a capture-phase listener
236
- * on the container itself and nothing the caller spells.
237
- *
238
- * **One container at a time.** A further call moves the observation, so a widget
239
- * whose content element is swapped at runtime releases the element it let go by
240
- * naming the new one — there is no second place for the release to drift from.
241
- */
242
292
  observeDescendantLoads(container) {
243
293
  this.unobserveDescendantLoads();
244
294
  this.#loadContainer = container;
245
295
  container.addEventListener("load", this.#handleDescendantLoad, true);
246
296
  }
247
- /** Stops reporting descendant loads without affecting element or viewport observation. */
248
297
  unobserveDescendantLoads() {
249
298
  this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
250
299
  this.#loadContainer = null;
251
300
  }
252
- /**
253
- * Releases every observation: disconnects the {@link ResizeObserver} and removes
254
- * the viewport and descendant-load listeners. Safe to call multiple times. Call
255
- * this from a controller's `disconnect()`.
256
- */
257
301
  disconnect() {
258
302
  this.#resizeObserver?.disconnect();
259
303
  this.#resizeObserver = null;
@@ -262,7 +306,6 @@ var LayoutObserver = class {
262
306
  }
263
307
  };
264
308
 
265
- // src/utils/logical_scroll.ts
266
309
  function isRtl(element) {
267
310
  return window.getComputedStyle(element).direction === "rtl";
268
311
  }
@@ -276,27 +319,33 @@ function logicalScrollMetrics(element, horizontal) {
276
319
  return { position: Math.min(max, Math.max(0, position)), max };
277
320
  }
278
321
 
279
- // src/utils/microtask_coalescer.ts
322
+ var ListenerSet = class {
323
+ #abort = new AbortController();
324
+ add(target, type, handler, options) {
325
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
326
+ }
327
+ dispose() {
328
+ this.#abort.abort();
329
+ this.#abort = new AbortController();
330
+ }
331
+ };
332
+
280
333
  var MicrotaskCoalescer = class {
281
334
  #run;
282
335
  #queued = false;
283
336
  #active = false;
284
337
  #generation = 0;
285
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
286
338
  constructor(run) {
287
339
  this.#run = run;
288
340
  }
289
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
290
341
  activate() {
291
342
  this.#active = true;
292
343
  }
293
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
294
344
  cancel() {
295
345
  this.#active = false;
296
346
  this.#queued = false;
297
347
  this.#generation += 1;
298
348
  }
299
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
300
349
  schedule() {
301
350
  if (!this.#active || this.#queued) return;
302
351
  this.#queued = true;
@@ -309,90 +358,284 @@ var MicrotaskCoalescer = class {
309
358
  }
310
359
  };
311
360
 
312
- // src/utils/style_property_lease.ts
313
- var StylePropertyLease = class {
361
+ var MorphRenderWatcher = class {
362
+ #listeners = new ListenerSet();
363
+ #pass;
364
+ #element = null;
365
+ #onMorph = () => this.#pass.schedule();
366
+ constructor(run) {
367
+ this.#pass = new MicrotaskCoalescer(run);
368
+ }
369
+ observe(element) {
370
+ if (this.#element === element) return;
371
+ this.disconnect();
372
+ this.#element = element;
373
+ this.#pass.activate();
374
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
375
+ }
376
+ schedule() {
377
+ this.#pass.schedule();
378
+ }
379
+ disconnect() {
380
+ this.#listeners.dispose();
381
+ this.#pass.cancel();
382
+ this.#element = null;
383
+ }
384
+ };
385
+
386
+ var StylePropertyLease = class _StylePropertyLease {
387
+ static #registry = sharedRegistry(
388
+ "stimeo-ui.style-property-lease.registry.v1",
389
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
390
+ );
314
391
  #property;
315
- #records = /* @__PURE__ */ new Map();
316
- /** @param property - The CSS property whose temporary values this lease owns. */
317
- constructor(property) {
392
+ #record;
393
+ #held = /* @__PURE__ */ new Set();
394
+ constructor(property, owner) {
318
395
  this.#property = property;
396
+ this.#record = `data-${owner}-style-${property}-lease`;
397
+ const names = _StylePropertyLease.#registry.names;
398
+ names.set(property, (names.get(property) ?? /* @__PURE__ */ new Set()).add(this.#record));
319
399
  }
320
- /** Writes or removes the leased declaration while preserving its authored value. */
321
400
  write(element, value, priority = "") {
322
- const existing = this.#records.get(element);
323
- if (existing) {
324
- existing.writtenValue = value;
325
- existing.writtenPriority = value === null ? "" : priority;
326
- } else {
327
- this.#records.set(element, {
328
- originalValue: element.style.getPropertyValue(this.#property),
329
- originalPriority: element.style.getPropertyPriority(this.#property),
330
- writtenValue: value,
331
- writtenPriority: value === null ? "" : priority
332
- });
333
- }
334
- this.#reflect(element, value, priority);
401
+ const stack = this.#stack(element);
402
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
403
+ this.#held.add(element);
404
+ const shows = !this.#covered(element, stack, hold);
405
+ hold.writtenValue = value;
406
+ hold.writtenPriority = value === null ? "" : priority;
407
+ this.#mark(element, stack);
408
+ if (shows) this.#show(element, value, hold.writtenPriority);
335
409
  }
336
- /** Returns one lease without overwriting a later consumer declaration. */
337
410
  return(element) {
338
- const record = this.#records.get(element);
339
- if (!record) return;
340
- this.#records.delete(element);
341
- const style = element.style;
342
- const stillOwned = style.getPropertyValue(this.#property) === (record.writtenValue ?? "") && style.getPropertyPriority(this.#property) === record.writtenPriority;
343
- if (stillOwned) {
344
- this.#reflect(element, record.originalValue, record.originalPriority);
345
- }
411
+ const held = this.#held.delete(element);
412
+ const known = _StylePropertyLease.#registry.stacks.get(element)?.has(this.#property);
413
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
414
+ const hold = this.#take(stack);
415
+ if (!hold) return;
416
+ const owns = this.#carries(element, hold) && !this.#covered(element, stack, hold);
417
+ const index = stack.indexOf(hold);
418
+ stack.splice(index, 1);
419
+ const above = stack[index];
420
+ if (above) above.linked &&= hold.linked;
421
+ this.#mark(element, stack);
422
+ if (!owns) return;
423
+ const below = hold.linked ? stack[index - 1] : void 0;
424
+ if (below) this.#show(element, below.writtenValue, below.writtenPriority);
425
+ else this.#show(element, hold.originalValue, hold.originalPriority);
346
426
  }
347
- /** Returns every outstanding declaration lease. */
348
427
  returnAll() {
349
- for (const element of Array.from(this.#records.keys())) this.return(element);
428
+ for (const element of Array.from(this.#held)) this.return(element);
429
+ }
430
+ #recorded(element, name) {
431
+ try {
432
+ const parsed = JSON.parse(element.getAttribute(name) ?? "null");
433
+ if (!Array.isArray(parsed)) return null;
434
+ const [originalValue, originalPriority, writtenValue, writtenPriority, ...rest] = parsed;
435
+ if (typeof originalValue !== "string" || typeof originalPriority !== "string") return null;
436
+ if (typeof writtenValue !== "string" && writtenValue !== null) return null;
437
+ if (typeof writtenPriority !== "string") return null;
438
+ const linked = rest[0] !== false;
439
+ const beneath = linked ? rest : rest.slice(1);
440
+ if (!beneath.every((under) => typeof under === "string")) return null;
441
+ return { originalValue, originalPriority, writtenValue, writtenPriority, linked, beneath };
442
+ } catch {
443
+ return null;
444
+ }
350
445
  }
351
- /** Reflects only a real declaration transition. */
352
- #reflect(element, value, priority) {
446
+ #stack(element) {
447
+ const stacks = _StylePropertyLease.#registry.stacks;
448
+ const byProperty = stacks.get(element) ?? /* @__PURE__ */ new Map();
449
+ stacks.set(element, byProperty);
450
+ const stack = byProperty.get(this.#property) ?? this.#seed(element);
451
+ byProperty.set(this.#property, stack);
452
+ return stack;
453
+ }
454
+ #seed(element) {
455
+ const found = [];
456
+ for (const record of _StylePropertyLease.#registry.names.get(this.#property) ?? []) {
457
+ const read = this.#recorded(element, record);
458
+ if (read) found.push({ ...read, record });
459
+ }
460
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
461
+ return found.sort((one, other) => depth(one) - depth(other)).map((entry, index) => ({
462
+ lease: null,
463
+ record: entry.record,
464
+ writtenValue: entry.writtenValue,
465
+ writtenPriority: entry.writtenPriority,
466
+ originalValue: entry.originalValue,
467
+ originalPriority: entry.originalPriority,
468
+ linked: entry.linked && index > 0
469
+ }));
470
+ }
471
+ #take(stack) {
472
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
473
+ if (hold) hold.lease = this;
474
+ return hold;
475
+ }
476
+ #acquire(element, stack) {
477
+ const top = stack.at(-1);
478
+ const over = top && this.#carries(element, top) ? top : void 0;
353
479
  const style = element.style;
354
- const nextValue = value ?? "";
355
- const nextPriority = value === null ? "" : priority;
356
- if (style.getPropertyValue(this.#property) === nextValue && style.getPropertyPriority(this.#property) === nextPriority) {
357
- return;
480
+ const hold = {
481
+ lease: this,
482
+ record: this.#record,
483
+ writtenValue: null,
484
+ writtenPriority: "",
485
+ originalValue: over ? over.originalValue : style.getPropertyValue(this.#property),
486
+ originalPriority: over ? over.originalPriority : style.getPropertyPriority(this.#property),
487
+ linked: over !== void 0
488
+ };
489
+ stack.push(hold);
490
+ return hold;
491
+ }
492
+ #covered(element, stack, hold) {
493
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => this.#carries(element, above));
494
+ }
495
+ #mark(element, stack) {
496
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
497
+ stack.forEach((hold, index) => {
498
+ const names = stack.slice(0, index).map((under) => under.record);
499
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
500
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
501
+ const { originalValue, originalPriority, writtenValue, writtenPriority } = hold;
502
+ records.set(
503
+ hold.record,
504
+ (writtenValue ?? "") === originalValue && writtenPriority === originalPriority && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([
505
+ originalValue,
506
+ originalPriority,
507
+ writtenValue,
508
+ writtenPriority,
509
+ ...marker,
510
+ ...beneath
511
+ ])
512
+ );
513
+ });
514
+ for (const [name, recorded] of records) {
515
+ if (element.getAttribute(name) === recorded) continue;
516
+ if (recorded === null) element.removeAttribute(name);
517
+ else element.setAttribute(name, recorded);
358
518
  }
359
- if (value === null) style.removeProperty(this.#property);
360
- else style.setProperty(this.#property, value, priority);
519
+ }
520
+ #carries(element, hold) {
521
+ return this.#holds(element, hold.writtenValue ?? "", hold.writtenPriority);
522
+ }
523
+ #holds(element, value, priority) {
524
+ const style = element.style;
525
+ return style.getPropertyValue(this.#property) === value && style.getPropertyPriority(this.#property) === priority;
526
+ }
527
+ #show(element, value, priority) {
528
+ if (this.#holds(element, value ?? "", priority)) return;
529
+ if (value === null) element.style.removeProperty(this.#property);
530
+ else element.style.setProperty(this.#property, value, priority);
361
531
  }
362
532
  };
363
533
 
364
- // src/utils/tabindex_loan.ts
365
- var TabindexLoan = class {
534
+ function queryOpenDescendants(root, selector) {
535
+ const matches = [];
536
+ const roots = [root];
537
+ for (const scope of roots) {
538
+ matches.push(...scope.querySelectorAll(selector));
539
+ if (scope.nodeType === Node.ELEMENT_NODE) {
540
+ const shadow = scope.shadowRoot;
541
+ if (shadow) roots.push(shadow);
542
+ }
543
+ for (const element of scope.querySelectorAll("*")) {
544
+ if (element.shadowRoot) roots.push(element.shadowRoot);
545
+ }
546
+ }
547
+ return matches;
548
+ }
549
+
550
+ var TabindexLoan = class _TabindexLoan {
551
+ static #registry = sharedRegistry(
552
+ "stimeo-ui.tabindex-loan.registry.v1",
553
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
554
+ );
366
555
  #value;
556
+ #record;
367
557
  #lent = /* @__PURE__ */ new Set();
368
- /** Returns live loans before Turbo can copy them into its page snapshot. */
369
- #beforeCache = new BeforeCacheReset(() => this.returnAll());
370
- /**
371
- * @param value - the `tabindex` to lend. `"-1"` (the default) is
372
- * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
373
- * which a scroll region with no focusable content of its own needs.
374
- */
375
- constructor(value = "-1") {
558
+ constructor(value, owner) {
376
559
  this.#value = value;
560
+ this.#record = `data-${owner}-tabindex-loan`;
561
+ _TabindexLoan.#registry.names.add(this.#record);
377
562
  }
378
- /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
379
563
  lend(element) {
380
- if (element.hasAttribute("tabindex")) return;
381
- element.setAttribute("tabindex", this.#value);
564
+ const stack = this.#stack(element);
565
+ const current = element.getAttribute("tabindex");
566
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
567
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
568
+ );
569
+ const linked = stack.at(-1)?.value === current;
570
+ if (!held && current !== null && !linked) return;
382
571
  this.#lent.add(element);
383
- this.#beforeCache.activate();
572
+ if (held) held.loan = this;
573
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
574
+ this.#mark(element, stack);
575
+ if (current === null || !held && current !== this.#value) {
576
+ element.setAttribute("tabindex", this.#value);
577
+ }
384
578
  }
385
- /** Takes back every loan whose value is still the one that was lent. */
386
579
  returnAll() {
387
580
  for (const element of this.#lent) {
388
- if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
581
+ const stack = this.#stack(element);
582
+ const hold = stack.find((held) => held.loan === this);
583
+ if (hold) this.#release(element, stack, hold);
389
584
  }
390
585
  this.#lent.clear();
391
- this.#beforeCache.deactivate();
586
+ }
587
+ reclaim(element) {
588
+ if (element.getAttribute(this.#record) !== this.#value) return;
589
+ const stack = this.#stack(element);
590
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
591
+ if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
592
+ }
593
+ reclaimWithin(root) {
594
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
595
+ this.reclaim(element);
596
+ }
597
+ }
598
+ #stack(element) {
599
+ const stacks = _TabindexLoan.#registry.stacks;
600
+ const stack = stacks.get(element) ?? this.#seed(element);
601
+ stacks.set(element, stack);
602
+ return stack;
603
+ }
604
+ #seed(element) {
605
+ const current = element.getAttribute("tabindex");
606
+ const holds = [];
607
+ for (const record of _TabindexLoan.#registry.names) {
608
+ const value = element.getAttribute(record);
609
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
610
+ }
611
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
612
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
613
+ return holds;
614
+ }
615
+ #release(element, stack, hold) {
616
+ const current = element.getAttribute("tabindex");
617
+ const index = stack.indexOf(hold);
618
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
619
+ stack.splice(index, 1);
620
+ const above = stack[index];
621
+ if (above) above.linked &&= hold.linked;
622
+ this.#mark(element, stack);
623
+ if (!owns) return;
624
+ const below = hold.linked ? stack[index - 1] : void 0;
625
+ if (!below) element.removeAttribute("tabindex");
626
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
627
+ }
628
+ #mark(element, stack) {
629
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
630
+ for (const hold of stack) records.set(hold.record, hold.value);
631
+ for (const [name, recorded] of records) {
632
+ if (element.getAttribute(name) === recorded) continue;
633
+ if (recorded === null) element.removeAttribute(name);
634
+ else element.setAttribute(name, recorded);
635
+ }
392
636
  }
393
637
  };
394
638
 
395
- // src/controllers/scroll_area_controller.ts
396
639
  var EDGE_EPSILON = 1;
397
640
  var ScrollAreaController = class extends Controller {
398
641
  static targets = ["viewport"];
@@ -401,13 +644,15 @@ var ScrollAreaController = class extends Controller {
401
644
  };
402
645
  static events = ["reach"];
403
646
  #layout = new LayoutObserver(() => this.#refresh());
404
- #rebind = new MicrotaskCoalescer(() => this.#syncViewport());
405
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
406
- #tabindex = new TabindexLoan("0");
407
- #role = new AttributeLease("role");
408
- #overflowState = new AttributeLease("data-overflow");
409
- #scrollState = new AttributeLease("data-scroll");
410
- #progress = new StylePropertyLease("--stimeo--scroll-progress");
647
+ #rebind = new MorphRenderWatcher(() => this.#syncViewport());
648
+ #tabindex = new TabindexLoan("0", this.identifier);
649
+ #role = new AttributeLease("role", this.identifier);
650
+ #overflowState = new AttributeLease("data-overflow", this.identifier);
651
+ #scrollState = new AttributeLease("data-scroll", this.identifier);
652
+ #progress = new StylePropertyLease(
653
+ "--stimeo--scroll-progress",
654
+ this.identifier
655
+ );
411
656
  #viewport = null;
412
657
  #content = null;
413
658
  #nameSources = null;
@@ -415,7 +660,6 @@ var ScrollAreaController = class extends Controller {
415
660
  #observedNameIds = /* @__PURE__ */ new Set();
416
661
  #observedNameSources = [];
417
662
  #fonts = null;
418
- /** Coalesces scroll bursts into one position sync per frame. */
419
663
  #frames = new FrameCoalescer();
420
664
  #overflowing = false;
421
665
  #lastEdge = null;
@@ -428,39 +672,31 @@ var ScrollAreaController = class extends Controller {
428
672
  this.#refresh();
429
673
  };
430
674
  connect() {
431
- this.#beforeCache.activate();
432
- this.#rebind.activate();
675
+ this.#rebind.observe(this.element);
433
676
  this.#layout.observeViewport();
434
677
  this.#bindFonts();
435
678
  this.#syncViewport();
436
679
  }
437
680
  disconnect() {
438
- this.#beforeCache.deactivate();
439
- this.#rebind.cancel();
440
- this.#frames.cancel();
681
+ this.#rebind.disconnect();
441
682
  if (this.#viewport) this.#unbindViewport(this.#viewport);
442
683
  this.#layout.disconnect();
443
684
  this.#unbindFonts();
444
685
  this.#clearHostState();
445
686
  }
446
- /** Re-measures when a retained viewport's orientation Value is morphed. */
447
687
  orientationValueChanged() {
448
688
  this.#refresh();
449
689
  }
450
- /** Schedules a complete resource rebind for a runtime viewport target. */
451
690
  viewportTargetConnected() {
452
691
  this.#rebind.schedule();
453
692
  }
454
- /** Schedules cleanup or replacement binding after a viewport leaves. */
455
693
  viewportTargetDisconnected() {
456
694
  this.#rebind.schedule();
457
695
  }
458
- /** Rebinds every viewport-owned resource against the final target in the mutation batch. */
459
696
  #syncViewport() {
460
697
  const next = this.hasViewportTarget ? this.viewportTarget : null;
461
698
  if (next === this.#viewport) {
462
699
  if (next) this.#refresh();
463
- else this.#clearHostState();
464
700
  return;
465
701
  }
466
702
  if (this.#viewport) this.#unbindViewport(this.#viewport);
@@ -475,7 +711,6 @@ var ScrollAreaController = class extends Controller {
475
711
  this.#bindContentObserver(next);
476
712
  this.#refresh();
477
713
  }
478
- /** Observes structural and attribute changes that can alter overflow or Tab stops. */
479
714
  #bindContentObserver(viewport) {
480
715
  this.#content = new MutationObserver((records) => {
481
716
  if (this.#viewport !== viewport || !this.#hasRelevantContentMutation(records)) return;
@@ -488,7 +723,6 @@ var ScrollAreaController = class extends Controller {
488
723
  characterData: true
489
724
  });
490
725
  }
491
- /** Ignores only state-hook mutations whose exact value this controller just wrote. */
492
726
  #hasRelevantContentMutation(records) {
493
727
  let relevant = false;
494
728
  const consumed = /* @__PURE__ */ new Set();
@@ -503,7 +737,6 @@ var ScrollAreaController = class extends Controller {
503
737
  for (const attribute of consumed) this.#ownedHostMutations.delete(attribute);
504
738
  return relevant;
505
739
  }
506
- /** Releases every resource and borrowed attribute owned by one former viewport. */
507
740
  #unbindViewport(viewport) {
508
741
  this.#frames.cancel();
509
742
  viewport.removeEventListener("scroll", this.#onScroll);
@@ -518,7 +751,6 @@ var ScrollAreaController = class extends Controller {
518
751
  this.#overflowing = false;
519
752
  this.#lastEdge = null;
520
753
  }
521
- /** Runs the full structural and positional measurement pass. */
522
754
  #refresh() {
523
755
  const viewport = this.#viewport;
524
756
  if (!viewport) return;
@@ -528,13 +760,11 @@ var ScrollAreaController = class extends Controller {
528
760
  this.#syncKeyboardReach(viewport, this.#overflowing);
529
761
  this.#syncPosition(viewport, this.#overflowing);
530
762
  }
531
- /** Measures overflow and reflects the state hook without identical DOM writes. */
532
763
  #syncOverflow(viewport) {
533
764
  const overflowing = this.#measureOverflow(viewport);
534
765
  this.#writeHostAttribute(this.#overflowState, "data-overflow", overflowing ? "true" : "false");
535
766
  return overflowing;
536
767
  }
537
- /** Measures the primary-axis position and dispatches a newly established edge. */
538
768
  #syncPosition(viewport, overflowing) {
539
769
  const { position, progress } = this.#measurePosition(viewport);
540
770
  this.#writeHostAttribute(this.#scrollState, "data-scroll", position);
@@ -544,41 +774,40 @@ var ScrollAreaController = class extends Controller {
544
774
  this.#lastEdge = edge;
545
775
  if (edge) this.dispatch("reach", { detail: { edge } });
546
776
  }
547
- /** Writes one leased host attribute and records self-generated observer input. */
548
777
  #writeHostAttribute(lease, attribute, value) {
778
+ const record = `data-${this.identifier}-${attribute}-lease`;
549
779
  const before = this.element.getAttribute(attribute);
780
+ const recordBefore = this.element.getAttribute(record);
550
781
  lease.write(this.element, value);
551
782
  this.#recordOwnedHostMutation(attribute, before);
783
+ this.#recordOwnedHostMutation(record, recordBefore);
552
784
  }
553
- /** Writes the leased progress property and records its serialized style mutation. */
554
785
  #writeHostProgress(value) {
786
+ const record = `data-${this.identifier}-style---stimeo--scroll-progress-lease`;
555
787
  const before = this.element.getAttribute("style");
788
+ const recordBefore = this.element.getAttribute(record);
556
789
  this.#progress.write(this.element, value);
557
790
  this.#recordOwnedHostMutation("style", before);
791
+ this.#recordOwnedHostMutation(record, recordBefore);
558
792
  }
559
- /** Records an exact host mutation only when the host is also the observed viewport. */
560
793
  #recordOwnedHostMutation(attribute, before) {
561
794
  if (this.#viewport !== this.element) return;
562
795
  const after = this.element.getAttribute(attribute);
563
796
  if (after !== before) this.#ownedHostMutations.set(attribute, after);
564
797
  }
565
- /** Returns host hooks to authored values and clears edge bookkeeping. */
566
798
  #clearHostState() {
567
799
  this.#overflowState.returnAll();
568
800
  this.#scrollState.returnAll();
569
801
  this.#progress.returnAll();
570
- this.#ownedHostMutations.clear();
571
802
  this.#overflowing = false;
572
803
  this.#lastEdge = null;
573
804
  }
574
- /** Whether the configured axis currently has a scroll range. */
575
805
  #measureOverflow(viewport) {
576
806
  const orientation = this.orientationValue;
577
807
  const vertical = orientation !== "horizontal" && viewport.scrollHeight > viewport.clientHeight + EDGE_EPSILON;
578
808
  const horizontal = orientation !== "vertical" && viewport.scrollWidth > viewport.clientWidth + EDGE_EPSILON;
579
809
  return vertical || horizontal;
580
810
  }
581
- /** Reports the primary-axis position bucket and normalized 0–1 progress. */
582
811
  #measurePosition(viewport) {
583
812
  const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && viewport.scrollHeight <= viewport.clientHeight + EDGE_EPSILON;
584
813
  const { position: scrollPosition, max: maxScroll } = logicalScrollMetrics(
@@ -591,7 +820,6 @@ var ScrollAreaController = class extends Controller {
591
820
  if (scrollPosition >= maxScroll - EDGE_EPSILON) return { position: "end", progress };
592
821
  return { position: "middle", progress };
593
822
  }
594
- /** Borrows keyboard reach only when no usable descendant already owns a Tab stop. */
595
823
  #syncKeyboardReach(viewport, overflowing) {
596
824
  const wantsTabindex = overflowing && !hasTabStop(viewport);
597
825
  if (!wantsTabindex) {
@@ -602,15 +830,17 @@ var ScrollAreaController = class extends Controller {
602
830
  if (this.#hasAccessibleName(viewport)) {
603
831
  if (!viewport.hasAttribute("role")) this.#role.write(viewport, "region");
604
832
  } else {
605
- this.#role.return(viewport);
833
+ this.#returnRole(viewport);
606
834
  }
607
835
  }
608
- /** Returns only the viewport attributes borrowed by this controller. */
609
836
  #clearViewportAttributes(viewport) {
610
837
  this.#tabindex.returnAll();
838
+ this.#tabindex.reclaim(viewport);
839
+ this.#returnRole(viewport);
840
+ }
841
+ #returnRole(viewport) {
611
842
  this.#role.return(viewport);
612
843
  }
613
- /** Whether the viewport already has a non-empty accessible name. */
614
844
  #hasAccessibleName(viewport) {
615
845
  const sources = this.#resolveNameSources(viewport);
616
846
  if (sources.length > 0) {
@@ -621,21 +851,15 @@ var ScrollAreaController = class extends Controller {
621
851
  }
622
852
  return Boolean(viewport.getAttribute("aria-label")?.trim());
623
853
  }
624
- /** Resolves unique, live `aria-labelledby` references in authored order. */
625
854
  #resolveNameSources(viewport) {
626
855
  const ids = this.#nameReferenceIds(viewport);
627
856
  const sources = [];
628
- const seen = /* @__PURE__ */ new Set();
629
857
  for (const id of ids) {
630
858
  const source = viewport.ownerDocument.getElementById(id);
631
- if (source && !seen.has(source)) {
632
- seen.add(source);
633
- sources.push(source);
634
- }
859
+ if (source) sources.push(source);
635
860
  }
636
861
  return sources;
637
862
  }
638
- /** Observes external name sources so a retained region never becomes unnamed. */
639
863
  #syncNameSources(viewport) {
640
864
  const ids = this.#nameReferenceIds(viewport);
641
865
  const sources = this.#resolveNameSources(viewport).filter(
@@ -677,12 +901,10 @@ var ScrollAreaController = class extends Controller {
677
901
  });
678
902
  }
679
903
  }
680
- /** Returns normalized unique label-reference ids in authored order. */
681
904
  #nameReferenceIds(viewport) {
682
905
  const tokens = viewport.getAttribute("aria-labelledby")?.trim().split(/\s+/).filter(Boolean);
683
906
  return Array.from(new Set(tokens));
684
907
  }
685
- /** Whether document-graph changes can resolve or invalidate a referenced id. */
686
908
  #hasRelevantNameGraphMutation(records) {
687
909
  for (const record of records) {
688
910
  if (record.type === "attributes") {
@@ -698,7 +920,6 @@ var ScrollAreaController = class extends Controller {
698
920
  }
699
921
  return false;
700
922
  }
701
- /** Whether a changed subtree contains one of the current label-reference ids. */
702
923
  #nodeContainsObservedNameId(node) {
703
924
  if (!(node instanceof Element)) return false;
704
925
  if (this.#observedNameIds.has(node.id)) return true;
@@ -706,7 +927,6 @@ var ScrollAreaController = class extends Controller {
706
927
  (element) => this.#observedNameIds.has(element.id)
707
928
  );
708
929
  }
709
- /** Stops observing accessible-name sources. */
710
930
  #disconnectNameSources() {
711
931
  this.#nameSources?.disconnect();
712
932
  this.#nameSources = null;
@@ -715,32 +935,19 @@ var ScrollAreaController = class extends Controller {
715
935
  this.#observedNameIds.clear();
716
936
  this.#observedNameSources = [];
717
937
  }
718
- /** Subscribes to font completion when the current engine exposes `document.fonts`. */
719
938
  #bindFonts() {
720
939
  const fonts = this.element.ownerDocument.fonts;
721
940
  if (!fonts || this.#fonts === fonts) return;
722
- this.#unbindFonts();
723
941
  this.#fonts = fonts;
724
942
  fonts.addEventListener("loadingdone", this.#onFontsSettled);
725
943
  fonts.addEventListener("loadingerror", this.#onFontsSettled);
726
944
  }
727
- /** Removes the font completion subscriptions. */
728
945
  #unbindFonts() {
729
946
  this.#fonts?.removeEventListener("loadingdone", this.#onFontsSettled);
730
947
  this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
731
948
  this.#fonts = null;
732
949
  }
733
- /** Suspends live resources and returns all derived state before snapshotting. */
734
- #rewindForCache() {
735
- this.#rebind.cancel();
736
- this.#frames.cancel();
737
- if (this.#viewport) this.#unbindViewport(this.#viewport);
738
- this.#layout.disconnect();
739
- this.#unbindFonts();
740
- this.#clearHostState();
741
- }
742
950
  };
743
951
 
744
952
  export { ScrollAreaController };
745
- //# sourceMappingURL=scroll_area_controller.js.map
746
953
  //# sourceMappingURL=scroll_area_controller.js.map