stimeo-ui 0.5.0 → 0.7.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 (230) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
  6. package/dist/controllers/aspect_ratio_controller.js +19 -11
  7. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  8. package/dist/controllers/auto_submit_controller.d.ts +2 -0
  9. package/dist/controllers/auto_submit_controller.js.map +1 -1
  10. package/dist/controllers/avatar_controller.d.ts +36 -15
  11. package/dist/controllers/avatar_controller.js +237 -40
  12. package/dist/controllers/avatar_controller.js.map +1 -1
  13. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  14. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  15. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  16. package/dist/controllers/bulk_select_controller.js.map +1 -1
  17. package/dist/controllers/calendar_controller.d.ts +2 -0
  18. package/dist/controllers/calendar_controller.js.map +1 -1
  19. package/dist/controllers/carousel_controller.d.ts +12 -3
  20. package/dist/controllers/carousel_controller.js +85 -9
  21. package/dist/controllers/carousel_controller.js.map +1 -1
  22. package/dist/controllers/character_counter_controller.d.ts +52 -20
  23. package/dist/controllers/character_counter_controller.js +338 -63
  24. package/dist/controllers/character_counter_controller.js.map +1 -1
  25. package/dist/controllers/checkbox_controller.d.ts +34 -5
  26. package/dist/controllers/checkbox_controller.js +136 -25
  27. package/dist/controllers/checkbox_controller.js.map +1 -1
  28. package/dist/controllers/color_picker_controller.d.ts +10 -1
  29. package/dist/controllers/color_picker_controller.js +35 -9
  30. package/dist/controllers/color_picker_controller.js.map +1 -1
  31. package/dist/controllers/combobox_controller.d.ts +2 -0
  32. package/dist/controllers/combobox_controller.js.map +1 -1
  33. package/dist/controllers/command_palette_controller.d.ts +2 -0
  34. package/dist/controllers/command_palette_controller.js +75 -4
  35. package/dist/controllers/command_palette_controller.js.map +1 -1
  36. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  37. package/dist/controllers/conditional_fields_controller.js +345 -51
  38. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  39. package/dist/controllers/confirm_controller.d.ts +2 -0
  40. package/dist/controllers/confirm_controller.js +75 -4
  41. package/dist/controllers/confirm_controller.js.map +1 -1
  42. package/dist/controllers/count_up_controller.d.ts +2 -0
  43. package/dist/controllers/count_up_controller.js.map +1 -1
  44. package/dist/controllers/countdown_controller.d.ts +4 -0
  45. package/dist/controllers/countdown_controller.js.map +1 -1
  46. package/dist/controllers/currency_input_controller.d.ts +2 -0
  47. package/dist/controllers/currency_input_controller.js.map +1 -1
  48. package/dist/controllers/data_grid_controller.d.ts +3 -0
  49. package/dist/controllers/data_grid_controller.js.map +1 -1
  50. package/dist/controllers/date_range_picker_controller.d.ts +23 -4
  51. package/dist/controllers/date_range_picker_controller.js +157 -30
  52. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  53. package/dist/controllers/dialog_controller.js +75 -4
  54. package/dist/controllers/dialog_controller.js.map +1 -1
  55. package/dist/controllers/direct_upload_controller.d.ts +56 -24
  56. package/dist/controllers/direct_upload_controller.js +201 -45
  57. package/dist/controllers/direct_upload_controller.js.map +1 -1
  58. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  59. package/dist/controllers/dirty_form_controller.js +192 -29
  60. package/dist/controllers/dirty_form_controller.js.map +1 -1
  61. package/dist/controllers/dismissible_controller.d.ts +2 -0
  62. package/dist/controllers/dismissible_controller.js +83 -18
  63. package/dist/controllers/dismissible_controller.js.map +1 -1
  64. package/dist/controllers/drawer_controller.js +75 -4
  65. package/dist/controllers/drawer_controller.js.map +1 -1
  66. package/dist/controllers/empty_state_controller.d.ts +2 -0
  67. package/dist/controllers/empty_state_controller.js.map +1 -1
  68. package/dist/controllers/file_dropzone_controller.d.ts +9 -1
  69. package/dist/controllers/file_dropzone_controller.js +26 -3
  70. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  71. package/dist/controllers/filter_controller.d.ts +2 -0
  72. package/dist/controllers/filter_controller.js.map +1 -1
  73. package/dist/controllers/flash_controller.d.ts +4 -0
  74. package/dist/controllers/flash_controller.js.map +1 -1
  75. package/dist/controllers/focus_controller.d.ts +4 -3
  76. package/dist/controllers/focus_controller.js +75 -4
  77. package/dist/controllers/focus_controller.js.map +1 -1
  78. package/dist/controllers/form_field_controller.d.ts +50 -10
  79. package/dist/controllers/form_field_controller.js +280 -62
  80. package/dist/controllers/form_field_controller.js.map +1 -1
  81. package/dist/controllers/form_validation_controller.d.ts +10 -8
  82. package/dist/controllers/form_validation_controller.js +208 -83
  83. package/dist/controllers/form_validation_controller.js.map +1 -1
  84. package/dist/controllers/frame_loading_controller.d.ts +2 -0
  85. package/dist/controllers/frame_loading_controller.js.map +1 -1
  86. package/dist/controllers/highlight_controller.d.ts +2 -0
  87. package/dist/controllers/highlight_controller.js.map +1 -1
  88. package/dist/controllers/hover_card_controller.d.ts +2 -2
  89. package/dist/controllers/hover_card_controller.js.map +1 -1
  90. package/dist/controllers/idle_controller.d.ts +5 -3
  91. package/dist/controllers/idle_controller.js +27 -5
  92. package/dist/controllers/idle_controller.js.map +1 -1
  93. package/dist/controllers/input_mask_controller.d.ts +2 -0
  94. package/dist/controllers/input_mask_controller.js.map +1 -1
  95. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  96. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  97. package/dist/controllers/listbox_controller.d.ts +2 -0
  98. package/dist/controllers/listbox_controller.js.map +1 -1
  99. package/dist/controllers/local_time_controller.d.ts +2 -0
  100. package/dist/controllers/local_time_controller.js.map +1 -1
  101. package/dist/controllers/masonry_controller.d.ts +2 -0
  102. package/dist/controllers/masonry_controller.js.map +1 -1
  103. package/dist/controllers/menubar_controller.js +5 -3
  104. package/dist/controllers/menubar_controller.js.map +1 -1
  105. package/dist/controllers/meter_controller.d.ts +2 -0
  106. package/dist/controllers/meter_controller.js.map +1 -1
  107. package/dist/controllers/multi_select_controller.d.ts +48 -12
  108. package/dist/controllers/multi_select_controller.js +460 -151
  109. package/dist/controllers/multi_select_controller.js.map +1 -1
  110. package/dist/controllers/nested_form_controller.d.ts +2 -0
  111. package/dist/controllers/nested_form_controller.js.map +1 -1
  112. package/dist/controllers/network_status_controller.d.ts +2 -0
  113. package/dist/controllers/network_status_controller.js.map +1 -1
  114. package/dist/controllers/number_input_controller.d.ts +26 -6
  115. package/dist/controllers/number_input_controller.js +317 -51
  116. package/dist/controllers/number_input_controller.js.map +1 -1
  117. package/dist/controllers/otp_controller.d.ts +3 -0
  118. package/dist/controllers/otp_controller.js.map +1 -1
  119. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  120. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  121. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  122. package/dist/controllers/overflow_menu_controller.js +6 -1
  123. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  124. package/dist/controllers/pagination_controller.d.ts +2 -0
  125. package/dist/controllers/pagination_controller.js +35 -1
  126. package/dist/controllers/pagination_controller.js.map +1 -1
  127. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  128. package/dist/controllers/password_reveal_controller.js.map +1 -1
  129. package/dist/controllers/password_strength_controller.d.ts +5 -3
  130. package/dist/controllers/password_strength_controller.js +20 -2
  131. package/dist/controllers/password_strength_controller.js.map +1 -1
  132. package/dist/controllers/persist_controller.d.ts +33 -19
  133. package/dist/controllers/persist_controller.js +449 -120
  134. package/dist/controllers/persist_controller.js.map +1 -1
  135. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  136. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  137. package/dist/controllers/popover_controller.d.ts +2 -2
  138. package/dist/controllers/popover_controller.js +77 -4
  139. package/dist/controllers/popover_controller.js.map +1 -1
  140. package/dist/controllers/portal_controller.d.ts +3 -2
  141. package/dist/controllers/portal_controller.js.map +1 -1
  142. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  143. package/dist/controllers/preview_guard_controller.js.map +1 -1
  144. package/dist/controllers/progress_controller.d.ts +3 -0
  145. package/dist/controllers/progress_controller.js.map +1 -1
  146. package/dist/controllers/radio_group_controller.d.ts +44 -15
  147. package/dist/controllers/radio_group_controller.js +540 -56
  148. package/dist/controllers/radio_group_controller.js.map +1 -1
  149. package/dist/controllers/rating_controller.d.ts +38 -31
  150. package/dist/controllers/rating_controller.js +276 -89
  151. package/dist/controllers/rating_controller.js.map +1 -1
  152. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  153. package/dist/controllers/reading_progress_controller.js.map +1 -1
  154. package/dist/controllers/resizable_controller.d.ts +2 -0
  155. package/dist/controllers/resizable_controller.js +33 -0
  156. package/dist/controllers/resizable_controller.js.map +1 -1
  157. package/dist/controllers/roving_controller.d.ts +6 -0
  158. package/dist/controllers/roving_controller.js +60 -5
  159. package/dist/controllers/roving_controller.js.map +1 -1
  160. package/dist/controllers/scroll_area_controller.d.ts +25 -11
  161. package/dist/controllers/scroll_area_controller.js +557 -125
  162. package/dist/controllers/scroll_area_controller.js.map +1 -1
  163. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  164. package/dist/controllers/scroll_visibility_controller.js +33 -0
  165. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  166. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  167. package/dist/controllers/scrollspy_controller.js.map +1 -1
  168. package/dist/controllers/separator_controller.d.ts +54 -11
  169. package/dist/controllers/separator_controller.js +354 -38
  170. package/dist/controllers/separator_controller.js.map +1 -1
  171. package/dist/controllers/sidebar_controller.js +83 -10
  172. package/dist/controllers/sidebar_controller.js.map +1 -1
  173. package/dist/controllers/skeleton_controller.d.ts +2 -0
  174. package/dist/controllers/skeleton_controller.js.map +1 -1
  175. package/dist/controllers/slider_controller.d.ts +2 -0
  176. package/dist/controllers/slider_controller.js.map +1 -1
  177. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  178. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  179. package/dist/controllers/spinner_controller.d.ts +2 -0
  180. package/dist/controllers/spinner_controller.js.map +1 -1
  181. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  182. package/dist/controllers/step_indicator_controller.js.map +1 -1
  183. package/dist/controllers/stepper_controller.d.ts +2 -0
  184. package/dist/controllers/stepper_controller.js.map +1 -1
  185. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  186. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  187. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  188. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  189. package/dist/controllers/submit_once_controller.d.ts +94 -38
  190. package/dist/controllers/submit_once_controller.js +399 -121
  191. package/dist/controllers/submit_once_controller.js.map +1 -1
  192. package/dist/controllers/switch_controller.d.ts +2 -0
  193. package/dist/controllers/switch_controller.js.map +1 -1
  194. package/dist/controllers/tags_input_controller.d.ts +43 -11
  195. package/dist/controllers/tags_input_controller.js +356 -120
  196. package/dist/controllers/tags_input_controller.js.map +1 -1
  197. package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
  198. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  199. package/dist/controllers/theme_controller.d.ts +2 -0
  200. package/dist/controllers/theme_controller.js +8 -6
  201. package/dist/controllers/theme_controller.js.map +1 -1
  202. package/dist/controllers/time_picker_controller.d.ts +44 -13
  203. package/dist/controllers/time_picker_controller.js +296 -107
  204. package/dist/controllers/time_picker_controller.js.map +1 -1
  205. package/dist/controllers/toast_controller.d.ts +4 -0
  206. package/dist/controllers/toast_controller.js.map +1 -1
  207. package/dist/controllers/toggle_group_controller.d.ts +41 -13
  208. package/dist/controllers/toggle_group_controller.js +378 -55
  209. package/dist/controllers/toggle_group_controller.js.map +1 -1
  210. package/dist/controllers/toolbar_controller.js +5 -3
  211. package/dist/controllers/toolbar_controller.js.map +1 -1
  212. package/dist/controllers/tooltip_controller.d.ts +2 -2
  213. package/dist/controllers/tooltip_controller.js.map +1 -1
  214. package/dist/controllers/transition_controller.d.ts +4 -3
  215. package/dist/controllers/transition_controller.js.map +1 -1
  216. package/dist/controllers/tree_view_controller.d.ts +2 -0
  217. package/dist/controllers/tree_view_controller.js +7 -4
  218. package/dist/controllers/tree_view_controller.js.map +1 -1
  219. package/dist/index.js +4963 -1581
  220. package/dist/index.js.map +1 -1
  221. package/dist/inspector/cli.d.ts +123 -6
  222. package/dist/inspector/cli.js +212 -18
  223. package/dist/inspector/cli.js.map +1 -1
  224. package/dist/inspector/cli_bin.js +274 -50
  225. package/dist/inspector/cli_bin.js.map +1 -1
  226. package/dist/inspector/examples.json +15 -15
  227. package/dist/inspector/manifest.json +602 -80
  228. package/dist/positioning/index.d.ts +4 -2
  229. package/dist/positioning/index.js.map +1 -1
  230. package/package.json +1 -1
