stimeo-ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (227) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/dist/cable/index.d.ts +16 -16
  3. package/dist/cable/index.js.map +1 -1
  4. package/dist/controllers/accordion_controller.d.ts +3 -2
  5. package/dist/controllers/accordion_controller.js +10 -0
  6. package/dist/controllers/accordion_controller.js.map +1 -1
  7. package/dist/controllers/alert_dialog_controller.d.ts +64 -0
  8. package/dist/controllers/alert_dialog_controller.js +318 -0
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -0
  10. package/dist/controllers/breadcrumb_controller.d.ts +97 -10
  11. package/dist/controllers/breadcrumb_controller.js +225 -13
  12. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  13. package/dist/controllers/calendar_controller.d.ts +11 -1
  14. package/dist/controllers/calendar_controller.js +89 -22
  15. package/dist/controllers/calendar_controller.js.map +1 -1
  16. package/dist/controllers/carousel_controller.d.ts +115 -0
  17. package/dist/controllers/carousel_controller.js +313 -0
  18. package/dist/controllers/carousel_controller.js.map +1 -0
  19. package/dist/controllers/clipboard_controller.d.ts +72 -0
  20. package/dist/controllers/clipboard_controller.js +144 -0
  21. package/dist/controllers/clipboard_controller.js.map +1 -0
  22. package/dist/controllers/collapsible_controller.d.ts +70 -0
  23. package/dist/controllers/collapsible_controller.js +327 -0
  24. package/dist/controllers/collapsible_controller.js.map +1 -0
  25. package/dist/controllers/color_picker_controller.d.ts +87 -0
  26. package/dist/controllers/color_picker_controller.js +252 -0
  27. package/dist/controllers/color_picker_controller.js.map +1 -0
  28. package/dist/controllers/combobox_controller.d.ts +14 -1
  29. package/dist/controllers/combobox_controller.js +162 -23
  30. package/dist/controllers/combobox_controller.js.map +1 -1
  31. package/dist/controllers/command_palette_controller.d.ts +2 -0
  32. package/dist/controllers/command_palette_controller.js +194 -17
  33. package/dist/controllers/command_palette_controller.js.map +1 -1
  34. package/dist/controllers/confirm_controller.js.map +1 -1
  35. package/dist/controllers/context_menu_controller.d.ts +7 -6
  36. package/dist/controllers/context_menu_controller.js +32 -10
  37. package/dist/controllers/context_menu_controller.js.map +1 -1
  38. package/dist/controllers/count_up_controller.js +8 -1
  39. package/dist/controllers/count_up_controller.js.map +1 -1
  40. package/dist/controllers/currency_input_controller.d.ts +72 -0
  41. package/dist/controllers/currency_input_controller.js +147 -0
  42. package/dist/controllers/currency_input_controller.js.map +1 -0
  43. package/dist/controllers/data_grid_controller.d.ts +89 -0
  44. package/dist/controllers/data_grid_controller.js +246 -0
  45. package/dist/controllers/data_grid_controller.js.map +1 -0
  46. package/dist/controllers/date_range_picker_controller.d.ts +88 -0
  47. package/dist/controllers/date_range_picker_controller.js +441 -0
  48. package/dist/controllers/date_range_picker_controller.js.map +1 -0
  49. package/dist/controllers/dialog_controller.js.map +1 -1
  50. package/dist/controllers/dismissible_controller.d.ts +57 -0
  51. package/dist/controllers/dismissible_controller.js +117 -0
  52. package/dist/controllers/dismissible_controller.js.map +1 -0
  53. package/dist/controllers/drawer_controller.d.ts +96 -0
  54. package/dist/controllers/drawer_controller.js +630 -0
  55. package/dist/controllers/drawer_controller.js.map +1 -0
  56. package/dist/controllers/dropdown_controller.d.ts +1 -2
  57. package/dist/controllers/dropdown_controller.js.map +1 -1
  58. package/dist/controllers/editable_controller.d.ts +73 -0
  59. package/dist/controllers/editable_controller.js +169 -0
  60. package/dist/controllers/editable_controller.js.map +1 -0
  61. package/dist/controllers/file_dropzone_controller.d.ts +92 -0
  62. package/dist/controllers/file_dropzone_controller.js +165 -0
  63. package/dist/controllers/file_dropzone_controller.js.map +1 -0
  64. package/dist/controllers/filter_controller.d.ts +60 -0
  65. package/dist/controllers/filter_controller.js +86 -0
  66. package/dist/controllers/filter_controller.js.map +1 -0
  67. package/dist/controllers/flash_controller.js +36 -5
  68. package/dist/controllers/flash_controller.js.map +1 -1
  69. package/dist/controllers/focus_controller.d.ts +1 -1
  70. package/dist/controllers/focus_controller.js.map +1 -1
  71. package/dist/controllers/form_field_controller.js.map +1 -1
  72. package/dist/controllers/form_validation_controller.js +1 -1
  73. package/dist/controllers/form_validation_controller.js.map +1 -1
  74. package/dist/controllers/highlight_controller.js +6 -4
  75. package/dist/controllers/highlight_controller.js.map +1 -1
  76. package/dist/controllers/hover_card_controller.d.ts +1 -1
  77. package/dist/controllers/hover_card_controller.js.map +1 -1
  78. package/dist/controllers/intersection_controller.js +67 -19
  79. package/dist/controllers/intersection_controller.js.map +1 -1
  80. package/dist/controllers/lazy_frame_controller.js +54 -11
  81. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  82. package/dist/controllers/listbox_controller.d.ts +9 -2
  83. package/dist/controllers/listbox_controller.js +257 -53
  84. package/dist/controllers/listbox_controller.js.map +1 -1
  85. package/dist/controllers/local_time_controller.js +2 -2
  86. package/dist/controllers/local_time_controller.js.map +1 -1
  87. package/dist/controllers/masonry_controller.d.ts +54 -0
  88. package/dist/controllers/masonry_controller.js +142 -0
  89. package/dist/controllers/masonry_controller.js.map +1 -0
  90. package/dist/controllers/menu_controller.d.ts +29 -11
  91. package/dist/controllers/menu_controller.js +104 -17
  92. package/dist/controllers/menu_controller.js.map +1 -1
  93. package/dist/controllers/menubar_controller.d.ts +142 -0
  94. package/dist/controllers/menubar_controller.js +785 -0
  95. package/dist/controllers/menubar_controller.js.map +1 -0
  96. package/dist/controllers/meter_controller.js.map +1 -1
  97. package/dist/controllers/multi_select_controller.d.ts +141 -0
  98. package/dist/controllers/multi_select_controller.js +755 -0
  99. package/dist/controllers/multi_select_controller.js.map +1 -0
  100. package/dist/controllers/navigation_menu_controller.d.ts +146 -0
  101. package/dist/controllers/navigation_menu_controller.js +511 -0
  102. package/dist/controllers/navigation_menu_controller.js.map +1 -0
  103. package/dist/controllers/nested_form_controller.d.ts +4 -5
  104. package/dist/controllers/nested_form_controller.js.map +1 -1
  105. package/dist/controllers/number_input_controller.d.ts +1 -1
  106. package/dist/controllers/number_input_controller.js +7 -0
  107. package/dist/controllers/number_input_controller.js.map +1 -1
  108. package/dist/controllers/otp_controller.js +18 -1
  109. package/dist/controllers/otp_controller.js.map +1 -1
  110. package/dist/controllers/overflow_indicator_controller.d.ts +15 -6
  111. package/dist/controllers/overflow_indicator_controller.js +246 -27
  112. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  113. package/dist/controllers/overflow_menu_controller.d.ts +78 -19
  114. package/dist/controllers/overflow_menu_controller.js +381 -57
  115. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  116. package/dist/controllers/pagination_controller.d.ts +36 -5
  117. package/dist/controllers/pagination_controller.js +163 -32
  118. package/dist/controllers/pagination_controller.js.map +1 -1
  119. package/dist/controllers/password_reveal_controller.d.ts +49 -0
  120. package/dist/controllers/password_reveal_controller.js +117 -0
  121. package/dist/controllers/password_reveal_controller.js.map +1 -0
  122. package/dist/controllers/persist_controller.js +6 -6
  123. package/dist/controllers/persist_controller.js.map +1 -1
  124. package/dist/controllers/pointer_drag_controller.d.ts +2 -3
  125. package/dist/controllers/pointer_drag_controller.js +9 -1
  126. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  127. package/dist/controllers/popover_controller.d.ts +1 -2
  128. package/dist/controllers/popover_controller.js +2 -2
  129. package/dist/controllers/popover_controller.js.map +1 -1
  130. package/dist/controllers/portal_controller.d.ts +4 -4
  131. package/dist/controllers/portal_controller.js.map +1 -1
  132. package/dist/controllers/progress_controller.js.map +1 -1
  133. package/dist/controllers/radio_group_controller.js +22 -3
  134. package/dist/controllers/radio_group_controller.js.map +1 -1
  135. package/dist/controllers/range_slider_controller.d.ts +97 -0
  136. package/dist/controllers/range_slider_controller.js +192 -0
  137. package/dist/controllers/range_slider_controller.js.map +1 -0
  138. package/dist/controllers/rating_controller.js +16 -2
  139. package/dist/controllers/rating_controller.js.map +1 -1
  140. package/dist/controllers/read_more_controller.d.ts +52 -0
  141. package/dist/controllers/read_more_controller.js +238 -0
  142. package/dist/controllers/read_more_controller.js.map +1 -0
  143. package/dist/controllers/reading_progress_controller.js.map +1 -1
  144. package/dist/controllers/resizable_controller.d.ts +3 -1
  145. package/dist/controllers/resizable_controller.js +65 -1
  146. package/dist/controllers/resizable_controller.js.map +1 -1
  147. package/dist/controllers/roving_controller.js +17 -2
  148. package/dist/controllers/roving_controller.js.map +1 -1
  149. package/dist/controllers/scroll_area_controller.js +101 -14
  150. package/dist/controllers/scroll_area_controller.js.map +1 -1
  151. package/dist/controllers/scroll_restore_controller.d.ts +54 -0
  152. package/dist/controllers/scroll_restore_controller.js +93 -0
  153. package/dist/controllers/scroll_restore_controller.js.map +1 -0
  154. package/dist/controllers/scroll_visibility_controller.d.ts +6 -3
  155. package/dist/controllers/scroll_visibility_controller.js +40 -6
  156. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  157. package/dist/controllers/scrollspy_controller.d.ts +64 -15
  158. package/dist/controllers/scrollspy_controller.js +369 -74
  159. package/dist/controllers/scrollspy_controller.js.map +1 -1
  160. package/dist/controllers/separator_controller.d.ts +69 -0
  161. package/dist/controllers/separator_controller.js +96 -0
  162. package/dist/controllers/separator_controller.js.map +1 -0
  163. package/dist/controllers/sidebar_controller.d.ts +107 -0
  164. package/dist/controllers/sidebar_controller.js +761 -0
  165. package/dist/controllers/sidebar_controller.js.map +1 -0
  166. package/dist/controllers/skeleton_controller.js +1 -1
  167. package/dist/controllers/skeleton_controller.js.map +1 -1
  168. package/dist/controllers/slider_controller.d.ts +11 -1
  169. package/dist/controllers/slider_controller.js +32 -6
  170. package/dist/controllers/slider_controller.js.map +1 -1
  171. package/dist/controllers/sortable_controller.d.ts +3 -1
  172. package/dist/controllers/sortable_controller.js +34 -3
  173. package/dist/controllers/sortable_controller.js.map +1 -1
  174. package/dist/controllers/spinner_controller.js +1 -1
  175. package/dist/controllers/spinner_controller.js.map +1 -1
  176. package/dist/controllers/stepper_controller.d.ts +4 -0
  177. package/dist/controllers/stepper_controller.js +28 -12
  178. package/dist/controllers/stepper_controller.js.map +1 -1
  179. package/dist/controllers/stick_to_bottom_controller.d.ts +5 -6
  180. package/dist/controllers/stick_to_bottom_controller.js +9 -4
  181. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  182. package/dist/controllers/sticky_observer_controller.d.ts +17 -4
  183. package/dist/controllers/sticky_observer_controller.js +109 -20
  184. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  185. package/dist/controllers/submit_once_controller.d.ts +2 -2
  186. package/dist/controllers/submit_once_controller.js.map +1 -1
  187. package/dist/controllers/switch_controller.js +1 -0
  188. package/dist/controllers/switch_controller.js.map +1 -1
  189. package/dist/controllers/tabs_controller.d.ts +13 -4
  190. package/dist/controllers/tabs_controller.js +26 -3
  191. package/dist/controllers/tabs_controller.js.map +1 -1
  192. package/dist/controllers/tags_input_controller.d.ts +91 -0
  193. package/dist/controllers/tags_input_controller.js +295 -0
  194. package/dist/controllers/tags_input_controller.js.map +1 -0
  195. package/dist/controllers/theme_controller.d.ts +1 -1
  196. package/dist/controllers/theme_controller.js +42 -13
  197. package/dist/controllers/theme_controller.js.map +1 -1
  198. package/dist/controllers/time_picker_controller.d.ts +67 -0
  199. package/dist/controllers/time_picker_controller.js +231 -0
  200. package/dist/controllers/time_picker_controller.js.map +1 -0
  201. package/dist/controllers/toast_controller.d.ts +5 -14
  202. package/dist/controllers/toast_controller.js +40 -14
  203. package/dist/controllers/toast_controller.js.map +1 -1
  204. package/dist/controllers/toggle_group_controller.js +23 -2
  205. package/dist/controllers/toggle_group_controller.js.map +1 -1
  206. package/dist/controllers/toolbar_controller.d.ts +53 -13
  207. package/dist/controllers/toolbar_controller.js +230 -31
  208. package/dist/controllers/toolbar_controller.js.map +1 -1
  209. package/dist/controllers/tooltip_controller.d.ts +1 -1
  210. package/dist/controllers/tooltip_controller.js.map +1 -1
  211. package/dist/controllers/transition_controller.d.ts +14 -8
  212. package/dist/controllers/transition_controller.js +153 -38
  213. package/dist/controllers/transition_controller.js.map +1 -1
  214. package/dist/controllers/tree_view_controller.d.ts +135 -0
  215. package/dist/controllers/tree_view_controller.js +691 -0
  216. package/dist/controllers/tree_view_controller.js.map +1 -0
  217. package/dist/index.d.ts +27 -1818
  218. package/dist/index.js +4256 -915
  219. package/dist/index.js.map +1 -1
  220. package/dist/inspector/cli.d.ts +375 -39
  221. package/dist/inspector/cli.js +228 -14
  222. package/dist/inspector/cli.js.map +1 -1
  223. package/dist/inspector/cli_bin.js +419 -48
  224. package/dist/inspector/cli_bin.js.map +1 -1
  225. package/dist/inspector/examples.json +26 -26
  226. package/dist/inspector/manifest.json +1054 -127
  227. package/package.json +5 -6
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/detach_gate.ts","../../src/controllers/pointer_drag_controller.ts"],"names":[],"mappings":"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","file":"pointer_drag_controller.js","sourcesContent":["/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer (the `SafeTimeout` / `FocusTrap` lineage —\n * this util owns the discrimination only):\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers (pinned by `pointer_drag_controller.ts`\n * and its teardown contract): teardown into a dead (detached) tree is silent —\n * consumers restore from their own `connect()`; a detach that keeps the tree\n * alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { DetachGate } from \"../utils/detach_gate\";\n\n/** How a drag session was initiated; surfaced in every event detail. */\ntype DragPointerType = \"mouse\" | \"touch\" | \"pen\" | \"keyboard\";\n\n/** A pointer drag in flight (from `pointerdown` until up/cancel). */\ninterface PointerSession {\n pointerId: number;\n handle: HTMLElement;\n originX: number;\n originY: number;\n /** True once the movement passed `threshold` and `start` was dispatched. */\n started: boolean;\n /** Last axis-filtered deltas, reported again by `end`. */\n dx: number;\n dy: number;\n pointerType: DragPointerType;\n}\n\n/** A keyboard \"grabbed\" session (Space/Enter on the handle until drop/cancel). */\ninterface KeyboardSession {\n handle: HTMLElement;\n /** Cumulative synthetic deltas accumulated by the arrow keys. */\n dx: number;\n dy: number;\n}\n\n/**\n * Headless **pointer-drag primitive**: a normalized pointer/touch/mouse drag\n * lifecycle with a built-in keyboard alternative. It deliberately does NOT decide\n * what a drag *means* (reorder, dismiss, resize…) — it only emits a clean,\n * accessibility-friendly drag signal that higher-level behaviors (sortable,\n * swipe-actions, bottom-sheet, split-pane…) compose from. No dedicated APG\n * pattern exists for drag-and-drop; the keyboard model below is the accepted\n * alternative-input technique (WCAG 2.1.1 / 2.5.7 Dragging Movements). Core\n * (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--pointer-drag`):\n * <li data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <button data-stimeo--pointer-drag-target=\"handle\" aria-label=\"Reorder\">⠿</button>\n * </li>\n *\n * The `handle` target is optional — without one, the controller element itself\n * is the handle. Pointer flow: `pointerdown` on a handle arms the drag; once the\n * axis-filtered movement exceeds `threshold`, `start` fires, every subsequent\n * movement fires `move` (deltas cumulative from the origin), and release fires\n * `end` (`pointercancel` → `cancel`). Keyboard flow: Space/Enter on the handle\n * grabs (`start`, `data-grabbed`), the arrow keys emit synthetic `move` events of\n * `keyboardStep` px each, Space/Enter drops (`end`) and Escape cancels — so every\n * consumer gets a keyboard path for free.\n *\n * @remarks\n * Behavior only: consumers own all visuals (ghost, placeholder, transforms) by\n * reacting to the events and the `data-dragging` / `data-grabbed` hooks. The one\n * deliberate exception is the opt-in `follow` value for the \"just move the\n * element\" case: it applies the in-flight deltas to the element's CSS\n * `translate` (a property independent of `transform`, so authored transforms\n * survive), commits the offset on drop and restores it on cancel — the exact\n * boilerplate every simple consumer would otherwise re-write. With `follow` the\n * element's inline `translate` belongs to this controller (`connect()` re-reads\n * a previously committed `<x>px <y>px` offset, so Turbo restores stay\n * consistent). ARIA\n * 1.1 deprecated `aria-grabbed`/`aria-dropeffect`, so the grabbed state is\n * published as `data-grabbed` and the *meaning* of a drag must be announced by\n * the consumer (pair with `stimeo--announcer`). `touch-action` on the handles is\n * derived from `axis` (marker-guarded, authored values win) so the page does not\n * pan mid-drag. Deltas are physical (client coordinates): RTL semantics belong\n * to the consumer. `disconnect()` (Turbo navigation included) tears down any\n * active session — except across an in-page move, which the session survives\n * (the full lifecycle contract lives on `disconnect()`); `connect()` clears\n * stale hooks a Turbo cache restore may have preserved (a drag cannot survive\n * a navigation).\n */\nexport class PointerDragController extends Controller<HTMLElement> {\n static override targets = [\"handle\"];\n static override values = {\n axis: { type: String, default: \"both\" },\n threshold: { type: Number, default: 3 },\n keyboardStep: { type: Number, default: 10 },\n disabled: { type: Boolean, default: false },\n follow: { type: Boolean, default: false },\n };\n static events = [\"start\", \"move\", \"end\", \"cancel\"] as const;\n\n declare readonly handleTargets: HTMLElement[];\n declare readonly hasHandleTarget: boolean;\n declare axisValue: string;\n declare thresholdValue: number;\n declare keyboardStepValue: number;\n declare disabledValue: boolean;\n declare followValue: boolean;\n\n /** Marker attribute recording that this controller set the touch-action. */\n static readonly #TOUCH_ACTION_MARKER = \"data-pointer-drag-touch-action\";\n /** Marker attribute recording that this controller set `tabindex`. */\n static readonly #TABINDEX_MARKER = \"data-pointer-drag-tabindex\";\n\n #pointer: PointerSession | null = null;\n #keyboard: KeyboardSession | null = null;\n /** Committed follow offset from past drops; a new drag's deltas add onto it. */\n #followBase = { x: 0, y: 0 };\n /** Aborts in-progress pointer-drag listeners on drag end / teardown. */\n #dragAbort: AbortController | null = null;\n /** Decides whether a mid-session disconnect() is an in-page move or a detach. */\n readonly #gate = new DetachGate();\n\n override connect(): void {\n // A reconnect disarms the probe a mid-session disconnect() armed — the\n // element was moved in-page and the session survives (see disconnect).\n this.#gate.cancel();\n // Transient drag state cannot survive a navigation: clear hooks a Turbo\n // cache restore may have snapshotted mid-drag (idempotent reconnect). But a\n // session preserved across an in-page move keeps its hooks.\n if (!this.#pointer) this.element.removeAttribute(\"data-dragging\");\n if (!this.#keyboard) this.element.removeAttribute(\"data-grabbed\");\n // Follow mode owns the element's inline `translate`: re-read a committed\n // offset so reconnects (Turbo cache restore included) keep accumulating from\n // where the element visually sits. A session surviving an in-page move keeps\n // its base — mid-drag inline state must not be committed as a new base.\n if (this.followValue && !this.#pointer && !this.#keyboard) {\n this.#followBase = this.#parseFollowBase();\n }\n // Delegated on the container so dynamically added handles need no per-element\n // data-action (stimulus-lifecycle-turbo Rule A′).\n this.element.addEventListener(\"pointerdown\", this.#onPointerDown);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n if (!this.hasHandleTarget) this.#prepareHandle(this.element);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"pointerdown\", this.#onPointerDown);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n // A mid-session disconnect on a STILL-connected element is ambiguous: an\n // in-page move (a consumer like sortable re-inserts the item mid-grab —\n // Stimulus reconnects the same instance in the same mutation batch, and\n // the session must survive) vs a real detach that keeps the element\n // (`data-controller` removed, Turbo morph, element moved outside the\n // observed root). DetachGate discriminates: the token check is the\n // synchronous fast path (a morph's teardown — and its `cancel` — fires in\n // the same tick), the one-microtask probe cancelled by connect() covers\n // the rest — a detached-in-place controller never leaks its session or\n // document listeners. Probe only while a session needs protecting; an\n // idle controller tears down synchronously either way.\n if (this.#pointer || this.#keyboard) {\n this.#gate.disconnected(this, () => this.#teardown());\n return;\n }\n this.#teardown();\n }\n\n /** Prepares handles added at runtime (touch-action + focusability). */\n handleTargetConnected(handle: HTMLElement): void {\n this.#prepareHandle(handle);\n }\n\n handleTargetDisconnected(handle: HTMLElement): void {\n // A handle REMOVED mid-session can never deliver its keydown (the delegated\n // listener sits on the container it left), so the session would leak — and\n // the one-session guard would then reject every future pointerdown. End it\n // silently: the DOM is changing under the user, same as disconnect. But\n // target callbacks ALSO fire when the CONTROLLER disconnects (Stimulus\n // stops the target observer — including the in-page move a live session\n // must survive): a handle still inside the element was not removed, so\n // that case is left to disconnect()'s deferred probe.\n if (!this.element.contains(handle)) {\n if (this.#pointer?.handle === handle) this.#endPointerSession();\n if (this.#keyboard?.handle === handle) this.#clearKeyboardSession();\n }\n this.#restoreHandle(handle);\n }\n\n /** Re-derives the handles' touch-action when the axis changes. */\n axisValueChanged(): void {\n for (const handle of this.#handles()) {\n if (handle.hasAttribute(PointerDragController.#TOUCH_ACTION_MARKER)) {\n handle.style.touchAction = this.#touchActionForAxis();\n }\n }\n }\n\n /** Cancels any in-flight session when the controller is disabled mid-drag. */\n disabledValueChanged(): void {\n if (!this.disabledValue) return;\n if (this.#pointer?.started || this.#keyboard) {\n this.#dispatchCancel(this.#keyboard ? \"keyboard\" : (this.#pointer?.pointerType ?? \"mouse\"));\n }\n this.#teardownSessions();\n }\n\n /** Arms a pointer drag on a handle; `start` waits for the threshold. */\n readonly #onPointerDown = (event: PointerEvent): void => {\n // One session at a time: ignore a second pointerdown (multi-touch, an\n // errant tap, a grabbed keyboard session) so an in-flight drag is never\n // silently overwritten and its captured pointer orphaned.\n if (this.disabledValue || this.#pointer || this.#keyboard || event.button !== 0) return;\n const handle = this.#handleFor(event.target);\n if (!handle) return;\n\n // Suppress text selection / native image drag; restore the focus the\n // suppressed default would have given, so Escape and the keyboard path\n // stay reachable right after a pointer interaction (WCAG 2.1.1).\n event.preventDefault();\n handle.focus();\n\n this.#pointer = {\n pointerId: event.pointerId,\n handle,\n originX: event.clientX,\n originY: event.clientY,\n started: false,\n dx: 0,\n dy: 0,\n pointerType: this.#pointerTypeOf(event),\n };\n\n // Pointer capture keeps fast drags delivering to the handle even when the\n // pointer strays outside it — but a consumer may re-insert the dragged\n // element mid-drag (sortable's live reorder), which silently releases the\n // capture. Tracking listeners therefore live on the DOCUMENT (captured\n // events bubble there; uncaptured ones fire there anyway), and the\n // AbortController releases them on drag end and disconnect() (Turbo included).\n handle.setPointerCapture(event.pointerId);\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n document.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n document.addEventListener(\"pointercancel\", this.#onPointerCancel, { signal: abort.signal });\n };\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n const session = this.#pointer;\n if (!session || event.pointerId !== session.pointerId) return;\n\n const [dx, dy] = this.#filterAxis(\n event.clientX - session.originX,\n event.clientY - session.originY,\n );\n if (!session.started) {\n if (Math.hypot(dx, dy) < this.thresholdValue) return;\n session.started = true;\n this.element.setAttribute(\"data-dragging\", \"true\");\n this.dispatch(\"start\", {\n detail: { x: event.clientX, y: event.clientY, pointerType: session.pointerType },\n });\n }\n\n session.dx = dx;\n session.dy = dy;\n this.#followMove(dx, dy);\n this.dispatch(\"move\", {\n detail: { dx, dy, x: event.clientX, y: event.clientY, pointerType: session.pointerType },\n });\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n const session = this.#pointer;\n if (!session || event.pointerId !== session.pointerId) return;\n const { started, dx, dy, pointerType } = session;\n this.#endPointerSession();\n // Below the threshold the gesture was a plain click, not a drag: stay silent.\n if (!started) return;\n this.#followCommit(dx, dy);\n this.dispatch(\"end\", { detail: { dx, dy, pointerType } });\n };\n\n /** OS gesture / scroll takeover interrupted the drag: cancel, don't drop. */\n readonly #onPointerCancel = (event: PointerEvent): void => {\n const session = this.#pointer;\n if (!session || event.pointerId !== session.pointerId) return;\n const { started, pointerType } = session;\n this.#endPointerSession();\n if (started) this.#dispatchCancel(pointerType);\n };\n\n /** Keyboard alternative: Space/Enter grab & drop, arrows move, Escape cancels. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (this.disabledValue) return;\n const handle = this.#handleFor(event.target);\n if (!handle) return;\n\n // Escape cancels whichever session is live (pointer drag or keyboard grab).\n if (event.key === \"Escape\") {\n // Layered-Escape rule 1: leave a press an inner handler already owned;\n // a press during IME composition steers the conversion, not the drag.\n if (event.defaultPrevented || event.isComposing) return;\n if (this.#pointer?.started) {\n const { pointerType } = this.#pointer;\n this.#teardownSessions();\n event.preventDefault();\n this.#dispatchCancel(pointerType);\n } else if (this.#keyboard) {\n this.#teardownSessions();\n event.preventDefault();\n this.#dispatchCancel(\"keyboard\");\n }\n return;\n }\n\n if (event.key === \" \" || event.key === \"Enter\") {\n // A live pointer drag owns the session: pressing Space mid-drag (the\n // pointerdown focused the handle) must not start a parallel grab.\n if (this.#pointer) return;\n event.preventDefault();\n if (this.#keyboard) this.#dropKeyboard();\n else this.#grabKeyboard(handle);\n return;\n }\n\n if (!this.#keyboard) return;\n const step = this.#keyboardDelta(event.key);\n if (!step) return;\n // Consume every arrow while grabbed (even on a locked axis) so the page\n // never scrolls mid-grab; only allowed-axis arrows emit a move.\n event.preventDefault();\n const [dx, dy] = this.#filterAxis(step[0], step[1]);\n if (dx === 0 && dy === 0) return;\n this.#keyboard.dx += dx;\n this.#keyboard.dy += dy;\n this.#followMove(this.#keyboard.dx, this.#keyboard.dy);\n this.dispatch(\"move\", {\n detail: {\n dx: this.#keyboard.dx,\n dy: this.#keyboard.dy,\n x: this.#keyboard.dx,\n y: this.#keyboard.dy,\n pointerType: \"keyboard\",\n },\n });\n };\n\n /** Enters the grabbed mode: `data-grabbed` on element + handle, then `start`. */\n #grabKeyboard(handle: HTMLElement): void {\n this.#keyboard = { handle, dx: 0, dy: 0 };\n this.element.setAttribute(\"data-grabbed\", \"true\");\n handle.setAttribute(\"data-grabbed\", \"true\");\n this.dispatch(\"start\", { detail: { x: 0, y: 0, pointerType: \"keyboard\" } });\n }\n\n #dropKeyboard(): void {\n const session = this.#keyboard;\n if (!session) return;\n this.#clearKeyboardSession();\n this.#followCommit(session.dx, session.dy);\n this.dispatch(\"end\", { detail: { dx: session.dx, dy: session.dy, pointerType: \"keyboard\" } });\n }\n\n /** Maps an arrow key to a raw (unfiltered) `keyboardStep` delta. */\n #keyboardDelta(key: string): [number, number] | null {\n const step = this.keyboardStepValue;\n switch (key) {\n case \"ArrowRight\":\n return [step, 0];\n case \"ArrowLeft\":\n return [-step, 0];\n case \"ArrowDown\":\n return [0, step];\n case \"ArrowUp\":\n return [0, -step];\n default:\n return null;\n }\n }\n\n /** Zeroes the delta on the locked axis (`axis` = x | y | both). */\n #filterAxis(dx: number, dy: number): [number, number] {\n if (this.axisValue === \"x\") return [dx, 0];\n if (this.axisValue === \"y\") return [0, dy];\n return [dx, dy];\n }\n\n /** Releases capture + listeners and clears the pointer session and its hook. */\n #endPointerSession(): void {\n this.#releasePointerCapture();\n this.#pointer = null;\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.element.removeAttribute(\"data-dragging\");\n }\n\n /** Releases the pointer capture the active session set (idempotent, safe). */\n #releasePointerCapture(): void {\n const session = this.#pointer;\n if (session?.handle.hasPointerCapture(session.pointerId)) {\n session.handle.releasePointerCapture(session.pointerId);\n }\n }\n\n #clearKeyboardSession(): void {\n this.#keyboard?.handle.removeAttribute(\"data-grabbed\");\n this.#keyboard = null;\n this.element.removeAttribute(\"data-grabbed\");\n }\n\n /**\n * Full teardown for a controller whose element is going away or lost its\n * controller. Into a dead (detached) tree it is silent — consumers restore\n * from their own `connect()`, not from a cancel event. But a detach that\n * KEEPS the element (Turbo morph, `data-controller` removed) leaves the\n * consumers alive and listening: an in-flight session must end in `cancel`\n * there, or a composer like sortable strands its one-at-a-time session\n * bookkeeping forever.\n */\n #teardown(): void {\n // Disarm any probe still queued: without this, an immediate teardown\n // (element removed right after a deferring disconnect) would let the\n // orphaned probe run #teardown a second time — a double cancel.\n this.#gate.cancel();\n const interrupted = this.#pointer?.started\n ? this.#pointer.pointerType\n : this.#keyboard\n ? (\"keyboard\" as const)\n : null;\n this.#teardownSessions();\n for (const handle of this.#handles()) this.#restoreHandle(handle);\n if (interrupted && this.element.isConnected) this.#dispatchCancel(interrupted);\n }\n\n /**\n * Silently tears down whatever session is live (disconnect / disabled /\n * Escape). Composed from the two single-session teardowns so pointer and\n * keyboard cleanup cannot diverge as either path evolves.\n */\n #teardownSessions(): void {\n this.#endPointerSession();\n this.#clearKeyboardSession();\n }\n\n #dispatchCancel(pointerType: DragPointerType): void {\n // Every cancel path funnels here, so follow's snap-back can never be missed\n // (Escape, pointercancel, mid-session disable, detach-that-keeps-the-element).\n this.#followReset();\n this.dispatch(\"cancel\", { detail: { pointerType } });\n }\n\n /** Applies the in-flight offset to the element's `translate` (follow only). */\n #followMove(dx: number, dy: number): void {\n if (!this.followValue) return;\n this.#applyFollow(this.#followBase.x + dx, this.#followBase.y + dy);\n }\n\n /** Folds a drop's deltas into the committed base offset (follow only). */\n #followCommit(dx: number, dy: number): void {\n if (!this.followValue) return;\n this.#followBase = { x: this.#followBase.x + dx, y: this.#followBase.y + dy };\n this.#applyFollow(this.#followBase.x, this.#followBase.y);\n }\n\n /** Snaps back to the committed position (follow only) — the cancel contract. */\n #followReset(): void {\n if (!this.followValue) return;\n this.#applyFollow(this.#followBase.x, this.#followBase.y);\n }\n\n /**\n * Writes the offset as the standalone CSS `translate` property — independent\n * of `transform`, so an authored transform (scale, rotate…) is never\n * clobbered. At the origin the property is removed to keep the DOM clean.\n */\n #applyFollow(x: number, y: number): void {\n if (x === 0 && y === 0) this.element.style.removeProperty(\"translate\");\n else this.element.style.setProperty(\"translate\", `${x}px ${y}px`);\n }\n\n /**\n * Reads a committed follow offset back from the inline `translate`. Only the\n * controller's own `<x>px <y>px` shape is recognized; anything else (an\n * authored percentage, `none`, an empty string) starts the base at 0 — follow\n * mode owns this property, per the class contract.\n */\n #parseFollowBase(): { x: number; y: number } {\n const raw = this.element.style.getPropertyValue(\"translate\").trim();\n const match = /^(-?\\d+(?:\\.\\d+)?)px(?:\\s+(-?\\d+(?:\\.\\d+)?)px)?$/.exec(raw);\n if (!match) return { x: 0, y: 0 };\n return { x: Number(match[1]), y: Number(match[2] ?? \"0\") };\n }\n\n /** The active handles: the `handle` targets, else the element itself. */\n #handles(): HTMLElement[] {\n return this.hasHandleTarget ? this.handleTargets : [this.element];\n }\n\n /** Resolves the handle owning an event target (the handle or a descendant). */\n #handleFor(target: EventTarget | null): HTMLElement | null {\n const node = target as Node | null;\n if (!node) return null;\n return this.#handles().find((handle) => handle === node || handle.contains(node)) ?? null;\n }\n\n #pointerTypeOf(event: PointerEvent): DragPointerType {\n const type = event.pointerType;\n if (type === \"touch\" || type === \"pen\") return type;\n return \"mouse\";\n }\n\n /** `touch-action` that lets the page keep panning on the locked axis only. */\n #touchActionForAxis(): string {\n if (this.axisValue === \"x\") return \"pan-y\";\n if (this.axisValue === \"y\") return \"pan-x\";\n return \"none\";\n }\n\n /**\n * Establishes the handle contract: a `touch-action` derived from `axis`\n * (marker-guarded so an authored inline value wins and reconnects stay\n * idempotent) and focusability for the keyboard path (`tabindex=\"0\"` only when\n * the author supplied none — prefer a real `<button>` handle). Both additions\n * are marker-owned so `#restoreHandle` reverts them symmetrically on teardown.\n */\n #prepareHandle(handle: HTMLElement): void {\n if (\n handle.style.touchAction === \"\" ||\n handle.hasAttribute(PointerDragController.#TOUCH_ACTION_MARKER)\n ) {\n handle.style.touchAction = this.#touchActionForAxis();\n handle.setAttribute(PointerDragController.#TOUCH_ACTION_MARKER, \"true\");\n }\n if (handle.tabIndex < 0 && !handle.hasAttribute(\"tabindex\")) {\n handle.setAttribute(\"tabindex\", \"0\");\n handle.setAttribute(PointerDragController.#TABINDEX_MARKER, \"true\");\n }\n }\n\n /** Reverts the marker-owned touch-action + tabindex (authored values untouched). */\n #restoreHandle(handle: HTMLElement): void {\n if (handle.hasAttribute(PointerDragController.#TOUCH_ACTION_MARKER)) {\n handle.style.touchAction = \"\";\n handle.removeAttribute(PointerDragController.#TOUCH_ACTION_MARKER);\n }\n if (handle.hasAttribute(PointerDragController.#TABINDEX_MARKER)) {\n handle.removeAttribute(PointerDragController.#TABINDEX_MARKER);\n // Only remove what is verifiably still ours and safe to remove: another\n // owner (e.g. a roving list) may have rewritten the value since, and\n // stripping tabindex off the focused element would blur it to <body>\n // (losing the user's place — worse than leaking one tab stop).\n if (handle.getAttribute(\"tabindex\") === \"0\" && document.activeElement !== handle) {\n handle.removeAttribute(\"tabindex\");\n }\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/arrow_step.ts","../../src/utils/detach_gate.ts","../../src/controllers/pointer_drag_controller.ts"],"names":[],"mappings":";;;;;AA+FO,SAAS,oBAAA,CACd,KAAA,EACA,KAAA,GAAkC,EAAC,EAC1B;AACT,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,UAAA,CAAW,OAAO,GAAG,OAAO,KAAA;AAC3C,EAAA,OACG,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,IACrC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,KACvC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACvC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAE9C;;;AC9BO,IAAM,UAAA,GAAN,MAAM,WAAA,CAAW;AAAA;AAAA,EAEtB,QAAA,GAAW,KAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOX,OAAO,WAAW,IAAA,EAA+B;AAC/C,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,OAAO,IAAA;AACtC,IAAA,MAAM,MAAA,GAAA,CAAU,KAAK,OAAA,CAAQ,YAAA,CAAa,iBAAiB,CAAA,IAAK,EAAA,EAAI,MAAM,KAAK,CAAA;AAC/E,IAAA,OAAO,CAAC,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,UAAU,CAAA;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,YAAA,CAAa,MAAsB,QAAA,EAA4B;AAC7D,IAAA,IAAI,WAAA,CAAW,UAAA,CAAW,IAAI,CAAA,EAAG;AAG/B,MAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAChB,MAAA,QAAA,EAAS;AACT,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,cAAA,CAAe,MAAM;AACnB,MAAA,IAAI,CAAC,KAAK,QAAA,EAAU;AACpB,MAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAChB,MAAA,QAAA,EAAS;AAAA,IACX,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,QAAA,GAAW,KAAA;AAAA,EAClB;AACF,CAAA;;;AChDO,IAAM,qBAAA,GAAN,MAAM,sBAAA,SAA8B,UAAA,CAAwB;AAAA,EACjE,OAAgB,OAAA,GAAU,CAAC,QAAQ,CAAA;AAAA,EACnC,OAAgB,MAAA,GAAS;AAAA,IACvB,IAAA,EAAM,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,MAAA,EAAO;AAAA,IACtC,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IACtC,YAAA,EAAc,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA,EAAG;AAAA,IAC1C,QAAA,EAAU,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA,EAAM;AAAA,IAC1C,MAAA,EAAQ,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA;AAAM,GAC1C;AAAA,EACA,OAAO,MAAA,GAAS,CAAC,OAAA,EAAS,MAAA,EAAQ,OAAO,QAAQ,CAAA;AAAA;AAAA,EAWjD,OAAgB,oBAAA,GAAuB,gCAAA;AAAA;AAAA,EAEvC,OAAgB,gBAAA,GAAmB,4BAAA;AAAA,EAEnC,QAAA,GAAkC,IAAA;AAAA,EAClC,SAAA,GAAoC,IAAA;AAAA;AAAA,EAEpC,WAAA,GAAc,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAAA;AAAA,EAE3B,UAAA,GAAqC,IAAA;AAAA;AAAA,EAE5B,KAAA,GAAQ,IAAI,UAAA,EAAW;AAAA,EAEvB,OAAA,GAAgB;AAGvB,IAAA,IAAA,CAAK,MAAM,MAAA,EAAO;AAIlB,IAAA,IAAI,CAAC,IAAA,CAAK,QAAA,EAAU,IAAA,CAAK,OAAA,CAAQ,gBAAgB,eAAe,CAAA;AAChE,IAAA,IAAI,CAAC,IAAA,CAAK,SAAA,EAAW,IAAA,CAAK,OAAA,CAAQ,gBAAgB,cAAc,CAAA;AAKhE,IAAA,IAAI,KAAK,WAAA,IAAe,CAAC,KAAK,QAAA,IAAY,CAAC,KAAK,SAAA,EAAW;AACzD,MAAA,IAAA,CAAK,WAAA,GAAc,KAAK,gBAAA,EAAiB;AAAA,IAC3C;AAGA,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,aAAA,EAAe,IAAA,CAAK,cAAc,CAAA;AAChE,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACxD,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,cAAA,CAAe,KAAK,OAAO,CAAA;AAAA,EAC7D;AAAA,EAES,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,aAAA,EAAe,IAAA,CAAK,cAAc,CAAA;AACnE,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AAY3D,IAAA,IAAI,IAAA,CAAK,QAAA,IAAY,IAAA,CAAK,SAAA,EAAW;AACnC,MAAA,IAAA,CAAK,MAAM,YAAA,CAAa,IAAA,EAAM,MAAM,IAAA,CAAK,WAAW,CAAA;AACpD,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,SAAA,EAAU;AAAA,EACjB;AAAA;AAAA,EAGA,sBAAsB,MAAA,EAA2B;AAC/C,IAAA,IAAA,CAAK,eAAe,MAAM,CAAA;AAAA,EAC5B;AAAA,EAEA,yBAAyB,MAAA,EAA2B;AASlD,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA,EAAG;AAClC,MAAA,IAAI,IAAA,CAAK,QAAA,EAAU,MAAA,KAAW,MAAA,OAAa,kBAAA,EAAmB;AAC9D,MAAA,IAAI,IAAA,CAAK,SAAA,EAAW,MAAA,KAAW,MAAA,OAAa,qBAAA,EAAsB;AAAA,IACpE;AACA,IAAA,IAAA,CAAK,eAAe,MAAM,CAAA;AAAA,EAC5B;AAAA;AAAA,EAGA,gBAAA,GAAyB;AACvB,IAAA,KAAA,MAAW,MAAA,IAAU,IAAA,CAAK,QAAA,EAAS,EAAG;AACpC,MAAA,IAAI,MAAA,CAAO,YAAA,CAAa,sBAAA,CAAsB,oBAAoB,CAAA,EAAG;AACnE,QAAA,MAAA,CAAO,KAAA,CAAM,WAAA,GAAc,IAAA,CAAK,mBAAA,EAAoB;AAAA,MACtD;AAAA,IACF;AAAA,EACF;AAAA;AAAA,EAGA,oBAAA,GAA6B;AAC3B,IAAA,IAAI,CAAC,KAAK,aAAA,EAAe;AACzB,IAAA,IAAI,IAAA,CAAK,QAAA,EAAU,OAAA,IAAW,IAAA,CAAK,SAAA,EAAW;AAC5C,MAAA,IAAA,CAAK,gBAAgB,IAAA,CAAK,SAAA,GAAY,aAAc,IAAA,CAAK,QAAA,EAAU,eAAe,OAAQ,CAAA;AAAA,IAC5F;AACA,IAAA,IAAA,CAAK,iBAAA,EAAkB;AAAA,EACzB;AAAA;AAAA,EAGS,cAAA,GAAiB,CAAC,KAAA,KAA8B;AAIvD,IAAA,IAAI,IAAA,CAAK,iBAAiB,IAAA,CAAK,QAAA,IAAY,KAAK,SAAA,IAAa,KAAA,CAAM,WAAW,CAAA,EAAG;AACjF,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,KAAA,CAAM,MAAM,CAAA;AAC3C,IAAA,IAAI,CAAC,MAAA,EAAQ;AAKb,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,MAAA,CAAO,KAAA,EAAM;AAEb,IAAA,IAAA,CAAK,QAAA,GAAW;AAAA,MACd,WAAW,KAAA,CAAM,SAAA;AAAA,MACjB,MAAA;AAAA,MACA,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,SAAS,KAAA,CAAM,OAAA;AAAA,MACf,OAAA,EAAS,KAAA;AAAA,MACT,EAAA,EAAI,CAAA;AAAA,MACJ,EAAA,EAAI,CAAA;AAAA,MACJ,WAAA,EAAa,IAAA,CAAK,cAAA,CAAe,KAAK;AAAA,KACxC;AAQA,IAAA,MAAA,CAAO,iBAAA,CAAkB,MAAM,SAAS,CAAA;AACxC,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AACvB,IAAA,MAAM,KAAA,GAAQ,IAAI,eAAA,EAAgB;AAClC,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,QAAA,CAAS,gBAAA,CAAiB,eAAe,IAAA,CAAK,cAAA,EAAgB,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AACtF,IAAA,QAAA,CAAS,gBAAA,CAAiB,aAAa,IAAA,CAAK,YAAA,EAAc,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AAClF,IAAA,QAAA,CAAS,gBAAA,CAAiB,iBAAiB,IAAA,CAAK,gBAAA,EAAkB,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AAAA,EAC5F,CAAA;AAAA,EAES,cAAA,GAAiB,CAAC,KAAA,KAA8B;AACvD,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,IAAI,CAAC,OAAA,IAAW,KAAA,CAAM,SAAA,KAAc,QAAQ,SAAA,EAAW;AAEvD,IAAA,MAAM,CAAC,EAAA,EAAI,EAAE,CAAA,GAAI,IAAA,CAAK,WAAA;AAAA,MACpB,KAAA,CAAM,UAAU,OAAA,CAAQ,OAAA;AAAA,MACxB,KAAA,CAAM,UAAU,OAAA,CAAQ;AAAA,KAC1B;AACA,IAAA,IAAI,CAAC,QAAQ,OAAA,EAAS;AACpB,MAAA,IAAI,KAAK,KAAA,CAAM,EAAA,EAAI,EAAE,CAAA,GAAI,KAAK,cAAA,EAAgB;AAC9C,MAAA,OAAA,CAAQ,OAAA,GAAU,IAAA;AAClB,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,eAAA,EAAiB,MAAM,CAAA;AACjD,MAAA,IAAA,CAAK,SAAS,OAAA,EAAS;AAAA,QACrB,MAAA,EAAQ,EAAE,CAAA,EAAG,KAAA,CAAM,OAAA,EAAS,GAAG,KAAA,CAAM,OAAA,EAAS,WAAA,EAAa,OAAA,CAAQ,WAAA;AAAY,OAChF,CAAA;AAAA,IACH;AAEA,IAAA,OAAA,CAAQ,EAAA,GAAK,EAAA;AACb,IAAA,OAAA,CAAQ,EAAA,GAAK,EAAA;AACb,IAAA,IAAA,CAAK,WAAA,CAAY,IAAI,EAAE,CAAA;AACvB,IAAA,IAAA,CAAK,SAAS,MAAA,EAAQ;AAAA,MACpB,MAAA,EAAQ,EAAE,EAAA,EAAI,EAAA,EAAI,CAAA,EAAG,KAAA,CAAM,OAAA,EAAS,CAAA,EAAG,KAAA,CAAM,OAAA,EAAS,WAAA,EAAa,OAAA,CAAQ,WAAA;AAAY,KACxF,CAAA;AAAA,EACH,CAAA;AAAA,EAES,YAAA,GAAe,CAAC,KAAA,KAA8B;AACrD,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,IAAI,CAAC,OAAA,IAAW,KAAA,CAAM,SAAA,KAAc,QAAQ,SAAA,EAAW;AACvD,IAAA,MAAM,EAAE,OAAA,EAAS,EAAA,EAAI,EAAA,EAAI,aAAY,GAAI,OAAA;AACzC,IAAA,IAAA,CAAK,kBAAA,EAAmB;AAExB,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,IAAA,CAAK,aAAA,CAAc,IAAI,EAAE,CAAA;AACzB,IAAA,IAAA,CAAK,QAAA,CAAS,OAAO,EAAE,MAAA,EAAQ,EAAE,EAAA,EAAI,EAAA,EAAI,WAAA,EAAY,EAAG,CAAA;AAAA,EAC1D,CAAA;AAAA;AAAA,EAGS,gBAAA,GAAmB,CAAC,KAAA,KAA8B;AACzD,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,IAAI,CAAC,OAAA,IAAW,KAAA,CAAM,SAAA,KAAc,QAAQ,SAAA,EAAW;AACvD,IAAA,MAAM,EAAE,OAAA,EAAS,WAAA,EAAY,GAAI,OAAA;AACjC,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAI,OAAA,EAAS,IAAA,CAAK,eAAA,CAAgB,WAAW,CAAA;AAAA,EAC/C,CAAA;AAAA;AAAA,EAGS,UAAA,GAAa,CAAC,KAAA,KAA+B;AACpD,IAAA,IAAI,KAAK,aAAA,EAAe;AACxB,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,UAAA,CAAW,KAAA,CAAM,MAAM,CAAA;AAC3C,IAAA,IAAI,CAAC,MAAA,EAAQ;AAOb,IAAA,IAAI,MAAM,gBAAA,EAAkB;AAG5B,IAAA,IAAI,oBAAA,CAAqB,KAAK,CAAA,EAAG;AAGjC,IAAA,IAAI,KAAA,CAAM,QAAQ,QAAA,EAAU;AAI1B,MAAA,IAAI,MAAM,WAAA,EAAa;AACvB,MAAA,IAAI,IAAA,CAAK,UAAU,OAAA,EAAS;AAC1B,QAAA,MAAM,EAAE,WAAA,EAAY,GAAI,IAAA,CAAK,QAAA;AAC7B,QAAA,IAAA,CAAK,iBAAA,EAAkB;AACvB,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,gBAAgB,WAAW,CAAA;AAAA,MAClC,CAAA,MAAA,IAAW,KAAK,SAAA,EAAW;AACzB,QAAA,IAAA,CAAK,iBAAA,EAAkB;AACvB,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,IAAA,CAAK,gBAAgB,UAAU,CAAA;AAAA,MACjC;AACA,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,GAAA,IAAO,KAAA,CAAM,QAAQ,OAAA,EAAS;AAG9C,MAAA,IAAI,KAAK,QAAA,EAAU;AACnB,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAI,IAAA,CAAK,SAAA,EAAW,IAAA,CAAK,aAAA,EAAc;AAAA,WAClC,IAAA,CAAK,cAAc,MAAM,CAAA;AAC9B,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,CAAC,KAAK,SAAA,EAAW;AACrB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,cAAA,CAAe,KAAA,CAAM,GAAG,CAAA;AAC1C,IAAA,IAAI,CAAC,IAAA,EAAM;AAGX,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,MAAM,CAAC,EAAA,EAAI,EAAE,CAAA,GAAI,IAAA,CAAK,WAAA,CAAY,IAAA,CAAK,CAAC,CAAA,EAAG,IAAA,CAAK,CAAC,CAAC,CAAA;AAClD,IAAA,IAAI,EAAA,KAAO,CAAA,IAAK,EAAA,KAAO,CAAA,EAAG;AAC1B,IAAA,IAAA,CAAK,UAAU,EAAA,IAAM,EAAA;AACrB,IAAA,IAAA,CAAK,UAAU,EAAA,IAAM,EAAA;AACrB,IAAA,IAAA,CAAK,YAAY,IAAA,CAAK,SAAA,CAAU,EAAA,EAAI,IAAA,CAAK,UAAU,EAAE,CAAA;AACrD,IAAA,IAAA,CAAK,SAAS,MAAA,EAAQ;AAAA,MACpB,MAAA,EAAQ;AAAA,QACN,EAAA,EAAI,KAAK,SAAA,CAAU,EAAA;AAAA,QACnB,EAAA,EAAI,KAAK,SAAA,CAAU,EAAA;AAAA,QACnB,CAAA,EAAG,KAAK,SAAA,CAAU,EAAA;AAAA,QAClB,CAAA,EAAG,KAAK,SAAA,CAAU,EAAA;AAAA,QAClB,WAAA,EAAa;AAAA;AACf,KACD,CAAA;AAAA,EACH,CAAA;AAAA;AAAA,EAGA,cAAc,MAAA,EAA2B;AACvC,IAAA,IAAA,CAAK,YAAY,EAAE,MAAA,EAAQ,EAAA,EAAI,CAAA,EAAG,IAAI,CAAA,EAAE;AACxC,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,cAAA,EAAgB,MAAM,CAAA;AAChD,IAAA,MAAA,CAAO,YAAA,CAAa,gBAAgB,MAAM,CAAA;AAC1C,IAAA,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS,EAAE,MAAA,EAAQ,EAAE,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,WAAA,EAAa,UAAA,EAAW,EAAG,CAAA;AAAA,EAC5E;AAAA,EAEA,aAAA,GAAsB;AACpB,IAAA,MAAM,UAAU,IAAA,CAAK,SAAA;AACrB,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAC3B,IAAA,IAAA,CAAK,aAAA,CAAc,OAAA,CAAQ,EAAA,EAAI,OAAA,CAAQ,EAAE,CAAA;AACzC,IAAA,IAAA,CAAK,QAAA,CAAS,KAAA,EAAO,EAAE,MAAA,EAAQ,EAAE,EAAA,EAAI,OAAA,CAAQ,EAAA,EAAI,EAAA,EAAI,OAAA,CAAQ,EAAA,EAAI,WAAA,EAAa,UAAA,IAAc,CAAA;AAAA,EAC9F;AAAA;AAAA,EAGA,eAAe,GAAA,EAAsC;AACnD,IAAA,MAAM,OAAO,IAAA,CAAK,iBAAA;AAClB,IAAA,QAAQ,GAAA;AAAK,MACX,KAAK,YAAA;AACH,QAAA,OAAO,CAAC,MAAM,CAAC,CAAA;AAAA,MACjB,KAAK,WAAA;AACH,QAAA,OAAO,CAAC,CAAC,IAAA,EAAM,CAAC,CAAA;AAAA,MAClB,KAAK,WAAA;AACH,QAAA,OAAO,CAAC,GAAG,IAAI,CAAA;AAAA,MACjB,KAAK,SAAA;AACH,QAAA,OAAO,CAAC,CAAA,EAAG,CAAC,IAAI,CAAA;AAAA,MAClB;AACE,QAAA,OAAO,IAAA;AAAA;AACX,EACF;AAAA;AAAA,EAGA,WAAA,CAAY,IAAY,EAAA,EAA8B;AACpD,IAAA,IAAI,KAAK,SAAA,KAAc,GAAA,EAAK,OAAO,CAAC,IAAI,CAAC,CAAA;AACzC,IAAA,IAAI,KAAK,SAAA,KAAc,GAAA,EAAK,OAAO,CAAC,GAAG,EAAE,CAAA;AACzC,IAAA,OAAO,CAAC,IAAI,EAAE,CAAA;AAAA,EAChB;AAAA;AAAA,EAGA,kBAAA,GAA2B;AACzB,IAAA,IAAA,CAAK,sBAAA,EAAuB;AAC5B,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA;AAChB,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,eAAe,CAAA;AAAA,EAC9C;AAAA;AAAA,EAGA,sBAAA,GAA+B;AAC7B,IAAA,MAAM,UAAU,IAAA,CAAK,QAAA;AACrB,IAAA,IAAI,OAAA,EAAS,MAAA,CAAO,iBAAA,CAAkB,OAAA,CAAQ,SAAS,CAAA,EAAG;AACxD,MAAA,OAAA,CAAQ,MAAA,CAAO,qBAAA,CAAsB,OAAA,CAAQ,SAAS,CAAA;AAAA,IACxD;AAAA,EACF;AAAA,EAEA,qBAAA,GAA8B;AAC5B,IAAA,IAAA,CAAK,SAAA,EAAW,MAAA,CAAO,eAAA,CAAgB,cAAc,CAAA;AACrD,IAAA,IAAA,CAAK,SAAA,GAAY,IAAA;AACjB,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,cAAc,CAAA;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,SAAA,GAAkB;AAIhB,IAAA,IAAA,CAAK,MAAM,MAAA,EAAO;AAClB,IAAA,MAAM,WAAA,GAAc,KAAK,QAAA,EAAU,OAAA,GAC/B,KAAK,QAAA,CAAS,WAAA,GACd,IAAA,CAAK,SAAA,GACF,UAAA,GACD,IAAA;AACN,IAAA,IAAA,CAAK,iBAAA,EAAkB;AACvB,IAAA,KAAA,MAAW,UAAU,IAAA,CAAK,QAAA,EAAS,EAAG,IAAA,CAAK,eAAe,MAAM,CAAA;AAChE,IAAA,IAAI,eAAe,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,IAAA,CAAK,gBAAgB,WAAW,CAAA;AAAA,EAC/E;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,iBAAA,GAA0B;AACxB,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAA,CAAK,qBAAA,EAAsB;AAAA,EAC7B;AAAA,EAEA,gBAAgB,WAAA,EAAoC;AAGlD,IAAA,IAAA,CAAK,YAAA,EAAa;AAClB,IAAA,IAAA,CAAK,SAAS,QAAA,EAAU,EAAE,QAAQ,EAAE,WAAA,IAAe,CAAA;AAAA,EACrD;AAAA;AAAA,EAGA,WAAA,CAAY,IAAY,EAAA,EAAkB;AACxC,IAAA,IAAI,CAAC,KAAK,WAAA,EAAa;AACvB,IAAA,IAAA,CAAK,YAAA,CAAa,KAAK,WAAA,CAAY,CAAA,GAAI,IAAI,IAAA,CAAK,WAAA,CAAY,IAAI,EAAE,CAAA;AAAA,EACpE;AAAA;AAAA,EAGA,aAAA,CAAc,IAAY,EAAA,EAAkB;AAC1C,IAAA,IAAI,CAAC,KAAK,WAAA,EAAa;AACvB,IAAA,IAAA,CAAK,WAAA,GAAc,EAAE,CAAA,EAAG,IAAA,CAAK,WAAA,CAAY,CAAA,GAAI,EAAA,EAAI,CAAA,EAAG,IAAA,CAAK,WAAA,CAAY,CAAA,GAAI,EAAA,EAAG;AAC5E,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,WAAA,CAAY,CAAA,EAAG,IAAA,CAAK,YAAY,CAAC,CAAA;AAAA,EAC1D;AAAA;AAAA,EAGA,YAAA,GAAqB;AACnB,IAAA,IAAI,CAAC,KAAK,WAAA,EAAa;AACvB,IAAA,IAAA,CAAK,aAAa,IAAA,CAAK,WAAA,CAAY,CAAA,EAAG,IAAA,CAAK,YAAY,CAAC,CAAA;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,YAAA,CAAa,GAAW,CAAA,EAAiB;AACvC,IAAA,IAAI,CAAA,KAAM,KAAK,CAAA,KAAM,CAAA,OAAQ,OAAA,CAAQ,KAAA,CAAM,eAAe,WAAW,CAAA;AAAA,SAChE,IAAA,CAAK,QAAQ,KAAA,CAAM,WAAA,CAAY,aAAa,CAAA,EAAG,CAAC,CAAA,GAAA,EAAM,CAAC,CAAA,EAAA,CAAI,CAAA;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,gBAAA,GAA6C;AAC3C,IAAA,MAAM,MAAM,IAAA,CAAK,OAAA,CAAQ,MAAM,gBAAA,CAAiB,WAAW,EAAE,IAAA,EAAK;AAClE,IAAA,MAAM,KAAA,GAAQ,kDAAA,CAAmD,IAAA,CAAK,GAAG,CAAA;AACzE,IAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAE,CAAA,EAAG,CAAA,EAAG,GAAG,CAAA,EAAE;AAChC,IAAA,OAAO,EAAE,CAAA,EAAG,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA,EAAG,CAAA,EAAG,MAAA,CAAO,KAAA,CAAM,CAAC,CAAA,IAAK,GAAG,CAAA,EAAE;AAAA,EAC3D;AAAA;AAAA,EAGA,QAAA,GAA0B;AACxB,IAAA,OAAO,KAAK,eAAA,GAAkB,IAAA,CAAK,aAAA,GAAgB,CAAC,KAAK,OAAO,CAAA;AAAA,EAClE;AAAA;AAAA,EAGA,WAAW,MAAA,EAAgD;AACzD,IAAA,MAAM,IAAA,GAAO,MAAA;AACb,IAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAClB,IAAA,OAAO,IAAA,CAAK,QAAA,EAAS,CAAE,IAAA,CAAK,CAAC,MAAA,KAAW,MAAA,KAAW,IAAA,IAAQ,MAAA,CAAO,QAAA,CAAS,IAAI,CAAC,CAAA,IAAK,IAAA;AAAA,EACvF;AAAA,EAEA,eAAe,KAAA,EAAsC;AACnD,IAAA,MAAM,OAAO,KAAA,CAAM,WAAA;AACnB,IAAA,IAAI,IAAA,KAAS,OAAA,IAAW,IAAA,KAAS,KAAA,EAAO,OAAO,IAAA;AAC/C,IAAA,OAAO,OAAA;AAAA,EACT;AAAA;AAAA,EAGA,mBAAA,GAA8B;AAC5B,IAAA,IAAI,IAAA,CAAK,SAAA,KAAc,GAAA,EAAK,OAAO,OAAA;AACnC,IAAA,IAAI,IAAA,CAAK,SAAA,KAAc,GAAA,EAAK,OAAO,OAAA;AACnC,IAAA,OAAO,MAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,eAAe,MAAA,EAA2B;AACxC,IAAA,IACE,MAAA,CAAO,MAAM,WAAA,KAAgB,EAAA,IAC7B,OAAO,YAAA,CAAa,sBAAA,CAAsB,oBAAoB,CAAA,EAC9D;AACA,MAAA,MAAA,CAAO,KAAA,CAAM,WAAA,GAAc,IAAA,CAAK,mBAAA,EAAoB;AACpD,MAAA,MAAA,CAAO,YAAA,CAAa,sBAAA,CAAsB,oBAAA,EAAsB,MAAM,CAAA;AAAA,IACxE;AACA,IAAA,IAAI,OAAO,QAAA,GAAW,CAAA,IAAK,CAAC,MAAA,CAAO,YAAA,CAAa,UAAU,CAAA,EAAG;AAC3D,MAAA,MAAA,CAAO,YAAA,CAAa,YAAY,GAAG,CAAA;AACnC,MAAA,MAAA,CAAO,YAAA,CAAa,sBAAA,CAAsB,gBAAA,EAAkB,MAAM,CAAA;AAAA,IACpE;AAAA,EACF;AAAA;AAAA,EAGA,eAAe,MAAA,EAA2B;AACxC,IAAA,IAAI,MAAA,CAAO,YAAA,CAAa,sBAAA,CAAsB,oBAAoB,CAAA,EAAG;AACnE,MAAA,MAAA,CAAO,MAAM,WAAA,GAAc,EAAA;AAC3B,MAAA,MAAA,CAAO,eAAA,CAAgB,uBAAsB,oBAAoB,CAAA;AAAA,IACnE;AACA,IAAA,IAAI,MAAA,CAAO,YAAA,CAAa,sBAAA,CAAsB,gBAAgB,CAAA,EAAG;AAC/D,MAAA,MAAA,CAAO,eAAA,CAAgB,uBAAsB,gBAAgB,CAAA;AAK7D,MAAA,IAAI,OAAO,YAAA,CAAa,UAAU,MAAM,GAAA,IAAO,QAAA,CAAS,kBAAkB,MAAA,EAAQ;AAChF,QAAA,MAAA,CAAO,gBAAgB,UAAU,CAAA;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AACF","file":"pointer_drag_controller.js","sourcesContent":["import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer (the `SafeTimeout` / `FocusTrap` lineage —\n * this util owns the discrimination only):\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { DetachGate } from \"../utils/detach_gate\";\n\n/** How a drag session was initiated; surfaced in every event detail. */\ntype DragPointerType = \"mouse\" | \"touch\" | \"pen\" | \"keyboard\";\n\n/** A pointer drag in flight (from `pointerdown` until up/cancel). */\ninterface PointerSession {\n pointerId: number;\n handle: HTMLElement;\n originX: number;\n originY: number;\n /** True once the movement passed `threshold` and `start` was dispatched. */\n started: boolean;\n /** Last axis-filtered deltas, reported again by `end`. */\n dx: number;\n dy: number;\n pointerType: DragPointerType;\n}\n\n/** A keyboard \"grabbed\" session (Space/Enter on the handle until drop/cancel). */\ninterface KeyboardSession {\n handle: HTMLElement;\n /** Cumulative synthetic deltas accumulated by the arrow keys. */\n dx: number;\n dy: number;\n}\n\n/**\n * Headless **pointer-drag primitive**: a normalized pointer/touch/mouse drag\n * lifecycle with a built-in keyboard alternative. It deliberately does NOT decide\n * what a drag *means* (reorder, dismiss, resize…) — it only emits a clean,\n * accessibility-friendly drag signal that higher-level behaviors (sortable,\n * swipe-actions, bottom-sheet, split-pane…) compose from. No dedicated APG\n * pattern exists for drag-and-drop; the keyboard model below is the accepted\n * alternative-input technique (WCAG 2.1.1 / 2.5.7 Dragging Movements). Core\n * (zero dependencies).\n *\n * Markup contract (identifier: `stimeo--pointer-drag`):\n * <li data-controller=\"stimeo--pointer-drag\"\n * data-stimeo--pointer-drag-axis-value=\"y\">\n * <button data-stimeo--pointer-drag-target=\"handle\" aria-label=\"Reorder\">⠿</button>\n * </li>\n *\n * The `handle` target is optional — without one, the controller element itself\n * is the handle. Pointer flow: `pointerdown` on a handle arms the drag; once the\n * axis-filtered movement exceeds `threshold`, `start` fires, every subsequent\n * movement fires `move` (deltas cumulative from the origin), and release fires\n * `end` (`pointercancel` → `cancel`). Keyboard flow: Space/Enter on the handle\n * grabs (`start`, `data-grabbed`), the arrow keys emit synthetic `move` events of\n * `keyboardStep` px each, Space/Enter drops (`end`) and Escape cancels — so every\n * consumer gets a keyboard path for free.\n *\n * @remarks\n * Behavior only: consumers own all visuals (ghost, placeholder, transforms) by\n * reacting to the events and the `data-dragging` / `data-grabbed` hooks. The one\n * deliberate exception is the opt-in `follow` value for the \"just move the\n * element\" case: it applies the in-flight deltas to the element's CSS\n * `translate` (a property independent of `transform`, so authored transforms\n * survive), commits the offset on drop and restores it on cancel — the exact\n * boilerplate every simple consumer would otherwise re-write. With `follow` the\n * element's inline `translate` belongs to this controller (`connect()` re-reads\n * a committed `<x>px <y>px` offset, so Turbo restores stay consistent). ARIA 1.1\n * deprecated `aria-grabbed`/`aria-dropeffect`, so the grabbed state is\n * published as `data-grabbed` and the *meaning* of a drag must be announced by\n * the consumer (pair with `stimeo--announcer`). `touch-action` on the handles is\n * derived from `axis` (marker-guarded, authored values win) so the page does not\n * pan mid-drag. Deltas are physical (client coordinates): RTL semantics belong\n * to the consumer. `disconnect()` (Turbo navigation included) tears down any\n * active session — except across an in-page move, which the session survives\n * (the full lifecycle contract lives on `disconnect()`); `connect()` clears\n * stale hooks a Turbo cache restore may have preserved (a drag cannot survive\n * a navigation).\n */\nexport class PointerDragController extends Controller<HTMLElement> {\n static override targets = [\"handle\"];\n static override values = {\n axis: { type: String, default: \"both\" },\n threshold: { type: Number, default: 3 },\n keyboardStep: { type: Number, default: 10 },\n disabled: { type: Boolean, default: false },\n follow: { type: Boolean, default: false },\n };\n static events = [\"start\", \"move\", \"end\", \"cancel\"] as const;\n\n declare readonly handleTargets: HTMLElement[];\n declare readonly hasHandleTarget: boolean;\n declare axisValue: string;\n declare thresholdValue: number;\n declare keyboardStepValue: number;\n declare disabledValue: boolean;\n declare followValue: boolean;\n\n /** Marker attribute recording that this controller set the touch-action. */\n static readonly #TOUCH_ACTION_MARKER = \"data-pointer-drag-touch-action\";\n /** Marker attribute recording that this controller set `tabindex`. */\n static readonly #TABINDEX_MARKER = \"data-pointer-drag-tabindex\";\n\n #pointer: PointerSession | null = null;\n #keyboard: KeyboardSession | null = null;\n /** Committed follow offset from past drops; a new drag's deltas add onto it. */\n #followBase = { x: 0, y: 0 };\n /** Aborts in-progress pointer-drag listeners on drag end / teardown. */\n #dragAbort: AbortController | null = null;\n /** Decides whether a mid-session disconnect() is an in-page move or a detach. */\n readonly #gate = new DetachGate();\n\n override connect(): void {\n // A reconnect disarms the probe a mid-session disconnect() armed — the\n // element was moved in-page and the session survives (see disconnect).\n this.#gate.cancel();\n // Transient drag state cannot survive a navigation: clear hooks a Turbo\n // cache restore may have snapshotted mid-drag (idempotent reconnect). But a\n // session preserved across an in-page move keeps its hooks.\n if (!this.#pointer) this.element.removeAttribute(\"data-dragging\");\n if (!this.#keyboard) this.element.removeAttribute(\"data-grabbed\");\n // Follow mode owns the element's inline `translate`: re-read a committed\n // offset so reconnects (Turbo cache restore included) keep accumulating from\n // where the element visually sits. A session surviving an in-page move keeps\n // its base — mid-drag inline state must not be committed as a new base.\n if (this.followValue && !this.#pointer && !this.#keyboard) {\n this.#followBase = this.#parseFollowBase();\n }\n // Delegated on the container so dynamically added handles need no per-element\n // data-action.\n this.element.addEventListener(\"pointerdown\", this.#onPointerDown);\n this.element.addEventListener(\"keydown\", this.#onKeydown);\n if (!this.hasHandleTarget) this.#prepareHandle(this.element);\n }\n\n override disconnect(): void {\n this.element.removeEventListener(\"pointerdown\", this.#onPointerDown);\n this.element.removeEventListener(\"keydown\", this.#onKeydown);\n // A mid-session disconnect on a STILL-connected element is ambiguous: an\n // in-page move (a consumer like sortable re-inserts the item mid-grab —\n // Stimulus reconnects the same instance in the same mutation batch, and\n // the session must survive) vs a real detach that keeps the element\n // (`data-controller` removed, Turbo morph, element moved outside the\n // observed root). DetachGate discriminates: the token check is the\n // synchronous fast path (a morph's teardown — and its `cancel` — fires in\n // the same tick), the one-microtask probe cancelled by connect() covers\n // the rest — a detached-in-place controller never leaks its session or\n // document listeners. Probe only while a session needs protecting; an\n // idle controller tears down synchronously either way.\n if (this.#pointer || this.#keyboard) {\n this.#gate.disconnected(this, () => this.#teardown());\n return;\n }\n this.#teardown();\n }\n\n /** Prepares handles added at runtime (touch-action + focusability). */\n handleTargetConnected(handle: HTMLElement): void {\n this.#prepareHandle(handle);\n }\n\n handleTargetDisconnected(handle: HTMLElement): void {\n // A handle REMOVED mid-session can never deliver its keydown (the delegated\n // listener sits on the container it left), so the session would leak — and\n // the one-session guard would then reject every future pointerdown. End it\n // silently: the DOM is changing under the user, same as disconnect. But\n // target callbacks ALSO fire when the CONTROLLER disconnects (Stimulus\n // stops the target observer — including the in-page move a live session\n // must survive): a handle still inside the element was not removed, so\n // that case is left to disconnect()'s deferred probe.\n if (!this.element.contains(handle)) {\n if (this.#pointer?.handle === handle) this.#endPointerSession();\n if (this.#keyboard?.handle === handle) this.#clearKeyboardSession();\n }\n this.#restoreHandle(handle);\n }\n\n /** Re-derives the handles' touch-action when the axis changes. */\n axisValueChanged(): void {\n for (const handle of this.#handles()) {\n if (handle.hasAttribute(PointerDragController.#TOUCH_ACTION_MARKER)) {\n handle.style.touchAction = this.#touchActionForAxis();\n }\n }\n }\n\n /** Cancels any in-flight session when the controller is disabled mid-drag. */\n disabledValueChanged(): void {\n if (!this.disabledValue) return;\n if (this.#pointer?.started || this.#keyboard) {\n this.#dispatchCancel(this.#keyboard ? \"keyboard\" : (this.#pointer?.pointerType ?? \"mouse\"));\n }\n this.#teardownSessions();\n }\n\n /** Arms a pointer drag on a handle; `start` waits for the threshold. */\n readonly #onPointerDown = (event: PointerEvent): void => {\n // One session at a time: ignore a second pointerdown (multi-touch, an\n // errant tap, a grabbed keyboard session) so an in-flight drag is never\n // silently overwritten and its captured pointer orphaned.\n if (this.disabledValue || this.#pointer || this.#keyboard || event.button !== 0) return;\n const handle = this.#handleFor(event.target);\n if (!handle) return;\n\n // Suppress text selection / native image drag; restore the focus the\n // suppressed default would have given, so Escape and the keyboard path\n // stay reachable right after a pointer interaction (WCAG 2.1.1).\n event.preventDefault();\n handle.focus();\n\n this.#pointer = {\n pointerId: event.pointerId,\n handle,\n originX: event.clientX,\n originY: event.clientY,\n started: false,\n dx: 0,\n dy: 0,\n pointerType: this.#pointerTypeOf(event),\n };\n\n // Pointer capture keeps fast drags delivering to the handle even when the\n // pointer strays outside it — but a consumer may re-insert the dragged\n // element mid-drag (sortable's live reorder), which silently releases the\n // capture. Tracking listeners therefore live on the DOCUMENT (captured\n // events bubble there; uncaptured ones fire there anyway), and the\n // AbortController releases them on drag end and disconnect() (Turbo included).\n handle.setPointerCapture(event.pointerId);\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n document.addEventListener(\"pointermove\", this.#onPointerMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", this.#onPointerUp, { signal: abort.signal });\n document.addEventListener(\"pointercancel\", this.#onPointerCancel, { signal: abort.signal });\n };\n\n readonly #onPointerMove = (event: PointerEvent): void => {\n const session = this.#pointer;\n if (!session || event.pointerId !== session.pointerId) return;\n\n const [dx, dy] = this.#filterAxis(\n event.clientX - session.originX,\n event.clientY - session.originY,\n );\n if (!session.started) {\n if (Math.hypot(dx, dy) < this.thresholdValue) return;\n session.started = true;\n this.element.setAttribute(\"data-dragging\", \"true\");\n this.dispatch(\"start\", {\n detail: { x: event.clientX, y: event.clientY, pointerType: session.pointerType },\n });\n }\n\n session.dx = dx;\n session.dy = dy;\n this.#followMove(dx, dy);\n this.dispatch(\"move\", {\n detail: { dx, dy, x: event.clientX, y: event.clientY, pointerType: session.pointerType },\n });\n };\n\n readonly #onPointerUp = (event: PointerEvent): void => {\n const session = this.#pointer;\n if (!session || event.pointerId !== session.pointerId) return;\n const { started, dx, dy, pointerType } = session;\n this.#endPointerSession();\n // Below the threshold the gesture was a plain click, not a drag: stay silent.\n if (!started) return;\n this.#followCommit(dx, dy);\n this.dispatch(\"end\", { detail: { dx, dy, pointerType } });\n };\n\n /** OS gesture / scroll takeover interrupted the drag: cancel, don't drop. */\n readonly #onPointerCancel = (event: PointerEvent): void => {\n const session = this.#pointer;\n if (!session || event.pointerId !== session.pointerId) return;\n const { started, pointerType } = session;\n this.#endPointerSession();\n if (started) this.#dispatchCancel(pointerType);\n };\n\n /** Keyboard alternative: Space/Enter grab & drop, arrows move, Escape cancels. */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (this.disabledValue) return;\n const handle = this.#handleFor(event.target);\n if (!handle) return;\n\n // A descendant widget that already claimed the key (a nested roving list, a\n // segmented field inside the handle) must not ALSO drive the drag —\n // composition depends on this yield. This controller is the \"grabbed drag\n // handle\" that the other yields name as the widget they defer to, so it owes\n // the same courtesy downward.\n if (event.defaultPrevented) return;\n // A chorded arrow belongs to the browser. \"Someone already consumed it\"\n // outranks that, so it is checked after the yield above.\n if (isReservedArrowChord(event)) return;\n\n // Escape cancels whichever session is live (pointer drag or keyboard grab).\n if (event.key === \"Escape\") {\n // A press an inner handler already owned is yielded at the top of this\n // handler. What is checked here is the IME half: a press during a\n // composition steers the conversion, not the drag.\n if (event.isComposing) return;\n if (this.#pointer?.started) {\n const { pointerType } = this.#pointer;\n this.#teardownSessions();\n event.preventDefault();\n this.#dispatchCancel(pointerType);\n } else if (this.#keyboard) {\n this.#teardownSessions();\n event.preventDefault();\n this.#dispatchCancel(\"keyboard\");\n }\n return;\n }\n\n if (event.key === \" \" || event.key === \"Enter\") {\n // A live pointer drag owns the session: pressing Space mid-drag (the\n // pointerdown focused the handle) must not start a parallel grab.\n if (this.#pointer) return;\n event.preventDefault();\n if (this.#keyboard) this.#dropKeyboard();\n else this.#grabKeyboard(handle);\n return;\n }\n\n if (!this.#keyboard) return;\n const step = this.#keyboardDelta(event.key);\n if (!step) return;\n // Consume every arrow while grabbed (even on a locked axis) so the page\n // never scrolls mid-grab; only allowed-axis arrows emit a move.\n event.preventDefault();\n const [dx, dy] = this.#filterAxis(step[0], step[1]);\n if (dx === 0 && dy === 0) return;\n this.#keyboard.dx += dx;\n this.#keyboard.dy += dy;\n this.#followMove(this.#keyboard.dx, this.#keyboard.dy);\n this.dispatch(\"move\", {\n detail: {\n dx: this.#keyboard.dx,\n dy: this.#keyboard.dy,\n x: this.#keyboard.dx,\n y: this.#keyboard.dy,\n pointerType: \"keyboard\",\n },\n });\n };\n\n /** Enters the grabbed mode: `data-grabbed` on element + handle, then `start`. */\n #grabKeyboard(handle: HTMLElement): void {\n this.#keyboard = { handle, dx: 0, dy: 0 };\n this.element.setAttribute(\"data-grabbed\", \"true\");\n handle.setAttribute(\"data-grabbed\", \"true\");\n this.dispatch(\"start\", { detail: { x: 0, y: 0, pointerType: \"keyboard\" } });\n }\n\n #dropKeyboard(): void {\n const session = this.#keyboard;\n if (!session) return;\n this.#clearKeyboardSession();\n this.#followCommit(session.dx, session.dy);\n this.dispatch(\"end\", { detail: { dx: session.dx, dy: session.dy, pointerType: \"keyboard\" } });\n }\n\n /** Maps an arrow key to a raw (unfiltered) `keyboardStep` delta. */\n #keyboardDelta(key: string): [number, number] | null {\n const step = this.keyboardStepValue;\n switch (key) {\n case \"ArrowRight\":\n return [step, 0];\n case \"ArrowLeft\":\n return [-step, 0];\n case \"ArrowDown\":\n return [0, step];\n case \"ArrowUp\":\n return [0, -step];\n default:\n return null;\n }\n }\n\n /** Zeroes the delta on the locked axis (`axis` = x | y | both). */\n #filterAxis(dx: number, dy: number): [number, number] {\n if (this.axisValue === \"x\") return [dx, 0];\n if (this.axisValue === \"y\") return [0, dy];\n return [dx, dy];\n }\n\n /** Releases capture + listeners and clears the pointer session and its hook. */\n #endPointerSession(): void {\n this.#releasePointerCapture();\n this.#pointer = null;\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n this.element.removeAttribute(\"data-dragging\");\n }\n\n /** Releases the pointer capture the active session set (idempotent, safe). */\n #releasePointerCapture(): void {\n const session = this.#pointer;\n if (session?.handle.hasPointerCapture(session.pointerId)) {\n session.handle.releasePointerCapture(session.pointerId);\n }\n }\n\n #clearKeyboardSession(): void {\n this.#keyboard?.handle.removeAttribute(\"data-grabbed\");\n this.#keyboard = null;\n this.element.removeAttribute(\"data-grabbed\");\n }\n\n /**\n * Full teardown for a controller whose element is going away or lost its\n * controller. Into a dead (detached) tree it is silent — consumers restore\n * from their own `connect()`, not from a cancel event. But a detach that\n * KEEPS the element (Turbo morph, `data-controller` removed) leaves the\n * consumers alive and listening: an in-flight session must end in `cancel`\n * there, or a composer like sortable strands its one-at-a-time session\n * bookkeeping forever.\n */\n #teardown(): void {\n // Disarm any probe still queued: without this, an immediate teardown\n // (element removed right after a deferring disconnect) would let the\n // orphaned probe run #teardown a second time — a double cancel.\n this.#gate.cancel();\n const interrupted = this.#pointer?.started\n ? this.#pointer.pointerType\n : this.#keyboard\n ? (\"keyboard\" as const)\n : null;\n this.#teardownSessions();\n for (const handle of this.#handles()) this.#restoreHandle(handle);\n if (interrupted && this.element.isConnected) this.#dispatchCancel(interrupted);\n }\n\n /**\n * Silently tears down whatever session is live (disconnect / disabled /\n * Escape). Composed from the two single-session teardowns so pointer and\n * keyboard cleanup cannot diverge as either path evolves.\n */\n #teardownSessions(): void {\n this.#endPointerSession();\n this.#clearKeyboardSession();\n }\n\n #dispatchCancel(pointerType: DragPointerType): void {\n // Every cancel path funnels here, so follow's snap-back can never be missed\n // (Escape, pointercancel, mid-session disable, detach-that-keeps-the-element).\n this.#followReset();\n this.dispatch(\"cancel\", { detail: { pointerType } });\n }\n\n /** Applies the in-flight offset to the element's `translate` (follow only). */\n #followMove(dx: number, dy: number): void {\n if (!this.followValue) return;\n this.#applyFollow(this.#followBase.x + dx, this.#followBase.y + dy);\n }\n\n /** Folds a drop's deltas into the committed base offset (follow only). */\n #followCommit(dx: number, dy: number): void {\n if (!this.followValue) return;\n this.#followBase = { x: this.#followBase.x + dx, y: this.#followBase.y + dy };\n this.#applyFollow(this.#followBase.x, this.#followBase.y);\n }\n\n /** Snaps back to the committed position (follow only) — the cancel contract. */\n #followReset(): void {\n if (!this.followValue) return;\n this.#applyFollow(this.#followBase.x, this.#followBase.y);\n }\n\n /**\n * Writes the offset as the standalone CSS `translate` property — independent\n * of `transform`, so an authored transform (scale, rotate…) is never\n * clobbered. At the origin the property is removed to keep the DOM clean.\n */\n #applyFollow(x: number, y: number): void {\n if (x === 0 && y === 0) this.element.style.removeProperty(\"translate\");\n else this.element.style.setProperty(\"translate\", `${x}px ${y}px`);\n }\n\n /**\n * Reads a committed follow offset back from the inline `translate`. Only the\n * controller's own `<x>px <y>px` shape is recognized; anything else (an\n * authored percentage, `none`, an empty string) starts the base at 0 — follow\n * mode owns this property, per the class contract.\n */\n #parseFollowBase(): { x: number; y: number } {\n const raw = this.element.style.getPropertyValue(\"translate\").trim();\n const match = /^(-?\\d+(?:\\.\\d+)?)px(?:\\s+(-?\\d+(?:\\.\\d+)?)px)?$/.exec(raw);\n if (!match) return { x: 0, y: 0 };\n return { x: Number(match[1]), y: Number(match[2] ?? \"0\") };\n }\n\n /** The active handles: the `handle` targets, else the element itself. */\n #handles(): HTMLElement[] {\n return this.hasHandleTarget ? this.handleTargets : [this.element];\n }\n\n /** Resolves the handle owning an event target (the handle or a descendant). */\n #handleFor(target: EventTarget | null): HTMLElement | null {\n const node = target as Node | null;\n if (!node) return null;\n return this.#handles().find((handle) => handle === node || handle.contains(node)) ?? null;\n }\n\n #pointerTypeOf(event: PointerEvent): DragPointerType {\n const type = event.pointerType;\n if (type === \"touch\" || type === \"pen\") return type;\n return \"mouse\";\n }\n\n /** `touch-action` that lets the page keep panning on the locked axis only. */\n #touchActionForAxis(): string {\n if (this.axisValue === \"x\") return \"pan-y\";\n if (this.axisValue === \"y\") return \"pan-x\";\n return \"none\";\n }\n\n /**\n * Establishes the handle contract: a `touch-action` derived from `axis`\n * (marker-guarded so an authored inline value wins and reconnects stay\n * idempotent) and focusability for the keyboard path (`tabindex=\"0\"` only when\n * the author supplied none — prefer a real `<button>` handle). Both additions\n * are marker-owned so `#restoreHandle` reverts them symmetrically on teardown.\n */\n #prepareHandle(handle: HTMLElement): void {\n if (\n handle.style.touchAction === \"\" ||\n handle.hasAttribute(PointerDragController.#TOUCH_ACTION_MARKER)\n ) {\n handle.style.touchAction = this.#touchActionForAxis();\n handle.setAttribute(PointerDragController.#TOUCH_ACTION_MARKER, \"true\");\n }\n if (handle.tabIndex < 0 && !handle.hasAttribute(\"tabindex\")) {\n handle.setAttribute(\"tabindex\", \"0\");\n handle.setAttribute(PointerDragController.#TABINDEX_MARKER, \"true\");\n }\n }\n\n /** Reverts the marker-owned touch-action + tabindex (authored values untouched). */\n #restoreHandle(handle: HTMLElement): void {\n if (handle.hasAttribute(PointerDragController.#TOUCH_ACTION_MARKER)) {\n handle.style.touchAction = \"\";\n handle.removeAttribute(PointerDragController.#TOUCH_ACTION_MARKER);\n }\n if (handle.hasAttribute(PointerDragController.#TABINDEX_MARKER)) {\n handle.removeAttribute(PointerDragController.#TABINDEX_MARKER);\n // Only remove what is verifiably still ours and safe to remove: another\n // owner (e.g. a roving list) may have rewritten the value since, and\n // stripping tabindex off the focused element would blur it to <body>\n // (losing the user's place — worse than leaking one tab stop).\n if (handle.getAttribute(\"tabindex\") === \"0\" && document.activeElement !== handle) {\n handle.removeAttribute(\"tabindex\");\n }\n }\n }\n}\n"]}
@@ -32,8 +32,7 @@ import { Controller } from '@hotwired/stimulus';
32
32
  * layer on the shared {@link EscapeLayer} stack; it claims a press only while
33
33
  * focus is inside the controller or fell to the body (a click on non-focusable
34
34
  * panel content), so a press aimed at another layer is never consumed here,
35
- * and one keypress closes exactly one layer (the shared layered-Escape
36
- * contract).
35
+ * and one keypress closes exactly one layer.
37
36
  * - An outside click (anywhere off the controller element) closes without moving
38
37
  * focus. Focus stays at the clicked element, or falls back to the document body
39
38
  * for a non-focusable destination.
@@ -133,7 +133,7 @@ var PopoverController = class _PopoverController extends Controller {
133
133
  /** Starts closed and registers the standing dismissal listeners. */
134
134
  connect() {
135
135
  this.close();
136
- document.addEventListener("click", this.#onOutsideClick);
136
+ document.addEventListener("click", this.#onOutsideClick, true);
137
137
  this.element.addEventListener("focusout", this.#onFocusOut);
138
138
  }
139
139
  /**
@@ -144,7 +144,7 @@ var PopoverController = class _PopoverController extends Controller {
144
144
  */
145
145
  disconnect() {
146
146
  this.#escapeLayer.deactivate();
147
- document.removeEventListener("click", this.#onOutsideClick);
147
+ document.removeEventListener("click", this.#onOutsideClick, true);
148
148
  this.element.removeEventListener("focusout", this.#onFocusOut);
149
149
  this.#stopScrollDismiss?.();
150
150
  this.#stopScrollDismiss = null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/scroll_dismiss.ts","../../src/controllers/popover_controller.ts"],"names":[],"mappings":";;;;;AAgEO,SAAS,uBAAuB,OAAA,EAAiC;AACtE,EAAA,OAAO,MAAM;AACX,IAAA,MAAM,MAAA,GAAS,QAAQ,aAAA,CAAc,aAAA;AACrC,IAAA,OAAO,MAAA,KAAW,QAAQ,MAAA,KAAW,OAAA,CAAQ,cAAc,IAAA,IAAQ,OAAA,CAAQ,SAAS,MAAM,CAAA;AAAA,EAC5F,CAAA;AACF;AAEO,IAAM,WAAA,GAAN,MAAM,YAAA,CAAY;AAAA,EACvB,OAAgB,WAAA,mBAAc,IAAI,OAAA,EAAuC;AAAA,EAEzE,cAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,UAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,OAAA,GAAkC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,QAAA,CAAS,aAAA,GAA0B,QAAA,EAAU,OAAA,EAAmC;AAC9E,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAI,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AACxD,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,QAAA,GAAW,aAAY,eAAA,EAAgB;AACvC,MAAA,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAA,EAAe,QAAQ,CAAA;AACnD,MAAA,aAAA,CAAc,gBAAA,CAAiB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAAA,IAC9D;AACA,IAAA,QAAA,CAAS,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,cAAA,GAAiB,aAAA;AACtB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,SAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,MAAA,IAAU,IAAA;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAA,GAAmB;AACjB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA;AAC7C,MAAA,IAAI,SAAS,CAAA,EAAG,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9C,MAAA,IAAI,QAAA,CAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC/B,QAAA,aAAA,CAAc,mBAAA,CAAoB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAC/D,QAAA,YAAA,CAAY,WAAA,CAAY,OAAO,aAAa,CAAA;AAAA,MAC9C;AAAA,IACF;AACA,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,UAAA,GAAsB;AACxB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,eAAe,OAAO,KAAA;AAC3B,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,CAAC,UAAU,OAAO,KAAA;AACtB,IAAA,OAAO,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA,KAAM,IAAA;AAAA,EACvD;AAAA;AAAA,EAGA,OAAO,eAAA,GAAuC;AAC5C,IAAA,MAAM,QAAA,GAAgC;AAAA,MACpC,OAAO,EAAC;AAAA,MACR,SAAA,EAAW,CAAC,KAAA,KAA+B;AACzC,QAAA,IAAI,MAAM,GAAA,KAAQ,QAAA,IAAY,KAAA,CAAM,gBAAA,IAAoB,MAAM,WAAA,EAAa;AAC3E,QAAA,MAAM,KAAA,GAAQ,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA;AACtD,QAAA,IAAI,CAAC,KAAA,EAAO;AACZ,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,KAAA,CAAM,UAAA,IAAa;AAAA,MACrB;AAAA,KACF;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,cAAc,KAAA,EAA0C;AAC7D,IAAA,KAAA,IAAS,QAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,KAAA,IAAS,GAAG,KAAA,EAAA,EAAS;AACtD,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAK,CAAA;AACzB,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,IAAI,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,SAAQ,EAAG;AACvC,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACF,CAAA;;;AC9IO,SAAS,oBAAA,CAAqB,SAAkB,QAAA,EAAkC;AACvF,EAAA,MAAM,UAAmC,CAAC,GAAG,aAAA,CAAc,OAAO,GAAG,MAAM,CAAA;AAC3E,EAAA,MAAM,OAAA,GAAU,MAAY,QAAA,EAAS;AACrC,EAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,IAAA,MAAA,CAAO,iBAAiB,QAAA,EAAU,OAAA,EAAS,EAAE,OAAA,EAAS,MAAM,CAAA;AAAA,EAC9D;AACA,EAAA,OAAO,MAAM;AACX,IAAA,KAAA,MAAW,MAAA,IAAU,OAAA,EAAS,MAAA,CAAO,mBAAA,CAAoB,UAAU,OAAO,CAAA;AAAA,EAC5E,CAAA;AACF;AAGA,SAAS,cAAc,OAAA,EAA6B;AAClD,EAAA,MAAM,UAAqB,EAAC;AAC5B,EAAA,IAAI,OAAO,OAAA,CAAQ,aAAA;AACnB,EAAA,OAAO,IAAA,EAAM;AACX,IAAA,IAAI,YAAA,CAAa,IAAI,CAAA,EAAG,OAAA,CAAQ,KAAK,IAAI,CAAA;AACzC,IAAA,IAAA,GAAO,IAAA,CAAK,aAAA;AAAA,EACd;AACA,EAAA,OAAO,OAAA;AACT;AAGA,SAAS,aAAa,OAAA,EAA2B;AAC/C,EAAA,MAAM,KAAA,GAAQ,iBAAiB,OAAO,CAAA;AACtC,EAAA,OAAO,sBAAsB,IAAA,CAAK,KAAA,CAAM,WAAW,KAAA,CAAM,SAAA,GAAY,MAAM,SAAS,CAAA;AACtF;;;ACQO,IAAM,iBAAA,GAAN,MAAM,kBAAA,SAA0B,UAAA,CAAwB;AAAA,EAC7D,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,OAAO,CAAA;AAAA,EAC7C,OAAgB,MAAA,GAAS;AAAA,IACvB,aAAA,EAAe,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA;AAAM,GACjD;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,OAAA,EAAS,QAAQ,QAAQ,CAAA;AAAA;AAAA,EAS3C,kBAAA,GAA0C,IAAA;AAAA;AAAA,EAEjC,YAAA,GAAe,IAAI,WAAA,EAAY;AAAA;AAAA,EAGxC,OAAgB,UAAA,GACd,2IAAA;AAAA;AAAA,EAGO,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,KAAA,EAAM;AACX,IAAA,QAAA,CAAS,gBAAA,CAAiB,OAAA,EAAS,IAAA,CAAK,eAAe,CAAA;AACvD,IAAA,IAAA,CAAK,OAAA,CAAQ,gBAAA,CAAiB,UAAA,EAAY,IAAA,CAAK,WAAW,CAAA;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,QAAA,CAAS,mBAAA,CAAoB,OAAA,EAAS,IAAA,CAAK,eAAe,CAAA;AAC1D,IAAA,IAAA,CAAK,OAAA,CAAQ,mBAAA,CAAoB,UAAA,EAAY,IAAA,CAAK,WAAW,CAAA;AAC7D,IAAA,IAAA,CAAK,kBAAA,IAAqB;AAC1B,IAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAAA,EAC5B;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAI,KAAK,OAAA,EAAS;AAChB,MAAA,IAAA,CAAK,KAAA,EAAM;AAAA,IACb,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ;AAAA,EACF;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,IAAA,CAAK,cAAA,IAAkB,IAAA,CAAK,OAAA,EAAS;AAC1C,IAAA,IAAA,CAAK,YAAY,MAAA,GAAS,KAAA;AAC1B,IAAA,IAAI,KAAK,gBAAA,EAAkB,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,iBAAiB,MAAM,CAAA;AAClF,IAAA,IAAA,CAAK,YAAA,CAAa,SAAS,QAAA,EAAU;AAAA,MACnC,SAAA,EAAW,MAAM,IAAA,CAAK,gBAAA,EAAiB;AAAA,MACvC,MAAA,EAAQ,sBAAA,CAAuB,IAAA,CAAK,OAAO;AAAA,KAC5C,CAAA;AACD,IAAA,IAAI,IAAA,CAAK,kBAAA,IAAsB,CAAC,IAAA,CAAK,kBAAA,EAAoB;AAEvD,MAAA,IAAA,CAAK,qBAAqB,oBAAA,CAAqB,IAAA,CAAK,SAAS,MAAM,IAAA,CAAK,OAAO,CAAA;AAAA,IACjF;AACA,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA;AAAA,EAGA,KAAA,GAAc;AAGZ,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAA,CAAK,kBAAA,IAAqB;AAC1B,IAAA,IAAA,CAAK,kBAAA,GAAqB,IAAA;AAC1B,IAAA,IAAI,IAAA,CAAK,cAAA,EAAgB,IAAA,CAAK,WAAA,CAAY,MAAA,GAAS,IAAA;AACnD,IAAA,IAAI,KAAK,gBAAA,EAAkB,IAAA,CAAK,aAAA,CAAc,YAAA,CAAa,iBAAiB,OAAO,CAAA;AAAA,EACrF;AAAA;AAAA,EAGA,WAAA,GAAoB;AAClB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,WAAA,CAAY,aAAA,CAA2B,mBAAkB,UAAU,CAAA;AACtF,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,KAAA,CAAM,KAAA,EAAM;AACZ,MAAA;AAAA,IACF;AACA,IAAA,IAAI,CAAC,KAAK,WAAA,CAAY,YAAA,CAAa,UAAU,CAAA,EAAG,IAAA,CAAK,YAAY,QAAA,GAAW,EAAA;AAC5E,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AAAA,EACzB;AAAA;AAAA,EAGA,gBAAA,GAAyB;AACvB,IAAA,IAAA,CAAK,KAAA,EAAM;AACX,IAAA,IAAI,IAAA,CAAK,gBAAA,EAAkB,IAAA,CAAK,aAAA,CAAc,KAAA,EAAM;AAAA,EACtD;AAAA;AAAA,EAGS,eAAA,GAAkB,CAAC,KAAA,KAA4B;AACtD,IAAA,MAAM,SAAS,KAAA,CAAM,MAAA;AACrB,IAAA,IAAI,IAAA,CAAK,OAAA,IAAW,MAAA,YAAkB,IAAA,IAAQ,CAAC,IAAA,CAAK,OAAA,CAAQ,QAAA,CAAS,MAAM,CAAA,EAAG,IAAA,CAAK,KAAA,EAAM;AAAA,EAC3F,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUS,WAAA,GAAc,CAAC,KAAA,KAA4B;AAClD,IAAA,IAAI,CAAC,KAAK,OAAA,EAAS;AACnB,IAAA,MAAM,OAAO,KAAA,CAAM,aAAA;AACnB,IAAA,IAAI,EAAE,IAAA,YAAgB,IAAA,CAAA,IAAS,KAAK,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,EAAG;AAC5D,IAAA,IAAA,CAAK,KAAA,EAAM;AAAA,EACb,CAAA;AAAA;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,cAAA,IAAkB,CAAC,IAAA,CAAK,WAAA,CAAY,MAAA;AAAA,EAClD;AACF","file":"popover_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest-first half of the\n * shared layered-Escape contract. A keydown that is part of an IME composition\n * (`event.isComposing`) cancels the composition, never a layer, and is ignored\n * here for every layer at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Invokes `onScroll` when the page — or any scrollable ancestor of `element` —\n * scrolls, returning a cleanup that detaches every listener.\n *\n * `scroll` events do not bubble, and a single capture-phase listener on\n * `window`/`document` is unreliable for descendant scroll containers — so this\n * attaches a `scroll` listener directly to each scroll-parent ancestor (walking\n * `parentElement` and checking `overflow`) plus the window. That mirrors the\n * *intent* of `@floating-ui/dom`'s `autoUpdate` scroll tracking; it is a\n * deliberately simple resolver and does not cross shadow roots. This is precisely\n * the case a consumer cannot cover with `data-action` alone, which is why the\n * surface controllers own it.\n *\n * The listeners are `passive` (the handler never calls `preventDefault`), so they\n * never delay scrolling. Pass the surface's anchor/root so the relevant scroll\n * parents are found even when the floating element is portaled elsewhere.\n */\nexport function observeScrollDismiss(element: Element, onScroll: () => void): () => void {\n const targets: Array<Element | Window> = [...scrollParents(element), window];\n const handler = (): void => onScroll();\n for (const target of targets) {\n target.addEventListener(\"scroll\", handler, { passive: true });\n }\n return () => {\n for (const target of targets) target.removeEventListener(\"scroll\", handler);\n };\n}\n\n/** The scrollable ancestors of `element`, nearest first (window excluded). */\nfunction scrollParents(element: Element): Element[] {\n const parents: Element[] = [];\n let node = element.parentElement;\n while (node) {\n if (isScrollable(node)) parents.push(node);\n node = node.parentElement;\n }\n return parents;\n}\n\n/** Whether `element` is a scroll container (any axis `auto`/`scroll`/`overlay`). */\nfunction isScrollable(element: Element): boolean {\n const style = getComputedStyle(element);\n return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { observeScrollDismiss } from \"../utils/scroll_dismiss\";\n\n/**\n * Headless, accessible **non-modal popover** behavior.\n *\n * Markup contract (identifier: `stimeo--popover`):\n * <div data-controller=\"stimeo--popover\">\n * <button data-stimeo--popover-target=\"trigger\"\n * aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"pop\"\n * data-action=\"click->stimeo--popover#toggle\">Edit profile</button>\n * <div id=\"pop\" data-stimeo--popover-target=\"panel\"\n * role=\"dialog\" aria-label=\"Edit profile\" hidden>…</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog** pattern run *non-modally* (no\n * `aria-modal`, no focus trap, no `inert`/scroll lock). The background stays\n * fully interactive; this is the modeless counterpart to `stimeo--dialog`. For\n * a roving `role=\"menu\"` of commands use `stimeo--menu`; for decorative-only\n * text use `stimeo--tooltip`.\n *\n * @remarks\n * Behavior only — static placement is the consumer's CSS, and dynamic\n * edge-collision avoidance is delegated to the opt-in `stimeo-ui/positioning`\n * module (this controller never imports it, preserving the zero-dep core). State\n * is exposed via the trigger's `aria-expanded` and the panel's `hidden`.\n *\n * Behavior provided:\n * - Click the trigger to toggle (`aria-expanded` + `hidden` reflect state).\n * - On open, focus moves to the first focusable element inside the panel (or the\n * panel itself if it has none).\n * - `Escape` closes and restores focus to the trigger. While open the panel is a\n * layer on the shared {@link EscapeLayer} stack; it claims a press only while\n * focus is inside the controller or fell to the body (a click on non-focusable\n * panel content), so a press aimed at another layer is never consumed here,\n * and one keypress closes exactly one layer (the shared layered-Escape\n * contract).\n * - An outside click (anywhere off the controller element) closes without moving\n * focus. Focus stays at the clicked element, or falls back to the document body\n * for a non-focusable destination.\n * - Because it is modeless, focus is *not* trapped: when `Tab` moves focus out of\n * the controller it closes (detected via bubbling `focusout`) without yanking\n * focus back, so forward and reverse traversal preserve their natural destination.\n * A `focusout` with no destination is ignored because it also occurs for clicks\n * on non-focusable content and when the browser window loses focus.\n * - Opt-in **dismiss on scroll** (`closeOnScroll`): while open, scrolling a tracked\n * scroll-parent ancestor (or the window) closes the panel — the Radix / floating-ui\n * convention. Closes without restoring focus (like the modeless `focusout` path)\n * so the close never fights the user's scroll. Off by default.\n */\nexport class PopoverController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\"];\n static override values = {\n closeOnScroll: { type: Boolean, default: false },\n };\n static actions = [\"close\", \"open\", \"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLButtonElement;\n declare readonly panelTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasPanelTarget: boolean;\n declare readonly closeOnScrollValue: boolean;\n\n /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */\n #stopScrollDismiss: (() => void) | null = null;\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Selector for natively focusable elements used to find the first one. */\n static readonly #FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n /** Starts closed and registers the standing dismissal listeners. */\n override connect(): void {\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /**\n * Removes every standing listener registered in {@link connect} plus any active\n * dismiss-on-scroll observers. `removeEventListener` is a no-op when it was\n * never added, so this is safe in the closed state too — no listener outlives\n * the element after a Turbo navigation.\n */\n override disconnect(): void {\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick);\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n }\n\n /** Toggles the popover. Bound via `data-action` (click on the trigger). */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Opens the panel, reflects state, and moves focus inside it. */\n open(): void {\n if (!this.hasPanelTarget || this.#isOpen) return;\n this.panelTarget.hidden = false;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\n claims: claimsWhileFocusWithin(this.element),\n });\n if (this.closeOnScrollValue && !this.#stopScrollDismiss) {\n // Close (no focus restore) so dismissing never fights the user's scroll.\n this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.close());\n }\n this.#focusFirst();\n }\n\n /** Closes the panel and reflects the collapsed state. Bound via `data-action`. */\n close(): void {\n // Release listeners first, unconditionally: a consumer may remove the panel\n // target while it is open, and an early return would leak scroll observers.\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n if (this.hasPanelTarget) this.panelTarget.hidden = true;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n\n /** Moves focus to the first focusable element in the panel, or the panel itself. */\n #focusFirst(): void {\n const first = this.panelTarget.querySelector<HTMLElement>(PopoverController.#FOCUSABLE);\n if (first) {\n first.focus();\n return;\n }\n if (!this.panelTarget.hasAttribute(\"tabindex\")) this.panelTarget.tabIndex = -1;\n this.panelTarget.focus();\n }\n\n /** Closes and restores focus to the trigger for explicit keyboard dismissal. */\n #closeAndRestore(): void {\n this.close();\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n }\n\n /** Closes without moving focus when a click lands outside the controller element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n const target = event.target;\n if (this.#isOpen && target instanceof Node && !this.element.contains(target)) this.close();\n };\n\n /**\n * Closes when focus leaves the controller for a known external destination\n * (e.g. forward Tab past the panel or reverse Tab past the trigger). Focus is\n * not restored — the natural destination is kept, which is the modeless\n * contract. A null/non-Node destination is indeterminate: browsers use it for\n * clicks on non-focusable content and window deactivation, so the later outside\n * click handler decides pointer dismissal instead.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.#isOpen) return;\n const next = event.relatedTarget;\n if (!(next instanceof Node) || this.element.contains(next)) return;\n this.close();\n };\n\n /** Whether the panel is currently visible. */\n get #isOpen(): boolean {\n return this.hasPanelTarget && !this.panelTarget.hidden;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/escape_layer.ts","../../src/utils/scroll_dismiss.ts","../../src/controllers/popover_controller.ts"],"names":[],"mappings":"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","file":"popover_controller.js","sourcesContent":["/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest 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 * Invokes `onScroll` when the page — or any scrollable ancestor of `element` —\n * scrolls, returning a cleanup that detaches every listener.\n *\n * `scroll` events do not bubble, and a single capture-phase listener on\n * `window`/`document` is unreliable for descendant scroll containers — so this\n * attaches a `scroll` listener directly to each scroll-parent ancestor (walking\n * `parentElement` and checking `overflow`) plus the window. That mirrors the\n * *intent* of `@floating-ui/dom`'s `autoUpdate` scroll tracking; it is a\n * deliberately simple resolver and does not cross shadow roots. This is precisely\n * the case a consumer cannot cover with `data-action` alone, which is why the\n * surface controllers own it.\n *\n * The listeners are `passive` (the handler never calls `preventDefault`), so they\n * never delay scrolling. Pass the surface's anchor/root so the relevant scroll\n * parents are found even when the floating element is portaled elsewhere.\n */\nexport function observeScrollDismiss(element: Element, onScroll: () => void): () => void {\n const targets: Array<Element | Window> = [...scrollParents(element), window];\n const handler = (): void => onScroll();\n for (const target of targets) {\n target.addEventListener(\"scroll\", handler, { passive: true });\n }\n return () => {\n for (const target of targets) target.removeEventListener(\"scroll\", handler);\n };\n}\n\n/** The scrollable ancestors of `element`, nearest first (window excluded). */\nfunction scrollParents(element: Element): Element[] {\n const parents: Element[] = [];\n let node = element.parentElement;\n while (node) {\n if (isScrollable(node)) parents.push(node);\n node = node.parentElement;\n }\n return parents;\n}\n\n/** Whether `element` is a scroll container (any axis `auto`/`scroll`/`overlay`). */\nfunction isScrollable(element: Element): boolean {\n const style = getComputedStyle(element);\n return /auto|scroll|overlay/.test(style.overflow + style.overflowX + style.overflowY);\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { observeScrollDismiss } from \"../utils/scroll_dismiss\";\n\n/**\n * Headless, accessible **non-modal popover** behavior.\n *\n * Markup contract (identifier: `stimeo--popover`):\n * <div data-controller=\"stimeo--popover\">\n * <button data-stimeo--popover-target=\"trigger\"\n * aria-haspopup=\"dialog\" aria-expanded=\"false\" aria-controls=\"pop\"\n * data-action=\"click->stimeo--popover#toggle\">Edit profile</button>\n * <div id=\"pop\" data-stimeo--popover-target=\"panel\"\n * role=\"dialog\" aria-label=\"Edit profile\" hidden>…</div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog** pattern run *non-modally* (no\n * `aria-modal`, no focus trap, no `inert`/scroll lock). The background stays\n * fully interactive; this is the modeless counterpart to `stimeo--dialog`. For\n * a roving `role=\"menu\"` of commands use `stimeo--menu`; for decorative-only\n * text use `stimeo--tooltip`.\n *\n * @remarks\n * Behavior only — static placement is the consumer's CSS, and dynamic\n * edge-collision avoidance is delegated to the opt-in `stimeo-ui/positioning`\n * module (this controller never imports it, preserving the zero-dep core). State\n * is exposed via the trigger's `aria-expanded` and the panel's `hidden`.\n *\n * Behavior provided:\n * - Click the trigger to toggle (`aria-expanded` + `hidden` reflect state).\n * - On open, focus moves to the first focusable element inside the panel (or the\n * panel itself if it has none).\n * - `Escape` closes and restores focus to the trigger. While open the panel is a\n * layer on the shared {@link EscapeLayer} stack; it claims a press only while\n * focus is inside the controller or fell to the body (a click on non-focusable\n * panel content), so a press aimed at another layer is never consumed here,\n * and one keypress closes exactly one layer.\n * - An outside click (anywhere off the controller element) closes without moving\n * focus. Focus stays at the clicked element, or falls back to the document body\n * for a non-focusable destination.\n * - Because it is modeless, focus is *not* trapped: when `Tab` moves focus out of\n * the controller it closes (detected via bubbling `focusout`) without yanking\n * focus back, so forward and reverse traversal preserve their natural destination.\n * A `focusout` with no destination is ignored because it also occurs for clicks\n * on non-focusable content and when the browser window loses focus.\n * - Opt-in **dismiss on scroll** (`closeOnScroll`): while open, scrolling a tracked\n * scroll-parent ancestor (or the window) closes the panel — the Radix / floating-ui\n * convention. Closes without restoring focus (like the modeless `focusout` path)\n * so the close never fights the user's scroll. Off by default.\n */\nexport class PopoverController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"panel\"];\n static override values = {\n closeOnScroll: { type: Boolean, default: false },\n };\n static actions = [\"close\", \"open\", \"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLButtonElement;\n declare readonly panelTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasPanelTarget: boolean;\n declare readonly closeOnScrollValue: boolean;\n\n /** Cleanup for the dismiss-on-scroll listeners while open, or `null`. */\n #stopScrollDismiss: (() => void) | null = null;\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Selector for natively focusable elements used to find the first one. */\n static readonly #FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])';\n\n /** Starts closed and registers the standing dismissal listeners. */\n override connect(): void {\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /**\n * Removes every standing listener registered in {@link connect} plus any active\n * dismiss-on-scroll observers. `removeEventListener` is a no-op when it was\n * never added, so this is safe in the closed state too — no listener outlives\n * the element after a Turbo navigation.\n */\n override disconnect(): void {\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n }\n\n /** Toggles the popover. Bound via `data-action` (click on the trigger). */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Opens the panel, reflects state, and moves focus inside it. */\n open(): void {\n if (!this.hasPanelTarget || this.#isOpen) return;\n this.panelTarget.hidden = false;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\n claims: claimsWhileFocusWithin(this.element),\n });\n if (this.closeOnScrollValue && !this.#stopScrollDismiss) {\n // Close (no focus restore) so dismissing never fights the user's scroll.\n this.#stopScrollDismiss = observeScrollDismiss(this.element, () => this.close());\n }\n this.#focusFirst();\n }\n\n /** Closes the panel and reflects the collapsed state. Bound via `data-action`. */\n close(): void {\n // Release listeners first, unconditionally: a consumer may remove the panel\n // target while it is open, and an early return would leak scroll observers.\n this.#escapeLayer.deactivate();\n this.#stopScrollDismiss?.();\n this.#stopScrollDismiss = null;\n if (this.hasPanelTarget) this.panelTarget.hidden = true;\n if (this.hasTriggerTarget) this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n\n /** Moves focus to the first focusable element in the panel, or the panel itself. */\n #focusFirst(): void {\n const first = this.panelTarget.querySelector<HTMLElement>(PopoverController.#FOCUSABLE);\n if (first) {\n first.focus();\n return;\n }\n if (!this.panelTarget.hasAttribute(\"tabindex\")) this.panelTarget.tabIndex = -1;\n this.panelTarget.focus();\n }\n\n /** Closes and restores focus to the trigger for explicit keyboard dismissal. */\n #closeAndRestore(): void {\n this.close();\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n }\n\n /** Closes without moving focus when a click lands outside the controller element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n const target = event.target;\n if (this.#isOpen && target instanceof Node && !this.element.contains(target)) this.close();\n };\n\n /**\n * Closes when focus leaves the controller for a known external destination\n * (e.g. forward Tab past the panel or reverse Tab past the trigger). Focus is\n * not restored — the natural destination is kept, which is the modeless\n * contract. A null/non-Node destination is indeterminate: browsers use it for\n * clicks on non-focusable content and window deactivation, so the later outside\n * click handler decides pointer dismissal instead.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n if (!this.#isOpen) return;\n const next = event.relatedTarget;\n if (!(next instanceof Node) || this.element.contains(next)) return;\n this.close();\n };\n\n /** Whether the panel is currently visible. */\n get #isOpen(): boolean {\n return this.hasPanelTarget && !this.panelTarget.hidden;\n }\n}\n"]}
@@ -27,10 +27,10 @@ import { Controller } from '@hotwired/stimulus';
27
27
  * and the teleported node is restored/removed rather than orphaned under `body`. The
28
28
  * move is idempotent (guarded by the placeholder) and reversed on `disconnect()` (Turbo
29
29
  * navigation included). The "in-page move vs real detach" split on `disconnect()` is
30
- * `DetachGate` (`src/utils/detach_gate.ts`); in the `content` form the source element
31
- * may even leave a scoped application's observed root and the content is still
32
- * restored, while the no-`content` form teleporting itself out of the observed root is
33
- * fire-and-forget by design (see `disconnect()`).
30
+ * {@link DetachGate}; in the `content` form the source element may even leave a
31
+ * scoped application's observed root and the content is still restored, while the
32
+ * no-`content` form teleporting itself out of the observed root is fire-and-forget
33
+ * by design (see `disconnect()`).
34
34
  */
35
35
  declare class PortalController extends Controller<HTMLElement> {
36
36
  #private;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/detach_gate.ts","../../src/controllers/portal_controller.ts"],"names":[],"mappings":"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","file":"portal_controller.js","sourcesContent":["/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer (the `SafeTimeout` / `FocusTrap` lineage —\n * this util owns the discrimination only):\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers (pinned by `pointer_drag_controller.ts`\n * and its teardown contract): teardown into a dead (detached) tree is silent —\n * consumers restore from their own `connect()`; a detach that keeps the tree\n * alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { DetachGate } from \"../utils/detach_gate\";\n\n/**\n * Teleport bookkeeping keyed by the controller element (stable across the\n * connect/disconnect churn some DOM runtimes emit when an observed element is moved).\n * Holds the moved node and its placeholder so any controller instance for that element\n * can finish the teardown.\n */\nconst portalState = new WeakMap<Element, { node: HTMLElement; placeholder: Comment }>();\n\n/**\n * Headless **portal / teleport**: moves an element to another place in the DOM (e.g.\n * directly under `body`) on connect and cleans up on disconnect — the shared substrate\n * for overlays that must escape an ancestor's `overflow: hidden`, `transform`, or\n * stacking context (no APG pattern; a DOM utility). Counterpart to Radix `Portal` /\n * Alpine `x-teleport`.\n *\n * Markup contract (identifier: `stimeo--portal`):\n * <div data-controller=\"stimeo--portal\" data-stimeo--portal-to-value=\"body\">\n * <div data-stimeo--portal-target=\"content\">Teleported content</div>\n * </div>\n *\n * Moves `content` (or `this.element` when no `content` target) into the first element\n * matching `to` (default `body`), `append`ed or `prepend`ed per `position`. A comment\n * placeholder records the original spot so `disconnect()` can return the node there\n * (when `restore`) — or remove it — leaving no orphan behind. The moved node carries\n * `data-portaled`.\n *\n * @remarks\n * Behavior only — no positioning (pair with `stimeo-ui/positioning`) and no focus\n * trapping (pair with a Focus Scope / the overlay). Moving a Stimulus element within\n * the same document does not re-fire connect/disconnect, so the move is safe. For Turbo\n * compatibility prefer the `content`-target form: the controller then stays on the\n * in-place source, so its `disconnect()` fires when the original container is replaced\n * and the teleported node is restored/removed rather than orphaned under `body`. The\n * move is idempotent (guarded by the placeholder) and reversed on `disconnect()` (Turbo\n * navigation included). The \"in-page move vs real detach\" split on `disconnect()` is\n * `DetachGate` (`src/utils/detach_gate.ts`); in the `content` form the source element\n * may even leave a scoped application's observed root and the content is still\n * restored, while the no-`content` form teleporting itself out of the observed root is\n * fire-and-forget by design (see `disconnect()`).\n */\nexport class PortalController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n to: { type: String, default: \"body\" },\n position: { type: String, default: \"append\" },\n restore: { type: Boolean, default: true },\n };\n static events = [\"mount\", \"unmount\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare toValue: string;\n declare positionValue: string;\n declare restoreValue: boolean;\n\n /** Decides whether a `disconnect()` is an in-page move or a real detach. */\n readonly #gate = new DetachGate();\n\n override connect(): void {\n // A reconnect proves an in-page move of the source element: disarm the\n // probe the mid-move disconnect() deferred (see disconnect).\n this.#gate.cancel();\n if (portalState.has(this.element)) return; // already portaled here (idempotent)\n const node = this.hasContentTarget ? this.contentTarget : this.element;\n const destination = this.#destination();\n if (!destination || destination === node || node.contains(destination)) return;\n\n const placeholder = document.createComment(\"stimeo--portal\");\n node.parentNode?.insertBefore(placeholder, node);\n portalState.set(this.element, { node, placeholder });\n\n if (this.positionValue === \"prepend\") {\n destination.prepend(node);\n } else {\n destination.appendChild(node);\n }\n node.setAttribute(\"data-portaled\", \"true\");\n this.dispatch(\"mount\", { detail: { target: destination } });\n }\n\n override disconnect(): void {\n const state = portalState.get(this.element);\n if (!state) return;\n if (state.node === this.element) {\n // No-`content` form: the teleported node IS the controller element, so the\n // teleport itself may exit a scoped application's observed root — the controller\n // doing its job, not a detach. A probe-driven restore would re-enter the root,\n // reconnect, re-teleport and disconnect again, forever. So an ambiguous\n // disconnect (in the document, identifier still listed — also the churn a\n // self-move emits in some runtimes) KEEPS the teleport, and only a definite\n // detach — the element left the DOM, or `data-controller` no longer lists us\n // (a Turbo 8 morph) — restores. The cost, by design: a teleport that left the\n // observed root is fire-and-forget (Stimulus never fires for it again).\n if (!DetachGate.isDetached(this)) return;\n this.#restore(state);\n return;\n }\n // `content` form: the controller element stays put, so its own teleport emits no\n // churn — an ambiguous disconnect means the SOURCE element moved. In-page, the\n // same-batch reconnect cancels the probe and the teleport survives; out of the\n // observed root, no reconnect comes and the probe restores, so the content is\n // never stranded at the destination with a dead owner.\n this.#gate.disconnected(this, () => {\n const current = portalState.get(this.element);\n if (current) this.#restore(current);\n });\n }\n\n /** Returns the node to its placeholder (or removes it) and clears the bookkeeping. */\n #restore(state: { node: HTMLElement; placeholder: Comment }): void {\n portalState.delete(this.element);\n const { node, placeholder } = state;\n\n node.removeAttribute(\"data-portaled\");\n if (this.restoreValue && placeholder.parentNode) {\n placeholder.parentNode.insertBefore(node, placeholder);\n } else {\n node.remove();\n }\n placeholder.remove();\n this.dispatch(\"unmount\", { detail: {} });\n }\n\n /** Resolves the destination for `to`, tolerating an invalid selector. */\n #destination(): Element | null {\n const selector = this.toValue.trim();\n if (!selector) return null;\n try {\n return document.querySelector(selector);\n } catch {\n return null;\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/detach_gate.ts","../../src/controllers/portal_controller.ts"],"names":[],"mappings":"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","file":"portal_controller.js","sourcesContent":["/**\n * Shared \"in-page move vs real detach\" discriminator for `disconnect()`.\n *\n * A mid-session `disconnect()` whose element is still in the document is\n * ambiguous. It can be:\n *\n * - an **in-page move** — a consumer (sortable, a teleport) re-inserted the\n * element; Stimulus reconnects the SAME instance in the same mutation batch\n * and the controller's runtime state must survive;\n * - a **detach that keeps the element** — `data-controller` no longer lists the\n * identifier (a Turbo 8 morph), or the element moved outside the observed\n * root (a scoped `Application.start(root)`, a shadow root); no reconnect will\n * ever come and the controller must tear down or it leaks its sessions,\n * document listeners, and moved nodes.\n *\n * Neither available signal decides this alone. A synchronous `data-controller`\n * token check catches the morph case but misses an observed-root exit (the\n * token stays, the reconnect never comes). Deferring teardown one microtask and\n * letting `connect()` cancel it catches the root exit, but leans on Stimulus\n * reconnecting moved elements within the same mutation batch — internal\n * behavior, not a public contract. {@link DetachGate} composes both: the token\n * check is the synchronous fast path (no probe, no reliance on batching), the\n * microtask probe is the fallback for the genuinely ambiguous remainder.\n *\n * Policy stays with the consumer (the `SafeTimeout` / `FocusTrap` lineage —\n * this util owns the discrimination only):\n *\n * - **Probe** ({@link disconnected}) when the teardown must eventually happen —\n * live drag sessions, document listeners, teleported *content*. Call\n * {@link cancel} from `connect()` (a reconnect means in-page move) and from\n * the head of any direct teardown path (see below).\n * - **Keep** (bare {@link DetachGate.isDetached}) when undoing the ambiguous\n * case would fight the controller's own effect: a controller that moves its\n * OWN element (portal's no-`content` form) exits a scoped observed root as\n * its normal job — a probe-driven teardown would restore the element into\n * the root, reconnect, re-teleport, and disconnect again, forever. Such\n * controllers keep their state on an ambiguous disconnect and tear down only\n * on {@link DetachGate.isDetached}.\n *\n * Event convention for probe consumers: teardown into a dead (detached) tree is\n * silent — consumers restore from their own `connect()`; a detach that keeps the\n * tree alive must end an in-flight session with a `cancel` event, or composing\n * consumers strand their session bookkeeping.\n */\n\n/** The slice of a Stimulus controller the gate inspects. */\nexport interface DetachGateHost {\n readonly element: Element;\n readonly identifier: string;\n}\n\n/**\n * Per-controller-instance gate deciding whether a `disconnect()` is a real\n * detach. Hold one per controller (`#gate = new DetachGate()`) — Stimulus\n * reuses the instance across reconnects, so the pending probe state carries\n * over exactly as the protocol needs.\n *\n * @example\n * ```ts\n * #gate = new DetachGate();\n *\n * connect(): void {\n * this.#gate.cancel(); // a reconnect: the element moved in-page\n * // …\n * }\n *\n * disconnect(): void {\n * this.#gate.disconnected(this, () => this.#teardown());\n * }\n *\n * #teardown(): void {\n * this.#gate.cancel(); // disarm a still-queued probe (double-run guard)\n * // …\n * }\n * ```\n */\nexport class DetachGate {\n /** Set while a probe is queued, waiting for a reconnect to cancel it. */\n #pending = false;\n\n /**\n * True when the disconnect is definitely a real detach — the element left\n * the document, or `data-controller` no longer lists the identifier. False\n * means ambiguous (in-page move or observed-root exit), NOT \"alive\".\n */\n static isDetached(host: DetachGateHost): boolean {\n if (!host.element.isConnected) return true;\n const tokens = (host.element.getAttribute(\"data-controller\") ?? \"\").split(/\\s+/);\n return !tokens.includes(host.identifier);\n }\n\n /**\n * Call from `disconnect()`: runs `teardown` synchronously on a definite\n * detach (fast path), otherwise defers it one microtask — a reconnect\n * ({@link cancel} from `connect()`) keeps the state, no reconnect runs it.\n * One microtask is the whole probe window: Stimulus reconnects a moved\n * element within the same mutation batch, before the checkpoint drains.\n */\n disconnected(host: DetachGateHost, teardown: () => void): void {\n if (DetachGate.isDetached(host)) {\n // Also disarms a probe a previous ambiguous disconnect left queued\n // (defer, element removed, disconnect again) — exactly one teardown.\n this.#pending = false;\n teardown();\n return;\n }\n this.#pending = true;\n queueMicrotask(() => {\n if (!this.#pending) return;\n this.#pending = false;\n teardown();\n });\n }\n\n /**\n * Disarms a pending probe. Call from `connect()` (the reconnect that proves\n * an in-page move) and from the head of any teardown path not routed through\n * {@link disconnected} (disabled-toggle, Escape), so an orphaned probe can\n * never run the teardown a second time.\n */\n cancel(): void {\n this.#pending = false;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { DetachGate } from \"../utils/detach_gate\";\n\n/**\n * Teleport bookkeeping keyed by the controller element (stable across the\n * connect/disconnect churn some DOM runtimes emit when an observed element is moved).\n * Holds the moved node and its placeholder so any controller instance for that element\n * can finish the teardown.\n */\nconst portalState = new WeakMap<Element, { node: HTMLElement; placeholder: Comment }>();\n\n/**\n * Headless **portal / teleport**: moves an element to another place in the DOM (e.g.\n * directly under `body`) on connect and cleans up on disconnect — the shared substrate\n * for overlays that must escape an ancestor's `overflow: hidden`, `transform`, or\n * stacking context (no APG pattern; a DOM utility). Counterpart to Radix `Portal` /\n * Alpine `x-teleport`.\n *\n * Markup contract (identifier: `stimeo--portal`):\n * <div data-controller=\"stimeo--portal\" data-stimeo--portal-to-value=\"body\">\n * <div data-stimeo--portal-target=\"content\">Teleported content</div>\n * </div>\n *\n * Moves `content` (or `this.element` when no `content` target) into the first element\n * matching `to` (default `body`), `append`ed or `prepend`ed per `position`. A comment\n * placeholder records the original spot so `disconnect()` can return the node there\n * (when `restore`) — or remove it — leaving no orphan behind. The moved node carries\n * `data-portaled`.\n *\n * @remarks\n * Behavior only — no positioning (pair with `stimeo-ui/positioning`) and no focus\n * trapping (pair with a Focus Scope / the overlay). Moving a Stimulus element within\n * the same document does not re-fire connect/disconnect, so the move is safe. For Turbo\n * compatibility prefer the `content`-target form: the controller then stays on the\n * in-place source, so its `disconnect()` fires when the original container is replaced\n * and the teleported node is restored/removed rather than orphaned under `body`. The\n * move is idempotent (guarded by the placeholder) and reversed on `disconnect()` (Turbo\n * navigation included). The \"in-page move vs real detach\" split on `disconnect()` is\n * {@link DetachGate}; in the `content` form the source element may even leave a\n * scoped application's observed root and the content is still restored, while the\n * no-`content` form teleporting itself out of the observed root is fire-and-forget\n * by design (see `disconnect()`).\n */\nexport class PortalController extends Controller<HTMLElement> {\n static override targets = [\"content\"];\n static override values = {\n to: { type: String, default: \"body\" },\n position: { type: String, default: \"append\" },\n restore: { type: Boolean, default: true },\n };\n static events = [\"mount\", \"unmount\"] as const;\n\n declare readonly contentTarget: HTMLElement;\n declare readonly hasContentTarget: boolean;\n\n declare toValue: string;\n declare positionValue: string;\n declare restoreValue: boolean;\n\n /** Decides whether a `disconnect()` is an in-page move or a real detach. */\n readonly #gate = new DetachGate();\n\n override connect(): void {\n // A reconnect proves an in-page move of the source element: disarm the\n // probe the mid-move disconnect() deferred (see disconnect).\n this.#gate.cancel();\n if (portalState.has(this.element)) return; // already portaled here (idempotent)\n const node = this.hasContentTarget ? this.contentTarget : this.element;\n const destination = this.#destination();\n if (!destination || destination === node || node.contains(destination)) return;\n\n const placeholder = document.createComment(\"stimeo--portal\");\n node.parentNode?.insertBefore(placeholder, node);\n portalState.set(this.element, { node, placeholder });\n\n if (this.positionValue === \"prepend\") {\n destination.prepend(node);\n } else {\n destination.appendChild(node);\n }\n node.setAttribute(\"data-portaled\", \"true\");\n this.dispatch(\"mount\", { detail: { target: destination } });\n }\n\n override disconnect(): void {\n const state = portalState.get(this.element);\n if (!state) return;\n if (state.node === this.element) {\n // No-`content` form: the teleported node IS the controller element, so the\n // teleport itself may exit a scoped application's observed root — the controller\n // doing its job, not a detach. A probe-driven restore would re-enter the root,\n // reconnect, re-teleport and disconnect again, forever. So an ambiguous\n // disconnect (in the document, identifier still listed — also the churn a\n // self-move emits in some runtimes) KEEPS the teleport, and only a definite\n // detach — the element left the DOM, or `data-controller` no longer lists us\n // (a Turbo 8 morph) — restores. The cost, by design: a teleport that left the\n // observed root is fire-and-forget (Stimulus never fires for it again).\n if (!DetachGate.isDetached(this)) return;\n this.#restore(state);\n return;\n }\n // `content` form: the controller element stays put, so its own teleport emits no\n // churn — an ambiguous disconnect means the SOURCE element moved. In-page, the\n // same-batch reconnect cancels the probe and the teleport survives; out of the\n // observed root, no reconnect comes and the probe restores, so the content is\n // never stranded at the destination with a dead owner.\n this.#gate.disconnected(this, () => {\n const current = portalState.get(this.element);\n if (current) this.#restore(current);\n });\n }\n\n /** Returns the node to its placeholder (or removes it) and clears the bookkeeping. */\n #restore(state: { node: HTMLElement; placeholder: Comment }): void {\n portalState.delete(this.element);\n const { node, placeholder } = state;\n\n node.removeAttribute(\"data-portaled\");\n if (this.restoreValue && placeholder.parentNode) {\n placeholder.parentNode.insertBefore(node, placeholder);\n } else {\n node.remove();\n }\n placeholder.remove();\n this.dispatch(\"unmount\", { detail: {} });\n }\n\n /** Resolves the destination for `to`, tolerating an invalid selector. */\n #destination(): Element | null {\n const selector = this.toValue.trim();\n if (!selector) return null;\n try {\n return document.querySelector(selector);\n } catch {\n return null;\n }\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/coerce.ts","../../src/controllers/progress_controller.ts"],"names":[],"mappings":"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","file":"progress_controller.js","sourcesContent":["/**\n * Numeric coercion shared by the value-bearing status controllers (progress,\n * meter).\n *\n * Stimulus already coerces numeric action params to numbers, but a value can also\n * arrive as a string — via a `*:set` CustomEvent `detail`, or an action param\n * whose attribute does not look numeric. Centralizing the parse keeps `setValue`\n * tolerant of either form while rejecting anything that is not a finite number.\n */\n\n/**\n * Coerces `raw` to a finite number, or returns `null` when it is absent, empty,\n * or not parseable. Empty strings are treated as \"no value\" rather than `0`, so a\n * stray blank param cannot silently reset the value.\n */\nexport function toFiniteNumber(raw: number | string | null | undefined): number | null {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { toFiniteNumber } from \"../utils/coerce\";\n\n/**\n * Event shape `setValue` accepts: an action param `amount` or a `detail.value`.\n * Both are typed `number | string` because, while Stimulus coerces numeric action\n * params to numbers, a `progress:set` CustomEvent (or a non-numeric-looking param)\n * may carry a string; {@link ProgressController.setValue} normalizes either form.\n */\ntype SetValueEvent = Event & {\n params?: { amount?: number | string };\n detail?: { value?: number | string };\n};\n\n/**\n * Headless progress-bar behavior backed by the WAI-ARIA `progressbar` role.\n *\n * Markup contract (identifier: `stimeo--progress`):\n * <div data-controller=\"stimeo--progress\" role=\"progressbar\"\n * aria-label=\"Upload\" aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"0\"\n * data-stimeo--progress-value-value=\"0\"\n * data-action=\"progress:set->stimeo--progress#setValue\">\n * <div data-stimeo--progress-target=\"bar\"></div>\n * </div>\n *\n * This is the *progress over time* role (a task advancing toward completion),\n * deliberately distinct from {@link MeterController}'s point-in-time `meter`. The\n * controller owns value normalization and ARIA value-attribute synchronization;\n * the bar's width/animation is the consumer's, driven off the\n * `--stimeo-progress-ratio` (0–1) custom property and `data-state`.\n *\n * @remarks\n * Behavior only — no styling is emitted. An indeterminate bar drops\n * `aria-valuenow` (per ARIA) so assistive tech announces \"busy\" rather than a\n * stale value; the consumer animates the indeterminate look off\n * `data-state=\"indeterminate\"`.\n */\nexport class ProgressController extends Controller<HTMLElement> {\n static override targets = [\"bar\"];\n static override values = {\n value: { type: Number, default: 0 },\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n indeterminate: { type: Boolean, default: false },\n valueText: { type: String, default: \"\" },\n };\n static actions = [\"setValue\"] as const;\n static events = [\"change\", \"complete\"] as const;\n\n declare readonly barTarget: HTMLElement;\n declare readonly hasBarTarget: boolean;\n\n declare valueValue: number;\n declare minValue: number;\n declare maxValue: number;\n declare indeterminateValue: boolean;\n declare valueTextValue: string;\n\n override connect(): void {\n this.#render();\n }\n\n /**\n * Updates the progress value from an action param (`amount`) or a\n * `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and\n * dispatches `change` (always) plus `complete` when `max` is reached.\n */\n setValue(event: SetValueEvent): void {\n const next = toFiniteNumber(event.params?.amount ?? event.detail?.value);\n if (next === null) return;\n const value = this.#clamp(next);\n this.valueValue = value;\n this.indeterminateValue = false;\n this.#render();\n this.dispatch(\"change\", { detail: { value, ratio: this.#ratio } });\n if (value >= this.maxValue) {\n this.dispatch(\"complete\", { detail: { value } });\n }\n }\n\n /** Re-render when the indeterminate flag is toggled via its data attribute. */\n indeterminateValueChanged(): void {\n // Stimulus fires this once during initialization (before `connect`), where\n // touching the DOM is still safe; guard nothing.\n this.#render();\n }\n\n /** Clamps `raw` into the configured `[min, max]` range. */\n #clamp(raw: number): number {\n return Math.min(this.maxValue, Math.max(this.minValue, raw));\n }\n\n /** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */\n get #ratio(): number {\n const span = this.maxValue - this.minValue;\n if (span <= 0) return 0;\n // Clamp first so an out-of-range value (e.g. an initial value past `max`)\n // can never push the ratio — and thus the bar width / percent — outside 0–1.\n return (this.#clamp(this.valueValue) - this.minValue) / span;\n }\n\n /** Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio. */\n #render(): void {\n this.element.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.element.setAttribute(\"aria-valuemax\", String(this.maxValue));\n\n if (this.indeterminateValue) {\n this.element.removeAttribute(\"aria-valuenow\");\n this.element.removeAttribute(\"aria-valuetext\");\n this.element.style.removeProperty(\"--stimeo-progress-ratio\");\n this.element.setAttribute(\"data-state\", \"indeterminate\");\n return;\n }\n\n const value = this.#clamp(this.valueValue);\n this.element.setAttribute(\"aria-valuenow\", String(value));\n this.element.style.setProperty(\"--stimeo-progress-ratio\", String(this.#ratio));\n this.element.setAttribute(\"data-state\", \"determinate\");\n this.#applyValueText(value);\n }\n\n /**\n * Sets `aria-valuetext` from the consumer-provided template, substituting\n * `{value}` and `{percent}`. Left to the consumer so the human-readable text\n * stays i18n-neutral in the library; cleared when no template is given.\n */\n #applyValueText(value: number): void {\n if (this.valueTextValue.length === 0) {\n this.element.removeAttribute(\"aria-valuetext\");\n return;\n }\n const percent = Math.round(this.#ratio * 100);\n const text = this.valueTextValue\n .replaceAll(\"{value}\", String(value))\n .replaceAll(\"{percent}\", String(percent));\n this.element.setAttribute(\"aria-valuetext\", text);\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/coerce.ts","../../src/controllers/progress_controller.ts"],"names":[],"mappings":";;;;;AAeO,SAAS,eAAe,GAAA,EAAwD;AACrF,EAAA,IAAI,QAAQ,IAAA,IAAQ,GAAA,KAAQ,MAAA,IAAa,GAAA,KAAQ,IAAI,OAAO,IAAA;AAC5D,EAAA,MAAM,QAAQ,OAAO,GAAA,KAAQ,QAAA,GAAW,GAAA,GAAM,OAAO,GAAG,CAAA;AACxD,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,GAAI,KAAA,GAAQ,IAAA;AAC1C;;;ACkBO,IAAM,kBAAA,GAAN,cAAiC,UAAA,CAAwB;AAAA,EAC9D,OAAgB,OAAA,GAAU,CAAC,KAAK,CAAA;AAAA,EAChC,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IAClC,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,CAAA,EAAE;AAAA,IAChC,GAAA,EAAK,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,GAAA,EAAI;AAAA,IAClC,aAAA,EAAe,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA,EAAM;AAAA,IAC/C,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,EAAA;AAAG,GACzC;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,UAAU,CAAA;AAAA,EAC5B,OAAO,MAAA,GAAS,CAAC,QAAA,EAAU,UAAU,CAAA;AAAA,EAW5B,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,SAAS,KAAA,EAA4B;AACnC,IAAA,MAAM,OAAO,cAAA,CAAe,KAAA,CAAM,QAAQ,MAAA,IAAU,KAAA,CAAM,QAAQ,KAAK,CAAA;AACvE,IAAA,IAAI,SAAS,IAAA,EAAM;AACnB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,IAAI,CAAA;AAC9B,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,IAAA,CAAK,kBAAA,GAAqB,KAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,EAAQ;AACb,IAAA,IAAA,CAAK,QAAA,CAAS,QAAA,EAAU,EAAE,MAAA,EAAQ,EAAE,OAAO,KAAA,EAAO,IAAA,CAAK,MAAA,EAAO,EAAG,CAAA;AACjE,IAAA,IAAI,KAAA,IAAS,KAAK,QAAA,EAAU;AAC1B,MAAA,IAAA,CAAK,SAAS,UAAA,EAAY,EAAE,QAAQ,EAAE,KAAA,IAAS,CAAA;AAAA,IACjD;AAAA,EACF;AAAA;AAAA,EAGA,yBAAA,GAAkC;AAGhC,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,OAAO,GAAA,EAAqB;AAC1B,IAAA,OAAO,IAAA,CAAK,IAAI,IAAA,CAAK,QAAA,EAAU,KAAK,GAAA,CAAI,IAAA,CAAK,QAAA,EAAU,GAAG,CAAC,CAAA;AAAA,EAC7D;AAAA;AAAA,EAGA,IAAI,MAAA,GAAiB;AACnB,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,QAAA;AAClC,IAAA,IAAI,IAAA,IAAQ,GAAG,OAAO,CAAA;AAGtB,IAAA,OAAA,CAAQ,KAAK,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA,GAAI,KAAK,QAAA,IAAY,IAAA;AAAA,EAC1D;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,QAAQ,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAC,CAAA;AAChE,IAAA,IAAA,CAAK,QAAQ,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAC,CAAA;AAEhE,IAAA,IAAI,KAAK,kBAAA,EAAoB;AAC3B,MAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,eAAe,CAAA;AAC5C,MAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,gBAAgB,CAAA;AAC7C,MAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,cAAA,CAAe,yBAAyB,CAAA;AAC3D,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,eAAe,CAAA;AACvD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,IAAA,CAAK,UAAU,CAAA;AACzC,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,KAAK,CAAC,CAAA;AACxD,IAAA,IAAA,CAAK,QAAQ,KAAA,CAAM,WAAA,CAAY,2BAA2B,MAAA,CAAO,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7E,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,YAAA,EAAc,aAAa,CAAA;AACrD,IAAA,IAAA,CAAK,gBAAgB,KAAK,CAAA;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,gBAAgB,KAAA,EAAqB;AACnC,IAAA,IAAI,IAAA,CAAK,cAAA,CAAe,MAAA,KAAW,CAAA,EAAG;AACpC,MAAA,IAAA,CAAK,OAAA,CAAQ,gBAAgB,gBAAgB,CAAA;AAC7C,MAAA;AAAA,IACF;AACA,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,SAAS,GAAG,CAAA;AAC5C,IAAA,MAAM,IAAA,GAAO,IAAA,CAAK,cAAA,CACf,UAAA,CAAW,SAAA,EAAW,MAAA,CAAO,KAAK,CAAC,CAAA,CACnC,UAAA,CAAW,WAAA,EAAa,MAAA,CAAO,OAAO,CAAC,CAAA;AAC1C,IAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,gBAAA,EAAkB,IAAI,CAAA;AAAA,EAClD;AACF","file":"progress_controller.js","sourcesContent":["/**\n * Numeric coercion shared by the value-bearing controllers (progress, meter,\n * color-picker).\n *\n * Stimulus already coerces numeric action params to numbers, but a value can also\n * arrive as a string — via a `*:set` CustomEvent `detail`, or an action param\n * whose attribute does not look numeric. Centralizing the parse keeps `setValue`\n * tolerant of either form while rejecting anything that is not a finite number.\n */\n\n/**\n * Coerces `raw` to a finite number, or returns `null` when it is absent, empty,\n * or not parseable. Empty strings are treated as \"no value\" rather than `0`, so a\n * stray blank param cannot silently reset the value.\n */\nexport function toFiniteNumber(raw: number | string | null | undefined): number | null {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { toFiniteNumber } from \"../utils/coerce\";\n\n/**\n * Event shape `setValue` accepts: an action param `amount` or a `detail.value`.\n * Both are typed `number | string` because, while Stimulus coerces numeric action\n * params to numbers, a `progress:set` CustomEvent (or a non-numeric-looking param)\n * may carry a string; {@link ProgressController.setValue} normalizes either form.\n */\ntype SetValueEvent = Event & {\n params?: { amount?: number | string };\n detail?: { value?: number | string };\n};\n\n/**\n * Headless progress-bar behavior backed by the WAI-ARIA `progressbar` role.\n *\n * Markup contract (identifier: `stimeo--progress`):\n * <div data-controller=\"stimeo--progress\" role=\"progressbar\"\n * aria-label=\"Upload\" aria-valuemin=\"0\" aria-valuemax=\"100\" aria-valuenow=\"0\"\n * data-stimeo--progress-value-value=\"0\"\n * data-action=\"progress:set->stimeo--progress#setValue\">\n * <div data-stimeo--progress-target=\"bar\"></div>\n * </div>\n *\n * This is the *progress over time* role (a task advancing toward completion),\n * deliberately distinct from {@link MeterController}'s point-in-time `meter`. The\n * controller owns value normalization and ARIA value-attribute synchronization;\n * the bar's width/animation is the consumer's, driven off the\n * `--stimeo-progress-ratio` (0–1) custom property and `data-state`.\n *\n * @remarks\n * Behavior only — no styling is emitted. An indeterminate bar drops\n * `aria-valuenow` (per ARIA) so assistive tech announces \"busy\" rather than a\n * stale value; the consumer animates the indeterminate look off\n * `data-state=\"indeterminate\"`.\n */\nexport class ProgressController extends Controller<HTMLElement> {\n static override targets = [\"bar\"];\n static override values = {\n value: { type: Number, default: 0 },\n min: { type: Number, default: 0 },\n max: { type: Number, default: 100 },\n indeterminate: { type: Boolean, default: false },\n valueText: { type: String, default: \"\" },\n };\n static actions = [\"setValue\"] as const;\n static events = [\"change\", \"complete\"] as const;\n\n declare readonly barTarget: HTMLElement;\n declare readonly hasBarTarget: boolean;\n\n declare valueValue: number;\n declare minValue: number;\n declare maxValue: number;\n declare indeterminateValue: boolean;\n declare valueTextValue: string;\n\n override connect(): void {\n this.#render();\n }\n\n /**\n * Updates the progress value from an action param (`amount`) or a\n * `detail.value` CustomEvent, normalizes it into range, syncs ARIA, and\n * dispatches `change` (always) plus `complete` when `max` is reached.\n */\n setValue(event: SetValueEvent): void {\n const next = toFiniteNumber(event.params?.amount ?? event.detail?.value);\n if (next === null) return;\n const value = this.#clamp(next);\n this.valueValue = value;\n this.indeterminateValue = false;\n this.#render();\n this.dispatch(\"change\", { detail: { value, ratio: this.#ratio } });\n if (value >= this.maxValue) {\n this.dispatch(\"complete\", { detail: { value } });\n }\n }\n\n /** Re-render when the indeterminate flag is toggled via its data attribute. */\n indeterminateValueChanged(): void {\n // Stimulus fires this once during initialization (before `connect`), where\n // touching the DOM is still safe; guard nothing.\n this.#render();\n }\n\n /** Clamps `raw` into the configured `[min, max]` range. */\n #clamp(raw: number): number {\n return Math.min(this.maxValue, Math.max(this.minValue, raw));\n }\n\n /** Current fraction of the range in `[0, 1]`; `0` when the range is empty. */\n get #ratio(): number {\n const span = this.maxValue - this.minValue;\n if (span <= 0) return 0;\n // Clamp first so an out-of-range value (e.g. an initial value past `max`)\n // can never push the ratio — and thus the bar width / percent — outside 0–1.\n return (this.#clamp(this.valueValue) - this.minValue) / span;\n }\n\n /** Reflects value/range/indeterminate onto ARIA, `data-state`, and the ratio. */\n #render(): void {\n this.element.setAttribute(\"aria-valuemin\", String(this.minValue));\n this.element.setAttribute(\"aria-valuemax\", String(this.maxValue));\n\n if (this.indeterminateValue) {\n this.element.removeAttribute(\"aria-valuenow\");\n this.element.removeAttribute(\"aria-valuetext\");\n this.element.style.removeProperty(\"--stimeo-progress-ratio\");\n this.element.setAttribute(\"data-state\", \"indeterminate\");\n return;\n }\n\n const value = this.#clamp(this.valueValue);\n this.element.setAttribute(\"aria-valuenow\", String(value));\n this.element.style.setProperty(\"--stimeo-progress-ratio\", String(this.#ratio));\n this.element.setAttribute(\"data-state\", \"determinate\");\n this.#applyValueText(value);\n }\n\n /**\n * Sets `aria-valuetext` from the consumer-provided template, substituting\n * `{value}` and `{percent}`. Left to the consumer so the human-readable text\n * stays i18n-neutral in the library; cleared when no template is given.\n */\n #applyValueText(value: number): void {\n if (this.valueTextValue.length === 0) {\n this.element.removeAttribute(\"aria-valuetext\");\n return;\n }\n const percent = Math.round(this.#ratio * 100);\n const text = this.valueTextValue\n .replaceAll(\"{value}\", String(value))\n .replaceAll(\"{percent}\", String(percent));\n this.element.setAttribute(\"aria-valuetext\", text);\n }\n}\n"]}
@@ -2,6 +2,24 @@ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  // src/controllers/radio_group_controller.ts
4
4
 
5
+ // src/utils/logical_scroll.ts
6
+ function isRtl(element) {
7
+ return window.getComputedStyle(element).direction === "rtl";
8
+ }
9
+
10
+ // src/utils/arrow_step.ts
11
+ function logicalArrowStep(key, element) {
12
+ if (key === "ArrowDown") return 1;
13
+ if (key === "ArrowUp") return -1;
14
+ if (key !== "ArrowRight" && key !== "ArrowLeft") return 0;
15
+ const forward = isRtl(element) ? "ArrowLeft" : "ArrowRight";
16
+ return key === forward ? 1 : -1;
17
+ }
18
+ function isReservedArrowChord(event, allow = []) {
19
+ if (!event.key.startsWith("Arrow")) return false;
20
+ return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
21
+ }
22
+
5
23
  // src/utils/roving_tabindex.ts
6
24
  var RovingTabindex = class {
7
25
  /** Returns the current ordered item elements; called on every operation. */
@@ -59,17 +77,18 @@ var RadioGroupController = class extends Controller {
59
77
  }
60
78
  /** Arrow/Home/End/Space navigation with selection-follows-focus. */
61
79
  onKeydown(event) {
80
+ if (event.defaultPrevented) return;
81
+ if (isReservedArrowChord(event)) return;
62
82
  const current = this.radioTargets.indexOf(event.currentTarget);
63
83
  if (current === -1) return;
64
84
  let next = null;
85
+ const step = logicalArrowStep(event.key, this.element);
65
86
  switch (event.key) {
66
87
  case "ArrowDown":
67
88
  case "ArrowRight":
68
- next = rovingMove(current, this.radioTargets.length, 1);
69
- break;
70
89
  case "ArrowUp":
71
90
  case "ArrowLeft":
72
- next = rovingMove(current, this.radioTargets.length, -1);
91
+ next = rovingMove(current, this.radioTargets.length, step);
73
92
  break;
74
93
  case "Home":
75
94
  next = 0;