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,323 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # A mutually exclusive option set: radio dots or selectable choice cards.
6
+ module RadioGroup
7
+ # A set of mutually exclusive options, exactly one checkable -
8
+ # radio dots with labels, or selectable choice cards (item
9
+ # variant: :card). Reach for it when a handful of options should all
10
+ # be visible at once; larger lists belong in a Select.
11
+ #
12
+ # Keyboard: the group is one Tab stop; arrow keys move between items
13
+ # and selection follows that navigation - Tab into the group never
14
+ # changes the value. Form participation: one hidden native
15
+ # <input type=radio> per item under the shared name:, so the checked
16
+ # value serializes exactly like collection_radio_buttons and NOTHING
17
+ # submits while none is checked (presence validation stays honest).
18
+ # The server renders the roving tab stop (checked item tabindex=0,
19
+ # rest -1) so the one-Tab-stop contract holds before JS connects.
20
+ #
21
+ # @example
22
+ # render Poetry::Ui::RadioGroup::Component.new(
23
+ # name: "plan", value: "monthly", label: "Billing plan"
24
+ # ) do |group|
25
+ # group.with_item(value: "monthly", label: "Monthly")
26
+ # group.with_item(value: "yearly", label: "Yearly")
27
+ # end
28
+ class Component < Poetry::Core::Component
29
+ # The closed vocabulary for the keyboard-axis option.
30
+ ORIENTATIONS = %i[both vertical horizontal].freeze
31
+
32
+ # The closed vocabulary for the per-item variant axis.
33
+ ITEM_VARIANTS = %i[default card].freeze
34
+
35
+ # Projected into the registry, llms.txt, and the agent surface.
36
+ AGENT_RULES = [
37
+ "Use poetry_radio_group / form.radio_group - never hand-roll role=radio buttons.",
38
+ "Every item MUST have a unique value: (ArgumentError on duplicates).",
39
+ "The GROUP must be labelled - label: (or aria-labelledby) - an unlabelled radiogroup is an " \
40
+ "APG violation (ArgumentError).",
41
+ "Pair every item with a visible label (item label: renders the Label for= pairing) - a bare " \
42
+ "dot is not an option.",
43
+ "variant: :card renders the choice-card row (title + description: inside a selectable " \
44
+ "bordered label) - the pick-a-plan pattern; the whole card toggles the radio.",
45
+ "NEVER write the checked attributes (data-checked/data-unchecked) without aria-checked (the " \
46
+ "controller writes both; agents patching DOM must too).",
47
+ "Do not use RadioGroup for navigation or immediate actions; checking must not submit or " \
48
+ "navigate by itself.",
49
+ "7+ options: use Select instead.",
50
+ "Wire errors through Field/FormBuilder (invalid: + describedby on the root) - never a bare " \
51
+ "red ring."
52
+ ].freeze
53
+
54
+ # The same facts the before_render raise enforces, stated statically
55
+ # so static checks can flag a missing item without rendering.
56
+ REQUIRED_SLOTS = { item: "at least one radio item" }.freeze
57
+
58
+ renders_many :items,
59
+ doc: "One item per option: a real button[role=radio] carrying its own hidden native radio; " \
60
+ "label: renders the dot beside a paired Label. variant: :card renders the choice-card row " \
61
+ "instead - title (+ optional description:) inside a selectable bordered label, the radio " \
62
+ "pinned to the right.",
63
+ renders: lambda { |value:, label: nil, id: nil, disabled: false,
64
+ description: nil, variant: :default, **options|
65
+ unless ITEM_VARIANTS.include?(variant)
66
+ raise ArgumentError,
67
+ "unknown RadioGroup item variant #{variant.inspect} - known: #{ITEM_VARIANTS.join(", ")}"
68
+ end
69
+
70
+ item_value = register_item_value!(value)
71
+ item_id = id.presence || "#{control_id}-#{slug(item_value)}"
72
+ item_disabled = disabled || self.disabled
73
+ item = radio_item(item_value, item_id, item_disabled, options)
74
+
75
+ next card_row(item, item_id, label, description) if variant == :card
76
+
77
+ if description.present?
78
+ raise ArgumentError, "RadioGroup description: rides the choice-card form - pass variant: :card"
79
+ end
80
+
81
+ next item if label.blank?
82
+
83
+ # The item + Label pairing row. The for= targets the BUTTON id -
84
+ # label clicks check via the controller (native label->button
85
+ # activation).
86
+ content_tag(:div, class: css(:row)) do
87
+ safe_join([item, render(Label::Component.new(for_id: item_id).with_content(label))])
88
+ end
89
+ }
90
+
91
+ use_stimulus do
92
+ on :root do
93
+ controller :radio_group do
94
+ register
95
+ value :value, from: :value_string, if: :value?
96
+ # Selection follows focus: entry fires ONLY on arrow/Home/End
97
+ # navigation (never Tab), so checking on entry IS the APG
98
+ # radio contract.
99
+ action :entry_check, on: event(:roving_focus, :entry)
100
+ end
101
+ controller :roving_focus do
102
+ register
103
+ # DEFAULT tabindex-managing mode: the group is ONE Tab stop.
104
+ value :orientation
105
+ value :loop
106
+ action :keydown, on: :keydown
107
+ end
108
+ end
109
+ on :item do
110
+ controller(:radio_group) { action :check, on: :click }
111
+ end
112
+ # THE form participant: the hidden native radio the controller
113
+ # syncs.
114
+ on :input do
115
+ controller(:radio_group) { target :input }
116
+ end
117
+ end
118
+
119
+ option :name, :string, required: true,
120
+ doc: "The shared form name for every hidden radio (FormBuilder derives object[method])."
121
+ option :value, :string, doc: "The checked item's value; nil = nothing checked (pre-selection)."
122
+ option :required, :boolean, default: false,
123
+ doc: "aria-required on the ROOT only - never native required on the hidden " \
124
+ "inputs (constraint-validation focus would land on an aria-hidden input)."
125
+ option :disabled, :boolean, default: false, doc: "Disables every item (root-level)."
126
+ option :loop, :boolean, default: true, doc: "Arrow-key navigation wraps at the ends."
127
+ option :orientation, :symbol, default: :both,
128
+ doc: "Keyboard axis: :both allows all four arrows (the standard radio " \
129
+ "pattern); :vertical/:horizontal restrict the axis. No visual effect."
130
+ option :invalid, :boolean, default: false,
131
+ doc: "aria-invalid on the items (the destructive ring) - set by Field/FormBuilder " \
132
+ "from model errors."
133
+ option :label, :string,
134
+ doc: "The group accessible name -> aria-label (or wire aria-labelledby yourself) - REQUIRED: an " \
135
+ "unlabelled radiogroup fails the audit."
136
+
137
+ validates :orientation, inclusion: { in: ORIENTATIONS }
138
+
139
+ part "radio-group", "The role=radiogroup root - one Tab stop; items (and their " \
140
+ "label-pairing rows) render as direct children",
141
+ states: {
142
+ "data-disabled" => "disabled: is set on the root - every item disables with it"
143
+ }
144
+ part "radio-group-item", "A button[role=radio] per item - carries its own hidden " \
145
+ "native radio as a sibling",
146
+ states: {
147
+ "data-checked" => "the checked item (the controller writes the pair and " \
148
+ "aria-checked together on every item)",
149
+ "data-unchecked" => "every other item",
150
+ "data-disabled" => "the item (or the whole group) is disabled - also the " \
151
+ "roving-focus collection filter",
152
+ "data-value" => "always - the item's value (keys the checked-value machine)"
153
+ }
154
+ part "radio-group-indicator", "The theme-sized centering box holding the checked dot " \
155
+ "(the dot itself is the themed " \
156
+ ".cn-radio-group-indicator-icon span) - hidden (the " \
157
+ "native attribute, toggled by the controller) while " \
158
+ "unchecked"
159
+ part "radio-group-card", "The choice-card row (variant: :card) - a <label> for= the " \
160
+ "radio button, so the whole card toggles; the checked " \
161
+ "treatments key on data-checked inside it"
162
+ part "radio-group-card-content", "Text column of a choice-card item (variant: :card) - " \
163
+ "title and description stack inside the card label"
164
+ part "radio-group-card-title", "The choice card's title line (the item label:)"
165
+ part "radio-group-card-description", "Muted copy under the choice card's title " \
166
+ "(description:)"
167
+
168
+ # @api private
169
+ def before_render
170
+ raise ArgumentError, "RadioGroup requires at least one with_item" unless items?
171
+
172
+ return if named?
173
+
174
+ raise ArgumentError,
175
+ "unlabelled RadioGroup - give it label: (or aria-labelledby); a radiogroup without an " \
176
+ "accessible name is an APG violation"
177
+ end
178
+
179
+ # @api private
180
+ def checked?(item_value)
181
+ value.present? && value.to_s == item_value
182
+ end
183
+
184
+ # The label-for/aria target root id - server-stable (Field-issued
185
+ # or auto) so item ids derive deterministically.
186
+ # @api private
187
+ def control_id
188
+ @control_id ||= poetry_instance_id("poetry-radio-group")
189
+ end
190
+
191
+ # @api private
192
+ def root_attributes
193
+ attrs = {
194
+ "role" => "radiogroup", "data-slot" => "radio-group", "id" => control_id
195
+ }
196
+ attrs["aria-required"] = true if required
197
+ attrs["aria-label"] = label if label.present?
198
+ attrs["data-disabled"] = "" if disabled
199
+ html_attributes.merge_if_not_set(
200
+ attrs.merge(stimulus_attributes_for(:root)).merge(component_data_attributes)
201
+ )
202
+ end
203
+
204
+ private
205
+
206
+ def register_item_value!(value)
207
+ @item_values ||= Set.new
208
+ item_value = value.to_s
209
+ unless @item_values.add?(item_value)
210
+ raise ArgumentError, "duplicate RadioGroup item value #{item_value.inspect} - values key " \
211
+ "the checked-value machine"
212
+ end
213
+
214
+ item_value
215
+ end
216
+
217
+ # No raw user string becomes an id/selector fragment.
218
+ def slug(value)
219
+ value.gsub(/[^a-zA-Z0-9_-]+/, "-")
220
+ end
221
+
222
+ # The choice-card row: the whole card IS a label (for= the radio
223
+ # button, native label->button activation), title + description
224
+ # stacked left, the radio right. A plain <label> with its own
225
+ # radio-group-card slot, NOT the Label component - a nested
226
+ # data-component boundary would hand this subtree's part
227
+ # ownership to Label (the part-contract walker attributes DOM to
228
+ # the nearest component). The checked/bordered treatments ride
229
+ # the theme's cn-field-label rules off the item's data-checked
230
+ # inside.
231
+ def card_row(item, item_id, label, description)
232
+ raise ArgumentError, "RadioGroup card items need label: (the card title)" if label.blank?
233
+
234
+ text = content_tag(:div, "data-slot" => "radio-group-card-content", class: css(:card_content)) do
235
+ parts = [content_tag(:div, label, "data-slot" => "radio-group-card-title",
236
+ class: css(:card_title))]
237
+ if description.present?
238
+ parts << content_tag(:p, description, "data-slot" => "radio-group-card-description",
239
+ class: css(:card_description))
240
+ end
241
+ safe_join(parts)
242
+ end
243
+ content_tag(:label, safe_join([text, item]),
244
+ "data-slot" => "radio-group-card", "for" => item_id, class: css(:card))
245
+ end
246
+
247
+ def named?
248
+ aria = html_attributes["aria"] || {}
249
+ label.present? ||
250
+ html_attributes["aria-label"].present? || html_attributes["aria-labelledby"].present? ||
251
+ aria["label"].present? || aria["labelledby"].present?
252
+ end
253
+
254
+ def radio_item(item_value, item_id, item_disabled, options)
255
+ checked = checked?(item_value)
256
+
257
+ attrs = {
258
+ type: "button", role: "radio", id: item_id, class: item_classes(options.delete(:class)),
259
+ "data-slot" => "radio-group-item", "data-poetry-collection-item" => "",
260
+ "data-value" => item_value,
261
+ "aria-checked" => checked.to_s, (checked ? "data-checked" : "data-unchecked") => "",
262
+ "tabindex" => tab_stop?(item_value, item_disabled) ? "0" : "-1"
263
+ }
264
+ attrs["aria-invalid"] = "true" if invalid
265
+ if item_disabled
266
+ attrs[:disabled] = true
267
+ attrs["data-disabled"] = "" # the roving-focus collection filter
268
+ end
269
+ attrs.merge!(stimulus_attributes_for(:item))
270
+
271
+ # The native input is the button's SIBLING - a focusable native
272
+ # control inside a role=radio button is an axe nested-interactive
273
+ # violation.
274
+ safe_join([
275
+ content_tag(:button, Poetry::Core::HTML::Attributes.merged(attrs, options)) do
276
+ indicator(checked)
277
+ end,
278
+ hidden_radio(item_value, item_id, checked, item_disabled)
279
+ ])
280
+ end
281
+
282
+ def indicator(checked)
283
+ attrs = { class: css(:indicator), "data-slot" => "radio-group-indicator" }
284
+ # A hidden attr toggle, not element presence - no check animation
285
+ # is rendered (morph-safe).
286
+ attrs[:hidden] = true unless checked
287
+ content_tag(:span, content_tag(:span, "", class: css(:dot)), attrs)
288
+ end
289
+
290
+ # THE form participant: native radio serialization (same form+name
291
+ # group), aria-hidden + tabindex=-1 (the role=radio button is the
292
+ # focusable/AT surface - render tests pin the no-double-Tab-stop
293
+ # invariant).
294
+ def hidden_radio(item_value, item_id, checked, item_disabled)
295
+ tag.input(type: "radio", name: name, value: item_value, id: "#{item_id}-input",
296
+ checked: checked, disabled: item_disabled, "aria-hidden": true,
297
+ tabindex: "-1", class: css(:input),
298
+ **stimulus_attributes_for(:input).transform_keys(&:to_sym))
299
+ end
300
+
301
+ # The server renders the roving contract: exactly one tabindex=0 -
302
+ # the checked item, else the FIRST enabled item (Tab lands there
303
+ # when nothing is checked). Sequential across item registration.
304
+ def tab_stop?(item_value, item_disabled)
305
+ return false if @tab_stop_assigned || item_disabled
306
+
307
+ stop = value.present? ? checked?(item_value) : true
308
+ @tab_stop_assigned = true if stop
309
+ stop
310
+ end
311
+
312
+ def item_classes(extra)
313
+ classnames(css(:item), extra)
314
+ end
315
+
316
+ def value_string = value.to_s
317
+ def value? = value.present?
318
+
319
+ private :checked?, :control_id, :root_attributes
320
+ end
321
+ end
322
+ end
323
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module RadioGroup
6
+ # The RadioGroup preview matrix: group configs (none-checked,
7
+ # one-checked, required, disabled) x item states (checked, disabled,
8
+ # invalid), the radio-group-demo port, and the horizontal-layout
9
+ # recipe (caller classes; the keyboard stays both-axis).
10
+ class Preview < Poetry::Core::Preview::Base
11
+ # @!group States
12
+
13
+ # The radio-group-demo port: value pre-checked, items paired with
14
+ # Labels via item label:.
15
+ def default
16
+ render_component(name: "density", value: "comfortable", label: "Density") do |group|
17
+ group.with_item(value: "default", label: "Default")
18
+ group.with_item(value: "comfortable", label: "Comfortable")
19
+ group.with_item(value: "compact", label: "Compact")
20
+ end
21
+ end
22
+
23
+ # Nothing checked (pre-selection): Tab lands on the FIRST enabled
24
+ # item; nothing submits until the user chooses (presence
25
+ # validation stays honest).
26
+ def none_checked
27
+ render_component(name: "plan", label: "Plan") do |group|
28
+ group.with_item(value: "monthly", label: "Monthly")
29
+ group.with_item(value: "yearly", label: "Yearly")
30
+ end
31
+ end
32
+
33
+ # A disabled option mid-list: skipped by Tab AND filtered from the
34
+ # arrow collection.
35
+ def disabled_item
36
+ render_component(name: "tier", value: "free", label: "Tier") do |group|
37
+ group.with_item(value: "free", label: "Free")
38
+ group.with_item(value: "pro", label: "Pro (unavailable)", disabled: true)
39
+ group.with_item(value: "enterprise", label: "Enterprise")
40
+ end
41
+ end
42
+
43
+ # Root-level disabled: a locked-in choice still renders its dot
44
+ # (and still submits - hidden radios disable too, so it does NOT).
45
+ def disabled_group
46
+ render_component(name: "plan", value: "yearly", disabled: true, label: "Plan") do |group|
47
+ group.with_item(value: "monthly", label: "Monthly")
48
+ group.with_item(value: "yearly", label: "Yearly")
49
+ end
50
+ end
51
+
52
+ # The field-level error treatment: aria-invalid on every item (the
53
+ # destructive ring), wired by Field/FormBuilder upstream.
54
+ def invalid
55
+ render_component(name: "plan", invalid: true, required: true, label: "Plan") do |group|
56
+ group.with_item(value: "monthly", label: "Monthly")
57
+ group.with_item(value: "yearly", label: "Yearly")
58
+ end
59
+ end
60
+
61
+ # The choice-card items (variant: :card): title + description
62
+ # inside a selectable bordered label - the whole card toggles the
63
+ # radio (the upstream Choice Card recipe).
64
+ def choice_cards
65
+ render_component(name: "compute", value: "kubernetes", label: "Compute environment",
66
+ class: "w-80") do |group|
67
+ group.with_item(value: "kubernetes", label: "Kubernetes", variant: :card,
68
+ description: "Run GPU workloads on a K8s cluster.")
69
+ group.with_item(value: "vm", label: "Virtual Machine", variant: :card,
70
+ description: "Access a cluster to run GPU workloads.")
71
+ end
72
+ end
73
+
74
+ # @!endgroup
75
+
76
+ # @!group Recipes
77
+
78
+ # Horizontal layout is a CALLER class (source has no orientation
79
+ # styling); the keyboard stays both-axis.
80
+ def horizontal_layout
81
+ render_component(name: "align", value: "left", label: "Alignment", class: "grid-flow-col") do |group|
82
+ group.with_item(value: "left", label: "Left")
83
+ group.with_item(value: "center", label: "Center")
84
+ group.with_item(value: "right", label: "Right")
85
+ end
86
+ end
87
+
88
+ # Field-bound (the FormBuilder shape): the group takes the Field's
89
+ # id + describedby; the hint reads on the ROOT (group-level).
90
+ def in_a_field
91
+ field = Field::Component.new(
92
+ id: "preview-plan", label_text: "Plan",
93
+ hint: "You can change this anytime.", required: true
94
+ )
95
+ render_component(field) do
96
+ embed(Component.new(name: "plan", value: "monthly", required: true, label: "Plan",
97
+ id: "preview-plan",
98
+ "aria-describedby": "preview-plan-hint")) do |group|
99
+ group.with_item(value: "monthly", label: "Monthly")
100
+ group.with_item(value: "yearly", label: "Yearly")
101
+ end
102
+ end
103
+ end
104
+
105
+ # @!endgroup
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module RadioGroup
6
+ # Style dictionary for the RadioGroup family: the well + dot
7
+ # treatments ride the themes. The indicator is a theme-SIZED box so
8
+ # the absolutely-centered dot anchors against a real rect regardless
9
+ # of the item's display - an unsized anchor span centers only by
10
+ # flow accident (7px off under nova's flex item). :input stays the
11
+ # hidden native form bridge and :row the label pairing row - both
12
+ # structural, no cn names.
13
+ class Style < Poetry::Core::Style
14
+ base "cn-radio-group w-full"
15
+
16
+ element :item, "cn-radio-group-item group/radio-group-item peer relative aspect-square " \
17
+ "shrink-0 border outline-none after:absolute after:-inset-x-3 " \
18
+ "after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50"
19
+
20
+ element :indicator, "cn-radio-group-indicator"
21
+
22
+ # The checked dot - a bare themed span (bg + size + centering all
23
+ # live in the theme's cn-radio-group-indicator-icon rule).
24
+ element :dot, "cn-radio-group-indicator-icon"
25
+
26
+ # The form participant: one hidden native radio PER item, shared
27
+ # name - out of the Tab order and the accessibility tree (the
28
+ # role=radio button is the accessible control).
29
+ element :input, "sr-only"
30
+
31
+ # The item+Label pairing row (radio-group-demo parity) rendered
32
+ # when an item passes label:.
33
+ element :row, "flex items-center gap-3"
34
+
35
+ # The choice-card row (item variant: :card): the card IS the
36
+ # label, wearing the Field family's cn-field-label hooks so one
37
+ # theme rule styles choice cards wherever they appear. Upstream
38
+ # nests FieldLabel > Field > FieldContent; poetry flattens the
39
+ # row onto the label (cn-field-label-card carries the border/
40
+ # padding the nested [data-slot=field] selectors keyed upstream).
41
+ # cn-label rides along because the card is a plain <label>, not
42
+ # the Label component (part-ownership: a nested data-component
43
+ # boundary would hand the card slots to Label).
44
+ element :card, "cn-label cn-field-label cn-field-label-card flex w-full items-center " \
45
+ "select-none has-data-disabled:opacity-50"
46
+ element :card_content, "cn-field-content group/field-content flex flex-1 flex-col " \
47
+ "leading-snug"
48
+ element :card_title, "cn-field-title flex w-fit items-center"
49
+ element :card_description, "cn-field-description"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,10 @@
1
+ <%= content_tag(:div, **root_attributes.to_attributes) do %>
2
+ <% panel_defs.each_with_index do |panel, index| %>
3
+ <% unless index.zero? %>
4
+ <%= content_tag(:div, handle_attributes(index - 1)) do %>
5
+ <% if grip %><div class="<%= css(:grip) %>"></div><% end %>
6
+ <% end %>
7
+ <% end %>
8
+ <%= content_tag(:div, capture(&panel.block), panel_attributes(panel, index)) %>
9
+ <% end %>
10
+ <% end %>
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # A panel group divided by draggable splitter handles.
6
+ module Resizable
7
+ # A group of panels - side by side or stacked - divided by draggable
8
+ # splitter handles, so the reader can redistribute the space. Panels
9
+ # are flex children whose flex-grow IS the percentage, handles are
10
+ # role=separator splitters with full keyboard support (arrows step,
11
+ # Home/End jump), and the controller owns the drag + keyboard
12
+ # redistribution. Declare panels with with_panel; the component
13
+ # interleaves the handles and wires the ARIA.
14
+ #
15
+ # @example
16
+ # render Poetry::Ui::Resizable::Component.new(class: "h-48 rounded-lg border") do |group|
17
+ # group.with_panel(default_size: 25) { tag.div("Sidebar") }
18
+ # group.with_panel { tag.div("Content") }
19
+ # end
20
+ class Component < Poetry::Core::Component
21
+ # The closed vocabulary for the group-axis option.
22
+ DIRECTIONS = %i[horizontal vertical].freeze
23
+
24
+ # Projected into the registry, llms.txt, and the agent surface.
25
+ AGENT_RULES = [
26
+ "Declare panels with with_panel(default_size:, min_size:, max_size:) - sizes are " \
27
+ "PERCENTAGES and the component interleaves the separator handles.",
28
+ "direction: :horizontal is side-by-side (the default); :vertical stacks.",
29
+ "Handles are keyboard splitters (arrows step, Home/End jump) - never replace them " \
30
+ "with styled divs.",
31
+ "Nest a group inside a panel for two-axis layouts - groups self-scope."
32
+ ].freeze
33
+
34
+ # The with_panel content-block requirement, stated statically for
35
+ # static checks.
36
+ SLOT_REQUIRED_CONTENT = { panel: "the panel content" }.freeze
37
+
38
+ # The same facts the before_render raise enforces, stated statically
39
+ # so static checks can flag too few panels without rendering.
40
+ REQUIRED_SLOTS = { panel: "at least two panels" }.freeze
41
+
42
+ renders_many :panels,
43
+ doc: "One panel per call: default_size/min_size/max_size are percentages of the group; the " \
44
+ "content block is required.",
45
+ renders: lambda { |default_size: nil, min_size: nil, max_size: nil, classes: nil, &block|
46
+ unless block
47
+ raise ArgumentError,
48
+ "Resizable with_panel requires a content block (the panel content)"
49
+ end
50
+
51
+ panel_defs << Panel.new(default_size: default_size, min_size: min_size,
52
+ max_size: max_size, classes: classes, block: block)
53
+ nil
54
+ }
55
+
56
+ use_stimulus do
57
+ on :root do
58
+ controller :resizable do
59
+ register
60
+ value :orientation, from: :direction
61
+ end
62
+ end
63
+ # The full drag trio + keyboard on each handle; panels are found
64
+ # by DOM position (no targets).
65
+ on :handle do
66
+ controller :resizable do
67
+ action :drag_start, on: :pointerdown
68
+ action :drag_move, on: :pointermove
69
+ action :drag_end, on: :pointerup
70
+ action :keydown, on: :keydown
71
+ end
72
+ end
73
+ end
74
+
75
+ option :direction, :symbol, default: :horizontal,
76
+ doc: "The group axis: :horizontal lays panels side by side, :vertical stacks them."
77
+ option :grip, :boolean, default: false, doc: "Renders the grip dots on each handle."
78
+
79
+ validates :direction, inclusion: { in: DIRECTIONS }
80
+
81
+ part "resizable-panel-group", "The flex group root - the splitter controller rides here",
82
+ states: {
83
+ "data-orientation" => { condition: "the group axis", values: DIRECTIONS.map(&:to_s) }
84
+ }
85
+ part "resizable-panel", "One flex child whose flex-grow IS its percentage - the controller " \
86
+ "rewrites the inline flex on every resize",
87
+ states: {
88
+ "data-min-size" => "the panel's minimum percentage, rendered when with_panel passes " \
89
+ "min_size: (the controller's clamp floor)",
90
+ "data-max-size" => "the panel's maximum percentage, rendered when with_panel passes " \
91
+ "max_size: (the controller's clamp ceiling)"
92
+ }
93
+ part "resizable-handle", "The role=separator splitter between panels - drag and keyboard " \
94
+ "resizing live here; its aria-valuenow tracks the preceding panel"
95
+
96
+ # @api private
97
+ def before_render
98
+ # panels? forces the render block (the slot-predicate rule -
99
+ # panel_defs is empty until it runs).
100
+ raise ArgumentError, "Resizable requires at least two with_panel declarations" unless
101
+ panels? && panel_defs.size >= 2
102
+ end
103
+
104
+ # @api private
105
+ def panel_defs
106
+ @panel_defs ||= []
107
+ end
108
+
109
+ # Even shares when default_size: is omitted.
110
+ # @api private
111
+ def size_of(panel)
112
+ panel.default_size || (100.0 / panel_defs.size).round(2)
113
+ end
114
+
115
+ # @api private
116
+ def root_attributes
117
+ html_attributes.merge_if_not_set(
118
+ {
119
+ "data-slot" => "resizable-panel-group", "data-orientation" => direction
120
+ }.merge(stimulus_attributes_for(:root)).merge(component_data_attributes)
121
+ )
122
+ end
123
+
124
+ # @api private
125
+ def panel_attributes(panel, index)
126
+ attrs = {
127
+ "id" => panel_id(index), "data-slot" => "resizable-panel",
128
+ "class" => [css(:panel), panel.classes].compact.join(" "),
129
+ "style" => "flex: #{size_of(panel)} 1 0px"
130
+ }
131
+ attrs["data-min-size"] = panel.min_size if panel.min_size
132
+ attrs["data-max-size"] = panel.max_size if panel.max_size
133
+ attrs
134
+ end
135
+
136
+ # The splitter: a separator whose value tracks the PRECEDING
137
+ # panel; its visual orientation is PERPENDICULAR to the group axis
138
+ # (a side-by-side group has a vertical bar), which is also what
139
+ # the dictionary's aria-[orientation] selectors key on.
140
+ # @api private
141
+ def handle_attributes(index)
142
+ before = panel_defs[index]
143
+ {
144
+ "role" => "separator", "tabindex" => "0",
145
+ "data-slot" => "resizable-handle",
146
+ "aria-orientation" => direction == :horizontal ? "vertical" : "horizontal",
147
+ "aria-controls" => panel_id(index),
148
+ # Server-rendered initial value (the controller reconciles on
149
+ # connect) - the splitter announces even before JS.
150
+ "aria-valuenow" => size_of(before).round,
151
+ # The announced bounds fall back to 10/90 percent when the
152
+ # panel declares no min_size:/max_size: clamp.
153
+ "aria-valuemin" => before.min_size || 10,
154
+ "aria-valuemax" => before.max_size || 90,
155
+ "class" => css(:handle)
156
+ }.merge(stimulus_attributes_for(:handle))
157
+ end
158
+
159
+ # @api private
160
+ def panel_id(index)
161
+ "#{instance_id}-panel-#{index}"
162
+ end
163
+
164
+ # One declared panel: its size bounds, extra classes, and content block.
165
+ # @api private
166
+ Panel = Data.define(:default_size, :min_size, :max_size, :classes, :block)
167
+
168
+ private
169
+
170
+ def instance_id
171
+ @instance_id ||= poetry_instance_id("poetry-resizable")
172
+ end
173
+
174
+ private :panel_defs, :size_of, :root_attributes, :panel_attributes, :handle_attributes, :panel_id
175
+ end
176
+ end
177
+ end
178
+ end