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
@@ -34,38 +34,18 @@ module Shadcn
34
34
  @open = open
35
35
  end
36
36
 
37
- def call
38
- content_tag(:div, dialog_content, dialog_attributes)
39
- end
40
-
41
37
  private
42
38
 
43
- def dialog_content
44
- safe_join([
45
- trigger_wrapper,
46
- body
47
- ].compact)
48
- end
49
-
50
- def trigger_wrapper
51
- return unless trigger
52
-
53
- content_tag(:div, trigger, {
54
- "data-shadcn--dialog-target": "trigger",
55
- "data-action": "click->shadcn--dialog#open"
56
- })
39
+ def alert_dialog_classes
40
+ class_name
57
41
  end
58
42
 
59
- def dialog_attributes
60
- attrs = {
61
- class: class_name,
62
- "data-controller": "shadcn--dialog",
63
- "data-shadcn--dialog-open-value": @open.to_s,
64
- "data-shadcn--dialog-modal-value": "true"
43
+ def alert_dialog_data_attrs
44
+ {
45
+ controller: "shadcn--dialog",
46
+ "shadcn--dialog-open-value": @open.to_s,
47
+ "shadcn--dialog-modal-value": "true"
65
48
  }
66
- attrs.merge!(html_options)
67
- attrs.merge!(build_data)
68
- attrs.compact
69
49
  end
70
50
  end
71
51
  end
@@ -0,0 +1,7 @@
1
+ <div class="<%= wrapper_classes %>"
2
+ style="<%= wrapper_style %>"
3
+ <%= tag_attributes %>>
4
+ <div class="absolute inset-0" style="position: absolute; top: 0; right: 0; bottom: 0; left: 0;">
5
+ <%= content %>
6
+ </div>
7
+ </div>
@@ -21,29 +21,14 @@ module Shadcn
21
21
  @ratio = ratio.to_f
22
22
  end
23
23
 
24
- def call
25
- content_tag(:div, wrapper_attributes) do
26
- content_tag(:div, content, inner_attributes)
27
- end
28
- end
29
-
30
24
  private
31
25
 
32
- def wrapper_attributes
33
- attrs = {
34
- class: cn("relative w-full", class_name),
35
- style: "padding-bottom: #{(1.0 / @ratio) * 100}%;"
36
- }
37
- attrs.merge!(html_options)
38
- attrs.merge!(build_data)
39
- attrs.compact
26
+ def wrapper_classes
27
+ cn("relative w-full", class_name)
40
28
  end
41
29
 
42
- def inner_attributes
43
- {
44
- class: "absolute inset-0",
45
- style: "position: absolute; top: 0; right: 0; bottom: 0; left: 0;"
46
- }
30
+ def wrapper_style
31
+ "padding-bottom: #{(1.0 / @ratio) * 100}%;"
47
32
  end
48
33
  end
49
34
  end
@@ -0,0 +1,20 @@
1
+ <span class="<%= avatar_classes %>" <%= tag_attributes %>>
2
+ <% if has_image? %>
3
+ <span class="contents" data-controller="shadcn--avatar">
4
+ <img src="<%= @src %>"
5
+ alt="<%= @alt %>"
6
+ class="<%= IMAGE_CLASSES %>"
7
+ data-shadcn--avatar-target="image"
8
+ data-action="error->shadcn--avatar#handleError" />
9
+ <span class="<%= FALLBACK_CLASSES %> hidden" data-shadcn--avatar-target="fallback">
10
+ <%= fallback_text %>
11
+ </span>
12
+ </span>
13
+ <% elsif has_fallback_slot? %>
14
+ <%= fallback %>
15
+ <% else %>
16
+ <span class="<%= FALLBACK_CLASSES %>">
17
+ <%= fallback_text %>
18
+ </span>
19
+ <% end %>
20
+ </span>
@@ -46,50 +46,22 @@ module Shadcn
46
46
  @size = size.to_sym
47
47
  end
48
48
 
49
- def call
50
- content_tag(:span, avatar_content, avatar_attributes)
51
- end
52
-
53
49
  private
54
50
 
55
- def avatar_content
56
- if @src.present?
57
- image_with_fallback
58
- elsif fallback?
59
- fallback
60
- else
61
- fallback_element
62
- end
63
- end
64
-
65
- def image_with_fallback
66
- # Use Stimulus controller to handle image loading errors
67
- content_tag(:span, class: "contents", data: stimulus_data(controller: "shadcn--avatar")) do
68
- safe_join([
69
- tag(:img,
70
- src: @src,
71
- alt: @alt,
72
- class: IMAGE_CLASSES,
73
- data: { "shadcn--avatar-target": "image", action: "error->shadcn--avatar#handleError" }
74
- ),
75
- content_tag(:span, @fallback, class: "#{FALLBACK_CLASSES} hidden", data: { "shadcn--avatar-target": "fallback" })
76
- ])
77
- end
51
+ def avatar_classes
52
+ cn(BASE_CLASSES, SIZES[@size], class_name)
78
53
  end
79
54
 
80
- def fallback_element
81
- content_tag(:span, @fallback, class: FALLBACK_CLASSES)
55
+ def fallback_text
56
+ @fallback
82
57
  end
83
58
 
84
- def avatar_classes
85
- cn(BASE_CLASSES, SIZES[@size], class_name)
59
+ def has_image?
60
+ @src.present?
86
61
  end
87
62
 
88
- def avatar_attributes
89
- attrs = { class: avatar_classes }
90
- attrs.merge!(html_options)
91
- attrs.merge!(build_data)
92
- attrs.compact
63
+ def has_fallback_slot?
64
+ fallback?
93
65
  end
94
66
 
95
67
  def generate_fallback(alt)
@@ -0,0 +1 @@
1
+ <span class="<%= badge_classes %>" <%= tag_attributes %>><%= content %></span>
@@ -29,21 +29,10 @@ module Shadcn
29
29
  @variant = variant.to_sym
30
30
  end
31
31
 
32
- def call
33
- content_tag(:span, content, badge_attributes)
34
- end
35
-
36
32
  private
37
33
 
38
34
  def badge_classes
39
35
  cn(BASE_CLASSES, VARIANTS[@variant], class_name)
40
36
  end
41
-
42
- def badge_attributes
43
- attrs = { class: badge_classes }
44
- attrs.merge!(html_options)
45
- attrs.merge!(build_data)
46
- attrs.compact
47
- end
48
37
  end
49
38
  end
@@ -10,11 +10,15 @@ module Shadcn
10
10
  # Common attributes shared by all components
11
11
  attr_reader :class_name, :data, :html_options
12
12
 
13
- # @param class_name [String, nil] Additional CSS classes
13
+ # @param class_name [String, nil] Additional CSS classes (preferred)
14
+ # @param class [String, nil] Alias for class_name (for Rails-like API)
14
15
  # @param data [Hash] Data attributes (will be prefixed with data-)
15
16
  # @param html_options [Hash] Additional HTML attributes
16
17
  def initialize(class_name: nil, data: {}, **html_options, &block)
17
- @class_name = class_name
18
+ # Support both class: and class_name: for better Rails compatibility
19
+ # class_name takes precedence if both are provided
20
+ html_class = html_options.delete(:class)
21
+ @class_name = class_name || html_class
18
22
  @data = data
19
23
  @html_options = html_options
20
24
  @constructor_block = block
@@ -96,5 +100,14 @@ module Shadcn
96
100
 
97
101
  classes.split.map { |c| "#{config.tailwind_prefix}#{c}" }.join(" ")
98
102
  end
103
+
104
+ # Build HTML attributes string for use in templates
105
+ # Combines html_options and data attributes
106
+ # Uses html_escape_once to avoid double-escaping already-escaped content
107
+ # @return [String] HTML-safe attribute string
108
+ def tag_attributes
109
+ attrs = html_options.merge(build_data)
110
+ attrs.map { |k, v| "#{k}=\"#{ERB::Util.html_escape_once(v)}\"" if v }.compact.join(" ").html_safe
111
+ end
99
112
  end
100
113
  end
@@ -0,0 +1,5 @@
1
+ <nav aria-label="Breadcrumb" class="<%= breadcrumb_classes %>" <%= tag_attributes %>>
2
+ <ol class="flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5">
3
+ <%= breadcrumb_list_content %>
4
+ </ol>
5
+ </nav>
@@ -21,24 +21,14 @@ module Shadcn
21
21
  )
