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,8 +1,45 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/local_time_controller.ts
3
+ var DATETIME = /^([+-]\d{6}|\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
4
+ function readDatetime(element) {
5
+ const raw = element.getAttribute("datetime");
6
+ if (raw === null) return null;
7
+ const match = DATETIME.exec(raw.trim());
8
+ if (match === null) return null;
9
+ const [
10
+ ,
11
+ yearText,
12
+ monthText,
13
+ dayText,
14
+ hourText = "00",
15
+ minuteText = "00",
16
+ secondText = "00",
17
+ fraction = "",
18
+ zone = "Z"
19
+ ] = match;
20
+ const year = Number(yearText);
21
+ const month = Number(monthText);
22
+ const day = Number(dayText);
23
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
24
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
25
+ if (month < 1 || month > 12 || day < 1 || day > (days[month - 1] ?? 0)) return null;
26
+ const hour = Number(hourText);
27
+ const minute = Number(minuteText);
28
+ const second = Number(secondText);
29
+ if (hour === 24 && (minute !== 0 || second !== 0 || /[1-9]/.test(fraction))) return null;
30
+ let offset = zone;
31
+ if (zone !== "Z") {
32
+ const digits = zone.slice(1).replace(":", "");
33
+ const offsetHour = digits.slice(0, 2);
34
+ const offsetMinute = digits.slice(2);
35
+ offset = `${zone[0]}${offsetHour}:${offsetMinute}`;
36
+ }
37
+ const milliseconds = fraction.slice(0, 3).padEnd(3, "0");
38
+ const canonical = `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}.${milliseconds}${offset}`;
39
+ const instant = Date.parse(canonical);
40
+ return Number.isNaN(instant) ? null : instant;
41
+ }
4
42
 
5
- // src/utils/intl_format.ts
6
43
  var formatters = /* @__PURE__ */ new WeakMap();
7
44
  function intlFormatter(factory, locale, options, fallbackLocale) {
8
45
  const cache = cacheFor(factory);
@@ -31,32 +68,40 @@ function construct(factory, locale, options, fallbackLocale) {
31
68
  }
32
69
  }
33
70
 
34
- // src/utils/locale.ts
35
71
  function resolveLocale(element, declared = "") {
36
72
  return declared || element.closest("[lang]")?.getAttribute("lang") || void 0;
37
73
  }
38
74
 
39
- // src/utils/microtask_coalescer.ts
75
+ var ListenerSet = class {
76
+ #abort = new AbortController();
77
+ add(target, type, handler, options) {
78
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
79
+ }
80
+ dispose() {
81
+ this.#abort.abort();
82
+ this.#abort = new AbortController();
83
+ }
84
+ };
85
+
40
86
  var MicrotaskCoalescer = class {
41
87
  #run;
42
88
  #queued = false;
43
89
  #active = false;
44
90
  #generation = 0;
45
- /** @param run - the single reconciliation pass, invoked at most once per batch. */
46
91
  constructor(run) {
47
92
  this.#run = run;
48
93
  }
49
- /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
50
94
  activate() {
51
95
  this.#active = true;
52
96
  }
53
- /** Closes the window and drops any pending pass; call from `disconnect()`. */
54
97
  cancel() {
55
98
  this.#active = false;
56
99
  this.#queued = false;
57
100
  this.#generation += 1;
58
101
  }
59
- /** Requests one pass after the batch settles. Idempotent; inert outside the window. */
102
+ get pending() {
103
+ return this.#queued;
104
+ }
60
105
  schedule() {
61
106
  if (!this.#active || this.#queued) return;
62
107
  this.#queued = true;
@@ -69,7 +114,34 @@ var MicrotaskCoalescer = class {
69
114
  }
70
115
  };
71
116
 
72
- // src/controllers/local_time_controller.ts
117
+ var MorphRenderWatcher = class {
118
+ #listeners = new ListenerSet();
119
+ #pass;
120
+ #element = null;
121
+ #onMorph = () => this.#pass.schedule();
122
+ constructor(run) {
123
+ this.#pass = new MicrotaskCoalescer(run);
124
+ }
125
+ observe(element) {
126
+ if (this.#element === element) return;
127
+ this.disconnect();
128
+ this.#element = element;
129
+ this.#pass.activate();
130
+ this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
131
+ }
132
+ schedule() {
133
+ this.#pass.schedule();
134
+ }
135
+ get pending() {
136
+ return this.#pass.pending;
137
+ }
138
+ disconnect() {
139
+ this.#listeners.dispose();
140
+ this.#pass.cancel();
141
+ this.#element = null;
142
+ }
143
+ };
144
+
73
145
  var STYLES = /* @__PURE__ */ new Set(["full", "long", "medium", "short"]);
74
146
  function toStyle(value) {
75
147
  return STYLES.has(value) ? value : void 0;
@@ -83,55 +155,48 @@ var LocalTimeController = class extends Controller {
83
155
  titleFormat: { type: String, default: "" }
84
156
  };
85
157
  static events = ["format"];
86
- /** Collapses a morph that swaps several render inputs at once into one repaint. */
87
- #resync = new MicrotaskCoalescer(() => this.#render());
88
- /**
89
- * Watches the one render input that is not a Value. Only `datetime` is filtered
90
- * in, so the text and `title` this controller writes cannot re-enter the pass.
91
- */
158
+ #resync = new MorphRenderWatcher(() => {
159
+ const datetimeChanged = this.#datetimeWatch.takeRecords().length > 0;
160
+ const notify = this.#formatPending || datetimeChanged;
161
+ this.#formatPending = false;
162
+ this.#render(notify);
163
+ });
164
+ #formatPending = false;
92
165
  #datetimeWatch = new MutationObserver(() => {
166
+ this.#formatPending = true;
93
167
  this.#resync.schedule();
94
168
  });
95
169
  connect() {
96
- this.#resync.activate();
170
+ this.#resync.observe(this.element);
97
171
  this.#datetimeWatch.observe(this.element, { attributeFilter: ["datetime"] });
172
+ this.#formatPending = false;
98
173
  this.#render();
99
174
  }
100
175
  disconnect() {
101
- this.#resync.cancel();
176
+ this.#resync.disconnect();
102
177
  this.#datetimeWatch.disconnect();
103
178
  }
104
- /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */
105
179
  localeValueChanged() {
180
+ this.#formatPending = true;
106
181
  this.#resync.schedule();
107
182
  }
108
- /** Repaints when application code (or a Turbo morph) changes `timeZone` at runtime. */
109
183
  timeZoneValueChanged() {
184
+ this.#formatPending = true;
110
185
  this.#resync.schedule();
111
186
  }
112
- /** Repaints when application code (or a Turbo morph) changes `dateStyle` at runtime. */
113
187
  dateStyleValueChanged() {
188
+ this.#formatPending = true;
114
189
  this.#resync.schedule();
115
190
  }
116
- /** Repaints when application code (or a Turbo morph) changes `timeStyle` at runtime. */
117
191
  timeStyleValueChanged() {
192
+ this.#formatPending = true;
118
193
  this.#resync.schedule();
119
194
  }
120
- /** Repaints when application code (or a Turbo morph) changes `titleFormat` at runtime. */
121
195
  titleFormatValueChanged() {
196
+ this.#formatPending = true;
122
197
  this.#resync.schedule();
123
198
  }
124
- /**
125
- * Formats the instant in `datetime` against the current Values and writes it out.
126
- *
127
- * The `format` event rides with every pass, including a repaint a morph triggers:
128
- * its condition is that formatting was applied, and a repaint applies it with a
129
- * new result. A pass that cannot format writes nothing and emits nothing, so the
130
- * authored absolute text stays as the fallback.
131
- *
132
- * @stimeoRenderRoot
133
- */
134
- #render() {
199
+ #render(notify = true) {
135
200
  const date = this.#parse();
136
201
  if (date === null) return;
137
202
  const formatted = this.#applyFormat(date, this.dateStyleValue, this.timeStyleValue);
@@ -139,55 +204,16 @@ var LocalTimeController = class extends Controller {
139
204
  this.element.textContent = formatted;
140
205
  const title = this.#title(date);
141
206
  if (title !== null) this.element.setAttribute("title", title);
142
- this.dispatch("format", { detail: { formatted } });
207
+ if (notify) this.dispatch("format", { detail: { formatted } });
143
208
  }
144
- /**
145
- * Parses the UTC `datetime` attribute into a {@link Date}, or `null`. Whitespace
146
- * around the attribute value is tolerated.
147
- */
148
209
  #parse() {
149
- const raw = this.element.getAttribute("datetime");
150
- if (!raw) return null;
151
- const ms = Date.parse(this.#asUtc(raw.trim()));
152
- return Number.isNaN(ms) ? null : new Date(ms);
153
- }
154
- /**
155
- * Reads a timezone-less date-time as UTC — the input contract of this
156
- * controller — since `Date.parse` would otherwise read `"2026-06-08T12:30:00"` in
157
- * the *runtime's* local zone, contradicting "the server emits UTC". Values that
158
- * already carry `Z` or a `±hh:mm` offset (and bare `YYYY-MM-DD` dates, already
159
- * parsed as UTC) are returned unchanged.
160
- *
161
- * HTML accepts a space where ISO 8601 wants `T`, and `Date.parse` of that form is
162
- * left to each engine, so a whole value shaped that way is normalized to the `T`
163
- * separator first. The pattern is anchored: a value trailing anything else — a
164
- * zone word such as `"2026-06-08 12:30:00 UTC"` — is handed to `Date.parse` as
165
- * authored instead of being turned into a string nothing can parse.
166
- */
167
- #asUtc(value) {
168
- const isoLike = value.replace(
169
- /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)$/,
170
- "$1T$2"
171
- );
172
- const hasTime = /T\d{2}:\d{2}/.test(isoLike);
173
- const hasZone = /(Z|[+-]\d{2}:?\d{2})$/.test(isoLike);
174
- return hasTime && !hasZone ? `${isoLike}Z` : isoLike;
175
- }
176
- /**
177
- * Builds the optional detailed `title`. `titleFormat` is an `Intl` style
178
- * keyword applied to *both* date and time; empty (the default) adds no title.
179
- */
210
+ const instant = readDatetime(this.element);
211
+ return instant === null ? null : new Date(instant);
212
+ }
180
213
  #title(date) {
