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,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,11 +751,6 @@ var ScrollAreaController = class extends Controller {
518
751
  this.#overflowing = false;
519
752
  this.#lastEdge = null;
520
753
  }
521
- /**
522
- * Runs the full structural and positional measurement pass.
523
- *
524
- * @stimeoRenderRoot
525
- */
526
754
  #refresh() {
527
755
  const viewport = this.#viewport;
528
756
  if (!viewport) return;
@@ -532,13 +760,11 @@ var ScrollAreaController = class extends Controller {
532
760
  this.#syncKeyboardReach(viewport, this.#overflowing);
533
761
  this.#syncPosition(viewport, this.#overflowing);
534
762
  }
535
- /** Measures overflow and reflects the state hook without identical DOM writes. */
536
763
  #syncOverflow(viewport) {
537
764
  const overflowing = this.#measureOverflow(viewport);
538
765
  this.#writeHostAttribute(this.#overflowState, "data-overflow", overflowing ? "true" : "false");
539
766
  return overflowing;
540
767
  }
541
- /** Measures the primary-axis position and dispatches a newly established edge. */
542
768
  #syncPosition(viewport, overflowing) {
543
769
  const { position, progress } = this.#measurePosition(viewport);
544
770
  this.#writeHostAttribute(this.#scrollState, "data-scroll", position);
@@ -548,41 +774,40 @@ var ScrollAreaController = class extends Controller {
548
774
  this.#lastEdge = edge;
549
775
  if (edge) this.dispatch("reach", { detail: { edge } });
550
776
  }
551
- /** Writes one leased host attribute and records self-generated observer input. */
552
777
  #writeHostAttribute(lease, attribute, value) {
778
+ const record = `data-${this.identifier}-${attribute}-lease`;
553
779
  const before = this.element.getAttribute(attribute);
780
+ const recordBefore = this.element.getAttribute(record);
554
781
  lease.write(this.element, value);
555
782
  this.#recordOwnedHostMutation(attribute, before);
783
+ this.#recordOwnedHostMutation(record, recordBefore);
556
784
  }
557
- /** Writes the leased progress property and records its serialized style mutation. */
558
785
  #writeHostProgress(value) {
786
+ const record = `data-${this.identifier}-style---stimeo--scroll-progress-lease`;
559
787
  const before = this.element.getAttribute("style");
788
+ const recordBefore = this.element.getAttribute(record);
560
789
  this.#progress.write(this.element, value);
561
790
  this.#recordOwnedHostMutation("style", before);
791
+ this.#recordOwnedHostMutation(record, recordBefore);
562
792
  }
563
- /** Records an exact host mutation only when the host is also the observed viewport. */
564
793
  #recordOwnedHostMutation(attribute, before) {
565
794
  if (this.#viewport !== this.element) return;
566
795
  const after = this.element.getAttribute(attribute);
567
796
  if (after !== before) this.#ownedHostMutations.set(attribute, after);
568
797
  }
569
- /** Returns host hooks to authored values and clears edge bookkeeping. */
570
798
  #clearHostState() {
571
799
  this.#overflowState.returnAll();
572
800
  this.#scrollState.returnAll();
573
801
  this.#progress.returnAll();
574
- this.#ownedHostMutations.clear();
575
802
  this.#overflowing = false;
576
803
  this.#lastEdge = null;
577
804
  }
578
- /** Whether the configured axis currently has a scroll range. */
579
805
  #measureOverflow(viewport) {
580
806
  const orientation = this.orientationValue;
581
807
  const vertical = orientation !== "horizontal" && viewport.scrollHeight > viewport.clientHeight + EDGE_EPSILON;
582
808
  const horizontal = orientation !== "vertical" && viewport.scrollWidth > viewport.clientWidth + EDGE_EPSILON;
583
809
  return vertical || horizontal;
584
810
  }
585
- /** Reports the primary-axis position bucket and normalized 0–1 progress. */
586
811
  #measurePosition(viewport) {
587
812
  const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && viewport.scrollHeight <= viewport.clientHeight + EDGE_EPSILON;
588
813
  const { position: scrollPosition, max: maxScroll } = logicalScrollMetrics(
@@ -595,7 +820,6 @@ var ScrollAreaController = class extends Controller {
595
820
  if (scrollPosition >= maxScroll - EDGE_EPSILON) return { position: "end", progress };
596
821
  return { position: "middle", progress };
597
822
  }
598
- /** Borrows keyboard reach only when no usable descendant already owns a Tab stop. */
599
823
  #syncKeyboardReach(viewport, overflowing) {
600
824
  const wantsTabindex = overflowing && !hasTabStop(viewport);
601
825
  if (!wantsTabindex) {
@@ -606,15 +830,17 @@ var ScrollAreaController = class extends Controller {
606
830
  if (this.#hasAccessibleName(viewport)) {
607
831
  if (!viewport.hasAttribute("role")) this.#role.write(viewport, "region");
608
832
  } else {
609
- this.#role.return(viewport);
833
+ this.#returnRole(viewport);
610
834
  }
611
835
  }
612
- /** Returns only the viewport attributes borrowed by this controller. */
613
836
  #clearViewportAttributes(viewport) {
614
837
  this.#tabindex.returnAll();
838
+ this.#tabindex.reclaim(viewport);
839
+ this.#returnRole(viewport);
840
+ }
841
+ #returnRole(viewport) {
615
842
  this.#role.return(viewport);
616
843
  }