22
22
  end
23
23
 
24
- def call
25
- content_tag(:nav, breadcrumb_attributes) do
26
- content_tag(:ol, class: "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5") do
27
- safe_join(items_with_separators)
28
- end
29
- end
30
- end
31
-
32
24
  private
33
25
 
34
- def breadcrumb_attributes
35
- attrs = {
36
- "aria-label": "Breadcrumb",
37
- class: merge_classes("")
38
- }
39
- attrs.merge!(html_options)
40
- attrs.merge!(build_data)
41
- attrs.compact
26
+ def breadcrumb_classes
27
+ merge_classes("")
28
+ end
29
+
30
+ def breadcrumb_list_content
31
+ safe_join(items_with_separators)
42
32
  end
43
33
 
44
34
  def items_with_separators
@@ -0,0 +1,18 @@
1
+ <% if @href %>
2
+ <a href="<%= @href %>"
3
+ class="<%= button_classes %>"
4
+ role="button"
5
+ <%= "aria-disabled=true tabindex=-1" if @disabled %>
6
+ <%= tag_attributes %>>
7
+ <%= button_content %>
8
+ </a>
9
+ <% else %>
10
+ <button type="<%= @type %>"
11
+ class="<%= button_classes %>"
12
+ <%= "disabled" if @disabled || @loading %>
13
+ <%= "aria-disabled=true" if @disabled || @loading %>
14
+ <%= "aria-busy=true" if @loading %>
15
+ <%= tag_attributes %>>
16
+ <%= button_content %>
17
+ </button>
18
+ <% end %>
@@ -75,24 +75,8 @@ module Shadcn
75
75
  @loading = loading
