stimeo-ui 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +128 -1
  3. package/dist/cable/index.d.ts +79 -23
  4. package/dist/cable/index.js +392 -417
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/controllers/accordion_controller.d.ts +34 -5
  7. package/dist/controllers/accordion_controller.js +228 -28
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.d.ts +20 -9
  10. package/dist/controllers/alert_dialog_controller.js +862 -221
  11. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  12. package/dist/controllers/announcer_controller.d.ts +38 -7
  13. package/dist/controllers/announcer_controller.js +135 -191
  14. package/dist/controllers/announcer_controller.js.map +1 -1
  15. package/dist/controllers/aspect_ratio_controller.js +0 -10
  16. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  17. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  18. package/dist/controllers/auto_submit_controller.js +295 -110
  19. package/dist/controllers/auto_submit_controller.js.map +1 -1
  20. package/dist/controllers/avatar_controller.js +172 -106
  21. package/dist/controllers/avatar_controller.js.map +1 -1
  22. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  23. package/dist/controllers/breadcrumb_controller.js +0 -129
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +22 -4
  26. package/dist/controllers/bulk_select_controller.js +264 -53
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +109 -28
  29. package/dist/controllers/calendar_controller.js +346 -180
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +99 -27
  32. package/dist/controllers/carousel_controller.js +587 -300
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +18 -2
  35. package/dist/controllers/character_counter_controller.js +271 -143
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +77 -75
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +44 -7
  41. package/dist/controllers/clipboard_controller.js +187 -108
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +41 -5
  44. package/dist/controllers/collapsible_controller.js +374 -108
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +47 -14
  47. package/dist/controllers/color_picker_controller.js +183 -127
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +33 -6
  50. package/dist/controllers/combobox_controller.js +426 -109
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +45 -14
  53. package/dist/controllers/command_palette_controller.js +994 -350
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +212 -125
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +939 -245
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +36 -3
  62. package/dist/controllers/context_menu_controller.js +275 -127
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +75 -2
  68. package/dist/controllers/countdown_controller.js +235 -97
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -15
  71. package/dist/controllers/currency_input_controller.js +241 -118
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +78 -22
  74. package/dist/controllers/data_grid_controller.js +229 -135
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +64 -11
  77. package/dist/controllers/date_range_picker_controller.js +359 -165
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +22 -3
  80. package/dist/controllers/dialog_controller.js +887 -225
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +24 -7
  83. package/dist/controllers/direct_upload_controller.js +80 -142
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  86. package/dist/controllers/dirty_form_controller.js +26 -47
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +175 -20
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +52 -16
  92. package/dist/controllers/drawer_controller.js +963 -336
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +39 -3
  95. package/dist/controllers/dropdown_controller.js +342 -85
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +182 -58
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +67 -53
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +60 -18
  103. package/dist/controllers/file_dropzone_controller.js +426 -229
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.d.ts +10 -4
  106. package/dist/controllers/filter_controller.js +78 -36
  107. package/dist/controllers/filter_controller.js.map +1 -1
  108. package/dist/controllers/flash_controller.d.ts +57 -14
  109. package/dist/controllers/flash_controller.js +328 -211
  110. package/dist/controllers/flash_controller.js.map +1 -1
  111. package/dist/controllers/focus_controller.d.ts +12 -3
  112. package/dist/controllers/focus_controller.js +654 -239
  113. package/dist/controllers/focus_controller.js.map +1 -1
  114. package/dist/controllers/form_field_controller.js +153 -136
  115. package/dist/controllers/form_field_controller.js.map +1 -1
  116. package/dist/controllers/form_validation_controller.js +29 -96
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +41 -9
  119. package/dist/controllers/frame_loading_controller.js +263 -198
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +22 -2
  122. package/dist/controllers/highlight_controller.js +119 -70
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +65 -4
  125. package/dist/controllers/hover_card_controller.js +335 -118
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +58 -4
  128. package/dist/controllers/idle_controller.js +316 -65
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +5 -2
  131. package/dist/controllers/input_mask_controller.js +116 -61
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +147 -103
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +16 -0
  137. package/dist/controllers/lazy_frame_controller.js +9 -56
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +84 -10
  140. package/dist/controllers/listbox_controller.js +413 -153
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +1 -1
  143. package/dist/controllers/local_time_controller.js +54 -64
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +70 -95
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +40 -9
  149. package/dist/controllers/menu_controller.js +254 -167
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +15 -4
  152. package/dist/controllers/menubar_controller.js +206 -331
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +20 -0
  155. package/dist/controllers/meter_controller.js +159 -71
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +50 -7
  158. package/dist/controllers/multi_select_controller.js +797 -388
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +19 -1
  161. package/dist/controllers/navigation_menu_controller.js +156 -231
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +373 -174
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +241 -45
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +62 -10
  170. package/dist/controllers/number_input_controller.js +387 -227
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.js +28 -75
  173. package/dist/controllers/optimistic_controller.js.map +1 -1
  174. package/dist/controllers/otp_controller.d.ts +38 -7
  175. package/dist/controllers/otp_controller.js +344 -196
  176. package/dist/controllers/otp_controller.js.map +1 -1
  177. package/dist/controllers/overflow_indicator_controller.d.ts +13 -12
  178. package/dist/controllers/overflow_indicator_controller.js +167 -93
  179. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  180. package/dist/controllers/overflow_menu_controller.d.ts +66 -9
  181. package/dist/controllers/overflow_menu_controller.js +408 -277
  182. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  183. package/dist/controllers/pagination_controller.d.ts +52 -15
  184. package/dist/controllers/pagination_controller.js +342 -120
  185. package/dist/controllers/pagination_controller.js.map +1 -1
  186. package/dist/controllers/password_reveal_controller.d.ts +60 -9
  187. package/dist/controllers/password_reveal_controller.js +424 -99
  188. package/dist/controllers/password_reveal_controller.js.map +1 -1
  189. package/dist/controllers/password_strength_controller.d.ts +14 -5
  190. package/dist/controllers/password_strength_controller.js +99 -153
  191. package/dist/controllers/password_strength_controller.js.map +1 -1
  192. package/dist/controllers/persist_controller.d.ts +11 -4
  193. package/dist/controllers/persist_controller.js +252 -96
  194. package/dist/controllers/persist_controller.js.map +1 -1
  195. package/dist/controllers/pointer_drag_controller.d.ts +19 -7
  196. package/dist/controllers/pointer_drag_controller.js +263 -164
  197. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  198. package/dist/controllers/popover_controller.d.ts +46 -4
  199. package/dist/controllers/popover_controller.js +267 -94
  200. package/dist/controllers/popover_controller.js.map +1 -1
  201. package/dist/controllers/portal_controller.d.ts +44 -4
  202. package/dist/controllers/portal_controller.js +209 -72
  203. package/dist/controllers/portal_controller.js.map +1 -1
  204. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  205. package/dist/controllers/preview_guard_controller.js +243 -112
  206. package/dist/controllers/preview_guard_controller.js.map +1 -1
  207. package/dist/controllers/progress_controller.d.ts +11 -0
  208. package/dist/controllers/progress_controller.js +132 -59
  209. package/dist/controllers/progress_controller.js.map +1 -1
  210. package/dist/controllers/radio_group_controller.d.ts +19 -8
  211. package/dist/controllers/radio_group_controller.js +138 -133
  212. package/dist/controllers/radio_group_controller.js.map +1 -1
  213. package/dist/controllers/range_slider_controller.d.ts +92 -4
  214. package/dist/controllers/range_slider_controller.js +245 -112
  215. package/dist/controllers/range_slider_controller.js.map +1 -1
  216. package/dist/controllers/rating_controller.d.ts +33 -6
  217. package/dist/controllers/rating_controller.js +429 -194
  218. package/dist/controllers/rating_controller.js.map +1 -1
  219. package/dist/controllers/read_more_controller.d.ts +26 -2
  220. package/dist/controllers/read_more_controller.js +185 -47
  221. package/dist/controllers/read_more_controller.js.map +1 -1
  222. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  223. package/dist/controllers/reading_progress_controller.js +147 -135
  224. package/dist/controllers/reading_progress_controller.js.map +1 -1
  225. package/dist/controllers/relative_time_controller.d.ts +18 -4
  226. package/dist/controllers/relative_time_controller.js +145 -110
  227. package/dist/controllers/relative_time_controller.js.map +1 -1
  228. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  229. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  230. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  231. package/dist/controllers/resizable_controller.d.ts +44 -4
  232. package/dist/controllers/resizable_controller.js +294 -118
  233. package/dist/controllers/resizable_controller.js.map +1 -1
  234. package/dist/controllers/roving_controller.js +39 -84
  235. package/dist/controllers/roving_controller.js.map +1 -1
  236. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  237. package/dist/controllers/scroll_area_controller.js +428 -221
  238. package/dist/controllers/scroll_area_controller.js.map +1 -1
  239. package/dist/controllers/scroll_restore_controller.js +0 -53
  240. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  241. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  242. package/dist/controllers/scroll_visibility_controller.js +395 -146
  243. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  244. package/dist/controllers/scrollspy_controller.d.ts +9 -3
  245. package/dist/controllers/scrollspy_controller.js +181 -236
  246. package/dist/controllers/scrollspy_controller.js.map +1 -1
  247. package/dist/controllers/separator_controller.d.ts +59 -17
  248. package/dist/controllers/separator_controller.js +288 -132
  249. package/dist/controllers/separator_controller.js.map +1 -1
  250. package/dist/controllers/sidebar_controller.d.ts +81 -7
  251. package/dist/controllers/sidebar_controller.js +1071 -375
  252. package/dist/controllers/sidebar_controller.js.map +1 -1
  253. package/dist/controllers/skeleton_controller.d.ts +23 -0
  254. package/dist/controllers/skeleton_controller.js +248 -92
  255. package/dist/controllers/skeleton_controller.js.map +1 -1
  256. package/dist/controllers/slider_controller.d.ts +72 -8
  257. package/dist/controllers/slider_controller.js +211 -96
  258. package/dist/controllers/slider_controller.js.map +1 -1
  259. package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
  260. package/dist/controllers/smart_sticky_header_controller.js +163 -54
  261. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  262. package/dist/controllers/sortable_controller.js +13 -110
  263. package/dist/controllers/sortable_controller.js.map +1 -1
  264. package/dist/controllers/spinner_controller.d.ts +39 -4
  265. package/dist/controllers/spinner_controller.js +277 -161
  266. package/dist/controllers/spinner_controller.js.map +1 -1
  267. package/dist/controllers/step_indicator_controller.d.ts +24 -17
  268. package/dist/controllers/step_indicator_controller.js +107 -53
  269. package/dist/controllers/step_indicator_controller.js.map +1 -1
  270. package/dist/controllers/stepper_controller.d.ts +43 -9
  271. package/dist/controllers/stepper_controller.js +171 -48
  272. package/dist/controllers/stepper_controller.js.map +1 -1
  273. package/dist/controllers/stick_to_bottom_controller.d.ts +25 -0
  274. package/dist/controllers/stick_to_bottom_controller.js +310 -116
  275. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  276. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  277. package/dist/controllers/sticky_observer_controller.js +214 -49
  278. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  279. package/dist/controllers/submit_once_controller.d.ts +22 -6
  280. package/dist/controllers/submit_once_controller.js +294 -192
  281. package/dist/controllers/submit_once_controller.js.map +1 -1
  282. package/dist/controllers/switch_controller.d.ts +40 -7
  283. package/dist/controllers/switch_controller.js +107 -43
  284. package/dist/controllers/switch_controller.js.map +1 -1
  285. package/dist/controllers/tabs_controller.d.ts +13 -1
  286. package/dist/controllers/tabs_controller.js +50 -25
  287. package/dist/controllers/tabs_controller.js.map +1 -1
  288. package/dist/controllers/tags_input_controller.d.ts +27 -3
  289. package/dist/controllers/tags_input_controller.js +290 -149
  290. package/dist/controllers/tags_input_controller.js.map +1 -1
  291. package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
  292. package/dist/controllers/textarea_autosize_controller.js +71 -58
  293. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  294. package/dist/controllers/theme_controller.d.ts +37 -8
  295. package/dist/controllers/theme_controller.js +148 -80
  296. package/dist/controllers/theme_controller.js.map +1 -1
  297. package/dist/controllers/time_picker_controller.d.ts +18 -1
  298. package/dist/controllers/time_picker_controller.js +162 -81
  299. package/dist/controllers/time_picker_controller.js.map +1 -1
  300. package/dist/controllers/toast_controller.d.ts +85 -23
  301. package/dist/controllers/toast_controller.js +365 -193
  302. package/dist/controllers/toast_controller.js.map +1 -1
  303. package/dist/controllers/toggle_group_controller.d.ts +60 -9
  304. package/dist/controllers/toggle_group_controller.js +194 -99
  305. package/dist/controllers/toggle_group_controller.js.map +1 -1
  306. package/dist/controllers/toolbar_controller.js +92 -90
  307. package/dist/controllers/toolbar_controller.js.map +1 -1
  308. package/dist/controllers/tooltip_controller.d.ts +52 -2
  309. package/dist/controllers/tooltip_controller.js +185 -125
  310. package/dist/controllers/tooltip_controller.js.map +1 -1
  311. package/dist/controllers/transition_controller.d.ts +22 -4
  312. package/dist/controllers/transition_controller.js +123 -115
  313. package/dist/controllers/transition_controller.js.map +1 -1
  314. package/dist/controllers/tree_view_controller.d.ts +59 -18
  315. package/dist/controllers/tree_view_controller.js +248 -290
  316. package/dist/controllers/tree_view_controller.js.map +1 -1
  317. package/dist/index.d.ts +33 -3
  318. package/dist/index.js +8649 -8572
  319. package/dist/index.js.map +1 -1
  320. package/dist/inspector/cli.d.ts +95 -9
  321. package/dist/inspector/cli.js +343 -77
  322. package/dist/inspector/cli.js.map +1 -1
  323. package/dist/inspector/cli_bin.js +390 -96
  324. package/dist/inspector/cli_bin.js.map +1 -1
  325. package/dist/inspector/examples.json +45 -45
  326. package/dist/inspector/manifest.json +1308 -155
  327. package/dist/positioning/index.d.ts +8 -0
  328. package/dist/positioning/index.js +148 -45
  329. package/dist/positioning/index.js.map +1 -1
  330. package/package.json +7 -7
  331. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  332. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,9 +1,6 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
  import { createConsumer } from '@rails/actioncable';