@@ -2,6 +2,154 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/scroll_area_controller.ts
4
4
 
5
+ // src/utils/attribute_lease.ts
6
+ var AttributeLease = class {
7
+ #attribute;
8
+ #records = /* @__PURE__ */ new Map();
9
+ /** @param attribute - The attribute whose temporary values this lease owns. */
10
+ constructor(attribute) {
11
+ this.#attribute = attribute;
12
+ }
13
+ /** Writes or removes the leased attribute while preserving its authored value. */
14
+ write(element, value) {
15
+ const existing = this.#records.get(element);
16
+ if (existing) {
17
+ existing.written = value;
18
+ } else {
19
+ this.#records.set(element, {
20
+ original: element.getAttribute(this.#attribute),
21
+ written: value
22
+ });
23
+ }
24
+ this.#reflect(element, value);
25
+ }
26
+ /** Returns one lease without overwriting a value subsequently authored by a consumer. */
27
+ 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);
39
+ }
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
+ };
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);
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
+ function inheritsFieldsetDisabled(control) {
77
+ let fieldset = control.closest("fieldset[disabled]");
78
+ while (fieldset) {
79
+ const legend = Array.from(fieldset.children).find((child) => child.tagName === "LEGEND");
80
+ if (!legend?.contains(control)) return true;
81
+ fieldset = fieldset.parentElement?.closest("fieldset[disabled]") ?? null;
82
+ }
83
+ return false;
84
+ }
85
+ function canTakeFocus(element) {
86
+ if (element.closest("[hidden], [inert]")) return false;
87
+ if (element instanceof HTMLInputElement && element.type === "hidden") return false;
88
+ if (!("disabled" in element)) return true;
89
+ if (element.disabled) return false;
90
+ return !inheritsFieldsetDisabled(element);
91
+ }
92
+ var TAB_STOP_CANDIDATE_SELECTOR = [
93
+ "a[href]",
94
+ "area[href]",
95
+ "button",
96
+ "input",
97
+ "select",
98
+ "textarea",
99
+ "summary",
100
+ "iframe",
101
+ "audio[controls]",
102
+ "video[controls]",
103
+ "[tabindex]",
104
+ "[contenteditable]"
105
+ ].join(",");
106
+ function isRenderedForFocus(element) {
107
+ const check = element.checkVisibility;
108
+ return typeof check === "function" ? check.call(element, { visibilityProperty: true }) : true;
109
+ }
110
+ function authoredTabindex(element) {
111
+ const value = element.getAttribute("tabindex");
112
+ if (value === null || !/^[+-]?\d+$/.test(value.trim())) return null;
113
+ return Number(value);
114
+ }
115
+ function hasNativeTabStop(element) {
116
+ if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {
117
+ return element.hasAttribute("href");
118
+ }
119
+ if (element instanceof HTMLButtonElement || element instanceof HTMLSelectElement || element instanceof HTMLTextAreaElement) {
120
+ return true;
121
+ }
122
+ if (element instanceof HTMLInputElement) return element.type !== "hidden";
123
+ if (element instanceof HTMLIFrameElement) return true;
124
+ if (element.tagName === "AUDIO" || element.tagName === "VIDEO") {
125
+ return element.hasAttribute("controls");
126
+ }
127
+ if (element instanceof HTMLElement && element.tagName === "SUMMARY") {
128
+ const details = element.parentElement;
129
+ return details instanceof HTMLDetailsElement && Array.from(details.children).find((child) => child.tagName === "SUMMARY") === element;
130
+ }
131
+ return false;
132
+ }
133
+ function hasEditableTabStop(element) {
134
+ const value = element.getAttribute("contenteditable")?.toLowerCase();
135
+ return value === "" || value === "true" || value === "plaintext-only";
136
+ }
137
+ function isTabStop(element) {
138
+ if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;
139
+ const tabindex = authoredTabindex(element);
140
+ if (tabindex !== null) return tabindex >= 0;
141
+ return hasNativeTabStop(element) || hasEditableTabStop(element);
142
+ }
143
+ function firstTabStop(root) {
144
+ for (const candidate of root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)) {
145
+ if (isTabStop(candidate)) return candidate;
146
+ }
147
+ return null;
148
+ }
149
+ function hasTabStop(root) {
150
+ return firstTabStop(root) !== null;
151
+ }
152
+
5
153
  // src/utils/layout_observer.ts
