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/sidebar_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,713 @@ 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)
373
+ );
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)
189
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;
190
458
  }
191
459
 
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. */
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
+ }
506
+
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;
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);
790
+ }
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);
337
797
  }
338
798
  }
339
- /** Reverts the `inert` flags applied by `#isolateBackground`. */
340
- #releaseBackground() {
341
- for (const sibling of this.#inertedSiblings) {
342
- sibling.inert = false;
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 NUMBER_BOUNDS = {
1013
+ nonNegative: { finite: true, min: 0 }};
1014
+ function matchesNumberBounds(value, bounds) {
1015
+ if (!Number.isFinite(value)) {
1016
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
1017
+ if (direction === null) return false;
1018
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
1019
+ }
1020
+ if (bounds.min !== void 0 && value < bounds.min) return false;
1021
+ if (bounds.max !== void 0 && value > bounds.max) return false;
1022
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
1023
+ if (bounds.integer && !Number.isInteger(value)) return false;
1024
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
1025
+ return true;
1026
+ }
1027
+
1028
+ function readNumber(raw, fallback, bounds) {
1029
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
1030
+ }
1031
+
1032
+ var NumberValueReader = class {
1033
+ #lastRejected = /* @__PURE__ */ new Map();
1034
+ read(owner, name, raw, fallback, bounds) {
1035
+ const resolved = readNumber(raw, fallback, bounds);
1036
+ if (matchesNumberBounds(raw, bounds)) {
1037
+ this.#lastRejected.delete(name);
1038
+ return resolved;
1039
+ }
1040
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
1041
+ const literal = owner.element.getAttribute(attribute);
1042
+ if (literal === null) {
1043
+ this.#lastRejected.delete(name);
1044
+ return resolved;
1045
+ }
1046
+ if (this.#lastRejected.get(name) !== literal) {
1047
+ this.#lastRejected.set(name, literal);
1048
+ console.warn(
1049
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
1050
+ );
1051
+ }
1052
+ return resolved;
365
1053
  }
366
1054
  };
367
1055
 
368
- // src/utils/safe_storage.ts
369
1056
  function readLocalStorage(key) {
370
1057
  try {
371
1058
  return { ok: true, value: window.localStorage.getItem(key) };
@@ -382,7 +1069,6 @@ function writeLocalStorage(key, value) {
382
1069
  }
383
1070
  }
384
1071
 
385
- // src/utils/state_reason.ts
386
1072
  var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
387
1073
  var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
388
1074
  function stateReasonFor(event) {
@@ -392,43 +1078,24 @@ function stateReasonFor(event) {
392
1078
  return "user";
393
1079
  }
394
1080
 
395
- // src/utils/safe_timeout.ts
396
1081
  var TimerRegistry = class {
397
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
398
1082
  ids = /* @__PURE__ */ new Set();
399
- /**
400
- * Cancels a single tracked timer.
401
- *
402
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
403
- * registry), so callers can clear defensively without guarding.
404
- */
405
1083
  clear(id) {
406
1084
  if (this.ids.delete(id)) {
407
1085
  this.cancel(id);
408
1086
  }
409
1087
  }
410
- /**
411
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
412
- * to guarantee no timer outlives the element.
413
- */
414
1088
  clearAll() {
415
1089
  for (const id of this.ids) {
416
1090
  this.cancel(id);
417
1091
  }
418
1092
  this.ids.clear();
419
1093
  }
420
- /** Number of timers currently tracked (pending). */
421
1094
  get size() {
422
1095
  return this.ids.size;
423
1096
  }
424
1097
  };
425
1098
  var SafeTimeout = class extends TimerRegistry {
426
- /**
427
- * Schedules `callback` after `delay` ms and returns the timer id.
428
- *
429
- * The id is removed from the registry automatically when the timeout fires,
430
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
431
- */
432
1099
  set(callback, delay) {
433
1100
  const id = this.schedule(() => {
434
1101
  this.ids.delete(id);
@@ -445,7 +1112,6 @@ var SafeTimeout = class extends TimerRegistry {
445
1112
  }
446
1113
  };
447
1114
 
448
- // src/utils/transition_completion.ts
449
1115
  function timeMs(value) {
450
1116
  const trimmed = value.trim();
451
1117
  const amount = Number.parseFloat(trimmed);
@@ -481,13 +1147,6 @@ var TransitionCompletion = class {
481
1147
  #complete = null;
482
1148
  #pendingProperties = null;
483
1149
  #deadline = 0;
484
- /**
485
- * Replaces any prior wait and invokes `complete` synchronously for a 0ms
486
- * transition (including when `getComputedStyle` is unavailable).
487
- *
488
- * With a positive `options.timeoutMs` the synchronous fast-path is skipped and
489
- * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
490
- */
491
1150
  wait(element, complete, options = {}) {
492
1151
  this.cancel();
493
1152
  const requested = options.timeoutMs ?? 0;
@@ -507,18 +1166,10 @@ var TransitionCompletion = class {
507
1166
  element.addEventListener("transitioncancel", this.#onTerminal);
508
1167
  this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
509
1168
  }
510
- /** Cancels the pending wait without invoking its completion callback. */
511
1169
  cancel() {
512
1170
  this.#complete = null;
513
1171
  this.#teardown();
514
1172
  }
515
- /**
516
- * Handles terminal events from the observed element only.
517
- *
518
- * For explicit property lists, every declared positive-time property must
519
- * settle. For `all`, no reliable property set exists, so an event can finish
520
- * only after the computed maximum time; the safety timeout owns the usual path.
521
- */
522
1173
  #onTerminal = (event) => {
523
1174
  if (event.target !== this.#element) return;
524
1175
  const transitionEvent = event;
@@ -538,10 +1189,6 @@ var TransitionCompletion = class {
538
1189
  }
539
1190
  if (Date.now() >= this.#deadline) this.#finish();
540
1191
  };
541
- /**
542
- * Returns active CSS transition properties expanded to the names reported by
543
- * terminal events. CSS animations and pseudo-element effects are excluded.
544
- */
545
1192
  #activeTransitionProperties(element) {
546
1193
  if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
547
1194
  try {
@@ -559,7 +1206,6 @@ var TransitionCompletion = class {
559
1206
  return /* @__PURE__ */ new Set();
560
1207
  }
561
1208
  }
562
- /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
563
1209
  #explicitPendingProperties(timings) {
564
1210
  if (timings.some(({ property }) => property === "all")) return null;
565
1211
  const pending = new Set(
@@ -567,7 +1213,6 @@ var TransitionCompletion = class {
567
1213
  );
568
1214
  return pending.size > 0 ? pending : null;
569
1215
  }
570
- /** Completes exactly once, releasing listeners and the fallback before the callback. */
571
1216
  #finish() {
572
1217
  const complete = this.#complete;
573
1218
  if (!complete) return;
@@ -575,7 +1220,6 @@ var TransitionCompletion = class {
575
1220
  this.#teardown();
576
1221
  complete();
577
1222
  }
578
- /** Releases the exact element listeners and timer owned by the current wait. */
579
1223
  #teardown() {
580
1224
  this.#timers.clearAll();
581
1225
  this.#element?.removeEventListener("transitionend", this.#onTerminal);
@@ -586,111 +1230,138 @@ var TransitionCompletion = class {
586
1230
  }
587
1231
  };
588
1232
 
589
- // src/controllers/sidebar_controller.ts
590
- var SidebarController = class extends Controller {
1233
+ var SidebarController = class _SidebarController extends Controller {
1234
+ #numbers = new NumberValueReader();
591
1235
  static targets = ["trigger", "panel", "backdrop"];
592
1236
  static values = {
593
1237
  breakpoint: { type: Number, default: 768 },
594
1238
  key: { type: String, default: "" },
595
1239
  collapsed: { type: Boolean, default: false }
596
1240
  };
1241
+ static valueConstraints = {
1242
+ breakpoint: NUMBER_BOUNDS.nonNegative
1243
+ };
597
1244
  static actions = ["close", "open", "toggle"];
598
1245
  static events = ["close", "open", "reconcile"];
599
- /** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
600
1246
  #activePanel = null;
601
- /** Owns the overlay modal side effects; Escape closes, focus falls to trigger. */
602
1247
  #trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
603
1248
  onEscape: () => this.#closeOverlay("escape"),
604
1249
  fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
605
1250
  });
606
- /** Current responsive mode. */
607
1251
  #mode = "inline";
608
- /** Persisted inline preference: whether the rail is collapsed. */
609
1252
  #collapsed = false;
610
- /** The matched media query (`min-width: breakpoint`), watched for mode changes. */
611
1253
  #mql = null;
612
- /** Exact normalized query currently represented by the active media-query listener. */
1254
+ #mediaListeners = new ListenerSet();
613
1255
  #mqlQuery = null;
614
- /** Owns the cancellable close-transition wait and its bounded fallback. */
615
1256
  #transition = new TransitionCompletion();
616
- /** Distinguishes dynamic target churn from callbacks around controller teardown. */
617
1257
  #connected = false;
618
- /** Whether state moves are reported: set once `connect()` settled the baseline. */
619
- #reporting = false;
1258
+ #storageKeyRead = null;
1259
+ #expanded = false;
1260
+ #backdrop = {
1261
+ state: "closed",
1262
+ hidden: true
1263
+ };
1264
+ #expandedLease = new AttributeLease("aria-expanded", this.identifier);
1265
+ #stateLease = new AttributeLease("data-state", this.identifier);
1266
+ #hiddenLease = new AttributeLease("hidden", this.identifier);
1267
+ #modeLease = new AttributeLease("data-mode", this.identifier);
1268
+ #morphRender = new MorphRenderWatcher(() => this.#repair());
1269
+ #lived = false;
620
1270
  connect() {
1271
+ this.#trap.connect();
621
1272
  this.#connected = true;
622
- this.#beforeCache.activate();
623
1273
  this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
1274
+ this.#storageKeyRead = this.#storageKey;
624
1275
  this.#collapsed = this.#restoreCollapsed();
625
1276
  this.#mqlQuery = this.#breakpointQuery;
626
1277
  this.#mql = this.#matchBreakpoint(this.#mqlQuery);
627
- this.#mql?.addEventListener("change", this.#onMediaChange);
628
- this.#applyMode(this.#computeMode(), false);
629
- this.#reporting = true;
1278
+ if (this.#mql) this.#mediaListeners.add(this.#mql, "change", this.#onMediaChange);
1279
+ const mode = this.#computeMode();
1280
+ const resume = this.#lived && mode === "overlay" && this.#isOverlayOpen;
1281
+ this.#lived = true;
1282
+ if (resume) this.#trap.activate();
1283
+ else this.#applyMode(mode, false);
1284
+ this.#morphRender.observe(this.element);
630
1285
  }
631
1286
  disconnect() {
632
1287
  this.#connected = false;
633
- this.#reporting = false;
634
- this.#beforeCache.deactivate();
635
- this.#mql?.removeEventListener("change", this.#onMediaChange);
1288
+ this.#morphRender.disconnect();
1289
+ this.#mediaListeners.dispose();
636
1290
  this.#mql = null;
637
1291
  this.#mqlQuery = null;
638
1292
  this.#transition.cancel();
639
- this.#trap.deactivate({ restoreFocus: false });
1293
+ this.#trap.disconnect(this);
640
1294
  this.#activePanel = null;
641
1295
  }
642
- /** Adopts a panel target added by a Turbo morph after the controller connected. */
643
1296
  panelTargetConnected(panel) {
644
- if (!this.#connected || this.#activePanel?.isConnected && this.#activePanel !== panel) return;
1297
+ if (!this.#connected || this.#activePanel?.isConnected) return;
645
1298
  this.#adoptPanel(panel);
646
1299
  }
647
- /** Closes and releases overlay side effects when the actively trapped panel disappears. */
648
1300
  panelTargetDisconnected(panel) {
649
- if (panel !== this.#activePanel) return;
1301
+ const stays = this.panelTargets.includes(panel);
1302
+ if (!stays) {
1303
+ this.#modeLease.return(panel);
1304
+ this.#stateLease.return(panel);
1305
+ this.#hiddenLease.return(panel);
1306
+ }
1307
+ if (panel !== this.#activePanel || stays && this.panelTarget === panel) return;
650
1308
  this.#transition.cancel();
651
1309
  this.#activePanel = null;
652
- if (!this.#connected) return;
653
- panel.setAttribute("data-state", "closed");
654
- panel.hidden = true;
1310
+ if (stays) {
1311
+ this.#writeState(panel, "closed");
1312
+ this.#writeHidden(panel, true);
1313
+ }
655
1314
  this.#hideBackdrop();
656
1315
  this.#setExpandedAttr(false);
657
- this.#trap.deactivate();
1316
+ if (!this.#connected) {
1317
+ this.#trap.deactivate({ restoreFocus: false });
1318
+ return;
1319
+ }
658
1320
  if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);
1321
+ else this.#trap.deactivate();
1322
+ }
1323
+ triggerTargetConnected() {
1324
+ if (this.#connected) this.#reflectExpanded();
1325
+ }
1326
+ triggerTargetDisconnected(trigger) {
1327
+ if (!this.triggerTargets.includes(trigger)) this.#expandedLease.return(trigger);
1328
+ if (this.#connected) this.#reflectExpanded();
1329
+ }
1330
+ backdropTargetConnected() {
1331
+ if (this.#connected) this.#reflectBackdrop();
1332
+ }
1333
+ backdropTargetDisconnected(backdrop) {
1334
+ if (!this.backdropTargets.includes(backdrop)) {
1335
+ this.#stateLease.return(backdrop);
1336
+ this.#hiddenLease.return(backdrop);
1337
+ }
1338
+ if (this.#connected) this.#reflectBackdrop();
659
1339
  }
660
- /**
661
- * Rebinds responsive observation when `breakpoint` changes at runtime.
662
- *
663
- * Stimulus calls value callbacks before `connect()`, so the connected guard
664
- * prevents an eager subscription. Equivalent normalized queries retain the
665
- * existing listener instead of allocating duplicate `MediaQueryList` objects.
666
- */
667
1340
  breakpointValueChanged() {
668
1341
  if (!this.#connected) return;
669
1342
  const query = this.#breakpointQuery;
670
1343
  if (this.#mqlQuery === query) return;
671
- this.#mql?.removeEventListener("change", this.#onMediaChange);
1344
+ this.#mediaListeners.dispose();
672
1345
  this.#mqlQuery = query;
673
1346
  this.#mql = this.#matchBreakpoint(query);
674
- this.#mql?.addEventListener("change", this.#onMediaChange);
1347
+ if (this.#mql) this.#mediaListeners.add(this.#mql, "change", this.#onMediaChange);
675
1348
  const next = this.#computeMode();
676
1349
  if (next !== this.#mode) this.#applyMode(next, true);
677
1350
  }
678
- /**
679
- * Sanitizes overlay markup before Turbo snapshots the page.
680
- *
681
- * Inline state is durable and remains untouched. Overlay state is transient:
682
- * pending transitions are cancelled, controller-owned open attributes are
683
- * closed immediately, and modal side effects are released without moving
684
- * focus during navigation.
685
- */
686
- #beforeCache = new BeforeCacheReset(() => {
687
- if (!this.#connected || !this.#isOverlay) return;
688
- this.#transition.cancel();
689
- this.#trap.deactivate({ restoreFocus: false });
690
- this.#setOverlayClosedImmediate();
691
- });
692
- /** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */
1351
+ keyValueChanged() {
1352
+ if (!this.#connected) return;
1353
+ const key = this.#storageKey;
1354
+ if (key === this.#storageKeyRead) return;
1355
+ this.#storageKeyRead = key;
1356
+ const stored = this.#readStoredCollapsed(key);
1357
+ if (stored === null || stored === this.#collapsed) return;
1358
+ this.#collapsed = stored;
1359
+ if (this.#isOverlay) return;
1360
+ this.#applyInlineState(stored);
1361
+ this.#reportReconcile();
1362
+ }
693
1363
  toggle(event) {
1364
+ if (!this.#connected) return;
694
1365
  const reason = stateReasonFor(event);
695
1366
  if (this.#isOverlay) {
696
1367
  this.#isOverlayOpen ? this.#closeOverlay(reason) : this.#openOverlay(reason);
@@ -698,83 +1369,78 @@ var SidebarController = class extends Controller {
698
1369
  this.#setCollapsed(!this.#collapsed, reason);
699
1370
  }
700
1371
  }
701
- /** Shows the panel (inline: expand; overlay: open). */
702
1372
  open(event) {
1373
+ if (!this.#connected) return;
703
1374
  const reason = stateReasonFor(event);
704
1375
  if (this.#isOverlay) this.#openOverlay(reason);
705
1376
  else this.#setCollapsed(false, reason);
706
1377
  }
707
- /** Hides the panel (inline: collapse; overlay: close). */
708
1378
  close(event) {
1379
+ if (!this.#connected) return;
709
1380
  const reason = this.#closeReason(event);
710
1381
  if (this.#isOverlay) this.#closeOverlay(reason);
711
1382
  else this.#setCollapsed(true, reason);
712
1383
  }
713
- /**
714
- * Reads why a close happened. The backdrop has no action of its own — the
715
- * consumer wires this one onto it — so the shared `"outside"` vocabulary is
716
- * only reachable by recognising that target here.
717
- */
718
1384
  #closeReason(event) {
719
1385
  if (this.hasBackdropTarget && event?.currentTarget === this.backdropTarget) return "outside";
720
1386
  return stateReasonFor(event);
721
1387
  }
722
- // --- Mode handling ---------------------------------------------------------
723
- /**
724
- * Re-renders the closed/default state for `mode` and records it.
725
- *
726
- * @param report - Whether the mode change is one the consumer can observe: a
727
- * viewport change re-derives the state under them, the connect-time baseline
728
- * does not.
729
- */
730
1388
  #applyMode(mode, report) {
731
1389
  this.#mode = mode;
732
- if (this.hasPanelTarget) this.panelTarget.setAttribute("data-mode", mode);
1390
+ if (this.hasPanelTarget) this.#modeLease.write(this.panelTarget, mode);
733
1391
  if (mode === "inline") {
734
1392
  this.#transition.cancel();
735
- this.#trap.deactivate({ restoreFocus: false });
736
- if (this.hasPanelTarget) this.panelTarget.hidden = false;
1393
+ if (report) this.#trap.deactivate({ restoreFocus: false });
1394
+ if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, false);
737
1395
  this.#hideBackdrop();
738
1396
  this.#applyInlineState(this.#collapsed);
739
1397
  } else {
740
- this.#transition.cancel();
741
- this.#trap.deactivate({ restoreFocus: false });
742
1398
  this.#setOverlayClosedImmediate();
743
1399
  }
744
- if (report && this.#reporting) {
745
- this.dispatch("reconcile", {
746
- detail: { mode, open: this.#isExpanded },
747
- cancelable: false
748
- });
1400
+ if (report) this.#reportReconcile();
1401
+ }
1402
+ #repair() {
1403
+ if (!this.hasPanelTarget) return;
1404
+ if (this.#mode === "inline") {
1405
+ this.#applyMode("inline", false);
1406
+ return;
749
1407
  }
1408
+ const panel = this.panelTarget;
1409
+ this.#modeLease.write(panel, "overlay");
1410
+ this.#setOverlayState(this.#expanded ? "open" : "closed");
1411
+ const hidden = !this.#expanded && !this.#trap.active;
1412
+ this.#writeHidden(panel, hidden);
1413
+ this.#writeBackdropHidden(hidden);
1414
+ this.#setExpandedAttr(this.#expanded);
1415
+ }
1416
+ #reportReconcile() {
1417
+ this.dispatch("reconcile", {
1418
+ detail: { mode: this.#mode, open: this.#isExpanded },
1419
+ cancelable: false
1420
+ });
750
1421
  }
751
- /** Reconciles a replacement panel with the current responsive mode and DOM state. */
752
1422
  #adoptPanel(panel) {
753
1423
  this.#transition.cancel();
754
- const trapWasActive = this.#trap.active;
755
1424
  this.#activePanel = panel;
756
- panel.setAttribute("data-mode", this.#mode);
1425
+ this.#modeLease.write(panel, this.#mode);
757
1426
  if (this.#mode === "inline") {
758
- panel.hidden = false;
759
- panel.setAttribute("data-state", this.#collapsed ? "collapsed" : "expanded");
1427
+ this.#writeHidden(panel, false);
1428
+ this.#writeState(panel, this.#collapsed ? "collapsed" : "expanded");
760
1429
  this.#hideBackdrop();
761
1430
  this.#setExpandedAttr(!this.#collapsed);
762
- this.#trap.deactivate({ restoreFocus: false });
763
1431
  return;
764
1432
  }
765
1433
  if (panel.getAttribute("data-state") === "open") {
766
- panel.hidden = false;
767
- if (this.hasBackdropTarget) {
768
- this.backdropTarget.setAttribute("data-state", "open");
769
- this.backdropTarget.hidden = false;
770
- }
1434
+ this.#writeHidden(panel, false);
1435
+ this.#writeBackdropState("open");
1436
+ this.#writeBackdropHidden(false);
771
1437
  this.#setExpandedAttr(true);
772
- if (trapWasActive) this.#trap.deactivate({ restoreFocus: false });
773
- this.#trap.activate();
1438
+ if (this.#trap.active) this.#trap.refreshContainer();
1439
+ else this.#trap.activate();
774
1440
  return;
775
1441
  }
776
- panel.setAttribute("data-state", "closed");
777
- panel.hidden = true;
1442
+ this.#writeState(panel, "closed");
1443
+ this.#writeHidden(panel, true);
778
1444
  this.#hideBackdrop();
779
1445
  this.#setExpandedAttr(false);
780
1446
  this.#trap.deactivate();
@@ -790,8 +1456,6 @@ var SidebarController = class extends Controller {
790
1456
  if (typeof window.matchMedia !== "function") return null;
791
1457
  return window.matchMedia(query);
792
1458
  }
793
- // --- Inline (rail) ---------------------------------------------------------
794
- /** Sets, reflects, and persists the inline collapsed preference. */
795
1459
  #setCollapsed(collapsed, reason) {
796
1460
  if (collapsed === this.#collapsed) return;
797
1461
  this.#collapsed = collapsed;
@@ -799,21 +1463,18 @@ var SidebarController = class extends Controller {
799
1463
  this.#report(!collapsed, reason);
800
1464
  this.#persistCollapsed(collapsed);
801
1465
  }
802
- /** Reflects the inline rail state onto the panel and trigger. */
803
1466
  #applyInlineState(collapsed) {
804
1467
  if (this.hasPanelTarget) {
805
- this.panelTarget.setAttribute("data-state", collapsed ? "collapsed" : "expanded");
1468
+ this.#writeState(this.panelTarget, collapsed ? "collapsed" : "expanded");
806
1469
  }
807
1470
  this.#setExpandedAttr(!collapsed);
808
1471
  }
809
- // --- Overlay (off-canvas modal) -------------------------------------------
810
- /** Opens the overlay: reveal it, commit a starting frame, then trap focus. */
811
1472
  #openOverlay(reason) {
812
1473
  if (!this.hasPanelTarget || this.#isOverlayOpen) return;
813
1474
  this.#transition.cancel();
814
1475
  this.#activePanel = this.panelTarget;
815
- this.panelTarget.hidden = false;
816
- if (this.hasBackdropTarget) this.backdropTarget.hidden = false;
1476
+ this.#writeHidden(this.panelTarget, false);
1477
+ this.#writeBackdropHidden(false);
817
1478
  void this.panelTarget.offsetWidth;
818
1479
  this.#setOverlayState("open");
819
1480
  this.#setExpandedAttr(true);
@@ -821,7 +1482,6 @@ var SidebarController = class extends Controller {
821
1482
  if (!this.#isOverlayOpen) return;
822
1483
  this.#trap.activate();
823
1484
  }
824
- /** Closes the overlay: start the exit transition, defer hide + trap teardown. */
825
1485
  #closeOverlay(reason) {
826
1486
  if (!this.hasPanelTarget || !this.#isOverlayOpen) return;
827
1487
  this.#setOverlayState("closed");
@@ -830,93 +1490,90 @@ var SidebarController = class extends Controller {
830
1490
  if (this.#isOverlayOpen) return;
831
1491
  this.#hideAfterTransition();
832
1492
  }
833
- /** Renders the overlay closed state up front (used when entering overlay mode). */
834
1493
  #setOverlayClosedImmediate() {
835
1494
  this.#setOverlayState("closed");
836
1495
  this.#setExpandedAttr(false);
837
- if (this.hasPanelTarget) this.panelTarget.hidden = true;
1496
+ if (this.hasPanelTarget) this.#writeHidden(this.panelTarget, true);
838
1497
  this.#hideBackdrop();
839
1498
  }
840
- /** Syncs `data-state` on the panel and backdrop together (overlay). */
841
1499
  #setOverlayState(state) {
842
- if (this.hasPanelTarget) this.panelTarget.setAttribute("data-state", state);
843
- if (this.hasBackdropTarget) this.backdropTarget.setAttribute("data-state", state);
844
- }
845
- /**
846
- * Applies `hidden` once the close transition ends so the exit slide can play,
847
- * then reverts the modal side effects. The shared waiter completes
848
- * synchronously for 0ms transitions and supplies a bounded fallback when the
849
- * browser emits no terminal event.
850
- */
1500
+ if (this.hasPanelTarget) this.#writeState(this.panelTarget, state);
1501
+ this.#writeBackdropState(state);
1502
+ }
851
1503
  #hideAfterTransition() {
852
1504
  const panel = this.panelTarget;
853
1505
  this.#activePanel = panel;
854
1506
  this.#transition.wait(panel, () => this.#applyOverlayHidden(panel));
855
1507
  }
856
- /** Hides the panel/backdrop and tears down the trap after the exit transition. */
857
1508
  #applyOverlayHidden(panel) {
858
- panel.hidden = true;
1509
+ this.#writeHidden(panel, true);
859
1510
  this.#hideBackdrop();
860
1511
  this.#trap.deactivate();
861
1512
  }
862
1513
  #hideBackdrop() {
863
- if (!this.hasBackdropTarget) return;
864
- this.backdropTarget.setAttribute("data-state", "closed");
865
- this.backdropTarget.hidden = true;
1514
+ this.#writeBackdropState("closed");
1515
+ this.#writeBackdropHidden(true);
1516
+ }
1517
+ #writeState(element, state) {
1518
+ this.#stateLease.write(element, state);
1519
+ }
1520
+ #writeHidden(element, hidden) {
1521
+ this.#hiddenLease.write(element, hidden ? "" : null);
1522
+ }
1523
+ #writeBackdropState(state) {
1524
+ this.#backdrop.state = state;
1525
+ if (this.hasBackdropTarget) this.#writeState(this.backdropTarget, state);
1526
+ }
1527
+ #writeBackdropHidden(hidden) {
1528
+ this.#backdrop.hidden = hidden;
1529
+ if (this.hasBackdropTarget) this.#writeHidden(this.backdropTarget, hidden);
1530
+ }
1531
+ #reflectBackdrop() {
1532
+ this.#writeBackdropState(this.#backdrop.state);
1533
+ this.#writeBackdropHidden(this.#backdrop.hidden);
866
1534
  }
867
- // --- Shared helpers --------------------------------------------------------
868
1535
  #setExpandedAttr(expanded) {
1536
+ this.#expanded = expanded;
1537
+ this.#reflectExpanded();
1538
+ }
1539
+ #reflectExpanded() {
869
1540
  if (this.hasTriggerTarget) {
870
- this.triggerTarget.setAttribute("aria-expanded", expanded ? "true" : "false");
1541
+ this.#expandedLease.write(this.triggerTarget, this.#expanded ? "true" : "false");
871
1542
  }
872
1543
  }
873
- /** Reports a move of the expanded state, naming the mode it happened in. */
874
1544
  #report(open, reason) {
875
- if (!this.#reporting) return;
876
1545
  const detail = { reason, mode: this.#mode };
877
1546
  if (open) this.dispatch("open", { detail, cancelable: false });
878
1547
  else this.dispatch("close", { detail, cancelable: false });
879
1548
  }
880
- /** Whether the panel currently counts as expanded in the mode it is in. */
881
1549
  get #isExpanded() {
882
1550
  return this.#isOverlay ? this.#isOverlayOpen : !this.#collapsed;
883
1551
  }
884
- /**
885
- * Resolves the inline collapsed preference, in priority order:
886
- * 1. the persisted value (when a `key` is set and storage is readable),
887
- * 2. the current DOM `data-state` — so a Turbo cache restore / morph that
888
- * reconnects over already-rendered markup keeps the live state (the DOM is
889
- * the source of truth) even when no `key` / localStorage is available,
890
- * 3. the declared `collapsed` value.
891
- */
892
1552
  #restoreCollapsed() {
893
- const key = this.#storageKey;
894
- if (key) {
895
- const result = readLocalStorage(key);
896
- if (result.ok && result.value !== null) return result.value === "1";
897
- }
1553
+ const stored = this.#readStoredCollapsed(this.#storageKey);
1554
+ if (stored !== null) return stored;
898
1555
  const domState = this.hasPanelTarget ? this.panelTarget.getAttribute("data-state") : null;
899
1556
  if (domState === "collapsed") return true;
900
1557
  if (domState === "expanded") return false;
901
1558
  return this.collapsedValue;
902
1559
  }
903
- /** Persists the collapsed preference when a key is configured. */
1560
+ #readStoredCollapsed(key) {
1561
+ if (!key) return null;
1562
+ const result = readLocalStorage(key);
1563
+ if (!result.ok || result.value === null) return null;
1564
+ return result.value === "1";
1565
+ }
904
1566
  #persistCollapsed(collapsed) {
905
1567
  const key = this.#storageKey;
906
1568
  if (!key) return;
907
1569
  writeLocalStorage(key, collapsed ? "1" : "0");
908
1570
  }
909
- /**
910
- * @stimeoRuntimeOnly `key` names the storage slot the collapsed state is saved under; the state
911
- * itself comes from the user.
912
- */
913
1571
  get #storageKey() {
914
1572
  return this.keyValue ? `stimeo--sidebar:${this.keyValue}` : "";
915
1573
  }
916
- /** Valid breakpoint CSS query, defaulting malformed or negative values to 768px. */
917
1574
  get #breakpointQuery() {
918
- const value = this.breakpointValue;
919
- const breakpoint = Number.isFinite(value) && value >= 0 ? value : 768;
1575
+ const value = this.#safeBreakpoint;
1576
+ const breakpoint = value;
920
1577
  return `(min-width: ${breakpoint}px)`;
921
1578
  }
922
1579
  get #isOverlay() {
@@ -925,8 +1582,16 @@ var SidebarController = class extends Controller {
925
1582
  get #isOverlayOpen() {
926
1583
  return this.#isOverlay && this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
927
1584
  }
1585
+ get #safeBreakpoint() {
1586
+ return this.#numbers.read(
1587
+ this,
1588
+ "breakpoint",
1589
+ this.breakpointValue,
1590
+ _SidebarController.values.breakpoint.default,
1591
+ _SidebarController.valueConstraints.breakpoint
1592
+ );
1593
+ }
928
1594
  };
929
1595
 
930
1596
  export { SidebarController };
931
- //# sourceMappingURL=sidebar_controller.js.map
932
1597
  //# sourceMappingURL=sidebar_controller.js.map