stimeo-ui 0.14.0 → 0.16.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -0
  3. data/README.md +120 -0
  4. data/dist/cable/index.js +123 -29
  5. data/dist/controllers/accordion_controller.js +98 -9
  6. data/dist/controllers/announcer_controller.js +96 -62
  7. data/dist/controllers/auto_submit_controller.js +83 -7
  8. data/dist/controllers/avatar_controller.js +1 -1
  9. data/dist/controllers/breadcrumb_controller.js +38 -11
  10. data/dist/controllers/bulk_select_controller.js +7 -6
  11. data/dist/controllers/calendar_controller.js +340 -123
  12. data/dist/controllers/carousel_controller.js +263 -38
  13. data/dist/controllers/character_counter_controller.js +40 -2
  14. data/dist/controllers/checkbox_controller.js +81 -12
  15. data/dist/controllers/clipboard_controller.js +63 -5
  16. data/dist/controllers/collapsible_controller.js +99 -14
  17. data/dist/controllers/color_picker_controller.js +80 -34
  18. data/dist/controllers/combobox_controller.js +106 -16
  19. data/dist/controllers/command_palette_controller.js +35 -3
  20. data/dist/controllers/conditional_fields_controller.js +85 -17
  21. data/dist/controllers/confirm_controller.js +3 -0
  22. data/dist/controllers/context_menu_controller.js +32 -12
  23. data/dist/controllers/countdown_controller.js +129 -26
  24. data/dist/controllers/currency_input_controller.js +221 -67
  25. data/dist/controllers/data_grid_controller.js +195 -29
  26. data/dist/controllers/date_range_picker_controller.js +151 -30
  27. data/dist/controllers/dialog_controller.js +35 -8
  28. data/dist/controllers/direct_upload_controller.js +22 -4
  29. data/dist/controllers/dirty_form_controller.js +46 -13
  30. data/dist/controllers/dismissible_controller.js +1 -0
  31. data/dist/controllers/drawer_controller.js +54 -19
  32. data/dist/controllers/dropdown_controller.js +36 -9
  33. data/dist/controllers/editable_controller.js +34 -0
  34. data/dist/controllers/file_dropzone_controller.js +144 -51
  35. data/dist/controllers/filter_controller.js +20 -6
  36. data/dist/controllers/flash_controller.js +432 -71
  37. data/dist/controllers/focus_controller.js +1 -0
  38. data/dist/controllers/form_field_controller.js +7 -5
  39. data/dist/controllers/form_validation_controller.js +19 -13
  40. data/dist/controllers/frame_loading_controller.js +45 -8
  41. data/dist/controllers/highlight_controller.js +82 -25
  42. data/dist/controllers/hover_card_controller.js +40 -14
  43. data/dist/controllers/idle_controller.js +90 -5
  44. data/dist/controllers/input_mask_controller.js +65 -9
  45. data/dist/controllers/intersection_controller.js +3 -0
  46. data/dist/controllers/lazy_frame_controller.js +11 -2
  47. data/dist/controllers/listbox_controller.js +203 -45
  48. data/dist/controllers/local_time_controller.js +10 -5
  49. data/dist/controllers/masonry_controller.js +31 -15
  50. data/dist/controllers/menu_controller.js +45 -16
  51. data/dist/controllers/menubar_controller.js +58 -24
  52. data/dist/controllers/meter_controller.js +9 -5
  53. data/dist/controllers/multi_select_controller.js +278 -104
  54. data/dist/controllers/navigation_menu_controller.js +48 -15
  55. data/dist/controllers/nested_form_controller.js +37 -8
  56. data/dist/controllers/network_status_controller.js +9 -1
  57. data/dist/controllers/number_input_controller.js +124 -21
  58. data/dist/controllers/optimistic_controller.js +42 -5
  59. data/dist/controllers/otp_controller.js +198 -55
  60. data/dist/controllers/overflow_indicator_controller.js +115 -21
  61. data/dist/controllers/overflow_menu_controller.js +141 -44
  62. data/dist/controllers/pagination_controller.js +74 -28
  63. data/dist/controllers/password_reveal_controller.js +59 -2
  64. data/dist/controllers/persist_controller.js +30 -8
  65. data/dist/controllers/pointer_drag_controller.js +131 -52
  66. data/dist/controllers/popover_controller.js +45 -11
  67. data/dist/controllers/portal_controller.js +6 -2
  68. data/dist/controllers/preview_guard_controller.js +16 -1
  69. data/dist/controllers/progress_controller.js +8 -4
  70. data/dist/controllers/radio_group_controller.js +42 -17
  71. data/dist/controllers/range_slider_controller.js +88 -42
  72. data/dist/controllers/rating_controller.js +39 -15
  73. data/dist/controllers/read_more_controller.js +100 -7
  74. data/dist/controllers/reading_progress_controller.js +65 -19
  75. data/dist/controllers/relative_time_controller.js +10 -5
  76. data/dist/controllers/resizable_controller.js +82 -22
  77. data/dist/controllers/scroll_area_controller.js +75 -27
  78. data/dist/controllers/scroll_restore_controller.js +37 -16
  79. data/dist/controllers/scroll_visibility_controller.js +49 -30
  80. data/dist/controllers/scrollspy_controller.js +71 -26
  81. data/dist/controllers/separator_controller.js +66 -37
  82. data/dist/controllers/sidebar_controller.js +77 -18
  83. data/dist/controllers/skeleton_controller.js +6 -1
  84. data/dist/controllers/slider_controller.js +82 -47
  85. data/dist/controllers/smart_sticky_header_controller.js +60 -26
  86. data/dist/controllers/sortable_controller.js +17 -2
  87. data/dist/controllers/spinner_controller.js +10 -2
  88. data/dist/controllers/step_indicator_controller.js +18 -17
  89. data/dist/controllers/stepper_controller.js +101 -19
  90. data/dist/controllers/stick_to_bottom_controller.js +104 -8
  91. data/dist/controllers/submit_once_controller.js +45 -9
  92. data/dist/controllers/switch_controller.js +101 -10
  93. data/dist/controllers/tabs_controller.js +21 -2
  94. data/dist/controllers/tags_input_controller.js +209 -59
  95. data/dist/controllers/textarea_autosize_controller.js +29 -3
  96. data/dist/controllers/theme_controller.js +64 -14
  97. data/dist/controllers/time_picker_controller.js +23 -8
  98. data/dist/controllers/toast_controller.js +451 -105
  99. data/dist/controllers/toggle_group_controller.js +159 -23
  100. data/dist/controllers/toolbar_controller.js +32 -0
  101. data/dist/controllers/tooltip_controller.js +39 -13
  102. data/dist/controllers/transition_controller.js +4 -0
  103. data/dist/controllers/tree_view_controller.js +169 -16
  104. data/dist/index.js +5002 -1911
  105. data/dist/positioning/index.js +2 -0
  106. data/lib/stimeo/ui/version.rb +1 -1
  107. metadata +2 -2
