stimeo-ui 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +8 -1
  3. package/dist/cable/index.d.ts +79 -23
  4. package/dist/cable/index.js +363 -436
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/controllers/accordion_controller.d.ts +4 -2
  7. package/dist/controllers/accordion_controller.js +166 -55
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  10. package/dist/controllers/alert_dialog_controller.js +862 -221
  11. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  12. package/dist/controllers/announcer_controller.d.ts +35 -5
  13. package/dist/controllers/announcer_controller.js +135 -198
  14. package/dist/controllers/announcer_controller.js.map +1 -1
  15. package/dist/controllers/aspect_ratio_controller.js +0 -10
  16. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  17. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  18. package/dist/controllers/auto_submit_controller.js +271 -127
  19. package/dist/controllers/auto_submit_controller.js.map +1 -1
  20. package/dist/controllers/avatar_controller.js +171 -105
  21. package/dist/controllers/avatar_controller.js.map +1 -1
  22. package/dist/controllers/breadcrumb_controller.js +0 -130
  23. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  24. package/dist/controllers/bulk_select_controller.d.ts +18 -0
  25. package/dist/controllers/bulk_select_controller.js +262 -52
  26. package/dist/controllers/bulk_select_controller.js.map +1 -1
  27. package/dist/controllers/calendar_controller.d.ts +24 -5
  28. package/dist/controllers/calendar_controller.js +175 -209
  29. package/dist/controllers/calendar_controller.js.map +1 -1
  30. package/dist/controllers/carousel_controller.d.ts +34 -7
  31. package/dist/controllers/carousel_controller.js +461 -399
  32. package/dist/controllers/carousel_controller.js.map +1 -1
  33. package/dist/controllers/character_counter_controller.d.ts +17 -1
  34. package/dist/controllers/character_counter_controller.js +246 -156
  35. package/dist/controllers/character_counter_controller.js.map +1 -1
  36. package/dist/controllers/checkbox_controller.d.ts +7 -2
  37. package/dist/controllers/checkbox_controller.js +32 -74
  38. package/dist/controllers/checkbox_controller.js.map +1 -1
  39. package/dist/controllers/clipboard_controller.d.ts +16 -5
  40. package/dist/controllers/clipboard_controller.js +168 -147
  41. package/dist/controllers/clipboard_controller.js.map +1 -1
  42. package/dist/controllers/collapsible_controller.d.ts +16 -4
  43. package/dist/controllers/collapsible_controller.js +301 -120
  44. package/dist/controllers/collapsible_controller.js.map +1 -1
  45. package/dist/controllers/color_picker_controller.d.ts +18 -6
  46. package/dist/controllers/color_picker_controller.js +149 -139
  47. package/dist/controllers/color_picker_controller.js.map +1 -1
  48. package/dist/controllers/combobox_controller.d.ts +23 -5
  49. package/dist/controllers/combobox_controller.js +360 -133
  50. package/dist/controllers/combobox_controller.js.map +1 -1
  51. package/dist/controllers/command_palette_controller.d.ts +45 -14
  52. package/dist/controllers/command_palette_controller.js +970 -358
  53. package/dist/controllers/command_palette_controller.js.map +1 -1
  54. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  55. package/dist/controllers/conditional_fields_controller.js +166 -124
  56. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  57. package/dist/controllers/confirm_controller.d.ts +49 -12
  58. package/dist/controllers/confirm_controller.js +939 -248
  59. package/dist/controllers/confirm_controller.js.map +1 -1
  60. package/dist/controllers/context_menu_controller.d.ts +31 -4
  61. package/dist/controllers/context_menu_controller.js +256 -128
  62. package/dist/controllers/context_menu_controller.js.map +1 -1
  63. package/dist/controllers/count_up_controller.d.ts +13 -1
  64. package/dist/controllers/count_up_controller.js +73 -21
  65. package/dist/controllers/count_up_controller.js.map +1 -1
  66. package/dist/controllers/countdown_controller.d.ts +45 -1
  67. package/dist/controllers/countdown_controller.js +183 -148
  68. package/dist/controllers/countdown_controller.js.map +1 -1
  69. package/dist/controllers/currency_input_controller.d.ts +21 -2
  70. package/dist/controllers/currency_input_controller.js +134 -158
  71. package/dist/controllers/currency_input_controller.js.map +1 -1
  72. package/dist/controllers/data_grid_controller.d.ts +15 -4
  73. package/dist/controllers/data_grid_controller.js +114 -186
  74. package/dist/controllers/data_grid_controller.js.map +1 -1
  75. package/dist/controllers/date_range_picker_controller.d.ts +30 -5
  76. package/dist/controllers/date_range_picker_controller.js +288 -206
  77. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  78. package/dist/controllers/dialog_controller.d.ts +13 -1
  79. package/dist/controllers/dialog_controller.js +858 -223
  80. package/dist/controllers/dialog_controller.js.map +1 -1
  81. package/dist/controllers/direct_upload_controller.d.ts +24 -7
  82. package/dist/controllers/direct_upload_controller.js +73 -153
  83. package/dist/controllers/direct_upload_controller.js.map +1 -1
  84. package/dist/controllers/dirty_form_controller.js +14 -48
  85. package/dist/controllers/dirty_form_controller.js.map +1 -1
  86. package/dist/controllers/dismissible_controller.d.ts +19 -0
  87. package/dist/controllers/dismissible_controller.js +175 -21
  88. package/dist/controllers/dismissible_controller.js.map +1 -1
  89. package/dist/controllers/drawer_controller.d.ts +39 -11
  90. package/dist/controllers/drawer_controller.js +933 -341
  91. package/dist/controllers/drawer_controller.js.map +1 -1
  92. package/dist/controllers/dropdown_controller.d.ts +31 -1
  93. package/dist/controllers/dropdown_controller.js +312 -82
  94. package/dist/controllers/dropdown_controller.js.map +1 -1
  95. package/dist/controllers/editable_controller.d.ts +12 -1
  96. package/dist/controllers/editable_controller.js +158 -68
  97. package/dist/controllers/editable_controller.js.map +1 -1
  98. package/dist/controllers/empty_state_controller.js +67 -53
  99. package/dist/controllers/empty_state_controller.js.map +1 -1
  100. package/dist/controllers/file_dropzone_controller.d.ts +60 -18
  101. package/dist/controllers/file_dropzone_controller.js +311 -207
  102. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  103. package/dist/controllers/filter_controller.js +67 -39
  104. package/dist/controllers/filter_controller.js.map +1 -1
  105. package/dist/controllers/flash_controller.d.ts +31 -8
  106. package/dist/controllers/flash_controller.js +172 -314
  107. package/dist/controllers/flash_controller.js.map +1 -1
  108. package/dist/controllers/focus_controller.d.ts +12 -3
  109. package/dist/controllers/focus_controller.js +654 -240
  110. package/dist/controllers/focus_controller.js.map +1 -1
  111. package/dist/controllers/form_field_controller.js +147 -132
  112. package/dist/controllers/form_field_controller.js.map +1 -1
  113. package/dist/controllers/form_validation_controller.js +13 -86
  114. package/dist/controllers/form_validation_controller.js.map +1 -1
  115. package/dist/controllers/frame_loading_controller.d.ts +41 -9
  116. package/dist/controllers/frame_loading_controller.js +264 -213
  117. package/dist/controllers/frame_loading_controller.js.map +1 -1
  118. package/dist/controllers/highlight_controller.d.ts +22 -2
  119. package/dist/controllers/highlight_controller.js +107 -75
  120. package/dist/controllers/highlight_controller.js.map +1 -1
  121. package/dist/controllers/hover_card_controller.d.ts +56 -3
  122. package/dist/controllers/hover_card_controller.js +309 -118
  123. package/dist/controllers/hover_card_controller.js.map +1 -1
  124. package/dist/controllers/idle_controller.d.ts +42 -1
  125. package/dist/controllers/idle_controller.js +267 -101
  126. package/dist/controllers/idle_controller.js.map +1 -1
  127. package/dist/controllers/input_mask_controller.js +71 -72
  128. package/dist/controllers/input_mask_controller.js.map +1 -1
  129. package/dist/controllers/intersection_controller.d.ts +32 -13
  130. package/dist/controllers/intersection_controller.js +147 -106
  131. package/dist/controllers/intersection_controller.js.map +1 -1
  132. package/dist/controllers/lazy_frame_controller.d.ts +16 -0
  133. package/dist/controllers/lazy_frame_controller.js +9 -65
  134. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  135. package/dist/controllers/listbox_controller.d.ts +34 -2
  136. package/dist/controllers/listbox_controller.js +306 -204
  137. package/dist/controllers/listbox_controller.js.map +1 -1
  138. package/dist/controllers/local_time_controller.d.ts +1 -1
  139. package/dist/controllers/local_time_controller.js +54 -64
  140. package/dist/controllers/local_time_controller.js.map +1 -1
  141. package/dist/controllers/masonry_controller.d.ts +11 -3
  142. package/dist/controllers/masonry_controller.js +70 -93
  143. package/dist/controllers/masonry_controller.js.map +1 -1
  144. package/dist/controllers/menu_controller.d.ts +32 -7
  145. package/dist/controllers/menu_controller.js +214 -156
  146. package/dist/controllers/menu_controller.js.map +1 -1
  147. package/dist/controllers/menubar_controller.d.ts +4 -4
  148. package/dist/controllers/menubar_controller.js +170 -329
  149. package/dist/controllers/menubar_controller.js.map +1 -1
  150. package/dist/controllers/meter_controller.d.ts +20 -0
  151. package/dist/controllers/meter_controller.js +151 -67
  152. package/dist/controllers/meter_controller.js.map +1 -1
  153. package/dist/controllers/multi_select_controller.d.ts +32 -4
  154. package/dist/controllers/multi_select_controller.js +593 -358
  155. package/dist/controllers/multi_select_controller.js.map +1 -1
  156. package/dist/controllers/navigation_menu_controller.d.ts +8 -1
  157. package/dist/controllers/navigation_menu_controller.js +124 -232
  158. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  159. package/dist/controllers/nested_form_controller.d.ts +10 -0
  160. package/dist/controllers/nested_form_controller.js +352 -182
  161. package/dist/controllers/nested_form_controller.js.map +1 -1
  162. package/dist/controllers/network_status_controller.d.ts +23 -0
  163. package/dist/controllers/network_status_controller.js +242 -54
  164. package/dist/controllers/network_status_controller.js.map +1 -1
  165. package/dist/controllers/number_input_controller.d.ts +26 -2
  166. package/dist/controllers/number_input_controller.js +320 -263
  167. package/dist/controllers/number_input_controller.js.map +1 -1
  168. package/dist/controllers/optimistic_controller.js +16 -76
  169. package/dist/controllers/optimistic_controller.js.map +1 -1
  170. package/dist/controllers/otp_controller.d.ts +20 -4
  171. package/dist/controllers/otp_controller.js +198 -193
  172. package/dist/controllers/otp_controller.js.map +1 -1
  173. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  174. package/dist/controllers/overflow_indicator_controller.js +116 -113
  175. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  176. package/dist/controllers/overflow_menu_controller.d.ts +48 -11
  177. package/dist/controllers/overflow_menu_controller.js +343 -283
  178. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  179. package/dist/controllers/pagination_controller.d.ts +35 -6
  180. package/dist/controllers/pagination_controller.js +295 -119
  181. package/dist/controllers/pagination_controller.js.map +1 -1
  182. package/dist/controllers/password_reveal_controller.d.ts +45 -8
  183. package/dist/controllers/password_reveal_controller.js +389 -121
  184. package/dist/controllers/password_reveal_controller.js.map +1 -1
  185. package/dist/controllers/password_strength_controller.d.ts +14 -5
  186. package/dist/controllers/password_strength_controller.js +99 -153
  187. package/dist/controllers/password_strength_controller.js.map +1 -1
  188. package/dist/controllers/persist_controller.d.ts +11 -4
  189. package/dist/controllers/persist_controller.js +237 -103
  190. package/dist/controllers/persist_controller.js.map +1 -1
  191. package/dist/controllers/pointer_drag_controller.d.ts +19 -7
  192. package/dist/controllers/pointer_drag_controller.js +185 -165
  193. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  194. package/dist/controllers/popover_controller.d.ts +38 -2
  195. package/dist/controllers/popover_controller.js +236 -97
  196. package/dist/controllers/popover_controller.js.map +1 -1
  197. package/dist/controllers/portal_controller.d.ts +44 -4
  198. package/dist/controllers/portal_controller.js +209 -76
  199. package/dist/controllers/portal_controller.js.map +1 -1
  200. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  201. package/dist/controllers/preview_guard_controller.js +232 -116
  202. package/dist/controllers/preview_guard_controller.js.map +1 -1
  203. package/dist/controllers/progress_controller.d.ts +11 -0
  204. package/dist/controllers/progress_controller.js +125 -56
  205. package/dist/controllers/progress_controller.js.map +1 -1
  206. package/dist/controllers/radio_group_controller.d.ts +14 -5
  207. package/dist/controllers/radio_group_controller.js +117 -137
  208. package/dist/controllers/radio_group_controller.js.map +1 -1
  209. package/dist/controllers/range_slider_controller.d.ts +43 -3
  210. package/dist/controllers/range_slider_controller.js +192 -105
  211. package/dist/controllers/range_slider_controller.js.map +1 -1
  212. package/dist/controllers/rating_controller.d.ts +20 -4
  213. package/dist/controllers/rating_controller.js +418 -207
  214. package/dist/controllers/rating_controller.js.map +1 -1
  215. package/dist/controllers/read_more_controller.d.ts +2 -0
  216. package/dist/controllers/read_more_controller.js +134 -63
  217. package/dist/controllers/read_more_controller.js.map +1 -1
  218. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  219. package/dist/controllers/reading_progress_controller.js +147 -135
  220. package/dist/controllers/reading_progress_controller.js.map +1 -1
  221. package/dist/controllers/relative_time_controller.d.ts +18 -4
  222. package/dist/controllers/relative_time_controller.js +145 -110
  223. package/dist/controllers/relative_time_controller.js.map +1 -1
  224. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  225. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  226. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  227. package/dist/controllers/resizable_controller.d.ts +28 -2
  228. package/dist/controllers/resizable_controller.js +255 -139
  229. package/dist/controllers/resizable_controller.js.map +1 -1
  230. package/dist/controllers/roving_controller.js +39 -84
  231. package/dist/controllers/roving_controller.js.map +1 -1
  232. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  233. package/dist/controllers/scroll_area_controller.js +428 -225
  234. package/dist/controllers/scroll_area_controller.js.map +1 -1
  235. package/dist/controllers/scroll_restore_controller.js +0 -53
  236. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  237. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  238. package/dist/controllers/scroll_visibility_controller.js +395 -145
  239. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  240. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  241. package/dist/controllers/scrollspy_controller.js +181 -242
  242. package/dist/controllers/scrollspy_controller.js.map +1 -1
  243. package/dist/controllers/separator_controller.d.ts +19 -4
  244. package/dist/controllers/separator_controller.js +267 -140
  245. package/dist/controllers/separator_controller.js.map +1 -1
  246. package/dist/controllers/sidebar_controller.d.ts +64 -7
  247. package/dist/controllers/sidebar_controller.js +1023 -386
  248. package/dist/controllers/sidebar_controller.js.map +1 -1
  249. package/dist/controllers/skeleton_controller.d.ts +23 -0
  250. package/dist/controllers/skeleton_controller.js +248 -97
  251. package/dist/controllers/skeleton_controller.js.map +1 -1
  252. package/dist/controllers/slider_controller.d.ts +27 -1
  253. package/dist/controllers/slider_controller.js +159 -79
  254. package/dist/controllers/slider_controller.js.map +1 -1
  255. package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
  256. package/dist/controllers/smart_sticky_header_controller.js +162 -63
  257. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  258. package/dist/controllers/sortable_controller.js +0 -112
  259. package/dist/controllers/sortable_controller.js.map +1 -1
  260. package/dist/controllers/spinner_controller.d.ts +39 -4
  261. package/dist/controllers/spinner_controller.js +277 -169
  262. package/dist/controllers/spinner_controller.js.map +1 -1
  263. package/dist/controllers/step_indicator_controller.d.ts +5 -0
  264. package/dist/controllers/step_indicator_controller.js +99 -46
  265. package/dist/controllers/step_indicator_controller.js.map +1 -1
  266. package/dist/controllers/stepper_controller.d.ts +12 -3
  267. package/dist/controllers/stepper_controller.js +114 -73
  268. package/dist/controllers/stepper_controller.js.map +1 -1
  269. package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
  270. package/dist/controllers/stick_to_bottom_controller.js +262 -138
  271. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  272. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  273. package/dist/controllers/sticky_observer_controller.js +214 -49
  274. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  275. package/dist/controllers/submit_once_controller.d.ts +23 -8
  276. package/dist/controllers/submit_once_controller.js +294 -205
  277. package/dist/controllers/submit_once_controller.js.map +1 -1
  278. package/dist/controllers/switch_controller.d.ts +12 -1
  279. package/dist/controllers/switch_controller.js +33 -60
  280. package/dist/controllers/switch_controller.js.map +1 -1
  281. package/dist/controllers/tabs_controller.d.ts +2 -2
  282. package/dist/controllers/tabs_controller.js +35 -29
  283. package/dist/controllers/tabs_controller.js.map +1 -1
  284. package/dist/controllers/tags_input_controller.d.ts +12 -0
  285. package/dist/controllers/tags_input_controller.js +136 -145
  286. package/dist/controllers/tags_input_controller.js.map +1 -1
  287. package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
  288. package/dist/controllers/textarea_autosize_controller.js +71 -58
  289. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  290. package/dist/controllers/theme_controller.d.ts +21 -9
  291. package/dist/controllers/theme_controller.js +130 -112
  292. package/dist/controllers/theme_controller.js.map +1 -1
  293. package/dist/controllers/time_picker_controller.d.ts +18 -1
  294. package/dist/controllers/time_picker_controller.js +146 -80
  295. package/dist/controllers/time_picker_controller.js.map +1 -1
  296. package/dist/controllers/toast_controller.d.ts +36 -11
  297. package/dist/controllers/toast_controller.js +186 -297
  298. package/dist/controllers/toast_controller.js.map +1 -1
  299. package/dist/controllers/toggle_group_controller.d.ts +12 -3
  300. package/dist/controllers/toggle_group_controller.js +95 -136
  301. package/dist/controllers/toggle_group_controller.js.map +1 -1
  302. package/dist/controllers/toolbar_controller.js +68 -98
  303. package/dist/controllers/toolbar_controller.js.map +1 -1
  304. package/dist/controllers/tooltip_controller.d.ts +45 -3
  305. package/dist/controllers/tooltip_controller.js +158 -124
  306. package/dist/controllers/tooltip_controller.js.map +1 -1
  307. package/dist/controllers/transition_controller.d.ts +22 -4
  308. package/dist/controllers/transition_controller.js +123 -119
  309. package/dist/controllers/transition_controller.js.map +1 -1
  310. package/dist/controllers/tree_view_controller.d.ts +21 -11
  311. package/dist/controllers/tree_view_controller.js +150 -345
  312. package/dist/controllers/tree_view_controller.js.map +1 -1
  313. package/dist/index.d.ts +8 -5
  314. package/dist/index.js +6370 -9346
  315. package/dist/index.js.map +1 -1
  316. package/dist/inspector/cli.d.ts +23 -3
  317. package/dist/inspector/cli.js +92 -37
  318. package/dist/inspector/cli.js.map +1 -1
  319. package/dist/inspector/cli_bin.js +92 -56
  320. package/dist/inspector/cli_bin.js.map +1 -1
  321. package/dist/inspector/examples.json +8 -8
  322. package/dist/inspector/manifest.json +787 -100
  323. package/dist/positioning/index.d.ts +8 -0
  324. package/dist/positioning/index.js +148 -47
  325. package/dist/positioning/index.js.map +1 -1
  326. package/package.json +6 -6
  327. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  328. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,13 +1,31 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/carousel_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
