stimeo-ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +27 -11
  3. package/dist/cable/index.d.ts +88 -31
  4. package/dist/cable/index.js +384 -449
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/coerce-JtZVNG5h.d.ts +20 -0
  7. package/dist/controllers/accordion_controller.d.ts +4 -2
  8. package/dist/controllers/accordion_controller.js +174 -55
  9. package/dist/controllers/accordion_controller.js.map +1 -1
  10. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  11. package/dist/controllers/alert_dialog_controller.js +903 -235
  12. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  13. package/dist/controllers/announcer_controller.d.ts +37 -5
  14. package/dist/controllers/announcer_controller.js +238 -205
  15. package/dist/controllers/announcer_controller.js.map +1 -1
  16. package/dist/controllers/aspect_ratio_controller.js +0 -10
  17. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  18. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  19. package/dist/controllers/auto_submit_controller.js +414 -145
  20. package/dist/controllers/auto_submit_controller.js.map +1 -1
  21. package/dist/controllers/avatar_controller.js +185 -107
  22. package/dist/controllers/avatar_controller.js.map +1 -1
  23. package/dist/controllers/breadcrumb_controller.js +0 -130
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +21 -3
  26. package/dist/controllers/bulk_select_controller.js +282 -58
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +38 -16
  29. package/dist/controllers/calendar_controller.js +228 -260
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +43 -12
  32. package/dist/controllers/carousel_controller.js +536 -436
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +44 -7
  35. package/dist/controllers/character_counter_controller.js +493 -207
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +38 -74
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +16 -5
  41. package/dist/controllers/clipboard_controller.js +261 -149
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +17 -5
  44. package/dist/controllers/collapsible_controller.js +304 -120
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +18 -6
  47. package/dist/controllers/color_picker_controller.js +178 -146
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +24 -6
  50. package/dist/controllers/combobox_controller.js +522 -156
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +51 -20
  53. package/dist/controllers/command_palette_controller.js +1156 -395
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +300 -171
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +980 -262
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +80 -18
  62. package/dist/controllers/context_menu_controller.js +346 -153
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +47 -3
  68. package/dist/controllers/countdown_controller.js +281 -158
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -10
  71. package/dist/controllers/currency_input_controller.js +575 -225
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +25 -12
  74. package/dist/controllers/data_grid_controller.js +248 -214
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +42 -14
  77. package/dist/controllers/date_range_picker_controller.js +338 -235
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +17 -6
  80. package/dist/controllers/dialog_controller.js +899 -237
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +25 -8
  83. package/dist/controllers/direct_upload_controller.js +97 -167
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +6 -1
  86. package/dist/controllers/dirty_form_controller.js +72 -69
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +196 -24
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +39 -11
  92. package/dist/controllers/drawer_controller.js +974 -355
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +31 -1
  95. package/dist/controllers/dropdown_controller.js +339 -96
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +315 -88
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +79 -58
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +63 -18
  103. package/dist/controllers/file_dropzone_controller.js +322 -211
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.js +77 -42
  106. package/dist/controllers/filter_controller.js.map +1 -1
  107. package/dist/controllers/flash_controller.d.ts +35 -12
  108. package/dist/controllers/flash_controller.js +238 -347
  109. package/dist/controllers/flash_controller.js.map +1 -1
  110. package/dist/controllers/focus_controller.d.ts +15 -6
  111. package/dist/controllers/focus_controller.js +679 -240
  112. package/dist/controllers/focus_controller.js.map +1 -1
  113. package/dist/controllers/form_field_controller.js +156 -132
  114. package/dist/controllers/form_field_controller.js.map +1 -1
  115. package/dist/controllers/form_validation_controller.d.ts +3 -3
  116. package/dist/controllers/form_validation_controller.js +31 -97
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +47 -14
  119. package/dist/controllers/frame_loading_controller.js +275 -217
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +27 -6
  122. package/dist/controllers/highlight_controller.js +107 -75
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +60 -4
  125. package/dist/controllers/hover_card_controller.js +327 -130
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +48 -6
  128. package/dist/controllers/idle_controller.js +271 -101
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +30 -13
  131. package/dist/controllers/input_mask_controller.js +483 -97
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +150 -106
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +19 -2
  137. package/dist/controllers/lazy_frame_controller.js +24 -68
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +44 -9
  140. package/dist/controllers/listbox_controller.js +523 -240
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +9 -7
  143. package/dist/controllers/local_time_controller.js +102 -77
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +80 -96
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +55 -11
  149. package/dist/controllers/menu_controller.js +302 -188
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +51 -9
  152. package/dist/controllers/menubar_controller.js +232 -345
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +33 -20
  155. package/dist/controllers/meter_controller.js +417 -87
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +48 -18
  158. package/dist/controllers/multi_select_controller.js +785 -393
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +27 -6
  161. package/dist/controllers/navigation_menu_controller.js +190 -250
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +396 -192
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +268 -62
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +50 -8
  170. package/dist/controllers/number_input_controller.js +610 -303
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.d.ts +8 -5
  173. package/dist/controllers/optimistic_controller.js +45 -87
  174. package/dist/controllers/optimistic_controller.js.map +1 -1
  175. package/dist/controllers/otp_controller.d.ts +55 -8
  176. package/dist/controllers/otp_controller.js +658 -255
  177. package/dist/controllers/otp_controller.js.map +1 -1
  178. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  179. package/dist/controllers/overflow_indicator_controller.js +122 -113
  180. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  181. package/dist/controllers/overflow_menu_controller.d.ts +58 -20
  182. package/dist/controllers/overflow_menu_controller.js +470 -295
  183. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  184. package/dist/controllers/pagination_controller.d.ts +37 -7
  185. package/dist/controllers/pagination_controller.js +332 -123
  186. package/dist/controllers/pagination_controller.js.map +1 -1
  187. package/dist/controllers/password_reveal_controller.d.ts +46 -8
  188. package/dist/controllers/password_reveal_controller.js +392 -121
  189. package/dist/controllers/password_reveal_controller.js.map +1 -1
  190. package/dist/controllers/password_strength_controller.d.ts +15 -6
  191. package/dist/controllers/password_strength_controller.js +111 -158
  192. package/dist/controllers/password_strength_controller.js.map +1 -1
  193. package/dist/controllers/persist_controller.d.ts +11 -4
  194. package/dist/controllers/persist_controller.js +165 -111
  195. package/dist/controllers/persist_controller.js.map +1 -1
  196. package/dist/controllers/pointer_drag_controller.d.ts +35 -12
  197. package/dist/controllers/pointer_drag_controller.js +434 -188
  198. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  199. package/dist/controllers/popover_controller.d.ts +41 -5
  200. package/dist/controllers/popover_controller.js +269 -113
  201. package/dist/controllers/popover_controller.js.map +1 -1
  202. package/dist/controllers/portal_controller.d.ts +44 -4
  203. package/dist/controllers/portal_controller.js +215 -76
  204. package/dist/controllers/portal_controller.js.map +1 -1
  205. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  206. package/dist/controllers/preview_guard_controller.js +243 -116
  207. package/dist/controllers/preview_guard_controller.js.map +1 -1
  208. package/dist/controllers/progress_controller.d.ts +20 -19
  209. package/dist/controllers/progress_controller.js +391 -78
  210. package/dist/controllers/progress_controller.js.map +1 -1
  211. package/dist/controllers/radio_group_controller.d.ts +21 -9
  212. package/dist/controllers/radio_group_controller.js +278 -211
  213. package/dist/controllers/radio_group_controller.js.map +1 -1
  214. package/dist/controllers/range_slider_controller.d.ts +47 -6
  215. package/dist/controllers/range_slider_controller.js +220 -113
  216. package/dist/controllers/range_slider_controller.js.map +1 -1
  217. package/dist/controllers/rating_controller.d.ts +20 -4
  218. package/dist/controllers/rating_controller.js +458 -211
  219. package/dist/controllers/rating_controller.js.map +1 -1
  220. package/dist/controllers/read_more_controller.d.ts +2 -0
  221. package/dist/controllers/read_more_controller.js +138 -63
  222. package/dist/controllers/read_more_controller.js.map +1 -1
  223. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  224. package/dist/controllers/reading_progress_controller.js +166 -137
  225. package/dist/controllers/reading_progress_controller.js.map +1 -1
  226. package/dist/controllers/relative_time_controller.d.ts +20 -6
  227. package/dist/controllers/relative_time_controller.js +211 -112
  228. package/dist/controllers/relative_time_controller.js.map +1 -1
  229. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  230. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  231. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  232. package/dist/controllers/resizable_controller.d.ts +31 -2
  233. package/dist/controllers/resizable_controller.js +299 -150
  234. package/dist/controllers/resizable_controller.js.map +1 -1
  235. package/dist/controllers/roving_controller.d.ts +3 -3
  236. package/dist/controllers/roving_controller.js +49 -88
  237. package/dist/controllers/roving_controller.js.map +1 -1
  238. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  239. package/dist/controllers/scroll_area_controller.js +490 -246
  240. package/dist/controllers/scroll_area_controller.js.map +1 -1
  241. package/dist/controllers/scroll_restore_controller.d.ts +3 -3
  242. package/dist/controllers/scroll_restore_controller.js +14 -55
  243. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  244. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  245. package/dist/controllers/scroll_visibility_controller.js +432 -149
  246. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  247. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  248. package/dist/controllers/scrollspy_controller.js +193 -248
  249. package/dist/controllers/scrollspy_controller.js.map +1 -1
  250. package/dist/controllers/separator_controller.d.ts +20 -5
  251. package/dist/controllers/separator_controller.js +285 -142
  252. package/dist/controllers/separator_controller.js.map +1 -1
  253. package/dist/controllers/sidebar_controller.d.ts +65 -8
  254. package/dist/controllers/sidebar_controller.js +1069 -404
  255. package/dist/controllers/sidebar_controller.js.map +1 -1
  256. package/dist/controllers/skeleton_controller.d.ts +23 -0
  257. package/dist/controllers/skeleton_controller.js +259 -101
  258. package/dist/controllers/skeleton_controller.js.map +1 -1
  259. package/dist/controllers/slider_controller.d.ts +29 -2
  260. package/dist/controllers/slider_controller.js +187 -86
  261. package/dist/controllers/slider_controller.js.map +1 -1
  262. package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
  263. package/dist/controllers/smart_sticky_header_controller.js +173 -66
  264. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  265. package/dist/controllers/sortable_controller.d.ts +4 -4
  266. package/dist/controllers/sortable_controller.js +26 -125
  267. package/dist/controllers/sortable_controller.js.map +1 -1
  268. package/dist/controllers/spinner_controller.d.ts +39 -4
  269. package/dist/controllers/spinner_controller.js +288 -173
  270. package/dist/controllers/spinner_controller.js.map +1 -1
  271. package/dist/controllers/step_indicator_controller.d.ts +9 -4
  272. package/dist/controllers/step_indicator_controller.js +105 -46
  273. package/dist/controllers/step_indicator_controller.js.map +1 -1
  274. package/dist/controllers/stepper_controller.d.ts +14 -6
  275. package/dist/controllers/stepper_controller.js +120 -73
  276. package/dist/controllers/stepper_controller.js.map +1 -1
  277. package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
  278. package/dist/controllers/stick_to_bottom_controller.js +275 -140
  279. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  280. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  281. package/dist/controllers/sticky_observer_controller.js +220 -49
  282. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  283. package/dist/controllers/submit_once_controller.d.ts +24 -8
  284. package/dist/controllers/submit_once_controller.js +376 -227
  285. package/dist/controllers/submit_once_controller.js.map +1 -1
  286. package/dist/controllers/switch_controller.d.ts +18 -4
  287. package/dist/controllers/switch_controller.js +201 -84
  288. package/dist/controllers/switch_controller.js.map +1 -1
  289. package/dist/controllers/tabs_controller.d.ts +9 -6
  290. package/dist/controllers/tabs_controller.js +41 -30
  291. package/dist/controllers/tabs_controller.js.map +1 -1
  292. package/dist/controllers/tags_input_controller.d.ts +16 -3
  293. package/dist/controllers/tags_input_controller.js +293 -173
  294. package/dist/controllers/tags_input_controller.js.map +1 -1
  295. package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
  296. package/dist/controllers/textarea_autosize_controller.js +108 -74
  297. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  298. package/dist/controllers/theme_controller.d.ts +24 -11
  299. package/dist/controllers/theme_controller.js +140 -116
  300. package/dist/controllers/theme_controller.js.map +1 -1
  301. package/dist/controllers/time_picker_controller.d.ts +55 -5
  302. package/dist/controllers/time_picker_controller.js +331 -127
  303. package/dist/controllers/time_picker_controller.js.map +1 -1
  304. package/dist/controllers/toast_controller.d.ts +40 -16
  305. package/dist/controllers/toast_controller.js +244 -324
  306. package/dist/controllers/toast_controller.js.map +1 -1
  307. package/dist/controllers/toggle_group_controller.d.ts +14 -6
  308. package/dist/controllers/toggle_group_controller.js +252 -191
  309. package/dist/controllers/toggle_group_controller.js.map +1 -1
  310. package/dist/controllers/toolbar_controller.d.ts +8 -6
  311. package/dist/controllers/toolbar_controller.js +219 -120
  312. package/dist/controllers/toolbar_controller.js.map +1 -1
  313. package/dist/controllers/tooltip_controller.d.ts +49 -4
  314. package/dist/controllers/tooltip_controller.js +158 -124
  315. package/dist/controllers/tooltip_controller.js.map +1 -1
  316. package/dist/controllers/transition_controller.d.ts +22 -4
  317. package/dist/controllers/transition_controller.js +123 -119
  318. package/dist/controllers/transition_controller.js.map +1 -1
  319. package/dist/controllers/tree_view_controller.d.ts +22 -12
  320. package/dist/controllers/tree_view_controller.js +283 -369
  321. package/dist/controllers/tree_view_controller.js.map +1 -1
  322. package/dist/index.d.ts +10 -5
  323. package/dist/index.js +8573 -10410
  324. package/dist/index.js.map +1 -1
  325. package/dist/inspector/cli.d.ts +38 -14
  326. package/dist/inspector/cli.js +108 -40
  327. package/dist/inspector/cli.js.map +1 -1
  328. package/dist/inspector/cli_bin.js +145 -64
  329. package/dist/inspector/cli_bin.js.map +1 -1
  330. package/dist/inspector/examples.json +20 -20
  331. package/dist/inspector/manifest.json +802 -101
  332. package/dist/positioning/index.d.ts +15 -7
  333. package/dist/positioning/index.js +154 -47
  334. package/dist/positioning/index.js.map +1 -1
  335. package/package.json +11 -10
  336. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  337. 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;
