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 +1 @@
1
- {"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/escape_layer.ts","../../src/utils/safe_timeout.ts","../../src/utils/state_reason.ts","../../src/controllers/context_menu_controller.ts"],"names":[],"mappings":"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","file":"context_menu_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press arrived carrying any modifier, for the keys the browser and\n * the OS own outright: return without calling `preventDefault()` and without\n * moving any state.\n *\n * `Control+Home` and `Control+End` jump the document to its ends, and a widget\n * that swallows them makes the shortcut work or not depending on where focus\n * happens to sit. {@link isReservedArrowChord} answers the same question for the\n * arrows, but returns `false` for every other key so that this one can decide.\n *\n * There is no `allow` list here on purpose. APG assigns no modifier chord to\n * `Home`/`End`, so a widget that wanted one would be claiming a combination the\n * pattern never gave it.\n *\n * @example\n * ```ts\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","/**\n * Why a component's public open/closed state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { type StateReason, stateReasonFor } from \"../utils/state_reason\";\n\n/**\n * Headless, accessible **context menu** behavior.\n *\n * Markup contract (identifier: `stimeo--context-menu`):\n * <div data-controller=\"stimeo--context-menu\">\n * <div data-stimeo--context-menu-target=\"region\" tabindex=\"0\"\n * aria-haspopup=\"menu\" aria-controls=\"ctx\"\n * data-action=\"contextmenu->stimeo--context-menu#open\n * keydown->stimeo--context-menu#onRegionKeydown\">…</div>\n * <ul id=\"ctx\" role=\"menu\" data-stimeo--context-menu-target=\"menu\" hidden>\n * <li role=\"none\">\n * <button role=\"menuitem\" tabindex=\"-1\"\n * data-stimeo--context-menu-target=\"item\"\n * data-action=\"click->stimeo--context-menu#activate\n * keydown->stimeo--context-menu#onItemKeydown\">…</button>\n * </li>\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Menu** pattern. The trigger is a `contextmenu`\n * event or the `Shift+F10` / `ContextMenu` key rather than a button click, the\n * menu is shown at the pointer coordinate, open state is exposed as `data-state`\n * rather than `aria-expanded`, and each item carries its own `data-action`\n * bindings.\n *\n * @remarks\n * Behavior only — the controller reflects the click coordinate as the CSS custom\n * properties `--stimeo--context-menu-x` / `--stimeo--context-menu-y` on the menu\n * so the consumer's CSS can place it (works standalone, no positioning module\n * required). Viewport-edge flip/shift is delegated to the opt-in\n * `stimeo-ui/positioning` module, which this controller never imports.\n *\n * Open state is exposed on the region as `data-state` (`open`/`closed`) — a CSS\n * hook, not an ARIA one. `aria-expanded` is deliberately *not* set on the region\n * because it is a generic container, not a role that supports that state (doing so\n * is an ARIA violation); the region's static `aria-haspopup=\"menu\"` advertises the\n * popup, and assistive tech perceives the open state when focus moves into the\n * `role=\"menu\"`.\n *\n * Behavior provided:\n * - `contextmenu` on the region suppresses the browser menu and opens this one at\n * the pointer; `Shift+F10` / `ContextMenu` opens it at the region's center.\n * - On open, focus moves to the first item; the region's `data-state` syncs.\n * - Roving focus inside the menu: `ArrowUp`/`ArrowDown` (wrapping), `Home`/`End`.\n * - Activating an enabled item (click / native `Enter`/`Space` on the button)\n * closes the menu and restores focus to the region. `aria-disabled` activation\n * is blocked before consumer click handlers run.\n * - `Escape` closes and restores focus to the region. While open the menu is a\n * layer on the shared `EscapeLayer` stack; it claims a press only while\n * focus is inside the controller or fell to the body, so one keypress closes\n * exactly one layer. `Tab` lets the browser move focus first, then closes on\n * the next task. An outside click or context-menu invocation closes without\n * stealing focus from its destination.\n * - Each move of the open state is reported: `stimeo--context-menu:open` and\n * `stimeo--context-menu:close` dispatch `{ reason: StateReason }`, after the\n * state attributes are written. Both are informational, so neither is\n * cancelable. A call that leaves the state where it already was, the\n * normalization in {@link connect}, and {@link disconnect} are all silent.\n *\n * Roving focus skips `hidden` and natively `disabled` items. An\n * `aria-disabled=\"true\"` item stays reachable by arrow keys — APG marks that\n * attribute precisely for controls that must remain discoverable — while its\n * activation is suppressed, so it announces itself and does nothing.\n */\nexport class ContextMenuController extends Controller<HTMLElement> {\n static override targets = [\"region\", \"menu\", \"item\"];\n static actions = [\"activate\", \"onItemKeydown\", \"onRegionKeydown\", \"open\"] as const;\n static events = [\"close\", \"open\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly menuTarget: HTMLElement;\n declare readonly itemTargets: HTMLButtonElement[];\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMenuTarget: boolean;\n\n readonly #timers = new SafeTimeout();\n\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Whether state moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /** Starts closed and registers delegated activation and outside-pointer listeners. */\n override connect(): void {\n this.#closeMenu(\"api\");\n this.element.addEventListener(\"click\", this.#onItemClickCapture, true);\n document.addEventListener(\"click\", this.#onOutsidePointer, true);\n document.addEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n this.#reporting = true;\n }\n\n /** Releases the listeners, stack membership, and pending Tab-close task. */\n override disconnect(): void {\n this.#reporting = false;\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n this.element.removeEventListener(\"click\", this.#onItemClickCapture, true);\n document.removeEventListener(\"click\", this.#onOutsidePointer, true);\n document.removeEventListener(\"contextmenu\", this.#onOutsidePointer, true);\n }\n\n /**\n * Opens the menu from a `contextmenu` event: suppresses the native menu and\n * places this one at the pointer coordinate.\n */\n open(event: MouseEvent): void {\n event.preventDefault();\n this.#openAt(event.clientX, event.clientY, stateReasonFor(event));\n }\n\n /** Keyboard entry on the region: `Shift+F10` / `ContextMenu` open at center. */\n onRegionKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n const isContextKey = event.key === \"ContextMenu\" || (event.key === \"F10\" && event.shiftKey);\n if (!isContextKey) return;\n event.preventDefault();\n const rect = this.hasRegionTarget\n ? this.regionTarget.getBoundingClientRect()\n : { left: 0, top: 0, width: 0, height: 0 };\n this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, stateReasonFor(event));\n }\n\n /** Roving focus and closing keys inside the menu. */\n onItemKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const items = this.#navigableItems;\n const currentIndex = items.indexOf(event.currentTarget as HTMLButtonElement);\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n if (items.length > 0) {\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length;\n items[nextIndex]?.focus();\n }\n break;\n case \"ArrowUp\":\n event.preventDefault();\n if (items.length > 0) {\n const previousIndex = currentIndex < 0 ? items.length - 1 : currentIndex - 1;\n items[(previousIndex + items.length) % items.length]?.focus();\n }\n break;\n case \"Home\":\n event.preventDefault();\n items[0]?.focus();\n break;\n case \"End\":\n event.preventDefault();\n items[items.length - 1]?.focus();\n break;\n case \"Tab\":\n // Closing synchronously removes the focused item before the browser's\n // default Tab action, which can restart traversal at the document head.\n this.#timers.clearAll();\n this.#timers.set(() => this.#closeMenu(\"focus\"), 0);\n break;\n default:\n break;\n }\n }\n\n /** Closes after an item is activated and restores focus to the region. */\n activate(): void {\n this.#closeAndRestore(\"select\");\n }\n\n /** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */\n #openAt(x: number, y: number, reason: StateReason): void {\n if (!this.hasMenuTarget) return;\n this.#timers.clearAll();\n const was = this.#isOpen;\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(\"escape\"),\n claims: claimsWhileFocusWithin(this.element),\n });\n this.menuTarget.style.setProperty(\"--stimeo--context-menu-x\", `${x}px`);\n this.menuTarget.style.setProperty(\"--stimeo--context-menu-y\", `${y}px`);\n this.menuTarget.hidden = false;\n if (this.hasRegionTarget) this.regionTarget.setAttribute(\"data-state\", \"open\");\n if (!was && this.#reporting) this.dispatch(\"open\", { detail: { reason }, cancelable: false });\n // A subscriber may close it again from the handler above; focusing then puts\n // the caret on an item nobody can see.\n if (!this.#isOpen) return;\n this.#navigableItems[0]?.focus();\n }\n\n /** Hides the menu, reflects the collapsed state on the region, and reports a move. */\n #closeMenu(reason: StateReason): void {\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n if (!this.hasMenuTarget) return;\n const was = this.#isOpen;\n this.menuTarget.hidden = true;\n if (this.hasRegionTarget) this.regionTarget.setAttribute(\"data-state\", \"closed\");\n if (was && this.#reporting) this.dispatch(\"close\", { detail: { reason }, cancelable: false });\n }\n\n /** Closes the menu and returns focus to the region (Escape / activation). */\n #closeAndRestore(reason: StateReason): void {\n this.#closeMenu(reason);\n if (this.hasRegionTarget) this.regionTarget.focus();\n }\n\n /**\n * Closes when a click or context-menu invocation lands outside this instance.\n * Both subscriptions observe in the capture phase, so the target is judged\n * against the tree the user actually pressed even when a consumer handler\n * detaches it, and application code that stops bubbling cannot leave the menu\n * open.\n */\n readonly #onOutsidePointer = (event: MouseEvent): void => {\n if (this.#isOpen && !this.element.contains(event.target as Node)) this.#closeMenu(\"outside\");\n };\n\n /**\n * Captures clicks so `aria-disabled` commands cannot reach consumer handlers.\n * Native Enter/Space activation also synthesizes a click and is blocked here.\n */\n readonly #onItemClickCapture = (event: MouseEvent): void => {\n const target = event.target;\n if (!(target instanceof Node)) return;\n const disabled = this.itemTargets.some(\n (item) => item.getAttribute(\"aria-disabled\") === \"true\" && item.contains(target),\n );\n if (!disabled) return;\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n /** Menu items eligible for roving focus (excludes hidden / natively disabled). */\n get #navigableItems(): HTMLButtonElement[] {\n return this.itemTargets.filter((item) => this.#isNavigable(item));\n }\n\n /**\n * An item can take roving focus unless it is `hidden` or a natively `disabled`\n * form control. CSS-only visibility is not detectable here and is the\n * consumer's responsibility.\n *\n * **`aria-disabled=\"true\"` stays reachable.** APG separates the two attributes\n * by intent: `disabled` is for controls whose existence can be inferred from a\n * neighbour (a greyed Next next to a Prev), while `aria-disabled` marks a\n * control that must stay *discoverable* — and it names menu items as the\n * example. Skipping it would hide the command's existence from a keyboard user\n * entirely. Activation is suppressed separately, so the item announces itself\n * and does nothing.\n */\n #isNavigable(item: HTMLButtonElement): boolean {\n if (item.hasAttribute(\"hidden\")) return false;\n return !item.disabled;\n }\n\n /** Whether the menu is currently visible. */\n get #isOpen(): boolean {\n return this.hasMenuTarget && !this.menuTarget.hidden;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/state_reason.ts","../../src/utils/action_source.ts","../../src/utils/aria_disabled.ts","../../src/utils/arrow_step.ts","../../src/utils/attribute_write.ts","../../src/utils/shared_registry.ts","../../src/utils/attribute_lease.ts","../../src/utils/click_verdicts.ts","../../src/utils/escape_layer.ts","../../src/utils/event_owner.ts","../../src/utils/listener_set.ts","../../src/utils/safe_timeout.ts","../../src/controllers/context_menu_controller.ts"],"names":["registry"],"mappings":"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","file":"context_menu_controller.js","sourcesContent":["/**\n * Why a component's public state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n * | `\"timeout\"` | an automatic timer advanced or dismissed the state |\n * | `\"limit\"` | a capacity limit dismissed an item |\n * | `\"media\"` | an operating-system media preference changed |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\"\n | \"timeout\"\n | \"limit\"\n | \"media\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { stateReasonFor } from \"./state_reason\";\n\n/** The DOM binding and origin of a public action, or its explicit API element. */\nexport interface ActionSource {\n readonly event: Event | null;\n readonly host: Element | null;\n readonly origin: Element | null;\n readonly reason: ReturnType<typeof stateReasonFor>;\n}\n\n/**\n * Normalizes an action's DOM event or explicit element without choosing an owned target.\n *\n * Event hosts and origins stay separate so consumers can yield to nested controls.\n * An explicit element supplies both, with no DOM event and an `\"api\"` reason.\n * Consumers resolve current targets and enforce their own scope and interaction rules.\n *\n * @param source - The DOM event, explicit element, or absent optional argument.\n * @returns The source elements, original event and public interaction reason.\n */\nexport function actionSource(source?: Event | HTMLElement): ActionSource {\n if (source instanceof HTMLElement) {\n return { event: null, host: source, origin: source, reason: \"api\" };\n }\n const event = source ?? null;\n return {\n event,\n host: event?.currentTarget instanceof Element ? event.currentTarget : null,\n origin: event?.target instanceof Element ? event.target : null,\n reason: stateReasonFor(event),\n };\n}\n","/**\n * Whether `element` or one of its ancestors carries `aria-disabled=\"true\"`.\n *\n * WAI-ARIA defines the state for an element and all of its focusable descendants\n * (https://www.w3.org/TR/wai-aria-1.2/#aria-disabled), so an element inside one\n * that carries it is disabled whatever it carries itself — `aria-disabled=\"false\"`\n * on the element does not lift it. Only the value `\"true\"` disables. The walk\n * follows parent elements to the top of the element's tree, the document element\n * for an element in a document.\n *\n * `aria-disabled` keeps an element focusable and reachable, so this answers whether\n * it may be *activated*, not whether it may take focus.\n */\nexport function isAriaDisabled(element: Element): boolean {\n for (let current: Element | null = element; current; current = current.parentElement) {\n if (current.getAttribute(\"aria-disabled\") === \"true\") return true;\n }\n return false;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/**\n * The element whose writing direction orders a grid's columns: the nearest\n * `<table>` holding `grid` inside `root`, else `grid` itself. Table columns follow\n * the table's direction, so a direction declared only on a row group such as\n * `<tbody>` does not reorder them and must not reverse the arrows either.\n */\nexport function gridDirectionElement(grid: Element, root: Element): Element {\n const table = grid.closest(\"table\");\n return table !== null && root.contains(table) ? table : grid;\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — for example `Alt+Down`/`Alt+Up`, which the APG Combobox\n * pattern makes optional and `stimeo--combobox` implements.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n\n/**\n * True when a press carries Alt, Control, Meta, or Shift, regardless of its key.\n *\n * This predicate reports modifier presence only; key ownership stays with the\n * caller. Check the widget's implemented key/modifier bindings and their state\n * conditions before using this result to yield a chord. An unimplemented chord\n * must not fall through to the bare-key behavior.\n *\n * Dedicated bindings, such as a grid's Control+Home/End navigation, belong in\n * the caller. Unlike {@link isReservedArrowChord}, this function examines every\n * key and has no arrow-specific reservation or `allow` list.\n *\n * @example\n * ```ts\n * // A Home/End handler with no modifier bindings yields every modified press.\n * case \"Home\":\n * case \"End\":\n * if (hasModifierChord(event)) return;\n * ```\n */\nexport function hasModifierChord(event: KeyboardEvent): boolean {\n return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;\n}\n","/**\n * Reflects a serialized attribute value only when the current value differs.\n *\n * A same-value `setAttribute` still queues a mutation record for every observer, so an\n * unchanged value writes nothing.\n *\n * @param element - The element whose attribute is reflected.\n * @param name - The attribute name, using the non-namespaced DOM API.\n * @param value - The serialized value, or null to remove the attribute.\n * @param beforeWrite - Called only for a change, before the DOM mutation, so the\n * caller can record ownership or identify its own observer records.\n * @returns Whether an attribute was written or removed.\n */\nexport function setAttributeIfChanged(\n element: Element,\n name: string,\n value: string | null,\n beforeWrite?: () => void,\n): boolean {\n if (element.getAttribute(name) === value) return false;\n beforeWrite?.();\n if (value === null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n return true;\n}\n","/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","import { setAttributeIfChanged } from \"./attribute_write\";\nimport { sharedRegistry } from \"./shared_registry\";\n\n/** What a lease's record says: the author's value, its last write, the leases beneath. */\ninterface AttributeLeaseRecord {\n readonly original: string | null;\n readonly written: string | null;\n /** Whether it hands back what the lease right beneath it wrote last. */\n readonly linked: boolean;\n /** The records of the other owners' leases beneath it, nearest first. */\n readonly beneath: readonly string[];\n}\n\n/** One lease's hold on one element's attribute, in the stack every lease of it shares. */\ninterface AttributeHold {\n /** The lease holding it; `null` for a hold read from a copy's record until its lease takes it. */\n lease: object | null;\n readonly record: string;\n written: string | null;\n /** What it hands back with nothing beneath it: the author's value. */\n readonly original: string | null;\n /** Whether it hands back what the hold beneath it wrote last instead. */\n linked: boolean;\n}\n\n/** The holds on each element by attribute, and every lease's record name by attribute. */\ninterface AttributeLeaseRegistry {\n readonly stacks: WeakMap<Element, Map<string, AttributeHold[]>>;\n readonly names: Map<string, Set<string>>;\n}\n\n/** Holds and record names coordinate across separately built package entries. */\nconst registry = sharedRegistry(\n \"stimeo-ui.attribute-lease.registry.v1\",\n (): AttributeLeaseRegistry => ({ stacks: new WeakMap(), names: new Map() }),\n);\n\n/** Whether a parsed record entry is an attribute value: a string, or `null` for absence. */\nfunction isAttributeValue(value: unknown): value is string | null {\n return value === null || typeof value === \"string\";\n}\n\n/**\n * Reads the record a lease left on `element`: the author's value, the value the lease wrote\n * last, whether it hands back the value beneath it (`false` before the names when it does not)\n * and the records beneath it. `undefined` when the record is missing or holds anything else.\n */\nfunction readLeaseRecord(element: Element, record: string): AttributeLeaseRecord | undefined {\n try {\n const parsed: unknown = JSON.parse(element.getAttribute(record) ?? \"null\");\n if (!Array.isArray(parsed)) return undefined;\n const [original, written, ...rest] = parsed as unknown[];\n const linked = rest[0] !== false;\n const beneath = linked ? rest : rest.slice(1);\n if (!beneath.every((under): under is string => typeof under === \"string\")) return undefined;\n return isAttributeValue(original) && isAttributeValue(written)\n ? { original, written, linked, beneath }\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** The element's stack, seeded from complete ownership records once. */\nfunction attributeStack(element: Element, attribute: string): AttributeHold[] {\n const byAttribute = registry.stacks.get(element) ?? new Map<string, AttributeHold[]>();\n registry.stacks.set(element, byAttribute);\n const known = byAttribute.get(attribute);\n if (known) return known;\n const found: (AttributeLeaseRecord & { readonly record: string })[] = [];\n for (const record of registry.names.get(attribute) ?? []) {\n const read = readLeaseRecord(element, record);\n if (read) found.push({ ...read, record });\n }\n const depth = ({ beneath }: (typeof found)[number]) =>\n beneath.filter((name) => found.some((other) => other.record === name)).length;\n const stack = found\n .sort((one, other) => depth(one) - depth(other))\n .map(({ record, written, original, linked }, index) => ({\n lease: null,\n record,\n written,\n original,\n linked: linked && index > 0,\n }));\n byAttribute.set(attribute, stack);\n return stack;\n}\n\n/** The lease's live hold or the unclaimed hold described by its owner's record. */\nfunction findHold(\n stack: readonly AttributeHold[],\n lease: object,\n record: string,\n): AttributeHold | undefined {\n return (\n stack.find((held) => held.lease === lease) ??\n stack.find((held) => held.lease === null && held.record === record)\n );\n}\n\n/**\n * Inspects a derived writer's continuation without acquiring or returning a hold.\n * A covered hold may update beneath its cover. Without a hold, the expected authored\n * value is compared with the original beneath any temporary cover. The original\n * distinguishes an absent attribute from an empty one.\n */\nexport function readAttributeLease(\n element: Element,\n attribute: string,\n owner: string,\n lease: object,\n expected: string | null,\n): { readonly allowed: boolean; readonly original: string | null } {\n const stack = attributeStack(element, attribute);\n const hold = findHold(stack, lease, `data-${owner}-${attribute}-lease`);\n const current = element.getAttribute(attribute);\n const covering = [...stack].reverse().find((other) => other.written === current);\n const original = (hold ?? covering)?.original;\n const authored = original === undefined ? current : original;\n return {\n original: authored,\n allowed: hold\n ? current === hold.written ||\n stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current)\n : authored === expected,\n };\n}\n\n/**\n * The value `element`'s author wrote for `attribute`, as the record a lease of any owner leaves\n * on the element carries it; `undefined` while no lease leaves one, as when nothing has moved\n * the attribute off the author's value. A field a reveal turned from `password` into `text`\n * still reads `\"password\"` here.\n */\nexport function leasedAuthorValue(element: Element, attribute: string): string | null | undefined {\n const suffix = `-${attribute}-lease`;\n for (const name of element.getAttributeNames()) {\n if (!name.startsWith(\"data-\") || !name.endsWith(suffix)) continue;\n const record = readLeaseRecord(element, name);\n if (record) return record.original;\n }\n return undefined;\n}\n\n/**\n * Temporarily controls one attribute across a changing set of elements.\n *\n * The first write remembers the authored value, including the distinction between\n * an absent attribute and an authored empty string. Returning a lease restores that\n * value only while the attribute still matches the controller's last write. If a\n * consumer changed it in the meantime, the consumer owns the new value and teardown\n * leaves it alone.\n *\n * A `null` write deliberately removes the attribute while retaining the lease. This\n * is useful for derived ARIA whose valid absence is itself controller state, such as\n * an unbounded `aria-valuemin` or a blank spinbutton's `aria-valuenow`.\n *\n * **Every lease of an attribute shares one stack of holds per element**, whatever its owner\n * and whichever file of the package constructed it (the stacks are one per page): the holds\n * sit in the order their leases first wrote the element, each with the value it wrote last.\n * The element's value is the highest hold's that carries it, and the page's when none does.\n * A write reaches the element unless a hold above the writer carries the element's value: the\n * top hold always shows its writes, a hold beneath it shows nothing while a hold above keeps\n * its value on the element, and once the page has put a value there that no hold carries, the\n * next write of any hold replaces it. A return gives a value back only from the hold the\n * element's value is: the value the hold beneath it wrote last, or the author's once nothing\n * is beneath it; any other return only leaves the stack, and the page's value stays. Holds\n * that wrote the same value are told apart by the stack, never by the value: the higher one\n * keeps it. A first write over a top hold whose value the page has replaced takes the\n * element's value as the author's. Two instances of one controller on one element — an\n * element outside their scope — stack the same way.\n *\n * A **base lease** (`{ base: true }`) holds the element's own state rather than a value over\n * it — a form field's ARIA on its control is one — so its first write goes to the bottom of\n * the stack, beneath the leases of other owners already there: a temporary value another\n * owner leased stays shown and returns to the base lease's latest value.\n *\n * **The stack travels with the element.** While a hold shows a value other than the author's,\n * lies on another hold or is covered by one, the element carries its owner's record\n * `data-<owner>-<attribute>-lease`: JSON of the author's value and the value the hold wrote\n * last (each a string, or `null` for an absent attribute), `false` where the hold hands back\n * the author's value rather than the latest value of the hold beneath it, and the records of\n * every hold beneath it, nearest first. Returning the last lease of an owner removes its\n * record. A copy of the element — a page Turbo restores from its cache is one — therefore\n * carries the stack, and the first lease to touch the copy reads it back under the record\n * names of the leases constructed so far, which include every lease that wrote a record the\n * cache holds, and orders the holds by the records each one lists beneath it. Each lease takes\n * the hold its owner's record describes over in place. Complete records for distinct owners\n * preserve their order across a copy. The records describe holds by owner, not by instance:\n * one owner holding the element more than once leaves only its topmost hold's record, which\n * the first of its leases to write or return on the copy takes. Lower holds of that owner and\n * their positions are absent; mutually listed owners cannot express their full live order.\n * A hold that showed the author's value alone carries no record and is taken by its first\n * write on the copy. Missing or unreadable records also leave their holds unknown, so copying\n * the DOM does not promise identical decisions for every live stack. Without a record it can\n * read, a new stack takes the current value as the author's. A Turbo morph removes records\n * absent from the server HTML; existing holds remain in memory until their leases return.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events,\n * and nothing rewinds a live page before Turbo caches it: `turbo:before-cache` also\n * fires on pages that stay.\n */\nexport class AttributeLease<T extends Element = Element> {\n readonly #attribute: string;\n readonly #record: string;\n readonly #base: boolean;\n readonly #preserveAuthor: boolean;\n readonly #held = new Set<T>();\n\n /**\n * @param attribute - The attribute whose temporary values this lease owns.\n * @param owner - Names the record on the element; the controller's identifier.\n * @param options - `base`: written beneath the other owners' leases;\n * `preserveAuthor`: a new base hold keeps a current value that no hold carries as authored.\n */\n constructor(\n attribute: string,\n owner: string,\n options: { readonly base?: boolean; readonly preserveAuthor?: boolean } = {},\n ) {\n this.#attribute = attribute;\n this.#record = `data-${owner}-${attribute}-lease`;\n this.#base = options.base === true;\n this.#preserveAuthor = options.preserveAuthor === true;\n const names = registry.names;\n names.set(attribute, (names.get(attribute) ?? new Set<string>()).add(this.#record));\n }\n\n /** Writes or removes the leased attribute while preserving its authored value. */\n write(element: T, value: string | null): void {\n const stack = attributeStack(element, this.#attribute);\n const hold = this.#take(stack) ?? this.#acquire(element, stack);\n this.#held.add(element);\n const shows = !this.#covered(element, stack, hold);\n hold.written = value;\n this.#mark(element, stack);\n if (shows) this.#reflect(element, value);\n }\n\n /**\n * Returns one lease, also on a restored copy, to the value the hold beneath it wrote last or\n * the author's, without overwriting a value a consumer wrote since. Says whether there was a\n * lease to return: a hold this lease has, or the hold a copy's record describes while no live\n * lease of the owner has taken it — a copy of a page an earlier connection wrote carries one.\n */\n return(element: T): boolean {\n const held = this.#held.delete(element);\n const known = registry.stacks.get(element)?.has(this.#attribute);\n const stack =\n held || known || element.hasAttribute(this.#record)\n ? attributeStack(element, this.#attribute)\n : [];\n const hold = this.#take(stack);\n if (!hold) return false;\n const carries = element.getAttribute(this.#attribute) === hold.written;\n const owns = carries && !this.#covered(element, stack, hold);\n const index = stack.indexOf(hold);\n stack.splice(index, 1);\n const above = stack[index];\n if (above) above.linked &&= hold.linked;\n this.#mark(element, stack);\n if (!owns) return true;\n const below = hold.linked ? stack[index - 1] : undefined;\n this.#reflect(element, below ? below.written : hold.original);\n return true;\n }\n\n /** Returns every lease it holds, with the same ownership check as {@link return}. */\n returnAll(): void {\n for (const element of Array.from(this.#held)) this.return(element);\n }\n\n /** Its hold in `stack`, taking over one read from its record. */\n #take(stack: AttributeHold[]): AttributeHold | undefined {\n const hold = findHold(stack, this, this.#record);\n if (hold) hold.lease = this;\n return hold;\n }\n\n /** A new hold: on top, over the top hold while the element shows it; a base one at the bottom. */\n #acquire(element: T, stack: AttributeHold[]): AttributeHold {\n const current = element.getAttribute(this.#attribute);\n const top = stack.at(-1);\n const bottom = stack[0];\n const over = top?.written === current ? top : undefined;\n const from = this.#base\n ? this.#preserveAuthor && !stack.some((held) => held.written === current)\n ? undefined\n : bottom\n : over;\n const hold: AttributeHold = {\n lease: this,\n record: this.#record,\n written: current,\n original: from ? from.original : current,\n linked: !this.#base && over !== undefined,\n };\n if (this.#base && bottom) {\n bottom.linked = true;\n stack.unshift(hold);\n } else stack.push(hold);\n return hold;\n }\n\n /** Whether a hold above `hold` carries the element's value, which is then that hold's. */\n #covered(element: T, stack: readonly AttributeHold[], hold: AttributeHold): boolean {\n const current = element.getAttribute(this.#attribute);\n return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);\n }\n\n /** Leaves each owner's record of its topmost hold, or none where nothing needs one. */\n #mark(element: T, stack: readonly AttributeHold[]): void {\n const records = new Map<string, string | null>([[this.#record, null]]);\n stack.forEach((hold, index) => {\n const names = stack.slice(0, index).map((under) => under.record);\n const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);\n const marker = hold.linked || beneath.length === 0 ? [] : [false];\n records.set(\n hold.record,\n hold.written === hold.original && beneath.length === 0 && index === stack.length - 1\n ? null\n : JSON.stringify([hold.original, hold.written, ...marker, ...beneath]),\n );\n });\n for (const [name, recorded] of records) {\n setAttributeIfChanged(element, name, recorded);\n }\n }\n\n /** Reflects only a real value transition, avoiding self-triggered mutation work. */\n #reflect(element: T, value: string | null): void {\n setAttributeIfChanged(element, this.#attribute, value);\n }\n}\n","/**\n * The activation verdict a click took as it started, kept for every action that\n * click goes on to reach.\n *\n * A consumer handler on an entry can run before the action bound there, and may\n * change the entry while the click runs — mark it `disabled` or `aria-disabled` while\n * the command it starts is in flight. Judging the entry again when the action runs\n * would then take back the activation the click started. So a controller judges a\n * click once, in a capture listener on its own element that runs before the handlers\n * inside that element, records the verdict here with every entry that held\n * the click's target, and each action that click reaches reads the verdict back for\n * the entry it resolved.\n *\n * A verdict is read back only for an entry it was recorded with. An action that\n * resolves another entry — a handler moved the target into one — reads `undefined`\n * and judges that entry when it runs, as it does every event that is not a click.\n * Records are keyed by the event and are released with it.\n */\nexport class ClickVerdicts {\n readonly #verdicts = new WeakMap<\n Event,\n { readonly entries: readonly Element[]; readonly refused: boolean }\n >();\n\n /**\n * Records whether `event` is refused, with the `entries` that held its target as it\n * started. A later record of the same event replaces this one, and a record with no\n * entries matches no entry.\n */\n record(event: Event, entries: readonly Element[], refused: boolean): void {\n this.#verdicts.set(event, { entries, refused });\n }\n\n /**\n * The verdict recorded for `event` when `entry` is one of the entries it was\n * recorded with; `undefined` when there is no event, the event was never recorded,\n * or it was recorded without `entry`.\n */\n for(event: Event | null | undefined, entry: Element): boolean | undefined {\n const verdict = event && this.#verdicts.get(event);\n if (!verdict?.entries.includes(entry)) return undefined;\n return verdict.refused;\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered. The map is one per page\n * (`sharedRegistry`), so the layers of controllers imported from different\n * files share one stack and one listener.\n */\n\nimport { sharedRegistry } from \"./shared_registry\";\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** An active layer on its document's stack: what the resolver reads, from any copy. */\ninterface LayerEntry {\n readonly onDismiss: () => void;\n readonly claims: (() => boolean) | null;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n readonly stack: LayerEntry[];\n readonly onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = sharedRegistry(\n \"stimeo-ui.escape-layer.registry.v1\",\n () => new WeakMap<Document, EscapeLayerRegistry>(),\n );\n\n /** The document and the entry of this layer while it is active. */\n #active: { readonly document: Document; readonly entry: LayerEntry } | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };\n registry.stack.push(entry);\n this.#active = { document: ownerDocument, entry };\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const active = this.#active;\n if (!active) return;\n\n const registry = EscapeLayer.#registries.get(active.document);\n if (registry) {\n const index = registry.stack.lastIndexOf(active.entry);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n active.document.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(active.document);\n }\n }\n this.#active = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const active = this.#active;\n if (!active) return false;\n const registry = EscapeLayer.#registries.get(active.document);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === active.entry;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.onDismiss();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: readonly LayerEntry[]): LayerEntry | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.claims && !layer.claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Resolves which of a controller's elements owns a node.\n *\n * A delegated listener hears events from a whole subtree, so the handler's first\n * job is almost always the same question: which item, handle, or control does\n * this `event.target` belong to? The same question comes up for a\n * `MutationRecord.target`, for `document.activeElement`, and for a\n * `relatedTarget` on the way out of a hover region.\n *\n * The answer is `Node.contains()`, which is **inclusive** — an element contains\n * itself — so testing the candidate for identity as well would be redundant.\n *\n * **The guard is what makes this safe to call with a raw event target.**\n * `event.target` is typed `EventTarget | null`, and `contains()` takes a `Node?`:\n * browsers throw `TypeError` for anything else, and `window` — the everyday\n * `EventTarget` that is not a `Node` — is what an event dispatched at it carries.\n * Narrowing here means a caller never has to cast, and the rule cannot drift\n * between the places that ask the question.\n *\n * Scope stays with the caller. These helpers say *which candidate owns the node*,\n * not *whether the node belongs to this controller at all* — a component with\n * nested instances of itself decides that first (by comparing the closest\n * annotated ancestor) and passes the candidates it owns.\n *\n * Candidates can nest, and then more than one holds the node: {@link ownerIndex}\n * and {@link ownerOf} answer the first one listed, {@link ownersOf} all of them.\n */\n\n/**\n * Index in `candidates` of the first one that is, or contains, `node`.\n *\n * `-1` when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. Candidates are tested in array order, so a nested pair\n * resolves to whichever the caller listed first.\n */\nexport function ownerIndex<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): number {\n if (!(node instanceof Node)) return -1;\n return candidates.findIndex((candidate) => candidate.contains(node));\n}\n\n/**\n * The first candidate that is, or contains, `node`; `null` when none does.\n *\n * A miss indexes the array at `-1`, which reads as `undefined` and lands on the\n * same `null` the absent cases produce.\n */\nexport function ownerOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T | null {\n return candidates[ownerIndex(candidates, node)] ?? null;\n}\n\n/**\n * Every candidate that is, or contains, `node`, in array order.\n *\n * Empty when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. A caller asking whether any owner of a node refuses\n * something reads the whole list, since an owner nested in another one holds the\n * node too.\n */\nexport function ownersOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T[] {\n if (!(node instanceof Node)) return [];\n return candidates.filter((candidate) => candidate.contains(node));\n}\n","/** What {@link ListenerSet.add} passes on besides the signal it supplies itself. */\nexport type ListenerOptions = Omit<AddEventListenerOptions, \"signal\">;\n\n/**\n * The event map a target's own `addEventListener` types its handlers with: the\n * window's, a document's, an HTML or SVG element's, any other element's, or a\n * media query list's. Other targets name none.\n */\ntype EventMapOf<T extends EventTarget> = T extends Window\n ? WindowEventMap\n : T extends Document\n ? DocumentEventMap\n : T extends HTMLElement\n ? HTMLElementEventMap\n : T extends SVGElement\n ? SVGElementEventMap\n : T extends Element\n ? ElementEventMap\n : T extends MediaQueryList\n ? MediaQueryListEventMap\n : Record<never, never>;\n\n/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt.\n * {@link ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * A lifetime shorter than the connection — while a Value holds, until a first\n * load, while a subscription is open — gets a set of its own, which the end of\n * that lifetime disposes. A controller therefore holds one set per lifetime: the\n * connection's, and one for each swapped receiver or shorter span.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, listeners of\n * several elements that come and go one at a time, and those one gesture holds\n * keep their own pair of calls or a signal of their own.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included. A removal by hand would have to repeat that\n * flag; aborting the signal releases the listener without it.\n *\n * A `type` the target's own event map names types `handler` with that event,\n * as the target's `addEventListener` does, so a handler written for a\n * `KeyboardEvent` is passed as it is; any other `type` takes an\n * `EventListener`.\n */\n add<T extends EventTarget, K extends keyof EventMapOf<T> & string>(\n target: T,\n type: K,\n handler: (event: EventMapOf<T>[K]) => void,\n options?: ListenerOptions,\n ): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. One pending timer per key is `KeyedTimers`\n * (`keyed_timers.ts`), and a timer a person can hold open, with its remaining-time\n * accounting, is `PausableTimers` (`pausable_timers.ts`).\n */\n\nexport { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { actionSource } from \"../utils/action_source\";\nimport { isAriaDisabled } from \"../utils/aria_disabled\";\nimport { hasModifierChord, isReservedArrowChord } from \"../utils/arrow_step\";\nimport { AttributeLease } from \"../utils/attribute_lease\";\nimport { ClickVerdicts } from \"../utils/click_verdicts\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { ownerOf, ownersOf } from \"../utils/event_owner\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { type StateReason, stateReasonFor } from \"../utils/state_reason\";\n\n/**\n * Headless, accessible **context menu** behavior.\n *\n * Markup contract (identifier: `stimeo--context-menu`):\n * <div data-controller=\"stimeo--context-menu\">\n * <div data-stimeo--context-menu-target=\"region\" tabindex=\"0\"\n * role=\"group\" aria-label=\"Files\"\n * aria-keyshortcuts=\"Shift+F10\" aria-controls=\"ctx\"\n * data-action=\"contextmenu->stimeo--context-menu#open\n * keydown->stimeo--context-menu#onRegionKeydown\">…</div>\n * <ul id=\"ctx\" role=\"menu\" data-stimeo--context-menu-target=\"menu\" hidden>\n * <li role=\"none\">\n * <button role=\"menuitem\" tabindex=\"-1\"\n * data-stimeo--context-menu-target=\"item\"\n * data-action=\"click->stimeo--context-menu#activate\n * keydown->stimeo--context-menu#onItemKeydown\">…</button>\n * </li>\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Menu** pattern. The trigger is a `contextmenu`\n * event or the `Shift+F10` / `ContextMenu` key rather than a button click, the\n * menu is shown at the pointer coordinate, open state is exposed as `data-state`\n * rather than `aria-expanded`, and each item carries its own `data-action`\n * bindings.\n *\n * @remarks\n * Behavior only — the controller reflects the click coordinate as the CSS custom\n * properties `--stimeo--context-menu-x` / `--stimeo--context-menu-y` on the menu\n * so the consumer's CSS can place it (works standalone, no positioning module\n * required). Viewport-edge flip/shift is delegated to the opt-in\n * `stimeo-ui/positioning` module, which this controller never imports.\n *\n * The region's role and name are the author's: a `role=\"group\"` with\n * `aria-label`, or an element that has a role of its own (a table row, a list\n * item, a button), chosen to match what the region holds. The controller writes\n * no role. The author also points `aria-controls` at the menu and advertises the\n * opening key with `aria-keyshortcuts=\"Shift+F10\"`. The region carries no\n * `aria-haspopup`, whatever its role. On a role that supports it, such as a\n * button, the attribute conveys what ARIA 1.2's note says its visual\n * counterparts, such as a downward-pointing triangle, signal: that activating\n * the control (a click, `Enter`) displays a popup. This menu opens on a\n * `contextmenu` event or the `Shift+F10` / `ContextMenu` key instead, so the\n * attribute would point to the wrong gesture. On a role that does not, such as\n * `generic`, `group`, `row` or `listitem`, it would be a global use of the\n * attribute, which ARIA 1.2 deprecates.\n *\n * Open state is exposed on the region as `data-state` (`open`/`closed`) — a CSS\n * hook, not an ARIA one. `aria-expanded` is deliberately *not* set on the region:\n * the role is the author's, and ARIA 1.2 lists neither `generic` nor `group`\n * among the roles that support that state. Assistive tech perceives the open\n * state when focus moves into the `role=\"menu\"`.\n *\n * Behavior provided:\n * - `contextmenu` on the region suppresses the browser menu and opens this one at\n * the pointer; `Shift+F10` / `ContextMenu` opens it at the region's center.\n * - On open, focus moves to the first item; the region's `data-state` syncs.\n * - Roving focus inside the menu: `ArrowUp`/`ArrowDown` (wrapping), `Home`/`End`.\n * - Activating an enabled item (click / native `Enter`/`Space` on the button)\n * closes the menu and restores focus to the region. An item that is `hidden`,\n * natively `disabled`, or `aria-disabled=\"true\"` itself or through an ancestor\n * is not activated on any path, and a click on an `aria-disabled` one is also\n * stopped, in a capture listener on the controller element, before the click\n * handlers inside that element run. Items can nest, and an event is\n * refused when any item holding its target refuses. A click is judged once, in\n * that capture pass, so a consumer handler that disables the item while the\n * click runs does not take back the activation. The verdict holds only for an\n * item that held the click's target when it was taken: an item it was not taken\n * for — a handler moved the target into another one — is judged when\n * {@link activate} runs, as is any other event.\n * - `Escape` closes and restores focus to the region. While open the menu is a\n * layer on the shared `EscapeLayer` stack; it claims a press only while\n * focus is inside the controller or fell to the body, so one keypress closes\n * exactly one layer. `Tab` lets the browser move focus first, then closes on\n * the next task. An outside click or context-menu invocation closes without\n * stealing focus from its destination.\n * - A region or a menu that takes over — in one task, or after an earlier one\n * leaves in a later task — carries the open state, and the menu that takes over\n * an open one is placed where it was opened; focus stays where the swap left it.\n * With no menu left the region reads closed and the menu leaves the Escape stack.\n * One that stops resolving as the target gets back the `data-state` or `hidden` it\n * carried before this controller wrote on it.\n * - Each move of the open state is reported: `stimeo--context-menu:open` and\n * `stimeo--context-menu:close` dispatch `{ reason: StateReason }`, after the\n * state attributes are written. Both are informational, so neither is\n * cancelable. A call that leaves the state where it already was, the\n * normalization in {@link connect}, a region or a menu that takes over, and\n * {@link disconnect} are all silent.\n *\n * Roving focus skips `hidden` and natively `disabled` items. An item that\n * `aria-disabled=\"true\"` reaches, on the item or an ancestor, stays reachable by\n * arrow keys — APG marks that attribute precisely for controls that must remain\n * discoverable — while its activation is suppressed, so it announces itself and\n * does nothing.\n */\nexport class ContextMenuController extends Controller<HTMLElement> {\n /** The custom properties that carry the coordinate the menu was opened at, x then y. */\n static readonly #COORDINATE = [\"--stimeo--context-menu-x\", \"--stimeo--context-menu-y\"] as const;\n\n static override targets = [\"region\", \"menu\", \"item\"];\n static actions = [\"activate\", \"onItemKeydown\", \"onRegionKeydown\", \"open\"] as const;\n static events = [\"close\", \"open\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly regionTargets: HTMLElement[];\n declare readonly menuTarget: HTMLElement;\n declare readonly menuTargets: HTMLElement[];\n declare readonly itemTargets: HTMLButtonElement[];\n declare readonly hasRegionTarget: boolean;\n declare readonly hasMenuTarget: boolean;\n\n readonly #timers = new SafeTimeout();\n\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n /** Borrows `hidden` on the menu, to give back when an element stops being the menu. */\n readonly #menuHidden = new AttributeLease<HTMLElement>(\"hidden\", this.identifier);\n /** Borrows `data-state` on the region, for the same return. */\n readonly #regionState = new AttributeLease<HTMLElement>(\"data-state\", this.identifier);\n /** The menu the open state was last applied to. */\n #menu: HTMLElement | null = null;\n\n /** Whether `connect()` has run for this connection; target callbacks arrive outside it too. */\n #connected = false;\n\n /** Whether state moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /** The activation verdict the capture pass took for a click, with the items it judged. */\n readonly #clickVerdicts = new ClickVerdicts();\n\n /** The listeners of one connection, released together on `disconnect()`. */\n readonly #listeners = new ListenerSet();\n\n /** Starts closed and registers delegated activation and outside-pointer listeners. */\n override connect(): void {\n this.#closeMenu(\"api\");\n this.#listeners.add(this.element, \"click\", this.#onItemClickCapture, { capture: true });\n this.#listeners.add(document, \"click\", this.#onOutsidePointer, { capture: true });\n this.#listeners.add(document, \"contextmenu\", this.#onOutsidePointer, { capture: true });\n this.#connected = true;\n this.#reporting = true;\n }\n\n /** Releases the listeners, stack membership, and pending Tab-close task. */\n override disconnect(): void {\n this.#connected = false;\n this.#reporting = false;\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n this.#listeners.dispose();\n }\n\n /** Brings a region that arrives after connect to the open state. */\n regionTargetConnected(): void {\n if (this.#connected) this.#reflectRegion();\n }\n\n /**\n * Gives a region that no longer resolves as one its own `data-state` back — after\n * `disconnect()` too, since dropping the identifier leaves the element on the page — and\n * brings the region that stays to the open state.\n */\n regionTargetDisconnected(region: HTMLElement): void {\n if (!this.regionTargets.includes(region)) this.#regionState.return(region);\n if (this.#connected) this.#reflectRegion();\n }\n\n /** Applies the open state to a menu that arrives after connect in front of the others. */\n menuTargetConnected(): void {\n this.#adoptMenu();\n }\n\n /**\n * Applies the open state to the menu left, then gives a menu that no longer resolves as the\n * target its own `hidden` back — after `disconnect()` too, since dropping the identifier\n * leaves the element on the page. The open state is read off the departing menu first,\n * while it still carries it.\n */\n menuTargetDisconnected(menu: HTMLElement): void {\n this.#adoptMenu();\n if (!this.menuTargets.includes(menu)) this.#menuHidden.return(menu);\n }\n\n /**\n * Opens at a mouse event's coordinate, an explicit element's center, or the\n * region's center with no argument. Opening focuses the new menu in all paths.\n * An explicit element may be an anchor outside the controller.\n */\n open(input?: Event | HTMLElement): void {\n const source = actionSource(input);\n source.event?.preventDefault();\n if (source.event instanceof MouseEvent) {\n this.#openAt(source.event.clientX, source.event.clientY, source.reason);\n return;\n }\n const anchor = source.host ?? (this.hasRegionTarget ? this.regionTarget : null);\n const rect = anchor?.getBoundingClientRect() ?? { left: 0, top: 0, width: 0, height: 0 };\n this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, source.reason);\n }\n\n /** Keyboard entry on the region: `Shift+F10` / `ContextMenu` open at center. */\n onRegionKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n const isContextKey = event.key === \"ContextMenu\" || (event.key === \"F10\" && event.shiftKey);\n if (!isContextKey) return;\n event.preventDefault();\n const rect = this.hasRegionTarget\n ? this.regionTarget.getBoundingClientRect()\n : { left: 0, top: 0, width: 0, height: 0 };\n this.#openAt(rect.left + rect.width / 2, rect.top + rect.height / 2, stateReasonFor(event));\n }\n\n /** Roving focus and closing keys inside the menu. */\n onItemKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n if ((event.key === \"Home\" || event.key === \"End\") && hasModifierChord(event)) return;\n const items = this.#navigableItems;\n const currentIndex = items.indexOf(event.currentTarget as HTMLButtonElement);\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n if (items.length > 0) {\n const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length;\n items[nextIndex]?.focus();\n }\n break;\n case \"ArrowUp\":\n event.preventDefault();\n if (items.length > 0) {\n const previousIndex = currentIndex < 0 ? items.length - 1 : currentIndex - 1;\n items[(previousIndex + items.length) % items.length]?.focus();\n }\n break;\n case \"Home\":\n event.preventDefault();\n items[0]?.focus();\n break;\n case \"End\":\n event.preventDefault();\n items[items.length - 1]?.focus();\n break;\n case \"Tab\":\n // Closing synchronously removes the focused item before the browser's\n // default Tab action, which can restart traversal at the document head.\n this.#timers.clearAll();\n this.#timers.set(() => this.#closeMenu(\"focus\"), 0);\n break;\n default:\n break;\n }\n }\n\n /**\n * Closes after an item is activated and restores focus to the region. An event\n * whose target lies in an item that is `hidden`, natively `disabled`, or\n * `aria-disabled=\"true\"` itself or through an ancestor does nothing: the menu\n * stays open, nothing is dispatched and focus stays where it is. Every item\n * holding the target counts, so an item nested in another one refuses for both,\n * wherever the action is bound. A click is judged as its capture pass found the\n * items, before the handlers inside the controller element ran, when the\n * item this resolves was among them; an item that was not, and any other event, is\n * judged when this runs.\n * An event that resolves to no item, and a call with no event, always close.\n */\n activate(event?: Event): void {\n const item = ownerOf(this.itemTargets, event?.target);\n if (item !== null && this.#refuses(item, event)) return;\n this.#closeAndRestore(\"select\");\n }\n\n /**\n * Whether the activation `event` asks of `item` is refused: the verdict the capture\n * pass took for the click when `item` was among the items holding its target,\n * otherwise whether any item holding the event's target refuses activation now.\n */\n #refuses(item: HTMLButtonElement, event: Event | undefined): boolean {\n const recorded = this.#clickVerdicts.for(event, item);\n if (recorded !== undefined) return recorded;\n return ownersOf(this.itemTargets, event?.target).some((owner) =>\n this.#isActivationDisabled(owner),\n );\n }\n\n /** Opens the menu at viewport coordinates `(x, y)` and focuses the first item. */\n #openAt(x: number, y: number, reason: StateReason): void {\n if (!this.hasMenuTarget) return;\n this.#timers.clearAll();\n const menu = this.menuTarget;\n const was = this.#isOpen;\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(\"escape\"),\n claims: claimsWhileFocusWithin(this.element),\n });\n menu.style.setProperty(ContextMenuController.#COORDINATE[0], `${x}px`);\n menu.style.setProperty(ContextMenuController.#COORDINATE[1], `${y}px`);\n this.#menuHidden.write(menu, null);\n this.#menu = menu;\n if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, \"open\");\n if (!was && this.#reporting) this.dispatch(\"open\", { detail: { reason }, cancelable: false });\n // A subscriber may close it again from the handler above; focusing then puts\n // the caret on an item nobody can see.\n if (!this.#isOpen) return;\n this.#navigableItems[0]?.focus();\n }\n\n /** Hides the menu, reflects the collapsed state on the region, and reports a move. */\n #closeMenu(reason: StateReason): void {\n this.#timers.clearAll();\n this.#escapeLayer.deactivate();\n const menu = this.hasMenuTarget ? this.menuTarget : null;\n this.#menu = menu;\n if (!menu) return;\n const was = this.#isOpen;\n this.#menuHidden.write(menu, \"\");\n if (this.hasRegionTarget) this.#regionState.write(this.regionTarget, \"closed\");\n if (was && this.#reporting) this.dispatch(\"close\", { detail: { reason }, cancelable: false });\n }\n\n /**\n * Moves the open state onto the menu that is now first, when that menu changed: one that\n * takes over an open menu is shown where that one was opened, and one that arrives with none\n * before it arrives closed. With none left, the menu leaves the Escape stack. The region then\n * reads the menu, changed or not. Focus stays where the swap left it, and nothing is\n * dispatched.\n */\n #adoptMenu(): void {\n if (!this.#connected) return;\n const menu = this.hasMenuTarget ? this.menuTarget : null;\n const previous = this.#menu;\n this.#menu = menu;\n if (!menu) {\n this.#escapeLayer.deactivate();\n } else if (menu !== previous) {\n const open = previous !== null && !previous.hidden;\n if (open) {\n for (const property of ContextMenuController.#COORDINATE) {\n menu.style.setProperty(property, previous.style.getPropertyValue(property));\n }\n }\n this.#menuHidden.write(menu, open ? null : \"\");\n }\n this.#reflectRegion();\n }\n\n /** Writes the open state onto the region that is first. */\n #reflectRegion(): void {\n if (!this.hasRegionTarget) return;\n this.#regionState.write(this.regionTarget, this.#isOpen ? \"open\" : \"closed\");\n }\n\n /** Closes the menu and returns focus to the region (Escape / activation). */\n #closeAndRestore(reason: StateReason): void {\n this.#closeMenu(reason);\n if (this.hasRegionTarget) this.regionTarget.focus();\n }\n\n /**\n * Closes when a click or context-menu invocation lands outside this instance.\n * Both subscriptions observe in the capture phase, so the target is judged\n * against the tree the user actually pressed even when a consumer handler\n * detaches it, and application code that stops bubbling cannot leave the menu\n * open.\n */\n readonly #onOutsidePointer = (event: MouseEvent): void => {\n if (this.#isOpen && !this.element.contains(event.target as Node)) this.#closeMenu(\"outside\");\n };\n\n /**\n * Judges each click on an item before the handlers inside the controller\n * element run: records whether any item holding the target refuses activation, with\n * all of those items, so {@link activate} acts on this one verdict for any of them,\n * and stops a click that `aria-disabled` reaches — an item's own state or an\n * ancestor's — so the command cannot reach those handlers. Every item that holds the\n * target counts, so an `aria-disabled` item nested in an enabled one stops the click\n * too. Native Enter/Space activation also synthesizes a click and is blocked here.\n */\n readonly #onItemClickCapture = (event: MouseEvent): void => {\n const items = ownersOf(this.itemTargets, event.target);\n this.#clickVerdicts.record(\n event,\n items,\n items.some((item) => this.#isActivationDisabled(item)),\n );\n if (!items.some((item) => isAriaDisabled(item))) return;\n event.preventDefault();\n event.stopImmediatePropagation();\n };\n\n /** Menu items eligible for roving focus (excludes hidden / natively disabled). */\n get #navigableItems(): HTMLButtonElement[] {\n return this.itemTargets.filter((item) => this.#isNavigable(item));\n }\n\n /**\n * An item can take roving focus unless it is `hidden` or a natively `disabled`\n * form control. CSS-only visibility is not detectable here and is the\n * consumer's responsibility.\n *\n * **`aria-disabled=\"true\"` stays reachable**, on the item or an ancestor. APG\n * separates the two attributes by intent: `disabled` is for controls whose\n * existence can be inferred from a neighbour (a greyed Next next to a Prev), while\n * `aria-disabled` marks a control that must stay *discoverable* — and it names\n * menu items as the example. Skipping it would hide the command's existence from a\n * keyboard user entirely. Activation is suppressed separately, so the item\n * announces itself and does nothing.\n */\n #isNavigable(item: HTMLButtonElement): boolean {\n if (item.hasAttribute(\"hidden\")) return false;\n return !item.disabled;\n }\n\n /**\n * Whether an item refuses activation: one roving focus skips (`hidden`, natively\n * `disabled`), or one that `aria-disabled=\"true\"` reaches.\n */\n #isActivationDisabled(item: HTMLButtonElement): boolean {\n return !this.#isNavigable(item) || isAriaDisabled(item);\n }\n\n /** Whether the menu is currently visible. */\n get #isOpen(): boolean {\n return this.hasMenuTarget && !this.menuTarget.hidden;\n }\n}\n"]}
@@ -16,7 +16,9 @@ import { Controller } from '@hotwired/stimulus';
16
16
  * real value); `start` animates the displayed text from `from` to it over
