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,639 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # A single-select dropdown field: a combobox trigger opening a popup listbox.
6
+ module Select
7
+ # The closed vocabulary for the trigger size axis.
8
+ SIZES = %i[sm default].freeze
9
+ # The closed vocabulary for the popup placement-side axis.
10
+ SIDES = %i[top right bottom left].freeze
11
+ # The closed vocabulary for the popup alignment axis.
12
+ ALIGNS = %i[start center end].freeze
13
+ # The closed vocabulary for the text-direction axis.
14
+ DIRS = %i[ltr rtl].freeze
15
+
16
+ # The server-side option registry: every rendered option (DOM order)
17
+ # lands here so the hidden native <select> and the trigger's value
18
+ # display are rendered from the same truth as the listbox items.
19
+ # Duplicate values raise at render.
20
+ #
21
+ # @api private
22
+ class OptionSet
23
+ # One registered option: committable value, plain-text label, disabled flag.
24
+ Entry = Struct.new(:value, :label, :disabled)
25
+
26
+ attr_reader :entries
27
+
28
+ def initialize
29
+ @entries = []
30
+ @seen = Set.new
31
+ end
32
+
33
+ def add(value:, label:, disabled: false)
34
+ key = value.to_s
35
+ raise ArgumentError, "Select item requires a non-blank value:" if key.blank?
36
+ unless @seen.add?(key)
37
+ raise ArgumentError, "duplicate Select option value #{key.inspect} - option values must be " \
38
+ "unique within their select"
39
+ end
40
+
41
+ @entries << Entry.new(key, label, disabled)
42
+ end
43
+
44
+ def label_for(value)
45
+ entries.find { |entry| entry.value == value }&.label
46
+ end
47
+ end
48
+
49
+ # Shared part builders for the option union - mixed into the root
50
+ # Component and the nested Group so both levels render the same
51
+ # anatomy. Hosts must expose #option_set (the shared OptionSet) and
52
+ # #selected_value.
53
+ #
54
+ # @api private
55
+ module Helpers
56
+ private
57
+
58
+ # Hosts must expose #item_wiring (the root Component computes it
59
+ # from its :item declaration; Group receives it at construction).
60
+ def item_component(**)
61
+ Item.new(option_set: option_set, selected_value: selected_value,
62
+ item_wiring: item_wiring, **)
63
+ end
64
+
65
+ def separator_part(**options)
66
+ attrs = {
67
+ "data-slot" => "select-separator", "aria-hidden" => "true",
68
+ "class" => Style.css(:separator, class: options.delete(:class))
69
+ }
70
+ content_tag(:div, nil, attrs.merge(options))
71
+ end
72
+
73
+ # Server-rendered always - the parent item's data-selected absence
74
+ # hides the check, so it stays decorative.
75
+ def item_indicator
76
+ content_tag(:span, "data-slot" => "select-item-indicator",
77
+ "class" => Style.css(:item_indicator, class: Style.css(:item_indicator_state))) do
78
+ render(Icon::Component.new(name: :check, class: Style.css(:indicator_check)))
79
+ end
80
+ end
81
+ end
82
+
83
+ # One role=option div. A part component on purpose (not an eager
84
+ # lambda): rendering happens in DOM order inside the viewport, so the
85
+ # shared OptionSet registers options in exactly the order the native
86
+ # <select> must mirror - whether the item sits at the top level or
87
+ # inside a group. aria-selected and data-selected are written
88
+ # together, never separately (unselected = data-selected absent);
89
+ # disabled divs carry aria-disabled + data-disabled together.
90
+ #
91
+ # @api private
92
+ class Item < Poetry::Core::Component
93
+ internal_component!
94
+ include Helpers
95
+
96
+ attr_reader :option_set, :selected_value
97
+
98
+ def initialize(option_set:, selected_value:, value:, item_wiring: {}, **options)
99
+ super(options)
100
+ @item_wiring = item_wiring
101
+ @option_set = option_set
102
+ @selected_value = selected_value
103
+ @value = value.to_s
104
+ @disabled = options.delete(:disabled) || false
105
+ @text_value = options.delete(:text_value)
106
+ end
107
+
108
+ def call
109
+ label_html = content || "".html_safe
110
+ plain_label = (@text_value.presence || ActionView::Base.full_sanitizer.sanitize(label_html.to_s)).squish
111
+ option_set.add(value: @value, label: plain_label, disabled: @disabled)
112
+
113
+ selected = selected_value.present? && @value == selected_value
114
+ attrs = {
115
+ "data-slot" => "select-item", "role" => "option", "tabindex" => "-1",
116
+ "data-poetry-collection-item" => "", "data-value" => @value,
117
+ "aria-selected" => selected.to_s,
118
+ "class" => Style.css(:item, class: html_attributes.delete(:class))
119
+ }.merge(@item_wiring)
120
+ # Selected state: bare data-selected on the committed option,
121
+ # nothing while unselected (absence IS the state).
122
+ attrs["data-selected"] = "" if selected
123
+ if @disabled
124
+ attrs["aria-disabled"] = "true"
125
+ attrs["data-disabled"] = ""
126
+ end
127
+ attrs["data-text-value"] = @text_value if @text_value
128
+ merged = Poetry::Core::HTML::Attributes.merged(attrs, html_attributes)
129
+ # data-value is RESERVED (the native-<select> mirror matches
130
+ # options by it) - the value: argument beats any caller spelling.
131
+ merged["data-value"] = @value
132
+ content_tag(:div, merged) do
133
+ safe_join([item_indicator,
134
+ content_tag(:span, label_html, "data-slot" => "select-item-text",
135
+ "class" => "cn-select-item-text shrink-0 whitespace-nowrap")])
136
+ end
137
+ end
138
+ end
139
+
140
+ # A single-select dropdown: a button showing the committed value that
141
+ # opens a popup listbox of options, with arrow-key navigation and
142
+ # typeahead. Reach for it when a form field commits exactly one value
143
+ # from a short, known list (long or filterable lists belong to
144
+ # Combobox; 2-4 options read better as a RadioGroup).
145
+ #
146
+ # Form participation: a visually-hidden native <select> with real
147
+ # server-rendered <option>s is the serialization truth - the initial
148
+ # value posts before any JS runs, required: rides native constraint
149
+ # validation, browser autofill lands on the native select and is
150
+ # adopted into the UI, and committing an option dispatches native
151
+ # change/input events so auto-submitting forms work unmodified. The
152
+ # popup drops below the trigger by default. Every Select needs an
153
+ # accessible name (a Field label via id:, or aria-label) or it
154
+ # raises at render.
155
+ #
156
+ # @example A named form select
157
+ # render Poetry::Ui::Select::Component.new(name: "fruit", id: "fruit",
158
+ # placeholder: "Pick a fruit") do |select|
159
+ # select.with_item(value: "apple") { "Apple" }
160
+ # select.with_item(value: "banana") { "Banana" }
161
+ # end
162
+ class Component < Poetry::Core::Component
163
+ include Helpers
164
+
165
+ # Projected into the registry, llms.txt, and the agent surface.
166
+ AGENT_RULES = [
167
+ "Use poetry_select (f.poetry_select in forms) - never hand-roll role=listbox popups, and never " \
168
+ "fake a select with DropdownMenu radio items bound to a hidden field.",
169
+ "Options are VALUES. If activating an option should DO something beyond setting a value, it's a " \
170
+ "DropdownMenu item.",
171
+ "In forms, ALWAYS go through f.poetry_select - it wires name/id/value/errors/required; bare " \
172
+ "poetry_select in a form is a smell.",
173
+ "Every Select MUST be named: a Field label (id: + label[for]) or aria-label. A bare unnamed " \
174
+ "select fails at render - do not suppress it.",
175
+ "NEVER write aria-selected without its data-selected twin, and NEVER write the display text without " \
176
+ "writing the native select's value first - the controller does all three; agents patching DOM must too.",
177
+ "Do not put interactive elements inside options (an option IS the interactive unit).",
178
+ "Long/filterable/async lists or multi-select -> Combobox, not a 50-option Select; 2-4 options -> " \
179
+ "RadioGroup.",
180
+ "The hidden native select is plumbing - never target it with styles, labels, or Capybara " \
181
+ "selectors (drive the combobox like a user).",
182
+ "Positioning is popper-only: poetry Select drops below the trigger (the item-aligned overlay " \
183
+ "mode is not ported - a documented parity delta)."
184
+ ].freeze
185
+
186
+ # The trigger-bound ARIA surface: Field's control_attributes (and
187
+ # bare aria-label usage) land on the TRIGGER - the combobox is the
188
+ # interactive control the label must reach - never on the root div.
189
+ TRIGGER_ARIA_KEYS = %w[label labelledby describedby invalid required].freeze
190
+
191
+ # The same facts the before_render raise enforces, stated statically
192
+ # so static checks can flag a missing item without rendering.
193
+ REQUIRED_SLOTS = { item: "at least one item" }.freeze
194
+
195
+ renders_one :trigger, doc: "Optional custom trigger content rendered BEFORE the value span (rare); the " \
196
+ "component owns role=combobox + the aria wiring + the chevron regardless, so " \
197
+ "composition cannot drop the contract."
198
+
199
+ renders_many :items,
200
+ doc: "The option UNION: item | group (label + items) | separator - one ordered collection " \
201
+ "(interleaving preserved; items and groups are part COMPONENTS so option registration " \
202
+ "follows render/DOM order). Scroll buttons, the viewport, and the native select are " \
203
+ "component-owned anatomy, never caller-placed.",
204
+ types: {
205
+ item: { renders: ->(**options) { item_component(**options) }, as: :item },
206
+ group: {
207
+ renders: lambda { |**options|
208
+ Group.new(option_set: option_set, selected_value: selected_value,
209
+ item_wiring: item_wiring, **options)
210
+ },
211
+ as: :group
212
+ },
213
+ separator: { renders: ->(**options) { separator_part(**options) }, as: :separator }
214
+ }
215
+
216
+ use_stimulus do
217
+ on :root do
218
+ controller :select do
219
+ register
220
+ value :open
221
+ value :value, from: :value_string
222
+ value :modal
223
+ value :loop
224
+ value :align_item_with_trigger
225
+ end
226
+ controller :popper do
227
+ register
228
+ value :side
229
+ value :align
230
+ value :side_offset
231
+ value :align_offset
232
+ value :avoid_collisions
233
+ end
234
+ end
235
+ on :trigger do
236
+ controller :select do
237
+ action :toggle, on: :click
238
+ action :trigger_keydown, on: :keydown
239
+ end
240
+ controller(:popper) { target :anchor }
241
+ end
242
+ on :content do
243
+ controller(:popper) { target :content }
244
+ end
245
+ # Every option row (root-level and grouped) - the anatomy
246
+ # classes receive it as item_wiring at construction.
247
+ on :item do
248
+ controller(:select) { action :commit, on: :click }
249
+ end
250
+ on :native do
251
+ controller(:select) { action :native_changed, on: :change }
252
+ end
253
+ on :viewport do
254
+ controller(:select) { action :sync_scroll_buttons, on: :scroll }
255
+ end
256
+ on :scroll_button do
257
+ controller :select do
258
+ action :scroll_hold_start, on: :pointerenter
259
+ action :scroll_hold_stop, on: :pointerleave
260
+ end
261
+ end
262
+ end
263
+
264
+ option :value, :string,
265
+ doc: "The committed option value - the item whose value: matches renders as selected and its label " \
266
+ "fills the trigger."
267
+ option :name, :string, doc: "The form field name, carried by the hidden native <select>."
268
+ option :placeholder, :string,
269
+ doc: "Text shown in the trigger until an option is committed; also rendered as the blank native " \
270
+ "<option> (posts \"\" when untouched)."
271
+ option :id, :string,
272
+ doc: "The trigger's DOM id - what a Field label's for: must point at; giving one also satisfies the " \
273
+ "accessible-name requirement."
274
+ option :open, :boolean, default: false, doc: "Server-renders the popup already open."
275
+ option :required, :boolean, default: false,
276
+ doc: "Marks the hidden native <select> required - native constraint validation " \
277
+ "blocks submission while unset."
278
+ option :disabled, :boolean, default: false, doc: "Disables the trigger and the hidden native <select>."
279
+ option :modal, :boolean, default: true, doc: "While open, blocks pointer interaction outside the popup."
280
+ option :side, :symbol, default: :bottom, doc: "Preferred popup side relative to the trigger."
281
+ option :align, :symbol, default: :start, doc: "Popup alignment along the chosen side's edge."
282
+ option :side_offset, :integer, default: 4, doc: "Gap in pixels between trigger and popup."
283
+ option :align_offset, :integer, default: 0, doc: "Pixel shift along the alignment axis."
284
+ option :avoid_collisions, :boolean, default: true, doc: "Flips/shifts the popup to keep it inside the viewport."
285
+ option :loop, :boolean, default: false,
286
+ doc: "Arrow-key navigation wraps from the last option back to the first."
287
+ option :align_item_with_trigger, :boolean, default: false,
288
+ doc: "Opens the popup OVER the trigger with the selected item " \
289
+ "aligned on it (native-select feel); falls back to regular " \
290
+ "below-the-trigger positioning on touch, viewport-edge " \
291
+ "triggers, or squeezed heights."
292
+ option :dir, :symbol, doc: "Text direction for the select and its popup."
293
+ option :size, :symbol, default: :default, doc: "The trigger size axis."
294
+ option :trigger_class, :string,
295
+ doc: "Extra classes merged onto the trigger button (e.g. w-full over the base w-fit); class: styles " \
296
+ "the root wrapper instead."
297
+
298
+ validates :size, inclusion: { in: SIZES }
299
+ validates :side, inclusion: { in: SIDES }
300
+ validates :align, inclusion: { in: ALIGNS }
301
+ validates :dir, inclusion: { in: DIRS }, allow_nil: true
302
+
303
+ part "select", "Root wrapper carrying both controllers (select + popper) and the " \
304
+ "optional dir attribute"
305
+ part "select-native", "The visually-hidden native <select> - the serialization truth " \
306
+ "(name/required/disabled + every option); plumbing, never styled or targeted"
307
+ part "select-trigger", "The role=combobox button the field label reaches - the value " \
308
+ "display and chevron ride inside",
309
+ states: {
310
+ "data-size" => { condition: "always - the resolved size variant",
311
+ values: SIZES.map(&:to_s) },
312
+ "data-placeholder" => "no option is committed (bare; the controller toggles it " \
313
+ "on every commit)",
314
+ "data-popup-open" => "the popup is open (bare while open, absent while closed - " \
315
+ "the controller flips it with the open state)"
316
+ }
317
+ part "select-value", "The value display span - the selected option's label, or the placeholder",
318
+ states: {
319
+ "data-placeholder" => "placeholder: is given - carries the placeholder text so a " \
320
+ "later clear can restore it"
321
+ }
322
+ part "select-content", "The popper-positioned popup shell (scroll buttons + viewport) - " \
323
+ "open/closed and the resolved placement ride here",
324
+ states: {
325
+ "data-open" => "popup is open (the controller flips the pair at runtime)",
326
+ "data-closed" => "popup is closed or animating out (the server-rendered state)",
327
+ "data-side" => { condition: "the placement side - server-rendered from side:, " \
328
+ "rewritten to the resolved side by popper on open",
329
+ values: SIDES.map(&:to_s) },
330
+ "data-align" => { condition: "the placement alignment - server-rendered from " \
331
+ "align:, rewritten by popper on open",
332
+ values: ALIGNS.map(&:to_s) }
333
+ },
334
+ vars: {
335
+ "--transform-origin" => "popper - the animation origin matching the resolved placement",
336
+ "--available-width" => "popper - viewport space available to the popup post-flip",
337
+ "--available-height" => "popper - viewport space available to the popup post-flip",
338
+ "--anchor-width" => "popper - the trigger's measured width",
339
+ "--anchor-height" => "popper - the trigger's measured height",
340
+ "--radix-select-trigger-width" => "the select controller measures the trigger on " \
341
+ "open - the viewport's min-width binding",
342
+ "--radix-select-trigger-height" => "the select controller measures the trigger on " \
343
+ "open - the viewport's MINIMUM-height binding " \
344
+ "(a hard height collapses the popup to the " \
345
+ "trigger; the list grows past it)"
346
+ }
347
+ part "select-scroll-up-button", "Hover-scroll affordance above the viewport - rendered " \
348
+ "always but hidden; the controller unhides it per scroll " \
349
+ "extremes (aria-hidden)"
350
+ part "select-scroll-down-button", "Hover-scroll affordance below the viewport (the same " \
351
+ "contract as the up button)"
352
+ part "select-viewport", "The role=listbox scroll container - the options' actual parent, " \
353
+ "labelled from the trigger"
354
+ part "select-group", "role=group wrapper labelled by its select-label heading"
355
+ part "select-label", "The group heading - styled, no ARIA role (the group points at it " \
356
+ "via aria-labelledby)"
357
+ part "select-item", "One role=option div - selection, disablement, and the committable " \
358
+ "value ride here",
359
+ states: {
360
+ "data-value" => "always - the option's committable value (the native <option> twin)",
361
+ "data-selected" => "the option is committed (bare; absent while unselected - the " \
362
+ "controller twin-writes it with aria-selected)",
363
+ "data-disabled" => "disabled: is set (aria-disabled rides along)"
364
+ }
365
+ part "select-item-indicator", "The check gutter - server-rendered always; the parent " \
366
+ "item's data-selected absence hides it"
367
+ part "select-item-text", "The option's label span - the value display copies from it"
368
+ part "select-separator", "Decorative divider between options (aria-hidden)"
369
+
370
+ # @api private
371
+ def initialize(attributes = {})
372
+ if attributes.key?(:multiple) || attributes.key?("multiple")
373
+ raise ArgumentError, "Select does not support multiple: - multi-select is Combobox territory"
374
+ end
375
+
376
+ super
377
+ @trigger_aria = extract_trigger_aria!
378
+ end
379
+
380
+ # @api private
381
+ def before_render
382
+ raise ArgumentError, "Select requires at least one item (with_item / with_group)" unless items?
383
+
384
+ return if named?
385
+
386
+ raise ArgumentError, "Select requires an accessible name - compose with a Field label " \
387
+ "(id: + label[for: id]) or pass 'aria-label'"
388
+ end
389
+
390
+ # The Field-targetable id lands on the TRIGGER (label[for=id]
391
+ # click-focuses the combobox); content/native ids derive from it.
392
+ # @api private
393
+ def trigger_id
394
+ @trigger_id ||= id.presence || poetry_instance_id("poetry-select")
395
+ end
396
+
397
+ # @api private
398
+ def content_id
399
+ "#{trigger_id}-content"
400
+ end
401
+
402
+ # @api private
403
+ def native_id
404
+ "#{trigger_id}-native"
405
+ end
406
+
407
+ # @api private
408
+ def option_set
409
+ @option_set ||= OptionSet.new
410
+ end
411
+
412
+ # @api private
413
+ def selected_value
414
+ @selected_value ||= value.presence.to_s
415
+ end
416
+
417
+ # @api private
418
+ def selected_label
419
+ option_set.label_for(selected_value) if selected_value.present?
420
+ end
421
+
422
+ # @api private
423
+ def root_attributes
424
+ root = { "data-slot" => "select" }
425
+ root["dir"] = dir.to_s if dir
426
+ html_attributes.merge_if_not_set(
427
+ root.merge(stimulus_attributes_for(:root)).merge(component_data_attributes)
428
+ )
429
+ end
430
+
431
+ # The serialization truth: a real <select> carrying name/required/
432
+ # disabled and ALL options with selected - visually hidden (sr-only,
433
+ # painted) and out of both trees (aria-hidden + tabindex=-1). Its
434
+ # change event is the autofill-adoption path.
435
+ # @api private
436
+ def native_select
437
+ attrs = {
438
+ "id" => native_id, "data-slot" => "select-native",
439
+ "aria-hidden" => "true", "tabindex" => "-1", "class" => Style.css(:native)
440
+ }
441
+ attrs["name"] = name if name.present?
442
+ attrs["required"] = true if required
443
+ attrs["disabled"] = true if disabled
444
+ attrs.merge!(stimulus_attributes_for(:native))
445
+ content_tag(:select, native_options, attrs)
446
+ end
447
+
448
+ # @api private
449
+ def trigger_button
450
+ attrs = {
451
+ "id" => trigger_id, "data-slot" => "select-trigger", "type" => "button",
452
+ "role" => "combobox", "aria-expanded" => open.to_s, "aria-controls" => content_id,
453
+ "aria-autocomplete" => "none", "data-size" => size.to_s,
454
+ "class" => css(:trigger, class: trigger_class)
455
+ }
456
+ # Trigger open state: bare data-popup-open while open, no
457
+ # attribute while closed (absence IS the state).
458
+ attrs["data-popup-open"] = "" if open
459
+ attrs["data-placeholder"] = "" unless selected_label
460
+ attrs["disabled"] = true if disabled
461
+ attrs.merge!(stimulus_attributes_for(:trigger))
462
+ attrs.merge!(trigger_aria_attributes)
463
+ content_tag(:button, attrs) do
464
+ safe_join([trigger, value_display, chevron].compact)
465
+ end
466
+ end
467
+
468
+ # @api private
469
+ def content_attributes
470
+ # No widget role here: the popup shell holds scroll buttons too,
471
+ # and role=listbox permits only option/group children (axe
472
+ # aria-required-children) - the role lives on the
473
+ # viewport, the options' actual parent.
474
+ attrs = {
475
+ "id" => content_id,
476
+ "tabindex" => "-1", "data-slot" => "select-content", (open ? "data-open" : "data-closed") => "",
477
+ # Initial placement, re-resolved live by popper on open.
478
+ "data-side" => side, "data-align" => align,
479
+ "class" => css(:content)
480
+ }.merge(stimulus_attributes_for(:content))
481
+ attrs["hidden"] = true unless open
482
+ attrs
483
+ end
484
+
485
+ # @api private
486
+ def viewport_attributes
487
+ {
488
+ "data-slot" => "select-viewport", "role" => "listbox",
489
+ "aria-labelledby" => @trigger_aria["labelledby"].presence || trigger_id,
490
+ "class" => css(:viewport)
491
+ }.merge(stimulus_attributes_for(:viewport))
492
+ end
493
+
494
+ # Scroll affordances render ALWAYS but hidden - the controller owns
495
+ # visibility per scroll extremes (syncScrollButtons) and runs the
496
+ # rAF hover-scroll; aria-hidden throughout (keyboard scrolls via
497
+ # focus + scroll-my-1).
498
+ # @api private
499
+ def scroll_button(direction)
500
+ attrs = {
501
+ "data-slot" => "select-scroll-#{direction}-button", "aria-hidden" => "true",
502
+ "hidden" => true,
503
+ "class" => Style.css(:scroll_button, class: "cn-select-scroll-#{direction}-button")
504
+ }.merge(stimulus_attributes_for(:scroll_button))
505
+ content_tag(:div, attrs) do
506
+ render(Icon::Component.new(name: :"chevron-#{direction}", class: Style.css(:scroll_icon)))
507
+ end
508
+ end
509
+
510
+ private
511
+
512
+ def named?
513
+ id.present? || @trigger_aria["label"].present? || @trigger_aria["labelledby"].present?
514
+ end
515
+
516
+ # Pull the trigger-bound aria-* out of the root's html attributes
517
+ # (both flat "aria-label" and nested aria: {label:} spellings) so
518
+ # Field's control_attributes wire the combobox, not the wrapper.
519
+ def extract_trigger_aria!
520
+ aria = {}.with_indifferent_access
521
+ nested = @html_attributes.delete("aria")
522
+ nested.each { |nested_key, nested_value| aria[nested_key] = nested_value } if nested.is_a?(Hash)
523
+ @html_attributes.keys.grep(/\Aaria-/).each do |flat|
524
+ aria[flat.delete_prefix("aria-")] = @html_attributes.delete(flat)
525
+ end
526
+ aria
527
+ end
528
+
529
+ def trigger_aria_attributes
530
+ TRIGGER_ARIA_KEYS.each_with_object({}) do |key, attrs|
531
+ attrs["aria-#{key}"] = @trigger_aria[key] unless @trigger_aria[key].nil?
532
+ end
533
+ end
534
+
535
+ # The value DISPLAY: server-rendered selected-option label (no
536
+ # FOUC), or the placeholder text; data-placeholder on the span
537
+ # carries the placeholder so a later clear can restore it.
538
+ def value_display
539
+ attrs = { "data-slot" => "select-value", "class" => "cn-select-value" }
540
+ attrs["data-placeholder"] = placeholder if placeholder.present?
541
+ content_tag(:span, selected_label || placeholder, attrs)
542
+ end
543
+
544
+ def chevron
545
+ render(Icon::Component.new(name: :"chevron-down", class: Style.css(:trigger_icon)))
546
+ end
547
+
548
+ # The blank option (Rails include_blank semantics, value="") rides
549
+ # the placeholder - and is always present when no option matches,
550
+ # so the native select never silently rests on the first option
551
+ # while the trigger shows the placeholder.
552
+ def native_options
553
+ rendered = []
554
+ if placeholder.present? || selected_label.nil?
555
+ rendered << tag.option(placeholder.to_s, value: "", selected: selected_label.nil? || nil)
556
+ end
557
+ option_set.entries.each do |entry|
558
+ rendered << tag.option(entry.label, value: entry.value,
559
+ selected: entry.value == selected_value || nil,
560
+ disabled: entry.disabled || nil)
561
+ end
562
+ safe_join(rendered)
563
+ end
564
+
565
+ # BOTH controllers build into ONE Attributes instance - a plain
566
+ # Hash#merge of two would overwrite data-controller instead of
567
+ # token-concatenating it.
568
+ def value_string = value.to_s
569
+
570
+ def item_wiring
571
+ @item_wiring ||= stimulus_attributes_for(:item)
572
+ end
573
+
574
+ private :trigger_id, :content_id, :native_id, :option_set, :selected_value, :selected_label, :root_attributes
575
+ private :native_select, :trigger_button, :content_attributes, :viewport_attributes, :scroll_button
576
+ end
577
+
578
+ # role=group with an optional heading label (aria-labelledby wired) -
579
+ # the same item union one level down, registering its options into
580
+ # the PARENT's option set so the native select and value display see
581
+ # every option.
582
+ #
583
+ # @api private
584
+ class Group < Poetry::Core::Component
585
+ internal_component!
586
+ include Helpers
587
+
588
+ attr_reader :option_set, :selected_value, :item_wiring
589
+
590
+ renders_many :items,
591
+ doc: "The same item | separator union as the root, one level down.",
592
+ types: {
593
+ item: { renders: ->(**options) { item_component(**options) }, as: :item },
594
+ separator: { renders: ->(**options) { separator_part(**options) }, as: :separator }
595
+ }
596
+
597
+ # heading: is the canonical keyword; label: is accepted as an alias.
598
+ def initialize(option_set:, selected_value:, heading: nil, label: nil, item_wiring: {}, **extra_attributes)
599
+ super(extra_attributes)
600
+ @item_wiring = item_wiring
601
+ @option_set = option_set
602
+ @selected_value = selected_value
603
+ @label_text = heading || label
604
+ end
605
+
606
+ def before_render
607
+ raise ArgumentError, "Select group requires at least one item" unless items?
608
+ end
609
+
610
+ def call
611
+ attrs = { "data-slot" => "select-group", "role" => "group", "class" => "cn-select-group" }
612
+ attrs["aria-labelledby"] = label_id if @label_text
613
+ content_tag(:div, Poetry::Core::HTML::Attributes.merged(attrs, html_attributes)) do
614
+ safe_join([label_part, *items].compact)
615
+ end
616
+ end
617
+
618
+ private
619
+
620
+ def group_id
621
+ @group_id ||= poetry_instance_id("poetry-select-group")
622
+ end
623
+
624
+ def label_id
625
+ "#{group_id}-label"
626
+ end
627
+
628
+ # A styled heading, no ARIA role - the group points at it via
629
+ # aria-labelledby.
630
+ def label_part
631
+ return if @label_text.blank?
632
+
633
+ content_tag(:div, @label_text, "data-slot" => "select-label", "id" => label_id,
634
+ "class" => Style.css(:label))
635
+ end
636
+ end
637
+ end
638
+ end
639
+ end