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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import { useMatchMedia } from "stimulus-use"
|
|
2
3
|
|
|
3
4
|
// Constants for sidebar dimensions
|
|
4
5
|
const SIDEBAR_COOKIE_NAME = "sidebar:state"
|
|
@@ -7,6 +8,10 @@ const SIDEBAR_WIDTH = "16rem"
|
|
|
7
8
|
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
|
8
9
|
const SIDEBAR_WIDTH_ICON = "3rem"
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Sidebar Controller
|
|
13
|
+
* Uses stimulus-use useMatchMedia for responsive behavior
|
|
14
|
+
*/
|
|
10
15
|
export default class extends Controller {
|
|
11
16
|
static targets = ["sidebar"]
|
|
12
17
|
static values = {
|
|
@@ -26,10 +31,13 @@ export default class extends Controller {
|
|
|
26
31
|
this.handleKeyDown = this.handleKeyDown.bind(this)
|
|
27
32
|
document.addEventListener("keydown", this.handleKeyDown)
|
|
28
33
|
|
|
29
|
-
//
|
|
34
|
+
// Use stimulus-use for responsive media query detection
|
|
30
35
|
this.isMobile = window.innerWidth < 768
|
|
31
|
-
this
|
|
32
|
-
|
|
36
|
+
useMatchMedia(this, {
|
|
37
|
+
mediaQueries: {
|
|
38
|
+
mobile: "(max-width: 767px)"
|
|
39
|
+
}
|
|
40
|
+
})
|
|
33
41
|
|
|
34
42
|
// Initial state sync
|
|
35
43
|
this.syncState()
|
|
@@ -37,7 +45,19 @@ export default class extends Controller {
|
|
|
37
45
|
|
|
38
46
|
disconnect() {
|
|
39
47
|
document.removeEventListener("keydown", this.handleKeyDown)
|
|
40
|
-
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Called by stimulus-use when mobile media query state changes
|
|
51
|
+
mobileChanged({ matches }) {
|
|
52
|
+
const wasMobile = this.isMobile
|
|
53
|
+
this.isMobile = matches
|
|
54
|
+
|
|
55
|
+
// Close mobile sidebar when switching to desktop
|
|
56
|
+
if (wasMobile && !this.isMobile) {
|
|
57
|
+
this.openMobileValue = false
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.syncState()
|
|
41
61
|
}
|
|
42
62
|
|
|
43
63
|
handleKeyDown(event) {
|
|
@@ -51,16 +71,6 @@ export default class extends Controller {
|
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
|
|
54
|
-
handleResize() {
|
|
55
|
-
const wasMobile = this.isMobile
|
|
56
|
-
this.isMobile = window.innerWidth < 768
|
|
57
|
-
|
|
58
|
-
// Close mobile sidebar when switching to desktop
|
|
59
|
-
if (wasMobile && !this.isMobile) {
|
|
60
|
-
this.openMobileValue = false
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
74
|
toggle() {
|
|
65
75
|
if (this.isMobile) {
|
|
66
76
|
this.openMobileValue = !this.openMobileValue
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
// Import all controllers
|
|
17
|
+
import BaseMenuController from "./controllers/base_menu_controller"
|
|
17
18
|
import AccordionController from "./controllers/accordion_controller"
|
|
18
19
|
import AvatarController from "./controllers/avatar_controller"
|
|
19
20
|
import CalendarController from "./controllers/calendar_controller"
|
|
@@ -49,6 +50,7 @@ import SidebarController from "./controllers/sidebar_controller"
|
|
|
49
50
|
|
|
50
51
|
// Export individual controllers
|
|
51
52
|
export {
|
|
53
|
+
BaseMenuController,
|
|
52
54
|
AccordionController,
|
|
53
55
|
AvatarController,
|
|
54
56
|
CalendarController,
|
|
@@ -437,6 +437,18 @@ button[data-state="off"].shadcn-toggle {
|
|
|
437
437
|
animation: fade-out 150ms ease-in, zoom-out 150ms ease-in;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
/* ============================================
|
|
441
|
+
Context Menu Component
|
|
442
|
+
============================================ */
|
|
443
|
+
|
|
444
|
+
.shadcn-context-menu[data-state="open"] {
|
|
445
|
+
animation: fade-in 100ms ease-out, zoom-in 100ms ease-out;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.shadcn-context-menu[data-state="closed"] {
|
|
449
|
+
animation: fade-out 100ms ease-in, zoom-out 100ms ease-in;
|
|
450
|
+
}
|
|
451
|
+
|
|
440
452
|
/* ============================================
|
|
441
453
|
Drawer Component
|
|
442
454
|
============================================ */
|
|
@@ -10,19 +10,26 @@ module Shadcn
|
|
|
10
10
|
CommandEmptyComponent.new(**options)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
#
|
|
14
|
-
renders_many :
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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,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
|
|
@@ -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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
#
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|