stimeo-ui 0.2.1 → 0.3.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 (184) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/cable/index.d.ts +16 -16
  3. package/dist/cable/index.js.map +1 -1
  4. package/dist/controllers/accordion_controller.js +10 -0
  5. package/dist/controllers/accordion_controller.js.map +1 -1
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  7. package/dist/controllers/breadcrumb_controller.d.ts +97 -10
  8. package/dist/controllers/breadcrumb_controller.js +225 -13
  9. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  10. package/dist/controllers/calendar_controller.d.ts +11 -1
  11. package/dist/controllers/calendar_controller.js +89 -22
  12. package/dist/controllers/calendar_controller.js.map +1 -1
  13. package/dist/controllers/carousel_controller.d.ts +16 -1
  14. package/dist/controllers/carousel_controller.js +47 -6
  15. package/dist/controllers/carousel_controller.js.map +1 -1
  16. package/dist/controllers/collapsible_controller.js +2 -2
  17. package/dist/controllers/collapsible_controller.js.map +1 -1
  18. package/dist/controllers/color_picker_controller.d.ts +12 -1
  19. package/dist/controllers/color_picker_controller.js +46 -7
  20. package/dist/controllers/color_picker_controller.js.map +1 -1
  21. package/dist/controllers/combobox_controller.d.ts +14 -1
  22. package/dist/controllers/combobox_controller.js +162 -23
  23. package/dist/controllers/combobox_controller.js.map +1 -1
  24. package/dist/controllers/command_palette_controller.d.ts +2 -0
  25. package/dist/controllers/command_palette_controller.js +194 -17
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/confirm_controller.js.map +1 -1
  28. package/dist/controllers/context_menu_controller.d.ts +7 -6
  29. package/dist/controllers/context_menu_controller.js +32 -10
  30. package/dist/controllers/context_menu_controller.js.map +1 -1
  31. package/dist/controllers/count_up_controller.js.map +1 -1
  32. package/dist/controllers/data_grid_controller.d.ts +19 -1
  33. package/dist/controllers/data_grid_controller.js +82 -4
  34. package/dist/controllers/data_grid_controller.js.map +1 -1
  35. package/dist/controllers/date_range_picker_controller.d.ts +1 -0
  36. package/dist/controllers/date_range_picker_controller.js +27 -3
  37. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  38. package/dist/controllers/dialog_controller.js.map +1 -1
  39. package/dist/controllers/dismissible_controller.js.map +1 -1
  40. package/dist/controllers/drawer_controller.js.map +1 -1
  41. package/dist/controllers/dropdown_controller.d.ts +1 -2
  42. package/dist/controllers/dropdown_controller.js.map +1 -1
  43. package/dist/controllers/editable_controller.js +1 -0
  44. package/dist/controllers/editable_controller.js.map +1 -1
  45. package/dist/controllers/focus_controller.d.ts +1 -1
  46. package/dist/controllers/focus_controller.js.map +1 -1
  47. package/dist/controllers/form_field_controller.js.map +1 -1
  48. package/dist/controllers/form_validation_controller.js +1 -1
  49. package/dist/controllers/form_validation_controller.js.map +1 -1
  50. package/dist/controllers/highlight_controller.js.map +1 -1
  51. package/dist/controllers/hover_card_controller.d.ts +1 -1
  52. package/dist/controllers/hover_card_controller.js.map +1 -1
  53. package/dist/controllers/intersection_controller.js +36 -11
  54. package/dist/controllers/intersection_controller.js.map +1 -1
  55. package/dist/controllers/lazy_frame_controller.js +31 -10
  56. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  57. package/dist/controllers/listbox_controller.d.ts +9 -2
  58. package/dist/controllers/listbox_controller.js +257 -53
  59. package/dist/controllers/listbox_controller.js.map +1 -1
  60. package/dist/controllers/local_time_controller.js +2 -2
  61. package/dist/controllers/local_time_controller.js.map +1 -1
  62. package/dist/controllers/menu_controller.d.ts +29 -11
  63. package/dist/controllers/menu_controller.js +104 -17
  64. package/dist/controllers/menu_controller.js.map +1 -1
  65. package/dist/controllers/menubar_controller.d.ts +91 -11
  66. package/dist/controllers/menubar_controller.js +415 -63
  67. package/dist/controllers/menubar_controller.js.map +1 -1
  68. package/dist/controllers/meter_controller.js.map +1 -1
  69. package/dist/controllers/multi_select_controller.d.ts +31 -5
  70. package/dist/controllers/multi_select_controller.js +312 -29
  71. package/dist/controllers/multi_select_controller.js.map +1 -1
  72. package/dist/controllers/navigation_menu_controller.d.ts +65 -18
  73. package/dist/controllers/navigation_menu_controller.js +154 -27
  74. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  75. package/dist/controllers/nested_form_controller.d.ts +4 -5
  76. package/dist/controllers/nested_form_controller.js.map +1 -1
  77. package/dist/controllers/number_input_controller.d.ts +1 -1
  78. package/dist/controllers/number_input_controller.js +7 -0
  79. package/dist/controllers/number_input_controller.js.map +1 -1
  80. package/dist/controllers/otp_controller.js +18 -1
  81. package/dist/controllers/otp_controller.js.map +1 -1
  82. package/dist/controllers/overflow_indicator_controller.d.ts +3 -2
  83. package/dist/controllers/overflow_indicator_controller.js +81 -13
  84. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  85. package/dist/controllers/overflow_menu_controller.d.ts +78 -19
  86. package/dist/controllers/overflow_menu_controller.js +381 -57
  87. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  88. package/dist/controllers/pagination_controller.d.ts +36 -5
  89. package/dist/controllers/pagination_controller.js +163 -32
  90. package/dist/controllers/pagination_controller.js.map +1 -1
  91. package/dist/controllers/persist_controller.js +6 -6
  92. package/dist/controllers/persist_controller.js.map +1 -1
  93. package/dist/controllers/pointer_drag_controller.d.ts +2 -3
  94. package/dist/controllers/pointer_drag_controller.js +9 -1
  95. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  96. package/dist/controllers/popover_controller.d.ts +1 -2
  97. package/dist/controllers/popover_controller.js +2 -2
  98. package/dist/controllers/popover_controller.js.map +1 -1
  99. package/dist/controllers/portal_controller.d.ts +4 -4
  100. package/dist/controllers/portal_controller.js.map +1 -1
  101. package/dist/controllers/progress_controller.js.map +1 -1
  102. package/dist/controllers/radio_group_controller.js +22 -3
  103. package/dist/controllers/radio_group_controller.js.map +1 -1
  104. package/dist/controllers/range_slider_controller.d.ts +14 -4
  105. package/dist/controllers/range_slider_controller.js +32 -6
  106. package/dist/controllers/range_slider_controller.js.map +1 -1
  107. package/dist/controllers/rating_controller.js +16 -2
  108. package/dist/controllers/rating_controller.js.map +1 -1
  109. package/dist/controllers/read_more_controller.js +63 -19
  110. package/dist/controllers/read_more_controller.js.map +1 -1
  111. package/dist/controllers/reading_progress_controller.js.map +1 -1
  112. package/dist/controllers/resizable_controller.d.ts +3 -1
  113. package/dist/controllers/resizable_controller.js +65 -1
  114. package/dist/controllers/resizable_controller.js.map +1 -1
  115. package/dist/controllers/roving_controller.js +17 -2
  116. package/dist/controllers/roving_controller.js.map +1 -1
  117. package/dist/controllers/scroll_area_controller.js +86 -12
  118. package/dist/controllers/scroll_area_controller.js.map +1 -1
  119. package/dist/controllers/scroll_restore_controller.js +1 -1
  120. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  121. package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
  122. package/dist/controllers/scroll_visibility_controller.js +33 -3
  123. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  124. package/dist/controllers/scrollspy_controller.d.ts +64 -15
  125. package/dist/controllers/scrollspy_controller.js +346 -73
  126. package/dist/controllers/scrollspy_controller.js.map +1 -1
  127. package/dist/controllers/separator_controller.d.ts +20 -5
  128. package/dist/controllers/separator_controller.js +9 -0
  129. package/dist/controllers/separator_controller.js.map +1 -1
  130. package/dist/controllers/sidebar_controller.d.ts +1 -1
  131. package/dist/controllers/sidebar_controller.js.map +1 -1
  132. package/dist/controllers/skeleton_controller.js +1 -1
  133. package/dist/controllers/skeleton_controller.js.map +1 -1
  134. package/dist/controllers/slider_controller.d.ts +11 -1
  135. package/dist/controllers/slider_controller.js +32 -6
  136. package/dist/controllers/slider_controller.js.map +1 -1
  137. package/dist/controllers/sortable_controller.d.ts +3 -1
  138. package/dist/controllers/sortable_controller.js +34 -3
  139. package/dist/controllers/sortable_controller.js.map +1 -1
  140. package/dist/controllers/spinner_controller.js +1 -1
  141. package/dist/controllers/spinner_controller.js.map +1 -1
  142. package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
  143. package/dist/controllers/stick_to_bottom_controller.js +2 -1
  144. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  145. package/dist/controllers/sticky_observer_controller.js +32 -11
  146. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  147. package/dist/controllers/submit_once_controller.d.ts +2 -2
  148. package/dist/controllers/submit_once_controller.js.map +1 -1
  149. package/dist/controllers/switch_controller.js +1 -0
  150. package/dist/controllers/switch_controller.js.map +1 -1
  151. package/dist/controllers/tabs_controller.d.ts +11 -3
  152. package/dist/controllers/tabs_controller.js +26 -3
  153. package/dist/controllers/tabs_controller.js.map +1 -1
  154. package/dist/controllers/tags_input_controller.js +22 -2
  155. package/dist/controllers/tags_input_controller.js.map +1 -1
  156. package/dist/controllers/theme_controller.d.ts +1 -1
  157. package/dist/controllers/theme_controller.js +22 -3
  158. package/dist/controllers/theme_controller.js.map +1 -1
  159. package/dist/controllers/time_picker_controller.js +20 -1
  160. package/dist/controllers/time_picker_controller.js.map +1 -1
  161. package/dist/controllers/toast_controller.d.ts +4 -5
  162. package/dist/controllers/toast_controller.js +4 -5
  163. package/dist/controllers/toast_controller.js.map +1 -1
  164. package/dist/controllers/toggle_group_controller.js +23 -2
  165. package/dist/controllers/toggle_group_controller.js.map +1 -1
  166. package/dist/controllers/toolbar_controller.d.ts +53 -13
  167. package/dist/controllers/toolbar_controller.js +230 -31
  168. package/dist/controllers/toolbar_controller.js.map +1 -1
  169. package/dist/controllers/tooltip_controller.d.ts +1 -1
  170. package/dist/controllers/tooltip_controller.js.map +1 -1
  171. package/dist/controllers/transition_controller.js.map +1 -1
  172. package/dist/controllers/tree_view_controller.d.ts +87 -6
  173. package/dist/controllers/tree_view_controller.js +467 -51
  174. package/dist/controllers/tree_view_controller.js.map +1 -1
  175. package/dist/index.js +3514 -689
  176. package/dist/index.js.map +1 -1
  177. package/dist/inspector/cli.d.ts +375 -39
  178. package/dist/inspector/cli.js +228 -14
  179. package/dist/inspector/cli.js.map +1 -1
  180. package/dist/inspector/cli_bin.js +419 -48
  181. package/dist/inspector/cli_bin.js.map +1 -1
  182. package/dist/inspector/examples.json +24 -24
  183. package/dist/inspector/manifest.json +1040 -125
  184. package/package.json +5 -6
@@ -2,6 +2,24 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/theme_controller.ts
4
4
 
5
+ // src/utils/logical_scroll.ts
6
+ function isRtl(element) {
7
+ return window.getComputedStyle(element).direction === "rtl";
8
+ }
9
+
10
+ // src/utils/arrow_step.ts
11
+ function logicalArrowStep(key, element) {
12
+ if (key === "ArrowDown") return 1;
13
+ if (key === "ArrowUp") return -1;
14
+ if (key !== "ArrowRight" && key !== "ArrowLeft") return 0;
15
+ const forward = isRtl(element) ? "ArrowLeft" : "ArrowRight";
16
+ return key === forward ? 1 : -1;
17
+ }
18
+ function isReservedArrowChord(event, allow = []) {
19
+ if (!event.key.startsWith("Arrow")) return false;
20
+ return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
21
+ }
22
+
5
23
  // src/utils/safe_storage.ts
