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
data/lib/poetry/ui.rb ADDED
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "poetry/core"
4
+ require "yaml"
5
+ require_relative "ui/version"
6
+ require_relative "ui/themes"
7
+ require_relative "ui/code_block_highlighter"
8
+ require_relative "ui/recipes"
9
+ require_relative "ui/chat"
10
+ require_relative "ui/webmcp"
11
+
12
+ # The poetry namespace: poetry-ui shares it with poetry-core (the DSL)
13
+ # and the optional poetry-charts.
14
+ module Poetry
15
+ # The component library: ViewComponents tracking the full ported
16
+ # component vocabulary, built entirely on
17
+ # poetry-core's PUBLIC DSL - if a component here needs private core API,
18
+ # that is a core API gap, not a license to reach in.
19
+ module Ui
20
+ # The committed static-template-class list (herb-extracted in poetry's
21
+ # CI, drift-gated) - poetry:install reads it so hosts never need herb.
22
+ TEMPLATE_CLASSES_PATH = "config/template_classes.txt"
23
+
24
+ # Where the block templates live: the generator's source tree,
25
+ # scanned by the registry builder, the template-class extraction,
26
+ # the dummy's /blocks previews, and the MCP server's describe_block.
27
+ BLOCKS_DIR = "lib/generators/poetry/block/templates"
28
+
29
+ # Curated composition edges: which components a copy-in of X also needs
30
+ # locally. Single-sourced here - the add generator's recursive copy AND
31
+ # every registry item's registryDependencies read this one map.
32
+ # (Replaced by registry-carried anatomy when the contract's anatomy
33
+ # section lands.)
34
+ COMPONENT_DEPENDENCIES = {
35
+ "button" => %w[icon],
36
+ "dialog" => %w[button icon],
37
+ "sheet" => %w[dialog button icon], # subclasses Dialog::Component (shared dialog controller)
38
+ "alert_dialog" => %w[dialog button], # shares the dialog controller + posture
39
+ "field" => %w[label input],
40
+ "alert" => %w[icon],
41
+ "select" => %w[icon], # chevrons + check; Field is an optional pairing, not a hard edge
42
+ "number_field" => %w[input input_group button icon], # composes their chrome
43
+ "stat" => %w[icon], # the delta's trend arrow
44
+ "toolbar" => %w[button input separator], # the typed slots render them
45
+ "file_input" => %w[input icon], # input variant wears Input; the dropzone's upload glyph
46
+ "date_field" => %w[input], # the pre-enhancement native input wears Input's chrome
47
+ "time_field" => %w[date_field input], # DateField at hour granularity - one segment engine
48
+ "meter" => %w[progress], # wears Progress::Style chrome verbatim
49
+ "search_field" => %w[input input_group button icon], # composes their chrome
50
+ "tag_group" => %w[icon], # the remove glyph
51
+ "timeline" => %w[icon], # the indicator glyph
52
+ "tree" => %w[icon], # the chevron
53
+ "clipboard_text" => %w[input input_group button icon], # composes their chrome
54
+ "sensitive_input" => %w[input input_group button icon clipboard_text], # + the copy: engine
55
+ "code_block" => %w[button icon] # the copy affordance
56
+ }.freeze
57
+
58
+ # The usage skill's family partition: every component in exactly one
59
+ # reference file, so the skill's menu stays lean and an agent loads
60
+ # only the family it is composing in. The coverage gate fails on any
61
+ # new component until it is mapped here.
62
+ SKILL_FAMILIES = {
63
+ "forms" => %w[autocomplete button button_group calendar checkbox combobox date_field date_picker field
64
+ field_group field_separator fieldset
65
+ file_input input input_group input_otp label native_select number_field
66
+ questionnaire radio_group search_field select sensitive_input slider switch textarea time_field
67
+ toggle toggle_group],
68
+ "overlays" => %w[alert_dialog command command_dialog context_menu dialog drawer
69
+ dropdown_menu hover_card menubar popover sheet tooltip],
70
+ "data" => %w[accordion avatar badge card carousel clipboard_text code_block collapsible data_table empty
71
+ item metadata_list meter stat table tag_group timeline toolbar tree typeset],
72
+ "feedback" => %w[alert deferred progress skeleton spinner toast toast_trigger toaster],
73
+ "navigation" => %w[breadcrumb navigation_menu pagination sidebar tabs],
74
+ "foundations" => %w[icon kbd link marker separator],
75
+ "chat" => %w[attachment bubble message message_scroller],
76
+ "layout" => %w[aspect_ratio resizable scroll_area]
77
+ }.freeze
78
+
79
+ class << self
80
+ # Gem root (the directory containing lib/, app/, config/).
81
+ def root
82
+ @root ||= Pathname.new(File.expand_path("../..", __dir__))
83
+ end
84
+
85
+ # @return [Array<String>] the committed template-static classes
86
+ def template_classes
87
+ root.join(TEMPLATE_CLASSES_PATH).read.lines
88
+ .map(&:strip).reject { |line| line.empty? || line.start_with?("#") }
89
+ end
90
+
91
+ # The public poetry_* helper names, so poetry check / poetry-agent know
92
+ # the full set (group / provider helpers AND the define_method'd part
93
+ # helpers like poetry_table_cell) WITHOUT booting Rails. The module's
94
+ # method bodies reference component constants only at call time, so
95
+ # requiring the file standalone is safe.
96
+ def helper_names
97
+ require root.join("app/helpers/poetry/ui/components_helper.rb")
98
+ ComponentsHelper.public_instance_methods(false).grep(/\Apoetry_/).map(&:to_s)
99
+ rescue StandardError
100
+ # Fallback: the static-def helpers (misses define_method ones) if the
101
+ # module can't load standalone in some host.
102
+ root.join("app/helpers/poetry/ui/components_helper.rb").read.scan(/def (poetry_[a-z_]+)/).flatten.uniq
103
+ end
104
+
105
+ # The registry builder this gem commits from (booted contexts only:
106
+ # rake registry:generate/verify and the sync test share it, so the
107
+ # drift gate always compares against the exact construction that
108
+ # generated the file - helpers AND blocks sections included).
109
+ def registry
110
+ components = Poetry::Core::Registry.new(source_root: root).components
111
+ component_paths = components.map(&:component_path)
112
+ Poetry::Core::Registry.new(
113
+ components: components, source_root: root,
114
+ helpers: registry_helpers(component_paths: component_paths),
115
+ blocks: registry_blocks(component_paths: component_paths),
116
+ helper_args: registry_helper_args,
117
+ descriptions: registry_descriptions,
118
+ form_builder: Poetry::Ui::FormBuilder.registry_section
119
+ )
120
+ end
121
+
122
+ # The editorial per-component descriptions merged into the registry
123
+ # (component_path => one-liner, from config/component_descriptions.yml).
124
+ # Absent file -> nil, so a registry without it stays lint-identical, like
125
+ # every other optional section.
126
+ def registry_descriptions
127
+ path = root.join("config/component_descriptions.yml")
128
+ path.exist? ? YAML.safe_load_file(path) : nil
129
+ end
130
+
131
+ # The interop item projection - registry items in the ecosystem's
132
+ # shared JSON item schema, boot-free
133
+ # from the COMMITTED registry - the docs site serves /r/*.json from
134
+ # this, and the add generator matches gem-satisfied dependencies
135
+ # against its names.
136
+ def registry_items
137
+ Poetry::Core::RegistryItems.new(
138
+ registry: YAML.safe_load_file(root.join(Poetry::Core::Registry::RELATIVE_PATH)),
139
+ root: root, gem_name: "poetry-ui", gem_version: VERSION,
140
+ dependencies: COMPONENT_DEPENDENCIES
141
+ )
142
+ end
143
+
144
+ # The recipes projection: skill bundles, scaffold template
145
+ # sets, and screen slices as registry items -
146
+ # served at /r/*.json beside components and blocks, installed by
147
+ # poetry:add or any client speaking the same item schema.
148
+ def recipe_items
149
+ Poetry::Core::RecipeItems.new(
150
+ recipes: Recipes.definitions, gem_name: "poetry-ui", gem_version: VERSION
151
+ )
152
+ end
153
+
154
+ # The installable component-usage skill: a lean SKILL.md menu +
155
+ # per-family references, generated from the live
156
+ # registry - the seam the poetry:skill generator and the eval
157
+ # harness share (the agents_section_text pattern).
158
+ def skill_files
159
+ Poetry::Core::SkillText.new(
160
+ registry: registry, families: SKILL_FAMILIES, charts_registry: charts_registry
161
+ ).files
162
+ end
163
+
164
+ # Tolerant on charts, like the AGENTS.md census: a host without the
165
+ # gem (or with a stubbed/partial one) just drops the charts reference.
166
+ def charts_registry
167
+ return nil unless defined?(Poetry::Charts::Engine)
168
+
169
+ Poetry::Charts.registry
170
+ rescue StandardError
171
+ nil
172
+ end
173
+
174
+ # The MCP server's runtime skill map (get_skill): the SAME
175
+ # files `rails g poetry:skill` writes, for hosts that cannot write
176
+ # files. Boot-free by construction - the usage skill regenerates from
177
+ # the COMMITTED registries (never the booted builders above), the
178
+ # design skill reads its static templates - so the exe can serve both
179
+ # without Rails. Lazy: nothing generates until an agent asks.
180
+ def agent_skills
181
+ {
182
+ "poetry" => -> { runtime_skill_files },
183
+ "poetry-design" => lambda {
184
+ require "generators/poetry/skills_section"
185
+ Object.new.extend(Poetry::Generators::SkillsSection).design_skill_files
186
+ },
187
+ "poetry-component" => lambda {
188
+ require "generators/poetry/skills_section"
189
+ Object.new.extend(Poetry::Generators::SkillsSection).component_skill_files
190
+ }
191
+ }
192
+ end
193
+
194
+ # The usage-skill files regenerated boot-free from the COMMITTED
195
+ # registries (never the booted builders above) - what the MCP server
196
+ # and the recipes projection serve without Rails.
197
+ #
198
+ # @api private
199
+ def runtime_skill_files
200
+ Poetry::Core::SkillText.new(
201
+ registry: Poetry::Core::Registry.committed(root),
202
+ families: SKILL_FAMILIES, charts_registry: committed_charts_registry
203
+ ).files
204
+ end
205
+
206
+ # The charts reference rides along whenever the charts gem is present
207
+ # (its committed registry, not its booted builder); absent, the skill
208
+ # simply drops it - the charts_registry tolerance, boot-free.
209
+ def committed_charts_registry
210
+ require "poetry/charts"
211
+ Poetry::Core::Registry.committed(Poetry::Charts.root)
212
+ rescue LoadError, StandardError
213
+ nil
214
+ end
215
+
216
+ # The parameter kinds that count as positional in a signature.
217
+ POSITIONAL_PARAM_KINDS = %i[req opt].freeze
218
+
219
+ # The registry "helper_args" map: max positional arity for EVERY
220
+ # poetry_* helper, introspected from the real method signatures, so
221
+ # the linter can flag the crash class of `poetry_link "text", href:`
222
+ # against a kwargs-only helper. Rest-signatures are omitted - the
223
+ # linter enforces arity only where a key exists.
224
+ #
225
+ # @api private
226
+ def registry_helper_args
227
+ helper_names.sort.filter_map do |name|
228
+ params = ComponentsHelper.instance_method(name.to_sym).parameters
229
+ next if params.any? { |kind, _param| kind == :rest }
230
+
231
+ [name, params.count { |kind, _param| POSITIONAL_PARAM_KINDS.include?(kind) }]
232
+ end.to_h
233
+ end
234
+
235
+ # The registry "helpers" section: every poetry_* helper that
236
+ # maps to no component - group/provider/item wrappers - each carrying
237
+ # its declared value contract (ComponentsHelper::HELPER_CONTRACTS) or
238
+ # {} for a plain wrapper.
239
+ def registry_helpers(component_paths:)
240
+ mapped = component_paths.map { |path| "poetry_#{path.delete_prefix("poetry/ui/").tr("/", "_")}" }
241
+ (helper_names - mapped).sort.to_h do |name|
242
+ [name, ComponentsHelper::HELPER_CONTRACTS.fetch(name, {})]
243
+ end
244
+ end
245
+
246
+ # The registry "blocks" section: every block template's metadata,
247
+ # all source-derived - title/description/keywords from the
248
+ # mandatory poetry:block header (keywords power the MCP compose
249
+ # tool's brief routing), the composed component list from the
250
+ # template's own poetry_* calls (longest-prefix fold:
251
+ # sidebar_menu_button counts as sidebar), the gem-relative template
252
+ # path for boot-free source reads. No hand-authored catalog to drift.
253
+ def registry_blocks(component_paths:)
254
+ titles = component_paths.map { |path| path.delete_prefix("poetry/ui/").tr("/", "_") }
255
+ Dir.glob(root.join(BLOCKS_DIR, "*.html.erb").to_s).to_h do |file|
256
+ source = File.read(file)
257
+ header = source.match(
258
+ /\A<%#\s*poetry:block\s+title="([^"]*)"\s+description="([^"]*)"(?:\s+keywords="([^"]*)")?\s*%>/
259
+ )
260
+ raise Poetry::Core::Error, "#{file} is missing its poetry:block header" unless header
261
+
262
+ entry = { "title" => header[1], "description" => header[2],
263
+ "components" => block_components(source, titles: titles),
264
+ "template" => "#{BLOCKS_DIR}/#{File.basename(file)}" }
265
+ entry["keywords"] = header[3].split(",").map(&:strip) if header[3]
266
+ [File.basename(file, ".html.erb").tr("_", "-"), entry]
267
+ end
268
+ end
269
+
270
+ # The distinct components a block composes, folded from its poetry_*
271
+ # helper calls: each call maps to the LONGEST component title that
272
+ # prefixes it (poetry_sidebar_menu_badge -> sidebar; poetry_table_head
273
+ # -> table); calls matching no component (pure wrapper helpers like
274
+ # poetry_input_group_addon fold through input_group) are dropped
275
+ # rather than guessed.
276
+ def block_components(source, titles:)
277
+ source.scan(/\bpoetry_([a-z0-9_]+)/).flatten.uniq.filter_map do |called|
278
+ titles.select { |title| called == title || called.start_with?("#{title}_") }.max_by(&:length)
279
+ end.uniq.sort
280
+ end
281
+ end
282
+ end
283
+ end
284
+
285
+ require_relative "ui/engine"
data/lib/poetry-ui.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Convenience entrypoint so `require "poetry-ui"` (Bundler auto-require)
4
+ # resolves to the namespaced Poetry::Ui gem.
5
+ require_relative "poetry/ui"
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Host-side poetry check: lint the app's ERB against the committed
4
+ # registry + controllers manifest.
5
+ # Loaded automatically by the engine (lib/tasks).
6
+ namespace :poetry do
7
+ desc "Lint app ERB + icon declarations in app Ruby against the poetry registry " \
8
+ "(glob arg overrides the default sweep; POETRY_CHECK_JSON=1 for JSON)"
9
+ task :check, [:glob] => :environment do |_task, args|
10
+ # The linter parses ERB with herb, which stays out of poetry's runtime
11
+ # dependencies on purpose (hosts never need it to RENDER) - same
12
+ # optional-parser posture as poetry:verify's template gate.
13
+ unless Poetry::Core::CSS::TemplateClasses.available?
14
+ abort "poetry:check: the herb gem is required to parse templates - " \
15
+ "add `gem \"herb\"` to your Gemfile (development group is enough)"
16
+ end
17
+
18
+ # Default sweep: ERB through the template tier, app Ruby through the
19
+ # icon-declaration tier (the FLASH_ICONS pattern lives in .rb).
20
+ patterns = args[:glob] ? [args[:glob]] : ["app/{views,components}/**/*.html.erb", "app/**/*.rb"]
21
+ paths = patterns.flat_map { |pattern| Dir.glob(Rails.root.join(pattern).to_s) }.uniq
22
+ if paths.empty?
23
+ warn "poetry check: no files matched #{patterns.join(", ").inspect}"
24
+ exit 0
25
+ end
26
+
27
+ helpers = Poetry::Ui.helper_names
28
+ roots = [Poetry::Ui.root]
29
+ if defined?(Poetry::Charts::ComponentsHelper)
30
+ helpers += Poetry::Charts::ComponentsHelper.public_instance_methods(false)
31
+ .grep(/\Apoetry_/).map(&:to_s)
32
+ # The charts registry must join the catalog, not just the helper
33
+ # names - a name-valid pathless helper reads as a yielding wrapper
34
+ # (the chart yieldless-block false-positive class).
35
+ roots << Poetry::Charts.root
36
+ end
37
+ # The active set's names light up membership validation (the value
38
+ # contract) and the declaration tier; a host without a
39
+ # registered set still checks icon-name shape.
40
+ icon_names = begin
41
+ Poetry::Core::Icons.set.names
42
+ rescue Poetry::Core::Error
43
+ nil
44
+ end
45
+ catalog = Poetry::Core::Check::Catalog.from_registries(roots, helpers: helpers, icon_names: icon_names)
46
+ findings = Poetry::Core::Check::Runner.new(catalog).run(paths)
47
+
48
+ # The taste tier: design-slop warnings join the mechanical
49
+ # findings on request - same vocabulary, same JSON/text output. The
50
+ # stock-theme nudge fires only for a FOREIGN DESIGN.md (a brand waiting
51
+ # to be applied) - poetry's own export IS the current state.
52
+ if ENV["POETRY_CHECK_DESIGN"] == "1"
53
+ # DesignLint herb-parses templates - the .rb paths in the sweep are
54
+ # the declaration tier's, not its.
55
+ findings += paths.grep(/\.erb\z/).flat_map { |path| Poetry::Core::DesignLint.lint(File.read(path), file: path) }
56
+ design_md = Rails.root.join("DESIGN.md")
57
+ foreign = design_md.exist? && Poetry::Core::DesignMd.parse(design_md.read)["theme"].nil?
58
+ findings += Poetry::Core::DesignLint.lint_dom(
59
+ doc: nil,
60
+ context: { design_md_present: foreign,
61
+ overrides_present: Rails.root.join("app/assets/tailwind/poetry/design-overrides.css").exist? }
62
+ )
63
+ end
64
+
65
+ if ENV["POETRY_CHECK_JSON"] == "1"
66
+ puts Poetry::Core::Check.to_json(findings)
67
+ else
68
+ puts Poetry::Core::Check.to_text(findings)
69
+ end
70
+
71
+ exit 1 if findings.any? { |finding| finding.severity == :error }
72
+ end
73
+ end
@@ -0,0 +1,293 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Host-side DESIGN.md export: serialize THIS app's poetry design as
4
+ # the design-skill ecosystem's shared artifact, so external design
5
+ # skills can read the system the app actually ships. Loaded
6
+ # automatically by the engine (lib/tasks).
7
+ namespace :poetry do
8
+ namespace :design do
9
+ desc "Export this app's poetry design to DESIGN.md " \
10
+ "(POETRY_DESIGN_PATH overrides the target; POETRY_DESIGN_FORCE=1 overwrites a non-poetry file)"
11
+ task export: :environment do
12
+ path = Pathname.new(ENV["POETRY_DESIGN_PATH"] || Rails.root.join("DESIGN.md"))
13
+ if path.exist? && Poetry::Core::DesignMd.parse(path.read)["theme"].nil? && ENV["POETRY_DESIGN_FORCE"] != "1"
14
+ abort "poetry:design:export: #{path} exists and was not written by poetry - " \
15
+ "set POETRY_DESIGN_PATH to export elsewhere, or POETRY_DESIGN_FORCE=1 to overwrite"
16
+ end
17
+
18
+ theme = poetry_design_installed_theme
19
+ registry = YAML.safe_load_file(Poetry::Ui.root.join(Poetry::Core::Registry::RELATIVE_PATH))
20
+ details = Poetry::Ui::Themes.details(theme).merge(
21
+ "components_count" => registry.fetch("components").size,
22
+ "generator" => "bin/rails poetry:design:export"
23
+ )
24
+ doc = Poetry::Core::DesignMd.build(tokens: Poetry::Core::Tokens.load, theme: theme, details: details,
25
+ deviations: poetry_design_declared_overrides)
26
+ poetry_design_apply_overrides(doc)
27
+ path.write(Poetry::Core::DesignMd.serialize(doc))
28
+
29
+ poetry_design_warn_on_token_drift
30
+ puts "poetry:design:export: wrote #{path} (theme #{theme})"
31
+ end
32
+
33
+ # The .cn-* override contract (the intent-vs-accident
34
+ # model): token-level restyling has a sanctioned channel
35
+ # (poetry:design:import -> design-overrides.css); host CSS that targets
36
+ # theme-owned .cn-* classes is the OTHER channel, and every such
37
+ # override must be declared - dated, reasoned, scoped - under
38
+ # `overrides:` in config/poetry_components.yml. This task reports both
39
+ # directions (undeclared drift AND stale declarations); undeclared
40
+ # findings print a paste-ready declaration. Never silence a finding to
41
+ # skip fixing it - declare only what the design owner confirms.
42
+ desc "Report host CSS overriding theme-owned .cn-* classes against the declared contract " \
43
+ "(config/poetry_components.yml `overrides:`; STRICT=1 exits nonzero on undeclared/invalid)"
44
+ task overrides: :environment do
45
+ scan = Poetry::Core::CSS::OverrideScan.new(
46
+ sources: poetry_design_host_css_sources,
47
+ declarations: poetry_design_declared_overrides
48
+ )
49
+
50
+ scan.invalid.each { |message| puts " INVALID #{message}" }
51
+ scan.stale.each do |declaration|
52
+ puts " STALE overrides[#{declaration.index}] (cn: #{declaration.cn.inspect}) matches no host CSS - " \
53
+ "remove it or fix its `files:` scope"
54
+ end
55
+ scan.undeclared.each do |path, classes|
56
+ puts " UNDECLARED #{path}: #{classes.join(", ")}"
57
+ puts " declare it (after confirming intent) in config/poetry_components.yml under overrides:"
58
+ puts scan.snippet_for(path, classes).gsub(/^/, " ")
59
+ end
60
+
61
+ if scan.ok?
62
+ puts "poetry:design:overrides: clean - #{scan.declared_count} declared override(s), " \
63
+ "#{scan.stale.size} stale declaration(s)"
64
+ else
65
+ message = "poetry:design:overrides: #{scan.undeclared.sum { |_, c| c.size }} undeclared .cn-* " \
66
+ "override(s), #{scan.invalid.size} invalid declaration(s)"
67
+ ENV["STRICT"] == "1" ? abort(message) : puts(message)
68
+ end
69
+ end
70
+
71
+ desc "Import a design source into token overrides " \
72
+ "(a DESIGN.md, a Figma/DTCG variables .json, or a Paper/CSS .css theme -> " \
73
+ "app/assets/tailwind/poetry/design-overrides.css; AA enforced at the door, " \
74
+ "POETRY_DESIGN_FORCE=1 ships failing pairs, POETRY_DESIGN_JSON=1 for a JSON report)"
75
+ task :import, [:path] => :environment do |_task, args|
76
+ poetry_design_run_import(args[:path], task: "poetry:design:import")
77
+ end
78
+ end
79
+
80
+ # Design-tool token bridges: thin, discoverable entry points onto the SAME
81
+ # importer. `TokenImport.load` dispatches by extension, so a Figma variables
82
+ # .json or a Paper "Copy theme" .css runs through the exact AA-gated pipeline
83
+ # poetry:design:import uses -- imported swatches inherit the contrast law, the
84
+ # drop-not-fabricate rule, and the dark-mode pins for free.
85
+ namespace :figma do
86
+ desc "Import a Figma variables export (DTCG .json) into token overrides " \
87
+ "(same AA-gated pipeline as poetry:design:import; POETRY_DESIGN_FORCE / POETRY_DESIGN_JSON honored)"
88
+ task :import, [:path] => :environment do |_task, args|
89
+ poetry_design_run_import(args[:path], task: "poetry:figma:import")
90
+ end
91
+ end
92
+
93
+ namespace :paper do
94
+ desc "Import a Paper 'Copy theme' export (CSS .css) into token overrides " \
95
+ "(same AA-gated pipeline as poetry:design:import; POETRY_DESIGN_FORCE / POETRY_DESIGN_JSON honored)"
96
+ task :import, [:path] => :environment do |_task, args|
97
+ poetry_design_run_import(args[:path], task: "poetry:paper:import")
98
+ end
99
+ end
100
+ end
101
+
102
+ # Shared importer for the DESIGN.md / Figma / Paper entry points: one file in,
103
+ # one AA-gated design-overrides.css out. Poetry::Core::TokenImport.load
104
+ # dispatches by extension (.md -> DesignMd.parse, .json -> Figma/DTCG variables,
105
+ # .css -> Paper/CSS custom properties); everything downstream is the pipeline
106
+ # poetry:design:import already ran, so nothing new decides what ships.
107
+ def poetry_design_run_import(source, task:)
108
+ abort "#{task}: pass the file - bin/rails '#{task}[path]'" unless source
109
+ abort "#{task}: no such file #{source}" unless File.exist?(source)
110
+
111
+ doc = Poetry::Core::TokenImport.load(source)
112
+ plan = Poetry::Core::DesignMd::Import.new.plan(doc, force: ENV["POETRY_DESIGN_FORCE"] == "1")
113
+
114
+ written = nil
115
+ if plan.any_overrides?
116
+ written = Rails.root.join("app/assets/tailwind/poetry/design-overrides.css")
117
+ written.write(poetry_design_overrides_css(plan, File.basename(source)))
118
+ poetry_design_wire_overrides
119
+ end
120
+
121
+ if ENV["POETRY_DESIGN_JSON"] == "1"
122
+ puts JSON.pretty_generate(poetry_design_report_json(plan, written))
123
+ else
124
+ puts poetry_design_report(plan, written)
125
+ end
126
+ end
127
+
128
+ # The declared .cn-* overrides from the host manifest - tolerant
129
+ # like every other manifest reader (missing file/key = none declared).
130
+ def poetry_design_declared_overrides
131
+ path = Rails.root.join("config/poetry_components.yml")
132
+ # Hosts write `created:` as a bare YAML date - permit it, then normalize
133
+ # to ISO strings so everything downstream (scan, DESIGN.md) is string-typed.
134
+ config = path.exist? ? YAML.safe_load_file(path, permitted_classes: [Date]) : nil
135
+ return [] unless config.is_a?(Hash) && config["overrides"].is_a?(Array)
136
+
137
+ config["overrides"].map do |entry|
138
+ entry.is_a?(Hash) ? entry.merge("created" => entry["created"]&.to_s) : entry
139
+ end
140
+ end
141
+
142
+ # Host-owned stylesheet SOURCES: everything the app authors, excluding the
143
+ # poetry-vendored set (which legitimately defines cn-*) and compiled output.
144
+ def poetry_design_host_css_sources
145
+ root = Rails.root
146
+ sources = {}
147
+ root.glob("app/assets/**/*.css").each do |path|
148
+ relative = path.relative_path_from(root).to_s
149
+ next if relative.start_with?("app/assets/tailwind/poetry/", "app/assets/builds/")
150
+
151
+ sources[relative] = path.read
152
+ end
153
+ sources
154
+ end
155
+
156
+ # The installed theme, recovered from the slot bytes themselves (the
157
+ # installer vendors the chosen fragment verbatim into the style-default.css
158
+ # slot) - "custom" when the slot was hand-edited past recognition.
159
+ def poetry_design_installed_theme
160
+ slot = Rails.root.join("app/assets/tailwind/poetry/style-default.css")
161
+ abort "poetry:design:export: no poetry install found (#{slot} missing) - run `bin/rails g poetry:install`" \
162
+ unless slot.exist?
163
+
164
+ bytes = slot.read
165
+ Poetry::Ui::Themes.names.find { |name| Poetry::Ui.root.join("themes/#{name}.css").read == bytes } || "custom"
166
+ end
167
+
168
+ # The vendored tokens.css may have been replaced wholesale (the shadcn
169
+ # drop-in contract) - this export reads the gem's DTCG source, so say so
170
+ # rather than silently exporting values the app no longer paints.
171
+ def poetry_design_warn_on_token_drift
172
+ host_tokens = Rails.root.join("app/assets/tailwind/poetry/tokens.css")
173
+ return unless host_tokens.exist?
174
+ return if host_tokens.read == Poetry::Core.root.join("tokens/tokens.css").read
175
+
176
+ puts "poetry:design:export: NOTE - app/assets/tailwind/poetry/tokens.css diverges from the gem " \
177
+ "defaults (drop-in theme or hand edits); this export lists the gem's token values"
178
+ end
179
+
180
+ # Exports must tell the truth AFTER an import: fold the app's
181
+ # design-overrides.css (our own generated format) back into the document
182
+ # so the exported DESIGN.md carries the values the app actually paints.
183
+ def poetry_design_apply_overrides(doc)
184
+ path = Rails.root.join("app/assets/tailwind/poetry/design-overrides.css")
185
+ return unless path.exist?
186
+
187
+ path.read.scan(/^(:root|\.dark)\s*\{([^}]*)\}/m).each do |selector, body|
188
+ mode = selector == ":root" ? "light" : "dark"
189
+ body.scan(/--([a-z0-9-]+):\s*([^;]+);/).each do |name, value|
190
+ if name == "radius"
191
+ doc["radius"] = value.strip if mode == "light"
192
+ elsif doc["colors"][mode].key?(name) && (color = Poetry::Core::Tokens::Color.parse(value.strip))
193
+ doc["colors"][mode][name] = color
194
+ end
195
+ end
196
+ end
197
+ puts "poetry:design:export: includes app/assets/tailwind/poetry/design-overrides.css values"
198
+ end
199
+
200
+ # The overrides stylesheet: :root carries the imported light values (+
201
+ # radius); .dark carries imported dark values plus the poetry-default PINS
202
+ # for light-overridden names the source left dark-silent (without them the
203
+ # later :root block would also win in dark mode - equal specificity).
204
+ def poetry_design_overrides_css(plan, source_name)
205
+ lines = ["/* Generated by `bin/rails poetry:design:import` from #{source_name} - do not edit.",
206
+ " Loads AFTER the poetry theme imports so these blocks win the cascade;",
207
+ " re-run the import to regenerate. */", ""]
208
+ lines << ":root {"
209
+ lines << " --radius: #{plan.radius};" if plan.radius
210
+ plan.overrides["light"].sort.each { |name, color| lines << " --#{name}: #{color.css};" }
211
+ lines << "}"
212
+
213
+ dark = plan.overrides["dark"].sort + plan.pins.sort
214
+ if dark.any?
215
+ lines << "" << ".dark {"
216
+ plan.overrides["dark"].sort.each { |name, color| lines << " --#{name}: #{color.css};" }
217
+ if plan.pins.any?
218
+ lines << " /* pinned to the poetry defaults (source had no dark palette) */"
219
+ plan.pins.sort.each { |name, color| lines << " --#{name}: #{color.css};" }
220
+ end
221
+ lines << "}"
222
+ end
223
+ "#{lines.join("\n")}\n"
224
+ end
225
+
226
+ # Idempotently add the overrides import after the last top-level ./poetry/
227
+ # import in the Tailwind entry (later = wins at equal specificity).
228
+ def poetry_design_wire_overrides
229
+ entry = Rails.root.join("app/assets/tailwind/application.css")
230
+ import_line = %(@import "./poetry/design-overrides.css";)
231
+ unless entry.exist?
232
+ puts "poetry:design:import: NOTE - no #{entry}; add #{import_line} to your Tailwind entry yourself"
233
+ return
234
+ end
235
+
236
+ lines = entry.read.lines
237
+ return if lines.any? { |line| line.start_with?(import_line) }
238
+
239
+ last = lines.rindex { |line| line.match?(%r{\A@(?:import|source) "\./poetry/}) }
240
+ if last
241
+ lines.insert(last + 1, "#{import_line}\n")
242
+ entry.write(lines.join)
243
+ puts "poetry:design:import: wired #{import_line} into app/assets/tailwind/application.css"
244
+ else
245
+ puts "poetry:design:import: NOTE - no ./poetry/ imports found in #{entry}; " \
246
+ "add #{import_line} after them yourself"
247
+ end
248
+ end
249
+
250
+ def poetry_design_report(plan, written)
251
+ out = []
252
+ plan.overrides.each do |mode, colors|
253
+ next if colors.empty?
254
+
255
+ out << (" applied (#{mode}): " + colors.sort.map { |name, color| "#{name} #{color.css}" }.join(", "))
256
+ end
257
+ out << " radius: #{plan.radius}" if plan.radius
258
+ out << " pinned (dark, poetry defaults): #{plan.pins.keys.sort.join(", ")}" if plan.pins.any?
259
+ if plan.contrast.any?
260
+ out << " contrast (merged set, AA floor):"
261
+ plan.contrast.each { |result| out << " #{result}" }
262
+ end
263
+ if plan.dropped.any?
264
+ out << " dropped (never guessed):"
265
+ plan.dropped.each { |drop| out << " #{drop.name}: #{drop.reason}" }
266
+ end
267
+ out << " #{plan.typography_note}" if plan.typography_note
268
+ out << if written
269
+ "wrote #{written.relative_path_from(Rails.root)}"
270
+ else
271
+ "no overrides written (nothing importable survived the gate)"
272
+ end
273
+ out.join("\n")
274
+ end
275
+
276
+ def poetry_design_report_json(plan, written)
277
+ {
278
+ "applied" => plan.applied.map do |a|
279
+ { "role" => a.role, "from" => a.from, "mode" => a.mode, "css" => a.color.css }
280
+ end,
281
+ "pins" => plan.pins.transform_values(&:css),
282
+ "radius" => plan.radius,
283
+ "contrast" => plan.contrast.map do |r|
284
+ { "mode" => r.mode, "pair" => r.label, "ratio" => r.ratio.round(2), "pass" => r.pass,
285
+ "suggestion" => r.suggestion, "shipped_failing" => r.shipped }.compact
286
+ end,
287
+ "dropped" => plan.dropped.map do |d|
288
+ { "name" => d.name, "value" => d.value, "mode" => d.mode, "reason" => d.reason }.compact
289
+ end,
290
+ "typography_note" => plan.typography_note,
291
+ "written" => written&.relative_path_from(Rails.root)&.to_s
292
+ }.compact
293
+ end