3
3
 
4
- // src/cable/live_counter_controller.ts
5
-
6
- // src/utils/announce.ts
7
4
  function announce(message, options = {}) {
8
5
  const text = message.trim();
9
6
  if (text.length === 0) return;
@@ -20,7 +17,6 @@ function fillTemplate(template, values) {
20
17
  });
21
18
  }
22
19
 
23
- // src/utils/authored_integer.ts
24
20
  var TOKEN_BODY = /\d[\d.,'’’    _]*\d|\d/;
25
21
  var SIGN_OPENER = /[\s([]/;
26
22
  function authoredInteger(text) {
@@ -42,29 +38,21 @@ function authoredInteger(text) {
42
38
  return signed ? -magnitude || 0 : magnitude;
43
39
  }
44
40
 
45
- // src/utils/blur_deferral.ts
46
41
  var BlurDeferral = class {
47
- /** Elements currently holding an update back, mapped to their `blur` listener. */
48
42
  #pending = /* @__PURE__ */ new Map();
49
- /** Called after a pending element blurs and has been detached. */
50
43
  #onRelease;
51
- /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */
52
44
  constructor(onRelease) {
53
45
  this.#onRelease = onRelease;
54
46
  }
55
- /** Number of elements currently holding an update back. */
56
47
  get size() {
57
48
  return this.#pending.size;
58
49
  }
59
- /** Snapshot of the pending elements, safe to iterate while releasing them. */
60
50
  get elements() {
61
51
  return [...this.#pending.keys()];
62
52
  }
63
- /** Whether `element` is currently holding an update back. */
64
53
  has(element) {
65
54
  return this.#pending.has(element);
66
55
  }
67
- /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */
68
56
  defer(element) {
69
57
  if (this.#pending.has(element)) return;
70
58
  const onBlur = () => {
@@ -74,22 +62,18 @@ var BlurDeferral = class {
74
62
  this.#pending.set(element, onBlur);
75
63
  element.addEventListener("blur", onBlur);
76
64
  }
77
- /** Defers `element` as the only pending entry, cancelling any others. */
78
65
  deferOnly(element) {
79
66
  for (const pending of this.elements) {
80
67
  if (pending !== element) this.#detach(pending);
81
68
  }
82
69
  this.defer(element);
83
70
  }
84
- /** Cancels `element`'s deferral without completing it; no-ops when not pending. */
85
71
  release(element) {
86
72
  this.#detach(element);
87
73
  }
88
- /** Cancels every deferral without completing any of them. */
89
74
  releaseAll() {
90
75
  for (const element of this.elements) this.#detach(element);
91
76
  }
92
- /** Removes the `blur` listener for `element` and forgets it. */
93
77
  #detach(element) {
94
78
  const onBlur = this.#pending.get(element);
95
79
  if (onBlur) element.removeEventListener("blur", onBlur);
@@ -97,44 +81,90 @@ var BlurDeferral = class {
97
81
  }
98
82
  };
99
83
 
100
- // src/utils/safe_timeout.ts
84
+ var MicrotaskCoalescer = class {
85
+ #run;
86
+ #queued = false;
87
+ #active = false;
88
+ #generation = 0;
89
+ constructor(run) {
90
+ this.#run = run;
91
+ }
92
+ activate() {
93
+ this.#active = true;
94
+ }
95
+ cancel() {
96
+ this.#active = false;
97
+ this.#queued = false;
98
+ this.#generation += 1;
99
+ }
100
+ schedule() {
101
+ if (!this.#active || this.#queued) return;
102
+ this.#queued = true;
103
+ const generation = this.#generation;
104
+ queueMicrotask(() => {
105
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
106
+ this.#queued = false;
107
+ this.#run();
108
+ });
109
+ }
110
+ };
111
+
112
+ var ListenerSet = class {
113
+ #abort = new AbortController();
114
+ add(target, type, handler, options) {
115
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
116
+ }
117
+ dispose() {
118
+ this.#abort.abort();
119
+ this.#abort = new AbortController();
120
+ }
121
+ };
122
+
123
+ var MorphRenderWatcher = class {
124
+ #listeners = new ListenerSet();
125
+ #pass;
126
+ #element = null;
127
+ #onMorph = () => this.#pass.schedule();
128
+ constructor(run) {
129
+ this.#pass = new MicrotaskCoalescer(run);
130
+ }
131
+ observe(element) {
132
+ if (this.#element === element) return;
133
+ this.disconnect();
134
+ this.#element = element;
135
+ this.#pass.activate();
136
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
137
+ }
138
+ schedule() {
139
+ this.#pass.schedule();
140
+ }
141
+ disconnect() {
142
+ this.#listeners.dispose();
143
+ this.#pass.cancel();
144
+ this.#element = null;
145
+ }
146
+ };
147
+
101
148
  var MAX_TIMER_DELAY_MS = 2147483647;
149
+
102
150
  var TimerRegistry = class {
103
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
104
151
  ids = /* @__PURE__ */ new Set();
105
- /**
106
- * Cancels a single tracked timer.
107
- *
108
- * No-ops if the id is unknown (already cleared, fired, or never owned by this
109
- * registry), so callers can clear defensively without guarding.
110
- */
111
152
  clear(id) {
112
153
  if (this.ids.delete(id)) {
113
154
  this.cancel(id);
114
155
  }
115
156
  }
116
- /**
117
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
118
- * to guarantee no timer outlives the element.
119
- */
120
157
  clearAll() {
121
158
  for (const id of this.ids) {
122
159
  this.cancel(id);
123
160
  }
124
161
  this.ids.clear();
125
162
  }
126
- /** Number of timers currently tracked (pending). */
127
163
  get size() {
128
164
  return this.ids.size;
129
165
  }
130
166
  };
131
167
  var SafeTimeout = class extends TimerRegistry {
132
- /**
133
- * Schedules `callback` after `delay` ms and returns the timer id.
134
- *
135
- * The id is removed from the registry automatically when the timeout fires,
136
- * so {@link TimerRegistry.size | size} reflects only still-pending timers.
137
- */
138
168
  set(callback, delay) {
139
169
  const id = this.schedule(() => {
140
170
  this.ids.delete(id);
@@ -151,7 +181,6 @@ var SafeTimeout = class extends TimerRegistry {
151
181
  }
152
182
  };
153
183
  var SafeInterval = class extends TimerRegistry {
154
- /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
155
184
  set(callback, delay) {
156
185
  const id = this.schedule(callback, delay);
157
186
  this.ids.add(id);
@@ -164,6 +193,27 @@ var SafeInterval = class extends TimerRegistry {
164
193
  window.clearInterval(id);
165
194
  }
166
195
  };
196
+
197
+ var TransientHooks = class {
198
+ #attributes;
199
+ constructor(options) {
200
+ this.#attributes = options.attributes;
201
+ }
202
+ reset(element) {
203
+ for (const attribute of this.#attributes) element.removeAttribute(attribute);
204
+ }
205
+ };
206
+
207
+ function sharedRegistry(key, create) {
208
+ const symbol = Symbol.for(key);
209
+ const scope = globalThis;
210
+ const existing = scope[symbol];
211
+ if (existing !== void 0) return existing;
212
+ const registry = create();
213
+ Object.defineProperty(globalThis, symbol, { value: registry });
214
+ return registry;
215
+ }
216
+
167
217
  function parseSubscriptionParams(raw) {
168
218
  try {
169
219
  const parsed = JSON.parse(raw);
@@ -173,15 +223,21 @@ function parseSubscriptionParams(raw) {
173
223
  return {};
174
224
  }
175
225
  }
176
- var sharedConsumer = null;
226
+ var consumerRegistry = sharedRegistry(
227
+ "stimeo-ui.cable-consumer.registry.v1",
228
+ () => ({ consumer: null })
229
+ );
177
230
  function setCableConsumer(consumer) {
178
- sharedConsumer = consumer;
231
+ consumerRegistry.consumer = consumer;
179
232
  }
180
233
  function getCableConsumer() {
181
- if (!sharedConsumer) sharedConsumer = createConsumer();
182
- return sharedConsumer;
234
+ if (!consumerRegistry.consumer) consumerRegistry.consumer = createConsumer();
235
+ return consumerRegistry.consumer;
183
236
  }
184
- var sharedSubscriptions = /* @__PURE__ */ new WeakMap();
237
+ var sharedSubscriptions = sharedRegistry(
238
+ "stimeo-ui.cable-subscriptions.registry.v1",
239
+ () => /* @__PURE__ */ new WeakMap()
240
+ );
185
241
  function identifierOf(channel) {
186
242
  return JSON.stringify(typeof channel === "string" ? { channel } : channel);
187
243
  }
@@ -245,9 +301,6 @@ function openSharedSubscription(consumer, channel) {
245
301
  confirmed: false,
246
302
  rejected: false,
247
303
  subscription: consumer.subscriptions.create(channel, {
248
- // A refusal is final: Action Cable never confirms a rejected subscription,
249
- // so a late connected/disconnected (only possible from a misbehaving
250
- // consumer double) must not reopen the gate `rejected` promised shut.
251
304
  connected: () => {
252
305
  if (shared.rejected) return;
253
306
  shared.confirmed = true;
@@ -269,7 +322,7 @@ function openSharedSubscription(consumer, channel) {
269
322
  return shared;
270
323
  }
271
324
 
272
- // src/cable/live_counter_controller.ts
325
+ var TRANSIENT = new TransientHooks({ attributes: ["data-live-counter-rejected"] });
273
326
  var DISABLED_MARKER = "data-live-counter-disabled";
274
327
  var LiveCounterController = class _LiveCounterController extends Controller {
275
328
  static targets = ["value", "trigger"];
@@ -279,80 +332,42 @@ var LiveCounterController = class _LiveCounterController extends Controller {
279
332
  id: { type: String, default: "" },
280
333
  announceText: { type: String, default: "" }
281
334
  };
282
- /** Collapses a burst of broadcasts into one announcement. */
283
335
  static #announceDelay = 200;
284
336
  static actions = ["increment"];
285
337
  static events = ["change"];
286
- /** Identifier parameters parsed once from their declaration, never in the hot path. */
287
- #params = {};
288
338
  #subscription = null;
289
- #connected = false;
290
- /**
291
- * The optimistic bumps this client has yet to see echoed back, in the order it
292
- * sent them. An echo belongs to the send that caused it, and the amount is what
293
- * names that send: a counter that guessed nothing — or guessed a different
294
- * amount — applies the delta like any other, so siblings and other tabs
295
- * converge whatever step each of them uses.
296
- */
339
+ #follow = new MicrotaskCoalescer(() => this.#followDeclaration());
340
+ #identifier = "";
341
+ #morphRender = new MorphRenderWatcher(() => this.#syncTriggers());
297
342
  #outstanding = [];
298
- /** Holds a disable back while the trigger owns focus, so focus is never taken away. */
299
343
  #focusedTriggers = new BlurDeferral((trigger) => this.#syncTrigger(trigger));
300
344
  #timers = new SafeTimeout();
301
- /**
302
- * Re-parses the identifier parameters and moves the subscription to the
303
- * identifier they now name.
304
- *
305
- * A malformed declaration falls back to no parameters, so the identifier keeps
306
- * naming the channel instead of the subscription never being created at all.
307
- */
308
345
  paramsValueChanged() {
309
- this.#params = parseSubscriptionParams(this.paramsValue);
310
- this.#resubscribe();
346
+ this.#follow.schedule();
311
347
  }
312
- /** Moves the subscription when the declaration names a different channel. */
313
348
  channelValueChanged() {
314
- this.#resubscribe();
349
+ this.#follow.schedule();
315
350
  }
316
351
  connect() {
317
- this.#connected = true;
352
+ this.#morphRender.observe(this.element);
353
+ this.#follow.activate();
318
354
  this.#subscribe();
319
355
  }
320
- /**
321
- * Opens the subscription the declaration names, and reflects the gate.
322
- *
323
- * The rejected hook is transient server state: a Turbo cache snapshot must not
324
- * resurrect it — the fresh subscription re-decides it. An outstanding guess
325
- * belongs to the identifier that is going away, so it is dropped with it.
326
- */
327
356
  #subscribe() {
328
- this.element.removeAttribute("data-live-counter-rejected");
357
+ TRANSIENT.reset(this.element);
329
358
  this.#outstanding.length = 0;
359
+ const descriptor = this.#descriptor;
360
+ this.#identifier = identifierOf(descriptor);
330
361
  if (this.channelValue) {
331
- this.#subscription = this.#open();
362
+ this.#subscription = this.#open(descriptor);
332
363
  }
333
364
  this.#syncTriggers();
334
365
  }
335
- /**
336
- * Opens the wire subscription, or returns null when it cannot be opened.
337
- *
338
- * A consumer that cannot be built (no Action Cable meta tag, a boot-order
339
- * mistake) would otherwise throw out of `connect()` and leave the element
340
- * half-wired: no gate reflected, and a trigger that looks live. The gate reads
341
- * the declaration, so a null subscription keeps it shut and the display never
342
- * moves past what the server can receive.
343
- */
344
- #open() {
366
+ #open(descriptor) {
345
367
  try {
346
- return createConfirmedSubscription(this.#descriptor, {
368
+ return createConfirmedSubscription(descriptor, {
347
369
  connected: () => this.#syncTriggers(),
348
- // A drop closes the send window (the shared subscription tracks it)
349
- // until Action Cable reconnects and re-confirms — an increment during
350
- // the outage would bump the display while its perform() is silently
351
- // discarded by the closed socket.
352
370
  disconnected: () => this.#syncTriggers(),
353
- // The server refused the subscription (auth, bad params): the gate
354
- // stays shut for good, and the hook lets the consumer's CSS disable
355
- // or hide the trigger instead of leaving a silently dead button.
356
371
  rejected: () => {
357
372
  this.element.setAttribute("data-live-counter-rejected", "true");
358
373
  this.#syncTriggers();
@@ -364,47 +379,33 @@ var LiveCounterController = class _LiveCounterController extends Controller {
364
379
  return null;
365
380
  }
366
381
  }
367
- /**
368
- * The identifier this declaration names.
369
- *
370
- * `channel` comes first so the identifier text matches what Action Cable
371
- * builds, and a `channel` key inside `params` cannot displace the declared
372
- * one — `params` names *additional* identifier parameters.
373
- */
374
382
  get #descriptor() {
375
- const { channel: _ignored, ...rest } = this.#params;
383
+ const { channel: _ignored, ...rest } = parseSubscriptionParams(this.paramsValue);
376
384
  return { channel: this.channelValue, ...rest };
377
385
  }
378
- /** Moves to the identifier the declaration now names. */
379
- #resubscribe() {
380
- if (!this.#connected) return;
386
+ #followDeclaration() {
387
+ if (identifierOf(this.#descriptor) === this.#identifier) return;
381
388
  this.#subscription?.unsubscribe();
382
389
  this.#subscription = null;
383
390
  this.#subscribe();
384
391
  }
385
392
  disconnect() {
386
- this.#connected = false;
393
+ this.#morphRender.disconnect();
394
+ this.#follow.cancel();
387
395
  this.#timers.clearAll();
388
396
  this.#focusedTriggers.releaseAll();
389
397
  this.#subscription?.unsubscribe();
390
398
  this.#subscription = null;
391
- this.element.removeAttribute("data-live-counter-rejected");
399
+ TRANSIENT.reset(this.element);
392
400
  for (const trigger of this.triggerTargets) this.#release(trigger);
393
401
  }
394
- /** Late-added triggers (e.g. via a Turbo Stream) pick up the current gate. */
395
402
  triggerTargetConnected(target) {
396
403
  this.#syncTrigger(target);
397
404
  }
398
- /** A trigger that stops being one keeps nothing this controller lent it. */
399
405
  triggerTargetDisconnected(target) {
400
406
  this.#focusedTriggers.release(target);
401
407
  this.#release(target);
402
408
  }
403
- /**
404
- * Optimistic local increment: bumps the display immediately, then asks the
405
- * server to persist and broadcast. The step comes from the action param
406
- * (`data-stimeo--live-counter-delta-param`), default 1. Bound via `data-action`.
407
- */
408
409
  increment(event) {
409
410
  if (!this.#ready) return;
410
411
  const raw = Number(event?.params?.delta ?? 1);
@@ -417,15 +418,6 @@ var LiveCounterController = class _LiveCounterController extends Controller {
417
418
  }
418
419
  this.#subscription?.perform("increment", { id: this.idValue, delta });
419
420
  }
420
- /**
421
- * Reconciles a broadcast: absolute `count` wins; the echo of this client's own
422
- * guess is skipped.
423
- *
424
- * Only whole numbers reach the display. The wire carries values another client
425
- * named and the server relayed, and the display is the state this controller
426
- * reads back — a fraction, an infinity or a NaN would be written as text that
427
- * reads back as a different number, or as none at all.
428
- */
429
421
  #onReceived(data) {
430
422
  const message = data;
431
423
  if (Number.isSafeInteger(message?.count)) {
@@ -444,30 +436,15 @@ var LiveCounterController = class _LiveCounterController extends Controller {
444
436
  this.#reconcile(this.#current + delta);
445
437
  }
446
438
  }
447
- /** True when no channel is declared: the local-only mode, with no server to diverge from. */
448
439
  get #local() {
449
440
  return this.channelValue === "";
450
441
  }
451
- /** True while an increment would go through (local-only counters always are). */
452
442
  get #ready() {
453
443
  return this.#local || (this.#subscription?.confirmed ?? false);
454
444
  }
455
- /**
456
- * Reflects the send gate onto the optional `trigger` targets as the real
457
- * `disabled` attribute — the declarative alternative to styling off the
458
- * `data-live-counter-rejected` hook (a disabled control is also skipped by
459
- * keyboard focus and announced by AT, which CSS alone cannot do).
460
- */
461
445
  #syncTriggers() {
462
446
  for (const trigger of this.triggerTargets) this.#syncTrigger(trigger);
463
447
  }
464
- /**
465
- * `disabled` is a shared attribute: only disable what is currently enabled
466
- * (marking it ours), and only lift a disabled carrying our marker — so an
467
- * authored-disabled trigger ("disabled until valid", say) is never
468
- * re-enabled by the gate. A marked disabled restored from a Turbo cache
469
- * snapshot is recognized as ours and lifted once the gate opens.
470
- */
471
448
  #syncTrigger(trigger) {
472
449
  if (this.#ready) {
473
450
  this.#focusedTriggers.release(trigger);
@@ -482,47 +459,26 @@ var LiveCounterController = class _LiveCounterController extends Controller {
482
459
  trigger.setAttribute("disabled", "");
483
460
  trigger.setAttribute(DISABLED_MARKER, "");
484
461
  }
485
- /** Gives back a `disabled` this controller lent, leaving an authored one alone. */
486
462
  #release(trigger) {
487
463
  if (!trigger.hasAttribute(DISABLED_MARKER)) return;
488
464
  trigger.removeAttribute("disabled");
489
465
  trigger.removeAttribute(DISABLED_MARKER);
490
466
  }
491
- /** The displayed element: the `value` target, else the controller element. */
492
467
  get #display() {
493
468
  return this.hasValueTarget ? this.valueTarget : this.element;
494
469
  }
495
- /** The current count, parsed from the DOM (the single source of truth). */
496
470
  get #current() {
497
471
  return authoredInteger(this.#display.textContent ?? "") ?? 0;
498
472
  }
499
- /**
500
- * Writes a whole count — every caller has already established that it is one —
501
- * and reports whether the display actually moved.
502
- */
503
473
  #write(count) {
504
474
  if (count === this.#current) return false;
505
475
  this.#display.textContent = String(count);
506
476
  this.dispatch("change", { detail: { count } });
507
477
  return true;
508
478
  }
509
- /**
510
- * Settles the display on a count the server stated, announcing what it settled
511
- * on. Announcing the optimistic bump instead would read a guess out and then
512
- * correct it, so the announcement rides this path only.
513
- */
514
479
  #reconcile(count) {
515
480
  if (this.#write(count)) this.#announce(count);
516
481
  }
517
- /**
518
- * Hands a reconciled count to the page's shared announcer.
519
- *
520
- * Debounced: a burst of broadcasts is one announcement, not one per message,
521
- * and only the last one is ever sent. Wording comes from the consumer and
522
- * {@link announce} drops an empty message, so an undeclared template announces
523
- * nothing at all — a count that ticks constantly should not interrupt a reader
524
- * unless the page asked for it.
525
- */
526
482
  #announce(count) {
527
483
  this.#timers.clearAll();
528
484
  this.#timers.set(() => {
@@ -531,16 +487,9 @@ var LiveCounterController = class _LiveCounterController extends Controller {
531
487
  }
532
488
  };
533
489
 
534
- // src/utils/keyed_timers.ts
535
490
  var KeyedTimers = class {
536
491
  #timers = new SafeTimeout();
537
- /** The pending timer of each key; an entry lives exactly as long as its timer. */
538
492
  #pending = /* @__PURE__ */ new Map();
539
- /**
540
- * Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had
541
- * pending. The entry is dropped before the callback runs, so the callback sees
542
- * the key unarmed and may arm it again for the next round.
543
- */
544
493
  set(key, callback, delay) {
545
494
  this.clear(key);
546
495
  const id = this.#timers.set(() => {
@@ -549,37 +498,80 @@ var KeyedTimers = class {
549
498
  }, delay);
550
499
  this.#pending.set(key, id);
551
500
  }
552
- /**
553
- * Cancels `key`'s pending timer, if it has one.
554
- *
555
- * A timer id is a positive integer, so `-1` stands for "nothing pending" and
556
- * the registry ignores an id it does not own — the unarmed case needs no
557
- * branch of its own, and no other key's timer can be reached from here.
558
- */
559
501
  clear(key) {
560
502
  this.#timers.clear(this.#pending.get(key) ?? -1);
561
503
  this.#pending.delete(key);
562
504
  }
563
- /**
564
- * Cancels every pending timer and forgets every key. Call this from a
565
- * controller's `disconnect()` so no timer, and no entry, outlives the element.
566
- */
567
505
  clearAll() {
568
506
  this.#timers.clearAll();
569
507
  this.#pending.clear();
570
508
  }
571
- /** Whether `key` has a timer pending. */
572
509
  has(key) {
573
510
  return this.#pending.has(key);
574
511
  }
575
512
  };
576
513
 
577
- // src/cable/presence_controller.ts
514
+ var NUMBER_BOUNDS = {
515
+ nonNegative: { finite: true, min: 0 },
516
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },
517
+ positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS }
518
+ };
519
+ function matchesNumberBounds(value, bounds) {
520
+ if (!Number.isFinite(value)) {
521
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
522
+ if (direction === null) return false;
523
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
524
+ }
525
+ if (bounds.min !== void 0 && value < bounds.min) return false;
526
+ if (bounds.max !== void 0 && value > bounds.max) return false;
527
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
528
+ if (bounds.integer && !Number.isInteger(value)) return false;
529
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
530
+ return true;
531
+ }
532
+
533
+ function readNumber(raw, fallback, bounds) {
534
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
535
+ }
536
+
537
+ var NumberValueReader = class {
538
+ #lastRejected = /* @__PURE__ */ new Map();
539
+ read(owner, name, raw, fallback, bounds) {
540
+ const resolved = readNumber(raw, fallback, bounds);
541
+ if (matchesNumberBounds(raw, bounds)) {
542
+ this.#lastRejected.delete(name);
543
+ return resolved;
544
+ }
545
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
546
+ const literal = owner.element.getAttribute(attribute);
547
+ if (literal === null) {
548
+ this.#lastRejected.delete(name);
549
+ return resolved;
550
+ }
551
+ if (this.#lastRejected.get(name) !== literal) {
552
+ this.#lastRejected.set(name, literal);
553
+ console.warn(
554
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
555
+ );
556
+ }
557
+ return resolved;
558
+ }
559
+ };
560
+
561
+ function cloneTemplateRoot(template) {
562
+ const root = template.content.firstElementChild;
563
+ return root instanceof HTMLElement ? root.cloneNode(true) : null;
564
+ }
565
+
566
+ var TRANSIENT2 = new TransientHooks({
567
+ attributes: ["data-present", "data-present-count", "data-presence-rejected"]
568
+ });
578
569
  var BEACON_THROTTLE_MS = 2e3;
579
570
  var DEFAULT_HEARTBEAT_MS = 15e3;
580
571
  var DEFAULT_TIMEOUT_MS = 4e4;
581
- var speakers = /* @__PURE__ */ new Map();
572
+ var speakers = sharedRegistry("stimeo-ui.presence.registry.v1", () => /* @__PURE__ */ new Map());
582
573
  var PresenceController = class _PresenceController extends Controller {
574
+ #numbers = new NumberValueReader();
583
575
  static targets = ["count", "list", "template"];
584
576
  static values = {
585
577
  channel: { type: String, default: "" },
@@ -591,170 +583,143 @@ var PresenceController = class _PresenceController extends Controller {
591
583
  announceJoinText: { type: String, default: "" },
592
584
  announceLeaveText: { type: String, default: "" }
593
585
  };
586
+ static valueConstraints = {
587
+ heartbeat: NUMBER_BOUNDS.positiveTimer,
588
+ timeout: NUMBER_BOUNDS.positiveTimer
589
+ };
594
590
  static events = ["join", "leave", "change"];
595
- /** Delay (ms) before one roster change is sent to the shared announcer. */
596
591
  static #announceDelay = 200;
597
- /** Identifier parameters parsed once from their declaration, never in the hot path. */
598
- #params = {};
599
592
  #subscription = null;
600
- /** Present peers keyed by id (insertion order = join order). */
593
+ #follow = new MicrotaskCoalescer(() => this.#followDeclaration());
594
+ #identifier = "";
595
+ #period = 0;
601
596
  #peers = /* @__PURE__ */ new Map();
602
- /** Each peer's auto-expiry timer, restarted by every beacon from that peer. */
603
597
  #expiry = new KeyedTimers();
604
598
  #timers = new SafeTimeout();
605
599
  #intervals = new SafeInterval();
606
- /** Epoch ms of the last outgoing beacon, for the convergence throttle. */
607
600
  #lastBeaconAt = 0;
608
- /** Pending trailing-edge convergence beacon (at most one queued). */
609
601
  #pendingBeacon = null;
610
- /** The one outstanding announcement, so a newer roster change supersedes it. */
611
602
  #announceId = null;
612
- /** The `speakers` key this element holds, recomputed on every `connect()`. */
603
+ #connected = false;
613
604
  #speakerKey = "";
614
- /**
615
- * Re-parses the identifier parameters when the declaration changes.
616
- *
617
- * A malformed declaration falls back to no parameters, so the identifier keeps
618
- * naming the channel instead of the subscription never being created at all.
619
- */
605
+ #speakerId = "";
606
+ channelValueChanged() {
607
+ this.#follow.schedule();
608
+ }
620
609
  paramsValueChanged() {
621
- this.#params = parseSubscriptionParams(this.paramsValue);
610
+ this.#follow.schedule();
611
+ }
612
+ idValueChanged() {
613
+ this.#follow.schedule();
614
+ }
615
+ heartbeatValueChanged() {
616
+ this.#follow.schedule();
622
617
  }
623
- /**
624
- * The identifier this declaration names. `params` *adds* to the identifier, so a
625
- * `channel` key inside it names an extra parameter and never replaces the channel
626
- * the element declares.
627
- */
628
618
  get #descriptor() {
629
- const { channel: _declared, ...rest } = this.#params;
619
+ const { channel: _declared, ...rest } = parseSubscriptionParams(this.paramsValue);
630
620
  return { channel: this.channelValue, ...rest };
631
621
  }
632
- /**
633
- * The beacon period, in ms: a finite, positive number a timer can hold. Anything
634
- * else names no interval — `setInterval` reads `NaN`, a negative value, `Infinity`
635
- * and a value past {@link MAX_TIMER_DELAY_MS} alike as "as often as possible", so
636
- * the declaration would flood the channel rather than heartbeat on it. Zero is out
637
- * for the same reason, which is why this bound is tighter than the non-negative one
638
- * a throttle or a plain delay can use. Such a declaration falls back to the default.
639
- */
640
622
  get #heartbeat() {
641
- const declared = this.heartbeatValue;
642
- return Number.isFinite(declared) && declared > 0 && declared <= MAX_TIMER_DELAY_MS ? declared : DEFAULT_HEARTBEAT_MS;
643
- }
644
- /**
645
- * The silence after which a peer is dropped, in ms: a finite, positive number a
646
- * timer can hold. Anything else names no delay and every peer expires in the task
647
- * that records it, so the roster could never hold anyone. Such a declaration falls
648
- * back to the default.
649
- */
623
+ return this.#safeHeartbeat;
624
+ }
650
625
  get #timeout() {
651
- const declared = this.timeoutValue;
652
- return Number.isFinite(declared) && declared > 0 && declared <= MAX_TIMER_DELAY_MS ? declared : DEFAULT_TIMEOUT_MS;
653
- }
654
- /**
655
- * Draws the roster into a list target, which arrives empty from a Turbo Stream
656
- * replacement or a morph. Stimulus runs this before `connect()` on the first mount
657
- * too, where the roster is still empty and clearing whatever a cache snapshot
658
- * preserved is what `connect()` goes on to do anyway.
659
- */
626
+ return this.#safeTimeout;
627
+ }
660
628
  listTargetConnected() {
661
- for (const child of this.listTarget.querySelectorAll("[data-presence-id]")) {
662
- child.remove();
663
- }
664
- for (const [id, peer] of this.#peers) this.#appendClone(id, peer.name);
629
+ this.#drawList();
630
+ }
631
+ listTargetDisconnected() {
632
+ if (this.#connected) this.#drawList();
665
633
  }
666
- /** Paints the roster size into a count target that arrives blank, for the same reason. */
667
634
  countTargetConnected() {
668
- this.countTarget.textContent = this.#countMessage(this.#peers.size);
635
+ this.#drawCount();
636
+ }
637
+ countTargetDisconnected() {
638
+ if (this.#connected) this.#drawCount();
669
639
  }
670
640
  connect() {
641
+ this.#connected = true;
642
+ this.#follow.activate();
643
+ this.#start();
644
+ }
645
+ disconnect() {
646
+ this.#connected = false;
647
+ this.#follow.cancel();
648
+ this.#close();
649
+ this.#reset();
650
+ }
651
+ #start() {
671
652
  this.#reset();
672
- this.element.removeAttribute("data-presence-rejected");
673
653
  if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(0);
654
+ this.#open();
655
+ }
656
+ #open() {
657
+ const descriptor = this.#descriptor;
658
+ this.#identifier = identifierOf(descriptor);
674
659
  if (!this.channelValue) return;
675
660
  this.#claimSpeaker();
676
- this.#subscription = createConfirmedSubscription(this.#descriptor, {
677
- // The first beacon must wait for the confirmed subscription — a
678
- // perform() before that is silently dropped by Action Cable. Fires
679
- // again on every reconnect, so the roster self-heals after an outage.
661
+ this.#subscription = createConfirmedSubscription(descriptor, {
680
662
  connected: () => this.#beacon(true),
681
- // The server refused the subscription: no beacon will ever go through,
682
- // and the hook lets the consumer's CSS reflect the dead stream.
683
663
  rejected: () => {
684
664
  this.element.setAttribute("data-presence-rejected", "true");
685
665
  },
686
666
  received: (data) => this.#onReceived(data)
687
667
  });
688
- this.#intervals.set(() => this.#beacon(true), this.#heartbeat);
668
+ this.#armHeartbeat();
689
669
  window.addEventListener("pagehide", this.#onPageHide);
690
670
  }
691
- disconnect() {
671
+ #close() {
692
672
  window.removeEventListener("pagehide", this.#onPageHide);
693
- if (this.#releaseSpeaker()) this.#sendLeaveNotice();
673
+ this.#sendLeaveNotice(this.#releaseSpeaker());
694
674
  this.#subscription?.unsubscribe();
695
675
  this.#subscription = null;
696
676
  this.#intervals.clearAll();
697
- this.#reset();
698
- this.element.removeAttribute("data-presence-rejected");
699
- }
700
- /**
701
- * Registers this element as one of the voices for its peer, so a sibling on the
702
- * same channel claiming the same `id` keeps the peer present until they all go.
703
- * Whether the peer is worth announcing at all is {@link #sendLeaveNotice}'s
704
- * question, so an element with no own `id` claims a voice like any other and is
705
- * simply never heard.
706
- */
677
+ }
678
+ #armHeartbeat() {
679
+ this.#intervals.clearAll();
680
+ this.#period = this.#heartbeat;
681
+ this.#intervals.set(() => this.#beacon(true), this.#period);
682
+ }
683
+ #followDeclaration() {
684
+ if (identifierOf(this.#descriptor) !== this.#identifier) {
685
+ this.#close();
686
+ this.#start();
687
+ } else if (this.#subscription) {
688
+ if (this.idValue !== this.#speakerId) this.#moveVoice();
689
+ if (this.#heartbeat !== this.#period) this.#armHeartbeat();
690
+ }
691
+ }
692
+ #moveVoice() {
693
+ this.#sendLeaveNotice(this.#releaseSpeaker());
694
+ this.#claimSpeaker();
695
+ this.#forget(this.#speakerId);
696
+ this.#timers.clear(this.#pendingBeacon ?? -1);
697
+ this.#pendingBeacon = null;
698
+ this.#beacon(true);
699
+ }
707
700
  #claimSpeaker() {
708
- this.#speakerKey = `${identifierOf(this.#descriptor)}
709
- ${this.idValue}`;
701
+ this.#speakerId = this.idValue;
702
+ this.#speakerKey = `${this.#identifier}
703
+ ${this.#speakerId}`;
710
704
  speakers.set(this.#speakerKey, (speakers.get(this.#speakerKey) ?? 0) + 1);
711
705
  }
712
- /**
713
- * Gives up the claim, reporting whether this element was the last voice for the
714
- * peer. The key is spent here: an element that goes on to connect without a
715
- * channel claims nothing, and a stale key would spend a voice its siblings own.
716
- */
717
706
  #releaseSpeaker() {
718
707
  const key = this.#speakerKey;
708
+ const id = this.#speakerId;
719
709
  this.#speakerKey = "";
710
+ this.#speakerId = "";
720
711
  const remaining = (speakers.get(key) ?? 1) - 1;
721
712
  if (remaining > 0) speakers.set(key, remaining);
722
713
  else speakers.delete(key);
723
- return remaining <= 0;
714
+ return remaining > 0 ? "" : id;
715
+ }
716
+ #sendLeaveNotice(id) {
717
+ if (!this.#subscription?.confirmed || !id) return;
718
+ this.#subscription.perform("appear", { id, leaving: true });
724
719
  }
725
- /**
726
- * Sends the best-effort leaving notice (skipped without an own `id`, and
727
- * outside the confirmed window, where Action Cable would discard it anyway).
728
- */
729
- #sendLeaveNotice() {
730
- if (!this.#subscription?.confirmed || !this.idValue) return;
731
- this.#subscription.perform("appear", { id: this.idValue, leaving: true });
732
- }
733
- /**
734
- * `pagehide` covers the leaves `disconnect()` cannot see: closing the tab or
735
- * a hard (non-Turbo) navigation destroys the page without running Stimulus
736
- * teardown, so this listener is the only chance to announce them. Best-effort
737
- * by nature (the socket may close before the frame flushes); peers' expiry
738
- * timers stay the safety net. If the page enters the bfcache and is restored
739
- * instead, the next heartbeat re-announces this client, so an over-eager
740
- * leave self-heals.
741
- */
742
720
  #onPageHide = () => {
743
- this.#sendLeaveNotice();
721
+ this.#sendLeaveNotice(this.#speakerId);
744
722
  };
745
- /**
746
- * Broadcasts this client's beacon (skipped without an own `id`). A throttled
747
- * convergence answer is deferred to the trailing edge rather than dropped —
748
- * otherwise a peer joining right after a heartbeat would not learn about
749
- * this client until the next full heartbeat period.
750
- *
751
- * Gated on the confirmed subscription: before confirmation and during an
752
- * outage Action Cable silently discards perform(), so a beacon sent then is
753
- * pure waste — worse, it would burn `#lastBeaconAt` and delay the next real
754
- * convergence answer by up to the throttle window. The heartbeat interval
755
- * keeps ticking regardless; `connected` re-fires on reconfirm and
756
- * force-beacons immediately, so a gated tick is never missed for long.
757
- */
758
723
  #beacon(force) {
759
724
  if (!this.#subscription?.confirmed || !this.idValue) return;
760
725
  const now = Date.now();
@@ -771,11 +736,6 @@ ${this.idValue}`;
771
736
  this.#lastBeaconAt = now;
772
737
  this.#subscription.perform("appear", { id: this.idValue, name: this.nameValue });
773
738
  }
774
- /**
775
- * Tracks a broadcast beacon: upserts the peer (restarting its expiry timer),
776
- * removes it on a `leaving` notice, and re-announces this client when the
777
- * peer was unknown (roster convergence — see the class doc).
778
- */
779
739
  #onReceived(data) {
780
740
  const beacon = data;
781
741
  const id = beacon?.id;
@@ -799,7 +759,6 @@ ${this.idValue}`;
799
759
  this.#render();
800
760
  }
801
761
  }
802
- /** Removes a peer (expiry or graceful leave) and reflects the change. */
803
762
  #drop(id) {
804
763
  const peer = this.#peers.get(id);
805
764
  if (peer === void 0) return;
@@ -810,23 +769,22 @@ ${this.idValue}`;
810
769
  this.#announce(this.announceLeaveTextValue, peer.name);
811
770
  this.dispatch("leave", { detail: { id } });
812
771
  }
813
- /** Reflects the roster onto the hooks + count target and emits `change`. */
772
+ #forget(id) {
773
+ if (!this.#peers.delete(id)) return;
774
+ this.#expiry.clear(id);
775
+ this.#removeClone(id);
776
+ this.#paint();
777
+ }
814
778
  #render() {
779
+ this.dispatch("change", { detail: { users: this.#paint() } });
780
+ }
781
+ #paint() {
815
782
  const users = [...this.#peers.entries()].map(([id, peer]) => ({ id, name: peer.name }));
816
783
  this.element.setAttribute("data-present", users.length > 0 ? "true" : "false");
817
784
  this.element.setAttribute("data-present-count", String(users.length));
818
785
  if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(users.length);
819
- this.dispatch("change", { detail: { users } });
820
- }
821
- /**
822
- * Hands one roster change to the page's shared announcer.
823
- *
824
- * Debounced: a burst of arrivals is one announcement, not one per peer — reading
825
- * out every entry and exit of a busy room is the noise the default avoids. Wording
826
- * comes from the consumer (`{name}` / `{count}`, where the count is the roster
827
- * after the change), and an undeclared template does nothing at all — not even
828
- * cancel an announcement already pending for the other direction.
829
- */
786
+ return users;
787
+ }
830
788
  #announce(template, name) {
831
789
  const message = fillTemplate(template, { name, count: this.#peers.size });
832
790
  if (message.trim() === "") return;
@@ -836,21 +794,25 @@ ${this.idValue}`;
836
794
  this.#announceId = null;
837
795
  }, _PresenceController.#announceDelay);
838
796
  }
839
- /**
840
- * Builds the count copy. Localizable through `data-zero` / `data-one` /
841
- * `data-other` templates on the count target (`%{count}`); the bare number is
842
- * the fallback (copy-free, so nothing to localize by default).
843
- */
844
797
  #countMessage(count) {
845
798
  const templates = this.countTarget.dataset;
846
799
  const template = (count === 0 ? templates.zero : count === 1 ? templates.one : templates.other) ?? templates.other;
847
800
  return template ? template.replace("%{count}", String(count)) : String(count);
848
801
  }
849
- /** Appends one template clone for a newly present peer (list + template only). */
802
+ #drawList() {
803
+ if (!this.hasListTarget) return;
804
+ for (const child of this.listTarget.querySelectorAll("[data-presence-id]")) {
805
+ child.remove();
806
+ }
807
+ for (const [id, peer] of this.#peers) this.#appendClone(id, peer.name);
808
+ }
809
+ #drawCount() {
810
+ if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(this.#peers.size);
811
+ }
850
812
  #appendClone(id, name) {
851
813
  if (!this.hasListTarget || !this.hasTemplateTarget) return;
852
- const root = this.templateTarget.content.firstElementChild?.cloneNode(true);
853
- if (!(root instanceof Element)) return;
814
+ const root = cloneTemplateRoot(this.templateTarget);
815
+ if (!root) return;
854
816
  root.setAttribute("data-presence-id", id);
855
817
  this.#fillName(root, name);
856
818
  this.listTarget.appendChild(root);
@@ -869,7 +831,6 @@ ${this.idValue}`;
869
831
  }
870
832
  return null;
871
833
  }
872
- /** Writes the peer's name into the clone's `data-presence-name` slots. */
873
834
  #fillName(root, name) {
874
835
  const slots = root.querySelectorAll("[data-presence-name]");
875
836
  for (const slot of slots) slot.textContent = name;
@@ -877,7 +838,6 @@ ${this.idValue}`;
877
838
  root.textContent = name;
878
839
  }
879
840
  }
880
- /** Clears the transient roster state (connect reset + disconnect teardown). */
881
841
  #reset() {
882
842
  this.#timers.clearAll();
883
843
  this.#expiry.clearAll();
@@ -885,8 +845,7 @@ ${this.idValue}`;
885
845
  this.#announceId = null;
886
846
  this.#peers.clear();
887
847
  this.#lastBeaconAt = 0;
888
- this.element.removeAttribute("data-present");
889
- this.element.removeAttribute("data-present-count");
848
+ TRANSIENT2.reset(this.element);
890
849
  if (this.hasCountTarget) this.countTarget.textContent = "";
891
850
  if (this.hasListTarget) {
892
851
  for (const child of this.listTarget.querySelectorAll("[data-presence-id]")) {
@@ -894,92 +853,129 @@ ${this.idValue}`;
894
853
  }
895
854
  }
896
855
  }
856
+ get #safeHeartbeat() {
857
+ return this.#numbers.read(
858
+ this,
859
+ "heartbeat",
860
+ this.heartbeatValue,
861
+ _PresenceController.values.heartbeat.default,
862
+ _PresenceController.valueConstraints.heartbeat
863
+ );
864
+ }
865
+ get #safeTimeout() {
866
+ return this.#numbers.read(
867
+ this,
868
+ "timeout",
869
+ this.timeoutValue,
870
+ _PresenceController.values.timeout.default,
871
+ _PresenceController.valueConstraints.timeout
872
+ );
873
+ }
897
874
  };
875
+ var TRANSIENT3 = new TransientHooks({
876
+ attributes: ["data-typing", "data-typing-indicator-rejected"]
877
+ });
898
878
  var DEFAULT_TIMEOUT = 3e3;
899
879
  var DEFAULT_THROTTLE = 2e3;
900
880
  var fillTokens = (template, values) => template.replace(/%\{(name|names|count)\}/g, (match, token) => values[token] ?? match);
901
881
  var TypingIndicatorController = class _TypingIndicatorController extends Controller {
882
+ #numbers = new NumberValueReader();
902
883
  static targets = ["input", "status"];
903
884
  static values = {
904
885
  channel: { type: String, default: "" },
905
886
  params: { type: String, default: "" },
906
887
  name: { type: String, default: "" },
907
888
  timeout: { type: Number, default: DEFAULT_TIMEOUT },
908
- // Must stay below `timeout`: the throttle is leading-edge only (no trailing
909
- // send), so a receiver's display survives continuous typing only while a
910
- // fresh signal lands within its timeout window.
911
889
  throttle: { type: Number, default: DEFAULT_THROTTLE },
912
890
  announceOneText: { type: String, default: "" },
913
891
  announceManyText: { type: String, default: "" }
914
892
  };
893
+ static valueConstraints = {
894
+ timeout: NUMBER_BOUNDS.timer,
895
+ throttle: NUMBER_BOUNDS.nonNegative
896
+ };
915
897
  static events = ["change"];
916
- /** Delay (ms) before one settled typer set is sent to the shared announcer. */
917
898
  static #announceDelay = 200;
918
- /** Identifier parameters parsed once from their declaration, never in the hot path. */
919
- #params = {};
920
899
  #subscription = null;
921
- /** Names currently typing (other clients), in the order their first signal arrived. */
900
+ #follow = new MicrotaskCoalescer(() => this.#followDeclaration());
901
+ #identifier = "";
922
902
  #typers = /* @__PURE__ */ new Set();
923
- /** Each typer's auto-clear timer, restarted by every further signal from that name. */
924
903
  #expiry = new KeyedTimers();
925
904
  #timers = new SafeTimeout();
926
- /** Epoch ms of the last broadcast, for leading-edge throttling. */
927
905
  #lastSentAt = 0;
928
- /** The one outstanding announcement, so a newer set supersedes it. */
929
906
  #announceId = null;
930
- /**
931
- * Re-parses the identifier parameters when the declaration changes.
932
- *
933
- * A malformed declaration falls back to no parameters, so the identifier keeps
934
- * naming the channel instead of the subscription never being created at all.
935
- */
907
+ #connected = false;
908
+ channelValueChanged() {
909
+ this.#follow.schedule();
910
+ }
936
911
  paramsValueChanged() {
937
- this.#params = parseSubscriptionParams(this.paramsValue);
938
- }
939
- /**
940
- * Paints the current copy into a `status` target that arrived at runtime.
941
- *
942
- * No event: the set of typers did not change, only the element that displays it.
943
- * Without this a region swapped in mid-conversation stays empty while `data-typing`
944
- * still says someone is typing, leaving the state in the visual hook alone. An empty
945
- * set needs no paint — a fresh region already shows it.
946
- */
912
+ this.#follow.schedule();
913
+ }
914
+ nameValueChanged() {
915
+ this.#follow.schedule();
916
+ }
917
+ get #descriptor() {
918
+ const { channel: _declared, ...rest } = parseSubscriptionParams(this.paramsValue);
919
+ return { channel: this.channelValue, ...rest };
920
+ }
947
921
  statusTargetConnected() {
948
- if (this.#typers.size > 0) this.#paint();
922
+ if (this.#connected) this.#paintStatus([...this.#typers]);
923
+ }
924
+ statusTargetDisconnected() {
925
+ if (this.#connected) this.#paintStatus([...this.#typers]);
949
926
  }
950
927
  connect() {
951
- this.element.removeAttribute("data-typing");
952
- this.element.removeAttribute("data-typing-indicator-rejected");
953
- if (this.hasStatusTarget) this.statusTarget.textContent = "";
928
+ this.#connected = true;
929
+ this.#reset();
954
930
  this.element.addEventListener("input", this.#onInput);
955
- if (this.channelValue) {
956
- this.#subscription = createConfirmedSubscription(
957
- { channel: this.channelValue, ...this.#params },
958
- {
959
- // The server refused the subscription: the send gate stays shut for
960
- // good, and the hook lets the consumer's CSS reflect the dead stream.
961
- rejected: () => {
962
- this.element.setAttribute("data-typing-indicator-rejected", "true");
963
- },
964
- received: (data) => this.#onReceived(data)
965
- }
966
- );
967
- }
931
+ this.#follow.activate();
932
+ this.#open();
968
933
  }
969
934
  disconnect() {
935
+ this.#connected = false;
936
+ this.#follow.cancel();
970
937
  this.element.removeEventListener("input", this.#onInput);
938
+ this.#close();
939
+ this.#reset();
940
+ }
941
+ #open() {
942
+ const descriptor = this.#descriptor;
943
+ this.#identifier = identifierOf(descriptor);
944
+ if (!this.channelValue) return;
945
+ this.#subscription = createConfirmedSubscription(descriptor, {
946
+ rejected: () => {
947
+ this.element.setAttribute("data-typing-indicator-rejected", "true");
948
+ },
949
+ received: (data) => this.#onReceived(data)
950
+ });
951
+ }
952
+ #close() {
971
953
  this.#subscription?.unsubscribe();
972
954
  this.#subscription = null;
955
+ }
956
+ #followDeclaration() {
957
+ if (identifierOf(this.#descriptor) !== this.#identifier) {
958
+ this.#close();
959
+ this.#reset();
960
+ this.#open();
961
+ } else {
962
+ this.#forget(this.nameValue);
963
+ }
964
+ }
965
+ #forget(name) {
966
+ if (!this.#typers.delete(name)) return;
967
+ this.#expiry.clear(name);
968
+ this.#paint();
969
+ }
970
+ #reset() {
973
971
  this.#timers.clearAll();
974
972
  this.#expiry.clearAll();
975
973
  this.#announceId = null;
976
974
  this.#typers.clear();
977
- this.element.removeAttribute("data-typing");
978
- this.element.removeAttribute("data-typing-indicator-rejected");
975
+ TRANSIENT3.reset(this.element);
979
976
  if (this.hasStatusTarget) this.statusTarget.textContent = "";
980
977
  this.#lastSentAt = 0;
981
978
  }
982
- /** Throttled (leading-edge) broadcast of this client's typing signal. */
983
979
  #onInput = () => {
984
980
  if (!this.#subscription?.confirmed) return;
985
981
  const now = Date.now();
@@ -987,11 +983,6 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
987
983
  this.#lastSentAt = now;
988
984
  this.#subscription.perform("typing", { name: this.nameValue });
989
985
  };
990
- /**
991
- * Tracks a broadcast typer. The own echo is dropped (same `name`); every
992
- * further signal from a name restarts its auto-clear timer, so the indicator
993
- * survives continuous typing and clears `timeout` ms after the last signal.
994
- */
995
986
  #onReceived(data) {
996
987
  const name = data?.name;
997
988
  if (typeof name !== "string" || name === "" || name === this.nameValue) return;
@@ -1000,48 +991,21 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1000
991
  this.#expiry.set(name, () => this.#untrack(name), this.#timeout);
1001
992
  if (added) this.#render();
1002
993
  }
