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
@@ -7,8 +7,8 @@ import { Controller, Application } from '@hotwired/stimulus';
7
7
  * `anchor`, flipping/shifting it away from viewport edges as the page scrolls or
8
8
  * resizes. It is the declarative surface of the opt-in {@link attachPositioning}
9
9
  * engine (`@floating-ui/dom`-based), exposing its `autoUpdate` behavior as a
10
- * controller. No dedicated APG pattern; it is the
11
- * placement primitive the popup patterns (Tooltip / Menu / Popover …) build on.
10
+ * controller. No dedicated APG pattern; it is a placement primitive a consumer pairs
11
+ * with a popup such as `stimeo--tooltip`, `stimeo--menu` or `stimeo--popover`.
12
12
  *
13
13
  * Markup contract (identifier: `stimeo--anchored`):
14
14
  * <div data-controller="stimeo--anchored"
@@ -18,9 +18,9 @@ import { Controller, Application } from '@hotwired/stimulus';
18
18
  * <div data-stimeo--anchored-target="floating" role="…">…</div>
19
19
  * </div>
20
20
  *
21
- * `active` drives tracking (start/stop) and fires on connect, mirroring Focus
22
- * Scope's `trapValueChanged`; set it `false` while the floating element is hidden
23
- * so no measurement runs. The other Values map to {@link PositioningOptions} and
21
+ * `active` drives tracking (start/stop) and fires on connect, mirroring
22
+ * `stimeo--focus`'s `trapValueChanged`; set it `false` while the floating element is
23
+ * hidden so no measurement runs. The other Values map to {@link PositioningOptions} and
24
24
  * re-apply live while tracking. A declaration the engine cannot use — a
25
25
  * `placement` outside its set, a non-finite `offset` or `padding` — falls back to
26
26
  * that Value's default, so a typo never reaches the coordinates or the hook. Only
@@ -36,8 +36,8 @@ import { Controller, Application } from '@hotwired/stimulus';
36
36
  *
37
37
  * @remarks
38
38
  * Behavior only. It does **not** open/close, manage focus, or render an overlay
39
- * (pair with Dialog / Popover and {@link "../controllers/focus_controller"}), and
40
- * it does **not** move DOM (pair with Portal). It lives in the opt-in
39
+ * (pair with `stimeo--dialog` / `stimeo--popover` and `stimeo--focus`), and it does
40
+ * **not** move DOM (pair with `stimeo--portal`). It lives in the opt-in
41
41
  * `stimeo-ui/positioning` subpath so the core `import "stimeo-ui"` stays
42
42
  * zero-dependency; only consumers who register it pull in `@floating-ui/dom`. The
43
43
  * `autoUpdate` cleanup is released on `disconnect()` (Turbo navigation included)
@@ -80,6 +80,14 @@ declare class AnchoredController extends Controller<HTMLElement> {
80
80
  default: boolean;
81
81
  };
82
82
  };
83
+ static valueConstraints: {
84
+ offset: {
85
+ readonly finite: true;
86
+ };
87
+ padding: {
88
+ readonly finite: true;
89
+ };
90
+ };
83
91
  static events: readonly ["position"];
84
92
  readonly anchorTarget: HTMLElement;
85
93
  readonly floatingTarget: HTMLElement;
@@ -1,7 +1,120 @@
1
1
  import { computePosition, autoUpdate, offset, flip, shift } from '@floating-ui/dom';
2
2
  import { Controller } from '@hotwired/stimulus';
3
3
 
