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,320 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { uniqueId } from "shadcn/id"
|
|
3
|
+
import { FloatingLayer } from "shadcn/floating"
|
|
4
|
+
import { Typeahead } from "shadcn/typeahead"
|
|
5
|
+
|
|
6
|
+
// Radix's Select: a `role="combobox"` trigger driving a `role="listbox"` layer.
|
|
7
|
+
//
|
|
8
|
+
// The chosen item's label is copied into `[data-slot=select-value]` and the
|
|
9
|
+
// value is mirrored onto a hidden input so the control submits with the form,
|
|
10
|
+
// exactly like Radix's BubbleSelect.
|
|
11
|
+
export default class extends Controller {
|
|
12
|
+
// `input` is the hidden field that submits with the form, which is why the
|
|
13
|
+
// search box is `search`.
|
|
14
|
+
static targets = [ "trigger", "content", "item", "value", "input", "search", "list", "empty",
|
|
15
|
+
"scrollUpButton", "scrollDownButton", "viewport" ]
|
|
16
|
+
static values = {
|
|
17
|
+
open: Boolean,
|
|
18
|
+
value: String,
|
|
19
|
+
placeholder: String,
|
|
20
|
+
side: { type: String, default: "bottom" },
|
|
21
|
+
align: { type: String, default: "center" },
|
|
22
|
+
sideOffset: { type: Number, default: 4 },
|
|
23
|
+
searchable: Boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
connect() {
|
|
27
|
+
if (!this.hasContentTarget || !this.hasTriggerTarget) return
|
|
28
|
+
|
|
29
|
+
this.typeahead = new Typeahead()
|
|
30
|
+
|
|
31
|
+
this.contentTarget.hidden = true
|
|
32
|
+
this.contentTarget.id ||= uniqueId("shadcn-select")
|
|
33
|
+
|
|
34
|
+
// The id is generated here, so the server has nothing to point this at.
|
|
35
|
+
this.triggerTarget.setAttribute("aria-controls", this.contentTarget.id)
|
|
36
|
+
this.triggerTarget.dataset.state = "closed"
|
|
37
|
+
|
|
38
|
+
if (this.searchableValue && this.hasSearchTarget && this.hasListTarget) {
|
|
39
|
+
this.listTarget.id ||= uniqueId("shadcn-select-list")
|
|
40
|
+
this.searchTarget.setAttribute("aria-controls", this.listTarget.id)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.layer = new FloatingLayer({
|
|
44
|
+
trigger: this.triggerTarget,
|
|
45
|
+
content: this.contentTarget,
|
|
46
|
+
prefix: "select",
|
|
47
|
+
side: this.sideValue,
|
|
48
|
+
align: this.alignValue,
|
|
49
|
+
sideOffset: this.sideOffsetValue,
|
|
50
|
+
matchAnchorWidth: true,
|
|
51
|
+
onOpen: () => {
|
|
52
|
+
this.triggerTarget.setAttribute("aria-expanded", "true")
|
|
53
|
+
// Radix resets here rather than on close (vendor/radix/ui/select.tsx:331-336),
|
|
54
|
+
// so characters typed before this panel was last dismissed cannot join
|
|
55
|
+
// the next search.
|
|
56
|
+
this.typeahead.reset()
|
|
57
|
+
// Focus goes to the search field, not the popover: a searchable select
|
|
58
|
+
// is typed into the moment it opens.
|
|
59
|
+
if (this.searchableValue && this.hasSearchTarget) {
|
|
60
|
+
this.searchTarget.focus({ preventScroll: true })
|
|
61
|
+
} else {
|
|
62
|
+
this.contentTarget.focus({ preventScroll: true })
|
|
63
|
+
}
|
|
64
|
+
this.highlight(this.selectedItem || this.enabledItems[0])
|
|
65
|
+
this.syncScrollButtons()
|
|
66
|
+
},
|
|
67
|
+
onClose: () => {
|
|
68
|
+
this.triggerTarget.setAttribute("aria-expanded", "false")
|
|
69
|
+
this.clearHighlight()
|
|
70
|
+
// Clearing through `search()` rather than by hand keeps one definition
|
|
71
|
+
// of what the list looks like for a given query — the empty state and
|
|
72
|
+
// the list's own `hidden` included.
|
|
73
|
+
if (this.searchableValue && this.hasSearchTarget) {
|
|
74
|
+
this.searchTarget.value = ""
|
|
75
|
+
this.search()
|
|
76
|
+
}
|
|
77
|
+
this.triggerTarget.focus({ preventScroll: true })
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
this.render()
|
|
82
|
+
if (this.openValue) this.layer.show()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
disconnect() {
|
|
86
|
+
this.stopAutoScroll()
|
|
87
|
+
if (this.layer) this.layer.destroy()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
toggle() {
|
|
91
|
+
this.layer.toggle()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
triggerKeydown(event) {
|
|
95
|
+
if (![ "ArrowDown", "ArrowUp", "Enter", " " ].includes(event.key)) return
|
|
96
|
+
|
|
97
|
+
event.preventDefault()
|
|
98
|
+
this.layer.show()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
contentKeydown(event) {
|
|
102
|
+
const items = this.enabledItems
|
|
103
|
+
const current = items.indexOf(this.highlighted)
|
|
104
|
+
|
|
105
|
+
switch (event.key) {
|
|
106
|
+
// Clamp rather than wrap: Radix's own keydown handler slices the
|
|
107
|
+
// candidate list from the current index and never wraps it back around
|
|
108
|
+
// (vendor/radix/ui/select.tsx:904's
|
|
109
|
+
// `candidateNodes = candidateNodes.slice(currentIndex + 1)`).
|
|
110
|
+
case "ArrowDown":
|
|
111
|
+
event.preventDefault()
|
|
112
|
+
this.highlight(items[Math.min(current + 1, items.length - 1)])
|
|
113
|
+
return
|
|
114
|
+
// Radix reverses the candidates for ArrowUp before slicing
|
|
115
|
+
// (vendor/radix/ui/select.tsx:898-904), so an index of -1 slices nothing
|
|
116
|
+
// off the reversed list and focus enters at the end. `onOpen` always
|
|
117
|
+
// highlights, which leaves one way to be here with items to move
|
|
118
|
+
// through: a selected item that is *disabled*, which is what
|
|
119
|
+
// `selectedItem` finds and what `enabledItems` leaves out.
|
|
120
|
+
case "ArrowUp":
|
|
121
|
+
event.preventDefault()
|
|
122
|
+
this.highlight(current === -1 ? items[items.length - 1] : items[Math.max(current - 1, 0)])
|
|
123
|
+
return
|
|
124
|
+
// Home and End move the caret when a search field has focus. Jumping the
|
|
125
|
+
// highlight instead would make the field uneditable at either end.
|
|
126
|
+
case "Home":
|
|
127
|
+
if (this.searchableValue) return
|
|
128
|
+
event.preventDefault()
|
|
129
|
+
this.highlight(items[0])
|
|
130
|
+
return
|
|
131
|
+
case "End":
|
|
132
|
+
if (this.searchableValue) return
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
this.highlight(items[items.length - 1])
|
|
135
|
+
return
|
|
136
|
+
case "Enter":
|
|
137
|
+
case " ":
|
|
138
|
+
// A space is a character while the search field has focus; Enter still
|
|
139
|
+
// chooses. Without this, typing a space picks an option and closes.
|
|
140
|
+
if (event.key === " " && this.searchableValue) return
|
|
141
|
+
if (!this.highlighted) return
|
|
142
|
+
event.preventDefault()
|
|
143
|
+
this.highlighted.click()
|
|
144
|
+
return
|
|
145
|
+
case "Tab":
|
|
146
|
+
event.preventDefault()
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The search field owns the characters when there is one; running the
|
|
151
|
+
// typeahead too would accumulate a second, invisible query alongside it.
|
|
152
|
+
if (!this.searchableValue && event.key.length === 1 && !event.metaKey && !event.ctrlKey) {
|
|
153
|
+
const match = this.typeahead.search(event.key, this.enabledItems, this.highlighted)
|
|
154
|
+
if (match) this.highlight(match)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// shadcn's aria variant filters on substring rather than prefix — "err"
|
|
159
|
+
// reaches Blueberry — and drops non-matching options from the DOM. Here they
|
|
160
|
+
// are hidden instead: the options are server-rendered ERB, and one removed
|
|
161
|
+
// cannot be put back.
|
|
162
|
+
search() {
|
|
163
|
+
const query = this.searchTarget.value.trim().toLowerCase()
|
|
164
|
+
|
|
165
|
+
this.itemTargets.forEach((item) => {
|
|
166
|
+
item.hidden = query !== "" && !item.textContent.trim().toLowerCase().includes(query)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const matches = this.enabledItems
|
|
170
|
+
if (this.hasListTarget) this.listTarget.hidden = matches.length === 0
|
|
171
|
+
if (this.hasEmptyTarget) this.emptyTarget.hidden = matches.length > 0
|
|
172
|
+
|
|
173
|
+
this.highlight(matches[0])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Radix mounts each button only while the viewport can scroll that way and
|
|
177
|
+
// recomputes on every scroll (vendor/radix/ui/select.tsx:1585, :1630-1634).
|
|
178
|
+
// `Math.ceil` is theirs too: zoomed in, `scrollTop` is not always an integer.
|
|
179
|
+
syncScrollButtons() {
|
|
180
|
+
if (!this.hasScrollUpButtonTarget || !this.hasScrollDownButtonTarget) return
|
|
181
|
+
|
|
182
|
+
const box = this.scrollContainer
|
|
183
|
+
const maxScroll = box.scrollHeight - box.clientHeight
|
|
184
|
+
|
|
185
|
+
this.scrollUpButtonTarget.hidden = box.scrollTop <= 0
|
|
186
|
+
this.scrollDownButtonTarget.hidden = Math.ceil(box.scrollTop) >= maxScroll
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Pointer only. Radix starts the same 50ms interval on pointerdown and on
|
|
190
|
+
// pointermove (vendor/radix/ui/select.tsx:1697-1706) — move as well as down,
|
|
191
|
+
// so hovering scrolls without pressing.
|
|
192
|
+
startAutoScroll(event) {
|
|
193
|
+
if (this.autoScrollTimer) return
|
|
194
|
+
|
|
195
|
+
const up = event.currentTarget === this.scrollUpButtonTarget
|
|
196
|
+
this.autoScrollTimer = setInterval(() => this.autoScroll(up ? -1 : 1), 50)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
stopAutoScroll() {
|
|
200
|
+
clearInterval(this.autoScrollTimer)
|
|
201
|
+
this.autoScrollTimer = null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
autoScroll(direction) {
|
|
205
|
+
// One item per tick, as Radix does: it steps by the selected item's
|
|
206
|
+
// offsetHeight (vendor/radix/ui/select.tsx:1601, :1647). With nothing
|
|
207
|
+
// selected there is still a list to measure.
|
|
208
|
+
const step = (this.selectedItem || this.itemTargets[0])?.offsetHeight || 0
|
|
209
|
+
|
|
210
|
+
this.scrollContainer.scrollTop += direction * step
|
|
211
|
+
this.syncScrollButtons()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
pointerenter(event) {
|
|
215
|
+
this.highlight(event.currentTarget)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
select(event) {
|
|
219
|
+
const item = event.currentTarget
|
|
220
|
+
if (item.dataset.disabled !== undefined) return
|
|
221
|
+
|
|
222
|
+
this.valueValue = item.dataset.value || ""
|
|
223
|
+
this.render()
|
|
224
|
+
this.layer.hide()
|
|
225
|
+
this.dispatch("change", { detail: { value: this.valueValue } })
|
|
226
|
+
|
|
227
|
+
if (this.hasInputTarget) {
|
|
228
|
+
this.inputTarget.dispatchEvent(new Event("change", { bubbles: true }))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- helpers -------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
// `!item.hidden` is what keeps the arrows, Home, End and `selectedItem` out
|
|
235
|
+
// of rows the filter has taken away. Nothing hides items unless `searchable`,
|
|
236
|
+
// so the plain select is unaffected.
|
|
237
|
+
// The viewport scrolls, as Radix's does (vendor/radix/ui/select.tsx:1247) —
|
|
238
|
+
// which is what keeps the scroll buttons pinned outside it. A searchable
|
|
239
|
+
// panel moves the scrolling one level further in, onto the list, so the
|
|
240
|
+
// search field stays put while the options move.
|
|
241
|
+
get scrollContainer() {
|
|
242
|
+
if (this.searchableValue && this.hasListTarget) return this.listTarget
|
|
243
|
+
|
|
244
|
+
return this.hasViewportTarget ? this.viewportTarget : this.contentTarget
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
get enabledItems() {
|
|
248
|
+
return this.itemTargets.filter((item) => item.dataset.disabled === undefined && !item.hidden)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
get selectedItem() {
|
|
252
|
+
return this.itemTargets.find((item) => (item.dataset.value || "") === this.valueValue) || null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get highlighted() {
|
|
256
|
+
return this.itemTargets.find((item) => item.dataset.highlighted !== undefined) || null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
highlight(item) {
|
|
260
|
+
this.clearHighlight()
|
|
261
|
+
if (!item) return
|
|
262
|
+
|
|
263
|
+
item.dataset.highlighted = ""
|
|
264
|
+
|
|
265
|
+
// Moving DOM focus would take it out of the search field and typing would
|
|
266
|
+
// stop, so a searchable select points at the item instead of focusing it.
|
|
267
|
+
if (this.searchableValue) {
|
|
268
|
+
// Assigned here rather than in `connect`, which only ever saw the items
|
|
269
|
+
// the server rendered. Anything added afterwards — a dependent select
|
|
270
|
+
// refilling its options when another one changes — arrived without an id,
|
|
271
|
+
// and `aria-activedescendant` then pointed at nothing: the highlight moved
|
|
272
|
+
// on screen and a screen reader followed none of it. Silent, and invisible
|
|
273
|
+
// to anyone not listening.
|
|
274
|
+
//
|
|
275
|
+
// `uniqueId` rather than `crypto.randomUUID()`, which is secure-context
|
|
276
|
+
// only and `undefined` over plain HTTP.
|
|
277
|
+
item.id ||= uniqueId("shadcn-select-item")
|
|
278
|
+
this.searchTarget.setAttribute("aria-activedescendant", item.id)
|
|
279
|
+
} else {
|
|
280
|
+
item.focus({ preventScroll: true })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
item.scrollIntoView({ block: "nearest" })
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
clearHighlight() {
|
|
287
|
+
this.itemTargets.forEach((item) => delete item.dataset.highlighted)
|
|
288
|
+
if (this.searchableValue && this.hasSearchTarget) {
|
|
289
|
+
this.searchTarget.removeAttribute("aria-activedescendant")
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
render() {
|
|
294
|
+
const selected = this.selectedItem
|
|
295
|
+
|
|
296
|
+
this.itemTargets.forEach((item) => {
|
|
297
|
+
const isSelected = item === selected
|
|
298
|
+
item.setAttribute("aria-selected", String(isSelected))
|
|
299
|
+
item.dataset.state = isSelected ? "checked" : "unchecked"
|
|
300
|
+
|
|
301
|
+
const indicator = item.querySelector("[data-slot='select-item-indicator'] > *")
|
|
302
|
+
if (indicator) indicator.hidden = !isSelected
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
if (this.hasValueTarget) {
|
|
306
|
+
this.valueTarget.textContent = selected
|
|
307
|
+
? selected.querySelector("[data-slot='select-item-text']")?.textContent.trim() ||
|
|
308
|
+
selected.textContent.trim()
|
|
309
|
+
: this.placeholderValue
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (selected) {
|
|
313
|
+
delete this.triggerTarget.dataset.placeholder
|
|
314
|
+
} else {
|
|
315
|
+
this.triggerTarget.dataset.placeholder = ""
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (this.hasInputTarget) this.inputTarget.value = this.valueValue
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { pushLayer, removeLayer } from "shadcn/dismiss"
|
|
3
|
+
import { trapFocus, focusFirst, lockScroll, unlockScroll } from "shadcn/focus"
|
|
4
|
+
import * as topLayer from "shadcn/top_layer"
|
|
5
|
+
import { ExitQueue } from "shadcn/animation"
|
|
6
|
+
|
|
7
|
+
// Upstream's own name and lifetime (vendor/shadcn/ui/sidebar.tsx:28-29).
|
|
8
|
+
const COOKIE = "sidebar_state"
|
|
9
|
+
const COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
|
10
|
+
const SHORTCUT = "b"
|
|
11
|
+
|
|
12
|
+
// Not a number this port chose: upstream's desktop tree carries `md:block`
|
|
13
|
+
// (vendor/shadcn/ui/sidebar.tsx:210), and `md` is 768px in Tailwind's default
|
|
14
|
+
// scale. Nothing had to be vendored to learn it.
|
|
15
|
+
const MOBILE_QUERY = "(max-width: 767px)"
|
|
16
|
+
|
|
17
|
+
// Radix has no Sidebar — shadcn builds it on its own React context
|
|
18
|
+
// (vendor/shadcn/ui/sidebar.tsx:56), so this controller has no Radix source to
|
|
19
|
+
// answer to, only shadcn's own.
|
|
20
|
+
//
|
|
21
|
+
// The state is small: expanded or collapsed on desktop, and a separate
|
|
22
|
+
// ephemeral flag on mobile that is deliberately never persisted.
|
|
23
|
+
export default class extends Controller {
|
|
24
|
+
static targets = [ "sidebar", "trigger", "container", "overlay", "sheetHeader" ]
|
|
25
|
+
static values = { open: Boolean, openMobile: Boolean }
|
|
26
|
+
|
|
27
|
+
connect() {
|
|
28
|
+
// Holds the DOM half of a close until the sheet's slide-out has played.
|
|
29
|
+
// Same module and the same reason as `dialog_controller.js`.
|
|
30
|
+
this.exits = new ExitQueue()
|
|
31
|
+
|
|
32
|
+
// Bound on `window` rather than on the element, so it fires wherever focus
|
|
33
|
+
// happens to be — which is what upstream does (sidebar.tsx:96-111), and the
|
|
34
|
+
// point of a shortcut. Removed again in `disconnect()`: Turbo tears
|
|
35
|
+
// controllers down on every navigation, and a listener left behind would
|
|
36
|
+
// accumulate one per visit.
|
|
37
|
+
this.onKeydown = (event) => {
|
|
38
|
+
if (event.key !== SHORTCUT) return
|
|
39
|
+
// A bare "b" is a character somebody may be typing.
|
|
40
|
+
if (!event.metaKey && !event.ctrlKey) return
|
|
41
|
+
|
|
42
|
+
event.preventDefault()
|
|
43
|
+
this.toggle()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
window.addEventListener("keydown", this.onKeydown)
|
|
47
|
+
|
|
48
|
+
// Feature-detected the way `top_layer.js` treats the Popover API: without
|
|
49
|
+
// matchMedia the desktop branch is the whole component.
|
|
50
|
+
this.media = typeof window.matchMedia === "function" ? window.matchMedia(MOBILE_QUERY) : null
|
|
51
|
+
this.onMediaChange = () => this.render()
|
|
52
|
+
this.media?.addEventListener("change", this.onMediaChange)
|
|
53
|
+
|
|
54
|
+
this.render()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
disconnect() {
|
|
58
|
+
window.removeEventListener("keydown", this.onKeydown)
|
|
59
|
+
this.media?.removeEventListener("change", this.onMediaChange)
|
|
60
|
+
this.closeMobile()
|
|
61
|
+
// Turbo may be detaching the element: there is nothing to animate for, and
|
|
62
|
+
// the deferred teardown still has to run.
|
|
63
|
+
this.exits.flushAll()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get isMobile() {
|
|
67
|
+
return this.media?.matches === true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
toggle() {
|
|
71
|
+
// Upstream branches here rather than in the state: mobile moves an
|
|
72
|
+
// ephemeral flag, desktop moves the persisted one
|
|
73
|
+
// (vendor/shadcn/ui/sidebar.tsx:92-95). A phone must not overwrite what the
|
|
74
|
+
// desktop remembered.
|
|
75
|
+
if (this.isMobile) {
|
|
76
|
+
this.openMobileValue = !this.openMobileValue
|
|
77
|
+
} else {
|
|
78
|
+
this.openValue = !this.openValue
|
|
79
|
+
this.persist()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.render()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Written, never read. A Rails layout reads it and passes the result back as
|
|
86
|
+
// the `open` value, the way a Next.js layout does with `defaultOpen`
|
|
87
|
+
// (vendor/shadcn/ui/sidebar.tsx:86) — which is the whole reason this is a
|
|
88
|
+
// cookie and not `localStorage`: the server has to be able to render the
|
|
89
|
+
// sidebar already collapsed, and on this component the alternative is a
|
|
90
|
+
// full-width layout shift on every page.
|
|
91
|
+
//
|
|
92
|
+
// `samesite=lax` is this port's one addition. Upstream's line carries no
|
|
93
|
+
// SameSite, and `theme.js` here already writes its cookie with it, so
|
|
94
|
+
// matching upstream character for character would leave one library
|
|
95
|
+
// disagreeing with itself.
|
|
96
|
+
//
|
|
97
|
+
// No `try`/`catch`: a blocked cookie assignment is a silent no-op, not an
|
|
98
|
+
// exception. `theme.js` wraps `localStorage`, which does throw in private
|
|
99
|
+
// browsing, and leaves its own cookie line outside the guard for this reason.
|
|
100
|
+
persist() {
|
|
101
|
+
document.cookie =
|
|
102
|
+
`${COOKIE}=${this.openValue}; path=/; max-age=${COOKIE_MAX_AGE}; samesite=lax`
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Named `render` because `index.js` re-runs exactly that on every `shadcn--*`
|
|
106
|
+
// controller after `turbo:morph`, which is what puts a controller back in
|
|
107
|
+
// agreement with markup the server has just rewritten underneath it.
|
|
108
|
+
render() {
|
|
109
|
+
if (!this.hasSidebarTarget) return
|
|
110
|
+
|
|
111
|
+
const sidebar = this.sidebarTarget
|
|
112
|
+
|
|
113
|
+
sidebar.dataset.state = this.openValue ? "expanded" : "collapsed"
|
|
114
|
+
|
|
115
|
+
// Upstream writes this empty while expanded and fills it only when
|
|
116
|
+
// collapsed (vendor/shadcn/ui/sidebar.tsx:212). The classes match on
|
|
117
|
+
// `group-data-[collapsible=offcanvas]:…`, so filling it always would style
|
|
118
|
+
// an open sidebar as a closed one. The value to collapse *to* therefore
|
|
119
|
+
// cannot live here; it lives in `data-sidebar-collapsible`.
|
|
120
|
+
//
|
|
121
|
+
// Empty on the mobile branch for a second reason: upstream's mobile tree
|
|
122
|
+
// has no collapsed state to carry (sidebar.tsx:182-204), and here the same
|
|
123
|
+
// element serves both, so a `collapsed` left over from the desktop would
|
|
124
|
+
// style the sheet as a rail of icons — or, with `offcanvas`, slide it off
|
|
125
|
+
// the screen it was just opened on.
|
|
126
|
+
sidebar.dataset.collapsible =
|
|
127
|
+
this.openValue || this.isMobile ? "" : (sidebar.dataset.sidebarCollapsible || "offcanvas")
|
|
128
|
+
|
|
129
|
+
if (this.isMobile && this.openMobileValue) this.openMobile()
|
|
130
|
+
else this.closeMobile()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Everything a Sheet does, applied to markup the server already sent. The
|
|
134
|
+
// element never moves, so the Stimulus actions on the links inside it stay
|
|
135
|
+
// bound — which is why this port does not build a Sheet and relocate the
|
|
136
|
+
// content into it, the way React renders a different tree.
|
|
137
|
+
// `dialog_controller.js` composes the same three modules.
|
|
138
|
+
openMobile() {
|
|
139
|
+
const sidebar = this.sidebarTarget
|
|
140
|
+
if (sidebar.dataset.mobile === "true") return
|
|
141
|
+
|
|
142
|
+
// Reopened before the slide-out finished: both teardowns are still queued
|
|
143
|
+
// and would undo an open sheet a moment after it appeared.
|
|
144
|
+
if (this.hasContainerTarget) this.exits.cancel(this.containerTarget)
|
|
145
|
+
if (this.hasOverlayTarget) this.exits.cancel(this.overlayTarget)
|
|
146
|
+
|
|
147
|
+
sidebar.dataset.mobile = "true"
|
|
148
|
+
this.announceSheet()
|
|
149
|
+
// Undoes upstream's `hidden … md:block`, which is CSS-hidden below the
|
|
150
|
+
// breakpoint because React renders a different tree there rather than
|
|
151
|
+
// showing this one. Inline beats the utility without having to out-specify
|
|
152
|
+
// `md:block` at every other width, and removing the property below restores
|
|
153
|
+
// the class exactly.
|
|
154
|
+
sidebar.style.display = "block"
|
|
155
|
+
|
|
156
|
+
// `sheet-content` and `sheet-overlay` are animated by their own
|
|
157
|
+
// `data-[state=…]` classes, exactly as upstream writes them; nothing else
|
|
158
|
+
// here reads these two attributes.
|
|
159
|
+
this.setSheetState("open")
|
|
160
|
+
|
|
161
|
+
topLayer.enable(sidebar)
|
|
162
|
+
topLayer.show(sidebar)
|
|
163
|
+
lockScroll()
|
|
164
|
+
|
|
165
|
+
// The panel, not the sheet: the overlay is a child of `sidebar`, so a layer
|
|
166
|
+
// registered on `sidebar` counts a click on the dimmed backdrop as a click
|
|
167
|
+
// *inside* itself and never dismisses. Upstream has no such problem — its
|
|
168
|
+
// overlay is the content's sibling, portalled beside it. Here the two share
|
|
169
|
+
// a parent, and the layer has to be the half that is not the backdrop.
|
|
170
|
+
const panel = this.hasContainerTarget ? this.containerTarget : sidebar
|
|
171
|
+
|
|
172
|
+
this.releaseFocus = trapFocus(panel, this.hasTriggerTarget ? this.triggerTarget : undefined)
|
|
173
|
+
focusFirst(panel)
|
|
174
|
+
this.layer = pushLayer({
|
|
175
|
+
element: panel,
|
|
176
|
+
// Without the trigger as an anchor, the very click that opens the sheet
|
|
177
|
+
// reaches the dismiss layer as an outside click and closes it again.
|
|
178
|
+
anchors: this.hasTriggerTarget ? [ this.triggerTarget ] : [],
|
|
179
|
+
onDismiss: () => {
|
|
180
|
+
this.openMobileValue = false
|
|
181
|
+
this.render()
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// A sheet is a modal dialog, and this one traps focus, locks the page's
|
|
187
|
+
// scroll and dims what is behind it while announcing nothing at all —
|
|
188
|
+
// upstream gets `role="dialog"`, `aria-modal` and a name for free by
|
|
189
|
+
// rendering a real Sheet below `md`, and this port has one tree instead.
|
|
190
|
+
//
|
|
191
|
+
// Applied here rather than in the markup because the server cannot know the
|
|
192
|
+
// viewport: on desktop this panel is not a dialog, and saying it is would be
|
|
193
|
+
// worse than saying nothing.
|
|
194
|
+
announceSheet() {
|
|
195
|
+
if (!this.hasContainerTarget) return
|
|
196
|
+
|
|
197
|
+
const panel = this.containerTarget
|
|
198
|
+
|
|
199
|
+
panel.setAttribute("role", "dialog")
|
|
200
|
+
panel.setAttribute("aria-modal", "true")
|
|
201
|
+
if (panel.dataset.sheetTitle) panel.setAttribute("aria-labelledby", panel.dataset.sheetTitle)
|
|
202
|
+
if (panel.dataset.sheetDescription) panel.setAttribute("aria-describedby", panel.dataset.sheetDescription)
|
|
203
|
+
if (this.hasSheetHeaderTarget) this.sheetHeaderTarget.hidden = false
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
silenceSheet() {
|
|
207
|
+
if (!this.hasContainerTarget) return
|
|
208
|
+
|
|
209
|
+
const panel = this.containerTarget
|
|
210
|
+
|
|
211
|
+
for (const name of [ "role", "aria-modal", "aria-labelledby", "aria-describedby" ]) {
|
|
212
|
+
panel.removeAttribute(name)
|
|
213
|
+
}
|
|
214
|
+
if (this.hasSheetHeaderTarget) this.sheetHeaderTarget.hidden = true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
closeMobile() {
|
|
218
|
+
if (!this.hasSidebarTarget) return
|
|
219
|
+
|
|
220
|
+
const sidebar = this.sidebarTarget
|
|
221
|
+
if (sidebar.dataset.mobile !== "true") return
|
|
222
|
+
|
|
223
|
+
// Interaction state goes now: a sheet that is sliding out must not answer
|
|
224
|
+
// Escape, keep the page's scroll locked or hold focus. Only what is still
|
|
225
|
+
// being *looked at* waits — the same split `floating.js` makes.
|
|
226
|
+
unlockScroll()
|
|
227
|
+
this.releaseFocus?.()
|
|
228
|
+
this.releaseFocus = null
|
|
229
|
+
if (this.layer) removeLayer(this.layer)
|
|
230
|
+
this.layer = null
|
|
231
|
+
|
|
232
|
+
this.setSheetState("closed")
|
|
233
|
+
|
|
234
|
+
// Each element waits on its *own* animations, as the dialog's layers do.
|
|
235
|
+
// Hiding the backdrop on the container's clock instead looks broken rather
|
|
236
|
+
// than merely late: nothing in the compiled bundle sets
|
|
237
|
+
// `animation-fill-mode`, so the moment `fade-out-0` ends the overlay snaps
|
|
238
|
+
// back to a full `bg-black/50` and sits there — the panel gone, a grey
|
|
239
|
+
// sheet of glass left over it — until the longer animation finishes.
|
|
240
|
+
// Upstream's two clocks differ the same way, 150ms against `duration-300`.
|
|
241
|
+
if (this.hasOverlayTarget) {
|
|
242
|
+
this.exits.defer(this.overlayTarget, () => { this.overlayTarget.hidden = true })
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// What ends the sheet is `data-mobile` going away — it is what
|
|
246
|
+
// `group-data-[mobile=true]:flex` reads — so it has to outlast the
|
|
247
|
+
// slide-out rather than start it.
|
|
248
|
+
const finish = () => {
|
|
249
|
+
this.silenceSheet()
|
|
250
|
+
delete sidebar.dataset.mobile
|
|
251
|
+
sidebar.style.removeProperty("display")
|
|
252
|
+
|
|
253
|
+
topLayer.hide(sidebar)
|
|
254
|
+
// Paired with the `enable` above, and not optional: this element is laid
|
|
255
|
+
// out around, so leaving it a popover leaves it `position: fixed` and the
|
|
256
|
+
// page draws over the desktop sidebar from then on.
|
|
257
|
+
topLayer.disable(sidebar)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (this.hasContainerTarget) this.exits.defer(this.containerTarget, finish)
|
|
261
|
+
else finish()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// The overlay is `hidden` in the server's markup and stays that way on
|
|
265
|
+
// desktop, where nothing ever opens a sheet.
|
|
266
|
+
setSheetState(state) {
|
|
267
|
+
if (this.hasContainerTarget) this.containerTarget.dataset.state = state
|
|
268
|
+
if (!this.hasOverlayTarget) return
|
|
269
|
+
|
|
270
|
+
if (state === "open") this.overlayTarget.hidden = false
|
|
271
|
+
this.overlayTarget.dataset.state = state
|
|
272
|
+
}
|
|
273
|
+
}
|