@@ -230,24 +230,37 @@ var SeparatorController = class extends Controller {
230
230
  value: { type: Number, default: DEFAULT_MIN }
231
231
  };
232
232
  static actions = ["onKeydown"];
233
- static events = ["change"];
233
+ static events = ["change", "reconcile"];
234
234
  #role = new AttributeLease("role");
235
235
  #tabindex = new AttributeLease("tabindex");
236
236
  #orientation = new AttributeLease("aria-orientation");
237
237
  #minimum = new AttributeLease("aria-valuemin");
238
238
  #maximum = new AttributeLease("aria-valuemax");
239
239
  #current = new AttributeLease("aria-valuenow");
240
- /** Collapses a morph that changes several render Values into one silent repaint. */
240
+ /** Collapses a morph that changes several render Values into one repaint. */
241
241
  #repaint = new MicrotaskCoalescer(() => this.#render());
242
242
  /** Restores authored semantics before Turbo snapshots the element. */
243
243
  #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
244
- /** Hydrates legacy ARIA input once, normalizes the current Value, and renders. */
244
+ /**
245
+ * The baseline a published value is compared with: the value last published
246
+ * through `aria-valuenow`, or `null` while this connection has published none.
247
+ * It moves to each value a key the user presses or a repaint publishes. While
248
+ * nothing is published — the separator is decorative — it keeps the last
249
+ * value published, so a value that moved meanwhile is reported once when the
250
+ * separator publishes again; one that connected decorative keeps `null`, so
251
+ * its first published value is only taken as the baseline.
252
+ */
253
+ #settled = null;
254
+ /**
255
+ * Hydrates authored range ARIA into absent Values once and renders them
256
+ * without writing the normalized value back. What the first render publishes
257
+ * is the baseline, so connecting reports nothing.
258
+ */
245
259
  connect() {
246
260
  this.#hydrateValues();
247
261
  this.#repaint.activate();
248
262
  this.#beforeCache.activate();
249
- const value = this.#currentValue();
250
- if (!Object.is(this.valueValue, value)) this.valueValue = value;
263
+ this.#settled = null;
251
264
  this.#render();
252
265
  }
