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,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # File/image upload chips.
6
+ module Attachment
7
+ # A file or image chip showing an upload's name, metadata, and
8
+ # lifecycle state. The server owns the state: render state:
9
+ # (idle/uploading/processing/error/done) and update it by
10
+ # re-rendering or a Turbo Stream replace - the component ships no
11
+ # upload JavaScript of its own.
12
+ #
13
+ # In-flight and error states carry a translated screen-reader
14
+ # status announcement; the visual lifecycle is pure CSS.
15
+ #
16
+ # @example A finished upload
17
+ # render Poetry::Ui::Attachment::Component.new do |attachment|
18
+ # attachment.with_title { "quarterly-report.pdf" }
19
+ # attachment.with_description { "1.2 MB" }
20
+ # end
21
+ class Component < Poetry::Core::Component
22
+ # The closed vocabulary for the state axis (the server-owned upload lifecycle).
23
+ STATES = %i[idle uploading processing error done].freeze
24
+ # The closed vocabulary for the size axis.
25
+ SIZES = %i[default sm xs].freeze
26
+ # The closed vocabulary for the orientation axis.
27
+ ORIENTATIONS = %i[horizontal vertical].freeze
28
+ # The closed vocabulary for the with_media variant.
29
+ MEDIA_VARIANTS = %i[icon image].freeze
30
+ # States that render the screen-reader status announcement.
31
+ ANNOUNCED_STATES = %i[uploading processing error].freeze
32
+
33
+ # Projected into the registry, llms.txt, and the agent surface.
34
+ AGENT_RULES = [
35
+ "State is server-owned: render data-upload-state and flip it by Turbo Stream replace - " \
36
+ "never toggle it in JS.",
37
+ "with_media(variant: :image) wraps the caller's <img>; file names and URLs are " \
38
+ "user content - never render them html_safe.",
39
+ "Actions are with_action(...) poetry Buttons (ghost/icon-xs defaults) - each needs label: (icon-only).",
40
+ "with_trigger makes the whole chip the control (a stretched overlay UNDER the " \
41
+ "actions) - don't also wrap the chip in a link.",
42
+ "error state needs a with_description explaining the failure - the tint alone is not the message."
43
+ ].freeze
44
+
45
+ renders_one :media,
46
+ doc: "Leading visual: :icon (default) boxes an icon tile, :image wraps the caller's <img>.",
47
+ renders: lambda { |variant: :icon, &block|
48
+ unless MEDIA_VARIANTS.include?(variant)
49
+ raise ArgumentError,
50
+ "media variant must be :icon or :image"
51
+ end
52
+
53
+ content_tag(:div, "data-slot" => "attachment-media", "data-variant" => variant,
54
+ class: css(:media, class: (if variant == :image
55
+ "cn-attachment-media-variant-image"
56
+ end)), &block)
57
+ }
58
+ renders_one :title, doc: "The file name line. User content - never mark it html_safe."
59
+ renders_one :description, doc: "Muted metadata under the title (size, type); in the error state, the " \
60
+ "failure explanation."
61
+ renders_many :actions,
62
+ doc: "Trailing icon actions - each renders a Button (ghost, icon-xs defaults) and requires " \
63
+ "label:.",
64
+ renders: lambda { |label:, **options, &block|
65
+ # Caller data: augments the slot marker instead of replacing it
66
+ # at the kwargs splat.
67
+ data = { slot: "attachment-action" }.merge(options.delete(:data) || {})
68
+ Button::Component.new(variant: options.delete(:variant) || :ghost,
69
+ size: options.delete(:size) || :"icon-xs",
70
+ label: label, data: data, **options, &block)
71
+ }
72
+ renders_one :trigger,
73
+ doc: "Makes the whole chip the control - a stretched button (or anchor via tag: :a, href:) " \
74
+ "layered under the actions. Don't also wrap the chip in a link.",
75
+ renders: lambda { |tag: :button, href: nil, **options, &block|
76
+ attrs = Poetry::Core::HTML::Attributes.merged(
77
+ { class: css(:trigger), "data-slot" => "attachment-trigger" }, options
78
+ )
79
+ attrs[:type] = "button" if tag == :button
80
+ attrs[:href] = href if tag == :a
81
+ content_tag(tag, attrs, &block)
82
+ }
83
+
84
+ style :size, default: :default, required: true, variants: SIZES, doc: "The chip density axis."
85
+ style :orientation, default: :horizontal, required: true, variants: ORIENTATIONS,
86
+ doc: "Row (:horizontal) or stacked thumbnail-card (:vertical) layout."
87
+
88
+ option :state, :symbol, default: :done,
89
+ doc: "The upload lifecycle state; flip it by re-render or Turbo Stream replace, " \
90
+ "never in JS."
91
+
92
+ validates :state, inclusion: { in: STATES }
93
+
94
+ part "attachment", "The chip root - the server-owned upload lifecycle rides here " \
95
+ "(flip data-upload-state by re-render / Turbo Stream replace)",
96
+ states: {
97
+ "data-upload-state" => { condition: "always - the resolved state",
98
+ values: STATES.map(&:to_s) },
99
+ "data-size" => { condition: "always - the resolved size",
100
+ values: SIZES.map(&:to_s) },
101
+ "data-orientation" => { condition: "always - the resolved orientation",
102
+ values: ORIENTATIONS.map(&:to_s) }
103
+ }
104
+ part "attachment-media", "The media slot's box - the icon tile or the caller's <img>",
105
+ states: {
106
+ "data-variant" => { condition: "always - the media variant",
107
+ values: MEDIA_VARIANTS.map(&:to_s) }
108
+ }
109
+ part "attachment-content", "Text column wrapping title/description - renders when " \
110
+ "either slot is set"
111
+ part "attachment-title", "The file name line (title slot; user content, never html_safe)"
112
+ part "attachment-description", "Muted metadata / failure copy under the title"
113
+ part "attachment-actions", "Row of with_action poetry Buttons"
114
+ part "attachment-trigger", "The whole-chip control (with_trigger: a button or tag: :a " \
115
+ "anchor) - wraps the picker/download affordance"
116
+ part "attachment-status", "sr-only role=status announcement for the in-flight and " \
117
+ "error states (uploading/processing/error)"
118
+
119
+ # Whether the current state renders the screen-reader status announcement.
120
+ # @api private
121
+ def announced?
122
+ ANNOUNCED_STATES.include?(state)
123
+ end
124
+
125
+ # @api private
126
+ def root_attributes
127
+ html_attributes.merge_if_not_set(
128
+ {
129
+ "data-slot" => "attachment", "data-upload-state" => state,
130
+ "data-size" => size, "data-orientation" => orientation
131
+ }.merge(component_data_attributes)
132
+ )
133
+ end
134
+
135
+ private :announced?, :root_attributes
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module Attachment
6
+ # The Attachment preview matrix - every lifecycle state + both
7
+ # orientations (the visual states are pure CSS on data-upload-state).
8
+ class Preview < Poetry::Core::Preview::Base
9
+ def default
10
+ render_component do |attachment|
11
+ attachment.with_media { icon(:file) }
12
+ attachment.with_title { "quarterly-report.pdf" }
13
+ attachment.with_description { "1.2 MB" }
14
+ end
15
+ end
16
+
17
+ # The whole-chip trigger control (download/open affordance).
18
+ def with_trigger
19
+ render_component do |attachment|
20
+ attachment.with_trigger(tag: :a, href: "/files/report.pdf") { "Download" }
21
+ attachment.with_media { icon(:file) }
22
+ attachment.with_title { "quarterly-report.pdf" }
23
+ attachment.with_description { "1.2 MB" }
24
+ end
25
+ end
26
+
27
+ def uploading
28
+ render_component(state: :uploading) do |attachment|
29
+ attachment.with_media { icon(:file) }
30
+ attachment.with_title { "photo.png" }
31
+ attachment.with_description { "uploading…" }
32
+ end
33
+ end
34
+
35
+ def error
36
+ render_component(state: :error) do |attachment|
37
+ attachment.with_media { icon(:"circle-alert") }
38
+ attachment.with_title { "notes.txt" }
39
+ attachment.with_description { "Too large (max 10 MB)" }
40
+ end
41
+ end
42
+
43
+ def idle_dropzone
44
+ render_component(state: :idle) do |attachment|
45
+ attachment.with_media { icon(:plus) }
46
+ attachment.with_title { "Add a file" }
47
+ end
48
+ end
49
+
50
+ # The compact rows cover the size axis: sm for dense lists,
51
+ # xs for inline chips.
52
+ def size_sm
53
+ render_component(size: :sm) do |attachment|
54
+ attachment.with_media { icon(:file) }
55
+ attachment.with_title { "invoice.pdf" }
56
+ attachment.with_description { "84 KB" }
57
+ end
58
+ end
59
+
60
+ def size_xs
61
+ render_component(size: :xs) do |attachment|
62
+ attachment.with_media { icon(:file) }
63
+ attachment.with_title { "readme.md" }
64
+ end
65
+ end
66
+
67
+ def vertical_with_actions
68
+ render_component(orientation: :vertical) do |attachment|
69
+ attachment.with_media(variant: :image) { icon(:image) }
70
+ attachment.with_title { "screenshot.png" }
71
+ attachment.with_action(label: "Remove") { icon(:x) }
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def icon(name)
78
+ embed(Icon::Component.new(name: name))
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module Attachment
6
+ # Re-expressed through the cn-* theme layer. The upload
7
+ # lifecycle stays PURE CSS and INLINE per upstream's own split (idle
8
+ # dashes, error tints, the in-flight image dim) - it is coupled to
9
+ # the data-[upload-state] machine; sizes/orientation/surface ride the
10
+ # theme. w-fit moved theme-side WITH the vertical w-24 (split-side).
11
+ class Style < Poetry::Core::Style
12
+ base "cn-attachment group/attachment relative flex max-w-full min-w-0 shrink-0 flex-wrap border " \
13
+ "bg-card text-card-foreground transition-colors " \
14
+ "has-[>a,>button]:hover:bg-muted/50 data-[upload-state=error]:border-destructive/30 " \
15
+ "data-[upload-state=idle]:border-dashed"
16
+
17
+ variant :size, {
18
+ default: "cn-attachment-size-default",
19
+ sm: "cn-attachment-size-sm",
20
+ xs: "cn-attachment-size-xs"
21
+ }
22
+
23
+ variant :orientation, {
24
+ horizontal: "cn-attachment-orientation-horizontal items-center",
25
+ vertical: "cn-attachment-orientation-vertical flex-col"
26
+ }
27
+
28
+ element :media, "cn-attachment-media relative flex aspect-square shrink-0 items-center " \
29
+ "justify-center overflow-hidden " \
30
+ "group-data-[upload-state=error]/attachment:bg-destructive/10 " \
31
+ "group-data-[upload-state=error]/attachment:text-destructive " \
32
+ "group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! " \
33
+ "[&_svg]:pointer-events-none " \
34
+ "data-[variant=image]:opacity-60 " \
35
+ "data-[variant=image]:group-data-[upload-state=done]/attachment:opacity-100 " \
36
+ "data-[variant=image]:group-data-[upload-state=idle]/attachment:opacity-100 " \
37
+ "data-[variant=image]:*:[img]:aspect-square data-[variant=image]:*:[img]:w-full " \
38
+ "data-[variant=image]:*:[img]:object-cover"
39
+
40
+ element :content, "cn-attachment-content max-w-full min-w-0 flex-1"
41
+
42
+ element :title, "cn-attachment-title block max-w-full min-w-0 truncate " \
43
+ "group-data-[upload-state=processing]/attachment:shimmer " \
44
+ "group-data-[upload-state=uploading]/attachment:shimmer"
45
+
46
+ element :description, "cn-attachment-description block max-w-full min-w-0 truncate " \
47
+ "group-data-[upload-state=error]/attachment:text-destructive/80 text-muted-foreground"
48
+
49
+ element :actions, "cn-attachment-actions flex shrink-0 items-center"
50
+
51
+ element :trigger, "cn-attachment-trigger"
52
+
53
+ element :group, "cn-attachment-group flex min-w-0 scroll-fade-x snap-x snap-mandatory " \
54
+ "scrollbar-none overflow-x-auto overscroll-x-contain " \
55
+ "*:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start"
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,16 @@
1
+ <%= content_tag(:div, **root_attributes.to_attributes) do %>
2
+ <%= tag.input(**input_attributes, class: Poetry::Ui::Input::Style.css) %>
3
+ <%= tag.div(**content_attributes) do %>
4
+ <div data-slot="autocomplete-list" role="listbox" id="<%= list_id %>"
5
+ class="<%= Poetry::Ui::Command::Style.css(:list) %>"
6
+ <%= tag.attributes(stimulus_attributes_for(:list)) %>>
7
+ <% item_models.each_with_index do |item, index| %>
8
+ <%= tag.div(**item_attributes(item, index)) do %>
9
+ <span><%= item.label %></span>
10
+ <% end %>
11
+ <% end %>
12
+ </div>
13
+ <div data-slot="autocomplete-empty" class="<%= Poetry::Ui::Command::Style.css(:empty) %>" hidden
14
+ <%= tag.attributes(stimulus_attributes_for(:empty)) %>><%= empty_text %></div>
15
+ <% end %>
16
+ <% end %>
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # Free-text inputs with filtering suggestion popups.
6
+ module Autocomplete
7
+ # A text input that suggests from a server-rendered list, filtered
8
+ # as you type, while the text itself stays the form value: free text
9
+ # submits as an ordinary param, and picking a suggestion writes it
10
+ # into the input and closes the popup. When the value must be a
11
+ # selected item rather than free text, use Combobox instead.
12
+ #
13
+ # @example Free text with suggestions
14
+ # render Poetry::Ui::Autocomplete::Component.new(name: "tag", label: "Search tags") do |auto|
15
+ # auto.with_item(label: "feature")
16
+ # auto.with_item(label: "fix")
17
+ # end
18
+ class Component < Poetry::Core::Component
19
+ # Projected into the registry, llms.txt, and the agent surface.
20
+ AGENT_RULES = [
21
+ "The input IS the value: name: is the param key and free text submits as-is - " \
22
+ "suggestions are conveniences, not constraints (constrained pick = Combobox).",
23
+ "Items via with_item(label:) - label is what filtering matches and what commit " \
24
+ "writes; value: overrides the committed text when it differs from the label.",
25
+ "empty_text: renders the no-matches state (hidden while anything matches).",
26
+ "open_on_focus: false waits for typing before suggesting.",
27
+ "Server-side filtering stays yours: render fewer items on re-render - the client " \
28
+ "filter only narrows what the server sent."
29
+ ].freeze
30
+
31
+ use_stimulus do
32
+ on :root do
33
+ controller :autocomplete do
34
+ register
35
+ value :open_on_focus, "false", if: -> { !open_on_focus }
36
+ end
37
+ controller(:popper) { register }
38
+ end
39
+ on :input do
40
+ controller :autocomplete do
41
+ target :input
42
+ action :input, on: :input
43
+ action :focus, on: :focus
44
+ action :blurred, on: :focusout
45
+ action :keydown, on: :keydown
46
+ end
47
+ controller(:popper) { target :anchor }
48
+ end
49
+ on :content do
50
+ controller(:autocomplete) { target :content }
51
+ controller(:popper) { target :content }
52
+ end
53
+ on :list do
54
+ controller(:autocomplete) { target :list }
55
+ end
56
+ on :empty do
57
+ controller(:autocomplete) { target :empty }
58
+ end
59
+ on :item do
60
+ controller :autocomplete do
61
+ action :itemPress, on: :pointerdown
62
+ action :itemEnter, on: :pointerenter
63
+ end
64
+ end
65
+ end
66
+
67
+ option :name, :string, required: true, doc: "The form param key; the input's text submits under it as-is."
68
+ option :value, :string, doc: "The initial input text."
69
+ option :id, :string, doc: "Stable DOM id token for the root and list ids."
70
+ option :placeholder, :string, doc: "Placeholder text shown while the input is empty."
71
+ option :label, :string, doc: "The accessible name (or wire aria-labelledby via html attrs)."
72
+ option :empty_text, :string, default: "No results.",
73
+ doc: "The no-matches message; hidden while anything matches."
74
+ option :open, :boolean, default: false, doc: "Server-renders the suggestion popup open."
75
+ option :open_on_focus, :boolean, default: true, doc: "Opens the suggestions on focus; false waits for typing."
76
+
77
+ part "autocomplete", "Root wrapper carrying the controller + popper pair"
78
+ part "autocomplete-input", "The REAL text input - role=combobox with aria-expanded " \
79
+ "tracking the popup, the form value itself"
80
+ part "autocomplete-content", "The popper-positioned popup shell",
81
+ states: {
82
+ "data-open" => "popup visible",
83
+ "data-closed" => "popup hidden (the server-rendered default)",
84
+ "data-empty" => "no item matches the query - the empty state shows"
85
+ }
86
+ part "autocomplete-list", "role=listbox holding the options"
87
+ part "autocomplete-item", "One suggestion - role=option; commit writes its label " \
88
+ "(or value:) into the input",
89
+ states: {
90
+ "data-label" => "always - what filtering matches and commit writes",
91
+ "data-value" => "value: given - overrides the committed text",
92
+ "data-highlighted" => "the keyboard/pointer highlight",
93
+ "data-disabled" => "disabled: - skipped by filtering and commit"
94
+ }
95
+ part "autocomplete-empty", "The no-matches message (hidden while anything matches)"
96
+
97
+ # Forces the composition block so with_item calls land before the template renders.
98
+ # @api private
99
+ def before_render
100
+ content
101
+ end
102
+
103
+ # Hand-rolled builder: ViewComponent lambda slots nil-wrap
104
+ # non-component returns, so the builder collects plain models and
105
+ # before_render forces the composition block.
106
+
107
+ # Adds one suggestion to the popup list.
108
+ #
109
+ # @param label [String] what filtering matches and what commit writes into the input
110
+ # @param value [String, nil] overrides the committed text when it differs from the label
111
+ # @param disabled [Boolean] skipped by filtering and commit
112
+ # @param highlighted [Boolean] server-renders the item highlighted
113
+ def with_item(label:, value: nil, disabled: false, highlighted: false)
114
+ item_models << Item.new(label: label, value: value, disabled: disabled,
115
+ highlighted: highlighted)
116
+ self
117
+ end
118
+
119
+ # The collected suggestion models.
120
+ # @api private
121
+ def item_models = (@item_models ||= [])
122
+
123
+ # @api private
124
+ def autocomplete_id
125
+ @autocomplete_id ||= if (token = dom_id_token(id))
126
+ "poetry-autocomplete-#{token}"
127
+ else
128
+ poetry_instance_id("poetry-autocomplete")
129
+ end
130
+ end
131
+
132
+ # @api private
133
+ def list_id = "#{autocomplete_id}-list"
134
+
135
+ # @api private
136
+ def root_attributes
137
+ html_attributes.merge_if_not_set(
138
+ { "class" => css, "data-slot" => "autocomplete", "id" => autocomplete_id }
139
+ .merge(stimulus_attributes_for(:root))
140
+ .merge(component_data_attributes)
141
+ )
142
+ end
143
+
144
+ # @api private
145
+ def input_attributes
146
+ attrs = {
147
+ name: name, value: value, placeholder: placeholder, type: :text,
148
+ autocomplete: "off", role: "combobox",
149
+ "aria-autocomplete": "list", "aria-expanded": open.to_s, "aria-controls": list_id,
150
+ "data-slot": "autocomplete-input"
151
+ }
152
+ attrs[:"aria-label"] = label if label.present?
153
+ attrs.merge(stimulus_attributes_for(:input).transform_keys(&:to_sym))
154
+ end
155
+
156
+ # @api private
157
+ def content_attributes
158
+ attrs = {
159
+ "class" => css(:content), "data-slot" => "autocomplete-content"
160
+ }
161
+ if open
162
+ attrs["data-open"] = ""
163
+ else
164
+ attrs["data-closed"] = ""
165
+ attrs["hidden"] = "hidden"
166
+ end
167
+ attrs.merge(stimulus_attributes_for(:content))
168
+ end
169
+
170
+ # @api private
171
+ def item_attributes(item, index)
172
+ attrs = {
173
+ "class" => Poetry::Ui::Command::Style.css(:item), "data-slot" => "autocomplete-item",
174
+ "id" => "#{autocomplete_id}-item-#{index}", "role" => "option",
175
+ "data-label" => item.label
176
+ }
177
+ attrs["data-value"] = item.value if item.value.present?
178
+ attrs["data-highlighted"] = "" if item.highlighted
179
+ attrs["data-disabled"] = "" if item.disabled
180
+ attrs["aria-disabled"] = "true" if item.disabled
181
+ attrs.merge(stimulus_attributes_for(:item))
182
+ end
183
+
184
+ # One suggestion's plain data model, collected by with_item.
185
+ # @api private
186
+ Item = Struct.new(:label, :value, :disabled, :highlighted, keyword_init: true)
187
+
188
+ private :item_models, :autocomplete_id, :list_id, :root_attributes, :input_attributes, :content_attributes
189
+ private :item_attributes
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module Autocomplete
6
+ # The Autocomplete matrix: the closed hero, the server-rendered open
7
+ # state with a highlight (the contract carrier for data-open /
8
+ # data-highlighted), a value-override item, and the empty state.
9
+ class Preview < Poetry::Core::Preview::Base
10
+ def default
11
+ render_component(name: "tag", placeholder: "e.g. feature", label: "Search tags",
12
+ class: "w-64") do |auto|
13
+ auto.with_item(label: "feature")
14
+ auto.with_item(label: "fix")
15
+ auto.with_item(label: "docs")
16
+ auto.with_item(label: "internal")
17
+ auto.with_item(label: "mobile", disabled: true)
18
+ end
19
+ end
20
+
21
+ # Server-rendered open + highlighted (the states the contract holds).
22
+ def open_and_highlighted
23
+ render_component(name: "city", value: "sp", open: true, label: "City",
24
+ class: "w-64") do |auto|
25
+ auto.with_item(label: "Springfield", highlighted: true)
26
+ auto.with_item(label: "Spokane")
27
+ auto.with_item(label: "São Paulo", value: "Sao Paulo")
28
+ end
29
+ end
30
+
31
+ # No items at all: the empty message is the whole popup.
32
+ def empty_state
33
+ render_component(name: "search", open: true, label: "Search", open_on_focus: false,
34
+ empty_text: "Nothing matches.", class: "w-64")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module Autocomplete
6
+ # The Autocomplete wears the Combobox/Command visual language (the
7
+ # popup shell and item treatments are the same family upstream);
8
+ # no autocomplete-specific theme rules exist in the upstream
9
+ # dictionaries, so every class here is already-emitted vocabulary.
10
+ class Style < Poetry::Core::Style
11
+ base "relative"
12
+
13
+ element :content, "cn-combobox-content cn-menu-translucent z-50 w-(--anchor-width) " \
14
+ "origin-(--transform-origin) outline-hidden"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # Person images with initials fallbacks.
6
+ module Avatar
7
+ # A person's image over an always-rendered initials fallback. The
8
+ # image layers absolutely above the fallback with an empty alt, so a
9
+ # failed load paints nothing and the initials show through - zero
10
+ # JS, no layout shift. The accessible name lives on the root
11
+ # (role=img + aria-label via label:), never on the layered img.
12
+ #
13
+ # @example Image with initials fallback
14
+ # render Poetry::Ui::Avatar::Component.new(src: user.avatar_url, label: "Ada Lovelace") { "AL" }
15
+ class Component < Poetry::Core::Component
16
+ requires_content "the initials fallback"
17
+
18
+ # The closed vocabulary for the size axis.
19
+ SIZES = %i[default sm lg].freeze
20
+
21
+ # Projected into the registry, llms.txt, and the agent surface.
22
+ AGENT_RULES = [
23
+ "label: (the person's name) is REQUIRED - it is the avatar's accessible name (role=img).",
24
+ "The content block is the fallback (initials) and is also required - it is what shows " \
25
+ "while the image loads or when it fails.",
26
+ "The badge slot is decorative (a presence dot); put the status meaning in label:, " \
27
+ "not in the badge.",
28
+ "Stack avatars with poetry_avatar_group; the overflow count is poetry_avatar_group_count."
29
+ ].freeze
30
+
31
+ renders_one :badge, doc: "Decorative presence dot, bottom-right; keep the status meaning in label:."
32
+
33
+ option :src, :string, doc: "The image URL; without it only the initials fallback shows."
34
+ option :label, :string, required: true,
35
+ doc: "The person's name - the avatar's accessible name (blank raises). The required " \
36
+ "flag also carries the fact to the registry so static checks see it."
37
+ option :size, :symbol, default: :default, doc: "The diameter axis."
38
+
39
+ validates :size, inclusion: { in: SIZES }
40
+
41
+ part "avatar", "Root span (role=img carrying the accessible name) - fallback, image, " \
42
+ "and badge layer inside it",
43
+ states: {
44
+ "data-size" => { condition: "always - the resolved size", values: SIZES.map(&:to_s) }
45
+ }
46
+ part "avatar-fallback", "The initials layer (the content block) - always in the DOM, " \
47
+ "showing until the image covers it"
48
+ part "avatar-image", "The <img> layered absolutely over the fallback - only when src: " \
49
+ "is given; a failed load paints nothing"
50
+ part "avatar-badge", "The decorative presence dot (the badge slot), bottom-right"
51
+
52
+ # Enforces label: and the initials content block.
53
+ # @api private
54
+ def before_render
55
+ raise ArgumentError, "Avatar requires label: (the person's name - its accessible name)" if label.blank?
56
+
57
+ ensure_content!
58
+ end
59
+
60
+ # @api private
61
+ def call
62
+ content_tag(:span, root_attributes.to_attributes) do
63
+ safe_join([fallback, image, badge_part].compact)
64
+ end
65
+ end
66
+
67
+ # @api private
68
+ def root_attributes
69
+ html_attributes.merge_if_not_set(
70
+ {
71
+ "data-slot" => "avatar", "data-size" => size,
72
+ "role" => "img", "aria-label" => label
73
+ }.merge(component_data_attributes)
74
+ )
75
+ end
76
+
77
+ private
78
+
79
+ def fallback
80
+ content_tag(:span, content, "data-slot" => "avatar-fallback", "aria-hidden" => "true",
81
+ class: css(:fallback))
82
+ end
83
+
84
+ def image
85
+ return if src.blank?
86
+
87
+ # alt="" on purpose: the root carries the name, and an empty alt
88
+ # keeps a FAILED load from painting a broken-image glyph over the
89
+ # initials - the layered-fallback contract.
90
+ tag.img(src: src, alt: "", "data-slot": "avatar-image", class: css(:image))
91
+ end
92
+
93
+ def badge_part
94
+ return unless badge?
95
+
96
+ content_tag(:span, badge, "data-slot" => "avatar-badge", "aria-hidden" => "true", class: css(:badge))
97
+ end
98
+
99
+ private :root_attributes
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,11 @@
1
+ <%# The stacked group: overlapping avatars ringed by the background, with
2
+ the overflow count - the poetry_avatar_group / _group_count part
3
+ helpers. %>
4
+ <div class="p-4">
5
+ <%= poetry_avatar_group do %>
6
+ <%= poetry_avatar(label: "Grace Hopper") { "GH" } %>
7
+ <%= poetry_avatar(label: "Ada Lovelace") { "AL" } %>
8
+ <%= poetry_avatar(label: "Annie Easley") { "AE" } %>
9
+ <%= poetry_avatar_group_count { "+3" } %>
10
+ <% end %>
11
+ </div>