stimeo-ui 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +27 -11
  3. package/dist/cable/index.d.ts +88 -31
  4. package/dist/cable/index.js +384 -449
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/coerce-JtZVNG5h.d.ts +20 -0
  7. package/dist/controllers/accordion_controller.d.ts +4 -2
  8. package/dist/controllers/accordion_controller.js +174 -55
  9. package/dist/controllers/accordion_controller.js.map +1 -1
  10. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  11. package/dist/controllers/alert_dialog_controller.js +903 -235
  12. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  13. package/dist/controllers/announcer_controller.d.ts +37 -5
  14. package/dist/controllers/announcer_controller.js +238 -205
  15. package/dist/controllers/announcer_controller.js.map +1 -1
  16. package/dist/controllers/aspect_ratio_controller.js +0 -10
  17. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  18. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  19. package/dist/controllers/auto_submit_controller.js +414 -145
  20. package/dist/controllers/auto_submit_controller.js.map +1 -1
  21. package/dist/controllers/avatar_controller.js +185 -107
  22. package/dist/controllers/avatar_controller.js.map +1 -1
  23. package/dist/controllers/breadcrumb_controller.js +0 -130
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +21 -3
  26. package/dist/controllers/bulk_select_controller.js +282 -58
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +38 -16
  29. package/dist/controllers/calendar_controller.js +228 -260
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +43 -12
  32. package/dist/controllers/carousel_controller.js +536 -436
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +44 -7
  35. package/dist/controllers/character_counter_controller.js +493 -207
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +38 -74
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +16 -5
  41. package/dist/controllers/clipboard_controller.js +261 -149
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +17 -5
  44. package/dist/controllers/collapsible_controller.js +304 -120
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +18 -6
  47. package/dist/controllers/color_picker_controller.js +178 -146
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +24 -6
  50. package/dist/controllers/combobox_controller.js +522 -156
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +51 -20
  53. package/dist/controllers/command_palette_controller.js +1156 -395
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +300 -171
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +980 -262
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +80 -18
  62. package/dist/controllers/context_menu_controller.js +346 -153
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +47 -3
  68. package/dist/controllers/countdown_controller.js +281 -158
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -10
  71. package/dist/controllers/currency_input_controller.js +575 -225
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +25 -12
  74. package/dist/controllers/data_grid_controller.js +248 -214
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +42 -14
  77. package/dist/controllers/date_range_picker_controller.js +338 -235
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +17 -6
  80. package/dist/controllers/dialog_controller.js +899 -237
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +25 -8
  83. package/dist/controllers/direct_upload_controller.js +97 -167
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +6 -1
  86. package/dist/controllers/dirty_form_controller.js +72 -69
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +196 -24
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +39 -11
  92. package/dist/controllers/drawer_controller.js +974 -355
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +31 -1
  95. package/dist/controllers/dropdown_controller.js +339 -96
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +315 -88
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +79 -58
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +63 -18
  103. package/dist/controllers/file_dropzone_controller.js +322 -211
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.js +77 -42
  106. package/dist/controllers/filter_controller.js.map +1 -1
  107. package/dist/controllers/flash_controller.d.ts +35 -12
  108. package/dist/controllers/flash_controller.js +238 -347
  109. package/dist/controllers/flash_controller.js.map +1 -1
  110. package/dist/controllers/focus_controller.d.ts +15 -6
  111. package/dist/controllers/focus_controller.js +679 -240
  112. package/dist/controllers/focus_controller.js.map +1 -1
  113. package/dist/controllers/form_field_controller.js +156 -132
  114. package/dist/controllers/form_field_controller.js.map +1 -1
  115. package/dist/controllers/form_validation_controller.d.ts +3 -3
  116. package/dist/controllers/form_validation_controller.js +31 -97
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +47 -14
  119. package/dist/controllers/frame_loading_controller.js +275 -217
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +27 -6
  122. package/dist/controllers/highlight_controller.js +107 -75
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +60 -4
  125. package/dist/controllers/hover_card_controller.js +327 -130
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +48 -6
  128. package/dist/controllers/idle_controller.js +271 -101
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +30 -13
  131. package/dist/controllers/input_mask_controller.js +483 -97
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +150 -106
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +19 -2
  137. package/dist/controllers/lazy_frame_controller.js +24 -68
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +44 -9
  140. package/dist/controllers/listbox_controller.js +523 -240
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +9 -7
  143. package/dist/controllers/local_time_controller.js +102 -77
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +80 -96
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +55 -11
  149. package/dist/controllers/menu_controller.js +302 -188
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +51 -9
  152. package/dist/controllers/menubar_controller.js +232 -345
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +33 -20
  155. package/dist/controllers/meter_controller.js +417 -87
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +48 -18
  158. package/dist/controllers/multi_select_controller.js +785 -393
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +27 -6
  161. package/dist/controllers/navigation_menu_controller.js +190 -250
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +396 -192
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +268 -62
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +50 -8
  170. package/dist/controllers/number_input_controller.js +610 -303
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.d.ts +8 -5
  173. package/dist/controllers/optimistic_controller.js +45 -87
  174. package/dist/controllers/optimistic_controller.js.map +1 -1
  175. package/dist/controllers/otp_controller.d.ts +55 -8
  176. package/dist/controllers/otp_controller.js +658 -255
  177. package/dist/controllers/otp_controller.js.map +1 -1
  178. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  179. package/dist/controllers/overflow_indicator_controller.js +122 -113
  180. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  181. package/dist/controllers/overflow_menu_controller.d.ts +58 -20
  182. package/dist/controllers/overflow_menu_controller.js +470 -295
  183. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  184. package/dist/controllers/pagination_controller.d.ts +37 -7
  185. package/dist/controllers/pagination_controller.js +332 -123
  186. package/dist/controllers/pagination_controller.js.map +1 -1
  187. package/dist/controllers/password_reveal_controller.d.ts +46 -8
  188. package/dist/controllers/password_reveal_controller.js +392 -121
  189. package/dist/controllers/password_reveal_controller.js.map +1 -1
  190. package/dist/controllers/password_strength_controller.d.ts +15 -6
  191. package/dist/controllers/password_strength_controller.js +111 -158
  192. package/dist/controllers/password_strength_controller.js.map +1 -1
  193. package/dist/controllers/persist_controller.d.ts +11 -4
  194. package/dist/controllers/persist_controller.js +165 -111
  195. package/dist/controllers/persist_controller.js.map +1 -1
  196. package/dist/controllers/pointer_drag_controller.d.ts +35 -12
  197. package/dist/controllers/pointer_drag_controller.js +434 -188
  198. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  199. package/dist/controllers/popover_controller.d.ts +41 -5
  200. package/dist/controllers/popover_controller.js +269 -113
  201. package/dist/controllers/popover_controller.js.map +1 -1
  202. package/dist/controllers/portal_controller.d.ts +44 -4
  203. package/dist/controllers/portal_controller.js +215 -76
  204. package/dist/controllers/portal_controller.js.map +1 -1
  205. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  206. package/dist/controllers/preview_guard_controller.js +243 -116
  207. package/dist/controllers/preview_guard_controller.js.map +1 -1
  208. package/dist/controllers/progress_controller.d.ts +20 -19
  209. package/dist/controllers/progress_controller.js +391 -78
  210. package/dist/controllers/progress_controller.js.map +1 -1
  211. package/dist/controllers/radio_group_controller.d.ts +21 -9
  212. package/dist/controllers/radio_group_controller.js +278 -211
  213. package/dist/controllers/radio_group_controller.js.map +1 -1
  214. package/dist/controllers/range_slider_controller.d.ts +47 -6
  215. package/dist/controllers/range_slider_controller.js +220 -113
  216. package/dist/controllers/range_slider_controller.js.map +1 -1
  217. package/dist/controllers/rating_controller.d.ts +20 -4
  218. package/dist/controllers/rating_controller.js +458 -211
  219. package/dist/controllers/rating_controller.js.map +1 -1
  220. package/dist/controllers/read_more_controller.d.ts +2 -0
  221. package/dist/controllers/read_more_controller.js +138 -63
  222. package/dist/controllers/read_more_controller.js.map +1 -1
  223. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  224. package/dist/controllers/reading_progress_controller.js +166 -137
  225. package/dist/controllers/reading_progress_controller.js.map +1 -1
  226. package/dist/controllers/relative_time_controller.d.ts +20 -6
  227. package/dist/controllers/relative_time_controller.js +211 -112
  228. package/dist/controllers/relative_time_controller.js.map +1 -1
  229. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  230. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  231. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  232. package/dist/controllers/resizable_controller.d.ts +31 -2
  233. package/dist/controllers/resizable_controller.js +299 -150
  234. package/dist/controllers/resizable_controller.js.map +1 -1
  235. package/dist/controllers/roving_controller.d.ts +3 -3
  236. package/dist/controllers/roving_controller.js +49 -88
  237. package/dist/controllers/roving_controller.js.map +1 -1
  238. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  239. package/dist/controllers/scroll_area_controller.js +490 -246
  240. package/dist/controllers/scroll_area_controller.js.map +1 -1
  241. package/dist/controllers/scroll_restore_controller.d.ts +3 -3
  242. package/dist/controllers/scroll_restore_controller.js +14 -55
  243. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  244. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  245. package/dist/controllers/scroll_visibility_controller.js +432 -149
  246. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  247. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  248. package/dist/controllers/scrollspy_controller.js +193 -248
  249. package/dist/controllers/scrollspy_controller.js.map +1 -1
  250. package/dist/controllers/separator_controller.d.ts +20 -5
  251. package/dist/controllers/separator_controller.js +285 -142
  252. package/dist/controllers/separator_controller.js.map +1 -1
  253. package/dist/controllers/sidebar_controller.d.ts +65 -8
  254. package/dist/controllers/sidebar_controller.js +1069 -404
  255. package/dist/controllers/sidebar_controller.js.map +1 -1
  256. package/dist/controllers/skeleton_controller.d.ts +23 -0
  257. package/dist/controllers/skeleton_controller.js +259 -101
  258. package/dist/controllers/skeleton_controller.js.map +1 -1
  259. package/dist/controllers/slider_controller.d.ts +29 -2
  260. package/dist/controllers/slider_controller.js +187 -86
  261. package/dist/controllers/slider_controller.js.map +1 -1
  262. package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
  263. package/dist/controllers/smart_sticky_header_controller.js +173 -66
  264. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  265. package/dist/controllers/sortable_controller.d.ts +4 -4
  266. package/dist/controllers/sortable_controller.js +26 -125
  267. package/dist/controllers/sortable_controller.js.map +1 -1
  268. package/dist/controllers/spinner_controller.d.ts +39 -4
  269. package/dist/controllers/spinner_controller.js +288 -173
  270. package/dist/controllers/spinner_controller.js.map +1 -1
  271. package/dist/controllers/step_indicator_controller.d.ts +9 -4
  272. package/dist/controllers/step_indicator_controller.js +105 -46
  273. package/dist/controllers/step_indicator_controller.js.map +1 -1
  274. package/dist/controllers/stepper_controller.d.ts +14 -6
  275. package/dist/controllers/stepper_controller.js +120 -73
  276. package/dist/controllers/stepper_controller.js.map +1 -1
  277. package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
  278. package/dist/controllers/stick_to_bottom_controller.js +275 -140
  279. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  280. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  281. package/dist/controllers/sticky_observer_controller.js +220 -49
  282. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  283. package/dist/controllers/submit_once_controller.d.ts +24 -8
  284. package/dist/controllers/submit_once_controller.js +376 -227
  285. package/dist/controllers/submit_once_controller.js.map +1 -1
  286. package/dist/controllers/switch_controller.d.ts +18 -4
  287. package/dist/controllers/switch_controller.js +201 -84
  288. package/dist/controllers/switch_controller.js.map +1 -1
  289. package/dist/controllers/tabs_controller.d.ts +9 -6
  290. package/dist/controllers/tabs_controller.js +41 -30
  291. package/dist/controllers/tabs_controller.js.map +1 -1
  292. package/dist/controllers/tags_input_controller.d.ts +16 -3
  293. package/dist/controllers/tags_input_controller.js +293 -173
  294. package/dist/controllers/tags_input_controller.js.map +1 -1
  295. package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
  296. package/dist/controllers/textarea_autosize_controller.js +108 -74
  297. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  298. package/dist/controllers/theme_controller.d.ts +24 -11
  299. package/dist/controllers/theme_controller.js +140 -116
  300. package/dist/controllers/theme_controller.js.map +1 -1
  301. package/dist/controllers/time_picker_controller.d.ts +55 -5
  302. package/dist/controllers/time_picker_controller.js +331 -127
  303. package/dist/controllers/time_picker_controller.js.map +1 -1
  304. package/dist/controllers/toast_controller.d.ts +40 -16
  305. package/dist/controllers/toast_controller.js +244 -324
  306. package/dist/controllers/toast_controller.js.map +1 -1
  307. package/dist/controllers/toggle_group_controller.d.ts +14 -6
  308. package/dist/controllers/toggle_group_controller.js +252 -191
  309. package/dist/controllers/toggle_group_controller.js.map +1 -1
  310. package/dist/controllers/toolbar_controller.d.ts +8 -6
  311. package/dist/controllers/toolbar_controller.js +219 -120
  312. package/dist/controllers/toolbar_controller.js.map +1 -1
  313. package/dist/controllers/tooltip_controller.d.ts +49 -4
  314. package/dist/controllers/tooltip_controller.js +158 -124
  315. package/dist/controllers/tooltip_controller.js.map +1 -1
  316. package/dist/controllers/transition_controller.d.ts +22 -4
  317. package/dist/controllers/transition_controller.js +123 -119
  318. package/dist/controllers/transition_controller.js.map +1 -1
  319. package/dist/controllers/tree_view_controller.d.ts +22 -12
  320. package/dist/controllers/tree_view_controller.js +283 -369
  321. package/dist/controllers/tree_view_controller.js.map +1 -1
  322. package/dist/index.d.ts +10 -5
  323. package/dist/index.js +8573 -10410
  324. package/dist/index.js.map +1 -1
  325. package/dist/inspector/cli.d.ts +38 -14
  326. package/dist/inspector/cli.js +108 -40
  327. package/dist/inspector/cli.js.map +1 -1
  328. package/dist/inspector/cli_bin.js +145 -64
  329. package/dist/inspector/cli_bin.js.map +1 -1
  330. package/dist/inspector/examples.json +20 -20
  331. package/dist/inspector/manifest.json +802 -101
  332. package/dist/positioning/index.d.ts +15 -7
  333. package/dist/positioning/index.js +154 -47
  334. package/dist/positioning/index.js.map +1 -1
  335. package/package.json +11 -10
  336. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  337. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,9 +1,13 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
  import { createConsumer } from '@rails/actioncable';