6
154
  var LayoutObserver = class {
7
155
  #callback;
@@ -72,10 +220,97 @@ function logicalScrollMetrics(element, horizontal) {
72
220
  return { position: Math.min(max, Math.max(0, position)), max };
73
221
  }
74
222
 
223
+ // src/utils/microtask_coalescer.ts
224
+ var MicrotaskCoalescer = class {
225
+ #run;
226
+ #queued = false;
227
+ #active = false;
228
+ #generation = 0;
229
+ /** @param run - the single reconciliation pass, invoked at most once per batch. */
230
+ constructor(run) {
231
+ this.#run = run;
232
+ }
233
+ /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
234
+ activate() {
235
+ this.#active = true;
236
+ }
237
+ /** Closes the window and drops any pending pass; call from `disconnect()`. */
238
+ cancel() {
239
+ this.#active = false;
240
+ this.#queued = false;
241
+ this.#generation += 1;
242
+ }
243
+ /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
244
+ schedule() {
245
+ if (!this.#active || this.#queued) return;
246
+ this.#queued = true;
247
+ const generation = this.#generation;
248
+ queueMicrotask(() => {
249
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
250
+ this.#queued = false;
251
+ this.#run();
252
+ });
253
+ }
254
+ };
255
+
256
+ // src/utils/style_property_lease.ts
257
+ var StylePropertyLease = class {
258
+ #property;
259
+ #records = /* @__PURE__ */ new Map();
260
+ /** @param property - The CSS property whose temporary values this lease owns. */
261
+ constructor(property) {
262
+ this.#property = property;
263
+ }
264
+ /** Writes or removes the leased declaration while preserving its authored value. */
265
+ write(element, value, priority = "") {
266
+ const existing = this.#records.get(element);
267
+ if (existing) {
268
+ existing.writtenValue = value;
269
+ existing.writtenPriority = value === null ? "" : priority;
270
+ } else {
271
+ this.#records.set(element, {
272
+ originalValue: element.style.getPropertyValue(this.#property),
273
+ originalPriority: element.style.getPropertyPriority(this.#property),
274
+ writtenValue: value,
275
+ writtenPriority: value === null ? "" : priority
276
+ });
277
+ }
278
+ this.#reflect(element, value, priority);
279
+ }
280
+ /** Returns one lease without overwriting a later consumer declaration. */
281
+ return(element) {
282
+ const record = this.#records.get(element);
283
+ if (!record) return;
284
+ this.#records.delete(element);
285
+ const style = element.style;
286
+ const stillOwned = style.getPropertyValue(this.#property) === (record.writtenValue ?? "") && style.getPropertyPriority(this.#property) === record.writtenPriority;
287
+ if (stillOwned) {
288
+ this.#reflect(element, record.originalValue, record.originalPriority);
289
+ }
290
+ }
291
+ /** Returns every outstanding declaration lease. */
292
+ returnAll() {
293
+ for (const element of Array.from(this.#records.keys())) this.return(element);
294
+ }
295
+ /** Reflects only a real declaration transition. */
296
+ #reflect(element, value, priority) {
297
+ const style = element.style;
298
+ const nextValue = value ?? "";
299
+ const nextPriority = value === null ? "" : priority;
300
+ if (style.getPropertyValue(this.#property) === nextValue && style.getPropertyPriority(this.#property) === nextPriority) {
301
+ return;
302
+ }
303
+ if (value === null) style.removeProperty(this.#property);
304
+ else style.setProperty(this.#property, value, priority);
305
+ }
306
+ };
307
+
75
308
  // src/utils/tabindex_loan.ts
76
309
  var TabindexLoan = class {
77
310
  #value;
78
311
  #lent = /* @__PURE__ */ new Set();
312
+ /** Returns live loans before Turbo can copy them into its page snapshot. */
313
+ #beforeCache = new BeforeCacheReset(() => this.returnAll());
79
314
  /**
80
315
  * @param value - the `tabindex` to lend. `"-1"` (the default) is
81
316
  * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
@@ -89,6 +324,7 @@ var TabindexLoan = class {
89
324
  if (element.hasAttribute("tabindex")) return;
90
325
  element.setAttribute("tabindex", this.#value);
91
326
  this.#lent.add(element);
327
+ this.#beforeCache.activate();
92
328
  }
93
329
  /** Takes back every loan whose value is still the one that was lent. */
94
330
  returnAll() {
@@ -96,19 +332,11 @@ var TabindexLoan = class {
96
332
  if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
97
333
  }
98
334
  this.#lent.clear();
335
+ this.#beforeCache.deactivate();
99
336
  }
100
337
  };
101
338
 
102
339
  // src/controllers/scroll_area_controller.ts
103
- var FOCUSABLE_SELECTOR = [
104
- "a[href]",
105
- "button:not([disabled])",
106
- "input:not([disabled])",
107
- "select:not([disabled])",
108
- "textarea:not([disabled])",
109
- "[tabindex]:not([tabindex='-1'])",
110
- "[contenteditable='true']"
111
- ].join(",");
112
340
  var EDGE_EPSILON = 1;
113
341
  var ScrollAreaController = class extends Controller {
114
342
  static targets = ["viewport"];
@@ -116,152 +344,356 @@ var ScrollAreaController = class extends Controller {
116
344
  orientation: { type: String, default: "vertical" }
117
345
  };
118
346
  static events = ["reach"];
119
- #layout = new LayoutObserver(() => this.#update());
120
- /** Re-checks the tab stop when the viewport's focusable content comes or goes. */
347
+ #layout = new LayoutObserver(() => this.#refresh());
348
+ #rebind = new MicrotaskCoalescer(() => this.#syncViewport());
349
+ #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
350
+ #tabindex = new TabindexLoan("0");
351
+ #role = new AttributeLease("role");
352
+ #overflowState = new AttributeLease("data-overflow");
353
+ #scrollState = new AttributeLease("data-scroll");
354
+ #progress = new StylePropertyLease("--stimeo--scroll-progress");
355
+ #viewport = null;
121
356
  #content = null;
122
- /** Last edge reported via `reach`, so the event fires once per arrival. */
357
+ #nameSources = null;
358
+ #nameGraph = null;
359
+ #observedNameIds = /* @__PURE__ */ new Set();
360
+ #observedNameSources = [];
361
+ #fonts = null;
362
+ #scrollFrame = null;
363
+ #overflowing = false;
123
364
  #lastEdge = null;
124
- /** Whether this controller added `tabindex`, so teardown only removes its own. */
125
- #tabindex = new TabindexLoan("0");
126
- /** Whether this controller added `role="region"`, for symmetric teardown. */
127
- #addedRole = false;
365
+ #ownedHostMutations = /* @__PURE__ */ new Map();
128
366
  #onScroll = () => {
129
- this.#update();
367
+ if (this.#scrollFrame !== null) return;
368
+ this.#scrollFrame = requestAnimationFrame(() => {
369
+ this.#scrollFrame = null;
370
+ const viewport = this.#viewport;
371
+ if (viewport) this.#syncPosition(viewport, this.#overflowing);
372
+ });
373
+ };
374
+ #onLoad = () => {
375
+ this.#refresh();
376
+ };
377
+ #onFontsSettled = () => {
378
+ this.#refresh();
130
379
  };
131
380
  connect() {
132
- if (!this.hasViewportTarget) return;
133
- this.viewportTarget.addEventListener("scroll", this.#onScroll, { passive: true });
134
- this.#layout.observe(this.viewportTarget);
381
+ this.#beforeCache.activate();
382
+ this.#rebind.activate();
135
383
  this.#layout.observeViewport();
136
- if (typeof MutationObserver !== "undefined") {
137
- this.#content = new MutationObserver(() => {
138
- if (!this.hasViewportTarget) return;
139
- const vp = this.viewportTarget;
140
- this.#syncKeyboardReach(vp, this.#syncOverflow(vp));
141
- });
142
- this.#content.observe(this.viewportTarget, {
143
- subtree: true,
144
- childList: true,
145
- attributes: true
146
- });
147
- }
148
- this.#update();
384
+ this.#bindFonts();
385
+ this.#syncViewport();
149
386
  }
150
387
  disconnect() {
151
- if (this.hasViewportTarget) {
152
- this.viewportTarget.removeEventListener("scroll", this.#onScroll);
153
- this.#clearAddedAttributes(this.viewportTarget);
154
- }
388
+ this.#beforeCache.deactivate();
389
+ this.#rebind.cancel();
390
+ this.#cancelScrollFrame();
391
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
155
392
  this.#layout.disconnect();
393
+ this.#unbindFonts();
394
+ this.#clearHostState();
395
+ }
396
+ /** Re-measures when a retained viewport's orientation Value is morphed. */
397
+ orientationValueChanged() {
398
+ this.#refresh();
399
+ }
400
+ /** Schedules a complete resource rebind for a runtime viewport target. */
401
+ viewportTargetConnected() {
402
+ this.#rebind.schedule();
403
+ }
404
+ /** Schedules cleanup or replacement binding after a viewport leaves. */
405
+ viewportTargetDisconnected() {
406
+ this.#rebind.schedule();
407
+ }
408
+ /** Rebinds every viewport-owned resource against the final target in the mutation batch. */
409
+ #syncViewport() {
410
+ const next = this.hasViewportTarget ? this.viewportTarget : null;
411
+ if (next === this.#viewport) {
412
+ if (next) this.#refresh();
413
+ else this.#clearHostState();
414
+ return;
415
+ }
416
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
417
+ this.#viewport = next;
418
+ if (!next) {
419
+ this.#clearHostState();
420
+ return;
421
+ }
422
+ next.addEventListener("scroll", this.#onScroll, { passive: true });
423
+ next.addEventListener("load", this.#onLoad, true);
424
+ this.#layout.observe(next);
425
+ this.#bindContentObserver(next);
426
+ this.#refresh();
427
+ }
428
+ /** Observes structural and attribute changes that can alter overflow or Tab stops. */
429
+ #bindContentObserver(viewport) {
430
+ this.#content = new MutationObserver((records) => {
431
+ if (this.#viewport !== viewport || !this.#hasRelevantContentMutation(records)) return;
432
+ this.#refresh();
433
+ });
434
+ this.#content.observe(viewport, {
435
+ subtree: true,
436
+ childList: true,
437
+ attributes: true,
438
+ characterData: true
439
+ });
440
+ }
441
+ /** Ignores only state-hook mutations whose exact value this controller just wrote. */
442
+ #hasRelevantContentMutation(records) {
443
+ let relevant = false;
444
+ const consumed = /* @__PURE__ */ new Set();
445
+ for (const record of records) {
446
+ const attribute = record.attributeName;
447
+ if (record.type === "attributes" && record.target === this.element && attribute !== null && this.#ownedHostMutations.has(attribute) && this.element.getAttribute(attribute) === this.#ownedHostMutations.get(attribute)) {
448
+ consumed.add(attribute);
449
+ } else {
450
+ relevant = true;
451
+ }
452
+ }
453
+ for (const attribute of consumed) this.#ownedHostMutations.delete(attribute);
454
+ return relevant;
455
+ }
456
+ /** Releases every resource and borrowed attribute owned by one former viewport. */
457
+ #unbindViewport(viewport) {
458
+ this.#cancelScrollFrame();
459
+ viewport.removeEventListener("scroll", this.#onScroll);
460
+ viewport.removeEventListener("load", this.#onLoad, true);
461
+ this.#layout.unobserve(viewport);
156
462
  this.#content?.disconnect();
157
463
  this.#content = null;
464
+ this.#disconnectNameSources();
465
+ this.#clearViewportAttributes(viewport);
466
+ this.#ownedHostMutations.clear();
467
+ this.#viewport = null;
468
+ this.#overflowing = false;
158
469
  this.#lastEdge = null;
159
470
  }
160
- /** Re-measures overflow and scroll position and reflects the state hooks. */
161
- #update() {
162
- if (!this.hasViewportTarget) return;
163
- const vp = this.viewportTarget;
164
- const overflowing = this.#syncOverflow(vp);
165
- this.#syncKeyboardReach(vp, overflowing);
166
- const { position, progress } = this.#measurePosition(vp);
167
- this.element.setAttribute("data-scroll", position);
168
- this.element.style.setProperty("--stimeo--scroll-progress", String(progress));
169
- const edge = position === "start" ? "start" : position === "end" ? "end" : null;
170
- if (overflowing && edge && edge !== this.#lastEdge) {
171
- this.#lastEdge = edge;
172
- this.dispatch("reach", { detail: { edge } });
173
- } else if (!edge) {
174
- this.#lastEdge = null;
175
- }
471
+ /** Runs the full structural and positional measurement pass. */
472
+ #refresh() {
473
+ const viewport = this.#viewport;
474
+ if (!viewport) return;
475
+ this.#cancelScrollFrame();
476
+ this.#syncNameSources(viewport);
477
+ this.#overflowing = this.#syncOverflow(viewport);
478
+ this.#syncKeyboardReach(viewport, this.#overflowing);
479
+ this.#syncPosition(viewport, this.#overflowing);
176
480
  }
