m9sh 0.1.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 +7 -0
- data/.do/app.yaml +25 -0
- data/.dockerignore +51 -0
- data/.idea/.gitignore +8 -0
- data/.idea/aws.xml +17 -0
- data/.idea/hotcdn.iml +189 -0
- data/.idea/jsLibraryMappings.xml +6 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.mise.toml +3 -0
- data/.node-version +1 -0
- data/Dockerfile +84 -0
- data/README.md +230 -0
- data/Rakefile +6 -0
- data/app/components/m9sh/accordion_component.rb +122 -0
- data/app/components/m9sh/alert_component.rb +72 -0
- data/app/components/m9sh/alert_dialog_component.rb +100 -0
- data/app/components/m9sh/avatar_component.rb +65 -0
- data/app/components/m9sh/badge_component.rb +37 -0
- data/app/components/m9sh/base_component.rb +21 -0
- data/app/components/m9sh/breadcrumb_component.rb +100 -0
- data/app/components/m9sh/button_component.rb +54 -0
- data/app/components/m9sh/card_component.rb +90 -0
- data/app/components/m9sh/checkbox_component.rb +36 -0
- data/app/components/m9sh/collapsible_component.rb +47 -0
- data/app/components/m9sh/dialog_component.rb +123 -0
- data/app/components/m9sh/dropdown_menu_component.rb +27 -0
- data/app/components/m9sh/dropdown_menu_content_component.rb +24 -0
- data/app/components/m9sh/dropdown_menu_item_component.rb +36 -0
- data/app/components/m9sh/dropdown_menu_separator_component.rb +9 -0
- data/app/components/m9sh/dropdown_menu_trigger_component.rb +19 -0
- data/app/components/m9sh/hover_card_component.rb +48 -0
- data/app/components/m9sh/input_component.rb +33 -0
- data/app/components/m9sh/label_component.rb +27 -0
- data/app/components/m9sh/main_component.rb +16 -0
- data/app/components/m9sh/navigation_menu_component.rb +95 -0
- data/app/components/m9sh/popover_component.rb +47 -0
- data/app/components/m9sh/progress_component.rb +46 -0
- data/app/components/m9sh/radio_group_component.rb +88 -0
- data/app/components/m9sh/select_component.rb +51 -0
- data/app/components/m9sh/separator_component.rb +40 -0
- data/app/components/m9sh/sheet_component.rb +123 -0
- data/app/components/m9sh/sidebar_component.rb +126 -0
- data/app/components/m9sh/sidebar_group_component.rb +51 -0
- data/app/components/m9sh/sidebar_inset_component.rb +16 -0
- data/app/components/m9sh/sidebar_menu_button_component.rb +56 -0
- data/app/components/m9sh/sidebar_menu_component.rb +16 -0
- data/app/components/m9sh/sidebar_menu_item_component.rb +16 -0
- data/app/components/m9sh/sidebar_provider_component.rb +29 -0
- data/app/components/m9sh/sidebar_trigger_component.rb +44 -0
- data/app/components/m9sh/skeleton_component.rb +32 -0
- data/app/components/m9sh/slider_component.rb +83 -0
- data/app/components/m9sh/spinner_component.rb +46 -0
- data/app/components/m9sh/switch_component.rb +47 -0
- data/app/components/m9sh/table_component.rb +111 -0
- data/app/components/m9sh/tabs_component.rb +92 -0
- data/app/components/m9sh/textarea_component.rb +44 -0
- data/app/components/m9sh/theme_toggle_component.rb +88 -0
- data/app/components/m9sh/toast_component.rb +86 -0
- data/app/components/m9sh/toaster_component.rb +20 -0
- data/app/components/m9sh/toggle_component.rb +64 -0
- data/app/components/m9sh/tooltip_component.rb +48 -0
- data/app/components/m9sh/typography_component.rb +56 -0
- data/app/components/m9sh/utilities.rb +26 -0
- data/app/javascript/controllers/m9sh/accordion_controller.js +110 -0
- data/app/javascript/controllers/m9sh/alert_dialog_controller.js +47 -0
- data/app/javascript/controllers/m9sh/collapsible_controller.js +57 -0
- data/app/javascript/controllers/m9sh/dialog_controller.js +119 -0
- data/app/javascript/controllers/m9sh/dropdown_menu_controller.js +103 -0
- data/app/javascript/controllers/m9sh/hover_card_controller.js +66 -0
- data/app/javascript/controllers/m9sh/navigation_menu_controller.js +219 -0
- data/app/javascript/controllers/m9sh/popover_controller.js +113 -0
- data/app/javascript/controllers/m9sh/radio_controller.js +59 -0
- data/app/javascript/controllers/m9sh/sheet_controller.js +46 -0
- data/app/javascript/controllers/m9sh/sidebar_controller.js +114 -0
- data/app/javascript/controllers/m9sh/sidebar_provider_controller.js +12 -0
- data/app/javascript/controllers/m9sh/slider_controller.js +90 -0
- data/app/javascript/controllers/m9sh/switch_controller.js +33 -0
- data/app/javascript/controllers/m9sh/tabs_controller.js +51 -0
- data/app/javascript/controllers/m9sh/theme_controller.js +50 -0
- data/app/javascript/controllers/m9sh/toast_controller.js +46 -0
- data/app/javascript/controllers/m9sh/toaster_controller.js +70 -0
- data/app/javascript/controllers/m9sh/toggle_controller.js +27 -0
- data/app/javascript/controllers/m9sh/tooltip_controller.js +86 -0
- data/components.json +21 -0
- data/config.ru +6 -0
- data/exe/m9sh +12 -0
- data/fix_namespaces.py +32 -0
- data/fly.toml +30 -0
- data/koyeb.yaml +26 -0
- data/lib/m9sh/cli.rb +234 -0
- data/lib/m9sh/config.rb +114 -0
- data/lib/m9sh/generator.rb +183 -0
- data/lib/m9sh/registry.rb +107 -0
- data/lib/m9sh/registry.yml +384 -0
- data/lib/m9sh/version.rb +5 -0
- data/lib/m9sh.rb +11 -0
- data/package-lock.json +99 -0
- data/package.json +28 -0
- data/pnpm-lock.yaml +75 -0
- data/tailwind.config.js +93 -0
- data/update_namespace.py +73 -0
- metadata +208 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class AccordionComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
renders_many :items, "ItemComponent"
|
|
8
|
+
|
|
9
|
+
def initialize(type: "single", collapsible: true, default_value: nil, **extra_attrs)
|
|
10
|
+
@type = type
|
|
11
|
+
@collapsible = collapsible
|
|
12
|
+
@default_value = default_value
|
|
13
|
+
super(**extra_attrs)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
tag.div(
|
|
18
|
+
class: cn("w-full", @class_name),
|
|
19
|
+
data: {
|
|
20
|
+
controller: "m9sh--accordion",
|
|
21
|
+
m9sh__accordion_type_value: @type,
|
|
22
|
+
m9sh__accordion_collapsible_value: @collapsible,
|
|
23
|
+
m9sh__accordion_default_value: @default_value
|
|
24
|
+
}.merge(@extra_attrs.fetch(:data, {}))
|
|
25
|
+
) do
|
|
26
|
+
safe_join(items)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ItemComponent < BaseComponent
|
|
31
|
+
include Utilities
|
|
32
|
+
|
|
33
|
+
renders_one :trigger
|
|
34
|
+
renders_one :body
|
|
35
|
+
|
|
36
|
+
def initialize(value:, **extra_attrs)
|
|
37
|
+
@value = value
|
|
38
|
+
super(**extra_attrs)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call
|
|
42
|
+
tag.div(
|
|
43
|
+
**component_attrs(item_classes),
|
|
44
|
+
data: {
|
|
45
|
+
m9sh__accordion_target: "item",
|
|
46
|
+
value: @value
|
|
47
|
+
}
|
|
48
|
+
) do
|
|
49
|
+
safe_join([
|
|
50
|
+
render_trigger,
|
|
51
|
+
render_content
|
|
52
|
+
])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def item_classes
|
|
59
|
+
"border-b"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_trigger
|
|
63
|
+
return "" unless trigger
|
|
64
|
+
|
|
65
|
+
tag.h3(class: "flex", data: { m9sh__accordion_target: "trigger" }) do
|
|
66
|
+
tag.button(
|
|
67
|
+
class: trigger_classes,
|
|
68
|
+
type: "button",
|
|
69
|
+
data: {
|
|
70
|
+
action: "click->m9sh--accordion#toggle",
|
|
71
|
+
m9sh__accordion_target: "button"
|
|
72
|
+
}
|
|
73
|
+
) do
|
|
74
|
+
safe_join([
|
|
75
|
+
tag.span(trigger),
|
|
76
|
+
chevron_icon
|
|
77
|
+
])
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_content
|
|
83
|
+
return "" unless body
|
|
84
|
+
|
|
85
|
+
tag.div(
|
|
86
|
+
class: content_classes,
|
|
87
|
+
data: {
|
|
88
|
+
m9sh__accordion_target: "content"
|
|
89
|
+
},
|
|
90
|
+
style: "height: 0; display: block;"
|
|
91
|
+
) do
|
|
92
|
+
tag.div(body, class: "pb-4 pt-0")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def trigger_classes
|
|
97
|
+
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[aria-expanded=true]>svg]:rotate-180"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def content_classes
|
|
101
|
+
"overflow-hidden text-sm transition-all duration-300 ease-in-out"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def chevron_icon
|
|
105
|
+
tag.svg(
|
|
106
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
107
|
+
width: "16",
|
|
108
|
+
height: "16",
|
|
109
|
+
viewBox: "0 0 24 24",
|
|
110
|
+
fill: "none",
|
|
111
|
+
stroke: "currentColor",
|
|
112
|
+
stroke_width: "2",
|
|
113
|
+
stroke_linecap: "round",
|
|
114
|
+
stroke_linejoin: "round",
|
|
115
|
+
class: "h-4 w-4 shrink-0 transition-transform duration-200"
|
|
116
|
+
) do
|
|
117
|
+
tag.path(d: "m6 9 6 6 6-6")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class AlertComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
VARIANTS = {
|
|
8
|
+
default: "bg-card text-card-foreground",
|
|
9
|
+
destructive: "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90"
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
renders_one :icon
|
|
13
|
+
renders_one :title
|
|
14
|
+
renders_one :description
|
|
15
|
+
|
|
16
|
+
def initialize(variant: :default, **extra_attrs)
|
|
17
|
+
@variant = variant.to_sym
|
|
18
|
+
super(**extra_attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
tag.div(
|
|
23
|
+
**component_attrs(class_names(base_classes, variant_classes)),
|
|
24
|
+
role: "alert",
|
|
25
|
+
data: { slot: "alert" }
|
|
26
|
+
) do
|
|
27
|
+
safe_join([
|
|
28
|
+
render_icon,
|
|
29
|
+
render_title,
|
|
30
|
+
render_description,
|
|
31
|
+
content
|
|
32
|
+
].compact)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def base_classes
|
|
39
|
+
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[1rem_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def variant_classes
|
|
43
|
+
VARIANTS[@variant] || VARIANTS[:default]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render_icon
|
|
47
|
+
return unless icon?
|
|
48
|
+
|
|
49
|
+
tag.div(class: "col-start-1") { icon }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def render_title
|
|
53
|
+
return unless title?
|
|
54
|
+
|
|
55
|
+
tag.div(
|
|
56
|
+
title,
|
|
57
|
+
class: "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight text-foreground",
|
|
58
|
+
data: { slot: "alert-title" }
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_description
|
|
63
|
+
return unless description?
|
|
64
|
+
|
|
65
|
+
tag.div(
|
|
66
|
+
description,
|
|
67
|
+
class: "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
|
68
|
+
data: { slot: "alert-description" }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class AlertDialogComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
renders_one :trigger, lambda { |&block|
|
|
8
|
+
tag.div(
|
|
9
|
+
data: { action: "click->m9sh--alert-dialog#open" }
|
|
10
|
+
) { block.call }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
renders_one :title, lambda { |&block|
|
|
14
|
+
tag.h2(class: "text-lg font-semibold") { block.call }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
renders_one :description, lambda { |&block|
|
|
18
|
+
tag.p(class: "text-sm text-muted-foreground") { block.call }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
renders_one :alert_content, lambda { |&block|
|
|
22
|
+
tag.div { block.call }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
renders_one :footer, lambda { |&block|
|
|
26
|
+
tag.div(
|
|
27
|
+
class: "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2"
|
|
28
|
+
) { block.call }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def initialize(**extra_attrs)
|
|
32
|
+
super(**extra_attrs)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
tag.div(
|
|
37
|
+
**component_attrs(""),
|
|
38
|
+
data: { controller: "m9sh--alert-dialog" }
|
|
39
|
+
) do
|
|
40
|
+
safe_join([
|
|
41
|
+
trigger,
|
|
42
|
+
render_overlay,
|
|
43
|
+
render_dialog_content
|
|
44
|
+
].compact)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def render_overlay
|
|
51
|
+
tag.div(
|
|
52
|
+
class: "fixed inset-0 z-50 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",
|
|
53
|
+
data: {
|
|
54
|
+
m9sh__alert_dialog_target: "overlay",
|
|
55
|
+
action: "click->m9sh--alert-dialog#close"
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def render_dialog_content
|
|
61
|
+
tag.div(
|
|
62
|
+
class: dialog_content_classes,
|
|
63
|
+
data: {
|
|
64
|
+
m9sh__alert_dialog_target: "content",
|
|
65
|
+
action: "click->m9sh--alert-dialog#stopPropagation"
|
|
66
|
+
},
|
|
67
|
+
role: "alertdialog"
|
|
68
|
+
) do
|
|
69
|
+
safe_join([
|
|
70
|
+
render_header,
|
|
71
|
+
render_body,
|
|
72
|
+
render_footer_section
|
|
73
|
+
].compact)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def dialog_content_classes
|
|
78
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def render_header
|
|
82
|
+
return unless title? || description?
|
|
83
|
+
|
|
84
|
+
tag.div(class: "flex flex-col space-y-2 text-center sm:text-left") do
|
|
85
|
+
safe_join([
|
|
86
|
+
title,
|
|
87
|
+
description
|
|
88
|
+
].compact)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def render_body
|
|
93
|
+
alert_content
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def render_footer_section
|
|
97
|
+
footer
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class AvatarComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
def initialize(src: nil, alt: nil, fallback: nil, size: :md, **extra_attrs)
|
|
8
|
+
@src = src
|
|
9
|
+
@alt = alt
|
|
10
|
+
@fallback = fallback
|
|
11
|
+
@size = size.to_sym
|
|
12
|
+
super(**extra_attrs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
tag.div(
|
|
17
|
+
**component_attrs(class_names(base_classes, size_classes)),
|
|
18
|
+
data: { slot: "avatar" }
|
|
19
|
+
) do
|
|
20
|
+
safe_join([
|
|
21
|
+
render_image,
|
|
22
|
+
render_fallback
|
|
23
|
+
].compact)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
SIZES = {
|
|
30
|
+
sm: "size-6",
|
|
31
|
+
md: "size-8",
|
|
32
|
+
lg: "size-10",
|
|
33
|
+
xl: "size-12"
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
def base_classes
|
|
37
|
+
"relative flex shrink-0 overflow-hidden rounded-full"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def size_classes
|
|
41
|
+
SIZES[@size] || SIZES[:md]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_image
|
|
45
|
+
return unless @src
|
|
46
|
+
|
|
47
|
+
tag.img(
|
|
48
|
+
src: @src,
|
|
49
|
+
alt: @alt || "",
|
|
50
|
+
class: "aspect-square size-full object-cover",
|
|
51
|
+
data: { slot: "avatar-image" }
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_fallback
|
|
56
|
+
return unless @fallback
|
|
57
|
+
|
|
58
|
+
tag.div(
|
|
59
|
+
@fallback,
|
|
60
|
+
class: "bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm font-medium",
|
|
61
|
+
data: { slot: "avatar-fallback" }
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class BadgeComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
VARIANTS = {
|
|
8
|
+
default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
|
9
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
10
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
|
11
|
+
outline: "border-border text-foreground",
|
|
12
|
+
success: "border-transparent bg-green-500/10 text-green-700 hover:bg-green-500/20 dark:text-green-400",
|
|
13
|
+
warning: "border-transparent bg-amber-500/10 text-amber-700 hover:bg-amber-500/20 dark:text-amber-400"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize(variant: :default, **extra_attrs)
|
|
17
|
+
@variant = variant.to_sym
|
|
18
|
+
super(**extra_attrs)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call
|
|
22
|
+
tag.span(
|
|
23
|
+
**component_attrs(class_names(base_classes, variant_classes))
|
|
24
|
+
) { content }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def base_classes
|
|
30
|
+
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def variant_classes
|
|
34
|
+
VARIANTS[@variant] || VARIANTS[:default]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class BaseComponent < ViewComponent::Base
|
|
5
|
+
def initialize(class_name: nil, **extra_attrs)
|
|
6
|
+
@class_name = class_name
|
|
7
|
+
@extra_attrs = extra_attrs
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def class_names(*args)
|
|
13
|
+
args.compact.join(" ")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def component_attrs(default_classes, custom_classes = nil)
|
|
17
|
+
classes = class_names(default_classes, custom_classes, @class_name)
|
|
18
|
+
@extra_attrs.merge(class: classes)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class BreadcrumbComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
renders_many :items, "ItemComponent"
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
tag.nav(
|
|
11
|
+
**component_attrs(""),
|
|
12
|
+
"aria-label": "breadcrumb",
|
|
13
|
+
data: { slot: "breadcrumb" }
|
|
14
|
+
) do
|
|
15
|
+
tag.ol(
|
|
16
|
+
class: "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
|
17
|
+
data: { slot: "breadcrumb-list" }
|
|
18
|
+
) do
|
|
19
|
+
safe_join(items.map(&:to_s))
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ItemComponent < BaseComponent
|
|
25
|
+
include Utilities
|
|
26
|
+
|
|
27
|
+
renders_one :link
|
|
28
|
+
renders_one :page
|
|
29
|
+
|
|
30
|
+
def initialize(current: false, **extra_attrs)
|
|
31
|
+
@current = current
|
|
32
|
+
super(**extra_attrs)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
tag.li(
|
|
37
|
+
**component_attrs("inline-flex items-center gap-1.5"),
|
|
38
|
+
data: { slot: "breadcrumb-item" }
|
|
39
|
+
) do
|
|
40
|
+
safe_join([
|
|
41
|
+
render_content,
|
|
42
|
+
render_separator
|
|
43
|
+
].compact)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def render_content
|
|
50
|
+
if @current
|
|
51
|
+
tag.span(
|
|
52
|
+
page || content,
|
|
53
|
+
class: "text-foreground font-normal",
|
|
54
|
+
role: "link",
|
|
55
|
+
"aria-disabled": "true",
|
|
56
|
+
"aria-current": "page",
|
|
57
|
+
data: { slot: "breadcrumb-page" }
|
|
58
|
+
)
|
|
59
|
+
elsif link?
|
|
60
|
+
tag.a(
|
|
61
|
+
link,
|
|
62
|
+
class: "hover:text-foreground transition-colors",
|
|
63
|
+
data: { slot: "breadcrumb-link" }
|
|
64
|
+
)
|
|
65
|
+
else
|
|
66
|
+
content
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_separator
|
|
71
|
+
return if @current
|
|
72
|
+
|
|
73
|
+
tag.li(
|
|
74
|
+
role: "presentation",
|
|
75
|
+
"aria-hidden": "true",
|
|
76
|
+
class: "[&>svg]:size-3.5",
|
|
77
|
+
data: { slot: "breadcrumb-separator" }
|
|
78
|
+
) do
|
|
79
|
+
chevron_right_icon
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def chevron_right_icon
|
|
84
|
+
tag.svg(
|
|
85
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
86
|
+
width: "16",
|
|
87
|
+
height: "16",
|
|
88
|
+
viewBox: "0 0 24 24",
|
|
89
|
+
fill: "none",
|
|
90
|
+
stroke: "currentColor",
|
|
91
|
+
stroke_width: "2",
|
|
92
|
+
stroke_linecap: "round",
|
|
93
|
+
stroke_linejoin: "round"
|
|
94
|
+
) do
|
|
95
|
+
tag.polyline(points: "9 18 15 12 9 6")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class ButtonComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
VARIANTS = {
|
|
8
|
+
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
9
|
+
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
10
|
+
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
11
|
+
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
12
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
13
|
+
link: "text-primary underline-offset-4 hover:underline"
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
SIZES = {
|
|
17
|
+
default: "h-10 px-4 py-2",
|
|
18
|
+
sm: "h-9 rounded-md px-3",
|
|
19
|
+
lg: "h-11 rounded-md px-8",
|
|
20
|
+
icon: "h-10 w-10"
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def initialize(variant: :default, size: :default, type: "button", disabled: false, **extra_attrs)
|
|
24
|
+
@variant = variant.to_sym
|
|
25
|
+
@size = size.to_sym
|
|
26
|
+
@type = type
|
|
27
|
+
@disabled = disabled
|
|
28
|
+
super(**extra_attrs)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
tag.button(
|
|
33
|
+
content,
|
|
34
|
+
**component_attrs(class_names(base_classes, variant_classes, size_classes)),
|
|
35
|
+
type: @type,
|
|
36
|
+
disabled: @disabled
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def base_classes
|
|
43
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def variant_classes
|
|
47
|
+
VARIANTS[@variant] || VARIANTS[:default]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def size_classes
|
|
51
|
+
SIZES[@size] || SIZES[:md]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class CardComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
renders_one :header, "HeaderComponent"
|
|
8
|
+
renders_one :footer, lambda { |&block|
|
|
9
|
+
tag.div(
|
|
10
|
+
class: "flex items-center px-6 [.border-t]:pt-6",
|
|
11
|
+
data: { slot: "card-footer" }
|
|
12
|
+
) { block.call }
|
|
13
|
+
}
|
|
14
|
+
renders_one :body, lambda { |&block|
|
|
15
|
+
tag.div(
|
|
16
|
+
class: "px-6",
|
|
17
|
+
data: { slot: "card-content" }
|
|
18
|
+
) { block.call }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
def initialize(**extra_attrs)
|
|
22
|
+
super(**extra_attrs)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def call
|
|
26
|
+
tag.div(
|
|
27
|
+
**component_attrs(base_classes),
|
|
28
|
+
data: { slot: "card" }
|
|
29
|
+
) do
|
|
30
|
+
safe_join([
|
|
31
|
+
header,
|
|
32
|
+
body,
|
|
33
|
+
footer
|
|
34
|
+
].compact)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def base_classes
|
|
41
|
+
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border border-border py-6 shadow-sm transition-all duration-200 hover:border-primary/30 hover:shadow-lg dark:hover:shadow-black/20"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class HeaderComponent < BaseComponent
|
|
45
|
+
include Utilities
|
|
46
|
+
|
|
47
|
+
renders_one :title, lambda { |**attrs, &block|
|
|
48
|
+
extra_class = attrs[:class] || ""
|
|
49
|
+
tag.div(
|
|
50
|
+
class: "text-lg font-semibold tracking-tight col-start-1 row-start-1 #{extra_class}",
|
|
51
|
+
data: { slot: "card-title" }
|
|
52
|
+
) { block.call }
|
|
53
|
+
}
|
|
54
|
+
renders_one :description, lambda { |**attrs, &block|
|
|
55
|
+
extra_class = attrs[:class] || ""
|
|
56
|
+
tag.div(
|
|
57
|
+
class: "text-muted-foreground text-sm col-start-1 row-start-2 #{extra_class}",
|
|
58
|
+
data: { slot: "card-description" }
|
|
59
|
+
) { block.call }
|
|
60
|
+
}
|
|
61
|
+
renders_one :action, lambda { |**attrs, &block|
|
|
62
|
+
extra_class = attrs[:class] || ""
|
|
63
|
+
tag.div(
|
|
64
|
+
class: "col-start-2 row-span-2 row-start-1 self-start justify-self-end #{extra_class}",
|
|
65
|
+
data: { slot: "card-action" }
|
|
66
|
+
) { block.call }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def call
|
|
70
|
+
tag.div(
|
|
71
|
+
**component_attrs(header_classes),
|
|
72
|
+
data: { slot: "card-header" }
|
|
73
|
+
) do
|
|
74
|
+
safe_join([
|
|
75
|
+
title,
|
|
76
|
+
description,
|
|
77
|
+
action,
|
|
78
|
+
content
|
|
79
|
+
].compact)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def header_classes
|
|
86
|
+
"grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-[>[data-slot=card-action]]:grid-cols-[1fr_auto] [.border-b]:pb-6"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module M9sh
|
|
4
|
+
class CheckboxComponent < BaseComponent
|
|
5
|
+
include Utilities
|
|
6
|
+
|
|
7
|
+
def initialize(name: nil, checked: false, value: "1", disabled: false, **extra_attrs)
|
|
8
|
+
@name = name
|
|
9
|
+
@checked = checked
|
|
10
|
+
@value = value
|
|
11
|
+
@disabled = disabled
|
|
12
|
+
super(**extra_attrs)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call
|
|
16
|
+
tag.input(
|
|
17
|
+
**component_attrs(base_classes),
|
|
18
|
+
type: "checkbox",
|
|
19
|
+
name: @name,
|
|
20
|
+
value: @value,
|
|
21
|
+
checked: @checked,
|
|
22
|
+
disabled: @disabled,
|
|
23
|
+
data: {
|
|
24
|
+
controller: "m9sh--checkbox",
|
|
25
|
+
action: "change->m9sh--checkbox#toggle"
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def base_classes
|
|
33
|
+
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|