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,153 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const SWIPE_THRESHOLD = 45
4
+ const TIME_BEFORE_UNMOUNT = 200
5
+
6
+ // Ported from ruby_ui's toast controller (Stimulus-only upstream), identifiers and
7
+ // events renamed ruby-ui → phlex-kit; behaviour (timer, swipe, Escape) unchanged.
8
+ // Connects to data-controller="phlex-kit--toast"
9
+ export default class extends Controller {
10
+ static values = {
11
+ duration: { type: Number, default: 4000 },
12
+ dismissible: { type: Boolean, default: true },
13
+ invert: { type: Boolean, default: false },
14
+ onDismiss: String,
15
+ onAutoClose: String,
16
+ }
17
+
18
+ connect() {
19
+ this._timer = null
20
+ this._startedAt = 0
21
+ this._remaining = this.durationValue
22
+ this._paused = false
23
+ this._swipe = { active: false, x: 0, y: 0, startedAt: 0 }
24
+
25
+ this._onPointerDown = this._onPointerDown.bind(this)
26
+ this._onPointerMove = this._onPointerMove.bind(this)
27
+ this._onPointerUp = this._onPointerUp.bind(this)
28
+ this._onPointerEnter = () => this._pause()
29
+ this._onPointerLeave = () => { if (!this._swipe.active) this._resume() }
30
+ this._onKeyDown = this._onKeyDown.bind(this)
31
+ this._onForceDismiss = (e) => { e.stopPropagation(); this._close() }
32
+ this._onRestart = () => this._restart()
33
+ this._onRegionPause = () => this._pause()
34
+ this._onRegionResume = () => this._resume()
35
+
36
+ this.element.addEventListener("pointerdown", this._onPointerDown)
37
+ this.element.addEventListener("pointerenter", this._onPointerEnter)
38
+ this.element.addEventListener("pointerleave", this._onPointerLeave)
39
+ this.element.addEventListener("keydown", this._onKeyDown)
40
+ this.element.addEventListener("phlex-kit:toast:force-dismiss", this._onForceDismiss)
41
+ this.element.addEventListener("phlex-kit:toast:restart", this._onRestart)
42
+ document.addEventListener("phlex-kit:toast:pause", this._onRegionPause)
43
+ document.addEventListener("phlex-kit:toast:resume", this._onRegionResume)
44
+
45
+ requestAnimationFrame(() => {
46
+ this.element.dataset.state = "open"
47
+ this._start()
48
+ })
49
+ }
50
+
51
+ disconnect() {
52
+ this._clearTimer()
53
+ this.element.removeEventListener("pointerdown", this._onPointerDown)
54
+ this.element.removeEventListener("pointerenter", this._onPointerEnter)
55
+ this.element.removeEventListener("pointerleave", this._onPointerLeave)
56
+ this.element.removeEventListener("keydown", this._onKeyDown)
57
+ this.element.removeEventListener("phlex-kit:toast:force-dismiss", this._onForceDismiss)
58
+ this.element.removeEventListener("phlex-kit:toast:restart", this._onRestart)
59
+ document.removeEventListener("phlex-kit:toast:pause", this._onRegionPause)
60
+ document.removeEventListener("phlex-kit:toast:resume", this._onRegionResume)
61
+ }
62
+
63
+ dismiss(e) {
64
+ e?.preventDefault()
65
+ if (!this.dismissibleValue) return
66
+ this._close("dismiss")
67
+ }
68
+
69
+ _close(reason) {
70
+ if (this.element.dataset.state === "closing") return
71
+ this.element.dataset.state = "closing"
72
+ this.element.dispatchEvent(new CustomEvent(reason === "auto" ? "phlex-kit:toast:auto-close" : "phlex-kit:toast:dismiss", { bubbles: true, detail: { id: this.element.id } }))
73
+ setTimeout(() => this.element.remove(), TIME_BEFORE_UNMOUNT)
74
+ }
75
+
76
+ _start() {
77
+ if (!Number.isFinite(this.durationValue) || this.durationValue <= 0) return
78
+ this._startedAt = performance.now()
79
+ this._remaining = this.durationValue
80
+ this._timer = setTimeout(() => this._close("auto"), this._remaining)
81
+ }
82
+
83
+ _restart() {
84
+ this._clearTimer()
85
+ this._start()
86
+ }
87
+
88
+ _pause() {
89
+ if (this._paused || !this._timer) return
90
+ this._paused = true
91
+ clearTimeout(this._timer)
92
+ this._timer = null
93
+ this._remaining -= performance.now() - this._startedAt
94
+ }
95
+
96
+ _resume() {
97
+ if (!this._paused) return
98
+ this._paused = false
99
+ if (this._remaining <= 0) return this._close("auto")
100
+ this._startedAt = performance.now()
101
+ this._timer = setTimeout(() => this._close("auto"), this._remaining)
102
+ }
103
+
104
+ _clearTimer() {
105
+ if (this._timer) clearTimeout(this._timer)
106
+ this._timer = null
107
+ }
108
+
109
+ _onKeyDown(e) {
110
+ if (e.key === "Escape" && this.dismissibleValue) this.dismiss(e)
111
+ }
112
+
113
+ _onPointerDown(e) {
114
+ if (!this.dismissibleValue) return
115
+ if (e.target.closest("button")) return
116
+ try { this.element.setPointerCapture(e.pointerId) } catch {}
117
+ this._swipe = { active: true, x: e.clientX, y: e.clientY, startedAt: performance.now(), pointerId: e.pointerId }
118
+ this.element.dataset.swipe = "start"
119
+ this.element.addEventListener("pointermove", this._onPointerMove)
120
+ this.element.addEventListener("pointerup", this._onPointerUp)
121
+ this.element.addEventListener("pointercancel", this._onPointerUp)
122
+ }
123
+
124
+ _onPointerMove(e) {
125
+ const dx = e.clientX - this._swipe.x
126
+ const dy = e.clientY - this._swipe.y
127
+ this.element.dataset.swipe = "move"
128
+ this.element.style.transform = `translate(${dx}px, ${dy}px)`
129
+ }
130
+
131
+ _onPointerUp(e) {
132
+ const dx = e.clientX - this._swipe.x
133
+ const dy = e.clientY - this._swipe.y
134
+ const dist = Math.hypot(dx, dy)
135
+ const dt = performance.now() - this._swipe.startedAt
136
+ const velocity = dist / Math.max(dt, 1)
137
+ this.element.removeEventListener("pointermove", this._onPointerMove)
138
+ this.element.removeEventListener("pointerup", this._onPointerUp)
139
+ this.element.removeEventListener("pointercancel", this._onPointerUp)
140
+ this._swipe.active = false
141
+ if (dist > SWIPE_THRESHOLD || velocity > 0.5) {
142
+ this.element.style.setProperty("--swipe-end-x", `${Math.sign(dx) * 500}px`)
143
+ this.element.style.setProperty("--swipe-end-y", `${Math.sign(dy) * 500}px`)
144
+ this.element.dataset.swipe = "end"
145
+ this.element.style.transform = ""
146
+ this._close("dismiss")
147
+ } else {
148
+ this.element.dataset.swipe = "cancel"
149
+ this.element.style.transform = ""
150
+ this._resume()
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,321 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ const VARIANTS = ["default", "success", "error", "warning", "info", "loading"]
4
+
5
+ let streamActionRegistered = false
6
+
7
+ function registerStreamAction() {
8
+ if (streamActionRegistered) return
9
+ if (typeof window === "undefined") return
10
+ const Turbo = window.Turbo
11
+ if (!Turbo?.StreamActions) return
12
+ Turbo.StreamActions.toast = function () {
13
+ const detail = {}
14
+ for (const attr of this.attributes) {
15
+ if (attr.name === "action" || attr.name === "target" || attr.name === "targets") continue
16
+ detail[attr.name] = attr.value
17
+ }
18
+ if (detail.duration != null && detail.duration !== "") detail.duration = Number(detail.duration)
19
+ if (detail.dismissible != null) detail.dismissible = detail.dismissible !== "false"
20
+ window.dispatchEvent(new CustomEvent("phlex-kit:toast", { detail }))
21
+ }
22
+ streamActionRegistered = true
23
+ }
24
+
25
+ // Ported from ruby_ui's toaster controller (Stimulus-only upstream) with identifiers,
26
+ // events and the window API renamed ruby-ui/RubyUI → phlex-kit/PhlexKit, and the
27
+ // Tailwind pr-10 spawn class swapped for .pk-toast-with-close (toast.css).
28
+ // Connects to data-controller="phlex-kit--toaster"
29
+ export default class extends Controller {
30
+ static targets = ["skeleton", "toast", "actionTpl", "cancelTpl", "closeTpl"]
31
+ static values = {
32
+ position: { type: String, default: "bottom-right" },
33
+ expand: { type: Boolean, default: false },
34
+ max: { type: Number, default: 3 },
35
+ duration: { type: Number, default: 4000 },
36
+ gap: { type: Number, default: 14 },
37
+ offset: { type: Number, default: 24 },
38
+ theme: { type: String, default: "system" },
39
+ richColors: { type: Boolean, default: false },
40
+ closeButton: { type: Boolean, default: false },
41
+ hotkey: { type: String, default: "alt+t" },
42
+ dir: { type: String, default: "ltr" },
43
+ }
44
+
45
+ // State the target callbacks touch lives here, NOT in connect(): Stimulus
46
+ // fires toastTargetConnected() BEFORE connect() for toasts already in the
47
+ // HTML (server-rendered flash), and upstream's connect()-time init crashes
48
+ // on that path.
49
+ initialize() {
50
+ this._heights = new Map()
51
+ this._resizeObservers = new WeakMap()
52
+ this._expanded = false
53
+ }
54
+
55
+ connect() {
56
+ this._expanded = this.expandValue
57
+ this._listEl = this.element.querySelector("ol") || (this.element.tagName === "OL" ? this.element : null)
58
+ this._registerGlobalApi()
59
+ registerStreamAction()
60
+ if (!this._listEl) return
61
+
62
+ this._onPointerEnter = () => this._setExpanded(true)
63
+ this._onPointerLeave = () => { if (!this.expandValue) this._setExpanded(false) }
64
+ this._onWindowToast = (e) => this._spawn(e.detail || {})
65
+ this._onWindowDismissAll = () => this._dismissById(null)
66
+ this._onKey = this._onKey.bind(this)
67
+
68
+ window.addEventListener("phlex-kit:toast", this._onWindowToast)
69
+ window.addEventListener("phlex-kit:toast:dismiss-all", this._onWindowDismissAll)
70
+ this._listEl.addEventListener("pointerenter", this._onPointerEnter)
71
+ this._listEl.addEventListener("pointerleave", this._onPointerLeave)
72
+ document.addEventListener("keydown", this._onKey)
73
+
74
+ // Settle any server-rendered toasts (their targetConnected ran pre-connect,
75
+ // before _listEl existed, so their reflow was a no-op).
76
+ this._reflow()
77
+ }
78
+
79
+ disconnect() {
80
+ window.removeEventListener("phlex-kit:toast", this._onWindowToast)
81
+ window.removeEventListener("phlex-kit:toast:dismiss-all", this._onWindowDismissAll)
82
+ this._listEl?.removeEventListener("pointerenter", this._onPointerEnter)
83
+ this._listEl?.removeEventListener("pointerleave", this._onPointerLeave)
84
+ document.removeEventListener("keydown", this._onKey)
85
+ }
86
+
87
+ toastTargetConnected(el) {
88
+ if (typeof ResizeObserver !== "undefined") {
89
+ const ro = new ResizeObserver(() => {
90
+ this._heights.set(el, el.offsetHeight)
91
+ this._reflow()
92
+ })
93
+ ro.observe(el)
94
+ this._resizeObservers.set(el, ro)
95
+ }
96
+ this._heights.set(el, el.offsetHeight || 64)
97
+ this._reflow()
98
+ }
99
+
100
+ toastTargetDisconnected(el) {
101
+ this._resizeObservers.get(el)?.disconnect()
102
+ this._resizeObservers.delete(el)
103
+ this._heights.delete(el)
104
+ this._reflow()
105
+ }
106
+
107
+ _spawn(detail) {
108
+ const variant = VARIANTS.includes(detail.variant) ? detail.variant : "default"
109
+ const tpl = this._skeletonFor(variant)
110
+ if (!tpl) return null
111
+ if (detail.position) {
112
+ this.element.setAttribute("data-position", detail.position)
113
+ this.positionValue = detail.position
114
+ }
115
+ const node = tpl.content.firstElementChild.cloneNode(true)
116
+
117
+ node.id = detail.id || `toast-${this._uuid()}`
118
+ if (detail.duration != null) {
119
+ const dur = detail.duration === Infinity ? 0 : detail.duration
120
+ node.setAttribute("data-phlex-kit--toast-duration-value", String(dur))
121
+ }
122
+ if (detail.dismissible === false) {
123
+ node.setAttribute("data-phlex-kit--toast-dismissible-value", "false")
124
+ }
125
+ if (detail.className) node.className += ` ${detail.className}`
126
+
127
+ const titleEl = node.querySelector('[data-slot="title"]')
128
+ if (titleEl) titleEl.textContent = detail.title || detail.message || ""
129
+ const descEl = node.querySelector('[data-slot="description"]')
130
+ if (descEl) {
131
+ if (detail.description) descEl.textContent = detail.description
132
+ else descEl.remove()
133
+ }
134
+
135
+ if (detail.action && detail.action.label && this.hasActionTplTarget) {
136
+ const btn = this._cloneSlot(this.actionTplTarget)
137
+ btn.textContent = detail.action.label
138
+ btn.addEventListener("click", (ev) => {
139
+ try { detail.action.onClick?.(ev) } finally {
140
+ node.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
141
+ }
142
+ })
143
+ node.appendChild(btn)
144
+ }
145
+
146
+ if (detail.cancel && detail.cancel.label && this.hasCancelTplTarget) {
147
+ const btn = this._cloneSlot(this.cancelTplTarget)
148
+ btn.textContent = detail.cancel.label
149
+ node.appendChild(btn)
150
+ }
151
+
152
+ if (detail.closeButton && this.hasCloseTplTarget) {
153
+ const x = this._cloneSlot(this.closeTplTarget)
154
+ node.classList.add("pk-toast-with-close")
155
+ node.appendChild(x)
156
+ }
157
+
158
+ this._listEl.appendChild(node)
159
+ return node.id
160
+ }
161
+
162
+ _dismissById(id) {
163
+ if (!id) {
164
+ this.toastTargets.forEach((el) =>
165
+ el.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
166
+ )
167
+ return
168
+ }
169
+ const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
170
+ if (el) el.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
171
+ }
172
+
173
+ _skeletonFor(variant) {
174
+ return this.skeletonTargets.find((t) => t.dataset.variant === variant)
175
+ }
176
+
177
+ _cloneSlot(tpl) {
178
+ return tpl.content.firstElementChild.cloneNode(true)
179
+ }
180
+
181
+ _setExpanded(value) {
182
+ if (this._expanded === value) return
183
+ this._expanded = value
184
+ document.dispatchEvent(new CustomEvent(value ? "phlex-kit:toast:pause" : "phlex-kit:toast:resume"))
185
+ this._reflow()
186
+ }
187
+
188
+ _reflow() {
189
+ if (!this._listEl) return
190
+ const isBottom = this.positionValue.startsWith("bottom")
191
+ const items = this.toastTargets
192
+ const order = isBottom ? items.slice().reverse() : items.slice()
193
+ const heights = order.map(el => this._heights.get(el) || el.offsetHeight || 64)
194
+ const gap = this.gapValue
195
+ const peekOffset = 16
196
+ const peekScaleStep = 0.05
197
+ const peekOpacityStep = 0.2
198
+
199
+ const expandedHeight = heights.reduce((a, b) => a + b, 0) + gap * Math.max(0, heights.length - 1)
200
+ const collapsedHeight = (heights[0] || 0) + Math.min(2, Math.max(0, heights.length - 1)) * peekOffset
201
+ this._listEl.style.minHeight = `${this._expanded ? expandedHeight : collapsedHeight}px`
202
+
203
+ let acc = 0
204
+ order.forEach((el, i) => {
205
+ const visible = i < this.maxValue
206
+ let yOffset, scale, opacity
207
+
208
+ if (this._expanded) {
209
+ yOffset = acc + i * gap
210
+ scale = 1
211
+ opacity = visible ? 1 : 0
212
+ } else {
213
+ yOffset = i * peekOffset
214
+ scale = Math.max(0.85, 1 - i * peekScaleStep)
215
+ opacity = visible ? Math.max(0, 1 - i * peekOpacityStep) : 0
216
+ }
217
+
218
+ const sign = isBottom ? -1 : 1
219
+ const ty = sign * yOffset
220
+
221
+ el.style.setProperty("--opacity", String(opacity))
222
+ el.style.setProperty("--scale", String(scale))
223
+ el.style.setProperty("--y-offset", `${ty}px`)
224
+ el.style.transformOrigin = isBottom ? "center bottom" : "center top"
225
+ el.style.top = isBottom ? "auto" : "0"
226
+ el.style.bottom = isBottom ? "0" : "auto"
227
+ el.style.transform = `translate3d(0, ${ty}px, 0) scale(${scale})`
228
+ el.style.zIndex = String(1000 - i)
229
+ el.style.pointerEvents = visible ? "auto" : "none"
230
+ el.tabIndex = visible ? 0 : -1
231
+
232
+ acc += heights[i] || 0
233
+ })
234
+
235
+ this._enforceMax(items)
236
+ }
237
+
238
+ _enforceMax(items) {
239
+ if (items.length <= this.maxValue) return
240
+ const isBottom = this.positionValue.startsWith("bottom")
241
+ const dropping = items.length - this.maxValue
242
+ const candidates = isBottom ? items.slice(0, dropping) : items.slice(-dropping)
243
+ candidates.forEach(el => {
244
+ if (el.dataset.state !== "closing") {
245
+ el.dispatchEvent(new CustomEvent("phlex-kit:toast:force-dismiss", { bubbles: true }))
246
+ }
247
+ })
248
+ }
249
+
250
+ _onKey(e) {
251
+ const parts = (this.hotkeyValue || "alt+t").split("+")
252
+ const key = parts.pop()
253
+ const wantAlt = parts.includes("alt")
254
+ const wantCtrl = parts.includes("ctrl")
255
+ const wantMeta = parts.includes("meta")
256
+ if (e.key.toLowerCase() !== key.toLowerCase()) return
257
+ if (wantAlt !== e.altKey) return
258
+ if (wantCtrl !== e.ctrlKey) return
259
+ if (wantMeta !== e.metaKey) return
260
+ e.preventDefault()
261
+ const first = this._listEl.firstElementChild
262
+ first?.focus()
263
+ }
264
+
265
+ _registerGlobalApi() {
266
+ const fire = (variant, message, opts = {}) =>
267
+ window.dispatchEvent(new CustomEvent("phlex-kit:toast", {
268
+ detail: { ...opts, variant, message: opts.title || message }
269
+ }))
270
+
271
+ const api = (message, opts) => fire("default", message, opts)
272
+ api.success = (m, o) => fire("success", m, o)
273
+ api.error = (m, o) => fire("error", m, o)
274
+ api.warning = (m, o) => fire("warning", m, o)
275
+ api.info = (m, o) => fire("info", m, o)
276
+ api.loading = (m, o = {}) => fire("loading", m, { ...o, duration: o.duration ?? 0 })
277
+ api.dismiss = (id) => {
278
+ if (id) this._dismissById(id)
279
+ else window.dispatchEvent(new CustomEvent("phlex-kit:toast:dismiss-all"))
280
+ }
281
+ api.promise = (p, msgs = {}) => {
282
+ const id = `toast-${this._uuid()}`
283
+ fire("loading", typeof msgs.loading === "function" ? msgs.loading() : (msgs.loading || "Loading..."), { id, duration: 0 })
284
+ Promise.resolve(p).then(
285
+ (val) => this._mutate(id, "success", typeof msgs.success === "function" ? msgs.success(val) : msgs.success),
286
+ (err) => this._mutate(id, "error", typeof msgs.error === "function" ? msgs.error(err) : msgs.error)
287
+ )
288
+ return id
289
+ }
290
+
291
+ window.PhlexKit = window.PhlexKit || {}
292
+ window.PhlexKit.toast = api
293
+ }
294
+
295
+ _mutate(id, variant, text) {
296
+ const el = this._listEl.querySelector(`#${CSS.escape(id)}`)
297
+ if (!el) return
298
+ el.dataset.variant = variant
299
+ el.setAttribute("role", variant === "error" ? "alert" : "status")
300
+ this._swapIcon(el, variant)
301
+ const t = el.querySelector('[data-slot="title"]')
302
+ if (t && text) t.textContent = text
303
+ const dur = String(this.durationValue)
304
+ el.setAttribute("data-phlex-kit--toast-duration-value", dur)
305
+ el.dispatchEvent(new CustomEvent("phlex-kit:toast:restart", { bubbles: true }))
306
+ }
307
+
308
+ _swapIcon(el, variant) {
309
+ const iconHost = el.querySelector('[data-slot="icon"]')
310
+ if (!iconHost) return
311
+ const tpl = this._skeletonFor(variant)
312
+ if (!tpl) return
313
+ const sourceIcon = tpl.content.firstElementChild?.querySelector('[data-slot="icon"]')
314
+ iconHost.innerHTML = sourceIcon ? sourceIcon.innerHTML : ""
315
+ }
316
+
317
+ _uuid() {
318
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID()
319
+ return Math.random().toString(36).slice(2) + Date.now().toString(36)
320
+ }
321
+ }
@@ -0,0 +1,25 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="phlex-kit--toggle"
4
+ export default class extends Controller {
5
+ static targets = ["button", "input"]
6
+ static values = { pressed: Boolean, value: String, unpressedValue: String }
7
+
8
+ toggle() {
9
+ if (this.buttonTarget.disabled) return
10
+ this.pressedValue = !this.pressedValue
11
+ }
12
+
13
+ pressedValueChanged(current, previous) {
14
+ if (this.hasButtonTarget) {
15
+ this.buttonTarget.setAttribute("aria-pressed", current ? "true" : "false")
16
+ this.buttonTarget.dataset.state = current ? "on" : "off"
17
+ }
18
+ if (this.hasInputTarget) {
19
+ this.inputTarget.value = current ? this.valueValue : this.unpressedValueValue
20
+ }
21
+ if (previous !== undefined) {
22
+ this.dispatch("change", { detail: { pressed: current }, bubbles: true })
23
+ }
24
+ }
25
+ }
@@ -0,0 +1,91 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="phlex-kit--toggle-group"
4
+ export default class extends Controller {
5
+ static targets = ["item", "input"]
6
+ static values = { type: String, name: String }
7
+
8
+ connect() { this.reconcile() }
9
+
10
+ select(event) {
11
+ const item = event.currentTarget
12
+ if (item.disabled) return
13
+ if (this.typeValue === "single") {
14
+ this.itemTargets.forEach((el) => this.setPressed(el, el === item))
15
+ } else {
16
+ this.setPressed(item, !this.isPressed(item))
17
+ }
18
+ this.rebuildInputs()
19
+ this.updateRovingTabindex(item)
20
+ }
21
+
22
+ navigate(event) {
23
+ if (this.typeValue !== "single") return
24
+ const items = this.enabledItems()
25
+ if (items.length === 0) return
26
+ const currentIndex = items.indexOf(event.currentTarget)
27
+ let nextIndex = currentIndex
28
+ switch (event.key) {
29
+ case "ArrowRight": case "ArrowDown": nextIndex = (currentIndex + 1) % items.length; break
30
+ case "ArrowLeft": case "ArrowUp": nextIndex = (currentIndex - 1 + items.length) % items.length; break
31
+ case "Home": nextIndex = 0; break
32
+ case "End": nextIndex = items.length - 1; break
33
+ case " ": case "Enter": event.preventDefault(); event.currentTarget.click(); return
34
+ default: return
35
+ }
36
+ event.preventDefault()
37
+ const next = items[nextIndex]
38
+ this.updateRovingTabindex(next)
39
+ next.focus()
40
+ }
41
+
42
+ reconcile() {
43
+ if (this.typeValue === "single") {
44
+ const pressed = this.itemTargets.find((el) => this.isPressed(el))
45
+ const first = pressed || this.enabledItems()[0]
46
+ this.itemTargets.forEach((el) => el.setAttribute("tabindex", el === first ? "0" : "-1"))
47
+ } else {
48
+ this.itemTargets.forEach((el) => el.setAttribute("tabindex", "0"))
49
+ }
50
+ this.rebuildInputs()
51
+ }
52
+
53
+ isPressed(item) { return item.dataset.state === "on" }
54
+
55
+ setPressed(item, pressed) {
56
+ item.dataset.state = pressed ? "on" : "off"
57
+ if (this.typeValue === "single") {
58
+ item.setAttribute("aria-checked", pressed ? "true" : "false")
59
+ } else {
60
+ item.setAttribute("aria-pressed", pressed ? "true" : "false")
61
+ }
62
+ }
63
+
64
+ updateRovingTabindex(focusedItem) {
65
+ if (this.typeValue !== "single") return
66
+ this.itemTargets.forEach((el) => el.setAttribute("tabindex", el === focusedItem ? "0" : "-1"))
67
+ }
68
+
69
+ enabledItems() { return this.itemTargets.filter((el) => !el.disabled) }
70
+
71
+ rebuildInputs() {
72
+ if (!this.nameValue) return
73
+ this.inputTargets.forEach((el) => el.remove())
74
+ const pressed = this.itemTargets.filter((el) => this.isPressed(el))
75
+ if (this.typeValue === "single") {
76
+ const val = pressed[0]?.dataset.value || ""
77
+ this.element.appendChild(this.buildInput(this.nameValue, val))
78
+ } else {
79
+ pressed.forEach((item) => this.element.appendChild(this.buildInput(`${this.nameValue}[]`, item.dataset.value)))
80
+ }
81
+ }
82
+
83
+ buildInput(name, value) {
84
+ const input = document.createElement("input")
85
+ input.type = "hidden"
86
+ input.name = name
87
+ input.value = value
88
+ input.setAttribute("data-phlex-kit--toggle-group-target", "input")
89
+ return input
90
+ }
91
+ }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pins the PhlexKit Stimulus controllers so importmap-rails hosts can register
4
+ # them. The engine appends this file to the host's importmap paths.
5
+ pin_all_from PhlexKit::Engine.root.join("app/javascript/phlex_kit/controllers"),
6
+ under: "phlex_kit/controllers",
7
+ to: "phlex_kit/controllers"
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module PhlexKit
6
+ module Generators
7
+ # `rails g phlex_kit:component button` — the shadcn-style "eject": copies a
8
+ # component's folder (.rb parts + co-located .css) out of the gem and into the
9
+ # host app so the team owns and can edit the source. Appends its @import to
10
+ # application.css. The ejected copy keeps the PhlexKit namespace; the host's
11
+ # copy shadows the gem's via Zeitwerk load-path order.
12
+ class ComponentGenerator < Rails::Generators::NamedBase
13
+ source_root PhlexKit::Engine.root.join("app/components/phlex_kit").to_s
14
+
15
+ def eject_component
16
+ unless File.directory?(File.join(self.class.source_root, file_name))
17
+ say_status :error, "no PhlexKit component named '#{file_name}'", :red
18
+ return
19
+ end
20
+ directory file_name, "app/components/phlex_kit/#{file_name}"
21
+ end
22
+
23
+ def wire_import
24
+ css = "app/assets/stylesheets/application.css"
25
+ line = %(@import url("phlex_kit/#{file_name}/#{file_name}.css");\n)
26
+ return unless File.exist?(css)
27
+ prepend_to_file(css, line) unless File.read(css).include?(line.strip)
28
+ end
29
+
30
+ def remind_collapse
31
+ say <<~MSG
32
+ Ejected phlex_kit/#{file_name}. Ensure your app collapses ejected
33
+ component folders (config/application.rb):
34
+
35
+ Rails.autoloaders.main.collapse("app/components/phlex_kit/*")
36
+
37
+ MSG
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module PhlexKit
6
+ module Generators
7
+ # `rails g phlex_kit:install` — wires PhlexKit into a host app:
8
+ # - drops an initializer,
9
+ # - adds the manifest @import to application.css (url() form),
10
+ # - prints the Stimulus registration snippet.
11
+ class InstallGenerator < Rails::Generators::Base
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ IMPORT_LINE = %(@import url("phlex_kit/phlex_kit.css");\n)
15
+
16
+ def create_initializer
17
+ template "phlex_kit.rb", "config/initializers/phlex_kit.rb"
18
+ end
19
+
20
+ def add_stylesheet_import
21
+ css = "app/assets/stylesheets/application.css"
22
+ unless File.exist?(css)
23
+ say_status :skip, "#{css} not found — add `#{IMPORT_LINE.strip}` to your main stylesheet", :yellow
24
+ return
25
+ end
26
+ if File.read(css).include?("phlex_kit/phlex_kit.css")
27
+ say_status :identical, css, :blue
28
+ else
29
+ # @import must precede every rule, so prepend it.
30
+ prepend_to_file css, IMPORT_LINE
31
+ end
32
+ end
33
+
34
+ def print_stimulus_instructions
35
+ say <<~MSG
36
+
37
+ PhlexKit installed. To enable the interactive components (Dialog,
38
+ Dropdown, Select, Avatar), register the Stimulus controllers:
39
+
40
+ import { registerPhlexKitControllers } from "phlex_kit/controllers"
41
+ registerPhlexKitControllers(application)
42
+
43
+ Render a component anywhere a Phlex view is allowed:
44
+
45
+ render PhlexKit::Button.new(variant: :primary) { "Save" }
46
+
47
+ MSG
48
+ end
49
+ end
50
+ end
51
+ end