stimeo-ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +27 -11
  3. package/dist/cable/index.d.ts +88 -31
  4. package/dist/cable/index.js +384 -449
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/coerce-JtZVNG5h.d.ts +20 -0
  7. package/dist/controllers/accordion_controller.d.ts +4 -2
  8. package/dist/controllers/accordion_controller.js +174 -55
  9. package/dist/controllers/accordion_controller.js.map +1 -1
  10. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  11. package/dist/controllers/alert_dialog_controller.js +903 -235
  12. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  13. package/dist/controllers/announcer_controller.d.ts +37 -5
  14. package/dist/controllers/announcer_controller.js +238 -205
  15. package/dist/controllers/announcer_controller.js.map +1 -1
  16. package/dist/controllers/aspect_ratio_controller.js +0 -10
  17. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  18. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  19. package/dist/controllers/auto_submit_controller.js +414 -145
  20. package/dist/controllers/auto_submit_controller.js.map +1 -1
  21. package/dist/controllers/avatar_controller.js +185 -107
  22. package/dist/controllers/avatar_controller.js.map +1 -1
  23. package/dist/controllers/breadcrumb_controller.js +0 -130
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +21 -3
  26. package/dist/controllers/bulk_select_controller.js +282 -58
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +38 -16
  29. package/dist/controllers/calendar_controller.js +228 -260
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +43 -12
  32. package/dist/controllers/carousel_controller.js +536 -436
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +44 -7
  35. package/dist/controllers/character_counter_controller.js +493 -207
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +38 -74
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +16 -5
  41. package/dist/controllers/clipboard_controller.js +261 -149
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +17 -5
  44. package/dist/controllers/collapsible_controller.js +304 -120
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +18 -6
  47. package/dist/controllers/color_picker_controller.js +178 -146
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +24 -6
  50. package/dist/controllers/combobox_controller.js +522 -156
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +51 -20
  53. package/dist/controllers/command_palette_controller.js +1156 -395
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +300 -171
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +980 -262
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +80 -18
  62. package/dist/controllers/context_menu_controller.js +346 -153
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +47 -3
  68. package/dist/controllers/countdown_controller.js +281 -158
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -10
  71. package/dist/controllers/currency_input_controller.js +575 -225
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +25 -12
  74. package/dist/controllers/data_grid_controller.js +248 -214
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +42 -14
  77. package/dist/controllers/date_range_picker_controller.js +338 -235
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +17 -6
  80. package/dist/controllers/dialog_controller.js +899 -237
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +25 -8
  83. package/dist/controllers/direct_upload_controller.js +97 -167
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +6 -1
  86. package/dist/controllers/dirty_form_controller.js +72 -69
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +196 -24
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +39 -11
  92. package/dist/controllers/drawer_controller.js +974 -355
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +31 -1
  95. package/dist/controllers/dropdown_controller.js +339 -96
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +315 -88
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +79 -58
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +63 -18
  103. package/dist/controllers/file_dropzone_controller.js +322 -211
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.js +77 -42
  106. package/dist/controllers/filter_controller.js.map +1 -1
  107. package/dist/controllers/flash_controller.d.ts +35 -12
  108. package/dist/controllers/flash_controller.js +238 -347
  109. package/dist/controllers/flash_controller.js.map +1 -1
  110. package/dist/controllers/focus_controller.d.ts +15 -6
  111. package/dist/controllers/focus_controller.js +679 -240
  112. package/dist/controllers/focus_controller.js.map +1 -1
  113. package/dist/controllers/form_field_controller.js +156 -132
  114. package/dist/controllers/form_field_controller.js.map +1 -1
  115. package/dist/controllers/form_validation_controller.d.ts +3 -3
  116. package/dist/controllers/form_validation_controller.js +31 -97
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +47 -14
  119. package/dist/controllers/frame_loading_controller.js +275 -217
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +27 -6
  122. package/dist/controllers/highlight_controller.js +107 -75
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +60 -4
  125. package/dist/controllers/hover_card_controller.js +327 -130
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +48 -6
  128. package/dist/controllers/idle_controller.js +271 -101
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +30 -13
  131. package/dist/controllers/input_mask_controller.js +483 -97
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +150 -106
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +19 -2
  137. package/dist/controllers/lazy_frame_controller.js +24 -68
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +44 -9
  140. package/dist/controllers/listbox_controller.js +523 -240
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +9 -7
  143. package/dist/controllers/local_time_controller.js +102 -77
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +80 -96
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +55 -11
  149. package/dist/controllers/menu_controller.js +302 -188
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +51 -9
  152. package/dist/controllers/menubar_controller.js +232 -345
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +33 -20
  155. package/dist/controllers/meter_controller.js +417 -87
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +48 -18
  158. package/dist/controllers/multi_select_controller.js +785 -393
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +27 -6
  161. package/dist/controllers/navigation_menu_controller.js +190 -250
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +396 -192
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +268 -62
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +50 -8
  170. package/dist/controllers/number_input_controller.js +610 -303
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.d.ts +8 -5
  173. package/dist/controllers/optimistic_controller.js +45 -87
  174. package/dist/controllers/optimistic_controller.js.map +1 -1
  175. package/dist/controllers/otp_controller.d.ts +55 -8
  176. package/dist/controllers/otp_controller.js +658 -255
  177. package/dist/controllers/otp_controller.js.map +1 -1
  178. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  179. package/dist/controllers/overflow_indicator_controller.js +122 -113
  180. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  181. package/dist/controllers/overflow_menu_controller.d.ts +58 -20
  182. package/dist/controllers/overflow_menu_controller.js +470 -295
  183. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  184. package/dist/controllers/pagination_controller.d.ts +37 -7
  185. package/dist/controllers/pagination_controller.js +332 -123
  186. package/dist/controllers/pagination_controller.js.map +1 -1
  187. package/dist/controllers/password_reveal_controller.d.ts +46 -8
  188. package/dist/controllers/password_reveal_controller.js +392 -121
  189. package/dist/controllers/password_reveal_controller.js.map +1 -1
  190. package/dist/controllers/password_strength_controller.d.ts +15 -6
  191. package/dist/controllers/password_strength_controller.js +111 -158
  192. package/dist/controllers/password_strength_controller.js.map +1 -1
  193. package/dist/controllers/persist_controller.d.ts +11 -4
  194. package/dist/controllers/persist_controller.js +165 -111
  195. package/dist/controllers/persist_controller.js.map +1 -1
  196. package/dist/controllers/pointer_drag_controller.d.ts +35 -12
  197. package/dist/controllers/pointer_drag_controller.js +434 -188
  198. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  199. package/dist/controllers/popover_controller.d.ts +41 -5
  200. package/dist/controllers/popover_controller.js +269 -113
  201. package/dist/controllers/popover_controller.js.map +1 -1
  202. package/dist/controllers/portal_controller.d.ts +44 -4
  203. package/dist/controllers/portal_controller.js +215 -76
  204. package/dist/controllers/portal_controller.js.map +1 -1
  205. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  206. package/dist/controllers/preview_guard_controller.js +243 -116
  207. package/dist/controllers/preview_guard_controller.js.map +1 -1
  208. package/dist/controllers/progress_controller.d.ts +20 -19
  209. package/dist/controllers/progress_controller.js +391 -78
  210. package/dist/controllers/progress_controller.js.map +1 -1
  211. package/dist/controllers/radio_group_controller.d.ts +21 -9
  212. package/dist/controllers/radio_group_controller.js +278 -211
  213. package/dist/controllers/radio_group_controller.js.map +1 -1
  214. package/dist/controllers/range_slider_controller.d.ts +47 -6
  215. package/dist/controllers/range_slider_controller.js +220 -113
  216. package/dist/controllers/range_slider_controller.js.map +1 -1
  217. package/dist/controllers/rating_controller.d.ts +20 -4
  218. package/dist/controllers/rating_controller.js +458 -211
  219. package/dist/controllers/rating_controller.js.map +1 -1
  220. package/dist/controllers/read_more_controller.d.ts +2 -0
  221. package/dist/controllers/read_more_controller.js +138 -63
  222. package/dist/controllers/read_more_controller.js.map +1 -1
  223. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  224. package/dist/controllers/reading_progress_controller.js +166 -137
  225. package/dist/controllers/reading_progress_controller.js.map +1 -1
  226. package/dist/controllers/relative_time_controller.d.ts +20 -6
  227. package/dist/controllers/relative_time_controller.js +211 -112
  228. package/dist/controllers/relative_time_controller.js.map +1 -1
  229. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  230. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  231. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  232. package/dist/controllers/resizable_controller.d.ts +31 -2
  233. package/dist/controllers/resizable_controller.js +299 -150
  234. package/dist/controllers/resizable_controller.js.map +1 -1
  235. package/dist/controllers/roving_controller.d.ts +3 -3
  236. package/dist/controllers/roving_controller.js +49 -88
  237. package/dist/controllers/roving_controller.js.map +1 -1
  238. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  239. package/dist/controllers/scroll_area_controller.js +490 -246
  240. package/dist/controllers/scroll_area_controller.js.map +1 -1
  241. package/dist/controllers/scroll_restore_controller.d.ts +3 -3
  242. package/dist/controllers/scroll_restore_controller.js +14 -55
  243. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  244. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  245. package/dist/controllers/scroll_visibility_controller.js +432 -149
  246. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  247. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  248. package/dist/controllers/scrollspy_controller.js +193 -248
  249. package/dist/controllers/scrollspy_controller.js.map +1 -1
  250. package/dist/controllers/separator_controller.d.ts +20 -5
  251. package/dist/controllers/separator_controller.js +285 -142
  252. package/dist/controllers/separator_controller.js.map +1 -1
  253. package/dist/controllers/sidebar_controller.d.ts +65 -8
  254. package/dist/controllers/sidebar_controller.js +1069 -404
  255. package/dist/controllers/sidebar_controller.js.map +1 -1
  256. package/dist/controllers/skeleton_controller.d.ts +23 -0
  257. package/dist/controllers/skeleton_controller.js +259 -101
  258. package/dist/controllers/skeleton_controller.js.map +1 -1
  259. package/dist/controllers/slider_controller.d.ts +29 -2
  260. package/dist/controllers/slider_controller.js +187 -86
  261. package/dist/controllers/slider_controller.js.map +1 -1
  262. package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
  263. package/dist/controllers/smart_sticky_header_controller.js +173 -66
  264. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  265. package/dist/controllers/sortable_controller.d.ts +4 -4
  266. package/dist/controllers/sortable_controller.js +26 -125
  267. package/dist/controllers/sortable_controller.js.map +1 -1
  268. package/dist/controllers/spinner_controller.d.ts +39 -4
  269. package/dist/controllers/spinner_controller.js +288 -173
  270. package/dist/controllers/spinner_controller.js.map +1 -1
  271. package/dist/controllers/step_indicator_controller.d.ts +9 -4
  272. package/dist/controllers/step_indicator_controller.js +105 -46
  273. package/dist/controllers/step_indicator_controller.js.map +1 -1
  274. package/dist/controllers/stepper_controller.d.ts +14 -6
  275. package/dist/controllers/stepper_controller.js +120 -73
  276. package/dist/controllers/stepper_controller.js.map +1 -1
  277. package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
  278. package/dist/controllers/stick_to_bottom_controller.js +275 -140
  279. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  280. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  281. package/dist/controllers/sticky_observer_controller.js +220 -49
  282. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  283. package/dist/controllers/submit_once_controller.d.ts +24 -8
  284. package/dist/controllers/submit_once_controller.js +376 -227
  285. package/dist/controllers/submit_once_controller.js.map +1 -1
  286. package/dist/controllers/switch_controller.d.ts +18 -4
  287. package/dist/controllers/switch_controller.js +201 -84
  288. package/dist/controllers/switch_controller.js.map +1 -1
  289. package/dist/controllers/tabs_controller.d.ts +9 -6
  290. package/dist/controllers/tabs_controller.js +41 -30
  291. package/dist/controllers/tabs_controller.js.map +1 -1
  292. package/dist/controllers/tags_input_controller.d.ts +16 -3
  293. package/dist/controllers/tags_input_controller.js +293 -173
  294. package/dist/controllers/tags_input_controller.js.map +1 -1
  295. package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
  296. package/dist/controllers/textarea_autosize_controller.js +108 -74
  297. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  298. package/dist/controllers/theme_controller.d.ts +24 -11
  299. package/dist/controllers/theme_controller.js +140 -116
  300. package/dist/controllers/theme_controller.js.map +1 -1
  301. package/dist/controllers/time_picker_controller.d.ts +55 -5
  302. package/dist/controllers/time_picker_controller.js +331 -127
  303. package/dist/controllers/time_picker_controller.js.map +1 -1
  304. package/dist/controllers/toast_controller.d.ts +40 -16
  305. package/dist/controllers/toast_controller.js +244 -324
  306. package/dist/controllers/toast_controller.js.map +1 -1
  307. package/dist/controllers/toggle_group_controller.d.ts +14 -6
  308. package/dist/controllers/toggle_group_controller.js +252 -191
  309. package/dist/controllers/toggle_group_controller.js.map +1 -1
  310. package/dist/controllers/toolbar_controller.d.ts +8 -6
  311. package/dist/controllers/toolbar_controller.js +219 -120
  312. package/dist/controllers/toolbar_controller.js.map +1 -1
  313. package/dist/controllers/tooltip_controller.d.ts +49 -4
  314. package/dist/controllers/tooltip_controller.js +158 -124
  315. package/dist/controllers/tooltip_controller.js.map +1 -1
  316. package/dist/controllers/transition_controller.d.ts +22 -4
  317. package/dist/controllers/transition_controller.js +123 -119
  318. package/dist/controllers/transition_controller.js.map +1 -1
  319. package/dist/controllers/tree_view_controller.d.ts +22 -12
  320. package/dist/controllers/tree_view_controller.js +283 -369
  321. package/dist/controllers/tree_view_controller.js.map +1 -1
  322. package/dist/index.d.ts +10 -5
  323. package/dist/index.js +8573 -10410
  324. package/dist/index.js.map +1 -1
  325. package/dist/inspector/cli.d.ts +38 -14
  326. package/dist/inspector/cli.js +108 -40
  327. package/dist/inspector/cli.js.map +1 -1
  328. package/dist/inspector/cli_bin.js +145 -64
  329. package/dist/inspector/cli_bin.js.map +1 -1
  330. package/dist/inspector/examples.json +20 -20
  331. package/dist/inspector/manifest.json +802 -101
  332. package/dist/positioning/index.d.ts +15 -7
  333. package/dist/positioning/index.js +154 -47
  334. package/dist/positioning/index.js.map +1 -1
  335. package/package.json +11 -10
  336. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  337. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,121 +1,258 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/drawer_controller.ts
