stimeo-ui 0.15.0 → 0.17.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 (332) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +128 -1
  3. package/dist/cable/index.d.ts +79 -23
  4. package/dist/cable/index.js +392 -417
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/controllers/accordion_controller.d.ts +34 -5
  7. package/dist/controllers/accordion_controller.js +228 -28
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.d.ts +20 -9
  10. package/dist/controllers/alert_dialog_controller.js +862 -221
  11. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  12. package/dist/controllers/announcer_controller.d.ts +38 -7
  13. package/dist/controllers/announcer_controller.js +135 -191
  14. package/dist/controllers/announcer_controller.js.map +1 -1
  15. package/dist/controllers/aspect_ratio_controller.js +0 -10
  16. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  17. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  18. package/dist/controllers/auto_submit_controller.js +295 -110
  19. package/dist/controllers/auto_submit_controller.js.map +1 -1
  20. package/dist/controllers/avatar_controller.js +172 -106
  21. package/dist/controllers/avatar_controller.js.map +1 -1
  22. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  23. package/dist/controllers/breadcrumb_controller.js +0 -129
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +22 -4
  26. package/dist/controllers/bulk_select_controller.js +264 -53
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +109 -28
  29. package/dist/controllers/calendar_controller.js +346 -180
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +99 -27
  32. package/dist/controllers/carousel_controller.js +587 -300
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +18 -2
  35. package/dist/controllers/character_counter_controller.js +271 -143
  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 +77 -75
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +44 -7
  41. package/dist/controllers/clipboard_controller.js +187 -108
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +41 -5
  44. package/dist/controllers/collapsible_controller.js +374 -108
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +47 -14
  47. package/dist/controllers/color_picker_controller.js +183 -127
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +33 -6
  50. package/dist/controllers/combobox_controller.js +426 -109
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +45 -14
  53. package/dist/controllers/command_palette_controller.js +994 -350
  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 +212 -125
  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 +939 -245
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +36 -3
  62. package/dist/controllers/context_menu_controller.js +275 -127
  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 +75 -2
  68. package/dist/controllers/countdown_controller.js +235 -97
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -15
  71. package/dist/controllers/currency_input_controller.js +241 -118
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +78 -22
  74. package/dist/controllers/data_grid_controller.js +229 -135
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +64 -11
  77. package/dist/controllers/date_range_picker_controller.js +359 -165
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +22 -3
  80. package/dist/controllers/dialog_controller.js +887 -225
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +24 -7
  83. package/dist/controllers/direct_upload_controller.js +80 -142
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  86. package/dist/controllers/dirty_form_controller.js +26 -47
  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 +175 -20
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +52 -16
  92. package/dist/controllers/drawer_controller.js +963 -336
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +39 -3
  95. package/dist/controllers/dropdown_controller.js +342 -85
  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 +182 -58
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +67 -53
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +60 -18
  103. package/dist/controllers/file_dropzone_controller.js +426 -229
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.d.ts +10 -4
  106. package/dist/controllers/filter_controller.js +78 -36
  107. package/dist/controllers/filter_controller.js.map +1 -1
  108. package/dist/controllers/flash_controller.d.ts +57 -14
  109. package/dist/controllers/flash_controller.js +328 -211
  110. package/dist/controllers/flash_controller.js.map +1 -1
  111. package/dist/controllers/focus_controller.d.ts +12 -3
  112. package/dist/controllers/focus_controller.js +654 -239
  113. package/dist/controllers/focus_controller.js.map +1 -1
  114. package/dist/controllers/form_field_controller.js +153 -136
  115. package/dist/controllers/form_field_controller.js.map +1 -1
  116. package/dist/controllers/form_validation_controller.js +29 -96
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +41 -9
  119. package/dist/controllers/frame_loading_controller.js +263 -198
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +22 -2
  122. package/dist/controllers/highlight_controller.js +119 -70
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +65 -4
  125. package/dist/controllers/hover_card_controller.js +335 -118
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +58 -4
  128. package/dist/controllers/idle_controller.js +316 -65
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +5 -2
  131. package/dist/controllers/input_mask_controller.js +116 -61
  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 +147 -103
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +16 -0
  137. package/dist/controllers/lazy_frame_controller.js +9 -56
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +84 -10
  140. package/dist/controllers/listbox_controller.js +413 -153
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +1 -1
  143. package/dist/controllers/local_time_controller.js +54 -64
  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 +70 -95
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +40 -9
  149. package/dist/controllers/menu_controller.js +254 -167
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +15 -4
  152. package/dist/controllers/menubar_controller.js +206 -331
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +20 -0
  155. package/dist/controllers/meter_controller.js +159 -71
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +50 -7
  158. package/dist/controllers/multi_select_controller.js +797 -388
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +19 -1
  161. package/dist/controllers/navigation_menu_controller.js +156 -231
  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 +373 -174
  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 +241 -45
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +62 -10
  170. package/dist/controllers/number_input_controller.js +387 -227
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.js +28 -75
  173. package/dist/controllers/optimistic_controller.js.map +1 -1
  174. package/dist/controllers/otp_controller.d.ts +38 -7
  175. package/dist/controllers/otp_controller.js +344 -196
  176. package/dist/controllers/otp_controller.js.map +1 -1
  177. package/dist/controllers/overflow_indicator_controller.d.ts +13 -12
  178. package/dist/controllers/overflow_indicator_controller.js +167 -93
  179. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  180. package/dist/controllers/overflow_menu_controller.d.ts +66 -9
  181. package/dist/controllers/overflow_menu_controller.js +408 -277
  182. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  183. package/dist/controllers/pagination_controller.d.ts +52 -15
  184. package/dist/controllers/pagination_controller.js +342 -120
  185. package/dist/controllers/pagination_controller.js.map +1 -1
  186. package/dist/controllers/password_reveal_controller.d.ts +60 -9
  187. package/dist/controllers/password_reveal_controller.js +424 -99
  188. package/dist/controllers/password_reveal_controller.js.map +1 -1
  189. package/dist/controllers/password_strength_controller.d.ts +14 -5
  190. package/dist/controllers/password_strength_controller.js +99 -153
  191. package/dist/controllers/password_strength_controller.js.map +1 -1
  192. package/dist/controllers/persist_controller.d.ts +11 -4
  193. package/dist/controllers/persist_controller.js +252 -96
  194. package/dist/controllers/persist_controller.js.map +1 -1
  195. package/dist/controllers/pointer_drag_controller.d.ts +19 -7
  196. package/dist/controllers/pointer_drag_controller.js +263 -164
  197. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  198. package/dist/controllers/popover_controller.d.ts +46 -4
  199. package/dist/controllers/popover_controller.js +267 -94
  200. package/dist/controllers/popover_controller.js.map +1 -1
  201. package/dist/controllers/portal_controller.d.ts +44 -4
  202. package/dist/controllers/portal_controller.js +209 -72
  203. package/dist/controllers/portal_controller.js.map +1 -1
  204. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  205. package/dist/controllers/preview_guard_controller.js +243 -112
  206. package/dist/controllers/preview_guard_controller.js.map +1 -1
  207. package/dist/controllers/progress_controller.d.ts +11 -0
  208. package/dist/controllers/progress_controller.js +132 -59
  209. package/dist/controllers/progress_controller.js.map +1 -1
  210. package/dist/controllers/radio_group_controller.d.ts +19 -8
  211. package/dist/controllers/radio_group_controller.js +138 -133
  212. package/dist/controllers/radio_group_controller.js.map +1 -1
  213. package/dist/controllers/range_slider_controller.d.ts +92 -4
  214. package/dist/controllers/range_slider_controller.js +245 -112
  215. package/dist/controllers/range_slider_controller.js.map +1 -1
  216. package/dist/controllers/rating_controller.d.ts +33 -6
  217. package/dist/controllers/rating_controller.js +429 -194
  218. package/dist/controllers/rating_controller.js.map +1 -1
  219. package/dist/controllers/read_more_controller.d.ts +26 -2
  220. package/dist/controllers/read_more_controller.js +185 -47
  221. package/dist/controllers/read_more_controller.js.map +1 -1
  222. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  223. package/dist/controllers/reading_progress_controller.js +147 -135
  224. package/dist/controllers/reading_progress_controller.js.map +1 -1
  225. package/dist/controllers/relative_time_controller.d.ts +18 -4
  226. package/dist/controllers/relative_time_controller.js +145 -110
  227. package/dist/controllers/relative_time_controller.js.map +1 -1
  228. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  229. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  230. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  231. package/dist/controllers/resizable_controller.d.ts +44 -4
  232. package/dist/controllers/resizable_controller.js +294 -118
  233. package/dist/controllers/resizable_controller.js.map +1 -1
  234. package/dist/controllers/roving_controller.js +39 -84
  235. package/dist/controllers/roving_controller.js.map +1 -1
  236. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  237. package/dist/controllers/scroll_area_controller.js +428 -221
  238. package/dist/controllers/scroll_area_controller.js.map +1 -1
  239. package/dist/controllers/scroll_restore_controller.js +0 -53
  240. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  241. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  242. package/dist/controllers/scroll_visibility_controller.js +395 -146
  243. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  244. package/dist/controllers/scrollspy_controller.d.ts +9 -3
  245. package/dist/controllers/scrollspy_controller.js +181 -236
  246. package/dist/controllers/scrollspy_controller.js.map +1 -1
  247. package/dist/controllers/separator_controller.d.ts +59 -17
  248. package/dist/controllers/separator_controller.js +288 -132
  249. package/dist/controllers/separator_controller.js.map +1 -1
  250. package/dist/controllers/sidebar_controller.d.ts +81 -7
  251. package/dist/controllers/sidebar_controller.js +1071 -375
  252. package/dist/controllers/sidebar_controller.js.map +1 -1
  253. package/dist/controllers/skeleton_controller.d.ts +23 -0
  254. package/dist/controllers/skeleton_controller.js +248 -92
  255. package/dist/controllers/skeleton_controller.js.map +1 -1
  256. package/dist/controllers/slider_controller.d.ts +72 -8
  257. package/dist/controllers/slider_controller.js +211 -96
  258. package/dist/controllers/slider_controller.js.map +1 -1
  259. package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
  260. package/dist/controllers/smart_sticky_header_controller.js +163 -54
  261. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  262. package/dist/controllers/sortable_controller.js +13 -110
  263. package/dist/controllers/sortable_controller.js.map +1 -1
  264. package/dist/controllers/spinner_controller.d.ts +39 -4
  265. package/dist/controllers/spinner_controller.js +277 -161
  266. package/dist/controllers/spinner_controller.js.map +1 -1
  267. package/dist/controllers/step_indicator_controller.d.ts +24 -17
  268. package/dist/controllers/step_indicator_controller.js +107 -53
  269. package/dist/controllers/step_indicator_controller.js.map +1 -1
  270. package/dist/controllers/stepper_controller.d.ts +43 -9
  271. package/dist/controllers/stepper_controller.js +171 -48
  272. package/dist/controllers/stepper_controller.js.map +1 -1
  273. package/dist/controllers/stick_to_bottom_controller.d.ts +25 -0
  274. package/dist/controllers/stick_to_bottom_controller.js +310 -116
  275. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  276. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  277. package/dist/controllers/sticky_observer_controller.js +214 -49
  278. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  279. package/dist/controllers/submit_once_controller.d.ts +22 -6
  280. package/dist/controllers/submit_once_controller.js +294 -192
  281. package/dist/controllers/submit_once_controller.js.map +1 -1
  282. package/dist/controllers/switch_controller.d.ts +40 -7
  283. package/dist/controllers/switch_controller.js +107 -43
  284. package/dist/controllers/switch_controller.js.map +1 -1
  285. package/dist/controllers/tabs_controller.d.ts +13 -1
  286. package/dist/controllers/tabs_controller.js +50 -25
  287. package/dist/controllers/tabs_controller.js.map +1 -1
  288. package/dist/controllers/tags_input_controller.d.ts +27 -3
  289. package/dist/controllers/tags_input_controller.js +290 -149
  290. package/dist/controllers/tags_input_controller.js.map +1 -1
  291. package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
  292. package/dist/controllers/textarea_autosize_controller.js +71 -58
  293. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  294. package/dist/controllers/theme_controller.d.ts +37 -8
  295. package/dist/controllers/theme_controller.js +148 -80
  296. package/dist/controllers/theme_controller.js.map +1 -1
  297. package/dist/controllers/time_picker_controller.d.ts +18 -1
  298. package/dist/controllers/time_picker_controller.js +162 -81
  299. package/dist/controllers/time_picker_controller.js.map +1 -1
  300. package/dist/controllers/toast_controller.d.ts +85 -23
  301. package/dist/controllers/toast_controller.js +365 -193
  302. package/dist/controllers/toast_controller.js.map +1 -1
  303. package/dist/controllers/toggle_group_controller.d.ts +60 -9
  304. package/dist/controllers/toggle_group_controller.js +194 -99
  305. package/dist/controllers/toggle_group_controller.js.map +1 -1
  306. package/dist/controllers/toolbar_controller.js +92 -90
  307. package/dist/controllers/toolbar_controller.js.map +1 -1
  308. package/dist/controllers/tooltip_controller.d.ts +52 -2
  309. package/dist/controllers/tooltip_controller.js +185 -125
  310. package/dist/controllers/tooltip_controller.js.map +1 -1
  311. package/dist/controllers/transition_controller.d.ts +22 -4
  312. package/dist/controllers/transition_controller.js +123 -115
  313. package/dist/controllers/transition_controller.js.map +1 -1
  314. package/dist/controllers/tree_view_controller.d.ts +59 -18
  315. package/dist/controllers/tree_view_controller.js +248 -290
  316. package/dist/controllers/tree_view_controller.js.map +1 -1
  317. package/dist/index.d.ts +33 -3
  318. package/dist/index.js +8649 -8572
  319. package/dist/index.js.map +1 -1
  320. package/dist/inspector/cli.d.ts +95 -9
  321. package/dist/inspector/cli.js +343 -77
  322. package/dist/inspector/cli.js.map +1 -1
  323. package/dist/inspector/cli_bin.js +390 -96
  324. package/dist/inspector/cli_bin.js.map +1 -1
  325. package/dist/inspector/examples.json +45 -45
  326. package/dist/inspector/manifest.json +1308 -155
  327. package/dist/positioning/index.d.ts +8 -0
  328. package/dist/positioning/index.js +148 -45
  329. package/dist/positioning/index.js.map +1 -1
  330. package/package.json +7 -7
  331. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  332. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,8 +1,27 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/multi_select_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
