stimeo-ui 0.2.0 → 0.2.1

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 (124) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/controllers/accordion_controller.d.ts +3 -2
  3. package/dist/controllers/accordion_controller.js.map +1 -1
  4. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  5. package/dist/controllers/alert_dialog_controller.js +318 -0
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  7. package/dist/controllers/carousel_controller.d.ts +100 -0
  8. package/dist/controllers/carousel_controller.js +272 -0
  9. package/dist/controllers/carousel_controller.js.map +1 -0
  10. package/dist/controllers/clipboard_controller.d.ts +72 -0
  11. package/dist/controllers/clipboard_controller.js +144 -0
  12. package/dist/controllers/clipboard_controller.js.map +1 -0
  13. package/dist/controllers/collapsible_controller.d.ts +70 -0
  14. package/dist/controllers/collapsible_controller.js +327 -0
  15. package/dist/controllers/collapsible_controller.js.map +1 -0
  16. package/dist/controllers/color_picker_controller.d.ts +76 -0
  17. package/dist/controllers/color_picker_controller.js +213 -0
  18. package/dist/controllers/color_picker_controller.js.map +1 -0
  19. package/dist/controllers/count_up_controller.js +8 -1
  20. package/dist/controllers/count_up_controller.js.map +1 -1
  21. package/dist/controllers/currency_input_controller.d.ts +72 -0
  22. package/dist/controllers/currency_input_controller.js +147 -0
  23. package/dist/controllers/currency_input_controller.js.map +1 -0
  24. package/dist/controllers/data_grid_controller.d.ts +71 -0
  25. package/dist/controllers/data_grid_controller.js +168 -0
  26. package/dist/controllers/data_grid_controller.js.map +1 -0
  27. package/dist/controllers/date_range_picker_controller.d.ts +87 -0
  28. package/dist/controllers/date_range_picker_controller.js +417 -0
  29. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  30. package/dist/controllers/dismissible_controller.d.ts +57 -0
  31. package/dist/controllers/dismissible_controller.js +117 -0
  32. package/dist/controllers/dismissible_controller.js.map +1 -0
  33. package/dist/controllers/drawer_controller.d.ts +96 -0
  34. package/dist/controllers/drawer_controller.js +630 -0
  35. package/dist/controllers/drawer_controller.js.map +1 -0
  36. package/dist/controllers/editable_controller.d.ts +73 -0
  37. package/dist/controllers/editable_controller.js +168 -0
  38. package/dist/controllers/editable_controller.js.map +1 -0
  39. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  40. package/dist/controllers/file_dropzone_controller.js +165 -0
  41. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  42. package/dist/controllers/filter_controller.d.ts +60 -0
  43. package/dist/controllers/filter_controller.js +86 -0
  44. package/dist/controllers/filter_controller.js.map +1 -0
  45. package/dist/controllers/flash_controller.js +36 -5
  46. package/dist/controllers/flash_controller.js.map +1 -1
  47. package/dist/controllers/highlight_controller.js +6 -4
  48. package/dist/controllers/highlight_controller.js.map +1 -1
  49. package/dist/controllers/intersection_controller.js +41 -18
  50. package/dist/controllers/intersection_controller.js.map +1 -1
  51. package/dist/controllers/lazy_frame_controller.js +33 -11
  52. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  53. package/dist/controllers/masonry_controller.d.ts +54 -0
  54. package/dist/controllers/masonry_controller.js +142 -0
  55. package/dist/controllers/masonry_controller.js.map +1 -0
  56. package/dist/controllers/menubar_controller.d.ts +62 -0
  57. package/dist/controllers/menubar_controller.js +433 -0
  58. package/dist/controllers/menubar_controller.js.map +1 -0
  59. package/dist/controllers/multi_select_controller.d.ts +115 -0
  60. package/dist/controllers/multi_select_controller.js +472 -0
  61. package/dist/controllers/multi_select_controller.js.map +1 -0
  62. package/dist/controllers/navigation_menu_controller.d.ts +99 -0
  63. package/dist/controllers/navigation_menu_controller.js +384 -0
  64. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  65. package/dist/controllers/overflow_indicator_controller.d.ts +12 -4
  66. package/dist/controllers/overflow_indicator_controller.js +178 -27
  67. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  68. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  69. package/dist/controllers/password_reveal_controller.js +117 -0
  70. package/dist/controllers/password_reveal_controller.js.map +1 -0
  71. package/dist/controllers/range_slider_controller.d.ts +87 -0
  72. package/dist/controllers/range_slider_controller.js +166 -0
  73. package/dist/controllers/range_slider_controller.js.map +1 -0
  74. package/dist/controllers/read_more_controller.d.ts +52 -0
  75. package/dist/controllers/read_more_controller.js +194 -0
  76. package/dist/controllers/read_more_controller.js.map +1 -0
  77. package/dist/controllers/scroll_area_controller.js +15 -2
  78. package/dist/controllers/scroll_area_controller.js.map +1 -1
  79. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  80. package/dist/controllers/scroll_restore_controller.js +93 -0
  81. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  82. package/dist/controllers/scroll_visibility_controller.js +8 -4
  83. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  84. package/dist/controllers/scrollspy_controller.js +33 -11
  85. package/dist/controllers/scrollspy_controller.js.map +1 -1
  86. package/dist/controllers/separator_controller.d.ts +54 -0
  87. package/dist/controllers/separator_controller.js +87 -0
  88. package/dist/controllers/separator_controller.js.map +1 -0
  89. package/dist/controllers/sidebar_controller.d.ts +107 -0
  90. package/dist/controllers/sidebar_controller.js +761 -0
  91. package/dist/controllers/sidebar_controller.js.map +1 -0
  92. package/dist/controllers/stepper_controller.d.ts +4 -0
  93. package/dist/controllers/stepper_controller.js +28 -12
  94. package/dist/controllers/stepper_controller.js.map +1 -1
  95. package/dist/controllers/stick_to_bottom_controller.js +8 -4
  96. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  97. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  98. package/dist/controllers/sticky_observer_controller.js +88 -20
  99. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  100. package/dist/controllers/tabs_controller.d.ts +3 -2
  101. package/dist/controllers/tabs_controller.js.map +1 -1
  102. package/dist/controllers/tags_input_controller.d.ts +91 -0
  103. package/dist/controllers/tags_input_controller.js +275 -0
  104. package/dist/controllers/tags_input_controller.js.map +1 -0
  105. package/dist/controllers/theme_controller.js +20 -10
  106. package/dist/controllers/theme_controller.js.map +1 -1
  107. package/dist/controllers/time_picker_controller.d.ts +67 -0
  108. package/dist/controllers/time_picker_controller.js +212 -0
  109. package/dist/controllers/time_picker_controller.js.map +1 -0
  110. package/dist/controllers/toast_controller.d.ts +1 -9
  111. package/dist/controllers/toast_controller.js +36 -9
  112. package/dist/controllers/toast_controller.js.map +1 -1
  113. package/dist/controllers/transition_controller.d.ts +14 -8
  114. package/dist/controllers/transition_controller.js +153 -38
  115. package/dist/controllers/transition_controller.js.map +1 -1
  116. package/dist/controllers/tree_view_controller.d.ts +54 -0
  117. package/dist/controllers/tree_view_controller.js +275 -0
  118. package/dist/controllers/tree_view_controller.js.map +1 -0
  119. package/dist/index.d.ts +27 -1818
  120. package/dist/index.js +811 -295
  121. package/dist/index.js.map +1 -1
  122. package/dist/inspector/examples.json +2 -2
  123. package/dist/inspector/manifest.json +15 -3
  124. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/focus_trap.ts","../../src/utils/safe_storage.ts","../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/sidebar_controller.ts"],"names":[],"mappings":"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","file":"sidebar_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest-first half of the\n * shared layered-Escape contract. A keydown that is part of an IME composition\n * (`event.isComposing`) cancels the composition, never a layer, and is ignored\n * here for every layer at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","import { EscapeLayer } from \"./escape_layer\";\n\n/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / confirm / drawer / command-palette / sidebar); the\n * non-modal focus scope (focus) reuses it with the modal side effects opted out.\n *\n * The WAI-ARIA APG modal pattern is more than \"cycle Tab inside a box\": a modal\n * also locks background scroll, makes the rest of the page `inert` (so assistive\n * technology and pointer/Tab cannot reach it, honoring `aria-modal=\"true\"`),\n * sends focus inside on open, and restores it to the opener on close — and every\n * one of those side effects must be reverted if the element is torn down while\n * open (a Turbo navigation mid-dialog) and kept out of the snapshot Turbo\n * caches (`turbo:before-cache`). {@link FocusTrap} owns that whole modal\n * lifecycle so each controller only decides *when* to open/close and *what*\n * \"close\" means.\n *\n * It is intentionally **policy-free about closing**. Escape semantics differ per\n * widget (a plain dialog just closes; an alert-dialog closes *as a cancel* with a\n * reason; a drawer runs an exit transition), so the trap merely forwards Escape\n * to an {@link FocusTrapOptions.onEscape | onEscape} callback and never decides on\n * its own what closing entails.\n *\n * @remarks\n * The container is read through a getter so a controller can hand over a Stimulus\n * target without worrying about when the trap instance is constructed relative to\n * `connect()`.\n */\n\n/**\n * Selector matching the elements considered focusable. Shared by the trap's Tab\n * cycling and by form-validation's invalid-focus delegation.\n */\nexport const FOCUSABLE =\n 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback (the shared layered-Escape contract).\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n document.addEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n document.removeEventListener(\"turbo:before-cache\", this.#onBeforeCache);\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #onBeforeCache = (): void => {\n this.deactivate({ restoreFocus: false });\n };\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (const sibling of Array.from(document.body.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling.contains(container) || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return Array.from(this.#getContainer().querySelectorAll<HTMLElement>(FOCUSABLE)).filter(\n (el) => !el.hidden,\n );\n }\n}\n","/**\n * Guarded `localStorage` access shared by the preference-persisting controllers\n * (sidebar, theme).\n *\n * `window.localStorage` can throw on access or read/write — storage disabled by\n * browser policy, sandboxed frames, private modes, or quota exhaustion. These\n * helpers centralize the try/catch so callers treat persistence as best-effort:\n * a failed read behaves like an unset key and a failed write is dropped,\n * leaving the in-DOM state as this session's source of truth.\n *\n * @remarks\n * Only the guarded access lives here — interpreting the stored string\n * (validation, defaults, key derivation) stays in each controller. The\n * `stimeo--persist` controller keeps its own storage layer on purpose: storage\n * *is* its domain (engine selection, per-field bookkeeping, TTL semantics).\n */\n\n/**\n * Reads `key` from `localStorage`.\n *\n * @param key - The storage key to read.\n * @returns The stored string, or `null` when the key is unset or storage is\n * unavailable (the read threw).\n */\nexport function readLocalStorage(key: string): string | null {\n try {\n return window.localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\n/**\n * Writes `value` under `key` in `localStorage`, ignoring failures (private\n * mode / quota / storage disabled) — persistence is best-effort by contract.\n *\n * @param key - The storage key to write.\n * @param value - The string to store.\n */\nexport function writeLocalStorage(key: string, value: string): void {\n try {\n window.localStorage.setItem(key, value);\n } catch {\n // Best-effort: the in-DOM state still applies for this session.\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { SafeTimeout } from \"./safe_timeout\";\n\n/** Computed transition fields needed to determine when an element has settled. */\nexport type TransitionStyle = Pick<\n CSSStyleDeclaration,\n \"transitionProperty\" | \"transitionDuration\" | \"transitionDelay\"\n>;\n\n/** Parsed transition property paired with its effective duration and delay. */\ninterface TransitionTiming {\n property: string;\n totalMs: number;\n}\n\n/** Parses a CSS `<time>` into milliseconds, returning `0` for malformed values. */\nfunction timeMs(value: string): number {\n const trimmed = value.trim();\n const amount = Number.parseFloat(trimmed);\n if (!Number.isFinite(amount)) return 0;\n if (trimmed.endsWith(\"ms\")) return amount;\n if (trimmed.endsWith(\"s\")) return amount * 1000;\n return 0;\n}\n\n/** Splits a computed comma-separated CSS list into normalized, non-empty items. */\nfunction cssList(value: string): string[] {\n return value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\n/**\n * Pairs transition properties, durations, and delays using CSS list repetition.\n *\n * The `transition-property` list determines the number of transitions. Shorter\n * duration/delay lists repeat, while extra values are ignored. A negative delay\n * shortens the remaining wall-clock time, and a non-positive total is settled.\n */\nfunction transitionTimings(style: TransitionStyle): TransitionTiming[] {\n const properties = cssList(style.transitionProperty);\n const durations = cssList(style.transitionDuration).map(timeMs);\n const delays = cssList(style.transitionDelay).map(timeMs);\n const effectiveProperties =\n properties.length > 0\n ? properties\n : Array.from({ length: Math.max(durations.length, delays.length, 1) }, () => \"all\");\n const effectiveDurations = durations.length > 0 ? durations : [0];\n const effectiveDelays = delays.length > 0 ? delays : [0];\n\n return effectiveProperties\n .filter((property) => property !== \"none\")\n .map((property, index) => ({\n property,\n totalMs: Math.max(\n 0,\n (effectiveDurations[index % effectiveDurations.length] ?? 0) +\n (effectiveDelays[index % effectiveDelays.length] ?? 0),\n ),\n }));\n}\n\n/** Maximum total over already-parsed timings (0 when the list is empty). */\nfunction maxTotalMs(timings: TransitionTiming[]): number {\n return timings.reduce((max, { totalMs }) => Math.max(max, totalMs), 0);\n}\n\n/** Maximum effective transition duration plus delay, in milliseconds. */\nexport function maxTransitionTotalMs(style: TransitionStyle): number {\n return maxTotalMs(transitionTimings(style));\n}\n\n/** Optional tuning for a single {@link TransitionCompletion.wait} call. */\nexport interface TransitionWaitOptions {\n /**\n * Positive wall-clock override (ms) for the bounded fallback, replacing the\n * auto-computed `max + 50ms` timer verbatim. It also keeps the wait armed for\n * a computed 0ms transition (no synchronous completion): the consumer asserts\n * a transition budget that computed styles cannot see. Non-positive and\n * non-finite (`NaN` / `Infinity`) values are ignored.\n */\n timeoutMs?: number;\n}\n\n/**\n * Owns one cancellable wait for an element's CSS transitions to settle.\n *\n * When available, active CSS transitions provide their expanded property names.\n * Tracking those names lets shorthand declarations settle from their longhand\n * terminal events. A terminal event from an interrupted phase is ignored while a\n * replacement transition for the same property is still active. Because that set\n * is sampled when the wait is armed, a property whose transition only starts a\n * frame later would otherwise be settled over; the still-running set is therefore\n * re-read before completing and adopted when it is not yet empty. Without Web\n * Animations data, explicit transition properties retain the declared-property\n * behavior and the ambiguous `transition-property: all` form waits until the\n * computed maximum wall-clock time. A `max + 50ms` timeout is always armed\n * because browsers emit no terminal event when a property never transitions, an\n * ancestor disappears, or rendering is otherwise interrupted; a consumer with\n * better knowledge can replace that fallback via\n * {@link TransitionWaitOptions.timeoutMs}.\n */\nexport class TransitionCompletion {\n readonly #timers = new SafeTimeout();\n #element: HTMLElement | null = null;\n #complete: (() => void) | null = null;\n #pendingProperties: Set<string> | null = null;\n #deadline = 0;\n\n /**\n * Replaces any prior wait and invokes `complete` synchronously for a 0ms\n * transition (including when `getComputedStyle` is unavailable).\n *\n * With a positive `options.timeoutMs` the synchronous fast-path is skipped and\n * the override replaces the auto-computed fallback (see {@link TransitionWaitOptions}).\n */\n wait(element: HTMLElement, complete: () => void, options: TransitionWaitOptions = {}): void {\n this.cancel();\n\n const requested = options.timeoutMs ?? 0;\n const override = Number.isFinite(requested) && requested > 0 ? requested : 0;\n // A non-browser / partial-DOM environment cannot report a computable\n // transition, which is indistinguishable from a 0ms one.\n const timings =\n typeof window.getComputedStyle === \"function\"\n ? transitionTimings(window.getComputedStyle(element))\n : [];\n const maximum = maxTotalMs(timings);\n if (maximum <= 0 && override <= 0) {\n complete();\n return;\n }\n\n this.#element = element;\n this.#complete = complete;\n this.#deadline = Date.now() + maximum;\n const activeProperties = this.#activeTransitionProperties(element);\n this.#pendingProperties =\n activeProperties.size > 0 ? activeProperties : this.#explicitPendingProperties(timings);\n element.addEventListener(\"transitionend\", this.#onTerminal);\n element.addEventListener(\"transitioncancel\", this.#onTerminal);\n this.#timers.set(() => this.#finish(), override > 0 ? override : maximum + 50);\n }\n\n /** Cancels the pending wait without invoking its completion callback. */\n cancel(): void {\n this.#complete = null;\n this.#teardown();\n }\n\n /**\n * Handles terminal events from the observed element only.\n *\n * For explicit property lists, every declared positive-time property must\n * settle. For `all`, no reliable property set exists, so an event can finish\n * only after the computed maximum time; the safety timeout owns the usual path.\n */\n readonly #onTerminal = (event: Event): void => {\n if (event.target !== this.#element) return;\n const transitionEvent = event as TransitionEvent;\n // A pseudo-element's transition is reported with its originating element\n // as `target`; it must not settle the element's own transition.\n if (transitionEvent.pseudoElement) return;\n\n if (this.#pendingProperties) {\n const propertyName = transitionEvent.propertyName;\n const active = this.#activeTransitionProperties(this.#element);\n // An interrupted transition queues its cancel event before the replacement\n // phase starts, but dispatch can happen after the new wait is armed. The\n // replacement is then the active transition for this property, so the stale\n // terminal event must not settle the new phase.\n if (active.has(propertyName)) return;\n if (!this.#pendingProperties.delete(propertyName)) return;\n if (this.#pendingProperties.size > 0) return;\n // The tracked set is an arm-time snapshot, so a transition the phase starts\n // one frame later is absent from it. Re-reading the still-running set before\n // settling adopts those stragglers instead of completing over them; the\n // fallback timer stays armed, so this cannot extend the wait unboundedly.\n if (active.size > 0) {\n this.#pendingProperties = active;\n return;\n }\n this.#finish();\n return;\n }\n\n if (Date.now() >= this.#deadline) this.#finish();\n };\n\n /**\n * Returns active CSS transition properties expanded to the names reported by\n * terminal events. CSS animations and pseudo-element effects are excluded.\n */\n #activeTransitionProperties(element: HTMLElement | null): Set<string> {\n if (!element || typeof element.getAnimations !== \"function\") return new Set();\n\n try {\n const properties = element.getAnimations().flatMap((animation) => {\n if (animation.playState === \"idle\" || animation.playState === \"finished\") return [];\n const effect = animation.effect as KeyframeEffect | null;\n // A pseudo-element effect names the originating element as its target, so\n // `target` alone cannot separate it. Its terminal events are ignored, so\n // tracking its properties would only strand the wait on the fallback.\n if (effect?.pseudoElement) return [];\n const target = effect?.target;\n if (target && target !== element) return [];\n const property = (animation as Partial<CSSTransition>).transitionProperty;\n return typeof property === \"string\" && property.length > 0 ? [property] : [];\n });\n return new Set(properties);\n } catch {\n // Partial DOM implementations can expose getAnimations without supporting\n // it. The declared-property tracker and bounded timer remain safe fallbacks.\n return new Set();\n }\n }\n\n /** Returns explicit positive-time properties, or `null` for the ambiguous `all`. */\n #explicitPendingProperties(timings: TransitionTiming[]): Set<string> | null {\n if (timings.some(({ property }) => property === \"all\")) return null;\n const pending = new Set(\n timings.filter(({ totalMs }) => totalMs > 0).map(({ property }) => property),\n );\n // An empty set is only reachable through a timeout override (a 0ms transition\n // otherwise completes synchronously). With nothing to track, fall back to the\n // deadline rule so any terminal event can settle the wait, exactly like `all`.\n return pending.size > 0 ? pending : null;\n }\n\n /** Completes exactly once, releasing listeners and the fallback before the callback. */\n #finish(): void {\n const complete = this.#complete;\n if (!complete) return;\n this.#complete = null;\n this.#teardown();\n complete();\n }\n\n /** Releases the exact element listeners and timer owned by the current wait. */\n #teardown(): void {\n this.#timers.clearAll();\n this.#element?.removeEventListener(\"transitionend\", this.#onTerminal);\n this.#element?.removeEventListener(\"transitioncancel\", this.#onTerminal);\n this.#element = null;\n this.#pendingProperties = null;\n this.#deadline = 0;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\nimport { readLocalStorage, writeLocalStorage } from \"../utils/safe_storage\";\nimport { TransitionCompletion } from \"../utils/transition_completion\";\n\n/** Current responsive mode, driven by a `min-width` media query. */\ntype Mode = \"inline\" | \"overlay\";\n\n/**\n * Headless, accessible **responsive collapsible sidebar** behavior.\n *\n * Markup contract (identifier: `stimeo--sidebar`):\n * <div data-controller=\"stimeo--sidebar\"\n * data-action=\"turbo:before-cache@document->stimeo--sidebar#beforeCache\"\n * data-stimeo--sidebar-breakpoint-value=\"768\"\n * data-stimeo--sidebar-key-value=\"main-nav\">\n * <header>\n * <button data-stimeo--sidebar-target=\"trigger\"\n * data-action=\"click->stimeo--sidebar#toggle\"\n * aria-expanded=\"true\" aria-controls=\"app-sidebar\">Menu</button>\n * </header>\n * <div data-stimeo--sidebar-target=\"backdrop\"\n * data-action=\"click->stimeo--sidebar#close\" hidden></div>\n * <aside id=\"app-sidebar\" data-stimeo--sidebar-target=\"panel\"\n * aria-label=\"Main\" data-mode=\"inline\" data-state=\"expanded\">\n * <nav aria-label=\"…\">…</nav>\n * </aside>\n * </div>\n * <main>…</main> <!-- a body-level sibling so it can be made inert in overlay -->\n *\n * No dedicated APG pattern: the base is **Disclosure** (the trigger's\n * `aria-expanded` controls the panel's expanded state) and, *below* the\n * `breakpoint`, it borrows the **Dialog (Modal)** focus behavior via the shared\n * {@link FocusTrap} (the same trap used by dialog / alert-dialog / drawer).\n *\n * Above the breakpoint it is an **inline**, non-modal element that toggles\n * `expanded`↔`collapsed` (a rail), persisting that preference in `localStorage`.\n * Below it, it becomes an **overlay** off-canvas panel: opening activates the\n * trap (focus move, `Tab` cycle, `Escape`, body scroll lock, background `inert`,\n * focus restore); closing defers `hidden` and the trap teardown until the exit\n * transition ends (synchronously when there is none).\n *\n * @remarks\n * Behavior only — rail width, slide, and backdrop are the consumer's CSS, keyed\n * off `data-mode` (`inline`/`overlay`) and `data-state`. `aria-expanded` is an\n * abstract \"is the panel expanded\" flag, independent of the visual difference\n * between an inline collapsed rail (still in the DOM) and an overlay closed panel\n * (`hidden`/off-canvas). The `role=\"dialog\"` semantics are intentionally **not**\n * applied — the sidebar stays an `<aside>`/`<nav>` landmark and only borrows the\n * modal *behavior* (cf. drawer's note on `<aside role=\"dialog\">` conflicts).\n * The collapsed preference persists across Turbo navigations and full reloads;\n * the transient overlay-open state never persists, so \"back/forward\" never\n * restores a stuck-open menu.\n */\nexport class SidebarController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\", \"backdrop\"];\n static override values = {\n breakpoint: { type: Number, default: 768 },\n key: { type: String, default: \"\" },\n collapsed: { type: Boolean, default: false },\n };\n static actions = [\"beforeCache\", \"close\", \"open\", \"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly panelTarget: HTMLElement;\n declare readonly backdropTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasPanelTarget: boolean;\n declare readonly hasBackdropTarget: boolean;\n\n declare breakpointValue: number;\n declare keyValue: string;\n declare collapsedValue: boolean;\n\n /** Exact panel currently owned by the modal lifecycle (survives target churn safely). */\n #activePanel: HTMLElement | null = null;\n\n /** Owns the overlay modal side effects; Escape closes, focus falls to trigger. */\n readonly #trap = new FocusTrap(() => this.#activePanel ?? this.panelTarget, {\n onEscape: () => this.close(),\n fallbackFocus: () => (this.hasTriggerTarget ? this.triggerTarget : null),\n });\n\n /** Current responsive mode. */\n #mode: Mode = \"inline\";\n /** Persisted inline preference: whether the rail is collapsed. */\n #collapsed = false;\n /** The matched media query (`min-width: breakpoint`), watched for mode changes. */\n #mql: MediaQueryList | null = null;\n /** Exact normalized query currently represented by the active media-query listener. */\n #mqlQuery: string | null = null;\n /** Owns the cancellable close-transition wait and its bounded fallback. */\n readonly #transition = new TransitionCompletion();\n /** Distinguishes dynamic target churn from callbacks around controller teardown. */\n #connected = false;\n\n override connect(): void {\n this.#connected = true;\n this.#activePanel = this.hasPanelTarget ? this.panelTarget : null;\n this.#collapsed = this.#restoreCollapsed();\n this.#mqlQuery = this.#breakpointQuery;\n this.#mql = this.#matchBreakpoint(this.#mqlQuery);\n this.#mql?.addEventListener(\"change\", this.#onMediaChange);\n this.#applyMode(this.#computeMode());\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#mql?.removeEventListener(\"change\", this.#onMediaChange);\n this.#mql = null;\n this.#mqlQuery = null;\n this.#transition.cancel();\n this.#trap.deactivate({ restoreFocus: false });\n this.#activePanel = null;\n }\n\n /** Adopts a panel target added by a Turbo morph after the controller connected. */\n panelTargetConnected(panel: HTMLElement): void {\n if (!this.#connected || (this.#activePanel?.isConnected && this.#activePanel !== panel)) return;\n this.#adoptPanel(panel);\n }\n\n /** Closes and releases overlay side effects when the actively trapped panel disappears. */\n panelTargetDisconnected(panel: HTMLElement): void {\n if (panel !== this.#activePanel) return;\n this.#transition.cancel();\n this.#activePanel = null;\n if (!this.#connected) return;\n\n panel.setAttribute(\"data-state\", \"closed\");\n panel.hidden = true;\n this.#hideBackdrop();\n this.#setExpandedAttr(false);\n this.#trap.deactivate();\n\n // Handles morph implementations that add the replacement before removing\n // the old target (its connected callback was intentionally ignored above).\n if (this.hasPanelTarget) this.#adoptPanel(this.panelTarget);\n }\n\n /**\n * Rebinds responsive observation when `breakpoint` changes at runtime.\n *\n * Stimulus calls value callbacks before `connect()`, so the connected guard\n * prevents an eager subscription. Equivalent normalized queries retain the\n * existing listener instead of allocating duplicate `MediaQueryList` objects.\n */\n breakpointValueChanged(): void {\n if (!this.#connected) return;\n const query = this.#breakpointQuery;\n if (this.#mqlQuery === query) return;\n\n this.#mql?.removeEventListener(\"change\", this.#onMediaChange);\n this.#mqlQuery = query;\n this.#mql = this.#matchBreakpoint(query);\n this.#mql?.addEventListener(\"change\", this.#onMediaChange);\n const next = this.#computeMode();\n if (next !== this.#mode) this.#applyMode(next);\n }\n\n /**\n * Sanitizes overlay markup before Turbo snapshots the page.\n *\n * Inline state is durable and remains untouched. Overlay state is transient:\n * pending transitions are cancelled, controller-owned open attributes are\n * closed immediately, and modal side effects are released without moving\n * focus during navigation.\n */\n beforeCache(): void {\n if (!this.#connected || !this.#isOverlay) return;\n this.#transition.cancel();\n this.#trap.deactivate({ restoreFocus: false });\n this.#setOverlayClosedImmediate();\n }\n\n /** Toggles the panel: inline flips collapsed/expanded, overlay flips open/closed. */\n toggle(): void {\n if (this.#isOverlay) {\n this.#isOverlayOpen ? this.#closeOverlay() : this.#openOverlay();\n } else {\n this.#setCollapsed(!this.#collapsed);\n }\n }\n\n /** Shows the panel (inline: expand; overlay: open). */\n open(): void {\n if (this.#isOverlay) this.#openOverlay();\n else this.#setCollapsed(false);\n }\n\n /** Hides the panel (inline: collapse; overlay: close). */\n close(): void {\n if (this.#isOverlay) this.#closeOverlay();\n else this.#setCollapsed(true);\n }\n\n // --- Mode handling ---------------------------------------------------------\n\n /** Re-renders the closed/default state for `mode` and records it. */\n #applyMode(mode: Mode): void {\n this.#mode = mode;\n if (this.hasPanelTarget) this.panelTarget.setAttribute(\"data-mode\", mode);\n if (mode === \"inline\") {\n // Drop any overlay residue, then render the persisted rail state.\n this.#transition.cancel();\n this.#trap.deactivate({ restoreFocus: false });\n if (this.hasPanelTarget) this.panelTarget.hidden = false;\n this.#hideBackdrop();\n this.#applyInlineState(this.#collapsed);\n } else {\n // Overlay always starts closed; never auto-open on a mode switch.\n this.#transition.cancel();\n this.#trap.deactivate({ restoreFocus: false });\n this.#setOverlayClosedImmediate();\n }\n }\n\n /** Reconciles a replacement panel with the current responsive mode and DOM state. */\n #adoptPanel(panel: HTMLElement): void {\n this.#transition.cancel();\n const trapWasActive = this.#trap.active;\n this.#activePanel = panel;\n panel.setAttribute(\"data-mode\", this.#mode);\n if (this.#mode === \"inline\") {\n panel.hidden = false;\n panel.setAttribute(\"data-state\", this.#collapsed ? \"collapsed\" : \"expanded\");\n this.#hideBackdrop();\n this.#setExpandedAttr(!this.#collapsed);\n this.#trap.deactivate({ restoreFocus: false });\n return;\n }\n\n if (panel.getAttribute(\"data-state\") === \"open\") {\n panel.hidden = false;\n if (this.hasBackdropTarget) {\n this.backdropTarget.setAttribute(\"data-state\", \"open\");\n this.backdropTarget.hidden = false;\n }\n this.#setExpandedAttr(true);\n if (trapWasActive) this.#trap.deactivate({ restoreFocus: false });\n this.#trap.activate();\n return;\n }\n\n panel.setAttribute(\"data-state\", \"closed\");\n panel.hidden = true;\n this.#hideBackdrop();\n this.#setExpandedAttr(false);\n this.#trap.deactivate();\n }\n\n readonly #onMediaChange = (event: MediaQueryListEvent): void => {\n const next: Mode = event.matches ? \"inline\" : \"overlay\";\n if (next !== this.#mode) this.#applyMode(next);\n };\n\n #computeMode(): Mode {\n return (this.#mql?.matches ?? true) ? \"inline\" : \"overlay\";\n }\n\n #matchBreakpoint(query = this.#breakpointQuery): MediaQueryList | null {\n if (typeof window.matchMedia !== \"function\") return null;\n return window.matchMedia(query);\n }\n\n // --- Inline (rail) ---------------------------------------------------------\n\n /** Sets, reflects, and persists the inline collapsed preference. */\n #setCollapsed(collapsed: boolean): void {\n if (collapsed === this.#collapsed) return;\n this.#collapsed = collapsed;\n this.#applyInlineState(collapsed);\n this.#persistCollapsed(collapsed);\n }\n\n /** Reflects the inline rail state onto the panel and trigger. */\n #applyInlineState(collapsed: boolean): void {\n if (this.hasPanelTarget) {\n this.panelTarget.setAttribute(\"data-state\", collapsed ? \"collapsed\" : \"expanded\");\n }\n this.#setExpandedAttr(!collapsed);\n }\n\n // --- Overlay (off-canvas modal) -------------------------------------------\n\n /** Opens the overlay: reveal it, commit a starting frame, then trap focus. */\n #openOverlay(): void {\n if (!this.hasPanelTarget || this.#isOverlayOpen) return;\n this.#transition.cancel();\n this.#activePanel = this.panelTarget;\n this.panelTarget.hidden = false;\n if (this.hasBackdropTarget) this.backdropTarget.hidden = false;\n // Commit the closed (off-canvas) frame before flipping to open so the enter\n // transition has a starting frame to animate (cf. drawer).\n void this.panelTarget.offsetWidth;\n this.#setOverlayState(\"open\");\n this.#setExpandedAttr(true);\n this.#trap.activate();\n }\n\n /** Closes the overlay: start the exit transition, defer hide + trap teardown. */\n #closeOverlay(): void {\n if (!this.hasPanelTarget || !this.#isOverlayOpen) return;\n this.#setOverlayState(\"closed\");\n this.#setExpandedAttr(false);\n this.#hideAfterTransition();\n }\n\n /** Renders the overlay closed state up front (used when entering overlay mode). */\n #setOverlayClosedImmediate(): void {\n this.#setOverlayState(\"closed\");\n this.#setExpandedAttr(false);\n if (this.hasPanelTarget) this.panelTarget.hidden = true;\n this.#hideBackdrop();\n }\n\n /** Syncs `data-state` on the panel and backdrop together (overlay). */\n #setOverlayState(state: \"open\" | \"closed\"): void {\n if (this.hasPanelTarget) this.panelTarget.setAttribute(\"data-state\", state);\n if (this.hasBackdropTarget) this.backdropTarget.setAttribute(\"data-state\", state);\n }\n\n /**\n * Applies `hidden` once the close transition ends so the exit slide can play,\n * then reverts the modal side effects. The shared waiter completes\n * synchronously for 0ms transitions and supplies a bounded fallback when the\n * browser emits no terminal event.\n */\n #hideAfterTransition(): void {\n const panel = this.panelTarget;\n this.#activePanel = panel;\n this.#transition.wait(panel, () => this.#applyOverlayHidden(panel));\n }\n\n /** Hides the panel/backdrop and tears down the trap after the exit transition. */\n #applyOverlayHidden(panel: HTMLElement): void {\n panel.hidden = true;\n this.#hideBackdrop();\n this.#trap.deactivate();\n }\n\n #hideBackdrop(): void {\n if (!this.hasBackdropTarget) return;\n this.backdropTarget.setAttribute(\"data-state\", \"closed\");\n this.backdropTarget.hidden = true;\n }\n\n // --- Shared helpers --------------------------------------------------------\n\n #setExpandedAttr(expanded: boolean): void {\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n }\n\n /**\n * Resolves the inline collapsed preference, in priority order:\n * 1. the persisted value (when a `key` is set and storage is readable),\n * 2. the current DOM `data-state` — so a Turbo cache restore / morph that\n * reconnects over already-rendered markup keeps the live state (the DOM is\n * the source of truth) even when no `key` / localStorage is available,\n * 3. the declared `collapsed` value.\n */\n #restoreCollapsed(): boolean {\n const key = this.#storageKey;\n if (key) {\n // An unreadable storage reads as null and falls through to the DOM / declared default.\n const stored = readLocalStorage(key);\n if (stored !== null) return stored === \"1\";\n }\n const domState = this.hasPanelTarget ? this.panelTarget.getAttribute(\"data-state\") : null;\n if (domState === \"collapsed\") return true;\n if (domState === \"expanded\") return false;\n return this.collapsedValue;\n }\n\n /** Persists the collapsed preference when a key is configured. */\n #persistCollapsed(collapsed: boolean): void {\n const key = this.#storageKey;\n if (!key) return;\n writeLocalStorage(key, collapsed ? \"1\" : \"0\");\n }\n\n get #storageKey(): string {\n return this.keyValue ? `stimeo--sidebar:${this.keyValue}` : \"\";\n }\n\n /** Valid breakpoint CSS query, defaulting malformed or negative values to 768px. */\n get #breakpointQuery(): string {\n const value = this.breakpointValue;\n const breakpoint = Number.isFinite(value) && value >= 0 ? value : 768;\n return `(min-width: ${breakpoint}px)`;\n }\n\n get #isOverlay(): boolean {\n return this.#mode === \"overlay\";\n }\n\n get #isOverlayOpen(): boolean {\n return (\n this.#isOverlay &&\n this.hasPanelTarget &&\n this.panelTarget.getAttribute(\"data-state\") === \"open\"\n );\n }\n}\n"]}