253
266
  /** Cancels repaint work and returns every semantic attribute this instance controlled. */
@@ -256,27 +269,27 @@ var SeparatorController = class extends Controller {
256
269
  this.#beforeCache.deactivate();
257
270
  this.#returnAttributes();
258
271
  }
259
- /** Silently repaints an orientation changed by application code or a Turbo morph. */
272
+ /** Repaints an orientation changed by application code or a Turbo morph. */
260
273
  orientationValueChanged() {
261
274
  this.#repaint.schedule();
262
275
  }
263
- /** Silently adds or removes the focusable range semantics at runtime. */
276
+ /** Adds or removes the focusable range semantics at runtime. */
264
277
  focusableValueChanged() {
265
278
  this.#repaint.schedule();
266
279
  }
267
- /** Silently repaints a minimum changed by application code or a Turbo morph. */
280
+ /** Repaints a minimum changed by application code or a Turbo morph. */
268
281
  minValueChanged() {
269
282
  this.#repaint.schedule();
270
283
  }
271
- /** Silently repaints a maximum changed by application code or a Turbo morph. */
284
+ /** Repaints a maximum changed by application code or a Turbo morph. */
272
285
  maxValueChanged() {
273
286
  this.#repaint.schedule();
274
287
  }
275
- /** Silently repaints a step changed by application code or a Turbo morph. */
288
+ /** Repaints a step changed by application code or a Turbo morph. */
276
289
  stepValueChanged() {
277
290
  this.#repaint.schedule();
278
291
  }
279
- /** Silently repaints a current value changed by application code or a Turbo morph. */
292
+ /** Repaints a current value changed by application code or a Turbo morph. */
280
293
  valueValueChanged() {
281
294
  this.#repaint.schedule();
282
295
  }
