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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/Dockerfile +2 -1
  3. data/GEM_README.md +284 -0
  4. data/LICENSE.txt +21 -0
  5. data/M9SH_CLI.md +453 -0
  6. data/PUBLISHING.md +331 -0
  7. data/README.md +120 -52
  8. data/app/components/m9sh/accordion_component.rb +3 -3
  9. data/app/components/m9sh/alert_component.rb +7 -9
  10. data/app/components/m9sh/base_component.rb +1 -0
  11. data/app/components/m9sh/button_component.rb +3 -2
  12. data/app/components/m9sh/color_customizer_component.rb +624 -0
  13. data/app/components/m9sh/dialog_close_component.rb +30 -0
  14. data/app/components/m9sh/dialog_component.rb +11 -99
  15. data/app/components/m9sh/dialog_content_component.rb +102 -0
  16. data/app/components/m9sh/dialog_description_component.rb +14 -0
  17. data/app/components/m9sh/dialog_footer_component.rb +14 -0
  18. data/app/components/m9sh/dialog_header_component.rb +27 -0
  19. data/app/components/m9sh/dialog_title_component.rb +14 -0
  20. data/app/components/m9sh/dialog_trigger_component.rb +23 -0
  21. data/app/components/m9sh/dropdown_menu_content_component.rb +1 -1
  22. data/app/components/m9sh/dropdown_menu_item_component.rb +1 -1
  23. data/app/components/m9sh/dropdown_menu_trigger_component.rb +1 -1
  24. data/app/components/m9sh/icon_component.rb +78 -0
  25. data/app/components/m9sh/main_component.rb +1 -1
  26. data/app/components/m9sh/menu_component.rb +85 -0
  27. data/app/components/m9sh/navbar_component.rb +186 -0
  28. data/app/components/m9sh/navigation_menu_component.rb +2 -2
  29. data/app/components/m9sh/popover_component.rb +12 -7
  30. data/app/components/m9sh/radio_group_component.rb +45 -13
  31. data/app/components/m9sh/sheet_component.rb +6 -6
  32. data/app/components/m9sh/sidebar_component.rb +6 -1
  33. data/app/components/m9sh/skeleton_component.rb +7 -1
  34. data/app/components/m9sh/tabs_component.rb +76 -48
  35. data/app/components/m9sh/textarea_component.rb +1 -1
  36. data/app/components/m9sh/theme_toggle_component.rb +1 -0
  37. data/app/javascript/controllers/m9sh/popover_controller.js +24 -18
  38. data/app/javascript/controllers/m9sh/sidebar_controller.js +29 -7
  39. data/lib/m9sh/config.rb +5 -5
  40. data/lib/m9sh/registry.rb +2 -2
  41. data/lib/m9sh/registry.yml +37 -0
  42. data/lib/m9sh/version.rb +1 -1
  43. data/lib/tasks/tailwindcss.rake +15 -0
  44. data/m9sh.gemspec +48 -0
  45. data/publish.sh +48 -0
  46. metadata +20 -3
  47. 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-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",
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: "absolute z-50 w-72 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",
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
- safe_join(items)
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:, description: nil, checked: false, disabled: false, **extra_attrs)
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
- tag.div(
37
- class: "flex items-center space-x-2"
38
- ) do
39
- safe_join([
40
- render_radio,
41
- render_label_and_description
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
- class: radio_classes,
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 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",
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-50 gap-4 bg-background p-6 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"
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-sm"
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
- "fixed inset-y-0 z-50 transition-transform duration-300 data-[state=collapsed]:-translate-x-full"
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("animate-pulse bg-muted", rounded_classes))
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
- renders_many :tabs, "TabComponent"
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(base_classes),
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
- render_tab_list,
24
- render_tab_panels
25
- ])
24
+ tabs_list,
25
+ safe_join(tabs_contents)
26
+ ].compact)
26
27
  end
27
28
  end
28
29
 
29
- private
30
+ # Nested TabsList component
31
+ class TabsListComponent < BaseComponent
32
+ include Utilities
30
33
 
31
- def base_classes
32
- "w-full"
33
- end
34
+ renders_many :triggers, "TabsTriggerComponent"
34
35
 
35
- def render_tab_list
36
- tag.div(
37
- class: "inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
38
- role: "tablist",
39
- data: { m9sh__tabs_target: "list" }
40
- ) do
41
- tabs.each_with_index.map do |tab, index|
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
- tab.label,
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: tab.value
51
- },
52
- "aria-selected": @default_value == tab.value ? "true" : "false"
53
- )
54
- end.join.html_safe
55
- end
56
- end
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
- def render_tab_panels
59
- tag.div(class: "mt-2") do
60
- tabs.map do |tab|
61
- tag.div(
62
- tab.panel,
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
- class TabComponent < BaseComponent
76
- attr_reader :value, :label
77
-
78
- renders_one :panel
97
+ # Nested TabsContent component
98
+ class TabsContentComponent < BaseComponent
99
+ include Utilities
79
100
 
80
- def initialize(value:, label:, **extra_attrs)
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
- # Tabs are rendered by parent component
88
- nil
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
- "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base text-foreground shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm"
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
@@ -31,6 +31,7 @@ module M9sh
31
31
 
32
32
  def button_classes
33
33
  cn(
34
+ "relative",
34
35
  "inline-flex items-center justify-center",
35
36
  "rounded-md text-sm font-medium",
36
37
  "h-9 w-9",