+ }
4
24
 
5
- // src/utils/announce.ts
6
25
  function announce(message, options = {}) {
7
26
  const text = message.trim();
8
27
  if (text.length === 0) return;
@@ -19,13 +38,22 @@ function fillTemplate(template, values) {
19
38
  });
20
39
  }
21
40
 
22
- // src/utils/aria_ids.ts
23
- var counter = 0;
41
+ function sharedRegistry(key, create) {
42
+ const symbol = Symbol.for(key);
43
+ const scope = globalThis;
44
+ const existing = scope[symbol];
45
+ if (existing !== void 0) return existing;
46
+ const registry = create();
47
+ Object.defineProperty(globalThis, symbol, { value: registry });
48
+ return registry;
49
+ }
50
+
51
+ var ids = sharedRegistry("stimeo-ui.aria-ids.registry.v1", () => ({ counter: 0 }));
24
52
  function uniqueId(prefix = "stimeo") {
25
53
  let candidate;
26
54
  do {
27
- counter += 1;
28
- candidate = `${prefix}-${counter}`;
55
+ ids.counter += 1;
56
+ candidate = `${prefix}-${ids.counter}`;
29
57
  } while (typeof document !== "undefined" && document.getElementById(candidate) !== null);
30
58
  return candidate;
31
59
  }
@@ -36,12 +64,10 @@ function ensureId(element, prefix = "stimeo") {
36
64
  return id;
37
65
  }
38
66
 
39
- // src/utils/logical_scroll.ts
40
67
  function isRtl(element) {
41
68
  return window.getComputedStyle(element).direction === "rtl";
42
69
  }
43
70
 
