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,761 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // src/controllers/sidebar_controller.ts
4
+
5
+ // src/utils/escape_layer.ts
6
+ var EscapeLayer = class _EscapeLayer {
7
+ static #registries = /* @__PURE__ */ new WeakMap();
8
+ #ownerDocument = null;
9
+ /** Dismissal callback while active; `null` when inactive. */
10
+ #onDismiss = null;
11
+ /** Live predicate deciding whether the layer claims a press; `null` = always. */
12
+ #claims = null;
13
+ /**
14
+ * Activates this layer at the top of its document's Escape stack, installing
15
+ * the document's shared resolver listener if this is its first layer.
16
+ * Re-activating an already-active layer moves it to the top.
17
+ */
18
+ activate(ownerDocument = document, options) {
19
+ this.deactivate();
20
+ let registry = _EscapeLayer.#registries.get(ownerDocument);
21
+ if (!registry) {
22
+ registry = _EscapeLayer.#createRegistry();
23
+ _EscapeLayer.#registries.set(ownerDocument, registry);
24
+ ownerDocument.addEventListener("keydown", registry.onKeydown);
25
+ }
26
+ registry.stack.push(this);
27
+ this.#ownerDocument = ownerDocument;
28
+ this.#onDismiss = options.onDismiss;
29
+ this.#claims = options.claims ?? null;
30
+ }
31
+ /**
32
+ * Removes this layer from its document's Escape stack, uninstalling the
33
+ * shared listener when the stack empties. Safe to call when inactive.
34
+ */
35
+ deactivate() {
36
+ const ownerDocument = this.#ownerDocument;
37
+ if (!ownerDocument) return;
38
+ const registry = _EscapeLayer.#registries.get(ownerDocument);
39
+ if (registry) {
40
+ const index = registry.stack.lastIndexOf(this);
41
+ if (index >= 0) registry.stack.splice(index, 1);
42
+ if (registry.stack.length === 0) {
43
+ ownerDocument.removeEventListener("keydown", registry.onKeydown);
44
+ _EscapeLayer.#registries.delete(ownerDocument);
45
+ }
46
+ }
47
+ this.#ownerDocument = null;
48
+ this.#onDismiss = null;
49
+ this.#claims = null;
50
+ }
51
+ /**
52
+ * Whether this active layer would own a press right now: it is the topmost
53
+ * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests
54
+ * and diagnostics — production dismissal goes through the shared listener.
55
+ */
56
+ get ownsEscape() {
57
+ const ownerDocument = this.#ownerDocument;
58
+ if (!ownerDocument) return false;
59
+ const registry = _EscapeLayer.#registries.get(ownerDocument);
60
+ if (!registry) return false;
61
+ return _EscapeLayer.#resolveOwner(registry.stack) === this;
62
+ }
63
+ /** Builds a document's registry with its shared resolver listener. */
64
+ static #createRegistry() {
65
+ const registry = {
66
+ stack: [],
67
+ onKeydown: (event) => {
68
+ if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
69
+ const owner = _EscapeLayer.#resolveOwner(registry.stack);
70
+ if (!owner) return;
71
+ event.preventDefault();
72
+ owner.#onDismiss?.();
73
+ }
74
+ };
75
+ return registry;
76
+ }
77
+ /** The topmost stack layer whose claims predicate passes, or `null`. */
78
+ static #resolveOwner(stack) {
79
+ for (let index = stack.length - 1; index >= 0; index--) {
80
+ const layer = stack[index];
81
+ if (!layer) continue;
82
+ if (layer.#claims && !layer.#claims()) continue;
83
+ return layer;
84
+ }
85
+ return null;
86
+ }
87
+ };
88
+
89
+ // src/utils/focus_trap.ts
90
+ var FOCUSABLE = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
91
+ var FocusTrap = class {
92
+ /** The element focused before activation, restored on deactivation. */
93
+ #previouslyFocused = null;
94
+ /** The body's inline `overflow` before locking, restored on deactivation. */
95
+ #previousBodyOverflow = "";
96
+ /** Whether scroll was locked this activation (so it is only restored if applied). */
97
+ #scrollLocked = false;
98
+ /** Background siblings made `inert` while active, restored on deactivation. */
99
+ #inertedSiblings = [];
100
+ /** Whether the modal side effects are currently applied. */
101
+ #activeState = false;
102
+ /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */
103
+ #escapeLayer = new EscapeLayer();
104
+ /** Returns the trapped element; called on every operation for the live target. */
105
+ #getContainer;
106
+ /** Closing/focus hooks; see {@link FocusTrapOptions}. */
107
+ #options;
108
+ /**
109
+ * @param getContainer - Returns the trapped element. Called on every operation
110
+ * so the live target is always used.
111
+ * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.
112
+ */
113
+ constructor(getContainer, options = {}) {
114
+ this.#getContainer = getContainer;
115
+ this.#options = options;
116
+ }
117
+ /** Whether the trap is currently active. */
118
+ get active() {
119
+ return this.#activeState;
120
+ }
121
+ /**
122
+ * Applies the trap: records the current focus, optionally locks background scroll
123
+ * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless
124
+ * `autoFocus` is off) moves focus inside. No-ops if already active.
125
+ */
126
+ activate() {
127
+ if (this.#activeState) return;
128
+ this.#activeState = true;
129
+ const active = document.activeElement;
130
+ this.#previouslyFocused = active instanceof HTMLElement && active !== document.body ? active : null;
131
+ if (this.#flag(this.#options.lockScroll, true)) {
132
+ this.#previousBodyOverflow = document.body.style.overflow;
133
+ document.body.style.overflow = "hidden";
134
+ this.#scrollLocked = true;
135
+ }
136
+ if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();
137
+ document.addEventListener("keydown", this.#onKeydown);
138
+ document.addEventListener("turbo:before-cache", this.#onBeforeCache);
139
+ const onEscape = this.#options.onEscape;
140
+ if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });
141
+ if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();
142
+ }
143
+ /**
144
+ * Reverts every side effect applied by {@link activate}. No-ops if inactive, so
145
+ * a controller can call it defensively from both `close()` and `disconnect()`.
146
+ *
147
+ * @param restoreFocus - Move focus back to the opener (default `true`). Pass
148
+ * `false` on teardown (`disconnect`), where yanking focus is undesirable.
149
+ */
150
+ deactivate({ restoreFocus = true } = {}) {
151
+ if (!this.#activeState) return;
152
+ this.#activeState = false;
153
+ this.#escapeLayer.deactivate();
154
+ document.removeEventListener("keydown", this.#onKeydown);
155
+ document.removeEventListener("turbo:before-cache", this.#onBeforeCache);
156
+ if (this.#scrollLocked) {
157
+ document.body.style.overflow = this.#previousBodyOverflow;
158
+ this.#scrollLocked = false;
159
+ }
160
+ this.#releaseBackground();
161
+ if (restoreFocus) {
162
+ const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;
163
+ target?.focus();
164
+ }
165
+ }
166
+ /** Resolves a boolean-or-getter option, defaulting when it was not provided. */
167
+ #flag(option, fallback) {
168
+ if (option === void 0) return fallback;
169
+ return typeof option === "function" ? option() : option;
170
+ }
171
+ /**
172
+ * Reverts the side effects just before Turbo caches the page snapshot, so an
173
+ * overlay left open does not bake the scroll lock into `body[style]` — a
174
+ * restored page would feed that locked value back into {@link activate} as the
175
+ * baseline, and closing would then never unlock the page. Markup state stays
176
+ * untouched (restore-open designs reopen against a clean baseline), and focus
177
+ * is left alone mid-navigation. The listener lives only while active.
178
+ */
179
+ #onBeforeCache = () => {
180
+ this.deactivate({ restoreFocus: false });
181
+ };
182
+ /**
183
+ * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the
184
+ * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of
185
+ * which layer currently owns Escape.
186
+ */
187
+ #onKeydown = (event) => {
188
+ if (event.key === "Tab") this.#trapTab(event);
189
+ };
190
+ /** Keeps `Tab` focus cycling within the container's focusable elements. */
191
+ #trapTab(event) {
192
+ const focusable = this.#focusableElements();
193
+ if (focusable.length === 0) {
194
+ event.preventDefault();
195
+ return;
196
+ }
197
+ const first = focusable[0];
198
+ const last = focusable[focusable.length - 1];
199
+ const active = document.activeElement;
200
+ if (!(active instanceof Node) || !this.#getContainer().contains(active)) {
201
+ event.preventDefault();
202
+ first?.focus();
203
+ return;
204
+ }
205
+ if (event.shiftKey && active === first) {
206
+ event.preventDefault();
207
+ last?.focus();
208
+ } else if (!event.shiftKey && active === last) {
209
+ event.preventDefault();
210
+ first?.focus();
211
+ }
212
+ }
213
+ /**
214
+ * Marks every element outside the container's subtree as `inert` so background
215
+ * content cannot be focused or reached by assistive technology, honoring the
216
+ * `aria-modal="true"` contract. An element that was *already* `inert` is left
217
+ * untracked so `#releaseBackground` does not wrongly clear it.
218
+ */
219
+ #isolateBackground() {
220
+ const container = this.#getContainer();
221
+ this.#inertedSiblings = [];
222
+ for (const sibling of Array.from(document.body.children)) {
223
+ if (!(sibling instanceof HTMLElement)) continue;
224
+ if (sibling.contains(container) || sibling.inert) continue;
225
+ sibling.inert = true;
226
+ this.#inertedSiblings.push(sibling);
227
+ }
228
+ }
229
+ /** Reverts the `inert` flags applied by `#isolateBackground`. */
230
+ #releaseBackground() {
231
+ for (const sibling of this.#inertedSiblings) {
232
+ sibling.inert = false;
233
+ }
234
+ this.#inertedSiblings = [];
235
+ }
236
+ /** Moves focus to the initial target, the first focusable, or the container. */
237
+ #focusInitial() {
238
+ const preferred = this.#options.initialFocus?.();
239
+ if (preferred) {
240
+ preferred.focus();
241
+ return;
242
+ }
243
+ const focusable = this.#focusableElements();
244
+ if (focusable[0]) {
245
+ focusable[0].focus();
246
+ return;
247
+ }
248
+ const container = this.#getContainer();
249
+ container.tabIndex = -1;
250
+ container.focus();
251
+ }
252
+ /** Collects the container's currently focusable descendants in DOM order. */
253
+ #focusableElements() {
254
+ return Array.from(this.#getContainer().querySelectorAll(FOCUSABLE)).filter(
255
+ (el) => !el.hidden
256
+ );
257
+ }
258
+ };
259
+
260
+ // src/utils/safe_storage.ts
261
+ function readLocalStorage(key) {
262
+ try {
263
+ return window.localStorage.getItem(key);
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+ function writeLocalStorage(key, value) {
269
+ try {
270
+ window.localStorage.setItem(key, value);
271
+ } catch {
272
+ }
273
+ }
274
+
275
+ // src/utils/safe_timeout.ts
276
+ var TimerRegistry = class {
277
+ /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
278
+ ids = /* @__PURE__ */ new Set();
279
+ /**
280
+ * Cancels a single tracked timer.
281
+ *
282
+ * No-ops if the id is unknown (already cleared, fired, or never owned by this
283
+ * registry), so callers can clear defensively without guarding.
284
+ */
285
+ clear(id) {
286
+ if (this.ids.delete(id)) {
287
+ this.cancel(id);
288
+ }
289
+ }
290
+ /**
291
+ * Cancels every tracked timer. Call this from a controller's `disconnect()`
292
+ * to guarantee no timer outlives the element.
293
+ */
294
+ clearAll() {
295
+ for (const id of this.ids) {
296
+ this.cancel(id);
297
+ }
298
+ this.ids.clear();
299
+ }
300
+ /** Number of timers currently tracked (pending). */
301
+ get size() {
302
+ return this.ids.size;
303
+ }
304
+ };
305
+ var SafeTimeout = class extends TimerRegistry {
306
+ /**
307
+ * Schedules `callback` after `delay` ms and returns the timer id.
308
+ *
309
+ * The id is removed from the registry automatically when the timeout fires,
310
+ * so {@link TimerRegistry.size | size} reflects only still-pending timers.
311
+ */
312
+ set(callback, delay) {
313
+ const id = this.schedule(() => {
314
+ this.ids.delete(id);
315
+ callback();
316
+ }, delay);
317
+ this.ids.add(id);
318
+ return id;
319
+ }
320
+ schedule(callback, delay) {
321
+ return window.setTimeout(callback, delay);
322
+ }
323
+ cancel(id) {
324
+ window.clearTimeout(id);
325
+ }
326
+ };
327
+
328
+ // src/utils/transition_completion.ts
329
+ function timeMs(value) {
330
+ const trimmed = value.trim();
331
+ const amount = Number.parseFloat(trimmed);
332
+ if (!Number.isFinite(amount)) return 0;
333
+ if (trimmed.endsWith("ms")) return amount;
334
+ if (trimmed.endsWith("s")) return amount * 1e3;
335
+ return 0;
336
+ }
337
+ function cssList(value) {
338
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
339
+ }
340
+ function transitionTimings(style) {
341
+ const properties = cssList(style.transitionProperty);
342
+ const durations = cssList(style.transitionDuration).map(timeMs);
343
+ const delays = cssList(style.transitionDelay).map(timeMs);
344
+ const effectiveProperties = properties.length > 0 ? properties : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => "all");
345
+ const effectiveDurations = durations.length > 0 ? durations : [0];
346
+ const effectiveDelays = delays.length > 0 ? delays : [0];
347
+ return effectiveProperties.filter((property) => property !== "none").map((property, index) => ({
348
+ property,
349
+ totalMs: Math.max(
350
+ 0,
351
+ (effectiveDurations[index % effectiveDurations.length] ?? 0) + (effectiveDelays[index % effectiveDelays.length] ?? 0)
352
+ )
353
+ }));
354
+ }
355
+ function maxTotalMs(timings) {
356
+ return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);
357
+ }
358
+ var TransitionCompletion = class {
359
+ #timers = new SafeTimeout();
360
+ #element = null;
361
+ #complete = null;
362
+ #pendingProperties = null;
363
+ #deadline = 0;
364
+ /**
365
+ * Replaces any prior wait and invokes `complete` synchronously for a 0ms
366
+ * transition (including when `getComputedStyle` is unavailable).
367
+ *
368
+ * With a positive `options.timeoutMs` the synchronous fast-path is skipped and
369
+ * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).
370
+ */
371
+ wait(element, complete, options = {}) {
372
+ this.cancel();
373
+ const requested = options.timeoutMs ?? 0;
374
+ const override = Number.isFinite(requested) && requested > 0 ? requested : 0;
375
+ const timings = typeof window.getComputedStyle === "function" ? transitionTimings(window.getComputedStyle(element)) : [];
376
+ const maximum = maxTotalMs(timings);
377
+ if (maximum <= 0 && override <= 0) {
378
+ complete();
379
+ return;
380
+ }
381
+ this.#element = element;
382
+ this.#complete = complete;
383
+ this.#deadline = Date.now() + maximum;
384
+ const activeProperties = this.#activeTransitionProperties(element);
385
+ this.#pendingProperties = activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);
386
+ element.addEventListener("transitionend", this.#onTerminal);
387
+ element.addEventListener("transitioncancel", this.#onTerminal);
388
+ this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);
389
+ }
390
+ /** Cancels the pending wait without invoking its completion callback. */
391
+ cancel() {
392
+ this.#complete = null;
393
+ this.#teardown();
394
+ }
395
+ /**
396
+ * Handles terminal events from the observed element only.
397
+ *
398
+ * For explicit property lists, every declared positive-time property must
399
+ * settle. For `all`, no reliable property set exists, so an event can finish
400
+ * only after the computed maximum time; the safety timeout owns the usual path.
401
+ */
402
+ #onTerminal = (event) => {
403
+ if (event.target !== this.#element) return;
404
+ const transitionEvent = event;
405
+ if (transitionEvent.pseudoElement) return;
406
+ if (this.#pendingProperties) {
407
+ const propertyName = transitionEvent.propertyName;
408
+ const active = this.#activeTransitionProperties(this.#element);
409
+ if (active.has(propertyName)) return;
410
+ if (!this.#pendingProperties.delete(propertyName)) return;
411
+ if (this.#pendingProperties.size > 0) return;
412
+ if (active.size > 0) {
413
+ this.#pendingProperties = active;
414
+ return;
415
+ }
416
+ this.#finish();
417
+ return;
418
+ }
419
+ if (Date.now() >= this.#deadline) this.#finish();
420
+ };
421
+ /**
422
+ * Returns active CSS transition properties expanded to the names reported by
423
+ * terminal events. CSS animations and pseudo-element effects are excluded.
424
+ */
425
+ #activeTransitionProperties(element) {
426
+ if (!element || typeof element.getAnimations !== "function") return /* @__PURE__ */ new Set();
427
+ try {
428
+ const properties = element.getAnimations().flatMap((animation) => {
429
+ if (animation.playState === "idle" || animation.playState === "finished") return [];
430
+ const effect = animation.effect;
431
+ if (effect?.pseudoElement) return [];
432
+ const target = effect?.target;
433
+ if (target && target !== element) return [];
434
+ const property = animation.transitionProperty;
435
+ return typeof property === "string" && property.length > 0 ? [property] : [];
436
+ });
437
+ return new Set(properties);
438
+ } catch {
439
+ return /* @__PURE__ */ new Set();
440
+ }
441
+ }
442
+ /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */
443
+ #explicitPendingProperties(timings) {
444
+ if (timings.some(({ property }) => property === "all")) return null;
445
+ const pending = new Set(
446
+ timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property)
447
+ );
448
+ return pending.size > 0 ? pending : null;
449
+ }
450
+ /** Completes exactly once, releasing listeners and the fallback before the callback. */
451
+ #finish() {
452
+ const complete = this.#complete;
453
+ if (!complete) return;
454
+ this.#complete = null;
455
+ this.#teardown();
456
+ complete();
457
+ }
458
+ /** Releases the exact element listeners and timer owned by the current wait. */
459
+ #teardown() {
460
+ this.#timers.clearAll();
461
+ this.#element?.removeEventListener("transitionend", this.#onTerminal);
462
+ this.#element?.removeEventListener("transitioncancel", this.#onTerminal);
463
+ this.#element = null;
464
+ this.#pendingProperties = null;
465
+ this.#deadline = 0;
466
+ }
467
+ };
468
+
469
+ // src/controllers/sidebar_controller.ts
470
+ var SidebarController = class extends Controller {
471
+ static targets = ["trigger", "panel", "backdrop"];
472
+ static values = {
473
+ breakpoint: { type: Number, default: 768 },
474
+ key: { type: String, default: "" },
475
+ collapsed: { type: Boolean, default: false }
476
+ };
477
+ static actions = ["beforeCache", "close", "open", "toggle"];
478
+ /** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
479
+ #activePanel = null;
480
+ /** Owns the overlay modal side effects; Escape closes, focus falls to trigger. */
481
+ #trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
482
+ onEscape: () => this.close(),
483
+ fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
484
+ });
485
+ /** Current responsive mode. */
486
+ #mode = "inline";
487
+ /** Persisted inline preference: whether the rail is collapsed. */
488
+ #collapsed = false;
489
+ /** The matched media query (`min-width: breakpoint`), watched for mode changes. */
490
+ #mql = null;
491
+ /** Exact normalized query currently represented by the active media-query listener. */
492
+ #mqlQuery = null;
493
+ /** Owns the cancellable close-transition wait and its bounded fallback. */
494
+ #transition = new TransitionCompletion();
495
+ /** Distinguishes dynamic target churn from callbacks around controller teardown. */
496
+ #connected = false;
497
+ connect() {
498
+ this.#connected = true;
499
+ this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;
500
+ this.#collapsed = this.#restoreCollapsed();
501
+ this.#mqlQuery = this.#breakpointQuery;
502
+ this.#mql = this.#matchBreakpoint(this.#mqlQuery);
503
+ this.#mql?.addEventListener("change", this.#onMediaChange);
504
+ this.#applyMode(this.#computeMode());
505
+ }
506
+ disconnect() {
507
+ this.#connected = false;
508
+ this.#mql?.removeEventListener("change", this.#onMediaChange);
509
+ this.#mql = null;
510
+ this.#mqlQuery = null;
511
+ this.#transition.cancel();
512
+ this.#trap.deactivate({ restoreFocus: false });
513
+ this.#activePanel = null;
514
+ }
515
+ /** Adopts a panel target added by a Turbo morph after the controller connected. */
516
+ panelTargetConnected(panel) {
517
+ if (!this.#connected || this.#activePanel?.isConnected && this.#activePanel !== panel) return;
518
+ this.#adoptPanel(panel);
519
+ }
520
+ /** Closes and releases overlay side effects when the actively trapped panel disappears. */
521
+ panelTargetDisconnected(panel) {
522
+ if (panel !== this.#activePanel) return;
523
+ this.#transition.cancel();
524
+ this.#activePanel = null;
525
+ if (!this.#connected) return;
526
+ panel.setAttribute("data-state", "closed");
527
+ panel.hidden = true;
528
+ this.#hideBackdrop();
529
+ this.#setExpandedAttr(false);
530
+ this.#trap.deactivate();
531
+ if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);
532
+ }
533
+ /**
534
+ * Rebinds responsive observation when `breakpoint` changes at runtime.
535
+ *
536
+ * Stimulus calls value callbacks before `connect()`, so the connected guard
537
+ * prevents an eager subscription. Equivalent normalized queries retain the
538
+ * existing listener instead of allocating duplicate `MediaQueryList` objects.
539
+ */
540
+ breakpointValueChanged() {
541
+ if (!this.#connected) return;
542
+ const query = this.#breakpointQuery;
543
+ if (this.#mqlQuery === query) return;
544
+ this.#mql?.removeEventListener("change", this.#onMediaChange);
545
+ this.#mqlQuery = query;
546
+ this.#mql = this.#matchBreakpoint(query);
547
+ this.#mql?.addEventListener("change", this.#onMediaChange);
548
+ const next = this.#computeMode();
549
+ if (next !== this.#mode) this.#applyMode(next);
550
+ }
551
+ /**
552
+ * Sanitizes overlay markup before Turbo snapshots the page.
553
+ *
554
+ * Inline state is durable and remains untouched. Overlay state is transient:
555
+ * pending transitions are cancelled, controller-owned open attributes are
556
+ * closed immediately, and modal side effects are released without moving
557
+ * focus during navigation.
558
+ */
559
+ beforeCache() {
560
+ if (!this.#connected || !this.#isOverlay) return;
561
+ this.#transition.cancel();
562
+ this.#trap.deactivate({ restoreFocus: false });
563
+ this.#setOverlayClosedImmediate();
564
+ }
565
+ /** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */
566
+ toggle() {
567
+ if (this.#isOverlay) {
568
+ this.#isOverlayOpen ? this.#closeOverlay() : this.#openOverlay();
569
+ } else {
570
+ this.#setCollapsed(!this.#collapsed);
571
+ }
572
+ }
573
+ /** Shows the panel (inline: expand; overlay: open). */
574
+ open() {
575
+ if (this.#isOverlay) this.#openOverlay();
576
+ else this.#setCollapsed(false);
577
+ }
578
+ /** Hides the panel (inline: collapse; overlay: close). */
579
+ close() {
580
+ if (this.#isOverlay) this.#closeOverlay();
581
+ else this.#setCollapsed(true);
582
+ }
583
+ // --- Mode handling ---------------------------------------------------------
584
+ /** Re-renders the closed/default state for `mode` and records it. */
585
+ #applyMode(mode) {
586
+ this.#mode = mode;
587
+ if (this.hasPanelTarget) this.panelTarget.setAttribute("data-mode", mode);
588
+ if (mode === "inline") {
589
+ this.#transition.cancel();
590
+ this.#trap.deactivate({ restoreFocus: false });
591
+ if (this.hasPanelTarget) this.panelTarget.hidden = false;
592
+ this.#hideBackdrop();
593
+ this.#applyInlineState(this.#collapsed);
594
+ } else {
595
+ this.#transition.cancel();
596
+ this.#trap.deactivate({ restoreFocus: false });
597
+ this.#setOverlayClosedImmediate();
598
+ }
599
+ }
600
+ /** Reconciles a replacement panel with the current responsive mode and DOM state. */
601
+ #adoptPanel(panel) {
602
+ this.#transition.cancel();
603
+ const trapWasActive = this.#trap.active;
604
+ this.#activePanel = panel;
605
+ panel.setAttribute("data-mode", this.#mode);
606
+ if (this.#mode === "inline") {
607
+ panel.hidden = false;
608
+ panel.setAttribute("data-state", this.#collapsed ? "collapsed" : "expanded");
609
+ this.#hideBackdrop();
610
+ this.#setExpandedAttr(!this.#collapsed);
611
+ this.#trap.deactivate({ restoreFocus: false });
612
+ return;
613
+ }
614
+ if (panel.getAttribute("data-state") === "open") {
615
+ panel.hidden = false;
616
+ if (this.hasBackdropTarget) {
617
+ this.backdropTarget.setAttribute("data-state", "open");
618
+ this.backdropTarget.hidden = false;
619
+ }
620
+ this.#setExpandedAttr(true);
621
+ if (trapWasActive) this.#trap.deactivate({ restoreFocus: false });
622
+ this.#trap.activate();
623
+ return;
624
+ }
625
+ panel.setAttribute("data-state", "closed");
626
+ panel.hidden = true;
627
+ this.#hideBackdrop();
628
+ this.#setExpandedAttr(false);
629
+ this.#trap.deactivate();
630
+ }
631
+ #onMediaChange = (event) => {
632
+ const next = event.matches ? "inline" : "overlay";
633
+ if (next !== this.#mode) this.#applyMode(next);
634
+ };
635
+ #computeMode() {
636
+ return this.#mql?.matches ?? true ? "inline" : "overlay";
637
+ }
638
+ #matchBreakpoint(query = this.#breakpointQuery) {
639
+ if (typeof window.matchMedia !== "function") return null;
640
+ return window.matchMedia(query);
641
+ }
642
+ // --- Inline (rail) ---------------------------------------------------------
643
+ /** Sets, reflects, and persists the inline collapsed preference. */
644
+ #setCollapsed(collapsed) {
645
+ if (collapsed === this.#collapsed) return;
646
+ this.#collapsed = collapsed;
647
+ this.#applyInlineState(collapsed);
648
+ this.#persistCollapsed(collapsed);
649
+ }
650
+ /** Reflects the inline rail state onto the panel and trigger. */
651
+ #applyInlineState(collapsed) {
652
+ if (this.hasPanelTarget) {
653
+ this.panelTarget.setAttribute("data-state", collapsed ? "collapsed" : "expanded");
654
+ }
655
+ this.#setExpandedAttr(!collapsed);
656
+ }
657
+ // --- Overlay (off-canvas modal) -------------------------------------------
658
+ /** Opens the overlay: reveal it, commit a starting frame, then trap focus. */
659
+ #openOverlay() {
660
+ if (!this.hasPanelTarget || this.#isOverlayOpen) return;
661
+ this.#transition.cancel();
662
+ this.#activePanel = this.panelTarget;
663
+ this.panelTarget.hidden = false;
664
+ if (this.hasBackdropTarget) this.backdropTarget.hidden = false;
665
+ void this.panelTarget.offsetWidth;
666
+ this.#setOverlayState("open");
667
+ this.#setExpandedAttr(true);
668
+ this.#trap.activate();
669
+ }
670
+ /** Closes the overlay: start the exit transition, defer hide + trap teardown. */
671
+ #closeOverlay() {
672
+ if (!this.hasPanelTarget || !this.#isOverlayOpen) return;
673
+ this.#setOverlayState("closed");
674
+ this.#setExpandedAttr(false);
675
+ this.#hideAfterTransition();
676
+ }
677
+ /** Renders the overlay closed state up front (used when entering overlay mode). */
678
+ #setOverlayClosedImmediate() {
679
+ this.#setOverlayState("closed");
680
+ this.#setExpandedAttr(false);
681
+ if (this.hasPanelTarget) this.panelTarget.hidden = true;
682
+ this.#hideBackdrop();
683
+ }
684
+ /** Syncs `data-state` on the panel and backdrop together (overlay). */
685
+ #setOverlayState(state) {
686
+ if (this.hasPanelTarget) this.panelTarget.setAttribute("data-state", state);
687
+ if (this.hasBackdropTarget) this.backdropTarget.setAttribute("data-state", state);
688
+ }
689
+ /**
690
+ * Applies `hidden` once the close transition ends so the exit slide can play,
691
+ * then reverts the modal side effects. The shared waiter completes
692
+ * synchronously for 0ms transitions and supplies a bounded fallback when the
693
+ * browser emits no terminal event.
694
+ */
695
+ #hideAfterTransition() {
696
+ const panel = this.panelTarget;
697
+ this.#activePanel = panel;
698
+ this.#transition.wait(panel, () => this.#applyOverlayHidden(panel));
699
+ }
700
+ /** Hides the panel/backdrop and tears down the trap after the exit transition. */
701
+ #applyOverlayHidden(panel) {
702
+ panel.hidden = true;
703
+ this.#hideBackdrop();
704
+ this.#trap.deactivate();
705
+ }
706
+ #hideBackdrop() {
707
+ if (!this.hasBackdropTarget) return;
708
+ this.backdropTarget.setAttribute("data-state", "closed");
709
+ this.backdropTarget.hidden = true;
710
+ }
711
+ // --- Shared helpers --------------------------------------------------------
712
+ #setExpandedAttr(expanded) {
713
+ if (this.hasTriggerTarget) {
714
+ this.triggerTarget.setAttribute("aria-expanded", expanded ? "true" : "false");
715
+ }
716
+ }
717
+ /**
718
+ * Resolves the inline collapsed preference, in priority order:
719
+ * 1. the persisted value (when a `key` is set and storage is readable),
720
+ * 2. the current DOM `data-state` — so a Turbo cache restore / morph that
721
+ * reconnects over already-rendered markup keeps the live state (the DOM is
722
+ * the source of truth) even when no `key` / localStorage is available,
723
+ * 3. the declared `collapsed` value.
724
+ */
725
+ #restoreCollapsed() {
726
+ const key = this.#storageKey;
727
+ if (key) {
728
+ const stored = readLocalStorage(key);
729
+ if (stored !== null) return stored === "1";
730
+ }
731
+ const domState = this.hasPanelTarget ? this.panelTarget.getAttribute("data-state") : null;
732
+ if (domState === "collapsed") return true;
733
+ if (domState === "expanded") return false;
734
+ return this.collapsedValue;
735
+ }
736
+ /** Persists the collapsed preference when a key is configured. */
737
+ #persistCollapsed(collapsed) {
738
+ const key = this.#storageKey;
739
+ if (!key) return;
740
+ writeLocalStorage(key, collapsed ? "1" : "0");
741
+ }
742
+ get #storageKey() {
743
+ return this.keyValue ? `stimeo--sidebar:${this.keyValue}` : "";
744
+ }
745
+ /** Valid breakpoint CSS query, defaulting malformed or negative values to 768px. */
746
+ get #breakpointQuery() {
747
+ const value = this.breakpointValue;
748
+ const breakpoint = Number.isFinite(value) && value >= 0 ? value : 768;
749
+ return `(min-width: ${breakpoint}px)`;
750
+ }
751
+ get #isOverlay() {
752
+ return this.#mode === "overlay";
753
+ }
754
+ get #isOverlayOpen() {
755
+ return this.#isOverlay && this.hasPanelTarget && this.panelTarget.getAttribute("data-state") === "open";
756
+ }
757
+ };
758
+
759
+ export { SidebarController };
760
+ //# sourceMappingURL=sidebar_controller.js.map
761
+ //# sourceMappingURL=sidebar_controller.js.map