stimeo-ui 0.2.0 → 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 (227) hide show
  1. package/CHANGELOG.md +163 -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.d.ts +3 -2
  5. package/dist/controllers/accordion_controller.js +10 -0
  6. package/dist/controllers/accordion_controller.js.map +1 -1
  7. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  8. package/dist/controllers/alert_dialog_controller.js +318 -0
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  10. package/dist/controllers/breadcrumb_controller.d.ts +97 -10
  11. package/dist/controllers/breadcrumb_controller.js +225 -13
  12. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.d.ts +11 -1
  14. package/dist/controllers/calendar_controller.js +89 -22
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.d.ts +115 -0
  17. package/dist/controllers/carousel_controller.js +313 -0
  18. package/dist/controllers/carousel_controller.js.map +1 -0
  19. package/dist/controllers/clipboard_controller.d.ts +72 -0
  20. package/dist/controllers/clipboard_controller.js +144 -0
  21. package/dist/controllers/clipboard_controller.js.map +1 -0
  22. package/dist/controllers/collapsible_controller.d.ts +70 -0
  23. package/dist/controllers/collapsible_controller.js +327 -0
  24. package/dist/controllers/collapsible_controller.js.map +1 -0
  25. package/dist/controllers/color_picker_controller.d.ts +87 -0
  26. package/dist/controllers/color_picker_controller.js +252 -0
  27. package/dist/controllers/color_picker_controller.js.map +1 -0
  28. package/dist/controllers/combobox_controller.d.ts +14 -1
  29. package/dist/controllers/combobox_controller.js +162 -23
  30. package/dist/controllers/combobox_controller.js.map +1 -1
  31. package/dist/controllers/command_palette_controller.d.ts +2 -0
  32. package/dist/controllers/command_palette_controller.js +194 -17
  33. package/dist/controllers/command_palette_controller.js.map +1 -1
  34. package/dist/controllers/confirm_controller.js.map +1 -1
  35. package/dist/controllers/context_menu_controller.d.ts +7 -6
  36. package/dist/controllers/context_menu_controller.js +32 -10
  37. package/dist/controllers/context_menu_controller.js.map +1 -1
  38. package/dist/controllers/count_up_controller.js +8 -1
  39. package/dist/controllers/count_up_controller.js.map +1 -1
  40. package/dist/controllers/currency_input_controller.d.ts +72 -0
  41. package/dist/controllers/currency_input_controller.js +147 -0
  42. package/dist/controllers/currency_input_controller.js.map +1 -0
  43. package/dist/controllers/data_grid_controller.d.ts +89 -0
  44. package/dist/controllers/data_grid_controller.js +246 -0
  45. package/dist/controllers/data_grid_controller.js.map +1 -0
  46. package/dist/controllers/date_range_picker_controller.d.ts +88 -0
  47. package/dist/controllers/date_range_picker_controller.js +441 -0
  48. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  49. package/dist/controllers/dialog_controller.js.map +1 -1
  50. package/dist/controllers/dismissible_controller.d.ts +57 -0
  51. package/dist/controllers/dismissible_controller.js +117 -0
  52. package/dist/controllers/dismissible_controller.js.map +1 -0
  53. package/dist/controllers/drawer_controller.d.ts +96 -0
  54. package/dist/controllers/drawer_controller.js +630 -0
  55. package/dist/controllers/drawer_controller.js.map +1 -0
  56. package/dist/controllers/dropdown_controller.d.ts +1 -2
  57. package/dist/controllers/dropdown_controller.js.map +1 -1
  58. package/dist/controllers/editable_controller.d.ts +73 -0
  59. package/dist/controllers/editable_controller.js +169 -0
  60. package/dist/controllers/editable_controller.js.map +1 -0
  61. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  62. package/dist/controllers/file_dropzone_controller.js +165 -0
  63. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  64. package/dist/controllers/filter_controller.d.ts +60 -0
  65. package/dist/controllers/filter_controller.js +86 -0
  66. package/dist/controllers/filter_controller.js.map +1 -0
  67. package/dist/controllers/flash_controller.js +36 -5
  68. package/dist/controllers/flash_controller.js.map +1 -1
  69. package/dist/controllers/focus_controller.d.ts +1 -1
  70. package/dist/controllers/focus_controller.js.map +1 -1
  71. package/dist/controllers/form_field_controller.js.map +1 -1
  72. package/dist/controllers/form_validation_controller.js +1 -1
  73. package/dist/controllers/form_validation_controller.js.map +1 -1
  74. package/dist/controllers/highlight_controller.js +6 -4
  75. package/dist/controllers/highlight_controller.js.map +1 -1
  76. package/dist/controllers/hover_card_controller.d.ts +1 -1
  77. package/dist/controllers/hover_card_controller.js.map +1 -1
  78. package/dist/controllers/intersection_controller.js +67 -19
  79. package/dist/controllers/intersection_controller.js.map +1 -1
  80. package/dist/controllers/lazy_frame_controller.js +54 -11
  81. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  82. package/dist/controllers/listbox_controller.d.ts +9 -2
  83. package/dist/controllers/listbox_controller.js +257 -53
  84. package/dist/controllers/listbox_controller.js.map +1 -1
  85. package/dist/controllers/local_time_controller.js +2 -2
  86. package/dist/controllers/local_time_controller.js.map +1 -1
  87. package/dist/controllers/masonry_controller.d.ts +54 -0
  88. package/dist/controllers/masonry_controller.js +142 -0
  89. package/dist/controllers/masonry_controller.js.map +1 -0
  90. package/dist/controllers/menu_controller.d.ts +29 -11
  91. package/dist/controllers/menu_controller.js +104 -17
  92. package/dist/controllers/menu_controller.js.map +1 -1
  93. package/dist/controllers/menubar_controller.d.ts +142 -0
  94. package/dist/controllers/menubar_controller.js +785 -0
  95. package/dist/controllers/menubar_controller.js.map +1 -0
  96. package/dist/controllers/meter_controller.js.map +1 -1
  97. package/dist/controllers/multi_select_controller.d.ts +141 -0
  98. package/dist/controllers/multi_select_controller.js +755 -0
  99. package/dist/controllers/multi_select_controller.js.map +1 -0
  100. package/dist/controllers/navigation_menu_controller.d.ts +146 -0
  101. package/dist/controllers/navigation_menu_controller.js +511 -0
  102. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  103. package/dist/controllers/nested_form_controller.d.ts +4 -5
  104. package/dist/controllers/nested_form_controller.js.map +1 -1
  105. package/dist/controllers/number_input_controller.d.ts +1 -1
  106. package/dist/controllers/number_input_controller.js +7 -0
  107. package/dist/controllers/number_input_controller.js.map +1 -1
  108. package/dist/controllers/otp_controller.js +18 -1
  109. package/dist/controllers/otp_controller.js.map +1 -1
  110. package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
  111. package/dist/controllers/overflow_indicator_controller.js +246 -27
  112. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  113. package/dist/controllers/overflow_menu_controller.d.ts +78 -19
  114. package/dist/controllers/overflow_menu_controller.js +381 -57
  115. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  116. package/dist/controllers/pagination_controller.d.ts +36 -5
  117. package/dist/controllers/pagination_controller.js +163 -32
  118. package/dist/controllers/pagination_controller.js.map +1 -1
  119. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  120. package/dist/controllers/password_reveal_controller.js +117 -0
  121. package/dist/controllers/password_reveal_controller.js.map +1 -0
  122. package/dist/controllers/persist_controller.js +6 -6
  123. package/dist/controllers/persist_controller.js.map +1 -1
  124. package/dist/controllers/pointer_drag_controller.d.ts +2 -3
  125. package/dist/controllers/pointer_drag_controller.js +9 -1
  126. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  127. package/dist/controllers/popover_controller.d.ts +1 -2
  128. package/dist/controllers/popover_controller.js +2 -2
  129. package/dist/controllers/popover_controller.js.map +1 -1
  130. package/dist/controllers/portal_controller.d.ts +4 -4
  131. package/dist/controllers/portal_controller.js.map +1 -1
  132. package/dist/controllers/progress_controller.js.map +1 -1
  133. package/dist/controllers/radio_group_controller.js +22 -3
  134. package/dist/controllers/radio_group_controller.js.map +1 -1
  135. package/dist/controllers/range_slider_controller.d.ts +97 -0
  136. package/dist/controllers/range_slider_controller.js +192 -0
  137. package/dist/controllers/range_slider_controller.js.map +1 -0
  138. package/dist/controllers/rating_controller.js +16 -2
  139. package/dist/controllers/rating_controller.js.map +1 -1
  140. package/dist/controllers/read_more_controller.d.ts +52 -0
  141. package/dist/controllers/read_more_controller.js +238 -0
  142. package/dist/controllers/read_more_controller.js.map +1 -0
  143. package/dist/controllers/reading_progress_controller.js.map +1 -1
  144. package/dist/controllers/resizable_controller.d.ts +3 -1
  145. package/dist/controllers/resizable_controller.js +65 -1
  146. package/dist/controllers/resizable_controller.js.map +1 -1
  147. package/dist/controllers/roving_controller.js +17 -2
  148. package/dist/controllers/roving_controller.js.map +1 -1
  149. package/dist/controllers/scroll_area_controller.js +101 -14
  150. package/dist/controllers/scroll_area_controller.js.map +1 -1
  151. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  152. package/dist/controllers/scroll_restore_controller.js +93 -0
  153. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  154. package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
  155. package/dist/controllers/scroll_visibility_controller.js +40 -6
  156. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  157. package/dist/controllers/scrollspy_controller.d.ts +64 -15
  158. package/dist/controllers/scrollspy_controller.js +369 -74
  159. package/dist/controllers/scrollspy_controller.js.map +1 -1
  160. package/dist/controllers/separator_controller.d.ts +69 -0
  161. package/dist/controllers/separator_controller.js +96 -0
  162. package/dist/controllers/separator_controller.js.map +1 -0
  163. package/dist/controllers/sidebar_controller.d.ts +107 -0
  164. package/dist/controllers/sidebar_controller.js +761 -0
  165. package/dist/controllers/sidebar_controller.js.map +1 -0
  166. package/dist/controllers/skeleton_controller.js +1 -1
  167. package/dist/controllers/skeleton_controller.js.map +1 -1
  168. package/dist/controllers/slider_controller.d.ts +11 -1
  169. package/dist/controllers/slider_controller.js +32 -6
  170. package/dist/controllers/slider_controller.js.map +1 -1
  171. package/dist/controllers/sortable_controller.d.ts +3 -1
  172. package/dist/controllers/sortable_controller.js +34 -3
  173. package/dist/controllers/sortable_controller.js.map +1 -1
  174. package/dist/controllers/spinner_controller.js +1 -1
  175. package/dist/controllers/spinner_controller.js.map +1 -1
  176. package/dist/controllers/stepper_controller.d.ts +4 -0
  177. package/dist/controllers/stepper_controller.js +28 -12
  178. package/dist/controllers/stepper_controller.js.map +1 -1
  179. package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
  180. package/dist/controllers/stick_to_bottom_controller.js +9 -4
  181. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  182. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  183. package/dist/controllers/sticky_observer_controller.js +109 -20
  184. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  185. package/dist/controllers/submit_once_controller.d.ts +2 -2
  186. package/dist/controllers/submit_once_controller.js.map +1 -1
  187. package/dist/controllers/switch_controller.js +1 -0
  188. package/dist/controllers/switch_controller.js.map +1 -1
  189. package/dist/controllers/tabs_controller.d.ts +13 -4
  190. package/dist/controllers/tabs_controller.js +26 -3
  191. package/dist/controllers/tabs_controller.js.map +1 -1
  192. package/dist/controllers/tags_input_controller.d.ts +91 -0
  193. package/dist/controllers/tags_input_controller.js +295 -0
  194. package/dist/controllers/tags_input_controller.js.map +1 -0
  195. package/dist/controllers/theme_controller.d.ts +1 -1
  196. package/dist/controllers/theme_controller.js +42 -13
  197. package/dist/controllers/theme_controller.js.map +1 -1
  198. package/dist/controllers/time_picker_controller.d.ts +67 -0
  199. package/dist/controllers/time_picker_controller.js +231 -0
  200. package/dist/controllers/time_picker_controller.js.map +1 -0
  201. package/dist/controllers/toast_controller.d.ts +5 -14
  202. package/dist/controllers/toast_controller.js +40 -14
  203. package/dist/controllers/toast_controller.js.map +1 -1
  204. package/dist/controllers/toggle_group_controller.js +23 -2
  205. package/dist/controllers/toggle_group_controller.js.map +1 -1
  206. package/dist/controllers/toolbar_controller.d.ts +53 -13
  207. package/dist/controllers/toolbar_controller.js +230 -31
  208. package/dist/controllers/toolbar_controller.js.map +1 -1
  209. package/dist/controllers/tooltip_controller.d.ts +1 -1
  210. package/dist/controllers/tooltip_controller.js.map +1 -1
  211. package/dist/controllers/transition_controller.d.ts +14 -8
  212. package/dist/controllers/transition_controller.js +153 -38
  213. package/dist/controllers/transition_controller.js.map +1 -1
  214. package/dist/controllers/tree_view_controller.d.ts +135 -0
  215. package/dist/controllers/tree_view_controller.js +691 -0
  216. package/dist/controllers/tree_view_controller.js.map +1 -0
  217. package/dist/index.d.ts +27 -1818
  218. package/dist/index.js +4256 -915
  219. package/dist/index.js.map +1 -1
  220. package/dist/inspector/cli.d.ts +375 -39
  221. package/dist/inspector/cli.js +228 -14
  222. package/dist/inspector/cli.js.map +1 -1
  223. package/dist/inspector/cli_bin.js +419 -48
  224. package/dist/inspector/cli_bin.js.map +1 -1
  225. package/dist/inspector/examples.json +26 -26
  226. package/dist/inspector/manifest.json +1054 -127
  227. package/package.json +5 -6