@@ -28,6 +28,7 @@ import { Controller } from '@hotwired/stimulus';
28
28
  * - `goto` jumps to the step in its `index` action param.
29
29
  * - With `linear=true`, `goto` may not skip more than one step ahead of the
30
30
  * current one (moving backward is always allowed).
31
+ * - Runtime changes to the `index` Value re-derive every step's state.
31
32
  * - Each move re-derives `data-state`/`aria-current` and dispatches
32
33
  * `stimeo--stepper:change`.
33
34
  */
@@ -51,6 +52,9 @@ declare class StepperController extends Controller<HTMLElement> {
51
52
  linearValue: boolean;
52
53
  /** Normalizes an out-of-range initial `index` and renders the initial state. */
53
54
  connect(): void;
55
+ disconnect(): void;
56
+ /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
57
+ indexValueChanged(): void;
54
58
  /** Advances to the next step (ignored at the last step). */
55
59
  next(): void;
56
60
  /** Returns to the previous step (ignored at the first step). */
@@ -9,23 +9,32 @@ var StepperController = class extends Controller {
9
9
  };
10
10
  static actions = ["goto", "next", "prev"];
11
11
  static events = ["change"];
12
+ #isConnected = false;
12
13
  /** Normalizes an out-of-range initial `index` and renders the initial state. */
13
14
  connect() {
14
- this.indexValue = this.#clampIndex(this.indexValue);
15
- this.#render();
15
+ this.#isConnected = true;
16
+ this.#normalizeAndRender();
17
+ }
18
+ disconnect() {
19
+ this.#isConnected = false;
20
+ }
21
+ /** Re-renders when Turbo Morph or application code changes `index` at runtime. */
22
+ indexValueChanged() {
23
+ if (!this.#isConnected) return;
24
+ this.#normalizeAndRender();
16
25
  }
17
26
  /** Advances to the next step (ignored at the last step). */
18
27
  next() {
19
- this.#moveTo(this.indexValue + 1);
28
+ this.#moveTo(this.#clampIndex(this.indexValue) + 1);
20
29
  }
21
30
  /** Returns to the previous step (ignored at the first step). */
22
31
  prev() {
23
- this.#moveTo(this.indexValue - 1);
32
+ this.#moveTo(this.#clampIndex(this.indexValue) - 1);
24
33
  }
25
34
  /** Jumps to the step carried in the action's `index` param. */
26
35
  goto(event) {
27
36
  const target = Number(event.params.index);
28
- if (!Number.isFinite(target)) return;
37
+ if (!Number.isFinite(target) || !Number.isInteger(target)) return;
29
38
  this.#moveTo(target);
30
39
  }
31
40
  /**
@@ -35,16 +44,24 @@ var StepperController = class extends Controller {
35
44
  */
36
45
  #moveTo(target) {
37
46
  const total = this.stepTargets.length;
47
+ if (!Number.isFinite(target) || !Number.isInteger(target)) return;
38
48
  if (target < 0 || target >= total) return;
39
- if (target === this.indexValue) return;
40
- if (this.linearValue && target > this.indexValue + 1) return;
41
- const previous = this.indexValue;
49
+ const current = this.#clampIndex(this.indexValue);
50
+ if (target === current) return;
51
+ if (this.linearValue && target > current + 1) return;
52
+ const previous = current;
42
53
  this.indexValue = target;
43
- this.#render();
54
+ this.#render(target);
44
55
  this.dispatch("change", {
45
56
  detail: { index: target, previous, step: this.stepTargets[target] }
46
57
  });
47
58
  }
59
+ /** Normalizes the public Value and reflects it without dispatching an action event. */
60
+ #normalizeAndRender() {
61
+ const normalized = this.#clampIndex(this.indexValue);
62
+ if (!Object.is(normalized, this.indexValue)) this.indexValue = normalized;
63
+ this.#render(normalized);
64
+ }
48
65
  /**
49
66
  * Derives each step's `data-state` and the current button's `aria-current`.
50
67
  *
@@ -52,8 +69,7 @@ var StepperController = class extends Controller {
52
69
  * contract assumes one operable button per step. If a step needs multiple
53
70
  * buttons, mark the navigational one first (or this would target the wrong one).
54
71
  */