76
76
  end
77
77
 
78
- def call
79
- if @href
80
- link_tag
81
- else
82
- button_tag
83
- end
84
- end
85
-
86
78
  private
87
79
 
88
- def button_tag
89
- content_tag(:button, button_content, button_attributes)
90
- end
91
-
92
- def link_tag
93
- content_tag(:a, button_content, link_attributes)
94
- end
95
-
96
80
  def button_content
97
81
  if @loading
98
82
  safe_join([loading_spinner, content])
@@ -102,7 +86,7 @@ module Shadcn
102
86
  end
103
87
 
104
88
  def loading_spinner
105
- content_tag(:span, "", class: "animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full", "aria-hidden": true)
89
+ tag.span("", class: "animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full", "aria-hidden": true)
106
90
  end
107
91
 
108
92
  def button_classes
@@ -113,29 +97,5 @@ module Shadcn
113
97
  class_name
114
98
  )
115
99
  end
116
-
117
- def button_attributes
118
- attrs = html_options.merge(
119
- type: @type,
120
- class: button_classes,
121
- disabled: @disabled || @loading || nil,
122
- "aria-disabled": (@disabled || @loading) ? "true" : nil,
123
- "aria-busy": @loading ? "true" : nil
124
- )
125
- attrs.merge!(build_data)
126
- attrs.compact
127
- end
128
-
129
- def link_attributes
130
- attrs = html_options.merge(
131
- href: @href,
132
- class: button_classes,
133
- role: "button",
134
- "aria-disabled": @disabled ? "true" : nil,
135
- tabindex: @disabled ? "-1" : nil
136
- )
137
- attrs.merge!(build_data)
138
- attrs.compact
139
- end
140
100
  end
141
101
  end
@@ -0,0 +1,8 @@
1
+ <div class="<%= card_classes %>" <%= tag_attributes %>>
2
+ <%= header %>
3
+ <%= title %>
4
+ <%= description %>
5
+ <%= content_slot %>
6
+ <%= content %>
7
+ <%= footer %>
8
+ </div>
@@ -50,14 +50,10 @@ module Shadcn
50
50
 
51
51
  BASE_CLASSES = "rounded-xl border bg-card text-card-foreground shadow"
52
52
 
53
- def call
54
- content_tag(:div, card_content, class: merge_classes(BASE_CLASSES), **html_options.merge(build_data))
55
- end
56
-
57
53
  private
58
54
 
59
- def card_content
60
- safe_join([header, title, description, content_slot, content, footer].compact)
55
+ def card_classes
56
+ merge_classes(BASE_CLASSES)
61
57
  end
62
58
  end
63
59
  end
