poetry-ui 0.0.2

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 (442) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +6 -0
  3. data/CHANGELOG.md +3 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +87 -0
  6. data/THIRD_PARTY_NOTICES.md +201 -0
  7. data/app/components/poetry/ui/accordion/component.html.erb +3 -0
  8. data/app/components/poetry/ui/accordion/component.rb +197 -0
  9. data/app/components/poetry/ui/accordion/preview.rb +39 -0
  10. data/app/components/poetry/ui/accordion/style.rb +37 -0
  11. data/app/components/poetry/ui/alert/component.html.erb +6 -0
  12. data/app/components/poetry/ui/alert/component.rb +61 -0
  13. data/app/components/poetry/ui/alert/preview.rb +35 -0
  14. data/app/components/poetry/ui/alert/style.rb +24 -0
  15. data/app/components/poetry/ui/alert_dialog/component.html.erb +17 -0
  16. data/app/components/poetry/ui/alert_dialog/component.rb +208 -0
  17. data/app/components/poetry/ui/alert_dialog/preview.rb +48 -0
  18. data/app/components/poetry/ui/alert_dialog/style.rb +46 -0
  19. data/app/components/poetry/ui/aspect_ratio/component.rb +63 -0
  20. data/app/components/poetry/ui/aspect_ratio/preview.rb +29 -0
  21. data/app/components/poetry/ui/aspect_ratio/style.rb +13 -0
  22. data/app/components/poetry/ui/attachment/component.html.erb +13 -0
  23. data/app/components/poetry/ui/attachment/component.rb +139 -0
  24. data/app/components/poetry/ui/attachment/preview.rb +83 -0
  25. data/app/components/poetry/ui/attachment/style.rb +59 -0
  26. data/app/components/poetry/ui/autocomplete/component.html.erb +16 -0
  27. data/app/components/poetry/ui/autocomplete/component.rb +193 -0
  28. data/app/components/poetry/ui/autocomplete/preview.rb +39 -0
  29. data/app/components/poetry/ui/autocomplete/style.rb +18 -0
  30. data/app/components/poetry/ui/avatar/component.rb +103 -0
  31. data/app/components/poetry/ui/avatar/group_preview.html.erb +11 -0
  32. data/app/components/poetry/ui/avatar/preview.rb +50 -0
  33. data/app/components/poetry/ui/avatar/style.rb +34 -0
  34. data/app/components/poetry/ui/badge/component.rb +73 -0
  35. data/app/components/poetry/ui/badge/preview.rb +54 -0
  36. data/app/components/poetry/ui/badge/style.rb +35 -0
  37. data/app/components/poetry/ui/breadcrumb/component.html.erb +32 -0
  38. data/app/components/poetry/ui/breadcrumb/component.rb +102 -0
  39. data/app/components/poetry/ui/breadcrumb/dropdown_crumb_preview.html.erb +21 -0
  40. data/app/components/poetry/ui/breadcrumb/preview.rb +45 -0
  41. data/app/components/poetry/ui/breadcrumb/style.rb +25 -0
  42. data/app/components/poetry/ui/bubble/component.html.erb +4 -0
  43. data/app/components/poetry/ui/bubble/component.rb +106 -0
  44. data/app/components/poetry/ui/bubble/preview.rb +62 -0
  45. data/app/components/poetry/ui/bubble/style.rb +47 -0
  46. data/app/components/poetry/ui/button/component.html.erb +14 -0
  47. data/app/components/poetry/ui/button/component.rb +168 -0
  48. data/app/components/poetry/ui/button/preview.rb +94 -0
  49. data/app/components/poetry/ui/button/style.rb +41 -0
  50. data/app/components/poetry/ui/button_group/component.rb +70 -0
  51. data/app/components/poetry/ui/button_group/popup_triggers_preview.html.erb +21 -0
  52. data/app/components/poetry/ui/button_group/preview.rb +46 -0
  53. data/app/components/poetry/ui/button_group/style.rb +27 -0
  54. data/app/components/poetry/ui/button_group/text_prefix_preview.html.erb +7 -0
  55. data/app/components/poetry/ui/calendar/component.html.erb +72 -0
  56. data/app/components/poetry/ui/calendar/component.rb +366 -0
  57. data/app/components/poetry/ui/calendar/preview.rb +55 -0
  58. data/app/components/poetry/ui/calendar/style.rb +55 -0
  59. data/app/components/poetry/ui/card/component.html.erb +11 -0
  60. data/app/components/poetry/ui/card/component.rb +90 -0
  61. data/app/components/poetry/ui/card/preview.rb +29 -0
  62. data/app/components/poetry/ui/card/style.rb +23 -0
  63. data/app/components/poetry/ui/carousel/component.html.erb +17 -0
  64. data/app/components/poetry/ui/carousel/component.rb +179 -0
  65. data/app/components/poetry/ui/carousel/preview.rb +67 -0
  66. data/app/components/poetry/ui/carousel/style.rb +39 -0
  67. data/app/components/poetry/ui/checkbox/component.html.erb +22 -0
  68. data/app/components/poetry/ui/checkbox/component.rb +179 -0
  69. data/app/components/poetry/ui/checkbox/preview.rb +68 -0
  70. data/app/components/poetry/ui/checkbox/style.rb +28 -0
  71. data/app/components/poetry/ui/clipboard_text/component.html.erb +15 -0
  72. data/app/components/poetry/ui/clipboard_text/component.rb +154 -0
  73. data/app/components/poetry/ui/clipboard_text/preview.rb +24 -0
  74. data/app/components/poetry/ui/clipboard_text/style.rb +23 -0
  75. data/app/components/poetry/ui/code_block/component.html.erb +13 -0
  76. data/app/components/poetry/ui/code_block/component.rb +141 -0
  77. data/app/components/poetry/ui/code_block/preview.rb +40 -0
  78. data/app/components/poetry/ui/code_block/style.rb +38 -0
  79. data/app/components/poetry/ui/collapsible/component.html.erb +4 -0
  80. data/app/components/poetry/ui/collapsible/component.rb +134 -0
  81. data/app/components/poetry/ui/collapsible/preview.rb +24 -0
  82. data/app/components/poetry/ui/combobox/component.html.erb +49 -0
  83. data/app/components/poetry/ui/combobox/component.rb +1086 -0
  84. data/app/components/poetry/ui/combobox/preview.rb +164 -0
  85. data/app/components/poetry/ui/combobox/style.rb +148 -0
  86. data/app/components/poetry/ui/command/component.html.erb +20 -0
  87. data/app/components/poetry/ui/command/component.rb +526 -0
  88. data/app/components/poetry/ui/command/dialog_component.html.erb +14 -0
  89. data/app/components/poetry/ui/command/dialog_component.rb +206 -0
  90. data/app/components/poetry/ui/command/dialog_preview.rb +39 -0
  91. data/app/components/poetry/ui/command/preview.rb +121 -0
  92. data/app/components/poetry/ui/command/style.rb +78 -0
  93. data/app/components/poetry/ui/composable_trigger.rb +63 -0
  94. data/app/components/poetry/ui/context_menu/component.html.erb +6 -0
  95. data/app/components/poetry/ui/context_menu/component.rb +358 -0
  96. data/app/components/poetry/ui/context_menu/preview.rb +111 -0
  97. data/app/components/poetry/ui/context_menu/style.rb +59 -0
  98. data/app/components/poetry/ui/data_table/component.html.erb +78 -0
  99. data/app/components/poetry/ui/data_table/component.rb +277 -0
  100. data/app/components/poetry/ui/data_table/preview.rb +68 -0
  101. data/app/components/poetry/ui/data_table/state.rb +115 -0
  102. data/app/components/poetry/ui/data_table/style.rb +42 -0
  103. data/app/components/poetry/ui/date_field/component.html.erb +7 -0
  104. data/app/components/poetry/ui/date_field/component.rb +209 -0
  105. data/app/components/poetry/ui/date_field/preview.rb +42 -0
  106. data/app/components/poetry/ui/date_field/style.rb +35 -0
  107. data/app/components/poetry/ui/date_picker/component.html.erb +23 -0
  108. data/app/components/poetry/ui/date_picker/component.rb +242 -0
  109. data/app/components/poetry/ui/date_picker/preview.rb +31 -0
  110. data/app/components/poetry/ui/date_picker/style.rb +25 -0
  111. data/app/components/poetry/ui/deferred/component.html.erb +16 -0
  112. data/app/components/poetry/ui/deferred/component.rb +90 -0
  113. data/app/components/poetry/ui/deferred/preview.rb +24 -0
  114. data/app/components/poetry/ui/dialog/component.html.erb +21 -0
  115. data/app/components/poetry/ui/dialog/component.rb +194 -0
  116. data/app/components/poetry/ui/dialog/preview.rb +33 -0
  117. data/app/components/poetry/ui/dialog/style.rb +30 -0
  118. data/app/components/poetry/ui/drawer/component.html.erb +16 -0
  119. data/app/components/poetry/ui/drawer/component.rb +207 -0
  120. data/app/components/poetry/ui/drawer/preview.rb +69 -0
  121. data/app/components/poetry/ui/drawer/style.rb +105 -0
  122. data/app/components/poetry/ui/dropdown_menu/component.html.erb +6 -0
  123. data/app/components/poetry/ui/dropdown_menu/component.rb +350 -0
  124. data/app/components/poetry/ui/dropdown_menu/preview.rb +101 -0
  125. data/app/components/poetry/ui/dropdown_menu/style.rb +60 -0
  126. data/app/components/poetry/ui/empty/component.html.erb +10 -0
  127. data/app/components/poetry/ui/empty/component.rb +79 -0
  128. data/app/components/poetry/ui/empty/preview.rb +27 -0
  129. data/app/components/poetry/ui/empty/style.rb +27 -0
  130. data/app/components/poetry/ui/family_identity.rb +52 -0
  131. data/app/components/poetry/ui/field/component.html.erb +19 -0
  132. data/app/components/poetry/ui/field/component.rb +183 -0
  133. data/app/components/poetry/ui/field/preview.html.erb +30 -0
  134. data/app/components/poetry/ui/field/preview.rb +72 -0
  135. data/app/components/poetry/ui/field/style.rb +68 -0
  136. data/app/components/poetry/ui/field_group/component.rb +61 -0
  137. data/app/components/poetry/ui/field_group/preview.html.erb +30 -0
  138. data/app/components/poetry/ui/field_group/preview.rb +20 -0
  139. data/app/components/poetry/ui/field_group/style.rb +25 -0
  140. data/app/components/poetry/ui/field_separator/component.rb +70 -0
  141. data/app/components/poetry/ui/field_separator/preview.rb +18 -0
  142. data/app/components/poetry/ui/field_separator/style.rb +20 -0
  143. data/app/components/poetry/ui/fieldset/component.rb +90 -0
  144. data/app/components/poetry/ui/fieldset/preview.html.erb +22 -0
  145. data/app/components/poetry/ui/fieldset/preview.rb +25 -0
  146. data/app/components/poetry/ui/fieldset/style.rb +18 -0
  147. data/app/components/poetry/ui/file_input/component.html.erb +14 -0
  148. data/app/components/poetry/ui/file_input/component.rb +188 -0
  149. data/app/components/poetry/ui/file_input/preview.rb +30 -0
  150. data/app/components/poetry/ui/file_input/style.rb +35 -0
  151. data/app/components/poetry/ui/hover_card/component.html.erb +6 -0
  152. data/app/components/poetry/ui/hover_card/component.rb +201 -0
  153. data/app/components/poetry/ui/hover_card/preview.rb +60 -0
  154. data/app/components/poetry/ui/hover_card/style.rb +17 -0
  155. data/app/components/poetry/ui/icon/component.rb +127 -0
  156. data/app/components/poetry/ui/icon/preview.rb +21 -0
  157. data/app/components/poetry/ui/input/component.rb +80 -0
  158. data/app/components/poetry/ui/input/preview.rb +35 -0
  159. data/app/components/poetry/ui/input/style.rb +18 -0
  160. data/app/components/poetry/ui/input_group/button_preview.html.erb +12 -0
  161. data/app/components/poetry/ui/input_group/component.rb +69 -0
  162. data/app/components/poetry/ui/input_group/preview.rb +23 -0
  163. data/app/components/poetry/ui/input_group/search_preview.html.erb +13 -0
  164. data/app/components/poetry/ui/input_group/style.rb +40 -0
  165. data/app/components/poetry/ui/input_group/textarea_preview.html.erb +10 -0
  166. data/app/components/poetry/ui/input_group_field.rb +61 -0
  167. data/app/components/poetry/ui/input_otp/component.html.erb +25 -0
  168. data/app/components/poetry/ui/input_otp/component.rb +250 -0
  169. data/app/components/poetry/ui/input_otp/preview.rb +91 -0
  170. data/app/components/poetry/ui/input_otp/style.rb +36 -0
  171. data/app/components/poetry/ui/item/component.html.erb +13 -0
  172. data/app/components/poetry/ui/item/component.rb +99 -0
  173. data/app/components/poetry/ui/item/group_preview.html.erb +15 -0
  174. data/app/components/poetry/ui/item/preview.rb +52 -0
  175. data/app/components/poetry/ui/item/style.rb +45 -0
  176. data/app/components/poetry/ui/kbd/component.rb +47 -0
  177. data/app/components/poetry/ui/kbd/group_preview.html.erb +4 -0
  178. data/app/components/poetry/ui/kbd/preview.rb +23 -0
  179. data/app/components/poetry/ui/kbd/style.rb +13 -0
  180. data/app/components/poetry/ui/label/component.rb +46 -0
  181. data/app/components/poetry/ui/label/preview.html.erb +5 -0
  182. data/app/components/poetry/ui/label/preview.rb +16 -0
  183. data/app/components/poetry/ui/label/style.rb +13 -0
  184. data/app/components/poetry/ui/link/component.rb +62 -0
  185. data/app/components/poetry/ui/link/preview.rb +31 -0
  186. data/app/components/poetry/ui/link/style.rb +20 -0
  187. data/app/components/poetry/ui/marker/component.html.erb +4 -0
  188. data/app/components/poetry/ui/marker/component.rb +81 -0
  189. data/app/components/poetry/ui/marker/preview.rb +43 -0
  190. data/app/components/poetry/ui/marker/style.rb +24 -0
  191. data/app/components/poetry/ui/menubar/component.html.erb +3 -0
  192. data/app/components/poetry/ui/menubar/component.rb +493 -0
  193. data/app/components/poetry/ui/menubar/preview.rb +164 -0
  194. data/app/components/poetry/ui/menubar/style.rb +71 -0
  195. data/app/components/poetry/ui/menus.rb +386 -0
  196. data/app/components/poetry/ui/message/component.html.erb +8 -0
  197. data/app/components/poetry/ui/message/component.rb +64 -0
  198. data/app/components/poetry/ui/message/preview.rb +33 -0
  199. data/app/components/poetry/ui/message/style.rb +29 -0
  200. data/app/components/poetry/ui/message_scroller/component.html.erb +15 -0
  201. data/app/components/poetry/ui/message_scroller/component.rb +163 -0
  202. data/app/components/poetry/ui/message_scroller/preview.rb +33 -0
  203. data/app/components/poetry/ui/message_scroller/style.rb +39 -0
  204. data/app/components/poetry/ui/metadata_list/component.rb +88 -0
  205. data/app/components/poetry/ui/metadata_list/preview.rb +55 -0
  206. data/app/components/poetry/ui/metadata_list/style.rb +35 -0
  207. data/app/components/poetry/ui/meter/component.rb +117 -0
  208. data/app/components/poetry/ui/meter/preview.rb +28 -0
  209. data/app/components/poetry/ui/meter/style.rb +16 -0
  210. data/app/components/poetry/ui/native_select/component.rb +124 -0
  211. data/app/components/poetry/ui/native_select/labelled_preview.html.erb +7 -0
  212. data/app/components/poetry/ui/native_select/preview.rb +20 -0
  213. data/app/components/poetry/ui/native_select/style.rb +22 -0
  214. data/app/components/poetry/ui/navigation_menu/bar_preview.html.erb +14 -0
  215. data/app/components/poetry/ui/navigation_menu/component.html.erb +25 -0
  216. data/app/components/poetry/ui/navigation_menu/component.rb +254 -0
  217. data/app/components/poetry/ui/navigation_menu/preview.rb +20 -0
  218. data/app/components/poetry/ui/navigation_menu/style.rb +73 -0
  219. data/app/components/poetry/ui/navigation_menu/viewport_preview.html.erb +23 -0
  220. data/app/components/poetry/ui/number_field/component.html.erb +19 -0
  221. data/app/components/poetry/ui/number_field/component.rb +263 -0
  222. data/app/components/poetry/ui/number_field/preview.rb +66 -0
  223. data/app/components/poetry/ui/number_field/style.rb +14 -0
  224. data/app/components/poetry/ui/pagination/component.html.erb +40 -0
  225. data/app/components/poetry/ui/pagination/component.rb +182 -0
  226. data/app/components/poetry/ui/pagination/preview.rb +47 -0
  227. data/app/components/poetry/ui/pagination/style.rb +27 -0
  228. data/app/components/poetry/ui/popover/component.html.erb +17 -0
  229. data/app/components/poetry/ui/popover/component.rb +203 -0
  230. data/app/components/poetry/ui/popover/preview.html.erb +29 -0
  231. data/app/components/poetry/ui/popover/preview.rb +66 -0
  232. data/app/components/poetry/ui/popover/style.rb +26 -0
  233. data/app/components/poetry/ui/popper_consumer.rb +69 -0
  234. data/app/components/poetry/ui/progress/component.rb +95 -0
  235. data/app/components/poetry/ui/progress/preview.rb +22 -0
  236. data/app/components/poetry/ui/progress/style.rb +19 -0
  237. data/app/components/poetry/ui/questionnaire/component.html.erb +80 -0
  238. data/app/components/poetry/ui/questionnaire/component.rb +396 -0
  239. data/app/components/poetry/ui/questionnaire/preview.rb +99 -0
  240. data/app/components/poetry/ui/questionnaire/style.rb +53 -0
  241. data/app/components/poetry/ui/radio_group/component.html.erb +4 -0
  242. data/app/components/poetry/ui/radio_group/component.rb +323 -0
  243. data/app/components/poetry/ui/radio_group/preview.rb +109 -0
  244. data/app/components/poetry/ui/radio_group/style.rb +53 -0
  245. data/app/components/poetry/ui/resizable/component.html.erb +10 -0
  246. data/app/components/poetry/ui/resizable/component.rb +178 -0
  247. data/app/components/poetry/ui/resizable/preview.rb +42 -0
  248. data/app/components/poetry/ui/resizable/style.rb +30 -0
  249. data/app/components/poetry/ui/scroll_area/component.rb +76 -0
  250. data/app/components/poetry/ui/scroll_area/preview.rb +35 -0
  251. data/app/components/poetry/ui/scroll_area/style.rb +19 -0
  252. data/app/components/poetry/ui/search_field/component.html.erb +15 -0
  253. data/app/components/poetry/ui/search_field/component.rb +137 -0
  254. data/app/components/poetry/ui/search_field/preview.rb +23 -0
  255. data/app/components/poetry/ui/search_field/style.rb +18 -0
  256. data/app/components/poetry/ui/select/component.html.erb +15 -0
  257. data/app/components/poetry/ui/select/component.rb +639 -0
  258. data/app/components/poetry/ui/select/preview.rb +113 -0
  259. data/app/components/poetry/ui/select/style.rb +58 -0
  260. data/app/components/poetry/ui/sensitive_input/component.html.erb +29 -0
  261. data/app/components/poetry/ui/sensitive_input/component.rb +274 -0
  262. data/app/components/poetry/ui/sensitive_input/preview.rb +40 -0
  263. data/app/components/poetry/ui/sensitive_input/style.rb +49 -0
  264. data/app/components/poetry/ui/separator/component.rb +57 -0
  265. data/app/components/poetry/ui/separator/preview.rb +19 -0
  266. data/app/components/poetry/ui/separator/style.rb +15 -0
  267. data/app/components/poetry/ui/sheet/component.html.erb +21 -0
  268. data/app/components/poetry/ui/sheet/component.rb +97 -0
  269. data/app/components/poetry/ui/sheet/preview.rb +57 -0
  270. data/app/components/poetry/ui/sheet/style.rb +47 -0
  271. data/app/components/poetry/ui/sidebar/component.html.erb +20 -0
  272. data/app/components/poetry/ui/sidebar/component.rb +249 -0
  273. data/app/components/poetry/ui/sidebar/preview.rb +21 -0
  274. data/app/components/poetry/ui/sidebar/shell_preview.html.erb +47 -0
  275. data/app/components/poetry/ui/sidebar/style.rb +139 -0
  276. data/app/components/poetry/ui/skeleton/component.rb +39 -0
  277. data/app/components/poetry/ui/skeleton/preview.rb +22 -0
  278. data/app/components/poetry/ui/skeleton/style.rb +13 -0
  279. data/app/components/poetry/ui/slider/component.html.erb +13 -0
  280. data/app/components/poetry/ui/slider/component.rb +360 -0
  281. data/app/components/poetry/ui/slider/preview.rb +73 -0
  282. data/app/components/poetry/ui/slider/style.rb +55 -0
  283. data/app/components/poetry/ui/spinner/component.rb +58 -0
  284. data/app/components/poetry/ui/spinner/preview.rb +22 -0
  285. data/app/components/poetry/ui/spinner/style.rb +12 -0
  286. data/app/components/poetry/ui/stat/component.html.erb +13 -0
  287. data/app/components/poetry/ui/stat/component.rb +102 -0
  288. data/app/components/poetry/ui/stat/preview.rb +47 -0
  289. data/app/components/poetry/ui/stat/style.rb +29 -0
  290. data/app/components/poetry/ui/switch/component.html.erb +15 -0
  291. data/app/components/poetry/ui/switch/component.rb +136 -0
  292. data/app/components/poetry/ui/switch/preview.rb +66 -0
  293. data/app/components/poetry/ui/switch/style.rb +22 -0
  294. data/app/components/poetry/ui/table/component.html.erb +3 -0
  295. data/app/components/poetry/ui/table/component.rb +93 -0
  296. data/app/components/poetry/ui/table/preview.rb +96 -0
  297. data/app/components/poetry/ui/table/style.rb +26 -0
  298. data/app/components/poetry/ui/tabs/component.html.erb +10 -0
  299. data/app/components/poetry/ui/tabs/component.rb +253 -0
  300. data/app/components/poetry/ui/tabs/preview.rb +43 -0
  301. data/app/components/poetry/ui/tabs/style.rb +49 -0
  302. data/app/components/poetry/ui/tag_group/component.html.erb +8 -0
  303. data/app/components/poetry/ui/tag_group/component.rb +203 -0
  304. data/app/components/poetry/ui/tag_group/preview.rb +43 -0
  305. data/app/components/poetry/ui/tag_group/style.rb +29 -0
  306. data/app/components/poetry/ui/textarea/component.rb +64 -0
  307. data/app/components/poetry/ui/textarea/preview.rb +75 -0
  308. data/app/components/poetry/ui/textarea/style.rb +18 -0
  309. data/app/components/poetry/ui/time_field/component.html.erb +6 -0
  310. data/app/components/poetry/ui/time_field/component.rb +85 -0
  311. data/app/components/poetry/ui/time_field/preview.rb +40 -0
  312. data/app/components/poetry/ui/time_field/style.rb +30 -0
  313. data/app/components/poetry/ui/timeline/component.rb +134 -0
  314. data/app/components/poetry/ui/timeline/preview.rb +61 -0
  315. data/app/components/poetry/ui/timeline/style.rb +54 -0
  316. data/app/components/poetry/ui/toast/component.html.erb +24 -0
  317. data/app/components/poetry/ui/toast/component.rb +178 -0
  318. data/app/components/poetry/ui/toast/preview.rb +69 -0
  319. data/app/components/poetry/ui/toast/style.rb +33 -0
  320. data/app/components/poetry/ui/toast_trigger/addressed_preview.html.erb +12 -0
  321. data/app/components/poetry/ui/toast_trigger/component.html.erb +3 -0
  322. data/app/components/poetry/ui/toast_trigger/component.rb +82 -0
  323. data/app/components/poetry/ui/toast_trigger/default_preview.html.erb +11 -0
  324. data/app/components/poetry/ui/toast_trigger/preview.rb +19 -0
  325. data/app/components/poetry/ui/toaster/component.html.erb +1 -0
  326. data/app/components/poetry/ui/toaster/component.rb +99 -0
  327. data/app/components/poetry/ui/toaster/positions_preview.html.erb +24 -0
  328. data/app/components/poetry/ui/toaster/preview.rb +46 -0
  329. data/app/components/poetry/ui/toaster/stamp_preview.html.erb +15 -0
  330. data/app/components/poetry/ui/toaster/style.rb +26 -0
  331. data/app/components/poetry/ui/toggle/component.rb +120 -0
  332. data/app/components/poetry/ui/toggle/preview.rb +79 -0
  333. data/app/components/poetry/ui/toggle/style.rb +33 -0
  334. data/app/components/poetry/ui/toggle_group/component.html.erb +4 -0
  335. data/app/components/poetry/ui/toggle_group/component.rb +252 -0
  336. data/app/components/poetry/ui/toggle_group/preview.rb +143 -0
  337. data/app/components/poetry/ui/toggle_group/style.rb +33 -0
  338. data/app/components/poetry/ui/toolbar/component.rb +125 -0
  339. data/app/components/poetry/ui/toolbar/preview.rb +41 -0
  340. data/app/components/poetry/ui/toolbar/style.rb +20 -0
  341. data/app/components/poetry/ui/tooltip/component.html.erb +20 -0
  342. data/app/components/poetry/ui/tooltip/component.rb +185 -0
  343. data/app/components/poetry/ui/tooltip/preview.rb +74 -0
  344. data/app/components/poetry/ui/tooltip/previews/provider_row.html.erb +25 -0
  345. data/app/components/poetry/ui/tooltip/style.rb +24 -0
  346. data/app/components/poetry/ui/tree/component.html.erb +32 -0
  347. data/app/components/poetry/ui/tree/component.rb +233 -0
  348. data/app/components/poetry/ui/tree/preview.rb +55 -0
  349. data/app/components/poetry/ui/tree/style.rb +29 -0
  350. data/app/components/poetry/ui/typeset/component.rb +73 -0
  351. data/app/components/poetry/ui/typeset/preview.rb +45 -0
  352. data/app/components/poetry/ui/typeset/style.rb +15 -0
  353. data/app/controllers/poetry/ui/llms_controller.rb +28 -0
  354. data/app/helpers/poetry/ui/components_helper.rb +1794 -0
  355. data/app/helpers/poetry/ui/form_builder/type_inference.rb +117 -0
  356. data/app/helpers/poetry/ui/form_builder.rb +916 -0
  357. data/app/helpers/poetry/ui/optimistic_form_builder.rb +55 -0
  358. data/app/helpers/poetry/ui/toast_stream_actions.rb +27 -0
  359. data/config/component_descriptions.yml +96 -0
  360. data/config/component_registry.yml +12257 -0
  361. data/config/locales/en.yml +93 -0
  362. data/config/routes.rb +8 -0
  363. data/config/template_classes.txt +113 -0
  364. data/lib/generators/poetry/add/add_generator.rb +218 -0
  365. data/lib/generators/poetry/agent_rules/agent_rules_generator.rb +91 -0
  366. data/lib/generators/poetry/agents/agents_generator.rb +24 -0
  367. data/lib/generators/poetry/agents_section.rb +148 -0
  368. data/lib/generators/poetry/block/block_generator.rb +67 -0
  369. data/lib/generators/poetry/block/templates/action_bar.html.erb +45 -0
  370. data/lib/generators/poetry/block/templates/app_shell.html.erb +139 -0
  371. data/lib/generators/poetry/block/templates/data_index.html.erb +85 -0
  372. data/lib/generators/poetry/block/templates/destructive_panel.html.erb +26 -0
  373. data/lib/generators/poetry/block/templates/page_header.html.erb +25 -0
  374. data/lib/generators/poetry/block/templates/section_card.html.erb +42 -0
  375. data/lib/generators/poetry/block/templates/stepper.html.erb +79 -0
  376. data/lib/generators/poetry/block/templates/top_nav.html.erb +44 -0
  377. data/lib/generators/poetry/diff/diff_generator.rb +135 -0
  378. data/lib/generators/poetry/editor/editor_generator.rb +344 -0
  379. data/lib/generators/poetry/editor/templates/poetry-check-hook +81 -0
  380. data/lib/generators/poetry/install/install_generator.rb +364 -0
  381. data/lib/generators/poetry/pagination/pagination_generator.rb +76 -0
  382. data/lib/generators/poetry/pagination/templates/kaminari_paginator.html.erb +29 -0
  383. data/lib/generators/poetry/pagination/templates/poetry_link_renderer.rb +34 -0
  384. data/lib/generators/poetry/pagination/templates/poetry_pagy_helper.rb +28 -0
  385. data/lib/generators/poetry/recipes/agent_embed/app/javascript/controllers/agent_demo_controller.js +131 -0
  386. data/lib/generators/poetry/recipes/screen_data_index/app/controllers/orders_controller.rb +15 -0
  387. data/lib/generators/poetry/recipes/screen_data_index/app/views/orders/index.html.erb +7 -0
  388. data/lib/generators/poetry/recipes/screen_data_index/test/system/orders_index_test.rb +16 -0
  389. data/lib/generators/poetry/recipes/screen_settings/app/controllers/settings_controller.rb +14 -0
  390. data/lib/generators/poetry/recipes/screen_settings/app/views/settings/show.html.erb +9 -0
  391. data/lib/generators/poetry/recipes/screen_settings/test/system/settings_show_test.rb +16 -0
  392. data/lib/generators/poetry/scaffold_templates/scaffold_templates_generator.rb +60 -0
  393. data/lib/generators/poetry/scaffold_templates/templates/_form.html.erb.tt +77 -0
  394. data/lib/generators/poetry/scaffold_templates/templates/controller.rb.tt +78 -0
  395. data/lib/generators/poetry/scaffold_templates/templates/edit.html.erb.tt +14 -0
  396. data/lib/generators/poetry/scaffold_templates/templates/index.html.erb.tt +24 -0
  397. data/lib/generators/poetry/scaffold_templates/templates/new.html.erb.tt +11 -0
  398. data/lib/generators/poetry/scaffold_templates/templates/partial.html.erb.tt +13 -0
  399. data/lib/generators/poetry/scaffold_templates/templates/show.html.erb.tt +18 -0
  400. data/lib/generators/poetry/skill/skill_generator.rb +34 -0
  401. data/lib/generators/poetry/skill/templates/poetry-component/SKILL.md +58 -0
  402. data/lib/generators/poetry/skill/templates/poetry-component/references/anatomy.md +152 -0
  403. data/lib/generators/poetry/skill/templates/poetry-component/references/checklist.md +67 -0
  404. data/lib/generators/poetry/skill/templates/poetry-component/references/documentation.md +137 -0
  405. data/lib/generators/poetry/skill/templates/poetry-design/SKILL.md +56 -0
  406. data/lib/generators/poetry/skill/templates/poetry-design/references/audit.md +123 -0
  407. data/lib/generators/poetry/skill/templates/poetry-design/references/compose.md +73 -0
  408. data/lib/generators/poetry/skill/templates/poetry-design/references/figma.md +68 -0
  409. data/lib/generators/poetry/skill/templates/poetry-design/references/paper.md +56 -0
  410. data/lib/generators/poetry/skill/templates/poetry-design/references/study.md +47 -0
  411. data/lib/generators/poetry/skill/templates/poetry-design/references/theme.md +70 -0
  412. data/lib/generators/poetry/skills_section.rb +58 -0
  413. data/lib/poetry/ui/chat.rb +283 -0
  414. data/lib/poetry/ui/code_block_highlighter.rb +60 -0
  415. data/lib/poetry/ui/engine.rb +55 -0
  416. data/lib/poetry/ui/recipes.rb +157 -0
  417. data/lib/poetry/ui/testing/combobox.rb +167 -0
  418. data/lib/poetry/ui/testing/dialog.rb +73 -0
  419. data/lib/poetry/ui/testing/menu.rb +127 -0
  420. data/lib/poetry/ui/testing/registration.rb +58 -0
  421. data/lib/poetry/ui/testing/select.rb +147 -0
  422. data/lib/poetry/ui/testing/tester.rb +130 -0
  423. data/lib/poetry/ui/testing.rb +130 -0
  424. data/lib/poetry/ui/themes.rb +109 -0
  425. data/lib/poetry/ui/version.rb +8 -0
  426. data/lib/poetry/ui/webmcp.rb +91 -0
  427. data/lib/poetry/ui.rb +285 -0
  428. data/lib/poetry-ui.rb +5 -0
  429. data/lib/tasks/poetry/check.rake +73 -0
  430. data/lib/tasks/poetry/design.rake +293 -0
  431. data/lib/tasks/poetry/verify.rake +36 -0
  432. data/themes/default.css +1793 -0
  433. data/themes/luma.css +1764 -0
  434. data/themes/lyra.css +1768 -0
  435. data/themes/maia.css +1768 -0
  436. data/themes/mira.css +1760 -0
  437. data/themes/nova.css +1755 -0
  438. data/themes/rhea.css +1771 -0
  439. data/themes/sera.css +1757 -0
  440. data/themes/vega.css +1775 -0
  441. data/typeset/typeset.css +499 -0
  442. metadata +502 -0
