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/microtask_coalescer.ts","../../src/controllers/step_indicator_controller.ts"],"names":[],"mappings":"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","file":"step_indicator_controller.js","sourcesContent":["/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/**\n * Headless, accessible **read-only** step-progress indicator.\n *\n * Markup contract (identifier: `stimeo--step-indicator`):\n * <ol data-controller=\"stimeo--step-indicator\" aria-label=\"Checkout progress\"\n * data-stimeo--step-indicator-index-value=\"1\"\n * data-action=\"stimeo--stepper:change->stimeo--step-indicator#setIndex\">\n * <li data-stimeo--step-indicator-target=\"step\">Cart</li>\n * <li data-stimeo--step-indicator-target=\"step\">Shipping</li>\n * <li data-stimeo--step-indicator-target=\"step\">Payment</li>\n * </ol>\n *\n * There is no dedicated APG widget; the current position is expressed with\n * `aria-current=\"step\"`. This indicator is **read only** — it never moves focus\n * and the steps are not operable. For an interactive wizard whose steps are\n * `<button>`s, use {@link StepperController | Stepper}.\n *\n * `change` dispatches `{ index: number, previous: number, total: number }`.\n *\n * @remarks\n * Behavior only. Each step `<li>` gets a `data-state` (`complete`/`current`/\n * `upcoming`) derived from the current index; the consumer draws the circles,\n * lines, and numbers from those hooks. A `--stimeo--step-indicator-ratio`\n * (0–1) custom property on the root expresses overall progress for CSS.\n *\n * Behavior provided:\n * - Reflects `index` onto each step's `data-state` and `aria-current`.\n * - Re-derives every step when the step set changes at runtime.\n * - `setIndex` (bound to an event whose `detail.index` is the 0-based position)\n * moves the indicator and dispatches `stimeo--step-indicator:change`. A\n * {@link StepperController | Stepper} dispatches exactly that shape, so the two\n * compose with one `data-action` and no glue.\n */\nexport class StepIndicatorController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n };\n static actions = [\"setIndex\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n\n /**\n * Collapses a batch of step callbacks — and a morph that swaps `index` with\n * them — into one repaint. Replacing a list of N steps delivers N callbacks, and\n * each one would otherwise rewrite every step's state.\n */\n readonly #repaint = new MicrotaskCoalescer(() => this.#render());\n\n /** Renders the initial state from the `index` value. */\n override connect(): void {\n this.#repaint.activate();\n this.#render();\n }\n\n /** Closes the window in which a queued repaint may still run. */\n override disconnect(): void {\n this.#repaint.cancel();\n }\n\n /** Syncs a step appended or replaced at runtime (the consumer owns the list). */\n stepTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives the remaining steps when one is removed at runtime. */\n stepTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `index` at runtime. */\n indexValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /**\n * Moves the indicator from an external event (`detail.index`, 0-based) and\n * dispatches `change`. Out-of-range positions are clamped to the step set,\n * and both sides of the no-op test are clamped, so moving onto the step an\n * out-of-range `index` already renders is not reported as a change.\n */\n setIndex(event: CustomEvent<{ index?: number }>): void {\n const next = event.detail?.index;\n if (typeof next !== \"number\" || !Number.isFinite(next)) return;\n const clamped = this.#clamp(next);\n const previous = this.#clamp(this.indexValue);\n const moved = clamped !== previous;\n // Normalise even when the display does not move: an out-of-range `index` left\n // in the markup would otherwise be re-clamped against a later step set and land\n // somewhere the consumer never asked for. Writing it here is the consumer-driven\n // path, the only one that owns the Value.\n this.indexValue = clamped;\n if (!moved) return;\n this.#render();\n this.dispatch(\"change\", {\n detail: { index: clamped, previous, total: this.stepTargets.length },\n });\n }\n\n /**\n * Applies `data-state`, `aria-current`, and the progress ratio custom property.\n *\n * A pure function of the step set and `index`, so running it again writes the\n * same values — which is what lets the action path paint synchronously (the event\n * goes out after the DOM is updated) while a coalesced pass may still follow.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const total = this.stepTargets.length;\n const current = this.#clamp(this.indexValue);\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n if (index === current) {\n step.setAttribute(\"aria-current\", \"step\");\n } else {\n step.removeAttribute(\"aria-current\");\n }\n });\n const ratio = total > 1 ? current / (total - 1) : 0;\n this.element.style.setProperty(\"--stimeo--step-indicator-ratio\", String(ratio));\n }\n\n /**\n * Constrains an index to `[0, total-1]` (or `0` when there are no steps). A\n * non-finite index falls back to the first step: `index` is read from markup,\n * so an unparsable attribute arrives as `NaN` and would otherwise propagate\n * into every state hook.\n */\n #clamp(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0 || !Number.isFinite(index)) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/utils/number_bounds.ts","../../src/utils/coerce.ts","../../src/utils/number_value.ts","../../src/controllers/step_indicator_controller.ts"],"names":[],"mappings":"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","file":"step_indicator_controller.js","sourcesContent":["/** 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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Whether a requested pass has yet to run. */\n get pending(): boolean {\n return this.#queued;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Whether a pass requested by a morph or {@link schedule} has yet to run. */\n get pending(): boolean {\n return this.#pass.pending;\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/** Semantic constraints on an already decoded number, independent of the DOM. */\nexport interface NumberBounds {\n /** Finite numbers are required, including when this field is omitted. */\n readonly finite?: true;\n /** Inclusive lower endpoint. */\n readonly min?: number;\n /** Inclusive upper endpoint. */\n readonly max?: number;\n /** Exclusive lower endpoint. */\n readonly exclusiveMin?: number;\n /** Require a number without a fractional part. */\n readonly integer?: boolean;\n /** Exact decoded numbers accepted by a discrete contract. */\n readonly allowedValues?: readonly number[];\n /** JSON-safe exceptions to finite-number checking for unbounded endpoints. */\n readonly allowInfinity?: \"negative\" | \"positive\" | \"both\";\n}\n\n/** One constraint for each Number Value, including shorthand declarations. */\nexport type NumberValueConstraints<Values> = {\n readonly [Key in keyof Values as Values[Key] extends\n | NumberConstructor\n | { type: NumberConstructor }\n ? Key\n : never]: NumberBounds;\n};\n\n/** Reusable semantic domains; fractional inputs remain fractional unless excluded. */\nexport const NUMBER_BOUNDS = {\n finite: { finite: true },\n nonNegative: { finite: true, min: 0 },\n positive: { finite: true, exclusiveMin: 0 },\n nonNegativeInteger: { finite: true, min: 0, integer: true },\n positiveInteger: { finite: true, exclusiveMin: 0, integer: true },\n lowerBound: { finite: true, allowInfinity: \"negative\" },\n upperBound: { finite: true, allowInfinity: \"positive\" },\n timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },\n positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS },\n} as const satisfies Record<string, NumberBounds>;\n\n/**\n * Checks all declared bounds without rounding, clamping, or coercing the input.\n * Infinity exceptions relax only finite checking; every other bound still applies.\n */\nexport function matchesNumberBounds(value: number, bounds: NumberBounds): boolean {\n if (!Number.isFinite(value)) {\n const direction = value === Infinity ? \"positive\" : value === -Infinity ? \"negative\" : null;\n if (direction === null) return false;\n if (bounds.allowInfinity !== \"both\" && bounds.allowInfinity !== direction) return false;\n }\n if (bounds.min !== undefined && value < bounds.min) return false;\n if (bounds.max !== undefined && value > bounds.max) return false;\n if (bounds.exclusiveMin !== undefined && value <= bounds.exclusiveMin) return false;\n if (bounds.integer && !Number.isInteger(value)) return false;\n if (bounds.allowedValues !== undefined && !bounds.allowedValues.includes(value)) return false;\n return true;\n}\n\n/** Decodes a Number Value literal; action params use their own JSON decoder. */\nexport function decodeNumberValue(raw: string): number {\n return Number(raw.replace(/_/g, \"\"));\n}\n","/**\n * Numeric coercion for controllers that accept a number through a Value, an action\n * param, or an event detail.\n *\n * Stimulus decodes action params as JSON when possible and leaves other text\n * untouched. CustomEvent detail can also supply a number or numeric string.\n * The reader accepts those two types while rejecting blank and non-finite input.\n */\n\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** Numeric update input, accepting decoded action params before CustomEvent detail. */\nexport type NumericValueEvent = Event & {\n params?: { amount?: unknown };\n detail?: { value?: unknown };\n};\n\n/** Reads the nullish action-param/detail precedence without falling back from invalid input. */\nexport function eventNumber(event: NumericValueEvent): number | null {\n return toFiniteNumber(event.params?.amount ?? event.detail?.value);\n}\n\n/**\n * Reads a finite number or nonblank numeric string; other input returns `null`.\n * Blank strings cannot silently reset a value to zero.\n */\nexport function toFiniteNumber(raw: unknown): number | null {\n if (typeof raw !== \"number\" && typeof raw !== \"string\") return null;\n if (typeof raw === \"string\" && raw.trim().length === 0) return null;\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Returns an accepted decoded Number Value or the caller's fallback unchanged.\n * The caller owns the fallback contract; this reader does not parse or write a Value.\n */\nexport function readNumber(raw: number, fallback: number, bounds: NumberBounds): number {\n return matchesNumberBounds(raw, bounds) ? raw : fallback;\n}\n","import { readNumber } from \"./coerce\";\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** The instance identity carried by a Number Value read. */\nexport interface NumberValueOwner {\n readonly identifier: string;\n readonly element: Element;\n}\n\n/** Reads current declarations at the point of use without rewriting their attributes. */\nexport class NumberValueReader {\n /** The last rejected literal per Value; no history grows for an unchanged instance. */\n readonly #lastRejected = new Map<string, string>();\n\n /** Resolves the declared number using its own fallback and shared class contract. */\n read(\n owner: NumberValueOwner,\n name: string,\n raw: number,\n fallback: number,\n bounds: NumberBounds,\n ): number {\n const resolved = readNumber(raw, fallback, bounds);\n if (matchesNumberBounds(raw, bounds)) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;\n const literal = owner.element.getAttribute(attribute);\n if (literal === null) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n if (this.#lastRejected.get(name) !== literal) {\n this.#lastRejected.set(name, literal);\n console.warn(\n `Stimeo UI: \"${owner.identifier}\" has an invalid number Value \"${name}\" declaration ${JSON.stringify(literal)}; using ${fallback}.`,\n );\n }\n return resolved;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\nimport { NUMBER_BOUNDS, type NumberValueConstraints } from \"../utils/number_bounds\";\nimport { NumberValueReader } from \"../utils/number_value\";\n\n/**\n * Headless, accessible **read-only** step-progress indicator.\n *\n * Markup contract (identifier: `stimeo--step-indicator`):\n * <ol data-controller=\"stimeo--step-indicator\" aria-label=\"Checkout progress\"\n * data-stimeo--step-indicator-index-value=\"1\"\n * data-action=\"stimeo--stepper:change->stimeo--step-indicator#setIndex\">\n * <li data-stimeo--step-indicator-target=\"step\">Cart</li>\n * <li data-stimeo--step-indicator-target=\"step\">Shipping</li>\n * <li data-stimeo--step-indicator-target=\"step\">Payment</li>\n * </ol>\n *\n * There is no dedicated APG widget; the current position is expressed with\n * `aria-current=\"step\"`. This indicator is **read only** — it never moves focus\n * and the steps are not operable. For an interactive wizard whose steps are\n * `<button>`s, use `stimeo--stepper`.\n *\n * `change` dispatches `{ index: number, previous: number, total: number }`.\n *\n * @remarks\n * Behavior only. Each step `<li>` gets a `data-state` (`complete`/`current`/\n * `upcoming`) derived from the current index; the consumer draws the circles,\n * lines, and numbers from those hooks. A `--stimeo--step-indicator-ratio`\n * (0–1) custom property on the root expresses overall progress for CSS.\n *\n * Behavior provided:\n * - Reflects `index` onto each step's `data-state` and `aria-current`.\n * - Re-derives every step when the step set changes at runtime.\n * - `setIndex` (bound to an event whose `detail.index` is the 0-based position)\n * moves the indicator and dispatches `stimeo--step-indicator:change`.\n * `stimeo--stepper:change` carries the 0-based position as `detail.index`, so\n * the two compose with one `data-action` and no glue.\n */\nexport class StepIndicatorController extends Controller<HTMLElement> {\n /** Numeric read boundaries share one reader for this controller instance. */\n readonly #numbers = new NumberValueReader();\n\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n };\n\n static valueConstraints = {\n index: NUMBER_BOUNDS.finite,\n } satisfies NumberValueConstraints<typeof StepIndicatorController.values>;\n static actions = [\"setIndex\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n\n /**\n * Collapses a batch of step callbacks — and a morph that swaps `index` with\n * them — into one repaint. Replacing a list of N steps delivers N callbacks, and\n * each one would otherwise rewrite every step's state.\n */\n readonly #repaint = new MorphRenderWatcher(() => this.#render());\n\n /** Renders the initial state from the `index` value. */\n override connect(): void {\n this.#repaint.observe(this.element);\n this.#render();\n }\n\n /** Closes the window in which a queued repaint may still run. */\n override disconnect(): void {\n this.#repaint.disconnect();\n }\n\n /** Syncs a step appended or replaced at runtime (the consumer owns the list). */\n stepTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives the remaining steps when one is removed at runtime. */\n stepTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `index` at runtime. */\n indexValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /**\n * Moves the indicator from an external event (`detail.index`, 0-based) and\n * dispatches `change`. Out-of-range positions are clamped to the step set,\n * and both sides of the no-op test are clamped, so moving onto the step an\n * out-of-range `index` already renders is not reported as a change.\n */\n setIndex(event: CustomEvent<{ index?: number }>): void {\n const next = event.detail?.index;\n if (typeof next !== \"number\" || !Number.isFinite(next)) return;\n const clamped = this.#clamp(next);\n const previous = this.#clamp(this.#safeIndex);\n const moved = clamped !== previous;\n // Normalise even when the display does not move: an out-of-range `index` left\n // in the markup would otherwise be re-clamped against a later step set and land\n // somewhere the consumer never asked for. Writing it here is the consumer-driven\n // path, the only one that owns the Value.\n this.indexValue = clamped;\n if (!moved) return;\n this.#render();\n this.dispatch(\"change\", {\n detail: { index: clamped, previous, total: this.stepTargets.length },\n });\n }\n\n /**\n * Applies `data-state`, `aria-current`, and the progress ratio custom property.\n *\n * A pure function of the step set and `index`, so running it again writes the\n * same values — which is what lets the action path paint synchronously (the event\n * goes out after the DOM is updated) while a coalesced pass may still follow.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const total = this.stepTargets.length;\n const current = this.#clamp(this.#safeIndex);\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n if (index === current) {\n step.setAttribute(\"aria-current\", \"step\");\n } else {\n step.removeAttribute(\"aria-current\");\n }\n });\n const ratio = total > 1 ? current / (total - 1) : 0;\n this.element.style.setProperty(\"--stimeo--step-indicator-ratio\", String(ratio));\n }\n\n /** Constrains a finite index to the live step range, or zero when there are no steps. */\n #clamp(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n /** Current `index` declaration resolved against its numeric contract. */\n get #safeIndex(): number {\n return this.#numbers.read(\n this,\n \"index\",\n this.indexValue,\n StepIndicatorController.values.index.default,\n StepIndicatorController.valueConstraints.index,\n );\n }\n}\n"]}
@@ -15,9 +15,8 @@ import { Controller } from '@hotwired/stimulus';
15
15
  * There is no dedicated APG widget; the current step is expressed with
