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,130 +1,416 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/combobox_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
+ }
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 };
85
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/field_mirror.ts
103
380
  function commitField(target) {
104
381
  target.dispatchEvent(new Event("change", { bubbles: true }));
105
382
  }
106
383
 
107
- // src/utils/microtask_coalescer.ts
384
+ var ListenerSet = class {
385
+ #abort = new AbortController();
386
+ add(target, type, handler, options) {
387
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
388
+ }
389
+ dispose() {
390
+ this.#abort.abort();
391
+ this.#abort = new AbortController();
392
+ }
393
+ };
394
+
108
395
  var MicrotaskCoalescer = class {
109
396
  #run;
110
397
  #queued = false;
111
398
  #active = false;
112
399
  #generation = 0;
113
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
114
400
  constructor(run) {
115
401
  this.#run = run;
116
402
  }
117
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
118
403
  activate() {
119
404
  this.#active = true;
120
405
  }
121
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
122
406
  cancel() {
123
407
  this.#active = false;
124
408
  this.#queued = false;
125
409
  this.#generation += 1;
126
410
  }
127
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
411
+ get pending() {
412
+ return this.#queued;
413
+ }
128
414
  schedule() {
129
415
  if (!this.#active || this.#queued) return;
130
416
  this.#queued = true;
@@ -137,7 +423,17 @@ var MicrotaskCoalescer = class {
137
423
  }
138
424
  };
139
425
 
140
- // src/utils/option_scroll.ts
426
+ var MoveCounter = class {
427
+ #sequence = 0;
428
+ record() {
429
+ this.#sequence += 1;
430
+ return this.#sequence;
431
+ }
432
+ isLatest(token) {
433
+ return token === this.#sequence;
434
+ }
435
+ };
436
+
141
437
  function scrollOptionIntoView(list, option) {
142
438
  if (list.scrollHeight <= list.clientHeight) return;
143
439
  const listRect = list.getBoundingClientRect();
@@ -149,16 +445,20 @@ function scrollOptionIntoView(list, option) {
149
445
  }
150
446
  }
151
447
 
152
- // src/utils/state_regions.ts
153
- var StateRegions = class {
448
+ var StateRegions = class _StateRegions {
449
+ static #registry = sharedRegistry(
450
+ "stimeo-ui.state-regions.registry.v1",
451
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
452
+ );
154
453
  #whenTrue;
155
454
  #whenFalse;
156
- #taken = /* @__PURE__ */ new WeakSet();
157
- constructor(options) {
455
+ #record;
456
+ constructor(options, owner) {
158
457
  this.#whenTrue = options.whenTrue;
159
458
  this.#whenFalse = options.whenFalse ?? null;
459
+ this.#record = `data-${owner}-hidden-region`;
460
+ _StateRegions.#registry.names.add(this.#record);
160
461
  }
161
- /** Shows the regions inside `host` that belong to `isTrue` and hides the others. */
162
462
  reflect(host, isTrue) {
163
463
  const shown = this.#inside(host, this.#whenTrue());
164
464
  if (!this.#whenFalse) {
@@ -174,103 +474,199 @@ var StateRegions = class {
174
474
  for (const region of shown) this.#write(region, !isTrue);
175
475
  for (const region of hiddenSide) this.#write(region, isTrue);
176
476
  }
177
- /** Writes `hidden` where it moves, noting which regions it takes out of view. */
477
+ release(host) {
478
+ for (const region of [host, ...host.querySelectorAll("*")]) {
479
+ if (!(region instanceof HTMLElement)) continue;
480
+ const stack = this.#held(region);
481
+ const hold = stack && this.#take(stack);
482
+ if (!stack || !hold) continue;
483
+ const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
484
+ const index = stack.indexOf(hold);
485
+ stack.splice(index, 1);
486
+ const above = stack[index];
487
+ if (above) above.linked &&= hold.linked;
488
+ if (owns) {
489
+ const below = hold.linked ? stack[index - 1] : void 0;
490
+ if (below) {
491
+ if (region.hidden !== below.written) region.hidden = below.written;
492
+ } else {
493
+ setAttributeIfChanged(region, "hidden", hold.authored);
494
+ }
495
+ }
496
+ this.#mark(region, stack);
497
+ }
498
+ }
178
499
  #write(region, hidden) {
179
- if (hidden) this.#taken.add(region);
180
- else this.#taken.delete(region);
181
- if (region.hidden !== hidden) region.hidden = hidden;
500
+ const stack = this.#stack(region);
501
+ const hold = this.#take(stack) ?? this.#acquire(region, stack);
502
+ const shows = !this.#covered(region, stack, hold);
503
+ hold.written = hidden;
504
+ if (shows && region.hidden !== hidden) region.hidden = hidden;
505
+ this.#mark(region, stack);
182
506
  }
183
- /** Returns a region this instance hid; one it never hid keeps what it carries. */
184
507
  #give(region) {
185
- if (this.#taken.delete(region)) region.hidden = false;
508
+ const stack = this.#held(region);
509
+ if (stack && this.#take(stack)?.written) this.#write(region, false);
510
+ }
511
+ #held(region) {
512
+ return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
513
+ }
514
+ #stack(region) {
515
+ const stacks = _StateRegions.#registry.stacks;
516
+ const stack = stacks.get(region) ?? this.#seed(region);
517
+ stacks.set(region, stack);
518
+ return stack;
519
+ }
520
+ #seed(region) {
521
+ const found = [];
522
+ for (const record of _StateRegions.#registry.names) {
523
+ const read = this.#recorded(region, record);
524
+ if (read) found.push({ ...read, record });
525
+ }
526
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
527
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
528
+ instance: null,
529
+ record,
530
+ authored,
531
+ written,
532
+ linked: linked && index > 0
533
+ }));
534
+ }
535
+ #take(stack) {
536
+ const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
537
+ if (hold) hold.instance = this;
538
+ return hold;
539
+ }
540
+ #covered(region, stack, hold) {
541
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
542
+ }
543
+ #acquire(region, stack) {
544
+ const top = stack.at(-1);
545
+ const over = top && region.hidden === top.written ? top : void 0;
546
+ const hold = {
547
+ instance: this,
548
+ record: this.#record,
549
+ authored: over ? over.authored : region.getAttribute("hidden"),
550
+ written: false,
551
+ linked: over !== void 0
552
+ };
553
+ stack.push(hold);
554
+ return hold;
555
+ }
556
+ #mark(region, stack) {
557
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
558
+ stack.forEach((hold, index) => {
559
+ const names = stack.slice(0, index).map((under) => under.record);
560
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
561
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
562
+ records.set(
563
+ hold.record,
564
+ region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
565
+ );
566
+ });
567
+ for (const [name, recorded] of records) {
568
+ setAttributeIfChanged(region, name, recorded);
569
+ }
570
+ }
571
+ #recorded(region, name) {
572
+ try {
573
+ const parsed = JSON.parse(region.getAttribute(name) ?? "null");
574
+ if (!Array.isArray(parsed)) return null;
575
+ const [authored, written, ...rest] = parsed;
576
+ if (authored !== null && typeof authored !== "string") return null;
577
+ const linked = rest[0] !== false;
578
+ const beneath = linked ? rest : rest.slice(1);
579
+ if (!beneath.every((under) => typeof under === "string")) return null;
580
+ return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
581
+ } catch {
582
+ return null;
583
+ }
186
584
  }
