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,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module Drawer
6
+ # The Drawer preview: the bottom sheet with its grab handle (the
7
+ # gesture's discoverability affordance), and a right-edge panel.
8
+ # Previews render CLOSED (the trigger page, the Dialog convention);
9
+ # the dommy tier drives the open/swipe behavior.
10
+ class Preview < Poetry::Core::Preview::Base
11
+ def default
12
+ render_component(show_swipe_handle: true) do |drawer|
13
+ drawer.with_trigger { "Open drawer" }
14
+ drawer.with_title { "Move goal" }
15
+ drawer.with_description { "Set your daily activity goal." }
16
+ drawer.with_footer do
17
+ embed(Poetry::Ui::Button::Component.new.with_content("Submit"))
18
+ end
19
+ "Drag the handle down to dismiss."
20
+ end
21
+ end
22
+
23
+ def right_edge
24
+ render_component(direction: :right) do |drawer|
25
+ drawer.with_trigger { "Open panel" }
26
+ drawer.with_title { "Filters" }
27
+ "Swipe right to dismiss."
28
+ end
29
+ end
30
+
31
+ def top_sheet
32
+ render_component(direction: :up) do |drawer|
33
+ drawer.with_trigger { "Open notice" }
34
+ drawer.with_title { "Service notice" }
35
+ "Swipe up to dismiss."
36
+ end
37
+ end
38
+
39
+ def left_edge
40
+ render_component(direction: :left) do |drawer|
41
+ drawer.with_trigger { "Open navigation" }
42
+ drawer.with_title { "Navigate" }
43
+ "Swipe left to dismiss."
44
+ end
45
+ end
46
+
47
+ # modal: false keeps the page interactive behind the drawer -
48
+ # Esc rides its own keydown exit (a non-modal dialog never fires
49
+ # cancel).
50
+ def non_modal
51
+ render_component(modal: false) do |drawer|
52
+ drawer.with_trigger { "Open player" }
53
+ drawer.with_title { "Now playing" }
54
+ "The page stays interactive behind this drawer."
55
+ end
56
+ end
57
+
58
+ # snap_points: the swipe physics settle at fractional heights.
59
+ def with_snap_points
60
+ render_component(snap_points: [0.4, 1]) do |drawer|
61
+ drawer.with_trigger { "Open details" }
62
+ drawer.with_title { "Details" }
63
+ "Drag the handle - the sheet settles at 40% or full height."
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module Drawer
6
+ # Re-expressed through the cn-* theme layer, still the
7
+ # native-dialog swipe spine. The ENTIRE swipe mechanism stays inline
8
+ # (transform vars, swipe-progress backdrop math, starting/ending
9
+ # transforms, the direction margin geometry + closed-transform vars)
10
+ # - a swapped theme can restyle surfaces but never break the gesture.
11
+ # Margins stay dictionary-side ON BOTH content and directions so the
12
+ # merger keeps collapsing m-0 against the side margins exactly as
13
+ # before (the split-side conflict rule, applied by keeping the
14
+ # conflict on the inline side this time). Corners/edges/surface ride
15
+ # the theme. Snap points landed 2026-08-01; nested-stack visuals
16
+ # and bleed stay deferred.
17
+ class Style < Poetry::Core::Style
18
+ # open:flex, not flex: never defeat the UA's dialog:not([open])
19
+ # display:none (the Dialog browser-pass lesson).
20
+ # Scrim color + blur are theme-owned: each
21
+ # themes/<name>.css carries backdrop:bg-* / backdrop-blur-* on
22
+ # .cn-drawer-content. Only the swipe machinery (transition +
23
+ # opacity calc) stays structural here.
24
+ # No `relative`: the top layer discards it and the UA reasserts
25
+ # `absolute`, pinning the drawer to the DOCUMENT edge (it scrolls
26
+ # off-screen below the fold). The UA `:modal` rule keeps it
27
+ # viewport-fixed so the direction auto-margins pin it to the viewport
28
+ # edge; the swipe transform rides on top.
29
+ element :content,
30
+ "cn-drawer-content m-0 open:flex min-h-0 flex-col " \
31
+ "outline-none select-none will-change-transform " \
32
+ "transition-[transform,opacity] duration-450 ease-[cubic-bezier(0.22,1,0.36,1)] " \
33
+ "transform-[translate3d(var(--translate-x,0px),var(--translate-y,0px),0)] " \
34
+ "data-swiping:duration-0 " \
35
+ "data-starting-style:transform-(--closed-transform) " \
36
+ "data-ending-style:transform-(--closed-transform) " \
37
+ "data-ending-style:duration-[calc(var(--drawer-swipe-strength,1)*400ms)] " \
38
+ "backdrop:transition-opacity backdrop:duration-450 " \
39
+ "backdrop:opacity-[calc(1-var(--drawer-swipe-progress,0))] " \
40
+ "data-swiping:backdrop:transition-none"
41
+
42
+ # The dismiss-direction geometry: top-layer edge margins + the
43
+ # direction's closed transform, with the controller's positive
44
+ # movement mapped onto the axis (negative directions invert it).
45
+ # The corner/edge treatment rides cn-drawer-direction-*.
46
+ # --drawer-inset (theme-owned; upstream's "floats the drawer from
47
+ # the viewport edges", default 0px = flush) gaps every non-anchored
48
+ # edge: the anchored side swaps its 0-margin for the inset, the
49
+ # cross axis swaps its explicit size (w-full/h-full) for auto so
50
+ # the UA's inset:0 over-constraint fills viewport-minus-margins
51
+ # (identical to the full size at 0px), and the closed transform
52
+ # travels the extra inset so the exit still clears the viewport.
53
+ # rhea/mira/luma/maia ship --spacing(2), like their source styles.
54
+ variant :direction, {
55
+ # down/up wear h-fit, never h-auto: the UA :modal sets top:0 AND
56
+ # bottom:0, and an abspos box with both insets and height:auto
57
+ # SOLVES height to fill (CSS2 10.6.4) - h-auto blew every bottom
58
+ # sheet up to its max-h cap. fit-content restores the UA's own
59
+ # content sizing; the auto margin still bottom-anchors. The
60
+ # x-axis directions keep h-auto: there fill IS the intent, the
61
+ # edge panel runs the full viewport.
62
+ down: "cn-drawer-direction-down mt-auto mb-(--drawer-inset,0px) mx-(--drawer-inset,0px) " \
63
+ "h-fit w-auto max-w-none max-h-[calc(100dvh-6rem)] " \
64
+ "data-snap-points:h-dvh data-snap-points:max-h-none " \
65
+ "[--closed-transform:translate3d(0,calc(100%+var(--drawer-inset,0px)+2px),0)] " \
66
+ "[--translate-y:calc(var(--drawer-snap-point-offset,0px)+var(--drawer-swipe-movement-y,0px))]",
67
+ up: "cn-drawer-direction-up mb-auto mt-(--drawer-inset,0px) mx-(--drawer-inset,0px) " \
68
+ "h-fit w-auto max-w-none max-h-[calc(100dvh-6rem)] " \
69
+ "[--closed-transform:translate3d(0,calc(-100%-var(--drawer-inset,0px)-2px),0)] " \
70
+ "[--translate-y:calc(-1*var(--drawer-swipe-movement-y,0px))]",
71
+ left: "cn-drawer-direction-left mr-auto ml-(--drawer-inset,0px) my-(--drawer-inset,0px) " \
72
+ "h-auto max-h-none w-3/4 sm:max-w-sm " \
73
+ "[--closed-transform:translate3d(calc(-100%-var(--drawer-inset,0px)-2px),0,0)] " \
74
+ "[--translate-x:calc(-1*var(--drawer-swipe-movement-x,0px))]",
75
+ right: "cn-drawer-direction-right ml-auto mr-(--drawer-inset,0px) my-(--drawer-inset,0px) " \
76
+ "h-auto max-h-none w-3/4 sm:max-w-sm " \
77
+ "[--closed-transform:translate3d(calc(100%+var(--drawer-inset,0px)+2px),0,0)] " \
78
+ "[--translate-x:var(--drawer-swipe-movement-x,0px)]"
79
+ }
80
+
81
+ # The grab pill (touch-none keeps the browser from stealing the
82
+ # drag for scroll); the pill's look rides the theme rule.
83
+ element :handle, "cn-drawer-swipe-handle relative z-10 mx-auto flex h-3 w-full shrink-0 " \
84
+ "cursor-grab touch-none items-center justify-center active:cursor-grabbing " \
85
+ "after:block after:shrink-0"
86
+
87
+ element :header, "cn-drawer-header flex shrink-0 flex-col"
88
+ # Non-modal show() skips the top layer; the UA :modal positioning
89
+ # must be restated (dictionary-resident so safelists ship it).
90
+ element :nonmodal, "fixed inset-0 z-50"
91
+ element :footer, "cn-drawer-footer mt-auto flex shrink-0 flex-col"
92
+ element :title, "cn-drawer-title"
93
+ element :description, "cn-drawer-description text-balance"
94
+ element :body, "cn-drawer-body flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain"
95
+
96
+ # The direction's edge classes for the <dialog> element (the Sheet
97
+ # resolver pattern: variants render at the dictionary root, and the
98
+ # Drawer's visual root is the :content element).
99
+ def self.direction(value)
100
+ resolver.variants.fetch(:direction).fetch(value.to_sym)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,6 @@
1
+ <%= content_tag(:div, **root_attributes.to_attributes) do %>
2
+ <%= trigger %>
3
+ <%= content_tag(:div, **Poetry::Core::HTML::Attributes.new(content_attributes).to_attributes) do %>
4
+ <% items.each do |item| %><%= item %><% end %>
5
+ <% end %>
6
+ <% end %>
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # The DropdownMenu family - the button-triggered action menu.
6
+ module DropdownMenu
7
+ # The closed vocabulary for the side placement axis.
8
+ SIDES = %i[top right bottom left].freeze
9
+ # The closed vocabulary for the align placement axis.
10
+ ALIGNS = %i[start center end].freeze
11
+ # The closed vocabulary for the dir (reading direction) axis.
12
+ DIRS = %i[ltr rtl].freeze
13
+
14
+ # The shared menus item union wearing this family's identity -
15
+ # SLOT_BUILDERS is added in a reopen at the bottom of the file,
16
+ # once the family classes it names exist.
17
+ module ItemSlots
18
+ extend ActiveSupport::Concern
19
+ include Poetry::Ui::Menus::ItemSlots
20
+ end
21
+
22
+ # A dropdown menu: a button that opens a popup list of actions
23
+ # (role=menu), composed from items, checkbox items, radio groups,
24
+ # labels, separators, groups, and nested submenus. Reach for it
25
+ # when one control offers several actions; choosing a form VALUE
26
+ # belongs to Select or Combobox instead.
27
+ #
28
+ # Requires with_trigger (the menu button) and at least one item.
29
+ # Arrow-key navigation, typeahead, and focus containment activate
30
+ # when the menu opens; the closed menu renders inert and hidden.
31
+ #
32
+ # @example A menu button with actions
33
+ # render Poetry::Ui::DropdownMenu::Component.new do |menu|
34
+ # menu.with_trigger(variant: :outline) { "Open" }
35
+ # menu.with_item { "Rename" }
36
+ # menu.with_item(variant: :destructive) { "Delete" }
37
+ # end
38
+ class Component < Poetry::Core::Component
39
+ include Poetry::Ui::ComposableTrigger
40
+
41
+ include ItemSlots
42
+
43
+ # Projected into the registry, llms.txt, and the agent surface.
44
+ AGENT_RULES = [
45
+ ComposableTrigger::AGENT_RULE,
46
+ "Use poetry_dropdown_menu - never hand-roll role=menu popups with Tailwind.",
47
+ "Items are ACTIONS. Choosing a form VALUE is a Select/Combobox - do not fake it with radio items.",
48
+ "Navigation items pass with_item(href:) (external: for a new tab); a form action (sign-out, a " \
49
+ "DELETE) passes with_item(submit:, method:). The item renders AS the anchor / submit button " \
50
+ "(role=menuitem on the <a> or <button>) - one interactive element - so NEVER nest a link_to or " \
51
+ "button_to inside an item.",
52
+ "Icon-only triggers MUST have an accessible name (the composed Button's label: rule).",
53
+ "Never write the state attributes (data-popup-open / data-checked / data-unchecked) without " \
54
+ "their aria twin (aria-expanded / aria-checked) - the controller writes both; agents patching " \
55
+ "DOM must too.",
56
+ "Destructive items use variant: :destructive AND still confirm irreversible actions via a dialog.",
57
+ "shortcut: is a visual hint only - it does NOT bind the key; wire a real hotkey separately or omit it.",
58
+ "Do not nest interactive elements inside items (a menuitem IS the interactive unit).",
59
+ "Keep submenus <= 2 levels; prefer grouping + separators over deep nesting.",
60
+ "Critical actions must exist somewhere reachable without JS (menus are JS-required interaction)."
61
+ ].freeze
62
+
63
+ # Slot-to-component map: with_trigger renders a Button, so callers
64
+ # get Button's full option and slot contract on the trigger.
65
+ SLOT_RENDERS = { trigger: Button::Component }.freeze
66
+
67
+ renders_one :trigger,
68
+ doc: "The menu button - a poetry Button (options forward to it, e.g. variant: :outline). The " \
69
+ "slot owns the aria-haspopup/expanded/ controls wiring regardless of the composed content, " \
70
+ "so composition cannot drop the aria.",
71
+ renders: lambda { |**options, &block|
72
+ wiring = {
73
+ "id" => trigger_id, "data-slot" => "dropdown-menu-trigger",
74
+ "aria-haspopup" => "menu", "aria-expanded" => open.to_s, "aria-controls" => content_id
75
+ }.merge(stimulus_attributes_for(:trigger))
76
+ # Trigger state: bare data-popup-open while open, NO attribute
77
+ # while closed (absence IS the state).
78
+ wiring["data-popup-open"] = "" if open
79
+ composed_trigger(wiring, options, &block) || begin
80
+ options[:disabled] = true if disabled && !options.key?(:disabled)
81
+ Button::Component.new(**wiring, **options, &block)
82
+ end
83
+ }
84
+
85
+ # The content's layer behaviors (focus containment, dismissal,
86
+ # roving focus) are attached by the menu controller on open rather
87
+ # than declared statically here - a statically-connected focus
88
+ # trap on a hidden menu would steal focus at page load.
89
+ use_stimulus do
90
+ on :root do
91
+ controller :menu do
92
+ register
93
+ value :open
94
+ value :modal
95
+ value :loop
96
+ end
97
+ controller :popper do
98
+ register
99
+ value :side
100
+ value :align
101
+ value :side_offset
102
+ value :align_offset
103
+ value :avoid_collisions
104
+ end
105
+ end
106
+ on :trigger do
107
+ controller :menu do
108
+ action :toggle, on: :click
109
+ action :trigger_keydown, on: :keydown
110
+ end
111
+ controller(:popper) { target :anchor }
112
+ end
113
+ on :content do
114
+ controller(:popper) { target :content }
115
+ end
116
+ # Anatomy-rendered wiring, declared here so the contract covers
117
+ # the whole family surface (items + submenus render via the
118
+ # escape hatch in the anatomy classes).
119
+ on :item do
120
+ controller(:menu) { action :activate, on: :click }
121
+ end
122
+ on :sub_trigger do
123
+ controller :menu do
124
+ action :sub_enter, on: :pointerenter
125
+ action :sub_leave, on: :pointerleave
126
+ action :open_sub, on: :click
127
+ end
128
+ controller(:popper) { target :anchor }
129
+ end
130
+ end
131
+
132
+ option :open, :boolean, default: false, doc: "Renders the menu already open on page load."
133
+ option :modal, :boolean, default: true,
134
+ doc: "While open, pointer interaction outside the menu is blocked; false keeps the " \
135
+ "rest of the page interactive."
136
+ option :side, :symbol, default: :bottom,
137
+ doc: "Which side of the trigger the menu opens on (flips on collision)."
138
+ option :align, :symbol, default: :center, doc: "The menu's alignment against the trigger's edge."
139
+ option :side_offset, :integer, default: 4, doc: "Gap in pixels between the trigger and the menu."
140
+ option :align_offset, :integer, default: 0, doc: "Pixel shift along the alignment edge."
141
+ option :avoid_collisions, :boolean, default: true,
142
+ doc: "Flips/shifts placement to keep the menu inside the viewport."
143
+ option :loop, :boolean, default: false, doc: "Arrow-key navigation wraps from the last item back to the first."
144
+ option :dir, :symbol, doc: "Reading direction; :rtl flips submenu sides and indicators."
145
+ option :disabled, :boolean, default: false, doc: "Disables the menu trigger button."
146
+
147
+ validates :side, inclusion: { in: SIDES }
148
+ validates :align, inclusion: { in: ALIGNS }
149
+ validates :dir, inclusion: { in: DIRS }, allow_nil: true
150
+
151
+ # (dropdown-menu-trigger rides the composed Button, so that element
152
+ # belongs to Button's anatomy, not this contract.)
153
+ part "dropdown-menu", "Root wrapper hosting the menu + popper controllers around the trigger " \
154
+ "and content"
155
+ part "dropdown-menu-content", "The role=menu popup panel - positioning, animation, and the " \
156
+ "open state ride here",
157
+ states: {
158
+ "data-open" => "menu is open (presence flips the pair at runtime)",
159
+ "data-closed" => "menu is closed or animating out (the server-rendered state)",
160
+ "data-side" => { condition: "the placement side (popper re-writes it after collision flips)",
161
+ values: %w[top right bottom left] },
162
+ "data-align" => { condition: "the alignment against the trigger (popper re-resolves it)",
163
+ values: %w[start center end] }
164
+ },
165
+ vars: {
166
+ "--transform-origin" => "popper's anchor-facing animation origin",
167
+ "--available-width" => "popper: viewport space left for the panel (post-flip)",
168
+ "--available-height" => "popper: viewport space left for the panel (post-flip)",
169
+ "--anchor-width" => "popper: the trigger's measured width",
170
+ "--anchor-height" => "popper: the trigger's measured height"
171
+ }
172
+ part "dropdown-menu-group", "role=group semantic grouping between separators"
173
+ part "dropdown-menu-label", "Non-interactive heading for a run of items",
174
+ states: {
175
+ "data-inset" => "indented to align with checkbox/radio item text (inset: true)"
176
+ }
177
+ part "dropdown-menu-item", "One role=menuitem action row",
178
+ states: {
179
+ "data-variant" => "default or destructive (the danger treatment)",
180
+ "data-inset" => "indented to align with checkbox/radio item text (inset: true)",
181
+ "data-disabled" => "item is disabled (always written together with aria-disabled)"
182
+ }
183
+ part "dropdown-menu-checkbox-item", "A role=menuitemcheckbox toggle row",
184
+ states: {
185
+ "data-checked" => "checked (the controller re-writes the pair with aria-checked on " \
186
+ "activation)",
187
+ "data-unchecked" => "unchecked",
188
+ "data-disabled" => "item is disabled (always written together with aria-disabled)",
189
+ "data-close-on-select" => "per-item override of the menu's close-on-select default " \
190
+ "(\"false\" keeps the menu open)"
191
+ }
192
+ part "dropdown-menu-radio-group", "role=group scoping one single-select value",
193
+ states: {
194
+ "data-value" => "the selected radio value (the controller re-writes it on change)"
195
+ }
196
+ part "dropdown-menu-radio-item", "A role=menuitemradio row inside a radio group",
197
+ states: {
198
+ "data-checked" => "the selected radio (the controller re-writes the pair with aria-checked)",
199
+ "data-unchecked" => "not selected",
200
+ "data-value" => "the radio's value",
201
+ "data-disabled" => "item is disabled (always written together with aria-disabled)"
202
+ }
203
+ part "dropdown-menu-checkbox-item-indicator",
204
+ "The check glyph inside checkbox items (aria-hidden; the item carries the checked state)"
205
+ part "dropdown-menu-radio-item-indicator",
206
+ "The circle glyph inside radio items (aria-hidden; the item carries the checked state)"
207
+ part "dropdown-menu-separator", "role=separator rule between groups"
208
+ part "dropdown-menu-shortcut", "The trailing keybinding HINT - aria-hidden, never binds the key"
209
+ part "dropdown-menu-sub", "A submenu scope - hosts its own popper around the sub trigger/" \
210
+ "content pair"
211
+ part "dropdown-menu-sub-trigger", "The role=menuitem row opening its submenu",
212
+ states: {
213
+ "data-popup-open" => "its submenu is open (written with aria-expanded; absence is the " \
214
+ "closed state)",
215
+ "data-inset" => "indented to align with checkbox/radio item text (inset: true)"
216
+ }
217
+ part "dropdown-menu-sub-content", "The nested role=menu panel - its own popper content on the " \
218
+ "same presence machinery",
219
+ states: {
220
+ "data-open" => "submenu is open (presence flips the pair at runtime)",
221
+ "data-closed" => "submenu is closed (the server-rendered state)",
222
+ "data-side" => { condition: "the placement side (right/left by direction; popper resolves " \
223
+ "it at runtime)",
224
+ values: %w[top right bottom left] },
225
+ "data-align" => { condition: "the alignment against the sub-trigger (popper resolves it " \
226
+ "at runtime)",
227
+ values: %w[start center end] }
228
+ },
229
+ vars: {
230
+ "--transform-origin" => "popper's anchor-facing animation origin",
231
+ "--available-width" => "popper: viewport space left for the panel (post-flip)",
232
+ "--available-height" => "popper: viewport space left for the panel (post-flip)",
233
+ "--anchor-width" => "popper: the sub-trigger's measured width",
234
+ "--anchor-height" => "popper: the sub-trigger's measured height"
235
+ }
236
+
237
+ # Enforces the required slots before render.
238
+ # @api private
239
+ def before_render
240
+ raise ArgumentError, "DropdownMenu requires with_trigger (the menu button)" unless trigger?
241
+ raise ArgumentError, "DropdownMenu requires at least one item" unless items?
242
+ end
243
+
244
+ # The trigger element's server-stable id.
245
+ # @api private
246
+ def trigger_id
247
+ "#{instance_id}-trigger"
248
+ end
249
+
250
+ # The content element's server-stable id (aria-controls target).
251
+ # @api private
252
+ def content_id
253
+ "#{instance_id}-content"
254
+ end
255
+
256
+ # The root wrapper's attributes.
257
+ # @api private
258
+ def root_attributes
259
+ root = { "data-slot" => "dropdown-menu" }
260
+ root["dir"] = dir.to_s if dir
261
+ html_attributes.merge_if_not_set(
262
+ root.merge(stimulus_attributes_for(:root)).merge(component_data_attributes)
263
+ )
264
+ end
265
+
266
+ # The role=menu panel's attributes.
267
+ # @api private
268
+ def content_attributes
269
+ attrs = {
270
+ "id" => content_id, "role" => "menu", "aria-orientation" => "vertical",
271
+ "aria-labelledby" => trigger_id, "tabindex" => "-1",
272
+ "data-slot" => "dropdown-menu-content", (open ? "data-open" : "data-closed") => "",
273
+ # Initial placement, re-resolved live by popper on open.
274
+ "data-side" => side, "data-align" => align,
275
+ "class" => css(:content)
276
+ }.merge(stimulus_attributes_for(:content))
277
+ attrs["hidden"] = true unless open
278
+ attrs
279
+ end
280
+
281
+ private
282
+
283
+ def menu_dir
284
+ dir
285
+ end
286
+
287
+ # Server-stable unique id pair for the aria wiring (two menus on
288
+ # one page must not share ids); portal-safe (the controller
289
+ # resolves content via aria-controls, not a Stimulus target).
290
+ def instance_id
291
+ @instance_id ||= poetry_instance_id("poetry-dropdown-menu")
292
+ end
293
+
294
+ private :trigger_id, :content_id, :root_attributes, :content_attributes
295
+ end
296
+
297
+ # role=group semantic grouping between separators - the shared kernel
298
+ # anatomy wearing this family identity.
299
+ #
300
+ # @api private
301
+ class Group < Poetry::Ui::Menus::Group
302
+ include ItemSlots
303
+ end
304
+
305
+ # role=group scoping the single-select value for its radio items -
306
+ # the shared kernel anatomy wearing this family identity.
307
+ #
308
+ # @api private
309
+ class RadioGroup < Poetry::Ui::Menus::RadioGroup
310
+ end
311
+
312
+ # A submenu scope (recursive item union on its own popper) - the
313
+ # shared kernel anatomy wearing this family identity.
314
+ #
315
+ # @api private
316
+ class Sub < Poetry::Ui::Menus::Sub
317
+ include ItemSlots
318
+ end
319
+
320
+ # The builder classes behind lambda-wrapped slot types: a lambda
321
+ # hides its return class from introspection, so the owner declares
322
+ # it and the registry walker recurses into the builder's own call
323
+ # surface (with_sub yields a Sub with its own items).
324
+ module ItemSlots
325
+ # Nested-slot recursion map for the registry projection.
326
+ SLOT_BUILDERS = { sub: Sub, group: Group, radio_group: RadioGroup }.freeze
327
+ end
328
+
329
+ class Component
330
+ # The slots before_render enforces, stated statically for render-free checks.
331
+ REQUIRED_SLOTS = { trigger: "the menu button", item: "at least one item" }.freeze
332
+ end
333
+
334
+ class Group
335
+ # The slots before_render enforces, stated statically for render-free checks.
336
+ REQUIRED_SLOTS = { item: "at least one item" }.freeze
337
+ end
338
+
339
+ class RadioGroup
340
+ # The slots before_render enforces, stated statically for render-free checks.
341
+ REQUIRED_SLOTS = { radio_item: "at least one radio item" }.freeze
342
+ end
343
+
344
+ class Sub
345
+ # The slots before_render enforces, stated statically for render-free checks.
346
+ REQUIRED_SLOTS = { trigger: "the sub-menu item", item: "at least one item" }.freeze
347
+ end
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module DropdownMenu
6
+ # The DropdownMenu preview matrix: the ported demo composition plus
7
+ # both item variants, both toggle kinds, inset, disabled, a 2-deep
8
+ # sub, RTL, and the server-rendered open state.
9
+ class Preview < Poetry::Core::Preview::Base
10
+ # The dropdown-menu-demo port: Button trigger, label, grouped items
11
+ # with shortcuts, a submenu, a disabled item, a destructive tail.
12
+ def default
13
+ render_component(align: :start) do |menu|
14
+ menu.with_trigger(variant: :outline) { "Open" }
15
+ menu.with_label { "My Account" }
16
+ menu.with_group do |group|
17
+ group.with_item(shortcut: "⇧⌘P") { "Profile" }
18
+ group.with_item(shortcut: "⌘B") { "Billing" }
19
+ group.with_item(shortcut: "⌘S") { "Settings" }
20
+ end
21
+ menu.with_separator
22
+ menu.with_sub do |sub|
23
+ sub.with_trigger { "Invite users" }
24
+ sub.with_item { "Email" }
25
+ sub.with_item { "Message" }
26
+ sub.with_sub do |nested|
27
+ nested.with_trigger { "More..." }
28
+ nested.with_item { "Import from CSV" }
29
+ end
30
+ end
31
+ menu.with_separator
32
+ menu.with_item(disabled: true) { "API" }
33
+ menu.with_item(variant: :destructive, shortcut: "⇧⌘Q") { "Log out" }
34
+ end
35
+ end
36
+
37
+ # dropdown-menu-checkboxes parity: toggles keep the menu open.
38
+ def checkboxes
39
+ render_component(align: :start) do |menu|
40
+ menu.with_trigger(variant: :outline) { "View" }
41
+ menu.with_label { "Appearance" }
42
+ menu.with_separator
43
+ menu.with_checkbox_item(checked: true, close_on_select: false) { "Status Bar" }
44
+ menu.with_checkbox_item(checked: false, close_on_select: false, disabled: true) { "Activity Bar" }
45
+ menu.with_checkbox_item(checked: false, close_on_select: false) { "Panel" }
46
+ end
47
+ end
48
+
49
+ # dropdown-menu-radio-group parity: one checked value per group.
50
+ def radio_group
51
+ render_component(align: :start) do |menu|
52
+ menu.with_trigger(variant: :outline) { "Panel Position" }
53
+ menu.with_label { "Panel Position" }
54
+ menu.with_separator
55
+ menu.with_radio_group(value: "bottom") do |group|
56
+ group.with_radio_item(value: "top") { "Top" }
57
+ group.with_radio_item(value: "bottom") { "Bottom" }
58
+ group.with_radio_item(value: "right") { "Right" }
59
+ end
60
+ end
61
+ end
62
+
63
+ # inset aligns flush items with the checkbox/radio gutter.
64
+ def inset_and_disabled
65
+ render_component do |menu|
66
+ menu.with_trigger(variant: :outline) { "Edit" }
67
+ menu.with_label(inset: true) { "Actions" }
68
+ menu.with_item(inset: true) { "Undo" }
69
+ menu.with_item(inset: true, disabled: true) { "Redo" }
70
+ menu.with_separator
71
+ menu.with_checkbox_item(checked: true, close_on_select: false) { "Autosave" }
72
+ end
73
+ end
74
+
75
+ # The submenu side flips under RTL (chevron mirrors via ml-auto).
76
+ def rtl
77
+ render_component(dir: :rtl) do |menu|
78
+ menu.with_trigger(variant: :outline) { "فتح" }
79
+ menu.with_item { "الملف الشخصي" }
80
+ menu.with_sub do |sub|
81
+ sub.with_trigger { "المزيد" }
82
+ sub.with_item { "استيراد" }
83
+ end
84
+ end
85
+ end
86
+
87
+ # Server-rendered open state (controllable-state: the attributes
88
+ # are the store; the controller reconciles on connect).
89
+ def open
90
+ render_component(open: true, align: :start) do |menu|
91
+ menu.with_trigger(variant: :outline) { "Open" }
92
+ menu.with_label { "My Account" }
93
+ menu.with_item { "Profile" }
94
+ menu.with_separator
95
+ menu.with_item(variant: :destructive) { "Delete" }
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end