177
- /** Whether the viewport can scroll on the configured axis. */
178
- /**
179
- * Measures overflow and reflects the `data-overflow` hook.
180
- *
181
- * The write is skipped when the value is unchanged. An identical `setAttribute` still
182
- * queues a MutationRecord, and markup that puts the viewport target on the controller
183
- * element itself would then have the content observer trigger its own next callback.
184
- */
185
- #syncOverflow(vp) {
186
- const overflowing = this.#measureOverflow(vp);
187
- const next = overflowing ? "true" : "false";
188
- if (this.element.getAttribute("data-overflow") !== next) {
189
- this.element.setAttribute("data-overflow", next);
190
- }
481
+ /** Measures overflow and reflects the state hook without identical DOM writes. */
482
+ #syncOverflow(viewport) {
483
+ const overflowing = this.#measureOverflow(viewport);
484
+ this.#writeHostAttribute(this.#overflowState, "data-overflow", overflowing ? "true" : "false");
191
485
  return overflowing;
192
486
  }
193
- #measureOverflow(vp) {
194
- const o = this.orientationValue;
195
- const vertical = o !== "horizontal" && vp.scrollHeight > vp.clientHeight + EDGE_EPSILON;
196
- const horizontal = o !== "vertical" && vp.scrollWidth > vp.clientWidth + EDGE_EPSILON;
487
+ /** Measures the primary-axis position and dispatches a newly established edge. */
488
+ #syncPosition(viewport, overflowing) {
489
+ const { position, progress } = this.#measurePosition(viewport);
490
+ this.#writeHostAttribute(this.#scrollState, "data-scroll", position);
491
+ this.#writeHostProgress(String(progress));
492
+ const edge = overflowing && (position === "start" || position === "end") ? position : null;
493
+ if (edge === this.#lastEdge) return;
494
+ this.#lastEdge = edge;
495
+ if (edge) this.dispatch("reach", { detail: { edge } });
496
+ }
497
+ /** Writes one leased host attribute and records self-generated observer input. */
498
+ #writeHostAttribute(lease, attribute, value) {
499
+ const before = this.element.getAttribute(attribute);
500
+ lease.write(this.element, value);
501
+ this.#recordOwnedHostMutation(attribute, before);
502
+ }
503
+ /** Writes the leased progress property and records its serialized style mutation. */
504
+ #writeHostProgress(value) {
505
+ const before = this.element.getAttribute("style");
506
+ this.#progress.write(this.element, value);
507
+ this.#recordOwnedHostMutation("style", before);
508
+ }
509
+ /** Records an exact host mutation only when the host is also the observed viewport. */
510
+ #recordOwnedHostMutation(attribute, before) {
511
+ if (this.#viewport !== this.element) return;
512
+ const after = this.element.getAttribute(attribute);
513
+ if (after !== before) this.#ownedHostMutations.set(attribute, after);
514
+ }
515
+ /** Returns host hooks to authored values and clears edge bookkeeping. */
516
+ #clearHostState() {
517
+ this.#overflowState.returnAll();
518
+ this.#scrollState.returnAll();
519
+ this.#progress.returnAll();
520
+ this.#ownedHostMutations.clear();
521
+ this.#overflowing = false;
522
+ this.#lastEdge = null;
523
+ }
524
+ /** Whether the configured axis currently has a scroll range. */
525
+ #measureOverflow(viewport) {
526
+ const orientation = this.orientationValue;
527
+ const vertical = orientation !== "horizontal" && viewport.scrollHeight > viewport.clientHeight + EDGE_EPSILON;
528
+ const horizontal = orientation !== "vertical" && viewport.scrollWidth > viewport.clientWidth + EDGE_EPSILON;
197
529
  return vertical || horizontal;