187
- /** The declared regions that sit within `host`, which may be the host itself. */
188
585
  #inside(host, regions) {
189
586
  return regions.filter((region) => host.contains(region));
190
587
  }
191
588
  };
192
589
 
193
- // src/controllers/combobox_controller.ts
590
+ function targetSelector(identifier, name) {
591
+ return `[data-${identifier}-target~="${name}"]`;
592
+ }
593
+
194
594
  var ComboboxController = class extends Controller {
595
+ #moves = new MoveCounter();
596
+ #moveToken = 0;
195
597
  static targets = ["input", "list", "option", "empty"];
196
- static actions = ["close", "filter", "onKeydown", "open", "selectByClick"];
598
+ static actions = ["close", "filter", "onKeydown", "open", "select"];
197
599
  static events = ["selected"];
198
- /** Owns `hidden` on the regions declared for the empty state. */
199
- #emptyRegion = new StateRegions({ whenTrue: () => this.emptyTargets });
200
- /** Stable ID of the active option; the live element is resolved before every use. */
600
+ #emptyRegion = new StateRegions({ whenTrue: () => this.emptyTargets }, this.identifier);
201
601
  #activeId = null;
202
602
  #connected = false;
203
- /** Collapses one mutation batch of target callbacks into a single pass. */
603
+ #list = null;
604
+ #listHidden = new AttributeLease("hidden", this.identifier);
605
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
606
+ #activeDescendant = new AttributeLease(
607
+ "aria-activedescendant",
608
+ this.identifier
609
+ );
204
610
  #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());