1003
- /**
1004
- * The silence after which a typer is dropped, in ms: a finite, non-negative number
1005
- * a timer can hold. Anything else names no delay — `setTimeout` reads `NaN`, a
1006
- * negative value and `Infinity` alike as "now", and a value past
1007
- * {@link MAX_TIMER_DELAY_MS} overflows to the same place — so the typer would
1008
- * vanish in the same task it appeared and the indicator could never be seen. Such
1009
- * a declaration falls back to the default.
1010
- */
1011
994
  get #timeout() {
1012
- const declared = this.timeoutValue;
1013
- return Number.isFinite(declared) && declared >= 0 && declared <= MAX_TIMER_DELAY_MS ? declared : DEFAULT_TIMEOUT;
1014
- }
1015
- /**
1016
- * The minimum gap between outgoing signals, in ms: a finite, non-negative number.
1017
- * Anything else names no interval, and the gate then settles the same way at every
1018
- * keystroke — `NaN` and a negative gap leave it open, so every keystroke broadcasts,
1019
- * while `Infinity` is never exceeded, so nothing is ever sent. Such a declaration
1020
- * falls back to the default.
1021
- */
995
+ return this.#safeTimeout;
996
+ }
1022
997
  get #throttle() {
1023
- const declared = this.throttleValue;
1024
- return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_THROTTLE;
998
+ return this.#safeThrottle;
1025
999
  }
