shadcn-rails 0.1.0
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/.dockerignore +40 -0
- data/CHANGELOG.md +54 -0
- data/CLAUDE.md +463 -0
- data/PROGRESS.md +485 -0
- data/README.md +1483 -0
- data/Rakefile +29 -0
- data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +13 -0
- data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +46 -0
- data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +111 -0
- data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +27 -0
- data/__tests__/controllers/accordion_controller.test.js +904 -0
- data/__tests__/controllers/calendar_controller.test.js +1370 -0
- data/__tests__/controllers/carousel_controller.test.js +912 -0
- data/__tests__/controllers/checkbox_controller.test.js +454 -0
- data/__tests__/controllers/collapsible_controller.test.js +407 -0
- data/__tests__/controllers/combobox_controller.test.js +966 -0
- data/__tests__/controllers/context_menu_controller.test.js +627 -0
- data/__tests__/controllers/date_picker_controller.test.js +636 -0
- data/__tests__/controllers/dialog_controller.test.js +878 -0
- data/__tests__/controllers/drawer_controller.test.js +995 -0
- data/__tests__/controllers/menubar_controller.test.js +736 -0
- data/__tests__/controllers/navigation_menu_controller.test.js +598 -0
- data/__tests__/controllers/popover_controller.test.js +1007 -0
- data/__tests__/controllers/radio_group_controller.test.js +640 -0
- data/__tests__/controllers/resizable_controller.test.js +680 -0
- data/__tests__/controllers/select_controller.test.js +674 -0
- data/__tests__/controllers/sheet_controller.test.js +986 -0
- data/__tests__/controllers/slider_controller.test.js +1036 -0
- data/__tests__/controllers/switch_controller.test.js +424 -0
- data/__tests__/controllers/tabs_controller.test.js +907 -0
- data/__tests__/controllers/toggle_group_controller.test.js +839 -0
- data/__tests__/controllers/tooltip_controller.test.js +808 -0
- data/__tests__/helpers/stimulus-test-helper.js +203 -0
- data/app/assets/config/manifest.js +1 -0
- data/app/assets/javascripts/shadcn/controllers/accordion_controller.d.ts +53 -0
- data/app/assets/javascripts/shadcn/controllers/accordion_controller.js +140 -0
- data/app/assets/javascripts/shadcn/controllers/avatar_controller.d.ts +22 -0
- data/app/assets/javascripts/shadcn/controllers/avatar_controller.js +26 -0
- data/app/assets/javascripts/shadcn/controllers/calendar_controller.js +592 -0
- data/app/assets/javascripts/shadcn/controllers/carousel_controller.js +263 -0
- data/app/assets/javascripts/shadcn/controllers/checkbox_controller.d.ts +31 -0
- data/app/assets/javascripts/shadcn/controllers/checkbox_controller.js +48 -0
- data/app/assets/javascripts/shadcn/controllers/collapsible_controller.d.ts +43 -0
- data/app/assets/javascripts/shadcn/controllers/collapsible_controller.js +73 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +234 -0
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +141 -0
- data/app/assets/javascripts/shadcn/controllers/command_dialog_controller.js +162 -0
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +202 -0
- data/app/assets/javascripts/shadcn/controllers/date_picker_controller.js +282 -0
- data/app/assets/javascripts/shadcn/controllers/dialog_controller.d.ts +67 -0
- data/app/assets/javascripts/shadcn/controllers/dialog_controller.js +187 -0
- data/app/assets/javascripts/shadcn/controllers/drawer_controller.d.ts +58 -0
- data/app/assets/javascripts/shadcn/controllers/drawer_controller.js +112 -0
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.d.ts +83 -0
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +225 -0
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.d.ts +59 -0
- data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +143 -0
- data/app/assets/javascripts/shadcn/controllers/input_otp_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/input_otp_controller.js +206 -0
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +323 -0
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +251 -0
- data/app/assets/javascripts/shadcn/controllers/popover_controller.d.ts +56 -0
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +141 -0
- data/app/assets/javascripts/shadcn/controllers/radio_group_controller.d.ts +47 -0
- data/app/assets/javascripts/shadcn/controllers/radio_group_controller.js +108 -0
- data/app/assets/javascripts/shadcn/controllers/resizable_controller.js +272 -0
- data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/scroll_area_controller.js +74 -0
- data/app/assets/javascripts/shadcn/controllers/select_controller.d.ts +84 -0
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +222 -0
- data/app/assets/javascripts/shadcn/controllers/sheet_controller.d.ts +60 -0
- data/app/assets/javascripts/shadcn/controllers/sheet_controller.js +151 -0
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +148 -0
- data/app/assets/javascripts/shadcn/controllers/slider_controller.d.ts +102 -0
- data/app/assets/javascripts/shadcn/controllers/slider_controller.js +364 -0
- data/app/assets/javascripts/shadcn/controllers/switch_controller.d.ts +46 -0
- data/app/assets/javascripts/shadcn/controllers/switch_controller.js +78 -0
- data/app/assets/javascripts/shadcn/controllers/tabs_controller.d.ts +51 -0
- data/app/assets/javascripts/shadcn/controllers/tabs_controller.js +126 -0
- data/app/assets/javascripts/shadcn/controllers/toast_controller.d.ts +37 -0
- data/app/assets/javascripts/shadcn/controllers/toast_controller.js +58 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_controller.d.ts +27 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_controller.js +42 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.d.ts +44 -0
- data/app/assets/javascripts/shadcn/controllers/toggle_group_controller.js +68 -0
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.d.ts +56 -0
- data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +117 -0
- data/app/assets/javascripts/shadcn/index.d.ts +74 -0
- data/app/assets/javascripts/shadcn/index.js +133 -0
- data/app/assets/stylesheets/.keep +0 -0
- data/app/assets/stylesheets/shadcn/base.css +445 -0
- data/app/assets/stylesheets/shadcn/components.css +513 -0
- data/app/assets/stylesheets/shadcn/index.css +18 -0
- data/app/assets/stylesheets/shadcn/themes/gray.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/slate.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/stone.css +68 -0
- data/app/assets/stylesheets/shadcn/themes/zinc.css +68 -0
- data/app/components/shadcn/accordion_component.rb +63 -0
- data/app/components/shadcn/accordion_content_component.rb +29 -0
- data/app/components/shadcn/accordion_item_component.rb +40 -0
- data/app/components/shadcn/accordion_trigger_component.rb +49 -0
- data/app/components/shadcn/alert_component.rb +75 -0
- data/app/components/shadcn/alert_description_component.rb +12 -0
- data/app/components/shadcn/alert_dialog_action_component.rb +24 -0
- data/app/components/shadcn/alert_dialog_cancel_component.rb +24 -0
- data/app/components/shadcn/alert_dialog_component.rb +71 -0
- data/app/components/shadcn/alert_dialog_content_component.rb +57 -0
- data/app/components/shadcn/alert_dialog_description_component.rb +12 -0
- data/app/components/shadcn/alert_dialog_footer_component.rb +19 -0
- data/app/components/shadcn/alert_dialog_header_component.rb +19 -0
- data/app/components/shadcn/alert_dialog_title_component.rb +12 -0
- data/app/components/shadcn/alert_title_component.rb +12 -0
- data/app/components/shadcn/aspect_ratio_component.rb +49 -0
- data/app/components/shadcn/avatar_component.rb +107 -0
- data/app/components/shadcn/avatar_fallback_component.rb +17 -0
- data/app/components/shadcn/badge_component.rb +49 -0
- data/app/components/shadcn/base_component.rb +100 -0
- data/app/components/shadcn/breadcrumb_component.rb +70 -0
- data/app/components/shadcn/breadcrumb_item_component.rb +50 -0
- data/app/components/shadcn/button_component.rb +141 -0
- data/app/components/shadcn/button_group_component.rb +69 -0
- data/app/components/shadcn/calendar_component.rb +337 -0
- data/app/components/shadcn/card_action_component.rb +10 -0
- data/app/components/shadcn/card_component.rb +63 -0
- data/app/components/shadcn/card_content_component.rb +19 -0
- data/app/components/shadcn/card_description_component.rb +12 -0
- data/app/components/shadcn/card_footer_component.rb +12 -0
- data/app/components/shadcn/card_header_component.rb +24 -0
- data/app/components/shadcn/card_title_component.rb +18 -0
- data/app/components/shadcn/carousel_component.rb +275 -0
- data/app/components/shadcn/checkbox_component.rb +103 -0
- data/app/components/shadcn/collapsible_component.rb +66 -0
- data/app/components/shadcn/collapsible_content_component.rb +28 -0
- data/app/components/shadcn/combobox_component.rb +322 -0
- data/app/components/shadcn/command_component.rb +52 -0
- data/app/components/shadcn/command_dialog_component.rb +76 -0
- data/app/components/shadcn/command_empty_component.rb +12 -0
- data/app/components/shadcn/command_group_component.rb +34 -0
- data/app/components/shadcn/command_input_component.rb +59 -0
- data/app/components/shadcn/command_item_component.rb +48 -0
- data/app/components/shadcn/command_list_component.rb +38 -0
- data/app/components/shadcn/command_separator_component.rb +12 -0
- data/app/components/shadcn/command_shortcut_component.rb +12 -0
- data/app/components/shadcn/context_menu_component.rb +64 -0
- data/app/components/shadcn/context_menu_content_component.rb +44 -0
- data/app/components/shadcn/context_menu_item_component.rb +63 -0
- data/app/components/shadcn/context_menu_label_component.rb +18 -0
- data/app/components/shadcn/context_menu_separator_component.rb +12 -0
- data/app/components/shadcn/context_menu_shortcut_component.rb +12 -0
- data/app/components/shadcn/date_picker_component.rb +368 -0
- data/app/components/shadcn/dialog_component.rb +77 -0
- data/app/components/shadcn/dialog_content_component.rb +91 -0
- data/app/components/shadcn/dialog_description_component.rb +12 -0
- data/app/components/shadcn/dialog_footer_component.rb +12 -0
- data/app/components/shadcn/dialog_header_component.rb +19 -0
- data/app/components/shadcn/dialog_title_component.rb +12 -0
- data/app/components/shadcn/drawer_component.rb +72 -0
- data/app/components/shadcn/drawer_content_component.rb +76 -0
- data/app/components/shadcn/drawer_description_component.rb +12 -0
- data/app/components/shadcn/drawer_footer_component.rb +12 -0
- data/app/components/shadcn/drawer_header_component.rb +19 -0
- data/app/components/shadcn/drawer_title_component.rb +12 -0
- data/app/components/shadcn/dropdown_menu_component.rb +75 -0
- data/app/components/shadcn/dropdown_menu_content_component.rb +49 -0
- data/app/components/shadcn/dropdown_menu_group_component.rb +10 -0
- data/app/components/shadcn/dropdown_menu_item_component.rb +63 -0
- data/app/components/shadcn/dropdown_menu_label_component.rb +18 -0
- data/app/components/shadcn/dropdown_menu_separator_component.rb +12 -0
- data/app/components/shadcn/dropdown_menu_shortcut_component.rb +12 -0
- data/app/components/shadcn/empty_component.rb +48 -0
- data/app/components/shadcn/empty_content_component.rb +12 -0
- data/app/components/shadcn/empty_description_component.rb +12 -0
- data/app/components/shadcn/empty_header_component.rb +29 -0
- data/app/components/shadcn/empty_media_component.rb +21 -0
- data/app/components/shadcn/empty_title_component.rb +12 -0
- data/app/components/shadcn/field_component.rb +113 -0
- data/app/components/shadcn/hover_card_component.rb +64 -0
- data/app/components/shadcn/hover_card_content_component.rb +36 -0
- data/app/components/shadcn/input_component.rb +108 -0
- data/app/components/shadcn/input_group_component.rb +70 -0
- data/app/components/shadcn/input_otp_component.rb +183 -0
- data/app/components/shadcn/item_actions_component.rb +12 -0
- data/app/components/shadcn/item_component.rb +98 -0
- data/app/components/shadcn/item_content_component.rb +24 -0
- data/app/components/shadcn/item_description_component.rb +12 -0
- data/app/components/shadcn/item_footer_component.rb +12 -0
- data/app/components/shadcn/item_group_component.rb +24 -0
- data/app/components/shadcn/item_header_component.rb +12 -0
- data/app/components/shadcn/item_media_component.rb +22 -0
- data/app/components/shadcn/item_separator_component.rb +12 -0
- data/app/components/shadcn/item_title_component.rb +12 -0
- data/app/components/shadcn/kbd_component.rb +36 -0
- data/app/components/shadcn/label_component.rb +49 -0
- data/app/components/shadcn/menubar_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/menubar_component.rb +56 -0
- data/app/components/shadcn/menubar_content_component.rb +64 -0
- data/app/components/shadcn/menubar_item_component.rb +65 -0
- data/app/components/shadcn/menubar_label_component.rb +27 -0
- data/app/components/shadcn/menubar_menu_component.rb +34 -0
- data/app/components/shadcn/menubar_radio_group_component.rb +42 -0
- data/app/components/shadcn/menubar_radio_item_component.rb +76 -0
- data/app/components/shadcn/menubar_separator_component.rb +22 -0
- data/app/components/shadcn/menubar_shortcut_component.rb +21 -0
- data/app/components/shadcn/menubar_sub_component.rb +38 -0
- data/app/components/shadcn/menubar_sub_content_component.rb +45 -0
- data/app/components/shadcn/menubar_sub_trigger_component.rb +59 -0
- data/app/components/shadcn/menubar_trigger_component.rb +31 -0
- data/app/components/shadcn/native_select_component.rb +150 -0
- data/app/components/shadcn/navigation_menu_component.rb +76 -0
- data/app/components/shadcn/navigation_menu_content_component.rb +30 -0
- data/app/components/shadcn/navigation_menu_item_component.rb +39 -0
- data/app/components/shadcn/navigation_menu_link_component.rb +38 -0
- data/app/components/shadcn/navigation_menu_list_component.rb +29 -0
- data/app/components/shadcn/navigation_menu_trigger_component.rb +59 -0
- data/app/components/shadcn/pagination_component.rb +195 -0
- data/app/components/shadcn/pagination_content_component.rb +47 -0
- data/app/components/shadcn/pagination_ellipsis_component.rb +30 -0
- data/app/components/shadcn/pagination_item_component.rb +53 -0
- data/app/components/shadcn/pagination_next_component.rb +48 -0
- data/app/components/shadcn/pagination_previous_component.rb +48 -0
- data/app/components/shadcn/popover_component.rb +76 -0
- data/app/components/shadcn/popover_content_component.rb +25 -0
- data/app/components/shadcn/progress_component.rb +77 -0
- data/app/components/shadcn/radio_group_component.rb +129 -0
- data/app/components/shadcn/radio_group_item_component.rb +109 -0
- data/app/components/shadcn/resizable_handle_component.rb +98 -0
- data/app/components/shadcn/resizable_panel_component.rb +56 -0
- data/app/components/shadcn/resizable_panel_group_component.rb +94 -0
- data/app/components/shadcn/scroll_area_component.rb +110 -0
- data/app/components/shadcn/select_component.rb +151 -0
- data/app/components/shadcn/select_group_component.rb +32 -0
- data/app/components/shadcn/select_item_component.rb +59 -0
- data/app/components/shadcn/select_separator_component.rb +12 -0
- data/app/components/shadcn/separator_component.rb +54 -0
- data/app/components/shadcn/sheet_component.rb +82 -0
- data/app/components/shadcn/sheet_content_component.rb +95 -0
- data/app/components/shadcn/sheet_description_component.rb +12 -0
- data/app/components/shadcn/sheet_footer_component.rb +12 -0
- data/app/components/shadcn/sheet_header_component.rb +19 -0
- data/app/components/shadcn/sheet_title_component.rb +12 -0
- data/app/components/shadcn/sidebar_component.rb +180 -0
- data/app/components/shadcn/sidebar_content_component.rb +32 -0
- data/app/components/shadcn/sidebar_footer_component.rb +24 -0
- data/app/components/shadcn/sidebar_group_action_component.rb +26 -0
- data/app/components/shadcn/sidebar_group_component.rb +38 -0
- data/app/components/shadcn/sidebar_group_content_component.rb +32 -0
- data/app/components/shadcn/sidebar_group_label_component.rb +25 -0
- data/app/components/shadcn/sidebar_header_component.rb +24 -0
- data/app/components/shadcn/sidebar_inset_component.rb +25 -0
- data/app/components/shadcn/sidebar_menu_action_component.rb +37 -0
- data/app/components/shadcn/sidebar_menu_badge_component.rb +25 -0
- data/app/components/shadcn/sidebar_menu_button_component.rb +52 -0
- data/app/components/shadcn/sidebar_menu_component.rb +32 -0
- data/app/components/shadcn/sidebar_menu_item_component.rb +41 -0
- data/app/components/shadcn/sidebar_menu_skeleton_component.rb +46 -0
- data/app/components/shadcn/sidebar_menu_sub_button_component.rb +43 -0
- data/app/components/shadcn/sidebar_menu_sub_component.rb +33 -0
- data/app/components/shadcn/sidebar_menu_sub_item_component.rb +30 -0
- data/app/components/shadcn/sidebar_provider_component.rb +57 -0
- data/app/components/shadcn/sidebar_rail_component.rb +30 -0
- data/app/components/shadcn/sidebar_separator_component.rb +24 -0
- data/app/components/shadcn/sidebar_trigger_component.rb +51 -0
- data/app/components/shadcn/skeleton_component.rb +29 -0
- data/app/components/shadcn/slider_component.rb +76 -0
- data/app/components/shadcn/spinner_component.rb +67 -0
- data/app/components/shadcn/switch_component.rb +147 -0
- data/app/components/shadcn/table_body_component.rb +16 -0
- data/app/components/shadcn/table_caption_component.rb +12 -0
- data/app/components/shadcn/table_cell_component.rb +12 -0
- data/app/components/shadcn/table_component.rb +57 -0
- data/app/components/shadcn/table_footer_component.rb +16 -0
- data/app/components/shadcn/table_head_component.rb +12 -0
- data/app/components/shadcn/table_header_component.rb +16 -0
- data/app/components/shadcn/table_row_component.rb +40 -0
- data/app/components/shadcn/tabs_component.rb +78 -0
- data/app/components/shadcn/tabs_content_component.rb +32 -0
- data/app/components/shadcn/tabs_list_component.rb +30 -0
- data/app/components/shadcn/tabs_trigger_component.rb +37 -0
- data/app/components/shadcn/textarea_component.rb +84 -0
- data/app/components/shadcn/toast_action_component.rb +18 -0
- data/app/components/shadcn/toast_component.rb +114 -0
- data/app/components/shadcn/toast_description_component.rb +12 -0
- data/app/components/shadcn/toast_title_component.rb +12 -0
- data/app/components/shadcn/toast_viewport_component.rb +12 -0
- data/app/components/shadcn/toggle_component.rb +77 -0
- data/app/components/shadcn/toggle_group_component.rb +96 -0
- data/app/components/shadcn/toggle_group_item_component.rb +62 -0
- data/app/components/shadcn/tooltip_component.rb +89 -0
- data/app/components/shadcn/typography_component.rb +112 -0
- data/babel.config.cjs +5 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/config/importmap.rb +5 -0
- data/fly.toml +26 -0
- data/jest.config.js +19 -0
- data/jest.setup.js +8 -0
- data/lib/generators/shadcn/component/component_generator.rb +188 -0
- data/lib/generators/shadcn/install/install_generator.rb +140 -0
- data/lib/generators/shadcn/install/templates/initializer.rb.tt +35 -0
- data/lib/generators/shadcn/install/templates/shadcn.yml.tt +35 -0
- data/lib/generators/shadcn/theme/theme_generator.rb +128 -0
- data/lib/shadcn/rails/class_merger.rb +228 -0
- data/lib/shadcn/rails/configuration.rb +341 -0
- data/lib/shadcn/rails/engine.rb +59 -0
- data/lib/shadcn/rails/helpers/class_name_helper.rb +35 -0
- data/lib/shadcn/rails/helpers/component_helper.rb +60 -0
- data/lib/shadcn/rails/helpers/pagination_helper.rb +187 -0
- data/lib/shadcn/rails/version.rb +7 -0
- data/lib/shadcn/rails.rb +179 -0
- data/package-lock.json +7415 -0
- data/package.json +68 -0
- data/rollup.config.js +29 -0
- data/sig/shadcn/rails.rbs +6 -0
- metadata +526 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Container for navigation menu items
|
|
5
|
+
class NavigationMenuListComponent < BaseComponent
|
|
6
|
+
BASE_CLASSES = "group flex flex-1 list-none items-center justify-center space-x-1"
|
|
7
|
+
|
|
8
|
+
renders_many :items, lambda { |**options|
|
|
9
|
+
NavigationMenuItemComponent.new(**options)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
content_tag(:ul, list_content, list_attributes)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def list_content
|
|
19
|
+
safe_join(items)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def list_attributes
|
|
23
|
+
{
|
|
24
|
+
class: cn(BASE_CLASSES, class_name),
|
|
25
|
+
"data-shadcn--navigation-menu-target": "list"
|
|
26
|
+
}.merge(html_options).compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Trigger button that opens navigation menu content
|
|
5
|
+
class NavigationMenuTriggerComponent < BaseComponent
|
|
6
|
+
TRIGGER_CLASSES = [
|
|
7
|
+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2",
|
|
8
|
+
"text-sm font-medium transition-colors",
|
|
9
|
+
"hover:bg-accent hover:text-accent-foreground",
|
|
10
|
+
"focus:bg-accent focus:text-accent-foreground focus:outline-none",
|
|
11
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
12
|
+
"data-[state=open]:bg-accent/50"
|
|
13
|
+
].join(" ").freeze
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
content_tag(:button, trigger_content, trigger_attributes)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def trigger_content
|
|
22
|
+
safe_join([
|
|
23
|
+
content,
|
|
24
|
+
chevron_icon
|
|
25
|
+
])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def chevron_icon
|
|
29
|
+
content_tag(:svg, chevron_path,
|
|
30
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
31
|
+
width: "24",
|
|
32
|
+
height: "24",
|
|
33
|
+
viewBox: "0 0 24 24",
|
|
34
|
+
fill: "none",
|
|
35
|
+
stroke: "currentColor",
|
|
36
|
+
"stroke-width": "2",
|
|
37
|
+
"stroke-linecap": "round",
|
|
38
|
+
"stroke-linejoin": "round",
|
|
39
|
+
class: "relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180",
|
|
40
|
+
"aria-hidden": "true"
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def chevron_path
|
|
45
|
+
content_tag(:path, nil, d: "m6 9 6 6 6-6")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def trigger_attributes
|
|
49
|
+
{
|
|
50
|
+
class: cn(TRIGGER_CLASSES, class_name),
|
|
51
|
+
type: "button",
|
|
52
|
+
"data-shadcn--navigation-menu-target": "trigger",
|
|
53
|
+
"data-action": "click->shadcn--navigation-menu#toggle mouseenter->shadcn--navigation-menu#hoverOpen",
|
|
54
|
+
"aria-expanded": "false",
|
|
55
|
+
"data-state": "closed"
|
|
56
|
+
}.merge(html_options).compact
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Pagination component for navigating paged content
|
|
5
|
+
# Matches shadcn/ui Pagination component
|
|
6
|
+
#
|
|
7
|
+
# Supports three usage patterns:
|
|
8
|
+
#
|
|
9
|
+
# 1. Slot-based API (full control):
|
|
10
|
+
# <%= render Shadcn::PaginationComponent.new do |pagination| %>
|
|
11
|
+
# <% pagination.with_pagination_content do |content| %>
|
|
12
|
+
# <% content.with_previous(href: "/page/1") %>
|
|
13
|
+
# <% content.with_item(href: "/page/1") { "1" } %>
|
|
14
|
+
# <% content.with_item(href: "/page/2", active: true) { "2" } %>
|
|
15
|
+
# <% content.with_item(href: "/page/3") { "3" } %>
|
|
16
|
+
# <% content.with_ellipse %>
|
|
17
|
+
# <% content.with_next_page(href: "/page/3") %>
|
|
18
|
+
# <% end %>
|
|
19
|
+
# <% end %>
|
|
20
|
+
#
|
|
21
|
+
# 2. Collection-based API (Kaminari/will_paginate):
|
|
22
|
+
# <%= render Shadcn::PaginationComponent.new(collection: @posts) %>
|
|
23
|
+
#
|
|
24
|
+
# 3. Pagy-based API:
|
|
25
|
+
# <%= render Shadcn::PaginationComponent.new(pagy: @pagy) %>
|
|
26
|
+
#
|
|
27
|
+
class PaginationComponent < BaseComponent
|
|
28
|
+
BASE_CLASSES = "mx-auto flex w-full justify-center"
|
|
29
|
+
|
|
30
|
+
renders_one :pagination_content, lambda { |**options|
|
|
31
|
+
PaginationContentComponent.new(**options)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# @param collection [Object, nil] Kaminari or will_paginate collection
|
|
35
|
+
# @param pagy [Object, nil] Pagy object
|
|
36
|
+
# @param url_builder [Proc, nil] Lambda to generate page URLs, receives page number
|
|
37
|
+
# @param window [Integer] Number of pages to show around current page
|
|
38
|
+
def initialize(collection: nil, pagy: nil, url_builder: nil, window: 2, **options)
|
|
39
|
+
super(**options)
|
|
40
|
+
@collection = collection
|
|
41
|
+
@pagy = pagy
|
|
42
|
+
@url_builder = url_builder || ->(page) { "?page=#{page}" }
|
|
43
|
+
@window = window
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call
|
|
47
|
+
if auto_generate?
|
|
48
|
+
return "" if total_pages <= 1
|
|
49
|
+
|
|
50
|
+
content_tag(:nav, auto_generated_content, pagination_attributes)
|
|
51
|
+
else
|
|
52
|
+
content_tag(:nav, build_pagination_content, pagination_attributes)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def auto_generate?
|
|
59
|
+
@collection.present? || @pagy.present?
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def auto_generated_content
|
|
63
|
+
pagination_data = extract_pagination_data
|
|
64
|
+
series = generate_page_series(pagination_data[:current_page], pagination_data[:total_pages])
|
|
65
|
+
|
|
66
|
+
# Build the items for the content component
|
|
67
|
+
items_html = []
|
|
68
|
+
|
|
69
|
+
# Previous button
|
|
70
|
+
prev_href = pagination_data[:prev_page] ? @url_builder.call(pagination_data[:prev_page]) : nil
|
|
71
|
+
items_html << PaginationPreviousComponent.new(href: prev_href, disabled: pagination_data[:prev_page].nil?).render_in(view_context)
|
|
72
|
+
|
|
73
|
+
# Page items
|
|
74
|
+
series.each do |item|
|
|
75
|
+
case item
|
|
76
|
+
when :gap
|
|
77
|
+
items_html << PaginationEllipsisComponent.new.render_in(view_context)
|
|
78
|
+
when Integer
|
|
79
|
+
items_html << PaginationItemComponent.new(href: @url_builder.call(item)).render_in(view_context) { item.to_s }
|
|
80
|
+
when String # Current page (string format)
|
|
81
|
+
page_num = item.to_i
|
|
82
|
+
items_html << PaginationItemComponent.new(href: @url_builder.call(page_num), active: true).render_in(view_context) { item }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Next button
|
|
87
|
+
next_href = pagination_data[:next_page] ? @url_builder.call(pagination_data[:next_page]) : nil
|
|
88
|
+
items_html << PaginationNextComponent.new(href: next_href, disabled: pagination_data[:next_page].nil?).render_in(view_context)
|
|
89
|
+
|
|
90
|
+
content_tag(:ul, safe_join(items_html), class: "flex flex-row items-center gap-1")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def extract_pagination_data
|
|
94
|
+
obj = @pagy || @collection
|
|
95
|
+
|
|
96
|
+
# Pagy detection
|
|
97
|
+
if defined?(::Pagy) && obj.is_a?(::Pagy)
|
|
98
|
+
{
|
|
99
|
+
current_page: obj.page,
|
|
100
|
+
total_pages: obj.pages,
|
|
101
|
+
prev_page: obj.prev,
|
|
102
|
+
next_page: obj.next
|
|
103
|
+
}
|
|
104
|
+
# Duck typing for Pagy-like objects
|
|
105
|
+
elsif obj.respond_to?(:page) && obj.respond_to?(:pages) && obj.respond_to?(:prev) && obj.respond_to?(:next)
|
|
106
|
+
{
|
|
107
|
+
current_page: obj.page,
|
|
108
|
+
total_pages: obj.pages,
|
|
109
|
+
prev_page: obj.prev,
|
|
110
|
+
next_page: obj.next
|
|
111
|
+
}
|
|
112
|
+
# Kaminari detection
|
|
113
|
+
elsif obj.respond_to?(:current_page) && obj.respond_to?(:total_pages) && obj.respond_to?(:prev_page)
|
|
114
|
+
{
|
|
115
|
+
current_page: obj.current_page,
|
|
116
|
+
total_pages: obj.total_pages,
|
|
117
|
+
prev_page: obj.prev_page,
|
|
118
|
+
next_page: obj.next_page
|
|
119
|
+
}
|
|
120
|
+
# will_paginate detection
|
|
121
|
+
elsif obj.respond_to?(:current_page) && obj.respond_to?(:total_pages) && obj.respond_to?(:previous_page)
|
|
122
|
+
{
|
|
123
|
+
current_page: obj.current_page,
|
|
124
|
+
total_pages: obj.total_pages,
|
|
125
|
+
prev_page: obj.previous_page,
|
|
126
|
+
next_page: obj.next_page
|
|
127
|
+
}
|
|
128
|
+
# Generic fallback
|
|
129
|
+
elsif obj.respond_to?(:current_page) && obj.respond_to?(:total_pages)
|
|
130
|
+
current = obj.current_page
|
|
131
|
+
total = obj.total_pages
|
|
132
|
+
{
|
|
133
|
+
current_page: current,
|
|
134
|
+
total_pages: total,
|
|
135
|
+
prev_page: current > 1 ? current - 1 : nil,
|
|
136
|
+
next_page: current < total ? current + 1 : nil
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
raise ArgumentError, "Expected a paginated collection (Kaminari/will_paginate) or Pagy object"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def total_pages
|
|
144
|
+
extract_pagination_data[:total_pages]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def generate_page_series(current_page, total_pages)
|
|
148
|
+
return [current_page.to_s] if total_pages <= 1
|
|
149
|
+
|
|
150
|
+
series = []
|
|
151
|
+
|
|
152
|
+
# Always include first page
|
|
153
|
+
series << (current_page == 1 ? "1" : 1)
|
|
154
|
+
|
|
155
|
+
# Calculate range around current page
|
|
156
|
+
range_start = [2, current_page - @window].max
|
|
157
|
+
range_end = [total_pages - 1, current_page + @window].min
|
|
158
|
+
|
|
159
|
+
# Add gap before range if needed
|
|
160
|
+
series << :gap if range_start > 2
|
|
161
|
+
|
|
162
|
+
# Add pages in range
|
|
163
|
+
(range_start..range_end).each do |page|
|
|
164
|
+
next if page == 1 || page == total_pages
|
|
165
|
+
|
|
166
|
+
series << (page == current_page ? page.to_s : page)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Add gap after range if needed
|
|
170
|
+
series << :gap if range_end < total_pages - 1
|
|
171
|
+
|
|
172
|
+
# Always include last page if more than 1 page
|
|
173
|
+
if total_pages > 1
|
|
174
|
+
series << (current_page == total_pages ? total_pages.to_s : total_pages)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
series
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_pagination_content
|
|
181
|
+
pagination_content || ""
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def pagination_attributes
|
|
185
|
+
attrs = {
|
|
186
|
+
role: "navigation",
|
|
187
|
+
"aria-label": "pagination",
|
|
188
|
+
class: merge_classes(BASE_CLASSES)
|
|
189
|
+
}
|
|
190
|
+
attrs.merge!(html_options)
|
|
191
|
+
attrs.merge!(build_data)
|
|
192
|
+
attrs.compact
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Pagination Content component - container for items
|
|
5
|
+
# Uses a single polymorphic slot to maintain ordering between items and ellipses
|
|
6
|
+
class PaginationContentComponent < BaseComponent
|
|
7
|
+
BASE_CLASSES = "flex flex-row items-center gap-1"
|
|
8
|
+
|
|
9
|
+
renders_one :previous, lambda { |href: nil, disabled: false, **options|
|
|
10
|
+
PaginationPreviousComponent.new(href: href, disabled: disabled, **options)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
renders_one :next_page, lambda { |href: nil, disabled: false, **options|
|
|
14
|
+
PaginationNextComponent.new(href: href, disabled: disabled, **options)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Single slot for all page elements (items and ellipses) to maintain order
|
|
18
|
+
renders_many :elements, types: {
|
|
19
|
+
item: {
|
|
20
|
+
renders: lambda { |href: nil, active: false, disabled: false, **options|
|
|
21
|
+
PaginationItemComponent.new(href: href, active: active, disabled: disabled, **options)
|
|
22
|
+
},
|
|
23
|
+
as: :item
|
|
24
|
+
},
|
|
25
|
+
ellipse: {
|
|
26
|
+
renders: lambda { |**options|
|
|
27
|
+
PaginationEllipsisComponent.new(**options)
|
|
28
|
+
},
|
|
29
|
+
as: :ellipse
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def call
|
|
34
|
+
content_tag(:ul, list_content, class: merge_classes(BASE_CLASSES))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def list_content
|
|
40
|
+
safe_join([
|
|
41
|
+
previous,
|
|
42
|
+
elements,
|
|
43
|
+
next_page
|
|
44
|
+
].flatten.compact)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Pagination Ellipsis component
|
|
5
|
+
class PaginationEllipsisComponent < BaseComponent
|
|
6
|
+
BASE_CLASSES = "flex h-9 w-9 items-center justify-center"
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
content_tag(:li) do
|
|
10
|
+
content_tag(:span, ellipsis_content, class: merge_classes(BASE_CLASSES), "aria-hidden": "true")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def ellipsis_content
|
|
17
|
+
content_tag(:svg,
|
|
18
|
+
content_tag(:circle, nil, cx: "12", cy: "12", r: "1") +
|
|
19
|
+
content_tag(:circle, nil, cx: "19", cy: "12", r: "1") +
|
|
20
|
+
content_tag(:circle, nil, cx: "5", cy: "12", r: "1"),
|
|
21
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
22
|
+
width: "16",
|
|
23
|
+
height: "16",
|
|
24
|
+
viewBox: "0 0 24 24",
|
|
25
|
+
fill: "currentColor",
|
|
26
|
+
class: "h-4 w-4"
|
|
27
|
+
) + content_tag(:span, "More pages", class: "sr-only")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Pagination Item component - wrapper for links
|
|
5
|
+
class PaginationItemComponent < BaseComponent
|
|
6
|
+
LINK_CLASSES = "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 w-9"
|
|
7
|
+
ACTIVE_CLASSES = "border border-input bg-background shadow-sm"
|
|
8
|
+
|
|
9
|
+
def initialize(href: nil, active: false, disabled: false, **options)
|
|
10
|
+
super(**options)
|
|
11
|
+
@href = href
|
|
12
|
+
@active = active
|
|
13
|
+
@disabled = disabled
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
content_tag(:li) do
|
|
18
|
+
link_element
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def link_element
|
|
25
|
+
classes = cn(LINK_CLASSES, @active ? ACTIVE_CLASSES : "", class_name)
|
|
26
|
+
|
|
27
|
+
if @href
|
|
28
|
+
content_tag(:a, content, link_attributes(classes))
|
|
29
|
+
else
|
|
30
|
+
content_tag(:span, content, span_attributes(classes))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def link_attributes(classes)
|
|
35
|
+
attrs = {
|
|
36
|
+
href: @href,
|
|
37
|
+
class: classes,
|
|
38
|
+
"aria-current": @active ? "page" : nil
|
|
39
|
+
}
|
|
40
|
+
attrs.merge!(html_options)
|
|
41
|
+
attrs.compact
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def span_attributes(classes)
|
|
45
|
+
attrs = {
|
|
46
|
+
class: classes,
|
|
47
|
+
"aria-current": @active ? "page" : nil
|
|
48
|
+
}
|
|
49
|
+
attrs.merge!(html_options)
|
|
50
|
+
attrs.compact
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Pagination Next button
|
|
5
|
+
class PaginationNextComponent < BaseComponent
|
|
6
|
+
BASE_CLASSES = "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2 gap-1 pr-2.5"
|
|
7
|
+
|
|
8
|
+
def initialize(href: nil, disabled: false, **options)
|
|
9
|
+
super(**options)
|
|
10
|
+
@href = href
|
|
11
|
+
@disabled = disabled
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
content_tag(:li) do
|
|
16
|
+
link_content
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def link_content
|
|
23
|
+
inner = safe_join(["Next", chevron_right])
|
|
24
|
+
|
|
25
|
+
if @href && !@disabled
|
|
26
|
+
content_tag(:a, inner, href: @href, class: merge_classes(BASE_CLASSES), "aria-label": "Go to next page")
|
|
27
|
+
else
|
|
28
|
+
content_tag(:span, inner, class: cn(merge_classes(BASE_CLASSES), "pointer-events-none opacity-50"), "aria-disabled": "true")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def chevron_right
|
|
33
|
+
content_tag(:svg,
|
|
34
|
+
content_tag(:path, nil, d: "m9 18 6-6-6-6"),
|
|
35
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
36
|
+
width: "16",
|
|
37
|
+
height: "16",
|
|
38
|
+
viewBox: "0 0 24 24",
|
|
39
|
+
fill: "none",
|
|
40
|
+
stroke: "currentColor",
|
|
41
|
+
"stroke-width": "2",
|
|
42
|
+
"stroke-linecap": "round",
|
|
43
|
+
"stroke-linejoin": "round",
|
|
44
|
+
class: "h-4 w-4"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Pagination Previous button
|
|
5
|
+
class PaginationPreviousComponent < BaseComponent
|
|
6
|
+
BASE_CLASSES = "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2 gap-1 pl-2.5"
|
|
7
|
+
|
|
8
|
+
def initialize(href: nil, disabled: false, **options)
|
|
9
|
+
super(**options)
|
|
10
|
+
@href = href
|
|
11
|
+
@disabled = disabled
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
content_tag(:li) do
|
|
16
|
+
link_content
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def link_content
|
|
23
|
+
inner = safe_join([chevron_left, "Previous"])
|
|
24
|
+
|
|
25
|
+
if @href && !@disabled
|
|
26
|
+
content_tag(:a, inner, href: @href, class: merge_classes(BASE_CLASSES), "aria-label": "Go to previous page")
|
|
27
|
+
else
|
|
28
|
+
content_tag(:span, inner, class: cn(merge_classes(BASE_CLASSES), "pointer-events-none opacity-50"), "aria-disabled": "true")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def chevron_left
|
|
33
|
+
content_tag(:svg,
|
|
34
|
+
content_tag(:path, nil, d: "m15 18-6-6 6-6"),
|
|
35
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
36
|
+
width: "16",
|
|
37
|
+
height: "16",
|
|
38
|
+
viewBox: "0 0 24 24",
|
|
39
|
+
fill: "none",
|
|
40
|
+
stroke: "currentColor",
|
|
41
|
+
"stroke-width": "2",
|
|
42
|
+
"stroke-linecap": "round",
|
|
43
|
+
"stroke-linejoin": "round",
|
|
44
|
+
class: "h-4 w-4"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Popover component for rich content in an overlay
|
|
5
|
+
# Matches shadcn/ui Popover component
|
|
6
|
+
# Uses Stimulus for interactivity
|
|
7
|
+
#
|
|
8
|
+
# @example Basic popover
|
|
9
|
+
# <%= render Shadcn::PopoverComponent.new do |popover| %>
|
|
10
|
+
# <% popover.with_trigger do %>
|
|
11
|
+
# <%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open popover" } %>
|
|
12
|
+
# <% end %>
|
|
13
|
+
# <% popover.with_content do %>
|
|
14
|
+
# <div class="grid gap-4">
|
|
15
|
+
# <h4 class="font-medium leading-none">Dimensions</h4>
|
|
16
|
+
# <p class="text-sm text-muted-foreground">Set the dimensions for the layer.</p>
|
|
17
|
+
# </div>
|
|
18
|
+
# <% end %>
|
|
19
|
+
# <% end %>
|
|
20
|
+
#
|
|
21
|
+
class PopoverComponent < BaseComponent
|
|
22
|
+
renders_one :trigger
|
|
23
|
+
renders_one :body, lambda { |**options, &block|
|
|
24
|
+
PopoverContentComponent.new(**options, &block)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# @param open [Boolean] Whether popover starts open
|
|
28
|
+
# @param side [Symbol] Side to show content (:top, :right, :bottom, :left)
|
|
29
|
+
# @param align [Symbol] Alignment (:start, :center, :end)
|
|
30
|
+
# @param modal [Boolean] Whether to trap focus
|
|
31
|
+
def initialize(open: false, side: :bottom, align: :center, modal: false, **options)
|
|
32
|
+
super(**options)
|
|
33
|
+
@open = open
|
|
34
|
+
@side = side
|
|
35
|
+
@align = align
|
|
36
|
+
@modal = modal
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def call
|
|
40
|
+
content_tag(:div, popover_structure, popover_attributes)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def popover_structure
|
|
46
|
+
safe_join([
|
|
47
|
+
trigger_wrapper,
|
|
48
|
+
body
|
|
49
|
+
].compact)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def trigger_wrapper
|
|
53
|
+
return unless trigger
|
|
54
|
+
|
|
55
|
+
content_tag(:div, trigger, {
|
|
56
|
+
"data-shadcn--popover-target": "trigger",
|
|
57
|
+
"data-action": "click->shadcn--popover#toggle"
|
|
58
|
+
})
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def popover_attributes
|
|
62
|
+
attrs = {
|
|
63
|
+
class: cn("relative inline-block", class_name),
|
|
64
|
+
"data-controller": "shadcn--popover",
|
|
65
|
+
"data-shadcn--popover-open-value": @open.to_s,
|
|
66
|
+
"data-shadcn--popover-side-value": @side.to_s,
|
|
67
|
+
"data-shadcn--popover-align-value": @align.to_s,
|
|
68
|
+
"data-shadcn--popover-modal-value": @modal.to_s,
|
|
69
|
+
"data-action": "keydown.escape->shadcn--popover#close"
|
|
70
|
+
}
|
|
71
|
+
attrs.merge!(html_options)
|
|
72
|
+
attrs.merge!(build_data)
|
|
73
|
+
attrs.compact
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Popover Content component
|
|
5
|
+
class PopoverContentComponent < BaseComponent
|
|
6
|
+
BASE_CLASSES = "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
|
|
7
|
+
|
|
8
|
+
def call
|
|
9
|
+
content_tag(:div, content, content_attributes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def content_attributes
|
|
15
|
+
{
|
|
16
|
+
class: merge_classes(BASE_CLASSES),
|
|
17
|
+
"data-shadcn--popover-target": "content",
|
|
18
|
+
"data-state": "closed",
|
|
19
|
+
"data-side": "bottom",
|
|
20
|
+
tabindex: "-1",
|
|
21
|
+
hidden: true
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Progress component for showing completion status
|
|
5
|
+
# Matches shadcn/ui Progress component
|
|
6
|
+
#
|
|
7
|
+
# @example Basic progress
|
|
8
|
+
# <%= render Shadcn::ProgressComponent.new(value: 60) %>
|
|
9
|
+
#
|
|
10
|
+
# @example With custom max
|
|
11
|
+
# <%= render Shadcn::ProgressComponent.new(value: 30, max: 50) %>
|
|
12
|
+
#
|
|
13
|
+
# @example Indeterminate (loading)
|
|
14
|
+
# <%= render Shadcn::ProgressComponent.new(indeterminate: true) %>
|
|
15
|
+
#
|
|
16
|
+
class ProgressComponent < BaseComponent
|
|
17
|
+
BASE_CLASSES = "relative h-2 w-full overflow-hidden rounded-full bg-primary/20"
|
|
18
|
+
INDICATOR_CLASSES = "h-full w-full flex-1 bg-primary transition-all"
|
|
19
|
+
|
|
20
|
+
# @param value [Integer, Float, nil] Current progress value
|
|
21
|
+
# @param max [Integer, Float] Maximum progress value
|
|
22
|
+
# @param indeterminate [Boolean] Whether to show indeterminate state
|
|
23
|
+
def initialize(value: nil, max: 100, indeterminate: false, **options)
|
|
24
|
+
super(**options)
|
|
25
|
+
@value = value
|
|
26
|
+
@max = max
|
|
27
|
+
@indeterminate = indeterminate
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call
|
|
31
|
+
content_tag(:div, progress_indicator, progress_attributes)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def progress_indicator
|
|
37
|
+
content_tag(:div, "", indicator_attributes)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def progress_percentage
|
|
41
|
+
return 0 if @value.nil? || @max.zero?
|
|
42
|
+
[(@value.to_f / @max.to_f * 100).round, 100].min
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def indicator_style
|
|
46
|
+
if @indeterminate
|
|
47
|
+
nil # Animation handled by CSS
|
|
48
|
+
else
|
|
49
|
+
"transform: translateX(-#{100 - progress_percentage}%)"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def progress_attributes
|
|
54
|
+
attrs = {
|
|
55
|
+
class: merge_classes(BASE_CLASSES),
|
|
56
|
+
role: "progressbar",
|
|
57
|
+
"aria-valuemin": 0,
|
|
58
|
+
"aria-valuemax": @max,
|
|
59
|
+
"aria-valuenow": @indeterminate ? nil : @value,
|
|
60
|
+
"data-state": @indeterminate ? "indeterminate" : "determinate",
|
|
61
|
+
"data-value": @value,
|
|
62
|
+
"data-max": @max
|
|
63
|
+
}
|
|
64
|
+
attrs.merge!(html_options)
|
|
65
|
+
attrs.merge!(build_data)
|
|
66
|
+
attrs.compact
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def indicator_attributes
|
|
70
|
+
{
|
|
71
|
+
class: cn(INDICATOR_CLASSES, @indeterminate ? "animate-progress-indeterminate" : ""),
|
|
72
|
+
style: indicator_style,
|
|
73
|
+
"data-state": @indeterminate ? "indeterminate" : "determinate"
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|