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/carousel_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/roving_tabindex.ts
6
24
  var RovingTabindex = class {
7
25
  /** Returns the current ordered item elements; called on every operation. */
@@ -104,6 +122,8 @@ var CarouselController = class extends Controller {
104
122
  ];
105
123
  static events = ["change", "pause", "play"];
106
124
  #roving = new RovingTabindex(() => this.pickerTargets);
125
+ /** Gates the target callback so it does not repaint once per authored picker on mount. */
126
+ #connected = false;
107
127
  #intervals = new SafeInterval();
108
128
  /** Index of the visible slide. */
109
129
  #index = 0;
@@ -113,7 +133,12 @@ var CarouselController = class extends Controller {
113
133
  #pointerPaused = false;
114
134
  /** Id of the live autoplay interval, or null when stopped. */
115
135
  #timerId = null;
116
- /** Renders the initial slide and starts autoplay when requested. */
136
+ /**
137
+ * Renders the initial slide and starts autoplay when requested.
138
+ *
139
+ * `findIndex` makes the authored pre-selection first-wins when several pickers
140
+ * are marked; `#render` then writes an explicit value onto every picker.
141
+ */
117
142
  connect() {
118
143
  const preselected = this.pickerTargets.findIndex(
119
144
  (picker) => picker.getAttribute("aria-selected") === "true"
@@ -122,6 +147,20 @@ var CarouselController = class extends Controller {
122
147
  this.#playing = this.#initialPlaying();
123
148
  this.#render({ focus: false });
124
149
  this.#syncTimer();
150
+ this.#connected = true;
151
+ }
152
+ /**
153
+ * Re-establishes the single selected picker when one is added after connect.
154
+ *
155
+ * Without this an appended picker that arrives `aria-selected="true"` leaves two
156
+ * marked at once — the authored pre-selection is only read on connect, so
157
+ * nothing else ever resolves the conflict. The current slide is kept: the
158
+ * repaint re-derives every picker from `#index`, so a late arrival never steals
159
+ * the selection.
160
+ */
161
+ pickerTargetConnected() {
162
+ if (!this.#connected) return;
163
+ this.#render({ focus: false });
125
164
  }
126
165
  /**
127
166
  * Resolves the starting autoplay intent. The play toggle's `aria-pressed` is the
@@ -138,6 +177,7 @@ var CarouselController = class extends Controller {
138
177
  }
139
178
  /** Clears the autoplay interval so it never fires after teardown. */
140
179
  disconnect() {
180
+ this.#connected = false;
141
181
  this.#intervals.clearAll();
142
182
  this.#timerId = null;
143
183
  }
@@ -185,20 +225,21 @@ var CarouselController = class extends Controller {
185
225
  }
186
226
  /** Picker roving: arrows move focus only; Home/End activate first/last slide. */
187
227
  onPickerKeydown(event) {
228
+ if (event.defaultPrevented) return;
229
+ if (isReservedArrowChord(event)) return;
188
230
  const current = this.pickerTargets.indexOf(event.currentTarget);
189
231
  if (current === -1) return;
190
232
  const length = this.pickerTargets.length;
191
233
  switch (event.key) {
192
234
  case "ArrowRight":
193
235
  case "ArrowDown":
194
- event.preventDefault();
195
- this.#roving.setActive(rovingMove(current, length, 1), { focus: true });
196
- return;
197
236
  case "ArrowLeft":
198
- case "ArrowUp":
237
+ case "ArrowUp": {
199
238
  event.preventDefault();
200
- this.#roving.setActive(rovingMove(current, length, -1), { focus: true });
239
+ const step = logicalArrowStep(event.key, this.element);
240
+ this.#roving.setActive(rovingMove(current, length, step), { focus: true });
201
241
  return;
242
+ }
202
243
  case "Home":
203
244
  event.preventDefault();
204
245
  this.#select(0, { focus: true });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/roving_tabindex.ts","../../src/utils/safe_timeout.ts","../../src/controllers/carousel_controller.ts"],"names":[],"mappings":";;;;;AAqBO,IAAM,iBAAN,MAAqB;AAAA;AAAA,EAEjB,SAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMT,YAAY,QAAA,EAA+B;AACzC,IAAA,IAAA,CAAK,SAAA,GAAY,QAAA;AAAA,EACnB;AAAA;AAAA,EAGA,IAAI,WAAA,GAAsB;AACxB,IAAA,OAAO,IAAA,CAAK,WAAU,CAAE,SAAA,CAAU,CAAC,IAAA,KAAS,IAAA,CAAK,aAAa,CAAC,CAAA;AAAA,EACjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,UAAU,KAAA,EAAe,EAAE,QAAQ,KAAA,EAAM,GAAyB,EAAC,EAAS;AAC1E,IAAA,MAAM,KAAA,GAAQ,KAAK,SAAA,EAAU;AAC7B,IAAA,KAAA,CAAM,OAAA,CAAQ,CAAC,IAAA,EAAM,CAAA,KAAM;AACzB,MAAA,IAAA,CAAK,QAAA,GAAW,CAAA,KAAM,KAAA,GAAQ,CAAA,GAAI,EAAA;AAAA,IACpC,CAAC,CAAA;AACD,IAAA,IAAI,KAAA,EAAO,KAAA,CAAM,KAAK,CAAA,EAAG,KAAA,EAAM;AAAA,EACjC;AACF,CAAA;AAkBO,SAAS,UAAA,CACd,OAAA,EACA,MAAA,EACA,KAAA,EACA,IAAA,EACQ;AACR,EAAA,IAAI,MAAA,KAAW,GAAG,OAAO,EAAA;AACzB,EAAA,MAAM,OAAO,OAAA,GAAU,KAAA;AACvB,EAAqB,OAAA,CAAQ,IAAA,GAAO,MAAA,IAAU,MAAA;AAEhD;;;AC1DA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AA8DO,IAAM,YAAA,GAAN,cAA2B,aAAA,CAAc;AAAA;AAAA,EAE9C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,KAAK,CAAA;AACxC,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,WAAA,CAAY,QAAA,EAAU,KAAK,CAAA;AAAA,EAC3C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,cAAc,EAAE,CAAA;AAAA,EACzB;AACF,CAAA;;;AC3FO,IAAM,kBAAA,GAAN,cAAiC,UAAA,CAAwB;AAAA,EAC9D,OAAgB,UAAU,CAAC,OAAA,EAAS,YAAY,MAAA,EAAQ,MAAA,EAAQ,UAAU,YAAY,CAAA;AAAA,EACtF,OAAgB,MAAA,GAAS;AAAA,IACvB,QAAA,EAAU,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA,EAAM;AAAA,IAC1C,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,GAAA,EAAK;AAAA,IACxC,IAAA,EAAM,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA;AAAK,GACvC;AAAA,EACA,OAAO,OAAA,GAAU;AAAA,IACf,MAAA;AAAA,IACA,MAAA;AAAA,IACA,iBAAA;AAAA,IACA,OAAA;AAAA,IACA,MAAA;AAAA,IACA,QAAA;AAAA,IACA;AAAA,GACF;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,QAAA,EAAU,SAAS,MAAM,CAAA;AAAA,EAUjC,OAAA,GAAU,IAAI,cAAA,CAAe,MAAM,KAAK,aAAa,CAAA;AAAA,EACrD,UAAA,GAAa,IAAI,YAAA,EAAa;AAAA;AAAA,EAEvC,MAAA,GAAS,CAAA;AAAA;AAAA,EAET,QAAA,GAAW,KAAA;AAAA;AAAA,EAEX,cAAA,GAAiB,KAAA;AAAA;AAAA,EAEjB,QAAA,GAA0B,IAAA;AAAA;AAAA,EAGjB,OAAA,GAAgB;AACvB,IAAA,MAAM,WAAA,GAAc,KAAK,aAAA,CAAc,SAAA;AAAA,MACrC,CAAC,MAAA,KAAW,MAAA,CAAO,YAAA,CAAa,eAAe,CAAA,KAAM;AAAA,KACvD;AACA,IAAA,IAAA,CAAK,MAAA,GAAS,WAAA,KAAgB,EAAA,GAAK,CAAA,GAAI,WAAA;AACvC,IAAA,IAAA,CAAK,QAAA,GAAW,KAAK,eAAA,EAAgB;AACrC,IAAA,IAAA,CAAK,OAAA,CAAQ,EAAE,KAAA,EAAO,KAAA,EAAO,CAAA;AAC7B,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,eAAA,GAA2B;AACzB,IAAA,IAAI,KAAK,mBAAA,IAAuB,IAAA,CAAK,gBAAA,CAAiB,YAAA,CAAa,cAAc,CAAA,EAAG;AAClF,MAAA,OAAO,IAAA,CAAK,gBAAA,CAAiB,YAAA,CAAa,cAAc,CAAA,KAAM,MAAA;AAAA,IAChE;AACA,IAAA,OAAO,IAAA,CAAK,aAAA;AAAA,EACd;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,WAAW,QAAA,EAAS;AACzB,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAAA,EAClB;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,KAAA,CAAM,CAAC,GAAG,EAAE,KAAA,EAAO,OAAO,CAAA;AAAA,EAC9C;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAK,KAAA,CAAM,EAAE,GAAG,EAAE,KAAA,EAAO,OAAO,CAAA;AAAA,EAC/C;AAAA;AAAA,EAGA,KAAK,KAAA,EAAoB;AACvB,IAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AACrB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,aAAA,CAAc,OAAA,CAAQ,MAAM,CAAA;AAC/C,IAAA,IAAI,KAAA,KAAU,IAAI,IAAA,CAAK,OAAA,CAAQ,OAAO,EAAE,KAAA,EAAO,OAAO,CAAA;AAAA,EACxD;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAA,CAAK,QAAA,GAAW,CAAC,IAAA,CAAK,QAAA;AACtB,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,KAAA,EAAqB;AACzB,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG;AACnC,MAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAAA,IAClB,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AAAA,IACxB;AACA,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,KAAA,EAAqB;AAC1B,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,UAAA,CAAW,OAAO,CAAA,EAAG;AACrC,IAAA,IAAA,CAAK,cAAA,GAAiB,KAAA;AACtB,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA;AAAA,EAGA,gBAAgB,KAAA,EAA4B;AAC1C,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,aAAA,CAAc,OAAA,CAAQ,MAAM,aAA4B,CAAA;AAC7E,IAAA,IAAI,YAAY,EAAA,EAAI;AAEpB,IAAA,MAAM,MAAA,GAAS,KAAK,aAAA,CAAc,MAAA;AAClC,IAAA,QAAQ,MAAM,GAAA;AAAK,MACjB,KAAK,YAAA;AAAA,MACL,KAAK,WAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,OAAA,CAAQ,SAAA,CAAU,UAAA,CAAW,OAAA,EAAS,MAAA,EAAQ,CAAS,CAAA,EAAG,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAC9E,QAAA;AAAA,MACF,KAAK,WAAA;AAAA,MACL,KAAK,SAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,OAAA,CAAQ,SAAA,CAAU,UAAA,CAAW,OAAA,EAAS,MAAA,EAAQ,EAAU,CAAA,EAAG,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAC/E,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,OAAA,CAAQ,CAAA,EAAG,EAAE,KAAA,EAAO,MAAM,CAAA;AAC/B,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,QAAQ,MAAA,GAAS,CAAA,EAAG,EAAE,KAAA,EAAO,MAAM,CAAA;AACxC,QAAA;AACF;AACF,EACF;AAAA;AAAA,EAGA,MAAM,KAAA,EAAuB;AAC3B,IAAA,MAAM,KAAA,GAAQ,KAAK,YAAA,CAAa,MAAA;AAChC,IAAA,IAAI,KAAA,KAAU,GAAG,OAAO,CAAA;AACxB,IAAA,MAAM,IAAA,GAAO,KAAK,MAAA,GAAS,KAAA;AAC3B,IAAA,IAAI,IAAA,CAAK,SAAA,EAAW,OAAA,CAAQ,IAAA,GAAO,KAAA,IAAS,KAAA;AAC5C,IAAA,OAAO,IAAA,CAAK,IAAI,KAAA,GAAQ,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,IAAI,CAAC,CAAA;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,OAAA,CAAQ,KAAA,EAAe,EAAE,KAAA,EAAM,EAA6B;AAC1D,IAAA,MAAM,OAAA,GAAU,UAAU,IAAA,CAAK,MAAA;AAC/B,IAAA,IAAA,CAAK,MAAA,GAAS,KAAA;AACd,IAAA,IAAA,CAAK,OAAA,CAAQ,EAAE,KAAA,EAAO,CAAA;AAGtB,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAI,OAAA,EAAS,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,EAAE,MAAA,EAAQ,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,CAAK,YAAA,CAAa,MAAA,IAAU,CAAA;AAAA,EAC7F;AAAA;AAAA,EAGA,OAAA,CAAQ,EAAE,KAAA,EAAM,EAA6B;AAC3C,IAAA,IAAA,CAAK,YAAA,CAAa,OAAA,CAAQ,CAAC,KAAA,EAAO,CAAA,KAAM;AACtC,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,MAAA;AAC1B,MAAA,KAAA,CAAM,YAAA,CAAa,YAAA,EAAc,MAAA,GAAS,QAAA,GAAW,UAAU,CAAA;AAC/D,MAAA,KAAA,CAAM,SAAS,CAAC,MAAA;AAAA,IAClB,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,aAAA,CAAc,OAAA,CAAQ,CAAC,MAAA,EAAQ,CAAA,KAAM;AACxC,MAAA,MAAA,CAAO,aAAa,eAAA,EAAiB,CAAA,KAAM,IAAA,CAAK,MAAA,GAAS,SAAS,OAAO,CAAA;AAAA,IAC3E,CAAC,CAAA;AACD,IAAA,IAAA,CAAK,QAAQ,SAAA,CAAU,IAAA,CAAK,MAAA,EAAQ,EAAE,OAAO,CAAA;AAAA,EAC/C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAA,GAAmB;AAKjB,IAAA,IAAI,CAAC,KAAK,SAAA,IAAa,IAAA,CAAK,UAAU,IAAA,CAAK,YAAA,CAAa,SAAS,CAAA,EAAG;AAClE,MAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAAA,IAClB;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,QAAA,IAAY,CAAC,KAAK,cAAA,IAAkB,IAAA,CAAK,aAAa,MAAA,GAAS,CAAA;AAEtF,IAAA,IAAI,SAAA,IAAa,IAAA,CAAK,QAAA,KAAa,IAAA,EAAM;AACvC,MAAA,IAAA,CAAK,QAAA,GAAW,KAAK,UAAA,CAAW,GAAA,CAAI,MAAM,IAAA,CAAK,IAAA,EAAK,EAAG,IAAA,CAAK,aAAa,CAAA;AACzE,MAAA,IAAA,CAAK,SAAS,MAAM,CAAA;AAAA,IACtB,CAAA,MAAA,IAAW,CAAC,SAAA,IAAa,IAAA,CAAK,aAAa,IAAA,EAAM;AAC/C,MAAA,IAAA,CAAK,UAAA,CAAW,KAAA,CAAM,IAAA,CAAK,QAAQ,CAAA;AACnC,MAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,MAAA,IAAA,CAAK,SAAS,OAAO,CAAA;AAAA,IACvB;AAEA,IAAA,IAAI,KAAK,mBAAA,EAAqB;AAC5B,MAAA,IAAA,CAAK,iBAAiB,YAAA,CAAa,cAAA,EAAgB,IAAA,CAAK,QAAA,GAAW,SAAS,OAAO,CAAA;AAAA,IACrF;AAAA,EACF;AACF","file":"carousel_controller.js","sourcesContent":["/**\n * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item.\n */\n setActive(index: number, { focus = false }: { focus?: boolean } = {}): void {\n const items = this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { SafeInterval } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible **Carousel** (slideshow) behavior.\n *\n * Markup contract (identifier: `stimeo--carousel`):\n * <section data-controller=\"stimeo--carousel\" aria-roledescription=\"carousel\"\n * aria-label=\"Featured\"\n * data-stimeo--carousel-autoplay-value=\"false\"\n * data-stimeo--carousel-interval-value=\"5000\"\n * data-stimeo--carousel-loop-value=\"true\"\n * data-action=\"mouseenter->stimeo--carousel#pause\n * mouseleave->stimeo--carousel#resume\n * focusin->stimeo--carousel#pause\n * focusout->stimeo--carousel#resume\">\n * <button data-stimeo--carousel-target=\"playToggle\"\n * data-action=\"stimeo--carousel#togglePlay\">…</button>\n * <div data-stimeo--carousel-target=\"viewport\">\n * <div role=\"tabpanel\" data-stimeo--carousel-target=\"slide\">…</div>\n * <div role=\"tabpanel\" data-stimeo--carousel-target=\"slide\" hidden>…</div>\n * </div>\n * <button data-stimeo--carousel-target=\"prev\" data-action=\"stimeo--carousel#prev\">‹</button>\n * <button data-stimeo--carousel-target=\"next\" data-action=\"stimeo--carousel#next\">›</button>\n * <div role=\"tablist\">\n * <button role=\"tab\" data-stimeo--carousel-target=\"picker\"\n * data-action=\"stimeo--carousel#goto\n * keydown->stimeo--carousel#onPickerKeydown\"></button>\n * </div>\n * </section>\n *\n * Implements the WAI-ARIA APG **Carousel** (tabbed) pattern. The current slide is\n * exposed through `data-state` (`active`/`inactive`) and the `hidden` attribute on\n * inactive slides (removing them from focus order); the matching picker carries\n * `aria-selected` and the single roving `tabindex`. The play/pause toggle's\n * `aria-pressed` mirrors the autoplay state.\n *\n * @remarks\n * Behavior only — transitions, layout, and visuals are the consumer's CSS.\n * Autoplay honors WCAG 2.2.2: it suspends while the pointer is over the carousel\n * and **hard-stops** when keyboard focus enters (it does not silently resume on\n * focus out — the user must press play), so motion never surprises a keyboard\n * user. The interval is cleared on `disconnect()` (Turbo navigation included).\n * Picker arrow keys move focus only (manual activation); slide changes never steal\n * focus from the control the user operated.\n */\nexport class CarouselController extends Controller<HTMLElement> {\n static override targets = [\"slide\", \"viewport\", \"prev\", \"next\", \"picker\", \"playToggle\"];\n static override values = {\n autoplay: { type: Boolean, default: false },\n interval: { type: Number, default: 5000 },\n loop: { type: Boolean, default: true },\n };\n static actions = [\n \"goto\",\n \"next\",\n \"onPickerKeydown\",\n \"pause\",\n \"prev\",\n \"resume\",\n \"togglePlay\",\n ] as const;\n static events = [\"change\", \"pause\", \"play\"] as const;\n\n declare readonly slideTargets: HTMLElement[];\n declare readonly pickerTargets: HTMLElement[];\n declare readonly playToggleTarget: HTMLElement;\n declare readonly hasPlayToggleTarget: boolean;\n declare autoplayValue: boolean;\n declare intervalValue: number;\n declare loopValue: boolean;\n\n readonly #roving = new RovingTabindex(() => this.pickerTargets);\n readonly #intervals = new SafeInterval();\n /** Index of the visible slide. */\n #index = 0;\n /** User intent to autoplay (toggled by the play button / focus hard-stop). */\n #playing = false;\n /** Pointer is hovering the carousel: a temporary, auto-resuming suspension. */\n #pointerPaused = false;\n /** Id of the live autoplay interval, or null when stopped. */\n #timerId: number | null = null;\n\n /** Renders the initial slide and starts autoplay when requested. */\n override connect(): void {\n const preselected = this.pickerTargets.findIndex(\n (picker) => picker.getAttribute(\"aria-selected\") === \"true\",\n );\n this.#index = preselected === -1 ? 0 : preselected;\n this.#playing = this.#initialPlaying();\n this.#render({ focus: false });\n this.#syncTimer();\n }\n\n /**\n * Resolves the starting autoplay intent. The play toggle's `aria-pressed` is the\n * source of truth **when present**, so a Turbo Drive cache restore / morph that\n * re-runs `connect()` against existing DOM does not silently resume autoplay the\n * user had stopped (e.g. by focusing into the carousel). Only when no toggle\n * carries `aria-pressed` does it fall back to the declarative `autoplay` value.\n */\n #initialPlaying(): boolean {\n if (this.hasPlayToggleTarget && this.playToggleTarget.hasAttribute(\"aria-pressed\")) {\n return this.playToggleTarget.getAttribute(\"aria-pressed\") === \"true\";\n }\n return this.autoplayValue;\n }\n\n /** Clears the autoplay interval so it never fires after teardown. */\n override disconnect(): void {\n this.#intervals.clearAll();\n this.#timerId = null;\n }\n\n /** Advances to the next slide. Bound via `data-action`. */\n next(): void {\n this.#select(this.#step(1), { focus: false });\n }\n\n /** Returns to the previous slide. Bound via `data-action`. */\n prev(): void {\n this.#select(this.#step(-1), { focus: false });\n }\n\n /** Jumps to the slide whose picker was activated (click / Enter / Space). */\n goto(event: Event): void {\n const target = event.currentTarget as HTMLElement;\n const index = this.pickerTargets.indexOf(target);\n if (index !== -1) this.#select(index, { focus: false });\n }\n\n /** Toggles autoplay on the user's explicit request and syncs the timer. */\n togglePlay(): void {\n this.#playing = !this.#playing;\n this.#syncTimer();\n }\n\n /**\n * Suspends autoplay. Hover (`mouseenter`) is a temporary suspension that resumes\n * on leave; keyboard focus (`focusin`) is a hard stop that turns autoplay off so\n * it cannot resume without an explicit play (WCAG 2.2.2).\n */\n pause(event?: Event): void {\n if (event?.type.startsWith(\"focus\")) {\n this.#playing = false;\n } else {\n this.#pointerPaused = true;\n }\n this.#syncTimer();\n }\n\n /**\n * Lifts a hover suspension (`mouseleave`) and resumes autoplay if it is still\n * on. A `focusout` does nothing here: the focus pause was a hard stop, so the\n * user must press play to restart.\n */\n resume(event?: Event): void {\n if (event?.type.startsWith(\"focus\")) return;\n this.#pointerPaused = false;\n this.#syncTimer();\n }\n\n /** Picker roving: arrows move focus only; Home/End activate first/last slide. */\n onPickerKeydown(event: KeyboardEvent): void {\n const current = this.pickerTargets.indexOf(event.currentTarget as HTMLElement);\n if (current === -1) return;\n\n const length = this.pickerTargets.length;\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowDown\":\n event.preventDefault();\n this.#roving.setActive(rovingMove(current, length, 1, \"wrap\"), { focus: true });\n return;\n case \"ArrowLeft\":\n case \"ArrowUp\":\n event.preventDefault();\n this.#roving.setActive(rovingMove(current, length, -1, \"wrap\"), { focus: true });\n return;\n case \"Home\":\n event.preventDefault();\n this.#select(0, { focus: true });\n return;\n case \"End\":\n event.preventDefault();\n this.#select(length - 1, { focus: true });\n return;\n default:\n }\n }\n\n /** Resolves the index one step away from the current one, honoring `loop`. */\n #step(delta: number): number {\n const total = this.slideTargets.length;\n if (total === 0) return 0;\n const next = this.#index + delta;\n if (this.loopValue) return (next + total) % total;\n return Math.min(total - 1, Math.max(0, next));\n }\n\n /**\n * Changes the active slide, updates state hooks, and emits `change` — but only\n * when the index actually changes, so a `next`/`prev` clamped at the end (or an\n * autoplay tick at a non-looping boundary) re-renders without a spurious event\n * (matching the \"emit on real change\" policy of flash/masonry/bulk-select).\n */\n #select(index: number, { focus }: { focus: boolean }): void {\n const changed = index !== this.#index;\n this.#index = index;\n this.#render({ focus });\n // Re-evaluate autoplay after every move so reaching the non-looping end stops\n // the timer (see `#syncTimer`); idempotent for moves that don't cross a boundary.\n this.#syncTimer();\n if (changed) this.dispatch(\"change\", { detail: { index, total: this.slideTargets.length } });\n }\n\n /** Reflects `this.#index` onto slides and pickers (state hooks + roving). */\n #render({ focus }: { focus: boolean }): void {\n this.slideTargets.forEach((slide, i) => {\n const active = i === this.#index;\n slide.setAttribute(\"data-state\", active ? \"active\" : \"inactive\");\n slide.hidden = !active;\n });\n this.pickerTargets.forEach((picker, i) => {\n picker.setAttribute(\"aria-selected\", i === this.#index ? \"true\" : \"false\");\n });\n this.#roving.setActive(this.#index, { focus });\n }\n\n /**\n * Drives the autoplay interval toward the desired state. Autoplay should run\n * only when the user wants it (`playing`), the pointer is not hovering, and more\n * than one slide exists. Transitions emit `play`/`pause` and keep the toggle's\n * `aria-pressed` in sync.\n */\n #syncTimer(): void {\n // A non-looping carousel sitting on its last slide has nothing left to advance\n // to, so autoplay turns itself off (a hard stop, like the focus pause): the\n // timer is cleared, `aria-pressed` flips to false, and a manual step back will\n // not silently restart it without an explicit play.\n if (!this.loopValue && this.#index >= this.slideTargets.length - 1) {\n this.#playing = false;\n }\n const shouldRun = this.#playing && !this.#pointerPaused && this.slideTargets.length > 1;\n\n if (shouldRun && this.#timerId === null) {\n this.#timerId = this.#intervals.set(() => this.next(), this.intervalValue);\n this.dispatch(\"play\");\n } else if (!shouldRun && this.#timerId !== null) {\n this.#intervals.clear(this.#timerId);\n this.#timerId = null;\n this.dispatch(\"pause\");\n }\n\n if (this.hasPlayToggleTarget) {\n this.playToggleTarget.setAttribute(\"aria-pressed\", this.#playing ? \"true\" : \"false\");\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/roving_tabindex.ts","../../src/utils/safe_timeout.ts","../../src/controllers/carousel_controller.ts"],"names":[],"mappings":"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","file":"carousel_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 * Low-level roving-tabindex primitive shared by composite-widget controllers.\n *\n * The APG roving-tabindex pattern keeps a composite widget a single Tab stop:\n * exactly one item is in the Tab sequence (`tabindex=\"0\"`) while the rest are\n * removed from it (`tabindex=\"-1\"`), and the arrow keys move both DOM focus and\n * that single tabbable position together. {@link RovingTabindex} owns *only* that\n * mechanical bookkeeping — \"which one item is tabbable, and move focus there\".\n *\n * It is intentionally **policy-free**. Orientation, wrapping vs. clamping,\n * selection-follows-focus, typeahead, and `Home`/`End` semantics differ per APG\n * pattern (Radio Group, Toolbar, Rating, …); folding them into one helper would\n * flatten those widgets to a lowest common denominator and lose each pattern's\n * correctness. Those decisions therefore stay in each controller, which calls\n * {@link RovingTabindex.setActive} with an index it computed itself (optionally\n * via the pure {@link rovingMove} helper).\n *\n * @remarks\n * Items are read lazily through a getter so a controller can add or remove\n * targets (Stimulus re-scans the DOM) without re-wiring this helper.\n */\nexport class RovingTabindex {\n /** Returns the current ordered item elements; called on every operation. */\n readonly #getItems: () => HTMLElement[];\n\n /**\n * @param getItems - Returns the current ordered item elements. Called on every\n * operation so the live target list is always used.\n */\n constructor(getItems: () => HTMLElement[]) {\n this.#getItems = getItems;\n }\n\n /** Index of the currently tabbable item (`tabindex=\"0\"`), or `-1` if none. */\n get activeIndex(): number {\n return this.#getItems().findIndex((item) => item.tabIndex === 0);\n }\n\n /**\n * Makes exactly the item at `index` tabbable (`tabindex=\"0\"`) and removes every\n * other item from the Tab sequence (`tabindex=\"-1\"`). An out-of-range `index`\n * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express\n * \"nothing is currently tabbable\".\n *\n * @param index - Position of the item to make tabbable.\n * @param options - Pass `{ focus: true }` to also move DOM focus to that item.\n */\n setActive(index: number, { focus = false }: { focus?: boolean } = {}): void {\n const items = this.#getItems();\n items.forEach((item, i) => {\n item.tabIndex = i === index ? 0 : -1;\n });\n if (focus) items[index]?.focus();\n }\n}\n\n/** Edge behavior for {@link rovingMove}: cycle past the ends, or stop at them. */\nexport type RovingWrap = \"wrap\" | \"clamp\";\n\n/**\n * Pure helper that resolves the target index for a one-step directional move.\n *\n * Keyboard/orientation mapping stays in the caller: it decides that a key means\n * `delta` `+1` (next) or `-1` (previous) and whether the widget should `\"wrap\"`\n * (Radio Group, Toolbar) or `\"clamp\"` at the ends.\n *\n * @param current - The index focus is moving from.\n * @param length - Number of items in the set.\n * @param delta - `+1` to move to the next item, `-1` for the previous.\n * @param wrap - `\"wrap\"` cycles around the ends; `\"clamp\"` stops at them.\n * @returns The resolved index, or `-1` when there are no items.\n */\nexport function rovingMove(\n current: number,\n length: number,\n delta: number,\n wrap: RovingWrap,\n): number {\n if (length === 0) return -1;\n const next = current + delta;\n if (wrap === \"wrap\") return (next + length) % length;\n return Math.min(length - 1, Math.max(0, next));\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowStep } from \"../utils/arrow_step\";\nimport { RovingTabindex, rovingMove } from \"../utils/roving_tabindex\";\nimport { SafeInterval } from \"../utils/safe_timeout\";\n\n/**\n * Headless, accessible **Carousel** (slideshow) behavior.\n *\n * Markup contract (identifier: `stimeo--carousel`):\n * <section data-controller=\"stimeo--carousel\" aria-roledescription=\"carousel\"\n * aria-label=\"Featured\"\n * data-stimeo--carousel-autoplay-value=\"false\"\n * data-stimeo--carousel-interval-value=\"5000\"\n * data-stimeo--carousel-loop-value=\"true\"\n * data-action=\"mouseenter->stimeo--carousel#pause\n * mouseleave->stimeo--carousel#resume\n * focusin->stimeo--carousel#pause\n * focusout->stimeo--carousel#resume\">\n * <button data-stimeo--carousel-target=\"playToggle\"\n * data-action=\"stimeo--carousel#togglePlay\">…</button>\n * <div data-stimeo--carousel-target=\"viewport\">\n * <div role=\"tabpanel\" data-stimeo--carousel-target=\"slide\">…</div>\n * <div role=\"tabpanel\" data-stimeo--carousel-target=\"slide\" hidden>…</div>\n * </div>\n * <button data-stimeo--carousel-target=\"prev\" data-action=\"stimeo--carousel#prev\">‹</button>\n * <button data-stimeo--carousel-target=\"next\" data-action=\"stimeo--carousel#next\">›</button>\n * <div role=\"tablist\">\n * <button role=\"tab\" data-stimeo--carousel-target=\"picker\"\n * data-action=\"stimeo--carousel#goto\n * keydown->stimeo--carousel#onPickerKeydown\"></button>\n * </div>\n * </section>\n *\n * Implements the WAI-ARIA APG **Carousel** (tabbed) pattern. The current slide is\n * exposed through `data-state` (`active`/`inactive`) and the `hidden` attribute on\n * inactive slides (removing them from focus order); the matching picker carries\n * `aria-selected` and the single roving `tabindex`. The play/pause toggle's\n * `aria-pressed` mirrors the autoplay state.\n *\n * @remarks\n * Behavior only — transitions, layout, and visuals are the consumer's CSS.\n * Autoplay honors WCAG 2.2.2: it suspends while the pointer is over the carousel\n * and **hard-stops** when keyboard focus enters (it does not silently resume on\n * focus out — the user must press play), so motion never surprises a keyboard\n * user. The interval is cleared on `disconnect()` (Turbo navigation included).\n * Picker arrow keys move focus only (manual activation); slide changes never steal\n * focus from the control the user operated.\n */\nexport class CarouselController extends Controller<HTMLElement> {\n static override targets = [\"slide\", \"viewport\", \"prev\", \"next\", \"picker\", \"playToggle\"];\n static override values = {\n autoplay: { type: Boolean, default: false },\n interval: { type: Number, default: 5000 },\n loop: { type: Boolean, default: true },\n };\n static actions = [\n \"goto\",\n \"next\",\n \"onPickerKeydown\",\n \"pause\",\n \"prev\",\n \"resume\",\n \"togglePlay\",\n ] as const;\n static events = [\"change\", \"pause\", \"play\"] as const;\n\n declare readonly slideTargets: HTMLElement[];\n declare readonly pickerTargets: HTMLElement[];\n declare readonly playToggleTarget: HTMLElement;\n declare readonly hasPlayToggleTarget: boolean;\n declare autoplayValue: boolean;\n declare intervalValue: number;\n declare loopValue: boolean;\n\n readonly #roving = new RovingTabindex(() => this.pickerTargets);\n /** Gates the target callback so it does not repaint once per authored picker on mount. */\n #connected = false;\n readonly #intervals = new SafeInterval();\n /** Index of the visible slide. */\n #index = 0;\n /** User intent to autoplay (toggled by the play button / focus hard-stop). */\n #playing = false;\n /** Pointer is hovering the carousel: a temporary, auto-resuming suspension. */\n #pointerPaused = false;\n /** Id of the live autoplay interval, or null when stopped. */\n #timerId: number | null = null;\n\n /**\n * Renders the initial slide and starts autoplay when requested.\n *\n * `findIndex` makes the authored pre-selection first-wins when several pickers\n * are marked; `#render` then writes an explicit value onto every picker.\n */\n override connect(): void {\n const preselected = this.pickerTargets.findIndex(\n (picker) => picker.getAttribute(\"aria-selected\") === \"true\",\n );\n this.#index = preselected === -1 ? 0 : preselected;\n this.#playing = this.#initialPlaying();\n this.#render({ focus: false });\n this.#syncTimer();\n this.#connected = true;\n }\n\n /**\n * Re-establishes the single selected picker when one is added after connect.\n *\n * Without this an appended picker that arrives `aria-selected=\"true\"` leaves two\n * marked at once — the authored pre-selection is only read on connect, so\n * nothing else ever resolves the conflict. The current slide is kept: the\n * repaint re-derives every picker from `#index`, so a late arrival never steals\n * the selection.\n */\n pickerTargetConnected(): void {\n if (!this.#connected) return;\n this.#render({ focus: false });\n }\n\n /**\n * Resolves the starting autoplay intent. The play toggle's `aria-pressed` is the\n * source of truth **when present**, so a Turbo Drive cache restore / morph that\n * re-runs `connect()` against existing DOM does not silently resume autoplay the\n * user had stopped (e.g. by focusing into the carousel). Only when no toggle\n * carries `aria-pressed` does it fall back to the declarative `autoplay` value.\n */\n #initialPlaying(): boolean {\n if (this.hasPlayToggleTarget && this.playToggleTarget.hasAttribute(\"aria-pressed\")) {\n return this.playToggleTarget.getAttribute(\"aria-pressed\") === \"true\";\n }\n return this.autoplayValue;\n }\n\n /** Clears the autoplay interval so it never fires after teardown. */\n override disconnect(): void {\n this.#connected = false;\n this.#intervals.clearAll();\n this.#timerId = null;\n }\n\n /** Advances to the next slide. Bound via `data-action`. */\n next(): void {\n this.#select(this.#step(1), { focus: false });\n }\n\n /** Returns to the previous slide. Bound via `data-action`. */\n prev(): void {\n this.#select(this.#step(-1), { focus: false });\n }\n\n /** Jumps to the slide whose picker was activated (click / Enter / Space). */\n goto(event: Event): void {\n const target = event.currentTarget as HTMLElement;\n const index = this.pickerTargets.indexOf(target);\n if (index !== -1) this.#select(index, { focus: false });\n }\n\n /** Toggles autoplay on the user's explicit request and syncs the timer. */\n togglePlay(): void {\n this.#playing = !this.#playing;\n this.#syncTimer();\n }\n\n /**\n * Suspends autoplay. Hover (`mouseenter`) is a temporary suspension that resumes\n * on leave; keyboard focus (`focusin`) is a hard stop that turns autoplay off so\n * it cannot resume without an explicit play (WCAG 2.2.2).\n */\n pause(event?: Event): void {\n if (event?.type.startsWith(\"focus\")) {\n this.#playing = false;\n } else {\n this.#pointerPaused = true;\n }\n this.#syncTimer();\n }\n\n /**\n * Lifts a hover suspension (`mouseleave`) and resumes autoplay if it is still\n * on. A `focusout` does nothing here: the focus pause was a hard stop, so the\n * user must press play to restart.\n */\n resume(event?: Event): void {\n if (event?.type.startsWith(\"focus\")) return;\n this.#pointerPaused = false;\n this.#syncTimer();\n }\n\n /** Picker roving: arrows move focus only; Home/End activate first/last slide. */\n onPickerKeydown(event: KeyboardEvent): void {\n // A descendant widget that already claimed the key must not ALSO move the\n // picker focus or change the slide — composition depends on this yield.\n if (event.defaultPrevented) return;\n if (isReservedArrowChord(event)) return;\n const current = this.pickerTargets.indexOf(event.currentTarget as HTMLElement);\n if (current === -1) return;\n\n const length = this.pickerTargets.length;\n // Logical, not physical. The helper reverses only the horizontal\n // pair, so folding Down/Up into the same branch stays correct.\n switch (event.key) {\n case \"ArrowRight\":\n case \"ArrowDown\":\n case \"ArrowLeft\":\n case \"ArrowUp\": {\n event.preventDefault();\n const step = logicalArrowStep(event.key, this.element);\n this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n return;\n }\n case \"Home\":\n event.preventDefault();\n this.#select(0, { focus: true });\n return;\n case \"End\":\n event.preventDefault();\n this.#select(length - 1, { focus: true });\n return;\n default:\n }\n }\n\n /** Resolves the index one step away from the current one, honoring `loop`. */\n #step(delta: number): number {\n const total = this.slideTargets.length;\n if (total === 0) return 0;\n const next = this.#index + delta;\n if (this.loopValue) return (next + total) % total;\n return Math.min(total - 1, Math.max(0, next));\n }\n\n /**\n * Changes the active slide, updates state hooks, and emits `change` — but only\n * when the index actually changes, so a `next`/`prev` clamped at the end (or an\n * autoplay tick at a non-looping boundary) re-renders without a spurious event\n * (matching the \"emit on real change\" policy of flash/masonry/bulk-select).\n */\n #select(index: number, { focus }: { focus: boolean }): void {\n const changed = index !== this.#index;\n this.#index = index;\n this.#render({ focus });\n // Re-evaluate autoplay after every move so reaching the non-looping end stops\n // the timer (see `#syncTimer`); idempotent for moves that don't cross a boundary.\n this.#syncTimer();\n if (changed) this.dispatch(\"change\", { detail: { index, total: this.slideTargets.length } });\n }\n\n /** Reflects `this.#index` onto slides and pickers (state hooks + roving). */\n #render({ focus }: { focus: boolean }): void {\n this.slideTargets.forEach((slide, i) => {\n const active = i === this.#index;\n slide.setAttribute(\"data-state\", active ? \"active\" : \"inactive\");\n slide.hidden = !active;\n });\n this.pickerTargets.forEach((picker, i) => {\n picker.setAttribute(\"aria-selected\", i === this.#index ? \"true\" : \"false\");\n });\n this.#roving.setActive(this.#index, { focus });\n }\n\n /**\n * Drives the autoplay interval toward the desired state. Autoplay should run\n * only when the user wants it (`playing`), the pointer is not hovering, and more\n * than one slide exists. Transitions emit `play`/`pause` and keep the toggle's\n * `aria-pressed` in sync.\n */\n #syncTimer(): void {\n // A non-looping carousel sitting on its last slide has nothing left to advance\n // to, so autoplay turns itself off (a hard stop, like the focus pause): the\n // timer is cleared, `aria-pressed` flips to false, and a manual step back will\n // not silently restart it without an explicit play.\n if (!this.loopValue && this.#index >= this.slideTargets.length - 1) {\n this.#playing = false;\n }\n const shouldRun = this.#playing && !this.#pointerPaused && this.slideTargets.length > 1;\n\n if (shouldRun && this.#timerId === null) {\n this.#timerId = this.#intervals.set(() => this.next(), this.intervalValue);\n this.dispatch(\"play\");\n } else if (!shouldRun && this.#timerId !== null) {\n this.#intervals.clear(this.#timerId);\n this.#timerId = null;\n this.dispatch(\"pause\");\n }\n\n if (this.hasPlayToggleTarget) {\n this.playToggleTarget.setAttribute(\"aria-pressed\", this.#playing ? \"true\" : \"false\");\n }\n }\n}\n"]}
@@ -276,8 +276,8 @@ var CollapsibleController = class extends Controller {
276
276
  }
277
277
  /**
278
278
  * Drives the disclosure to `open`, syncing the trigger's `aria-expanded` and
279
- * the content's `hidden` / `data-state` / height variable in the order the
280
- * spec requires.
279
+ * the content's `hidden` / `data-state` / height variable in an order where
280
+ * `hidden` never blocks measurement or the transition.
281
281
  *
282
282
  * @param open - Target state.
283
283
  * @param waitForCloseTransition - When `false` (initial `connect`) the close
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/collapsible_controller.ts"],"names":[],"mappings":"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","file":"collapsible_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 { TransitionCompletion } from \"../utils/transition_completion\";\n\n/**\n * Headless, accessible single-disclosure (collapsible) behavior.\n *\n * Markup contract (identifier: `stimeo--collapsible`):\n * <div data-controller=\"stimeo--collapsible\">\n * <button data-stimeo--collapsible-target=\"trigger\"\n * data-action=\"stimeo--collapsible#toggle\"\n * aria-expanded=\"false\" aria-controls=\"more\">Show details</button>\n * <div id=\"more\" data-stimeo--collapsible-target=\"content\"\n * data-state=\"closed\" hidden>…</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Disclosure** pattern for a single inline region.\n * Unlike {@link AccordionController} it manages exactly one trigger/content pair\n * with no sibling coordination, and unlike a dropdown the content expands in\n * flow and never closes on an outside click.\n *\n * @remarks\n * Behavior only — the consumer owns the height transition and all decoration.\n * The controller keeps the open lifecycle ordered so `hidden` (effectively\n * `display:none`) never blocks measurement or the transition:\n * - **Open**: drop `hidden` → measure the natural height into\n * `--stimeo-collapsible-content-height` → set `data-state=\"open\"`.\n * - **Close**: set `data-state=\"closed\"` (CSS shrinks the height) → re-apply\n * `hidden` once the transition settles. With no transition (or reduced motion,\n * which the consumer's CSS expresses as a zero duration) it is applied\n * immediately; an owned fallback covers missing terminal events.\n */\nexport class CollapsibleController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"content\"];\n static override values = {\n open: { type: Boolean, default: false },\n };\n static actions = [\"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly contentTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasContentTarget: boolean;\n\n declare openValue: boolean;\n\n /** Owns the cancellable close-transition wait and its bounded fallback. */\n readonly #transition = new TransitionCompletion();\n /** Distinguishes dynamic target churn from the callbacks that precede `connect()`. */\n #connected = false;\n\n /**\n * Establishes the initial open/closed state without waiting for a close transition.\n *\n * The DOM is the source of truth on reconnect (Turbo cache restore / morph): an\n * **explicit** state attribute — `aria-expanded=\"true\"`/`\"false\"` (or, with no\n * trigger, `data-state=\"open\"`/`\"closed\"`) — is honored verbatim so a region the\n * user opened *or* closed survives a back-navigation, even when the declarative\n * `open` Value disagrees. An already-open region remains open without a\n * close/reopen cycle. The Value only seeds a genuinely fresh render where no\n * state attribute is present yet; any opening animation in that case belongs to\n * the consumer's CSS. Mirrors `sidebar`'s `#restoreCollapsed`.\n */\n override connect(): void {\n this.#connected = true;\n this.#apply(this.#initialOpen(), false);\n }\n\n /** Resolves the connect-time state: explicit DOM state wins, else the `open` Value. */\n #initialOpen(): boolean {\n if (this.hasTriggerTarget) {\n const expanded = this.triggerTarget.getAttribute(\"aria-expanded\");\n if (expanded === \"true\") return true;\n if (expanded === \"false\") return false;\n } else if (this.hasContentTarget) {\n const state = this.contentTarget.getAttribute(\"data-state\");\n if (state === \"open\") return true;\n if (state === \"closed\") return false;\n }\n return this.openValue;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#transition.cancel();\n }\n\n /** Reconciles a replacement trigger target with the content's live state. */\n triggerTargetConnected(trigger: HTMLElement): void {\n if (!this.#connected || !this.hasContentTarget) return;\n trigger.setAttribute(\n \"aria-expanded\",\n this.contentTarget.getAttribute(\"data-state\") === \"open\" ? \"true\" : \"false\",\n );\n }\n\n /** Reconciles a replacement content target with the disclosure's live state. */\n contentTargetConnected(content: HTMLElement): void {\n if (!this.#connected) return;\n this.#transition.cancel();\n const open = this.hasTriggerTarget\n ? this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\"\n : content.getAttribute(\"data-state\") === \"open\";\n this.#applyContent(content, open, false);\n }\n\n /** Cancels a wait tied to a content target that was removed or replaced. */\n contentTargetDisconnected(): void {\n this.#transition.cancel();\n }\n\n /** Toggles the region open/closed. Bound via `data-action` (click). */\n toggle(): void {\n this.#apply(!this.#isOpen, true);\n }\n\n /**\n * Whether the region is logically open. Read from `aria-expanded` (not the\n * content's `hidden`) so a click *during* a close transition — when `hidden`\n * is still deferred — correctly reopens instead of re-closing.\n */\n get #isOpen(): boolean {\n if (this.hasTriggerTarget) {\n return this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n return this.hasContentTarget && this.contentTarget.getAttribute(\"data-state\") === \"open\";\n }\n\n /**\n * Drives the disclosure to `open`, syncing the trigger's `aria-expanded` and\n * the content's `hidden` / `data-state` / height variable in the order the\n * spec requires.\n *\n * @param open - Target state.\n * @param waitForCloseTransition - When `false` (initial `connect`) the close\n * path applies `hidden` immediately. This flag does not suppress consumer CSS\n * on the open path.\n */\n #apply(open: boolean, waitForCloseTransition: boolean): void {\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", open ? \"true\" : \"false\");\n }\n if (!this.hasContentTarget) return;\n\n const content = this.contentTarget;\n this.#transition.cancel();\n this.#applyContent(content, open, waitForCloseTransition);\n }\n\n /** Reflects one content target without relying on a later target lookup. */\n #applyContent(content: HTMLElement, open: boolean, waitForCloseTransition: boolean): void {\n if (open) {\n content.hidden = false;\n // Measure the natural height only once it is laid out (hidden removed) so\n // the consumer's `height` transition has a concrete target to animate to.\n content.style.setProperty(\"--stimeo-collapsible-content-height\", `${content.scrollHeight}px`);\n content.setAttribute(\"data-state\", \"open\");\n return;\n }\n\n content.setAttribute(\"data-state\", \"closed\");\n if (waitForCloseTransition) {\n this.#applyHiddenAfterTransition(content);\n } else {\n content.hidden = true;\n }\n }\n\n /**\n * Re-applies `hidden` once the close transition settles. Guarded against a\n * reopen mid-transition; the shared waiter also guarantees a bounded fallback\n * when the browser emits no terminal transition event.\n */\n #applyHiddenAfterTransition(content: HTMLElement): void {\n this.#transition.wait(content, () => {\n if (content.getAttribute(\"data-state\") === \"closed\") {\n content.hidden = true;\n }\n });\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/collapsible_controller.ts"],"names":[],"mappings":"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","file":"collapsible_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 { TransitionCompletion } from \"../utils/transition_completion\";\n\n/**\n * Headless, accessible single-disclosure (collapsible) behavior.\n *\n * Markup contract (identifier: `stimeo--collapsible`):\n * <div data-controller=\"stimeo--collapsible\">\n * <button data-stimeo--collapsible-target=\"trigger\"\n * data-action=\"stimeo--collapsible#toggle\"\n * aria-expanded=\"false\" aria-controls=\"more\">Show details</button>\n * <div id=\"more\" data-stimeo--collapsible-target=\"content\"\n * data-state=\"closed\" hidden>…</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Disclosure** pattern for a single inline region.\n * Unlike {@link AccordionController} it manages exactly one trigger/content pair\n * with no sibling coordination, and unlike a dropdown the content expands in\n * flow and never closes on an outside click.\n *\n * @remarks\n * Behavior only — the consumer owns the height transition and all decoration.\n * The controller keeps the open lifecycle ordered so `hidden` (effectively\n * `display:none`) never blocks measurement or the transition:\n * - **Open**: drop `hidden` → measure the natural height into\n * `--stimeo-collapsible-content-height` → set `data-state=\"open\"`.\n * - **Close**: set `data-state=\"closed\"` (CSS shrinks the height) → re-apply\n * `hidden` once the transition settles. With no transition (or reduced motion,\n * which the consumer's CSS expresses as a zero duration) it is applied\n * immediately; an owned fallback covers missing terminal events.\n */\nexport class CollapsibleController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"content\"];\n static override values = {\n open: { type: Boolean, default: false },\n };\n static actions = [\"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly contentTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasContentTarget: boolean;\n\n declare openValue: boolean;\n\n /** Owns the cancellable close-transition wait and its bounded fallback. */\n readonly #transition = new TransitionCompletion();\n /** Distinguishes dynamic target churn from the callbacks that precede `connect()`. */\n #connected = false;\n\n /**\n * Establishes the initial open/closed state without waiting for a close transition.\n *\n * The DOM is the source of truth on reconnect (Turbo cache restore / morph): an\n * **explicit** state attribute — `aria-expanded=\"true\"`/`\"false\"` (or, with no\n * trigger, `data-state=\"open\"`/`\"closed\"`) — is honored verbatim so a region the\n * user opened *or* closed survives a back-navigation, even when the declarative\n * `open` Value disagrees. An already-open region remains open without a\n * close/reopen cycle. The Value only seeds a genuinely fresh render where no\n * state attribute is present yet; any opening animation in that case belongs to\n * the consumer's CSS. Mirrors `sidebar`'s `#restoreCollapsed`.\n */\n override connect(): void {\n this.#connected = true;\n this.#apply(this.#initialOpen(), false);\n }\n\n /** Resolves the connect-time state: explicit DOM state wins, else the `open` Value. */\n #initialOpen(): boolean {\n if (this.hasTriggerTarget) {\n const expanded = this.triggerTarget.getAttribute(\"aria-expanded\");\n if (expanded === \"true\") return true;\n if (expanded === \"false\") return false;\n } else if (this.hasContentTarget) {\n const state = this.contentTarget.getAttribute(\"data-state\");\n if (state === \"open\") return true;\n if (state === \"closed\") return false;\n }\n return this.openValue;\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#transition.cancel();\n }\n\n /** Reconciles a replacement trigger target with the content's live state. */\n triggerTargetConnected(trigger: HTMLElement): void {\n if (!this.#connected || !this.hasContentTarget) return;\n trigger.setAttribute(\n \"aria-expanded\",\n this.contentTarget.getAttribute(\"data-state\") === \"open\" ? \"true\" : \"false\",\n );\n }\n\n /** Reconciles a replacement content target with the disclosure's live state. */\n contentTargetConnected(content: HTMLElement): void {\n if (!this.#connected) return;\n this.#transition.cancel();\n const open = this.hasTriggerTarget\n ? this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\"\n : content.getAttribute(\"data-state\") === \"open\";\n this.#applyContent(content, open, false);\n }\n\n /** Cancels a wait tied to a content target that was removed or replaced. */\n contentTargetDisconnected(): void {\n this.#transition.cancel();\n }\n\n /** Toggles the region open/closed. Bound via `data-action` (click). */\n toggle(): void {\n this.#apply(!this.#isOpen, true);\n }\n\n /**\n * Whether the region is logically open. Read from `aria-expanded` (not the\n * content's `hidden`) so a click *during* a close transition — when `hidden`\n * is still deferred — correctly reopens instead of re-closing.\n */\n get #isOpen(): boolean {\n if (this.hasTriggerTarget) {\n return this.triggerTarget.getAttribute(\"aria-expanded\") === \"true\";\n }\n return this.hasContentTarget && this.contentTarget.getAttribute(\"data-state\") === \"open\";\n }\n\n /**\n * Drives the disclosure to `open`, syncing the trigger's `aria-expanded` and\n * the content's `hidden` / `data-state` / height variable in an order where\n * `hidden` never blocks measurement or the transition.\n *\n * @param open - Target state.\n * @param waitForCloseTransition - When `false` (initial `connect`) the close\n * path applies `hidden` immediately. This flag does not suppress consumer CSS\n * on the open path.\n */\n #apply(open: boolean, waitForCloseTransition: boolean): void {\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", open ? \"true\" : \"false\");\n }\n if (!this.hasContentTarget) return;\n\n const content = this.contentTarget;\n this.#transition.cancel();\n this.#applyContent(content, open, waitForCloseTransition);\n }\n\n /** Reflects one content target without relying on a later target lookup. */\n #applyContent(content: HTMLElement, open: boolean, waitForCloseTransition: boolean): void {\n if (open) {\n content.hidden = false;\n // Measure the natural height only once it is laid out (hidden removed) so\n // the consumer's `height` transition has a concrete target to animate to.\n content.style.setProperty(\"--stimeo-collapsible-content-height\", `${content.scrollHeight}px`);\n content.setAttribute(\"data-state\", \"open\");\n return;\n }\n\n content.setAttribute(\"data-state\", \"closed\");\n if (waitForCloseTransition) {\n this.#applyHiddenAfterTransition(content);\n } else {\n content.hidden = true;\n }\n }\n\n /**\n * Re-applies `hidden` once the close transition settles. Guarded against a\n * reopen mid-transition; the shared waiter also guarantees a bounded fallback\n * when the browser emits no terminal transition event.\n */\n #applyHiddenAfterTransition(content: HTMLElement): void {\n this.#transition.wait(content, () => {\n if (content.getAttribute(\"data-state\") === \"closed\") {\n content.hidden = true;\n }\n });\n }\n}\n"]}
@@ -29,7 +29,13 @@ import { Controller } from '@hotwired/stimulus';
29
29
  *
30
30
  * @remarks
31
31
  * Behavior only — the swatch/gradient visuals are the consumer's CSS/canvas, fed
32
- * by `--stimeo-color`. Pointer-drag listeners on `document` are bound to an
32
+ * by `--stimeo-color`. Only the consumer knows whether a channel track mirrors
33
+ * under RTL: set `logicalTrack` to declare that it does, and the pointer mapping
34
+ * and horizontal arrow pair follow the writing direction. Left unset, nothing
35
+ * here reads `direction`. A gradient has no logical `to` keyword, so mirroring
36
+ * one means swapping `to right`/`to left` under a `:dir(rtl)` selector.
37
+ *
38
+ * Pointer-drag listeners on `document` are bound to an
33
39
  * {@link AbortController} and released on drag end and on `disconnect()` (Turbo
34
40
  * navigation included). Color is fully reconstructable from the `value` (hex), so
35
41
  * there is no transient state to restore after a Turbo cache/morph.
@@ -51,6 +57,10 @@ declare class ColorPickerController extends Controller<HTMLElement> {
51
57
  type: BooleanConstructor;
52
58
  default: boolean;
53
59
  };
60
+ logicalTrack: {
61
+ type: BooleanConstructor;
62
+ default: boolean;
63
+ };
54
64
  };
55
65
  static actions: readonly ["onHexInput", "onKeydown", "onPointerDown"];
56
66
  static events: readonly ["change"];
@@ -61,6 +71,7 @@ declare class ColorPickerController extends Controller<HTMLElement> {
61
71
  readonly fieldTargets: HTMLInputElement[];
62
72
  valueValue: string;
63
73
  alphaValue: boolean;
74
+ logicalTrackValue: boolean;
64
75
  /** Seeds the model from the initial hex value and renders every surface. */
65
76
  connect(): void;
66
77
  /** Cancels any active pointer drag so document listeners never leak. */
@@ -1,5 +1,30 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ // src/controllers/color_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
+
21
+ // src/utils/coerce.ts
22
+ function toFiniteNumber(raw) {
23
+ if (raw === null || raw === void 0 || raw === "") return null;
24
+ const value = typeof raw === "number" ? raw : Number(raw);
25
+ return Number.isFinite(value) ? value : null;
26
+ }
27
+
3
28
  // src/controllers/color_picker_controller.ts
4
29
  var COLOR_PROPERTY = "--stimeo-color";
5
30
  var CHANNEL_RANGE = {
@@ -12,10 +37,15 @@ var ColorPickerController = class extends Controller {
12
37
  static targets = ["slider", "hex", "preview", "field"];
13
38
  static values = {
14
39
  value: { type: String, default: "#000000" },
15
- alpha: { type: Boolean, default: false }
40
+ alpha: { type: Boolean, default: false },
41
+ logicalTrack: { type: Boolean, default: false }
16
42
  };
17
43
  static actions = ["onHexInput", "onKeydown", "onPointerDown"];
18
44
  static events = ["change"];
45
+ /** Whether the consumer declared a mirroring track and the direction mirrors it. */
46
+ get #mirrored() {
47
+ return this.logicalTrackValue && isRtl(this.element);
48
+ }
19
49
  /** The current color in the editing model. */
20
50
  #color = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };
21
51
  /** Aborts in-progress pointer-drag listeners on drag end / teardown. */
@@ -33,13 +63,14 @@ var ColorPickerController = class extends Controller {
33
63
  }
34
64
  /** Keyboard stepping on the focused channel slider (APG Slider model). */
35
65
  onKeydown(event) {
66
+ if (isReservedArrowChord(event)) return;
36
67
  const slider = event.currentTarget;
37
68
  const channel = this.#channelOf(slider);
38
69
  if (!channel) return;
39
70
  const [min, max] = this.#rangeOf(slider, channel);
40
71
  const value = this.#color[channel];
41
72
  let next = null;
42
- switch (event.key) {
73
+ switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {
43
74
  case "ArrowRight":
44
75
  case "ArrowUp":
45
76
  next = value + 1;
@@ -74,10 +105,12 @@ var ColorPickerController = class extends Controller {
74
105
  event.preventDefault();
75
106
  slider.focus();
76
107
  const [min, max] = this.#rangeOf(slider, channel);
108
+ const mirrored = this.#mirrored;
77
109
  const update = (clientX) => {
78
110
  const rect = slider.getBoundingClientRect();
79
111
  if (rect.width === 0) return;
80
- const fraction = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
112
+ const offset = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));
113
+ const fraction = mirrored ? 1 - offset : offset;
81
114
  this.#setChannel(channel, min + fraction * (max - min), min, max);
82
115
  };
83
116
  update(event.clientX);
@@ -140,12 +173,18 @@ var ColorPickerController = class extends Controller {
140
173
  const channel = slider.getAttribute("data-channel");
141
174
  return channel && channel in CHANNEL_RANGE ? channel : null;
142
175
  }
143
- /** A slider's `[min, max]` from aria-valuemin/max, falling back per channel. */
176
+ /**
177
+ * A slider's `[min, max]` from aria-valuemin/max, falling back per channel.
178
+ * An absent or blank attribute means "not authored", not zero — coercing it to
179
+ * a number would pin the channel at `0` and make the per-channel default
180
+ * unreachable.
181
+ */
144
182
  #rangeOf(slider, channel) {
145
183
  const [defMin, defMax] = CHANNEL_RANGE[channel];
146
- const min = Number(slider.getAttribute("aria-valuemin"));
147
- const max = Number(slider.getAttribute("aria-valuemax"));
148
- return [Number.isFinite(min) ? min : defMin, Number.isFinite(max) ? max : defMax];
184
+ return [
185
+ toFiniteNumber(slider.getAttribute("aria-valuemin")) ?? defMin,
186
+ toFiniteNumber(slider.getAttribute("aria-valuemax")) ?? defMax
187
+ ];
149
188
  }
150
189
  };
151
190
  function valueText(channel, value) {