@@ -285,20 +298,20 @@ var SeparatorController = class extends Controller {
285
298
  if (isReservedArrowChord(event) || !this.focusableValue) return;
286
299
  const horizontal = this.#effectiveOrientation === "horizontal";
287
300
  const range = this.#effectiveRange;
288
- const current = snapSteppedValue(this.valueValue, range);
301
+ const declared = this.valueValue;
289
302
  let next = null;
290
303
  switch (event.key) {
291
304
  case "ArrowUp":
292
- if (horizontal) next = stepSteppedValue(current, 1, range);
305
+ if (horizontal) next = stepSteppedValue(declared, 1, range);
293
306
  break;
294
307
  case "ArrowDown":
295
- if (horizontal) next = stepSteppedValue(current, -1, range);
308
+ if (horizontal) next = stepSteppedValue(declared, -1, range);
296
309
  break;
297
310
  case "ArrowRight":
298
- if (!horizontal) next = stepSteppedValue(current, 1, range);
311
+ if (!horizontal) next = stepSteppedValue(declared, 1, range);
299
312
  break;
300
313
  case "ArrowLeft":
301
- if (!horizontal) next = stepSteppedValue(current, -1, range);
314
+ if (!horizontal) next = stepSteppedValue(declared, -1, range);
302
315
  break;
303
316
  case "Home":
304
317
  next = range.min;
@@ -311,36 +324,51 @@ var SeparatorController = class extends Controller {
311
324
  }
312
325
  if (next === null) return;
313
326
  event.preventDefault();
314
- this.#commit(next, current);
327
+ this.#commit(next);
315
328
  }
316
- /** Stores a normalized user value, renders immediately, and reports real changes. */
317
- #commit(raw, previous) {
329
+ /**
330
+ * Stores a normalized user value, renders immediately, and reports it as
331
+ * `change` when it differs from the value last published. The comparison is
332
+ * with the value last published, not with the value the key started from, so
333
+ * a page write no repaint has published yet is reported once, as this change,
334
+ * and a key that ends where the value was last published reports nothing. The
335
+ * baseline moves before the render and the report, so neither the repaint the
336
+ * Value write schedules nor a move a subscriber makes while `change` is
337
+ * dispatched is measured from the value this key replaced.
338
+ */
339
+ #commit(raw) {
318
340
  const value = snapSteppedValue(raw, this.#effectiveRange);
341
+ const reported = this.#settled;
342
+ this.#settled = value;
319
343
  if (!Object.is(this.valueValue, value)) this.valueValue = value;
320
344
  this.#render();
321
- if (value !== previous) this.dispatch("change", { detail: { value } });
345
+ if (value !== reported) this.dispatch("change", { detail: { value } });
322
346
  }
323
347
  /**
324
- * Reflects live Values into the connected element without dispatching.
348
+ * Reflects live Values into the connected element without writing them back,
349
+ * then compares what it published with the baseline. A decorative separator
350
+ * publishes no value (`null` here): the baseline stays the last value
351
+ * published. A value is reported once as `reconcile` only when it moved from a
352
+ * published baseline, so the first value a connection publishes is only taken
353
+ * as the baseline.
325
354
  *
326
355
  * @stimeoRenderRoot
327
356
  */
328
357
  #render() {
329
358
  const range = this.#effectiveRange;
330
- const value = snapSteppedValue(this.valueValue, range);
359
+ const focusable = this.focusableValue;
360
+ const published = focusable ? snapSteppedValue(this.valueValue, range) : null;
331
361
  this.#role.write(this.element, "separator");
332
362
  this.#orientation.write(this.element, this.#effectiveOrientation);
333
- if (!this.focusableValue) {
334
- this.#tabindex.write(this.element, null);
335
- this.#minimum.write(this.element, null);
336
- this.#maximum.write(this.element, null);
337
- this.#current.write(this.element, null);
338
- return;
363
+ this.#tabindex.write(this.element, focusable ? "0" : null);
364
+ this.#minimum.write(this.element, focusable ? String(range.min) : null);
365
+ this.#maximum.write(this.element, focusable ? String(range.max) : null);
366
+ this.#current.write(this.element, published === null ? null : String(published));
367
+ const previous = this.#settled;
368
+ if (published !== null) this.#settled = published;
369
+ if (previous !== null && published !== null && published !== previous) {
370
+ this.dispatch("reconcile", { detail: { value: published } });
339
371
  }
340
- this.#tabindex.write(this.element, "0");
341
- this.#minimum.write(this.element, String(range.min));
342
- this.#maximum.write(this.element, String(range.max));
343
- this.#current.write(this.element, String(value));
344
372
  }
345
373
  /** Effective orientation; unknown strings fall back to the horizontal contract. */
346
374
  get #effectiveOrientation() {
@@ -357,11 +385,12 @@ var SeparatorController = class extends Controller {
357
385
  base: min
358
386
  };
359
387
  }