1026
1000
  #untrack(name) {
1027
1001
  this.#typers.delete(name);
1028
1002
  this.#render();
1029
1003
  }
1030
- /** Reflects the typer set onto the display, announces it, and emits `change`. */
1031
1004
  #render() {
1032
1005
  const names = this.#paint();
1033
1006
  this.#announce(names);
1034
1007
  this.dispatch("change", { detail: { names } });
1035
1008
  }
1036
- /**
1037
- * Hands the settled typer set to the page's shared announcer.
1038
- *
1039
- * Debounced: a burst of arrivals is one announcement, not one per name. Only a
1040
- * non-empty set is announced — that typing stopped is not news worth interrupting
1041
- * a reader for, and the visible copy already clears. Wording comes from the
1042
- * consumer, and {@link announce} drops an empty message, so an undeclared template
1043
- * announces nothing at all.
1044
- */
1045
1009
  #announce(names) {
1046
1010
  if (this.#announceId !== null) this.#timers.clear(this.#announceId);
1047
1011
  this.#announceId = null;
@@ -1055,20 +1019,15 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1055
1019
  this.#announceId = null;
1056
1020
  }, _TypingIndicatorController.#announceDelay);
1057
1021
  }
1058
- /** Writes the current typer set onto the hook and the status slot. */
1059
1022
  #paint() {
