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,44 +1,7 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/reading_progress_controller.ts
4
-
5
- // src/utils/before_cache_reset.ts
6
- var BeforeCacheReset = class _BeforeCacheReset {
7
- /** Every subscribed instance, iterated by the one shared document listener. */
8
- static #subscribers = /* @__PURE__ */ new Set();
9
- /** The shared listener; installed while at least one instance is subscribed. */
10
- static #onBeforeCache = () => {
11
- for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
12
- };
13
- #rewind;
14
- /** @param rewind - the pass that returns this controller's state to its initial form. */
15
- constructor(rewind) {
16
- this.#rewind = rewind;
17
- }
18
- /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
19
- activate() {
20
- const first = _BeforeCacheReset.#subscribers.size === 0;
21
- _BeforeCacheReset.#subscribers.add(this);
22
- if (first) {
23
- document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
24
- }
25
- }
26
- /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
27
- deactivate() {
28
- _BeforeCacheReset.#subscribers.delete(this);
29
- if (_BeforeCacheReset.#subscribers.size > 0) return;
30
- document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
31
- }
32
- };
33
-
34
- // src/utils/frame_coalescer.ts
35
3
  var FrameCoalescer = class {
36
4
  #frame = null;
37
- /**
38
- * Runs `run` on the next frame, unless a frame is already pending — the first
39
- * request of a burst wins and the rest are dropped. The pending frame is
40
- * released before `run`, so `run` may request the next one.
41
- */
42
5
  schedule(run) {
43
6
  if (this.#frame !== null) return;
44
7
  this.#frame = requestAnimationFrame(() => {
@@ -46,14 +9,6 @@ var FrameCoalescer = class {
46
9
  run();
47
10
  });
48
11
  }
49
- /**
50
- * Drops the pending frame, and reaches the platform only when there is one.
51
- *
52
- * There is no handle value that stands for "nothing pending":
53
- * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
54
- * arrives as a large positive number that the same allocator can hand out, and
55
- * an idle cancel would then drop a frame belonging to someone else.
56
- */
57
12
  cancel() {
58
13
  if (this.#frame === null) return;
59
14
  cancelAnimationFrame(this.#frame);
@@ -61,18 +16,15 @@ var FrameCoalescer = class {
61
16
  }
62
17
  };
63
18
 
64
- // src/utils/layout_observer.ts
65
19
  var LayoutObserver = class {
66
20
  #callback;
67
21
  #resizeObserverFactory;
68
22
  #resizeObserver = null;
69
23
  #observingViewport = false;
70
24
  #loadContainer = null;
71
- /** Stable bound handler so add/removeEventListener target the same reference. */
72
25
  #handleViewportResize = () => {
73
26
  this.#callback();
74
27
  };
75
- /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */
76
28
  #handleDescendantLoad = () => {
77
29
  this.#callback();
78
30
  };
@@ -80,11 +32,6 @@ var LayoutObserver = class {
80
32
  this.#callback = callback;
81
33
  this.#resizeObserverFactory = options.resizeObserverFactory ?? (typeof ResizeObserver === "undefined" ? null : (cb) => new ResizeObserver(cb));
82
34
  }
83
- /**
84
- * Starts observing an element's size. Repeated calls observe additional
85
- * elements through the same shared observer. No-ops when no
86
- * `ResizeObserver` implementation is available.
87
- */
88
35
  observe(element) {
89
36
  if (!this.#resizeObserverFactory) return;
90
37
  if (!this.#resizeObserver) {
@@ -94,47 +41,28 @@ var LayoutObserver = class {
94
41
  }
95
42
  this.#resizeObserver.observe(element);
96
43
  }
97
- /** Stops observing a single element while leaving any others in place. */
98
44
  unobserve(element) {
99
45
  this.#resizeObserver?.unobserve(element);
100
46
  }
101
- /** Starts observing viewport resizes. Idempotent: the listener is added once. */
102
47
  observeViewport() {
103
48
  if (this.#observingViewport) return;
104
49
  this.#observingViewport = true;
105
50
  window.addEventListener("resize", this.#handleViewportResize);
106
51
  }
107
- /** Stops observing viewport resizes without affecting element observation. */
108
52
  unobserveViewport() {
109
53
  if (!this.#observingViewport) return;
110
54
  this.#observingViewport = false;
111
55
  window.removeEventListener("resize", this.#handleViewportResize);
112
56
  }
113
- /**
114
- * Starts reporting a `load` from anywhere inside `container` — an image or a
115
- * frame settling changes the box it sits in, and it measures as zero high until
116
- * then. `load` does not bubble, so the subscription is a capture-phase listener
117
- * on the container itself and nothing the caller spells.
118
- *
119
- * **One container at a time.** A further call moves the observation, so a widget
120
- * whose content element is swapped at runtime releases the element it let go by
121
- * naming the new one — there is no second place for the release to drift from.
122
- */
123
57
  observeDescendantLoads(container) {
124
58
  this.unobserveDescendantLoads();
125
59
  this.#loadContainer = container;
126
60
  container.addEventListener("load", this.#handleDescendantLoad, true);
127
61
  }
128
- /** Stops reporting descendant loads without affecting element or viewport observation. */
129
62
  unobserveDescendantLoads() {
130
63
  this.#loadContainer?.removeEventListener("load", this.#handleDescendantLoad, true);
131
64
  this.#loadContainer = null;
132
65
  }
133
- /**
134
- * Releases every observation: disconnects the {@link ResizeObserver} and removes
135
- * the viewport and descendant-load listeners. Safe to call multiple times. Call
136
- * this from a controller's `disconnect()`.
137
- */
138
66
  disconnect() {
139
67
  this.#resizeObserver?.disconnect();
140
68
  this.#resizeObserver = null;
@@ -143,81 +71,196 @@ var LayoutObserver = class {
143
71
  }
144
72
  };
145
73
 
146
- // src/utils/style_property_lease.ts
147
- var StylePropertyLease = class {
74
+ var ListenerSet = class {
75
+ #abort = new AbortController();
76
+ add(target, type, handler, options) {
77
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
78
+ }
79
+ dispose() {
80
+ this.#abort.abort();
81
+ this.#abort = new AbortController();
82
+ }
83
+ };
84
+
85
+ function setAttributeIfChanged(element, name, value, beforeWrite) {
86
+ if (element.getAttribute(name) === value) return false;
87
+ if (value === null) element.removeAttribute(name);
88
+ else element.setAttribute(name, value);
89
+ return true;
90
+ }
91
+
92
+ function sharedRegistry(key, create) {
93
+ const symbol = Symbol.for(key);
94
+ const scope = globalThis;
95
+ const existing = scope[symbol];
96
+ if (existing !== void 0) return existing;
97
+ const registry = create();
98
+ Object.defineProperty(globalThis, symbol, { value: registry });
99
+ return registry;
100
+ }
101
+
102
+ var StylePropertyLease = class _StylePropertyLease {
103
+ static #registry = sharedRegistry(
104
+ "stimeo-ui.style-property-lease.registry.v1",
105
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
106
+ );
148
107
  #property;
149
- #records = /* @__PURE__ */ new Map();
150
- /** @param property - The CSS property whose temporary values this lease owns. */
151
- constructor(property) {
108
+ #record;
109
+ #held = /* @__PURE__ */ new Set();
110
+ constructor(property, owner) {
152
111
  this.#property = property;
112
+ this.#record = `data-${owner}-style-${property}-lease`;
113
+ const names = _StylePropertyLease.#registry.names;
114
+ names.set(property, (names.get(property) ?? /* @__PURE__ */ new Set()).add(this.#record));
153
115
  }
154
- /** Writes or removes the leased declaration while preserving its authored value. */
155
116
  write(element, value, priority = "") {
156
- const existing = this.#records.get(element);
157
- if (existing) {
158
- existing.writtenValue = value;
159
- existing.writtenPriority = value === null ? "" : priority;
160
- } else {
161
- this.#records.set(element, {
162
- originalValue: element.style.getPropertyValue(this.#property),
163
- originalPriority: element.style.getPropertyPriority(this.#property),
164
- writtenValue: value,
165
- writtenPriority: value === null ? "" : priority
166
- });
167
- }
168
- this.#reflect(element, value, priority);
117
+ const stack = this.#stack(element);
118
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
119
+ this.#held.add(element);
120
+ const shows = !this.#covered(element, stack, hold);
121
+ hold.writtenValue = value;
122
+ hold.writtenPriority = value === null ? "" : priority;
123
+ this.#mark(element, stack);
124
+ if (shows) this.#show(element, value, hold.writtenPriority);
169
125
  }
170
- /** Returns one lease without overwriting a later consumer declaration. */
171
126
  return(element) {
172
- const record = this.#records.get(element);
173
- if (!record) return;
174
- this.#records.delete(element);
175
- const style = element.style;
176
- const stillOwned = style.getPropertyValue(this.#property) === (record.writtenValue ?? "") && style.getPropertyPriority(this.#property) === record.writtenPriority;
177
- if (stillOwned) {
178
- this.#reflect(element, record.originalValue, record.originalPriority);
179
- }
127
+ const held = this.#held.delete(element);
128
+ const known = _StylePropertyLease.#registry.stacks.get(element)?.has(this.#property);
129
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
130
+ const hold = this.#take(stack);
131
+ if (!hold) return;
132
+ const owns = this.#carries(element, hold) && !this.#covered(element, stack, hold);
133
+ const index = stack.indexOf(hold);
134
+ stack.splice(index, 1);
135
+ const above = stack[index];
136
+ if (above) above.linked &&= hold.linked;
137
+ this.#mark(element, stack);
138
+ if (!owns) return;
139
+ const below = hold.linked ? stack[index - 1] : void 0;
140
+ if (below) this.#show(element, below.writtenValue, below.writtenPriority);
141
+ else this.#show(element, hold.originalValue, hold.originalPriority);
180
142
  }
181
- /** Returns every outstanding declaration lease. */
182
143
  returnAll() {
183
- for (const element of Array.from(this.#records.keys())) this.return(element);
144
+ for (const element of Array.from(this.#held)) this.return(element);
145
+ }
146
+ #recorded(element, name) {
147
+ try {
148
+ const parsed = JSON.parse(element.getAttribute(name) ?? "null");
149
+ if (!Array.isArray(parsed)) return null;
150
+ const [originalValue, originalPriority, writtenValue, writtenPriority, ...rest] = parsed;
151
+ if (typeof originalValue !== "string" || typeof originalPriority !== "string") return null;
152
+ if (typeof writtenValue !== "string" && writtenValue !== null) return null;
153
+ if (typeof writtenPriority !== "string") return null;
154
+ const linked = rest[0] !== false;
155
+ const beneath = linked ? rest : rest.slice(1);
156
+ if (!beneath.every((under) => typeof under === "string")) return null;
157
+ return { originalValue, originalPriority, writtenValue, writtenPriority, linked, beneath };
158
+ } catch {
159
+ return null;
160
+ }
184
161
  }
185
- /** Reflects only a real declaration transition. */
186
- #reflect(element, value, priority) {
162
+ #stack(element) {
163
+ const stacks = _StylePropertyLease.#registry.stacks;
164
+ const byProperty = stacks.get(element) ?? /* @__PURE__ */ new Map();
165
+ stacks.set(element, byProperty);
166
+ const stack = byProperty.get(this.#property) ?? this.#seed(element);
167
+ byProperty.set(this.#property, stack);
168
+ return stack;
169
+ }
170
+ #seed(element) {
171
+ const found = [];
172
+ for (const record of _StylePropertyLease.#registry.names.get(this.#property) ?? []) {
173
+ const read = this.#recorded(element, record);
174
+ if (read) found.push({ ...read, record });
175
+ }
176
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
177
+ return found.sort((one, other) => depth(one) - depth(other)).map((entry, index) => ({
178
+ lease: null,
179
+ record: entry.record,
180
+ writtenValue: entry.writtenValue,
181
+ writtenPriority: entry.writtenPriority,
182
+ originalValue: entry.originalValue,
183
+ originalPriority: entry.originalPriority,
184
+ linked: entry.linked && index > 0
185
+ }));
186
+ }
187
+ #take(stack) {
188
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
189
+ if (hold) hold.lease = this;
190
+ return hold;
191
+ }
192
+ #acquire(element, stack) {
193
+ const top = stack.at(-1);
194
+ const over = top && this.#carries(element, top) ? top : void 0;
187
195
  const style = element.style;
188
- const nextValue = value ?? "";
189
- const nextPriority = value === null ? "" : priority;
190
- if (style.getPropertyValue(this.#property) === nextValue && style.getPropertyPriority(this.#property) === nextPriority) {
191
- return;
196
+ const hold = {
197
+ lease: this,
198
+ record: this.#record,
199
+ writtenValue: null,
200
+ writtenPriority: "",
201
+ originalValue: over ? over.originalValue : style.getPropertyValue(this.#property),
202
+ originalPriority: over ? over.originalPriority : style.getPropertyPriority(this.#property),
203
+ linked: over !== void 0
204
+ };
205
+ stack.push(hold);
206
+ return hold;
207
+ }
208
+ #covered(element, stack, hold) {
209
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => this.#carries(element, above));
210
+ }
211
+ #mark(element, stack) {
212
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
213
+ stack.forEach((hold, index) => {
214
+ const names = stack.slice(0, index).map((under) => under.record);
215
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
216
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
217
+ const { originalValue, originalPriority, writtenValue, writtenPriority } = hold;
218
+ records.set(
219
+ hold.record,
220
+ (writtenValue ?? "") === originalValue && writtenPriority === originalPriority && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([
221
+ originalValue,
222
+ originalPriority,
223
+ writtenValue,
224
+ writtenPriority,
225
+ ...marker,
226
+ ...beneath
227
+ ])
228
+ );
229
+ });
230
+ for (const [name, recorded] of records) {
231
+ setAttributeIfChanged(element, name, recorded);
192
232
  }
193
- if (value === null) style.removeProperty(this.#property);
194
- else style.setProperty(this.#property, value, priority);
233
+ }
234
+ #carries(element, hold) {
235
+ return this.#holds(element, hold.writtenValue ?? "", hold.writtenPriority);
236
+ }
237
+ #holds(element, value, priority) {
238
+ const style = element.style;
239
+ return style.getPropertyValue(this.#property) === value && style.getPropertyPriority(this.#property) === priority;
240
+ }
241
+ #show(element, value, priority) {
242
+ if (this.#holds(element, value ?? "", priority)) return;
243
+ if (value === null) element.style.removeProperty(this.#property);
244
+ else element.style.setProperty(this.#property, value, priority);
195
245
  }
196
246
  };
197
247
 
198
- // src/controllers/reading_progress_controller.ts
199
248
  var PROGRESS_PROPERTY = "--stimeo--reading-progress";
200
249
  var ReadingProgressController = class extends Controller {
201
250
  static events = ["change", "complete"];
202
- /** Owns both faces of the published property so teardown can hand them back. */
203
- #lease = new StylePropertyLease(PROGRESS_PROPERTY);
204
- /** The article's own box and the viewport: either changes the span. */
251
+ #lease = new StylePropertyLease(PROGRESS_PROPERTY, this.identifier);
205
252
  #layout = new LayoutObserver(() => this.#onScroll());
206
- #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
207
- /** Coalesces scroll bursts, and the connect baseline, into one frame. */
208
253
  #frames = new FrameCoalescer();
209
- /** Last published progress, so `change`/`complete` fire only on movement. */
210
254
  #progress = -1;
211
- /** False until the connect frame has run: `complete` needs real reading. */
212
255
  #baselined = false;
213
256
  #onScroll = () => this.#frames.schedule(() => this.#measure());
257
+ #listeners = new ListenerSet();
214
258
  connect() {
215
259
  this.#progress = -1;
216
260
  this.#baselined = false;
217
- window.addEventListener("scroll", this.#onScroll, { passive: true, capture: true });
261
+ this.#listeners.add(window, "scroll", this.#onScroll, { passive: true, capture: true });
218
262
  this.#layout.observe(this.element);
219
263
  this.#layout.observeViewport();
220
- this.#beforeCache.activate();
221
264
  this.#measure();
222
265
  this.#frames.schedule(() => {
223
266
  this.#measure();
@@ -225,25 +268,12 @@ var ReadingProgressController = class extends Controller {
225
268
  });
226
269
  }
227
270
  disconnect() {
228
- window.removeEventListener("scroll", this.#onScroll, { capture: true });
271
+ this.#listeners.dispose();
229
272
  this.#layout.disconnect();
230
- this.#beforeCache.deactivate();
231
- this.#frames.cancel();
232
- this.#lease.returnAll();
233
- }
234
- /**
235
- * Hands both declarations back before the page is snapshotted, so a restored
236
- * page starts from the authored DOM rather than from someone else's progress.
237
- * The baseline goes back with them: a cancelled visit leaves this page on
238
- * screen, and the next measurement has to publish afresh rather than match a
239
- * value that has already been handed back.
240
- */
241
- #rewindForCache() {
242
273
  this.#frames.cancel();
243
- this.#lease.returnAll();
244
- this.#progress = -1;
274
+ this.#lease.return(this.element);
275
+ this.#lease.return(document.documentElement);
245
276
  }
246
- /** Computes and publishes the progress; emits on movement only. */
247
277
  #measure() {
248
278
  const rect = this.element.getBoundingClientRect();
249
279
  if (rect.width === 0 && rect.height === 0) return;
@@ -262,5 +292,4 @@ var ReadingProgressController = class extends Controller {
262
292
  };
263
293
 
264
294
  export { ReadingProgressController };
265
- //# sourceMappingURL=reading_progress_controller.js.map
266
295
  //# sourceMappingURL=reading_progress_controller.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/frame_coalescer.ts","../../src/utils/layout_observer.ts","../../src/utils/style_property_lease.ts","../../src/controllers/reading_progress_controller.ts"],"names":[],"mappings":"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","file":"reading_progress_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Runs one piece of scroll-driven work per animation frame.\n *\n * A `scroll` listener fires many times between two paints, and each firing would\n * measure the same layout. Requesting a frame on the first firing and ignoring\n * the rest until it runs keeps the reads to one per frame; running on the frame\n * — rather than on a timer — keeps the measurement in step with what the reader\n * sees.\n *\n * **The first request of a burst wins.** Work handed in while a frame is already\n * pending is dropped, so a caller that queues a special first pass and then\n * receives scrolls gets that first pass, not the last scroll's.\n *\n * Cancelling drops the pending frame outright, and does nothing at all when none\n * is pending. A frame really is cancelled, so nothing needs a generation to tell\n * a stale callback from a fresh one, and nothing has to be refused before the\n * caller starts listening.\n *\n * Scope is the scheduling only. What to measure stays with the caller.\n *\n * @example\n * ```ts\n * readonly #frames = new FrameCoalescer();\n *\n * readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n *\n * disconnect(): void {\n * this.#frames.cancel();\n * }\n * ```\n */\nexport class FrameCoalescer {\n #frame: number | null = null;\n\n /**\n * Runs `run` on the next frame, unless a frame is already pending — the first\n * request of a burst wins and the rest are dropped. The pending frame is\n * released before `run`, so `run` may request the next one.\n */\n schedule(run: () => void): void {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n run();\n });\n }\n\n /**\n * Drops the pending frame, and reaches the platform only when there is one.\n *\n * There is no handle value that stands for \"nothing pending\":\n * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder\n * arrives as a large positive number that the same allocator can hand out, and\n * an idle cancel would then drop a frame belonging to someone else.\n */\n cancel(): void {\n if (this.#frame === null) return;\n cancelAnimationFrame(this.#frame);\n this.#frame = null;\n }\n}\n","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","/** One temporarily controlled style property and the authored declaration it displaced. */\ninterface StylePropertyLeaseRecord {\n readonly originalValue: string;\n readonly originalPriority: string;\n writtenValue: string | null;\n writtenPriority: string;\n}\n\n/**\n * Temporarily controls one inline CSS property across a changing set of elements.\n *\n * Authored value and priority are restored only while the declaration still matches\n * the last leased write. A later consumer write therefore wins.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events:\n * a consumer returns its leases from its own `turbo:before-cache` rewind.\n */\nexport class StylePropertyLease<T extends HTMLElement = HTMLElement> {\n readonly #property: string;\n readonly #records = new Map<T, StylePropertyLeaseRecord>();\n\n /** @param property - The CSS property whose temporary values this lease owns. */\n constructor(property: string) {\n this.#property = property;\n }\n\n /** Writes or removes the leased declaration while preserving its authored value. */\n write(element: T, value: string | null, priority = \"\"): void {\n const existing = this.#records.get(element);\n if (existing) {\n existing.writtenValue = value;\n existing.writtenPriority = value === null ? \"\" : priority;\n } else {\n this.#records.set(element, {\n originalValue: element.style.getPropertyValue(this.#property),\n originalPriority: element.style.getPropertyPriority(this.#property),\n writtenValue: value,\n writtenPriority: value === null ? \"\" : priority,\n });\n }\n\n this.#reflect(element, value, priority);\n }\n\n /** Returns one lease without overwriting a later consumer declaration. */\n return(element: T): void {\n const record = this.#records.get(element);\n if (!record) return;\n this.#records.delete(element);\n\n const style = element.style;\n const stillOwned =\n style.getPropertyValue(this.#property) === (record.writtenValue ?? \"\") &&\n style.getPropertyPriority(this.#property) === record.writtenPriority;\n if (stillOwned) {\n this.#reflect(element, record.originalValue, record.originalPriority);\n }\n }\n\n /** Returns every outstanding declaration lease. */\n returnAll(): void {\n for (const element of Array.from(this.#records.keys())) this.return(element);\n }\n\n /** Reflects only a real declaration transition. */\n #reflect(element: T, value: string | null, priority: string): void {\n const style = element.style;\n const nextValue = value ?? \"\";\n const nextPriority = value === null ? \"\" : priority;\n if (\n style.getPropertyValue(this.#property) === nextValue &&\n style.getPropertyPriority(this.#property) === nextPriority\n ) {\n return;\n }\n if (value === null) style.removeProperty(this.#property);\n else style.setProperty(this.#property, value, priority);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { StylePropertyLease } from \"../utils/style_property_lease\";\n\n/** Name of the CSS custom property exposing the reading progress (0..1). */\nconst PROGRESS_PROPERTY = \"--stimeo--reading-progress\";\n\n/**\n * Headless **reading progress**: how far the reader has scrolled *through* this\n * element (an article), published as a CSS custom property and a `change`\n * event stream — the classic top-of-page progress bar.\n * `IntersectionObserver` alone cannot express this (the ratio is constant\n * while a tall article scrolls through the viewport), so this controller owns\n * the scroll math; compose with `stimeo--intersection` when you also need\n * enter/exit triggers. Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--reading-progress`):\n * <article data-controller=\"stimeo--reading-progress\">…</article>\n * <div class=\"progress-bar\" aria-hidden=\"true\"></div>\n * <!-- .progress-bar { width: calc(var(--stimeo--reading-progress, 0) * 100%); } -->\n *\n * Progress is `0` before the article's top reaches the viewport top and `1`\n * once its bottom fits the viewport: `-top / (height - viewportHeight)`,\n * clamped. An article no taller than the viewport has no such span, so reading\n * it is binary: `1` from the moment its top reaches the viewport top. An\n * article with no layout box at all — a `display: none` ancestor, a collapsed\n * `<details>`, an inactive tab panel — is not measured: an empty rect sits at\n * the document origin and carries no reading position, so publishing anything\n * for it would report a place the reader never reached.\n *\n * The property is written on the controller element **and** on\n * `document.documentElement`, so a fixed bar anywhere in the page can consume\n * it without being a descendant. Both are written through a lease, so an\n * authored declaration comes back on teardown and a later writer is left alone.\n * `complete` fires on *reaching* 1, and never during the baseline: the frame in\n * which the controller connects belongs to establishing where the reader\n * already is (a restored scroll position lands there), not to reading.\n *\n * `change` dispatches `{ progress }`.\n *\n * @remarks\n * Behavior only — the bar itself (and hiding it, e.g. before any scroll) is\n * the consumer's CSS; the progress value carries no ARIA (a decorative\n * indicator — mark the bar `aria-hidden`; a *semantic* progress belongs to\n * `stimeo--progress`). The article's own box is watched as well as the\n * viewport, so content that settles late (images, fonts) re-measures instead of\n * leaving a stale span. Scroll and layout work is rAF-throttled; the listeners,\n * the observers, any pending frame and both leased declarations are released on\n * `disconnect()` and returned again for a `turbo:before-cache` snapshot.\n */\nexport class ReadingProgressController extends Controller<HTMLElement> {\n static events = [\"change\", \"complete\"] as const;\n\n /** Owns both faces of the published property so teardown can hand them back. */\n readonly #lease = new StylePropertyLease(PROGRESS_PROPERTY);\n /** The article's own box and the viewport: either changes the span. */\n readonly #layout = new LayoutObserver(() => this.#onScroll());\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n /** Coalesces scroll bursts, and the connect baseline, into one frame. */\n readonly #frames = new FrameCoalescer();\n /** Last published progress, so `change`/`complete` fire only on movement. */\n #progress = -1;\n /** False until the connect frame has run: `complete` needs real reading. */\n #baselined = false;\n\n readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n\n override connect(): void {\n this.#progress = -1;\n this.#baselined = false;\n // Capture phase: element scrolls do not bubble, but they ARE observable at\n // the window in capture — so an article inside an overflow container still\n // drives the progress.\n window.addEventListener(\"scroll\", this.#onScroll, { passive: true, capture: true });\n this.#layout.observe(this.element);\n this.#layout.observeViewport();\n this.#beforeCache.activate();\n this.#measure();\n // The page's scroll position is restored *after* the controller connects,\n // so that jump arrives as a move the reader never made. Everything up to\n // the end of this frame is still the baseline — a restored scroll coalesces\n // into the frame below, and no reader can cross an article inside one.\n this.#frames.schedule(() => {\n this.#measure();\n this.#baselined = true;\n });\n }\n\n override disconnect(): void {\n window.removeEventListener(\"scroll\", this.#onScroll, { capture: true });\n this.#layout.disconnect();\n this.#beforeCache.deactivate();\n this.#frames.cancel();\n this.#lease.returnAll();\n }\n\n /**\n * Hands both declarations back before the page is snapshotted, so a restored\n * page starts from the authored DOM rather than from someone else's progress.\n * The baseline goes back with them: a cancelled visit leaves this page on\n * screen, and the next measurement has to publish afresh rather than match a\n * value that has already been handed back.\n */\n #rewindForCache(): void {\n this.#frames.cancel();\n this.#lease.returnAll();\n this.#progress = -1;\n }\n\n /** Computes and publishes the progress; emits on movement only. */\n #measure(): void {\n const rect = this.element.getBoundingClientRect();\n // No layout box (a `display: none` ancestor, a collapsed `<details>`): the\n // empty rect sits at the document origin, where the binary branch below\n // would read its `top` of 0 as \"the reader reached it\". There is no reading\n // position to publish, so the last one stands until the article is laid out\n // again — which the box observer reports.\n if (rect.width === 0 && rect.height === 0) return;\n\n const span = rect.height - window.innerHeight;\n // Shorter than the viewport: reading it is binary (reached or not).\n const raw = span > 0 ? -rect.top / span : rect.top <= 0 ? 1 : 0;\n const progress = Math.min(1, Math.max(0, raw));\n if (progress === this.#progress) return;\n\n const previous = this.#progress;\n this.#progress = progress;\n const value = String(progress);\n this.#lease.write(this.element, value);\n this.#lease.write(document.documentElement, value);\n this.dispatch(\"change\", { detail: { progress } });\n if (progress === 1 && previous !== -1 && this.#baselined) this.dispatch(\"complete\");\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/frame_coalescer.ts","../../src/utils/layout_observer.ts","../../src/utils/listener_set.ts","../../src/utils/attribute_write.ts","../../src/utils/shared_registry.ts","../../src/utils/style_property_lease.ts","../../src/controllers/reading_progress_controller.ts"],"names":[],"mappings":"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","file":"reading_progress_controller.js","sourcesContent":["/**\n * Runs one piece of scroll-driven work per animation frame.\n *\n * A `scroll` listener fires many times between two paints, and each firing would\n * measure the same layout. Requesting a frame on the first firing and ignoring\n * the rest until it runs keeps the reads to one per frame; running on the frame\n * — rather than on a timer — keeps the measurement in step with what the reader\n * sees.\n *\n * **The first request of a burst wins.** Work handed in while a frame is already\n * pending is dropped, so a caller that queues a special first pass and then\n * receives scrolls gets that first pass, not the last scroll's.\n *\n * Cancelling drops the pending frame outright, and does nothing at all when none\n * is pending. A frame really is cancelled, so nothing needs a generation to tell\n * a stale callback from a fresh one, and nothing has to be refused before the\n * caller starts listening.\n *\n * Scope is the scheduling only. What to measure stays with the caller.\n *\n * @example\n * ```ts\n * readonly #frames = new FrameCoalescer();\n *\n * readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n *\n * disconnect(): void {\n * this.#frames.cancel();\n * }\n * ```\n */\nexport class FrameCoalescer {\n #frame: number | null = null;\n\n /**\n * Runs `run` on the next frame, unless a frame is already pending — the first\n * request of a burst wins and the rest are dropped. The pending frame is\n * released before `run`, so `run` may request the next one.\n */\n schedule(run: () => void): void {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n run();\n });\n }\n\n /**\n * Drops the pending frame, and reaches the platform only when there is one.\n *\n * There is no handle value that stands for \"nothing pending\":\n * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder\n * arrives as a large positive number that the same allocator can hand out, and\n * an idle cancel would then drop a frame belonging to someone else.\n */\n cancel(): void {\n if (this.#frame === null) return;\n cancelAnimationFrame(this.#frame);\n this.#frame = null;\n }\n}\n","/**\n * Unified layout observation for Stimeo controllers.\n *\n * Widgets whose output is measured — an overflow boundary, a masonry column count,\n * an autosized textarea — have three sources that move the layout under them: their\n * *own* box changing — via {@link ResizeObserver} — the *viewport* changing — via the\n * `window` `resize` event — and a descendant *resource settling*, because an image or\n * a frame reports a height of zero until it has loaded. Wiring those three by hand in\n * every controller risks leaked listeners on `disconnect()`. {@link LayoutObserver}\n * owns all three behind one callback and one\n * {@link LayoutObserver.disconnect | disconnect()} that releases everything.\n *\n * Behavior only: the helper reports *that* layout changed; it never reads or\n * writes styles. Consumers decide what to recompute.\n */\n\n/** Invoked whenever an observed element or the viewport changes size. */\nexport type LayoutCallback = () => void;\n\n/** Constructs a {@link ResizeObserver}; injectable so tests stay deterministic. */\nexport type ResizeObserverFactory = (callback: ResizeObserverCallback) => ResizeObserver;\n\n/** Options for {@link LayoutObserver}. */\nexport interface LayoutObserverOptions {\n /**\n * Factory for the {@link ResizeObserver} used by {@link LayoutObserver.observe}.\n * Defaults to the global constructor; override it in tests, or to no-op in\n * environments where `ResizeObserver` is unavailable.\n */\n resizeObserverFactory?: ResizeObserverFactory;\n}\n\n/**\n * Observes element resizes and/or viewport resizes through a single callback,\n * with guaranteed teardown.\n *\n * @example\n * ```ts\n * #layout = new LayoutObserver(() => this.#reposition());\n *\n * connect() {\n * this.#layout.observe(this.panelTarget);\n * this.#layout.observeViewport();\n * this.#layout.observeDescendantLoads(this.panelTarget);\n * }\n *\n * disconnect() {\n * this.#layout.disconnect();\n * }\n * ```\n */\nexport class LayoutObserver {\n readonly #callback: LayoutCallback;\n readonly #resizeObserverFactory: ResizeObserverFactory | null;\n #resizeObserver: ResizeObserver | null = null;\n #observingViewport = false;\n #loadContainer: Element | null = null;\n\n /** Stable bound handler so add/removeEventListener target the same reference. */\n readonly #handleViewportResize = (): void => {\n this.#callback();\n };\n\n /** Stable bound handler for the capture-phase `load`; see {@link observeDescendantLoads}. */\n readonly #handleDescendantLoad = (): void => {\n this.#callback();\n };\n\n constructor(callback: LayoutCallback, options: LayoutObserverOptions = {}) {\n this.#callback = callback;\n this.#resizeObserverFactory =\n options.resizeObserverFactory ??\n (typeof ResizeObserver === \"undefined\" ? null : (cb) => new ResizeObserver(cb));\n }\n\n /**\n * Starts observing an element's size. Repeated calls observe additional\n * elements through the same shared observer. No-ops when no\n * `ResizeObserver` implementation is available.\n */\n observe(element: Element): void {\n if (!this.#resizeObserverFactory) return;\n if (!this.#resizeObserver) {\n this.#resizeObserver = this.#resizeObserverFactory(() => {\n this.#callback();\n });\n }\n this.#resizeObserver.observe(element);\n }\n\n /** Stops observing a single element while leaving any others in place. */\n unobserve(element: Element): void {\n this.#resizeObserver?.unobserve(element);\n }\n\n /** Starts observing viewport resizes. Idempotent: the listener is added once. */\n observeViewport(): void {\n if (this.#observingViewport) return;\n this.#observingViewport = true;\n window.addEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /** Stops observing viewport resizes without affecting element observation. */\n unobserveViewport(): void {\n if (!this.#observingViewport) return;\n this.#observingViewport = false;\n window.removeEventListener(\"resize\", this.#handleViewportResize);\n }\n\n /**\n * Starts reporting a `load` from anywhere inside `container` — an image or a\n * frame settling changes the box it sits in, and it measures as zero high until\n * then. `load` does not bubble, so the subscription is a capture-phase listener\n * on the container itself and nothing the caller spells.\n *\n * **One container at a time.** A further call moves the observation, so a widget\n * whose content element is swapped at runtime releases the element it let go by\n * naming the new one — there is no second place for the release to drift from.\n */\n observeDescendantLoads(container: Element): void {\n this.unobserveDescendantLoads();\n this.#loadContainer = container;\n container.addEventListener(\"load\", this.#handleDescendantLoad, true);\n }\n\n /** Stops reporting descendant loads without affecting element or viewport observation. */\n unobserveDescendantLoads(): void {\n this.#loadContainer?.removeEventListener(\"load\", this.#handleDescendantLoad, true);\n this.#loadContainer = null;\n }\n\n /**\n * Releases every observation: disconnects the {@link ResizeObserver} and removes\n * the viewport and descendant-load listeners. Safe to call multiple times. Call\n * this from a controller's `disconnect()`.\n */\n disconnect(): void {\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n this.unobserveViewport();\n this.unobserveDescendantLoads();\n }\n}\n","/** What {@link ListenerSet.add} passes on besides the signal it supplies itself. */\nexport type ListenerOptions = Omit<AddEventListenerOptions, \"signal\">;\n\n/**\n * The event map a target's own `addEventListener` types its handlers with: the\n * window's, a document's, an HTML or SVG element's, any other element's, or a\n * media query list's. Other targets name none.\n */\ntype EventMapOf<T extends EventTarget> = T extends Window\n ? WindowEventMap\n : T extends Document\n ? DocumentEventMap\n : T extends HTMLElement\n ? HTMLElementEventMap\n : T extends SVGElement\n ? SVGElementEventMap\n : T extends Element\n ? ElementEventMap\n : T extends MediaQueryList\n ? MediaQueryListEventMap\n : Record<never, never>;\n\n/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt.\n * {@link ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * A lifetime shorter than the connection — while a Value holds, until a first\n * load, while a subscription is open — gets a set of its own, which the end of\n * that lifetime disposes. A controller therefore holds one set per lifetime: the\n * connection's, and one for each swapped receiver or shorter span.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, listeners of\n * several elements that come and go one at a time, and those one gesture holds\n * keep their own pair of calls or a signal of their own.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included. A removal by hand would have to repeat that\n * flag; aborting the signal releases the listener without it.\n *\n * A `type` the target's own event map names types `handler` with that event,\n * as the target's `addEventListener` does, so a handler written for a\n * `KeyboardEvent` is passed as it is; any other `type` takes an\n * `EventListener`.\n */\n add<T extends EventTarget, K extends keyof EventMapOf<T> & string>(\n target: T,\n type: K,\n handler: (event: EventMapOf<T>[K]) => void,\n options?: ListenerOptions,\n ): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Reflects a serialized attribute value only when the current value differs.\n *\n * A same-value `setAttribute` still queues a mutation record for every observer, so an\n * unchanged value writes nothing.\n *\n * @param element - The element whose attribute is reflected.\n * @param name - The attribute name, using the non-namespaced DOM API.\n * @param value - The serialized value, or null to remove the attribute.\n * @param beforeWrite - Called only for a change, before the DOM mutation, so the\n * caller can record ownership or identify its own observer records.\n * @returns Whether an attribute was written or removed.\n */\nexport function setAttributeIfChanged(\n element: Element,\n name: string,\n value: string | null,\n beforeWrite?: () => void,\n): boolean {\n if (element.getAttribute(name) === value) return false;\n beforeWrite?.();\n if (value === null) element.removeAttribute(name);\n else element.setAttribute(name, value);\n return true;\n}\n","/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","import { setAttributeIfChanged } from \"./attribute_write\";\nimport { sharedRegistry } from \"./shared_registry\";\n\n/** What a lease's record says: the authored declaration, the last write and the leases beneath. */\ninterface StylePropertyLeaseRecord {\n readonly originalValue: string;\n readonly originalPriority: string;\n readonly writtenValue: string | null;\n readonly writtenPriority: string;\n /** Whether it hands back what the lease right beneath it wrote last. */\n readonly linked: boolean;\n /** The records of the other owners' leases beneath it, nearest first. */\n readonly beneath: readonly string[];\n}\n\n/** One lease's hold on one element's property, in the stack every lease of it shares. */\ninterface StyleHold {\n /** The lease holding it; `null` for a hold read from a copy's record until its lease takes it. */\n lease: object | null;\n readonly record: string;\n writtenValue: string | null;\n writtenPriority: string;\n /** What it hands back with nothing beneath it: the author's declaration. */\n readonly originalValue: string;\n readonly originalPriority: string;\n /** Whether it hands back what the hold beneath it wrote last instead. */\n linked: boolean;\n}\n\n/** The holds on each element by property, and every lease's record name by property. */\ninterface StylePropertyLeaseRegistry {\n readonly stacks: WeakMap<HTMLElement, Map<string, StyleHold[]>>;\n readonly names: Map<string, Set<string>>;\n}\n\n/**\n * Temporarily controls one inline CSS property across a changing set of elements.\n *\n * Authored value and priority are restored only while the declaration still matches\n * the last leased write. A later consumer write therefore wins.\n *\n * **Every lease of a property shares one stack of holds per element**, whatever its owner\n * and whichever file of the package constructed it (the stacks are one per page): the holds\n * sit in the order their leases first wrote the element, each with the declaration it wrote\n * last. The element's declaration is the highest hold's that carries it, and the page's when\n * none does. A write reaches the element unless a hold above the writer carries the element's\n * declaration, so the top hold always shows its writes and the page's declaration gives way to\n * the next write of any hold. A return gives a declaration back only from the hold the\n * element's declaration is: the one the hold beneath it wrote last, or the author's once\n * nothing is beneath it; any other return only leaves the stack. Holds that wrote the same\n * declaration are told apart by the stack: the higher one keeps it. A first write over a top\n * hold whose declaration the page has replaced takes the element's declaration as the\n * author's. Two instances of one controller that write the same property on\n * `document.documentElement` stack the same way.\n *\n * **The stack travels with the element.** While a hold carries a declaration other than the\n * author's, lies on another hold or is covered by one, the element carries its owner's record\n * `data-<owner>-style-<property>-lease`: JSON of the author's value and priority, the value\n * and priority the hold wrote last (`null` for a removal), `false` where the hold hands back\n * the author's declaration rather than the latest one of the hold beneath it, and the records\n * of every hold beneath it, nearest first. Returning the last lease of an owner removes its\n * record. A copy of the element — a page Turbo restores from its cache — therefore carries the\n * stack, read back under the record names of the leases constructed so far and ordered by the\n * records each hold lists beneath it, and each lease takes the hold its owner's record\n * describes over in place. Complete records for distinct owners preserve their order across\n * a copy. Records describe owners, not instances: one owner holding the element more than\n * once leaves only its topmost hold's record, which the first of its leases to write or return\n * on the copy takes. Its lower holds and positions are absent; mutually listed owners cannot\n * express their full live order. A hold that showed the author's declaration alone carries\n * no record and is taken by its first write on the copy. Missing or unreadable records also\n * leave their holds unknown, so copying the DOM does not promise identical decisions for\n * every live stack. A new stack with no readable record takes the current declaration as the\n * author's; a morph can remove records while existing holds remain in memory until returned.\n *\n * The lease has no lifecycle of its own, so it never subscribes to document events.\n */\nexport class StylePropertyLease<T extends HTMLElement = HTMLElement> {\n /** The holds on each element, bottom first, and the record names; one per page. */\n static readonly #registry = sharedRegistry(\n \"stimeo-ui.style-property-lease.registry.v1\",\n (): StylePropertyLeaseRegistry => ({ stacks: new WeakMap(), names: new Map() }),\n );\n readonly #property: string;\n readonly #record: string;\n readonly #held = new Set<T>();\n\n /**\n * @param property - The CSS property whose temporary values this lease owns.\n * @param owner - Names the record on the element; the controller's identifier.\n */\n constructor(property: string, owner: string) {\n this.#property = property;\n this.#record = `data-${owner}-style-${property}-lease`;\n const names = StylePropertyLease.#registry.names;\n names.set(property, (names.get(property) ?? new Set<string>()).add(this.#record));\n }\n\n /** Writes or removes the leased declaration while preserving its authored value. */\n write(element: T, value: string | null, priority = \"\"): void {\n const stack = this.#stack(element);\n const hold = this.#take(stack) ?? this.#acquire(element, stack);\n this.#held.add(element);\n const shows = !this.#covered(element, stack, hold);\n hold.writtenValue = value;\n hold.writtenPriority = value === null ? \"\" : priority;\n this.#mark(element, stack);\n if (shows) this.#show(element, value, hold.writtenPriority);\n }\n\n /**\n * Returns one lease, also on a restored copy, to the declaration the hold beneath it wrote\n * last or the author's, without overwriting a declaration a consumer wrote since.\n */\n return(element: T): void {\n const held = this.#held.delete(element);\n const known = StylePropertyLease.#registry.stacks.get(element)?.has(this.#property);\n const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];\n const hold = this.#take(stack);\n if (!hold) return;\n const owns = this.#carries(element, hold) && !this.#covered(element, stack, hold);\n const index = stack.indexOf(hold);\n stack.splice(index, 1);\n const above = stack[index];\n if (above) above.linked &&= hold.linked;\n this.#mark(element, stack);\n if (!owns) return;\n const below = hold.linked ? stack[index - 1] : undefined;\n if (below) this.#show(element, below.writtenValue, below.writtenPriority);\n else this.#show(element, hold.originalValue, hold.originalPriority);\n }\n\n /** Returns every outstanding declaration lease. */\n returnAll(): void {\n for (const element of Array.from(this.#held)) this.return(element);\n }\n\n /** The record `name` on `element`, or `null` when it is none a lease could write. */\n #recorded(element: T, name: string): StylePropertyLeaseRecord | null {\n try {\n const parsed: unknown = JSON.parse(element.getAttribute(name) ?? \"null\");\n if (!Array.isArray(parsed)) return null;\n const [originalValue, originalPriority, writtenValue, writtenPriority, ...rest] =\n parsed as unknown[];\n if (typeof originalValue !== \"string\" || typeof originalPriority !== \"string\") return null;\n if (typeof writtenValue !== \"string\" && writtenValue !== null) return null;\n if (typeof writtenPriority !== \"string\") return null;\n const linked = rest[0] !== false;\n const beneath = linked ? rest : rest.slice(1);\n if (!beneath.every((under): under is string => typeof under === \"string\")) return null;\n return { originalValue, originalPriority, writtenValue, writtenPriority, linked, beneath };\n } catch {\n return null;\n }\n }\n\n /** The element's stack, read from its records the first time. */\n #stack(element: T): StyleHold[] {\n const stacks = StylePropertyLease.#registry.stacks;\n const byProperty = stacks.get(element) ?? new Map<string, StyleHold[]>();\n stacks.set(element, byProperty);\n const stack = byProperty.get(this.#property) ?? this.#seed(element);\n byProperty.set(this.#property, stack);\n return stack;\n }\n\n /** The holds a copy's records describe, each above every hold its record lists beneath. */\n #seed(element: T): StyleHold[] {\n const found: (StylePropertyLeaseRecord & { readonly record: string })[] = [];\n for (const record of StylePropertyLease.#registry.names.get(this.#property) ?? []) {\n const read = this.#recorded(element, record);\n if (read) found.push({ ...read, record });\n }\n const depth = ({ beneath }: (typeof found)[number]) =>\n beneath.filter((name) => found.some((other) => other.record === name)).length;\n return found\n .sort((one, other) => depth(one) - depth(other))\n .map((entry, index) => ({\n lease: null,\n record: entry.record,\n writtenValue: entry.writtenValue,\n writtenPriority: entry.writtenPriority,\n originalValue: entry.originalValue,\n originalPriority: entry.originalPriority,\n linked: entry.linked && index > 0,\n }));\n }\n\n /** Its hold in `stack`, taking over one read from its record. */\n #take(stack: StyleHold[]): StyleHold | undefined {\n const hold =\n stack.find((held) => held.lease === this) ??\n stack.find((held) => held.lease === null && held.record === this.#record);\n if (hold) hold.lease = this;\n return hold;\n }\n\n /** A new hold on top, over the top hold while the element carries its declaration. */\n #acquire(element: T, stack: StyleHold[]): StyleHold {\n const top = stack.at(-1);\n const over = top && this.#carries(element, top) ? top : undefined;\n const style = element.style;\n const hold: StyleHold = {\n lease: this,\n record: this.#record,\n writtenValue: null,\n writtenPriority: \"\",\n originalValue: over ? over.originalValue : style.getPropertyValue(this.#property),\n originalPriority: over ? over.originalPriority : style.getPropertyPriority(this.#property),\n linked: over !== undefined,\n };\n stack.push(hold);\n return hold;\n }\n\n /** Whether a hold above `hold` carries the element's declaration, then that hold's. */\n #covered(element: T, stack: readonly StyleHold[], hold: StyleHold): boolean {\n return stack.slice(stack.indexOf(hold) + 1).some((above) => this.#carries(element, above));\n }\n\n /** Leaves each owner's record of its topmost hold, or none where nothing needs one. */\n #mark(element: T, stack: readonly StyleHold[]): void {\n const records = new Map<string, string | null>([[this.#record, null]]);\n stack.forEach((hold, index) => {\n const names = stack.slice(0, index).map((under) => under.record);\n const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);\n const marker = hold.linked || beneath.length === 0 ? [] : [false];\n const { originalValue, originalPriority, writtenValue, writtenPriority } = hold;\n records.set(\n hold.record,\n (writtenValue ?? \"\") === originalValue &&\n writtenPriority === originalPriority &&\n beneath.length === 0 &&\n index === stack.length - 1\n ? null\n : JSON.stringify([\n originalValue,\n originalPriority,\n writtenValue,\n writtenPriority,\n ...marker,\n ...beneath,\n ]),\n );\n });\n for (const [name, recorded] of records) {\n setAttributeIfChanged(element, name, recorded);\n }\n }\n\n /** Whether the element carries the declaration `hold` wrote last. */\n #carries(element: T, hold: StyleHold): boolean {\n return this.#holds(element, hold.writtenValue ?? \"\", hold.writtenPriority);\n }\n\n /** Whether the inline declaration is `value` with `priority`. */\n #holds(element: T, value: string, priority: string): boolean {\n const style = element.style;\n return (\n style.getPropertyValue(this.#property) === value &&\n style.getPropertyPriority(this.#property) === priority\n );\n }\n\n /** Writes the declaration, or removes it for `null`, unless the element already carries it. */\n #show(element: T, value: string | null, priority: string): void {\n if (this.#holds(element, value ?? \"\", priority)) return;\n if (value === null) element.style.removeProperty(this.#property);\n else element.style.setProperty(this.#property, value, priority);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\nimport { LayoutObserver } from \"../utils/layout_observer\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { StylePropertyLease } from \"../utils/style_property_lease\";\n\n/** Name of the CSS custom property exposing the reading progress (0..1). */\nconst PROGRESS_PROPERTY = \"--stimeo--reading-progress\";\n\n/**\n * Headless **reading progress**: how far the reader has scrolled *through* this\n * element (an article), published as a CSS custom property and a `change`\n * event stream — the classic top-of-page progress bar.\n * `IntersectionObserver` alone cannot express this (the ratio is constant\n * while a tall article scrolls through the viewport), so this controller owns\n * the scroll math; compose with `stimeo--intersection` when you also need\n * enter/exit triggers. Core (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--reading-progress`):\n * <article data-controller=\"stimeo--reading-progress\">…</article>\n * <div class=\"progress-bar\" aria-hidden=\"true\"></div>\n * <!-- .progress-bar { width: calc(var(--stimeo--reading-progress, 0) * 100%); } -->\n *\n * Progress is `0` before the article's top reaches the viewport top and `1`\n * once its bottom fits the viewport: `-top / (height - viewportHeight)`,\n * clamped. An article no taller than the viewport has no such span, so reading\n * it is binary: `1` from the moment its top reaches the viewport top. An\n * article with no layout box at all — a `display: none` ancestor, a collapsed\n * `<details>`, an inactive tab panel — is not measured: an empty rect sits at\n * the document origin and carries no reading position, so publishing anything\n * for it would report a place the reader never reached.\n *\n * The property is written on the controller element **and** on\n * `document.documentElement`, so a fixed bar anywhere in the page can consume\n * it without being a descendant. Both are written through a lease, so an\n * authored declaration comes back on teardown and a later writer is left alone.\n * `complete` fires on *reaching* 1, and never during the baseline: the frame in\n * which the controller connects belongs to establishing where the reader\n * already is (a restored scroll position lands there), not to reading.\n *\n * `change` dispatches `{ progress }`.\n *\n * @remarks\n * Behavior only — the bar itself (and hiding it, e.g. before any scroll) is\n * the consumer's CSS; the progress value carries no ARIA (a decorative\n * indicator — mark the bar `aria-hidden`; a *semantic* progress belongs to\n * `stimeo--progress`). The article's own box is watched as well as the\n * viewport, so content that settles late (images, fonts) re-measures instead of\n * leaving a stale span. Scroll and layout work is rAF-throttled; the listeners,\n * the observers, any pending frame and both leased declarations are released on\n * `disconnect()`.\n */\nexport class ReadingProgressController extends Controller<HTMLElement> {\n static events = [\"change\", \"complete\"] as const;\n\n /** Owns both faces of the published property so teardown can hand them back. */\n readonly #lease = new StylePropertyLease(PROGRESS_PROPERTY, this.identifier);\n /** The article's own box and the viewport: either changes the span. */\n readonly #layout = new LayoutObserver(() => this.#onScroll());\n /** Coalesces scroll bursts, and the connect baseline, into one frame. */\n readonly #frames = new FrameCoalescer();\n /** Last published progress, so `change`/`complete` fire only on movement. */\n #progress = -1;\n /** False until the connect frame has run: `complete` needs real reading. */\n #baselined = false;\n\n readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n\n /** The listeners of one connection, released together on `disconnect()`. */\n readonly #listeners = new ListenerSet();\n\n override connect(): void {\n this.#progress = -1;\n this.#baselined = false;\n // Capture phase: element scrolls do not bubble, but they ARE observable at\n // the window in capture — so an article inside an overflow container still\n // drives the progress.\n this.#listeners.add(window, \"scroll\", this.#onScroll, { passive: true, capture: true });\n this.#layout.observe(this.element);\n this.#layout.observeViewport();\n this.#measure();\n // The page's scroll position is restored *after* the controller connects,\n // so that jump arrives as a move the reader never made. Everything up to\n // the end of this frame is still the baseline — a restored scroll coalesces\n // into the frame below, and no reader can cross an article inside one.\n this.#frames.schedule(() => {\n this.#measure();\n this.#baselined = true;\n });\n }\n\n override disconnect(): void {\n this.#listeners.dispose();\n this.#layout.disconnect();\n this.#frames.cancel();\n this.#lease.return(this.element);\n this.#lease.return(document.documentElement);\n }\n\n /** Computes and publishes the progress; emits on movement only. */\n #measure(): void {\n const rect = this.element.getBoundingClientRect();\n // No layout box (a `display: none` ancestor, a collapsed `<details>`): the\n // empty rect sits at the document origin, where the binary branch below\n // would read its `top` of 0 as \"the reader reached it\". There is no reading\n // position to publish, so the last one stands until the article is laid out\n // again — which the box observer reports.\n if (rect.width === 0 && rect.height === 0) return;\n\n const span = rect.height - window.innerHeight;\n // Shorter than the viewport: reading it is binary (reached or not).\n const raw = span > 0 ? -rect.top / span : rect.top <= 0 ? 1 : 0;\n const progress = Math.min(1, Math.max(0, raw));\n if (progress === this.#progress) return;\n\n const previous = this.#progress;\n this.#progress = progress;\n const value = String(progress);\n this.#lease.write(this.element, value);\n this.#lease.write(document.documentElement, value);\n this.dispatch(\"change\", { detail: { progress } });\n if (progress === 1 && previous !== -1 && this.#baselined) this.dispatch(\"complete\");\n }\n}\n"]}
@@ -22,12 +22,15 @@ import { Controller } from '@hotwired/stimulus';
22
22
  * timer is owned by `SafeTimeout` and torn down on `disconnect()` (Turbo
23
23
  * navigation included).
24
24
  *
25
- * Render inputs are followed at runtime: a morph that swaps `locale`, `threshold`,
26
- * or `tickInterval` on the live element repaints through one coalesced pass
27
- * (`MicrotaskCoalescer`) rather than leaving the reading frozen. The authored
28
- * absolute text goes back on the element via `BeforeCacheReset` before Turbo
29
- * caches the page — a snapshot taken mid-render would carry the relative form
30
- * alone, and that is a form `connect()` must not adopt as the fallback.
25
+ * Render inputs are followed at runtime: changing `datetime` or a morph that swaps
26
+ * `locale`, `threshold`, or `tickInterval` on the live element repaints through one
27
+ * coalesced pass (`MorphRenderWatcher`) rather than leaving the reading frozen. While
28
+ * the element shows the relative form it records the authored absolute text on itself as
29
+ * `data-<identifier>-text` (a JSON string), so a copy of it — a page Turbo restores from
30
+ * its cache — keeps the threshold fallback: the connection that adopts the copy takes
31
+ * the authored text from the record, never the relative form on display. Nothing is
32
+ * written back on `turbo:before-cache`, which Turbo also dispatches on pages that stay,
33
+ * where the reading must stay current.
31
34
  */
32
35
  declare class RelativeTimeController extends Controller<HTMLElement> {
33
36
  #private;
@@ -45,6 +48,17 @@ declare class RelativeTimeController extends Controller<HTMLElement> {
45
48
  default: number;
46
49
  };
47
50
  };
51
+ static valueConstraints: {
52
+ threshold: {
53
+ readonly finite: true;
54
+ readonly min: 0;
55
+ };
56
+ tickInterval: {
57
+ readonly finite: true;
58
+ readonly min: 0;
59
+ readonly max: 2147483647;
60
+ };
61
+ };
48
62
  localeValue: string;
49
63
  thresholdValue: number;
50
64
  tickIntervalValue: number;