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,558 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
module Calendar
|
|
5
|
+
# Port of registry/new-york-v4/ui/calendar.tsx.
|
|
6
|
+
#
|
|
7
|
+
# Upstream is 220 lines that hand every class to `react-day-picker` and let
|
|
8
|
+
# it render — so the class map is fully specified there, and the markup is
|
|
9
|
+
# the package's. That markup is a plain `<table role="grid">`, which is why
|
|
10
|
+
# this port has no dependency: what the package mostly *is* — 3,479 lines of
|
|
11
|
+
# locale data, 2,046 of Ethiopic, Hebrew, Hijri, Persian and Jalali
|
|
12
|
+
# calendars, and 576 wrapping `date-fns` — a Rails app already has in `Date`
|
|
13
|
+
# and `I18n`. See [features/calendar.md](../../../../.claude/docs/features/calendar.md).
|
|
14
|
+
#
|
|
15
|
+
# The grid is computed in `Calendar::Month` and rendered here, so a calendar
|
|
16
|
+
# is readable with no JavaScript at all — the one family in this gem where
|
|
17
|
+
# that is true of the whole component rather than of its resting state.
|
|
18
|
+
class Component < ApplicationViewComponent
|
|
19
|
+
# Upstream writes these two with `String.raw`, so the backslash is part of
|
|
20
|
+
# the class. Single-quoted here for the same reason: in a double-quoted
|
|
21
|
+
# Ruby string `\_` is just `_`, and the token would stop matching the
|
|
22
|
+
# element it names.
|
|
23
|
+
RTL_CHEVRONS = 'rtl:**:[.rdp-button\_next>svg]:rotate-180 rtl:**:[.rdp-button\_previous>svg]:rotate-180'
|
|
24
|
+
|
|
25
|
+
# The two `rdp-*` classes this port does render, and the only ones. They
|
|
26
|
+
# are not styling — they are what the two `RTL_CHEVRONS` selectors above
|
|
27
|
+
# point at, and upstream's own rule would match nothing without them.
|
|
28
|
+
# Every other `rdp-*` name comes from `getDefaultClassNames()`, a function
|
|
29
|
+
# call rather than a string in the vendored source, and nothing here
|
|
30
|
+
# styles them.
|
|
31
|
+
PREVIOUS_CLASS = "rdp-button_previous"
|
|
32
|
+
NEXT_CLASS = "rdp-button_next"
|
|
33
|
+
|
|
34
|
+
MODES = %i[single multiple range].freeze
|
|
35
|
+
CAPTION_LAYOUTS = %i[label dropdown dropdown_months dropdown_years].freeze
|
|
36
|
+
|
|
37
|
+
MONTHS_CLASS = "relative flex flex-col gap-4 md:flex-row"
|
|
38
|
+
MONTH_CLASS = "flex w-full flex-col gap-4"
|
|
39
|
+
NAV_CLASS = "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1"
|
|
40
|
+
CAPTION_CLASS = "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)"
|
|
41
|
+
CAPTION_LABEL_CLASS = "font-medium select-none"
|
|
42
|
+
CAPTION_LABEL_LABEL_CLASS = "text-sm"
|
|
43
|
+
CAPTION_LABEL_DROPDOWN_CLASS = "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm " \
|
|
44
|
+
"[&>svg]:size-3.5 [&>svg]:text-muted-foreground"
|
|
45
|
+
DROPDOWNS_CLASS = "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium"
|
|
46
|
+
DROPDOWN_ROOT_CLASS = "relative rounded-md border border-input shadow-xs has-focus:border-ring " \
|
|
47
|
+
"has-focus:ring-[3px] has-focus:ring-ring/50"
|
|
48
|
+
DROPDOWN_CLASS = "absolute inset-0 bg-popover opacity-0"
|
|
49
|
+
# What a screen reader is left with once the caption is two dropdowns: the
|
|
50
|
+
# month has to be announced when it changes, and the dropdowns announce
|
|
51
|
+
# themselves as controls rather than as the answer. Upstream's own inline
|
|
52
|
+
# style, which is `sr-only` written out.
|
|
53
|
+
STATUS_STYLE = "border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;" \
|
|
54
|
+
"position:absolute;width:1px;white-space:nowrap;word-wrap:normal"
|
|
55
|
+
GRID_CLASS = "w-full border-collapse"
|
|
56
|
+
WEEKDAYS_CLASS = "flex"
|
|
57
|
+
WEEKDAY_CLASS = "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none"
|
|
58
|
+
WEEK_CLASS = "mt-2 flex w-full"
|
|
59
|
+
WEEK_NUMBER_HEADER_CLASS = "w-(--cell-size) select-none"
|
|
60
|
+
WEEK_NUMBER_CLASS = "text-[0.8rem] text-muted-foreground select-none"
|
|
61
|
+
WEEK_NUMBER_INNER_CLASS = "flex size-(--cell-size) items-center justify-center text-center"
|
|
62
|
+
DAY_CLASS = "group/day relative aspect-square h-full w-full p-0 text-center select-none " \
|
|
63
|
+
"[&:last-child[data-selected=true]_button]:rounded-r-md"
|
|
64
|
+
DAY_FIRST_CLASS = "[&:first-child[data-selected=true]_button]:rounded-l-md"
|
|
65
|
+
DAY_FIRST_WITH_WEEK_NUMBER_CLASS = "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
|
66
|
+
TODAY_CLASS = "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none"
|
|
67
|
+
OUTSIDE_CLASS = "text-muted-foreground aria-selected:text-muted-foreground"
|
|
68
|
+
DISABLED_CLASS = "text-muted-foreground opacity-50"
|
|
69
|
+
HIDDEN_CLASS = "invisible"
|
|
70
|
+
RANGE_START_CLASS = "rounded-l-md bg-accent"
|
|
71
|
+
RANGE_MIDDLE_CLASS = "rounded-none"
|
|
72
|
+
RANGE_END_CLASS = "rounded-r-md bg-accent"
|
|
73
|
+
NAV_BUTTON_CLASS = "size-(--cell-size) p-0 select-none aria-disabled:opacity-50"
|
|
74
|
+
DAY_BUTTON_CLASS = "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none " \
|
|
75
|
+
"font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 " \
|
|
76
|
+
"group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] " \
|
|
77
|
+
"group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md " \
|
|
78
|
+
"data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary " \
|
|
79
|
+
"data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none " \
|
|
80
|
+
"data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground " \
|
|
81
|
+
"data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md " \
|
|
82
|
+
"data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground " \
|
|
83
|
+
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground " \
|
|
84
|
+
"dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70"
|
|
85
|
+
CHEVRON_CLASS = "size-4"
|
|
86
|
+
|
|
87
|
+
default_tag :div
|
|
88
|
+
slot_name :calendar
|
|
89
|
+
|
|
90
|
+
style do
|
|
91
|
+
base {
|
|
92
|
+
"w-fit group/calendar bg-background p-3 [--cell-size:--spacing(8)] " \
|
|
93
|
+
"[[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent " +
|
|
94
|
+
RTL_CHEVRONS
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
attr_reader :month, :months, :number_of_months, :mode, :selected, :disabled,
|
|
99
|
+
:button_variant, :caption_layout, :name, :required,
|
|
100
|
+
:show_outside_days, :show_week_number, :fixed_weeks,
|
|
101
|
+
:start_month, :end_month, :week_starts_on
|
|
102
|
+
|
|
103
|
+
# `month:` is which month is shown, `selected:` what is chosen in it, and
|
|
104
|
+
# they are separate on purpose — upstream's `month` and `selected` are two
|
|
105
|
+
# props for the same reason: a range can end in a month nobody is looking
|
|
106
|
+
# at.
|
|
107
|
+
#
|
|
108
|
+
# `disabled:` takes anything that answers `===` — a Date, a Range, a
|
|
109
|
+
# lambda — or an array of those. That is Ruby's own matcher protocol, and
|
|
110
|
+
# it covers what upstream's `Matcher` union covers without inventing a
|
|
111
|
+
# shape for it.
|
|
112
|
+
def initialize(month: nil, mode: :single, selected: nil, disabled: nil, name: nil, required: false,
|
|
113
|
+
caption_layout: :label, button_variant: :ghost, show_outside_days: true,
|
|
114
|
+
show_week_number: false, fixed_weeks: false, number_of_months: 1,
|
|
115
|
+
start_month: nil, end_month: nil, week_starts_on: nil,
|
|
116
|
+
**attributes)
|
|
117
|
+
@mode = MODES.include?(mode&.to_sym) ? mode.to_sym : :single
|
|
118
|
+
@selected = Array(selected.is_a?(Range) ? [ selected.begin, selected.end ] : selected).compact.map(&:to_date)
|
|
119
|
+
# A range arrives either as a `Range` or as the two ends of one, because
|
|
120
|
+
# that is what comes back from the controller and from a form. Both mean
|
|
121
|
+
# the same thing here, and the modifier classes read this rather than
|
|
122
|
+
# the argument's shape.
|
|
123
|
+
@range = if selected.is_a?(Range) then selected.begin.to_date..selected.end.to_date
|
|
124
|
+
elsif @mode == :range && @selected.size == 2 then @selected.first..@selected.last
|
|
125
|
+
end
|
|
126
|
+
@disabled = disabled
|
|
127
|
+
@name = name
|
|
128
|
+
@required = required
|
|
129
|
+
@caption_layout = CAPTION_LAYOUTS.include?(caption_layout&.to_sym) ? caption_layout.to_sym : :label
|
|
130
|
+
@button_variant = button_variant&.to_sym || :ghost
|
|
131
|
+
@show_outside_days = show_outside_days
|
|
132
|
+
@show_week_number = show_week_number
|
|
133
|
+
@fixed_weeks = fixed_weeks
|
|
134
|
+
@number_of_months = [ number_of_months.to_i, 1 ].max
|
|
135
|
+
@start_month = start_month&.to_date
|
|
136
|
+
@end_month = end_month&.to_date
|
|
137
|
+
@week_starts_on = week_starts_on
|
|
138
|
+
@month = Month.new(month || @selected.first || Date.current,
|
|
139
|
+
week_starts_on:, fixed_weeks:)
|
|
140
|
+
# Upstream's `numberOfMonths`: one nav for the lot, and a caption and a
|
|
141
|
+
# grid each. The months run forward from the one that was asked for.
|
|
142
|
+
@months = Array.new(@number_of_months) { |offset| offset.zero? ? @month : @month.plus_months(offset) }
|
|
143
|
+
super(**attributes)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Everything the controller needs to draw a month the server never
|
|
147
|
+
# rendered, and nothing else. The names, the formats and the modifier
|
|
148
|
+
# classes all cross from here rather than being written in JavaScript:
|
|
149
|
+
# `I18n` stays the only thing that decides how a date reads, and Tailwind
|
|
150
|
+
# only ever sees a class in Ruby source.
|
|
151
|
+
def element_attributes(**defaults)
|
|
152
|
+
super(**{
|
|
153
|
+
"data-controller" => "shadcn--calendar",
|
|
154
|
+
"data-shadcn--calendar-month-value" => month.first.iso8601,
|
|
155
|
+
"data-shadcn--calendar-today-value" => Date.current.iso8601,
|
|
156
|
+
"data-shadcn--calendar-week-starts-on-value" => month.week_starts_on,
|
|
157
|
+
"data-shadcn--calendar-fixed-weeks-value" => fixed_weeks,
|
|
158
|
+
"data-shadcn--calendar-show-outside-days-value" => show_outside_days,
|
|
159
|
+
"data-shadcn--calendar-show-week-number-value" => show_week_number,
|
|
160
|
+
"data-shadcn--calendar-start-month-value" => start_month&.beginning_of_month&.iso8601,
|
|
161
|
+
"data-shadcn--calendar-end-month-value" => end_month&.end_of_month&.iso8601,
|
|
162
|
+
"data-shadcn--calendar-mode-value" => mode,
|
|
163
|
+
"data-shadcn--calendar-required-value" => required,
|
|
164
|
+
"data-shadcn--calendar-input-names-value" => input_names.to_json,
|
|
165
|
+
"data-shadcn--calendar-selected-value" => selected_dates.to_json,
|
|
166
|
+
"data-shadcn--calendar-disabled-value" => portable_matchers.to_json,
|
|
167
|
+
"data-shadcn--calendar-day-names-value" => day_names.to_json,
|
|
168
|
+
"data-shadcn--calendar-short-day-names-value" => short_day_names.to_json,
|
|
169
|
+
"data-shadcn--calendar-month-names-value" => month_names.to_json,
|
|
170
|
+
"data-shadcn--calendar-short-month-names-value" => short_month_names.to_json,
|
|
171
|
+
"data-shadcn--calendar-date-format-value" => day_label_format,
|
|
172
|
+
"data-shadcn--calendar-month-format-value" => MONTH_FORMAT,
|
|
173
|
+
# Fetched with the placeholder as its own argument, so what crosses is
|
|
174
|
+
# the template rather than a rendered sentence — `I18n` raises on a
|
|
175
|
+
# missing interpolation and the controller needs the `%{date}` intact.
|
|
176
|
+
"data-shadcn--calendar-today-label-value" => shadcn_t("calendar.today_label", date: "%{date}"),
|
|
177
|
+
"data-shadcn--calendar-selected-label-value" => shadcn_t("calendar.selected_label", date: "%{date}"),
|
|
178
|
+
"data-shadcn--calendar-week-number-label-value" => shadcn_t("calendar.week_number", number: "%{number}"),
|
|
179
|
+
"data-shadcn--calendar-today-class-value" => TODAY_CLASS,
|
|
180
|
+
"data-shadcn--calendar-outside-class-value" => OUTSIDE_CLASS,
|
|
181
|
+
"data-shadcn--calendar-disabled-class-value" => DISABLED_CLASS,
|
|
182
|
+
"data-shadcn--calendar-hidden-class-value" => HIDDEN_CLASS,
|
|
183
|
+
"data-shadcn--calendar-range-start-class-value" => RANGE_START_CLASS,
|
|
184
|
+
"data-shadcn--calendar-range-middle-class-value" => RANGE_MIDDLE_CLASS,
|
|
185
|
+
"data-shadcn--calendar-range-end-class-value" => RANGE_END_CLASS
|
|
186
|
+
}.compact.merge(defaults))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def call
|
|
190
|
+
# Block content lands after the months, which is where upstream's
|
|
191
|
+
# `footer` prop renders — and inside the root, which is what lets a
|
|
192
|
+
# preset button's `data-action` reach this controller at all.
|
|
193
|
+
render_element(body: safe_join([
|
|
194
|
+
tag.div(safe_join([ nav, *months.map { |shown| month_body(shown) } ]), class: MONTHS_CLASS),
|
|
195
|
+
inputs,
|
|
196
|
+
content
|
|
197
|
+
].compact))
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private
|
|
201
|
+
|
|
202
|
+
# The nav sits over the caption rather than beside it — `absolute` on the
|
|
203
|
+
# nav, and the caption padded by a cell on each side to leave room.
|
|
204
|
+
# `labelNav()` returns an empty string upstream, and an empty `aria-label`
|
|
205
|
+
# names nothing, so this port leaves the attribute off rather than
|
|
206
|
+
# rendering one that does no work.
|
|
207
|
+
def nav
|
|
208
|
+
tag.nav(safe_join([ step_button(:previous), step_button(:next) ]), class: NAV_CLASS)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def step_button(direction)
|
|
212
|
+
target = direction == :previous ? month.previous : month.next
|
|
213
|
+
unavailable = out_of_bounds?(target.first)
|
|
214
|
+
|
|
215
|
+
tag.button(
|
|
216
|
+
render(Icon::Component.new("chevron-#{direction == :previous ? 'left' : 'right'}", class: CHEVRON_CLASS)),
|
|
217
|
+
type: "button",
|
|
218
|
+
"data-shadcn--calendar-target": direction,
|
|
219
|
+
"data-action": "click->shadcn--calendar##{direction}",
|
|
220
|
+
class: Button::Component.variant_classes(
|
|
221
|
+
variant: button_variant,
|
|
222
|
+
class: [ NAV_BUTTON_CLASS, direction == :previous ? PREVIOUS_CLASS : NEXT_CLASS ].join(" ")
|
|
223
|
+
),
|
|
224
|
+
tabindex: ("-1" if unavailable),
|
|
225
|
+
"aria-disabled": ("true" if unavailable),
|
|
226
|
+
"aria-label": shadcn_t("calendar.#{direction}")
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def month_body(shown)
|
|
231
|
+
tag.div(safe_join([ caption(shown), grid(shown) ]), class: MONTH_CLASS)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# `role="status"` with `aria-live="polite"`, as upstream's CaptionLabel
|
|
235
|
+
# carries: the month is the one thing on the page that changes when the
|
|
236
|
+
# nav is pressed, and a screen reader has to hear it without being moved.
|
|
237
|
+
def caption(shown)
|
|
238
|
+
tag.div(caption_layout == :label ? plain_caption(shown) : dropdown_caption(shown), class: CAPTION_CLASS)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def plain_caption(shown)
|
|
242
|
+
tag.span(label_of(shown), class: "#{CAPTION_LABEL_CLASS} #{CAPTION_LABEL_LABEL_CLASS}",
|
|
243
|
+
id: caption_id(shown), role: "status", "aria-live": "polite",
|
|
244
|
+
"data-shadcn--calendar-target": "caption")
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Two native `<select>`s, laid invisible over the label they change — the
|
|
248
|
+
# same technique the one-time-code field uses, and upstream's here: the
|
|
249
|
+
# control a person operates is the real one, and what they read is the
|
|
250
|
+
# `aria-hidden` span underneath it.
|
|
251
|
+
def dropdown_caption(shown)
|
|
252
|
+
controls = [
|
|
253
|
+
(month_dropdown(shown) if caption_layout != :dropdown_years),
|
|
254
|
+
(tag.span(I18n.l(shown.date, format: "%b")) if caption_layout == :dropdown_years),
|
|
255
|
+
(year_dropdown(shown) if caption_layout != :dropdown_months),
|
|
256
|
+
(tag.span(shown.date.year) if caption_layout == :dropdown_months)
|
|
257
|
+
].compact
|
|
258
|
+
|
|
259
|
+
tag.div(
|
|
260
|
+
safe_join(controls + [ tag.span(label_of(shown), id: caption_id(shown), role: "status",
|
|
261
|
+
"aria-live": "polite", style: STATUS_STYLE,
|
|
262
|
+
"data-shadcn--calendar-target": "caption") ]),
|
|
263
|
+
class: DROPDOWNS_CLASS
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def month_dropdown(shown)
|
|
268
|
+
options = (1..12).map do |number|
|
|
269
|
+
{ value: number, label: I18n.t("date.abbr_month_names")[number] }
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
dropdown(options, shown.date.month, shadcn_t("calendar.choose_month"), :monthSelect, :chooseMonth)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def year_dropdown(shown)
|
|
276
|
+
first = (start_month || shown.date - 100.years).year
|
|
277
|
+
last = (end_month || shown.date + 100.years).year
|
|
278
|
+
options = (first..last).map { |year| { value: year, label: year.to_s } }
|
|
279
|
+
|
|
280
|
+
dropdown(options, shown.date.year, shadcn_t("calendar.choose_year"), :yearSelect, :chooseYear)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def dropdown(options, value, label, target, action)
|
|
284
|
+
tag.span(
|
|
285
|
+
safe_join([
|
|
286
|
+
tag.select(
|
|
287
|
+
safe_join(options.map { |option| tag.option(option[:label], value: option[:value], selected: option[:value] == value) }),
|
|
288
|
+
class: DROPDOWN_CLASS,
|
|
289
|
+
"aria-label": label,
|
|
290
|
+
"data-shadcn--calendar-target": target,
|
|
291
|
+
"data-action": "change->shadcn--calendar##{action}"
|
|
292
|
+
),
|
|
293
|
+
tag.span(
|
|
294
|
+
safe_join([
|
|
295
|
+
options.find { |option| option[:value] == value }&.dig(:label),
|
|
296
|
+
render(Icon::Component.new("chevron-down", class: CHEVRON_CLASS))
|
|
297
|
+
].compact),
|
|
298
|
+
class: "#{CAPTION_LABEL_CLASS} #{CAPTION_LABEL_DROPDOWN_CLASS}",
|
|
299
|
+
"aria-hidden": "true"
|
|
300
|
+
)
|
|
301
|
+
]),
|
|
302
|
+
class: DROPDOWN_ROOT_CLASS
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# The grid is the control, so a caller who names this component names the
|
|
307
|
+
# *table* — and keeps the month, which is the other half of what a screen
|
|
308
|
+
# reader needs here. `aria-labelledby` wins over `aria-label` outright, so
|
|
309
|
+
# the two are combined by id rather than left to fight: "Starts on,
|
|
310
|
+
# August 2026".
|
|
311
|
+
def grid(shown)
|
|
312
|
+
tag.table(
|
|
313
|
+
safe_join([ weekday_header(shown), weeks(shown) ]),
|
|
314
|
+
class: GRID_CLASS,
|
|
315
|
+
role: "grid",
|
|
316
|
+
"data-shadcn--calendar-target": "grid",
|
|
317
|
+
"data-action": "keydown->shadcn--calendar#keydown focusin->shadcn--calendar#focused",
|
|
318
|
+
"aria-multiselectable": ("true" if mode != :single),
|
|
319
|
+
"aria-labelledby": "#{attributes[:"aria-labelledby"]} #{caption_id(shown)}".strip,
|
|
320
|
+
"aria-label": (label_of(shown) if attributes[:"aria-labelledby"].blank?)
|
|
321
|
+
)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def caption_id(shown)
|
|
325
|
+
@caption_ids ||= Hash.new { |ids, key| ids[key] = "shadcn-calendar-caption-#{SecureRandom.hex(4)}" }
|
|
326
|
+
@caption_ids[shown.first]
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# `aria-hidden` on the whole header row, which is upstream's: the day
|
|
330
|
+
# names repeat in every cell's own label, so a screen reader reading the
|
|
331
|
+
# grid would say each one eight times.
|
|
332
|
+
def weekday_header(shown)
|
|
333
|
+
cells = shown.weekday_numbers.map do |number|
|
|
334
|
+
tag.th(short_day_names[number], class: WEEKDAY_CLASS, scope: "col",
|
|
335
|
+
"aria-label": day_names[number])
|
|
336
|
+
end
|
|
337
|
+
if show_week_number
|
|
338
|
+
# No text and a name of its own: upstream's `formatWeekNumberHeader`
|
|
339
|
+
# returns an empty string and the column is named by its `aria-label`.
|
|
340
|
+
cells.unshift(tag.th(nil, class: WEEK_NUMBER_HEADER_CLASS, scope: "col",
|
|
341
|
+
"aria-label": shadcn_t("calendar.week_number_header")))
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
tag.thead(tag.tr(safe_join(cells), class: WEEKDAYS_CLASS), "aria-hidden": "true")
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def weeks(shown)
|
|
348
|
+
tag.tbody(safe_join(shown.weeks.map { |week| week_row(week, shown) }),
|
|
349
|
+
"data-shadcn--calendar-target": "weeks")
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def week_row(week, shown)
|
|
353
|
+
cells = week.map { |day| day_cell(day, shown) }
|
|
354
|
+
cells.unshift(week_number_cell(week, shown)) if show_week_number
|
|
355
|
+
|
|
356
|
+
tag.tr(safe_join(cells), class: WEEK_CLASS)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# A `<td>`, not the `<th>` the package renders: `calendar.tsx` overrides
|
|
360
|
+
# `WeekNumber` with one (calendar.tsx:167-175), and the `scope` and role
|
|
361
|
+
# come from DayPicker either way.
|
|
362
|
+
def week_number_cell(week, shown)
|
|
363
|
+
number = shown.number_of(week)
|
|
364
|
+
|
|
365
|
+
tag.td(
|
|
366
|
+
tag.div(number, class: WEEK_NUMBER_INNER_CLASS),
|
|
367
|
+
class: WEEK_NUMBER_CLASS, scope: "row", role: "rowheader",
|
|
368
|
+
"aria-label": shadcn_t("calendar.week_number", number:)
|
|
369
|
+
)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# The cell carries the state and the button carries the label: upstream
|
|
373
|
+
# puts `data-selected`, `data-today` and the rest on the `<td>`, and the
|
|
374
|
+
# classes that read them are written against that.
|
|
375
|
+
def day_cell(day, shown)
|
|
376
|
+
outside = shown.outside?(day)
|
|
377
|
+
hidden = outside && !show_outside_days
|
|
378
|
+
|
|
379
|
+
tag.td(
|
|
380
|
+
(day_button(day, shown) unless hidden),
|
|
381
|
+
class: day_classes(day, outside:, hidden:),
|
|
382
|
+
role: "gridcell",
|
|
383
|
+
"data-day": day.iso8601,
|
|
384
|
+
"data-month": (day.strftime("%Y-%m") if outside),
|
|
385
|
+
"data-outside": ("true" if outside),
|
|
386
|
+
"data-today": ("true" if day == Date.current),
|
|
387
|
+
"data-selected": ("true" if selected?(day)),
|
|
388
|
+
"data-disabled": ("true" if disabled?(day)),
|
|
389
|
+
"data-hidden": ("true" if hidden),
|
|
390
|
+
"aria-selected": ("true" if selected?(day))
|
|
391
|
+
)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def day_classes(day, outside:, hidden:)
|
|
395
|
+
[
|
|
396
|
+
DAY_CLASS,
|
|
397
|
+
show_week_number ? DAY_FIRST_WITH_WEEK_NUMBER_CLASS : DAY_FIRST_CLASS,
|
|
398
|
+
(TODAY_CLASS if day == Date.current),
|
|
399
|
+
(OUTSIDE_CLASS if outside),
|
|
400
|
+
(DISABLED_CLASS if disabled?(day)),
|
|
401
|
+
(HIDDEN_CLASS if hidden),
|
|
402
|
+
(RANGE_START_CLASS if range_start?(day)),
|
|
403
|
+
(RANGE_MIDDLE_CLASS if range_middle?(day)),
|
|
404
|
+
(RANGE_END_CLASS if range_end?(day))
|
|
405
|
+
].compact.join(" ")
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# A real `Button`, as upstream's `CalendarDayButton` is — so it carries
|
|
409
|
+
# `data-slot="button"` and the variant classes rather than a copy of them.
|
|
410
|
+
#
|
|
411
|
+
# `data-day` here is the *formatted* date where the cell's is ISO, which
|
|
412
|
+
# is upstream's split too (`day.date.toLocaleDateString()` against
|
|
413
|
+
# `day.isoDate`). The machine-readable one is the cell's.
|
|
414
|
+
def day_button(day, shown)
|
|
415
|
+
render(Button::Component.new(
|
|
416
|
+
variant: :ghost,
|
|
417
|
+
size: :icon,
|
|
418
|
+
class: DAY_BUTTON_CLASS,
|
|
419
|
+
type: "button",
|
|
420
|
+
disabled: disabled?(day),
|
|
421
|
+
tabindex: (focus_target?(day, shown) ? "0" : "-1"),
|
|
422
|
+
"data-action": "click->shadcn--calendar#select",
|
|
423
|
+
"data-day": I18n.l(day, format: :default),
|
|
424
|
+
"data-selected-single": ("true" if mode == :single && selected?(day)),
|
|
425
|
+
"data-range-start": ("true" if range_start?(day)),
|
|
426
|
+
"data-range-middle": ("true" if range_middle?(day)),
|
|
427
|
+
"data-range-end": ("true" if range_end?(day)),
|
|
428
|
+
"aria-label": day_label(day)
|
|
429
|
+
)) { day.day.to_s }
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Exactly one day in the grid is tabbable, and the rest are reached with
|
|
433
|
+
# the arrow keys — the grid pattern, and what upstream's `isFocusTarget`
|
|
434
|
+
# does. The selected day if it is here, otherwise today, otherwise the
|
|
435
|
+
# first day of the month.
|
|
436
|
+
#
|
|
437
|
+
# A disabled candidate is passed over rather than taken: a tab stop on a
|
|
438
|
+
# disabled button is no tab stop at all, and the grid would drop out of
|
|
439
|
+
# the tab order for a calendar whose selected day happens to be blocked.
|
|
440
|
+
# The controller repeats this rule after every re-render, because a month
|
|
441
|
+
# with nothing tabbable is a month the keyboard cannot reach.
|
|
442
|
+
# One tab stop per grid, so a two-month calendar has two — each grid is a
|
|
443
|
+
# `role="grid"` of its own and the arrows walk between them by date.
|
|
444
|
+
def focus_target(shown)
|
|
445
|
+
@focus_targets ||= {}
|
|
446
|
+
@focus_targets[shown.first] ||= [
|
|
447
|
+
selected.find { |date| shown.days.include?(date) },
|
|
448
|
+
(Date.current if shown.days.include?(Date.current)),
|
|
449
|
+
shown.first
|
|
450
|
+
].compact.find { |date| !disabled?(date) } || shown.first
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def focus_target?(day, shown) = day == focus_target(shown)
|
|
454
|
+
|
|
455
|
+
def selected?(day)
|
|
456
|
+
return @range.cover?(day) if @range
|
|
457
|
+
|
|
458
|
+
selected.include?(day)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def range_start?(day) = @range && day == @range.begin
|
|
462
|
+
def range_end?(day) = @range && day == @range.end
|
|
463
|
+
def range_middle?(day) = @range && @range.cover?(day) && day != @range.begin && day != @range.end
|
|
464
|
+
|
|
465
|
+
def disabled?(day)
|
|
466
|
+
out_of_bounds?(day) || Array.wrap(disabled).any? { |matcher| matcher === day } # rubocop:disable Style/CaseEquality
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def out_of_bounds?(day)
|
|
470
|
+
(start_month && day.end_of_month < start_month.beginning_of_month) ||
|
|
471
|
+
(end_month && day.beginning_of_month > end_month.end_of_month)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
MONTH_FORMAT = "%B %Y"
|
|
475
|
+
|
|
476
|
+
# What a Rails form receives. One input per value, named the way the mode
|
|
477
|
+
# means it:
|
|
478
|
+
#
|
|
479
|
+
# | `mode:` | `name: "trip[on]"` becomes |
|
|
480
|
+
# |-------------|---------------------------------------|
|
|
481
|
+
# | `:single` | `trip[on]` |
|
|
482
|
+
# | `:multiple` | `trip[on][]`, one per day |
|
|
483
|
+
# | `:range` | `trip[on][from]` and `trip[on][to]` |
|
|
484
|
+
#
|
|
485
|
+
# A range can also be given two names of its own —
|
|
486
|
+
# `name: %w[trip[from] trip[to]]` — because two dates are usually two
|
|
487
|
+
# columns, and `starts_on`/`ends_on` is what a Rails model calls them.
|
|
488
|
+
#
|
|
489
|
+
# An empty value is still submitted, so that clearing a date reaches the
|
|
490
|
+
# controller: a parameter that simply vanishes leaves the old value in
|
|
491
|
+
# place, which is the same trap Rails' own checkbox hidden field exists to
|
|
492
|
+
# avoid.
|
|
493
|
+
def inputs
|
|
494
|
+
return if input_names.empty?
|
|
495
|
+
|
|
496
|
+
tag.span(safe_join(input_values.map { |field, value|
|
|
497
|
+
tag.input(type: "hidden", name: field, value:, autocomplete: "off")
|
|
498
|
+
}), "data-shadcn--calendar-target": "inputs")
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def input_names
|
|
502
|
+
return [] if name.blank?
|
|
503
|
+
return Array(name) if name.is_a?(Array)
|
|
504
|
+
|
|
505
|
+
case mode
|
|
506
|
+
when :multiple then [ "#{name}[]" ]
|
|
507
|
+
when :range then [ "#{name}[from]", "#{name}[to]" ]
|
|
508
|
+
else [ name.to_s ]
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def input_values
|
|
513
|
+
return input_names.zip(selected.map(&:iso8601)).map { |field, value| [ field, value.to_s ] } if mode == :range
|
|
514
|
+
|
|
515
|
+
return [ [ input_names.first, "" ] ] if selected.empty?
|
|
516
|
+
|
|
517
|
+
selected.map { |date| [ input_names.first, date.iso8601 ] }
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def label_of(shown) = I18n.l(shown.date, format: MONTH_FORMAT)
|
|
521
|
+
|
|
522
|
+
# The two lists as the controller indexes them: weekdays by `Date#wday`,
|
|
523
|
+
# months from zero, which is what `getDay()` and `getMonth()` hand back.
|
|
524
|
+
def month_names = I18n.t("date.month_names").compact
|
|
525
|
+
def short_month_names = I18n.t("date.abbr_month_names").compact
|
|
526
|
+
|
|
527
|
+
def selected_dates = selected.map(&:iso8601)
|
|
528
|
+
|
|
529
|
+
# A `Date` and a `Range` of them cross to the browser as themselves. A
|
|
530
|
+
# callable cannot — it decided the month the server rendered, and says
|
|
531
|
+
# nothing about a month reached from the nav. Named here rather than left
|
|
532
|
+
# to be discovered; see features/calendar.md.
|
|
533
|
+
def portable_matchers
|
|
534
|
+
Array.wrap(disabled).filter_map do |matcher|
|
|
535
|
+
case matcher
|
|
536
|
+
when Date then matcher.iso8601
|
|
537
|
+
when Range then { from: matcher.begin&.to_date&.iso8601, to: matcher.end&.to_date&.iso8601 }
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# One pattern, localized once and then handed to the controller as text —
|
|
543
|
+
# rather than a weekday glued onto a formatted date on one side and not
|
|
544
|
+
# the other, which is how the two first disagreed.
|
|
545
|
+
def day_label_format = "%A, #{I18n.t('date.formats.long')}"
|
|
546
|
+
|
|
547
|
+
def day_label(day)
|
|
548
|
+
label = I18n.l(day, format: day_label_format)
|
|
549
|
+
label = shadcn_t("calendar.today_label", date: label) if day == Date.current
|
|
550
|
+
label = shadcn_t("calendar.selected_label", date: label) if selected?(day)
|
|
551
|
+
label
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def day_names = I18n.t("date.day_names")
|
|
555
|
+
def short_day_names = I18n.t("date.abbr_day_names")
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
module Calendar
|
|
5
|
+
# One month, as a grid of weeks.
|
|
6
|
+
#
|
|
7
|
+
# This is the part `react-day-picker` mostly *is* — `helpers/getDates.js`,
|
|
8
|
+
# `helpers/getWeeks.js` and the 576 lines of `classes/DateLib.js` that wrap
|
|
9
|
+
# `date-fns` — and it is the part Ruby already has. `Date` does the
|
|
10
|
+
# arithmetic and `I18n` does the names, so nothing here needs a dependency.
|
|
11
|
+
#
|
|
12
|
+
# A plain object rather than a component: it renders nothing, and being
|
|
13
|
+
# able to assert on a grid without a browser is worth more than the
|
|
14
|
+
# symmetry.
|
|
15
|
+
class Month
|
|
16
|
+
DAYS_IN_WEEK = 7
|
|
17
|
+
|
|
18
|
+
attr_reader :date, :week_starts_on, :fixed_weeks
|
|
19
|
+
|
|
20
|
+
# `week_starts_on` is a weekday number, 0 for Sunday, matching
|
|
21
|
+
# `Date#wday` — and defaulting to the host application's own
|
|
22
|
+
# `Date.beginning_of_week` rather than to a constant, because that is the
|
|
23
|
+
# setting a Rails app has already made. Upstream takes it from the locale.
|
|
24
|
+
def initialize(date = Date.current, week_starts_on: nil, fixed_weeks: false)
|
|
25
|
+
@date = date.to_date
|
|
26
|
+
@week_starts_on = week_starts_on || Date::DAYNAMES.index(Date.beginning_of_week.to_s.capitalize)
|
|
27
|
+
@fixed_weeks = fixed_weeks
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def first = date.beginning_of_month
|
|
31
|
+
def last = date.end_of_month
|
|
32
|
+
|
|
33
|
+
# Every date the grid holds, including the days either side that fill the
|
|
34
|
+
# first and last weeks out. Six rows when `fixed_weeks` is asked for, so a
|
|
35
|
+
# calendar that is navigated does not change height under the pointer —
|
|
36
|
+
# upstream's `fixedWeeks`, and the reason it exists.
|
|
37
|
+
def weeks
|
|
38
|
+
start = shift_back(first)
|
|
39
|
+
rows = fixed_weeks ? 6 : ((shift_forward(last) - start + 1) / DAYS_IN_WEEK)
|
|
40
|
+
|
|
41
|
+
Array.new(rows) do |row|
|
|
42
|
+
Array.new(DAYS_IN_WEEK) { |column| start + (row * DAYS_IN_WEEK) + column }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def days = weeks.flatten
|
|
47
|
+
|
|
48
|
+
def outside?(day) = day.month != date.month
|
|
49
|
+
|
|
50
|
+
# ISO-8601, which is `Date#cweek`. Upstream's week number follows the
|
|
51
|
+
# locale and can differ — the US counts from a different week — and this
|
|
52
|
+
# port does not reproduce that; see features/calendar.md.
|
|
53
|
+
def number_of(week) = week.first.cweek
|
|
54
|
+
|
|
55
|
+
def previous = self.class.new(first.prev_month, **options)
|
|
56
|
+
def next = self.class.new(first.next_month, **options)
|
|
57
|
+
def plus_months(count) = self.class.new(first + count.months, **options)
|
|
58
|
+
|
|
59
|
+
# Sunday-first `Date::DAYNAMES`, rotated to whichever day the week starts
|
|
60
|
+
# on. The names themselves come from `I18n` at render time, not from here.
|
|
61
|
+
def weekday_numbers
|
|
62
|
+
Array.new(DAYS_IN_WEEK) { |offset| (week_starts_on + offset) % DAYS_IN_WEEK }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def options = { week_starts_on:, fixed_weeks: }
|
|
68
|
+
|
|
69
|
+
def shift_back(day) = day - ((day.wday - week_starts_on) % DAYS_IN_WEEK)
|
|
70
|
+
|
|
71
|
+
def shift_forward(day) = day + ((week_starts_on - 1 - day.wday) % DAYS_IN_WEEK)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|