4
24
 
5
- // src/utils/logical_scroll.ts
6
25
  function isRtl(element) {
7
26
  return window.getComputedStyle(element).direction === "rtl";
8
27
  }
9
28
 
10
- // src/utils/arrow_step.ts
11
29
  function logicalArrowStep(key, element) {
12
30
  if (key === "ArrowDown") return 1;
13
31
  if (key === "ArrowUp") return -1;
@@ -20,182 +38,222 @@ function isReservedArrowChord(event, allow = []) {
20
38
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
21
39
  }
22
40
 
23
- // src/utils/attribute_lease.ts
24
- var AttributeLease = class {
41
+ function sharedRegistry(key, create) {
42
+ const symbol = Symbol.for(key);
43
+ const scope = globalThis;
44
+ const existing = scope[symbol];
45
+ if (existing !== void 0) return existing;
46
+ const registry = create();
47
+ Object.defineProperty(globalThis, symbol, { value: registry });
48
+ return registry;
49
+ }
50
+
51
+ function isAttributeValue(value) {
52
+ return value === null || typeof value === "string";
53
+ }
54
+ function readLeaseRecord(element, record) {
55
+ try {
56
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
57
+ if (!Array.isArray(parsed)) return void 0;
58
+ const [original, written, ...rest] = parsed;
59
+ const linked = rest[0] !== false;
60
+ const beneath = linked ? rest : rest.slice(1);
61
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
62
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
63
+ } catch {
64
+ return void 0;
65
+ }
66
+ }
67
+ var AttributeLease = class _AttributeLease {
68
+ static #registry = sharedRegistry(
69
+ "stimeo-ui.attribute-lease.registry.v1",
70
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
71
+ );
25
72
  #attribute;
26
- #records = /* @__PURE__ */ new Map();
27
- /** @param attribute - The attribute whose temporary values this lease owns. */
28
- constructor(attribute) {
73
+ #record;
74
+ #base;
75
+ #held = /* @__PURE__ */ new Set();
76
+ constructor(attribute, owner, options = {}) {
29
77
  this.#attribute = attribute;
78
+ this.#record = `data-${owner}-${attribute}-lease`;
79
+ this.#base = options.base === true;
80
+ const names = _AttributeLease.#registry.names;
81
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
30
82
  }
31
- /** Writes or removes the leased attribute while preserving its authored value. */
32
83
  write(element, value) {
33
- const existing = this.#records.get(element);
34
- if (existing) {
35
- existing.written = value;
36
- } else {
37
- this.#records.set(element, {
38
- original: element.getAttribute(this.#attribute),
39
- written: value
40
- });
41
- }
42
- this.#reflect(element, value);
84
+ const stack = this.#stack(element);
85
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
86
+ this.#held.add(element);
87
+ const shows = !this.#covered(element, stack, hold);
88
+ hold.written = value;
89
+ this.#mark(element, stack);
90
+ if (shows) this.#reflect(element, value);
43
91
  }
44
- /** Returns one lease without overwriting a value subsequently authored by a consumer. */
45
92
  return(element) {
46
- const record = this.#records.get(element);
47
- if (!record) return;
48
- this.#records.delete(element);
49
- const stillOwned = element.getAttribute(this.#attribute) === record.written;
50
- if (stillOwned) this.#reflect(element, record.original);
93
+ const held = this.#held.delete(element);
94
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
95
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
96
+ const hold = this.#take(stack);
97
+ if (!hold) return false;
98
+ const carries = element.getAttribute(this.#attribute) === hold.written;
99
+ const owns = carries && !this.#covered(element, stack, hold);
100
+ const index = stack.indexOf(hold);
101
+ stack.splice(index, 1);
102
+ const above = stack[index];
103
+ if (above) above.linked &&= hold.linked;
104
+ this.#mark(element, stack);
105
+ if (!owns) return true;
106
+ const below = hold.linked ? stack[index - 1] : void 0;
107
+ this.#reflect(element, below ? below.written : hold.original);
108
+ return true;
109
+ }
110
+ returnAll() {
111
+ for (const element of Array.from(this.#held)) this.return(element);
112
+ }
113
+ #stack(element) {
114
+ const stacks = _AttributeLease.#registry.stacks;
115
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
116
+ stacks.set(element, byAttribute);
117
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
118
+ byAttribute.set(this.#attribute, stack);
119
+ return stack;
120
+ }
121
+ #seed(element) {
122
+ const found = [];
123
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
124
+ const read = readLeaseRecord(element, record);
125
+ if (read) found.push({ ...read, record });
126
+ }
127
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
128
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
129
+ lease: null,
130
+ record,
131
+ written,
132
+ original,
133
+ linked: linked && index > 0
134
+ }));
135
+ }
136
+ #take(stack) {
137
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
138
+ if (hold) hold.lease = this;
139
+ return hold;
140
+ }
141
+ #acquire(element, stack) {
142
+ const current = element.getAttribute(this.#attribute);
143
+ const top = stack.at(-1);
144
+ const bottom = stack[0];
145
+ const over = top?.written === current ? top : void 0;
146
+ const from = this.#base ? bottom : over;
147
+ const hold = {
148
+ lease: this,
149
+ record: this.#record,
150
+ written: current,
151
+ original: from ? from.original : current,
152
+ linked: !this.#base && over !== void 0
153
+ };
154
+ if (this.#base && bottom) {
155
+ bottom.linked = true;
156
+ stack.unshift(hold);
157
+ } else stack.push(hold);
158
+ return hold;
159
+ }
160
+ #covered(element, stack, hold) {
161
+ const current = element.getAttribute(this.#attribute);
162
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
163
+ }
164
+ #mark(element, stack) {
165
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
166
+ stack.forEach((hold, index) => {
167
+ const names = stack.slice(0, index).map((under) => under.record);
168
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
169
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
170
+ records.set(
171
+ hold.record,
172
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
173
+ );
174
+ });
175
+ for (const [name, recorded] of records) {
176
+ if (element.getAttribute(name) === recorded) continue;
177
+ if (recorded === null) element.removeAttribute(name);
178
+ else element.setAttribute(name, recorded);
179
+ }
51
180
  }
