stimeo-ui 0.14.0 → 0.16.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 (279) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/README.md +120 -0
  3. package/dist/cable/index.js +123 -29
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +30 -3
  6. package/dist/controllers/accordion_controller.js +98 -9
  7. package/dist/controllers/accordion_controller.js.map +1 -1
  8. package/dist/controllers/alert_dialog_controller.d.ts +8 -7
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  10. package/dist/controllers/announcer_controller.d.ts +3 -2
  11. package/dist/controllers/announcer_controller.js +96 -62
  12. package/dist/controllers/announcer_controller.js.map +1 -1
  13. package/dist/controllers/auto_submit_controller.js +83 -7
  14. package/dist/controllers/auto_submit_controller.js.map +1 -1
  15. package/dist/controllers/avatar_controller.js +1 -1
  16. package/dist/controllers/avatar_controller.js.map +1 -1
  17. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  18. package/dist/controllers/breadcrumb_controller.js +38 -11
  19. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  20. package/dist/controllers/bulk_select_controller.d.ts +4 -4
  21. package/dist/controllers/bulk_select_controller.js +7 -6
  22. package/dist/controllers/bulk_select_controller.js.map +1 -1
  23. package/dist/controllers/calendar_controller.d.ts +101 -23
  24. package/dist/controllers/calendar_controller.js +340 -123
  25. package/dist/controllers/calendar_controller.js.map +1 -1
  26. package/dist/controllers/carousel_controller.d.ts +67 -22
  27. package/dist/controllers/carousel_controller.js +263 -38
  28. package/dist/controllers/carousel_controller.js.map +1 -1
  29. package/dist/controllers/character_counter_controller.d.ts +1 -1
  30. package/dist/controllers/character_counter_controller.js +40 -2
  31. package/dist/controllers/character_counter_controller.js.map +1 -1
  32. package/dist/controllers/checkbox_controller.js +81 -12
  33. package/dist/controllers/checkbox_controller.js.map +1 -1
  34. package/dist/controllers/clipboard_controller.d.ts +28 -2
  35. package/dist/controllers/clipboard_controller.js +63 -5
  36. package/dist/controllers/clipboard_controller.js.map +1 -1
  37. package/dist/controllers/collapsible_controller.d.ts +25 -1
  38. package/dist/controllers/collapsible_controller.js +99 -14
  39. package/dist/controllers/collapsible_controller.js.map +1 -1
  40. package/dist/controllers/color_picker_controller.d.ts +29 -8
  41. package/dist/controllers/color_picker_controller.js +80 -34
  42. package/dist/controllers/color_picker_controller.js.map +1 -1
  43. package/dist/controllers/combobox_controller.d.ts +10 -1
  44. package/dist/controllers/combobox_controller.js +106 -16
  45. package/dist/controllers/combobox_controller.js.map +1 -1
  46. package/dist/controllers/command_palette_controller.d.ts +3 -3
  47. package/dist/controllers/command_palette_controller.js +35 -3
  48. package/dist/controllers/command_palette_controller.js.map +1 -1
  49. package/dist/controllers/conditional_fields_controller.js +85 -17
  50. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  51. package/dist/controllers/confirm_controller.js +3 -0
  52. package/dist/controllers/confirm_controller.js.map +1 -1
  53. package/dist/controllers/context_menu_controller.d.ts +6 -0
  54. package/dist/controllers/context_menu_controller.js +32 -12
  55. package/dist/controllers/context_menu_controller.js.map +1 -1
  56. package/dist/controllers/count_up_controller.js.map +1 -1
  57. package/dist/controllers/countdown_controller.d.ts +30 -1
  58. package/dist/controllers/countdown_controller.js +129 -26
  59. package/dist/controllers/countdown_controller.js.map +1 -1
  60. package/dist/controllers/currency_input_controller.d.ts +77 -16
  61. package/dist/controllers/currency_input_controller.js +221 -67
  62. package/dist/controllers/currency_input_controller.js.map +1 -1
  63. package/dist/controllers/data_grid_controller.d.ts +63 -18
  64. package/dist/controllers/data_grid_controller.js +195 -29
  65. package/dist/controllers/data_grid_controller.js.map +1 -1
  66. package/dist/controllers/date_range_picker_controller.d.ts +41 -6
  67. package/dist/controllers/date_range_picker_controller.js +151 -30
  68. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  69. package/dist/controllers/dialog_controller.d.ts +10 -3
  70. package/dist/controllers/dialog_controller.js +35 -8
  71. package/dist/controllers/dialog_controller.js.map +1 -1
  72. package/dist/controllers/direct_upload_controller.js +22 -4
  73. package/dist/controllers/direct_upload_controller.js.map +1 -1
  74. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  75. package/dist/controllers/dirty_form_controller.js +46 -13
  76. package/dist/controllers/dirty_form_controller.js.map +1 -1
  77. package/dist/controllers/dismissible_controller.js +1 -0
  78. package/dist/controllers/dismissible_controller.js.map +1 -1
  79. package/dist/controllers/drawer_controller.d.ts +18 -10
  80. package/dist/controllers/drawer_controller.js +54 -19
  81. package/dist/controllers/drawer_controller.js.map +1 -1
  82. package/dist/controllers/dropdown_controller.d.ts +9 -3
  83. package/dist/controllers/dropdown_controller.js +36 -9
  84. package/dist/controllers/dropdown_controller.js.map +1 -1
  85. package/dist/controllers/editable_controller.js +34 -0
  86. package/dist/controllers/editable_controller.js.map +1 -1
  87. package/dist/controllers/file_dropzone_controller.js +144 -51
  88. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  89. package/dist/controllers/filter_controller.d.ts +10 -4
  90. package/dist/controllers/filter_controller.js +20 -6
  91. package/dist/controllers/filter_controller.js.map +1 -1
  92. package/dist/controllers/flash_controller.d.ts +26 -6
  93. package/dist/controllers/flash_controller.js +432 -71
  94. package/dist/controllers/flash_controller.js.map +1 -1
  95. package/dist/controllers/focus_controller.js +1 -0
  96. package/dist/controllers/focus_controller.js.map +1 -1
  97. package/dist/controllers/form_field_controller.js +7 -5
  98. package/dist/controllers/form_field_controller.js.map +1 -1
  99. package/dist/controllers/form_validation_controller.js +19 -13
  100. package/dist/controllers/form_validation_controller.js.map +1 -1
  101. package/dist/controllers/frame_loading_controller.js +45 -8
  102. package/dist/controllers/frame_loading_controller.js.map +1 -1
  103. package/dist/controllers/highlight_controller.js +82 -25
  104. package/dist/controllers/highlight_controller.js.map +1 -1
  105. package/dist/controllers/hover_card_controller.d.ts +10 -2
  106. package/dist/controllers/hover_card_controller.js +40 -14
  107. package/dist/controllers/hover_card_controller.js.map +1 -1
  108. package/dist/controllers/idle_controller.d.ts +16 -3
  109. package/dist/controllers/idle_controller.js +90 -5
  110. package/dist/controllers/idle_controller.js.map +1 -1
  111. package/dist/controllers/input_mask_controller.d.ts +5 -2
  112. package/dist/controllers/input_mask_controller.js +65 -9
  113. package/dist/controllers/input_mask_controller.js.map +1 -1
  114. package/dist/controllers/intersection_controller.js +3 -0
  115. package/dist/controllers/intersection_controller.js.map +1 -1
  116. package/dist/controllers/lazy_frame_controller.js +11 -2
  117. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  118. package/dist/controllers/listbox_controller.d.ts +50 -8
  119. package/dist/controllers/listbox_controller.js +203 -45
  120. package/dist/controllers/listbox_controller.js.map +1 -1
  121. package/dist/controllers/local_time_controller.js +10 -5
  122. package/dist/controllers/local_time_controller.js.map +1 -1
  123. package/dist/controllers/masonry_controller.d.ts +3 -3
  124. package/dist/controllers/masonry_controller.js +31 -15
  125. package/dist/controllers/masonry_controller.js.map +1 -1
  126. package/dist/controllers/menu_controller.d.ts +9 -3
  127. package/dist/controllers/menu_controller.js +45 -16
  128. package/dist/controllers/menu_controller.js.map +1 -1
  129. package/dist/controllers/menubar_controller.d.ts +11 -0
  130. package/dist/controllers/menubar_controller.js +58 -24
  131. package/dist/controllers/menubar_controller.js.map +1 -1
  132. package/dist/controllers/meter_controller.js +9 -5
  133. package/dist/controllers/meter_controller.js.map +1 -1
  134. package/dist/controllers/multi_select_controller.d.ts +18 -3
  135. package/dist/controllers/multi_select_controller.js +278 -104
  136. package/dist/controllers/multi_select_controller.js.map +1 -1
  137. package/dist/controllers/navigation_menu_controller.d.ts +11 -0
  138. package/dist/controllers/navigation_menu_controller.js +48 -15
  139. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  140. package/dist/controllers/nested_form_controller.js +37 -8
  141. package/dist/controllers/nested_form_controller.js.map +1 -1
  142. package/dist/controllers/network_status_controller.js +9 -1
  143. package/dist/controllers/network_status_controller.js.map +1 -1
  144. package/dist/controllers/number_input_controller.d.ts +36 -8
  145. package/dist/controllers/number_input_controller.js +124 -21
  146. package/dist/controllers/number_input_controller.js.map +1 -1
  147. package/dist/controllers/optimistic_controller.js +42 -5
  148. package/dist/controllers/optimistic_controller.js.map +1 -1
  149. package/dist/controllers/otp_controller.d.ts +22 -7
  150. package/dist/controllers/otp_controller.js +198 -55
  151. package/dist/controllers/otp_controller.js.map +1 -1
  152. package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
  153. package/dist/controllers/overflow_indicator_controller.js +115 -21
  154. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  155. package/dist/controllers/overflow_menu_controller.d.ts +26 -6
  156. package/dist/controllers/overflow_menu_controller.js +141 -44
  157. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  158. package/dist/controllers/pagination_controller.d.ts +19 -11
  159. package/dist/controllers/pagination_controller.js +74 -28
  160. package/dist/controllers/pagination_controller.js.map +1 -1
  161. package/dist/controllers/password_reveal_controller.d.ts +15 -1
  162. package/dist/controllers/password_reveal_controller.js +59 -2
  163. package/dist/controllers/password_reveal_controller.js.map +1 -1
  164. package/dist/controllers/persist_controller.js +30 -8
  165. package/dist/controllers/persist_controller.js.map +1 -1
  166. package/dist/controllers/pointer_drag_controller.js +131 -52
  167. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  168. package/dist/controllers/popover_controller.d.ts +9 -3
  169. package/dist/controllers/popover_controller.js +45 -11
  170. package/dist/controllers/popover_controller.js.map +1 -1
  171. package/dist/controllers/portal_controller.d.ts +1 -1
  172. package/dist/controllers/portal_controller.js +6 -2
  173. package/dist/controllers/portal_controller.js.map +1 -1
  174. package/dist/controllers/preview_guard_controller.js +16 -1
  175. package/dist/controllers/preview_guard_controller.js.map +1 -1
  176. package/dist/controllers/progress_controller.js +8 -4
  177. package/dist/controllers/progress_controller.js.map +1 -1
  178. package/dist/controllers/radio_group_controller.d.ts +6 -4
  179. package/dist/controllers/radio_group_controller.js +42 -17
  180. package/dist/controllers/radio_group_controller.js.map +1 -1
  181. package/dist/controllers/range_slider_controller.d.ts +49 -1
  182. package/dist/controllers/range_slider_controller.js +88 -42
  183. package/dist/controllers/range_slider_controller.js.map +1 -1
  184. package/dist/controllers/rating_controller.d.ts +14 -3
  185. package/dist/controllers/rating_controller.js +39 -15
  186. package/dist/controllers/rating_controller.js.map +1 -1
  187. package/dist/controllers/read_more_controller.d.ts +24 -2
  188. package/dist/controllers/read_more_controller.js +100 -7
  189. package/dist/controllers/read_more_controller.js.map +1 -1
  190. package/dist/controllers/reading_progress_controller.js +65 -19
  191. package/dist/controllers/reading_progress_controller.js.map +1 -1
  192. package/dist/controllers/relative_time_controller.js +10 -5
  193. package/dist/controllers/relative_time_controller.js.map +1 -1
  194. package/dist/controllers/resizable_controller.d.ts +16 -2
  195. package/dist/controllers/resizable_controller.js +82 -22
  196. package/dist/controllers/resizable_controller.js.map +1 -1
  197. package/dist/controllers/scroll_area_controller.js +75 -27
  198. package/dist/controllers/scroll_area_controller.js.map +1 -1
  199. package/dist/controllers/scroll_restore_controller.js +37 -16
  200. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  201. package/dist/controllers/scroll_visibility_controller.js +49 -30
  202. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  203. package/dist/controllers/scrollspy_controller.d.ts +3 -2
  204. package/dist/controllers/scrollspy_controller.js +71 -26
  205. package/dist/controllers/scrollspy_controller.js.map +1 -1
  206. package/dist/controllers/separator_controller.d.ts +41 -14
  207. package/dist/controllers/separator_controller.js +66 -37
  208. package/dist/controllers/separator_controller.js.map +1 -1
  209. package/dist/controllers/sidebar_controller.d.ts +20 -3
  210. package/dist/controllers/sidebar_controller.js +77 -18
  211. package/dist/controllers/sidebar_controller.js.map +1 -1
  212. package/dist/controllers/skeleton_controller.js +6 -1
  213. package/dist/controllers/skeleton_controller.js.map +1 -1
  214. package/dist/controllers/slider_controller.d.ts +45 -7
  215. package/dist/controllers/slider_controller.js +82 -47
  216. package/dist/controllers/slider_controller.js.map +1 -1
  217. package/dist/controllers/smart_sticky_header_controller.js +60 -26
  218. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  219. package/dist/controllers/sortable_controller.js +17 -2
  220. package/dist/controllers/sortable_controller.js.map +1 -1
  221. package/dist/controllers/spinner_controller.js +10 -2
  222. package/dist/controllers/spinner_controller.js.map +1 -1
  223. package/dist/controllers/step_indicator_controller.d.ts +19 -17
  224. package/dist/controllers/step_indicator_controller.js +18 -17
  225. package/dist/controllers/step_indicator_controller.js.map +1 -1
  226. package/dist/controllers/stepper_controller.d.ts +34 -9
  227. package/dist/controllers/stepper_controller.js +101 -19
  228. package/dist/controllers/stepper_controller.js.map +1 -1
  229. package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
  230. package/dist/controllers/stick_to_bottom_controller.js +104 -8
  231. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  232. package/dist/controllers/submit_once_controller.d.ts +3 -2
  233. package/dist/controllers/submit_once_controller.js +45 -9
  234. package/dist/controllers/submit_once_controller.js.map +1 -1
  235. package/dist/controllers/switch_controller.d.ts +29 -7
  236. package/dist/controllers/switch_controller.js +101 -10
  237. package/dist/controllers/switch_controller.js.map +1 -1
  238. package/dist/controllers/tabs_controller.d.ts +12 -0
  239. package/dist/controllers/tabs_controller.js +21 -2
  240. package/dist/controllers/tabs_controller.js.map +1 -1
  241. package/dist/controllers/tags_input_controller.d.ts +15 -3
  242. package/dist/controllers/tags_input_controller.js +209 -59
  243. package/dist/controllers/tags_input_controller.js.map +1 -1
  244. package/dist/controllers/textarea_autosize_controller.js +29 -3
  245. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  246. package/dist/controllers/theme_controller.d.ts +20 -3
  247. package/dist/controllers/theme_controller.js +64 -14
  248. package/dist/controllers/theme_controller.js.map +1 -1
  249. package/dist/controllers/time_picker_controller.js +23 -8
  250. package/dist/controllers/time_picker_controller.js.map +1 -1
  251. package/dist/controllers/toast_controller.d.ts +55 -15
  252. package/dist/controllers/toast_controller.js +451 -105
  253. package/dist/controllers/toast_controller.js.map +1 -1
  254. package/dist/controllers/toggle_group_controller.d.ts +49 -7
  255. package/dist/controllers/toggle_group_controller.js +159 -23
  256. package/dist/controllers/toggle_group_controller.js.map +1 -1
  257. package/dist/controllers/toolbar_controller.js +32 -0
  258. package/dist/controllers/toolbar_controller.js.map +1 -1
  259. package/dist/controllers/tooltip_controller.d.ts +8 -0
  260. package/dist/controllers/tooltip_controller.js +39 -13
  261. package/dist/controllers/tooltip_controller.js.map +1 -1
  262. package/dist/controllers/transition_controller.js +4 -0
  263. package/dist/controllers/transition_controller.js.map +1 -1
  264. package/dist/controllers/tree_view_controller.d.ts +39 -8
  265. package/dist/controllers/tree_view_controller.js +169 -16
  266. package/dist/controllers/tree_view_controller.js.map +1 -1
  267. package/dist/index.d.ts +28 -1
  268. package/dist/index.js +5002 -1911
  269. package/dist/index.js.map +1 -1
  270. package/dist/inspector/cli.d.ts +72 -6
  271. package/dist/inspector/cli.js +262 -51
  272. package/dist/inspector/cli.js.map +1 -1
  273. package/dist/inspector/cli_bin.js +309 -51
  274. package/dist/inspector/cli_bin.js.map +1 -1
  275. package/dist/inspector/examples.json +40 -40
  276. package/dist/inspector/manifest.json +529 -61
  277. package/dist/positioning/index.js +2 -0
  278. package/dist/positioning/index.js.map +1 -1
  279. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/detach_gate.ts","../../src/controllers/optimistic_controller.ts"],"names":[],"mappings":"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","file":"optimistic_controller.js","sourcesContent":["/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer; this util owns the discrimination only:\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { DetachGate } from \"../utils/detach_gate\";\n\n/** Records that this controller wrote `hidden`, and the authored value to put back. */\nconst HIDDEN_MARKER = \"data-optimistic-toggled\";\n\n/** The same record for `aria-busy` on the controller element. */\nconst BUSY_MARKER = \"data-optimistic-busy\";\n\n/** Marker value standing for \"the attribute was not there\". */\nconst AUTHORED_ABSENT = \"absent\";\n\n/** Marker prefix carrying the authored attribute value that follows it. */\nconst AUTHORED_VALUE = \"value:\";\n\n/**\n * Stands in for the submission a synthetic lifecycle event does not name. Turbo\n * puts a `formSubmission` on both events, so this only ever pairs hand-dispatched\n * events with each other — one shared instance, so a start and its end match.\n */\nconst SYNTHETIC_SUBMISSION = {};\n\n/** The submission a Turbo lifecycle event belongs to. */\nfunction submissionOf(event: Event): unknown {\n const detail = (event as CustomEvent<{ formSubmission?: unknown }>).detail;\n return detail?.formSubmission ?? SYNTHETIC_SUBMISSION;\n}\n\n/**\n * Headless **optimistic UI** for Turbo form submissions — the one server-bound\n * behavior that needs no Action Cable: it wraps a Turbo form\n * and applies a *declared* optimistic state the moment the submission starts,\n * keeps it when the server confirms, and **rolls it back** when the submission\n * fails. The pattern: flip the like button instantly; the success response (a\n * Turbo Stream) replaces the fragment with the server truth anyway, so the\n * client only needs the instant flip and the failure rollback. Core (zero\n * dependencies — it rides Turbo's own `turbo:submit-*` events).\n *\n * Markup contract (identifier: `stimeo--optimistic`):\n * <form data-controller=\"stimeo--optimistic\" method=\"post\" action=\"/likes\">\n * <button type=\"submit\" aria-label=\"Like\">\n * <span data-stimeo--optimistic-target=\"hide\">♡</span>\n * <span hidden data-stimeo--optimistic-target=\"show\">♥</span>\n * </button>\n * </form>\n *\n * On `turbo:submit-start`, every `show` target is unhidden and every `hide`\n * target hidden, the element gains `data-optimistic=\"true\"` + `aria-busy=\"true\"`,\n * and the submission named by the event takes ownership of that state. On\n * `turbo:submit-end`: the terminal of any *other* submission is ignored; the\n * owner's terminal clears the pending hook and, on success, dispatches `commit`\n * (the toggled state stays — the server response owns the final DOM), or on\n * failure restores the authored markup and dispatches `rollback`.\n *\n * **Every write records what it displaced.** A target this controller hides or\n * reveals carries the authored `hidden` value in `data-optimistic-toggled`, and\n * the element carries the authored `aria-busy` in `data-optimistic-busy`, so a\n * rollback puts back exactly what the author wrote — not the inverse of whatever\n * the attribute says at that moment. The record lives in the DOM rather than in\n * memory because a Turbo cache snapshot has to carry it: `connect()` is what\n * rewinds a restored page.\n *\n * @remarks\n * Behavior only — what \"optimistic\" looks like is the author's markup (the\n * show/hide pair) and CSS (`[data-optimistic=\"true\"]`). Pairs with\n * `stimeo--submit-once` (double-submit guard) and `stimeo--live-counter`\n * (optimistic numbers). The listeners are delegated on the element, so a form\n * nested under it (or swapped for a new one) keeps working, and the submission\n * ownership is what keeps a sibling form's terminal from resolving this state.\n * With several submissions in flight the newest start owns the optimistic state,\n * so a submission whose terminal never arrives — its form replaced mid-flight —\n * cannot strand the element. `connect()` is idempotent: a Turbo cache snapshot\n * taken mid-submit is rewound, since no submission can be in flight across a\n * restore, while an in-page move (where the submission *does* survive) keeps it.\n * On a real detach the listeners go with the element.\n */\nexport class OptimisticController extends Controller<HTMLElement> {\n static override targets = [\"show\", \"hide\"];\n static events = [\"commit\", \"rollback\"] as const;\n\n declare readonly showTargets: HTMLElement[];\n declare readonly hideTargets: HTMLElement[];\n\n /** The submission that owns the applied optimistic state, if any. */\n #pending: unknown = null;\n\n readonly #gate = new DetachGate();\n\n /** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */\n #connected = false;\n\n override connect(): void {\n // Only a page this instance did not apply the state to can be holding a stale\n // one: a restored snapshot. An in-page move and a repeat connect both leave a\n // live submission behind, so neither may rewind — the gate names the first and\n // the connected flag the second.\n const restored = !this.#connected && !this.#gate.pending;\n this.#gate.cancel();\n this.#connected = true;\n if (restored && this.element.hasAttribute(\"data-optimistic\")) this.#revert();\n this.element.addEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.addEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\n // Ownership outlives an in-page move: only a real detach forgets it.\n this.#gate.disconnected(this, () => {\n this.#pending = null;\n });\n }\n\n readonly #onSubmitStart = (event: Event): void => {\n this.#pending = submissionOf(event);\n // Turbo owns `aria-busy` on a form and marks it before this event, so what is\n // there now is Turbo's, not the author's. Recording it would put Turbo's value\n // back on a page restored mid-submit, where no terminal ever arrives to clear it.\n if (this.element instanceof HTMLFormElement && !this.element.hasAttribute(BUSY_MARKER)) {\n this.element.setAttribute(BUSY_MARKER, AUTHORED_ABSENT);\n }\n this.#write(this.element, \"aria-busy\", \"true\", BUSY_MARKER);\n this.element.setAttribute(\"data-optimistic\", \"true\");\n for (const target of this.showTargets) this.#setHidden(target, false);\n for (const target of this.hideTargets) this.#setHidden(target, true);\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n // A terminal always names a submission, so an element holding none (nothing\n // started, or this one already resolved) falls out here too.\n if (submissionOf(event) !== this.#pending) return;\n this.#pending = null;\n\n const success = (event as CustomEvent<{ success?: boolean }>).detail?.success === true;\n if (success) {\n // Keep the toggled faces (the server response owns the final DOM) but drop\n // their records, so a later failure cannot revert a confirmed state.\n this.element.removeAttribute(\"data-optimistic\");\n this.#restore(this.element, \"aria-busy\", BUSY_MARKER, \"true\");\n for (const target of [...this.showTargets, ...this.hideTargets]) {\n target.removeAttribute(HIDDEN_MARKER);\n }\n this.dispatch(\"commit\");\n } else {\n this.#revert();\n this.dispatch(\"rollback\");\n }\n };\n\n /** Hides or reveals a target, recording the authored `hidden` on first write. */\n #setHidden(target: HTMLElement, hidden: boolean): void {\n if (target.hasAttribute(\"hidden\") === hidden) return;\n this.#write(target, \"hidden\", hidden ? \"\" : null, HIDDEN_MARKER);\n }\n\n /** Writes an attribute, recording what it displaced under `marker` once. */\n #write(element: Element, attribute: string, value: string | null, marker: string): void {\n if (!element.hasAttribute(marker)) {\n const authored = element.getAttribute(attribute);\n element.setAttribute(\n marker,\n authored === null ? AUTHORED_ABSENT : `${AUTHORED_VALUE}${authored}`,\n );\n }\n if (value === null) element.removeAttribute(attribute);\n else element.setAttribute(attribute, value);\n }\n\n /**\n * Puts back the value `marker` recorded and drops the record.\n *\n * `written` is what this controller last put in the attribute. Anything else\n * there now belongs to whoever wrote it — Turbo owns `aria-busy` on a form and\n * clears it before the terminal arrives, and a consumer may have moved a face\n * mid-flight — so the record is dropped without touching the attribute. A\n * marker this build did not write names no authored value, so the attribute\n * goes back to being absent.\n */\n #restore(element: Element, attribute: string, marker: string, written: string | null): void {\n const recorded = element.getAttribute(marker);\n if (recorded === null) return;\n element.removeAttribute(marker);\n if (element.getAttribute(attribute) !== written) return;\n if (recorded.startsWith(AUTHORED_VALUE)) {\n element.setAttribute(attribute, recorded.slice(AUTHORED_VALUE.length));\n return;\n }\n element.removeAttribute(attribute);\n }\n\n /**\n * Puts back exactly the markup this controller displaced (record-owned only).\n *\n * A face can be registered on both sides, and the hide pass runs last, so what\n * this controller wrote there is `hidden` — matching it against the reveal's\n * absent value instead would read as someone else's write and leave the face\n * hidden. Each target is therefore restored once, against the write it last\n * received.\n */\n #revert(): void {\n this.element.removeAttribute(\"data-optimistic\");\n this.#restore(this.element, \"aria-busy\", BUSY_MARKER, \"true\");\n const hidden = new Set(this.hideTargets);\n for (const target of new Set([...this.showTargets, ...this.hideTargets])) {\n this.#restore(target, \"hidden\", HIDDEN_MARKER, hidden.has(target) ? \"\" : null);\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/detach_gate.ts","../../src/utils/listener_set.ts","../../src/utils/transient_hooks.ts","../../src/controllers/optimistic_controller.ts"],"names":[],"mappings":"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","file":"optimistic_controller.js","sourcesContent":["/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer; this util owns the discrimination only:\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True while a probe is queued — the last disconnect was ambiguous and no\n * reconnect has cancelled it yet. Read it from `connect()` to tell the\n * reconnect half of an in-page move from a first connect: a controller whose\n * initialisation restarts a measurement (a min-duration floor, an elapsed\n * counter) must skip it for the move, where nothing actually restarted.\n */\n get pending(): boolean {\n return this.#pending;\n }\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, so `dispose()` belongs exactly where\n * the `removeEventListener` block stood. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Drops the transient state hooks a connection inherited, so the cycle it opens\n * starts from a DOM that claims nothing.\n *\n * A transient hook is an attribute a controller writes to say \"this is happening\n * right now\" — a drag in progress, a form with unsaved edits, a refused\n * subscription. Its lifetime belongs to the interaction, not to the element, and\n * the two come apart whenever the element outlives the instance that wrote it: a\n * Turbo cache snapshot taken mid-interaction restores the hook with the markup,\n * and an element re-inserted elsewhere in the page carries it along. Nothing\n * takes such a hook off, because the instance that owned it is gone; consumer\n * CSS then keeps claiming a state no one is in.\n *\n * **`turbo:before-cache` cannot do this job**, which is why the pass belongs in\n * `connect()`: that event fires for a navigation only, so an in-page move — the\n * other half of the population — never reaches it, and a hook that arrived with\n * a moved element would survive. `connect()` covers both, because both end in a\n * connection.\n *\n * Scope is the **drop** only. What the fresh cycle should say instead is the\n * consumer's: a value re-read from the live DOM, a roster refilled by the\n * stream, a measurement retaken from the current scroll position, a slot emptied\n * of the last result. Those answers differ per consumer and none of them is a\n * hook removal, so the pass stops where every consumer agrees.\n *\n * A declaration names one group of hooks and carries no per-instance state, so\n * it belongs at module scope, shared by every instance the page connects. A\n * consumer writes as many as it has groups — hooks it drops under different\n * conditions cannot share one. The exception is a hook whose name is only known\n * at runtime, such as one built from the registered identifier: that\n * declaration is a field, because the name is.\n *\n * **A drop is not always right.** The population splits on one question: can the\n * state a hook describes outlive the instance that wrote it? Where it cannot — a\n * drag ends with the pointer, a subscription dies with its socket — dropping on\n * every connection is correct. Where it can — a submission in flight is not\n * interrupted by an in-page move — only a connection that follows a real restore\n * may drop, and the consumer answers \"did a session survive?\" with the signal it\n * already holds for its own teardown (a live-session field, a `DetachGate`\n * probe). The drop then sits behind that answer, as the second example shows.\n *\n * @example Unconditional — the state cannot outlive the instance.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n *\n * connect(): void {\n * TRANSIENT.reset(this.element);\n * }\n * ```\n *\n * @example Gated — an in-page move must not drop a live state.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-optimistic\"] });\n *\n * connect(): void {\n * const restored = !this.#connected && !this.#gate.pending;\n * this.#gate.cancel();\n * this.#connected = true;\n * if (restored) TRANSIENT.reset(this.element);\n * }\n * ```\n */\n\n/** What a consumer declares once about the hooks its interaction owns. */\nexport interface TransientHooksOptions {\n /**\n * The attribute names a connection may find written by an earlier one. Hooks\n * a consumer drops under different conditions belong to different\n * declarations, so each one can be reset on its own.\n */\n readonly attributes: readonly string[];\n}\n\n/**\n * One consumer's declaration of the hooks its interaction owns, dropped from an\n * element by {@link TransientHooks.reset}.\n *\n * Every member comment inside this class body is inlined into each consumer\n * bundle (`tsup` builds with `splitting: false`), so rationale belongs in this\n * file's own block, which `dist` drops, and only the contract belongs inside.\n */\nexport class TransientHooks {\n readonly #attributes: readonly string[];\n\n constructor(options: TransientHooksOptions) {\n this.#attributes = options.attributes;\n }\n\n /** Drops the declared hooks from `element`; what it carries besides stays. */\n reset(element: Element): void {\n for (const attribute of this.#attributes) element.removeAttribute(attribute);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { DetachGate } from \"../utils/detach_gate\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { TransientHooks } from \"../utils/transient_hooks\";\n\n/** The hook a connection may find written by an earlier, now-gone one. */\nconst TRANSIENT = new TransientHooks({ attributes: [\"data-optimistic\"] });\n\n/** Records that this controller wrote `hidden`, and the authored value to put back. */\nconst HIDDEN_MARKER = \"data-optimistic-toggled\";\n\n/** The same record for `aria-busy` on the controller element. */\nconst BUSY_MARKER = \"data-optimistic-busy\";\n\n/** Marker value standing for \"the attribute was not there\". */\nconst AUTHORED_ABSENT = \"absent\";\n\n/** Marker prefix carrying the authored attribute value that follows it. */\nconst AUTHORED_VALUE = \"value:\";\n\n/**\n * Stands in for the submission a synthetic lifecycle event does not name. Turbo\n * puts a `formSubmission` on both events, so this only ever pairs hand-dispatched\n * events with each other — one shared instance, so a start and its end match.\n */\nconst SYNTHETIC_SUBMISSION = {};\n\n/** The submission a Turbo lifecycle event belongs to. */\nfunction submissionOf(event: Event): unknown {\n const detail = (event as CustomEvent<{ formSubmission?: unknown }>).detail;\n return detail?.formSubmission ?? SYNTHETIC_SUBMISSION;\n}\n\n/**\n * Headless **optimistic UI** for Turbo form submissions — the one server-bound\n * behavior that needs no Action Cable: it wraps a Turbo form\n * and applies a *declared* optimistic state the moment the submission starts,\n * keeps it when the server confirms, and **rolls it back** when the submission\n * fails. The pattern: flip the like button instantly; the success response (a\n * Turbo Stream) replaces the fragment with the server truth anyway, so the\n * client only needs the instant flip and the failure rollback. Core (zero\n * dependencies — it rides Turbo's own `turbo:submit-*` events).\n *\n * Markup contract (identifier: `stimeo--optimistic`):\n * <form data-controller=\"stimeo--optimistic\" method=\"post\" action=\"/likes\">\n * <button type=\"submit\" aria-label=\"Like\">\n * <span data-stimeo--optimistic-target=\"hide\">♡</span>\n * <span hidden data-stimeo--optimistic-target=\"show\">♥</span>\n * </button>\n * </form>\n *\n * On `turbo:submit-start`, every `show` target is unhidden and every `hide`\n * target hidden, the element gains `data-optimistic=\"true\"` + `aria-busy=\"true\"`,\n * and the submission named by the event takes ownership of that state. On\n * `turbo:submit-end`: the terminal of any *other* submission is ignored; the\n * owner's terminal clears the pending hook and, on success, dispatches `commit`\n * (the toggled state stays — the server response owns the final DOM), or on\n * failure restores the authored markup and dispatches `rollback`.\n *\n * **Every write records what it displaced.** A target this controller hides or\n * reveals carries the authored `hidden` value in `data-optimistic-toggled`, and\n * the element carries the authored `aria-busy` in `data-optimistic-busy`, so a\n * rollback puts back exactly what the author wrote — not the inverse of whatever\n * the attribute says at that moment. The record lives in the DOM rather than in\n * memory because a Turbo cache snapshot has to carry it: `connect()` is what\n * rewinds a restored page.\n *\n * @remarks\n * Behavior only — what \"optimistic\" looks like is the author's markup (the\n * show/hide pair) and CSS (`[data-optimistic=\"true\"]`). Pairs with\n * `stimeo--submit-once` (double-submit guard) and `stimeo--live-counter`\n * (optimistic numbers). The listeners are delegated on the element, so a form\n * nested under it (or swapped for a new one) keeps working, and the submission\n * ownership is what keeps a sibling form's terminal from resolving this state.\n * With several submissions in flight the newest start owns the optimistic state,\n * so a submission whose terminal never arrives — its form replaced mid-flight —\n * cannot strand the element. `connect()` is idempotent: a Turbo cache snapshot\n * taken mid-submit is rewound, since no submission can be in flight across a\n * restore, while an in-page move (where the submission *does* survive) keeps it.\n * On a real detach the listeners go with the element.\n */\nexport class OptimisticController extends Controller<HTMLElement> {\n static override targets = [\"show\", \"hide\"];\n static events = [\"commit\", \"rollback\"] as const;\n\n declare readonly showTargets: HTMLElement[];\n declare readonly hideTargets: HTMLElement[];\n\n /** The submission that owns the applied optimistic state, if any. */\n #pending: unknown = null;\n\n readonly #gate = new DetachGate();\n readonly #listeners = new ListenerSet();\n\n /** True between `connect()` and `disconnect()`, so a repeat connect rewinds nothing. */\n #connected = false;\n\n override connect(): void {\n // Only a page this instance did not apply the state to can be holding a stale\n // one: a restored snapshot. An in-page move and a repeat connect both leave a\n // live submission behind, so neither may rewind — the gate names the first and\n // the connected flag the second.\n const restored = !this.#connected && !this.#gate.pending;\n this.#gate.cancel();\n this.#connected = true;\n if (restored && this.element.hasAttribute(\"data-optimistic\")) this.#revert();\n this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#listeners.dispose();\n // Ownership outlives an in-page move: only a real detach forgets it.\n this.#gate.disconnected(this, () => {\n this.#pending = null;\n });\n }\n\n readonly #onSubmitStart = (event: Event): void => {\n this.#pending = submissionOf(event);\n // Turbo owns `aria-busy` on a form and marks it before this event, so what is\n // there now is Turbo's, not the author's. Recording it would put Turbo's value\n // back on a page restored mid-submit, where no terminal ever arrives to clear it.\n if (this.element instanceof HTMLFormElement && !this.element.hasAttribute(BUSY_MARKER)) {\n this.element.setAttribute(BUSY_MARKER, AUTHORED_ABSENT);\n }\n this.#write(this.element, \"aria-busy\", \"true\", BUSY_MARKER);\n this.element.setAttribute(\"data-optimistic\", \"true\");\n for (const target of this.showTargets) this.#setHidden(target, false);\n for (const target of this.hideTargets) this.#setHidden(target, true);\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n // A terminal always names a submission, so an element holding none (nothing\n // started, or this one already resolved) falls out here too.\n if (submissionOf(event) !== this.#pending) return;\n this.#pending = null;\n\n const success = (event as CustomEvent<{ success?: boolean }>).detail?.success === true;\n if (success) {\n // Keep the toggled faces (the server response owns the final DOM) but drop\n // their records, so a later failure cannot revert a confirmed state.\n this.element.removeAttribute(\"data-optimistic\");\n this.#restore(this.element, \"aria-busy\", BUSY_MARKER, \"true\");\n for (const target of [...this.showTargets, ...this.hideTargets]) {\n target.removeAttribute(HIDDEN_MARKER);\n }\n this.dispatch(\"commit\");\n } else {\n this.#revert();\n this.dispatch(\"rollback\");\n }\n };\n\n /** Hides or reveals a target, recording the authored `hidden` on first write. */\n #setHidden(target: HTMLElement, hidden: boolean): void {\n if (target.hasAttribute(\"hidden\") === hidden) return;\n this.#write(target, \"hidden\", hidden ? \"\" : null, HIDDEN_MARKER);\n }\n\n /** Writes an attribute, recording what it displaced under `marker` once. */\n #write(element: Element, attribute: string, value: string | null, marker: string): void {\n if (!element.hasAttribute(marker)) {\n const authored = element.getAttribute(attribute);\n element.setAttribute(\n marker,\n authored === null ? AUTHORED_ABSENT : `${AUTHORED_VALUE}${authored}`,\n );\n }\n if (value === null) element.removeAttribute(attribute);\n else element.setAttribute(attribute, value);\n }\n\n /**\n * Puts back the value `marker` recorded and drops the record.\n *\n * `written` is what this controller last put in the attribute. Anything else\n * there now belongs to whoever wrote it — Turbo owns `aria-busy` on a form and\n * clears it before the terminal arrives, and a consumer may have moved a face\n * mid-flight — so the record is dropped without touching the attribute. A\n * marker this build did not write names no authored value, so the attribute\n * goes back to being absent.\n */\n #restore(element: Element, attribute: string, marker: string, written: string | null): void {\n const recorded = element.getAttribute(marker);\n if (recorded === null) return;\n element.removeAttribute(marker);\n if (element.getAttribute(attribute) !== written) return;\n if (recorded.startsWith(AUTHORED_VALUE)) {\n element.setAttribute(attribute, recorded.slice(AUTHORED_VALUE.length));\n return;\n }\n element.removeAttribute(attribute);\n }\n\n /**\n * Puts back exactly the markup this controller displaced (record-owned only).\n *\n * A face can be registered on both sides, and the hide pass runs last, so what\n * this controller wrote there is `hidden` — matching it against the reveal's\n * absent value instead would read as someone else's write and leave the face\n * hidden. Each target is therefore restored once, against the write it last\n * received.\n */\n #revert(): void {\n TRANSIENT.reset(this.element);\n this.#restore(this.element, \"aria-busy\", BUSY_MARKER, \"true\");\n const hidden = new Set(this.hideTargets);\n for (const target of new Set([...this.showTargets, ...this.hideTargets])) {\n this.#restore(target, \"hidden\", HIDDEN_MARKER, hidden.has(target) ? \"\" : null);\n }\n }\n}\n"]}
