stimeo-ui 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +212 -0
- package/README.md +120 -0
- package/dist/cable/index.js +123 -29
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +30 -3
- package/dist/controllers/accordion_controller.js +98 -9
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +8 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +3 -2
- package/dist/controllers/announcer_controller.js +96 -62
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js +83 -7
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +10 -7
- package/dist/controllers/breadcrumb_controller.js +38 -11
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +4 -4
- package/dist/controllers/bulk_select_controller.js +7 -6
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +101 -23
- package/dist/controllers/calendar_controller.js +340 -123
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +67 -22
- package/dist/controllers/carousel_controller.js +263 -38
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +1 -1
- package/dist/controllers/character_counter_controller.js +40 -2
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.js +81 -12
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +28 -2
- package/dist/controllers/clipboard_controller.js +63 -5
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +25 -1
- package/dist/controllers/collapsible_controller.js +99 -14
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +29 -8
- package/dist/controllers/color_picker_controller.js +80 -34
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +10 -1
- package/dist/controllers/combobox_controller.js +106 -16
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +35 -3
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js +85 -17
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +3 -0
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +6 -0
- package/dist/controllers/context_menu_controller.js +32 -12
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +30 -1
- package/dist/controllers/countdown_controller.js +129 -26
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +77 -16
- package/dist/controllers/currency_input_controller.js +221 -67
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +63 -18
- package/dist/controllers/data_grid_controller.js +195 -29
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +41 -6
- package/dist/controllers/date_range_picker_controller.js +151 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +10 -3
- package/dist/controllers/dialog_controller.js +35 -8
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js +22 -4
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +2 -2
- package/dist/controllers/dirty_form_controller.js +46 -13
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js +1 -0
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +18 -10
- package/dist/controllers/drawer_controller.js +54 -19
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +9 -3
- package/dist/controllers/dropdown_controller.js +36 -9
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.js +34 -0
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js +144 -51
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +10 -4
- package/dist/controllers/filter_controller.js +20 -6
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +26 -6
- package/dist/controllers/flash_controller.js +432 -71
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.js +1 -0
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +7 -5
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +19 -13
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js +45 -8
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js +82 -25
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +10 -2
- package/dist/controllers/hover_card_controller.js +40 -14
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +16 -3
- package/dist/controllers/idle_controller.js +90 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +5 -2
- package/dist/controllers/input_mask_controller.js +65 -9
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js +3 -0
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js +11 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +50 -8
- package/dist/controllers/listbox_controller.js +203 -45
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.js +10 -5
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -3
- package/dist/controllers/masonry_controller.js +31 -15
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +9 -3
- package/dist/controllers/menu_controller.js +45 -16
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +11 -0
- package/dist/controllers/menubar_controller.js +58 -24
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.js +9 -5
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +18 -3
- package/dist/controllers/multi_select_controller.js +278 -104
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +11 -0
- package/dist/controllers/navigation_menu_controller.js +48 -15
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js +37 -8
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js +9 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +36 -8
- package/dist/controllers/number_input_controller.js +124 -21
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +42 -5
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +22 -7
- package/dist/controllers/otp_controller.js +198 -55
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
- package/dist/controllers/overflow_indicator_controller.js +115 -21
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +26 -6
- package/dist/controllers/overflow_menu_controller.js +141 -44
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +19 -11
- package/dist/controllers/pagination_controller.js +74 -28
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +15 -1
- package/dist/controllers/password_reveal_controller.js +59 -2
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js +30 -8
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.js +131 -52
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +9 -3
- package/dist/controllers/popover_controller.js +45 -11
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +1 -1
- package/dist/controllers/portal_controller.js +6 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.js +16 -1
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js +8 -4
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +6 -4
- package/dist/controllers/radio_group_controller.js +42 -17
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +49 -1
- package/dist/controllers/range_slider_controller.js +88 -42
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +14 -3
- package/dist/controllers/rating_controller.js +39 -15
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +24 -2
- package/dist/controllers/read_more_controller.js +100 -7
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.js +65 -19
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js +10 -5
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +16 -2
- package/dist/controllers/resizable_controller.js +82 -22
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js +75 -27
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +37 -16
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.js +49 -30
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +3 -2
- package/dist/controllers/scrollspy_controller.js +71 -26
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +41 -14
- package/dist/controllers/separator_controller.js +66 -37
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +20 -3
- package/dist/controllers/sidebar_controller.js +77 -18
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js +6 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +45 -7
- package/dist/controllers/slider_controller.js +82 -47
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.js +60 -26
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +17 -2
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js +10 -2
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +19 -17
- package/dist/controllers/step_indicator_controller.js +18 -17
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +34 -9
- package/dist/controllers/stepper_controller.js +101 -19
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
- package/dist/controllers/stick_to_bottom_controller.js +104 -8
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -2
- package/dist/controllers/submit_once_controller.js +45 -9
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +29 -7
- package/dist/controllers/switch_controller.js +101 -10
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +12 -0
- package/dist/controllers/tabs_controller.js +21 -2
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +15 -3
- package/dist/controllers/tags_input_controller.js +209 -59
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js +29 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +20 -3
- package/dist/controllers/theme_controller.js +64 -14
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js +23 -8
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +55 -15
- package/dist/controllers/toast_controller.js +451 -105
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +49 -7
- package/dist/controllers/toggle_group_controller.js +159 -23
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +32 -0
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +8 -0
- package/dist/controllers/tooltip_controller.js +39 -13
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.js +4 -0
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +39 -8
- package/dist/controllers/tree_view_controller.js +169 -16
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +5002 -1911
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +72 -6
- package/dist/inspector/cli.js +262 -51
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +309 -51
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +40 -40
- package/dist/inspector/manifest.json +529 -61
- package/dist/positioning/index.js +2 -0
- package/dist/positioning/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/utils/transition_completion.ts","../../src/controllers/flash_controller.ts"],"names":[],"mappings":"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","file":"flash_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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 { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { maxTransitionTotalMs } from \"../utils/transition_completion\";\n\n/** Flash types that map to an assertive `alert` (everything else is a polite `status`). */\nconst ASSERTIVE_TYPES = new Set([\"alert\", \"error\"]);\n\n/**\n * Selector for message targets: spots dynamic inserts, resolves a close control's\n * message, and confirms a departed target was one.\n */\nconst MESSAGE_SELECTOR = '[data-stimeo--flash-target=\"message\"]';\n\n/** Per-message auto-dismiss timer bookkeeping (id 0 means paused). */\ninterface FlashTimer {\n id: number;\n startedAt: number;\n remaining: number;\n /** Active pause reasons; the timer resumes only once every one is released. */\n paused: Set<\"focus\" | \"hover\">;\n}\n\n/**\n * Headless **Rails flash bridge**: turns server-rendered (and Turbo Stream-inserted)\n * `flash` elements into live-region announcements with auto-dismiss and a stacking\n * cap (no dedicated APG pattern; follows the WAI-ARIA status/alert guidance and WCAG\n * 2.2 **4.1.3 Status Messages**). The general-purpose sibling is Toast; this one is\n * specialized to the Rails `flash` convention.\n *\n * Markup contract (identifier: `stimeo--flash`):\n * <div data-controller=\"stimeo--flash\" data-stimeo--flash-target=\"region\">\n * <!-- server-rendered or Turbo Stream-inserted -->\n * <div data-stimeo--flash-target=\"message\" data-flash-type=\"notice\">Saved</div>\n * </div>\n *\n * Each message is mapped by `data-flash-type` to `role=\"status\"` (notice) or\n * `role=\"alert\"` (alert/error), flagged `data-flash-state=\"visible\"`, auto-dismissed\n * after `duration` (paused while hovered *or* focused when `pauseOnHover`, and\n * resumed only once both are released), and capped at `max` simultaneous messages.\n * A close button wired to the `dismiss` action removes one manually.\n *\n * `dismiss` dispatches `{ element, reason }`.\n *\n * `show` dispatches `{ type, message }`.\n * `reconcile` dispatches `{ removed: number }` — the messages the Turbo cache\n * rewind took out, which no `dismiss` will report because nobody dismissed them.\n *\n * @remarks\n * Reading is **delegated to the shared Announcer** — but only for the *initial*,\n * page-loaded messages: an in-place live region present at load is not announced by\n * assistive tech, so the controller bridges those via an `stimeo--announcer:announce`\n * event. Messages inserted *later* (Turbo Stream) are announced by their own freshly\n * inserted `role`, exactly like Toast, so they are not bridged again (no double\n * announcement). Behavior only — no styling; `data-flash-state=\"leaving\"` lets CSS\n * animate removal. Focus is never moved (WCAG 2.2 4.1.3): pausing never removes a\n * message, so the control under the pointer or holding focus stays put. The\n * observation follows a `region` element replaced at runtime; the managed set is the\n * current region's subtree, and a message that leaves it gives up its stacking slot,\n * its pending auto-dismiss, and any removal already scheduled. The observer, timers,\n * and per-message listeners are torn down on `disconnect()` (Turbo navigation\n * included), and the managed flashes leave the page before Turbo caches it so a\n * restored snapshot does not replay a notification the visitor already received.\n */\nexport class FlashController extends Controller<HTMLElement> {\n static override targets = [\"region\", \"message\"];\n static override values = {\n duration: { type: Number, default: 5000 },\n pauseOnHover: { type: Boolean, default: true },\n max: { type: Number, default: 0 },\n };\n static actions = [\"dismiss\"] as const;\n static events = [\"show\", \"dismiss\", \"reconcile\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTargets: HTMLElement[];\n declare readonly hasRegionTarget: boolean;\n\n declare durationValue: number;\n declare pauseOnHoverValue: boolean;\n declare maxValue: number;\n\n readonly #timers = new SafeTimeout();\n #observer: MutationObserver | null = null;\n /** Whether the controller is between `connect()` and `disconnect()`. */\n #connected = false;\n /** Auto-dismiss timer state keyed by message element. */\n readonly #state = new Map<HTMLElement, FlashTimer>();\n /** Messages already processed, in insertion order, to enforce `max` and avoid double work. */\n readonly #order: HTMLElement[] = [];\n /**\n * Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}\n * releases the bookkeeping above *before* the transition wait, so for that window the\n * element is in the DOM but in neither collection — without this set a re-scan would\n * read it as a brand-new flash and show it a second time.\n */\n readonly #leaving = new Set<HTMLElement>();\n\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n readonly #onEnter = (event: Event): void =>\n this.#pause(event.currentTarget as HTMLElement, event.type === \"focusin\" ? \"focus\" : \"hover\");\n readonly #onLeave = (event: Event): void =>\n this.#resume(event.currentTarget as HTMLElement, event.type === \"focusout\" ? \"focus\" : \"hover\");\n\n override connect(): void {\n this.#connected = true;\n // Initial, server-rendered flashes: bridge them to the Announcer because an\n // in-place live region present at page load is not announced on its own.\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, true);\n }\n this.#syncObservation();\n this.#beforeCache.activate();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#beforeCache.deactivate();\n this.#stopObserving();\n this.#timers.clearAll();\n for (const message of this.#order) this.#unbindPause(message);\n this.#state.clear();\n this.#order.length = 0;\n // The pending finalizes died with the timers above, so a message still marked\n // `leaving` is free to be shown again by the next connect (a snapshot restore).\n this.#leaving.clear();\n }\n\n /**\n * Takes the managed flashes out of the page just before Turbo freezes it, so a\n * restored snapshot carries no notification the visitor has already received: the\n * fresh `connect()` there reads a leftover flash as a brand-new one and announces it\n * a second time. A message that never auto-dismisses (`duration: 0`) is one of these\n * too — that value governs the timer, not what belongs in a cached page. Removal\n * only: `dismiss` reports a dismissal, and freezing the page is not one.\n */\n #rewindForCache(): void {\n const removed = this.#order.length;\n for (const message of [...this.#order]) {\n message.remove();\n this.#forget(message);\n }\n for (const message of this.#leaving) message.remove();\n this.#leaving.clear();\n if (removed > 0) this.dispatch(\"reconcile\", { detail: { removed } });\n }\n\n /** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */\n regionTargetConnected(): void {\n this.#resync();\n }\n\n /** Releases the observation when the `region` element leaves the target set. */\n regionTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Whether this controller owns `message`. Ownership is the current `region`'s\n * subtree: a message target anywhere else in the controller's scope is the\n * consumer's, and so is one in a region that has gone away. The initial scan, a\n * re-scan after a `region` swap, and a departure from the target set all resolve\n * ownership through this one test; the observation gets it structurally, by watching\n * that subtree and nothing else.\n */\n #owns(message: HTMLElement): boolean {\n return this.hasRegionTarget && this.regionTarget.contains(message);\n }\n\n /**\n * Releases a message that left the target set (a Turbo Stream `remove`, the consumer\n * detaching the node, or a morph that rewrote the target attribute in place): it\n * stops occupying a `max` slot, and both its pending auto-dismiss and an already\n * scheduled removal are cancelled. A move *within* the region keeps all of them —\n * which is why the element must still be a message to be treated as one: ownership\n * alone reads an in-place attribute rewrite as a move, and a node outside the target\n * set belongs to the consumer, so nothing here may dismiss it.\n */\n messageTargetDisconnected(message: HTMLElement): void {\n if (!this.#connected) return;\n const moved = this.#owns(message) && message.matches(MESSAGE_SELECTOR);\n if (moved) return;\n this.#forget(message);\n // Ownership is re-checked at both ends of the leaving transition: dropping the\n // claim here is what the pending finalize reads to leave the node alone.\n this.#leaving.delete(message);\n }\n\n /**\n * Re-points the observation after a `region` swap and picks up the messages the\n * new element brought with it (dynamic inserts, so their own `role` announces\n * them). The `#connected` guard is load-bearing: Stimulus runs target callbacks\n * for the initial markup *before* `connect()` and again during teardown *after*\n * `disconnect()`, and re-observing there would outlive the controller.\n */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncObservation();\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, false);\n }\n }\n\n /**\n * Points the mutation observation at the current `region` target, re-resolved on\n * every sync rather than captured at connect, so an element swapped in at runtime\n * is observed instead of the detached original.\n */\n #syncObservation(): void {\n this.#stopObserving();\n if (!this.hasRegionTarget || typeof MutationObserver === \"undefined\") return;\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.regionTarget, { childList: true, subtree: true });\n }\n\n #stopObserving(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * Pause-on-hover/focus listeners, bound and unbound as a pair so the two sides\n * stay in sync. Binding is gated by `pauseOnHover`; unbinding is unconditional\n * and idempotent (a no-op when nothing was bound), which keeps teardown correct\n * even if `pauseOnHover` were ever toggled during a message's life.\n */\n #bindPause(message: HTMLElement): void {\n if (!this.pauseOnHoverValue) return;\n message.addEventListener(\"mouseenter\", this.#onEnter);\n message.addEventListener(\"mouseleave\", this.#onLeave);\n message.addEventListener(\"focusin\", this.#onEnter);\n message.addEventListener(\"focusout\", this.#onLeave);\n }\n\n #unbindPause(message: HTMLElement): void {\n message.removeEventListener(\"mouseenter\", this.#onEnter);\n message.removeEventListener(\"mouseleave\", this.#onLeave);\n message.removeEventListener(\"focusin\", this.#onEnter);\n message.removeEventListener(\"focusout\", this.#onLeave);\n }\n\n /** Dismisses the flash whose close control fired the event. */\n dismiss(event: Event): void {\n const target = (event.currentTarget || event.target) as HTMLElement | null;\n const message = target?.closest<HTMLElement>(MESSAGE_SELECTOR);\n if (message) this.#beginDismiss(message, \"user\");\n }\n\n /** Processes messages added after connect (Turbo Stream); their own role announces them. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.matches(MESSAGE_SELECTOR)) this.#process(node, false);\n for (const message of node.querySelectorAll<HTMLElement>(MESSAGE_SELECTOR)) {\n this.#process(message, false);\n }\n }\n }\n }\n\n /**\n * Applies role/state, wires pause listeners, schedules auto-dismiss, and either\n * bridges to the Announcer (`bridge`, for initial flashes) or leaves the message's\n * own role to do the announcing (dynamic inserts). Idempotent per message.\n */\n #process(message: HTMLElement, bridge: boolean): void {\n if (this.#state.has(message) || this.#order.includes(message)) return;\n if (this.#leaving.has(message)) return;\n\n const type = message.getAttribute(\"data-flash-type\") ?? \"\";\n const assertive = ASSERTIVE_TYPES.has(type);\n // Don't clobber an authored role; otherwise map the flash type to a live role.\n if (!message.hasAttribute(\"role\")) {\n message.setAttribute(\"role\", assertive ? \"alert\" : \"status\");\n }\n message.setAttribute(\"data-flash-state\", \"visible\");\n this.#order.push(message);\n\n this.#bindPause(message);\n\n const text = message.textContent?.trim() ?? \"\";\n this.dispatch(\"show\", { target: message, detail: { type, message: text } });\n if (bridge && text) {\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", { detail: { message: text, assertive } }),\n );\n }\n\n this.#startTimer(message);\n this.#enforceMax();\n }\n\n /** Removes the oldest visible flashes once the count exceeds `max` (0 = unlimited). */\n #enforceMax(): void {\n if (this.maxValue <= 0) return;\n while (this.#order.length > this.maxValue) {\n const oldest = this.#order[0];\n if (!oldest) break;\n this.#beginDismiss(oldest, \"limit\");\n }\n }\n\n #startTimer(message: HTMLElement, duration = this.durationValue): void {\n if (duration <= 0) return;\n const existing = this.#state.get(message);\n if (existing?.id) this.#timers.clear(existing.id);\n const id = this.#timers.set(() => this.#beginDismiss(message, \"timeout\"), duration);\n this.#state.set(message, {\n id,\n startedAt: Date.now(),\n remaining: duration,\n paused: existing?.paused ?? new Set(),\n });\n }\n\n /**\n * Pauses a message's auto-dismiss, banking the time left (hover/focus, WCAG 2.2.1).\n * Hover and focus are independent reasons: the remaining time is banked on the\n * first of them, and {@link FlashController.#resume} waits for the last one.\n */\n #pause(message: HTMLElement, reason: \"focus\" | \"hover\"): void {\n const timer = this.#state.get(message);\n if (!timer) return;\n timer.paused.add(reason);\n if (timer.id === 0) return;\n\n this.#timers.clear(timer.id);\n // The bank has a floor of 1ms, so a deadline already spent when the pause arrives\n // (the timer sat queued in a throttled tab or behind a long task) settles right\n // after the last reason is released rather than during the event that paused it.\n // Pausing therefore never removes a message — which is what keeps the pointer's\n // target under the pointer and the focused control focused (WCAG 2.2 4.1.3).\n // Banking zero instead would strand the message: nothing resumes a spent window.\n const remaining = Math.max(1, timer.remaining - (Date.now() - timer.startedAt));\n this.#state.set(message, { id: 0, startedAt: 0, remaining, paused: timer.paused });\n }\n\n /** Resumes a paused message's auto-dismiss with the banked time. */\n #resume(message: HTMLElement, reason: \"focus\" | \"hover\"): void {\n const timer = this.#state.get(message);\n if (!timer) return;\n timer.paused.delete(reason);\n // Still held by the other reason (the pointer left, focus stayed, or vice versa).\n if (timer.paused.size > 0) return;\n // Only resume a genuinely paused timer; a running one keeps its own deadline.\n if (timer.id !== 0) return;\n this.#startTimer(message, timer.remaining);\n }\n\n /** Releases every per-message resource: timer, stacking slot, pause listeners. */\n #forget(message: HTMLElement): void {\n const timer = this.#state.get(message);\n if (timer?.id) this.#timers.clear(timer.id);\n this.#state.delete(message);\n const index = this.#order.indexOf(message);\n if (index !== -1) this.#order.splice(index, 1);\n this.#unbindPause(message);\n }\n\n /** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */\n #beginDismiss(message: HTMLElement, reason: \"timeout\" | \"user\" | \"limit\"): void {\n // One removal, one `dismiss`: a close control fired during the leaving\n // transition, or a repeated request, finds the message already released and\n // must not start a second finalize. Reading the bookkeeping rather than\n // `data-flash-state` also keeps `#enforceMax`'s loop terminating, since the\n // release below is what shrinks `#order`.\n if (!this.#state.has(message) && !this.#order.includes(message)) return;\n this.#forget(message);\n this.#leaving.add(message);\n\n message.setAttribute(\"data-flash-state\", \"leaving\");\n\n const finalize = (): void => {\n // The claim taken above is the license to remove: a message that lost the target\n // attribute during the transition belongs to the consumer by the time this runs.\n if (!this.#leaving.delete(message)) return;\n message.remove();\n this.dispatch(\"dismiss\", { detail: { element: message, reason } });\n };\n\n const transition = this.#transitionMs(message);\n if (transition > 0) {\n this.#timers.set(finalize, transition);\n } else {\n finalize();\n }\n }\n\n /** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */\n #transitionMs(el: HTMLElement): number {\n if (typeof window.getComputedStyle !== \"function\") return 0;\n return maxTransitionTotalMs(window.getComputedStyle(el));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/event_owner.ts","../../src/utils/safe_timeout.ts","../../src/utils/keyed_timers.ts","../../src/utils/listener_set.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/pausable_timers.ts","../../src/utils/target_selector.ts","../../src/utils/transition_completion.ts","../../src/controllers/flash_controller.ts"],"names":[],"mappings":";;;;;AAoBO,SAAS,QAAA,CAAS,OAAA,EAAiB,OAAA,GAAmC,EAAC,EAAS;AACrF,EAAA,MAAM,IAAA,GAAO,QAAQ,IAAA,EAAK;AAC1B,EAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACvB,EAAA,MAAA,CAAO,aAAA;AAAA,IACL,IAAI,YAAY,4BAAA,EAA8B;AAAA,MAC5C,QAAQ,EAAE,OAAA,EAAS,MAAM,SAAA,EAAW,OAAA,CAAQ,cAAc,IAAA;AAAK,KAChE;AAAA,GACH;AACF;;;ACeO,IAAM,gBAAA,GAAN,MAAM,iBAAA,CAAiB;AAAA;AAAA,EAE5B,OAAgB,YAAA,mBAAe,IAAI,GAAA,EAAsB;AAAA;AAAA,EAGzD,OAAgB,iBAAiB,MAAY;AAC3C,IAAA,KAAA,MAAW,UAAA,IAAc,iBAAA,CAAiB,YAAA,EAAc,UAAA,CAAW,OAAA,EAAQ;AAAA,EAC7E,CAAA;AAAA,EAES,OAAA;AAAA;AAAA,EAGT,YAAY,MAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,MAAM,KAAA,GAAQ,iBAAA,CAAiB,YAAA,CAAa,IAAA,KAAS,CAAA;AACrD,IAAA,iBAAA,CAAiB,YAAA,CAAa,IAAI,IAAI,CAAA;AACtC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,IACjF;AAAA,EACF;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,iBAAA,CAAiB,YAAA,CAAa,OAAO,IAAI,CAAA;AACzC,IAAA,IAAI,iBAAA,CAAiB,YAAA,CAAa,IAAA,GAAO,CAAA,EAAG;AAC5C,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,EACpF;AACF,CAAA;;;AC1CO,SAAS,UAAA,CACd,YACA,IAAA,EACQ;AACR,EAAA,IAAI,EAAE,IAAA,YAAgB,IAAA,CAAA,EAAO,OAAO,EAAA;AACpC,EAAA,OAAO,WAAW,SAAA,CAAU,CAAC,cAAc,SAAA,CAAU,QAAA,CAAS,IAAI,CAAC,CAAA;AACrE;AAQO,SAAS,OAAA,CACd,YACA,IAAA,EACU;AACV,EAAA,OAAO,UAAA,CAAW,UAAA,CAAW,UAAA,EAAY,IAAI,CAAC,CAAA,IAAK,IAAA;AACrD;;;ACnBA,IAAe,gBAAf,MAA6B;AAAA;AAAA,EAER,GAAA,uBAAU,GAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAczC,MAAM,EAAA,EAAkB;AACtB,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,EAAE,CAAA,EAAG;AACvB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,KAAA,MAAW,EAAA,IAAM,KAAK,GAAA,EAAK;AACzB,MAAA,IAAA,CAAK,OAAO,EAAE,CAAA;AAAA,IAChB;AACA,IAAA,IAAA,CAAK,IAAI,KAAA,EAAM;AAAA,EACjB;AAAA;AAAA,EAGA,IAAI,IAAA,GAAe;AACjB,IAAA,OAAO,KAAK,GAAA,CAAI,IAAA;AAAA,EAClB;AACF,CAAA;AAmBO,IAAM,WAAA,GAAN,cAA0B,aAAA,CAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO7C,GAAA,CAAI,UAAsB,KAAA,EAAuB;AAC/C,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,QAAA,CAAS,MAAM;AAC7B,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,EAAE,CAAA;AAClB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,GAAA,CAAI,IAAI,EAAE,CAAA;AACf,IAAA,OAAO,EAAA;AAAA,EACT;AAAA,EAEU,QAAA,CAAS,UAAsB,KAAA,EAAuB;AAC9D,IAAA,OAAO,MAAA,CAAO,UAAA,CAAW,QAAA,EAAU,KAAK,CAAA;AAAA,EAC1C;AAAA,EAEU,OAAO,EAAA,EAAkB;AACjC,IAAA,MAAA,CAAO,aAAa,EAAE,CAAA;AAAA,EACxB;AACF,CAAA;;;ACrEO,IAAM,cAAN,MAAqB;AAAA,EACjB,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA;AAAA,EAG1B,QAAA,uBAAe,GAAA,EAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOvC,GAAA,CAAI,GAAA,EAAQ,QAAA,EAAsB,KAAA,EAAqB;AACrD,IAAA,IAAA,CAAK,MAAM,GAAG,CAAA;AACd,IAAA,MAAM,EAAA,GAAK,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,MAAM;AAChC,MAAA,IAAA,CAAK,QAAA,CAAS,OAAO,GAAG,CAAA;AACxB,MAAA,QAAA,EAAS;AAAA,IACX,GAAG,KAAK,CAAA;AACR,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAA,EAAK,EAAE,CAAA;AAAA,EAC3B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,GAAA,EAAc;AAClB,IAAA,IAAA,CAAK,QAAQ,KAAA,CAAM,IAAA,CAAK,SAAS,GAAA,CAAI,GAAG,KAAK,EAAE,CAAA;AAC/C,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,GAAG,CAAA;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AAAA,EACtB;AAAA;AAAA,EAGA,IAAI,GAAA,EAAiB;AACnB,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AAAA,EAC9B;AACF,CAAA;;;ACjCO,IAAM,cAAN,MAAkB;AAAA;AAAA,EAEvB,MAAA,GAAS,IAAI,eAAA,EAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAS7B,GAAA,CACE,MAAA,EACA,IAAA,EACA,OAAA,EACA,OAAA,EACM;AACN,IAAA,MAAA,CAAO,gBAAA,CAAiB,IAAA,EAAM,OAAA,EAAS,EAAE,GAAG,SAAS,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,MAAA,EAAQ,CAAA;AAAA,EACnF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,OAAO,KAAA,EAAM;AAClB,IAAA,IAAA,CAAK,MAAA,GAAS,IAAI,eAAA,EAAgB;AAAA,EACpC;AACF,CAAA;;;AC7BO,IAAM,qBAAN,MAAyB;AAAA,EACrB,IAAA;AAAA,EACT,OAAA,GAAU,KAAA;AAAA,EACV,OAAA,GAAU,KAAA;AAAA,EACV,WAAA,GAAc,CAAA;AAAA;AAAA,EAGd,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,IAAA,GAAO,GAAA;AAAA,EACd;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,WAAA,IAAe,CAAA;AAAA,EACtB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,MAAM,aAAa,IAAA,CAAK,WAAA;AACxB,IAAA,cAAA,CAAe,MAAM;AAEnB,MAAA,IAAI,UAAA,KAAe,KAAK,WAAA,IAAe,CAAC,KAAK,OAAA,IAAW,CAAC,KAAK,OAAA,EAAS;AACvE,MAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ,CAAC,CAAA;AAAA,EACH;AACF,CAAA;;;AClCO,IAAM,iBAAN,MAAwB;AAAA,EACpB,OAAA,GAAU,IAAI,WAAA,EAAe;AAAA;AAAA,EAG7B,QAAA,uBAAe,GAAA,EAAc;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtC,GAAA,CAAI,GAAA,EAAQ,QAAA,EAAsB,KAAA,EAAqB;AACrD,IAAA,MAAM,OAAA,GAAU,KAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA,EAAG,OAAA,wBAAe,GAAA,EAAY;AACnE,IAAA,MAAM,QAAe,EAAE,QAAA,EAAU,WAAW,CAAA,EAAG,SAAA,EAAW,OAAO,OAAA,EAAQ;AACzE,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAC5B,IAAA,IAAI,QAAQ,IAAA,KAAS,CAAA,OAAQ,IAAA,CAAK,GAAA,EAAK,OAAO,QAAQ,CAAA;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,KAAA,CAAM,KAAQ,MAAA,EAAyB;AACrC,IAAA,IAAI,KAAA,GAAQ,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AACjC,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,KAAA,GAAQ,EAAE,QAAA,EAAU,IAAA,EAAM,SAAA,EAAW,CAAA,EAAG,WAAW,CAAA,EAAG,OAAA,kBAAS,IAAI,GAAA,EAAY,EAAE;AACjF,MAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAA,EAAK,KAAK,CAAA;AAAA,IAC9B;AACA,IAAA,KAAA,CAAM,OAAA,CAAQ,IAAI,MAAM,CAAA;AACxB,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,GAAG,GAAG,OAAO,KAAA;AACnC,IAAA,IAAA,CAAK,OAAA,CAAQ,MAAM,GAAG,CAAA;AACtB,IAAA,KAAA,CAAM,SAAA,GAAY,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,KAAA,CAAM,aAAa,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,SAAA,CAAU,CAAA;AAC9E,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAA,CAAO,KAAQ,MAAA,EAAyB;AACtC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AACnC,IAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAChC,IAAA,KAAA,CAAM,OAAA,CAAQ,OAAO,MAAM,CAAA;AAC3B,IAAA,IAAI,KAAA,CAAM,QAAQ,IAAA,GAAO,CAAA,IAAK,KAAK,OAAA,CAAQ,GAAA,CAAI,GAAG,CAAA,EAAG,OAAO,KAAA;AAC5D,IAAA,MAAM,EAAE,UAAS,GAAI,KAAA;AACrB,IAAA,IAAI,aAAa,IAAA,EAAM;AACrB,MAAA,IAAA,CAAK,QAAA,CAAS,OAAO,GAAG,CAAA;AACxB,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,IAAA,CAAK,IAAA,CAAK,GAAA,EAAK,KAAA,EAAO,QAAQ,CAAA;AAC9B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,GAAA,EAAiB;AACtB,IAAA,OAAA,CAAQ,KAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA,EAAG,OAAA,CAAQ,QAAQ,CAAA,IAAK,CAAA;AAAA,EACvD;AAAA;AAAA,EAGA,OAAO,GAAA,EAAiB;AACtB,IAAA,OAAO,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,GAAA,EAAc;AACnB,IAAA,IAAA,CAAK,OAAA,CAAQ,MAAM,GAAG,CAAA;AACtB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AACnC,IAAA,IAAI,KAAA,EAAO,OAAA,CAAQ,IAAA,EAAM,KAAA,CAAM,QAAA,GAAW,IAAA;AAAA,SACrC,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,GAAG,CAAA;AAAA,EAC/B;AAAA;AAAA,EAGA,MAAM,GAAA,EAAc;AAClB,IAAA,IAAA,CAAK,OAAA,CAAQ,MAAM,GAAG,CAAA;AACtB,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,GAAG,CAAA;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AAAA,EACtB;AAAA;AAAA,EAGA,IAAA,CAAK,GAAA,EAAQ,KAAA,EAAc,QAAA,EAA4B;AACrD,IAAA,KAAA,CAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,IAAA,IAAA,CAAK,OAAA,CAAQ,GAAA;AAAA,MACX,GAAA;AAAA,MACA,MAAM;AACJ,QAAA,IAAA,CAAK,QAAA,CAAS,OAAO,GAAG,CAAA;AACxB,QAAA,QAAA,EAAS;AAAA,MACX,CAAA;AAAA,MACA,KAAA,CAAM;AAAA,KACR;AAAA,EACF;AACF,CAAA;;;AClJO,SAAS,cAAA,CAAe,YAAoB,IAAA,EAAsB;AACvE,EAAA,OAAO,CAAA,MAAA,EAAS,UAAU,CAAA,UAAA,EAAa,IAAI,CAAA,EAAA,CAAA;AAC7C;;;ACEA,SAAS,OAAO,KAAA,EAAuB;AACrC,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,MAAM,MAAA,GAAS,MAAA,CAAO,UAAA,CAAW,OAAO,CAAA;AACxC,EAAA,IAAI,CAAC,MAAA,CAAO,QAAA,CAAS,MAAM,GAAG,OAAO,CAAA;AACrC,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,EAAG,OAAO,MAAA;AACnC,EAAA,IAAI,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,SAAU,MAAA,GAAS,GAAA;AAC3C,EAAA,OAAO,CAAA;AACT;AAGA,SAAS,QAAQ,KAAA,EAAyB;AACxC,EAAA,OAAO,KAAA,CACJ,KAAA,CAAM,GAAG,CAAA,CACT,GAAA,CAAI,CAAC,IAAA,KAAS,IAAA,CAAK,IAAA,EAAM,CAAA,CACzB,MAAA,CAAO,OAAO,CAAA;AACnB;AASA,SAAS,kBAAkB,KAAA,EAA4C;AACrE,EAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,KAAA,CAAM,kBAAkB,CAAA;AACnD,EAAA,MAAM,YAAY,OAAA,CAAQ,KAAA,CAAM,kBAAkB,CAAA,CAAE,IAAI,MAAM,CAAA;AAC9D,EAAA,MAAM,SAAS,OAAA,CAAQ,KAAA,CAAM,eAAe,CAAA,CAAE,IAAI,MAAM,CAAA;AACxD,EAAA,MAAM,sBACJ,UAAA,CAAW,MAAA,GAAS,IAChB,UAAA,GACA,KAAA,CAAM,KAAK,EAAE,MAAA,EAAQ,KAAK,GAAA,CAAI,SAAA,CAAU,QAAQ,MAAA,CAAO,MAAA,EAAQ,CAAC,CAAA,EAAE,EAAG,MAAM,KAAK,CAAA;AACtF,EAAA,MAAM,qBAAqB,SAAA,CAAU,MAAA,GAAS,CAAA,GAAI,SAAA,GAAY,CAAC,CAAC,CAAA;AAChE,EAAA,MAAM,kBAAkB,MAAA,CAAO,MAAA,GAAS,CAAA,GAAI,MAAA,GAAS,CAAC,CAAC,CAAA;AAEvD,EAAA,OAAO,mBAAA,CACJ,MAAA,CAAO,CAAC,QAAA,KAAa,QAAA,KAAa,MAAM,CAAA,CACxC,GAAA,CAAI,CAAC,QAAA,EAAU,KAAA,MAAW;AAAA,IACzB,QAAA;AAAA,IACA,SAAS,IAAA,CAAK,GAAA;AAAA,MACZ,CAAA;AAAA,MAAA,CACC,kBAAA,CAAmB,KAAA,GAAQ,kBAAA,CAAmB,MAAM,CAAA,IAAK,MACvD,eAAA,CAAgB,KAAA,GAAQ,eAAA,CAAgB,MAAM,CAAA,IAAK,CAAA;AAAA;AACxD,GACF,CAAE,CAAA;AACN;AAGA,SAAS,WAAW,OAAA,EAAqC;AACvD,EAAA,OAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,GAAA,EAAK,EAAE,OAAA,EAAQ,KAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,OAAO,CAAA,EAAG,CAAC,CAAA;AACvE;AAGO,SAAS,qBAAqB,KAAA,EAAgC;AACnE,EAAA,OAAO,UAAA,CAAW,iBAAA,CAAkB,KAAK,CAAC,CAAA;AAC5C;;;ACzDA,IAAM,kCAAkB,IAAI,GAAA,CAAI,CAAC,OAAA,EAAS,OAAO,CAAC,CAAA;AAMlD,IAAM,YAAA,GAAe,SAAA;AAqDd,IAAM,eAAA,GAAN,cAA8B,UAAA,CAAwB;AAAA;AAAA,EAE3D,IAAI,gBAAA,GAA2B;AAC7B,IAAA,OAAO,cAAA,CAAe,IAAA,CAAK,UAAA,EAAY,YAAY,CAAA;AAAA,EACrD;AAAA,EAEA,OAAgB,OAAA,GAAU,CAAC,QAAA,EAAU,SAAS,CAAA;AAAA,EAC9C,OAAgB,MAAA,GAAS;AAAA,IACvB,QAAA,EAAU,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,GAAA,EAAK;AAAA,IACxC,YAAA,EAAc,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,IAAA,EAAK;AAAA,IAC7C,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA;AAAE,GAClC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,SAAS,CAAA;AAAA,EAC3B,OAAO,MAAA,GAAS,CAAC,MAAA,EAAQ,WAAW,WAAW,CAAA;AAAA;AAAA,EAWtC,OAAA,GAAU,IAAI,WAAA,EAAY;AAAA;AAAA;AAAA;AAAA;AAAA,EAK1B,QAAA,GAAW,IAAI,cAAA,EAA4B;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3C,aAAA,GAAgB,IAAI,WAAA,EAAyB;AAAA;AAAA,EAE7C,WAAW,IAAI,kBAAA,CAAmB,MAAM,IAAA,CAAK,aAAa,CAAA;AAAA;AAAA,EAEnE,UAAyB,EAAC;AAAA;AAAA,EAEjB,QAAA,GAAW,IAAI,WAAA,EAAY;AAAA;AAAA,EAE3B,gBAAA,uBAAuB,GAAA,EAAiB;AAAA,EACjD,SAAA,GAAqC,IAAA;AAAA;AAAA,EAErC,UAAA,GAAa,KAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMb,WAAA,GAA6B,IAAA;AAAA;AAAA,EAEpB,SAAwB,EAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOzB,QAAA,uBAAe,GAAA,EAAiB;AAAA,EAEhC,eAAe,IAAI,gBAAA,CAAiB,MAAM,IAAA,CAAK,iBAAiB,CAAA;AAAA,EAEhE,QAAA,GAAW,CAAC,KAAA,KACnB,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,aAAA,EAA8B,KAAA,CAAM,IAAA,KAAS,SAAA,GAAY,OAAA,GAAU,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOrF,QAAA,GAAW,CAAC,KAAA,KAAyC;AAC5D,IAAA,MAAM,UAAU,KAAA,CAAM,aAAA;AACtB,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,IAAA,CAAK,MAAA,EAAQ,MAAM,aAAa,CAAA;AACrD,IAAA,IAAI,SAAS,OAAA,EAAS;AACtB,IAAA,IAAA,CAAK,QAAQ,OAAA,EAAS,KAAA,CAAM,SAAS,UAAA,GAAa,OAAA,GAAU,SAAS,IAAI,CAAA;AAAA,EAC3E,CAAA;AAAA,EAES,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,SAAS,QAAA,EAAS;AAGvB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,cAAA,EAAgB;AACzC,MAAA,IAAI,KAAK,KAAA,CAAM,OAAO,GAAG,IAAA,CAAK,QAAA,CAAS,SAAS,IAAI,CAAA;AAAA,IACtD;AACA,IAAA,IAAI,CAAC,MAAA,CAAO,EAAA,CAAG,KAAK,WAAA,EAAa,IAAA,CAAK,QAAQ,CAAA,EAAG;AAC/C,MAAA,IAAA,CAAK,cAAc,IAAA,CAAK,QAAA;AACxB,MAAA,IAAA,CAAK,WAAA,EAAY;AAAA,IACnB;AACA,IAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,IAAA,IAAA,CAAK,aAAa,QAAA,EAAS;AAAA,EAC7B;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAA,CAAK,QAAQ,QAAA,EAAS;AACtB,IAAA,IAAA,CAAK,SAAS,QAAA,EAAS;AACvB,IAAA,IAAA,CAAK,cAAc,QAAA,EAAS;AAC5B,IAAA,IAAA,CAAK,SAAS,MAAA,EAAO;AACrB,IAAA,IAAA,CAAK,UAAU,EAAC;AAChB,IAAA,IAAA,CAAK,SAAS,OAAA,EAAQ;AACtB,IAAA,IAAA,CAAK,iBAAiB,KAAA,EAAM;AAC5B,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,MAAA,EAAQ,IAAA,CAAK,aAAa,OAAO,CAAA;AAC5D,IAAA,IAAA,CAAK,OAAO,MAAA,GAAS,CAAA;AAGrB,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eAAA,GAAwB;AACtB,IAAA,MAAM,OAAA,GAAU,KAAK,MAAA,CAAO,MAAA;AAC5B,IAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,IAAA,CAAK,MAAM,CAAA,EAAG;AACtC,MAAA,OAAA,CAAQ,MAAA,EAAO;AACf,MAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AAAA,IACtB;AACA,IAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,QAAA,EAAU,OAAA,CAAQ,MAAA,EAAO;AACpD,IAAA,IAAA,CAAK,SAAS,KAAA,EAAM;AACpB,IAAA,IAAI,OAAA,GAAU,CAAA,EAAG,IAAA,CAAK,QAAA,CAAS,WAAA,EAAa,EAAE,MAAA,EAAQ,EAAE,OAAA,EAAQ,EAAG,CAAA;AAAA,EACrE;AAAA;AAAA,EAGA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,wBAAA,GAAiC;AAC/B,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,eAAA,GAAwB;AACtB,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,cAAc,IAAA,CAAK,QAAA;AACxB,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,OAAA,EAA+B;AACnC,IAAA,OAAO,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,YAAA,CAAa,SAAS,OAAO,CAAA;AAAA,EACnE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYA,0BAA0B,OAAA,EAA4B;AACpD,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,MAAM,KAAA,GAAQ,KAAK,KAAA,CAAM,OAAO,KAAK,OAAA,CAAQ,OAAA,CAAQ,KAAK,gBAAgB,CAAA;AAC1E,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,IAAA,CAAK,WAAW,OAAO,CAAA;AACvB,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AAGpB,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,OAAO,CAAA;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAA,GAAgB;AACd,IAAA,IAAI,CAAC,KAAK,UAAA,EAAY;AACtB,IAAA,IAAA,CAAK,gBAAA,EAAiB;AACtB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,cAAA,EAAgB;AACzC,MAAA,IAAI,KAAK,KAAA,CAAM,OAAO,GAAG,IAAA,CAAK,QAAA,CAAS,SAAS,KAAK,CAAA;AAAA,IACvD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAA,GAAyB;AACvB,IAAA,IAAA,CAAK,cAAA,EAAe;AACpB,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,OAAO,qBAAqB,WAAA,EAAa;AACtE,IAAA,IAAA,CAAK,SAAA,GAAY,IAAI,gBAAA,CAAiB,CAAC,cAAc,IAAA,CAAK,YAAA,CAAa,SAAS,CAAC,CAAA;AACjF,IAAA,IAAA,CAAK,SAAA,CAAU,QAAQ,IAAA,CAAK,YAAA,EAAc,EAAE,SAAA,EAAW,IAAA,EAAM,OAAA,EAAS,IAAA,EAAM,CAAA;AAAA,EAC9E;AAAA,EAEA,cAAA,GAAuB;AACrB,IAAA,IAAA,CAAK,WAAW,UAAA,EAAW;AAC3B,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,OAAA,EAA4B;AACrC,IAAA,OAAA,CAAQ,gBAAA,CAAiB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACpD,IAAA,OAAA,CAAQ,gBAAA,CAAiB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACpD,IAAA,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,QAAQ,CAAA;AACjD,IAAA,OAAA,CAAQ,gBAAA,CAAiB,UAAA,EAAY,IAAA,CAAK,QAAQ,CAAA;AAAA,EACpD;AAAA,EAEA,aAAa,OAAA,EAA4B;AACvC,IAAA,OAAA,CAAQ,mBAAA,CAAoB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACvD,IAAA,OAAA,CAAQ,mBAAA,CAAoB,YAAA,EAAc,IAAA,CAAK,QAAQ,CAAA;AACvD,IAAA,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,QAAQ,CAAA;AACpD,IAAA,OAAA,CAAQ,mBAAA,CAAoB,UAAA,EAAY,IAAA,CAAK,QAAQ,CAAA;AAAA,EACvD;AAAA;AAAA,EAGA,QAAQ,KAAA,EAAoB;AAC1B,IAAA,MAAM,MAAA,GAAU,KAAA,CAAM,aAAA,IAAiB,KAAA,CAAM,MAAA;AAC7C,IAAA,MAAM,OAAA,GAAU,MAAA,EAAQ,OAAA,CAAqB,IAAA,CAAK,gBAAgB,CAAA;AAClE,IAAA,IAAI,OAAA,EAAS,IAAA,CAAK,aAAA,CAAc,OAAA,EAAS,MAAM,CAAA;AAAA,EACjD;AAAA;AAAA,EAGA,aAAa,SAAA,EAAmC;AAC9C,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,KAAA,MAAW,IAAA,IAAQ,SAAS,UAAA,EAAY;AACtC,QAAA,IAAI,EAAE,gBAAgB,WAAA,CAAA,EAAc;AACpC,QAAA,IAAI,IAAA,CAAK,QAAQ,IAAA,CAAK,gBAAgB,GAAG,IAAA,CAAK,QAAA,CAAS,MAAM,KAAK,CAAA;AAClE,QAAA,KAAA,MAAW,OAAA,IAAW,IAAA,CAAK,gBAAA,CAA8B,IAAA,CAAK,gBAAgB,CAAA,EAAG;AAC/E,UAAA,IAAA,CAAK,QAAA,CAAS,SAAS,KAAK,CAAA;AAAA,QAC9B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,QAAA,CAAS,SAAsB,SAAA,EAA0B;AACvD,IAAA,IAAI,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,KAAK,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,OAAO,CAAA,IAAK,CAAC,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA,EAAG;AACvF,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,YAAA,CAAa,iBAAiB,CAAA,IAAK,EAAA;AACxD,IAAA,MAAM,SAAA,GAAY,eAAA,CAAgB,GAAA,CAAI,IAAI,CAAA;AAE1C,IAAA,IAAI,CAAC,OAAA,CAAQ,YAAA,CAAa,MAAM,CAAA,EAAG;AACjC,MAAA,OAAA,CAAQ,YAAA,CAAa,MAAA,EAAQ,SAAA,GAAY,OAAA,GAAU,QAAQ,CAAA;AAAA,IAC7D;AACA,IAAA,OAAA,CAAQ,YAAA,CAAa,oBAAoB,SAAS,CAAA;AAClD,IAAA,IAAA,CAAK,MAAA,CAAO,KAAK,OAAO,CAAA;AAExB,IAAA,IAAA,CAAK,WAAW,OAAO,CAAA;AACvB,IAAA,IAAA,CAAK,WAAW,OAAO,CAAA;AAEvB,IAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,WAAA,EAAa,IAAA,EAAK,IAAK,EAAA;AAC5C,IAAA,IAAA,CAAK,QAAA,CAAS,MAAA,EAAQ,EAAE,MAAA,EAAQ,OAAA,EAAS,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAA,EAAS,IAAA,EAAK,EAAG,CAAA;AAC1E,IAAA,IAAI,SAAA,EAAW,QAAA,CAAS,IAAA,EAAM,EAAE,WAAW,CAAA;AAE3C,IAAA,IAAA,CAAK,YAAY,OAAO,CAAA;AACxB,IAAA,IAAI,CAAC,SAAA,EAAW,IAAA,CAAK,WAAA,CAAY,CAAC,OAAO,CAAC,CAAA;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,WAAA,CAAY,MAAA,GAAiC,EAAC,EAAS;AACrD,IAAA,IAAI,CAAC,OAAO,QAAA,CAAS,IAAA,CAAK,QAAQ,CAAA,IAAK,IAAA,CAAK,YAAY,CAAA,EAAG;AAC3D,IAAA,KAAA,MAAW,OAAA,IAAW,CAAC,GAAG,IAAA,CAAK,MAAM,CAAA,EAAG;AACtC,MAAA,IAAI,IAAA,CAAK,WAAA,EAAY,IAAK,IAAA,CAAK,QAAA,EAAU;AACzC,MAAA,IAAI,IAAA,CAAK,KAAA,CAAM,OAAO,CAAA,IAAK,CAAC,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,IAAK,CAAC,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,OAAO,CAAA,EAAG;AACtF,QAAA,IAAA,CAAK,aAAA,CAAc,SAAS,OAAO,CAAA;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,WAAA,GAAsB;AACpB,IAAA,OAAO,IAAA,CAAK,OAAO,MAAA,CAAO,CAAC,YAAY,IAAA,CAAK,KAAA,CAAM,OAAO,CAAC,CAAA,CAAE,MAAA;AAAA,EAC9D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,YAAY,OAAA,EAA4B;AACtC,IAAA,IAAI,IAAA,CAAK,iBAAiB,CAAA,EAAG;AAC7B,IAAA,MAAM,OAAA,GAAU,MAAY,IAAA,CAAK,aAAA,CAAc,SAAS,SAAS,CAAA;AACjE,IAAA,IAAI,IAAA,CAAK,mBAAmB,IAAA,CAAK,QAAA,CAAS,IAAI,OAAA,EAAS,OAAA,EAAS,KAAK,aAAa,CAAA;AAAA,cACxE,aAAA,CAAc,GAAA,CAAI,OAAA,EAAS,OAAA,EAAS,KAAK,aAAa,CAAA;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAA,CAAO,SAAsB,MAAA,EAAiC;AAC5D,IAAA,IAAA,CAAK,QAAA,CAAS,KAAA,CAAM,OAAA,EAAS,MAAM,CAAA;AAAA,EACrC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAA,CAAQ,OAAA,EAAsB,MAAA,EAA2B,KAAA,EAAiC;AACxF,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,OAAO,CAAA;AACzC,IAAA,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,OAAA,EAAS,MAAM,CAAA;AACpC,IAAA,IAAI,IAAA,IAAQ,CAAC,IAAA,CAAK,QAAA,CAAS,OAAO,OAAO,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,UAAU,KAAA,EAAiC;AACzC,IAAA,IAAI,KAAA,EAAO,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,KAAK,CAAA;AAClC,IAAA,IAAA,CAAK,SAAS,QAAA,EAAS;AAAA,EACzB;AAAA;AAAA,EAGA,WAAA,GAAoB;AAClB,IAAA,MAAM,SAAS,IAAA,CAAK,OAAA;AACpB,IAAA,IAAA,CAAK,UAAU,EAAC;AAChB,IAAA,IAAA,CAAK,YAAY,MAAM,CAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAW,OAAA,EAA4B;AACrC,IAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,QAAA,CAAS,aAAa,GAAG,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,OAAA,EAAS,IAAI,CAAA;AAClF,IAAA,IAAI,KAAK,QAAA,CAAS,MAAA,CAAO,OAAO,CAAA,EAAG,IAAA,CAAK,cAAc,OAAO,CAAA;AAAA,EAC/D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,OAAA,EAA4B;AACrC,IAAA,IAAI,OAAA,CAAQ,SAAS,QAAA,CAAS,aAAa,GAAG,IAAA,CAAK,MAAA,CAAO,SAAS,OAAO,CAAA;AAC1E,IAAA,IAAI,CAAC,OAAA,CAAQ,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAChC,IAAA,IAAA,CAAK,MAAA,CAAO,SAAS,OAAO,CAAA;AAC5B,IAAA,IAAA,CAAK,cAAc,OAAO,CAAA;AAAA,EAC5B;AAAA;AAAA,EAGA,cAAc,OAAA,EAA4B;AACxC,IAAA,IAAA,CAAK,gBAAA,CAAiB,IAAI,OAAO,CAAA;AACjC,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,QAAA,EAAU,aAAA,EAAe,KAAK,cAAA,EAAgB;AAAA,MAC9D,OAAA,EAAS,IAAA;AAAA,MACT,OAAA,EAAS;AAAA,KACV,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMS,iBAAiB,MAAY;AACpC,IAAA,IAAA,CAAK,SAAS,OAAA,EAAQ;AACtB,IAAA,MAAM,OAAA,GAAU,CAAC,GAAG,IAAA,CAAK,gBAAgB,CAAA;AACzC,IAAA,IAAA,CAAK,iBAAiB,KAAA,EAAM;AAC5B,IAAA,KAAA,MAAW,WAAW,OAAA,EAAS;AAC7B,MAAA,IAAI,CAAC,QAAQ,OAAA,CAAQ,QAAQ,GAAG,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,OAAA,EAAS,IAAI,CAAA;AAAA,IACrE;AAAA,EACF,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAQ,OAAA,EAA4B;AAClC,IAAA,IAAI,KAAK,QAAA,CAAS,MAAA,CAAO,OAAO,CAAA,EAAG,IAAA,CAAK,UAAU,IAAI,CAAA;AACtD,IAAA,IAAA,CAAK,QAAA,CAAS,MAAM,OAAO,CAAA;AAC3B,IAAA,IAAA,CAAK,aAAA,CAAc,MAAM,OAAO,CAAA;AAChC,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA;AACzC,IAAA,IAAI,UAAU,EAAA,EAAI,IAAA,CAAK,MAAA,CAAO,MAAA,CAAO,OAAO,CAAC,CAAA;AAC7C,IAAA,IAAA,CAAK,aAAa,OAAO,CAAA;AACzB,IAAA,IAAA,CAAK,gBAAA,CAAiB,OAAO,OAAO,CAAA;AACpC,IAAA,IAAI,KAAK,gBAAA,CAAiB,IAAA,KAAS,CAAA,EAAG,IAAA,CAAK,SAAS,OAAA,EAAQ;AAAA,EAC9D;AAAA;AAAA,EAGA,aAAA,CAAc,SAAsB,MAAA,EAA4C;AAM9E,IAAA,IAAI,CAAC,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,OAAO,CAAA,EAAG;AACpC,IAAA,IAAA,CAAK,QAAQ,OAAO,CAAA;AACpB,IAAA,IAAA,CAAK,QAAA,CAAS,IAAI,OAAO,CAAA;AAEzB,IAAA,OAAA,CAAQ,YAAA,CAAa,oBAAoB,SAAS,CAAA;AAElD,IAAA,MAAM,WAAW,MAAY;AAG3B,MAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,MAAA,CAAO,OAAO,CAAA,EAAG;AACpC,MAAA,OAAA,CAAQ,MAAA,EAAO;AACf,MAAA,IAAA,CAAK,QAAA,CAAS,WAAW,EAAE,MAAA,EAAQ,EAAE,OAAA,EAAS,OAAA,EAAS,MAAA,EAAO,EAAG,CAAA;AAAA,IACnE,CAAA;AAEA,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,aAAA,CAAc,OAAO,CAAA;AAC7C,IAAA,IAAI,aAAa,CAAA,EAAG;AAClB,MAAA,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,QAAA,EAAU,UAAU,CAAA;AAAA,IACvC,CAAA,MAAO;AACL,MAAA,QAAA,EAAS;AAAA,IACX;AAAA,EACF;AAAA;AAAA,EAGA,cAAc,EAAA,EAAyB;AACrC,IAAA,IAAI,OAAO,MAAA,CAAO,gBAAA,KAAqB,UAAA,EAAY,OAAO,CAAA;AAC1D,IAAA,OAAO,oBAAA,CAAqB,MAAA,CAAO,gBAAA,CAAiB,EAAE,CAAC,CAAA;AAAA,EACzD;AACF","file":"flash_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Resolves which of a controller's elements owns a node.\n *\n * A delegated listener hears events from a whole subtree, so the handler's first\n * job is almost always the same question: which item, handle, or control does\n * this `event.target` belong to? The same question comes up for a\n * `MutationRecord.target`, for `document.activeElement`, and for a\n * `relatedTarget` on the way out of a hover region.\n *\n * The answer is `Node.contains()`, which is **inclusive** — an element contains\n * itself — so testing the candidate for identity as well would be redundant.\n *\n * **The guard is what makes this safe to call with a raw event target.**\n * `event.target` is typed `EventTarget | null`, and `contains()` takes a `Node?`:\n * browsers throw `TypeError` for anything else, and `window` — the everyday\n * `EventTarget` that is not a `Node` — is what an event dispatched at it carries.\n * Narrowing here means a caller never has to cast, and the rule cannot drift\n * between the places that ask the question.\n *\n * Scope stays with the caller. These helpers say *which candidate owns the node*,\n * not *whether the node belongs to this controller at all* — a component with\n * nested instances of itself decides that first (by comparing the closest\n * annotated ancestor) and passes the candidates it owns.\n */\n\n/**\n * Index in `candidates` of the first one that is, or contains, `node`.\n *\n * `-1` when none does, when `node` is absent, and when it is an `EventTarget`\n * that is not a `Node`. Candidates are tested in array order, so a nested pair\n * resolves to whichever the caller listed first.\n */\nexport function ownerIndex<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): number {\n if (!(node instanceof Node)) return -1;\n return candidates.findIndex((candidate) => candidate.contains(node));\n}\n\n/**\n * The first candidate that is, or contains, `node`; `null` when none does.\n *\n * A miss indexes the array at `-1`, which reads as `undefined` and lands on the\n * same `null` the absent cases produce.\n */\nexport function ownerOf<T extends Element>(\n candidates: readonly T[],\n node: EventTarget | null | undefined,\n): T | null {\n return candidates[ownerIndex(candidates, node)] ?? null;\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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/**\n * Keeps at most one pending timer per key, and releases them all together.\n *\n * A region, a row, a message or a speaker that is armed again while its earlier\n * timer is still pending must end up with exactly one timer deciding when its\n * state ends. Two timers on one key means the earlier deadline wins: it cuts the\n * new emphasis short, clears text written after it, or drops a speaker who is\n * still typing — and then reports an ending for a state that is already gone.\n * Arming through this registry cancels the key's earlier timer in the same step,\n * and no timer id ever reaches the caller.\n *\n * An entry lives exactly as long as its timer: it goes as the timer fires, and\n * the whole ledger goes on `clearAll()`. That is a correctness rule, not\n * tidiness — a platform may hand an id released by `clearTimeout` to the next\n * timer it creates, so an entry that outlived its timer would let a later\n * `clear()` on that key cancel whatever timer now holds the id, on some other\n * key entirely.\n *\n * The ledger is a plain `Map`, and the key may be anything. Weak keys would\n * retain nothing less: while a timer is pending the platform already holds the\n * key through the callback, and the entry is gone the moment the timer is.\n *\n * Scope is the one-per-key timer only. What the timer means, and the roster of\n * keys it is armed for, stay with the consumer, so a second kind of timer on the\n * same keys (a transition wait, an animation frame) is a second registry, and a\n * timer that belongs to no key stays on a plain `SafeTimeout`.\n *\n * @example\n * ```ts\n * readonly #expiry = new KeyedTimers<string>();\n *\n * #onSignal(name: string): void {\n * this.#expiry.set(name, () => this.#drop(name), this.#timeout);\n * }\n *\n * disconnect(): void {\n * this.#expiry.clearAll();\n * }\n * ```\n */\nexport class KeyedTimers<K> {\n readonly #timers = new SafeTimeout();\n\n /** The pending timer of each key; an entry lives exactly as long as its timer. */\n readonly #pending = new Map<K, number>();\n\n /**\n * Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had\n * pending. The entry is dropped before the callback runs, so the callback sees\n * the key unarmed and may arm it again for the next round.\n */\n set(key: K, callback: () => void, delay: number): void {\n this.clear(key);\n const id = this.#timers.set(() => {\n this.#pending.delete(key);\n callback();\n }, delay);\n this.#pending.set(key, id);\n }\n\n /**\n * Cancels `key`'s pending timer, if it has one.\n *\n * A timer id is a positive integer, so `-1` stands for \"nothing pending\" and\n * the registry ignores an id it does not own — the unarmed case needs no\n * branch of its own, and no other key's timer can be reached from here.\n */\n clear(key: K): void {\n this.#timers.clear(this.#pending.get(key) ?? -1);\n this.#pending.delete(key);\n }\n\n /**\n * Cancels every pending timer and forgets every key. Call this from a\n * controller's `disconnect()` so no timer, and no entry, outlives the element.\n */\n clearAll(): void {\n this.#timers.clearAll();\n this.#pending.clear();\n }\n\n /** Whether `key` has a timer pending. */\n has(key: K): boolean {\n return this.#pending.has(key);\n }\n}\n","/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, so `dispose()` belongs exactly where\n * the `removeEventListener` block stood. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { KeyedTimers } from \"./keyed_timers\";\n\n/** What a key needs to carry on: the work, when it was last armed, and who is holding it. */\ninterface Entry {\n /** The work to run once the time is up; `null` while the key is held with no timer. */\n callback: (() => void) | null;\n /** Epoch ms the running timer was armed at; written only where the timer is armed. */\n startedAt: number;\n remaining: number;\n reasons: Set<string>;\n}\n\n/**\n * Keeps one self-dismissing timer per key that a person can hold open.\n *\n * A notification that takes itself away has to stop while the pointer is over it\n * or the focus is inside it, and pick up where it left off once both have gone\n * (WAI-ARIA and WCAG 2.2 call this being able to pause a time limit). Each reason\n * to hold is tracked on its own, because a pointer leaving while focus stays is\n * not a release: the time left is banked when the first reason arrives, and the\n * timer is armed again with that bank only after the last one is released.\n *\n * **Holding never dismisses.** A deadline that passed while the timer sat queued\n * — a throttled tab, a long task — is banked as one millisecond, so the\n * notification goes right after the last reason is released rather than during\n * the event that holds it. Taking it away there would remove the element under\n * the pointer, or the control that has focus, and focus would fall to the body.\n *\n * Arming a held key keeps the hold and banks the new delay for the resume, so a\n * duration that changes at runtime reaches a held notification without\n * dismissing it or releasing it.\n *\n * A hold is recorded whether or not the key has a timer: one that arrives before\n * the timer is set holds that timer back, and one on a key that never\n * auto-dismisses still answers {@link PausableTimers.isHeld}. That answer is what\n * a consumer that removes keys for reasons of its own — a cap on how many show\n * at once — reads to leave a held one in place. {@link PausableTimers.disarm}\n * drops a timer and keeps its hold, and a key with neither is forgotten.\n *\n * Scope is the timer and its hold. Whether a key may be held at all, and the\n * state hook that shows it is held, stay with the consumer.\n *\n * @example\n * ```ts\n * readonly #dismiss = new PausableTimers<HTMLElement>();\n *\n * pause(event: Event): void {\n * const item = this.#itemFromEvent(event);\n * if (item && this.#dismiss.pause(item, this.#pauseReason(event))) {\n * item.setAttribute(\"data-paused\", \"true\");\n * }\n * }\n * ```\n */\nexport class PausableTimers<K> {\n readonly #timers = new KeyedTimers<K>();\n\n /** Every key armed or held; an entry goes as its timer fires, or with the key. */\n readonly #entries = new Map<K, Entry>();\n\n /**\n * Arms `callback` after `delay` ms for `key`, replacing whatever `key` had.\n * A held key stays held and banks `delay` for its resume.\n */\n set(key: K, callback: () => void, delay: number): void {\n const reasons = this.#entries.get(key)?.reasons ?? new Set<string>();\n const entry: Entry = { callback, startedAt: 0, remaining: delay, reasons };\n this.#entries.set(key, entry);\n if (reasons.size === 0) this.#arm(key, entry, callback);\n }\n\n /**\n * Holds `key` for `reason` and banks the time left, with a floor of one\n * millisecond. A key with no timer is held all the same. Reports whether this\n * call is the one that stopped a running timer, so the caller can write its\n * held-state hook exactly once.\n */\n pause(key: K, reason: string): boolean {\n let entry = this.#entries.get(key);\n if (entry === undefined) {\n entry = { callback: null, startedAt: 0, remaining: 0, reasons: new Set<string>() };\n this.#entries.set(key, entry);\n }\n entry.reasons.add(reason);\n if (!this.#timers.has(key)) return false;\n this.#timers.clear(key);\n entry.remaining = Math.max(1, entry.remaining - (Date.now() - entry.startedAt));\n return true;\n }\n\n /**\n * Releases `reason` on `key`, arming the banked time again once no reason is\n * left; a key released with no timer is forgotten. Reports whether this call\n * is the one that started the timer again.\n */\n resume(key: K, reason: string): boolean {\n const entry = this.#entries.get(key);\n if (entry === undefined) return false;\n entry.reasons.delete(reason);\n if (entry.reasons.size > 0 || this.#timers.has(key)) return false;\n const { callback } = entry;\n if (callback === null) {\n this.#entries.delete(key);\n return false;\n }\n this.#arm(key, entry, callback);\n return true;\n }\n\n /** Whether any reason holds `key`, with or without a timer. */\n isHeld(key: K): boolean {\n return (this.#entries.get(key)?.reasons.size ?? 0) > 0;\n }\n\n /** Whether `key` is armed or held — that is, whether this registry drives it at all. */\n tracks(key: K): boolean {\n return this.#entries.has(key);\n }\n\n /**\n * Cancels `key`'s timer and keeps its hold, so a release arms nothing; a key\n * nothing holds is forgotten.\n */\n disarm(key: K): void {\n this.#timers.clear(key);\n const entry = this.#entries.get(key);\n if (entry?.reasons.size) entry.callback = null;\n else this.#entries.delete(key);\n }\n\n /** Cancels `key`'s timer and drops its hold. */\n clear(key: K): void {\n this.#timers.clear(key);\n this.#entries.delete(key);\n }\n\n /**\n * Cancels every timer and forgets every key. Call this from a controller's\n * `disconnect()` so neither a timer nor a hold outlives the element.\n */\n clearAll(): void {\n this.#timers.clearAll();\n this.#entries.clear();\n }\n\n /** Starts `entry`'s banked time running for `key`, and drops it as `callback` fires. */\n #arm(key: K, entry: Entry, callback: () => void): void {\n entry.startedAt = Date.now();\n this.#timers.set(\n key,\n () => {\n this.#entries.delete(key);\n callback();\n },\n entry.remaining,\n );\n }\n}\n","/**\n * The attribute selector for one declared target, in a controller's own namespace.\n *\n * Stimulus reads `data-<identifier>-target` as a **space-separated token list**, so a\n * single element can carry several target names at once. Matching the attribute value\n * whole would reach only the elements that declare exactly one name, which is why the\n * name is paired with `~=`.\n *\n * `identifier` is the name the controller is registered under, so the selector reaches\n * the markup that registration renders rather than one fixed spelling.\n */\nexport function targetSelector(identifier: string, name: string): string {\n return `[data-${identifier}-target~=\"${name}\"]`;\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 { announce } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { ownerOf } from \"../utils/event_owner\";\nimport { KeyedTimers } from \"../utils/keyed_timers\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { PausableTimers } from \"../utils/pausable_timers\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { targetSelector } from \"../utils/target_selector\";\nimport { maxTransitionTotalMs } from \"../utils/transition_completion\";\n\n/** Flash types that map to an assertive `alert` (everything else is a polite `status`). */\nconst ASSERTIVE_TYPES = new Set([\"alert\", \"error\"]);\n\n/**\n * The message target's name, from which the selector is built that spots dynamic\n * inserts, resolves a close control's message, and confirms a departed target was one.\n */\nconst MESSAGE_PART = \"message\";\n\n/**\n * Headless **Rails flash bridge**: turns server-rendered (and Turbo Stream-inserted)\n * `flash` elements into live-region announcements with auto-dismiss and a stacking\n * cap (no dedicated APG pattern; follows the WAI-ARIA status/alert guidance and WCAG\n * 2.2 **4.1.3 Status Messages**). The general-purpose sibling is Toast; this one is\n * specialized to the Rails `flash` convention.\n *\n * Markup contract (identifier: `stimeo--flash`):\n * <div data-controller=\"stimeo--flash\" data-stimeo--flash-target=\"region\">\n * <!-- server-rendered or Turbo Stream-inserted -->\n * <div data-stimeo--flash-target=\"message\" data-flash-type=\"notice\">Saved</div>\n * </div>\n *\n * Each message is mapped by `data-flash-type` to `role=\"status\"` (notice) or\n * `role=\"alert\"` (alert/error), flagged `data-flash-state=\"visible\"`, auto-dismissed\n * after `duration` (paused while hovered *or* focused when `pauseOnHover`, and\n * resumed only once both are released), and capped at `max` simultaneous messages.\n * `duration` and `pauseOnHover` are read as each message is taken on, so a change\n * reaches the messages that arrive after it. The cap is applied on every arrival, on\n * every change to `max`, whenever hover and focus have both left a message, and when a\n * message they held leaves: it dismisses the oldest messages with reason `limit`,\n * passing over every message the pointer is over or focus is inside (whatever\n * `pauseOnHover` says), the message just taken on, and the message the pointer or focus\n * is moving into. While nothing else can go, the stack stays over the cap until one of\n * those holds is released.\n * A close button wired to the `dismiss` action removes one manually.\n *\n * `dismiss` dispatches `{ element, reason }`.\n *\n * `show` dispatches `{ type, message }`.\n * `reconcile` dispatches `{ removed: number }` — the messages the Turbo cache\n * rewind took out, which no `dismiss` will report because nobody dismissed them.\n *\n * @remarks\n * Reading is **delegated to the shared Announcer** — but only for the *initial*,\n * page-loaded messages: an in-place live region present at load is not announced by\n * assistive tech, so the controller bridges those via an `stimeo--announcer:announce`\n * event. Messages inserted *later* (Turbo Stream) are announced by their own freshly\n * inserted `role`, exactly like Toast, so they are not bridged again (no double\n * announcement). Behavior only — no styling; `data-flash-state=\"leaving\"` lets CSS\n * animate removal. Focus is never moved (WCAG 2.2 4.1.3): neither a pause nor the `max`\n * cap removes a message the pointer is over or focus is inside, so neither takes away\n * the control under the pointer or holding focus; with `pauseOnHover` off the\n * auto-dismiss still runs out under them. The observation follows a\n * `region` element replaced at runtime; the managed set is the current region's\n * subtree, and a message that leaves it gives up its stacking slot, its pending\n * auto-dismiss, and any removal already scheduled. The observer, timers,\n * and per-message listeners are torn down on `disconnect()` (Turbo navigation\n * included), and the managed flashes leave the page before Turbo caches it so a\n * restored snapshot does not replay a notification the visitor already received.\n */\nexport class FlashController extends Controller<HTMLElement> {\n /** Selects the message parts, in the namespace this controller is registered under. */\n get #messageSelector(): string {\n return targetSelector(this.identifier, MESSAGE_PART);\n }\n\n static override targets = [\"region\", \"message\"];\n static override values = {\n duration: { type: Number, default: 5000 },\n pauseOnHover: { type: Boolean, default: true },\n max: { type: Number, default: 0 },\n };\n static actions = [\"dismiss\"] as const;\n static events = [\"show\", \"dismiss\", \"reconcile\"] as const;\n\n declare readonly regionTarget: HTMLElement;\n declare readonly messageTargets: HTMLElement[];\n declare readonly hasRegionTarget: boolean;\n\n declare durationValue: number;\n declare pauseOnHoverValue: boolean;\n declare maxValue: number;\n\n /** Removal timers for the leaving transition; the auto-dismiss ones live below. */\n readonly #timers = new SafeTimeout();\n /**\n * Per-message auto-dismiss held open while the message is hovered or focused, and\n * the one record of which messages hover or focus holds, which the cap reads.\n */\n readonly #dismiss = new PausableTimers<HTMLElement>();\n /**\n * Auto-dismiss of the messages taken on while `pauseOnHover` is off. It runs on\n * whatever holds the message; the hold itself still keeps the cap away.\n */\n readonly #fixedDismiss = new KeyedTimers<HTMLElement>();\n /** Applies the cap again once the event that released a hold has run its course. */\n readonly #reapply = new MicrotaskCoalescer(() => this.#reapplyCap());\n /** Messages the pointer or focus moved into since the cap was last applied again. */\n #spared: HTMLElement[] = [];\n /** The next pointer movement, listened for while a hover hold waits on it. */\n readonly #pointer = new ListenerSet();\n /** Messages whose hover hold waits for the next pointer movement to be confirmed. */\n readonly #awaitingPointer = new Set<HTMLElement>();\n #observer: MutationObserver | null = null;\n /** Whether the controller is between `connect()` and `disconnect()`. */\n #connected = false;\n /**\n * The `max` the stack was last held to, or `null` before the first time. `connect()`\n * applies the cap only when `max` is not that one, so a reconnect of the same instance\n * leaves the stack as it was unless `max` changed while it was away.\n */\n #appliedMax: number | null = null;\n /** Messages already processed, in insertion order, to enforce `max` and avoid double work. */\n readonly #order: HTMLElement[] = [];\n /**\n * Messages between `leaving` and their removal. {@link FlashController.#beginDismiss}\n * releases the bookkeeping above *before* the transition wait, so for that window the\n * element is in the DOM but in neither collection — without this set a re-scan would\n * read it as a brand-new flash and show it a second time.\n */\n readonly #leaving = new Set<HTMLElement>();\n\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n readonly #onEnter = (event: Event): void =>\n this.#pause(event.currentTarget as HTMLElement, event.type === \"focusin\" ? \"focus\" : \"hover\");\n /**\n * Releases hover or focus on a message. Focus moving between two of the message's\n * own controls is no release, since `focusout` names a control still inside it; and\n * the message the pointer or focus is moving into is spared when the release\n * applies the cap again, because its own hold only arrives after this event.\n */\n readonly #onLeave = (event: MouseEvent | FocusEvent): void => {\n const message = event.currentTarget as HTMLElement;\n const next = ownerOf(this.#order, event.relatedTarget);\n if (next === message) return;\n this.#resume(message, event.type === \"focusout\" ? \"focus\" : \"hover\", next);\n };\n\n override connect(): void {\n this.#connected = true;\n this.#reapply.activate();\n // Initial, server-rendered flashes: bridge them to the Announcer because an\n // in-place live region present at page load is not announced on its own.\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, true);\n }\n if (!Object.is(this.#appliedMax, this.maxValue)) {\n this.#appliedMax = this.maxValue;\n this.#enforceMax();\n }\n this.#syncObservation();\n this.#beforeCache.activate();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#beforeCache.deactivate();\n this.#stopObserving();\n this.#timers.clearAll();\n this.#dismiss.clearAll();\n this.#fixedDismiss.clearAll();\n this.#reapply.cancel();\n this.#spared = [];\n this.#pointer.dispose();\n this.#awaitingPointer.clear();\n for (const message of this.#order) this.#unbindPause(message);\n this.#order.length = 0;\n // The pending finalizes died with the timers above, so a message still marked\n // `leaving` is free to be shown again by the next connect (a snapshot restore).\n this.#leaving.clear();\n }\n\n /**\n * Takes the managed flashes out of the page just before Turbo freezes it, so a\n * restored snapshot carries no notification the visitor has already received: the\n * fresh `connect()` there reads a leftover flash as a brand-new one and announces it\n * a second time. A message that never auto-dismisses (`duration: 0`) is one of these\n * too — that value governs the timer, not what belongs in a cached page. Removal\n * only: `dismiss` reports a dismissal, and freezing the page is not one.\n */\n #rewindForCache(): void {\n const removed = this.#order.length;\n for (const message of [...this.#order]) {\n message.remove();\n this.#forget(message);\n }\n for (const message of this.#leaving) message.remove();\n this.#leaving.clear();\n if (removed > 0) this.dispatch(\"reconcile\", { detail: { removed } });\n }\n\n /** Follows a `region` element swapped in — or arriving — at runtime (Turbo Stream). */\n regionTargetConnected(): void {\n this.#resync();\n }\n\n /** Releases the observation when the `region` element leaves the target set. */\n regionTargetDisconnected(): void {\n this.#resync();\n }\n\n /**\n * Holds the stack to a changed `max`: lowering it below the count dismisses the\n * oldest messages hover and focus do not hold, with reason `limit`, as an arrival\n * past the cap does, while raising it or setting `0` dismisses nothing. Stimulus can\n * also call this ahead of `connect()` — for an attribute that changed while the\n * controller was away, and on every connect for an undeclared `max`, with its default;\n * `connect()` decides then.\n */\n maxValueChanged(): void {\n if (!this.#connected) return;\n this.#appliedMax = this.maxValue;\n this.#enforceMax();\n }\n\n /**\n * Whether this controller owns `message`. Ownership is the current `region`'s\n * subtree: a message target anywhere else in the controller's scope is the\n * consumer's, and so is one in a region that has gone away. The initial scan, a\n * re-scan after a `region` swap, and a departure from the target set all resolve\n * ownership through this one test; the observation gets it structurally, by watching\n * that subtree and nothing else.\n */\n #owns(message: HTMLElement): boolean {\n return this.hasRegionTarget && this.regionTarget.contains(message);\n }\n\n /**\n * Releases a message that left the target set (a Turbo Stream `remove`, the consumer\n * detaching the node, or a morph that rewrote the target attribute in place): it\n * stops occupying a `max` slot, and both its pending auto-dismiss and an already\n * scheduled removal are cancelled. A move *within* the region keeps all of them —\n * which is why the element must still be a message to be treated as one: ownership\n * alone reads an in-place attribute rewrite as a move, and a node outside the target\n * set belongs to the consumer, so nothing here may dismiss it. A move may still have\n * ended a hold, so those are read again.\n */\n messageTargetDisconnected(message: HTMLElement): void {\n if (!this.#connected) return;\n const moved = this.#owns(message) && message.matches(this.#messageSelector);\n if (moved) {\n this.#afterMove(message);\n return;\n }\n this.#forget(message);\n // Ownership is re-checked at both ends of the leaving transition: dropping the\n // claim here is what the pending finalize reads to leave the node alone.\n this.#leaving.delete(message);\n }\n\n /**\n * Re-points the observation after a `region` swap and picks up the messages the\n * new element brought with it (dynamic inserts, so their own `role` announces\n * them). The `#connected` guard is load-bearing: Stimulus runs target callbacks\n * for the initial markup *before* `connect()` and again during teardown *after*\n * `disconnect()`, and re-observing there would outlive the controller.\n */\n #resync(): void {\n if (!this.#connected) return;\n this.#syncObservation();\n for (const message of this.messageTargets) {\n if (this.#owns(message)) this.#process(message, false);\n }\n }\n\n /**\n * Points the mutation observation at the current `region` target, re-resolved on\n * every sync rather than captured at connect, so an element swapped in at runtime\n * is observed instead of the detached original.\n */\n #syncObservation(): void {\n this.#stopObserving();\n if (!this.hasRegionTarget || typeof MutationObserver === \"undefined\") return;\n this.#observer = new MutationObserver((mutations) => this.#onMutations(mutations));\n this.#observer.observe(this.regionTarget, { childList: true, subtree: true });\n }\n\n #stopObserving(): void {\n this.#observer?.disconnect();\n this.#observer = null;\n }\n\n /**\n * Hover and focus listeners, bound and unbound as a pair so the two sides stay in\n * sync. Every message gets them, whatever `pauseOnHover` says, because the cap reads\n * the holds they record; unbinding is idempotent (a no-op when nothing was bound).\n */\n #bindPause(message: HTMLElement): void {\n message.addEventListener(\"mouseenter\", this.#onEnter);\n message.addEventListener(\"mouseleave\", this.#onLeave);\n message.addEventListener(\"focusin\", this.#onEnter);\n message.addEventListener(\"focusout\", this.#onLeave);\n }\n\n #unbindPause(message: HTMLElement): void {\n message.removeEventListener(\"mouseenter\", this.#onEnter);\n message.removeEventListener(\"mouseleave\", this.#onLeave);\n message.removeEventListener(\"focusin\", this.#onEnter);\n message.removeEventListener(\"focusout\", this.#onLeave);\n }\n\n /** Dismisses the flash whose close control fired the event. */\n dismiss(event: Event): void {\n const target = (event.currentTarget || event.target) as HTMLElement | null;\n const message = target?.closest<HTMLElement>(this.#messageSelector);\n if (message) this.#beginDismiss(message, \"user\");\n }\n\n /** Processes messages added after connect (Turbo Stream); their own role announces them. */\n #onMutations(mutations: MutationRecord[]): void {\n for (const mutation of mutations) {\n for (const node of mutation.addedNodes) {\n if (!(node instanceof HTMLElement)) continue;\n if (node.matches(this.#messageSelector)) this.#process(node, false);\n for (const message of node.querySelectorAll<HTMLElement>(this.#messageSelector)) {\n this.#process(message, false);\n }\n }\n }\n }\n\n /**\n * Applies role/state, wires pause listeners, takes the holds the message already has,\n * and schedules auto-dismiss. A message `connect()` takes on (`atConnect`) is bridged\n * to the Announcer and meets no cap here, `connect()` holding the stack to it; a later\n * insert announces through its own role and meets the cap as it arrives. Idempotent\n * per message, and a node that has left the region by the time its insertion is\n * reported is not taken on.\n */\n #process(message: HTMLElement, atConnect: boolean): void {\n if (this.#order.includes(message) || this.#leaving.has(message) || !this.#owns(message)) {\n return;\n }\n\n const type = message.getAttribute(\"data-flash-type\") ?? \"\";\n const assertive = ASSERTIVE_TYPES.has(type);\n // Don't clobber an authored role; otherwise map the flash type to a live role.\n if (!message.hasAttribute(\"role\")) {\n message.setAttribute(\"role\", assertive ? \"alert\" : \"status\");\n }\n message.setAttribute(\"data-flash-state\", \"visible\");\n this.#order.push(message);\n\n this.#bindPause(message);\n this.#readHolds(message);\n\n const text = message.textContent?.trim() ?? \"\";\n this.dispatch(\"show\", { target: message, detail: { type, message: text } });\n if (atConnect) announce(text, { assertive });\n\n this.#startTimer(message);\n if (!atConnect) this.#enforceMax([message]);\n }\n\n /**\n * Dismisses the oldest messages with reason `limit` while more than `max` are shown\n * (0 or less, or not a finite number, means no cap), passing over every message hover\n * or focus holds and those in `spared` — the message just taken on, or the ones the\n * pointer or focus moved into. Only messages still in the region count or go: one a\n * script has just taken out stays on the stack until its removal is reported. With\n * nothing else left to go the stack stays over the cap. The loop walks a copy and\n * re-reads the count on every step, because a `dismiss` listener may already have\n * changed both.\n *\n * @stimeoRenderRoot\n */\n #enforceMax(spared: readonly HTMLElement[] = []): void {\n if (!Number.isFinite(this.maxValue) || this.maxValue <= 0) return;\n for (const message of [...this.#order]) {\n if (this.#shownCount() <= this.maxValue) return;\n if (this.#owns(message) && !spared.includes(message) && !this.#dismiss.isHeld(message)) {\n this.#beginDismiss(message, \"limit\");\n }\n }\n }\n\n /** How many messages the stack shows: the ones taken on that are still in the region. */\n #shownCount(): number {\n return this.#order.filter((message) => this.#owns(message)).length;\n }\n\n /**\n * Arms a message's auto-dismiss; a non-positive `duration` means it never expires.\n * The timer waits out hover and focus when `pauseOnHover` is on, and runs on\n * regardless when it is off.\n *\n * @stimeoRuntimeOnly `duration` is the delay of the one dismissal timer this call arms,\n * and `pauseOnHover` whether hover and focus hold that timer.\n */\n #startTimer(message: HTMLElement): void {\n if (this.durationValue <= 0) return;\n const dismiss = (): void => this.#beginDismiss(message, \"timeout\");\n if (this.pauseOnHoverValue) this.#dismiss.set(message, dismiss, this.durationValue);\n else this.#fixedDismiss.set(message, dismiss, this.durationValue);\n }\n\n /**\n * Records hover or focus on a message. Hover and focus are independent reasons:\n * the registry banks the time left on the first of them and waits for the last\n * (WCAG 2.2 2.2.1), and the cap passes over the message while either is held.\n */\n #pause(message: HTMLElement, reason: \"focus\" | \"hover\"): void {\n this.#dismiss.pause(message, reason);\n }\n\n /**\n * Releases one reason, resuming the banked time once no reason is left. Releasing\n * the last one applies the cap again, sparing `spare`.\n */\n #resume(message: HTMLElement, reason: \"focus\" | \"hover\", spare: HTMLElement | null): void {\n const held = this.#dismiss.isHeld(message);\n this.#dismiss.resume(message, reason);\n if (held && !this.#dismiss.isHeld(message)) this.#capLater(spare);\n }\n\n /**\n * Applies the cap again once the current event is over, sparing `spare`. A hold can\n * end in a `focusout` the engine fires while it is itself taking the node out for a\n * script's move or removal, and dismissing the message inside that event would pull\n * the node from under the caller's own call.\n */\n #capLater(spare: HTMLElement | null): void {\n if (spare) this.#spared.push(spare);\n this.#reapply.schedule();\n }\n\n /** Applies the cap again, sparing every message gathered since the last pass. */\n #reapplyCap(): void {\n const spared = this.#spared;\n this.#spared = [];\n this.#enforceMax(spared);\n }\n\n /**\n * Reads the holds of a message that moved within the region. Focus is read at once:\n * an ordinary move ends it with a `focusout` and `moveBefore` keeps it. Where the\n * pointer is, nothing says until it moves again: no `mouseleave` reaches a moved node,\n * and `:hover` can answer from before the move. A message still held waits for that\n * movement.\n */\n #afterMove(message: HTMLElement): void {\n if (!message.contains(document.activeElement)) this.#resume(message, \"focus\", null);\n if (this.#dismiss.isHeld(message)) this.#awaitPointer(message);\n }\n\n /**\n * Takes the holds a message already has as it is taken on: focus inside it, and the\n * pointer over it by `:hover`. That answer can date from before the message came here,\n * so a hover hold taken on it waits for the next pointer movement to be confirmed.\n */\n #readHolds(message: HTMLElement): void {\n if (message.contains(document.activeElement)) this.#pause(message, \"focus\");\n if (!message.matches(\":hover\")) return;\n this.#pause(message, \"hover\");\n this.#awaitPointer(message);\n }\n\n /** Leaves `message`'s hover hold to be confirmed or let go on the next pointer movement. */\n #awaitPointer(message: HTMLElement): void {\n this.#awaitingPointer.add(message);\n this.#pointer.add(document, \"pointermove\", this.#onPointerMove, {\n capture: true,\n passive: true,\n });\n }\n\n /**\n * Reads `:hover` once the pointer has moved, when the answer is current, and lets go of\n * the hover hold of every waiting message the pointer is not over. Listens once.\n */\n readonly #onPointerMove = (): void => {\n this.#pointer.dispose();\n const waiting = [...this.#awaitingPointer];\n this.#awaitingPointer.clear();\n for (const message of waiting) {\n if (!message.matches(\":hover\")) this.#resume(message, \"hover\", null);\n }\n };\n\n /**\n * Releases every per-message resource: timers, hold, stacking slot, pause listeners,\n * and the wait on the pointer. A message something still held counts as released: the\n * cap is applied again.\n */\n #forget(message: HTMLElement): void {\n if (this.#dismiss.isHeld(message)) this.#capLater(null);\n this.#dismiss.clear(message);\n this.#fixedDismiss.clear(message);\n const index = this.#order.indexOf(message);\n if (index !== -1) this.#order.splice(index, 1);\n this.#unbindPause(message);\n this.#awaitingPointer.delete(message);\n if (this.#awaitingPointer.size === 0) this.#pointer.dispose();\n }\n\n /** Marks a message leaving, then removes it after its CSS transition and emits dismiss. */\n #beginDismiss(message: HTMLElement, reason: \"timeout\" | \"user\" | \"limit\"): void {\n // One removal, one `dismiss`: a close control fired during the leaving\n // transition, or a repeated request, finds the message already released and\n // must not start a second finalize. Reading the bookkeeping rather than\n // `data-flash-state` also keeps `#enforceMax`'s count honest, since the release\n // below is what shrinks `#order`.\n if (!this.#order.includes(message)) return;\n this.#forget(message);\n this.#leaving.add(message);\n\n message.setAttribute(\"data-flash-state\", \"leaving\");\n\n const finalize = (): void => {\n // The claim taken above is the license to remove: a message that lost the target\n // attribute during the transition belongs to the consumer by the time this runs.\n if (!this.#leaving.delete(message)) return;\n message.remove();\n this.dispatch(\"dismiss\", { detail: { element: message, reason } });\n };\n\n const transition = this.#transitionMs(message);\n if (transition > 0) {\n this.#timers.set(finalize, transition);\n } else {\n finalize();\n }\n }\n\n /** Maximum transition total (duration + delay) of `el` in ms (0 when none / unsupported). */\n #transitionMs(el: HTMLElement): number {\n if (typeof window.getComputedStyle !== \"function\") return 0;\n return maxTransitionTotalMs(window.getComputedStyle(el));\n }\n}\n"]}
|
|
@@ -437,6 +437,7 @@ var FocusController = class extends Controller {
|
|
|
437
437
|
this.element.setAttribute("data-focus-trapped", "true");
|
|
438
438
|
this.dispatch("activate", { detail: {} });
|
|
439
439
|
}
|
|
440
|
+
/** @stimeoRuntimeOnly `restore` decides whether releasing this one trap returns focus. */
|
|
440
441
|
#deactivate() {
|
|
441
442
|
if (!this.#active) return;
|
|
442
443
|
this.#active = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/focus_trap.ts","../../src/controllers/focus_controller.ts"],"names":[],"mappings":"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","file":"focus_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\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 handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * 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","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\nimport { tabStopsWithin } from \"./focus_candidate\";\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/** 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.\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 this.#beforeCache.activate();\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 this.#beforeCache.deactivate();\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 #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\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 * The walk climbs from the container to `body` and inerts each ancestor's other\n * children. Scanning only `body`'s children would skip the branch the container\n * sits in — everything beside it inside that branch is background too, and a\n * nested container is the ordinary case.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (let node: HTMLElement = container; node !== document.body; ) {\n const parent = node.parentElement;\n if (!parent) break;\n for (const sibling of Array.from(parent.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling === node || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n node = parent;\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 tabStopsWithin(this.#getContainer());\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/**\n * Headless **focus scope**: exposes the shared `FocusTrap` primitive as a standalone,\n * declarative focus boundary — `Tab` cycling, initial focus, and restore-on-release —\n * for any region, without building a full modal. No dedicated APG pattern; it is the\n * primitive the overlay patterns use, surfaced for direct use.\n *\n * Markup contract (identifier: `stimeo--focus`):\n * <div data-controller=\"stimeo--focus\" data-stimeo--focus-trap-value=\"true\">\n * <input data-stimeo--focus-target=\"initial\" />\n * …\n * </div>\n *\n * While `trap` is on, `Tab` / `Shift+Tab` cycle within the element, focus moves to the\n * `initial` target (or the first focusable) when `auto`, `Escape` releases it, and on\n * release focus returns to the opener when `restore`. With `inert` the rest of the page\n * is made `inert` (a hard, modal-style isolation); left off it is a soft boundary —\n * `Tab` still cycles but the background stays reachable. The element carries\n * `data-focus-trapped` while active and emits `activate` / `deactivate`.\n *\n * Only `trap` is watched for changes. `auto` and `inert` are read when the trap turns\n * on and `restore` when it turns off, so rewriting one mid-trap describes the *next*\n * activation rather than the one in progress.\n *\n * `activate` and `deactivate` dispatch `{}`.\n *\n * @remarks\n * Behavior only — it does not open/close or render an overlay (pair with Dialog) and\n * does not move DOM (pair with Portal). It reuses the shared `FocusTrap`, so it never\n * scroll-locks the page (unlike the modal overlays) and tracks live focusable children\n * (dynamic additions are picked up on the next `Tab`). The opener is recorded on\n * activate and refocused on release if still present. Everything is torn down on\n * `disconnect()` (Turbo navigation included) without yanking focus.\n */\nexport class FocusController extends Controller<HTMLElement> {\n static override targets = [\"initial\"];\n static override values = {\n trap: { type: Boolean, default: false },\n auto: { type: Boolean, default: true },\n restore: { type: Boolean, default: true },\n inert: { type: Boolean, default: false },\n };\n static actions = [\"activate\", \"deactivate\"] as const;\n static events = [\"activate\", \"deactivate\"] as const;\n\n declare readonly initialTarget: HTMLElement;\n declare readonly hasInitialTarget: boolean;\n\n declare trapValue: boolean;\n declare autoValue: boolean;\n declare restoreValue: boolean;\n declare inertValue: boolean;\n\n readonly #trap = new FocusTrap(() => this.element, {\n // A focus scope is not a modal: never lock scroll, and only isolate the background\n // when `inert` is requested. `auto` gates the initial focus move; Escape releases.\n lockScroll: false,\n isolate: () => this.inertValue,\n autoFocus: () => this.autoValue,\n initialFocus: () => (this.hasInitialTarget ? this.initialTarget : null),\n onEscape: () => this.deactivate(),\n });\n\n /**\n * Whether this scope is currently trapping.\n *\n * Held here rather than read back from the trap: the trap releases itself\n * before Turbo caches the page, so its own flag stops answering for the state\n * this controller publishes on the element and in its events.\n */\n #active = false;\n\n /**\n * Returns the element to its untrapped form before Turbo copies the page. The\n * trap performs its own release on the same event, so what is left is the hook\n * this controller owns — carried into the snapshot it would describe a scope\n * that is no longer trapping.\n *\n * Silent, like `disconnect()`: the page is about to be frozen, and a release\n * nobody asked for is not a close to report.\n */\n readonly #beforeCache = new BeforeCacheReset(() => {\n this.#active = false;\n this.element.removeAttribute(\"data-focus-trapped\");\n });\n\n /** Stimulus drives activation from the `trap` value (also fires on connect). */\n trapValueChanged(): void {\n if (this.trapValue) this.#activate();\n else this.#deactivate();\n }\n\n override connect(): void {\n this.#beforeCache.activate();\n }\n\n override disconnect(): void {\n // Release without restoring focus — the element is leaving the DOM. This is a\n // teardown, not a user-driven close, so it intentionally does NOT emit\n // `deactivate` or reset `trapValue` (which would resurrect on a Turbo cache\n // restore); the event travels with a release asked for through the public\n // `deactivate()` action or the `trap` value.\n this.#trap.deactivate({ restoreFocus: false });\n this.#beforeCache.deactivate();\n this.#active = false;\n this.element.removeAttribute(\"data-focus-trapped\");\n }\n\n /** Turns the trap on. Acts synchronously and keeps the `trap` value in sync. */\n activate(): void {\n this.trapValue = true;\n this.#activate();\n }\n\n /** Turns the trap off (also wired to Escape). */\n deactivate(): void {\n this.trapValue = false;\n this.#deactivate();\n }\n\n #activate(): void {\n if (this.#active) return;\n this.#active = true;\n this.#trap.activate();\n this.element.setAttribute(\"data-focus-trapped\", \"true\");\n this.dispatch(\"activate\", { detail: {} });\n }\n\n #deactivate(): void {\n if (!this.#active) return;\n this.#active = false;\n this.#trap.deactivate({ restoreFocus: this.restoreValue });\n this.element.removeAttribute(\"data-focus-trapped\");\n this.dispatch(\"deactivate\", { detail: {} });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/focus_trap.ts","../../src/controllers/focus_controller.ts"],"names":[],"mappings":"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","file":"focus_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\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 handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * 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","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\nimport { tabStopsWithin } from \"./focus_candidate\";\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/** 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.\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 this.#beforeCache.activate();\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 this.#beforeCache.deactivate();\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 #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\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 * The walk climbs from the container to `body` and inerts each ancestor's other\n * children. Scanning only `body`'s children would skip the branch the container\n * sits in — everything beside it inside that branch is background too, and a\n * nested container is the ordinary case.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (let node: HTMLElement = container; node !== document.body; ) {\n const parent = node.parentElement;\n if (!parent) break;\n for (const sibling of Array.from(parent.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling === node || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n node = parent;\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 tabStopsWithin(this.#getContainer());\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/**\n * Headless **focus scope**: exposes the shared `FocusTrap` primitive as a standalone,\n * declarative focus boundary — `Tab` cycling, initial focus, and restore-on-release —\n * for any region, without building a full modal. No dedicated APG pattern; it is the\n * primitive the overlay patterns use, surfaced for direct use.\n *\n * Markup contract (identifier: `stimeo--focus`):\n * <div data-controller=\"stimeo--focus\" data-stimeo--focus-trap-value=\"true\">\n * <input data-stimeo--focus-target=\"initial\" />\n * …\n * </div>\n *\n * While `trap` is on, `Tab` / `Shift+Tab` cycle within the element, focus moves to the\n * `initial` target (or the first focusable) when `auto`, `Escape` releases it, and on\n * release focus returns to the opener when `restore`. With `inert` the rest of the page\n * is made `inert` (a hard, modal-style isolation); left off it is a soft boundary —\n * `Tab` still cycles but the background stays reachable. The element carries\n * `data-focus-trapped` while active and emits `activate` / `deactivate`.\n *\n * Only `trap` is watched for changes. `auto` and `inert` are read when the trap turns\n * on and `restore` when it turns off, so rewriting one mid-trap describes the *next*\n * activation rather than the one in progress.\n *\n * `activate` and `deactivate` dispatch `{}`.\n *\n * @remarks\n * Behavior only — it does not open/close or render an overlay (pair with Dialog) and\n * does not move DOM (pair with Portal). It reuses the shared `FocusTrap`, so it never\n * scroll-locks the page (unlike the modal overlays) and tracks live focusable children\n * (dynamic additions are picked up on the next `Tab`). The opener is recorded on\n * activate and refocused on release if still present. Everything is torn down on\n * `disconnect()` (Turbo navigation included) without yanking focus.\n */\nexport class FocusController extends Controller<HTMLElement> {\n static override targets = [\"initial\"];\n static override values = {\n trap: { type: Boolean, default: false },\n auto: { type: Boolean, default: true },\n restore: { type: Boolean, default: true },\n inert: { type: Boolean, default: false },\n };\n static actions = [\"activate\", \"deactivate\"] as const;\n static events = [\"activate\", \"deactivate\"] as const;\n\n declare readonly initialTarget: HTMLElement;\n declare readonly hasInitialTarget: boolean;\n\n declare trapValue: boolean;\n declare autoValue: boolean;\n declare restoreValue: boolean;\n declare inertValue: boolean;\n\n readonly #trap = new FocusTrap(() => this.element, {\n // A focus scope is not a modal: never lock scroll, and only isolate the background\n // when `inert` is requested. `auto` gates the initial focus move; Escape releases.\n lockScroll: false,\n isolate: () => this.inertValue,\n autoFocus: () => this.autoValue,\n initialFocus: () => (this.hasInitialTarget ? this.initialTarget : null),\n onEscape: () => this.deactivate(),\n });\n\n /**\n * Whether this scope is currently trapping.\n *\n * Held here rather than read back from the trap: the trap releases itself\n * before Turbo caches the page, so its own flag stops answering for the state\n * this controller publishes on the element and in its events.\n */\n #active = false;\n\n /**\n * Returns the element to its untrapped form before Turbo copies the page. The\n * trap performs its own release on the same event, so what is left is the hook\n * this controller owns — carried into the snapshot it would describe a scope\n * that is no longer trapping.\n *\n * Silent, like `disconnect()`: the page is about to be frozen, and a release\n * nobody asked for is not a close to report.\n */\n readonly #beforeCache = new BeforeCacheReset(() => {\n this.#active = false;\n this.element.removeAttribute(\"data-focus-trapped\");\n });\n\n /** Stimulus drives activation from the `trap` value (also fires on connect). */\n trapValueChanged(): void {\n if (this.trapValue) this.#activate();\n else this.#deactivate();\n }\n\n override connect(): void {\n this.#beforeCache.activate();\n }\n\n override disconnect(): void {\n // Release without restoring focus — the element is leaving the DOM. This is a\n // teardown, not a user-driven close, so it intentionally does NOT emit\n // `deactivate` or reset `trapValue` (which would resurrect on a Turbo cache\n // restore); the event travels with a release asked for through the public\n // `deactivate()` action or the `trap` value.\n this.#trap.deactivate({ restoreFocus: false });\n this.#beforeCache.deactivate();\n this.#active = false;\n this.element.removeAttribute(\"data-focus-trapped\");\n }\n\n /** Turns the trap on. Acts synchronously and keeps the `trap` value in sync. */\n activate(): void {\n this.trapValue = true;\n this.#activate();\n }\n\n /** Turns the trap off (also wired to Escape). */\n deactivate(): void {\n this.trapValue = false;\n this.#deactivate();\n }\n\n #activate(): void {\n if (this.#active) return;\n this.#active = true;\n this.#trap.activate();\n this.element.setAttribute(\"data-focus-trapped\", \"true\");\n this.dispatch(\"activate\", { detail: {} });\n }\n\n /** @stimeoRuntimeOnly `restore` decides whether releasing this one trap returns focus. */\n #deactivate(): void {\n if (!this.#active) return;\n this.#active = false;\n this.#trap.deactivate({ restoreFocus: this.restoreValue });\n this.element.removeAttribute(\"data-focus-trapped\");\n this.dispatch(\"deactivate\", { detail: {} });\n }\n}\n"]}
|