stimeo-ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +27 -11
  3. package/dist/cable/index.d.ts +88 -31
  4. package/dist/cable/index.js +384 -449
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/coerce-JtZVNG5h.d.ts +20 -0
  7. package/dist/controllers/accordion_controller.d.ts +4 -2
  8. package/dist/controllers/accordion_controller.js +174 -55
  9. package/dist/controllers/accordion_controller.js.map +1 -1
  10. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  11. package/dist/controllers/alert_dialog_controller.js +903 -235
  12. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  13. package/dist/controllers/announcer_controller.d.ts +37 -5
  14. package/dist/controllers/announcer_controller.js +238 -205
  15. package/dist/controllers/announcer_controller.js.map +1 -1
  16. package/dist/controllers/aspect_ratio_controller.js +0 -10
  17. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  18. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  19. package/dist/controllers/auto_submit_controller.js +414 -145
  20. package/dist/controllers/auto_submit_controller.js.map +1 -1
  21. package/dist/controllers/avatar_controller.js +185 -107
  22. package/dist/controllers/avatar_controller.js.map +1 -1
  23. package/dist/controllers/breadcrumb_controller.js +0 -130
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +21 -3
  26. package/dist/controllers/bulk_select_controller.js +282 -58
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +38 -16
  29. package/dist/controllers/calendar_controller.js +228 -260
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +43 -12
  32. package/dist/controllers/carousel_controller.js +536 -436
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +44 -7
  35. package/dist/controllers/character_counter_controller.js +493 -207
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +38 -74
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +16 -5
  41. package/dist/controllers/clipboard_controller.js +261 -149
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +17 -5
  44. package/dist/controllers/collapsible_controller.js +304 -120
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +18 -6
  47. package/dist/controllers/color_picker_controller.js +178 -146
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +24 -6
  50. package/dist/controllers/combobox_controller.js +522 -156
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +51 -20
  53. package/dist/controllers/command_palette_controller.js +1156 -395
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +300 -171
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +980 -262
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +80 -18
  62. package/dist/controllers/context_menu_controller.js +346 -153
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +47 -3
  68. package/dist/controllers/countdown_controller.js +281 -158
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -10
  71. package/dist/controllers/currency_input_controller.js +575 -225
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +25 -12
  74. package/dist/controllers/data_grid_controller.js +248 -214
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +42 -14
  77. package/dist/controllers/date_range_picker_controller.js +338 -235
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +17 -6
  80. package/dist/controllers/dialog_controller.js +899 -237
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +25 -8
  83. package/dist/controllers/direct_upload_controller.js +97 -167
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +6 -1
  86. package/dist/controllers/dirty_form_controller.js +72 -69
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +196 -24
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +39 -11
  92. package/dist/controllers/drawer_controller.js +974 -355
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +31 -1
  95. package/dist/controllers/dropdown_controller.js +339 -96
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +315 -88
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +79 -58
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +63 -18
  103. package/dist/controllers/file_dropzone_controller.js +322 -211
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.js +77 -42
  106. package/dist/controllers/filter_controller.js.map +1 -1
  107. package/dist/controllers/flash_controller.d.ts +35 -12
  108. package/dist/controllers/flash_controller.js +238 -347
  109. package/dist/controllers/flash_controller.js.map +1 -1
  110. package/dist/controllers/focus_controller.d.ts +15 -6
  111. package/dist/controllers/focus_controller.js +679 -240
  112. package/dist/controllers/focus_controller.js.map +1 -1
  113. package/dist/controllers/form_field_controller.js +156 -132
  114. package/dist/controllers/form_field_controller.js.map +1 -1
  115. package/dist/controllers/form_validation_controller.d.ts +3 -3
  116. package/dist/controllers/form_validation_controller.js +31 -97
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +47 -14
  119. package/dist/controllers/frame_loading_controller.js +275 -217
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +27 -6
  122. package/dist/controllers/highlight_controller.js +107 -75
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +60 -4
  125. package/dist/controllers/hover_card_controller.js +327 -130
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +48 -6
  128. package/dist/controllers/idle_controller.js +271 -101
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +30 -13
  131. package/dist/controllers/input_mask_controller.js +483 -97
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +150 -106
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +19 -2
  137. package/dist/controllers/lazy_frame_controller.js +24 -68
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +44 -9
  140. package/dist/controllers/listbox_controller.js +523 -240
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +9 -7
  143. package/dist/controllers/local_time_controller.js +102 -77
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +80 -96
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +55 -11
  149. package/dist/controllers/menu_controller.js +302 -188
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +51 -9
  152. package/dist/controllers/menubar_controller.js +232 -345
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +33 -20
  155. package/dist/controllers/meter_controller.js +417 -87
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +48 -18
  158. package/dist/controllers/multi_select_controller.js +785 -393
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +27 -6
  161. package/dist/controllers/navigation_menu_controller.js +190 -250
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +396 -192
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +268 -62
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +50 -8
  170. package/dist/controllers/number_input_controller.js +610 -303
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.d.ts +8 -5
  173. package/dist/controllers/optimistic_controller.js +45 -87
  174. package/dist/controllers/optimistic_controller.js.map +1 -1
  175. package/dist/controllers/otp_controller.d.ts +55 -8
  176. package/dist/controllers/otp_controller.js +658 -255
  177. package/dist/controllers/otp_controller.js.map +1 -1
  178. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  179. package/dist/controllers/overflow_indicator_controller.js +122 -113
  180. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  181. package/dist/controllers/overflow_menu_controller.d.ts +58 -20
  182. package/dist/controllers/overflow_menu_controller.js +470 -295
  183. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  184. package/dist/controllers/pagination_controller.d.ts +37 -7
  185. package/dist/controllers/pagination_controller.js +332 -123
  186. package/dist/controllers/pagination_controller.js.map +1 -1
  187. package/dist/controllers/password_reveal_controller.d.ts +46 -8
  188. package/dist/controllers/password_reveal_controller.js +392 -121
  189. package/dist/controllers/password_reveal_controller.js.map +1 -1
  190. package/dist/controllers/password_strength_controller.d.ts +15 -6
  191. package/dist/controllers/password_strength_controller.js +111 -158
  192. package/dist/controllers/password_strength_controller.js.map +1 -1
  193. package/dist/controllers/persist_controller.d.ts +11 -4
  194. package/dist/controllers/persist_controller.js +165 -111
  195. package/dist/controllers/persist_controller.js.map +1 -1
  196. package/dist/controllers/pointer_drag_controller.d.ts +35 -12
  197. package/dist/controllers/pointer_drag_controller.js +434 -188
  198. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  199. package/dist/controllers/popover_controller.d.ts +41 -5
  200. package/dist/controllers/popover_controller.js +269 -113
  201. package/dist/controllers/popover_controller.js.map +1 -1
  202. package/dist/controllers/portal_controller.d.ts +44 -4
  203. package/dist/controllers/portal_controller.js +215 -76
  204. package/dist/controllers/portal_controller.js.map +1 -1
  205. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  206. package/dist/controllers/preview_guard_controller.js +243 -116
  207. package/dist/controllers/preview_guard_controller.js.map +1 -1
  208. package/dist/controllers/progress_controller.d.ts +20 -19
  209. package/dist/controllers/progress_controller.js +391 -78
  210. package/dist/controllers/progress_controller.js.map +1 -1
  211. package/dist/controllers/radio_group_controller.d.ts +21 -9
  212. package/dist/controllers/radio_group_controller.js +278 -211
  213. package/dist/controllers/radio_group_controller.js.map +1 -1
  214. package/dist/controllers/range_slider_controller.d.ts +47 -6
  215. package/dist/controllers/range_slider_controller.js +220 -113
  216. package/dist/controllers/range_slider_controller.js.map +1 -1
  217. package/dist/controllers/rating_controller.d.ts +20 -4
  218. package/dist/controllers/rating_controller.js +458 -211
  219. package/dist/controllers/rating_controller.js.map +1 -1
  220. package/dist/controllers/read_more_controller.d.ts +2 -0
  221. package/dist/controllers/read_more_controller.js +138 -63
  222. package/dist/controllers/read_more_controller.js.map +1 -1
  223. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  224. package/dist/controllers/reading_progress_controller.js +166 -137
  225. package/dist/controllers/reading_progress_controller.js.map +1 -1
  226. package/dist/controllers/relative_time_controller.d.ts +20 -6
  227. package/dist/controllers/relative_time_controller.js +211 -112
  228. package/dist/controllers/relative_time_controller.js.map +1 -1
  229. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  230. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  231. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  232. package/dist/controllers/resizable_controller.d.ts +31 -2
  233. package/dist/controllers/resizable_controller.js +299 -150
  234. package/dist/controllers/resizable_controller.js.map +1 -1
  235. package/dist/controllers/roving_controller.d.ts +3 -3
  236. package/dist/controllers/roving_controller.js +49 -88
  237. package/dist/controllers/roving_controller.js.map +1 -1
  238. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  239. package/dist/controllers/scroll_area_controller.js +490 -246
  240. package/dist/controllers/scroll_area_controller.js.map +1 -1
  241. package/dist/controllers/scroll_restore_controller.d.ts +3 -3
  242. package/dist/controllers/scroll_restore_controller.js +14 -55
  243. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  244. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  245. package/dist/controllers/scroll_visibility_controller.js +432 -149
  246. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  247. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  248. package/dist/controllers/scrollspy_controller.js +193 -248
  249. package/dist/controllers/scrollspy_controller.js.map +1 -1
  250. package/dist/controllers/separator_controller.d.ts +20 -5
  251. package/dist/controllers/separator_controller.js +285 -142
  252. package/dist/controllers/separator_controller.js.map +1 -1
  253. package/dist/controllers/sidebar_controller.d.ts +65 -8
  254. package/dist/controllers/sidebar_controller.js +1069 -404
  255. package/dist/controllers/sidebar_controller.js.map +1 -1
  256. package/dist/controllers/skeleton_controller.d.ts +23 -0
  257. package/dist/controllers/skeleton_controller.js +259 -101
  258. package/dist/controllers/skeleton_controller.js.map +1 -1
  259. package/dist/controllers/slider_controller.d.ts +29 -2
  260. package/dist/controllers/slider_controller.js +187 -86
  261. package/dist/controllers/slider_controller.js.map +1 -1
  262. package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
  263. package/dist/controllers/smart_sticky_header_controller.js +173 -66
  264. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  265. package/dist/controllers/sortable_controller.d.ts +4 -4
  266. package/dist/controllers/sortable_controller.js +26 -125
  267. package/dist/controllers/sortable_controller.js.map +1 -1
  268. package/dist/controllers/spinner_controller.d.ts +39 -4
  269. package/dist/controllers/spinner_controller.js +288 -173
  270. package/dist/controllers/spinner_controller.js.map +1 -1
  271. package/dist/controllers/step_indicator_controller.d.ts +9 -4
  272. package/dist/controllers/step_indicator_controller.js +105 -46
  273. package/dist/controllers/step_indicator_controller.js.map +1 -1
  274. package/dist/controllers/stepper_controller.d.ts +14 -6
  275. package/dist/controllers/stepper_controller.js +120 -73
  276. package/dist/controllers/stepper_controller.js.map +1 -1
  277. package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
  278. package/dist/controllers/stick_to_bottom_controller.js +275 -140
  279. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  280. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  281. package/dist/controllers/sticky_observer_controller.js +220 -49
  282. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  283. package/dist/controllers/submit_once_controller.d.ts +24 -8
  284. package/dist/controllers/submit_once_controller.js +376 -227
  285. package/dist/controllers/submit_once_controller.js.map +1 -1
  286. package/dist/controllers/switch_controller.d.ts +18 -4
  287. package/dist/controllers/switch_controller.js +201 -84
  288. package/dist/controllers/switch_controller.js.map +1 -1
  289. package/dist/controllers/tabs_controller.d.ts +9 -6
  290. package/dist/controllers/tabs_controller.js +41 -30
  291. package/dist/controllers/tabs_controller.js.map +1 -1
  292. package/dist/controllers/tags_input_controller.d.ts +16 -3
  293. package/dist/controllers/tags_input_controller.js +293 -173
  294. package/dist/controllers/tags_input_controller.js.map +1 -1
  295. package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
  296. package/dist/controllers/textarea_autosize_controller.js +108 -74
  297. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  298. package/dist/controllers/theme_controller.d.ts +24 -11
  299. package/dist/controllers/theme_controller.js +140 -116
  300. package/dist/controllers/theme_controller.js.map +1 -1
  301. package/dist/controllers/time_picker_controller.d.ts +55 -5
  302. package/dist/controllers/time_picker_controller.js +331 -127
  303. package/dist/controllers/time_picker_controller.js.map +1 -1
  304. package/dist/controllers/toast_controller.d.ts +40 -16
  305. package/dist/controllers/toast_controller.js +244 -324
  306. package/dist/controllers/toast_controller.js.map +1 -1
  307. package/dist/controllers/toggle_group_controller.d.ts +14 -6
  308. package/dist/controllers/toggle_group_controller.js +252 -191
  309. package/dist/controllers/toggle_group_controller.js.map +1 -1
  310. package/dist/controllers/toolbar_controller.d.ts +8 -6
  311. package/dist/controllers/toolbar_controller.js +219 -120
  312. package/dist/controllers/toolbar_controller.js.map +1 -1
  313. package/dist/controllers/tooltip_controller.d.ts +49 -4
  314. package/dist/controllers/tooltip_controller.js +158 -124
  315. package/dist/controllers/tooltip_controller.js.map +1 -1
  316. package/dist/controllers/transition_controller.d.ts +22 -4
  317. package/dist/controllers/transition_controller.js +123 -119
  318. package/dist/controllers/transition_controller.js.map +1 -1
  319. package/dist/controllers/tree_view_controller.d.ts +22 -12
  320. package/dist/controllers/tree_view_controller.js +283 -369
  321. package/dist/controllers/tree_view_controller.js.map +1 -1
  322. package/dist/index.d.ts +10 -5
  323. package/dist/index.js +8573 -10410
  324. package/dist/index.js.map +1 -1
  325. package/dist/inspector/cli.d.ts +38 -14
  326. package/dist/inspector/cli.js +108 -40
  327. package/dist/inspector/cli.js.map +1 -1
  328. package/dist/inspector/cli_bin.js +145 -64
  329. package/dist/inspector/cli_bin.js.map +1 -1
  330. package/dist/inspector/examples.json +20 -20
  331. package/dist/inspector/manifest.json +802 -101
  332. package/dist/positioning/index.d.ts +15 -7
  333. package/dist/positioning/index.js +154 -47
  334. package/dist/positioning/index.js.map +1 -1
  335. package/package.json +11 -10
  336. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  337. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,14 +1,21 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/otp_controller.ts