55
- #render() {
56
- const current = this.indexValue;
72
+ #render(current) {
57
73
  this.stepTargets.forEach((step, index) => {
58
74
  step.dataset.state = index < current ? "complete" : index === current ? "current" : "upcoming";
59
75
  const button = step.querySelector("button");
@@ -68,7 +84,7 @@ var StepperController = class extends Controller {
68
84
  /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */
69
85
  #clampIndex(index) {
70
86
  const last = this.stepTargets.length - 1;
71
- if (last < 0) return 0;
87
+ if (last < 0 || !Number.isFinite(index)) return 0;
72
88
  return Math.min(last, Math.max(0, Math.trunc(index)));
73
89
  }
74
90
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use Tabs.\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index`\n * (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n /** Normalizes an out-of-range initial `index` and renders the initial state. */\n override connect(): void {\n this.indexValue = this.#clampIndex(this.indexValue);\n this.#render();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.indexValue + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.indexValue - 1);\n }\n\n /** Jumps to the step carried in the action's `index` param. */\n goto(event: { params: { index?: number } }): void {\n const target = Number(event.params.index);\n if (!Number.isFinite(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (target < 0 || target >= total) return;\n if (target === this.indexValue) return;\n if (this.linearValue && target > this.indexValue + 1) return;\n\n const previous = this.indexValue;\n this.indexValue = target;\n this.#render();\n this.dispatch(\"change\", {\n detail: { index: target, previous, step: this.stepTargets[target] },\n });\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current`.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n */\n #render(): void {\n const current = this.indexValue;\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/controllers/stepper_controller.ts"],"names":[],"mappings":"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","file":"stepper_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/**\n * Headless, accessible stepper / wizard navigation behavior.\n *\n * Markup contract (identifier: `stimeo--stepper`):\n * <ol data-controller=\"stimeo--stepper\" data-stimeo--stepper-index-value=\"0\">\n * <li data-stimeo--stepper-target=\"step\">\n * <button aria-current=\"step\" data-stimeo--stepper-index-param=\"0\"\n * data-action=\"click->stimeo--stepper#goto\">Account</button>\n * </li>\n * <!-- more steps -->\n * </ol>\n *\n * There is no dedicated APG widget; the current step is expressed with\n * `aria-current=\"step\"` on the operable `<button>`. Each step `<li>` also gets a\n * `data-state` (`complete`/`current`/`upcoming`) derived from the current index.\n * For a read-only progress display use\n * {@link StepIndicatorController | Step Indicator}; for panel switching use Tabs.\n *\n * @remarks\n * Behavior only. The controller never traps or restores focus — each step button\n * is in the natural Tab order. `data-state` is purely derived from `index`\n * (completion is not persisted).\n *\n * Behavior provided:\n * - `next`/`prev` move one step, ignoring moves past either end.\n * - `goto` jumps to the step in its `index` action param.\n * - With `linear=true`, `goto` may not skip more than one step ahead of the\n * current one (moving backward is always allowed).\n * - Runtime changes to the `index` Value re-derive every step's state.\n * - Each move re-derives `data-state`/`aria-current` and dispatches\n * `stimeo--stepper:change`.\n */\nexport class StepperController extends Controller<HTMLElement> {\n static override targets = [\"step\"];\n static override values = {\n index: { type: Number, default: 0 },\n linear: { type: Boolean, default: false },\n };\n static actions = [\"goto\", \"next\", \"prev\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly stepTargets: HTMLElement[];\n declare indexValue: number;\n declare linearValue: boolean;\n\n #isConnected = false;\n\n /** Normalizes an out-of-range initial `index` and renders the initial state. */\n override connect(): void {\n this.#isConnected = true;\n this.#normalizeAndRender();\n }\n\n override disconnect(): void {\n this.#isConnected = false;\n }\n\n /** Re-renders when Turbo Morph or application code changes `index` at runtime. */\n indexValueChanged(): void {\n if (!this.#isConnected) return;\n this.#normalizeAndRender();\n }\n\n /** Advances to the next step (ignored at the last step). */\n next(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) + 1);\n }\n\n /** Returns to the previous step (ignored at the first step). */\n prev(): void {\n this.#moveTo(this.#clampIndex(this.indexValue) - 1);\n }\n\n /** Jumps to the step carried in the action's `index` param. */\n goto(event: { params: { index?: number } }): void {\n const target = Number(event.params.index);\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n this.#moveTo(target);\n }\n\n /**\n * Moves the current step to `target` when allowed: in range, not a no-op, and\n * — under `linear` — not skipping more than one step ahead. Re-renders and\n * dispatches `change`.\n */\n #moveTo(target: number): void {\n const total = this.stepTargets.length;\n if (!Number.isFinite(target) || !Number.isInteger(target)) return;\n if (target < 0 || target >= total) return;\n const current = this.#clampIndex(this.indexValue);\n if (target === current) return;\n if (this.linearValue && target > current + 1) return;\n\n const previous = current;\n this.indexValue = target;\n this.#render(target);\n this.dispatch(\"change\", {\n detail: { index: target, previous, step: this.stepTargets[target] },\n });\n }\n\n /** Normalizes the public Value and reflects it without dispatching an action event. */\n #normalizeAndRender(): void {\n const normalized = this.#clampIndex(this.indexValue);\n if (!Object.is(normalized, this.indexValue)) this.indexValue = normalized;\n this.#render(normalized);\n }\n\n /**\n * Derives each step's `data-state` and the current button's `aria-current`.\n *\n * `aria-current=\"step\"` is placed on the step's **first** `<button>`; the markup\n * contract assumes one operable button per step. If a step needs multiple\n * buttons, mark the navigational one first (or this would target the wrong one).\n */\n #render(current: number): void {\n this.stepTargets.forEach((step, index) => {\n step.dataset.state =\n index < current ? \"complete\" : index === current ? \"current\" : \"upcoming\";\n const button = step.querySelector<HTMLElement>(\"button\");\n if (!button) return;\n if (index === current) {\n button.setAttribute(\"aria-current\", \"step\");\n } else {\n button.removeAttribute(\"aria-current\");\n }\n });\n }\n\n /** Constrains an index to `[0, total-1]` (or `0` when there are no steps). */\n #clampIndex(index: number): number {\n const last = this.stepTargets.length - 1;\n if (last < 0 || !Number.isFinite(index)) return 0;\n return Math.min(last, Math.max(0, Math.trunc(index)));\n }\n}\n"]}
@@ -1,5 +1,12 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
+ // src/controllers/stick_to_bottom_controller.ts
4
+
5
+ // src/utils/reduced_motion.ts
6
+ function prefersReducedMotion() {
7
+ return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
8
+ }
9
+
3
10
  // src/controllers/stick_to_bottom_controller.ts
4
11
  var countElements = (nodes) => {
5
12
  let n = 0;
@@ -87,12 +94,9 @@ var StickToBottomController = class extends Controller {
87
94
  return this.hasContentTarget ? this.contentTarget : this.element;
88
95
  }
89
96
  #behavior() {
90
- if (this.#prefersReducedMotion()) return "auto";
97
+ if (prefersReducedMotion()) return "auto";
91
98
  return this.behaviorValue === "smooth" ? "smooth" : "auto";
92
99
  }
93
- #prefersReducedMotion() {
94
- return typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
95
- }
96
100
  };
97
101
 
98
102
  export { StickToBottomController };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controllers/stick_to_bottom_controller.ts"],"names":[],"mappings":"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","file":"stick_to_bottom_controller.js","sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\n\n/** Counts the element nodes in an added-node list (text nodes are ignored). */\nconst countElements = (nodes: NodeList): number => {\n let n = 0;\n for (const node of nodes) if (node.nodeType === Node.ELEMENT_NODE) n += 1;\n return n;\n};\n\n/**\n * Headless **stick-to-bottom**: auto-follows new content to the bottom of a scroll\n * container (a chat log, a live console) — but only while the user is already near the\n * bottom; if they have scrolled up to read, it holds position and flags that new content\n * arrived instead (no APG pattern; honors WCAG 2.3.3 via `prefers-reduced-motion` and\n * never steals focus).\n *\n * Markup contract (identifier: `stimeo--stick-to-bottom`):\n * <div data-controller=\"stimeo--stick-to-bottom\"\n * data-stimeo--stick-to-bottom-threshold-value=\"80\" style=\"overflow:auto\">\n * <ul data-stimeo--stick-to-bottom-target=\"content\"><!-- Turbo Stream appends --></ul>\n * </div>\n *\n * The container is \"pinned\" while its distance from the bottom is within `threshold`. A\n * `MutationObserver` on `content` (or the element) reacts to appended children: while\n * pinned it scrolls to the bottom; while unpinned it sets `data-has-new` and emits `new`.\n * Scrolling recomputes pinned and reflects `data-pinned`, emitting `pin` on change; the\n * `scrollToBottom` action jumps back down (a \"new messages\" button).\n *\n * @remarks\n * The `MutationObserver` watches `childList` only (not `subtree`), so it follows\n * direct appends to `content` (or the element). Appends made deeper inside a nested\n * wrapper are not detected — keep messages as direct children, or call the public\n * `scrollToBottom` action after such inserts.\n *\n * Behavior only — it does not add content (Turbo Stream / the consumer does) and is the\n * minimal follow primitive, not a full chat UI (no virtualization / message input). It is\n * the lightweight member of the scroll family. State is derived from the scroll position\n * each pass (no module-scope state), so `connect()` re-syncs after a Turbo Stream insert;\n * `behavior` falls back to `auto` under reduced motion; auto-scroll never moves focus; the\n * observer and the passive scroll listener are released on `disconnect()` (Turbo\n * navigation included).\n */\nexport class StickToBottomController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n threshold: { type: Number, default: 80 },\n behavior: { type: String, default: \"auto\" },\n };\n static actions = [\"scrollToBottom\"] as const;\n static events = [\"pin\", \"new\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare thresholdValue: number;\n declare behaviorValue: string;\n\n #observer: MutationObserver | null = null;\n #pinned = false;\n\n readonly #onScroll = (): void => this.#updatePinned();\n\n override connect(): void {\n this.#pinned = this.#isPinned();\n // Re-sync the hooks from the current geometry — a Turbo cache restore may bring back a\n // stale data-pinned / data-has-new that no longer matches the scroll position.\n this.#reflectPinned();\n\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.#watched(), { childList: true });\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /** Jumps to the bottom and re-pins (wired to a \"new messages\" button). */\n scrollToBottom(): void {\n this.#scrollToBottom();\n this.element.removeAttribute(\"data-has-new\");\n if (!this.#pinned) {\n this.#pinned = true;\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.dispatch(\"pin\", { detail: { pinned: true } });\n }\n }\n\n /** Follows appended children while pinned; otherwise flags new content. */\n #onMutations(mutations: MutationRecord[]): void {\n let added = 0;\n for (const mutation of mutations) added += countElements(mutation.addedNodes);\n if (added === 0) return;\n\n if (this.#pinned) {\n this.#scrollToBottom();\n } else {\n this.element.setAttribute(\"data-has-new\", \"true\");\n this.dispatch(\"new\", { detail: { count: added } });\n }\n }\n\n /** Recomputes pinned from the scroll position and reflects it on a transition. */\n #updatePinned(): void {\n const pinned = this.#isPinned();\n if (pinned === this.#pinned) return;\n this.#pinned = pinned;\n this.#reflectPinned();\n this.dispatch(\"pin\", { detail: { pinned } });\n }\n\n /** Mirrors the current `#pinned` onto the state hooks (clearing has-new once pinned). */\n #reflectPinned(): void {\n if (this.#pinned) {\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.element.removeAttribute(\"data-has-new\"); // caught up with the bottom\n } else {\n this.element.removeAttribute(\"data-pinned\");\n }\n }\n\n #isPinned(): boolean {\n const el = this.element;\n return el.scrollHeight - el.clientHeight - el.scrollTop <= this.thresholdValue;\n }\n\n #scrollToBottom(): void {\n const top = this.element.scrollHeight;\n if (typeof this.element.scrollTo === \"function\") {\n this.element.scrollTo({ top, behavior: this.#behavior() });\n } else {\n this.element.scrollTop = top;\n }\n }\n\n /** The append-watched element: the `content` target, or the container itself. */\n #watched(): HTMLElement {\n return this.hasContentTarget ? this.contentTarget : this.element;\n }\n\n #behavior(): ScrollBehavior {\n if (this.#prefersReducedMotion()) return \"auto\";\n return this.behaviorValue === \"smooth\" ? \"smooth\" : \"auto\";\n }\n\n #prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/reduced_motion.ts","../../src/controllers/stick_to_bottom_controller.ts"],"names":[],"mappings":"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","file":"stick_to_bottom_controller.js","sourcesContent":["/**\n * Shared `prefers-reduced-motion` lookup for the motion-aware controllers\n * (count-up, highlight, overflow-indicator, scroll-visibility, stick-to-bottom,\n * transition).\n *\n * Each of those controllers used to duplicate the same guarded `matchMedia`\n * read; this one-liner keeps the media query string and the environment guard\n * single-sourced. The preference is intentionally re-read on every call — the\n * controllers check it at each animation/scroll start (WCAG 2.2 **2.3.3**), so\n * flipping the OS setting takes effect immediately without any listener or\n * cache bookkeeping here.\n */\n\n/**\n * Whether the user currently requests reduced motion.\n *\n * @returns `true` when `(prefers-reduced-motion: reduce)` matches; `false`\n * otherwise, including environments without `window.matchMedia` (treated as\n * \"no preference\").\n */\nexport function prefersReducedMotion(): boolean {\n return (\n typeof window.matchMedia === \"function\" &&\n window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n );\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { prefersReducedMotion } from \"../utils/reduced_motion\";\n\n/** Counts the element nodes in an added-node list (text nodes are ignored). */\nconst countElements = (nodes: NodeList): number => {\n let n = 0;\n for (const node of nodes) if (node.nodeType === Node.ELEMENT_NODE) n += 1;\n return n;\n};\n\n/**\n * Headless **stick-to-bottom**: auto-follows new content to the bottom of a scroll\n * container (a chat log, a live console) — but only while the user is already near the\n * bottom; if they have scrolled up to read, it holds position and flags that new content\n * arrived instead (no APG pattern; honors WCAG 2.3.3 via `prefers-reduced-motion` and\n * never steals focus).\n *\n * Markup contract (identifier: `stimeo--stick-to-bottom`):\n * <div data-controller=\"stimeo--stick-to-bottom\"\n * data-stimeo--stick-to-bottom-threshold-value=\"80\" style=\"overflow:auto\">\n * <ul data-stimeo--stick-to-bottom-target=\"content\"><!-- Turbo Stream appends --></ul>\n * </div>\n *\n * The container is \"pinned\" while its distance from the bottom is within `threshold`. A\n * `MutationObserver` on `content` (or the element) reacts to appended children: while\n * pinned it scrolls to the bottom; while unpinned it sets `data-has-new` and emits `new`.\n * Scrolling recomputes pinned and reflects `data-pinned`, emitting `pin` on change; the\n * `scrollToBottom` action jumps back down (a \"new messages\" button).\n *\n * @remarks\n * The `MutationObserver` watches `childList` only (not `subtree`), so it follows\n * direct appends to `content` (or the element). Appends made deeper inside a nested\n * wrapper are not detected — keep messages as direct children, or call the public\n * `scrollToBottom` action after such inserts.\n *\n * Behavior only — it does not add content (Turbo Stream / the consumer does) and is the\n * minimal follow primitive, not a full chat UI (no virtualization / message input). It is\n * the lightweight member of the scroll family. State is derived from the scroll position\n * each pass (no module-scope state), so `connect()` re-syncs after a Turbo Stream insert;\n * `behavior` falls back to `auto` under reduced motion; auto-scroll never moves focus; the\n * observer and the passive scroll listener are released on `disconnect()` (Turbo\n * navigation included).\n */\nexport class StickToBottomController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n threshold: { type: Number, default: 80 },\n behavior: { type: String, default: \"auto\" },\n };\n static actions = [\"scrollToBottom\"] as const;\n static events = [\"pin\", \"new\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare thresholdValue: number;\n declare behaviorValue: string;\n\n #observer: MutationObserver | null = null;\n #pinned = false;\n\n readonly #onScroll = (): void => this.#updatePinned();\n\n override connect(): void {\n this.#pinned = this.#isPinned();\n // Re-sync the hooks from the current geometry — a Turbo cache restore may bring back a\n // stale data-pinned / data-has-new that no longer matches the scroll position.\n this.#reflectPinned();\n\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n if (typeof MutationObserver !== \"undefined\") {\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.#watched(), { childList: true });\n }\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /** Jumps to the bottom and re-pins (wired to a \"new messages\" button). */\n scrollToBottom(): void {\n this.#scrollToBottom();\n this.element.removeAttribute(\"data-has-new\");\n if (!this.#pinned) {\n this.#pinned = true;\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.dispatch(\"pin\", { detail: { pinned: true } });\n }\n }\n\n /** Follows appended children while pinned; otherwise flags new content. */\n #onMutations(mutations: MutationRecord[]): void {\n let added = 0;\n for (const mutation of mutations) added += countElements(mutation.addedNodes);\n if (added === 0) return;\n\n if (this.#pinned) {\n this.#scrollToBottom();\n } else {\n this.element.setAttribute(\"data-has-new\", \"true\");\n this.dispatch(\"new\", { detail: { count: added } });\n }\n }\n\n /** Recomputes pinned from the scroll position and reflects it on a transition. */\n #updatePinned(): void {\n const pinned = this.#isPinned();\n if (pinned === this.#pinned) return;\n this.#pinned = pinned;\n this.#reflectPinned();\n this.dispatch(\"pin\", { detail: { pinned } });\n }\n\n /** Mirrors the current `#pinned` onto the state hooks (clearing has-new once pinned). */\n #reflectPinned(): void {\n if (this.#pinned) {\n this.element.setAttribute(\"data-pinned\", \"true\");\n this.element.removeAttribute(\"data-has-new\"); // caught up with the bottom\n } else {\n this.element.removeAttribute(\"data-pinned\");\n }\n }\n\n #isPinned(): boolean {\n const el = this.element;\n return el.scrollHeight - el.clientHeight - el.scrollTop <= this.thresholdValue;\n }\n\n #scrollToBottom(): void {\n const top = this.element.scrollHeight;\n if (typeof this.element.scrollTo === \"function\") {\n this.element.scrollTo({ top, behavior: this.#behavior() });\n } else {\n this.element.scrollTop = top;\n }\n }\n\n /** The append-watched element: the `content` target, or the container itself. */\n #watched(): HTMLElement {\n return this.hasContentTarget ? this.contentTarget : this.element;\n }\n\n #behavior(): ScrollBehavior {\n if (prefersReducedMotion()) return \"auto\";\n return this.behaviorValue === \"smooth\" ? \"smooth\" : \"auto\";\n }\n}\n"]}