52
- /** Reflects only a real value transition, avoiding self-triggered mutation work. */
53
181
  #reflect(element, value) {
54
182
  if (element.getAttribute(this.#attribute) === value) return;
55
183
  if (value === null) element.removeAttribute(this.#attribute);
56
184
  else element.setAttribute(this.#attribute, value);
57
185
  }
58
- /** Returns every outstanding lease using the same ownership check as {@link return}. */
59
- returnAll() {
60
- for (const element of Array.from(this.#records.keys())) this.return(element);
61
- }
62
- };
63
-
64
- // src/utils/before_cache_reset.ts
65
- var BeforeCacheReset = class _BeforeCacheReset {
66
- /** Every subscribed instance, iterated by the one shared document listener. */
67
- static #subscribers = /* @__PURE__ */ new Set();
68
- /** The shared listener; installed while at least one instance is subscribed. */
69
- static #onBeforeCache = () => {
70
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
71
- };
72
- #rewind;
73
- /** @param rewind - the pass that returns this controller's state to its initial form. */
74
- constructor(rewind) {
75
- this.#rewind = rewind;
76
- }
77
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
78
- activate() {
79
- const first = _BeforeCacheReset.#subscribers.size === 0;
80
- _BeforeCacheReset.#subscribers.add(this);
81
- if (first) {
82
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
83
- }
84
- }
85
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
86
- deactivate() {
87
- _BeforeCacheReset.#subscribers.delete(this);
88
- if (_BeforeCacheReset.#subscribers.size > 0) return;
89
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
90
- }
91
186
  };
92
187
 
93
- // src/utils/detach_gate.ts
94
188
  var DetachGate = class _DetachGate {
95
- /** Set while a probe is queued, waiting for a reconnect to cancel it. */
96
189
  #pending = false;
97
- /**
98
- * True while a probe is queued — the last disconnect was ambiguous and no
99
- * reconnect has cancelled it yet. Read it from `connect()` to tell the
100
- * reconnect half of an in-page move from a first connect: a controller whose
101
- * initialisation restarts a measurement (a min-duration floor, an elapsed
102
- * counter) must skip it for the move, where nothing actually restarted.
103
- */
104
190
  get pending() {
105
191
  return this.#pending;
106
192
  }
107
- /**
108
- * True when the disconnect is definitely a real detach — the element left
109
- * the document, or `data-controller` no longer lists the identifier. False
110
- * means ambiguous (in-page move or observed-root exit), NOT "alive".
111
- */
112
193
  static isDetached(host) {
113
- if (!host.element.isConnected) return true;
114
- const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
115
- return !tokens.includes(host.identifier);
116
- }
117
- /**
118
- * Call from `disconnect()`: runs `teardown` synchronously on a definite
119
- * detach (fast path), otherwise defers it one microtask — a reconnect
120
- * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.
121
- * One microtask is the whole probe window: Stimulus reconnects a moved
122
- * element within the same mutation batch, before the checkpoint drains.
123
- */
194
+ return !host.element.isConnected || !_DetachGate.#listed(host);
195
+ }
124
196
  disconnected(host, teardown) {
125
- if (_DetachGate.isDetached(host)) {
197
+ const carried = _DetachGate.#carried(host);
198
+ if (!carried && _DetachGate.isDetached(host)) {
126
199
  this.#pending = false;
127
200
  teardown();
128
201
  return;
129
202
  }
130
203
  this.#pending = true;
131
- queueMicrotask(() => {
204
+ const probe = () => {
132
205
  if (!this.#pending) return;
133
206
  this.#pending = false;
134
207
  teardown();
135
- });
208
+ };
209
+ if (carried) setTimeout(probe, 0);
210
+ else queueMicrotask(probe);
211
+ }
212
+ static #carried(host) {
213
+ const element = host.element;
214
+ return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
215
+ }
216
+ static #listed(host) {
217
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
218
+ return tokens.includes(host.identifier);
136
219
  }
137
- /**
138
- * Disarms a pending probe. Call from `connect()` (the reconnect that proves
139
- * an in-page move) and from the head of any teardown path not routed through
140
- * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can
141
- * never run the teardown a second time.
142
- */
143
220
  cancel() {
144
221
  this.#pending = false;
145
222
  }
146
223
  };
147
224
 
148
- // src/utils/event_owner.ts
149
225
  function ownerIndex(candidates, node) {
150
226
  if (!(node instanceof Node)) return -1;
151
227
  return candidates.findIndex((candidate) => candidate.contains(node));
152
228
  }
153
229
 
154
- // src/utils/listener_set.ts
155
230
  var ListenerSet = class {
156
- /** The generation every `add` joins until the next `dispose()`. */
157
231
  #abort = new AbortController();
158
- /**
159
- * Attaches `handler` to the open generation, exactly as the caller spelled it.
160
- *
161
- * The set supplies the signal, so `options` carries everything else the DOM
162
- * accepts — `capture` included, which has to match at release time and no
163
- * longer has a second place to drift from.
164
- */
165
232
  add(target, type, handler, options) {
166
233
  target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
167
234
  }
168
- /**
169
- * Releases every listener of the open generation, synchronously, and opens the
170
- * next one. Idempotent, and safe before anything has been added.
171
- */
172
235
  dispose() {
173
236
  this.#abort.abort();
174
237
  this.#abort = new AbortController();
175
238
  }
176
239
  };
177
240
 
178
- // src/utils/microtask_coalescer.ts
179
241
  var MicrotaskCoalescer = class {
180
242
  #run;
181
243
  #queued = false;
182
244
  #active = false;
183
245
  #generation = 0;
184
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
185
246
  constructor(run) {
186
247
  this.#run = run;
187
248
  }
188
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
189
249
  activate() {
190
250
  this.#active = true;
191
251
  }
192
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
193
252
  cancel() {
194
253
  this.#active = false;
195
254
  this.#queued = false;
196
255
  this.#generation += 1;
197
256
  }
198
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
199
257
  schedule() {
200
258
  if (!this.#active || this.#queued) return;
201
259
  this.#queued = true;
@@ -208,36 +266,101 @@ var MicrotaskCoalescer = class {
208
266
  }
209
267
  };
210
268
 
211
- // src/utils/reduced_motion.ts
269
+ var MorphRenderWatcher = class {
270
+ #listeners = new ListenerSet();
271
+ #pass;
272
+ #element = null;
273
+ #onMorph = () => this.#pass.schedule();
274
+ constructor(run) {
275
+ this.#pass = new MicrotaskCoalescer(run);
276
+ }
277
+ observe(element) {
278
+ if (this.#element === element) return;
279
+ this.disconnect();
280
+ this.#element = element;
281
+ this.#pass.activate();
282
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
283
+ }
284
+ schedule() {
285
+ this.#pass.schedule();
286
+ }
287
+ disconnect() {
288
+ this.#listeners.dispose();
289
+ this.#pass.cancel();
290
+ this.#element = null;
291
+ }
292
+ };
293
+
294
+ var MoveCounter = class {
295
+ #sequence = 0;
296
+ record() {
297
+ this.#sequence += 1;
298
+ return this.#sequence;
299
+ }
300
+ isLatest(token) {
301
+ return token === this.#sequence;
302
+ }
303
+ };
304
+
305
+ var MAX_TIMER_DELAY_MS = 2147483647;
306
+
307
+ var NUMBER_BOUNDS = {
308
+ positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS }
309
+ };
310
+ function matchesNumberBounds(value, bounds) {
311
+ if (!Number.isFinite(value)) {
312
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
313
+ if (direction === null) return false;
314
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
315
+ }
316
+ if (bounds.min !== void 0 && value < bounds.min) return false;
317
+ if (bounds.max !== void 0 && value > bounds.max) return false;
318
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
319
+ if (bounds.integer && !Number.isInteger(value)) return false;
320
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
321
+ return true;
322
+ }
323
+
324
+ function readNumber(raw, fallback, bounds) {
325
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
326
+ }
327
+
328
+ var NumberValueReader = class {
329
+ #lastRejected = /* @__PURE__ */ new Map();
330
+ read(owner, name, raw, fallback, bounds) {
331
+ const resolved = readNumber(raw, fallback, bounds);
332
+ if (matchesNumberBounds(raw, bounds)) {
333
+ this.#lastRejected.delete(name);
334
+ return resolved;
335
+ }
336
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
337
+ const literal = owner.element.getAttribute(attribute);
338
+ if (literal === null) {
339
+ this.#lastRejected.delete(name);
340
+ return resolved;
341
+ }
342
+ if (this.#lastRejected.get(name) !== literal) {
343
+ this.#lastRejected.set(name, literal);
344
+ console.warn(
345
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
346
+ );
347
+ }
348
+ return resolved;
349
+ }
350
+ };
351
+
212
352
  function prefersReducedMotion() {
213
353
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
214
354
  }
215
355
 
216
- // src/utils/roving_tabindex.ts
217
356
  var RovingTabindex = class {
218
- /** Returns the current ordered item elements; called on every operation. */
219
357
  #getItems;
220
- /**
221
- * @param getItems - Returns the current ordered item elements. Called on every
222
- * operation so the live target list is always used.
223
- */
224
358
  constructor(getItems) {
225
359
  this.#getItems = getItems;
226
360
  }
227
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
228
361
  get activeIndex() {
229
362
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
230
363
  }
231
- /**
232
- * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
233
- * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
234
- * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
235
- * "nothing is currently tabbable".
236
- *
237
- * @param index - Position of the item to make tabbable.
238
- * @param options - Pass `{ focus: true }` to also move DOM focus to that item,
239
- * and `items` to reuse an event-scoped collection snapshot.
240
- */
241
364
  setActive(index, options = {}) {
242
365
  const { focus = false } = options;
243
366
  const items = options.items ?? this.#getItems();
@@ -253,38 +376,24 @@ function rovingMove(current, length, delta, wrap) {
253
376
  return (next + length) % length;
254
377
  }
255
378
 
256
- // src/utils/safe_timeout.ts
257
379
  var TimerRegistry = class {
258
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
259
380
  ids = /* @__PURE__ */ new Set();
260
- /**
261
- * Cancels a single tracked timer.
262
- *
263
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
264
- * registry), so callers can clear defensively without guarding.
265
- */
266
381
  clear(id) {
267
382
  if (this.ids.delete(id)) {
268
383
  this.cancel(id);
269
384
  }
270
385
  }
271
- /**
272
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
273
- * to guarantee no timer outlives the element.
274
- */
275
386
  clearAll() {
276
387
  for (const id of this.ids) {
277
388
  this.cancel(id);
278
389
  }
279
390
  this.ids.clear();
280
391
  }
281
- /** Number of timers currently tracked (pending). */
282
392
  get size() {
283
393
  return this.ids.size;
284
394
  }
285
395
  };
286
396
  var SafeInterval = class extends TimerRegistry {
287
- /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
288
397
  set(callback, delay) {
289
398
  const id = this.schedule(callback, delay);
290
399
  this.ids.add(id);
@@ -298,16 +407,20 @@ var SafeInterval = class extends TimerRegistry {
298
407
  }
299
408
  };
300
409
 
301
- // src/utils/state_regions.ts
302
- var StateRegions = class {
410
+ var StateRegions = class _StateRegions {
411
+ static #registry = sharedRegistry(
412
+ "stimeo-ui.state-regions.registry.v1",
413
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
414
+ );
303
415
  #whenTrue;
304
416
  #whenFalse;
305
- #taken = /* @__PURE__ */ new WeakSet();
306
- constructor(options) {
417
+ #record;
418
+ constructor(options, owner) {
307
419
  this.#whenTrue = options.whenTrue;
308
420
  this.#whenFalse = options.whenFalse ?? null;
421
+ this.#record = `data-${owner}-hidden-region`;
422
+ _StateRegions.#registry.names.add(this.#record);
309
423
  }
310
- /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
311
424
  reflect(host, isTrue) {
312
425
  const shown = this.#inside(host, this.#whenTrue());
313
426
  if (!this.#whenFalse) {
@@ -323,26 +436,131 @@ var StateRegions = class {
323
436
  for (const region of shown) this.#write(region, !isTrue);
324
437
  for (const region of hiddenSide) this.#write(region, isTrue);
325
438
  }
326
- /** Writes `hidden` where it moves, noting which regions it takes out of view. */
439
+ release(host) {
440
+ for (const region of [host, ...host.querySelectorAll("*")]) {
441
+ if (!(region instanceof HTMLElement)) continue;
442
+ const stack = this.#held(region);
443
+ const hold = stack && this.#take(stack);
444
+ if (!stack || !hold) continue;
445
+ const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
446
+ const index = stack.indexOf(hold);
447
+ stack.splice(index, 1);
448
+ const above = stack[index];
449
+ if (above) above.linked &&= hold.linked;
450
+ if (owns) {
451
+ const below = hold.linked ? stack[index - 1] : void 0;
452
+ if (below) {
453
+ if (region.hidden !== below.written) region.hidden = below.written;
454
+ } else if (region.getAttribute("hidden") !== hold.authored) {
455
+ if (hold.authored === null) region.removeAttribute("hidden");
456
+ else region.setAttribute("hidden", hold.authored);
457
+ }
458
+ }
459
+ this.#mark(region, stack);
460
+ }
461
+ }
327
462
  #write(region, hidden) {
328
- if (hidden) this.#taken.add(region);
329
- else this.#taken.delete(region);
330
- if (region.hidden !== hidden) region.hidden = hidden;
463
+ const stack = this.#stack(region);
464
+ const hold = this.#take(stack) ?? this.#acquire(region, stack);
465
+ const shows = !this.#covered(region, stack, hold);
466
+ hold.written = hidden;
467
+ if (shows && region.hidden !== hidden) region.hidden = hidden;
468
+ this.#mark(region, stack);
331
469
  }
332
- /** Returns a region this instance hid; one it never hid keeps what it carries. */
333
470
  #give(region) {
334
- if (this.#taken.delete(region)) region.hidden = false;
471
+ const stack = this.#held(region);
472
+ if (stack && this.#take(stack)?.written) this.#write(region, false);
473
+ }
474
+ #held(region) {
475
+ return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
476
+ }
477
+ #stack(region) {
478
+ const stacks = _StateRegions.#registry.stacks;
479
+ const stack = stacks.get(region) ?? this.#seed(region);
480
+ stacks.set(region, stack);
481
+ return stack;
482
+ }
483
+ #seed(region) {
484
+ const found = [];
485
+ for (const record of _StateRegions.#registry.names) {
486
+ const read = this.#recorded(region, record);
487
+ if (read) found.push({ ...read, record });
488
+ }
489
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
490
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
491
+ instance: null,
492
+ record,
493
+ authored,
494
+ written,
495
+ linked: linked && index > 0
496
+ }));
497
+ }
498
+ #take(stack) {
499
+ const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
500
+ if (hold) hold.instance = this;
501
+ return hold;
502
+ }
503
+ #covered(region, stack, hold) {
504
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
505
+ }
506
+ #acquire(region, stack) {
507
+ const top = stack.at(-1);
508
+ const over = top && region.hidden === top.written ? top : void 0;
509
+ const hold = {
510
+ instance: this,
511
+ record: this.#record,
512
+ authored: over ? over.authored : region.getAttribute("hidden"),
513
+ written: false,
514
+ linked: over !== void 0
515
+ };
516
+ stack.push(hold);
517
+ return hold;
518
+ }
519
+ #mark(region, stack) {
520
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
521
+ stack.forEach((hold, index) => {
522
+ const names = stack.slice(0, index).map((under) => under.record);
523
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
524
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
525
+ records.set(
526
+ hold.record,
527
+ region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
528
+ );
529
+ });
530
+ for (const [name, recorded] of records) {
531
+ if (region.getAttribute(name) === recorded) continue;
532
+ if (recorded === null) region.removeAttribute(name);
533
+ else region.setAttribute(name, recorded);
534
+ }
535
+ }
536
+ #recorded(region, name) {
537
+ try {
538
+ const parsed = JSON.parse(region.getAttribute(name) ?? "null");
539
+ if (!Array.isArray(parsed)) return null;
540
+ const [authored, written, ...rest] = parsed;
541
+ if (authored !== null && typeof authored !== "string") return null;
542
+ const linked = rest[0] !== false;
543
+ const beneath = linked ? rest : rest.slice(1);
544
+ if (!beneath.every((under) => typeof under === "string")) return null;
545
+ return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
546
+ } catch {
547
+ return null;
548
+ }
335
549
  }
336
- /** The declared regions that sit within `host`, which may be the host itself. */
337
550
  #inside(host, regions) {
338
551
  return regions.filter((region) => host.contains(region));
339
552
  }