@@ -39,8 +39,17 @@ import { Controller } from '@hotwired/stimulus';
39
39
  * `change` and `complete` dispatch `{ value: string }` and fire only when the
40
40
  * combined value actually moves — one confirmed IME character emits one event,
41
41
  * and a passcode re-completed with a different digit reports the new value.
42
- * State moved by adding or removing fields is the page's doing rather than an
43
- * edit, so it is reported as `reconcile` with the same `{ value: string }`.
42
+ * State moved by adding or removing fields, by a `pattern` change dropping
43
+ * entered digits it no longer accepts, or by a native form reset restoring the
44
+ * fields is not an edit of the passcode, so it is reported as `reconcile` with
45
+ * the same `{ value: string }` — once per change, after it settles — and never
46
+ * as `change` or `complete`. While a field is composing, the IME's uncommitted
47
+ * text is neither read nor written: a change already in the DOM — fields added
48
+ * or removed, a reset — is reported at once, counting that field by the
49
+ * character it had committed, while a `pattern` change's drop waits for the
50
+ * composition to end and is then applied and reported before the commit is
51
+ * taken. A composition that ends on the text the field held when it began was
52
+ * cancelled: no edit, so it reports nothing and focus stays.
44
53
  * Completeness belongs to that state: dropping a trailing empty field completes
