m9sh 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/Dockerfile +2 -1
- data/GEM_README.md +284 -0
- data/LICENSE.txt +21 -0
- data/M9SH_CLI.md +453 -0
- data/PUBLISHING.md +331 -0
- data/README.md +120 -52
- data/app/components/m9sh/accordion_component.rb +3 -3
- data/app/components/m9sh/alert_component.rb +7 -9
- data/app/components/m9sh/base_component.rb +1 -0
- data/app/components/m9sh/button_component.rb +3 -2
- data/app/components/m9sh/color_customizer_component.rb +624 -0
- data/app/components/m9sh/dialog_close_component.rb +30 -0
- data/app/components/m9sh/dialog_component.rb +11 -99
- data/app/components/m9sh/dialog_content_component.rb +102 -0
- data/app/components/m9sh/dialog_description_component.rb +14 -0
- data/app/components/m9sh/dialog_footer_component.rb +14 -0
- data/app/components/m9sh/dialog_header_component.rb +27 -0
- data/app/components/m9sh/dialog_title_component.rb +14 -0
- data/app/components/m9sh/dialog_trigger_component.rb +23 -0
- data/app/components/m9sh/dropdown_menu_content_component.rb +1 -1
- data/app/components/m9sh/dropdown_menu_item_component.rb +1 -1
- data/app/components/m9sh/dropdown_menu_trigger_component.rb +1 -1
- data/app/components/m9sh/icon_component.rb +78 -0
- data/app/components/m9sh/main_component.rb +1 -1
- data/app/components/m9sh/menu_component.rb +85 -0
- data/app/components/m9sh/navbar_component.rb +186 -0
- data/app/components/m9sh/navigation_menu_component.rb +2 -2
- data/app/components/m9sh/popover_component.rb +12 -7
- data/app/components/m9sh/radio_group_component.rb +45 -13
- data/app/components/m9sh/sheet_component.rb +6 -6
- data/app/components/m9sh/sidebar_component.rb +6 -1
- data/app/components/m9sh/skeleton_component.rb +7 -1
- data/app/components/m9sh/tabs_component.rb +76 -48
- data/app/components/m9sh/textarea_component.rb +1 -1
- data/app/components/m9sh/theme_toggle_component.rb +1 -0
- data/app/javascript/controllers/m9sh/popover_controller.js +24 -18
- data/app/javascript/controllers/m9sh/sidebar_controller.js +29 -7
- data/lib/m9sh/config.rb +5 -5
- data/lib/m9sh/registry.rb +2 -2
- data/lib/m9sh/registry.yml +37 -0
- data/lib/m9sh/version.rb +1 -1
- data/lib/tasks/tailwindcss.rake +15 -0
- data/m9sh.gemspec +48 -0
- data/publish.sh +48 -0
- metadata +20 -3
- data/fix_namespaces.py +0 -32
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module M9sh
|
4
|
+
class NavbarComponent < BaseComponent
|
5
|
+
include Utilities
|
6
|
+
|
7
|
+
renders_one :brand
|
8
|
+
renders_one :navigation
|
9
|
+
renders_one :left_action
|
10
|
+
renders_one :right_action
|
11
|
+
|
12
|
+
def initialize(
|
13
|
+
sticky: true,
|
14
|
+
transparent: false,
|
15
|
+
container: true,
|
16
|
+
size: :default,
|
17
|
+
border: true,
|
18
|
+
blur: false,
|
19
|
+
class_name: nil,
|
20
|
+
**extra_attrs
|
21
|
+
)
|
22
|
+
@sticky = sticky
|
23
|
+
@transparent = transparent
|
24
|
+
@container = container
|
25
|
+
@size = size
|
26
|
+
@border = border
|
27
|
+
@blur = blur
|
28
|
+
@action_blocks = []
|
29
|
+
@mobile_menu_block = nil
|
30
|
+
super(class_name: class_name, **extra_attrs)
|
31
|
+
end
|
32
|
+
|
33
|
+
def with_actions(&block)
|
34
|
+
@action_blocks << block if block
|
35
|
+
end
|
36
|
+
|
37
|
+
def with_mobile_menu(&block)
|
38
|
+
@mobile_menu_block = block if block
|
39
|
+
end
|
40
|
+
|
41
|
+
def call
|
42
|
+
tag.header(
|
43
|
+
**component_attrs(header_classes),
|
44
|
+
role: "banner"
|
45
|
+
) do
|
46
|
+
tag.div(class: container_classes) do
|
47
|
+
tag.div(class: content_classes) do
|
48
|
+
safe_join([
|
49
|
+
left_action,
|
50
|
+
render_brand_and_nav,
|
51
|
+
render_actions_with_mobile_menu
|
52
|
+
].compact)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def header_classes
|
61
|
+
classes = ["w-full transition-colors"]
|
62
|
+
classes << "md:border-b md:border-border" if @border
|
63
|
+
|
64
|
+
# Add positioning classes
|
65
|
+
if @sticky
|
66
|
+
classes << "sticky top-0 z-50"
|
67
|
+
else
|
68
|
+
classes << "fixed top-0 left-0 right-0 z-50"
|
69
|
+
end
|
70
|
+
|
71
|
+
if @transparent
|
72
|
+
classes << "bg-transparent backdrop-blur-lg supports-[backdrop-filter]:bg-background/60"
|
73
|
+
elsif @blur
|
74
|
+
classes << "bg-transparent backdrop-blur-md"
|
75
|
+
else
|
76
|
+
classes << "bg-transparent"
|
77
|
+
end
|
78
|
+
|
79
|
+
classes.join(" ")
|
80
|
+
end
|
81
|
+
|
82
|
+
def container_classes
|
83
|
+
if @container
|
84
|
+
"container mx-auto px-4 sm:px-6 lg:px-8"
|
85
|
+
else
|
86
|
+
"w-full px-4 sm:px-6 lg:px-8"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def content_classes
|
91
|
+
height = case @size
|
92
|
+
when :sm then "h-12"
|
93
|
+
when :lg then "h-20"
|
94
|
+
else "h-16"
|
95
|
+
end
|
96
|
+
|
97
|
+
"flex #{height} items-center justify-between"
|
98
|
+
end
|
99
|
+
|
100
|
+
def render_brand_and_nav
|
101
|
+
tag.div(class: "flex items-center gap-6 lg:gap-8") do
|
102
|
+
safe_join([
|
103
|
+
brand,
|
104
|
+
navigation ? render_desktop_navigation : nil
|
105
|
+
].compact)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def render_desktop_navigation
|
110
|
+
tag.div(class: "hidden md:flex") do
|
111
|
+
navigation.to_s.html_safe
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def render_actions_with_mobile_menu
|
116
|
+
tag.div(class: "flex items-center gap-2") do
|
117
|
+
safe_join([
|
118
|
+
render_desktop_actions,
|
119
|
+
right_action,
|
120
|
+
render_mobile_menu_trigger
|
121
|
+
].compact)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def render_desktop_actions
|
126
|
+
return nil if @action_blocks.empty?
|
127
|
+
|
128
|
+
tag.div(class: "hidden md:flex items-center gap-2") do
|
129
|
+
@action_blocks.each { |block| concat(view_context.capture(&block)) }
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def render_mobile_menu_trigger
|
135
|
+
return nil unless @mobile_menu_block
|
136
|
+
|
137
|
+
tag.div(
|
138
|
+
data: {
|
139
|
+
controller: "m9sh--dropdown-menu",
|
140
|
+
m9sh__dropdown_menu_align_value: "end",
|
141
|
+
m9sh__dropdown_menu_side_value: "bottom"
|
142
|
+
},
|
143
|
+
class: "md:hidden relative inline-block text-left"
|
144
|
+
) do
|
145
|
+
safe_join([
|
146
|
+
render_mobile_menu_button,
|
147
|
+
render_mobile_menu_content
|
148
|
+
])
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def render_mobile_menu_button
|
153
|
+
render(M9sh::ButtonComponent.new(
|
154
|
+
variant: :ghost,
|
155
|
+
size: :icon,
|
156
|
+
type: "button",
|
157
|
+
data: {
|
158
|
+
action: "click->m9sh--dropdown-menu#toggle click@window->m9sh--dropdown-menu#hide",
|
159
|
+
m9sh__dropdown_menu_target: "trigger"
|
160
|
+
},
|
161
|
+
aria: { label: "Open menu" }
|
162
|
+
)) do
|
163
|
+
render(M9sh::IconComponent.new(name: "menu", size: "20"))
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def render_mobile_menu_content
|
168
|
+
tag.div(
|
169
|
+
data: {
|
170
|
+
m9sh__dropdown_menu_target: "content",
|
171
|
+
transition_enter: "transition ease-out duration-100",
|
172
|
+
transition_enter_start: "transform opacity-0 scale-95",
|
173
|
+
transition_enter_end: "transform opacity-100 scale-100",
|
174
|
+
transition_leave: "transition ease-in duration-75",
|
175
|
+
transition_leave_start: "transform opacity-100 scale-100",
|
176
|
+
transition_leave_end: "transform opacity-0 scale-95"
|
177
|
+
},
|
178
|
+
class: "absolute right-0 z-50 mt-2 w-[200px] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg hidden",
|
179
|
+
style: "position: absolute;"
|
180
|
+
) do
|
181
|
+
view_context.capture(&@mobile_menu_block)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
end
|
@@ -27,7 +27,7 @@ module M9sh
|
|
27
27
|
content = block ? block.call : text
|
28
28
|
tag.button(
|
29
29
|
type: "button",
|
30
|
-
class: "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent/50",
|
30
|
+
class: "group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:bg-accent/50",
|
31
31
|
data: {
|
32
32
|
action: "mouseenter->m9sh--navigation-menu#onTriggerEnter mouseleave->m9sh--navigation-menu#onTriggerLeave click->m9sh--navigation-menu#toggle",
|
33
33
|
m9sh__navigation_menu_target: "trigger",
|
@@ -66,7 +66,7 @@ module M9sh
|
|
66
66
|
}
|
67
67
|
) do
|
68
68
|
tag.div(
|
69
|
-
class: "relative mt-1.5 overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-
|
69
|
+
class: "relative mt-1.5 overflow-hidden rounded-lg border border-border bg-popover 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",
|
70
70
|
data: { state: "closed" }
|
71
71
|
) do
|
72
72
|
block.call
|
@@ -13,21 +13,26 @@ module M9sh
|
|
13
13
|
) { block.call }
|
14
14
|
}
|
15
15
|
|
16
|
+
def initialize(align: "center", side: "bottom", width: "w-72", **extra_attrs)
|
17
|
+
@align = align
|
18
|
+
@side = side
|
19
|
+
@width = width
|
20
|
+
super(**extra_attrs)
|
21
|
+
end
|
22
|
+
|
23
|
+
def before_render
|
24
|
+
@popover_content_classes = "fixed z-[9999] #{@width} rounded-lg border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none hidden 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"
|
25
|
+
end
|
26
|
+
|
16
27
|
renders_one :popover_content, lambda { |&block|
|
17
28
|
tag.div(
|
18
|
-
class:
|
29
|
+
class: @popover_content_classes,
|
19
30
|
data: {
|
20
31
|
m9sh__popover_target: "content"
|
21
32
|
}
|
22
33
|
) { block.call }
|
23
34
|
}
|
24
35
|
|
25
|
-
def initialize(align: "center", side: "bottom", **extra_attrs)
|
26
|
-
@align = align
|
27
|
-
@side = side
|
28
|
-
super(**extra_attrs)
|
29
|
-
end
|
30
|
-
|
31
36
|
def call
|
32
37
|
tag.div(
|
33
38
|
**component_attrs("relative inline-block"),
|
@@ -6,25 +6,48 @@ module M9sh
|
|
6
6
|
|
7
7
|
renders_many :items, "ItemComponent"
|
8
8
|
|
9
|
-
def initialize(name:, **extra_attrs)
|
9
|
+
def initialize(name:, default_value: nil, **extra_attrs)
|
10
10
|
@name = name
|
11
|
+
@default_value = default_value
|
11
12
|
super(**extra_attrs)
|
12
13
|
end
|
13
14
|
|
14
15
|
def call
|
15
16
|
tag.div(
|
16
17
|
**component_attrs("grid gap-2"),
|
17
|
-
role: "radiogroup"
|
18
|
+
role: "radiogroup",
|
19
|
+
data: {
|
20
|
+
controller: "m9sh--radio-group",
|
21
|
+
m9sh__radio_group_name_value: @name,
|
22
|
+
m9sh__radio_group_default_value_value: @default_value
|
23
|
+
}
|
18
24
|
) do
|
19
|
-
|
25
|
+
if items.any?
|
26
|
+
safe_join(items)
|
27
|
+
else
|
28
|
+
content
|
29
|
+
end
|
20
30
|
end
|
21
31
|
end
|
22
32
|
|
33
|
+
# Helper method to create radio group items
|
34
|
+
def item(value:, id:, **extra_attrs)
|
35
|
+
ItemComponent.new(
|
36
|
+
value: value,
|
37
|
+
id: id,
|
38
|
+
name: @name,
|
39
|
+
checked: @default_value == value,
|
40
|
+
**extra_attrs
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
23
44
|
class ItemComponent < BaseComponent
|
24
45
|
include Utilities
|
25
46
|
|
26
|
-
def initialize(value:, label
|
47
|
+
def initialize(value:, id: nil, name: nil, label: nil, description: nil, checked: false, disabled: false, **extra_attrs)
|
27
48
|
@value = value
|
49
|
+
@id = id || "radio-#{value}"
|
50
|
+
@name = name
|
28
51
|
@label = label
|
29
52
|
@description = description
|
30
53
|
@checked = checked
|
@@ -33,13 +56,19 @@ module M9sh
|
|
33
56
|
end
|
34
57
|
|
35
58
|
def call
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
59
|
+
# If label and description are provided, render the full component
|
60
|
+
if @label || @description
|
61
|
+
tag.div(
|
62
|
+
class: "flex items-center space-x-2"
|
63
|
+
) do
|
64
|
+
safe_join([
|
65
|
+
render_radio,
|
66
|
+
render_label_and_description
|
67
|
+
])
|
68
|
+
end
|
69
|
+
else
|
70
|
+
# Otherwise just render the radio button
|
71
|
+
render_radio
|
43
72
|
end
|
44
73
|
end
|
45
74
|
|
@@ -47,13 +76,15 @@ module M9sh
|
|
47
76
|
|
48
77
|
def render_radio
|
49
78
|
tag.button(
|
79
|
+
**component_attrs(radio_classes),
|
50
80
|
type: "button",
|
51
81
|
role: "radio",
|
52
|
-
|
82
|
+
id: @id,
|
53
83
|
data: {
|
54
84
|
controller: "m9sh--radio",
|
55
85
|
m9sh__radio_value_value: @value,
|
56
86
|
m9sh__radio_checked_value: @checked,
|
87
|
+
m9sh__radio_name_value: @name,
|
57
88
|
action: "click->m9sh--radio#toggle"
|
58
89
|
},
|
59
90
|
aria: {
|
@@ -71,8 +102,9 @@ module M9sh
|
|
71
102
|
def render_label_and_description
|
72
103
|
tag.div(class: "grid gap-1.5 leading-none") do
|
73
104
|
safe_join([
|
74
|
-
tag.label(
|
105
|
+
@label && tag.label(
|
75
106
|
@label,
|
107
|
+
for: @id,
|
76
108
|
class: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
77
109
|
),
|
78
110
|
@description && tag.p(@description, class: "text-sm text-muted-foreground")
|
@@ -47,7 +47,7 @@ module M9sh
|
|
47
47
|
|
48
48
|
def render_overlay
|
49
49
|
tag.div(
|
50
|
-
class: "fixed inset-0 z-
|
50
|
+
class: "fixed inset-0 z-[100] bg-black/50 hidden data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
51
51
|
data: {
|
52
52
|
m9sh__sheet_target: "overlay",
|
53
53
|
action: "click->m9sh--sheet#close"
|
@@ -71,17 +71,17 @@ module M9sh
|
|
71
71
|
end
|
72
72
|
|
73
73
|
def panel_classes
|
74
|
-
base = "fixed z-
|
74
|
+
base = "fixed z-[100] bg-background shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 hidden flex flex-col"
|
75
75
|
|
76
76
|
case @side
|
77
77
|
when "top"
|
78
|
-
"#{base} inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top"
|
78
|
+
"#{base} inset-x-0 top-0 border-b border-border data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top"
|
79
79
|
when "bottom"
|
80
|
-
"#{base} inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
|
80
|
+
"#{base} inset-x-0 bottom-0 border-t border-border data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom"
|
81
81
|
when "left"
|
82
|
-
"#{base} inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm"
|
82
|
+
"#{base} inset-y-0 left-0 h-full w-3/4 border-r border-border data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm"
|
83
83
|
when "right"
|
84
|
-
"#{base} inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-
|
84
|
+
"#{base} inset-y-0 right-0 h-full w-3/4 border-l border-border data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-5xl"
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
@@ -74,7 +74,12 @@ module M9sh
|
|
74
74
|
when :icon
|
75
75
|
"transition-all duration-300 data-[state=collapsed]:w-16"
|
76
76
|
when :offcanvas
|
77
|
-
|
77
|
+
# Fixed positioning only on mobile, flex layout on desktop
|
78
|
+
if @side == :left
|
79
|
+
"md:relative md:translate-x-0 fixed inset-y-0 z-50 transition-transform duration-300 data-[state=collapsed]:-translate-x-full data-[state=collapsed]:md:translate-x-0"
|
80
|
+
else
|
81
|
+
"md:relative md:translate-x-0 fixed inset-y-0 z-50 transition-transform duration-300 data-[state=collapsed]:translate-x-full data-[state=collapsed]:md:translate-x-0"
|
82
|
+
end
|
78
83
|
else
|
79
84
|
""
|
80
85
|
end
|
@@ -11,12 +11,18 @@ module M9sh
|
|
11
11
|
|
12
12
|
def call
|
13
13
|
tag.div(
|
14
|
-
**component_attrs(class_names("
|
14
|
+
**component_attrs(class_names("relative overflow-hidden", skeleton_classes, rounded_classes)),
|
15
|
+
style: "background-color: rgb(228 228 231); animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;"
|
15
16
|
)
|
16
17
|
end
|
17
18
|
|
18
19
|
private
|
19
20
|
|
21
|
+
def skeleton_classes
|
22
|
+
# Add a shimmer effect using pseudo-element
|
23
|
+
"before:absolute before:inset-0 before:-translate-x-full before:animate-shimmer before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent"
|
24
|
+
end
|
25
|
+
|
20
26
|
def rounded_classes
|
21
27
|
case @rounded
|
22
28
|
when :none then "rounded-none"
|
@@ -4,7 +4,8 @@ module M9sh
|
|
4
4
|
class TabsComponent < BaseComponent
|
5
5
|
include Utilities
|
6
6
|
|
7
|
-
|
7
|
+
renders_one :tabs_list, "TabsListComponent"
|
8
|
+
renders_many :tabs_contents, "TabsContentComponent"
|
8
9
|
|
9
10
|
def initialize(default_value: nil, **extra_attrs)
|
10
11
|
@default_value = default_value
|
@@ -13,80 +14,107 @@ module M9sh
|
|
13
14
|
|
14
15
|
def call
|
15
16
|
tag.div(
|
16
|
-
**component_attrs(
|
17
|
+
**component_attrs("w-full"),
|
17
18
|
data: {
|
18
19
|
controller: "m9sh--tabs",
|
19
20
|
m9sh__tabs_default_value: @default_value
|
20
21
|
}
|
21
22
|
) do
|
22
23
|
safe_join([
|
23
|
-
|
24
|
-
|
25
|
-
])
|
24
|
+
tabs_list,
|
25
|
+
safe_join(tabs_contents)
|
26
|
+
].compact)
|
26
27
|
end
|
27
28
|
end
|
28
29
|
|
29
|
-
|
30
|
+
# Nested TabsList component
|
31
|
+
class TabsListComponent < BaseComponent
|
32
|
+
include Utilities
|
30
33
|
|
31
|
-
|
32
|
-
"w-full"
|
33
|
-
end
|
34
|
+
renders_many :triggers, "TabsTriggerComponent"
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
36
|
+
def initialize(**extra_attrs)
|
37
|
+
super(**extra_attrs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def call
|
41
|
+
tag.div(
|
42
|
+
**component_attrs("inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground"),
|
43
|
+
role: "tablist",
|
44
|
+
data: { m9sh__tabs_target: "list" }
|
45
|
+
) do
|
46
|
+
if triggers.any?
|
47
|
+
safe_join(triggers)
|
48
|
+
else
|
49
|
+
content
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Helper method to create trigger
|
55
|
+
def tabs_trigger(value:, **extra_attrs, &block)
|
56
|
+
TabsTriggerComponent.new(value: value, **extra_attrs).tap do |trigger|
|
57
|
+
trigger.instance_variable_set(:@content_block, block)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Nested TabsTrigger component
|
62
|
+
class TabsTriggerComponent < BaseComponent
|
63
|
+
include Utilities
|
64
|
+
|
65
|
+
def initialize(value:, **extra_attrs)
|
66
|
+
@value = value
|
67
|
+
super(**extra_attrs)
|
68
|
+
end
|
69
|
+
|
70
|
+
def call
|
42
71
|
tag.button(
|
43
|
-
|
72
|
+
**component_attrs(trigger_classes),
|
44
73
|
type: "button",
|
45
74
|
role: "tab",
|
46
|
-
class: "inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
47
75
|
data: {
|
48
76
|
m9sh__tabs_target: "trigger",
|
49
77
|
action: "click->m9sh--tabs#selectTab",
|
50
|
-
value:
|
51
|
-
}
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
78
|
+
value: @value
|
79
|
+
}
|
80
|
+
) do
|
81
|
+
if @content_block
|
82
|
+
@content_block.call
|
83
|
+
else
|
84
|
+
content
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
57
88
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
role: "tabpanel",
|
64
|
-
class: "ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
65
|
-
data: {
|
66
|
-
m9sh__tabs_target: "panel",
|
67
|
-
value: tab.value
|
68
|
-
},
|
69
|
-
style: @default_value == tab.value ? "" : "display: none;"
|
70
|
-
)
|
71
|
-
end.join.html_safe
|
89
|
+
private
|
90
|
+
|
91
|
+
def trigger_classes
|
92
|
+
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow"
|
93
|
+
end
|
72
94
|
end
|
73
95
|
end
|
74
96
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
renders_one :panel
|
97
|
+
# Nested TabsContent component
|
98
|
+
class TabsContentComponent < BaseComponent
|
99
|
+
include Utilities
|
79
100
|
|
80
|
-
def initialize(value:,
|
101
|
+
def initialize(value:, **extra_attrs)
|
81
102
|
@value = value
|
82
|
-
@label = label
|
83
103
|
super(**extra_attrs)
|
84
104
|
end
|
85
105
|
|
86
106
|
def call
|
87
|
-
|
88
|
-
|
107
|
+
tag.div(
|
108
|
+
**component_attrs("mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"),
|
109
|
+
role: "tabpanel",
|
110
|
+
data: {
|
111
|
+
m9sh__tabs_target: "panel",
|
112
|
+
value: @value
|
113
|
+
}
|
114
|
+
) do
|
115
|
+
content
|
116
|
+
end
|
89
117
|
end
|
90
118
|
end
|
91
119
|
end
|
92
|
-
end
|
120
|
+
end
|
@@ -38,7 +38,7 @@ module M9sh
|
|
38
38
|
private
|
39
39
|
|
40
40
|
def base_classes
|
41
|
-
"
|
41
|
+
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|