360
- /** Current normalized value derived from the live declarative inputs. */
361
- #currentValue() {
362
- return snapSteppedValue(this.valueValue, this.#effectiveRange);
363
- }
364
- /** Copies valid authored ARIA into Values only when no explicit Value exists. */
388
+ /**
389
+ * Copies valid authored ARIA into Values only when no explicit Value exists.
390
+ * This fill is the one Value write `connect()` makes: it never overwrites a
391
+ * declaration, and no Value callback runs it, so a declaration the page
392
+ * changes later is only ever read.
393
+ */
365
394
  #hydrateValues() {
366
395
  if (!this.#hasInput("orientation")) {
367
396
  const orientation = this.element.getAttribute("aria-orientation");
@@ -382,6 +382,16 @@ function writeLocalStorage(key, value) {
382
382
  }
383
383
  }
384
384
 
385
+ // src/utils/state_reason.ts
386
+ var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
387
+ var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
388
+ function stateReasonFor(event) {
389
+ if (!event) return "api";
390
+ if (FOCUS_EVENTS.has(event.type)) return "focus";
391
+ if (POINTER_EVENTS.has(event.type)) return "pointer";
392
+ return "user";
393
+ }
394
+
385
395
  // src/utils/safe_timeout.ts
386
396
  var TimerRegistry = class {
387
397
  /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
@@ -585,11 +595,12 @@ var SidebarController = class extends Controller {
585
595
  collapsed: { type: Boolean, default: false }
586
596
  };
587
597
  static actions = ["close", "open", "toggle"];
598
+ static events = ["close", "open", "reconcile"];
588
599
  /** Exact panel currently owned by the modal lifecycle (survives target churn safely). */
589
600
  #activePanel = null;
590
601
  /** Owns the overlay modal side effects; Escape closes, focus falls to trigger. */
591
602
  #trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {
592
- onEscape: () => this.close(),
603
+ onEscape: () => this.#closeOverlay("escape"),
593
604
  fallbackFocus: () => this.hasTriggerTarget ? this.triggerTarget : null
594
605
  });
595
606
  /** Current responsive mode. */
