stimeo-ui 0.1.0-alpha.1

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 (238) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +72 -0
  3. package/dist/controllers/accordion_controller.d.ts +59 -0
  4. package/dist/controllers/accordion_controller.js +76 -0
  5. package/dist/controllers/accordion_controller.js.map +1 -0
  6. package/dist/controllers/announcer_controller.d.ts +78 -0
  7. package/dist/controllers/announcer_controller.js +184 -0
  8. package/dist/controllers/announcer_controller.js.map +1 -0
  9. package/dist/controllers/aspect_ratio_controller.d.ts +39 -0
  10. package/dist/controllers/aspect_ratio_controller.js +36 -0
  11. package/dist/controllers/aspect_ratio_controller.js.map +1 -0
  12. package/dist/controllers/auto_submit_controller.d.ts +74 -0
  13. package/dist/controllers/auto_submit_controller.js +147 -0
  14. package/dist/controllers/auto_submit_controller.js.map +1 -0
  15. package/dist/controllers/avatar_controller.d.ts +50 -0
  16. package/dist/controllers/avatar_controller.js +66 -0
  17. package/dist/controllers/avatar_controller.js.map +1 -0
  18. package/dist/controllers/breadcrumb_controller.d.ts +60 -0
  19. package/dist/controllers/breadcrumb_controller.js +123 -0
  20. package/dist/controllers/breadcrumb_controller.js.map +1 -0
  21. package/dist/controllers/bulk_select_controller.d.ts +67 -0
  22. package/dist/controllers/bulk_select_controller.js +104 -0
  23. package/dist/controllers/bulk_select_controller.js.map +1 -0
  24. package/dist/controllers/calendar_controller.d.ts +100 -0
  25. package/dist/controllers/calendar_controller.js +394 -0
  26. package/dist/controllers/calendar_controller.js.map +1 -0
  27. package/dist/controllers/character_counter_controller.d.ts +63 -0
  28. package/dist/controllers/character_counter_controller.js +179 -0
  29. package/dist/controllers/character_counter_controller.js.map +1 -0
  30. package/dist/controllers/checkbox_controller.d.ts +52 -0
  31. package/dist/controllers/checkbox_controller.js +73 -0
  32. package/dist/controllers/checkbox_controller.js.map +1 -0
  33. package/dist/controllers/combobox_controller.d.ts +76 -0
  34. package/dist/controllers/combobox_controller.js +186 -0
  35. package/dist/controllers/combobox_controller.js.map +1 -0
  36. package/dist/controllers/command_palette_controller.d.ts +103 -0
  37. package/dist/controllers/command_palette_controller.js +381 -0
  38. package/dist/controllers/command_palette_controller.js.map +1 -0
  39. package/dist/controllers/conditional_fields_controller.d.ts +56 -0
  40. package/dist/controllers/conditional_fields_controller.js +112 -0
  41. package/dist/controllers/conditional_fields_controller.js.map +1 -0
  42. package/dist/controllers/confirm_controller.d.ts +83 -0
  43. package/dist/controllers/confirm_controller.js +276 -0
  44. package/dist/controllers/confirm_controller.js.map +1 -0
  45. package/dist/controllers/context_menu_controller.d.ts +81 -0
  46. package/dist/controllers/context_menu_controller.js +112 -0
  47. package/dist/controllers/context_menu_controller.js.map +1 -0
  48. package/dist/controllers/countdown_controller.d.ts +93 -0
  49. package/dist/controllers/countdown_controller.js +202 -0
  50. package/dist/controllers/countdown_controller.js.map +1 -0
  51. package/dist/controllers/dialog_controller.d.ts +57 -0
  52. package/dist/controllers/dialog_controller.js +207 -0
  53. package/dist/controllers/dialog_controller.js.map +1 -0
  54. package/dist/controllers/direct_upload_controller.d.ts +77 -0
  55. package/dist/controllers/direct_upload_controller.js +212 -0
  56. package/dist/controllers/direct_upload_controller.js.map +1 -0
  57. package/dist/controllers/dirty_form_controller.d.ts +52 -0
  58. package/dist/controllers/dirty_form_controller.js +128 -0
  59. package/dist/controllers/dirty_form_controller.js.map +1 -0
  60. package/dist/controllers/dropdown_controller.d.ts +49 -0
  61. package/dist/controllers/dropdown_controller.js +66 -0
  62. package/dist/controllers/dropdown_controller.js.map +1 -0
  63. package/dist/controllers/empty_state_controller.d.ts +60 -0
  64. package/dist/controllers/empty_state_controller.js +67 -0
  65. package/dist/controllers/empty_state_controller.js.map +1 -0
  66. package/dist/controllers/flash_controller.d.ts +63 -0
  67. package/dist/controllers/flash_controller.js +221 -0
  68. package/dist/controllers/flash_controller.js.map +1 -0
  69. package/dist/controllers/focus_controller.d.ts +69 -0
  70. package/dist/controllers/focus_controller.js +216 -0
  71. package/dist/controllers/focus_controller.js.map +1 -0
  72. package/dist/controllers/form_field_controller.d.ts +79 -0
  73. package/dist/controllers/form_field_controller.js +154 -0
  74. package/dist/controllers/form_field_controller.js.map +1 -0
  75. package/dist/controllers/form_validation_controller.d.ts +114 -0
  76. package/dist/controllers/form_validation_controller.js +202 -0
  77. package/dist/controllers/form_validation_controller.js.map +1 -0
  78. package/dist/controllers/frame_loading_controller.d.ts +58 -0
  79. package/dist/controllers/frame_loading_controller.js +177 -0
  80. package/dist/controllers/frame_loading_controller.js.map +1 -0
  81. package/dist/controllers/highlight_controller.d.ts +50 -0
  82. package/dist/controllers/highlight_controller.js +107 -0
  83. package/dist/controllers/highlight_controller.js.map +1 -0
  84. package/dist/controllers/hover_card_controller.d.ts +79 -0
  85. package/dist/controllers/hover_card_controller.js +165 -0
  86. package/dist/controllers/hover_card_controller.js.map +1 -0
  87. package/dist/controllers/idle_controller.d.ts +51 -0
  88. package/dist/controllers/idle_controller.js +141 -0
  89. package/dist/controllers/idle_controller.js.map +1 -0
  90. package/dist/controllers/input_mask_controller.d.ts +74 -0
  91. package/dist/controllers/input_mask_controller.js +166 -0
  92. package/dist/controllers/input_mask_controller.js.map +1 -0
  93. package/dist/controllers/lazy_frame_controller.d.ts +53 -0
  94. package/dist/controllers/lazy_frame_controller.js +68 -0
  95. package/dist/controllers/lazy_frame_controller.js.map +1 -0
  96. package/dist/controllers/listbox_controller.d.ts +88 -0
  97. package/dist/controllers/listbox_controller.js +256 -0
  98. package/dist/controllers/listbox_controller.js.map +1 -0
  99. package/dist/controllers/local_time_controller.d.ts +60 -0
  100. package/dist/controllers/local_time_controller.js +81 -0
  101. package/dist/controllers/local_time_controller.js.map +1 -0
  102. package/dist/controllers/menu_controller.d.ts +81 -0
  103. package/dist/controllers/menu_controller.js +134 -0
  104. package/dist/controllers/menu_controller.js.map +1 -0
  105. package/dist/controllers/meter_controller.d.ts +93 -0
  106. package/dist/controllers/meter_controller.js +96 -0
  107. package/dist/controllers/meter_controller.js.map +1 -0
  108. package/dist/controllers/nested_form_controller.d.ts +85 -0
  109. package/dist/controllers/nested_form_controller.js +131 -0
  110. package/dist/controllers/nested_form_controller.js.map +1 -0
  111. package/dist/controllers/network_status_controller.d.ts +49 -0
  112. package/dist/controllers/network_status_controller.js +126 -0
  113. package/dist/controllers/network_status_controller.js.map +1 -0
  114. package/dist/controllers/number_input_controller.d.ts +93 -0
  115. package/dist/controllers/number_input_controller.js +306 -0
  116. package/dist/controllers/number_input_controller.js.map +1 -0
  117. package/dist/controllers/otp_controller.d.ts +68 -0
  118. package/dist/controllers/otp_controller.js +201 -0
  119. package/dist/controllers/otp_controller.js.map +1 -0
  120. package/dist/controllers/overflow_indicator_controller.d.ts +62 -0
  121. package/dist/controllers/overflow_indicator_controller.js +169 -0
  122. package/dist/controllers/overflow_indicator_controller.js.map +1 -0
  123. package/dist/controllers/overflow_menu_controller.d.ts +64 -0
  124. package/dist/controllers/overflow_menu_controller.js +274 -0
  125. package/dist/controllers/overflow_menu_controller.js.map +1 -0
  126. package/dist/controllers/pagination_controller.d.ts +69 -0
  127. package/dist/controllers/pagination_controller.js +89 -0
  128. package/dist/controllers/pagination_controller.js.map +1 -0
  129. package/dist/controllers/password_strength_controller.d.ts +67 -0
  130. package/dist/controllers/password_strength_controller.js +175 -0
  131. package/dist/controllers/password_strength_controller.js.map +1 -0
  132. package/dist/controllers/persist_controller.d.ts +63 -0
  133. package/dist/controllers/persist_controller.js +259 -0
  134. package/dist/controllers/persist_controller.js.map +1 -0
  135. package/dist/controllers/popover_controller.d.ts +63 -0
  136. package/dist/controllers/popover_controller.js +94 -0
  137. package/dist/controllers/popover_controller.js.map +1 -0
  138. package/dist/controllers/portal_controller.d.ts +58 -0
  139. package/dist/controllers/portal_controller.js +63 -0
  140. package/dist/controllers/portal_controller.js.map +1 -0
  141. package/dist/controllers/preview_guard_controller.d.ts +47 -0
  142. package/dist/controllers/preview_guard_controller.js +69 -0
  143. package/dist/controllers/preview_guard_controller.js.map +1 -0
  144. package/dist/controllers/progress_controller.d.ts +85 -0
  145. package/dist/controllers/progress_controller.js +93 -0
  146. package/dist/controllers/progress_controller.js.map +1 -0
  147. package/dist/controllers/radio_group_controller.d.ts +49 -0
  148. package/dist/controllers/radio_group_controller.js +128 -0
  149. package/dist/controllers/radio_group_controller.js.map +1 -0
  150. package/dist/controllers/rating_controller.d.ts +82 -0
  151. package/dist/controllers/rating_controller.js +179 -0
  152. package/dist/controllers/rating_controller.js.map +1 -0
  153. package/dist/controllers/relative_time_controller.d.ts +48 -0
  154. package/dist/controllers/relative_time_controller.js +129 -0
  155. package/dist/controllers/relative_time_controller.js.map +1 -0
  156. package/dist/controllers/reset_before_cache_controller.d.ts +54 -0
  157. package/dist/controllers/reset_before_cache_controller.js +62 -0
  158. package/dist/controllers/reset_before_cache_controller.js.map +1 -0
  159. package/dist/controllers/resizable_controller.d.ts +81 -0
  160. package/dist/controllers/resizable_controller.js +163 -0
  161. package/dist/controllers/resizable_controller.js.map +1 -0
  162. package/dist/controllers/roving_controller.d.ts +63 -0
  163. package/dist/controllers/roving_controller.js +116 -0
  164. package/dist/controllers/roving_controller.js.map +1 -0
  165. package/dist/controllers/scroll_area_controller.d.ts +45 -0
  166. package/dist/controllers/scroll_area_controller.js +183 -0
  167. package/dist/controllers/scroll_area_controller.js.map +1 -0
  168. package/dist/controllers/scroll_visibility_controller.d.ts +72 -0
  169. package/dist/controllers/scroll_visibility_controller.js +103 -0
  170. package/dist/controllers/scroll_visibility_controller.js.map +1 -0
  171. package/dist/controllers/scrollspy_controller.d.ts +65 -0
  172. package/dist/controllers/scrollspy_controller.js +171 -0
  173. package/dist/controllers/scrollspy_controller.js.map +1 -0
  174. package/dist/controllers/skeleton_controller.d.ts +50 -0
  175. package/dist/controllers/skeleton_controller.js +125 -0
  176. package/dist/controllers/skeleton_controller.js.map +1 -0
  177. package/dist/controllers/slider_controller.d.ts +76 -0
  178. package/dist/controllers/slider_controller.js +109 -0
  179. package/dist/controllers/slider_controller.js.map +1 -0
  180. package/dist/controllers/spinner_controller.d.ts +62 -0
  181. package/dist/controllers/spinner_controller.js +164 -0
  182. package/dist/controllers/spinner_controller.js.map +1 -0
  183. package/dist/controllers/step_indicator_controller.d.ts +55 -0
  184. package/dist/controllers/step_indicator_controller.js +55 -0
  185. package/dist/controllers/step_indicator_controller.js.map +1 -0
  186. package/dist/controllers/stepper_controller.d.ts +66 -0
  187. package/dist/controllers/stepper_controller.js +78 -0
  188. package/dist/controllers/stepper_controller.js.map +1 -0
  189. package/dist/controllers/stick_to_bottom_controller.d.ts +61 -0
  190. package/dist/controllers/stick_to_bottom_controller.js +100 -0
  191. package/dist/controllers/stick_to_bottom_controller.js.map +1 -0
  192. package/dist/controllers/sticky_observer_controller.d.ts +54 -0
  193. package/dist/controllers/sticky_observer_controller.js +53 -0
  194. package/dist/controllers/sticky_observer_controller.js.map +1 -0
  195. package/dist/controllers/submit_once_controller.d.ts +66 -0
  196. package/dist/controllers/submit_once_controller.js +206 -0
  197. package/dist/controllers/submit_once_controller.js.map +1 -0
  198. package/dist/controllers/switch_controller.d.ts +43 -0
  199. package/dist/controllers/switch_controller.js +50 -0
  200. package/dist/controllers/switch_controller.js.map +1 -0
  201. package/dist/controllers/tabs_controller.d.ts +48 -0
  202. package/dist/controllers/tabs_controller.js +63 -0
  203. package/dist/controllers/tabs_controller.js.map +1 -0
  204. package/dist/controllers/textarea_autosize_controller.d.ts +47 -0
  205. package/dist/controllers/textarea_autosize_controller.js +72 -0
  206. package/dist/controllers/textarea_autosize_controller.js.map +1 -0
  207. package/dist/controllers/theme_controller.d.ts +70 -0
  208. package/dist/controllers/theme_controller.js +154 -0
  209. package/dist/controllers/theme_controller.js.map +1 -0
  210. package/dist/controllers/toast_controller.d.ts +123 -0
  211. package/dist/controllers/toast_controller.js +310 -0
  212. package/dist/controllers/toast_controller.js.map +1 -0
  213. package/dist/controllers/toggle_group_controller.d.ts +56 -0
  214. package/dist/controllers/toggle_group_controller.js +130 -0
  215. package/dist/controllers/toggle_group_controller.js.map +1 -0
  216. package/dist/controllers/toolbar_controller.d.ts +59 -0
  217. package/dist/controllers/toolbar_controller.js +113 -0
  218. package/dist/controllers/toolbar_controller.js.map +1 -0
  219. package/dist/controllers/tooltip_controller.d.ts +74 -0
  220. package/dist/controllers/tooltip_controller.js +165 -0
  221. package/dist/controllers/tooltip_controller.js.map +1 -0
  222. package/dist/controllers/transition_controller.d.ts +86 -0
  223. package/dist/controllers/transition_controller.js +203 -0
  224. package/dist/controllers/transition_controller.js.map +1 -0
  225. package/dist/index.d.ts +1931 -0
  226. package/dist/index.js +12241 -0
  227. package/dist/index.js.map +1 -0
  228. package/dist/inspector/cli.d.ts +149 -0
  229. package/dist/inspector/cli.js +663 -0
  230. package/dist/inspector/cli.js.map +1 -0
  231. package/dist/inspector/cli_bin.d.ts +2 -0
  232. package/dist/inspector/cli_bin.js +665 -0
  233. package/dist/inspector/cli_bin.js.map +1 -0
  234. package/dist/inspector/manifest.json +2235 -0
  235. package/dist/positioning/index.d.ts +226 -0
  236. package/dist/positioning/index.js +145 -0
  237. package/dist/positioning/index.js.map +1 -0
  238. package/package.json +93 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/relative_time_controller.ts"],"names":[],"mappings":";;;;;AAwBA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;AC5FA,IAAM,UAAA,GAAwB,EAAE,KAAA,EAAO,MAAA,CAAO,mBAAmB,IAAA,EAAM,MAAA,EAAQ,IAAI,QAAA,EAAe;AAGlG,IAAM,KAAA,GAA8B;AAAA,EAClC,EAAE,KAAA,EAAO,EAAA,EAAI,IAAA,EAAM,QAAA,EAAU,IAAI,GAAA,EAAK;AAAA,EACtC,EAAE,KAAA,EAAO,IAAA,EAAM,IAAA,EAAM,QAAA,EAAU,IAAI,GAAA,EAAO;AAAA,EAC1C,EAAE,KAAA,EAAO,KAAA,EAAQ,IAAA,EAAM,MAAA,EAAQ,IAAI,IAAA,EAAU;AAAA,EAC7C,EAAE,KAAA,EAAO,MAAA,EAAS,IAAA,EAAM,KAAA,EAAO,IAAI,KAAA,EAAW;AAAA,EAC9C,EAAE,KAAA,EAAO,OAAA,EAAW,IAAA,EAAM,MAAA,EAAQ,IAAI,MAAA,EAAY;AAAA,EAClD,EAAE,KAAA,EAAO,QAAA,EAAY,IAAA,EAAM,OAAA,EAAS,IAAI,OAAA,EAAc;AAAA,EACtD;AACF,CAAA;AAwBO,IAAM,sBAAA,GAAN,cAAqC,UAAA,CAAwB;AAAA,EAClE,OAAgB,MAAA,GAAS;AAAA,IACvB,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACpC,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACtC,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,GAAA;AAAO,GAChD;AAAA,EAMS,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA;AAAA,EAGnC,YAAY,MAAA,CAAO,GAAA;AAAA;AAAA,EAEnB,aAAA,GAAgB,EAAA;AAAA,EAEP,OAAA,GAAgB;AAKvB,IAAA,IAAI,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAY,MAAM,UAAA,EAAY;AAC1D,MAAA,IAAA,CAAK,aAAA,GAAA,CAAiB,IAAA,CAAK,OAAA,CAAQ,WAAA,IAAe,IAAI,IAAA,EAAK;AAAA,IAC7D;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,KAAK,KAAA,CAAM,IAAA,CAAK,QAAQ,YAAA,CAAa,UAAU,KAAK,EAAE,CAAA;AACvE,IAAA,IAAI,MAAA,CAAO,KAAA,CAAM,IAAA,CAAK,SAAS,CAAA,EAAG;AAClC,IAAA,IAAA,CAAK,SAAA,EAAU;AAAA,EACjB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AAAA,EACxB;AAAA;AAAA,EAGA,SAAA,GAAkB;AAChB,IAAA,MAAM,SAAA,GAAY,KAAK,qBAAA,EAAsB;AAC7C,IAAA,IAAI,cAAc,IAAA,EAAM;AACtB,MAAA,IAAA,CAAK,QAAQ,GAAA,CAAI,MAAM,IAAA,CAAK,SAAA,IAAa,SAAS,CAAA;AAAA,IACpD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,qBAAA,GAAuC;AACrC,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,SAAA,GAAY,IAAA,CAAK,GAAA,EAAI;AAC1C,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,GAAA,CAAI,OAAO,CAAA,GAAI,GAAA;AAKvC,IAAA,IAAI,KAAK,cAAA,GAAiB,CAAA,IAAK,cAAc,IAAA,CAAK,cAAA,IAAkB,KAAK,aAAA,EAAe;AACtF,MAAA,IAAA,CAAK,OAAA,CAAQ,cAAc,IAAA,CAAK,aAAA;AAChC,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,UAAU,CAAA;AAClD,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,CAAC,MAAM,UAAA,GAAa,CAAA,CAAE,KAAK,CAAA,IAAK,UAAA;AACzD,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,OAAA,GAAU,MAAM,EAAE,CAAA;AAC3C,IAAA,IAAA,CAAK,QAAQ,WAAA,GAAc,IAAA,CAAK,WAAW,MAAA,CAAO,KAAA,EAAO,MAAM,IAAI,CAAA;AACnE,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,UAAU,CAAA;AAIlD,IAAA,MAAM,SAAA,GAAY,MAAM,IAAA,KAAS,QAAA,IAAY,MAAM,IAAA,KAAS,QAAA,GAAW,MAAS,KAAA,CAAM,EAAA;AACtF,IAAA,OAAO,IAAA,CAAK,IAAI,IAAA,CAAK,iBAAA,EAAmB,KAAK,GAAA,CAAI,SAAA,EAAW,KAAU,CAAC,CAAA;AAAA,EACzE;AAAA;AAAA,EAGA,IAAI,UAAA,GAAsC;AACxC,IAAA,OAAO,IAAI,KAAK,kBAAA,CAAmB,IAAA,CAAK,SAAS,EAAE,OAAA,EAAS,QAAQ,CAAA;AAAA,EACtE;AAAA;AAAA,EAGA,IAAI,OAAA,GAA8B;AAChC,IAAA,OAAO,KAAK,WAAA,IAAe,IAAA,CAAK,QAAQ,IAAA,IAAQ,QAAA,CAAS,gBAAgB,IAAA,IAAQ,MAAA;AAAA,EACnF;AACF","file":"relative_time_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** A time scale: the upper bound (seconds) it covers and its `Intl` unit/divisor. */\ninterface TimeScale {\n limit: number;\n unit: Intl.RelativeTimeFormatUnit;\n ms: number;\n}\n\n/** Coarsest scale, used as the fallback for anything a year or older. */\nconst YEAR_SCALE: TimeScale = { limit: Number.POSITIVE_INFINITY, unit: \"year\", ms: 31_557_600_000 };\n\n/** Boundaries (in seconds) and the `Intl` unit/divisor used at each scale. */\nconst UNITS: readonly TimeScale[] = [\n { limit: 60, unit: \"second\", ms: 1000 },\n { limit: 3600, unit: \"minute\", ms: 60_000 },\n { limit: 86_400, unit: \"hour\", ms: 3_600_000 },\n { limit: 604_800, unit: \"day\", ms: 86_400_000 },\n { limit: 2_629_800, unit: \"week\", ms: 604_800_000 },\n { limit: 31_557_600, unit: \"month\", ms: 2_629_800_000 },\n YEAR_SCALE,\n];\n\n/**\n * Headless relative-time behavior: renders an absolute timestamp as \"3 minutes\n * ago\" / \"in 2 days\" and keeps it fresh. No dedicated APG pattern; it follows\n * the HTML `<time>` semantics.\n *\n * Markup contract (identifier: `stimeo--relative-time`):\n * <time data-controller=\"stimeo--relative-time\"\n * datetime=\"2026-05-30T12:00:00+09:00\" title=\"2026-05-30 12:00\"\n * data-stimeo--relative-time-locale-value=\"ja\">2026-05-30 12:00</time>\n *\n * Computes the difference from `datetime` to now and formats it with\n * `Intl.RelativeTimeFormat` (a browser standard — no added dependency). The\n * polling interval widens as the timestamp ages (seconds → minutes → hours →\n * days). Past a `threshold`, it falls back to the authored absolute text.\n *\n * @remarks\n * Behavior only. The machine-readable `datetime` attribute is left untouched\n * while only the visible text updates, and the element is intentionally **not**\n * a live region (silent updates, no announcement interruptions). The polling\n * timer is owned by {@link SafeTimeout} and torn down on `disconnect()` (Turbo\n * navigation included).\n */\nexport class RelativeTimeController extends Controller<HTMLElement> {\n static override values = {\n locale: { type: String, default: \"\" },\n threshold: { type: Number, default: 0 },\n tickInterval: { type: Number, default: 60_000 },\n };\n\n declare localeValue: string;\n declare thresholdValue: number;\n declare tickIntervalValue: number;\n\n readonly #timers = new SafeTimeout();\n\n /** Epoch ms parsed from `datetime`; `NaN` when absent or invalid. */\n #targetMs = Number.NaN;\n /** The authored absolute text, restored when the threshold fallback kicks in. */\n #absoluteText = \"\";\n\n override connect(): void {\n // Don't adopt already-rendered relative text as the absolute fallback: after a\n // Turbo morph that preserved the live \"3 minutes ago\" text, a fresh re-connect\n // would otherwise capture that relative string as `#absoluteText`. Only read the\n // authored textContent before the element has been rendered to a relative form.\n if (this.element.getAttribute(\"data-state\") !== \"relative\") {\n this.#absoluteText = (this.element.textContent ?? \"\").trim();\n }\n this.#targetMs = Date.parse(this.element.getAttribute(\"datetime\") ?? \"\");\n if (Number.isNaN(this.#targetMs)) return;\n this.#schedule();\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n }\n\n /** Renders the current representation and reschedules unless it is now absolute. */\n #schedule(): void {\n const nextDelay = this.#applyAndComputeDelay();\n if (nextDelay !== null) {\n this.#timers.set(() => this.#schedule(), nextDelay);\n }\n }\n\n /**\n * Updates the visible text and returns the next poll delay (ms), or `null`\n * once the absolute fallback is shown (which never changes, so stop polling).\n */\n #applyAndComputeDelay(): number | null {\n const deltaMs = this.#targetMs - Date.now();\n const absSeconds = Math.abs(deltaMs) / 1000;\n\n // Only switch to the absolute fallback when we actually hold authored absolute\n // text; otherwise (e.g. it could not be recovered after a morph) keep rendering\n // the relative form rather than blanking the element.\n if (this.thresholdValue > 0 && absSeconds >= this.thresholdValue && this.#absoluteText) {\n this.element.textContent = this.#absoluteText;\n this.element.setAttribute(\"data-state\", \"absolute\");\n return null;\n }\n\n const scale = UNITS.find((u) => absSeconds < u.limit) ?? YEAR_SCALE;\n const value = Math.round(deltaMs / scale.ms);\n this.element.textContent = this.#formatter.format(value, scale.unit);\n this.element.setAttribute(\"data-state\", \"relative\");\n\n // Poll no finer than the configured minimum; widen for coarser units so an\n // hours-old stamp is not re-rendered every minute.\n const unitFloor = scale.unit === \"second\" || scale.unit === \"minute\" ? 60_000 : scale.ms;\n return Math.max(this.tickIntervalValue, Math.min(unitFloor, 86_400_000));\n }\n\n /** A `RelativeTimeFormat` for the resolved locale (`numeric: \"auto\"`). */\n get #formatter(): Intl.RelativeTimeFormat {\n return new Intl.RelativeTimeFormat(this.#locale, { numeric: \"auto\" });\n }\n\n /** Locale precedence: the value, then the element's `lang`, then the document's. */\n get #locale(): string | undefined {\n return this.localeValue || this.element.lang || document.documentElement.lang || undefined;\n }\n}\n"]}