198
530
  }
199
- /**
200
- * Reports the scroll position bucket and 0–1 progress on the primary axis. For
201
- * `both`, the vertical axis is used when it overflows, otherwise the horizontal.
202
- */
203
- #measurePosition(vp) {
204
- const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && vp.scrollHeight <= vp.clientHeight + EDGE_EPSILON;
205
- const { position: scrollPos, max: maxScroll } = logicalScrollMetrics(vp, horizontalPrimary);
531
+ /** Reports the primary-axis position bucket and normalized 0–1 progress. */
532
+ #measurePosition(viewport) {
533
+ const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && viewport.scrollHeight <= viewport.clientHeight + EDGE_EPSILON;
534
+ const { position: scrollPosition, max: maxScroll } = logicalScrollMetrics(
535
+ viewport,
536
+ horizontalPrimary
537
+ );
206
538
  if (maxScroll <= EDGE_EPSILON) return { position: "start", progress: 0 };
207
- const progress = Math.min(1, Math.max(0, scrollPos / maxScroll));
208
- if (scrollPos <= EDGE_EPSILON) return { position: "start", progress };
209
- if (scrollPos >= maxScroll - EDGE_EPSILON) return { position: "end", progress };
539
+ const progress = Math.min(1, Math.max(0, scrollPosition / maxScroll));
540
+ if (scrollPosition <= EDGE_EPSILON) return { position: "start", progress };
541
+ if (scrollPosition >= maxScroll - EDGE_EPSILON) return { position: "end", progress };
210
542
  return { position: "middle", progress };