3
3
 
4
- // src/cable/live_counter_controller.ts
4
+ function fillTemplate(template, values) {
5
+ return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
6
+ const replacement = Object.hasOwn(values, name) ? values[name] : void 0;
7
+ return replacement === void 0 ? match : String(replacement);
8
+ });
9
+ }
5
10
 
6
- // src/utils/announce.ts
7
11
  function announce(message, options = {}) {
8
12
  const text = message.trim();
9
13
  if (text.length === 0) return;
@@ -13,14 +17,7 @@ function announce(message, options = {}) {
13
17
  })
14
18
  );
15
19
  }
16
- function fillTemplate(template, values) {
17
- return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
18
- const replacement = values[name];
19
- return replacement === void 0 ? match : String(replacement);
20
- });
21
- }
22
20
 
23
- // src/utils/authored_integer.ts
24
21
  var TOKEN_BODY = /\d[\d.,'’’    _]*\d|\d/;
25
22
  var SIGN_OPENER = /[\s([]/;
26
23
  function authoredInteger(text) {
@@ -42,29 +39,21 @@ function authoredInteger(text) {
42
39
  return signed ? -magnitude || 0 : magnitude;
43
40
  }
44
41
 
45
- // src/utils/blur_deferral.ts
46
42
  var BlurDeferral = class {
47
- /** Elements currently holding an update back, mapped to their `blur` listener. */
48
43
  #pending = /* @__PURE__ */ new Map();
49
- /** Called after a pending element blurs and has been detached. */
50
44
  #onRelease;
51
- /** @param onRelease - Invoked once `element` actually blurs; never on `release`. */
52
45
  constructor(onRelease) {
53
46
  this.#onRelease = onRelease;
54
47
  }
55
- /** Number of elements currently holding an update back. */
56
48
  get size() {
57
49
  return this.#pending.size;
58
50
  }
59
- /** Snapshot of the pending elements, safe to iterate while releasing them. */
60
51
  get elements() {
61
52
  return [...this.#pending.keys()];
62
53
  }
63
- /** Whether `element` is currently holding an update back. */
64
54
  has(element) {
65
55
  return this.#pending.has(element);
66
56
  }
67
- /** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */
68
57
  defer(element) {
69
58
  if (this.#pending.has(element)) return;
70
59
  const onBlur = () => {
@@ -74,22 +63,18 @@ var BlurDeferral = class {
74
63
  this.#pending.set(element, onBlur);
75
64
  element.addEventListener("blur", onBlur);
76
65
  }
77
- /** Defers `element` as the only pending entry, cancelling any others. */
78
66
  deferOnly(element) {
79
67
  for (const pending of this.elements) {
80
68
  if (pending !== element) this.#detach(pending);
81
69
  }
82
70
  this.defer(element);
83
71
  }
84
- /** Cancels `element`'s deferral without completing it; no-ops when not pending. */
85
72
  release(element) {
86
73
  this.#detach(element);
87
74
  }
88
- /** Cancels every deferral without completing any of them. */
89
75
  releaseAll() {
90
76
  for (const element of this.elements) this.#detach(element);
91
77
  }
92
- /** Removes the `blur` listener for `element` and forgets it. */
93
78
  #detach(element) {
94
79
  const onBlur = this.#pending.get(element);
95
80
  if (onBlur) element.removeEventListener("blur", onBlur);
@@ -97,44 +82,96 @@ var BlurDeferral = class {
97
82
  }
98
83
  };
99
84
 
100
- // src/utils/safe_timeout.ts
85
+ var MicrotaskCoalescer = class {
86
+ #run;
87
+ #queued = false;
88
+ #active = false;
89
+ #generation = 0;
90
+ constructor(run) {
91
+ this.#run = run;
92
+ }
93
+ activate() {
94
+ this.#active = true;
95
+ }
96
+ cancel() {
97
+ this.#active = false;
98
+ this.#queued = false;
99
+ this.#generation += 1;
100
+ }
101
+ get pending() {
102
+ return this.#queued;
103
+ }
104
+ schedule() {
105
+ if (!this.#active || this.#queued) return;
106
+ this.#queued = true;
107
+ const generation = this.#generation;
108
+ queueMicrotask(() => {
109
+ if (generation !== this.#generation || !this.#queued || !this.#active) return;
110
+ this.#queued = false;
111
+ this.#run();
112
+ });
113
+ }
114
+ };
115
+
116
+ var ListenerSet = class {
117
+ #abort = new AbortController();
118
+ add(target, type, handler, options) {
119
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
120
+ }
121
+ dispose() {
122
+ this.#abort.abort();
123
+ this.#abort = new AbortController();
124
+ }
125
+ };
126
+
127
+ var MorphRenderWatcher = class {
128
+ #listeners = new ListenerSet();
129
+ #pass;
130
+ #element = null;
131
+ #onMorph = () => this.#pass.schedule();
132
+ constructor(run) {
133
+ this.#pass = new MicrotaskCoalescer(run);
134
+ }
135
+ observe(element) {
136
+ if (this.#element === element) return;
137
+ this.disconnect();
138
+ this.#element = element;
139
+ this.#pass.activate();
140
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
141
+ }
142
+ schedule() {
143
+ this.#pass.schedule();
144
+ }
145
+ get pending() {
146
+ return this.#pass.pending;
147
+ }
148
+ disconnect() {
149
+ this.#listeners.dispose();
150
+ this.#pass.cancel();
151
+ this.#element = null;
152
+ }
153
+ };
154
+
101
155
  var MAX_TIMER_DELAY_MS = 2147483647;
156
+
102
157
  var TimerRegistry = class {
103
- /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
104
158
  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
159
  clear(id) {
112
160
  if (this.ids.delete(id)) {
113
161
  this.cancel(id);
114
162
  }
115
163
  }
116
- /**
117
- * Cancels every tracked timer. Call this from a controller's `disconnect()`
118
- * to guarantee no timer outlives the element.
119
- */
120
164
  clearAll() {
121
165
  for (const id of this.ids) {
122
166
  this.cancel(id);
123
167
  }
124
168
  this.ids.clear();
125
169
  }
126
- /** Number of timers currently tracked (pending). */
127
170
  get size() {
128
171
  return this.ids.size;
129
172
  }
130
173
  };
131
174
  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
175
  set(callback, delay) {
139
176
  const id = this.schedule(() => {
140
177
  this.ids.delete(id);
@@ -151,7 +188,6 @@ var SafeTimeout = class extends TimerRegistry {
151
188
  }
152
189
  };
153
190
  var SafeInterval = class extends TimerRegistry {
154
- /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
155
191
  set(callback, delay) {
156
192
  const id = this.schedule(callback, delay);
157
193
  this.ids.add(id);
@@ -165,17 +201,26 @@ var SafeInterval = class extends TimerRegistry {
165
201
  }
166
202
  };
167
203
 
168
- // src/utils/transient_hooks.ts
169
204
  var TransientHooks = class {
170
205
  #attributes;
171
206
  constructor(options) {
172
207
  this.#attributes = options.attributes;
173
208
  }
174
- /** Drops the declared hooks from `element`; what it carries besides stays. */
175
209
  reset(element) {
176
210
  for (const attribute of this.#attributes) element.removeAttribute(attribute);
177
211
  }
178
212
  };
213
+
214
+ function sharedRegistry(key, create) {
215
+ const symbol = Symbol.for(key);
216
+ const scope = globalThis;
217
+ const existing = scope[symbol];
218
+ if (existing !== void 0) return existing;
219
+ const registry = create();
220
+ Object.defineProperty(globalThis, symbol, { value: registry });
221
+ return registry;
222
+ }
223
+
179
224
  function parseSubscriptionParams(raw) {
180
225
  try {
181
226
  const parsed = JSON.parse(raw);
@@ -185,15 +230,21 @@ function parseSubscriptionParams(raw) {
185
230
  return {};
186
231
  }
187
232
  }
188
- var sharedConsumer = null;
233
+ var consumerRegistry = sharedRegistry(
234
+ "stimeo-ui.cable-consumer.registry.v1",
235
+ () => ({ consumer: null })
236
+ );
189
237
  function setCableConsumer(consumer) {
190
- sharedConsumer = consumer;
238
+ consumerRegistry.consumer = consumer;
191
239
  }
192
240
  function getCableConsumer() {
193
- if (!sharedConsumer) sharedConsumer = createConsumer();
194
- return sharedConsumer;
241
+ if (!consumerRegistry.consumer) consumerRegistry.consumer = createConsumer();
242
+ return consumerRegistry.consumer;
195
243
  }
196
- var sharedSubscriptions = /* @__PURE__ */ new WeakMap();
244
+ var sharedSubscriptions = sharedRegistry(
245
+ "stimeo-ui.cable-subscriptions.registry.v1",
246
+ () => /* @__PURE__ */ new WeakMap()
247
+ );
197
248
  function identifierOf(channel) {
198
249
  return JSON.stringify(typeof channel === "string" ? { channel } : channel);
199
250
  }
@@ -257,9 +308,6 @@ function openSharedSubscription(consumer, channel) {
257
308
  confirmed: false,
258
309
  rejected: false,
259
310
  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
311
  connected: () => {
264
312
  if (shared.rejected) return;
265
313
  shared.confirmed = true;
@@ -281,7 +329,6 @@ function openSharedSubscription(consumer, channel) {
281
329
  return shared;
282
330
  }
283
331
 
284
- // src/cable/live_counter_controller.ts
285
332
  var TRANSIENT = new TransientHooks({ attributes: ["data-live-counter-rejected"] });
286
333
  var DISABLED_MARKER = "data-live-counter-disabled";
287
334
  var LiveCounterController = class _LiveCounterController extends Controller {
@@ -292,86 +339,42 @@ var LiveCounterController = class _LiveCounterController extends Controller {
292
339
  id: { type: String, default: "" },
293
340
  announceText: { type: String, default: "" }
294
341
  };
295
- /** Collapses a burst of broadcasts into one announcement. */
296
342
  static #announceDelay = 200;
297
343
  static actions = ["increment"];
298
344
  static events = ["change"];
299
- /** Identifier parameters parsed once from their declaration, never in the hot path. */
300
- #params = {};
301
345
  #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
- */
346
+ #follow = new MicrotaskCoalescer(() => this.#followDeclaration());
347
+ #identifier = "";
348
+ #morphRender = new MorphRenderWatcher(() => this.#syncTriggers());
310
349
  #outstanding = [];
311
- /** Holds a disable back while the trigger owns focus, so focus is never taken away. */
312
350
  #focusedTriggers = new BlurDeferral((trigger) => this.#syncTrigger(trigger));
313
351
  #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
352
  paramsValueChanged() {
322
- this.#params = parseSubscriptionParams(this.paramsValue);
323
- this.#resubscribe();
353
+ this.#follow.schedule();
324
354
  }
325
- /** Moves the subscription when the declaration names a different channel. */
326
355
  channelValueChanged() {
327
- this.#resubscribe();
356
+ this.#follow.schedule();
328
357
  }
329
358
  connect() {
330
- this.#connected = true;
359
+ this.#morphRender.observe(this.element);
360
+ this.#follow.activate();
331
361
  this.#subscribe();
332
362
  }
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
363
  #subscribe() {
344
364
  TRANSIENT.reset(this.element);
345
365
  this.#outstanding.length = 0;
366
+ const descriptor = this.#descriptor;
367
+ this.#identifier = identifierOf(descriptor);
346
368
  if (this.channelValue) {
347
- this.#subscription = this.#open();
369
+ this.#subscription = this.#open(descriptor);
348
370
  }
349
371
  this.#syncTriggers();
350
372
  }
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() {
373
+ #open(descriptor) {
364
374
  try {
365
- return createConfirmedSubscription(this.#descriptor, {
375
+ return createConfirmedSubscription(descriptor, {
366
376
  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
377
  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
378
  rejected: () => {
376
379
  this.element.setAttribute("data-live-counter-rejected", "true");
377
380
  this.#syncTriggers();
@@ -383,26 +386,19 @@ var LiveCounterController = class _LiveCounterController extends Controller {
383
386
  return null;
384
387
  }
385
388
  }
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
389
  get #descriptor() {
394
- const { channel: _ignored, ...rest } = this.#params;
390
+ const { channel: _ignored, ...rest } = parseSubscriptionParams(this.paramsValue);
395
391
  return { channel: this.channelValue, ...rest };
396
392
  }
397
- /** Moves to the identifier the declaration now names. */
398
- #resubscribe() {
399
- if (!this.#connected) return;
393
+ #followDeclaration() {
394
+ if (identifierOf(this.#descriptor) === this.#identifier) return;
400
395
  this.#subscription?.unsubscribe();
401
396
  this.#subscription = null;
402
397
  this.#subscribe();
403
398
  }
404
399
  disconnect() {
405
- this.#connected = false;
400
+ this.#morphRender.disconnect();
401
+ this.#follow.cancel();
406
402
  this.#timers.clearAll();
407
403
  this.#focusedTriggers.releaseAll();
408
404
  this.#subscription?.unsubscribe();
@@ -410,20 +406,13 @@ var LiveCounterController = class _LiveCounterController extends Controller {
410
406
  TRANSIENT.reset(this.element);
411
407
  for (const trigger of this.triggerTargets) this.#release(trigger);
412
408
  }
413
- /** Late-added triggers (e.g. via a Turbo Stream) pick up the current gate. */
414
409
  triggerTargetConnected(target) {
415
410
  this.#syncTrigger(target);
416
411
  }
417
- /** A trigger that stops being one keeps nothing this controller lent it. */
418
412
  triggerTargetDisconnected(target) {
419
413
  this.#focusedTriggers.release(target);
420
414
  this.#release(target);
421
415
  }
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
416
  increment(event) {
428
417
  if (!this.#ready) return;
429
418
  const raw = Number(event?.params?.delta ?? 1);
@@ -436,15 +425,6 @@ var LiveCounterController = class _LiveCounterController extends Controller {
436
425
  }
437
426
  this.#subscription?.perform("increment", { id: this.idValue, delta });
438
427
  }
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
428
  #onReceived(data) {
449
429
  const message = data;
450
430
  if (Number.isSafeInteger(message?.count)) {
@@ -463,32 +443,15 @@ var LiveCounterController = class _LiveCounterController extends Controller {
463
443
  this.#reconcile(this.#current + delta);
464
444
  }
465
445
  }
466
- /** True when no channel is declared: the local-only mode, with no server to diverge from. */
467
446
  get #local() {
468
447
  return this.channelValue === "";
469
448
  }
470
- /** True while an increment would go through (local-only counters always are). */
471
449
  get #ready() {
472
450
  return this.#local || (this.#subscription?.confirmed ?? false);
473
451
  }
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
452
  #syncTriggers() {
481
453
  for (const trigger of this.triggerTargets) this.#syncTrigger(trigger);
482
454
  }
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
455
  #syncTrigger(trigger) {
493
456
  if (this.#ready) {
494
457
  this.#focusedTriggers.release(trigger);
@@ -503,50 +466,26 @@ var LiveCounterController = class _LiveCounterController extends Controller {
503
466
  trigger.setAttribute("disabled", "");
504
467
  trigger.setAttribute(DISABLED_MARKER, "");
505
468
  }
506
- /** Gives back a `disabled` this controller lent, leaving an authored one alone. */
507
469
  #release(trigger) {
508
470
  if (!trigger.hasAttribute(DISABLED_MARKER)) return;
509
471
  trigger.removeAttribute("disabled");
510
472
  trigger.removeAttribute(DISABLED_MARKER);
511
473
  }
512
- /** The displayed element: the `value` target, else the controller element. */
513
474
  get #display() {
514
475
  return this.hasValueTarget ? this.valueTarget : this.element;
515
476
  }
516
- /** The current count, parsed from the DOM (the single source of truth). */
517
477
  get #current() {
518
478
  return authoredInteger(this.#display.textContent ?? "") ?? 0;
519
479
  }
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
480
  #write(count) {
525
481
  if (count === this.#current) return false;
526
482
  this.#display.textContent = String(count);
527
483
  this.dispatch("change", { detail: { count } });
528
484
  return true;
529
485
  }
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
486
  #reconcile(count) {
539
487
  if (this.#write(count)) this.#announce(count);
540
488
  }
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
489
  #announce(count) {
551
490
  this.#timers.clearAll();
552
491
  this.#timers.set(() => {
@@ -555,16 +494,9 @@ var LiveCounterController = class _LiveCounterController extends Controller {
555
494
  }
556
495
  };
557
496
 
558
- // src/utils/keyed_timers.ts
559
497
  var KeyedTimers = class {
560
498
  #timers = new SafeTimeout();
561
- /** The pending timer of each key; an entry lives exactly as long as its timer. */
562
499
  #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
500
  set(key, callback, delay) {
569
501
  this.clear(key);
570
502
  const id = this.#timers.set(() => {
@@ -573,46 +505,80 @@ var KeyedTimers = class {
573
505
  }, delay);
574
506
  this.#pending.set(key, id);
575
507
  }
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
508
  clear(key) {
584
509
  this.#timers.clear(this.#pending.get(key) ?? -1);
585
510
  this.#pending.delete(key);
586
511
  }
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
512
  clearAll() {
592
513
  this.#timers.clearAll();
593
514
  this.#pending.clear();
594
515
  }
595
- /** Whether `key` has a timer pending. */
596
516
  has(key) {
597
517
  return this.#pending.has(key);
598
518
  }
599
519
  };
600
520
 
601
- // src/utils/template_row.ts
521
+ var NUMBER_BOUNDS = {
522
+ nonNegative: { finite: true, min: 0 },
523
+ timer: { finite: true, min: 0, max: MAX_TIMER_DELAY_MS },
524
+ positiveTimer: { finite: true, exclusiveMin: 0, max: MAX_TIMER_DELAY_MS }
525
+ };
526
+ function matchesNumberBounds(value, bounds) {
527
+ if (!Number.isFinite(value)) {
528
+ const direction = value === Infinity ? "positive" : value === -Infinity ? "negative" : null;
529
+ if (direction === null) return false;
530
+ if (bounds.allowInfinity !== "both" && bounds.allowInfinity !== direction) return false;
531
+ }
532
+ if (bounds.min !== void 0 && value < bounds.min) return false;
533
+ if (bounds.max !== void 0 && value > bounds.max) return false;
534
+ if (bounds.exclusiveMin !== void 0 && value <= bounds.exclusiveMin) return false;
535
+ if (bounds.integer && !Number.isInteger(value)) return false;
536
+ if (bounds.allowedValues !== void 0 && !bounds.allowedValues.includes(value)) return false;
537
+ return true;
538
+ }
539
+
540
+ function readNumber(raw, fallback, bounds) {
541
+ return matchesNumberBounds(raw, bounds) ? raw : fallback;
542
+ }
543
+
544
+ var NumberValueReader = class {
545
+ #lastRejected = /* @__PURE__ */ new Map();
546
+ read(owner, name, raw, fallback, bounds) {
547
+ const resolved = readNumber(raw, fallback, bounds);
548
+ if (matchesNumberBounds(raw, bounds)) {
549
+ this.#lastRejected.delete(name);
550
+ return resolved;
551
+ }
552
+ const attribute = `data-${owner.identifier}-${name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}-value`;
553
+ const literal = owner.element.getAttribute(attribute);
554
+ if (literal === null) {
555
+ this.#lastRejected.delete(name);
556
+ return resolved;
557
+ }
558
+ if (this.#lastRejected.get(name) !== literal) {
559
+ this.#lastRejected.set(name, literal);
560
+ console.warn(
561
+ `Stimeo UI: "${owner.identifier}" has an invalid number Value "${name}" declaration ${JSON.stringify(literal)}; using ${fallback}.`
562
+ );
563
+ }
564
+ return resolved;
565
+ }
566
+ };
567
+
602
568
  function cloneTemplateRoot(template) {
603
569
  const root = template.content.firstElementChild;
604
570
  return root instanceof HTMLElement ? root.cloneNode(true) : null;
605
571
  }
606
572
 
607
- // src/cable/presence_controller.ts
608
573
  var TRANSIENT2 = new TransientHooks({
609
574
  attributes: ["data-present", "data-present-count", "data-presence-rejected"]
610
575
  });
611
576
  var BEACON_THROTTLE_MS = 2e3;
612
577
  var DEFAULT_HEARTBEAT_MS = 15e3;
613
578
  var DEFAULT_TIMEOUT_MS = 4e4;
614
- var speakers = /* @__PURE__ */ new Map();
579
+ var speakers = sharedRegistry("stimeo-ui.presence.registry.v1", () => /* @__PURE__ */ new Map());
615
580
  var PresenceController = class _PresenceController extends Controller {
581
+ #numbers = new NumberValueReader();
616
582
  static targets = ["count", "list", "template"];
617
583
  static values = {
618
584
  channel: { type: String, default: "" },
@@ -624,168 +590,144 @@ var PresenceController = class _PresenceController extends Controller {
624
590
  announceJoinText: { type: String, default: "" },
625
591
  announceLeaveText: { type: String, default: "" }
626
592
  };
593
+ static valueConstraints = {
594
+ heartbeat: NUMBER_BOUNDS.positiveTimer,
595
+ timeout: NUMBER_BOUNDS.positiveTimer
596
+ };
627
597
  static events = ["join", "leave", "change"];
628
- /** Delay (ms) before one roster change is sent to the shared announcer. */
629
598
  static #announceDelay = 200;
630
- /** Identifier parameters parsed once from their declaration, never in the hot path. */
631
- #params = {};
632
599
  #subscription = null;
633
- /** Present peers keyed by id (insertion order = join order). */
600
+ #follow = new MicrotaskCoalescer(() => this.#followDeclaration());
601
+ #identifier = "";
602
+ #period = 0;
634
603
  #peers = /* @__PURE__ */ new Map();
635
- /** Each peer's auto-expiry timer, restarted by every beacon from that peer. */
636
604
  #expiry = new KeyedTimers();
637
605
  #timers = new SafeTimeout();
638
606
  #intervals = new SafeInterval();
639
- /** Epoch ms of the last outgoing beacon, for the convergence throttle. */
640
607
  #lastBeaconAt = 0;
641
- /** Pending trailing-edge convergence beacon (at most one queued). */
642
608
  #pendingBeacon = null;
643
- /** The one outstanding announcement, so a newer roster change supersedes it. */
644
609
  #announceId = null;
645
- /** The `speakers` key this element holds, recomputed on every `connect()`. */
610
+ #connected = false;
611
+ #cycleListeners = new ListenerSet();
646
612
  #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
- */
613
+ #speakerId = "";
614
+ channelValueChanged() {
615
+ this.#follow.schedule();
616
+ }
653
617
  paramsValueChanged() {
654
- this.#params = parseSubscriptionParams(this.paramsValue);
618
+ this.#follow.schedule();
619
+ }
620
+ idValueChanged() {
621
+ this.#follow.schedule();
622
+ }
623
+ heartbeatValueChanged() {
624
+ this.#follow.schedule();
655
625
  }
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
626
  get #descriptor() {
662
- const { channel: _declared, ...rest } = this.#params;
627
+ const { channel: _declared, ...rest } = parseSubscriptionParams(this.paramsValue);
663
628
  return { channel: this.channelValue, ...rest };
664
629
  }
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
630
  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
- */
631
+ return this.#safeHeartbeat;
632
+ }
683
633
  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
- */
634
+ return this.#safeTimeout;
635
+ }
693
636
  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);
637
+ this.#drawList();
638
+ }
639
+ listTargetDisconnected() {
640
+ if (this.#connected) this.#drawList();
698
641
  }
699
- /** Paints the roster size into a count target that arrives blank, for the same reason. */
700
642
  countTargetConnected() {
701
- this.countTarget.textContent = this.#countMessage(this.#peers.size);
643
+ this.#drawCount();
644
+ }
645
+ countTargetDisconnected() {
646
+ if (this.#connected) this.#drawCount();
702
647
  }
703
648
  connect() {
649
+ this.#connected = true;
650
+ this.#follow.activate();
651
+ this.#start();
652
+ }
653
+ disconnect() {
654
+ this.#connected = false;
655
+ this.#follow.cancel();
656
+ this.#close();
657
+ this.#reset();
658
+ }
659
+ #start() {
704
660
  this.#reset();
705
661
  if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(0);
662
+ this.#open();
663
+ }
664
+ #open() {
665
+ const descriptor = this.#descriptor;
666
+ this.#identifier = identifierOf(descriptor);
706
667
  if (!this.channelValue) return;
707
668
  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.
669
+ this.#subscription = createConfirmedSubscription(descriptor, {
712
670
  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
671
  rejected: () => {
716
672
  this.element.setAttribute("data-presence-rejected", "true");
717
673
  },
718
674
  received: (data) => this.#onReceived(data)
719
675
  });
720
- this.#intervals.set(() => this.#beacon(true), this.#heartbeat);
721
- window.addEventListener("pagehide", this.#onPageHide);
676
+ this.#armHeartbeat();
677
+ this.#cycleListeners.add(window, "pagehide", this.#onPageHide);
722
678
  }
723
- disconnect() {
724
- window.removeEventListener("pagehide", this.#onPageHide);
725
- if (this.#releaseSpeaker()) this.#sendLeaveNotice();
679
+ #close() {
680
+ this.#cycleListeners.dispose();
681
+ this.#sendLeaveNotice(this.#releaseSpeaker());
726
682
  this.#subscription?.unsubscribe();
727
683
  this.#subscription = null;
728
684
  this.#intervals.clearAll();
729
- this.#reset();
730
685
  }
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
- */
686
+ #armHeartbeat() {
687
+ this.#intervals.clearAll();
688
+ this.#period = this.#heartbeat;
689
+ this.#intervals.set(() => this.#beacon(true), this.#period);
690
+ }
691
+ #followDeclaration() {
692
+ if (identifierOf(this.#descriptor) !== this.#identifier) {
693
+ this.#close();
694
+ this.#start();
695
+ } else if (this.#subscription) {
696
+ if (this.idValue !== this.#speakerId) this.#moveVoice();
697
+ if (this.#heartbeat !== this.#period) this.#armHeartbeat();
698
+ }
699
+ }
700
+ #moveVoice() {
701
+ this.#sendLeaveNotice(this.#releaseSpeaker());
702
+ this.#claimSpeaker();
703
+ this.#forget(this.#speakerId);
704
+ this.#timers.clear(this.#pendingBeacon ?? -1);
705
+ this.#pendingBeacon = null;
706
+ this.#beacon(true);
707
+ }
738
708
  #claimSpeaker() {
739
- this.#speakerKey = `${identifierOf(this.#descriptor)}
740
- ${this.idValue}`;
709
+ this.#speakerId = this.idValue;
710
+ this.#speakerKey = `${this.#identifier}
711
+ ${this.#speakerId}`;
741
712
  speakers.set(this.#speakerKey, (speakers.get(this.#speakerKey) ?? 0) + 1);
742
713
  }
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
714
  #releaseSpeaker() {
749
715
  const key = this.#speakerKey;
716
+ const id = this.#speakerId;
750
717
  this.#speakerKey = "";
718
+ this.#speakerId = "";
751
719
  const remaining = (speakers.get(key) ?? 1) - 1;
752
720
  if (remaining > 0) speakers.set(key, remaining);
753
721
  else speakers.delete(key);
754
- return remaining <= 0;
722
+ return remaining > 0 ? "" : id;
723
+ }
724
+ #sendLeaveNotice(id) {
725
+ if (!this.#subscription?.confirmed || !id) return;
726
+ this.#subscription.perform("appear", { id, leaving: true });
755
727
  }
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
728
  #onPageHide = () => {
774
- this.#sendLeaveNotice();
729
+ this.#sendLeaveNotice(this.#speakerId);
775
730
  };
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
731
  #beacon(force) {
790
732
  if (!this.#subscription?.confirmed || !this.idValue) return;
791
733
  const now = Date.now();
@@ -802,14 +744,6 @@ ${this.idValue}`;
802
744
  this.#lastBeaconAt = now;
803
745
  this.#subscription.perform("appear", { id: this.idValue, name: this.nameValue });
804
746
  }
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
747
  #onReceived(data) {
814
748
  const beacon = data;
815
749
  const id = beacon?.id;
@@ -833,11 +767,6 @@ ${this.idValue}`;
833
767
  this.#render();
834
768
  }
835
769
  }
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
770
  #drop(id) {
842
771
  const peer = this.#peers.get(id);
843
772
  if (peer === void 0) return;
@@ -848,25 +777,22 @@ ${this.idValue}`;
848
777
  this.#announce(this.announceLeaveTextValue, peer.name);
849
778
  this.dispatch("leave", { detail: { id } });
850
779
  }
851
- /**
852
- * Reflects the roster onto the hooks + count target and emits `change`.
853
- */
780
+ #forget(id) {
781
+ if (!this.#peers.delete(id)) return;
782
+ this.#expiry.clear(id);
783
+ this.#removeClone(id);
784
+ this.#paint();
785
+ }
854
786
  #render() {
787
+ this.dispatch("change", { detail: { users: this.#paint() } });
788
+ }
789
+ #paint() {
855
790
  const users = [...this.#peers.entries()].map(([id, peer]) => ({ id, name: peer.name }));
856
791
  this.element.setAttribute("data-present", users.length > 0 ? "true" : "false");
857
792
  this.element.setAttribute("data-present-count", String(users.length));
858
793
  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
- */
794
+ return users;
795
+ }
870
796
  #announce(template, name) {
871
797
  const message = fillTemplate(template, { name, count: this.#peers.size });
872
798
  if (message.trim() === "") return;
@@ -876,17 +802,21 @@ ${this.idValue}`;
876
802
  this.#announceId = null;
877
803
  }, _PresenceController.#announceDelay);
878
804
  }
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
805
  #countMessage(count) {
885
806
  const templates = this.countTarget.dataset;
886
807
  const template = (count === 0 ? templates.zero : count === 1 ? templates.one : templates.other) ?? templates.other;
887
- return template ? template.replace("%{count}", String(count)) : String(count);
808
+ return template ? fillTemplate(template, { count }) : String(count);
809
+ }
810
+ #drawList() {
811
+ if (!this.hasListTarget) return;
812
+ for (const child of this.listTarget.querySelectorAll("[data-presence-id]")) {
813
+ child.remove();
814
+ }
815
+ for (const [id, peer] of this.#peers) this.#appendClone(id, peer.name);
816
+ }
817
+ #drawCount() {
818
+ if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(this.#peers.size);
888
819
  }
889
- /** Appends one template clone for a newly present peer (list + template only). */
890
820
  #appendClone(id, name) {
891
821
  if (!this.hasListTarget || !this.hasTemplateTarget) return;
892
822
  const root = cloneTemplateRoot(this.templateTarget);
@@ -909,7 +839,6 @@ ${this.idValue}`;
909
839
  }
910
840
  return null;
911
841
  }
912
- /** Writes the peer's name into the clone's `data-presence-name` slots. */
913
842
  #fillName(root, name) {
914
843
  const slots = root.querySelectorAll("[data-presence-name]");
915
844
  for (const slot of slots) slot.textContent = name;
@@ -917,7 +846,6 @@ ${this.idValue}`;
917
846
  root.textContent = name;
918
847
  }
919
848
  }
920
- /** Clears the transient roster state (connect reset + disconnect teardown). */
921
849
  #reset() {
922
850
  this.#timers.clearAll();
923
851
  this.#expiry.clearAll();
@@ -933,84 +861,121 @@ ${this.idValue}`;
933
861
  }
934
862
  }
935
863
  }
864
+ get #safeHeartbeat() {
865
+ return this.#numbers.read(
866
+ this,
867
+ "heartbeat",
868
+ this.heartbeatValue,
869
+ _PresenceController.values.heartbeat.default,
870
+ _PresenceController.valueConstraints.heartbeat
871
+ );
872
+ }
873
+ get #safeTimeout() {
874
+ return this.#numbers.read(
875
+ this,
876
+ "timeout",
877
+ this.timeoutValue,
878
+ _PresenceController.values.timeout.default,
879
+ _PresenceController.valueConstraints.timeout
880
+ );
881
+ }
936
882
  };
937
883
  var TRANSIENT3 = new TransientHooks({
938
884
  attributes: ["data-typing", "data-typing-indicator-rejected"]
939
885
  });
940
886
  var DEFAULT_TIMEOUT = 3e3;
941
887
  var DEFAULT_THROTTLE = 2e3;
942
- var fillTokens = (template, values) => template.replace(/%\{(name|names|count)\}/g, (match, token) => values[token] ?? match);
943
888
  var TypingIndicatorController = class _TypingIndicatorController extends Controller {
889
+ #numbers = new NumberValueReader();
944
890
  static targets = ["input", "status"];
945
891
  static values = {
946
892
  channel: { type: String, default: "" },
947
893
  params: { type: String, default: "" },
948
894
  name: { type: String, default: "" },
949
895
  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
896
  throttle: { type: Number, default: DEFAULT_THROTTLE },
954
897
  announceOneText: { type: String, default: "" },
955
898
  announceManyText: { type: String, default: "" }
956
899
  };
900
+ static valueConstraints = {
901
+ timeout: NUMBER_BOUNDS.timer,
902
+ throttle: NUMBER_BOUNDS.nonNegative
903
+ };
957
904
  static events = ["change"];
958
- /** Delay (ms) before one settled typer set is sent to the shared announcer. */
959
905
  static #announceDelay = 200;
960
- /** Identifier parameters parsed once from their declaration, never in the hot path. */
961
- #params = {};
962
906
  #subscription = null;
963
- /** Names currently typing (other clients), in the order their first signal arrived. */
907
+ #follow = new MicrotaskCoalescer(() => this.#followDeclaration());
908
+ #identifier = "";
964
909
  #typers = /* @__PURE__ */ new Set();
965
- /** Each typer's auto-clear timer, restarted by every further signal from that name. */
966
910
  #expiry = new KeyedTimers();
967
911
  #timers = new SafeTimeout();
968
- /** Epoch ms of the last broadcast, for leading-edge throttling. */
969
912
  #lastSentAt = 0;
970
- /** The one outstanding announcement, so a newer set supersedes it. */
971
913
  #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
- */
914
+ #connected = false;
915
+ #listeners = new ListenerSet();
916
+ channelValueChanged() {
917
+ this.#follow.schedule();
918
+ }
978
919
  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
- */
920
+ this.#follow.schedule();
921
+ }
922
+ nameValueChanged() {
923
+ this.#follow.schedule();
924
+ }
925
+ get #descriptor() {
926
+ const { channel: _declared, ...rest } = parseSubscriptionParams(this.paramsValue);
927
+ return { channel: this.channelValue, ...rest };
928
+ }
989
929
  statusTargetConnected() {
990
- if (this.#typers.size > 0) this.#paint();
930
+ if (this.#connected) this.#paintStatus([...this.#typers]);
931
+ }
932
+ statusTargetDisconnected() {
933
+ if (this.#connected) this.#paintStatus([...this.#typers]);
991
934
  }
992
935
  connect() {
993
- TRANSIENT3.reset(this.element);
994
- if (this.hasStatusTarget) this.statusTarget.textContent = "";
995
- 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
- }
936
+ this.#connected = true;
937
+ this.#reset();
938
+ this.#listeners.add(this.element, "input", this.#onInput);
939
+ this.#follow.activate();
940
+ this.#open();
1009
941
  }
1010
942
  disconnect() {
1011
- this.element.removeEventListener("input", this.#onInput);
943
+ this.#connected = false;
944
+ this.#follow.cancel();
945
+ this.#listeners.dispose();
946
+ this.#close();
947
+ this.#reset();
948
+ }
949
+ #open() {
950
+ const descriptor = this.#descriptor;
951
+ this.#identifier = identifierOf(descriptor);
952
+ if (!this.channelValue) return;
953
+ this.#subscription = createConfirmedSubscription(descriptor, {
954
+ rejected: () => {
955
+ this.element.setAttribute("data-typing-indicator-rejected", "true");
956
+ },
957
+ received: (data) => this.#onReceived(data)
958
+ });
959
+ }
960
+ #close() {
1012
961
  this.#subscription?.unsubscribe();
1013
962
  this.#subscription = null;
963
+ }
964
+ #followDeclaration() {
965
+ if (identifierOf(this.#descriptor) !== this.#identifier) {
966
+ this.#close();
967
+ this.#reset();
968
+ this.#open();
969
+ } else {
970
+ this.#forget(this.nameValue);
971
+ }
972
+ }
973
+ #forget(name) {
974
+ if (!this.#typers.delete(name)) return;
975
+ this.#expiry.clear(name);
976
+ this.#paint();
977
+ }
978
+ #reset() {
1014
979
  this.#timers.clearAll();
1015
980
  this.#expiry.clearAll();
1016
981
  this.#announceId = null;
@@ -1019,7 +984,6 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1019
984
  if (this.hasStatusTarget) this.statusTarget.textContent = "";
1020
985
  this.#lastSentAt = 0;
1021
986
  }
1022
- /** Throttled (leading-edge) broadcast of this client's typing signal. */
1023
987
  #onInput = () => {
1024
988
  if (!this.#subscription?.confirmed) return;
1025
989
  const now = Date.now();
@@ -1027,14 +991,6 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1027
991
  this.#lastSentAt = now;
1028
992
  this.#subscription.perform("typing", { name: this.nameValue });
1029
993
  };
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
994
  #onReceived(data) {
1039
995
  const name = data?.name;
1040
996
  if (typeof name !== "string" || name === "" || name === this.nameValue) return;
@@ -1043,53 +999,21 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1043
999
  this.#expiry.set(name, () => this.#untrack(name), this.#timeout);
1044
1000
  if (added) this.#render();
1045
1001
  }
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
1002
  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
- */
1003
+ return this.#safeTimeout;
1004
+ }
1065
1005
  get #throttle() {
1066
- const declared = this.throttleValue;
1067
- return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_THROTTLE;
1006
+ return this.#safeThrottle;
1068
1007
  }
1069
1008
  #untrack(name) {
1070
1009
  this.#typers.delete(name);
1071
1010
  this.#render();
1072
1011
  }
1073
- /**
1074
- * Reflects the typer set onto the display, announces it, and emits `change`.
1075
- */
1076
1012
  #render() {
1077
1013
  const names = this.#paint();
1078
1014
  this.#announce(names);
1079
1015
  this.dispatch("change", { detail: { names } });
1080
1016
  }
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
1017
  #announce(names) {
1094
1018
  if (this.#announceId !== null) this.#timers.clear(this.#announceId);
1095
1019
  this.#announceId = null;
@@ -1103,34 +1027,46 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
1103
1027
  this.#announceId = null;
1104
1028
  }, _TypingIndicatorController.#announceDelay);
1105
1029
  }
1106
- /** Writes the current typer set onto the hook and the status slot. */
1107
1030
  #paint() {
1108
1031
  const names = [...this.#typers];
1109
1032
  this.element.setAttribute("data-typing", names.length > 0 ? "true" : "false");
1110
- if (this.hasStatusTarget) {
1111
- this.statusTarget.textContent = this.#message(names);
1112
- }
1033
+ this.#paintStatus(names);
1113
1034
  return names;
1114
1035
  }
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
- */
1036
+ #paintStatus(names) {
1037
+ if (this.hasStatusTarget) this.statusTarget.textContent = this.#message(names);
1038
+ }
1120
1039
  #message(names) {
1121
1040
  if (names.length === 0) return "";
1122
1041
  const joined = names.join(", ");
1123
1042
  if (names.length === 1) {
1124
1043
  const template2 = this.statusTarget.dataset.one;
1125
1044
  const name = names[0] ?? "";
1126
- return template2 ? fillTokens(template2, { name }) : `${name} is typing\u2026`;
1045
+ return template2 ? fillTemplate(template2, { name }) : `${name} is typing\u2026`;
1127
1046
  }
1128
1047
  const template = this.statusTarget.dataset.many;
1129
- return template ? fillTokens(template, { names: joined, count: String(names.length) }) : `${joined} are typing\u2026`;
1048
+ return template ? fillTemplate(template, { names: joined, count: names.length }) : `${joined} are typing\u2026`;
1049
+ }
1050
+ get #safeTimeout() {
1051
+ return this.#numbers.read(
1052
+ this,
1053
+ "timeout",
1054
+ this.timeoutValue,
1055
+ _TypingIndicatorController.values.timeout.default,
1056
+ _TypingIndicatorController.valueConstraints.timeout
1057
+ );
1058
+ }
1059
+ get #safeThrottle() {
1060
+ return this.#numbers.read(
1061
+ this,
1062
+ "throttle",
1063
+ this.throttleValue,
1064
+ _TypingIndicatorController.values.throttle.default,
1065
+ _TypingIndicatorController.valueConstraints.throttle
1066
+ );
1130
1067
  }
1131
1068
  };
1132
1069
 
1133
- // src/cable/index.ts
1134
1070
  var cableControllers = {
1135
1071
  "stimeo--live-counter": LiveCounterController,
1136
1072
  "stimeo--presence": PresenceController,
@@ -1143,5 +1079,4 @@ function registerCable(application) {
1143
1079
  }
1144
1080
 
1145
1081
  export { LiveCounterController, PresenceController, TypingIndicatorController, cableControllers, createConfirmedSubscription, getCableConsumer, registerCable, setCableConsumer };
1146
- //# sourceMappingURL=index.js.map
1147
1082
  //# sourceMappingURL=index.js.map