@@ -19,183 +37,277 @@ function isReservedArrowChord(event, allow = []) {
19
37
  if (!event.key.startsWith("Arrow")) return false;
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
  }
40
+ function hasModifierChord(event) {
41
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
42
+ }
43
+
44
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
45
+ if (element.getAttribute(name) === value) return false;
46
+ if (value === null) element.removeAttribute(name);
47
+ else element.setAttribute(name, value);
48
+ return true;
49
+ }
22
50
 
23
- // src/utils/attribute_lease.ts
51
+ function sharedRegistry(key, create) {
52
+ const symbol = Symbol.for(key);
53
+ const scope = globalThis;
54
+ const existing = scope[symbol];
55
+ if (existing !== void 0) return existing;
56
+ const registry2 = create();
57
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
58
+ return registry2;
59
+ }
60
+
61
+ var registry = sharedRegistry(
62
+ "stimeo-ui.attribute-lease.registry.v1",
63
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
64
+ );
65
+ function isAttributeValue(value) {
66
+ return value === null || typeof value === "string";
67
+ }
68
+ function readLeaseRecord(element, record) {
69
+ try {
70
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
71
+ if (!Array.isArray(parsed)) return void 0;
72
+ const [original, written, ...rest] = parsed;
73
+ const linked = rest[0] !== false;
74
+ const beneath = linked ? rest : rest.slice(1);
75
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
76
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
77
+ } catch {
78
+ return void 0;
79
+ }
80
+ }
81
+ function attributeStack(element, attribute) {
82
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
83
+ registry.stacks.set(element, byAttribute);
84
+ const known = byAttribute.get(attribute);
85
+ if (known) return known;
86
+ const found = [];
87
+ for (const record of registry.names.get(attribute) ?? []) {
88
+ const read = readLeaseRecord(element, record);
89
+ if (read) found.push({ ...read, record });
90
+ }
91
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
92
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
93
+ lease: null,
94
+ record,
95
+ written,
96
+ original,
97
+ linked: linked && index > 0
98
+ }));
99
+ byAttribute.set(attribute, stack);
100
+ return stack;
101
+ }
102
+ function findHold(stack, lease, record) {
103
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
104
+ }
24
105
  var AttributeLease = class {
25
106
  #attribute;
26
- #records = /* @__PURE__ */ new Map();
27
- /** @param attribute - The attribute whose temporary values this lease owns. */
28
- constructor(attribute) {
107
+ #record;
108
+ #base;
109
+ #preserveAuthor;
110
+ #held = /* @__PURE__ */ new Set();
111
+ constructor(attribute, owner, options = {}) {
29
112
  this.#attribute = attribute;
113
+ this.#record = `data-${owner}-${attribute}-lease`;
114
+ this.#base = options.base === true;
115
+ this.#preserveAuthor = options.preserveAuthor === true;
116
+ const names = registry.names;
117
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
30
118
  }
31
- /** Writes or removes the leased attribute while preserving its authored value. */
32
119
  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);
120
+ const stack = attributeStack(element, this.#attribute);
121
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
122
+ this.#held.add(element);
123
+ const shows = !this.#covered(element, stack, hold);
124
+ hold.written = value;
125
+ this.#mark(element, stack);
126
+ if (shows) this.#reflect(element, value);
43
127
  }
44
- /** Returns one lease without overwriting a value subsequently authored by a consumer. */
45
128
  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);
