stimeo-ui 0.6.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 (199) hide show
  1. package/CHANGELOG.md +62 -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/auto_submit_controller.d.ts +2 -0
  6. package/dist/controllers/auto_submit_controller.js.map +1 -1
  7. package/dist/controllers/avatar_controller.js +47 -5
  8. package/dist/controllers/avatar_controller.js.map +1 -1
  9. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  10. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  11. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  12. package/dist/controllers/bulk_select_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.d.ts +2 -0
  14. package/dist/controllers/calendar_controller.js.map +1 -1
  15. package/dist/controllers/character_counter_controller.d.ts +52 -20
  16. package/dist/controllers/character_counter_controller.js +338 -63
  17. package/dist/controllers/character_counter_controller.js.map +1 -1
  18. package/dist/controllers/checkbox_controller.d.ts +3 -0
  19. package/dist/controllers/checkbox_controller.js.map +1 -1
  20. package/dist/controllers/color_picker_controller.d.ts +3 -0
  21. package/dist/controllers/color_picker_controller.js.map +1 -1
  22. package/dist/controllers/combobox_controller.d.ts +2 -0
  23. package/dist/controllers/combobox_controller.js.map +1 -1
  24. package/dist/controllers/command_palette_controller.d.ts +2 -0
  25. package/dist/controllers/command_palette_controller.js +75 -4
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  28. package/dist/controllers/conditional_fields_controller.js +345 -51
  29. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  30. package/dist/controllers/confirm_controller.d.ts +2 -0
  31. package/dist/controllers/confirm_controller.js +75 -4
  32. package/dist/controllers/confirm_controller.js.map +1 -1
  33. package/dist/controllers/count_up_controller.d.ts +2 -0
  34. package/dist/controllers/count_up_controller.js.map +1 -1
  35. package/dist/controllers/countdown_controller.d.ts +4 -0
  36. package/dist/controllers/countdown_controller.js.map +1 -1
  37. package/dist/controllers/currency_input_controller.d.ts +2 -0
  38. package/dist/controllers/currency_input_controller.js.map +1 -1
  39. package/dist/controllers/data_grid_controller.d.ts +3 -0
  40. package/dist/controllers/data_grid_controller.js.map +1 -1
  41. package/dist/controllers/dialog_controller.js +75 -4
  42. package/dist/controllers/dialog_controller.js.map +1 -1
  43. package/dist/controllers/direct_upload_controller.d.ts +56 -24
  44. package/dist/controllers/direct_upload_controller.js +201 -45
  45. package/dist/controllers/direct_upload_controller.js.map +1 -1
  46. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  47. package/dist/controllers/dirty_form_controller.js +192 -29
  48. package/dist/controllers/dirty_form_controller.js.map +1 -1
  49. package/dist/controllers/dismissible_controller.d.ts +2 -0
  50. package/dist/controllers/dismissible_controller.js +83 -18
  51. package/dist/controllers/dismissible_controller.js.map +1 -1
  52. package/dist/controllers/drawer_controller.js +75 -4
  53. package/dist/controllers/drawer_controller.js.map +1 -1
  54. package/dist/controllers/empty_state_controller.d.ts +2 -0
  55. package/dist/controllers/empty_state_controller.js.map +1 -1
  56. package/dist/controllers/file_dropzone_controller.d.ts +2 -0
  57. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  58. package/dist/controllers/filter_controller.d.ts +2 -0
  59. package/dist/controllers/filter_controller.js.map +1 -1
  60. package/dist/controllers/flash_controller.d.ts +4 -0
  61. package/dist/controllers/flash_controller.js.map +1 -1
  62. package/dist/controllers/focus_controller.d.ts +2 -0
  63. package/dist/controllers/focus_controller.js +75 -4
  64. package/dist/controllers/focus_controller.js.map +1 -1
  65. package/dist/controllers/form_field_controller.d.ts +50 -10
  66. package/dist/controllers/form_field_controller.js +280 -62
  67. package/dist/controllers/form_field_controller.js.map +1 -1
  68. package/dist/controllers/form_validation_controller.d.ts +10 -8
  69. package/dist/controllers/form_validation_controller.js +208 -83
  70. package/dist/controllers/form_validation_controller.js.map +1 -1
  71. package/dist/controllers/frame_loading_controller.d.ts +2 -0
  72. package/dist/controllers/frame_loading_controller.js.map +1 -1
  73. package/dist/controllers/highlight_controller.d.ts +2 -0
  74. package/dist/controllers/highlight_controller.js.map +1 -1
  75. package/dist/controllers/idle_controller.d.ts +2 -0
  76. package/dist/controllers/idle_controller.js.map +1 -1
  77. package/dist/controllers/input_mask_controller.d.ts +2 -0
  78. package/dist/controllers/input_mask_controller.js.map +1 -1
  79. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  80. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  81. package/dist/controllers/listbox_controller.d.ts +2 -0
  82. package/dist/controllers/listbox_controller.js.map +1 -1
  83. package/dist/controllers/local_time_controller.d.ts +2 -0
  84. package/dist/controllers/local_time_controller.js.map +1 -1
  85. package/dist/controllers/masonry_controller.d.ts +2 -0
  86. package/dist/controllers/masonry_controller.js.map +1 -1
  87. package/dist/controllers/meter_controller.d.ts +2 -0
  88. package/dist/controllers/meter_controller.js.map +1 -1
  89. package/dist/controllers/multi_select_controller.d.ts +2 -0
  90. package/dist/controllers/multi_select_controller.js.map +1 -1
  91. package/dist/controllers/nested_form_controller.d.ts +2 -0
  92. package/dist/controllers/nested_form_controller.js.map +1 -1
  93. package/dist/controllers/network_status_controller.d.ts +2 -0
  94. package/dist/controllers/network_status_controller.js.map +1 -1
  95. package/dist/controllers/number_input_controller.d.ts +2 -0
  96. package/dist/controllers/number_input_controller.js +47 -5
  97. package/dist/controllers/number_input_controller.js.map +1 -1
  98. package/dist/controllers/otp_controller.d.ts +3 -0
  99. package/dist/controllers/otp_controller.js.map +1 -1
  100. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  101. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  102. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  103. package/dist/controllers/overflow_menu_controller.js +2 -1
  104. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  105. package/dist/controllers/pagination_controller.d.ts +2 -0
  106. package/dist/controllers/pagination_controller.js +2 -1
  107. package/dist/controllers/pagination_controller.js.map +1 -1
  108. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  109. package/dist/controllers/password_reveal_controller.js.map +1 -1
  110. package/dist/controllers/password_strength_controller.d.ts +2 -0
  111. package/dist/controllers/password_strength_controller.js.map +1 -1
  112. package/dist/controllers/persist_controller.d.ts +30 -16
  113. package/dist/controllers/persist_controller.js +432 -122
  114. package/dist/controllers/persist_controller.js.map +1 -1
  115. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  116. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  117. package/dist/controllers/popover_controller.js +77 -4
  118. package/dist/controllers/popover_controller.js.map +1 -1
  119. package/dist/controllers/portal_controller.d.ts +2 -0
  120. package/dist/controllers/portal_controller.js.map +1 -1
  121. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  122. package/dist/controllers/preview_guard_controller.js.map +1 -1
  123. package/dist/controllers/progress_controller.d.ts +3 -0
  124. package/dist/controllers/progress_controller.js.map +1 -1
  125. package/dist/controllers/radio_group_controller.d.ts +3 -0
  126. package/dist/controllers/radio_group_controller.js.map +1 -1
  127. package/dist/controllers/rating_controller.d.ts +2 -0
  128. package/dist/controllers/rating_controller.js +9 -5
  129. package/dist/controllers/rating_controller.js.map +1 -1
  130. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  131. package/dist/controllers/reading_progress_controller.js.map +1 -1
  132. package/dist/controllers/resizable_controller.d.ts +2 -0
  133. package/dist/controllers/resizable_controller.js.map +1 -1
  134. package/dist/controllers/roving_controller.d.ts +2 -0
  135. package/dist/controllers/roving_controller.js.map +1 -1
  136. package/dist/controllers/scroll_area_controller.d.ts +23 -15
  137. package/dist/controllers/scroll_area_controller.js +462 -162
  138. package/dist/controllers/scroll_area_controller.js.map +1 -1
  139. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  140. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  141. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  142. package/dist/controllers/scrollspy_controller.js.map +1 -1
  143. package/dist/controllers/separator_controller.d.ts +54 -11
  144. package/dist/controllers/separator_controller.js +354 -38
  145. package/dist/controllers/separator_controller.js.map +1 -1
  146. package/dist/controllers/sidebar_controller.js +83 -10
  147. package/dist/controllers/sidebar_controller.js.map +1 -1
  148. package/dist/controllers/skeleton_controller.d.ts +2 -0
  149. package/dist/controllers/skeleton_controller.js.map +1 -1
  150. package/dist/controllers/slider_controller.d.ts +2 -0
  151. package/dist/controllers/slider_controller.js.map +1 -1
  152. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  153. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  154. package/dist/controllers/spinner_controller.d.ts +2 -0
  155. package/dist/controllers/spinner_controller.js.map +1 -1
  156. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  157. package/dist/controllers/step_indicator_controller.js.map +1 -1
  158. package/dist/controllers/stepper_controller.d.ts +2 -0
  159. package/dist/controllers/stepper_controller.js.map +1 -1
  160. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  161. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  162. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  163. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  164. package/dist/controllers/submit_once_controller.d.ts +94 -38
  165. package/dist/controllers/submit_once_controller.js +399 -121
  166. package/dist/controllers/submit_once_controller.js.map +1 -1
  167. package/dist/controllers/switch_controller.d.ts +2 -0
  168. package/dist/controllers/switch_controller.js.map +1 -1
  169. package/dist/controllers/tags_input_controller.d.ts +2 -0
  170. package/dist/controllers/tags_input_controller.js.map +1 -1
  171. package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
  172. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  173. package/dist/controllers/theme_controller.d.ts +2 -0
  174. package/dist/controllers/theme_controller.js +8 -6
  175. package/dist/controllers/theme_controller.js.map +1 -1
  176. package/dist/controllers/time_picker_controller.d.ts +2 -0
  177. package/dist/controllers/time_picker_controller.js.map +1 -1
  178. package/dist/controllers/toast_controller.d.ts +4 -0
  179. package/dist/controllers/toast_controller.js.map +1 -1
  180. package/dist/controllers/toggle_group_controller.d.ts +2 -0
  181. package/dist/controllers/toggle_group_controller.js.map +1 -1
  182. package/dist/controllers/toolbar_controller.js.map +1 -1
  183. package/dist/controllers/transition_controller.d.ts +2 -0
  184. package/dist/controllers/transition_controller.js.map +1 -1
  185. package/dist/controllers/tree_view_controller.d.ts +2 -0
  186. package/dist/controllers/tree_view_controller.js +2 -1
  187. package/dist/controllers/tree_view_controller.js.map +1 -1
  188. package/dist/index.js +2387 -861
  189. package/dist/index.js.map +1 -1
  190. package/dist/inspector/cli.d.ts +107 -3
  191. package/dist/inspector/cli.js +192 -14
  192. package/dist/inspector/cli.js.map +1 -1
  193. package/dist/inspector/cli_bin.js +254 -46
  194. package/dist/inspector/cli_bin.js.map +1 -1
  195. package/dist/inspector/examples.json +7 -7
  196. package/dist/inspector/manifest.json +434 -44
  197. package/dist/positioning/index.d.ts +2 -0
  198. package/dist/positioning/index.js.map +1 -1
  199. package/package.json +1 -1