4
- // src/positioning/index.ts
4
+ var ListenerSet = class {
5
+ #abort = new AbortController();
6
+ add(target, type, handler, options) {
7
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
8
+ }
9
+ dispose() {
10
+ this.#abort.abort();
11
+ this.#abort = new AbortController();
12
+ }
13
+ };
14
+
15
+ var MicrotaskCoalescer = class {
16
+ #run;
17
+ #queued = false;
18
+ #active = false;
19
+ #generation = 0;
20
+ constructor(run) {
21
+ this.#run = run;
22
+ }
23
+ activate() {
24
+ this.#active = true;
25
+ }
26
+ cancel() {
27
+ this.#active = false;
28
+ this.#queued = false;
29
+ this.#generation += 1;
30
+ }
31
+ get pending() {
32
+ return this.#queued;
33
+ }
34
+ schedule() {
35
+ if (!this.#active || this.#queued) return;
36
+ this.#queued = true;
37
+ const generation = this.#generation;
38
+ queueMicrotask(() => {
39
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
40
+ this.#queued = false;
41
+ this.#run();
42
+ });
43
+ }
44
+ };
45
+
46
+ var MorphRenderWatcher = class {
47
+ #listeners = new ListenerSet();
48
+ #pass;
49
+ #element = null;
50
+ #onMorph = () => this.#pass.schedule();
51
+ constructor(run) {
52
+ this.#pass = new MicrotaskCoalescer(run);
53
+ }
54
+ observe(element) {
55
+ if (this.#element === element) return;
56
+ this.disconnect();
57
+ this.#element = element;
58
+ this.#pass.activate();
59
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
60
+ }
61
+ schedule() {
62
+ this.#pass.schedule();
63
+ }
64
+ get pending() {
65
+ return this.#pass.pending;
66
+ }
67
+ disconnect() {
68
+ this.#listeners.dispose();
69
+ this.#pass.cancel();
70
+ this.#element = null;
71
+ }
72
+ };
73
+
74
+ var NUMBER_BOUNDS = {
75
+ finite: { finite: true }};
76
+ function matchesNumberBounds(value, bounds) {
77
+ if (!Number.isFinite(value)) {
78
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
79
+ if (direction === null) return false;
80
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
81
+ }
82
+ if (bounds.min !== void 0 && value < bounds.min) return false;
83
+ if (bounds.max !== void 0 && value > bounds.max) return false;
84
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
85
+ if (bounds.integer && !Number.isInteger(value)) return false;
86
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
87
+ return true;
88
+ }
89
+
90
+ function readNumber(raw, fallback, bounds) {
91
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
92
+ }
93
+
94
+ var NumberValueReader = class {
95
+ #lastRejected = /* @__PURE__ */ new Map();
96
+ read(owner, name, raw, fallback, bounds) {
97
+ const resolved = readNumber(raw, fallback, bounds);
98
+ if (matchesNumberBounds(raw, bounds)) {
99
+ this.#lastRejected.delete(name);
100
+ return resolved;
101
+ }
102
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
103
+ const literal = owner.element.getAttribute(attribute);
104
+ if (literal === null) {
105
+ this.#lastRejected.delete(name);
106
+ return resolved;
107
+ }
108
+ if (this.#lastRejected.get(name) !== literal) {
109
+ this.#lastRejected.set(name, literal);
110
+ console.warn(
111
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
112
+ );
113
+ }
114
+ return resolved;
115
+ }
116
+ };
117
+
5
118
  var PLACEMENTS = /* @__PURE__ */ new Set([
6
119
  "top",
7
120
  "top-start",
@@ -16,7 +129,8 @@ var PLACEMENTS = /* @__PURE__ */ new Set([
16
129
  "left-start",
17
130
  "left-end"
18
131
  ]);
19
- var AnchoredController = class extends Controller {
132
+ var AnchoredController = class _AnchoredController extends Controller {
133
+ #numbers = new NumberValueReader();
20
134
  static targets = ["anchor", "floating"];
21
135
  static values = {
22
136
  placement: { type: String, default: "bottom" },
@@ -27,32 +141,29 @@ var AnchoredController = class extends Controller {
27
141
  strategy: { type: String, default: "absolute" },
28
142
  active: { type: Boolean, default: true }
29
143
  };
144
+ static valueConstraints = {
145
+ offset: NUMBER_BOUNDS.finite,
146
+ padding: NUMBER_BOUNDS.finite
147
+ };
30
148
  static events = ["position"];
31
- /** `autoUpdate` cleanup while tracking; `null` when detached. */
32
149
  #stop = null;
33
- /** Identity of the live attach; `null` when detached — see {@link #onComputed}. */
34
150
  #attachId = null;
35
- /** True between connect and disconnect (Stimulus may fire value callbacks before connect). */
36
151
  #connected = false;
37
- /**
38
- * Serialized options as of the last reconcile, empty only before the first one —
39
- * a value no serialization produces, so it can never match a real key.
40
- */
152
+ #position = null;
153
+ #morphRender = new MorphRenderWatcher(() => this.#sync());
41
154
  #appliedKey = "";
42
- /** Elements the live observer holds, or `null` when detached — see {@link #sync}. */
43
155
  #appliedAnchor = null;
44
156
  #appliedFloating = null;
45
157
  connect() {
158
+ this.#morphRender.observe(this.element);
46
159
  this.#connected = true;
47
160
  this.#sync();
48
161
  }
49
162
  disconnect() {
163
+ this.#morphRender.disconnect();
50
164
  this.#connected = false;
51
165
  this.#sync();
52
166
  }
53
- // Every value change (active or an option) and every target arrival or
54
- // departure re-syncs. `#sync` is idempotent and order-independent, so no Value
55
- // has to be declared in a particular position.
56
167
  anchorTargetConnected() {
57
168
  this.#sync();
58
169
  }
@@ -86,41 +197,23 @@ var AnchoredController = class extends Controller {
86
197
  strategyValueChanged() {
87
198
  this.#sync();
88
199
  }
89
- /** Current Values mapped to the positioning engine's options. */
90
200
  get #options() {
91
201
  return {
92
- // Narrow every free-form Value to what the engine can use. A placement
93
- // outside the set would be published on the hook as a resolved side it is
94
- // not, and a non-finite distance poisons the coordinate it feeds until the
95
- // browser drops the whole declaration and leaves that axis unplaced.
96
202
  placement: PLACEMENTS.has(this.placementValue) ? this.placementValue : "bottom",
97
- offset: Number.isFinite(this.offsetValue) ? this.offsetValue : 0,
203
+ offset: this.#safeOffset,
98
204
  flip: this.flipValue,
99
205
  shift: this.shiftValue,
100
- padding: Number.isFinite(this.paddingValue) ? this.paddingValue : 0,
206
+ padding: this.#safePadding,
101
207
  strategy: this.strategyValue === "fixed" ? "fixed" : "absolute"
102
208
  };
103
209
  }
104
- /**
105
- * Reconciles the live observer with the desired state — track iff connected,
106
- * `active`, and both targets exist — re-attaching only when that state, the
107
- * options, or the elements actually changed. Stimulus fires the value-changed
108
- * callbacks on connect in declaration order and may run them before or after
109
- * `connect()`; keying on what is applied collapses that whole burst (in any
110
- * order) to a single attach, while an option or target change at runtime
111
- * re-attaches exactly once. So correctness never depends on `active` being
112
- * declared last. The elements are part of the key because the engine holds the
113
- * pair it was handed: a swap leaves the same options behind and would otherwise
114
- * keep measuring the node that just left the document.
115
- *
116
- * @stimeoRenderRoot
117
- */
118
210
  #sync() {
119
211
  const shouldTrack = this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;
120
212
  const anchor = shouldTrack ? this.anchorTarget : null;
121
213
  const floating = shouldTrack ? this.floatingTarget : null;
122
214
  const key = JSON.stringify(this.#options);
123
215
  if (key === this.#appliedKey && anchor === this.#appliedAnchor && floating === this.#appliedFloating) {
216
+ if (floating && this.#position) this.#reflectPosition(floating, this.#position);
124
217
  return;
125
218
  }
126
219
  this.#detach();
@@ -143,27 +236,42 @@ var AnchoredController = class extends Controller {
143
236
  this.#stop?.();
144
237
  this.#stop = null;
145
238
  this.#attachId = null;
239
+ this.#position = null;
146
240
  }
147
- /**
148
- * Reflects the resolved side onto the CSS hook and announces the placement.
149
- *
150
- * The pass carries the attach that started it and the element it positioned,
151
- * and lands only while that attach is still the live one: `autoUpdate`'s
152
- * cleanup stops further updates but cannot cancel one already computing, so a
153
- * pass superseded mid-flight would otherwise write and dispatch afterwards.
154
- * The element cannot stand in for that identity — an option change re-attaches
155
- * against the very same pair.
156
- */
157
241
  #onComputed(id, floating, result) {
158
242
  if (id !== this.#attachId) return;
159
- floating.setAttribute("data-anchored-placement", result.placement);
243
+ this.#position = result;
244
+ this.#reflectPosition(floating, result);
160
245
  this.dispatch("position", {
161
246
  detail: { placement: result.placement, x: result.x, y: result.y }
162
247
  });
163
248
  }
249
+ #reflectPosition(floating, result) {
250
+ floating.setAttribute("data-anchored-placement", result.placement);
251
+ floating.style.position = this.#options.strategy ?? "absolute";
252
+ floating.style.left = `${result.x}px`;
253
+ floating.style.top = `${result.y}px`;
254
+ }
255
+ get #safeOffset() {
256
+ return this.#numbers.read(
257
+ this,
258
+ "offset",
259
+ this.offsetValue,
260
+ _AnchoredController.values.offset.default,
261
+ _AnchoredController.valueConstraints.offset
262
+ );
263
+ }
264
+ get #safePadding() {
265
+ return this.#numbers.read(
266
+ this,
267
+ "padding",
268
+ this.paddingValue,
269
+ _AnchoredController.values.padding.default,
270
+ _AnchoredController.valueConstraints.padding
271
+ );
272
+ }
164
273
  };
165
274
 
166
- // src/positioning/index.ts
167
275
  function buildMiddleware(options) {
168
276
  const padding = options.padding ?? 0;
169
277
  const middleware = [];
@@ -202,5 +310,4 @@ function registerPositioning(application) {
202
310
  }
203
311
 
204
312
  export { AnchoredController, attachPositioning, position, positioningControllers, registerPositioning };
205
- //# sourceMappingURL=index.js.map
206
313
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/positioning/anchored_controller.ts","../../src/positioning/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\nimport { attachPositioning, type Placement, type PositioningOptions, type PositionResult } from \".\";\n\n/**\n * Every placement the engine accepts. A declaration outside the set has no\n * resolved side to publish, so it falls back to the Value's default rather than\n * reaching the state hook and the event detail as-is.\n */\nconst PLACEMENTS: ReadonlySet<string> = new Set([\n \"top\",\n \"top-start\",\n \"top-end\",\n \"right\",\n \"right-start\",\n \"right-end\",\n \"bottom\",\n \"bottom-start\",\n \"bottom-end\",\n \"left\",\n \"left-start\",\n \"left-end\",\n]);\n\n/**\n * Headless **anchored positioning**: keeps a `floating` element placed against an\n * `anchor`, flipping/shifting it away from viewport edges as the page scrolls or\n * resizes. It is the declarative surface of the opt-in {@link attachPositioning}\n * engine (`@floating-ui/dom`-based), exposing its `autoUpdate` behavior as a\n * controller. No dedicated APG pattern; it is the\n * placement primitive the popup patterns (Tooltip / Menu / Popover …) build on.\n *\n * Markup contract (identifier: `stimeo--anchored`):\n * <div data-controller=\"stimeo--anchored\"\n * data-stimeo--anchored-placement-value=\"bottom-start\"\n * data-stimeo--anchored-offset-value=\"8\">\n * <button data-stimeo--anchored-target=\"anchor\">Open</button>\n * <div data-stimeo--anchored-target=\"floating\" role=\"…\">…</div>\n * </div>\n *\n * `active` drives tracking (start/stop) and fires on connect, mirroring Focus\n * Scope's `trapValueChanged`; set it `false` while the floating element is hidden\n * so no measurement runs. The other Values map to {@link PositioningOptions} and\n * re-apply live while tracking. A declaration the engine cannot use — a\n * `placement` outside its set, a non-finite `offset` or `padding` — falls back to\n * that Value's default, so a typo never reaches the coordinates or the hook. Only\n * `position`/`left`/`top` inline styles are written — never decoration — and the\n * resolved (post-flip) side is mirrored onto `data-anchored-placement` on the\n * floating element for CSS hooks (e.g. an arrow).\n *\n * Tracking follows the targets themselves, not just the Values: the engine holds\n * the two elements it was handed, so a target added, removed or swapped at\n * runtime re-attaches against whatever is there now.\n *\n * `position` dispatches `{ placement, x, y }`.\n *\n * @remarks\n * Behavior only. It does **not** open/close, manage focus, or render an overlay\n * (pair with Dialog / Popover and {@link \"../controllers/focus_controller\"}), and\n * it does **not** move DOM (pair with Portal). It lives in the opt-in\n * `stimeo-ui/positioning` subpath so the core `import \"stimeo-ui\"` stays\n * zero-dependency; only consumers who register it pull in `@floating-ui/dom`. The\n * `autoUpdate` cleanup is released on `disconnect()` (Turbo navigation included)\n * so no observer outlives the element, and `#sync` reconciles to a single live\n * observer (keyed on the applied options and elements) so reconnects never stack\n * observers. That cleanup stops future updates but cannot cancel one already\n * computing, so a pass that lands after its own attach was replaced stands down\n * instead of writing.\n */\nexport class AnchoredController extends Controller<HTMLElement> {\n static override targets = [\"anchor\", \"floating\"];\n static override values = {\n placement: { type: String, default: \"bottom\" },\n offset: { type: Number, default: 0 },\n flip: { type: Boolean, default: true },\n shift: { type: Boolean, default: true },\n padding: { type: Number, default: 0 },\n strategy: { type: String, default: \"absolute\" },\n active: { type: Boolean, default: true },\n };\n static events = [\"position\"] as const;\n\n declare readonly anchorTarget: HTMLElement;\n declare readonly floatingTarget: HTMLElement;\n declare readonly hasAnchorTarget: boolean;\n declare readonly hasFloatingTarget: boolean;\n\n declare placementValue: string;\n declare offsetValue: number;\n declare flipValue: boolean;\n declare shiftValue: boolean;\n declare paddingValue: number;\n declare strategyValue: string;\n declare activeValue: boolean;\n\n /** `autoUpdate` cleanup while tracking; `null` when detached. */\n #stop: (() => void) | null = null;\n /** Identity of the live attach; `null` when detached — see {@link #onComputed}. */\n #attachId: symbol | null = null;\n /** True between connect and disconnect (Stimulus may fire value callbacks before connect). */\n #connected = false;\n /**\n * Serialized options as of the last reconcile, empty only before the first one —\n * a value no serialization produces, so it can never match a real key.\n */\n #appliedKey = \"\";\n /** Elements the live observer holds, or `null` when detached — see {@link #sync}. */\n #appliedAnchor: Element | null = null;\n #appliedFloating: HTMLElement | null = null;\n\n override connect(): void {\n this.#connected = true;\n this.#sync();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#sync();\n }\n\n // Every value change (active or an option) and every target arrival or\n // departure re-syncs. `#sync` is idempotent and order-independent, so no Value\n // has to be declared in a particular position.\n anchorTargetConnected(): void {\n this.#sync();\n }\n anchorTargetDisconnected(): void {\n this.#sync();\n }\n floatingTargetConnected(): void {\n this.#sync();\n }\n floatingTargetDisconnected(): void {\n this.#sync();\n }\n activeValueChanged(): void {\n this.#sync();\n }\n placementValueChanged(): void {\n this.#sync();\n }\n offsetValueChanged(): void {\n this.#sync();\n }\n flipValueChanged(): void {\n this.#sync();\n }\n shiftValueChanged(): void {\n this.#sync();\n }\n paddingValueChanged(): void {\n this.#sync();\n }\n strategyValueChanged(): void {\n this.#sync();\n }\n\n /** Current Values mapped to the positioning engine's options. */\n get #options(): PositioningOptions {\n return {\n // Narrow every free-form Value to what the engine can use. A placement\n // outside the set would be published on the hook as a resolved side it is\n // not, and a non-finite distance poisons the coordinate it feeds until the\n // browser drops the whole declaration and leaves that axis unplaced.\n placement: PLACEMENTS.has(this.placementValue)\n ? (this.placementValue as Placement)\n : \"bottom\",\n offset: Number.isFinite(this.offsetValue) ? this.offsetValue : 0,\n flip: this.flipValue,\n shift: this.shiftValue,\n padding: Number.isFinite(this.paddingValue) ? this.paddingValue : 0,\n strategy: this.strategyValue === \"fixed\" ? \"fixed\" : \"absolute\",\n };\n }\n\n /**\n * Reconciles the live observer with the desired state — track iff connected,\n * `active`, and both targets exist — re-attaching only when that state, the\n * options, or the elements actually changed. Stimulus fires the value-changed\n * callbacks on connect in declaration order and may run them before or after\n * `connect()`; keying on what is applied collapses that whole burst (in any\n * order) to a single attach, while an option or target change at runtime\n * re-attaches exactly once. So correctness never depends on `active` being\n * declared last. The elements are part of the key because the engine holds the\n * pair it was handed: a swap leaves the same options behind and would otherwise\n * keep measuring the node that just left the document.\n *\n * @stimeoRenderRoot\n */\n #sync(): void {\n const shouldTrack =\n this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;\n const anchor = shouldTrack ? this.anchorTarget : null;\n const floating = shouldTrack ? this.floatingTarget : null;\n // The elements already say whether anything is tracked, so the key only has\n // to say which options are on it.\n const key = JSON.stringify(this.#options);\n if (\n key === this.#appliedKey &&\n anchor === this.#appliedAnchor &&\n floating === this.#appliedFloating\n ) {\n return;\n }\n this.#detach();\n this.#appliedKey = key;\n this.#appliedAnchor = anchor;\n this.#appliedFloating = floating;\n if (anchor && floating) this.#attach(anchor, floating);\n }\n\n #attach(anchor: Element, floating: HTMLElement): void {\n const id = Symbol(\"anchored-attach\");\n this.#attachId = id;\n this.#stop = attachPositioning(anchor, floating, this.#options, (result) =>\n this.#onComputed(id, floating, result),\n );\n }\n\n #detach(): void {\n this.#stop?.();\n this.#stop = null;\n this.#attachId = null;\n }\n\n /**\n * Reflects the resolved side onto the CSS hook and announces the placement.\n *\n * The pass carries the attach that started it and the element it positioned,\n * and lands only while that attach is still the live one: `autoUpdate`'s\n * cleanup stops further updates but cannot cancel one already computing, so a\n * pass superseded mid-flight would otherwise write and dispatch afterwards.\n * The element cannot stand in for that identity — an option change re-attaches\n * against the very same pair.\n */\n #onComputed(id: symbol, floating: HTMLElement, result: PositionResult): void {\n if (id !== this.#attachId) return;\n floating.setAttribute(\"data-anchored-placement\", result.placement);\n this.dispatch(\"position\", {\n detail: { placement: result.placement, x: result.x, y: result.y },\n });\n }\n}\n","import {\n autoUpdate,\n type ComputePositionConfig,\n computePosition,\n flip,\n type Middleware,\n offset,\n type Placement,\n shift,\n} from \"@floating-ui/dom\";\nimport type { Application } from \"@hotwired/stimulus\";\nimport { AnchoredController } from \"./anchored_controller\";\n\nexport type { Placement };\nexport { AnchoredController };\n\n/**\n * Opt-in shared positioning helper for Stimeo's floating components\n * (popover / tooltip / hover-card / context-menu).\n *\n * **Why this is a separate entry point.** The core library is zero-runtime-dep:\n * `import \"stimeo-ui\"` pulls in nothing but `@hotwired/stimulus`. Dynamic\n * placement — measuring the viewport / scroll parents to flip and shift a\n * floating element away from screen edges — genuinely needs a small, trustworthy\n * dependency (`@floating-ui/dom`). To keep that cost *opt-in*, this lives at\n * `stimeo-ui/positioning` and is loaded only when a consumer explicitly imports\n * it. The controllers themselves never import this module, so the core install\n * stays dependency-free.\n *\n * **What it does and does not own.** This helper writes **coordinates only** —\n * `position`, `left`, `top` inline\n * styles on the floating element. It never emits color, border, shadow, size, or\n * any other decoration: the consumer's CSS still owns the entire look. Static\n * placement (a fixed `top`/`left` in CSS) needs no JS at all; reach for this only\n * when you want edge-collision avoidance.\n */\n\n/** Options accepted by {@link position} and {@link attachPositioning}. */\nexport interface PositioningOptions {\n /**\n * Preferred side of the anchor to place the floating element on. Mirrors\n * floating-ui's `Placement` (e.g. `\"bottom\"`, `\"top-start\"`). Default `\"bottom\"`.\n */\n placement?: Placement;\n /** Gap in pixels between the anchor and the floating element. Default `0`. */\n offset?: number;\n /**\n * Flip to the opposite side when the preferred side would overflow the\n * viewport. Default `true`.\n */\n flip?: boolean;\n /**\n * Shift the floating element along its axis to keep it in view. Default `true`.\n */\n shift?: boolean;\n /**\n * Padding (px) kept between the floating element and the viewport edge when\n * flipping/shifting. Default `0`.\n */\n padding?: number;\n /**\n * CSS positioning strategy written to the floating element. `\"absolute\"`\n * (default) positions against the nearest positioned ancestor; `\"fixed\"`\n * positions against the viewport (useful inside `overflow` containers).\n */\n strategy?: \"absolute\" | \"fixed\";\n}\n\n/**\n * The resolved outcome of one positioning pass: the coordinates written to the\n * floating element and the **final** placement after flip/shift. Returned by\n * {@link position} and surfaced per update through {@link attachPositioning}'s\n * `onComputed` callback so callers can react to the resolved side (e.g. flip an\n * arrow, mirror the placement onto a `data-*` hook) without re-measuring.\n */\nexport interface PositionResult {\n /** Final placement after flip/shift resolved it (e.g. `\"top-start\"`). */\n placement: Placement;\n /** X coordinate written as the floating element's inline `left`. */\n x: number;\n /** Y coordinate written as the floating element's inline `top`. */\n y: number;\n}\n\n/** Builds the floating-ui middleware stack from {@link PositioningOptions}. */\nfunction buildMiddleware(options: PositioningOptions): Middleware[] {\n const padding = options.padding ?? 0;\n const middleware: Middleware[] = [];\n if (options.offset) middleware.push(offset(options.offset));\n // flip before shift so a side change is considered before nudging along-axis.\n if (options.flip !== false) middleware.push(flip({ padding }));\n if (options.shift !== false) middleware.push(shift({ padding }));\n return middleware;\n}\n\n/**\n * Computes a single placement for `floating` relative to `anchor` and writes the\n * resulting coordinates as inline styles on `floating`.\n *\n * This is the one-shot form: it positions once and returns. For a floating\n * element that must track scrolling/resizing while open, use\n * {@link attachPositioning} instead.\n *\n * Only `position`, `left`, and `top` are written — never any decoration. The\n * resolved {@link PositionResult} (final placement + coordinates) is returned so\n * callers can react to the side flip/shift chose.\n */\nexport async function position(\n anchor: Element,\n floating: HTMLElement,\n options: PositioningOptions = {},\n): Promise<PositionResult> {\n const strategy = options.strategy ?? \"absolute\";\n const config: Partial<ComputePositionConfig> = {\n placement: options.placement ?? \"bottom\",\n middleware: buildMiddleware(options),\n strategy,\n };\n const { x, y, placement } = await computePosition(anchor, floating, config);\n Object.assign(floating.style, {\n position: strategy,\n left: `${x}px`,\n top: `${y}px`,\n });\n return { x, y, placement };\n}\n\n/**\n * Positions `floating` against `anchor` and keeps it positioned across scroll,\n * resize, and layout changes via floating-ui's `autoUpdate`.\n *\n * Returns a cleanup function that stops tracking; call it when the floating\n * element closes or the controller disconnects (Turbo navigation included) so no\n * observer outlives the element.\n *\n * Pass `onComputed` to receive the resolved {@link PositionResult} on every\n * update (initial placement and each scroll/resize re-computation) — used to\n * mirror the final placement onto a hook or emit an event without re-measuring.\n *\n * @example\n * ```ts\n * import { attachPositioning } from \"stimeo-ui/positioning\";\n *\n * // when a popover opens:\n * const stop = attachPositioning(trigger, panel, { placement: \"bottom-start\", offset: 8 });\n * // when it closes:\n * stop();\n * ```\n */\nexport function attachPositioning(\n anchor: Element,\n floating: HTMLElement,\n options: PositioningOptions = {},\n onComputed?: (result: PositionResult) => void,\n): () => void {\n return autoUpdate(anchor, floating, () => {\n void position(anchor, floating, options).then((result) => onComputed?.(result));\n });\n}\n\n/**\n * Maps the opt-in positioning controller identifiers to their classes. Kept\n * separate from the core `stimeoControllers` (`src/index.ts`) so the core\n * install never imports `@floating-ui/dom`; the Inspector manifest reflects both\n * core and positioning controllers so `stimeo check` recognizes them.\n */\nexport const positioningControllers = {\n \"stimeo--anchored\": AnchoredController,\n} as const;\n\n/**\n * Registers the opt-in positioning controllers (e.g. `stimeo--anchored`) on a\n * Stimulus application. Call this **in addition to** `registerStimeo` only when\n * you want the declarative positioning primitives — importing this module is\n * what pulls in `@floating-ui/dom`, so the core stays zero-dependency for\n * consumers who never call it.\n *\n * @param application - The Stimulus application to register controllers on.\n *\n * @example\n * ```ts\n * import { Application } from \"@hotwired/stimulus\";\n * import { registerStimeo } from \"stimeo-ui\";\n * import { registerPositioning } from \"stimeo-ui/positioning\";\n *\n * const application = Application.start();\n * registerStimeo(application);\n * registerPositioning(application); // opt-in: adds stimeo--anchored\n * ```\n */\nexport function registerPositioning(application: Application): void {\n for (const [identifier, controller] of Object.entries(positioningControllers)) {\n application.register(identifier, controller);\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/positioning/anchored_controller.ts","../../src/positioning/index.ts"],"names":[],"mappings":"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","file":"index.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\";\nimport { attachPositioning, type Placement, type PositioningOptions, type PositionResult } from \".\";\n\n/**\n * Every placement the engine accepts. A declaration outside the set has no\n * resolved side to publish, so it falls back to the Value's default rather than\n * reaching the state hook and the event detail as-is.\n */\nconst PLACEMENTS: ReadonlySet<string> = new Set([\n \"top\",\n \"top-start\",\n \"top-end\",\n \"right\",\n \"right-start\",\n \"right-end\",\n \"bottom\",\n \"bottom-start\",\n \"bottom-end\",\n \"left\",\n \"left-start\",\n \"left-end\",\n]);\n\n/**\n * Headless **anchored positioning**: keeps a `floating` element placed against an\n * `anchor`, flipping/shifting it away from viewport edges as the page scrolls or\n * resizes. It is the declarative surface of the opt-in {@link attachPositioning}\n * engine (`@floating-ui/dom`-based), exposing its `autoUpdate` behavior as a\n * controller. No dedicated APG pattern; it is a placement primitive a consumer pairs\n * with a popup such as `stimeo--tooltip`, `stimeo--menu` or `stimeo--popover`.\n *\n * Markup contract (identifier: `stimeo--anchored`):\n * <div data-controller=\"stimeo--anchored\"\n * data-stimeo--anchored-placement-value=\"bottom-start\"\n * data-stimeo--anchored-offset-value=\"8\">\n * <button data-stimeo--anchored-target=\"anchor\">Open</button>\n * <div data-stimeo--anchored-target=\"floating\" role=\"…\">…</div>\n * </div>\n *\n * `active` drives tracking (start/stop) and fires on connect, mirroring\n * `stimeo--focus`'s `trapValueChanged`; set it `false` while the floating element is\n * hidden so no measurement runs. The other Values map to {@link PositioningOptions} and\n * re-apply live while tracking. A declaration the engine cannot use — a\n * `placement` outside its set, a non-finite `offset` or `padding` — falls back to\n * that Value's default, so a typo never reaches the coordinates or the hook. Only\n * `position`/`left`/`top` inline styles are written — never decoration — and the\n * resolved (post-flip) side is mirrored onto `data-anchored-placement` on the\n * floating element for CSS hooks (e.g. an arrow).\n *\n * Tracking follows the targets themselves, not just the Values: the engine holds\n * the two elements it was handed, so a target added, removed or swapped at\n * runtime re-attaches against whatever is there now.\n *\n * `position` dispatches `{ placement, x, y }`.\n *\n * @remarks\n * Behavior only. It does **not** open/close, manage focus, or render an overlay\n * (pair with `stimeo--dialog` / `stimeo--popover` and `stimeo--focus`), and it does\n * **not** move DOM (pair with `stimeo--portal`). It lives in the opt-in\n * `stimeo-ui/positioning` subpath so the core `import \"stimeo-ui\"` stays\n * zero-dependency; only consumers who register it pull in `@floating-ui/dom`. The\n * `autoUpdate` cleanup is released on `disconnect()` (Turbo navigation included)\n * so no observer outlives the element, and `#sync` reconciles to a single live\n * observer (keyed on the applied options and elements) so reconnects never stack\n * observers. That cleanup stops future updates but cannot cancel one already\n * computing, so a pass that lands after its own attach was replaced stands down\n * instead of writing.\n */\nexport class AnchoredController extends Controller<HTMLElement> {\n /** Numeric read boundaries share one reader for this controller instance. */\n readonly #numbers = new NumberValueReader();\n\n static override targets = [\"anchor\", \"floating\"];\n static override values = {\n placement: { type: String, default: \"bottom\" },\n offset: { type: Number, default: 0 },\n flip: { type: Boolean, default: true },\n shift: { type: Boolean, default: true },\n padding: { type: Number, default: 0 },\n strategy: { type: String, default: \"absolute\" },\n active: { type: Boolean, default: true },\n };\n\n static valueConstraints = {\n offset: NUMBER_BOUNDS.finite,\n padding: NUMBER_BOUNDS.finite,\n } satisfies NumberValueConstraints<typeof AnchoredController.values>;\n static events = [\"position\"] as const;\n\n declare readonly anchorTarget: HTMLElement;\n declare readonly floatingTarget: HTMLElement;\n declare readonly hasAnchorTarget: boolean;\n declare readonly hasFloatingTarget: boolean;\n\n declare placementValue: string;\n declare offsetValue: number;\n declare flipValue: boolean;\n declare shiftValue: boolean;\n declare paddingValue: number;\n declare strategyValue: string;\n declare activeValue: boolean;\n\n /** `autoUpdate` cleanup while tracking; `null` when detached. */\n #stop: (() => void) | null = null;\n /** Identity of the live attach; `null` when detached — see {@link #onComputed}. */\n #attachId: symbol | null = null;\n /** True between connect and disconnect (Stimulus may fire value callbacks before connect). */\n #connected = false;\n #position: PositionResult | null = null;\n readonly #morphRender = new MorphRenderWatcher(() => this.#sync());\n /**\n * Serialized options as of the last reconcile, empty only before the first one —\n * a value no serialization produces, so it can never match a real key.\n */\n #appliedKey = \"\";\n /** Elements the live observer holds, or `null` when detached — see {@link #sync}. */\n #appliedAnchor: Element | null = null;\n #appliedFloating: HTMLElement | null = null;\n\n override connect(): void {\n this.#morphRender.observe(this.element);\n this.#connected = true;\n this.#sync();\n }\n\n override disconnect(): void {\n this.#morphRender.disconnect();\n this.#connected = false;\n this.#sync();\n }\n\n // Every value change (active or an option) and every target arrival or\n // departure re-syncs. `#sync` is idempotent and order-independent, so no Value\n // has to be declared in a particular position.\n anchorTargetConnected(): void {\n this.#sync();\n }\n anchorTargetDisconnected(): void {\n this.#sync();\n }\n floatingTargetConnected(): void {\n this.#sync();\n }\n floatingTargetDisconnected(): void {\n this.#sync();\n }\n activeValueChanged(): void {\n this.#sync();\n }\n placementValueChanged(): void {\n this.#sync();\n }\n offsetValueChanged(): void {\n this.#sync();\n }\n flipValueChanged(): void {\n this.#sync();\n }\n shiftValueChanged(): void {\n this.#sync();\n }\n paddingValueChanged(): void {\n this.#sync();\n }\n strategyValueChanged(): void {\n this.#sync();\n }\n\n /** Current Values mapped to the positioning engine's options. */\n get #options(): PositioningOptions {\n return {\n // Narrow every free-form Value to what the engine can use. A placement\n // outside the set would be published on the hook as a resolved side it is\n // not, and a non-finite distance poisons the coordinate it feeds until the\n // browser drops the whole declaration and leaves that axis unplaced.\n placement: PLACEMENTS.has(this.placementValue)\n ? (this.placementValue as Placement)\n : \"bottom\",\n offset: this.#safeOffset,\n flip: this.flipValue,\n shift: this.shiftValue,\n padding: this.#safePadding,\n strategy: this.strategyValue === \"fixed\" ? \"fixed\" : \"absolute\",\n };\n }\n\n /**\n * Reconciles the live observer with the desired state — track iff connected,\n * `active`, and both targets exist — re-attaching only when that state, the\n * options, or the elements actually changed. Stimulus fires the value-changed\n * callbacks on connect in declaration order and may run them before or after\n * `connect()`; keying on what is applied collapses that whole burst (in any\n * order) to a single attach, while an option or target change at runtime\n * re-attaches exactly once. So correctness never depends on `active` being\n * declared last. The elements are part of the key because the engine holds the\n * pair it was handed: a swap leaves the same options behind and would otherwise\n * keep measuring the node that just left the document.\n *\n * @stimeoRenderRoot\n */\n #sync(): void {\n const shouldTrack =\n this.#connected && this.activeValue && this.hasAnchorTarget && this.hasFloatingTarget;\n const anchor = shouldTrack ? this.anchorTarget : null;\n const floating = shouldTrack ? this.floatingTarget : null;\n // The elements already say whether anything is tracked, so the key only has\n // to say which options are on it.\n const key = JSON.stringify(this.#options);\n if (\n key === this.#appliedKey &&\n anchor === this.#appliedAnchor &&\n floating === this.#appliedFloating\n ) {\n if (floating && this.#position) this.#reflectPosition(floating, this.#position);\n return;\n }\n this.#detach();\n this.#appliedKey = key;\n this.#appliedAnchor = anchor;\n this.#appliedFloating = floating;\n if (anchor && floating) this.#attach(anchor, floating);\n }\n\n #attach(anchor: Element, floating: HTMLElement): void {\n const id = Symbol(\"anchored-attach\");\n this.#attachId = id;\n this.#stop = attachPositioning(anchor, floating, this.#options, (result) =>\n this.#onComputed(id, floating, result),\n );\n }\n\n #detach(): void {\n this.#stop?.();\n this.#stop = null;\n this.#attachId = null;\n this.#position = null;\n }\n\n /**\n * Reflects the resolved side onto the CSS hook and announces the placement.\n *\n * The pass carries the attach that started it and the element it positioned,\n * and lands only while that attach is still the live one: `autoUpdate`'s\n * cleanup stops further updates but cannot cancel one already computing, so a\n * pass superseded mid-flight would otherwise write and dispatch afterwards.\n * The element cannot stand in for that identity — an option change re-attaches\n * against the very same pair.\n */\n #onComputed(id: symbol, floating: HTMLElement, result: PositionResult): void {\n if (id !== this.#attachId) return;\n this.#position = result;\n this.#reflectPosition(floating, result);\n this.dispatch(\"position\", {\n detail: { placement: result.placement, x: result.x, y: result.y },\n });\n }\n /** Restores the last completed placement without restarting the attached engine. */\n #reflectPosition(floating: HTMLElement, result: PositionResult): void {\n floating.setAttribute(\"data-anchored-placement\", result.placement);\n floating.style.position = this.#options.strategy ?? \"absolute\";\n floating.style.left = `${result.x}px`;\n floating.style.top = `${result.y}px`;\n }\n /** Current `offset` declaration resolved against its numeric contract. */\n get #safeOffset(): number {\n return this.#numbers.read(\n this,\n \"offset\",\n this.offsetValue,\n AnchoredController.values.offset.default,\n AnchoredController.valueConstraints.offset,\n );\n }\n\n /** Current `padding` declaration resolved against its numeric contract. */\n get #safePadding(): number {\n return this.#numbers.read(\n this,\n \"padding\",\n this.paddingValue,\n AnchoredController.values.padding.default,\n AnchoredController.valueConstraints.padding,\n );\n }\n}\n","import {\n autoUpdate,\n type ComputePositionConfig,\n computePosition,\n flip,\n type Middleware,\n offset,\n type Placement,\n shift,\n} from \"@floating-ui/dom\";\nimport type { Application } from \"@hotwired/stimulus\";\nimport { AnchoredController } from \"./anchored_controller\";\n\nexport type { Placement };\nexport { AnchoredController };\n\n/**\n * Opt-in shared positioning helper for Stimeo's floating components\n * (popover / tooltip / hover-card / context-menu).\n *\n * **Why this is a separate entry point.** The core library is zero-runtime-dep:\n * `import \"stimeo-ui\"` pulls in nothing but `@hotwired/stimulus`. Dynamic\n * placement — measuring the viewport / scroll parents to flip and shift a\n * floating element away from screen edges — genuinely needs a small, trustworthy\n * dependency (`@floating-ui/dom`). To keep that cost *opt-in*, this lives at\n * `stimeo-ui/positioning` and is loaded only when a consumer explicitly imports\n * it. The controllers themselves never import this module, so the core install\n * stays dependency-free.\n *\n * **What it does and does not own.** This helper writes **coordinates only** —\n * `position`, `left`, `top` inline\n * styles on the floating element. It never emits color, border, shadow, size, or\n * any other decoration: the consumer's CSS still owns the entire look. Static\n * placement (a fixed `top`/`left` in CSS) needs no JS at all; reach for this only\n * when you want edge-collision avoidance.\n */\n\n/** Options accepted by {@link position} and {@link attachPositioning}. */\nexport interface PositioningOptions {\n /**\n * Preferred side of the anchor to place the floating element on. Mirrors\n * floating-ui's `Placement` (e.g. `\"bottom\"`, `\"top-start\"`). Default `\"bottom\"`.\n */\n placement?: Placement;\n /** Gap in pixels between the anchor and the floating element. Default `0`. */\n offset?: number;\n /**\n * Flip to the opposite side when the preferred side would overflow the\n * viewport. Default `true`.\n */\n flip?: boolean;\n /**\n * Shift the floating element along its axis to keep it in view. Default `true`.\n */\n shift?: boolean;\n /**\n * Padding (px) kept between the floating element and the viewport edge when\n * flipping/shifting. Default `0`.\n */\n padding?: number;\n /**\n * CSS positioning strategy written to the floating element. `\"absolute\"`\n * (default) positions against the nearest positioned ancestor; `\"fixed\"`\n * positions against the viewport (useful inside `overflow` containers).\n */\n strategy?: \"absolute\" | \"fixed\";\n}\n\n/**\n * The resolved outcome of one positioning pass: the coordinates written to the\n * floating element and the **final** placement after flip/shift. Returned by\n * {@link position} and surfaced per update through {@link attachPositioning}'s\n * `onComputed` callback so callers can react to the resolved side (e.g. flip an\n * arrow, mirror the placement onto a `data-*` hook) without re-measuring.\n */\nexport interface PositionResult {\n /** Final placement after flip/shift resolved it (e.g. `\"top-start\"`). */\n placement: Placement;\n /** X coordinate written as the floating element's inline `left`. */\n x: number;\n /** Y coordinate written as the floating element's inline `top`. */\n y: number;\n}\n\n/** Builds the floating-ui middleware stack from {@link PositioningOptions}. */\nfunction buildMiddleware(options: PositioningOptions): Middleware[] {\n const padding = options.padding ?? 0;\n const middleware: Middleware[] = [];\n if (options.offset) middleware.push(offset(options.offset));\n // flip before shift so a side change is considered before nudging along-axis.\n if (options.flip !== false) middleware.push(flip({ padding }));\n if (options.shift !== false) middleware.push(shift({ padding }));\n return middleware;\n}\n\n/**\n * Computes a single placement for `floating` relative to `anchor` and writes the\n * resulting coordinates as inline styles on `floating`.\n *\n * This is the one-shot form: it positions once and returns. For a floating\n * element that must track scrolling/resizing while open, use\n * {@link attachPositioning} instead.\n *\n * Only `position`, `left`, and `top` are written — never any decoration. The\n * resolved {@link PositionResult} (final placement + coordinates) is returned so\n * callers can react to the side flip/shift chose.\n */\nexport async function position(\n anchor: Element,\n floating: HTMLElement,\n options: PositioningOptions = {},\n): Promise<PositionResult> {\n const strategy = options.strategy ?? \"absolute\";\n const config: Partial<ComputePositionConfig> = {\n placement: options.placement ?? \"bottom\",\n middleware: buildMiddleware(options),\n strategy,\n };\n const { x, y, placement } = await computePosition(anchor, floating, config);\n Object.assign(floating.style, {\n position: strategy,\n left: `${x}px`,\n top: `${y}px`,\n });\n return { x, y, placement };\n}\n\n/**\n * Positions `floating` against `anchor` and keeps it positioned across scroll,\n * resize, and layout changes via floating-ui's `autoUpdate`.\n *\n * Returns a cleanup function that stops tracking; call it when the floating\n * element closes or the controller disconnects (Turbo navigation included) so no\n * observer outlives the element.\n *\n * Pass `onComputed` to receive the resolved {@link PositionResult} on every\n * update (initial placement and each scroll/resize re-computation) — used to\n * mirror the final placement onto a hook or emit an event without re-measuring.\n *\n * @example\n * ```ts\n * import { attachPositioning } from \"stimeo-ui/positioning\";\n *\n * // when a popover opens:\n * const stop = attachPositioning(trigger, panel, { placement: \"bottom-start\", offset: 8 });\n * // when it closes:\n * stop();\n * ```\n */\nexport function attachPositioning(\n anchor: Element,\n floating: HTMLElement,\n options: PositioningOptions = {},\n onComputed?: (result: PositionResult) => void,\n): () => void {\n return autoUpdate(anchor, floating, () => {\n void position(anchor, floating, options).then((result) => onComputed?.(result));\n });\n}\n\n/**\n * Maps the opt-in positioning controller identifiers to their classes. Kept\n * separate from the core `stimeoControllers` (`src/index.ts`) so the core\n * install never imports `@floating-ui/dom`; the Inspector manifest reflects both\n * core and positioning controllers so `stimeo check` recognizes them.\n */\nexport const positioningControllers = {\n \"stimeo--anchored\": AnchoredController,\n} as const;\n\n/**\n * Registers the opt-in positioning controllers (e.g. `stimeo--anchored`) on a\n * Stimulus application. Call this **in addition to** `registerStimeo` only when\n * you want the declarative positioning primitives — importing this module is\n * what pulls in `@floating-ui/dom`, so the core stays zero-dependency for\n * consumers who never call it.\n *\n * @param application - The Stimulus application to register controllers on.\n *\n * @example\n * ```ts\n * import { Application } from \"@hotwired/stimulus\";\n * import { registerStimeo } from \"stimeo-ui\";\n * import { registerPositioning } from \"stimeo-ui/positioning\";\n *\n * const application = Application.start();\n * registerStimeo(application);\n * registerPositioning(application); // opt-in: adds stimeo--anchored\n * ```\n */\nexport function registerPositioning(application: Application): void {\n for (const [identifier, controller] of Object.entries(positioningControllers)) {\n application.register(identifier, controller);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stimeo-ui",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Headless Stimulus UI framework for Ruby on Rails — behavior-only, accessible components driven by HTML attributes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -64,7 +64,7 @@
64
64
  "test": "vitest run",
65
65
  "test:watch": "vitest",
66
66
  "test:coverage": "vitest run --coverage",
67
- "typecheck": "tsc --noEmit",
67
+ "typecheck": "bun node_modules/@typescript/native/bin/tsc --noEmit",
68
68
  "lint": "biome check .",
69
69
  "lint:fix": "biome check --write .",
70
70
  "format": "biome format --write .",
@@ -84,21 +84,22 @@
84
84
  }
85
85
  },
86
86
  "devDependencies": {
87
- "@biomejs/biome": "2.5.14",
87
+ "@biomejs/biome": "2.5.15",
88
88
  "@floating-ui/dom": "1.8.0",
89
89
  "@guidepup/virtual-screen-reader": "0.33.0",
90
90
  "@hotwired/stimulus": "3.2.2",
91
91
  "@hotwired/turbo": "8.0.23",
92
- "@rails/actioncable": "8.1.301",
93
- "@types/node": "24.13.6",
92
+ "@rails/actioncable": "8.1.400",
93
+ "@types/node": "24.19.2",
94
94
  "@types/rails__actioncable": "8.0.3",
95
- "@vitest/coverage-istanbul": "5.0.1",
96
- "axe-core": "4.13.0",
97
- "happy-dom": "20.14.5",
98
- "rollup": "4.63.3",
95
+ "@typescript/native": "npm:typescript@7.0.2",
96
+ "@vitest/coverage-istanbul": "5.0.5",
97
+ "axe-core": "4.14.0",
98
+ "happy-dom": "20.14.6",
99
+ "rollup": "4.64.12",
99
100
  "tsup": "8.5.1",
100
101
  "typescript": "6.0.3",
101
- "vitest": "5.0.1",
102
+ "vitest": "5.0.5",
102
103
  "vitest-axe": "0.1.0"
103
104
  }
104
105
  }