16
16
  * `aria-current="step"` on the operable `<button>`. Each step `<li>` also gets a
17
17
  * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.
18
- * For a read-only progress display use
19
- * {@link StepIndicatorController | Step Indicator}; for panel switching use
20
- * {@link TabsController | Tabs}.
18
+ * For a read-only progress display use `stimeo--step-indicator`; for panel
19
+ * switching use `stimeo--tabs`.
21
20
  *
22
21
  * `change` dispatches `{ index: number, previous: number, total: number, step: HTMLElement }`,
23
22
  * and `reconcile` dispatches the same
@@ -25,6 +24,11 @@ import { Controller } from '@hotwired/stimulus';
25
24
  * is the position shown before — when a change the page made moves the position of
26
25
  * the current step (the clamped `index`).
27
26
  *
27
+ * User `change` reports compare the resulting state with the last published
28
+ * state. A pending page write handled in the same script joins that confirmation;
29
+ * a browser-delivered listener may settle it first as `reconcile`. Confirming
30
+ * the last published state reports nothing.
31
+ *
28
32
  * @remarks
29
33
  * Behavior only. The controller never traps or restores focus — each step button
30
34
  * is in the natural Tab order. `data-state` is purely derived from `index` and the
@@ -63,6 +67,11 @@ declare class StepperController extends Controller<HTMLElement> {
63
67
  default: boolean;
64
68
  };