@@ -21,17 +21,21 @@ var AttributeLease = class {
21
21
  written: value
22
22
  });
23
23
  }
24
- if (value === null) element.removeAttribute(this.#attribute);
25
- else element.setAttribute(this.#attribute, value);
24
+ this.#reflect(element, value);
26
25
  }
27
26
  /** Returns one lease without overwriting a value subsequently authored by a consumer. */
28
27
  return(element) {
29
28
  const record = this.#records.get(element);
30
29
  if (!record) return;
31
30
  this.#records.delete(element);
32
- if (element.getAttribute(this.#attribute) !== record.written) return;
33
- if (record.original === null) element.removeAttribute(this.#attribute);
34
- else element.setAttribute(this.#attribute, record.original);
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);
35
39
  }
36
40
  /** Returns every outstanding lease using the same ownership check as {@link return}. */
37
41
  returnAll() {
@@ -39,6 +43,113 @@ var AttributeLease = class {
39
43
  }
40
44
  };
41
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
+
42
153
  // src/utils/layout_observer.ts
43
154
  var LayoutObserver = class {
44
155
  #callback;
@@ -142,32 +253,55 @@ var MicrotaskCoalescer = class {
142
253
  }
143
254
  };
144
255
 
145
- // src/utils/before_cache_reset.ts
146
- var BeforeCacheReset = class _BeforeCacheReset {
147
- /** Every subscribed instance, iterated by the one shared document listener. */
148
- static #subscribers = /* @__PURE__ */ new Set();
149
- /** The shared listener; installed while at least one instance is subscribed. */
150
- static #onBeforeCache = () => {
151
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
152
- };
153
- #rewind;
154
- /** @param rewind - the pass that returns this controller's state to its initial form. */
155
- constructor(rewind) {
156
- this.#rewind = rewind;
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;
157
263
  }
158
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
159
- activate() {
160
- const first = _BeforeCacheReset.#subscribers.size === 0;
161
- _BeforeCacheReset.#subscribers.add(this);
162
- if (first) {
163
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
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
+ });
164
277
  }
278
+ this.#reflect(element, value, priority);
165
279
  }
166
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
167
- deactivate() {
168
- _BeforeCacheReset.#subscribers.delete(this);
169
- if (_BeforeCacheReset.#subscribers.size > 0) return;
170
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
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);
171
305
  }
172
306
  };
173
307
 
@@ -203,15 +337,6 @@ var TabindexLoan = class {
203
337
  };
204
338
 
205
339
  // src/controllers/scroll_area_controller.ts
206
- var FOCUSABLE_SELECTOR = [
207
- "a[href]",
208
- "button:not([disabled])",
209
- "input:not([disabled])",
210
- "select:not([disabled])",
211
- "textarea:not([disabled])",
212
- "[tabindex]:not([tabindex='-1'])",
213
- "[contenteditable='true']"
214
- ].join(",");
215
340
  var EDGE_EPSILON = 1;
216
341
  var ScrollAreaController = class extends Controller {
217
342
  static targets = ["viewport"];
@@ -219,31 +344,60 @@ var ScrollAreaController = class extends Controller {
219
344
  orientation: { type: String, default: "vertical" }
220
345
  };
221
346
  static events = ["reach"];
222
- #layout = new LayoutObserver(() => this.#update());
223
- /** Current element receiving scroll, resize, mutation, and keyboard-reach behavior. */
224
- #viewport = null;
225
- /** Guards target callbacks before connect and after disconnect. */
226
- #connected = false;
227
- /** Collapses target replacement callbacks into one final-DOM rebind. */
347
+ #layout = new LayoutObserver(() => this.#refresh());
228
348
  #rebind = new MicrotaskCoalescer(() => this.#syncViewport());
229
- /** Re-checks the tab stop when the viewport's focusable content comes or goes. */
230
- #content = null;
231
- /** Last edge reported via `reach`, so the event fires once per arrival. */
232
- #lastEdge = null;
233
- /** Whether this controller added `tabindex`, so teardown only removes its own. */
349
+ #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
234
350
  #tabindex = new TabindexLoan("0");
235
- /** Temporarily owns a derived `role="region"` without losing an authored replacement. */
236
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;
356
+ #content = null;
357
+ #nameSources = null;
358
+ #nameGraph = null;
359
+ #observedNameIds = /* @__PURE__ */ new Set();
360
+ #observedNameSources = [];
361
+ #fonts = null;
362
+ #scrollFrame = null;
363
+ #overflowing = false;
364
+ #lastEdge = null;
365
+ #ownedHostMutations = /* @__PURE__ */ new Map();
237
366
  #onScroll = () => {
238
- 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();
239
379
  };
240
380
  connect() {
241
- this.#connected = true;
381
+ this.#beforeCache.activate();
242
382
  this.#rebind.activate();
243
383
  this.#layout.observeViewport();
384
+ this.#bindFonts();
244
385
  this.#syncViewport();
245
386
  }
246
- /** Schedules a complete observer/listener rebind for a runtime viewport target. */
387
+ disconnect() {
388
+ this.#beforeCache.deactivate();
389
+ this.#rebind.cancel();
390
+ this.#cancelScrollFrame();
391
+ if (this.#viewport) this.#unbindViewport(this.#viewport);
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. */
247
401
  viewportTargetConnected() {
248
402
  this.#rebind.schedule();
249
403
  }
@@ -251,149 +405,295 @@ var ScrollAreaController = class extends Controller {
251
405
  viewportTargetDisconnected() {
252
406
  this.#rebind.schedule();
253
407
  }
254
- /** Rebinds every viewport-owned resource against the final target in this mutation batch. */
408
+ /** Rebinds every viewport-owned resource against the final target in the mutation batch. */
255
409
  #syncViewport() {
256
- if (!this.#connected) return;
257
410
  const next = this.hasViewportTarget ? this.viewportTarget : null;
258
411
  if (next === this.#viewport) {
259
- this.#update();
412
+ if (next) this.#refresh();
413
+ else this.#clearHostState();
260
414
  return;
261
415
  }
262
416
  if (this.#viewport) this.#unbindViewport(this.#viewport);
263
417
  this.#viewport = next;
264
- if (!next) return;
418
+ if (!next) {
419
+ this.#clearHostState();
420
+ return;
421
+ }
265
422
  next.addEventListener("scroll", this.#onScroll, { passive: true });
423
+ next.addEventListener("load", this.#onLoad, true);
266
424
  this.#layout.observe(next);
267
- if (typeof MutationObserver !== "undefined") {
268
- this.#content = new MutationObserver(() => {
269
- if (this.#viewport !== next) return;
270
- this.#syncKeyboardReach(next, this.#syncOverflow(next));
271
- });
272
- this.#content.observe(next, {
273
- subtree: true,
274
- childList: true,
275
- attributes: true
276
- });
277
- }
278
- this.#update();
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
+ });
279
440
  }
280
- disconnect() {
281
- this.#connected = false;
282
- this.#rebind.cancel();
283
- if (this.#viewport) this.#unbindViewport(this.#viewport);
284
- this.#layout.disconnect();
285
- this.#lastEdge = null;
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;
286
455
  }
287
- /** Releases every resource and derived attribute owned by one former viewport. */
456
+ /** Releases every resource and borrowed attribute owned by one former viewport. */
288
457
  #unbindViewport(viewport) {
458
+ this.#cancelScrollFrame();
289
459
  viewport.removeEventListener("scroll", this.#onScroll);
460
+ viewport.removeEventListener("load", this.#onLoad, true);
290
461
  this.#layout.unobserve(viewport);
291
462
  this.#content?.disconnect();
292
463
  this.#content = null;
293
- this.#clearAddedAttributes(viewport);
294
- if (this.#viewport === viewport) this.#viewport = null;
464
+ this.#disconnectNameSources();
465
+ this.#clearViewportAttributes(viewport);
466
+ this.#ownedHostMutations.clear();
467
+ this.#viewport = null;
468
+ this.#overflowing = false;
295
469
  this.#lastEdge = null;
296
470
  }
297
- /** Re-measures overflow and scroll position and reflects the state hooks. */
298
- #update() {
299
- const vp = this.#viewport;
300
- if (!vp) return;
301
- const overflowing = this.#syncOverflow(vp);
302
- this.#syncKeyboardReach(vp, overflowing);
303
- const { position, progress } = this.#measurePosition(vp);
304
- this.element.setAttribute("data-scroll", position);
305
- this.element.style.setProperty("--stimeo--scroll-progress", String(progress));
306
- const edge = position === "start" ? "start" : position === "end" ? "end" : null;
307
- if (overflowing && edge && edge !== this.#lastEdge) {
308
- this.#lastEdge = edge;
309
- this.dispatch("reach", { detail: { edge } });
310
- } else if (!edge) {
311
- this.#lastEdge = null;
312
- }
313
- }
314
- /**
315
- * Measures overflow and reflects the `data-overflow` hook.
316
- *
317
- * The write is skipped when the value is unchanged. An identical `setAttribute` still
318
- * queues a MutationRecord, and markup that puts the viewport target on the controller
319
- * element itself would then have the content observer trigger its own next callback.
320
- */
321
- #syncOverflow(vp) {
322
- const overflowing = this.#measureOverflow(vp);
323
- const next = overflowing ? "true" : "false";
324
- if (this.element.getAttribute("data-overflow") !== next) {
325
- this.element.setAttribute("data-overflow", next);
326
- }
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);
480
+ }
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");
327
485
  return overflowing;
328
486
  }
329
- #measureOverflow(vp) {
330
- const o = this.orientationValue;
331
- const vertical = o !== "horizontal" && vp.scrollHeight > vp.clientHeight + EDGE_EPSILON;
332
- 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;
333
529
  return vertical || horizontal;
334
530
  }