@@ -18,14 +18,17 @@ import { Controller } from '@hotwired/stimulus';
18
18
  *
19
19
  * When the sentinel scrolls out past the top of the viewport (or `rootSelector`
20
20
  * container), the sticky element is considered stuck and `data-stuck="true"` is
21
- * set; otherwise `false`. The `change` event fires on each transition.
21
+ * set; otherwise `false`. The observer's initial snapshot dispatches `change`
22
+ * once with the current state, including after a Turbo reconnect; subsequent
23
+ * notifications dispatch only when that state changes.
22
24
  *
23
25
  * @remarks
24
26
  * Behavior only — `position: sticky`, shadows, and shrink effects are the
25
27
  * consumer's CSS (`[data-stuck="true"] { … }`). `data-stuck` is a visual hook
26
- * only: it carries no ARIA role/state. `offset` feeds a negative top `rootMargin`
27
- * and must match the sticky element's CSS `top`. The observer is disconnected on
28
- * `disconnect()` (Turbo navigation included).
28
+ * only: it carries no ARIA role/state. `offset` is negated numerically for the
29
+ * top `rootMargin` (so negative offsets remain valid) and must match the sticky
30
+ * element's CSS `top`. The observer follows dynamic sentinel targets and value
31
+ * changes, and is disconnected on `disconnect()` (Turbo navigation included).
29
32
  */