617
- /** Whether the viewport already has a non-empty accessible name. */
618
844
  #hasAccessibleName(viewport) {
619
845
  const sources = this.#resolveNameSources(viewport);
620
846
  if (sources.length > 0) {
@@ -625,21 +851,15 @@ var ScrollAreaController = class extends Controller {
625
851
  }
626
852
  return Boolean(viewport.getAttribute("aria-label")?.trim());
627
853
  }
628
- /** Resolves unique, live `aria-labelledby` references in authored order. */
629
854
  #resolveNameSources(viewport) {
630
855
  const ids = this.#nameReferenceIds(viewport);
631
856
  const sources = [];
632
- const seen = /* @__PURE__ */ new Set();
633
857
  for (const id of ids) {
634
858
  const source = viewport.ownerDocument.getElementById(id);
635
- if (source && !seen.has(source)) {
636
- seen.add(source);
637
- sources.push(source);
638
- }
859
+ if (source) sources.push(source);
639
860
  }
640
861
  return sources;
641
862
  }
642
- /** Observes external name sources so a retained region never becomes unnamed. */
643
863
  #syncNameSources(viewport) {
644
864
  const ids = this.#nameReferenceIds(viewport);
645
865
  const sources = this.#resolveNameSources(viewport).filter(
@@ -681,12 +901,10 @@ var ScrollAreaController = class extends Controller {
681
901
  });
682
902
  }
683
903
  }
684
- /** Returns normalized unique label-reference ids in authored order. */
685
904
  #nameReferenceIds(viewport) {
686
905
  const tokens = viewport.getAttribute("aria-labelledby")?.trim().split(/\s+/).filter(Boolean);
687
906
  return Array.from(new Set(tokens));
688
907
  }
689
- /** Whether document-graph changes can resolve or invalidate a referenced id. */
690
908
  #hasRelevantNameGraphMutation(records) {
691
909
  for (const record of records) {
692
910
  if (record.type === "attributes") {
@@ -702,7 +920,6 @@ var ScrollAreaController = class extends Controller {
702
920
  }
703
921
  return false;
704
922
  }
705
- /** Whether a changed subtree contains one of the current label-reference ids. */
706
923
  #nodeContainsObservedNameId(node) {
707
924
  if (!(node instanceof Element)) return false;
708
925
  if (this.#observedNameIds.has(node.id)) return true;
@@ -710,7 +927,6 @@ var ScrollAreaController = class extends Controller {
710
927
  (element) => this.#observedNameIds.has(element.id)
711
928
  );
712
929
  }
713
- /** Stops observing accessible-name sources. */
714
930
  #disconnectNameSources() {
715
931
  this.#nameSources?.disconnect();
716
932
  this.#nameSources = null;
@@ -719,32 +935,19 @@ var ScrollAreaController = class extends Controller {
719
935
  this.#observedNameIds.clear();
720
936
  this.#observedNameSources = [];
721
937
  }
722
- /** Subscribes to font completion when the current engine exposes `document.fonts`. */
723
938
  #bindFonts() {
724
939
  const fonts = this.element.ownerDocument.fonts;
725
940
  if (!fonts || this.#fonts === fonts) return;
726
- this.#unbindFonts();
727
941
  this.#fonts = fonts;
728
942
  fonts.addEventListener("loadingdone", this.#onFontsSettled);
729
943
  fonts.addEventListener("loadingerror", this.#onFontsSettled);
730
944
  }
731
- /** Removes the font completion subscriptions. */
732
945
  #unbindFonts() {
733
946
  this.#fonts?.removeEventListener("loadingdone", this.#onFontsSettled);
734
947
  this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
735
948
  this.#fonts = null;
736
949
  }
737
- /** Suspends live resources and returns all derived state before snapshotting. */
738
- #rewindForCache() {
739
- this.#rebind.cancel();
740
- this.#frames.cancel();
741
- if (this.#viewport) this.#unbindViewport(this.#viewport);
742
- this.#layout.disconnect();
743
- this.#unbindFonts();
744
- this.#clearHostState();
745
- }
746
950
  };
747
951
 
748
952
  export { ScrollAreaController };
749
- //# sourceMappingURL=scroll_area_controller.js.map
750
953
  //# sourceMappingURL=scroll_area_controller.js.map