shadcn-rails 0.1.0 → 0.2.1

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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -2
  3. data/README.md +102 -1398
  4. data/__mocks__/@floating-ui/dom.js +67 -0
  5. data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
  6. data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +34 -8
  7. data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
  8. data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +64 -135
  9. data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +56 -186
  10. data/app/assets/javascripts/shadcn/controllers/hover_card_controller.js +29 -55
  11. data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
  12. data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
  13. data/app/assets/javascripts/shadcn/controllers/popover_controller.js +35 -60
  14. data/app/assets/javascripts/shadcn/controllers/select_controller.js +37 -17
  15. data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
  16. data/app/assets/javascripts/shadcn/controllers/tooltip_controller.js +28 -59
  17. data/app/assets/javascripts/shadcn/index.js +9 -1
  18. data/app/assets/javascripts/shadcn/utils/floating.js +179 -0
  19. data/app/assets/stylesheets/shadcn/base.css +32 -0
  20. data/app/assets/stylesheets/shadcn/components.css +12 -0
  21. data/app/components/shadcn/accordion_component.html.erb +8 -0
  22. data/app/components/shadcn/accordion_component.rb +6 -15
  23. data/app/components/shadcn/alert_component.html.erb +6 -0
  24. data/app/components/shadcn/alert_component.rb +0 -18
  25. data/app/components/shadcn/alert_dialog_component.html.erb +12 -0
  26. data/app/components/shadcn/alert_dialog_component.rb +7 -27
  27. data/app/components/shadcn/aspect_ratio_component.html.erb +7 -0
  28. data/app/components/shadcn/aspect_ratio_component.rb +4 -19
  29. data/app/components/shadcn/avatar_component.html.erb +20 -0
  30. data/app/components/shadcn/avatar_component.rb +8 -36
  31. data/app/components/shadcn/badge_component.html.erb +1 -0
  32. data/app/components/shadcn/badge_component.rb +0 -11
  33. data/app/components/shadcn/base_component.rb +15 -2
  34. data/app/components/shadcn/breadcrumb_component.html.erb +5 -0
  35. data/app/components/shadcn/breadcrumb_component.rb +6 -16
  36. data/app/components/shadcn/button_component.html.erb +18 -0
  37. data/app/components/shadcn/button_component.rb +1 -41
  38. data/app/components/shadcn/card_component.html.erb +8 -0
  39. data/app/components/shadcn/card_component.rb +2 -6
  40. data/app/components/shadcn/checkbox_component.html.erb +32 -0
  41. data/app/components/shadcn/checkbox_component.rb +4 -43
  42. data/app/components/shadcn/collapsible_component.html.erb +8 -0
  43. data/app/components/shadcn/collapsible_component.rb +6 -15
  44. data/app/components/shadcn/command_list_component.rb +29 -14
  45. data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
  46. data/app/components/shadcn/context_menu_component.html.erb +11 -0
  47. data/app/components/shadcn/context_menu_component.rb +6 -26
  48. data/app/components/shadcn/context_menu_content_component.rb +37 -14
  49. data/app/components/shadcn/context_menu_item_component.rb +3 -2
  50. data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
  51. data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
  52. data/app/components/shadcn/dialog_component.html.erb +14 -0
  53. data/app/components/shadcn/dialog_component.rb +8 -29
  54. data/app/components/shadcn/drawer_component.html.erb +12 -0
  55. data/app/components/shadcn/drawer_component.rb +7 -27
  56. data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
  57. data/app/components/shadcn/dropdown_menu_component.html.erb +14 -0
  58. data/app/components/shadcn/dropdown_menu_component.rb +9 -29
  59. data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
  60. data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
  61. data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
  62. data/app/components/shadcn/field_component.rb +7 -8
  63. data/app/components/shadcn/hover_card_component.html.erb +12 -0
  64. data/app/components/shadcn/hover_card_component.rb +7 -26
  65. data/app/components/shadcn/input_component.html.erb +18 -0
  66. data/app/components/shadcn/input_component.rb +2 -27
  67. data/app/components/shadcn/input_otp_component.rb +3 -3
  68. data/app/components/shadcn/kbd_component.html.erb +1 -0
  69. data/app/components/shadcn/kbd_component.rb +3 -10
  70. data/app/components/shadcn/label_component.html.erb +3 -0
  71. data/app/components/shadcn/label_component.rb +2 -18
  72. data/app/components/shadcn/menubar_component.html.erb +6 -0
  73. data/app/components/shadcn/menubar_component.rb +4 -15
  74. data/app/components/shadcn/menubar_content_component.rb +45 -20
  75. data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
  76. data/app/components/shadcn/native_select_component.html.erb +22 -0
  77. data/app/components/shadcn/native_select_component.rb +9 -39
  78. data/app/components/shadcn/navigation_menu_component.html.erb +6 -0
  79. data/app/components/shadcn/navigation_menu_component.rb +4 -15
  80. data/app/components/shadcn/pagination_component.html.erb +5 -0
  81. data/app/components/shadcn/pagination_component.rb +11 -15
  82. data/app/components/shadcn/popover_component.html.erb +15 -0
  83. data/app/components/shadcn/popover_component.rb +10 -30
  84. data/app/components/shadcn/progress_component.html.erb +13 -0
  85. data/app/components/shadcn/progress_component.rb +6 -26
  86. data/app/components/shadcn/radio_group_component.html.erb +8 -0
  87. data/app/components/shadcn/radio_group_component.rb +12 -26
  88. data/app/components/shadcn/radio_group_item_component.rb +32 -6
  89. data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
  90. data/app/components/shadcn/scroll_area_component.html.erb +7 -0
  91. data/app/components/shadcn/scroll_area_component.rb +4 -16
  92. data/app/components/shadcn/select_component.html.erb +46 -0
  93. data/app/components/shadcn/select_component.rb +29 -86
  94. data/app/components/shadcn/separator_component.html.erb +5 -0
  95. data/app/components/shadcn/separator_component.rb +6 -14
  96. data/app/components/shadcn/sheet_component.html.erb +12 -0
  97. data/app/components/shadcn/sheet_component.rb +7 -27
  98. data/app/components/shadcn/sidebar_component.rb +2 -2
  99. data/app/components/shadcn/skeleton_component.html.erb +1 -0
  100. data/app/components/shadcn/skeleton_component.rb +4 -2
  101. data/app/components/shadcn/slider_component.html.erb +12 -0
  102. data/app/components/shadcn/slider_component.rb +2 -21
  103. data/app/components/shadcn/spinner_component.html.erb +18 -0
  104. data/app/components/shadcn/spinner_component.rb +2 -30
  105. data/app/components/shadcn/switch_component.html.erb +72 -0
  106. data/app/components/shadcn/switch_component.rb +4 -82
  107. data/app/components/shadcn/table_component.html.erb +9 -0
  108. data/app/components/shadcn/table_component.rb +2 -10
  109. data/app/components/shadcn/tabs_component.html.erb +8 -0
  110. data/app/components/shadcn/tabs_component.rb +4 -17
  111. data/app/components/shadcn/textarea_component.html.erb +13 -0
  112. data/app/components/shadcn/textarea_component.rb +6 -22
  113. data/app/components/shadcn/toast_component.html.erb +36 -0
  114. data/app/components/shadcn/toast_component.rb +6 -54
  115. data/app/components/shadcn/toggle_component.html.erb +12 -0
  116. data/app/components/shadcn/toggle_component.rb +6 -21
  117. data/app/components/shadcn/toggle_group_component.html.erb +14 -0
  118. data/app/components/shadcn/toggle_group_component.rb +6 -29
  119. data/app/components/shadcn/tooltip_component.html.erb +20 -0
  120. data/app/components/shadcn/tooltip_component.rb +13 -38
  121. data/lib/generators/shadcn/add/USAGE +24 -0
  122. data/lib/generators/shadcn/add/add_generator.rb +279 -0
  123. data/lib/generators/shadcn/install/USAGE +22 -0
  124. data/lib/generators/shadcn/install/install_generator.rb +8 -3
  125. data/lib/generators/shadcn/install/templates/initializer.rb.tt +7 -27
  126. data/lib/generators/shadcn/install/templates/shadcn.yml.tt +15 -31
  127. data/lib/shadcn/rails/version.rb +1 -1
  128. metadata +54 -42
  129. data/.dockerignore +0 -40
  130. data/CLAUDE.md +0 -463
  131. data/PROGRESS.md +0 -485
  132. data/Rakefile +0 -29
  133. data/__tests__/controllers/__snapshots__/calendar_controller.test.js.snap +0 -13
  134. data/__tests__/controllers/__snapshots__/popover_controller.test.js.snap +0 -46
  135. data/__tests__/controllers/__snapshots__/sheet_controller.test.js.snap +0 -111
  136. data/__tests__/controllers/__snapshots__/tabs_controller.test.js.snap +0 -27
  137. data/__tests__/controllers/accordion_controller.test.js +0 -904
  138. data/__tests__/controllers/calendar_controller.test.js +0 -1370
  139. data/__tests__/controllers/carousel_controller.test.js +0 -912
  140. data/__tests__/controllers/checkbox_controller.test.js +0 -454
  141. data/__tests__/controllers/collapsible_controller.test.js +0 -407
  142. data/__tests__/controllers/combobox_controller.test.js +0 -966
  143. data/__tests__/controllers/context_menu_controller.test.js +0 -627
  144. data/__tests__/controllers/date_picker_controller.test.js +0 -636
  145. data/__tests__/controllers/dialog_controller.test.js +0 -878
  146. data/__tests__/controllers/drawer_controller.test.js +0 -995
  147. data/__tests__/controllers/menubar_controller.test.js +0 -736
  148. data/__tests__/controllers/navigation_menu_controller.test.js +0 -598
  149. data/__tests__/controllers/popover_controller.test.js +0 -1007
  150. data/__tests__/controllers/radio_group_controller.test.js +0 -640
  151. data/__tests__/controllers/resizable_controller.test.js +0 -680
  152. data/__tests__/controllers/select_controller.test.js +0 -674
  153. data/__tests__/controllers/sheet_controller.test.js +0 -986
  154. data/__tests__/controllers/slider_controller.test.js +0 -1036
  155. data/__tests__/controllers/switch_controller.test.js +0 -424
  156. data/__tests__/controllers/tabs_controller.test.js +0 -907
  157. data/__tests__/controllers/toggle_group_controller.test.js +0 -839
  158. data/__tests__/controllers/tooltip_controller.test.js +0 -808
  159. data/__tests__/helpers/stimulus-test-helper.js +0 -203
  160. data/babel.config.cjs +0 -5
  161. data/bin/console +0 -11
  162. data/bin/setup +0 -8
  163. data/jest.config.js +0 -19
  164. data/jest.setup.js +0 -8
  165. data/lib/generators/shadcn/component/component_generator.rb +0 -188
  166. data/lib/generators/shadcn/theme/theme_generator.rb +0 -128
  167. data/package-lock.json +0 -7415
  168. data/package.json +0 -68
  169. data/rollup.config.js +0 -29
