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,526 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # Command family: the filterable command palette and its dialog form.
6
+ module Command
7
+ # The engine pieces below (ItemSet, Helpers, Item, Group) build the
8
+ # palette's anatomy; Combobox reuses the same markup contract.
9
+
10
+ # The server-side item registry: every rendered item (DOM order)
11
+ # lands here so option ids are server-stable ("#{id}-item-<n>" - the
12
+ # aria-activedescendant contract) and the initial highlight (the
13
+ # value: option, else the first enabled item) is decided in render
14
+ # order before the input derives its activedescendant. Duplicate or
15
+ # blank values raise at render.
16
+ #
17
+ # @api private
18
+ class ItemSet
19
+ attr_reader :highlighted_id, :count
20
+
21
+ def initialize(base_id:, highlight_value: nil)
22
+ @base_id = base_id
23
+ @highlight_value = highlight_value.presence
24
+ @seen = Set.new
25
+ @count = 0
26
+ @highlighted_id = nil
27
+ end
28
+
29
+ # Registers one item; returns [item_id, highlighted].
30
+ def register(value:, disabled: false)
31
+ key = value.to_s
32
+ raise ArgumentError, "Command item requires a non-blank value:" if key.blank?
33
+ unless @seen.add?(key)
34
+ raise ArgumentError, "duplicate Command item value #{key.inspect} - item values must be " \
35
+ "unique within their command"
36
+ end
37
+
38
+ item_id = "#{@base_id}-item-#{@count}"
39
+ @count += 1
40
+ highlighted = highlight?(key, disabled)
41
+ @highlighted_id = item_id if highlighted
42
+ [item_id, highlighted]
43
+ end
44
+
45
+ private
46
+
47
+ # value: given -> only that item (unless disabled; the controller's
48
+ # connect-seat falls back client-side); no value -> first enabled.
49
+ def highlight?(value, disabled)
50
+ return false if @highlighted_id || disabled
51
+
52
+ @highlight_value ? value == @highlight_value : true
53
+ end
54
+ end
55
+
56
+ # Shared part builders for the item union - mixed into the root
57
+ # Component and the nested Group so both levels render the same
58
+ # anatomy through the same Builder. Hosts must expose #item_set.
59
+ #
60
+ # @api private
61
+ module Helpers
62
+ private
63
+
64
+ # Hosts must also expose #item_wiring (the root Component computes
65
+ # it from its :item declaration; Group receives it at construction)
66
+ # so every item renders the same declared wiring.
67
+ def item_component(**)
68
+ Item.new(item_set: item_set, item_wiring: item_wiring, **)
69
+ end
70
+
71
+ # role=separator, server-rendered VISIBLE - the controller hides
72
+ # every separator whenever the query is non-empty (a filtered
73
+ # list has no stable sections).
74
+ def separator_part(**options)
75
+ attrs = {
76
+ # Decorative inside role=listbox (only option/group children
77
+ # are valid - axe aria-required-children, the Select precedent).
78
+ "data-slot" => "command-separator", "aria-hidden" => "true",
79
+ "class" => Style.css(:separator, class: options.delete(:class))
80
+ }
81
+ content_tag(:div, nil, attrs.merge(options))
82
+ end
83
+ end
84
+
85
+ # One role=option div (APG editable combobox, activedescendant
86
+ # flavor). A part component ON PURPOSE (not an eager lambda):
87
+ # rendering happens in DOM order, so the shared ItemSet assigns
88
+ # server-stable ids and the initial highlight in exactly the order
89
+ # the controller's collection walks. NO tabindex (options are never
90
+ # DOM-focused - the activedescendant design) and NO aria-selected
91
+ # (reserved for committed values - a bare palette has none; the
92
+ # highlight is data-highlighted + the input's activedescendant).
93
+ #
94
+ # @api private
95
+ class Item < Poetry::Core::Component
96
+ internal_component!
97
+ include Helpers
98
+
99
+ attr_reader :item_set, :item_wiring
100
+
101
+ def initialize(item_set:, value:, item_wiring: {}, **options)
102
+ super(options)
103
+ @item_set = item_set
104
+ @item_wiring = item_wiring
105
+ @value = value.to_s
106
+ @disabled = options.delete(:disabled) || false
107
+ @keywords = Array(options.delete(:keywords)).map(&:to_s)
108
+ @filter_value = options.delete(:filter_value)
109
+ @always_render = options.delete(:always_render) || false
110
+ @shortcut = options.delete(:shortcut)
111
+ end
112
+
113
+ def call
114
+ item_id, highlighted = item_set.register(value: @value, disabled: @disabled)
115
+ attrs = {
116
+ "id" => item_id, "data-slot" => "command-item", "role" => "option",
117
+ "data-poetry-collection-item" => "", "data-value" => @value,
118
+ "class" => Style.css(:item, class: html_attributes.delete(:class))
119
+ }.merge(@item_wiring)
120
+ attrs["data-highlighted"] = "" if highlighted
121
+ if @disabled
122
+ attrs["aria-disabled"] = "true"
123
+ attrs["data-disabled"] = ""
124
+ end
125
+ attrs["data-keywords"] = @keywords.join(" ") if @keywords.any?
126
+ attrs["data-filter-value"] = @filter_value if @filter_value
127
+ attrs["data-always-render"] = "" if @always_render
128
+ content_tag(:div, Poetry::Core::HTML::Attributes.merged(attrs, html_attributes)) do
129
+ safe_join([label_part, shortcut_part].compact)
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ # The item's LABEL - the filter/typematch text source (shortcuts
136
+ # excluded; icons contribute no textContent; icon-rich content
137
+ # overrides via filter_value:/keywords:).
138
+ def label_part
139
+ content_tag(:span, content, "data-slot" => "command-item-text",
140
+ "class" => Style.css(:item_text))
141
+ end
142
+
143
+ # Presentational only - Command never binds the hinted key (the
144
+ # host recipe does); excluded from the filter text.
145
+ def shortcut_part
146
+ return if @shortcut.blank?
147
+
148
+ content_tag(:span, @shortcut, "data-slot" => "command-shortcut",
149
+ "class" => Style.css(:shortcut))
150
+ end
151
+ end
152
+
153
+ # role=group labelled by its heading part (aria-labelledby wired) -
154
+ # the same item union one level down, registering its items into the
155
+ # PARENT's item set so ids and the initial highlight stay in DOM
156
+ # order. Kept internal ON PURPOSE: nested parts are anatomy, not
157
+ # registered components.
158
+ #
159
+ # @api private
160
+ class Group < Poetry::Core::Component
161
+ internal_component!
162
+ include Helpers
163
+
164
+ attr_reader :item_set, :item_wiring
165
+
166
+ renders_many :items,
167
+ doc: "The group's members: with_item actions and with_separator dividers, interleaved in " \
168
+ "declaration order.",
169
+ types: {
170
+ item: { renders: ->(**options) { item_component(**options) }, as: :item },
171
+ separator: { renders: ->(**options) { separator_part(**options) }, as: :separator }
172
+ }
173
+
174
+ def initialize(item_set:, heading:, item_wiring: {}, always_render: false, **extra_attributes)
175
+ raise ArgumentError, "Command group requires heading: (the group's accessible name)" if heading.blank?
176
+
177
+ super(extra_attributes)
178
+ @item_set = item_set
179
+ @item_wiring = item_wiring
180
+ @heading_text = heading
181
+ @always_render = always_render
182
+ end
183
+
184
+ def before_render
185
+ raise ArgumentError, "Command group requires at least one item" unless items?
186
+ end
187
+
188
+ def call
189
+ attrs = {
190
+ "data-slot" => "command-group", "role" => "group", "aria-labelledby" => heading_id,
191
+ "class" => Style.css(:group, class: html_attributes.delete(:class))
192
+ }
193
+ attrs["data-always-render"] = "" if @always_render
194
+ content_tag(:div, Poetry::Core::HTML::Attributes.merged(attrs, html_attributes)) do
195
+ safe_join([heading_part, *items])
196
+ end
197
+ end
198
+
199
+ private
200
+
201
+ def group_id
202
+ @group_id ||= poetry_instance_id("poetry-command-group")
203
+ end
204
+
205
+ def heading_id
206
+ "#{group_id}-heading"
207
+ end
208
+
209
+ # A styled heading, no ARIA role - the group points at it via
210
+ # aria-labelledby.
211
+ def heading_part
212
+ content_tag(:div, @heading_text, "data-slot" => "command-group-heading", "id" => heading_id,
213
+ "class" => Style.css(:heading))
214
+ end
215
+ end
216
+
217
+ # A command palette: an always-visible search input over a listbox
218
+ # of actions, filtered client-side as you type. Real focus stays
219
+ # pinned to the input for the whole session; the highlighted option
220
+ # carries data-highlighted and its server-stable id in the input's
221
+ # aria-activedescendant, and options never take focus themselves.
222
+ #
223
+ # The filter is deterministic substring matching with ranked bands,
224
+ # and HIDE-ONLY: the server renders ALL items and the controller
225
+ # hides non-matches, never reordering the DOM. Activating an item
226
+ # fires a cancelable poetry:command:select event - the host (or
227
+ # Combobox, which wraps this engine) owns the consequences.
228
+ # filter: false leaves filtering to the server (the Turbo-frame
229
+ # async recipe).
230
+ #
231
+ # @example
232
+ # render Poetry::Ui::Command::Component.new("aria-label": "Command menu") do |command|
233
+ # command.with_item(value: "new-file") { "New file" }
234
+ # command.with_item(value: "search") { "Search" }
235
+ # end
236
+ class Component < Poetry::Core::Component
237
+ include Helpers
238
+
239
+ # Projected into the registry, llms.txt, and the agent surface.
240
+ AGENT_RULES = [
241
+ "Use poetry_command - never hand-roll a filterable listbox with an input + a list and ad-hoc JS.",
242
+ "Command items DO things; they carry no form value. Picking a value for a form is Combobox " \
243
+ "(which wraps this) - never bind a hidden input to a bare Command.",
244
+ "Every item needs a unique value: (ArgumentError) and gets a server id - never strip item ids " \
245
+ "(aria-activedescendant depends on them).",
246
+ "Never put tabindex or focus on options; never write aria-selected in a bare Command - " \
247
+ "highlight is data-highlighted + activedescendant only.",
248
+ "Filtering is hide-only: never reorder, remove, or re-append items to 'sort' results - " \
249
+ "DOM order is the contract.",
250
+ "The poetry:command:select event is the ONLY activation surface - act in a listener (or item " \
251
+ "data-action); don't patch the controller to navigate.",
252
+ "Long/async data: filter: false + a Turbo frame (the recipe) - don't render 5,000 items and hope.",
253
+ "Icon-rich labels: set filter_value:/keywords: rather than stuffing hidden text into items."
254
+ ].freeze
255
+
256
+ # The input-bound ARIA surface: aria-label/labelledby/describedby
257
+ # passed to the component land on the INPUT - the combobox is the
258
+ # interactive control the label must reach - never on the root div.
259
+ INPUT_ARIA_KEYS = %w[label labelledby describedby].freeze
260
+
261
+ # The accessible-name requirement stated statically: an id or an
262
+ # aria name must be given, checkable at write time.
263
+ REQUIRES_ANY = [
264
+ { hint: "the input's accessible name",
265
+ options: %w[id aria-label aria-labelledby aria] }
266
+ ].freeze
267
+
268
+ renders_one :empty, doc: "Custom zero-results content (defaults to t('poetry.command.empty'))."
269
+ renders_one :loading, doc: "Custom pending content (a spinner); the HOST toggles visibility (Turbo frame " \
270
+ "events) - Command renders the part, never sets it."
271
+
272
+ renders_many :items,
273
+ doc: "The item UNION: item | group (heading + items) | separator - one ordered collection " \
274
+ "(interleaving preserved; items and groups are part COMPONENTS so id assignment follows " \
275
+ "render/DOM order).",
276
+ types: {
277
+ item: { renders: ->(**options) { item_component(**options) }, as: :item },
278
+ group: { renders: lambda { |**options|
279
+ Group.new(item_set: item_set, item_wiring: item_wiring, **options)
280
+ }, as: :group },
281
+ separator: { renders: ->(**options) { separator_part(**options) }, as: :separator }
282
+ }
283
+
284
+ use_stimulus do
285
+ on :root do
286
+ controller :command do
287
+ register
288
+ value :filter
289
+ value :loop
290
+ end
291
+ end
292
+ on :input do
293
+ controller :command do
294
+ action :filter_input, on: :input
295
+ action :keydown, on: :keydown
296
+ end
297
+ end
298
+ # Every item (root-level and grouped) wears this - the anatomy
299
+ # classes receive it as item_wiring at construction.
300
+ on :item do
301
+ controller :command do
302
+ action :activate, on: :click
303
+ action :pointer_highlight, on: :pointermove
304
+ end
305
+ end
306
+ end
307
+
308
+ option :filter, :boolean, default: true, doc: "Client-side filtering; false leaves the list server-driven."
309
+ option :loop, :boolean, default: false, doc: "Wraps arrow-key highlight movement past either end of the list."
310
+ option :placeholder, :string, doc: "The filter input's placeholder text."
311
+ option :list_label, :string, default: -> { I18n.t("poetry.command.list_label") },
312
+ doc: "The listbox's accessible name."
313
+ option :value, :string, doc: "Seats the initial highlight on the item with this value."
314
+ option :disabled, :boolean, default: false, doc: "Disables the filter input."
315
+ option :id, :string, doc: "The base DOM id; the input, list, and item ids derive from it."
316
+
317
+ part "command", "Root of the palette - the input row over the listbox, carrying the " \
318
+ "engine controller"
319
+ part "command-input-wrapper", "The input row - search icon + filter input above the list"
320
+ part "command-search-icon", "Decorative search glyph beside the input"
321
+ part "command-input", "The role=combobox filter input - real focus stays pinned here " \
322
+ "for the whole session; the highlight rides aria-activedescendant"
323
+ part "command-list", "The role=listbox holding empty/loading/items - the input's " \
324
+ "aria-controls target"
325
+ part "command-empty", "Zero-matches message - rendered hidden; the controller unhides " \
326
+ "it when the filter pass leaves no visible items"
327
+ part "command-loading", "Pending affordance (role=status) - rendered hidden; the HOST " \
328
+ "toggles it (Turbo frame events), Command never does"
329
+ part "command-group", "role=group labelled by its heading - hidden by the controller " \
330
+ "when every member item is filtered out",
331
+ states: {
332
+ "data-always-render" => "always_render: is set - the group survives every filter pass"
333
+ }
334
+ part "command-group-heading", "The group heading - styled, no ARIA role (the group " \
335
+ "points at it via aria-labelledby)"
336
+ part "command-item", "One role=option action row - highlight, filtering, and " \
337
+ "disablement ride here (never aria-selected in a bare Command)",
338
+ states: {
339
+ "data-value" => "always - the item's unique value (its server-stable id follows " \
340
+ "registration order)",
341
+ "data-highlighted" => "the item holds the highlight (bare; the controller " \
342
+ "twin-writes it with the input's aria-activedescendant)",
343
+ "data-disabled" => "disabled: is set (aria-disabled rides along)",
344
+ "data-keywords" => "keywords: given - extra filter terms beyond the label",
345
+ "data-always-render" => "always_render: is set - the item survives every filter pass",
346
+ "data-hidden" => "the filter scored the item zero (the controller pairs it with " \
347
+ "hidden; never rendered server-side)"
348
+ }
349
+ part "command-item-text", "The item's label span - the filter/typematch text source " \
350
+ "(shortcuts and icons excluded)"
351
+ part "command-shortcut", "Presentational keyboard hint - excluded from the filter text; " \
352
+ "Command never binds the hinted key"
353
+ part "command-separator", "Decorative divider (aria-hidden) - hidden by the controller " \
354
+ "whenever the query is non-empty"
355
+ part "command-status", "The sr-only polite result-count live region - the controller " \
356
+ "writes the debounced count from the localized templates",
357
+ states: {
358
+ "data-zero" => "always - the localized zero-results template",
359
+ "data-one" => "always - the localized one-result template",
360
+ "data-other" => "always - the localized many-results template (a literal count " \
361
+ "placeholder the controller interpolates)"
362
+ }
363
+
364
+ # Pulls the input-bound aria-* out of the root attributes.
365
+ # @api private
366
+ def initialize(attributes = {})
367
+ super
368
+ @input_aria = extract_input_aria!
369
+ end
370
+
371
+ # The input row's trailing content - the seam the CommandDialog
372
+ # seats its close button in (IN the row, never laid over the input).
373
+ # A lambda the owner supplies, called at render time in the OWNER's
374
+ # view context so it can render components. Never a helper kwarg:
375
+ # this is composition between two components of the gem.
376
+ # @api private
377
+ attr_writer :input_trailing
378
+
379
+ # The rendered trailing content of the input row, or nil.
380
+ # @api private
381
+ def input_trailing_html
382
+ @input_trailing&.call
383
+ end
384
+
385
+ # Enforces the accessible-name requirement.
386
+ # @api private
387
+ def before_render
388
+ return if named?
389
+
390
+ raise ArgumentError, "Command requires an accessible name for its input - pass 'aria-label' " \
391
+ "(or id: plus a visible label[for: \"<id>-input\"])"
392
+ end
393
+
394
+ # Stable server ids: "#{id}-input" / "#{id}-list" / "#{id}-item-<n>"
395
+ # (aria-activedescendant NEEDS server-stable option ids).
396
+ # @api private
397
+ def base_id
398
+ @base_id ||= id.presence || poetry_instance_id("poetry-command")
399
+ end
400
+
401
+ # The filter input's id.
402
+ # @api private
403
+ def input_id
404
+ "#{base_id}-input"
405
+ end
406
+
407
+ # The listbox's id - the input's aria-controls target.
408
+ # @api private
409
+ def list_id
410
+ "#{base_id}-list"
411
+ end
412
+
413
+ # The shared item registry for this render.
414
+ # @api private
415
+ def item_set
416
+ @item_set ||= ItemSet.new(base_id: base_id, highlight_value: value)
417
+ end
418
+
419
+ # Attributes for the palette root.
420
+ # @api private
421
+ def root_attributes
422
+ html_attributes.merge_if_not_set(
423
+ { "id" => base_id, "data-slot" => "command" }
424
+ .merge(stimulus_attributes_for(:root))
425
+ .merge(component_data_attributes)
426
+ )
427
+ end
428
+
429
+ # THE combobox: aria-expanded is STATICALLY true - the listbox is
430
+ # always rendered and visible in bare Command (Combobox flips it on
431
+ # its trigger instead). The activedescendant is server-rendered
432
+ # from the ItemSet (populated by the template's capture pass).
433
+ # @api private
434
+ def input_attributes
435
+ attrs = {
436
+ "type" => "text", "id" => input_id, "data-slot" => "command-input",
437
+ "role" => "combobox", "aria-expanded" => "true", "aria-controls" => list_id,
438
+ "aria-autocomplete" => "list", "autocomplete" => "off", "autocorrect" => "off",
439
+ "spellcheck" => "false", "class" => css(:input)
440
+ }
441
+ attrs["placeholder"] = placeholder if placeholder.present?
442
+ attrs["disabled"] = true if disabled
443
+ attrs["aria-activedescendant"] = item_set.highlighted_id if item_set.highlighted_id
444
+ attrs.merge!(stimulus_attributes_for(:input))
445
+ attrs.merge!(input_aria_attributes)
446
+ attrs
447
+ end
448
+
449
+ # Attributes for the role=listbox list.
450
+ # @api private
451
+ def list_attributes
452
+ {
453
+ "id" => list_id, "data-slot" => "command-list", "role" => "listbox",
454
+ "tabindex" => "-1", "aria-label" => list_label, "class" => css(:list)
455
+ }
456
+ end
457
+
458
+ # Zero-matches message - rendered hidden; the controller unhides it
459
+ # when the filter pass leaves no visible items. No role: the sr
460
+ # story rides the status live region.
461
+ # @api private
462
+ def empty_part
463
+ content_tag(:div, empty? ? empty : t("poetry.command.empty"),
464
+ "data-slot" => "command-empty", "hidden" => true, "class" => css(:empty))
465
+ end
466
+
467
+ # Pending affordance (role=status) - rendered hidden; the HOST
468
+ # toggles it.
469
+ # @api private
470
+ def loading_part
471
+ content_tag(:div, "data-slot" => "command-loading", "role" => "status",
472
+ "hidden" => true, "class" => css(:loading)) do
473
+ safe_join([content_tag(:span, t("poetry.command.loading"), class: css(:sr_only)),
474
+ loading].compact)
475
+ end
476
+ end
477
+
478
+ # The sr-only polite result-count region - the controller writes
479
+ # the debounced count from the LOCALIZED templates carried as
480
+ # data attributes (data-other keeps a literal %{count}
481
+ # placeholder), so the engine stays i18n-free.
482
+ # @api private
483
+ def status_part
484
+ content_tag(:span, nil,
485
+ "data-slot" => "command-status", "role" => "status", "aria-live" => "polite",
486
+ "class" => css(:status),
487
+ "data-zero" => t("poetry.command.results", count: 0),
488
+ "data-one" => t("poetry.command.results", count: 1),
489
+ "data-other" => t("poetry.command.results.other", count: "%{count}")) # rubocop:disable Style/FormatStringToken
490
+ end
491
+
492
+ private
493
+
494
+ def named?
495
+ id.present? || @input_aria["label"].present? || @input_aria["labelledby"].present?
496
+ end
497
+
498
+ # Pull the input-bound aria-* out of the root's html attributes
499
+ # (both flat "aria-label" and nested aria: {label:} spellings) so
500
+ # the accessible name wires the combobox, not the wrapper.
501
+ def extract_input_aria!
502
+ aria = {}.with_indifferent_access
503
+ nested = @html_attributes.delete("aria")
504
+ nested.each { |nested_key, nested_value| aria[nested_key] = nested_value } if nested.is_a?(Hash)
505
+ @html_attributes.keys.grep(/\Aaria-/).each do |flat|
506
+ aria[flat.delete_prefix("aria-")] = @html_attributes.delete(flat)
507
+ end
508
+ aria
509
+ end
510
+
511
+ def input_aria_attributes
512
+ INPUT_ARIA_KEYS.each_with_object({}) do |key, attrs|
513
+ attrs["aria-#{key}"] = @input_aria[key] unless @input_aria[key].nil?
514
+ end
515
+ end
516
+
517
+ def item_wiring
518
+ @item_wiring ||= stimulus_attributes_for(:item)
519
+ end
520
+
521
+ private :base_id, :input_id, :list_id, :item_set, :root_attributes, :input_attributes, :list_attributes
522
+ private :empty_part, :loading_part, :status_part
523
+ end
524
+ end
525
+ end
526
+ end
@@ -0,0 +1,14 @@
1
+ <%= content_tag(:div, **root_attributes.to_attributes) do %>
2
+ <%= trigger %>
3
+ <%= content_tag(:dialog, **Poetry::Core::HTML::Attributes.new(dialog_attributes).to_attributes) do %>
4
+ <%# sr-only header (source-exact): the dialog stays labelled/described
5
+ while the palette owns the visible surface. %>
6
+ <div data-slot="dialog-header" class="sr-only">
7
+ <h2 id="<%= title_id %>" data-slot="dialog-title"><%= title %></h2>
8
+ <p id="<%= description_id %>" data-slot="dialog-description"><%= description %></p>
9
+ </div>
10
+ <%# The close X (when shown) rides INSIDE the command's input row - the
11
+ embedded Command renders it as the row's trailing item. %>
12
+ <%= render(command) %>
13
+ <% end %>
14
+ <% end %>