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,1794 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Poetry
|
|
4
|
+
module Ui
|
|
5
|
+
# The `poetry_*` view helpers - the agent-facing surface ("use
|
|
6
|
+
# poetry_button, never a raw <button> with hand-written Tailwind").
|
|
7
|
+
# Every registered component ships one (drift-gated by
|
|
8
|
+
# ComponentsHelperTest); slot blocks receive the component:
|
|
9
|
+
#
|
|
10
|
+
# <%= poetry_dialog do |dialog| %>
|
|
11
|
+
# <% dialog.with_trigger { "Open" } %>
|
|
12
|
+
# <% dialog.with_title { "Settings" } %>
|
|
13
|
+
# <% end %>
|
|
14
|
+
module ComponentsHelper
|
|
15
|
+
# Triggers an action or event, such as submitting a form or opening a
|
|
16
|
+
# dialog.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# poetry_button(variant: :default) { "Save" }
|
|
20
|
+
# @see Poetry::Ui::Button::Component
|
|
21
|
+
def poetry_button(**, &)
|
|
22
|
+
render(Poetry::Ui::Button::Component.new(**), &)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Renders an inline SVG icon from the icon set.
|
|
26
|
+
#
|
|
27
|
+
# @example A decorative icon inside a labeled control
|
|
28
|
+
# poetry_icon(name: :plus)
|
|
29
|
+
# @see Poetry::Ui::Icon::Component
|
|
30
|
+
def poetry_icon(**)
|
|
31
|
+
render(Poetry::Ui::Icon::Component.new(**))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# A styled navigational hyperlink.
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# poetry_link(href: "/docs") { "Documentation" }
|
|
38
|
+
# @see Poetry::Ui::Link::Component
|
|
39
|
+
def poetry_link(**, &)
|
|
40
|
+
render(Poetry::Ui::Link::Component.new(**), &)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A small count or status descriptor.
|
|
44
|
+
#
|
|
45
|
+
# @example A soft status pill
|
|
46
|
+
# poetry_badge(variant: :success) { "Fulfilled" }
|
|
47
|
+
# @see Poetry::Ui::Badge::Component
|
|
48
|
+
def poetry_badge(**, &)
|
|
49
|
+
render(Poetry::Ui::Badge::Component.new(**), &)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# A single KPI: a muted label over a large metric value.
|
|
53
|
+
#
|
|
54
|
+
# @example Revenue KPI with a delta
|
|
55
|
+
# poetry_stat(label: "Revenue", delta: "+12.5%", trend: :up) do
|
|
56
|
+
# "$45,231"
|
|
57
|
+
# end
|
|
58
|
+
# @see Poetry::Ui::Stat::Component
|
|
59
|
+
def poetry_stat(**, &)
|
|
60
|
+
render(Poetry::Ui::Stat::Component.new(**), &)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A key-value list for labeled attributes on detail pages.
|
|
64
|
+
#
|
|
65
|
+
# @example A record's fact sheet
|
|
66
|
+
# poetry_metadata_list(columns: :two) do |list|
|
|
67
|
+
# list.with_item(label: "Status") { "Active" }
|
|
68
|
+
# list.with_item(label: "Owner") { "Ada Lovelace" }
|
|
69
|
+
# end
|
|
70
|
+
# @see Poetry::Ui::MetadataList::Component
|
|
71
|
+
def poetry_metadata_list(**, &)
|
|
72
|
+
render(Poetry::Ui::MetadataList::Component.new(**), &)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# A sequence of dated events as an ordered list.
|
|
76
|
+
#
|
|
77
|
+
# @example
|
|
78
|
+
# poetry_timeline do |timeline|
|
|
79
|
+
# timeline.with_item(title: "Order placed", time: "Mar 15", completed: true)
|
|
80
|
+
# timeline.with_item(title: "In transit") { "Estimated delivery Thursday." }
|
|
81
|
+
# end
|
|
82
|
+
# @see Poetry::Ui::Timeline::Component
|
|
83
|
+
def poetry_timeline(**, &)
|
|
84
|
+
render(Poetry::Ui::Timeline::Component.new(**), &)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A horizontal group of controls that acts as one keyboard tab stop -
|
|
88
|
+
# Tab passes over the group, Arrow keys move between its controls.
|
|
89
|
+
#
|
|
90
|
+
# @example
|
|
91
|
+
# poetry_toolbar(label: "Bulk actions") do |toolbar|
|
|
92
|
+
# toolbar.with_button(variant: :outline) { "Archive" }
|
|
93
|
+
# toolbar.with_separator
|
|
94
|
+
# toolbar.with_input(name: "q", placeholder: "Filter…")
|
|
95
|
+
# end
|
|
96
|
+
# @see Poetry::Ui::Toolbar::Component
|
|
97
|
+
def poetry_toolbar(**, &)
|
|
98
|
+
render(Poetry::Ui::Toolbar::Component.new(**), &)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Prose styling for long-form and rendered-markdown content.
|
|
102
|
+
#
|
|
103
|
+
# @example
|
|
104
|
+
# poetry_typeset(preset: "docs") do
|
|
105
|
+
# @article_html
|
|
106
|
+
# end
|
|
107
|
+
# @see Poetry::Ui::Typeset::Component
|
|
108
|
+
def poetry_typeset(**, &)
|
|
109
|
+
render(Poetry::Ui::Typeset::Component.new(**), &)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A control for selecting, previewing, and removing files to upload.
|
|
113
|
+
#
|
|
114
|
+
# @example Drag-and-drop upload surface
|
|
115
|
+
# poetry_file_input(
|
|
116
|
+
# variant: :dropzone, name: "attachments[]", multiple: true,
|
|
117
|
+
# hint: "PDF or PNG, up to 10 MB"
|
|
118
|
+
# )
|
|
119
|
+
# @see Poetry::Ui::FileInput::Component
|
|
120
|
+
def poetry_file_input(**, &)
|
|
121
|
+
render(Poetry::Ui::FileInput::Component.new(**), &)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A callout that highlights an important inline message.
|
|
125
|
+
#
|
|
126
|
+
# @example Destructive alert with a title
|
|
127
|
+
# poetry_alert(variant: :destructive) do |alert|
|
|
128
|
+
# alert.with_title { "Payment failed" }
|
|
129
|
+
# "Your card was declined. Update your billing details."
|
|
130
|
+
# end
|
|
131
|
+
# @see Poetry::Ui::Alert::Component
|
|
132
|
+
def poetry_alert(**, &)
|
|
133
|
+
render(Poetry::Ui::Alert::Component.new(**), &)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A container that groups related content and actions.
|
|
137
|
+
#
|
|
138
|
+
# @example
|
|
139
|
+
# poetry_card do |card|
|
|
140
|
+
# card.with_title { "Team" }
|
|
141
|
+
# card.with_description { "Invite and manage members." }
|
|
142
|
+
# "Body content"
|
|
143
|
+
# end
|
|
144
|
+
# @see Poetry::Ui::Card::Component
|
|
145
|
+
def poetry_card(**, &)
|
|
146
|
+
render(Poetry::Ui::Card::Component.new(**), &)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The Table: the component renders the overflow container + real
|
|
150
|
+
# `<table>`; the part helpers stamp the data-slot + source-exact classes
|
|
151
|
+
# onto the semantic table elements the consumer composes.
|
|
152
|
+
#
|
|
153
|
+
# @example
|
|
154
|
+
# poetry_table do
|
|
155
|
+
# safe_join([
|
|
156
|
+
# poetry_table_header { poetry_table_row { poetry_table_head { "Invoice" } } },
|
|
157
|
+
# poetry_table_body { poetry_table_row { poetry_table_cell { "INV001" } } }
|
|
158
|
+
# ])
|
|
159
|
+
# end
|
|
160
|
+
# @see Poetry::Ui::Table::Component
|
|
161
|
+
def poetry_table(**, &)
|
|
162
|
+
render(Poetry::Ui::Table::Component.new(**), &)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
{
|
|
166
|
+
table_header: [:thead, "table-header", :header],
|
|
167
|
+
table_body: [:tbody, "table-body", :body],
|
|
168
|
+
table_footer: [:tfoot, "table-footer", :footer],
|
|
169
|
+
table_row: [:tr, "table-row", :row],
|
|
170
|
+
table_head: [:th, "table-head", :head],
|
|
171
|
+
table_cell: [:td, "table-cell", :cell],
|
|
172
|
+
table_caption: [:caption, "table-caption", :caption]
|
|
173
|
+
}.each do |name, (tag_name, slot, element)|
|
|
174
|
+
define_method("poetry_#{name}") do |**attrs, &block|
|
|
175
|
+
classes = [Poetry::Ui::Table::Style.css(element), attrs.delete(:class)].compact.join(" ")
|
|
176
|
+
data = { slot: slot }.merge(attrs.delete(:data) || {})
|
|
177
|
+
content_tag(tag_name, (capture(&block) if block), **attrs, class: classes, data: data)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Data-driven pagination: poetry_pagination(current:, total:,
|
|
182
|
+
# path:) - path: is a callable ->(page) { url }; the component owns the
|
|
183
|
+
# truncation math and the accessible nav.
|
|
184
|
+
#
|
|
185
|
+
# @example Paginating a product list
|
|
186
|
+
# poetry_pagination(current: 3, total: 12,
|
|
187
|
+
# path: ->(page) { products_path(page: page) })
|
|
188
|
+
# @see Poetry::Ui::Pagination::Component
|
|
189
|
+
def poetry_pagination(**)
|
|
190
|
+
render(Poetry::Ui::Pagination::Component.new(**))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# A pulsing placeholder shown while content loads.
|
|
194
|
+
#
|
|
195
|
+
# @example An avatar-and-line placeholder pair
|
|
196
|
+
# poetry_skeleton(class: "size-10 rounded-full")
|
|
197
|
+
# poetry_skeleton(class: "h-4 w-32")
|
|
198
|
+
# @see Poetry::Ui::Skeleton::Component
|
|
199
|
+
def poetry_skeleton(**, &)
|
|
200
|
+
render(Poetry::Ui::Skeleton::Component.new(**), &)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# A thin divider between content, decorative or semantic.
|
|
204
|
+
#
|
|
205
|
+
# @example Horizontal divider between sections
|
|
206
|
+
# poetry_separator
|
|
207
|
+
# @see Poetry::Ui::Separator::Component
|
|
208
|
+
def poetry_separator(**)
|
|
209
|
+
render(Poetry::Ui::Separator::Component.new(**))
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# An indeterminate loading indicator that announces itself.
|
|
213
|
+
#
|
|
214
|
+
# @example
|
|
215
|
+
# poetry_spinner(label: "Saving...")
|
|
216
|
+
# @see Poetry::Ui::Spinner::Component
|
|
217
|
+
def poetry_spinner(**)
|
|
218
|
+
render(Poetry::Ui::Spinner::Component.new(**))
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A deferred region - Turbo loading physics (lazy/eager) plus
|
|
222
|
+
# poetry-owned placeholder and retryable-error states. The block is the
|
|
223
|
+
# placeholder.
|
|
224
|
+
#
|
|
225
|
+
# @example
|
|
226
|
+
# poetry_deferred(src: "/dashboard/activity")
|
|
227
|
+
# @see Poetry::Ui::Deferred::Component
|
|
228
|
+
def poetry_deferred(**, &)
|
|
229
|
+
render(Poetry::Ui::Deferred::Component.new(**), &)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Optimistic UI for a Turbo form: the block's
|
|
233
|
+
# form.optimistic_template authors the PREDICTED state as a
|
|
234
|
+
# turbo-stream in a <template>; the controller paints it on submit
|
|
235
|
+
# and reconciles (morph refresh) only when the server rejects. The
|
|
236
|
+
# server contract - success answers 204 or a targeted stream, NEVER
|
|
237
|
+
# a redirect; failure answers 4xx - and the morph-refresh meta
|
|
238
|
+
# prerequisite live in the Optimistic Forms guide on the docs site.
|
|
239
|
+
# attribute_name:/value: auto-inject the submitted-value hidden field
|
|
240
|
+
# (false survives; form.optimistic_hidden_field places it explicitly).
|
|
241
|
+
#
|
|
242
|
+
# @example
|
|
243
|
+
# <%= poetry_optimistic_form(model: task, attribute_name: :done, value: true) do |form| %>
|
|
244
|
+
# <%= form.optimistic_template do %>
|
|
245
|
+
# <%# the predicted turbo-stream(s) %>
|
|
246
|
+
# <% end %>
|
|
247
|
+
# <%= form.submit "Done" %>
|
|
248
|
+
# <% end %>
|
|
249
|
+
# @see Poetry::Ui::OptimisticFormBuilder
|
|
250
|
+
def poetry_optimistic_form(attribute_name: nil, value: OptimisticFormBuilder::UNSET,
|
|
251
|
+
**options, &)
|
|
252
|
+
options[:builder] = OptimisticFormBuilder
|
|
253
|
+
options[:data] = (options[:data] || {}).dup
|
|
254
|
+
options[:data][:controller] =
|
|
255
|
+
[options[:data][:controller], "poetry--core--optimistic-form"].compact.join(" ")
|
|
256
|
+
optimistic = Poetry::Core::Stimulus::Builder.new("poetry--core--optimistic-form",
|
|
257
|
+
Poetry::Core::HTML::Attributes.new)
|
|
258
|
+
options[:data][:action] = [
|
|
259
|
+
options[:data][:action],
|
|
260
|
+
optimistic.action(:apply, on: "turbo:submit-start"),
|
|
261
|
+
optimistic.action(:reconcile, on: "turbo:submit-end")
|
|
262
|
+
].compact.join(" ")
|
|
263
|
+
|
|
264
|
+
form_with(**options) do |form|
|
|
265
|
+
body = capture(form, &)
|
|
266
|
+
prefix = if optimistic_auto_inject?(form, attribute_name, value)
|
|
267
|
+
form.optimistic_hidden_field(attribute_name, value: value)
|
|
268
|
+
end
|
|
269
|
+
safe_join([prefix, body].compact)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Declares a form as a WebMCP tool - the declarative registration
|
|
274
|
+
# path: the <form> carries toolname/tooldescription, so a WebMCP
|
|
275
|
+
# browser registers it as an agent-callable tool with NO JavaScript
|
|
276
|
+
# (the parameter schema is synthesized from the controls; each
|
|
277
|
+
# parameter's description comes from its <label>, so the poetry
|
|
278
|
+
# FormBuilder's model-derived labels describe the tool for free;
|
|
279
|
+
# tool_description: on a field overrides one). Defaults the builder
|
|
280
|
+
# to Poetry::Ui::FormBuilder. autosubmit: true is GET-only by
|
|
281
|
+
# construction - a mutating form always keeps the user's Submit.
|
|
282
|
+
#
|
|
283
|
+
# @example A read-only lookup the agent may submit itself
|
|
284
|
+
# <%= poetry_webmcp_form(url: orders_path, method: :get,
|
|
285
|
+
# tool: { name: "find_orders", description: "Search orders by timeframe.",
|
|
286
|
+
# autosubmit: true }) do |form| %>
|
|
287
|
+
# <%= form.field(:timeframe, tool_description: "A relative range such as last_7_days.") %>
|
|
288
|
+
# <% end %>
|
|
289
|
+
# @see Poetry::Ui::Webmcp
|
|
290
|
+
def poetry_webmcp_form(tool:, **options, &)
|
|
291
|
+
options[:builder] ||= FormBuilder
|
|
292
|
+
options[:html] = (options[:html] || {}).merge(Webmcp.form_attributes(tool, method: options[:method]))
|
|
293
|
+
# The poetry-agent form controller answers an agent-invoked submit
|
|
294
|
+
# with the outcome (SubmitEvent.respondWith); without the runtime
|
|
295
|
+
# gem the token is inert and the browser's own submission runs.
|
|
296
|
+
options[:data] = (options[:data] || {}).dup
|
|
297
|
+
options[:data][:controller] = [options[:data][:controller], Webmcp::FORM_CONTROLLER].compact.join(" ")
|
|
298
|
+
form_with(**options, &)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# The key text is the content block: poetry_kbd { "⌘" }.
|
|
302
|
+
#
|
|
303
|
+
# @example
|
|
304
|
+
# poetry_kbd { "Esc" }
|
|
305
|
+
# @see Poetry::Ui::Kbd::Component
|
|
306
|
+
def poetry_kbd(**, &)
|
|
307
|
+
render(Poetry::Ui::Kbd::Component.new(**), &)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# A run of key chords (upstream KbdGroup): a wrapping <kbd> carrying
|
|
311
|
+
# the themed gap - poetry_kbd children render inside.
|
|
312
|
+
#
|
|
313
|
+
# @example
|
|
314
|
+
# <%= poetry_kbd_group do %>
|
|
315
|
+
# <%= poetry_kbd { "⌘" } %><%= poetry_kbd { "K" } %>
|
|
316
|
+
# <% end %>
|
|
317
|
+
# @see Poetry::Ui::Kbd::Component
|
|
318
|
+
def poetry_kbd_group(**attrs, &)
|
|
319
|
+
classes = ["cn-kbd-group inline-flex items-center", attrs.delete(:class)].compact.join(" ")
|
|
320
|
+
content_tag(:kbd, capture(&), class: classes, "data-slot": "kbd-group", **attrs)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# ratio: is a string fraction ("16/9") - Ruby's 16/9 would truncate.
|
|
324
|
+
#
|
|
325
|
+
# @example A 16:9 media box
|
|
326
|
+
# poetry_aspect_ratio(ratio: "16/9") do
|
|
327
|
+
# image_tag "cover.jpg", class: "size-full object-cover"
|
|
328
|
+
# end
|
|
329
|
+
# @see Poetry::Ui::AspectRatio::Component
|
|
330
|
+
def poetry_aspect_ratio(**, &)
|
|
331
|
+
render(Poetry::Ui::AspectRatio::Component.new(**), &)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# An empty-state placeholder with an icon, message, and actions.
|
|
335
|
+
#
|
|
336
|
+
# @example Empty collection with a next action
|
|
337
|
+
# poetry_empty do |empty|
|
|
338
|
+
# empty.with_title { "No projects yet" }
|
|
339
|
+
# empty.with_description { "Create your first project to get started." }
|
|
340
|
+
# poetry_button { "New project" }
|
|
341
|
+
# end
|
|
342
|
+
# @see Poetry::Ui::Empty::Component
|
|
343
|
+
def poetry_empty(**, &)
|
|
344
|
+
render(Poetry::Ui::Empty::Component.new(**), &)
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The server-driven DataTable: rows/state/path come from the
|
|
348
|
+
# controller (State.from_params with a sortable: whitelist); columns
|
|
349
|
+
# are declared in the block. Sorting/filter/page are URL state.
|
|
350
|
+
#
|
|
351
|
+
# @example A sortable notes table
|
|
352
|
+
# <%= poetry_data_table(rows: @notes, state: state, total: @pages,
|
|
353
|
+
# path: ->(p) { notes_path(**p) }, caption: "Notes") do |t| %>
|
|
354
|
+
# <% t.with_column("Title", key: :title, sortable: true) { |note| note.title } %>
|
|
355
|
+
# <% t.with_column("Created", key: :created_at, sortable: true) { |note| note.created_at.to_date } %>
|
|
356
|
+
# <% end %>
|
|
357
|
+
# @see Poetry::Ui::DataTable::Component
|
|
358
|
+
def poetry_data_table(**, &)
|
|
359
|
+
render(Poetry::Ui::DataTable::Component.new(**), &)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# An input that suggests options as you type - the text itself is the
|
|
363
|
+
# value.
|
|
364
|
+
#
|
|
365
|
+
# @example Free text with suggestions
|
|
366
|
+
# poetry_autocomplete(name: "tag", label: "Search tags") do |auto|
|
|
367
|
+
# auto.with_item(label: "feature")
|
|
368
|
+
# auto.with_item(label: "fix")
|
|
369
|
+
# end
|
|
370
|
+
# @see Poetry::Ui::Autocomplete::Component
|
|
371
|
+
def poetry_autocomplete(**, &)
|
|
372
|
+
render(Poetry::Ui::Autocomplete::Component.new(**), &)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# The content block is the initials fallback.
|
|
376
|
+
#
|
|
377
|
+
# @example Image with initials fallback
|
|
378
|
+
# poetry_avatar(src: user.avatar_url, label: "Ada Lovelace") { "AL" }
|
|
379
|
+
# @see Poetry::Ui::Avatar::Component
|
|
380
|
+
def poetry_avatar(**, &)
|
|
381
|
+
render(Poetry::Ui::Avatar::Component.new(**), &)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Shows the path to the current page as a trail of links.
|
|
385
|
+
#
|
|
386
|
+
# @example
|
|
387
|
+
# <%= poetry_breadcrumb do |crumb| %>
|
|
388
|
+
# <% crumb.with_item("Home", href: "/") %>
|
|
389
|
+
# <% crumb.with_ellipsis %>
|
|
390
|
+
# <% crumb.with_item("Breadcrumb") %>
|
|
391
|
+
# <% end %>
|
|
392
|
+
# @see Poetry::Ui::Breadcrumb::Component
|
|
393
|
+
def poetry_breadcrumb(**, &)
|
|
394
|
+
render(Poetry::Ui::Breadcrumb::Component.new(**), &)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# A determinate progress bar toward task completion.
|
|
398
|
+
#
|
|
399
|
+
# @example
|
|
400
|
+
# poetry_progress(value: 60, label: "Uploading")
|
|
401
|
+
# @see Poetry::Ui::Progress::Component
|
|
402
|
+
def poetry_progress(**)
|
|
403
|
+
render(Poetry::Ui::Progress::Component.new(**))
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# A generic list row with media, content, and actions.
|
|
407
|
+
#
|
|
408
|
+
# @example
|
|
409
|
+
# poetry_item(variant: :outline) do |item|
|
|
410
|
+
# item.with_title { "Backups" }
|
|
411
|
+
# item.with_description { "Nightly, retained 30 days" }
|
|
412
|
+
# end
|
|
413
|
+
# @see Poetry::Ui::Item::Component
|
|
414
|
+
def poetry_item(**, &)
|
|
415
|
+
render(Poetry::Ui::Item::Component.new(**), &)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Part helpers (the Table pattern): pure class/slot stamps around
|
|
419
|
+
# composed Avatars and Items.
|
|
420
|
+
{
|
|
421
|
+
avatar_group: [Poetry::Ui::Avatar, "avatar-group", :group, {}],
|
|
422
|
+
avatar_group_count: [Poetry::Ui::Avatar, "avatar-group-count", :group_count, {}],
|
|
423
|
+
item_group: [Poetry::Ui::Item, "item-group", :group, { "role" => "list" }]
|
|
424
|
+
}.each do |name, (namespace, slot, element, extra)|
|
|
425
|
+
define_method("poetry_#{name}") do |**attrs, &block|
|
|
426
|
+
classes = [namespace::Style.css(element), attrs.delete(:class)].compact.join(" ")
|
|
427
|
+
data = { slot: slot }.merge(attrs.delete(:data) || {})
|
|
428
|
+
content_tag(:div, (capture(&block) if block), **extra, **attrs, class: classes, data: data)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# The row divider inside an item group: the Separator with the
|
|
433
|
+
# item-separator slot + spacing.
|
|
434
|
+
#
|
|
435
|
+
# @example
|
|
436
|
+
# poetry_item_separator
|
|
437
|
+
# @see Poetry::Ui::Item::Component
|
|
438
|
+
def poetry_item_separator(**attrs)
|
|
439
|
+
classes = [Poetry::Ui::Item::Style.css(:separator), attrs.delete(:class)].compact.join(" ")
|
|
440
|
+
render(Poetry::Ui::Separator::Component.new(**attrs, class: classes, "data-slot": "item-separator"))
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# The Calendar: a server-rendered month grid; name: makes it
|
|
444
|
+
# a form control. poetry--core--calendar adds nav + selection.
|
|
445
|
+
#
|
|
446
|
+
# @example A form-posting date pick
|
|
447
|
+
# poetry_calendar(name: "event[date]", selected: "2026-07-04")
|
|
448
|
+
# @see Poetry::Ui::Calendar::Component
|
|
449
|
+
def poetry_calendar(**, &)
|
|
450
|
+
render(Poetry::Ui::Calendar::Component.new(**), &)
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# The DatePicker: a field-shaped trigger opening a Calendar in
|
|
454
|
+
# a Popover; name: is the form field.
|
|
455
|
+
#
|
|
456
|
+
# @example
|
|
457
|
+
# poetry_date_picker(
|
|
458
|
+
# name: "due_on", label: "Due date", value: Date.new(2026, 6, 5)
|
|
459
|
+
# )
|
|
460
|
+
# @see Poetry::Ui::DatePicker::Component
|
|
461
|
+
def poetry_date_picker(**, &)
|
|
462
|
+
render(Poetry::Ui::DatePicker::Component.new(**), &)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# The Sidebar: the app-shell frame - with_nav is the column,
|
|
466
|
+
# with_inset the page area; poetry--core--sidebar owns the collapse.
|
|
467
|
+
#
|
|
468
|
+
# @example App shell wired to the persisted cookie
|
|
469
|
+
# <%= poetry_sidebar(open: cookies[:sidebar_state] != "false", collapsible: :icon) do |shell| %>
|
|
470
|
+
# <% shell.with_nav do %>
|
|
471
|
+
# <%= poetry_sidebar_group do %>...menu...<% end %>
|
|
472
|
+
# <% end %>
|
|
473
|
+
# <% shell.with_inset do %>
|
|
474
|
+
# <%= poetry_sidebar_trigger %>
|
|
475
|
+
# <main>...page...</main>
|
|
476
|
+
# <% end %>
|
|
477
|
+
# <% end %>
|
|
478
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
479
|
+
def poetry_sidebar(**, &)
|
|
480
|
+
render(Poetry::Ui::Sidebar::Component.new(**), &)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Client-side toast delivery: pressing the trigger clones the
|
|
484
|
+
# addressed <template>'s toast into the toaster region - no server
|
|
485
|
+
# round-trip.
|
|
486
|
+
#
|
|
487
|
+
# @example
|
|
488
|
+
# <%= poetry_toast_trigger(template: "copied-toast") do %>
|
|
489
|
+
# Copy link
|
|
490
|
+
# <% end %>
|
|
491
|
+
# <template id="copied-toast">
|
|
492
|
+
# <%= poetry_toast(duration: 4000) do |toast| %>
|
|
493
|
+
# <% toast.with_title { "Copied" } %>
|
|
494
|
+
# <% end %>
|
|
495
|
+
# </template>
|
|
496
|
+
# @see Poetry::Ui::ToastTrigger::Component
|
|
497
|
+
def poetry_toast_trigger(**, &)
|
|
498
|
+
render(Poetry::Ui::ToastTrigger::Component.new(**), &)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# The sidebar collapse toggle (lives in the inset): a ghost icon
|
|
502
|
+
# Button wired to the sidebar controller on the wrapper.
|
|
503
|
+
#
|
|
504
|
+
# @example
|
|
505
|
+
# poetry_sidebar_trigger
|
|
506
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
507
|
+
def poetry_sidebar_trigger(**attrs)
|
|
508
|
+
action = Poetry::Ui::Sidebar::Component.stimulus_action(:toggle, on: :click)
|
|
509
|
+
# A caller data: hash augments the toggle wiring instead of
|
|
510
|
+
# replacing it at the kwargs splat (actions token-join).
|
|
511
|
+
caller_data = attrs.delete(:data) || {}
|
|
512
|
+
data = { slot: "sidebar-trigger", action: action }.merge(caller_data) do |key, wired, caller|
|
|
513
|
+
key == :action ? Poetry::Core::Config.current.stimulus_merger.merge_actions(wired, caller) : caller
|
|
514
|
+
end
|
|
515
|
+
render(Poetry::Ui::Button::Component.new(
|
|
516
|
+
variant: :ghost, size: :"icon-sm", label: "Toggle Sidebar",
|
|
517
|
+
data: data, **attrs
|
|
518
|
+
)) { poetry_icon(name: :"panel-left") }
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# The rail: an edge strip that toggles the sidebar (tabindex -1 - the
|
|
522
|
+
# trigger is the keyboard affordance).
|
|
523
|
+
#
|
|
524
|
+
# @example
|
|
525
|
+
# poetry_sidebar_rail
|
|
526
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
527
|
+
def poetry_sidebar_rail(**attrs)
|
|
528
|
+
classes = [Poetry::Ui::Sidebar::Style.css(:rail), attrs.delete(:class)].compact.join(" ")
|
|
529
|
+
wiring = { type: "button", "aria-label": "Toggle Sidebar", tabindex: "-1",
|
|
530
|
+
title: "Toggle Sidebar", class: classes,
|
|
531
|
+
data: { slot: "sidebar-rail",
|
|
532
|
+
action: Poetry::Ui::Sidebar::Component.stimulus_action(:toggle, on: :click) } }
|
|
533
|
+
content_tag(:button, nil, Poetry::Core::HTML::Attributes.merged(wiring, attrs))
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# The simple content-part stamps (div/ul/li wrappers with the slot +
|
|
537
|
+
# dictionary classes).
|
|
538
|
+
{
|
|
539
|
+
sidebar_header: [:div, "sidebar-header", :header],
|
|
540
|
+
sidebar_footer: [:div, "sidebar-footer", :footer],
|
|
541
|
+
sidebar_content: [:div, "sidebar-content", :content],
|
|
542
|
+
sidebar_group: [:div, "sidebar-group", :group],
|
|
543
|
+
sidebar_group_content: [:div, "sidebar-group-content", :group_content],
|
|
544
|
+
sidebar_menu: [:ul, "sidebar-menu", :menu],
|
|
545
|
+
sidebar_menu_item: [:li, "sidebar-menu-item", :menu_item],
|
|
546
|
+
sidebar_menu_sub: [:ul, "sidebar-menu-sub", :menu_sub],
|
|
547
|
+
sidebar_menu_sub_item: [:li, "sidebar-menu-sub-item", :menu_sub_item]
|
|
548
|
+
}.each do |name, (tag_name, slot, element)|
|
|
549
|
+
define_method("poetry_#{name}") do |**attrs, &block|
|
|
550
|
+
classes = [Poetry::Ui::Sidebar::Style.css(element), attrs.delete(:class)].compact.join(" ")
|
|
551
|
+
data = { slot: slot }.merge(attrs.delete(:data) || {})
|
|
552
|
+
content_tag(tag_name, (capture(&block) if block), **attrs, class: classes, data: data)
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# The group heading: a muted label above a sidebar menu.
|
|
557
|
+
#
|
|
558
|
+
# @example
|
|
559
|
+
# poetry_sidebar_group_label { "Projects" }
|
|
560
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
561
|
+
def poetry_sidebar_group_label(**attrs, &block)
|
|
562
|
+
classes = [Poetry::Ui::Sidebar::Style.css(:group_label), attrs.delete(:class)].compact.join(" ")
|
|
563
|
+
content_tag(:div, (capture(&block) if block), **attrs, class: classes,
|
|
564
|
+
data: { slot: "sidebar-group-label" })
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# The themed divider between sidebar groups.
|
|
568
|
+
#
|
|
569
|
+
# @example
|
|
570
|
+
# poetry_sidebar_separator
|
|
571
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
572
|
+
def poetry_sidebar_separator(**attrs)
|
|
573
|
+
classes = [Poetry::Ui::Sidebar::Style.css(:separator), attrs.delete(:class)].compact.join(" ")
|
|
574
|
+
render(Poetry::Ui::Separator::Component.new(**attrs, class: classes, "data-slot": "sidebar-separator"))
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# A menu button: an anchor (href:) or a button, with the active state
|
|
578
|
+
# + size and variant axes. active: is the current route (data-active
|
|
579
|
+
# styles it); variant: :outline draws the bordered treatment.
|
|
580
|
+
#
|
|
581
|
+
# @example
|
|
582
|
+
# poetry_sidebar_menu_button(href: "/projects", active: true) { "Projects" }
|
|
583
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
584
|
+
def poetry_sidebar_menu_button(href: nil, active: false, size: :default,
|
|
585
|
+
variant: :default, **attrs, &block)
|
|
586
|
+
unless %i[default outline].include?(variant.to_sym)
|
|
587
|
+
raise ArgumentError, "sidebar menu button variant: must be :default or :outline"
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
style = Poetry::Ui::Sidebar::Style
|
|
591
|
+
classes = [style.css(:menu_button), style.menu_button_size(size),
|
|
592
|
+
"cn-sidebar-menu-button-variant-#{variant}", attrs.delete(:class)].compact.join(" ")
|
|
593
|
+
data = { slot: "sidebar-menu-button", size: size, variant: variant,
|
|
594
|
+
active: active ? "" : nil }.compact.merge(attrs.delete(:data) || {})
|
|
595
|
+
if href
|
|
596
|
+
content_tag(:a, (capture(&block) if block), href: href, class: classes, data: data,
|
|
597
|
+
"aria-current": active ? "page" : nil, **attrs)
|
|
598
|
+
else
|
|
599
|
+
content_tag(:button, (capture(&block) if block), type: "button", class: classes, data: data, **attrs)
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# The group-corner action (upstream SidebarGroupAction): pinned to the
|
|
604
|
+
# group's top-right by the theme.
|
|
605
|
+
#
|
|
606
|
+
# @example
|
|
607
|
+
# poetry_sidebar_group_action(label: "Add project") { poetry_icon(name: :plus) }
|
|
608
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
609
|
+
def poetry_sidebar_group_action(label: nil, **attrs, &block)
|
|
610
|
+
classes = [Poetry::Ui::Sidebar::Style.css(:group_action_button), attrs.delete(:class)].compact.join(" ")
|
|
611
|
+
data = { slot: "sidebar-group-action", sidebar: "group-action" }.merge(attrs.delete(:data) || {})
|
|
612
|
+
content_tag(:button, (capture(&block) if block), type: "button", class: classes, data: data,
|
|
613
|
+
"aria-label": label, **attrs)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
# The sidebar search input (upstream SidebarInput): the themed Input
|
|
617
|
+
# sized for the header well.
|
|
618
|
+
#
|
|
619
|
+
# @example
|
|
620
|
+
# poetry_sidebar_input(placeholder: "Search...")
|
|
621
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
622
|
+
def poetry_sidebar_input(**attrs)
|
|
623
|
+
classes = [Poetry::Ui::Sidebar::Style.css(:input_control), attrs.delete(:class)].compact.join(" ")
|
|
624
|
+
render(Poetry::Ui::Input::Component.new(**attrs, class: classes, "data-slot": "sidebar-input"))
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# A loading placeholder row (upstream SidebarMenuSkeleton): optional
|
|
628
|
+
# leading icon block + a text bar with a random-ish width the caller
|
|
629
|
+
# can pin via text_width:.
|
|
630
|
+
#
|
|
631
|
+
# @example
|
|
632
|
+
# poetry_sidebar_menu_skeleton(icon: true)
|
|
633
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
634
|
+
def poetry_sidebar_menu_skeleton(icon: false, text_width: "70%", **attrs)
|
|
635
|
+
classes = [Poetry::Ui::Sidebar::Style.css(:menu_skeleton), attrs.delete(:class)].compact.join(" ")
|
|
636
|
+
data = { slot: "sidebar-menu-skeleton" }.merge(attrs.delete(:data) || {})
|
|
637
|
+
content_tag(:div, class: classes, data: data, **attrs) do
|
|
638
|
+
safe_join([
|
|
639
|
+
(if icon
|
|
640
|
+
render(Poetry::Ui::Skeleton::Component.new(class: Poetry::Ui::Sidebar::Style.css(:menu_skeleton_icon),
|
|
641
|
+
"data-slot": "sidebar-menu-skeleton-icon"))
|
|
642
|
+
end),
|
|
643
|
+
render(Poetry::Ui::Skeleton::Component.new(class: Poetry::Ui::Sidebar::Style.css(:menu_skeleton_text),
|
|
644
|
+
"data-slot": "sidebar-menu-skeleton-text",
|
|
645
|
+
style: "--skeleton-width: #{text_width}"))
|
|
646
|
+
].compact)
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# An item-corner action (menu-action): absolutely positioned inside the
|
|
651
|
+
# menu item (the menu button reserves pr-8 room via the group marker).
|
|
652
|
+
# show_on_hover: keeps it invisible until the item is hovered/focused
|
|
653
|
+
# on desktop.
|
|
654
|
+
#
|
|
655
|
+
# @example
|
|
656
|
+
# poetry_sidebar_menu_action(label: "More", show_on_hover: true) { poetry_icon(name: :ellipsis) }
|
|
657
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
658
|
+
def poetry_sidebar_menu_action(show_on_hover: false, label: nil, **attrs, &block)
|
|
659
|
+
style = Poetry::Ui::Sidebar::Style
|
|
660
|
+
classes = [style.css(:menu_action), (style.css(:menu_action_hover) if show_on_hover),
|
|
661
|
+
attrs.delete(:class)].compact.join(" ")
|
|
662
|
+
data = { slot: "sidebar-menu-action", sidebar: "menu-action" }.merge(attrs.delete(:data) || {})
|
|
663
|
+
content_tag(:button, (capture(&block) if block), type: "button", class: classes, data: data,
|
|
664
|
+
"aria-label": label, **attrs)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# A trailing badge (menu-badge): count/status chrome in the item corner,
|
|
668
|
+
# pointer-transparent.
|
|
669
|
+
#
|
|
670
|
+
# @example
|
|
671
|
+
# poetry_sidebar_menu_badge { "24" }
|
|
672
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
673
|
+
def poetry_sidebar_menu_badge(**attrs, &block)
|
|
674
|
+
style = Poetry::Ui::Sidebar::Style
|
|
675
|
+
classes = [style.css(:menu_badge), attrs.delete(:class)].compact.join(" ")
|
|
676
|
+
data = { slot: "sidebar-menu-badge", sidebar: "menu-badge" }.merge(attrs.delete(:data) || {})
|
|
677
|
+
content_tag(:div, (capture(&block) if block), class: classes, data: data, **attrs)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
# A nested menu entry: an anchor (href:) or a button; active: marks
|
|
681
|
+
# the current route (aria-current + data-active).
|
|
682
|
+
#
|
|
683
|
+
# @example
|
|
684
|
+
# poetry_sidebar_menu_sub_button(href: "/settings") { "Settings" }
|
|
685
|
+
# @see Poetry::Ui::Sidebar::Component
|
|
686
|
+
def poetry_sidebar_menu_sub_button(href: nil, active: false, **attrs, &block)
|
|
687
|
+
style = Poetry::Ui::Sidebar::Style
|
|
688
|
+
classes = [style.css(:menu_sub_button), attrs.delete(:class)].compact.join(" ")
|
|
689
|
+
data = { slot: "sidebar-menu-sub-button",
|
|
690
|
+
active: active ? "" : nil }.compact.merge(attrs.delete(:data) || {})
|
|
691
|
+
tag_name = href ? :a : :button
|
|
692
|
+
extra = if href
|
|
693
|
+
{ href: href, "aria-current": active ? "page" : nil }
|
|
694
|
+
else
|
|
695
|
+
{ type: "button" }
|
|
696
|
+
end
|
|
697
|
+
content_tag(tag_name, (capture(&block) if block), class: classes, data: data, **extra, **attrs)
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# The NavigationMenu: a disclosure bar - with_item for
|
|
701
|
+
# trigger+panel, with_link for destinations; label: names the nav.
|
|
702
|
+
#
|
|
703
|
+
# @example Disclosure bar with a panel and a link
|
|
704
|
+
# <%= poetry_navigation_menu(label: "Main") do |nav| %>
|
|
705
|
+
# <% nav.with_item("Products", value: "products") do %>
|
|
706
|
+
# <%= poetry_navigation_menu_link(href: products_path) { "All products" } %>
|
|
707
|
+
# <% end %>
|
|
708
|
+
# <% nav.with_link("Docs", href: docs_path) %>
|
|
709
|
+
# <% end %>
|
|
710
|
+
# @see Poetry::Ui::NavigationMenu::Component
|
|
711
|
+
def poetry_navigation_menu(**, &)
|
|
712
|
+
render(Poetry::Ui::NavigationMenu::Component.new(**), &)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# A panel entry: a REAL link (active: marks the current page).
|
|
716
|
+
#
|
|
717
|
+
# @example
|
|
718
|
+
# poetry_navigation_menu_link(href: "/docs", active: true) { "Docs" }
|
|
719
|
+
# @see Poetry::Ui::NavigationMenu::Component
|
|
720
|
+
def poetry_navigation_menu_link(href:, active: false, **attrs, &block)
|
|
721
|
+
classes = [Poetry::Ui::NavigationMenu::Style.css(:link), attrs.delete(:class)].compact.join(" ")
|
|
722
|
+
data = { slot: "navigation-menu-link", active: active ? "true" : nil }.compact
|
|
723
|
+
.merge(attrs.delete(:data) || {})
|
|
724
|
+
content_tag(:a, (capture(&block) if block), **attrs, href: href, class: classes, data: data)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# The Carousel: native scroll-snap slides - with_item per
|
|
728
|
+
# slide; label: names the region.
|
|
729
|
+
#
|
|
730
|
+
# @example
|
|
731
|
+
# poetry_carousel(label: "Featured") do |carousel|
|
|
732
|
+
# carousel.with_item { "Slide one" }
|
|
733
|
+
# carousel.with_item { "Slide two" }
|
|
734
|
+
# end
|
|
735
|
+
# @see Poetry::Ui::Carousel::Component
|
|
736
|
+
def poetry_carousel(**, &)
|
|
737
|
+
render(Poetry::Ui::Carousel::Component.new(**), &)
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
# The Resizable panel group: with_panel x N; handles are
|
|
741
|
+
# interleaved automatically (APG window splitters).
|
|
742
|
+
#
|
|
743
|
+
# @example
|
|
744
|
+
# poetry_resizable(class: "h-48 rounded-lg border") do |group|
|
|
745
|
+
# group.with_panel(default_size: 25) { tag.div("Sidebar") }
|
|
746
|
+
# group.with_panel { tag.div("Content") }
|
|
747
|
+
# end
|
|
748
|
+
# @see Poetry::Ui::Resizable::Component
|
|
749
|
+
def poetry_resizable(**, &)
|
|
750
|
+
render(Poetry::Ui::Resizable::Component.new(**), &)
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
# The Drawer: the swipeable edge dialog - trigger/title/
|
|
754
|
+
# description/footer slots, direction: down/up/left/right.
|
|
755
|
+
#
|
|
756
|
+
# @example
|
|
757
|
+
# poetry_drawer(show_swipe_handle: true) do |drawer|
|
|
758
|
+
# drawer.with_trigger { "Open drawer" }
|
|
759
|
+
# drawer.with_title { "Move goal" }
|
|
760
|
+
# drawer.with_description { "Set your daily activity goal." }
|
|
761
|
+
# "Drawer body"
|
|
762
|
+
# end
|
|
763
|
+
# @see Poetry::Ui::Drawer::Component
|
|
764
|
+
def poetry_drawer(**, &)
|
|
765
|
+
render(Poetry::Ui::Drawer::Component.new(**), &)
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# The native scroll region: size it with classes; label: names it.
|
|
769
|
+
#
|
|
770
|
+
# @example A bounded list that scrolls
|
|
771
|
+
# poetry_scroll_area(label: "Tags", class: "h-72 w-48") do
|
|
772
|
+
# safe_join(tags.map { |tag| tag.name })
|
|
773
|
+
# end
|
|
774
|
+
# @see Poetry::Ui::ScrollArea::Component
|
|
775
|
+
def poetry_scroll_area(**, &)
|
|
776
|
+
render(Poetry::Ui::ScrollArea::Component.new(**), &)
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Tabs: declare with with_tab(title, value:) + panel blocks;
|
|
780
|
+
# the component owns the ARIA wiring and the two-controller split.
|
|
781
|
+
#
|
|
782
|
+
# @example
|
|
783
|
+
# <%= poetry_tabs(default: "account", label: "Account settings") do |tabs| %>
|
|
784
|
+
# <% tabs.with_tab("Account", value: "account") do %>...panel...<% end %>
|
|
785
|
+
# <% tabs.with_tab("Password", value: "password") do %>...panel...<% end %>
|
|
786
|
+
# <% end %>
|
|
787
|
+
# @see Poetry::Ui::Tabs::Component
|
|
788
|
+
def poetry_tabs(**, &)
|
|
789
|
+
render(Poetry::Ui::Tabs::Component.new(**), &)
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
# Visually joins adjacent buttons and controls into one group.
|
|
793
|
+
#
|
|
794
|
+
# @example A segmented pair
|
|
795
|
+
# poetry_button_group("aria-label": "Alignment") do
|
|
796
|
+
# safe_join([
|
|
797
|
+
# poetry_button(variant: :outline) { "Left" },
|
|
798
|
+
# poetry_button(variant: :outline) { "Right" }
|
|
799
|
+
# ])
|
|
800
|
+
# end
|
|
801
|
+
# @see Poetry::Ui::ButtonGroup::Component
|
|
802
|
+
def poetry_button_group(**, &)
|
|
803
|
+
render(Poetry::Ui::ButtonGroup::Component.new(**), &)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
# Non-interactive text inside a ButtonGroup run.
|
|
807
|
+
#
|
|
808
|
+
# @example
|
|
809
|
+
# poetry_button_group_text { "of 12" }
|
|
810
|
+
# @see Poetry::Ui::ButtonGroup::Component
|
|
811
|
+
def poetry_button_group_text(**attrs, &block)
|
|
812
|
+
classes = [Poetry::Ui::ButtonGroup::Style.css(:text), attrs.delete(:class)].compact.join(" ")
|
|
813
|
+
data = { slot: "button-group-text" }.merge(attrs.delete(:data) || {})
|
|
814
|
+
content_tag(:div, (capture(&block) if block), **attrs, class: classes, data: data)
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# The vertical divider between grouped controls.
|
|
818
|
+
#
|
|
819
|
+
# @example
|
|
820
|
+
# poetry_button_group_separator
|
|
821
|
+
# @see Poetry::Ui::ButtonGroup::Component
|
|
822
|
+
def poetry_button_group_separator(**attrs)
|
|
823
|
+
classes = [Poetry::Ui::ButtonGroup::Style.css(:separator), attrs.delete(:class)].compact.join(" ")
|
|
824
|
+
render(Poetry::Ui::Separator::Component.new(orientation: :vertical, **attrs, class: classes,
|
|
825
|
+
"data-slot": "button-group-separator"))
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# A styled wrapper around the real native select control.
|
|
829
|
+
#
|
|
830
|
+
# @example The options: fast path
|
|
831
|
+
# poetry_native_select(
|
|
832
|
+
# name: "sort", label: "Sort by",
|
|
833
|
+
# options: [["Newest first", "newest"], ["Oldest first", "oldest"]],
|
|
834
|
+
# selected: "newest"
|
|
835
|
+
# )
|
|
836
|
+
# @see Poetry::Ui::NativeSelect::Component
|
|
837
|
+
def poetry_native_select(**, &)
|
|
838
|
+
render(Poetry::Ui::NativeSelect::Component.new(**), &)
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
# One styled <option> inside poetry_native_select.
|
|
842
|
+
#
|
|
843
|
+
# @example
|
|
844
|
+
# poetry_native_select_option(value: "us") { "United States" }
|
|
845
|
+
# @see Poetry::Ui::NativeSelect::Component
|
|
846
|
+
def poetry_native_select_option(**attrs, &block)
|
|
847
|
+
classes = [Poetry::Ui::NativeSelect::Style.css(:option), attrs.delete(:class)].compact.join(" ")
|
|
848
|
+
data = { slot: "native-select-option" }.merge(attrs.delete(:data) || {})
|
|
849
|
+
content_tag(:option, (capture(&block) if block), **attrs, class: classes, data: data)
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
# A styled <optgroup> inside poetry_native_select.
|
|
853
|
+
#
|
|
854
|
+
# @example
|
|
855
|
+
# poetry_native_select_optgroup(label: "Europe") do
|
|
856
|
+
# poetry_native_select_option(value: "fr") { "France" }
|
|
857
|
+
# end
|
|
858
|
+
# @see Poetry::Ui::NativeSelect::Component
|
|
859
|
+
def poetry_native_select_optgroup(**attrs, &block)
|
|
860
|
+
classes = [Poetry::Ui::NativeSelect::Style.css(:optgroup), attrs.delete(:class)].compact.join(" ")
|
|
861
|
+
data = { slot: "native-select-optgroup" }.merge(attrs.delete(:data) || {})
|
|
862
|
+
content_tag(:optgroup, (capture(&block) if block), **attrs, class: classes, data: data)
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# One bordered surface combining an input with buttons, icons, or
|
|
866
|
+
# add-ons.
|
|
867
|
+
#
|
|
868
|
+
# @example
|
|
869
|
+
# <%= poetry_input_group do %>
|
|
870
|
+
# <%= poetry_input_group_addon do %>
|
|
871
|
+
# <%= poetry_icon(name: :search) %>
|
|
872
|
+
# <% end %>
|
|
873
|
+
# <%= poetry_input_group_input(name: "q", placeholder: "Search...") %>
|
|
874
|
+
# <% end %>
|
|
875
|
+
# @see Poetry::Ui::InputGroup::Component
|
|
876
|
+
def poetry_input_group(**, &)
|
|
877
|
+
render(Poetry::Ui::InputGroup::Component.new(**), &)
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
# The valid align: values for poetry_input_group_addon.
|
|
881
|
+
INPUT_GROUP_ALIGNS = %i[inline-start inline-end block-start block-end].freeze
|
|
882
|
+
|
|
883
|
+
# An addon region inside an InputGroup - icons, text, or small
|
|
884
|
+
# buttons aligned to one of the four edges of the group.
|
|
885
|
+
#
|
|
886
|
+
# @example
|
|
887
|
+
# poetry_input_group_addon(align: :"inline-end") { poetry_icon(name: :search) }
|
|
888
|
+
# @see Poetry::Ui::InputGroup::Component
|
|
889
|
+
def poetry_input_group_addon(align: :"inline-start", **attrs, &block)
|
|
890
|
+
unless INPUT_GROUP_ALIGNS.include?(align.to_sym)
|
|
891
|
+
raise ArgumentError, "InputGroup addon align: must be one of #{INPUT_GROUP_ALIGNS.inspect}"
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
style = Poetry::Ui::InputGroup::Style
|
|
895
|
+
classes = [style.css(:addon), style.css(:"addon_#{align.to_s.tr("-", "_")}"),
|
|
896
|
+
attrs.delete(:class)].compact.join(" ")
|
|
897
|
+
data = { slot: "input-group-addon", align: align }.merge(attrs.delete(:data) || {})
|
|
898
|
+
content_tag(:div, (capture(&block) if block), **attrs, role: "group", class: classes, data: data)
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# Inline text inside an InputGroup - prefixes, suffixes, hints.
|
|
902
|
+
#
|
|
903
|
+
# @example
|
|
904
|
+
# poetry_input_group_text { "https://" }
|
|
905
|
+
# @see Poetry::Ui::InputGroup::Component
|
|
906
|
+
def poetry_input_group_text(**attrs, &block)
|
|
907
|
+
classes = [Poetry::Ui::InputGroup::Style.css(:text), attrs.delete(:class)].compact.join(" ")
|
|
908
|
+
content_tag(:span, (capture(&block) if block), **attrs, class: classes)
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
# The borderless in-group control: the group wears the chrome; the
|
|
912
|
+
# data-slot=input-group-control is what its focus/invalid selectors
|
|
913
|
+
# key on.
|
|
914
|
+
#
|
|
915
|
+
# @example
|
|
916
|
+
# poetry_input_group_input(placeholder: "Search...")
|
|
917
|
+
# @see Poetry::Ui::InputGroup::Component
|
|
918
|
+
def poetry_input_group_input(**attrs)
|
|
919
|
+
classes = [Poetry::Ui::InputGroup::Style.css(:control_input), attrs.delete(:class)].compact.join(" ")
|
|
920
|
+
render(Poetry::Ui::Input::Component.new(**attrs, class: classes, "data-slot": "input-group-control"))
|
|
921
|
+
end
|
|
922
|
+
|
|
923
|
+
# The borderless multiline in-group control - the group wears the
|
|
924
|
+
# chrome, like poetry_input_group_input.
|
|
925
|
+
#
|
|
926
|
+
# @example
|
|
927
|
+
# poetry_input_group_textarea(placeholder: "Add a note...")
|
|
928
|
+
# @see Poetry::Ui::InputGroup::Component
|
|
929
|
+
def poetry_input_group_textarea(**attrs)
|
|
930
|
+
classes = [Poetry::Ui::InputGroup::Style.css(:control_textarea), attrs.delete(:class)].compact.join(" ")
|
|
931
|
+
render(Poetry::Ui::Textarea::Component.new(**attrs, class: classes, "data-slot": "input-group-control"))
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
# The valid size: values for poetry_input_group_button.
|
|
935
|
+
INPUT_GROUP_BUTTON_SIZES = %i[xs sm icon-xs icon-sm].freeze
|
|
936
|
+
|
|
937
|
+
# The tiny in-group action: a ghost Button re-sized by the group's
|
|
938
|
+
# dictionary (tailwind_merge lets h-6 beat the Button's own h-9).
|
|
939
|
+
#
|
|
940
|
+
# @example
|
|
941
|
+
# poetry_input_group_button(size: :"icon-xs", label: "Copy") { poetry_icon(name: :copy) }
|
|
942
|
+
# @see Poetry::Ui::InputGroup::Component
|
|
943
|
+
def poetry_input_group_button(size: :xs, **attrs, &)
|
|
944
|
+
unless INPUT_GROUP_BUTTON_SIZES.include?(size.to_sym)
|
|
945
|
+
raise ArgumentError, "InputGroup button size: must be one of #{INPUT_GROUP_BUTTON_SIZES.inspect}"
|
|
946
|
+
end
|
|
947
|
+
# Button's own icon-only guard keys off ITS size:, which this helper
|
|
948
|
+
# bypasses - re-enforce the accessible-name rule here.
|
|
949
|
+
if size.to_s.start_with?("icon") && attrs[:label].blank?
|
|
950
|
+
raise ArgumentError, "icon-sized InputGroup button requires label: (the accessible name)"
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
style = Poetry::Ui::InputGroup::Style
|
|
954
|
+
size_css = style.css(:"button_#{size.to_s.tr("-", "_")}")
|
|
955
|
+
classes = [style.css(:button), size_css, attrs.delete(:class)].compact.join(" ")
|
|
956
|
+
render(Poetry::Ui::Button::Component.new(variant: :ghost, **attrs, class: classes,
|
|
957
|
+
"data-size": size), &)
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
# The value contracts runtime-enforced inside wrapper helpers, in
|
|
961
|
+
# registry shape - emitted as the registry's "helpers" section
|
|
962
|
+
# (rakelib/registry.rake) so poetry check and the MCP server validate
|
|
963
|
+
# these literals statically (the crash class: a helper-only literal
|
|
964
|
+
# like align: :leading that no component contract covers, failing
|
|
965
|
+
# only at render). Plain wrapper helpers need no entry here;
|
|
966
|
+
# registry generation lists them name-only.
|
|
967
|
+
HELPER_CONTRACTS = {
|
|
968
|
+
"poetry_toast_trigger" => {
|
|
969
|
+
"options" => [
|
|
970
|
+
{ "name" => "template", "type" => "string",
|
|
971
|
+
"description" => "id of the <template> holding the rendered poetry_toast" },
|
|
972
|
+
{ "name" => "toaster", "type" => "string",
|
|
973
|
+
"description" => "optional toaster region id - omit for the page's toaster" }
|
|
974
|
+
]
|
|
975
|
+
},
|
|
976
|
+
"poetry_optimistic_form" => {
|
|
977
|
+
"yields" => "the form builder - form.optimistic_template authors the predicted " \
|
|
978
|
+
"turbo-stream(s); form.optimistic_hidden_field places the submitted value; " \
|
|
979
|
+
"every standard field helper works",
|
|
980
|
+
"options" => [
|
|
981
|
+
{ "name" => "attribute_name", "type" => "symbol" },
|
|
982
|
+
{ "name" => "value", "type" => "object" }
|
|
983
|
+
]
|
|
984
|
+
},
|
|
985
|
+
"poetry_input_group_addon" => {
|
|
986
|
+
"options" => [{ "name" => "align", "type" => "symbol", "default" => "inline-start",
|
|
987
|
+
"variants" => INPUT_GROUP_ALIGNS.map(&:to_s) }]
|
|
988
|
+
},
|
|
989
|
+
"poetry_input_group_button" => {
|
|
990
|
+
"options" => [{ "name" => "size", "type" => "symbol", "default" => "xs",
|
|
991
|
+
"variants" => INPUT_GROUP_BUTTON_SIZES.map(&:to_s) }]
|
|
992
|
+
}
|
|
993
|
+
}.freeze
|
|
994
|
+
|
|
995
|
+
# A window overlaid on the page for content that requires attention.
|
|
996
|
+
#
|
|
997
|
+
# @example A confirmation dialog
|
|
998
|
+
# poetry_dialog do |dialog|
|
|
999
|
+
# dialog.with_trigger(variant: :outline) { "Open" }
|
|
1000
|
+
# dialog.with_title { "Are you sure?" }
|
|
1001
|
+
# dialog.with_description { "This cannot be undone." }
|
|
1002
|
+
# end
|
|
1003
|
+
# @see Poetry::Ui::Dialog::Component
|
|
1004
|
+
def poetry_dialog(**, &)
|
|
1005
|
+
render(Poetry::Ui::Dialog::Component.new(**), &)
|
|
1006
|
+
end
|
|
1007
|
+
|
|
1008
|
+
# A dialog that slides in from a screen edge.
|
|
1009
|
+
#
|
|
1010
|
+
# @example
|
|
1011
|
+
# poetry_sheet(side: :right) do |sheet|
|
|
1012
|
+
# sheet.with_trigger { "Edit profile" }
|
|
1013
|
+
# sheet.with_title { "Edit profile" }
|
|
1014
|
+
# "Sheet body"
|
|
1015
|
+
# end
|
|
1016
|
+
# @see Poetry::Ui::Sheet::Component
|
|
1017
|
+
def poetry_sheet(**, &)
|
|
1018
|
+
render(Poetry::Ui::Sheet::Component.new(**), &)
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
# A modal dialog that interrupts the user and expects a response.
|
|
1022
|
+
#
|
|
1023
|
+
# @example Destructive confirmation
|
|
1024
|
+
# poetry_alert_dialog do |dialog|
|
|
1025
|
+
# dialog.with_trigger(variant: :destructive) { "Delete project" }
|
|
1026
|
+
# dialog.with_title { "Delete this project?" }
|
|
1027
|
+
# dialog.with_description { "This cannot be undone." }
|
|
1028
|
+
# dialog.with_cancel { "Cancel" }
|
|
1029
|
+
# dialog.with_action(variant: :destructive) { "Delete" }
|
|
1030
|
+
# end
|
|
1031
|
+
# @see Poetry::Ui::AlertDialog::Component
|
|
1032
|
+
def poetry_alert_dialog(**, &)
|
|
1033
|
+
render(Poetry::Ui::AlertDialog::Component.new(**), &)
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
# A form control for entering a single line of text.
|
|
1037
|
+
#
|
|
1038
|
+
# @example An email field
|
|
1039
|
+
# poetry_input(type: "email", name: "email",
|
|
1040
|
+
# placeholder: "you@example.com")
|
|
1041
|
+
# @see Poetry::Ui::Input::Component
|
|
1042
|
+
def poetry_input(**)
|
|
1043
|
+
render(Poetry::Ui::Input::Component.new(**))
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
# Input's multiline sibling - the value is the CONTENT (value:), not
|
|
1047
|
+
# a block; auto-grow is CSS (field-sizing-content), never a JS
|
|
1048
|
+
# autosizer.
|
|
1049
|
+
#
|
|
1050
|
+
# @example A free-text field
|
|
1051
|
+
# poetry_textarea(name: "bio", rows: 4,
|
|
1052
|
+
# placeholder: "Tell us about yourself")
|
|
1053
|
+
# @see Poetry::Ui::Textarea::Component
|
|
1054
|
+
def poetry_textarea(**)
|
|
1055
|
+
render(Poetry::Ui::Textarea::Component.new(**))
|
|
1056
|
+
end
|
|
1057
|
+
|
|
1058
|
+
# Fixed-length code entry: ONE native input over aria-hidden cells
|
|
1059
|
+
# (paste/SMS-autofill/IME all native) - never per-cell inputs.
|
|
1060
|
+
#
|
|
1061
|
+
# @example
|
|
1062
|
+
# poetry_input_otp(name: "code", length: 6, groups: [3, 3])
|
|
1063
|
+
# @see Poetry::Ui::InputOtp::Component
|
|
1064
|
+
def poetry_input_otp(**)
|
|
1065
|
+
render(Poetry::Ui::InputOtp::Component.new(**))
|
|
1066
|
+
end
|
|
1067
|
+
|
|
1068
|
+
# An accessible caption bound to a form control.
|
|
1069
|
+
#
|
|
1070
|
+
# @example A label wired to its control
|
|
1071
|
+
# poetry_label(for_id: "email") { "Email" }
|
|
1072
|
+
# @see Poetry::Ui::Label::Component
|
|
1073
|
+
def poetry_label(**, &)
|
|
1074
|
+
render(Poetry::Ui::Label::Component.new(**), &)
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
# The formatted number field: a formatted text input
|
|
1078
|
+
# over a hidden type=number that submits the raw value; steppers
|
|
1079
|
+
# with press-and-hold; ArrowUp/Down (Shift/Alt sizes) on the input.
|
|
1080
|
+
#
|
|
1081
|
+
# @example
|
|
1082
|
+
# poetry_number_field(
|
|
1083
|
+
# name: "quantity", value: 2, min: 0, label: "Quantity"
|
|
1084
|
+
# )
|
|
1085
|
+
# @see Poetry::Ui::NumberField::Component
|
|
1086
|
+
def poetry_number_field(**)
|
|
1087
|
+
render(Poetry::Ui::NumberField::Component.new(**))
|
|
1088
|
+
end
|
|
1089
|
+
|
|
1090
|
+
# The segmented date editor:
|
|
1091
|
+
# a native input type=date (ISO on the wire, native pickers with no
|
|
1092
|
+
# JS) enhanced into per-segment spinbutton editing.
|
|
1093
|
+
#
|
|
1094
|
+
# @example
|
|
1095
|
+
# poetry_date_field(name: "event[on]", label: "Event date")
|
|
1096
|
+
# @see Poetry::Ui::DateField::Component
|
|
1097
|
+
def poetry_date_field(**)
|
|
1098
|
+
render(Poetry::Ui::DateField::Component.new(**))
|
|
1099
|
+
end
|
|
1100
|
+
|
|
1101
|
+
# DateField at hour granularity: HH:MM[:SS] on the wire; the locale
|
|
1102
|
+
# decides 12- vs 24-hour editing (hour_cycle: pins it).
|
|
1103
|
+
#
|
|
1104
|
+
# @example
|
|
1105
|
+
# poetry_time_field(
|
|
1106
|
+
# name: "starts_at", label: "Start time", value: "09:30"
|
|
1107
|
+
# )
|
|
1108
|
+
# @see Poetry::Ui::TimeField::Component
|
|
1109
|
+
def poetry_time_field(**)
|
|
1110
|
+
render(Poetry::Ui::TimeField::Component.new(**))
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# A quantity within a known range (disk, seats, strength) - role
|
|
1114
|
+
# "meter progressbar" (the two-token fallback); never indeterminate.
|
|
1115
|
+
#
|
|
1116
|
+
# @example
|
|
1117
|
+
# poetry_meter(value: 62, label: "Storage used")
|
|
1118
|
+
# @see Poetry::Ui::Meter::Component
|
|
1119
|
+
def poetry_meter(**)
|
|
1120
|
+
render(Poetry::Ui::Meter::Component.new(**))
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
# type=search on InputGroup chrome: Escape clears-then-dismisses,
|
|
1124
|
+
# focus-holding clear button, native WebKit affordances suppressed.
|
|
1125
|
+
#
|
|
1126
|
+
# @example
|
|
1127
|
+
# poetry_search_field(name: "q", label: "Search", placeholder: "Search...")
|
|
1128
|
+
# @see Poetry::Ui::SearchField::Component
|
|
1129
|
+
def poetry_search_field(**)
|
|
1130
|
+
render(Poetry::Ui::SearchField::Component.new(**))
|
|
1131
|
+
end
|
|
1132
|
+
|
|
1133
|
+
# Removable-chip collection (grid semantics, roving arrows, Delete
|
|
1134
|
+
# removal w/ focus recovery); name: serializes name[] per tag.
|
|
1135
|
+
#
|
|
1136
|
+
# @example Removable recipients that submit as recipients[]
|
|
1137
|
+
# poetry_tag_group(label: "Recipients", name: "recipients") do |group|
|
|
1138
|
+
# group.with_tag(value: "ada", label: "Ada")
|
|
1139
|
+
# group.with_tag(value: "grace", label: "Grace")
|
|
1140
|
+
# end
|
|
1141
|
+
# @see Poetry::Ui::TagGroup::Component
|
|
1142
|
+
def poetry_tag_group(**, &)
|
|
1143
|
+
render(Poetry::Ui::TagGroup::Component.new(**), &)
|
|
1144
|
+
end
|
|
1145
|
+
|
|
1146
|
+
# Hierarchical expandable list (flat treegrid): items via the
|
|
1147
|
+
# nested with_item builder, expansion persisted by the host through
|
|
1148
|
+
# poetry:tree:toggle.
|
|
1149
|
+
#
|
|
1150
|
+
# @example
|
|
1151
|
+
# <%= poetry_tree(label: "Files") do |tree| %>
|
|
1152
|
+
# <% tree.with_item(text: "docs", value: "docs", expanded: true) do |docs| %>
|
|
1153
|
+
# <% docs.with_item(text: "intro.md", value: "intro", href: "/docs/intro") %>
|
|
1154
|
+
# <% end %>
|
|
1155
|
+
# <% end %>
|
|
1156
|
+
# @see Poetry::Ui::Tree::Component
|
|
1157
|
+
def poetry_tree(**, &)
|
|
1158
|
+
render(Poetry::Ui::Tree::Component.new(**), &)
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
# Wraps a form control with its label, hint, and validation message.
|
|
1162
|
+
#
|
|
1163
|
+
# @example
|
|
1164
|
+
# poetry_field(
|
|
1165
|
+
# id: "email", label_text: "Email", hint: "We never share it."
|
|
1166
|
+
# ) do |field|
|
|
1167
|
+
# tag.input(type: "email", name: "email", **field.control_attributes)
|
|
1168
|
+
# end
|
|
1169
|
+
# @see Poetry::Ui::Field::Component
|
|
1170
|
+
def poetry_field(**, &)
|
|
1171
|
+
render(Poetry::Ui::Field::Component.new(**), &)
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
# The Field family's group layer: a run of related fields inside a
|
|
1175
|
+
# real <fieldset>, named by legend: (a real <legend>).
|
|
1176
|
+
#
|
|
1177
|
+
# @example A named group of address fields
|
|
1178
|
+
# poetry_fieldset(legend: "Shipping address") do
|
|
1179
|
+
# # poetry_field_group with the fields
|
|
1180
|
+
# end
|
|
1181
|
+
# @see Poetry::Ui::Fieldset::Component
|
|
1182
|
+
def poetry_fieldset(**, &)
|
|
1183
|
+
render(Poetry::Ui::Fieldset::Component.new(**), &)
|
|
1184
|
+
end
|
|
1185
|
+
|
|
1186
|
+
# Stacks fields/fieldsets with the theme's rhythm; also the CSS
|
|
1187
|
+
# `@container` scope Field's orientation: :responsive measures against.
|
|
1188
|
+
#
|
|
1189
|
+
# @example Stacking fields with the theme's rhythm
|
|
1190
|
+
# poetry_field_group do
|
|
1191
|
+
# safe_join([
|
|
1192
|
+
# poetry_field { ... },
|
|
1193
|
+
# poetry_field_separator,
|
|
1194
|
+
# poetry_field { ... }
|
|
1195
|
+
# ])
|
|
1196
|
+
# end
|
|
1197
|
+
# @see Poetry::Ui::FieldGroup::Component
|
|
1198
|
+
def poetry_field_group(**, &)
|
|
1199
|
+
render(Poetry::Ui::FieldGroup::Component.new(**), &)
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
# Divider between stacked fields; pass a block for the inline
|
|
1203
|
+
# caption form ("Or continue with").
|
|
1204
|
+
#
|
|
1205
|
+
# @example A captioned divider between stacked fields
|
|
1206
|
+
# poetry_field_separator { "Or continue with" }
|
|
1207
|
+
# @see Poetry::Ui::FieldSeparator::Component
|
|
1208
|
+
def poetry_field_separator(**, &)
|
|
1209
|
+
render(Poetry::Ui::FieldSeparator::Component.new(**), &)
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
# Void control (no content block) - the label is EXTERNAL (Label/Field
|
|
1213
|
+
# for= the button id) or label: (the aria-label fallback).
|
|
1214
|
+
#
|
|
1215
|
+
# @example A form checkbox
|
|
1216
|
+
# poetry_checkbox(name: "terms", label: "Accept terms")
|
|
1217
|
+
# @see Poetry::Ui::Checkbox::Component
|
|
1218
|
+
def poetry_checkbox(**)
|
|
1219
|
+
render(Poetry::Ui::Checkbox::Component.new(**))
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
# The select-all family (the APG mixed-state
|
|
1223
|
+
# parent): the wrapper carries the poetry--core--checkbox-group
|
|
1224
|
+
# controller; the parent checkbox fans out to every enabled item and
|
|
1225
|
+
# item toggles re-derive it (all -> checked, none -> unchecked,
|
|
1226
|
+
# some -> indeterminate).
|
|
1227
|
+
#
|
|
1228
|
+
# @example
|
|
1229
|
+
# poetry_checkbox_group do
|
|
1230
|
+
# poetry_checkbox_group_all(id: "all")
|
|
1231
|
+
# poetry_checkbox_group_item(name: "ids[]", value: "1", id: "row-1")
|
|
1232
|
+
# end
|
|
1233
|
+
# @see Poetry::Ui::Checkbox::Component
|
|
1234
|
+
def poetry_checkbox_group(**attrs, &block)
|
|
1235
|
+
# role=group makes the aria-labelledby/-describedby the FormBuilder
|
|
1236
|
+
# routes here real for AT - on a bare div they announce nothing.
|
|
1237
|
+
wiring = { role: "group", class: attrs.delete(:class),
|
|
1238
|
+
data: { slot: "checkbox-group", controller: "poetry--core--checkbox-group",
|
|
1239
|
+
action: "poetry:checkbox:change->poetry--core--checkbox-group#changed" } }
|
|
1240
|
+
content_tag(:div, (capture(&block) if block),
|
|
1241
|
+
Poetry::Core::HTML::Attributes.merged(wiring, attrs))
|
|
1242
|
+
end
|
|
1243
|
+
|
|
1244
|
+
# The group's parent checkbox (target: all) - checking it fans out.
|
|
1245
|
+
#
|
|
1246
|
+
# @example
|
|
1247
|
+
# poetry_checkbox_group_all(id: "select-all")
|
|
1248
|
+
# @see Poetry::Ui::Checkbox::Component
|
|
1249
|
+
def poetry_checkbox_group_all(**attrs)
|
|
1250
|
+
data = { "poetry--core--checkbox-group-target": "all" }.merge(attrs.delete(:data) || {})
|
|
1251
|
+
render(Poetry::Ui::Checkbox::Component.new(**attrs, data: data))
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
# One member checkbox (target: item) - toggles re-derive the parent.
|
|
1255
|
+
#
|
|
1256
|
+
# @example
|
|
1257
|
+
# poetry_checkbox_group_item(name: "ids[]", value: "1", checked: true)
|
|
1258
|
+
# @see Poetry::Ui::Checkbox::Component
|
|
1259
|
+
def poetry_checkbox_group_item(**attrs)
|
|
1260
|
+
data = { "poetry--core--checkbox-group-target": "item" }.merge(attrs.delete(:data) || {})
|
|
1261
|
+
render(Poetry::Ui::Checkbox::Component.new(**attrs, data: data))
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1264
|
+
# Read-only value + one copy affordance (API keys, install commands,
|
|
1265
|
+
# IDs); text_to_copy: overrides the clipboard when the display
|
|
1266
|
+
# truncates. Editable text is poetry_input; masked secrets are
|
|
1267
|
+
# poetry_sensitive_input.
|
|
1268
|
+
#
|
|
1269
|
+
# @example An install command with a copy button
|
|
1270
|
+
# poetry_clipboard_text(value: "gem install poetry-ui",
|
|
1271
|
+
# label: "Install command")
|
|
1272
|
+
# @see Poetry::Ui::ClipboardText::Component
|
|
1273
|
+
def poetry_clipboard_text(**)
|
|
1274
|
+
render(Poetry::Ui::ClipboardText::Component.new(**))
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
# Server-rendered highlighted code panel (rouge, soft dependency):
|
|
1278
|
+
# language:, CSS-counter line_numbers:, highlight_lines:, and a
|
|
1279
|
+
# copy affordance reading the rendered code. Inline code stays
|
|
1280
|
+
# plain <code> typography.
|
|
1281
|
+
#
|
|
1282
|
+
# @example
|
|
1283
|
+
# poetry_code_block(
|
|
1284
|
+
# code: "puts \"hello\"", language: "ruby"
|
|
1285
|
+
# )
|
|
1286
|
+
# @see Poetry::Ui::CodeBlock::Component
|
|
1287
|
+
def poetry_code_block(**)
|
|
1288
|
+
render(Poetry::Ui::CodeBlock::Component.new(**))
|
|
1289
|
+
end
|
|
1290
|
+
|
|
1291
|
+
# Secret shown-on-demand (API keys, tokens): masked container is the
|
|
1292
|
+
# reveal button, blur/Escape/eye re-mask, copy: copies without
|
|
1293
|
+
# revealing. Plain passwords being SET (not shown) can stay a
|
|
1294
|
+
# poetry_input type: :password.
|
|
1295
|
+
#
|
|
1296
|
+
# @example An API key with copy-without-reveal
|
|
1297
|
+
# poetry_sensitive_input(name: "api_key", label: "API key",
|
|
1298
|
+
# value: token, copy: true)
|
|
1299
|
+
# @see Poetry::Ui::SensitiveInput::Component
|
|
1300
|
+
def poetry_sensitive_input(**)
|
|
1301
|
+
render(Poetry::Ui::SensitiveInput::Component.new(**))
|
|
1302
|
+
end
|
|
1303
|
+
|
|
1304
|
+
# Void control - instant-effect on/off (role=switch announces on/off);
|
|
1305
|
+
# values staged for submit belong to poetry_checkbox.
|
|
1306
|
+
#
|
|
1307
|
+
# @example A named setting switch
|
|
1308
|
+
# poetry_switch(name: "notifications", checked: true,
|
|
1309
|
+
# label: "Email notifications")
|
|
1310
|
+
# @see Poetry::Ui::Switch::Component
|
|
1311
|
+
def poetry_switch(**)
|
|
1312
|
+
render(Poetry::Ui::Switch::Component.new(**))
|
|
1313
|
+
end
|
|
1314
|
+
|
|
1315
|
+
# One-question-at-a-time survey: a REAL form of fieldset items -
|
|
1316
|
+
# native radio/checkbox/text answers, validate-gated navigation.
|
|
1317
|
+
#
|
|
1318
|
+
# @example
|
|
1319
|
+
# poetry_questionnaire(url: "/surveys") do |survey|
|
|
1320
|
+
# survey.with_item(name: "mood", title: "How was your week?") do |item|
|
|
1321
|
+
# item.with_choice(value: "good", label: "Good")
|
|
1322
|
+
# item.with_choice(value: "bad", label: "Bad")
|
|
1323
|
+
# end
|
|
1324
|
+
# end
|
|
1325
|
+
# @see Poetry::Ui::Questionnaire::Component
|
|
1326
|
+
def poetry_questionnaire(**, &)
|
|
1327
|
+
render(Poetry::Ui::Questionnaire::Component.new(**), &)
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
# The exclusive-choice control: group.with_item(value:, label:) -
|
|
1331
|
+
# one hidden native radio per item (collection_radio_buttons-exact
|
|
1332
|
+
# serialization). The group MUST be labelled (label: or
|
|
1333
|
+
# aria-labelledby).
|
|
1334
|
+
#
|
|
1335
|
+
# @example
|
|
1336
|
+
# poetry_radio_group(
|
|
1337
|
+
# name: "plan", value: "monthly", label: "Billing plan"
|
|
1338
|
+
# ) do |group|
|
|
1339
|
+
# group.with_item(value: "monthly", label: "Monthly")
|
|
1340
|
+
# group.with_item(value: "yearly", label: "Yearly")
|
|
1341
|
+
# end
|
|
1342
|
+
# @see Poetry::Ui::RadioGroup::Component
|
|
1343
|
+
def poetry_radio_group(**, &)
|
|
1344
|
+
render(Poetry::Ui::RadioGroup::Component.new(**), &)
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
# Void control - numeric value (value:) or [low, high] range
|
|
1348
|
+
# (values:) on a continuous track; every thumb needs a distinct
|
|
1349
|
+
# accessible name (label:).
|
|
1350
|
+
#
|
|
1351
|
+
# @example
|
|
1352
|
+
# poetry_slider(name: "volume", value: 50, label: "Volume")
|
|
1353
|
+
# @see Poetry::Ui::Slider::Component
|
|
1354
|
+
def poetry_slider(**)
|
|
1355
|
+
render(Poetry::Ui::Slider::Component.new(**))
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
# A chat message bubble aligned to its sender.
|
|
1359
|
+
#
|
|
1360
|
+
# @example An assistant reply
|
|
1361
|
+
# poetry_bubble(variant: :secondary) { "Here's the summary." }
|
|
1362
|
+
# @see Poetry::Ui::Bubble::Component
|
|
1363
|
+
def poetry_bubble(**, &)
|
|
1364
|
+
render(Poetry::Ui::Bubble::Component.new(**), &)
|
|
1365
|
+
end
|
|
1366
|
+
|
|
1367
|
+
# A styled wrapper stacking one sender's consecutive bubbles - a
|
|
1368
|
+
# dictionary element, not a component (a Bubble concern).
|
|
1369
|
+
#
|
|
1370
|
+
# @example
|
|
1371
|
+
# <%= poetry_bubble_group do %>
|
|
1372
|
+
# <%= poetry_bubble(variant: :sent) { "Hi!" } %>
|
|
1373
|
+
# <%= poetry_bubble(variant: :sent) { "You there?" } %>
|
|
1374
|
+
# <% end %>
|
|
1375
|
+
# @see Poetry::Ui::Bubble::Component
|
|
1376
|
+
def poetry_bubble_group(**attrs, &)
|
|
1377
|
+
poetry_chat_group(Poetry::Ui::Bubble::Style, "bubble-group", **attrs, &)
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
# Pressed-state button (aria-pressed) - UI state, NOT form data; the
|
|
1381
|
+
# content block is the icon/text (icon-only requires label:).
|
|
1382
|
+
#
|
|
1383
|
+
# @example An icon-only bookmark toggle
|
|
1384
|
+
# poetry_toggle(label: "Bookmark", pressed: bookmarked?) do
|
|
1385
|
+
# poetry_icon(name: :bookmark)
|
|
1386
|
+
# end
|
|
1387
|
+
# @see Poetry::Ui::Toggle::Component
|
|
1388
|
+
def poetry_toggle(**, &)
|
|
1389
|
+
render(Poetry::Ui::Toggle::Component.new(**), &)
|
|
1390
|
+
end
|
|
1391
|
+
|
|
1392
|
+
# A set of Toggle-styled items under one value machine + one roving
|
|
1393
|
+
# tab stop: group.with_item(value:, label:) { icon/text }.
|
|
1394
|
+
#
|
|
1395
|
+
# @example A text-alignment switcher
|
|
1396
|
+
# poetry_toggle_group(value: "left", label: "Text alignment") do |group|
|
|
1397
|
+
# group.with_item(value: "left", label: "Align left") { icon(:"align-left") }
|
|
1398
|
+
# group.with_item(value: "center", label: "Align center") { icon(:"align-center") }
|
|
1399
|
+
# end
|
|
1400
|
+
# @see Poetry::Ui::ToggleGroup::Component
|
|
1401
|
+
def poetry_toggle_group(**, &)
|
|
1402
|
+
render(Poetry::Ui::ToggleGroup::Component.new(**), &)
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
# A chat row pairing an author and avatar with message content.
|
|
1406
|
+
#
|
|
1407
|
+
# @example
|
|
1408
|
+
# poetry_message do |message|
|
|
1409
|
+
# message.with_avatar { "AI" }
|
|
1410
|
+
# message.with_header { "Assistant" }
|
|
1411
|
+
# tag.div("Here's the plan for today.")
|
|
1412
|
+
# end
|
|
1413
|
+
# @see Poetry::Ui::Message::Component
|
|
1414
|
+
def poetry_message(**, &)
|
|
1415
|
+
render(Poetry::Ui::Message::Component.new(**), &)
|
|
1416
|
+
end
|
|
1417
|
+
|
|
1418
|
+
# A styled wrapper stacking one sender's consecutive messages - a
|
|
1419
|
+
# dictionary element, not a component (a Message concern).
|
|
1420
|
+
#
|
|
1421
|
+
# @example
|
|
1422
|
+
# <%= poetry_message_group do %>
|
|
1423
|
+
# <%# consecutive poetry_message rows from one sender %>
|
|
1424
|
+
# <% end %>
|
|
1425
|
+
# @see Poetry::Ui::Message::Component
|
|
1426
|
+
def poetry_message_group(**attrs, &)
|
|
1427
|
+
poetry_chat_group(Poetry::Ui::Message::Style, "message-group", **attrs, &)
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
# A transcript divider or inline status marker for chat UIs.
|
|
1431
|
+
#
|
|
1432
|
+
# @example
|
|
1433
|
+
# poetry_marker(variant: :separator) { "Yesterday" }
|
|
1434
|
+
# @see Poetry::Ui::Marker::Component
|
|
1435
|
+
def poetry_marker(**, &)
|
|
1436
|
+
render(Poetry::Ui::Marker::Component.new(**), &)
|
|
1437
|
+
end
|
|
1438
|
+
|
|
1439
|
+
# A file or image chip showing its name, type, and size.
|
|
1440
|
+
#
|
|
1441
|
+
# @example A finished upload
|
|
1442
|
+
# poetry_attachment do |attachment|
|
|
1443
|
+
# attachment.with_title { "quarterly-report.pdf" }
|
|
1444
|
+
# attachment.with_description { "1.2 MB" }
|
|
1445
|
+
# end
|
|
1446
|
+
# @see Poetry::Ui::Attachment::Component
|
|
1447
|
+
def poetry_attachment(**, &)
|
|
1448
|
+
render(Poetry::Ui::Attachment::Component.new(**), &)
|
|
1449
|
+
end
|
|
1450
|
+
|
|
1451
|
+
# The horizontally-scrolling attachment rail (scroll-fade + snap).
|
|
1452
|
+
#
|
|
1453
|
+
# @example
|
|
1454
|
+
# <%= poetry_attachment_group do %>
|
|
1455
|
+
# <%# poetry_attachment chips %>
|
|
1456
|
+
# <% end %>
|
|
1457
|
+
# @see Poetry::Ui::Attachment::Component
|
|
1458
|
+
def poetry_attachment_group(**attrs, &)
|
|
1459
|
+
poetry_chat_group(Poetry::Ui::Attachment::Style, "attachment-group", **attrs, &)
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1462
|
+
# A streaming-aware transcript that keeps the latest message in view.
|
|
1463
|
+
#
|
|
1464
|
+
# @example A chat transcript
|
|
1465
|
+
# poetry_message_scroller(id: "chat") do
|
|
1466
|
+
# # poetry_message_scroller_item rows
|
|
1467
|
+
# end
|
|
1468
|
+
# @see Poetry::Ui::MessageScroller::Component
|
|
1469
|
+
def poetry_message_scroller(**, &)
|
|
1470
|
+
render(Poetry::Ui::MessageScroller::Component.new(**), &)
|
|
1471
|
+
end
|
|
1472
|
+
|
|
1473
|
+
# One transcript row - the id is how anchoring and Turbo Streams
|
|
1474
|
+
# find it (data-message-id; anchor: pins the reading position).
|
|
1475
|
+
#
|
|
1476
|
+
# @example
|
|
1477
|
+
# <%= poetry_message_scroller_item(id: message.id) do %>
|
|
1478
|
+
# <%= poetry_message(author: "Ada") { message.body } %>
|
|
1479
|
+
# <% end %>
|
|
1480
|
+
# @see Poetry::Ui::MessageScroller::Component
|
|
1481
|
+
def poetry_message_scroller_item(id:, anchor: false, **attrs, &)
|
|
1482
|
+
classes = [Poetry::Ui::MessageScroller::Style.css(:item), attrs.delete(:class)].compact.join(" ")
|
|
1483
|
+
# data-message-id is the anchoring/Turbo-Stream identity - RESERVED:
|
|
1484
|
+
# the id: argument wins over any caller spelling. Other caller data
|
|
1485
|
+
# keys (and stimulus wiring) ride along.
|
|
1486
|
+
attrs.delete("data-message-id")
|
|
1487
|
+
attrs.delete(:"data-message-id")
|
|
1488
|
+
data = (attrs.delete(:data) || {}).merge(slot: "message-scroller-item", "message-id": id)
|
|
1489
|
+
data[:"scroll-anchor"] = "true" if anchor
|
|
1490
|
+
tag.div(**Poetry::Core::HTML::Attributes.merged({ class: classes, data: data }, attrs)
|
|
1491
|
+
.symbolize_keys, &)
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
# A brief, auto-dismissing notification message.
|
|
1495
|
+
#
|
|
1496
|
+
# @example An undo toast (persistent because it carries an action)
|
|
1497
|
+
# poetry_toast(variant: :success) do |toast|
|
|
1498
|
+
# toast.with_title { "Message archived" }
|
|
1499
|
+
# toast.with_action { "Undo" }
|
|
1500
|
+
# end
|
|
1501
|
+
# @see Poetry::Ui::Toast::Component
|
|
1502
|
+
def poetry_toast(**, &)
|
|
1503
|
+
render(Poetry::Ui::Toast::Component.new(**), &)
|
|
1504
|
+
end
|
|
1505
|
+
|
|
1506
|
+
# The toast viewport - render ONCE in the application layout (it is
|
|
1507
|
+
# data-turbo-permanent; turbo_stream.poetry_toast appends into it).
|
|
1508
|
+
#
|
|
1509
|
+
# @example The flash -> toast mapping in the layout
|
|
1510
|
+
# <%= poetry_toaster do %>
|
|
1511
|
+
# <% flash.each do |kind, message| %>
|
|
1512
|
+
# <%= poetry_toast(variant: kind.to_s == "alert" ? :destructive : :default) do |toast| %>
|
|
1513
|
+
# <% toast.with_title { message } %>
|
|
1514
|
+
# <% end %>
|
|
1515
|
+
# <% end %>
|
|
1516
|
+
# <% end %>
|
|
1517
|
+
# @see Poetry::Ui::Toaster::Component
|
|
1518
|
+
def poetry_toaster(**, &)
|
|
1519
|
+
render(Poetry::Ui::Toaster::Component.new(**), &)
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
# An interactive element that expands and collapses a section of
|
|
1523
|
+
# content.
|
|
1524
|
+
#
|
|
1525
|
+
# @example
|
|
1526
|
+
# poetry_collapsible do |collapsible|
|
|
1527
|
+
# collapsible.with_trigger { "Show details" }
|
|
1528
|
+
# tag.div("Hidden until disclosed.")
|
|
1529
|
+
# end
|
|
1530
|
+
# @see Poetry::Ui::Collapsible::Component
|
|
1531
|
+
def poetry_collapsible(**, &)
|
|
1532
|
+
render(Poetry::Ui::Collapsible::Component.new(**), &)
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
# A vertically stacked set of interactive headings that each reveal a
|
|
1536
|
+
# section of content.
|
|
1537
|
+
#
|
|
1538
|
+
# @example Single-open accordion with the first item expanded
|
|
1539
|
+
# poetry_accordion(open: %w[a]) do |accordion|
|
|
1540
|
+
# accordion.with_item(value: "a", title: "First") { "First panel" }
|
|
1541
|
+
# accordion.with_item(value: "b", title: "Second") { "Second panel" }
|
|
1542
|
+
# end
|
|
1543
|
+
# @see Poetry::Ui::Accordion::Component
|
|
1544
|
+
def poetry_accordion(**, &)
|
|
1545
|
+
render(Poetry::Ui::Accordion::Component.new(**), &)
|
|
1546
|
+
end
|
|
1547
|
+
|
|
1548
|
+
# Rich floating content anchored to a trigger.
|
|
1549
|
+
#
|
|
1550
|
+
# @example
|
|
1551
|
+
# poetry_popover do |popover|
|
|
1552
|
+
# popover.with_trigger(variant: :outline) { "Open popover" }
|
|
1553
|
+
# popover.with_title { "Dimensions" }
|
|
1554
|
+
# tag.p("Set the dimensions for the layer.")
|
|
1555
|
+
# end
|
|
1556
|
+
# @see Poetry::Ui::Popover::Component
|
|
1557
|
+
def poetry_popover(**, &)
|
|
1558
|
+
render(Poetry::Ui::Popover::Component.new(**), &)
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
# A floating label describing an element on hover or focus.
|
|
1562
|
+
#
|
|
1563
|
+
# @example A described icon button
|
|
1564
|
+
# poetry_tooltip do |tooltip|
|
|
1565
|
+
# tooltip.with_trigger(variant: :outline, size: :icon, label: "Print") do
|
|
1566
|
+
# poetry_icon(name: :printer)
|
|
1567
|
+
# end
|
|
1568
|
+
# "Print the current page"
|
|
1569
|
+
# end
|
|
1570
|
+
# @see Poetry::Ui::Tooltip::Component
|
|
1571
|
+
def poetry_tooltip(**, &)
|
|
1572
|
+
render(Poetry::Ui::Tooltip::Component.new(**), &)
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
# The tooltip delay/warm SCOPE - a config-carrying div, NOT a
|
|
1576
|
+
# controller (the DOM ancestor IS the shared delay scope; the tooltip
|
|
1577
|
+
# controller reads closest('[data-slot=tooltip-provider]') and keys
|
|
1578
|
+
# the module-level warm registry by it). Wrap control rows in ONE
|
|
1579
|
+
# provider so the warm grace makes the row feel continuous.
|
|
1580
|
+
#
|
|
1581
|
+
# @example
|
|
1582
|
+
# <%= poetry_tooltip_provider(delay_duration: 300) do %>
|
|
1583
|
+
# <%# tooltips inside share the delay + warm state %>
|
|
1584
|
+
# <% end %>
|
|
1585
|
+
# @see Poetry::Ui::Tooltip::Component
|
|
1586
|
+
def poetry_tooltip_provider(delay_duration: 0, skip_delay_duration: 300,
|
|
1587
|
+
disable_hoverable_content: false, **attrs, &)
|
|
1588
|
+
data = (attrs.delete(:data) || {}).merge(
|
|
1589
|
+
slot: "tooltip-provider",
|
|
1590
|
+
delay_duration: delay_duration,
|
|
1591
|
+
skip_delay_duration: skip_delay_duration,
|
|
1592
|
+
disable_hoverable_content: disable_hoverable_content
|
|
1593
|
+
)
|
|
1594
|
+
tag.div(**attrs, data: data, &)
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
# A card that reveals preview content when its trigger is hovered.
|
|
1598
|
+
#
|
|
1599
|
+
# @example A profile preview behind a real link
|
|
1600
|
+
# poetry_hover_card do |card|
|
|
1601
|
+
# card.with_trigger(href: "/users/nextjs") { "@nextjs" }
|
|
1602
|
+
# "Joined December 2021."
|
|
1603
|
+
# end
|
|
1604
|
+
# @see Poetry::Ui::HoverCard::Component
|
|
1605
|
+
def poetry_hover_card(**, &)
|
|
1606
|
+
render(Poetry::Ui::HoverCard::Component.new(**), &)
|
|
1607
|
+
end
|
|
1608
|
+
|
|
1609
|
+
# A menu of actions or options triggered by a button.
|
|
1610
|
+
#
|
|
1611
|
+
# @example A menu button with actions
|
|
1612
|
+
# poetry_dropdown_menu do |menu|
|
|
1613
|
+
# menu.with_trigger(variant: :outline) { "Open" }
|
|
1614
|
+
# menu.with_item { "Rename" }
|
|
1615
|
+
# menu.with_item(variant: :destructive) { "Delete" }
|
|
1616
|
+
# end
|
|
1617
|
+
# @see Poetry::Ui::DropdownMenu::Component
|
|
1618
|
+
def poetry_dropdown_menu(**, &)
|
|
1619
|
+
render(Poetry::Ui::DropdownMenu::Component.new(**), &)
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1622
|
+
# The value-choosing listbox (options ARE values; actions belong to
|
|
1623
|
+
# poetry_dropdown_menu). Must be named: a Field label (id: +
|
|
1624
|
+
# label[for]) or aria-label. In forms, prefer f.poetry_select.
|
|
1625
|
+
#
|
|
1626
|
+
# @example A named form select
|
|
1627
|
+
# poetry_select(name: "fruit", id: "fruit",
|
|
1628
|
+
# placeholder: "Pick a fruit") do |select|
|
|
1629
|
+
# select.with_item(value: "apple") { "Apple" }
|
|
1630
|
+
# select.with_item(value: "banana") { "Banana" }
|
|
1631
|
+
# end
|
|
1632
|
+
# @see Poetry::Ui::Select::Component
|
|
1633
|
+
def poetry_select(**, &)
|
|
1634
|
+
render(Poetry::Ui::Select::Component.new(**), &)
|
|
1635
|
+
end
|
|
1636
|
+
|
|
1637
|
+
# The type-to-filter value picker (Select's shell x Command's
|
|
1638
|
+
# engine): options ARE values, committed to a hidden native select.
|
|
1639
|
+
# Must be named (Field label via id: or aria-label). In forms,
|
|
1640
|
+
# prefer f.poetry_combobox.
|
|
1641
|
+
#
|
|
1642
|
+
# @example
|
|
1643
|
+
# poetry_combobox(name: "framework", "aria-label" => "Framework") do |combobox|
|
|
1644
|
+
# combobox.with_item(value: "rails") { "Ruby on Rails" }
|
|
1645
|
+
# combobox.with_item(value: "hanami") { "Hanami" }
|
|
1646
|
+
# end
|
|
1647
|
+
# @see Poetry::Ui::Combobox::Component
|
|
1648
|
+
def poetry_combobox(**, &)
|
|
1649
|
+
render(Poetry::Ui::Combobox::Component.new(**), &)
|
|
1650
|
+
end
|
|
1651
|
+
|
|
1652
|
+
# A menu of actions revealed by right-clicking an element.
|
|
1653
|
+
#
|
|
1654
|
+
# @example Right-click surface with actions
|
|
1655
|
+
# poetry_context_menu do |menu|
|
|
1656
|
+
# menu.with_trigger(tag: :div) { "Right-click this card" }
|
|
1657
|
+
# menu.with_item { "Rename" }
|
|
1658
|
+
# menu.with_item(variant: :destructive) { "Delete" }
|
|
1659
|
+
# end
|
|
1660
|
+
# @see Poetry::Ui::ContextMenu::Component
|
|
1661
|
+
def poetry_context_menu(**, &)
|
|
1662
|
+
render(Poetry::Ui::ContextMenu::Component.new(**), &)
|
|
1663
|
+
end
|
|
1664
|
+
|
|
1665
|
+
# The filterable command-palette listbox: items DO
|
|
1666
|
+
# things - picking a VALUE for a form is Combobox territory.
|
|
1667
|
+
#
|
|
1668
|
+
# @example
|
|
1669
|
+
# poetry_command("aria-label": "Command menu") do |command|
|
|
1670
|
+
# command.with_item(value: "new-file") { "New file" }
|
|
1671
|
+
# command.with_item(value: "search") { "Search" }
|
|
1672
|
+
# end
|
|
1673
|
+
# @see Poetry::Ui::Command::Component
|
|
1674
|
+
def poetry_command(**, &)
|
|
1675
|
+
render(Poetry::Ui::Command::Component.new(**), &)
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
# The ⌘K variant: a Command inside the Dialog chrome (sr-only
|
|
1679
|
+
# title/description) with the OPT-IN hotkey: global shortcut.
|
|
1680
|
+
#
|
|
1681
|
+
# @example
|
|
1682
|
+
# poetry_command_dialog(hotkey: "meta+k") do |dialog|
|
|
1683
|
+
# dialog.with_trigger(variant: :outline) { "Open palette" }
|
|
1684
|
+
# dialog.with_item(value: "settings") { "Settings" }
|
|
1685
|
+
# end
|
|
1686
|
+
# @see Poetry::Ui::Command::DialogComponent
|
|
1687
|
+
def poetry_command_dialog(**, &)
|
|
1688
|
+
render(Poetry::Ui::Command::DialogComponent.new(**), &)
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
# A horizontal bar of menus, like a desktop application menu.
|
|
1692
|
+
#
|
|
1693
|
+
# @example An application command bar
|
|
1694
|
+
# poetry_menubar(label: "Application") do |bar|
|
|
1695
|
+
# bar.with_menu do |menu|
|
|
1696
|
+
# menu.with_trigger { "File" }
|
|
1697
|
+
# menu.with_item(shortcut: "⌘N") { "New" }
|
|
1698
|
+
# menu.with_separator
|
|
1699
|
+
# menu.with_item { "Print..." }
|
|
1700
|
+
# end
|
|
1701
|
+
# end
|
|
1702
|
+
# @see Poetry::Ui::Menubar::Component
|
|
1703
|
+
def poetry_menubar(**, &)
|
|
1704
|
+
render(Poetry::Ui::Menubar::Component.new(**), &)
|
|
1705
|
+
end
|
|
1706
|
+
|
|
1707
|
+
# The composed-DOM duplicate-id tripwire: a DEVELOPMENT
|
|
1708
|
+
# guard that scans the live page for duplicate [id] values after
|
|
1709
|
+
# every composition event (load, Turbo loads, frame loads, morphs,
|
|
1710
|
+
# stream insertions) and console-warns - the runtime complement to
|
|
1711
|
+
# poetry check's static stable-identity heuristics, and the only
|
|
1712
|
+
# check that sees real composition. Render in the development
|
|
1713
|
+
# layout's <head>; it emits nothing outside development unless
|
|
1714
|
+
# force: true (the dummy's test pages dogfood it that way).
|
|
1715
|
+
#
|
|
1716
|
+
# @example
|
|
1717
|
+
# <%# in the development layout's <head> %>
|
|
1718
|
+
# <%= poetry_id_integrity_script %>
|
|
1719
|
+
def poetry_id_integrity_script(force: false)
|
|
1720
|
+
return unless force || Rails.env.development?
|
|
1721
|
+
|
|
1722
|
+
javascript_tag(<<~JS, type: "module", nonce: true)
|
|
1723
|
+
import { installPoetryIdIntegrityCheck } from "@poetry/controllers/helpers/id_integrity"
|
|
1724
|
+
installPoetryIdIntegrityCheck()
|
|
1725
|
+
JS
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
# The color-scheme bootstrap (the pothole other ports patch
|
|
1729
|
+
# into every host by hand): tokens ship `.dark` + `color-scheme`, but
|
|
1730
|
+
# WHEN `.dark` applies is the host's job - and it must happen before
|
|
1731
|
+
# first paint or every visit flashes light. Render inside <head>,
|
|
1732
|
+
# before the stylesheets. Also wires window.Poetry.colorScheme
|
|
1733
|
+
# (current/set/toggle/clear) for toggle controls; an unset preference
|
|
1734
|
+
# follows the OS and tracks its changes live. Full recipe:
|
|
1735
|
+
# the Theming guide, "Color scheme (dark mode)".
|
|
1736
|
+
#
|
|
1737
|
+
# @example
|
|
1738
|
+
# <%# in the layout <head>, before the stylesheets %>
|
|
1739
|
+
# <%= poetry_color_scheme_script %>
|
|
1740
|
+
def poetry_color_scheme_script
|
|
1741
|
+
javascript_tag(COLOR_SCHEME_JS, nonce: true)
|
|
1742
|
+
end
|
|
1743
|
+
|
|
1744
|
+
# The inline bootstrap poetry_color_scheme_script emits: applies the
|
|
1745
|
+
# stored (or OS) scheme before first paint and wires
|
|
1746
|
+
# window.Poetry.colorScheme.
|
|
1747
|
+
COLOR_SCHEME_JS = <<~JS
|
|
1748
|
+
(() => {
|
|
1749
|
+
const KEY = "poetry-color-scheme";
|
|
1750
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
1751
|
+
const stored = () => { try { return localStorage.getItem(KEY); } catch { return null; } };
|
|
1752
|
+
const current = () => stored() || (media.matches ? "dark" : "light");
|
|
1753
|
+
const apply = () => {
|
|
1754
|
+
const mode = current();
|
|
1755
|
+
document.documentElement.classList.toggle("dark", mode === "dark");
|
|
1756
|
+
document.documentElement.dispatchEvent(
|
|
1757
|
+
new CustomEvent("poetry:color-scheme", { bubbles: true, detail: { mode } })
|
|
1758
|
+
);
|
|
1759
|
+
};
|
|
1760
|
+
apply();
|
|
1761
|
+
media.addEventListener("change", () => { if (!stored()) apply(); });
|
|
1762
|
+
window.Poetry = window.Poetry || {};
|
|
1763
|
+
window.Poetry.colorScheme = {
|
|
1764
|
+
current,
|
|
1765
|
+
set(mode) {
|
|
1766
|
+
try { mode ? localStorage.setItem(KEY, mode) : localStorage.removeItem(KEY); } catch {}
|
|
1767
|
+
apply();
|
|
1768
|
+
},
|
|
1769
|
+
toggle() { this.set(current() === "dark" ? "light" : "dark"); },
|
|
1770
|
+
clear() { this.set(null); }
|
|
1771
|
+
};
|
|
1772
|
+
})();
|
|
1773
|
+
JS
|
|
1774
|
+
|
|
1775
|
+
private
|
|
1776
|
+
|
|
1777
|
+
# Auto-inject only when the caller supplied the pair AND didn't place
|
|
1778
|
+
# the field explicitly inside the block (the block is captured first,
|
|
1779
|
+
# so an explicit optimistic_hidden_field call wins).
|
|
1780
|
+
def optimistic_auto_inject?(form, attribute_name, value)
|
|
1781
|
+
attribute_name.present? &&
|
|
1782
|
+
!value.nil? &&
|
|
1783
|
+
!OptimisticFormBuilder::UNSET.equal?(value) &&
|
|
1784
|
+
!form.optimistic_hidden_field_rendered?
|
|
1785
|
+
end
|
|
1786
|
+
|
|
1787
|
+
# The chat-set group wrappers are dictionary ELEMENTS, not components.
|
|
1788
|
+
def poetry_chat_group(style, slot, **attrs, &)
|
|
1789
|
+
classes = [style.css(slot.tr("-", "_").split("_").last.to_sym), attrs.delete(:class)].compact.join(" ")
|
|
1790
|
+
tag.div(**attrs.merge(class: classes, "data-slot" => slot), &)
|
|
1791
|
+
end
|
|
1792
|
+
end
|
|
1793
|
+
end
|
|
1794
|
+
end
|