17
17
  * `duration` ms with an ease-out curve, then restores the exact authored text.
18
18
  * With `once` (default) later starts are ignored (`data-count-up-done` records
19
- * a finished run across Turbo cache restores).
19
+ * a finished run across Turbo cache restores). `from` and `duration` are read as
20
+ * a run starts: a running animation keeps the values it began with, the next run
21
+ * reads the new ones, and a change on its own starts nothing.
20
22
  *
21
23
  * Only the text node holding the number is animated, so sibling markup — a unit
22
24
  * in a `<small>`, a label in a `<b>` — is left where the author put it.
@@ -51,6 +53,15 @@ declare class CountUpController extends Controller<HTMLElement> {
51
53
  default: boolean;
52
54
  };
53
55
  };
56
+ static valueConstraints: {
57
+ duration: {
58
+ readonly finite: true;
59
+ readonly exclusiveMin: 0;
60
+ };
61
+ from: {
62
+ readonly finite: true;
63
+ };
64
+ };
54
65
  static actions: readonly ["start"];
55
66
  static events: readonly ["end"];
56
67
  durationValue: number;
@@ -61,6 +72,7 @@ declare class CountUpController extends Controller<HTMLElement> {
61
72
  /**
62
73
  * Starts the animation (typically from `stimeo--intersection:enter` via
63
74
  * `data-action`). No-ops while running, and after a finished run when `once`.
75
+ * The run keeps the `from` and `duration` read here to its last frame.
64
76
  */
65
77
  start(): void;
66
78
  }