data/README.md CHANGED
@@ -2,1482 +2,186 @@
2
2
 
3
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
4
 
5
- [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-red.svg)](https://www.ruby-lang.org/)
6
- [![Rails](https://img.shields.io/badge/rails-%3E%3D%207.0-red.svg)](https://rubyonrails.org/)
7
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![CI](https://github.com/iheanyi/shadcn-rails/actions/workflows/ci.yml/badge.svg)](https://github.com/iheanyi/shadcn-rails/actions/workflows/ci.yml)
6
+ [![Gem Version](https://badge.fury.io/rb/shadcn-rails.svg)](https://rubygems.org/gems/shadcn-rails)
7
+ [![npm version](https://badge.fury.io/js/shadcn-rails-stimulus.svg)](https://www.npmjs.com/package/shadcn-rails-stimulus)
8
8
 
9
9
  ## Features
10
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
11
+ - **47 Components** - Buttons, forms, dialogs, menus, and more
12
+ - **Accessible** - Built with WAI-ARIA patterns
13
+ - **Dark Mode** - Built-in light/dark theme support
14
+ - **Customizable** - CSS variables for easy theming
15
+ - **Rails-first** - ViewComponents + Stimulus + Tailwind CSS
19
16
 
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
- ```
17
+ ## Installation
888
18
 
889
- **4. Custom URL builder:**
19
+ ### Ruby Gem
890
20
 
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
- ) %>
21
+ ```bash
22
+ bundle add shadcn-rails
23
+ rails generate shadcn:install
897
24
  ```
898
25
 
899
- **5. Full slot-based control:**
26
+ ### Stimulus Controllers (npm)
900
27
 
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 %>
28
+ ```bash
29
+ npm install shadcn-rails-stimulus
30
+ # or
31
+ yarn add shadcn-rails-stimulus
913
32
  ```
914
33
 
915
- **6. Using the `shadcn_paginate` helper:**
916
-
917
- ```erb
918
- <%# Simple one-liner that auto-detects your pagination gem %>
919
- <%= shadcn_paginate @posts %>
34
+ Then register the controllers:
920
35
 
921
- <%# With Pagy %>
922
- <%= shadcn_paginate @pagy %>
36
+ ```javascript
37
+ import { Application } from "@hotwired/stimulus"
38
+ import { registerShadcnControllers } from "shadcn-rails-stimulus"
923
39
 
924
- <%# Custom URL builder and window size %>
925
- <%= shadcn_paginate @posts,
926
- url_builder: ->(page) { posts_path(page: page) },
927
- window: 3 %>
40
+ const application = Application.start()
41
+ registerShadcnControllers(application)
928
42
  ```
929
43
 
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 |
44
+ ## Adding Components
936
45
 
937
- **Supported Pagination Gems:**
46
+ Copy components into your app for customization:
938
47
 
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)`) |
48
+ ```bash
49
+ # List all available components
50
+ rails generate shadcn:add --list
944
51
 
945
- #### Collapsible
52
+ # Add specific components
53
+ rails generate shadcn:add button dialog tabs
946
54
 
947
- An interactive component which expands/collapses a panel.
55
+ # Add all components
56
+ rails generate shadcn:add --all
948
57
 
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 %>
58
+ # Add without Stimulus controllers
59
+ rails generate shadcn:add dialog --exclude-controllers
963
60
  ```
964
61
 
965
- ### Layout
62
+ Components are copied to `app/components/shadcn/` and controllers to `app/javascript/controllers/shadcn/`. Local files take precedence over the gem's built-in components.
966
63
 
967
- #### Separator
968
-
969
- Visually or semantically separates content.
64
+ ## Quick Start
970
65
 
971
66
  ```erb
972
- <%# Horizontal separator %>
973
- <%= render Shadcn::SeparatorComponent.new %>
67
+ <%# Button %>
68
+ <%= render Shadcn::ButtonComponent.new(variant: :default) { "Click me" } %>
974
69
 
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.
70
+ <%# Card %>
71
+ <%= render Shadcn::CardComponent.new do |card| %>
72
+ <% card.with_header do |header| %>
73
+ <% header.with_title { "Welcome" } %>
74
+ <% end %>
75
+ <% card.with_content_slot do %>
76
+ <p>Your content here</p>
77
+ <% end %>
78
+ <% end %>
986
79
 
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>
80
+ <%# Dialog %>
81
+ <%= render Shadcn::DialogComponent.new do |dialog| %>
82
+ <% dialog.with_trigger do %>
83
+ <%= render Shadcn::ButtonComponent.new { "Open" } %>
84
+ <% end %>
85
+ <% dialog.with_body do |body| %>
86
+ <% body.with_header do |header| %>
87
+ <% header.with_title { "Edit Profile" } %>
992
88
  <% end %>
993
- </div>
89
+ <p>Dialog content here</p>
90
+ <% end %>
994
91
  <% end %>
995
92
  ```
996
93
 
997
- ## Theming
998
-
999
- ### Available Themes
94
+ ## Components
1000
95
 
1001
- shadcn-rails includes 5 built-in color themes:
96
+ | Category | Components |
97
+ |----------|------------|
98
+ | **Actions** | Button, Toggle, Toggle Group |
99
+ | **Forms** | Input, Textarea, Label, Checkbox, Switch, Radio Group, Select, Slider |
100
+ | **Data Display** | Badge, Avatar, Card, Table, Progress, Skeleton, Aspect Ratio |
101
+ | **Feedback** | Alert, Tooltip, Toast |
102
+ | **Overlays** | Dialog, Alert Dialog, Sheet, Drawer, Popover, Hover Card, Dropdown Menu, Context Menu |
103
+ | **Navigation** | Tabs, Accordion, Breadcrumb, Pagination, Collapsible, Navigation Menu, Menubar |
104
+ | **Layout** | Separator, Scroll Area, Resizable |
1002
105
 
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 |
106
+ ## Theming
1010
107
 
1011
- ### Switching Themes
108
+ Configure colors in your initializer:
1012
109
 
1013
110
  ```ruby
1014
111
  # config/initializers/shadcn.rb
1015
112
  Shadcn::Rails.configure do |config|
1016
113
  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
114
+ config.dark_mode = :class # :class, :media, :both
1131
115
  end
1132
116
  ```
1133
117
 
1134
118
  ## Stimulus Controllers
1135
119
 
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
120
+ All interactive components have corresponding Stimulus controllers:
1231
121
 
1232
122
  | Controller | Components |
1233
123
  |------------|------------|
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
124
  | `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 |
125
+ | `shadcn--sheet` | Sheet |
126
+ | `shadcn--tabs` | Tabs |
127
+ | `shadcn--accordion` | Accordion |
1247
128
  | `shadcn--popover` | Popover |
1248
- | `shadcn--radio-group` | RadioGroup |
1249
- | `shadcn--resizable` | Resizable |
1250
- | `shadcn--scroll-area` | ScrollArea |
129
+ | `shadcn--dropdown-menu` | DropdownMenu |
1251
130
  | `shadcn--select` | Select |
1252
- | `shadcn--sheet` | Sheet |
1253
- | `shadcn--slider` | Slider |
1254
131
  | `shadcn--switch` | Switch |
1255
- | `shadcn--tabs` | Tabs |
1256
- | `shadcn--toast` | Toast |
1257
- | `shadcn--toggle` | Toggle |
1258
- | `shadcn--toggle-group` | ToggleGroup |
132
+ | `shadcn--slider` | Slider |
1259
133
  | `shadcn--tooltip` | Tooltip |
134
+ | `shadcn--toast` | Toast |
1260
135
 
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
136
+ Register individual controllers for tree-shaking:
1316
137
 
1317
- // Methods
1318
- open(): void
1319
- close(): void
1320
- toggle(): void
1321
- }
138
+ ```javascript
139
+ import DialogController from "shadcn-rails-stimulus/controllers/dialog_controller"
140
+ application.register("shadcn--dialog", DialogController)
1322
141
  ```
1323
142
 
1324
- **For Importmap Users:**
143
+ ## TypeScript Support
1325
144
 
1326
- If using TypeScript with importmaps, add a type declaration file at `types/shadcn-rails.d.ts`:
145
+ Full TypeScript definitions included for all controllers:
1327
146
 
1328
147
  ```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
148
+ import { registerShadcnControllers } from "shadcn-rails-stimulus"
149
+ import DialogController from "shadcn-rails-stimulus/controllers/dialog_controller"
1346
150
 
1347
- Run the full test suite:
1348
-
1349
- ```bash
1350
- bundle exec rake test
151
+ // Full autocomplete and type checking
152
+ const dialog = new DialogController()
153
+ dialog.open() // Methods are typed
154
+ dialog.openValue // Values are typed (boolean)
1351
155
  ```
1352
156
 
1353
- Run component tests only:
157
+ ## Requirements
1354
158
 
1355
- ```bash
1356
- bundle exec rake test_components
1357
- ```
159
+ - Ruby >= 3.1
160
+ - Rails >= 7.0
161
+ - Tailwind CSS >= 3.0
162
+ - Stimulus >= 3.0
163
+ - ViewComponent >= 3.0
1358
164
 
1359
165
  ## Development
1360
166
 
1361
- After checking out the repo:
1362
-
1363
167
  ```bash
1364
168
  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 }
169
+ cd test/dummy && rails server
1449
170
  ```
1450
171
 
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
- ```
172
+ Visit http://localhost:3000/docs for the component documentation.
1463
173
 
1464
174
  ## Contributing
1465
175
 
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
176
+ Bug reports and pull requests are welcome at https://github.com/iheanyi/shadcn-rails.
1473
177
 
1474
178
  ## License
1475
179
 
1476
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
180
+ MIT License - see [LICENSE](LICENSE) for details.
1477
181
 
1478
182
  ## Credits
1479
183
 
1480
- - [shadcn/ui](https://ui.shadcn.com) - The original React component library
184
+ - [shadcn/ui](https://ui.shadcn.com) - Original React component library
1481
185
  - [ViewComponent](https://viewcomponent.org) - Ruby component framework
1482
186
  - [Stimulus](https://stimulus.hotwired.dev) - JavaScript framework
1483
- - [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS framework
187
+ - [Tailwind CSS](https://tailwindcss.com) - Utility-first CSS