@@ -0,0 +1,32 @@
1
+ <% if has_label? %>
2
+ <label class="flex items-center space-x-2 cursor-pointer">
3
+ <% if @name %>
4
+ <input type="hidden" name="<%= ERB::Util.html_escape_once(@name) %>" value="0" autocomplete="off">
5
+ <% end %>
6
+ <input type="checkbox"
7
+ class="<%= checkbox_classes %>"<% if @name %>
8
+ name="<%= ERB::Util.html_escape_once(@name) %>"<% end %><% if @id %>
9
+ id="<%= ERB::Util.html_escape_once(@id) %>"<% end %><% if @value %>
10
+ value="<%= ERB::Util.html_escape_once(@value) %>"<% end %><% if @checked %>
11
+ checked<% end %><% if @disabled %>
12
+ disabled<% end %><% if @required %>
13
+ required<% end %>
14
+ <%= tag_attributes %>>
15
+ <span class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
16
+ <%= content %>
17
+ </span>
18
+ </label>
19
+ <% else %>
20
+ <% if @name %>
21
+ <input type="hidden" name="<%= ERB::Util.html_escape_once(@name) %>" value="0" autocomplete="off">
22
+ <% end %>
23
+ <input type="checkbox"
24
+ class="<%= checkbox_classes %>"<% if @name %>
25
+ name="<%= ERB::Util.html_escape_once(@name) %>"<% end %><% if @id %>
26
+ id="<%= ERB::Util.html_escape_once(@id) %>"<% end %><% if @value %>
27
+ value="<%= ERB::Util.html_escape_once(@value) %>"<% end %><% if @checked %>
28
+ checked<% end %><% if @disabled %>
29
+ disabled<% end %><% if @required %>
30
+ required<% end %>
31
+ <%= tag_attributes %>>
32
+ <% end %>
@@ -51,53 +51,14 @@ module Shadcn
51
51
  @required = required
52
52
  end
53
53
 
54
- def call
55
- if content.present?
56
- # Render with integrated label
57
- content_tag(:label, class: "flex items-center space-x-2 cursor-pointer") do
58
- safe_join([
59
- hidden_input,
60
- checkbox_input,
61
- content_tag(:span, content, class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
62
- ])
63
- end
64
- else
65
- # Render just the checkbox (for use with external labels)
66
- safe_join([hidden_input, checkbox_input].compact)
67
- end
68
- end
69
-
70
54
  private
71
55
 
72
- def hidden_input
73
- # Include hidden input with "0" value for unchecked state (Rails convention)
74
- return unless @name
75
-
76
- tag(:input,
77
- type: "hidden",
78
- name: @name,
79
- value: "0",
80
- autocomplete: "off"
81
- )
82
- end
83
-
84
- def checkbox_input
85
- tag(:input, input_attributes)
56
+ def checkbox_classes
57
+ cn(BASE_CLASSES, class_name)
86
58
  end
87
59
 
88
- def input_attributes
89
- attrs = {
90
- type: "checkbox",
91
- name: @name,
92
- id: @id,
93
- value: @value,
94
- class: cn(BASE_CLASSES, class_name),
95
- disabled: @disabled || nil,
96
- checked: @checked || nil,
97
- required: @required || nil
98
- }
99
- attrs.merge!(html_options.except(:class))
100
- attrs.compact
60
+ def has_label?
61
+ content.present?
101
62
  end
102
63
  end
103
64
  end
@@ -0,0 +1,8 @@
1
+ <div class="<%= collapsible_classes %>"
2
+ data-controller="shadcn--collapsible"
3
+ data-shadcn--collapsible-open-value="<%= @open %>"
4
+ data-shadcn--collapsible-disabled-value="<%= @disabled %>"
5
+ data-state="<%= state %>"
6
+ <%= tag_attributes %>>
7
+ <%= collapsible_content %>
8
+ </div>
@@ -31,12 +31,12 @@ module Shadcn
31
31
  @disabled = disabled
32
32
  end
33
33
 
34
- def call
35
- content_tag(:div, collapsible_content, collapsible_attributes)
36
- end
37
-
38
34
  private
39
35
 
36
+ def collapsible_classes
37
+ class_name
38
+ end
39
+
40
40
  def collapsible_content
41
41
  safe_join([trigger_wrapper, body].compact)
42
42
  end
@@ -50,17 +50,8 @@ module Shadcn
50
50
  })
51
51
  end
52
52
 
53
- def collapsible_attributes
54
- attrs = {
55
- class: class_name,
56
- "data-controller": "shadcn--collapsible",
57
- "data-shadcn--collapsible-open-value": @open.to_s,
58
- "data-shadcn--collapsible-disabled-value": @disabled.to_s,
59
- "data-state": @open ? "open" : "closed"
60
- }
61
- attrs.merge!(html_options)
62
- attrs.merge!(build_data)
63
- attrs.compact
53
+ def state
54
+ @open ? "open" : "closed"
64
55
  end
65
56
  end
66
57
  end
@@ -10,19 +10,26 @@ module Shadcn
10
10
  CommandEmptyComponent.new(**options)
11
11
  }
12
12
 