211
543
  }
212
- /**
213
- * Makes the viewport keyboard-scrollable when it overflows and contains no
214
- * focusable elements of its own (avoiding a double tab stop). Adds `role="region"`
215
- * only when the viewport already carries an accessible name.
216
- */
217
- #syncKeyboardReach(vp, overflowing) {
218
- const wantsTabindex = overflowing && !this.#hasFocusableContent(vp);
219
- if (wantsTabindex) {
220
- this.#tabindex.lend(vp);
221
- if (!vp.hasAttribute("role") && this.#hasAccessibleName(vp)) {
222
- vp.setAttribute("role", "region");
223
- this.#addedRole = true;
224
- }
544
+ /** Borrows keyboard reach only when no usable descendant already owns a Tab stop. */
545
+ #syncKeyboardReach(viewport, overflowing) {
546
+ const wantsTabindex = overflowing && !hasTabStop(viewport);
547
+ if (!wantsTabindex) {
548
+ this.#clearViewportAttributes(viewport);
549
+ return;
550
+ }
551
+ this.#tabindex.lend(viewport);
552
+ if (this.#hasAccessibleName(viewport)) {
553
+ if (!viewport.hasAttribute("role")) this.#role.write(viewport, "region");
225
554
  } else {
226
- this.#clearAddedAttributes(vp);
555
+ this.#role.return(viewport);
227
556
  }
228
557
  }