340
553
  };
341
554
 
342
- // src/controllers/carousel_controller.ts
555
+ function targetSelector(identifier, name) {
556
+ return `[data-${identifier}-target~="${name}"]`;
557
+ }
558
+
343
559
  var DEFAULT_INTERVAL = 5e3;
344
560
  var OBSERVED_ATTRIBUTES = ["aria-selected", "data-state", "hidden"];
345
- var CarouselController = class extends Controller {
561
+ var CarouselController = class _CarouselController extends Controller {
562
+ #numbers = new NumberValueReader();
563
+ #moves = new MoveCounter();
346
564
  static targets = [
347
565
  "slide",
348
566
  "viewport",
@@ -358,6 +576,9 @@ var CarouselController = class extends Controller {
358
576
  interval: { type: Number, default: DEFAULT_INTERVAL },
359
577
  loop: { type: Boolean, default: true }
360
578
  };
579
+ static valueConstraints = {
580
+ interval: NUMBER_BOUNDS.positiveTimer
581
+ };
361
582
  static actions = [
362
583
  "goto",
363
584
  "next",
@@ -369,79 +590,37 @@ var CarouselController = class extends Controller {
369
590
  ];
370
591
  static events = ["change", "pause", "play", "reconcile"];
371
592
  #roving = new RovingTabindex(() => this.pickerTargets);
372
- #reconcileTargets = new MicrotaskCoalescer(() => this.#reconcileTargetSet());
593
+ #reconcileTargets = new MorphRenderWatcher(() => this.#reconcileTargetSet());
373
594
  #intervals = new SafeInterval();
374
- /** Unreachable step controls and the toggle of a carousel that cannot rotate. */
375
- #ariaDisabled = new AttributeLease("aria-disabled");
376
- /** The slide container's live-region politeness, which tracks the rotation. */
377
- #ariaLive = new AttributeLease("aria-live");
378
- /** Pairs with {@link CarouselController.#ariaLive}: only the changed slide is read. */
379
- #ariaAtomic = new AttributeLease("aria-atomic");
380
- /** The label pair a play toggle may carry, one half per rotation state. */
381
- #labels = new StateRegions({
382
- whenTrue: () => this.onLabelTargets,
383
- whenFalse: () => this.offLabelTargets
384
- });
385
- #beforeCache = new BeforeCacheReset(() => this.#returnLeases());
386
- /** Tells an in-page move, which keeps the rotation, from a real detach. */
595
+ #ariaDisabled = new AttributeLease("aria-disabled", this.identifier);
596
+ #ariaPressed = new AttributeLease("aria-pressed", this.identifier);
597
+ #ariaLive = new AttributeLease("aria-live", this.identifier);
598
+ #ariaAtomic = new AttributeLease("aria-atomic", this.identifier);
599
+ #labels = new StateRegions(
600
+ {
601
+ whenTrue: () => this.onLabelTargets,
602
+ whenFalse: () => this.offLabelTargets
603
+ },
604
+ this.identifier
605
+ );
387
606
  #gate = new DetachGate();
388
- /** The next pointer movement, listened for while a pointer suspension waits on it. */
389
607
  #pointer = new ListenerSet();
390
- /**
391
- * Events an authored action binding already took. The delegated listener runs
392
- * later — it sits on the controller element, above every control — so it can
393
- * consume the mark and stand down, letting the two wirings coexist without
394
- * handling one interaction twice.
395
- */
396
608
  #handledEvents = /* @__PURE__ */ new WeakSet();
397
- /**
398
- * Whether `connect()` has run. Scheduling is already inert outside that window
399
- * (`MicrotaskCoalescer`), so this gates the Value callbacks Stimulus
400
- * delivers ahead of `connect()`.
401
- */
402
609
  #connected = false;
403
- /** Index of the visible slide. */
404
610
  #index = 0;
405
- /** The visible slide element, which identifies it across a changing target set. */
406
611
  #activeSlide = null;
407
- /** Slide count at the last resolved state, so a changed total is reportable. */
408
612
  #total = 0;
409
- /**
410
- * Pointer rests on the carousel: a suspension that lifts on `mouseleave`, or on the
411
- * pointer movement that confirms a suspension a move or a connect left unconfirmed.
412
- */
413
613
  #pointerPaused = false;
414
- /** Focus is inside the carousel: a suspension that lifts when it leaves. */
415
614
  #focusPaused = false;
416
- /** The tab is in the background: a suspension that lifts when it returns. */
417
615
  #hiddenPaused = false;
418
- /** Reduced motion holds back the intent declared at connect, until a press or a new one. */
419
616
  #motionHold = false;
420
- /** Id of the live autoplay interval, or null when stopped. */
421
617
  #timerId = null;
422
- /** Delay the live interval was armed with, so an `interval` change re-arms it. */
423
618
  #timerInterval = 0;
424
- /** Follows state-attribute rewrites on retained slides and pickers. */
425
619
  #observer = null;
426
- /** Keeps a hidden tab from advancing behind the user's back. */
427
620
  #onVisibilityChange = () => {
428
621
  this.#hiddenPaused = document.visibilityState === "hidden";
429
622
  this.#syncTimer();
430
623
  };
