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,1794 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # The `poetry_*` view helpers - the agent-facing surface ("use
6
+ # poetry_button, never a raw <button> with hand-written Tailwind").
7
+ # Every registered component ships one (drift-gated by
8
+ # ComponentsHelperTest); slot blocks receive the component:
9
+ #
10
+ # <%= poetry_dialog do |dialog| %>
11
+ # <% dialog.with_trigger { "Open" } %>
12
+ # <% dialog.with_title { "Settings" } %>
13
+ # <% end %>
14
+ module ComponentsHelper
15
+ # Triggers an action or event, such as submitting a form or opening a
16
+ # dialog.
17
+ #
18
+ # @example
19
+ # poetry_button(variant: :default) { "Save" }
20
+ # @see Poetry::Ui::Button::Component
21
+ def poetry_button(**, &)
22
+ render(Poetry::Ui::Button::Component.new(**), &)
23
+ end
24
+
25
+ # Renders an inline SVG icon from the icon set.
26
+ #
27
+ # @example A decorative icon inside a labeled control
28
+ # poetry_icon(name: :plus)
29
+ # @see Poetry::Ui::Icon::Component
30
+ def poetry_icon(**)
31
+ render(Poetry::Ui::Icon::Component.new(**))
32
+ end
33
+
34
+ # A styled navigational hyperlink.
35
+ #
36
+ # @example
37
+ # poetry_link(href: "/docs") { "Documentation" }
38
+ # @see Poetry::Ui::Link::Component
39
+ def poetry_link(**, &)
40
+ render(Poetry::Ui::Link::Component.new(**), &)
41
+ end
42
+
43
+ # A small count or status descriptor.
44
+ #
45
+ # @example A soft status pill
46
+ # poetry_badge(variant: :success) { "Fulfilled" }
47
+ # @see Poetry::Ui::Badge::Component
48
+ def poetry_badge(**, &)
49
+ render(Poetry::Ui::Badge::Component.new(**), &)
50
+ end
51
+
52
+ # A single KPI: a muted label over a large metric value.
53
+ #
54
+ # @example Revenue KPI with a delta
55
+ # poetry_stat(label: "Revenue", delta: "+12.5%", trend: :up) do
56
+ # "$45,231"
57
+ # end
58
+ # @see Poetry::Ui::Stat::Component
59
+ def poetry_stat(**, &)
60
+ render(Poetry::Ui::Stat::Component.new(**), &)
61
+ end
62
+
63
+ # A key-value list for labeled attributes on detail pages.
64
+ #
65
+ # @example A record's fact sheet
66
+ # poetry_metadata_list(columns: :two) do |list|
67
+ # list.with_item(label: "Status") { "Active" }
68
+ # list.with_item(label: "Owner") { "Ada Lovelace" }
69
+ # end
70
+ # @see Poetry::Ui::MetadataList::Component
71
+ def poetry_metadata_list(**, &)
72
+ render(Poetry::Ui::MetadataList::Component.new(**), &)
73
+ end
74
+
75
+ # A sequence of dated events as an ordered list.
76
+ #
77
+ # @example
78
+ # poetry_timeline do |timeline|
79
+ # timeline.with_item(title: "Order placed", time: "Mar 15", completed: true)
80
+ # timeline.with_item(title: "In transit") { "Estimated delivery Thursday." }
81
+ # end
82
+ # @see Poetry::Ui::Timeline::Component
83
+ def poetry_timeline(**, &)
84
+ render(Poetry::Ui::Timeline::Component.new(**), &)
85
+ end
86
+
87
+ # A horizontal group of controls that acts as one keyboard tab stop -
88
+ # Tab passes over the group, Arrow keys move between its controls.
89
+ #
90
+ # @example
91
+ # poetry_toolbar(label: "Bulk actions") do |toolbar|
92
+ # toolbar.with_button(variant: :outline) { "Archive" }
93
+ # toolbar.with_separator
94
+ # toolbar.with_input(name: "q", placeholder: "Filter…")
95
+ # end
96
+ # @see Poetry::Ui::Toolbar::Component
97
+ def poetry_toolbar(**, &)
98
+ render(Poetry::Ui::Toolbar::Component.new(**), &)
99
+ end
100
+
101
+ # Prose styling for long-form and rendered-markdown content.
102
+ #
103
+ # @example
104
+ # poetry_typeset(preset: "docs") do
105
+ # @article_html
106
+ # end
107
+ # @see Poetry::Ui::Typeset::Component
108
+ def poetry_typeset(**, &)
109
+ render(Poetry::Ui::Typeset::Component.new(**), &)
110
+ end
111
+
112
+ # A control for selecting, previewing, and removing files to upload.
113
+ #
114
+ # @example Drag-and-drop upload surface
115
+ # poetry_file_input(
116
+ # variant: :dropzone, name: "attachments[]", multiple: true,
117
+ # hint: "PDF or PNG, up to 10 MB"
118
+ # )
119
+ # @see Poetry::Ui::FileInput::Component
120
+ def poetry_file_input(**, &)
121
+ render(Poetry::Ui::FileInput::Component.new(**), &)
122
+ end
123
+
124
+ # A callout that highlights an important inline message.
125
+ #
126
+ # @example Destructive alert with a title
127
+ # poetry_alert(variant: :destructive) do |alert|
128
+ # alert.with_title { "Payment failed" }
129
+ # "Your card was declined. Update your billing details."
130
+ # end
131
+ # @see Poetry::Ui::Alert::Component
132
+ def poetry_alert(**, &)
133
+ render(Poetry::Ui::Alert::Component.new(**), &)
134
+ end
135
+
136
+ # A container that groups related content and actions.
137
+ #
138
+ # @example
139
+ # poetry_card do |card|
140
+ # card.with_title { "Team" }
141
+ # card.with_description { "Invite and manage members." }
142
+ # "Body content"
143
+ # end
144
+ # @see Poetry::Ui::Card::Component
145
+ def poetry_card(**, &)
146
+ render(Poetry::Ui::Card::Component.new(**), &)
147
+ end
148
+
149
+ # The Table: the component renders the overflow container + real
150
+ # `<table>`; the part helpers stamp the data-slot + source-exact classes
151
+ # onto the semantic table elements the consumer composes.
152
+ #
153
+ # @example
154
+ # poetry_table do
155
+ # safe_join([
156
+ # poetry_table_header { poetry_table_row { poetry_table_head { "Invoice" } } },
157
+ # poetry_table_body { poetry_table_row { poetry_table_cell { "INV001" } } }
158
+ # ])
159
+ # end
160
+ # @see Poetry::Ui::Table::Component
161
+ def poetry_table(**, &)
162
+ render(Poetry::Ui::Table::Component.new(**), &)
163
+ end
164
+
165
+ {
166
+ table_header: [:thead, "table-header", :header],
167
+ table_body: [:tbody, "table-body", :body],
168
+ table_footer: [:tfoot, "table-footer", :footer],
169
+ table_row: [:tr, "table-row", :row],
170
+ table_head: [:th, "table-head", :head],
171
+ table_cell: [:td, "table-cell", :cell],
172
+ table_caption: [:caption, "table-caption", :caption]
173
+ }.each do |name, (tag_name, slot, element)|
174
+ define_method("poetry_#{name}") do |**attrs, &block|
175
+ classes = [Poetry::Ui::Table::Style.css(element), attrs.delete(:class)].compact.join(" ")
176
+ data = { slot: slot }.merge(attrs.delete(:data) || {})
177
+ content_tag(tag_name, (capture(&block) if block), **attrs, class: classes, data: data)
178
+ end
179
+ end
180
+
181
+ # Data-driven pagination: poetry_pagination(current:, total:,
182
+ # path:) - path: is a callable ->(page) { url }; the component owns the
183
+ # truncation math and the accessible nav.
184
+ #
185
+ # @example Paginating a product list
186
+ # poetry_pagination(current: 3, total: 12,
187
+ # path: ->(page) { products_path(page: page) })
188
+ # @see Poetry::Ui::Pagination::Component
189
+ def poetry_pagination(**)
190
+ render(Poetry::Ui::Pagination::Component.new(**))
191
+ end
192
+
193
+ # A pulsing placeholder shown while content loads.
194
+ #
195
+ # @example An avatar-and-line placeholder pair
196
+ # poetry_skeleton(class: "size-10 rounded-full")
197
+ # poetry_skeleton(class: "h-4 w-32")
198
+ # @see Poetry::Ui::Skeleton::Component
199
+ def poetry_skeleton(**, &)
200
+ render(Poetry::Ui::Skeleton::Component.new(**), &)
201
+ end
202
+
203
+ # A thin divider between content, decorative or semantic.
204
+ #
205
+ # @example Horizontal divider between sections
206
+ # poetry_separator
207
+ # @see Poetry::Ui::Separator::Component
208
+ def poetry_separator(**)
209
+ render(Poetry::Ui::Separator::Component.new(**))
210
+ end
211
+
212
+ # An indeterminate loading indicator that announces itself.
213
+ #
214
+ # @example
215
+ # poetry_spinner(label: "Saving...")
216
+ # @see Poetry::Ui::Spinner::Component
217
+ def poetry_spinner(**)
218
+ render(Poetry::Ui::Spinner::Component.new(**))
219
+ end
220
+
221
+ # A deferred region - Turbo loading physics (lazy/eager) plus
222
+ # poetry-owned placeholder and retryable-error states. The block is the
223
+ # placeholder.
224
+ #
225
+ # @example
226
+ # poetry_deferred(src: "/dashboard/activity")
227
+ # @see Poetry::Ui::Deferred::Component
228
+ def poetry_deferred(**, &)
229
+ render(Poetry::Ui::Deferred::Component.new(**), &)
230
+ end
231
+
232
+ # Optimistic UI for a Turbo form: the block's
233
+ # form.optimistic_template authors the PREDICTED state as a
234
+ # turbo-stream in a <template>; the controller paints it on submit
235
+ # and reconciles (morph refresh) only when the server rejects. The
236
+ # server contract - success answers 204 or a targeted stream, NEVER
237
+ # a redirect; failure answers 4xx - and the morph-refresh meta
238
+ # prerequisite live in the Optimistic Forms guide on the docs site.
239
+ # attribute_name:/value: auto-inject the submitted-value hidden field
240
+ # (false survives; form.optimistic_hidden_field places it explicitly).
241
+ #
242
+ # @example
243
+ # <%= poetry_optimistic_form(model: task, attribute_name: :done, value: true) do |form| %>
244
+ # <%= form.optimistic_template do %>
245
+ # <%# the predicted turbo-stream(s) %>
246
+ # <% end %>
247
+ # <%= form.submit "Done" %>
248
+ # <% end %>
249
+ # @see Poetry::Ui::OptimisticFormBuilder
250
+ def poetry_optimistic_form(attribute_name: nil, value: OptimisticFormBuilder::UNSET,
251
+ **options, &)
252
+ options[:builder] = OptimisticFormBuilder
253
+ options[:data] = (options[:data] || {}).dup
254
+ options[:data][:controller] =
255
+ [options[:data][:controller], "poetry--core--optimistic-form"].compact.join(" ")
256
+ optimistic = Poetry::Core::Stimulus::Builder.new("poetry--core--optimistic-form",
257
+ Poetry::Core::HTML::Attributes.new)
258
+ options[:data][:action] = [
259
+ options[:data][:action],
260
+ optimistic.action(:apply, on: "turbo:submit-start"),
261
+ optimistic.action(:reconcile, on: "turbo:submit-end")
262
+ ].compact.join(" ")
263
+
264
+ form_with(**options) do |form|
265
+ body = capture(form, &)
266
+ prefix = if optimistic_auto_inject?(form, attribute_name, value)
267
+ form.optimistic_hidden_field(attribute_name, value: value)
268
+ end
269
+ safe_join([prefix, body].compact)
270
+ end
271
+ end
272
+
273
+ # Declares a form as a WebMCP tool - the declarative registration
274
+ # path: the <form> carries toolname/tooldescription, so a WebMCP
275
+ # browser registers it as an agent-callable tool with NO JavaScript
276
+ # (the parameter schema is synthesized from the controls; each
277
+ # parameter's description comes from its <label>, so the poetry
278
+ # FormBuilder's model-derived labels describe the tool for free;
279
+ # tool_description: on a field overrides one). Defaults the builder
280
+ # to Poetry::Ui::FormBuilder. autosubmit: true is GET-only by
281
+ # construction - a mutating form always keeps the user's Submit.
282
+ #
283
+ # @example A read-only lookup the agent may submit itself
284
+ # <%= poetry_webmcp_form(url: orders_path, method: :get,
285
+ # tool: { name: "find_orders", description: "Search orders by timeframe.",
286
+ # autosubmit: true }) do |form| %>
287
+ # <%= form.field(:timeframe, tool_description: "A relative range such as last_7_days.") %>
288
+ # <% end %>
289
+ # @see Poetry::Ui::Webmcp
290
+ def poetry_webmcp_form(tool:, **options, &)
291
+ options[:builder] ||= FormBuilder
292
+ options[:html] = (options[:html] || {}).merge(Webmcp.form_attributes(tool, method: options[:method]))
293
+ # The poetry-agent form controller answers an agent-invoked submit
294
+ # with the outcome (SubmitEvent.respondWith); without the runtime
295
+ # gem the token is inert and the browser's own submission runs.
296
+ options[:data] = (options[:data] || {}).dup
297
+ options[:data][:controller] = [options[:data][:controller], Webmcp::FORM_CONTROLLER].compact.join(" ")
298
+ form_with(**options, &)
299
+ end
300
+
301
+ # The key text is the content block: poetry_kbd { "⌘" }.
302
+ #
303
+ # @example
304
+ # poetry_kbd { "Esc" }
305
+ # @see Poetry::Ui::Kbd::Component
306
+ def poetry_kbd(**, &)
307
+ render(Poetry::Ui::Kbd::Component.new(**), &)
308
+ end
309
+
310
+ # A run of key chords (upstream KbdGroup): a wrapping <kbd> carrying
311
+ # the themed gap - poetry_kbd children render inside.
312
+ #
313
+ # @example
314
+ # <%= poetry_kbd_group do %>
315
+ # <%= poetry_kbd { "⌘" } %><%= poetry_kbd { "K" } %>
316
+ # <% end %>
317
+ # @see Poetry::Ui::Kbd::Component
318
+ def poetry_kbd_group(**attrs, &)
319
+ classes = ["cn-kbd-group inline-flex items-center", attrs.delete(:class)].compact.join(" ")
320
+ content_tag(:kbd, capture(&), class: classes, "data-slot": "kbd-group", **attrs)
321
+ end
322
+
323
+ # ratio: is a string fraction ("16/9") - Ruby's 16/9 would truncate.
324
+ #
325
+ # @example A 16:9 media box
326
+ # poetry_aspect_ratio(ratio: "16/9") do
327
+ # image_tag "cover.jpg", class: "size-full object-cover"
328
+ # end
329
+ # @see Poetry::Ui::AspectRatio::Component
330
+ def poetry_aspect_ratio(**, &)
331
+ render(Poetry::Ui::AspectRatio::Component.new(**), &)
332
+ end
333
+
334
+ # An empty-state placeholder with an icon, message, and actions.
335
+ #
336
+ # @example Empty collection with a next action
337
+ # poetry_empty do |empty|
338
+ # empty.with_title { "No projects yet" }
339
+ # empty.with_description { "Create your first project to get started." }
340
+ # poetry_button { "New project" }
341
+ # end
342
+ # @see Poetry::Ui::Empty::Component
343
+ def poetry_empty(**, &)
344
+ render(Poetry::Ui::Empty::Component.new(**), &)
345
+ end
346
+
347
+ # The server-driven DataTable: rows/state/path come from the
348
+ # controller (State.from_params with a sortable: whitelist); columns
349
+ # are declared in the block. Sorting/filter/page are URL state.
350
+ #
351
+ # @example A sortable notes table
352
+ # <%= poetry_data_table(rows: @notes, state: state, total: @pages,
353
+ # path: ->(p) { notes_path(**p) }, caption: "Notes") do |t| %>
354
+ # <% t.with_column("Title", key: :title, sortable: true) { |note| note.title } %>
355
+ # <% t.with_column("Created", key: :created_at, sortable: true) { |note| note.created_at.to_date } %>
356
+ # <% end %>
357
+ # @see Poetry::Ui::DataTable::Component
358
+ def poetry_data_table(**, &)
359
+ render(Poetry::Ui::DataTable::Component.new(**), &)
360
+ end
361
+
362
+ # An input that suggests options as you type - the text itself is the
363
+ # value.
364
+ #
365
+ # @example Free text with suggestions
366
+ # poetry_autocomplete(name: "tag", label: "Search tags") do |auto|
367
+ # auto.with_item(label: "feature")
368
+ # auto.with_item(label: "fix")
369
+ # end
370
+ # @see Poetry::Ui::Autocomplete::Component
371
+ def poetry_autocomplete(**, &)
372
+ render(Poetry::Ui::Autocomplete::Component.new(**), &)
373
+ end
374
+
375
+ # The content block is the initials fallback.
376
+ #
377
+ # @example Image with initials fallback
378
+ # poetry_avatar(src: user.avatar_url, label: "Ada Lovelace") { "AL" }
379
+ # @see Poetry::Ui::Avatar::Component
380
+ def poetry_avatar(**, &)
381
+ render(Poetry::Ui::Avatar::Component.new(**), &)
382
+ end
383
+
384
+ # Shows the path to the current page as a trail of links.
385
+ #
386
+ # @example
387
+ # <%= poetry_breadcrumb do |crumb| %>
388
+ # <% crumb.with_item("Home", href: "/") %>
389
+ # <% crumb.with_ellipsis %>
390
+ # <% crumb.with_item("Breadcrumb") %>
391
+ # <% end %>
392
+ # @see Poetry::Ui::Breadcrumb::Component
393
+ def poetry_breadcrumb(**, &)
394
+ render(Poetry::Ui::Breadcrumb::Component.new(**), &)
395
+ end
396
+
397
+ # A determinate progress bar toward task completion.
398
+ #
399
+ # @example
400
+ # poetry_progress(value: 60, label: "Uploading")
401
+ # @see Poetry::Ui::Progress::Component
402
+ def poetry_progress(**)
403
+ render(Poetry::Ui::Progress::Component.new(**))
404
+ end
405
+
406
+ # A generic list row with media, content, and actions.
407
+ #
408
+ # @example
409
+ # poetry_item(variant: :outline) do |item|
410
+ # item.with_title { "Backups" }
411
+ # item.with_description { "Nightly, retained 30 days" }
412
+ # end
413
+ # @see Poetry::Ui::Item::Component
414
+ def poetry_item(**, &)
415
+ render(Poetry::Ui::Item::Component.new(**), &)
416
+ end
417
+
418
+ # Part helpers (the Table pattern): pure class/slot stamps around
419
+ # composed Avatars and Items.
420
+ {
421
+ avatar_group: [Poetry::Ui::Avatar, "avatar-group", :group, {}],
422
+ avatar_group_count: [Poetry::Ui::Avatar, "avatar-group-count", :group_count, {}],
423
+ item_group: [Poetry::Ui::Item, "item-group", :group, { "role" => "list" }]
424
+ }.each do |name, (namespace, slot, element, extra)|
425
+ define_method("poetry_#{name}") do |**attrs, &block|
426
+ classes = [namespace::Style.css(element), attrs.delete(:class)].compact.join(" ")
427
+ data = { slot: slot }.merge(attrs.delete(:data) || {})
428
+ content_tag(:div, (capture(&block) if block), **extra, **attrs, class: classes, data: data)
429
+ end
430
+ end
431
+
432
+ # The row divider inside an item group: the Separator with the
433
+ # item-separator slot + spacing.
434
+ #
435
+ # @example
436
+ # poetry_item_separator
437
+ # @see Poetry::Ui::Item::Component
438
+ def poetry_item_separator(**attrs)
439
+ classes = [Poetry::Ui::Item::Style.css(:separator), attrs.delete(:class)].compact.join(" ")
440
+ render(Poetry::Ui::Separator::Component.new(**attrs, class: classes, "data-slot": "item-separator"))
441
+ end
442
+
443
+ # The Calendar: a server-rendered month grid; name: makes it
444
+ # a form control. poetry--core--calendar adds nav + selection.
445
+ #
446
+ # @example A form-posting date pick
447
+ # poetry_calendar(name: "event[date]", selected: "2026-07-04")
448
+ # @see Poetry::Ui::Calendar::Component
449
+ def poetry_calendar(**, &)
450
+ render(Poetry::Ui::Calendar::Component.new(**), &)
451
+ end
452
+
453
+ # The DatePicker: a field-shaped trigger opening a Calendar in
454
+ # a Popover; name: is the form field.
455
+ #
456
+ # @example
457
+ # poetry_date_picker(
458
+ # name: "due_on", label: "Due date", value: Date.new(2026, 6, 5)
459
+ # )
460
+ # @see Poetry::Ui::DatePicker::Component
461
+ def poetry_date_picker(**, &)
462
+ render(Poetry::Ui::DatePicker::Component.new(**), &)
463
+ end
464
+
465
+ # The Sidebar: the app-shell frame - with_nav is the column,
466
+ # with_inset the page area; poetry--core--sidebar owns the collapse.
467
+ #
468
+ # @example App shell wired to the persisted cookie
469
+ # <%= poetry_sidebar(open: cookies[:sidebar_state] != "false", collapsible: :icon) do |shell| %>
470
+ # <% shell.with_nav do %>
471
+ # <%= poetry_sidebar_group do %>...menu...<% end %>
472
+ # <% end %>
473
+ # <% shell.with_inset do %>
474
+ # <%= poetry_sidebar_trigger %>
475
+ # <main>...page...</main>
476
+ # <% end %>
477
+ # <% end %>
478
+ # @see Poetry::Ui::Sidebar::Component
479
+ def poetry_sidebar(**, &)
480
+ render(Poetry::Ui::Sidebar::Component.new(**), &)
481
+ end
482
+
483
+ # Client-side toast delivery: pressing the trigger clones the
484
+ # addressed <template>'s toast into the toaster region - no server
485
+ # round-trip.
486
+ #
487
+ # @example
488
+ # <%= poetry_toast_trigger(template: "copied-toast") do %>
489
+ # Copy link
490
+ # <% end %>
491
+ # <template id="copied-toast">
492
+ # <%= poetry_toast(duration: 4000) do |toast| %>
493
+ # <% toast.with_title { "Copied" } %>
494
+ # <% end %>
495
+ # </template>
496
+ # @see Poetry::Ui::ToastTrigger::Component
497
+ def poetry_toast_trigger(**, &)
498
+ render(Poetry::Ui::ToastTrigger::Component.new(**), &)
499
+ end
500
+
501
+ # The sidebar collapse toggle (lives in the inset): a ghost icon
502
+ # Button wired to the sidebar controller on the wrapper.
503
+ #
504
+ # @example
505
+ # poetry_sidebar_trigger
506
+ # @see Poetry::Ui::Sidebar::Component
507
+ def poetry_sidebar_trigger(**attrs)
508
+ action = Poetry::Ui::Sidebar::Component.stimulus_action(:toggle, on: :click)
509
+ # A caller data: hash augments the toggle wiring instead of
510
+ # replacing it at the kwargs splat (actions token-join).
511
+ caller_data = attrs.delete(:data) || {}
512
+ data = { slot: "sidebar-trigger", action: action }.merge(caller_data) do |key, wired, caller|
513
+ key == :action ? Poetry::Core::Config.current.stimulus_merger.merge_actions(wired, caller) : caller
514
+ end
515
+ render(Poetry::Ui::Button::Component.new(
516
+ variant: :ghost, size: :"icon-sm", label: "Toggle Sidebar",
517
+ data: data, **attrs
518
+ )) { poetry_icon(name: :"panel-left") }
519
+ end
520
+
521
+ # The rail: an edge strip that toggles the sidebar (tabindex -1 - the
522
+ # trigger is the keyboard affordance).
523
+ #
524
+ # @example
525
+ # poetry_sidebar_rail
526
+ # @see Poetry::Ui::Sidebar::Component
527
+ def poetry_sidebar_rail(**attrs)
528
+ classes = [Poetry::Ui::Sidebar::Style.css(:rail), attrs.delete(:class)].compact.join(" ")
529
+ wiring = { type: "button", "aria-label": "Toggle Sidebar", tabindex: "-1",
530
+ title: "Toggle Sidebar", class: classes,
531
+ data: { slot: "sidebar-rail",
532
+ action: Poetry::Ui::Sidebar::Component.stimulus_action(:toggle, on: :click) } }
533
+ content_tag(:button, nil, Poetry::Core::HTML::Attributes.merged(wiring, attrs))
534
+ end
535
+
536
+ # The simple content-part stamps (div/ul/li wrappers with the slot +
537
+ # dictionary classes).
538
+ {
539
+ sidebar_header: [:div, "sidebar-header", :header],
540
+ sidebar_footer: [:div, "sidebar-footer", :footer],
541
+ sidebar_content: [:div, "sidebar-content", :content],
542
+ sidebar_group: [:div, "sidebar-group", :group],
543
+ sidebar_group_content: [:div, "sidebar-group-content", :group_content],
544
+ sidebar_menu: [:ul, "sidebar-menu", :menu],
545
+ sidebar_menu_item: [:li, "sidebar-menu-item", :menu_item],
546
+ sidebar_menu_sub: [:ul, "sidebar-menu-sub", :menu_sub],
547
+ sidebar_menu_sub_item: [:li, "sidebar-menu-sub-item", :menu_sub_item]
548
+ }.each do |name, (tag_name, slot, element)|
549
+ define_method("poetry_#{name}") do |**attrs, &block|
550
+ classes = [Poetry::Ui::Sidebar::Style.css(element), attrs.delete(:class)].compact.join(" ")
551
+ data = { slot: slot }.merge(attrs.delete(:data) || {})
552
+ content_tag(tag_name, (capture(&block) if block), **attrs, class: classes, data: data)
553
+ end
554
+ end
555
+
556
+ # The group heading: a muted label above a sidebar menu.
557
+ #
558
+ # @example
559
+ # poetry_sidebar_group_label { "Projects" }
560
+ # @see Poetry::Ui::Sidebar::Component
561
+ def poetry_sidebar_group_label(**attrs, &block)
562
+ classes = [Poetry::Ui::Sidebar::Style.css(:group_label), attrs.delete(:class)].compact.join(" ")
563
+ content_tag(:div, (capture(&block) if block), **attrs, class: classes,
564
+ data: { slot: "sidebar-group-label" })
565
+ end
566
+
567
+ # The themed divider between sidebar groups.
568
+ #
569
+ # @example
570
+ # poetry_sidebar_separator
571
+ # @see Poetry::Ui::Sidebar::Component
572
+ def poetry_sidebar_separator(**attrs)
573
+ classes = [Poetry::Ui::Sidebar::Style.css(:separator), attrs.delete(:class)].compact.join(" ")
574
+ render(Poetry::Ui::Separator::Component.new(**attrs, class: classes, "data-slot": "sidebar-separator"))
575
+ end
576
+
577
+ # A menu button: an anchor (href:) or a button, with the active state
578
+ # + size and variant axes. active: is the current route (data-active
579
+ # styles it); variant: :outline draws the bordered treatment.
580
+ #
581
+ # @example
582
+ # poetry_sidebar_menu_button(href: "/projects", active: true) { "Projects" }
583
+ # @see Poetry::Ui::Sidebar::Component
584
+ def poetry_sidebar_menu_button(href: nil, active: false, size: :default,
585
+ variant: :default, **attrs, &block)
586
+ unless %i[default outline].include?(variant.to_sym)
587
+ raise ArgumentError, "sidebar menu button variant: must be :default or :outline"
588
+ end
589
+
590
+ style = Poetry::Ui::Sidebar::Style
591
+ classes = [style.css(:menu_button), style.menu_button_size(size),
592
+ "cn-sidebar-menu-button-variant-#{variant}", attrs.delete(:class)].compact.join(" ")
593
+ data = { slot: "sidebar-menu-button", size: size, variant: variant,
594
+ active: active ? "" : nil }.compact.merge(attrs.delete(:data) || {})
595
+ if href
596
+ content_tag(:a, (capture(&block) if block), href: href, class: classes, data: data,
597
+ "aria-current": active ? "page" : nil, **attrs)
598
+ else
599
+ content_tag(:button, (capture(&block) if block), type: "button", class: classes, data: data, **attrs)
600
+ end
601
+ end
602
+
603
+ # The group-corner action (upstream SidebarGroupAction): pinned to the
604
+ # group's top-right by the theme.
605
+ #
606
+ # @example
607
+ # poetry_sidebar_group_action(label: "Add project") { poetry_icon(name: :plus) }
608
+ # @see Poetry::Ui::Sidebar::Component
609
+ def poetry_sidebar_group_action(label: nil, **attrs, &block)
610
+ classes = [Poetry::Ui::Sidebar::Style.css(:group_action_button), attrs.delete(:class)].compact.join(" ")
611
+ data = { slot: "sidebar-group-action", sidebar: "group-action" }.merge(attrs.delete(:data) || {})
612
+ content_tag(:button, (capture(&block) if block), type: "button", class: classes, data: data,
613
+ "aria-label": label, **attrs)
614
+ end
615
+
616
+ # The sidebar search input (upstream SidebarInput): the themed Input
617
+ # sized for the header well.
618
+ #
619
+ # @example
620
+ # poetry_sidebar_input(placeholder: "Search...")
621
+ # @see Poetry::Ui::Sidebar::Component
622
+ def poetry_sidebar_input(**attrs)
623
+ classes = [Poetry::Ui::Sidebar::Style.css(:input_control), attrs.delete(:class)].compact.join(" ")
624
+ render(Poetry::Ui::Input::Component.new(**attrs, class: classes, "data-slot": "sidebar-input"))
625
+ end
626
+
627
+ # A loading placeholder row (upstream SidebarMenuSkeleton): optional
628
+ # leading icon block + a text bar with a random-ish width the caller
629
+ # can pin via text_width:.
630
+ #
631
+ # @example
632
+ # poetry_sidebar_menu_skeleton(icon: true)
633
+ # @see Poetry::Ui::Sidebar::Component
634
+ def poetry_sidebar_menu_skeleton(icon: false, text_width: "70%", **attrs)
635
+ classes = [Poetry::Ui::Sidebar::Style.css(:menu_skeleton), attrs.delete(:class)].compact.join(" ")
636
+ data = { slot: "sidebar-menu-skeleton" }.merge(attrs.delete(:data) || {})
637
+ content_tag(:div, class: classes, data: data, **attrs) do
638
+ safe_join([
639
+ (if icon
640
+ render(Poetry::Ui::Skeleton::Component.new(class: Poetry::Ui::Sidebar::Style.css(:menu_skeleton_icon),
641
+ "data-slot": "sidebar-menu-skeleton-icon"))
642
+ end),
643
+ render(Poetry::Ui::Skeleton::Component.new(class: Poetry::Ui::Sidebar::Style.css(:menu_skeleton_text),
644
+ "data-slot": "sidebar-menu-skeleton-text",
645
+ style: "--skeleton-width: #{text_width}"))
646
+ ].compact)
647
+ end
648
+ end
649
+
650
+ # An item-corner action (menu-action): absolutely positioned inside the
651
+ # menu item (the menu button reserves pr-8 room via the group marker).
652
+ # show_on_hover: keeps it invisible until the item is hovered/focused
653
+ # on desktop.
654
+ #
655
+ # @example
656
+ # poetry_sidebar_menu_action(label: "More", show_on_hover: true) { poetry_icon(name: :ellipsis) }
657
+ # @see Poetry::Ui::Sidebar::Component
658
+ def poetry_sidebar_menu_action(show_on_hover: false, label: nil, **attrs, &block)
659
+ style = Poetry::Ui::Sidebar::Style
660
+ classes = [style.css(:menu_action), (style.css(:menu_action_hover) if show_on_hover),
661
+ attrs.delete(:class)].compact.join(" ")
662
+ data = { slot: "sidebar-menu-action", sidebar: "menu-action" }.merge(attrs.delete(:data) || {})
663
+ content_tag(:button, (capture(&block) if block), type: "button", class: classes, data: data,
664
+ "aria-label": label, **attrs)
665
+ end
666
+
667
+ # A trailing badge (menu-badge): count/status chrome in the item corner,
668
+ # pointer-transparent.
669
+ #
670
+ # @example
671
+ # poetry_sidebar_menu_badge { "24" }
672
+ # @see Poetry::Ui::Sidebar::Component
673
+ def poetry_sidebar_menu_badge(**attrs, &block)
674
+ style = Poetry::Ui::Sidebar::Style
675
+ classes = [style.css(:menu_badge), attrs.delete(:class)].compact.join(" ")
676
+ data = { slot: "sidebar-menu-badge", sidebar: "menu-badge" }.merge(attrs.delete(:data) || {})
677
+ content_tag(:div, (capture(&block) if block), class: classes, data: data, **attrs)
678
+ end
679
+
680
+ # A nested menu entry: an anchor (href:) or a button; active: marks
681
+ # the current route (aria-current + data-active).
682
+ #
683
+ # @example
684
+ # poetry_sidebar_menu_sub_button(href: "/settings") { "Settings" }
685
+ # @see Poetry::Ui::Sidebar::Component
686
+ def poetry_sidebar_menu_sub_button(href: nil, active: false, **attrs, &block)
687
+ style = Poetry::Ui::Sidebar::Style
688
+ classes = [style.css(:menu_sub_button), attrs.delete(:class)].compact.join(" ")
689
+ data = { slot: "sidebar-menu-sub-button",
690
+ active: active ? "" : nil }.compact.merge(attrs.delete(:data) || {})
691
+ tag_name = href ? :a : :button
692
+ extra = if href
693
+ { href: href, "aria-current": active ? "page" : nil }
694
+ else
695
+ { type: "button" }
696
+ end
697
+ content_tag(tag_name, (capture(&block) if block), class: classes, data: data, **extra, **attrs)
698
+ end
699
+
700
+ # The NavigationMenu: a disclosure bar - with_item for
701
+ # trigger+panel, with_link for destinations; label: names the nav.
702
+ #
703
+ # @example Disclosure bar with a panel and a link
704
+ # <%= poetry_navigation_menu(label: "Main") do |nav| %>
705
+ # <% nav.with_item("Products", value: "products") do %>
706
+ # <%= poetry_navigation_menu_link(href: products_path) { "All products" } %>
707
+ # <% end %>
708
+ # <% nav.with_link("Docs", href: docs_path) %>
709
+ # <% end %>
710
+ # @see Poetry::Ui::NavigationMenu::Component
711
+ def poetry_navigation_menu(**, &)
712
+ render(Poetry::Ui::NavigationMenu::Component.new(**), &)
713
+ end
714
+
715
+ # A panel entry: a REAL link (active: marks the current page).
716
+ #
717
+ # @example
718
+ # poetry_navigation_menu_link(href: "/docs", active: true) { "Docs" }
719
+ # @see Poetry::Ui::NavigationMenu::Component
720
+ def poetry_navigation_menu_link(href:, active: false, **attrs, &block)
721
+ classes = [Poetry::Ui::NavigationMenu::Style.css(:link), attrs.delete(:class)].compact.join(" ")
722
+ data = { slot: "navigation-menu-link", active: active ? "true" : nil }.compact
723
+ .merge(attrs.delete(:data) || {})
724
+ content_tag(:a, (capture(&block) if block), **attrs, href: href, class: classes, data: data)
725
+ end
726
+
727
+ # The Carousel: native scroll-snap slides - with_item per
728
+ # slide; label: names the region.
729
+ #
730
+ # @example
731
+ # poetry_carousel(label: "Featured") do |carousel|
732
+ # carousel.with_item { "Slide one" }
733
+ # carousel.with_item { "Slide two" }
734
+ # end
735
+ # @see Poetry::Ui::Carousel::Component
736
+ def poetry_carousel(**, &)
737
+ render(Poetry::Ui::Carousel::Component.new(**), &)
738
+ end
739
+
740
+ # The Resizable panel group: with_panel x N; handles are
741
+ # interleaved automatically (APG window splitters).
742
+ #
743
+ # @example
744
+ # poetry_resizable(class: "h-48 rounded-lg border") do |group|
745
+ # group.with_panel(default_size: 25) { tag.div("Sidebar") }
746
+ # group.with_panel { tag.div("Content") }
747
+ # end
748
+ # @see Poetry::Ui::Resizable::Component
749
+ def poetry_resizable(**, &)
750
+ render(Poetry::Ui::Resizable::Component.new(**), &)
751
+ end
752
+
753
+ # The Drawer: the swipeable edge dialog - trigger/title/
754
+ # description/footer slots, direction: down/up/left/right.
755
+ #
756
+ # @example
757
+ # poetry_drawer(show_swipe_handle: true) do |drawer|
758
+ # drawer.with_trigger { "Open drawer" }
759
+ # drawer.with_title { "Move goal" }
760
+ # drawer.with_description { "Set your daily activity goal." }
761
+ # "Drawer body"
762
+ # end
763
+ # @see Poetry::Ui::Drawer::Component
764
+ def poetry_drawer(**, &)
765
+ render(Poetry::Ui::Drawer::Component.new(**), &)
766
+ end
767
+
768
+ # The native scroll region: size it with classes; label: names it.
769
+ #
770
+ # @example A bounded list that scrolls
771
+ # poetry_scroll_area(label: "Tags", class: "h-72 w-48") do
772
+ # safe_join(tags.map { |tag| tag.name })
773
+ # end
774
+ # @see Poetry::Ui::ScrollArea::Component
775
+ def poetry_scroll_area(**, &)
776
+ render(Poetry::Ui::ScrollArea::Component.new(**), &)
777
+ end
778
+
779
+ # Tabs: declare with with_tab(title, value:) + panel blocks;
780
+ # the component owns the ARIA wiring and the two-controller split.
781
+ #
782
+ # @example
783
+ # <%= poetry_tabs(default: "account", label: "Account settings") do |tabs| %>
784
+ # <% tabs.with_tab("Account", value: "account") do %>...panel...<% end %>
785
+ # <% tabs.with_tab("Password", value: "password") do %>...panel...<% end %>
786
+ # <% end %>
787
+ # @see Poetry::Ui::Tabs::Component
788
+ def poetry_tabs(**, &)
789
+ render(Poetry::Ui::Tabs::Component.new(**), &)
790
+ end
791
+
792
+ # Visually joins adjacent buttons and controls into one group.
793
+ #
794
+ # @example A segmented pair
795
+ # poetry_button_group("aria-label": "Alignment") do
796
+ # safe_join([
797
+ # poetry_button(variant: :outline) { "Left" },
798
+ # poetry_button(variant: :outline) { "Right" }
799
+ # ])
800
+ # end
801
+ # @see Poetry::Ui::ButtonGroup::Component
802
+ def poetry_button_group(**, &)
803
+ render(Poetry::Ui::ButtonGroup::Component.new(**), &)
804
+ end
805
+
806
+ # Non-interactive text inside a ButtonGroup run.
807
+ #
808
+ # @example
809
+ # poetry_button_group_text { "of 12" }
810
+ # @see Poetry::Ui::ButtonGroup::Component
811
+ def poetry_button_group_text(**attrs, &block)
812
+ classes = [Poetry::Ui::ButtonGroup::Style.css(:text), attrs.delete(:class)].compact.join(" ")
813
+ data = { slot: "button-group-text" }.merge(attrs.delete(:data) || {})
814
+ content_tag(:div, (capture(&block) if block), **attrs, class: classes, data: data)
815
+ end
816
+
817
+ # The vertical divider between grouped controls.
818
+ #
819
+ # @example
820
+ # poetry_button_group_separator
821
+ # @see Poetry::Ui::ButtonGroup::Component
822
+ def poetry_button_group_separator(**attrs)
823
+ classes = [Poetry::Ui::ButtonGroup::Style.css(:separator), attrs.delete(:class)].compact.join(" ")
824
+ render(Poetry::Ui::Separator::Component.new(orientation: :vertical, **attrs, class: classes,
825
+ "data-slot": "button-group-separator"))
826
+ end
827
+
828
+ # A styled wrapper around the real native select control.
829
+ #
830
+ # @example The options: fast path
831
+ # poetry_native_select(
832
+ # name: "sort", label: "Sort by",
833
+ # options: [["Newest first", "newest"], ["Oldest first", "oldest"]],
834
+ # selected: "newest"
835
+ # )
836
+ # @see Poetry::Ui::NativeSelect::Component
837
+ def poetry_native_select(**, &)
838
+ render(Poetry::Ui::NativeSelect::Component.new(**), &)
839
+ end
840
+
841
+ # One styled <option> inside poetry_native_select.
842
+ #
843
+ # @example
844
+ # poetry_native_select_option(value: "us") { "United States" }
845
+ # @see Poetry::Ui::NativeSelect::Component
846
+ def poetry_native_select_option(**attrs, &block)
847
+ classes = [Poetry::Ui::NativeSelect::Style.css(:option), attrs.delete(:class)].compact.join(" ")
848
+ data = { slot: "native-select-option" }.merge(attrs.delete(:data) || {})
849
+ content_tag(:option, (capture(&block) if block), **attrs, class: classes, data: data)
850
+ end
851
+
852
+ # A styled <optgroup> inside poetry_native_select.
853
+ #
854
+ # @example
855
+ # poetry_native_select_optgroup(label: "Europe") do
856
+ # poetry_native_select_option(value: "fr") { "France" }
857
+ # end
858
+ # @see Poetry::Ui::NativeSelect::Component
859
+ def poetry_native_select_optgroup(**attrs, &block)
860
+ classes = [Poetry::Ui::NativeSelect::Style.css(:optgroup), attrs.delete(:class)].compact.join(" ")
861
+ data = { slot: "native-select-optgroup" }.merge(attrs.delete(:data) || {})
862
+ content_tag(:optgroup, (capture(&block) if block), **attrs, class: classes, data: data)
863
+ end
864
+
865
+ # One bordered surface combining an input with buttons, icons, or
866
+ # add-ons.
867
+ #
868
+ # @example
869
+ # <%= poetry_input_group do %>
870
+ # <%= poetry_input_group_addon do %>
871
+ # <%= poetry_icon(name: :search) %>
872
+ # <% end %>
873
+ # <%= poetry_input_group_input(name: "q", placeholder: "Search...") %>
874
+ # <% end %>
875
+ # @see Poetry::Ui::InputGroup::Component
876
+ def poetry_input_group(**, &)
877
+ render(Poetry::Ui::InputGroup::Component.new(**), &)
878
+ end
879
+
880
+ # The valid align: values for poetry_input_group_addon.
881
+ INPUT_GROUP_ALIGNS = %i[inline-start inline-end block-start block-end].freeze
882
+
883
+ # An addon region inside an InputGroup - icons, text, or small
884
+ # buttons aligned to one of the four edges of the group.
885
+ #
886
+ # @example
887
+ # poetry_input_group_addon(align: :"inline-end") { poetry_icon(name: :search) }
888
+ # @see Poetry::Ui::InputGroup::Component
889
+ def poetry_input_group_addon(align: :"inline-start", **attrs, &block)
890
+ unless INPUT_GROUP_ALIGNS.include?(align.to_sym)
891
+ raise ArgumentError, "InputGroup addon align: must be one of #{INPUT_GROUP_ALIGNS.inspect}"
892
+ end
893
+
894
+ style = Poetry::Ui::InputGroup::Style
895
+ classes = [style.css(:addon), style.css(:"addon_#{align.to_s.tr("-", "_")}"),
896
+ attrs.delete(:class)].compact.join(" ")
897
+ data = { slot: "input-group-addon", align: align }.merge(attrs.delete(:data) || {})
898
+ content_tag(:div, (capture(&block) if block), **attrs, role: "group", class: classes, data: data)
899
+ end
900
+
901
+ # Inline text inside an InputGroup - prefixes, suffixes, hints.
902
+ #
903
+ # @example
904
+ # poetry_input_group_text { "https://" }
905
+ # @see Poetry::Ui::InputGroup::Component
906
+ def poetry_input_group_text(**attrs, &block)
907
+ classes = [Poetry::Ui::InputGroup::Style.css(:text), attrs.delete(:class)].compact.join(" ")
908
+ content_tag(:span, (capture(&block) if block), **attrs, class: classes)
909
+ end
910
+
911
+ # The borderless in-group control: the group wears the chrome; the
912
+ # data-slot=input-group-control is what its focus/invalid selectors
913
+ # key on.
914
+ #
915
+ # @example
916
+ # poetry_input_group_input(placeholder: "Search...")
917
+ # @see Poetry::Ui::InputGroup::Component
918
+ def poetry_input_group_input(**attrs)
919
+ classes = [Poetry::Ui::InputGroup::Style.css(:control_input), attrs.delete(:class)].compact.join(" ")
920
+ render(Poetry::Ui::Input::Component.new(**attrs, class: classes, "data-slot": "input-group-control"))
921
+ end
922
+
923
+ # The borderless multiline in-group control - the group wears the
924
+ # chrome, like poetry_input_group_input.
925
+ #
926
+ # @example
927
+ # poetry_input_group_textarea(placeholder: "Add a note...")
928
+ # @see Poetry::Ui::InputGroup::Component
929
+ def poetry_input_group_textarea(**attrs)
930
+ classes = [Poetry::Ui::InputGroup::Style.css(:control_textarea), attrs.delete(:class)].compact.join(" ")
931
+ render(Poetry::Ui::Textarea::Component.new(**attrs, class: classes, "data-slot": "input-group-control"))
932
+ end
933
+
934
+ # The valid size: values for poetry_input_group_button.
935
+ INPUT_GROUP_BUTTON_SIZES = %i[xs sm icon-xs icon-sm].freeze
936
+
937
+ # The tiny in-group action: a ghost Button re-sized by the group's
938
+ # dictionary (tailwind_merge lets h-6 beat the Button's own h-9).
939
+ #
940
+ # @example
941
+ # poetry_input_group_button(size: :"icon-xs", label: "Copy") { poetry_icon(name: :copy) }
942
+ # @see Poetry::Ui::InputGroup::Component
943
+ def poetry_input_group_button(size: :xs, **attrs, &)
944
+ unless INPUT_GROUP_BUTTON_SIZES.include?(size.to_sym)
945
+ raise ArgumentError, "InputGroup button size: must be one of #{INPUT_GROUP_BUTTON_SIZES.inspect}"
946
+ end
947
+ # Button's own icon-only guard keys off ITS size:, which this helper
948
+ # bypasses - re-enforce the accessible-name rule here.
949
+ if size.to_s.start_with?("icon") && attrs[:label].blank?
950
+ raise ArgumentError, "icon-sized InputGroup button requires label: (the accessible name)"
951
+ end
952
+
953
+ style = Poetry::Ui::InputGroup::Style
954
+ size_css = style.css(:"button_#{size.to_s.tr("-", "_")}")
955
+ classes = [style.css(:button), size_css, attrs.delete(:class)].compact.join(" ")
956
+ render(Poetry::Ui::Button::Component.new(variant: :ghost, **attrs, class: classes,
957
+ "data-size": size), &)
958
+ end
959
+
960
+ # The value contracts runtime-enforced inside wrapper helpers, in
961
+ # registry shape - emitted as the registry's "helpers" section
962
+ # (rakelib/registry.rake) so poetry check and the MCP server validate
963
+ # these literals statically (the crash class: a helper-only literal
964
+ # like align: :leading that no component contract covers, failing
965
+ # only at render). Plain wrapper helpers need no entry here;
966
+ # registry generation lists them name-only.
967
+ HELPER_CONTRACTS = {
968
+ "poetry_toast_trigger" => {
969
+ "options" => [
970
+ { "name" => "template", "type" => "string",
971
+ "description" => "id of the <template> holding the rendered poetry_toast" },
972
+ { "name" => "toaster", "type" => "string",
973
+ "description" => "optional toaster region id - omit for the page's toaster" }
974
+ ]
975
+ },
976
+ "poetry_optimistic_form" => {
977
+ "yields" => "the form builder - form.optimistic_template authors the predicted " \
978
+ "turbo-stream(s); form.optimistic_hidden_field places the submitted value; " \
979
+ "every standard field helper works",
980
+ "options" => [
981
+ { "name" => "attribute_name", "type" => "symbol" },
982
+ { "name" => "value", "type" => "object" }
983
+ ]
984
+ },
985
+ "poetry_input_group_addon" => {
986
+ "options" => [{ "name" => "align", "type" => "symbol", "default" => "inline-start",
987
+ "variants" => INPUT_GROUP_ALIGNS.map(&:to_s) }]
988
+ },
989
+ "poetry_input_group_button" => {
990
+ "options" => [{ "name" => "size", "type" => "symbol", "default" => "xs",
991
+ "variants" => INPUT_GROUP_BUTTON_SIZES.map(&:to_s) }]
992
+ }
993
+ }.freeze
994
+
995
+ # A window overlaid on the page for content that requires attention.
996
+ #
997
+ # @example A confirmation dialog
998
+ # poetry_dialog do |dialog|
999
+ # dialog.with_trigger(variant: :outline) { "Open" }
1000
+ # dialog.with_title { "Are you sure?" }
1001
+ # dialog.with_description { "This cannot be undone." }
1002
+ # end
1003
+ # @see Poetry::Ui::Dialog::Component
1004
+ def poetry_dialog(**, &)
1005
+ render(Poetry::Ui::Dialog::Component.new(**), &)
1006
+ end
1007
+
1008
+ # A dialog that slides in from a screen edge.
1009
+ #
1010
+ # @example
1011
+ # poetry_sheet(side: :right) do |sheet|
1012
+ # sheet.with_trigger { "Edit profile" }
1013
+ # sheet.with_title { "Edit profile" }
1014
+ # "Sheet body"
1015
+ # end
1016
+ # @see Poetry::Ui::Sheet::Component
1017
+ def poetry_sheet(**, &)
1018
+ render(Poetry::Ui::Sheet::Component.new(**), &)
1019
+ end
1020
+
1021
+ # A modal dialog that interrupts the user and expects a response.
1022
+ #
1023
+ # @example Destructive confirmation
1024
+ # poetry_alert_dialog do |dialog|
1025
+ # dialog.with_trigger(variant: :destructive) { "Delete project" }
1026
+ # dialog.with_title { "Delete this project?" }
1027
+ # dialog.with_description { "This cannot be undone." }
1028
+ # dialog.with_cancel { "Cancel" }
1029
+ # dialog.with_action(variant: :destructive) { "Delete" }
1030
+ # end
1031
+ # @see Poetry::Ui::AlertDialog::Component
1032
+ def poetry_alert_dialog(**, &)
1033
+ render(Poetry::Ui::AlertDialog::Component.new(**), &)
1034
+ end
1035
+
1036
+ # A form control for entering a single line of text.
1037
+ #
1038
+ # @example An email field
1039
+ # poetry_input(type: "email", name: "email",
1040
+ # placeholder: "you@example.com")
1041
+ # @see Poetry::Ui::Input::Component
1042
+ def poetry_input(**)
1043
+ render(Poetry::Ui::Input::Component.new(**))
1044
+ end
1045
+
1046
+ # Input's multiline sibling - the value is the CONTENT (value:), not
1047
+ # a block; auto-grow is CSS (field-sizing-content), never a JS
1048
+ # autosizer.
1049
+ #
1050
+ # @example A free-text field
1051
+ # poetry_textarea(name: "bio", rows: 4,
1052
+ # placeholder: "Tell us about yourself")
1053
+ # @see Poetry::Ui::Textarea::Component
1054
+ def poetry_textarea(**)
1055
+ render(Poetry::Ui::Textarea::Component.new(**))
1056
+ end
1057
+
1058
+ # Fixed-length code entry: ONE native input over aria-hidden cells
1059
+ # (paste/SMS-autofill/IME all native) - never per-cell inputs.
1060
+ #
1061
+ # @example
1062
+ # poetry_input_otp(name: "code", length: 6, groups: [3, 3])
1063
+ # @see Poetry::Ui::InputOtp::Component
1064
+ def poetry_input_otp(**)
1065
+ render(Poetry::Ui::InputOtp::Component.new(**))
1066
+ end
1067
+
1068
+ # An accessible caption bound to a form control.
1069
+ #
1070
+ # @example A label wired to its control
1071
+ # poetry_label(for_id: "email") { "Email" }
1072
+ # @see Poetry::Ui::Label::Component
1073
+ def poetry_label(**, &)
1074
+ render(Poetry::Ui::Label::Component.new(**), &)
1075
+ end
1076
+
1077
+ # The formatted number field: a formatted text input
1078
+ # over a hidden type=number that submits the raw value; steppers
1079
+ # with press-and-hold; ArrowUp/Down (Shift/Alt sizes) on the input.
1080
+ #
1081
+ # @example
1082
+ # poetry_number_field(
1083
+ # name: "quantity", value: 2, min: 0, label: "Quantity"
1084
+ # )
1085
+ # @see Poetry::Ui::NumberField::Component
1086
+ def poetry_number_field(**)
1087
+ render(Poetry::Ui::NumberField::Component.new(**))
1088
+ end
1089
+
1090
+ # The segmented date editor:
1091
+ # a native input type=date (ISO on the wire, native pickers with no
1092
+ # JS) enhanced into per-segment spinbutton editing.
1093
+ #
1094
+ # @example
1095
+ # poetry_date_field(name: "event[on]", label: "Event date")
1096
+ # @see Poetry::Ui::DateField::Component
1097
+ def poetry_date_field(**)
1098
+ render(Poetry::Ui::DateField::Component.new(**))
1099
+ end
1100
+
1101
+ # DateField at hour granularity: HH:MM[:SS] on the wire; the locale
1102
+ # decides 12- vs 24-hour editing (hour_cycle: pins it).
1103
+ #
1104
+ # @example
1105
+ # poetry_time_field(
1106
+ # name: "starts_at", label: "Start time", value: "09:30"
1107
+ # )
1108
+ # @see Poetry::Ui::TimeField::Component
1109
+ def poetry_time_field(**)
1110
+ render(Poetry::Ui::TimeField::Component.new(**))
1111
+ end
1112
+
1113
+ # A quantity within a known range (disk, seats, strength) - role
1114
+ # "meter progressbar" (the two-token fallback); never indeterminate.
1115
+ #
1116
+ # @example
1117
+ # poetry_meter(value: 62, label: "Storage used")
1118
+ # @see Poetry::Ui::Meter::Component
1119
+ def poetry_meter(**)
1120
+ render(Poetry::Ui::Meter::Component.new(**))
1121
+ end
1122
+
1123
+ # type=search on InputGroup chrome: Escape clears-then-dismisses,
1124
+ # focus-holding clear button, native WebKit affordances suppressed.
1125
+ #
1126
+ # @example
1127
+ # poetry_search_field(name: "q", label: "Search", placeholder: "Search...")
1128
+ # @see Poetry::Ui::SearchField::Component
1129
+ def poetry_search_field(**)
1130
+ render(Poetry::Ui::SearchField::Component.new(**))
1131
+ end
1132
+
1133
+ # Removable-chip collection (grid semantics, roving arrows, Delete
1134
+ # removal w/ focus recovery); name: serializes name[] per tag.
1135
+ #
1136
+ # @example Removable recipients that submit as recipients[]
1137
+ # poetry_tag_group(label: "Recipients", name: "recipients") do |group|
1138
+ # group.with_tag(value: "ada", label: "Ada")
1139
+ # group.with_tag(value: "grace", label: "Grace")
1140
+ # end
1141
+ # @see Poetry::Ui::TagGroup::Component
1142
+ def poetry_tag_group(**, &)
1143
+ render(Poetry::Ui::TagGroup::Component.new(**), &)
1144
+ end
1145
+
1146
+ # Hierarchical expandable list (flat treegrid): items via the
1147
+ # nested with_item builder, expansion persisted by the host through
1148
+ # poetry:tree:toggle.
1149
+ #
1150
+ # @example
1151
+ # <%= poetry_tree(label: "Files") do |tree| %>
1152
+ # <% tree.with_item(text: "docs", value: "docs", expanded: true) do |docs| %>
1153
+ # <% docs.with_item(text: "intro.md", value: "intro", href: "/docs/intro") %>
1154
+ # <% end %>
1155
+ # <% end %>
1156
+ # @see Poetry::Ui::Tree::Component
1157
+ def poetry_tree(**, &)
1158
+ render(Poetry::Ui::Tree::Component.new(**), &)
1159
+ end
1160
+
1161
+ # Wraps a form control with its label, hint, and validation message.
1162
+ #
1163
+ # @example
1164
+ # poetry_field(
1165
+ # id: "email", label_text: "Email", hint: "We never share it."
1166
+ # ) do |field|
1167
+ # tag.input(type: "email", name: "email", **field.control_attributes)
1168
+ # end
1169
+ # @see Poetry::Ui::Field::Component
1170
+ def poetry_field(**, &)
1171
+ render(Poetry::Ui::Field::Component.new(**), &)
1172
+ end
1173
+
1174
+ # The Field family's group layer: a run of related fields inside a
1175
+ # real <fieldset>, named by legend: (a real <legend>).
1176
+ #
1177
+ # @example A named group of address fields
1178
+ # poetry_fieldset(legend: "Shipping address") do
1179
+ # # poetry_field_group with the fields
1180
+ # end
1181
+ # @see Poetry::Ui::Fieldset::Component
1182
+ def poetry_fieldset(**, &)
1183
+ render(Poetry::Ui::Fieldset::Component.new(**), &)
1184
+ end
1185
+
1186
+ # Stacks fields/fieldsets with the theme's rhythm; also the CSS
1187
+ # `@container` scope Field's orientation: :responsive measures against.
1188
+ #
1189
+ # @example Stacking fields with the theme's rhythm
1190
+ # poetry_field_group do
1191
+ # safe_join([
1192
+ # poetry_field { ... },
1193
+ # poetry_field_separator,
1194
+ # poetry_field { ... }
1195
+ # ])
1196
+ # end
1197
+ # @see Poetry::Ui::FieldGroup::Component
1198
+ def poetry_field_group(**, &)
1199
+ render(Poetry::Ui::FieldGroup::Component.new(**), &)
1200
+ end
1201
+
1202
+ # Divider between stacked fields; pass a block for the inline
1203
+ # caption form ("Or continue with").
1204
+ #
1205
+ # @example A captioned divider between stacked fields
1206
+ # poetry_field_separator { "Or continue with" }
1207
+ # @see Poetry::Ui::FieldSeparator::Component
1208
+ def poetry_field_separator(**, &)
1209
+ render(Poetry::Ui::FieldSeparator::Component.new(**), &)
1210
+ end
1211
+
1212
+ # Void control (no content block) - the label is EXTERNAL (Label/Field
1213
+ # for= the button id) or label: (the aria-label fallback).
1214
+ #
1215
+ # @example A form checkbox
1216
+ # poetry_checkbox(name: "terms", label: "Accept terms")
1217
+ # @see Poetry::Ui::Checkbox::Component
1218
+ def poetry_checkbox(**)
1219
+ render(Poetry::Ui::Checkbox::Component.new(**))
1220
+ end
1221
+
1222
+ # The select-all family (the APG mixed-state
1223
+ # parent): the wrapper carries the poetry--core--checkbox-group
1224
+ # controller; the parent checkbox fans out to every enabled item and
1225
+ # item toggles re-derive it (all -> checked, none -> unchecked,
1226
+ # some -> indeterminate).
1227
+ #
1228
+ # @example
1229
+ # poetry_checkbox_group do
1230
+ # poetry_checkbox_group_all(id: "all")
1231
+ # poetry_checkbox_group_item(name: "ids[]", value: "1", id: "row-1")
1232
+ # end
1233
+ # @see Poetry::Ui::Checkbox::Component
1234
+ def poetry_checkbox_group(**attrs, &block)
1235
+ # role=group makes the aria-labelledby/-describedby the FormBuilder
1236
+ # routes here real for AT - on a bare div they announce nothing.
1237
+ wiring = { role: "group", class: attrs.delete(:class),
1238
+ data: { slot: "checkbox-group", controller: "poetry--core--checkbox-group",
1239
+ action: "poetry:checkbox:change->poetry--core--checkbox-group#changed" } }
1240
+ content_tag(:div, (capture(&block) if block),
1241
+ Poetry::Core::HTML::Attributes.merged(wiring, attrs))
1242
+ end
1243
+
1244
+ # The group's parent checkbox (target: all) - checking it fans out.
1245
+ #
1246
+ # @example
1247
+ # poetry_checkbox_group_all(id: "select-all")
1248
+ # @see Poetry::Ui::Checkbox::Component
1249
+ def poetry_checkbox_group_all(**attrs)
1250
+ data = { "poetry--core--checkbox-group-target": "all" }.merge(attrs.delete(:data) || {})
1251
+ render(Poetry::Ui::Checkbox::Component.new(**attrs, data: data))
1252
+ end
1253
+
1254
+ # One member checkbox (target: item) - toggles re-derive the parent.
1255
+ #
1256
+ # @example
1257
+ # poetry_checkbox_group_item(name: "ids[]", value: "1", checked: true)
1258
+ # @see Poetry::Ui::Checkbox::Component
1259
+ def poetry_checkbox_group_item(**attrs)
1260
+ data = { "poetry--core--checkbox-group-target": "item" }.merge(attrs.delete(:data) || {})
1261
+ render(Poetry::Ui::Checkbox::Component.new(**attrs, data: data))
1262
+ end
1263
+
1264
+ # Read-only value + one copy affordance (API keys, install commands,
1265
+ # IDs); text_to_copy: overrides the clipboard when the display
1266
+ # truncates. Editable text is poetry_input; masked secrets are
1267
+ # poetry_sensitive_input.
1268
+ #
1269
+ # @example An install command with a copy button
1270
+ # poetry_clipboard_text(value: "gem install poetry-ui",
1271
+ # label: "Install command")
1272
+ # @see Poetry::Ui::ClipboardText::Component
1273
+ def poetry_clipboard_text(**)
1274
+ render(Poetry::Ui::ClipboardText::Component.new(**))
1275
+ end
1276
+
1277
+ # Server-rendered highlighted code panel (rouge, soft dependency):
1278
+ # language:, CSS-counter line_numbers:, highlight_lines:, and a
1279
+ # copy affordance reading the rendered code. Inline code stays
1280
+ # plain <code> typography.
1281
+ #
1282
+ # @example
1283
+ # poetry_code_block(
1284
+ # code: "puts \"hello\"", language: "ruby"
1285
+ # )
1286
+ # @see Poetry::Ui::CodeBlock::Component
1287
+ def poetry_code_block(**)
1288
+ render(Poetry::Ui::CodeBlock::Component.new(**))
1289
+ end
1290
+
1291
+ # Secret shown-on-demand (API keys, tokens): masked container is the
1292
+ # reveal button, blur/Escape/eye re-mask, copy: copies without
1293
+ # revealing. Plain passwords being SET (not shown) can stay a
1294
+ # poetry_input type: :password.
1295
+ #
1296
+ # @example An API key with copy-without-reveal
1297
+ # poetry_sensitive_input(name: "api_key", label: "API key",
1298
+ # value: token, copy: true)
1299
+ # @see Poetry::Ui::SensitiveInput::Component
1300
+ def poetry_sensitive_input(**)
1301
+ render(Poetry::Ui::SensitiveInput::Component.new(**))
1302
+ end
1303
+
1304
+ # Void control - instant-effect on/off (role=switch announces on/off);
1305
+ # values staged for submit belong to poetry_checkbox.
1306
+ #
1307
+ # @example A named setting switch
1308
+ # poetry_switch(name: "notifications", checked: true,
1309
+ # label: "Email notifications")
1310
+ # @see Poetry::Ui::Switch::Component
1311
+ def poetry_switch(**)
1312
+ render(Poetry::Ui::Switch::Component.new(**))
1313
+ end
1314
+
1315
+ # One-question-at-a-time survey: a REAL form of fieldset items -
1316
+ # native radio/checkbox/text answers, validate-gated navigation.
1317
+ #
1318
+ # @example
1319
+ # poetry_questionnaire(url: "/surveys") do |survey|
1320
+ # survey.with_item(name: "mood", title: "How was your week?") do |item|
1321
+ # item.with_choice(value: "good", label: "Good")
1322
+ # item.with_choice(value: "bad", label: "Bad")
1323
+ # end
1324
+ # end
1325
+ # @see Poetry::Ui::Questionnaire::Component
1326
+ def poetry_questionnaire(**, &)
1327
+ render(Poetry::Ui::Questionnaire::Component.new(**), &)
1328
+ end
1329
+
1330
+ # The exclusive-choice control: group.with_item(value:, label:) -
1331
+ # one hidden native radio per item (collection_radio_buttons-exact
1332
+ # serialization). The group MUST be labelled (label: or
1333
+ # aria-labelledby).
1334
+ #
1335
+ # @example
1336
+ # poetry_radio_group(
1337
+ # name: "plan", value: "monthly", label: "Billing plan"
1338
+ # ) do |group|
1339
+ # group.with_item(value: "monthly", label: "Monthly")
1340
+ # group.with_item(value: "yearly", label: "Yearly")
1341
+ # end
1342
+ # @see Poetry::Ui::RadioGroup::Component
1343
+ def poetry_radio_group(**, &)
1344
+ render(Poetry::Ui::RadioGroup::Component.new(**), &)
1345
+ end
1346
+
1347
+ # Void control - numeric value (value:) or [low, high] range
1348
+ # (values:) on a continuous track; every thumb needs a distinct
1349
+ # accessible name (label:).
1350
+ #
1351
+ # @example
1352
+ # poetry_slider(name: "volume", value: 50, label: "Volume")
1353
+ # @see Poetry::Ui::Slider::Component
1354
+ def poetry_slider(**)
1355
+ render(Poetry::Ui::Slider::Component.new(**))
1356
+ end
1357
+
1358
+ # A chat message bubble aligned to its sender.
1359
+ #
1360
+ # @example An assistant reply
1361
+ # poetry_bubble(variant: :secondary) { "Here's the summary." }
1362
+ # @see Poetry::Ui::Bubble::Component
1363
+ def poetry_bubble(**, &)
1364
+ render(Poetry::Ui::Bubble::Component.new(**), &)
1365
+ end
1366
+
1367
+ # A styled wrapper stacking one sender's consecutive bubbles - a
1368
+ # dictionary element, not a component (a Bubble concern).
1369
+ #
1370
+ # @example
1371
+ # <%= poetry_bubble_group do %>
1372
+ # <%= poetry_bubble(variant: :sent) { "Hi!" } %>
1373
+ # <%= poetry_bubble(variant: :sent) { "You there?" } %>
1374
+ # <% end %>
1375
+ # @see Poetry::Ui::Bubble::Component
1376
+ def poetry_bubble_group(**attrs, &)
1377
+ poetry_chat_group(Poetry::Ui::Bubble::Style, "bubble-group", **attrs, &)
1378
+ end
1379
+
1380
+ # Pressed-state button (aria-pressed) - UI state, NOT form data; the
1381
+ # content block is the icon/text (icon-only requires label:).
1382
+ #
1383
+ # @example An icon-only bookmark toggle
1384
+ # poetry_toggle(label: "Bookmark", pressed: bookmarked?) do
1385
+ # poetry_icon(name: :bookmark)
1386
+ # end
1387
+ # @see Poetry::Ui::Toggle::Component
1388
+ def poetry_toggle(**, &)
1389
+ render(Poetry::Ui::Toggle::Component.new(**), &)
1390
+ end
1391
+
1392
+ # A set of Toggle-styled items under one value machine + one roving
1393
+ # tab stop: group.with_item(value:, label:) { icon/text }.
1394
+ #
1395
+ # @example A text-alignment switcher
1396
+ # poetry_toggle_group(value: "left", label: "Text alignment") do |group|
1397
+ # group.with_item(value: "left", label: "Align left") { icon(:"align-left") }
1398
+ # group.with_item(value: "center", label: "Align center") { icon(:"align-center") }
1399
+ # end
1400
+ # @see Poetry::Ui::ToggleGroup::Component
1401
+ def poetry_toggle_group(**, &)
1402
+ render(Poetry::Ui::ToggleGroup::Component.new(**), &)
1403
+ end
1404
+
1405
+ # A chat row pairing an author and avatar with message content.
1406
+ #
1407
+ # @example
1408
+ # poetry_message do |message|
1409
+ # message.with_avatar { "AI" }
1410
+ # message.with_header { "Assistant" }
1411
+ # tag.div("Here's the plan for today.")
1412
+ # end
1413
+ # @see Poetry::Ui::Message::Component
1414
+ def poetry_message(**, &)
1415
+ render(Poetry::Ui::Message::Component.new(**), &)
1416
+ end
1417
+
1418
+ # A styled wrapper stacking one sender's consecutive messages - a
1419
+ # dictionary element, not a component (a Message concern).
1420
+ #
1421
+ # @example
1422
+ # <%= poetry_message_group do %>
1423
+ # <%# consecutive poetry_message rows from one sender %>
1424
+ # <% end %>
1425
+ # @see Poetry::Ui::Message::Component
1426
+ def poetry_message_group(**attrs, &)
1427
+ poetry_chat_group(Poetry::Ui::Message::Style, "message-group", **attrs, &)
1428
+ end
1429
+
1430
+ # A transcript divider or inline status marker for chat UIs.
1431
+ #
1432
+ # @example
1433
+ # poetry_marker(variant: :separator) { "Yesterday" }
1434
+ # @see Poetry::Ui::Marker::Component
1435
+ def poetry_marker(**, &)
1436
+ render(Poetry::Ui::Marker::Component.new(**), &)
1437
+ end
1438
+
1439
+ # A file or image chip showing its name, type, and size.
1440
+ #
1441
+ # @example A finished upload
1442
+ # poetry_attachment do |attachment|
1443
+ # attachment.with_title { "quarterly-report.pdf" }
1444
+ # attachment.with_description { "1.2 MB" }
1445
+ # end
1446
+ # @see Poetry::Ui::Attachment::Component
1447
+ def poetry_attachment(**, &)
1448
+ render(Poetry::Ui::Attachment::Component.new(**), &)
1449
+ end
1450
+
1451
+ # The horizontally-scrolling attachment rail (scroll-fade + snap).
1452
+ #
1453
+ # @example
1454
+ # <%= poetry_attachment_group do %>
1455
+ # <%# poetry_attachment chips %>
1456
+ # <% end %>
1457
+ # @see Poetry::Ui::Attachment::Component
1458
+ def poetry_attachment_group(**attrs, &)
1459
+ poetry_chat_group(Poetry::Ui::Attachment::Style, "attachment-group", **attrs, &)
1460
+ end
1461
+
1462
+ # A streaming-aware transcript that keeps the latest message in view.
1463
+ #
1464
+ # @example A chat transcript
1465
+ # poetry_message_scroller(id: "chat") do
1466
+ # # poetry_message_scroller_item rows
1467
+ # end
1468
+ # @see Poetry::Ui::MessageScroller::Component
1469
+ def poetry_message_scroller(**, &)
1470
+ render(Poetry::Ui::MessageScroller::Component.new(**), &)
1471
+ end
1472
+
1473
+ # One transcript row - the id is how anchoring and Turbo Streams
1474
+ # find it (data-message-id; anchor: pins the reading position).
1475
+ #
1476
+ # @example
1477
+ # <%= poetry_message_scroller_item(id: message.id) do %>
1478
+ # <%= poetry_message(author: "Ada") { message.body } %>
1479
+ # <% end %>
1480
+ # @see Poetry::Ui::MessageScroller::Component
1481
+ def poetry_message_scroller_item(id:, anchor: false, **attrs, &)
1482
+ classes = [Poetry::Ui::MessageScroller::Style.css(:item), attrs.delete(:class)].compact.join(" ")
1483
+ # data-message-id is the anchoring/Turbo-Stream identity - RESERVED:
1484
+ # the id: argument wins over any caller spelling. Other caller data
1485
+ # keys (and stimulus wiring) ride along.
1486
+ attrs.delete("data-message-id")
1487
+ attrs.delete(:"data-message-id")
1488
+ data = (attrs.delete(:data) || {}).merge(slot: "message-scroller-item", "message-id": id)
1489
+ data[:"scroll-anchor"] = "true" if anchor
1490
+ tag.div(**Poetry::Core::HTML::Attributes.merged({ class: classes, data: data }, attrs)
1491
+ .symbolize_keys, &)
1492
+ end
1493
+
1494
+ # A brief, auto-dismissing notification message.
1495
+ #
1496
+ # @example An undo toast (persistent because it carries an action)
1497
+ # poetry_toast(variant: :success) do |toast|
1498
+ # toast.with_title { "Message archived" }
1499
+ # toast.with_action { "Undo" }
1500
+ # end
1501
+ # @see Poetry::Ui::Toast::Component
1502
+ def poetry_toast(**, &)
1503
+ render(Poetry::Ui::Toast::Component.new(**), &)
1504
+ end
1505
+
1506
+ # The toast viewport - render ONCE in the application layout (it is
1507
+ # data-turbo-permanent; turbo_stream.poetry_toast appends into it).
1508
+ #
1509
+ # @example The flash -> toast mapping in the layout
1510
+ # <%= poetry_toaster do %>
1511
+ # <% flash.each do |kind, message| %>
1512
+ # <%= poetry_toast(variant: kind.to_s == "alert" ? :destructive : :default) do |toast| %>
1513
+ # <% toast.with_title { message } %>
1514
+ # <% end %>
1515
+ # <% end %>
1516
+ # <% end %>
1517
+ # @see Poetry::Ui::Toaster::Component
1518
+ def poetry_toaster(**, &)
1519
+ render(Poetry::Ui::Toaster::Component.new(**), &)
1520
+ end
1521
+
1522
+ # An interactive element that expands and collapses a section of
1523
+ # content.
1524
+ #
1525
+ # @example
1526
+ # poetry_collapsible do |collapsible|
1527
+ # collapsible.with_trigger { "Show details" }
1528
+ # tag.div("Hidden until disclosed.")
1529
+ # end
1530
+ # @see Poetry::Ui::Collapsible::Component
1531
+ def poetry_collapsible(**, &)
1532
+ render(Poetry::Ui::Collapsible::Component.new(**), &)
1533
+ end
1534
+
1535
+ # A vertically stacked set of interactive headings that each reveal a
1536
+ # section of content.
1537
+ #
1538
+ # @example Single-open accordion with the first item expanded
1539
+ # poetry_accordion(open: %w[a]) do |accordion|
1540
+ # accordion.with_item(value: "a", title: "First") { "First panel" }
1541
+ # accordion.with_item(value: "b", title: "Second") { "Second panel" }
1542
+ # end
1543
+ # @see Poetry::Ui::Accordion::Component
1544
+ def poetry_accordion(**, &)
1545
+ render(Poetry::Ui::Accordion::Component.new(**), &)
1546
+ end
1547
+
1548
+ # Rich floating content anchored to a trigger.
1549
+ #
1550
+ # @example
1551
+ # poetry_popover do |popover|
1552
+ # popover.with_trigger(variant: :outline) { "Open popover" }
1553
+ # popover.with_title { "Dimensions" }
1554
+ # tag.p("Set the dimensions for the layer.")
1555
+ # end
1556
+ # @see Poetry::Ui::Popover::Component
1557
+ def poetry_popover(**, &)
1558
+ render(Poetry::Ui::Popover::Component.new(**), &)
1559
+ end
1560
+
1561
+ # A floating label describing an element on hover or focus.
1562
+ #
1563
+ # @example A described icon button
1564
+ # poetry_tooltip do |tooltip|
1565
+ # tooltip.with_trigger(variant: :outline, size: :icon, label: "Print") do
1566
+ # poetry_icon(name: :printer)
1567
+ # end
1568
+ # "Print the current page"
1569
+ # end
1570
+ # @see Poetry::Ui::Tooltip::Component
1571
+ def poetry_tooltip(**, &)
1572
+ render(Poetry::Ui::Tooltip::Component.new(**), &)
1573
+ end
1574
+
1575
+ # The tooltip delay/warm SCOPE - a config-carrying div, NOT a
1576
+ # controller (the DOM ancestor IS the shared delay scope; the tooltip
1577
+ # controller reads closest('[data-slot=tooltip-provider]') and keys
1578
+ # the module-level warm registry by it). Wrap control rows in ONE
1579
+ # provider so the warm grace makes the row feel continuous.
1580
+ #
1581
+ # @example
1582
+ # <%= poetry_tooltip_provider(delay_duration: 300) do %>
1583
+ # <%# tooltips inside share the delay + warm state %>
1584
+ # <% end %>
1585
+ # @see Poetry::Ui::Tooltip::Component
1586
+ def poetry_tooltip_provider(delay_duration: 0, skip_delay_duration: 300,
1587
+ disable_hoverable_content: false, **attrs, &)
1588
+ data = (attrs.delete(:data) || {}).merge(
1589
+ slot: "tooltip-provider",
1590
+ delay_duration: delay_duration,
1591
+ skip_delay_duration: skip_delay_duration,
1592
+ disable_hoverable_content: disable_hoverable_content
1593
+ )
1594
+ tag.div(**attrs, data: data, &)
1595
+ end
1596
+
1597
+ # A card that reveals preview content when its trigger is hovered.
1598
+ #
1599
+ # @example A profile preview behind a real link
1600
+ # poetry_hover_card do |card|
1601
+ # card.with_trigger(href: "/users/nextjs") { "@nextjs" }
1602
+ # "Joined December 2021."
1603
+ # end
1604
+ # @see Poetry::Ui::HoverCard::Component
1605
+ def poetry_hover_card(**, &)
1606
+ render(Poetry::Ui::HoverCard::Component.new(**), &)
1607
+ end
1608
+
1609
+ # A menu of actions or options triggered by a button.
1610
+ #
1611
+ # @example A menu button with actions
1612
+ # poetry_dropdown_menu do |menu|
1613
+ # menu.with_trigger(variant: :outline) { "Open" }
1614
+ # menu.with_item { "Rename" }
1615
+ # menu.with_item(variant: :destructive) { "Delete" }
1616
+ # end
1617
+ # @see Poetry::Ui::DropdownMenu::Component
1618
+ def poetry_dropdown_menu(**, &)
1619
+ render(Poetry::Ui::DropdownMenu::Component.new(**), &)
1620
+ end
1621
+
1622
+ # The value-choosing listbox (options ARE values; actions belong to
1623
+ # poetry_dropdown_menu). Must be named: a Field label (id: +
1624
+ # label[for]) or aria-label. In forms, prefer f.poetry_select.
1625
+ #
1626
+ # @example A named form select
1627
+ # poetry_select(name: "fruit", id: "fruit",
1628
+ # placeholder: "Pick a fruit") do |select|
1629
+ # select.with_item(value: "apple") { "Apple" }
1630
+ # select.with_item(value: "banana") { "Banana" }
1631
+ # end
1632
+ # @see Poetry::Ui::Select::Component
1633
+ def poetry_select(**, &)
1634
+ render(Poetry::Ui::Select::Component.new(**), &)
1635
+ end
1636
+
1637
+ # The type-to-filter value picker (Select's shell x Command's
1638
+ # engine): options ARE values, committed to a hidden native select.
1639
+ # Must be named (Field label via id: or aria-label). In forms,
1640
+ # prefer f.poetry_combobox.
1641
+ #
1642
+ # @example
1643
+ # poetry_combobox(name: "framework", "aria-label" => "Framework") do |combobox|
1644
+ # combobox.with_item(value: "rails") { "Ruby on Rails" }
1645
+ # combobox.with_item(value: "hanami") { "Hanami" }
1646
+ # end
1647
+ # @see Poetry::Ui::Combobox::Component
1648
+ def poetry_combobox(**, &)
1649
+ render(Poetry::Ui::Combobox::Component.new(**), &)
1650
+ end
1651
+
1652
+ # A menu of actions revealed by right-clicking an element.
1653
+ #
1654
+ # @example Right-click surface with actions
1655
+ # poetry_context_menu do |menu|
1656
+ # menu.with_trigger(tag: :div) { "Right-click this card" }
1657
+ # menu.with_item { "Rename" }
1658
+ # menu.with_item(variant: :destructive) { "Delete" }
1659
+ # end
1660
+ # @see Poetry::Ui::ContextMenu::Component
1661
+ def poetry_context_menu(**, &)
1662
+ render(Poetry::Ui::ContextMenu::Component.new(**), &)
1663
+ end
1664
+
1665
+ # The filterable command-palette listbox: items DO
1666
+ # things - picking a VALUE for a form is Combobox territory.
1667
+ #
1668
+ # @example
1669
+ # poetry_command("aria-label": "Command menu") do |command|
1670
+ # command.with_item(value: "new-file") { "New file" }
1671
+ # command.with_item(value: "search") { "Search" }
1672
+ # end
1673
+ # @see Poetry::Ui::Command::Component
1674
+ def poetry_command(**, &)
1675
+ render(Poetry::Ui::Command::Component.new(**), &)
1676
+ end
1677
+
1678
+ # The ⌘K variant: a Command inside the Dialog chrome (sr-only
1679
+ # title/description) with the OPT-IN hotkey: global shortcut.
1680
+ #
1681
+ # @example
1682
+ # poetry_command_dialog(hotkey: "meta+k") do |dialog|
1683
+ # dialog.with_trigger(variant: :outline) { "Open palette" }
1684
+ # dialog.with_item(value: "settings") { "Settings" }
1685
+ # end
1686
+ # @see Poetry::Ui::Command::DialogComponent
1687
+ def poetry_command_dialog(**, &)
1688
+ render(Poetry::Ui::Command::DialogComponent.new(**), &)
1689
+ end
1690
+
1691
+ # A horizontal bar of menus, like a desktop application menu.
1692
+ #
1693
+ # @example An application command bar
1694
+ # poetry_menubar(label: "Application") do |bar|
1695
+ # bar.with_menu do |menu|
1696
+ # menu.with_trigger { "File" }
1697
+ # menu.with_item(shortcut: "⌘N") { "New" }
1698
+ # menu.with_separator
1699
+ # menu.with_item { "Print..." }
1700
+ # end
1701
+ # end
1702
+ # @see Poetry::Ui::Menubar::Component
1703
+ def poetry_menubar(**, &)
1704
+ render(Poetry::Ui::Menubar::Component.new(**), &)
1705
+ end
1706
+
1707
+ # The composed-DOM duplicate-id tripwire: a DEVELOPMENT
1708
+ # guard that scans the live page for duplicate [id] values after
1709
+ # every composition event (load, Turbo loads, frame loads, morphs,
1710
+ # stream insertions) and console-warns - the runtime complement to
1711
+ # poetry check's static stable-identity heuristics, and the only
1712
+ # check that sees real composition. Render in the development
1713
+ # layout's <head>; it emits nothing outside development unless
1714
+ # force: true (the dummy's test pages dogfood it that way).
1715
+ #
1716
+ # @example
1717
+ # <%# in the development layout's <head> %>
1718
+ # <%= poetry_id_integrity_script %>
1719
+ def poetry_id_integrity_script(force: false)
1720
+ return unless force || Rails.env.development?
1721
+
1722
+ javascript_tag(<<~JS, type: "module", nonce: true)
1723
+ import { installPoetryIdIntegrityCheck } from "@poetry/controllers/helpers/id_integrity"
1724
+ installPoetryIdIntegrityCheck()
1725
+ JS
1726
+ end
1727
+
1728
+ # The color-scheme bootstrap (the pothole other ports patch
1729
+ # into every host by hand): tokens ship `.dark` + `color-scheme`, but
1730
+ # WHEN `.dark` applies is the host's job - and it must happen before
1731
+ # first paint or every visit flashes light. Render inside <head>,
1732
+ # before the stylesheets. Also wires window.Poetry.colorScheme
1733
+ # (current/set/toggle/clear) for toggle controls; an unset preference
1734
+ # follows the OS and tracks its changes live. Full recipe:
1735
+ # the Theming guide, "Color scheme (dark mode)".
1736
+ #
1737
+ # @example
1738
+ # <%# in the layout <head>, before the stylesheets %>
1739
+ # <%= poetry_color_scheme_script %>
1740
+ def poetry_color_scheme_script
1741
+ javascript_tag(COLOR_SCHEME_JS, nonce: true)
1742
+ end
1743
+
1744
+ # The inline bootstrap poetry_color_scheme_script emits: applies the
1745
+ # stored (or OS) scheme before first paint and wires
1746
+ # window.Poetry.colorScheme.
1747
+ COLOR_SCHEME_JS = <<~JS
1748
+ (() => {
1749
+ const KEY = "poetry-color-scheme";
1750
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
1751
+ const stored = () => { try { return localStorage.getItem(KEY); } catch { return null; } };
1752
+ const current = () => stored() || (media.matches ? "dark" : "light");
1753
+ const apply = () => {
1754
+ const mode = current();
1755
+ document.documentElement.classList.toggle("dark", mode === "dark");
1756
+ document.documentElement.dispatchEvent(
1757
+ new CustomEvent("poetry:color-scheme", { bubbles: true, detail: { mode } })
1758
+ );
1759
+ };
1760
+ apply();
1761
+ media.addEventListener("change", () => { if (!stored()) apply(); });
1762
+ window.Poetry = window.Poetry || {};
1763
+ window.Poetry.colorScheme = {
1764
+ current,
1765
+ set(mode) {
1766
+ try { mode ? localStorage.setItem(KEY, mode) : localStorage.removeItem(KEY); } catch {}
1767
+ apply();
1768
+ },
1769
+ toggle() { this.set(current() === "dark" ? "light" : "dark"); },
1770
+ clear() { this.set(null); }
1771
+ };
1772
+ })();
1773
+ JS
1774
+
1775
+ private
1776
+
1777
+ # Auto-inject only when the caller supplied the pair AND didn't place
1778
+ # the field explicitly inside the block (the block is captured first,
1779
+ # so an explicit optimistic_hidden_field call wins).
1780
+ def optimistic_auto_inject?(form, attribute_name, value)
1781
+ attribute_name.present? &&
1782
+ !value.nil? &&
1783
+ !OptimisticFormBuilder::UNSET.equal?(value) &&
1784
+ !form.optimistic_hidden_field_rendered?
1785
+ end
1786
+
1787
+ # The chat-set group wrappers are dictionary ELEMENTS, not components.
1788
+ def poetry_chat_group(style, slot, **attrs, &)
1789
+ classes = [style.css(slot.tr("-", "_").split("_").last.to_sym), attrs.delete(:class)].compact.join(" ")
1790
+ tag.div(**attrs.merge(class: classes, "data-slot" => slot), &)
1791
+ end
1792
+ end
1793
+ end
1794
+ end