229
- /** Removes (and resets the flags for) only the attributes this controller added. */
230
- #clearAddedAttributes(vp) {
558
+ /** Returns only the viewport attributes borrowed by this controller. */
559
+ #clearViewportAttributes(viewport) {
231
560
  this.#tabindex.returnAll();
232
- if (this.#addedRole) {
233
- vp.removeAttribute("role");
234
- this.#addedRole = false;
561
+ this.#role.return(viewport);
562
+ }
563
+ /** Whether the viewport already has a non-empty accessible name. */
564
+ #hasAccessibleName(viewport) {
565
+ const sources = this.#resolveNameSources(viewport);
566
+ if (sources.length > 0) {
567
+ return sources.some((source) => {
568
+ const label = source.getAttribute("aria-label")?.trim();
569
+ return Boolean(label || source.textContent?.trim());
570
+ });
235
571
  }
572
+ return Boolean(viewport.getAttribute("aria-label")?.trim());
236
573
  }
237
- /**
238
- * Whether the viewport owns something the user can Tab to *right now*.
239
- *
240
- * The selector alone is not enough: a `display: none` button still matches it,
241
- * so a viewport whose only control is revealed on demand would never get a tab
242
- * stop — leaving it unreachable by keyboard exactly while it has nothing else to
243
- * offer. Only rendered candidates count.
244
- */
245
- #hasFocusableContent(vp) {
246
- return Array.from(vp.querySelectorAll(FOCUSABLE_SELECTOR)).some(
247
- (el) => this.#isRendered(el)
574
+ /** Resolves unique, live `aria-labelledby` references in authored order. */
575
+ #resolveNameSources(viewport) {
576
+ const ids = this.#nameReferenceIds(viewport);
577
+ const sources = [];
578
+ const seen = /* @__PURE__ */ new Set();
579
+ for (const id of ids) {
580
+ const source = viewport.ownerDocument.getElementById(id);
581
+ if (source && !seen.has(source)) {
582
+ seen.add(source);
583
+ sources.push(source);
584
+ }
585
+ }
586
+ return sources;
587
+ }
588
+ /** Observes external name sources so a retained region never becomes unnamed. */
589
+ #syncNameSources(viewport) {
590
+ const ids = this.#nameReferenceIds(viewport);
591
+ const sources = this.#resolveNameSources(viewport).filter(
592
+ (source) => !viewport.contains(source)
248
593
  );
