stimeo-ui 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/CHANGELOG.md +212 -0
  2. package/README.md +120 -0
  3. package/dist/cable/index.js +123 -29
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/accordion_controller.d.ts +30 -3
  6. package/dist/controllers/accordion_controller.js +98 -9
  7. package/dist/controllers/accordion_controller.js.map +1 -1
  8. package/dist/controllers/alert_dialog_controller.d.ts +8 -7
  9. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  10. package/dist/controllers/announcer_controller.d.ts +3 -2
  11. package/dist/controllers/announcer_controller.js +96 -62
  12. package/dist/controllers/announcer_controller.js.map +1 -1
  13. package/dist/controllers/auto_submit_controller.js +83 -7
  14. package/dist/controllers/auto_submit_controller.js.map +1 -1
  15. package/dist/controllers/avatar_controller.js +1 -1
  16. package/dist/controllers/avatar_controller.js.map +1 -1
  17. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  18. package/dist/controllers/breadcrumb_controller.js +38 -11
  19. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  20. package/dist/controllers/bulk_select_controller.d.ts +4 -4
  21. package/dist/controllers/bulk_select_controller.js +7 -6
  22. package/dist/controllers/bulk_select_controller.js.map +1 -1
  23. package/dist/controllers/calendar_controller.d.ts +101 -23
  24. package/dist/controllers/calendar_controller.js +340 -123
  25. package/dist/controllers/calendar_controller.js.map +1 -1
  26. package/dist/controllers/carousel_controller.d.ts +67 -22
  27. package/dist/controllers/carousel_controller.js +263 -38
  28. package/dist/controllers/carousel_controller.js.map +1 -1
  29. package/dist/controllers/character_counter_controller.d.ts +1 -1
  30. package/dist/controllers/character_counter_controller.js +40 -2
  31. package/dist/controllers/character_counter_controller.js.map +1 -1
  32. package/dist/controllers/checkbox_controller.js +81 -12
  33. package/dist/controllers/checkbox_controller.js.map +1 -1
  34. package/dist/controllers/clipboard_controller.d.ts +28 -2
  35. package/dist/controllers/clipboard_controller.js +63 -5
  36. package/dist/controllers/clipboard_controller.js.map +1 -1
  37. package/dist/controllers/collapsible_controller.d.ts +25 -1
  38. package/dist/controllers/collapsible_controller.js +99 -14
  39. package/dist/controllers/collapsible_controller.js.map +1 -1
  40. package/dist/controllers/color_picker_controller.d.ts +29 -8
  41. package/dist/controllers/color_picker_controller.js +80 -34
  42. package/dist/controllers/color_picker_controller.js.map +1 -1
  43. package/dist/controllers/combobox_controller.d.ts +10 -1
  44. package/dist/controllers/combobox_controller.js +106 -16
  45. package/dist/controllers/combobox_controller.js.map +1 -1
  46. package/dist/controllers/command_palette_controller.d.ts +3 -3
  47. package/dist/controllers/command_palette_controller.js +35 -3
  48. package/dist/controllers/command_palette_controller.js.map +1 -1
  49. package/dist/controllers/conditional_fields_controller.js +85 -17
  50. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  51. package/dist/controllers/confirm_controller.js +3 -0
  52. package/dist/controllers/confirm_controller.js.map +1 -1
  53. package/dist/controllers/context_menu_controller.d.ts +6 -0
  54. package/dist/controllers/context_menu_controller.js +32 -12
  55. package/dist/controllers/context_menu_controller.js.map +1 -1
  56. package/dist/controllers/count_up_controller.js.map +1 -1
  57. package/dist/controllers/countdown_controller.d.ts +30 -1
  58. package/dist/controllers/countdown_controller.js +129 -26
  59. package/dist/controllers/countdown_controller.js.map +1 -1
  60. package/dist/controllers/currency_input_controller.d.ts +77 -16
  61. package/dist/controllers/currency_input_controller.js +221 -67
  62. package/dist/controllers/currency_input_controller.js.map +1 -1
  63. package/dist/controllers/data_grid_controller.d.ts +63 -18
  64. package/dist/controllers/data_grid_controller.js +195 -29
  65. package/dist/controllers/data_grid_controller.js.map +1 -1
  66. package/dist/controllers/date_range_picker_controller.d.ts +41 -6
  67. package/dist/controllers/date_range_picker_controller.js +151 -30
  68. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  69. package/dist/controllers/dialog_controller.d.ts +10 -3
  70. package/dist/controllers/dialog_controller.js +35 -8
  71. package/dist/controllers/dialog_controller.js.map +1 -1
  72. package/dist/controllers/direct_upload_controller.js +22 -4
  73. package/dist/controllers/direct_upload_controller.js.map +1 -1
  74. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  75. package/dist/controllers/dirty_form_controller.js +46 -13
  76. package/dist/controllers/dirty_form_controller.js.map +1 -1
  77. package/dist/controllers/dismissible_controller.js +1 -0
  78. package/dist/controllers/dismissible_controller.js.map +1 -1
  79. package/dist/controllers/drawer_controller.d.ts +18 -10
  80. package/dist/controllers/drawer_controller.js +54 -19
  81. package/dist/controllers/drawer_controller.js.map +1 -1
  82. package/dist/controllers/dropdown_controller.d.ts +9 -3
  83. package/dist/controllers/dropdown_controller.js +36 -9
  84. package/dist/controllers/dropdown_controller.js.map +1 -1
  85. package/dist/controllers/editable_controller.js +34 -0
  86. package/dist/controllers/editable_controller.js.map +1 -1
  87. package/dist/controllers/file_dropzone_controller.js +144 -51
  88. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  89. package/dist/controllers/filter_controller.d.ts +10 -4
  90. package/dist/controllers/filter_controller.js +20 -6
  91. package/dist/controllers/filter_controller.js.map +1 -1
  92. package/dist/controllers/flash_controller.d.ts +26 -6
  93. package/dist/controllers/flash_controller.js +432 -71
  94. package/dist/controllers/flash_controller.js.map +1 -1
  95. package/dist/controllers/focus_controller.js +1 -0
  96. package/dist/controllers/focus_controller.js.map +1 -1
  97. package/dist/controllers/form_field_controller.js +7 -5
  98. package/dist/controllers/form_field_controller.js.map +1 -1
  99. package/dist/controllers/form_validation_controller.js +19 -13
  100. package/dist/controllers/form_validation_controller.js.map +1 -1
  101. package/dist/controllers/frame_loading_controller.js +45 -8
  102. package/dist/controllers/frame_loading_controller.js.map +1 -1
  103. package/dist/controllers/highlight_controller.js +82 -25
  104. package/dist/controllers/highlight_controller.js.map +1 -1
  105. package/dist/controllers/hover_card_controller.d.ts +10 -2
  106. package/dist/controllers/hover_card_controller.js +40 -14
  107. package/dist/controllers/hover_card_controller.js.map +1 -1
  108. package/dist/controllers/idle_controller.d.ts +16 -3
  109. package/dist/controllers/idle_controller.js +90 -5
  110. package/dist/controllers/idle_controller.js.map +1 -1
  111. package/dist/controllers/input_mask_controller.d.ts +5 -2
  112. package/dist/controllers/input_mask_controller.js +65 -9
  113. package/dist/controllers/input_mask_controller.js.map +1 -1
  114. package/dist/controllers/intersection_controller.js +3 -0
  115. package/dist/controllers/intersection_controller.js.map +1 -1
  116. package/dist/controllers/lazy_frame_controller.js +11 -2
  117. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  118. package/dist/controllers/listbox_controller.d.ts +50 -8
  119. package/dist/controllers/listbox_controller.js +203 -45
  120. package/dist/controllers/listbox_controller.js.map +1 -1
  121. package/dist/controllers/local_time_controller.js +10 -5
  122. package/dist/controllers/local_time_controller.js.map +1 -1
  123. package/dist/controllers/masonry_controller.d.ts +3 -3
  124. package/dist/controllers/masonry_controller.js +31 -15
  125. package/dist/controllers/masonry_controller.js.map +1 -1
  126. package/dist/controllers/menu_controller.d.ts +9 -3
  127. package/dist/controllers/menu_controller.js +45 -16
  128. package/dist/controllers/menu_controller.js.map +1 -1
  129. package/dist/controllers/menubar_controller.d.ts +11 -0
  130. package/dist/controllers/menubar_controller.js +58 -24
  131. package/dist/controllers/menubar_controller.js.map +1 -1
  132. package/dist/controllers/meter_controller.js +9 -5
  133. package/dist/controllers/meter_controller.js.map +1 -1
  134. package/dist/controllers/multi_select_controller.d.ts +18 -3
  135. package/dist/controllers/multi_select_controller.js +278 -104
  136. package/dist/controllers/multi_select_controller.js.map +1 -1
  137. package/dist/controllers/navigation_menu_controller.d.ts +11 -0
  138. package/dist/controllers/navigation_menu_controller.js +48 -15
  139. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  140. package/dist/controllers/nested_form_controller.js +37 -8
  141. package/dist/controllers/nested_form_controller.js.map +1 -1
  142. package/dist/controllers/network_status_controller.js +9 -1
  143. package/dist/controllers/network_status_controller.js.map +1 -1
  144. package/dist/controllers/number_input_controller.d.ts +36 -8
  145. package/dist/controllers/number_input_controller.js +124 -21
  146. package/dist/controllers/number_input_controller.js.map +1 -1
  147. package/dist/controllers/optimistic_controller.js +42 -5
  148. package/dist/controllers/optimistic_controller.js.map +1 -1
  149. package/dist/controllers/otp_controller.d.ts +22 -7
  150. package/dist/controllers/otp_controller.js +198 -55
  151. package/dist/controllers/otp_controller.js.map +1 -1
  152. package/dist/controllers/overflow_indicator_controller.d.ts +8 -12
  153. package/dist/controllers/overflow_indicator_controller.js +115 -21
  154. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  155. package/dist/controllers/overflow_menu_controller.d.ts +26 -6
  156. package/dist/controllers/overflow_menu_controller.js +141 -44
  157. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  158. package/dist/controllers/pagination_controller.d.ts +19 -11
  159. package/dist/controllers/pagination_controller.js +74 -28
  160. package/dist/controllers/pagination_controller.js.map +1 -1
  161. package/dist/controllers/password_reveal_controller.d.ts +15 -1
  162. package/dist/controllers/password_reveal_controller.js +59 -2
  163. package/dist/controllers/password_reveal_controller.js.map +1 -1
  164. package/dist/controllers/persist_controller.js +30 -8
  165. package/dist/controllers/persist_controller.js.map +1 -1
  166. package/dist/controllers/pointer_drag_controller.js +131 -52
  167. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  168. package/dist/controllers/popover_controller.d.ts +9 -3
  169. package/dist/controllers/popover_controller.js +45 -11
  170. package/dist/controllers/popover_controller.js.map +1 -1
  171. package/dist/controllers/portal_controller.d.ts +1 -1
  172. package/dist/controllers/portal_controller.js +6 -2
  173. package/dist/controllers/portal_controller.js.map +1 -1
  174. package/dist/controllers/preview_guard_controller.js +16 -1
  175. package/dist/controllers/preview_guard_controller.js.map +1 -1
  176. package/dist/controllers/progress_controller.js +8 -4
  177. package/dist/controllers/progress_controller.js.map +1 -1
  178. package/dist/controllers/radio_group_controller.d.ts +6 -4
  179. package/dist/controllers/radio_group_controller.js +42 -17
  180. package/dist/controllers/radio_group_controller.js.map +1 -1
  181. package/dist/controllers/range_slider_controller.d.ts +49 -1
  182. package/dist/controllers/range_slider_controller.js +88 -42
  183. package/dist/controllers/range_slider_controller.js.map +1 -1
  184. package/dist/controllers/rating_controller.d.ts +14 -3
  185. package/dist/controllers/rating_controller.js +39 -15
  186. package/dist/controllers/rating_controller.js.map +1 -1
  187. package/dist/controllers/read_more_controller.d.ts +24 -2
  188. package/dist/controllers/read_more_controller.js +100 -7
  189. package/dist/controllers/read_more_controller.js.map +1 -1
  190. package/dist/controllers/reading_progress_controller.js +65 -19
  191. package/dist/controllers/reading_progress_controller.js.map +1 -1
  192. package/dist/controllers/relative_time_controller.js +10 -5
  193. package/dist/controllers/relative_time_controller.js.map +1 -1
  194. package/dist/controllers/resizable_controller.d.ts +16 -2
  195. package/dist/controllers/resizable_controller.js +82 -22
  196. package/dist/controllers/resizable_controller.js.map +1 -1
  197. package/dist/controllers/scroll_area_controller.js +75 -27
  198. package/dist/controllers/scroll_area_controller.js.map +1 -1
  199. package/dist/controllers/scroll_restore_controller.js +37 -16
  200. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  201. package/dist/controllers/scroll_visibility_controller.js +49 -30
  202. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  203. package/dist/controllers/scrollspy_controller.d.ts +3 -2
  204. package/dist/controllers/scrollspy_controller.js +71 -26
  205. package/dist/controllers/scrollspy_controller.js.map +1 -1
  206. package/dist/controllers/separator_controller.d.ts +41 -14
  207. package/dist/controllers/separator_controller.js +66 -37
  208. package/dist/controllers/separator_controller.js.map +1 -1
  209. package/dist/controllers/sidebar_controller.d.ts +20 -3
  210. package/dist/controllers/sidebar_controller.js +77 -18
  211. package/dist/controllers/sidebar_controller.js.map +1 -1
  212. package/dist/controllers/skeleton_controller.js +6 -1
  213. package/dist/controllers/skeleton_controller.js.map +1 -1
  214. package/dist/controllers/slider_controller.d.ts +45 -7
  215. package/dist/controllers/slider_controller.js +82 -47
  216. package/dist/controllers/slider_controller.js.map +1 -1
  217. package/dist/controllers/smart_sticky_header_controller.js +60 -26
  218. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  219. package/dist/controllers/sortable_controller.js +17 -2
  220. package/dist/controllers/sortable_controller.js.map +1 -1
  221. package/dist/controllers/spinner_controller.js +10 -2
  222. package/dist/controllers/spinner_controller.js.map +1 -1
  223. package/dist/controllers/step_indicator_controller.d.ts +19 -17
  224. package/dist/controllers/step_indicator_controller.js +18 -17
  225. package/dist/controllers/step_indicator_controller.js.map +1 -1
  226. package/dist/controllers/stepper_controller.d.ts +34 -9
  227. package/dist/controllers/stepper_controller.js +101 -19
  228. package/dist/controllers/stepper_controller.js.map +1 -1
  229. package/dist/controllers/stick_to_bottom_controller.d.ts +19 -0
  230. package/dist/controllers/stick_to_bottom_controller.js +104 -8
  231. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  232. package/dist/controllers/submit_once_controller.d.ts +3 -2
  233. package/dist/controllers/submit_once_controller.js +45 -9
  234. package/dist/controllers/submit_once_controller.js.map +1 -1
  235. package/dist/controllers/switch_controller.d.ts +29 -7
  236. package/dist/controllers/switch_controller.js +101 -10
  237. package/dist/controllers/switch_controller.js.map +1 -1
  238. package/dist/controllers/tabs_controller.d.ts +12 -0
  239. package/dist/controllers/tabs_controller.js +21 -2
  240. package/dist/controllers/tabs_controller.js.map +1 -1
  241. package/dist/controllers/tags_input_controller.d.ts +15 -3
  242. package/dist/controllers/tags_input_controller.js +209 -59
  243. package/dist/controllers/tags_input_controller.js.map +1 -1
  244. package/dist/controllers/textarea_autosize_controller.js +29 -3
  245. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  246. package/dist/controllers/theme_controller.d.ts +20 -3
  247. package/dist/controllers/theme_controller.js +64 -14
  248. package/dist/controllers/theme_controller.js.map +1 -1
  249. package/dist/controllers/time_picker_controller.js +23 -8
  250. package/dist/controllers/time_picker_controller.js.map +1 -1
  251. package/dist/controllers/toast_controller.d.ts +55 -15
  252. package/dist/controllers/toast_controller.js +451 -105
  253. package/dist/controllers/toast_controller.js.map +1 -1
  254. package/dist/controllers/toggle_group_controller.d.ts +49 -7
  255. package/dist/controllers/toggle_group_controller.js +159 -23
  256. package/dist/controllers/toggle_group_controller.js.map +1 -1
  257. package/dist/controllers/toolbar_controller.js +32 -0
  258. package/dist/controllers/toolbar_controller.js.map +1 -1
  259. package/dist/controllers/tooltip_controller.d.ts +8 -0
  260. package/dist/controllers/tooltip_controller.js +39 -13
  261. package/dist/controllers/tooltip_controller.js.map +1 -1
  262. package/dist/controllers/transition_controller.js +4 -0
  263. package/dist/controllers/transition_controller.js.map +1 -1
  264. package/dist/controllers/tree_view_controller.d.ts +39 -8
  265. package/dist/controllers/tree_view_controller.js +169 -16
  266. package/dist/controllers/tree_view_controller.js.map +1 -1
  267. package/dist/index.d.ts +28 -1
  268. package/dist/index.js +5002 -1911
  269. package/dist/index.js.map +1 -1
  270. package/dist/inspector/cli.d.ts +72 -6
  271. package/dist/inspector/cli.js +262 -51
  272. package/dist/inspector/cli.js.map +1 -1
  273. package/dist/inspector/cli_bin.js +309 -51
  274. package/dist/inspector/cli_bin.js.map +1 -1
  275. package/dist/inspector/examples.json +40 -40
  276. package/dist/inspector/manifest.json +529 -61
  277. package/dist/positioning/index.js +2 -0
  278. package/dist/positioning/index.js.map +1 -1
  279. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/aria_ids.ts","../../src/utils/escape_layer.ts","../../src/controllers/dropdown_controller.ts"],"names":[],"mappings":"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","file":"dropdown_controller.js","sourcesContent":["/**\n * Minimal id primitives for wiring ARIA relationships.\n *\n * Accessible relationships such as `aria-describedby`, `aria-errormessage`,\n * `aria-labelledby`, and `aria-activedescendant` reference their targets by DOM\n * `id`. When a controller must establish such a relationship but the consumer's\n * markup left the target without an `id`, it needs to mint one that is stable for\n * the page's lifetime and guaranteed not to collide with another generated id.\n *\n * These helpers own *only* id generation/assignment. The linking policy (which\n * attribute, what order, when to add or remove it) stays in each controller —\n * this deliberately avoids a premature generic \"ARIA linker\" abstraction.\n */\n\n/**\n * Monotonic counter backing {@link uniqueId}. Module-scoped so every generated\n * id is unique across all controller instances sharing this module in a document.\n */\nlet counter = 0;\n\n/**\n * Returns a unique, DOM-id-safe string of the form `` `${prefix}-${n}` `` where\n * `n` increases on each call.\n *\n * The counter alone guarantees uniqueness against other *generated* ids, but a\n * consumer-authored element could already own `` `${prefix}-${n}` ``. When a\n * `document` is available the candidate is therefore advanced until no element\n * with that id exists, so a generated id never collides with author markup\n * either — the minimal \"id registry\" guarantee these helpers exist to provide.\n *\n * @param prefix - Leading segment of the id (e.g. a controller's identifier).\n * Defaults to `\"stimeo\"`.\n */\nexport function uniqueId(prefix = \"stimeo\"): string {\n let candidate: string;\n do {\n counter += 1;\n candidate = `${prefix}-${counter}`;\n } while (typeof document !== \"undefined\" && document.getElementById(candidate) !== null);\n return candidate;\n}\n\n/**\n * Returns `element`'s existing `id`, or assigns it a freshly generated\n * {@link uniqueId} (with the given `prefix`) and returns that. Idempotent: an\n * element that already has an id keeps it, so consumer-authored ids are never\n * overwritten.\n *\n * @param element - The element to read or stamp an `id` onto.\n * @param prefix - Prefix forwarded to {@link uniqueId} when one must be generated.\n * @returns The element's id (existing or newly assigned).\n */\nexport function ensureId(element: Element, prefix = \"stimeo\"): string {\n if (element.id) return element.id;\n const id = uniqueId(prefix);\n element.id = id;\n return id;\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { ensureId } from \"../utils/aria_ids\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\n\n/**\n * Headless, accessible dropdown menu behavior.\n *\n * Markup contract (identifier: `stimeo--dropdown`):\n * <div data-controller=\"stimeo--dropdown\">\n * <button data-stimeo--dropdown-target=\"trigger\"\n * data-action=\"click->stimeo--dropdown#toggle\">Menu</button>\n * <div data-stimeo--dropdown-target=\"menu\">...</div>\n * </div>\n *\n * This is a **disclosure** pattern (WAI-ARIA APG): a button toggles the\n * visibility of an adjacent region. It is intentionally *not* a full APG\n * \"menu\" widget — there is no roving-tabindex arrow-key navigation.\n *\n * @remarks\n * The library owns behavior only (ARIA state, keyboard, focus, outside-click).\n * Visual styling is left entirely to the consumer's CSS.\n *\n * Behavior provided:\n * - The trigger is associated with the menu through `aria-controls`.\n * - Click the trigger to toggle the menu (`aria-expanded` + `hidden` reflect state).\n * - `Escape` closes the menu and returns focus to the trigger. While open the\n * menu is a layer on the shared `EscapeLayer` stack; it claims a press\n * only while focus is inside the controller or fell to the body (a click on\n * non-focusable menu content), so a press aimed at another layer never closes\n * the menu or steals focus, and one keypress closes exactly one layer.\n * - A click outside the controller element closes the menu.\n */\nexport class DropdownController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"menu\"];\n static actions = [\"close\", \"open\", \"toggle\"] as const;\n\n declare readonly triggerTarget: HTMLButtonElement;\n declare readonly menuTarget: HTMLElement;\n declare readonly hasMenuTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Closes the menu when a click lands outside the controller's element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.element.contains(event.target as Node)) {\n this.close();\n }\n };\n\n /** Starts in the closed state and registers outside-click handling. */\n override connect(): void {\n this.#associateTriggerWithMenu();\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n }\n\n /** Removes the listeners registered in {@link connect}. */\n override disconnect(): void {\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n }\n\n /** Toggles the menu between open and closed. Bound via `data-action`. */\n toggle(): void {\n if (this.#isOpen) {\n this.close();\n } else {\n this.open();\n }\n }\n\n /** Reveals the menu and reflects the open state on the trigger. */\n open(): void {\n if (!this.hasMenuTarget) return;\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\n claims: claimsWhileFocusWithin(this.element),\n });\n this.menuTarget.hidden = false;\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n }\n }\n\n /** Hides the menu and reflects the closed state on the trigger. */\n close(): void {\n this.#escapeLayer.deactivate();\n if (!this.hasMenuTarget) return;\n this.menuTarget.hidden = true;\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n }\n\n /** Closes and restores focus to the trigger (the keyboard-dismissal path). */\n #closeAndRestore(): void {\n this.close();\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n }\n\n /** Whether the menu is currently visible. */\n get #isOpen(): boolean {\n return this.hasMenuTarget && !this.menuTarget.hidden;\n }\n\n /**\n * Associates the disclosure trigger and controlled region without clobbering\n * authored markup.\n */\n #associateTriggerWithMenu(): void {\n if (!this.hasTriggerTarget || !this.hasMenuTarget) return;\n if (this.triggerTarget.hasAttribute(\"aria-controls\")) return;\n\n this.triggerTarget.setAttribute(\n \"aria-controls\",\n ensureId(this.menuTarget, \"stimeo--dropdown-menu\"),\n );\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/aria_ids.ts","../../src/utils/escape_layer.ts","../../src/utils/state_reason.ts","../../src/controllers/dropdown_controller.ts"],"names":[],"mappings":"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","file":"dropdown_controller.js","sourcesContent":["/**\n * Minimal id primitives for wiring ARIA relationships.\n *\n * Accessible relationships such as `aria-describedby`, `aria-errormessage`,\n * `aria-labelledby`, and `aria-activedescendant` reference their targets by DOM\n * `id`. When a controller must establish such a relationship but the consumer's\n * markup left the target without an `id`, it needs to mint one that is stable for\n * the page's lifetime and guaranteed not to collide with another generated id.\n *\n * These helpers own *only* id generation/assignment. The linking policy (which\n * attribute, what order, when to add or remove it) stays in each controller —\n * this deliberately avoids a premature generic \"ARIA linker\" abstraction.\n */\n\n/**\n * Monotonic counter backing {@link uniqueId}. Module-scoped so every generated\n * id is unique across all controller instances sharing this module in a document.\n */\nlet counter = 0;\n\n/**\n * Returns a unique, DOM-id-safe string of the form `` `${prefix}-${n}` `` where\n * `n` increases on each call.\n *\n * The counter alone guarantees uniqueness against other *generated* ids, but a\n * consumer-authored element could already own `` `${prefix}-${n}` ``. When a\n * `document` is available the candidate is therefore advanced until no element\n * with that id exists, so a generated id never collides with author markup\n * either — the minimal \"id registry\" guarantee these helpers exist to provide.\n *\n * @param prefix - Leading segment of the id (e.g. a controller's identifier).\n * Defaults to `\"stimeo\"`.\n */\nexport function uniqueId(prefix = \"stimeo\"): string {\n let candidate: string;\n do {\n counter += 1;\n candidate = `${prefix}-${counter}`;\n } while (typeof document !== \"undefined\" && document.getElementById(candidate) !== null);\n return candidate;\n}\n\n/**\n * Returns `element`'s existing `id`, or assigns it a freshly generated\n * {@link uniqueId} (with the given `prefix`) and returns that. Idempotent: an\n * element that already has an id keeps it, so consumer-authored ids are never\n * overwritten.\n *\n * @param element - The element to read or stamp an `id` onto.\n * @param prefix - Prefix forwarded to {@link uniqueId} when one must be generated.\n * @returns The element's id (existing or newly assigned).\n */\nexport function ensureId(element: Element, prefix = \"stimeo\"): string {\n if (element.id) return element.id;\n const id = uniqueId(prefix);\n element.id = id;\n return id;\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Why a component's public open/closed state moved, carried as `detail.reason`\n * on the state event that reports the move.\n *\n * The attribute a component publishes (`aria-expanded`, `hidden`, `data-state`)\n * only says *what* the state is now. A subscriber that mirrors the state\n * elsewhere, or reports it, needs *why*: closing on `Escape` and closing because\n * the consumer called the action are the same attribute write and different\n * events to the page around it.\n *\n * `\"api\"` is the one every subscriber has to look at. A close the consumer asked\n * for arrives back at their own listener, so a listener that closes something\n * else on `close` loops unless it ignores its own calls.\n *\n * | Reason | The state moved because |\n * | --- | --- |\n * | `\"user\"` | a control of this component was operated |\n * | `\"select\"` | an item inside it was activated |\n * | `\"escape\"` | `Escape` was pressed and this layer owned it |\n * | `\"outside\"` | a pointer landed outside it, or on its backdrop |\n * | `\"focus\"` | focus entered or left it |\n * | `\"pointer\"` | the pointer entered or left it |\n * | `\"scroll\"` | a tracked scroll container scrolled |\n * | `\"api\"` | a public action was called with no DOM event |\n */\nexport type StateReason =\n | \"user\"\n | \"select\"\n | \"escape\"\n | \"outside\"\n | \"focus\"\n | \"pointer\"\n | \"scroll\"\n | \"api\";\n\n/** Focus modality: the events a focus move delivers to an action. */\nconst FOCUS_EVENTS = new Set([\"blur\", \"focus\", \"focusin\", \"focusout\"]);\n\n/** Pointer modality: the crossing events an action is bound to for hover. */\nconst POINTER_EVENTS = new Set([\"mouseenter\", \"mouseleave\", \"pointerenter\", \"pointerleave\"]);\n\n/**\n * Reads the interaction behind a **public action** from the DOM event it was\n * handed.\n *\n * Stimulus always passes the event to an action it invoked from `data-action`,\n * and a consumer calling the method themselves passes nothing — which is what\n * separates `\"api\"` from the rest.\n *\n * Only an action entry may resolve a reason this way. Inside the component the\n * event type no longer identifies the interaction: one `click` is the trigger\n * being pressed, the page outside being pressed, and an item being activated,\n * and those paths pass their own reason instead.\n *\n * @param event - The event the action received, if any.\n * @returns `\"api\"` with no event, else the modality the event type names, else\n * `\"user\"`.\n *\n * @example\n * ```ts\n * toggle(event?: Event): void {\n * this.#apply(!this.#isOpen, stateReasonFor(event));\n * }\n * ```\n */\nexport function stateReasonFor(event?: Event | null): \"user\" | \"focus\" | \"pointer\" | \"api\" {\n if (!event) return \"api\";\n if (FOCUS_EVENTS.has(event.type)) return \"focus\";\n if (POINTER_EVENTS.has(event.type)) return \"pointer\";\n return \"user\";\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { ensureId } from \"../utils/aria_ids\";\nimport { claimsWhileFocusWithin, EscapeLayer } from \"../utils/escape_layer\";\nimport { type StateReason, stateReasonFor } from \"../utils/state_reason\";\n\n/**\n * Headless, accessible dropdown menu behavior.\n *\n * Markup contract (identifier: `stimeo--dropdown`):\n * <div data-controller=\"stimeo--dropdown\">\n * <button data-stimeo--dropdown-target=\"trigger\"\n * data-action=\"click->stimeo--dropdown#toggle\">Menu</button>\n * <div data-stimeo--dropdown-target=\"menu\">...</div>\n * </div>\n *\n * This is a **disclosure** pattern (WAI-ARIA APG): a button toggles the\n * visibility of an adjacent region. It is intentionally *not* a full APG\n * \"menu\" widget — there is no roving-tabindex arrow-key navigation.\n *\n * @remarks\n * The library owns behavior only (ARIA state, keyboard, focus, outside-click).\n * Visual styling is left entirely to the consumer's CSS.\n *\n * Behavior provided:\n * - The trigger is associated with the menu through `aria-controls`.\n * - Click the trigger to toggle the menu (`aria-expanded` + `hidden` reflect state).\n * - `Escape` closes the menu and returns focus to the trigger. While open the\n * menu is a layer on the shared `EscapeLayer` stack; it claims a press\n * only while focus is inside the controller or fell to the body (a click on\n * non-focusable menu content), so a press aimed at another layer never closes\n * the menu or steals focus, and one keypress closes exactly one layer.\n * - A click outside the controller element closes the menu.\n * - Each move of the open state is reported: `stimeo--dropdown:open` and\n * `stimeo--dropdown:close` dispatch `{ reason: StateReason }`, after the\n * attributes above are written. Both are informational, so neither is\n * cancelable. A call that leaves the state where it already was, the\n * normalization in {@link connect}, and {@link disconnect} are all silent.\n */\nexport class DropdownController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"menu\"];\n static actions = [\"close\", \"open\", \"toggle\"] as const;\n static events = [\"close\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLButtonElement;\n declare readonly menuTarget: HTMLElement;\n declare readonly hasMenuTarget: boolean;\n declare readonly hasTriggerTarget: boolean;\n\n /** Escape-stack membership while open; the shared resolver dismisses via it. */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Whether state moves are reported: set once `connect()` settled the baseline. */\n #reporting = false;\n\n /** Closes the menu when a click lands outside the controller's element. */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.element.contains(event.target as Node)) {\n this.#close(\"outside\");\n }\n };\n\n /** Starts in the closed state and registers outside-click handling. */\n override connect(): void {\n this.#associateTriggerWithMenu();\n this.#close(\"api\");\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.#reporting = true;\n }\n\n /** Removes the listeners registered in {@link connect}. */\n override disconnect(): void {\n this.#reporting = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n }\n\n /** Toggles the menu between open and closed. Bound via `data-action`. */\n toggle(event?: Event): void {\n if (this.#isOpen) {\n this.close(event);\n } else {\n this.open(event);\n }\n }\n\n /** Reveals the menu and reflects the open state on the trigger. */\n open(event?: Event): void {\n this.#open(stateReasonFor(event));\n }\n\n /** Hides the menu and reflects the closed state on the trigger. */\n close(event?: Event): void {\n this.#close(stateReasonFor(event));\n }\n\n /** Reveals the menu, reflects the open state, and reports a move. */\n #open(reason: StateReason): void {\n if (!this.hasMenuTarget) return;\n const was = this.#isOpen;\n this.#escapeLayer.activate(document, {\n onDismiss: () => this.#closeAndRestore(),\n claims: claimsWhileFocusWithin(this.element),\n });\n this.menuTarget.hidden = false;\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", \"true\");\n }\n if (!was && this.#reporting) this.dispatch(\"open\", { detail: { reason }, cancelable: false });\n }\n\n /** Hides the menu, reflects the closed state, and reports a move. */\n #close(reason: StateReason): void {\n const was = this.#isOpen;\n this.#escapeLayer.deactivate();\n if (!this.hasMenuTarget) return;\n this.menuTarget.hidden = true;\n if (this.hasTriggerTarget) {\n this.triggerTarget.setAttribute(\"aria-expanded\", \"false\");\n }\n if (was && this.#reporting) this.dispatch(\"close\", { detail: { reason }, cancelable: false });\n }\n\n /** Closes and restores focus to the trigger (the keyboard-dismissal path). */\n #closeAndRestore(): void {\n this.#close(\"escape\");\n if (this.hasTriggerTarget) this.triggerTarget.focus();\n }\n\n /** Whether the menu is currently visible. */\n get #isOpen(): boolean {\n return this.hasMenuTarget && !this.menuTarget.hidden;\n }\n\n /**\n * Associates the disclosure trigger and controlled region without clobbering\n * authored markup.\n */\n #associateTriggerWithMenu(): void {\n if (!this.hasTriggerTarget || !this.hasMenuTarget) return;\n if (this.triggerTarget.hasAttribute(\"aria-controls\")) return;\n\n this.triggerTarget.setAttribute(\n \"aria-controls\",\n ensureId(this.menuTarget, \"stimeo--dropdown-menu\"),\n );\n }\n}\n"]}
