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