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,218 +1,482 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/command_palette_controller.ts
3
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
4
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
5
+ function stateReasonFor(event) {
6
+ if (!event) return "api";
7
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
8
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
9
+ return "user";
10
+ }
11
+
12
+ function actionSource(source) {
13
+ if (source instanceof HTMLElement) {
14
+ return { event: null, host: source, origin: source, reason: "api" };
15
+ }
16
+ const event = source ?? null;
17
+ return {
18
+ event,
19
+ host: event?.currentTarget instanceof Element ? event.currentTarget : null,
20
+ origin: event?.target instanceof Element ? event.target : null,
21
+ reason: stateReasonFor(event)
22
+ };
23
+ }
24
+
25
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
26
+ if (element.getAttribute(name) === value) return false;
27
+ if (value === null) element.removeAttribute(name);
28
+ else element.setAttribute(name, value);
29
+ return true;
30
+ }
4
31
 
5
- // src/utils/active_option.ts
6
32
  function syncActiveOption(options, active) {
7
33
  let written = 0;
8
34
  for (const option of options) {
9
35
  const next = option === active ? "true" : "false";
10
- if (option.getAttribute("aria-selected") === next) continue;
11
- option.setAttribute("aria-selected", next);
12
- written += 1;
36
+ if (setAttributeIfChanged(option, "aria-selected", next)) written += 1;
13
37
  }
14
38
  return written;
15
39
  }
16
40
 
17
- // src/utils/arrow_step.ts
18
41
  function isReservedArrowChord(event, allow = []) {
19
42
  if (!event.key.startsWith("Arrow")) return false;
20
43
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
21
44
  }