@@ -604,6 +615,8 @@ var SidebarController = class extends Controller {
604
615
  #transition = new TransitionCompletion();
605
616
  /** Distinguishes dynamic target churn from callbacks around controller teardown. */
606
617
  #connected = false;
618
+ /** Whether state moves are reported: set once `connect()` settled the baseline. */
619
+ #reporting = false;
607
620
  connect() {
608
621
  this.#connected = true;
609
622
  this.#beforeCache.activate();
@@ -612,10 +625,12 @@ var SidebarController = class extends Controller {
612
625
  this.#mqlQuery = this.#breakpointQuery;
613
626
  this.#mql = this.#matchBreakpoint(this.#mqlQuery);
614
627
  this.#mql?.addEventListener("change", this.#onMediaChange);
615
- this.#applyMode(this.#computeMode());
628
+ this.#applyMode(this.#computeMode(), false);
629
+ this.#reporting = true;
616
630
  }
617
631
  disconnect() {
618
632
  this.#connected = false;
633
+ this.#reporting = false;
619
634
  this.#beforeCache.deactivate();
620
635
  this.#mql?.removeEventListener("change", this.#onMediaChange);
621
636
  this.#mql = null;
@@ -658,7 +673,7 @@ var SidebarController = class extends Controller {
658
673
  this.#mql = this.#matchBreakpoint(query);
659
674
  this.#mql?.addEventListener("change", this.#onMediaChange);
660
675
  const next = this.#computeMode();
661
- if (next !== this.#mode) this.#applyMode(next);
676
+ if (next !== this.#mode) this.#applyMode(next, true);
662
677
  }
663
678
  /**
664
679
  * Sanitizes overlay markup before Turbo snapshots the page.
@@ -675,26 +690,44 @@ var SidebarController = class extends Controller {
675
690
  this.#setOverlayClosedImmediate();
676
691
  });
677
692
  /** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */
678
- toggle() {
693
+ toggle(event) {
694
+ const reason = stateReasonFor(event);
679
695
  if (this.#isOverlay) {
680
- this.#isOverlayOpen ? this.#closeOverlay() : this.#openOverlay();
696
+ this.#isOverlayOpen ? this.#closeOverlay(reason) : this.#openOverlay(reason);
681
697
  } else {
682
- this.#setCollapsed(!this.#collapsed);
698
+ this.#setCollapsed(!this.#collapsed, reason);
683
699
  }
684
700
  }
685
701
  /** Shows the panel (inline: expand; overlay: open). */
686
- open() {
687
- if (this.#isOverlay) this.#openOverlay();
688
- else this.#setCollapsed(false);
702
+ open(event) {
703
+ const reason = stateReasonFor(event);
704
+ if (this.#isOverlay) this.#openOverlay(reason);
705
+ else this.#setCollapsed(false, reason);
689
706
  }
690
707
  /** Hides the panel (inline: collapse; overlay: close). */
691
- close() {
692
- if (this.#isOverlay) this.#closeOverlay();
693
- else this.#setCollapsed(true);
708
+ close(event) {
709
+ const reason = this.#closeReason(event);
710
+ if (this.#isOverlay) this.#closeOverlay(reason);
711
+ else this.#setCollapsed(true, reason);
712
+ }
713
+ /**
714
+ * Reads why a close happened. The backdrop has no action of its own — the
715
+ * consumer wires this one onto it — so the shared `"outside"` vocabulary is
716
+ * only reachable by recognising that target here.
717
+ */
718
+ #closeReason(event) {
719
+ if (this.hasBackdropTarget && event?.currentTarget === this.backdropTarget) return "outside";
720
+ return stateReasonFor(event);
694
721
  }
695
722
  // --- Mode handling ---------------------------------------------------------
696
- /** Re-renders the closed/default state for `mode` and records it. */
697
- #applyMode(mode) {
723
+ /**
724
+ * Re-renders the closed/default state for `mode` and records it.
725
+ *
726
+ * @param report - Whether the mode change is one the consumer can observe: a
727
+ * viewport change re-derives the state under them, the connect-time baseline
728
+ * does not.
729
+ */
730
+ #applyMode(mode, report) {
698
731
  this.#mode = mode;
699
732
  if (this.hasPanelTarget) this.panelTarget.setAttribute("data-mode", mode);
700
733
  if (mode === "inline") {
@@ -708,6 +741,12 @@ var SidebarController = class extends Controller {
708
741
  this.#trap.deactivate({ restoreFocus: false });
709
742
  this.#setOverlayClosedImmediate();
710
743
  }
744
+ if (report && this.#reporting) {
745
+ this.dispatch("reconcile", {
746
+ detail: { mode, open: this.#isExpanded },
747
+ cancelable: false
748
+ });
749
+ }
711
750
  }
712
751
  /** Reconciles a replacement panel with the current responsive mode and DOM state. */
713
752
  #adoptPanel(panel) {
@@ -742,7 +781,7 @@ var SidebarController = class extends Controller {
742
781
  }
743
782
  #onMediaChange = (event) => {
744
783
  const next = event.matches ? "inline" : "overlay";
745
- if (next !== this.#mode) this.#applyMode(next);
784
+ if (next !== this.#mode) this.#applyMode(next, true);
746
785
  };
747
786
  #computeMode() {
748
787
  return this.#mql?.matches ?? true ? "inline" : "overlay";
@@ -753,10 +792,11 @@ var SidebarController = class extends Controller {
753
792
  }
754
793
  // --- Inline (rail) ---------------------------------------------------------
755
794
  /** Sets, reflects, and persists the inline collapsed preference. */
756
- #setCollapsed(collapsed) {
795
+ #setCollapsed(collapsed, reason) {
757
796
  if (collapsed === this.#collapsed) return;
758
797
  this.#collapsed = collapsed;
759
798
  this.#applyInlineState(collapsed);
799
+ this.#report(!collapsed, reason);
760
800
  this.#persistCollapsed(collapsed);
761
801
  }
762
802
  /** Reflects the inline rail state onto the panel and trigger. */
@@ -768,7 +808,7 @@ var SidebarController = class extends Controller {
768
808
  }
769
809
  // --- Overlay (off-canvas modal) -------------------------------------------
770
810
  /** Opens the overlay: reveal it, commit a starting frame, then trap focus. */
771
- #openOverlay() {
811
+ #openOverlay(reason) {
772
812
  if (!this.hasPanelTarget || this.#isOverlayOpen) return;
773
813
  this.#transition.cancel();
774
814
  this.#activePanel = this.panelTarget;
@@ -777,13 +817,17 @@ var SidebarController = class extends Controller {
777
817
  void this.panelTarget.offsetWidth;
778
818
  this.#setOverlayState("open");
779
819
  this.#setExpandedAttr(true);
820
+ this.#report(true, reason);
821
+ if (!this.#isOverlayOpen) return;
780
822
  this.#trap.activate();
781
823
  }
782
824
  /** Closes the overlay: start the exit transition, defer hide + trap teardown. */
783
- #closeOverlay() {
825
+ #closeOverlay(reason) {
784
826
  if (!this.hasPanelTarget || !this.#isOverlayOpen) return;
785
827
  this.#setOverlayState("closed");
786
828
  this.#setExpandedAttr(false);
829
+ this.#report(false, reason);
830
+ if (this.#isOverlayOpen) return;
787
831
  this.#hideAfterTransition();
788
832
  }
789
833
  /** Renders the overlay closed state up front (used when entering overlay mode). */
@@ -826,6 +870,17 @@ var SidebarController = class extends Controller {
826
870
  this.triggerTarget.setAttribute("aria-expanded", expanded ? "true" : "false");
827
871
  }
828
872
  }
873
+ /** Reports a move of the expanded state, naming the mode it happened in. */
874
+ #report(open, reason) {
875
+ if (!this.#reporting) return;
876
+ const detail = { reason, mode: this.#mode };
877
+ if (open) this.dispatch("open", { detail, cancelable: false });
878
+ else this.dispatch("close", { detail, cancelable: false });
879
+ }
880
+ /** Whether the panel currently counts as expanded in the mode it is in. */
881
+ get #isExpanded() {
882
+ return this.#isOverlay ? this.#isOverlayOpen : !this.#collapsed;
883
+ }
829
884
  /**
830
885
  * Resolves the inline collapsed preference, in priority order:
831
886
  * 1. the persisted value (when a `key` is set and storage is readable),
@@ -851,6 +906,10 @@ var SidebarController = class extends Controller {
851
906
  if (!key) return;
852
907
  writeLocalStorage(key, collapsed ? "1" : "0");
853
908
  }
909
+ /**
910
+ * @stimeoRuntimeOnly `key` names the storage slot the collapsed state is saved under; the state
911
+ * itself comes from the user.
912
+ */
854
913
  get #storageKey() {
855
914
  return this.keyValue ? `stimeo--sidebar:${this.keyValue}` : "";
856
915
  }
@@ -215,7 +215,12 @@ var SkeletonController = class extends Controller {
215
215
  this.element.setAttribute("aria-busy", "true");
216
216
  this.element.setAttribute("data-state", "loading");
217
217
  }
218
- /** Hides the placeholder, shows content, and clears the busy state. */
218
+ /**
219
+ * Hides the placeholder, shows content, and clears the busy state.
220
+ *
221
+ * @stimeoRuntimeOnly `announceReadyText` is the wording of the one announcement this reveal
222
+ * makes.
223
+ */
219
224
  #reveal() {
220
225
  if (this.hasPlaceholderTarget) this.placeholderTarget.hidden = true;
221
226
  if (this.hasContentTarget) this.contentTarget.hidden = false;