431
- /**
432
- * Renders the initial slide, wires the delegated listeners, and starts autoplay
433
- * when requested.
434
- *
435
- * Focus, the tab's visibility and the reduced-motion hold are read afresh. The
436
- * pointer is not something the document can answer yet: no `mouseleave` reaches a
437
- * node that moved, and `:hover` can answer from before the move until the pointer
438
- * moves again. So an in-page move — which Stimulus delivers to the *same* controller
439
- * instance as `disconnect()` then `connect()` — keeps the pointer suspension it had,
440
- * `:hover` reading true adds one, and either waits for the next pointer movement to
441
- * be confirmed or lifted. A rotation the move carried is still running, so the edges
442
- * are judged against it. The attribute observer starts after the first render so the
443
- * controller's own opening writes are not fed back to it.
444
- */
445
624
  connect() {
446
625
  const moved = this.#gate.pending;
447
626
  this.#gate.cancel();
@@ -455,8 +634,7 @@ var CarouselController = class extends Controller {
455
634
  this.#total = this.slideTargets.length;
456
635
  this.#syncTimer();
457
636
  this.#connected = true;
458
- this.#reconcileTargets.activate();
459
- this.#beforeCache.activate();
637
+ this.#reconcileTargets.observe(this.element);
460
638
  this.element.addEventListener("click", this.#onClick);
461
639
  this.element.addEventListener("keydown", this.#onKeydown);
462
640
  this.element.addEventListener("focusin", this.#onFocusin);
@@ -471,12 +649,6 @@ var CarouselController = class extends Controller {
471
649
  attributeFilter: OBSERVED_ATTRIBUTES
472
650
  });
473
651
  }
474
- /**
475
- * Releases every listener and observer, returns the leased ARIA, drops the focus
476
- * and tab suspensions. The rotation goes through the detach gate: a reconnect in
477
- * the same batch — an in-page move — keeps it, and the pointer suspension with it;
478
- * anything else ends it.
479
- */
480
652
  disconnect() {
481
653
  this.#connected = false;
482
654
  this.#pointer.dispose();
@@ -489,129 +661,99 @@ var CarouselController = class extends Controller {
489
661
  document.removeEventListener("visibilitychange", this.#onVisibilityChange);
490
662
  this.#observer?.disconnect();
491
663
  this.#observer = null;
492
- this.#reconcileTargets.cancel();
664
+ this.#reconcileTargets.disconnect();
493
665
  this.#focusPaused = false;
494
666
  this.#hiddenPaused = false;
495
667
  this.#activeSlide = null;
496
668
  this.#returnLeases();
497
- this.#beforeCache.deactivate();
498
669
  this.#gate.disconnected(this, () => this.#endRun());
499
670
  }
500
- /**
501
- * Re-establishes the single selected picker when one is added after connect.
502
- *
503
- * Without this an appended picker that arrives `aria-selected="true"` leaves two
504
- * marked at once — the authored pre-selection is only read on connect, so
505
- * nothing else ever resolves the conflict. The current slide is kept: the
506
- * repaint re-derives every picker from `#index`, so a late arrival never steals
507
- * the selection.
508
- */
509
671
  pickerTargetConnected(picker) {
510
672
  picker.tabIndex = -1;
511
673
  this.#reconcileTargets.schedule();
512
674
  }
513
- /** Repairs selection and roving after a picker leaves a retained carousel. */
514
675
  pickerTargetDisconnected() {
515
676
  this.#reconcileTargets.schedule();
516
677
  }
517
- /** Reconciles a slide added in the same DOM batch as its picker. */
518
678
  slideTargetConnected() {
519
679
  this.#reconcileTargets.schedule();
520
680
  }
521
- /** Re-clamps the active index after a slide is removed. */
522
681
  slideTargetDisconnected() {
523
682
  this.#reconcileTargets.schedule();
524
683
  }
525
- /**
526
- * Publishes the rotation state on a toggle that joins a connected carousel.
527
- *
528
- * A toggle the markup already carries connects ahead of `connect()`, where the
529
- * suspensions and the reduced-motion hold are not settled yet; the opening pass
530
- * covers it.
531
- */
684
+ viewportTargetConnected() {
685
+ this.#reconcileTargets.schedule();
686
+ }
687
+ viewportTargetDisconnected(viewport) {
688
+ if (!this.viewportTargets.includes(viewport)) {
689
+ this.#ariaLive.return(viewport);
690
+ this.#ariaAtomic.return(viewport);
691
+ }
692
+ this.#reconcileTargets.schedule();
693
+ }
694
+ playToggleTargetDisconnected(toggle) {
695
+ if (this.playToggleTargets.includes(toggle)) return;
696
+ this.#labels.release(toggle);
697
+ this.#ariaDisabled.return(toggle);
698
+ this.#ariaPressed.return(toggle);
699
+ }
700
+ prevTargetDisconnected(button) {
701
+ if (!this.prevTargets.includes(button)) this.#ariaDisabled.return(button);
702
+ }
703
+ nextTargetDisconnected(button) {
704
+ if (!this.nextTargets.includes(button)) this.#ariaDisabled.return(button);
705
+ }
532
706
  playToggleTargetConnected() {
533
707
  if (this.#connected) this.#syncTimer();
534
708
  }
535
- /**
536
- * Publishes the rotation state on a label half that joins a connected toggle.
537
- *
538
- * A half arriving changes the toggle's subtree, not its attributes, so the
539
- * attribute observer never sees it; where the arrival completes a pair that was
540
- * standing alone, this is what makes that pair whole and reflectable.
541
- */
542
709
  onLabelTargetConnected() {
543
710
  if (this.#connected) this.#syncTimer();
544
711
  }
545
- /** Publishes the rotation state on a label half that joins a connected toggle. */
546
712
  offLabelTargetConnected() {
547
713
  if (this.#connected) this.#syncTimer();
548
714
  }
549
- /** Follows a rotation intent the page changed at runtime, lifting the reduced-motion hold. */
550
715
  autoplayValueChanged() {
551
716
  if (!this.#connected) return;
552
717
  this.#motionHold = false;
553
718
  this.#syncTimer();
554
719
  }
555
- /** Re-arms the live interval at the new delay without reporting a state change. */
556
720
  intervalValueChanged() {
557
721
  if (this.#connected) this.#syncTimer();
558
722
  }
559
- /**
560
- * Re-publishes the step controls and re-evaluates the non-looping end through the
561
- * reconcile pass, which reports a move only when the position changed.
562
- */
563
723
  loopValueChanged() {
564
724
  this.#reconcileTargets.schedule();
565
725
  }
566
- /** Advances to the next slide. Delegated; `data-action` wiring is optional. */
567
726
  next(event) {
568
727
  if (event?.defaultPrevented) return;
569
728
  this.#markHandled(event);
570
- this.#select(this.#step(1), { focus: false });
729
+ this.#select(this.#step(1), { focus: false }, stateReasonFor(event));
571
730
  }
572
- /** Returns to the previous slide. Delegated; `data-action` wiring is optional. */
573
731
  prev(event) {
574
732
  if (event?.defaultPrevented) return;
575
733
  this.#markHandled(event);
576
- this.#select(this.#step(-1), { focus: false });
734
+ this.#select(this.#step(-1), { focus: false }, stateReasonFor(event));
735
+ }
736
+ goto(source) {
737
+ const { event, host, origin, reason } = actionSource(source);
738
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
739
+ return;
740
+ if (event?.defaultPrevented) return;
741
+ const picker = host?.closest(targetSelector(this.identifier, "picker"));
742
+ if (!picker || !this.pickerTargets.includes(picker)) return;
743
+ this.#markHandled(event ?? void 0);
744
+ this.#select(this.pickerTargets.indexOf(picker), { focus: false }, reason);
577
745
  }
578
- /** Jumps to the slide whose picker was activated (click / Enter / Space). */
579
- goto(event) {
580
- if (event.defaultPrevented) return;
581
- this.#markHandled(event);
582
- const index = this.#pickerIndexFor(event.currentTarget);
583
- if (index !== -1) this.#select(index, { focus: false });
584
- }
585
- /**
586
- * Flips the rotation intent the toggle shows, on the user's explicit request.
587
- *
588
- * The result is written to the `autoplay` Value, which is where every other path
589
- * reads it from. A carousel with nothing to advance to has no intent to flip: the
590
- * toggle is marked `aria-disabled` and does nothing.
591
- */
592
746
  togglePlay(event) {
593
747
  if (event?.defaultPrevented) return;
594
748
  this.#markHandled(event);
595
749
  this.#togglePlay();
596
750
  }
597
- /**
598
- * Suspends rotation. A `focus`-family event records that focus is inside the
599
- * carousel; anything else — hover, or a bare programmatic call — records the
600
- * pointer suspension. Both lift through the matching {@link resume}, and
601
- * neither touches the rotation intent.
602
- */
603
751
  pause(event) {
604
752
  this.#markHandled(event);
605
753
  if (isFocusEvent(event)) this.#focusPaused = true;
606
754
  else this.#setPointerPaused(true);
607
755
  this.#syncTimer();
608
756
  }
609
- /**
610
- * Lifts the matching suspension. A `focusout` whose `relatedTarget` is still
611
- * inside the carousel is focus moving between its own controls, which leaves the
612
- * focus suspension in place — releasing it there would stop and restart the
613
- * interval on every Tab press.
614
- */
615
757
  resume(event) {
616
758
  this.#markHandled(event);
617
759
  if (isFocusEvent(event)) {
@@ -622,14 +764,12 @@ var CarouselController = class extends Controller {
622
764
  }
623
765
  this.#syncTimer();
624
766
  }
625
- /** Picker roving for authored bindings; the delegated path is `#onKeydown`. */
626
767
  onPickerKeydown(event) {
627
768
  const index = this.#pickerIndexFor(event.currentTarget);
628
769
  if (index === -1) return;
629
770
  this.#markHandled(event);
630
771
  this.#handlePickerKeydown(event, index);
631
772
  }
632
- /** Delegated activation for pickers and step controls without authored actions. */
633
773
  #onClick = (event) => this.#delegate(event, () => {
634
774
  const index = this.#pickerIndexFor(event.target);
635
775
  if (index !== -1) {
@@ -642,91 +782,60 @@ var CarouselController = class extends Controller {
642
782
  this.#togglePlay();
643
783
  }
644
784
  });
645
- /** Delegated picker roving for pickers without authored actions. */
646
785
  #onKeydown = (event) => this.#delegate(event, () => {
647
786
  const index = this.#pickerIndexFor(event.target);
648
787
  if (index !== -1) this.#handlePickerKeydown(event, index);
649
788
  });
650
- /** Focus arriving anywhere inside suspends the rotation. */
651
789
  #onFocusin = (event) => this.#delegate(event, () => {
652
790
  this.#focusPaused = true;
653
791
  this.#syncTimer();
654
792
  });
655
- /** Focus genuinely leaving lifts the suspension; moves between own controls do not. */
656
793
  #onFocusout = (event) => this.#delegate(event, () => {
657
794
  if (this.#focusStaysInside(event)) return;
658
795
  this.#focusPaused = false;
659
796
  this.#syncTimer();
660
797
  });
661
- /** Pointer entry suspends the rotation. */
662
798
  #onMouseenter = (event) => this.#delegate(event, () => {
663
799
  this.#setPointerPaused(true);
664
800
  this.#syncTimer();
665
801
  });
666
- /** Pointer exit lifts the suspension. */
667
802
  #onMouseleave = (event) => this.#delegate(event, () => {
668
803
  this.#setPointerPaused(false);
669
804
  this.#syncTimer();
670
805
  });
671
- /**
672
- * Reads `:hover` once the pointer has moved, when the answer is current, and lifts
673
- * the pointer suspension if the pointer is not over the carousel. Listens once.
674
- */
675
806
  #onPointerMove = () => {
676
807
  this.#pointer.dispose();
677
808
  if (this.element.matches(":hover")) return;
678
809
  this.#pointerPaused = false;
679
810
  this.#syncTimer();
680
811
  };
