phlex_kit 0.2.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 (405) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +135 -0
  4. data/app/assets/stylesheets/phlex_kit/_tokens.css +91 -0
  5. data/app/assets/stylesheets/phlex_kit/phlex_kit.css +82 -0
  6. data/app/components/phlex_kit/accordion/accordion.css +14 -0
  7. data/app/components/phlex_kit/accordion/accordion.rb +10 -0
  8. data/app/components/phlex_kit/accordion/accordion_content.rb +8 -0
  9. data/app/components/phlex_kit/accordion/accordion_default_content.rb +6 -0
  10. data/app/components/phlex_kit/accordion/accordion_default_trigger.rb +12 -0
  11. data/app/components/phlex_kit/accordion/accordion_icon.rb +16 -0
  12. data/app/components/phlex_kit/accordion/accordion_item.rb +12 -0
  13. data/app/components/phlex_kit/accordion/accordion_trigger.rb +8 -0
  14. data/app/components/phlex_kit/alert/alert.css +32 -0
  15. data/app/components/phlex_kit/alert/alert.rb +37 -0
  16. data/app/components/phlex_kit/alert/alert_description.rb +12 -0
  17. data/app/components/phlex_kit/alert/alert_title.rb +12 -0
  18. data/app/components/phlex_kit/alert_dialog/alert_dialog.css +37 -0
  19. data/app/components/phlex_kit/alert_dialog/alert_dialog.rb +20 -0
  20. data/app/components/phlex_kit/alert_dialog/alert_dialog_action.rb +14 -0
  21. data/app/components/phlex_kit/alert_dialog/alert_dialog_cancel.rb +13 -0
  22. data/app/components/phlex_kit/alert_dialog/alert_dialog_content.rb +19 -0
  23. data/app/components/phlex_kit/alert_dialog/alert_dialog_description.rb +12 -0
  24. data/app/components/phlex_kit/alert_dialog/alert_dialog_footer.rb +12 -0
  25. data/app/components/phlex_kit/alert_dialog/alert_dialog_header.rb +12 -0
  26. data/app/components/phlex_kit/alert_dialog/alert_dialog_title.rb +12 -0
  27. data/app/components/phlex_kit/alert_dialog/alert_dialog_trigger.rb +12 -0
  28. data/app/components/phlex_kit/aspect_ratio/aspect_ratio.css +12 -0
  29. data/app/components/phlex_kit/aspect_ratio/aspect_ratio.rb +27 -0
  30. data/app/components/phlex_kit/attachment/attachment.css +62 -0
  31. data/app/components/phlex_kit/attachment/attachment.rb +16 -0
  32. data/app/components/phlex_kit/attachment/attachment_action.rb +24 -0
  33. data/app/components/phlex_kit/attachment/attachment_actions.rb +7 -0
  34. data/app/components/phlex_kit/attachment/attachment_content.rb +7 -0
  35. data/app/components/phlex_kit/attachment/attachment_description.rb +7 -0
  36. data/app/components/phlex_kit/attachment/attachment_media.rb +8 -0
  37. data/app/components/phlex_kit/attachment/attachment_title.rb +7 -0
  38. data/app/components/phlex_kit/avatar/avatar.css +35 -0
  39. data/app/components/phlex_kit/avatar/avatar.rb +25 -0
  40. data/app/components/phlex_kit/avatar/avatar_fallback.rb +13 -0
  41. data/app/components/phlex_kit/avatar/avatar_group.rb +7 -0
  42. data/app/components/phlex_kit/avatar/avatar_image.rb +24 -0
  43. data/app/components/phlex_kit/badge/badge.css +50 -0
  44. data/app/components/phlex_kit/badge/badge.rb +39 -0
  45. data/app/components/phlex_kit/breadcrumb/breadcrumb.css +18 -0
  46. data/app/components/phlex_kit/breadcrumb/breadcrumb.rb +8 -0
  47. data/app/components/phlex_kit/breadcrumb/breadcrumb_ellipsis.rb +15 -0
  48. data/app/components/phlex_kit/breadcrumb/breadcrumb_item.rb +6 -0
  49. data/app/components/phlex_kit/breadcrumb/breadcrumb_link.rb +9 -0
  50. data/app/components/phlex_kit/breadcrumb/breadcrumb_list.rb +6 -0
  51. data/app/components/phlex_kit/breadcrumb/breadcrumb_page.rb +8 -0
  52. data/app/components/phlex_kit/breadcrumb/breadcrumb_separator.rb +10 -0
  53. data/app/components/phlex_kit/bubble/bubble.css +32 -0
  54. data/app/components/phlex_kit/bubble/bubble.rb +17 -0
  55. data/app/components/phlex_kit/bubble/bubble_content.rb +11 -0
  56. data/app/components/phlex_kit/bubble/bubble_group.rb +6 -0
  57. data/app/components/phlex_kit/bubble/bubble_reactions.rb +12 -0
  58. data/app/components/phlex_kit/button/button.css +72 -0
  59. data/app/components/phlex_kit/button/button.rb +51 -0
  60. data/app/components/phlex_kit/button_group/button_group.css +8 -0
  61. data/app/components/phlex_kit/button_group/button_group.rb +14 -0
  62. data/app/components/phlex_kit/calendar/calendar.css +109 -0
  63. data/app/components/phlex_kit/calendar/calendar.rb +47 -0
  64. data/app/components/phlex_kit/calendar/calendar_body.rb +13 -0
  65. data/app/components/phlex_kit/calendar/calendar_days.rb +98 -0
  66. data/app/components/phlex_kit/calendar/calendar_header.rb +13 -0
  67. data/app/components/phlex_kit/calendar/calendar_next.rb +40 -0
  68. data/app/components/phlex_kit/calendar/calendar_prev.rb +40 -0
  69. data/app/components/phlex_kit/calendar/calendar_title.rb +19 -0
  70. data/app/components/phlex_kit/calendar/calendar_weekdays.rb +27 -0
  71. data/app/components/phlex_kit/card/card.css +15 -0
  72. data/app/components/phlex_kit/card/card.rb +29 -0
  73. data/app/components/phlex_kit/card/card_content.rb +12 -0
  74. data/app/components/phlex_kit/card/card_description.rb +12 -0
  75. data/app/components/phlex_kit/card/card_footer.rb +12 -0
  76. data/app/components/phlex_kit/card/card_header.rb +12 -0
  77. data/app/components/phlex_kit/card/card_title.rb +12 -0
  78. data/app/components/phlex_kit/carousel/carousel.css +41 -0
  79. data/app/components/phlex_kit/carousel/carousel.rb +37 -0
  80. data/app/components/phlex_kit/carousel/carousel_content.rb +16 -0
  81. data/app/components/phlex_kit/carousel/carousel_item.rb +17 -0
  82. data/app/components/phlex_kit/carousel/carousel_next.rb +39 -0
  83. data/app/components/phlex_kit/carousel/carousel_previous.rb +40 -0
  84. data/app/components/phlex_kit/chart/chart.css +9 -0
  85. data/app/components/phlex_kit/chart/chart.rb +31 -0
  86. data/app/components/phlex_kit/checkbox/checkbox.css +27 -0
  87. data/app/components/phlex_kit/checkbox/checkbox.rb +26 -0
  88. data/app/components/phlex_kit/clipboard/clipboard.css +8 -0
  89. data/app/components/phlex_kit/clipboard/clipboard.rb +19 -0
  90. data/app/components/phlex_kit/clipboard/clipboard_popover.rb +14 -0
  91. data/app/components/phlex_kit/clipboard/clipboard_source.rb +6 -0
  92. data/app/components/phlex_kit/clipboard/clipboard_trigger.rb +6 -0
  93. data/app/components/phlex_kit/codeblock/codeblock.css +7 -0
  94. data/app/components/phlex_kit/codeblock/codeblock.rb +23 -0
  95. data/app/components/phlex_kit/collapsible/collapsible.css +3 -0
  96. data/app/components/phlex_kit/collapsible/collapsible.rb +13 -0
  97. data/app/components/phlex_kit/collapsible/collapsible_content.rb +8 -0
  98. data/app/components/phlex_kit/collapsible/collapsible_trigger.rb +8 -0
  99. data/app/components/phlex_kit/combobox/combobox.css +310 -0
  100. data/app/components/phlex_kit/combobox/combobox.rb +33 -0
  101. data/app/components/phlex_kit/combobox/combobox_badge.rb +15 -0
  102. data/app/components/phlex_kit/combobox/combobox_badge_trigger.rb +55 -0
  103. data/app/components/phlex_kit/combobox/combobox_checkbox.rb +21 -0
  104. data/app/components/phlex_kit/combobox/combobox_clear_button.rb +39 -0
  105. data/app/components/phlex_kit/combobox/combobox_empty_state.rb +17 -0
  106. data/app/components/phlex_kit/combobox/combobox_input_trigger.rb +69 -0
  107. data/app/components/phlex_kit/combobox/combobox_item.rb +19 -0
  108. data/app/components/phlex_kit/combobox/combobox_item_indicator.rb +27 -0
  109. data/app/components/phlex_kit/combobox/combobox_list.rb +12 -0
  110. data/app/components/phlex_kit/combobox/combobox_list_group.rb +14 -0
  111. data/app/components/phlex_kit/combobox/combobox_popover.rb +27 -0
  112. data/app/components/phlex_kit/combobox/combobox_radio.rb +26 -0
  113. data/app/components/phlex_kit/combobox/combobox_search_input.rb +49 -0
  114. data/app/components/phlex_kit/combobox/combobox_toggle_all_checkbox.rb +21 -0
  115. data/app/components/phlex_kit/combobox/combobox_trigger.rb +48 -0
  116. data/app/components/phlex_kit/command/command.css +104 -0
  117. data/app/components/phlex_kit/command/command.rb +18 -0
  118. data/app/components/phlex_kit/command/command_dialog.rb +19 -0
  119. data/app/components/phlex_kit/command/command_dialog_content.rb +37 -0
  120. data/app/components/phlex_kit/command/command_dialog_trigger.rb +22 -0
  121. data/app/components/phlex_kit/command/command_empty.rb +17 -0
  122. data/app/components/phlex_kit/command/command_group.rb +32 -0
  123. data/app/components/phlex_kit/command/command_input.rb +56 -0
  124. data/app/components/phlex_kit/command/command_item.rb +22 -0
  125. data/app/components/phlex_kit/command/command_list.rb +12 -0
  126. data/app/components/phlex_kit/context_menu/context_menu.css +19 -0
  127. data/app/components/phlex_kit/context_menu/context_menu.rb +11 -0
  128. data/app/components/phlex_kit/context_menu/context_menu_content.rb +8 -0
  129. data/app/components/phlex_kit/context_menu/context_menu_item.rb +25 -0
  130. data/app/components/phlex_kit/context_menu/context_menu_label.rb +11 -0
  131. data/app/components/phlex_kit/context_menu/context_menu_separator.rb +8 -0
  132. data/app/components/phlex_kit/context_menu/context_menu_trigger.rb +8 -0
  133. data/app/components/phlex_kit/data_table/data_table.css +110 -0
  134. data/app/components/phlex_kit/data_table/data_table.rb +25 -0
  135. data/app/components/phlex_kit/data_table/data_table_bulk_actions.rb +15 -0
  136. data/app/components/phlex_kit/data_table/data_table_column_toggle.rb +61 -0
  137. data/app/components/phlex_kit/data_table/data_table_expand_toggle.rb +40 -0
  138. data/app/components/phlex_kit/data_table/data_table_form.rb +36 -0
  139. data/app/components/phlex_kit/data_table/data_table_kaminari_adapter.rb +17 -0
  140. data/app/components/phlex_kit/data_table/data_table_manual_adapter.rb +18 -0
  141. data/app/components/phlex_kit/data_table/data_table_pagination.rb +98 -0
  142. data/app/components/phlex_kit/data_table/data_table_pagination_bar.rb +13 -0
  143. data/app/components/phlex_kit/data_table/data_table_pagy_adapter.rb +17 -0
  144. data/app/components/phlex_kit/data_table/data_table_per_page_select.rb +29 -0
  145. data/app/components/phlex_kit/data_table/data_table_row_checkbox.rb +24 -0
  146. data/app/components/phlex_kit/data_table/data_table_search.rb +51 -0
  147. data/app/components/phlex_kit/data_table/data_table_select_all_checkbox.rb +19 -0
  148. data/app/components/phlex_kit/data_table/data_table_selection_summary.rb +19 -0
  149. data/app/components/phlex_kit/data_table/data_table_sort_head.rb +82 -0
  150. data/app/components/phlex_kit/data_table/data_table_toolbar.rb +13 -0
  151. data/app/components/phlex_kit/date_picker/date_picker.css +28 -0
  152. data/app/components/phlex_kit/date_picker/date_picker.rb +71 -0
  153. data/app/components/phlex_kit/dialog/dialog.css +32 -0
  154. data/app/components/phlex_kit/dialog/dialog.rb +14 -0
  155. data/app/components/phlex_kit/dialog/dialog_content.rb +21 -0
  156. data/app/components/phlex_kit/dialog/dialog_description.rb +6 -0
  157. data/app/components/phlex_kit/dialog/dialog_footer.rb +6 -0
  158. data/app/components/phlex_kit/dialog/dialog_header.rb +6 -0
  159. data/app/components/phlex_kit/dialog/dialog_middle.rb +6 -0
  160. data/app/components/phlex_kit/dialog/dialog_title.rb +6 -0
  161. data/app/components/phlex_kit/dialog/dialog_trigger.rb +8 -0
  162. data/app/components/phlex_kit/drawer/drawer.css +54 -0
  163. data/app/components/phlex_kit/drawer/drawer.rb +18 -0
  164. data/app/components/phlex_kit/drawer/drawer_close.rb +9 -0
  165. data/app/components/phlex_kit/drawer/drawer_content.rb +21 -0
  166. data/app/components/phlex_kit/drawer/drawer_description.rb +7 -0
  167. data/app/components/phlex_kit/drawer/drawer_footer.rb +7 -0
  168. data/app/components/phlex_kit/drawer/drawer_header.rb +7 -0
  169. data/app/components/phlex_kit/drawer/drawer_title.rb +7 -0
  170. data/app/components/phlex_kit/drawer/drawer_trigger.rb +9 -0
  171. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.css +38 -0
  172. data/app/components/phlex_kit/dropdown_menu/dropdown_menu.rb +22 -0
  173. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_content.rb +23 -0
  174. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_item.rb +22 -0
  175. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_label.rb +12 -0
  176. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_separator.rb +12 -0
  177. data/app/components/phlex_kit/dropdown_menu/dropdown_menu_trigger.rb +15 -0
  178. data/app/components/phlex_kit/empty/empty.css +25 -0
  179. data/app/components/phlex_kit/empty/empty.rb +6 -0
  180. data/app/components/phlex_kit/empty/empty_content.rb +6 -0
  181. data/app/components/phlex_kit/empty/empty_description.rb +6 -0
  182. data/app/components/phlex_kit/empty/empty_header.rb +6 -0
  183. data/app/components/phlex_kit/empty/empty_media.rb +14 -0
  184. data/app/components/phlex_kit/empty/empty_title.rb +6 -0
  185. data/app/components/phlex_kit/form/form.css +15 -0
  186. data/app/components/phlex_kit/form/form.rb +27 -0
  187. data/app/components/phlex_kit/form_field/form_field.css +31 -0
  188. data/app/components/phlex_kit/form_field/form_field.rb +31 -0
  189. data/app/components/phlex_kit/form_field/form_field_error.rb +19 -0
  190. data/app/components/phlex_kit/form_field/form_field_hint.rb +13 -0
  191. data/app/components/phlex_kit/form_field/form_field_label.rb +13 -0
  192. data/app/components/phlex_kit/hover_card/hover_card.css +8 -0
  193. data/app/components/phlex_kit/hover_card/hover_card.rb +10 -0
  194. data/app/components/phlex_kit/hover_card/hover_card_content.rb +8 -0
  195. data/app/components/phlex_kit/hover_card/hover_card_trigger.rb +6 -0
  196. data/app/components/phlex_kit/input/input.css +29 -0
  197. data/app/components/phlex_kit/input/input.rb +34 -0
  198. data/app/components/phlex_kit/input_group/input_group.css +35 -0
  199. data/app/components/phlex_kit/input_group/input_group.rb +15 -0
  200. data/app/components/phlex_kit/input_group/input_group_addon.rb +16 -0
  201. data/app/components/phlex_kit/input_group/input_group_text.rb +7 -0
  202. data/app/components/phlex_kit/input_otp/input_otp.css +32 -0
  203. data/app/components/phlex_kit/input_otp/input_otp.rb +29 -0
  204. data/app/components/phlex_kit/input_otp/input_otp_group.rb +7 -0
  205. data/app/components/phlex_kit/input_otp/input_otp_separator.rb +7 -0
  206. data/app/components/phlex_kit/input_otp/input_otp_slot.rb +27 -0
  207. data/app/components/phlex_kit/item/item.css +32 -0
  208. data/app/components/phlex_kit/item/item.rb +18 -0
  209. data/app/components/phlex_kit/item/item_actions.rb +7 -0
  210. data/app/components/phlex_kit/item/item_content.rb +7 -0
  211. data/app/components/phlex_kit/item/item_description.rb +7 -0
  212. data/app/components/phlex_kit/item/item_group.rb +7 -0
  213. data/app/components/phlex_kit/item/item_media.rb +7 -0
  214. data/app/components/phlex_kit/item/item_title.rb +7 -0
  215. data/app/components/phlex_kit/kbd/kbd.css +17 -0
  216. data/app/components/phlex_kit/kbd/kbd.rb +14 -0
  217. data/app/components/phlex_kit/kbd/kbd_group.rb +12 -0
  218. data/app/components/phlex_kit/label/label.css +12 -0
  219. data/app/components/phlex_kit/label/label.rb +14 -0
  220. data/app/components/phlex_kit/link/link.css +6 -0
  221. data/app/components/phlex_kit/link/link.rb +47 -0
  222. data/app/components/phlex_kit/masked_input/masked_input.rb +12 -0
  223. data/app/components/phlex_kit/menubar/menubar.css +66 -0
  224. data/app/components/phlex_kit/menubar/menubar.rb +24 -0
  225. data/app/components/phlex_kit/menubar/menubar_content.rb +9 -0
  226. data/app/components/phlex_kit/menubar/menubar_item.rb +26 -0
  227. data/app/components/phlex_kit/menubar/menubar_menu.rb +9 -0
  228. data/app/components/phlex_kit/menubar/menubar_separator.rb +7 -0
  229. data/app/components/phlex_kit/menubar/menubar_trigger.rb +14 -0
  230. data/app/components/phlex_kit/message/message.css +20 -0
  231. data/app/components/phlex_kit/message/message.rb +14 -0
  232. data/app/components/phlex_kit/message/message_avatar.rb +6 -0
  233. data/app/components/phlex_kit/message/message_content.rb +6 -0
  234. data/app/components/phlex_kit/message/message_footer.rb +6 -0
  235. data/app/components/phlex_kit/message/message_group.rb +6 -0
  236. data/app/components/phlex_kit/message/message_header.rb +6 -0
  237. data/app/components/phlex_kit/message_scroller/message_scroller.css +2 -0
  238. data/app/components/phlex_kit/message_scroller/message_scroller.rb +11 -0
  239. data/app/components/phlex_kit/native_select/native_select.css +51 -0
  240. data/app/components/phlex_kit/native_select/native_select.rb +48 -0
  241. data/app/components/phlex_kit/native_select/native_select_group.rb +13 -0
  242. data/app/components/phlex_kit/native_select/native_select_icon.rb +32 -0
  243. data/app/components/phlex_kit/native_select/native_select_option.rb +14 -0
  244. data/app/components/phlex_kit/navigation_menu/navigation_menu.css +67 -0
  245. data/app/components/phlex_kit/navigation_menu/navigation_menu.rb +23 -0
  246. data/app/components/phlex_kit/navigation_menu/navigation_menu_content.rb +9 -0
  247. data/app/components/phlex_kit/navigation_menu/navigation_menu_item.rb +9 -0
  248. data/app/components/phlex_kit/navigation_menu/navigation_menu_link.rb +13 -0
  249. data/app/components/phlex_kit/navigation_menu/navigation_menu_list.rb +7 -0
  250. data/app/components/phlex_kit/navigation_menu/navigation_menu_trigger.rb +27 -0
  251. data/app/components/phlex_kit/pagination/pagination.css +5 -0
  252. data/app/components/phlex_kit/pagination/pagination.rb +10 -0
  253. data/app/components/phlex_kit/pagination/pagination_content.rb +6 -0
  254. data/app/components/phlex_kit/pagination/pagination_ellipsis.rb +17 -0
  255. data/app/components/phlex_kit/pagination/pagination_item.rb +14 -0
  256. data/app/components/phlex_kit/popover/popover.css +9 -0
  257. data/app/components/phlex_kit/popover/popover.rb +11 -0
  258. data/app/components/phlex_kit/popover/popover_content.rb +8 -0
  259. data/app/components/phlex_kit/popover/popover_trigger.rb +8 -0
  260. data/app/components/phlex_kit/progress/progress.css +17 -0
  261. data/app/components/phlex_kit/progress/progress.rb +25 -0
  262. data/app/components/phlex_kit/radio_button/radio_button.css +9 -0
  263. data/app/components/phlex_kit/radio_button/radio_button.rb +19 -0
  264. data/app/components/phlex_kit/radio_group/radio_group.css +3 -0
  265. data/app/components/phlex_kit/radio_group/radio_group.rb +14 -0
  266. data/app/components/phlex_kit/resizable/resizable.css +23 -0
  267. data/app/components/phlex_kit/resizable/resizable_handle.rb +21 -0
  268. data/app/components/phlex_kit/resizable/resizable_panel.rb +19 -0
  269. data/app/components/phlex_kit/resizable/resizable_panel_group.rb +26 -0
  270. data/app/components/phlex_kit/scroll_area/scroll_area.css +21 -0
  271. data/app/components/phlex_kit/scroll_area/scroll_area.rb +15 -0
  272. data/app/components/phlex_kit/select/select.css +80 -0
  273. data/app/components/phlex_kit/select/select.rb +43 -0
  274. data/app/components/phlex_kit/select/select_content.rb +24 -0
  275. data/app/components/phlex_kit/select/select_group.rb +13 -0
  276. data/app/components/phlex_kit/select/select_input.rb +23 -0
  277. data/app/components/phlex_kit/select/select_item.rb +52 -0
  278. data/app/components/phlex_kit/select/select_label.rb +13 -0
  279. data/app/components/phlex_kit/select/select_trigger.rb +42 -0
  280. data/app/components/phlex_kit/select/select_value.rb +21 -0
  281. data/app/components/phlex_kit/separator/separator.css +6 -0
  282. data/app/components/phlex_kit/separator/separator.rb +30 -0
  283. data/app/components/phlex_kit/sheet/sheet.css +17 -0
  284. data/app/components/phlex_kit/sheet/sheet.rb +14 -0
  285. data/app/components/phlex_kit/sheet/sheet_content.rb +25 -0
  286. data/app/components/phlex_kit/sheet/sheet_description.rb +6 -0
  287. data/app/components/phlex_kit/sheet/sheet_footer.rb +6 -0
  288. data/app/components/phlex_kit/sheet/sheet_header.rb +6 -0
  289. data/app/components/phlex_kit/sheet/sheet_middle.rb +6 -0
  290. data/app/components/phlex_kit/sheet/sheet_title.rb +6 -0
  291. data/app/components/phlex_kit/sheet/sheet_trigger.rb +6 -0
  292. data/app/components/phlex_kit/shortcut_key/shortcut_key.css +17 -0
  293. data/app/components/phlex_kit/shortcut_key/shortcut_key.rb +9 -0
  294. data/app/components/phlex_kit/sidebar/sidebar.css +42 -0
  295. data/app/components/phlex_kit/sidebar/sidebar.rb +19 -0
  296. data/app/components/phlex_kit/sidebar/sidebar_content.rb +12 -0
  297. data/app/components/phlex_kit/sidebar/sidebar_footer.rb +12 -0
  298. data/app/components/phlex_kit/sidebar/sidebar_group.rb +12 -0
  299. data/app/components/phlex_kit/sidebar/sidebar_header.rb +12 -0
  300. data/app/components/phlex_kit/sidebar/sidebar_inset.rb +14 -0
  301. data/app/components/phlex_kit/sidebar/sidebar_menu.rb +12 -0
  302. data/app/components/phlex_kit/sidebar/sidebar_menu_button.rb +17 -0
  303. data/app/components/phlex_kit/sidebar/sidebar_menu_item.rb +12 -0
  304. data/app/components/phlex_kit/sidebar/sidebar_wrapper.rb +13 -0
  305. data/app/components/phlex_kit/skeleton/skeleton.css +7 -0
  306. data/app/components/phlex_kit/skeleton/skeleton.rb +9 -0
  307. data/app/components/phlex_kit/slider/slider.css +52 -0
  308. data/app/components/phlex_kit/slider/slider.rb +39 -0
  309. data/app/components/phlex_kit/spinner/spinner.css +5 -0
  310. data/app/components/phlex_kit/spinner/spinner.rb +27 -0
  311. data/app/components/phlex_kit/stars/stars.css +4 -0
  312. data/app/components/phlex_kit/stars/stars.rb +19 -0
  313. data/app/components/phlex_kit/switch/switch.css +28 -0
  314. data/app/components/phlex_kit/switch/switch.rb +21 -0
  315. data/app/components/phlex_kit/table/table.css +24 -0
  316. data/app/components/phlex_kit/table/table.rb +35 -0
  317. data/app/components/phlex_kit/table/table_body.rb +12 -0
  318. data/app/components/phlex_kit/table/table_caption.rb +12 -0
  319. data/app/components/phlex_kit/table/table_cell.rb +12 -0
  320. data/app/components/phlex_kit/table/table_footer.rb +12 -0
  321. data/app/components/phlex_kit/table/table_head.rb +12 -0
  322. data/app/components/phlex_kit/table/table_header.rb +12 -0
  323. data/app/components/phlex_kit/table/table_row.rb +12 -0
  324. data/app/components/phlex_kit/tabs/tabs.css +13 -0
  325. data/app/components/phlex_kit/tabs/tabs.rb +13 -0
  326. data/app/components/phlex_kit/tabs/tabs_content.rb +11 -0
  327. data/app/components/phlex_kit/tabs/tabs_list.rb +6 -0
  328. data/app/components/phlex_kit/tabs/tabs_trigger.rb +17 -0
  329. data/app/components/phlex_kit/textarea/textarea.css +27 -0
  330. data/app/components/phlex_kit/textarea/textarea.rb +24 -0
  331. data/app/components/phlex_kit/theme_toggle/theme_toggle.rb +15 -0
  332. data/app/components/phlex_kit/toast/toast.css +163 -0
  333. data/app/components/phlex_kit/toast/toast.rb +21 -0
  334. data/app/components/phlex_kit/toast/toast_action.rb +19 -0
  335. data/app/components/phlex_kit/toast/toast_cancel.rb +18 -0
  336. data/app/components/phlex_kit/toast/toast_close.rb +35 -0
  337. data/app/components/phlex_kit/toast/toast_description.rb +13 -0
  338. data/app/components/phlex_kit/toast/toast_icon.rb +63 -0
  339. data/app/components/phlex_kit/toast/toast_item.rb +70 -0
  340. data/app/components/phlex_kit/toast/toast_region.rb +121 -0
  341. data/app/components/phlex_kit/toast/toast_title.rb +13 -0
  342. data/app/components/phlex_kit/toggle/toggle.css +16 -0
  343. data/app/components/phlex_kit/toggle/toggle.rb +59 -0
  344. data/app/components/phlex_kit/toggle_group/toggle_group.css +10 -0
  345. data/app/components/phlex_kit/toggle_group/toggle_group.rb +65 -0
  346. data/app/components/phlex_kit/toggle_group/toggle_group_item.rb +37 -0
  347. data/app/components/phlex_kit/tooltip/tooltip.css +28 -0
  348. data/app/components/phlex_kit/tooltip/tooltip.rb +15 -0
  349. data/app/components/phlex_kit/tooltip/tooltip_content.rb +12 -0
  350. data/app/components/phlex_kit/tooltip/tooltip_trigger.rb +13 -0
  351. data/app/components/phlex_kit/typography/blockquote.rb +12 -0
  352. data/app/components/phlex_kit/typography/heading.rb +38 -0
  353. data/app/components/phlex_kit/typography/inline_code.rb +13 -0
  354. data/app/components/phlex_kit/typography/inline_link.rb +15 -0
  355. data/app/components/phlex_kit/typography/text.rb +48 -0
  356. data/app/components/phlex_kit/typography/typography.css +50 -0
  357. data/app/javascript/phlex_kit/controllers/accordion_controller.js +59 -0
  358. data/app/javascript/phlex_kit/controllers/alert_dialog_controller.js +24 -0
  359. data/app/javascript/phlex_kit/controllers/avatar_controller.js +30 -0
  360. data/app/javascript/phlex_kit/controllers/calendar_controller.js +316 -0
  361. data/app/javascript/phlex_kit/controllers/calendar_input_controller.js +10 -0
  362. data/app/javascript/phlex_kit/controllers/carousel_controller.js +189 -0
  363. data/app/javascript/phlex_kit/controllers/chart_controller.js +135 -0
  364. data/app/javascript/phlex_kit/controllers/clipboard_controller.js +30 -0
  365. data/app/javascript/phlex_kit/controllers/collapsible_controller.js +14 -0
  366. data/app/javascript/phlex_kit/controllers/combobox_controller.js +303 -0
  367. data/app/javascript/phlex_kit/controllers/command_controller.js +161 -0
  368. data/app/javascript/phlex_kit/controllers/command_dialog_controller.js +37 -0
  369. data/app/javascript/phlex_kit/controllers/context_menu_controller.js +28 -0
  370. data/app/javascript/phlex_kit/controllers/data_table_column_visibility_controller.js +18 -0
  371. data/app/javascript/phlex_kit/controllers/data_table_controller.js +61 -0
  372. data/app/javascript/phlex_kit/controllers/data_table_search_controller.js +67 -0
  373. data/app/javascript/phlex_kit/controllers/dialog_controller.js +20 -0
  374. data/app/javascript/phlex_kit/controllers/dropdown_menu_controller.js +106 -0
  375. data/app/javascript/phlex_kit/controllers/form_field_controller.js +69 -0
  376. data/app/javascript/phlex_kit/controllers/hover_card_controller.js +11 -0
  377. data/app/javascript/phlex_kit/controllers/index.js +87 -0
  378. data/app/javascript/phlex_kit/controllers/input_otp_controller.js +66 -0
  379. data/app/javascript/phlex_kit/controllers/masked_input_controller.js +26 -0
  380. data/app/javascript/phlex_kit/controllers/menubar_controller.js +43 -0
  381. data/app/javascript/phlex_kit/controllers/message_scroller_controller.js +335 -0
  382. data/app/javascript/phlex_kit/controllers/popover_controller.js +12 -0
  383. data/app/javascript/phlex_kit/controllers/resizable_controller.js +42 -0
  384. data/app/javascript/phlex_kit/controllers/select_controller.js +141 -0
  385. data/app/javascript/phlex_kit/controllers/select_item_controller.js +14 -0
  386. data/app/javascript/phlex_kit/controllers/sheet_content_controller.js +6 -0
  387. data/app/javascript/phlex_kit/controllers/sheet_controller.js +10 -0
  388. data/app/javascript/phlex_kit/controllers/slider_controller.js +19 -0
  389. data/app/javascript/phlex_kit/controllers/tabs_controller.js +28 -0
  390. data/app/javascript/phlex_kit/controllers/theme_toggle_controller.js +22 -0
  391. data/app/javascript/phlex_kit/controllers/toast_controller.js +153 -0
  392. data/app/javascript/phlex_kit/controllers/toaster_controller.js +321 -0
  393. data/app/javascript/phlex_kit/controllers/toggle_controller.js +25 -0
  394. data/app/javascript/phlex_kit/controllers/toggle_group_controller.js +91 -0
  395. data/config/importmap.rb +7 -0
  396. data/lib/generators/phlex_kit/component/component_generator.rb +41 -0
  397. data/lib/generators/phlex_kit/install/install_generator.rb +51 -0
  398. data/lib/generators/phlex_kit/install/templates/phlex_kit.rb +11 -0
  399. data/lib/phlex_kit/base_component.rb +22 -0
  400. data/lib/phlex_kit/configuration.rb +46 -0
  401. data/lib/phlex_kit/engine.rb +50 -0
  402. data/lib/phlex_kit/propshaft_skip_source.rb +22 -0
  403. data/lib/phlex_kit/version.rb +5 -0
  404. data/lib/phlex_kit.rb +21 -0
  405. metadata +545 -0