3
+ function sharedRegistry(key, create) {
4
+ const symbol = Symbol.for(key);
5
+ const scope = globalThis;
6
+ const existing = scope[symbol];
7
+ if (existing !== void 0) return existing;
8
+ const registry2 = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry2 });
10
+ return registry2;
11
+ }
4
12
 
5
- // src/utils/aria_ids.ts
6
- var counter = 0;
13
+ var ids = sharedRegistry("stimeo-ui.aria-ids.registry.v1", () => ({ counter: 0 }));
7
14
  function uniqueId(prefix = "stimeo") {
8
15
  let candidate;
9
16
  do {
10
- counter += 1;
11
- candidate = `${prefix}-${counter}`;
17
+ ids.counter += 1;
18
+ candidate = `${prefix}-${ids.counter}`;
12
19
  } while (typeof document !== "undefined" && document.getElementById(candidate) !== null);
13
20
  return candidate;
14
21
  }
@@ -19,12 +26,10 @@ function ensureId(element, prefix = "stimeo") {
19
26
  return id;
20
27
  }
21
28
 
22
- // src/utils/logical_scroll.ts
23
29
  function isRtl(element) {
24
30
  return window.getComputedStyle(element).direction === "rtl";
25
31
  }
26
32
 
27
- // src/utils/arrow_step.ts
28
33
  function logicalArrowKey(key, element) {
29
34
  if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
30
35
  if (!isRtl(element)) return key;
@@ -34,157 +39,338 @@ function isReservedArrowChord(event, allow = []) {
34
39
  if (!event.key.startsWith("Arrow")) return false;
35
40
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
36
41
  }
42
+ function hasModifierChord(event) {
43
+ return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
44
+ }
45
+
46
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
47
+ if (element.getAttribute(name) === value) return false;
48
+ if (value === null) element.removeAttribute(name);
49
+ else element.setAttribute(name, value);
50
+ return true;
51
+ }
37
52
 
