stimeo-ui 0.5.0 → 0.7.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 (230) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/dist/controllers/alert_dialog_controller.d.ts +2 -0
  3. package/dist/controllers/alert_dialog_controller.js +75 -4
  4. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  5. package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
  6. package/dist/controllers/aspect_ratio_controller.js +19 -11
  7. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  8. package/dist/controllers/auto_submit_controller.d.ts +2 -0
  9. package/dist/controllers/auto_submit_controller.js.map +1 -1
  10. package/dist/controllers/avatar_controller.d.ts +36 -15
  11. package/dist/controllers/avatar_controller.js +237 -40
  12. package/dist/controllers/avatar_controller.js.map +1 -1
  13. package/dist/controllers/breadcrumb_controller.d.ts +2 -0
  14. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  15. package/dist/controllers/bulk_select_controller.d.ts +2 -0
  16. package/dist/controllers/bulk_select_controller.js.map +1 -1
  17. package/dist/controllers/calendar_controller.d.ts +2 -0
  18. package/dist/controllers/calendar_controller.js.map +1 -1
  19. package/dist/controllers/carousel_controller.d.ts +12 -3
  20. package/dist/controllers/carousel_controller.js +85 -9
  21. package/dist/controllers/carousel_controller.js.map +1 -1
  22. package/dist/controllers/character_counter_controller.d.ts +52 -20
  23. package/dist/controllers/character_counter_controller.js +338 -63
  24. package/dist/controllers/character_counter_controller.js.map +1 -1
  25. package/dist/controllers/checkbox_controller.d.ts +34 -5
  26. package/dist/controllers/checkbox_controller.js +136 -25
  27. package/dist/controllers/checkbox_controller.js.map +1 -1
  28. package/dist/controllers/color_picker_controller.d.ts +10 -1
  29. package/dist/controllers/color_picker_controller.js +35 -9
  30. package/dist/controllers/color_picker_controller.js.map +1 -1
  31. package/dist/controllers/combobox_controller.d.ts +2 -0
  32. package/dist/controllers/combobox_controller.js.map +1 -1
  33. package/dist/controllers/command_palette_controller.d.ts +2 -0
  34. package/dist/controllers/command_palette_controller.js +75 -4
  35. package/dist/controllers/command_palette_controller.js.map +1 -1
  36. package/dist/controllers/conditional_fields_controller.d.ts +42 -14
  37. package/dist/controllers/conditional_fields_controller.js +345 -51
  38. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  39. package/dist/controllers/confirm_controller.d.ts +2 -0
  40. package/dist/controllers/confirm_controller.js +75 -4
  41. package/dist/controllers/confirm_controller.js.map +1 -1
  42. package/dist/controllers/count_up_controller.d.ts +2 -0
  43. package/dist/controllers/count_up_controller.js.map +1 -1
  44. package/dist/controllers/countdown_controller.d.ts +4 -0
  45. package/dist/controllers/countdown_controller.js.map +1 -1
  46. package/dist/controllers/currency_input_controller.d.ts +2 -0
  47. package/dist/controllers/currency_input_controller.js.map +1 -1
  48. package/dist/controllers/data_grid_controller.d.ts +3 -0
  49. package/dist/controllers/data_grid_controller.js.map +1 -1
  50. package/dist/controllers/date_range_picker_controller.d.ts +23 -4
  51. package/dist/controllers/date_range_picker_controller.js +157 -30
  52. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  53. package/dist/controllers/dialog_controller.js +75 -4
  54. package/dist/controllers/dialog_controller.js.map +1 -1
  55. package/dist/controllers/direct_upload_controller.d.ts +56 -24
  56. package/dist/controllers/direct_upload_controller.js +201 -45
  57. package/dist/controllers/direct_upload_controller.js.map +1 -1
  58. package/dist/controllers/dirty_form_controller.d.ts +14 -6
  59. package/dist/controllers/dirty_form_controller.js +192 -29
  60. package/dist/controllers/dirty_form_controller.js.map +1 -1
  61. package/dist/controllers/dismissible_controller.d.ts +2 -0
  62. package/dist/controllers/dismissible_controller.js +83 -18
  63. package/dist/controllers/dismissible_controller.js.map +1 -1
  64. package/dist/controllers/drawer_controller.js +75 -4
  65. package/dist/controllers/drawer_controller.js.map +1 -1
  66. package/dist/controllers/empty_state_controller.d.ts +2 -0
  67. package/dist/controllers/empty_state_controller.js.map +1 -1
  68. package/dist/controllers/file_dropzone_controller.d.ts +9 -1
  69. package/dist/controllers/file_dropzone_controller.js +26 -3
  70. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  71. package/dist/controllers/filter_controller.d.ts +2 -0
  72. package/dist/controllers/filter_controller.js.map +1 -1
  73. package/dist/controllers/flash_controller.d.ts +4 -0
  74. package/dist/controllers/flash_controller.js.map +1 -1
  75. package/dist/controllers/focus_controller.d.ts +4 -3
  76. package/dist/controllers/focus_controller.js +75 -4
  77. package/dist/controllers/focus_controller.js.map +1 -1
  78. package/dist/controllers/form_field_controller.d.ts +50 -10
  79. package/dist/controllers/form_field_controller.js +280 -62
  80. package/dist/controllers/form_field_controller.js.map +1 -1
  81. package/dist/controllers/form_validation_controller.d.ts +10 -8
  82. package/dist/controllers/form_validation_controller.js +208 -83
  83. package/dist/controllers/form_validation_controller.js.map +1 -1
  84. package/dist/controllers/frame_loading_controller.d.ts +2 -0
  85. package/dist/controllers/frame_loading_controller.js.map +1 -1
  86. package/dist/controllers/highlight_controller.d.ts +2 -0
  87. package/dist/controllers/highlight_controller.js.map +1 -1
  88. package/dist/controllers/hover_card_controller.d.ts +2 -2
  89. package/dist/controllers/hover_card_controller.js.map +1 -1
  90. package/dist/controllers/idle_controller.d.ts +5 -3
  91. package/dist/controllers/idle_controller.js +27 -5
  92. package/dist/controllers/idle_controller.js.map +1 -1
  93. package/dist/controllers/input_mask_controller.d.ts +2 -0
  94. package/dist/controllers/input_mask_controller.js.map +1 -1
  95. package/dist/controllers/lazy_frame_controller.d.ts +2 -0
  96. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  97. package/dist/controllers/listbox_controller.d.ts +2 -0
  98. package/dist/controllers/listbox_controller.js.map +1 -1
  99. package/dist/controllers/local_time_controller.d.ts +2 -0
  100. package/dist/controllers/local_time_controller.js.map +1 -1
  101. package/dist/controllers/masonry_controller.d.ts +2 -0
  102. package/dist/controllers/masonry_controller.js.map +1 -1
  103. package/dist/controllers/menubar_controller.js +5 -3
  104. package/dist/controllers/menubar_controller.js.map +1 -1
  105. package/dist/controllers/meter_controller.d.ts +2 -0
  106. package/dist/controllers/meter_controller.js.map +1 -1
  107. package/dist/controllers/multi_select_controller.d.ts +48 -12
  108. package/dist/controllers/multi_select_controller.js +460 -151
  109. package/dist/controllers/multi_select_controller.js.map +1 -1
  110. package/dist/controllers/nested_form_controller.d.ts +2 -0
  111. package/dist/controllers/nested_form_controller.js.map +1 -1
  112. package/dist/controllers/network_status_controller.d.ts +2 -0
  113. package/dist/controllers/network_status_controller.js.map +1 -1
  114. package/dist/controllers/number_input_controller.d.ts +26 -6
  115. package/dist/controllers/number_input_controller.js +317 -51
  116. package/dist/controllers/number_input_controller.js.map +1 -1
  117. package/dist/controllers/otp_controller.d.ts +3 -0
  118. package/dist/controllers/otp_controller.js.map +1 -1
  119. package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
  120. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  121. package/dist/controllers/overflow_menu_controller.d.ts +2 -0
  122. package/dist/controllers/overflow_menu_controller.js +6 -1
  123. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  124. package/dist/controllers/pagination_controller.d.ts +2 -0
  125. package/dist/controllers/pagination_controller.js +35 -1
  126. package/dist/controllers/pagination_controller.js.map +1 -1
  127. package/dist/controllers/password_reveal_controller.d.ts +2 -0
  128. package/dist/controllers/password_reveal_controller.js.map +1 -1
  129. package/dist/controllers/password_strength_controller.d.ts +5 -3
  130. package/dist/controllers/password_strength_controller.js +20 -2
  131. package/dist/controllers/password_strength_controller.js.map +1 -1
  132. package/dist/controllers/persist_controller.d.ts +33 -19
  133. package/dist/controllers/persist_controller.js +449 -120
  134. package/dist/controllers/persist_controller.js.map +1 -1
  135. package/dist/controllers/pointer_drag_controller.d.ts +3 -0
  136. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  137. package/dist/controllers/popover_controller.d.ts +2 -2
  138. package/dist/controllers/popover_controller.js +77 -4
  139. package/dist/controllers/popover_controller.js.map +1 -1
  140. package/dist/controllers/portal_controller.d.ts +3 -2
  141. package/dist/controllers/portal_controller.js.map +1 -1
  142. package/dist/controllers/preview_guard_controller.d.ts +2 -0
  143. package/dist/controllers/preview_guard_controller.js.map +1 -1
  144. package/dist/controllers/progress_controller.d.ts +3 -0
  145. package/dist/controllers/progress_controller.js.map +1 -1
  146. package/dist/controllers/radio_group_controller.d.ts +44 -15
  147. package/dist/controllers/radio_group_controller.js +540 -56
  148. package/dist/controllers/radio_group_controller.js.map +1 -1
  149. package/dist/controllers/rating_controller.d.ts +38 -31
  150. package/dist/controllers/rating_controller.js +276 -89
  151. package/dist/controllers/rating_controller.js.map +1 -1
  152. package/dist/controllers/reading_progress_controller.d.ts +2 -0
  153. package/dist/controllers/reading_progress_controller.js.map +1 -1
  154. package/dist/controllers/resizable_controller.d.ts +2 -0
  155. package/dist/controllers/resizable_controller.js +33 -0
  156. package/dist/controllers/resizable_controller.js.map +1 -1
  157. package/dist/controllers/roving_controller.d.ts +6 -0
  158. package/dist/controllers/roving_controller.js +60 -5
  159. package/dist/controllers/roving_controller.js.map +1 -1
  160. package/dist/controllers/scroll_area_controller.d.ts +25 -11
  161. package/dist/controllers/scroll_area_controller.js +557 -125
  162. package/dist/controllers/scroll_area_controller.js.map +1 -1
  163. package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
  164. package/dist/controllers/scroll_visibility_controller.js +33 -0
  165. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  166. package/dist/controllers/scrollspy_controller.d.ts +2 -0
  167. package/dist/controllers/scrollspy_controller.js.map +1 -1
  168. package/dist/controllers/separator_controller.d.ts +54 -11
  169. package/dist/controllers/separator_controller.js +354 -38
  170. package/dist/controllers/separator_controller.js.map +1 -1
  171. package/dist/controllers/sidebar_controller.js +83 -10
  172. package/dist/controllers/sidebar_controller.js.map +1 -1
  173. package/dist/controllers/skeleton_controller.d.ts +2 -0
  174. package/dist/controllers/skeleton_controller.js.map +1 -1
  175. package/dist/controllers/slider_controller.d.ts +2 -0
  176. package/dist/controllers/slider_controller.js.map +1 -1
  177. package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
  178. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  179. package/dist/controllers/spinner_controller.d.ts +2 -0
  180. package/dist/controllers/spinner_controller.js.map +1 -1
  181. package/dist/controllers/step_indicator_controller.d.ts +2 -0
  182. package/dist/controllers/step_indicator_controller.js.map +1 -1
  183. package/dist/controllers/stepper_controller.d.ts +2 -0
  184. package/dist/controllers/stepper_controller.js.map +1 -1
  185. package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
  186. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  187. package/dist/controllers/sticky_observer_controller.d.ts +2 -0
  188. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  189. package/dist/controllers/submit_once_controller.d.ts +94 -38
  190. package/dist/controllers/submit_once_controller.js +399 -121
  191. package/dist/controllers/submit_once_controller.js.map +1 -1
  192. package/dist/controllers/switch_controller.d.ts +2 -0
  193. package/dist/controllers/switch_controller.js.map +1 -1
  194. package/dist/controllers/tags_input_controller.d.ts +43 -11
  195. package/dist/controllers/tags_input_controller.js +356 -120
  196. package/dist/controllers/tags_input_controller.js.map +1 -1
  197. package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
  198. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  199. package/dist/controllers/theme_controller.d.ts +2 -0
  200. package/dist/controllers/theme_controller.js +8 -6
  201. package/dist/controllers/theme_controller.js.map +1 -1
  202. package/dist/controllers/time_picker_controller.d.ts +44 -13
  203. package/dist/controllers/time_picker_controller.js +296 -107
  204. package/dist/controllers/time_picker_controller.js.map +1 -1
  205. package/dist/controllers/toast_controller.d.ts +4 -0
  206. package/dist/controllers/toast_controller.js.map +1 -1
  207. package/dist/controllers/toggle_group_controller.d.ts +41 -13
  208. package/dist/controllers/toggle_group_controller.js +378 -55
  209. package/dist/controllers/toggle_group_controller.js.map +1 -1
  210. package/dist/controllers/toolbar_controller.js +5 -3
  211. package/dist/controllers/toolbar_controller.js.map +1 -1
  212. package/dist/controllers/tooltip_controller.d.ts +2 -2
  213. package/dist/controllers/tooltip_controller.js.map +1 -1
  214. package/dist/controllers/transition_controller.d.ts +4 -3
  215. package/dist/controllers/transition_controller.js.map +1 -1
  216. package/dist/controllers/tree_view_controller.d.ts +2 -0
  217. package/dist/controllers/tree_view_controller.js +7 -4
  218. package/dist/controllers/tree_view_controller.js.map +1 -1
  219. package/dist/index.js +4963 -1581
  220. package/dist/index.js.map +1 -1
  221. package/dist/inspector/cli.d.ts +123 -6
  222. package/dist/inspector/cli.js +212 -18
  223. package/dist/inspector/cli.js.map +1 -1
  224. package/dist/inspector/cli_bin.js +274 -50
  225. package/dist/inspector/cli_bin.js.map +1 -1
  226. package/dist/inspector/examples.json +15 -15
  227. package/dist/inspector/manifest.json +602 -80
  228. package/dist/positioning/index.d.ts +4 -2
  229. package/dist/positioning/index.js.map +1 -1
  230. package/package.json +1 -1