129
+ const held = this.#held.delete(element);
130
+ const known = registry.stacks.get(element)?.has(this.#attribute);
131
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
132
+ const hold = this.#take(stack);
133
+ if (!hold) return false;
134
+ const carries = element.getAttribute(this.#attribute) === hold.written;
135
+ const owns = carries && !this.#covered(element, stack, hold);
136
+ const index = stack.indexOf(hold);
137
+ stack.splice(index, 1);
138
+ const above = stack[index];
139
+ if (above) above.linked &&= hold.linked;
140
+ this.#mark(element, stack);
141
+ if (!owns) return true;
142
+ const below = hold.linked ? stack[index - 1] : void 0;
143
+ this.#reflect(element, below ? below.written : hold.original);
144
+ return true;
51
145
  }
52
- /** Reflects only a real value transition, avoiding self-triggered mutation work. */
53
- #reflect(element, value) {
54
- if (element.getAttribute(this.#attribute) === value) return;
55
- if (value === null) element.removeAttribute(this.#attribute);
56
- else element.setAttribute(this.#attribute, value);
57
- }
58
- /** Returns every outstanding lease using the same ownership check as {@link return}. */
59
146
  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);
147
+ for (const element of Array.from(this.#held)) this.return(element);
148
+ }
149
+ #take(stack) {
150
+ const hold = findHold(stack, this, this.#record);
151
+ if (hold) hold.lease = this;
152
+ return hold;
153
+ }
154
+ #acquire(element, stack) {
155
+ const current = element.getAttribute(this.#attribute);
156
+ const top = stack.at(-1);
157
+ const bottom = stack[0];
158
+ const over = top?.written === current ? top : void 0;
159
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
160
+ const hold = {
161
+ lease: this,
162
+ record: this.#record,
163
+ written: current,
164
+ original: from ? from.original : current,
165
+ linked: !this.#base && over !== void 0
166
+ };
167
+ if (this.#base && bottom) {
168
+ bottom.linked = true;
169
+ stack.unshift(hold);
170
+ } else stack.push(hold);
171
+ return hold;
172
+ }
173
+ #covered(element, stack, hold) {
174
+ const current = element.getAttribute(this.#attribute);
175
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
176
+ }
177
+ #mark(element, stack) {
178
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
179
+ stack.forEach((hold, index) => {
180
+ const names = stack.slice(0, index).map((under) => under.record);
181
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
182
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
183
+ records.set(
184
+ hold.record,
185
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
186
+ );
187
+ });
188
+ for (const [name, recorded] of records) {
189
+ setAttributeIfChanged(element, name, recorded);
83
190
  }
84
191
  }
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);
192
+ #reflect(element, value) {
193
+ setAttributeIfChanged(element, this.#attribute, value);
90
194
  }
91
195
  };
92
196
 
93
- // src/utils/detach_gate.ts
94
197
  var DetachGate = class _DetachGate {
95
- /** Set while a probe is queued, waiting for a reconnect to cancel it. */
96
198
  #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
199
  get pending() {
105
200
  return this.#pending;
106
201
  }
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
202
  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
- */
203
+ return !host.element.isConnected || !_DetachGate.#listed(host);
204
+ }
124
205
  disconnected(host, teardown) {
125
- if (_DetachGate.isDetached(host)) {
206
+ const carried = _DetachGate.#carried(host);
207
+ if (!carried && _DetachGate.isDetached(host)) {
126
208
  this.#pending = false;
127
209
  teardown();
128
210
  return;
129
211
  }
130
212
  this.#pending = true;
131
- queueMicrotask(() => {
213
+ const probe = () => {
132
214
  if (!this.#pending) return;
133
215
  this.#pending = false;
134
216
  teardown();
135
- });
217
+ };
218
+ if (carried) setTimeout(probe, 0);
219
+ else queueMicrotask(probe);
220
+ }
221
+ static #carried(host) {
222
+ const element = host.element;
223
+ return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
224
+ }
225
+ static #listed(host) {
226
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
227
+ return tokens.includes(host.identifier);
136
228
  }
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
229
  cancel() {
144
230
  this.#pending = false;
145
231
  }
146
232
  };
147
233
 
148
- // src/utils/event_owner.ts
149
234
  function ownerIndex(candidates, node) {
150
235
  if (!(node instanceof Node)) return -1;
151
236
  return candidates.findIndex((candidate) => candidate.contains(node));
152
237
  }