3
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
4
+ if (element.getAttribute(name) === value) return false;
5
+ if (value === null) element.removeAttribute(name);
6
+ else element.setAttribute(name, value);
7
+ return true;
8
+ }
4
9
 
5
- // src/utils/before_cache_reset.ts
6
- var BeforeCacheReset = class _BeforeCacheReset {
7
- /** Every subscribed instance, iterated by the one shared document listener. */
8
- static #subscribers = /* @__PURE__ */ new Set();
9
- /** The shared listener; installed while at least one instance is subscribed. */
10
- static #onBeforeCache = () => {
11
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
12
- };
13
- #rewind;
14
- /** @param rewind - the pass that returns this controller's state to its initial form. */
15
- constructor(rewind) {
16
- this.#rewind = rewind;
10
+ function sharedRegistry(key, create) {
11
+ const symbol = Symbol.for(key);
12
+ const scope = globalThis;
13
+ const existing = scope[symbol];
14
+ if (existing !== void 0) return existing;
15
+ const registry2 = create();
16
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
17
+ return registry2;
18
+ }
19
+
20
+ var registry = sharedRegistry(
21
+ "stimeo-ui.attribute-lease.registry.v1",
22
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
23
+ );
24
+ function isAttributeValue(value) {
25
+ return value === null || typeof value === "string";
26
+ }
27
+ function readLeaseRecord(element, record) {
28
+ try {
29
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
30
+ if (!Array.isArray(parsed)) return void 0;
31
+ const [original, written, ...rest] = parsed;
32
+ const linked = rest[0] !== false;
33
+ const beneath = linked ? rest : rest.slice(1);
34
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
35
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
36
+ } catch {
37
+ return void 0;
17
38
  }
18
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
19
- activate() {
20
- const first = _BeforeCacheReset.#subscribers.size === 0;
21
- _BeforeCacheReset.#subscribers.add(this);
22
- if (first) {
23
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
39
+ }
40
+ function attributeStack(element, attribute) {
41
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
42
+ registry.stacks.set(element, byAttribute);
43
+ const known = byAttribute.get(attribute);
44
+ if (known) return known;
45
+ const found = [];
46
+ for (const record of registry.names.get(attribute) ?? []) {
47
+ const read = readLeaseRecord(element, record);
48
+ if (read) found.push({ ...read, record });
49
+ }
50
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
51
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
52
+ lease: null,
53
+ record,
54
+ written,
55
+ original,
56
+ linked: linked && index > 0
57
+ }));
58
+ byAttribute.set(attribute, stack);
59
+ return stack;
60
+ }
61
+ function findHold(stack, lease, record) {
62
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
63
+ }
64
+ var AttributeLease = class {
65
+ #attribute;
66
+ #record;
67
+ #base;
68
+ #preserveAuthor;
69
+ #held = /* @__PURE__ */ new Set();
70
+ constructor(attribute, owner, options = {}) {
71
+ this.#attribute = attribute;
72
+ this.#record = `data-${owner}-${attribute}-lease`;
73
+ this.#base = options.base === true;
74
+ this.#preserveAuthor = options.preserveAuthor === true;
75
+ const names = registry.names;
76
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
77
+ }
78
+ write(element, value) {
79
+ const stack = attributeStack(element, this.#attribute);
80
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
81
+ this.#held.add(element);
82
+ const shows = !this.#covered(element, stack, hold);
83
+ hold.written = value;
84
+ this.#mark(element, stack);
85
+ if (shows) this.#reflect(element, value);
86
+ }
87
+ return(element) {
88
+ const held = this.#held.delete(element);
89
+ const known = registry.stacks.get(element)?.has(this.#attribute);
90
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
91
+ const hold = this.#take(stack);
92
+ if (!hold) return false;
93
+ const carries = element.getAttribute(this.#attribute) === hold.written;
94
+ const owns = carries && !this.#covered(element, stack, hold);
95
+ const index = stack.indexOf(hold);
96
+ stack.splice(index, 1);
97
+ const above = stack[index];
98
+ if (above) above.linked &&= hold.linked;
99
+ this.#mark(element, stack);
100
+ if (!owns) return true;
101
+ const below = hold.linked ? stack[index - 1] : void 0;
102
+ this.#reflect(element, below ? below.written : hold.original);
103
+ return true;
104
+ }
105
+ returnAll() {
106
+ for (const element of Array.from(this.#held)) this.return(element);
107
+ }
108
+ #take(stack) {
109
+ const hold = findHold(stack, this, this.#record);
110
+ if (hold) hold.lease = this;
111
+ return hold;
112
+ }
113
+ #acquire(element, stack) {
114
+ const current = element.getAttribute(this.#attribute);
115
+ const top = stack.at(-1);
116
+ const bottom = stack[0];
117
+ const over = top?.written === current ? top : void 0;
118
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
119
+ const hold = {
120
+ lease: this,
121
+ record: this.#record,
122
+ written: current,
123
+ original: from ? from.original : current,
124
+ linked: !this.#base && over !== void 0
125
+ };
126
+ if (this.#base && bottom) {
127
+ bottom.linked = true;
128
+ stack.unshift(hold);
129
+ } else stack.push(hold);
130
+ return hold;
131
+ }
132
+ #covered(element, stack, hold) {
133
+ const current = element.getAttribute(this.#attribute);
134
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
135
+ }
136
+ #mark(element, stack) {
137
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
138
+ stack.forEach((hold, index) => {
139
+ const names = stack.slice(0, index).map((under) => under.record);
140
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
141
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
142
+ records.set(
143
+ hold.record,
144
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
145
+ );
146
+ });
147
+ for (const [name, recorded] of records) {
148
+ setAttributeIfChanged(element, name, recorded);
24
149
  }
25
150
  }
26
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
27
- deactivate() {
28
- _BeforeCacheReset.#subscribers.delete(this);
29
- if (_BeforeCacheReset.#subscribers.size > 0) return;
30
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
151
+ #reflect(element, value) {
152
+ setAttributeIfChanged(element, this.#attribute, value);
153
+ }
154
+ };
155
+
156
+ var DetachGate = class _DetachGate {
157
+ #pending = false;
158
+ get pending() {
159
+ return this.#pending;
160
+ }
161
+ static isDetached(host) {
162
+ return !host.element.isConnected || !_DetachGate.#listed(host);
163
+ }
164
+ disconnected(host, teardown) {
165
+ const carried = _DetachGate.#carried(host);
166
+ if (!carried && _DetachGate.isDetached(host)) {
167
+ this.#pending = false;
168
+ teardown();
169
+ return;
170
+ }
171
+ this.#pending = true;
172
+ const probe = () => {
173
+ if (!this.#pending) return;
174
+ this.#pending = false;
175
+ teardown();
176
+ };
177
+ if (carried) setTimeout(probe, 0);
178
+ else queueMicrotask(probe);
179
+ }
180
+ static #carried(host) {
181
+ const element = host.element;
182
+ return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
183
+ }
184
+ static #listed(host) {
185
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
186
+ return tokens.includes(host.identifier);
187
+ }
188
+ cancel() {
189
+ this.#pending = false;
31
190
  }
32
191
  };
33
192
 
34
- // src/utils/escape_layer.ts
35
193
  var EscapeLayer = class _EscapeLayer {
36
- static #registries = /* @__PURE__ */ new WeakMap();
37
- #ownerDocument = null;
38
- /** Dismissal callback while active; `null` when inactive. */
39
- #onDismiss = null;
40
- /** Live predicate deciding whether the layer claims a press; `null` = always. */
41
- #claims = null;
42
- /**
43
- * Activates this layer at the top of its document's Escape stack, installing
44
- * the document's shared resolver listener if this is its first layer.
45
- * Re-activating an already-active layer moves it to the top.
46
- */
194
+ static #registries = sharedRegistry(
195
+ "stimeo-ui.escape-layer.registry.v1",
196
+ () => /* @__PURE__ */ new WeakMap()
197
+ );
198
+ #active = null;
47
199
  activate(ownerDocument = document, options) {
48
200
  this.deactivate();
49
- let registry = _EscapeLayer.#registries.get(ownerDocument);
50
- if (!registry) {
51
- registry = _EscapeLayer.#createRegistry();
52
- _EscapeLayer.#registries.set(ownerDocument, registry);
53
- ownerDocument.addEventListener("keydown", registry.onKeydown);
54
- }
55
- registry.stack.push(this);
56
- this.#ownerDocument = ownerDocument;
57
- this.#onDismiss = options.onDismiss;
58
- this.#claims = options.claims ?? null;
59
- }
60
- /**
61
- * Removes this layer from its document's Escape stack, uninstalling the
62
- * shared listener when the stack empties. Safe to call when inactive.
63
- */
201
+ let registry2 = _EscapeLayer.#registries.get(ownerDocument);
202
+ if (!registry2) {
203
+ registry2 = _EscapeLayer.#createRegistry();
204
+ _EscapeLayer.#registries.set(ownerDocument, registry2);
205
+ ownerDocument.addEventListener("keydown", registry2.onKeydown);
206
+ }
207
+ const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
208
+ registry2.stack.push(entry);
209
+ this.#active = { document: ownerDocument, entry };
210
+ }
64
211
  deactivate() {
65
- const ownerDocument = this.#ownerDocument;
66
- if (!ownerDocument) return;
67
- const registry = _EscapeLayer.#registries.get(ownerDocument);
68
- if (registry) {
69
- const index = registry.stack.lastIndexOf(this);
70
- if (index >= 0) registry.stack.splice(index, 1);
71
- if (registry.stack.length === 0) {
72
- ownerDocument.removeEventListener("keydown", registry.onKeydown);
73
- _EscapeLayer.#registries.delete(ownerDocument);
212
+ const active = this.#active;
213
+ if (!active) return;
214
+ const registry2 = _EscapeLayer.#registries.get(active.document);
215
+ if (registry2) {
216
+ const index = registry2.stack.lastIndexOf(active.entry);
217
+ if (index >= 0) registry2.stack.splice(index, 1);
218
+ if (registry2.stack.length === 0) {
219
+ active.document.removeEventListener("keydown", registry2.onKeydown);
220
+ _EscapeLayer.#registries.delete(active.document);
74
221
  }
75
222
  }
76
- this.#ownerDocument = null;
77
- this.#onDismiss = null;
78
- this.#claims = null;
223
+ this.#active = null;
79
224
  }
80
- /**
81
- * Whether this active layer would own a press right now: it is the topmost
82
- * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
83
- * and diagnostics — production dismissal goes through the shared listener.
84
- */
85
225
  get ownsEscape() {
86
- const ownerDocument = this.#ownerDocument;
87
- if (!ownerDocument) return false;
88
- const registry = _EscapeLayer.#registries.get(ownerDocument);
89
- if (!registry) return false;
90
- return _EscapeLayer.#resolveOwner(registry.stack) === this;
226
+ const active = this.#active;
227
+ if (!active) return false;
228
+ const registry2 = _EscapeLayer.#registries.get(active.document);
229
+ if (!registry2) return false;
230
+ return _EscapeLayer.#resolveOwner(registry2.stack) === active.entry;
91
231
  }
92
- /** Builds a document's registry with its shared resolver listener. */
93
232
  static #createRegistry() {
94
- const registry = {
233
+ const registry2 = {
95
234
  stack: [],
96
235
  onKeydown: (event) => {
97
236
  if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
98
- const owner = _EscapeLayer.#resolveOwner(registry.stack);
237
+ const owner = _EscapeLayer.#resolveOwner(registry2.stack);
99
238
  if (!owner) return;
100
239
  event.preventDefault();
101
- owner.#onDismiss?.();
240
+ owner.onDismiss();
102
241
  }
103
242
  };
104
- return registry;
243
+ return registry2;
105
244
  }
106
- /** The topmost stack layer whose claims predicate passes, or `null`. */
107
245
  static #resolveOwner(stack) {
108
246
  for (let index = stack.length - 1; index >= 0; index--) {
109
247
  const layer = stack[index];
110
248
  if (!layer) continue;
111
- if (layer.#claims && !layer.#claims()) continue;
249
+ if (layer.claims && !layer.claims()) continue;
112
250
  return layer;
113
251
  }
114
252
  return null;
115
253
  }
116
254
  };
117
255
 
118
- // src/utils/focus_candidate.ts
119
256
  function inheritsFieldsetDisabled(control) {
120
257
  let fieldset = control.closest("fieldset[disabled]");
121
258
  while (fieldset) {
@@ -125,8 +262,34 @@ function inheritsFieldsetDisabled(control) {
125
262
  }
126
263
  return false;
127
264
  }
265
+ function unslotted(element) {
266
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
267
+ }
268
+ function flatTreeParent(element) {
269
+ if (unslotted(element)) return null;
270
+ const parent = element.assignedSlot ?? element.parentNode;
271
+ if (parent instanceof ShadowRoot) return parent.host;
272
+ return parent instanceof Element ? parent : null;
273
+ }
274
+ function flatTreeContains(ancestor, element) {
275
+ for (let node = element; node; node = flatTreeParent(node)) {
276
+ if (node === ancestor) return true;
277
+ }
278
+ return false;
279
+ }
280
+ function closestInFlatTree(element, selector) {
281
+ for (let node = element; node; node = flatTreeParent(node)) {
282
+ if (node.matches(selector)) return node;
283
+ }
284
+ return null;
285
+ }
286
+ function flatTreeChildren(element) {
287
+ return ownedScope(element) ?? Array.from(element.children);
288
+ }
128
289
  function canTakeFocus(element) {
129
- if (element.closest("[hidden], [inert]")) return false;
290
+ for (let node = element; node; node = flatTreeParent(node)) {
291
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
292
+ }
130
293
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
131
294
  if (!("disabled" in element)) return true;
132
295
  if (element.disabled) return false;
@@ -183,189 +346,693 @@ function isTabStop(element) {
183
346
  if (tabindex !== null) return tabindex >= 0;
184
347
  return hasNativeTabStop(element) || hasEditableTabStop(element);
185
348
  }
186
- function tabStopsWithin(root) {
187
- return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
188
- isTabStop
349
+ function isRadio(element) {
350
+ return element instanceof HTMLInputElement && element.type === "radio";
351
+ }
352
+ function sameRadioGroup(a, b) {
353
+ return a.name !== "" && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
354
+ }
355
+ function isInEditableContent(element) {
356
+ for (let node = element.parentElement; node; node = node.parentElement) {
357
+ if (node.getAttribute("contenteditable")?.toLowerCase() === "false") return false;
358
+ if (hasEditableTabStop(node)) return true;
359
+ }
360
+ return false;
361
+ }
362
+ var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
363
+ function isSvgLink(element) {
364
+ return element instanceof SVGElement && element.localName === "a" && (element.hasAttribute("href") || element.hasAttributeNS(XLINK_NAMESPACE, "href"));
365
+ }
366
+ function hasRenderedImage(area) {
367
+ const map = area.closest("map");
368
+ const key = map?.name || map?.id;
369
+ if (!map || !key) return false;
370
+ const root = map.getRootNode();
371
+ return Array.from(root.querySelectorAll("img[usemap]")).some(
372
+ (image) => image.getAttribute("usemap") === `#${key}` && isRenderedForFocus(image)
189
373
  );
190
374
  }
375
+ function isFocusableInOrder(element) {
376
+ const tabindex = authoredTabindex(element);
377
+ return (tabindex === null || tabindex >= 0) && canTakeFocus(element);
378
+ }
379
+ function extraStop(element) {
380
+ if (element instanceof HTMLAreaElement) {
381
+ const focusable = element.hasAttribute("href") || authoredTabindex(element) !== null;
382
+ return focusable && isFocusableInOrder(element) && hasRenderedImage(element);
383
+ }
384
+ if (element instanceof HTMLObjectElement) {
385
+ return isFocusableInOrder(element) && isRenderedForFocus(element) && element.contentWindow !== null;
386
+ }
387
+ if (isSvgLink(element))
388
+ return isFocusableInOrder(element) && isRenderedForFocus(element);
389
+ return null;
390
+ }
391
+ function isSequentialStop(element) {
392
+ const stop = extraStop(element) ?? (element.matches(TAB_STOP_CANDIDATE_SELECTOR) && isTabStop(element));
393
+ if (!stop) return false;
394
+ const link = element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement || isSvgLink(element);
395
+ return !(link && authoredTabindex(element) === null && isInEditableContent(element));
396
+ }
397
+ function ownedScope(element) {
398
+ if (element.shadowRoot) return Array.from(element.shadowRoot.children);
399
+ if (element instanceof HTMLSlotElement) {
400
+ const assigned = element.assignedElements();
401
+ return assigned.length > 0 ? assigned : Array.from(element.children);
402
+ }
403
+ return null;
404
+ }
405
+ function orderScope(elements, walk) {
406
+ const entries = [];
407
+ const visit = (element) => {
408
+ if (element === walk.from) walk.fromAt = walk.tree.length;
409
+ const tabindex = authoredTabindex(element) ?? 0;
410
+ const stop = (element instanceof HTMLElement || element instanceof SVGElement) && isSequentialStop(element) ? element : null;
411
+ if (stop) walk.tree.push(stop);
412
+ const owned = ownedScope(element);
413
+ if (owned) {
414
+ if (tabindex >= 0) {
415
+ entries.push({ tabindex, stops: [...stop ? [stop] : [], ...orderScope(owned, walk)] });
416
+ }
417
+ return;
418
+ }
419
+ if (stop) entries.push({ tabindex, stops: [stop] });
420
+ for (const child of Array.from(element.children)) visit(child);
421
+ };
422
+ for (const element of elements) visit(element);
423
+ const positive = entries.filter((entry) => entry.tabindex > 0);
424
+ positive.sort((a, b) => a.tabindex - b.tabindex);
425
+ const rest = entries.filter((entry) => entry.tabindex === 0);
426
+ return [...positive, ...rest].flatMap((entry) => entry.stops);
427
+ }
428
+ function passesRadioRule(element, tree, focusedRadio) {
429
+ if (!isRadio(element) || element.checked) return true;
430
+ const group = tree.filter(
431
+ (stop) => stop === element || isRadio(stop) && sameRadioGroup(stop, element)
432
+ );
433
+ if (group.some((radio) => radio.checked)) return false;
434
+ if (group[0] !== element) return false;
435
+ return focusedRadio === null || !sameRadioGroup(focusedRadio, element);
436
+ }
437
+ function startIndex(order, walk, backward, isStop) {
438
+ if (walk.fromAt < 0) return backward ? order.length - 1 : 0;
439
+ const at = order.indexOf(walk.from);
440
+ if (at >= 0) return backward ? at - 1 : at + 1;
441
+ const neighbour = backward ? walk.tree.slice(0, walk.fromAt).reverse().find(isStop) : walk.tree.slice(walk.fromAt).find(isStop);
442
+ if (neighbour) return order.indexOf(neighbour);
443
+ return backward ? order.length - 1 : 0;
444
+ }
445
+ function sequentialTabStops(root, from = null, backward = false) {
446
+ const walk = { from, tree: [], fromAt: from === root ? 0 : -1 };
447
+ const order = orderScope(ownedScope(root) ?? Array.from(root.children), walk);
448
+ const focusedRadio = walk.fromAt >= 0 && isRadio(from) ? from : null;
449
+ const isStop = (element) => passesRadioRule(element, walk.tree, focusedRadio);
450
+ const start = startIndex(order, walk, backward, isStop);
451
+ const visited = backward ? [...order.slice(0, start + 1).reverse(), ...order.slice(start + 1).reverse()] : [...order.slice(start), ...order.slice(0, start)];
452
+ return visited.filter((element) => element !== from && isStop(element));
453
+ }
454
+ function deepActiveElement(ownerDocument) {
455
+ let active = ownerDocument.activeElement;
456
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
457
+ return active;
458
+ }
459
+
460
+ var MicrotaskCoalescer = class {
461
+ #run;
462
+ #queued = false;
463
+ #active = false;
464
+ #generation = 0;
465
+ constructor(run) {
466
+ this.#run = run;
467
+ }
468
+ activate() {
469
+ this.#active = true;
470
+ }
471
+ cancel() {
472
+ this.#active = false;
473
+ this.#queued = false;
474
+ this.#generation += 1;
475
+ }
476
+ get pending() {
477
+ return this.#queued;
478
+ }
479
+ schedule() {
480
+ if (!this.#active || this.#queued) return;
481
+ this.#queued = true;
482
+ const generation = this.#generation;
483
+ queueMicrotask(() => {
484
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
485
+ this.#queued = false;
486
+ this.#run();
487
+ });
488
+ }
489
+ };
490
+
491
+ function queryOpenDescendants(root, selector) {
492
+ const matches = [];
493
+ const roots = [root];
494
+ for (const scope of roots) {
495
+ matches.push(...scope.querySelectorAll(selector));
496
+ if (scope.nodeType === Node.ELEMENT_NODE) {
497
+ const shadow = scope.shadowRoot;
498
+ if (shadow) roots.push(shadow);
499
+ }
500
+ for (const element of scope.querySelectorAll("*")) {
501
+ if (element.shadowRoot) roots.push(element.shadowRoot);
502
+ }
503
+ }
504
+ return matches;
505
+ }
191
506
 
192
- // src/utils/focus_trap.ts
193
- var FocusTrap = class {
194
- /** The element focused before activation, restored on deactivation. */
195
- #previouslyFocused = null;
196
- /** The body's inline `overflow` before locking, restored on deactivation. */
197
- #previousBodyOverflow = "";
198
- /** Whether scroll was locked this activation (so it is only restored if applied). */
199
- #scrollLocked = false;
200
- /** Background siblings made `inert` while active, restored on deactivation. */
201
- #inertedSiblings = [];
202
- /** Whether the modal side effects are currently applied. */
507
+ function holdsFocus(element) {
508
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
509
+ }
510
+ var TabindexLoan = class _TabindexLoan {
511
+ static #registry = sharedRegistry(
512
+ "stimeo-ui.tabindex-loan.registry.v1",
513
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
514
+ );
515
+ #value;
516
+ #record;
517
+ #preserveFocus;
518
+ #lent = /* @__PURE__ */ new Set();
519
+ constructor(value, owner, options = {}) {
520
+ this.#value = value;
521
+ this.#record = `data-${owner}-tabindex-loan`;
522
+ this.#preserveFocus = options.preserveFocus === true;
523
+ _TabindexLoan.#registry.names.add(this.#record);
524
+ }
525
+ lend(element, options = {}) {
526
+ const stack = this.#stack(element);
527
+ const current = element.getAttribute("tabindex");
528
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
529
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
530
+ );
531
+ const linked = stack.at(-1)?.value === current;
532
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
533
+ if (!held && current !== null && !linked) return;
534
+ this.#lent.add(element);
535
+ if (held) held.loan = this;
536
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
537
+ this.#mark(element, stack);
538
+ if (current === null || !held && current !== this.#value) {
539
+ element.setAttribute("tabindex", this.#value);
540
+ }
541
+ }
542
+ return(element) {
543
+ const stack = this.#stack(element);
544
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
545
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
546
+ );
547
+ if (!hold) return false;
548
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
549
+ hold.loan = this;
550
+ this.#lent.add(element);
551
+ return false;
552
+ }
553
+ this.#lent.delete(element);
554
+ return true;
555
+ }
556
+ returnAll() {
557
+ for (const element of Array.from(this.#lent)) this.return(element);
558
+ }
559
+ reclaim(element) {
560
+ if (element.getAttribute(this.#record) !== this.#value) return;
561
+ const stack = this.#stack(element);
562
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
563
+ if (hold && !holdsFocus(element)) this.#release(element, stack, hold);
564
+ }
565
+ reclaimWithin(root) {
566
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
567
+ this.reclaim(element);
568
+ }
569
+ }
570
+ #stack(element) {
571
+ const stacks = _TabindexLoan.#registry.stacks;
572
+ const stack = stacks.get(element) ?? this.#seed(element);
573
+ stacks.set(element, stack);
574
+ return stack;
575
+ }
576
+ #seed(element) {
577
+ const current = element.getAttribute("tabindex");
578
+ const holds = [];
579
+ for (const record of _TabindexLoan.#registry.names) {
580
+ const value = element.getAttribute(record);
581
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
582
+ }
583
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
584
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
585
+ return holds;
586
+ }
587
+ #release(element, stack, hold, preserveFocus = false) {
588
+ const current = element.getAttribute("tabindex");
589
+ const index = stack.indexOf(hold);
590
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
591
+ if (owns && preserveFocus && holdsFocus(element)) return false;
592
+ stack.splice(index, 1);
593
+ const above = stack[index];
594
+ if (above) above.linked &&= hold.linked;
595
+ this.#mark(element, stack);
596
+ if (!owns) return true;
597
+ const below = hold.linked ? stack[index - 1] : void 0;
598
+ if (!below) element.removeAttribute("tabindex");
599
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
600
+ return true;
601
+ }
602
+ #mark(element, stack) {
603
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
604
+ for (const hold of stack) records.set(hold.record, hold.value);
605
+ for (const [name, recorded] of records) {
606
+ setAttributeIfChanged(element, name, recorded);
607
+ }
608
+ }
609
+ };
610
+
611
+ var FOCUS_TRAP_OWNER = "stimeo-focus-trap";
612
+ var INERT_MARK = `data-${FOCUS_TRAP_OWNER}-inert`;
613
+ var OVERFLOW_MARK = `data-${FOCUS_TRAP_OWNER}-overflow`;
614
+ var FocusTrap = class _FocusTrap {
615
+ static #registries = sharedRegistry(
616
+ "stimeo-ui.focus-trap.registry.v2",
617
+ () => /* @__PURE__ */ new WeakMap()
618
+ );
619
+ static #TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
620
+ "text",
621
+ "search",
622
+ "email",
623
+ "url",
624
+ "tel",
625
+ "password",
626
+ "number"
627
+ ]);
628
+ #openers = [];
629
+ #entry;
630
+ #tabindexLoans = /* @__PURE__ */ new Map();
203
631
  #activeState = false;
204
- /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
205
632
  #escapeLayer = new EscapeLayer();
206
- /** Returns the trapped element; called on every operation for the live target. */
633
+ #gate = new DetachGate();
207
634
  #getContainer;
208
- /** Closing/focus hooks; see {@link FocusTrapOptions}. */
209
635
  #options;
210
- /**
211
- * @param getContainer - Returns the trapped element. Called on every operation
212
- * so the live target is always used.
213
- * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
214
- */
215
636
  constructor(getContainer, options = {}) {
216
637
  this.#getContainer = getContainer;
217
638
  this.#options = options;
639
+ this.#entry = {
640
+ container: () => this.#readContainer(),
641
+ moveFocus: (backward) => this.#moveFocus(backward),
642
+ refocus: () => this.#refocus(),
643
+ isolated: false,
644
+ lockedBody: null,
645
+ openedFrom: null,
646
+ handedBack: []
647
+ };
218
648
  }
219
- /** Whether the trap is currently active. */
220
649
  get active() {
221
650
  return this.#activeState;
222
651
  }
223
- /**
224
- * Applies the trap: records the current focus, optionally locks background scroll
225
- * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
226
- * `autoFocus` is off) moves focus inside. No-ops if already active.
227
- */
228
652
  activate() {
229
653
  if (this.#activeState) return;
654
+ _FocusTrap.#sweep();
230
655
  this.#activeState = true;
231
- const active = document.activeElement;
232
- this.#previouslyFocused = active instanceof HTMLElement && active !== document.body ? active : null;
233
- if (this.#flag(this.#options.lockScroll, true)) {
234
- this.#previousBodyOverflow = document.body.style.overflow;
235
- document.body.style.overflow = "hidden";
236
- this.#scrollLocked = true;
237
- }
238
- if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
239
- document.addEventListener("keydown", this.#onKeydown);
240
- this.#beforeCache.activate();
656
+ this.#openers = _FocusTrap.#focusPath();
657
+ this.#entry.openedFrom = _FocusTrap.#trapHolding(this.#openers[0] ?? null);
658
+ if (this.#flag(this.#options.lockScroll, true)) this.#lockScroll();
659
+ this.#entry.isolated = this.#flag(this.#options.isolate, true);
660
+ this.#joinStack();
661
+ _FocusTrap.#reconcileBackground(document);
241
662
  const onEscape = this.#options.onEscape;
242
663
  if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
243
664
  if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
244
665
  }
245
- /**
246
- * Reverts every side effect applied by {@link activate}. No-ops if inactive, so
247
- * a controller can call it defensively from both `close()` and `disconnect()`.
248
- *
249
- * @param restoreFocus - Move focus back to the opener (default `true`). Pass
250
- * `false` on teardown (`disconnect`), where yanking focus is undesirable.
251
- */
252
666
  deactivate({ restoreFocus = true } = {}) {
253
- if (!this.#activeState) return;
667
+ if (!this.#activeState) {
668
+ this.#returnTabindexes();
669
+ return;
670
+ }
254
671
  this.#activeState = false;
255
672
  this.#escapeLayer.deactivate();
256
- document.removeEventListener("keydown", this.#onKeydown);
257
- this.#beforeCache.deactivate();
258
- if (this.#scrollLocked) {
259
- document.body.style.overflow = this.#previousBodyOverflow;
260
- this.#scrollLocked = false;
261
- }
262
- this.#releaseBackground();
263
- if (restoreFocus) {
264
- const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;
265
- target?.focus();
266
- }
673
+ const above = this.#leaveStack();
674
+ this.#unlockScroll();
675
+ _FocusTrap.#reconcileBackground(document);
676
+ const wayBack = this.#handOver(above);
677
+ if (restoreFocus) this.#restore(wayBack, above);
678
+ this.#returnTabindexes();
679
+ }
680
+ connect() {
681
+ this.#gate.cancel();
682
+ _FocusTrap.#sweep();
683
+ new TabindexLoan("-1", FOCUS_TRAP_OWNER).reclaimWithin(document);
684
+ }
685
+ disconnect(host) {
686
+ if (this.#activeState) this.deactivate({ restoreFocus: false });
687
+ this.#gate.disconnected(host, () => this.#returnTabindexes());
688
+ }
689
+ refreshIsolation() {
690
+ if (!this.#activeState) return;
691
+ const isolate = this.#flag(this.#options.isolate, true);
692
+ if (isolate === this.#entry.isolated) return;
693
+ this.#entry.isolated = isolate;
694
+ const stranded = _FocusTrap.#reconcileBackground(document);
695
+ const active = deepActiveElement(document);
696
+ if (stranded.some((element) => flatTreeContains(element, active))) this.#focusInitial();
697
+ }
698
+ refreshContainer() {
699
+ if (!this.#activeState) return;
700
+ _FocusTrap.#reconcileBackground(document);
701
+ this.#returnTabindexes();
702
+ if (this.#ownsTab()) this.#refocus();
267
703
  }
268
- /** Resolves a boolean-or-getter option, defaulting when it was not provided. */
269
704
  #flag(option, fallback) {
270
705
  if (option === void 0) return fallback;
271
706
  return typeof option === "function" ? option() : option;
272
707
  }
273
- /**
274
- * Reverts the side effects just before Turbo caches the page snapshot, so an
275
- * overlay left open does not bake the scroll lock into `body[style]` — a
276
- * restored page would feed that locked value back into {@link activate} as the
277
- * baseline, and closing would then never unlock the page. Markup state stays
278
- * untouched (restore-open designs reopen against a clean baseline), and focus
279
- * is left alone mid-navigation. The listener lives only while active.
280
- */
281
- #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
282
- /**
283
- * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
284
- * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
285
- * which layer currently owns Escape.
286
- */
287
- #onKeydown = (event) => {
288
- if (event.key === "Tab") this.#trapTab(event);
289
- };
290
- /** Keeps `Tab` focus cycling within the container's focusable elements. */
291
- #trapTab(event) {
292
- const focusable = this.#focusableElements();
293
- if (focusable.length === 0) {
294
- event.preventDefault();
295
- return;
708
+ #joinStack() {
709
+ const registry2 = _FocusTrap.#registry(document);
710
+ if (registry2.stack.length === 0) {
711
+ document.addEventListener("keydown", registry2.onKeydown);
712
+ document.addEventListener("turbo:morph", registry2.onMorph);
713
+ document.addEventListener("turbo:morph-element", registry2.onMorph);
296
714
  }
297
- const first = focusable[0];
298
- const last = focusable[focusable.length - 1];
299
- const active = document.activeElement;
300
- if (!(active instanceof Node) || !this.#getContainer().contains(active)) {
301
- event.preventDefault();
302
- first?.focus();
303
- return;
715
+ registry2.stack.push(this.#entry);
716
+ }
717
+ #leaveStack() {
718
+ const registry2 = _FocusTrap.#registry(document);
719
+ const index = registry2.stack.indexOf(this.#entry);
720
+ const above = registry2.stack.slice(index + 1);
721
+ registry2.stack.splice(index, 1);
722
+ if (registry2.stack.length === 0) {
723
+ document.removeEventListener("keydown", registry2.onKeydown);
724
+ document.removeEventListener("turbo:morph", registry2.onMorph);
725
+ document.removeEventListener("turbo:morph-element", registry2.onMorph);
304
726
  }
305
- if (event.shiftKey && active === first) {
306
- event.preventDefault();
307
- last?.focus();
308
- } else if (!event.shiftKey && active === last) {
309
- event.preventDefault();
310
- first?.focus();
311
- }
312
- }
313
- /**
314
- * Marks every element outside the container's subtree as `inert` so background
315
- * content cannot be focused or reached by assistive technology, honoring the
316
- * `aria-modal="true"` contract. An element that was *already* `inert` is left
317
- * untracked so `#releaseBackground` does not wrongly clear it.
318
- *
319
- * The walk climbs from the container to `body` and inerts each ancestor's other
320
- * children. Scanning only `body`'s children would skip the branch the container
321
- * sits in — everything beside it inside that branch is background too, and a
322
- * nested container is the ordinary case.
323
- */
324
- #isolateBackground() {
325
- const container = this.#getContainer();
326
- this.#inertedSiblings = [];
327
- for (let node = container; node !== document.body; ) {
328
- const parent = node.parentElement;
329
- if (!parent) break;
330
- for (const sibling of Array.from(parent.children)) {
331
- if (!(sibling instanceof HTMLElement)) continue;
332
- if (sibling === node || sibling.inert) continue;
333
- sibling.inert = true;
334
- this.#inertedSiblings.push(sibling);
727
+ return above;
728
+ }
729
+ static #focusPath() {
730
+ const path = [];
731
+ for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
732
+ if (active instanceof HTMLElement && active !== document.body) path.unshift(active);
733
+ }
734
+ return path;
735
+ }
736
+ static #trapHolding(element) {
737
+ for (const trap of [..._FocusTrap.#registry(document).stack].reverse()) {
738
+ const container = trap.container();
739
+ if (container && flatTreeContains(container, element)) return trap;
740
+ }
741
+ return null;
742
+ }
743
+ #handOver(above) {
744
+ const entry = this.#entry;
745
+ const wayBack = [
746
+ ...this.#openers,
747
+ this.#options.fallbackFocus?.() ?? null,
748
+ ...entry.handedBack
749
+ ].filter((element) => element !== null);
750
+ for (const trap of above) {
751
+ if (trap.openedFrom !== entry) continue;
752
+ trap.handedBack = [...trap.handedBack, ...wayBack];
753
+ trap.openedFrom = entry.openedFrom;
754
+ }
755
+ this.#openers = [];
756
+ entry.openedFrom = null;
757
+ entry.handedBack = [];
758
+ return wayBack;
759
+ }
760
+ #restore(wayBack, above) {
761
+ const active = deepActiveElement(document);
762
+ const released = this.#readContainer();
763
+ const holds = (trap) => {
764
+ const container = trap.container();
765
+ if (!container || !_FocusTrap.#takesTab(trap) || !flatTreeContains(container, active)) {
766
+ return false;
335
767
  }
336
- node = parent;
768
+ return !released || !flatTreeContains(released, active) || flatTreeContains(released, container);
769
+ };
770
+ if (above.some(holds)) return;
771
+ for (const element of wayBack) {
772
+ if (this.#activeState) return;
773
+ if (!element.isConnected || !canTakeFocus(element) || !isRenderedForFocus(element)) continue;
774
+ const before = deepActiveElement(document);
775
+ element.focus();
776
+ const after = deepActiveElement(document);
777
+ if (after === element || after !== before) return;
337
778
  }
779
+ _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack)?.refocus();
780
+ }
781
+ #refocus() {
782
+ if (!this.#flag(this.#options.autoFocus, true)) return;
783
+ if (flatTreeContains(this.#getContainer(), deepActiveElement(document))) return;
784
+ this.#focusInitial();
785
+ }
786
+ #lendTabindex(container) {
787
+ const loan = this.#tabindexLoans.get(container) ?? new TabindexLoan("-1", FOCUS_TRAP_OWNER);
788
+ loan.lend(container);
789
+ this.#tabindexLoans.set(container, loan);
338
790
  }
339
- /** Reverts the `inert` flags applied by `#isolateBackground`. */
340
- #releaseBackground() {
341
- for (const sibling of this.#inertedSiblings) {
342
- sibling.inert = false;
791
+ #returnTabindexes() {
792
+ const focused = deepActiveElement(document);
793
+ for (const [element, loan] of [...this.#tabindexLoans]) {
794
+ if (element === focused) continue;
795
+ loan.returnAll();
796
+ this.#tabindexLoans.delete(element);
797
+ }
798
+ }
799
+ #lockScroll() {
800
+ const registry2 = _FocusTrap.#registry(document);
801
+ const body = document.body;
802
+ if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
803
+ registry2.scrollLocks.add(this.#entry);
804
+ this.#entry.lockedBody = body;
805
+ body.style.overflow = "hidden";
806
+ }
807
+ #unlockScroll() {
808
+ const registry2 = _FocusTrap.#registry(document);
809
+ const body = this.#entry.lockedBody;
810
+ this.#entry.lockedBody = null;
811
+ if (!registry2.scrollLocks.delete(this.#entry) || !body) return;
812
+ if (!_FocusTrap.#isLocked(registry2, body)) _FocusTrap.#unlock(body);
813
+ }
814
+ static #isLocked(registry2, body) {
815
+ return [...registry2.scrollLocks].some((trap) => trap.lockedBody === body);
816
+ }
817
+ static #unlock(body) {
818
+ const saved = body.getAttribute(OVERFLOW_MARK);
819
+ if (saved === null) return;
820
+ body.removeAttribute(OVERFLOW_MARK);
821
+ body.style.overflow = saved;
822
+ }
823
+ static #sweep() {
824
+ const registry2 = _FocusTrap.#registry(document);
825
+ for (const element of queryOpenDescendants(document, `[${INERT_MARK}]`)) {
826
+ if (registry2.inerted.has(element)) continue;
827
+ element.removeAttribute(INERT_MARK);
828
+ element.inert = false;
829
+ }
830
+ if (!_FocusTrap.#isLocked(registry2, document.body)) _FocusTrap.#unlock(document.body);
831
+ }
832
+ static #registry(ownerDocument) {
833
+ const existing = _FocusTrap.#registries.get(ownerDocument);
834
+ if (existing) return existing;
835
+ const morphs = new MicrotaskCoalescer(() => _FocusTrap.#reapply(registry2));
836
+ morphs.activate();
837
+ const registry2 = {
838
+ stack: [],
839
+ onKeydown: (event) => _FocusTrap.#resolveTab(registry2.stack, event),
840
+ onMorph: () => morphs.schedule(),
841
+ inerted: /* @__PURE__ */ new Set(),
842
+ scrollLocks: /* @__PURE__ */ new Set()
843
+ };
844
+ _FocusTrap.#registries.set(ownerDocument, registry2);
845
+ return registry2;
846
+ }
847
+ static #reapply(registry2) {
848
+ _FocusTrap.#reconcileBackground(document);
849
+ const body = document.body;
850
+ if (!_FocusTrap.#isLocked(registry2, body)) return;
851
+ if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
852
+ body.style.overflow = "hidden";
853
+ }
854
+ static #reconcileBackground(ownerDocument) {
855
+ const registry2 = _FocusTrap.#registry(ownerDocument);
856
+ for (const element of registry2.inerted) {
857
+ if (!element.hasAttribute(INERT_MARK)) registry2.inerted.delete(element);
858
+ }
859
+ const background = _FocusTrap.#backgroundOf(registry2);
860
+ for (const element of registry2.inerted) {
861
+ if (background.has(element)) continue;
862
+ element.removeAttribute(INERT_MARK);
863
+ element.inert = false;
864
+ registry2.inerted.delete(element);
865
+ }
866
+ const added = [];
867
+ for (const element of background) {
868
+ if (registry2.inerted.has(element)) continue;
869
+ element.setAttribute(INERT_MARK, "");
870
+ element.inert = true;
871
+ registry2.inerted.add(element);
872
+ added.push(element);
873
+ }
874
+ return added;
875
+ }
876
+ static #backgroundOf(registry2) {
877
+ const containers = registry2.stack.map((trap) => trap.container());
878
+ let top = -1;
879
+ registry2.stack.forEach((trap, index) => {
880
+ if (trap.isolated && containers[index]) top = index;
881
+ });
882
+ const background = /* @__PURE__ */ new Set();
883
+ if (top < 0) return background;
884
+ const kept = /* @__PURE__ */ new Set();
885
+ const keep = (path) => {
886
+ for (const node of path) {
887
+ kept.add(node);
888
+ background.delete(node);
889
+ }
890
+ for (const node of path) {
891
+ const parent = flatTreeParent(node);
892
+ for (const sibling of parent ? flatTreeChildren(parent) : []) {
893
+ if (!(sibling instanceof HTMLElement) || kept.has(sibling)) continue;
894
+ if (sibling.inert && !registry2.inerted.has(sibling)) continue;
895
+ background.add(sibling);
896
+ }
897
+ }
898
+ };
899
+ containers.slice(top).forEach((container, index) => {
900
+ const path = [];
901
+ for (let node = container; node && node !== document.body && !kept.has(node); ) {
902
+ path.push(node);
903
+ node = flatTreeParent(node);
904
+ }
905
+ if (index === 0 || path.some((node) => background.has(node))) keep(path);
906
+ });
907
+ return background;
908
+ }
909
+ #readContainer() {
910
+ try {
911
+ return this.#getContainer();
912
+ } catch {
913
+ return null;
914
+ }
915
+ }
916
+ static #resolveTab(stack, event) {
917
+ if (event.key !== "Tab" || event.defaultPrevented) return;
918
+ if (event.isComposing || event.keyCode === 229) return;
919
+ if (event.ctrlKey || event.metaKey) return;
920
+ const owner = _FocusTrap.#tabOwner(stack);
921
+ if (!owner) return;
922
+ event.preventDefault();
923
+ owner.moveFocus(event.shiftKey);
924
+ }
925
+ static #tabOwner(stack) {
926
+ for (const trap of [...stack].reverse()) {
927
+ if (_FocusTrap.#takesTab(trap)) return trap;
928
+ }
929
+ return null;
930
+ }
931
+ static #takesTab(trap) {
932
+ const container = trap.container();
933
+ if (!container) return false;
934
+ return container.isConnected && !closestInFlatTree(container, "[hidden]") && isRenderedForFocus(container);
935
+ }
936
+ #ownsTab() {
937
+ return _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack) === this.#entry;
938
+ }
939
+ #moveFocus(backward) {
940
+ const from = deepActiveElement(document);
941
+ for (const stop of sequentialTabStops(this.#getContainer(), from, backward)) {
942
+ if (!this.#ownsTab()) return;
943
+ const before = deepActiveElement(document);
944
+ stop.focus();
945
+ const after = deepActiveElement(document);
946
+ if (after === before) continue;
947
+ if (after === stop) _FocusTrap.#selectOnEntry(stop);
948
+ return;
949
+ }
950
+ }
951
+ static #selectOnEntry(stop) {
952
+ if (stop instanceof HTMLInputElement && _FocusTrap.#TEXT_ENTRY_TYPES.has(stop.type)) {
953
+ stop.select();
343
954
  }
344
- this.#inertedSiblings = [];
345
955
  }
346
- /** Moves focus to the initial target, the first focusable, or the container. */
347
956
  #focusInitial() {
348
957
  const preferred = this.#options.initialFocus?.();
349
958
  if (preferred) {
350
959
  preferred.focus();
351
960
  return;
352
961
  }
353
- const focusable = this.#focusableElements();
354
- if (focusable[0]) {
355
- focusable[0].focus();
962
+ const container = this.#getContainer();
963
+ const [first] = sequentialTabStops(container);
964
+ if (first) {
965
+ first.focus();
356
966
  return;
357
967
  }
358
- const container = this.#getContainer();
359
- container.tabIndex = -1;
968
+ this.#lendTabindex(container);
360
969
  container.focus();
361
970
  }
362
- /** Collects the container's currently focusable descendants in DOM order. */
363
- #focusableElements() {
364
- return tabStopsWithin(this.#getContainer());
971
+ };
972
+
973
+ var ListenerSet = class {
974
+ #abort = new AbortController();
975
+ add(target, type, handler, options) {
976
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
977
+ }
978
+ dispose() {
979
+ this.#abort.abort();
980
+ this.#abort = new AbortController();
981
+ }
982
+ };
983
+
984
+ var MorphRenderWatcher = class {
985
+ #listeners = new ListenerSet();
986
+ #pass;
987
+ #element = null;
988
+ #onMorph = () => this.#pass.schedule();
989
+ constructor(run) {
990
+ this.#pass = new MicrotaskCoalescer(run);
991
+ }
992
+ observe(element) {
993
+ if (this.#element === element) return;
994
+ this.disconnect();
995
+ this.#element = element;
996
+ this.#pass.activate();
997
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
998
+ }
999
+ schedule() {
1000
+ this.#pass.schedule();
1001
+ }
1002
+ get pending() {
1003
+ return this.#pass.pending;
1004
+ }
1005
+ disconnect() {
1006
+ this.#listeners.dispose();
1007
+ this.#pass.cancel();
1008
+ this.#element = null;
1009
+ }
1010
+ };
1011
+
1012
+ var LivedMark = class {
1013
+ #name;
1014
+ #lived = false;
1015
+ #element = null;
1016
+ #morph = new MorphRenderWatcher(() => this.#write());
1017
+ constructor(identifier) {
1018
+ this.#name = `data-${identifier}-lived`;
1019
+ }
1020
+ connect(element) {
1021
+ const connection = this.#lived ? "reconnect" : element.hasAttribute(this.#name) ? "restored" : "fresh";
1022
+ this.#lived = true;
1023
+ this.#element = element;
1024
+ this.#write();
1025
+ this.#morph.observe(element);
1026
+ return connection;
1027
+ }
1028
+ disconnect() {
1029
+ this.#morph.disconnect();
1030
+ }
1031
+ #write() {
1032
+ this.#element?.setAttribute(this.#name, "");
365
1033
  }
366
1034
  };
367
1035
 
368
- // src/utils/state_reason.ts
369
1036
  var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
370
1037
  var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
371
1038
  function stateReasonFor(event) {
@@ -375,43 +1042,24 @@ function stateReasonFor(event) {
375
1042
  return "user";
376
1043
  }
377
1044
 
378
- // src/utils/safe_timeout.ts
379
1045
  var TimerRegistry = class {
380
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
381
1046
  ids = /* @__PURE__ */ new Set();
382
- /**
383
- * Cancels a single tracked timer.
384
- *
385
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
386
- * registry), so callers can clear defensively without guarding.
387
- */
388
1047
  clear(id) {
389
1048
  if (this.ids.delete(id)) {
390
1049
  this.cancel(id);
391
1050
  }
392
1051
  }
393
- /**
394
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
395
- * to guarantee no timer outlives the element.
396
- */
397
1052
  clearAll() {
398
1053
  for (const id of this.ids) {
399
1054
  this.cancel(id);
400
1055
  }
401
1056
  this.ids.clear();
402
1057
  }
403
- /** Number of timers currently tracked (pending). */
404
1058
  get size() {
405
1059
  return this.ids.size;
406
1060
  }
407
1061
  };
408
1062
  var SafeTimeout = class extends TimerRegistry {
409
- /**
410
- * Schedules `callback` after `delay` ms and returns the timer id.
411
- *
412
- * The id is removed from the registry automatically when the timeout fires,
413
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
414
- */
415
1063
  set(callback, delay) {
416
1064
  const id = this.schedule(() => {
417
1065
  this.ids.delete(id);
@@ -428,7 +1076,6 @@ var SafeTimeout = class extends TimerRegistry {
428
1076
  }
429
1077
  };
430
1078
 
431
- // src/utils/transition_completion.ts
432
1079
  function timeMs(value) {
433
1080
  const trimmed = value.trim();
434
1081
  const amount = Number.parseFloat(trimmed);
@@ -464,13 +1111,6 @@ var TransitionCompletion = class {
464
1111
  #complete = null;
465
1112
  #pendingProperties = null;
466
1113
  #deadline = 0;
467
- /**
468
- * Replaces any prior wait and invokes `complete` synchronously for a 0ms
469
- * transition (including when `getComputedStyle` is unavailable).
470
- *
471
- * With a positive `options.timeoutMs` the synchronous fast-path is skipped and
472
- * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
473
- */
474
1114
  wait(element, complete, options = {}) {
475
1115
  this.cancel();
476
1116
  const requested = options.timeoutMs ?? 0;
@@ -490,18 +1130,10 @@ var TransitionCompletion = class {
490
1130
  element.addEventListener("transitioncancel", this.#onTerminal);
491
1131
  this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
492
1132
  }
493
- /** Cancels the pending wait without invoking its completion callback. */
494
1133
  cancel() {
495
1134
  this.#complete = null;
496
1135
  this.#teardown();
497
1136
  }
498
- /**
499
- * Handles terminal events from the observed element only.
500
- *
501
- * For explicit property lists, every declared positive-time property must
502
- * settle. For `all`, no reliable property set exists, so an event can finish
503
- * only after the computed maximum time; the safety timeout owns the usual path.
504
- */
505
1137
  #onTerminal = (event) => {
506
1138
  if (event.target !== this.#element) return;
507
1139
  const transitionEvent = event;
@@ -521,10 +1153,6 @@ var TransitionCompletion = class {
521
1153
  }
522
1154
  if (Date.now() >= this.#deadline) this.#finish();
523
1155
  };
524
- /**
525
- * Returns active CSS transition properties expanded to the names reported by
526
- * terminal events. CSS animations and pseudo-element effects are excluded.
527
- */
528
1156
  #activeTransitionProperties(element) {
529
1157
  if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
530
1158
  try {
@@ -542,7 +1170,6 @@ var TransitionCompletion = class {
542
1170
  return /* @__PURE__ */ new Set();
543
1171
  }
544
1172
  }
545
- /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
546
1173
  #explicitPendingProperties(timings) {
547
1174
  if (timings.some(({ property }) => property === "all")) return null;
548
1175
  const pending = new Set(
@@ -550,7 +1177,6 @@ var TransitionCompletion = class {
550
1177
  );
551
1178
  return pending.size > 0 ? pending : null;
552
1179
  }
553
- /** Completes exactly once, releasing listeners and the fallback before the callback. */
554
1180
  #finish() {
555
1181
  const complete = this.#complete;
556
1182
  if (!complete) return;
@@ -558,7 +1184,6 @@ var TransitionCompletion = class {
558
1184
  this.#teardown();
559
1185
  complete();
560
1186
  }
561
- /** Releases the exact element listeners and timer owned by the current wait. */
562
1187
  #teardown() {
563
1188
  this.#timers.clearAll();
564
1189
  this.#element?.removeEventListener("transitionend", this.#onTerminal);
@@ -569,7 +1194,6 @@ var TransitionCompletion = class {
569
1194
  }
570
1195
  };
571
1196
 
572
- // src/controllers/drawer_controller.ts
573
1197
  var DrawerController = class extends Controller {
574
1198
  static targets = ["trigger", "overlay", "panel"];
575
1199
  static values = {
@@ -578,83 +1202,95 @@ var DrawerController = class extends Controller {
578
1202
  };
579
1203
  static actions = ["close", "closeOnBackdrop", "open"];
580
1204
  static events = ["close", "open"];
581
- /** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
582
1205
  #activePanel = null;
583
- /** Owns the modal side effects; Escape closes, focus falls back to the trigger. */
1206
+ #openState = false;
584
1207
  #trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
585
1208
  onEscape: () => this.#close("escape"),
586
1209
  fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
587
1210
  });
588
- /** Owns the cancellable close-transition wait and its bounded fallback. */
589
1211
  #transition = new TransitionCompletion();
590
- /** Distinguishes dynamic target churn from callbacks around controller teardown. */
591
1212
  #connected = false;
592
- /** Whether state moves are reported: set once `connect()` settled the baseline. */
593
1213
  #reporting = false;
594
- /**
595
- * Reflects placement and establishes the initial open/closed state.
596
- *
597
- * The DOM is the source of truth on reconnect (Turbo cache restore / morph): a
598
- * restored snapshot whose panel is already `data-state="open"` stays open
599
- * rather than being re-derived from the declarative `open` Value (which would
600
- * close a user-opened drawer). The `open` Value only seeds a genuinely fresh
601
- * render. We normalize to a clean closed baseline first so
602
- * {@link DrawerController.open | open} runs its full reveal + trap activation —
603
- * the `FocusTrap` is inactive after a disconnect and must be re-activated.
604
- */
1214
+ #lived = new LivedMark(this.identifier);
1215
+ #morphRender = new MorphRenderWatcher(() => this.#repair());
1216
+ #stateLease = new AttributeLease("data-state", this.identifier);
1217
+ #hiddenLease = new AttributeLease("hidden", this.identifier);
1218
+ #placementLease = new AttributeLease("data-placement", this.identifier);
605
1219
  connect() {
1220
+ const restored = this.#lived.connect(this.element) === "restored";
1221
+ this.#trap.connect();
1222
+ this.#morphRender.observe(this.element);
606
1223
  this.#connected = true;
607
1224
  this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
608
1225
  this.#reflectPlacement();
609
- const shouldOpen = this.#isOpen || this.openValue;
1226
+ const shouldOpen = !restored && (this.#isOpen || this.openValue);
610
1227
  this.#applyClosedState();
1228
+ if (restored) this.openValue = false;
611
1229
  if (shouldOpen) this.#open("api");
612
1230
  this.#reporting = true;
613
1231
  }
614
- /** Reverts the modal side effects and pending hide if torn down while open. */
615
1232
  disconnect() {
1233
+ this.#lived.disconnect();
1234
+ this.#morphRender.disconnect();
616
1235
  this.#connected = false;
617
1236
  this.#reporting = false;
618
1237
  this.#transition.cancel();
619
- this.#trap.deactivate({ restoreFocus: false });
1238
+ this.#trap.disconnect(this);
620
1239
  this.#activePanel = null;
621
1240
  }
622
- /** Adopts a panel target added by a Turbo morph after the controller connected. */
623
1241
  panelTargetConnected(panel) {
624
- if (!this.#connected || this.#activePanel?.isConnected && this.#activePanel !== panel) return;
1242
+ if (!this.#connected || this.#activePanel?.isConnected) return;
625
1243
  this.#adoptPanel(panel);
626
1244
  }
627
- /** Closes and releases modal side effects when the actively trapped panel disappears. */
628
1245
  panelTargetDisconnected(panel) {
629
- if (panel !== this.#activePanel) return;
1246
+ const stays = this.panelTargets.includes(panel);
1247
+ if (!stays) {
1248
+ this.#stateLease.return(panel);
1249
+ this.#hiddenLease.return(panel);
1250
+ this.#placementLease.return(panel);
1251
+ }
1252
+ if (panel !== this.#activePanel || stays && this.panelTarget === panel) return;
630
1253
  this.#transition.cancel();
631
1254
  this.#activePanel = null;
632
- if (!this.#connected) return;
633
- panel.setAttribute("data-state", "closed");
634
- panel.hidden = true;
635
- if (this.hasOverlayTarget) {
636
- this.overlayTarget.setAttribute("data-state", "closed");
637
- this.overlayTarget.hidden = true;
1255
+ if (!this.#connected) {
1256
+ this.#trap.deactivate({ restoreFocus: false });
1257
+ return;
638
1258
  }
1259
+ this.#openState = false;
1260
+ if (stays) {
1261
+ this.#writeState(panel, "closed");
1262
+ this.#writeHidden(panel, true);
1263
+ }
1264
+ this.#writeOverlayState("closed");
1265
+ this.#writeOverlayHidden(true);
639
1266
  this.openValue = false;
640
- this.#trap.deactivate();
641
1267
  if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);
1268
+ else this.#trap.deactivate();
1269
+ }
1270
+ overlayTargetConnected() {
1271
+ this.#morphRender.schedule();
1272
+ }
1273
+ overlayTargetDisconnected(overlay) {
1274
+ if (!this.overlayTargets.includes(overlay)) {
1275
+ this.#stateLease.return(overlay);
1276
+ this.#hiddenLease.return(overlay);
1277
+ }
1278
+ this.#morphRender.schedule();
642
1279
  }
643
- /** Keeps `data-placement` in sync if the value changes at runtime. */
644
1280
  placementValueChanged() {
645
- this.#reflectPlacement();
1281
+ if (this.#connected) this.#repair();
1282
+ else this.#reflectPlacement();
646
1283
  }
647
- /** Opens the drawer: reveals it, syncs `data-state`, traps focus. */
648
1284
  open(event) {
1285
+ if (!this.#connected) return;
649
1286
  this.#open(stateReasonFor(event));
650
1287
  }
651
- /** Reveals the drawer, syncs `data-state`, reports a move, then traps focus. */
652
1288
  #open(reason) {
653
1289
  if (!this.hasPanelTarget || this.#isOpen) return;
654
1290
  this.#transition.cancel();
655
1291
  this.#activePanel = this.panelTarget;
656
- this.panelTarget.hidden = false;
657
- if (this.hasOverlayTarget) this.overlayTarget.hidden = false;
1292
+ this.#writeHidden(this.panelTarget, false);
1293
+ this.#writeOverlayHidden(false);
658
1294
  void this.panelTarget.offsetWidth;
659
1295
  this.#setState("open");
660
1296
  this.openValue = true;
@@ -662,21 +1298,14 @@ var DrawerController = class extends Controller {
662
1298
  if (!this.#isOpen) return;
663
1299
  this.#trap.activate();
664
1300
  }
665
- /**
666
- * Closes the drawer: syncs `data-state` to start the exit transition, then
667
- * defers both `hidden` *and* the modal teardown (scroll lock / background
668
- * `inert` / focus restore) until the transition finishes. This keeps the
669
- * background inert and focus trapped while the drawer is still visually on
670
- * screen, preserving the modal contract during the exit animation.
671
- */
672
1301
  close(event) {
1302
+ if (!this.#connected) return;
673
1303
  this.#close(stateReasonFor(event));
674
1304
  }
675
- /** Closes only when the overlay itself (not its contents) is clicked. */
676
1305
  closeOnBackdrop(event) {
1306
+ if (!this.#connected) return;
677
1307
  if (this.hasOverlayTarget && event.target === this.overlayTarget) this.#close("outside");
678
1308
  }
679
- /** Starts the exit transition, reports a move, then defers `hidden` and teardown. */
680
1309
  #close(reason) {
681
1310
  if (!this.hasPanelTarget || !this.#isOpen) return;
682
1311
  this.openValue = false;
@@ -685,89 +1314,79 @@ var DrawerController = class extends Controller {
685
1314
  if (this.#isOpen) return;
686
1315
  this.#hideAfterTransition();
687
1316
  }
688
- /**
689
- * Writes `data-placement` from the current `placement` value.
690
- *
691
- * @stimeoRenderRoot
692
- */
693
1317
  #reflectPlacement() {
694
- if (this.hasPanelTarget) this.panelTarget.setAttribute("data-placement", this.#placement);
1318
+ if (this.hasPanelTarget) this.#placementLease.write(this.panelTarget, this.#placement);
695
1319
  }
696
- /**
697
- * Reconciles a replacement panel and companion overlay from its explicit DOM state.
698
- *
699
- * @stimeoRenderRoot
700
- */
701
1320
  #adoptPanel(panel) {
702
1321
  this.#transition.cancel();
703
- const trapWasActive = this.#trap.active;
704
1322
  this.#activePanel = panel;
705
- panel.setAttribute("data-placement", this.#placement);
1323
+ this.#placementLease.write(panel, this.#placement);
706
1324
  if (panel.getAttribute("data-state") === "open") {
707
- panel.hidden = false;
708
- if (this.hasOverlayTarget) {
709
- this.overlayTarget.setAttribute("data-state", "open");
710
- this.overlayTarget.hidden = false;
711
- }
1325
+ this.#openState = true;
1326
+ this.#writeHidden(panel, false);
1327
+ this.#writeOverlayState("open");
1328
+ this.#writeOverlayHidden(false);
712
1329
  this.openValue = true;
713
- if (trapWasActive) this.#trap.deactivate({ restoreFocus: false });
714
- this.#trap.activate();
1330
+ if (this.#trap.active) this.#trap.refreshContainer();
1331
+ else this.#trap.activate();
715
1332
  return;
716
1333
  }
717
- panel.setAttribute("data-state", "closed");
718
- panel.hidden = true;
719
- if (this.hasOverlayTarget) {
720
- this.overlayTarget.setAttribute("data-state", "closed");
721
- this.overlayTarget.hidden = true;
722
- }
1334
+ this.#openState = false;
1335
+ this.#writeState(panel, "closed");
1336
+ this.#writeHidden(panel, true);
1337
+ this.#writeOverlayState("closed");
1338
+ this.#writeOverlayHidden(true);
723
1339
  this.openValue = false;
724
1340
  this.#trap.deactivate();
725
1341
  }
726
- /** Validated placement (`left`/`right`/`top`/`bottom`), defaulting to `right`. */
1342
+ #repair() {
1343
+ this.#reflectPlacement();
1344
+ if (!this.hasPanelTarget) return;
1345
+ this.#setState(this.#openState ? "open" : "closed");
1346
+ const hidden = !this.#openState && !this.#trap.active;
1347
+ this.#writeHidden(this.panelTarget, hidden);
1348
+ this.#writeOverlayHidden(hidden);
1349
+ }
727
1350
  get #placement() {
728
1351
  const value = this.placementValue;
729
1352
  return value === "left" || value === "top" || value === "bottom" ? value : "right";
730
1353
  }
731
- /** Syncs `data-state` on the panel and overlay together. */
732
1354
  #setState(state) {
733
- if (this.hasPanelTarget) this.panelTarget.setAttribute("data-state", state);
734
- if (this.hasOverlayTarget) this.overlayTarget.setAttribute("data-state", state);
1355
+ this.#openState = state === "open";
1356
+ if (this.hasPanelTarget) this.#writeState(this.panelTarget, state);
1357
+ this.#writeOverlayState(state);
1358
+ }
1359
+ #writeState(element, state) {
1360
+ this.#stateLease.write(element, state);
1361
+ }
1362
+ #writeHidden(element, hidden) {
1363
+ this.#hiddenLease.write(element, hidden ? "" : null);
1364
+ }
1365
+ #writeOverlayState(state) {
1366
+ if (this.hasOverlayTarget) this.#writeState(this.overlayTarget, state);
1367
+ }
1368
+ #writeOverlayHidden(hidden) {
1369
+ if (this.hasOverlayTarget) this.#writeHidden(this.overlayTarget, hidden);
735
1370
  }
736
- /** Fully reflects the closed state up front (used on connect when not open). */
737
1371
  #applyClosedState() {
738
1372
  this.#setState("closed");
739
- if (this.hasPanelTarget) this.panelTarget.hidden = true;
740
- if (this.hasOverlayTarget) this.overlayTarget.hidden = true;
741
- }
742
- /**
743
- * Applies `hidden` once the panel's close transition ends, so the exit slide
744
- * can play. The shared waiter hides synchronously for 0ms transitions and
745
- * supplies a bounded fallback when the browser emits no terminal event.
746
- */
1373
+ if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, true);
1374
+ this.#writeOverlayHidden(true);
1375
+ }
747
1376
  #hideAfterTransition() {
748
1377
  const panel = this.panelTarget;
749
1378
  this.#activePanel = panel;
750
1379
  this.#transition.wait(panel, () => this.#applyHidden(panel));
751
1380
  }
752
- /**
753
- * Runs once the close transition has finished: applies `hidden` to the panel
754
- * and overlay, then reverts the modal side effects (scroll lock, background
755
- * `inert`, keydown listener) and restores focus to the opener. Deferring the
756
- * `FocusTrap` teardown to here — rather than at
757
- * {@link DrawerController.close | close} time — keeps the background unreachable
758
- * and focus trapped for the whole exit animation.
759
- */
760
1381
  #applyHidden(panel) {
761
- panel.hidden = true;
762
- if (this.hasOverlayTarget) this.overlayTarget.hidden = true;
1382
+ this.#writeHidden(panel, true);
1383
+ this.#writeOverlayHidden(true);
763
1384
  this.#trap.deactivate();
764
1385
  }
765
- /** Whether the drawer is open (tracked via `data-state`, not `hidden`). */
766
1386
  get #isOpen() {
767
1387
  return this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
768
1388
  }
769
1389
  };
770
1390
 
771
1391
  export { DrawerController };
772
- //# sourceMappingURL=drawer_controller.js.map
773
1392
  //# sourceMappingURL=drawer_controller.js.map