681
- /** Leaves the pointer suspension to be confirmed or lifted on the next pointer movement. */
682
812
  #awaitPointer() {
683
813
  this.#pointer.add(document, "pointermove", this.#onPointerMove, {
684
814
  capture: true,
685
815
  passive: true
686
816
  });
687
817
  }
688
- /** Records the pointer suspension; a newer answer ends any wait on the next movement. */
689
818
  #setPointerPaused(paused) {
690
819
  this.#pointer.dispose();
691
820
  this.#pointerPaused = paused;
692
821
  }
693
- /**
694
- * Schedules one reconciliation when a state attribute is rewritten in place —
695
- * the only shape of change no target callback reports.
696
- *
697
- * The filter is the `attributeFilter` alone: those three attributes belong to
698
- * the slides and pickers, and a coalesced pass over an unrelated one costs a
699
- * repaint that writes nothing. Narrowing further here would add a branch no
700
- * test could distinguish.
701
- */
702
822
  #onAttributeMutations = () => {
703
823
  this.#reconcileTargets.schedule();
704
824
  };
705
- /**
706
- * Records that an action binding took this event.
707
- *
708
- * An authored binding sits on the control, so it runs while the event is still
709
- * below the controller element and always precedes the delegated listener.
710
- * Marking is the one signal that keeps the two wirings from both acting.
711
- */
712
825
  #markHandled(event) {