13
- # Groups of items
14
- renders_many :groups, lambda { |heading: nil, **options|
15
- CommandGroupComponent.new(heading: heading, **options)
16
- }
17
-
18
- # Direct items (without group)
19
- renders_many :items, lambda { |value: nil, disabled: false, **options|
20
- CommandItemComponent.new(value: value, disabled: disabled, **options)
21
- }
22
-
23
- # Separators
24
- renders_many :separators, lambda { |**options|
25
- CommandSeparatorComponent.new(**options)
13
+ # Use polymorphic slots to preserve the order of groups, items, and separators
14
+ renders_many :list_items, types: {
15
+ group: {
16
+ renders: lambda { |heading: nil, **options, &block|
17
+ CommandGroupComponent.new(heading: heading, **options, &block)
18
+ },
19
+ as: :group
20
+ },
21
+ item: {
22
+ renders: lambda { |value: nil, disabled: false, **options, &block|
23
+ CommandItemComponent.new(value: value, disabled: disabled, **options, &block)
24
+ },
25
+ as: :item
26
+ },
27
+ separator: {
28
+ renders: lambda { |**options|
29
+ CommandSeparatorComponent.new(**options)
30
+ },
31
+ as: :separator
32
+ }
26
33
  }
27
34
 
28
35
  def call
@@ -32,7 +39,15 @@ module Shadcn
32
39
  private
33
40
 
34
41
  def list_content
35
- safe_join([empty, groups, items, separators, content].flatten.compact)
42
+ # Trigger slot evaluation first by accessing content
43
+ raw_content = content
44
+ # If polymorphic slots were used, render them in order with empty at the start
45
+ if list_items.any?
46
+ safe_join([empty, list_items].flatten.compact)
47
+ else
48
+ # Otherwise render the raw block content (for backwards compatibility)
49
+ safe_join([empty, raw_content].flatten.compact)
50
+ end
36
51
  end
37
52
  end
38
53
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Checkbox Item component
5
+ # A menu item that can be checked/unchecked
6
+ class ContextMenuCheckboxItemComponent < BaseComponent
7
+ BASE_CLASSES = "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
8
+
9
+ renders_one :shortcut, lambda { |**options|
10
+ ContextMenuShortcutComponent.new(**options)
11
+ }
12
+
13
+ # @param checked [Boolean] Whether item is checked
14
+ # @param disabled [Boolean] Whether item is disabled
15
+ def initialize(checked: false, disabled: false, **options, &block)
16
+ super(**options, &block)
17
+ @checked = checked
18
+ @disabled = disabled
19
+ end
20
+
21
+ def call
22
+ content_tag(:div, item_content, item_attributes)
23
+ end
24
+
25
+ private
26
+
27
+ def item_content
28
+ safe_join([
29
+ check_indicator,
30
+ content,
31
+ shortcut
32
+ ].compact)
33
+ end
34
+
35
+ def check_indicator
36
+ content_tag(:span, check_icon, class: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center")
37
+ end
38
+
39
+ def check_icon
40
+ return "" unless @checked
41
+
42
+ content_tag(:svg, check_svg_path, {
43
+ xmlns: "http://www.w3.org/2000/svg",
44
+ width: "16",
45
+ height: "16",
46
+ viewBox: "0 0 24 24",
47
+ fill: "none",
48
+ stroke: "currentColor",
49
+ "stroke-width": "2",
50
+ "stroke-linecap": "round",
51
+ "stroke-linejoin": "round",
52
+ class: "h-4 w-4"
53
+ })
54
+ end
55
+
56
+ def check_svg_path
57
+ content_tag(:polyline, "", points: "20 6 9 17 4 12")
58
+ end
59
+
60
+ def item_attributes
61
+ attrs = {
62
+ class: cn(BASE_CLASSES, class_name),
63
+ role: "menuitemcheckbox",
64
+ "aria-checked": @checked.to_s,
65
+ tabindex: @disabled ? nil : "-1",
66
+ "data-disabled": @disabled ? "" : nil,
67
+ "data-state": @checked ? "checked" : "unchecked",
68
+ "data-shadcn--context-menu-target": "item",
69
+ "data-action": "click->shadcn--context-menu#selectItem"
70
+ }
71
+ attrs.merge!(html_options)
72
+ attrs.merge!(build_data)
73
+ attrs.compact
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,11 @@
1
+ <div class="<%= context_menu_classes %>"
2
+ data-controller="<%= context_menu_data_attrs[:controller] %>"
3
+ data-action="<%= context_menu_data_attrs[:action] %>"
4
+ <%= tag_attributes %>>
5
+ <% if trigger? %>
6
+ <div data-shadcn--context-menu-target="trigger" data-action="contextmenu->shadcn--context-menu#show:prevent">
7
+ <%= trigger %>
8
+ </div>
9
+ <% end %>
10
+ <%= menu if menu? %>
11
+ </div>