335
- /**
336
- * Reports the scroll position bucket and 0–1 progress on the primary axis. For
337
- * `both`, the vertical axis is used when it overflows, otherwise the horizontal.
338
- */
339
- #measurePosition(vp) {
340
- const horizontalPrimary = this.orientationValue === "horizontal" || this.orientationValue === "both" && vp.scrollHeight <= vp.clientHeight + EDGE_EPSILON;
341
- 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
+ );
342
538
  if (maxScroll <= EDGE_EPSILON) return { position: "start", progress: 0 };
343
- const progress = Math.min(1, Math.max(0, scrollPos / maxScroll));
344
- if (scrollPos <= EDGE_EPSILON) return { position: "start", progress };
345
- 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 };
346
542
  return { position: "middle", progress };
347
543
  }
348
- /**
349
- * Makes the viewport keyboard-scrollable when it overflows and contains no
350
- * focusable elements of its own (avoiding a double tab stop). Adds `role="region"`
351
- * only when the viewport already carries an accessible name.
352
- */
353
- #syncKeyboardReach(vp, overflowing) {
354
- const wantsTabindex = overflowing && !this.#hasFocusableContent(vp);
355
- if (wantsTabindex) {
356
- this.#tabindex.lend(vp);
357
- if (!vp.hasAttribute("role") && this.#hasAccessibleName(vp)) {
358
- this.#role.write(vp, "region");
359
- }
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");
360
554
  } else {
361
- this.#clearAddedAttributes(vp);
555
+ this.#role.return(viewport);
362
556
  }