@@ -3,11 +3,19 @@ import { Controller } from '@hotwired/stimulus';
3
3
  // src/controllers/editable_controller.ts
4
4
 
5
5
  // src/utils/composition_tracker.ts
6
+ var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
7
+ "insertCompositionText",
8
+ "insertFromComposition",
9
+ "deleteCompositionText",
10
+ "deleteByComposition"
11
+ ]);
6
12
  var CompositionTracker = class {
7
13
  #observedTargets = /* @__PURE__ */ new Set();
8
14
  #activeTargets = /* @__PURE__ */ new Set();
9
15
  #onStart;
10
16
  #onEnd;
17
+ /** The field whose confirming `input` is still owed, while the window is open. */
18
+ #confirmedTarget = null;
11
19
  constructor(options = {}) {
12
20
  this.#onStart = options.onStart;
13
21
  this.#onEnd = options.onEnd;
@@ -17,6 +25,7 @@ var CompositionTracker = class {
17
25
  if (this.#observedTargets.has(target)) return;
18
26
  target.addEventListener("compositionstart", this.#handleStart);
19
27
  target.addEventListener("compositionend", this.#handleEnd);
28
+ target.addEventListener("keydown", this.#handleKeydown);
20
29
  this.#observedTargets.add(target);
21
30
  }
22
31
  /** Stops tracking one target and clears any active composition it owned. */
@@ -24,29 +33,52 @@ var CompositionTracker = class {
24
33
  if (!this.#observedTargets.delete(target)) return;
25
34
  target.removeEventListener("compositionstart", this.#handleStart);
26
35
  target.removeEventListener("compositionend", this.#handleEnd);
36
+ target.removeEventListener("keydown", this.#handleKeydown);
27
37
  this.#activeTargets.delete(target);
38
+ if (this.#confirmedTarget === target) this.#confirmedTarget = null;
28
39
  }
29
40
  /** Releases every listener and clears state so reconnect starts cleanly. */
30
41
  disconnect() {
31
42
  for (const target of this.#observedTargets) {
32
43
  target.removeEventListener("compositionstart", this.#handleStart);
33
44
  target.removeEventListener("compositionend", this.#handleEnd);
45
+ target.removeEventListener("keydown", this.#handleKeydown);
34
46
  }
35
47
  this.#observedTargets.clear();
36
48
  this.#activeTargets.clear();
49
+ this.#confirmedTarget = null;
37
50
  }
38
51
  /** True when lifecycle tracking or the current event reports composition. */
39
52
  isComposing(event) {
40
53
  return this.#activeTargets.size > 0 || event?.isComposing === true;
41
54
  }
55
+ /**
56
+ * Whether `event` is the `input` echoing the composition just confirmed.
57
+ *
58
+ * Asking closes the window either way, so one confirmation is folded at most
59
+ * once and a consumer asks once per `input`.
60
+ */
61
+ consumesConfirmedInput(event) {
62
+ const confirmed = this.#confirmedTarget;
63
+ this.#confirmedTarget = null;
64
+ if (confirmed === null || confirmed !== event.target) return false;
65
+ const inputType = event.inputType;
66
+ return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
67
+ }
42
68
  #handleStart = (event) => {
69
+ this.#confirmedTarget = null;
43
70
  if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
44
71
  this.#onStart?.(event);
45
72
  };
46
73
  #handleEnd = (event) => {
47
74
  if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
75
+ this.#confirmedTarget = event.target;
48
76
  this.#onEnd?.(event);
49
77
  };
78
+ /** A key on an observed field opens an edit of its own, so no echo is owed. */
79
+ #handleKeydown = () => {
80
+ this.#confirmedTarget = null;
81
+ };
50
82
  };
51
83
 
52
84
  // src/controllers/editable_controller.ts
@@ -79,6 +111,8 @@ var EditableController = class extends Controller {
79
111
  * "saves wherever focus moved" true only for focus that leaves the input
80
112
  * itself, and tabbing straight past an inner button would strand the editor
81
113
  * open.
114
+ *
115
+ * @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.
82
116
  */
83
117
  #onFocusOut = (event) => {
84
118
  const from = event.target;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/controllers/editable_controller.ts"],"names":[],"mappings":"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","file":"editable_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n this.#activeTargets.delete(target);\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n readonly #handleStart = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#onEnd?.(event);\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible inline-editing behavior.\n *\n * Markup contract (identifier: `stimeo--editable`):\n * <div data-controller=\"stimeo--editable\"\n * data-stimeo--editable-submit-on-blur-value=\"true\">\n * <button type=\"button\" aria-label=\"Edit title\"\n * data-stimeo--editable-target=\"display\"\n * data-action=\"click->stimeo--editable#edit\n * keydown->stimeo--editable#onDisplayKeydown\">Current title</button>\n * <input type=\"text\" aria-label=\"Title\" hidden\n * data-stimeo--editable-target=\"input\"\n * data-action=\"keydown->stimeo--editable#onKeydown\" />\n * </div>\n *\n * There is no dedicated APG pattern; this implements a display ⇄ edit toggle with\n * focus management and keyboard commit/cancel, leaning on native form-control\n * labeling (WCAG 1.3.1 / 4.1.2). The look is the consumer's CSS, keyed off\n * `data-mode`.\n *\n * Behavior provided:\n * - Activating the display element (`Enter`/`Space` via the `<button>`, or `F2`)\n * enters edit mode, focuses the input, and selects its text.\n * - `Enter` (single-line) or `Ctrl+Enter` / `Cmd+Enter` (multiline `<textarea>`)\n * saves; `Escape` cancels. Both return focus to the display element. Keys\n * pressed during an IME composition act on the composition, never the edit:\n * a conversion-cancelling `Escape` keeps editing and a conversion-confirming\n * `Enter` never saves (lifecycle-tracked, covering events that omit\n * `isComposing`).\n * - Losing focus while editing saves when `submitOnBlur` is true (the default),\n * honoring wherever focus moved. The departure is watched on the controller\n * element, so it is seen from wherever focus sat — the input, or a Save or\n * Cancel button beside it — and no per-control wiring is required. Focus\n * landing on another element inside this controller has not left the edit, so\n * it commits nothing. When `submitOnBlur` is false, editing is kept.\n * - `save()`, `cancel()`, and `revert()` are actions, so a pointer-only user\n * reaches every path a keyboard user does. `revert()` restores the value from\n * before the last save once, for a consumer whose server rejected it.\n * - Saving dispatches `stimeo--editable:change` with `{ value, previous }` only\n * when the value actually changed; cancelling dispatches\n * `stimeo--editable:cancel` with an empty `{}` detail.\n *\n * The value is whatever the display element declares. A `data-value` attribute\n * holds it when the element's text is a rendering of the value rather than the\n * value itself — a formatted date, a grouped number, an icon beside a label.\n * Without that attribute the trimmed text is the value, and it must then be a\n * string the editing control accepts: `<input type=\"date\">` keeps `2026-08-23`\n * but sanitizes `August 23, 2026` away to an empty string. Saving updates\n * whichever of the two carries the value, so a rendered display keeps its markup\n * and the consumer re-renders it from the `change` event.\n */\nexport class EditableController extends Controller<HTMLElement> {\n static override targets = [\"display\", \"input\"];\n static override values = {\n submitOnBlur: { type: Boolean, default: true },\n };\n static actions = [\"cancel\", \"edit\", \"onDisplayKeydown\", \"onKeydown\", \"revert\", \"save\"] as const;\n static events = [\"cancel\", \"change\"] as const;\n\n declare readonly displayTarget: HTMLElement;\n declare readonly inputTarget: HTMLInputElement | HTMLTextAreaElement;\n declare readonly hasDisplayTarget: boolean;\n declare readonly hasInputTarget: boolean;\n\n declare submitOnBlurValue: boolean;\n\n /** The value captured when edit mode began, used to detect real changes. */\n #previousValue = \"\";\n\n /**\n * The value the last save replaced, or `null` when there is nothing to undo.\n * Cleared on connect: a value from before a page restore is unrecoverable, so\n * `revert()` must not resurrect one.\n */\n #revertValue: string | null = null;\n\n /**\n * Owns IME lifecycle state for the edit surface, so a keydown that belongs to\n * a composition (cancel or confirm) is never treated as an edit command.\n */\n readonly #composition = new CompositionTracker();\n\n /**\n * Watches focus leaving the editor from wherever it currently sits.\n *\n * `focusout` bubbles where `blur` does not, so one listener on the root sees\n * every departure — including one from a Save or Cancel button the consumer\n * placed beside the input. Binding the input alone would make the promise\n * \"saves wherever focus moved\" true only for focus that leaves the input\n * itself, and tabbing straight past an inner button would strand the editor\n * open.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n // Entering edit mode hides the display element, and hiding the element that\n // holds focus reports a departure with nowhere to go. That hand-off is the\n // edit starting, not the user leaving it, and the input is focused moments\n // later in the same task. The display is hidden for the whole edit, so it\n // can never be the source of a real departure from one.\n const from = event.target;\n if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;\n // Focus reaching another element inside this controller has not left the\n // edit. Without this the commit would beat an inner button's own click —\n // and a Cancel button would save what it promises to discard.\n const next = event.relatedTarget;\n if (next instanceof Node && this.element.contains(next)) return;\n if (this.submitOnBlurValue) this.#commit(false);\n };\n\n /** Establishes the initial display mode (display shown, input hidden). */\n override connect(): void {\n // The tracker is idempotent, so observing here and in the target callback\n // keeps IME tracking correct whichever of the two the framework runs first.\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n this.#revertValue = null;\n this.#setMode(\"display\");\n }\n\n /** Releases the composition and focus listeners so nothing outlives the element. */\n override disconnect(): void {\n this.#composition.disconnect();\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /** Tracks an input added initially or after connect (e.g. a Turbo swap). */\n inputTargetConnected(input: HTMLElement): void {\n this.#composition.observe(input);\n this.#applyMode();\n }\n\n /** Removes composition listeners when the active input is replaced or removed. */\n inputTargetDisconnected(input: HTMLElement): void {\n this.#composition.unobserve(input);\n }\n\n /** Re-hides or re-shows a display element that arrived after the mode was set. */\n displayTargetConnected(): void {\n this.#applyMode();\n }\n\n /** Enters edit mode: seeds the input from the declared value, focuses, selects. */\n edit(): void {\n if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;\n this.#previousValue = this.#currentValue;\n this.inputTarget.value = this.#previousValue;\n this.#setMode(\"editing\");\n this.inputTarget.focus();\n this.inputTarget.select();\n }\n\n /** Commits the edit and returns focus to the display element. */\n save(): void {\n this.#commit(true);\n }\n\n /** Discards edits, returns to display mode, and dispatches `cancel`. */\n cancel(): void {\n if (!this.#isEditing) return;\n this.#setMode(\"display\");\n if (this.hasDisplayTarget) this.displayTarget.focus();\n this.dispatch(\"cancel\", { detail: {} });\n }\n\n /**\n * Puts back the value the last save replaced — for a consumer whose server\n * rejected it. Silent by design: a `change` here would re-enter the same\n * handler that asked for the undo. One save, one undo.\n */\n revert(): void {\n if (this.#revertValue === null || this.#isEditing) return;\n this.#writeValue(this.#revertValue);\n this.#revertValue = null;\n }\n\n /** Adds `F2` as an editing entry point alongside the button's native activation. */\n onDisplayKeydown(event: KeyboardEvent): void {\n if (event.key === \"F2\") {\n event.preventDefault();\n this.edit();\n }\n }\n\n /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */\n onKeydown(event: KeyboardEvent): void {\n // Keys fired during IME composition steer the composition (cancel a\n // conversion, confirm a candidate) and must never cancel or save the edit.\n if (this.#composition.isComposing(event)) return;\n if (event.key === \"Escape\") {\n // Leave a press an inner handler already owned.\n if (event.defaultPrevented) return;\n event.preventDefault();\n this.cancel();\n return;\n }\n if (event.key === \"Enter\") {\n // A descendant widget that already claimed the key (a completion popup\n // confirming its candidate) must not ALSO commit the edit — composition\n // depends on this yield. Kept separate from the Escape guard above, which\n // answers a different question: who owns an Escape press.\n if (event.defaultPrevented) return;\n // A bare Enter inserts a newline in a textarea; only Ctrl+Enter (or Cmd+Enter on\n // macOS, the platform-conventional commit chord) saves there.\n if (this.#isMultiline && !(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n this.#commit(true);\n }\n }\n\n /**\n * Returns to display mode, storing the input's value and dispatching `change`\n * when it differs from where editing began.\n *\n * @param restoreFocus - Move focus back to the display element (explicit\n * commit) rather than honoring the user's new focus target (blur).\n */\n #commit(restoreFocus: boolean): void {\n if (!this.#isEditing || !this.hasInputTarget) return;\n // One normalization for all three consumers below, so the stored value, the\n // comparison, and the detail can never disagree about what was saved.\n const value = this.inputTarget.value.trim();\n const previous = this.#previousValue;\n this.#writeValue(value);\n this.#setMode(\"display\");\n if (restoreFocus && this.hasDisplayTarget) this.displayTarget.focus();\n if (value !== previous) {\n this.#revertValue = previous;\n this.dispatch(\"change\", { detail: { value, previous } });\n }\n }\n\n /** Stores `value` wherever the display element declares it. */\n #writeValue(value: string): void {\n if (!this.hasDisplayTarget) return;\n const display = this.displayTarget;\n // Text that renders the value is the consumer's to compose; overwriting it\n // with the raw value would destroy a rendering this controller cannot rebuild.\n if (display.hasAttribute(\"data-value\")) display.dataset.value = value;\n else display.textContent = value;\n }\n\n /** Derives both elements' visibility from the mode currently in the DOM. */\n #applyMode(): void {\n const editing = this.#isEditing;\n if (this.hasDisplayTarget) this.displayTarget.hidden = editing;\n if (this.hasInputTarget) this.inputTarget.hidden = !editing;\n }\n\n /** Records the mode, then brings both elements in line with it. */\n #setMode(mode: \"display\" | \"editing\"): void {\n this.element.dataset.mode = mode;\n this.#applyMode();\n }\n\n /** The value the display element declares, trimmed. */\n get #currentValue(): string {\n const display = this.displayTarget;\n return (display.dataset.value ?? display.textContent ?? \"\").trim();\n }\n\n /** Whether the editing control is a multi-line `<textarea>`. */\n get #isMultiline(): boolean {\n return this.inputTarget.tagName === \"TEXTAREA\";\n }\n\n /** Whether the controller is currently in edit mode. */\n get #isEditing(): boolean {\n return this.element.dataset.mode === \"editing\";\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/composition_tracker.ts","../../src/controllers/editable_controller.ts"],"names":[],"mappings":"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","file":"editable_controller.js","sourcesContent":["/** Minimal event shape carrying the platform's per-event composition signal. */\nexport interface CompositionSignal {\n readonly isComposing?: boolean;\n}\n\n/** Minimal `input` shape needed to tell a composition echo from a fresh edit. */\nexport interface ConfirmedInputSignal {\n readonly target: EventTarget | null;\n readonly inputType?: string;\n}\n\n/**\n * The `inputType` values an engine reports on an `input` that belongs to a\n * composition rather than to an edit the user just made.\n */\nconst COMPOSITION_INPUT_TYPES = new Set([\n \"insertCompositionText\",\n \"insertFromComposition\",\n \"deleteCompositionText\",\n \"deleteByComposition\",\n]);\n\n/** Hooks that keep component-specific work outside {@link CompositionTracker}. */\nexport interface CompositionTrackerOptions {\n /** Runs after lifecycle state is set for a `compositionstart` event. */\n readonly onStart?: (event: Event) => void;\n /** Runs after lifecycle state is cleared for a `compositionend` event. */\n readonly onEnd?: (event: Event) => void;\n}\n\n/**\n * Owns IME composition listeners and transient state for one or more event targets.\n *\n * Some browsers omit `KeyboardEvent.isComposing` on the keydown that confirms a\n * conversion. Tracking `compositionstart` through `compositionend` supplies the\n * missing lifecycle signal without relying on deprecated numeric key codes.\n * Component policy stays with the consumer: filtering, validation, and submission\n * belong in the optional {@link CompositionTrackerOptions.onEnd} callback.\n *\n * The tracker also owns the other half of that lifecycle: **the `input` some\n * engines send after `compositionend` to echo the text just confirmed.** A\n * consumer that ran `onEnd` and then handled the echo would commit the same\n * confirmation twice, so {@link CompositionTracker.consumesConfirmedInput} tells\n * the echo apart and the consumer returns early on it.\n *\n * **What the echo is not, is the harder half.** Not every engine sends one —\n * Chromium ends a confirmation at `compositionend` with no `input` after it — so\n * a window that waits for the echo and only closes on the next key would swallow\n * the first edit that arrives without one: dictation, autofill, a drop,\n * `insertText`. Two things keep that from happening. The window closes on the\n * next anything — a key, a new composition, any `input`, losing the field — and\n * an `input` that reports an `inputType` outside {@link COMPOSITION_INPUT_TYPES}\n * is an edit, so it is never folded. Only an engine that reports no `inputType`\n * at all is left relying on the window alone.\n *\n * @example\n * ```ts\n * #composition = new CompositionTracker({ onEnd: () => this.filter() });\n *\n * connect(): void {\n * this.#composition.observe(this.inputTarget);\n * }\n *\n * disconnect(): void {\n * this.#composition.disconnect();\n * }\n *\n * onInput(event: InputEvent): void {\n * if (this.#composition.consumesConfirmedInput(event)) return;\n * if (this.#composition.isComposing(event)) return;\n * }\n *\n * onKeydown(event: KeyboardEvent): void {\n * if (this.#composition.isComposing(event)) return;\n * }\n * ```\n */\nexport class CompositionTracker {\n readonly #observedTargets = new Set<EventTarget>();\n readonly #activeTargets = new Set<EventTarget>();\n readonly #onStart: ((event: Event) => void) | undefined;\n readonly #onEnd: ((event: Event) => void) | undefined;\n /** The field whose confirming `input` is still owed, while the window is open. */\n #confirmedTarget: EventTarget | null = null;\n\n constructor(options: CompositionTrackerOptions = {}) {\n this.#onStart = options.onStart;\n this.#onEnd = options.onEnd;\n }\n\n /** Starts lifecycle tracking for `target`; repeated calls are idempotent. */\n observe(target: EventTarget): void {\n if (this.#observedTargets.has(target)) return;\n target.addEventListener(\"compositionstart\", this.#handleStart);\n target.addEventListener(\"compositionend\", this.#handleEnd);\n target.addEventListener(\"keydown\", this.#handleKeydown);\n this.#observedTargets.add(target);\n }\n\n /** Stops tracking one target and clears any active composition it owned. */\n unobserve(target: EventTarget): void {\n if (!this.#observedTargets.delete(target)) return;\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n target.removeEventListener(\"keydown\", this.#handleKeydown);\n this.#activeTargets.delete(target);\n if (this.#confirmedTarget === target) this.#confirmedTarget = null;\n }\n\n /** Releases every listener and clears state so reconnect starts cleanly. */\n disconnect(): void {\n for (const target of this.#observedTargets) {\n target.removeEventListener(\"compositionstart\", this.#handleStart);\n target.removeEventListener(\"compositionend\", this.#handleEnd);\n target.removeEventListener(\"keydown\", this.#handleKeydown);\n }\n this.#observedTargets.clear();\n this.#activeTargets.clear();\n this.#confirmedTarget = null;\n }\n\n /** True when lifecycle tracking or the current event reports composition. */\n isComposing(event?: CompositionSignal): boolean {\n return this.#activeTargets.size > 0 || event?.isComposing === true;\n }\n\n /**\n * Whether `event` is the `input` echoing the composition just confirmed.\n *\n * Asking closes the window either way, so one confirmation is folded at most\n * once and a consumer asks once per `input`.\n */\n consumesConfirmedInput(event: ConfirmedInputSignal): boolean {\n const confirmed = this.#confirmedTarget;\n this.#confirmedTarget = null;\n if (confirmed === null || confirmed !== event.target) return false;\n // An engine that reports no kind at all leaves `inputType` empty, and the\n // window is then the only signal there is.\n const inputType = event.inputType;\n return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);\n }\n\n readonly #handleStart = (event: Event): void => {\n this.#confirmedTarget = null;\n if (event.currentTarget) this.#activeTargets.add(event.currentTarget);\n this.#onStart?.(event);\n };\n\n readonly #handleEnd = (event: Event): void => {\n if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);\n this.#confirmedTarget = event.target;\n this.#onEnd?.(event);\n };\n\n /** A key on an observed field opens an edit of its own, so no echo is owed. */\n readonly #handleKeydown = (): void => {\n this.#confirmedTarget = null;\n };\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\n\n/**\n * Headless, accessible inline-editing behavior.\n *\n * Markup contract (identifier: `stimeo--editable`):\n * <div data-controller=\"stimeo--editable\"\n * data-stimeo--editable-submit-on-blur-value=\"true\">\n * <button type=\"button\" aria-label=\"Edit title\"\n * data-stimeo--editable-target=\"display\"\n * data-action=\"click->stimeo--editable#edit\n * keydown->stimeo--editable#onDisplayKeydown\">Current title</button>\n * <input type=\"text\" aria-label=\"Title\" hidden\n * data-stimeo--editable-target=\"input\"\n * data-action=\"keydown->stimeo--editable#onKeydown\" />\n * </div>\n *\n * There is no dedicated APG pattern; this implements a display ⇄ edit toggle with\n * focus management and keyboard commit/cancel, leaning on native form-control\n * labeling (WCAG 1.3.1 / 4.1.2). The look is the consumer's CSS, keyed off\n * `data-mode`.\n *\n * Behavior provided:\n * - Activating the display element (`Enter`/`Space` via the `<button>`, or `F2`)\n * enters edit mode, focuses the input, and selects its text.\n * - `Enter` (single-line) or `Ctrl+Enter` / `Cmd+Enter` (multiline `<textarea>`)\n * saves; `Escape` cancels. Both return focus to the display element. Keys\n * pressed during an IME composition act on the composition, never the edit:\n * a conversion-cancelling `Escape` keeps editing and a conversion-confirming\n * `Enter` never saves (lifecycle-tracked, covering events that omit\n * `isComposing`).\n * - Losing focus while editing saves when `submitOnBlur` is true (the default),\n * honoring wherever focus moved. The departure is watched on the controller\n * element, so it is seen from wherever focus sat — the input, or a Save or\n * Cancel button beside it — and no per-control wiring is required. Focus\n * landing on another element inside this controller has not left the edit, so\n * it commits nothing. When `submitOnBlur` is false, editing is kept.\n * - `save()`, `cancel()`, and `revert()` are actions, so a pointer-only user\n * reaches every path a keyboard user does. `revert()` restores the value from\n * before the last save once, for a consumer whose server rejected it.\n * - Saving dispatches `stimeo--editable:change` with `{ value, previous }` only\n * when the value actually changed; cancelling dispatches\n * `stimeo--editable:cancel` with an empty `{}` detail.\n *\n * The value is whatever the display element declares. A `data-value` attribute\n * holds it when the element's text is a rendering of the value rather than the\n * value itself — a formatted date, a grouped number, an icon beside a label.\n * Without that attribute the trimmed text is the value, and it must then be a\n * string the editing control accepts: `<input type=\"date\">` keeps `2026-08-23`\n * but sanitizes `August 23, 2026` away to an empty string. Saving updates\n * whichever of the two carries the value, so a rendered display keeps its markup\n * and the consumer re-renders it from the `change` event.\n */\nexport class EditableController extends Controller<HTMLElement> {\n static override targets = [\"display\", \"input\"];\n static override values = {\n submitOnBlur: { type: Boolean, default: true },\n };\n static actions = [\"cancel\", \"edit\", \"onDisplayKeydown\", \"onKeydown\", \"revert\", \"save\"] as const;\n static events = [\"cancel\", \"change\"] as const;\n\n declare readonly displayTarget: HTMLElement;\n declare readonly inputTarget: HTMLInputElement | HTMLTextAreaElement;\n declare readonly hasDisplayTarget: boolean;\n declare readonly hasInputTarget: boolean;\n\n declare submitOnBlurValue: boolean;\n\n /** The value captured when edit mode began, used to detect real changes. */\n #previousValue = \"\";\n\n /**\n * The value the last save replaced, or `null` when there is nothing to undo.\n * Cleared on connect: a value from before a page restore is unrecoverable, so\n * `revert()` must not resurrect one.\n */\n #revertValue: string | null = null;\n\n /**\n * Owns IME lifecycle state for the edit surface, so a keydown that belongs to\n * a composition (cancel or confirm) is never treated as an edit command.\n */\n readonly #composition = new CompositionTracker();\n\n /**\n * Watches focus leaving the editor from wherever it currently sits.\n *\n * `focusout` bubbles where `blur` does not, so one listener on the root sees\n * every departure — including one from a Save or Cancel button the consumer\n * placed beside the input. Binding the input alone would make the promise\n * \"saves wherever focus moved\" true only for focus that leaves the input\n * itself, and tabbing straight past an inner button would strand the editor\n * open.\n *\n * @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.\n */\n readonly #onFocusOut = (event: FocusEvent): void => {\n // Entering edit mode hides the display element, and hiding the element that\n // holds focus reports a departure with nowhere to go. That hand-off is the\n // edit starting, not the user leaving it, and the input is focused moments\n // later in the same task. The display is hidden for the whole edit, so it\n // can never be the source of a real departure from one.\n const from = event.target;\n if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;\n // Focus reaching another element inside this controller has not left the\n // edit. Without this the commit would beat an inner button's own click —\n // and a Cancel button would save what it promises to discard.\n const next = event.relatedTarget;\n if (next instanceof Node && this.element.contains(next)) return;\n if (this.submitOnBlurValue) this.#commit(false);\n };\n\n /** Establishes the initial display mode (display shown, input hidden). */\n override connect(): void {\n // The tracker is idempotent, so observing here and in the target callback\n // keeps IME tracking correct whichever of the two the framework runs first.\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.element.addEventListener(\"focusout\", this.#onFocusOut);\n this.#revertValue = null;\n this.#setMode(\"display\");\n }\n\n /** Releases the composition and focus listeners so nothing outlives the element. */\n override disconnect(): void {\n this.#composition.disconnect();\n this.element.removeEventListener(\"focusout\", this.#onFocusOut);\n }\n\n /** Tracks an input added initially or after connect (e.g. a Turbo swap). */\n inputTargetConnected(input: HTMLElement): void {\n this.#composition.observe(input);\n this.#applyMode();\n }\n\n /** Removes composition listeners when the active input is replaced or removed. */\n inputTargetDisconnected(input: HTMLElement): void {\n this.#composition.unobserve(input);\n }\n\n /** Re-hides or re-shows a display element that arrived after the mode was set. */\n displayTargetConnected(): void {\n this.#applyMode();\n }\n\n /** Enters edit mode: seeds the input from the declared value, focuses, selects. */\n edit(): void {\n if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;\n this.#previousValue = this.#currentValue;\n this.inputTarget.value = this.#previousValue;\n this.#setMode(\"editing\");\n this.inputTarget.focus();\n this.inputTarget.select();\n }\n\n /** Commits the edit and returns focus to the display element. */\n save(): void {\n this.#commit(true);\n }\n\n /** Discards edits, returns to display mode, and dispatches `cancel`. */\n cancel(): void {\n if (!this.#isEditing) return;\n this.#setMode(\"display\");\n if (this.hasDisplayTarget) this.displayTarget.focus();\n this.dispatch(\"cancel\", { detail: {} });\n }\n\n /**\n * Puts back the value the last save replaced — for a consumer whose server\n * rejected it. Silent by design: a `change` here would re-enter the same\n * handler that asked for the undo. One save, one undo.\n */\n revert(): void {\n if (this.#revertValue === null || this.#isEditing) return;\n this.#writeValue(this.#revertValue);\n this.#revertValue = null;\n }\n\n /** Adds `F2` as an editing entry point alongside the button's native activation. */\n onDisplayKeydown(event: KeyboardEvent): void {\n if (event.key === \"F2\") {\n event.preventDefault();\n this.edit();\n }\n }\n\n /** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */\n onKeydown(event: KeyboardEvent): void {\n // Keys fired during IME composition steer the composition (cancel a\n // conversion, confirm a candidate) and must never cancel or save the edit.\n if (this.#composition.isComposing(event)) return;\n if (event.key === \"Escape\") {\n // Leave a press an inner handler already owned.\n if (event.defaultPrevented) return;\n event.preventDefault();\n this.cancel();\n return;\n }\n if (event.key === \"Enter\") {\n // A descendant widget that already claimed the key (a completion popup\n // confirming its candidate) must not ALSO commit the edit — composition\n // depends on this yield. Kept separate from the Escape guard above, which\n // answers a different question: who owns an Escape press.\n if (event.defaultPrevented) return;\n // A bare Enter inserts a newline in a textarea; only Ctrl+Enter (or Cmd+Enter on\n // macOS, the platform-conventional commit chord) saves there.\n if (this.#isMultiline && !(event.ctrlKey || event.metaKey)) return;\n event.preventDefault();\n this.#commit(true);\n }\n }\n\n /**\n * Returns to display mode, storing the input's value and dispatching `change`\n * when it differs from where editing began.\n *\n * @param restoreFocus - Move focus back to the display element (explicit\n * commit) rather than honoring the user's new focus target (blur).\n */\n #commit(restoreFocus: boolean): void {\n if (!this.#isEditing || !this.hasInputTarget) return;\n // One normalization for all three consumers below, so the stored value, the\n // comparison, and the detail can never disagree about what was saved.\n const value = this.inputTarget.value.trim();\n const previous = this.#previousValue;\n this.#writeValue(value);\n this.#setMode(\"display\");\n if (restoreFocus && this.hasDisplayTarget) this.displayTarget.focus();\n if (value !== previous) {\n this.#revertValue = previous;\n this.dispatch(\"change\", { detail: { value, previous } });\n }\n }\n\n /** Stores `value` wherever the display element declares it. */\n #writeValue(value: string): void {\n if (!this.hasDisplayTarget) return;\n const display = this.displayTarget;\n // Text that renders the value is the consumer's to compose; overwriting it\n // with the raw value would destroy a rendering this controller cannot rebuild.\n if (display.hasAttribute(\"data-value\")) display.dataset.value = value;\n else display.textContent = value;\n }\n\n /** Derives both elements' visibility from the mode currently in the DOM. */\n #applyMode(): void {\n const editing = this.#isEditing;\n if (this.hasDisplayTarget) this.displayTarget.hidden = editing;\n if (this.hasInputTarget) this.inputTarget.hidden = !editing;\n }\n\n /** Records the mode, then brings both elements in line with it. */\n #setMode(mode: \"display\" | \"editing\"): void {\n this.element.dataset.mode = mode;\n this.#applyMode();\n }\n\n /** The value the display element declares, trimmed. */\n get #currentValue(): string {\n const display = this.displayTarget;\n return (display.dataset.value ?? display.textContent ?? \"\").trim();\n }\n\n /** Whether the editing control is a multi-line `<textarea>`. */\n get #isMultiline(): boolean {\n return this.inputTarget.tagName === \"TEXTAREA\";\n }\n\n /** Whether the controller is currently in edit mode. */\n get #isEditing(): boolean {\n return this.element.dataset.mode === \"editing\";\n }\n}\n"]}
@@ -103,6 +103,25 @@ var DetachGate = class _DetachGate {
103
103
  }
104
104
  };
105
105
 
106
+ // src/utils/element_part.ts
107
+ function matchingPart(root, selector) {
108
+ return root.matches(selector) ? root : root.querySelector(selector);
109
+ }
110
+ function writeLabel(slot, text) {
111
+ if (slot.childElementCount === 0) {
112
+ slot.textContent = text;
113
+ return;
114
+ }
115
+ let first = null;
116
+ for (const node of Array.from(slot.childNodes)) {
117
+ if (node.nodeType !== Node.TEXT_NODE) continue;
118
+ if (first) node.remove();
119
+ else first = node;
120
+ }
121
+ if (first) first.nodeValue = text;
122
+ else slot.prepend(text);
123
+ }
124
+
106
125
  // src/utils/focus_candidate.ts
107
126
  function inheritsFieldsetDisabled(control) {
108
127
  let fieldset = control.closest("fieldset[disabled]");
@@ -114,10 +133,93 @@ function inheritsFieldsetDisabled(control) {
114
133
  return false;
115
134
  }
116
135
 
136
+ // src/utils/target_selector.ts
137
+ function targetSelector(identifier, name) {
138
+ return `[data-${identifier}-target~="${name}"]`;
139
+ }
140
+
141
+ // src/utils/template_row.ts
142
+ function cloneTemplateRoot(template) {
143
+ const root = template.content.firstElementChild;
144
+ return root instanceof HTMLElement ? root.cloneNode(true) : null;
145
+ }
146
+ var TemplateRow = class {
147
+ #options;
148
+ #warned = false;
149
+ constructor(options) {
150
+ this.#options = options;
151
+ }
152
+ /** The attribute selector for one declared part, in this controller's namespace. */
153
+ selector(name) {
154
+ return targetSelector(this.#options.identifier, name);
155
+ }
156
+ /** Re-arms the once-per-connection diagnostic. */
157
+ connect() {
158
+ this.#warned = false;
159
+ }
160
+ /**
161
+ * Names one missing part on the console, at most once per connection, and
162
+ * returns `null` so a caller can hand it straight back.
163
+ */
164
+ report(missing) {
165
+ return this.#say(`lacks ${missing}`);
166
+ }
167
+ /** Writes one diagnostic per connection, saying what the template got wrong. */
168
+ #say(problem) {
169
+ if (this.#warned) return null;
170
+ this.#warned = true;
171
+ const { identifier, outcome, noun } = this.#options;
172
+ console.warn(`Stimeo UI: "${identifier}" ${outcome} because its ${noun} ${problem}.`);
173
+ return null;
174
+ }
175
+ /**
176
+ * Clones the row and resolves its parts, or names what the template got wrong
177
+ * and returns `null`. `values` fills the button's authored `aria-label`.
178
+ */
179
+ instantiate(template, values) {
180
+ const { root: rootName, required, optional, button: buttonName } = this.#options;
181
+ const root = cloneTemplateRoot(template);
182
+ if (!root?.matches(this.selector(rootName))) {
183
+ return this.report(`${this.#article(rootName)} "${rootName}" root`);
184
+ }
185
+ const count = template.content.children.length;
186
+ if (count !== 1) {
187
+ return this.#say(`holds ${count} elements, and the row has to be its only one`);
188
+ }
189
+ const slots = {};
190
+ for (const name2 of required) {
191
+ const part = this.#resolve(root, name2);
192
+ if (!part) return this.report(`${this.#article(name2)} "${name2}" target`);
193
+ slots[name2] = part;
194
+ }
195
+ for (const name2 of optional ?? []) slots[name2] = this.#resolve(root, name2);
196
+ const named = this.#resolve(root, buttonName);
197
+ if (!(named instanceof HTMLButtonElement)) {
198
+ return this.report(`${this.#article(buttonName)} "${buttonName}" target <button>`);
199
+ }
200
+ const button = named;
201
+ const name = button.getAttribute("aria-label")?.trim() ?? "";
202
+ if (name === "") return this.report(`a non-empty aria-label on its "${buttonName}" target`);
203
+ button.setAttribute("aria-label", fillTemplate(name, values));
204
+ return { root, slots, button };
205
+ }
206
+ /** The row may be its own part; otherwise the first descendant carrying the target. */
207
+ #resolve(root, name) {
208
+ return matchingPart(root, this.selector(name));
209
+ }
210
+ #article(name) {
211
+ return /^[aeiou]/i.test(name) ? "an" : "a";
212
+ }
213
+ };
214
+
117
215
  // src/controllers/file_dropzone_controller.ts
118
216
  var DRAGOVER_ATTRIBUTE = "data-dragover";
119
- var INVALID_ATTRIBUTE = "data-stimeo--file-dropzone-invalid";
217
+ var INVALID_ATTRIBUTE = "invalid";
120
218
  var FileDropzoneController = class extends Controller {
219
+ /** The hook above, in the namespace this controller is registered under. */
220
+ get #invalidAttribute() {
221
+ return `data-${this.identifier}-${INVALID_ATTRIBUTE}`;
222
+ }
121
223
  static targets = [
122
224
  "zone",
123
225
  "trigger",
@@ -147,14 +249,22 @@ var FileDropzoneController = class extends Controller {
147
249
  #entries = [];
148
250
  /** Whether a drag is currently over the zone; the source for `data-dragover`. */
149
251
  #dragging = false;
150
- /** Whether this connection already reported its unusable item template. */
151
- #warnedTemplate = false;
252
+ /** Builds one preview item from the authored template and owns its diagnostic. */
253
+ #rows = new TemplateRow({
254
+ identifier: this.identifier,
255
+ root: "item",
256
+ required: ["name"],
257
+ optional: ["thumb"],
258
+ button: "remove",
259
+ outcome: "added no file",
260
+ noun: "item template"
261
+ });
152
262
  #gate = new DetachGate();
153
263
  #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
154
264
  /** Subscribes to the Turbo cache rewind and re-arms the template diagnostic. */
155
265
  connect() {
156
266
  this.#gate.cancel();
157
- this.#warnedTemplate = false;
267
+ this.#rows.connect();
158
268
  this.#beforeCache.activate();
159
269
  }
160
270
  /**
@@ -246,15 +356,20 @@ var FileDropzoneController = class extends Controller {
246
356
  */
247
357
  #onItemClick = (event) => {
248
358
  const button = event.target.closest(
249
- 'button[data-stimeo--file-dropzone-target~="remove"]'
359
+ `button${this.#rows.selector("remove")}`
250
360
  );
251
361
  const index = this.#entries.findIndex((entry) => entry.item.contains(button));
252
362
  if (index !== -1) this.#removeAt(index);
253
363
  };
254
- /** Validates each incoming file, renders the accepted ones, and reports the batch. */
364
+ /**
365
+ * Validates each incoming file, renders the accepted ones, and reports the batch.
366
+ *
367
+ * @stimeoRuntimeOnly The limits decide which files of this one drop are taken, and the texts word
368
+ * the announcements it makes.
369
+ */
255
370
  #addFiles(files) {
256
371
  this.#rehome();
257
- if (this.hasZoneTarget) this.zoneTarget.removeAttribute(INVALID_ATTRIBUTE);
372
+ if (this.hasZoneTarget) this.zoneTarget.removeAttribute(this.#invalidAttribute);
258
373
  const rejected = /* @__PURE__ */ new Map();
259
374
  const turnedAway = [];
260
375
  let addedName = "";
@@ -262,7 +377,7 @@ var FileDropzoneController = class extends Controller {
262
377
  for (const file of Array.from(files)) {
263
378
  const reason = this.#validate(file);
264
379
  if (reason !== null) {
265
- if (this.hasZoneTarget) this.zoneTarget.setAttribute(INVALID_ATTRIBUTE, "");
380
+ if (this.hasZoneTarget) this.zoneTarget.setAttribute(this.#invalidAttribute, "");
266
381
  const batch = rejected.get(reason);
267
382
  if (batch) batch.count += 1;
268
383
  else rejected.set(reason, { name: file.name, count: 1 });
@@ -310,26 +425,25 @@ var FileDropzoneController = class extends Controller {
310
425
  /**
311
426
  * Builds one preview item (name, optional thumbnail, remove button) and reports
312
427
  * whether it was rendered.
428
+ *
429
+ * A refused row leaves the addition a no-op — nothing about the selection, the
430
+ * native input, the announcements, or the events changes — and the row reports
431
+ * why on the console once per connection.
313
432
  */
314
433
  #appendFile(file) {
315
- if (!this.hasListTarget) return this.#warnTemplate('a "list" target to render into');
316
- if (!this.hasItemTemplateTarget) return this.#warnTemplate('an "itemTemplate" target');
317
- const fragment = this.itemTemplateTarget.content.cloneNode(true);
318
- const item = this.#slot(fragment, "item");
319
- const name = this.#slot(fragment, "name");
320
- const thumb = this.#slot(fragment, "thumb");
321
- const button = fragment.querySelector(
322
- 'button[data-stimeo--file-dropzone-target~="remove"]'
323
- );
324
- const removeName = button?.getAttribute("aria-label")?.trim() ?? "";
325
- if (!item) return this.#warnTemplate('an "item" root');
326
- if (!name) return this.#warnTemplate('a "name" element');
327
- if (!button) return this.#warnTemplate('a "remove" target <button>');
328
- if (removeName === "") {
329
- return this.#warnTemplate('a non-empty aria-label on its "remove" target');
434
+ if (!this.hasListTarget) {
435
+ this.#rows.report('a "list" target to render into');
436
+ return false;
437
+ }
438
+ if (!this.hasItemTemplateTarget) {
439
+ this.#rows.report('an "itemTemplate" target');
440
+ return false;
330
441
  }
331
- name.textContent = file.name;
332
- button.setAttribute("aria-label", fillTemplate(removeName, { name: file.name }));
442
+ const row = this.#rows.instantiate(this.itemTemplateTarget, { name: file.name });
443
+ if (!row) return false;
444
+ const item = row.root;
445
+ writeLabel(row.slots.name, file.name);
446
+ const thumb = row.slots.thumb;
333
447
  let url;
334
448
  if (thumb && file.type.startsWith("image/")) {
335
449
  url = URL.createObjectURL(file);
@@ -339,32 +453,10 @@ var FileDropzoneController = class extends Controller {
339
453
  } else if (thumb) {
340
454
  thumb.hidden = true;
341
455
  }
342
- this.listTarget.appendChild(fragment);
456
+ this.listTarget.appendChild(item);
343
457
  this.#entries.push({ file, item, url });
344
458
  return true;
345
459
  }
346
- /** Resolves one declared part inside a cloned item template. */
347
- #slot(fragment, name) {
348
- return fragment.querySelector(`[data-stimeo--file-dropzone-target~="${name}"]`);
349
- }
350
- /**
351
- * Reports an unusable item template to the author, once per connection.
352
- *
353
- * The addition itself stays a no-op — nothing about the selection, the native
354
- * input, the announcements, or the events changes. Without this line the only
355
- * symptom is a picker that accepts no file at all, and the causes the Inspector
356
- * cannot see statically (a server-rendered template, a name that renders empty
357
- * from a missing translation) would have no diagnostic anywhere.
358
- */
359
- #warnTemplate(missing) {
360
- if (!this.#warnedTemplate) {
361
- this.#warnedTemplate = true;
362
- console.warn(
363
- `Stimeo UI: "${this.identifier}" added no file because its item template lacks ${missing}.`
364
- );
365
- }
366
- return false;
367
- }
368
460
  /** Removes entry `index`, revokes its preview, and re-homes focus. */
369
461
  #removeAt(index) {
370
462
  const entry = this.#entries[index];
@@ -473,7 +565,7 @@ var FileDropzoneController = class extends Controller {
473
565
  this.#entries.length = 0;
474
566
  this.#syncInput();
475
567
  this.#endDrag();
476
- if (this.hasZoneTarget) this.zoneTarget.removeAttribute(INVALID_ATTRIBUTE);
568
+ if (this.hasZoneTarget) this.zoneTarget.removeAttribute(this.#invalidAttribute);
477
569
  }
478
570
  /** Releases the selection once the disconnect is known to be a real detach. */
479
571
  #teardown() {
@@ -493,8 +585,9 @@ var FileDropzoneController = class extends Controller {
493
585
  get #removeButtons() {
494
586
  const buttons = [];
495
587
  for (const entry of this.#entries) {
496
- const button = entry.item.querySelector(
497
- 'button[data-stimeo--file-dropzone-target~="remove"]'
588
+ const button = matchingPart(
589
+ entry.item,
590
+ `button${this.#rows.selector("remove")}`
498
591
  );
499
592
  if (button) buttons.push(button);
500
593
  }