shadcn-rails 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -1
- data/CLAUDE.md +151 -2
- data/PROGRESS.md +30 -20
- data/README.md +89 -1398
- data/Rakefile +66 -0
- data/__tests__/controllers/combobox_controller.test.js +56 -51
- data/__tests__/controllers/context_menu_controller.test.js +280 -2
- data/__tests__/controllers/menubar_controller.test.js +5 -4
- data/__tests__/controllers/navigation_menu_controller.test.js +5 -4
- data/__tests__/controllers/popover_controller.test.js +35 -60
- data/__tests__/controllers/select_controller.test.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/base_menu_controller.js +266 -0
- data/app/assets/javascripts/shadcn/controllers/combobox_controller.js +13 -8
- data/app/assets/javascripts/shadcn/controllers/command_controller.js +5 -1
- data/app/assets/javascripts/shadcn/controllers/context_menu_controller.js +61 -105
- data/app/assets/javascripts/shadcn/controllers/dropdown_controller.js +49 -170
- data/app/assets/javascripts/shadcn/controllers/menubar_controller.js +10 -7
- data/app/assets/javascripts/shadcn/controllers/navigation_menu_controller.js +10 -6
- data/app/assets/javascripts/shadcn/controllers/popover_controller.js +7 -7
- data/app/assets/javascripts/shadcn/controllers/select_controller.js +12 -10
- data/app/assets/javascripts/shadcn/controllers/sidebar_controller.js +24 -14
- data/app/assets/javascripts/shadcn/index.js +2 -0
- data/app/assets/stylesheets/shadcn/components.css +12 -0
- data/app/components/shadcn/command_list_component.rb +29 -14
- data/app/components/shadcn/context_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/context_menu_content_component.rb +37 -14
- data/app/components/shadcn/context_menu_item_component.rb +3 -2
- data/app/components/shadcn/context_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/context_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/dropdown_menu_checkbox_item_component.rb +76 -0
- data/app/components/shadcn/dropdown_menu_content_component.rb +45 -16
- data/app/components/shadcn/dropdown_menu_radio_group_component.rb +42 -0
- data/app/components/shadcn/dropdown_menu_radio_item_component.rb +76 -0
- data/app/components/shadcn/menubar_content_component.rb +45 -20
- data/app/components/shadcn/menubar_sub_content_component.rb +21 -8
- data/app/components/shadcn/radio_group_item_component.rb +32 -6
- data/app/components/shadcn/resizable_panel_group_component.rb +27 -16
- data/app/components/shadcn/select_component.rb +23 -6
- data/bin/bump +321 -0
- data/bin/release +205 -0
- data/bin/test +75 -0
- data/jest.config.js +1 -1
- data/lib/shadcn/rails/version.rb +1 -1
- data/package-lock.json +27 -4
- data/package.json +4 -1
- metadata +11 -1
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shadcn
|
|
4
|
+
# Dropdown Menu Radio Item component
|
|
5
|
+
# A radio button within a radio group
|
|
6
|
+
class DropdownMenuRadioItemComponent < 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 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--dropdown-target": "item",
|
|
69
|
+
"data-action": "click->shadcn--dropdown#selectRadio"
|
|
70
|
+
}
|
|
71
|
+
attrs.merge!(html_options)
|
|
72
|
+
attrs.merge!(build_data)
|
|
73
|
+
attrs.compact
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -6,23 +6,44 @@ module Shadcn
|
|
|
6
6
|
class MenubarContentComponent < BaseComponent
|
|
7
7
|
BASE_CLASSES = "z-50 min-w-[12rem] 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"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
9
|
+
# Use polymorphic slots to preserve the order of items, labels, separators, etc.
|
|
10
|
+
renders_many :menu_items, types: {
|
|
11
|
+
item: {
|
|
12
|
+
renders: lambda { |**options, &block|
|
|
13
|
+
MenubarItemComponent.new(**options, &block)
|
|
14
|
+
},
|
|
15
|
+
as: :item
|
|
16
|
+
},
|
|
17
|
+
label: {
|
|
18
|
+
renders: lambda { |**options, &block|
|
|
19
|
+
MenubarLabelComponent.new(**options, &block)
|
|
20
|
+
},
|
|
21
|
+
as: :label
|
|
22
|
+
},
|
|
23
|
+
separator: {
|
|
24
|
+
renders: lambda { |**options|
|
|
25
|
+
MenubarSeparatorComponent.new(**options)
|
|
26
|
+
},
|
|
27
|
+
as: :separator
|
|
28
|
+
},
|
|
29
|
+
checkbox_item: {
|
|
30
|
+
renders: lambda { |**options, &block|
|
|
31
|
+
MenubarCheckboxItemComponent.new(**options, &block)
|
|
32
|
+
},
|
|
33
|
+
as: :checkbox_item
|
|
34
|
+
},
|
|
35
|
+
radio_group: {
|
|
36
|
+
renders: lambda { |**options, &block|
|
|
37
|
+
MenubarRadioGroupComponent.new(**options, &block)
|
|
38
|
+
},
|
|
39
|
+
as: :radio_group
|
|
40
|
+
},
|
|
41
|
+
sub_menu: {
|
|
42
|
+
renders: lambda { |**options, &block|
|
|
43
|
+
MenubarSubComponent.new(**options, &block)
|
|
44
|
+
},
|
|
45
|
+
as: :sub_menu
|
|
46
|
+
}
|
|
26
47
|
}
|
|
27
48
|
|
|
28
49
|
# @param align [Symbol] Content alignment (:start, :center, :end)
|
|
@@ -40,10 +61,14 @@ module Shadcn
|
|
|
40
61
|
private
|
|
41
62
|
|
|
42
63
|
def menu_content
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
# Trigger slot evaluation first by accessing content
|
|
65
|
+
raw_content = content
|
|
66
|
+
# If polymorphic slots were used, render them in order
|
|
67
|
+
if menu_items.any?
|
|
68
|
+
safe_join(menu_items)
|
|
45
69
|
else
|
|
46
|
-
content
|
|
70
|
+
# Otherwise render the raw block content (for backwards compatibility)
|
|
71
|
+
raw_content
|
|
47
72
|
end
|
|
48
73
|
end
|
|
49
74
|
|
|
@@ -6,11 +6,20 @@ module Shadcn
|
|
|
6
6
|
class MenubarSubContentComponent < BaseComponent
|
|
7
7
|
BASE_CLASSES = "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg 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"
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
# Use polymorphic slots to preserve the order of items and separators
|
|
10
|
+
renders_many :menu_items, types: {
|
|
11
|
+
item: {
|
|
12
|
+
renders: lambda { |**options, &block|
|
|
13
|
+
MenubarItemComponent.new(**options, &block)
|
|
14
|
+
},
|
|
15
|
+
as: :item
|
|
16
|
+
},
|
|
17
|
+
separator: {
|
|
18
|
+
renders: lambda { |**options|
|
|
19
|
+
MenubarSeparatorComponent.new(**options)
|
|
20
|
+
},
|
|
21
|
+
as: :separator
|
|
22
|
+
}
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
def call
|
|
@@ -20,10 +29,14 @@ module Shadcn
|
|
|
20
29
|
private
|
|
21
30
|
|
|
22
31
|
def sub_content
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
# Trigger slot evaluation first by accessing content
|
|
33
|
+
raw_content = content
|
|
34
|
+
# If polymorphic slots were used, render them in order
|
|
35
|
+
if menu_items.any?
|
|
36
|
+
safe_join(menu_items)
|
|
25
37
|
else
|
|
26
|
-
content
|
|
38
|
+
# Otherwise render the raw block content (for backwards compatibility)
|
|
39
|
+
raw_content
|
|
27
40
|
end
|
|
28
41
|
end
|
|
29
42
|
|
|
@@ -8,6 +8,9 @@ module Shadcn
|
|
|
8
8
|
# @example With label parameter (Tier 2 API)
|
|
9
9
|
# <%= group.with_item(value: "free", label: "Free") %>
|
|
10
10
|
#
|
|
11
|
+
# @example With label and description
|
|
12
|
+
# <%= group.with_item(value: "pro", label: "Pro", description: "For professional developers") %>
|
|
13
|
+
#
|
|
11
14
|
# @example With block content (backward compatible)
|
|
12
15
|
# <%= group.with_item(value: "free") { "Free" } %>
|
|
13
16
|
#
|
|
@@ -36,10 +39,12 @@ module Shadcn
|
|
|
36
39
|
].join(" ")
|
|
37
40
|
|
|
38
41
|
LABEL_CLASSES = "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
42
|
+
DESCRIPTION_CLASSES = "text-sm text-muted-foreground"
|
|
39
43
|
|
|
40
44
|
# @param value [String] The value for this radio option
|
|
41
45
|
# @param id [String, nil] HTML id attribute
|
|
42
46
|
# @param label [String, nil] Label text (alternative to block content)
|
|
47
|
+
# @param description [String, nil] Description text displayed below the label
|
|
43
48
|
# @param disabled [Boolean] Whether this option is disabled
|
|
44
49
|
# @param group_name [String, nil] The name attribute from parent group
|
|
45
50
|
# @param selected [Boolean] Whether this option is selected
|
|
@@ -47,6 +52,7 @@ module Shadcn
|
|
|
47
52
|
value:,
|
|
48
53
|
id: nil,
|
|
49
54
|
label: nil,
|
|
55
|
+
description: nil,
|
|
50
56
|
disabled: false,
|
|
51
57
|
group_name: nil,
|
|
52
58
|
selected: false,
|
|
@@ -57,6 +63,7 @@ module Shadcn
|
|
|
57
63
|
@value = value
|
|
58
64
|
@id = id || "radio-#{value}"
|
|
59
65
|
@label = label
|
|
66
|
+
@description = description
|
|
60
67
|
@disabled = disabled
|
|
61
68
|
@group_name = group_name
|
|
62
69
|
@selected = selected
|
|
@@ -66,12 +73,17 @@ module Shadcn
|
|
|
66
73
|
label_text = @label || content.presence
|
|
67
74
|
|
|
68
75
|
if label_text.present?
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
if @description.present?
|
|
77
|
+
# Render with label and description
|
|
78
|
+
render_with_description(label_text)
|
|
79
|
+
else
|
|
80
|
+
# Render with integrated label only
|
|
81
|
+
content_tag(:label, label_wrapper_attributes) do
|
|
82
|
+
safe_join([
|
|
83
|
+
radio_input,
|
|
84
|
+
content_tag(:span, label_text, class: LABEL_CLASSES)
|
|
85
|
+
])
|
|
86
|
+
end
|
|
75
87
|
end
|
|
76
88
|
else
|
|
77
89
|
# Render just the radio input (for use with external labels)
|
|
@@ -81,6 +93,20 @@ module Shadcn
|
|
|
81
93
|
|
|
82
94
|
private
|
|
83
95
|
|
|
96
|
+
def render_with_description(label_text)
|
|
97
|
+
content_tag(:div, class: "flex items-start space-x-3") do
|
|
98
|
+
safe_join([
|
|
99
|
+
content_tag(:div, class: "mt-0.5") { radio_input },
|
|
100
|
+
content_tag(:div, class: "grid gap-1.5 leading-none") do
|
|
101
|
+
safe_join([
|
|
102
|
+
content_tag(:label, label_text, class: cn(LABEL_CLASSES, "cursor-pointer"), for: @id),
|
|
103
|
+
content_tag(:p, @description, class: DESCRIPTION_CLASSES)
|
|
104
|
+
])
|
|
105
|
+
end
|
|
106
|
+
])
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
84
110
|
def radio_input
|
|
85
111
|
tag(:input, input_attributes)
|
|
86
112
|
end
|
|
@@ -32,22 +32,30 @@ module Shadcn
|
|
|
32
32
|
# <% end %>
|
|
33
33
|
#
|
|
34
34
|
class ResizablePanelGroupComponent < BaseComponent
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
min_size:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
# Use polymorphic slots to preserve the order of panels and handles
|
|
36
|
+
renders_many :items, types: {
|
|
37
|
+
panel: {
|
|
38
|
+
renders: lambda { |default_size: nil, min_size: nil, max_size: nil, **options|
|
|
39
|
+
ResizablePanelComponent.new(
|
|
40
|
+
default_size: default_size,
|
|
41
|
+
min_size: min_size,
|
|
42
|
+
max_size: max_size,
|
|
43
|
+
direction: @direction,
|
|
44
|
+
**options
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
as: :panel
|
|
48
|
+
},
|
|
49
|
+
handle: {
|
|
50
|
+
renders: lambda { |with_handle: false, **options|
|
|
51
|
+
ResizableHandleComponent.new(
|
|
52
|
+
with_handle: with_handle,
|
|
53
|
+
direction: @direction,
|
|
54
|
+
**options
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
as: :handle
|
|
58
|
+
}
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
DIRECTIONS = {
|
|
@@ -70,7 +78,10 @@ module Shadcn
|
|
|
70
78
|
private
|
|
71
79
|
|
|
72
80
|
def group_content
|
|
81
|
+
# Trigger slot evaluation first
|
|
73
82
|
content
|
|
83
|
+
# Render all items in the order they were added
|
|
84
|
+
safe_join(items)
|
|
74
85
|
end
|
|
75
86
|
|
|
76
87
|
def group_attributes
|
|
@@ -25,11 +25,20 @@ module Shadcn
|
|
|
25
25
|
CONTENT_CLASSES = "absolute left-0 top-full z-50 mt-1 max-h-96 min-w-[var(--radix-select-trigger-width)] w-max overflow-hidden rounded-md border bg-popover 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"
|
|
26
26
|
VIEWPORT_CLASSES = "p-1"
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
# Use polymorphic slots to preserve the order of items and groups
|
|
29
|
+
renders_many :select_items, types: {
|
|
30
|
+
item: {
|
|
31
|
+
renders: lambda { |value:, **options, &block|
|
|
32
|
+
SelectItemComponent.new(value: value, **options, &block)
|
|
33
|
+
},
|
|
34
|
+
as: :item
|
|
35
|
+
},
|
|
36
|
+
group: {
|
|
37
|
+
renders: lambda { |label: nil, **options, &block|
|
|
38
|
+
SelectGroupComponent.new(label: label, **options, &block)
|
|
39
|
+
},
|
|
40
|
+
as: :group
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
# @param name [String, nil] Form field name
|
|
@@ -133,7 +142,15 @@ module Shadcn
|
|
|
133
142
|
end
|
|
134
143
|
|
|
135
144
|
def items_content
|
|
136
|
-
|
|
145
|
+
# Trigger slot evaluation first by accessing content
|
|
146
|
+
raw_content = content
|
|
147
|
+
# If polymorphic slots were used, render them in order
|
|
148
|
+
if select_items.any?
|
|
149
|
+
safe_join(select_items)
|
|
150
|
+
else
|
|
151
|
+
# Otherwise render the raw block content (for backwards compatibility)
|
|
152
|
+
raw_content
|
|
153
|
+
end
|
|
137
154
|
end
|
|
138
155
|
|
|
139
156
|
def select_attributes
|