713
826
  if (event) this.#handledEvents.add(event);
714
827
  }
715
- /** Runs a delegated handler unless an action binding, or a descendant, took the event. */
716
828
  #delegate(event, run) {
717
829
  if (this.#handledEvents.delete(event) || event.defaultPrevented) return;
718
830
  run();
719
831
  }
720
- /** Position of the picker that is or contains `node`, or `-1` when none does. */
721
832
  #pickerIndexFor(node) {
722
833
  return ownerIndex(this.pickerTargets, node);
723
834
  }
724
- /** Whether a focus transition lands on another control of this same carousel. */
725
835
  #focusStaysInside(event) {
726
836
  const next = event.relatedTarget;
727
837
  return next instanceof Node && this.element.contains(next);
728
838
  }
729
- /** Flips the rotation intent unless there is nothing to rotate to; lifts the motion hold. */
730
839
  #togglePlay() {
731
840
  if (!this.#canAutoplay()) return;
732
841
  const next = !this.#intent;
@@ -734,7 +843,6 @@ var CarouselController = class extends Controller {
734
843
  this.autoplayValue = next;
735
844
  this.#syncTimer();
736
845
  }
737
- /** Applies the APG picker key map: arrows, Home, and End all move focus only. */
738
846
  #handlePickerKeydown(event, current) {
739
847
  if (event.defaultPrevented) return;
740
848
  if (isReservedArrowChord(event)) return;
@@ -749,8 +857,6 @@ var CarouselController = class extends Controller {
749
857
  this.#roving.setActive(rovingMove(current, length, step), { focus: true });
750
858
  return;
751
859
  }
752
- // A chorded Home/End is a document-level shortcut (`Control+Home` scrolls
753
- // the page) that belongs to the browser, exactly as a chorded arrow does.
754
860
  case "Home":
755
861
  if (hasModifier(event)) return;
756
862
  event.preventDefault();
@@ -763,54 +869,30 @@ var CarouselController = class extends Controller {
763
869
  return;
764
870
  }
765
871
  }
766
- /**
767
- * Resolves the index one step away from the current one, honoring `loop`.
768
- *
769
- * Bounding is {@link CarouselController.#clampToSlides}'s job alone, so an
770
- * empty set is allowed to fall out of the arithmetic here rather than being
771
- * special-cased in two places that could disagree.
772
- */
773
872
  #step(delta) {
774
873
  const total = this.slideTargets.length;
775
- const next = this.#index + delta;
874
+ const next = this.#resolveIndex() + delta;
776
875
  if (this.loopValue) return (next + total) % total;
777
876
  return Math.min(total - 1, Math.max(0, next));
778
877
  }