153
238
 
154
- // src/utils/listener_set.ts
155
239
  var ListenerSet = class {
156
- /** The generation every `add` joins until the next `dispose()`. */
157
240
  #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
241
  add(target, type, handler, options) {
166
242
  target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
167
243
  }
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
244
  dispose() {
173
245
  this.#abort.abort();
174
246
  this.#abort = new AbortController();
175
247
  }
176
248
  };
177
249
 
178
- // src/utils/microtask_coalescer.ts
250
+ var HoverRecheck = class {
251
+ #document;
252
+ #release;
253
+ #listeners = new ListenerSet();
254
+ #pending = /* @__PURE__ */ new Set();
255
+ #tickets = /* @__PURE__ */ new WeakMap();
256
+ constructor(document2, release) {
257
+ this.#document = document2;
258
+ this.#release = release;
259
+ }
260
+ wait(element) {
261
+ this.#pending.add(element);
262
+ this.#tickets.set(element, /* @__PURE__ */ Symbol());
263
+ this.#listeners.add(this.#document, "pointermove", this.#onPointerMove, {
264
+ capture: true,
265
+ passive: true
266
+ });
267
+ }
268
+ cancel(element) {
269
+ this.#tickets.delete(element);
270
+ this.#pending.delete(element);
271
+ if (this.#pending.size === 0) this.#listeners.dispose();
272
+ }
273
+ dispose() {
274
+ this.#listeners.dispose();
275
+ this.#pending.clear();
276
+ this.#tickets = /* @__PURE__ */ new WeakMap();
277
+ }
278
+ #onPointerMove = () => {
279
+ this.#listeners.dispose();
280
+ const waiting = [...this.#pending].map(
281
+ (element) => [element, this.#tickets.get(element)]
282
+ );
283
+ this.#pending.clear();
284
+ for (const [element, ticket] of waiting) {
285
+ if (this.#tickets.get(element) !== ticket) continue;
286
+ if (element.matches(":hover")) continue;
287
+ this.#release(element);
288
+ }
289
+ };
290
+ };
291
+
179
292
  var MicrotaskCoalescer = class {
180
293
  #run;
181
294
  #queued = false;
182
295
  #active = false;
183
296
  #generation = 0;
184
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
185
297
  constructor(run) {
186
298
  this.#run = run;
187
299
  }
188
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
189
300
  activate() {
190
301
  this.#active = true;
191
302
  }
192
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
193
303
  cancel() {
194
304
  this.#active = false;
195
305
  this.#queued = false;
196
306
  this.#generation += 1;
197
307
  }
198
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
308
+ get pending() {
309
+ return this.#queued;
310
+ }
199
311
  schedule() {
200
312
  if (!this.#active || this.#queued) return;
201
313
  this.#queued = true;
@@ -208,36 +320,104 @@ var MicrotaskCoalescer = class {
208
320
  }
209
321
  };
210
322
 
211
- // src/utils/reduced_motion.ts
323
+ var MorphRenderWatcher = class {
324
+ #listeners = new ListenerSet();
325
+ #pass;
326
+ #element = null;
327
+ #onMorph = () => this.#pass.schedule();
328
+ constructor(run) {
329
+ this.#pass = new MicrotaskCoalescer(run);
330
+ }
331
+ observe(element) {
332
+ if (this.#element === element) return;
333
+ this.disconnect();
334
+ this.#element = element;
335
+ this.#pass.activate();
336
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
337
+ }
338
+ schedule() {
339
+ this.#pass.schedule();
340
+ }
341
+ get pending() {
342
+ return this.#pass.pending;
343
+ }
344
+ disconnect() {
345
+ this.#listeners.dispose();
346
+ this.#pass.cancel();
347
+ this.#element = null;
348
+ }
349
+ };
350
+
351
+ var MoveCounter = class {
352
+ #sequence = 0;
353
+ record() {
354
+ this.#sequence += 1;
355
+ return this.#sequence;
356
+ }
357
+ isLatest(token) {
358
+ return token === this.#sequence;
359
+ }
360
+ };
361
+
362
+ var MAX_TIMER_DELAY_MS = 2147483647;
363
+
364
+ var NUMBER_BOUNDS = {
365
+ positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS }
366
+ };
367
+ function matchesNumberBounds(value, bounds) {
368
+ if (!Number.isFinite(value)) {
369
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
370
+ if (direction === null) return false;
371
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
372
+ }
373
+ if (bounds.min !== void 0 && value < bounds.min) return false;
374
+ if (bounds.max !== void 0 && value > bounds.max) return false;
375
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
376
+ if (bounds.integer && !Number.isInteger(value)) return false;
377
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
378
+ return true;
379
+ }
380
+
381
+ function readNumber(raw, fallback, bounds) {
382
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
383
+ }
384
+
385
+ var NumberValueReader = class {
386
+ #lastRejected = /* @__PURE__ */ new Map();
387
+ read(owner, name, raw, fallback, bounds) {
388
+ const resolved = readNumber(raw, fallback, bounds);
389
+ if (matchesNumberBounds(raw, bounds)) {
390
+ this.#lastRejected.delete(name);
391
+ return resolved;
392
+ }
393
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
394
+ const literal = owner.element.getAttribute(attribute);
395
+ if (literal === null) {
396
+ this.#lastRejected.delete(name);
397
+ return resolved;
398
+ }
399
+ if (this.#lastRejected.get(name) !== literal) {
400
+ this.#lastRejected.set(name, literal);
401
+ console.warn(
402
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
403
+ );
404
+ }
405
+ return resolved;
406
+ }
407
+ };
408
+
212
409
  function prefersReducedMotion() {
213
410
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
214
411
  }
215
412
 
216
- // src/utils/roving_tabindex.ts
217
413
  var RovingTabindex = class {
218
- /** Returns the current ordered item elements; called on every operation. */
219
414
  #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
415
  constructor(getItems) {
225
416
  this.#getItems = getItems;
226
417
  }
227
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
228
418
  get activeIndex() {
229
419
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
230
420
  }
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
421
  setActive(index, options = {}) {
242
422
  const { focus = false } = options;
243
423
  const items = options.items ?? this.#getItems();
@@ -253,38 +433,24 @@ function rovingMove(current, length, delta, wrap) {
253
433
  return (next + length) % length;
254
434
  }
255
435
 
256
- // src/utils/safe_timeout.ts
257
436
  var TimerRegistry = class {
258
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
259
437
  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
438
  clear(id) {
267
439
  if (this.ids.delete(id)) {
268
440
  this.cancel(id);
269
441
  }
270
442
  }
271
- /**
272
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
273
- * to guarantee no timer outlives the element.
274
- */
275
443
  clearAll() {
276
444
  for (const id of this.ids) {
277
445
  this.cancel(id);
278
446
  }
279
447
  this.ids.clear();
280
448
  }
281
- /** Number of timers currently tracked (pending). */
282
449
  get size() {
283
450
  return this.ids.size;
284
451
  }
285
452
  };
286
453
  var SafeInterval = class extends TimerRegistry {
287
- /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
288
454
  set(callback, delay) {
289
455
  const id = this.schedule(callback, delay);
290
456
  this.ids.add(id);
@@ -298,16 +464,20 @@ var SafeInterval = class extends TimerRegistry {
298
464
  }
299
465
  };
300
466
 
301
- // src/utils/state_regions.ts
302
- var StateRegions = class {
467
+ var StateRegions = class _StateRegions {
468
+ static #registry = sharedRegistry(
469
+ "stimeo-ui.state-regions.registry.v1",
470
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
471
+ );
303
472
  #whenTrue;
304
473
  #whenFalse;
305
- #taken = /* @__PURE__ */ new WeakSet();
306
- constructor(options) {
474
+ #record;
475
+ constructor(options, owner) {
307
476
  this.#whenTrue = options.whenTrue;
308
477
  this.#whenFalse = options.whenFalse ?? null;
478
+ this.#record = `data-${owner}-hidden-region`;
479
+ _StateRegions.#registry.names.add(this.#record);
309
480
  }
310
- /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
311
481
  reflect(host, isTrue) {
312
482
  const shown = this.#inside(host, this.#whenTrue());
313
483
  if (!this.#whenFalse) {
@@ -323,32 +493,135 @@ var StateRegions = class {
323
493
  for (const region of shown) this.#write(region, !isTrue);
324
494
  for (const region of hiddenSide) this.#write(region, isTrue);
325
495
  }
326
- /** Writes `hidden` where it moves, noting which regions it takes out of view. */
496
+ release(host) {
497
+ for (const region of [host, ...host.querySelectorAll("*")]) {
498
+ if (!(region instanceof HTMLElement)) continue;
499
+ const stack = this.#held(region);
500
+ const hold = stack && this.#take(stack);
501
+ if (!stack || !hold) continue;
502
+ const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
503
+ const index = stack.indexOf(hold);
504
+ stack.splice(index, 1);
505
+ const above = stack[index];
506
+ if (above) above.linked &&= hold.linked;
507
+ if (owns) {
508
+ const below = hold.linked ? stack[index - 1] : void 0;
509
+ if (below) {
510
+ if (region.hidden !== below.written) region.hidden = below.written;
511
+ } else {
512
+ setAttributeIfChanged(region, "hidden", hold.authored);
513
+ }
514
+ }
515
+ this.#mark(region, stack);
516
+ }
517
+ }
327
518
  #write(region, hidden) {
328
- if (hidden) this.#taken.add(region);
329
- else this.#taken.delete(region);
330
- if (region.hidden !== hidden) region.hidden = hidden;
519
+ const stack = this.#stack(region);
520
+ const hold = this.#take(stack) ?? this.#acquire(region, stack);
521
+ const shows = !this.#covered(region, stack, hold);
522
+ hold.written = hidden;
523
+ if (shows && region.hidden !== hidden) region.hidden = hidden;
524
+ this.#mark(region, stack);
331
525
  }
332
- /** Returns a region this instance hid; one it never hid keeps what it carries. */
333
526
  #give(region) {
334
- if (this.#taken.delete(region)) region.hidden = false;
527
+ const stack = this.#held(region);
528
+ if (stack && this.#take(stack)?.written) this.#write(region, false);
529
+ }
530
+ #held(region) {
531
+ return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
532
+ }
533
+ #stack(region) {
534
+ const stacks = _StateRegions.#registry.stacks;
535
+ const stack = stacks.get(region) ?? this.#seed(region);
536
+ stacks.set(region, stack);
537
+ return stack;
538
+ }
539
+ #seed(region) {
540
+ const found = [];
541
+ for (const record of _StateRegions.#registry.names) {
542
+ const read = this.#recorded(region, record);
543
+ if (read) found.push({ ...read, record });
544
+ }
545
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
546
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
547
+ instance: null,
548
+ record,
549
+ authored,
550
+ written,
551
+ linked: linked && index > 0
552
+ }));
553
+ }
554
+ #take(stack) {
555
+ const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
556
+ if (hold) hold.instance = this;
557
+ return hold;
558
+ }
559
+ #covered(region, stack, hold) {
560
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
561
+ }
562
+ #acquire(region, stack) {
563
+ const top = stack.at(-1);
564
+ const over = top && region.hidden === top.written ? top : void 0;
565
+ const hold = {
566
+ instance: this,
567
+ record: this.#record,
568
+ authored: over ? over.authored : region.getAttribute("hidden"),
569
+ written: false,
570
+ linked: over !== void 0
571
+ };
572
+ stack.push(hold);
573
+ return hold;
574
+ }
575
+ #mark(region, stack) {
576
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
577
+ stack.forEach((hold, index) => {
578
+ const names = stack.slice(0, index).map((under) => under.record);
579
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
580
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
581
+ records.set(
582
+ hold.record,
583
+ region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
584
+ );
585
+ });
586
+ for (const [name, recorded] of records) {
587
+ setAttributeIfChanged(region, name, recorded);
588
+ }
589
+ }
590
+ #recorded(region, name) {
591
+ try {
592
+ const parsed = JSON.parse(region.getAttribute(name) ?? "null");
593
+ if (!Array.isArray(parsed)) return null;
594
+ const [authored, written, ...rest] = parsed;
595
+ if (authored !== null && typeof authored !== "string") return null;
596
+ const linked = rest[0] !== false;
597
+ const beneath = linked ? rest : rest.slice(1);
598
+ if (!beneath.every((under) => typeof under === "string")) return null;
599
+ return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
600
+ } catch {
601
+ return null;
602
+ }
335
603
  }