@@ -8,6 +8,123 @@ function isReservedArrowChord(event, allow = []) {
8
8
  return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
9
9
  }
10
10
 
11
+ // src/utils/attribute_lease.ts
12
+ var AttributeLease = class {
13
+ #attribute;
14
+ #records = /* @__PURE__ */ new Map();
15
+ /** @param attribute - The attribute whose temporary values this lease owns. */
16
+ constructor(attribute) {
17
+ this.#attribute = attribute;
18
+ }
19
+ /** Writes or removes the leased attribute while preserving its authored value. */
20
+ write(element, value) {
21
+ const existing = this.#records.get(element);
22
+ if (existing) {
23
+ existing.written = value;
24
+ } else {
25
+ this.#records.set(element, {
26
+ original: element.getAttribute(this.#attribute),
27
+ written: value
28
+ });
29
+ }
30
+ this.#reflect(element, value);
31
+ }
32
+ /** Returns one lease without overwriting a value subsequently authored by a consumer. */
33
+ return(element) {
34
+ const record = this.#records.get(element);
35
+ if (!record) return;
36
+ this.#records.delete(element);
37
+ const stillOwned = element.getAttribute(this.#attribute) === record.written;
38
+ if (stillOwned) this.#reflect(element, record.original);
39
+ }
40
+ /** Reflects only a real value transition, avoiding self-triggered mutation work. */
41
+ #reflect(element, value) {
42
+ if (element.getAttribute(this.#attribute) === value) return;
43
+ if (value === null) element.removeAttribute(this.#attribute);
44
+ else element.setAttribute(this.#attribute, value);
45
+ }
46
+ /** Returns every outstanding lease using the same ownership check as {@link return}. */
47
+ returnAll() {
48
+ for (const element of Array.from(this.#records.keys())) this.return(element);
49
+ }
50
+ };
51
+
52
+ // src/utils/before_cache_reset.ts
53
+ var BeforeCacheReset = class _BeforeCacheReset {
54
+ /** Every subscribed instance, iterated by the one shared document listener. */
55
+ static #subscribers = /* @__PURE__ */ new Set();
56
+ /** The shared listener; installed while at least one instance is subscribed. */
57
+ static #onBeforeCache = () => {
58
+ for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
59
+ };
60
+ #rewind;
61
+ /** @param rewind - the pass that returns this controller's state to its initial form. */
62
+ constructor(rewind) {
63
+ this.#rewind = rewind;
64
+ }
65
+ /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
66
+ activate() {
67
+ const first = _BeforeCacheReset.#subscribers.size === 0;
68
+ _BeforeCacheReset.#subscribers.add(this);
69
+ if (first) {
70
+ document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
71
+ }
72
+ }
73
+ /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
74
+ deactivate() {
75
+ _BeforeCacheReset.#subscribers.delete(this);
76
+ if (_BeforeCacheReset.#subscribers.size > 0) return;
77
+ document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
78
+ }
79
+ };
80
+
81
+ // src/utils/composition_tracker.ts
82
+ var CompositionTracker = class {
83
+ #observedTargets = /* @__PURE__ */ new Set();
84
+ #activeTargets = /* @__PURE__ */ new Set();
85
+ #onStart;
86
+ #onEnd;
87
+ constructor(options = {}) {
88
+ this.#onStart = options.onStart;
89
+ this.#onEnd = options.onEnd;
90
+ }
91
+ /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
92
+ observe(target) {
93
+ if (this.#observedTargets.has(target)) return;
94
+ target.addEventListener("compositionstart", this.#handleStart);
95
+ target.addEventListener("compositionend", this.#handleEnd);
96
+ this.#observedTargets.add(target);
97
+ }
98
+ /** Stops tracking one target and clears any active composition it owned. */
99
+ unobserve(target) {
100
+ if (!this.#observedTargets.delete(target)) return;
101
+ target.removeEventListener("compositionstart", this.#handleStart);
102
+ target.removeEventListener("compositionend", this.#handleEnd);
103
+ this.#activeTargets.delete(target);
104
+ }
105
+ /** Releases every listener and clears state so reconnect starts cleanly. */
106
+ disconnect() {
107
+ for (const target of this.#observedTargets) {
108
+ target.removeEventListener("compositionstart", this.#handleStart);
109
+ target.removeEventListener("compositionend", this.#handleEnd);
110
+ }
111
+ this.#observedTargets.clear();
112
+ this.#activeTargets.clear();
113
+ }
114
+ /** True when lifecycle tracking or the current event reports composition. */
115
+ isComposing(event) {
116
+ return this.#activeTargets.size > 0 || event?.isComposing === true;
117
+ }
118
+ #handleStart = (event) => {
119
+ if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
120
+ this.#onStart?.(event);
121
+ };
122
+ #handleEnd = (event) => {
123
+ if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
124
+ this.#onEnd?.(event);
125
+ };
126
+ };
127
+
11
128
  // src/utils/microtask_coalescer.ts
12
129
  var MicrotaskCoalescer = class {
13
130
  #run;
@@ -215,6 +332,7 @@ function finiteFallback(min, max) {
215
332
  }
216
333
 
217
334
  // src/controllers/number_input_controller.ts
335
+ var OWNED_DISABLED = "data-number-input-disabled";
218
336
  var NumberInputController = class _NumberInputController extends Controller {
219
337
  static targets = ["input", "increment", "decrement"];
220
338
  static values = {
@@ -224,7 +342,7 @@ var NumberInputController = class _NumberInputController extends Controller {
224
342
  pageStep: { type: Number, default: 0 }
225
343
  };
226
344
  static actions = ["decrement", "increment", "onInput", "onKeydown"];
227
- static events = ["change"];
345
+ static events = ["change", "reconcile"];
228
346
  /** Delay (ms) a button must be held before auto-repeat starts. */
229
347
  static #HOLD_DELAY_MS = 400;
230
348
  /** Interval (ms) between auto-repeat steps once a hold has started. */
@@ -236,8 +354,19 @@ var NumberInputController = class _NumberInputController extends Controller {
236
354
  * of poisoning the next legitimate click.
237
355
  */
238
356
  static #SUPPRESS_RESET_MS = 250;
239
- /** Aborts the pointer listeners on disconnect so none outlive the element. */
240
- #guards = null;
357
+ /** Aborts global pointer guards on disconnect so none outlive the element. */
358
+ #globalGuards = null;
359
+ /** Per-target guards allow an old button to be released without touching its replacement. */
360
+ #buttonGuards = /* @__PURE__ */ new Map();
361
+ /** Tracks IME lifecycle on the current input, including confirming keys without a signal. */
362
+ #composition = new CompositionTracker();
363
+ /** Restores authored custom-spinbutton ARIA when a target leaves or the controller stops. */
364
+ #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
365
+ #ariaValueNow = new AttributeLease("aria-valuenow");
366
+ #ariaValueMin = new AttributeLease("aria-valuemin");
367
+ #ariaValueMax = new AttributeLease("aria-valuemax");
368
+ /** Last reconciled or user-committed numeric value. */
369
+ #lastValue = null;
241
370
  /** Timers for the hold delay and the suppress-reset safety net. */
242
371
  #holdTimeouts = new SafeTimeout();
243
372
  /** The running auto-repeat interval (one at a time). */
@@ -248,63 +377,122 @@ var NumberInputController = class _NumberInputController extends Controller {
248
377
  #repeatedDuringHold = false;
249
378
  /** True when the next `click` is the trailing one after a hold and must be ignored. */
250
379
  #suppressNextClick = false;
251
- /** Collapses runtime range/step changes into one silent input reconciliation. */
380
+ /** Button and pointer that own the current hold, so other instances/pointers cannot end it. */
381
+ #holdButton = null;
382
+ #holdPointerId = null;
383
+ /** Collapses runtime range/step changes into one input reconciliation. */
252
384
  #repaint = new MicrotaskCoalescer(() => this.#reconcile());
253
385
  /** Normalizes any initial value and wires the focus/hold pointer guards. */
254
386
  connect() {
255
387
  this.#repaint.activate();
256
- if (!this.hasInputTarget) return;
388
+ this.#beforeCache.activate();
389
+ this.#globalGuards = new AbortController();
390
+ const { signal } = this.#globalGuards;
391
+ for (const button of this.incrementTargets) this.#wireButton(button, 1);
392
+ for (const button of this.decrementTargets) this.#wireButton(button, -1);
393
+ window.addEventListener("pointerup", this.#onPointerEnd, { signal });
394
+ window.addEventListener("pointercancel", this.#onPointerEnd, { signal });
395
+ window.addEventListener("blur", this.#onWindowBlur, { signal });
257
396
  this.#reconcile();
258
- this.#guards = new AbortController();
259
- const { signal } = this.#guards;
260
- if (this.hasIncrementTarget) this.#wireButton(this.incrementTarget, 1, signal);
261
- if (this.hasDecrementTarget) this.#wireButton(this.decrementTarget, -1, signal);
262
- window.addEventListener("pointerup", this.#stopHold, { signal });
263
- window.addEventListener("pointercancel", this.#stopHold, { signal });
264
- window.addEventListener("blur", this.#stopHold, { signal });
265
- }
266
- /** Releases the pointer guards and tears down every pending timer and hold state. */
397
+ }
398
+ /** Releases listeners, derived attributes, timers, and transient value state. */
267
399
  disconnect() {
268
400
  this.#repaint.cancel();
269
- this.#guards?.abort();
270
- this.#guards = null;
271
- this.#holdActive = false;
401
+ this.#composition.disconnect();
402
+ this.#beforeCache.deactivate();
403
+ this.#globalGuards?.abort();
404
+ this.#globalGuards = null;
405
+ const buttons = /* @__PURE__ */ new Set([
406
+ ...this.#buttonGuards.keys(),
407
+ ...this.incrementTargets,
408
+ ...this.decrementTargets
409
+ ]);
410
+ for (const guard of this.#buttonGuards.values()) guard.abort();
411
+ this.#buttonGuards.clear();
412
+ for (const button of buttons) this.#releaseButtonState(button);
413
+ this.#stopHold(false);
272
414
  this.#repeatedDuringHold = false;
273
415
  this.#suppressNextClick = false;
274
416
  this.#holdTimeouts.clearAll();
275
417
  this.#holdIntervals.clearAll();
418
+ this.#ariaValueNow.returnAll();
419
+ this.#ariaValueMin.returnAll();
420
+ this.#ariaValueMax.returnAll();
421
+ this.#lastValue = null;
276
422
  }
277
- /** Silently reconciles a minimum changed by application code or a Turbo morph. */
423
+ /** Reconciles the value against a minimum changed by application code or a Turbo morph. */
278
424
  minValueChanged() {
279
425
  this.#repaint.schedule();
280
426
  }
281
- /** Silently reconciles a maximum changed by application code or a Turbo morph. */
427
+ /** Reconciles the value against a maximum changed by application code or a Turbo morph. */
282
428
  maxValueChanged() {
283
429
  this.#repaint.schedule();
284
430
  }
285
- /** Silently reconciles a step changed by application code or a Turbo morph. */
431
+ /** Reconciles the value against a step changed by application code or a Turbo morph. */
286
432
  stepValueChanged() {
287
433
  this.#repaint.schedule();
288
434
  }
435
+ /** Tracks and reconciles an input inserted or replaced at runtime. */
436
+ inputTargetConnected(input) {
437
+ this.#composition.observe(input);
438
+ this.#repaint.schedule();
439
+ }
440
+ /** Releases ARIA and composition state owned by an input that left the controller. */
441
+ inputTargetDisconnected(input) {
442
+ this.#composition.unobserve(input);
443
+ this.#releaseInputAria(input);
444
+ this.#stopHold(false);
445
+ this.#lastValue = null;
446
+ this.#repaint.schedule();
447
+ }
448
+ /** Wires focus preservation and hold behavior on a runtime increment target. */
449
+ incrementTargetConnected(button) {
450
+ this.#wireButton(button, 1);
451
+ this.#repaint.schedule();
452
+ }
453
+ /** Releases only the increment target that actually disconnected. */
454
+ incrementTargetDisconnected(button) {
455
+ this.#unwireButton(button);
456
+ this.#repaint.schedule();
457
+ }
458
+ /** Wires focus preservation and hold behavior on a runtime decrement target. */
459
+ decrementTargetConnected(button) {
460
+ this.#wireButton(button, -1);
461
+ this.#repaint.schedule();
462
+ }
463
+ /** Releases only the decrement target that actually disconnected. */
464
+ decrementTargetDisconnected(button) {
465
+ this.#unwireButton(button);
466
+ this.#repaint.schedule();
467
+ }
289
468
  /** Increases by one step. Bound via `data-action` (click). */
290
469
  increment() {
291
470
  if (this.#consumeSuppressedClick()) return;
471
+ if (!this.hasInputTarget) return;
292
472
  this.#commitStep(1);
293
473
  this.inputTarget.focus();
294
474
  }
295
475
  /** Decreases by one step. Bound via `data-action` (click). */
296
476
  decrement() {
297
477
  if (this.#consumeSuppressedClick()) return;
478
+ if (!this.hasInputTarget) return;
298
479
  this.#commitStep(-1);
299
480
  this.inputTarget.focus();
300
481
  }
301
482
  /** Clamps and snaps a typed value. Bound via `data-action` (change). */
302
483
  onInput() {
303
- if (this.inputTarget.value.trim() === "") return;
484
+ if (!this.hasInputTarget) return;
485
+ if (this.inputTarget.value.trim() === "") {
486
+ this.#lastValue = null;
487
+ this.#reflectEmpty();
488
+ return;
489
+ }
304
490
  this.#commit(this.#currentValue());
305
491
  }
306
492
  /** Keyboard stepping per the APG spinbutton model. */
307
493
  onKeydown(event) {
494
+ if (!this.hasInputTarget) return;
495
+ if (this.#composition.isComposing(event)) return;
308
496
  if (isReservedArrowChord(event)) return;
309
497
  let next = null;
310
498
  switch (event.key) {
@@ -339,21 +527,39 @@ var NumberInputController = class _NumberInputController extends Controller {
339
527
  * hold; leaving the button while held stops it (the global listeners cover
340
528
  * release/cancel/blur).
341
529
  */
342
- #wireButton(button, direction, signal) {
530
+ #wireButton(button, direction) {
531
+ if (this.#buttonGuards.has(button)) return;
532
+ const guard = new AbortController();
533
+ this.#buttonGuards.set(button, guard);
343
534
  button.addEventListener("pointerdown", (event) => this.#armHold(event, button, direction), {
344
- signal
535
+ signal: guard.signal
345
536
  });
346
- button.addEventListener("pointerleave", this.#stopHold, { signal });
537
+ button.addEventListener(
538
+ "pointerleave",
539
+ (event) => {
540
+ if (this.#holdButton === button && this.#ownsPointer(event)) this.#stopHold();
541
+ },
542
+ { signal: guard.signal }
543
+ );
544
+ }
545
+ /** Aborts one target's listeners and returns any state derived onto that button. */
546
+ #unwireButton(button) {
547
+ this.#buttonGuards.get(button)?.abort();
548
+ this.#buttonGuards.delete(button);
549
+ if (this.#holdButton === button) this.#stopHold(false);
550
+ this.#releaseButtonState(button);
347
551
  }
348
552
  /** Starts a hold: focus the input, then schedule the first repeat after a delay. */
349
553
  #armHold(event, button, direction) {
350
554
  const pointerButton = event.button;
351
555
  if (typeof pointerButton === "number" && pointerButton !== 0) return;
352
- if (button.disabled) return;
556
+ if (button.disabled || !this.hasInputTarget) return;
353
557
  event.preventDefault();
354
558
  this.inputTarget.focus();
355
- this.#stopHold();
559
+ this.#stopHold(false);
356
560
  this.#holdActive = true;
561
+ this.#holdButton = button;
562
+ this.#holdPointerId = this.#pointerId(event);
357
563
  this.#repeatedDuringHold = false;
358
564
  this.#suppressNextClick = false;
359
565
  this.#holdTimeouts.set(() => {
@@ -372,18 +578,39 @@ var NumberInputController = class _NumberInputController extends Controller {
372
578
  * trailing synthetic `click` must be ignored, so it is suppressed until the
373
579
  * click consumes it or a short safety-net timeout clears it.
374
580
  */
375
- #stopHold = () => {
581
+ #stopHold(suppressTrailingClick = true) {
376
582
  if (!this.#holdActive) return;
377
583
  this.#holdActive = false;
584
+ this.#holdButton = null;
585
+ this.#holdPointerId = null;
378
586
  this.#holdTimeouts.clearAll();
379
587
  this.#holdIntervals.clearAll();
380
- if (this.#repeatedDuringHold) {
588
+ if (suppressTrailingClick && this.#repeatedDuringHold) {
381
589
  this.#suppressNextClick = true;
382
590
  this.#holdTimeouts.set(() => {
383
591
  this.#suppressNextClick = false;
384
592
  }, _NumberInputController.#SUPPRESS_RESET_MS);
593
+ } else if (!suppressTrailingClick) {
594
+ this.#suppressNextClick = false;
385
595
  }
596
+ }
597
+ /** Ends a hold only for its initiating pointer (legacy/synthetic events remain accepted). */
598
+ #onPointerEnd = (event) => {
599
+ if (this.#ownsPointer(event)) this.#stopHold();
600
+ };
601
+ /** Window deactivation is global and always ends the active hold. */
602
+ #onWindowBlur = () => {
603
+ this.#stopHold();
386
604
  };
605
+ /** Extracts pointer identity when the runtime event exposes it. */
606
+ #pointerId(event) {
607
+ return event.pointerId ?? null;
608
+ }
609
+ /** Whether `event` belongs to the active pointer, with a synthetic-event fallback. */
610
+ #ownsPointer(event) {
611
+ const pointerId = this.#pointerId(event);
612
+ return this.#holdActive && (this.#holdPointerId === null || pointerId === null || pointerId === this.#holdPointerId);
613
+ }
387
614
  /** Consumes a pending trailing-click suppression; returns true if the click was swallowed. */
388
615
  #consumeSuppressedClick() {
389
616
  if (!this.#suppressNextClick) return false;
@@ -392,53 +619,80 @@ var NumberInputController = class _NumberInputController extends Controller {
392
619
  return true;
393
620
  }
394
621
  /**
395
- * Normalizes `raw`, reflects it, and dispatches `change` only when the
396
- * displayed value actually changes.
622
+ * Normalizes `raw`, reflects it, and dispatches `{ value: number }` only when
623
+ * the committed numeric value differs from the last reconciled value.
397
624
  *
398
625
  * @returns Whether the value changed (drives the auto-repeat's bound stop).
399
626
  */
400
627
  #commit(raw) {
401
628
  const value = this.#normalize(raw);
402
- const changed = this.inputTarget.value !== String(value);
629
+ const changed = this.#lastValue === null || value !== this.#lastValue;
403
630
  this.#write(value);
631
+ this.#lastValue = value;
404
632
  if (changed) this.dispatch("change", { detail: { value } });
405
633
  return changed;
406
634
  }
407
635
  /** Commits an adjacent endpoint/grid value and reports whether it moved. */
408
636
  #commitStep(count) {
637
+ if (!this.hasInputTarget) return false;
409
638
  return this.#commit(stepSteppedValue(this.#currentValue(), count, this.#steppedRange));
410
639
  }
411
640
  /**
412
- * Silently reflects the current value after a range or step morph.
641
+ * Reflects the current value after a range or step morph, reporting a moved
642
+ * value as `reconcile`.
413
643
  *
414
644
  * @stimeoRenderRoot
415
645
  */
416
646
  #reconcile() {
417
- if (!this.hasInputTarget) return;
647
+ if (!this.hasInputTarget) {
648
+ this.#lastValue = null;
649
+ return;
650
+ }
418
651
  if (this.inputTarget.value.trim() !== "") {
419
- this.#write(this.#normalize(this.#currentValue()));
652
+ const previous = this.#lastValue;
653
+ const value = this.#normalize(this.#currentValue());
654
+ this.#write(value);
655
+ this.#lastValue = value;
656
+ if (previous !== null && value !== previous) {
657
+ this.dispatch("reconcile", { detail: { value } });
658
+ }
420
659
  } else {
421
- this.#updateButtons(this.#currentValue());
660
+ this.#lastValue = null;
661
+ this.#reflectEmpty();
422
662
  }
423
663
  }
424
664
  /** Reflects `value` on the input (and ARIA for non-native hosts) and the buttons. */
425
665
  #write(value) {
426
- this.inputTarget.value = String(value);
427
- if (this.inputTarget.getAttribute("role") === "spinbutton") {
428
- this.inputTarget.setAttribute("aria-valuenow", String(value));
429
- if (Number.isFinite(this.minValue)) {
430
- this.inputTarget.setAttribute("aria-valuemin", String(this.minValue));
431
- }
432
- if (Number.isFinite(this.maxValue)) {
433
- this.inputTarget.setAttribute("aria-valuemax", String(this.maxValue));
434
- }
435
- }
666
+ const input = this.inputTarget;
667
+ input.value = String(value);
668
+ this.#syncInputAria(input, value);
436
669
  this.#updateButtons(value);
437
670
  }
671
+ /** Reflects a blank value without inventing a numeric `aria-valuenow`. */
672
+ #reflectEmpty() {
673
+ this.#syncInputAria(this.inputTarget, null);
674
+ this.#updateButtons(this.#currentValue());
675
+ }
676
+ /** Synchronizes controller-derived ARIA while preserving authored teardown values. */
677
+ #syncInputAria(input, value) {
678
+ if (input.getAttribute("role") !== "spinbutton") {
679
+ this.#releaseInputAria(input);
680
+ return;
681
+ }
682
+ this.#ariaValueNow.write(input, value === null ? null : String(value));
683
+ this.#ariaValueMin.write(input, Number.isFinite(this.minValue) ? String(this.minValue) : null);
684
+ this.#ariaValueMax.write(input, Number.isFinite(this.maxValue) ? String(this.maxValue) : null);
685
+ }
686
+ /** Returns all custom-spinbutton ARIA leased on `input`. */
687
+ #releaseInputAria(input) {
688
+ this.#ariaValueNow.return(input);
689
+ this.#ariaValueMin.return(input);
690
+ this.#ariaValueMax.return(input);
691
+ }
438
692
  /** Disables a step button at its bound, returning focus to the input first. */
439
693
  #updateButtons(value) {
440
- if (this.hasIncrementTarget) this.#toggleButton(this.incrementTarget, value < this.maxValue);
441
- if (this.hasDecrementTarget) this.#toggleButton(this.decrementTarget, value > this.minValue);
694
+ for (const button of this.incrementTargets) this.#toggleButton(button, value < this.maxValue);
695
+ for (const button of this.decrementTargets) this.#toggleButton(button, value > this.minValue);
442
696
  }
443
697
  /**
444
698
  * Enables or disables a step button at its bound, never disabling one while it
@@ -448,16 +702,22 @@ var NumberInputController = class _NumberInputController extends Controller {
448
702
  */
449
703
  #toggleButton(button, enabled) {
450
704
  if (enabled) {
451
- if (button.hasAttribute("data-number-input-disabled")) {
705
+ if (button.hasAttribute(OWNED_DISABLED)) {
452
706
  button.disabled = false;
453
- button.removeAttribute("data-number-input-disabled");
707
+ button.removeAttribute(OWNED_DISABLED);
454
708
  }
455
709
  return;
456
710
  }
457
711
  if (button.disabled) return;
458
- if (document.activeElement === button) this.inputTarget.focus();
712
+ if (document.activeElement === button && this.hasInputTarget) this.inputTarget.focus();
459
713
  button.disabled = true;
460
- button.setAttribute("data-number-input-disabled", "");
714
+ button.setAttribute(OWNED_DISABLED, "");
715
+ }
716
+ /** Returns `disabled` only when its explicit marker still identifies controller ownership. */
717
+ #releaseButtonState(button) {
718
+ if (!button.hasAttribute(OWNED_DISABLED)) return;
719
+ button.disabled = false;
720
+ button.removeAttribute(OWNED_DISABLED);
461
721
  }
462
722
  /** The current numeric value, falling back to a finite min (else 0) when blank. */
463
723
  #currentValue() {
@@ -473,6 +733,12 @@ var NumberInputController = class _NumberInputController extends Controller {
473
733
  get #steppedRange() {
474
734
  return { min: this.minValue, max: this.maxValue, step: this.stepValue };
475
735
  }
736
+ /** Returns borrowed range ARIA before Turbo snapshots the page. */
737
+ #rewindForCache() {
738
+ this.#ariaValueNow.returnAll();
739
+ this.#ariaValueMin.returnAll();
740
+ this.#ariaValueMax.returnAll();
741
+ }
476
742
  };
477
743
 
478
744
  export { NumberInputController };