45
54
  * a passcode whose combined value never moved, and that transition is reported
46
55
  * too, so a consumer reading `data-state` is never left behind a silent move.
@@ -49,15 +58,18 @@ import { Controller } from '@hotwired/stimulus';
49
58
  * Controller-owned output: `data-filled` on each entered field, `data-state`
50
59
  * (`empty` / `partial` / `complete`) on the root, and — while input is being
51
60
  * reported invalid — `aria-invalid`, `aria-errormessage`, `aria-describedby`,
52
- * and the `error` target's `hidden`. Those four are leased, so authored values
53
- * return on teardown and before the page is cached.
61
+ * and the `error` target's `hidden`. Authored values return on teardown and
62
+ * before the page is cached: the three ARIA attributes keep their lease on the
63
+ * field itself, so a connection that adopts a restored DOM can give them back
64
+ * too, and the `hidden` is leased.
54
65
  *
55
66
  * @remarks
56
67
  * Behavior only. `connect()` reads the fields back as the source of truth, which
57
68
  * is what restores consistency after a `type="password"` field returns from the
58
- * Turbo cache emptied, or after a native form reset. A `pattern` that cannot
59
- * compile falls back to `[0-9]`; the compiled matcher is built once per
60
- * declaration rather than per keystroke.
69
+ * Turbo cache emptied; a native form reset is read back the same way once the
70
+ * browser has restored the fields. A `pattern` that cannot compile falls back to
71
+ * `[0-9]`; the compiled matcher is built once per declaration rather than per
72
+ * keystroke.
61
73
  */