336
- /** The declared regions that sit within `host`, which may be the host itself. */
337
604
  #inside(host, regions) {
338
605
  return regions.filter((region) => host.contains(region));
339
606
  }
340
607
  };
341
608
 
342
- // src/controllers/carousel_controller.ts
609
+ function targetSelector(identifier, name) {
610
+ return `[data-${identifier}-target~="${name}"]`;
611
+ }
612
+
343
613
  var DEFAULT_INTERVAL = 5e3;
344
614
  var OBSERVED_ATTRIBUTES = ["aria-selected", "data-state", "hidden"];
345
- var CarouselController = class extends Controller {
615
+ var CarouselController = class _CarouselController extends Controller {
616
+ #numbers = new NumberValueReader();
617
+ #moves = new MoveCounter();
346
618
  static targets = [
347
619
  "slide",
348
620
  "viewport",
349
621
  "prev",
350
622
  "next",
351
623
  "picker",
624
+ "pickerList",
352
625
  "playToggle",
353
626
  "onLabel",
354
627
  "offLabel"
@@ -358,6 +631,9 @@ var CarouselController = class extends Controller {
358
631
  interval: { type: Number, default: DEFAULT_INTERVAL },
359
632
  loop: { type: Boolean, default: true }
360
633
  };
634
+ static valueConstraints = {
635
+ interval: NUMBER_BOUNDS.positiveTimer
636
+ };
361
637
  static actions = [
362
638
  "goto",
363
639
  "next",
@@ -369,84 +645,46 @@ var CarouselController = class extends Controller {
369
645
  ];
370
646
  static events = ["change", "pause", "play", "reconcile"];
371
647
  #roving = new RovingTabindex(() => this.pickerTargets);
372
- #reconcileTargets = new MicrotaskCoalescer(() => this.#reconcileTargetSet());
648
+ #reconcileTargets = new MorphRenderWatcher(() => this.#reconcileTargetSet());
373
649
  #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. */
650
+ #ariaDisabled = new AttributeLease("aria-disabled", this.identifier);
651
+ #ariaPressed = new AttributeLease("aria-pressed", this.identifier);
652
+ #ariaLive = new AttributeLease("aria-live", this.identifier);
653
+ #ariaAtomic = new AttributeLease("aria-atomic", this.identifier);
654
+ #labels = new StateRegions(
655
+ {
656
+ whenTrue: () => this.onLabelTargets,
657
+ whenFalse: () => this.offLabelTargets
658
+ },
659
+ this.identifier
660
+ );
387
661
  #gate = new DetachGate();
388
- /** The next pointer movement, listened for while a pointer suspension waits on it. */
389
- #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
- */
662
+ #pointer = new HoverRecheck(this.element.ownerDocument, () => {
663
+ this.#pointerPaused = false;
664
+ this.#syncTimer();
665
+ });
396
666
  #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
667
  #connected = false;
403
- /** Index of the visible slide. */
404
668
  #index = 0;
405
- /** The visible slide element, which identifies it across a changing target set. */
406
669
  #activeSlide = null;
407
- /** Slide count at the last resolved state, so a changed total is reportable. */
408
670
  #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
671
  #pointerPaused = false;
414
- /** Focus is inside the carousel: a suspension that lifts when it leaves. */
415
672
  #focusPaused = false;
416
- /** The tab is in the background: a suspension that lifts when it returns. */
417
673
  #hiddenPaused = false;
418
- /** Reduced motion holds back the intent declared at connect, until a press or a new one. */
419
674
  #motionHold = false;
420
- /** Id of the live autoplay interval, or null when stopped. */
421
675
  #timerId = null;
422
- /** Delay the live interval was armed with, so an `interval` change re-arms it. */
423
676
  #timerInterval = 0;
424
- /** Follows state-attribute rewrites on retained slides and pickers. */
425
677
  #observer = null;
426
- /** Keeps a hidden tab from advancing behind the user's back. */
427
678
  #onVisibilityChange = () => {
428
679
  this.#hiddenPaused = document.visibilityState === "hidden";
429
680
  this.#syncTimer();
430
681
  };
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
- */
682
+ #listeners = new ListenerSet();
445
683
  connect() {
446
684
  const moved = this.#gate.pending;
447
685
  this.#gate.cancel();
448
686
  this.#pointerPaused = moved && this.#pointerPaused || this.element.matches(":hover");
449
- if (this.#pointerPaused) this.#awaitPointer();
687
+ if (this.#pointerPaused) this.#pointer.wait(this.element);
450
688
  this.#focusPaused = this.element.contains(document.activeElement);
451
689
  this.#hiddenPaused = document.visibilityState === "hidden";
452
690
  this.#motionHold = prefersReducedMotion();
@@ -455,15 +693,14 @@ var CarouselController = class extends Controller {
455
693
  this.#total = this.slideTargets.length;
456
694
  this.#syncTimer();
457
695
  this.#connected = true;
458
- this.#reconcileTargets.activate();
459
- this.#beforeCache.activate();
460
- this.element.addEventListener("click", this.#onClick);
461
- this.element.addEventListener("keydown", this.#onKeydown);
462
- this.element.addEventListener("focusin", this.#onFocusin);
463
- this.element.addEventListener("focusout", this.#onFocusout);
464
- this.element.addEventListener("mouseenter", this.#onMouseenter);
465
- this.element.addEventListener("mouseleave", this.#onMouseleave);
466
- document.addEventListener("visibilitychange", this.#onVisibilityChange);
696
+ this.#reconcileTargets.observe(this.element);
697
+ this.#listeners.add(this.element, "click", this.#onClick);
698
+ this.#listeners.add(this.element, "keydown", this.#onKeydown);
699
+ this.#listeners.add(this.element, "focusin", this.#onFocusin);
700
+ this.#listeners.add(this.element, "focusout", this.#onFocusout);
701
+ this.#listeners.add(this.element, "mouseenter", this.#onMouseenter);
702
+ this.#listeners.add(this.element, "mouseleave", this.#onMouseleave);
703
+ this.#listeners.add(document, "visibilitychange", this.#onVisibilityChange);
467
704
  this.#observer = new MutationObserver(this.#onAttributeMutations);
468
705
  this.#observer.observe(this.element, {
469
706
  subtree: true,
@@ -471,147 +708,105 @@ var CarouselController = class extends Controller {
471
708
  attributeFilter: OBSERVED_ATTRIBUTES
472
709
  });
473
710
  }
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
711
  disconnect() {
481
712
  this.#connected = false;
482
713
  this.#pointer.dispose();
483
- this.element.removeEventListener("click", this.#onClick);
484
- this.element.removeEventListener("keydown", this.#onKeydown);
485
- this.element.removeEventListener("focusin", this.#onFocusin);
486
- this.element.removeEventListener("focusout", this.#onFocusout);
487
- this.element.removeEventListener("mouseenter", this.#onMouseenter);
488
- this.element.removeEventListener("mouseleave", this.#onMouseleave);
489
- document.removeEventListener("visibilitychange", this.#onVisibilityChange);
714
+ this.#listeners.dispose();
490
715
  this.#observer?.disconnect();
491
716
  this.#observer = null;
492
- this.#reconcileTargets.cancel();
717
+ this.#reconcileTargets.disconnect();
493
718
  this.#focusPaused = false;
494
719
  this.#hiddenPaused = false;
495
720
  this.#activeSlide = null;
496
721
  this.#returnLeases();
497
- this.#beforeCache.deactivate();
498
722
  this.#gate.disconnected(this, () => this.#endRun());
499
723
  }
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
724
  pickerTargetConnected(picker) {
510
725
  picker.tabIndex = -1;
511
726
  this.#reconcileTargets.schedule();
512
727
  }
513
- /** Repairs selection and roving after a picker leaves a retained carousel. */
514
728
  pickerTargetDisconnected() {
515
729
  this.#reconcileTargets.schedule();
516
730
  }
517
- /** Reconciles a slide added in the same DOM batch as its picker. */
518
731
  slideTargetConnected() {
519
732
  this.#reconcileTargets.schedule();
520
733
  }
521
- /** Re-clamps the active index after a slide is removed. */
522
734
  slideTargetDisconnected() {
523
735
  this.#reconcileTargets.schedule();
524
736
  }
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
- */
737
+ viewportTargetConnected() {
738
+ this.#reconcileTargets.schedule();
739
+ }
740
+ viewportTargetDisconnected(viewport) {
741
+ if (!this.viewportTargets.includes(viewport)) {
742
+ this.#ariaLive.return(viewport);
743
+ this.#ariaAtomic.return(viewport);
744
+ }
745
+ this.#reconcileTargets.schedule();
746
+ }
747
+ playToggleTargetDisconnected(toggle) {
748
+ if (this.playToggleTargets.includes(toggle)) return;
749
+ this.#labels.release(toggle);
750
+ this.#ariaDisabled.return(toggle);
751
+ this.#ariaPressed.return(toggle);
752
+ }
753
+ prevTargetDisconnected(button) {
754
+ if (!this.prevTargets.includes(button)) this.#ariaDisabled.return(button);
755
+ }
756
+ nextTargetDisconnected(button) {
757
+ if (!this.nextTargets.includes(button)) this.#ariaDisabled.return(button);
758
+ }
532
759
  playToggleTargetConnected() {
533
760
  if (this.#connected) this.#syncTimer();
534
761
  }
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
762
  onLabelTargetConnected() {
543
763
  if (this.#connected) this.#syncTimer();
544
764
  }
545
- /** Publishes the rotation state on a label half that joins a connected toggle. */
546
765
  offLabelTargetConnected() {
547
766
  if (this.#connected) this.#syncTimer();
548
767
  }
549
- /** Follows a rotation intent the page changed at runtime, lifting the reduced-motion hold. */
550
768
  autoplayValueChanged() {
551
769
  if (!this.#connected) return;
552
770
  this.#motionHold = false;
553
771
  this.#syncTimer();
554
772
  }
555
- /** Re-arms the live interval at the new delay without reporting a state change. */
556
773
  intervalValueChanged() {
557
774
  if (this.#connected) this.#syncTimer();
558
775
  }
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
776
  loopValueChanged() {
564
777
  this.#reconcileTargets.schedule();
565
778
  }
566
- /** Advances to the next slide. Delegated; `data-action` wiring is optional. */
567
779
  next(event) {
568
780
  if (event?.defaultPrevented) return;
569
781
  this.#markHandled(event);
570
- this.#select(this.#step(1), { focus: false });
782
+ this.#select(this.#step(1), { focus: false }, stateReasonFor(event));
571
783
  }
572
- /** Returns to the previous slide. Delegated; `data-action` wiring is optional. */
573
784
  prev(event) {
574
785
  if (event?.defaultPrevented) return;
575
786
  this.#markHandled(event);
576
- this.#select(this.#step(-1), { focus: false });
787
+ this.#select(this.#step(-1), { focus: false }, stateReasonFor(event));
788
+ }
789
+ goto(source) {
790
+ const { event, host, origin, reason } = actionSource(source);
791
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
792
+ return;
793
+ if (event?.defaultPrevented) return;
794
+ const picker = host?.closest(targetSelector(this.identifier, "picker"));
795
+ if (!picker || !this.pickerTargets.includes(picker)) return;
796
+ this.#markHandled(event ?? void 0);
797
+ this.#select(this.pickerTargets.indexOf(picker), { focus: false }, reason);
577
798
  }
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
799
  togglePlay(event) {
593
800
  if (event?.defaultPrevented) return;
594
801
  this.#markHandled(event);
595
802
  this.#togglePlay();
596
803
  }
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
804
  pause(event) {
604
805
  this.#markHandled(event);
605
806
  if (isFocusEvent(event)) this.#focusPaused = true;
606
807
  else this.#setPointerPaused(true);
607
808
  this.#syncTimer();
608
809
  }
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
810
  resume(event) {
616
811
  this.#markHandled(event);
617
812
  if (isFocusEvent(event)) {
@@ -622,14 +817,12 @@ var CarouselController = class extends Controller {
622
817
  }
623
818
  this.#syncTimer();
624
819
  }
625
- /** Picker roving for authored bindings; the delegated path is `#onKeydown`. */
626
820
  onPickerKeydown(event) {
627
821
  const index = this.#pickerIndexFor(event.currentTarget);
628
822
  if (index === -1) return;
629
823
  this.#markHandled(event);
630
824
  this.#handlePickerKeydown(event, index);
631
825
  }
632
- /** Delegated activation for pickers and step controls without authored actions. */
633
826
  #onClick = (event) => this.#delegate(event, () => {
634
827
  const index = this.#pickerIndexFor(event.target);
635
828
  if (index !== -1) {
@@ -642,91 +835,48 @@ var CarouselController = class extends Controller {
642
835
  this.#togglePlay();
643
836
  }
644
837
  });
645
- /** Delegated picker roving for pickers without authored actions. */
646
838
  #onKeydown = (event) => this.#delegate(event, () => {
647
839
  const index = this.#pickerIndexFor(event.target);
648
840
  if (index !== -1) this.#handlePickerKeydown(event, index);
649
841
  });
650
- /** Focus arriving anywhere inside suspends the rotation. */
651
842
  #onFocusin = (event) => this.#delegate(event, () => {
652
843
  this.#focusPaused = true;
653
844
  this.#syncTimer();
654
845
  });
655
- /** Focus genuinely leaving lifts the suspension; moves between own controls do not. */
656
846
  #onFocusout = (event) => this.#delegate(event, () => {
657
847
  if (this.#focusStaysInside(event)) return;
658
848
  this.#focusPaused = false;
659
849
  this.#syncTimer();
660
850
  });
661
- /** Pointer entry suspends the rotation. */
662
851
  #onMouseenter = (event) => this.#delegate(event, () => {
663
852
  this.#setPointerPaused(true);
664
853
  this.#syncTimer();
665
854
  });
666
- /** Pointer exit lifts the suspension. */
667
855
  #onMouseleave = (event) => this.#delegate(event, () => {
668
856
  this.#setPointerPaused(false);
669
857
  this.#syncTimer();
670
858
  });
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
- #onPointerMove = () => {
676
- this.#pointer.dispose();
677
- if (this.element.matches(":hover")) return;
678
- this.#pointerPaused = false;
679
- this.#syncTimer();
680
- };
681
- /** Leaves the pointer suspension to be confirmed or lifted on the next pointer movement. */
682
- #awaitPointer() {
683
- this.#pointer.add(document, "pointermove", this.#onPointerMove, {
684
- capture: true,
685
- passive: true
686
- });
687
- }
688
- /** Records the pointer suspension; a newer answer ends any wait on the next movement. */
689
859
  #setPointerPaused(paused) {
690
860
  this.#pointer.dispose();
691
861
  this.#pointerPaused = paused;
692
862
  }
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
863
  #onAttributeMutations = () => {
703
864
  this.#reconcileTargets.schedule();
704
865
  };
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
866
  #markHandled(event) {
713
867
  if (event) this.#handledEvents.add(event);
714
868
  }
715
- /** Runs a delegated handler unless an action binding, or a descendant, took the event. */
716
869
  #delegate(event, run) {
717
870
  if (this.#handledEvents.delete(event) || event.defaultPrevented) return;
718
871
  run();
719
872
  }
720
- /** Position of the picker that is or contains `node`, or `-1` when none does. */
721
873
  #pickerIndexFor(node) {
722
874
  return ownerIndex(this.pickerTargets, node);
723
875
  }
724
- /** Whether a focus transition lands on another control of this same carousel. */
725
876
  #focusStaysInside(event) {
726
877
  const next = event.relatedTarget;
727
878
  return next instanceof Node && this.element.contains(next);
728
879
  }
729
- /** Flips the rotation intent unless there is nothing to rotate to; lifts the motion hold. */
730
880
  #togglePlay() {
731
881
  if (!this.#canAutoplay()) return;
732
882
  const next = !this.#intent;
@@ -734,7 +884,6 @@ var CarouselController = class extends Controller {
734
884
  this.autoplayValue = next;
735
885
  this.#syncTimer();
736
886
  }
737
- /** Applies the APG picker key map: arrows, Home, and End all move focus only. */
738
887
  #handlePickerKeydown(event, current) {
739
888
  if (event.defaultPrevented) return;
740
889
  if (isReservedArrowChord(event)) return;
@@ -745,72 +894,49 @@ var CarouselController = class extends Controller {
745
894
  case "ArrowLeft":
746
895
  case "ArrowUp": {
747
896
  event.preventDefault();
748
- const step = logicalArrowStep(event.key, this.element);
897
+ const step = logicalArrowStep(
898
+ event.key,
899
+ this.hasPickerListTarget ? this.pickerListTarget : this.element
900
+ );
749
901
  this.#roving.setActive(rovingMove(current, length, step), { focus: true });
750
902
  return;
751
903
  }
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
904
  case "Home":
755
- if (hasModifier(event)) return;
905
+ if (hasModifierChord(event)) return;
756
906
  event.preventDefault();
757
907
  this.#roving.setActive(0, { focus: true });
758
908
  return;
759
909
  case "End":
760
- if (hasModifier(event)) return;
910
+ if (hasModifierChord(event)) return;
761
911
  event.preventDefault();
762
912
  this.#roving.setActive(length - 1, { focus: true });
763
913
  return;
764
914
  }
765
915
  }
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
916
  #step(delta) {
774
917
  const total = this.slideTargets.length;
775
- const next = this.#index + delta;
918
+ const next = this.#resolveIndex() + delta;
776
919
  if (this.loopValue) return (next + total) % total;
777
920
  return Math.min(total - 1, Math.max(0, next));
778
921
  }
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
922
  #clampToSlides(index) {
786
923
  const last = this.slideTargets.length - 1;
787
924
  if (last < 0) return 0;
788
925
  return Math.min(last, Math.max(0, index));
789
926
  }
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 }) {
927
+ #select(index, { focus }, reason = "user") {
796
928
  const target = this.#clampToSlides(index);
797
- const changed = target !== this.#index;
929
+ const total = this.slideTargets.length;
930
+ const changed = target !== this.#index || total !== this.#total;
798
931
  this.#index = target;
932
+ this.#total = total;
933
+ const token = changed ? this.#moves.record() : null;
799
934
  this.#render({ focus });
800
935
  this.#syncTimer();
801
- if (changed) {
802
- this.dispatch("change", { detail: { index: target, total: this.slideTargets.length } });
936
+ if (token !== null && this.#moves.isLatest(token)) {
937
+ this.dispatch("change", { detail: { index: target, total, reason } });
803
938
  }
804
939
  }
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
940
  #render({ focus }) {
815
941
  this.#activeSlide = this.slideTargets[this.#index] ?? null;
816
942
  this.slideTargets.forEach((slide, i) => {
@@ -823,10 +949,9 @@ var CarouselController = class extends Controller {
823
949
  this.pickerTargets.forEach((picker, i) => {
824
950
  setAttributeIfChanged(picker, "aria-selected", i === pickerIndex ? "true" : "false");
825
951
  });
826
- this.#roving.setActive(pickerIndex, { focus });
827
952
  this.#syncStepControls();
953
+ this.#roving.setActive(pickerIndex, { focus });
828
954
  }
829
- /** Marks the step control a non-looping carousel has no slide left to reach. */
830
955
  #syncStepControls() {
831
956
  const last = this.slideTargets.length - 1;
832
957
  const atStart = !this.loopValue && this.#index <= 0;
@@ -838,16 +963,6 @@ var CarouselController = class extends Controller {
838
963
  this.#ariaDisabled.write(button, atEnd ? "true" : null);
839
964
  }
840
965
  }
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
966
  #resolveIndex() {
852
967
  const slides = this.slideTargets;
853
968
  const claims = [];
@@ -865,45 +980,30 @@ var CarouselController = class extends Controller {
865
980
  );
866
981
  return this.#clampToSlides(selected === -1 ? this.#index : selected);
867
982
  }
868
- /** Re-resolves the active slide after the target set changed and reports the move. */
869
983
  #reconcileTargetSet() {
870
984
  const previousIndex = this.#index;
871
985
  const previousTotal = this.#total;
872
986
  this.#index = this.#resolveIndex();
873
- this.#render({ focus: false });
874
987
  this.#total = this.slideTargets.length;
988
+ const changed = this.#index !== previousIndex || this.#total !== previousTotal;
989
+ const token = changed ? this.#moves.record() : null;
990
+ this.#render({ focus: false });
875
991
  this.#syncTimer();
876
- if (this.#index !== previousIndex || this.#total !== previousTotal) {
992
+ if (token !== null && this.#moves.isLatest(token)) {
877
993
  this.dispatch("reconcile", { detail: { index: this.#index, total: this.#total } });
878
994
  }
879
995
  }
880
- /** The rotation intent in effect: `autoplay`, unless reduced motion holds it back. */
881
996
  get #intent() {
882
997
  return this.autoplayValue && !this.#motionHold;
883
998
  }
884
- /** Whether autoplay has anywhere left to advance to. */
885
999
  #canAutoplay() {
886
1000
  const total = this.slideTargets.length;
887
1001
  if (total <= 1) return false;
888
1002
  return this.loopValue || this.#index < total - 1;
889
1003
  }
890
- /** The advance delay; a non-finite or non-positive declaration falls back. */
891
1004
  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
- */
1005
+ return this.#safeInterval;
1006
+ }
907
1007
  #syncTimer() {
908
1008
  const canAutoplay = this.#canAutoplay();
909
1009
  const pressed = this.#intent && canAutoplay;
@@ -916,15 +1016,14 @@ var CarouselController = class extends Controller {
916
1016
  }
917
1017
  if (shouldRun && this.#timerId === null) {
918
1018
  this.#timerInterval = interval;
919
- this.#timerId = this.#intervals.set(() => this.next(), interval);
1019
+ this.#timerId = this.#intervals.set(
1020
+ () => this.#select(this.#step(1), { focus: false }, "timeout"),
1021
+ interval
1022
+ );
920
1023
  }
