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.
- checksums.yaml +7 -0
- data/.gitattributes +6 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +87 -0
- data/THIRD_PARTY_NOTICES.md +201 -0
- data/app/components/poetry/ui/accordion/component.html.erb +3 -0
- data/app/components/poetry/ui/accordion/component.rb +197 -0
- data/app/components/poetry/ui/accordion/preview.rb +39 -0
- data/app/components/poetry/ui/accordion/style.rb +37 -0
- data/app/components/poetry/ui/alert/component.html.erb +6 -0
- data/app/components/poetry/ui/alert/component.rb +61 -0
- data/app/components/poetry/ui/alert/preview.rb +35 -0
- data/app/components/poetry/ui/alert/style.rb +24 -0
- data/app/components/poetry/ui/alert_dialog/component.html.erb +17 -0
- data/app/components/poetry/ui/alert_dialog/component.rb +208 -0
- data/app/components/poetry/ui/alert_dialog/preview.rb +48 -0
- data/app/components/poetry/ui/alert_dialog/style.rb +46 -0
- data/app/components/poetry/ui/aspect_ratio/component.rb +63 -0
- data/app/components/poetry/ui/aspect_ratio/preview.rb +29 -0
- data/app/components/poetry/ui/aspect_ratio/style.rb +13 -0
- data/app/components/poetry/ui/attachment/component.html.erb +13 -0
- data/app/components/poetry/ui/attachment/component.rb +139 -0
- data/app/components/poetry/ui/attachment/preview.rb +83 -0
- data/app/components/poetry/ui/attachment/style.rb +59 -0
- data/app/components/poetry/ui/autocomplete/component.html.erb +16 -0
- data/app/components/poetry/ui/autocomplete/component.rb +193 -0
- data/app/components/poetry/ui/autocomplete/preview.rb +39 -0
- data/app/components/poetry/ui/autocomplete/style.rb +18 -0
- data/app/components/poetry/ui/avatar/component.rb +103 -0
- data/app/components/poetry/ui/avatar/group_preview.html.erb +11 -0
- data/app/components/poetry/ui/avatar/preview.rb +50 -0
- data/app/components/poetry/ui/avatar/style.rb +34 -0
- data/app/components/poetry/ui/badge/component.rb +73 -0
- data/app/components/poetry/ui/badge/preview.rb +54 -0
- data/app/components/poetry/ui/badge/style.rb +35 -0
- data/app/components/poetry/ui/breadcrumb/component.html.erb +32 -0
- data/app/components/poetry/ui/breadcrumb/component.rb +102 -0
- data/app/components/poetry/ui/breadcrumb/dropdown_crumb_preview.html.erb +21 -0
- data/app/components/poetry/ui/breadcrumb/preview.rb +45 -0
- data/app/components/poetry/ui/breadcrumb/style.rb +25 -0
- data/app/components/poetry/ui/bubble/component.html.erb +4 -0
- data/app/components/poetry/ui/bubble/component.rb +106 -0
- data/app/components/poetry/ui/bubble/preview.rb +62 -0
- data/app/components/poetry/ui/bubble/style.rb +47 -0
- data/app/components/poetry/ui/button/component.html.erb +14 -0
- data/app/components/poetry/ui/button/component.rb +168 -0
- data/app/components/poetry/ui/button/preview.rb +94 -0
- data/app/components/poetry/ui/button/style.rb +41 -0
- data/app/components/poetry/ui/button_group/component.rb +70 -0
- data/app/components/poetry/ui/button_group/popup_triggers_preview.html.erb +21 -0
- data/app/components/poetry/ui/button_group/preview.rb +46 -0
- data/app/components/poetry/ui/button_group/style.rb +27 -0
- data/app/components/poetry/ui/button_group/text_prefix_preview.html.erb +7 -0
- data/app/components/poetry/ui/calendar/component.html.erb +72 -0
- data/app/components/poetry/ui/calendar/component.rb +366 -0
- data/app/components/poetry/ui/calendar/preview.rb +55 -0
- data/app/components/poetry/ui/calendar/style.rb +55 -0
- data/app/components/poetry/ui/card/component.html.erb +11 -0
- data/app/components/poetry/ui/card/component.rb +90 -0
- data/app/components/poetry/ui/card/preview.rb +29 -0
- data/app/components/poetry/ui/card/style.rb +23 -0
- data/app/components/poetry/ui/carousel/component.html.erb +17 -0
- data/app/components/poetry/ui/carousel/component.rb +179 -0
- data/app/components/poetry/ui/carousel/preview.rb +67 -0
- data/app/components/poetry/ui/carousel/style.rb +39 -0
- data/app/components/poetry/ui/checkbox/component.html.erb +22 -0
- data/app/components/poetry/ui/checkbox/component.rb +179 -0
- data/app/components/poetry/ui/checkbox/preview.rb +68 -0
- data/app/components/poetry/ui/checkbox/style.rb +28 -0
- data/app/components/poetry/ui/clipboard_text/component.html.erb +15 -0
- data/app/components/poetry/ui/clipboard_text/component.rb +154 -0
- data/app/components/poetry/ui/clipboard_text/preview.rb +24 -0
- data/app/components/poetry/ui/clipboard_text/style.rb +23 -0
- data/app/components/poetry/ui/code_block/component.html.erb +13 -0
- data/app/components/poetry/ui/code_block/component.rb +141 -0
- data/app/components/poetry/ui/code_block/preview.rb +40 -0
- data/app/components/poetry/ui/code_block/style.rb +38 -0
- data/app/components/poetry/ui/collapsible/component.html.erb +4 -0
- data/app/components/poetry/ui/collapsible/component.rb +134 -0
- data/app/components/poetry/ui/collapsible/preview.rb +24 -0
- data/app/components/poetry/ui/combobox/component.html.erb +49 -0
- data/app/components/poetry/ui/combobox/component.rb +1086 -0
- data/app/components/poetry/ui/combobox/preview.rb +164 -0
- data/app/components/poetry/ui/combobox/style.rb +148 -0
- data/app/components/poetry/ui/command/component.html.erb +20 -0
- data/app/components/poetry/ui/command/component.rb +526 -0
- data/app/components/poetry/ui/command/dialog_component.html.erb +14 -0
- data/app/components/poetry/ui/command/dialog_component.rb +206 -0
- data/app/components/poetry/ui/command/dialog_preview.rb +39 -0
- data/app/components/poetry/ui/command/preview.rb +121 -0
- data/app/components/poetry/ui/command/style.rb +78 -0
- data/app/components/poetry/ui/composable_trigger.rb +63 -0
- data/app/components/poetry/ui/context_menu/component.html.erb +6 -0
- data/app/components/poetry/ui/context_menu/component.rb +358 -0
- data/app/components/poetry/ui/context_menu/preview.rb +111 -0
- data/app/components/poetry/ui/context_menu/style.rb +59 -0
- data/app/components/poetry/ui/data_table/component.html.erb +78 -0
- data/app/components/poetry/ui/data_table/component.rb +277 -0
- data/app/components/poetry/ui/data_table/preview.rb +68 -0
- data/app/components/poetry/ui/data_table/state.rb +115 -0
- data/app/components/poetry/ui/data_table/style.rb +42 -0
- data/app/components/poetry/ui/date_field/component.html.erb +7 -0
- data/app/components/poetry/ui/date_field/component.rb +209 -0
- data/app/components/poetry/ui/date_field/preview.rb +42 -0
- data/app/components/poetry/ui/date_field/style.rb +35 -0
- data/app/components/poetry/ui/date_picker/component.html.erb +23 -0
- data/app/components/poetry/ui/date_picker/component.rb +242 -0
- data/app/components/poetry/ui/date_picker/preview.rb +31 -0
- data/app/components/poetry/ui/date_picker/style.rb +25 -0
- data/app/components/poetry/ui/deferred/component.html.erb +16 -0
- data/app/components/poetry/ui/deferred/component.rb +90 -0
- data/app/components/poetry/ui/deferred/preview.rb +24 -0
- data/app/components/poetry/ui/dialog/component.html.erb +21 -0
- data/app/components/poetry/ui/dialog/component.rb +194 -0
- data/app/components/poetry/ui/dialog/preview.rb +33 -0
- data/app/components/poetry/ui/dialog/style.rb +30 -0
- data/app/components/poetry/ui/drawer/component.html.erb +16 -0
- data/app/components/poetry/ui/drawer/component.rb +207 -0
- data/app/components/poetry/ui/drawer/preview.rb +69 -0
- data/app/components/poetry/ui/drawer/style.rb +105 -0
- data/app/components/poetry/ui/dropdown_menu/component.html.erb +6 -0
- data/app/components/poetry/ui/dropdown_menu/component.rb +350 -0
- data/app/components/poetry/ui/dropdown_menu/preview.rb +101 -0
- data/app/components/poetry/ui/dropdown_menu/style.rb +60 -0
- data/app/components/poetry/ui/empty/component.html.erb +10 -0
- data/app/components/poetry/ui/empty/component.rb +79 -0
- data/app/components/poetry/ui/empty/preview.rb +27 -0
- data/app/components/poetry/ui/empty/style.rb +27 -0
- data/app/components/poetry/ui/family_identity.rb +52 -0
- data/app/components/poetry/ui/field/component.html.erb +19 -0
- data/app/components/poetry/ui/field/component.rb +183 -0
- data/app/components/poetry/ui/field/preview.html.erb +30 -0
- data/app/components/poetry/ui/field/preview.rb +72 -0
- data/app/components/poetry/ui/field/style.rb +68 -0
- data/app/components/poetry/ui/field_group/component.rb +61 -0
- data/app/components/poetry/ui/field_group/preview.html.erb +30 -0
- data/app/components/poetry/ui/field_group/preview.rb +20 -0
- data/app/components/poetry/ui/field_group/style.rb +25 -0
- data/app/components/poetry/ui/field_separator/component.rb +70 -0
- data/app/components/poetry/ui/field_separator/preview.rb +18 -0
- data/app/components/poetry/ui/field_separator/style.rb +20 -0
- data/app/components/poetry/ui/fieldset/component.rb +90 -0
- data/app/components/poetry/ui/fieldset/preview.html.erb +22 -0
- data/app/components/poetry/ui/fieldset/preview.rb +25 -0
- data/app/components/poetry/ui/fieldset/style.rb +18 -0
- data/app/components/poetry/ui/file_input/component.html.erb +14 -0
- data/app/components/poetry/ui/file_input/component.rb +188 -0
- data/app/components/poetry/ui/file_input/preview.rb +30 -0
- data/app/components/poetry/ui/file_input/style.rb +35 -0
- data/app/components/poetry/ui/hover_card/component.html.erb +6 -0
- data/app/components/poetry/ui/hover_card/component.rb +201 -0
- data/app/components/poetry/ui/hover_card/preview.rb +60 -0
- data/app/components/poetry/ui/hover_card/style.rb +17 -0
- data/app/components/poetry/ui/icon/component.rb +127 -0
- data/app/components/poetry/ui/icon/preview.rb +21 -0
- data/app/components/poetry/ui/input/component.rb +80 -0
- data/app/components/poetry/ui/input/preview.rb +35 -0
- data/app/components/poetry/ui/input/style.rb +18 -0
- data/app/components/poetry/ui/input_group/button_preview.html.erb +12 -0
- data/app/components/poetry/ui/input_group/component.rb +69 -0
- data/app/components/poetry/ui/input_group/preview.rb +23 -0
- data/app/components/poetry/ui/input_group/search_preview.html.erb +13 -0
- data/app/components/poetry/ui/input_group/style.rb +40 -0
- data/app/components/poetry/ui/input_group/textarea_preview.html.erb +10 -0
- data/app/components/poetry/ui/input_group_field.rb +61 -0
- data/app/components/poetry/ui/input_otp/component.html.erb +25 -0
- data/app/components/poetry/ui/input_otp/component.rb +250 -0
- data/app/components/poetry/ui/input_otp/preview.rb +91 -0
- data/app/components/poetry/ui/input_otp/style.rb +36 -0
- data/app/components/poetry/ui/item/component.html.erb +13 -0
- data/app/components/poetry/ui/item/component.rb +99 -0
- data/app/components/poetry/ui/item/group_preview.html.erb +15 -0
- data/app/components/poetry/ui/item/preview.rb +52 -0
- data/app/components/poetry/ui/item/style.rb +45 -0
- data/app/components/poetry/ui/kbd/component.rb +47 -0
- data/app/components/poetry/ui/kbd/group_preview.html.erb +4 -0
- data/app/components/poetry/ui/kbd/preview.rb +23 -0
- data/app/components/poetry/ui/kbd/style.rb +13 -0
- data/app/components/poetry/ui/label/component.rb +46 -0
- data/app/components/poetry/ui/label/preview.html.erb +5 -0
- data/app/components/poetry/ui/label/preview.rb +16 -0
- data/app/components/poetry/ui/label/style.rb +13 -0
- data/app/components/poetry/ui/link/component.rb +62 -0
- data/app/components/poetry/ui/link/preview.rb +31 -0
- data/app/components/poetry/ui/link/style.rb +20 -0
- data/app/components/poetry/ui/marker/component.html.erb +4 -0
- data/app/components/poetry/ui/marker/component.rb +81 -0
- data/app/components/poetry/ui/marker/preview.rb +43 -0
- data/app/components/poetry/ui/marker/style.rb +24 -0
- data/app/components/poetry/ui/menubar/component.html.erb +3 -0
- data/app/components/poetry/ui/menubar/component.rb +493 -0
- data/app/components/poetry/ui/menubar/preview.rb +164 -0
- data/app/components/poetry/ui/menubar/style.rb +71 -0
- data/app/components/poetry/ui/menus.rb +386 -0
- data/app/components/poetry/ui/message/component.html.erb +8 -0
- data/app/components/poetry/ui/message/component.rb +64 -0
- data/app/components/poetry/ui/message/preview.rb +33 -0
- data/app/components/poetry/ui/message/style.rb +29 -0
- data/app/components/poetry/ui/message_scroller/component.html.erb +15 -0
- data/app/components/poetry/ui/message_scroller/component.rb +163 -0
- data/app/components/poetry/ui/message_scroller/preview.rb +33 -0
- data/app/components/poetry/ui/message_scroller/style.rb +39 -0
- data/app/components/poetry/ui/metadata_list/component.rb +88 -0
- data/app/components/poetry/ui/metadata_list/preview.rb +55 -0
- data/app/components/poetry/ui/metadata_list/style.rb +35 -0
- data/app/components/poetry/ui/meter/component.rb +117 -0
- data/app/components/poetry/ui/meter/preview.rb +28 -0
- data/app/components/poetry/ui/meter/style.rb +16 -0
- data/app/components/poetry/ui/native_select/component.rb +124 -0
- data/app/components/poetry/ui/native_select/labelled_preview.html.erb +7 -0
- data/app/components/poetry/ui/native_select/preview.rb +20 -0
- data/app/components/poetry/ui/native_select/style.rb +22 -0
- data/app/components/poetry/ui/navigation_menu/bar_preview.html.erb +14 -0
- data/app/components/poetry/ui/navigation_menu/component.html.erb +25 -0
- data/app/components/poetry/ui/navigation_menu/component.rb +254 -0
- data/app/components/poetry/ui/navigation_menu/preview.rb +20 -0
- data/app/components/poetry/ui/navigation_menu/style.rb +73 -0
- data/app/components/poetry/ui/navigation_menu/viewport_preview.html.erb +23 -0
- data/app/components/poetry/ui/number_field/component.html.erb +19 -0
- data/app/components/poetry/ui/number_field/component.rb +263 -0
- data/app/components/poetry/ui/number_field/preview.rb +66 -0
- data/app/components/poetry/ui/number_field/style.rb +14 -0
- data/app/components/poetry/ui/pagination/component.html.erb +40 -0
- data/app/components/poetry/ui/pagination/component.rb +182 -0
- data/app/components/poetry/ui/pagination/preview.rb +47 -0
- data/app/components/poetry/ui/pagination/style.rb +27 -0
- data/app/components/poetry/ui/popover/component.html.erb +17 -0
- data/app/components/poetry/ui/popover/component.rb +203 -0
- data/app/components/poetry/ui/popover/preview.html.erb +29 -0
- data/app/components/poetry/ui/popover/preview.rb +66 -0
- data/app/components/poetry/ui/popover/style.rb +26 -0
- data/app/components/poetry/ui/popper_consumer.rb +69 -0
- data/app/components/poetry/ui/progress/component.rb +95 -0
- data/app/components/poetry/ui/progress/preview.rb +22 -0
- data/app/components/poetry/ui/progress/style.rb +19 -0
- data/app/components/poetry/ui/questionnaire/component.html.erb +80 -0
- data/app/components/poetry/ui/questionnaire/component.rb +396 -0
- data/app/components/poetry/ui/questionnaire/preview.rb +99 -0
- data/app/components/poetry/ui/questionnaire/style.rb +53 -0
- data/app/components/poetry/ui/radio_group/component.html.erb +4 -0
- data/app/components/poetry/ui/radio_group/component.rb +323 -0
- data/app/components/poetry/ui/radio_group/preview.rb +109 -0
- data/app/components/poetry/ui/radio_group/style.rb +53 -0
- data/app/components/poetry/ui/resizable/component.html.erb +10 -0
- data/app/components/poetry/ui/resizable/component.rb +178 -0
- data/app/components/poetry/ui/resizable/preview.rb +42 -0
- data/app/components/poetry/ui/resizable/style.rb +30 -0
- data/app/components/poetry/ui/scroll_area/component.rb +76 -0
- data/app/components/poetry/ui/scroll_area/preview.rb +35 -0
- data/app/components/poetry/ui/scroll_area/style.rb +19 -0
- data/app/components/poetry/ui/search_field/component.html.erb +15 -0
- data/app/components/poetry/ui/search_field/component.rb +137 -0
- data/app/components/poetry/ui/search_field/preview.rb +23 -0
- data/app/components/poetry/ui/search_field/style.rb +18 -0
- data/app/components/poetry/ui/select/component.html.erb +15 -0
- data/app/components/poetry/ui/select/component.rb +639 -0
- data/app/components/poetry/ui/select/preview.rb +113 -0
- data/app/components/poetry/ui/select/style.rb +58 -0
- data/app/components/poetry/ui/sensitive_input/component.html.erb +29 -0
- data/app/components/poetry/ui/sensitive_input/component.rb +274 -0
- data/app/components/poetry/ui/sensitive_input/preview.rb +40 -0
- data/app/components/poetry/ui/sensitive_input/style.rb +49 -0
- data/app/components/poetry/ui/separator/component.rb +57 -0
- data/app/components/poetry/ui/separator/preview.rb +19 -0
- data/app/components/poetry/ui/separator/style.rb +15 -0
- data/app/components/poetry/ui/sheet/component.html.erb +21 -0
- data/app/components/poetry/ui/sheet/component.rb +97 -0
- data/app/components/poetry/ui/sheet/preview.rb +57 -0
- data/app/components/poetry/ui/sheet/style.rb +47 -0
- data/app/components/poetry/ui/sidebar/component.html.erb +20 -0
- data/app/components/poetry/ui/sidebar/component.rb +249 -0
- data/app/components/poetry/ui/sidebar/preview.rb +21 -0
- data/app/components/poetry/ui/sidebar/shell_preview.html.erb +47 -0
- data/app/components/poetry/ui/sidebar/style.rb +139 -0
- data/app/components/poetry/ui/skeleton/component.rb +39 -0
- data/app/components/poetry/ui/skeleton/preview.rb +22 -0
- data/app/components/poetry/ui/skeleton/style.rb +13 -0
- data/app/components/poetry/ui/slider/component.html.erb +13 -0
- data/app/components/poetry/ui/slider/component.rb +360 -0
- data/app/components/poetry/ui/slider/preview.rb +73 -0
- data/app/components/poetry/ui/slider/style.rb +55 -0
- data/app/components/poetry/ui/spinner/component.rb +58 -0
- data/app/components/poetry/ui/spinner/preview.rb +22 -0
- data/app/components/poetry/ui/spinner/style.rb +12 -0
- data/app/components/poetry/ui/stat/component.html.erb +13 -0
- data/app/components/poetry/ui/stat/component.rb +102 -0
- data/app/components/poetry/ui/stat/preview.rb +47 -0
- data/app/components/poetry/ui/stat/style.rb +29 -0
- data/app/components/poetry/ui/switch/component.html.erb +15 -0
- data/app/components/poetry/ui/switch/component.rb +136 -0
- data/app/components/poetry/ui/switch/preview.rb +66 -0
- data/app/components/poetry/ui/switch/style.rb +22 -0
- data/app/components/poetry/ui/table/component.html.erb +3 -0
- data/app/components/poetry/ui/table/component.rb +93 -0
- data/app/components/poetry/ui/table/preview.rb +96 -0
- data/app/components/poetry/ui/table/style.rb +26 -0
- data/app/components/poetry/ui/tabs/component.html.erb +10 -0
- data/app/components/poetry/ui/tabs/component.rb +253 -0
- data/app/components/poetry/ui/tabs/preview.rb +43 -0
- data/app/components/poetry/ui/tabs/style.rb +49 -0
- data/app/components/poetry/ui/tag_group/component.html.erb +8 -0
- data/app/components/poetry/ui/tag_group/component.rb +203 -0
- data/app/components/poetry/ui/tag_group/preview.rb +43 -0
- data/app/components/poetry/ui/tag_group/style.rb +29 -0
- data/app/components/poetry/ui/textarea/component.rb +64 -0
- data/app/components/poetry/ui/textarea/preview.rb +75 -0
- data/app/components/poetry/ui/textarea/style.rb +18 -0
- data/app/components/poetry/ui/time_field/component.html.erb +6 -0
- data/app/components/poetry/ui/time_field/component.rb +85 -0
- data/app/components/poetry/ui/time_field/preview.rb +40 -0
- data/app/components/poetry/ui/time_field/style.rb +30 -0
- data/app/components/poetry/ui/timeline/component.rb +134 -0
- data/app/components/poetry/ui/timeline/preview.rb +61 -0
- data/app/components/poetry/ui/timeline/style.rb +54 -0
- data/app/components/poetry/ui/toast/component.html.erb +24 -0
- data/app/components/poetry/ui/toast/component.rb +178 -0
- data/app/components/poetry/ui/toast/preview.rb +69 -0
- data/app/components/poetry/ui/toast/style.rb +33 -0
- data/app/components/poetry/ui/toast_trigger/addressed_preview.html.erb +12 -0
- data/app/components/poetry/ui/toast_trigger/component.html.erb +3 -0
- data/app/components/poetry/ui/toast_trigger/component.rb +82 -0
- data/app/components/poetry/ui/toast_trigger/default_preview.html.erb +11 -0
- data/app/components/poetry/ui/toast_trigger/preview.rb +19 -0
- data/app/components/poetry/ui/toaster/component.html.erb +1 -0
- data/app/components/poetry/ui/toaster/component.rb +99 -0
- data/app/components/poetry/ui/toaster/positions_preview.html.erb +24 -0
- data/app/components/poetry/ui/toaster/preview.rb +46 -0
- data/app/components/poetry/ui/toaster/stamp_preview.html.erb +15 -0
- data/app/components/poetry/ui/toaster/style.rb +26 -0
- data/app/components/poetry/ui/toggle/component.rb +120 -0
- data/app/components/poetry/ui/toggle/preview.rb +79 -0
- data/app/components/poetry/ui/toggle/style.rb +33 -0
- data/app/components/poetry/ui/toggle_group/component.html.erb +4 -0
- data/app/components/poetry/ui/toggle_group/component.rb +252 -0
- data/app/components/poetry/ui/toggle_group/preview.rb +143 -0
- data/app/components/poetry/ui/toggle_group/style.rb +33 -0
- data/app/components/poetry/ui/toolbar/component.rb +125 -0
- data/app/components/poetry/ui/toolbar/preview.rb +41 -0
- data/app/components/poetry/ui/toolbar/style.rb +20 -0
- data/app/components/poetry/ui/tooltip/component.html.erb +20 -0
- data/app/components/poetry/ui/tooltip/component.rb +185 -0
- data/app/components/poetry/ui/tooltip/preview.rb +74 -0
- data/app/components/poetry/ui/tooltip/previews/provider_row.html.erb +25 -0
- data/app/components/poetry/ui/tooltip/style.rb +24 -0
- data/app/components/poetry/ui/tree/component.html.erb +32 -0
- data/app/components/poetry/ui/tree/component.rb +233 -0
- data/app/components/poetry/ui/tree/preview.rb +55 -0
- data/app/components/poetry/ui/tree/style.rb +29 -0
- data/app/components/poetry/ui/typeset/component.rb +73 -0
- data/app/components/poetry/ui/typeset/preview.rb +45 -0
- data/app/components/poetry/ui/typeset/style.rb +15 -0
- data/app/controllers/poetry/ui/llms_controller.rb +28 -0
- data/app/helpers/poetry/ui/components_helper.rb +1794 -0
- data/app/helpers/poetry/ui/form_builder/type_inference.rb +117 -0
- data/app/helpers/poetry/ui/form_builder.rb +916 -0
- data/app/helpers/poetry/ui/optimistic_form_builder.rb +55 -0
- data/app/helpers/poetry/ui/toast_stream_actions.rb +27 -0
- data/config/component_descriptions.yml +96 -0
- data/config/component_registry.yml +12257 -0
- data/config/locales/en.yml +93 -0
- data/config/routes.rb +8 -0
- data/config/template_classes.txt +113 -0
- data/lib/generators/poetry/add/add_generator.rb +218 -0
- data/lib/generators/poetry/agent_rules/agent_rules_generator.rb +91 -0
- data/lib/generators/poetry/agents/agents_generator.rb +24 -0
- data/lib/generators/poetry/agents_section.rb +148 -0
- data/lib/generators/poetry/block/block_generator.rb +67 -0
- data/lib/generators/poetry/block/templates/action_bar.html.erb +45 -0
- data/lib/generators/poetry/block/templates/app_shell.html.erb +139 -0
- data/lib/generators/poetry/block/templates/data_index.html.erb +85 -0
- data/lib/generators/poetry/block/templates/destructive_panel.html.erb +26 -0
- data/lib/generators/poetry/block/templates/page_header.html.erb +25 -0
- data/lib/generators/poetry/block/templates/section_card.html.erb +42 -0
- data/lib/generators/poetry/block/templates/stepper.html.erb +79 -0
- data/lib/generators/poetry/block/templates/top_nav.html.erb +44 -0
- data/lib/generators/poetry/diff/diff_generator.rb +135 -0
- data/lib/generators/poetry/editor/editor_generator.rb +344 -0
- data/lib/generators/poetry/editor/templates/poetry-check-hook +81 -0
- data/lib/generators/poetry/install/install_generator.rb +364 -0
- data/lib/generators/poetry/pagination/pagination_generator.rb +76 -0
- data/lib/generators/poetry/pagination/templates/kaminari_paginator.html.erb +29 -0
- data/lib/generators/poetry/pagination/templates/poetry_link_renderer.rb +34 -0
- data/lib/generators/poetry/pagination/templates/poetry_pagy_helper.rb +28 -0
- data/lib/generators/poetry/recipes/agent_embed/app/javascript/controllers/agent_demo_controller.js +131 -0
- data/lib/generators/poetry/recipes/screen_data_index/app/controllers/orders_controller.rb +15 -0
- data/lib/generators/poetry/recipes/screen_data_index/app/views/orders/index.html.erb +7 -0
- data/lib/generators/poetry/recipes/screen_data_index/test/system/orders_index_test.rb +16 -0
- data/lib/generators/poetry/recipes/screen_settings/app/controllers/settings_controller.rb +14 -0
- data/lib/generators/poetry/recipes/screen_settings/app/views/settings/show.html.erb +9 -0
- data/lib/generators/poetry/recipes/screen_settings/test/system/settings_show_test.rb +16 -0
- data/lib/generators/poetry/scaffold_templates/scaffold_templates_generator.rb +60 -0
- data/lib/generators/poetry/scaffold_templates/templates/_form.html.erb.tt +77 -0
- data/lib/generators/poetry/scaffold_templates/templates/controller.rb.tt +78 -0
- data/lib/generators/poetry/scaffold_templates/templates/edit.html.erb.tt +14 -0
- data/lib/generators/poetry/scaffold_templates/templates/index.html.erb.tt +24 -0
- data/lib/generators/poetry/scaffold_templates/templates/new.html.erb.tt +11 -0
- data/lib/generators/poetry/scaffold_templates/templates/partial.html.erb.tt +13 -0
- data/lib/generators/poetry/scaffold_templates/templates/show.html.erb.tt +18 -0
- data/lib/generators/poetry/skill/skill_generator.rb +34 -0
- data/lib/generators/poetry/skill/templates/poetry-component/SKILL.md +58 -0
- data/lib/generators/poetry/skill/templates/poetry-component/references/anatomy.md +152 -0
- data/lib/generators/poetry/skill/templates/poetry-component/references/checklist.md +67 -0
- data/lib/generators/poetry/skill/templates/poetry-component/references/documentation.md +137 -0
- data/lib/generators/poetry/skill/templates/poetry-design/SKILL.md +56 -0
- data/lib/generators/poetry/skill/templates/poetry-design/references/audit.md +123 -0
- data/lib/generators/poetry/skill/templates/poetry-design/references/compose.md +73 -0
- data/lib/generators/poetry/skill/templates/poetry-design/references/figma.md +68 -0
- data/lib/generators/poetry/skill/templates/poetry-design/references/paper.md +56 -0
- data/lib/generators/poetry/skill/templates/poetry-design/references/study.md +47 -0
- data/lib/generators/poetry/skill/templates/poetry-design/references/theme.md +70 -0
- data/lib/generators/poetry/skills_section.rb +58 -0
- data/lib/poetry/ui/chat.rb +283 -0
- data/lib/poetry/ui/code_block_highlighter.rb +60 -0
- data/lib/poetry/ui/engine.rb +55 -0
- data/lib/poetry/ui/recipes.rb +157 -0
- data/lib/poetry/ui/testing/combobox.rb +167 -0
- data/lib/poetry/ui/testing/dialog.rb +73 -0
- data/lib/poetry/ui/testing/menu.rb +127 -0
- data/lib/poetry/ui/testing/registration.rb +58 -0
- data/lib/poetry/ui/testing/select.rb +147 -0
- data/lib/poetry/ui/testing/tester.rb +130 -0
- data/lib/poetry/ui/testing.rb +130 -0
- data/lib/poetry/ui/themes.rb +109 -0
- data/lib/poetry/ui/version.rb +8 -0
- data/lib/poetry/ui/webmcp.rb +91 -0
- data/lib/poetry/ui.rb +285 -0
- data/lib/poetry-ui.rb +5 -0
- data/lib/tasks/poetry/check.rake +73 -0
- data/lib/tasks/poetry/design.rake +293 -0
- data/lib/tasks/poetry/verify.rake +36 -0
- data/themes/default.css +1793 -0
- data/themes/luma.css +1764 -0
- data/themes/lyra.css +1768 -0
- data/themes/maia.css +1768 -0
- data/themes/mira.css +1760 -0
- data/themes/nova.css +1755 -0
- data/themes/rhea.css +1771 -0
- data/themes/sera.css +1757 -0
- data/themes/vega.css +1775 -0
- data/typeset/typeset.css +499 -0
- metadata +502 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Ui
|
|
5
|
+
# The poetry FormBuilder - the model-truth end of the error
|
|
6
|
+
# quartet: `form.field(:email)` renders a Field wrapping an Input with
|
|
7
|
+
# everything derived from the object, never hand-wired:
|
|
8
|
+
#
|
|
9
|
+
# label from human_attribute_name (Rails i18n)
|
|
10
|
+
# value from the object
|
|
11
|
+
# error from object.errors (auto-flow)
|
|
12
|
+
# required from the model's presence validators (-> aria-required
|
|
13
|
+
# only - the label-station rule; never the native attribute)
|
|
14
|
+
# ids/aria field_id + aria-describedby via Field#control_attributes
|
|
15
|
+
#
|
|
16
|
+
# Usage: form_with(model:, builder: Poetry::Ui::FormBuilder).
|
|
17
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
18
|
+
include TypeInference
|
|
19
|
+
|
|
20
|
+
# The agent-facing rules: flow into the registry's form_builder
|
|
21
|
+
# section and llms.txt's Forms section.
|
|
22
|
+
AGENT_RULES = [
|
|
23
|
+
"Model-bound forms use form_with(model:, builder: Poetry::Ui::FormBuilder) - inside " \
|
|
24
|
+
"them, ALWAYS the builder methods, never bare components (the builder derives label/" \
|
|
25
|
+
"value/error/required/aria from the object).",
|
|
26
|
+
"f.input(:attribute) is the default call - the type is inferred (attachment -> file, " \
|
|
27
|
+
"AR enum -> select, column type, name heuristics); as: overrides it.",
|
|
28
|
+
"f.association(:company) reflects the association - belongs_to renders a Combobox on " \
|
|
29
|
+
"the foreign key, has_many the select-all checkbox group on singular_ids.",
|
|
30
|
+
"Validations become attributes: presence -> aria-required (NEVER native required), " \
|
|
31
|
+
"length -> maxlength/minlength, numericality -> min/max/step.",
|
|
32
|
+
"Hints/placeholders resolve from poetry_form.* i18n (simple_form.* keys keep working " \
|
|
33
|
+
"as a fallback); pass hint:/placeholder: to override.",
|
|
34
|
+
"f.submit renders a poetry Button with the Rails i18n label; f.fieldset(legend:)/" \
|
|
35
|
+
"f.group lay out sections; boolean f.input renders the horizontal Field " \
|
|
36
|
+
"(switch: true -> the setting row)."
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# One-line summaries of the builder methods, projected into the
|
|
40
|
+
# registry's form_builder section (keys group related methods). The
|
|
41
|
+
# docblocks carry the fuller human reference for the same methods;
|
|
42
|
+
# FormBuilderDocsTest gates the pair against drifting apart.
|
|
43
|
+
METHOD_SUMMARIES = {
|
|
44
|
+
"input" => "the inferred entrypoint: type from as:/attachments/enums/column/name",
|
|
45
|
+
"association" => "reflection-derived: belongs_to -> Combobox(fk), has_many -> checkbox group(_ids)",
|
|
46
|
+
"field" => "Field-wrapped Input/Textarea (as: :textarea; orientation:/hint_position: pass through)",
|
|
47
|
+
"check_box / switch" => "bare toggles (Rails check_box parity; switch = role=switch)",
|
|
48
|
+
"radio_group" => "collection_radio_buttons-equivalent on RadioGroup",
|
|
49
|
+
"checkbox_group" => "collection_check_boxes-equivalent on the select-all group (ONE clearing hidden)",
|
|
50
|
+
"poetry_select / poetry_combobox" => "the rich pickers (Rails choice shapes; combobox multiple: chips)",
|
|
51
|
+
"native_select" => "the styled native <select> (zero JS)",
|
|
52
|
+
"slider / otp_field / number_field / date_field / time_field / file_input" =>
|
|
53
|
+
"dedicated Field-wrapped controls",
|
|
54
|
+
"search_field / sensitive_input / autocomplete / tag_group / date_picker / calendar" =>
|
|
55
|
+
"the poetry-only control mappings",
|
|
56
|
+
"submit / button" => "poetry Buttons (type submit; loading: opt-in)",
|
|
57
|
+
"fieldset / group" => "layout frames yielding the builder"
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# The registry's form_builder section (consumed by LlmsText's Forms
|
|
61
|
+
# section, the MCP server, and skills - boot-free from the committed
|
|
62
|
+
# registry).
|
|
63
|
+
def self.registry_section
|
|
64
|
+
{
|
|
65
|
+
"rules" => AGENT_RULES,
|
|
66
|
+
"methods" => METHOD_SUMMARIES,
|
|
67
|
+
"input_types" => (INPUT_DISPATCH.keys + TYPED_FIELDS + %i[boolean enum]).map(&:to_s)
|
|
68
|
+
}
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# One field entrypoint for field-shaped controls: as: :input (the
|
|
72
|
+
# default, with type:) or as: :textarea (rows: passes through) -
|
|
73
|
+
# Own-line controls slot in as as: values; group-shaped
|
|
74
|
+
# controls get dedicated methods (radio_group, slider, otp_field).
|
|
75
|
+
def field(method, as: :input, hint: nil, **input_options)
|
|
76
|
+
# tool_description: - the declarative WebMCP parameter override
|
|
77
|
+
# (the label describes the parameter otherwise).
|
|
78
|
+
if input_options.key?(:tool_description)
|
|
79
|
+
input_options.merge!(Webmcp.param_attributes(input_options.delete(:tool_description)))
|
|
80
|
+
end
|
|
81
|
+
field_component = field_for(method, hint: hint,
|
|
82
|
+
label: input_options.delete(:label),
|
|
83
|
+
orientation: input_options.delete(:orientation),
|
|
84
|
+
hint_position: input_options.delete(:hint_position))
|
|
85
|
+
control_options = {
|
|
86
|
+
name: field_name(method),
|
|
87
|
+
value: object.public_send(method).presence&.to_s,
|
|
88
|
+
**input_options,
|
|
89
|
+
**field_component.control_attributes.transform_keys(&:to_sym)
|
|
90
|
+
}
|
|
91
|
+
@template.render(field_component) do
|
|
92
|
+
@template.render(
|
|
93
|
+
if as == :textarea
|
|
94
|
+
Textarea::Component.new(**control_options)
|
|
95
|
+
else
|
|
96
|
+
Input::Component.new(**control_options)
|
|
97
|
+
end
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The f.check_box-equivalent (the toggle family's form story): name/id
|
|
103
|
+
# derived, checked: from the object's attribute truthiness, "1"/"0"
|
|
104
|
+
# plus the unchecked-hidden pair (ActionView::Helpers::Tags::CheckBox
|
|
105
|
+
# parity incl. hidden-input-first ordering - the Checkbox component
|
|
106
|
+
# renders the pair). A BARE control mapping: compose with a Field
|
|
107
|
+
# (control_attributes) for the label/hint/error quartet.
|
|
108
|
+
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
|
|
109
|
+
@template.render Checkbox::Component.new(**toggle_options(method, options, checked_value, unchecked_value))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The same mapping wearing switch semantics (Rails has NO native
|
|
113
|
+
# switch builder): role=switch announces on/off; use it for
|
|
114
|
+
# instant-effect settings, check_box for values staged for submit.
|
|
115
|
+
def switch(method, options = {}, checked_value = "1", unchecked_value = "0")
|
|
116
|
+
@template.render Switch::Component.new(**toggle_options(method, options, checked_value, unchecked_value))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The collection_radio_buttons-equivalent (the exclusive-choice
|
|
120
|
+
# story): a Field wrapping a RadioGroup, items from the collection
|
|
121
|
+
# ([[value, label], ...] pairs or bare values), value/label/error/
|
|
122
|
+
# required derived from the object. Serialization is byte-identical
|
|
123
|
+
# to collection_radio_buttons (one hidden native radio per item,
|
|
124
|
+
# shared name; nothing submits when none is checked).
|
|
125
|
+
def radio_group(method, collection, hint: nil, **options)
|
|
126
|
+
field_component = field_for(method, hint: hint, group: true)
|
|
127
|
+
@template.render(field_component) do
|
|
128
|
+
@template.render RadioGroup::Component.new(
|
|
129
|
+
name: field_name(method),
|
|
130
|
+
value: object.public_send(method).presence&.to_s,
|
|
131
|
+
required: required?(method),
|
|
132
|
+
invalid: field_component.invalid?,
|
|
133
|
+
# The group is named by the VISIBLE Field label via
|
|
134
|
+
# aria-labelledby (in group_control_attributes) - never a
|
|
135
|
+
# duplicated aria-label string that can drift from it.
|
|
136
|
+
**group_control_attributes(field_component),
|
|
137
|
+
**options.transform_keys(&:to_sym)
|
|
138
|
+
) do |group|
|
|
139
|
+
collection.each do |item|
|
|
140
|
+
value, label = item.is_a?(Array) ? item : [item, item.to_s.humanize]
|
|
141
|
+
group.with_item(value: value, label: label)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The bounded-numeric story: form.slider(:volume) single (label from
|
|
148
|
+
# human_attribute_name), form.slider(:price_range, range: true,
|
|
149
|
+
# label: [...]) reads an Array[2] and submits name[] (params:
|
|
150
|
+
# ["200", "800"] - Rails' own array convention). Field wraps for
|
|
151
|
+
# hint/error; the describedby lands on each THUMB.
|
|
152
|
+
def slider(method, range: false, hint: nil, **options)
|
|
153
|
+
field_component = field_for(method, hint: hint, group: true)
|
|
154
|
+
value = object.public_send(method)
|
|
155
|
+
slider_options = {
|
|
156
|
+
name: field_name(method),
|
|
157
|
+
label: options.delete(:label) || (range ? nil : field_component.label_text),
|
|
158
|
+
**group_control_attributes(field_component),
|
|
159
|
+
**options.transform_keys(&:to_sym)
|
|
160
|
+
}
|
|
161
|
+
# The thumbs carry their own names (label: - a range REQUIRES two
|
|
162
|
+
# distinct ones, which a group pointer would override); the Field
|
|
163
|
+
# label is visual-only here, so drop the group aria-labelledby.
|
|
164
|
+
slider_options.delete(:"aria-labelledby")
|
|
165
|
+
describedby = slider_options.delete(:"aria-describedby")
|
|
166
|
+
slider_options[:described_by] = describedby if describedby
|
|
167
|
+
if range
|
|
168
|
+
slider_options[:values] = Array(value).presence ||
|
|
169
|
+
[slider_options.fetch(:min, 0), slider_options.fetch(:max, 100)]
|
|
170
|
+
else
|
|
171
|
+
slider_options[:value] = value
|
|
172
|
+
end
|
|
173
|
+
@template.render(field_component) do
|
|
174
|
+
@template.render Slider::Component.new(**slider_options)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# The verification-code story: form.otp_field(:code, length: 6) - a
|
|
179
|
+
# Field wrapping an InputOTP, label/error/required from the object.
|
|
180
|
+
# The value is deliberately NEVER round-tripped (a rejected code is
|
|
181
|
+
# dead; re-rendering it invites resubmit-the-same-wrong-code loops)
|
|
182
|
+
# - pass value: explicitly to override.
|
|
183
|
+
def otp_field(method, length: 6, hint: nil, **options)
|
|
184
|
+
field_component = field_for(method, hint: hint)
|
|
185
|
+
@template.render(field_component) do
|
|
186
|
+
@template.render InputOtp::Component.new(
|
|
187
|
+
name: field_name(method),
|
|
188
|
+
length: length,
|
|
189
|
+
required: required?(method),
|
|
190
|
+
invalid: field_component.invalid?,
|
|
191
|
+
**field_component.control_attributes.slice("id", "aria-describedby").transform_keys(&:to_sym),
|
|
192
|
+
**options.transform_keys(&:to_sym)
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# form.number_field(:quantity, min: 0, max: 100) - a Field wrapping
|
|
198
|
+
# a NumberField, label/error/required from the object. The
|
|
199
|
+
# hidden <input type=number> submits the raw value; format: only
|
|
200
|
+
# shapes the display.
|
|
201
|
+
def number_field(method, hint: nil, **options)
|
|
202
|
+
field_component = field_for(method, hint: hint)
|
|
203
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
204
|
+
@template.render(field_component) do
|
|
205
|
+
@template.render NumberField::Component.new(
|
|
206
|
+
name: field_name(method),
|
|
207
|
+
value: object.public_send(method),
|
|
208
|
+
required: required?(method),
|
|
209
|
+
invalid: field_component.invalid?,
|
|
210
|
+
id: field_component.control_attributes["id"],
|
|
211
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
212
|
+
**options.transform_keys(&:to_sym)
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# form.date_field(:due_on) - a Field wrapping a DateField;
|
|
218
|
+
# params arrive as ISO yyyy-mm-dd with or without JS. OVERRIDES
|
|
219
|
+
# ActionView's date_field (the number_field precedent).
|
|
220
|
+
def date_field(method, hint: nil, **options)
|
|
221
|
+
field_component = field_for(method, hint: hint)
|
|
222
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
223
|
+
@template.render(field_component) do
|
|
224
|
+
@template.render DateField::Component.new(
|
|
225
|
+
name: field_name(method),
|
|
226
|
+
value: object.public_send(method),
|
|
227
|
+
required: required?(method),
|
|
228
|
+
invalid: field_component.invalid?,
|
|
229
|
+
id: field_component.control_attributes["id"],
|
|
230
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
231
|
+
**options.transform_keys(&:to_sym)
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# form.time_field(:starts_at) - a Field wrapping a TimeField;
|
|
237
|
+
# params arrive as HH:MM (HH:MM:SS with seconds: true).
|
|
238
|
+
def time_field(method, hint: nil, **options)
|
|
239
|
+
field_component = field_for(method, hint: hint)
|
|
240
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
241
|
+
@template.render(field_component) do
|
|
242
|
+
@template.render TimeField::Component.new(
|
|
243
|
+
name: field_name(method),
|
|
244
|
+
value: object.public_send(method),
|
|
245
|
+
required: required?(method),
|
|
246
|
+
invalid: field_component.invalid?,
|
|
247
|
+
id: field_component.control_attributes["id"],
|
|
248
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
249
|
+
**options.transform_keys(&:to_sym)
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# form.file_input(:document) / form.file_input(:photos, variant: :dropzone,
|
|
255
|
+
# multiple: true) - a Field wrapping a FileInput; the native
|
|
256
|
+
# input is the form value, so ActiveStorage attaches as usual.
|
|
257
|
+
def file_input(method, hint: nil, **options)
|
|
258
|
+
field_component = field_for(method, hint: hint)
|
|
259
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
260
|
+
@template.render(field_component) do
|
|
261
|
+
@template.render FileInput::Component.new(
|
|
262
|
+
name: field_name(method, multiple: options[:multiple] || false),
|
|
263
|
+
invalid: field_component.invalid?,
|
|
264
|
+
id: field_component.control_attributes["id"],
|
|
265
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
266
|
+
**options.transform_keys(&:to_sym)
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# The FormBuilder#select-equivalent (the listbox capstone): a
|
|
272
|
+
# Field wrapping a Select, everything derived from the object. The
|
|
273
|
+
# hidden native <select> is the serialization truth (name/value/
|
|
274
|
+
# required ride it), the Field's control_attributes land on the
|
|
275
|
+
# TRIGGER (id + aria-describedby error-before-hint + aria-invalid),
|
|
276
|
+
# and label[for: id] click-focuses the combobox.
|
|
277
|
+
#
|
|
278
|
+
# choices accept the Rails shapes: [["Label", value], ...] pairs,
|
|
279
|
+
# flat %w[a b], grouped {"Group" => [["Label", value], ...]}, or nil
|
|
280
|
+
# plus a block of select.with_item calls. include_blank: "Choose..."
|
|
281
|
+
# doubles as the placeholder text (Rails include_blank semantics -
|
|
282
|
+
# the blank option posts "" and fails native required validation,
|
|
283
|
+
# which is the correct behavior).
|
|
284
|
+
def poetry_select(method, choices = nil, include_blank: nil, hint: nil, **options, &block)
|
|
285
|
+
if options.key?(:multiple)
|
|
286
|
+
raise ArgumentError, "poetry_select does not support multiple: - multi-select is Combobox territory"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
field_component = field_for(method, hint: hint)
|
|
290
|
+
placeholder = include_blank.is_a?(String) ? include_blank : options.delete(:placeholder)
|
|
291
|
+
select_options = {
|
|
292
|
+
name: field_name(method),
|
|
293
|
+
value: object.public_send(method).presence&.to_s,
|
|
294
|
+
placeholder: placeholder,
|
|
295
|
+
required: required?(method),
|
|
296
|
+
**options.transform_keys(&:to_sym),
|
|
297
|
+
**field_component.control_attributes.transform_keys(&:to_sym)
|
|
298
|
+
}
|
|
299
|
+
@template.render(field_component) do
|
|
300
|
+
@template.render(Select::Component.new(**select_options)) do |select|
|
|
301
|
+
populate_select_choices(select, choices) if choices
|
|
302
|
+
block&.call(select)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# The poetry_select twin for the type-to-filter picker (the
|
|
308
|
+
# combobox capstone): a Field wrapping a Combobox, everything
|
|
309
|
+
# derived from the object. The hidden native <select> is the
|
|
310
|
+
# serialization truth, the Field's control_attributes land on the
|
|
311
|
+
# TRIGGER (id + aria-describedby error-before-hint + aria-invalid),
|
|
312
|
+
# and label[for: id] click-focuses the combobox.
|
|
313
|
+
#
|
|
314
|
+
# choices accept the same Rails shapes as poetry_select:
|
|
315
|
+
# [["Label", value], ...] pairs, flat %w[a b], grouped
|
|
316
|
+
# {"Group" => [["Label", value], ...]} (group parts wear Command's
|
|
317
|
+
# heading:), or nil plus a block of combobox.with_item calls.
|
|
318
|
+
# include_blank: "Choose..." doubles as the placeholder text (the
|
|
319
|
+
# blank option posts "" and fails native required validation -
|
|
320
|
+
# deselection is a form affordance, never a re-click toggle).
|
|
321
|
+
#
|
|
322
|
+
# multiple: true is the chips mode: the value is the model's ARRAY
|
|
323
|
+
# (params post name[] - Rails' own convention, the [] derived for
|
|
324
|
+
# you) and the control_attributes land on the INLINE INPUT (the
|
|
325
|
+
# chips field has no trigger); include_blank has no meaning there.
|
|
326
|
+
def poetry_combobox(method, choices = nil, include_blank: nil, hint: nil, **options, &block)
|
|
327
|
+
multiple = options[:multiple]
|
|
328
|
+
field_component = field_for(method, hint: hint)
|
|
329
|
+
placeholder = include_blank.is_a?(String) ? include_blank : options.delete(:placeholder)
|
|
330
|
+
combobox_options = {
|
|
331
|
+
name: field_name(method, multiple: multiple),
|
|
332
|
+
value: multiple ? Array(object.public_send(method)).map(&:to_s) : object.public_send(method).presence&.to_s,
|
|
333
|
+
placeholder: placeholder,
|
|
334
|
+
required: required?(method),
|
|
335
|
+
**options.transform_keys(&:to_sym),
|
|
336
|
+
**field_component.control_attributes.transform_keys(&:to_sym)
|
|
337
|
+
}
|
|
338
|
+
@template.render(field_component) do
|
|
339
|
+
@template.render(Combobox::Component.new(**combobox_options)) do |combobox|
|
|
340
|
+
populate_combobox_choices(combobox, choices) if choices
|
|
341
|
+
block&.call(combobox)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# -- f.input: the inferred entrypoint ------------------------------
|
|
347
|
+
# form.input(:email) - one call, everything derived: type from `as:`
|
|
348
|
+
# -> attachment duck-typing -> AR enum -> attribute type -> name
|
|
349
|
+
# heuristics; hint/placeholder from the poetry_form (or simple_form)
|
|
350
|
+
# i18n chain when not passed; maxlength/min/max from validations.
|
|
351
|
+
INPUT_DISPATCH = {
|
|
352
|
+
string: :field, search: :search_field, password: :password_field,
|
|
353
|
+
text: :text_area, number: :number_field, date: :date_field,
|
|
354
|
+
time: :time_field, file: :file_input, sensitive: :sensitive_input,
|
|
355
|
+
select: :poetry_select, combobox: :poetry_combobox,
|
|
356
|
+
radio_group: :radio_group, autocomplete: :autocomplete,
|
|
357
|
+
tag_group: :tag_group, date_picker: :date_picker, calendar: :calendar
|
|
358
|
+
}.freeze
|
|
359
|
+
# The as: values that answer with #field carrying a native input type.
|
|
360
|
+
TYPED_FIELDS = %i[email url tel].freeze
|
|
361
|
+
# The as: values whose dispatch method takes the collection as its
|
|
362
|
+
# second positional argument.
|
|
363
|
+
COLLECTION_ARGS = %i[select combobox radio_group autocomplete].freeze
|
|
364
|
+
|
|
365
|
+
# The inferred entrypoint: one call, everything derived from the
|
|
366
|
+
# model. The control type comes from as: when given, otherwise from
|
|
367
|
+
# attachment duck-typing, AR enums, the attribute/column type, and
|
|
368
|
+
# name heuristics; hint/placeholder resolve from the poetry_form
|
|
369
|
+
# (or simple_form) i18n chain when not passed; length and numeric
|
|
370
|
+
# validations become maxlength/min/max/step attributes.
|
|
371
|
+
#
|
|
372
|
+
# @example
|
|
373
|
+
# <%= form_with(model: user, builder: Poetry::Ui::FormBuilder) do |f| %>
|
|
374
|
+
# <%= f.input :email %>
|
|
375
|
+
# <%= f.input :role, as: :select, collection: %w[admin member] %>
|
|
376
|
+
# <%= f.input :active, switch: true %>
|
|
377
|
+
# <%= f.submit %>
|
|
378
|
+
# <% end %>
|
|
379
|
+
#
|
|
380
|
+
# @param method [Symbol] the model attribute
|
|
381
|
+
# @param as [Symbol, nil] explicit control type - an INPUT_DISPATCH
|
|
382
|
+
# key, :email/:url/:tel, :boolean, or :enum (overrides inference)
|
|
383
|
+
# @param collection [Enumerable, nil] choices for collection-shaped
|
|
384
|
+
# types (:select, :combobox, :radio_group, :autocomplete)
|
|
385
|
+
# @param hint [String, nil] hint text (overrides the i18n chain)
|
|
386
|
+
# @return [ActiveSupport::SafeBuffer] the rendered Field
|
|
387
|
+
def input(method, as: nil, collection: nil, hint: nil, **options)
|
|
388
|
+
type = as || infer_input_type(method, collection: collection)
|
|
389
|
+
hint ||= form_i18n(:hints, method)
|
|
390
|
+
options[:placeholder] = form_i18n(:placeholders, method) if options[:placeholder].nil?
|
|
391
|
+
options.compact!
|
|
392
|
+
|
|
393
|
+
return boolean_input(method, hint: hint, **options) if type == :boolean
|
|
394
|
+
return enum_input(method, hint: hint, **options) if type == :enum
|
|
395
|
+
if TYPED_FIELDS.include?(type)
|
|
396
|
+
return field(method, hint: hint, type: type, **length_attributes(method),
|
|
397
|
+
**options)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
if type == :datetime
|
|
401
|
+
raise ArgumentError, "f.input cannot infer :datetime (no composite control yet) - " \
|
|
402
|
+
"pass as: :date or as: :time explicitly"
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
options = length_attributes(method).merge(options) if %i[string password text].include?(type)
|
|
406
|
+
options = numeric_attributes(method).merge(options) if type == :number
|
|
407
|
+
dispatch = INPUT_DISPATCH.fetch(type) do
|
|
408
|
+
raise ArgumentError, "f.input does not know as: #{type.inspect} " \
|
|
409
|
+
"(one of #{INPUT_DISPATCH.keys.inspect}, :email, :url, :tel, :boolean, :enum)"
|
|
410
|
+
end
|
|
411
|
+
args = COLLECTION_ARGS.include?(type) ? [method, collection] : [method]
|
|
412
|
+
send(dispatch, *args, hint: hint, **options)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# form.association(:company) - reflection-derived: belongs_to ->
|
|
416
|
+
# company_id + Combobox, has_many/HABTM -> singular_ids + the
|
|
417
|
+
# checkbox group; collection from the association klass, label
|
|
418
|
+
# method auto-detected (to_label/name/title/to_s, the conventional
|
|
419
|
+
# chain). as: overrides (:select, :combobox, :checkbox_group).
|
|
420
|
+
def association(method, as: nil, collection: nil, hint: nil, **)
|
|
421
|
+
reflection = object.class.respond_to?(:reflect_on_association) &&
|
|
422
|
+
object.class.reflect_on_association(method)
|
|
423
|
+
raise ArgumentError, "no association #{method.inspect} on #{object.class}" unless reflection
|
|
424
|
+
if reflection.macro == :has_one
|
|
425
|
+
raise ArgumentError, "has_one associations have no form control (build the record and use fields_for)"
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
collection ||= reflection.klass.all
|
|
429
|
+
pairs = collection.map { |item| [association_label(item), item.id] }
|
|
430
|
+
|
|
431
|
+
if reflection.macro == :belongs_to
|
|
432
|
+
attribute = association_attribute(reflection, method)
|
|
433
|
+
# radio_group items read [value, label] - the inverse of the
|
|
434
|
+
# select/combobox [label, value] pairs.
|
|
435
|
+
if as == :radio_group
|
|
436
|
+
radio_group(attribute, pairs.map { |label, value| [value, label] }, hint: hint, **)
|
|
437
|
+
else
|
|
438
|
+
send(INPUT_DISPATCH.fetch(as || :combobox), attribute, pairs, hint: hint, **)
|
|
439
|
+
end
|
|
440
|
+
else
|
|
441
|
+
collection_association(method, pairs, as, hint: hint, **)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# -- Rails-parity typed inputs -------------------------------------
|
|
446
|
+
# The stock helper names answer with poetry Fields: text_field ->
|
|
447
|
+
# field(type: :text) etc. password_field NEVER round-trips the value
|
|
448
|
+
# (Rails' own behavior); revealable SECRETS (API keys) are
|
|
449
|
+
# sensitive_input, login passwords stay type=password here.
|
|
450
|
+
#
|
|
451
|
+
# @!method text_field(method, hint: nil, **options)
|
|
452
|
+
# form.text_field(:name) - a Field-wrapped Input (type: :text).
|
|
453
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
454
|
+
# @!method email_field(method, hint: nil, **options)
|
|
455
|
+
# form.email_field(:email) - a Field-wrapped Input (type: :email).
|
|
456
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
457
|
+
# @!method url_field(method, hint: nil, **options)
|
|
458
|
+
# form.url_field(:website) - a Field-wrapped Input (type: :url).
|
|
459
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
460
|
+
# @!method telephone_field(method, hint: nil, **options)
|
|
461
|
+
# form.telephone_field(:phone) - a Field-wrapped Input (type: :tel).
|
|
462
|
+
# @return [ActiveSupport::SafeBuffer]
|
|
463
|
+
{ text_field: :text, email_field: :email, url_field: :url,
|
|
464
|
+
telephone_field: :tel }.each do |helper, type|
|
|
465
|
+
define_method(helper) do |method, hint: nil, **options|
|
|
466
|
+
field(method, hint: hint, type: type, **options)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
# The Rails alias for telephone_field.
|
|
470
|
+
alias phone_field telephone_field
|
|
471
|
+
|
|
472
|
+
# form.password_field(:password) - a Field-wrapped Input
|
|
473
|
+
# (type: :password); the value NEVER round-trips (Rails' own
|
|
474
|
+
# behavior). Revealable secrets are #sensitive_input.
|
|
475
|
+
def password_field(method, hint: nil, **)
|
|
476
|
+
field(method, hint: hint, type: :password, value: nil, **)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# form.text_area(:bio) - a Field-wrapped Textarea (rows: passes
|
|
480
|
+
# through; auto-grow is CSS, never a JS autosizer).
|
|
481
|
+
def text_area(method, hint: nil, **)
|
|
482
|
+
field(method, as: :textarea, hint: hint, **)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# Rails' range_field IS poetry's Slider (a naked native range would
|
|
486
|
+
# silently bypass the Field quartet).
|
|
487
|
+
def range_field(method, hint: nil, **)
|
|
488
|
+
slider(method, hint: hint, **)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# form.search_field(:query) - a Field wrapping the SearchField
|
|
492
|
+
# (native type=search + the clear affordance).
|
|
493
|
+
def search_field(method, hint: nil, **options)
|
|
494
|
+
field_component = field_for(method, hint: hint)
|
|
495
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
496
|
+
@template.render(field_component) do
|
|
497
|
+
@template.render SearchField::Component.new(
|
|
498
|
+
name: field_name(method),
|
|
499
|
+
value: object.public_send(method).presence&.to_s,
|
|
500
|
+
invalid: field_component.invalid?,
|
|
501
|
+
id: field_component.control_attributes["id"],
|
|
502
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
503
|
+
**options.transform_keys(&:to_sym)
|
|
504
|
+
)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# form.sensitive_input(:api_key) - the revealable-secret story
|
|
509
|
+
# : masked at rest, reveal + optional copy:.
|
|
510
|
+
def sensitive_input(method, hint: nil, **options)
|
|
511
|
+
field_component = field_for(method, hint: hint)
|
|
512
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
513
|
+
@template.render(field_component) do
|
|
514
|
+
@template.render SensitiveInput::Component.new(
|
|
515
|
+
name: field_name(method),
|
|
516
|
+
value: object.public_send(method).presence&.to_s,
|
|
517
|
+
invalid: field_component.invalid?,
|
|
518
|
+
id: field_component.control_attributes["id"],
|
|
519
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
520
|
+
**options.transform_keys(&:to_sym)
|
|
521
|
+
)
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# form.autocomplete(:city, %w[...]) - the input IS the value
|
|
526
|
+
# (suggestions are conveniences, not constraints); items from the
|
|
527
|
+
# suggestions array ([label, value] pairs or bare strings) or a
|
|
528
|
+
# block of auto.with_item calls.
|
|
529
|
+
def autocomplete(method, suggestions = nil, hint: nil, **options, &block)
|
|
530
|
+
field_component = field_for(method, hint: hint)
|
|
531
|
+
control = {
|
|
532
|
+
name: field_name(method),
|
|
533
|
+
value: object.public_send(method).presence&.to_s,
|
|
534
|
+
**options.transform_keys(&:to_sym),
|
|
535
|
+
**field_component.control_attributes.transform_keys(&:to_sym)
|
|
536
|
+
}
|
|
537
|
+
@template.render(field_component) do
|
|
538
|
+
@template.render(Autocomplete::Component.new(**control)) do |auto|
|
|
539
|
+
Array(suggestions).each do |choice|
|
|
540
|
+
label, value = choice.is_a?(Array) ? choice : [choice.to_s, nil]
|
|
541
|
+
auto.with_item(label: label, **(value ? { value: value } : {}))
|
|
542
|
+
end
|
|
543
|
+
block&.call(auto)
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# form.native_select(:country, [["USA", "us"], ...]) - the styled
|
|
549
|
+
# NATIVE <select> (zero JS); poetry_select/poetry_combobox stay the
|
|
550
|
+
# rich paths. include_blank: posts "" (Rails semantics).
|
|
551
|
+
def native_select(method, choices = nil, include_blank: nil, hint: nil, **options)
|
|
552
|
+
field_component = field_for(method, hint: hint)
|
|
553
|
+
pairs = Array(choices).map { |c| c.is_a?(Array) ? c : [c.to_s, c] }
|
|
554
|
+
pairs.unshift([include_blank.is_a?(String) ? include_blank : "", ""]) if include_blank
|
|
555
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
556
|
+
@template.render(field_component) do
|
|
557
|
+
@template.render NativeSelect::Component.new(
|
|
558
|
+
name: field_name(method),
|
|
559
|
+
options: pairs,
|
|
560
|
+
selected: object.public_send(method).presence&.to_s,
|
|
561
|
+
invalid: field_component.invalid?,
|
|
562
|
+
id: field_component.control_attributes["id"],
|
|
563
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
564
|
+
**options.transform_keys(&:to_sym)
|
|
565
|
+
)
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
# form.tag_group(:topics) - the removable-chips story for an ARRAY
|
|
570
|
+
# attribute: one hidden name[] per tag, plus the leading empty
|
|
571
|
+
# hidden (Rails array convention: removing every tag still clears).
|
|
572
|
+
def tag_group(method, hint: nil, **options)
|
|
573
|
+
# The Field renders NO label here: TagGroup's own caption span is the
|
|
574
|
+
# grid's accessible name - a Field label too would render the text
|
|
575
|
+
# twice under one duplicated DOM id.
|
|
576
|
+
caption = options.delete(:label) || object.class.human_attribute_name(method)
|
|
577
|
+
field_component = field_for(method, hint: hint, group: true, label: false)
|
|
578
|
+
describedby = field_component.control_attributes["aria-describedby"]
|
|
579
|
+
values = Array(object.public_send(method)).map(&:to_s)
|
|
580
|
+
@template.hidden_field_tag(field_name(method, multiple: true), "", id: nil) +
|
|
581
|
+
@template.render(field_component) do
|
|
582
|
+
@template.render(TagGroup::Component.new(
|
|
583
|
+
name: field_name(method),
|
|
584
|
+
label: caption,
|
|
585
|
+
id: field_component.control_attributes["id"],
|
|
586
|
+
**(describedby ? { described_by: describedby } : {}),
|
|
587
|
+
**options.transform_keys(&:to_sym)
|
|
588
|
+
)) do |group|
|
|
589
|
+
values.each { |value| group.with_tag(value: value, label: value) }
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
# The collection_check_boxes-equivalent: a Field(group) wrapping the
|
|
595
|
+
# APG select-all group (DD sweep) - items post name[] and the
|
|
596
|
+
# leading empty hidden clears when none are checked. select_all:
|
|
597
|
+
# true (or a label string) adds the mixed-state parent checkbox.
|
|
598
|
+
def checkbox_group(method, collection, hint: nil, select_all: false, **options)
|
|
599
|
+
field_component = field_for(method, hint: hint, group: true)
|
|
600
|
+
chosen = Array(object.public_send(method)).map(&:to_s)
|
|
601
|
+
base_id = field_id(method)
|
|
602
|
+
@template.hidden_field_tag(field_name(method, multiple: true), "", id: nil) +
|
|
603
|
+
@template.render(field_component) do
|
|
604
|
+
@template.poetry_checkbox_group(
|
|
605
|
+
class: "flex flex-col gap-2",
|
|
606
|
+
**group_control_attributes(field_component).slice(:id, :"aria-labelledby", :"aria-describedby"),
|
|
607
|
+
**options.transform_keys(&:to_sym)
|
|
608
|
+
) do
|
|
609
|
+
rows = []
|
|
610
|
+
rows << checkbox_group_all_row(base_id, select_all) if select_all
|
|
611
|
+
rows.concat(checkbox_group_item_rows(method, collection, chosen, base_id))
|
|
612
|
+
@template.safe_join(rows)
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
# form.date_picker(:due_on) - the calendar-popup pick; value posts
|
|
618
|
+
# ISO like date_field. (Quartet ids land on the composed control's
|
|
619
|
+
# root for now - the input-level aria refinement comes later.)
|
|
620
|
+
def date_picker(method, hint: nil, **options)
|
|
621
|
+
field_component = field_for(method, hint: hint, group: true)
|
|
622
|
+
@template.render(field_component) do
|
|
623
|
+
@template.render DatePicker::Component.new(
|
|
624
|
+
name: field_name(method),
|
|
625
|
+
value: object.public_send(method).presence&.to_s,
|
|
626
|
+
**group_control_attributes(field_component).slice(:id, :"aria-describedby"),
|
|
627
|
+
**options.transform_keys(&:to_sym)
|
|
628
|
+
)
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# form.calendar(:starts_on) - the always-visible month grid as a
|
|
633
|
+
# form participant (mode: :range posts name[]).
|
|
634
|
+
def calendar(method, hint: nil, **options)
|
|
635
|
+
field_component = field_for(method, hint: hint, group: true)
|
|
636
|
+
value = object.public_send(method)
|
|
637
|
+
@template.render(field_component) do
|
|
638
|
+
@template.render Calendar::Component.new(
|
|
639
|
+
name: field_name(method),
|
|
640
|
+
# Calendar's keyword is selected:, not value: - value: would fall
|
|
641
|
+
# into html_attributes and the model's date silently never lands.
|
|
642
|
+
selected: value.is_a?(Array) ? value.map(&:to_s) : value.presence&.to_s,
|
|
643
|
+
**group_control_attributes(field_component).slice(:id, :"aria-describedby"),
|
|
644
|
+
**options.transform_keys(&:to_sym)
|
|
645
|
+
)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# -- Rails-arity adapters ------------------------------------------
|
|
650
|
+
|
|
651
|
+
# ActionView's select arity adapted onto poetry_select.
|
|
652
|
+
def select(method, choices = nil, options = {}, html_options = {}, &)
|
|
653
|
+
poetry_select(method, choices,
|
|
654
|
+
include_blank: options[:include_blank] || options[:prompt],
|
|
655
|
+
**html_options.transform_keys(&:to_sym), &)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# ActionView's collection_select arity, verbatim, adapted onto
|
|
659
|
+
# poetry_select (value_method/text_method read off each item).
|
|
660
|
+
def collection_select(method, collection, value_method, text_method, # rubocop:disable Metrics/ParameterLists
|
|
661
|
+
options = {}, html_options = {})
|
|
662
|
+
pairs = collection.map { |item| [item.public_send(text_method), item.public_send(value_method)] }
|
|
663
|
+
poetry_select(method, pairs,
|
|
664
|
+
include_blank: options[:include_blank] || options[:prompt],
|
|
665
|
+
**html_options.transform_keys(&:to_sym))
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# ActionView's collection_radio_buttons arity adapted onto
|
|
669
|
+
# radio_group (one hidden native radio per item, same serialization).
|
|
670
|
+
def collection_radio_buttons(method, collection, value_method, text_method, **)
|
|
671
|
+
pairs = collection.map { |item| [item.public_send(value_method), item.public_send(text_method)] }
|
|
672
|
+
radio_group(method, pairs, **)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
# ActionView's collection_check_boxes arity adapted onto
|
|
676
|
+
# checkbox_group (name[] items plus the leading clearing hidden).
|
|
677
|
+
def collection_check_boxes(method, collection, value_method, text_method, **)
|
|
678
|
+
pairs = collection.map { |item| [item.public_send(value_method), item.public_send(text_method)] }
|
|
679
|
+
checkbox_group(method, pairs, **)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# -- Actions -------------------------------------------------------
|
|
683
|
+
|
|
684
|
+
# form.submit -> a poetry Button (type submit); label from Rails'
|
|
685
|
+
# own i18n default ("Create Model" / "Update Model"). loading: true
|
|
686
|
+
# opts into the Button loading treatment.
|
|
687
|
+
def submit(value = nil, **options)
|
|
688
|
+
value ||= submit_default_value
|
|
689
|
+
@template.render(Button::Component.new(type: :submit, **options.transform_keys(&:to_sym))) { value }
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# form.button - #submit with block/content support: the block (or
|
|
693
|
+
# value) is the Button's content, type stays submit.
|
|
694
|
+
def button(value = nil, **options, &block)
|
|
695
|
+
content = block ? @template.capture(&block) : value || submit_default_value
|
|
696
|
+
@template.render(Button::Component.new(type: :submit, **options.transform_keys(&:to_sym))) { content }
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
# -- Layout --------------------------------------------------------
|
|
700
|
+
|
|
701
|
+
# form.fieldset(legend: "Shipping") { |f| ... } - the grouped-fields
|
|
702
|
+
# frame; yields the builder for nesting.
|
|
703
|
+
def fieldset(legend:, hint: nil, **options, &)
|
|
704
|
+
@template.render(Fieldset::Component.new(legend: legend, hint: hint,
|
|
705
|
+
**options.transform_keys(&:to_sym))) do
|
|
706
|
+
@template.capture(self, &)
|
|
707
|
+
end
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# form.group { |f| ... } - the FieldGroup stack (the @container the
|
|
711
|
+
# responsive Field orientation measures against).
|
|
712
|
+
def group(**options, &)
|
|
713
|
+
@template.render(FieldGroup::Component.new(**options.transform_keys(&:to_sym))) do
|
|
714
|
+
@template.capture(self, &)
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
private
|
|
719
|
+
|
|
720
|
+
# Rails choice shapes -> Select parts: a Hash groups (label part per
|
|
721
|
+
# key), arrays are ["Label", value] pairs (options_for_select-exact)
|
|
722
|
+
# or bare values (value doubles as the label).
|
|
723
|
+
def populate_select_choices(owner, choices)
|
|
724
|
+
if choices.is_a?(Hash)
|
|
725
|
+
choices.each do |group_label, group_choices|
|
|
726
|
+
owner.with_group(label: group_label.to_s) do |group|
|
|
727
|
+
group_choices.each { |choice| add_select_item(group, choice) }
|
|
728
|
+
end
|
|
729
|
+
end
|
|
730
|
+
else
|
|
731
|
+
choices.each { |choice| add_select_item(owner, choice) }
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def add_select_item(owner, choice)
|
|
736
|
+
label, value = choice.is_a?(Array) ? choice : [choice.to_s, choice]
|
|
737
|
+
owner.with_item(value: value.to_s) { label.to_s }
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# The same Rails shapes onto Combobox parts - groups wear the
|
|
741
|
+
# embedded Command's heading: (vs Select's label:).
|
|
742
|
+
def populate_combobox_choices(owner, choices)
|
|
743
|
+
if choices.is_a?(Hash)
|
|
744
|
+
choices.each do |group_heading, group_choices|
|
|
745
|
+
owner.with_group(heading: group_heading.to_s) do |group|
|
|
746
|
+
group_choices.each { |choice| add_select_item(group, choice) }
|
|
747
|
+
end
|
|
748
|
+
end
|
|
749
|
+
else
|
|
750
|
+
choices.each { |choice| add_select_item(owner, choice) }
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Group-shaped controls take the id (item ids derive from it) and
|
|
755
|
+
# the describedby wiring from the Field; invalid/required ride the
|
|
756
|
+
# component's own options (aria-invalid belongs on the ITEMS, not
|
|
757
|
+
# the root).
|
|
758
|
+
def group_control_attributes(field_component)
|
|
759
|
+
field_component.control_attributes
|
|
760
|
+
.slice("id", "aria-describedby", "aria-labelledby")
|
|
761
|
+
.transform_keys(&:to_sym)
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# -- one keyword per Field surface
|
|
765
|
+
# label: false suppresses the Field label entirely - for controls that
|
|
766
|
+
# render their own accessible name (TagGroup's caption span).
|
|
767
|
+
def field_for(method, hint: nil, group: false, orientation: nil, hint_position: nil, label: nil)
|
|
768
|
+
extras = { orientation: orientation, hint_position: hint_position }.compact
|
|
769
|
+
Field::Component.new(
|
|
770
|
+
id: field_id(method),
|
|
771
|
+
label_text: label == false ? nil : (label || object.class.human_attribute_name(method)),
|
|
772
|
+
hint: hint,
|
|
773
|
+
error: error_for(method),
|
|
774
|
+
required: required?(method),
|
|
775
|
+
group: group,
|
|
776
|
+
**extras
|
|
777
|
+
)
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
# Shared derivation for the toggle-family builder methods: everything
|
|
781
|
+
# from the object, never hand-wired. required maps to aria-required
|
|
782
|
+
# only (the label lock - the components never render native required).
|
|
783
|
+
def toggle_options(method, options, checked_value, unchecked_value)
|
|
784
|
+
{
|
|
785
|
+
name: field_name(method),
|
|
786
|
+
id: field_id(method),
|
|
787
|
+
checked: ActiveModel::Type::Boolean.new.cast(object.public_send(method)) || false,
|
|
788
|
+
value: checked_value,
|
|
789
|
+
unchecked_value: unchecked_value,
|
|
790
|
+
required: required?(method),
|
|
791
|
+
**options.transform_keys(&:to_sym)
|
|
792
|
+
}
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# f.input's boolean story: the horizontal boolean-control layout
|
|
796
|
+
# (checkbox on the label line); switch: true renders the setting row
|
|
797
|
+
# (label + hint left, switch right).
|
|
798
|
+
def boolean_input(method, hint: nil, switch: false, **options)
|
|
799
|
+
orientation = switch ? :setting : :horizontal
|
|
800
|
+
field_component = field_for(method, hint: hint, orientation: orientation)
|
|
801
|
+
control = toggle_options(method, options, "1", "0")
|
|
802
|
+
.merge(field_component.control_attributes.transform_keys(&:to_sym))
|
|
803
|
+
@template.render(field_component) do
|
|
804
|
+
@template.render((switch ? Switch::Component : Checkbox::Component).new(**control))
|
|
805
|
+
end
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
# AR enums: humanized keys as a select (as: :radio_group lays a
|
|
809
|
+
# small set flat).
|
|
810
|
+
def enum_input(method, hint: nil, **)
|
|
811
|
+
pairs = object.class.defined_enums.fetch(method.to_s).keys.map { |key| [key.humanize, key] }
|
|
812
|
+
poetry_select(method, pairs, hint: hint, **)
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
def association_label(item)
|
|
816
|
+
%i[to_label name title].each do |candidate|
|
|
817
|
+
return item.public_send(candidate) if item.respond_to?(candidate)
|
|
818
|
+
end
|
|
819
|
+
item.to_s
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def association_attribute(reflection, method)
|
|
823
|
+
attribute = (reflection.respond_to?(:foreign_key) && reflection.foreign_key) || "#{method}_id"
|
|
824
|
+
attribute.to_sym
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def collection_association(method, pairs, as, hint: nil, **)
|
|
828
|
+
attribute = :"#{method.to_s.singularize}_ids"
|
|
829
|
+
case as
|
|
830
|
+
when :combobox
|
|
831
|
+
poetry_combobox(attribute, pairs, hint: hint, multiple: true, **)
|
|
832
|
+
when :select, :radio_group
|
|
833
|
+
raise ArgumentError, "collection associations need a multi-value control (:checkbox_group or :combobox)"
|
|
834
|
+
else
|
|
835
|
+
checkbox_group(attribute, pairs.map { |label, value| [value, label] }, hint: hint, **)
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
# The checkbox-group rows (extracted for the coverage the builder
|
|
840
|
+
# method reads better without).
|
|
841
|
+
def checkbox_group_all_row(base_id, select_all)
|
|
842
|
+
all_id = "#{base_id}_all"
|
|
843
|
+
@template.content_tag(:div, class: "flex items-center gap-2") do
|
|
844
|
+
@template.poetry_checkbox_group_all(id: all_id) +
|
|
845
|
+
@template.poetry_label(for_id: all_id) do
|
|
846
|
+
select_all.is_a?(String) ? select_all : "All"
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def checkbox_group_item_rows(method, collection, chosen, base_id)
|
|
852
|
+
collection.map do |item|
|
|
853
|
+
value, label = item.is_a?(Array) ? item : [item, item.to_s.humanize]
|
|
854
|
+
item_id = "#{base_id}_#{value.to_s.parameterize(separator: "_")}"
|
|
855
|
+
@template.content_tag(:div, class: "flex items-center gap-2") do
|
|
856
|
+
@template.poetry_checkbox_group_item(
|
|
857
|
+
name: field_name(method, multiple: true), value: value.to_s,
|
|
858
|
+
checked: chosen.include?(value.to_s), unchecked_value: nil, id: item_id
|
|
859
|
+
) + @template.poetry_label(for_id: item_id) { label.to_s }
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
# The dual-key error read: company_id also reads errors on :company
|
|
865
|
+
# (validates :company, presence: true is the Rails idiom, but the
|
|
866
|
+
# form field is the _id attribute).
|
|
867
|
+
def error_for(method)
|
|
868
|
+
return nil unless object.respond_to?(:errors)
|
|
869
|
+
|
|
870
|
+
message = object.errors.full_messages_for(method).first
|
|
871
|
+
if message.nil? && (base = method.to_s[/\A(.+?)_ids?\z/, 1])
|
|
872
|
+
message = object.errors.full_messages_for(base.to_sym).first ||
|
|
873
|
+
object.errors.full_messages_for(base.pluralize.to_sym).first
|
|
874
|
+
end
|
|
875
|
+
message
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
# Presence -> required, with the necessary filtering: conditional
|
|
879
|
+
# validators (:if/:unless) never claim required, and :on contexts
|
|
880
|
+
# must match the record's persistence.
|
|
881
|
+
def required?(method)
|
|
882
|
+
return false unless object.class.respond_to?(:validators_on)
|
|
883
|
+
|
|
884
|
+
context = object.respond_to?(:persisted?) && object.persisted? ? :update : :create
|
|
885
|
+
object.class.validators_on(method).any? do |validator|
|
|
886
|
+
next false unless validator.kind == :presence
|
|
887
|
+
next false if validator.options[:if] || validator.options[:unless]
|
|
888
|
+
|
|
889
|
+
on = Array(validator.options[:on])
|
|
890
|
+
on.empty? || on.include?(context)
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# The Rails 8 respellings: ActionView 8
|
|
895
|
+
# aliases textarea/checkbox at ITS class body, so a subclass override
|
|
896
|
+
# of the old name never reaches them - define both, version-gated.
|
|
897
|
+
if ActionView::VERSION::MAJOR >= 8
|
|
898
|
+
|
|
899
|
+
public
|
|
900
|
+
|
|
901
|
+
# The Rails 8 spelling of #text_area.
|
|
902
|
+
def textarea(method, hint: nil, **) = text_area(method, hint: hint, **)
|
|
903
|
+
|
|
904
|
+
# The Rails 8 spelling of #check_box.
|
|
905
|
+
def checkbox(method, options = {}, checked_value = "1", unchecked_value = "0")
|
|
906
|
+
check_box(method, options, checked_value, unchecked_value)
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# The Rails 8 spelling of #collection_check_boxes.
|
|
910
|
+
def collection_checkboxes(method, collection, value_method, text_method, **)
|
|
911
|
+
collection_check_boxes(method, collection, value_method, text_method, **)
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
end
|