65
69
  };
70
+ static valueConstraints: {
71
+ index: {
72
+ readonly finite: true;
73
+ };
74
+ };
66
75
  static actions: readonly ["goto", "next", "prev"];
67
76
  static events: readonly ["change", "reconcile"];
68
77
  readonly stepTargets: HTMLElement[];
@@ -83,9 +92,8 @@ declare class StepperController extends Controller<HTMLElement> {
83
92
  /** Returns to the previous step (ignored at the first step). */
84
93
  prev(): void;
85
94
  /**
86
- * Jumps to the step carried in the action's `index` param. A param that is empty,
87
- * or that Stimulus parses into anything but a number or a string, is no index,
88
- * so it jumps nowhere.
95
+ * Jumps to the finite integer carried in the action's `index` param.
96
+ * Blank strings and nonnumeric JSON values do not select a step.
89
97
  */
90
98
  goto(event: {
91
99
  params: {
@@ -1,28 +1,61 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/stepper_controller.ts
3
+ var NUMBER_BOUNDS = {
4
+ finite: { finite: true }};
5
+ function matchesNumberBounds(value, bounds) {
6
+ if (!Number.isFinite(value)) {
7
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
8
+ if (direction === null) return false;
9
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
10
+ }
11
+ if (bounds.min !== void 0 && value < bounds.min) return false;
12
+ if (bounds.max !== void 0 && value > bounds.max) return false;
13
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
14
+ if (bounds.integer && !Number.isInteger(value)) return false;
15
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
16
+ return true;
17
+ }
18
+
19
+ function toFiniteNumber(raw) {
20
+ if (typeof raw !== "number" && typeof raw !== "string") return null;
21
+ if (typeof raw === "string" && raw.trim().length === 0) return null;
22
+ const value = Number(raw);
23
+ return Number.isFinite(value) ? value : null;
24
+ }
25
+ function readNumber(raw, fallback, bounds) {
26
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
27
+ }
28
+
29
+ var ListenerSet = class {
30
+ #abort = new AbortController();
31
+ add(target, type, handler, options) {
32
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
33
+ }
34
+ dispose() {
35
+ this.#abort.abort();
36
+ this.#abort = new AbortController();
37
+ }
38
+ };
4
39
 
5
- // src/utils/microtask_coalescer.ts
6
40
  var MicrotaskCoalescer = class {
7
41
  #run;
8
42
  #queued = false;
9
43
  #active = false;
10
44
  #generation = 0;
11
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
12
45
  constructor(run) {
13
46
  this.#run = run;
14
47
  }
15
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
16
48
  activate() {
17
49
  this.#active = true;
18
50
  }
19
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
20
51
  cancel() {
21
52
  this.#active = false;
22
53
  this.#queued = false;
23
54
  this.#generation += 1;
24
55
  }
25
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
56
+ get pending() {
57
+ return this.#queued;
58
+ }
26
59
  schedule() {
27
60
  if (!this.#active || this.#queued) return;
28
61
  this.#queued = true;
@@ -35,95 +68,116 @@ var MicrotaskCoalescer = class {
35
68
  }
36
69
  };
37
70
 
38
- // src/controllers/stepper_controller.ts
39
- var StepperController = class extends Controller {
71
+ var MorphRenderWatcher = class {
72
+ #listeners = new ListenerSet();
73
+ #pass;
74
+ #element = null;
75
+ #onMorph = () => this.#pass.schedule();
76
+ constructor(run) {
77
+ this.#pass = new MicrotaskCoalescer(run);
78
+ }
79
+ observe(element) {
80
+ if (this.#element === element) return;
81
+ this.disconnect();
82
+ this.#element = element;
83
+ this.#pass.activate();
84
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
85
+ }
86
+ schedule() {
87
+ this.#pass.schedule();
88
+ }
89
+ get pending() {
90
+ return this.#pass.pending;
91
+ }
92
+ disconnect() {
93
+ this.#listeners.dispose();
94
+ this.#pass.cancel();
95
+ this.#element = null;
96
+ }
97
+ };
98
+
99
+ var NumberValueReader = class {
100
+ #lastRejected = /* @__PURE__ */ new Map();
101
+ read(owner, name, raw, fallback, bounds) {
102
+ const resolved = readNumber(raw, fallback, bounds);
103
+ if (matchesNumberBounds(raw, bounds)) {
104
+ this.#lastRejected.delete(name);
105
+ return resolved;
106
+ }
107
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
108
+ const literal = owner.element.getAttribute(attribute);
109
+ if (literal === null) {
110
+ this.#lastRejected.delete(name);
111
+ return resolved;
112
+ }
113
+ if (this.#lastRejected.get(name) !== literal) {
114
+ this.#lastRejected.set(name, literal);
115
+ console.warn(
116
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
117
+ );
118
+ }
119
+ return resolved;
120
+ }
121
+ };
122
+
123
+ var StepperController = class _StepperController extends Controller {
124
+ #numbers = new NumberValueReader();
40
125
  static targets = ["step"];
41
126
  static values = {
42
127
  index: { type: Number, default: 0 },
43
128
  linear: { type: Boolean, default: false }
44
129
  };
130
+ static valueConstraints = {
131
+ index: NUMBER_BOUNDS.finite
132
+ };
45
133
  static actions = ["goto", "next", "prev"];
46
134
  static events = ["change", "reconcile"];
47
- /**
48
- * Collapses the Value and target callbacks of one mutation into one pass, and
49
- * refuses the ones Stimulus delivers before `connect()`, which renders itself.
50
- */
51
- #repaint = new MicrotaskCoalescer(() => this.#reconcileStep());
52
- /**
53
- * The position shown current last, which the next move is measured from, or
54
- * `null` before any step has been shown. While no step is present it keeps the
55
- * last one.
56
- */
135
+ #repaint = new MorphRenderWatcher(() => this.#reconcileStep());
57
136
  #shown = null;
58
- /** Renders the initial state from `index`, clamped into the step range. */
59
137
  connect() {
60
- this.#repaint.activate();
138
+ this.#repaint.observe(this.element);
61
139
  this.#shown = this.#currentStep;
62
140
  this.#render();
63
141
  }
64
- /** Drops a pending pass, so nothing renders or reports for a stepper that left. */
65
142
  disconnect() {
66
- this.#repaint.cancel();
143
+ this.#repaint.disconnect();
67
144
  }
68
- /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
69
145
  indexValueChanged() {
70
146
  this.#repaint.schedule();
71
147
  }
72
- /** Re-derives every step's state for a step added at runtime. */
73
148
  stepTargetConnected() {
74
149
  this.#repaint.schedule();
75
150
  }
76
- /** Re-derives every step's state for a step removed at runtime. */
77
151
  stepTargetDisconnected() {
78
152
  this.#repaint.schedule();
79
153
  }
80
- /** Advances to the next step (ignored at the last step). */
81
154
  next() {
82
- this.#moveTo(this.#clampIndex(this.indexValue) + 1);
155
+ this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);
83
156
  }
84
- /** Returns to the previous step (ignored at the first step). */
85
157
  prev() {
86
- this.#moveTo(this.#clampIndex(this.indexValue) - 1);
158
+ this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);
87
159
  }
88
- /**
89
- * Jumps to the step carried in the action's `index` param. A param that is empty,
90
- * or that Stimulus parses into anything but a number or a string, is no index,
91
- * so it jumps nowhere.
92
- */
93
160
  goto(event) {
94
161
  const { index } = event.params;
95
- if (index === "") return;
96
- if (typeof index !== "number" && typeof index !== "string") return;
97
- const target = Number(index);
98
- if (!Number.isFinite(target) || !Number.isInteger(target)) return;
162
+ const target = toFiniteNumber(index);
163
+ if (target === null || !Number.isInteger(target)) return;
99
164
  this.#moveTo(target);
100
165
  }
101
- /**
102
- * Moves the current step to `target` when allowed: in range, not a no-op, and
103
- * — under `linear` — not skipping more than one step ahead. Re-renders and
104
- * dispatches `change`.
105
- */
106
166
  #moveTo(target) {
107
167
  const total = this.stepTargets.length;
108
- if (!Number.isFinite(target) || !Number.isInteger(target)) return;
109
168
  if (target < 0 || target >= total) return;
110
- const current = this.#clampIndex(this.indexValue);
111
- if (target === current) return;
169
+ const current = this.#clampIndex(this.#safeIndex);
170
+ if (target === current && target === this.#shown) return;
112
171
  if (this.linearValue && target > current + 1) return;
113
- const previous = current;
172
+ const previous = this.#shown ?? current;
114
173
  this.indexValue = target;
115
174
  this.#shown = target;
116
175
  this.#render();
176
+ if (target === previous) return;
117
177
  this.dispatch("change", {
118
178
  detail: { index: target, previous, total, step: this.stepTargets[target] }
119
179
  });
120
180
  }
121
- /**
122
- * Re-derives one settled batch of Value and target changes, and reports a
123
- * current position that moved from the one shown before as `reconcile`. With no
124
- * step present there is nothing to report, and the position shown last stays the
125
- * one the next move is measured from.
126
- */
127
181
  #reconcileStep() {
128
182
  const previous = this.#shown;
129
183
  const index = this.#currentStep;
@@ -135,25 +189,11 @@ var StepperController = class extends Controller {
135
189
  detail: { index, previous, total: steps.length, step: steps[index] }
136
190
  });
137
191
  }
138
- /** The current position, clamped into the step range, or `null` when there is no step. */
139
192
  get #currentStep() {
140
- return this.stepTargets.length > 0 ? this.#clampIndex(this.indexValue) : null;
141
- }
142
- /**
143
- * Derives each step's `data-state` and the current button's `aria-current` from
144
- * `index`, clamped into the step range, without dispatching an action event.
145
- *
146
- * The clamp decides only what is shown. This body never writes `index`, so an
147
- * out-of-range declaration stays in the attribute as the page wrote it.
148
- *
149
- * `aria-current="step"` is placed on the step's **first** `<button>`; the markup
150
- * contract assumes one operable button per step. If a step needs multiple
151
- * buttons, mark the navigational one first (or this would target the wrong one).
152
- *
153
- * @stimeoRenderRoot
154
- */
193
+ return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;
194
+ }
155
195
  #render() {
156
- const current = this.#clampIndex(this.indexValue);
196
+ const current = this.#clampIndex(this.#safeIndex);
157
197
  this.stepTargets.forEach((step, index) => {
158
198
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
159
199
  const button = step.querySelector("button");
@@ -165,14 +205,21 @@ var StepperController = class extends Controller {
165
205
  }
166
206
  });
167
207
  }
168
- /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
169
208
  #clampIndex(index) {
170
209
  const last = this.stepTargets.length - 1;
171
- if (last < 0 || !Number.isFinite(index)) return 0;
210
+ if (last < 0) return 0;
172
211
  return Math.min(last, Math.max(0, Math.trunc(index)));
173
212
  }
213
+ get #safeIndex() {
214
+ return this.#numbers.read(
215
+ this,
216
+ "index",
217
+ this.indexValue,
218
+ _StepperController.values.index.default,
219
+ _StepperController.valueConstraints.index
220
+ );
221
+ }
174
222
  };