779
- /**
780
- * Confines an index to the slide range, so no index can hide every slide.
781
- *
782
- * The single place an index is bounded: an empty set collapses to the first
783
- * position, which is also where a non-numeric step from that empty set lands.
784
- */
785
878
  #clampToSlides(index) {
786
879
  const last = this.slideTargets.length - 1;
787
880
  if (last < 0) return 0;
788
881
  return Math.min(last, Math.max(0, index));
789
882
  }
790
- /**
791
- * Changes the active slide, updates state hooks, and emits `change` — but only
792
- * when the index actually changes, so a `next`/`prev` clamped at the end (or an
793
- * autoplay tick at a non-looping boundary) re-renders without a spurious event.
794
- */
795
- #select(index, { focus }) {
883
+ #select(index, { focus }, reason = "user") {
796
884
  const target = this.#clampToSlides(index);
797
- const changed = target !== this.#index;
885
+ const total = this.slideTargets.length;
886
+ const changed = target !== this.#index || total !== this.#total;
798
887
  this.#index = target;
888
+ this.#total = total;
889
+ const token = changed ? this.#moves.record() : null;
799
890
  this.#render({ focus });
800
891
  this.#syncTimer();
801
- if (changed) {
802
- this.dispatch("change", { detail: { index: target, total: this.slideTargets.length } });
892
+ if (token !== null && this.#moves.isLatest(token)) {
893
+ this.dispatch("change", { detail: { index: target, total, reason } });
803
894
  }
804
895
  }
805
- /**
806
- * Reflects `this.#index` onto the slides, the pickers, and the step controls.
807
- *
808
- * Every observed attribute is written only when its value actually changes: the
809
- * same writes are watched by {@link CarouselController.#onAttributeMutations},
810
- * and an unconditional rewrite would feed the controller its own output.
811
- *
812
- * @stimeoRenderRoot
813
- */
814
896
  #render({ focus }) {
815
897
  this.#activeSlide = this.slideTargets[this.#index] ?? null;
816
898
  this.slideTargets.forEach((slide, i) => {
@@ -823,10 +905,9 @@ var CarouselController = class extends Controller {
823
905
  this.pickerTargets.forEach((picker, i) => {
824
906
  setAttributeIfChanged(picker, "aria-selected", i === pickerIndex ? "true" : "false");
825
907
  });
826
- this.#roving.setActive(pickerIndex, { focus });
827
908
  this.#syncStepControls();
909
+ this.#roving.setActive(pickerIndex, { focus });
828
910
  }
829
- /** Marks the step control a non-looping carousel has no slide left to reach. */
830
911
  #syncStepControls() {
831
912
  const last = this.slideTargets.length - 1;
832
913
  const atStart = !this.loopValue && this.#index <= 0;
@@ -838,16 +919,6 @@ var CarouselController = class extends Controller {
838
919
  this.#ariaDisabled.write(button, atEnd ? "true" : null);
839
920
  }
840
921
  }
841
- /**
842
- * Resolves which slide is current from the strongest evidence available.
843
- *
844
- * A single `data-state="active"` is unambiguous and wins, which is what restores
845
- * the visible slide after a Turbo cache restore even with no pickers present.
846
- * Competing claims — a slide inserted already marked active — are settled by the
847
- * element the last render actually showed, so a newcomer never displaces what the
848
- * reader is looking at. With no claim at all the authored picker selection
849
- * decides, first in DOM order; failing that the previous position is kept.
850
- */
851
922
  #resolveIndex() {
852
923
  const slides = this.slideTargets;
853
924
  const claims = [];
@@ -865,45 +936,30 @@ var CarouselController = class extends Controller {
865
936
  );
866
937
  return this.#clampToSlides(selected === -1 ? this.#index : selected);
867
938
  }
868
- /** Re-resolves the active slide after the target set changed and reports the move. */
869
939
  #reconcileTargetSet() {
870
940
  const previousIndex = this.#index;
871
941
  const previousTotal = this.#total;
872
942
  this.#index = this.#resolveIndex();
873
- this.#render({ focus: false });
874
943
  this.#total = this.slideTargets.length;
944
+ const changed = this.#index !== previousIndex || this.#total !== previousTotal;
945
+ const token = changed ? this.#moves.record() : null;
946
+ this.#render({ focus: false });
875
947
  this.#syncTimer();
876
- if (this.#index !== previousIndex || this.#total !== previousTotal) {
948
+ if (token !== null && this.#moves.isLatest(token)) {
877
949
  this.dispatch("reconcile", { detail: { index: this.#index, total: this.#total } });
878
950
  }
879
951
  }
880
- /** The rotation intent in effect: `autoplay`, unless reduced motion holds it back. */
881
952
  get #intent() {
882
953
  return this.autoplayValue && !this.#motionHold;
883
954
  }
884
- /** Whether autoplay has anywhere left to advance to. */
885
955
  #canAutoplay() {
886
956
  const total = this.slideTargets.length;
887
957
  if (total <= 1) return false;
888
958
  return this.loopValue || this.#index < total - 1;
889
959
  }
890
- /** The advance delay; a non-finite or non-positive declaration falls back. */
891
960
  get #interval() {
892
- const declared = this.intervalValue;
893
- return Number.isFinite(declared) && declared > 0 ? declared : DEFAULT_INTERVAL;
894
- }
895
- /**
896
- * Drives the autoplay interval toward the desired state and publishes it.
897
- *
898
- * Rotation runs when the intent is on, nothing suspends it, and a slide is left
899
- * to advance to. The toggle shows the intent only while a slide is left, so
900
- * `aria-pressed` never claims a rotation that cannot happen. This body reads
901
- * `autoplay` and never writes it, so the declared intent returns once a slide
902
- * lies ahead again. Crossing the run/stop boundary emits `play`/`pause`;
903
- * re-arming at a new `interval` is the same state and stays silent.
904
- *
905
- * @stimeoRenderRoot
906
- */
961
+ return this.#safeInterval;
962
+ }
907
963
  #syncTimer() {
908
964
  const canAutoplay = this.#canAutoplay();
909
965
  const pressed = this.#intent && canAutoplay;
@@ -916,15 +972,14 @@ var CarouselController = class extends Controller {
916
972
  }
917
973
  if (shouldRun && this.#timerId === null) {
918
974
  this.#timerInterval = interval;
919
- this.#timerId = this.#intervals.set(() => this.next(), interval);
975
+ this.#timerId = this.#intervals.set(
976
+ () => this.#select(this.#step(1), { focus: false }, "timeout"),
977
+ interval
978
+ );
920
979
  }
921
980
  setAttributeIfChanged(this.element, "data-state", shouldRun ? "playing" : "paused");
922
- if (shouldRun !== wasRunning) {
923
- if (shouldRun) this.dispatch("play");
924
- else this.dispatch("pause");
925
- }
926
981
  for (const toggle of this.playToggleTargets) {
927
- toggle.setAttribute("aria-pressed", pressed ? "true" : "false");
982
+ this.#ariaPressed.write(toggle, pressed ? "true" : "false");
928
983
  this.#ariaDisabled.write(toggle, canAutoplay ? null : "true");
929
984
  this.#labels.reflect(toggle, pressed);
930
985
  }
@@ -932,12 +987,11 @@ var CarouselController = class extends Controller {
932
987
  this.#ariaLive.write(this.viewportTarget, shouldRun ? "off" : "polite");
933
988
  this.#ariaAtomic.write(this.viewportTarget, "false");
934
989
  }
990
+ if (shouldRun !== wasRunning) {
991
+ if (shouldRun) this.dispatch("play");
992
+ else this.dispatch("pause");
993
+ }
935
994
  }
936
- /**
937
- * Ends the rotation once the controller has really left. A carousel still in
938
- * the document hears the end — `pause`, and `data-state="paused"` — so no `play`
939
- * it reported is left without one; a detached tree keeps the markup it had.
940
- */
941
995
  #endRun() {
942
996
  const wasRunning = this.#timerId !== null;
943
997
  this.#intervals.clearAll();
@@ -946,12 +1000,21 @@ var CarouselController = class extends Controller {
946
1000
  setAttributeIfChanged(this.element, "data-state", "paused");
947
1001
  this.dispatch("pause");
948
1002
  }
949
- /** Hands every leased attribute back to the value the consumer authored. */
950
1003
  #returnLeases() {
951
1004
  this.#ariaDisabled.returnAll();
1005
+ this.#ariaPressed.returnAll();
952
1006
  this.#ariaLive.returnAll();
953
1007
  this.#ariaAtomic.returnAll();
954
1008
  }
1009
+ get #safeInterval() {
1010
+ return this.#numbers.read(
1011
+ this,
1012
+ "interval",
1013
+ this.intervalValue,
1014
+ _CarouselController.values.interval.default,
1015
+ _CarouselController.valueConstraints.interval
1016
+ );
1017
+ }
955
1018
  };
956
1019
  function isFocusEvent(event) {
957
1020
  return event?.type.startsWith("focus") === true;
@@ -967,5 +1030,4 @@ function setAttributeIfChanged(element, name, value) {
967
1030
  }
968
1031
 
969
1032
  export { CarouselController };
970
- //# sourceMappingURL=carousel_controller.js.map
971
1033
  //# sourceMappingURL=carousel_controller.js.map