shadcn_view_component 0.3.3
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/CHANGELOG.md +255 -0
- data/MIT-LICENSE +20 -0
- data/README.md +604 -0
- data/app/assets/stylesheets/shadcn-themes.css +1075 -0
- data/app/assets/stylesheets/shadcn.css +853 -0
- data/app/components/shadcn/accordion/component.rb +44 -0
- data/app/components/shadcn/accordion/content/component.rb +44 -0
- data/app/components/shadcn/accordion/item/component.rb +41 -0
- data/app/components/shadcn/accordion/preview.rb +16 -0
- data/app/components/shadcn/accordion/previews/default.html.erb +10 -0
- data/app/components/shadcn/accordion/previews/multiple.html.erb +13 -0
- data/app/components/shadcn/accordion/trigger/component.rb +52 -0
- data/app/components/shadcn/alert/component.rb +52 -0
- data/app/components/shadcn/alert/preview.rb +11 -0
- data/app/components/shadcn/alert/previews/default.html.erb +8 -0
- data/app/components/shadcn/alert.rb +17 -0
- data/app/components/shadcn/alert_dialog/action/component.rb +35 -0
- data/app/components/shadcn/alert_dialog/cancel/component.rb +16 -0
- data/app/components/shadcn/alert_dialog/component.rb +20 -0
- data/app/components/shadcn/alert_dialog/content/component.rb +55 -0
- data/app/components/shadcn/alert_dialog/header/component.rb +33 -0
- data/app/components/shadcn/alert_dialog/preview.rb +16 -0
- data/app/components/shadcn/alert_dialog/previews/default.html.erb +13 -0
- data/app/components/shadcn/alert_dialog/previews/small_with_media.html.erb +38 -0
- data/app/components/shadcn/alert_dialog.rb +32 -0
- data/app/components/shadcn/application_view_component.rb +225 -0
- data/app/components/shadcn/application_view_component_preview.rb +8 -0
- data/app/components/shadcn/aspect_ratio/component.rb +34 -0
- data/app/components/shadcn/aspect_ratio/preview.rb +16 -0
- data/app/components/shadcn/aspect_ratio/previews/default.html.erb +5 -0
- data/app/components/shadcn/aspect_ratio/previews/ratios.html.erb +12 -0
- data/app/components/shadcn/attachment/action/component.rb +19 -0
- data/app/components/shadcn/attachment/component.rb +76 -0
- data/app/components/shadcn/attachment/media/component.rb +56 -0
- data/app/components/shadcn/attachment/preview.rb +11 -0
- data/app/components/shadcn/attachment/previews/default.html.erb +80 -0
- data/app/components/shadcn/attachment/trigger/component.rb +31 -0
- data/app/components/shadcn/attachment.rb +42 -0
- data/app/components/shadcn/avatar/component.rb +43 -0
- data/app/components/shadcn/avatar/fallback/component.rb +23 -0
- data/app/components/shadcn/avatar/image/component.rb +20 -0
- data/app/components/shadcn/avatar/preview.rb +11 -0
- data/app/components/shadcn/avatar/previews/default.html.erb +13 -0
- data/app/components/shadcn/avatar.rb +32 -0
- data/app/components/shadcn/badge/component.rb +57 -0
- data/app/components/shadcn/badge/preview.rb +11 -0
- data/app/components/shadcn/badge/previews/default.html.erb +5 -0
- data/app/components/shadcn/breadcrumb/component.rb +15 -0
- data/app/components/shadcn/breadcrumb/ellipsis/component.rb +28 -0
- data/app/components/shadcn/breadcrumb/page/component.rb +25 -0
- data/app/components/shadcn/breadcrumb/preview.rb +16 -0
- data/app/components/shadcn/breadcrumb/previews/custom_separator.html.erb +17 -0
- data/app/components/shadcn/breadcrumb/previews/default.html.erb +15 -0
- data/app/components/shadcn/breadcrumb/separator/component.rb +31 -0
- data/app/components/shadcn/breadcrumb.rb +18 -0
- data/app/components/shadcn/bubble/component.rb +90 -0
- data/app/components/shadcn/bubble/preview.rb +16 -0
- data/app/components/shadcn/bubble/previews/alignment.html.erb +33 -0
- data/app/components/shadcn/bubble/previews/default.html.erb +22 -0
- data/app/components/shadcn/bubble/reactions/component.rb +54 -0
- data/app/components/shadcn/bubble.rb +24 -0
- data/app/components/shadcn/button/component.rb +85 -0
- data/app/components/shadcn/button/preview.rb +31 -0
- data/app/components/shadcn/button/previews/default.html.erb +8 -0
- data/app/components/shadcn/button/previews/sizes.html.erb +5 -0
- data/app/components/shadcn/button/previews/variants.html.erb +5 -0
- data/app/components/shadcn/button_group/component.rb +53 -0
- data/app/components/shadcn/button_group/preview.rb +15 -0
- data/app/components/shadcn/button_group/previews/default.html.erb +18 -0
- data/app/components/shadcn/button_group/previews/vertical.html.erb +5 -0
- data/app/components/shadcn/button_group/separator/component.rb +30 -0
- data/app/components/shadcn/button_group/text/component.rb +20 -0
- data/app/components/shadcn/calendar/component.rb +558 -0
- data/app/components/shadcn/calendar/month.rb +74 -0
- data/app/components/shadcn/calendar/preview.rb +64 -0
- data/app/components/shadcn/calendar/previews/booked.html.erb +18 -0
- data/app/components/shadcn/calendar/previews/custom_cell_size.html.erb +11 -0
- data/app/components/shadcn/calendar/previews/date_and_time.html.erb +31 -0
- data/app/components/shadcn/calendar/previews/default.html.erb +8 -0
- data/app/components/shadcn/calendar/previews/dropdown_caption.html.erb +11 -0
- data/app/components/shadcn/calendar/previews/in_a_form.html.erb +8 -0
- data/app/components/shadcn/calendar/previews/presets.html.erb +20 -0
- data/app/components/shadcn/calendar/previews/range.html.erb +10 -0
- data/app/components/shadcn/calendar/previews/week_numbers.html.erb +8 -0
- data/app/components/shadcn/card/component.rb +24 -0
- data/app/components/shadcn/card/header/component.rb +26 -0
- data/app/components/shadcn/card/preview.rb +16 -0
- data/app/components/shadcn/card/previews/default.html.erb +18 -0
- data/app/components/shadcn/card/previews/image.html.erb +37 -0
- data/app/components/shadcn/card.rb +21 -0
- data/app/components/shadcn/carousel/component.rb +61 -0
- data/app/components/shadcn/carousel/content/component.rb +86 -0
- data/app/components/shadcn/carousel/item/component.rb +42 -0
- data/app/components/shadcn/carousel/next/component.rb +59 -0
- data/app/components/shadcn/carousel/preview.rb +22 -0
- data/app/components/shadcn/carousel/previews/default.html.erb +22 -0
- data/app/components/shadcn/carousel/previews/sizes.html.erb +42 -0
- data/app/components/shadcn/carousel/previews/vertical.html.erb +27 -0
- data/app/components/shadcn/carousel/previous/component.rb +59 -0
- data/app/components/shadcn/chart/area/component.rb +43 -0
- data/app/components/shadcn/chart/bar/component.rb +75 -0
- data/app/components/shadcn/chart/cartesian/component.rb +152 -0
- data/app/components/shadcn/chart/component.rb +124 -0
- data/app/components/shadcn/chart/focusable.rb +38 -0
- data/app/components/shadcn/chart/legend/component.rb +55 -0
- data/app/components/shadcn/chart/line/component.rb +50 -0
- data/app/components/shadcn/chart/pie/component.rb +169 -0
- data/app/components/shadcn/chart/plot.rb +147 -0
- data/app/components/shadcn/chart/preview.rb +45 -0
- data/app/components/shadcn/chart/previews/area.html.erb +31 -0
- data/app/components/shadcn/chart/previews/bar.html.erb +32 -0
- data/app/components/shadcn/chart/previews/bar_stacked.html.erb +30 -0
- data/app/components/shadcn/chart/previews/default.html.erb +30 -0
- data/app/components/shadcn/chart/previews/donut.html.erb +16 -0
- data/app/components/shadcn/chart/previews/line.html.erb +30 -0
- data/app/components/shadcn/chart/previews/percentages.html.erb +17 -0
- data/app/components/shadcn/chart/table/component.rb +75 -0
- data/app/components/shadcn/chart/tooltip/component.rb +111 -0
- data/app/components/shadcn/checkbox/component.rb +109 -0
- data/app/components/shadcn/checkbox/preview.rb +16 -0
- data/app/components/shadcn/checkbox/previews/default.html.erb +14 -0
- data/app/components/shadcn/checkbox/previews/states.html.erb +23 -0
- data/app/components/shadcn/collapsible/component.rb +35 -0
- data/app/components/shadcn/collapsible/content/component.rb +20 -0
- data/app/components/shadcn/collapsible/preview.rb +11 -0
- data/app/components/shadcn/collapsible/previews/default.html.erb +9 -0
- data/app/components/shadcn/collapsible/trigger/component.rb +23 -0
- data/app/components/shadcn/combobox/chip/component.rb +60 -0
- data/app/components/shadcn/combobox/chip_input/component.rb +43 -0
- data/app/components/shadcn/combobox/chips/component.rb +54 -0
- data/app/components/shadcn/combobox/clear/component.rb +32 -0
- data/app/components/shadcn/combobox/component.rb +113 -0
- data/app/components/shadcn/combobox/content/component.rb +59 -0
- data/app/components/shadcn/combobox/empty/component.rb +23 -0
- data/app/components/shadcn/combobox/input/component.rb +85 -0
- data/app/components/shadcn/combobox/item/component.rb +69 -0
- data/app/components/shadcn/combobox/list/component.rb +27 -0
- data/app/components/shadcn/combobox/preview.rb +31 -0
- data/app/components/shadcn/combobox/previews/default.html.erb +20 -0
- data/app/components/shadcn/combobox/previews/grouped.html.erb +25 -0
- data/app/components/shadcn/combobox/previews/multiple.html.erb +28 -0
- data/app/components/shadcn/combobox/previews/with_clear.html.erb +16 -0
- data/app/components/shadcn/combobox/trigger/component.rb +54 -0
- data/app/components/shadcn/combobox.rb +16 -0
- data/app/components/shadcn/command/component.rb +67 -0
- data/app/components/shadcn/command/dialog/component.rb +72 -0
- data/app/components/shadcn/command/empty/component.rb +29 -0
- data/app/components/shadcn/command/group/component.rb +63 -0
- data/app/components/shadcn/command/input/component.rb +66 -0
- data/app/components/shadcn/command/item/component.rb +62 -0
- data/app/components/shadcn/command/list/component.rb +36 -0
- data/app/components/shadcn/command/preview.rb +26 -0
- data/app/components/shadcn/command/previews/default.html.erb +41 -0
- data/app/components/shadcn/command/previews/dialog.html.erb +33 -0
- data/app/components/shadcn/command/previews/ranking.html.erb +16 -0
- data/app/components/shadcn/command/separator/component.rb +33 -0
- data/app/components/shadcn/command.rb +14 -0
- data/app/components/shadcn/concerns/modal_root.rb +43 -0
- data/app/components/shadcn/concerns/submits_value.rb +81 -0
- data/app/components/shadcn/context_menu/component.rb +45 -0
- data/app/components/shadcn/context_menu/content/component.rb +40 -0
- data/app/components/shadcn/context_menu/preview.rb +16 -0
- data/app/components/shadcn/context_menu/previews/default.html.erb +34 -0
- data/app/components/shadcn/context_menu/previews/items.html.erb +35 -0
- data/app/components/shadcn/context_menu/sub/component.rb +47 -0
- data/app/components/shadcn/context_menu/sub_content/component.rb +31 -0
- data/app/components/shadcn/context_menu/trigger/component.rb +50 -0
- data/app/components/shadcn/context_menu.rb +43 -0
- data/app/components/shadcn/dialog/close/component.rb +20 -0
- data/app/components/shadcn/dialog/component.rb +20 -0
- data/app/components/shadcn/dialog/content/component.rb +78 -0
- data/app/components/shadcn/dialog/header/component.rb +23 -0
- data/app/components/shadcn/dialog/overlay/component.rb +28 -0
- data/app/components/shadcn/dialog/preview.rb +28 -0
- data/app/components/shadcn/dialog/previews/default.html.erb +17 -0
- data/app/components/shadcn/dialog/previews/inside_stacking_context.html.erb +25 -0
- data/app/components/shadcn/dialog/previews/no_close_button.html.erb +15 -0
- data/app/components/shadcn/dialog/previews/scrollable_content.html.erb +29 -0
- data/app/components/shadcn/dialog/trigger/component.rb +24 -0
- data/app/components/shadcn/dialog.rb +17 -0
- data/app/components/shadcn/drawer/component.rb +33 -0
- data/app/components/shadcn/drawer/content/component.rb +95 -0
- data/app/components/shadcn/drawer/header/component.rb +34 -0
- data/app/components/shadcn/drawer/preview.rb +29 -0
- data/app/components/shadcn/drawer/previews/default.html.erb +21 -0
- data/app/components/shadcn/drawer/previews/scrollable.html.erb +20 -0
- data/app/components/shadcn/drawer/previews/scrollable_side.html.erb +21 -0
- data/app/components/shadcn/drawer/previews/sides.html.erb +17 -0
- data/app/components/shadcn/drawer.rb +27 -0
- data/app/components/shadcn/dropdown_menu/checkbox_item/component.rb +61 -0
- data/app/components/shadcn/dropdown_menu/component.rb +48 -0
- data/app/components/shadcn/dropdown_menu/content/component.rb +49 -0
- data/app/components/shadcn/dropdown_menu/group/component.rb +16 -0
- data/app/components/shadcn/dropdown_menu/item/component.rb +50 -0
- data/app/components/shadcn/dropdown_menu/preview.rb +16 -0
- data/app/components/shadcn/dropdown_menu/previews/checkboxes_and_radio.html.erb +34 -0
- data/app/components/shadcn/dropdown_menu/previews/default.html.erb +23 -0
- data/app/components/shadcn/dropdown_menu/radio_group/component.rb +16 -0
- data/app/components/shadcn/dropdown_menu/radio_item/component.rb +63 -0
- data/app/components/shadcn/dropdown_menu/sub/component.rb +44 -0
- data/app/components/shadcn/dropdown_menu/sub_content/component.rb +45 -0
- data/app/components/shadcn/dropdown_menu/sub_trigger/component.rb +58 -0
- data/app/components/shadcn/dropdown_menu/trigger/component.rb +25 -0
- data/app/components/shadcn/dropdown_menu.rb +18 -0
- data/app/components/shadcn/empty/component.rb +17 -0
- data/app/components/shadcn/empty/media/component.rb +48 -0
- data/app/components/shadcn/empty/preview.rb +16 -0
- data/app/components/shadcn/empty/previews/default.html.erb +15 -0
- data/app/components/shadcn/empty/previews/media.html.erb +32 -0
- data/app/components/shadcn/empty.rb +26 -0
- data/app/components/shadcn/field/component.rb +61 -0
- data/app/components/shadcn/field/error/component.rb +49 -0
- data/app/components/shadcn/field/legend/component.rb +30 -0
- data/app/components/shadcn/field/preview.rb +17 -0
- data/app/components/shadcn/field/previews/default.html.erb +29 -0
- data/app/components/shadcn/field/previews/form_builder.html.erb +38 -0
- data/app/components/shadcn/field/separator/component.rb +35 -0
- data/app/components/shadcn/field.rb +44 -0
- data/app/components/shadcn/hover_card/component.rb +51 -0
- data/app/components/shadcn/hover_card/content/component.rb +45 -0
- data/app/components/shadcn/hover_card/preview.rb +16 -0
- data/app/components/shadcn/hover_card/previews/default.html.erb +36 -0
- data/app/components/shadcn/hover_card/previews/sides.html.erb +15 -0
- data/app/components/shadcn/hover_card/trigger/component.rb +33 -0
- data/app/components/shadcn/icon/component.rb +76 -0
- data/app/components/shadcn/icon.rb +43 -0
- data/app/components/shadcn/input/component.rb +26 -0
- data/app/components/shadcn/input/preview.rb +16 -0
- data/app/components/shadcn/input/previews/default.html.erb +5 -0
- data/app/components/shadcn/input/previews/states.html.erb +20 -0
- data/app/components/shadcn/input_group/addon/component.rb +65 -0
- data/app/components/shadcn/input_group/button/component.rb +40 -0
- data/app/components/shadcn/input_group/component.rb +43 -0
- data/app/components/shadcn/input_group/input/component.rb +25 -0
- data/app/components/shadcn/input_group/preview.rb +17 -0
- data/app/components/shadcn/input_group/previews/addons.html.erb +53 -0
- data/app/components/shadcn/input_group/previews/default.html.erb +25 -0
- data/app/components/shadcn/input_group/text/component.rb +20 -0
- data/app/components/shadcn/input_group/textarea/component.rb +21 -0
- data/app/components/shadcn/input_otp/component.rb +95 -0
- data/app/components/shadcn/input_otp/preview.rb +17 -0
- data/app/components/shadcn/input_otp/previews/default.html.erb +19 -0
- data/app/components/shadcn/input_otp/previews/variations.html.erb +33 -0
- data/app/components/shadcn/input_otp/separator/component.rb +21 -0
- data/app/components/shadcn/input_otp/slot/component.rb +60 -0
- data/app/components/shadcn/input_otp.rb +11 -0
- data/app/components/shadcn/item/component.rb +56 -0
- data/app/components/shadcn/item/group/component.rb +53 -0
- data/app/components/shadcn/item/media/component.rb +53 -0
- data/app/components/shadcn/item/preview.rb +20 -0
- data/app/components/shadcn/item/previews/default.html.erb +32 -0
- data/app/components/shadcn/item/previews/link.html.erb +39 -0
- data/app/components/shadcn/item/previews/outline.html.erb +8 -0
- data/app/components/shadcn/item/separator/component.rb +24 -0
- data/app/components/shadcn/item.rb +34 -0
- data/app/components/shadcn/kbd/component.rb +23 -0
- data/app/components/shadcn/kbd/group/component.rb +17 -0
- data/app/components/shadcn/kbd/preview.rb +11 -0
- data/app/components/shadcn/kbd/previews/default.html.erb +4 -0
- data/app/components/shadcn/label/component.rb +20 -0
- data/app/components/shadcn/label/preview.rb +11 -0
- data/app/components/shadcn/label/previews/default.html.erb +4 -0
- data/app/components/shadcn/marker/component.rb +46 -0
- data/app/components/shadcn/marker/icon/component.rb +23 -0
- data/app/components/shadcn/marker/preview.rb +17 -0
- data/app/components/shadcn/marker/previews/default.html.erb +18 -0
- data/app/components/shadcn/marker/previews/status.html.erb +32 -0
- data/app/components/shadcn/marker.rb +17 -0
- data/app/components/shadcn/menubar/component.rb +36 -0
- data/app/components/shadcn/menubar/content/component.rb +49 -0
- data/app/components/shadcn/menubar/menu/component.rb +39 -0
- data/app/components/shadcn/menubar/preview.rb +16 -0
- data/app/components/shadcn/menubar/previews/default.html.erb +52 -0
- data/app/components/shadcn/menubar/previews/items.html.erb +38 -0
- data/app/components/shadcn/menubar/sub/component.rb +43 -0
- data/app/components/shadcn/menubar/sub_content/component.rb +31 -0
- data/app/components/shadcn/menubar/sub_trigger/component.rb +31 -0
- data/app/components/shadcn/menubar/trigger/component.rb +41 -0
- data/app/components/shadcn/menubar.rb +47 -0
- data/app/components/shadcn/message/component.rb +33 -0
- data/app/components/shadcn/message/preview.rb +16 -0
- data/app/components/shadcn/message/previews/actions.html.erb +51 -0
- data/app/components/shadcn/message/previews/default.html.erb +35 -0
- data/app/components/shadcn/message.rb +35 -0
- data/app/components/shadcn/message_scroller/button/component.rb +78 -0
- data/app/components/shadcn/message_scroller/component.rb +59 -0
- data/app/components/shadcn/message_scroller/content/component.rb +50 -0
- data/app/components/shadcn/message_scroller/item/component.rb +45 -0
- data/app/components/shadcn/message_scroller/preview.rb +11 -0
- data/app/components/shadcn/message_scroller/previews/default.html.erb +72 -0
- data/app/components/shadcn/message_scroller/viewport/component.rb +41 -0
- data/app/components/shadcn/mode_switcher/component.rb +67 -0
- data/app/components/shadcn/mode_switcher/preview.rb +11 -0
- data/app/components/shadcn/mode_switcher/previews/default.html.erb +5 -0
- data/app/components/shadcn/mode_toggle/component.rb +75 -0
- data/app/components/shadcn/mode_toggle/preview.rb +11 -0
- data/app/components/shadcn/mode_toggle/previews/default.html.erb +20 -0
- data/app/components/shadcn/native_select/component.rb +59 -0
- data/app/components/shadcn/native_select/preview.rb +16 -0
- data/app/components/shadcn/native_select/previews/default.html.erb +9 -0
- data/app/components/shadcn/native_select/previews/groups.html.erb +26 -0
- data/app/components/shadcn/native_select.rb +16 -0
- data/app/components/shadcn/navigation_menu/component.rb +51 -0
- data/app/components/shadcn/navigation_menu/content/component.rb +58 -0
- data/app/components/shadcn/navigation_menu/preview.rb +11 -0
- data/app/components/shadcn/navigation_menu/previews/default.html.erb +49 -0
- data/app/components/shadcn/navigation_menu/trigger/component.rb +59 -0
- data/app/components/shadcn/navigation_menu.rb +28 -0
- data/app/components/shadcn/pagination/component.rb +19 -0
- data/app/components/shadcn/pagination/ellipsis/component.rb +28 -0
- data/app/components/shadcn/pagination/link/component.rb +37 -0
- data/app/components/shadcn/pagination/next/component.rb +31 -0
- data/app/components/shadcn/pagination/preview.rb +16 -0
- data/app/components/shadcn/pagination/previews/default.html.erb +18 -0
- data/app/components/shadcn/pagination/previews/simple.html.erb +39 -0
- data/app/components/shadcn/pagination/previous/component.rb +31 -0
- data/app/components/shadcn/pagination.rb +14 -0
- data/app/components/shadcn/parts.rb +38 -0
- data/app/components/shadcn/popover/anchor/component.rb +17 -0
- data/app/components/shadcn/popover/component.rb +42 -0
- data/app/components/shadcn/popover/content/component.rb +40 -0
- data/app/components/shadcn/popover/preview.rb +28 -0
- data/app/components/shadcn/popover/previews/align.html.erb +13 -0
- data/app/components/shadcn/popover/previews/default.html.erb +9 -0
- data/app/components/shadcn/popover/previews/inside_containing_block.html.erb +41 -0
- data/app/components/shadcn/popover/previews/inside_stacking_context.html.erb +30 -0
- data/app/components/shadcn/popover/trigger/component.rb +24 -0
- data/app/components/shadcn/popover.rb +16 -0
- data/app/components/shadcn/progress/component.rb +62 -0
- data/app/components/shadcn/progress/preview.rb +11 -0
- data/app/components/shadcn/progress/previews/default.html.erb +5 -0
- data/app/components/shadcn/radio_group/component.rb +51 -0
- data/app/components/shadcn/radio_group/item/component.rb +74 -0
- data/app/components/shadcn/radio_group/preview.rb +16 -0
- data/app/components/shadcn/radio_group/previews/default.html.erb +15 -0
- data/app/components/shadcn/radio_group/previews/states.html.erb +41 -0
- data/app/components/shadcn/resizable/component.rb +59 -0
- data/app/components/shadcn/resizable/handle/component.rb +70 -0
- data/app/components/shadcn/resizable/panel/component.rb +53 -0
- data/app/components/shadcn/resizable/preview.rb +23 -0
- data/app/components/shadcn/resizable/previews/default.html.erb +25 -0
- data/app/components/shadcn/resizable/previews/vertical.html.erb +19 -0
- data/app/components/shadcn/resizable/previews/with_handle.html.erb +17 -0
- data/app/components/shadcn/scroll_area/component.rb +84 -0
- data/app/components/shadcn/scroll_area/preview.rb +11 -0
- data/app/components/shadcn/scroll_area/previews/default.html.erb +36 -0
- data/app/components/shadcn/scroll_area/scrollbar/component.rb +72 -0
- data/app/components/shadcn/scroll_area/viewport/component.rb +70 -0
- data/app/components/shadcn/scroll_area.rb +14 -0
- data/app/components/shadcn/select/component.rb +67 -0
- data/app/components/shadcn/select/content/component.rb +157 -0
- data/app/components/shadcn/select/empty/component.rb +38 -0
- data/app/components/shadcn/select/group/component.rb +16 -0
- data/app/components/shadcn/select/item/component.rb +71 -0
- data/app/components/shadcn/select/list/component.rb +41 -0
- data/app/components/shadcn/select/preview.rb +24 -0
- data/app/components/shadcn/select/previews/default.html.erb +17 -0
- data/app/components/shadcn/select/previews/scrollable.html.erb +19 -0
- data/app/components/shadcn/select/previews/searchable.html.erb +18 -0
- data/app/components/shadcn/select/previews/states.html.erb +25 -0
- data/app/components/shadcn/select/search/component.rb +54 -0
- data/app/components/shadcn/select/trigger/component.rb +78 -0
- data/app/components/shadcn/select/value/component.rb +28 -0
- data/app/components/shadcn/select.rb +15 -0
- data/app/components/shadcn/separator/component.rb +41 -0
- data/app/components/shadcn/separator/preview.rb +11 -0
- data/app/components/shadcn/separator/previews/default.html.erb +9 -0
- data/app/components/shadcn/sheet/component.rb +16 -0
- data/app/components/shadcn/sheet/content/component.rb +98 -0
- data/app/components/shadcn/sheet/header/component.rb +23 -0
- data/app/components/shadcn/sheet/preview.rb +16 -0
- data/app/components/shadcn/sheet/previews/default.html.erb +16 -0
- data/app/components/shadcn/sheet/previews/no_close_button.html.erb +14 -0
- data/app/components/shadcn/sheet.rb +22 -0
- data/app/components/shadcn/sidebar/component.rb +232 -0
- data/app/components/shadcn/sidebar/input/component.rb +25 -0
- data/app/components/shadcn/sidebar/menu_action/component.rb +58 -0
- data/app/components/shadcn/sidebar/menu_button/component.rb +100 -0
- data/app/components/shadcn/sidebar/menu_skeleton/component.rb +51 -0
- data/app/components/shadcn/sidebar/menu_sub_button/component.rb +60 -0
- data/app/components/shadcn/sidebar/preview.rb +22 -0
- data/app/components/shadcn/sidebar/previews/default.html.erb +189 -0
- data/app/components/shadcn/sidebar/previews/floating.html.erb +39 -0
- data/app/components/shadcn/sidebar/previews/inset.html.erb +48 -0
- data/app/components/shadcn/sidebar/provider/component.rb +53 -0
- data/app/components/shadcn/sidebar/rail/component.rb +43 -0
- data/app/components/shadcn/sidebar/separator/component.rb +23 -0
- data/app/components/shadcn/sidebar/trigger/component.rb +39 -0
- data/app/components/shadcn/sidebar.rb +98 -0
- data/app/components/shadcn/skeleton/component.rb +14 -0
- data/app/components/shadcn/skeleton/preview.rb +11 -0
- data/app/components/shadcn/skeleton/previews/default.html.erb +7 -0
- data/app/components/shadcn/slider/component.rb +145 -0
- data/app/components/shadcn/slider/preview.rb +16 -0
- data/app/components/shadcn/slider/previews/default.html.erb +17 -0
- data/app/components/shadcn/slider/previews/vertical.html.erb +7 -0
- data/app/components/shadcn/slider/range/component.rb +51 -0
- data/app/components/shadcn/slider/thumb/component.rb +58 -0
- data/app/components/shadcn/slider/track/component.rb +32 -0
- data/app/components/shadcn/spinner/component.rb +21 -0
- data/app/components/shadcn/spinner/preview.rb +11 -0
- data/app/components/shadcn/spinner/previews/default.html.erb +7 -0
- data/app/components/shadcn/switch/component.rb +104 -0
- data/app/components/shadcn/switch/preview.rb +11 -0
- data/app/components/shadcn/switch/previews/default.html.erb +24 -0
- data/app/components/shadcn/table/component.rb +26 -0
- data/app/components/shadcn/table/preview.rb +16 -0
- data/app/components/shadcn/table/previews/default.html.erb +19 -0
- data/app/components/shadcn/table/previews/footer.html.erb +25 -0
- data/app/components/shadcn/table.rb +33 -0
- data/app/components/shadcn/tabs/component.rb +40 -0
- data/app/components/shadcn/tabs/content/component.rb +34 -0
- data/app/components/shadcn/tabs/list/component.rb +51 -0
- data/app/components/shadcn/tabs/preview.rb +16 -0
- data/app/components/shadcn/tabs/previews/default.html.erb +21 -0
- data/app/components/shadcn/tabs/previews/line.html.erb +23 -0
- data/app/components/shadcn/tabs/trigger/component.rb +66 -0
- data/app/components/shadcn/textarea/component.rb +24 -0
- data/app/components/shadcn/textarea/preview.rb +16 -0
- data/app/components/shadcn/textarea/previews/default.html.erb +3 -0
- data/app/components/shadcn/textarea/previews/states.html.erb +18 -0
- data/app/components/shadcn/theme_selector/component.rb +88 -0
- data/app/components/shadcn/theme_selector/preview.rb +17 -0
- data/app/components/shadcn/theme_selector/previews/all_themes.html.erb +20 -0
- data/app/components/shadcn/theme_selector/previews/default.html.erb +18 -0
- data/app/components/shadcn/toaster/component.rb +139 -0
- data/app/components/shadcn/toaster/preview.rb +16 -0
- data/app/components/shadcn/toaster/previews/default.html.erb +19 -0
- data/app/components/shadcn/toaster/previews/from_the_server.html.erb +15 -0
- data/app/components/shadcn/toaster/toast/component.rb +134 -0
- data/app/components/shadcn/toggle/component.rb +81 -0
- data/app/components/shadcn/toggle/preview.rb +11 -0
- data/app/components/shadcn/toggle/previews/default.html.erb +6 -0
- data/app/components/shadcn/toggle_group/component.rb +62 -0
- data/app/components/shadcn/toggle_group/item/component.rb +59 -0
- data/app/components/shadcn/toggle_group/preview.rb +16 -0
- data/app/components/shadcn/toggle_group/previews/default.html.erb +12 -0
- data/app/components/shadcn/toggle_group/previews/sizes.html.erb +37 -0
- data/app/components/shadcn/tooltip/component.rb +38 -0
- data/app/components/shadcn/tooltip/content/component.rb +62 -0
- data/app/components/shadcn/tooltip/preview.rb +21 -0
- data/app/components/shadcn/tooltip/previews/default.html.erb +4 -0
- data/app/components/shadcn/tooltip/previews/sides.html.erb +19 -0
- data/app/components/shadcn/tooltip/previews/with_keyboard_shortcut.html.erb +11 -0
- data/app/components/shadcn/tooltip/provider/component.rb +28 -0
- data/app/components/shadcn/tooltip/trigger/component.rb +25 -0
- data/app/helpers/shadcn_view_component/form_helper.rb +25 -0
- data/app/helpers/shadcn_view_component/theme_helper.rb +61 -0
- data/app/javascript/shadcn/animation.js +180 -0
- data/app/javascript/shadcn/command_score.js +110 -0
- data/app/javascript/shadcn/controllers/accordion_controller.js +127 -0
- data/app/javascript/shadcn/controllers/avatar_controller.js +32 -0
- data/app/javascript/shadcn/controllers/calendar_controller.js +633 -0
- data/app/javascript/shadcn/controllers/carousel_controller.js +139 -0
- data/app/javascript/shadcn/controllers/chart_controller.js +110 -0
- data/app/javascript/shadcn/controllers/checkbox_controller.js +61 -0
- data/app/javascript/shadcn/controllers/collapsible_controller.js +55 -0
- data/app/javascript/shadcn/controllers/combobox_controller.js +360 -0
- data/app/javascript/shadcn/controllers/command_controller.js +199 -0
- data/app/javascript/shadcn/controllers/dialog_controller.js +154 -0
- data/app/javascript/shadcn/controllers/drawer_controller.js +265 -0
- data/app/javascript/shadcn/controllers/dropdown_menu_controller.js +259 -0
- data/app/javascript/shadcn/controllers/hover_card_controller.js +85 -0
- data/app/javascript/shadcn/controllers/input_group_controller.js +17 -0
- data/app/javascript/shadcn/controllers/input_otp_controller.js +80 -0
- data/app/javascript/shadcn/controllers/menubar_controller.js +151 -0
- data/app/javascript/shadcn/controllers/message_scroller_controller.js +456 -0
- data/app/javascript/shadcn/controllers/navigation_menu_controller.js +211 -0
- data/app/javascript/shadcn/controllers/popover_controller.js +66 -0
- data/app/javascript/shadcn/controllers/radio_group_controller.js +69 -0
- data/app/javascript/shadcn/controllers/resizable_controller.js +167 -0
- data/app/javascript/shadcn/controllers/scroll_area_controller.js +253 -0
- data/app/javascript/shadcn/controllers/select_controller.js +320 -0
- data/app/javascript/shadcn/controllers/sidebar_controller.js +273 -0
- data/app/javascript/shadcn/controllers/slider_controller.js +230 -0
- data/app/javascript/shadcn/controllers/switch_controller.js +39 -0
- data/app/javascript/shadcn/controllers/tabs_controller.js +81 -0
- data/app/javascript/shadcn/controllers/theme_controller.js +67 -0
- data/app/javascript/shadcn/controllers/toaster_controller.js +281 -0
- data/app/javascript/shadcn/controllers/toggle_controller.js +24 -0
- data/app/javascript/shadcn/controllers/toggle_group_controller.js +73 -0
- data/app/javascript/shadcn/controllers/tooltip_controller.js +58 -0
- data/app/javascript/shadcn/direction.js +41 -0
- data/app/javascript/shadcn/dismiss.js +73 -0
- data/app/javascript/shadcn/floating.js +207 -0
- data/app/javascript/shadcn/focus.js +86 -0
- data/app/javascript/shadcn/id.js +14 -0
- data/app/javascript/shadcn/index.js +110 -0
- data/app/javascript/shadcn/popper.js +262 -0
- data/app/javascript/shadcn/scroll_geometry.js +267 -0
- data/app/javascript/shadcn/theme.js +161 -0
- data/app/javascript/shadcn/top_layer.js +59 -0
- data/app/javascript/shadcn/typeahead.js +99 -0
- data/config/importmap.rb +6 -0
- data/config/locales/en.yml +62 -0
- data/lib/generators/shadcn_view_component/install/install_generator.rb +149 -0
- data/lib/shadcn_view_component/engine.rb +73 -0
- data/lib/shadcn_view_component/form_builder.rb +264 -0
- data/lib/shadcn_view_component/icon_registry.rb +66 -0
- data/lib/shadcn_view_component/icons.rb +32 -0
- data/lib/shadcn_view_component/themes.rb +1065 -0
- data/lib/shadcn_view_component/version.rb +3 -0
- data/lib/shadcn_view_component.rb +82 -0
- data/lib/tasks/icons.rake +53 -0
- data/lib/tasks/themes.rake +171 -0
- metadata +608 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module ShadcnViewComponent
|
|
6
|
+
module Generators
|
|
7
|
+
# `bin/rails generate shadcn_view_component:install`
|
|
8
|
+
#
|
|
9
|
+
# Wires the gem into a host app. The part worth automating is every line
|
|
10
|
+
# that names a path into this gem: they have to point inside whatever
|
|
11
|
+
# directory bundler put us in, and that differs between a system gem,
|
|
12
|
+
# `bundle config set path`, a `path:` checkout and a `git:` source.
|
|
13
|
+
#
|
|
14
|
+
# All three lines, not just `@source`. This generator used to write
|
|
15
|
+
# `@import "shadcn.css"`, which reads like an asset-pipeline path and is
|
|
16
|
+
# not one: `tailwindcss-rails` runs the CLI with `-i` and `-o` and no load
|
|
17
|
+
# path, so the CLI resolves a bare name the way Node does — beside the file,
|
|
18
|
+
# then `node_modules` — and a Rails app has neither. It stopped the build
|
|
19
|
+
# with `Can't resolve 'shadcn.css'`. The dummy never showed it because its
|
|
20
|
+
# own entrypoint reaches the engine with `../../../../..`, which is a
|
|
21
|
+
# relationship no host has.
|
|
22
|
+
class InstallGenerator < Rails::Generators::Base
|
|
23
|
+
source_root File.expand_path("templates", __dir__)
|
|
24
|
+
|
|
25
|
+
# Where a Stimulus application is in scope. `controllers/index.js` is the
|
|
26
|
+
# file importmap-rails generates, and it opens with
|
|
27
|
+
# `import { application } from "controllers/application"` — so a line
|
|
28
|
+
# appended there can register onto the app's own instance.
|
|
29
|
+
#
|
|
30
|
+
# `app/javascript/application.js` is not that file. It imports
|
|
31
|
+
# `"controllers"` for the side effect and defines no `application`
|
|
32
|
+
# binding, which is where this generator used to append: a stock Rails 8
|
|
33
|
+
# app got `ReferenceError: application is not defined`, no controllers
|
|
34
|
+
# registered, and every dialog, select and menu inert — with the CSS
|
|
35
|
+
# working, so it looked installed.
|
|
36
|
+
JAVASCRIPT_ENTRYPOINT = "app/javascript/controllers/index.js"
|
|
37
|
+
|
|
38
|
+
TAILWIND_ENTRYPOINTS = [
|
|
39
|
+
"app/assets/tailwind/application.css",
|
|
40
|
+
"app/assets/stylesheets/application.tailwind.css",
|
|
41
|
+
"app/assets/stylesheets/application.css"
|
|
42
|
+
].freeze
|
|
43
|
+
|
|
44
|
+
def add_tailwind_imports
|
|
45
|
+
entrypoint = TAILWIND_ENTRYPOINTS.find { |path| File.exist?(Rails.root.join(path)) }
|
|
46
|
+
|
|
47
|
+
unless entrypoint
|
|
48
|
+
say_status :skip, "no Tailwind entrypoint found; add the block below yourself", :yellow
|
|
49
|
+
say "(paths are written for #{TAILWIND_ENTRYPOINTS.first}; adjust them if yours sits elsewhere)"
|
|
50
|
+
say tailwind_block(TAILWIND_ENTRYPOINTS.first)
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if File.read(Rails.root.join(entrypoint)).include?("shadcn.css")
|
|
55
|
+
say_status :identical, entrypoint
|
|
56
|
+
return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
append_to_file entrypoint, "\n#{tailwind_block(entrypoint)}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def add_javascript
|
|
63
|
+
return say_status :skip, "no importmap; import \"shadcn\" yourself", :yellow unless importmap?
|
|
64
|
+
|
|
65
|
+
unless File.exist?(Rails.root.join(JAVASCRIPT_ENTRYPOINT))
|
|
66
|
+
say_status :skip, "#{JAVASCRIPT_ENTRYPOINT} not found; register the controllers yourself", :yellow
|
|
67
|
+
say javascript_block
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if File.read(Rails.root.join(JAVASCRIPT_ENTRYPOINT)).include?("registerShadcnControllers")
|
|
72
|
+
return say_status :identical, JAVASCRIPT_ENTRYPOINT
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
append_to_file JAVASCRIPT_ENTRYPOINT, "\n#{javascript_block}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def show_layout_hint
|
|
79
|
+
say "\nAdd the theming tags to your layout:", :green
|
|
80
|
+
say <<~ERB
|
|
81
|
+
<head>
|
|
82
|
+
<%= shadcn_theme_script_tag %>
|
|
83
|
+
</head>
|
|
84
|
+
<body class="<%= shadcn_theme_class %>">
|
|
85
|
+
ERB
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def importmap?
|
|
91
|
+
File.exist?(Rails.root.join("config/importmap.rb"))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Resolved from the loaded gem spec, so it is correct wherever bundler put
|
|
95
|
+
# us — including a `path:` or `git:` source.
|
|
96
|
+
def gem_root
|
|
97
|
+
Pathname(ShadcnViewComponent::Engine.root)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Relative to the file the line is written into, whenever this gem sits
|
|
101
|
+
# inside the application — which is what `bundle config set path
|
|
102
|
+
# vendor/bundle` does, and what CI and most containers do. An absolute
|
|
103
|
+
# path is correct only on the machine that generated it, and the CSS is
|
|
104
|
+
# built on every machine.
|
|
105
|
+
#
|
|
106
|
+
# Computed against the entrypoint rather than against a fixed depth: the
|
|
107
|
+
# three conventional locations happen to be three deep today, and a
|
|
108
|
+
# generator that would break on the fourth is a generator waiting.
|
|
109
|
+
def path_to(target, entrypoint)
|
|
110
|
+
return target.to_s unless inside_application?(target)
|
|
111
|
+
|
|
112
|
+
target.relative_path_from(Rails.root.join(entrypoint).dirname).to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A relative path is worth having only while it stays inside the
|
|
116
|
+
# application: between two unrelated trees — a system gem, a sibling
|
|
117
|
+
# checkout — it encodes the distance between them, which is as personal
|
|
118
|
+
# to one machine as an absolute path and harder to read.
|
|
119
|
+
def inside_application?(target)
|
|
120
|
+
!target.relative_path_from(Rails.root).to_s.start_with?("..")
|
|
121
|
+
rescue ArgumentError
|
|
122
|
+
# Different volumes: no relative path exists at all.
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# `application` is the binding `controllers/index.js` already has. Adding
|
|
127
|
+
# an `Application.start()` here instead would start a *second* Stimulus
|
|
128
|
+
# instance beside the app's own, and two instances fight over every
|
|
129
|
+
# element they both claim.
|
|
130
|
+
def javascript_block
|
|
131
|
+
<<~JS
|
|
132
|
+
import { registerShadcnControllers } from "shadcn"
|
|
133
|
+
registerShadcnControllers(application)
|
|
134
|
+
JS
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def tailwind_block(entrypoint)
|
|
138
|
+
stylesheets = gem_root.join("app/assets/stylesheets")
|
|
139
|
+
|
|
140
|
+
<<~CSS
|
|
141
|
+
/* shadcn_view_component */
|
|
142
|
+
@import "#{path_to(stylesheets.join('shadcn.css'), entrypoint)}";
|
|
143
|
+
@import "#{path_to(stylesheets.join('shadcn-themes.css'), entrypoint)}";
|
|
144
|
+
@source "#{path_to(gem_root.join('app/components'), entrypoint)}";
|
|
145
|
+
CSS
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module ShadcnViewComponent
|
|
2
|
+
# Rails engine wiring.
|
|
3
|
+
#
|
|
4
|
+
# `app/components` is picked up automatically as an autoload/eager-load root
|
|
5
|
+
# by the `app/{*,*/concerns}` glob every Rails::Engine declares, so
|
|
6
|
+
# `app/components/shadcn/card/component.rb` resolves to `Shadcn::Card::Component`
|
|
7
|
+
# with no extra configuration. Nesting everything under `shadcn/` keeps names
|
|
8
|
+
# such as `Card`, `Table` or `Field` out of the host application's top level,
|
|
9
|
+
# where they would clash with its own models.
|
|
10
|
+
class Engine < ::Rails::Engine
|
|
11
|
+
COMPONENTS_PATH = "app/components".freeze
|
|
12
|
+
|
|
13
|
+
# Host apps configure the gem through `config.shadcn_view_component` in
|
|
14
|
+
# their `application.rb`, e.g.
|
|
15
|
+
#
|
|
16
|
+
# config.shadcn_view_component.cache_size = 50_000
|
|
17
|
+
config.shadcn_view_component = ActiveSupport::OrderedOptions.new
|
|
18
|
+
|
|
19
|
+
# Register component previews (sidecar `preview.rb` next to `component.rb`).
|
|
20
|
+
initializer "shadcn_view_component.previews" do |app|
|
|
21
|
+
options = app.config.view_component
|
|
22
|
+
options.previews.paths << root.join(COMPONENTS_PATH).to_s
|
|
23
|
+
|
|
24
|
+
ActiveSupport.on_load(:view_component) do
|
|
25
|
+
ViewComponent::Preview.extend ViewComponentContrib::Preview::Sidecarable
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Build the tailwind-merge instance at boot rather than on the first
|
|
30
|
+
# request: it costs a few milliseconds to construct, and two Puma threads
|
|
31
|
+
# racing on a lazy `||=` would each build one and discard a warm cache.
|
|
32
|
+
initializer "shadcn_view_component.merger" do |app|
|
|
33
|
+
configured = app.config.shadcn_view_component.cache_size
|
|
34
|
+
ShadcnViewComponent.cache_size = configured if configured
|
|
35
|
+
|
|
36
|
+
ShadcnViewComponent.build_merger
|
|
37
|
+
|
|
38
|
+
# Style blocks are redefined when components reload, so the memoised
|
|
39
|
+
# class strings have to go with them.
|
|
40
|
+
app.config.to_prepare { ShadcnViewComponent.class_cache.clear }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Make the theming and form helpers available to the host application's
|
|
44
|
+
# views. The form builder is required here rather than at the top of the gem
|
|
45
|
+
# because it subclasses ActionView's.
|
|
46
|
+
initializer "shadcn_view_component.helpers" do
|
|
47
|
+
ActiveSupport.on_load(:action_view) do
|
|
48
|
+
require "shadcn_view_component/form_builder"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
52
|
+
helper ShadcnViewComponent::ThemeHelper
|
|
53
|
+
helper ShadcnViewComponent::FormHelper
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Expose the bundled stylesheet and Stimulus controllers to the asset pipeline.
|
|
58
|
+
initializer "shadcn_view_component.assets" do |app|
|
|
59
|
+
next unless app.config.respond_to?(:assets)
|
|
60
|
+
|
|
61
|
+
app.config.assets.paths << root.join("app/assets/stylesheets")
|
|
62
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Make `shadcn/*` importable when the host app uses importmap-rails.
|
|
66
|
+
initializer "shadcn_view_component.importmap", before: "importmap" do |app|
|
|
67
|
+
next unless app.config.respond_to?(:importmap)
|
|
68
|
+
|
|
69
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
70
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShadcnViewComponent
|
|
4
|
+
# Bridges the components to Rails forms.
|
|
5
|
+
#
|
|
6
|
+
# shadcn's `Field` family is the right shape for this — label, control,
|
|
7
|
+
# description, error — but on its own it knows nothing about a model. This
|
|
8
|
+
# builder wires the two together: ids and names come from Rails, the error
|
|
9
|
+
# text and `aria-invalid` come from `ActiveModel::Errors`, and `Field` gets the
|
|
10
|
+
# `data-invalid` its styles key off.
|
|
11
|
+
#
|
|
12
|
+
# <%= shadcn_form_with model: @user do |f| %>
|
|
13
|
+
# <%= f.shadcn_field :email, label: "Email", description: "We never share it." do %>
|
|
14
|
+
# <%= f.shadcn_input :email, type: "email" %>
|
|
15
|
+
# <% end %>
|
|
16
|
+
# <% end %>
|
|
17
|
+
#
|
|
18
|
+
# Or, when the control needs no arguments of its own:
|
|
19
|
+
#
|
|
20
|
+
# <%= f.shadcn_input_field :email, label: "Email", type: "email" %>
|
|
21
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
|
22
|
+
# A labelled control with its description and any validation errors, wired
|
|
23
|
+
# together by id. `Field` styles itself from `data-invalid`, so that is set
|
|
24
|
+
# from the model rather than passed in.
|
|
25
|
+
#
|
|
26
|
+
# @param method [Symbol] the attribute
|
|
27
|
+
# @param label [String] omit to leave the label out entirely
|
|
28
|
+
# @param description [String] hint text below the control
|
|
29
|
+
# @param orientation [Symbol] :vertical, :horizontal or :responsive
|
|
30
|
+
def shadcn_field(method, label: nil, description: nil, orientation: :vertical, **options, &block)
|
|
31
|
+
messages = errors_for(method)
|
|
32
|
+
described[method.to_s] = description.present?
|
|
33
|
+
|
|
34
|
+
@template.render(Shadcn::Field::Component.new(
|
|
35
|
+
orientation:,
|
|
36
|
+
"data-invalid": (true if messages.any?),
|
|
37
|
+
**options
|
|
38
|
+
)) do |field|
|
|
39
|
+
field.with_label(id: label_id(method), for: field_id(method)) { label } if label
|
|
40
|
+
field.with_field_content { @template.capture(&block) }
|
|
41
|
+
field.with_description(id: description_id(method)) { description } if description
|
|
42
|
+
field.with_error(id: error_id(method), errors: messages) if messages.any?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `shadcn_input_field`, `shadcn_textarea_field`, … — the common case where
|
|
47
|
+
# the field wraps exactly one control.
|
|
48
|
+
%i[input textarea native_select switch checkbox].each do |control|
|
|
49
|
+
define_method(:"shadcn_#{control}_field") do |method, label: nil, description: nil, orientation: nil, **options|
|
|
50
|
+
shadcn_field(
|
|
51
|
+
method,
|
|
52
|
+
label:,
|
|
53
|
+
description:,
|
|
54
|
+
orientation: orientation || (%i[switch checkbox].include?(control) ? :horizontal : :vertical)
|
|
55
|
+
) { public_send(:"shadcn_#{control}", method, **options) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def shadcn_input(method, **options)
|
|
60
|
+
@template.render(Shadcn::Input::Component.new(**control_options(method, **options)))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def shadcn_textarea(method, **options)
|
|
64
|
+
@template.render(Shadcn::Textarea::Component.new(**control_options(method, **options))) do
|
|
65
|
+
value_for(method).to_s
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A real `<select>`: browser validation, autofill and keyboard behaviour for
|
|
70
|
+
# free. Prefer it over `shadcn_select` unless you need the styled listbox.
|
|
71
|
+
def shadcn_native_select(method, choices = [], **options)
|
|
72
|
+
@template.render(Shadcn::NativeSelect::Component.new(**control_options(method, **options))) do
|
|
73
|
+
@template.safe_join(Array(choices).map { |choice| native_option(method, choice) })
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The Radix-style listbox. It submits through a hidden input, so it has no
|
|
78
|
+
# browser validation — `required` will not stop the form.
|
|
79
|
+
def shadcn_select(method, choices = [], placeholder: nil, **options)
|
|
80
|
+
selected = value_for(method).to_s
|
|
81
|
+
|
|
82
|
+
@template.render(Shadcn::Select::Component.new(
|
|
83
|
+
name: field_name(method), value: selected, placeholder:, **options
|
|
84
|
+
)) do |select|
|
|
85
|
+
select.with_trigger(**aria_labelled(method,
|
|
86
|
+
id: field_id(method),
|
|
87
|
+
placeholder: selected.blank?,
|
|
88
|
+
"aria-invalid": invalid(method))) do |trigger|
|
|
89
|
+
trigger.with_value(placeholder:) { label_for(choices, selected) }
|
|
90
|
+
end
|
|
91
|
+
select.with_select_content do
|
|
92
|
+
@template.safe_join(Array(choices).map { |choice|
|
|
93
|
+
label, value = option_pair(choice)
|
|
94
|
+
@template.render(Shadcn::Select::Item::Component.new(value:, selected: value == selected)) { label }
|
|
95
|
+
})
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A calendar bound to the model, in any of its three modes.
|
|
101
|
+
#
|
|
102
|
+
# f.shadcn_calendar :starts_on
|
|
103
|
+
# f.shadcn_calendar :dates, mode: :multiple
|
|
104
|
+
# f.shadcn_calendar :starts_on, mode: :range, to: :ends_on
|
|
105
|
+
#
|
|
106
|
+
# `to:` is what makes a range Rails-shaped: two dates are usually two
|
|
107
|
+
# columns, so the two ends are named after two attributes and each comes
|
|
108
|
+
# back as itself. Without it a range submits `name[from]` and `name[to]`
|
|
109
|
+
# under one parameter, which suits a form object rather than a record.
|
|
110
|
+
#
|
|
111
|
+
# The month shown follows the value rather than today, so reopening a form
|
|
112
|
+
# lands on the date already chosen.
|
|
113
|
+
def shadcn_calendar(method, mode: :single, to: nil, **options)
|
|
114
|
+
selected = calendar_selection(method, mode:, to:)
|
|
115
|
+
|
|
116
|
+
@template.render(Shadcn::Calendar::Component.new(
|
|
117
|
+
mode:,
|
|
118
|
+
selected:,
|
|
119
|
+
name: to ? [ field_name(method), field_name(to) ] : field_name(method),
|
|
120
|
+
month: options.delete(:month) || Array(selected).compact.first,
|
|
121
|
+
**aria_labelled(method, options)
|
|
122
|
+
))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def shadcn_checkbox(method, **options)
|
|
126
|
+
@template.render(Shadcn::Checkbox::Component.new(
|
|
127
|
+
name: field_name(method),
|
|
128
|
+
checked: !!value_for(method),
|
|
129
|
+
**aria_labelled(method, **control_options(method, **options))
|
|
130
|
+
))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def shadcn_switch(method, **options)
|
|
134
|
+
@template.render(Shadcn::Switch::Component.new(
|
|
135
|
+
name: field_name(method),
|
|
136
|
+
checked: !!value_for(method),
|
|
137
|
+
**aria_labelled(method, **control_options(method, **options))
|
|
138
|
+
))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Each option is rendered as block content rather than through the `item`
|
|
142
|
+
# slot: the items have to sit inside their own label rows, and slot content
|
|
143
|
+
# is emitted before block content, which would pull them all out of place.
|
|
144
|
+
def shadcn_radio_group(method, choices = [], **options)
|
|
145
|
+
selected = value_for(method).to_s
|
|
146
|
+
|
|
147
|
+
@template.render(Shadcn::RadioGroup::Component.new(
|
|
148
|
+
name: field_name(method), value: selected, **options
|
|
149
|
+
)) do
|
|
150
|
+
@template.safe_join(Array(choices).map { |choice|
|
|
151
|
+
label, value = option_pair(choice)
|
|
152
|
+
id = "#{field_id(method)}_#{value.parameterize.underscore}"
|
|
153
|
+
|
|
154
|
+
@template.tag.div(class: "flex items-center gap-2") do
|
|
155
|
+
@template.safe_join([
|
|
156
|
+
@template.render(Shadcn::RadioGroup::Item::Component.new(
|
|
157
|
+
value:, checked: value == selected, id:
|
|
158
|
+
)),
|
|
159
|
+
@template.render(Shadcn::Label::Component.new(for: id)) { label }
|
|
160
|
+
])
|
|
161
|
+
end
|
|
162
|
+
})
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def shadcn_submit(text = nil, **options, &block)
|
|
167
|
+
@template.render(Shadcn::Button::Component.new(type: "submit", **options)) do
|
|
168
|
+
block ? @template.capture(&block) : (text || submit_default_value)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Which attributes have actually had a description rendered. `aria-describedby`
|
|
175
|
+
# may only name elements that exist — a reference to a missing id is invalid
|
|
176
|
+
# ARIA and simply resolves to nothing, so the control ends up describing
|
|
177
|
+
# itself as having a description it does not have.
|
|
178
|
+
#
|
|
179
|
+
# Upstream has the same defect and it is not reproduced on purpose:
|
|
180
|
+
# `FormControl` sets `aria-describedby` to the description id unconditionally
|
|
181
|
+
# (form.tsx:113-119), whether or not a `FormDescription` was ever rendered.
|
|
182
|
+
# This port shipped it too, until writing features/form.md checked the claim
|
|
183
|
+
# that it did not.
|
|
184
|
+
#
|
|
185
|
+
# A control rendered outside any `shadcn_field` is the same case and gets the
|
|
186
|
+
# same answer: no description element, so nothing to point at.
|
|
187
|
+
def described
|
|
188
|
+
@described ||= {}
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def errors_for(method)
|
|
192
|
+
return [] unless object.respond_to?(:errors)
|
|
193
|
+
|
|
194
|
+
object.errors[method] || []
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def invalid(method)
|
|
198
|
+
"true" if errors_for(method).any?
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# What is already chosen, in the shape the component takes: one date, a
|
|
202
|
+
# list of them, or the two ends of a range.
|
|
203
|
+
def calendar_selection(method, mode:, to:)
|
|
204
|
+
return [ value_for(method), value_for(to) ].compact if to
|
|
205
|
+
return Array(value_for(method)).compact if mode == :multiple
|
|
206
|
+
|
|
207
|
+
value_for(method)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def value_for(method)
|
|
211
|
+
object.respond_to?(method) ? object.public_send(method) : nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Everything a control needs to belong to the form and to announce itself.
|
|
215
|
+
def control_options(method, **options)
|
|
216
|
+
wants_description = options.delete(:described_by) != false
|
|
217
|
+
described_by = [
|
|
218
|
+
(description_id(method) if wants_description && described[method.to_s]),
|
|
219
|
+
(error_id(method) if errors_for(method).any?)
|
|
220
|
+
].compact
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
id: field_id(method),
|
|
224
|
+
name: field_name(method),
|
|
225
|
+
value: value_for(method),
|
|
226
|
+
"aria-invalid": invalid(method),
|
|
227
|
+
"aria-describedby": described_by.presence&.join(" ")
|
|
228
|
+
}.compact.merge(options)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def description_id(method) = "#{field_id(method)}_description"
|
|
232
|
+
def error_id(method) = "#{field_id(method)}_error"
|
|
233
|
+
def label_id(method) = "#{field_id(method)}_label"
|
|
234
|
+
|
|
235
|
+
# Select, Checkbox and Switch render a `<button>` carrying an ARIA role, and
|
|
236
|
+
# a `<label for>` does not name a button — `role="combobox"` goes further and
|
|
237
|
+
# forbids taking the name from its own content. So they are named by pointing
|
|
238
|
+
# at the label instead. Without this they reach the browser unnamed, which
|
|
239
|
+
# `spec/system/accessibility_spec.rb` catches.
|
|
240
|
+
def aria_labelled(method, options)
|
|
241
|
+
return options if options.key?(:"aria-label") || options.key?(:"aria-labelledby")
|
|
242
|
+
|
|
243
|
+
options.merge("aria-labelledby": label_id(method))
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Accepts the same shapes as Rails' `options_for_select`: bare values, or
|
|
247
|
+
# `[label, value]` pairs.
|
|
248
|
+
def option_pair(choice)
|
|
249
|
+
choice.is_a?(Array) ? [ choice.first.to_s, choice.last.to_s ] : [ choice.to_s, choice.to_s ]
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def label_for(choices, value)
|
|
253
|
+
Array(choices).map { |choice| option_pair(choice) }.find { |(_label, v)| v == value }&.first
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def native_option(method, choice)
|
|
257
|
+
label, value = option_pair(choice)
|
|
258
|
+
|
|
259
|
+
@template.render(Shadcn::NativeSelect::Option::Component.new(
|
|
260
|
+
value:, selected: value == value_for(method).to_s
|
|
261
|
+
)) { label }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShadcnViewComponent
|
|
4
|
+
# Backs `Shadcn::Icon.register` / `.registered`. Deliberately outside
|
|
5
|
+
# `app/components`, which Rails reloads in development: a hash living on
|
|
6
|
+
# the `Shadcn::Icon` module object would be discarded with it on the next
|
|
7
|
+
# code reload, silently un-registering every icon a host added at boot.
|
|
8
|
+
# `lib/` is not reloaded, so this survives.
|
|
9
|
+
module IconRegistry
|
|
10
|
+
# lucide-react's own aliases, kept so call sites read like the TSX imports
|
|
11
|
+
# — `Icon::Component.new("more-horizontal")` is what `pagination.tsx`
|
|
12
|
+
# writes. They live here rather than beside the drawings because *this* is
|
|
13
|
+
# the file that has to know about them: a host registering
|
|
14
|
+
# `"more-horizontal"` and the gem rendering `"ellipsis"` are the same icon,
|
|
15
|
+
# and until they were resolved in one place the registration was stored
|
|
16
|
+
# under a key nothing ever looked up.
|
|
17
|
+
ALIASES = {
|
|
18
|
+
"loader-2" => "loader-circle",
|
|
19
|
+
"more-horizontal" => "ellipsis"
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def self.registered
|
|
23
|
+
@registered ||= {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Stored under the canonical name, so registering under either spelling
|
|
27
|
+
# reaches every call site that uses either — one icon, one entry.
|
|
28
|
+
def self.register(name, path)
|
|
29
|
+
registered[canonical(name)] = path
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.canonical(name)
|
|
33
|
+
ALIASES.fetch(name.to_s, name.to_s)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Register every `*.svg` in a directory, under its own basename. This is
|
|
37
|
+
# how a host adds icons the gem does not bundle: download the SVGs it wants
|
|
38
|
+
# — lucide publishes them as files, and so does everyone else — drop them
|
|
39
|
+
# in a directory and point here from an initializer.
|
|
40
|
+
#
|
|
41
|
+
# ShadcnViewComponent::IconRegistry.load_directory(
|
|
42
|
+
# Rails.root.join("app/assets/icons")
|
|
43
|
+
# )
|
|
44
|
+
#
|
|
45
|
+
# It reads files, so it is the caller who decides when: an initializer
|
|
46
|
+
# pays once at boot. Nothing in the gem calls it — the bundled icons are
|
|
47
|
+
# generated into `Icons::PATHS` at build time, so a host that adds none
|
|
48
|
+
# reads no files at all.
|
|
49
|
+
def self.load_directory(path)
|
|
50
|
+
Dir.glob(File.join(path, "*.svg")).sort.each do |file|
|
|
51
|
+
register(File.basename(file, ".svg"), drawing(File.read(file)))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# What a `<svg>` element contains, with the whitespace its author used to
|
|
56
|
+
# lay it out taken back out. The outer element is deliberately dropped:
|
|
57
|
+
# `Icon::Component` renders that itself, with lucide's own attributes and
|
|
58
|
+
# `lucide lucide-<name>` classes, so a file's own `<svg …>` would arrive as
|
|
59
|
+
# a second opinion about the thing the component already owns.
|
|
60
|
+
def self.drawing(source)
|
|
61
|
+
inside = source[%r{<svg\b[^>]*>(.*)</svg>}m, 1].to_s
|
|
62
|
+
|
|
63
|
+
inside.gsub(/>\s+</, "><").gsub(%r{\s+/>}, "/>").strip
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated from the vendored lucide SVGs (`vendor/lucide/icons`, taken from
|
|
4
|
+
# lucide-static at the version in `vendor/lucide/REVISION`) — do not edit by
|
|
5
|
+
# hand, run `rake icons:build`.
|
|
6
|
+
module ShadcnViewComponent
|
|
7
|
+
module Icons
|
|
8
|
+
PATHS = {
|
|
9
|
+
"arrow-left" => %(<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>),
|
|
10
|
+
"arrow-right" => %(<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>),
|
|
11
|
+
"check" => %(<path d="M20 6 9 17l-5-5"/>),
|
|
12
|
+
"chevron-down" => %(<path d="m6 9 6 6 6-6"/>),
|
|
13
|
+
"chevron-left" => %(<path d="m15 18-6-6 6-6"/>),
|
|
14
|
+
"chevron-right" => %(<path d="m9 18 6-6-6-6"/>),
|
|
15
|
+
"chevron-up" => %(<path d="m18 15-6-6-6 6"/>),
|
|
16
|
+
"circle-check" => %(<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>),
|
|
17
|
+
"circle" => %(<circle cx="12" cy="12" r="10"/>),
|
|
18
|
+
"ellipsis" => %(<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>),
|
|
19
|
+
"grip-vertical" => %(<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>),
|
|
20
|
+
"info" => %(<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>),
|
|
21
|
+
"loader-circle" => %(<path d="M21 12a9 9 0 1 1-6.219-8.56"/>),
|
|
22
|
+
"minus" => %(<path d="M5 12h14"/>),
|
|
23
|
+
"moon" => %(<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/>),
|
|
24
|
+
"octagon-x" => %(<path d="m15 9-6 6"/><path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"/><path d="m9 9 6 6"/>),
|
|
25
|
+
"panel-left" => %(<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/>),
|
|
26
|
+
"search" => %(<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>),
|
|
27
|
+
"sun" => %(<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>),
|
|
28
|
+
"triangle-alert" => %(<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>),
|
|
29
|
+
"x" => %(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>)
|
|
30
|
+
}.freeze
|
|
31
|
+
end
|
|
32
|
+
end
|