@@ -1,8 +1,5 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/count_up_controller.ts
4
-
5
- // src/utils/authored_integer.ts
6
3
  var TOKEN_BODY = /\d[\d.,'’’    _]*\d|\d/;
7
4
  var SIGN_OPENER = /[\s([]/;
8
5
  function authoredInteger(text) {
@@ -24,29 +21,75 @@ function authoredInteger(text) {
24
21
  return signed ? -magnitude || 0 : magnitude;
25
22
  }
26
23
 
27
- // src/utils/reduced_motion.ts
24
+ var NUMBER_BOUNDS = {
25
+ finite: { finite: true },
26
+ positive: { finite: true, exclusiveMin: 0 }};
27
+ function matchesNumberBounds(value, bounds) {
28
+ if (!Number.isFinite(value)) {
29
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
30
+ if (direction === null) return false;
31
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
32
+ }
33
+ if (bounds.min !== void 0 && value < bounds.min) return false;
34
+ if (bounds.max !== void 0 && value > bounds.max) return false;
35
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
36
+ if (bounds.integer && !Number.isInteger(value)) return false;
37
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
38
+ return true;
39
+ }
40
+
41
+ function readNumber(raw, fallback, bounds) {
42
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
43
+ }
44
+
45
+ var NumberValueReader = class {
46
+ #lastRejected = /* @__PURE__ */ new Map();
47
+ read(owner, name, raw, fallback, bounds) {
48
+ const resolved = readNumber(raw, fallback, bounds);
49
+ if (matchesNumberBounds(raw, bounds)) {
50
+ this.#lastRejected.delete(name);
51
+ return resolved;
52
+ }
53
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
54
+ const literal = owner.element.getAttribute(attribute);
55
+ if (literal === null) {
56
+ this.#lastRejected.delete(name);
57
+ return resolved;
58
+ }
59
+ if (this.#lastRejected.get(name) !== literal) {
60
+ this.#lastRejected.set(name, literal);
61
+ console.warn(
62
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
63
+ );
64
+ }
65
+ return resolved;
66
+ }
67
+ };
68
+
28
69
  function prefersReducedMotion() {
29
70
  return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
30
71
  }
31
72
 
32
- // src/controllers/count_up_controller.ts
33
73
  var DEFAULT_DURATION = 1200;
34
- var CountUpController = class extends Controller {
74
+ var CountUpController = class _CountUpController extends Controller {
75
+ #numbers = new NumberValueReader();
35
76
  static values = {
36
77
  duration: { type: Number, default: DEFAULT_DURATION },
37
78
  from: { type: Number, default: 0 },
38
79
  once: { type: Boolean, default: true }
39
80
  };
81
+ static valueConstraints = {
82
+ duration: NUMBER_BOUNDS.positive,
83
+ from: NUMBER_BOUNDS.finite
84
+ };
40
85
  static actions = ["start"];
41
86
  static events = ["end"];
42
87
  #frame = null;
43
- /** The animation length, with a declaration outside its domain read as the default. */
44
88
  get #duration() {
45
- return Number.isFinite(this.durationValue) && this.durationValue > 0 ? this.durationValue : DEFAULT_DURATION;
89
+ return this.#safeDuration;
46
90
  }
47
- /** The starting value, with a declaration that is not a finite number read as zero. */
48
91
  get #from() {
49
- return Number.isFinite(this.fromValue) ? this.fromValue : 0;
92
+ return this.#safeFrom;
50
93
  }
51
94
  connect() {
52
95
  const ticker = this.#ownedTicker();
@@ -58,10 +101,6 @@ var CountUpController = class extends Controller {
58
101
  disconnect() {
59
102
  if (this.#frame !== null) this.#settle();
60
103
  }
61
- /**
62
- * Starts the animation (typically from `stimeo--intersection:enter` via
63
- * `data-action`). No-ops while running, and after a finished run when `once`.
64
- */
65
104
  start() {
66
105
  if (this.#frame !== null) return;
67
106
  if (this.onceValue && this.element.hasAttribute("data-count-up-done")) return;
@@ -76,9 +115,10 @@ var CountUpController = class extends Controller {
76
115
  }
77
116
  const ticker = this.#wrap(node, authored);
78
117
  const from = this.#from;
118
+ const duration = this.#duration;
79
119
  const started = performance.now();
80
120
  const step = (now) => {
81
- const t = Math.min((now - started) / this.#duration, 1);
121
+ const t = Math.min((now - started) / duration, 1);
82
122
  const eased = 1 - (1 - t) ** 3;
83
123
  ticker.textContent = String(Math.round(from + (target - from) * eased));
84
124
  if (t < 1) {
@@ -90,7 +130,6 @@ var CountUpController = class extends Controller {
90
130
  };
91
131
  this.#frame = requestAnimationFrame(step);
92
132
  }
93
- /** The first text node that displays a number, or null when the host has none. */
94
133
  #numericNode() {
95
134
  const walker = document.createTreeWalker(this.element, NodeFilter.SHOW_TEXT);
96
135
  let node = walker.nextNode();
@@ -99,11 +138,9 @@ var CountUpController = class extends Controller {
99
138
  }
100
139
  return node;
101
140
  }
102
- /** The wrapper this controller published, if one outlived its run. */
103
141
  #ownedTicker() {
104
142
  return this.element.querySelector("[data-count-up-label]");
105
143
  }
106
- /** Publishes the ticking number inside a named wrapper, replacing `node`. */
107
144
  #wrap(node, authored) {
108
145
  const ticker = document.createElement("span");
109
146
  ticker.setAttribute("data-count-up-label", "true");
@@ -113,11 +150,9 @@ var CountUpController = class extends Controller {
113
150
  ticker.append(node);
114
151
  return ticker;
115
152
  }
116
- /** Puts `text` back where the wrapper stood, leaving the rest of the host alone. */
117
153
  #unwrap(ticker, text) {
118
154
  ticker.replaceWith(document.createTextNode(text ?? ticker.textContent ?? ""));
119
155
  }
120
- /** Ends the run: cancels the frame and restores the authored presentation. */
121
156
  #settle() {
122
157
  if (this.#frame !== null) cancelAnimationFrame(this.#frame);
123
158
  this.#frame = null;
@@ -125,8 +160,25 @@ var CountUpController = class extends Controller {
125
160
  if (ticker !== null) this.#unwrap(ticker, ticker.getAttribute("aria-label"));
126
161
  this.element.setAttribute("data-count-up-done", "true");
127
162
  }
163
+ get #safeDuration() {
164
+ return this.#numbers.read(
165
+ this,
166
+ "duration",
167
+ this.durationValue,
168
+ _CountUpController.values.duration.default,
169
+ _CountUpController.valueConstraints.duration
170
+ );
171
+ }
172
+ get #safeFrom() {
173
+ return this.#numbers.read(
174
+ this,
175
+ "from",
176
+ this.fromValue,
177
+ _CountUpController.values.from.default,
178
+ _CountUpController.valueConstraints.from
179
+ );
180
+ }
128
181
  };
129
182
 
130
183
  export { CountUpController };
131
- //# sourceMappingURL=count_up_controller.js.map
132
184
  //# sourceMappingURL=count_up_controller.js.map