@@ -0,0 +1,54 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless **before-cache reset** — the most Hotwire-specific gap part (no APG
5
+ * pattern). On `turbo:before-cache` it returns transient UI (open menus/modals,
6
+ * typed-in values, spinning indicators) to its initial state, so a page restored by
7
+ * the Back button is not frozen mid-interaction. Place one on `<body>`.
8
+ *
9
+ * Markup contract (identifier: `stimeo--reset-before-cache`):
10
+ * <body data-controller="stimeo--reset-before-cache">
11
+ * <details data-reset-attr="open">…</details> <!-- remove these attributes -->
12
+ * <div data-reset-class="is-open is-loading">…</div> <!-- remove these classes -->
13
+ * <form data-reset-form>…</form> <!-- form.reset() -->
14
+ * <input data-reset-value> <!-- clear the value -->
15
+ * <div data-reset-hidden>transient overlay</div> <!-- re-hide -->
16
+ * <div data-reset-remove>flash toast</div> <!-- drop from the DOM -->
17
+ * </body>
18
+ *
19
+ * @remarks
20
+ * Behavior only and **idempotent** — every run converges on the same initial state,
21
+ * holding no module-scope state. It does the cross-cutting DOM cleanup directly
22
+ * (attribute removal, class removal, value clearing, re-hiding, node removal) and, when
23
+ * `dispatchReset` is on, fires `stimeo--reset-before-cache:request` so individual
24
+ * Stimeo controllers can run their own close logic. The `turbo:before-cache`
25
+ * listener is paired to `connect()` / `disconnect()` so it never double-registers or
26
+ * leaks. {@link reset} is also a public action for manual triggering.
27
+ */
28
+ declare class ResetBeforeCacheController extends Controller<HTMLElement> {
29
+ #private;
30
+ static values: {
31
+ scope: {
32
+ type: StringConstructor;
33
+ default: string;
34
+ };
35
+ dispatchReset: {
36
+ type: BooleanConstructor;
37
+ default: boolean;
38
+ };
39
+ };
40
+ static actions: readonly ["reset"];
41
+ static events: readonly ["reset", "request"];
42
+ scopeValue: string;
43
+ dispatchResetValue: boolean;
44
+ connect(): void;
45
+ disconnect(): void;
46
+ /**
47
+ * Resets transient UI within scope to its initial state. Asks controllers to
48
+ * close (via `request`) first, then applies the declarative `data-reset-*` cleanup,
49
+ * and finally emits `reset`. Safe to call any number of times (idempotent).
50
+ */
51
+ reset(): void;
52
+ }
53
+
54
+ export { ResetBeforeCacheController };
@@ -0,0 +1,62 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/reset_before_cache_controller.ts
4
+ var ResetBeforeCacheController = class extends Controller {
5
+ static values = {
6
+ scope: { type: String, default: "" },
7
+ dispatchReset: { type: Boolean, default: true }
8
+ };
9
+ static actions = ["reset"];
10
+ static events = ["reset", "request"];
11
+ /** Runs the reset just before Turbo caches the snapshot. */
12
+ #onBeforeCache = () => this.reset();
13
+ connect() {
14
+ document.addEventListener("turbo:before-cache", this.#onBeforeCache);
15
+ }
16
+ disconnect() {
17
+ document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
18
+ }
19
+ /**
20
+ * Resets transient UI within scope to its initial state. Asks controllers to
21
+ * close (via `request`) first, then applies the declarative `data-reset-*` cleanup,
22
+ * and finally emits `reset`. Safe to call any number of times (idempotent).
23
+ */
24
+ reset() {
25
+ const root = this.#scopeRoot();
26
+ if (this.dispatchResetValue) this.dispatch("request");
27
+ for (const element of root.querySelectorAll("[data-reset-attr]")) {
28
+ for (const name of (element.getAttribute("data-reset-attr") ?? "").split(/\s+/)) {
29
+ if (name) element.removeAttribute(name);
30
+ }
31
+ }
32
+ for (const element of root.querySelectorAll("[data-reset-class]")) {
33
+ for (const token of (element.getAttribute("data-reset-class") ?? "").split(/\s+/)) {
34
+ if (token) element.classList.remove(token);
35
+ }
36
+ }
37
+ for (const element of root.querySelectorAll("[data-reset-form]")) {
38
+ if (element instanceof HTMLFormElement) element.reset();
39
+ }
40
+ for (const element of root.querySelectorAll("[data-reset-value]")) {
41
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
42
+ element.value = "";
43
+ }
44
+ }
45
+ for (const element of root.querySelectorAll("[data-reset-hidden]")) {
46
+ element.hidden = true;
47
+ }
48
+ for (const element of root.querySelectorAll("[data-reset-remove]")) {
49
+ element.remove();
50
+ }
51
+ this.dispatch("reset");
52
+ }
53
+ /** The scan root: a `scope` descendant when set, else the controller element. */
54
+ #scopeRoot() {
55
+ if (!this.scopeValue) return this.element;
56
+ return this.element.querySelector(this.scopeValue) ?? this.element;
57
+ }
58
+ };
59
+
60
+ export { ResetBeforeCacheController };
61
+ //# sourceMappingURL=reset_before_cache_controller.js.map
62
+ //# sourceMappingURL=reset_before_cache_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/controllers/reset_before_cache_controller.ts"],"names":[],"mappings":";;;AA2BO,IAAM,0BAAA,GAAN,cAAyC,UAAA,CAAwB;AAAA,EACtE,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IACnC,aAAA,EAAe,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GAChD;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,OAAO,CAAA;AAAA,EACzB,OAAO,MAAA,GAAS,CAAC,OAAA,EAAS,SAAS,CAAA;AAAA;AAAA,EAM1B,cAAA,GAAiB,MAAY,IAAA,CAAK,KAAA,EAAM;AAAA,EAExC,OAAA,GAAgB;AACvB,IAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,IAAA,CAAK,cAAc,CAAA;AAAA,EACrE;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,IAAA,CAAK,cAAc,CAAA;AAAA,EACxE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,KAAA,GAAc;AACZ,IAAA,MAAM,IAAA,GAAO,KAAK,UAAA,EAAW;AAE7B,IAAA,IAAI,IAAA,CAAK,kBAAA,EAAoB,IAAA,CAAK,QAAA,CAAS,SAAS,CAAA;AAEpD,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAAiB,mBAAmB,CAAA,EAAG;AAChE,MAAA,KAAA,MAAW,IAAA,IAAA,CAAS,QAAQ,YAAA,CAAa,iBAAiB,KAAK,EAAA,EAAI,KAAA,CAAM,KAAK,CAAA,EAAG;AAC/E,QAAA,IAAI,IAAA,EAAM,OAAA,CAAQ,eAAA,CAAgB,IAAI,CAAA;AAAA,MACxC;AAAA,IACF;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAAiB,oBAAoB,CAAA,EAAG;AACjE,MAAA,KAAA,MAAW,KAAA,IAAA,CAAU,QAAQ,YAAA,CAAa,kBAAkB,KAAK,EAAA,EAAI,KAAA,CAAM,KAAK,CAAA,EAAG;AACjF,QAAA,IAAI,KAAA,EAAO,OAAA,CAAQ,SAAA,CAAU,MAAA,CAAO,KAAK,CAAA;AAAA,MAC3C;AAAA,IACF;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAAiB,mBAAmB,CAAA,EAAG;AAChE,MAAA,IAAI,OAAA,YAAmB,eAAA,EAAiB,OAAA,CAAQ,KAAA,EAAM;AAAA,IACxD;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAAiB,oBAAoB,CAAA,EAAG;AACjE,MAAA,IACE,OAAA,YAAmB,gBAAA,IACnB,OAAA,YAAmB,mBAAA,IACnB,mBAAmB,iBAAA,EACnB;AACA,QAAA,OAAA,CAAQ,KAAA,GAAQ,EAAA;AAAA,MAClB;AAAA,IACF;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAA8B,qBAAqB,CAAA,EAAG;AAC/E,MAAA,OAAA,CAAQ,MAAA,GAAS,IAAA;AAAA,IACnB;AAEA,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAAiB,qBAAqB,CAAA,EAAG;AAClE,MAAA,OAAA,CAAQ,MAAA,EAAO;AAAA,IACjB;AAEA,IAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AAAA,EACvB;AAAA;AAAA,EAGA,UAAA,GAAsB;AACpB,IAAA,IAAI,CAAC,IAAA,CAAK,UAAA,EAAY,OAAO,IAAA,CAAK,OAAA;AAClC,IAAA,OAAO,KAAK,OAAA,CAAQ,aAAA,CAAc,IAAA,CAAK,UAAU,KAAK,IAAA,CAAK,OAAA;AAAA,EAC7D;AACF","file":"reset_before_cache_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless **before-cache reset** — the most Hotwire-specific gap part (no APG\n * pattern). On `turbo:before-cache` it returns transient UI (open menus/modals,\n * typed-in values, spinning indicators) to its initial state, so a page restored by\n * the Back button is not frozen mid-interaction. Place one on `<body>`.\n *\n * Markup contract (identifier: `stimeo--reset-before-cache`):\n * <body data-controller=\"stimeo--reset-before-cache\">\n * <details data-reset-attr=\"open\">…</details> <!-- remove these attributes -->\n * <div data-reset-class=\"is-open is-loading\">…</div> <!-- remove these classes -->\n * <form data-reset-form>…</form> <!-- form.reset() -->\n * <input data-reset-value> <!-- clear the value -->\n * <div data-reset-hidden>transient overlay</div> <!-- re-hide -->\n * <div data-reset-remove>flash toast</div> <!-- drop from the DOM -->\n * </body>\n *\n * @remarks\n * Behavior only and **idempotent** — every run converges on the same initial state,\n * holding no module-scope state. It does the cross-cutting DOM cleanup directly\n * (attribute removal, class removal, value clearing, re-hiding, node removal) and, when\n * `dispatchReset` is on, fires `stimeo--reset-before-cache:request` so individual\n * Stimeo controllers can run their own close logic. The `turbo:before-cache`\n * listener is paired to `connect()` / `disconnect()` so it never double-registers or\n * leaks. {@link reset} is also a public action for manual triggering.\n */\nexport class ResetBeforeCacheController extends Controller<HTMLElement> {\n static override values = {\n scope: { type: String, default: \"\" },\n dispatchReset: { type: Boolean, default: true },\n };\n static actions = [\"reset\"] as const;\n static events = [\"reset\", \"request\"] as const;\n\n declare scopeValue: string;\n declare dispatchResetValue: boolean;\n\n /** Runs the reset just before Turbo caches the snapshot. */\n readonly #onBeforeCache = (): void => this.reset();\n\n override connect(): void {\n document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n }\n\n override disconnect(): void {\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n }\n\n /**\n * Resets transient UI within scope to its initial state. Asks controllers to\n * close (via `request`) first, then applies the declarative `data-reset-*` cleanup,\n * and finally emits `reset`. Safe to call any number of times (idempotent).\n */\n reset(): void {\n const root = this.#scopeRoot();\n\n if (this.dispatchResetValue) this.dispatch(\"request\");\n\n for (const element of root.querySelectorAll(\"[data-reset-attr]\")) {\n for (const name of (element.getAttribute(\"data-reset-attr\") ?? \"\").split(/\\s+/)) {\n if (name) element.removeAttribute(name);\n }\n }\n for (const element of root.querySelectorAll(\"[data-reset-class]\")) {\n for (const token of (element.getAttribute(\"data-reset-class\") ?? \"\").split(/\\s+/)) {\n if (token) element.classList.remove(token);\n }\n }\n for (const element of root.querySelectorAll(\"[data-reset-form]\")) {\n if (element instanceof HTMLFormElement) element.reset();\n }\n for (const element of root.querySelectorAll(\"[data-reset-value]\")) {\n if (\n element instanceof HTMLInputElement ||\n element instanceof HTMLTextAreaElement ||\n element instanceof HTMLSelectElement\n ) {\n element.value = \"\";\n }\n }\n for (const element of root.querySelectorAll<HTMLElement>(\"[data-reset-hidden]\")) {\n element.hidden = true;\n }\n // Removal runs last so it cannot drop a node another rule still needed to visit.\n for (const element of root.querySelectorAll(\"[data-reset-remove]\")) {\n element.remove();\n }\n\n this.dispatch(\"reset\");\n }\n\n /** The scan root: a `scope` descendant when set, else the controller element. */\n #scopeRoot(): Element {\n if (!this.scopeValue) return this.element;\n return this.element.querySelector(this.scopeValue) ?? this.element;\n }\n}\n"]}
@@ -0,0 +1,81 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless, highly accessible Window Splitter / Resizable panes logic.
5
+ *
6
+ * Markup contract (identifier: `stimeo--resizable`):
7
+ * <div data-controller="stimeo--resizable"
8
+ * data-stimeo--resizable-min-value="20"
9
+ * data-stimeo--resizable-max-value="80"
10
+ * data-stimeo--resizable-value-value="50">
11
+ * <div data-stimeo--resizable-target="primary" id="pane-1">Pane A</div>
12
+ * <div role="separator" tabindex="0" aria-orientation="vertical"
13
+ * aria-controls="pane-1" aria-valuemin="20" aria-valuemax="80"
14
+ * aria-valuenow="50" aria-label="Resize splitter"
15
+ * data-stimeo--resizable-target="separator"
16
+ * data-action="pointerdown->stimeo--resizable#onPointerDown
17
+ * keydown->stimeo--resizable#onKeydown"></div>
18
+ * <div data-stimeo--resizable-target="secondary">Pane B</div>
19
+ * </div>
20
+ *
21
+ * Implements the WAI-ARIA APG **Window Splitter** pattern:
22
+ * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging
23
+ * even when pointers quickly stray outside the separator area.
24
+ * - Auto-clamped flex-percentage updates with bounds safety.
25
+ * - Smooth step keyboard adjustments for Arrow keys based on vertical/horizontal orientation.
26
+ * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) to drive presentation styles.
27
+ *
28
+ * @remarks
29
+ * Behavior only. The controller adjusts the CSS custom property on the root element,
30
+ * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.
31
+ */
32
+ declare class ResizableController extends Controller<HTMLElement> {
33
+ #private;
34
+ static targets: string[];
35
+ static values: {
36
+ min: {
37
+ type: NumberConstructor;
38
+ default: number;
39
+ };
40
+ max: {
41
+ type: NumberConstructor;
42
+ default: number;
43
+ };
44
+ step: {
45
+ type: NumberConstructor;
46
+ default: number;
47
+ };
48
+ value: {
49
+ type: NumberConstructor;
50
+ default: number;
51
+ };
52
+ };
53
+ static actions: readonly ["onKeydown", "onPointerDown", "toggle"];
54
+ static events: readonly ["change"];
55
+ readonly primaryTarget: HTMLElement;
56
+ readonly secondaryTarget: HTMLElement;
57
+ readonly separatorTarget: HTMLElement;
58
+ readonly hasPrimaryTarget: boolean;
59
+ readonly hasSecondaryTarget: boolean;
60
+ readonly hasSeparatorTarget: boolean;
61
+ minValue: number;
62
+ maxValue: number;
63
+ stepValue: number;
64
+ valueValue: number;
65
+ connect(): void;
66
+ /** Cancels any active pointer drag so listeners never leak past disconnect. */
67
+ disconnect(): void;
68
+ /**
69
+ * Stimulus lifecycle callback when the valueValue changes.
70
+ * Keeps CSS fractions and ARIA status completely aligned.
71
+ */
72
+ valueValueChanged(): void;
73
+ /** Starts active pointer drag tracking and locks capture. */
74
+ onPointerDown(event: PointerEvent): void;
75
+ /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */
76
+ onKeydown(event: KeyboardEvent): void;
77
+ /** Double-click or Enter to collapse/restore pane to min/max levels. */
78
+ toggle(): void;
79
+ }
80
+
81
+ export { ResizableController };
@@ -0,0 +1,163 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/resizable_controller.ts
4
+ var ResizableController = class extends Controller {
5
+ static targets = ["primary", "secondary", "separator"];
6
+ static values = {
7
+ min: { type: Number, default: 0 },
8
+ max: { type: Number, default: 100 },
9
+ step: { type: Number, default: 1 },
10
+ value: { type: Number, default: 50 }
11
+ };
12
+ static actions = ["onKeydown", "onPointerDown", "toggle"];
13
+ static events = ["change"];
14
+ /** Track previously held value before collapse toggles. */
15
+ #valueBeforeCollapse = 50;
16
+ /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
17
+ #dragAbort = null;
18
+ connect() {
19
+ this.#clampAndSync();
20
+ }
21
+ /** Cancels any active pointer drag so listeners never leak past disconnect. */
22
+ disconnect() {
23
+ this.#dragAbort?.abort();
24
+ this.#dragAbort = null;
25
+ }
26
+ /**
27
+ * Stimulus lifecycle callback when the valueValue changes.
28
+ * Keeps CSS fractions and ARIA status completely aligned.
29
+ */
30
+ valueValueChanged() {
31
+ this.#clampAndSync();
32
+ }
33
+ /** Starts active pointer drag tracking and locks capture. */
34
+ onPointerDown(event) {
35
+ if (!this.hasSeparatorTarget || event.button !== 0) return;
36
+ event.preventDefault();
37
+ const separator = this.separatorTarget;
38
+ separator.setPointerCapture(event.pointerId);
39
+ separator.focus();
40
+ this.element.setAttribute("data-dragging", "true");
41
+ this.#dragAbort?.abort();
42
+ const abort = new AbortController();
43
+ this.#dragAbort = abort;
44
+ separator.addEventListener("pointermove", this.#onPointerMove, { signal: abort.signal });
45
+ separator.addEventListener("pointerup", this.#onPointerUp, { signal: abort.signal });
46
+ separator.addEventListener("pointercancel", this.#onPointerUp, { signal: abort.signal });
47
+ }
48
+ /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */
49
+ onKeydown(event) {
50
+ if (!this.hasSeparatorTarget) return;
51
+ const orientation = this.separatorTarget.getAttribute("aria-orientation") || "vertical";
52
+ const isVertical = orientation === "vertical";
53
+ let handled = true;
54
+ let nextValue = this.valueValue;
55
+ switch (event.key) {
56
+ case "ArrowLeft":
57
+ if (isVertical) {
58
+ nextValue -= this.stepValue;
59
+ } else {
60
+ handled = false;
61
+ }
62
+ break;
63
+ case "ArrowRight":
64
+ if (isVertical) {
65
+ nextValue += this.stepValue;
66
+ } else {
67
+ handled = false;
68
+ }
69
+ break;
70
+ case "ArrowUp":
71
+ if (!isVertical) {
72
+ nextValue -= this.stepValue;
73
+ } else {
74
+ handled = false;
75
+ }
76
+ break;
77
+ case "ArrowDown":
78
+ if (!isVertical) {
79
+ nextValue += this.stepValue;
80
+ } else {
81
+ handled = false;
82
+ }
83
+ break;
84
+ case "Home":
85
+ nextValue = this.minValue;
86
+ break;
87
+ case "End":
88
+ nextValue = this.maxValue;
89
+ break;
90
+ case "Enter":
91
+ event.preventDefault();
92
+ this.toggle();
93
+ return;
94
+ default:
95
+ handled = false;
96
+ break;
97
+ }
98
+ if (handled) {
99
+ event.preventDefault();
100
+ this.valueValue = Math.max(this.minValue, Math.min(nextValue, this.maxValue));
101
+ this.#clampAndSync();
102
+ this.#dispatchChange();
103
+ }
104
+ }
105
+ /** Double-click or Enter to collapse/restore pane to min/max levels. */
106
+ toggle() {
107
+ const threshold = this.minValue + (this.maxValue - this.minValue) / 2;
108
+ if (this.valueValue > this.minValue) {
109
+ this.#valueBeforeCollapse = this.valueValue;
110
+ this.valueValue = this.minValue;
111
+ } else {
112
+ this.valueValue = this.#valueBeforeCollapse >= threshold ? this.#valueBeforeCollapse : this.maxValue;
113
+ }
114
+ this.#clampAndSync();
115
+ this.#dispatchChange();
116
+ }
117
+ #onPointerMove = (event) => {
118
+ if (!this.hasSeparatorTarget) return;
119
+ const rect = this.element.getBoundingClientRect();
120
+ const orientation = this.separatorTarget.getAttribute("aria-orientation") || "vertical";
121
+ const isVertical = orientation === "vertical";
122
+ let fraction = 0.5;
123
+ if (isVertical) {
124
+ fraction = (event.clientX - rect.left) / rect.width;
125
+ } else {
126
+ fraction = (event.clientY - rect.top) / rect.height;
127
+ }
128
+ fraction = Math.max(0, Math.min(fraction, 1));
129
+ const percent = Math.round(fraction * 100);
130
+ this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));
131
+ this.#clampAndSync();
132
+ };
133
+ #onPointerUp = (event) => {
134
+ if (!this.hasSeparatorTarget) return;
135
+ const separator = this.separatorTarget;
136
+ separator.releasePointerCapture(event.pointerId);
137
+ this.element.removeAttribute("data-dragging");
138
+ this.#dragAbort?.abort();
139
+ this.#dragAbort = null;
140
+ this.#dispatchChange();
141
+ };
142
+ #clampAndSync() {
143
+ const clamped = Math.max(this.minValue, Math.min(this.valueValue, this.maxValue));
144
+ if (this.valueValue !== clamped) {
145
+ this.valueValue = clamped;
146
+ }
147
+ const fraction = clamped / 100;
148
+ this.element.style.setProperty("--stimeo--resizable-fraction", String(fraction));
149
+ if (this.hasSeparatorTarget) {
150
+ this.separatorTarget.setAttribute("aria-valuenow", String(clamped));
151
+ this.separatorTarget.setAttribute("aria-valuemin", String(this.minValue));
152
+ this.separatorTarget.setAttribute("aria-valuemax", String(this.maxValue));
153
+ }
154
+ }
155
+ #dispatchChange() {
156
+ const fraction = this.valueValue / 100;
157
+ this.dispatch("change", { detail: { value: this.valueValue, fraction } });
158
+ }
159
+ };
160
+
161
+ export { ResizableController };
162
+ //# sourceMappingURL=resizable_controller.js.map
163
+ //# sourceMappingURL=resizable_controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/controllers/resizable_controller.ts"],"names":[],"mappings":"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","file":"resizable_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, highly accessible Window Splitter / Resizable panes logic.\n *\n * Markup contract (identifier: `stimeo--resizable`):\n * <div data-controller=\"stimeo--resizable\"\n * data-stimeo--resizable-min-value=\"20\"\n * data-stimeo--resizable-max-value=\"80\"\n * data-stimeo--resizable-value-value=\"50\">\n * <div data-stimeo--resizable-target=\"primary\" id=\"pane-1\">Pane A</div>\n * <div role=\"separator\" tabindex=\"0\" aria-orientation=\"vertical\"\n * aria-controls=\"pane-1\" aria-valuemin=\"20\" aria-valuemax=\"80\"\n * aria-valuenow=\"50\" aria-label=\"Resize splitter\"\n * data-stimeo--resizable-target=\"separator\"\n * data-action=\"pointerdown->stimeo--resizable#onPointerDown\n * keydown->stimeo--resizable#onKeydown\"></div>\n * <div data-stimeo--resizable-target=\"secondary\">Pane B</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Window Splitter** pattern:\n * - Robust dragging track using W3C `setPointerCapture` to ensure uninterrupted dragging\n * even when pointers quickly stray outside the separator area.\n * - Auto-clamped flex-percentage updates with bounds safety.\n * - Smooth step keyboard adjustments for Arrow keys based on vertical/horizontal orientation.\n * - Root-level CSS custom property `--stimeo--resizable-fraction` (0..1) to drive presentation styles.\n *\n * @remarks\n * Behavior only. The controller adjusts the CSS custom property on the root element,\n * updates ARIA values on the separator, and emits `stimeo--resizable:change` events.\n */\nexport class ResizableController extends Controller<HTMLElement> {\n static override targets = [\"primary\", \"secondary\", \"separator\"];\n static override values = {\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n step: { type: Number, default: 1 },\n value: { type: Number, default: 50 },\n };\n static actions = [\"onKeydown\", \"onPointerDown\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly primaryTarget: HTMLElement;\n declare readonly secondaryTarget: HTMLElement;\n declare readonly separatorTarget: HTMLElement;\n declare readonly hasPrimaryTarget: boolean;\n declare readonly hasSecondaryTarget: boolean;\n declare readonly hasSeparatorTarget: boolean;\n\n declare minValue: number;\n declare maxValue: number;\n declare stepValue: number;\n declare valueValue: number;\n\n /** Track previously held value before collapse toggles. */\n #valueBeforeCollapse = 50;\n\n /** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */\n #dragAbort: AbortController | null = null;\n\n override connect(): void {\n this.#clampAndSync();\n }\n\n /** Cancels any active pointer drag so listeners never leak past disconnect. */\n override disconnect(): void {\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n }\n\n /**\n * Stimulus lifecycle callback when the valueValue changes.\n * Keeps CSS fractions and ARIA status completely aligned.\n */\n valueValueChanged(): void {\n this.#clampAndSync();\n }\n\n /** Starts active pointer drag tracking and locks capture. */\n onPointerDown(event: PointerEvent): void {\n if (!this.hasSeparatorTarget || event.button !== 0) return;\n\n event.preventDefault();\n\n const separator = this.separatorTarget;\n separator.setPointerCapture(event.pointerId);\n // preventDefault() above suppresses the implicit focus, so move focus\n // explicitly — otherwise keyboard (arrow) adjustments never reach the\n // separator after a pointer interaction (WCAG 2.1.1).\n separator.focus();\n\n this.element.setAttribute(\"data-dragging\", \"true\");\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n separator.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n separator.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n separator.addEventListener(\"pointercancel\", this.#onPointerUp, { signal: abort.signal });\n }\n\n /** Keydown adjustments for ArrowUp/Down/Left/Right and Home/End. */\n onKeydown(event: KeyboardEvent): void {\n if (!this.hasSeparatorTarget) return;\n\n const orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let handled = true;\n let nextValue = this.valueValue;\n\n switch (event.key) {\n case \"ArrowLeft\":\n if (isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowRight\":\n if (isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowUp\":\n if (!isVertical) {\n nextValue -= this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"ArrowDown\":\n if (!isVertical) {\n nextValue += this.stepValue;\n } else {\n handled = false;\n }\n break;\n case \"Home\":\n nextValue = this.minValue;\n break;\n case \"End\":\n nextValue = this.maxValue;\n break;\n case \"Enter\":\n event.preventDefault();\n this.toggle();\n return;\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n event.preventDefault();\n this.valueValue = Math.max(this.minValue, Math.min(nextValue, this.maxValue));\n this.#clampAndSync();\n this.#dispatchChange();\n }\n }\n\n /** Double-click or Enter to collapse/restore pane to min/max levels. */\n toggle(): void {\n const threshold = this.minValue + (this.maxValue - this.minValue) / 2;\n if (this.valueValue > this.minValue) {\n this.#valueBeforeCollapse = this.valueValue;\n this.valueValue = this.minValue;\n } else {\n this.valueValue =\n this.#valueBeforeCollapse >= threshold ? this.#valueBeforeCollapse : this.maxValue;\n }\n this.#clampAndSync();\n this.#dispatchChange();\n }\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const rect = this.element.getBoundingClientRect();\n const orientation = this.separatorTarget.getAttribute(\"aria-orientation\") || \"vertical\";\n const isVertical = orientation === \"vertical\";\n\n let fraction = 0.5;\n\n if (isVertical) {\n fraction = (event.clientX - rect.left) / rect.width;\n } else {\n fraction = (event.clientY - rect.top) / rect.height;\n }\n\n // Restrict within absolute boundaries\n fraction = Math.max(0, Math.min(fraction, 1));\n const percent = Math.round(fraction * 100);\n\n this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));\n this.#clampAndSync();\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n if (!this.hasSeparatorTarget) return;\n\n const separator = this.separatorTarget;\n separator.releasePointerCapture(event.pointerId);\n\n this.element.removeAttribute(\"data-dragging\");\n\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n\n this.#dispatchChange();\n };\n\n #clampAndSync(): void {\n const clamped = Math.max(this.minValue, Math.min(this.valueValue, this.maxValue));\n\n // Keep the stored value in range so valueValue, ARIA, CSS, and dispatched\n // events never diverge (e.g. an initial value above max). Writing back\n // re-enters valueValueChanged once, but the value is already clamped there\n // so it does not recurse.\n if (this.valueValue !== clamped) {\n this.valueValue = clamped;\n }\n\n // Expose local fraction to consumer CSS custom property\n const fraction = clamped / 100;\n this.element.style.setProperty(\"--stimeo--resizable-fraction\", String(fraction));\n\n if (this.hasSeparatorTarget) {\n this.separatorTarget.setAttribute(\"aria-valuenow\", String(clamped));\n this.separatorTarget.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.separatorTarget.setAttribute(\"aria-valuemax\", String(this.maxValue));\n }\n }\n\n #dispatchChange(): void {\n const fraction = this.valueValue / 100;\n this.dispatch(\"change\", { detail: { value: this.valueValue, fraction } });\n }\n}\n"]}
@@ -0,0 +1,63 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ /**
4
+ * Headless **roving tabindex**: makes a set of `item`s a single Tab stop and
5
+ * moves focus between them with the arrow keys — the APG roving-tabindex
6
+ * technique, surfaced as a standalone controller. It is the policy layer over the
7
+ * shared {@link RovingTabindex} util (counterpart to Focus Scope over `FocusTrap`),
8
+ * giving the orientation / wrap / Home-End the util deliberately leaves out. No
9
+ * dedicated APG pattern; it is the keyboard primitive Toolbar / Menu / Radio Group
10
+ * and friends build on. Core (zero dependencies).
11
+ *
12
+ * Markup contract (identifier: `stimeo--roving`):
13
+ * <div data-controller="stimeo--roving"
14
+ * data-stimeo--roving-orientation-value="horizontal">
15
+ * <button data-stimeo--roving-target="item">A</button>
16
+ * <button data-stimeo--roving-target="item">B</button>
17
+ * <button data-stimeo--roving-target="item">C</button>
18
+ * </div>
19
+ *
20
+ * Exactly one item is tabbable (`tabindex="0"`); the arrow keys (per
21
+ * `orientation`) move focus and that tab stop together, `Home`/`End` jump to the
22
+ * ends (`homeEnd`), and `wrap` cycles past the ends or clamps at them. Listeners
23
+ * are **delegated on the container** — `keydown` for movement and `focusin` to
24
+ * sync the tab stop when focus arrives by click or programmatically — so
25
+ * dynamically added/removed items need no per-item `data-action`. It emits
26
+ * `change` whenever the tabbable item changes.
27
+ *
28
+ * @remarks
29
+ * Behavior only: it owns `tabindex` and focus movement, nothing else. It does
30
+ * **not** assign roles (`role="toolbar"`/`"radiogroup"` is the author's), manage
31
+ * selection / selection-follows-focus, typeahead, or activation (`Enter`/`Space`)
32
+ * — those stay with the consuming pattern. `connect()` is idempotent: it keeps an
33
+ * existing tab stop (reads it back from the DOM) and only defaults to the first
34
+ * item when none is set, so a Turbo cache restore / morph never resets the user's
35
+ * position. The delegated listeners are torn down on `disconnect()`.
36
+ */
37
+ declare class RovingController extends Controller<HTMLElement> {
38
+ #private;
39
+ static targets: string[];
40
+ static values: {
41
+ orientation: {
42
+ type: StringConstructor;
43
+ default: string;
44
+ };
45
+ wrap: {
46
+ type: BooleanConstructor;
47
+ default: boolean;
48
+ };
49
+ homeEnd: {
50
+ type: BooleanConstructor;
51
+ default: boolean;
52
+ };
53
+ };
54
+ static events: readonly ["change"];
55
+ readonly itemTargets: HTMLElement[];
56
+ orientationValue: string;
57
+ wrapValue: boolean;
58
+ homeEndValue: boolean;
59
+ connect(): void;
60
+ disconnect(): void;
61
+ }
62
+
63
+ export { RovingController };
@@ -0,0 +1,116 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/roving_controller.ts
4
+
5
+ // src/utils/roving_tabindex.ts
6
+ var RovingTabindex = class {
7
+ /** Returns the current ordered item elements; called on every operation. */
8
+ #getItems;
9
+ /**
10
+ * @param getItems - Returns the current ordered item elements. Called on every
11
+ * operation so the live target list is always used.
12
+ */
13
+ constructor(getItems) {
14
+ this.#getItems = getItems;
15
+ }
16
+ /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
17
+ get activeIndex() {
18
+ return this.#getItems().findIndex((item) => item.tabIndex === 0);
19
+ }
20
+ /**
21
+ * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
22
+ * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
23
+ * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
24
+ * "nothing is currently tabbable".
25
+ *
26
+ * @param index - Position of the item to make tabbable.
27
+ * @param options - Pass `{ focus: true }` to also move DOM focus to that item.
28
+ */
29
+ setActive(index, { focus = false } = {}) {
30
+ const items = this.#getItems();
31
+ items.forEach((item, i) => {
32
+ item.tabIndex = i === index ? 0 : -1;
33
+ });
34
+ if (focus) items[index]?.focus();
35
+ }
36
+ };
37
+ function rovingMove(current, length, delta, wrap) {
38
+ if (length === 0) return -1;
39
+ const next = current + delta;
40
+ if (wrap === "wrap") return (next + length) % length;
41
+ return Math.min(length - 1, Math.max(0, next));
42
+ }
43
+
44
+ // src/controllers/roving_controller.ts
45
+ var RovingController = class extends Controller {
46
+ static targets = ["item"];
47
+ static values = {
48
+ orientation: { type: String, default: "horizontal" },
49
+ wrap: { type: Boolean, default: true },
50
+ homeEnd: { type: Boolean, default: true }
51
+ };
52
+ static events = ["change"];
53
+ #roving = new RovingTabindex(() => this.itemTargets);
54
+ connect() {
55
+ const active = this.#roving.activeIndex;
56
+ this.#roving.setActive(active === -1 ? 0 : active);
57
+ this.element.addEventListener("keydown", this.#onKeydown);
58
+ this.element.addEventListener("focusin", this.#onFocusin);
59
+ }
60
+ disconnect() {
61
+ this.element.removeEventListener("keydown", this.#onKeydown);
62
+ this.element.removeEventListener("focusin", this.#onFocusin);
63
+ }
64
+ /** Arrow keys move focus + the tab stop; Home/End jump to the ends. */
65
+ #onKeydown = (event) => {
66
+ const items = this.itemTargets;
67
+ const current = this.#indexOf(event.target);
68
+ if (current === -1) return;
69
+ const length = items.length;
70
+ const wrap = this.wrapValue ? "wrap" : "clamp";
71
+ const orientation = this.orientationValue;
72
+ const horizontal = orientation === "horizontal" || orientation === "both";
73
+ const vertical = orientation === "vertical" || orientation === "both";
74
+ let next;
75
+ if (horizontal && event.key === "ArrowRight" || vertical && event.key === "ArrowDown") {
76
+ next = rovingMove(current, length, 1, wrap);
77
+ } else if (horizontal && event.key === "ArrowLeft" || vertical && event.key === "ArrowUp") {
78
+ next = rovingMove(current, length, -1, wrap);
79
+ } else if (this.homeEndValue && event.key === "Home") {
80
+ next = 0;
81
+ } else if (this.homeEndValue && event.key === "End") {
82
+ next = length - 1;
83
+ } else {
84
+ return;
85
+ }
86
+ event.preventDefault();
87
+ this.#activate(next, true);
88
+ };
89
+ /**
90
+ * Syncs the single tab stop to an item that received focus by other means
91
+ * (click, programmatic `focus()`), so returning via Tab lands on it. The
92
+ * keyboard path's own `focus()` re-enters here but is a no-op (index unchanged).
93
+ */
94
+ #onFocusin = (event) => {
95
+ const index = this.#indexOf(event.target);
96
+ if (index !== -1) this.#activate(index, false);
97
+ };
98
+ /** Resolves the item index owning an event target (the item or a descendant). */
99
+ #indexOf(target) {
100
+ const node = target;
101
+ if (!node) return -1;
102
+ return this.itemTargets.findIndex((item) => item === node || item.contains(node));
103
+ }
104
+ /** Makes `index` the tab stop (optionally focusing it), emitting `change` once. */
105
+ #activate(index, focus) {
106
+ const previous = this.#roving.activeIndex;
107
+ this.#roving.setActive(index, { focus });
108
+ if (index !== previous) {
109
+ this.dispatch("change", { detail: { index, item: this.itemTargets[index] } });
110
+ }
111
+ }
112
+ };
113
+
114
+ export { RovingController };
115
+ //# sourceMappingURL=roving_controller.js.map
116
+ //# sourceMappingURL=roving_controller.js.map