6
24
  function readLocalStorage(key) {
7
25
  try {
@@ -41,6 +59,8 @@ var ThemeController = class extends Controller {
41
59
  };
42
60
  /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */
43
61
  #onKeydown = (event) => {
62
+ if (event.defaultPrevented) return;
63
+ if (isReservedArrowChord(event)) return;
44
64
  const options = this.optionTargets;
45
65
  if (options.length === 0) return;
46
66
  const target = event.target;
@@ -48,14 +68,13 @@ var ThemeController = class extends Controller {
48
68
  if (current === -1) return;
49
69
  const last = options.length - 1;
50
70
  let next = current;
71
+ const step = logicalArrowStep(event.key, this.element);
51
72
  switch (event.key) {
52
73
  case "ArrowDown":
53
74
  case "ArrowRight":
54
- next = current === last ? 0 : current + 1;
55
- break;
56
75
  case "ArrowUp":
57
76
  case "ArrowLeft":
58
- next = current === 0 ? last : current - 1;
77
+ next = step === 1 ? current === last ? 0 : current + 1 : current === 0 ? last : current - 1;
59
78
  break;
60
79
  case "Home":
61
80
  next = 0;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":";;;;;AAwBO,SAAS,iBAAiB,GAAA,EAA4B;AAC3D,EAAA,IAAI;AACF,IAAA,OAAO,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAG,CAAA;AAAA,EACxC,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AASO,SAAS,iBAAA,CAAkB,KAAa,KAAA,EAAqB;AAClE,EAAA,IAAI;AACF,IAAA,MAAA,CAAO,YAAA,CAAa,OAAA,CAAQ,GAAA,EAAK,KAAK,CAAA;AAAA,EACxC,CAAA,CAAA,MAAQ;AAAA,EAER;AACF;;;ACrCA,IAAM,KAAA,GAA8B,CAAC,OAAA,EAAS,MAAA,EAAQ,QAAQ,CAAA;AAC9D,IAAM,MAAA,GAAS,CAAC,KAAA,KACd,OAAO,UAAU,QAAA,IAAa,KAAA,CAA4B,SAAS,KAAK,CAAA;AAqCnE,IAAM,eAAA,GAAN,cAA8B,UAAA,CAAwB;AAAA,EAC3D,OAAgB,OAAA,GAAU,CAAC,QAAQ,CAAA;AAAA,EACnC,OAAgB,MAAA,GAAS;AAAA,IACvB,IAAA,EAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,QAAA,EAAS;AAAA,IACxC,UAAA,EAAY,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,cAAA,EAAe;AAAA,IACpD,MAAA,EAAQ,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,MAAA;AAAO,GAC1C;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,KAAA,EAAO,QAAQ,CAAA;AAAA,EACjC,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAUzB,MAAA,GAAgC,IAAA;AAAA;AAAA,EAGvB,iBAAiB,MAAY;AACpC,IAAA,IAAI,IAAA,CAAK,cAAc,QAAA,EAAU;AAC/B,MAAA,IAAA,CAAK,WAAA,EAAY;AAGjB,MAAA,IAAA,CAAK,aAAA,EAAc;AACnB,MAAA,IAAA,CAAK,eAAA,EAAgB;AAAA,IACvB;AAAA,EACF,CAAA;AAAA;AAAA,EAGS,UAAA,GAAa,CAAC,KAAA,KAA+B;AAEpD,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,IAAI,OAAA,CAAQ,WAAW,CAAA,EAAG;AAC1B,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,MAAqB,CAAA;AACrD,IAAA,IAAI,YAAY,EAAA,EAAI;AAEpB,IAAA,MAAM,IAAA,GAAO,QAAQ,MAAA,GAAS,CAAA;AAC9B,IAAA,IAAI,IAAA,GAAO,OAAA;AACX,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,WAAA;AAAA,MACL,KAAK,YAAA;AACH,QAAA,IAAA,GAAO,OAAA,KAAY,IAAA,GAAO,CAAA,GAAI,OAAA,GAAU,CAAA;AACxC,QAAA;AAAA,MACF,KAAK,SAAA;AAAA,MACL,KAAK,WAAA;AACH,QAAA,IAAA,GAAO,OAAA,KAAY,CAAA,GAAI,IAAA,GAAO,OAAA,GAAU,CAAA;AACxC,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,IAAA,GAAO,CAAA;AACP,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,IAAA,GAAO,IAAA;AACP,QAAA;AAAA,MACF;AACE,QAAA;AAAA;AAEJ,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,MAAM,MAAA,GAAS,QAAQ,IAAI,CAAA;AAC3B,IAAA,IAAI,CAAC,MAAA,EAAQ;AACb,IAAA,MAAA,CAAO,KAAA,EAAM;AACb,IAAA,IAAA,CAAK,QAAA,CAAS,IAAA,CAAK,WAAA,CAAY,MAAM,CAAC,CAAA;AAAA,EACxC,CAAA;AAAA,EAES,OAAA,GAAgB;AACvB,IAAA,MAAM,MAAA,GAAS,KAAK,WAAA,EAAY;AAChC,IAAA,IAAI,MAAA,OAAa,SAAA,GAAY,MAAA;AAE7B,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA,CAAO,UAAA,GAAa,8BAA8B,CAAA,IAAK,IAAA;AACrE,IAAA,IAAA,CAAK,MAAA,EAAQ,gBAAA,CAAiB,QAAA,EAAU,IAAA,CAAK,cAAc,CAAA;AAC3D,IAAA,IAAI,KAAK,eAAA,EAAiB,IAAA,CAAK,QAAQ,gBAAA,CAAiB,SAAA,EAAW,KAAK,UAAU,CAAA;AAElF,IAAA,IAAA,CAAK,WAAA,EAAY;AACjB,IAAA,IAAA,CAAK,aAAA,EAAc;AAAA,EACrB;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,MAAA,EAAQ,mBAAA,CAAoB,QAAA,EAAU,IAAA,CAAK,cAAc,CAAA;AAC9D,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAAA,EAC7D;AAAA;AAAA,EAGA,IAAI,KAAA,EAAoB;AACtB,IAAA,MAAM,IAAA,GAAQ,MAA+C,MAAA,EAAQ,IAAA;AACrE,IAAA,IAAI,MAAA,CAAO,IAAI,CAAA,EAAG,IAAA,CAAK,SAAS,IAAI,CAAA;AAAA,EACtC;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,SAAS,IAAA,CAAK,SAAA,EAAU,KAAM,MAAA,GAAS,UAAU,MAAM,CAAA;AAAA,EAC9D;AAAA;AAAA,EAGA,SAAS,IAAA,EAAuB;AAC9B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AACjB,IAAA,IAAA,CAAK,aAAa,IAAI,CAAA;AACtB,IAAA,IAAA,CAAK,WAAA,EAAY;AACjB,IAAA,IAAA,CAAK,aAAA,EAAc;AACnB,IAAA,IAAA,CAAK,eAAA,EAAgB;AAAA,EACvB;AAAA;AAAA,EAGA,WAAA,GAAoB;AAClB,IAAA,MAAM,IAAA,GAAO,KAAK,cAAA,EAAe;AACjC,IAAA,IAAI,CAAC,IAAA,EAAM;AACX,IAAA,MAAM,QAAA,GAAW,KAAK,SAAA,EAAU;AAChC,IAAA,IAAA,CAAK,YAAA,CAAa,cAAc,QAAQ,CAAA;AACxC,IAAA,IAAA,CAAK,KAAA,CAAM,WAAA,CAAY,cAAA,EAAgB,QAAQ,CAAA;AAAA,EACjD;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,MAAM,UAAU,IAAA,CAAK,aAAA;AACrB,IAAA,IAAI,OAAA,CAAQ,SAAS,CAAA,EAAG;AACtB,MAAA,IAAI,WAAA,GAAc,KAAA;AAClB,MAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,QAAA,MAAM,QAAA,GAAW,IAAA,CAAK,WAAA,CAAY,MAAM,MAAM,IAAA,CAAK,SAAA;AACnD,QAAA,MAAA,CAAO,YAAA,CAAa,cAAA,EAAgB,MAAA,CAAO,QAAQ,CAAC,CAAA;AACpD,QAAA,MAAA,CAAO,QAAA,GAAW,WAAW,CAAA,GAAI,EAAA;AACjC,QAAA,WAAA,KAAgB,QAAA;AAAA,MAClB;AAEA,MAAA,MAAM,KAAA,GAAQ,QAAQ,CAAC,CAAA;AACvB,MAAA,IAAI,CAAC,WAAA,IAAe,KAAA,EAAO,KAAA,CAAM,QAAA,GAAW,CAAA;AAC5C,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,OAAA,CAAQ,aAAa,cAAA,EAAgB,MAAA,CAAO,KAAK,SAAA,EAAU,KAAM,MAAM,CAAC,CAAA;AAAA,EAC/E;AAAA;AAAA,EAGA,eAAA,GAAwB;AACtB,IAAA,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,EAAE,MAAA,EAAQ,EAAE,IAAA,EAAM,IAAA,CAAK,SAAA,EAAW,QAAA,EAAU,IAAA,CAAK,SAAA,EAAU,IAAK,CAAA;AAAA,EAC1F;AAAA;AAAA,EAGA,SAAA,GAA2B;AACzB,IAAA,IAAI,IAAA,CAAK,SAAA,KAAc,MAAA,EAAQ,OAAO,MAAA;AACtC,IAAA,IAAI,IAAA,CAAK,SAAA,KAAc,OAAA,EAAS,OAAO,OAAA;AACvC,IAAA,OAAO,IAAA,CAAK,MAAA,EAAQ,OAAA,GAAU,MAAA,GAAS,OAAA;AAAA,EACzC;AAAA;AAAA,EAGA,YAAY,MAAA,EAAgC;AAC1C,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,YAAA,CAAa,+BAA+B,CAAA;AAChE,IAAA,OAAO,MAAA,CAAO,IAAI,CAAA,GAAI,IAAA,GAAO,QAAA;AAAA,EAC/B;AAAA;AAAA,EAGA,cAAA,GAAqC;AACnC,IAAA,IAAI,IAAA,CAAK,WAAA,KAAgB,MAAA,IAAU,IAAA,CAAK,WAAA,KAAgB,OAAA;AACtD,MAAA,OAAO,QAAA,CAAS,eAAA;AAClB,IAAA,OAAO,QAAA,CAAS,aAAA,CAA2B,IAAA,CAAK,WAAW,CAAA;AAAA,EAC7D;AAAA;AAAA,EAGA,WAAA,GAAgC;AAC9B,IAAA,MAAM,KAAA,GAAQ,gBAAA,CAAiB,IAAA,CAAK,eAAe,CAAA;AACnD,IAAA,OAAO,MAAA,CAAO,KAAK,CAAA,GAAI,KAAA,GAAQ,IAAA;AAAA,EACjC;AAAA;AAAA,EAGA,aAAa,IAAA,EAAuB;AAClC,IAAA,iBAAA,CAAkB,IAAA,CAAK,iBAAiB,IAAI,CAAA;AAAA,EAC9C;AACF","file":"theme_controller.js","sourcesContent":["/**\n * Guarded `localStorage` access shared by the preference-persisting controllers\n * (sidebar, theme).\n *\n * `window.localStorage` can throw on access or read/write — storage disabled by\n * browser policy, sandboxed frames, private modes, or quota exhaustion. These\n * helpers centralize the try/catch so callers treat persistence as best-effort:\n * a failed read behaves like an unset key and a failed write is dropped,\n * leaving the in-DOM state as this session's source of truth.\n *\n * @remarks\n * Only the guarded access lives here — interpreting the stored string\n * (validation, defaults, key derivation) stays in each controller. The\n * `stimeo--persist` controller keeps its own storage layer on purpose: storage\n * *is* its domain (engine selection, per-field bookkeeping, TTL semantics).\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns The stored string, or `null` when the key is unset or storage is\n * unavailable (the read threw).\n */\nexport function readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`, ignoring failures (private\n * mode / quota / storage disabled) — persistence is best-effort by contract.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n */\nexport function writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Best-effort: the in-DOM state still applies for this session.\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync, and never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet (documented), not this controller.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: \"system\" },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: \"html\" },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n if (this.modeValue === \"system\") {\n this.#applyTheme();\n // Re-sync controls too: a 2-value toggle's `aria-pressed` reflects the\n // *resolved* theme, so it must follow the OS flip — not only `data-theme`.\n this.#syncControls();\n this.#dispatchChange();\n }\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n if (options.length === 0) return;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n const last = options.length - 1;\n let next = current;\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n next = current === last ? 0 : current + 1;\n break;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next = current === 0 ? last : current - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n this.#setMode(this.#optionMode(option));\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n if (this.hasOptionTarget) this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#applyTheme();\n this.#syncControls();\n }\n\n override disconnect(): void {\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Selects an explicit mode from the `mode` action param (radiogroup option). */\n set(event: Event): void {\n const mode = (event as { params?: Record<string, unknown> }).params?.mode;\n if (isMode(mode)) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#applyTheme();\n this.#syncControls();\n this.#dispatchChange();\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n let hasTabbable = false;\n for (const option of options) {\n const selected = this.#optionMode(option) === this.modeValue;\n option.setAttribute(\"aria-checked\", String(selected));\n option.tabIndex = selected ? 0 : -1;\n hasTabbable ||= selected;\n }\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n const first = options[0];\n if (!hasTabbable && first) first.tabIndex = 0;\n return;\n }\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n\n /** Emits `change` with the selected mode and the resolved theme. */\n #dispatchChange(): void {\n this.dispatch(\"change\", { detail: { mode: this.modeValue, resolved: this.#resolved() } });\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n if (this.modeValue === \"dark\") return \"dark\";\n if (this.modeValue === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** Reads an option's mode from its action param attribute. */\n #optionMode(option: HTMLElement): ThemeMode {\n const mode = option.getAttribute(\"data-stimeo--theme-mode-param\");\n return isMode(mode) ? mode : \"system\";\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n if (this.targetValue === \"html\" || this.targetValue === \":root\")\n return document.documentElement;\n return document.querySelector<HTMLElement>(this.targetValue);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const value = readLocalStorage(this.storageKeyValue);\n return isMode(value) ? value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/safe_storage.ts","../../src/controllers/theme_controller.ts"],"names":[],"mappings":"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","file":"theme_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Guarded `localStorage` access shared by the preference-persisting controllers\n * (sidebar, theme).\n *\n * `window.localStorage` can throw on access or read/write — storage disabled by\n * browser policy, sandboxed frames, private modes, or quota exhaustion. These\n * helpers centralize the try/catch so callers treat persistence as best-effort:\n * a failed read behaves like an unset key and a failed write is dropped,\n * leaving the in-DOM state as this session's source of truth.\n *\n * @remarks\n * Only the guarded access lives here — interpreting the stored string\n * (validation, defaults, key derivation) stays in each controller. The\n * `stimeo--persist` controller keeps its own storage layer on purpose: storage\n * *is* its domain (engine selection, per-field bookkeeping, TTL semantics).\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns The stored string, or `null` when the key is unset or storage is\n * unavailable (the read threw).\n */\nexport function readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`, ignoring failures (private\n * mode / quota / storage disabled) — persistence is best-effort by contract.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n */\nexport function writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Best-effort: the in-DOM state still applies for this session.\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\n\n/** The three selectable modes; `system` follows the OS `prefers-color-scheme`. */\ntype ThemeMode = \"light\" | \"dark\" | \"system\";\n/** The two effective (resolved) themes applied to the root. */\ntype ResolvedTheme = \"light\" | \"dark\";\n\nconst MODES: readonly ThemeMode[] = [\"light\", \"dark\", \"system\"];\nconst isMode = (value: unknown): value is ThemeMode =>\n typeof value === \"string\" && (MODES as readonly string[]).includes(value);\n\n/**\n * Headless **theme / color-scheme toggle** — persists a light/dark/system choice to\n * `localStorage`, follows the OS setting while in `system`, and reflects the\n * effective theme onto the root for the consumer's CSS. Ships no colors; only state\n * hooks.\n *\n * Two markup contracts (identifier: `stimeo--theme`):\n *\n * Canonical 3-value radiogroup (use this when `system` is offered):\n * <div data-controller=\"stimeo--theme\" data-stimeo--theme-mode-value=\"system\"\n * role=\"radiogroup\" aria-label=\"Theme\">\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"light\">Light</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"dark\">Dark</button>\n * <button data-stimeo--theme-target=\"option\" role=\"radio\"\n * data-action=\"click->stimeo--theme#set\"\n * data-stimeo--theme-mode-param=\"system\">System</button>\n * </div>\n *\n * Auxiliary 2-value toggle (light↔dark only — `system` is not representable):\n * <button data-controller=\"stimeo--theme\" data-action=\"click->stimeo--theme#toggle\"\n * aria-pressed=\"false\">Dark mode</button>\n *\n * @remarks\n * Behavior only — the actual palette is the consumer's CSS keyed off `data-theme`\n * on the root. It applies `data-theme` (the *resolved* light/dark) and a matching\n * `color-scheme` to the `target` element (`<html>` by default), keeps the radiogroup\n * `aria-checked` + roving tabindex (APG radio) or the single button's `aria-pressed`\n * in sync, and never moves focus. The `prefers-color-scheme` listener is attached on\n * `connect()` and removed on `disconnect()` (Turbo included). FOUC avoidance for the\n * very first paint is an inline `<head>` snippet, not this controller.\n */\nexport class ThemeController extends Controller<HTMLElement> {\n static override targets = [\"option\"];\n static override values = {\n mode: { type: String, default: \"system\" },\n storageKey: { type: String, default: \"stimeo-theme\" },\n target: { type: String, default: \"html\" },\n };\n static actions = [\"set\", \"toggle\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasOptionTarget: boolean;\n\n declare modeValue: string;\n declare storageKeyValue: string;\n declare targetValue: string;\n\n /** The OS dark-mode query, watched so `system` tracks live changes. */\n #media: MediaQueryList | null = null;\n\n /** Re-resolves while in `system` mode when the OS preference flips. */\n readonly #onMediaChange = (): void => {\n if (this.modeValue === \"system\") {\n this.#applyTheme();\n // Re-sync controls too: a 2-value toggle's `aria-pressed` reflects the\n // *resolved* theme, so it must follow the OS flip — not only `data-theme`.\n this.#syncControls();\n this.#dispatchChange();\n }\n };\n\n /** Arrow/Home/End navigation for the radiogroup (APG radio pattern). */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n // A descendant widget that already claimed the key (a grabbed drag handle, a\n // nested menu) must not ALSO act on it — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n // Resolved once per keydown: every `optionTargets` access re-queries the scope.\n const options = this.optionTargets;\n if (options.length === 0) return;\n const target = event.target as HTMLElement | null;\n const current = options.indexOf(target as HTMLElement);\n if (current === -1) return;\n\n const last = options.length - 1;\n let next = current;\n // Logical, not physical. The helper reverses only the horizontal pair, so\n // folding Down/Up into the same branch stays correct.\n const step = logicalArrowStep(event.key, this.element);\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n case \"ArrowUp\":\n case \"ArrowLeft\":\n next =\n step === 1 ? (current === last ? 0 : current + 1) : current === 0 ? last : current - 1;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = last;\n break;\n default:\n return;\n }\n event.preventDefault();\n const option = options[next];\n if (!option) return;\n option.focus();\n this.#setMode(this.#optionMode(option));\n };\n\n override connect(): void {\n const stored = this.#readStored();\n if (stored) this.modeValue = stored;\n\n this.#media = window.matchMedia?.(\"(prefers-color-scheme: dark)\") ?? null;\n this.#media?.addEventListener(\"change\", this.#onMediaChange);\n if (this.hasOptionTarget) this.element.addEventListener(\"keydown\", this.#onKeydown);\n\n this.#applyTheme();\n this.#syncControls();\n }\n\n override disconnect(): void {\n this.#media?.removeEventListener(\"change\", this.#onMediaChange);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n }\n\n /** Selects an explicit mode from the `mode` action param (radiogroup option). */\n set(event: Event): void {\n const mode = (event as { params?: Record<string, unknown> }).params?.mode;\n if (isMode(mode)) this.#setMode(mode);\n }\n\n /** Toggles light↔dark for the 2-value single-button contract. */\n toggle(): void {\n this.#setMode(this.#resolved() === \"dark\" ? \"light\" : \"dark\");\n }\n\n /** Central mode change: persist, apply to the root, resync controls, announce. */\n #setMode(mode: ThemeMode): void {\n this.modeValue = mode;\n this.#writeStored(mode);\n this.#applyTheme();\n this.#syncControls();\n this.#dispatchChange();\n }\n\n /** Writes `data-theme` + `color-scheme` (the resolved theme) onto the target. */\n #applyTheme(): void {\n const root = this.#targetElement();\n if (!root) return;\n const resolved = this.#resolved();\n root.setAttribute(\"data-theme\", resolved);\n root.style.setProperty(\"color-scheme\", resolved);\n }\n\n /** Keeps the radiogroup (aria-checked + roving tabindex) or toggle (aria-pressed) in sync. */\n #syncControls(): void {\n const options = this.optionTargets;\n if (options.length > 0) {\n let hasTabbable = false;\n for (const option of options) {\n const selected = this.#optionMode(option) === this.modeValue;\n option.setAttribute(\"aria-checked\", String(selected));\n option.tabIndex = selected ? 0 : -1;\n hasTabbable ||= selected;\n }\n // APG: a radiogroup with no selection keeps its first radio tabbable.\n const first = options[0];\n if (!hasTabbable && first) first.tabIndex = 0;\n return;\n }\n this.element.setAttribute(\"aria-pressed\", String(this.#resolved() === \"dark\"));\n }\n\n /** Emits `change` with the selected mode and the resolved theme. */\n #dispatchChange(): void {\n this.dispatch(\"change\", { detail: { mode: this.modeValue, resolved: this.#resolved() } });\n }\n\n /** The effective theme: the OS preference when `system`, else the mode itself. */\n #resolved(): ResolvedTheme {\n if (this.modeValue === \"dark\") return \"dark\";\n if (this.modeValue === \"light\") return \"light\";\n return this.#media?.matches ? \"dark\" : \"light\";\n }\n\n /** Reads an option's mode from its action param attribute. */\n #optionMode(option: HTMLElement): ThemeMode {\n const mode = option.getAttribute(\"data-stimeo--theme-mode-param\");\n return isMode(mode) ? mode : \"system\";\n }\n\n /** Resolves the state-hook target (`<html>` by default). */\n #targetElement(): HTMLElement | null {\n if (this.targetValue === \"html\" || this.targetValue === \":root\")\n return document.documentElement;\n return document.querySelector<HTMLElement>(this.targetValue);\n }\n\n /** Reads a persisted, validated mode from `localStorage` (null when absent/blocked). */\n #readStored(): ThemeMode | null {\n const value = readLocalStorage(this.storageKeyValue);\n return isMode(value) ? value : null;\n }\n\n /** Persists the mode, swallowing storage errors (private mode / quota). */\n #writeStored(mode: ThemeMode): void {\n writeLocalStorage(this.storageKeyValue, mode);\n }\n}\n"]}
@@ -1,5 +1,23 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ // src/controllers/time_picker_controller.ts
4
+
5
+ // src/utils/logical_scroll.ts
6
+ function isRtl(element) {
7
+ return window.getComputedStyle(element).direction === "rtl";
8
+ }
9
+
10
+ // src/utils/arrow_step.ts
11
+ function logicalArrowKey(key, element) {
12
+ if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
13
+ if (!isRtl(element)) return key;
14
+ return key === "ArrowRight" ? "ArrowLeft" : "ArrowRight";
15
+ }
16
+ function isReservedArrowChord(event, allow = []) {
17
+ if (!event.key.startsWith("Arrow")) return false;
18
+ return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
19
+ }
20
+
3
21
  // src/controllers/time_picker_controller.ts
4
22
  var AM = 0;
5
23
  var PM = 1;
@@ -34,12 +52,13 @@ var TimePickerController = class extends Controller {
34
52
  }
35
53
  /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */
36
54
  onKeydown(event) {
55
+ if (isReservedArrowChord(event)) return;
37
56
  const segment = event.target?.closest(
38
57
  "[data-stimeo--time-picker-target='segment']"
39
58
  );
40
59
  const kind = segment ? this.#kindOf(segment) : null;
41
60
  if (!segment || !kind) return;
42
- switch (event.key) {
61
+ switch (logicalArrowKey(event.key, this.element)) {
43
62
  case "ArrowUp":
44
63
  event.preventDefault();
45
64
  this.#step(kind, this.#delta(kind));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controllers/time_picker_controller.ts"],"names":["text"],"mappings":"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","file":"time_picker_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** A time segment kind, as declared by `data-segment` on each spinbutton. */\ntype SegmentKind = \"hour\" | \"minute\" | \"second\" | \"meridiem\";\n\n/** Meridiem encoding: 0 = AM, 1 = PM. */\nconst AM = 0;\nconst PM = 1;\n\n/**\n * Headless, accessible **time picker** behavior. Each segment (hour, minute,\n * optional second, optional AM/PM) is an APG **Spinbutton**; the controller\n * composes them into an `HH:MM[:SS]` value mirrored to a hidden field.\n *\n * Markup contract (identifier: `stimeo--time-picker`):\n * <div data-controller=\"stimeo--time-picker\"\n * data-stimeo--time-picker-hour-cycle-value=\"24\"\n * role=\"group\" aria-label=\"Time\">\n * <span role=\"spinbutton\" aria-label=\"Hours\" tabindex=\"0\"\n * aria-valuenow=\"9\" aria-valuemin=\"0\" aria-valuemax=\"23\" aria-valuetext=\"09\"\n * data-segment=\"hour\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">09</span>\n * <span aria-hidden=\"true\">:</span>\n * <span role=\"spinbutton\" aria-label=\"Minutes\" tabindex=\"0\"\n * data-segment=\"minute\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">30</span>\n * <input type=\"hidden\" data-stimeo--time-picker-target=\"field\" />\n * </div>\n *\n * @remarks\n * Behavior only — no styling, no locale formatting. Every segment is its own Tab\n * stop (multi-tabstop, *not* roving); `ArrowLeft`/`ArrowRight` are an auxiliary\n * move between segments. `ArrowUp`/`ArrowDown` step the focused segment, wrapping\n * (and, when `wrap` is set, carrying minutes→hours and seconds→minutes). Typing\n * digits enters a value directly and advances after two digits. AM/PM is modeled\n * as a `meridiem` spinbutton that toggles; the hidden field is always 24-hour.\n */\nexport class TimePickerController extends Controller<HTMLElement> {\n static override targets = [\"segment\", \"field\"];\n static override values = {\n hourCycle: { type: Number, default: 24 },\n step: { type: Number, default: 1 },\n seconds: { type: Boolean, default: false },\n wrap: { type: Boolean, default: true },\n };\n static actions = [\"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly segmentTargets: HTMLElement[];\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly hasFieldTarget: boolean;\n declare hourCycleValue: number;\n declare stepValue: number;\n declare secondsValue: boolean;\n declare wrapValue: boolean;\n\n /** Current numeric value per segment kind (hours are the *displayed* hours). */\n #state: Record<SegmentKind, number> = { hour: 0, minute: 0, second: 0, meridiem: AM };\n /** Direct-entry digit buffer and the segment it belongs to. */\n #typeBuffer = \"\";\n #typeSegment: SegmentKind | null = null;\n /** Last composed field value, to suppress duplicate `change` dispatches. */\n #lastValue = \"\";\n\n /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */\n override connect(): void {\n for (const segment of this.segmentTargets) {\n const kind = this.#kindOf(segment);\n if (!kind) continue;\n const now = Number(segment.getAttribute(\"aria-valuenow\"));\n const { min, max } = this.#bounds(kind);\n // Clamp the seeded value so malformed markup (out-of-range aria-valuenow)\n // never propagates into the rendered state or the composed field.\n this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;\n }\n for (const segment of this.segmentTargets) this.#renderSegment(segment);\n this.#syncField(false);\n }\n\n /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */\n onKeydown(event: KeyboardEvent): void {\n const segment = (event.target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--time-picker-target='segment']\",\n );\n const kind = segment ? this.#kindOf(segment) : null;\n if (!segment || !kind) return;\n\n switch (event.key) {\n case \"ArrowUp\":\n event.preventDefault();\n this.#step(kind, this.#delta(kind));\n break;\n case \"ArrowDown\":\n event.preventDefault();\n this.#step(kind, -this.#delta(kind));\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n this.#focusSibling(segment, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n this.#focusSibling(segment, 1);\n break;\n case \"Home\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).min);\n break;\n case \"End\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).max);\n break;\n default:\n if (/^[0-9]$/.test(event.key)) {\n event.preventDefault();\n this.#typeDigit(segment, kind, event.key);\n }\n return;\n }\n // Any non-digit action ends the current direct-entry sequence.\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n }\n\n /** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */\n #delta(kind: SegmentKind): number {\n return kind === \"minute\" ? this.stepValue : 1;\n }\n\n /** Steps a segment, wrapping at its bounds and carrying over when enabled. */\n #step(kind: SegmentKind, delta: number): void {\n if (kind === \"meridiem\") {\n this.#set(\"meridiem\", this.#state.meridiem === AM ? PM : AM);\n return;\n }\n const { min, max } = this.#bounds(kind);\n const span = max - min + 1;\n const raw = this.#state[kind] + delta;\n\n if (raw > max || raw < min) {\n if (!this.wrapValue) {\n this.#set(kind, Math.min(max, Math.max(min, raw)));\n return;\n }\n // Wrap within the segment, carrying the overflow into the larger unit.\n const wrapped = ((((raw - min) % span) + span) % span) + min;\n const carry = Math.floor((raw - min) / span);\n this.#set(kind, wrapped);\n this.#carry(kind, carry);\n return;\n }\n this.#set(kind, raw);\n }\n\n /** Propagates a wrap carry from `kind` into the next larger segment. */\n #carry(kind: SegmentKind, amount: number): void {\n if (amount === 0) return;\n if (kind === \"second\") this.#step(\"minute\", amount);\n else if (kind === \"minute\") this.#step(\"hour\", amount);\n // Hours wrap on their own (no day rollover); meridiem has no carry.\n }\n\n /**\n * Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,\n * and resyncs the composed field. Clamping here guards the direct-entry path:\n * typing `0` into a 12-hour hour (min 1) must not commit an out-of-range\n * `aria-valuenow=\"0\"`. The stepping path already passes in-bounds values, so\n * the clamp is a no-op there.\n */\n #set(kind: SegmentKind, value: number): void {\n const { min, max } = this.#bounds(kind);\n this.#state[kind] = Math.min(max, Math.max(min, value));\n const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);\n if (segment) this.#renderSegment(segment);\n this.#syncField(true);\n }\n\n /** Accumulates a typed digit, committing and advancing after two digits. */\n #typeDigit(segment: HTMLElement, kind: SegmentKind, digit: string): void {\n if (kind === \"meridiem\") return;\n if (this.#typeSegment !== kind) this.#typeBuffer = \"\";\n this.#typeSegment = kind;\n\n const { max } = this.#bounds(kind);\n const candidate = Number(`${this.#typeBuffer}${digit}`);\n if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;\n else this.#typeBuffer = digit; // restart from this digit when it overflows\n\n this.#set(kind, Number(this.#typeBuffer));\n\n // Two digits (or a value that can't grow further) completes the segment.\n if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n this.#focusSibling(segment, 1);\n }\n }\n\n /** Moves focus to the previous/next segment, if one exists. */\n #focusSibling(segment: HTMLElement, direction: 1 | -1): void {\n const index = this.segmentTargets.indexOf(segment);\n const next = this.segmentTargets[index + direction];\n next?.focus();\n }\n\n /** Reflects a segment's current value onto its ARIA/text representation. */\n #renderSegment(segment: HTMLElement): void {\n const kind = this.#kindOf(segment);\n if (!kind) return;\n const value = this.#state[kind];\n\n if (kind === \"meridiem\") {\n const text = value === PM ? \"PM\" : \"AM\";\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(AM));\n segment.setAttribute(\"aria-valuemax\", String(PM));\n segment.textContent = text;\n return;\n }\n\n const { min, max } = this.#bounds(kind);\n const text = String(value).padStart(2, \"0\");\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(min));\n segment.setAttribute(\"aria-valuemax\", String(max));\n segment.textContent = text;\n }\n\n /** Composes `HH:MM[:SS]` (24-hour) into the hidden field; dispatches `change`. */\n #syncField(notify: boolean): void {\n const h24 = this.#hours24();\n const parts = [pad(h24), pad(this.#state.minute)];\n if (this.secondsValue) parts.push(pad(this.#state.second));\n const value = parts.join(\":\");\n\n if (this.hasFieldTarget && this.fieldTarget.value !== value) {\n this.fieldTarget.value = value;\n }\n // Dispatch only when the composed value actually changed, so a no-op step\n // (e.g. Home at the minimum) does not emit a redundant event.\n if (notify && value !== this.#lastValue) this.dispatch(\"change\", { detail: { value } });\n this.#lastValue = value;\n }\n\n /** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */\n #hours24(): number {\n if (this.hourCycleValue !== 12) return this.#state.hour;\n const base = this.#state.hour % 12; // 12 → 0\n return base + (this.#state.meridiem === PM ? 12 : 0);\n }\n\n /** The inclusive `[min, max]` bounds for a segment kind. */\n #bounds(kind: SegmentKind): { min: number; max: number } {\n switch (kind) {\n case \"hour\":\n return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };\n case \"minute\":\n case \"second\":\n return { min: 0, max: 59 };\n case \"meridiem\":\n return { min: AM, max: PM };\n }\n }\n\n /** Reads a segment's declared kind, or null when absent/invalid. */\n #kindOf(segment: HTMLElement): SegmentKind | null {\n const kind = segment.getAttribute(\"data-segment\");\n if (kind === \"hour\" || kind === \"minute\" || kind === \"second\" || kind === \"meridiem\") {\n return kind;\n }\n return null;\n }\n}\n\n/** Zero-pads a number to two digits. */\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/controllers/time_picker_controller.ts"],"names":["text"],"mappings":"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","file":"time_picker_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\n\n/** A time segment kind, as declared by `data-segment` on each spinbutton. */\ntype SegmentKind = \"hour\" | \"minute\" | \"second\" | \"meridiem\";\n\n/** Meridiem encoding: 0 = AM, 1 = PM. */\nconst AM = 0;\nconst PM = 1;\n\n/**\n * Headless, accessible **time picker** behavior. Each segment (hour, minute,\n * optional second, optional AM/PM) is an APG **Spinbutton**; the controller\n * composes them into an `HH:MM[:SS]` value mirrored to a hidden field.\n *\n * Markup contract (identifier: `stimeo--time-picker`):\n * <div data-controller=\"stimeo--time-picker\"\n * data-stimeo--time-picker-hour-cycle-value=\"24\"\n * role=\"group\" aria-label=\"Time\">\n * <span role=\"spinbutton\" aria-label=\"Hours\" tabindex=\"0\"\n * aria-valuenow=\"9\" aria-valuemin=\"0\" aria-valuemax=\"23\" aria-valuetext=\"09\"\n * data-segment=\"hour\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">09</span>\n * <span aria-hidden=\"true\">:</span>\n * <span role=\"spinbutton\" aria-label=\"Minutes\" tabindex=\"0\"\n * data-segment=\"minute\" data-stimeo--time-picker-target=\"segment\"\n * data-action=\"keydown->stimeo--time-picker#onKeydown\">30</span>\n * <input type=\"hidden\" data-stimeo--time-picker-target=\"field\" />\n * </div>\n *\n * @remarks\n * Behavior only — no styling, no locale formatting. Every segment is its own Tab\n * stop (multi-tabstop, *not* roving); `ArrowLeft`/`ArrowRight` are an auxiliary\n * move between segments. `ArrowUp`/`ArrowDown` step the focused segment, wrapping\n * (and, when `wrap` is set, carrying minutes→hours and seconds→minutes). Typing\n * digits enters a value directly and advances after two digits. AM/PM is modeled\n * as a `meridiem` spinbutton that toggles; the hidden field is always 24-hour.\n */\nexport class TimePickerController extends Controller<HTMLElement> {\n static override targets = [\"segment\", \"field\"];\n static override values = {\n hourCycle: { type: Number, default: 24 },\n step: { type: Number, default: 1 },\n seconds: { type: Boolean, default: false },\n wrap: { type: Boolean, default: true },\n };\n static actions = [\"onKeydown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly segmentTargets: HTMLElement[];\n declare readonly fieldTarget: HTMLInputElement;\n declare readonly hasFieldTarget: boolean;\n declare hourCycleValue: number;\n declare stepValue: number;\n declare secondsValue: boolean;\n declare wrapValue: boolean;\n\n /** Current numeric value per segment kind (hours are the *displayed* hours). */\n #state: Record<SegmentKind, number> = { hour: 0, minute: 0, second: 0, meridiem: AM };\n /** Direct-entry digit buffer and the segment it belongs to. */\n #typeBuffer = \"\";\n #typeSegment: SegmentKind | null = null;\n /** Last composed field value, to suppress duplicate `change` dispatches. */\n #lastValue = \"\";\n\n /** Seeds each segment from its initial `aria-valuenow` and syncs the field. */\n override connect(): void {\n for (const segment of this.segmentTargets) {\n const kind = this.#kindOf(segment);\n if (!kind) continue;\n const now = Number(segment.getAttribute(\"aria-valuenow\"));\n const { min, max } = this.#bounds(kind);\n // Clamp the seeded value so malformed markup (out-of-range aria-valuenow)\n // never propagates into the rendered state or the composed field.\n this.#state[kind] = Number.isFinite(now) ? Math.min(max, Math.max(min, now)) : min;\n }\n for (const segment of this.segmentTargets) this.#renderSegment(segment);\n this.#syncField(false);\n }\n\n /** Handles stepping, inter-segment focus moves, jumps, and direct entry. */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const segment = (event.target as HTMLElement | null)?.closest<HTMLElement>(\n \"[data-stimeo--time-picker-target='segment']\",\n );\n const kind = segment ? this.#kindOf(segment) : null;\n if (!segment || !kind) return;\n // Logical, not physical, for the horizontal pair: the segments form an\n // ordered row laid out inline, so a right-to-left writing direction mirrors\n // them. `ArrowUp` / `ArrowDown` change the *value* and pass through\n // untouched — `logicalArrowKey` only ever rewrites the horizontal pair.\n\n switch (logicalArrowKey(event.key, this.element)) {\n case \"ArrowUp\":\n event.preventDefault();\n this.#step(kind, this.#delta(kind));\n break;\n case \"ArrowDown\":\n event.preventDefault();\n this.#step(kind, -this.#delta(kind));\n break;\n case \"ArrowLeft\":\n event.preventDefault();\n this.#focusSibling(segment, -1);\n break;\n case \"ArrowRight\":\n event.preventDefault();\n this.#focusSibling(segment, 1);\n break;\n case \"Home\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).min);\n break;\n case \"End\":\n event.preventDefault();\n this.#set(kind, this.#bounds(kind).max);\n break;\n default:\n if (/^[0-9]$/.test(event.key)) {\n event.preventDefault();\n this.#typeDigit(segment, kind, event.key);\n }\n return;\n }\n // Any non-digit action ends the current direct-entry sequence.\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n }\n\n /** The per-step delta: minutes step by `step`, others by 1, meridiem toggles. */\n #delta(kind: SegmentKind): number {\n return kind === \"minute\" ? this.stepValue : 1;\n }\n\n /** Steps a segment, wrapping at its bounds and carrying over when enabled. */\n #step(kind: SegmentKind, delta: number): void {\n if (kind === \"meridiem\") {\n this.#set(\"meridiem\", this.#state.meridiem === AM ? PM : AM);\n return;\n }\n const { min, max } = this.#bounds(kind);\n const span = max - min + 1;\n const raw = this.#state[kind] + delta;\n\n if (raw > max || raw < min) {\n if (!this.wrapValue) {\n this.#set(kind, Math.min(max, Math.max(min, raw)));\n return;\n }\n // Wrap within the segment, carrying the overflow into the larger unit.\n const wrapped = ((((raw - min) % span) + span) % span) + min;\n const carry = Math.floor((raw - min) / span);\n this.#set(kind, wrapped);\n this.#carry(kind, carry);\n return;\n }\n this.#set(kind, raw);\n }\n\n /** Propagates a wrap carry from `kind` into the next larger segment. */\n #carry(kind: SegmentKind, amount: number): void {\n if (amount === 0) return;\n if (kind === \"second\") this.#step(\"minute\", amount);\n else if (kind === \"minute\") this.#step(\"hour\", amount);\n // Hours wrap on their own (no day rollover); meridiem has no carry.\n }\n\n /**\n * Sets a segment's value (clamped to its `[min, max]` bounds), re-renders it,\n * and resyncs the composed field. Clamping here guards the direct-entry path:\n * typing `0` into a 12-hour hour (min 1) must not commit an out-of-range\n * `aria-valuenow=\"0\"`. The stepping path already passes in-bounds values, so\n * the clamp is a no-op there.\n */\n #set(kind: SegmentKind, value: number): void {\n const { min, max } = this.#bounds(kind);\n this.#state[kind] = Math.min(max, Math.max(min, value));\n const segment = this.segmentTargets.find((s) => this.#kindOf(s) === kind);\n if (segment) this.#renderSegment(segment);\n this.#syncField(true);\n }\n\n /** Accumulates a typed digit, committing and advancing after two digits. */\n #typeDigit(segment: HTMLElement, kind: SegmentKind, digit: string): void {\n if (kind === \"meridiem\") return;\n if (this.#typeSegment !== kind) this.#typeBuffer = \"\";\n this.#typeSegment = kind;\n\n const { max } = this.#bounds(kind);\n const candidate = Number(`${this.#typeBuffer}${digit}`);\n if (candidate <= max) this.#typeBuffer = `${this.#typeBuffer}${digit}`;\n else this.#typeBuffer = digit; // restart from this digit when it overflows\n\n this.#set(kind, Number(this.#typeBuffer));\n\n // Two digits (or a value that can't grow further) completes the segment.\n if (this.#typeBuffer.length >= 2 || Number(this.#typeBuffer) * 10 > max) {\n this.#typeBuffer = \"\";\n this.#typeSegment = null;\n this.#focusSibling(segment, 1);\n }\n }\n\n /** Moves focus to the previous/next segment, if one exists. */\n #focusSibling(segment: HTMLElement, direction: 1 | -1): void {\n const index = this.segmentTargets.indexOf(segment);\n const next = this.segmentTargets[index + direction];\n next?.focus();\n }\n\n /** Reflects a segment's current value onto its ARIA/text representation. */\n #renderSegment(segment: HTMLElement): void {\n const kind = this.#kindOf(segment);\n if (!kind) return;\n const value = this.#state[kind];\n\n if (kind === \"meridiem\") {\n const text = value === PM ? \"PM\" : \"AM\";\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(AM));\n segment.setAttribute(\"aria-valuemax\", String(PM));\n segment.textContent = text;\n return;\n }\n\n const { min, max } = this.#bounds(kind);\n const text = String(value).padStart(2, \"0\");\n segment.setAttribute(\"aria-valuenow\", String(value));\n segment.setAttribute(\"aria-valuetext\", text);\n segment.setAttribute(\"aria-valuemin\", String(min));\n segment.setAttribute(\"aria-valuemax\", String(max));\n segment.textContent = text;\n }\n\n /** Composes `HH:MM[:SS]` (24-hour) into the hidden field; dispatches `change`. */\n #syncField(notify: boolean): void {\n const h24 = this.#hours24();\n const parts = [pad(h24), pad(this.#state.minute)];\n if (this.secondsValue) parts.push(pad(this.#state.second));\n const value = parts.join(\":\");\n\n if (this.hasFieldTarget && this.fieldTarget.value !== value) {\n this.fieldTarget.value = value;\n }\n // Dispatch only when the composed value actually changed, so a no-op step\n // (e.g. Home at the minimum) does not emit a redundant event.\n if (notify && value !== this.#lastValue) this.dispatch(\"change\", { detail: { value } });\n this.#lastValue = value;\n }\n\n /** Converts the displayed hour (+ meridiem in 12-hour mode) to 24-hour. */\n #hours24(): number {\n if (this.hourCycleValue !== 12) return this.#state.hour;\n const base = this.#state.hour % 12; // 12 → 0\n return base + (this.#state.meridiem === PM ? 12 : 0);\n }\n\n /** The inclusive `[min, max]` bounds for a segment kind. */\n #bounds(kind: SegmentKind): { min: number; max: number } {\n switch (kind) {\n case \"hour\":\n return this.hourCycleValue === 12 ? { min: 1, max: 12 } : { min: 0, max: 23 };\n case \"minute\":\n case \"second\":\n return { min: 0, max: 59 };\n case \"meridiem\":\n return { min: AM, max: PM };\n }\n }\n\n /** Reads a segment's declared kind, or null when absent/invalid. */\n #kindOf(segment: HTMLElement): SegmentKind | null {\n const kind = segment.getAttribute(\"data-segment\");\n if (kind === \"hour\" || kind === \"minute\" || kind === \"second\" || kind === \"meridiem\") {\n return kind;\n }\n return null;\n }\n}\n\n/** Zero-pads a number to two digits. */\nfunction pad(value: number): string {\n return String(value).padStart(2, \"0\");\n}\n"]}
@@ -101,11 +101,10 @@ declare class ToastController extends Controller<HTMLElement> {
101
101
  /**
102
102
  * Removes the oldest toasts when the list exceeds `maxValue`.
103
103
  *
104
- * Public (not `#private`) as a deterministic test seam: enforcement normally
105
- * runs from `itemTargetConnected`, a Stimulus callback that fires via a
106
- * MutationObserver which happy-dom does not reliably deliver — so the unit
107
- * tests invoke it directly. It is therefore listed in the contract guard's
108
- * NON_ACTION_ALLOWLIST (it is not a user-wired action).
104
+ * Public (not `#private`) as a deterministic seam: enforcement normally runs
105
+ * from `itemTargetConnected`, a Stimulus callback delivered through a
106
+ * MutationObserver, which a DOM-only environment does not reliably fire — so
107
+ * it can also be invoked directly. It is not a user-wired action.
109
108
  */
110
109
  enforceMaxLimit(): void;
111
110
  }
@@ -348,11 +348,10 @@ var ToastController = class extends Controller {
348
348
  /**
349
349
  * Removes the oldest toasts when the list exceeds `maxValue`.
350
350
  *
351
- * Public (not `#private`) as a deterministic test seam: enforcement normally
352
- * runs from `itemTargetConnected`, a Stimulus callback that fires via a
353
- * MutationObserver which happy-dom does not reliably deliver — so the unit
354
- * tests invoke it directly. It is therefore listed in the contract guard's
355
- * NON_ACTION_ALLOWLIST (it is not a user-wired action).
351
+ * Public (not `#private`) as a deterministic seam: enforcement normally runs
352
+ * from `itemTargetConnected`, a Stimulus callback delivered through a
353
+ * MutationObserver, which a DOM-only environment does not reliably fire — so
354
+ * it can also be invoked directly. It is not a user-wired action.
356
355
  */
357
356
  enforceMaxLimit() {
358
357
  const currentItems = this.itemTargets;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/toast_controller.ts"],"names":[],"mappings":"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","file":"toast_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 { SafeTimeout } from \"./safe_timeout\";\n\n/** Computed transition fields needed to determine when an element has settled. */\nexport type TransitionStyle = Pick<\n CSSStyleDeclaration,\n \"transitionProperty\" | \"transitionDuration\" | \"transitionDelay\"\n>;\n\n/** Parsed transition property paired with its effective duration and delay. */\ninterface TransitionTiming {\n property: string;\n totalMs: number;\n}\n\n/** Parses a CSS `<time>` into milliseconds, returning `0` for malformed values. */\nfunction timeMs(value: string): number {\n const trimmed = value.trim();\n const amount = Number.parseFloat(trimmed);\n if (!Number.isFinite(amount)) return 0;\n if (trimmed.endsWith(\"ms\")) return amount;\n if (trimmed.endsWith(\"s\")) return amount * 1000;\n return 0;\n}\n\n/** Splits a computed comma-separated CSS list into normalized, non-empty items. */\nfunction cssList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Pairs transition properties, durations, and delays using CSS list repetition.\n *\n * The `transition-property` list determines the number of transitions. Shorter\n * duration/delay lists repeat, while extra values are ignored. A negative delay\n * shortens the remaining wall-clock time, and a non-positive total is settled.\n */\nfunction transitionTimings(style: TransitionStyle): TransitionTiming[] {\n const properties = cssList(style.transitionProperty);\n const durations = cssList(style.transitionDuration).map(timeMs);\n const delays = cssList(style.transitionDelay).map(timeMs);\n const effectiveProperties =\n properties.length > 0\n ? properties\n : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => \"all\");\n const effectiveDurations = durations.length > 0 ? durations : [0];\n const effectiveDelays = delays.length > 0 ? delays : [0];\n\n return effectiveProperties\n .filter((property) => property !== \"none\")\n .map((property, index) => ({\n property,\n totalMs: Math.max(\n 0,\n (effectiveDurations[index % effectiveDurations.length] ?? 0) +\n (effectiveDelays[index % effectiveDelays.length] ?? 0),\n ),\n }));\n}\n\n/** Maximum total over already-parsed timings (0 when the list is empty). */\nfunction maxTotalMs(timings: TransitionTiming[]): number {\n return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);\n}\n\n/** Maximum effective transition duration plus delay, in milliseconds. */\nexport function maxTransitionTotalMs(style: TransitionStyle): number {\n return maxTotalMs(transitionTimings(style));\n}\n\n/** Optional tuning for a single {@link TransitionCompletion.wait} call. */\nexport interface TransitionWaitOptions {\n /**\n * Positive wall-clock override (ms) for the bounded fallback, replacing the\n * auto-computed `max + 50ms` timer verbatim. It also keeps the wait armed for\n * a computed 0ms transition (no synchronous completion): the consumer asserts\n * a transition budget that computed styles cannot see. Non-positive and\n * non-finite (`NaN` / `Infinity`) values are ignored.\n */\n timeoutMs?: number;\n}\n\n/**\n * Owns one cancellable wait for an element's CSS transitions to settle.\n *\n * When available, active CSS transitions provide their expanded property names.\n * Tracking those names lets shorthand declarations settle from their longhand\n * terminal events. A terminal event from an interrupted phase is ignored while a\n * replacement transition for the same property is still active. Because that set\n * is sampled when the wait is armed, a property whose transition only starts a\n * frame later would otherwise be settled over; the still-running set is therefore\n * re-read before completing and adopted when it is not yet empty. Without Web\n * Animations data, explicit transition properties retain the declared-property\n * behavior and the ambiguous `transition-property: all` form waits until the\n * computed maximum wall-clock time. A `max + 50ms` timeout is always armed\n * because browsers emit no terminal event when a property never transitions, an\n * ancestor disappears, or rendering is otherwise interrupted; a consumer with\n * better knowledge can replace that fallback via\n * {@link TransitionWaitOptions.timeoutMs}.\n */\nexport class TransitionCompletion {\n readonly #timers = new SafeTimeout();\n #element: HTMLElement | null = null;\n #complete: (() => void) | null = null;\n #pendingProperties: Set<string> | null = null;\n #deadline = 0;\n\n /**\n * Replaces any prior wait and invokes `complete` synchronously for a 0ms\n * transition (including when `getComputedStyle` is unavailable).\n *\n * With a positive `options.timeoutMs` the synchronous fast-path is skipped and\n * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).\n */\n wait(element: HTMLElement, complete: () => void, options: TransitionWaitOptions = {}): void {\n this.cancel();\n\n const requested = options.timeoutMs ?? 0;\n const override = Number.isFinite(requested) && requested > 0 ? requested : 0;\n // A non-browser / partial-DOM environment cannot report a computable\n // transition, which is indistinguishable from a 0ms one.\n const timings =\n typeof window.getComputedStyle === \"function\"\n ? transitionTimings(window.getComputedStyle(element))\n : [];\n const maximum = maxTotalMs(timings);\n if (maximum <= 0 && override <= 0) {\n complete();\n return;\n }\n\n this.#element = element;\n this.#complete = complete;\n this.#deadline = Date.now() + maximum;\n const activeProperties = this.#activeTransitionProperties(element);\n this.#pendingProperties =\n activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);\n element.addEventListener(\"transitionend\", this.#onTerminal);\n element.addEventListener(\"transitioncancel\", this.#onTerminal);\n this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);\n }\n\n /** Cancels the pending wait without invoking its completion callback. */\n cancel(): void {\n this.#complete = null;\n this.#teardown();\n }\n\n /**\n * Handles terminal events from the observed element only.\n *\n * For explicit property lists, every declared positive-time property must\n * settle. For `all`, no reliable property set exists, so an event can finish\n * only after the computed maximum time; the safety timeout owns the usual path.\n */\n readonly #onTerminal = (event: Event): void => {\n if (event.target !== this.#element) return;\n const transitionEvent = event as TransitionEvent;\n // A pseudo-element's transition is reported with its originating element\n // as `target`; it must not settle the element's own transition.\n if (transitionEvent.pseudoElement) return;\n\n if (this.#pendingProperties) {\n const propertyName = transitionEvent.propertyName;\n const active = this.#activeTransitionProperties(this.#element);\n // An interrupted transition queues its cancel event before the replacement\n // phase starts, but dispatch can happen after the new wait is armed. The\n // replacement is then the active transition for this property, so the stale\n // terminal event must not settle the new phase.\n if (active.has(propertyName)) return;\n if (!this.#pendingProperties.delete(propertyName)) return;\n if (this.#pendingProperties.size > 0) return;\n // The tracked set is an arm-time snapshot, so a transition the phase starts\n // one frame later is absent from it. Re-reading the still-running set before\n // settling adopts those stragglers instead of completing over them; the\n // fallback timer stays armed, so this cannot extend the wait unboundedly.\n if (active.size > 0) {\n this.#pendingProperties = active;\n return;\n }\n this.#finish();\n return;\n }\n\n if (Date.now() >= this.#deadline) this.#finish();\n };\n\n /**\n * Returns active CSS transition properties expanded to the names reported by\n * terminal events. CSS animations and pseudo-element effects are excluded.\n */\n #activeTransitionProperties(element: HTMLElement | null): Set<string> {\n if (!element || typeof element.getAnimations !== \"function\") return new Set();\n\n try {\n const properties = element.getAnimations().flatMap((animation) => {\n if (animation.playState === \"idle\" || animation.playState === \"finished\") return [];\n const effect = animation.effect as KeyframeEffect | null;\n // A pseudo-element effect names the originating element as its target, so\n // `target` alone cannot separate it. Its terminal events are ignored, so\n // tracking its properties would only strand the wait on the fallback.\n if (effect?.pseudoElement) return [];\n const target = effect?.target;\n if (target && target !== element) return [];\n const property = (animation as Partial<CSSTransition>).transitionProperty;\n return typeof property === \"string\" && property.length > 0 ? [property] : [];\n });\n return new Set(properties);\n } catch {\n // Partial DOM implementations can expose getAnimations without supporting\n // it. The declared-property tracker and bounded timer remain safe fallbacks.\n return new Set();\n }\n }\n\n /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */\n #explicitPendingProperties(timings: TransitionTiming[]): Set<string> | null {\n if (timings.some(({ property }) => property === \"all\")) return null;\n const pending = new Set(\n timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property),\n );\n // An empty set is only reachable through a timeout override (a 0ms transition\n // otherwise completes synchronously). With nothing to track, fall back to the\n // deadline rule so any terminal event can settle the wait, exactly like `all`.\n return pending.size > 0 ? pending : null;\n }\n\n /** Completes exactly once, releasing listeners and the fallback before the callback. */\n #finish(): void {\n const complete = this.#complete;\n if (!complete) return;\n this.#complete = null;\n this.#teardown();\n complete();\n }\n\n /** Releases the exact element listeners and timer owned by the current wait. */\n #teardown(): void {\n this.#timers.clearAll();\n this.#element?.removeEventListener(\"transitionend\", this.#onTerminal);\n this.#element?.removeEventListener(\"transitioncancel\", this.#onTerminal);\n this.#element = null;\n this.#pendingProperties = null;\n this.#deadline = 0;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { maxTransitionTotalMs } from \"../utils/transition_completion\";\n\nconst DELEGATED_EVENTS = [\"click\", \"focusin\", \"focusout\", \"keydown\", \"mouseover\", \"mouseout\"];\n\n/**\n * Headless, highly accessible toast notification behavior.\n *\n * Markup contract (identifier: `stimeo--toast`):\n * <div data-controller=\"stimeo--toast\"\n * data-stimeo--toast-duration-value=\"5000\"\n * data-stimeo--toast-max-value=\"3\">\n * <!-- Attribute-only trigger: no hand-written JS required. -->\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n * <!-- The live region is a descendant; the controller element only needs\n * to enclose both the trigger(s) and the list/template targets. -->\n * <div role=\"region\" aria-label=\"Notifications\">\n * <ol data-stimeo--toast-target=\"list\"></ol>\n * <template data-stimeo--toast-target=\"template\">\n * <li data-stimeo--toast-target=\"item\" tabindex=\"0\">\n * <span role=\"status\" data-toast-slot=\"body\"></span>\n * <button type=\"button\" data-toast-dismiss>Dismiss</button>\n * </li>\n * </template>\n * </div>\n * </div>\n *\n * Implements WAI-ARIA live region status/alert announcements, limits simultaneous\n * elements, and pauses dismiss timeouts on hover or focus to comply with WCAG 2.2.1.\n *\n * @remarks\n * Behavior only. The controller handles state updates via `data-state` and lifecycle events\n * while leaving visual styling completely to the client's CSS transitions.\n */\nexport class ToastController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"item\"];\n static override values = {\n duration: { type: Number, default: 0 },\n max: { type: Number, default: 3 },\n };\n static actions = [\"dismiss\", \"onKeydown\", \"pause\", \"resume\", \"show\"] as const;\n static events = [\"dismiss\", \"show\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n\n declare durationValue: number;\n declare maxValue: number;\n\n /**\n * Registry for every auto-dismiss and transition-finalize timer the controller\n * schedules. {@link SafeTimeout} owns *registration and teardown only*; the\n * pause/resume remaining-time accounting stays in `#activeTimeouts` so the\n * per-widget WCAG 2.2.1 semantics are not flattened into the helper.\n */\n #timers = new SafeTimeout();\n\n /**\n * Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).\n * Tracked so {@link disconnect} can cancel any that have not fired, preventing a\n * detached element from being mutated after it leaves the DOM (Turbo).\n */\n #rafHandles = new Map<HTMLElement, number>();\n\n /** Track active timeouts mapped by each toast element for safe cancellation. */\n #activeTimeouts = new Map<HTMLElement, { id: number; startedAt: number; remaining: number }>();\n\n /** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */\n #pauseReasons = new Map<HTMLElement, Set<string>>();\n\n /** The stable list that owns delegated listeners for dynamically added items. */\n #delegatedList: HTMLElement | null = null;\n\n override connect(): void {\n this.#connectDelegatedEvents();\n this.enforceMaxLimit();\n for (const item of this.itemTargets) {\n if (!this.#activeTimeouts.has(item) && item.dataset.state !== \"leaving\") {\n this.#startTimer(item);\n }\n }\n }\n\n override disconnect(): void {\n // SafeTimeout owns every auto-dismiss + finalize timer; one call tears them\n // all down so none fires against the detached controller.\n this.#disconnectDelegatedEvents();\n this.#timers.clearAll();\n for (const handle of this.#rafHandles.values()) {\n window.cancelAnimationFrame(handle);\n }\n this.#rafHandles.clear();\n this.#activeTimeouts.clear();\n this.#pauseReasons.clear();\n }\n\n /** Rebinds delegated interaction when Turbo replaces the list target in place. */\n listTargetConnected(element: HTMLElement): void {\n if (this.#delegatedList !== element) this.#connectDelegatedEvents(element);\n }\n\n /** Releases delegation only when the removed target is its current owner. */\n listTargetDisconnected(element: HTMLElement): void {\n if (this.#delegatedList === element) this.#disconnectDelegatedEvents();\n }\n\n durationValueChanged(): void {\n for (const item of this.itemTargets) {\n if (item.dataset.state === \"leaving\") continue;\n\n const pauseReasons = this.#pauseReasons.get(item);\n this.#clearTimer(item);\n if (this.durationValue <= 0) {\n item.removeAttribute(\"data-paused\");\n } else if (pauseReasons && pauseReasons.size > 0) {\n this.#pauseReasons.set(item, pauseReasons);\n this.#activeTimeouts.set(item, {\n id: 0,\n startedAt: 0,\n remaining: this.durationValue,\n });\n item.setAttribute(\"data-paused\", \"true\");\n } else {\n this.#startTimer(item);\n }\n }\n }\n\n maxValueChanged(): void {\n this.enforceMaxLimit();\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when a new item target\n * enters the DOM. Perfectly handles dynamic client-side injections and server-side\n * Turbo Stream appends alike.\n */\n itemTargetConnected(element: HTMLElement): void {\n this.enforceMaxLimit();\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#startTimer(element);\n element.setAttribute(\"data-state\", \"entering\");\n this.#cancelAnimation(element);\n const handle = window.requestAnimationFrame(() => {\n this.#rafHandles.delete(element);\n if (element.parentNode !== this.listTarget || element.dataset.state === \"leaving\") return;\n element.setAttribute(\"data-state\", \"visible\");\n });\n this.#rafHandles.set(element, handle);\n }\n\n /** Clears any active timer when a toast is removed from the DOM. */\n itemTargetDisconnected(element: HTMLElement): void {\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n }\n\n /**\n * Shows a new toast. Accepts its content from either a Stimulus action param\n * (attribute-only trigger) or a programmatic `show` CustomEvent `detail`\n * (remote / Turbo trigger); the action param wins when both are present.\n *\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n *\n * element.dispatchEvent(new CustomEvent(\"show\", { detail: { body, type } }))\n *\n * Clones the template slot, interpolates the body text, and appends to the list.\n */\n show(event: Event): void {\n if (!this.hasTemplateTarget || !this.hasListTarget) return;\n\n const body = this.#readField(event, \"body\");\n if (!body) return;\n\n const clone = this.templateTarget.content.cloneNode(true) as DocumentFragment;\n const item = clone.querySelector(\"[data-stimeo--toast-target='item']\") as HTMLElement | null;\n if (!item) return;\n\n const bodySlot = item.querySelector<HTMLElement>(\"[data-toast-slot='body']\");\n if (!bodySlot) return;\n bodySlot.textContent = body;\n\n // Validate the live-region role at runtime; untrusted params/detail could\n // otherwise write an invalid ARIA role. Anything but \"alert\" stays polite.\n bodySlot.setAttribute(\"role\", this.#readField(event, \"type\") === \"alert\" ? \"alert\" : \"status\");\n\n this.listTarget.appendChild(item);\n this.dispatch(\"show\", { detail: { item } });\n }\n\n /**\n * Reads a string field from a Stimulus action param or a CustomEvent `detail`,\n * preferring the action param. Returns null unless a non-empty string is found,\n * so untrusted runtime payloads cannot inject non-string values.\n */\n #readField(event: Event, key: \"body\" | \"type\"): string | null {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParams = params?.[key];\n if (typeof fromParams === \"string\" && fromParams.length > 0) return fromParams;\n\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail && typeof detail === \"object\" && key in detail) {\n const value = (detail as Record<string, unknown>)[key];\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Dismisses the toast that contained the trigger. */\n dismiss(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n this.#removeWithTransition(item, \"user\");\n }\n\n /** Dismisses the focused toast when Escape is pressed. */\n onKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n // Layered-Escape rule 1: leave a press an inner handler already owned.\n // A press during IME composition (a text field inside the toast) cancels\n // the conversion, never the toast.\n if (event.defaultPrevented || event.isComposing) return;\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n event.preventDefault();\n this.#removeWithTransition(item, \"user\");\n }\n }\n\n /**\n * Pauses the auto-dismiss timer on mouse entry or keyboard focus.\n *\n * Hover and focus are tracked as independent reasons: the timer is only\n * snapshotted on the first active reason, and only resumed once *every*\n * reason has been released (see {@link resume}). This keeps a toast paused\n * while it is still hovered *or* focused, per WCAG 2.2.1.\n */\n pause(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n\n const reasons = this.#pauseReasonsFor(item);\n const wasActive = reasons.size > 0;\n reasons.add(this.#pauseReason(event));\n\n // Only snapshot the remaining time on the first reason; subsequent reasons\n // must not recompute elapsed against the already-cleared timer.\n if (wasActive || timeout.id === 0) return;\n\n this.#timers.clear(timeout.id);\n const elapsed = Date.now() - timeout.startedAt;\n const remaining = Math.max(0, timeout.remaining - elapsed);\n\n if (remaining <= 0) {\n this.#removeWithTransition(item, \"timeout\");\n return;\n }\n\n this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });\n item.setAttribute(\"data-paused\", \"true\");\n }\n\n /** Resumes the auto-dismiss timer once both hover and focus have been released. */\n resume(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const reasons = this.#pauseReasonsFor(item);\n reasons.delete(this.#pauseReason(event));\n // Still paused by the other reason (e.g. mouse left but focus remains).\n if (reasons.size > 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n if (timeout.id !== 0 || timeout.remaining <= 0) return;\n\n item.removeAttribute(\"data-paused\");\n this.#startTimer(item, timeout.remaining);\n }\n\n /** Resolves the toast item element a pause/resume event targets. */\n #itemFromEvent(event: Event): HTMLElement | null {\n const target = event.target instanceof Element ? event.target : event.currentTarget;\n if (!(target instanceof Element) || !this.hasListTarget) return null;\n const item = target.closest<HTMLElement>(\"[data-stimeo--toast-target='item']\");\n return item && this.listTarget.contains(item) ? item : null;\n }\n\n /** Classifies a pause/resume event as a hover or focus reason. */\n #pauseReason(event: Event): \"focus\" | \"hover\" {\n return event.type === \"focusin\" || event.type === \"focusout\" ? \"focus\" : \"hover\";\n }\n\n /** Lazily creates and returns the active pause-reason set for an item. */\n #pauseReasonsFor(item: HTMLElement): Set<string> {\n let reasons = this.#pauseReasons.get(item);\n if (!reasons) {\n reasons = new Set<string>();\n this.#pauseReasons.set(item, reasons);\n }\n return reasons;\n }\n\n #startTimer(element: HTMLElement, duration = this.durationValue): void {\n if (\n duration <= 0 ||\n element.dataset.state === \"leaving\" ||\n element.parentNode !== this.listTarget\n ) {\n return;\n }\n\n this.#clearTimer(element);\n const id = this.#timers.set(() => {\n this.#removeWithTransition(element, \"timeout\");\n }, duration);\n\n this.#activeTimeouts.set(element, { id, startedAt: Date.now(), remaining: duration });\n }\n\n #clearTimer(element: HTMLElement): void {\n const timeout = this.#activeTimeouts.get(element);\n if (timeout) {\n if (timeout.id) this.#timers.clear(timeout.id);\n this.#activeTimeouts.delete(element);\n }\n this.#pauseReasons.delete(element);\n }\n\n #removeWithTransition(element: HTMLElement, reason: \"timeout\" | \"user\"): void {\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n element.setAttribute(\"data-state\", \"leaving\");\n\n const finalize = () => {\n if (element.parentNode !== this.listTarget) return;\n this.listTarget.removeChild(element);\n this.dispatch(\"dismiss\", { detail: { item: element, reason } });\n };\n\n const duration = maxTransitionTotalMs(window.getComputedStyle(element));\n if (duration > 0) {\n this.#timers.set(finalize, duration);\n } else {\n finalize();\n }\n }\n\n /**\n * Removes the oldest toasts when the list exceeds `maxValue`.\n *\n * Public (not `#private`) as a deterministic test seam: enforcement normally\n * runs from `itemTargetConnected`, a Stimulus callback that fires via a\n * MutationObserver — which happy-dom does not reliably deliver — so the unit\n * tests invoke it directly. It is therefore listed in the contract guard's\n * NON_ACTION_ALLOWLIST (it is not a user-wired action).\n */\n enforceMaxLimit(): void {\n const currentItems = this.itemTargets;\n const max = Math.max(0, this.maxValue);\n if (currentItems.length > max) {\n const excessCount = currentItems.length - max;\n for (let i = 0; i < excessCount; i++) {\n const oldest = currentItems[i];\n if (oldest) this.#removeWithTransition(oldest, \"timeout\");\n }\n }\n }\n\n /** Wires stable-container delegation so newly appended items work immediately. */\n #connectDelegatedEvents(list = this.hasListTarget ? this.listTarget : null): void {\n this.#disconnectDelegatedEvents();\n if (!list) return;\n\n this.#delegatedList = list;\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.addEventListener(type, this.#onListEvent);\n }\n }\n\n /** Releases every delegated listener from the exact list that owns it. */\n #disconnectDelegatedEvents(): void {\n if (!this.#delegatedList) return;\n\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.removeEventListener(type, this.#onListEvent);\n }\n this.#delegatedList = null;\n }\n\n /** Routes every delegated item interaction from the stable list. */\n readonly #onListEvent = (event: Event): void => {\n if (event.type === \"click\") {\n const target = event.target instanceof Element ? event.target : null;\n const trigger = target?.closest<HTMLElement>(\"[data-toast-dismiss]\");\n if (trigger && this.#delegatedList?.contains(trigger)) this.dismiss(event);\n } else if (event.type === \"keydown\") {\n this.onKeydown(event as KeyboardEvent);\n } else if (this.#crossesItemBoundary(event)) {\n if (event.type === \"focusin\" || event.type === \"mouseover\") this.pause(event);\n else this.resume(event);\n }\n };\n\n /** Whether a bubbling focus/pointer event enters or leaves a toast boundary. */\n #crossesItemBoundary(event: Event): boolean {\n const item = this.#itemFromEvent(event);\n if (!item) return false;\n const related = \"relatedTarget\" in event ? event.relatedTarget : null;\n return !(related instanceof Node && item.contains(related));\n }\n\n /** Cancels the pending entering-to-visible frame owned by one item. */\n #cancelAnimation(element: HTMLElement): void {\n const handle = this.#rafHandles.get(element);\n if (handle === undefined) return;\n window.cancelAnimationFrame(handle);\n this.#rafHandles.delete(element);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/toast_controller.ts"],"names":[],"mappings":"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","file":"toast_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 { SafeTimeout } from \"./safe_timeout\";\n\n/** Computed transition fields needed to determine when an element has settled. */\nexport type TransitionStyle = Pick<\n CSSStyleDeclaration,\n \"transitionProperty\" | \"transitionDuration\" | \"transitionDelay\"\n>;\n\n/** Parsed transition property paired with its effective duration and delay. */\ninterface TransitionTiming {\n property: string;\n totalMs: number;\n}\n\n/** Parses a CSS `<time>` into milliseconds, returning `0` for malformed values. */\nfunction timeMs(value: string): number {\n const trimmed = value.trim();\n const amount = Number.parseFloat(trimmed);\n if (!Number.isFinite(amount)) return 0;\n if (trimmed.endsWith(\"ms\")) return amount;\n if (trimmed.endsWith(\"s\")) return amount * 1000;\n return 0;\n}\n\n/** Splits a computed comma-separated CSS list into normalized, non-empty items. */\nfunction cssList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Pairs transition properties, durations, and delays using CSS list repetition.\n *\n * The `transition-property` list determines the number of transitions. Shorter\n * duration/delay lists repeat, while extra values are ignored. A negative delay\n * shortens the remaining wall-clock time, and a non-positive total is settled.\n */\nfunction transitionTimings(style: TransitionStyle): TransitionTiming[] {\n const properties = cssList(style.transitionProperty);\n const durations = cssList(style.transitionDuration).map(timeMs);\n const delays = cssList(style.transitionDelay).map(timeMs);\n const effectiveProperties =\n properties.length > 0\n ? properties\n : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => \"all\");\n const effectiveDurations = durations.length > 0 ? durations : [0];\n const effectiveDelays = delays.length > 0 ? delays : [0];\n\n return effectiveProperties\n .filter((property) => property !== \"none\")\n .map((property, index) => ({\n property,\n totalMs: Math.max(\n 0,\n (effectiveDurations[index % effectiveDurations.length] ?? 0) +\n (effectiveDelays[index % effectiveDelays.length] ?? 0),\n ),\n }));\n}\n\n/** Maximum total over already-parsed timings (0 when the list is empty). */\nfunction maxTotalMs(timings: TransitionTiming[]): number {\n return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);\n}\n\n/** Maximum effective transition duration plus delay, in milliseconds. */\nexport function maxTransitionTotalMs(style: TransitionStyle): number {\n return maxTotalMs(transitionTimings(style));\n}\n\n/** Optional tuning for a single {@link TransitionCompletion.wait} call. */\nexport interface TransitionWaitOptions {\n /**\n * Positive wall-clock override (ms) for the bounded fallback, replacing the\n * auto-computed `max + 50ms` timer verbatim. It also keeps the wait armed for\n * a computed 0ms transition (no synchronous completion): the consumer asserts\n * a transition budget that computed styles cannot see. Non-positive and\n * non-finite (`NaN` / `Infinity`) values are ignored.\n */\n timeoutMs?: number;\n}\n\n/**\n * Owns one cancellable wait for an element's CSS transitions to settle.\n *\n * When available, active CSS transitions provide their expanded property names.\n * Tracking those names lets shorthand declarations settle from their longhand\n * terminal events. A terminal event from an interrupted phase is ignored while a\n * replacement transition for the same property is still active. Because that set\n * is sampled when the wait is armed, a property whose transition only starts a\n * frame later would otherwise be settled over; the still-running set is therefore\n * re-read before completing and adopted when it is not yet empty. Without Web\n * Animations data, explicit transition properties retain the declared-property\n * behavior and the ambiguous `transition-property: all` form waits until the\n * computed maximum wall-clock time. A `max + 50ms` timeout is always armed\n * because browsers emit no terminal event when a property never transitions, an\n * ancestor disappears, or rendering is otherwise interrupted; a consumer with\n * better knowledge can replace that fallback via\n * {@link TransitionWaitOptions.timeoutMs}.\n */\nexport class TransitionCompletion {\n readonly #timers = new SafeTimeout();\n #element: HTMLElement | null = null;\n #complete: (() => void) | null = null;\n #pendingProperties: Set<string> | null = null;\n #deadline = 0;\n\n /**\n * Replaces any prior wait and invokes `complete` synchronously for a 0ms\n * transition (including when `getComputedStyle` is unavailable).\n *\n * With a positive `options.timeoutMs` the synchronous fast-path is skipped and\n * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).\n */\n wait(element: HTMLElement, complete: () => void, options: TransitionWaitOptions = {}): void {\n this.cancel();\n\n const requested = options.timeoutMs ?? 0;\n const override = Number.isFinite(requested) && requested > 0 ? requested : 0;\n // A non-browser / partial-DOM environment cannot report a computable\n // transition, which is indistinguishable from a 0ms one.\n const timings =\n typeof window.getComputedStyle === \"function\"\n ? transitionTimings(window.getComputedStyle(element))\n : [];\n const maximum = maxTotalMs(timings);\n if (maximum <= 0 && override <= 0) {\n complete();\n return;\n }\n\n this.#element = element;\n this.#complete = complete;\n this.#deadline = Date.now() + maximum;\n const activeProperties = this.#activeTransitionProperties(element);\n this.#pendingProperties =\n activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);\n element.addEventListener(\"transitionend\", this.#onTerminal);\n element.addEventListener(\"transitioncancel\", this.#onTerminal);\n this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);\n }\n\n /** Cancels the pending wait without invoking its completion callback. */\n cancel(): void {\n this.#complete = null;\n this.#teardown();\n }\n\n /**\n * Handles terminal events from the observed element only.\n *\n * For explicit property lists, every declared positive-time property must\n * settle. For `all`, no reliable property set exists, so an event can finish\n * only after the computed maximum time; the safety timeout owns the usual path.\n */\n readonly #onTerminal = (event: Event): void => {\n if (event.target !== this.#element) return;\n const transitionEvent = event as TransitionEvent;\n // A pseudo-element's transition is reported with its originating element\n // as `target`; it must not settle the element's own transition.\n if (transitionEvent.pseudoElement) return;\n\n if (this.#pendingProperties) {\n const propertyName = transitionEvent.propertyName;\n const active = this.#activeTransitionProperties(this.#element);\n // An interrupted transition queues its cancel event before the replacement\n // phase starts, but dispatch can happen after the new wait is armed. The\n // replacement is then the active transition for this property, so the stale\n // terminal event must not settle the new phase.\n if (active.has(propertyName)) return;\n if (!this.#pendingProperties.delete(propertyName)) return;\n if (this.#pendingProperties.size > 0) return;\n // The tracked set is an arm-time snapshot, so a transition the phase starts\n // one frame later is absent from it. Re-reading the still-running set before\n // settling adopts those stragglers instead of completing over them; the\n // fallback timer stays armed, so this cannot extend the wait unboundedly.\n if (active.size > 0) {\n this.#pendingProperties = active;\n return;\n }\n this.#finish();\n return;\n }\n\n if (Date.now() >= this.#deadline) this.#finish();\n };\n\n /**\n * Returns active CSS transition properties expanded to the names reported by\n * terminal events. CSS animations and pseudo-element effects are excluded.\n */\n #activeTransitionProperties(element: HTMLElement | null): Set<string> {\n if (!element || typeof element.getAnimations !== \"function\") return new Set();\n\n try {\n const properties = element.getAnimations().flatMap((animation) => {\n if (animation.playState === \"idle\" || animation.playState === \"finished\") return [];\n const effect = animation.effect as KeyframeEffect | null;\n // A pseudo-element effect names the originating element as its target, so\n // `target` alone cannot separate it. Its terminal events are ignored, so\n // tracking its properties would only strand the wait on the fallback.\n if (effect?.pseudoElement) return [];\n const target = effect?.target;\n if (target && target !== element) return [];\n const property = (animation as Partial<CSSTransition>).transitionProperty;\n return typeof property === \"string\" && property.length > 0 ? [property] : [];\n });\n return new Set(properties);\n } catch {\n // Partial DOM implementations can expose getAnimations without supporting\n // it. The declared-property tracker and bounded timer remain safe fallbacks.\n return new Set();\n }\n }\n\n /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */\n #explicitPendingProperties(timings: TransitionTiming[]): Set<string> | null {\n if (timings.some(({ property }) => property === \"all\")) return null;\n const pending = new Set(\n timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property),\n );\n // An empty set is only reachable through a timeout override (a 0ms transition\n // otherwise completes synchronously). With nothing to track, fall back to the\n // deadline rule so any terminal event can settle the wait, exactly like `all`.\n return pending.size > 0 ? pending : null;\n }\n\n /** Completes exactly once, releasing listeners and the fallback before the callback. */\n #finish(): void {\n const complete = this.#complete;\n if (!complete) return;\n this.#complete = null;\n this.#teardown();\n complete();\n }\n\n /** Releases the exact element listeners and timer owned by the current wait. */\n #teardown(): void {\n this.#timers.clearAll();\n this.#element?.removeEventListener(\"transitionend\", this.#onTerminal);\n this.#element?.removeEventListener(\"transitioncancel\", this.#onTerminal);\n this.#element = null;\n this.#pendingProperties = null;\n this.#deadline = 0;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { maxTransitionTotalMs } from \"../utils/transition_completion\";\n\nconst DELEGATED_EVENTS = [\"click\", \"focusin\", \"focusout\", \"keydown\", \"mouseover\", \"mouseout\"];\n\n/**\n * Headless, highly accessible toast notification behavior.\n *\n * Markup contract (identifier: `stimeo--toast`):\n * <div data-controller=\"stimeo--toast\"\n * data-stimeo--toast-duration-value=\"5000\"\n * data-stimeo--toast-max-value=\"3\">\n * <!-- Attribute-only trigger: no hand-written JS required. -->\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n * <!-- The live region is a descendant; the controller element only needs\n * to enclose both the trigger(s) and the list/template targets. -->\n * <div role=\"region\" aria-label=\"Notifications\">\n * <ol data-stimeo--toast-target=\"list\"></ol>\n * <template data-stimeo--toast-target=\"template\">\n * <li data-stimeo--toast-target=\"item\" tabindex=\"0\">\n * <span role=\"status\" data-toast-slot=\"body\"></span>\n * <button type=\"button\" data-toast-dismiss>Dismiss</button>\n * </li>\n * </template>\n * </div>\n * </div>\n *\n * Implements WAI-ARIA live region status/alert announcements, limits simultaneous\n * elements, and pauses dismiss timeouts on hover or focus to comply with WCAG 2.2.1.\n *\n * @remarks\n * Behavior only. The controller handles state updates via `data-state` and lifecycle events\n * while leaving visual styling completely to the client's CSS transitions.\n */\nexport class ToastController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"template\", \"item\"];\n static override values = {\n duration: { type: Number, default: 0 },\n max: { type: Number, default: 3 },\n };\n static actions = [\"dismiss\", \"onKeydown\", \"pause\", \"resume\", \"show\"] as const;\n static events = [\"dismiss\", \"show\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly templateTarget: HTMLTemplateElement;\n declare readonly itemTargets: HTMLElement[];\n declare readonly hasListTarget: boolean;\n declare readonly hasTemplateTarget: boolean;\n\n declare durationValue: number;\n declare maxValue: number;\n\n /**\n * Registry for every auto-dismiss and transition-finalize timer the controller\n * schedules. {@link SafeTimeout} owns *registration and teardown only*; the\n * pause/resume remaining-time accounting stays in `#activeTimeouts` so the\n * per-widget WCAG 2.2.1 semantics are not flattened into the helper.\n */\n #timers = new SafeTimeout();\n\n /**\n * Pending one-shot `requestAnimationFrame` handles (the entering→visible flip).\n * Tracked so {@link disconnect} can cancel any that have not fired, preventing a\n * detached element from being mutated after it leaves the DOM (Turbo).\n */\n #rafHandles = new Map<HTMLElement, number>();\n\n /** Track active timeouts mapped by each toast element for safe cancellation. */\n #activeTimeouts = new Map<HTMLElement, { id: number; startedAt: number; remaining: number }>();\n\n /** Track active pause reasons (hover/focus) per toast for WCAG 2.2.1 pause/resume. */\n #pauseReasons = new Map<HTMLElement, Set<string>>();\n\n /** The stable list that owns delegated listeners for dynamically added items. */\n #delegatedList: HTMLElement | null = null;\n\n override connect(): void {\n this.#connectDelegatedEvents();\n this.enforceMaxLimit();\n for (const item of this.itemTargets) {\n if (!this.#activeTimeouts.has(item) && item.dataset.state !== \"leaving\") {\n this.#startTimer(item);\n }\n }\n }\n\n override disconnect(): void {\n // SafeTimeout owns every auto-dismiss + finalize timer; one call tears them\n // all down so none fires against the detached controller.\n this.#disconnectDelegatedEvents();\n this.#timers.clearAll();\n for (const handle of this.#rafHandles.values()) {\n window.cancelAnimationFrame(handle);\n }\n this.#rafHandles.clear();\n this.#activeTimeouts.clear();\n this.#pauseReasons.clear();\n }\n\n /** Rebinds delegated interaction when Turbo replaces the list target in place. */\n listTargetConnected(element: HTMLElement): void {\n if (this.#delegatedList !== element) this.#connectDelegatedEvents(element);\n }\n\n /** Releases delegation only when the removed target is its current owner. */\n listTargetDisconnected(element: HTMLElement): void {\n if (this.#delegatedList === element) this.#disconnectDelegatedEvents();\n }\n\n durationValueChanged(): void {\n for (const item of this.itemTargets) {\n if (item.dataset.state === \"leaving\") continue;\n\n const pauseReasons = this.#pauseReasons.get(item);\n this.#clearTimer(item);\n if (this.durationValue <= 0) {\n item.removeAttribute(\"data-paused\");\n } else if (pauseReasons && pauseReasons.size > 0) {\n this.#pauseReasons.set(item, pauseReasons);\n this.#activeTimeouts.set(item, {\n id: 0,\n startedAt: 0,\n remaining: this.durationValue,\n });\n item.setAttribute(\"data-paused\", \"true\");\n } else {\n this.#startTimer(item);\n }\n }\n }\n\n maxValueChanged(): void {\n this.enforceMaxLimit();\n }\n\n /**\n * Stimulus lifecycle callback triggered automatically when a new item target\n * enters the DOM. Perfectly handles dynamic client-side injections and server-side\n * Turbo Stream appends alike.\n */\n itemTargetConnected(element: HTMLElement): void {\n this.enforceMaxLimit();\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#startTimer(element);\n element.setAttribute(\"data-state\", \"entering\");\n this.#cancelAnimation(element);\n const handle = window.requestAnimationFrame(() => {\n this.#rafHandles.delete(element);\n if (element.parentNode !== this.listTarget || element.dataset.state === \"leaving\") return;\n element.setAttribute(\"data-state\", \"visible\");\n });\n this.#rafHandles.set(element, handle);\n }\n\n /** Clears any active timer when a toast is removed from the DOM. */\n itemTargetDisconnected(element: HTMLElement): void {\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n }\n\n /**\n * Shows a new toast. Accepts its content from either a Stimulus action param\n * (attribute-only trigger) or a programmatic `show` CustomEvent `detail`\n * (remote / Turbo trigger); the action param wins when both are present.\n *\n * <button data-action=\"click->stimeo--toast#show\"\n * data-stimeo--toast-body-param=\"Saved\"\n * data-stimeo--toast-type-param=\"status\">Show</button>\n *\n * element.dispatchEvent(new CustomEvent(\"show\", { detail: { body, type } }))\n *\n * Clones the template slot, interpolates the body text, and appends to the list.\n */\n show(event: Event): void {\n if (!this.hasTemplateTarget || !this.hasListTarget) return;\n\n const body = this.#readField(event, \"body\");\n if (!body) return;\n\n const clone = this.templateTarget.content.cloneNode(true) as DocumentFragment;\n const item = clone.querySelector(\"[data-stimeo--toast-target='item']\") as HTMLElement | null;\n if (!item) return;\n\n const bodySlot = item.querySelector<HTMLElement>(\"[data-toast-slot='body']\");\n if (!bodySlot) return;\n bodySlot.textContent = body;\n\n // Validate the live-region role at runtime; untrusted params/detail could\n // otherwise write an invalid ARIA role. Anything but \"alert\" stays polite.\n bodySlot.setAttribute(\"role\", this.#readField(event, \"type\") === \"alert\" ? \"alert\" : \"status\");\n\n this.listTarget.appendChild(item);\n this.dispatch(\"show\", { detail: { item } });\n }\n\n /**\n * Reads a string field from a Stimulus action param or a CustomEvent `detail`,\n * preferring the action param. Returns null unless a non-empty string is found,\n * so untrusted runtime payloads cannot inject non-string values.\n */\n #readField(event: Event, key: \"body\" | \"type\"): string | null {\n const params = (event as { params?: Record<string, unknown> }).params;\n const fromParams = params?.[key];\n if (typeof fromParams === \"string\" && fromParams.length > 0) return fromParams;\n\n const detail = (event as CustomEvent<unknown>).detail;\n if (detail && typeof detail === \"object\" && key in detail) {\n const value = (detail as Record<string, unknown>)[key];\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n return null;\n }\n\n /** Dismisses the toast that contained the trigger. */\n dismiss(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n this.#removeWithTransition(item, \"user\");\n }\n\n /** Dismisses the focused toast when Escape is pressed. */\n onKeydown(event: KeyboardEvent): void {\n if (event.key === \"Escape\") {\n // Leave a press an inner handler already owned.\n // A press during IME composition (a text field inside the toast) cancels\n // the conversion, never the toast.\n if (event.defaultPrevented || event.isComposing) return;\n const item = this.#itemFromEvent(event);\n if (!item) return;\n\n event.preventDefault();\n this.#removeWithTransition(item, \"user\");\n }\n }\n\n /**\n * Pauses the auto-dismiss timer on mouse entry or keyboard focus.\n *\n * Hover and focus are tracked as independent reasons: the timer is only\n * snapshotted on the first active reason, and only resumed once *every*\n * reason has been released (see {@link resume}). This keeps a toast paused\n * while it is still hovered *or* focused, per WCAG 2.2.1.\n */\n pause(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n\n const reasons = this.#pauseReasonsFor(item);\n const wasActive = reasons.size > 0;\n reasons.add(this.#pauseReason(event));\n\n // Only snapshot the remaining time on the first reason; subsequent reasons\n // must not recompute elapsed against the already-cleared timer.\n if (wasActive || timeout.id === 0) return;\n\n this.#timers.clear(timeout.id);\n const elapsed = Date.now() - timeout.startedAt;\n const remaining = Math.max(0, timeout.remaining - elapsed);\n\n if (remaining <= 0) {\n this.#removeWithTransition(item, \"timeout\");\n return;\n }\n\n this.#activeTimeouts.set(item, { id: 0, startedAt: 0, remaining });\n item.setAttribute(\"data-paused\", \"true\");\n }\n\n /** Resumes the auto-dismiss timer once both hover and focus have been released. */\n resume(event: Event): void {\n const item = this.#itemFromEvent(event);\n if (!item || this.durationValue <= 0) return;\n\n const reasons = this.#pauseReasonsFor(item);\n reasons.delete(this.#pauseReason(event));\n // Still paused by the other reason (e.g. mouse left but focus remains).\n if (reasons.size > 0) return;\n\n const timeout = this.#activeTimeouts.get(item);\n if (!timeout) return;\n if (timeout.id !== 0 || timeout.remaining <= 0) return;\n\n item.removeAttribute(\"data-paused\");\n this.#startTimer(item, timeout.remaining);\n }\n\n /** Resolves the toast item element a pause/resume event targets. */\n #itemFromEvent(event: Event): HTMLElement | null {\n const target = event.target instanceof Element ? event.target : event.currentTarget;\n if (!(target instanceof Element) || !this.hasListTarget) return null;\n const item = target.closest<HTMLElement>(\"[data-stimeo--toast-target='item']\");\n return item && this.listTarget.contains(item) ? item : null;\n }\n\n /** Classifies a pause/resume event as a hover or focus reason. */\n #pauseReason(event: Event): \"focus\" | \"hover\" {\n return event.type === \"focusin\" || event.type === \"focusout\" ? \"focus\" : \"hover\";\n }\n\n /** Lazily creates and returns the active pause-reason set for an item. */\n #pauseReasonsFor(item: HTMLElement): Set<string> {\n let reasons = this.#pauseReasons.get(item);\n if (!reasons) {\n reasons = new Set<string>();\n this.#pauseReasons.set(item, reasons);\n }\n return reasons;\n }\n\n #startTimer(element: HTMLElement, duration = this.durationValue): void {\n if (\n duration <= 0 ||\n element.dataset.state === \"leaving\" ||\n element.parentNode !== this.listTarget\n ) {\n return;\n }\n\n this.#clearTimer(element);\n const id = this.#timers.set(() => {\n this.#removeWithTransition(element, \"timeout\");\n }, duration);\n\n this.#activeTimeouts.set(element, { id, startedAt: Date.now(), remaining: duration });\n }\n\n #clearTimer(element: HTMLElement): void {\n const timeout = this.#activeTimeouts.get(element);\n if (timeout) {\n if (timeout.id) this.#timers.clear(timeout.id);\n this.#activeTimeouts.delete(element);\n }\n this.#pauseReasons.delete(element);\n }\n\n #removeWithTransition(element: HTMLElement, reason: \"timeout\" | \"user\"): void {\n if (element.dataset.state === \"leaving\" || element.parentNode !== this.listTarget) return;\n\n this.#clearTimer(element);\n this.#cancelAnimation(element);\n element.setAttribute(\"data-state\", \"leaving\");\n\n const finalize = () => {\n if (element.parentNode !== this.listTarget) return;\n this.listTarget.removeChild(element);\n this.dispatch(\"dismiss\", { detail: { item: element, reason } });\n };\n\n const duration = maxTransitionTotalMs(window.getComputedStyle(element));\n if (duration > 0) {\n this.#timers.set(finalize, duration);\n } else {\n finalize();\n }\n }\n\n /**\n * Removes the oldest toasts when the list exceeds `maxValue`.\n *\n * Public (not `#private`) as a deterministic seam: enforcement normally runs\n * from `itemTargetConnected`, a Stimulus callback delivered through a\n * MutationObserver, which a DOM-only environment does not reliably fire — so\n * it can also be invoked directly. It is not a user-wired action.\n */\n enforceMaxLimit(): void {\n const currentItems = this.itemTargets;\n const max = Math.max(0, this.maxValue);\n if (currentItems.length > max) {\n const excessCount = currentItems.length - max;\n for (let i = 0; i < excessCount; i++) {\n const oldest = currentItems[i];\n if (oldest) this.#removeWithTransition(oldest, \"timeout\");\n }\n }\n }\n\n /** Wires stable-container delegation so newly appended items work immediately. */\n #connectDelegatedEvents(list = this.hasListTarget ? this.listTarget : null): void {\n this.#disconnectDelegatedEvents();\n if (!list) return;\n\n this.#delegatedList = list;\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.addEventListener(type, this.#onListEvent);\n }\n }\n\n /** Releases every delegated listener from the exact list that owns it. */\n #disconnectDelegatedEvents(): void {\n if (!this.#delegatedList) return;\n\n for (const type of DELEGATED_EVENTS) {\n this.#delegatedList.removeEventListener(type, this.#onListEvent);\n }\n this.#delegatedList = null;\n }\n\n /** Routes every delegated item interaction from the stable list. */\n readonly #onListEvent = (event: Event): void => {\n if (event.type === \"click\") {\n const target = event.target instanceof Element ? event.target : null;\n const trigger = target?.closest<HTMLElement>(\"[data-toast-dismiss]\");\n if (trigger && this.#delegatedList?.contains(trigger)) this.dismiss(event);\n } else if (event.type === \"keydown\") {\n this.onKeydown(event as KeyboardEvent);\n } else if (this.#crossesItemBoundary(event)) {\n if (event.type === \"focusin\" || event.type === \"mouseover\") this.pause(event);\n else this.resume(event);\n }\n };\n\n /** Whether a bubbling focus/pointer event enters or leaves a toast boundary. */\n #crossesItemBoundary(event: Event): boolean {\n const item = this.#itemFromEvent(event);\n if (!item) return false;\n const related = \"relatedTarget\" in event ? event.relatedTarget : null;\n return !(related instanceof Node && item.contains(related));\n }\n\n /** Cancels the pending entering-to-visible frame owned by one item. */\n #cancelAnimation(element: HTMLElement): void {\n const handle = this.#rafHandles.get(element);\n if (handle === undefined) return;\n window.cancelAnimationFrame(handle);\n this.#rafHandles.delete(element);\n }\n}\n"]}