363
557
  }
364
- /** Removes (and resets the flags for) only the attributes this controller added. */
365
- #clearAddedAttributes(vp) {
558
+ /** Returns only the viewport attributes borrowed by this controller. */
559
+ #clearViewportAttributes(viewport) {
366
560
  this.#tabindex.returnAll();
367
- this.#role.return(vp);
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
+ });
571
+ }
572
+ return Boolean(viewport.getAttribute("aria-label")?.trim());
573
+ }
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;
368
587
  }
369
- /**
370
- * Whether the viewport owns something the user can Tab to *right now*.
371
- *
372
- * The selector alone is not enough: a `display: none` button still matches it,
373
- * so a viewport whose only control is revealed on demand would never get a tab
374
- * stop — leaving it unreachable by keyboard exactly while it has nothing else to
375
- * offer. Only rendered candidates count.
376
- */
377
- #hasFocusableContent(vp) {
378
- return Array.from(vp.querySelectorAll(FOCUSABLE_SELECTOR)).some(
379
- (el) => this.#isRendered(el)
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)
380
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
+ }
381
629
  }
382
- /**
383
- * Whether `el` is actually rendered, and so can hold focus.
384
- *
385
- * `checkVisibility()` answers this for every way CSS can remove a box, including
386
- * a class-driven `display: none` that no attribute reveals. The `hidden` walk in
387
- * front of it is not redundant: it is the one case a DOM-only environment with no
388
- * layout engine has to be told about explicitly.
389
- */
390
- #isRendered(el) {
391
- if (el.closest("[hidden]") !== null) return false;
392
- const check = el.checkVisibility;
393
- 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;
650
+ }
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
+ );
394
658
  }
395
- #hasAccessibleName(vp) {
396
- return vp.hasAttribute("aria-label") || vp.hasAttribute("aria-labelledby");
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();
397
697
  }
398
698
  };
399
699