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.
Files changed (104) hide show
  1. checksums.yaml +7 -0
  2. data/.do/app.yaml +25 -0
  3. data/.dockerignore +51 -0
  4. data/.idea/.gitignore +8 -0
  5. data/.idea/aws.xml +17 -0
  6. data/.idea/hotcdn.iml +189 -0
  7. data/.idea/jsLibraryMappings.xml +6 -0
  8. data/.idea/misc.xml +4 -0
  9. data/.idea/modules.xml +8 -0
  10. data/.idea/vcs.xml +6 -0
  11. data/.mise.toml +3 -0
  12. data/.node-version +1 -0
  13. data/Dockerfile +84 -0
  14. data/README.md +230 -0
  15. data/Rakefile +6 -0
  16. data/app/components/m9sh/accordion_component.rb +122 -0
  17. data/app/components/m9sh/alert_component.rb +72 -0
  18. data/app/components/m9sh/alert_dialog_component.rb +100 -0
  19. data/app/components/m9sh/avatar_component.rb +65 -0
  20. data/app/components/m9sh/badge_component.rb +37 -0
  21. data/app/components/m9sh/base_component.rb +21 -0
  22. data/app/components/m9sh/breadcrumb_component.rb +100 -0
  23. data/app/components/m9sh/button_component.rb +54 -0
  24. data/app/components/m9sh/card_component.rb +90 -0
  25. data/app/components/m9sh/checkbox_component.rb +36 -0
  26. data/app/components/m9sh/collapsible_component.rb +47 -0
  27. data/app/components/m9sh/dialog_component.rb +123 -0
  28. data/app/components/m9sh/dropdown_menu_component.rb +27 -0
  29. data/app/components/m9sh/dropdown_menu_content_component.rb +24 -0
  30. data/app/components/m9sh/dropdown_menu_item_component.rb +36 -0
  31. data/app/components/m9sh/dropdown_menu_separator_component.rb +9 -0
  32. data/app/components/m9sh/dropdown_menu_trigger_component.rb +19 -0
  33. data/app/components/m9sh/hover_card_component.rb +48 -0
  34. data/app/components/m9sh/input_component.rb +33 -0
  35. data/app/components/m9sh/label_component.rb +27 -0
  36. data/app/components/m9sh/main_component.rb +16 -0
  37. data/app/components/m9sh/navigation_menu_component.rb +95 -0
  38. data/app/components/m9sh/popover_component.rb +47 -0
  39. data/app/components/m9sh/progress_component.rb +46 -0
  40. data/app/components/m9sh/radio_group_component.rb +88 -0
  41. data/app/components/m9sh/select_component.rb +51 -0
  42. data/app/components/m9sh/separator_component.rb +40 -0
  43. data/app/components/m9sh/sheet_component.rb +123 -0
  44. data/app/components/m9sh/sidebar_component.rb +126 -0
  45. data/app/components/m9sh/sidebar_group_component.rb +51 -0
  46. data/app/components/m9sh/sidebar_inset_component.rb +16 -0
  47. data/app/components/m9sh/sidebar_menu_button_component.rb +56 -0
  48. data/app/components/m9sh/sidebar_menu_component.rb +16 -0
  49. data/app/components/m9sh/sidebar_menu_item_component.rb +16 -0
  50. data/app/components/m9sh/sidebar_provider_component.rb +29 -0
  51. data/app/components/m9sh/sidebar_trigger_component.rb +44 -0
  52. data/app/components/m9sh/skeleton_component.rb +32 -0
  53. data/app/components/m9sh/slider_component.rb +83 -0
  54. data/app/components/m9sh/spinner_component.rb +46 -0
  55. data/app/components/m9sh/switch_component.rb +47 -0
  56. data/app/components/m9sh/table_component.rb +111 -0
  57. data/app/components/m9sh/tabs_component.rb +92 -0
  58. data/app/components/m9sh/textarea_component.rb +44 -0
  59. data/app/components/m9sh/theme_toggle_component.rb +88 -0
  60. data/app/components/m9sh/toast_component.rb +86 -0
  61. data/app/components/m9sh/toaster_component.rb +20 -0
  62. data/app/components/m9sh/toggle_component.rb +64 -0
  63. data/app/components/m9sh/tooltip_component.rb +48 -0
  64. data/app/components/m9sh/typography_component.rb +56 -0
  65. data/app/components/m9sh/utilities.rb +26 -0
  66. data/app/javascript/controllers/m9sh/accordion_controller.js +110 -0
  67. data/app/javascript/controllers/m9sh/alert_dialog_controller.js +47 -0
  68. data/app/javascript/controllers/m9sh/collapsible_controller.js +57 -0
  69. data/app/javascript/controllers/m9sh/dialog_controller.js +119 -0
  70. data/app/javascript/controllers/m9sh/dropdown_menu_controller.js +103 -0
  71. data/app/javascript/controllers/m9sh/hover_card_controller.js +66 -0
  72. data/app/javascript/controllers/m9sh/navigation_menu_controller.js +219 -0
  73. data/app/javascript/controllers/m9sh/popover_controller.js +113 -0
  74. data/app/javascript/controllers/m9sh/radio_controller.js +59 -0
  75. data/app/javascript/controllers/m9sh/sheet_controller.js +46 -0
  76. data/app/javascript/controllers/m9sh/sidebar_controller.js +114 -0
  77. data/app/javascript/controllers/m9sh/sidebar_provider_controller.js +12 -0
  78. data/app/javascript/controllers/m9sh/slider_controller.js +90 -0
  79. data/app/javascript/controllers/m9sh/switch_controller.js +33 -0
  80. data/app/javascript/controllers/m9sh/tabs_controller.js +51 -0
  81. data/app/javascript/controllers/m9sh/theme_controller.js +50 -0
  82. data/app/javascript/controllers/m9sh/toast_controller.js +46 -0
  83. data/app/javascript/controllers/m9sh/toaster_controller.js +70 -0
  84. data/app/javascript/controllers/m9sh/toggle_controller.js +27 -0
  85. data/app/javascript/controllers/m9sh/tooltip_controller.js +86 -0
  86. data/components.json +21 -0
  87. data/config.ru +6 -0
  88. data/exe/m9sh +12 -0
  89. data/fix_namespaces.py +32 -0
  90. data/fly.toml +30 -0
  91. data/koyeb.yaml +26 -0
  92. data/lib/m9sh/cli.rb +234 -0
  93. data/lib/m9sh/config.rb +114 -0
  94. data/lib/m9sh/generator.rb +183 -0
  95. data/lib/m9sh/registry.rb +107 -0
  96. data/lib/m9sh/registry.yml +384 -0
  97. data/lib/m9sh/version.rb +5 -0
  98. data/lib/m9sh.rb +11 -0
  99. data/package-lock.json +99 -0
  100. data/package.json +28 -0
  101. data/pnpm-lock.yaml +75 -0
  102. data/tailwind.config.js +93 -0
  103. data/update_namespace.py +73 -0
  104. 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