30
33
  declare class StickyObserverController extends Controller<HTMLElement> {
31
34
  #private;
@@ -49,6 +52,16 @@ declare class StickyObserverController extends Controller<HTMLElement> {
49
52
  offsetValue: number;
50
53
  connect(): void;
51
54
  disconnect(): void;
55
+ /** Starts observation when a sentinel is inserted after connection. */
56
+ sentinelTargetConnected(): void;
57
+ /** Stops or transfers observation when the current sentinel is removed. */
58
+ sentinelTargetDisconnected(): void;
59
+ /** Reflects the last snapshot onto an element inserted after that snapshot. */
60
+ elementTargetConnected(element: HTMLElement): void;
61
+ /** Rebuilds the observer when Turbo morphs the configured root. */
62
+ rootSelectorValueChanged(): void;
63
+ /** Rebuilds the observer when Turbo morphs the configured top offset. */
64
+ offsetValueChanged(): void;
52
65
  }
53
66
 
54
67
  export { StickyObserverController };
@@ -3,6 +3,12 @@ import { Controller } from '@hotwired/stimulus';
3
3
  // src/controllers/sticky_observer_controller.ts
4
4
 
5
5
  // src/utils/intersection_watcher.ts
6
+ function isBeforeRootStart(entry) {
7
+ const rect = entry.boundingClientRect;
8
+ if (rect.width === 0 && rect.height === 0) return false;
9
+ const rootTop = entry.rootBounds?.top ?? 0;
10
+ return rect.bottom <= rootTop;
11
+ }
6
12
  var IntersectionWatcher = class {
7
13
  #onEntries;
8
14
  #observer = null;
@@ -18,6 +24,11 @@ var IntersectionWatcher = class {
18
24
  * (Re)creates the observer and observes `targets`. Returns `false` — leaving
19
25
  * the watcher inert — without `IntersectionObserver` support (very old
20
26
  * browsers; the caller's no-JS fallback stays in charge) or with no targets.
27
+ *
28
+ * @throws Whatever the platform throws for an invalid `rootMargin`/`threshold`
29
+ * or a failing `observe()`. The exception is passed through unchanged, but
30
+ * the watcher rolls back first: every target observed so far is released and
31
+ * `active` stays `false`, so a caller that retries starts from a clean slate.
21
32
  */
22
33
  start(targets, options = {}) {
23
34
  this.stop();
@@ -25,25 +36,42 @@ var IntersectionWatcher = class {
25
36
  const list = Array.isArray(targets) ? targets : [targets];
26
37
  if (list.length === 0) return false;
27
38
  const root = "root" in options ? options.root ?? null : options.rootSelector ? document.querySelector(options.rootSelector) : null;
28
- this.#active = true;
29
- this.#observer = new IntersectionObserver(
30
- (entries) => {
31
- if (this.#active) this.#onEntries(entries);
32
- },
33
- { root, rootMargin: options.rootMargin, threshold: options.threshold }
34
- );
35
- for (const target of list) this.#observer.observe(target);
36
- return true;
39
+ let observer = null;
40
+ try {
41
+ observer = new IntersectionObserver(
42
+ (entries) => {
43
+ if (this.#active && this.#observer === observer) this.#onEntries(entries);
44
+ },
45
+ { root, rootMargin: options.rootMargin, threshold: options.threshold }
46
+ );
47
+ for (const target of list) observer.observe(target);
48
+ this.#observer = observer;
49
+ this.#active = true;
50
+ return true;
51
+ } catch (error) {
52
+ observer?.disconnect();
53
+ this.#observer = null;
54
+ this.#active = false;
55
+ throw error;
56
+ }
37
57
  }
38
58
  /**
39
59
  * Re-delivers `target`'s CURRENT intersection state: `IntersectionObserver`
40
60
  * only reports *changes*, but `observe()` always reports the present state,
41
61
  * so unobserve→observe turns "still intersecting" into a fresh callback.
62
+ *
63
+ * @throws Whatever `unobserve()`/`observe()` throws. The watcher is stopped
64
+ * first, so it never stays live with a half-rearmed target.
42
65
  */
43
66
  rearm(target) {
44
67
  if (!this.#observer) return;
45
- this.#observer.unobserve(target);
46
- this.#observer.observe(target);
68
+ try {
69
+ this.#observer.unobserve(target);
70
+ this.#observer.observe(target);
71
+ } catch (error) {
72
+ this.stop();
73
+ throw error;
74
+ }
47
75
  }
48
76
  /** Severs the observer; late queued callbacks become no-ops via the guard. */
49
77
  stop() {
@@ -65,23 +93,63 @@ var StickyObserverController = class extends Controller {
65
93
  #watcher = new IntersectionWatcher((entries) => this.#onIntersect(entries));
66
94
  /** Last reported stuck state, so `change` fires only on transitions. */
67
95
  #stuck = null;
96
+ /** Target currently owned by the watcher, used to avoid duplicate restarts. */
97
+ #observedSentinel = null;
98
+ #connected = false;
68
99
  #onIntersect(entries) {
69
- const entry = entries[entries.length - 1];
70
- if (!entry) return;
71
- this.#setStuck(!entry.isIntersecting);
100
+ for (const entry of entries) {
101
+ if (!this.#connected || !this.#watcher.active) return;
102
+ this.#setStuck(!entry.isIntersecting && isBeforeRootStart(entry));
103
+ }
72
104
  }
73
105
  connect() {
74
- if (!this.hasSentinelTarget) return;
75
- this.#watcher.start(this.sentinelTarget, {
76
- rootSelector: this.rootSelectorValue,
77
- rootMargin: `-${this.offsetValue}px 0px 0px 0px`,
78
- threshold: [0]
79
- });
106
+ this.#connected = true;
107
+ this.#stuck = null;
108
+ this.#syncObserver();
80
109
  }
81
110
  disconnect() {
111
+ this.#connected = false;
82
112
  this.#watcher.stop();
113
+ this.#observedSentinel = null;
83
114
  this.#stuck = null;
84
115
  }
116
+ /** Starts observation when a sentinel is inserted after connection. */
117
+ sentinelTargetConnected() {
118
+ if (this.#connected) this.#syncObserver();
119
+ }
120
+ /** Stops or transfers observation when the current sentinel is removed. */
121
+ sentinelTargetDisconnected() {
122
+ if (this.#connected) this.#syncObserver();
123
+ }
124
+ /** Reflects the last snapshot onto an element inserted after that snapshot. */
125
+ elementTargetConnected(element) {
126
+ if (this.#stuck !== null) {
127
+ element.setAttribute("data-stuck", this.#stuck ? "true" : "false");
128
+ }
129
+ }
130
+ /** Rebuilds the observer when Turbo morphs the configured root. */
131
+ rootSelectorValueChanged() {
132
+ if (this.#connected) this.#syncObserver(true);
133
+ }
134
+ /** Rebuilds the observer when Turbo morphs the configured top offset. */
135
+ offsetValueChanged() {
136
+ if (this.#connected) this.#syncObserver(true);
137
+ }
138
+ #syncObserver(force = false) {
139
+ const sentinel = this.hasSentinelTarget ? this.sentinelTarget : null;
140
+ if (!force && sentinel === this.#observedSentinel && this.#watcher.active) return;
141
+ this.#watcher.stop();
142
+ this.#observedSentinel = null;
143
+ if (!sentinel) return;
144
+ const configuredOffset = this.offsetValue;
145
+ const offset = Number.isFinite(configuredOffset) ? configuredOffset : 0;
146
+ const started = this.#watcher.start(sentinel, {
147
+ rootSelector: this.rootSelectorValue,
148
+ rootMargin: `${-offset}px 0px 0px 0px`,
149
+ threshold: [0]
150
+ });
151
+ if (started) this.#observedSentinel = sentinel;
152
+ }
85
153
  /** Reflects the stuck state onto the sticky element and emits `change`. */
86
154
  #setStuck(next) {
87
155
  if (next === this.#stuck) return;