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,277 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ # DataTable family: the sortable, filterable, paginated table plus
6
+ # its URL-state object.
7
+ module DataTable
8
+ # A data table: the composed Table with sortable column headers, a
9
+ # filter box, and Pagination, where sort/filter/page live in the
10
+ # URL as query params - shareable and back-button-correct. Data
11
+ # stays with the host: the controller builds a sanitized State from
12
+ # params, runs its own scope, and hands the page of rows in.
13
+ # Row-level mutation (inline edit, row actions) belongs to reactive
14
+ # components rendered inside cells, never to this component.
15
+ #
16
+ # @example A sortable notes table
17
+ # <%= poetry_data_table(rows: @notes, state: state, total: @pages,
18
+ # path: ->(p) { notes_path(**p) }, caption: "Notes") do |t| %>
19
+ # <% t.with_column("Title", key: :title, sortable: true) { |note| note.title } %>
20
+ # <% t.with_column("Created", key: :created_at, sortable: true) { |note| note.created_at.to_date } %>
21
+ # <% end %>
22
+ class Component < Poetry::Core::Component
23
+ # Projected into the registry, llms.txt, and the agent surface.
24
+ AGENT_RULES = [
25
+ "Build State.from_params(params, sortable: [...]) in the controller - NEVER order by raw params; " \
26
+ "the whitelist is what makes state.order_clause injection-safe.",
27
+ "Column cell blocks RETURN the cell content ({ |row| row.title }) - they must not write to the " \
28
+ "template buffer.",
29
+ "Sort/filter/page are URL state over GET links and a GET form. Row mutations (inline edit, row " \
30
+ "actions) belong to poetry-reactive components rendered inside cells - never to this component.",
31
+ "Give the table a caption: - it is the table's accessible purpose."
32
+ ].freeze
33
+
34
+ # The cell block is a per-row RENDERER, not captured content: it
35
+ # receives each row record (SLOT_BLOCK_YIELDS exempts it from the
36
+ # yieldless contract), and a column cannot exist without one.
37
+ SLOT_BLOCK_YIELDS = { column: "the row record" }.freeze
38
+ # Slots whose block content is required, with the hint the check surfaces.
39
+ SLOT_REQUIRED_CONTENT = { column: "the cell renderer - { |row| ... }" }.freeze
40
+
41
+ # The required slots, stated statically so static checks can flag
42
+ # a missing column without rendering.
43
+ REQUIRED_SLOTS = { column: "at least one column" }.freeze
44
+
45
+ renders_many :columns,
46
+ doc: "Columns are DECLARED here and rendered per row by the template. A sortable column's key " \
47
+ "must be in the state's whitelist - catching drift between the view's columns and the " \
48
+ "controller's sortable: list at render, not as a silently unsortable header.",
49
+ renders: lambda { |label, key: nil, sortable: false, classes: nil, &cell|
50
+ if sortable && !key
51
+ raise ArgumentError,
52
+ "DataTable column #{label.inspect}: sortable: true requires a key:"
53
+ end
54
+ if sortable && !state.sortable?(key)
55
+ raise ArgumentError,
56
+ "DataTable column #{key.inspect} is sortable in the view but missing from the " \
57
+ "controller's State sortable: whitelist (#{state.sortable.inspect}) - add it there"
58
+ end
59
+ raise ArgumentError, "DataTable column #{label.inspect} requires a cell block" unless cell
60
+
61
+ column_defs << Column.new(label: label, key: key&.to_s, sortable: sortable, classes: classes,
62
+ cell: cell)
63
+ nil
64
+ }
65
+
66
+ # The whole selection surface is selectable?-gated.
67
+ use_stimulus do
68
+ on :root, if: :selectable? do
69
+ controller :table_selection do
70
+ register
71
+ value :label, from: :selected_count_label
72
+ end
73
+ end
74
+ on :select_all do
75
+ controller :table_selection do
76
+ target :all
77
+ action :toggleAll, on: :change
78
+ end
79
+ end
80
+ on :row_checkbox do
81
+ controller :table_selection do
82
+ action :press, on: %i[pointerdown keydown]
83
+ action :toggled, on: :change
84
+ end
85
+ end
86
+ end
87
+
88
+ option :caption, :string, doc: "The table's accessible purpose, rendered as its <caption>."
89
+ option :empty_text, :string, default: "No results.", doc: "Shown in a full-width row when rows are empty."
90
+ option :filter, :boolean, default: true, doc: "Renders the filter form; false drops the toolbar row."
91
+ option :filter_label, :string, default: "Filter", doc: "The filter input's accessible label."
92
+ option :filter_placeholder, :string, default: "Filter…", doc: "The filter input's placeholder text."
93
+ option :filter_name, :string, default: "q", doc: "The query-param key the filter submits under."
94
+ option :frame, :string,
95
+ doc: "Wrap in a <turbo-frame data-turbo-action=\"advance\"> so hosts with Turbo scope the round trip " \
96
+ "to the table while the URL still advances. The host response must render the same frame id."
97
+ option :sticky_header, :boolean, default: false,
98
+ doc: "Forwarded to the inner Table: sticky_header pins the thead while the " \
99
+ "table's scroll container scrolls; container_class caps that " \
100
+ "container's height (\"max-h-96\") - without a cap nothing sticks. The " \
101
+ "sticky scroll region needs an accessible name (the ScrollArea rule); " \
102
+ "scroll_label: falls back to caption:."
103
+ option :container_class, :string,
104
+ doc: "Caps the scroll container's height (e.g. \"max-h-96\") - without a cap the sticky header has " \
105
+ "nothing to stick inside."
106
+ option :scroll_label, :string, doc: "Accessible name for the sticky scroll region; falls back to caption:."
107
+ option :selectable, ActiveModel::Type::Value.new,
108
+ doc: "Row selection: a lambda mapping each row to its id turns the feature ON - a leading checkbox " \
109
+ "column (select-all with a real indeterminate middle state, shift ranges, count announcements) " \
110
+ "whose checkboxes ARE the form value (selection_name[], plain checkboxes with no JS). Pair with " \
111
+ "the action-bar block for bulk actions."
112
+ option :selection_name, :string, default: "selected_ids",
113
+ doc: "The checkbox field name; selected row ids post as selection_name[]."
114
+
115
+ part "data-table", "Root surface - toolbar, table, and pagination footer stack here"
116
+ part "data-table-toolbar", "The row above the table holding the filter form - " \
117
+ "renders unless filter: false"
118
+ part "data-table-filter", "The GET filter form (role=search) - hidden fields carry the " \
119
+ "current sort; a new filter resets the page"
120
+ part "table-container", "The composed Table's scroll container - Table renders it, " \
121
+ "this surface owns where it sits"
122
+ part "data-table-footer", "The Pagination row - renders when total: is more than one page"
123
+ # The selection checkboxes (selectable: only) render INSIDE the
124
+ # composed Table's root, so ownership attributes them to Table
125
+ # (the NumberField-stepper precedent) - prose, not parts:
126
+ # data-slot="data-table-select-all" is the header checkbox (the
127
+ # controller drives its INDETERMINATE middle state as a property);
128
+ # data-slot="data-table-select-row" is one row's checkbox - THE
129
+ # form value (selection_name[], value from the selectable: lambda);
130
+ # the controller mirrors aria-selected/data-selected onto rows.
131
+
132
+ # The structural collaborators handed in at construction.
133
+ # @api private
134
+ attr_reader :rows, :state, :total
135
+
136
+ # Enforces at least one declared column.
137
+ # @api private
138
+ def before_render
139
+ raise ArgumentError, "DataTable requires at least one with_column" unless columns?
140
+ end
141
+
142
+ # The declared columns in order.
143
+ # @api private
144
+ def column_defs
145
+ @column_defs ||= []
146
+ end
147
+
148
+ # Attributes for the root surface.
149
+ # @api private
150
+ def root_attributes
151
+ attrs = { "data-slot" => "data-table" }.merge(component_data_attributes)
152
+ attrs = attrs.merge(stimulus_attributes_for(:root))
153
+ html_attributes.merge_if_not_set(attrs)
154
+ end
155
+
156
+ # Whether row selection is on (selectable: present).
157
+ # @api private
158
+ def selectable?
159
+ selectable.present?
160
+ end
161
+
162
+ # Attributes for the select-all header checkbox.
163
+ # @api private
164
+ def select_all_attributes
165
+ {
166
+ "type" => "checkbox", "data-slot" => "data-table-select-all",
167
+ "class" => css(:checkbox),
168
+ "aria-label" => t("poetry.data_table.select_all")
169
+ }.merge(stimulus_attributes_for(:select_all))
170
+ end
171
+
172
+ # Attributes for one row's selection checkbox - the form value.
173
+ # @api private
174
+ def select_row_attributes(row)
175
+ {
176
+ "type" => "checkbox", "data-slot" => "data-table-select-row",
177
+ "name" => "#{selection_name}[]", "value" => selectable.call(row),
178
+ "class" => css(:checkbox),
179
+ "aria-label" => t("poetry.data_table.select_row")
180
+ }.merge(stimulus_attributes_for(:row_checkbox))
181
+ end
182
+
183
+ # The localized count-announcement template.
184
+ # @api private
185
+ def selected_count_label = t("poetry.data_table.selected_count")
186
+
187
+ # Builds a URL for the given state params via the path: lambda.
188
+ # @api private
189
+ def path_for(params)
190
+ @path.call(params)
191
+ end
192
+
193
+ # th attributes: the Table head classes plus aria-sort on the
194
+ # actively sorted column (the APG announcement mechanism - one
195
+ # column at a time).
196
+ # @api private
197
+ def head_attributes(column)
198
+ attrs = { "data-slot" => "table-head", class: merge_classes(Table::Style.css(:head), column.classes) }
199
+ if column.sortable && state.sorted_by?(column.key)
200
+ attrs["aria-sort"] = state.dir == "asc" ? "ascending" : "descending"
201
+ end
202
+ attrs
203
+ end
204
+
205
+ # td attributes for one column's cells.
206
+ # @api private
207
+ def cell_attributes(column)
208
+ { "data-slot" => "table-cell", class: merge_classes(Table::Style.css(:cell), column.classes) }
209
+ end
210
+
211
+ # The sort affordance: a ghost Button-styled LINK to the toggled
212
+ # ordering (a real href - keyboard, middle-click, and share all work).
213
+ # @api private
214
+ def sort_link_options(column)
215
+ {
216
+ tag: :a, href: path_for(state.toggle_params(column.key)),
217
+ variant: :ghost, size: :sm, class: Style.css(:sort_link),
218
+ data: { slot: "data-table-sort" }
219
+ }
220
+ end
221
+
222
+ # The header glyph for a column's current sort state.
223
+ # @api private
224
+ def sort_icon(column)
225
+ return :"arrow-up-down" unless state.sorted_by?(column.key)
226
+
227
+ state.dir == "asc" ? :"arrow-up" : :"arrow-down"
228
+ end
229
+
230
+ # The filter input's id (the label's for= target).
231
+ # @api private
232
+ def filter_id
233
+ @filter_id ||= "#{poetry_instance_id("poetry-data-table")}-filter"
234
+ end
235
+
236
+ # The filter form's action is the bare collection URL; the current
237
+ # sort rides hidden fields and the page deliberately resets (a new
238
+ # filter is a new list).
239
+ # @api private
240
+ def filter_form_action
241
+ path_for({})
242
+ end
243
+
244
+ # Whether the pagination footer renders (total: > 1 page).
245
+ # @api private
246
+ def pagination?
247
+ total.present? && total > 1
248
+ end
249
+
250
+ # A declared column: header label, the whitelisted sort key, and the
251
+ # cell block (called per row, returns the cell content).
252
+ # @api private
253
+ Column = Data.define(:label, :key, :sortable, :classes, :cell)
254
+
255
+ private
256
+
257
+ # rows/state/path/total are structural collaborators, not typed
258
+ # options (see Pagination's path: for the precedent).
259
+ def initialize(rows:, state:, path:, total: nil, **)
260
+ super(**)
261
+ @rows = rows
262
+ @state = state
263
+ @path = path
264
+ @total = total
265
+ end
266
+
267
+ def merge_classes(*values)
268
+ values.compact.join(" ")
269
+ end
270
+
271
+ private :column_defs, :root_attributes, :selectable?, :select_all_attributes, :select_row_attributes
272
+ private :selected_count_label, :path_for, :head_attributes, :cell_attributes, :sort_link_options, :sort_icon
273
+ private :filter_id, :filter_form_action, :pagination?
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module DataTable
6
+ # The DataTable preview: a small invoices dataset in the three states
7
+ # a host sees - sorted, filtered+paginated, and empty.
8
+ class Preview < Poetry::Core::Preview::Base
9
+ Invoice = Struct.new(:number, :customer, :amount, keyword_init: true)
10
+
11
+ ROWS = [
12
+ Invoice.new(number: "INV-001", customer: "Acme", amount: "$250.00"),
13
+ Invoice.new(number: "INV-002", customer: "Globex", amount: "$1,150.00"),
14
+ Invoice.new(number: "INV-003", customer: "Initech", amount: "$420.00")
15
+ ].freeze
16
+
17
+ def default
18
+ render_component(rows: ROWS, state: state(sort: "number", dir: "asc"),
19
+ path: path, caption: "A list of recent invoices.") do |table|
20
+ columns(table)
21
+ end
22
+ end
23
+
24
+ # Row selection: selectable: maps rows to ids; the header
25
+ # checkbox tri-states, shift-click ranges, the checkboxes ARE the
26
+ # selected_ids[] form value. Pair with the action-bar block.
27
+ def selectable
28
+ render_component(rows: ROWS, state: state(sort: "number", dir: "asc"),
29
+ path: path, caption: "A list of recent invoices.",
30
+ selectable: ->(invoice) { invoice.number }) do |table| # rubocop:disable Style/SymbolProc
31
+ columns(table)
32
+ end
33
+ end
34
+
35
+ def filtered_and_paginated
36
+ render_component(rows: ROWS.first(2), total: 3,
37
+ state: state(q: "inv", sort: "customer", dir: "desc", page: 2),
38
+ path: path, caption: "A list of recent invoices.") do |table|
39
+ columns(table)
40
+ end
41
+ end
42
+
43
+ def empty
44
+ render_component(rows: [], state: state(q: "zzz"), path: path,
45
+ caption: "A list of recent invoices.") do |table|
46
+ columns(table)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def columns(table)
53
+ table.with_column("Invoice", key: :number, sortable: true, &:number)
54
+ table.with_column("Customer", key: :customer, sortable: true, &:customer)
55
+ table.with_column("Amount", classes: "text-right", &:amount)
56
+ end
57
+
58
+ def state(**params)
59
+ State.from_params(params, sortable: %w[number customer])
60
+ end
61
+
62
+ def path
63
+ ->(params) { "?#{params.to_query}" }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module DataTable
6
+ # The DataTable's URL state - sort/filter/page as query params
7
+ # (shareable, back-button-correct), sanitized at the door so request
8
+ # params can never reach an ORDER BY. `sort` is kept ONLY when it
9
+ # appears in the `sortable:` whitelist and `dir` only when it is
10
+ # asc/desc - so #order_clause is safe by construction, never by
11
+ # caller discipline.
12
+ #
13
+ # @example Building state in the controller
14
+ # state = Poetry::Ui::DataTable::State.from_params(
15
+ # params, sortable: %w[title created_at], default_sort: "created_at", default_dir: "desc"
16
+ # )
17
+ # scope = Note.all
18
+ # scope = scope.where("title LIKE ?", "%#{Note.sanitize_sql_like(state.q)}%") if state.q
19
+ # notes = scope.order(state.order_clause).offset(...).limit(per)
20
+ class State
21
+ # The closed vocabulary for the sort direction.
22
+ DIRECTIONS = %w[asc desc].freeze
23
+
24
+ # The sanitized pieces: filter query, sort column, direction,
25
+ # page number, and the sortable whitelist.
26
+ attr_reader :q, :sort, :dir, :page, :sortable
27
+
28
+ # Builds state from request params: sort survives only when it
29
+ # appears in the sortable: whitelist, dir only when asc/desc, the
30
+ # page floors at 1, and the query is stripped.
31
+ #
32
+ # @param params [Hash] the request params
33
+ # @param sortable [Array] the whitelist of sortable column keys
34
+ # @param default_sort [String, nil] the column when none is given
35
+ # @param default_dir [String] the direction when none is given
36
+ def self.from_params(params, sortable:, default_sort: nil, default_dir: "asc")
37
+ allowed = Array(sortable).map(&:to_s)
38
+ raw_sort = params[:sort].to_s
39
+ raw_dir = params[:dir].to_s
40
+
41
+ new(
42
+ q: params[:q].to_s.strip.presence,
43
+ sort: allowed.include?(raw_sort) ? raw_sort : default_sort&.to_s,
44
+ dir: DIRECTIONS.include?(raw_dir) ? raw_dir : default_dir.to_s,
45
+ page: [params[:page].to_i, 1].max,
46
+ sortable: allowed
47
+ )
48
+ end
49
+
50
+ # Direct construction with pre-sanitized values; prefer
51
+ # .from_params for anything that came in on a request.
52
+ def initialize(q: nil, sort: nil, dir: "asc", page: 1, sortable: [])
53
+ @q = q
54
+ @sort = sort
55
+ @dir = dir
56
+ @page = page
57
+ @sortable = sortable
58
+ end
59
+
60
+ # Whether the column is in the sortable whitelist.
61
+ def sortable?(column)
62
+ sortable.include?(column.to_s)
63
+ end
64
+
65
+ # Whether the column is the actively sorted one.
66
+ def sorted_by?(column)
67
+ sort == column.to_s
68
+ end
69
+
70
+ # Injection-safe by construction: sort survived the whitelist and dir
71
+ # the asc/desc check, so this can go straight into .order(). Nil when
72
+ # nothing is sorted.
73
+ def order_clause
74
+ return nil unless sort
75
+
76
+ { sort => dir.to_sym }
77
+ end
78
+
79
+ # The full state as query params (blank pieces omitted; page 1 is the
80
+ # default and stays out of the URL).
81
+ def to_params
82
+ params = {}
83
+ params[:q] = q if q
84
+ if sort
85
+ params[:sort] = sort
86
+ params[:dir] = dir
87
+ end
88
+ params[:page] = page if page > 1
89
+ params
90
+ end
91
+
92
+ # Params for clicking a sortable header: same column flips the
93
+ # direction, a new column starts ascending; the page resets (a new
94
+ # ordering is a new list).
95
+ def toggle_params(column)
96
+ column = column.to_s
97
+ next_dir = sorted_by?(column) && dir == "asc" ? "desc" : "asc"
98
+ to_params.except(:page).merge(sort: column, dir: next_dir)
99
+ end
100
+
101
+ # Params for navigating to a page of the current view.
102
+ def page_params(number)
103
+ number > 1 ? to_params.merge(page: number) : to_params.except(:page)
104
+ end
105
+
106
+ # The state a filter submission must carry as hidden fields: sort
107
+ # survives, the page resets (a new filter is a new list), and q is
108
+ # the form input itself.
109
+ def filter_params
110
+ to_params.except(:q, :page)
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Poetry
4
+ module Ui
5
+ module DataTable
6
+ # Re-expressed through the cn-* theme layer: the chrome rides
7
+ # poetry-named rules (own component - no upstream cn vocabulary).
8
+ class Style < Poetry::Core::Style
9
+ base "w-full"
10
+
11
+ element :toolbar, "cn-data-table-toolbar flex items-center"
12
+ element :container, "cn-data-table-container overflow-hidden " \
13
+ "[&_tr[data-selected]]:bg-muted/50"
14
+ element :footer, "cn-data-table-footer"
15
+ element :empty, "cn-data-table-empty"
16
+
17
+ # POETRY ADDITION: the caption lives INSIDE the bordered container
18
+ # (a <caption> cannot leave its <table>), so Table's mt-4 needs a
19
+ # matching bottom margin or the text hugs the frame's border.
20
+ # Upstream never composes this - its bordered data-table demo has
21
+ # no caption; its captioned table demo has no border.
22
+ element :caption, "mb-4"
23
+
24
+ # POETRY ADDITION: the ghost sort button's px-3 shifts the header
25
+ # LABEL 12px off the column's data text (upstream's demo carries
26
+ # the same quirk and users patch it with a negative margin - here
27
+ # the component owns the link, so the component compensates).
28
+ # Negative on BOTH sides: only the binding side of the cell's
29
+ # text-align has any layout effect, so one class serves left- and
30
+ # right-aligned columns alike.
31
+ element :sort_link, "-mx-3"
32
+ # Selection: native checkboxes on the token accent; the
33
+ # selected-row wash rides the container (rows are controller-
34
+ # marked data-selected).
35
+ element :checkbox, "size-4 shrink-0 translate-y-px rounded-sm border-input " \
36
+ "accent-primary outline-none " \
37
+ "focus-visible:ring-[3px] focus-visible:ring-ring/50"
38
+ element :select_cell, "w-10"
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,7 @@
1
+ <%# Progressive enhancement: the native input is the visible control (and
2
+ the form value) until the controller builds segments into the group
3
+ and hides it from the tab order. %>
4
+ <%= content_tag(:div, **root_attributes.to_attributes) do %>
5
+ <%= content_tag(:span, "", **Poetry::Core::HTML::Attributes.new(group_attributes).to_attributes) %>
6
+ <%= tag.input(**input_attributes.to_attributes) %>
7
+ <% end %>