62
74
  declare class OtpController extends Controller<HTMLElement> {
63
75
  #private;
@@ -89,6 +101,9 @@ declare class OtpController extends Controller<HTMLElement> {
89
101
  /**
90
102
  * Re-validates a changed `pattern` declaration once and drops any entered digit
91
103
  * the new pattern no longer accepts, so the combined value stays interpretable.
104
+ * The page changed the declaration, so the move joins the same reconciliation
105
+ * as fields added or removed in that mutation. While a field is composing, its
106
+ * text is the IME's, and the drop waits for the composition to end.
92
107
  */
93
108
  patternValueChanged(): void;
94
109
  /** Handles keystroke inputs, distributes autofilled text, and advances focus. */
@@ -106,11 +106,19 @@ var BeforeCacheReset = class _BeforeCacheReset {
106
106
  };
107
107
 
108
108
  // src/utils/composition_tracker.ts
109
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
110
+ "insertCompositionText",
111
+ "insertFromComposition",
112
+ "deleteCompositionText",
113
+ "deleteByComposition"
114
+ ]);
109
115
  var CompositionTracker = class {
110
116
  #observedTargets = /* @__PURE__ */ new Set();
111
117
  #activeTargets = /* @__PURE__ */ new Set();
112
118
  #onStart;
113
119
  #onEnd;
120
+ /** The field whose confirming `input` is still owed, while the window is open. */
121
+ #confirmedTarget = null;
114
122
  constructor(options = {}) {
115
123
  this.#onStart = options.onStart;
116
124
  this.#onEnd = options.onEnd;
@@ -120,6 +128,7 @@ var CompositionTracker = class {
120
128
  if (this.#observedTargets.has(target)) return;
121
129
  target.addEventListener("compositionstart", this.#handleStart);
122
130
  target.addEventListener("compositionend", this.#handleEnd);
131
+ target.addEventListener("keydown", this.#handleKeydown);
123
132
  this.#observedTargets.add(target);
124
133
  }
125
134
  /** Stops tracking one target and clears any active composition it owned. */
@@ -127,29 +136,52 @@ var CompositionTracker = class {
127
136
  if (!this.#observedTargets.delete(target)) return;
128
137
  target.removeEventListener("compositionstart", this.#handleStart);
129
138
  target.removeEventListener("compositionend", this.#handleEnd);
139
+ target.removeEventListener("keydown", this.#handleKeydown);
130
140
  this.#activeTargets.delete(target);
141
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
131
142
  }
132
143
  /** Releases every listener and clears state so reconnect starts cleanly. */
133
144
  disconnect() {
134
145
  for (const target of this.#observedTargets) {
135
146
  target.removeEventListener("compositionstart", this.#handleStart);
136
147
  target.removeEventListener("compositionend", this.#handleEnd);
148
+ target.removeEventListener("keydown", this.#handleKeydown);
137
149
  }
138
150
  this.#observedTargets.clear();
139
151
  this.#activeTargets.clear();
152
+ this.#confirmedTarget = null;
140
153
  }
141
154
  /** True when lifecycle tracking or the current event reports composition. */
142
155
  isComposing(event) {
143
156
  return this.#activeTargets.size > 0 || event?.isComposing === true;
144
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
+ */
164
+ consumesConfirmedInput(event) {
165
+ const confirmed = this.#confirmedTarget;
166
+ this.#confirmedTarget = null;
167
+ if (confirmed === null || confirmed !== event.target) return false;
168
+ const inputType = event.inputType;
169
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
170
+ }
145
171
  #handleStart = (event) => {
172
+ this.#confirmedTarget = null;
146
173
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
147
174
  this.#onStart?.(event);
148
175
  };
149
176
  #handleEnd = (event) => {
150
177
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
178
+ this.#confirmedTarget = event.target;
151
179
  this.#onEnd?.(event);
152
180
  };
181
+ /** A key on an observed field opens an edit of its own, so no echo is owed. */
182
+ #handleKeydown = () => {
183
+ this.#confirmedTarget = null;
184
+ };
153
185
  };
154
186
 
155
187
  // src/utils/declared_value.ts
@@ -171,6 +203,54 @@ function compileRegExp(source, anchor = "none") {
171
203
  );
172
204
  }
173
205
 
206
+ // src/utils/form_reset_watcher.ts
207
+ var FormResetWatcher = class {
208
+ #owns;
209
+ #onReset;
210
+ #frames = /* @__PURE__ */ new Set();
211
+ #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
+ constructor(owns, onReset) {
217
+ this.#owns = owns;
218
+ this.#onReset = onReset;
219
+ }
220
+ /** Subscribes to `reset` at the document; call from `connect()`. Idempotent. */
221
+ observe() {
222
+ document.addEventListener("reset", this.#handleReset, true);
223
+ }
224
+ /** Unsubscribes and drops every pending pass; call from `disconnect()`. */
225
+ disconnect() {
226
+ document.removeEventListener("reset", this.#handleReset, true);
227
+ this.#generation += 1;
228
+ for (const frame of this.#frames) cancelAnimationFrame(frame);
229
+ this.#frames.clear();
230
+ }
231
+ #handleReset = (event) => {
232
+ const form = event.target;
233
+ if (!(form instanceof HTMLFormElement) || !this.#owns(form)) return;
234
+ const generation = this.#generation;
235
+ queueMicrotask(() => {
236
+ if (generation !== this.#generation) return;
237
+ if (event.eventPhase === Event.NONE) {
238
+ this.#settle(event);
239
+ return;
240
+ }
241
+ const frame = requestAnimationFrame(() => {
242
+ this.#frames.delete(frame);
243
+ this.#settle(event);
244
+ });
245
+ this.#frames.add(frame);
246
+ });
247
+ };
248
+ /** Runs the pass unless a listener cancelled the reset before it landed. */
249
+ #settle(event) {
250
+ if (!event.defaultPrevented) this.#onReset();
251
+ }
252
+ };
253
+
174
254
  // src/utils/half_width.ts
175
255
  var FULL_WIDTH_SHIFT = 65248;
176
256
  function halfWidthChar(char) {
@@ -219,6 +299,27 @@ var MicrotaskCoalescer = class {
219
299
  };
220
300
 
221
301
  // src/controllers/otp_controller.ts
302
+ var LEASE_MARKERS = {
303
+ "aria-invalid": "data-otp-invalid-lease",
304
+ "aria-errormessage": "data-otp-errormessage-lease",
305
+ "aria-describedby": "data-otp-describedby-lease"
306
+ };
307
+ function isErrorLease(value) {
308
+ const lease = value;
309
+ return typeof lease?.written === "string" && (lease.authored === null || typeof lease.authored === "string");
310
+ }
311
+ function readLease(field, marker) {
312
+ try {
313
+ const parsed = JSON.parse(field.getAttribute(marker) ?? "null");
314
+ return isErrorLease(parsed) ? parsed : null;
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+ function describedByWith(described, errorId) {
320
+ const tokens = (described ?? "").split(/\s+/).filter((token) => token.length > 0 && token !== errorId);
321
+ return [...tokens, errorId].join(" ");
322
+ }
222
323
  var DEFAULT_PATTERN = "[0-9]";
223
324
  var DEFAULT_MATCHER = new RegExp(`^(?:${DEFAULT_PATTERN})$`);
224
325
  function hasModifier(event) {
@@ -240,38 +341,59 @@ var OtpController = class extends Controller {
240
341
  #patternSource = DEFAULT_PATTERN;
241
342
  /** Public state carried by the last dispatch; keeps a no-op sync silent. */
242
343
  #published = null;
243
- /** Field whose confirming `input` after `compositionend` is already handled. */
244
- #confirmedField = null;
245
344
  /** True between connect and disconnect, so pre-connect Value changes stay silent. */
246
345
  #connected = false;
247
346
  /** Digit each field last committed, restored when rejected input replaced it. */
248
347
  #committed = /* @__PURE__ */ new WeakMap();
249
- /** Collapses one batch of field target callbacks into a single reconciliation. */
348
+ /** The field being composed, and the text it held when the composition began. */
349
+ #composing = null;
350
+ /** A `pattern` change whose rejected characters wait for the composition to end. */
351
+ #dropHeld = false;
352
+ /**
353
+ * Collapses one batch of page changes — field target and `pattern` callbacks, a
354
+ * form reset — into a single reconciliation.
355
+ */
250
356
  #reconcile = new MicrotaskCoalescer(() => this.#reconcileFields());
251
- #ariaInvalid = new AttributeLease("aria-invalid");
252
- #ariaErrorMessage = new AttributeLease("aria-errormessage");
253
- #ariaDescribedBy = new AttributeLease("aria-describedby");
254
357
  #errorHidden = new AttributeLease("hidden");
255
358
  #state = new AttributeLease("data-state");
256
359
  /** Rewinds the transient error surface before Turbo freezes the page. */
257
360
  #beforeCache = new BeforeCacheReset(() => this.#clearError());
258
- /** Owns IME lifecycle state across every digit field. */
361
+ /** Reads the fields back once a native reset restored them, as a reconciliation. */
362
+ #formReset = new FormResetWatcher(
363
+ (form) => this.#ownedBy(form),
364
+ () => {
365
+ this.#adopt();
366
+ this.#reconcile.schedule();
367
+ }
368
+ );
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
+ */
259
374
  #composition = new CompositionTracker({
260
- onStart: () => {
261
- this.#confirmedField = null;
375
+ onStart: (event) => {
376
+ const field = this.#fieldFrom(event);
377
+ if (field) this.#composing = { field, before: field.value };
262
378
  },
263
379
  onEnd: (event) => {
380
+ const composing = this.#composing;
381
+ this.#composing = null;
264
382
  const input = this.#fieldFrom(event);
265
- if (!input) return;
266
- this.#confirmedField = input;
267
- const committed = event.data ?? "";
268
- this.#accept(input, committed.length > input.value.length ? committed : input.value);
383
+ 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) ?? "";
388
+ this.#reconcileFields();
389
+ }
390
+ if (input && commit) this.#accept(input, text);
269
391
  }
270
392
  });
271
393
  connect() {
272
394
  this.#connected = true;
273
395
  for (const field of this.fieldTargets) this.#bind(field);
274
- document.addEventListener("reset", this.#onReset, true);
396
+ this.#formReset.observe();
275
397
  this.#beforeCache.activate();
276
398
  this.#reconcile.activate();
277
399
  this.#adopt();
@@ -281,9 +403,10 @@ var OtpController = class extends Controller {
281
403
  this.#connected = false;
282
404
  for (const field of this.fieldTargets) this.#unbind(field);
283
405
  this.#composition.disconnect();
284
- document.removeEventListener("reset", this.#onReset, true);
406
+ this.#formReset.disconnect();
285
407
  this.#beforeCache.deactivate();
286
408
  this.#reconcile.cancel();
409
+ this.#dropHeld = false;
287
410
  this.#clearError();
288
411
  this.#state.return(this.element);
289
412
  }
@@ -300,18 +423,29 @@ var OtpController = class extends Controller {
300
423
  /** Releases a dropped field's listeners and leases, then reconciles the rest. */
301
424
  fieldTargetDisconnected(element) {
302
425
  this.#unbind(element);
303
- this.#returnFieldLeases(element);
426
+ this.#returnLeases(element);
304
427
  this.#reconcile.schedule();
305
428
  }
306
429
  /**
307
430
  * Re-validates a changed `pattern` declaration once and drops any entered digit
308
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.
309
435
  */
310
436
  patternValueChanged() {
311
437
  const compiled = compileRegExp(this.patternValue, "exact");
312
438
  this.#patternSource = compiled ? this.patternValue : DEFAULT_PATTERN;
313
439
  this.#pattern = compiled ?? DEFAULT_MATCHER;
314
440
  if (!this.#connected) return;
441
+ if (this.#composing !== null) {
442
+ this.#dropHeld = true;
443
+ return;
444
+ }
445
+ if (this.#dropRejected()) this.#reconcile.schedule();
446
+ }
447
+ /** Empties every writable field holding a character the pattern rejects; whether one did. */
448
+ #dropRejected() {
315
449
  let dropped = false;
316
450
  for (const field of this.fieldTargets) {
317
451
  if (field.value === "" || !this.#isWritable(field)) continue;
@@ -319,15 +453,13 @@ var OtpController = class extends Controller {
319
453
  this.#writeField(field, "");
320
454
  dropped = true;
321
455
  }
322
- if (dropped) this.#syncAndDispatch();
456
+ return dropped;
323
457
  }
324
458
  /** Handles keystroke inputs, distributes autofilled text, and advances focus. */
325
459
  onInput(event) {
326
460
  const input = this.#fieldFrom(event);
327
461
  if (!input) return;
328
- const confirmed = this.#confirmedField;
329
- this.#confirmedField = null;
330
- if (confirmed === input) return;
462
+ if (this.#composition.consumesConfirmedInput(event)) return;
331
463
  if (this.#composition.isComposing(event)) return;
332
464
  this.#accept(input);
333
465
  }
@@ -336,7 +468,6 @@ var OtpController = class extends Controller {
336
468
  if (isReservedArrowChord(event)) return;
337
469
  const input = this.#fieldFrom(event);
338
470
  if (!input) return;
339
- this.#confirmedField = null;
340
471
  if (this.#composition.isComposing(event)) return;
341
472
  const index = this.fieldTargets.indexOf(input);
342
473
  const fields = this.fieldTargets;
@@ -400,7 +531,6 @@ var OtpController = class extends Controller {
400
531
  const input = this.#fieldFrom(event);
401
532
  if (!input) return;
402
533
  event.preventDefault();
403
- this.#confirmedField = null;
404
534
  this.#distribute(input, toHalfWidth(event.clipboardData?.getData("text") ?? ""));
405
535
  }
406
536
  /**
@@ -431,16 +561,6 @@ var OtpController = class extends Controller {
431
561
  input.select();
432
562
  }
433
563
  };
434
- /** Reconciles derived state after a non-cancelled reset restores the fields. */
435
- #onReset = (event) => {
436
- const form = event.target;
437
- if (!(form instanceof HTMLFormElement) || !this.#ownedBy(form)) return;
438
- queueMicrotask(() => {
439
- if (event.defaultPrevented) return;
440
- this.#adopt();
441
- this.#syncAndDispatch();
442
- });
443
- };
444
564
  /** Whether a form owns at least one field or the hidden combined value. */
445
565
  #ownedBy(form) {
446
566
  if (this.fieldTargets.some((field) => field.form === form)) return true;
@@ -453,7 +573,7 @@ var OtpController = class extends Controller {
453
573
  #unbind(field) {
454
574
  field.removeEventListener("focus", this.#onFieldFocus);
455
575
  this.#composition.unobserve(field);
456
- if (this.#confirmedField === field) this.#confirmedField = null;
576
+ if (this.#composing?.field === field) this.#composing = null;
457
577
  }
458
578
  /** Reads every field back so a restored or reset group starts consistent. */
459
579
  #adopt() {
@@ -461,25 +581,55 @@ var OtpController = class extends Controller {
461
581
  this.#clearError();
462
582
  if (this.hasErrorTarget) this.errorTarget.setAttribute("hidden", "");
463
583
  }
584
+ /** Writes one error attribute, keeping the first authored value it displaces. */
585
+ #lease(field, attribute, value) {
586
+ const marker = LEASE_MARKERS[attribute];
587
+ const held = readLease(field, marker);
588
+ const authored = held ? held.authored : field.getAttribute(attribute);
589
+ field.setAttribute(marker, JSON.stringify({ authored, written: value }));
590
+ field.setAttribute(attribute, value);
591
+ }
592
+ /** Gives one field's error attributes back where they still hold what this wrote. */
593
+ #returnLeases(field) {
594
+ for (const [attribute, marker] of Object.entries(LEASE_MARKERS)) {
595
+ const held = readLease(field, marker);
596
+ if (!held) continue;
597
+ field.removeAttribute(marker);
598
+ if (field.getAttribute(attribute) !== held.written) continue;
599
+ if (held.authored === null) field.removeAttribute(attribute);
600
+ else field.setAttribute(attribute, held.authored);
601
+ }
602
+ }
464
603
  /** Takes one field's current value as the truth behind its derived state. */
465
604
  #adoptField(field) {
466
605
  this.#committed.set(field, field.value);
467
606
  this.#markFilled(field, field.value);
468
607
  }
469
608
  /**
470
- * Absorbs a batch of field additions or removals as one state transition.
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.
471
612
  *
472
- * The page, not the user, moved the state here, so it is reported as
473
- * `reconcile`: automation listening for `change` must not read a re-render as
474
- * an edit, and a passcode that happens to end up full must not fire the
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
475
616
  * `complete` that submits it.
476
617
  *
477
618
  * Completeness moves on its own when the field count changes: dropping a
478
619
  * trailing empty field completes a passcode whose combined value never moved,
479
620
  * and adding one un-completes it. Comparing the whole derived state, not the
480
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.
481
627
  */
482
628
  #reconcileFields() {
629
+ if (this.#dropHeld && this.#composing === null) {
630
+ this.#dropHeld = false;
631
+ this.#dropRejected();
632
+ }
483
633
  const previous = this.#published;
484
634
  const current = this.#sync();
485
635
  if (previous && !statesDiffer(previous, current)) return;
@@ -571,12 +721,16 @@ var OtpController = class extends Controller {
571
721
  return this.fieldTargets.find((field) => field === input) ?? null;
572
722
  }
573
723
  #combinedValue() {
574
- return this.fieldTargets.map((field) => field.value).join("");
724
+ return this.fieldTargets.map((field) => this.#publishedText(field)).join("");
575
725
  }
576
726
  /** Every field carries a character, and there is at least one field. */
577
727
  #isComplete() {
578
728
  const fields = this.fieldTargets;
579
- return fields.length > 0 && fields.every((field) => field.value.length > 0);
729
+ return fields.length > 0 && fields.every((field) => this.#publishedText(field).length > 0);
730
+ }
731
+ /** A field's text as the combined value counts it: a composing field by its committed character. */
732
+ #publishedText(field) {
733
+ return field === this.#composing?.field ? this.#committed.get(field) ?? "" : field.value;
580
734
  }
581
735
  /**
582
736
  * Mirrors the combined value into the form and the root's readable state, and
@@ -610,31 +764,20 @@ var OtpController = class extends Controller {
610
764
  #showError() {
611
765
  const errorId = this.hasErrorTarget ? ensureId(this.errorTarget, "stimeo--otp-error") : null;
612
766
  for (const field of this.fieldTargets) {
613
- this.#ariaInvalid.write(field, "true");
767
+ this.#lease(field, "aria-invalid", "true");
614
768
  if (!errorId) continue;
615
- this.#ariaErrorMessage.write(field, errorId);
616
- this.#ariaDescribedBy.write(field, this.#describedByWith(field, errorId));
769
+ this.#lease(field, "aria-errormessage", errorId);
770
+ const described = field.getAttribute("aria-describedby");
771
+ this.#lease(field, "aria-describedby", describedByWith(described, errorId));
617
772
  }
618
773
  if (this.hasErrorTarget) this.#errorHidden.write(this.errorTarget, null);
619
774
  this.dispatch("invalid", { detail: { pattern: this.#patternSource } });
620
775
  }
621
776
  /** Returns every error lease, restoring the authored error surface. */
622
777
  #clearError() {
623
- this.#ariaInvalid.returnAll();
624
- this.#ariaErrorMessage.returnAll();
625
- this.#ariaDescribedBy.returnAll();
778
+ for (const field of this.fieldTargets) this.#returnLeases(field);
626
779
  this.#errorHidden.returnAll();
627
780
  }
628
- #returnFieldLeases(field) {
629
- this.#ariaInvalid.return(field);
630
- this.#ariaErrorMessage.return(field);
631
- this.#ariaDescribedBy.return(field);
632
- }
633
- /** The field's own description tokens with the error id appended once. */
634
- #describedByWith(field, errorId) {
635
- const tokens = (field.getAttribute("aria-describedby") ?? "").split(/\s+/).filter((token) => token.length > 0 && token !== errorId);
636
- return [...tokens, errorId].join(" ");
637
- }
638
781
  };
639
782
 
640
783
  export { OtpController };