stimeo-ui 0.4.0 → 0.6.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 (173) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/controllers/announcer_controller.js.map +1 -1
  3. package/dist/controllers/aspect_ratio_controller.d.ts +3 -5
  4. package/dist/controllers/aspect_ratio_controller.js +19 -11
  5. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  6. package/dist/controllers/avatar_controller.d.ts +36 -15
  7. package/dist/controllers/avatar_controller.js +195 -40
  8. package/dist/controllers/avatar_controller.js.map +1 -1
  9. package/dist/controllers/breadcrumb_controller.js +5 -1
  10. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  11. package/dist/controllers/carousel_controller.d.ts +12 -3
  12. package/dist/controllers/carousel_controller.js +90 -10
  13. package/dist/controllers/carousel_controller.js.map +1 -1
  14. package/dist/controllers/checkbox_controller.d.ts +31 -5
  15. package/dist/controllers/checkbox_controller.js +136 -25
  16. package/dist/controllers/checkbox_controller.js.map +1 -1
  17. package/dist/controllers/clipboard_controller.js +8 -3
  18. package/dist/controllers/clipboard_controller.js.map +1 -1
  19. package/dist/controllers/collapsible_controller.d.ts +1 -1
  20. package/dist/controllers/collapsible_controller.js +4 -1
  21. package/dist/controllers/collapsible_controller.js.map +1 -1
  22. package/dist/controllers/color_picker_controller.d.ts +9 -3
  23. package/dist/controllers/color_picker_controller.js +41 -11
  24. package/dist/controllers/color_picker_controller.js.map +1 -1
  25. package/dist/controllers/combobox_controller.js.map +1 -1
  26. package/dist/controllers/command_palette_controller.js.map +1 -1
  27. package/dist/controllers/context_menu_controller.d.ts +1 -1
  28. package/dist/controllers/context_menu_controller.js +2 -2
  29. package/dist/controllers/context_menu_controller.js.map +1 -1
  30. package/dist/controllers/countdown_controller.d.ts +5 -3
  31. package/dist/controllers/countdown_controller.js +5 -1
  32. package/dist/controllers/countdown_controller.js.map +1 -1
  33. package/dist/controllers/date_range_picker_controller.d.ts +23 -4
  34. package/dist/controllers/date_range_picker_controller.js +162 -31
  35. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  36. package/dist/controllers/direct_upload_controller.d.ts +1 -1
  37. package/dist/controllers/direct_upload_controller.js +3 -3
  38. package/dist/controllers/direct_upload_controller.js.map +1 -1
  39. package/dist/controllers/empty_state_controller.d.ts +27 -7
  40. package/dist/controllers/empty_state_controller.js +107 -16
  41. package/dist/controllers/empty_state_controller.js.map +1 -1
  42. package/dist/controllers/file_dropzone_controller.d.ts +7 -1
  43. package/dist/controllers/file_dropzone_controller.js +26 -3
  44. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  45. package/dist/controllers/flash_controller.d.ts +25 -5
  46. package/dist/controllers/flash_controller.js +161 -21
  47. package/dist/controllers/flash_controller.js.map +1 -1
  48. package/dist/controllers/focus_controller.d.ts +2 -3
  49. package/dist/controllers/focus_controller.js.map +1 -1
  50. package/dist/controllers/form_validation_controller.js +8 -2
  51. package/dist/controllers/form_validation_controller.js.map +1 -1
  52. package/dist/controllers/frame_loading_controller.d.ts +19 -2
  53. package/dist/controllers/frame_loading_controller.js +94 -22
  54. package/dist/controllers/frame_loading_controller.js.map +1 -1
  55. package/dist/controllers/highlight_controller.d.ts +8 -4
  56. package/dist/controllers/highlight_controller.js +38 -1
  57. package/dist/controllers/highlight_controller.js.map +1 -1
  58. package/dist/controllers/hover_card_controller.d.ts +2 -2
  59. package/dist/controllers/hover_card_controller.js.map +1 -1
  60. package/dist/controllers/idle_controller.d.ts +5 -4
  61. package/dist/controllers/idle_controller.js +39 -6
  62. package/dist/controllers/idle_controller.js.map +1 -1
  63. package/dist/controllers/listbox_controller.js.map +1 -1
  64. package/dist/controllers/local_time_controller.js +2 -0
  65. package/dist/controllers/local_time_controller.js.map +1 -1
  66. package/dist/controllers/masonry_controller.d.ts +1 -1
  67. package/dist/controllers/masonry_controller.js +1 -1
  68. package/dist/controllers/masonry_controller.js.map +1 -1
  69. package/dist/controllers/menubar_controller.js +5 -3
  70. package/dist/controllers/menubar_controller.js.map +1 -1
  71. package/dist/controllers/meter_controller.js +3 -1
  72. package/dist/controllers/meter_controller.js.map +1 -1
  73. package/dist/controllers/multi_select_controller.d.ts +46 -12
  74. package/dist/controllers/multi_select_controller.js +460 -151
  75. package/dist/controllers/multi_select_controller.js.map +1 -1
  76. package/dist/controllers/network_status_controller.d.ts +15 -13
  77. package/dist/controllers/network_status_controller.js +1 -3
  78. package/dist/controllers/network_status_controller.js.map +1 -1
  79. package/dist/controllers/number_input_controller.d.ts +29 -3
  80. package/dist/controllers/number_input_controller.js +455 -64
  81. package/dist/controllers/number_input_controller.js.map +1 -1
  82. package/dist/controllers/overflow_menu_controller.js +5 -1
  83. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  84. package/dist/controllers/pagination_controller.js +38 -1
  85. package/dist/controllers/pagination_controller.js.map +1 -1
  86. package/dist/controllers/password_strength_controller.d.ts +4 -4
  87. package/dist/controllers/password_strength_controller.js +21 -3
  88. package/dist/controllers/password_strength_controller.js.map +1 -1
  89. package/dist/controllers/persist_controller.d.ts +4 -4
  90. package/dist/controllers/persist_controller.js +24 -5
  91. package/dist/controllers/persist_controller.js.map +1 -1
  92. package/dist/controllers/pointer_drag_controller.js +10 -0
  93. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  94. package/dist/controllers/popover_controller.d.ts +2 -2
  95. package/dist/controllers/popover_controller.js.map +1 -1
  96. package/dist/controllers/portal_controller.d.ts +1 -2
  97. package/dist/controllers/portal_controller.js +10 -0
  98. package/dist/controllers/portal_controller.js.map +1 -1
  99. package/dist/controllers/progress_controller.d.ts +1 -1
  100. package/dist/controllers/progress_controller.js +7 -3
  101. package/dist/controllers/progress_controller.js.map +1 -1
  102. package/dist/controllers/radio_group_controller.d.ts +41 -15
  103. package/dist/controllers/radio_group_controller.js +540 -56
  104. package/dist/controllers/radio_group_controller.js.map +1 -1
  105. package/dist/controllers/range_slider_controller.d.ts +23 -0
  106. package/dist/controllers/range_slider_controller.js +385 -94
  107. package/dist/controllers/range_slider_controller.js.map +1 -1
  108. package/dist/controllers/rating_controller.d.ts +36 -31
  109. package/dist/controllers/rating_controller.js +274 -89
  110. package/dist/controllers/rating_controller.js.map +1 -1
  111. package/dist/controllers/relative_time_controller.js +2 -0
  112. package/dist/controllers/relative_time_controller.js.map +1 -1
  113. package/dist/controllers/resizable_controller.js +33 -0
  114. package/dist/controllers/resizable_controller.js.map +1 -1
  115. package/dist/controllers/roving_controller.d.ts +4 -0
  116. package/dist/controllers/roving_controller.js +60 -5
  117. package/dist/controllers/roving_controller.js.map +1 -1
  118. package/dist/controllers/scroll_area_controller.d.ts +7 -1
  119. package/dist/controllers/scroll_area_controller.js +155 -23
  120. package/dist/controllers/scroll_area_controller.js.map +1 -1
  121. package/dist/controllers/scroll_visibility_controller.js +33 -0
  122. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  123. package/dist/controllers/separator_controller.js +13 -17
  124. package/dist/controllers/separator_controller.js.map +1 -1
  125. package/dist/controllers/skeleton_controller.d.ts +2 -2
  126. package/dist/controllers/skeleton_controller.js +71 -3
  127. package/dist/controllers/skeleton_controller.js.map +1 -1
  128. package/dist/controllers/slider_controller.d.ts +17 -1
  129. package/dist/controllers/slider_controller.js +325 -48
  130. package/dist/controllers/slider_controller.js.map +1 -1
  131. package/dist/controllers/spinner_controller.d.ts +15 -10
  132. package/dist/controllers/spinner_controller.js +18 -3
  133. package/dist/controllers/spinner_controller.js.map +1 -1
  134. package/dist/controllers/step_indicator_controller.d.ts +1 -1
  135. package/dist/controllers/step_indicator_controller.js +3 -1
  136. package/dist/controllers/step_indicator_controller.js.map +1 -1
  137. package/dist/controllers/stepper_controller.js +2 -0
  138. package/dist/controllers/stepper_controller.js.map +1 -1
  139. package/dist/controllers/switch_controller.d.ts +12 -8
  140. package/dist/controllers/switch_controller.js +162 -18
  141. package/dist/controllers/switch_controller.js.map +1 -1
  142. package/dist/controllers/tags_input_controller.d.ts +41 -11
  143. package/dist/controllers/tags_input_controller.js +356 -120
  144. package/dist/controllers/tags_input_controller.js.map +1 -1
  145. package/dist/controllers/textarea_autosize_controller.js +1 -1
  146. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  147. package/dist/controllers/time_picker_controller.d.ts +42 -10
  148. package/dist/controllers/time_picker_controller.js +296 -104
  149. package/dist/controllers/time_picker_controller.js.map +1 -1
  150. package/dist/controllers/toggle_group_controller.d.ts +39 -13
  151. package/dist/controllers/toggle_group_controller.js +378 -55
  152. package/dist/controllers/toggle_group_controller.js.map +1 -1
  153. package/dist/controllers/toolbar_controller.js +5 -3
  154. package/dist/controllers/toolbar_controller.js.map +1 -1
  155. package/dist/controllers/tooltip_controller.d.ts +2 -2
  156. package/dist/controllers/tooltip_controller.js.map +1 -1
  157. package/dist/controllers/transition_controller.d.ts +2 -3
  158. package/dist/controllers/transition_controller.js.map +1 -1
  159. package/dist/controllers/tree_view_controller.d.ts +1 -2
  160. package/dist/controllers/tree_view_controller.js +24 -4
  161. package/dist/controllers/tree_view_controller.js.map +1 -1
  162. package/dist/index.js +3811 -1048
  163. package/dist/index.js.map +1 -1
  164. package/dist/inspector/cli.d.ts +92 -4
  165. package/dist/inspector/cli.js +174 -6
  166. package/dist/inspector/cli.js.map +1 -1
  167. package/dist/inspector/cli_bin.js +234 -8
  168. package/dist/inspector/cli_bin.js.map +1 -1
  169. package/dist/inspector/examples.json +24 -24
  170. package/dist/inspector/manifest.json +573 -59
  171. package/dist/positioning/index.d.ts +2 -2
  172. package/dist/positioning/index.js.map +1 -1
  173. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/coerce.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/color_picker_controller.ts"],"names":[],"mappings":";;;;;AAiBO,SAAS,MAAM,OAAA,EAA2B;AAC/C,EAAA,OAAO,MAAA,CAAO,gBAAA,CAAiB,OAAO,CAAA,CAAE,SAAA,KAAc,KAAA;AACxD;;;AC6CO,SAAS,eAAA,CAAgB,KAAa,OAAA,EAA0B;AACrE,EAAA,IAAI,GAAA,KAAQ,YAAA,IAAgB,GAAA,KAAQ,WAAA,EAAa,OAAO,GAAA;AACxD,EAAA,IAAI,CAAC,KAAA,CAAM,OAAO,CAAA,EAAG,OAAO,GAAA;AAC5B,EAAA,OAAO,GAAA,KAAQ,eAAe,WAAA,GAAc,YAAA;AAC9C;AA2BO,SAAS,oBAAA,CACd,KAAA,EACA,KAAA,GAAkC,EAAC,EAC1B;AACT,EAAA,IAAI,CAAC,KAAA,CAAM,GAAA,CAAI,UAAA,CAAW,OAAO,GAAG,OAAO,KAAA;AAC3C,EAAA,OACG,KAAA,CAAM,MAAA,IAAU,CAAC,KAAA,CAAM,QAAA,CAAS,KAAK,CAAA,IACrC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,KACvC,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,QAAA,CAAS,MAAM,CAAA,IACvC,KAAA,CAAM,QAAA,IAAY,CAAC,KAAA,CAAM,QAAA,CAAS,OAAO,CAAA;AAE9C;;;AC3FO,SAAS,eAAe,GAAA,EAAwD;AACrF,EAAA,IAAI,QAAQ,IAAA,IAAQ,GAAA,KAAQ,MAAA,IAAa,GAAA,KAAQ,IAAI,OAAO,IAAA;AAC5D,EAAA,MAAM,QAAQ,OAAO,GAAA,KAAQ,QAAA,GAAW,GAAA,GAAM,OAAO,GAAG,CAAA;AACxD,EAAA,OAAO,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,GAAI,KAAA,GAAQ,IAAA;AAC1C;;;ACgCO,IAAM,qBAAN,MAAyB;AAAA,EACrB,IAAA;AAAA,EACT,OAAA,GAAU,KAAA;AAAA,EACV,OAAA,GAAU,KAAA;AAAA,EACV,WAAA,GAAc,CAAA;AAAA;AAAA,EAGd,YAAY,GAAA,EAAiB;AAC3B,IAAA,IAAA,CAAK,IAAA,GAAO,GAAA;AAAA,EACd;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA,EAGA,MAAA,GAAe;AACb,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,IAAA,IAAA,CAAK,WAAA,IAAe,CAAA;AAAA,EACtB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,IAAI,CAAC,IAAA,CAAK,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACnC,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AACf,IAAA,MAAM,aAAa,IAAA,CAAK,WAAA;AACxB,IAAA,cAAA,CAAe,MAAM;AAEnB,MAAA,IAAI,UAAA,KAAe,KAAK,WAAA,IAAe,CAAC,KAAK,OAAA,IAAW,CAAC,KAAK,OAAA,EAAS;AACvE,MAAA,IAAA,CAAK,OAAA,GAAU,KAAA;AACf,MAAA,IAAA,CAAK,IAAA,EAAK;AAAA,IACZ,CAAC,CAAA;AAAA,EACH;AACF,CAAA;;;AC/EA,IAAM,cAAA,GAAiB,gBAAA;AAMvB,IAAM,aAAA,GAAmD;AAAA,EACvD,GAAA,EAAK,CAAC,CAAA,EAAG,GAAG,CAAA;AAAA,EACZ,UAAA,EAAY,CAAC,CAAA,EAAG,GAAG,CAAA;AAAA,EACnB,SAAA,EAAW,CAAC,CAAA,EAAG,GAAG,CAAA;AAAA,EAClB,KAAA,EAAO,CAAC,CAAA,EAAG,GAAG;AAChB,CAAA;AAuDO,IAAM,qBAAA,GAAN,cAAoC,UAAA,CAAwB;AAAA,EACjE,OAAgB,OAAA,GAAU,CAAC,QAAA,EAAU,KAAA,EAAO,WAAW,OAAO,CAAA;AAAA,EAC9D,OAAgB,MAAA,GAAS;AAAA,IACvB,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAQ,SAAS,SAAA,EAAU;AAAA,IAC1C,KAAA,EAAO,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA,EAAM;AAAA,IACvC,YAAA,EAAc,EAAE,IAAA,EAAM,OAAA,EAAS,SAAS,KAAA;AAAM,GAChD;AAAA,EACA,OAAO,OAAA,GAAU,CAAC,YAAA,EAAc,aAAa,eAAe,CAAA;AAAA,EAC5D,OAAO,MAAA,GAAS,CAAC,QAAQ,CAAA;AAAA;AAAA,EAYzB,IAAI,SAAA,GAAqB;AACvB,IAAA,OAAO,IAAA,CAAK,iBAAA,IAAqB,KAAA,CAAM,IAAA,CAAK,OAAO,CAAA;AAAA,EACrD;AAAA;AAAA,EAGA,MAAA,GAAe,EAAE,GAAA,EAAK,CAAA,EAAG,YAAY,CAAA,EAAG,SAAA,EAAW,CAAA,EAAG,KAAA,EAAO,GAAA,EAAI;AAAA;AAAA,EAEjE,UAAA,GAAqC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO5B,QAAA,GAAW,IAAI,kBAAA,CAAmB,MAAM;AAC/C,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf,CAAC,CAAA;AAAA,EAEQ,OAAA,GAAgB;AACvB,IAAA,IAAA,CAAK,SAAS,QAAA,EAAS;AACvB,IAAA,MAAM,MAAA,GAAS,SAAA,CAAU,IAAA,CAAK,UAAU,CAAA;AAIxC,IAAA,IAAI,MAAA,EAAQ,IAAA,CAAK,MAAA,GAAS,IAAA,CAAK,UAAA,GAAa,SAAS,EAAE,GAAG,MAAA,EAAQ,KAAA,EAAO,GAAA,EAAI;AAC7E,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,SAAS,MAAA,EAAO;AACrB,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AACvB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAAA,EACpB;AAAA;AAAA,EAGA,iBAAA,GAA0B;AACxB,IAAA,IAAA,CAAK,SAAS,QAAA,EAAS;AAAA,EACzB;AAAA;AAAA,EAGA,UAAU,KAAA,EAA4B;AACpC,IAAA,IAAI,oBAAA,CAAqB,KAAK,CAAA,EAAG;AACjC,IAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AACrB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA;AACtC,IAAA,IAAI,CAAC,OAAA,EAAS;AAEd,IAAA,MAAM,CAAC,GAAA,EAAK,GAAG,IAAI,IAAA,CAAK,QAAA,CAAS,QAAQ,OAAO,CAAA;AAChD,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA;AACjC,IAAA,IAAI,IAAA,GAAsB,IAAA;AAG1B,IAAA,QAAQ,IAAA,CAAK,YAAY,eAAA,CAAgB,KAAA,CAAM,KAAK,IAAA,CAAK,OAAO,CAAA,GAAI,KAAA,CAAM,GAAA;AAAK,MAC7E,KAAK,YAAA;AAAA,MACL,KAAK,SAAA;AACH,QAAA,IAAA,GAAO,KAAA,GAAQ,CAAA;AACf,QAAA;AAAA,MACF,KAAK,WAAA;AAAA,MACL,KAAK,WAAA;AACH,QAAA,IAAA,GAAO,KAAA,GAAQ,CAAA;AACf,QAAA;AAAA,MACF,KAAK,QAAA;AACH,QAAA,IAAA,GAAO,KAAA,GAAQ,EAAA;AACf,QAAA;AAAA,MACF,KAAK,UAAA;AACH,QAAA,IAAA,GAAO,KAAA,GAAQ,EAAA;AACf,QAAA;AAAA,MACF,KAAK,MAAA;AACH,QAAA,IAAA,GAAO,GAAA;AACP,QAAA;AAAA,MACF,KAAK,KAAA;AACH,QAAA,IAAA,GAAO,GAAA;AACP,QAAA;AAAA,MACF;AACE,QAAA;AAAA;AAEJ,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,IAAA,CAAK,WAAA,CAAY,OAAA,EAAS,IAAA,EAAM,GAAA,EAAK,GAAG,CAAA;AAAA,EAC1C;AAAA;AAAA,EAGA,cAAc,KAAA,EAA2B;AACvC,IAAA,MAAM,SAAS,KAAA,CAAM,aAAA;AACrB,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA;AACtC,IAAA,IAAI,CAAC,OAAA,EAAS;AACd,IAAA,KAAA,CAAM,cAAA,EAAe;AACrB,IAAA,MAAA,CAAO,KAAA,EAAM;AAEb,IAAA,MAAM,CAAC,GAAA,EAAK,GAAG,IAAI,IAAA,CAAK,QAAA,CAAS,QAAQ,OAAO,CAAA;AAIhD,IAAA,MAAM,WAAW,IAAA,CAAK,SAAA;AACtB,IAAA,MAAM,MAAA,GAAS,CAAC,OAAA,KAA0B;AACxC,MAAA,MAAM,IAAA,GAAO,OAAO,qBAAA,EAAsB;AAC1C,MAAA,IAAI,IAAA,CAAK,UAAU,CAAA,EAAG;AACtB,MAAA,MAAM,MAAA,GAAS,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAA,EAAA,CAAI,OAAA,GAAU,IAAA,CAAK,IAAA,IAAQ,IAAA,CAAK,KAAK,CAAC,CAAA;AAC1E,MAAA,MAAM,QAAA,GAAW,QAAA,GAAW,CAAA,GAAI,MAAA,GAAS,MAAA;AACzC,MAAA,IAAA,CAAK,YAAY,OAAA,EAAS,GAAA,GAAM,YAAY,GAAA,GAAM,GAAA,CAAA,EAAM,KAAK,GAAG,CAAA;AAAA,IAClE,CAAA;AACA,IAAA,MAAA,CAAO,MAAM,OAAO,CAAA;AAEpB,IAAA,IAAA,CAAK,YAAY,KAAA,EAAM;AACvB,IAAA,MAAM,KAAA,GAAQ,IAAI,eAAA,EAAgB;AAClC,IAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,IAAA,MAAM,MAAA,GAAS,CAAC,IAAA,KAA6B,MAAA,CAAO,KAAK,OAAO,CAAA;AAChE,IAAA,MAAM,OAAO,MAAY;AACvB,MAAA,KAAA,CAAM,KAAA,EAAM;AACZ,MAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAAA,IACpB,CAAA;AACA,IAAA,QAAA,CAAS,iBAAiB,aAAA,EAAe,MAAA,EAAQ,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AACzE,IAAA,QAAA,CAAS,iBAAiB,WAAA,EAAa,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AACrE,IAAA,QAAA,CAAS,iBAAiB,eAAA,EAAiB,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAA,CAAM,QAAQ,CAAA;AAAA,EAC3E;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,MAAM,MAAA,GAAS,SAAA,CAAU,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AAC7C,IAAA,IAAI,CAAC,MAAA,EAAQ;AAEX,MAAA,IAAA,CAAK,SAAA,CAAU,KAAA,GAAQ,IAAA,CAAK,UAAA,EAAW;AACvC,MAAA;AAAA,IACF;AACA,IAAA,IAAA,CAAK,MAAA,GAAS,KAAK,UAAA,GAAa,MAAA,GAAS,EAAE,GAAG,MAAA,EAAQ,OAAO,GAAA,EAAI;AACjE,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,WAAA,CAAY,OAAA,EAAkB,GAAA,EAAa,GAAA,EAAa,GAAA,EAAmB;AACzE,IAAA,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA,GAAI,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,GAAG,CAAC,CAAC,CAAA;AACnE,IAAA,IAAA,CAAK,OAAA,EAAQ;AAAA,EACf;AAAA;AAAA,EAGA,OAAA,GAAgB;AACd,IAAA,KAAA,MAAW,MAAA,IAAU,KAAK,aAAA,EAAe;AACvC,MAAA,MAAM,OAAA,GAAU,IAAA,CAAK,UAAA,CAAW,MAAM,CAAA;AACtC,MAAA,IAAI,CAAC,OAAA,EAAS;AACd,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,OAAO,CAAA;AACjC,MAAA,MAAA,CAAO,YAAA,CAAa,eAAA,EAAiB,MAAA,CAAO,KAAK,CAAC,CAAA;AAClD,MAAA,MAAA,CAAO,YAAA,CAAa,gBAAA,EAAkB,SAAA,CAAU,OAAA,EAAS,KAAK,CAAC,CAAA;AAAA,IACjE;AAEA,IAAA,MAAM,GAAA,GAAM,KAAK,UAAA,EAAW;AAC5B,IAAA,IAAI,IAAA,CAAK,YAAA,EAAc,IAAA,CAAK,SAAA,CAAU,KAAA,GAAQ,GAAA;AAC9C,IAAA,KAAA,MAAW,KAAA,IAAS,IAAA,CAAK,YAAA,EAAc,KAAA,CAAM,KAAA,GAAQ,GAAA;AACrD,IAAA,KAAA,MAAW,WAAW,IAAA,CAAK,cAAA,UAAwB,KAAA,CAAM,WAAA,CAAY,gBAAgB,GAAG,CAAA;AACxF,IAAA,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,WAAA,CAAY,cAAA,EAAgB,GAAG,CAAA;AAElD,IAAA,MAAM,GAAA,GAAM,QAAA,CAAS,IAAA,CAAK,MAAA,CAAO,GAAA,EAAK,KAAK,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,MAAA,CAAO,SAAS,CAAA;AACnF,IAAA,IAAA,CAAK,SAAS,QAAA,EAAU;AAAA,MACtB,MAAA,EAAQ,EAAE,KAAA,EAAO,GAAA,EAAK,IAAA,EAAM,EAAE,GAAG,GAAA,EAAK,CAAA,EAAG,IAAA,CAAK,MAAA,CAAO,KAAA,GAAQ,KAAI;AAAE,KACpE,CAAA;AAAA,EACH;AAAA;AAAA,EAGA,UAAA,GAAqB;AACnB,IAAA,MAAM,GAAA,GAAM,QAAA,CAAS,IAAA,CAAK,MAAA,CAAO,GAAA,EAAK,KAAK,MAAA,CAAO,UAAA,EAAY,IAAA,CAAK,MAAA,CAAO,SAAS,CAAA;AACnF,IAAA,MAAM,IAAA,GAAO,CAAA,CAAA,EAAI,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,CAAC,CAAC,CAAA,CAAA;AACxD,IAAA,IAAI,CAAC,IAAA,CAAK,UAAA,EAAY,OAAO,IAAA;AAC7B,IAAA,OAAO,CAAA,EAAG,IAAI,CAAA,EAAG,IAAA,CAAK,IAAA,CAAK,KAAA,CAAO,IAAA,CAAK,MAAA,CAAO,KAAA,GAAQ,GAAA,GAAO,GAAG,CAAC,CAAC,CAAA,CAAA;AAAA,EACpE;AAAA;AAAA,EAGA,WAAW,MAAA,EAAqC;AAC9C,IAAA,MAAM,OAAA,GAAU,MAAA,CAAO,YAAA,CAAa,cAAc,CAAA;AAClD,IAAA,OAAO,OAAA,IAAW,OAAA,IAAW,aAAA,GAAiB,OAAA,GAAsB,IAAA;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,QAAA,CAAS,QAAqB,OAAA,EAAoC;AAChE,IAAA,MAAM,CAAC,MAAA,EAAQ,MAAM,CAAA,GAAI,cAAc,OAAO,CAAA;AAC9C,IAAA,OAAO;AAAA,MACL,cAAA,CAAe,MAAA,CAAO,YAAA,CAAa,eAAe,CAAC,CAAA,IAAK,MAAA;AAAA,MACxD,cAAA,CAAe,MAAA,CAAO,YAAA,CAAa,eAAe,CAAC,CAAA,IAAK;AAAA,KAC1D;AAAA,EACF;AACF;AAGA,SAAS,SAAA,CAAU,SAAkB,KAAA,EAAuB;AAC1D,EAAA,MAAM,KAAA,GAAQ,QAAQ,MAAA,CAAO,CAAC,EAAE,WAAA,EAAY,GAAI,OAAA,CAAQ,KAAA,CAAM,CAAC,CAAA;AAC/D,EAAA,MAAM,IAAA,GAAO,OAAA,KAAY,KAAA,GAAQ,SAAA,GAAY,SAAA;AAC7C,EAAA,OAAO,CAAA,EAAG,KAAK,CAAA,CAAA,EAAI,KAAK,IAAI,IAAI,CAAA,CAAA;AAClC;AAGA,SAAS,KAAK,KAAA,EAAuB;AACnC,EAAA,OAAO,KAAK,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,GAAA,EAAK,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,CAAC,CAAC,CAAA,CAChD,QAAA,CAAS,EAAE,CAAA,CACX,QAAA,CAAS,GAAG,GAAG,CAAA;AACpB;AAGA,SAAS,QAAA,CAAS,CAAA,EAAW,CAAA,EAAW,CAAA,EAAgD;AACtF,EAAA,MAAM,MAAM,CAAA,GAAI,GAAA;AAChB,EAAA,MAAM,QAAQ,CAAA,GAAI,GAAA;AAClB,EAAA,MAAM,KAAK,CAAA,GAAI,IAAA,CAAK,IAAI,CAAA,GAAI,KAAA,GAAQ,CAAC,CAAA,IAAK,GAAA;AAC1C,EAAA,MAAM,EAAA,GAAA,CAAQ,CAAA,GAAI,GAAA,GAAO,GAAA,IAAO,GAAA,GAAO,EAAA;AACvC,EAAA,MAAM,IAAI,CAAA,IAAK,CAAA,GAAI,KAAK,GAAA,CAAK,EAAA,GAAK,IAAK,CAAC,CAAA,CAAA;AACxC,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,IAAI,EAAA,IAAM,CAAA,IAAK,EAAA,GAAK,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,OAAA,IAClC,EAAA,GAAK,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,OAAA,IAC5B,EAAA,GAAK,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,OAAA,IAC5B,EAAA,GAAK,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,OAAA,IAC5B,EAAA,GAAK,CAAA,EAAG,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA,GAAI,CAAC,CAAA,EAAG,CAAA,EAAG,CAAC,CAAA;AAAA,OAChC,CAAC,GAAG,CAAA,EAAG,CAAC,IAAI,CAAC,CAAA,EAAG,GAAG,CAAC,CAAA;AACzB,EAAA,MAAM,CAAA,GAAI,QAAQ,CAAA,GAAI,CAAA;AACtB,EAAA,OAAO;AAAA,IACL,CAAA,EAAG,IAAA,CAAK,KAAA,CAAA,CAAO,CAAA,GAAI,KAAK,GAAG,CAAA;AAAA,IAC3B,CAAA,EAAG,IAAA,CAAK,KAAA,CAAA,CAAO,CAAA,GAAI,KAAK,GAAG,CAAA;AAAA,IAC3B,CAAA,EAAG,IAAA,CAAK,KAAA,CAAA,CAAO,CAAA,GAAI,KAAK,GAAG;AAAA,GAC7B;AACF;AAGA,SAAS,QAAA,CAAS,CAAA,EAAW,CAAA,EAAW,CAAA,EAAgD;AACtF,EAAA,MAAM,KAAK,CAAA,GAAI,GAAA;AACf,EAAA,MAAM,KAAK,CAAA,GAAI,GAAA;AACf,EAAA,MAAM,KAAK,CAAA,GAAI,GAAA;AACf,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,IAAI,EAAE,CAAA;AAC/B,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,CAAI,EAAA,EAAI,IAAI,EAAE,CAAA;AAC/B,EAAA,MAAM,QAAQ,GAAA,GAAM,GAAA;AACpB,EAAA,MAAM,CAAA,GAAA,CAAK,MAAM,GAAA,IAAO,CAAA;AAExB,EAAA,IAAI,CAAA,GAAI,CAAA;AACR,EAAA,IAAI,UAAU,CAAA,EAAG;AACf,IAAA,IAAI,GAAA,KAAQ,EAAA,EAAI,CAAA,GAAA,CAAM,EAAA,GAAK,MAAM,KAAA,GAAS,CAAA;AAAA,SAAA,IACjC,GAAA,KAAQ,EAAA,EAAI,CAAA,GAAA,CAAK,EAAA,GAAK,MAAM,KAAA,GAAQ,CAAA;AAAA,SACxC,CAAA,GAAA,CAAK,EAAA,GAAK,EAAA,IAAM,KAAA,GAAQ,CAAA;AAC7B,IAAA,CAAA,GAAA,CAAK,CAAA,GAAI,KAAK,GAAA,IAAO,GAAA;AAAA,EACvB;AACA,EAAA,MAAM,CAAA,GAAI,KAAA,KAAU,CAAA,GAAI,CAAA,GAAI,KAAA,IAAS,IAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAI,CAAA,GAAI,CAAC,CAAA,CAAA;AAC3D,EAAA,OAAO,EAAE,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,CAAC,GAAG,CAAA,EAAG,IAAA,CAAK,KAAA,CAAM,CAAA,GAAI,GAAG,CAAA,EAAG,CAAA,EAAG,KAAK,KAAA,CAAM,CAAA,GAAI,GAAG,CAAA,EAAE;AAC5E;AAMA,SAAS,UAAU,KAAA,EAA4B;AAC7C,EAAA,MAAM,MAAM,KAAA,CAAM,IAAA,EAAK,CAAE,OAAA,CAAQ,MAAM,EAAE,CAAA;AACzC,EAAA,IAAI,CAAC,sDAAA,CAAuD,IAAA,CAAK,GAAG,GAAG,OAAO,IAAA;AAG9E,EAAA,MAAM,OACJ,GAAA,CAAI,MAAA,IAAU,CAAA,GACV,GAAA,CACG,MAAM,EAAE,CAAA,CACR,GAAA,CAAI,CAAC,SAAS,IAAA,GAAO,IAAI,CAAA,CACzB,IAAA,CAAK,EAAE,CAAA,GACV,GAAA;AAEN,EAAA,MAAM,CAAA,GAAI,OAAO,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AAC9C,EAAA,MAAM,CAAA,GAAI,OAAO,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AAC9C,EAAA,MAAM,CAAA,GAAI,OAAO,QAAA,CAAS,IAAA,CAAK,MAAM,CAAA,EAAG,CAAC,GAAG,EAAE,CAAA;AAC9C,EAAA,MAAM,CAAA,GAAI,IAAA,CAAK,MAAA,KAAW,CAAA,GAAI,MAAA,CAAO,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA,EAAG,EAAE,CAAA,GAAI,GAAA;AAEtE,EAAA,MAAM,EAAE,GAAG,CAAA,EAAG,CAAA,KAAM,QAAA,CAAS,CAAA,EAAG,GAAG,CAAC,CAAA;AACpC,EAAA,OAAO,EAAE,GAAA,EAAK,CAAA,EAAG,UAAA,EAAY,CAAA,EAAG,SAAA,EAAW,CAAA,EAAG,KAAA,EAAO,IAAA,CAAK,KAAA,CAAO,CAAA,GAAI,GAAA,GAAO,GAAG,CAAA,EAAE;AACnF","file":"color_picker_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Numeric coercion shared by the value-bearing controllers (progress, meter,\n * color-picker).\n *\n * Stimulus already coerces numeric action params to numbers, but a value can also\n * arrive as a string — via a `*:set` CustomEvent `detail`, or an action param\n * whose attribute does not look numeric. Centralizing the parse keeps `setValue`\n * tolerant of either form while rejecting anything that is not a finite number.\n */\n\n/**\n * Coerces `raw` to a finite number, or returns `null` when it is absent, empty,\n * or not parseable. Empty strings are treated as \"no value\" rather than `0`, so a\n * stray blank param cannot silently reset the value.\n */\nexport function toFiniteNumber(raw: number | string | null | undefined): number | null {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n","/**\n * Collapses many target callbacks from one DOM mutation into a single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element, so\n * replacing a list of N options delivers N callbacks — but the useful unit of\n * work is \"reconcile against the DOM that resulted\", once, after the batch has\n * settled. Every controller that owns a reconcilable target set needs the same\n * shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers the *initial* target callbacks\n * ahead of `connect()`. Reconciling there would compute a fallback against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { toFiniteNumber } from \"../utils/coerce\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** CSS custom property exposing the current color to consumer CSS. */\nconst COLOR_PROPERTY = \"--stimeo-color\";\n\n/** A color channel slider, identified by its `data-channel` attribute. */\ntype Channel = \"hue\" | \"saturation\" | \"lightness\" | \"alpha\";\n\n/** Default `[min, max]` per channel when the slider omits aria-valuemin/max. */\nconst CHANNEL_RANGE: Record<Channel, [number, number]> = {\n hue: [0, 360],\n saturation: [0, 100],\n lightness: [0, 100],\n alpha: [0, 100],\n};\n\n/** Internal color model: HSL plus an alpha percentage (0–100). */\ninterface Hsla {\n hue: number;\n saturation: number;\n lightness: number;\n alpha: number;\n}\n\n/**\n * Headless, accessible **Color Picker** behavior.\n *\n * Markup contract (identifier: `stimeo--color-picker`):\n * <div data-controller=\"stimeo--color-picker\"\n * data-stimeo--color-picker-value-value=\"#3366cc\">\n * <div role=\"slider\" aria-label=\"Hue\" data-channel=\"hue\" tabindex=\"0\"\n * aria-valuemin=\"0\" aria-valuemax=\"360\" aria-valuenow=\"210\"\n * data-stimeo--color-picker-target=\"slider\"\n * data-action=\"keydown->stimeo--color-picker#onKeydown\n * pointerdown->stimeo--color-picker#onPointerDown\"></div>\n * <!-- saturation / lightness / alpha sliders share the same contract -->\n * <input type=\"text\" aria-label=\"Hex color\"\n * data-stimeo--color-picker-target=\"hex\"\n * data-action=\"change->stimeo--color-picker#onHexInput\" />\n * <div data-stimeo--color-picker-target=\"preview\" aria-hidden=\"true\"></div>\n * <input type=\"hidden\" data-stimeo--color-picker-target=\"field\" />\n * </div>\n *\n * Decomposes color selection into independent APG **Slider** channels (hue,\n * saturation, lightness, optional alpha) instead of a 2-D palette, so every\n * adjustment is keyboard- and screen-reader-operable. Each slider exposes\n * `aria-valuenow` and a human-readable `aria-valuetext` (e.g. \"Hue 210 degrees\");\n * the hex input stays two-way synced; the current color is published on the\n * preview and root as the `--stimeo-color` custom property and mirrored into a\n * hidden form field.\n *\n * @remarks\n * Behavior only — the swatch/gradient visuals are the consumer's CSS/canvas, fed\n * by `--stimeo-color`. Only the consumer knows whether a channel track mirrors\n * under RTL: set `logicalTrack` to declare that it does, and the pointer mapping\n * and horizontal arrow pair follow the writing direction. Left unset, nothing\n * here reads `direction`. A gradient has no logical `to` keyword, so mirroring\n * one means swapping `to right`/`to left` under a `:dir(rtl)` selector.\n *\n * Pointer-drag listeners on `document` are bound to an\n * {@link AbortController} and released on drag end and on `disconnect()` (Turbo\n * navigation included). Color is fully reconstructable from the `value` (hex), so\n * there is no transient state to restore after a Turbo cache/morph.\n *\n * The internal model is integer HSL(A), so a hex → HSL → hex round-trip is not\n * exactly bijective: a typed hex can normalize to a near (not identical) value\n * once the HSL sliders are touched. This keeps the model small and zero-dep; use a\n * dedicated color library on the consumer side if exact hex preservation matters.\n */\nexport class ColorPickerController extends Controller<HTMLElement> {\n static override targets = [\"slider\", \"hex\", \"preview\", \"field\"];\n static override values = {\n value: { type: String, default: \"#000000\" },\n alpha: { type: Boolean, default: false },\n logicalTrack: { type: Boolean, default: false },\n };\n static actions = [\"onHexInput\", \"onKeydown\", \"onPointerDown\"] as const;\n static events = [\"change\"] as const;\n\n declare readonly sliderTargets: HTMLElement[];\n declare readonly hexTarget: HTMLInputElement;\n declare readonly hasHexTarget: boolean;\n declare readonly previewTargets: HTMLElement[];\n declare readonly fieldTargets: HTMLInputElement[];\n declare valueValue: string;\n declare alphaValue: boolean;\n declare logicalTrackValue: boolean;\n\n /** Whether the consumer declared a mirroring track and the direction mirrors it. */\n get #mirrored(): boolean {\n return this.logicalTrackValue && isRtl(this.element);\n }\n\n /** The current color in the editing model. */\n #color: Hsla = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };\n /** Aborts in-progress pointer-drag listeners on drag end / teardown. */\n #dragAbort: AbortController | null = null;\n\n /** Seeds the model from the initial hex value and renders every surface. */\n /**\n * Collapses a morph that swaps render inputs into one repaint, and refuses the\n * pass Stimulus delivers before `connect()`.\n */\n readonly #repaint = new MicrotaskCoalescer(() => {\n this.#render();\n });\n\n override connect(): void {\n this.#repaint.activate();\n const parsed = hexToHsla(this.valueValue);\n // When alpha is disabled, drop any alpha carried by an `#RRGGBBAA` value so\n // the model stays opaque — otherwise `hexString()` would emit `#RRGGBB`\n // while `change` reported `rgba.a < 1`. Mirrors `onHexInput()`.\n if (parsed) this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };\n this.#render();\n }\n\n /** Cancels any active pointer drag so document listeners never leak. */\n override disconnect(): void {\n this.#repaint.cancel();\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n }\n\n /** Repaints when application code (or a Turbo morph) changes `alpha` at runtime. */\n alphaValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Keyboard stepping on the focused channel slider (APG Slider model). */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const slider = event.currentTarget as HTMLElement;\n const channel = this.#channelOf(slider);\n if (!channel) return;\n\n const [min, max] = this.#rangeOf(slider, channel);\n const value = this.#color[channel];\n let next: number | null = null;\n // On a mirrored track the greater value sits at the visual left, so the\n // horizontal pair trades places; the vertical pair passes through.\n switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n next = value + 1;\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n next = value - 1;\n break;\n case \"PageUp\":\n next = value + 10;\n break;\n case \"PageDown\":\n next = value - 10;\n break;\n case \"Home\":\n next = min;\n break;\n case \"End\":\n next = max;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#setChannel(channel, next, min, max);\n }\n\n /** Begins a pointer drag on a channel slider and tracks movement. */\n onPointerDown(event: PointerEvent): void {\n const slider = event.currentTarget as HTMLElement;\n const channel = this.#channelOf(slider);\n if (!channel) return;\n event.preventDefault();\n slider.focus();\n\n const [min, max] = this.#rangeOf(slider, channel);\n // Resolve the direction once for the whole gesture: reading it per move\n // would query computed style on every frame, and a drag that flipped\n // mid-gesture would be incoherent anyway.\n const mirrored = this.#mirrored;\n const update = (clientX: number): void => {\n const rect = slider.getBoundingClientRect();\n if (rect.width === 0) return;\n const offset = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));\n const fraction = mirrored ? 1 - offset : offset;\n this.#setChannel(channel, min + fraction * (max - min), min, max);\n };\n update(event.clientX);\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n const onMove = (move: PointerEvent): void => update(move.clientX);\n const onUp = (): void => {\n abort.abort();\n this.#dragAbort = null;\n };\n document.addEventListener(\"pointermove\", onMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", onUp, { signal: abort.signal });\n document.addEventListener(\"pointercancel\", onUp, { signal: abort.signal });\n }\n\n /** Parses the hex input on confirm and syncs every channel + surface. */\n onHexInput(): void {\n if (!this.hasHexTarget) return;\n const parsed = hexToHsla(this.hexTarget.value);\n if (!parsed) {\n // Reject invalid input by restoring the last valid hex.\n this.hexTarget.value = this.#hexString();\n return;\n }\n this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };\n this.#render();\n }\n\n /** Clamps and snaps one channel to an integer, then re-renders + emits change. */\n #setChannel(channel: Channel, raw: number, min: number, max: number): void {\n this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));\n this.#render();\n }\n\n /** Reflects the model onto sliders, the hex input, preview, and form field. */\n #render(): void {\n for (const slider of this.sliderTargets) {\n const channel = this.#channelOf(slider);\n if (!channel) continue;\n const value = this.#color[channel];\n slider.setAttribute(\"aria-valuenow\", String(value));\n slider.setAttribute(\"aria-valuetext\", valueText(channel, value));\n }\n\n const hex = this.#hexString();\n if (this.hasHexTarget) this.hexTarget.value = hex;\n for (const field of this.fieldTargets) field.value = hex;\n for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);\n this.element.style.setProperty(COLOR_PROPERTY, hex);\n\n const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);\n this.dispatch(\"change\", {\n detail: { value: hex, rgba: { ...rgb, a: this.#color.alpha / 100 } },\n });\n }\n\n /** The current color as `#RRGGBB`, or `#RRGGBBAA` when alpha is enabled. */\n #hexString(): string {\n const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);\n const base = `#${hex2(rgb.r)}${hex2(rgb.g)}${hex2(rgb.b)}`;\n if (!this.alphaValue) return base;\n return `${base}${hex2(Math.round((this.#color.alpha / 100) * 255))}`;\n }\n\n /** Reads a slider's `data-channel`, if it is a known channel. */\n #channelOf(slider: HTMLElement): Channel | null {\n const channel = slider.getAttribute(\"data-channel\");\n return channel && channel in CHANNEL_RANGE ? (channel as Channel) : null;\n }\n\n /**\n * A slider's `[min, max]` from aria-valuemin/max, falling back per channel.\n * An absent or blank attribute means \"not authored\", not zero — coercing it to\n * a number would pin the channel at `0` and make the per-channel default\n * unreachable.\n */\n #rangeOf(slider: HTMLElement, channel: Channel): [number, number] {\n const [defMin, defMax] = CHANNEL_RANGE[channel];\n return [\n toFiniteNumber(slider.getAttribute(\"aria-valuemin\")) ?? defMin,\n toFiniteNumber(slider.getAttribute(\"aria-valuemax\")) ?? defMax,\n ];\n }\n}\n\n/** Capitalizes a channel name for `aria-valuetext` (e.g. \"Hue\"). */\nfunction valueText(channel: Channel, value: number): string {\n const label = channel.charAt(0).toUpperCase() + channel.slice(1);\n const unit = channel === \"hue\" ? \"degrees\" : \"percent\";\n return `${label} ${value} ${unit}`;\n}\n\n/** Formats a 0–255 byte as a 2-digit lowercase hex pair. */\nfunction hex2(value: number): string {\n return Math.round(Math.min(255, Math.max(0, value)))\n .toString(16)\n .padStart(2, \"0\");\n}\n\n/** Converts HSL (h:0–360, s/l:0–100) to RGB bytes (0–255). */\nfunction hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {\n const sat = s / 100;\n const light = l / 100;\n const c = (1 - Math.abs(2 * light - 1)) * sat;\n const hp = (((h % 360) + 360) % 360) / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r = 0;\n let g = 0;\n let b = 0;\n if (hp >= 0 && hp < 1) [r, g, b] = [c, x, 0];\n else if (hp < 2) [r, g, b] = [x, c, 0];\n else if (hp < 3) [r, g, b] = [0, c, x];\n else if (hp < 4) [r, g, b] = [0, x, c];\n else if (hp < 5) [r, g, b] = [x, 0, c];\n else [r, g, b] = [c, 0, x];\n const m = light - c / 2;\n return {\n r: Math.round((r + m) * 255),\n g: Math.round((g + m) * 255),\n b: Math.round((b + m) * 255),\n };\n}\n\n/** Converts RGB bytes (0–255) to HSL (h:0–360, s/l:0–100). */\nfunction rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {\n const rn = r / 255;\n const gn = g / 255;\n const bn = b / 255;\n const max = Math.max(rn, gn, bn);\n const min = Math.min(rn, gn, bn);\n const delta = max - min;\n const l = (max + min) / 2;\n\n let h = 0;\n if (delta !== 0) {\n if (max === rn) h = ((gn - bn) / delta) % 6;\n else if (max === gn) h = (bn - rn) / delta + 2;\n else h = (rn - gn) / delta + 4;\n h = (h * 60 + 360) % 360;\n }\n const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));\n return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };\n}\n\n/**\n * Parses `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA` into the HSLA model, or\n * returns null when the string is not a valid hex color.\n */\nfunction hexToHsla(input: string): Hsla | null {\n const hex = input.trim().replace(/^#/, \"\");\n if (!/^(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) return null;\n\n // Expand shorthand (#RGB / #RGBA) so every channel is a full byte pair.\n const full =\n hex.length <= 4\n ? hex\n .split(\"\")\n .map((char) => char + char)\n .join(\"\")\n : hex;\n\n const r = Number.parseInt(full.slice(0, 2), 16);\n const g = Number.parseInt(full.slice(2, 4), 16);\n const b = Number.parseInt(full.slice(4, 6), 16);\n const a = full.length === 8 ? Number.parseInt(full.slice(6, 8), 16) : 255;\n\n const { h, s, l } = rgbToHsl(r, g, b);\n return { hue: h, saturation: s, lightness: l, alpha: Math.round((a / 255) * 100) };\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/logical_scroll.ts","../../src/utils/arrow_step.ts","../../src/utils/coerce.ts","../../src/utils/microtask_coalescer.ts","../../src/controllers/color_picker_controller.ts"],"names":[],"mappings":"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","file":"color_picker_controller.js","sourcesContent":["/** Normalized scroll position and maximum distance on one logical axis. */\nexport interface LogicalScrollMetrics {\n position: number;\n max: number;\n}\n\n/**\n * Whether horizontal scrolling on `element` follows right-to-left inline flow.\n *\n * Resolved from the **computed** `direction`, so the authoring contract is the\n * usual `dir=\"rtl\"` (or a stylesheet) on the element or any ancestor.\n *\n * Scope: horizontal writing modes. A vertical writing mode (`writing-mode:\n * vertical-rl`) also inverts the horizontal axis, which this check does not\n * model — vertical writing modes are out of scope for the scroll utilities\n * (their consumers describe axes as horizontal/vertical, not inline/block).\n */\nexport function isRtl(element: Element): boolean {\n return window.getComputedStyle(element).direction === \"rtl\";\n}\n\n/**\n * Returns scroll distance from the logical start edge.\n *\n * CSSOM View exposes standards-mode RTL horizontal offsets as `0` at the inline\n * start (right) and increasingly negative values toward the inline end (left).\n * The normalized position is always clamped to `[0, max]`, which also absorbs\n * Safari's elastic overscroll values.\n */\nexport function logicalScrollMetrics(\n element: HTMLElement,\n horizontal: boolean,\n): LogicalScrollMetrics {\n const max = Math.max(\n 0,\n horizontal\n ? element.scrollWidth - element.clientWidth\n : element.scrollHeight - element.clientHeight,\n );\n const raw = horizontal ? element.scrollLeft : element.scrollTop;\n const position = horizontal && isRtl(element) ? -raw : raw;\n return { position: Math.min(max, Math.max(0, position)), max };\n}\n\n/**\n * Converts a logical start/end delta to the physical value accepted by\n * `Element.scrollBy`.\n */\nexport function physicalScrollDelta(\n element: HTMLElement,\n horizontal: boolean,\n logicalDelta: number,\n): number {\n return horizontal && isRtl(element) ? -logicalDelta : logicalDelta;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/**\n * Numeric coercion shared by the value-bearing controllers (progress, meter,\n * color-picker).\n *\n * Stimulus already coerces numeric action params to numbers, but a value can also\n * arrive as a string — via a `*:set` CustomEvent `detail`, or an action param\n * whose attribute does not look numeric. Centralizing the parse keeps `setValue`\n * tolerant of either form while rejecting anything that is not a finite number.\n */\n\n/**\n * Coerces `raw` to a finite number, or returns `null` when it is absent, empty,\n * or not parseable. Empty strings are treated as \"no value\" rather than `0`, so a\n * stray blank param cannot silently reset the value.\n */\nexport function toFiniteNumber(raw: number | string | null | undefined): number | null {\n if (raw === null || raw === undefined || raw === \"\") return null;\n const value = typeof raw === \"number\" ? raw : Number(raw);\n return Number.isFinite(value) ? value : null;\n}\n","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { isReservedArrowChord, logicalArrowKey } from \"../utils/arrow_step\";\nimport { toFiniteNumber } from \"../utils/coerce\";\nimport { isRtl } from \"../utils/logical_scroll\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\n\n/** CSS custom property exposing the current color to consumer CSS. */\nconst COLOR_PROPERTY = \"--stimeo--color\";\n\n/** A color channel slider, identified by its `data-channel` attribute. */\ntype Channel = \"hue\" | \"saturation\" | \"lightness\" | \"alpha\";\n\n/** Default `[min, max]` per channel when the slider omits aria-valuemin/max. */\nconst CHANNEL_RANGE: Record<Channel, [number, number]> = {\n hue: [0, 360],\n saturation: [0, 100],\n lightness: [0, 100],\n alpha: [0, 100],\n};\n\n/** Internal color model: HSL plus an alpha percentage (0–100). */\ninterface Hsla {\n hue: number;\n saturation: number;\n lightness: number;\n alpha: number;\n}\n\n/**\n * Headless, accessible **Color Picker** behavior.\n *\n * Markup contract (identifier: `stimeo--color-picker`):\n * <div data-controller=\"stimeo--color-picker\"\n * data-stimeo--color-picker-value-value=\"#3366cc\">\n * <div role=\"slider\" aria-label=\"Hue\" data-channel=\"hue\" tabindex=\"0\"\n * aria-valuemin=\"0\" aria-valuemax=\"360\" aria-valuenow=\"210\"\n * data-stimeo--color-picker-target=\"slider\"\n * data-action=\"keydown->stimeo--color-picker#onKeydown\n * pointerdown->stimeo--color-picker#onPointerDown\"></div>\n * <!-- saturation / lightness / alpha sliders share the same contract -->\n * <input type=\"text\" aria-label=\"Hex color\"\n * data-stimeo--color-picker-target=\"hex\"\n * data-action=\"change->stimeo--color-picker#onHexInput\" />\n * <div data-stimeo--color-picker-target=\"preview\" aria-hidden=\"true\"></div>\n * <input type=\"hidden\" data-stimeo--color-picker-target=\"field\" />\n * </div>\n *\n * Decomposes color selection into independent APG **Slider** channels (hue,\n * saturation, lightness, optional alpha) instead of a 2-D palette, so every\n * adjustment is keyboard- and screen-reader-operable. Each slider exposes\n * `aria-valuenow` and a human-readable `aria-valuetext` (e.g. \"Hue 210 degrees\");\n * the hex input stays two-way synced; the current color is published on the\n * preview and root as the `--stimeo--color` custom property and mirrored into a\n * hidden form field.\n *\n * @remarks\n * Behavior only — the swatch/gradient visuals are the consumer's CSS/canvas, fed\n * by `--stimeo--color`. Only the consumer knows whether a channel track mirrors\n * under RTL: set `logicalTrack` to declare that it does, and the pointer mapping\n * and horizontal arrow pair follow the writing direction. Left unset, nothing\n * here reads `direction`. A gradient has no logical `to` keyword, so mirroring\n * one means swapping `to right`/`to left` under a `:dir(rtl)` selector.\n *\n * Pointer-drag listeners on `document` are bound to an\n * {@link AbortController} and released on drag end and on `disconnect()` (Turbo\n * navigation included). Color is fully reconstructable from the `value` (hex), so\n * there is no transient state to restore after a Turbo cache/morph.\n *\n * The internal model is integer HSL(A), so a hex → HSL → hex round-trip is not\n * exactly bijective: a typed hex can normalize to a near (not identical) value\n * once the HSL sliders are touched. This keeps the model small and zero-dep; use a\n * dedicated color library on the consumer side if exact hex preservation matters.\n *\n * A color the user set through a slider or the hex input is reported as\n * `stimeo--color-picker:change`. Toggling `alpha` at runtime can move the\n * committed color without a user edit, and that arrives as\n * `stimeo--color-picker:reconcile` with the same detail. Neither fires on connect.\n */\nexport class ColorPickerController extends Controller<HTMLElement> {\n static override targets = [\"slider\", \"hex\", \"preview\", \"field\"];\n static override values = {\n value: { type: String, default: \"#000000\" },\n alpha: { type: Boolean, default: false },\n logicalTrack: { type: Boolean, default: false },\n };\n static actions = [\"onHexInput\", \"onKeydown\", \"onPointerDown\"] as const;\n static events = [\"change\", \"reconcile\"] as const;\n\n declare readonly sliderTargets: HTMLElement[];\n declare readonly hexTarget: HTMLInputElement;\n declare readonly hasHexTarget: boolean;\n declare readonly previewTargets: HTMLElement[];\n declare readonly fieldTargets: HTMLInputElement[];\n declare valueValue: string;\n declare alphaValue: boolean;\n declare logicalTrackValue: boolean;\n\n /** Whether the consumer declared a mirroring track and the direction mirrors it. */\n get #mirrored(): boolean {\n return this.logicalTrackValue && isRtl(this.element);\n }\n\n /** The current color in the editing model. */\n #color: Hsla = { hue: 0, saturation: 0, lightness: 0, alpha: 100 };\n /** Aborts in-progress pointer-drag listeners on drag end / teardown. */\n #dragAbort: AbortController | null = null;\n /** Color the last repaint settled on, so a configuration-driven move is reported once. */\n #committedHex: string | null = null;\n\n /**\n * Collapses a morph that swaps render inputs into one repaint, and refuses the\n * pass Stimulus delivers before `connect()`.\n */\n readonly #repaint = new MicrotaskCoalescer(() => this.#reconcileColor());\n\n /** Seeds the model from the initial hex value and renders every surface. */\n override connect(): void {\n this.#repaint.activate();\n const parsed = hexToHsla(this.valueValue);\n // When alpha is disabled, drop any alpha carried by an `#RRGGBBAA` value so\n // the model stays opaque — otherwise `hexString()` would emit `#RRGGBB`\n // while `change` reported `rgba.a < 1`. Mirrors `onHexInput()`.\n if (parsed) this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };\n this.#render();\n }\n\n /** Cancels any active pointer drag so document listeners never leak. */\n override disconnect(): void {\n this.#repaint.cancel();\n this.#dragAbort?.abort();\n this.#dragAbort = null;\n }\n\n /** Repaints when application code (or a Turbo morph) changes `alpha` at runtime. */\n alphaValueChanged(): void {\n this.#repaint.schedule();\n }\n\n /** Keyboard stepping on the focused channel slider (APG Slider model). */\n onKeydown(event: KeyboardEvent): void {\n if (isReservedArrowChord(event)) return;\n const slider = event.currentTarget as HTMLElement;\n const channel = this.#channelOf(slider);\n if (!channel) return;\n\n const [min, max] = this.#rangeOf(slider, channel);\n const value = this.#color[channel];\n let next: number | null = null;\n // On a mirrored track the greater value sits at the visual left, so the\n // horizontal pair trades places; the vertical pair passes through.\n switch (this.#mirrored ? logicalArrowKey(event.key, this.element) : event.key) {\n case \"ArrowRight\":\n case \"ArrowUp\":\n next = value + 1;\n break;\n case \"ArrowLeft\":\n case \"ArrowDown\":\n next = value - 1;\n break;\n case \"PageUp\":\n next = value + 10;\n break;\n case \"PageDown\":\n next = value - 10;\n break;\n case \"Home\":\n next = min;\n break;\n case \"End\":\n next = max;\n break;\n default:\n return;\n }\n event.preventDefault();\n this.#setChannel(channel, next, min, max);\n }\n\n /** Begins a pointer drag on a channel slider and tracks movement. */\n onPointerDown(event: PointerEvent): void {\n const slider = event.currentTarget as HTMLElement;\n const channel = this.#channelOf(slider);\n if (!channel) return;\n event.preventDefault();\n slider.focus();\n\n const [min, max] = this.#rangeOf(slider, channel);\n // Resolve the direction once for the whole gesture: reading it per move\n // would query computed style on every frame, and a drag that flipped\n // mid-gesture would be incoherent anyway.\n const mirrored = this.#mirrored;\n const update = (clientX: number): void => {\n const rect = slider.getBoundingClientRect();\n if (rect.width === 0) return;\n const offset = Math.min(1, Math.max(0, (clientX - rect.left) / rect.width));\n const fraction = mirrored ? 1 - offset : offset;\n this.#setChannel(channel, min + fraction * (max - min), min, max);\n };\n update(event.clientX);\n\n this.#dragAbort?.abort();\n const abort = new AbortController();\n this.#dragAbort = abort;\n const onMove = (move: PointerEvent): void => update(move.clientX);\n const onUp = (): void => {\n abort.abort();\n this.#dragAbort = null;\n };\n document.addEventListener(\"pointermove\", onMove, { signal: abort.signal });\n document.addEventListener(\"pointerup\", onUp, { signal: abort.signal });\n document.addEventListener(\"pointercancel\", onUp, { signal: abort.signal });\n }\n\n /** Parses the hex input on confirm and syncs every channel + surface. */\n onHexInput(): void {\n if (!this.hasHexTarget) return;\n const parsed = hexToHsla(this.hexTarget.value);\n if (!parsed) {\n // Reject invalid input by restoring the last valid hex.\n this.hexTarget.value = this.#hexString();\n return;\n }\n this.#color = this.alphaValue ? parsed : { ...parsed, alpha: 100 };\n this.#commitColor();\n }\n\n /** Clamps and snaps one channel to an integer, then re-renders + emits change. */\n #setChannel(channel: Channel, raw: number, min: number, max: number): void {\n this.#color[channel] = Math.round(Math.min(max, Math.max(min, raw)));\n this.#commitColor();\n }\n\n /**\n * Renders the model and reports a color the user actually moved. A key pressed\n * at a bound, a pointer that lands on the step already showing, and a re-confirmed\n * hex all leave the committed color where it was, so no `change` describes them.\n */\n #commitColor(): void {\n const previous = this.#committedHex;\n this.#render();\n if (this.#committedHex !== previous) {\n this.dispatch(\"change\", { detail: this.#settledDetail() });\n }\n }\n\n /**\n * Reflects the model onto sliders, the hex input, preview, and form field.\n *\n * @stimeoRenderRoot\n */\n #render(): void {\n for (const slider of this.sliderTargets) {\n const channel = this.#channelOf(slider);\n if (!channel) continue;\n const value = this.#color[channel];\n slider.setAttribute(\"aria-valuenow\", String(value));\n slider.setAttribute(\"aria-valuetext\", valueText(channel, value));\n }\n\n const hex = this.#hexString();\n this.#committedHex = hex;\n if (this.hasHexTarget) this.hexTarget.value = hex;\n for (const field of this.fieldTargets) field.value = hex;\n for (const preview of this.previewTargets) preview.style.setProperty(COLOR_PROPERTY, hex);\n this.element.style.setProperty(COLOR_PROPERTY, hex);\n }\n\n /**\n * Repaints after `alpha` changed at runtime and reports a color this controller\n * settled on. Disabling alpha drops it from the model, so the committed color can\n * move without a user edit; `change` stays reserved for the picker's own actions.\n */\n #reconcileColor(): void {\n // Compared against the last rendered color, not against the model: the Value\n // callback that scheduled this pass has already moved `alphaValue`, so\n // re-deriving the \"before\" state here would always match the \"after\" one.\n const previous = this.#committedHex;\n this.#render();\n if (previous !== null && this.#committedHex !== previous) {\n this.dispatch(\"reconcile\", { detail: this.#settledDetail() });\n }\n }\n\n /** The settled color as event detail, shared by both report paths. */\n #settledDetail(): { value: string; rgba: { r: number; g: number; b: number; a: number } } {\n const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);\n return { value: this.#hexString(), rgba: { ...rgb, a: this.#color.alpha / 100 } };\n }\n\n /** The current color as `#RRGGBB`, or `#RRGGBBAA` when alpha is enabled. */\n #hexString(): string {\n const rgb = hslToRgb(this.#color.hue, this.#color.saturation, this.#color.lightness);\n const base = `#${hex2(rgb.r)}${hex2(rgb.g)}${hex2(rgb.b)}`;\n if (!this.alphaValue) return base;\n return `${base}${hex2(Math.round((this.#color.alpha / 100) * 255))}`;\n }\n\n /** Reads a slider's `data-channel`, if it is a known channel. */\n #channelOf(slider: HTMLElement): Channel | null {\n const channel = slider.getAttribute(\"data-channel\");\n return channel && channel in CHANNEL_RANGE ? (channel as Channel) : null;\n }\n\n /**\n * A slider's `[min, max]` from aria-valuemin/max, falling back per channel.\n * An absent or blank attribute means \"not authored\", not zero — coercing it to\n * a number would pin the channel at `0` and make the per-channel default\n * unreachable.\n */\n #rangeOf(slider: HTMLElement, channel: Channel): [number, number] {\n const [defMin, defMax] = CHANNEL_RANGE[channel];\n return [\n toFiniteNumber(slider.getAttribute(\"aria-valuemin\")) ?? defMin,\n toFiniteNumber(slider.getAttribute(\"aria-valuemax\")) ?? defMax,\n ];\n }\n}\n\n/** Capitalizes a channel name for `aria-valuetext` (e.g. \"Hue\"). */\nfunction valueText(channel: Channel, value: number): string {\n const label = channel.charAt(0).toUpperCase() + channel.slice(1);\n const unit = channel === \"hue\" ? \"degrees\" : \"percent\";\n return `${label} ${value} ${unit}`;\n}\n\n/** Formats a 0–255 byte as a 2-digit lowercase hex pair. */\nfunction hex2(value: number): string {\n return Math.round(Math.min(255, Math.max(0, value)))\n .toString(16)\n .padStart(2, \"0\");\n}\n\n/** Converts HSL (h:0–360, s/l:0–100) to RGB bytes (0–255). */\nfunction hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {\n const sat = s / 100;\n const light = l / 100;\n const c = (1 - Math.abs(2 * light - 1)) * sat;\n const hp = (((h % 360) + 360) % 360) / 60;\n const x = c * (1 - Math.abs((hp % 2) - 1));\n let r = 0;\n let g = 0;\n let b = 0;\n if (hp >= 0 && hp < 1) [r, g, b] = [c, x, 0];\n else if (hp < 2) [r, g, b] = [x, c, 0];\n else if (hp < 3) [r, g, b] = [0, c, x];\n else if (hp < 4) [r, g, b] = [0, x, c];\n else if (hp < 5) [r, g, b] = [x, 0, c];\n else [r, g, b] = [c, 0, x];\n const m = light - c / 2;\n return {\n r: Math.round((r + m) * 255),\n g: Math.round((g + m) * 255),\n b: Math.round((b + m) * 255),\n };\n}\n\n/** Converts RGB bytes (0–255) to HSL (h:0–360, s/l:0–100). */\nfunction rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {\n const rn = r / 255;\n const gn = g / 255;\n const bn = b / 255;\n const max = Math.max(rn, gn, bn);\n const min = Math.min(rn, gn, bn);\n const delta = max - min;\n const l = (max + min) / 2;\n\n let h = 0;\n if (delta !== 0) {\n if (max === rn) h = ((gn - bn) / delta) % 6;\n else if (max === gn) h = (bn - rn) / delta + 2;\n else h = (rn - gn) / delta + 4;\n h = (h * 60 + 360) % 360;\n }\n const s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));\n return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) };\n}\n\n/**\n * Parses `#RGB`, `#RGBA`, `#RRGGBB`, or `#RRGGBBAA` into the HSLA model, or\n * returns null when the string is not a valid hex color.\n */\nfunction hexToHsla(input: string): Hsla | null {\n const hex = input.trim().replace(/^#/, \"\");\n if (!/^(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(hex)) return null;\n\n // Expand shorthand (#RGB / #RGBA) so every channel is a full byte pair.\n const full =\n hex.length <= 4\n ? hex\n .split(\"\")\n .map((char) => char + char)\n .join(\"\")\n : hex;\n\n const r = Number.parseInt(full.slice(0, 2), 16);\n const g = Number.parseInt(full.slice(2, 4), 16);\n const b = Number.parseInt(full.slice(4, 6), 16);\n const a = full.length === 8 ? Number.parseInt(full.slice(6, 8), 16) : 255;\n\n const { h, s, l } = rgbToHsl(r, g, b);\n return { hue: h, saturation: s, lightness: l, alpha: Math.round((a / 255) * 100) };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/active_option.ts","../../src/utils/arrow_step.ts","../../src/utils/composition_tracker.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/option_scroll.ts","../../src/controllers/combobox_controller.ts"],"names":[],"mappings":"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","file":"combobox_controller.js","sourcesContent":["/**\n * Reflects a single **active option** across an option set, writing only where\n * the value actually changes.\n *\n * Every controller that drives a virtually-focused list answers the same question\n * on each keystroke and each arrow repeat: *which one option is the active\n * candidate, and what does the rest of the set look like now?* Answering it by\n * writing `aria-selected` to **every** option is the obvious implementation, and\n * the one this helper exists to replace.\n *\n * **The write count is the point.** At most two options change per move (the old\n * active and the new one), while the option count is authored — a consumer can\n * render hundreds, and filtering re-runs this on each input event. An\n * unconditional pass costs one attribute mutation per option per keypress, each\n * one an observable DOM change that `MutationObserver`s and the a11y tree react\n * to. Comparing before writing turns O(n) mutations into O(1) without changing\n * what the DOM ends up saying.\n *\n * **`aria-selected` here means \"active candidate\", not \"committed selection\".**\n * That distinction decides where this helper may be used: in a combobox the\n * committed value lives *outside* the popup (the input's text), so\n * `aria-selected` is free to track virtual focus; in a listbox or a grid it is\n * the selection itself and must not be repurposed. Only the former kind belongs\n * here — which is why `listbox` marks its active row with `data-active` instead\n * and is deliberately not a consumer of this helper.\n *\n * Scope is deliberately narrow: this owns the option-set write and nothing else.\n * `aria-activedescendant` on the host, scrolling the active option into view, and\n * the `data-active` marker stay in the controllers, because each one differs\n * (different host element, different scroll container, and not every consumer\n * uses `data-active` at all). Folding them in would mean a helper with three\n * optional behaviours — more surface than the duplication it removes.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * #setActive(option: HTMLElement | null): void {\n * syncActiveOption(this.optionTargets, option);\n * // host-specific parts stay here: aria-activedescendant, scrolling, data-active\n * }\n * ```\n */\n\n/**\n * Writes `aria-selected` across `options` so that exactly `active` reads `\"true\"`.\n *\n * Skips every option already holding the right value, so a move mutates only the\n * two that changed. Pass `null` to clear the whole set — the state a closed or\n * empty-filtered popup needs.\n *\n * `options` must be the **full** set, not the currently visible subset: an option\n * hidden by filtering while it was active would otherwise keep a stale\n * `aria-selected=\"true\"` and resurface as selected when the filter widens.\n *\n * @param options - every option in the set, visible or not\n * @param active - the one option to mark active, or `null` to clear\n * @returns how many attributes were actually written\n */\nexport function syncActiveOption(\n options: Iterable<HTMLElement>,\n active: HTMLElement | null,\n): number {\n let written = 0;\n for (const option of options) {\n const next = option === active ? \"true\" : \"false\";\n if (option.getAttribute(\"aria-selected\") === next) continue;\n option.setAttribute(\"aria-selected\", next);\n written += 1;\n }\n return written;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/** 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","/**\n * Collapses many target callbacks from one DOM mutation into a single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element, so\n * replacing a list of N options delivers N callbacks — but the useful unit of\n * work is \"reconcile against the DOM that resulted\", once, after the batch has\n * settled. Every controller that owns a reconcilable target set needs the same\n * shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers the *initial* target callbacks\n * ahead of `connect()`. Reconciling there would compute a fallback against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Scroll-follow for virtually-focused listbox options.\n *\n * Listbox-style widgets that track their active option with\n * `aria-activedescendant` keep DOM focus on the input/trigger, so the\n * browser's native \"scroll the focused element into view\" never runs — in a\n * scrollable list (`max-height` + `overflow`) the active option can walk right\n * out of sight on ArrowDown/ArrowUp. This helper keeps it visible.\n *\n * It adjusts the LIST's own `scrollTop` only — deliberately not\n * `scrollIntoView({ block: \"nearest\" })`, which may also scroll ancestor\n * scrolling boxes (the page) when a floating popup pokes past a viewport\n * edge, desyncing anchored/marker-composed placements. (Command-palette uses\n * `scrollIntoView` because its page is scroll-locked behind the modal;\n * page-floating popups must use this helper instead.)\n */\n\n/**\n * Scrolls `option` into view within `list` by minimally adjusting\n * `list.scrollTop`. No-ops when the list does not actually scroll. Never\n * touches any other scrolling box.\n *\n * @param list - The scrollable listbox container.\n * @param option - The (virtually focused) option to keep visible.\n */\nexport function scrollOptionIntoView(list: HTMLElement, option: HTMLElement): void {\n if (list.scrollHeight <= list.clientHeight) return;\n const listRect = list.getBoundingClientRect();\n const optionRect = option.getBoundingClientRect();\n if (optionRect.top < listRect.top) {\n list.scrollTop -= listRect.top - optionRect.top;\n } else if (optionRect.bottom > listRect.bottom) {\n list.scrollTop += optionRect.bottom - listRect.bottom;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { syncActiveOption } from \"../utils/active_option\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { scrollOptionIntoView } from \"../utils/option_scroll\";\n\n/**\n * Headless, accessible combobox behavior (list autocomplete).\n *\n * Markup contract (identifier: `stimeo--combobox`):\n * <div data-controller=\"stimeo--combobox\">\n * <input type=\"text\" role=\"combobox\" aria-expanded=\"false\"\n * aria-autocomplete=\"list\" aria-controls=\"listbox\"\n * data-stimeo--combobox-target=\"input\"\n * data-action=\"input->stimeo--combobox#filter\n * keydown->stimeo--combobox#onKeydown\n * focus->stimeo--combobox#open\n * click->stimeo--combobox#open\" />\n * <ul id=\"listbox\" role=\"listbox\" data-stimeo--combobox-target=\"list\" hidden>\n * <li role=\"option\" id=\"opt-apple\" data-value=\"apple\"\n * data-stimeo--combobox-target=\"option\"\n * data-action=\"click->stimeo--combobox#selectByClick\">Apple</li>\n * <!-- more options -->\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Combobox** pattern with a listbox popup and\n * list-autocomplete. Focus stays in the input; the active option is tracked with\n * `aria-activedescendant` rather than by moving DOM focus.\n *\n * @remarks\n * Behavior only. Options are authored in the DOM; the controller filters them by\n * toggling each option's `hidden` attribute (case-insensitive substring match on\n * its text). The consumer owns styling, typically keyed off `[aria-selected]`.\n * When an open listbox has no matching options, the root element gets\n * `data-stimeo--combobox-empty` so the consumer can style the empty state (hide\n * the list, show a \"no results\" node, …) — the library imposes no visuals.\n *\n * Behavior provided:\n * - Typing filters the options and opens the listbox.\n * - Focusing or clicking the input opens the listbox, re-filtered against the\n * current value (so re-opening with a non-matching value keeps the empty state).\n * - `ArrowDown`/`ArrowUp` move the active option (wrapping); `Enter` selects it;\n * `Escape` closes the listbox; `Home`/`End` jump to the first/last visible\n * option.\n * - Selecting an option fills the input (with the option's `data-value` if set,\n * otherwise its text) and closes the listbox.\n * - A click outside the combobox closes the listbox.\n */\nexport class ComboboxController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"list\", \"option\"];\n static actions = [\"close\", \"filter\", \"onKeydown\", \"open\", \"selectByClick\"] as const;\n static events = [\"selected\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasInputTarget: boolean;\n declare readonly hasListTarget: boolean;\n\n /** Stable ID of the active option; the live element is resolved before every use. */\n #activeId: string | null = null;\n #connected = false;\n /** Collapses one mutation batch of target callbacks into a single pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n /**\n * Suppresses {@link open} for the duration of the programmatic re-focus in\n * `#select`, so committing a value (which returns focus to the input)\n * does not immediately re-open the listbox via a `focus`-bound action.\n */\n #suppressOpen = false;\n /** Owns IME lifecycle state; confirmed text re-filters the list once. */\n readonly #composition = new CompositionTracker({ onEnd: () => this.filter() });\n\n /** Starts closed with no active option and registers the outside-click listener. */\n override connect(): void {\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.#connected = true;\n this.#reconcile.activate();\n }\n\n /** Removes the document-level listener registered in {@link connect}. */\n override disconnect(): void {\n this.#connected = false;\n this.#reconcile.cancel();\n this.#composition.disconnect();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n }\n\n /** Tracks an input added initially or after connect. */\n inputTargetConnected(input: HTMLInputElement): void {\n this.#composition.observe(input);\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /** Removes composition listeners when the active input is replaced or removed. */\n inputTargetDisconnected(input: HTMLInputElement): void {\n this.#composition.unobserve(input);\n }\n\n /** Establishes the active-state baseline, then reconciles a runtime addition. */\n optionTargetConnected(option: HTMLElement): void {\n option.setAttribute(\"aria-selected\", \"false\");\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /** Clears the disconnected node's active state, then reconciles the survivors. */\n optionTargetDisconnected(option: HTMLElement): void {\n option.setAttribute(\"aria-selected\", \"false\");\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /** Filters confirmed input text and opens the listbox. */\n filter(event?: InputEvent): void {\n if (this.#composition.isComposing(event)) return;\n this.open();\n }\n\n /**\n * Opens the listbox, re-filtering the options against the current input value\n * so the visible options and empty state always match what is typed (e.g.\n * re-opening with a stale non-matching value still surfaces the empty state).\n */\n open(): void {\n if (!this.hasListTarget || !this.hasInputTarget || this.#suppressOpen) return;\n this.#applyFilter();\n this.listTarget.hidden = false;\n this.inputTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#setActive(-1);\n this.#reflectEmptyState();\n }\n\n /**\n * Hides options that don't match the current input value (case-insensitive\n * substring). An empty query shows every option. Does not change open state.\n */\n #applyFilter(): void {\n const query = this.inputTarget.value.trim().toLowerCase();\n for (const option of this.optionTargets) {\n const text = (option.textContent ?? \"\").trim().toLowerCase();\n option.hidden = query.length > 0 && !text.includes(query);\n }\n }\n\n /**\n * Closes the listbox, clears the active option, and updates ARIA state.\n *\n * Survives a missing input in both directions. {@link inputTargetConnected}\n * accepts an input added after connect, so dereferencing it here would abort\n * `connect()` before it registers the outside-click listener, and nothing\n * re-registers it later. Symmetrically, an input removed while the popup is\n * open must still let the popup come down — so hiding the list is\n * unconditional and only the ARIA half is guarded.\n */\n close(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = true;\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n if (this.hasInputTarget) this.inputTarget.setAttribute(\"aria-expanded\", \"false\");\n this.#setActive(-1);\n }\n\n /** Routes keyboard interaction per the APG combobox model. */\n onKeydown(event: KeyboardEvent): void {\n // Ignore keys fired during IME composition: the `Enter` that confirms a\n // candidate (and arrows that move within it) must not select an option or\n // close the popup. Controller-owned lifecycle state covers confirming events\n // that omit the standard per-event signal.\n if (this.#composition.isComposing(event)) return;\n // A widget that already claimed the key (an enclosing composite consuming\n // arrows, a descendant editor) must not ALSO move the active option, commit\n // one, or drop the popup — composition depends on this yield.\n if (event.defaultPrevented) return;\n // A chorded arrow is the browser's, except the Alt+Down/Up this pattern\n // claims below. \"Someone already consumed it\" outranks it, so it is checked\n // after the yield above.\n if (isReservedArrowChord(event, [\"alt\"])) return;\n // The two chords the pattern claims. `Alt+Down` shows the popup *without*\n // moving focus into it, and `Alt+Up` returns focus and closes — which is\n // what separates them from the bare arrows below, where the point is to\n // move the active option.\n if (event.altKey && (event.key === \"ArrowDown\" || event.key === \"ArrowUp\")) {\n // Each chord is bound in one popup state only. In the other state the\n // pattern defines nothing, so the press stays the browser's rather than\n // being swallowed on a no-op.\n if (event.key === \"ArrowDown\" ? !this.#isClosed : this.#isClosed) return;\n event.preventDefault();\n if (event.key === \"ArrowDown\") {\n this.open();\n } else {\n // The popup never holds DOM focus here (the active option is virtual, via\n // aria-activedescendant), so \"return focus to the combobox\" is already\n // true and closing is the whole action. Calling focus() would re-trigger\n // the focus-bound open().\n this.close();\n }\n return;\n }\n if (!this.#isClosed) this.#reconcileActive();\n switch (event.key) {\n case \"ArrowDown\": {\n event.preventDefault();\n // open() re-filters, so read the visible set afterwards.\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n const activeIndex = this.#findActiveIndex(visible);\n const next = activeIndex === -1 ? 0 : (activeIndex + 1) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"ArrowUp\": {\n event.preventDefault();\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n const activeIndex = this.#findActiveIndex(visible);\n // From the input (no active option) ArrowUp jumps to the last option,\n // per the APG; otherwise it wraps backwards.\n const next =\n activeIndex === -1\n ? visible.length - 1\n : (activeIndex - 1 + visible.length) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"Home\":\n if (!this.#isClosed && this.#visibleOptions().length > 0) {\n event.preventDefault();\n this.#setActive(0);\n }\n break;\n case \"End\": {\n const visible = this.#visibleOptions();\n if (!this.#isClosed && visible.length > 0) {\n event.preventDefault();\n this.#setActive(visible.length - 1);\n }\n break;\n }\n case \"Enter\": {\n const visible = this.#visibleOptions();\n const activeIndex = this.#findActiveIndex(visible);\n const active = activeIndex === -1 ? undefined : visible[activeIndex];\n if (active) {\n event.preventDefault();\n this.#select(active);\n }\n break;\n }\n case \"Escape\":\n // A press an inner handler already owned is yielded at the top of this\n // handler. Escape is consumed only while the list is open: with it closed\n // the press stays free for the shared resolver, so an enclosing dialog\n // still closes from a focused input.\n if (this.#isClosed) break;\n event.preventDefault();\n this.close();\n break;\n case \"Tab\":\n // Let focus leave naturally, but don't keep a stale popup open.\n this.close();\n break;\n default:\n break;\n }\n }\n\n /**\n * Closes the listbox when a click lands outside the combobox. Mirrors the menu\n * button's outside-click behavior; clicks on an option are inside the element,\n * so `#select` (not this handler) closes the popup after committing.\n */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.#isClosed && !this.element.contains(event.target as Node)) this.close();\n };\n\n /** Selects the clicked option. Bound via `data-action` (click). */\n selectByClick(event: Event): void {\n const option = (event.currentTarget as HTMLElement).closest<HTMLElement>('[role=\"option\"]');\n if (option && this.optionTargets.includes(option)) this.#select(option);\n }\n\n /**\n * Commits an option: fills the input, closes the listbox, notifies listeners.\n *\n * An input removed while the popup is open leaves the options clickable, and\n * {@link close} already survives that state. Selection does too: the popup\n * comes down and listeners still hear the choice, with only the field-bound\n * half — the value write, the focus return, and the native `change` — skipped,\n * because there is no field to carry them.\n */\n #select(option: HTMLElement): void {\n const value = option.dataset.value ?? (option.textContent ?? \"\").trim();\n if (!this.hasInputTarget) {\n this.close();\n this.dispatch(\"selected\", { detail: { value } });\n return;\n }\n const changed = this.inputTarget.value !== value;\n this.inputTarget.value = value;\n this.close();\n // Returning focus to the input would re-trigger a `focus`-bound open(); guard\n // it so the listbox stays closed after a selection.\n this.#suppressOpen = true;\n this.inputTarget.focus();\n this.#suppressOpen = false;\n if (changed) {\n // A native bubbling `change` (matching <select>/listbox semantics: only on\n // an actual value change) so form-level behaviors — validation re-checks,\n // auto-submit — hear the commit without knowing this widget. Deliberately\n // NOT `input`: that is this combobox's own filter trigger and would reopen\n // the popup on every selection.\n this.inputTarget.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n this.dispatch(\"selected\", { detail: { value } });\n }\n\n /**\n * Reflects whether the open listbox currently has zero matching options by\n * toggling `data-stimeo--combobox-empty` on the root element. Behavior only:\n * consumers decide how to present the empty state (hide the list, show a\n * \"no results\" node, etc.) via CSS keyed off this attribute.\n */\n #reflectEmptyState(): void {\n const empty = !this.#isClosed && this.#visibleOptions().length === 0;\n if (empty) {\n this.element.setAttribute(\"data-stimeo--combobox-empty\", \"\");\n } else {\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n }\n }\n\n /**\n * Marks the visible option at `index` active via `aria-selected` and the\n * input's `aria-activedescendant`. Pass `-1` to clear the active option.\n */\n #setActive(index: number): void {\n const visible = this.#visibleOptions();\n const active = index === -1 ? null : (visible[index] ?? null);\n // The full target set, not just the visible ones: an option hidden by\n // filtering while active would otherwise keep a stale selected state.\n syncActiveOption(this.optionTargets, active);\n this.#activeId = active?.id || null;\n if (this.hasInputTarget) {\n if (active?.id) {\n this.inputTarget.setAttribute(\"aria-activedescendant\", active.id);\n } else {\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n }\n // Virtual focus never triggers the browser's native focus-scrolling, so a\n // scrollable list must follow the active option itself (list-only scroll).\n if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);\n }\n\n /** Re-applies filtering and active state after a target collection change. */\n #reconcileOptions(): void {\n if (this.hasInputTarget && !this.#isClosed) this.#applyFilter();\n this.#reconcileActive();\n this.#reflectEmptyState();\n }\n\n /** Preserves a surviving active ID; disappearance deliberately clears active state. */\n #reconcileActive(): void {\n if (!this.hasInputTarget || this.#isClosed) {\n this.#setActive(-1);\n return;\n }\n const visible = this.#visibleOptions();\n const index = this.#findActiveIndex(visible);\n const active = index === -1 ? null : (visible[index] ?? null);\n const selected = this.optionTargets.filter(\n (option) => option.getAttribute(\"aria-selected\") === \"true\",\n );\n const idref = this.inputTarget.getAttribute(\"aria-activedescendant\");\n const stateMatches = active\n ? this.#activeId === (active.id || null) &&\n selected.length === 1 &&\n selected[0] === active &&\n idref === (active.id || null)\n : this.#activeId === null && selected.length === 0 && idref === null;\n if (!stateMatches) this.#setActive(index);\n }\n\n /** Resolves the stable ID, with `aria-selected` as the ID-less compatibility marker. */\n #findActiveIndex(options: readonly HTMLElement[]): number {\n const activeId =\n (this.hasInputTarget ? this.inputTarget.getAttribute(\"aria-activedescendant\") : null) ??\n this.#activeId;\n if (activeId) return options.findIndex((option) => option.id === activeId);\n return options.findIndex((option) => option.getAttribute(\"aria-selected\") === \"true\");\n }\n\n /** Coalesces all target callbacks from one MutationObserver batch. */\n #queueOptionReconciliation(): void {\n this.#reconcile.schedule();\n }\n\n /** The options currently shown (not filtered out). */\n #visibleOptions(): HTMLElement[] {\n return this.optionTargets.filter((option) => !option.hidden);\n }\n\n /** Whether the listbox is currently hidden. */\n get #isClosed(): boolean {\n return !this.hasListTarget || this.listTarget.hidden !== false;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/active_option.ts","../../src/utils/arrow_step.ts","../../src/utils/composition_tracker.ts","../../src/utils/microtask_coalescer.ts","../../src/utils/option_scroll.ts","../../src/controllers/combobox_controller.ts"],"names":[],"mappings":"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","file":"combobox_controller.js","sourcesContent":["/**\n * Reflects a single **active option** across an option set, writing only where\n * the value actually changes.\n *\n * Every controller that drives a virtually-focused list answers the same question\n * on each keystroke and each arrow repeat: *which one option is the active\n * candidate, and what does the rest of the set look like now?* Answering it by\n * writing `aria-selected` to **every** option is the obvious implementation, and\n * the one this helper exists to replace.\n *\n * **The write count is the point.** At most two options change per move (the old\n * active and the new one), while the option count is authored — a consumer can\n * render hundreds, and filtering re-runs this on each input event. An\n * unconditional pass costs one attribute mutation per option per keypress, each\n * one an observable DOM change that `MutationObserver`s and the a11y tree react\n * to. Comparing before writing turns O(n) mutations into O(1) without changing\n * what the DOM ends up saying.\n *\n * **`aria-selected` here means \"active candidate\", not \"committed selection\".**\n * That distinction decides where this helper may be used: in a combobox the\n * committed value lives *outside* the popup (the input's text), so\n * `aria-selected` is free to track virtual focus; in a listbox or a grid it is\n * the selection itself and must not be repurposed. Only the former kind belongs\n * here — which is why `listbox` marks its active row with `data-active` instead\n * and is deliberately not a consumer of this helper.\n *\n * Scope is deliberately narrow: this owns the option-set write and nothing else.\n * `aria-activedescendant` on the host, scrolling the active option into view, and\n * the `data-active` marker stay in the controllers, because each one differs\n * (different host element, different scroll container, and not every consumer\n * uses `data-active` at all). Folding them in would mean a helper with three\n * optional behaviours — more surface than the duplication it removes.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * #setActive(option: HTMLElement | null): void {\n * syncActiveOption(this.optionTargets, option);\n * // host-specific parts stay here: aria-activedescendant, scrolling, data-active\n * }\n * ```\n */\n\n/**\n * Writes `aria-selected` across `options` so that exactly `active` reads `\"true\"`.\n *\n * Skips every option already holding the right value, so a move mutates only the\n * two that changed. Pass `null` to clear the whole set — the state a closed or\n * empty-filtered popup needs.\n *\n * `options` must be the **full** set, not the currently visible subset: an option\n * hidden by filtering while it was active would otherwise keep a stale\n * `aria-selected=\"true\"` and resurface as selected when the filter widens.\n *\n * @param options - every option in the set, visible or not\n * @param active - the one option to mark active, or `null` to clear\n * @returns how many attributes were actually written\n */\nexport function syncActiveOption(\n options: Iterable<HTMLElement>,\n active: HTMLElement | null,\n): number {\n let written = 0;\n for (const option of options) {\n const next = option === active ? \"true\" : \"false\";\n if (option.getAttribute(\"aria-selected\") === next) continue;\n option.setAttribute(\"aria-selected\", next);\n written += 1;\n }\n return written;\n}\n","import { isRtl } from \"./logical_scroll\";\n\n/**\n * Turns an arrow key into a **logical** step: `+1` for \"next\", `-1` for\n * \"previous\", `0` when the key names neither.\n *\n * APG defines the horizontal pair as *next / previous* and says a vertical\n * arrangement swaps in Down/Up for the same meaning — so the pair is one axis's\n * spelling of an order, and the order reverses with the writing direction. Only\n * the horizontal pair reverses. Down/Up name an axis the writing direction does\n * not mirror, and returning them unchanged is the point: many controllers fold\n * both pairs into one branch, where swapping the branches under RTL would flip\n * the vertical axis too — a bug that reads as \"the arrows work\" until someone\n * presses Down.\n *\n * **Direction is read from the element the caller passes, which should be the\n * container that lays the items out** — not the focused child. A child may carry\n * its own `dir` (an LTR input inside an RTL form is ordinary authoring), and\n * probing per handler makes two handlers disagree at the boundary between them.\n *\n * This decides direction only. Whether the axis is even active (an\n * `orientation=\"horizontal\"` widget ignoring Down/Up), how far the step lands,\n * and what wrapping does all stay with the caller.\n *\n * **It encodes the list-order convention: `ArrowDown` is *next*.** Widgets that\n * pair the arrows by *value* instead — `ArrowUp` meaning \"more\", as a rating or a\n * slider does — must not use this, or their vertical axis inverts. Reverse the\n * horizontal pair on its own there.\n *\n * @example\n * ```ts\n * const step = logicalArrowStep(event.key, this.element);\n * if (step === 0) return;\n * this.#roving.setActive(rovingMove(current, length, step, \"wrap\"), { focus: true });\n * ```\n */\nexport function logicalArrowStep(key: string, element: Element): 1 | -1 | 0 {\n if (key === \"ArrowDown\") return 1;\n if (key === \"ArrowUp\") return -1;\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return 0;\n const forward = isRtl(element) ? \"ArrowLeft\" : \"ArrowRight\";\n return key === forward ? 1 : -1;\n}\n\n/**\n * Rewrites `key` so an existing LTR-shaped branch keeps working under RTL:\n * `ArrowRight` and `ArrowLeft` trade places, everything else passes through.\n *\n * The alternative — negating a delta — silently breaks handlers whose two\n * horizontal branches are **not mirror images**. A grid that clamps one edge but\n * not the other, or a segmented field guarding `index > 0` on one side and\n * `index < length - 1` on the other, ends up applying the wrong guard to the\n * wrong direction. Swapping the key leaves each branch, guards and all, exactly\n * where its author put it.\n *\n * Same rule as {@link logicalArrowStep} about which element to read: pass the\n * container that lays the items out, not the focused child.\n *\n * @example\n * ```ts\n * switch (logicalArrowKey(event.key, this.element)) {\n * case \"ArrowLeft\": // \"previous\" — whatever direction that is on screen\n * ```\n */\nexport function logicalArrowKey(key: string, element: Element): string {\n if (key !== \"ArrowRight\" && key !== \"ArrowLeft\") return key;\n if (!isRtl(element)) return key;\n return key === \"ArrowRight\" ? \"ArrowLeft\" : \"ArrowRight\";\n}\n\n/** Modifiers a widget may claim on an arrow key, named for the `allow` list. */\nexport type ArrowModifier = \"alt\" | \"ctrl\" | \"meta\" | \"shift\";\n\n/**\n * True when an arrow key arrived carrying a modifier the widget must leave to\n * the browser: return without calling `preventDefault()` and without moving any\n * state.\n *\n * A bare arrow belongs to the widget; a chorded one usually does not.\n * `Alt`/`Meta` plus a horizontal arrow is history back/forward on every desktop\n * browser, and a widget that swallows it makes the shortcut work or not\n * depending on where focus happens to sit — a coin-flip the user cannot see.\n *\n * `allow` is for the combinations APG assigns to a pattern **and the widget\n * actually implements** — today only Combobox's optional `Alt+Down`/`Alt+Up`.\n * Listing one the widget does not implement defeats the point: the chord then\n * runs the plain-arrow branch, which is exactly what this guard exists to stop.\n * Non-arrow keys return `false`, so chorded letters and\n * `Control+Home`/`Control+End` are untouched.\n *\n * @example\n * ```ts\n * if (isReservedArrowChord(event)) return;\n * ```\n */\nexport function isReservedArrowChord(\n event: KeyboardEvent,\n allow: readonly ArrowModifier[] = [],\n): boolean {\n if (!event.key.startsWith(\"Arrow\")) return false;\n return (\n (event.altKey && !allow.includes(\"alt\")) ||\n (event.ctrlKey && !allow.includes(\"ctrl\")) ||\n (event.metaKey && !allow.includes(\"meta\")) ||\n (event.shiftKey && !allow.includes(\"shift\"))\n );\n}\n","/** 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","/**\n * Collapses many Stimulus lifecycle callbacks from one DOM mutation into a\n * single pass.\n *\n * Stimulus fires `<name>TargetConnected` / `Disconnected` once per element and\n * `<name>ValueChanged` once per changed attribute. Replacing a list of N options\n * or morphing several render Values therefore delivers N callbacks — but the\n * useful unit of work is \"reconcile against the resulting declarative input\",\n * once, after the batch has settled. Every controller with reconcilable targets\n * or render Values needs the same shape: a `queued` flag plus `queueMicrotask`.\n *\n * **A microtask is the right horizon, and the reason is specific.** Stimulus\n * drives these callbacks from a `MutationObserver`, whose own callback already\n * runs as a microtask with the whole batch in hand; scheduling one more lands\n * after the last sibling callback of that batch and still before paint or any\n * event handler. A timer would be later than it needs to be, and reconciling\n * synchronously would run once per element against a half-applied DOM.\n *\n * **The two guards are not the same guard.** Scheduling is refused before the\n * controller connects, and running is refused after it disconnects:\n *\n * - **Before `connect()`** — Stimulus delivers initial target and Value callbacks\n * ahead of `connect()`. Reconciling there would compute output against a\n * controller whose own state has not been initialised, and `connect()` is\n * about to do a full pass anyway.\n * - **After `disconnect()`** — Stimulus fires a callback for **every** target\n * during teardown, and a microtask queued just before it would otherwise run\n * against a detached tree. {@link MicrotaskCoalescer.cancel} exists for the\n * teardown path to drop the pending pass outright.\n *\n * Both guards are part of one contract here rather than something each consumer\n * has to remember separately.\n *\n * Scope is the scheduling only. *What* to reconcile — keep the surviving active\n * option, fall back to the next / previous / first visible one, rebuild derived\n * chips or hidden fields — stays in the controller, because no two consumers\n * answer it the same way.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n *\n * connect() { this.#reconcile.activate(); }\n * disconnect() { this.#reconcile.cancel(); }\n *\n * optionTargetConnected() { this.#reconcile.schedule(); }\n * optionTargetDisconnected() { this.#reconcile.schedule(); }\n * ```\n */\nexport class MicrotaskCoalescer {\n readonly #run: () => void;\n #queued = false;\n #active = false;\n #generation = 0;\n\n /** @param run - the single reconciliation pass, invoked at most once per batch. */\n constructor(run: () => void) {\n this.#run = run;\n }\n\n /** Opens the window in which {@link schedule} is honoured; call from `connect()`. */\n activate(): void {\n this.#active = true;\n }\n\n /** Closes the window and drops any pending pass; call from `disconnect()`. */\n cancel(): void {\n this.#active = false;\n this.#queued = false;\n this.#generation += 1;\n }\n\n /** Requests one pass after the batch settles. Idempotent; inert outside the window. */\n schedule(): void {\n if (!this.#active || this.#queued) return;\n this.#queued = true;\n const generation = this.#generation;\n queueMicrotask(() => {\n // A cancelled callback must not consume a pass queued after reconnect.\n if (generation !== this.#generation || !this.#queued || !this.#active) return;\n this.#queued = false;\n this.#run();\n });\n }\n}\n","/**\n * Scroll-follow for virtually-focused listbox options.\n *\n * Listbox-style widgets that track their active option with\n * `aria-activedescendant` keep DOM focus on the input/trigger, so the\n * browser's native \"scroll the focused element into view\" never runs — in a\n * scrollable list (`max-height` + `overflow`) the active option can walk right\n * out of sight on ArrowDown/ArrowUp. This helper keeps it visible.\n *\n * It adjusts the LIST's own `scrollTop` only — deliberately not\n * `scrollIntoView({ block: \"nearest\" })`, which may also scroll ancestor\n * scrolling boxes (the page) when a floating popup pokes past a viewport\n * edge, desyncing anchored/marker-composed placements. (Command-palette uses\n * `scrollIntoView` because its page is scroll-locked behind the modal;\n * page-floating popups must use this helper instead.)\n */\n\n/**\n * Scrolls `option` into view within `list` by minimally adjusting\n * `list.scrollTop`. No-ops when the list does not actually scroll. Never\n * touches any other scrolling box.\n *\n * @param list - The scrollable listbox container.\n * @param option - The (virtually focused) option to keep visible.\n */\nexport function scrollOptionIntoView(list: HTMLElement, option: HTMLElement): void {\n if (list.scrollHeight <= list.clientHeight) return;\n const listRect = list.getBoundingClientRect();\n const optionRect = option.getBoundingClientRect();\n if (optionRect.top < listRect.top) {\n list.scrollTop -= listRect.top - optionRect.top;\n } else if (optionRect.bottom > listRect.bottom) {\n list.scrollTop += optionRect.bottom - listRect.bottom;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { syncActiveOption } from \"../utils/active_option\";\nimport { isReservedArrowChord } from \"../utils/arrow_step\";\nimport { CompositionTracker } from \"../utils/composition_tracker\";\nimport { MicrotaskCoalescer } from \"../utils/microtask_coalescer\";\nimport { scrollOptionIntoView } from \"../utils/option_scroll\";\n\n/**\n * Headless, accessible combobox behavior (list autocomplete).\n *\n * Markup contract (identifier: `stimeo--combobox`):\n * <div data-controller=\"stimeo--combobox\">\n * <input type=\"text\" role=\"combobox\" aria-expanded=\"false\"\n * aria-autocomplete=\"list\" aria-controls=\"listbox\"\n * data-stimeo--combobox-target=\"input\"\n * data-action=\"input->stimeo--combobox#filter\n * keydown->stimeo--combobox#onKeydown\n * focus->stimeo--combobox#open\n * click->stimeo--combobox#open\" />\n * <ul id=\"listbox\" role=\"listbox\" data-stimeo--combobox-target=\"list\" hidden>\n * <li role=\"option\" id=\"opt-apple\" data-value=\"apple\"\n * data-stimeo--combobox-target=\"option\"\n * data-action=\"click->stimeo--combobox#selectByClick\">Apple</li>\n * <!-- more options -->\n * </ul>\n * </div>\n *\n * Implements the WAI-ARIA APG **Combobox** pattern with a listbox popup and\n * list-autocomplete. Focus stays in the input; the active option is tracked with\n * `aria-activedescendant` rather than by moving DOM focus.\n *\n * @remarks\n * Behavior only. Options are authored in the DOM; the controller filters them by\n * toggling each option's `hidden` attribute (case-insensitive substring match on\n * its text). The consumer owns styling, typically keyed off `[aria-selected]`.\n * When an open listbox has no matching options, the root element gets\n * `data-stimeo--combobox-empty` so the consumer can style the empty state (hide\n * the list, show a \"no results\" node, …) — the library imposes no visuals.\n *\n * Behavior provided:\n * - Typing filters the options and opens the listbox.\n * - Focusing or clicking the input opens the listbox, re-filtered against the\n * current value (so re-opening with a non-matching value keeps the empty state).\n * - `ArrowDown`/`ArrowUp` move the active option (wrapping); `Enter` selects it;\n * `Escape` closes the listbox; `Home`/`End` jump to the first/last visible\n * option.\n * - Selecting an option fills the input (with the option's `data-value` if set,\n * otherwise its text) and closes the listbox.\n * - A click outside the combobox closes the listbox.\n */\nexport class ComboboxController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"list\", \"option\"];\n static actions = [\"close\", \"filter\", \"onKeydown\", \"open\", \"selectByClick\"] as const;\n static events = [\"selected\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly listTarget: HTMLElement;\n declare readonly optionTargets: HTMLElement[];\n declare readonly hasInputTarget: boolean;\n declare readonly hasListTarget: boolean;\n\n /** Stable ID of the active option; the live element is resolved before every use. */\n #activeId: string | null = null;\n #connected = false;\n /** Collapses one mutation batch of target callbacks into a single pass. */\n readonly #reconcile = new MicrotaskCoalescer(() => this.#reconcileOptions());\n /**\n * Suppresses {@link open} for the duration of the programmatic re-focus in\n * `#select`, so committing a value (which returns focus to the input)\n * does not immediately re-open the listbox via a `focus`-bound action.\n */\n #suppressOpen = false;\n /** Owns IME lifecycle state; confirmed text re-filters the list once. */\n readonly #composition = new CompositionTracker({ onEnd: () => this.filter() });\n\n /** Starts closed with no active option and registers the outside-click listener. */\n override connect(): void {\n if (this.hasInputTarget) this.#composition.observe(this.inputTarget);\n this.close();\n document.addEventListener(\"click\", this.#onOutsideClick, true);\n this.#connected = true;\n this.#reconcile.activate();\n }\n\n /** Removes the document-level listener registered in {@link connect}. */\n override disconnect(): void {\n this.#connected = false;\n this.#reconcile.cancel();\n this.#composition.disconnect();\n document.removeEventListener(\"click\", this.#onOutsideClick, true);\n }\n\n /** Tracks an input added initially or after connect. */\n inputTargetConnected(input: HTMLInputElement): void {\n this.#composition.observe(input);\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /** Removes composition listeners when the active input is replaced or removed. */\n inputTargetDisconnected(input: HTMLInputElement): void {\n this.#composition.unobserve(input);\n }\n\n /** Establishes the active-state baseline, then reconciles a runtime addition. */\n optionTargetConnected(option: HTMLElement): void {\n option.setAttribute(\"aria-selected\", \"false\");\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /** Clears the disconnected node's active state, then reconciles the survivors. */\n optionTargetDisconnected(option: HTMLElement): void {\n option.setAttribute(\"aria-selected\", \"false\");\n if (this.#connected) this.#queueOptionReconciliation();\n }\n\n /** Filters confirmed input text and opens the listbox. */\n filter(event?: InputEvent): void {\n if (this.#composition.isComposing(event)) return;\n this.open();\n }\n\n /**\n * Opens the listbox, re-filtering the options against the current input value\n * so the visible options and empty state always match what is typed (e.g.\n * re-opening with a stale non-matching value still surfaces the empty state).\n */\n open(): void {\n if (!this.hasListTarget || !this.hasInputTarget || this.#suppressOpen) return;\n this.#applyFilter();\n this.listTarget.hidden = false;\n this.inputTarget.setAttribute(\"aria-expanded\", \"true\");\n this.#setActive(-1);\n this.#reflectEmptyState();\n }\n\n /**\n * Hides options that don't match the current input value (case-insensitive\n * substring). An empty query shows every option. Does not change open state.\n */\n #applyFilter(): void {\n const query = this.inputTarget.value.trim().toLowerCase();\n for (const option of this.optionTargets) {\n const text = (option.textContent ?? \"\").trim().toLowerCase();\n option.hidden = query.length > 0 && !text.includes(query);\n }\n }\n\n /**\n * Closes the listbox, clears the active option, and updates ARIA state.\n *\n * Survives a missing input in both directions. {@link inputTargetConnected}\n * accepts an input added after connect, so dereferencing it here would abort\n * `connect()` before it registers the outside-click listener, and nothing\n * re-registers it later. Symmetrically, an input removed while the popup is\n * open must still let the popup come down — so hiding the list is\n * unconditional and only the ARIA half is guarded.\n */\n close(): void {\n if (!this.hasListTarget) return;\n this.listTarget.hidden = true;\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n if (this.hasInputTarget) this.inputTarget.setAttribute(\"aria-expanded\", \"false\");\n this.#setActive(-1);\n }\n\n /** Routes keyboard interaction per the APG combobox model. */\n onKeydown(event: KeyboardEvent): void {\n // Ignore keys fired during IME composition: the `Enter` that confirms a\n // candidate (and arrows that move within it) must not select an option or\n // close the popup. Controller-owned lifecycle state covers confirming events\n // that omit the standard per-event signal.\n if (this.#composition.isComposing(event)) return;\n // A widget that already claimed the key (an enclosing composite consuming\n // arrows, a descendant editor) must not ALSO move the active option, commit\n // one, or drop the popup — composition depends on this yield.\n if (event.defaultPrevented) return;\n // A chorded arrow is the browser's, except the Alt+Down/Up this pattern\n // claims below. \"Someone already consumed it\" outranks it, so it is checked\n // after the yield above.\n if (isReservedArrowChord(event, [\"alt\"])) return;\n // The two chords the pattern claims. `Alt+Down` shows the popup *without*\n // moving focus into it, and `Alt+Up` returns focus and closes — which is\n // what separates them from the bare arrows below, where the point is to\n // move the active option.\n if (event.altKey && (event.key === \"ArrowDown\" || event.key === \"ArrowUp\")) {\n // Each chord is bound in one popup state only. In the other state the\n // pattern defines nothing, so the press stays the browser's rather than\n // being swallowed on a no-op.\n if (event.key === \"ArrowDown\" ? !this.#isClosed : this.#isClosed) return;\n event.preventDefault();\n if (event.key === \"ArrowDown\") {\n this.open();\n } else {\n // The popup never holds DOM focus here (the active option is virtual, via\n // aria-activedescendant), so \"return focus to the combobox\" is already\n // true and closing is the whole action. Calling focus() would re-trigger\n // the focus-bound open().\n this.close();\n }\n return;\n }\n if (!this.#isClosed) this.#reconcileActive();\n switch (event.key) {\n case \"ArrowDown\": {\n event.preventDefault();\n // open() re-filters, so read the visible set afterwards.\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n const activeIndex = this.#findActiveIndex(visible);\n const next = activeIndex === -1 ? 0 : (activeIndex + 1) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"ArrowUp\": {\n event.preventDefault();\n if (this.#isClosed) this.open();\n const visible = this.#visibleOptions();\n if (visible.length > 0) {\n const activeIndex = this.#findActiveIndex(visible);\n // From the input (no active option) ArrowUp jumps to the last option,\n // per the APG; otherwise it wraps backwards.\n const next =\n activeIndex === -1\n ? visible.length - 1\n : (activeIndex - 1 + visible.length) % visible.length;\n this.#setActive(next);\n }\n break;\n }\n case \"Home\":\n if (!this.#isClosed && this.#visibleOptions().length > 0) {\n event.preventDefault();\n this.#setActive(0);\n }\n break;\n case \"End\": {\n const visible = this.#visibleOptions();\n if (!this.#isClosed && visible.length > 0) {\n event.preventDefault();\n this.#setActive(visible.length - 1);\n }\n break;\n }\n case \"Enter\": {\n const visible = this.#visibleOptions();\n const activeIndex = this.#findActiveIndex(visible);\n const active = activeIndex === -1 ? undefined : visible[activeIndex];\n if (active) {\n event.preventDefault();\n this.#select(active);\n }\n break;\n }\n case \"Escape\":\n // A press an inner handler already owned is yielded at the top of this\n // handler. Escape is consumed only while the list is open: with it closed\n // the press stays free for the shared resolver, so an enclosing dialog\n // still closes from a focused input.\n if (this.#isClosed) break;\n event.preventDefault();\n this.close();\n break;\n case \"Tab\":\n // Let focus leave naturally, but don't keep a stale popup open.\n this.close();\n break;\n default:\n break;\n }\n }\n\n /**\n * Closes the listbox when a click lands outside the combobox. Mirrors the menu\n * button's outside-click behavior; clicks on an option are inside the element,\n * so `#select` (not this handler) closes the popup after committing.\n */\n readonly #onOutsideClick = (event: MouseEvent): void => {\n if (!this.#isClosed && !this.element.contains(event.target as Node)) this.close();\n };\n\n /** Selects the clicked option. Bound via `data-action` (click). */\n selectByClick(event: Event): void {\n const option = (event.currentTarget as HTMLElement).closest<HTMLElement>('[role=\"option\"]');\n if (option && this.optionTargets.includes(option)) this.#select(option);\n }\n\n /**\n * Commits an option: fills the input, closes the listbox, notifies listeners.\n *\n * An input removed while the popup is open leaves the options clickable, and\n * {@link close} already survives that state. Selection does too: the popup\n * comes down and listeners still hear the choice, with only the field-bound\n * half — the value write, the focus return, and the native `change` — skipped,\n * because there is no field to carry them.\n */\n #select(option: HTMLElement): void {\n const value = option.dataset.value ?? (option.textContent ?? \"\").trim();\n if (!this.hasInputTarget) {\n this.close();\n this.dispatch(\"selected\", { detail: { value } });\n return;\n }\n const changed = this.inputTarget.value !== value;\n this.inputTarget.value = value;\n this.close();\n // Returning focus to the input would re-trigger a `focus`-bound open(); guard\n // it so the listbox stays closed after a selection.\n this.#suppressOpen = true;\n this.inputTarget.focus();\n this.#suppressOpen = false;\n if (changed) {\n // A native bubbling `change` (matching <select>/listbox semantics: only on\n // an actual value change) so form-level behaviors — validation re-checks,\n // auto-submit — hear the commit without knowing this widget. Deliberately\n // NOT `input`: that is this combobox's own filter trigger and would reopen\n // the popup on every selection.\n this.inputTarget.dispatchEvent(new Event(\"change\", { bubbles: true }));\n }\n this.dispatch(\"selected\", { detail: { value } });\n }\n\n /**\n * Reflects whether the open listbox currently has zero matching options by\n * toggling `data-stimeo--combobox-empty` on the root element. Behavior only:\n * consumers decide how to present the empty state (hide the list, show a\n * \"no results\" node, etc.) via CSS keyed off this attribute.\n */\n #reflectEmptyState(): void {\n const empty = !this.#isClosed && this.#visibleOptions().length === 0;\n if (empty) {\n this.element.setAttribute(\"data-stimeo--combobox-empty\", \"\");\n } else {\n this.element.removeAttribute(\"data-stimeo--combobox-empty\");\n }\n }\n\n /**\n * Marks the visible option at `index` active via `aria-selected` and the\n * input's `aria-activedescendant`. Pass `-1` to clear the active option.\n */\n #setActive(index: number): void {\n const visible = this.#visibleOptions();\n const active = index === -1 ? null : (visible[index] ?? null);\n // The full target set, not just the visible ones: an option hidden by\n // filtering while active would otherwise keep a stale selected state.\n syncActiveOption(this.optionTargets, active);\n this.#activeId = active?.id || null;\n if (this.hasInputTarget) {\n if (active?.id) {\n this.inputTarget.setAttribute(\"aria-activedescendant\", active.id);\n } else {\n this.inputTarget.removeAttribute(\"aria-activedescendant\");\n }\n }\n // Virtual focus never triggers the browser's native focus-scrolling, so a\n // scrollable list must follow the active option itself (list-only scroll).\n if (active && this.hasListTarget) scrollOptionIntoView(this.listTarget, active);\n }\n\n /** Re-applies filtering and active state after a target collection change. */\n #reconcileOptions(): void {\n if (this.hasInputTarget && !this.#isClosed) this.#applyFilter();\n this.#reconcileActive();\n this.#reflectEmptyState();\n }\n\n /** Preserves a surviving active ID; disappearance deliberately clears active state. */\n #reconcileActive(): void {\n if (!this.hasInputTarget || this.#isClosed) {\n this.#setActive(-1);\n return;\n }\n const visible = this.#visibleOptions();\n const index = this.#findActiveIndex(visible);\n const active = index === -1 ? null : (visible[index] ?? null);\n const selected = this.optionTargets.filter(\n (option) => option.getAttribute(\"aria-selected\") === \"true\",\n );\n const idref = this.inputTarget.getAttribute(\"aria-activedescendant\");\n const stateMatches = active\n ? this.#activeId === (active.id || null) &&\n selected.length === 1 &&\n selected[0] === active &&\n idref === (active.id || null)\n : this.#activeId === null && selected.length === 0 && idref === null;\n if (!stateMatches) this.#setActive(index);\n }\n\n /** Resolves the stable ID, with `aria-selected` as the ID-less compatibility marker. */\n #findActiveIndex(options: readonly HTMLElement[]): number {\n const activeId =\n (this.hasInputTarget ? this.inputTarget.getAttribute(\"aria-activedescendant\") : null) ??\n this.#activeId;\n if (activeId) return options.findIndex((option) => option.id === activeId);\n return options.findIndex((option) => option.getAttribute(\"aria-selected\") === \"true\");\n }\n\n /** Coalesces all target callbacks from one MutationObserver batch. */\n #queueOptionReconciliation(): void {\n this.#reconcile.schedule();\n }\n\n /** The options currently shown (not filtered out). */\n #visibleOptions(): HTMLElement[] {\n return this.optionTargets.filter((option) => !option.hidden);\n }\n\n /** Whether the listbox is currently hidden. */\n get #isClosed(): boolean {\n return !this.hasListTarget || this.listTarget.hidden !== false;\n }\n}\n"]}