181
214
  if (this.titleFormatValue.length === 0) return null;
182
215
  return this.#applyFormat(date, this.titleFormatValue, this.titleFormatValue);
183
216
  }
184
- /**
185
- * Formats `date` with `Intl.DateTimeFormat`, including each style only when it
186
- * is a valid keyword (so a consumer can show date-only or time-only by clearing
187
- * the other). Returns `null` when neither style is usable or the declaration
188
- * names a locale or time zone `Intl` rejects, so the caller can leave the
189
- * authored text untouched.
190
- */
191
217
  #applyFormat(date, dateStyle, timeStyle) {
192
218
  const options = {
193
219
  dateStyle: toStyle(dateStyle),
@@ -205,5 +231,4 @@ var LocalTimeController = class extends Controller {
205
231
  };
206
232
 
207
233
  export { LocalTimeController };
208
- //# sourceMappingURL=local_time_controller.js.map
209
234
  //# sourceMappingURL=local_time_controller.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/intl_format.ts","../../src/utils/locale.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/local_time_controller.ts"],"names":[],"mappings":"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","file":"local_time_controller.js","sourcesContent":["/**\n * Construction of `Intl` formatters for the controllers that take a locale, and\n * the options that go with it, from a declaration.\n *\n * Every `Intl` constructor resolves locale data on the way in, which makes it\n * too costly to rebuild on a hot path, and rejects a malformed language tag,\n * currency code, time zone or digit count with a `RangeError`. Both concerns are\n * handled here once. A formatter is built at most once per distinct request and\n * shared by every caller that asks for the same one. A `RangeError` is caught\n * and answered the way the caller asked: by rebuilding under a fallback locale,\n * or by returning `null` so the caller can leave its authored text alone. Any\n * other failure is a programming fault and propagates.\n */\n\n/** The constructor shape the `Intl` formatter classes share. */\nexport interface IntlFormatterConstructor<F, O extends object> {\n new (locales?: string | string[], options?: O): F;\n}\n\n/** Built formatters, and refusals, per constructor; see `cacheKey` for the key. */\nconst formatters = new WeakMap<object, Map<string, unknown>>();\n\n/**\n * A formatter for `locale` with `options`.\n *\n * With `fallbackLocale`, a `RangeError` from the constructor is answered by\n * building the same options under the fallback instead, so the result is never\n * `null`; the fallback's own failure propagates. Without it, a `RangeError`\n * yields `null` and the caller decides what to show. Errors of any other kind\n * propagate in both forms.\n *\n * Results are cached per constructor and `(locale, options, fallback)`, `null`\n * included, so asking again on every render costs one map lookup.\n */\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string,\n): F;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n): F | null;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale?: string,\n): F | null {\n const cache = cacheFor(factory);\n const key = cacheKey(locale, options, fallbackLocale);\n if (cache.has(key)) return cache.get(key) as F | null;\n const formatter = construct(factory, locale, options, fallbackLocale);\n cache.set(key, formatter);\n return formatter;\n}\n\n/** The cache for one constructor, created on first use. */\nfunction cacheFor(factory: object): Map<string, unknown> {\n const existing = formatters.get(factory);\n if (existing) return existing;\n const created = new Map<string, unknown>();\n formatters.set(factory, created);\n return created;\n}\n\n/**\n * One key per distinct request. `JSON.stringify` drops an option left\n * `undefined`, which is how the constructor reads it too. Two spellings of the\n * same options differing only in key order key separately; that costs one extra\n * formatter, never a wrong one, and call sites pass a fixed literal anyway.\n */\nfunction cacheKey(\n locale: string | undefined,\n options: object,\n fallbackLocale: string | undefined,\n): string {\n return JSON.stringify([locale, options, fallbackLocale]);\n}\n\n/** Builds the formatter under the `RangeError` contract described on `intlFormatter`. */\nfunction construct<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string | undefined,\n): F | null {\n try {\n return new factory(locale, options);\n } catch (error) {\n if (!(error instanceof RangeError)) throw error;\n if (fallbackLocale === undefined) return null;\n return new factory(fallbackLocale, options);\n }\n}\n","/**\n * The locale a component formats in: its own declaration, else the language of\n * the text around it, else the runtime default.\n *\n * `lang` is inherited in HTML, so the language that applies to an element is the\n * nearest one on the element itself or on an ancestor — not the document\n * element's, which is only the farthest of them. An empty `lang` there means the\n * language is unknown and stops the inheritance at that point, so it resolves to\n * the runtime default rather than to a `lang` further up.\n *\n * Resolution only, and the value is passed through as authored. Whether `Intl`\n * accepts the tag, and what to show when it does not, is the caller's contract.\n *\n * @example\n * ```ts\n * intlFormatter(Intl.DateTimeFormat, resolveLocale(this.element, this.localeValue), options);\n * ```\n */\nexport function resolveLocale(element: Element, declared = \"\"): string | undefined {\n return declared || element.closest(\"[lang]\")?.getAttribute(\"lang\") || undefined;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\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 #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { intlFormatter } from \"../utils/intl_format\";\nimport { resolveLocale } from \"../utils/locale\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** A date/time style keyword accepted by `Intl` `dateStyle` / `timeStyle`. */\ntype DateTimeStyle = \"full\" | \"long\" | \"medium\" | \"short\";\n\n/** The valid `Intl` style keywords, used to validate the string-typed values. */\nconst STYLES = new Set<DateTimeStyle>([\"full\", \"long\", \"medium\", \"short\"]);\n\n/** Narrows an arbitrary string to a valid {@link DateTimeStyle}, or `undefined`. */\nfunction toStyle(value: string): DateTimeStyle | undefined {\n return (STYLES as Set<string>).has(value) ? (value as DateTimeStyle) : undefined;\n}\n\n/**\n * Headless local-time behavior: renders the UTC instant in a `<time datetime>`\n * as an absolute, viewer-localized string via `Intl.DateTimeFormat`. No\n * dedicated APG pattern; it follows the HTML `<time>` semantics.\n *\n * Markup contract (identifier: `stimeo--local-time`):\n * <time datetime=\"2026-06-08T12:30:00Z\"\n * data-controller=\"stimeo--local-time\"\n * data-stimeo--local-time-date-style-value=\"medium\"\n * data-stimeo--local-time-time-style-value=\"short\">2026-06-08 12:30 UTC</time>\n *\n * The server emits UTC (cache-friendly — it never needs to know the viewer's\n * timezone), and the controller reformats the visible text into the viewer's\n * locale/zone on connect. This is the *absolute* localization axis, distinct from\n * {@link RelativeTimeController}'s \"3 minutes ago\".\n *\n * `format` dispatches `{ formatted }`.\n *\n * @remarks\n * Behavior only. The machine-readable `datetime` (UTC) is left untouched so\n * assistive tech and crawlers keep the canonical value while only the display\n * text — and an optional `title` — change. Formatting is a pure function of\n * `datetime` with no module-scope state or timers, so a Turbo Drive cache restore\n * re-runs `connect()` and stays consistent. A `datetime` that does not parse, and\n * a locale or time zone `Intl` rejects, leave the authored absolute text in place;\n * any other failure surfaces rather than being absorbed.\n *\n * Render inputs are followed at runtime: a morph that swaps a Value or the\n * `datetime` attribute on the live element — which keeps the element, so\n * `connect()` never runs again — repaints through one coalesced pass\n * (`MicrotaskCoalescer`) rather than leaving a stale reading on screen.\n */\nexport class LocalTimeController extends Controller<HTMLElement> {\n static override values = {\n locale: { type: String, default: \"\" },\n timeZone: { type: String, default: \"\" },\n dateStyle: { type: String, default: \"medium\" },\n timeStyle: { type: String, default: \"short\" },\n titleFormat: { type: String, default: \"\" },\n };\n static events = [\"format\"] as const;\n\n declare localeValue: string;\n declare timeZoneValue: string;\n declare dateStyleValue: string;\n declare timeStyleValue: string;\n declare titleFormatValue: string;\n\n /** Collapses a morph that swaps several render inputs at once into one repaint. */\n readonly #resync = new MicrotaskCoalescer(() => this.#render());\n /**\n * Watches the one render input that is not a Value. Only `datetime` is filtered\n * in, so the text and `title` this controller writes cannot re-enter the pass.\n */\n readonly #datetimeWatch = new MutationObserver(() => {\n this.#resync.schedule();\n });\n\n override connect(): void {\n this.#resync.activate();\n this.#datetimeWatch.observe(this.element, { attributeFilter: [\"datetime\"] });\n this.#render();\n }\n\n override disconnect(): void {\n this.#resync.cancel();\n this.#datetimeWatch.disconnect();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */\n localeValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeZone` at runtime. */\n timeZoneValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `dateStyle` at runtime. */\n dateStyleValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeStyle` at runtime. */\n timeStyleValueChanged(): void {\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `titleFormat` at runtime. */\n titleFormatValueChanged(): void {\n this.#resync.schedule();\n }\n\n /**\n * Formats the instant in `datetime` against the current Values and writes it out.\n *\n * The `format` event rides with every pass, including a repaint a morph triggers:\n * its condition is that formatting was applied, and a repaint applies it with a\n * new result. A pass that cannot format writes nothing and emits nothing, so the\n * authored absolute text stays as the fallback.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n const date = this.#parse();\n if (date === null) return;\n\n const formatted = this.#applyFormat(date, this.dateStyleValue, this.timeStyleValue);\n if (formatted === null) return;\n\n // Only the visible text (and optional title) change; `datetime` is the\n // immutable machine-readable source and is never rewritten.\n this.element.textContent = formatted;\n\n const title = this.#title(date);\n if (title !== null) this.element.setAttribute(\"title\", title);\n\n this.dispatch(\"format\", { detail: { formatted } });\n }\n\n /**\n * Parses the UTC `datetime` attribute into a {@link Date}, or `null`. Whitespace\n * around the attribute value is tolerated.\n */\n #parse(): Date | null {\n const raw = this.element.getAttribute(\"datetime\");\n if (!raw) return null;\n const ms = Date.parse(this.#asUtc(raw.trim()));\n return Number.isNaN(ms) ? null : new Date(ms);\n }\n\n /**\n * Reads a timezone-less date-time as UTC — the input contract of this\n * controller — since `Date.parse` would otherwise read `\"2026-06-08T12:30:00\"` in\n * the *runtime's* local zone, contradicting \"the server emits UTC\". Values that\n * already carry `Z` or a `±hh:mm` offset (and bare `YYYY-MM-DD` dates, already\n * parsed as UTC) are returned unchanged.\n *\n * HTML accepts a space where ISO 8601 wants `T`, and `Date.parse` of that form is\n * left to each engine, so a whole value shaped that way is normalized to the `T`\n * separator first. The pattern is anchored: a value trailing anything else — a\n * zone word such as `\"2026-06-08 12:30:00 UTC\"` — is handed to `Date.parse` as\n * authored instead of being turned into a string nothing can parse.\n */\n #asUtc(value: string): string {\n const isoLike = value.replace(\n /^(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d+)?)?(?:Z|[+-]\\d{2}:?\\d{2})?)$/,\n \"$1T$2\",\n );\n const hasTime = /T\\d{2}:\\d{2}/.test(isoLike);\n const hasZone = /(Z|[+-]\\d{2}:?\\d{2})$/.test(isoLike);\n return hasTime && !hasZone ? `${isoLike}Z` : isoLike;\n }\n\n /**\n * Builds the optional detailed `title`. `titleFormat` is an `Intl` style\n * keyword applied to *both* date and time; empty (the default) adds no title.\n */\n #title(date: Date): string | null {\n if (this.titleFormatValue.length === 0) return null;\n return this.#applyFormat(date, this.titleFormatValue, this.titleFormatValue);\n }\n\n /**\n * Formats `date` with `Intl.DateTimeFormat`, including each style only when it\n * is a valid keyword (so a consumer can show date-only or time-only by clearing\n * the other). Returns `null` when neither style is usable or the declaration\n * names a locale or time zone `Intl` rejects, so the caller can leave the\n * authored text untouched.\n */\n #applyFormat(date: Date, dateStyle: string, timeStyle: string): string | null {\n // `toStyle` yields `undefined` for an empty/invalid keyword; assigning it is\n // equivalent to omitting the option, so a consumer can show date- or time-only.\n const options: Intl.DateTimeFormatOptions = {\n dateStyle: toStyle(dateStyle),\n timeStyle: toStyle(timeStyle),\n };\n if (options.dateStyle === undefined && options.timeStyle === undefined) return null;\n if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;\n\n // An invalid locale or timeZone must not break the page: the authored\n // absolute text remains as the graceful fallback. `date` is already known to\n // be a real instant (`#parse` rejects the rest), so formatting it cannot fail.\n const formatter = intlFormatter(\n Intl.DateTimeFormat,\n resolveLocale(this.element, this.localeValue),\n options,\n );\n return formatter === null ? null : formatter.format(date);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/datetime.ts","../../src/utils/intl_format.ts","../../src/utils/locale.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/morph_render_watcher.ts","../../src/controllers/local_time_controller.ts"],"names":[],"mappings":"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","file":"local_time_controller.js","sourcesContent":["/** Complete Gregorian dates with an optional clock and numeric UTC offset. */\nconst DATETIME =\n /^([+-]\\d{6}|\\d{4})-(\\d{2})-(\\d{2})(?:[T ](\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d+))?)?(Z|[+-]\\d{2}:?\\d{2})?)?$/;\n\n/**\n * Reads a complete Gregorian `datetime` as epoch milliseconds, or `null`.\n *\n * Dates and clocks without a zone use UTC; explicit offsets retain their meaning.\n * Surrounding whitespace is tolerated, fractions are truncated to milliseconds,\n * and 24:00 is allowed only when every smaller unit is zero. Invalid civil dates,\n * incomplete fields, and instants outside the Date range return `null`, and the caller\n * keeps what the element shows.\n */\nexport function readDatetime(element: Element): number | null {\n const raw = element.getAttribute(\"datetime\");\n if (raw === null) return null;\n const match = DATETIME.exec(raw.trim());\n if (match === null) return null;\n const [\n ,\n yearText,\n monthText,\n dayText,\n hourText = \"00\",\n minuteText = \"00\",\n secondText = \"00\",\n fraction = \"\",\n zone = \"Z\",\n ] = match;\n const year = Number(yearText);\n const month = Number(monthText);\n const day = Number(dayText);\n const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);\n const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];\n if (month < 1 || month > 12 || day < 1 || day > (days[month - 1] ?? 0)) return null;\n const hour = Number(hourText);\n const minute = Number(minuteText);\n const second = Number(secondText);\n if (hour === 24 && (minute !== 0 || second !== 0 || /[1-9]/.test(fraction))) return null;\n let offset = zone;\n if (zone !== \"Z\") {\n const digits = zone.slice(1).replace(\":\", \"\");\n const offsetHour = digits.slice(0, 2);\n const offsetMinute = digits.slice(2);\n offset = `${zone[0]}${offsetHour}:${offsetMinute}`;\n }\n const milliseconds = fraction.slice(0, 3).padEnd(3, \"0\");\n const canonical = `${yearText}-${monthText}-${dayText}T${hourText}:${minuteText}:${secondText}.${milliseconds}${offset}`;\n const instant = Date.parse(canonical);\n return Number.isNaN(instant) ? null : instant;\n}\n","/**\n * Construction of `Intl` formatters for the controllers that take a locale, and\n * the options that go with it, from a declaration.\n *\n * Every `Intl` constructor resolves locale data on the way in, which makes it\n * too costly to rebuild on a hot path, and rejects a malformed language tag,\n * currency code, time zone or digit count with a `RangeError`. Both concerns are\n * handled here once. A formatter is built at most once per distinct request and\n * shared by every caller that asks for the same one. A `RangeError` is caught\n * and answered the way the caller asked: by rebuilding under a fallback locale,\n * or by returning `null` so the caller can leave its authored text alone. Any\n * other failure is a programming fault and propagates.\n */\n\n/** The constructor shape the `Intl` formatter classes share. */\nexport interface IntlFormatterConstructor<F, O extends object> {\n new (locales?: string | string[], options?: O): F;\n}\n\n/** Built formatters, and refusals, per constructor; see `cacheKey` for the key. */\nconst formatters = new WeakMap<object, Map<string, unknown>>();\n\n/**\n * A formatter for `locale` with `options`.\n *\n * With `fallbackLocale`, a `RangeError` from the constructor is answered by\n * building the same options under the fallback instead, so the result is never\n * `null`; the fallback's own failure propagates. Without it, a `RangeError`\n * yields `null` and the caller decides what to show. Errors of any other kind\n * propagate in both forms.\n *\n * Results are cached per constructor and `(locale, options, fallback)`, `null`\n * included, so asking again on every render costs one map lookup.\n */\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string,\n): F;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n): F | null;\nexport function intlFormatter<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale?: string,\n): F | null {\n const cache = cacheFor(factory);\n const key = cacheKey(locale, options, fallbackLocale);\n if (cache.has(key)) return cache.get(key) as F | null;\n const formatter = construct(factory, locale, options, fallbackLocale);\n cache.set(key, formatter);\n return formatter;\n}\n\n/** The cache for one constructor, created on first use. */\nfunction cacheFor(factory: object): Map<string, unknown> {\n const existing = formatters.get(factory);\n if (existing) return existing;\n const created = new Map<string, unknown>();\n formatters.set(factory, created);\n return created;\n}\n\n/**\n * One key per distinct request. `JSON.stringify` drops an option left\n * `undefined`, which is how the constructor reads it too. Two spellings of the\n * same options differing only in key order key separately; that costs one extra\n * formatter, never a wrong one, and call sites pass a fixed literal anyway.\n */\nfunction cacheKey(\n locale: string | undefined,\n options: object,\n fallbackLocale: string | undefined,\n): string {\n return JSON.stringify([locale, options, fallbackLocale]);\n}\n\n/** Builds the formatter under the `RangeError` contract described on `intlFormatter`. */\nfunction construct<F, O extends object>(\n factory: IntlFormatterConstructor<F, O>,\n locale: string | undefined,\n options: O,\n fallbackLocale: string | undefined,\n): F | null {\n try {\n return new factory(locale, options);\n } catch (error) {\n if (!(error instanceof RangeError)) throw error;\n if (fallbackLocale === undefined) return null;\n return new factory(fallbackLocale, options);\n }\n}\n","/**\n * The locale a component formats in: its own declaration, else the language of\n * the text around it, else the runtime default.\n *\n * `lang` is inherited in HTML, so the language that applies to an element is the\n * nearest one on the element itself or on an ancestor — not the document\n * element's, which is only the farthest of them. An empty `lang` there means the\n * language is unknown and stops the inheritance at that point, so it resolves to\n * the runtime default rather than to a `lang` further up.\n *\n * Resolution only, and the value is passed through as authored. Whether `Intl`\n * accepts the tag, and what to show when it does not, is the caller's contract.\n *\n * @example\n * ```ts\n * intlFormatter(Intl.DateTimeFormat, resolveLocale(this.element, this.localeValue), options);\n * ```\n */\nexport function resolveLocale(element: Element, declared = \"\"): string | undefined {\n return declared || element.closest(\"[lang]\")?.getAttribute(\"lang\") || undefined;\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 * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Whether a requested pass has yet to run. */\n get pending(): boolean {\n return this.#queued;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { ListenerSet } from \"./listener_set\";\nimport { MicrotaskCoalescer } from \"./microtask_coalescer\";\n\n/**\n * Coalesces retained-element morphs and explicit lifecycle requests into one pass.\n *\n * Observe the controller's element after its initial state is settled. Morphs on\n * that element and bubbling morphs on its descendants share the same microtask\n * as {@link schedule}. The callback repairs derived output; the controller owns\n * any logical transition reporting. {@link disconnect} releases the listener and\n * cancels pending work, including work from an earlier connected lifetime.\n */\nexport class MorphRenderWatcher {\n readonly #listeners = new ListenerSet();\n readonly #pass: MicrotaskCoalescer;\n #element: Element | null = null;\n readonly #onMorph = (): void => this.#pass.schedule();\n\n /** @param run - the single pass that repairs the settled controller's output. */\n constructor(run: () => void) {\n this.#pass = new MicrotaskCoalescer(run);\n }\n\n /** Opens observation; observing the same element again preserves queued work. */\n observe(element: Element): void {\n if (this.#element === element) return;\n this.disconnect();\n this.#element = element;\n this.#pass.activate();\n this.#listeners.add(element, \"turbo:morph-element\", this.#onMorph);\n }\n\n /** Requests one pass; requests outside an observed lifetime are ignored. */\n schedule(): void {\n this.#pass.schedule();\n }\n\n /** Whether a pass requested by a morph or {@link schedule} has yet to run. */\n get pending(): boolean {\n return this.#pass.pending;\n }\n\n /** Releases observation and pending work synchronously; safe to repeat. */\n disconnect(): void {\n this.#listeners.dispose();\n this.#pass.cancel();\n this.#element = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { readDatetime } from \"../utils/datetime\";\nimport { intlFormatter } from \"../utils/intl_format\";\nimport { resolveLocale } from \"../utils/locale\";\nimport { MorphRenderWatcher } from \"../utils/morph_render_watcher\";\n\n/** A date/time style keyword accepted by `Intl` `dateStyle` / `timeStyle`. */\ntype DateTimeStyle = \"full\" | \"long\" | \"medium\" | \"short\";\n\n/** The valid `Intl` style keywords, used to validate the string-typed values. */\nconst STYLES = new Set<DateTimeStyle>([\"full\", \"long\", \"medium\", \"short\"]);\n\n/** Narrows an arbitrary string to a valid {@link DateTimeStyle}, or `undefined`. */\nfunction toStyle(value: string): DateTimeStyle | undefined {\n return (STYLES as Set<string>).has(value) ? (value as DateTimeStyle) : undefined;\n}\n\n/**\n * Headless local-time behavior: renders the UTC instant in a `<time datetime>`\n * as an absolute, viewer-localized string via `Intl.DateTimeFormat`. No\n * dedicated APG pattern; it follows the HTML `<time>` semantics.\n *\n * Markup contract (identifier: `stimeo--local-time`):\n * <time datetime=\"2026-06-08T12:30:00Z\"\n * data-controller=\"stimeo--local-time\"\n * data-stimeo--local-time-date-style-value=\"medium\"\n * data-stimeo--local-time-time-style-value=\"short\">2026-06-08 12:30 UTC</time>\n *\n * The server emits UTC (cache-friendly — it never needs to know the viewer's\n * timezone), and the controller reformats the visible text into the viewer's\n * locale/zone on connect. A `datetime` without a zone is read as UTC; an explicit\n * `Z` or numeric offset keeps its meaning. This is the *absolute* localization axis,\n * distinct from `stimeo--relative-time`'s \"3 minutes ago\".\n *\n * `format` dispatches `{ formatted }`.\n *\n * @remarks\n * Behavior only. The machine-readable `datetime` is left untouched, exactly as written,\n * so assistive tech and crawlers keep the canonical value while only the display\n * text — and an optional `title` — change. Formatting is a pure function of\n * `datetime` with no module-scope state or timers, so a Turbo Drive cache restore\n * re-runs `connect()` and stays consistent. A `datetime` that does not parse, and\n * a locale or time zone `Intl` rejects, leave the text already shown in place — the\n * authored text until the first format; any other failure surfaces rather than being\n * absorbed.\n *\n * Render inputs are followed at runtime: a morph that swaps a Value or the\n * `datetime` attribute on the live element — which keeps the element, so\n * `connect()` never runs again — repaints through one coalesced pass\n * (`MorphRenderWatcher`) rather than leaving a stale reading on screen.\n */\nexport class LocalTimeController extends Controller<HTMLElement> {\n static override values = {\n locale: { type: String, default: \"\" },\n timeZone: { type: String, default: \"\" },\n dateStyle: { type: String, default: \"medium\" },\n timeStyle: { type: String, default: \"short\" },\n titleFormat: { type: String, default: \"\" },\n };\n static events = [\"format\"] as const;\n\n declare localeValue: string;\n declare timeZoneValue: string;\n declare dateStyleValue: string;\n declare timeStyleValue: string;\n declare titleFormatValue: string;\n\n /** Collapses a morph that swaps several render inputs at once into one repaint. */\n readonly #resync = new MorphRenderWatcher(() => {\n const datetimeChanged = this.#datetimeWatch.takeRecords().length > 0;\n const notify = this.#formatPending || datetimeChanged;\n this.#formatPending = false;\n this.#render(notify);\n });\n #formatPending = false;\n /**\n * Watches the one render input that is not a Value. Only `datetime` is filtered\n * in, so the text and `title` this controller writes cannot re-enter the pass.\n */\n readonly #datetimeWatch = new MutationObserver(() => {\n this.#formatPending = true;\n this.#resync.schedule();\n });\n\n override connect(): void {\n this.#resync.observe(this.element);\n this.#datetimeWatch.observe(this.element, { attributeFilter: [\"datetime\"] });\n this.#formatPending = false;\n this.#render();\n }\n\n override disconnect(): void {\n this.#resync.disconnect();\n this.#datetimeWatch.disconnect();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `locale` at runtime. */\n localeValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeZone` at runtime. */\n timeZoneValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `dateStyle` at runtime. */\n dateStyleValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `timeStyle` at runtime. */\n timeStyleValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /** Repaints when application code (or a Turbo morph) changes `titleFormat` at runtime. */\n titleFormatValueChanged(): void {\n this.#formatPending = true;\n this.#resync.schedule();\n }\n\n /**\n * Formats the instant in `datetime` against the current Values and writes it out.\n *\n * Formatting from a changed input reports `format`; restoring output alone is\n * silent. A pass that cannot format leaves the text already shown.\n *\n * @stimeoRenderRoot\n */\n #render(notify = true): void {\n const date = this.#parse();\n if (date === null) return;\n\n const formatted = this.#applyFormat(date, this.dateStyleValue, this.timeStyleValue);\n if (formatted === null) return;\n\n // Only the visible text (and optional title) change; `datetime` is the\n // immutable machine-readable source and is never rewritten.\n this.element.textContent = formatted;\n\n const title = this.#title(date);\n if (title !== null) this.element.setAttribute(\"title\", title);\n\n if (notify) this.dispatch(\"format\", { detail: { formatted } });\n }\n\n /**\n * Reads the `datetime` instant with the strict reader `stimeo--relative-time` also uses;\n * the attribute is never rewritten.\n */\n #parse(): Date | null {\n const instant = readDatetime(this.element);\n return instant === null ? null : new Date(instant);\n }\n\n /**\n * Builds the optional detailed `title`. `titleFormat` is an `Intl` style\n * keyword applied to *both* date and time; empty (the default) adds no title.\n */\n #title(date: Date): string | null {\n if (this.titleFormatValue.length === 0) return null;\n return this.#applyFormat(date, this.titleFormatValue, this.titleFormatValue);\n }\n\n /**\n * Formats `date` with `Intl.DateTimeFormat`, including each style only when it\n * is a valid keyword (so a consumer can show date-only or time-only by clearing\n * the other). Returns `null` when neither style is usable or the declaration\n * names a locale or time zone `Intl` rejects, so the caller can leave the text\n * already shown untouched.\n */\n #applyFormat(date: Date, dateStyle: string, timeStyle: string): string | null {\n // `toStyle` yields `undefined` for an empty/invalid keyword; assigning it is\n // equivalent to omitting the option, so a consumer can show date- or time-only.\n const options: Intl.DateTimeFormatOptions = {\n dateStyle: toStyle(dateStyle),\n timeStyle: toStyle(timeStyle),\n };\n if (options.dateStyle === undefined && options.timeStyle === undefined) return null;\n if (this.timeZoneValue.length > 0) options.timeZone = this.timeZoneValue;\n\n // An invalid locale or timeZone must not break the page: the text already shown\n // stays as the graceful fallback. `date` is already known to be a real instant\n // (`#parse` rejects the rest), so formatting it cannot fail.\n const formatter = intlFormatter(\n Intl.DateTimeFormat,\n resolveLocale(this.element, this.localeValue),\n options,\n );\n return formatter === null ? null : formatter.format(date);\n }\n}\n"]}
@@ -59,6 +59,14 @@ declare class MasonryController extends Controller<HTMLElement> {
59
59
  default: number;
60
60
  };
61
61
  };
62
+ static valueConstraints: {
63
+ minColumnWidth: {
64
+ readonly finite: true;
65
+ };
66
+ gap: {
67
+ readonly finite: true;
68
+ };
69
+ };
62
70
  static events: readonly ["layout"];
63
71
  readonly itemTargets: HTMLElement[];
64
72
  minColumnWidthValue: number;
@@ -73,9 +81,9 @@ declare class MasonryController extends Controller<HTMLElement> {
73
81
  * Queues the column hook of an element that stopped being an item for removal.
74
82
  *
75
83
  * The removal is queued rather than immediate because teardown reports every
76
- * target as disconnected: doing it here would strip the whole grid just before
77
- * a Turbo snapshot is taken. The coalescer's `cancel` drops the queue
78
- * with the pass, so only a genuine target change reaches it.
84
+ * target as disconnected. The pass that drains the queue skips an element
85
+ * that is still an item, which drops what teardown queued, and keeps a departure
86
+ * queued before a disconnect for the pass after the reconnect.
79
87
  */
80
88
  itemTargetDisconnected(item: HTMLElement): void;
81
89
  /** Observes size/content changes and performs the first layout pass. */