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
data/README.md
ADDED
|
@@ -0,0 +1,1483 @@
|
|
|
1
|
+
# shadcn-rails
|
|
2
|
+
|
|
3
|
+
Beautiful, accessible UI components for Rails built with ViewComponents, Stimulus, and Tailwind CSS. A Ruby port of [shadcn/ui](https://ui.shadcn.com).
|
|
4
|
+
|
|
5
|
+
[](https://www.ruby-lang.org/)
|
|
6
|
+
[](https://rubyonrails.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **Beautiful by default** - Carefully crafted components that look great out of the box
|
|
12
|
+
- **Accessible** - Built with accessibility in mind, following WAI-ARIA patterns
|
|
13
|
+
- **Customizable** - Use CSS variables to customize the look and feel
|
|
14
|
+
- **Dark mode** - Built-in dark mode support with multiple strategies
|
|
15
|
+
- **ViewComponents** - Leverages Rails' ViewComponent library for composable, testable components
|
|
16
|
+
- **Stimulus** - Interactive components powered by Stimulus controllers
|
|
17
|
+
- **Rails-first** - Designed specifically for Ruby on Rails applications
|
|
18
|
+
- **47 Components** - Comprehensive library covering all common UI patterns
|
|
19
|
+
|
|
20
|
+
## Table of Contents
|
|
21
|
+
|
|
22
|
+
- [Installation](#installation)
|
|
23
|
+
- [Quick Start](#quick-start)
|
|
24
|
+
- [Components](#components)
|
|
25
|
+
- [Buttons & Actions](#buttons--actions)
|
|
26
|
+
- [Form Inputs](#form-inputs)
|
|
27
|
+
- [Data Display](#data-display)
|
|
28
|
+
- [Feedback](#feedback)
|
|
29
|
+
- [Overlays](#overlays)
|
|
30
|
+
- [Navigation](#navigation)
|
|
31
|
+
- [Layout](#layout)
|
|
32
|
+
- [Theming](#theming)
|
|
33
|
+
- [Dark Mode](#dark-mode)
|
|
34
|
+
- [Configuration](#configuration)
|
|
35
|
+
- [Stimulus Controllers](#stimulus-controllers)
|
|
36
|
+
- [Testing](#testing)
|
|
37
|
+
- [Development](#development)
|
|
38
|
+
- [Security Considerations](#security-considerations)
|
|
39
|
+
- [Contributing](#contributing)
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
Add this line to your application's Gemfile:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
gem "shadcn-rails"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then execute:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bundle install
|
|
53
|
+
rails generate shadcn:install
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This will:
|
|
57
|
+
1. Create a configuration initializer at `config/initializers/shadcn.rb`
|
|
58
|
+
2. Add the required CSS imports to your application
|
|
59
|
+
3. Configure your Stimulus controllers
|
|
60
|
+
|
|
61
|
+
### Stylesheets
|
|
62
|
+
|
|
63
|
+
shadcn-rails includes two CSS files:
|
|
64
|
+
|
|
65
|
+
| File | Purpose |
|
|
66
|
+
|------|---------|
|
|
67
|
+
| `shadcn/base.css` | CSS variables for theming (colors, border radius), animations, and focus styles |
|
|
68
|
+
| `shadcn/components.css` | Component-specific styles for interactive elements (`data-state` attributes, custom inputs) |
|
|
69
|
+
|
|
70
|
+
**For Tailwind CSS** (application.tailwind.css):
|
|
71
|
+
|
|
72
|
+
```css
|
|
73
|
+
@import "shadcn/base";
|
|
74
|
+
@import "shadcn/components";
|
|
75
|
+
|
|
76
|
+
@tailwind base;
|
|
77
|
+
@tailwind components;
|
|
78
|
+
@tailwind utilities;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**For Sprockets** (application.css):
|
|
82
|
+
|
|
83
|
+
```css
|
|
84
|
+
/*
|
|
85
|
+
*= require shadcn/base
|
|
86
|
+
*= require shadcn/components
|
|
87
|
+
*= require_self
|
|
88
|
+
*/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The `components.css` file includes essential styles for:
|
|
92
|
+
- **Switch** - `data-state` based checked/unchecked styling
|
|
93
|
+
- **Slider** - Custom range input with fill indicator
|
|
94
|
+
- **Checkbox/Radio** - Native inputs with custom styling
|
|
95
|
+
- **Accordion/Collapsible** - Content animations
|
|
96
|
+
- **Dialog/Sheet/Popover** - Open/close animations and overlays
|
|
97
|
+
- **Tabs** - Active/inactive state styling
|
|
98
|
+
|
|
99
|
+
### Requirements
|
|
100
|
+
|
|
101
|
+
- Ruby >= 3.1
|
|
102
|
+
- Rails >= 7.0
|
|
103
|
+
- Tailwind CSS >= 3.0
|
|
104
|
+
- Stimulus >= 3.0
|
|
105
|
+
- ViewComponent >= 3.0
|
|
106
|
+
|
|
107
|
+
### Tailwind CSS Configuration
|
|
108
|
+
|
|
109
|
+
Ensure your `tailwind.config.js` includes the shadcn-rails color configuration:
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
module.exports = {
|
|
113
|
+
darkMode: 'class',
|
|
114
|
+
theme: {
|
|
115
|
+
extend: {
|
|
116
|
+
colors: {
|
|
117
|
+
border: "hsl(var(--border))",
|
|
118
|
+
input: "hsl(var(--input))",
|
|
119
|
+
ring: "hsl(var(--ring))",
|
|
120
|
+
background: "hsl(var(--background))",
|
|
121
|
+
foreground: "hsl(var(--foreground))",
|
|
122
|
+
primary: {
|
|
123
|
+
DEFAULT: "hsl(var(--primary))",
|
|
124
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
125
|
+
},
|
|
126
|
+
secondary: {
|
|
127
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
128
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
129
|
+
},
|
|
130
|
+
destructive: {
|
|
131
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
132
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
133
|
+
},
|
|
134
|
+
muted: {
|
|
135
|
+
DEFAULT: "hsl(var(--muted))",
|
|
136
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
137
|
+
},
|
|
138
|
+
accent: {
|
|
139
|
+
DEFAULT: "hsl(var(--accent))",
|
|
140
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
141
|
+
},
|
|
142
|
+
popover: {
|
|
143
|
+
DEFAULT: "hsl(var(--popover))",
|
|
144
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
145
|
+
},
|
|
146
|
+
card: {
|
|
147
|
+
DEFAULT: "hsl(var(--card))",
|
|
148
|
+
foreground: "hsl(var(--card-foreground))",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
borderRadius: {
|
|
152
|
+
lg: "var(--radius)",
|
|
153
|
+
md: "calc(var(--radius) - 2px)",
|
|
154
|
+
sm: "calc(var(--radius) - 4px)",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Quick Start
|
|
162
|
+
|
|
163
|
+
```erb
|
|
164
|
+
<%# Simple button %>
|
|
165
|
+
<%= render Shadcn::ButtonComponent.new { "Click me" } %>
|
|
166
|
+
|
|
167
|
+
<%# Button with variant %>
|
|
168
|
+
<%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Delete" } %>
|
|
169
|
+
|
|
170
|
+
<%# Card with slots %>
|
|
171
|
+
<%= render Shadcn::CardComponent.new do |card| %>
|
|
172
|
+
<% card.with_header do |header| %>
|
|
173
|
+
<% header.with_title { "Welcome" } %>
|
|
174
|
+
<% header.with_description { "Get started with shadcn-rails" } %>
|
|
175
|
+
<% end %>
|
|
176
|
+
<% card.with_content_slot do %>
|
|
177
|
+
<p>Your content here</p>
|
|
178
|
+
<% end %>
|
|
179
|
+
<% end %>
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Components
|
|
183
|
+
|
|
184
|
+
### Buttons & Actions
|
|
185
|
+
|
|
186
|
+
#### Button
|
|
187
|
+
|
|
188
|
+
Displays a button or a component that looks like a button.
|
|
189
|
+
|
|
190
|
+
```erb
|
|
191
|
+
<%# Variants %>
|
|
192
|
+
<%= render Shadcn::ButtonComponent.new(variant: :default) { "Default" } %>
|
|
193
|
+
<%= render Shadcn::ButtonComponent.new(variant: :secondary) { "Secondary" } %>
|
|
194
|
+
<%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Destructive" } %>
|
|
195
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline) { "Outline" } %>
|
|
196
|
+
<%= render Shadcn::ButtonComponent.new(variant: :ghost) { "Ghost" } %>
|
|
197
|
+
<%= render Shadcn::ButtonComponent.new(variant: :link) { "Link" } %>
|
|
198
|
+
|
|
199
|
+
<%# Sizes %>
|
|
200
|
+
<%= render Shadcn::ButtonComponent.new(size: :sm) { "Small" } %>
|
|
201
|
+
<%= render Shadcn::ButtonComponent.new(size: :default) { "Default" } %>
|
|
202
|
+
<%= render Shadcn::ButtonComponent.new(size: :lg) { "Large" } %>
|
|
203
|
+
<%= render Shadcn::ButtonComponent.new(size: :icon) { "+" } %>
|
|
204
|
+
|
|
205
|
+
<%# States %>
|
|
206
|
+
<%= render Shadcn::ButtonComponent.new(disabled: true) { "Disabled" } %>
|
|
207
|
+
|
|
208
|
+
<%# As link %>
|
|
209
|
+
<%= render Shadcn::ButtonComponent.new(href: "/path", variant: :outline) { "Link Button" } %>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Props:**
|
|
213
|
+
| Prop | Type | Default | Description |
|
|
214
|
+
|------|------|---------|-------------|
|
|
215
|
+
| `variant` | Symbol | `:default` | `:default`, `:secondary`, `:destructive`, `:outline`, `:ghost`, `:link` |
|
|
216
|
+
| `size` | Symbol | `:default` | `:default`, `:sm`, `:lg`, `:icon` |
|
|
217
|
+
| `disabled` | Boolean | `false` | Disables the button |
|
|
218
|
+
| `href` | String | `nil` | Renders as a link when provided |
|
|
219
|
+
| `type` | String | `"button"` | Button type attribute |
|
|
220
|
+
|
|
221
|
+
#### Toggle
|
|
222
|
+
|
|
223
|
+
A two-state button that can be either on or off.
|
|
224
|
+
|
|
225
|
+
```erb
|
|
226
|
+
<%= render Shadcn::ToggleComponent.new do %>
|
|
227
|
+
<svg><!-- icon --></svg>
|
|
228
|
+
<% end %>
|
|
229
|
+
|
|
230
|
+
<%= render Shadcn::ToggleComponent.new(variant: :outline, pressed: true) do %>
|
|
231
|
+
Bold
|
|
232
|
+
<% end %>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
#### Toggle Group
|
|
236
|
+
|
|
237
|
+
A set of two-state buttons that can be toggled on or off.
|
|
238
|
+
|
|
239
|
+
```erb
|
|
240
|
+
<%# Single selection %>
|
|
241
|
+
<%= render Shadcn::ToggleGroupComponent.new(type: :single) do |group| %>
|
|
242
|
+
<% group.with_item(value: "bold") { "B" } %>
|
|
243
|
+
<% group.with_item(value: "italic") { "I" } %>
|
|
244
|
+
<% group.with_item(value: "underline") { "U" } %>
|
|
245
|
+
<% end %>
|
|
246
|
+
|
|
247
|
+
<%# Multiple selection %>
|
|
248
|
+
<%= render Shadcn::ToggleGroupComponent.new(type: :multiple, variant: :outline) do |group| %>
|
|
249
|
+
<% group.with_item(value: "left") { "Left" } %>
|
|
250
|
+
<% group.with_item(value: "center") { "Center" } %>
|
|
251
|
+
<% group.with_item(value: "right") { "Right" } %>
|
|
252
|
+
<% end %>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Form Inputs
|
|
256
|
+
|
|
257
|
+
#### Input
|
|
258
|
+
|
|
259
|
+
Displays a form input field.
|
|
260
|
+
|
|
261
|
+
```erb
|
|
262
|
+
<%= render Shadcn::InputComponent.new(
|
|
263
|
+
type: :email,
|
|
264
|
+
placeholder: "you@example.com",
|
|
265
|
+
id: "email",
|
|
266
|
+
name: "user[email]"
|
|
267
|
+
) %>
|
|
268
|
+
|
|
269
|
+
<%# Disabled %>
|
|
270
|
+
<%= render Shadcn::InputComponent.new(
|
|
271
|
+
type: :text,
|
|
272
|
+
placeholder: "Disabled",
|
|
273
|
+
disabled: true
|
|
274
|
+
) %>
|
|
275
|
+
|
|
276
|
+
<%# With validation error %>
|
|
277
|
+
<%= render Shadcn::InputComponent.new(
|
|
278
|
+
type: :text,
|
|
279
|
+
class_name: "border-destructive"
|
|
280
|
+
) %>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Props:**
|
|
284
|
+
| Prop | Type | Default | Description |
|
|
285
|
+
|------|------|---------|-------------|
|
|
286
|
+
| `type` | Symbol/String | `:text` | Input type (`:text`, `:email`, `:password`, `:number`, `:search`, `:file`, etc.) |
|
|
287
|
+
| `placeholder` | String | `nil` | Placeholder text |
|
|
288
|
+
| `disabled` | Boolean | `false` | Disables the input |
|
|
289
|
+
| `required` | Boolean | `false` | Makes input required |
|
|
290
|
+
|
|
291
|
+
#### Textarea
|
|
292
|
+
|
|
293
|
+
Displays a multi-line text input.
|
|
294
|
+
|
|
295
|
+
```erb
|
|
296
|
+
<%= render Shadcn::TextareaComponent.new(
|
|
297
|
+
placeholder: "Type your message here...",
|
|
298
|
+
rows: 4,
|
|
299
|
+
name: "message"
|
|
300
|
+
) %>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### Label
|
|
304
|
+
|
|
305
|
+
Renders an accessible label associated with controls.
|
|
306
|
+
|
|
307
|
+
```erb
|
|
308
|
+
<%= render Shadcn::LabelComponent.new(for: "email") { "Email Address" } %>
|
|
309
|
+
<%= render Shadcn::InputComponent.new(id: "email", type: :email) %>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### Checkbox
|
|
313
|
+
|
|
314
|
+
A control that allows toggling between checked and not checked.
|
|
315
|
+
|
|
316
|
+
```erb
|
|
317
|
+
<%= render Shadcn::CheckboxComponent.new(
|
|
318
|
+
id: "terms",
|
|
319
|
+
label: "Accept terms and conditions"
|
|
320
|
+
) %>
|
|
321
|
+
|
|
322
|
+
<%# Checked by default %>
|
|
323
|
+
<%= render Shadcn::CheckboxComponent.new(
|
|
324
|
+
id: "newsletter",
|
|
325
|
+
label: "Subscribe to newsletter",
|
|
326
|
+
checked: true
|
|
327
|
+
) %>
|
|
328
|
+
|
|
329
|
+
<%# Disabled %>
|
|
330
|
+
<%= render Shadcn::CheckboxComponent.new(
|
|
331
|
+
id: "disabled",
|
|
332
|
+
label: "Disabled option",
|
|
333
|
+
disabled: true
|
|
334
|
+
) %>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
#### Switch
|
|
338
|
+
|
|
339
|
+
A control that allows toggling between a checked and not checked state.
|
|
340
|
+
|
|
341
|
+
```erb
|
|
342
|
+
<%= render Shadcn::SwitchComponent.new(
|
|
343
|
+
id: "airplane",
|
|
344
|
+
label: "Airplane Mode"
|
|
345
|
+
) %>
|
|
346
|
+
|
|
347
|
+
<%= render Shadcn::SwitchComponent.new(
|
|
348
|
+
id: "notifications",
|
|
349
|
+
label: "Enable notifications",
|
|
350
|
+
checked: true
|
|
351
|
+
) %>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### Radio Group
|
|
355
|
+
|
|
356
|
+
A set of checkable buttons where only one can be checked at a time.
|
|
357
|
+
|
|
358
|
+
```erb
|
|
359
|
+
<%= render Shadcn::RadioGroupComponent.new(name: "plan", default_value: "comfortable") do |group| %>
|
|
360
|
+
<% group.with_item(value: "default", label: "Default") %>
|
|
361
|
+
<% group.with_item(value: "comfortable", label: "Comfortable") %>
|
|
362
|
+
<% group.with_item(value: "compact", label: "Compact") %>
|
|
363
|
+
<% end %>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
#### Select
|
|
367
|
+
|
|
368
|
+
Displays a list of options for the user to pick from.
|
|
369
|
+
|
|
370
|
+
```erb
|
|
371
|
+
<%= render Shadcn::SelectComponent.new(placeholder: "Select a fruit") do |select| %>
|
|
372
|
+
<% select.with_group(label: "Fruits") do |group| %>
|
|
373
|
+
<% group.with_item(value: "apple") { "Apple" } %>
|
|
374
|
+
<% group.with_item(value: "banana") { "Banana" } %>
|
|
375
|
+
<% group.with_item(value: "orange") { "Orange" } %>
|
|
376
|
+
<% end %>
|
|
377
|
+
<% end %>
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Slider
|
|
381
|
+
|
|
382
|
+
An input where the user selects a value from within a given range.
|
|
383
|
+
|
|
384
|
+
```erb
|
|
385
|
+
<%= render Shadcn::SliderComponent.new(value: 50, max: 100) %>
|
|
386
|
+
<%= render Shadcn::SliderComponent.new(value: 25, min: 0, max: 100, step: 5) %>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Data Display
|
|
390
|
+
|
|
391
|
+
#### Badge
|
|
392
|
+
|
|
393
|
+
Displays a badge or label.
|
|
394
|
+
|
|
395
|
+
```erb
|
|
396
|
+
<%= render Shadcn::BadgeComponent.new(variant: :default) { "Default" } %>
|
|
397
|
+
<%= render Shadcn::BadgeComponent.new(variant: :secondary) { "Secondary" } %>
|
|
398
|
+
<%= render Shadcn::BadgeComponent.new(variant: :destructive) { "Error" } %>
|
|
399
|
+
<%= render Shadcn::BadgeComponent.new(variant: :outline) { "Outline" } %>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### Avatar
|
|
403
|
+
|
|
404
|
+
An image element with a fallback for representing the user.
|
|
405
|
+
|
|
406
|
+
```erb
|
|
407
|
+
<%# With image %>
|
|
408
|
+
<%= render Shadcn::AvatarComponent.new(
|
|
409
|
+
src: "https://example.com/avatar.jpg",
|
|
410
|
+
alt: "John Doe",
|
|
411
|
+
fallback: "JD"
|
|
412
|
+
) %>
|
|
413
|
+
|
|
414
|
+
<%# Without image (shows fallback) %>
|
|
415
|
+
<%= render Shadcn::AvatarComponent.new(
|
|
416
|
+
alt: "Jane Smith",
|
|
417
|
+
fallback: "JS"
|
|
418
|
+
) %>
|
|
419
|
+
|
|
420
|
+
<%# Sizes %>
|
|
421
|
+
<%= render Shadcn::AvatarComponent.new(size: :sm, fallback: "SM") %>
|
|
422
|
+
<%= render Shadcn::AvatarComponent.new(size: :default, fallback: "MD") %>
|
|
423
|
+
<%= render Shadcn::AvatarComponent.new(size: :lg, fallback: "LG") %>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### Card
|
|
427
|
+
|
|
428
|
+
Displays a card with header, content, and footer.
|
|
429
|
+
|
|
430
|
+
```erb
|
|
431
|
+
<%= render Shadcn::CardComponent.new do |card| %>
|
|
432
|
+
<% card.with_header do |header| %>
|
|
433
|
+
<% header.with_title { "Card Title" } %>
|
|
434
|
+
<% header.with_description { "Card description goes here" } %>
|
|
435
|
+
<% end %>
|
|
436
|
+
<% card.with_content_slot do %>
|
|
437
|
+
<p>This is the main content of the card.</p>
|
|
438
|
+
<% end %>
|
|
439
|
+
<% card.with_footer do %>
|
|
440
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline) { "Cancel" } %>
|
|
441
|
+
<%= render Shadcn::ButtonComponent.new { "Save" } %>
|
|
442
|
+
<% end %>
|
|
443
|
+
<% end %>
|
|
444
|
+
|
|
445
|
+
<%# Simple card (content only) %>
|
|
446
|
+
<%= render Shadcn::CardComponent.new do |card| %>
|
|
447
|
+
<% card.with_content_slot(standalone: true) do %>
|
|
448
|
+
<p>A simple card with just content.</p>
|
|
449
|
+
<% end %>
|
|
450
|
+
<% end %>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
#### Table
|
|
454
|
+
|
|
455
|
+
A responsive table component.
|
|
456
|
+
|
|
457
|
+
```erb
|
|
458
|
+
<%= render Shadcn::TableComponent.new do |table| %>
|
|
459
|
+
<% table.with_header do |header| %>
|
|
460
|
+
<% header.with_row do |row| %>
|
|
461
|
+
<% row.with_head { "Name" } %>
|
|
462
|
+
<% row.with_head { "Status" } %>
|
|
463
|
+
<% row.with_head(class_name: "text-right") { "Amount" } %>
|
|
464
|
+
<% end %>
|
|
465
|
+
<% end %>
|
|
466
|
+
<% table.with_body do |body| %>
|
|
467
|
+
<% body.with_row do |row| %>
|
|
468
|
+
<% row.with_cell { "John Doe" } %>
|
|
469
|
+
<% row.with_cell { "Active" } %>
|
|
470
|
+
<% row.with_cell(class_name: "text-right") { "$250.00" } %>
|
|
471
|
+
<% end %>
|
|
472
|
+
<% end %>
|
|
473
|
+
<% end %>
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### Progress
|
|
477
|
+
|
|
478
|
+
Displays an indicator showing the completion progress of a task.
|
|
479
|
+
|
|
480
|
+
```erb
|
|
481
|
+
<%= render Shadcn::ProgressComponent.new(value: 33) %>
|
|
482
|
+
<%= render Shadcn::ProgressComponent.new(value: 66) %>
|
|
483
|
+
<%= render Shadcn::ProgressComponent.new(value: 100) %>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
#### Skeleton
|
|
487
|
+
|
|
488
|
+
Use to show a placeholder while content is loading.
|
|
489
|
+
|
|
490
|
+
```erb
|
|
491
|
+
<div class="flex items-center space-x-4">
|
|
492
|
+
<%= render Shadcn::SkeletonComponent.new(class_name: "h-12 w-12 rounded-full") %>
|
|
493
|
+
<div class="space-y-2">
|
|
494
|
+
<%= render Shadcn::SkeletonComponent.new(class_name: "h-4 w-[250px]") %>
|
|
495
|
+
<%= render Shadcn::SkeletonComponent.new(class_name: "h-4 w-[200px]") %>
|
|
496
|
+
</div>
|
|
497
|
+
</div>
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
#### Aspect Ratio
|
|
501
|
+
|
|
502
|
+
Displays content within a desired ratio.
|
|
503
|
+
|
|
504
|
+
```erb
|
|
505
|
+
<%= render Shadcn::AspectRatioComponent.new(ratio: "16/9") do %>
|
|
506
|
+
<img src="image.jpg" class="object-cover w-full h-full" />
|
|
507
|
+
<% end %>
|
|
508
|
+
|
|
509
|
+
<%# Common ratios: "1/1", "4/3", "16/9", "21/9" %>
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### Feedback
|
|
513
|
+
|
|
514
|
+
#### Alert
|
|
515
|
+
|
|
516
|
+
Displays a callout for user attention.
|
|
517
|
+
|
|
518
|
+
```erb
|
|
519
|
+
<%# Default alert %>
|
|
520
|
+
<%= render Shadcn::AlertComponent.new do |alert| %>
|
|
521
|
+
<% alert.with_title { "Heads up!" } %>
|
|
522
|
+
<% alert.with_description { "You can add components using the CLI." } %>
|
|
523
|
+
<% end %>
|
|
524
|
+
|
|
525
|
+
<%# Destructive alert %>
|
|
526
|
+
<%= render Shadcn::AlertComponent.new(variant: :destructive) do |alert| %>
|
|
527
|
+
<% alert.with_title { "Error" } %>
|
|
528
|
+
<% alert.with_description { "Your session has expired." } %>
|
|
529
|
+
<% end %>
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Tooltip
|
|
533
|
+
|
|
534
|
+
A popup that displays information when hovering.
|
|
535
|
+
|
|
536
|
+
```erb
|
|
537
|
+
<%= render Shadcn::TooltipComponent.new(content: "Add to library", side: :top) do |tooltip| %>
|
|
538
|
+
<% tooltip.with_trigger do %>
|
|
539
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline, size: :icon) { "+" } %>
|
|
540
|
+
<% end %>
|
|
541
|
+
<% end %>
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**Props:**
|
|
545
|
+
| Prop | Type | Default | Description |
|
|
546
|
+
|------|------|---------|-------------|
|
|
547
|
+
| `content` | String | required | Tooltip text |
|
|
548
|
+
| `side` | Symbol | `:top` | `:top`, `:bottom`, `:left`, `:right` |
|
|
549
|
+
| `delay` | Integer | `200` | Delay in milliseconds before showing |
|
|
550
|
+
|
|
551
|
+
### Overlays
|
|
552
|
+
|
|
553
|
+
#### Dialog
|
|
554
|
+
|
|
555
|
+
A modal dialog window.
|
|
556
|
+
|
|
557
|
+
```erb
|
|
558
|
+
<%= render Shadcn::DialogComponent.new do |dialog| %>
|
|
559
|
+
<% dialog.with_trigger do %>
|
|
560
|
+
<%= render Shadcn::ButtonComponent.new { "Open Dialog" } %>
|
|
561
|
+
<% end %>
|
|
562
|
+
<% dialog.with_body do |body| %>
|
|
563
|
+
<% body.with_header do |header| %>
|
|
564
|
+
<% header.with_title { "Edit Profile" } %>
|
|
565
|
+
<% header.with_description { "Make changes to your profile here." } %>
|
|
566
|
+
<% end %>
|
|
567
|
+
<div class="py-4">
|
|
568
|
+
<%# Form content %>
|
|
569
|
+
</div>
|
|
570
|
+
<% body.with_footer do %>
|
|
571
|
+
<%= render Shadcn::ButtonComponent.new { "Save changes" } %>
|
|
572
|
+
<% end %>
|
|
573
|
+
<% end %>
|
|
574
|
+
<% end %>
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
##### Dialog with ID (for Turbo Stream targeting)
|
|
578
|
+
|
|
579
|
+
```erb
|
|
580
|
+
<%= render Shadcn::DialogComponent.new(id: "edit-profile-dialog") do |dialog| %>
|
|
581
|
+
<%# ... %>
|
|
582
|
+
<% end %>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
##### Closing Dialog Programmatically
|
|
586
|
+
|
|
587
|
+
Use Stimulus actions to close dialogs from buttons:
|
|
588
|
+
|
|
589
|
+
```erb
|
|
590
|
+
<%# Cancel button closes immediately %>
|
|
591
|
+
<%= render Shadcn::ButtonComponent.new(
|
|
592
|
+
variant: :outline,
|
|
593
|
+
type: "button",
|
|
594
|
+
data: { action: "click->shadcn--dialog#close" }
|
|
595
|
+
) { "Cancel" } %>
|
|
596
|
+
|
|
597
|
+
<%# Or close from any element %>
|
|
598
|
+
<button data-action="click->shadcn--dialog#close">Close</button>
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
##### Forms Inside Dialogs
|
|
602
|
+
|
|
603
|
+
For forms that should close the dialog only on successful submission:
|
|
604
|
+
|
|
605
|
+
```erb
|
|
606
|
+
<%= render Shadcn::DialogComponent.new do |dialog| %>
|
|
607
|
+
<% dialog.with_trigger do %>
|
|
608
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline) { "Edit Profile" } %>
|
|
609
|
+
<% end %>
|
|
610
|
+
<% dialog.with_body do |body| %>
|
|
611
|
+
<% body.with_header do |header| %>
|
|
612
|
+
<% header.with_title { "Edit Profile" } %>
|
|
613
|
+
<% header.with_description { "Make changes to your profile here." } %>
|
|
614
|
+
<% end %>
|
|
615
|
+
<%= form_with model: @user, data: { remote: "true" } do |f| %>
|
|
616
|
+
<div class="space-y-4">
|
|
617
|
+
<%= render Shadcn::LabelComponent.new(for: "name") { "Name" } %>
|
|
618
|
+
<%= render Shadcn::InputComponent.new(id: "name", name: "user[name]", value: @user.name) %>
|
|
619
|
+
</div>
|
|
620
|
+
<div class="flex justify-end gap-3 mt-4">
|
|
621
|
+
<%= render Shadcn::ButtonComponent.new(
|
|
622
|
+
variant: :outline,
|
|
623
|
+
type: "button",
|
|
624
|
+
data: { action: "click->shadcn--dialog#close" }
|
|
625
|
+
) { "Cancel" } %>
|
|
626
|
+
<%= render Shadcn::ButtonComponent.new(type: "submit") { "Save Changes" } %>
|
|
627
|
+
</div>
|
|
628
|
+
<% end %>
|
|
629
|
+
<% end %>
|
|
630
|
+
<% end %>
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
**Props:**
|
|
634
|
+
| Prop | Type | Default | Description |
|
|
635
|
+
|------|------|---------|-------------|
|
|
636
|
+
| `id` | String | `nil` | Unique identifier for Turbo Stream targeting |
|
|
637
|
+
| `open` | Boolean | `false` | Whether dialog starts open |
|
|
638
|
+
| `modal` | Boolean | `true` | Whether dialog traps focus and blocks interaction |
|
|
639
|
+
|
|
640
|
+
#### Alert Dialog
|
|
641
|
+
|
|
642
|
+
A modal dialog for destructive or important actions.
|
|
643
|
+
|
|
644
|
+
```erb
|
|
645
|
+
<%= render Shadcn::AlertDialogComponent.new do |dialog| %>
|
|
646
|
+
<% dialog.with_trigger do %>
|
|
647
|
+
<%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Delete Account" } %>
|
|
648
|
+
<% end %>
|
|
649
|
+
<% dialog.with_body do |body| %>
|
|
650
|
+
<% body.with_header do |header| %>
|
|
651
|
+
<% header.with_title { "Are you absolutely sure?" } %>
|
|
652
|
+
<% header.with_description { "This action cannot be undone." } %>
|
|
653
|
+
<% end %>
|
|
654
|
+
<% body.with_footer do %>
|
|
655
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline, data: { action: "click->shadcn--alert-dialog#close" }) { "Cancel" } %>
|
|
656
|
+
<%= render Shadcn::ButtonComponent.new(variant: :destructive) { "Delete" } %>
|
|
657
|
+
<% end %>
|
|
658
|
+
<% end %>
|
|
659
|
+
<% end %>
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
#### Sheet
|
|
663
|
+
|
|
664
|
+
Extends the Dialog component to display content that complements the main content.
|
|
665
|
+
|
|
666
|
+
```erb
|
|
667
|
+
<%= render Shadcn::SheetComponent.new(side: :right) do |sheet| %>
|
|
668
|
+
<% sheet.with_trigger do %>
|
|
669
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Sheet" } %>
|
|
670
|
+
<% end %>
|
|
671
|
+
<% sheet.with_body do |body| %>
|
|
672
|
+
<% body.with_header do |header| %>
|
|
673
|
+
<% header.with_title { "Settings" } %>
|
|
674
|
+
<% header.with_description { "Configure your preferences." } %>
|
|
675
|
+
<% end %>
|
|
676
|
+
<div class="py-4">
|
|
677
|
+
<%# Sheet content %>
|
|
678
|
+
</div>
|
|
679
|
+
<% end %>
|
|
680
|
+
<% end %>
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
**Props:**
|
|
684
|
+
| Prop | Type | Default | Description |
|
|
685
|
+
|------|------|---------|-------------|
|
|
686
|
+
| `side` | Symbol | `:right` | `:top`, `:right`, `:bottom`, `:left` |
|
|
687
|
+
|
|
688
|
+
#### Drawer
|
|
689
|
+
|
|
690
|
+
A drawer component for mobile interfaces.
|
|
691
|
+
|
|
692
|
+
```erb
|
|
693
|
+
<%= render Shadcn::DrawerComponent.new do |drawer| %>
|
|
694
|
+
<% drawer.with_trigger do %>
|
|
695
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Drawer" } %>
|
|
696
|
+
<% end %>
|
|
697
|
+
<% drawer.with_body do |body| %>
|
|
698
|
+
<% body.with_header do |header| %>
|
|
699
|
+
<% header.with_title { "Drawer Title" } %>
|
|
700
|
+
<% header.with_description { "Drawer description" } %>
|
|
701
|
+
<% end %>
|
|
702
|
+
<div class="p-4">
|
|
703
|
+
<%# Drawer content %>
|
|
704
|
+
</div>
|
|
705
|
+
<% body.with_footer do %>
|
|
706
|
+
<%= render Shadcn::ButtonComponent.new(class_name: "w-full") { "Submit" } %>
|
|
707
|
+
<% end %>
|
|
708
|
+
<% end %>
|
|
709
|
+
<% end %>
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
#### Popover
|
|
713
|
+
|
|
714
|
+
Displays rich content in a portal, triggered by a button.
|
|
715
|
+
|
|
716
|
+
```erb
|
|
717
|
+
<%= render Shadcn::PopoverComponent.new do |popover| %>
|
|
718
|
+
<% popover.with_trigger do %>
|
|
719
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Popover" } %>
|
|
720
|
+
<% end %>
|
|
721
|
+
<% popover.with_content do %>
|
|
722
|
+
<div class="grid gap-4">
|
|
723
|
+
<h4 class="font-medium">Dimensions</h4>
|
|
724
|
+
<p class="text-sm text-muted-foreground">Set the dimensions for the layer.</p>
|
|
725
|
+
</div>
|
|
726
|
+
<% end %>
|
|
727
|
+
<% end %>
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
#### Hover Card
|
|
731
|
+
|
|
732
|
+
For sighted users to preview content available behind a link.
|
|
733
|
+
|
|
734
|
+
```erb
|
|
735
|
+
<%= render Shadcn::HoverCardComponent.new do |card| %>
|
|
736
|
+
<% card.with_trigger do %>
|
|
737
|
+
<a href="#" class="underline">@shadcn</a>
|
|
738
|
+
<% end %>
|
|
739
|
+
<% card.with_card_content do %>
|
|
740
|
+
<div class="flex space-x-4">
|
|
741
|
+
<%= render Shadcn::AvatarComponent.new(src: "avatar.jpg", fallback: "SC") %>
|
|
742
|
+
<div>
|
|
743
|
+
<h4 class="text-sm font-semibold">@shadcn</h4>
|
|
744
|
+
<p class="text-sm">Creator of shadcn/ui</p>
|
|
745
|
+
</div>
|
|
746
|
+
</div>
|
|
747
|
+
<% end %>
|
|
748
|
+
<% end %>
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
#### Dropdown Menu
|
|
752
|
+
|
|
753
|
+
Displays a menu of actions or functions triggered by a button.
|
|
754
|
+
|
|
755
|
+
```erb
|
|
756
|
+
<%= render Shadcn::DropdownMenuComponent.new do |menu| %>
|
|
757
|
+
<% menu.with_trigger do %>
|
|
758
|
+
<%= render Shadcn::ButtonComponent.new(variant: :outline) { "Open Menu" } %>
|
|
759
|
+
<% end %>
|
|
760
|
+
<% menu.with_content do |content| %>
|
|
761
|
+
<% content.with_label { "My Account" } %>
|
|
762
|
+
<% content.with_separator %>
|
|
763
|
+
<% content.with_item { "Profile" } %>
|
|
764
|
+
<% content.with_item { "Settings" } %>
|
|
765
|
+
<% content.with_separator %>
|
|
766
|
+
<% content.with_item { "Log out" } %>
|
|
767
|
+
<% end %>
|
|
768
|
+
<% end %>
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
### Navigation
|
|
772
|
+
|
|
773
|
+
#### Tabs
|
|
774
|
+
|
|
775
|
+
A set of layered sections of content that are displayed one at a time.
|
|
776
|
+
|
|
777
|
+
```erb
|
|
778
|
+
<%= render Shadcn::TabsComponent.new(default_value: "account") do |tabs| %>
|
|
779
|
+
<% tabs.with_list do |list| %>
|
|
780
|
+
<% list.with_trigger(value: "account") { "Account" } %>
|
|
781
|
+
<% list.with_trigger(value: "password") { "Password" } %>
|
|
782
|
+
<% list.with_trigger(value: "settings", disabled: true) { "Settings" } %>
|
|
783
|
+
<% end %>
|
|
784
|
+
<% tabs.with_panel(value: "account") do %>
|
|
785
|
+
<p>Account settings here.</p>
|
|
786
|
+
<% end %>
|
|
787
|
+
<% tabs.with_panel(value: "password") do %>
|
|
788
|
+
<p>Password settings here.</p>
|
|
789
|
+
<% end %>
|
|
790
|
+
<% end %>
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
##### URL Synchronization
|
|
794
|
+
|
|
795
|
+
Sync the active tab state with the URL query parameter for shareable links and browser history support:
|
|
796
|
+
|
|
797
|
+
```erb
|
|
798
|
+
<%# Tab state syncs to URL: /settings?tab=billing %>
|
|
799
|
+
<%= render Shadcn::TabsComponent.new(default_value: "general", url_param: "tab") do |tabs| %>
|
|
800
|
+
<% tabs.with_list do |list| %>
|
|
801
|
+
<% list.with_trigger(value: "general") { "General" } %>
|
|
802
|
+
<% list.with_trigger(value: "billing") { "Billing" } %>
|
|
803
|
+
<% list.with_trigger(value: "security") { "Security" } %>
|
|
804
|
+
<% end %>
|
|
805
|
+
<% tabs.with_panel(value: "general") do %>
|
|
806
|
+
<p>General settings</p>
|
|
807
|
+
<% end %>
|
|
808
|
+
<% tabs.with_panel(value: "billing") do %>
|
|
809
|
+
<p>Billing settings</p>
|
|
810
|
+
<% end %>
|
|
811
|
+
<% tabs.with_panel(value: "security") do %>
|
|
812
|
+
<p>Security settings</p>
|
|
813
|
+
<% end %>
|
|
814
|
+
<% end %>
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
When `url_param` is set:
|
|
818
|
+
- The URL updates when tabs are clicked (e.g., `?tab=billing`)
|
|
819
|
+
- Direct navigation to URLs with the parameter selects the correct tab
|
|
820
|
+
- Browser back/forward navigation works as expected
|
|
821
|
+
|
|
822
|
+
**Props:**
|
|
823
|
+
| Prop | Type | Default | Description |
|
|
824
|
+
|------|------|---------|-------------|
|
|
825
|
+
| `default_value` | String | `nil` | Initially active tab value |
|
|
826
|
+
| `url_param` | String | `nil` | URL query parameter name for state sync |
|
|
827
|
+
|
|
828
|
+
#### Accordion
|
|
829
|
+
|
|
830
|
+
A vertically stacked set of interactive headings that reveal sections of content.
|
|
831
|
+
|
|
832
|
+
```erb
|
|
833
|
+
<%= render Shadcn::AccordionComponent.new(type: :single, collapsible: true) do |accordion| %>
|
|
834
|
+
<% accordion.with_item(value: "item-1") do |item| %>
|
|
835
|
+
<% item.with_trigger { "Is it accessible?" } %>
|
|
836
|
+
<% item.with_body { "Yes. It adheres to WAI-ARIA design patterns." } %>
|
|
837
|
+
<% end %>
|
|
838
|
+
<% accordion.with_item(value: "item-2") do |item| %>
|
|
839
|
+
<% item.with_trigger { "Is it styled?" } %>
|
|
840
|
+
<% item.with_body { "Yes. It comes with default styles." } %>
|
|
841
|
+
<% end %>
|
|
842
|
+
<% end %>
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
**Props:**
|
|
846
|
+
| Prop | Type | Default | Description |
|
|
847
|
+
|------|------|---------|-------------|
|
|
848
|
+
| `type` | Symbol | `:single` | `:single` (one open), `:multiple` (many open) |
|
|
849
|
+
| `collapsible` | Boolean | `false` | Allow closing all items |
|
|
850
|
+
| `default_value` | String | `nil` | Initially open item(s) |
|
|
851
|
+
|
|
852
|
+
#### Breadcrumb
|
|
853
|
+
|
|
854
|
+
Displays the path to the current resource using a hierarchy of links.
|
|
855
|
+
|
|
856
|
+
```erb
|
|
857
|
+
<%= render Shadcn::BreadcrumbComponent.new do |breadcrumb| %>
|
|
858
|
+
<% breadcrumb.with_item(href: "/") { "Home" } %>
|
|
859
|
+
<% breadcrumb.with_item(href: "/products") { "Products" } %>
|
|
860
|
+
<% breadcrumb.with_item(current: true) { "Widget" } %>
|
|
861
|
+
<% end %>
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
#### Pagination
|
|
865
|
+
|
|
866
|
+
Pagination with page navigation, next and previous links. Supports three usage patterns:
|
|
867
|
+
|
|
868
|
+
**1. Auto-generated from Kaminari collection:**
|
|
869
|
+
|
|
870
|
+
```erb
|
|
871
|
+
<%# Works with Kaminari paginated collections %>
|
|
872
|
+
<%= render Shadcn::PaginationComponent.new(collection: @posts) %>
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
**2. Auto-generated from will_paginate collection:**
|
|
876
|
+
|
|
877
|
+
```erb
|
|
878
|
+
<%# Works with will_paginate collections %>
|
|
879
|
+
<%= render Shadcn::PaginationComponent.new(collection: @users) %>
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
**3. Auto-generated from Pagy object:**
|
|
883
|
+
|
|
884
|
+
```erb
|
|
885
|
+
<%# Works with Pagy pagination objects %>
|
|
886
|
+
<%= render Shadcn::PaginationComponent.new(pagy: @pagy) %>
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
**4. Custom URL builder:**
|
|
890
|
+
|
|
891
|
+
```erb
|
|
892
|
+
<%# Use a custom URL builder for complex routes %>
|
|
893
|
+
<%= render Shadcn::PaginationComponent.new(
|
|
894
|
+
collection: @posts,
|
|
895
|
+
url_builder: ->(page) { posts_path(page: page, sort: params[:sort]) }
|
|
896
|
+
) %>
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
**5. Full slot-based control:**
|
|
900
|
+
|
|
901
|
+
```erb
|
|
902
|
+
<%= render Shadcn::PaginationComponent.new do |pagination| %>
|
|
903
|
+
<% pagination.with_pagination_content do |content| %>
|
|
904
|
+
<% content.with_previous(href: "?page=1") %>
|
|
905
|
+
<% content.with_item(href: "?page=1") { "1" } %>
|
|
906
|
+
<% content.with_item(href: "?page=2", active: true) { "2" } %>
|
|
907
|
+
<% content.with_item(href: "?page=3") { "3" } %>
|
|
908
|
+
<% content.with_ellipse %>
|
|
909
|
+
<% content.with_item(href: "?page=10") { "10" } %>
|
|
910
|
+
<% content.with_next_page(href: "?page=3") %>
|
|
911
|
+
<% end %>
|
|
912
|
+
<% end %>
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
**6. Using the `shadcn_paginate` helper:**
|
|
916
|
+
|
|
917
|
+
```erb
|
|
918
|
+
<%# Simple one-liner that auto-detects your pagination gem %>
|
|
919
|
+
<%= shadcn_paginate @posts %>
|
|
920
|
+
|
|
921
|
+
<%# With Pagy %>
|
|
922
|
+
<%= shadcn_paginate @pagy %>
|
|
923
|
+
|
|
924
|
+
<%# Custom URL builder and window size %>
|
|
925
|
+
<%= shadcn_paginate @posts,
|
|
926
|
+
url_builder: ->(page) { posts_path(page: page) },
|
|
927
|
+
window: 3 %>
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
| Prop | Type | Default | Description |
|
|
931
|
+
|------|------|---------|-------------|
|
|
932
|
+
| `collection` | Object | `nil` | Kaminari or will_paginate collection |
|
|
933
|
+
| `pagy` | Object | `nil` | Pagy pagination object |
|
|
934
|
+
| `url_builder` | Proc | `"?page=N"` | Lambda to generate page URLs |
|
|
935
|
+
| `window` | Integer | `2` | Pages to show around current page |
|
|
936
|
+
|
|
937
|
+
**Supported Pagination Gems:**
|
|
938
|
+
|
|
939
|
+
| Gem | Usage |
|
|
940
|
+
|-----|-------|
|
|
941
|
+
| [Kaminari](https://github.com/kaminari/kaminari) | `collection: @posts.page(1).per(10)` |
|
|
942
|
+
| [will_paginate](https://github.com/mislav/will_paginate) | `collection: @posts.paginate(page: 1)` |
|
|
943
|
+
| [Pagy](https://github.com/ddnexus/pagy) | `pagy: @pagy` (from `pagy(@posts)`) |
|
|
944
|
+
|
|
945
|
+
#### Collapsible
|
|
946
|
+
|
|
947
|
+
An interactive component which expands/collapses a panel.
|
|
948
|
+
|
|
949
|
+
```erb
|
|
950
|
+
<%= render Shadcn::CollapsibleComponent.new do |collapsible| %>
|
|
951
|
+
<% collapsible.with_trigger do %>
|
|
952
|
+
<div class="flex items-center justify-between px-4 py-2 border rounded-md">
|
|
953
|
+
<span>Click to expand</span>
|
|
954
|
+
<svg><!-- chevron icon --></svg>
|
|
955
|
+
</div>
|
|
956
|
+
<% end %>
|
|
957
|
+
<% collapsible.with_content do %>
|
|
958
|
+
<div class="mt-2 p-4 border rounded-md">
|
|
959
|
+
Collapsible content here
|
|
960
|
+
</div>
|
|
961
|
+
<% end %>
|
|
962
|
+
<% end %>
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### Layout
|
|
966
|
+
|
|
967
|
+
#### Separator
|
|
968
|
+
|
|
969
|
+
Visually or semantically separates content.
|
|
970
|
+
|
|
971
|
+
```erb
|
|
972
|
+
<%# Horizontal separator %>
|
|
973
|
+
<%= render Shadcn::SeparatorComponent.new %>
|
|
974
|
+
|
|
975
|
+
<%# Vertical separator %>
|
|
976
|
+
<div class="flex h-5 items-center space-x-4">
|
|
977
|
+
<span>Blog</span>
|
|
978
|
+
<%= render Shadcn::SeparatorComponent.new(orientation: :vertical) %>
|
|
979
|
+
<span>Docs</span>
|
|
980
|
+
</div>
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
#### Scroll Area
|
|
984
|
+
|
|
985
|
+
Augments native scroll functionality for custom, cross-browser styling.
|
|
986
|
+
|
|
987
|
+
```erb
|
|
988
|
+
<%= render Shadcn::ScrollAreaComponent.new(class_name: "h-[200px] w-[350px] rounded-md border p-4") do %>
|
|
989
|
+
<div class="space-y-4">
|
|
990
|
+
<% 20.times do |i| %>
|
|
991
|
+
<div>Item <%= i + 1 %></div>
|
|
992
|
+
<% end %>
|
|
993
|
+
</div>
|
|
994
|
+
<% end %>
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
## Theming
|
|
998
|
+
|
|
999
|
+
### Available Themes
|
|
1000
|
+
|
|
1001
|
+
shadcn-rails includes 5 built-in color themes:
|
|
1002
|
+
|
|
1003
|
+
| Theme | Description |
|
|
1004
|
+
|-------|-------------|
|
|
1005
|
+
| `neutral` | Clean grayscale palette (default) |
|
|
1006
|
+
| `slate` | Cool blue-gray tones |
|
|
1007
|
+
| `stone` | Warm brown-gray tones |
|
|
1008
|
+
| `gray` | Standard gray palette |
|
|
1009
|
+
| `zinc` | Cool gray with slight purple tint |
|
|
1010
|
+
|
|
1011
|
+
### Switching Themes
|
|
1012
|
+
|
|
1013
|
+
```ruby
|
|
1014
|
+
# config/initializers/shadcn.rb
|
|
1015
|
+
Shadcn::Rails.configure do |config|
|
|
1016
|
+
config.base_color = "slate" # neutral, slate, stone, gray, zinc
|
|
1017
|
+
end
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
Or use the generator:
|
|
1021
|
+
|
|
1022
|
+
```bash
|
|
1023
|
+
rails generate shadcn:theme slate
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### CSS Variables
|
|
1027
|
+
|
|
1028
|
+
shadcn-rails uses CSS variables for theming, matching the shadcn/ui approach:
|
|
1029
|
+
|
|
1030
|
+
```css
|
|
1031
|
+
:root {
|
|
1032
|
+
--background: 0 0% 100%;
|
|
1033
|
+
--foreground: 0 0% 3.9%;
|
|
1034
|
+
--card: 0 0% 100%;
|
|
1035
|
+
--card-foreground: 0 0% 3.9%;
|
|
1036
|
+
--popover: 0 0% 100%;
|
|
1037
|
+
--popover-foreground: 0 0% 3.9%;
|
|
1038
|
+
--primary: 0 0% 9%;
|
|
1039
|
+
--primary-foreground: 0 0% 98%;
|
|
1040
|
+
--secondary: 0 0% 96.1%;
|
|
1041
|
+
--secondary-foreground: 0 0% 9%;
|
|
1042
|
+
--muted: 0 0% 96.1%;
|
|
1043
|
+
--muted-foreground: 0 0% 45.1%;
|
|
1044
|
+
--accent: 0 0% 96.1%;
|
|
1045
|
+
--accent-foreground: 0 0% 9%;
|
|
1046
|
+
--destructive: 0 84.2% 60.2%;
|
|
1047
|
+
--destructive-foreground: 0 0% 98%;
|
|
1048
|
+
--border: 0 0% 89.8%;
|
|
1049
|
+
--input: 0 0% 89.8%;
|
|
1050
|
+
--ring: 0 0% 3.9%;
|
|
1051
|
+
--radius: 0.5rem;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.dark {
|
|
1055
|
+
--background: 0 0% 3.9%;
|
|
1056
|
+
--foreground: 0 0% 98%;
|
|
1057
|
+
/* ... dark mode overrides */
|
|
1058
|
+
}
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
### Custom Themes
|
|
1062
|
+
|
|
1063
|
+
Register custom themes in your initializer:
|
|
1064
|
+
|
|
1065
|
+
```ruby
|
|
1066
|
+
Shadcn::Rails.configure do |config|
|
|
1067
|
+
config.register_theme(:brand, {
|
|
1068
|
+
primary: "220 90% 56%",
|
|
1069
|
+
primary_foreground: "0 0% 100%",
|
|
1070
|
+
# ... other variables
|
|
1071
|
+
})
|
|
1072
|
+
end
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
## Dark Mode
|
|
1076
|
+
|
|
1077
|
+
### Class Strategy (Recommended)
|
|
1078
|
+
|
|
1079
|
+
Add the `dark` class to your `<html>` element:
|
|
1080
|
+
|
|
1081
|
+
```html
|
|
1082
|
+
<html class="dark">
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
Toggle with JavaScript:
|
|
1086
|
+
|
|
1087
|
+
```javascript
|
|
1088
|
+
// Toggle dark mode
|
|
1089
|
+
document.documentElement.classList.toggle('dark')
|
|
1090
|
+
|
|
1091
|
+
// Check system preference
|
|
1092
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
1093
|
+
document.documentElement.classList.add('dark')
|
|
1094
|
+
}
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
### Media Strategy
|
|
1098
|
+
|
|
1099
|
+
Use the system preference automatically:
|
|
1100
|
+
|
|
1101
|
+
```ruby
|
|
1102
|
+
Shadcn::Rails.configure do |config|
|
|
1103
|
+
config.dark_mode = :media
|
|
1104
|
+
end
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
## Configuration
|
|
1108
|
+
|
|
1109
|
+
Full configuration options:
|
|
1110
|
+
|
|
1111
|
+
```ruby
|
|
1112
|
+
# config/initializers/shadcn.rb
|
|
1113
|
+
Shadcn::Rails.configure do |config|
|
|
1114
|
+
# Base color theme: neutral, stone, zinc, gray, slate
|
|
1115
|
+
config.base_color = "neutral"
|
|
1116
|
+
|
|
1117
|
+
# Use CSS variables for theming
|
|
1118
|
+
config.css_variables = true
|
|
1119
|
+
|
|
1120
|
+
# Dark mode strategy: :class, :media, or :selector
|
|
1121
|
+
config.dark_mode = :class
|
|
1122
|
+
|
|
1123
|
+
# Default border radius
|
|
1124
|
+
config.radius = "0.5rem"
|
|
1125
|
+
|
|
1126
|
+
# Tailwind class prefix (if using one)
|
|
1127
|
+
config.tailwind_prefix = ""
|
|
1128
|
+
|
|
1129
|
+
# Icon library: :lucide (default), :heroicons
|
|
1130
|
+
config.icon_library = :lucide
|
|
1131
|
+
end
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
## Stimulus Controllers
|
|
1135
|
+
|
|
1136
|
+
Interactive components require Stimulus controllers. Setup depends on your JavaScript bundler.
|
|
1137
|
+
|
|
1138
|
+
### Importmap-rails (Rails Default)
|
|
1139
|
+
|
|
1140
|
+
Add to your `config/importmap.rb`:
|
|
1141
|
+
|
|
1142
|
+
```ruby
|
|
1143
|
+
pin "shadcn-rails", to: "shadcn/index.js"
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
Then in `app/javascript/controllers/index.js`:
|
|
1147
|
+
|
|
1148
|
+
```javascript
|
|
1149
|
+
import { Application } from "@hotwired/stimulus"
|
|
1150
|
+
import { registerShadcnControllers } from "shadcn-rails"
|
|
1151
|
+
|
|
1152
|
+
const application = Application.start()
|
|
1153
|
+
registerShadcnControllers(application)
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
### esbuild
|
|
1157
|
+
|
|
1158
|
+
Install the npm package:
|
|
1159
|
+
|
|
1160
|
+
```bash
|
|
1161
|
+
npm install shadcn-rails
|
|
1162
|
+
# or
|
|
1163
|
+
yarn add shadcn-rails
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
Then in `app/javascript/controllers/index.js`:
|
|
1167
|
+
|
|
1168
|
+
```javascript
|
|
1169
|
+
import { Application } from "@hotwired/stimulus"
|
|
1170
|
+
import { registerShadcnControllers } from "shadcn-rails"
|
|
1171
|
+
|
|
1172
|
+
const application = Application.start()
|
|
1173
|
+
registerShadcnControllers(application)
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
### Webpack
|
|
1177
|
+
|
|
1178
|
+
Install the npm package:
|
|
1179
|
+
|
|
1180
|
+
```bash
|
|
1181
|
+
npm install shadcn-rails
|
|
1182
|
+
# or
|
|
1183
|
+
yarn add shadcn-rails
|
|
1184
|
+
```
|
|
1185
|
+
|
|
1186
|
+
In `app/javascript/controllers/index.js`:
|
|
1187
|
+
|
|
1188
|
+
```javascript
|
|
1189
|
+
import { Application } from "@hotwired/stimulus"
|
|
1190
|
+
import { registerShadcnControllers } from "shadcn-rails"
|
|
1191
|
+
|
|
1192
|
+
const application = Application.start()
|
|
1193
|
+
registerShadcnControllers(application)
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
### Vite (vite-ruby)
|
|
1197
|
+
|
|
1198
|
+
Install the npm package:
|
|
1199
|
+
|
|
1200
|
+
```bash
|
|
1201
|
+
npm install shadcn-rails
|
|
1202
|
+
# or
|
|
1203
|
+
yarn add shadcn-rails
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
In your entrypoint (e.g., `app/frontend/entrypoints/application.js`):
|
|
1207
|
+
|
|
1208
|
+
```javascript
|
|
1209
|
+
import { Application } from "@hotwired/stimulus"
|
|
1210
|
+
import { registerShadcnControllers } from "shadcn-rails"
|
|
1211
|
+
|
|
1212
|
+
const application = Application.start()
|
|
1213
|
+
registerShadcnControllers(application)
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
### Registering Individual Controllers
|
|
1217
|
+
|
|
1218
|
+
If you prefer to only load specific controllers (tree-shaking):
|
|
1219
|
+
|
|
1220
|
+
```javascript
|
|
1221
|
+
import { Application } from "@hotwired/stimulus"
|
|
1222
|
+
import DialogController from "shadcn-rails/controllers/dialog_controller"
|
|
1223
|
+
import TabsController from "shadcn-rails/controllers/tabs_controller"
|
|
1224
|
+
|
|
1225
|
+
const application = Application.start()
|
|
1226
|
+
application.register("shadcn--dialog", DialogController)
|
|
1227
|
+
application.register("shadcn--tabs", TabsController)
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
### Available Controllers
|
|
1231
|
+
|
|
1232
|
+
| Controller | Components |
|
|
1233
|
+
|------------|------------|
|
|
1234
|
+
| `shadcn--accordion` | Accordion |
|
|
1235
|
+
| `shadcn--alert-dialog` | AlertDialog |
|
|
1236
|
+
| `shadcn--avatar` | Avatar |
|
|
1237
|
+
| `shadcn--checkbox` | Checkbox |
|
|
1238
|
+
| `shadcn--collapsible` | Collapsible |
|
|
1239
|
+
| `shadcn--context-menu` | ContextMenu |
|
|
1240
|
+
| `shadcn--dialog` | Dialog |
|
|
1241
|
+
| `shadcn--drawer` | Drawer |
|
|
1242
|
+
| `shadcn--dropdown-menu` | DropdownMenu |
|
|
1243
|
+
| `shadcn--hover-card` | HoverCard |
|
|
1244
|
+
| `shadcn--input-otp` | InputOtp |
|
|
1245
|
+
| `shadcn--menubar` | Menubar |
|
|
1246
|
+
| `shadcn--navigation-menu` | NavigationMenu |
|
|
1247
|
+
| `shadcn--popover` | Popover |
|
|
1248
|
+
| `shadcn--radio-group` | RadioGroup |
|
|
1249
|
+
| `shadcn--resizable` | Resizable |
|
|
1250
|
+
| `shadcn--scroll-area` | ScrollArea |
|
|
1251
|
+
| `shadcn--select` | Select |
|
|
1252
|
+
| `shadcn--sheet` | Sheet |
|
|
1253
|
+
| `shadcn--slider` | Slider |
|
|
1254
|
+
| `shadcn--switch` | Switch |
|
|
1255
|
+
| `shadcn--tabs` | Tabs |
|
|
1256
|
+
| `shadcn--toast` | Toast |
|
|
1257
|
+
| `shadcn--toggle` | Toggle |
|
|
1258
|
+
| `shadcn--toggle-group` | ToggleGroup |
|
|
1259
|
+
| `shadcn--tooltip` | Tooltip |
|
|
1260
|
+
|
|
1261
|
+
### TypeScript Support
|
|
1262
|
+
|
|
1263
|
+
shadcn-rails includes comprehensive TypeScript type definitions (`.d.ts` files) for all 20 Stimulus controllers. Types are provided without requiring TypeScript compilation - your JavaScript remains the source of truth.
|
|
1264
|
+
|
|
1265
|
+
**Using Types in TypeScript Projects:**
|
|
1266
|
+
|
|
1267
|
+
```typescript
|
|
1268
|
+
import { Application } from "@hotwired/stimulus"
|
|
1269
|
+
import { registerShadcnControllers } from "shadcn-rails"
|
|
1270
|
+
|
|
1271
|
+
// Full IDE autocomplete and type checking
|
|
1272
|
+
const application = Application.start()
|
|
1273
|
+
registerShadcnControllers(application)
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
**Importing Individual Controllers with Types:**
|
|
1277
|
+
|
|
1278
|
+
```typescript
|
|
1279
|
+
import DialogController from "shadcn-rails/controllers/dialog_controller"
|
|
1280
|
+
import TabsController from "shadcn-rails/controllers/tabs_controller"
|
|
1281
|
+
|
|
1282
|
+
// Full type information available
|
|
1283
|
+
const dialog = new DialogController()
|
|
1284
|
+
dialog.open() // ✓ TypeScript knows this method exists
|
|
1285
|
+
dialog.openValue // ✓ Type: boolean
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
**Available Type Definitions:**
|
|
1289
|
+
|
|
1290
|
+
Each controller includes typed definitions for:
|
|
1291
|
+
- Static `targets` and `values` declarations
|
|
1292
|
+
- Target accessors (`*Target`, `*Targets`, `has*Target`)
|
|
1293
|
+
- Value accessors (`*Value`, `has*Value`)
|
|
1294
|
+
- All public methods
|
|
1295
|
+
- Custom properties and getters
|
|
1296
|
+
|
|
1297
|
+
**Example Type Definition (DialogController):**
|
|
1298
|
+
|
|
1299
|
+
```typescript
|
|
1300
|
+
import { Controller } from "@hotwired/stimulus"
|
|
1301
|
+
|
|
1302
|
+
export default class DialogController extends Controller {
|
|
1303
|
+
static targets: ["trigger", "template", "overlay", "content"]
|
|
1304
|
+
static values: {
|
|
1305
|
+
open: { type: "Boolean"; default: false }
|
|
1306
|
+
modal: { type: "Boolean"; default: true }
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// Target accessors
|
|
1310
|
+
readonly triggerTarget: HTMLElement
|
|
1311
|
+
readonly hasTemplateTarget: boolean
|
|
1312
|
+
|
|
1313
|
+
// Value accessors
|
|
1314
|
+
openValue: boolean
|
|
1315
|
+
modalValue: boolean
|
|
1316
|
+
|
|
1317
|
+
// Methods
|
|
1318
|
+
open(): void
|
|
1319
|
+
close(): void
|
|
1320
|
+
toggle(): void
|
|
1321
|
+
}
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
**For Importmap Users:**
|
|
1325
|
+
|
|
1326
|
+
If using TypeScript with importmaps, add a type declaration file at `types/shadcn-rails.d.ts`:
|
|
1327
|
+
|
|
1328
|
+
```typescript
|
|
1329
|
+
declare module "shadcn-rails" {
|
|
1330
|
+
import { Application } from "@hotwired/stimulus"
|
|
1331
|
+
export function registerShadcnControllers(application: Application): void
|
|
1332
|
+
export const controllers: Record<string, typeof import("@hotwired/stimulus").Controller>
|
|
1333
|
+
}
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
## Helper Methods
|
|
1337
|
+
|
|
1338
|
+
shadcn-rails provides helper methods for your views:
|
|
1339
|
+
|
|
1340
|
+
```erb
|
|
1341
|
+
<%# Class name merging (like cn() in shadcn/ui) %>
|
|
1342
|
+
<div class="<%= Shadcn::Rails.cn("base-class", conditional && "conditional-class", @class_name) %>">
|
|
1343
|
+
```
|
|
1344
|
+
|
|
1345
|
+
## Testing
|
|
1346
|
+
|
|
1347
|
+
Run the full test suite:
|
|
1348
|
+
|
|
1349
|
+
```bash
|
|
1350
|
+
bundle exec rake test
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
Run component tests only:
|
|
1354
|
+
|
|
1355
|
+
```bash
|
|
1356
|
+
bundle exec rake test_components
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
## Development
|
|
1360
|
+
|
|
1361
|
+
After checking out the repo:
|
|
1362
|
+
|
|
1363
|
+
```bash
|
|
1364
|
+
bundle install
|
|
1365
|
+
cd test/dummy
|
|
1366
|
+
rails server
|
|
1367
|
+
```
|
|
1368
|
+
|
|
1369
|
+
Visit `http://localhost:3000` to see the demo app with:
|
|
1370
|
+
- `/docs` - Full documentation with examples
|
|
1371
|
+
- `/showcase` - Full component showcase
|
|
1372
|
+
- `/themes` - Theme preview and comparison
|
|
1373
|
+
- `/lookbook` - Component previews with Lookbook
|
|
1374
|
+
|
|
1375
|
+
### Deploying the Documentation Site
|
|
1376
|
+
|
|
1377
|
+
The documentation site in `test/dummy/` can be deployed as a standalone Rails application. Recommended platforms:
|
|
1378
|
+
|
|
1379
|
+
**Render (Free tier available)**
|
|
1380
|
+
```bash
|
|
1381
|
+
# In test/dummy/ directory
|
|
1382
|
+
render.yaml # Already configured for deployment
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
**Railway**
|
|
1386
|
+
```bash
|
|
1387
|
+
cd test/dummy
|
|
1388
|
+
railway init
|
|
1389
|
+
railway up
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
**Fly.io**
|
|
1393
|
+
```bash
|
|
1394
|
+
cd test/dummy
|
|
1395
|
+
fly launch
|
|
1396
|
+
fly deploy
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
**Heroku**
|
|
1400
|
+
```bash
|
|
1401
|
+
cd test/dummy
|
|
1402
|
+
heroku create your-shadcn-rails-docs
|
|
1403
|
+
git subtree push --prefix test/dummy heroku main
|
|
1404
|
+
```
|
|
1405
|
+
|
|
1406
|
+
## Security Considerations
|
|
1407
|
+
|
|
1408
|
+
shadcn-rails follows Rails security best practices. Here are important security guidelines:
|
|
1409
|
+
|
|
1410
|
+
### CSRF Protection
|
|
1411
|
+
|
|
1412
|
+
Always use Rails form helpers to automatically include CSRF tokens:
|
|
1413
|
+
|
|
1414
|
+
```erb
|
|
1415
|
+
<%= form_with url: "/submit" do |f| %>
|
|
1416
|
+
<%= render Shadcn::InputComponent.new(name: "email") %>
|
|
1417
|
+
<%= render Shadcn::ButtonComponent.new(type: "submit") { "Submit" } %>
|
|
1418
|
+
<% end %>
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
### XSS Prevention
|
|
1422
|
+
|
|
1423
|
+
ViewComponent auto-escapes all content by default. Never call `html_safe` on user-provided content:
|
|
1424
|
+
|
|
1425
|
+
```erb
|
|
1426
|
+
<%# SAFE - auto-escaped %>
|
|
1427
|
+
<%= render Shadcn::BadgeComponent.new { user.name } %>
|
|
1428
|
+
|
|
1429
|
+
<%# DANGEROUS - never do this with user input! %>
|
|
1430
|
+
<%= render Shadcn::BadgeComponent.new { user.name.html_safe } %>
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
### Input Validation
|
|
1434
|
+
|
|
1435
|
+
Form components (Input, Textarea, Select) pass through values without validation. Always implement:
|
|
1436
|
+
|
|
1437
|
+
- Server-side input validation
|
|
1438
|
+
- Strong parameters in controllers
|
|
1439
|
+
- Model validations
|
|
1440
|
+
|
|
1441
|
+
```ruby
|
|
1442
|
+
# In your controller
|
|
1443
|
+
def user_params
|
|
1444
|
+
params.require(:user).permit(:name, :email)
|
|
1445
|
+
end
|
|
1446
|
+
|
|
1447
|
+
# In your model
|
|
1448
|
+
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
### Content Security Policy
|
|
1452
|
+
|
|
1453
|
+
If using CSP headers, ensure your policy allows inline styles for theming:
|
|
1454
|
+
|
|
1455
|
+
```ruby
|
|
1456
|
+
# config/initializers/content_security_policy.rb
|
|
1457
|
+
Rails.application.configure do
|
|
1458
|
+
config.content_security_policy do |policy|
|
|
1459
|
+
policy.style_src :self, :unsafe_inline # Required for CSS variables
|
|
1460
|
+
end
|
|
1461
|
+
end
|
|
1462
|
+
```
|
|
1463
|
+
|
|
1464
|
+
## Contributing
|
|
1465
|
+
|
|
1466
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/shadcn-rails.
|
|
1467
|
+
|
|
1468
|
+
1. Fork the repository
|
|
1469
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
1470
|
+
3. Commit your changes (`git commit -am 'Add amazing feature'`)
|
|
1471
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
1472
|
+
5. Open a Pull Request
|
|
1473
|
+
|
|
1474
|
+
## License
|
|
1475
|
+
|
|
1476
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
1477
|
+
|
|
1478
|
+
## Credits
|
|
1479
|
+
|
|
1480
|
+
- [shadcn/ui](https://ui.shadcn.com) - The original React component library
|
|
1481
|
+
- [ViewComponent](https://viewcomponent.org) - Ruby component framework
|
|
1482
|
+
- [Stimulus](https://stimulus.hotwired.dev) - JavaScript framework
|
|
1483
|
+
- [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS framework
|