594
+ if (ids.length === this.#observedNameIds.size && ids.every((id) => this.#observedNameIds.has(id)) && sources.length === this.#observedNameSources.length && sources.every((source, index) => source === this.#observedNameSources[index])) {
595
+ return;
596
+ }
597
+ this.#disconnectNameSources();
598
+ this.#observedNameIds = new Set(ids);
599
+ this.#observedNameSources = sources;
600
+ if (ids.length > 0) {
601
+ this.#nameGraph = new MutationObserver((records) => {
602
+ if (this.#viewport === viewport && this.#hasRelevantNameGraphMutation(records)) {
603
+ this.#refresh();
604
+ }
605
+ });
606
+ const documentElement = viewport.ownerDocument.documentElement;
607
+ if (documentElement) {
608
+ this.#nameGraph.observe(documentElement, {
609
+ subtree: true,
610
+ childList: true,
611
+ attributes: true,
612
+ attributeFilter: ["id"],
613
+ attributeOldValue: true
614
+ });
615
+ }
616
+ }
617
+ if (sources.length === 0) return;
618
+ this.#nameSources = new MutationObserver(() => {
619
+ this.#refresh();
620
+ });
621
+ for (const source of sources) {
622
+ this.#nameSources.observe(source, {
623
+ subtree: true,
624
+ childList: true,
625
+ characterData: true,
626
+ attributes: true
627
+ });
628
+ }
249
629
  }
250
- /**
251
- * Whether `el` is actually rendered, and so can hold focus.
252
- *
253
- * `checkVisibility()` answers this for every way CSS can remove a box, including
254
- * a class-driven `display: none` that no attribute reveals. The `hidden` walk in
255
- * front of it is not redundant: it is the one case a DOM-only environment with no
256
- * layout engine has to be told about explicitly.
257
- */
258
- #isRendered(el) {
259
- if (el.closest("[hidden]") !== null) return false;
260
- const check = el.checkVisibility;
261
- return typeof check === "function" ? check.call(el, { visibilityProperty: true }) : true;
630
+ /** Returns normalized unique label-reference ids in authored order. */
631
+ #nameReferenceIds(viewport) {
632
+ const tokens = viewport.getAttribute("aria-labelledby")?.trim().split(/\s+/).filter(Boolean);
633
+ return Array.from(new Set(tokens));
634
+ }
635
+ /** Whether document-graph changes can resolve or invalidate a referenced id. */
636
+ #hasRelevantNameGraphMutation(records) {
637
+ for (const record of records) {
638
+ if (record.type === "attributes") {
639
+ const current = record.target.id;
640
+ if (this.#observedNameIds.has(current) || this.#observedNameIds.has(record.oldValue ?? "")) {
641
+ return true;
642
+ }
643
+ continue;
644
+ }
645
+ for (const node of [...record.addedNodes, ...record.removedNodes]) {
646
+ if (this.#nodeContainsObservedNameId(node)) return true;
647
+ }
648
+ }
649
+ return false;
262
650
  }
263
- #hasAccessibleName(vp) {
264
- return vp.hasAttribute("aria-label") || vp.hasAttribute("aria-labelledby");
651
+ /** Whether a changed subtree contains one of the current label-reference ids. */
652
+ #nodeContainsObservedNameId(node) {
653
+ if (!(node instanceof Element)) return false;
654
+ if (this.#observedNameIds.has(node.id)) return true;
655
+ return Array.from(node.querySelectorAll("[id]")).some(
656
+ (element) => this.#observedNameIds.has(element.id)
657
+ );
658
+ }
659
+ /** Stops observing accessible-name sources. */
660
+ #disconnectNameSources() {
661
+ this.#nameSources?.disconnect();
662
+ this.#nameSources = null;
663
+ this.#nameGraph?.disconnect();
664
+ this.#nameGraph = null;
665
+ this.#observedNameIds.clear();
666
+ this.#observedNameSources = [];
667
+ }
668
+ /** Subscribes to font completion when the current engine exposes `document.fonts`. */
669
+ #bindFonts() {
670
+ const fonts = this.element.ownerDocument.fonts;
671
+ if (!fonts || this.#fonts === fonts) return;
672
+ this.#unbindFonts();
673
+ this.#fonts = fonts;
674
+ fonts.addEventListener("loadingdone", this.#onFontsSettled);
675
+ fonts.addEventListener("loadingerror", this.#onFontsSettled);
676
+ }
677
+ /** Removes the font completion subscriptions. */
678
+ #unbindFonts() {
679
+ this.#fonts?.removeEventListener("loadingdone", this.#onFontsSettled);
680
+ this.#fonts?.removeEventListener("loadingerror", this.#onFontsSettled);
681
+ this.#fonts = null;
682
+ }
683
+ /** Cancels a pending scroll frame. */
684
+ #cancelScrollFrame() {
685
+ if (this.#scrollFrame === null) return;
686
+ cancelAnimationFrame(this.#scrollFrame);
687
+ this.#scrollFrame = null;
688
+ }
689
+ /** Suspends live resources and returns all derived state before snapshotting. */
690
+ #rewindForCache() {
691
+ this.#rebind.cancel();
692
+ this.#cancelScrollFrame();
693
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
694
+ this.#layout.disconnect();
695
+ this.#unbindFonts();
696
+ this.#clearHostState();
265
697
  }
266
698
  };
267
699