45
+ function hasModifierChord(event) {
46
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
47
+ }
48
+
49
+ function sharedRegistry(key, create) {
50
+ const symbol = Symbol.for(key);
51
+ const scope = globalThis;
52
+ const existing = scope[symbol];
53
+ if (existing !== void 0) return existing;
54
+ const registry2 = create();
55
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
56
+ return registry2;
57
+ }
58
+
59
+ var registry = sharedRegistry(
60
+ "stimeo-ui.attribute-lease.registry.v1",
61
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
62
+ );
63
+ function isAttributeValue(value) {
64
+ return value === null || typeof value === "string";
65
+ }
66
+ function readLeaseRecord(element, record) {
67
+ try {
68
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
69
+ if (!Array.isArray(parsed)) return void 0;
70
+ const [original, written, ...rest] = parsed;
71
+ const linked = rest[0] !== false;
72
+ const beneath = linked ? rest : rest.slice(1);
73
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
74
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
75
+ } catch {
76
+ return void 0;
77
+ }
78
+ }
79
+ function attributeStack(element, attribute) {
80
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
81
+ registry.stacks.set(element, byAttribute);
82
+ const known = byAttribute.get(attribute);
83
+ if (known) return known;
84
+ const found = [];
85
+ for (const record of registry.names.get(attribute) ?? []) {
86
+ const read = readLeaseRecord(element, record);
87
+ if (read) found.push({ ...read, record });
88
+ }
89
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
+ lease: null,
92
+ record,
93
+ written,
94
+ original,
95
+ linked: linked && index > 0
96
+ }));
97
+ byAttribute.set(attribute, stack);
98
+ return stack;
99
+ }
100
+ function findHold(stack, lease, record) {
101
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
102
+ }
103
+ var AttributeLease = class {
104
+ #attribute;
105
+ #record;
106
+ #base;
107
+ #preserveAuthor;
108
+ #held = /* @__PURE__ */ new Set();
109
+ constructor(attribute, owner, options = {}) {
110
+ this.#attribute = attribute;
111
+ this.#record = `data-${owner}-${attribute}-lease`;
112
+ this.#base = options.base === true;
113
+ this.#preserveAuthor = options.preserveAuthor === true;
114
+ const names = registry.names;
115
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
116
+ }
117
+ write(element, value) {
118
+ const stack = attributeStack(element, this.#attribute);
119
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
120
+ this.#held.add(element);
121
+ const shows = !this.#covered(element, stack, hold);
122
+ hold.written = value;
123
+ this.#mark(element, stack);
124
+ if (shows) this.#reflect(element, value);
125
+ }
126
+ return(element) {
127
+ const held = this.#held.delete(element);
128
+ const known = registry.stacks.get(element)?.has(this.#attribute);
129
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
130
+ const hold = this.#take(stack);
131
+ if (!hold) return false;
132
+ const carries = element.getAttribute(this.#attribute) === hold.written;
133
+ const owns = carries && !this.#covered(element, stack, hold);
134
+ const index = stack.indexOf(hold);
135
+ stack.splice(index, 1);
136
+ const above = stack[index];
137
+ if (above) above.linked &&= hold.linked;
138
+ this.#mark(element, stack);
139
+ if (!owns) return true;
140
+ const below = hold.linked ? stack[index - 1] : void 0;
141
+ this.#reflect(element, below ? below.written : hold.original);
142
+ return true;
143
+ }
144
+ returnAll() {
145
+ for (const element of Array.from(this.#held)) this.return(element);
146
+ }
147
+ #take(stack) {
148
+ const hold = findHold(stack, this, this.#record);
149
+ if (hold) hold.lease = this;
150
+ return hold;
151
+ }
152
+ #acquire(element, stack) {
153
+ const current = element.getAttribute(this.#attribute);
154
+ const top = stack.at(-1);
155
+ const bottom = stack[0];
156
+ const over = top?.written === current ? top : void 0;
157
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
158
+ const hold = {
159
+ lease: this,
160
+ record: this.#record,
161
+ written: current,
162
+ original: from ? from.original : current,
163
+ linked: !this.#base && over !== void 0
164
+ };
165
+ if (this.#base && bottom) {
166
+ bottom.linked = true;
167
+ stack.unshift(hold);
168
+ } else stack.push(hold);
169
+ return hold;
170
+ }
171
+ #covered(element, stack, hold) {
172
+ const current = element.getAttribute(this.#attribute);
173
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
174
+ }
175
+ #mark(element, stack) {
176
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
177
+ stack.forEach((hold, index) => {
178
+ const names = stack.slice(0, index).map((under) => under.record);
179
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
180
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
181
+ records.set(
182
+ hold.record,
183
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
184
+ );
185
+ });
186
+ for (const [name, recorded] of records) {
187
+ setAttributeIfChanged(element, name, recorded);
188
+ }
189
+ }
190
+ #reflect(element, value) {
191
+ setAttributeIfChanged(element, this.#attribute, value);
192
+ }
193
+ };
22
194
 
23
- // src/utils/composition_tracker.ts
24
195
  var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
25
196
  "insertCompositionText",
26
197
  "insertFromComposition",
27
198
  "deleteCompositionText",
28
199
  "deleteByComposition"
29
200
  ]);
201
+ function isCompositionInputType(inputType) {
202
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
203
+ }
204
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
205
+ var textOf = (element) => element.value;
206
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
30
207
  var CompositionTracker = class {
31
208
  #observedTargets = /* @__PURE__ */ new Set();
32
- #activeTargets = /* @__PURE__ */ new Set();
209
+ #composition = null;
33
210
  #onStart;
34
211
  #onEnd;
35
- /** The field whose confirming `input` is still owed, while the window is open. */
212
+ #onAbandon;
36
213
  #confirmedTarget = null;
214
+ #keyComposing = false;
215
+ #connected = false;
216
+ #abandoned = null;
37
217
  constructor(options = {}) {
38
218
  this.#onStart = options.onStart;
39
219
  this.#onEnd = options.onEnd;
220
+ this.#onAbandon = options.onAbandon;
40
221
  }
41
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
42
222
  observe(target) {
223
+ this.#connected = true;
43
224
  if (this.#observedTargets.has(target)) return;
44
225
  target.addEventListener("compositionstart", this.#handleStart);
45
226
  target.addEventListener("compositionend", this.#handleEnd);
46
- target.addEventListener("keydown", this.#handleKeydown);
227
+ target.addEventListener("keydown", this.#handleKeydown, true);
228
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
229
+ target.addEventListener("input", this.#handleInput, true);
230
+ target.addEventListener("blur", this.#handleBlur, true);
231
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
47
232
  this.#observedTargets.add(target);
48
233
  }
49
- /** Stops tracking one target and clears any active composition it owned. */
50
234
  unobserve(target) {
51
235
  if (!this.#observedTargets.delete(target)) return;
52
- target.removeEventListener("compositionstart", this.#handleStart);
53
- target.removeEventListener("compositionend", this.#handleEnd);
54
- target.removeEventListener("keydown", this.#handleKeydown);
55
- this.#activeTargets.delete(target);
236
+ this.#release(target);
237
+ if (this.#composition?.target === target) this.#letGo();
56
238
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
57
239
  }
58
- /** Releases every listener and clears state so reconnect starts cleanly. */
59
240
  disconnect() {
60
- for (const target of this.#observedTargets) {
61
- target.removeEventListener("compositionstart", this.#handleStart);
62
- target.removeEventListener("compositionend", this.#handleEnd);
63
- target.removeEventListener("keydown", this.#handleKeydown);
64
- }
241
+ this.#connected = false;
242
+ for (const target of this.#observedTargets) this.#release(target);
65
243
  this.#observedTargets.clear();
66
- this.#activeTargets.clear();
244
+ this.#letGo();
67
245
  this.#confirmedTarget = null;
246
+ this.#keyComposing = false;
247
+ this.#abandoned = null;
68
248
  }
69
- /** True when lifecycle tracking or the current event reports composition. */
70
249
  isComposing(event) {
71
- return this.#activeTargets.size > 0 || event?.isComposing === true;
72
- }
73
- /**
74
- * Whether `event` is the `input` echoing the composition just confirmed.
75
- *
76
- * Asking closes the window either way, so one confirmation is folded at most
77
- * once and a consumer asks once per `input`.
78
- */
250
+ this.#settle();
251
+ return event?.isComposing === true || this.#composition !== null;
252
+ }
253
+ composingElement() {
254
+ this.#settle();
255
+ return this.#composition?.element ?? null;
256
+ }
257
+ owns(event) {
258
+ const element = this.composingElement();
259
+ return event.isComposing === true || element !== null && element === event.target;
260
+ }
79
261
  consumesConfirmedInput(event) {
80
262
  const confirmed = this.#confirmedTarget;
81
263
  this.#confirmedTarget = null;
82
264
  if (confirmed === null || confirmed !== event.target) return false;
83
265
  const inputType = event.inputType;
84
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
266
+ return !inputType || isCompositionInputType(inputType);
267
+ }
268
+ #release(target) {
269
+ target.removeEventListener("compositionstart", this.#handleStart);
270
+ target.removeEventListener("compositionend", this.#handleEnd);
271
+ target.removeEventListener("keydown", this.#handleKeydown, true);
272
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
273
+ target.removeEventListener("input", this.#handleInput, true);
274
+ target.removeEventListener("blur", this.#handleBlur, true);
275
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
276
+ }
277
+ #letGo() {
278
+ const composition = this.#composition;
279
+ if (composition === null) return;
280
+ if (!holdsFocus(composition.element)) {
281
+ this.#composition = null;
282
+ return;
283
+ }
284
+ queueMicrotask(() => {
285
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
286
+ return;
287
+ }
288
+ this.#composition = null;
289
+ if (!this.#connected) return;
290
+ this.#abandoned = { element: composition.element, inPlace: true };
291
+ this.#runAbandon();
292
+ });
293
+ }
294
+ #settle() {
295
+ const composition = this.#composition;
296
+ if (composition === null) return;
297
+ const { target, element, text } = composition;
298
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
299
+ this.#composition = null;
300
+ } else if (textOf(element) !== text) {
301
+ this.#abandon(false);
302
+ }
85
303
  }
304
+ #abandon(inPlace) {
305
+ this.#owe(inPlace);
306
+ this.#composition = null;
307
+ queueMicrotask(this.#runAbandon);
308
+ }
309
+ #owe(inPlace) {
310
+ const composition = this.#composition;
311
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
312
+ }
313
+ #runAbandon = () => {
314
+ const abandoned = this.#abandoned;
315
+ if (abandoned === null) return;
316
+ this.#abandoned = null;
317
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
318
+ };
86
319
  #handleStart = (event) => {
87
320
  this.#confirmedTarget = null;
88
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
321
+ this.#settle();
322
+ this.#owe(true);
323
+ const target = event.currentTarget;
324
+ const element = event.target;
325
+ this.#composition = { target, element, text: textOf(element) };
326
+ this.#runAbandon();
89
327
  this.#onStart?.(event);
90
328
  };
91
329
  #handleEnd = (event) => {
92
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
330
+ this.#runAbandon();
331
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
93
332
  this.#confirmedTarget = event.target;
94
333
  this.#onEnd?.(event);
95
334
  };
96
- /** A key on an observed field opens an edit of its own, so no echo is owed. */
97
- #handleKeydown = () => {
335
+ #handleKeydown = (event) => {
336
+ this.#settle();
337
+ this.#runAbandon();
98
338
  this.#confirmedTarget = null;
339
+ this.#keyComposing = event.isComposing === true;
340
+ };
341
+ #handleBeforeInput = (event) => {
342
+ const keyComposing = this.#keyComposing;
343
+ this.#keyComposing = false;
344
+ this.#settle();
345
+ const { isComposing, inputType } = event;
346
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
347
+ this.#abandon(true);
348
+ }
349
+ this.#runAbandon();
350
+ };
351
+ #handleInput = (event) => {
352
+ const composition = this.#composition;
353
+ const { isTrusted, isComposing, inputType } = event;
354
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
355
+ composition.text = textOf(composition.element);
356
+ } else {
357
+ this.#settle();
358
+ }
359
+ this.#runAbandon();
360
+ };
361
+ #handleBlur = (event) => {
362
+ this.#settle();
363
+ this.#runAbandon();
364
+ const composition = this.#composition;
365
+ if (composition === null || composition.target === composition.element) return;
366
+ const { element } = composition;
367
+ if (!(element instanceof Node) || event.target !== element) return;
368
+ const root = element.getRootNode();
369
+ if (root.activeElement !== element) this.#composition = null;
370
+ };
371
+ #handleBeforeMorphAttribute = (event) => {
372
+ this.#settle();
373
+ const composition = this.#composition;
374
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
375
+ const detail = event.detail;
376
+ if (detail?.attributeName === "value") event.preventDefault();
99
377
  };
100
378
  };
101
379
 
102
- // src/utils/before_cache_reset.ts
103
- var BeforeCacheReset = class _BeforeCacheReset {
104
- /** Every subscribed instance, iterated by the one shared document listener. */
105
- static #subscribers = /* @__PURE__ */ new Set();
106
- /** The shared listener; installed while at least one instance is subscribed. */
107
- static #onBeforeCache = () => {
108
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
109
- };
110
- #rewind;
111
- /** @param rewind - the pass that returns this controller's state to its initial form. */
112
- constructor(rewind) {
113
- this.#rewind = rewind;
114
- }
115
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
116
- activate() {
117
- const first = _BeforeCacheReset.#subscribers.size === 0;
118
- _BeforeCacheReset.#subscribers.add(this);
119
- if (first) {
120
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
380
+ var DetachGate = class _DetachGate {
381
+ #pending = false;
382
+ get pending() {
383
+ return this.#pending;
384
+ }
385
+ static isDetached(host) {
386
+ return !host.element.isConnected || !_DetachGate.#listed(host);
387
+ }
388
+ disconnected(host, teardown) {
389
+ const carried = _DetachGate.#carried(host);
390
+ if (!carried && _DetachGate.isDetached(host)) {
391
+ this.#pending = false;
392
+ teardown();
393
+ return;
121
394
  }
395
+ this.#pending = true;
396
+ const probe = () => {
397
+ if (!this.#pending) return;
398
+ this.#pending = false;
399
+ teardown();
400
+ };
401
+ if (carried) setTimeout(probe, 0);
402
+ else queueMicrotask(probe);
122
403
  }
123
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
124
- deactivate() {
125
- _BeforeCacheReset.#subscribers.delete(this);
126
- if (_BeforeCacheReset.#subscribers.size > 0) return;
127
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
404
+ static #carried(host) {
405
+ const element = host.element;
406
+ return !element.isConnected && element.closest("[data-turbo-permanent]") !== null && _DetachGate.#listed(host);
407
+ }
408
+ static #listed(host) {
409
+ const tokens = (host.element.getAttribute("data-controller") ?? "").split(/\s+/);
410
+ return tokens.includes(host.identifier);
411
+ }
412
+ cancel() {
413
+ this.#pending = false;
128
414
  }
129
415
  };
130
416
 
131
- // src/utils/escape_layer.ts
132
417
  var EscapeLayer = class _EscapeLayer {
133
- static #registries = /* @__PURE__ */ new WeakMap();
134
- #ownerDocument = null;
135
- /** Dismissal callback while active; `null` when inactive. */
136
- #onDismiss = null;
137
- /** Live predicate deciding whether the layer claims a press; `null` = always. */
138
- #claims = null;
139
- /**
140
- * Activates this layer at the top of its document's Escape stack, installing
141
- * the document's shared resolver listener if this is its first layer.
142
- * Re-activating an already-active layer moves it to the top.
143
- */
418
+ static #registries = sharedRegistry(
419
+ "stimeo-ui.escape-layer.registry.v1",
420
+ () => /* @__PURE__ */ new WeakMap()
421
+ );
422
+ #active = null;
144
423
  activate(ownerDocument = document, options) {
145
424
  this.deactivate();
146
- let registry = _EscapeLayer.#registries.get(ownerDocument);
147
- if (!registry) {
148
- registry = _EscapeLayer.#createRegistry();
149
- _EscapeLayer.#registries.set(ownerDocument, registry);
150
- ownerDocument.addEventListener("keydown", registry.onKeydown);
151
- }
152
- registry.stack.push(this);
153
- this.#ownerDocument = ownerDocument;
154
- this.#onDismiss = options.onDismiss;
155
- this.#claims = options.claims ?? null;
156
- }
157
- /**
158
- * Removes this layer from its document's Escape stack, uninstalling the
159
- * shared listener when the stack empties. Safe to call when inactive.
160
- */
425
+ let registry2 = _EscapeLayer.#registries.get(ownerDocument);
426
+ if (!registry2) {
427
+ registry2 = _EscapeLayer.#createRegistry();
428
+ _EscapeLayer.#registries.set(ownerDocument, registry2);
429
+ ownerDocument.addEventListener("keydown", registry2.onKeydown);
430
+ }
431
+ const entry = { onDismiss: options.onDismiss, claims: options.claims ?? null };
432
+ registry2.stack.push(entry);
433
+ this.#active = { document: ownerDocument, entry };
434
+ }
161
435
  deactivate() {
162
- const ownerDocument = this.#ownerDocument;
163
- if (!ownerDocument) return;
164
- const registry = _EscapeLayer.#registries.get(ownerDocument);
165
- if (registry) {
166
- const index = registry.stack.lastIndexOf(this);
167
- if (index >= 0) registry.stack.splice(index, 1);
168
- if (registry.stack.length === 0) {
169
- ownerDocument.removeEventListener("keydown", registry.onKeydown);
170
- _EscapeLayer.#registries.delete(ownerDocument);
436
+ const active = this.#active;
437
+ if (!active) return;
438
+ const registry2 = _EscapeLayer.#registries.get(active.document);
439
+ if (registry2) {
440
+ const index = registry2.stack.lastIndexOf(active.entry);
441
+ if (index >= 0) registry2.stack.splice(index, 1);
442
+ if (registry2.stack.length === 0) {
443
+ active.document.removeEventListener("keydown", registry2.onKeydown);
444
+ _EscapeLayer.#registries.delete(active.document);
171
445
  }
172
446
  }
173
- this.#ownerDocument = null;
174
- this.#onDismiss = null;
175
- this.#claims = null;
447
+ this.#active = null;
176
448
  }
177
- /**
178
- * Whether this active layer would own a press right now: it is the topmost
179
- * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
180
- * and diagnostics — production dismissal goes through the shared listener.
181
- */
182
449
  get ownsEscape() {
183
- const ownerDocument = this.#ownerDocument;
184
- if (!ownerDocument) return false;
185
- const registry = _EscapeLayer.#registries.get(ownerDocument);
186
- if (!registry) return false;
187
- return _EscapeLayer.#resolveOwner(registry.stack) === this;
450
+ const active = this.#active;
451
+ if (!active) return false;
452
+ const registry2 = _EscapeLayer.#registries.get(active.document);
453
+ if (!registry2) return false;
454
+ return _EscapeLayer.#resolveOwner(registry2.stack) === active.entry;
188
455
  }
189
- /** Builds a document's registry with its shared resolver listener. */
190
456
  static #createRegistry() {
191
- const registry = {
457
+ const registry2 = {
192
458
  stack: [],
193
459
  onKeydown: (event) => {
194
460
  if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
195
- const owner = _EscapeLayer.#resolveOwner(registry.stack);
461
+ const owner = _EscapeLayer.#resolveOwner(registry2.stack);
196
462
  if (!owner) return;
197
463
  event.preventDefault();
198
- owner.#onDismiss?.();
464
+ owner.onDismiss();
199
465
  }
200
466
  };
201
- return registry;
467
+ return registry2;
202
468
  }
203
- /** The topmost stack layer whose claims predicate passes, or `null`. */
204
469
  static #resolveOwner(stack) {
205
470
  for (let index = stack.length - 1; index >= 0; index--) {
206
471
  const layer = stack[index];
207
472
  if (!layer) continue;
208
- if (layer.#claims && !layer.#claims()) continue;
473
+ if (layer.claims && !layer.claims()) continue;
209
474
  return layer;
210
475
  }
211
476
  return null;
212
477
  }
213
478
  };
214
479
 
215
- // src/utils/focus_candidate.ts
216
480
  function inheritsFieldsetDisabled(control) {
217
481
  let fieldset = control.closest("fieldset[disabled]");
218
482
  while (fieldset) {
@@ -222,8 +486,34 @@ function inheritsFieldsetDisabled(control) {
222
486
  }
223
487
  return false;
224
488
  }
489
+ function unslotted(element) {
490
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
491
+ }
492
+ function flatTreeParent(element) {
493
+ if (unslotted(element)) return null;
494
+ const parent = element.assignedSlot ?? element.parentNode;
495
+ if (parent instanceof ShadowRoot) return parent.host;
496
+ return parent instanceof Element ? parent : null;
497
+ }
498
+ function flatTreeContains(ancestor, element) {
499
+ for (let node = element; node; node = flatTreeParent(node)) {
500
+ if (node === ancestor) return true;
501
+ }
502
+ return false;
503
+ }
504
+ function closestInFlatTree(element, selector) {
505
+ for (let node = element; node; node = flatTreeParent(node)) {
506
+ if (node.matches(selector)) return node;
507
+ }
508
+ return null;
509
+ }
510
+ function flatTreeChildren(element) {
511
+ return ownedScope(element) ?? Array.from(element.children);
512
+ }
225
513
  function canTakeFocus(element) {
226
- if (element.closest("[hidden], [inert]")) return false;
514
+ for (let node = element; node; node = flatTreeParent(node)) {
515
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
516
+ }
227
517
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
228
518
  if (!("disabled" in element)) return true;
229
519
  if (element.disabled) return false;
@@ -280,222 +570,697 @@ function isTabStop(element) {
280
570
  if (tabindex !== null) return tabindex >= 0;
281
571
  return hasNativeTabStop(element) || hasEditableTabStop(element);
282
572
  }
283
- function tabStopsWithin(root) {
284
- return Array.from(root.querySelectorAll(TAB_STOP_CANDIDATE_SELECTOR)).filter(
285
- isTabStop
573
+ function isRadio(element) {
574
+ return element instanceof HTMLInputElement && element.type === "radio";
575
+ }
576
+ function sameRadioGroup(a, b) {
577
+ return a.name !== "" && a.name === b.name && a.form === b.form && a.getRootNode() === b.getRootNode();
578
+ }
579
+ function isInEditableContent(element) {
580
+ for (let node = element.parentElement; node; node = node.parentElement) {
581
+ if (node.getAttribute("contenteditable")?.toLowerCase() === "false") return false;
582
+ if (hasEditableTabStop(node)) return true;
583
+ }
584
+ return false;
585
+ }
586
+ var XLINK_NAMESPACE = "http://www.w3.org/1999/xlink";
587
+ function isSvgLink(element) {
588
+ return element instanceof SVGElement && element.localName === "a" && (element.hasAttribute("href") || element.hasAttributeNS(XLINK_NAMESPACE, "href"));
589
+ }
590
+ function hasRenderedImage(area) {
591
+ const map = area.closest("map");
592
+ const key = map?.name || map?.id;
593
+ if (!map || !key) return false;
594
+ const root = map.getRootNode();
595
+ return Array.from(root.querySelectorAll("img[usemap]")).some(
596
+ (image) => image.getAttribute("usemap") === `#${key}` && isRenderedForFocus(image)
597
+ );
598
+ }
599
+ function isFocusableInOrder(element) {
600
+ const tabindex = authoredTabindex(element);
601
+ return (tabindex === null || tabindex >= 0) && canTakeFocus(element);
602
+ }
603
+ function extraStop(element) {
604
+ if (element instanceof HTMLAreaElement) {
605
+ const focusable = element.hasAttribute("href") || authoredTabindex(element) !== null;
606
+ return focusable && isFocusableInOrder(element) && hasRenderedImage(element);
607
+ }
608
+ if (element instanceof HTMLObjectElement) {
609
+ return isFocusableInOrder(element) && isRenderedForFocus(element) && element.contentWindow !== null;
610
+ }
611
+ if (isSvgLink(element))
612
+ return isFocusableInOrder(element) && isRenderedForFocus(element);
613
+ return null;
614
+ }
615
+ function isSequentialStop(element) {
616
+ const stop = extraStop(element) ?? (element.matches(TAB_STOP_CANDIDATE_SELECTOR) && isTabStop(element));
617
+ if (!stop) return false;
618
+ const link = element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement || isSvgLink(element);
619
+ return !(link && authoredTabindex(element) === null && isInEditableContent(element));
620
+ }
621
+ function ownedScope(element) {
622
+ if (element.shadowRoot) return Array.from(element.shadowRoot.children);
623
+ if (element instanceof HTMLSlotElement) {
624
+ const assigned = element.assignedElements();
625
+ return assigned.length > 0 ? assigned : Array.from(element.children);
626
+ }
627
+ return null;
628
+ }
629
+ function orderScope(elements, walk) {
630
+ const entries = [];
631
+ const visit = (element) => {
632
+ if (element === walk.from) walk.fromAt = walk.tree.length;
633
+ const tabindex = authoredTabindex(element) ?? 0;
634
+ const stop = (element instanceof HTMLElement || element instanceof SVGElement) && isSequentialStop(element) ? element : null;
635
+ if (stop) walk.tree.push(stop);
636
+ const owned = ownedScope(element);
637
+ if (owned) {
638
+ if (tabindex >= 0) {
639
+ entries.push({ tabindex, stops: [...stop ? [stop] : [], ...orderScope(owned, walk)] });
640
+ }
641
+ return;
642
+ }
643
+ if (stop) entries.push({ tabindex, stops: [stop] });
644
+ for (const child of Array.from(element.children)) visit(child);
645
+ };
646
+ for (const element of elements) visit(element);
647
+ const positive = entries.filter((entry) => entry.tabindex > 0);
648
+ positive.sort((a, b) => a.tabindex - b.tabindex);
649
+ const rest = entries.filter((entry) => entry.tabindex === 0);
650
+ return [...positive, ...rest].flatMap((entry) => entry.stops);
651
+ }
652
+ function passesRadioRule(element, tree, focusedRadio) {
653
+ if (!isRadio(element) || element.checked) return true;
654
+ const group = tree.filter(
655
+ (stop) => stop === element || isRadio(stop) && sameRadioGroup(stop, element)
286
656
  );
657
+ if (group.some((radio) => radio.checked)) return false;
658
+ if (group[0] !== element) return false;
659
+ return focusedRadio === null || !sameRadioGroup(focusedRadio, element);
660
+ }
661
+ function startIndex(order, walk, backward, isStop) {
662
+ if (walk.fromAt < 0) return backward ? order.length - 1 : 0;
663
+ const at = order.indexOf(walk.from);
664
+ if (at >= 0) return backward ? at - 1 : at + 1;
665
+ const neighbour = backward ? walk.tree.slice(0, walk.fromAt).reverse().find(isStop) : walk.tree.slice(walk.fromAt).find(isStop);
666
+ if (neighbour) return order.indexOf(neighbour);
667
+ return backward ? order.length - 1 : 0;
668
+ }
669
+ function sequentialTabStops(root, from = null, backward = false) {
670
+ const walk = { from, tree: [], fromAt: from === root ? 0 : -1 };
671
+ const order = orderScope(ownedScope(root) ?? Array.from(root.children), walk);
672
+ const focusedRadio = walk.fromAt >= 0 && isRadio(from) ? from : null;
673
+ const isStop = (element) => passesRadioRule(element, walk.tree, focusedRadio);
674
+ const start = startIndex(order, walk, backward, isStop);
675
+ const visited = backward ? [...order.slice(0, start + 1).reverse(), ...order.slice(start + 1).reverse()] : [...order.slice(start), ...order.slice(0, start)];
676
+ return visited.filter((element) => element !== from && isStop(element));
677
+ }
678
+ function deepActiveElement(ownerDocument) {
679
+ let active = ownerDocument.activeElement;
680
+ while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
681
+ return active;
682
+ }
683
+
684
+ var MicrotaskCoalescer = class {
685
+ #run;
686
+ #queued = false;
687
+ #active = false;
688
+ #generation = 0;
689
+ constructor(run) {
690
+ this.#run = run;
691
+ }
692
+ activate() {
693
+ this.#active = true;
694
+ }
695
+ cancel() {
696
+ this.#active = false;
697
+ this.#queued = false;
698
+ this.#generation += 1;
699
+ }
700
+ get pending() {
701
+ return this.#queued;
702
+ }
703
+ schedule() {
704
+ if (!this.#active || this.#queued) return;
705
+ this.#queued = true;
706
+ const generation = this.#generation;
707
+ queueMicrotask(() => {
708
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
709
+ this.#queued = false;
710
+ this.#run();
711
+ });
712
+ }
713
+ };
714
+
715
+ function queryOpenDescendants(root, selector) {
716
+ const matches = [];
717
+ const roots = [root];
718
+ for (const scope of roots) {
719
+ matches.push(...scope.querySelectorAll(selector));
720
+ if (scope.nodeType === Node.ELEMENT_NODE) {
721
+ const shadow = scope.shadowRoot;
722
+ if (shadow) roots.push(shadow);
723
+ }
724
+ for (const element of scope.querySelectorAll("*")) {
725
+ if (element.shadowRoot) roots.push(element.shadowRoot);
726
+ }
727
+ }
728
+ return matches;
729
+ }
730
+
731
+ function holdsFocus2(element) {
732
+ return element.matches(":focus") || deepActiveElement(element.ownerDocument) === element;
287
733
  }
734
+ var TabindexLoan = class _TabindexLoan {
735
+ static #registry = sharedRegistry(
736
+ "stimeo-ui.tabindex-loan.registry.v1",
737
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
738
+ );
739
+ #value;
740
+ #record;
741
+ #preserveFocus;
742
+ #lent = /* @__PURE__ */ new Set();
743
+ constructor(value, owner, options = {}) {
744
+ this.#value = value;
745
+ this.#record = `data-${owner}-tabindex-loan`;
746
+ this.#preserveFocus = options.preserveFocus === true;
747
+ _TabindexLoan.#registry.names.add(this.#record);
748
+ }
749
+ lend(element, options = {}) {
750
+ const stack = this.#stack(element);
751
+ const current = element.getAttribute("tabindex");
752
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
753
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
754
+ );
755
+ const linked = stack.at(-1)?.value === current;
756
+ if (!held && options.onlyIfUnfocusable && (current !== null || element.tabIndex >= 0)) return;
757
+ if (!held && current !== null && !linked) return;
758
+ this.#lent.add(element);
759
+ if (held) held.loan = this;
760
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
761
+ this.#mark(element, stack);
762
+ if (current === null || !held && current !== this.#value) {
763
+ element.setAttribute("tabindex", this.#value);
764
+ }
765
+ }
766
+ return(element) {
767
+ const stack = this.#stack(element);
768
+ const hold = stack.find((held) => held.loan === this) ?? stack.find(
769
+ (held) => held.loan === null && held.record === this.#record && held.value === this.#value
770
+ );
771
+ if (!hold) return false;
772
+ if (!this.#release(element, stack, hold, this.#preserveFocus)) {
773
+ hold.loan = this;
774
+ this.#lent.add(element);
775
+ return false;
776
+ }
777
+ this.#lent.delete(element);
778
+ return true;
779
+ }
780
+ returnAll() {
781
+ for (const element of Array.from(this.#lent)) this.return(element);
782
+ }
783
+ reclaim(element) {
784
+ if (element.getAttribute(this.#record) !== this.#value) return;
785
+ const stack = this.#stack(element);
786
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
787
+ if (hold && !holdsFocus2(element)) this.#release(element, stack, hold);
788
+ }
789
+ reclaimWithin(root) {
790
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
791
+ this.reclaim(element);
792
+ }
793
+ }
794
+ #stack(element) {
795
+ const stacks = _TabindexLoan.#registry.stacks;
796
+ const stack = stacks.get(element) ?? this.#seed(element);
797
+ stacks.set(element, stack);
798
+ return stack;
799
+ }
800
+ #seed(element) {
801
+ const current = element.getAttribute("tabindex");
802
+ const holds = [];
803
+ for (const record of _TabindexLoan.#registry.names) {
804
+ const value = element.getAttribute(record);
805
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
806
+ }
807
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
808
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
809
+ return holds;
810
+ }
811
+ #release(element, stack, hold, preserveFocus = false) {
812
+ const current = element.getAttribute("tabindex");
813
+ const index = stack.indexOf(hold);
814
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
815
+ if (owns && preserveFocus && holdsFocus2(element)) return false;
816
+ stack.splice(index, 1);
817
+ const above = stack[index];
818
+ if (above) above.linked &&= hold.linked;
819
+ this.#mark(element, stack);
820
+ if (!owns) return true;
821
+ const below = hold.linked ? stack[index - 1] : void 0;
822
+ if (!below) element.removeAttribute("tabindex");
823
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
824
+ return true;
825
+ }
826
+ #mark(element, stack) {
827
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
828
+ for (const hold of stack) records.set(hold.record, hold.value);
829
+ for (const [name, recorded] of records) {
830
+ setAttributeIfChanged(element, name, recorded);
831
+ }
832
+ }
833
+ };
288
834
 
289
- // src/utils/focus_trap.ts
290
- var FocusTrap = class {
291
- /** The element focused before activation, restored on deactivation. */
292
- #previouslyFocused = null;
293
- /** The body's inline `overflow` before locking, restored on deactivation. */
294
- #previousBodyOverflow = "";
295
- /** Whether scroll was locked this activation (so it is only restored if applied). */
296
- #scrollLocked = false;
297
- /** Background siblings made `inert` while active, restored on deactivation. */
298
- #inertedSiblings = [];
299
- /** Whether the modal side effects are currently applied. */
835
+ var FOCUS_TRAP_OWNER = "stimeo-focus-trap";
836
+ var INERT_MARK = `data-${FOCUS_TRAP_OWNER}-inert`;
837
+ var OVERFLOW_MARK = `data-${FOCUS_TRAP_OWNER}-overflow`;
838
+ var FocusTrap = class _FocusTrap {
839
+ static #registries = sharedRegistry(
840
+ "stimeo-ui.focus-trap.registry.v2",
841
+ () => /* @__PURE__ */ new WeakMap()
842
+ );
843
+ static #TEXT_ENTRY_TYPES = /* @__PURE__ */ new Set([
844
+ "text",
845
+ "search",
846
+ "email",
847
+ "url",
848
+ "tel",
849
+ "password",
850
+ "number"
851
+ ]);
852
+ #openers = [];
853
+ #entry;
854
+ #tabindexLoans = /* @__PURE__ */ new Map();
300
855
  #activeState = false;
301
- /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
302
856
  #escapeLayer = new EscapeLayer();
303
- /** Returns the trapped element; called on every operation for the live target. */
857
+ #gate = new DetachGate();
304
858
  #getContainer;
305
- /** Closing/focus hooks; see {@link FocusTrapOptions}. */
306
859
  #options;
307
- /**
308
- * @param getContainer - Returns the trapped element. Called on every operation
309
- * so the live target is always used.
310
- * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
311
- */
312
860
  constructor(getContainer, options = {}) {
313
861
  this.#getContainer = getContainer;
314
862
  this.#options = options;
863
+ this.#entry = {
864
+ container: () => this.#readContainer(),
865
+ moveFocus: (backward) => this.#moveFocus(backward),
866
+ refocus: () => this.#refocus(),
867
+ isolated: false,
868
+ lockedBody: null,
869
+ openedFrom: null,
870
+ handedBack: []
871
+ };
315
872
  }
316
- /** Whether the trap is currently active. */
317
873
  get active() {
318
874
  return this.#activeState;
319
875
  }
320
- /**
321
- * Applies the trap: records the current focus, optionally locks background scroll
322
- * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
323
- * `autoFocus` is off) moves focus inside. No-ops if already active.
324
- */
325
876
  activate() {
326
877
  if (this.#activeState) return;
878
+ _FocusTrap.#sweep();
327
879
  this.#activeState = true;
328
- const active = document.activeElement;
329
- this.#previouslyFocused = active instanceof HTMLElement && active !== document.body ? active : null;
330
- if (this.#flag(this.#options.lockScroll, true)) {
331
- this.#previousBodyOverflow = document.body.style.overflow;
332
- document.body.style.overflow = "hidden";
333
- this.#scrollLocked = true;
334
- }
335
- if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
336
- document.addEventListener("keydown", this.#onKeydown);
337
- this.#beforeCache.activate();
880
+ this.#openers = _FocusTrap.#focusPath();
881
+ this.#entry.openedFrom = _FocusTrap.#trapHolding(this.#openers[0] ?? null);
882
+ if (this.#flag(this.#options.lockScroll, true)) this.#lockScroll();
883
+ this.#entry.isolated = this.#flag(this.#options.isolate, true);
884
+ this.#joinStack();
885
+ _FocusTrap.#reconcileBackground(document);
338
886
  const onEscape = this.#options.onEscape;
339
887
  if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
340
888
  if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
341
889
  }
342
- /**
343
- * Reverts every side effect applied by {@link activate}. No-ops if inactive, so
344
- * a controller can call it defensively from both `close()` and `disconnect()`.
345
- *
346
- * @param restoreFocus - Move focus back to the opener (default `true`). Pass
347
- * `false` on teardown (`disconnect`), where yanking focus is undesirable.
348
- */
349
890
  deactivate({ restoreFocus = true } = {}) {
350
- if (!this.#activeState) return;
891
+ if (!this.#activeState) {
892
+ this.#returnTabindexes();
893
+ return;
894
+ }
351
895
  this.#activeState = false;
352
896
  this.#escapeLayer.deactivate();
353
- document.removeEventListener("keydown", this.#onKeydown);
354
- this.#beforeCache.deactivate();
355
- if (this.#scrollLocked) {
356
- document.body.style.overflow = this.#previousBodyOverflow;
357
- this.#scrollLocked = false;
358
- }
359
- this.#releaseBackground();
360
- if (restoreFocus) {
361
- const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;
362
- target?.focus();
363
- }
897
+ const above = this.#leaveStack();
898
+ this.#unlockScroll();
899
+ _FocusTrap.#reconcileBackground(document);
900
+ const wayBack = this.#handOver(above);
901
+ if (restoreFocus) this.#restore(wayBack, above);
902
+ this.#returnTabindexes();
903
+ }
904
+ connect() {
905
+ this.#gate.cancel();
906
+ _FocusTrap.#sweep();
907
+ new TabindexLoan("-1", FOCUS_TRAP_OWNER).reclaimWithin(document);
908
+ }
909
+ disconnect(host) {
910
+ if (this.#activeState) this.deactivate({ restoreFocus: false });
911
+ this.#gate.disconnected(host, () => this.#returnTabindexes());
912
+ }
913
+ refreshIsolation() {
914
+ if (!this.#activeState) return;
915
+ const isolate = this.#flag(this.#options.isolate, true);
916
+ if (isolate === this.#entry.isolated) return;
917
+ this.#entry.isolated = isolate;
918
+ const stranded = _FocusTrap.#reconcileBackground(document);
919
+ const active = deepActiveElement(document);
920
+ if (stranded.some((element) => flatTreeContains(element, active))) this.#focusInitial();
921
+ }
922
+ refreshContainer() {
923
+ if (!this.#activeState) return;
924
+ _FocusTrap.#reconcileBackground(document);
925
+ this.#returnTabindexes();
926
+ if (this.#ownsTab()) this.#refocus();
364
927
  }
365
- /** Resolves a boolean-or-getter option, defaulting when it was not provided. */
366
928
  #flag(option, fallback) {
367
929
  if (option === void 0) return fallback;
368
930
  return typeof option === "function" ? option() : option;
369
931
  }
370
- /**
371
- * Reverts the side effects just before Turbo caches the page snapshot, so an
372
- * overlay left open does not bake the scroll lock into `body[style]` — a
373
- * restored page would feed that locked value back into {@link activate} as the
374
- * baseline, and closing would then never unlock the page. Markup state stays
375
- * untouched (restore-open designs reopen against a clean baseline), and focus
376
- * is left alone mid-navigation. The listener lives only while active.
377
- */
378
- #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));
379
- /**
380
- * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
381
- * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
382
- * which layer currently owns Escape.
383
- */
384
- #onKeydown = (event) => {
385
- if (event.key === "Tab") this.#trapTab(event);
386
- };
387
- /** Keeps `Tab` focus cycling within the container's focusable elements. */
388
- #trapTab(event) {
389
- const focusable = this.#focusableElements();
390
- if (focusable.length === 0) {
391
- event.preventDefault();
392
- return;
932
+ #joinStack() {
933
+ const registry2 = _FocusTrap.#registry(document);
934
+ if (registry2.stack.length === 0) {
935
+ document.addEventListener("keydown", registry2.onKeydown);
936
+ document.addEventListener("turbo:morph", registry2.onMorph);
937
+ document.addEventListener("turbo:morph-element", registry2.onMorph);
393
938
  }
394
- const first = focusable[0];
395
- const last = focusable[focusable.length - 1];
396
- const active = document.activeElement;
397
- if (!(active instanceof Node) || !this.#getContainer().contains(active)) {
398
- event.preventDefault();
399
- first?.focus();
400
- return;
939
+ registry2.stack.push(this.#entry);
940
+ }
941
+ #leaveStack() {
942
+ const registry2 = _FocusTrap.#registry(document);
943
+ const index = registry2.stack.indexOf(this.#entry);
944
+ const above = registry2.stack.slice(index + 1);
945
+ registry2.stack.splice(index, 1);
946
+ if (registry2.stack.length === 0) {
947
+ document.removeEventListener("keydown", registry2.onKeydown);
948
+ document.removeEventListener("turbo:morph", registry2.onMorph);
949
+ document.removeEventListener("turbo:morph-element", registry2.onMorph);
401
950
  }
402
- if (event.shiftKey && active === first) {
403
- event.preventDefault();
404
- last?.focus();
405
- } else if (!event.shiftKey && active === last) {
406
- event.preventDefault();
407
- first?.focus();
408
- }
409
- }
410
- /**
411
- * Marks every element outside the container's subtree as `inert` so background
412
- * content cannot be focused or reached by assistive technology, honoring the
413
- * `aria-modal="true"` contract. An element that was *already* `inert` is left
414
- * untracked so `#releaseBackground` does not wrongly clear it.
415
- *
416
- * The walk climbs from the container to `body` and inerts each ancestor's other
417
- * children. Scanning only `body`'s children would skip the branch the container
418
- * sits in — everything beside it inside that branch is background too, and a
419
- * nested container is the ordinary case.
420
- */
421
- #isolateBackground() {
422
- const container = this.#getContainer();
423
- this.#inertedSiblings = [];
424
- for (let node = container; node !== document.body; ) {
425
- const parent = node.parentElement;
426
- if (!parent) break;
427
- for (const sibling of Array.from(parent.children)) {
428
- if (!(sibling instanceof HTMLElement)) continue;
429
- if (sibling === node || sibling.inert) continue;
430
- sibling.inert = true;
431
- this.#inertedSiblings.push(sibling);
951
+ return above;
952
+ }
953
+ static #focusPath() {
954
+ const path = [];
955
+ for (let active = document.activeElement; active; active = active.shadowRoot?.activeElement ?? null) {
956
+ if (active instanceof HTMLElement && active !== document.body) path.unshift(active);
957
+ }
958
+ return path;
959
+ }
960
+ static #trapHolding(element) {
961
+ for (const trap of [..._FocusTrap.#registry(document).stack].reverse()) {
962
+ const container = trap.container();
963
+ if (container && flatTreeContains(container, element)) return trap;
964
+ }
965
+ return null;
966
+ }
967
+ #handOver(above) {
968
+ const entry = this.#entry;
969
+ const wayBack = [
970
+ ...this.#openers,
971
+ this.#options.fallbackFocus?.() ?? null,
972
+ ...entry.handedBack
973
+ ].filter((element) => element !== null);
974
+ for (const trap of above) {
975
+ if (trap.openedFrom !== entry) continue;
976
+ trap.handedBack = [...trap.handedBack, ...wayBack];
977
+ trap.openedFrom = entry.openedFrom;
978
+ }
979
+ this.#openers = [];
980
+ entry.openedFrom = null;
981
+ entry.handedBack = [];
982
+ return wayBack;
983
+ }
984
+ #restore(wayBack, above) {
985
+ const active = deepActiveElement(document);
986
+ const released = this.#readContainer();
987
+ const holds = (trap) => {
988
+ const container = trap.container();
989
+ if (!container || !_FocusTrap.#takesTab(trap) || !flatTreeContains(container, active)) {
990
+ return false;
991
+ }
992
+ return !released || !flatTreeContains(released, active) || flatTreeContains(released, container);
993
+ };
994
+ if (above.some(holds)) return;
995
+ for (const element of wayBack) {
996
+ if (this.#activeState) return;
997
+ if (!element.isConnected || !canTakeFocus(element) || !isRenderedForFocus(element)) continue;
998
+ const before = deepActiveElement(document);
999
+ element.focus();
1000
+ const after = deepActiveElement(document);
1001
+ if (after === element || after !== before) return;
1002
+ }
1003
+ _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack)?.refocus();
1004
+ }
1005
+ #refocus() {
1006
+ if (!this.#flag(this.#options.autoFocus, true)) return;
1007
+ if (flatTreeContains(this.#getContainer(), deepActiveElement(document))) return;
1008
+ this.#focusInitial();
1009
+ }
1010
+ #lendTabindex(container) {
1011
+ const loan = this.#tabindexLoans.get(container) ?? new TabindexLoan("-1", FOCUS_TRAP_OWNER);
1012
+ loan.lend(container);
1013
+ this.#tabindexLoans.set(container, loan);
1014
+ }
1015
+ #returnTabindexes() {
1016
+ const focused = deepActiveElement(document);
1017
+ for (const [element, loan] of [...this.#tabindexLoans]) {
1018
+ if (element === focused) continue;
1019
+ loan.returnAll();
1020
+ this.#tabindexLoans.delete(element);
1021
+ }
1022
+ }
1023
+ #lockScroll() {
1024
+ const registry2 = _FocusTrap.#registry(document);
1025
+ const body = document.body;
1026
+ if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
1027
+ registry2.scrollLocks.add(this.#entry);
1028
+ this.#entry.lockedBody = body;
1029
+ body.style.overflow = "hidden";
1030
+ }
1031
+ #unlockScroll() {
1032
+ const registry2 = _FocusTrap.#registry(document);
1033
+ const body = this.#entry.lockedBody;
1034
+ this.#entry.lockedBody = null;
1035
+ if (!registry2.scrollLocks.delete(this.#entry) || !body) return;
1036
+ if (!_FocusTrap.#isLocked(registry2, body)) _FocusTrap.#unlock(body);
1037
+ }
1038
+ static #isLocked(registry2, body) {
1039
+ return [...registry2.scrollLocks].some((trap) => trap.lockedBody === body);
1040
+ }
1041
+ static #unlock(body) {
1042
+ const saved = body.getAttribute(OVERFLOW_MARK);
1043
+ if (saved === null) return;
1044
+ body.removeAttribute(OVERFLOW_MARK);
1045
+ body.style.overflow = saved;
1046
+ }
1047
+ static #sweep() {
1048
+ const registry2 = _FocusTrap.#registry(document);
1049
+ for (const element of queryOpenDescendants(document, `[${INERT_MARK}]`)) {
1050
+ if (registry2.inerted.has(element)) continue;
1051
+ element.removeAttribute(INERT_MARK);
1052
+ element.inert = false;
1053
+ }
1054
+ if (!_FocusTrap.#isLocked(registry2, document.body)) _FocusTrap.#unlock(document.body);
1055
+ }
1056
+ static #registry(ownerDocument) {
1057
+ const existing = _FocusTrap.#registries.get(ownerDocument);
1058
+ if (existing) return existing;
1059
+ const morphs = new MicrotaskCoalescer(() => _FocusTrap.#reapply(registry2));
1060
+ morphs.activate();
1061
+ const registry2 = {
1062
+ stack: [],
1063
+ onKeydown: (event) => _FocusTrap.#resolveTab(registry2.stack, event),
1064
+ onMorph: () => morphs.schedule(),
1065
+ inerted: /* @__PURE__ */ new Set(),
1066
+ scrollLocks: /* @__PURE__ */ new Set()
1067
+ };
1068
+ _FocusTrap.#registries.set(ownerDocument, registry2);
1069
+ return registry2;
1070
+ }
1071
+ static #reapply(registry2) {
1072
+ _FocusTrap.#reconcileBackground(document);
1073
+ const body = document.body;
1074
+ if (!_FocusTrap.#isLocked(registry2, body)) return;
1075
+ if (!body.hasAttribute(OVERFLOW_MARK)) body.setAttribute(OVERFLOW_MARK, body.style.overflow);
1076
+ body.style.overflow = "hidden";
1077
+ }
1078
+ static #reconcileBackground(ownerDocument) {
1079
+ const registry2 = _FocusTrap.#registry(ownerDocument);
1080
+ for (const element of registry2.inerted) {
1081
+ if (!element.hasAttribute(INERT_MARK)) registry2.inerted.delete(element);
1082
+ }
1083
+ const background = _FocusTrap.#backgroundOf(registry2);
1084
+ for (const element of registry2.inerted) {
1085
+ if (background.has(element)) continue;
1086
+ element.removeAttribute(INERT_MARK);
1087
+ element.inert = false;
1088
+ registry2.inerted.delete(element);
1089
+ }
1090
+ const added = [];
1091
+ for (const element of background) {
1092
+ if (registry2.inerted.has(element)) continue;
1093
+ element.setAttribute(INERT_MARK, "");
1094
+ element.inert = true;
1095
+ registry2.inerted.add(element);
1096
+ added.push(element);
1097
+ }
1098
+ return added;
1099
+ }
1100
+ static #backgroundOf(registry2) {
1101
+ const containers = registry2.stack.map((trap) => trap.container());
1102
+ let top = -1;
1103
+ registry2.stack.forEach((trap, index) => {
1104
+ if (trap.isolated && containers[index]) top = index;
1105
+ });
1106
+ const background = /* @__PURE__ */ new Set();
1107
+ if (top < 0) return background;
1108
+ const kept = /* @__PURE__ */ new Set();
1109
+ const keep = (path) => {
1110
+ for (const node of path) {
1111
+ kept.add(node);
1112
+ background.delete(node);
432
1113
  }
433
- node = parent;
1114
+ for (const node of path) {
1115
+ const parent = flatTreeParent(node);
1116
+ for (const sibling of parent ? flatTreeChildren(parent) : []) {
1117
+ if (!(sibling instanceof HTMLElement) || kept.has(sibling)) continue;
1118
+ if (sibling.inert && !registry2.inerted.has(sibling)) continue;
1119
+ background.add(sibling);
1120
+ }
1121
+ }
1122
+ };
1123
+ containers.slice(top).forEach((container, index) => {
1124
+ const path = [];
1125
+ for (let node = container; node && node !== document.body && !kept.has(node); ) {
1126
+ path.push(node);
1127
+ node = flatTreeParent(node);
1128
+ }
1129
+ if (index === 0 || path.some((node) => background.has(node))) keep(path);
1130
+ });
1131
+ return background;
1132
+ }
1133
+ #readContainer() {
1134
+ try {
1135
+ return this.#getContainer();
1136
+ } catch {
1137
+ return null;
1138
+ }
1139
+ }
1140
+ static #resolveTab(stack, event) {
1141
+ if (event.key !== "Tab" || event.defaultPrevented) return;
1142
+ if (event.isComposing || event.keyCode === 229) return;
1143
+ if (event.ctrlKey || event.metaKey) return;
1144
+ const owner = _FocusTrap.#tabOwner(stack);
1145
+ if (!owner) return;
1146
+ event.preventDefault();
1147
+ owner.moveFocus(event.shiftKey);
1148
+ }
1149
+ static #tabOwner(stack) {
1150
+ for (const trap of [...stack].reverse()) {
1151
+ if (_FocusTrap.#takesTab(trap)) return trap;
1152
+ }
1153
+ return null;
1154
+ }
1155
+ static #takesTab(trap) {
1156
+ const container = trap.container();
1157
+ if (!container) return false;
1158
+ return container.isConnected && !closestInFlatTree(container, "[hidden]") && isRenderedForFocus(container);
1159
+ }
1160
+ #ownsTab() {
1161
+ return _FocusTrap.#tabOwner(_FocusTrap.#registry(document).stack) === this.#entry;
1162
+ }
1163
+ #moveFocus(backward) {
1164
+ const from = deepActiveElement(document);
1165
+ for (const stop of sequentialTabStops(this.#getContainer(), from, backward)) {
1166
+ if (!this.#ownsTab()) return;
1167
+ const before = deepActiveElement(document);
1168
+ stop.focus();
1169
+ const after = deepActiveElement(document);
1170
+ if (after === before) continue;
1171
+ if (after === stop) _FocusTrap.#selectOnEntry(stop);
1172
+ return;
434
1173
  }
435
1174
  }
436
- /** Reverts the `inert` flags applied by `#isolateBackground`. */
437
- #releaseBackground() {
438
- for (const sibling of this.#inertedSiblings) {
439
- sibling.inert = false;
1175
+ static #selectOnEntry(stop) {
1176
+ if (stop instanceof HTMLInputElement && _FocusTrap.#TEXT_ENTRY_TYPES.has(stop.type)) {
1177
+ stop.select();
440
1178
  }
441
- this.#inertedSiblings = [];
442
1179
  }
443
- /** Moves focus to the initial target, the first focusable, or the container. */
444
1180
  #focusInitial() {
445
1181
  const preferred = this.#options.initialFocus?.();
446
1182
  if (preferred) {
447
1183
  preferred.focus();
448
1184
  return;
449
1185
  }
450
- const focusable = this.#focusableElements();
451
- if (focusable[0]) {
452
- focusable[0].focus();
1186
+ const container = this.#getContainer();
1187
+ const [first] = sequentialTabStops(container);
1188
+ if (first) {
1189
+ first.focus();
453
1190
  return;
454
1191
  }
455
- const container = this.#getContainer();
456
- container.tabIndex = -1;
1192
+ this.#lendTabindex(container);
457
1193
  container.focus();
458
1194
  }
459
- /** Collects the container's currently focusable descendants in DOM order. */
460
- #focusableElements() {
461
- return tabStopsWithin(this.#getContainer());
1195
+ };
1196
+
1197
+ var ListenerSet = class {
1198
+ #abort = new AbortController();
1199
+ add(target, type, handler, options) {
1200
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
1201
+ }
1202
+ dispose() {
1203
+ this.#abort.abort();
1204
+ this.#abort = new AbortController();
462
1205
  }
463
1206
  };
464
1207
 
465
- // src/utils/microtask_coalescer.ts
466
- var MicrotaskCoalescer = class {
467
- #run;
468
- #queued = false;
469
- #active = false;
470
- #generation = 0;
471
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
1208
+ var MorphRenderWatcher = class {
1209
+ #listeners = new ListenerSet();
1210
+ #pass;
1211
+ #element = null;
1212
+ #onMorph = () => this.#pass.schedule();
472
1213
  constructor(run) {
473
- this.#run = run;
1214
+ this.#pass = new MicrotaskCoalescer(run);
474
1215
  }
475
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
476
- activate() {
477
- this.#active = true;
478
- }
479
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
480
- cancel() {
481
- this.#active = false;
482
- this.#queued = false;
483
- this.#generation += 1;
1216
+ observe(element) {
1217
+ if (this.#element === element) return;
1218
+ this.disconnect();
1219
+ this.#element = element;
1220
+ this.#pass.activate();
1221
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
484
1222
  }
485
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
486
1223
  schedule() {
487
- if (!this.#active || this.#queued) return;
488
- this.#queued = true;
489
- const generation = this.#generation;
490
- queueMicrotask(() => {
491
- if (generation !== this.#generation || !this.#queued || !this.#active) return;
492
- this.#queued = false;
493
- this.#run();
494
- });
1224
+ this.#pass.schedule();
1225
+ }
1226
+ get pending() {
1227
+ return this.#pass.pending;
1228
+ }
1229
+ disconnect() {
1230
+ this.#listeners.dispose();
1231
+ this.#pass.cancel();
1232
+ this.#element = null;
1233
+ }
1234
+ };
1235
+
1236
+ var LivedMark = class {
1237
+ #name;
1238
+ #lived = false;
1239
+ #element = null;
1240
+ #morph = new MorphRenderWatcher(() => this.#write());
1241
+ constructor(identifier) {
1242
+ this.#name = `data-${identifier}-lived`;
1243
+ }
1244
+ connect(element) {
1245
+ const connection = this.#lived ? "reconnect" : element.hasAttribute(this.#name) ? "restored" : "fresh";
1246
+ this.#lived = true;
1247
+ this.#element = element;
1248
+ this.#write();
1249
+ this.#morph.observe(element);
1250
+ return connection;
1251
+ }
1252
+ disconnect() {
1253
+ this.#morph.disconnect();
1254
+ }
1255
+ #write() {
1256
+ this.#element?.setAttribute(this.#name, "");
495
1257
  }
496
1258
  };
497
1259
 
498
- // src/controllers/command_palette_controller.ts
1260
+ function targetSelector(identifier, name) {
1261
+ return `[data-${identifier}-target~="${name}"]`;
1262
+ }
1263
+
499
1264
  var CommandPaletteController = class _CommandPaletteController extends Controller {
500
1265
  static #ORIGINAL_ARIA_DISABLED = "data-command-palette-original-aria-disabled";
501
1266
  static #ABSENT_ARIA_DISABLED = "absent";
@@ -510,85 +1275,104 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
510
1275
  "filter",
511
1276
  "onKeydown",
512
1277
  "open",
513
- "selectByClick",
1278
+ "select",
514
1279
  "toggle"
515
1280
  ];
516
1281
  static events = ["select"];
517
- /** Stable ID of the active option; the live target is resolved before every use. */
518
1282
  #activeId = null;
519
- /** Visible/selectable target ID order captured for removal fallback. */
520
1283
  #activeOrder = [];
521
1284
  #connected = false;
522
- /** Collapses one mutation batch of target callbacks into a single pass. */
523
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileActive());
524
- /** Owns IME lifecycle state; confirmed text re-filters an open palette once. */
1285
+ #lived = new LivedMark(this.identifier);
1286
+ #dialog = null;
1287
+ #hidden = new AttributeLease("hidden", this.identifier);
1288
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
1289
+ #activeDescendant = new AttributeLease(
1290
+ "aria-activedescendant",
1291
+ this.identifier
1292
+ );
1293
+ #reconcile = new MicrotaskCoalescer(() => {
1294
+ if (this.hasInputTarget) this.#expanded.write(this.inputTarget, String(this.#isOpen));
1295
+ this.#reconcileActive();
1296
+ });
1297
+ #morphRender = new MorphRenderWatcher(() => this.#repair());
525
1298
  #composition = new CompositionTracker({
526
1299
  onEnd: () => {
527
1300
  if (this.#isOpen) this.filter();
528
1301
  }
529
1302
  });
530
- /**
531
- * Owns the modal side effects (focus trap, scroll lock, background `inert`, focus
532
- * restore). Escape closes; focus on open goes to the input, and is restored to
533
- * whatever opened the palette on close.
534
- */
535
1303
  #trap = new FocusTrap(() => this.dialogTarget, {
536
1304
  onEscape: () => this.close(),
537
- initialFocus: () => this.hasInputTarget ? this.inputTarget : null
1305
+ initialFocus: () => {
1306
+ const dialog = this.dialogTarget;
1307
+ return this.inputTargets.find((input) => dialog.contains(input)) ?? null;
1308
+ }
538
1309
  });
539
- /**
540
- * Initializes the global hotkey handler and establishes the initial open state.
541
- *
542
- * The DOM is the source of truth on reconnect (Turbo cache restore / morph): if
543
- * the restored snapshot already shows the dialog open, honor that rather than
544
- * re-deriving from the declarative `open` Value (which would slam a user-opened
545
- * palette shut). The `open` Value only seeds the initial state of a genuinely
546
- * fresh render. We normalize to a clean closed baseline first so
547
- * {@link CommandPaletteController.open | open} runs its full setup — the
548
- * `FocusTrap` is a fresh instance after a reconnect and must be re-activated.
549
- */
1310
+ #listeners = new ListenerSet();
550
1311
  connect() {
551
- document.addEventListener("keydown", this.#onGlobalKeydown);
1312
+ const connection = this.#lived.connect(this.element);
1313
+ this.#trap.connect();
1314
+ this.#listeners.add(document, "keydown", this.#onGlobalKeydown);
552
1315
  if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
553
1316
  this.#syncOptionSemantics();
554
- const shouldOpen = this.#isOpen || this.openValue;
555
- this.#resetToClosedState();
556
- if (shouldOpen) this.open();
1317
+ if (connection === "reconnect" && this.#isOpen) {
1318
+ this.#trap.activate();
1319
+ } else {
1320
+ const shouldOpen = connection !== "restored" && (this.#isOpen || this.openValue);
1321
+ this.#resetToClosedState();
1322
+ if (shouldOpen) this.open();
1323
+ }
1324
+ this.#dialog = this.hasDialogTarget ? this.dialogTarget : null;
557
1325
  this.#connected = true;
558
1326
  this.#reconcile.activate();
1327
+ this.#morphRender.observe(this.element);
559
1328
  }
560
- /** Tears down the global hotkey listener and reverts the modal side effects. */
561
1329
  disconnect() {
562
1330
  this.#connected = false;
1331
+ this.#lived.disconnect();
1332
+ this.#morphRender.disconnect();
563
1333
  this.#reconcile.cancel();
564
- document.removeEventListener("keydown", this.#onGlobalKeydown);
1334
+ this.#listeners.dispose();
565
1335
  this.#composition.disconnect();
566
- this.#trap.deactivate({ restoreFocus: false });
567
- this.#resetToClosedState();
1336
+ this.#trap.disconnect(this);
568
1337
  }
569
- /** Initializes semantics for an option added before or after the controller connects. */
570
1338
  optionTargetConnected(option) {
571
1339
  this.#syncOptionSemanticsFor(option);
572
1340
  option.setAttribute("aria-selected", "false");
573
1341
  option.removeAttribute("data-active");
574
1342
  if (this.#connected) this.#queueOptionReconciliation();
575
1343
  }
576
- /** Clears active residue on the old node and reconciles the surviving targets. */
577
1344
  optionTargetDisconnected(option) {
578
1345
  option.setAttribute("aria-selected", "false");
579
1346
  option.removeAttribute("data-active");
580
1347
  if (this.#connected) this.#queueOptionReconciliation();
581
1348
  }
582
- /** Tracks an input added initially or after connect without extra consumer actions. */
583
1349
  inputTargetConnected(input) {
584
1350
  this.#composition.observe(input);
585
1351
  if (this.#connected) this.#queueOptionReconciliation();
586
1352
  }
587
- /** Removes controller-owned listeners when the input target is replaced or removed. */
588
1353
  inputTargetDisconnected(input) {
589
1354
  this.#composition.unobserve(input);
1355
+ if (!this.inputTargets.includes(input)) {
1356
+ this.#expanded.return(input);
1357
+ this.#activeDescendant.return(input);
1358
+ }
1359
+ this.#queueOptionReconciliation();
1360
+ }
1361
+ dialogTargetConnected() {
1362
+ if (this.#connected) this.#adoptDialog();
1363
+ }
1364
+ dialogTargetDisconnected(dialog) {
1365
+ if (this.dialogTargets.includes(dialog)) return;
1366
+ this.#hidden.return(dialog);
1367
+ if (this.#connected) this.#adoptDialog();
1368
+ }
1369
+ emptyTargetConnected() {
1370
+ this.#queueOptionReconciliation();
1371
+ }
1372
+ emptyTargetDisconnected(empty) {
1373
+ if (!this.emptyTargets.includes(empty)) this.#hidden.return(empty);
1374
+ this.#queueOptionReconciliation();
590
1375
  }
591
- /** Toggles the open state of the command palette. */
592
1376
  toggle() {
593
1377
  if (this.#isOpen) {
594
1378
  this.close();
@@ -596,25 +1380,43 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
596
1380
  this.open();
597
1381
  }
598
1382
  }
599
- /** Opens the palette, traps focus, and shifts focus to the input. */
600
1383
  open() {
601
1384
  if (!this.hasDialogTarget || this.#isOpen) return;
602
- this.dialogTarget.hidden = false;
1385
+ this.#hidden.write(this.dialogTarget, null);
603
1386
  this.openValue = true;
604
- if (this.hasInputTarget) this.inputTarget.setAttribute("aria-expanded", "true");
1387
+ if (this.hasInputTarget) this.#expanded.write(this.inputTarget, "true");
605
1388
  this.#resetFilter();
606
1389
  this.#trap.activate();
607
1390
  }
608
- /** Closes the palette and restores focus back to the opener. */
609
1391
  close() {
1392
+ this.#close(true);
1393
+ }
1394
+ #adoptDialog() {
1395
+ const dialog = this.hasDialogTarget ? this.dialogTarget : null;
1396
+ if (dialog === this.#dialog) return;
1397
+ this.#dialog = dialog;
1398
+ if (!this.#trap.active) {
1399
+ if (dialog) this.#hidden.write(dialog, "");
1400
+ return;
1401
+ }
1402
+ if (!dialog) {
1403
+ this.#resetToClosedState();
1404
+ this.#trap.deactivate();
1405
+ return;
1406
+ }
1407
+ this.#hidden.write(dialog, null);
1408
+ this.#trap.refreshContainer();
1409
+ }
1410
+ #close(restoreFocus) {
610
1411
  if (!this.hasDialogTarget || !this.#isOpen) return;
611
1412
  this.#resetToClosedState();
612
- this.#trap.deactivate();
1413
+ this.#trap.deactivate({ restoreFocus });
613
1414
  }
614
- /** Filters option elements in-memory matching the input value. Bound to input target. */
615
1415
  filter(event) {
616
1416
  if (!this.hasInputTarget || this.#composition.isComposing(event)) return;
617
- this.#syncOptionSemantics();
1417
+ this.#setActiveIndex(this.#applyQuery() ? 0 : -1);
1418
+ }
1419
+ #applyQuery() {
618
1420
  const query = this.inputTarget.value.trim().toLowerCase();
619
1421
  let hasSelectableMatch = false;
620
1422
  for (const option of this.optionTargets) {
@@ -628,34 +1430,39 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
628
1430
  }
629
1431
  }
630
1432
  if (this.hasEmptyTarget) {
631
- this.emptyTarget.hidden = hasSelectableMatch;
1433
+ this.#hidden.write(this.emptyTarget, hasSelectableMatch ? "" : null);
632
1434
  }
633
- this.#setActiveIndex(hasSelectableMatch ? 0 : -1);
1435
+ return hasSelectableMatch;
634
1436
  }
635
- /** Selects the clicked option. Bound to option targets. */
636
- selectByClick(event) {
637
- const target = event.currentTarget;
638
- if (!target) return;
639
- const option = target.closest("[role='option']");
1437
+ #repair() {
1438
+ if (!this.hasDialogTarget) return;
1439
+ const open = this.#trap.active;
1440
+ this.#hidden.write(this.dialogTarget, open ? null : "");
1441
+ if (open && this.hasInputTarget) this.#applyQuery();
1442
+ this.#reconcile.schedule();
1443
+ }
1444
+ select(source) {
1445
+ const { event, host, origin, reason } = actionSource(source);
1446
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
1447
+ return;
1448
+ const option = host?.closest(targetSelector(this.identifier, "option"));
640
1449
  if (!option || !this.optionTargets.includes(option)) return;
641
1450
  this.#syncOptionSemanticsFor(option);
642
1451
  if (!option.hasAttribute("hidden") && !this.#isDisabled(option)) {
643
- this.#confirmSelection(option);
1452
+ this.#confirmSelection(
1453
+ option,
1454
+ reason,
1455
+ event !== null || this.element.contains(document.activeElement)
1456
+ );
644
1457
  }
645
1458
  }
646
- /** Closes when the backdrop (the dialog target itself) is clicked, ignoring inner clicks. */
647
1459
  closeOnBackdrop(event) {
648
1460
  if (event.target === this.dialogTarget) this.close();
649
1461
  }
650
- /**
651
- * Combobox navigation keys (arrows / Home / End / Enter), bound to the input.
652
- * `Tab` (focus trap) and `Escape` (close) are owned by the shared `FocusTrap` at
653
- * the document level, so they work no matter which element inside the dialog has
654
- * focus — not only the input.
655
- */
656
1462
  onKeydown(event) {
657
1463
  if (event.defaultPrevented) return;
658
1464
  if (isReservedArrowChord(event)) return;
1465
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
659
1466
  if (this.#composition.isComposing(event) || !this.#isOpen) return;
660
1467
  this.#reconcileActive();
661
1468
  switch (event.key) {
@@ -708,14 +1515,9 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
708
1515
  this.#activeId = activeOption?.id || null;
709
1516
  this.#activeOrder = activeOption ? visible.map((option) => option.id).filter(Boolean) : [];
710
1517
  if (this.hasInputTarget) {
711
- if (activeOption?.id) {
712
- this.inputTarget.setAttribute("aria-activedescendant", activeOption.id);
713
- } else {
714
- this.inputTarget.removeAttribute("aria-activedescendant");
715
- }
1518
+ this.#activeDescendant.write(this.inputTarget, activeOption?.id || null);
716
1519
  }
717
1520
  }
718
- /** Re-resolves active identity and component-specific fallback against live targets. */
719
1521
  #reconcileActive() {
720
1522
  if (!this.#isOpen) {
721
1523
  this.#setActiveIndex(-1);
@@ -742,7 +1544,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
742
1544
  this.#setActiveIndex(activeId ? this.#findFallbackIndex(visible, activeId) : -1);
743
1545
  this.#reflectEmptyState();
744
1546
  }
745
- /** Resolves stable ID first, then active markers for the existing ID-less case. */
746
1547
  #findActiveIndex(options) {
747
1548
  const activeId = (this.hasInputTarget ? this.inputTarget.getAttribute("aria-activedescendant") : null) ?? this.#activeId;
748
1549
  if (activeId) return options.findIndex((option) => option.id === activeId);
@@ -750,7 +1551,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
750
1551
  (option) => option.hasAttribute("data-active") || option.getAttribute("aria-selected") === "true"
751
1552
  );
752
1553
  }
753
- /** Chooses a surviving former successor, then predecessor, from selectable targets only. */
754
1554
  #findFallbackIndex(options, activeId) {
755
1555
  const oldIndex = this.#activeOrder.indexOf(activeId);
756
1556
  if (oldIndex < 0) return -1;
@@ -765,28 +1565,21 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
765
1565
  }
766
1566
  return -1;
767
1567
  }
768
- /** Reflects whether any visible, navigable command remains. */
769
1568
  #reflectEmptyState() {
770
- if (this.hasEmptyTarget) this.emptyTarget.hidden = this.#visibleOptions.length > 0;
1569
+ if (this.hasEmptyTarget) {
1570
+ this.#hidden.write(this.emptyTarget, this.#visibleOptions.length > 0 ? "" : null);
1571
+ }
771
1572
  }
772
- /** Coalesces all target callbacks from one MutationObserver batch. */
773
1573
  #queueOptionReconciliation() {
774
1574
  this.#reconcile.schedule();
775
1575
  }
776
- /**
777
- * Runs `option`: dispatches `select` and closes.
778
- *
779
- * **Activation is refused here, not at the call sites.** Every path that runs a
780
- * command funnels through this method, so one check covers keyboard, pointer,
781
- * and anything added later. Per-call-site guards would leave `aria-disabled`
782
- * options — which stay within the keyboard's reach — runnable from whichever
783
- * path forgets to consult the predicate.
784
- */
785
- #confirmSelection(option) {
1576
+ #confirmSelection(option, reason = "user", restoreFocus = true) {
786
1577
  if (this.#isDisabled(option)) return;
787
1578
  const value = option.dataset.value || option.textContent || "";
788
- this.dispatch("select", { detail: { value, option } });
789
- this.close();
1579
+ this.dispatch("select", { detail: { value, option, reason } });
1580
+ this.#close(
1581
+ restoreFocus && (reason !== "api" || this.element.contains(document.activeElement))
1582
+ );
790
1583
  }
791
1584
  #resetFilter() {
792
1585
  if (this.hasInputTarget) this.inputTarget.value = "";
@@ -802,43 +1595,15 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
802
1595
  (option) => !option.hasAttribute("hidden") && this.#isNavigable(option)
803
1596
  );
804
1597
  }
805
- /**
806
- * Whether virtual focus may land on `option`.
807
- *
808
- * Only `data-disabled` disqualifies. That attribute is this controller's own —
809
- * authors reach for it to mark a row that is not a destination at all, such as
810
- * a group heading — so it can mean "skip entirely" without contradicting ARIA.
811
- *
812
- * **`aria-disabled` does not disqualify.** It marks a command that must stay
813
- * *discoverable*: `aria-activedescendant` names it, so a reader announces it as
814
- * unavailable rather than hiding its existence. Virtual focus does
815
- * not change that — pointing `aria-activedescendant` at a disabled option is
816
- * ordinary ARIA. Keeping the two attributes distinct is what leaves both
817
- * meanings expressible; collapsing them would take "show it, say it is
818
- * unavailable, do not run it" away from consumers.
819
- */
820
1598
  #isNavigable(option) {
821
1599
  return option.dataset.disabled !== "true";
822
1600
  }
823
- /** Whether activating `option` may run. Suppressed for either disabled marker. */
824
1601
  #isDisabled(option) {
825
1602
  return option.dataset.disabled === "true" || option.getAttribute("aria-disabled") === "true";
826
1603
  }
827
- /** Synchronizes every option's controller-owned selection and disabled semantics. */
828
1604
  #syncOptionSemantics() {
829
1605
  for (const option of this.optionTargets) this.#syncOptionSemanticsFor(option);
830
1606
  }
831
- /**
832
- * Reflects `data-disabled` to ARIA without losing a pre-existing authored value,
833
- * and puts `aria-selected` back to its baseline.
834
- *
835
- * Here `aria-selected` marks the *active* option, not a committed choice — the
836
- * palette runs a command and keeps nothing selected — so an authored value is
837
- * meaningless and is overwritten rather than preserved. Every caller either
838
- * establishes the active option straight after (`filter`, `#setActiveIndex`) or
839
- * is handling an option that cannot be the active one yet
840
- * (`optionTargetConnected`).
841
- */
842
1607
  #syncOptionSemanticsFor(option) {
843
1608
  const originalAttribute = _CommandPaletteController.#ORIGINAL_ARIA_DISABLED;
844
1609
  const originalValue = option.getAttribute(originalAttribute);
@@ -868,7 +1633,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
868
1633
  event.preventDefault();
869
1634
  this.toggle();
870
1635
  };
871
- /** Matches the configured `mod+key` or bare-key hotkey without extra modifiers. */
872
1636
  #matchesHotkey(event) {
873
1637
  const hotkey = this.#parseHotkey();
874
1638
  if (!hotkey || event.altKey || event.shiftKey) return false;
@@ -880,7 +1644,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
880
1644
  }
881
1645
  return event.key.toLowerCase() === hotkey.key;
882
1646
  }
883
- /** Parses the intentionally small public hotkey grammar. */
884
1647
  #parseHotkey() {
885
1648
  const parts = this.hotkeyValue.toLowerCase().split("+");
886
1649
  if (parts.length === 1 && parts[0] && parts[0] !== "mod") {
@@ -891,7 +1654,6 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
891
1654
  }
892
1655
  return null;
893
1656
  }
894
- /** Resets transient open state so reconnect starts from a predictable closed snapshot. */
895
1657
  #resetToClosedState() {
896
1658
  this.#activeId = null;
897
1659
  this.#activeOrder = [];
@@ -901,15 +1663,14 @@ var CommandPaletteController = class _CommandPaletteController extends Controlle
901
1663
  option.removeAttribute("data-active");
902
1664
  }
903
1665
  if (this.hasDialogTarget) {
904
- this.dialogTarget.hidden = true;
1666
+ this.#hidden.write(this.dialogTarget, "");
905
1667
  }
906
1668
  if (this.hasInputTarget) {
907
- this.inputTarget.setAttribute("aria-expanded", "false");
908
- this.inputTarget.removeAttribute("aria-activedescendant");
1669
+ this.#expanded.write(this.inputTarget, "false");
1670
+ this.#activeDescendant.write(this.inputTarget, null);
909
1671
  }
910
1672
  }
911
1673
  };
912
1674
 
913
1675
  export { CommandPaletteController };
914
- //# sourceMappingURL=command_palette_controller.js.map
915
1676
  //# sourceMappingURL=command_palette_controller.js.map