205
- /**
206
- * Suppresses {@link ComboboxController.open | open} for the duration of the
207
- * programmatic re-focus in `#select`, so committing a value (which returns focus
208
- * to the input) does not immediately re-open the listbox via a `focus`-bound
209
- * action.
210
- */
211
611
  #suppressOpen = false;
212
- /** Owns IME lifecycle state; confirmed text re-filters the list once. */
213
612
  #composition = new CompositionTracker({ onEnd: () => this.filter() });
214
- /** Starts closed with no active option and registers the outside-click listener. */
613
+ #listeners = new ListenerSet();
215
614
  connect() {
216
615
  if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
217
616
  this.close();
218
- document.addEventListener("click", this.#onOutsideClick, true);
617
+ this.#list = this.hasListTarget ? this.listTarget : null;
618
+ this.#listeners.add(document, "click", this.#onOutsideClick, { capture: true });
219
619
  this.#connected = true;
220
620
  this.#reconcile.activate();
221
621
  }
222
- /** Removes the document-level listener registered in {@link connect}. */
223
622
  disconnect() {
224
623
  this.#connected = false;
225
624
  this.#reconcile.cancel();
226
625
  this.#composition.disconnect();
227
- document.removeEventListener("click", this.#onOutsideClick, true);
626
+ this.#listeners.dispose();
228
627
  }
229
- /** Tracks an input added initially or after connect. */
230
628
  inputTargetConnected(input) {
231
629
  this.#composition.observe(input);
232
630
  if (this.#connected) this.#queueOptionReconciliation();
233
631
  }
234
- /** Removes composition listeners when the active input is replaced or removed. */
235
632
  inputTargetDisconnected(input) {
236
633
  this.#composition.unobserve(input);
634
+ if (!this.inputTargets.includes(input)) {
635
+ this.#expanded.return(input);
636
+ this.#activeDescendant.return(input);
637
+ }
638
+ this.#queueOptionReconciliation();
639
+ }
640
+ listTargetConnected() {
641
+ this.#adoptList();
642
+ }
643
+ listTargetDisconnected(list) {
644
+ this.#adoptList();
645
+ if (!this.listTargets.includes(list)) this.#listHidden.return(list);
237
646
  }
238
- /** Settles a connecting region on the side the empty state is on. */
239
647
  emptyTargetConnected() {
240
648
  this.#reflectEmptyState();
241
649
  }
242
- /** Establishes the active-state baseline, then reconciles a runtime addition. */
243
650
  optionTargetConnected(option) {
244
651
  option.setAttribute("aria-selected", "false");
245
652
  if (this.#connected) this.#queueOptionReconciliation();
246
653
  }
247
- /** Clears the disconnected node's active state, then reconciles the survivors. */
248
654
  optionTargetDisconnected(option) {
249
655
  option.setAttribute("aria-selected", "false");
250
656
  if (this.#connected) this.#queueOptionReconciliation();
251
657
  }
252
- /** Filters confirmed input text and opens the listbox. */
253
658
  filter(event) {
254
659
  if (this.#composition.isComposing(event)) return;
255
660
  this.open();
256
661
  }
257
- /**
258
- * Opens the listbox, re-filtering the options against the current input value
259
- * so the visible options and empty state always match what is typed (e.g.
260
- * re-opening with a stale non-matching value still surfaces the empty state).
261
- */
262
662
  open() {
263
663
  if (!this.hasListTarget || !this.hasInputTarget || this.#suppressOpen) return;
264
664
  this.#applyFilter();
265
- this.listTarget.hidden = false;
266
- this.inputTarget.setAttribute("aria-expanded", "true");
665
+ this.#listHidden.write(this.listTarget, null);
666
+ this.#expanded.write(this.inputTarget, "true");
267
667
  this.#setActive(-1);
268
668
  this.#reflectEmptyState();
269
669
  }
270
- /**
271
- * Hides options that don't match the current input value (case-insensitive
272
- * substring). An empty query shows every option. Does not change open state.
273
- */
274
670
  #applyFilter() {
275
671
  const query = this.inputTarget.value.trim().toLowerCase();
276
672
  for (const option of this.optionTargets) {
@@ -278,28 +674,18 @@ var ComboboxController = class extends Controller {
278
674
  option.hidden = query.length > 0 && !text.includes(query);
279
675
  }
280
676
  }
281
- /**
282
- * Closes the listbox, clears the active option, and updates ARIA state.
283
- *
284
- * Survives a missing input in both directions. {@link inputTargetConnected}
285
- * accepts an input added after connect, so dereferencing it here would abort
286
- * `connect()` before it registers the outside-click listener, and nothing
287
- * re-registers it later. Symmetrically, an input removed while the popup is
288
- * open must still let the popup come down — so hiding the list is
289
- * unconditional and only the ARIA half is guarded.
290
- */
291
677
  close() {
292
678
  if (!this.hasListTarget) return;
293
- this.listTarget.hidden = true;
679
+ this.#listHidden.write(this.listTarget, "");
294
680
  this.#reflectEmptyState();
295
- if (this.hasInputTarget) this.inputTarget.setAttribute("aria-expanded", "false");
681
+ if (this.hasInputTarget) this.#expanded.write(this.inputTarget, "false");
296
682
  this.#setActive(-1);
297
683
  }
298
- /** Routes keyboard interaction per the APG combobox model. */
299
684
  onKeydown(event) {
300
685
  if (this.#composition.isComposing(event)) return;
301
686
  if (event.defaultPrevented) return;
302
687
  if (isReservedArrowChord(event, ["alt"])) return;
688
+ if ((event.key === "Home" || event.key === "End") && hasModifierChord(event)) return;
303
689
  if (event.altKey && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
304
690
  if (event.key === "ArrowDown" ? !this.#isClosed : this.#isClosed) return;
305
691
  event.preventDefault();
@@ -368,56 +754,41 @@ var ComboboxController = class extends Controller {
368
754
  break;
369
755
  }
370
756
  }
371
- /**
372
- * Closes the listbox when a click lands outside the combobox. Mirrors the menu
373
- * button's outside-click behavior; clicks on an option are inside the element,
374
- * so `#select` (not this handler) closes the popup after committing.
375
- */
376
757
  #onOutsideClick = (event) => {
377
758
  if (!this.#isClosed && !this.element.contains(event.target)) this.close();
378
759
  };
379
- /** Selects the clicked option. Bound via `data-action` (click). */
380
- selectByClick(event) {
381
- const option = event.currentTarget.closest('[role="option"]');
382
- if (option && this.optionTargets.includes(option)) this.#select(option);
383
- }
384
- /**
385
- * Commits an option: fills the input, closes the listbox, notifies listeners.
386
- *
387
- * An input removed while the popup is open leaves the options clickable, and
388
- * {@link ComboboxController.close | close} already survives that state. Selection
389
- * does too: the popup comes down and listeners still hear the choice, with only
390
- * the field-bound half — the value write, the focus return, and the native
391
- * `change` — skipped, because there is no field to carry them.
392
- */
393
- #select(option) {
760
+ select(source) {
761
+ const { event, host, origin, reason } = actionSource(source);
762
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
763
+ return;
764
+ const option = host?.closest(targetSelector(this.identifier, "option"));
765
+ if (!option || !this.optionTargets.includes(option)) return;
766
+ const focus = event !== null || this.element.contains(document.activeElement);
767
+ this.#select(option, reason, focus);
768
+ }
769
+ #select(option, reason = "user", focus = true) {
394
770
  const value = option.dataset.value ?? (option.textContent ?? "").trim();
395
771
  if (!this.hasInputTarget) {
396
772
  this.close();
397
- this.dispatch("selected", { detail: { value } });
773
+ this.dispatch("selected", { detail: { value, reason } });
398
774
  return;
399
775
  }
400
776
  const changed = this.inputTarget.value !== value;
777
+ if (changed) this.#moveToken = this.#moves.record();
778
+ const token = this.#moveToken;
401
779
  this.inputTarget.value = value;
402
780
  this.close();
403
781
  this.#suppressOpen = true;
404
- this.inputTarget.focus();
782
+ if (focus && (reason !== "api" || this.element.contains(document.activeElement)))
783
+ this.inputTarget.focus();
405
784
  this.#suppressOpen = false;
785
+ if (!this.#moves.isLatest(token)) return;
406
786
  if (changed) {
407
787
  commitField(this.inputTarget);
408
788
  }
409
- this.dispatch("selected", { detail: { value } });
410
- }
411
- /**
412
- * Writes whether the open listbox has zero matching options to the state hook
413
- * `data-stimeo--combobox-empty` on the root element and to the regions declared
414
- * for that state. Behavior only: consumers decide how to present the empty state
415
- * via CSS keyed off the attribute, while the `empty` targets carry the copy.
416
- *
417
- * Every path that can change the answer — opening, filtering, reconciling a
418
- * runtime option change, and closing — ends here, so the hook and the regions
419
- * are a pure function of the state rather than of the order things happened in.
420
- */
789
+ if (!this.#moves.isLatest(token)) return;
790
+ this.dispatch("selected", { detail: { value, reason } });
791
+ }
421
792
  #reflectEmptyState() {
422
793
  const empty = !this.#isClosed && this.#visibleOptions().length === 0;
423
794
  if (empty) {
@@ -427,31 +798,31 @@ var ComboboxController = class extends Controller {
427
798
  }
428
799
  this.#emptyRegion.reflect(this.element, empty);
429
800
  }
430
- /**
431
- * Marks the visible option at `index` active via `aria-selected` and the
432
- * input's `aria-activedescendant`. Pass `-1` to clear the active option.
433
- */
434
801
  #setActive(index) {
435
802
  const visible = this.#visibleOptions();
436
803
  const active = index === -1 ? null : visible[index] ?? null;
437
804
  syncActiveOption(this.optionTargets, active);
438
805
  this.#activeId = active?.id || null;
439
- if (this.hasInputTarget) {
440
- if (active?.id) {
441
- this.inputTarget.setAttribute("aria-activedescendant", active.id);
442
- } else {
443
- this.inputTarget.removeAttribute("aria-activedescendant");
444
- }
445
- }
806
+ if (this.hasInputTarget) this.#activeDescendant.write(this.inputTarget, active?.id || null);
446
807
  if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);
447
808
  }
448
- /** Re-applies filtering and active state after a target collection change. */
809
+ #adoptList() {
810
+ if (!this.#connected) return;
811
+ const list = this.hasListTarget ? this.listTarget : null;
812
+ const previous = this.#list;
813
+ if (list === previous) return;
814
+ this.#list = list;
815
+ if (list) this.#listHidden.write(list, previous?.hidden ?? true ? "" : null);
816
+ this.#queueOptionReconciliation();
817
+ }
449
818
  #reconcileOptions() {
450
- if (this.hasInputTarget && !this.#isClosed) this.#applyFilter();
819
+ if (this.hasInputTarget) {
820
+ this.#expanded.write(this.inputTarget, String(!this.#isClosed));
821
+ if (!this.#isClosed) this.#applyFilter();
822
+ }
451
823
  this.#reconcileActive();
452
824
  this.#reflectEmptyState();
453
825
  }
454
- /** Preserves a surviving active ID; disappearance deliberately clears active state. */
455
826
  #reconcileActive() {
456
827
  if (!this.hasInputTarget || this.#isClosed) {
457
828
  this.#setActive(-1);
@@ -467,26 +838,21 @@ var ComboboxController = class extends Controller {
467
838
  const stateMatches = active ? this.#activeId === (active.id || null) && selected.length === 1 && selected[0] === active && idref === (active.id || null) : this.#activeId === null && selected.length === 0 && idref === null;
468
839
  if (!stateMatches) this.#setActive(index);
469
840
  }
470
- /** Resolves the stable ID, with `aria-selected` as the ID-less compatibility marker. */
471
841
  #findActiveIndex(options) {
472
842
  const activeId = (this.hasInputTarget ? this.inputTarget.getAttribute("aria-activedescendant") : null) ?? this.#activeId;
473
843
  if (activeId) return options.findIndex((option) => option.id === activeId);
474
844
  return options.findIndex((option) => option.getAttribute("aria-selected") === "true");
475
845
  }
476
- /** Coalesces all target callbacks from one MutationObserver batch. */
477
846
  #queueOptionReconciliation() {
478
847
  this.#reconcile.schedule();
479
848
  }
480
- /** The options currently shown (not filtered out). */
481
849
  #visibleOptions() {
482
850
  return this.optionTargets.filter((option) => !option.hidden);
483
851
  }
484
- /** Whether the listbox is currently hidden. */
485
852
  get #isClosed() {
486
853
  return !this.hasListTarget || this.listTarget.hidden !== false;
487
854
  }
488
855
  };
489
856
 
490
857
  export { ComboboxController };
491
- //# sourceMappingURL=combobox_controller.js.map
492
858
  //# sourceMappingURL=combobox_controller.js.map