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,56 @@
1
+ # paper - bring a paper.design canvas into poetry
2
+
3
+ Paper's canvas is real HTML + CSS, and its tokens are CSS custom properties
4
+ on the same axes poetry themes use, so it speaks poetry's dialect directly.
5
+ Two doors, independent - a theme handoff, and a canvas-to-components handoff.
6
+ Translate INTO poetry primitives and tokens; never transcribe raw styles.
7
+
8
+ ## Door 1: theme -> an AA-gated theme (no MCP needed)
9
+
10
+ 1. In Paper, run **Copy theme** and paste the CSS into a file (e.g.
11
+ `paper-theme.css`).
12
+ 2. Run `bin/rails poetry:paper:import[paper-theme.css]` (or
13
+ `poetry:design:import` - it detects `.css`).
14
+ 3. **Read the report.** The importer parses the `:root` / `@theme` custom
15
+ properties (stripping a Tailwind `color-` namespace), resolves `var()`
16
+ references, converts values into poetry's OKLCH model, then measures every
17
+ touched pair against WCAG AA. A value that fails AA is **dropped and
18
+ reported with the nearest passing suggestion** - never shipped; a property
19
+ with no poetry role is dropped and listed, never guessed.
20
+ 4. It writes `app/assets/tailwind/poetry/design-overrides.css` and wires the
21
+ import. Rebuild CSS. `POETRY_DESIGN_FORCE=1` / `POETRY_DESIGN_JSON=1` as
22
+ with any import.
23
+
24
+ Copy theme is a one-shot copy, not a live sync - re-run the import when the
25
+ Paper theme changes.
26
+
27
+ ## Door 2: a canvas -> components (dual-MCP)
28
+
29
+ Needs the **Paper MCP** connected alongside the poetry MCP. Paper's MCP ships
30
+ with the Paper Desktop app and runs locally once a file is open; register it
31
+ once: `claude mcp add paper --transport http http://127.0.0.1:29979/mcp
32
+ --scope user`. The free tier's 100 tool calls/week is enough for a handoff.
33
+
34
+ Flow:
35
+
36
+ 1. Read the design - `get_selection` / `get_tree_summary` for structure,
37
+ `get_jsx` (ask for the Tailwind form) and `get_computed_styles` for the
38
+ styling, `get_screenshot` to keep layout fidelity.
39
+ 2. Land the tokens first via Door 1 so Paper's variables line up with poetry
40
+ theme tokens.
41
+ 3. Translate, do not copy. Map each Paper subtree to the nearest poetry
42
+ primitive - consult `list_components` / `describe_component`; assemble with
43
+ `compose` / `build_page`, never hand-authored markup.
44
+ 4. Run poetry `check` LAST - a PASS verdict, not an eyeball.
45
+
46
+ Mapping guide:
47
+
48
+ - Artboard -> a poetry block or page.
49
+ - Node subtree -> the nearest poetry component.
50
+ - Tailwind classes from `get_jsx` -> poetry variants and theme tokens, not
51
+ copied class strings.
52
+ - Computed styles -> poetry theme tokens (Door 1), never per-instance CSS.
53
+
54
+ Optional round-trip: push a rendered poetry component back into Paper as
55
+ editable layers with `write_html`, so a designer can review poetry's output
56
+ on the canvas. Bank this as a review loop, not a build step.
@@ -0,0 +1,47 @@
1
+ # study - extract a brand into a DESIGN.md poetry can import
2
+
3
+ `study` turns a brand surface (a URL, screenshots, an existing style guide)
4
+ into the design-skill ecosystem's shared artifact: a DESIGN.md. poetry both
5
+ writes and reads the format, so the loop is:
6
+
7
+ study the brand -> DESIGN.md -> bin/rails "poetry:design:import[DESIGN.md]"
8
+
9
+ ## Writing the DESIGN.md
10
+
11
+ Front-matter carries the machine-readable part:
12
+
13
+ - Flat `colors:` = the LIGHT mode palette, role-named (`brand`, `surface`,
14
+ `text`, `accent`, ...). poetry's importer maps roles conservatively
15
+ (`brand` -> `--primary`, `surface` -> `--card`, `text` -> `--foreground`,
16
+ Material-style `on-*` -> `*-foreground`) and reports every mapping.
17
+ - `modes.dark:` for dark values when the brand defines them. A light-only
18
+ study is fine: poetry PINS dark to its shipped defaults rather than
19
+ letting light values bleed into dark mode.
20
+ - `rounded:` resolved to px; typography as metadata (family names, pairing
21
+ intent) - fonts never enter CSS via import.
22
+ - Markdown sections (voice, imagery, spacing notes) carry the prose the
23
+ front-matter can't; the importer ignores what it can't parse and REPORTS
24
+ it rather than guessing.
25
+
26
+ **Extract, never fabricate.** Record only what the brand actually shows.
27
+ If the brand has no dark mode, don't invent one; if you can't find a
28
+ motion language, omit the section. Dropped-with-a-note beats guessed - the
29
+ import report is the honest ledger.
30
+
31
+ ## What import will do with it
32
+
33
+ Role-mapped token overrides, WCAG AA enforced on the merged set: failing
34
+ pairs are DROPPED with a deterministic nearest-AA suggestion (an OKLCH
35
+ lightness walk, chroma held), and `POETRY_DESIGN_FORCE=1` is the only way
36
+ a failing pair ships. Expect the report to move some values - a brand hex
37
+ that fails AA against white comes back as the nearest passing neighbor,
38
+ which is usually what the brand's own accessible surfaces do anyway.
39
+
40
+ ## redesign - same content, different fingerprint
41
+
42
+ `redesign` keeps copy, information architecture, and brand meaning, and
43
+ swaps the visual fingerprint: pick a different shipped theme
44
+ (`references/theme.md`), re-run the brand's DESIGN.md on top if one
45
+ exists, and re-audit. Nothing in the templates should need to change - if
46
+ a redesign forces template edits, the original leaned on per-instance
47
+ styling it shouldn't have had.
@@ -0,0 +1,70 @@
1
+ # theme - pick, adapt, or import a visual fingerprint
2
+
3
+ One visual theme per app, chosen at install: `bin/rails g poetry:install
4
+ --theme <name>` (re-runnable; the chosen fragment's bytes land in
5
+ `app/assets/tailwind/poetry/style-default.css`). Components read tokens and
6
+ `cn-*` names; the theme owns every visual decision, so switching themes is
7
+ a reinstall, not a refactor.
8
+
9
+ ## The nine shipped themes
10
+
11
+ | Theme | Character |
12
+ |---|---|
13
+ | `default` | poetry's original look - the new-york-v4 lineage the library was built against |
14
+ | `vega` | closest to default: ring-based surfaces, subtle blur scrims, xl radii |
15
+ | `nova` | compact - tighter paddings and smaller surfaces |
16
+ | `mira` | compact and quiet - relaxed xs body text, heavily dimmed drawer scrims |
17
+ | `rhea` | soft and elevated - capped 4xl radii, shadow-xl, blurred scrims |
18
+ | `maia` | fully rounded - 4xl surfaces, deeply dimmed scrims |
19
+ | `luma` | soft-round - blurred scrims and a floating drawer frame |
20
+ | `lyra` | boxy and sharp - radius zero, type one step down, made for mono fonts |
21
+ | `sera` | editorial - uppercase tracked type, underline-only fields, serif pairing |
22
+
23
+ Choosing by brief, not by taste-in-a-vacuum:
24
+
25
+ - Dense operational tools (admin, monitoring, tables everywhere): `nova` or
26
+ `mira` - the compact surfaces buy rows.
27
+ - Consumer/marketing warmth: `rhea`, `maia`, or `luma` - rounded, elevated,
28
+ soft scrims.
29
+ - Technical/developer products: `lyra` - sharp, mono-biased, zero radius.
30
+ - Editorial/content products: `sera` - tracked uppercase labels, serif
31
+ pairing, underline fields.
32
+ - `default` is the stock look. Picking it is legitimate but should be a
33
+ DECISION - when a brief implies any character at all, a non-default theme
34
+ is usually the stronger answer (the design-lint `stock-theme-nudge` rule
35
+ exists for exactly this).
36
+
37
+ ## Fonts ride the theme as metadata
38
+
39
+ No poetry theme moves a font token - `lyra` is biased hard to mono
40
+ (JetBrains Mono; radius forced to zero) and `sera` keeps serif company
41
+ (Noto Serif, Instrument Serif), but both express their typography through
42
+ size, weight, tracking, and uppercase utilities. The font-family itself is
43
+ the app's call: add the family in the app's own CSS/font pipeline when
44
+ installing lyra or sera.
45
+
46
+ ## Status color vocabulary
47
+
48
+ The token set carries `--success`, `--warning`, and `--info` alongside
49
+ `--destructive` (all four AA-managed in both modes). Badges ship soft
50
+ status variants on them. Use the set; never invent status colors from
51
+ palette utilities.
52
+
53
+ ## The DESIGN.md door (brand import/export)
54
+
55
+ - `bin/rails poetry:design:export` writes this app's DESIGN.md (tokens +
56
+ treatment; `POETRY_DESIGN_PATH` to target elsewhere, `POETRY_DESIGN_FORCE=1`
57
+ to overwrite a non-poetry file). Run it so external design skills can read
58
+ the app's current design.
59
+ - `bin/rails "poetry:design:import[path/to/DESIGN.md]"` plans role-mapped
60
+ token overrides from a foreign DESIGN.md (a brand study, or an export
61
+ from another design tool): conservative role aliases, WCAG AA enforced on the
62
+ MERGED set, failing pairs dropped with a deterministic nearest-AA
63
+ suggestion, dark values pinned when the source is light-only. The report
64
+ lists every mapping and every drop (`POETRY_DESIGN_JSON=1` for JSON);
65
+ `POETRY_DESIGN_FORCE=1` is the only way a failing pair ships.
66
+ - Overrides land in `app/assets/tailwind/poetry/design-overrides.css` - the
67
+ one sanctioned place brand values live outside the theme fragment.
68
+
69
+ Adapting a theme = shipped theme + DESIGN.md overrides on top. Full brand
70
+ work = `study` (see `references/study.md`) then import.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Generators
5
+ # The Claude Code skills, shared by poetry:install and the standalone
6
+ # poetry:skill (the refresh path - re-run after updating poetry gems):
7
+ #
8
+ # .claude/skills/poetry/ GENERATED from the live registry
9
+ # .claude/skills/poetry-design/ curated taste layer (static templates)
10
+ # .claude/skills/poetry-component/ authoring layer: anatomy, docs
11
+ # standard, audit checklist (static)
12
+ #
13
+ # Same Thor note as AgentsSection: only methods on the generator class
14
+ # register as steps, so each generator declares its own public step and
15
+ # calls apply_poetry_skills.
16
+ #
17
+ # @api private
18
+ module SkillsSection
19
+ DESIGN_TEMPLATES = File.expand_path("skill/templates/poetry-design", __dir__)
20
+ COMPONENT_TEMPLATES = File.expand_path("skill/templates/poetry-component", __dir__)
21
+
22
+ # Writes or refreshes the three skill directories under
23
+ # .claude/skills/.
24
+ def apply_poetry_skills
25
+ Poetry::Ui.skill_files.each do |relative, content|
26
+ create_file ".claude/skills/poetry/#{relative}", content
27
+ end
28
+ design_skill_files.each do |relative, content|
29
+ create_file ".claude/skills/poetry-design/#{relative}", content
30
+ end
31
+ component_skill_files.each do |relative, content|
32
+ create_file ".claude/skills/poetry-component/#{relative}", content
33
+ end
34
+ end
35
+
36
+ # Path => content for the curated skills - the eval harness writes
37
+ # these without a Thor context (the agents_section_text pattern:
38
+ # Object.new.extend(SkillsSection).design_skill_files).
39
+ def design_skill_files
40
+ static_skill_files(DESIGN_TEMPLATES)
41
+ end
42
+
43
+ # Path => content for the poetry-component authoring skill (static
44
+ # templates).
45
+ def component_skill_files
46
+ static_skill_files(COMPONENT_TEMPLATES)
47
+ end
48
+
49
+ # Path => content for every .md under a template root.
50
+ def static_skill_files(templates)
51
+ root = Pathname(templates)
52
+ Dir.glob(root.join("**/*.md").to_s).to_h do |file|
53
+ [Pathname(file).relative_path_from(root).to_s, File.read(file)]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # The chat replay DSL: script a user/assistant conversation in
6
+ # Ruby, get a DETERMINISTIC timeline of
7
+ # streaming frames to replay through the real Turbo Stream pipeline -
8
+ # no model, no network, fixed ids, fixed pacing. The DSL is pure data
9
+ # (transcript + timing); rendering frames into Message rows is the
10
+ # consumer's job (the docs replay demo is the reference consumer).
11
+ #
12
+ # A tool with `approval: true` PAUSES its segment after the input frame;
13
+ # the frames that follow (its output - or denial - and later parts)
14
+ # belong to the continuation and are produced by
15
+ # `continuation_frames(approved:)` - the human-in-the-loop
16
+ # model, server-shaped: the pause is a rendered form, the continuation
17
+ # is the stream after the decision.
18
+ #
19
+ # @example Scripting a turn with a tool call
20
+ # script = Poetry::Ui::Chat.script do
21
+ # user "What's the weather in Tokyo?"
22
+ # assistant do |w|
23
+ # w.reasoning "Check the weather tool first."
24
+ # w.tool("getWeather", input: { city: "Tokyo" }, sleep_ms: 800,
25
+ # output: { temp: 21, condition: "clear" })
26
+ # w.text "Clear skies and 21°C in Tokyo."
27
+ # end
28
+ # end
29
+ # script.segments # => enumerable segments; assistant segments carry
30
+ # # frames (accumulated part states + sleeps + versions)
31
+ module Chat
32
+ # Builds a Script from the block - instance_eval'd, so bare `user`
33
+ # and `assistant` calls declare the segments in transcript order.
34
+ #
35
+ # @return [Script] the compiled, deterministic script
36
+ def self.script(&)
37
+ Script.new(&)
38
+ end
39
+
40
+ # One rendered state of an assistant row mid-stream.
41
+ Frame = Struct.new(:parts, :sleep_ms, :version, keyword_init: true)
42
+
43
+ # A step of the conversation: :user (text) or :assistant (frames,
44
+ # possibly pausing for approval).
45
+ class Segment
46
+ # kind (:user or :assistant), the deterministic DOM id, the user
47
+ # text, and the AssistantTurn (assistant segments only).
48
+ attr_reader :kind, :id, :text, :turn
49
+
50
+ def initialize(kind:, id:, text: nil, turn: nil)
51
+ @kind = kind
52
+ @id = id
53
+ @text = text
54
+ @turn = turn
55
+ end
56
+
57
+ # Whether this segment stops at an approval pause.
58
+ #
59
+ # @return [Boolean]
60
+ def pause? = kind == :assistant && turn.pause?
61
+
62
+ # The frame timeline up to (and including) an approval pause, or
63
+ # the whole turn when nothing pauses.
64
+ #
65
+ # @return [Array<Frame>]
66
+ def frames = turn.frames
67
+
68
+ # The frames after the approval pause, resolved by the decision.
69
+ #
70
+ # @param approved [Boolean] the human decision at the pause
71
+ # @return [Array<Frame>]
72
+ def continuation_frames(approved:) = turn.continuation_frames(approved: approved)
73
+
74
+ # The final resting parts (approval segments resolve per decision).
75
+ #
76
+ # @return [Array<Hash>] the part list as last rendered
77
+ def final_parts(approved: true)
78
+ return [{ kind: :text, text: text }] if kind == :user
79
+ return frames.last.parts unless pause?
80
+
81
+ continuation_frames(approved: approved).last.parts
82
+ end
83
+ end
84
+
85
+ # The DSL root: collects user/assistant segments in scripted order
86
+ # and assigns each a deterministic id.
87
+ class Script
88
+ # Per-chunk streaming delay (ms) when a text part names none.
89
+ DEFAULT_TEXT_DELAY_MS = 30
90
+ # Words per streamed text chunk in the compiled frames.
91
+ TEXT_CHUNK_WORDS = 3
92
+
93
+ def initialize(&)
94
+ @segments = []
95
+ @counter = 0
96
+ instance_eval(&)
97
+ end
98
+
99
+ # Declares a user message at this point in the transcript.
100
+ #
101
+ # @param text [String] the user's message
102
+ def user(text)
103
+ @segments << Segment.new(kind: :user, id: next_id, text: text)
104
+ end
105
+
106
+ # Declares an assistant turn: pass plain text, or take the block
107
+ # form and compose text / reasoning / tool parts in order.
108
+ #
109
+ # @param text [String, nil] shortcut for a single text part
110
+ # @yieldparam writer [Writer] appends parts to the turn
111
+ def assistant(text = nil, &block)
112
+ turn = AssistantTurn.new
113
+ if block
114
+ yield(turn.writer)
115
+ elsif text
116
+ turn.writer.text(text)
117
+ end
118
+ @segments << Segment.new(kind: :assistant, id: next_id, turn: turn)
119
+ end
120
+
121
+ # @return [Array<Segment>] the declared segments, in scripted order
122
+ attr_reader :segments
123
+
124
+ private
125
+
126
+ def next_id
127
+ @counter += 1
128
+ "chat-msg-#{@counter}"
129
+ end
130
+ end
131
+
132
+ # Collects writer calls into an ordered part list, then compiles the
133
+ # deterministic frame timeline.
134
+ class AssistantTurn
135
+ def initialize
136
+ @parts = []
137
+ end
138
+
139
+ # The Writer collecting this turn's parts (memoized - `assistant`
140
+ # yields it).
141
+ #
142
+ # @return [Writer]
143
+ def writer = @writer ||= Writer.new(@parts)
144
+
145
+ # Whether any tool part requests approval (pausing the segment).
146
+ #
147
+ # @return [Boolean]
148
+ def pause? = @parts.any? { |part| part[:kind] == :tool && part[:approval] }
149
+
150
+ # Frames up to (and including) the pause, or the whole turn.
151
+ #
152
+ # @return [Array<Frame>]
153
+ def frames
154
+ compile(@parts.take_while.with_index { |_part, i| i.zero? || !pause_before?(i) })
155
+ end
156
+
157
+ # Frames AFTER the pause point, resolved by the decision. Versions
158
+ # continue from the paused prefix so the morph guard stays monotonic.
159
+ # Parts scripted AFTER an approval tool are the APPROVED path
160
+ # (denial streams tool-output-denied and stops;
161
+ # the approved branch carries the follow-through). So a denied
162
+ # continuation is exactly the resolution frame.
163
+ #
164
+ # @param approved [Boolean] the human decision at the pause
165
+ # @return [Array<Frame>]
166
+ def continuation_frames(approved:)
167
+ raise Error, "turn has no approval pause" unless pause?
168
+
169
+ resolved = @parts.map do |part|
170
+ next part unless part[:kind] == :tool && part[:approval]
171
+
172
+ part.merge(state: approved ? :done : :denied,
173
+ output: approved ? part[:output] : nil)
174
+ end
175
+ keep = approved ? resolved : resolved.take(pause_index + 1)
176
+ compile(keep).drop(frames.length)
177
+ end
178
+
179
+ private
180
+
181
+ def pause_index
182
+ @parts.index { |part| part[:kind] == :tool && part[:approval] }
183
+ end
184
+
185
+ def pause_before?(index)
186
+ before = @parts.first(index)
187
+ before.any? { |part| part[:kind] == :tool && part[:approval] }
188
+ end
189
+
190
+ # The accumulated-state timeline: every frame is the FULL part list
191
+ # as rendered at that instant (streaming morphs the same row).
192
+ def compile(parts)
193
+ frames = []
194
+ acc = []
195
+ version = 0
196
+ push = lambda do |sleep_ms|
197
+ version += 1
198
+ frames << Frame.new(parts: acc.map(&:dup), sleep_ms: sleep_ms, version: version)
199
+ end
200
+
201
+ parts.each do |part|
202
+ case part[:kind]
203
+ when :reasoning, :text
204
+ chunks = part[:text].split(/(?<=\S)\s+/).each_slice(Script::TEXT_CHUNK_WORDS).map { |w| w.join(" ") }
205
+ delay = part.fetch(:delay_ms, Script::DEFAULT_TEXT_DELAY_MS) || Script::DEFAULT_TEXT_DELAY_MS
206
+ acc << part.merge(text: "")
207
+ chunks.each do |chunk|
208
+ acc[-1] = acc[-1].merge(text: [acc[-1][:text], chunk].reject(&:empty?).join(" "))
209
+ push.call(delay)
210
+ end
211
+ when :tool
212
+ acc << part.merge(state: :loading, output: nil)
213
+ push.call(0)
214
+ # An approval tool always passes through :awaiting_approval -
215
+ # resolved or not - so the paused prefix and the resolved
216
+ # timeline share frame counts up to the pause and the
217
+ # continuation begins AT the resolution frame (a denial with
218
+ # nothing after it still streams its denial).
219
+ if part[:approval]
220
+ acc[-1] = acc[-1].merge(state: :awaiting_approval)
221
+ push.call(part.fetch(:sleep_ms, 0))
222
+ end
223
+ unless part[:approval] && part[:state].nil?
224
+ acc[-1] = acc[-1].merge(state: part[:state] || :done, output: part[:output])
225
+ push.call(part.fetch(:sleep_ms, 0))
226
+ end
227
+ end
228
+ end
229
+ frames
230
+ end
231
+ end
232
+
233
+ # The builder handed to `assistant do |w| ... end` - each call
234
+ # appends one part (text / reasoning / tool) to the turn, in order.
235
+ class Writer
236
+ def initialize(parts)
237
+ @parts = parts
238
+ end
239
+
240
+ # Appends a streamed text part.
241
+ #
242
+ # @param text [String] the assistant prose
243
+ # @param delay_ms [Integer, nil] per-chunk delay override (defaults
244
+ # to Script::DEFAULT_TEXT_DELAY_MS)
245
+ # @return [Writer] self, for chaining
246
+ def text(text, delay_ms: nil)
247
+ @parts << { kind: :text, text: text, delay_ms: delay_ms }.compact
248
+ self
249
+ end
250
+
251
+ # Appends a reasoning part - streamed like text, rendered as the
252
+ # collapsible thinking block.
253
+ #
254
+ # @param text [String] the reasoning prose
255
+ # @param delay_ms [Integer, nil] per-chunk delay override
256
+ # @return [Writer] self, for chaining
257
+ def reasoning(text, delay_ms: nil)
258
+ @parts << { kind: :reasoning, text: text, delay_ms: delay_ms }.compact
259
+ self
260
+ end
261
+
262
+ # Appends a tool-call part: the input frame streams first (state
263
+ # :loading), then after sleep_ms the resolution frame carries the
264
+ # output. approval: true pauses the segment at the input frame for
265
+ # a human decision; denied: true scripts the denied resolution.
266
+ #
267
+ # @param name [String] the tool name as rendered
268
+ # @param input [Hash] the tool input payload
269
+ # @param output [Object, nil] the result shown on resolution
270
+ # @param sleep_ms [Integer] thinking time before the resolution
271
+ # @param approval [Boolean] pause for a human decision after input
272
+ # @param denied [Boolean] script the denied resolution
273
+ # @return [Writer] self, for chaining # -- the writer vocabulary mirrors the tool part's options
274
+ def tool(name, input:, output: nil, sleep_ms: 0, approval: false, denied: false)
275
+ @parts << { kind: :tool, name: name, input: input, output: output,
276
+ sleep_ms: sleep_ms, approval: approval,
277
+ state: denied ? :denied : nil }
278
+ self
279
+ end
280
+ end
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # The CodeBlock's rouge seam: highlighting is a SOFT capability -
6
+ # `gem "rouge"` in the host Gemfile turns it on; without it the component
7
+ # renders the plain escaped code unchanged (the fallback is simply
8
+ # "no rouge", and nothing shifts because the markup shape is identical).
9
+ module CodeBlockHighlighter
10
+ module_function
11
+
12
+ # Whether rouge is loadable in this host (memoized).
13
+ #
14
+ # @return [Boolean]
15
+ def available?
16
+ return @available if defined?(@available)
17
+
18
+ @available = begin
19
+ require "rouge"
20
+ true
21
+ rescue LoadError
22
+ false
23
+ end
24
+ end
25
+
26
+ # -> html_safe highlighted markup - every line wrapped in .line (the
27
+ # counter hook), the requested ones also .hll (the theme's tint hook) -
28
+ # or nil when rouge is absent. Unknown languages lex as plain text.
29
+ def highlight(code, language:, highlight_lines: [])
30
+ return nil unless available?
31
+
32
+ lexer = Rouge::Lexer.find(language.to_s) || Rouge::Lexers::PlainText.new
33
+ formatter.new(highlight_lines: Array(highlight_lines).map(&:to_i))
34
+ .format(lexer.lex(code)).html_safe
35
+ end
36
+
37
+ # Rouge 5 asserts a PLAIN HTML delegate inside its line-wise wrappers
38
+ # (HTMLLinewise cannot wrap HTMLLineHighlighter), so poetry carries its
39
+ # own line formatter over the stable token_lines API.
40
+ def formatter
41
+ @formatter ||= Class.new(Rouge::Formatters::HTML) do
42
+ def initialize(highlight_lines: [])
43
+ super()
44
+ @highlight_lines = highlight_lines
45
+ end
46
+
47
+ def stream(tokens)
48
+ token_lines(tokens).each.with_index(1) do |line_tokens, lineno|
49
+ classes = ["line"]
50
+ classes << "hll" if @highlight_lines.include?(lineno)
51
+ yield %(<span class="#{classes.join(" ")}">)
52
+ line_tokens.each { |token, value| yield span(token, value) }
53
+ yield "</span>\n"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module Poetry
6
+ module Ui
7
+ # The Rails engine: wires the component classes, the poetry_* view
8
+ # helpers, the Turbo Stream toast action, and the preview paths into
9
+ # the host app.
10
+ class Engine < ::Rails::Engine
11
+ # Standard engine layout: app/components, app/helpers, and
12
+ # config/locales are picked up by Rails' engine path conventions.
13
+ config.autoload_paths << "#{Poetry::Ui.root}/app/components"
14
+ config.eager_load_paths << "#{Poetry::Ui.root}/app/components"
15
+
16
+ # ComponentsHelper lives in app/helpers (Zeitwerk-autoloaded), so unlike
17
+ # a lib/ helper it is NOT already loaded at gem-require time. A host gem
18
+ # that forces ActionView to load *during* initialization - e.g. an
19
+ # ActionText editor prepending to ActionView::Helpers::FormHelper
20
+ # - fires this on_load hook before the engine's
21
+ # app/helpers constant is resolvable, raising NameError at boot. Deferring
22
+ # the hook registration into a to_prepare block runs it only once autoload
23
+ # paths are wired (and re-runs safely on reload), so the constant always
24
+ # resolves. (poetry-core's TagHelper avoids this only because it lives in
25
+ # lib/ and is required eagerly.)
26
+ initializer "poetry_ui.helpers" do |app|
27
+ app.config.to_prepare do
28
+ ActiveSupport.on_load(:action_view) do
29
+ include Poetry::Ui::ComponentsHelper
30
+ end
31
+ end
32
+ end
33
+
34
+ # turbo_stream.poetry_toast(...) - the canonical server-side toast.
35
+ # poetry-ui does not depend on turbo-rails; hosts that load it get
36
+ # the action through turbo's own load hook (a no-op otherwise).
37
+ initializer "poetry_ui.turbo_stream_actions" do
38
+ ActiveSupport.on_load(:turbo_streams_tag_builder) do
39
+ include Poetry::Ui::ToastStreamActions
40
+ end
41
+ end
42
+
43
+ initializer "poetry_ui.previews" do |app|
44
+ app.config.view_component.previews.paths << "#{Poetry::Ui.root}/app/components"
45
+ end
46
+
47
+ # Lookbook is a dev-only dependency; guard so the engine never crashes
48
+ # a production (or lean test) host that does not load it (the
49
+ # poetry-core pattern).
50
+ initializer "poetry_ui.setup_lookbook" do |app|
51
+ app.config.lookbook.preview_paths << "#{Poetry::Ui.root}/app/components" if defined?(Lookbook)
52
+ end
53
+ end
54
+ end
55
+ end