1060
1023
  const names = [...this.#typers];
1061
1024
  this.element.setAttribute("data-typing", names.length > 0 ? "true" : "false");
1062
- if (this.hasStatusTarget) {
1063
- this.statusTarget.textContent = this.#message(names);
1064
- }
1025
+ this.#paintStatus(names);
1065
1026
  return names;
1066
1027
  }
1067
- /**
1068
- * Builds the status copy. Localizable through `data-one` / `data-many`
1069
- * templates on the status target (`%{name}` / `%{names}` / `%{count}`); terse
1070
- * English is the fallback.
1071
- */
1028
+ #paintStatus(names) {
1029
+ if (this.hasStatusTarget) this.statusTarget.textContent = this.#message(names);
1030
+ }
1072
1031
  #message(names) {
1073
1032
  if (names.length === 0) return "";
1074
1033
  const joined = names.join(", ");
@@ -1080,9 +1039,26 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1080
1039
  const template = this.statusTarget.dataset.many;
1081
1040
  return template ? fillTokens(template, { names: joined, count: String(names.length) }) : `${joined} are typing\u2026`;
1082
1041
  }
1042
+ get #safeTimeout() {
1043
+ return this.#numbers.read(
1044
+ this,
1045
+ "timeout",
1046
+ this.timeoutValue,
1047
+ _TypingIndicatorController.values.timeout.default,
1048
+ _TypingIndicatorController.valueConstraints.timeout
1049
+ );
1050
+ }
1051
+ get #safeThrottle() {
1052
+ return this.#numbers.read(
1053
+ this,
1054
+ "throttle",
1055
+ this.throttleValue,
1056
+ _TypingIndicatorController.values.throttle.default,
1057
+ _TypingIndicatorController.valueConstraints.throttle
1058
+ );
1059
+ }
1083
1060
  };
1084
1061
 
1085
- // src/cable/index.ts
1086
1062
  var cableControllers = {
1087
1063
  "stimeo--live-counter": LiveCounterController,
1088
1064
  "stimeo--presence": PresenceController,
@@ -1095,5 +1071,4 @@ function registerCable(application) {
1095
1071
  }
1096
1072
 
1097
1073
  export { LiveCounterController, PresenceController, TypingIndicatorController, cableControllers, createConfirmedSubscription, getCableConsumer, registerCable, setCableConsumer };
1098
- //# sourceMappingURL=index.js.map
1099
1074
  //# sourceMappingURL=index.js.map