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