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,456 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import {
|
|
3
|
+
getElementScrollTop,
|
|
4
|
+
getElementViewportTop,
|
|
5
|
+
getFirstVisibleMessageItem,
|
|
6
|
+
getFlexGap,
|
|
7
|
+
getMaxScrollTop,
|
|
8
|
+
getMessageScrollerItems,
|
|
9
|
+
getMessageScrollerScrollable,
|
|
10
|
+
getNewScrollAnchor,
|
|
11
|
+
getTailSpacerHeight,
|
|
12
|
+
getUnanchoredScrollAnchor,
|
|
13
|
+
hasMultipleNewScrollAnchors
|
|
14
|
+
} from "shadcn/scroll_geometry"
|
|
15
|
+
|
|
16
|
+
// A chat log that follows its own live end, ported from
|
|
17
|
+
// `vendor/shadcn-react/message-scroller/` — shadcn's own primitive rather than
|
|
18
|
+
// a Radix one, which is why this controller has a vendored source to answer to
|
|
19
|
+
// at all.
|
|
20
|
+
//
|
|
21
|
+
// Two of upstream's surfaces are deliberately absent, measured and argued in
|
|
22
|
+
// `.claude/docs/features/message-scroller.md`: the visibility store, and
|
|
23
|
+
// `scrollToMessage`.
|
|
24
|
+
|
|
25
|
+
// Upstream's `AUTOSCROLLING_CLEAR_DELAY` (types.ts:22): how long
|
|
26
|
+
// `data-autoscrolling` outlives a programmatic smooth scroll.
|
|
27
|
+
const AUTOSCROLLING_CLEAR_DELAY = 180
|
|
28
|
+
// Upstream's `SCROLL_POSITION_EPSILON` (types.ts:18). Sub-pixel scroll
|
|
29
|
+
// positions are normal, so "the same place" needs a tolerance.
|
|
30
|
+
const EPSILON = 0.5
|
|
31
|
+
|
|
32
|
+
export default class extends Controller {
|
|
33
|
+
static targets = [ "viewport", "content", "spacer", "button" ]
|
|
34
|
+
static values = {
|
|
35
|
+
autoScroll: { type: Boolean, default: false },
|
|
36
|
+
defaultScrollPosition: { type: String, default: "end" },
|
|
37
|
+
// The distance from an edge that still counts as being at it. Upstream
|
|
38
|
+
// leaves this undefined and falls back inside the primitive; the fallback
|
|
39
|
+
// is here instead, which is the only place it can be in a Stimulus value.
|
|
40
|
+
scrollEdgeThreshold: { type: Number, default: 8 },
|
|
41
|
+
scrollMargin: { type: Number, default: 0 },
|
|
42
|
+
// How much of the previous turn stays visible above an anchored one, so it
|
|
43
|
+
// reads as a continuation rather than as the top of the world.
|
|
44
|
+
scrollPreviousItemPeek: { type: Number, default: 64 },
|
|
45
|
+
// Upstream hangs this off the *viewport* (components.tsx:131). Here every
|
|
46
|
+
// option is a value on the root, because the element that would have
|
|
47
|
+
// carried them — the Provider — renders no DOM at all.
|
|
48
|
+
preserveScrollOnPrepend: { type: Boolean, default: true }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
connect() {
|
|
52
|
+
if (!this.hasViewportTarget || !this.hasContentTarget) return
|
|
53
|
+
|
|
54
|
+
// `following-bottom` or `free-scrolling`. Upstream carries two more —
|
|
55
|
+
// `settling-jump` and `anchored-to-message` — which only the anchoring
|
|
56
|
+
// slice can enter.
|
|
57
|
+
this.mode = this.autoScrollValue ? "following-bottom" : "free-scrolling"
|
|
58
|
+
this.autoscrolling = false
|
|
59
|
+
this.lastScrollTop = 0
|
|
60
|
+
this.spacerHeight = 0
|
|
61
|
+
this.frame = null
|
|
62
|
+
this.autoscrollingTimer = null
|
|
63
|
+
|
|
64
|
+
// What a content change is measured against. `handledAnchors` is a WeakSet
|
|
65
|
+
// so an anchor removed from the DOM stops being remembered along with it.
|
|
66
|
+
const items = this.items()
|
|
67
|
+
|
|
68
|
+
this.itemCount = items.length
|
|
69
|
+
this.firstItem = items[0] ?? null
|
|
70
|
+
this.prependAnchor = null
|
|
71
|
+
|
|
72
|
+
// Anchors already in the markup count as handled, and this is the one place
|
|
73
|
+
// server rendering forces a difference from upstream rather than a
|
|
74
|
+
// translation of it. React mounts this component empty and fills it, so its
|
|
75
|
+
// first content change takes the `previousItemCount === 0` branch, goes to
|
|
76
|
+
// the end, and never jumps to an anchor that was there from the start.
|
|
77
|
+
// Measured on the live demo: at rest the tail spacer is hidden and the
|
|
78
|
+
// viewport sits at the very end, anchored last turn or not.
|
|
79
|
+
//
|
|
80
|
+
// Here the rows arrive with the document. Without this the first observer
|
|
81
|
+
// finds an unhandled anchor and takes the reader to it, so a conversation
|
|
82
|
+
// opens part-way up with a screenful of tail spacer below — which upstream
|
|
83
|
+
// never shows. `scroll_anchor` keeps its meaning for turns that *arrive*.
|
|
84
|
+
this.handledAnchors = new WeakSet()
|
|
85
|
+
for (const item of items) {
|
|
86
|
+
if (item.dataset.scrollAnchor === "true") this.handledAnchors.add(item)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.onScroll = () => this.scheduleStateCommit()
|
|
90
|
+
this.viewportTarget.addEventListener("scroll", this.onScroll, { passive: true })
|
|
91
|
+
|
|
92
|
+
// Content changing is a *DOM* event here, exactly as it is upstream: the
|
|
93
|
+
// React version does not learn about new messages from re-rendering, it
|
|
94
|
+
// observes them (components.tsx). So the mechanism transfers unchanged —
|
|
95
|
+
// rows arriving, and rows growing as they stream.
|
|
96
|
+
this.mutations = new MutationObserver(() => this.handleContentChange())
|
|
97
|
+
this.mutations.observe(this.contentTarget, { childList: true, subtree: true, characterData: true })
|
|
98
|
+
|
|
99
|
+
this.resizes = new ResizeObserver(() => this.handleResize())
|
|
100
|
+
this.resizes.observe(this.contentTarget)
|
|
101
|
+
this.resizes.observe(this.viewportTarget)
|
|
102
|
+
|
|
103
|
+
this.applyDefaultScrollPosition()
|
|
104
|
+
this.commitScrollState()
|
|
105
|
+
this.capturePrependAnchor()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
disconnect() {
|
|
109
|
+
this.viewportTarget?.removeEventListener("scroll", this.onScroll)
|
|
110
|
+
this.mutations?.disconnect()
|
|
111
|
+
this.resizes?.disconnect()
|
|
112
|
+
if (this.frame) cancelAnimationFrame(this.frame)
|
|
113
|
+
if (this.autoscrollingTimer) clearTimeout(this.autoscrollingTimer)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// What the two buttons call. Which end is its own `data-direction`, so one
|
|
117
|
+
// action serves both.
|
|
118
|
+
jump(event) {
|
|
119
|
+
const button = event.currentTarget
|
|
120
|
+
|
|
121
|
+
if (button.dataset.direction === "start") this.scrollToStart({ behavior: "smooth" })
|
|
122
|
+
else this.scrollToEnd({ behavior: "smooth" })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------- state
|
|
126
|
+
|
|
127
|
+
// `data-scrollable` and `data-autoscrolling` go on the root *and* the
|
|
128
|
+
// viewport, as upstream writes them (:107-131) — the viewport needs the
|
|
129
|
+
// second for `data-autoscrolling:scrollbar-none`, and the root carries both
|
|
130
|
+
// so a host can style from either.
|
|
131
|
+
writeStateAttributes(state) {
|
|
132
|
+
const scrollable = [ state.start && "start", state.end && "end" ].filter(Boolean).join(" ")
|
|
133
|
+
|
|
134
|
+
for (const element of [ this.element, this.viewportTarget ]) {
|
|
135
|
+
if (scrollable) element.setAttribute("data-scrollable", scrollable)
|
|
136
|
+
else element.removeAttribute("data-scrollable")
|
|
137
|
+
|
|
138
|
+
element.toggleAttribute("data-autoscrolling", this.autoscrolling)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.hasButtonTarget) {
|
|
142
|
+
for (const button of this.buttonTargets) {
|
|
143
|
+
const active = button.dataset.direction === "start" ? state.start : state.end
|
|
144
|
+
|
|
145
|
+
button.dataset.active = String(active)
|
|
146
|
+
// In step with `data-active`, never behind it: the button is invisible
|
|
147
|
+
// and `pointer-events-none` while inactive, so a tab stop there is one
|
|
148
|
+
// a pointer does not have.
|
|
149
|
+
if (active) button.removeAttribute("tabindex")
|
|
150
|
+
else button.setAttribute("tabindex", "-1")
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The one place `mode` changes. Content growing past the live edge also reads
|
|
156
|
+
// as "not at the end", so following must only be released by the reader
|
|
157
|
+
// actually scrolling *up* — which is the only thing that lowers `scrollTop`.
|
|
158
|
+
reconcileFollowMode(scrollable) {
|
|
159
|
+
const scrollTop = this.viewportTarget.scrollTop
|
|
160
|
+
const scrolledUp = scrollTop < this.lastScrollTop - EPSILON
|
|
161
|
+
|
|
162
|
+
this.lastScrollTop = scrollTop
|
|
163
|
+
|
|
164
|
+
// Arming is suppressed while anchored or settling, and that is the subtle
|
|
165
|
+
// half. A freshly anchored turn sits above a tail spacer, which makes it
|
|
166
|
+
// read as "at the end" — re-arming there would let the first streamed chunk
|
|
167
|
+
// pull the reader off the row they were just taken to.
|
|
168
|
+
if (
|
|
169
|
+
this.autoScrollValue &&
|
|
170
|
+
!scrollable.end &&
|
|
171
|
+
this.mode !== "settling-jump" &&
|
|
172
|
+
this.mode !== "anchored-to-message"
|
|
173
|
+
) {
|
|
174
|
+
this.mode = "following-bottom"
|
|
175
|
+
} else if (this.mode === "following-bottom" && scrollable.end && scrolledUp && !this.autoscrolling) {
|
|
176
|
+
this.mode = "free-scrolling"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
commitScrollState() {
|
|
181
|
+
const state = this.scrollable()
|
|
182
|
+
|
|
183
|
+
this.reconcileFollowMode(state)
|
|
184
|
+
|
|
185
|
+
// While following, the scroller is already closing whatever gap a streamed
|
|
186
|
+
// chunk just opened, so publishing that gap would strobe the end button
|
|
187
|
+
// once per chunk. Reconcile runs on the raw geometry first, so a commit
|
|
188
|
+
// that *releases* following still publishes the gap it released over.
|
|
189
|
+
this.writeStateAttributes(
|
|
190
|
+
this.mode === "following-bottom" ? { ...state, end: false } : state
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
scheduleStateCommit() {
|
|
195
|
+
if (this.frame !== null) return
|
|
196
|
+
|
|
197
|
+
this.frame = requestAnimationFrame(() => {
|
|
198
|
+
this.frame = null
|
|
199
|
+
this.commitScrollState()
|
|
200
|
+
// Re-captured after every settled scroll, not only after a content
|
|
201
|
+
// change: the anchor is "the row you are looking at", and scrolling is
|
|
202
|
+
// what changes which row that is. Leaving it behind makes the next
|
|
203
|
+
// prepend correct by a delta measured from somewhere you have left —
|
|
204
|
+
// which is a wilder jump than doing nothing at all.
|
|
205
|
+
this.capturePrependAnchor()
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
scrollable() {
|
|
210
|
+
return getMessageScrollerScrollable({
|
|
211
|
+
content: this.contentTarget,
|
|
212
|
+
scrollEdgeThreshold: this.scrollEdgeThresholdValue,
|
|
213
|
+
spacer: this.spacer,
|
|
214
|
+
viewport: this.viewportTarget
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// --------------------------------------------------------------- moving
|
|
219
|
+
|
|
220
|
+
// Set while a programmatic scroll is in flight and cleared on a timer, not on
|
|
221
|
+
// an event: `scrollend` is not everywhere yet, and the flag's job is to stop
|
|
222
|
+
// the animation's own scroll events from releasing follow mode.
|
|
223
|
+
setAutoScrolling(autoscrolling) {
|
|
224
|
+
if (this.autoscrollingTimer) {
|
|
225
|
+
clearTimeout(this.autoscrollingTimer)
|
|
226
|
+
this.autoscrollingTimer = null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (this.autoscrolling !== autoscrolling) {
|
|
230
|
+
this.autoscrolling = autoscrolling
|
|
231
|
+
this.commitScrollState()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!autoscrolling) return
|
|
235
|
+
|
|
236
|
+
this.autoscrollingTimer = setTimeout(() => {
|
|
237
|
+
this.autoscrollingTimer = null
|
|
238
|
+
this.autoscrolling = false
|
|
239
|
+
this.commitScrollState()
|
|
240
|
+
}, AUTOSCROLLING_CLEAR_DELAY)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
scrollToPosition(scrollTop, { behavior = "auto", autoscrolling = false } = {}) {
|
|
244
|
+
const next = Math.max(0, scrollTop)
|
|
245
|
+
|
|
246
|
+
// Already there: assign rather than animate, or a smooth scroll to where we
|
|
247
|
+
// stand never fires and the commit never comes.
|
|
248
|
+
if (Math.abs(this.viewportTarget.scrollTop - next) <= EPSILON) {
|
|
249
|
+
this.viewportTarget.scrollTop = next
|
|
250
|
+
this.commitScrollState()
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (autoscrolling) this.setAutoScrolling(true)
|
|
255
|
+
|
|
256
|
+
this.viewportTarget.scrollTo({ top: next, behavior })
|
|
257
|
+
this.scheduleStateCommit()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
scrollToStart({ behavior = "auto" } = {}) {
|
|
261
|
+
this.setTailSpacerHeight(0)
|
|
262
|
+
this.mode = "free-scrolling"
|
|
263
|
+
this.scrollToPosition(0, { behavior })
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
scrollToEnd({ behavior = "auto" } = {}) {
|
|
267
|
+
this.setTailSpacerHeight(0)
|
|
268
|
+
this.mode = this.autoScrollValue ? "following-bottom" : "free-scrolling"
|
|
269
|
+
this.scrollToPosition(getMaxScrollTop(this.viewportTarget), { autoscrolling: true, behavior })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// The spacer under the last row, sized so that row can reach the *top* of the
|
|
273
|
+
// viewport instead of stopping at the bottom. The negative margin cancels the
|
|
274
|
+
// content column's own gap, which would otherwise be counted twice — once as
|
|
275
|
+
// the gap above the spacer and once as the height asked for here.
|
|
276
|
+
setTailSpacerHeight(height) {
|
|
277
|
+
if (!this.hasSpacerTarget) return
|
|
278
|
+
|
|
279
|
+
const next = Math.max(0, Math.ceil(height))
|
|
280
|
+
if (this.spacerHeight === next) return
|
|
281
|
+
|
|
282
|
+
this.spacerHeight = next
|
|
283
|
+
this.spacerTarget.hidden = next === 0
|
|
284
|
+
this.spacerTarget.style.height = `${next}px`
|
|
285
|
+
this.spacerTarget.style.marginTop = next > 0 ? `${-getFlexGap(this.contentTarget)}px` : ""
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Take a row to the top of the viewport, sizing the tail spacer so it can
|
|
289
|
+
// actually get there — without it, the last messages stop at the bottom.
|
|
290
|
+
// `keepPreviousPeek` leaves the previous turn showing above, which is what
|
|
291
|
+
// makes an arriving turn read as a continuation.
|
|
292
|
+
scrollToElement(element, { align = "start", behavior = "auto", keepPreviousPeek = false } = {}) {
|
|
293
|
+
if (!this.contentTarget.contains(element)) return false
|
|
294
|
+
|
|
295
|
+
const scrollMargin = keepPreviousPeek
|
|
296
|
+
? this.scrollMarginValue + this.scrollPreviousItemPeekValue
|
|
297
|
+
: this.scrollMarginValue
|
|
298
|
+
|
|
299
|
+
const scrollTop = getElementScrollTop({
|
|
300
|
+
align, element, scrollMargin, spacer: this.spacer, viewport: this.viewportTarget
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
this.setTailSpacerHeight(this.tailSpacerFor(scrollTop))
|
|
304
|
+
|
|
305
|
+
// Seed the prepend anchor with the jump target, so a prepend landing before
|
|
306
|
+
// this scroll settles still preserves the row we were taken to.
|
|
307
|
+
this.prependAnchor = {
|
|
308
|
+
element,
|
|
309
|
+
viewportTop: getElementViewportTop(element, this.viewportTarget)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.mode = keepPreviousPeek ? "anchored-to-message" : "settling-jump"
|
|
313
|
+
this.scrollToPosition(scrollTop, { behavior })
|
|
314
|
+
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ------------------------------------------------------------- prepend
|
|
319
|
+
|
|
320
|
+
// The first visible row and where it sits *relative to the viewport*. That
|
|
321
|
+
// frame of reference is the whole trick: it is what survives rows being
|
|
322
|
+
// inserted above it.
|
|
323
|
+
capturePrependAnchor() {
|
|
324
|
+
const anchor = getFirstVisibleMessageItem({
|
|
325
|
+
content: this.contentTarget,
|
|
326
|
+
spacer: this.spacer,
|
|
327
|
+
viewport: this.viewportTarget
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
this.prependAnchor = anchor
|
|
331
|
+
? { element: anchor, viewportTop: getElementViewportTop(anchor, this.viewportTarget) }
|
|
332
|
+
: null
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Correct the scroll by however far the remembered row moved. Where the
|
|
336
|
+
// browser's own scroll anchoring already handled the prepend the delta is
|
|
337
|
+
// zero and this does nothing — which is why it corrects a measurement rather
|
|
338
|
+
// than checking a capability flag, since engines misreport those.
|
|
339
|
+
restorePrependedAnchor() {
|
|
340
|
+
const anchor = this.prependAnchor
|
|
341
|
+
|
|
342
|
+
if (!anchor || !anchor.element.isConnected) return false
|
|
343
|
+
|
|
344
|
+
const delta = getElementViewportTop(anchor.element, this.viewportTarget) - anchor.viewportTop
|
|
345
|
+
|
|
346
|
+
if (Math.abs(delta) <= EPSILON) return false
|
|
347
|
+
|
|
348
|
+
this.viewportTarget.scrollTop += delta
|
|
349
|
+
anchor.viewportTop = getElementViewportTop(anchor.element, this.viewportTarget)
|
|
350
|
+
this.scheduleStateCommit()
|
|
351
|
+
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ---------------------------------------------------------- reacting
|
|
356
|
+
|
|
357
|
+
// Branch order is load-bearing, and it is upstream's (:393-481): a prepend is
|
|
358
|
+
// not an append, and an arriving turn is not a row that merely grew.
|
|
359
|
+
handleContentChange() {
|
|
360
|
+
const items = this.items()
|
|
361
|
+
const previousCount = this.itemCount
|
|
362
|
+
const previousFirst = this.firstItem
|
|
363
|
+
|
|
364
|
+
this.itemCount = items.length
|
|
365
|
+
this.firstItem = items[0] ?? null
|
|
366
|
+
|
|
367
|
+
this.reconcileScrollPosition(items, previousCount, previousFirst)
|
|
368
|
+
this.capturePrependAnchor()
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
reconcileScrollPosition(items, previousCount, previousFirst) {
|
|
372
|
+
if (previousCount === 0) {
|
|
373
|
+
if (items.length > 0 && this.autoScrollValue) return this.scrollToEnd()
|
|
374
|
+
|
|
375
|
+
return this.scheduleStateCommit()
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Rows arrived *above* the ones already there, which is what loading older
|
|
379
|
+
// history looks like. Hold the view still rather than treating them as new.
|
|
380
|
+
const previousFirstIndex = previousFirst ? items.indexOf(previousFirst) : -1
|
|
381
|
+
|
|
382
|
+
if (this.preserveScrollOnPrependValue && previousFirstIndex > 0) {
|
|
383
|
+
return this.restorePrependedAnchor()
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (items.length > previousCount) {
|
|
387
|
+
const anchor = getNewScrollAnchor(items, previousCount)
|
|
388
|
+
|
|
389
|
+
if (anchor) {
|
|
390
|
+
// A batch of several anchored turns landing at once should keep
|
|
391
|
+
// following the end rather than yank back to anchor the first of them.
|
|
392
|
+
if (
|
|
393
|
+
this.autoScrollValue &&
|
|
394
|
+
this.mode === "following-bottom" &&
|
|
395
|
+
hasMultipleNewScrollAnchors(items, previousCount)
|
|
396
|
+
) {
|
|
397
|
+
return this.scrollToEnd()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
this.scrollToElement(anchor, { keepPreviousPeek: true })
|
|
401
|
+
this.handledAnchors.add(anchor)
|
|
402
|
+
return true
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Same rows, but one of them just became an anchor — a turn marked after it
|
|
407
|
+
// was already on the page.
|
|
408
|
+
if (items.length === previousCount) {
|
|
409
|
+
const anchor = getUnanchoredScrollAnchor(items, this.handledAnchors)
|
|
410
|
+
|
|
411
|
+
if (anchor) {
|
|
412
|
+
this.scrollToElement(anchor, { keepPreviousPeek: true })
|
|
413
|
+
this.handledAnchors.add(anchor)
|
|
414
|
+
return true
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (this.mode === "following-bottom" && this.autoScrollValue) return this.scrollToEnd()
|
|
419
|
+
|
|
420
|
+
return this.scheduleStateCommit()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
items() {
|
|
424
|
+
return getMessageScrollerItems(this.contentTarget, this.spacer)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// A resize is not a content change: rows growing as they stream arrive here
|
|
428
|
+
// rather than through the mutation observer, and the answer is the same one.
|
|
429
|
+
handleResize() {
|
|
430
|
+
this.handleContentChange()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
applyDefaultScrollPosition() {
|
|
434
|
+
if (this.defaultScrollPositionValue === "start") {
|
|
435
|
+
this.scrollToPosition(0)
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
this.scrollToPosition(getMaxScrollTop(this.viewportTarget))
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// `hasSpacerTarget` guards every caller, so this can hand back null and let
|
|
443
|
+
// the geometry treat the column as spacer-less.
|
|
444
|
+
get spacer() {
|
|
445
|
+
return this.hasSpacerTarget ? this.spacerTarget : null
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
tailSpacerFor(scrollTop) {
|
|
449
|
+
return getTailSpacerHeight({
|
|
450
|
+
content: this.contentTarget,
|
|
451
|
+
scrollTop,
|
|
452
|
+
spacer: this.spacer,
|
|
453
|
+
viewport: this.viewportTarget
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { pushLayer, removeLayer } from "shadcn/dismiss"
|
|
3
|
+
import { directionAwareKey, readDirection } from "shadcn/direction"
|
|
4
|
+
|
|
5
|
+
// Radix's NavigationMenu (vendor/radix/ui/navigation-menu.tsx), in the
|
|
6
|
+
// `viewport={false}` configuration — see the root component for why that is the
|
|
7
|
+
// only one this gem can reproduce.
|
|
8
|
+
//
|
|
9
|
+
// One panel is open at a time and it belongs to a trigger. Everything else here
|
|
10
|
+
// is about *when*: a menu that opened the instant a pointer crossed it would
|
|
11
|
+
// flash panels at anyone sweeping past, and one that always waited would feel
|
|
12
|
+
// stuck once you were already inside it.
|
|
13
|
+
export default class extends Controller {
|
|
14
|
+
static targets = [ "trigger", "content" ]
|
|
15
|
+
static values = {
|
|
16
|
+
// Radix's own (navigation-menu.tsx:136-137).
|
|
17
|
+
delay: { type: Number, default: 200 },
|
|
18
|
+
skipDelay: { type: Number, default: 300 }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
connect() {
|
|
22
|
+
this.open = null
|
|
23
|
+
this.timer = null
|
|
24
|
+
this.skipTimer = null
|
|
25
|
+
// True for `skipDelay` after a panel closes: while it is, the next trigger
|
|
26
|
+
// opens with no wait at all. Sweeping across the row should feel like one
|
|
27
|
+
// gesture, not like several.
|
|
28
|
+
this.skipping = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
disconnect() {
|
|
32
|
+
this.cancel()
|
|
33
|
+
if (this.skipTimer) clearTimeout(this.skipTimer)
|
|
34
|
+
if (this.layer) removeLayer(this.layer)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ------------------------------------------------------------- pointing
|
|
38
|
+
|
|
39
|
+
pointerEnter(event) {
|
|
40
|
+
if (event.pointerType === "touch") return
|
|
41
|
+
|
|
42
|
+
this.cancel()
|
|
43
|
+
|
|
44
|
+
const trigger = this.triggerFor(event.currentTarget)
|
|
45
|
+
if (!trigger || trigger === this.open) return
|
|
46
|
+
|
|
47
|
+
const wait = this.skipping || this.open ? 0 : this.delayValue
|
|
48
|
+
this.timer = setTimeout(() => this.show(trigger), wait)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Leaving anywhere starts a close the next enter can cancel — which is what
|
|
52
|
+
// lets the pointer cross the gap between a trigger and its panel.
|
|
53
|
+
pointerLeave(event) {
|
|
54
|
+
if (event.pointerType === "touch") return
|
|
55
|
+
|
|
56
|
+
this.cancel()
|
|
57
|
+
this.timer = setTimeout(() => this.close(), this.delayValue)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A tap has no hover to work with, so it toggles.
|
|
61
|
+
toggle(event) {
|
|
62
|
+
const trigger = this.triggerFor(event.currentTarget)
|
|
63
|
+
if (!trigger) return
|
|
64
|
+
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
this.cancel()
|
|
67
|
+
|
|
68
|
+
if (trigger === this.open) this.close({ focusTrigger: true })
|
|
69
|
+
else this.show(trigger)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ------------------------------------------------------------- keyboard
|
|
73
|
+
|
|
74
|
+
keydown(event) {
|
|
75
|
+
const key = directionAwareKey(event.key, readDirection(this.element))
|
|
76
|
+
const triggers = this.triggerTargets
|
|
77
|
+
const index = triggers.indexOf(event.currentTarget)
|
|
78
|
+
|
|
79
|
+
switch (key) {
|
|
80
|
+
case "ArrowRight":
|
|
81
|
+
event.preventDefault()
|
|
82
|
+
triggers[(index + 1) % triggers.length].focus()
|
|
83
|
+
return
|
|
84
|
+
case "ArrowLeft":
|
|
85
|
+
event.preventDefault()
|
|
86
|
+
triggers[(index - 1 + triggers.length) % triggers.length].focus()
|
|
87
|
+
return
|
|
88
|
+
case "Home":
|
|
89
|
+
event.preventDefault()
|
|
90
|
+
triggers[0].focus()
|
|
91
|
+
return
|
|
92
|
+
case "End":
|
|
93
|
+
event.preventDefault()
|
|
94
|
+
triggers[triggers.length - 1].focus()
|
|
95
|
+
return
|
|
96
|
+
// Down opens and steps into the panel, which is the only way a keyboard
|
|
97
|
+
// reaches the links: the panel is not in the tab order behind its
|
|
98
|
+
// trigger, it is under it.
|
|
99
|
+
case "ArrowDown":
|
|
100
|
+
event.preventDefault()
|
|
101
|
+
this.show(event.currentTarget)
|
|
102
|
+
this.focusFirstLink(event.currentTarget)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
focusFirstLink(trigger) {
|
|
107
|
+
const link = this.contentFor(trigger)?.querySelector("[data-slot=navigation-menu-link]")
|
|
108
|
+
|
|
109
|
+
link?.focus()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// -------------------------------------------------------------- opening
|
|
113
|
+
|
|
114
|
+
show(trigger) {
|
|
115
|
+
if (trigger === this.open) return
|
|
116
|
+
|
|
117
|
+
const previous = this.open
|
|
118
|
+
|
|
119
|
+
this.hideContent(previous, this.motionBetween(previous, trigger, "to"))
|
|
120
|
+
this.open = trigger
|
|
121
|
+
|
|
122
|
+
trigger.dataset.state = "open"
|
|
123
|
+
trigger.setAttribute("aria-expanded", "true")
|
|
124
|
+
|
|
125
|
+
const content = this.contentFor(trigger)
|
|
126
|
+
if (content) {
|
|
127
|
+
content.dataset.motion = this.motionBetween(previous, trigger, "from")
|
|
128
|
+
content.hidden = false
|
|
129
|
+
content.dataset.state = "open"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this.layer) removeLayer(this.layer)
|
|
133
|
+
this.layer = pushLayer({
|
|
134
|
+
element: content || trigger,
|
|
135
|
+
anchors: this.triggerTargets,
|
|
136
|
+
// Escape and an outside click both arrive here, and only one of them
|
|
137
|
+
// should move focus: dismissing with a key leaves you where the keyboard
|
|
138
|
+
// can carry on, dismissing with a pointer leaves you where you clicked.
|
|
139
|
+
// Handled in the layer rather than in a listener of this controller's,
|
|
140
|
+
// because `dismiss.js` answers Escape first and a second path would find
|
|
141
|
+
// the menu already closed.
|
|
142
|
+
onDismiss: ({ reason }) => this.close({ focusTrigger: reason === "escape" })
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
close({ focusTrigger = false } = {}) {
|
|
147
|
+
const trigger = this.open
|
|
148
|
+
if (!trigger) return
|
|
149
|
+
|
|
150
|
+
this.hideContent(trigger, null)
|
|
151
|
+
this.open = null
|
|
152
|
+
|
|
153
|
+
if (this.layer) removeLayer(this.layer)
|
|
154
|
+
this.layer = null
|
|
155
|
+
|
|
156
|
+
if (focusTrigger) trigger.focus()
|
|
157
|
+
|
|
158
|
+
// The grace period. Anything entered inside it opens at once.
|
|
159
|
+
this.skipping = true
|
|
160
|
+
if (this.skipTimer) clearTimeout(this.skipTimer)
|
|
161
|
+
this.skipTimer = setTimeout(() => { this.skipping = false }, this.skipDelayValue)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
hideContent(trigger, motion) {
|
|
165
|
+
if (!trigger) return
|
|
166
|
+
|
|
167
|
+
trigger.dataset.state = "closed"
|
|
168
|
+
trigger.setAttribute("aria-expanded", "false")
|
|
169
|
+
|
|
170
|
+
const content = this.contentFor(trigger)
|
|
171
|
+
if (!content) return
|
|
172
|
+
|
|
173
|
+
if (motion) content.dataset.motion = motion
|
|
174
|
+
content.dataset.state = "closed"
|
|
175
|
+
content.hidden = true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Which side the panel comes from, so the pair slides rather than swapping.
|
|
179
|
+
// `null` when there is nothing to come from — a first open is not a move.
|
|
180
|
+
motionBetween(previous, next, prefix) {
|
|
181
|
+
if (!previous || previous === next) return null
|
|
182
|
+
|
|
183
|
+
const triggers = this.triggerTargets
|
|
184
|
+
const forward = triggers.indexOf(next) > triggers.indexOf(previous)
|
|
185
|
+
|
|
186
|
+
return forward ? `${prefix}-end` : `${prefix}-start`
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// --------------------------------------------------------------- helpers
|
|
190
|
+
|
|
191
|
+
// A pointer event can arrive on a trigger or on a panel; both mean the same
|
|
192
|
+
// item.
|
|
193
|
+
triggerFor(element) {
|
|
194
|
+
if (this.triggerTargets.includes(element)) return element
|
|
195
|
+
|
|
196
|
+
const item = element.closest("[data-slot=navigation-menu-item]")
|
|
197
|
+
|
|
198
|
+
return this.triggerTargets.find((trigger) => item?.contains(trigger)) || null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
contentFor(trigger) {
|
|
202
|
+
const item = trigger.closest("[data-slot=navigation-menu-item]")
|
|
203
|
+
|
|
204
|
+
return this.contentTargets.find((content) => item?.contains(content)) || null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
cancel() {
|
|
208
|
+
if (this.timer) clearTimeout(this.timer)
|
|
209
|
+
this.timer = null
|
|
210
|
+
}
|
|
211
|
+
}
|