38
- // src/utils/attribute_lease.ts
53
+ var registry = sharedRegistry(
54
+ "stimeo-ui.attribute-lease.registry.v1",
55
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
56
+ );
57
+ function isAttributeValue(value) {
58
+ return value === null || typeof value === "string";
59
+ }
60
+ function readLeaseRecord(element, record) {
61
+ try {
62
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
63
+ if (!Array.isArray(parsed)) return void 0;
64
+ const [original, written, ...rest] = parsed;
65
+ const linked = rest[0] !== false;
66
+ const beneath = linked ? rest : rest.slice(1);
67
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
68
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
69
+ } catch {
70
+ return void 0;
71
+ }
72
+ }
73
+ function attributeStack(element, attribute) {
74
+ const byAttribute = registry.stacks.get(element) ?? /* @__PURE__ */ new Map();
75
+ registry.stacks.set(element, byAttribute);
76
+ const known = byAttribute.get(attribute);
77
+ if (known) return known;
78
+ const found = [];
79
+ for (const record of registry.names.get(attribute) ?? []) {
80
+ const read = readLeaseRecord(element, record);
81
+ if (read) found.push({ ...read, record });
82
+ }
83
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
84
+ const stack = found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
85
+ lease: null,
86
+ record,
87
+ written,
88
+ original,
89
+ linked: linked && index > 0
90
+ }));
91
+ byAttribute.set(attribute, stack);
92
+ return stack;
93
+ }
94
+ function findHold(stack, lease, record) {
95
+ return stack.find((held) => held.lease === lease) ?? stack.find((held) => held.lease === null && held.record === record);
96
+ }
39
97
  var AttributeLease = class {
40
98
  #attribute;
41
- #records = /* @__PURE__ */ new Map();
42
- /** @param attribute - The attribute whose temporary values this lease owns. */
43
- constructor(attribute) {
99
+ #record;
100
+ #base;
101
+ #preserveAuthor;
102
+ #held = /* @__PURE__ */ new Set();
103
+ constructor(attribute, owner, options = {}) {
44
104
  this.#attribute = attribute;
105
+ this.#record = `data-${owner}-${attribute}-lease`;
106
+ this.#base = options.base === true;
107
+ this.#preserveAuthor = options.preserveAuthor === true;
108
+ const names = registry.names;
109
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
45
110
  }
46
- /** Writes or removes the leased attribute while preserving its authored value. */
47
111
  write(element, value) {
48
- const existing = this.#records.get(element);
49
- if (existing) {
50
- existing.written = value;
51
- } else {
52
- this.#records.set(element, {
53
- original: element.getAttribute(this.#attribute),
54
- written: value
55
- });
56
- }
57
- this.#reflect(element, value);
112
+ const stack = attributeStack(element, this.#attribute);
113
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
114
+ this.#held.add(element);
115
+ const shows = !this.#covered(element, stack, hold);
116
+ hold.written = value;
117
+ this.#mark(element, stack);
118
+ if (shows) this.#reflect(element, value);
58
119
  }
59
- /** Returns one lease without overwriting a value subsequently authored by a consumer. */
60
120
  return(element) {
61
- const record = this.#records.get(element);
62
- if (!record) return;
63
- this.#records.delete(element);
64
- const stillOwned = element.getAttribute(this.#attribute) === record.written;
65
- if (stillOwned) this.#reflect(element, record.original);
66
- }
67
- /** Reflects only a real value transition, avoiding self-triggered mutation work. */
68
- #reflect(element, value) {
69
- if (element.getAttribute(this.#attribute) === value) return;
70
- if (value === null) element.removeAttribute(this.#attribute);
71
- else element.setAttribute(this.#attribute, value);
121
+ const held = this.#held.delete(element);
122
+ const known = registry.stacks.get(element)?.has(this.#attribute);
123
+ const stack = held || known || element.hasAttribute(this.#record) ? attributeStack(element, this.#attribute) : [];
124
+ const hold = this.#take(stack);
125
+ if (!hold) return false;
126
+ const carries = element.getAttribute(this.#attribute) === hold.written;
127
+ const owns = carries && !this.#covered(element, stack, hold);
128
+ const index = stack.indexOf(hold);
129
+ stack.splice(index, 1);
130
+ const above = stack[index];
131
+ if (above) above.linked &&= hold.linked;
132
+ this.#mark(element, stack);
133
+ if (!owns) return true;
134
+ const below = hold.linked ? stack[index - 1] : void 0;
135
+ this.#reflect(element, below ? below.written : hold.original);
136
+ return true;
72
137
  }
73
- /** Returns every outstanding lease using the same ownership check as {@link return}. */
74
138
  returnAll() {
75
- for (const element of Array.from(this.#records.keys())) this.return(element);
76
- }
77
- };
78
-
79
- // src/utils/before_cache_reset.ts
80
- var BeforeCacheReset = class _BeforeCacheReset {
81
- /** Every subscribed instance, iterated by the one shared document listener. */
82
- static #subscribers = /* @__PURE__ */ new Set();
83
- /** The shared listener; installed while at least one instance is subscribed. */
84
- static #onBeforeCache = () => {
85
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
86
- };
87
- #rewind;
88
- /** @param rewind - the pass that returns this controller's state to its initial form. */
89
- constructor(rewind) {
90
- this.#rewind = rewind;
91
- }
92
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
93
- activate() {
94
- const first = _BeforeCacheReset.#subscribers.size === 0;
95
- _BeforeCacheReset.#subscribers.add(this);
96
- if (first) {
97
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
139
+ for (const element of Array.from(this.#held)) this.return(element);
140
+ }
141
+ #take(stack) {
142
+ const hold = findHold(stack, this, this.#record);
143
+ if (hold) hold.lease = this;
144
+ return hold;
145
+ }
146
+ #acquire(element, stack) {
147
+ const current = element.getAttribute(this.#attribute);
148
+ const top = stack.at(-1);
149
+ const bottom = stack[0];
150
+ const over = top?.written === current ? top : void 0;
151
+ const from = this.#base ? this.#preserveAuthor && !stack.some((held) => held.written === current) ? void 0 : bottom : over;
152
+ const hold = {
153
+ lease: this,
154
+ record: this.#record,
155
+ written: current,
156
+ original: from ? from.original : current,
157
+ linked: !this.#base && over !== void 0
158
+ };
159
+ if (this.#base && bottom) {
160
+ bottom.linked = true;
161
+ stack.unshift(hold);
162
+ } else stack.push(hold);
163
+ return hold;
164
+ }
165
+ #covered(element, stack, hold) {
166
+ const current = element.getAttribute(this.#attribute);
167
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
168
+ }
169
+ #mark(element, stack) {
170
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
171
+ stack.forEach((hold, index) => {
172
+ const names = stack.slice(0, index).map((under) => under.record);
173
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
174
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
175
+ records.set(
176
+ hold.record,
177
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
178
+ );
179
+ });
180
+ for (const [name, recorded] of records) {
181
+ setAttributeIfChanged(element, name, recorded);
98
182
  }
99
183
  }
100
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
101
- deactivate() {
102
- _BeforeCacheReset.#subscribers.delete(this);
103
- if (_BeforeCacheReset.#subscribers.size > 0) return;
104
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
184
+ #reflect(element, value) {
185
+ setAttributeIfChanged(element, this.#attribute, value);
105
186
  }
106
187
  };
107
188
 
108
- // src/utils/composition_tracker.ts
109
189
  var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
110
190
  "insertCompositionText",
111
191
  "insertFromComposition",
112
192
  "deleteCompositionText",
113
193
  "deleteByComposition"
114
194
  ]);
195
+ function isCompositionInputType(inputType) {
196
+ return inputType !== void 0 && COMPOSITION_INPUT_TYPES.has(inputType);
197
+ }
198
+ var BEFORE_MORPH_ATTRIBUTE = "turbo:before-morph-attribute";
199
+ var textOf = (element) => element.value;
200
+ var holdsFocus = (element) => element instanceof Node && element.getRootNode().activeElement === element;
115
201
  var CompositionTracker = class {
116
202
  #observedTargets = /* @__PURE__ */ new Set();
117
- #activeTargets = /* @__PURE__ */ new Set();
203
+ #composition = null;
118
204
  #onStart;
119
205
  #onEnd;
120
- /** The field whose confirming `input` is still owed, while the window is open. */
206
+ #onAbandon;
121
207
  #confirmedTarget = null;
208
+ #keyComposing = false;
209
+ #connected = false;
210
+ #abandoned = null;
122
211
  constructor(options = {}) {
123
212
  this.#onStart = options.onStart;
124
213
  this.#onEnd = options.onEnd;
214
+ this.#onAbandon = options.onAbandon;
125
215
  }
126
- /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
127
216
  observe(target) {
217
+ this.#connected = true;
128
218
  if (this.#observedTargets.has(target)) return;
129
219
  target.addEventListener("compositionstart", this.#handleStart);
130
220
  target.addEventListener("compositionend", this.#handleEnd);
131
- target.addEventListener("keydown", this.#handleKeydown);
221
+ target.addEventListener("keydown", this.#handleKeydown, true);
222
+ target.addEventListener("beforeinput", this.#handleBeforeInput, true);
223
+ target.addEventListener("input", this.#handleInput, true);
224
+ target.addEventListener("blur", this.#handleBlur, true);
225
+ target.addEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
132
226
  this.#observedTargets.add(target);
133
227
  }
134
- /** Stops tracking one target and clears any active composition it owned. */
135
228
  unobserve(target) {
136
229
  if (!this.#observedTargets.delete(target)) return;
137
- target.removeEventListener("compositionstart", this.#handleStart);
138
- target.removeEventListener("compositionend", this.#handleEnd);
139
- target.removeEventListener("keydown", this.#handleKeydown);
140
- this.#activeTargets.delete(target);
230
+ this.#release(target);
231
+ if (this.#composition?.target === target) this.#letGo();
141
232
  if (this.#confirmedTarget === target) this.#confirmedTarget = null;
142
233
  }
143
- /** Releases every listener and clears state so reconnect starts cleanly. */
144
234
  disconnect() {
145
- for (const target of this.#observedTargets) {
146
- target.removeEventListener("compositionstart", this.#handleStart);
147
- target.removeEventListener("compositionend", this.#handleEnd);
148
- target.removeEventListener("keydown", this.#handleKeydown);
149
- }
235
+ this.#connected = false;
236
+ for (const target of this.#observedTargets) this.#release(target);
150
237
  this.#observedTargets.clear();
151
- this.#activeTargets.clear();
238
+ this.#letGo();
152
239
  this.#confirmedTarget = null;
240
+ this.#keyComposing = false;
241
+ this.#abandoned = null;
153
242
  }
154
- /** True when lifecycle tracking or the current event reports composition. */
155
243
  isComposing(event) {
156
- return this.#activeTargets.size > 0 || event?.isComposing === true;
157
- }
158
- /**
159
- * Whether `event` is the `input` echoing the composition just confirmed.
160
- *
161
- * Asking closes the window either way, so one confirmation is folded at most
162
- * once and a consumer asks once per `input`.
163
- */
244
+ this.#settle();
245
+ return event?.isComposing === true || this.#composition !== null;
246
+ }
247
+ composingElement() {
248
+ this.#settle();
249
+ return this.#composition?.element ?? null;
250
+ }
251
+ owns(event) {
252
+ const element = this.composingElement();
253
+ return event.isComposing === true || element !== null && element === event.target;
254
+ }
164
255
  consumesConfirmedInput(event) {
165
256
  const confirmed = this.#confirmedTarget;
166
257
  this.#confirmedTarget = null;
167
258
  if (confirmed === null || confirmed !== event.target) return false;
168
259
  const inputType = event.inputType;
169
- return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
260
+ return !inputType || isCompositionInputType(inputType);
170
261
  }
262
+ #release(target) {
263
+ target.removeEventListener("compositionstart", this.#handleStart);
264
+ target.removeEventListener("compositionend", this.#handleEnd);
265
+ target.removeEventListener("keydown", this.#handleKeydown, true);
266
+ target.removeEventListener("beforeinput", this.#handleBeforeInput, true);
267
+ target.removeEventListener("input", this.#handleInput, true);
268
+ target.removeEventListener("blur", this.#handleBlur, true);
269
+ target.removeEventListener(BEFORE_MORPH_ATTRIBUTE, this.#handleBeforeMorphAttribute);
270
+ }
271
+ #letGo() {
272
+ const composition = this.#composition;
273
+ if (composition === null) return;
274
+ if (!holdsFocus(composition.element)) {
275
+ this.#composition = null;
276
+ return;
277
+ }
278
+ queueMicrotask(() => {
279
+ if (this.#composition !== composition || this.#observedTargets.has(composition.target)) {
280
+ return;
281
+ }
282
+ this.#composition = null;
283
+ if (!this.#connected) return;
284
+ this.#abandoned = { element: composition.element, inPlace: true };
285
+ this.#runAbandon();
286
+ });
287
+ }
288
+ #settle() {
289
+ const composition = this.#composition;
290
+ if (composition === null) return;
291
+ const { target, element, text } = composition;
292
+ if (target instanceof Node && element instanceof Node && !target.contains(element)) {
293
+ this.#composition = null;
294
+ } else if (textOf(element) !== text) {
295
+ this.#abandon(false);
296
+ }
297
+ }
298
+ #abandon(inPlace) {
299
+ this.#owe(inPlace);
300
+ this.#composition = null;
301
+ queueMicrotask(this.#runAbandon);
302
+ }
303
+ #owe(inPlace) {
304
+ const composition = this.#composition;
305
+ if (composition !== null) this.#abandoned = { element: composition.element, inPlace };
306
+ }
307
+ #runAbandon = () => {
308
+ const abandoned = this.#abandoned;
309
+ if (abandoned === null) return;
310
+ this.#abandoned = null;
311
+ this.#onAbandon?.(abandoned.element, abandoned.inPlace);
312
+ };
171
313
  #handleStart = (event) => {
172
314
  this.#confirmedTarget = null;
173
- if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
315
+ this.#settle();
316
+ this.#owe(true);
317
+ const target = event.currentTarget;
318
+ const element = event.target;
319
+ this.#composition = { target, element, text: textOf(element) };
320
+ this.#runAbandon();
174
321
  this.#onStart?.(event);
175
322
  };
176
323
  #handleEnd = (event) => {
177
- if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
324
+ this.#runAbandon();
325
+ if (this.#composition?.target === event.currentTarget) this.#composition = null;
178
326
  this.#confirmedTarget = event.target;
179
327
  this.#onEnd?.(event);
180
328
  };
181
- /** A key on an observed field opens an edit of its own, so no echo is owed. */
182
- #handleKeydown = () => {
329
+ #handleKeydown = (event) => {
330
+ this.#settle();
331
+ this.#runAbandon();
183
332
  this.#confirmedTarget = null;
333
+ this.#keyComposing = event.isComposing === true;
334
+ };
335
+ #handleBeforeInput = (event) => {
336
+ const keyComposing = this.#keyComposing;
337
+ this.#keyComposing = false;
338
+ this.#settle();
339
+ const { isComposing, inputType } = event;
340
+ if (this.#composition !== null && !keyComposing && !isComposing && inputType && !isCompositionInputType(inputType)) {
341
+ this.#abandon(true);
342
+ }
343
+ this.#runAbandon();
344
+ };
345
+ #handleInput = (event) => {
346
+ const composition = this.#composition;
347
+ const { isTrusted, isComposing, inputType } = event;
348
+ if (composition !== null && event.target === composition.element && (isTrusted || isComposing || isCompositionInputType(inputType))) {
349
+ composition.text = textOf(composition.element);
350
+ } else {
351
+ this.#settle();
352
+ }
353
+ this.#runAbandon();
354
+ };
355
+ #handleBlur = (event) => {
356
+ this.#settle();
357
+ this.#runAbandon();
358
+ const composition = this.#composition;
359
+ if (composition === null || composition.target === composition.element) return;
360
+ const { element } = composition;
361
+ if (!(element instanceof Node) || event.target !== element) return;
362
+ const root = element.getRootNode();
363
+ if (root.activeElement !== element) this.#composition = null;
364
+ };
365
+ #handleBeforeMorphAttribute = (event) => {
366
+ this.#settle();
367
+ const composition = this.#composition;
368
+ if (composition?.target !== event.currentTarget || event.target !== composition.element) return;
369
+ const detail = event.detail;
370
+ if (detail?.attributeName === "value") event.preventDefault();
184
371
  };
185
372
  };
186
373
 
187
- // src/utils/declared_value.ts
188
374
  function parseDeclared(raw, parse, fallback) {
189
375
  try {
190
376
  return parse(raw);
@@ -203,25 +389,18 @@ function compileRegExp(source, anchor = "none") {
203
389
  );
204
390
  }
205
391
 
206
- // src/utils/form_reset_watcher.ts
207
392
  var FormResetWatcher = class {
208
393
  #owns;
209
394
  #onReset;
210
395
  #frames = /* @__PURE__ */ new Set();
211
396
  #generation = 0;
212
- /**
213
- * @param owns - whether `form` owns a control this controller derives from.
214
- * @param onReset - the reconciliation pass, run once per honoured reset.
215
- */
216
397
  constructor(owns, onReset) {
217
398
  this.#owns = owns;
218
399
  this.#onReset = onReset;
219
400
  }
220
- /** Subscribes to `reset` at the document; call from `connect()`. Idempotent. */
221
401
  observe() {
222
402
  document.addEventListener("reset", this.#handleReset, true);
223
403
  }
224
- /** Unsubscribes and drops every pending pass; call from `disconnect()`. */
225
404
  disconnect() {
226
405
  document.removeEventListener("reset", this.#handleReset, true);
227
406
  this.#generation += 1;
@@ -245,13 +424,11 @@ var FormResetWatcher = class {
245
424
  this.#frames.add(frame);
246
425
  });
247
426
  };
248
- /** Runs the pass unless a listener cancelled the reset before it landed. */
249
427
  #settle(event) {
250
428
  if (!event.defaultPrevented) this.#onReset();
251
429
  }
252
430
  };
253
431
 
254
- // src/utils/half_width.ts
255
432
  var FULL_WIDTH_SHIFT = 65248;
256
433
  function halfWidthChar(char) {
257
434
  if (char >= "\uFF01" && char <= "\uFF5E") {
@@ -265,27 +442,94 @@ function toHalfWidth(text) {
265
442
  return out;
266
443
  }
267
444
 
268
- // src/utils/microtask_coalescer.ts
445
+ var TimerRegistry = class {
446
+ ids = /* @__PURE__ */ new Set();
447
+ clear(id) {
448
+ if (this.ids.delete(id)) {
449
+ this.cancel(id);
450
+ }
451
+ }
452
+ clearAll() {
453
+ for (const id of this.ids) {
454
+ this.cancel(id);
455
+ }
456
+ this.ids.clear();
457
+ }
458
+ get size() {
459
+ return this.ids.size;
460
+ }
461
+ };
462
+ var SafeTimeout = class extends TimerRegistry {
463
+ set(callback, delay) {
464
+ const id = this.schedule(() => {
465
+ this.ids.delete(id);
466
+ callback();
467
+ }, delay);
468
+ this.ids.add(id);
469
+ return id;
470
+ }
471
+ schedule(callback, delay) {
472
+ return window.setTimeout(callback, delay);
473
+ }
474
+ cancel(id) {
475
+ window.clearTimeout(id);
476
+ }
477
+ };
478
+
479
+ var KeyedTimers = class {
480
+ #timers = new SafeTimeout();
481
+ #pending = /* @__PURE__ */ new Map();
482
+ set(key, callback, delay) {
483
+ this.clear(key);
484
+ const id = this.#timers.set(() => {
485
+ this.#pending.delete(key);
486
+ callback();
487
+ }, delay);
488
+ this.#pending.set(key, id);
489
+ }
490
+ clear(key) {
491
+ this.#timers.clear(this.#pending.get(key) ?? -1);
492
+ this.#pending.delete(key);
493
+ }
494
+ clearAll() {
495
+ this.#timers.clearAll();
496
+ this.#pending.clear();
497
+ }
498
+ has(key) {
499
+ return this.#pending.has(key);
500
+ }
501
+ };
502
+
503
+ var ListenerSet = class {
504
+ #abort = new AbortController();
505
+ add(target, type, handler, options) {
506
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
507
+ }
508
+ dispose() {
509
+ this.#abort.abort();
510
+ this.#abort = new AbortController();
511
+ }
512
+ };
513
+
269
514
  var MicrotaskCoalescer = class {
270
515
  #run;
271
516
  #queued = false;
272
517
  #active = false;
273
518
  #generation = 0;
274
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
275
519
  constructor(run) {
276
520
  this.#run = run;
277
521
  }
278
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
279
522
  activate() {
280
523
  this.#active = true;
281
524
  }
282
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
283
525
  cancel() {
284
526
  this.#active = false;
285
527
  this.#queued = false;
286
528
  this.#generation += 1;
287
529
  }
288
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
530
+ get pending() {
531
+ return this.#queued;
532
+ }
289
533
  schedule() {
290
534
  if (!this.#active || this.#queued) return;
291
535
  this.#queued = true;
@@ -298,7 +542,45 @@ var MicrotaskCoalescer = class {
298
542
  }
299
543
  };
300
544
 
301
- // src/controllers/otp_controller.ts
545
+ var MorphRenderWatcher = class {
546
+ #listeners = new ListenerSet();
547
+ #pass;
548
+ #element = null;
549
+ #onMorph = () => this.#pass.schedule();
550
+ constructor(run) {
551
+ this.#pass = new MicrotaskCoalescer(run);
552
+ }
553
+ observe(element) {
554
+ if (this.#element === element) return;
555
+ this.disconnect();
556
+ this.#element = element;
557
+ this.#pass.activate();
558
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
559
+ }
560
+ schedule() {
561
+ this.#pass.schedule();
562
+ }
563
+ get pending() {
564
+ return this.#pass.pending;
565
+ }
566
+ disconnect() {
567
+ this.#listeners.dispose();
568
+ this.#pass.cancel();
569
+ this.#element = null;
570
+ }
571
+ };
572
+
573
+ var MoveCounter = class {
574
+ #sequence = 0;
575
+ record() {
576
+ this.#sequence += 1;
577
+ return this.#sequence;
578
+ }
579
+ isLatest(token) {
580
+ return token === this.#sequence;
581
+ }
582
+ };
583
+
302
584
  var LEASE_MARKERS = {
303
585
  "aria-invalid": "data-otp-invalid-lease",
304
586
  "aria-errormessage": "data-otp-errormessage-lease",
@@ -316,49 +598,39 @@ function readLease(field, marker) {
316
598
  return null;
317
599
  }
318
600
  }
319
- function describedByWith(described, errorId) {
320
- const tokens = (described ?? "").split(/\s+/).filter((token) => token.length > 0 && token !== errorId);
601
+ function describedByWith(described, errorId, earlier) {
602
+ const tokens = (described ?? "").split(/\s+/).filter((token) => token.length > 0 && token !== errorId && token !== earlier);
321
603
  return [...tokens, errorId].join(" ");
322
604
  }
323
605
  var DEFAULT_PATTERN = "[0-9]";
324
606
  var DEFAULT_MATCHER = new RegExp(`^(?:${DEFAULT_PATTERN})$`);
325
- function hasModifier(event) {
326
- return event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
327
- }
328
607
  function statesDiffer(left, right) {
329
608
  return left.value !== right.value || left.state !== right.state;
330
609
  }
331
610
  var OtpController = class extends Controller {
611
+ #moves = new MoveCounter();
332
612
  static targets = ["field", "value", "error"];
333
613
  static values = {
334
614
  pattern: { type: String, default: DEFAULT_PATTERN }
335
615
  };
336
616
  static actions = ["onInput", "onKeydown", "onPaste", "onPointerDown", "clear"];
337
617
  static events = ["change", "complete", "invalid", "reconcile"];
338
- /** Validated matcher; the hot path never compiles a raw declaration. */
339
618
  #pattern = DEFAULT_MATCHER;
340
- /** Source of {@link #pattern}, reported in `invalid` so consumers can word it. */
341
619
  #patternSource = DEFAULT_PATTERN;
342
- /** Public state carried by the last dispatch; keeps a no-op sync silent. */
343
620
  #published = null;
344
- /** True between connect and disconnect, so pre-connect Value changes stay silent. */
345
621
  #connected = false;
346
- /** Digit each field last committed, restored when rejected input replaced it. */
347
622
  #committed = /* @__PURE__ */ new WeakMap();
348
- /** The field being composed, and the text it held when the composition began. */
623
+ #retained = /* @__PURE__ */ new WeakMap();
624
+ #inputBaselines = /* @__PURE__ */ new Map();
625
+ #inputExpiry = new KeyedTimers();
626
+ #endBaselines = /* @__PURE__ */ new Map();
627
+ #away = /* @__PURE__ */ new Set();
349
628
  #composing = null;
350
- /** A `pattern` change whose rejected characters wait for the composition to end. */
351
629
  #dropHeld = false;
352
- /**
353
- * Collapses one batch of page changes — field target and `pattern` callbacks, a
354
- * form reset — into a single reconciliation.
355
- */
356
- #reconcile = new MicrotaskCoalescer(() => this.#reconcileFields());
357
- #errorHidden = new AttributeLease("hidden");
358
- #state = new AttributeLease("data-state");
359
- /** Rewinds the transient error surface before Turbo freezes the page. */
360
- #beforeCache = new BeforeCacheReset(() => this.#clearError());
361
- /** Reads the fields back once a native reset restored them, as a reconciliation. */
630
+ #judgedBy = null;
631
+ #reconcile = new MorphRenderWatcher(() => this.#reconcileFields());
632
+ #errorHidden = new AttributeLease("hidden", this.identifier);
633
+ #state = new AttributeLease("data-state", this.identifier);
362
634
  #formReset = new FormResetWatcher(
363
635
  (form) => this.#ownedBy(form),
364
636
  () => {
@@ -366,114 +638,150 @@ var OtpController = class extends Controller {
366
638
  this.#reconcile.schedule();
367
639
  }
368
640
  );
369
- /**
370
- * Owns IME lifecycle state across every digit field. When a composition ends, a
371
- * `pattern` drop it held back applies first — it was declared first — and the
372
- * user's commit is taken after it.
373
- */
374
641
  #composition = new CompositionTracker({
375
642
  onStart: (event) => {
376
643
  const field = this.#fieldFrom(event);
377
- if (field) this.#composing = { field, before: field.value };
644
+ this.#clearInputBaseline();
645
+ if (field) this.#endBaselines.delete(field);
646
+ if (field && this.#composition.composingElement() === field) {
647
+ this.#composing = { field, before: field.value };
648
+ }
378
649
  },
379
650
  onEnd: (event) => {
380
- const composing = this.#composing;
381
- this.#composing = null;
382
651
  const input = this.#fieldFrom(event);
652
+ if (!input) return;
653
+ const baseline = this.#inputBaselines.get(input) ?? this.#endBaselines.get(input)?.baseline;
654
+ this.#clearEdits(input);
655
+ const composing = this.#composing;
656
+ if (!this.#composition.isComposing()) this.#composing = null;
383
657
  const data = event.data ?? "";
384
- const text = input && data.length <= input.value.length ? input.value : data;
385
- const commit = input !== null && (composing?.field !== input || text !== composing.before);
386
- if (this.#dropHeld) {
387
- if (input && commit) input.value = this.#committed.get(input) ?? "";
658
+ const text = data.length <= input.value.length ? input.value : data;
659
+ const commit = composing?.field !== input || text !== composing.before;
660
+ if (commit) this.#accept(input, text, baseline?.interrupted ? baseline : void 0);
661
+ else {
662
+ this.#restoreRetained(input);
663
+ if (this.#dropHeld) this.#reconcileFields();
664
+ }
665
+ },
666
+ onAbandon: (element, inPlace) => {
667
+ this.#composing = null;
668
+ if (inPlace && element instanceof HTMLInputElement && this.fieldTargets.includes(element)) {
669
+ const baseline = this.#inputBaselines.get(element) ?? {
670
+ field: element,
671
+ before: this.#committed.get(element) ?? "",
672
+ dropHeld: this.#dropHeld,
673
+ interrupted: true
674
+ };
675
+ baseline.interrupted = true;
676
+ this.#settleResidual(element);
677
+ this.#endBaselines.set(element, { text: element.value, baseline });
388
678
  this.#reconcileFields();
679
+ } else if (this.#dropHeld) {
680
+ this.#reconcile.schedule();
389
681
  }
390
- if (input && commit) this.#accept(input, text);
391
682
  }
392
683
  });
393
684
  connect() {
394
685
  this.#connected = true;
395
686
  for (const field of this.fieldTargets) this.#bind(field);
396
687
  this.#formReset.observe();
397
- this.#beforeCache.activate();
398
- this.#reconcile.activate();
688
+ this.#reconcile.observe(this.element);
399
689
  this.#adopt();
400
690
  this.#sync();
691
+ this.#judgedBy ??= this.#patternSource;
692
+ if (this.#judgedBy !== this.#patternSource) {
693
+ if (this.#composition.isComposing()) this.#dropHeld = true;
694
+ else if (this.#dropRejected()) this.#reconcile.schedule();
695
+ }
401
696
  }
402
697
  disconnect() {
403
698
  this.#connected = false;
404
699
  for (const field of this.fieldTargets) this.#unbind(field);
405
700
  this.#composition.disconnect();
701
+ this.#clearInputBaseline();
702
+ for (const field of this.#endBaselines.keys())
703
+ if (!this.#away.has(field)) this.#endBaselines.delete(field);
406
704
  this.#formReset.disconnect();
407
- this.#beforeCache.deactivate();
408
- this.#reconcile.cancel();
705
+ this.#reconcile.disconnect();
409
706
  this.#dropHeld = false;
410
707
  this.#clearError();
411
708
  this.#state.return(this.element);
412
709
  }
413
- /**
414
- * Stimulus lifecycle callback when a new field target enters the DOM.
415
- * Wires the new field and folds the wider digit count into one reconciliation,
416
- * which stays inert until `connect()` opens the window.
417
- */
418
710
  fieldTargetConnected(element) {
419
711
  this.#bind(element);
420
712
  this.#adoptField(element);
421
713
  this.#reconcile.schedule();
422
714
  }
423
- /** Releases a dropped field's listeners and leases, then reconciles the rest. */
424
715
  fieldTargetDisconnected(element) {
425
716
  this.#unbind(element);
426
- this.#returnLeases(element);
717
+ if (!this.fieldTargets.includes(element)) this.#returnLeases(element);
718
+ this.#reconcile.schedule();
719
+ }
720
+ valueTargetConnected() {
427
721
  this.#reconcile.schedule();
428
722
  }
429
- /**
430
- * Re-validates a changed `pattern` declaration once and drops any entered digit
431
- * the new pattern no longer accepts, so the combined value stays interpretable.
432
- * The page changed the declaration, so the move joins the same reconciliation
433
- * as fields added or removed in that mutation. While a field is composing, its
434
- * text is the IME's, and the drop waits for the composition to end.
435
- */
723
+ valueTargetDisconnected() {
724
+ this.#reconcile.schedule();
725
+ }
726
+ errorTargetConnected() {
727
+ this.#followError();
728
+ }
729
+ errorTargetDisconnected(element) {
730
+ if (!this.errorTargets.includes(element)) this.#errorHidden.return(element);
731
+ this.#followError();
732
+ }
436
733
  patternValueChanged() {
437
734
  const compiled = compileRegExp(this.patternValue, "exact");
438
735
  this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
439
736
  this.#pattern = compiled ?? DEFAULT_MATCHER;
440
737
  if (!this.#connected) return;
441
- if (this.#composing !== null) {
738
+ if (this.#composition.isComposing()) {
442
739
  this.#dropHeld = true;
443
740
  return;
444
741
  }
445
742
  if (this.#dropRejected()) this.#reconcile.schedule();
446
743
  }
447
- /** Empties every writable field holding a character the pattern rejects; whether one did. */
448
- #dropRejected() {
744
+ #dropRejected(composing = null) {
745
+ if (composing === null) this.#judgedBy = this.#patternSource;
449
746
  let dropped = false;
450
747
  for (const field of this.fieldTargets) {
451
- if (field.value === "" || !this.#isWritable(field)) continue;
452
- if (this.#pattern.test(field.value)) continue;
453
- this.#writeField(field, "");
748
+ if (field === composing) {
749
+ const committed = this.#committed.get(field) ?? "";
750
+ if (committed === "" || !this.#isWritable(field) || this.#pattern.test(committed)) continue;
751
+ this.#committed.set(field, "");
752
+ dropped = true;
753
+ continue;
754
+ }
755
+ const value = this.#settledText(field);
756
+ if (value === "" || !this.#isWritable(field)) continue;
757
+ if (this.#pattern.test(value)) continue;
758
+ if (this.#retained.has(field)) this.#holdText(field, "");
759
+ else this.#writeField(field, "");
454
760
  dropped = true;
455
761
  }
456
762
  return dropped;
457
763
  }
458
- /** Handles keystroke inputs, distributes autofilled text, and advances focus. */
459
764
  onInput(event) {
460
765
  const input = this.#fieldFrom(event);
461
766
  if (!input) return;
767
+ const baseline = this.#inputBaselines.get(input);
768
+ this.#clearEdits(input);
462
769
  if (this.#composition.consumesConfirmedInput(event)) return;
463
- if (this.#composition.isComposing(event)) return;
464
- this.#accept(input);
770
+ if (this.#composition.owns(event)) return;
771
+ this.#accept(input, input.value, baseline?.interrupted ? baseline : void 0);
465
772
  }
466
- /** Handles Backspace clearing, arrows, and home/end navigation. */
467
773
  onKeydown(event) {
774
+ this.#clearInputBaseline();
468
775
  if (isReservedArrowChord(event)) return;
469
776
  const input = this.#fieldFrom(event);
470
777
  if (!input) return;
471
- if (this.#composition.isComposing(event)) return;
778
+ if (this.#composition.owns(event)) return;
779
+ if (!this.#prepareEdit(input)) return;
472
780
  const index = this.fieldTargets.indexOf(input);
473
781
  const fields = this.fieldTargets;
474
782
  switch (logicalArrowKey(event.key, this.element)) {
475
783
  case "Backspace":
476
- if (hasModifier(event)) break;
784
+ if (hasModifierChord(event)) break;
477
785
  if (input.value && this.#isWritable(input)) {
478
786
  event.preventDefault();
479
787
  this.#writeField(input, "");
@@ -507,7 +815,7 @@ var OtpController = class extends Controller {
507
815
  break;
508
816
  }
509
817
  case "Home": {
510
- if (hasModifier(event)) break;
818
+ if (hasModifierChord(event)) break;
511
819
  const first = fields.find((field) => this.#isFocusable(field));
512
820
  if (first) {
513
821
  event.preventDefault();
@@ -516,7 +824,7 @@ var OtpController = class extends Controller {
516
824
  break;
517
825
  }
518
826
  case "End": {
519
- if (hasModifier(event)) break;
827
+ if (hasModifierChord(event)) break;
520
828
  const last = fields.slice().reverse().find((field) => this.#isFocusable(field));
521
829
  if (last) {
522
830
  event.preventDefault();
@@ -526,18 +834,13 @@ var OtpController = class extends Controller {
526
834
  }
527
835
  }
528
836
  }
529
- /** Divides pasted string characters across the available input fields. */
530
837
  onPaste(event) {
531
838
  const input = this.#fieldFrom(event);
532
839
  if (!input) return;
533
840
  event.preventDefault();
534
- this.#distribute(input, toHalfWidth(event.clipboardData?.getData("text") ?? ""));
841
+ if (this.#prepareEdit(input))
842
+ this.#distribute(input, toHalfWidth(event.clipboardData?.getData("text") ?? ""), false);
535
843
  }
536
- /**
537
- * Redirects a pointer landing on an empty field to the earliest empty one, so
538
- * a passcode is entered in order. Filled fields stay directly reachable for
539
- * correction, and keyboard focus is left alone.
540
- */
541
844
  onPointerDown(event) {
542
845
  const input = this.#fieldFrom(event);
543
846
  if (input?.value !== "") return;
@@ -546,7 +849,6 @@ var OtpController = class extends Controller {
546
849
  event.preventDefault();
547
850
  first.focus();
548
851
  }
549
- /** Empties every writable field and restarts entry at the first of them. */
550
852
  clear() {
551
853
  for (const field of this.fieldTargets) {
552
854
  if (this.#isWritable(field)) this.#writeField(field, "");
@@ -555,33 +857,66 @@ var OtpController = class extends Controller {
555
857
  this.fieldTargets.find((field) => this.#isWritable(field))?.focus();
556
858
  this.#syncAndDispatch();
557
859
  }
860
+ #onBeforeInput = (event) => {
861
+ const field = event.currentTarget;
862
+ this.#clearEdits(field);
863
+ if (event.isComposing || !event.inputType || isCompositionInputType(event.inputType)) return;
864
+ this.#inputBaselines.set(field, {
865
+ field,
866
+ before: this.#committed.get(field) ?? "",
867
+ dropHeld: this.#dropHeld,
868
+ interrupted: false
869
+ });
870
+ this.#inputExpiry.set(field, () => this.#clearInputBaseline(field), 0);
871
+ };
872
+ #clearInputBaseline(field) {
873
+ if (field) {
874
+ this.#inputBaselines.delete(field);
875
+ this.#inputExpiry.clear(field);
876
+ } else {
877
+ this.#inputBaselines.clear();
878
+ this.#inputExpiry.clearAll();
879
+ }
880
+ }
881
+ #clearEdits(field) {
882
+ this.#clearInputBaseline(field);
883
+ this.#endBaselines.delete(field);
884
+ }
558
885
  #onFieldFocus = (event) => {
559
886
  const input = event.currentTarget;
560
887
  if (input) {
561
888
  input.select();
562
889
  }
563
890
  };
564
- /** Whether a form owns at least one field or the hidden combined value. */
565
891
  #ownedBy(form) {
566
892
  if (this.fieldTargets.some((field) => field.form === form)) return true;
567
893
  return this.hasValueTarget && this.valueTarget.form === form;
568
894
  }
569
895
  #bind(field) {
570
896
  field.addEventListener("focus", this.#onFieldFocus);
897
+ field.addEventListener("beforeinput", this.#onBeforeInput, true);
898
+ if (this.#away.delete(field) && document.activeElement !== field)
899
+ this.#endBaselines.delete(field);
571
900
  this.#composition.observe(field);
572
901
  }
573
902
  #unbind(field) {
574
903
  field.removeEventListener("focus", this.#onFieldFocus);
904
+ field.removeEventListener("beforeinput", this.#onBeforeInput, true);
905
+ this.#clearInputBaseline(field);
906
+ if (document.activeElement === field) this.#holdAway(field);
907
+ else this.#endBaselines.delete(field);
575
908
  this.#composition.unobserve(field);
576
- if (this.#composing?.field === field) this.#composing = null;
577
909
  }
578
- /** Reads every field back so a restored or reset group starts consistent. */
910
+ #holdAway(field) {
911
+ this.#away.add(field);
912
+ queueMicrotask(() => {
913
+ if (this.#away.delete(field)) this.#endBaselines.delete(field);
914
+ });
915
+ }
579
916
  #adopt() {
580
917
  for (const field of this.fieldTargets) this.#adoptField(field);
581
918
  this.#clearError();
582
- if (this.hasErrorTarget) this.errorTarget.setAttribute("hidden", "");
583
919
  }
584
- /** Writes one error attribute, keeping the first authored value it displaces. */
585
920
  #lease(field, attribute, value) {
586
921
  const marker = LEASE_MARKERS[attribute];
587
922
  const held = readLease(field, marker);
@@ -589,7 +924,6 @@ var OtpController = class extends Controller {
589
924
  field.setAttribute(marker, JSON.stringify({ authored, written: value }));
590
925
  field.setAttribute(attribute, value);
591
926
  }
592
- /** Gives one field's error attributes back where they still hold what this wrote. */
593
927
  #returnLeases(field) {
594
928
  for (const [attribute, marker] of Object.entries(LEASE_MARKERS)) {
595
929
  const held = readLease(field, marker);
@@ -600,66 +934,50 @@ var OtpController = class extends Controller {
600
934
  else field.setAttribute(attribute, held.authored);
601
935
  }
602
936
  }
603
- /** Takes one field's current value as the truth behind its derived state. */
604
937
  #adoptField(field) {
605
- this.#committed.set(field, field.value);
606
- this.#markFilled(field, field.value);
607
- }
608
- /**
609
- * Absorbs one batch of page changes — fields added or removed, entered digits a
610
- * new `pattern` rejects, fields a native form reset restored — as one state
611
- * transition.
612
- *
613
- * Nobody edited the passcode here, so the move is reported as `reconcile`:
614
- * automation listening for `change` must not read a re-render or a reset as an
615
- * edit, and a passcode that happens to end up full must not fire the
616
- * `complete` that submits it.
617
- *
618
- * Completeness moves on its own when the field count changes: dropping a
619
- * trailing empty field completes a passcode whose combined value never moved,
620
- * and adding one un-completes it. Comparing the whole derived state, not the
621
- * string it contains, is what makes those transitions reportable.
622
- *
623
- * A change already in the DOM — a field added or removed, a reset — is
624
- * published at once, even while a field is composing: that field counts by the
625
- * character it had committed, never by the IME's uncommitted text. Only a
626
- * `pattern` drop waits for the composition to end.
627
- */
938
+ if (this.#composition.composingElement() === field) return;
939
+ const value = this.#settledText(field);
940
+ this.#committed.set(field, value);
941
+ this.#markFilled(field, value);
942
+ }
628
943
  #reconcileFields() {
629
- if (this.#dropHeld && this.#composing === null) {
944
+ const composing = this.#composition.composingElement();
945
+ if (this.#dropHeld && composing === null) {
630
946
  this.#dropHeld = false;
631
947
  this.#dropRejected();
632
948
  }
949
+ for (const field of this.fieldTargets) {
950
+ if (field === composing) this.#markFilled(field, this.#publishedText(field));
951
+ else this.#adoptField(field);
952
+ }
633
953
  const previous = this.#published;
634
954
  const current = this.#sync();
635
- if (previous && !statesDiffer(previous, current)) return;
955
+ if (previous && !statesDiffer(previous, current)) return true;
956
+ const token = this.#moves.record();
636
957
  this.dispatch("reconcile", { detail: { value: current.value } });
958
+ return this.#moves.isLatest(token);
637
959
  }
638
- /**
639
- * Validates the text an entry point received and distributes what it accepts.
640
- * `text` defaults to the field's own value; a confirmation passes the string it
641
- * committed, which `maxlength` would otherwise have truncated.
642
- */
643
- #accept(input, text = input.value) {
960
+ #accept(input, text = input.value, baseline) {
961
+ if (!this.#prepareEdit(input, true)) return;
962
+ const before = baseline ? this.fieldTargets.map(
963
+ (field) => field === input ? baseline.dropHeld && !this.#pattern.test(baseline.before) ? "" : baseline.before : this.#publishedText(field)
964
+ ).join("") : void 0;
644
965
  const raw = toHalfWidth(text);
645
966
  if (raw === "") {
646
967
  this.#writeField(input, "");
647
968
  this.#clearError();
648
- this.#syncAndDispatch();
969
+ this.#syncAndDispatch(before);
649
970
  return;
650
971
  }
651
- this.#distribute(input, raw);
972
+ this.#distribute(input, raw, true, before);
652
973
  }
653
- /**
654
- * Fills `text`'s accepted characters into the writable fields at and after the
655
- * entry point, then leaves focus on the field after the last one filled.
656
- */
657
- #distribute(from, text) {
974
+ #distribute(from, text, spare = true, before) {
658
975
  const accepted = Array.from(text).filter((char) => this.#pattern.test(char));
659
976
  let reached = false;
977
+ const composing = spare ? this.#composition.composingElement() : null;
660
978
  const slots = this.fieldTargets.filter((field) => {
661
979
  reached ||= field === from;
662
- return reached && this.#isWritable(field);
980
+ return reached && field !== composing && this.#isWritable(field);
663
981
  });
664
982
  const filled = Math.min(accepted.length, slots.length);
665
983
  if (filled === 0) {
@@ -674,16 +992,76 @@ var OtpController = class extends Controller {
674
992
  if (field && char) this.#writeField(field, char);
675
993
  }
676
994
  const last = slots[filled - 1];
677
- if (last) (this.#writableAfter(last) ?? last).focus();
995
+ if (last && composing === null) (this.#writableAfter(last) ?? last).focus();
678
996
  this.#clearError();
679
- this.#syncAndDispatch();
997
+ this.#syncAndDispatch(before);
998
+ }
999
+ #prepareEdit(input, edit = false) {
1000
+ if (!this.#dropHeld) return true;
1001
+ if (edit) this.#holdText(input, this.#committed.get(input) ?? "");
1002
+ const composing = this.#composition.composingElement();
1003
+ if (composing !== null) this.#dropRejected(composing);
1004
+ return this.#reconcileFields() && this.#connected && this.fieldTargets.includes(input) && this.#composition.composingElement() === composing;
1005
+ }
1006
+ #settleResidual(field) {
1007
+ const raw = toHalfWidth(field.value);
1008
+ const previous = this.#committed.get(field) ?? "";
1009
+ const accepts = (text) => Array.from(text).length === 1 && this.#pattern.test(text);
1010
+ const value = raw === "" || accepts(raw) ? raw : accepts(previous) ? previous : "";
1011
+ if (this.#isWritable(field) && this.#composition.composingElement() !== field) {
1012
+ const { selectionStart, selectionEnd } = field;
1013
+ this.#writeField(field, value);
1014
+ if (selectionStart !== null && selectionEnd !== null)
1015
+ field.setSelectionRange(selectionStart, selectionEnd);
1016
+ } else this.#holdText(field, value);
1017
+ }
1018
+ #holdText(field, value) {
1019
+ const retained = { text: field.value, value };
1020
+ this.#retained.set(field, retained);
1021
+ field.setAttribute(this.#retainedMarker, JSON.stringify(retained));
1022
+ this.#committed.set(field, value);
1023
+ }
1024
+ #settledText(field) {
1025
+ const ending = this.#endBaselines.get(field);
1026
+ if (ending && ending.text !== field.value) this.#clearEdits(field);
1027
+ const retained = this.#retained.get(field) ?? this.#readRetained(field);
1028
+ if (retained?.text === field.value) {
1029
+ this.#retained.set(field, retained);
1030
+ const marker = JSON.stringify(retained);
1031
+ if (field.getAttribute(this.#retainedMarker) !== marker)
1032
+ field.setAttribute(this.#retainedMarker, marker);
1033
+ return retained.value;
1034
+ }
1035
+ this.#retained.delete(field);
1036
+ field.removeAttribute(this.#retainedMarker);
1037
+ return field.value;
1038
+ }
1039
+ get #retainedMarker() {
1040
+ return `data-${this.identifier}-interrupted`;
1041
+ }
1042
+ #readRetained(field) {
1043
+ try {
1044
+ const record = JSON.parse(field.getAttribute(this.#retainedMarker) ?? "null");
1045
+ if (typeof record === "object" && record !== null && "text" in record && "value" in record && typeof record.text === "string" && typeof record.value === "string") {
1046
+ return { text: record.text, value: record.value };
1047
+ }
1048
+ } catch {
1049
+ return null;
1050
+ }
1051
+ return null;
1052
+ }
1053
+ #restoreRetained(field) {
1054
+ const retained = this.#retained.get(field);
1055
+ if (retained?.text === field.value && this.#isWritable(field))
1056
+ this.#writeField(field, retained.value);
680
1057
  }
681
- /** Restores the digit a field committed before rejected input replaced it. */
682
1058
  #restore(field) {
683
1059
  this.#writeField(field, this.#committed.get(field) ?? "");
684
1060
  }
685
- /** Commits one field's value and the derived hook that reports it as entered. */
686
1061
  #writeField(field, value) {
1062
+ this.#retained.delete(field);
1063
+ this.#endBaselines.delete(field);
1064
+ field.removeAttribute(this.#retainedMarker);
687
1065
  field.value = value;
688
1066
  this.#committed.set(field, value);
689
1067
  this.#markFilled(field, value);
@@ -711,11 +1089,9 @@ var OtpController = class extends Controller {
711
1089
  #focusableBefore(index) {
712
1090
  return this.#before(index).find((field) => this.#isFocusable(field)) ?? null;
713
1091
  }
714
- /** Fields before `index`, nearest first; empty at the first field. */
715
1092
  #before(index) {
716
1093
  return this.fieldTargets.slice(0, Math.max(index, 0)).reverse();
717
1094
  }
718
- /** The event's field target, or `null` when the wiring points somewhere else. */
719
1095
  #fieldFrom(event) {
720
1096
  const input = event.currentTarget;
721
1097
  return this.fieldTargets.find((field) => field === input) ?? null;
@@ -723,19 +1099,13 @@ var OtpController = class extends Controller {
723
1099
  #combinedValue() {
724
1100
  return this.fieldTargets.map((field) => this.#publishedText(field)).join("");
725
1101
  }
726
- /** Every field carries a character, and there is at least one field. */
727
1102
  #isComplete() {
728
1103
  const fields = this.fieldTargets;
729
1104
  return fields.length > 0 && fields.every((field) => this.#publishedText(field).length > 0);
730
1105
  }
731
- /** A field's text as the combined value counts it: a composing field by its committed character. */
732
1106
  #publishedText(field) {
733
- return field === this.#composing?.field ? this.#committed.get(field) ?? "" : field.value;
1107
+ return field === this.#composition.composingElement() ? this.#committed.get(field) ?? "" : this.#settledText(field);
734
1108
  }
735
- /**
736
- * Mirrors the combined value into the form and the root's readable state, and
737
- * records what was published so the next pass can compare against it.
738
- */
739
1109
  #sync() {
740
1110
  const combined = this.#combinedValue();
741
1111
  if (this.hasValueTarget) {
@@ -751,35 +1121,68 @@ var OtpController = class extends Controller {
751
1121
  if (combined.length === 0) return "empty";
752
1122
  return this.#isComplete() ? "complete" : "partial";
753
1123
  }
754
- #syncAndDispatch() {
1124
+ #syncAndDispatch(before) {
755
1125
  const previous = this.#published;
756
- const { value: combined } = this.#sync();
757
- if (previous?.value === combined) return;
1126
+ const current = this.#sync();
1127
+ const combined = current.value;
1128
+ if ((before ?? previous?.value) === combined) {
1129
+ if (previous && statesDiffer(previous, current)) {
1130
+ this.#moves.record();
1131
+ this.dispatch("reconcile", { detail: { value: combined } });
1132
+ }
1133
+ return;
1134
+ }
1135
+ const token = this.#moves.record();
1136
+ this.#endBaselines.clear();
758
1137
  this.dispatch("change", { detail: { value: combined } });
1138
+ if (!this.#moves.isLatest(token)) return;
759
1139
  if (this.#isComplete()) {
760
1140
  this.dispatch("complete", { detail: { value: combined } });
761
1141
  }
762
1142
  }
763
- /** Surfaces rejected input on every field and on the optional error target. */
764
1143
  #showError() {
765
1144
  const errorId = this.hasErrorTarget ? ensureId(this.errorTarget, "stimeo--otp-error") : null;
766
1145
  for (const field of this.fieldTargets) {
767
1146
  this.#lease(field, "aria-invalid", "true");
768
- if (!errorId) continue;
769
- this.#lease(field, "aria-errormessage", errorId);
770
- const described = field.getAttribute("aria-describedby");
771
- this.#lease(field, "aria-describedby", describedByWith(described, errorId));
1147
+ if (errorId) this.#pointAtError(field, errorId);
772
1148
  }
773
1149
  if (this.hasErrorTarget) this.#errorHidden.write(this.errorTarget, null);
774
1150
  this.dispatch("invalid", { detail: { pattern: this.#patternSource } });
775
1151
  }
776
- /** Returns every error lease, restoring the authored error surface. */
1152
+ #pointAtError(field, errorId) {
1153
+ const earlier = readLease(field, LEASE_MARKERS["aria-errormessage"])?.written ?? null;
1154
+ this.#lease(field, "aria-errormessage", errorId);
1155
+ const described = field.getAttribute("aria-describedby");
1156
+ this.#lease(field, "aria-describedby", describedByWith(described, errorId, earlier));
1157
+ }
1158
+ #followError() {
1159
+ if (!this.#connected) return;
1160
+ const reported = this.fieldTargets.filter(
1161
+ (field) => readLease(field, LEASE_MARKERS["aria-invalid"]) !== null
1162
+ );
1163
+ if (reported.length === 0) {
1164
+ this.#hideErrors();
1165
+ return;
1166
+ }
1167
+ if (!this.hasErrorTarget) return;
1168
+ const errorId = ensureId(this.errorTarget, "stimeo--otp-error");
1169
+ for (const field of reported) this.#pointAtError(field, errorId);
1170
+ this.#errorHidden.write(this.errorTarget, null);
1171
+ }
777
1172
  #clearError() {
778
1173
  for (const field of this.fieldTargets) this.#returnLeases(field);
779
- this.#errorHidden.returnAll();
1174
+ if (!this.#connected) {
1175
+ this.#errorHidden.returnAll();
1176
+ return;
1177
+ }
1178
+ this.#hideErrors();
1179
+ }
1180
+ #hideErrors() {
1181
+ const [first, ...others] = this.errorTargets;
1182
+ for (const other of others) this.#errorHidden.return(other);
1183
+ if (first) this.#errorHidden.write(first, "");
780
1184
  }
781
1185
  };
782
1186
 
783
1187
  export { OtpController };
784
- //# sourceMappingURL=otp_controller.js.map
785
1188
  //# sourceMappingURL=otp_controller.js.map