921
1024
  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
1025
  for (const toggle of this.playToggleTargets) {
927
- toggle.setAttribute("aria-pressed", pressed ? "true" : "false");
1026
+ this.#ariaPressed.write(toggle, pressed ? "true" : "false");
928
1027
  this.#ariaDisabled.write(toggle, canAutoplay ? null : "true");
929
1028
  this.#labels.reflect(toggle, pressed);
930
1029
  }
@@ -932,12 +1031,11 @@ var CarouselController = class extends Controller {
932
1031
  this.#ariaLive.write(this.viewportTarget, shouldRun ? "off" : "polite");
933
1032
  this.#ariaAtomic.write(this.viewportTarget, "false");
934
1033
  }
1034
+ if (shouldRun !== wasRunning) {
1035
+ if (shouldRun) this.dispatch("play");
1036
+ else this.dispatch("pause");
1037
+ }
935
1038
  }
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
1039
  #endRun() {
942
1040
  const wasRunning = this.#timerId !== null;
943
1041
  this.#intervals.clearAll();
@@ -946,26 +1044,28 @@ var CarouselController = class extends Controller {
946
1044
  setAttributeIfChanged(this.element, "data-state", "paused");
947
1045
  this.dispatch("pause");
948
1046
  }
949
- /** Hands every leased attribute back to the value the consumer authored. */
950
1047
  #returnLeases() {
951
1048
  this.#ariaDisabled.returnAll();
1049
+ this.#ariaPressed.returnAll();
952
1050
  this.#ariaLive.returnAll();
953
1051
  this.#ariaAtomic.returnAll();
954
1052
  }
1053
+ get #safeInterval() {
1054
+ return this.#numbers.read(
1055
+ this,
1056
+ "interval",
1057
+ this.intervalValue,
1058
+ _CarouselController.values.interval.default,
1059
+ _CarouselController.valueConstraints.interval
1060
+ );
1061
+ }
955
1062
  };
956
1063
  function isFocusEvent(event) {
957
1064
  return event?.type.startsWith("focus") === true;
958
1065
  }
959
- function hasModifier(event) {
960
- return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
961
- }
962
1066
  function hits(elements, node) {
963
1067
  return ownerIndex(elements, node) !== -1;
964
1068
  }
965
- function setAttributeIfChanged(element, name, value) {
966
- if (element.getAttribute(name) !== value) element.setAttribute(name, value);
967
- }
968
1069
 
969
1070
  export { CarouselController };
970
- //# sourceMappingURL=carousel_controller.js.map
971
1071
  //# sourceMappingURL=carousel_controller.js.map