175
223
 
176
224
  export { StepperController };
177
- //# sourceMappingURL=stepper_controller.js.map
178
225
  //# sourceMappingURL=stepper_controller.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/microtask_coalescer.ts","../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use\n * {@link TabsController | Tabs}.\n *\n * `change` dispatches `{ index: number, previous: number, total: number, step: HTMLElement }`,\n * and `reconcile` dispatches the same\n * `{ index: number, previous: number, total: number, step: HTMLElement }` — `previous`\n * is the position shown before — when a change the page made moves the position of\n * the current step (the clamped `index`).\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index` and the\n * steps present (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Runtime changes to the `index` Value, and steps added or removed at runtime,\n * re-derive every step's state once per batch. Once connected, a batch that\n * moves the current position (the clamped `index`) away from the one shown\n * reports `stimeo--stepper:reconcile` once. A batch that leaves the position\n * where it was reports nothing, even when another step now stands there — a\n * step added before the current one, or the current step removed while\n * another follows it — and only redraws `data-state`/`aria-current`;\n * connecting reports nothing.\n * - An `index` outside the step range, fractional, or not a number at all is\n * clamped for display only. The Value keeps what the page declared, so the\n * declared step comes back once enough steps are present; a move the user makes\n * is the one path that writes it.\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n /**\n * Collapses the Value and target callbacks of one mutation into one pass, and\n * refuses the ones Stimulus delivers before `connect()`, which renders itself.\n */\n readonly #repaint = new MicrotaskCoalescer(() => this.#reconcileStep());\n\n /**\n * The position shown current last, which the next move is measured from, or\n * `null` before any step has been shown. While no step is present it keeps the\n * last one.\n */\n #shown: number | null = null;\n\n /** Renders the initial state from `index`, clamped into the step range. */\n override connect(): void {\n this.#repaint.activate();\n this.#shown = this.#currentStep;\n this.#render();\n }\n\n /** Drops a pending pass, so nothing renders or reports for a stepper that left. */\n override disconnect(): void {\n this.#repaint.cancel();\n }\n\n /** Re-renders when Turbo Morph or application code changes `index` at runtime. */\n indexValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step added at runtime. */\n stepTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step removed at runtime. */\n stepTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) - 1);\n }\n\n /**\n * Jumps to the step carried in the action's `index` param. A param that is empty,\n * or that Stimulus parses into anything but a number or a string, is no index,\n * so it jumps nowhere.\n */\n goto(event: { params: { index?: unknown } }): void {\n const { index } = event.params;\n if (index === \"\") return;\n if (typeof index !== \"number\" && typeof index !== \"string\") return;\n const target = Number(index);\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n if (target < 0 || target >= total) return;\n const current = this.#clampIndex(this.indexValue);\n if (target === current) return;\n if (this.linearValue && target > current + 1) return;\n\n const previous = current;\n this.indexValue = target;\n // Settled before the report, so the pass the Value write starts finds the step\n // already shown, and a listener that moves on is measured from this step.\n this.#shown = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { index: target, previous, total, step: this.stepTargets[target] },\n });\n }\n\n /**\n * Re-derives one settled batch of Value and target changes, and reports a\n * current position that moved from the one shown before as `reconcile`. With no\n * step present there is nothing to report, and the position shown last stays the\n * one the next move is measured from.\n */\n #reconcileStep(): void {\n const previous = this.#shown;\n const index = this.#currentStep;\n if (index !== null) this.#shown = index;\n this.#render();\n if (index === null || previous === null || index === previous) return;\n const steps = this.stepTargets;\n this.dispatch(\"reconcile\", {\n detail: { index, previous, total: steps.length, step: steps[index] },\n });\n }\n\n /** The current position, clamped into the step range, or `null` when there is no step. */\n get #currentStep(): number | null {\n return this.stepTargets.length > 0 ? this.#clampIndex(this.indexValue) : null;\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current` from\n * `index`, clamped into the step range, without dispatching an action event.\n *\n * The clamp decides only what is shown. This body never writes `index`, so an\n * out-of-range declaration stays in the attribute as the page wrote it.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const current = this.#clampIndex(this.indexValue);\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0 || !Number.isFinite(index)) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/number_bounds.ts","../../src/utils/coerce.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/utils/number_value.ts","../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["import { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\n\n/** Semantic constraints on an already decoded number, independent of the DOM. */\nexport interface NumberBounds {\n /** Finite numbers are required, including when this field is omitted. */\n readonly finite?: true;\n /** Inclusive lower endpoint. */\n readonly min?: number;\n /** Inclusive upper endpoint. */\n readonly max?: number;\n /** Exclusive lower endpoint. */\n readonly exclusiveMin?: number;\n /** Require a number without a fractional part. */\n readonly integer?: boolean;\n /** Exact decoded numbers accepted by a discrete contract. */\n readonly allowedValues?: readonly number[];\n /** JSON-safe exceptions to finite-number checking for unbounded endpoints. */\n readonly allowInfinity?: \"negative\" | \"positive\" | \"both\";\n}\n\n/** One constraint for each Number Value, including shorthand declarations. */\nexport type NumberValueConstraints<Values> = {\n readonly [Key in keyof Values as Values[Key] extends\n | NumberConstructor\n | { type: NumberConstructor }\n ? Key\n : never]: NumberBounds;\n};\n\n/** Reusable semantic domains; fractional inputs remain fractional unless excluded. */\nexport const NUMBER_BOUNDS = {\n finite: { finite: true },\n nonNegative: { finite: true, min: 0 },\n positive: { finite: true, exclusiveMin: 0 },\n nonNegativeInteger: { finite: true, min: 0, integer: true },\n positiveInteger: { finite: true, exclusiveMin: 0, integer: true },\n lowerBound: { finite: true, allowInfinity: \"negative\" },\n upperBound: { finite: true, allowInfinity: \"positive\" },\n timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },\n positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS },\n} as const satisfies Record<string, NumberBounds>;\n\n/**\n * Checks all declared bounds without rounding, clamping, or coercing the input.\n * Infinity exceptions relax only finite checking; every other bound still applies.\n */\nexport function matchesNumberBounds(value: number, bounds: NumberBounds): boolean {\n if (!Number.isFinite(value)) {\n const direction = value === Infinity ? \"positive\" : value === -Infinity ? \"negative\" : null;\n if (direction === null) return false;\n if (bounds.allowInfinity !== \"both\" && bounds.allowInfinity !== direction) return false;\n }\n if (bounds.min !== undefined && value < bounds.min) return false;\n if (bounds.max !== undefined && value > bounds.max) return false;\n if (bounds.exclusiveMin !== undefined && value <= bounds.exclusiveMin) return false;\n if (bounds.integer && !Number.isInteger(value)) return false;\n if (bounds.allowedValues !== undefined && !bounds.allowedValues.includes(value)) return false;\n return true;\n}\n\n/** Decodes a Number Value literal; action params use their own JSON decoder. */\nexport function decodeNumberValue(raw: string): number {\n return Number(raw.replace(/_/g, \"\"));\n}\n","/**\n * Numeric coercion for controllers that accept a number through a Value, an action\n * param, or an event detail.\n *\n * Stimulus decodes action params as JSON when possible and leaves other text\n * untouched. CustomEvent detail can also supply a number or numeric string.\n * The reader accepts those two types while rejecting blank and non-finite input.\n */\n\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** Numeric update input, accepting decoded action params before CustomEvent detail. */\nexport type NumericValueEvent = Event & {\n params?: { amount?: unknown };\n detail?: { value?: unknown };\n};\n\n/** Reads the nullish action-param/detail precedence without falling back from invalid input. */\nexport function eventNumber(event: NumericValueEvent): number | null {\n return toFiniteNumber(event.params?.amount ?? event.detail?.value);\n}\n\n/**\n * Reads a finite number or nonblank numeric string; other input returns `null`.\n * Blank strings cannot silently reset a value to zero.\n */\nexport function toFiniteNumber(raw: unknown): number | null {\n if (typeof raw !== \"number\" && typeof raw !== \"string\") return null;\n if (typeof raw === \"string\" && raw.trim().length === 0) return null;\n const value = Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n\n/**\n * Returns an accepted decoded Number Value or the caller's fallback unchanged.\n * The caller owns the fallback contract; this reader does not parse or write a Value.\n */\nexport function readNumber(raw: number, fallback: number, bounds: NumberBounds): number {\n return matchesNumberBounds(raw, bounds) ? raw : fallback;\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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Whether a requested pass has yet to run. */\n get pending(): boolean {\n return this.#queued;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Whether a pass requested by a morph or {@link schedule} has yet to run. */\n get pending(): boolean {\n return this.#pass.pending;\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { readNumber } from \"./coerce\";\nimport { matchesNumberBounds, type NumberBounds } from \"./number_bounds\";\n\n/** The instance identity carried by a Number Value read. */\nexport interface NumberValueOwner {\n readonly identifier: string;\n readonly element: Element;\n}\n\n/** Reads current declarations at the point of use without rewriting their attributes. */\nexport class NumberValueReader {\n /** The last rejected literal per Value; no history grows for an unchanged instance. */\n readonly #lastRejected = new Map<string, string>();\n\n /** Resolves the declared number using its own fallback and shared class contract. */\n read(\n owner: NumberValueOwner,\n name: string,\n raw: number,\n fallback: number,\n bounds: NumberBounds,\n ): number {\n const resolved = readNumber(raw, fallback, bounds);\n if (matchesNumberBounds(raw, bounds)) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;\n const literal = owner.element.getAttribute(attribute);\n if (literal === null) {\n this.#lastRejected.delete(name);\n return resolved;\n }\n if (this.#lastRejected.get(name) !== literal) {\n this.#lastRejected.set(name, literal);\n console.warn(\n `Stimeo UI: \"${owner.identifier}\" has an invalid number Value \"${name}\" declaration ${JSON.stringify(literal)}; using ${fallback}.`,\n );\n }\n return resolved;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { toFiniteNumber } from \"../utils/coerce\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\nimport { NUMBER_BOUNDS, type NumberValueConstraints } from \"../utils/number_bounds\";\nimport { NumberValueReader } from \"../utils/number_value\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use `stimeo--step-indicator`; for panel\n * switching use `stimeo--tabs`.\n *\n * `change` dispatches `{ index: number, previous: number, total: number, step: HTMLElement }`,\n * and `reconcile` dispatches the same\n * `{ index: number, previous: number, total: number, step: HTMLElement }` — `previous`\n * is the position shown before — when a change the page made moves the position of\n * the current step (the clamped `index`).\n *\n * User `change` reports compare the resulting state with the last published\n * state. A pending page write handled in the same script joins that confirmation;\n * a browser-delivered listener may settle it first as `reconcile`. Confirming\n * the last published state reports nothing.\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index` and the\n * steps present (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Runtime changes to the `index` Value, and steps added or removed at runtime,\n * re-derive every step's state once per batch. Once connected, a batch that\n * moves the current position (the clamped `index`) away from the one shown\n * reports `stimeo--stepper:reconcile` once. A batch that leaves the position\n * where it was reports nothing, even when another step now stands there — a\n * step added before the current one, or the current step removed while\n * another follows it — and only redraws `data-state`/`aria-current`;\n * connecting reports nothing.\n * - An `index` outside the step range, fractional, or not a number at all is\n * clamped for display only. The Value keeps what the page declared, so the\n * declared step comes back once enough steps are present; a move the user makes\n * is the one path that writes it.\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n /** Numeric read boundaries share one reader for this controller instance. */\n readonly #numbers = new NumberValueReader();\n\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n\n static valueConstraints = {\n index: NUMBER_BOUNDS.finite,\n } satisfies NumberValueConstraints<typeof StepperController.values>;\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n /**\n * Collapses the Value and target callbacks of one mutation into one pass, and\n * refuses the ones Stimulus delivers before `connect()`, which renders itself.\n */\n readonly #repaint = new MorphRenderWatcher(() => this.#reconcileStep());\n\n /**\n * The position shown current last, which the next move is measured from, or\n * `null` before any step has been shown. While no step is present it keeps the\n * last one.\n */\n #shown: number | null = null;\n\n /** Renders the initial state from `index`, clamped into the step range. */\n override connect(): void {\n this.#repaint.observe(this.element);\n this.#shown = this.#currentStep;\n this.#render();\n }\n\n /** Drops a pending pass, so nothing renders or reports for a stepper that left. */\n override disconnect(): void {\n this.#repaint.disconnect();\n }\n\n /** Re-renders when Turbo Morph or application code changes `index` at runtime. */\n indexValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step added at runtime. */\n stepTargetConnected(): void {\n this.#repaint.schedule();\n }\n\n /** Re-derives every step's state for a step removed at runtime. */\n stepTargetDisconnected(): void {\n this.#repaint.schedule();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.#clampIndex(this.#safeIndex) + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.#clampIndex(this.#safeIndex) - 1);\n }\n\n /**\n * Jumps to the finite integer carried in the action's `index` param.\n * Blank strings and nonnumeric JSON values do not select a step.\n */\n goto(event: { params: { index?: unknown } }): void {\n const { index } = event.params;\n const target = toFiniteNumber(index);\n if (target === null || !Number.isInteger(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (target < 0 || target >= total) return;\n const current = this.#clampIndex(this.#safeIndex);\n if (target === current && target === this.#shown) return;\n if (this.linearValue && target > current + 1) return;\n\n const previous = this.#shown ?? current;\n this.indexValue = target;\n // Settled before the report, so the pass the Value write starts finds the step\n // already shown, and a listener that moves on is measured from this step.\n this.#shown = target;\n this.#render();\n if (target === previous) return;\n this.dispatch(\"change\", {\n detail: { index: target, previous, total, step: this.stepTargets[target] },\n });\n }\n\n /**\n * Re-derives one settled batch of Value and target changes, and reports a\n * current position that moved from the one shown before as `reconcile`. With no\n * step present there is nothing to report, and the position shown last stays the\n * one the next move is measured from.\n */\n #reconcileStep(): void {\n const previous = this.#shown;\n const index = this.#currentStep;\n if (index !== null) this.#shown = index;\n this.#render();\n if (index === null || previous === null || index === previous) return;\n const steps = this.stepTargets;\n this.dispatch(\"reconcile\", {\n detail: { index, previous, total: steps.length, step: steps[index] },\n });\n }\n\n /** The current position, clamped into the step range, or `null` when there is no step. */\n get #currentStep(): number | null {\n return this.stepTargets.length > 0 ? this.#clampIndex(this.#safeIndex) : null;\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current` from\n * `index`, clamped into the step range, without dispatching an action event.\n *\n * The clamp decides only what is shown. This body never writes `index`, so an\n * out-of-range declaration stays in the attribute as the page wrote it.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const current = this.#clampIndex(this.#safeIndex);\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n /** Current `index` declaration resolved against its numeric contract. */\n get #safeIndex(): number {\n return this.#numbers.read(\n this,\n \"index\",\n this.indexValue,\n StepperController.values.index.default,\n StepperController.valueConstraints.index,\n );\n }\n}\n"]}
@@ -76,6 +76,12 @@ declare class StickToBottomController extends Controller<HTMLElement> {
76
76
  default: boolean;
77
77
  };
78
78
  };
79
+ static valueConstraints: {
80
+ threshold: {
81
+ readonly finite: true;
82
+ readonly min: 0;
83
+ };
84
+ };
79
85
  static actions: readonly ["scrollToBottom"];
80
86
  static events: readonly ["pin", "new"];
81
87
  readonly contentTarget: HTMLElement;
@@ -114,7 +120,9 @@ declare class StickToBottomController extends Controller<HTMLElement> {
114
120
  * and a jump the engine cannot honor leaves the container unpinned, so the next append
115
121
  * flags it again instead of being swallowed by a pinned state that does not hold.
116
122
  *
117
- * Which of those happens is not this method's to decide — see {@link behaviorValue}.
123
+ * Whether the jump arrives at once or animates follows the `behavior` value: `"auto"`
124
+ * defers to the element's computed `scroll-behavior`, `"smooth"` animates, and reduced
125
+ * motion forces an instant jump.
118
126
  */
119
127
  scrollToBottom(): void;
120
128
  }