@@ -0,0 +1,785 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/menubar_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 isReservedArrowChord(event, allow = []) {
12
+ if (!event.key.startsWith("Arrow")) return false;
13
+ return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
14
+ }
15
+
16
+ // src/utils/escape_layer.ts
17
+ function claimsWhileFocusWithin(element) {
18
+ return () => {
19
+ const active = element.ownerDocument.activeElement;
20
+ return active === null || active === element.ownerDocument.body || element.contains(active);
21
+ };
22
+ }
23
+ var EscapeLayer = class _EscapeLayer {
24
+ static #registries = /* @__PURE__ */ new WeakMap();
25
+ #ownerDocument = null;
26
+ /** Dismissal callback while active; `null` when inactive. */
27
+ #onDismiss = null;
28
+ /** Live predicate deciding whether the layer claims a press; `null` = always. */
29
+ #claims = null;
30
+ /**
31
+ * Activates this layer at the top of its document's Escape stack, installing
32
+ * the document's shared resolver listener if this is its first layer.
33
+ * Re-activating an already-active layer moves it to the top.
34
+ */
35
+ activate(ownerDocument = document, options) {
36
+ this.deactivate();
37
+ let registry = _EscapeLayer.#registries.get(ownerDocument);
38
+ if (!registry) {
39
+ registry = _EscapeLayer.#createRegistry();
40
+ _EscapeLayer.#registries.set(ownerDocument, registry);
41
+ ownerDocument.addEventListener("keydown", registry.onKeydown);
42
+ }
43
+ registry.stack.push(this);
44
+ this.#ownerDocument = ownerDocument;
45
+ this.#onDismiss = options.onDismiss;
46
+ this.#claims = options.claims ?? null;
47
+ }
48
+ /**
49
+ * Removes this layer from its document's Escape stack, uninstalling the
50
+ * shared listener when the stack empties. Safe to call when inactive.
51
+ */
52
+ deactivate() {
53
+ const ownerDocument = this.#ownerDocument;
54
+ if (!ownerDocument) return;
55
+ const registry = _EscapeLayer.#registries.get(ownerDocument);
56
+ if (registry) {
57
+ const index = registry.stack.lastIndexOf(this);
58
+ if (index >= 0) registry.stack.splice(index, 1);
59
+ if (registry.stack.length === 0) {
60
+ ownerDocument.removeEventListener("keydown", registry.onKeydown);
61
+ _EscapeLayer.#registries.delete(ownerDocument);
62
+ }
63
+ }
64
+ this.#ownerDocument = null;
65
+ this.#onDismiss = null;
66
+ this.#claims = null;
67
+ }
68
+ /**
69
+ * Whether this active layer would own a press right now: it is the topmost
70
+ * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
71
+ * and diagnostics — production dismissal goes through the shared listener.
72
+ */
73
+ get ownsEscape() {
74
+ const ownerDocument = this.#ownerDocument;
75
+ if (!ownerDocument) return false;
76
+ const registry = _EscapeLayer.#registries.get(ownerDocument);
77
+ if (!registry) return false;
78
+ return _EscapeLayer.#resolveOwner(registry.stack) === this;
79
+ }
80
+ /** Builds a document's registry with its shared resolver listener. */
81
+ static #createRegistry() {
82
+ const registry = {
83
+ stack: [],
84
+ onKeydown: (event) => {
85
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
86
+ const owner = _EscapeLayer.#resolveOwner(registry.stack);
87
+ if (!owner) return;
88
+ event.preventDefault();
89
+ owner.#onDismiss?.();
90
+ }
91
+ };
92
+ return registry;
93
+ }
94
+ /** The topmost stack layer whose claims predicate passes, or `null`. */
95
+ static #resolveOwner(stack) {
96
+ for (let index = stack.length - 1; index >= 0; index--) {
97
+ const layer = stack[index];
98
+ if (!layer) continue;
99
+ if (layer.#claims && !layer.#claims()) continue;
100
+ return layer;
101
+ }
102
+ return null;
103
+ }
104
+ };
105
+
106
+ // src/utils/roving_tabindex.ts
107
+ var RovingTabindex = class {
108
+ /** Returns the current ordered item elements; called on every operation. */
109
+ #getItems;
110
+ /**
111
+ * @param getItems - Returns the current ordered item elements. Called on every
112
+ * operation so the live target list is always used.
113
+ */
114
+ constructor(getItems) {
115
+ this.#getItems = getItems;
116
+ }
117
+ /** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
118
+ get activeIndex() {
119
+ return this.#getItems().findIndex((item) => item.tabIndex === 0);
120
+ }
121
+ /**
122
+ * Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
123
+ * other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
124
+ * (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
125
+ * "nothing is currently tabbable".
126
+ *
127
+ * @param index - Position of the item to make tabbable.
128
+ * @param options - Pass `{ focus: true }` to also move DOM focus to that item.
129
+ */
130
+ setActive(index, { focus = false } = {}) {
131
+ const items = this.#getItems();
132
+ items.forEach((item, i) => {
133
+ item.tabIndex = i === index ? 0 : -1;
134
+ });
135
+ if (focus) items[index]?.focus();
136
+ }
137
+ };
138
+
139
+ // src/utils/safe_timeout.ts
140
+ var TimerRegistry = class {
141
+ /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
142
+ ids = /* @__PURE__ */ new Set();
143
+ /**
144
+ * Cancels a single tracked timer.
145
+ *
146
+ * No-ops if the id is unknown (already cleared, fired, or never owned by this
147
+ * registry), so callers can clear defensively without guarding.
148
+ */
149
+ clear(id) {
150
+ if (this.ids.delete(id)) {
151
+ this.cancel(id);
152
+ }
153
+ }
154
+ /**
155
+ * Cancels every tracked timer. Call this from a controller's `disconnect()`
156
+ * to guarantee no timer outlives the element.
157
+ */
158
+ clearAll() {
159
+ for (const id of this.ids) {
160
+ this.cancel(id);
161
+ }
162
+ this.ids.clear();
163
+ }
164
+ /** Number of timers currently tracked (pending). */
165
+ get size() {
166
+ return this.ids.size;
167
+ }
168
+ };
169
+ var SafeTimeout = class extends TimerRegistry {
170
+ /**
171
+ * Schedules `callback` after `delay` ms and returns the timer id.
172
+ *
173
+ * The id is removed from the registry automatically when the timeout fires,
174
+ * so {@link TimerRegistry.size | size} reflects only still-pending timers.
175
+ */
176
+ set(callback, delay) {
177
+ const id = this.schedule(() => {
178
+ this.ids.delete(id);
179
+ callback();
180
+ }, delay);
181
+ this.ids.add(id);
182
+ return id;
183
+ }
184
+ schedule(callback, delay) {
185
+ return window.setTimeout(callback, delay);
186
+ }
187
+ cancel(id) {
188
+ window.clearTimeout(id);
189
+ }
190
+ };
191
+
192
+ // src/utils/typeahead.ts
193
+ var TYPEAHEAD_RESET_MS = 500;
194
+ var Typeahead = class {
195
+ /** Timer registry for the pending idle reset; private so `reset()` is the only exit. */
196
+ #timers = new SafeTimeout();
197
+ /** Idle window before the query resets, in milliseconds. */
198
+ #resetMs;
199
+ /** The accumulated lowercase query, empty when idle. */
200
+ #query = "";
201
+ /** Id of the pending reset timer, `0` when none is scheduled. */
202
+ #timerId = 0;
203
+ /** @param options - Overrides for the idle window. */
204
+ constructor({ resetMs = TYPEAHEAD_RESET_MS } = {}) {
205
+ this.#resetMs = resetMs;
206
+ }
207
+ /** The query a search would currently run with; empty while idle. */
208
+ get query() {
209
+ return this.#query;
210
+ }
211
+ /**
212
+ * Folds `key` into the query, restarts the idle window, and returns the query to
213
+ * search with. A repeated character collapses the query to that one character.
214
+ */
215
+ push(key) {
216
+ const char = key.toLowerCase();
217
+ const repeated = this.#query.length > 0 && [...this.#query].every((c) => c === char);
218
+ this.#query = repeated ? char : this.#query + char;
219
+ this.#timers.clear(this.#timerId);
220
+ this.#timerId = this.#timers.set(() => this.reset(), this.#resetMs);
221
+ return this.#query;
222
+ }
223
+ /** Clears the query and cancels the pending idle reset. */
224
+ reset() {
225
+ this.#query = "";
226
+ this.#timers.clear(this.#timerId);
227
+ this.#timerId = 0;
228
+ }
229
+ };
230
+ function isTypeaheadKey(event) {
231
+ return event.key.length === 1 && event.key !== " " && !event.ctrlKey && !event.metaKey && !event.altKey && !event.isComposing;
232
+ }
233
+ function typeaheadLabel(element, fallbackText) {
234
+ const label = element.getAttribute("aria-label")?.trim();
235
+ if (label) return label.toLowerCase();
236
+ const text = element.textContent ?? "";
237
+ return text.trim().toLowerCase();
238
+ }
239
+ function findTypeaheadMatch(items, from, query, label = (item) => typeaheadLabel(item)) {
240
+ if (query === "") return -1;
241
+ const count = items.length;
242
+ for (let step = 1; step <= count; step += 1) {
243
+ const index = ((from + step) % count + count) % count;
244
+ const candidate = items[index];
245
+ if (candidate && label(candidate).startsWith(query)) return index;
246
+ }
247
+ return -1;
248
+ }
249
+
250
+ // src/controllers/menubar_controller.ts
251
+ var STATE_ATTRIBUTES = ["disabled", "hidden"];
252
+ var MenubarController = class extends Controller {
253
+ static targets = ["top", "menu", "item"];
254
+ static actions = ["activate", "onItemKeydown", "onTopKeydown", "toggle"];
255
+ /** Roving tabindex across the top-level menuitems (one Tab stop). */
256
+ #roving = new RovingTabindex(() => this.topTargets);
257
+ /** Typeahead query and its idle-reset timer (scoped to the open menu). */
258
+ #typeahead = new Typeahead();
259
+ #timers = new SafeTimeout();
260
+ /** Escape-stack membership while a menu is open; the shared resolver dismisses via it. */
261
+ #escapeLayer = new EscapeLayer();
262
+ /** Whether {@link #escapeLayer} is currently on the stack (see {@link #syncEscapeLayer}). */
263
+ #layerActive = false;
264
+ /** Watches `disabled`/`hidden` under the menubar; `null` while disconnected. */
265
+ #observer = null;
266
+ /**
267
+ * Live between `connect()` and `disconnect()`. Stimulus reports the *initial*
268
+ * targets before `connect()` and re-reports them as disconnected afterwards;
269
+ * gating on this keeps the target callbacks from clobbering the authored Tab
270
+ * stop on mount and from resurrecting one after teardown.
271
+ */
272
+ #connected = false;
273
+ /** The element inside the menubar that last took DOM focus, if any. */
274
+ #focused = null;
275
+ /**
276
+ * Records where DOM focus sits inside the menubar. `focusin` bubbles, so one
277
+ * listener covers the top items and everything nested in their menus. The
278
+ * record is needed because a top or menu becomes unreachable *before* this
279
+ * controller hears about it, by which point the browser has already reset
280
+ * `document.activeElement`.
281
+ */
282
+ #onFocusIn = (event) => {
283
+ this.#focused = event.target;
284
+ };
285
+ /**
286
+ * Forgets the record once focus genuinely lands on something else, so a later
287
+ * mutation cannot pull focus back in. A `relatedTarget` of `null` is the case
288
+ * that must be kept: it is what "focus went nowhere" looks like, which is
289
+ * exactly the state the rescue exists for.
290
+ */
291
+ #onFocusOut = (event) => {
292
+ const next = event.relatedTarget;
293
+ if (next && !this.element.contains(next)) this.#focused = null;
294
+ };
295
+ /**
296
+ * Establishes the closed baseline and the single tab stop: keep an existing tab
297
+ * stop when it is still navigable (so a Turbo cache restore preserves the user's
298
+ * position), else fall back to the first navigable top item.
299
+ *
300
+ * The outside-click listener is registered in the **capture** phase: in the
301
+ * bubble phase an inside handler that removes its own click target detaches the
302
+ * node before `contains()` runs, so an *inside* click would read as outside and
303
+ * close the menu.
304
+ */
305
+ connect() {
306
+ this.#closeAllMenus();
307
+ this.#reconcile();
308
+ this.element.addEventListener("click", this.#onDisabledClickCapture, true);
309
+ this.element.addEventListener("focusin", this.#onFocusIn);
310
+ this.element.addEventListener("focusout", this.#onFocusOut);
311
+ document.addEventListener("click", this.#onOutsideClick, true);
312
+ if (typeof MutationObserver !== "undefined") {
313
+ this.#observer = new MutationObserver(() => this.#reconcile());
314
+ this.#observer.observe(this.element, {
315
+ subtree: true,
316
+ childList: true,
317
+ attributes: true,
318
+ attributeFilter: STATE_ATTRIBUTES
319
+ });
320
+ }
321
+ this.#connected = true;
322
+ }
323
+ /** Removes the listeners, stack membership, and any pending timer (typeahead / Tab close). */
324
+ disconnect() {
325
+ this.#connected = false;
326
+ this.#escapeLayer.deactivate();
327
+ this.#layerActive = false;
328
+ this.element.removeEventListener("click", this.#onDisabledClickCapture, true);
329
+ this.element.removeEventListener("focusin", this.#onFocusIn);
330
+ this.element.removeEventListener("focusout", this.#onFocusOut);
331
+ document.removeEventListener("click", this.#onOutsideClick, true);
332
+ this.#focused = null;
333
+ this.#observer?.disconnect();
334
+ this.#observer = null;
335
+ this.#typeahead.reset();
336
+ this.#timers.clearAll();
337
+ }
338
+ /**
339
+ * A top item added at runtime is dropped out of the Tab sequence first — a fresh
340
+ * `<button>` is tabbable by default, which would leave the menubar with two Tab
341
+ * stops — before the lone stop is re-established.
342
+ */
343
+ topTargetConnected(top) {
344
+ if (!this.#connected) return;
345
+ top.tabIndex = -1;
346
+ this.#reconcile();
347
+ }
348
+ /**
349
+ * Removing a top item can strand two things: the Tab stop it held, and the menu
350
+ * it owned (which nothing could close afterwards).
351
+ *
352
+ * The departing element is handed back in a neutral state first. A target can
353
+ * leave without leaving the document — a morph that only drops the
354
+ * `data-*-target` token keeps the node — and it would then sit in the page
355
+ * carrying this controller's `tabindex="0"` (a second Tab stop next to the one
356
+ * re-established below) and an `aria-expanded="true"` nothing can collapse.
357
+ * When the node really is gone these writes are harmless no-ops.
358
+ */
359
+ topTargetDisconnected(top) {
360
+ if (!this.#connected) return;
361
+ top.tabIndex = -1;
362
+ if (this.#isExpanded(top)) top.setAttribute("aria-expanded", "false");
363
+ this.#reconcile();
364
+ }
365
+ /** See {@link MenubarController.menuTargetDisconnected}. */
366
+ menuTargetConnected() {
367
+ if (!this.#connected) return;
368
+ this.#reconcile();
369
+ }
370
+ /**
371
+ * A menu removed while open leaves its owner claiming `aria-expanded="true"` for
372
+ * a popup that no longer exists, and the menubar holding an Escape layer that
373
+ * would swallow presses meant for something else.
374
+ *
375
+ * As with {@link MenubarController.topTargetDisconnected}, the departing menu is
376
+ * closed first: a token-only removal leaves a visible popup in the page that no
377
+ * key and no click can dismiss any more.
378
+ */
379
+ menuTargetDisconnected(menu) {
380
+ if (!this.#connected) return;
381
+ if (!menu.hidden) menu.hidden = true;
382
+ this.#reconcile();
383
+ }
384
+ /** Toggles a top item's menu. Bound via `data-action` (click on the top item). */
385
+ toggle(event) {
386
+ const top = event.currentTarget;
387
+ if (this.#isExpanded(top)) {
388
+ this.#closeMenu(top);
389
+ } else {
390
+ this.#openMenu(top, "first");
391
+ }
392
+ }
393
+ /** Keyboard handling while focus is on a top item. */
394
+ onTopKeydown(event) {
395
+ if (event.defaultPrevented) return;
396
+ if (isReservedArrowChord(event)) return;
397
+ const tops = this.#navigableTops;
398
+ const index = tops.indexOf(event.currentTarget);
399
+ if (index === -1) return;
400
+ const length = tops.length;
401
+ const anyOpen = this.#isAnyOpen;
402
+ const step = isRtl(this.element) ? -1 : 1;
403
+ switch (event.key) {
404
+ case "ArrowRight":
405
+ event.preventDefault();
406
+ this.#gotoTop(tops[(index + step + length) % length], anyOpen);
407
+ break;
408
+ case "ArrowLeft":
409
+ event.preventDefault();
410
+ this.#gotoTop(tops[(index - step + length) % length], anyOpen);
411
+ break;
412
+ case "ArrowDown":
413
+ event.preventDefault();
414
+ this.#openMenu(tops[index], "first");
415
+ break;
416
+ case "ArrowUp":
417
+ event.preventDefault();
418
+ this.#openMenu(tops[index], "last");
419
+ break;
420
+ case "Home":
421
+ event.preventDefault();
422
+ this.#gotoTop(tops[0], anyOpen);
423
+ break;
424
+ case "End":
425
+ event.preventDefault();
426
+ this.#gotoTop(tops[length - 1], anyOpen);
427
+ break;
428
+ case "Tab":
429
+ this.#closeMenusSoon();
430
+ break;
431
+ }
432
+ }
433
+ /** Keyboard handling while focus is on a menu item. */
434
+ onItemKeydown(event) {
435
+ if (event.defaultPrevented) return;
436
+ if (isReservedArrowChord(event)) return;
437
+ const item = event.currentTarget;
438
+ const menu = item.closest("[role='menu']");
439
+ if (!menu) return;
440
+ const items = this.#itemsIn(menu);
441
+ const index = items.indexOf(item);
442
+ const length = items.length;
443
+ switch (event.key) {
444
+ case "ArrowDown":
445
+ event.preventDefault();
446
+ this.#focusAt(items, (index + 1) % length);
447
+ break;
448
+ case "ArrowUp":
449
+ event.preventDefault();
450
+ this.#focusAt(items, (index - 1 + length) % length);
451
+ break;
452
+ case "Home":
453
+ event.preventDefault();
454
+ this.#focusAt(items, 0);
455
+ break;
456
+ case "End":
457
+ event.preventDefault();
458
+ this.#focusAt(items, length - 1);
459
+ break;
460
+ case "ArrowRight":
461
+ event.preventDefault();
462
+ this.#moveToAdjacentMenu(menu, isRtl(this.element) ? -1 : 1);
463
+ break;
464
+ case "ArrowLeft":
465
+ event.preventDefault();
466
+ this.#moveToAdjacentMenu(menu, isRtl(this.element) ? 1 : -1);
467
+ break;
468
+ case "Tab":
469
+ this.#closeMenusSoon();
470
+ break;
471
+ default:
472
+ if (isTypeaheadKey(event)) {
473
+ event.preventDefault();
474
+ this.#typeaheadTo(items, index, event.key);
475
+ }
476
+ break;
477
+ }
478
+ }
479
+ /** Closes the owning menu after an item is activated and refocuses its top. */
480
+ activate(event) {
481
+ const item = event.currentTarget;
482
+ const menu = item.closest("[role='menu']");
483
+ const top = this.#topFor(menu);
484
+ this.#closeAllMenus();
485
+ this.#focusTop(top);
486
+ }
487
+ /** Moves the roving focus to `top`, opening its menu when one was open. */
488
+ #gotoTop(top, reopen) {
489
+ if (!top) return;
490
+ if (reopen) {
491
+ this.#openMenu(top, "first");
492
+ } else {
493
+ this.#roving.setActive(this.topTargets.indexOf(top), { focus: true });
494
+ }
495
+ }
496
+ /**
497
+ * Opens `top`'s menu (closing others) and focuses its first/last item.
498
+ *
499
+ * Three kinds of top item never open a menu:
500
+ * - **`aria-disabled`** — focusable but never activated, and opening a popup is
501
+ * activation.
502
+ * - **plain command** (no `aria-controls` at all) — a legitimate top item that
503
+ * simply has no popup.
504
+ * - **dangling `aria-controls`** (names a menu that is not a target) — broken
505
+ * markup.
506
+ *
507
+ * The first two still take the roving focus, and any open menu closes: the APG
508
+ * makes closing unconditional for the horizontal move ("closes the submenu,
509
+ * moves focus to the next menubar item, and *optionally* opens that item's
510
+ * submenu"), so only the opening half is skipped. Leaving the old menu open
511
+ * would strand a popup that no longer contains focus.
512
+ *
513
+ * The dangling case instead leaves the open/closed state and focus untouched
514
+ * rather than closing everything and dropping focus to the body — but only on
515
+ * this "open a menu" path. A dangling top is still an ordinary roving
516
+ * destination while nothing is open.
517
+ */
518
+ #openMenu(top, focus) {
519
+ if (!top) return;
520
+ const menu = this.#menuFor(top);
521
+ if (!menu || this.#isActivationBlocked(top)) {
522
+ if (menu || !top.hasAttribute("aria-controls")) {
523
+ this.#closeAllMenus();
524
+ this.#focusTop(top);
525
+ }
526
+ return;
527
+ }
528
+ this.#timers.clearAll();
529
+ this.#closeAllMenus();
530
+ menu.hidden = false;
531
+ top.setAttribute("aria-expanded", "true");
532
+ this.#escapeLayer.activate(document, {
533
+ onDismiss: () => this.#dismissOpenMenu(),
534
+ claims: claimsWhileFocusWithin(this.element)
535
+ });
536
+ this.#layerActive = true;
537
+ this.#roving.setActive(this.topTargets.indexOf(top));
538
+ const items = this.#itemsIn(menu);
539
+ this.#focusAt(items, focus === "first" ? 0 : items.length - 1);
540
+ }
541
+ /**
542
+ * Hides `top`'s menu and reflects the collapsed state. A top item that controls
543
+ * no menu (a plain command in an otherwise popup-bearing menubar) is left alone
544
+ * — stamping `aria-expanded="false"` on it would announce a popup it lacks.
545
+ */
546
+ #closeMenu(top) {
547
+ if (!top) return;
548
+ const menu = this.#menuFor(top);
549
+ if (!menu) return;
550
+ menu.hidden = true;
551
+ top.setAttribute("aria-expanded", "false");
552
+ this.#syncEscapeLayer();
553
+ }
554
+ /** Closes every menu and resets the typeahead query. */
555
+ #closeAllMenus() {
556
+ for (const top of this.topTargets) this.#closeMenu(top);
557
+ this.#typeahead.reset();
558
+ }
559
+ /**
560
+ * Closes every menu on the next task instead of synchronously.
561
+ *
562
+ * Used by `Tab`: closing right away removes the focused element before the
563
+ * browser performs its own Tab move, which can restart traversal at the
564
+ * document head and lose the user's place in the Tab order.
565
+ */
566
+ #closeMenusSoon() {
567
+ if (!this.#isAnyOpen) return;
568
+ this.#timers.clearAll();
569
+ this.#timers.set(() => this.#closeAllMenus(), 0);
570
+ }
571
+ /**
572
+ * Escape path, invoked by the shared resolver: pressed inside an open menu it
573
+ * closes that menu and returns focus to its top item (the APG behavior);
574
+ * pressed while focus is on a top item (or fell to the body) it closes every
575
+ * menu without moving focus.
576
+ */
577
+ #dismissOpenMenu() {
578
+ const active = document.activeElement;
579
+ const menu = this.menuTargets.find((candidate) => candidate.contains(active));
580
+ if (menu) {
581
+ const top = this.#topFor(menu);
582
+ this.#closeMenu(top);
583
+ this.#focusTop(top);
584
+ return;
585
+ }
586
+ this.#closeAllMenus();
587
+ }
588
+ /** Opens the menu of the navigable top item `delta` steps from `menu`'s owner. */
589
+ #moveToAdjacentMenu(menu, delta) {
590
+ const top = this.#topFor(menu);
591
+ if (!top) return;
592
+ const tops = this.#navigableTops;
593
+ const current = tops.indexOf(top);
594
+ if (current === -1) return;
595
+ const next = (current + delta + tops.length) % tops.length;
596
+ this.#openMenu(tops[next], "first");
597
+ }
598
+ /**
599
+ * Closes when a click lands outside the controller's element. Focus is left on
600
+ * whatever the user clicked — see the focus-restoration contract in the class
601
+ * docs.
602
+ */
603
+ #onOutsideClick = (event) => {
604
+ if (this.#isAnyOpen && !this.element.contains(event.target)) this.#closeAllMenus();
605
+ };
606
+ /**
607
+ * Captures clicks so `aria-disabled` top items and commands cannot reach
608
+ * consumer handlers (nor `toggle`/`activate`). Native Enter/Space activation
609
+ * also synthesizes a click and is blocked here; natively `disabled` buttons
610
+ * dispatch no click at all.
611
+ */
612
+ #onDisabledClickCapture = (event) => {
613
+ const target = event.target;
614
+ if (!(target instanceof Node)) return;
615
+ const blocked = [...this.topTargets, ...this.itemTargets].some(
616
+ (element) => this.#isActivationBlocked(element) && element.contains(target)
617
+ );
618
+ if (!blocked) return;
619
+ event.preventDefault();
620
+ event.stopImmediatePropagation();
621
+ };
622
+ /**
623
+ * Re-derives every piece of state this controller owns from the live targets:
624
+ * expanded flags, menu visibility, the single Tab stop, and Escape-stack
625
+ * membership. Runtime DOM edits can leave those out of step with each other — a
626
+ * top can claim `aria-expanded="true"` after its menu target was removed or after
627
+ * being hidden itself, a menu can stay visible after its owning top was removed,
628
+ * an open pair can appear from a morph with no layer registered for it, and the
629
+ * Tab stop can end up on a now-inert top, on a runtime-added one, or on none at
630
+ * all. Nothing is remembered between calls (except which side of the Escape stack
631
+ * this layer is on, which the stack itself does not expose), so the outcome is the
632
+ * same whichever mutation arrived and calling it more often than needed is free.
633
+ */
634
+ #reconcile() {
635
+ for (const top of this.topTargets) {
636
+ const own = this.#menuFor(top);
637
+ if (this.#isExpanded(top) && (!own || own.hidden || !this.#isNavigable(top))) {
638
+ top.setAttribute("aria-expanded", "false");
639
+ }
640
+ }
641
+ for (const menu of this.menuTargets) {
642
+ if (menu.hidden) continue;
643
+ const owner = this.#topFor(menu);
644
+ if (!owner || !this.#isExpanded(owner)) menu.hidden = true;
645
+ }
646
+ this.#ensureTabStop();
647
+ this.#rescueFocus();
648
+ this.#syncEscapeLayer();
649
+ }
650
+ /**
651
+ * Returns DOM focus to the menubar when the element holding it became
652
+ * unreachable — hidden, natively disabled, or removed — and the document had
653
+ * nowhere to put it. The destination is the top item that now owns the Tab
654
+ * stop, which is where `Escape` from a closed menu would have left the user.
655
+ *
656
+ * Focus is only *restored*, never *stolen*: it moves solely when the tracked
657
+ * element can no longer take it **and** focus is either already gone or still
658
+ * sitting on that element. The second half is what makes a `hidden` ancestor
659
+ * work — a browser blurs the element it hides on its next style pass, not when
660
+ * the attribute is written, so waiting to observe `<body>` here would always
661
+ * come too early. Removal is the other side of the same condition: the browser
662
+ * has already fallen back to the body, or left a detached `activeElement`.
663
+ */
664
+ #rescueFocus() {
665
+ const focused = this.#focused;
666
+ if (!focused) return;
667
+ const owner = this.topTargets.find(
668
+ (top2) => top2 === focused || this.#menuFor(top2)?.contains(focused)
669
+ );
670
+ if (focused.isConnected && owner && this.#isNavigable(owner)) return;
671
+ const doc = this.element.ownerDocument;
672
+ const active = doc.activeElement;
673
+ const lost = active === null || active === doc.body || active === doc.documentElement || !active.isConnected || active === focused || focused.contains(active);
674
+ if (!lost) return;
675
+ const index = this.#roving.activeIndex;
676
+ const top = index === -1 ? void 0 : this.topTargets[index];
677
+ if (top && top !== focused && !focused.contains(top)) top.focus();
678
+ }
679
+ /**
680
+ * Brings Escape-stack membership back in line with what the DOM now says is
681
+ * open, in both directions.
682
+ *
683
+ * The deactivate half is the common one (something closed). The activate half
684
+ * covers a menu that became open *without* going through {@link #openMenu} —
685
+ * a Turbo morph that patches `aria-expanded` and the menu's `hidden` in place,
686
+ * which is legitimate here because the DOM is this controller's only source of
687
+ * truth. Without it the popup is visible but `Escape` does nothing, since no
688
+ * layer is registered to claim the press.
689
+ *
690
+ * `#layerActive` exists because re-activating an already-active layer moves it
691
+ * to the top of the stack, which would reshuffle nested layers on every
692
+ * unrelated mutation. Registering only on the false→true edge keeps activation
693
+ * ordered by when each layer actually opened.
694
+ */
695
+ #syncEscapeLayer() {
696
+ const open = this.#isAnyOpen;
697
+ if (open === this.#layerActive) return;
698
+ if (open) {
699
+ this.#escapeLayer.activate(document, {
700
+ onDismiss: () => this.#dismissOpenMenu(),
701
+ claims: claimsWhileFocusWithin(this.element)
702
+ });
703
+ } else {
704
+ this.#escapeLayer.deactivate();
705
+ }
706
+ this.#layerActive = open;
707
+ }
708
+ /**
709
+ * Re-establishes the single Tab stop: keep the current one while it is still
710
+ * navigable, else hand it to the first navigable top item. Every other top is
711
+ * explicitly removed from the Tab sequence, so a top that arrived tabbable
712
+ * cannot leave two stops behind. No Tab stop at all (`-1`) happens only when
713
+ * every top is inert, and is recovered from as soon as one becomes navigable.
714
+ */
715
+ #ensureTabStop() {
716
+ const active = this.#roving.activeIndex;
717
+ const activeTop = active === -1 ? void 0 : this.topTargets[active];
718
+ if (activeTop && this.#isNavigable(activeTop)) {
719
+ this.#roving.setActive(active);
720
+ return;
721
+ }
722
+ const first = this.#navigableTops[0];
723
+ this.#roving.setActive(first ? this.topTargets.indexOf(first) : -1);
724
+ }
725
+ /** The menu element controlled by `top` (matched by `aria-controls`/`id`). */
726
+ #menuFor(top) {
727
+ const id = top.getAttribute("aria-controls");
728
+ return id ? this.menuTargets.find((menu) => menu.id === id) ?? null : null;
729
+ }
730
+ /** The top item that controls `menu` (reverse of `#menuFor`). */
731
+ #topFor(menu) {
732
+ if (!menu) return null;
733
+ return this.topTargets.find((top) => top.getAttribute("aria-controls") === menu.id) ?? null;
734
+ }
735
+ /** The navigable item targets that live inside `menu`, in DOM order. */
736
+ #itemsIn(menu) {
737
+ return this.itemTargets.filter((item) => menu.contains(item) && this.#isNavigable(item));
738
+ }
739
+ /** Top items eligible for the roving tab stop and the arrow keys. */
740
+ get #navigableTops() {
741
+ return this.topTargets.filter((top) => this.#isNavigable(top));
742
+ }
743
+ /**
744
+ * Whether `element` can take roving focus: `hidden` and natively `disabled`
745
+ * controls are out of reach and are skipped. `aria-disabled` is deliberately
746
+ * **not** checked — the APG keeps such items focusable so they stay
747
+ * discoverable, and {@link MenubarController.#isActivationBlocked} is what
748
+ * suppresses acting on them. CSS-only visibility is not detectable headlessly
749
+ * and stays the consumer's responsibility.
750
+ */
751
+ #isNavigable(element) {
752
+ if (element.hasAttribute("hidden")) return false;
753
+ return !element.disabled;
754
+ }
755
+ /** Whether `element` is `aria-disabled`: reachable, but never activated. */
756
+ #isActivationBlocked(element) {
757
+ return element.getAttribute("aria-disabled") === "true";
758
+ }
759
+ /** Moves DOM focus to the item at `index` (no-op if out of range). */
760
+ #focusAt(items, index) {
761
+ items[index]?.focus();
762
+ }
763
+ /** Returns roving focus to a top item (and makes it the single tab stop). */
764
+ #focusTop(top) {
765
+ if (!top) return;
766
+ this.#roving.setActive(this.topTargets.indexOf(top), { focus: true });
767
+ }
768
+ /** Whether `top`'s menu is currently expanded. */
769
+ #isExpanded(top) {
770
+ return top.getAttribute("aria-expanded") === "true";
771
+ }
772
+ /** Whether any top item's menu is currently open. */
773
+ get #isAnyOpen() {
774
+ return this.topTargets.some((top) => this.#isExpanded(top));
775
+ }
776
+ /** Advances focus to the next item in `items` matching the accumulated query. */
777
+ #typeaheadTo(items, current, key) {
778
+ const index = findTypeaheadMatch(items, current, this.#typeahead.push(key));
779
+ if (index !== -1) items[index]?.focus();
780
+ }
781
+ };
782
+
783
+ export { MenubarController };
784
+ //# sourceMappingURL=menubar_controller.js.map
785
+ //# sourceMappingURL=menubar_controller.js.map