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
@@ -28,37 +28,17 @@ module Shadcn
28
28
  ContextMenuContentComponent.new(**options)
29
29
  }
30
30
 
31
- def call
32
- content_tag(:div, context_menu_content, context_menu_attributes)
33
- end
34
-
35
31
  private
36
32
 
37
- def context_menu_content
38
- safe_join([
39
- trigger_wrapper,
40
- menu
41
- ].compact)
42
- end
43
-
44
- def trigger_wrapper
45
- return unless trigger
46
-
47
- content_tag(:div, trigger, {
48
- "data-shadcn--context-menu-target": "trigger",
49
- "data-action": "contextmenu->shadcn--context-menu#show:prevent"
50
- })
33
+ def context_menu_classes
34
+ cn("relative inline-block", class_name)
51
35
  end
52
36
 
53
- def context_menu_attributes
54
- attrs = {
55
- class: cn("relative inline-block", class_name),
56
- "data-controller": "shadcn--context-menu",
57
- "data-action": "keydown.escape->shadcn--context-menu#close"
37
+ def context_menu_data_attrs
38
+ {
39
+ controller: "shadcn--context-menu",
40
+ action: "keydown.escape->shadcn--context-menu#close"
58
41
  }
59
- attrs.merge!(html_options)
60
- attrs.merge!(build_data)
61
- attrs.compact
62
42
  end
63
43
  end
64
44
  end
@@ -3,16 +3,40 @@
3
3
  module Shadcn
4
4
  # Context Menu Content component
5
5
  class ContextMenuContentComponent < BaseComponent
6
- BASE_CLASSES = "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"
6
+ BASE_CLASSES = "shadcn-context-menu z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
7
7
 
8
- renders_many :items, lambda { |**options, &block|
9
- ContextMenuItemComponent.new(**options, &block)
10
- }
11
- renders_many :labels, lambda { |**options, &block|
12
- ContextMenuLabelComponent.new(**options, &block)
13
- }
14
- renders_many :separators, lambda { |**options|
15
- ContextMenuSeparatorComponent.new(**options)
8
+ # Use polymorphic slots to preserve the order of items, labels, and separators
9
+ renders_many :menu_items, types: {
10
+ item: {
11
+ renders: lambda { |**options, &block|
12
+ ContextMenuItemComponent.new(**options, &block)
13
+ },
14
+ as: :item
15
+ },
16
+ checkbox_item: {
17
+ renders: lambda { |**options, &block|
18
+ ContextMenuCheckboxItemComponent.new(**options, &block)
19
+ },
20
+ as: :checkbox_item
21
+ },
22
+ radio_group: {
23
+ renders: lambda { |**options, &block|
24
+ ContextMenuRadioGroupComponent.new(**options, &block)
25
+ },
26
+ as: :radio_group
27
+ },
28
+ label: {
29
+ renders: lambda { |**options, &block|
30
+ ContextMenuLabelComponent.new(**options, &block)
31
+ },
32
+ as: :label
33
+ },
34
+ separator: {
35
+ renders: lambda { |**options|
36
+ ContextMenuSeparatorComponent.new(**options)
37
+ },
38
+ as: :separator
39
+ }
16
40
  }
17
41
 
18
42
  def call
@@ -22,11 +46,10 @@ module Shadcn
22
46
  private
23
47
 
24
48
  def menu_content
25
- if items.any? || labels.any? || separators.any?
26
- safe_join([labels, items, separators, content].flatten.compact)
27
- else
28
- content
29
- end
49
+ # Trigger slot evaluation first
50
+ content
51
+ # Render all menu items in the order they were added
52
+ safe_join(menu_items)
30
53
  end
31
54
 
32
55
  def menu_attributes
@@ -3,11 +3,11 @@
3
3
  module Shadcn
4
4
  # Context Menu Item component
5
5
  class ContextMenuItemComponent < BaseComponent
6
- BASE_CLASSES = "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0"
6
+ BASE_CLASSES = "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 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 [&>svg]:size-4 [&>svg]:shrink-0"
7
7
 
8
8
  VARIANTS = {
9
9
  default: "",
10
- destructive: "text-destructive focus:bg-destructive focus:text-destructive-foreground"
10
+ destructive: "text-destructive hover:bg-destructive hover:text-destructive-foreground focus:bg-destructive focus:text-destructive-foreground"
11
11
  }.freeze
12
12
 
13
13
  renders_one :shortcut, lambda { |**options|
@@ -53,6 +53,7 @@ module Shadcn
53
53
  tabindex: @disabled ? nil : "-1",
54
54
  href: @href,
55
55
  "data-disabled": @disabled ? "" : nil,
56
+ "data-shadcn--context-menu-target": "item",
56
57
  "data-action": "click->shadcn--context-menu#selectItem"
57
58
  }
58
59
  attrs.merge!(html_options)
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Radio Group component
5
+ # Group of mutually exclusive radio items
6
+ class ContextMenuRadioGroupComponent < BaseComponent
7
+ renders_many :items, lambda { |**options, &block|
8
+ ContextMenuRadioItemComponent.new(**options, &block)
9
+ }
10
+
11
+ # @param value [String] Currently selected value
12
+ def initialize(value: nil, **options, &block)
13
+ super(**options, &block)
14
+ @value = value
15
+ end
16
+
17
+ def call
18
+ content_tag(:div, group_content, group_attributes)
19
+ end
20
+
21
+ private
22
+
23
+ def group_content
24
+ if items.any?
25
+ safe_join(items)
26
+ else
27
+ content
28
+ end
29
+ end
30
+
31
+ def group_attributes
32
+ attrs = {
33
+ class: class_name,
34
+ role: "group",
35
+ "data-value": @value
36
+ }
37
+ attrs.merge!(html_options)
38
+ attrs.merge!(build_data)
39
+ attrs.compact
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Context Menu Radio Item component
5
+ # A radio button within a radio group
6
+ class ContextMenuRadioItemComponent < 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 value [String] Value of this radio item
14
+ # @param checked [Boolean] Whether item is selected
15
+ # @param disabled [Boolean] Whether item is disabled
16
+ def initialize(value: nil, checked: false, disabled: false, **options, &block)
17
+ super(**options, &block)
18
+ @value = value
19
+ @checked = checked
20
+ @disabled = disabled
21
+ end
22
+
23
+ def call
24
+ content_tag(:div, item_content, item_attributes)
25
+ end
26
+
27
+ private
28
+
29
+ def item_content
30
+ safe_join([
31
+ radio_indicator,
32
+ content,
33
+ shortcut
34
+ ].compact)
35
+ end
36
+
37
+ def radio_indicator
38
+ content_tag(:span, radio_icon, class: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center")
39
+ end
40
+
41
+ def radio_icon
42
+ return "" unless @checked
43
+
44
+ content_tag(:svg, circle_svg, {
45
+ xmlns: "http://www.w3.org/2000/svg",
46
+ width: "16",
47
+ height: "16",
48
+ viewBox: "0 0 24 24",
49
+ fill: "currentColor",
50
+ stroke: "none",
51
+ class: "h-4 w-4"
52
+ })
53
+ end
54
+
55
+ def circle_svg
56
+ content_tag(:circle, "", cx: "12", cy: "12", r: "6")
57
+ end
58
+
59
+ def item_attributes
60
+ attrs = {
61
+ class: cn(BASE_CLASSES, class_name),
62
+ role: "menuitemradio",
63
+ "aria-checked": @checked.to_s,
64
+ tabindex: @disabled ? nil : "-1",
65
+ "data-disabled": @disabled ? "" : nil,
66
+ "data-state": @checked ? "checked" : "unchecked",
67
+ "data-value": @value,
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,14 @@
1
+ <div id="<%= @id %>"
2
+ class="<%= dialog_classes %>"
3
+ data-controller="<%= dialog_data_attrs[:controller] %>"
4
+ data-shadcn--dialog-open-value="<%= dialog_data_attrs[:"shadcn--dialog-open-value"] %>"
5
+ data-shadcn--dialog-modal-value="<%= dialog_data_attrs[:"shadcn--dialog-modal-value"] %>"
6
+ data-dialog-id="<%= dialog_data_attrs[:"dialog-id"] %>"
7
+ <%= tag_attributes %>>
8
+ <% if trigger? %>
9
+ <div data-shadcn--dialog-target="trigger" data-action="click->shadcn--dialog#open">
10
+ <%= trigger %>
11
+ </div>
12
+ <% end %>
13
+ <%= body if body? %>
14
+ </div>
@@ -38,40 +38,19 @@ module Shadcn
38
38
  @modal = modal
39
39
  end
40
40
 
41
- def call
42
- content_tag(:div, dialog_content, dialog_attributes)
43
- end
44
-
45
41
  private
46
42
 
47
- def dialog_content
48
- safe_join([
49
- trigger_wrapper,
50
- body
51
- ].compact)
52
- end
53
-
54
- def trigger_wrapper
55
- return unless trigger
56
-
57
- content_tag(:div, trigger, {
58
- "data-shadcn--dialog-target": "trigger",
59
- "data-action": "click->shadcn--dialog#open"
60
- })
43
+ def dialog_classes
44
+ class_name
61
45
  end
62
46
 
63
- def dialog_attributes
64
- attrs = {
65
- id: @id,
66
- class: class_name,
67
- "data-controller": "shadcn--dialog",
68
- "data-shadcn--dialog-open-value": @open.to_s,
69
- "data-shadcn--dialog-modal-value": @modal.to_s,
70
- "data-dialog-id": @id
47
+ def dialog_data_attrs
48
+ {
49
+ controller: "shadcn--dialog",
50
+ "shadcn--dialog-open-value": @open.to_s,
51
+ "shadcn--dialog-modal-value": @modal.to_s,
52
+ "dialog-id": @id
71
53
  }
72
- attrs.merge!(html_options)
73
- attrs.merge!(build_data)
74
- attrs.compact
75
54
  end
76
55
  end
77
56
  end
@@ -0,0 +1,12 @@
1
+ <div class="<%= drawer_classes %>"
2
+ data-controller="<%= drawer_data_attrs[:controller] %>"
3
+ data-shadcn--drawer-open-value="<%= drawer_data_attrs[:"shadcn--drawer-open-value"] %>"
4
+ data-shadcn--drawer-direction-value="<%= drawer_data_attrs[:"shadcn--drawer-direction-value"] %>"
5
+ <%= tag_attributes %>>
6
+ <% if trigger? %>
7
+ <div data-shadcn--drawer-target="trigger" data-action="click->shadcn--drawer#open">
8
+ <%= trigger %>
9
+ </div>
10
+ <% end %>
11
+ <%= body if body? %>
12
+ </div>
@@ -35,38 +35,18 @@ module Shadcn
35
35
  @direction = direction
36
36
  end
37
37
 
38
- def call
39
- content_tag(:div, drawer_content, drawer_attributes)
40
- end
41
-
42
38
  private
43
39
 
44
- def drawer_content
45
- safe_join([
46
- trigger_wrapper,
47
- body
48
- ].compact)
49
- end
50
-
51
- def trigger_wrapper
52
- return unless trigger
53
-
54
- content_tag(:div, trigger, {
55
- "data-shadcn--drawer-target": "trigger",
56
- "data-action": "click->shadcn--drawer#open"
57
- })
40
+ def drawer_classes
41
+ class_name
58
42
  end
59
43
 
60
- def drawer_attributes
61
- attrs = {
62
- class: class_name,
63
- "data-controller": "shadcn--drawer",
64
- "data-shadcn--drawer-open-value": @open.to_s,
65
- "data-shadcn--drawer-direction-value": @direction.to_s
44
+ def drawer_data_attrs
45
+ {
46
+ controller: "shadcn--drawer",
47
+ "shadcn--drawer-open-value": @open.to_s,
48
+ "shadcn--drawer-direction-value": @direction.to_s
66
49
  }
67
- attrs.merge!(html_options)
68
- attrs.merge!(build_data)
69
- attrs.compact
70
50
  end
71
51
  end
72
52
  end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Checkbox Item component
5
+ # A menu item that can be checked/unchecked
6
+ class DropdownMenuCheckboxItemComponent < 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
+ DropdownMenuShortcutComponent.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--dropdown-target": "item",
69
+ "data-action": "click->shadcn--dropdown#toggleCheckbox"
70
+ }
71
+ attrs.merge!(html_options)
72
+ attrs.merge!(build_data)
73
+ attrs.compact
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,14 @@
1
+ <div class="<%= dropdown_classes %>"
2
+ data-controller="<%= dropdown_data_attrs[:controller] %>"
3
+ data-shadcn--dropdown-open-value="<%= dropdown_data_attrs[:"shadcn--dropdown-open-value"] %>"
4
+ data-shadcn--dropdown-align-value="<%= dropdown_data_attrs[:"shadcn--dropdown-align-value"] %>"
5
+ data-shadcn--dropdown-side-value="<%= dropdown_data_attrs[:"shadcn--dropdown-side-value"] %>"
6
+ data-action="<%= dropdown_data_attrs[:action] %>"
7
+ <%= tag_attributes %>>
8
+ <% if trigger? %>
9
+ <div data-shadcn--dropdown-target="trigger" data-action="click->shadcn--dropdown#toggle">
10
+ <%= trigger %>
11
+ </div>
12
+ <% end %>
13
+ <%= menu if menu? %>
14
+ </div>
@@ -36,40 +36,20 @@ module Shadcn
36
36
  @side = side
37
37
  end
38
38
 
39
- def call
40
- content_tag(:div, dropdown_content, dropdown_attributes)
41
- end
42
-
43
39
  private
44
40
 
45
- def dropdown_content
46
- safe_join([
47
- trigger_wrapper,
48
- menu
49
- ].compact)
50
- end
51
-
52
- def trigger_wrapper
53
- return unless trigger
54
-
55
- content_tag(:div, trigger, {
56
- "data-shadcn--dropdown-target": "trigger",
57
- "data-action": "click->shadcn--dropdown#toggle"
58
- })
41
+ def dropdown_classes
42
+ cn("relative inline-block", class_name)
59
43
  end
60
44
 
61
- def dropdown_attributes
62
- attrs = {
63
- class: cn("relative inline-block", class_name),
64
- "data-controller": "shadcn--dropdown",
65
- "data-shadcn--dropdown-open-value": @open.to_s,
66
- "data-shadcn--dropdown-align-value": @align.to_s,
67
- "data-shadcn--dropdown-side-value": @side.to_s,
68
- "data-action": "keydown.escape->shadcn--dropdown#close clickOutside->shadcn--dropdown#close"
45
+ def dropdown_data_attrs
46
+ {
47
+ controller: "shadcn--dropdown",
48
+ "shadcn--dropdown-open-value": @open.to_s,
49
+ "shadcn--dropdown-align-value": @align.to_s,
50
+ "shadcn--dropdown-side-value": @side.to_s,
51
+ action: "keydown.escape->shadcn--dropdown#close clickOutside->shadcn--dropdown#close"
69
52
  }
70
- attrs.merge!(html_options)
71
- attrs.merge!(build_data)
72
- attrs.compact
73
53
  end
74
54
  end
75
55
  end
@@ -5,17 +5,44 @@ module Shadcn
5
5
  class DropdownMenuContentComponent < BaseComponent
6
6
  BASE_CLASSES = "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
7
7
 
8
- renders_many :items, lambda { |**options, &block|
9
- DropdownMenuItemComponent.new(**options, &block)
10
- }
11
- renders_many :labels, lambda { |**options, &block|
12
- DropdownMenuLabelComponent.new(**options, &block)
13
- }
14
- renders_many :separators, lambda { |**options|
15
- DropdownMenuSeparatorComponent.new(**options)
16
- }
17
- renders_many :groups, lambda { |**options, &block|
18
- DropdownMenuGroupComponent.new(**options, &block)
8
+ # Use polymorphic slots to preserve the order of items, labels, separators, groups, etc.
9
+ renders_many :menu_items, types: {
10
+ item: {
11
+ renders: lambda { |**options, &block|
12
+ DropdownMenuItemComponent.new(**options, &block)
13
+ },
14
+ as: :item
15
+ },
16
+ label: {
17
+ renders: lambda { |**options, &block|
18
+ DropdownMenuLabelComponent.new(**options, &block)
19
+ },
20
+ as: :label
21
+ },
22
+ separator: {
23
+ renders: lambda { |**options|
24
+ DropdownMenuSeparatorComponent.new(**options)
25
+ },
26
+ as: :separator
27
+ },
28
+ group: {
29
+ renders: lambda { |**options, &block|
30
+ DropdownMenuGroupComponent.new(**options, &block)
31
+ },
32
+ as: :group
33
+ },
34
+ checkbox_item: {
35
+ renders: lambda { |**options, &block|
36
+ DropdownMenuCheckboxItemComponent.new(**options, &block)
37
+ },
38
+ as: :checkbox_item
39
+ },
40
+ radio_group: {
41
+ renders: lambda { |**options, &block|
42
+ DropdownMenuRadioGroupComponent.new(**options, &block)
43
+ },
44
+ as: :radio_group
45
+ }
19
46
  }
20
47
 
21
48
  def call
@@ -25,12 +52,14 @@ module Shadcn
25
52
  private
26
53
 
27
54
  def menu_content
28
- # If items/labels/separators are used, render them
29
- # Otherwise render the block content
30
- if items.any? || labels.any? || separators.any? || groups.any?
31
- safe_join([labels, items, separators, groups, content].flatten.compact)
55
+ # Trigger slot evaluation first by accessing content
56
+ raw_content = content
57
+ # If polymorphic slots were used, render them in order
58
+ if menu_items.any?
59
+ safe_join(menu_items)
32
60
  else
33
- content
61
+ # Otherwise render the raw block content (for backwards compatibility)
62
+ raw_content
34
63
  end
35
64
  end
36
65
 
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shadcn
4
+ # Dropdown Menu Radio Group component
5
+ # Group of mutually exclusive radio items
6
+ class DropdownMenuRadioGroupComponent < BaseComponent
7
+ renders_many :items, lambda { |**options, &block|
8
+ DropdownMenuRadioItemComponent.new(**options, &block)
9
+ }
10
+
11
+ # @param value [String] Currently selected value
12
+ def initialize(value: nil, **options, &block)
13
+ super(**options, &block)
14
+ @value = value
15
+ end
16
+
17
+ def call
18
+ content_tag(:div, group_content, group_attributes)
19
+ end
20
+
21
+ private
22
+
23
+ def group_content
24
+ if items.any?
25
+ safe_join(items)
26
+ else
27
+ content
28
+ end
29
+ end
30
+
31
+ def group_attributes
32
+ attrs = {
33
+ class: class_name,
34
+ role: "group",
35
+ "data-value": @value
36
+ }
37
+ attrs.merge!(html_options)
38
+ attrs.merge!(build_data)
39
+ attrs.compact
40
+ end
41
+ end
42
+ end