44
- // src/utils/arrow_step.ts
45
71
  function logicalArrowKey(key, element) {
46
72
  if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
47
73
  if (!isRtl(element)) return key;
@@ -52,31 +78,177 @@ function isReservedArrowChord(event, allow = []) {
52
78
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
53
79
  }
54
80
 
55
- // src/utils/roving_tabindex.ts
81
+ function isAttributeValue(value) {
82
+ return value === null || typeof value === "string";
83
+ }
84
+ function readLeaseRecord(element, record) {
85
+ try {
86
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
87
+ if (!Array.isArray(parsed)) return void 0;
88
+ const [original, written, ...rest] = parsed;
89
+ const linked = rest[0] !== false;
90
+ const beneath = linked ? rest : rest.slice(1);
91
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
92
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
93
+ } catch {
94
+ return void 0;
95
+ }
96
+ }
97
+ var AttributeLease = class _AttributeLease {
98
+ static #registry = sharedRegistry(
99
+ "stimeo-ui.attribute-lease.registry.v1",
100
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
101
+ );
102
+ #attribute;
103
+ #record;
104
+ #base;
105
+ #held = /* @__PURE__ */ new Set();
106
+ constructor(attribute, owner, options = {}) {
107
+ this.#attribute = attribute;
108
+ this.#record = `data-${owner}-${attribute}-lease`;
109
+ this.#base = options.base === true;
110
+ const names = _AttributeLease.#registry.names;
111
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
112
+ }
113
+ write(element, value) {
114
+ const stack = this.#stack(element);
115
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
116
+ this.#held.add(element);
117
+ const shows = !this.#covered(element, stack, hold);
118
+ hold.written = value;
119
+ this.#mark(element, stack);
120
+ if (shows) this.#reflect(element, value);
121
+ }
122
+ return(element) {
123
+ const held = this.#held.delete(element);
124
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
125
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
126
+ const hold = this.#take(stack);
127
+ if (!hold) return false;
128
+ const carries = element.getAttribute(this.#attribute) === hold.written;
129
+ const owns = carries && !this.#covered(element, stack, hold);
130
+ const index = stack.indexOf(hold);
131
+ stack.splice(index, 1);
132
+ const above = stack[index];
133
+ if (above) above.linked &&= hold.linked;
134
+ this.#mark(element, stack);
135
+ if (!owns) return true;
136
+ const below = hold.linked ? stack[index - 1] : void 0;
137
+ this.#reflect(element, below ? below.written : hold.original);
138
+ return true;
139
+ }
140
+ returnAll() {
141
+ for (const element of Array.from(this.#held)) this.return(element);
142
+ }
143
+ #stack(element) {
144
+ const stacks = _AttributeLease.#registry.stacks;
145
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
146
+ stacks.set(element, byAttribute);
147
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
148
+ byAttribute.set(this.#attribute, stack);
149
+ return stack;
150
+ }
151
+ #seed(element) {
152
+ const found = [];
153
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
154
+ const read = readLeaseRecord(element, record);
155
+ if (read) found.push({ ...read, record });
156
+ }
157
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
158
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
159
+ lease: null,
160
+ record,
161
+ written,
162
+ original,
163
+ linked: linked && index > 0
164
+ }));
165
+ }
166
+ #take(stack) {
167
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
168
+ if (hold) hold.lease = this;
169
+ return hold;
170
+ }
171
+ #acquire(element, stack) {
172
+ const current = element.getAttribute(this.#attribute);
173
+ const top = stack.at(-1);
174
+ const bottom = stack[0];
175
+ const over = top?.written === current ? top : void 0;
176
+ const from = this.#base ? bottom : over;
177
+ const hold = {
178
+ lease: this,
179
+ record: this.#record,
180
+ written: current,
181
+ original: from ? from.original : current,
182
+ linked: !this.#base && over !== void 0
183
+ };
184
+ if (this.#base && bottom) {
185
+ bottom.linked = true;
186
+ stack.unshift(hold);
187
+ } else stack.push(hold);
188
+ return hold;
189
+ }
190
+ #covered(element, stack, hold) {
191
+ const current = element.getAttribute(this.#attribute);
192
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
193
+ }
194
+ #mark(element, stack) {
195
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
196
+ stack.forEach((hold, index) => {
197
+ const names = stack.slice(0, index).map((under) => under.record);
198
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
199
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
200
+ records.set(
201
+ hold.record,
202
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
203
+ );
204
+ });
205
+ for (const [name, recorded] of records) {
206
+ if (element.getAttribute(name) === recorded) continue;
207
+ if (recorded === null) element.removeAttribute(name);
208
+ else element.setAttribute(name, recorded);
209
+ }
210
+ }
211
+ #reflect(element, value) {
212
+ if (element.getAttribute(this.#attribute) === value) return;
213
+ if (value === null) element.removeAttribute(this.#attribute);
214
+ else element.setAttribute(this.#attribute, value);
215
+ }
216
+ };
217
+
218
+ function matchingPart(root, selector) {
219
+ return root.matches(selector) ? root : root.querySelector(selector);
220
+ }
221
+ function writeLabel(slot, text) {
222
+ if (slot.childElementCount === 0) {
223
+ slot.textContent = text;
224
+ return;
225
+ }
226
+ let first = null;
227
+ for (const node of Array.from(slot.childNodes)) {
228
+ if (node.nodeType !== Node.TEXT_NODE) continue;
229
+ if (first) node.remove();
230
+ else first = node;
231
+ }
232
+ if (first) first.nodeValue = text;
233
+ else slot.prepend(text);
234
+ }
235
+ function readLabel(slot) {
236
+ if (!slot) return "";
237
+ let text = "";
238
+ for (const node of Array.from(slot.childNodes)) {
239
+ if (node.nodeType === Node.TEXT_NODE) text += node.nodeValue ?? "";
240
+ }
241
+ return text.trim();
242
+ }
243
+
56
244
  var RovingTabindex = class {
57
- /** Returns the current ordered item elements; called on every operation. */
58
245
  #getItems;
59
- /**
60
- * @param getItems - Returns the current ordered item elements. Called on every
61
- * operation so the live target list is always used.
62
- */
63
246
  constructor(getItems) {
64
247
  this.#getItems = getItems;
65
248
  }
66
- /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
67
249
  get activeIndex() {
68
250
  return this.#getItems().findIndex((item) => item.tabIndex === 0);
69
251
  }
70
- /**
71
- * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
72
- * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
73
- * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
74
- * "nothing is currently tabbable".
75
- *
76
- * @param index - Position of the item to make tabbable.
77
- * @param options - Pass `{ focus: true }` to also move DOM focus to that item,
78
- * and `items` to reuse an event-scoped collection snapshot.
79
- */
80
252
  setActive(index, options = {}) {
81
253
  const { focus = false } = options;
82
254
  const items = options.items ?? this.#getItems();
@@ -87,7 +259,6 @@ var RovingTabindex = class {
87
259
  }
88
260
  };
89
261
 
90
- // src/utils/chip_row.ts
91
262
  var ChipRow = class {
92
263
  #directionElement;
93
264
  #getItems;
@@ -99,14 +270,10 @@ var ChipRow = class {
99
270
  constructor(options) {
100
271
  this.#directionElement = options.directionElement;
101
272
  this.#getItems = options.getItems;
102
- this.#getButton = options.getButton ?? ((item) => item.querySelector("button"));
273
+ this.#getButton = options.getButton ?? ((item) => matchingPart(item, "button"));
103
274
  this.#onRemove = options.onRemove;
104
275
  this.#focusAfterEnd = options.focusAfterEnd;
105
276
  }
106
- /**
107
- * Binds delegation to `container`; any row currently bound is released first.
108
- * Idempotent for the same node, so target callbacks may call it freely.
109
- */
110
277
  connect(container) {
111
278
  if (this.#container === container) return;
112
279
  this.disconnect();
@@ -114,10 +281,6 @@ var ChipRow = class {
114
281
  container.addEventListener("click", this.#onClick);
115
282
  container.addEventListener("keydown", this.#onKeydown);
116
283
  }
117
- /**
118
- * Releases the current row. When `container` is supplied, a stale disconnect
119
- * callback cannot tear listeners off a newer replacement target.
120
- */
121
284
  disconnect(container) {
122
285
  const current = this.#container;
123
286
  if (!current || container !== void 0 && current !== container) return;
@@ -125,19 +288,15 @@ var ChipRow = class {
125
288
  current.removeEventListener("keydown", this.#onKeydown);
126
289
  this.#container = null;
127
290
  }
128
- /** Current remove buttons, at most one consumer-resolved button per chip item. */
129
291
  get buttons() {
130
292
  return this.#entries.map(({ button }) => button);
131
293
  }
132
- /** Number of currently navigable remove buttons. */
133
294
  get length() {
134
295
  return this.buttons.length;
135
296
  }
136
- /** Declared-item index of the last navigable chip, or `-1` for an empty row. */
137
297
  get lastIndex() {
138
298
  return this.#entries.at(-1)?.itemIndex ?? -1;
139
299
  }
140
- /** Keeps exactly one button tabbable, preferring the first authored Tab stop. */
141
300
  ensureTabStop() {
142
301
  const buttons = this.#entries.map(({ button }) => button);
143
302
  const active = buttons.findIndex((button) => button.tabIndex === 0);
@@ -146,7 +305,6 @@ var ChipRow = class {
146
305
  this.#roving.setActive(active === -1 ? 0 : active, { items: buttons });
147
306
  }
148
307
  }
149
- /** Focuses the last chip, returning false for an empty row. */
150
308
  focusLast() {
151
309
  const buttons = this.#entries.map(({ button }) => button);
152
310
  const last = buttons.length - 1;
@@ -154,10 +312,6 @@ var ChipRow = class {
154
312
  this.#roving.setActive(last, { focus: true, items: buttons });
155
313
  return true;
156
314
  }
157
- /**
158
- * Focuses the chip that followed a removed index, or the new last chip.
159
- * Returns false when removal emptied the row so the consumer can rescue focus.
160
- */
161
315
  focusAfterRemoval(index) {
162
316
  const entries = this.#entries;
163
317
  if (entries.length === 0) return false;
@@ -168,14 +322,12 @@ var ChipRow = class {
168
322
  });
169
323
  return true;
170
324
  }
171
- /** Current remove-button entries, retaining each button's declared item index. */
172
325
  get #entries() {
173
326
  return this.#getItems().flatMap((item, itemIndex) => {
174
327
  const button = this.#getButton(item);
175
328
  return button ? [{ button, itemIndex }] : [];
176
329
  });
177
330
  }
178
- /** Resolves a delegated event to the remove button owned by a declared chip. */
179
331
  #entry(event) {
180
332
  const target = event.target;
181
333
  if (!(target instanceof Element)) return null;
@@ -186,13 +338,11 @@ var ChipRow = class {
186
338
  const entry = entries[buttonIndex];
187
339
  return { ...entry, buttonIndex, buttons: entries.map(({ button: button2 }) => button2) };
188
340
  }
189
- /** Delegates removal clicks without waiting for Stimulus to wire a new chip. */
190
341
  #onClick = (event) => {
191
342
  const entry = this.#entry(event);
192
343
  if (!entry) return;
193
344
  this.#onRemove(entry.itemIndex);
194
345
  };
195
- /** Applies the shared logical-arrow and removal policy within the chip row. */
196
346
  #onKeydown = (event) => {
197
347
  if (event.defaultPrevented || isReservedArrowChord(event)) return;
198
348
  const entry = this.#entry(event);
@@ -223,74 +373,121 @@ var ChipRow = class {
223
373
  };
224
374
  };
225
375
 
226
- // src/utils/composition_tracker.ts
376
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
377
+ "insertCompositionText",
378
+ "insertFromComposition",
379
+ "deleteCompositionText",
380
+ "deleteByComposition"
381
+ ]);
227
382
  var CompositionTracker = class {
228
383
  #observedTargets = /* @__PURE__ */ new Set();
229
384
  #activeTargets = /* @__PURE__ */ new Set();
230
385
  #onStart;
231
386
  #onEnd;
387
+ #confirmedTarget = null;
232
388
  constructor(options = {}) {
233
389
  this.#onStart = options.onStart;
234
390
  this.#onEnd = options.onEnd;
235
391
  }
236
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
237
392
  observe(target) {
238
393
  if (this.#observedTargets.has(target)) return;
239
394
  target.addEventListener("compositionstart", this.#handleStart);
240
395
  target.addEventListener("compositionend", this.#handleEnd);
396
+ target.addEventListener("keydown", this.#handleKeydown);
241
397
  this.#observedTargets.add(target);
242
398
  }
243
- /** Stops tracking one target and clears any active composition it owned. */
244
399
  unobserve(target) {
245
400
  if (!this.#observedTargets.delete(target)) return;
246
401
  target.removeEventListener("compositionstart", this.#handleStart);
247
402
  target.removeEventListener("compositionend", this.#handleEnd);
403
+ target.removeEventListener("keydown", this.#handleKeydown);
248
404
  this.#activeTargets.delete(target);
405
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
249
406
  }
250
- /** Releases every listener and clears state so reconnect starts cleanly. */
251
407
  disconnect() {
252
408
  for (const target of this.#observedTargets) {
253
409
  target.removeEventListener("compositionstart", this.#handleStart);
254
410
  target.removeEventListener("compositionend", this.#handleEnd);
411
+ target.removeEventListener("keydown", this.#handleKeydown);
255
412
  }
256
413
  this.#observedTargets.clear();
257
414
  this.#activeTargets.clear();
415
+ this.#confirmedTarget = null;
258
416
  }
259
- /** True when lifecycle tracking or the current event reports composition. */
260
417
  isComposing(event) {
261
418
  return this.#activeTargets.size > 0 || event?.isComposing === true;
262
419
  }
420
+ consumesConfirmedInput(event) {
421
+ const confirmed = this.#confirmedTarget;
422
+ this.#confirmedTarget = null;
423
+ if (confirmed === null || confirmed !== event.target) return false;
424
+ const inputType = event.inputType;
425
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
426
+ }
263
427
  #handleStart = (event) => {
428
+ this.#confirmedTarget = null;
264
429
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
265
430
  this.#onStart?.(event);
266
431
  };
267
432
  #handleEnd = (event) => {
268
433
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
434
+ this.#confirmedTarget = event.target;
269
435
  this.#onEnd?.(event);
270
436
  };
437
+ #handleKeydown = () => {
438
+ this.#confirmedTarget = null;
439
+ };
440
+ };
441
+
442
+ function writeFields(container, values, { name, form = "" }) {
443
+ const current = [...container.children];
444
+ const submits = current.length === values.length && current.every(
445
+ (child, index) => child instanceof HTMLInputElement && child.type === "hidden" && child.value === values[index] && child.name === name && (child.getAttribute("form") ?? "") === form
446
+ );
447
+ if (submits) return false;
448
+ container.replaceChildren(
449
+ ...values.map((value) => {
450
+ const input = document.createElement("input");
451
+ input.type = "hidden";
452
+ input.name = name;
453
+ input.value = value;
454
+ if (form !== "") input.setAttribute("form", form);
455
+ return input;
456
+ })
457
+ );
458
+ return true;
459
+ }
460
+ function commitField(target) {
461
+ target.dispatchEvent(new Event("change", { bubbles: true }));
462
+ }
463
+
464
+ var ListenerSet = class {
465
+ #abort = new AbortController();
466
+ add(target, type, handler, options) {
467
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
468
+ }
469
+ dispose() {
470
+ this.#abort.abort();
471
+ this.#abort = new AbortController();
472
+ }
271
473
  };
272
474
 
273
- // src/utils/microtask_coalescer.ts
274
475
  var MicrotaskCoalescer = class {
275
476
  #run;
276
477
  #queued = false;
277
478
  #active = false;
278
479
  #generation = 0;
279
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
280
480
  constructor(run) {
281
481
  this.#run = run;
282
482
  }
283
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
284
483
  activate() {
285
484
  this.#active = true;
286
485
  }
287
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
288
486
  cancel() {
289
487
  this.#active = false;
290
488
  this.#queued = false;
291
489
  this.#generation += 1;
292
490
  }
293
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
294
491
  schedule() {
295
492
  if (!this.#active || this.#queued) return;
296
493
  this.#queued = true;
@@ -303,7 +500,86 @@ var MicrotaskCoalescer = class {
303
500
  }
304
501
  };
305
502
 
306
- // src/utils/option_scroll.ts
503
+ var MorphRenderWatcher = class {
504
+ #listeners = new ListenerSet();
505
+ #pass;
506
+ #element = null;
507
+ #onMorph = () => this.#pass.schedule();
508
+ constructor(run) {
509
+ this.#pass = new MicrotaskCoalescer(run);
510
+ }
511
+ observe(element) {
512
+ if (this.#element === element) return;
513
+ this.disconnect();
514
+ this.#element = element;
515
+ this.#pass.activate();
516
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
517
+ }
518
+ schedule() {
519
+ this.#pass.schedule();
520
+ }
521
+ disconnect() {
522
+ this.#listeners.dispose();
523
+ this.#pass.cancel();
524
+ this.#element = null;
525
+ }
526
+ };
527
+
528
+ var MoveCounter = class {
529
+ #sequence = 0;
530
+ record() {
531
+ this.#sequence += 1;
532
+ return this.#sequence;
533
+ }
534
+ isLatest(token) {
535
+ return token === this.#sequence;
536
+ }
537
+ };
538
+
539
+ var NUMBER_BOUNDS = {
540
+ finite: { finite: true }};
541
+ function matchesNumberBounds(value, bounds) {
542
+ if (!Number.isFinite(value)) {
543
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
544
+ if (direction === null) return false;
545
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
546
+ }
547
+ if (bounds.min !== void 0 && value < bounds.min) return false;
548
+ if (bounds.max !== void 0 && value > bounds.max) return false;
549
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
550
+ if (bounds.integer && !Number.isInteger(value)) return false;
551
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
552
+ return true;
553
+ }
554
+
555
+ function readNumber(raw, fallback, bounds) {
556
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
557
+ }
558
+
559
+ var NumberValueReader = class {
560
+ #lastRejected = /* @__PURE__ */ new Map();
561
+ read(owner, name, raw, fallback, bounds) {
562
+ const resolved = readNumber(raw, fallback, bounds);
563
+ if (matchesNumberBounds(raw, bounds)) {
564
+ this.#lastRejected.delete(name);
565
+ return resolved;
566
+ }
567
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
568
+ const literal = owner.element.getAttribute(attribute);
569
+ if (literal === null) {
570
+ this.#lastRejected.delete(name);
571
+ return resolved;
572
+ }
573
+ if (this.#lastRejected.get(name) !== literal) {
574
+ this.#lastRejected.set(name, literal);
575
+ console.warn(
576
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
577
+ );
578
+ }
579
+ return resolved;
580
+ }
581
+ };
582
+
307
583
  function scrollOptionIntoView(list, option) {
308
584
  if (list.scrollHeight <= list.clientHeight) return;
309
585
  const listRect = list.getBoundingClientRect();
@@ -315,68 +591,324 @@ function scrollOptionIntoView(list, option) {
315
591
  }
316
592
  }
317
593
 
318
- // src/utils/before_cache_reset.ts
319
- var BeforeCacheReset = class _BeforeCacheReset {
320
- /** Every subscribed instance, iterated by the one shared document listener. */
321
- static #subscribers = /* @__PURE__ */ new Set();
322
- /** The shared listener; installed while at least one instance is subscribed. */
323
- static #onBeforeCache = () => {
324
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
325
- };
326
- #rewind;
327
- /** @param rewind - the pass that returns this controller's state to its initial form. */
328
- constructor(rewind) {
329
- this.#rewind = rewind;
594
+ var StateRegions = class _StateRegions {
595
+ static #registry = sharedRegistry(
596
+ "stimeo-ui.state-regions.registry.v1",
597
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
598
+ );
599
+ #whenTrue;
600
+ #whenFalse;
601
+ #record;
602
+ constructor(options, owner) {
603
+ this.#whenTrue = options.whenTrue;
604
+ this.#whenFalse = options.whenFalse ?? null;
605
+ this.#record = `data-${owner}-hidden-region`;
606
+ _StateRegions.#registry.names.add(this.#record);
607
+ }
608
+ reflect(host, isTrue) {
609
+ const shown = this.#inside(host, this.#whenTrue());
610
+ if (!this.#whenFalse) {
611
+ for (const region of shown) this.#write(region, !isTrue);
612
+ return;
613
+ }
614
+ const hiddenSide = this.#inside(host, this.#whenFalse());
615
+ if (shown.length === 0 || hiddenSide.length === 0) {
616
+ for (const region of shown) this.#give(region);
617
+ for (const region of hiddenSide) this.#give(region);
618
+ return;
619
+ }
620
+ for (const region of shown) this.#write(region, !isTrue);
621
+ for (const region of hiddenSide) this.#write(region, isTrue);
622
+ }
623
+ release(host) {
624
+ for (const region of [host, ...host.querySelectorAll("*")]) {
625
+ if (!(region instanceof HTMLElement)) continue;
626
+ const stack = this.#held(region);
627
+ const hold = stack && this.#take(stack);
628
+ if (!stack || !hold) continue;
629
+ const owns = region.hidden === hold.written && !this.#covered(region, stack, hold);
630
+ const index = stack.indexOf(hold);
631
+ stack.splice(index, 1);
632
+ const above = stack[index];
633
+ if (above) above.linked &&= hold.linked;
634
+ if (owns) {
635
+ const below = hold.linked ? stack[index - 1] : void 0;
636
+ if (below) {
637
+ if (region.hidden !== below.written) region.hidden = below.written;
638
+ } else if (region.getAttribute("hidden") !== hold.authored) {
639
+ if (hold.authored === null) region.removeAttribute("hidden");
640
+ else region.setAttribute("hidden", hold.authored);
641
+ }
642
+ }
643
+ this.#mark(region, stack);
644
+ }
330
645
  }
331
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
332
- activate() {
333
- const first = _BeforeCacheReset.#subscribers.size === 0;
334
- _BeforeCacheReset.#subscribers.add(this);
335
- if (first) {
336
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
646
+ #write(region, hidden) {
647
+ const stack = this.#stack(region);
648
+ const hold = this.#take(stack) ?? this.#acquire(region, stack);
649
+ const shows = !this.#covered(region, stack, hold);
650
+ hold.written = hidden;
651
+ if (shows && region.hidden !== hidden) region.hidden = hidden;
652
+ this.#mark(region, stack);
653
+ }
654
+ #give(region) {
655
+ const stack = this.#held(region);
656
+ if (stack && this.#take(stack)?.written) this.#write(region, false);
657
+ }
658
+ #held(region) {
659
+ return _StateRegions.#registry.stacks.has(region) || region.hasAttribute(this.#record) ? this.#stack(region) : void 0;
660
+ }
661
+ #stack(region) {
662
+ const stacks = _StateRegions.#registry.stacks;
663
+ const stack = stacks.get(region) ?? this.#seed(region);
664
+ stacks.set(region, stack);
665
+ return stack;
666
+ }
667
+ #seed(region) {
668
+ const found = [];
669
+ for (const record of _StateRegions.#registry.names) {
670
+ const read = this.#recorded(region, record);
671
+ if (read) found.push({ ...read, record });
672
+ }
673
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
674
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, authored, written, linked }, index) => ({
675
+ instance: null,
676
+ record,
677
+ authored,
678
+ written,
679
+ linked: linked && index > 0
680
+ }));
681
+ }
682
+ #take(stack) {
683
+ const hold = stack.find((held) => held.instance === this) ?? stack.find((held) => held.instance === null && held.record === this.#record);
684
+ if (hold) hold.instance = this;
685
+ return hold;
686
+ }
687
+ #covered(region, stack, hold) {
688
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === region.hidden);
689
+ }
690
+ #acquire(region, stack) {
691
+ const top = stack.at(-1);
692
+ const over = top && region.hidden === top.written ? top : void 0;
693
+ const hold = {
694
+ instance: this,
695
+ record: this.#record,
696
+ authored: over ? over.authored : region.getAttribute("hidden"),
697
+ written: false,
698
+ linked: over !== void 0
699
+ };
700
+ stack.push(hold);
701
+ return hold;
702
+ }
703
+ #mark(region, stack) {
704
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
705
+ stack.forEach((hold, index) => {
706
+ const names = stack.slice(0, index).map((under) => under.record);
707
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
708
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
709
+ records.set(
710
+ hold.record,
711
+ region.getAttribute("hidden") === hold.authored && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.authored, hold.written, ...marker, ...beneath])
712
+ );
713
+ });
714
+ for (const [name, recorded] of records) {
715
+ if (region.getAttribute(name) === recorded) continue;
716
+ if (recorded === null) region.removeAttribute(name);
717
+ else region.setAttribute(name, recorded);
718
+ }
719
+ }
720
+ #recorded(region, name) {
721
+ try {
722
+ const parsed = JSON.parse(region.getAttribute(name) ?? "null");
723
+ if (!Array.isArray(parsed)) return null;
724
+ const [authored, written, ...rest] = parsed;
725
+ if (authored !== null && typeof authored !== "string") return null;
726
+ const linked = rest[0] !== false;
727
+ const beneath = linked ? rest : rest.slice(1);
728
+ if (!beneath.every((under) => typeof under === "string")) return null;
729
+ return typeof written === "boolean" ? { authored, written, linked, beneath } : null;
730
+ } catch {
731
+ return null;
337
732
  }
338
733
  }
339
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
340
- deactivate() {
341
- _BeforeCacheReset.#subscribers.delete(this);
342
- if (_BeforeCacheReset.#subscribers.size > 0) return;
343
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
734
+ #inside(host, regions) {
735
+ return regions.filter((region) => host.contains(region));
344
736
  }
345
737
  };
346
738
 
347
- // src/utils/tabindex_loan.ts
348
- var TabindexLoan = class {
739
+ function queryOpenDescendants(root, selector) {
740
+ const matches = [];
741
+ const roots = [root];
742
+ for (const scope of roots) {
743
+ matches.push(...scope.querySelectorAll(selector));
744
+ if (scope.nodeType === Node.ELEMENT_NODE) {
745
+ const shadow = scope.shadowRoot;
746
+ if (shadow) roots.push(shadow);
747
+ }
748
+ for (const element of scope.querySelectorAll("*")) {
749
+ if (element.shadowRoot) roots.push(element.shadowRoot);
750
+ }
751
+ }
752
+ return matches;
753
+ }
754
+
755
+ var TabindexLoan = class _TabindexLoan {
756
+ static #registry = sharedRegistry(
757
+ "stimeo-ui.tabindex-loan.registry.v1",
758
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Set() })
759
+ );
349
760
  #value;
761
+ #record;
350
762
  #lent = /* @__PURE__ */ new Set();
351
- /** Returns live loans before Turbo can copy them into its page snapshot. */
352
- #beforeCache = new BeforeCacheReset(() => this.returnAll());
353
- /**
354
- * @param value - the `tabindex` to lend. `"-1"` (the default) is
355
- * programmatically focusable but not a Tab stop; `"0"` is a real Tab stop,
356
- * which a scroll region with no focusable content of its own needs.
357
- */
358
- constructor(value = "-1") {
763
+ constructor(value, owner) {
359
764
  this.#value = value;
765
+ this.#record = `data-${owner}-tabindex-loan`;
766
+ _TabindexLoan.#registry.names.add(this.#record);
360
767
  }
361
- /** Lends `element` the value; no-ops when it already carries a `tabindex`. */
362
768
  lend(element) {
363
- if (element.hasAttribute("tabindex")) return;
364
- element.setAttribute("tabindex", this.#value);
769
+ const stack = this.#stack(element);
770
+ const current = element.getAttribute("tabindex");
771
+ const held = stack.find((hold) => hold.loan === this) ?? stack.find(
772
+ (hold) => hold.loan === null && hold.record === this.#record && hold.value === this.#value
773
+ );
774
+ const linked = stack.at(-1)?.value === current;
775
+ if (!held && current !== null && !linked) return;
365
776
  this.#lent.add(element);
366
- this.#beforeCache.activate();
777
+ if (held) held.loan = this;
778
+ else stack.push({ loan: this, record: this.#record, value: this.#value, linked });
779
+ this.#mark(element, stack);
780
+ if (current === null || !held && current !== this.#value) {
781
+ element.setAttribute("tabindex", this.#value);
782
+ }
367
783
  }
368
- /** Takes back every loan whose value is still the one that was lent. */
369
784
  returnAll() {
370
785
  for (const element of this.#lent) {
371
- if (element.getAttribute("tabindex") === this.#value) element.removeAttribute("tabindex");
786
+ const stack = this.#stack(element);
787
+ const hold = stack.find((held) => held.loan === this);
788
+ if (hold) this.#release(element, stack, hold);
372
789
  }
373
790
  this.#lent.clear();
374
- this.#beforeCache.deactivate();
791
+ }
792
+ reclaim(element) {
793
+ if (element.getAttribute(this.#record) !== this.#value) return;
794
+ const stack = this.#stack(element);
795
+ const hold = stack.find((held) => held.loan === null && held.record === this.#record);
796
+ if (hold && !element.matches(":focus")) this.#release(element, stack, hold);
797
+ }
798
+ reclaimWithin(root) {
799
+ for (const element of queryOpenDescendants(root, `[${this.#record}]`)) {
800
+ this.reclaim(element);
801
+ }
802
+ }
803
+ #stack(element) {
804
+ const stacks = _TabindexLoan.#registry.stacks;
805
+ const stack = stacks.get(element) ?? this.#seed(element);
806
+ stacks.set(element, stack);
807
+ return stack;
808
+ }
809
+ #seed(element) {
810
+ const current = element.getAttribute("tabindex");
811
+ const holds = [];
812
+ for (const record of _TabindexLoan.#registry.names) {
813
+ const value = element.getAttribute(record);
814
+ if (value !== null) holds.push({ loan: null, record, value, linked: true });
815
+ }
816
+ holds.sort((one, other) => Number(one.value === current) - Number(other.value === current));
817
+ for (const [index, hold] of holds.entries()) hold.linked = index > 0;
818
+ return holds;
819
+ }
820
+ #release(element, stack, hold) {
821
+ const current = element.getAttribute("tabindex");
822
+ const index = stack.indexOf(hold);
823
+ const owns = current === hold.value && !stack.slice(index + 1).some((above2) => above2.value === current);
824
+ stack.splice(index, 1);
825
+ const above = stack[index];
826
+ if (above) above.linked &&= hold.linked;
827
+ this.#mark(element, stack);
828
+ if (!owns) return;
829
+ const below = hold.linked ? stack[index - 1] : void 0;
830
+ if (!below) element.removeAttribute("tabindex");
831
+ else if (below.value !== current) element.setAttribute("tabindex", below.value);
832
+ }
833
+ #mark(element, stack) {
834
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
835
+ for (const hold of stack) records.set(hold.record, hold.value);
836
+ for (const [name, recorded] of records) {
837
+ if (element.getAttribute(name) === recorded) continue;
838
+ if (recorded === null) element.removeAttribute(name);
839
+ else element.setAttribute(name, recorded);
840
+ }
841
+ }
842
+ };
843
+
844
+ function targetSelector(identifier, name) {
845
+ return `[data-${identifier}-target~="${name}"]`;
846
+ }
847
+
848
+ function cloneTemplateRoot(template) {
849
+ const root = template.content.firstElementChild;
850
+ return root instanceof HTMLElement ? root.cloneNode(true) : null;
851
+ }
852
+ var TemplateRow = class {
853
+ #options;
854
+ #warned = false;
855
+ constructor(options) {
856
+ this.#options = options;
857
+ }
858
+ selector(name) {
859
+ return targetSelector(this.#options.identifier, name);
860
+ }
861
+ connect() {
862
+ this.#warned = false;
863
+ }
864
+ report(missing) {
865
+ return this.#say(`lacks ${missing}`);
866
+ }
867
+ #say(problem) {
868
+ if (this.#warned) return null;
869
+ this.#warned = true;
870
+ const { identifier, outcome, noun } = this.#options;
871
+ console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
872
+ return null;
873
+ }
874
+ instantiate(template, values) {
875
+ const { root: rootName, required, optional, button: buttonName } = this.#options;
876
+ const root = cloneTemplateRoot(template);
877
+ if (!root?.matches(this.selector(rootName))) {
878
+ return this.report(`${this.#article(rootName)} "${rootName}" root`);
879
+ }
880
+ const count = template.content.children.length;
881
+ if (count !== 1) {
882
+ return this.#say(`holds ${count} elements, and the row has to be its only one`);
883
+ }
884
+ const slots = {};
885
+ for (const name2 of required) {
886
+ const part = this.#resolve(root, name2);
887
+ if (!part) return this.report(`${this.#article(name2)} "${name2}" target`);
888
+ slots[name2] = part;
889
+ }
890
+ for (const name2 of optional ?? []) slots[name2] = this.#resolve(root, name2);
891
+ const named = this.#resolve(root, buttonName);
892
+ if (!(named instanceof HTMLButtonElement)) {
893
+ return this.report(`${this.#article(buttonName)} "${buttonName}" target <button>`);
894
+ }
895
+ const button = named;
896
+ const name = button.getAttribute("aria-label")?.trim() ?? "";
897
+ if (name === "") return this.report(`a non-empty aria-label on its "${buttonName}" target`);
898
+ button.setAttribute("aria-label", fillTemplate(name, values));
899
+ return { root, slots, button };
900
+ }
901
+ #resolve(root, name) {
902
+ return matchingPart(root, this.selector(name));
903
+ }
904
+ #article(name) {
905
+ return /^[aeiou]/i.test(name) ? "an" : "a";
375
906
  }
376
907
  };
377
908
 
378
- // src/controllers/multi_select_controller.ts
379
- var MultiSelectController = class extends Controller {
909
+ var MultiSelectController = class _MultiSelectController extends Controller {
910
+ #numbers = new NumberValueReader();
911
+ #moves = new MoveCounter();
380
912
  static targets = [
381
913
  "input",
382
914
  "list",
@@ -386,7 +918,8 @@ var MultiSelectController = class extends Controller {
386
918
  "tagTemplate",
387
919
  "label",
388
920
  "remove",
389
- "fields"
921
+ "fields",
922
+ "empty"
390
923
  ];
391
924
  static values = {
392
925
  max: { type: Number, default: 0 },
@@ -395,46 +928,46 @@ var MultiSelectController = class extends Controller {
395
928
  announceText: { type: String, default: "" },
396
929
  announceRemovedText: { type: String, default: "" }
397
930
  };
931
+ static valueConstraints = {
932
+ max: NUMBER_BOUNDS.finite
933
+ };
398
934
  static actions = ["close", "filter", "onKeydown", "open", "toggleOption"];
399
935
  static events = ["change", "filter", "reconcile"];
400
- /** Stable id of the active option; the current target is resolved from the DOM. */
401
936
  #activeOptionId = null;
402
- /** Whether the root borrowed a tab stop to catch focus, so teardown can undo it. */
403
- #tabindex = new TabindexLoan();
404
- /** Last reconciled selection, used to distinguish state changes from derived-DOM repair. */
937
+ #tabindex = new TabindexLoan("-1", this.identifier);
405
938
  #selectionValues = [];
406
- /** Whether initial normalization finished, so a fields callback cannot mirror stale state. */
407
939
  #connected = false;
408
- /** Collapses one batch of target callbacks into a single final-DOM reconciliation. */
409
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());
410
- /** Absorbs the browser's redundant final input after compositionend. */
411
- #ignorePostCompositionInput = false;
412
- /** Owns IME lifecycle state; confirmed text emits one filter result. */
940
+ #emptyRegion = new StateRegions({ whenTrue: () => this.emptyTargets }, this.identifier);
941
+ #listHidden = new AttributeLease("hidden", this.identifier);
942
+ #expanded = new AttributeLease("aria-expanded", this.identifier);
943
+ #activeDescendant = new AttributeLease(
944
+ "aria-activedescendant",
945
+ this.identifier
946
+ );
947
+ #open = false;
948
+ #openList = null;
949
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileOptions());
413
950
  #composition = new CompositionTracker({
414
- onStart: () => {
415
- this.#ignorePostCompositionInput = false;
416
- },
417
- onEnd: () => {
418
- this.#ignorePostCompositionInput = true;
419
- queueMicrotask(() => {
420
- this.#ignorePostCompositionInput = false;
421
- });
422
- this.filter();
423
- }
951
+ onEnd: () => this.filter()
952
+ });
953
+ #rows = new TemplateRow({
954
+ identifier: this.identifier,
955
+ root: "tag",
956
+ required: ["label"],
957
+ button: "remove",
958
+ outcome: "changed no selection",
959
+ noun: "chip template"
424
960
  });
425
- /** Whether this connection already reported its unusable chip template. */
426
- #warnedTemplate = false;
427
- /** Shared delegated interaction for the replaceable row of removable chips. */
428
961
  #chipRow = new ChipRow({
429
962
  directionElement: this.element,
430
963
  getItems: () => this.tagTargets,
431
- getButton: (tag) => tag.querySelector('button[data-stimeo--multi-select-target~="remove"]'),
964
+ getButton: (tag) => matchingPart(tag, `button${this.#rows.selector("remove")}`),
432
965
  onRemove: (index) => this.#removeTagAt(index),
433
966
  focusAfterEnd: () => this.#focusInput()
434
967
  });
435
- /** Starts closed, syncs chips for any pre-selected options, and listens out. */
436
968
  connect() {
437
- this.#warnedTemplate = false;
969
+ this.#tabindex.reclaim(this.element);
970
+ this.#rows.connect();
438
971
  this.#normalizeSelection([]);
439
972
  this.close();
440
973
  if (this.hasTagsTarget) {
@@ -445,86 +978,81 @@ var MultiSelectController = class extends Controller {
445
978
  this.#syncFields();
446
979
  document.addEventListener("click", this.#onOutsideClick, true);
447
980
  this.#connected = true;
448
- this.#reconcile.activate();
981
+ this.#reconcile.observe(this.element);
449
982
  }
450
- /**
451
- * Derives the chips from the selected options, idempotently: a Turbo Drive cache
452
- * restore or morph can re-connect with chips already in the DOM, so they are
453
- * cleared before deriving afresh to avoid duplicates.
454
- */
455
983
  #rebuildTags() {
456
984
  if (!this.hasTagsTarget || !this.hasTagTemplateTarget) return;
457
- const fragments = [];
985
+ const chips = [];
458
986
  for (const option of this.#selectedOptions) {
459
- const fragment = this.#buildTag(option);
460
- if (!fragment) return;
461
- fragments.push(fragment);
987
+ const chip = this.#buildTag(option);
988
+ if (!chip) return;
989
+ chips.push(chip);
462
990
  }
463
991
  for (const tag of this.tagTargets) tag.remove();
464
- this.tagsTarget.append(...fragments);
992
+ this.tagsTarget.append(...chips);
465
993
  this.#chipRow.ensureTabStop();
466
994
  }
467
- /** Tears down document and chip listeners on disconnect (Turbo included). */
468
995
  disconnect() {
469
996
  this.#connected = false;
470
- this.#reconcile.cancel();
997
+ this.#reconcile.disconnect();
471
998
  this.#composition.disconnect();
472
- this.#ignorePostCompositionInput = false;
473
999
  this.#chipRow.disconnect();
474
1000
  document.removeEventListener("click", this.#onOutsideClick, true);
475
1001
  this.#releaseTabindex();
476
1002
  }
477
- /** Reconciles active state after an option target is added at runtime. */
478
1003
  optionTargetConnected() {
479
1004
  this.#scheduleOptionReconcile();
480
1005
  }
481
- /** Cleans a removed target and reconciles active state against the surviving DOM. */
482
1006
  optionTargetDisconnected(option) {
483
1007
  option.removeAttribute("data-active");
484
1008
  this.#scheduleOptionReconcile();
485
1009
  }
486
- /** Rebinds delegated chip interaction when Turbo replaces the tags container. */
1010
+ emptyTargetConnected() {
1011
+ if (this.#connected) this.#reflectEmpty();
1012
+ }
487
1013
  tagsTargetConnected(tags) {
488
1014
  this.#chipRow.connect(tags);
489
1015
  this.#scheduleOptionReconcile();
490
1016
  }
491
- /** Releases only the row that actually disconnected, never a newer replacement. */
492
1017
  tagsTargetDisconnected(tags) {
493
1018
  this.#chipRow.disconnect(tags);
494
1019
  this.#scheduleOptionReconcile();
495
1020
  }
496
- /** Seeds a fields target inserted after connect from the current selection. */
497
1021
  fieldsTargetConnected() {
498
1022
  if (this.#connected) this.#syncFields();
499
1023
  }
500
- /** Reconciles a runtime max change, including dropping any newly invalid overflow. */
1024
+ fieldsTargetDisconnected() {
1025
+ this.#scheduleOptionReconcile();
1026
+ }
1027
+ listTargetConnected(_list) {
1028
+ if (!this.#connected) return;
1029
+ this.#reconcileOpenState();
1030
+ this.#scheduleOptionReconcile();
1031
+ }
1032
+ listTargetDisconnected(list) {
1033
+ if (!this.listTargets.includes(list)) {
1034
+ if (list === this.#openList) {
1035
+ this.#adoptPageClose();
1036
+ this.#openList = null;
1037
+ }
1038
+ this.#listHidden.return(list);
1039
+ }
1040
+ if (!this.#connected) return;
1041
+ this.#reconcileOpenState();
1042
+ this.#scheduleOptionReconcile();
1043
+ }
501
1044
  maxValueChanged() {
502
1045
  this.#scheduleOptionReconcile();
503
1046
  }
504
- /** Rebuilds submitted fields when their public name changes at runtime. */
505
1047
  nameValueChanged() {
506
1048
  this.#scheduleOptionReconcile();
507
1049
  }
508
- /** Rebuilds submitted fields when their associated form changes at runtime. */
509
1050
  formValueChanged() {
510
1051
  this.#scheduleOptionReconcile();
511
1052
  }
512
- /** Schedules one reconciliation after all callbacks in the mutation batch. */
513
1053
  #scheduleOptionReconcile() {
514
1054
  this.#reconcile.schedule();
515
1055
  }
516
- /**
517
- * Keeps a surviving/same-id active target, otherwise falls back to the first
518
- * visible one — and brings the derived state back in line with the new option set.
519
- *
520
- * The baseline pass fills in any missing `aria-selected`, and the chips and
521
- * hidden fields are re-derived from it, because the options are the truth source
522
- * for the selection. The chips are rebuilt **only when the selected value set
523
- * actually moved**: the rebuild removes and recreates every chip, so running it
524
- * for an unrelated option would drop focus from a chip's remove button to
525
- * `<body>`, losing the keyboard user's place for something that did not concern
526
- * them.
527
- */
528
1056
  #reconcileOptions() {
529
1057
  const visible = this.#visibleOptions;
530
1058
  const active = this.#activeOption;
@@ -535,7 +1063,7 @@ var MultiSelectController = class extends Controller {
535
1063
  const previousLabels = new Map(
536
1064
  this.tagTargets.map((tag) => [
537
1065
  tag.dataset.value ?? "",
538
- (tag.querySelector('[data-stimeo--multi-select-target~="label"]')?.textContent ?? "").trim()
1066
+ readLabel(matchingPart(tag, this.#rows.selector("label")))
539
1067
  ])
540
1068
  );
541
1069
  const priority = [
@@ -553,24 +1081,23 @@ var MultiSelectController = class extends Controller {
553
1081
  const changed = !this.#sameSelection(previous, values);
554
1082
  this.#selectionValues = values;
555
1083
  if (!changed) return;
1084
+ const token = this.#moves.record();
556
1085
  const options = new Map(selected.map((option) => [this.#optionValue(option), option]));
557
1086
  for (const value of previous) {
1087
+ if (!this.#moves.isLatest(token)) return;
558
1088
  if (!values.includes(value)) {
559
1089
  this.#announceTransition(false, previousLabels.get(value) || value, value, values.length);
560
1090
  }
561
1091
  }
562
1092
  for (const value of values) {
1093
+ if (!this.#moves.isLatest(token)) return;
563
1094
  if (previous.includes(value)) continue;
564
1095
  const option = options.get(value);
565
1096
  this.#announceTransition(true, this.#optionLabel(option), value, values.length);
566
1097
  }
1098
+ if (!this.#moves.isLatest(token)) return;
567
1099
  this.dispatch("reconcile", { detail: { values } });
568
1100
  }
569
- /**
570
- * Gives every option an explicit `aria-selected` and enforces the current cap.
571
- * Priority preserves existing chip order at runtime; a fresh connection passes
572
- * no priority, so deterministic option DOM order chooses the initial survivors.
573
- */
574
1101
  #normalizeSelection(priority) {
575
1102
  const selected = this.optionTargets.filter(
576
1103
  (option) => option.getAttribute("aria-selected") === "true"
@@ -596,32 +1123,39 @@ var MultiSelectController = class extends Controller {
596
1123
  option.setAttribute("aria-selected", String(kept.has(option)));
597
1124
  }
598
1125
  }
599
- /**
600
- * Tracks an input added initially or after connect, and makes it describe the
601
- * widget that is actually on screen: a swapped-in input arrives with the
602
- * authored ARIA of a fresh node while this controller still holds the popup
603
- * state, and the open path cannot repair that (it seeds an active option only
604
- * when there is none), so a live list would go unannounced.
605
- */
606
1126
  inputTargetConnected(input) {
607
1127
  this.#composition.observe(input);
608
- input.setAttribute("aria-expanded", String(!this.#isClosed));
609
- const active = this.#activeOption;
610
- if (active) input.setAttribute("aria-activedescendant", ensureId(active, "stimeo-ms-opt"));
611
- else input.removeAttribute("aria-activedescendant");
1128
+ this.#describeInput(input);
612
1129
  }
613
- /** Removes composition listeners when the active input is replaced or removed. */
614
1130
  inputTargetDisconnected(input) {
615
1131
  this.#composition.unobserve(input);
616
- this.#ignorePostCompositionInput = false;
1132
+ if (!this.inputTargets.includes(input)) {
1133
+ this.#expanded.return(input);
1134
+ this.#activeDescendant.return(input);
1135
+ }
1136
+ if (this.#connected && this.hasInputTarget) this.#describeInput(this.inputTarget);
1137
+ }
1138
+ #describeInput(input) {
1139
+ this.#expanded.write(input, String(!this.#isClosed));
1140
+ const active = this.#activeOption;
1141
+ this.#activeDescendant.write(input, active ? ensureId(active, "stimeo-ms-opt") : null);
1142
+ }
1143
+ #reconcileOpenState() {
1144
+ if (!this.hasListTarget) return;
1145
+ this.#adoptPageClose();
1146
+ const list = this.listTarget;
1147
+ if (list !== this.#openList) {
1148
+ this.#showList(list, this.#open);
1149
+ this.#openList = list;
1150
+ }
1151
+ if (this.hasInputTarget) this.#expanded.write(this.inputTarget, String(!list.hidden));
1152
+ }
1153
+ #adoptPageClose() {
1154
+ if (this.#openList?.hidden) this.#open = false;
617
1155
  }
618
- /** Filters confirmed input text, opens, and re-seeds the active option. */
619
1156
  filter(event) {
620
1157
  if (!this.hasInputTarget) return;
621
- if (event && this.#ignorePostCompositionInput) {
622
- this.#ignorePostCompositionInput = false;
623
- return;
624
- }
1158
+ if (event && this.#composition.consumesConfirmedInput(event)) return;
625
1159
  if (this.#composition.isComposing(event)) return;
626
1160
  const query = this.inputTarget.value.trim().toLowerCase();
627
1161
  for (const option of this.optionTargets) {
@@ -630,45 +1164,31 @@ var MultiSelectController = class extends Controller {
630
1164
  }
631
1165
  this.open();
632
1166
  const visible = this.#visibleOptions;
633
- this.#reflectEmpty();
634
1167
  this.#setActive(visible[0] ?? null);
635
1168
  this.dispatch("filter", { detail: { query } });
636
1169
  }
637
- /**
638
- * Opens the list and activates the first visible option when none is active.
639
- *
640
- * Needs the input, which owns `aria-expanded` and `aria-activedescendant`: a
641
- * list shown without one is a popup no assistive technology is told about. So
642
- * opening is skipped entirely, where {@link close} still closes.
643
- */
644
1170
  open() {
645
1171
  if (!this.hasListTarget || !this.hasInputTarget) {
646
1172
  this.#reflectEmpty();
647
1173
  return;
648
1174
  }
649
- this.listTarget.hidden = false;
650
- this.inputTarget.setAttribute("aria-expanded", "true");
1175
+ this.#showList(this.listTarget, true);
1176
+ this.#open = true;
1177
+ this.#openList = this.listTarget;
1178
+ this.#expanded.write(this.inputTarget, "true");
651
1179
  if (!this.#activeOption) this.#setActive(this.#visibleOptions[0] ?? null);
652
1180
  this.#reflectEmpty();
653
1181
  }
654
- /**
655
- * Closes the list and clears the active option.
656
- *
657
- * Survives a missing input in both directions. `connect()` calls this second,
658
- * so dereferencing the input here would throw before the chips, the roving
659
- * seed, the chip listeners, the hidden fields and the outside-click listener —
660
- * and Stimulus keeps the controller alive after that throw, so none of them
661
- * would ever run and the selection would silently stop submitting. An input
662
- * removed while the list is open must still let it come down *and* forget its
663
- * active option, so only the `aria-expanded` write is guarded.
664
- */
665
1182
  close() {
666
- if (this.hasListTarget) this.listTarget.hidden = true;
1183
+ this.#open = false;
1184
+ if (this.hasListTarget) {
1185
+ this.#showList(this.listTarget, false);
1186
+ this.#openList = this.listTarget;
1187
+ }
667
1188
  this.#setActive(null);
668
- if (this.hasInputTarget) this.inputTarget.setAttribute("aria-expanded", "false");
1189
+ if (this.hasInputTarget) this.#expanded.write(this.inputTarget, "false");
669
1190
  this.#reflectEmpty();
670
1191
  }
671
- /** Routes input keyboard interaction per the multi-select combobox model. */
672
1192
  onKeydown(event) {
673
1193
  if (event.defaultPrevented) return;
674
1194
  if (isReservedArrowChord(event)) return;
@@ -733,39 +1253,20 @@ var MultiSelectController = class extends Controller {
733
1253
  break;
734
1254
  }
735
1255
  }
736
- /**
737
- * Toggles the clicked option's selection. Bound via `data-action`. Focus is
738
- * re-homed to the input afterwards: options are non-focusable, so the click blurs
739
- * the input to `body` — and with the list deliberately staying open, every
740
- * keyboard affordance (Escape, arrows, typing) is bound to the input and would
741
- * otherwise go dead until the user clicks back in ("focus stays on the input").
742
- */
743
- toggleOption(event) {
744
- const option = event.currentTarget.closest('[role="option"]');
1256
+ toggleOption(source) {
1257
+ const { event, host, origin, reason } = actionSource(source);
1258
+ if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
1259
+ return;
1260
+ const option = host?.closest(targetSelector(this.identifier, "option"));
745
1261
  if (!option || !this.optionTargets.includes(option)) return;
746
- this.#toggleSelection(option);
747
- this.#focusInput();
748
- }
749
- /**
750
- * Re-homes focus to the input, or leaves it where it is when there is none.
751
- *
752
- * All three callers run *after* an option or a chip already took focus, and all
753
- * three are reachable without an input — options and chips carry their own
754
- * `data-action`. Throwing here would leave the chip removed but focus stranded
755
- * on a detached button.
756
- */
1262
+ const focus = event !== null || this.element.contains(document.activeElement);
1263
+ if (!this.#toggleSelection(option, reason)) return;
1264
+ if (focus && (event !== null || this.element.contains(document.activeElement)))
1265
+ this.#focusInput();
1266
+ }
757
1267
  #focusInput() {
758
1268
  if (this.hasInputTarget) this.inputTarget.focus();
759
1269
  }
760
- /**
761
- * Re-homes focus after the last chip was removed: to the input, else the root.
762
- *
763
- * Unlike the other {@link #focusInput} callers, the element that held focus has
764
- * just left the DOM, so "leave it alone" is not an option — the browser already
765
- * dropped it to `<body>`. The root borrows a `tabindex="-1"` just-in-time (not a
766
- * Tab stop, handed back before Turbo caches or on teardown). Focus that landed
767
- * on a real element is left alone, so a chip removed out of band never steals it.
768
- */
769
1270
  #focusAfterLastTag() {
770
1271
  if (this.hasInputTarget) {
771
1272
  this.inputTarget.focus();
@@ -779,15 +1280,9 @@ var MultiSelectController = class extends Controller {
779
1280
  this.#tabindex.lend(this.element);
780
1281
  this.element.focus();
781
1282
  }
782
- /**
783
- * Returns the borrowed tab stop. Owning the borrow is not enough — the value
784
- * has to still be the one this instance wrote, since a consumer that changed it
785
- * afterwards owns it now.
786
- */
787
1283
  #releaseTabindex() {
788
1284
  this.#tabindex.returnAll();
789
1285
  }
790
- /** Moves the active option by `delta` among visible options, wrapping. */
791
1286
  #moveActive(delta) {
792
1287
  const visible = this.#visibleOptions;
793
1288
  const active = this.#activeOption;
@@ -796,128 +1291,70 @@ var MultiSelectController = class extends Controller {
796
1291
  const next = (candidate + visible.length) % visible.length;
797
1292
  this.#setActive(visible[next] ?? null);
798
1293
  }
799
- /** Selects/deselects `option`, honoring `max`, and syncs chip + announcement. */
800
- #toggleSelection(option) {
1294
+ #toggleSelection(option, reason = "user") {
801
1295
  const selected = option.getAttribute("aria-selected") === "true";
802
1296
  const limit = this.#selectionLimit;
803
1297
  if (!selected && limit > 0 && this.#selectedOptions.length >= limit) {
804
- return;
1298
+ return true;
805
1299
  }
806
1300
  if (selected) {
807
1301
  option.setAttribute("aria-selected", "false");
808
1302
  this.#removeTagFor(option);
809
1303
  } else {
810
- if (this.hasTagTemplateTarget && !this.#appendTag(option)) return;
1304
+ if (this.hasTagTemplateTarget && !this.#appendTag(option)) return true;
811
1305
  option.setAttribute("aria-selected", "true");
812
1306
  }
813
1307
  this.#refreshRoving();
814
- this.#syncFields();
815
- const values = this.#values;
816
- this.#selectionValues = values;
817
- this.#announceTransition(
1308
+ return this.#publishSelection(
818
1309
  !selected,
819
1310
  this.#optionLabel(option),
820
1311
  this.#optionValue(option),
821
- values.length
1312
+ reason
822
1313
  );
823
- this.dispatch("change", { detail: { values } });
824
- }
825
- /**
826
- * Re-reads each chip's label from its option, in place.
827
- *
828
- * The value order alone does not say the chips are still correct: a server can
829
- * re-render the same candidate with a new label ("Apple" → "Green Apple"), and
830
- * the chip text and its `Remove {label}` name are both derived from the option.
831
- * Updating them here keeps the rebuild — which would drop focus — for the case
832
- * that actually needs it, a changed selection.
833
- */
1314
+ }
834
1315
  #refreshTagLabels(selected) {
835
1316
  const options = new Map(selected.map((option) => [this.#optionValue(option), option]));
836
1317
  for (const tag of this.tagTargets) {
837
1318
  const option = options.get(tag.dataset.value ?? "");
838
1319
  const text = this.#optionLabel(option);
839
- const label = tag.querySelector('[data-stimeo--multi-select-target~="label"]');
840
- const button = tag.querySelector(
841
- 'button[data-stimeo--multi-select-target~="remove"]'
842
- );
1320
+ const label = matchingPart(tag, this.#rows.selector("label"));
1321
+ const button = matchingPart(tag, `button${this.#rows.selector("remove")}`);
843
1322
  const name = this.#removeName(text, this.#optionValue(option));
844
1323
  if (!label || !button || !name) continue;
845
- if (label.textContent !== text) label.textContent = text;
1324
+ if (readLabel(label) !== text) writeLabel(label, text);
846
1325
  if (button.getAttribute("aria-label") !== name) button.setAttribute("aria-label", name);
847
1326
  }
848
1327
  }
849
- /** Builds one chip from the template for `option`. */
850
1328
  #appendTag(option) {
851
1329
  if (!this.hasTagsTarget) {
852
- this.#warnTemplate('a "tags" target to append the chip to');
1330
+ this.#rows.report('a "tags" target to append the chip to');
853
1331
  return false;
854
1332
  }
855
- const fragment = this.#buildTag(option);
856
- if (!fragment) return false;
857
- this.tagsTarget.appendChild(fragment);
1333
+ const chip = this.#buildTag(option);
1334
+ if (!chip) return false;
1335
+ this.tagsTarget.appendChild(chip);
858
1336
  return true;
859
1337
  }
860
- /**
861
- * Builds one fully named chip without mutating the live tag row, or `null`
862
- * when the authored template cannot produce one. Both callers establish the
863
- * template first: a field authored without `tagTemplate` renders no chips at
864
- * all — a supported configuration — and never reaches here.
865
- */
866
1338
  #buildTag(option) {
867
- const fragment = this.tagTemplateTarget.content.cloneNode(true);
868
- const tag = fragment.querySelector('[data-stimeo--multi-select-target~="tag"]');
869
- const label = fragment.querySelector(
870
- '[data-stimeo--multi-select-target~="label"]'
871
- );
872
- const button = fragment.querySelector(
873
- 'button[data-stimeo--multi-select-target~="remove"]'
874
- );
875
1339
  const text = this.#optionLabel(option);
876
1340
  const value = this.#optionValue(option);
877
- const removeName = button?.getAttribute("aria-label")?.trim() ?? "";
878
- if (!tag) return this.#warnTemplate('a "tag" target');
879
- if (!label) return this.#warnTemplate('a "label" target');
880
- if (!button) return this.#warnTemplate('a "remove" target <button>');
881
- if (removeName === "") {
882
- return this.#warnTemplate('a non-empty aria-label on its "remove" target');
883
- }
884
- tag.dataset.value = value;
885
- label.textContent = text;
886
- button.setAttribute("aria-label", fillTemplate(removeName, { label: text, value }));
887
- button.tabIndex = -1;
888
- return fragment;
889
- }
890
- /**
891
- * Reports an unusable chip template to the author, once per connection.
892
- *
893
- * The selection itself stays untouched — no `aria-selected`, chip, hidden
894
- * field, announcement, or event moves. Without this line the only symptom is
895
- * a listbox whose options refuse to select, and the two causes the Inspector
896
- * cannot see statically (a name that renders empty from a missing
897
- * translation, a server-rendered template) would have no diagnostic anywhere.
898
- */
899
- #warnTemplate(missing) {
900
- if (!this.#warnedTemplate) {
901
- this.#warnedTemplate = true;
902
- console.warn(
903
- `Stimeo UI: "${this.identifier}" changed no selection because its chip template lacks ${missing}.`
904
- );
905
- }
906
- return null;
1341
+ const row = this.#rows.instantiate(this.tagTemplateTarget, { label: text, value });
1342
+ if (!row) return null;
1343
+ row.root.dataset.value = value;
1344
+ writeLabel(row.slots.label, text);
1345
+ row.button.tabIndex = -1;
1346
+ return row.root;
907
1347
  }
908
- /** Expands the current template's localized remove-button name. */
909
1348
  #removeName(label, value) {
910
1349
  if (!this.hasTagTemplateTarget) return null;
911
- const template = this.tagTemplateTarget.content.querySelector('button[data-stimeo--multi-select-target~="remove"]')?.getAttribute("aria-label")?.trim();
1350
+ const template = this.tagTemplateTarget.content.querySelector(`button${this.#rows.selector("remove")}`)?.getAttribute("aria-label")?.trim();
912
1351
  return template ? fillTemplate(template, { label, value }) : null;
913
1352
  }
914
- /** Removes the chip mirroring `option`, if present. */
915
1353
  #removeTagFor(option) {
916
1354
  const value = this.#optionValue(option);
917
1355
  const tag = this.tagTargets.find((candidate) => candidate.dataset.value === value);
918
1356
  tag?.remove();
919
1357
  }
920
- /** Removes chip `index` and deselects its option, re-homing focus. */
921
1358
  #removeTagAt(index, focus = "neighbor") {
922
1359
  const tag = this.tagTargets[index];
923
1360
  if (!tag) return;
@@ -926,31 +1363,13 @@ var MultiSelectController = class extends Controller {
926
1363
  if (option) option.setAttribute("aria-selected", "false");
927
1364
  tag.remove();
928
1365
  this.#refreshRoving();
929
- this.#syncFields();
930
- const values = this.#values;
931
- this.#selectionValues = values;
932
- this.#announceTransition(
933
- false,
934
- option ? this.#optionLabel(option) : value,
935
- value,
936
- values.length
937
- );
938
- this.dispatch("change", { detail: { values } });
1366
+ if (!this.#publishSelection(false, option ? this.#optionLabel(option) : value, value)) return;
939
1367
  if (focus === "input") {
940
1368
  this.#focusInput();
941
1369
  return;
942
1370
  }
943
1371
  if (!this.#chipRow.focusAfterRemoval(index)) this.#focusAfterLastTag();
944
1372
  }
945
- /**
946
- * Marks `option` active via `data-active` and the input's
947
- * `aria-activedescendant` (the attribute is removed, not emptied, when null).
948
- *
949
- * The state half runs even with no input, so a `close()` that cannot touch ARIA
950
- * still clears it: {@link open} seeds an active option only when there is none,
951
- * so a stale one makes the next open skip the seeding and a replacement input
952
- * gets no `aria-activedescendant` at all.
953
- */
954
1373
  #setActive(option) {
955
1374
  const activeId = option ? ensureId(option, "stimeo-ms-opt") : null;
956
1375
  this.#activeOptionId = activeId;
@@ -958,14 +1377,8 @@ var MultiSelectController = class extends Controller {
958
1377
  candidate.toggleAttribute("data-active", candidate === option);
959
1378
  }
960
1379
  if (option && this.hasListTarget) scrollOptionIntoView(this.listTarget, option);
961
- if (!this.hasInputTarget) return;
962
- if (activeId !== null) {
963
- this.inputTarget.setAttribute("aria-activedescendant", activeId);
964
- } else {
965
- this.inputTarget.removeAttribute("aria-activedescendant");
966
- }
1380
+ if (this.hasInputTarget) this.#activeDescendant.write(this.inputTarget, activeId);
967
1381
  }
968
- /** Repairs only active identity before a key; the fallback waits for the target callback. */
969
1382
  #reconcileActiveForInteraction() {
970
1383
  const activeId = this.#activeOptionId;
971
1384
  if (activeId === null) return;
@@ -977,88 +1390,84 @@ var MultiSelectController = class extends Controller {
977
1390
  this.#setActive(active);
978
1391
  }
979
1392
  }
980
- /** Reflects whether the open list currently has no visible option targets. */
981
1393
  #reflectEmpty() {
982
- this.element.toggleAttribute(
983
- "data-stimeo--multi-select-empty",
984
- !this.#isClosed && this.#visibleOptions.length === 0
985
- );
1394
+ const empty = !this.#isClosed && this.#visibleOptions.length === 0;
1395
+ this.element.toggleAttribute(`data-${this.identifier}-empty`, empty);
1396
+ this.#emptyRegion.reflect(this.element, empty);
986
1397
  }
987
- /**
988
- * Mirrors the selected values into named hidden inputs under the `fields`
989
- * target so the selection submits with a normal form (parity with tags-input).
990
- * No-ops without a `fields` target. When the
991
- * `form` value is set, each input gets a matching `form` attribute so the picker
992
- * can submit with a `<form>` it lives outside of.
993
- */
994
1398
  #syncFields() {
995
- if (!this.hasFieldsTarget) return;
996
- this.fieldsTarget.replaceChildren(
997
- ...this.#values.map((value) => {
998
- const input = document.createElement("input");
999
- input.type = "hidden";
1000
- input.name = this.nameValue;
1001
- input.value = value;
1002
- if (this.hasFormValue && this.formValue !== "") input.setAttribute("form", this.formValue);
1003
- return input;
1004
- })
1005
- );
1399
+ if (!this.hasFieldsTarget) return false;
1400
+ const options = { name: this.nameValue, form: this.formValue };
1401
+ return writeFields(this.fieldsTarget, this.#values, options);
1402
+ }
1403
+ #publishSelection(selected, label, value, reason = "user") {
1404
+ const values = this.#values;
1405
+ const changed = !this.#sameSelection(this.#selectionValues, values);
1406
+ const fieldChanged = this.#syncFields();
1407
+ this.#selectionValues = values;
1408
+ if (!changed) return true;
1409
+ const token = this.#moves.record();
1410
+ if (fieldChanged) commitField(this.fieldsTarget);
1411
+ if (!this.#moves.isLatest(token)) return false;
1412
+ this.#announceTransition(selected, label, value, values.length);
1413
+ if (!this.#moves.isLatest(token)) return false;
1414
+ this.dispatch("change", { detail: { values, reason } });
1415
+ return this.#moves.isLatest(token);
1006
1416
  }
1007
- /** Keeps exactly one chip remove button tabbable after the set changes. */
1008
1417
  #refreshRoving() {
1009
1418
  this.#chipRow.ensureTabStop();
1010
1419
  }
1011
- /** Sends one localized selection transition through the page's shared announcer. */
1012
1420
  #announceTransition(selected, label, value, count) {
1013
1421
  const template = selected ? this.announceTextValue : this.announceRemovedTextValue;
1014
1422
  announce(fillTemplate(template, { label, value, count }));
1015
1423
  }
1016
- /** Whether two arrays represent the same selection set, independent of DOM order. */
1017
1424
  #sameSelection(left, right) {
1018
1425
  if (left.length !== right.length) return false;
1019
1426
  const a = [...left].sort();
1020
1427
  const b = [...right].sort();
1021
1428
  return a.every((value, index) => value === b[index]);
1022
1429
  }
1023
- /** Closes the list on a click outside the controller element. */
1024
1430
  #onOutsideClick = (event) => {
1025
1431
  if (!this.#isClosed && !this.element.contains(event.target)) this.close();
1026
1432
  };
1027
- /** Trimmed visible label of an option. */
1028
1433
  #optionLabel(option) {
1029
1434
  return (option.textContent ?? "").trim();
1030
1435
  }
1031
- /** An option's stable value: its `data-value`, else its display label. */
1032
1436
  #optionValue(option) {
1033
1437
  return option.dataset.value ?? this.#optionLabel(option);
1034
1438
  }
1035
- /** Options not hidden by the current filter. */
1036
1439
  get #visibleOptions() {
1037
1440
  return this.optionTargets.filter((option) => !option.hidden);
1038
1441
  }
1039
- /** Current active target resolved by stable id, never a detached node reference. */
1040
1442
  get #activeOption() {
1041
1443
  return this.optionTargets.find((option) => option.id === this.#activeOptionId) ?? null;
1042
1444
  }
1043
- /** Options currently selected. */
1044
1445
  get #selectedOptions() {
1045
1446
  return this.optionTargets.filter((option) => option.getAttribute("aria-selected") === "true");
1046
1447
  }
1047
- /** Selected values in option order. */
1048
1448
  get #values() {
1049
1449
  return this.#selectedOptions.map((option) => this.#optionValue(option));
1050
1450
  }
1051
- /** Normalized cardinality cap: zero and below are unlimited; a positive value floors, never below 1. */
1052
1451
  get #selectionLimit() {
1053
- if (!Number.isFinite(this.maxValue) || this.maxValue <= 0) return 0;
1054
- return Math.max(1, Math.floor(this.maxValue));
1452
+ if (this.#safeMax <= 0) return 0;
1453
+ return Math.max(1, Math.floor(this.#safeMax));
1454
+ }
1455
+ #showList(list, open) {
1456
+ this.#listHidden.write(list, open ? null : "");
1055
1457
  }
1056
- /** Whether the list is currently hidden. */
1057
1458
  get #isClosed() {
1058
1459
  return !this.hasListTarget || this.listTarget.hidden !== false;
1059
1460
  }
1461
+ get #safeMax() {
1462
+ return this.#numbers.read(
1463
+ this,
1464
+ "max",
1465
+ this.maxValue,
1466
+ _MultiSelectController.values.max.default,
1467
+ _MultiSelectController.valueConstraints.max
1468
+ );
1469
+ }
1060
1470
  };
1061
1471
 
1062
1472
  export { MultiSelectController };
1063
- //# sourceMappingURL=multi_select_controller.js.map
1064
1473
  //# sourceMappingURL=multi_select_controller.js.map