@@ -1,76 +0,0 @@
1
- import { Controller } from '@hotwired/stimulus';
2
-
3
- /**
4
- * Headless **before-cache reset** — Hotwire-specific, with no APG pattern. On `turbo:before-cache` it returns transient UI (open menus/modals,
5
- * typed-in values, spinning indicators) to its initial state, so a page restored by
6
- * the Back button is not frozen mid-interaction. Place one on `<body>`.
7
- *
8
- * Markup contract (identifier: `stimeo--reset-before-cache`):
9
- * <body data-controller="stimeo--reset-before-cache">
10
- * <details data-reset-attr="open">…</details> <!-- remove these attributes -->
11
- * <div data-reset-class="is-open is-loading">…</div> <!-- remove these classes -->
12
- * <form data-reset-form>…</form> <!-- form.reset() -->
13
- * <input data-reset-value> <!-- back to its authored state -->
14
- * <div data-reset-hidden>transient overlay</div> <!-- re-hide -->
15
- * <div data-reset-remove>flash toast</div> <!-- drop from the DOM -->
16
- * </body>
17
- *
18
- * `scope` narrows the sweep to the first descendant matching that CSS selector;
19
- * left empty (the default) it covers the whole element. A selector the engine
20
- * cannot read falls back to that default rather than taking the sweep down with
21
- * it, and so does one that matches nothing.
22
- *
23
- * `reset` dispatches `{}`; `request` dispatches `{}`.
24
- *
25
- * @remarks
26
- * Behavior only and **idempotent** — every run converges on the same initial state,
27
- * holding no module-scope state. It does the cross-cutting DOM cleanup directly
28
- * (attribute removal, class removal, restoring a field to its authored state,
29
- * re-hiding, node removal) and, when
30
- * `dispatchReset` is on, fires `stimeo--reset-before-cache:request` so individual
31
- * Stimeo controllers can run their own close logic. The `turbo:before-cache`
32
- * listener is paired to `connect()` / `disconnect()` so it never double-registers or
33
- * leaks. {@link reset} is also a public action for manual triggering.
34
- */
35
- declare class ResetBeforeCacheController extends Controller<HTMLElement> {
36
- #private;
37
- static values: {
38
- scope: {
39
- type: StringConstructor;
40
- default: string;
41
- };
42
- dispatchReset: {
43
- type: BooleanConstructor;
44
- default: boolean;
45
- };
46
- };
47
- static actions: readonly ["reset"];
48
- static events: readonly ["reset", "request"];
49
- scopeValue: string;
50
- dispatchResetValue: boolean;
51
- /**
52
- * Validates the scope declaration once, keeping only a selector the engine can
53
- * read.
54
- *
55
- * A selector reads back as an ordinary string, so a malformed one survives
56
- * until it is handed to the DOM — and this part runs from a single listener
57
- * whose whole job is to keep a cached page from freezing. Falling back to the
58
- * default keeps that job running with a visible, findable result instead of
59
- * silently taking the sweep down.
60
- */
61
- scopeValueChanged(): void;
62
- connect(): void;
63
- disconnect(): void;
64
- /**
65
- * Resets transient UI within scope to its initial state. Asks controllers to
66
- * close (via `request`) first, then applies the declarative `data-reset-*` cleanup,
67
- * and finally emits `reset`. Safe to call any number of times (idempotent).
68
- *
69
- * `dispatchReset` decides only whether the `request` ask goes out; the cleanup
70
- * and the closing `reset` run either way. Both the listener and this action
71
- * reach the same sweep, so `reset` is emitted for a manual call too.
72
- */
73
- reset(): void;
74
- }
75
-
76
- export { ResetBeforeCacheController };