@@ -0,0 +1,79 @@
1
+ <%# poetry:block title="Stepper" description="A multi-step flow header: numbered step indicators with connectors and completed/current/upcoming states over the current step's panel with back/continue actions." keywords="stepper, wizard, steps, checkout, onboarding, progress, multi-step" %>
2
+ <%# Stepper is a BLOCK, not a component: neither shadcn nor Base UI
3
+ ships one, so the pattern lives here as copy-in source - adapt the
4
+ steps freely; nothing hides behind an API. The server owns step state:
5
+ pass step: as a local, or let it ride ?step= - Back/Continue are plain
6
+ links that re-render, and completed steps are links back. In a real
7
+ flow, swap the query param for your wizard's own routes/model state
8
+ and the final action for a form submit. %>
9
+ <% steps = ["Shipping", "Payment", "Review"] %>
10
+ <% step = (local_assigns[:step] || params[:step] || 2).to_i.clamp(1, steps.length) %>
11
+ <% panels = {
12
+ 1 => ["Shipping", "Step 1 of 3 - where this order lands.",
13
+ "Address fields mount here; this panel is the step's content slot."],
14
+ 2 => ["Payment", "Step 2 of 3 - how this order gets paid.",
15
+ "Card details are collected by the payment element that mounts here; this panel is the step's content slot."],
16
+ 3 => ["Review", "Step 3 of 3 - confirm and place the order.",
17
+ "The order summary renders here - line items, shipping choice, and the charge that Place order will submit."]
18
+ } %>
19
+ <% title, description, body = panels.fetch(step) %>
20
+ <div class="mx-auto max-w-2xl p-6">
21
+ <nav aria-label="Progress">
22
+ <ol class="flex items-center gap-3">
23
+ <% steps.each_with_index do |label, index| %>
24
+ <% number = index + 1 %>
25
+ <% unless number == 1 %>
26
+ <li aria-hidden="true" class="h-px min-w-8 flex-1 <%= number <= step ? "bg-primary" : "bg-border" %>"></li>
27
+ <% end %>
28
+ <% if number < step %>
29
+ <%# Completed: filled indicator with a check, revisitable. %>
30
+ <li class="flex items-center">
31
+ <a href="?step=<%= number %>" class="flex items-center gap-3 rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring">
32
+ <span class="flex size-8 items-center justify-center rounded-full bg-primary text-primary-foreground">
33
+ <%= poetry_icon(name: :check, class: "size-4", label: "Completed") %>
34
+ </span>
35
+ <span class="text-sm font-medium"><%= label %></span>
36
+ </a>
37
+ </li>
38
+ <% elsif number == step %>
39
+ <%# Current: outlined in primary, named by aria-current. %>
40
+ <li aria-current="step" class="flex items-center gap-3">
41
+ <span class="flex size-8 items-center justify-center rounded-full border-2 border-primary text-sm font-semibold text-primary"><%= number %></span>
42
+ <span class="text-sm font-medium"><%= label %></span>
43
+ </li>
44
+ <% else %>
45
+ <%# Upcoming: muted and inert until reached. %>
46
+ <li class="flex items-center gap-3">
47
+ <span class="flex size-8 items-center justify-center rounded-full border border-border text-sm text-muted-foreground"><%= number %></span>
48
+ <span class="text-sm text-muted-foreground"><%= label %></span>
49
+ </li>
50
+ <% end %>
51
+ <% end %>
52
+ </ol>
53
+ </nav>
54
+
55
+ <div class="mt-6">
56
+ <%= poetry_card do |card| %>
57
+ <% card.with_title { title } %>
58
+ <% card.with_description { description } %>
59
+ <div class="space-y-4 text-sm">
60
+ <p><%= body %></p>
61
+ </div>
62
+ <% card.with_footer do %>
63
+ <div class="flex w-full items-center justify-between">
64
+ <% if step > 1 %>
65
+ <%= poetry_button(tag: :a, href: "?step=#{step - 1}", variant: :outline) { "Back" } %>
66
+ <% else %>
67
+ <span></span>
68
+ <% end %>
69
+ <% if step < steps.length %>
70
+ <%= poetry_button(tag: :a, href: "?step=#{step + 1}") { "Continue to #{steps[step].downcase}" } %>
71
+ <% else %>
72
+ <%# The terminal action: wire this to your submit. %>
73
+ <%= poetry_button { "Place order" } %>
74
+ <% end %>
75
+ </div>
76
+ <% end %>
77
+ <% end %>
78
+ </div>
79
+ </div>
@@ -0,0 +1,44 @@
1
+ <%# poetry:block title="Top nav" description="A site navigation bar: brand mark, a products dropdown of rich title-and-description links, direct destinations, and the log-in / sign-up action pair." keywords="navigation, navbar, topbar, site, marketing, links" %>
2
+ <header class="w-full border-b">
3
+ <div class="mx-auto flex h-14 max-w-5xl items-center justify-between gap-6 px-6">
4
+ <%= poetry_link(href: "/", underline: :none, class: "inline-flex items-center gap-2 text-sm font-semibold") do %>
5
+ <%= poetry_icon(name: :"audio-waveform", class: "size-4") %> Meridian
6
+ <% end %>
7
+ <%= poetry_navigation_menu(label: "Main", viewport: true) do |nav| %>
8
+ <% nav.with_item("Products", value: "products") do %>
9
+ <div class="grid w-96 grid-cols-2 gap-1 p-2">
10
+ <%= poetry_navigation_menu_link(href: "/products/orders") do %>
11
+ <div class="space-y-0.5">
12
+ <div class="text-sm font-medium">Orders</div>
13
+ <p class="text-xs text-muted-foreground">Fulfillment from checkout to doorstep.</p>
14
+ </div>
15
+ <% end %>
16
+ <%= poetry_navigation_menu_link(href: "/products/customers") do %>
17
+ <div class="space-y-0.5">
18
+ <div class="text-sm font-medium">Customers</div>
19
+ <p class="text-xs text-muted-foreground">Profiles, segments, and lifetime history.</p>
20
+ </div>
21
+ <% end %>
22
+ <%= poetry_navigation_menu_link(href: "/products/analytics") do %>
23
+ <div class="space-y-0.5">
24
+ <div class="text-sm font-medium">Analytics</div>
25
+ <p class="text-xs text-muted-foreground">Live dashboards over every workspace event.</p>
26
+ </div>
27
+ <% end %>
28
+ <%= poetry_navigation_menu_link(href: "/products/automations") do %>
29
+ <div class="space-y-0.5">
30
+ <div class="text-sm font-medium">Automations</div>
31
+ <p class="text-xs text-muted-foreground">Workflows that run while the team sleeps.</p>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ <% end %>
36
+ <% nav.with_link("Pricing", href: "/pricing") %>
37
+ <% nav.with_link("Docs", href: "/docs") %>
38
+ <% end %>
39
+ <div class="flex items-center gap-2">
40
+ <%= poetry_button(variant: :ghost, size: :sm, tag: :a, href: "/login") { "Log in" } %>
41
+ <%= poetry_button(size: :sm, tag: :a, href: "/signup") { "Sign up" } %>
42
+ </div>
43
+ </div>
44
+ </header>
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "yaml"
5
+
6
+ module Poetry
7
+ # `rails g poetry:diff` - the copy-in upgrade report. poetry's
8
+ # three ownership tiers upgrade differently: gem-owned code rides
9
+ # `bundle update`, the vendored css set rides a `poetry:install` re-run,
10
+ # and copy-ins are app-OWNED - nothing may rewrite them. This generator
11
+ # closes the loop on that third tier: it reads the provenance
12
+ # manifest (config/poetry_components.yml) and reports, file by file, where
13
+ # the app's copies stand against what the installed gems ship NOW.
14
+ # Read-only by contract - it prints, it never writes; `poetry:add`
15
+ # re-run is the (skip-if-exists) way to pick up newly-shipped files.
16
+ #
17
+ # @example
18
+ # bin/rails g poetry:diff
19
+ class DiffGenerator < Rails::Generators::Base
20
+ # The copy-in provenance manifest poetry:add records into.
21
+ MANIFEST = "config/poetry_components.yml"
22
+ # Where component copy-ins land in the host app.
23
+ COMPONENT_ROOT = "app/components/poetry/ui"
24
+ # Where block copy-ins land in the host app.
25
+ BLOCKS_ROOT = "app/views/blocks"
26
+ # The ownership header poetry:block stamps on copy (stripped before
27
+ # comparing, like the gem template's own poetry:block header).
28
+ BLOCK_HEADER = /\A<%#[^%]*%>\n?/
29
+
30
+ desc "Report drift between copied-in poetry components/blocks and what the installed gems ship (read-only)"
31
+
32
+ # Step: reports drift for every manifest-recorded component copy-in.
33
+ # @api private
34
+ def report_components
35
+ entries = manifest_components
36
+ if entries.empty?
37
+ say "no copy-ins recorded in #{MANIFEST} - nothing to diff"
38
+ else
39
+ entries.each { |name, record| report_component(name, record) }
40
+ end
41
+ end
42
+
43
+ # Step: reports how copied blocks differ from the gem templates.
44
+ # @api private
45
+ def report_blocks
46
+ copied = Dir[File.join(destination_root, BLOCKS_ROOT, "_*.html.erb")]
47
+ return if copied.empty?
48
+
49
+ say "blocks are app-owned starting points - differing from the gem is normal, not drift:"
50
+ copied.each { |path| report_block(path) }
51
+ end
52
+
53
+ private
54
+
55
+ def report_component(name, record)
56
+ return report_remote(name, record) if record.is_a?(Hash) && record.key?("source")
57
+
58
+ version = record.is_a?(Hash) ? record["version"] : nil
59
+ gem_dir = Poetry::Ui.root.join("app/components/poetry/ui", name)
60
+ unless gem_dir.directory?
61
+ say_status :gone, "#{name} - copied at #{version}, no longer shipped by poetry-ui", :yellow
62
+ return
63
+ end
64
+
65
+ changes = component_changes(name, gem_dir)
66
+ header = "#{name} - copied at #{version || "unknown"}, gem ships #{Poetry::Ui::VERSION}"
67
+ if changes.empty?
68
+ say_status :same, "#{header}: all files match", :green
69
+ else
70
+ say_status :drift, header, :yellow
71
+ changes.each { |line| say " #{line}" }
72
+ end
73
+ end
74
+
75
+ # One line per file that is not byte-identical, in gem order, then any
76
+ # app-local extras. Line counts, not hunks - the report is a pointer,
77
+ # the diff tool of record is git in the app repo.
78
+ def component_changes(name, gem_dir)
79
+ changes = []
80
+ gem_files = gem_dir.glob("**/*").select(&:file?).sort
81
+ gem_files.each do |file|
82
+ relative = file.relative_path_from(gem_dir)
83
+ app_path = File.join(destination_root, COMPONENT_ROOT, name, relative.to_s)
84
+ if !File.exist?(app_path)
85
+ changes << "#{relative}: not copied locally (new upstream file? `bin/rails g poetry:add #{name}` " \
86
+ "adds missing files without touching existing ones)"
87
+ elsif File.read(app_path) != file.read
88
+ changes << "#{relative}: differs (app #{File.foreach(app_path).count} lines, " \
89
+ "gem #{file.each_line.count} lines)"
90
+ end
91
+ end
92
+ app_dir = File.join(destination_root, COMPONENT_ROOT, name)
93
+ Dir[File.join(app_dir, "**/*")].select { |f| File.file?(f) }.sort.each do |path|
94
+ relative = path.delete_prefix("#{app_dir}/")
95
+ changes << "#{relative}: local-only (yours)" unless gem_dir.join(relative).file?
96
+ end
97
+ changes
98
+ end
99
+
100
+ def report_remote(name, record)
101
+ say_status :remote, "#{name} - installed from #{record["source"]}; re-run " \
102
+ "`bin/rails g poetry:add #{record["source"]}` to compare (skip-if-exists)", :cyan
103
+ end
104
+
105
+ def report_block(path)
106
+ name = File.basename(path, ".html.erb").delete_prefix("_").tr("_", "-")
107
+ entry = block_catalog[name]
108
+ unless entry
109
+ say_status :yours, "#{name} - not in the gem catalog (renamed or your own)", :cyan
110
+ return
111
+ end
112
+
113
+ gem_body = Poetry::Ui.root.join(entry.fetch("template")).read.sub(BLOCK_HEADER, "")
114
+ app_body = File.read(path).sub(BLOCK_HEADER, "")
115
+ if app_body == gem_body
116
+ say_status :same, "#{name} - matches the gem's current template", :green
117
+ else
118
+ say_status :edited, "#{name} - differs from the gem's current template " \
119
+ "(reference: #{entry.fetch("template")})", :cyan
120
+ end
121
+ end
122
+
123
+ def manifest_components
124
+ path = File.join(destination_root, MANIFEST)
125
+ config = File.exist?(path) ? YAML.safe_load_file(path) : nil
126
+ config.is_a?(Hash) && config["components"].is_a?(Hash) ? config["components"] : {}
127
+ end
128
+
129
+ def block_catalog
130
+ @block_catalog ||= YAML.safe_load_file(
131
+ Poetry::Ui.root.join(Poetry::Core::Registry::RELATIVE_PATH)
132
+ )["blocks"] || {}
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,344 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "json"
5
+ require "yaml"
6
+ require "time"
7
+
8
+ module Poetry
9
+ # `rails g poetry:editor` - wire the poetry agent surface into the editors a
10
+ # Rails team actually uses. poetry has no bespoke extension (a possible
11
+ # future direction); what it DOES have is a standard MCP stdio server and a
12
+ # source-generated component registry, so this generator emits:
13
+ #
14
+ # .mcp.json Claude Code (mcpServers)
15
+ # .cursor/mcp.json Cursor (mcpServers)
16
+ # .vscode/mcp.json VS Code (servers + type: stdio)
17
+ # .vscode/poetry.code-snippets one snippet per poetry_* helper, with the
18
+ # real variant/size enums as tab-stop choices
19
+ # .herb.yml Herb (linter / formatter / language server), unless
20
+ # the app already has one
21
+ # .stimulus-lsp/config.json poetry's controller identifiers for Stimulus LSP
22
+ # (upsert: an existing ignore list keeps its entries)
23
+ #
24
+ # The MCP writes are upserts: an existing config keeps its other servers and
25
+ # gains a `poetry` entry; a config that already has one is left untouched; a
26
+ # JSONC file poetry can't parse is reported, never clobbered. Editors whose
27
+ # MCP config is global / IDE-managed (Zed, Windsurf, RubyMine) get a
28
+ # copy-paste block printed instead.
29
+ #
30
+ # @example
31
+ # bin/rails g poetry:editor
32
+ class EditorGenerator < Rails::Generators::Base
33
+ source_root File.expand_path("templates", __dir__)
34
+
35
+ # Editors whose MCP config is global or IDE-managed, so poetry prints a
36
+ # paste-block instead of writing a project file.
37
+ MANUAL_EDITORS = <<~TEXT
38
+ Zed settings.json ->
39
+ "context_servers": { "poetry": { "command": { "path": "bundle", "args": ["exec", "poetry-agent"] } } }
40
+ Windsurf ~/.codeium/windsurf/mcp_config.json ->
41
+ { "mcpServers": { "poetry": { "command": "bundle", "args": ["exec", "poetry-agent"] } } }
42
+ RubyMine Settings -> Tools -> AI Assistant -> Model Context Protocol (MCP) ->
43
+ add a stdio server: command `bundle exec poetry-agent` (or import .mcp.json). RubyMine 2025.2+.
44
+ TEXT
45
+
46
+ # The Herb toolchain (HTML+ERB parser, linter, formatter, language server)
47
+ # reads one file. Poetry templates are gated to parse AND compile under
48
+ # Herb (Rails core's ERB engine since 8.2), so a Poetry app can run the
49
+ # whole toolchain; two linter rules misread ViewComponent slot setters on
50
+ # helper-yielded builders today, so they start off with the upstream
51
+ # issues to watch. The version pin keeps a linter upgrade from enabling
52
+ # rules silently.
53
+ HERB_CONFIG = <<~YML
54
+ # Herb toolchain configuration (linter / formatter / language server).
55
+ # https://herb-tools.dev/configuration - written by `rails g poetry:editor`.
56
+ version: 0.10.3
57
+
58
+ framework: actionview
59
+
60
+ linter:
61
+ enabled: true
62
+ rules:
63
+ # `<%= poetry_card do |card| %><% card.with_title(...) %>` is flagged as
64
+ # a discarded value until marcoroth/herb#2426 lands.
65
+ erb-no-unused-expressions:
66
+ enabled: false
67
+ # `<% item.with_icon { ... } %>` (brace-form slot setters) is flagged and
68
+ # the autofix renders the wrong thing - marcoroth/herb#2340.
69
+ actionview-no-silent-helper:
70
+ enabled: false
71
+
72
+ formatter:
73
+ enabled: false
74
+ YML
75
+
76
+ # Where the Herb language server comes from, per editor.
77
+ HERB_EDITORS = <<~TEXT
78
+ VS Code install the "Herb LSP" extension (marcoroth.herb-lsp); Stimulus LSP (marcoroth.stimulus-lsp)
79
+ understands the same HTML+ERB
80
+ Zed ships inside the official Ruby extension - nothing to add
81
+ Neovim npm install -g @herb-tools/language-server, then lspconfig's herb_ls
82
+ CI npx --yes @herb-tools/linter app (or bundle exec herb lint)
83
+ Stimulus .stimulus-lsp/config.json lists poetry's controllers for Stimulus LSP - it checks YOUR
84
+ controllers; bin/rails poetry:check validates poetry's (data: keywords included)
85
+ TEXT
86
+
87
+ # Stimulus LSP resolves identifiers from the app's controller directories
88
+ # and node_modules; controllers a gem serves through importmap are
89
+ # invisible to it, so every hand-written `poetry--...` descriptor reads
90
+ # as an invalid controller with a quick-fix that scaffolds a bogus file.
91
+ # The LSP's one lever is an ignore list of exact identifiers, so poetry
92
+ # lists every controller its installed gems ship. Those descriptors are
93
+ # poetry:check's to validate (kwargs included); the LSP keeps yours.
94
+ STIMULUS_LSP_VERSION = "1.1.2" # the schema's writer version; the LSP rewrites it on its own saves
95
+
96
+ # The check hook: stdlib Ruby that runs `poetry check` on the one app
97
+ # template an agent just edited and feeds the findings back (errors as a
98
+ # blocking report, warnings as context). Registered for Claude Code and
99
+ # Cursor; the script filters to app templates itself, since neither
100
+ # hook's matcher can see a file path.
101
+ HOOK_SCRIPT = "bin/poetry-check-hook"
102
+ # The Claude Code entry: PostToolUse on the file-editing tools.
103
+ CLAUDE_HOOK = { "matcher" => "Edit|Write|MultiEdit",
104
+ "hooks" => [{ "type" => "command", "command" => HOOK_SCRIPT, "timeout" => 120 }] }.freeze
105
+ # Cursor's Write is its file-edit tool type; the payload is read
106
+ # leniently and the hook stays silent when it finds no path.
107
+ CURSOR_HOOK = { "command" => HOOK_SCRIPT, "matcher" => "Write", "timeout" => 120 }.freeze
108
+ # What the generator says about the hook once it is written.
109
+ HOOK_NOTE = <<~TEXT
110
+ bin/poetry-check-hook runs poetry check on every app template an agent edits and feeds the
111
+ findings back: registered in .claude/settings.json (Claude Code, PostToolUse on Edit/Write) and
112
+ .cursor/hooks.json (Cursor, postToolUse on Write). Claude Code reads hooks at session start -
113
+ start a new session (or check /hooks) before expecting feedback.
114
+ TEXT
115
+
116
+ desc "Wire poetry's MCP server + component snippets + Herb and Stimulus LSP config + the check hook into " \
117
+ "your editors (.mcp.json / .cursor/mcp.json / .vscode/mcp.json + .vscode/poetry.code-snippets + " \
118
+ ".herb.yml + .stimulus-lsp/config.json + bin/poetry-check-hook wired into .claude/settings.json and " \
119
+ ".cursor/hooks.json)"
120
+
121
+ # Step: upserts the poetry server into .mcp.json.
122
+ # @api private
123
+ def write_claude_code_config
124
+ upsert_mcp ".mcp.json", "mcpServers", stdio: false
125
+ end
126
+
127
+ # Step: upserts the poetry server into .cursor/mcp.json.
128
+ # @api private
129
+ def write_cursor_config
130
+ upsert_mcp ".cursor/mcp.json", "mcpServers", stdio: false
131
+ end
132
+
133
+ # Step: upserts the poetry server into .vscode/mcp.json.
134
+ # @api private
135
+ def write_vscode_config
136
+ upsert_mcp ".vscode/mcp.json", "servers", stdio: true
137
+ end
138
+
139
+ # Step: writes one VS Code snippet per poetry_* helper.
140
+ # @api private
141
+ def write_snippets
142
+ create_file ".vscode/poetry.code-snippets", "#{JSON.pretty_generate(snippets)}\n", force: true
143
+ end
144
+
145
+ # Step: writes .herb.yml for the Herb toolchain, unless the app has one.
146
+ # @api private
147
+ def write_herb_config
148
+ if File.exist?(File.join(destination_root, ".herb.yml"))
149
+ say_status :skip, ".herb.yml exists - keeping yours (poetry's rule notes: /editors on the docs site)",
150
+ :yellow
151
+ else
152
+ create_file ".herb.yml", HERB_CONFIG
153
+ end
154
+ end
155
+
156
+ # Step: lists poetry's controller identifiers for Stimulus LSP (upsert).
157
+ # @api private
158
+ def write_stimulus_lsp_config
159
+ upsert_stimulus_lsp ".stimulus-lsp/config.json"
160
+ end
161
+
162
+ # Step: writes the check hook script.
163
+ # @api private
164
+ def write_check_hook
165
+ copy_file "poetry-check-hook", HOOK_SCRIPT
166
+ chmod HOOK_SCRIPT, 0o755
167
+ end
168
+
169
+ # Step: registers the hook for Claude Code (upsert into .claude/settings.json).
170
+ # @api private
171
+ def write_claude_code_hook
172
+ upsert_hook ".claude/settings.json", "PostToolUse", CLAUDE_HOOK
173
+ end
174
+
175
+ # Step: registers the hook for Cursor (upsert into .cursor/hooks.json).
176
+ # @api private
177
+ def write_cursor_hook
178
+ upsert_hook ".cursor/hooks.json", "postToolUse", CURSOR_HOOK, seed: { "version" => 1 }
179
+ end
180
+
181
+ # Step: explains the check hook and when each editor picks it up.
182
+ # @api private
183
+ def announce_check_hook
184
+ say "\npoetry:editor - the check hook:", :green
185
+ say HOOK_NOTE
186
+ end
187
+
188
+ # Step: prints the paste-blocks for IDE-managed MCP configs.
189
+ # @api private
190
+ def announce_manual_editors
191
+ say "\npoetry:editor - editors with global / IDE-managed MCP config (paste-blocks):", :green
192
+ say MANUAL_EDITORS
193
+ say "\npoetry:editor - Herb (HTML+ERB linter + language server), configured by .herb.yml:", :green
194
+ say HERB_EDITORS
195
+ say "Full setup + a per-editor matrix: /editors on the docs site.\n"
196
+ end
197
+
198
+ private
199
+
200
+ # The stdio server every host launches the same way. Zero config beyond the
201
+ # bundle exec line; the agent auto-locates the gem and reads the committed
202
+ # registry (boot-free).
203
+ def server_entry(stdio:)
204
+ entry = { "command" => "bundle", "args" => %w[exec poetry-agent] }
205
+ stdio ? { "type" => "stdio" }.merge(entry) : entry
206
+ end
207
+
208
+ def upsert_mcp(relative, key, stdio:)
209
+ path = File.join(destination_root, relative)
210
+ unless File.exist?(path)
211
+ create_file relative, "#{JSON.pretty_generate(key => { "poetry" => server_entry(stdio: stdio) })}\n"
212
+ return
213
+ end
214
+
215
+ data = parse_json(File.read(path))
216
+ return say_status(:skip, "#{relative} isn't plain JSON (JSONC?) - add the poetry server by hand", :yellow) \
217
+ if data.nil?
218
+
219
+ servers = (data[key] ||= {})
220
+ return say_status(:identical, relative, :blue) if servers["poetry"]
221
+
222
+ servers["poetry"] = server_entry(stdio: stdio)
223
+ File.write(path, "#{JSON.pretty_generate(data)}\n")
224
+ say_status :update, "#{relative} (added the poetry server)", :green
225
+ end
226
+
227
+ # Upserts one hook entry under hooks.<event>: a file already naming the
228
+ # script anywhere in that list is identical, other hooks and keys are
229
+ # kept, a JSONC file is reported and never clobbered.
230
+ def upsert_hook(relative, event, entry, seed: {})
231
+ path = File.join(destination_root, relative)
232
+ unless File.exist?(path)
233
+ create_file relative, "#{JSON.pretty_generate(seed.merge("hooks" => { event => [entry] }))}\n"
234
+ return
235
+ end
236
+
237
+ data = parse_json(File.read(path))
238
+ if data.nil?
239
+ return say_status(:skip, "#{relative} isn't plain JSON (JSONC?) - add the poetry hook by hand",
240
+ :yellow)
241
+ end
242
+
243
+ hooks = (data["hooks"] ||= {})
244
+ entries = hooks.is_a?(Hash) ? (hooks[event] ||= []) : nil
245
+ unless entries.is_a?(Array)
246
+ return say_status(:skip, "#{relative} has an unexpected hooks shape - add the poetry hook by hand", :yellow)
247
+ end
248
+ return say_status(:identical, relative, :blue) if JSON.generate(entries).include?(HOOK_SCRIPT)
249
+
250
+ entries << entry
251
+ File.write(path, "#{JSON.pretty_generate(data)}\n")
252
+ say_status :update, "#{relative} (added the poetry check hook)", :green
253
+ end
254
+
255
+ # Every poetry-owned identifier the installed gems ship (core + any
256
+ # registered manifest), sorted for a stable file.
257
+ def poetry_identifiers
258
+ Poetry::Core::Stimulus::Manifest.catalog.keys.sort
259
+ end
260
+
261
+ def upsert_stimulus_lsp(relative)
262
+ path = File.join(destination_root, relative)
263
+ identifiers = poetry_identifiers
264
+ unless File.exist?(path)
265
+ create_file relative, "#{JSON.pretty_generate(stimulus_lsp_config(identifiers))}\n"
266
+ return
267
+ end
268
+
269
+ data = parse_json(File.read(path))
270
+ return say_status(:skip, "#{relative} isn't plain JSON - add poetry's identifiers by hand", :yellow) if data.nil?
271
+
272
+ options = (data["options"] ||= {})
273
+ ignored = Array(options["ignoredControllerIdentifiers"])
274
+ missing = identifiers - ignored
275
+ return say_status(:identical, relative, :blue) if missing.empty?
276
+
277
+ options["ignoredControllerIdentifiers"] = (ignored + missing).uniq.sort
278
+ options["ignoredAttributes"] = Array(options["ignoredAttributes"])
279
+ data["updatedAt"] = Time.now.utc.iso8601
280
+ File.write(path, "#{JSON.pretty_generate(data)}\n")
281
+ say_status :update, "#{relative} (+#{missing.size} poetry controller identifiers)", :green
282
+ end
283
+
284
+ def stimulus_lsp_config(identifiers)
285
+ now = Time.now.utc.iso8601
286
+ { "version" => STIMULUS_LSP_VERSION, "createdAt" => now, "updatedAt" => now,
287
+ "options" => { "ignoredControllerIdentifiers" => identifiers, "ignoredAttributes" => [] } }
288
+ end
289
+
290
+ # nil for anything we can't safely round-trip. JSONC comments are detected
291
+ # BEFORE parsing: some json versions silently accept `//`/`/* */` and would
292
+ # then drop the comments on re-serialize - poetry never clobbers a config it
293
+ # can't preserve.
294
+ def parse_json(text)
295
+ return nil if text.match?(%r{^\s*//}) || text.include?("/*")
296
+
297
+ JSON.parse(text)
298
+ rescue JSON::ParserError
299
+ nil
300
+ end
301
+
302
+ # One snippet per main component helper (poetry_<name>), driven by the
303
+ # committed registry so it never drifts from the shipped catalog. A block
304
+ # form for components that compose via slots; inline for atoms. The primary
305
+ # enum (variant, else the first declared one) becomes a choice tab-stop, so
306
+ # the completion only offers values that actually exist.
307
+ def snippets
308
+ registry = YAML.safe_load_file(Poetry::Ui.root.join("config/component_registry.yml"))
309
+ registry.fetch("components").keys.sort.to_h do |path|
310
+ name = path.delete_prefix("poetry/ui/").tr("/", "_")
311
+ component = registry.fetch("components").fetch(path)
312
+ [
313
+ "poetry #{name.tr("_", " ")}",
314
+ { "prefix" => "poetry_#{name}", "scope" => "erb",
315
+ "body" => snippet_body(name, component), "description" => "poetry #{humanize(name)} component" }
316
+ ]
317
+ end
318
+ end
319
+
320
+ def snippet_body(name, component)
321
+ call = "poetry_#{name}#{enum_argument(component)}"
322
+ if Array(component["slots"]).any?
323
+ ["<%= #{call} do |c| %>", "\t$0", "<% end %>"]
324
+ else
325
+ "<%= #{call} %>$0"
326
+ end
327
+ end
328
+
329
+ # `(variant: :${1|a,b,c|})` for the primary enum, or "" when the component
330
+ # declares none. Enums come from BOTH the styles DSL (variant/size) and
331
+ # options (tag/orientation/...); a declared `variant` wins.
332
+ def enum_argument(component)
333
+ enums = (Array(component["styles"]) + Array(component["options"])).select { |o| o["variants"] }
334
+ chosen = enums.find { |o| o["name"] == "variant" } || enums.first
335
+ return "" unless chosen
336
+
337
+ "(#{chosen["name"]}: :${1|#{Array(chosen["variants"]).join(",")}|})"
338
+ end
339
+
340
+ def humanize(name)
341
+ name.split("_").map(&:capitalize).join(" ")
342
+ end
343
+ end
344
+ end