@@ -0,0 +1,335 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="phlex-kit--message-scroller"
4
+ //
5
+ // A chat transcript scroller. Owns scroll state and behavior for a
6
+ // height-constrained message list:
7
+ //
8
+ // - autoScroll: follows the live edge while the reader is pinned to the
9
+ // bottom, and releases the moment they scroll, wheel, drag, or key away.
10
+ // - scrollAnchor: when a new anchored turn is appended, settles it near the
11
+ // top of the viewport keeping a peek of the previous turn above it.
12
+ // - defaultScrollPosition: where a freshly mounted transcript opens
13
+ // ("end", "start" or "last-anchor").
14
+ // - preserveScrollOnPrepend: keeps the visible row fixed when older messages
15
+ // are loaded in above the current view.
16
+ //
17
+ // Public API (callable from other controllers/outlets or future
18
+ // streaming/ActionCable code): scrollToEnd(), scrollToStart(),
19
+ // scrollToMessage(id). New rows appended to the content target are picked up
20
+ // automatically via MutationObserver — no manual call needed.
21
+ export default class extends Controller {
22
+ static targets = ["viewport", "content", "button"];
23
+
24
+ static values = {
25
+ autoScroll: { type: Boolean, default: true },
26
+ previousItemPeek: { type: Number, default: 64 },
27
+ defaultPosition: { type: String, default: "end" },
28
+ preserveOnPrepend: { type: Boolean, default: true },
29
+ endThreshold: { type: Number, default: 32 },
30
+ };
31
+
32
+ connect() {
33
+ // Reader is considered "following" the live edge until they move away.
34
+ this.following = true;
35
+ // True only while a programmatic scroll is in flight, so reader-intent
36
+ // handlers don't mistake our own scrolling for the reader's.
37
+ this.programmatic = false;
38
+
39
+ this.onScroll = this.onScroll.bind(this);
40
+ this.onWheel = this.onWheel.bind(this);
41
+ this.onTouchStart = this.onTouchStart.bind(this);
42
+ this.onKeydown = this.onKeydown.bind(this);
43
+
44
+ if (this.hasViewportTarget) {
45
+ this.viewportTarget.addEventListener("scroll", this.onScroll, { passive: true });
46
+ this.viewportTarget.addEventListener("wheel", this.onWheel, { passive: true });
47
+ this.viewportTarget.addEventListener("touchstart", this.onTouchStart, { passive: true });
48
+ this.viewportTarget.addEventListener("keydown", this.onKeydown);
49
+ }
50
+
51
+ if (this.hasContentTarget) {
52
+ // Announce streamed/added messages to assistive tech at a calm pace.
53
+ if (!this.contentTarget.hasAttribute("role")) {
54
+ this.contentTarget.setAttribute("role", "log");
55
+ }
56
+ if (!this.contentTarget.hasAttribute("aria-relevant")) {
57
+ this.contentTarget.setAttribute("aria-relevant", "additions text");
58
+ }
59
+
60
+ this.observer = new MutationObserver((records) => this.onMutations(records));
61
+ this.observer.observe(this.contentTarget, {
62
+ childList: true,
63
+ subtree: true,
64
+ characterData: true,
65
+ });
66
+ }
67
+
68
+ // Apply the opening position after layout settles.
69
+ requestAnimationFrame(() => {
70
+ this.applyDefaultPosition();
71
+ this.updateButton();
72
+ });
73
+ }
74
+
75
+ disconnect() {
76
+ if (this.hasViewportTarget) {
77
+ this.viewportTarget.removeEventListener("scroll", this.onScroll);
78
+ this.viewportTarget.removeEventListener("wheel", this.onWheel);
79
+ this.viewportTarget.removeEventListener("touchstart", this.onTouchStart);
80
+ this.viewportTarget.removeEventListener("keydown", this.onKeydown);
81
+ }
82
+ this.observer?.disconnect();
83
+ if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
84
+ }
85
+
86
+ // --- Reader intent -------------------------------------------------------
87
+
88
+ onScroll() {
89
+ if (this.programmatic) return;
90
+ this.following = this.isAtEnd();
91
+ this.updateButton();
92
+ }
93
+
94
+ // Any upward wheel is a deliberate move away from the live edge.
95
+ onWheel(event) {
96
+ if (event.deltaY < 0) this.release();
97
+ }
98
+
99
+ onTouchStart() {
100
+ // A touch that turns into an upward drag surfaces through onScroll; this
101
+ // just makes the release feel immediate when the reader grabs the list.
102
+ if (!this.isAtEnd()) this.release();
103
+ }
104
+
105
+ onKeydown(event) {
106
+ const navKeys = ["ArrowUp", "PageUp", "Home", "ArrowDown", "PageDown", "End", " "];
107
+ if (navKeys.includes(event.key)) this.release();
108
+ }
109
+
110
+ release() {
111
+ if (this.programmatic) return;
112
+ this.following = false;
113
+ }
114
+
115
+ // --- Mutations (new / prepended / streamed rows) -------------------------
116
+
117
+ onMutations(records) {
118
+ let appended = null;
119
+ let prependedHeight = 0;
120
+ let streamed = false;
121
+ const gap = this.rowGap();
122
+
123
+ for (const record of records) {
124
+ // Text streamed into an existing row (e.g. tokens) — not a new turn.
125
+ if (record.type === "characterData") {
126
+ streamed = true;
127
+ continue;
128
+ }
129
+ if (record.type !== "childList") continue;
130
+ // Only direct children of the content element are transcript rows.
131
+ // Markup inserted *inside* a message must not be mistaken for history.
132
+ if (record.target !== this.contentTarget) {
133
+ streamed = true;
134
+ continue;
135
+ }
136
+ for (const node of record.addedNodes) {
137
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
138
+ if (record.previousSibling === null && record.nextSibling !== null) {
139
+ // Inserted above existing rows → history prepend. Account for the
140
+ // flex row gap each prepended row introduces, or the preserved row
141
+ // drifts down by one gap per insertion.
142
+ prependedHeight += (node.offsetHeight || 0) + gap;
143
+ } else {
144
+ // Inserted at (or after) the end → new turn.
145
+ appended = node;
146
+ }
147
+ }
148
+ }
149
+
150
+ if (prependedHeight > 0 && this.preserveOnPrependValue) {
151
+ // Keep the reader's current row fixed while history loads in above.
152
+ this.viewportTarget.scrollTop += prependedHeight;
153
+ }
154
+
155
+ // Only move for new/streamed content while the reader is at the live edge.
156
+ // If they scrolled away, leave them there and let the button surface it.
157
+ const follow = this.autoScrollValue && this.following;
158
+ if (appended && follow) {
159
+ const anchor = appended.matches?.("[data-scroll-anchor]")
160
+ ? appended
161
+ : appended.querySelector?.("[data-scroll-anchor]");
162
+ if (anchor) {
163
+ this.scrollToAnchor(anchor);
164
+ } else {
165
+ this.scrollToEnd();
166
+ }
167
+ } else if (!appended && streamed && follow) {
168
+ // Text streamed into the last row. Stay pinned.
169
+ this.scrollToEnd("auto");
170
+ }
171
+
172
+ this.updateButton();
173
+ }
174
+
175
+ rowGap() {
176
+ if (!this.hasContentTarget) return 0;
177
+ const value = parseFloat(getComputedStyle(this.contentTarget).rowGap);
178
+ return Number.isFinite(value) ? value : 0;
179
+ }
180
+
181
+ // --- Public scroll commands ---------------------------------------------
182
+
183
+ scrollToEnd(behavior = "smooth") {
184
+ if (!this.hasViewportTarget) return;
185
+ this.following = true;
186
+ this.scrollTo(this.viewportTarget.scrollHeight, behavior);
187
+ }
188
+
189
+ scrollToStart(behavior = "smooth") {
190
+ if (!this.hasViewportTarget) return;
191
+ this.following = false;
192
+ this.scrollTo(0, behavior);
193
+ }
194
+
195
+ // Scroll a row with a matching messageId into view. Returns false when the
196
+ // target is not mounted.
197
+ scrollToMessage(id, behavior = "smooth") {
198
+ if (!this.hasContentTarget) return false;
199
+ const item = this.contentTarget.querySelector(`[data-message-id="${CSS.escape(id)}"]`);
200
+ if (!item) return false;
201
+ this.following = false;
202
+ this.scrollToAnchor(item, behavior);
203
+ return true;
204
+ }
205
+
206
+ scrollToAnchor(item, behavior = "smooth") {
207
+ const top = Math.max(0, item.offsetTop - this.previousItemPeekValue);
208
+ this.scrollTo(top, behavior);
209
+ }
210
+
211
+ // Bound to the scroll button's click action. Honors the button's
212
+ // data-direction so a start-direction button jumps to the start.
213
+ jump(event) {
214
+ if (event?.currentTarget?.dataset.direction === "start") {
215
+ this.scrollToStart();
216
+ } else {
217
+ this.scrollToEnd();
218
+ }
219
+ }
220
+
221
+ // --- Internals -----------------------------------------------------------
222
+
223
+ // Native scrollTo({ behavior: "smooth" }) is unreliable on a contained,
224
+ // virtualized viewport, so we animate scrollTop ourselves with rAF. This
225
+ // gives us full control over completion (no scrollend dependency) and lets
226
+ // us honor reduced-motion.
227
+ scrollTo(top, behavior = "smooth") {
228
+ if (!this.hasViewportTarget) return;
229
+ const max = this.viewportTarget.scrollHeight - this.viewportTarget.clientHeight;
230
+ const target = Math.max(0, Math.min(top, max));
231
+
232
+ this.programmatic = true;
233
+ this.element.setAttribute("data-autoscrolling", "");
234
+ this.viewportTarget.setAttribute("data-autoscrolling", "");
235
+ if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
236
+
237
+ if (behavior === "auto" || this.prefersReducedMotion()) {
238
+ this.viewportTarget.scrollTop = target;
239
+ this.finishScroll();
240
+ return;
241
+ }
242
+
243
+ const start = this.viewportTarget.scrollTop;
244
+ const distance = target - start;
245
+ const duration = 300;
246
+ let startTime = null;
247
+
248
+ const step = (now) => {
249
+ if (startTime === null) startTime = now;
250
+ const t = Math.min(1, (now - startTime) / duration);
251
+ // easeOutCubic
252
+ const eased = 1 - Math.pow(1 - t, 3);
253
+ this.viewportTarget.scrollTop = start + distance * eased;
254
+ if (t < 1) {
255
+ this.animationFrame = requestAnimationFrame(step);
256
+ } else {
257
+ this.finishScroll();
258
+ }
259
+ };
260
+ this.animationFrame = requestAnimationFrame(step);
261
+ }
262
+
263
+ finishScroll() {
264
+ this.programmatic = false;
265
+ this.element.removeAttribute("data-autoscrolling");
266
+ this.viewportTarget?.removeAttribute("data-autoscrolling");
267
+ this.following = this.isAtEnd();
268
+ this.updateButton();
269
+ }
270
+
271
+ prefersReducedMotion() {
272
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
273
+ }
274
+
275
+ applyDefaultPosition() {
276
+ if (!this.hasViewportTarget) return;
277
+ const position = this.defaultPositionValue;
278
+
279
+ if (position === "start") {
280
+ this.following = false;
281
+ this.viewportTarget.scrollTop = 0;
282
+ return;
283
+ }
284
+
285
+ if (position === "last-anchor") {
286
+ // Stimulus' contentTarget getter throws when missing — guard explicitly.
287
+ const anchors = this.hasContentTarget
288
+ ? this.contentTarget.querySelectorAll("[data-scroll-anchor]")
289
+ : [];
290
+ const last = anchors[anchors.length - 1];
291
+ // Fall back to the end when there's no anchor, or the last turn already
292
+ // fits in the viewport.
293
+ if (last && last.offsetTop - this.previousItemPeekValue > 0) {
294
+ this.following = false;
295
+ this.viewportTarget.scrollTop = Math.max(0, last.offsetTop - this.previousItemPeekValue);
296
+ this.updateButton();
297
+ return;
298
+ }
299
+ }
300
+
301
+ // Default: open at the live edge.
302
+ this.following = true;
303
+ this.viewportTarget.scrollTop = this.viewportTarget.scrollHeight;
304
+ }
305
+
306
+ isAtEnd() {
307
+ if (!this.hasViewportTarget) return true;
308
+ const { scrollTop, clientHeight, scrollHeight } = this.viewportTarget;
309
+ return scrollHeight - (scrollTop + clientHeight) <= this.endThresholdValue;
310
+ }
311
+
312
+ isAtStart() {
313
+ if (!this.hasViewportTarget) return true;
314
+ return this.viewportTarget.scrollTop <= this.endThresholdValue;
315
+ }
316
+
317
+ hasOverflow() {
318
+ if (!this.hasViewportTarget) return false;
319
+ return this.viewportTarget.scrollHeight - this.viewportTarget.clientHeight > this.endThresholdValue;
320
+ }
321
+
322
+ // Each button activates based on its own direction: an end button when the
323
+ // reader is away from the bottom, a start button when away from the top.
324
+ updateButton() {
325
+ if (!this.hasButtonTarget) return;
326
+ const overflow = this.hasOverflow();
327
+ this.buttonTargets.forEach((button) => {
328
+ const toStart = button.dataset.direction === "start";
329
+ const active = overflow && (toStart ? !this.isAtStart() : !this.isAtEnd());
330
+ button.setAttribute("data-active", active ? "true" : "false");
331
+ // Remove the inert button from the tab order so there are no ghost stops.
332
+ button.setAttribute("tabindex", active ? "0" : "-1");
333
+ });
334
+ }
335
+ }
@@ -0,0 +1,12 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="phlex-kit--popover". Click to toggle; click
4
+ // outside to close. CSS positions the panel under the trigger.
5
+ export default class extends Controller {
6
+ static targets = ["content"]
7
+ connect() { this.onDoc = (e) => { if (!this.element.contains(e.target)) this.hide() } }
8
+ disconnect() { document.removeEventListener("click", this.onDoc) }
9
+ toggle(e) { e?.preventDefault(); this.contentTarget.classList.contains("pk-hidden") ? this.show() : this.hide() }
10
+ show() { this.contentTarget.classList.remove("pk-hidden"); this.contentTarget.dataset.state = "open"; document.addEventListener("click", this.onDoc) }
11
+ hide() { this.contentTarget.classList.add("pk-hidden"); this.contentTarget.dataset.state = "closed"; document.removeEventListener("click", this.onDoc) }
12
+ }
@@ -0,0 +1,42 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // PhlexKit's stand-in for react-resizable-panels behind shadcn's Resizable:
4
+ // dragging a handle rebalances the flex-grow of the two panels around it.
5
+ // Connects to data-controller="phlex-kit--resizable"
6
+ export default class extends Controller {
7
+ static targets = ["panel", "handle"]
8
+ static values = { direction: { type: String, default: "horizontal" } }
9
+
10
+ start(e) {
11
+ const handle = e.currentTarget
12
+ const prev = handle.previousElementSibling
13
+ const next = handle.nextElementSibling
14
+ if (!prev || !next) return
15
+ e.preventDefault()
16
+
17
+ const horizontal = this.directionValue === "horizontal"
18
+ const sizeOf = (el) => horizontal ? el.getBoundingClientRect().width : el.getBoundingClientRect().height
19
+ const drag = {
20
+ startPos: horizontal ? e.clientX : e.clientY,
21
+ prevSize: sizeOf(prev),
22
+ nextSize: sizeOf(next),
23
+ }
24
+ try { handle.setPointerCapture(e.pointerId) } catch {}
25
+
26
+ const onMove = (ev) => {
27
+ const delta = (horizontal ? ev.clientX : ev.clientY) - drag.startPos
28
+ const total = drag.prevSize + drag.nextSize
29
+ const prevSize = Math.min(Math.max(drag.prevSize + delta, 0), total)
30
+ prev.style.flexGrow = (prevSize / total) * 2
31
+ next.style.flexGrow = ((total - prevSize) / total) * 2
32
+ }
33
+ const onUp = () => {
34
+ handle.removeEventListener("pointermove", onMove)
35
+ handle.removeEventListener("pointerup", onUp)
36
+ handle.removeEventListener("pointercancel", onUp)
37
+ }
38
+ handle.addEventListener("pointermove", onMove)
39
+ handle.addEventListener("pointerup", onUp)
40
+ handle.addEventListener("pointercancel", onUp)
41
+ }
42
+ }
@@ -0,0 +1,141 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Ported from ruby_ui's phlex-kit--select controller. The ONE change from upstream:
4
+ // the @floating-ui/dom dependency is removed — the panel is positioned with plain
5
+ // CSS (.ui-select-content { position:absolute; top:100% }) instead of
6
+ // computePosition/autoUpdate, so this app needs no npm/build step. Everything else
7
+ // (open/close, item selection, keyboard nav, click-outside) is unchanged.
8
+ export default class extends Controller {
9
+ static targets = ["trigger", "content", "input", "value", "item"];
10
+ static values = { open: Boolean };
11
+ static outlets = ["phlex-kit--select-item"];
12
+
13
+ connect() {
14
+ this.generateItemsIds();
15
+ }
16
+
17
+ selectItem(event) {
18
+ event.preventDefault();
19
+
20
+ this.phlexKitSelectItemOutlets.forEach((item) => item.handleSelectItem(event));
21
+
22
+ const oldValue = this.inputTarget.value;
23
+ const newValue = event.target.dataset.value;
24
+
25
+ this.inputTarget.value = newValue;
26
+ this.valueTarget.innerText = event.target.innerText;
27
+
28
+ this.dispatchOnChange(oldValue, newValue);
29
+ this.closeContent();
30
+ }
31
+
32
+ onClick() {
33
+ this.toogleContent();
34
+
35
+ if (this.openValue) {
36
+ this.setFocusAndCurrent();
37
+ } else {
38
+ this.resetCurrent();
39
+ }
40
+ }
41
+
42
+ handleKeyDown(event) {
43
+ event.preventDefault();
44
+
45
+ const currentIndex = this.itemTargets.findIndex(
46
+ (item) => item.getAttribute("aria-current") === "true",
47
+ );
48
+
49
+ if (currentIndex + 1 < this.itemTargets.length) {
50
+ this.itemTargets[currentIndex].removeAttribute("aria-current");
51
+ this.setAriaCurrentAndActiveDescendant(currentIndex + 1);
52
+ }
53
+ }
54
+
55
+ handleKeyUp(event) {
56
+ event.preventDefault();
57
+
58
+ const currentIndex = this.itemTargets.findIndex(
59
+ (item) => item.getAttribute("aria-current") === "true",
60
+ );
61
+
62
+ if (currentIndex > 0) {
63
+ this.itemTargets[currentIndex].removeAttribute("aria-current");
64
+ this.setAriaCurrentAndActiveDescendant(currentIndex - 1);
65
+ }
66
+ }
67
+
68
+ handleEsc(event) {
69
+ event.preventDefault();
70
+ this.closeContent();
71
+ }
72
+
73
+ setFocusAndCurrent() {
74
+ const selectedItem = this.itemTargets.find(
75
+ (item) => item.getAttribute("aria-selected") === "true",
76
+ );
77
+
78
+ if (selectedItem) {
79
+ selectedItem.focus({ preventScroll: true });
80
+ selectedItem.setAttribute("aria-current", "true");
81
+ this.triggerTarget.setAttribute("aria-activedescendant", selectedItem.getAttribute("id"));
82
+ } else {
83
+ this.itemTarget.focus({ preventScroll: true });
84
+ this.itemTarget.setAttribute("aria-current", "true");
85
+ this.triggerTarget.setAttribute("aria-activedescendant", this.itemTarget.getAttribute("id"));
86
+ }
87
+ }
88
+
89
+ resetCurrent() {
90
+ this.itemTargets.forEach((item) => item.removeAttribute("aria-current"));
91
+ }
92
+
93
+ clickOutside(event) {
94
+ if (!this.openValue) return;
95
+ if (this.element.contains(event.target)) return;
96
+
97
+ event.preventDefault();
98
+ this.toogleContent();
99
+ }
100
+
101
+ toogleContent() {
102
+ this.openValue = !this.openValue;
103
+ this.contentTarget.classList.toggle("hidden");
104
+ this.triggerTarget.setAttribute("aria-expanded", this.openValue);
105
+ }
106
+
107
+ generateItemsIds() {
108
+ const contentId = this.contentTarget.getAttribute("id");
109
+ this.triggerTarget.setAttribute("aria-controls", contentId);
110
+
111
+ this.itemTargets.forEach((item, index) => {
112
+ item.id = `${contentId}-${index}`;
113
+ });
114
+ }
115
+
116
+ setAriaCurrentAndActiveDescendant(currentIndex) {
117
+ const currentItem = this.itemTargets[currentIndex];
118
+ currentItem.focus({ preventScroll: true });
119
+ currentItem.setAttribute("aria-current", "true");
120
+ this.triggerTarget.setAttribute("aria-activedescendant", currentItem.getAttribute("id"));
121
+ }
122
+
123
+ closeContent() {
124
+ this.toogleContent();
125
+ this.resetCurrent();
126
+
127
+ this.triggerTarget.setAttribute("aria-activedescendant", true);
128
+ this.triggerTarget.focus({ preventScroll: true });
129
+ }
130
+
131
+ dispatchOnChange(oldValue, newValue) {
132
+ if (oldValue === newValue) return;
133
+
134
+ const event = new InputEvent("change", {
135
+ bubbles: true,
136
+ cancelable: true,
137
+ });
138
+
139
+ this.inputTarget.dispatchEvent(event);
140
+ }
141
+ }
@@ -0,0 +1,14 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Ported verbatim from ruby_ui's phlex-kit--select-item controller. Flips its own
4
+ // element's aria-selected when an item is chosen (the select controller invokes
5
+ // this on every item via the outlet), which drives the checkmark in select.css.
6
+ export default class extends Controller {
7
+ handleSelectItem({ target }) {
8
+ if (this.element.dataset.value == target.dataset.value) {
9
+ this.element.setAttribute("aria-selected", true);
10
+ } else {
11
+ this.element.removeAttribute("aria-selected");
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,6 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="phlex-kit--sheet-content" (on the cloned node).
4
+ export default class extends Controller {
5
+ close() { this.element.remove() }
6
+ }
@@ -0,0 +1,10 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="phlex-kit--sheet". Clones the content <template>
4
+ // into <body> on open. Ported from ruby_ui.
5
+ export default class extends Controller {
6
+ static targets = ["content"]
7
+ static values = { open: { type: Boolean, default: false } }
8
+ connect() { if (this.openValue) this.open() }
9
+ open() { document.body.insertAdjacentHTML("beforeend", this.contentTarget.innerHTML) }
10
+ }
@@ -0,0 +1,19 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Keeps .pk-slider's filled-track width (--pk-slider-progress) in sync with
4
+ // the native range value. PhlexKit addition for shadcn's Slider — no Radix.
5
+ // Connects to data-controller="phlex-kit--slider"
6
+ export default class extends Controller {
7
+ connect() {
8
+ this.update()
9
+ }
10
+
11
+ update() {
12
+ const el = this.element
13
+ const min = Number(el.min || 0)
14
+ const max = Number(el.max || 100)
15
+ const span = max - min
16
+ const progress = span === 0 ? 0 : ((Number(el.value) - min) / span) * 100
17
+ el.style.setProperty("--pk-slider-progress", `${progress}%`)
18
+ }
19
+ }
@@ -0,0 +1,28 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="phlex-kit--tabs"
4
+ export default class extends Controller {
5
+ static targets = ["trigger", "content"]
6
+ static values = { active: String }
7
+
8
+ connect() {
9
+ if (!this.hasActiveValue && this.triggerTargets.length > 0) {
10
+ this.activeValue = this.triggerTargets[0].dataset.value
11
+ }
12
+ }
13
+
14
+ show(e) { this.activeValue = e.currentTarget.dataset.value }
15
+
16
+ activeValueChanged(current, previous) {
17
+ if (current === "" || current === previous) return
18
+ this.contentTargets.forEach((el) => el.classList.add("pk-hidden"))
19
+ this.triggerTargets.forEach((el) => { el.dataset.state = "inactive" })
20
+ const c = this.activeContentTarget()
21
+ if (c) c.classList.remove("pk-hidden")
22
+ const t = this.activeTriggerTarget()
23
+ if (t) t.dataset.state = "active"
24
+ }
25
+
26
+ activeTriggerTarget() { return this.triggerTargets.find((el) => el.dataset.value === this.activeValue) }
27
+ activeContentTarget() { return this.contentTargets.find((el) => el.dataset.value === this.activeValue) }
28
+ }
@@ -0,0 +1,22 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="phlex-kit--theme-toggle". Sits on the same wrapper
4
+ // as phlex-kit--toggle; pressed = dark. Flips :root[data-theme] to match the
5
+ // PhlexKit token system (not .dark/.light classes). Adapted from ruby_ui.
6
+ export default class extends Controller {
7
+ connect() { this.applyTheme(this.currentTheme()) }
8
+ apply(e) {
9
+ const theme = e.detail?.pressed ? "dark" : "light"
10
+ localStorage.theme = theme
11
+ this.applyTheme(theme)
12
+ }
13
+ currentTheme() {
14
+ if (localStorage.theme === "dark") return "dark"
15
+ if (localStorage.theme === "light") return "light"
16
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
17
+ }
18
+ applyTheme(theme) {
19
+ document.documentElement.setAttribute("data-theme", theme)
20
+ this.element.setAttribute("data-phlex-kit--toggle-pressed-value", theme === "dark" ? "true" : "false")
21
+ }
22
+ }