better_ui 0.3.0 → 0.7.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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +257 -212
  3. data/Rakefile +11 -2
  4. data/app/components/better_ui/action_messages_component/action_messages_component.html.erb +48 -0
  5. data/app/components/better_ui/action_messages_component.rb +544 -0
  6. data/app/components/better_ui/application_component.rb +66 -0
  7. data/app/components/better_ui/button_component/button_component.html.erb +31 -0
  8. data/app/components/better_ui/button_component.rb +307 -0
  9. data/app/components/better_ui/card_component/card_component.html.erb +17 -0
  10. data/app/components/better_ui/card_component.rb +460 -0
  11. data/app/components/better_ui/drawer/header_component/header_component.html.erb +24 -0
  12. data/app/components/better_ui/drawer/header_component.rb +238 -0
  13. data/app/components/better_ui/drawer/layout_component/layout_component.html.erb +44 -0
  14. data/app/components/better_ui/drawer/layout_component.rb +270 -0
  15. data/app/components/better_ui/drawer/nav_group_component/nav_group_component.html.erb +10 -0
  16. data/app/components/better_ui/drawer/nav_group_component.rb +155 -0
  17. data/app/components/better_ui/drawer/nav_item_component/nav_item_component.html.erb +13 -0
  18. data/app/components/better_ui/drawer/nav_item_component.rb +225 -0
  19. data/app/components/better_ui/drawer/sidebar_component/sidebar_component.html.erb +17 -0
  20. data/app/components/better_ui/drawer/sidebar_component.rb +263 -0
  21. data/app/components/better_ui/forms/base_component.rb +450 -0
  22. data/app/components/better_ui/forms/checkbox_component/checkbox_component.html.erb +28 -0
  23. data/app/components/better_ui/forms/checkbox_component.rb +419 -0
  24. data/app/components/better_ui/forms/checkbox_group_component/checkbox_group_component.html.erb +40 -0
  25. data/app/components/better_ui/forms/checkbox_group_component.rb +363 -0
  26. data/app/components/better_ui/forms/number_input_component/number_input_component.html.erb +40 -0
  27. data/app/components/better_ui/forms/number_input_component.rb +320 -0
  28. data/app/components/better_ui/forms/password_input_component/password_input_component.html.erb +71 -0
  29. data/app/components/better_ui/forms/password_input_component.rb +206 -0
  30. data/app/components/better_ui/forms/text_input_component/text_input_component.html.erb +40 -0
  31. data/app/components/better_ui/forms/text_input_component.rb +258 -0
  32. data/app/components/better_ui/forms/textarea_component/textarea_component.html.erb +40 -0
  33. data/app/components/better_ui/forms/textarea_component.rb +329 -0
  34. data/app/form_builders/better_ui/ui_form_builder.rb +467 -0
  35. data/app/helpers/better_ui/application_helper.rb +325 -51
  36. data/app/views/layouts/better_ui/application.html.erb +1 -1
  37. data/config/routes.rb +1 -0
  38. data/lib/better_ui/engine.rb +34 -5
  39. data/lib/better_ui/version.rb +1 -1
  40. data/lib/better_ui.rb +32 -4
  41. data/lib/generators/better_ui/install/USAGE +44 -0
  42. data/lib/generators/better_ui/install/install_generator.rb +87 -0
  43. data/lib/generators/better_ui/install/templates/better_ui_theme.css.tt +280 -0
  44. data/lib/tasks/better_ui_tasks.rake +39 -4
  45. metadata +52 -185
  46. data/app/components/better_ui/application/card/component.html.erb +0 -20
  47. data/app/components/better_ui/application/card/component.rb +0 -214
  48. data/app/components/better_ui/application/main/component.html.erb +0 -9
  49. data/app/components/better_ui/application/main/component.rb +0 -123
  50. data/app/components/better_ui/application/navbar/component.html.erb +0 -92
  51. data/app/components/better_ui/application/navbar/component.rb +0 -136
  52. data/app/components/better_ui/application/sidebar/component.html.erb +0 -227
  53. data/app/components/better_ui/application/sidebar/component.rb +0 -130
  54. data/app/components/better_ui/general/accordion/component.html.erb +0 -5
  55. data/app/components/better_ui/general/accordion/component.rb +0 -92
  56. data/app/components/better_ui/general/accordion/item_component.html.erb +0 -12
  57. data/app/components/better_ui/general/accordion/item_component.rb +0 -176
  58. data/app/components/better_ui/general/alert/component.html.erb +0 -32
  59. data/app/components/better_ui/general/alert/component.rb +0 -242
  60. data/app/components/better_ui/general/avatar/component.html.erb +0 -20
  61. data/app/components/better_ui/general/avatar/component.rb +0 -301
  62. data/app/components/better_ui/general/badge/component.html.erb +0 -23
  63. data/app/components/better_ui/general/badge/component.rb +0 -248
  64. data/app/components/better_ui/general/breadcrumb/component.html.erb +0 -15
  65. data/app/components/better_ui/general/breadcrumb/component.rb +0 -187
  66. data/app/components/better_ui/general/button/component.html.erb +0 -34
  67. data/app/components/better_ui/general/button/component.rb +0 -214
  68. data/app/components/better_ui/general/divider/component.html.erb +0 -10
  69. data/app/components/better_ui/general/divider/component.rb +0 -226
  70. data/app/components/better_ui/general/dropdown/component.html.erb +0 -25
  71. data/app/components/better_ui/general/dropdown/component.rb +0 -170
  72. data/app/components/better_ui/general/dropdown/divider_component.html.erb +0 -1
  73. data/app/components/better_ui/general/dropdown/divider_component.rb +0 -41
  74. data/app/components/better_ui/general/dropdown/item_component.html.erb +0 -6
  75. data/app/components/better_ui/general/dropdown/item_component.rb +0 -119
  76. data/app/components/better_ui/general/field/component.html.erb +0 -27
  77. data/app/components/better_ui/general/field/component.rb +0 -37
  78. data/app/components/better_ui/general/heading/component.html.erb +0 -22
  79. data/app/components/better_ui/general/heading/component.rb +0 -257
  80. data/app/components/better_ui/general/icon/component.html.erb +0 -7
  81. data/app/components/better_ui/general/icon/component.rb +0 -239
  82. data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
  83. data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
  84. data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
  85. data/app/components/better_ui/general/input/datetime/component.rb +0 -223
  86. data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
  87. data/app/components/better_ui/general/input/radio/component.rb +0 -230
  88. data/app/components/better_ui/general/input/select/component.html.erb +0 -16
  89. data/app/components/better_ui/general/input/select/component.rb +0 -184
  90. data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
  91. data/app/components/better_ui/general/input/select/select_component.rb +0 -37
  92. data/app/components/better_ui/general/input/text/component.html.erb +0 -5
  93. data/app/components/better_ui/general/input/text/component.rb +0 -171
  94. data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
  95. data/app/components/better_ui/general/input/textarea/component.rb +0 -166
  96. data/app/components/better_ui/general/link/component.html.erb +0 -18
  97. data/app/components/better_ui/general/link/component.rb +0 -258
  98. data/app/components/better_ui/general/modal/component.html.erb +0 -5
  99. data/app/components/better_ui/general/modal/component.rb +0 -47
  100. data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
  101. data/app/components/better_ui/general/modal/modal_component.rb +0 -160
  102. data/app/components/better_ui/general/pagination/component.html.erb +0 -85
  103. data/app/components/better_ui/general/pagination/component.rb +0 -216
  104. data/app/components/better_ui/general/panel/component.html.erb +0 -28
  105. data/app/components/better_ui/general/panel/component.rb +0 -249
  106. data/app/components/better_ui/general/progress/component.html.erb +0 -11
  107. data/app/components/better_ui/general/progress/component.rb +0 -160
  108. data/app/components/better_ui/general/spinner/component.html.erb +0 -35
  109. data/app/components/better_ui/general/spinner/component.rb +0 -93
  110. data/app/components/better_ui/general/table/component.html.erb +0 -5
  111. data/app/components/better_ui/general/table/component.rb +0 -217
  112. data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
  113. data/app/components/better_ui/general/table/tbody_component.rb +0 -30
  114. data/app/components/better_ui/general/table/td_component.html.erb +0 -3
  115. data/app/components/better_ui/general/table/td_component.rb +0 -44
  116. data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
  117. data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
  118. data/app/components/better_ui/general/table/th_component.html.erb +0 -6
  119. data/app/components/better_ui/general/table/th_component.rb +0 -51
  120. data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
  121. data/app/components/better_ui/general/table/thead_component.rb +0 -28
  122. data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
  123. data/app/components/better_ui/general/table/tr_component.rb +0 -30
  124. data/app/components/better_ui/general/tabs/component.html.erb +0 -11
  125. data/app/components/better_ui/general/tabs/component.rb +0 -120
  126. data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
  127. data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
  128. data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
  129. data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
  130. data/app/components/better_ui/general/tag/component.html.erb +0 -3
  131. data/app/components/better_ui/general/tag/component.rb +0 -104
  132. data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
  133. data/app/components/better_ui/general/tooltip/component.rb +0 -239
  134. data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
  135. data/app/helpers/better_ui/application/components/card.rb +0 -11
  136. data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
  137. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
  138. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
  139. data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
  140. data/app/helpers/better_ui/general/components/accordion.rb +0 -11
  141. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
  142. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
  143. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
  144. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
  145. data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
  146. data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
  147. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
  148. data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
  149. data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -79
  150. data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -62
  151. data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
  152. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
  153. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
  154. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
  155. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
  156. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
  157. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
  158. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -70
  159. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
  160. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
  161. data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
  162. data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
  163. data/app/helpers/better_ui/general/components/modal.rb +0 -11
  164. data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
  165. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
  166. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
  167. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
  168. data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
  169. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
  170. data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
  171. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
  172. data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
  173. data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
  174. data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
  175. data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
  176. data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
  177. data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
  178. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
  179. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
  180. data/app/jobs/better_ui/application_job.rb +0 -4
  181. data/app/mailers/better_ui/application_mailer.rb +0 -6
  182. data/config/initializers/lookbook.rb +0 -23
  183. data/lib/better_ui/railtie.rb +0 -20
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Drawer
5
+ # A navigation group component for sidebar menus with a title and collection of items.
6
+ #
7
+ # @example Basic usage
8
+ # <%= render BetterUi::Drawer::NavGroupComponent.new(title: "Main") do |group| %>
9
+ # <% group.with_item(label: "Dashboard", href: "/dashboard", active: true) do |item| %>
10
+ # <% item.with_icon do %><svg>...</svg><% end %>
11
+ # <% end %>
12
+ # <% group.with_item(label: "Settings", href: "/settings") do |item| %>
13
+ # <% item.with_icon do %><svg>...</svg><% end %>
14
+ # <% end %>
15
+ # <% end %>
16
+ #
17
+ # @example Without title
18
+ # <%= render BetterUi::Drawer::NavGroupComponent.new do |group| %>
19
+ # <% group.with_item(label: "Home", href: "/") %>
20
+ # <% end %>
21
+ #
22
+ # @example With variant for dark sidebar
23
+ # <%= render BetterUi::Drawer::NavGroupComponent.new(title: "Menu", variant: :dark) do |group| %>
24
+ # <% group.with_item(label: "Dashboard", href: "/") %>
25
+ # <% end %>
26
+ class NavGroupComponent < ApplicationComponent
27
+ # Visual variant configurations for title
28
+ VARIANTS = {
29
+ light: {
30
+ title: "text-grayscale-500"
31
+ },
32
+ dark: {
33
+ title: "text-grayscale-400"
34
+ },
35
+ primary: {
36
+ title: "text-primary-200"
37
+ }
38
+ }.freeze
39
+
40
+ # @!method with_item
41
+ # Slot for rendering navigation items.
42
+ # All parameters are passed to NavItemComponent.
43
+ # @param label [String] the item label text
44
+ # @param href [String] the link URL
45
+ # @param active [Boolean] whether the item is currently active
46
+ # @param method [Symbol, nil] HTTP method for non-GET requests
47
+ # @yieldparam [NavItemComponent] item the nav item component instance
48
+ renders_many :items, ->(label:, href:, active: false, method: nil, container_classes: nil, **options, &block) {
49
+ NavItemComponent.new(
50
+ label: label,
51
+ href: href,
52
+ active: active,
53
+ method: method,
54
+ variant: @variant,
55
+ container_classes: container_classes,
56
+ **options,
57
+ &block
58
+ )
59
+ }
60
+
61
+ # Initializes a new nav group component.
62
+ #
63
+ # @param title [String, nil] the group title (optional)
64
+ # @param variant [Symbol] color variant (:light, :dark, :primary), default: :light
65
+ # @param container_classes [String, nil] additional CSS classes
66
+ # @param options [Hash] additional HTML attributes
67
+ #
68
+ # @raise [ArgumentError] if variant is not one of the allowed values
69
+ def initialize(
70
+ title: nil,
71
+ variant: :light,
72
+ container_classes: nil,
73
+ **options
74
+ )
75
+ @title = title
76
+ @variant = validate_variant(variant)
77
+ @container_classes = container_classes
78
+ @options = options
79
+ end
80
+
81
+ # Returns the title text.
82
+ #
83
+ # @return [String, nil] the title
84
+ attr_reader :title
85
+
86
+ private
87
+
88
+ # Returns CSS classes for the group container.
89
+ #
90
+ # @return [String] the merged CSS class string
91
+ # @api private
92
+ def component_classes
93
+ css_classes([
94
+ @container_classes
95
+ ].compact)
96
+ end
97
+
98
+ # Returns CSS classes for the title.
99
+ #
100
+ # @return [String] CSS classes for the title
101
+ # @api private
102
+ def title_classes
103
+ config = VARIANTS[@variant]
104
+ css_classes([
105
+ "px-4",
106
+ "text-xs",
107
+ "font-semibold",
108
+ "uppercase",
109
+ "tracking-wider",
110
+ config[:title]
111
+ ])
112
+ end
113
+
114
+ # Returns CSS classes for the items container.
115
+ #
116
+ # @return [String] CSS classes for items wrapper
117
+ # @api private
118
+ def items_wrapper_classes
119
+ css_classes([
120
+ @title ? "mt-2" : nil,
121
+ "space-y-1"
122
+ ].compact)
123
+ end
124
+
125
+ # Returns HTML attributes for the group element.
126
+ #
127
+ # @return [Hash] HTML attributes hash
128
+ # @api private
129
+ def html_attributes
130
+ @options
131
+ end
132
+
133
+ # Whether to render the title.
134
+ #
135
+ # @return [Boolean] true if title should be rendered
136
+ # @api private
137
+ def render_title?
138
+ @title.present?
139
+ end
140
+
141
+ # Validates the variant parameter.
142
+ #
143
+ # @param variant [Symbol] the variant to validate
144
+ # @return [Symbol] the validated variant
145
+ # @raise [ArgumentError] if variant is invalid
146
+ # @api private
147
+ def validate_variant(variant)
148
+ unless VARIANTS.key?(variant)
149
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{VARIANTS.keys.join(', ')}"
150
+ end
151
+ variant
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,13 @@
1
+ <a <%= tag.attributes(html_attributes) %>>
2
+ <% if icon? %>
3
+ <span class="<%= icon_classes %>">
4
+ <%= icon %>
5
+ </span>
6
+ <% end %>
7
+ <span><%= label %></span>
8
+ <% if badge? %>
9
+ <span class="<%= badge_classes %>">
10
+ <%= badge %>
11
+ </span>
12
+ <% end %>
13
+ </a>
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Drawer
5
+ # A navigation item component for sidebar menus with icon, label, and badge support.
6
+ #
7
+ # @example Basic usage
8
+ # <%= render BetterUi::Drawer::NavItemComponent.new(label: "Dashboard", href: "/dashboard") %>
9
+ #
10
+ # @example With icon and active state
11
+ # <%= render BetterUi::Drawer::NavItemComponent.new(
12
+ # label: "Dashboard",
13
+ # href: "/dashboard",
14
+ # active: true
15
+ # ) do |item| %>
16
+ # <% item.with_icon do %>
17
+ # <svg>...</svg>
18
+ # <% end %>
19
+ # <% end %>
20
+ #
21
+ # @example With badge
22
+ # <%= render BetterUi::Drawer::NavItemComponent.new(label: "Messages", href: "/messages") do |item| %>
23
+ # <% item.with_icon do %><svg>...</svg><% end %>
24
+ # <% item.with_badge do %>5<% end %>
25
+ # <% end %>
26
+ #
27
+ # @example With HTTP method for logout
28
+ # <%= render BetterUi::Drawer::NavItemComponent.new(
29
+ # label: "Logout",
30
+ # href: "/logout",
31
+ # method: :delete
32
+ # ) %>
33
+ class NavItemComponent < ApplicationComponent
34
+ # Visual variant configurations
35
+ VARIANTS = {
36
+ light: {
37
+ base: "text-grayscale-700",
38
+ active: "bg-primary-50 text-primary-700",
39
+ inactive: "hover:bg-grayscale-100",
40
+ badge_bg: "bg-primary-100",
41
+ badge_text: "text-primary-700"
42
+ },
43
+ dark: {
44
+ base: "text-grayscale-300",
45
+ active: "bg-white/10 text-white",
46
+ inactive: "hover:bg-white/5",
47
+ badge_bg: "bg-white/20",
48
+ badge_text: "text-white"
49
+ },
50
+ primary: {
51
+ base: "text-primary-100",
52
+ active: "bg-white/20 text-white",
53
+ inactive: "hover:bg-white/10",
54
+ badge_bg: "bg-white/20",
55
+ badge_text: "text-white"
56
+ }
57
+ }.freeze
58
+
59
+ # Allowed HTTP methods
60
+ METHODS = %i[get post put patch delete].freeze
61
+
62
+ # @!method with_icon
63
+ # Slot for rendering the icon (displayed before label).
64
+ # @yieldreturn [String] the SVG or icon HTML content
65
+ renders_one :icon
66
+
67
+ # @!method with_badge
68
+ # Slot for rendering a badge/counter (displayed after label).
69
+ # @yieldreturn [String] the badge content (usually a number)
70
+ renders_one :badge
71
+
72
+ # Initializes a new nav item component.
73
+ #
74
+ # @param label [String] the link text (required)
75
+ # @param href [String] the link URL (required)
76
+ # @param active [Boolean] whether the item is currently active (default: false)
77
+ # @param method [Symbol, nil] HTTP method for non-GET requests (:post, :put, :patch, :delete)
78
+ # @param variant [Symbol] color variant (:light, :dark, :primary), default: :light
79
+ # @param container_classes [String, nil] additional CSS classes
80
+ # @param options [Hash] additional HTML attributes passed to the link element
81
+ #
82
+ # @raise [ArgumentError] if variant is not one of the allowed values
83
+ # @raise [ArgumentError] if method is provided and not one of the allowed values
84
+ def initialize(
85
+ label:,
86
+ href:,
87
+ active: false,
88
+ method: nil,
89
+ variant: :light,
90
+ container_classes: nil,
91
+ **options
92
+ )
93
+ @label = label
94
+ @href = href
95
+ @active = active
96
+ @method = validate_method(method)
97
+ @variant = validate_variant(variant)
98
+ @container_classes = container_classes
99
+ @options = options
100
+ end
101
+
102
+ # Returns the label text.
103
+ #
104
+ # @return [String] the label
105
+ attr_reader :label
106
+
107
+ # Returns the href URL.
108
+ #
109
+ # @return [String] the URL
110
+ attr_reader :href
111
+
112
+ # Returns whether the item is active.
113
+ #
114
+ # @return [Boolean] true if active
115
+ def active?
116
+ @active
117
+ end
118
+
119
+ private
120
+
121
+ # Returns the complete CSS classes for the nav item link.
122
+ #
123
+ # @return [String] the merged CSS class string
124
+ # @api private
125
+ def component_classes
126
+ config = VARIANTS[@variant]
127
+ css_classes([
128
+ "flex",
129
+ "items-center",
130
+ "px-4",
131
+ "py-2",
132
+ "rounded-md",
133
+ "transition-colors",
134
+ config[:base],
135
+ active? ? config[:active] : config[:inactive],
136
+ @container_classes
137
+ ].compact)
138
+ end
139
+
140
+ # Returns CSS classes for the icon wrapper.
141
+ #
142
+ # @return [String] CSS classes for icon
143
+ # @api private
144
+ def icon_classes
145
+ css_classes([
146
+ "w-5",
147
+ "h-5",
148
+ "mr-3",
149
+ "shrink-0"
150
+ ])
151
+ end
152
+
153
+ # Returns CSS classes for the badge.
154
+ #
155
+ # @return [String] CSS classes for badge
156
+ # @api private
157
+ def badge_classes
158
+ config = VARIANTS[@variant]
159
+ css_classes([
160
+ "ml-auto",
161
+ "text-xs",
162
+ "font-medium",
163
+ "px-2",
164
+ "py-0.5",
165
+ "rounded-full",
166
+ config[:badge_bg],
167
+ config[:badge_text]
168
+ ])
169
+ end
170
+
171
+ # Returns HTML attributes for the link element.
172
+ #
173
+ # @return [Hash] HTML attributes hash
174
+ # @api private
175
+ def html_attributes
176
+ attrs = @options.dup
177
+ attrs[:href] = @href
178
+ attrs[:class] = component_classes
179
+
180
+ if @method && @method != :get
181
+ attrs[:data] ||= {}
182
+ attrs[:data][:turbo_method] = @method
183
+ end
184
+
185
+ attrs
186
+ end
187
+
188
+ # Whether a non-GET method is specified.
189
+ #
190
+ # @return [Boolean] true if using non-GET method
191
+ # @api private
192
+ def use_turbo_method?
193
+ @method && @method != :get
194
+ end
195
+
196
+ # Validates the variant parameter.
197
+ #
198
+ # @param variant [Symbol] the variant to validate
199
+ # @return [Symbol] the validated variant
200
+ # @raise [ArgumentError] if variant is invalid
201
+ # @api private
202
+ def validate_variant(variant)
203
+ unless VARIANTS.key?(variant)
204
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{VARIANTS.keys.join(', ')}"
205
+ end
206
+ variant
207
+ end
208
+
209
+ # Validates the method parameter.
210
+ #
211
+ # @param method [Symbol, nil] the method to validate
212
+ # @return [Symbol, nil] the validated method
213
+ # @raise [ArgumentError] if method is invalid
214
+ # @api private
215
+ def validate_method(method)
216
+ return nil if method.nil?
217
+
218
+ unless METHODS.include?(method)
219
+ raise ArgumentError, "Invalid method: #{method}. Must be one of: #{METHODS.join(', ')}"
220
+ end
221
+ method
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,17 @@
1
+ <aside class="<%= component_classes %>" <%= tag.attributes(html_attributes) %>>
2
+ <% if header? %>
3
+ <div class="<%= header_wrapper_classes %>">
4
+ <%= header %>
5
+ </div>
6
+ <% end %>
7
+
8
+ <div class="<%= navigation_wrapper_classes %>">
9
+ <%= navigation? ? navigation : content %>
10
+ </div>
11
+
12
+ <% if footer? %>
13
+ <div class="<%= footer_wrapper_classes %>">
14
+ <%= footer %>
15
+ </div>
16
+ <% end %>
17
+ </aside>
@@ -0,0 +1,263 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Drawer
5
+ # A flexible sidebar component for drawer layouts with support for header, navigation, and footer.
6
+ #
7
+ # This component provides a responsive sidebar that can slide in from left or right on mobile,
8
+ # with configurable width and visual variants. It supports slots for header (logo/brand),
9
+ # main navigation, and footer sections.
10
+ #
11
+ # @example Basic sidebar
12
+ # <%= render BetterUi::Drawer::SidebarComponent.new do |sidebar| %>
13
+ # <% sidebar.with_navigation do %>
14
+ # <nav>Navigation links</nav>
15
+ # <% end %>
16
+ # <% end %>
17
+ #
18
+ # @example Sidebar with all sections
19
+ # <%= render BetterUi::Drawer::SidebarComponent.new(position: :left, width: :md) do |sidebar| %>
20
+ # <% sidebar.with_header { image_tag("logo.svg") } %>
21
+ # <% sidebar.with_navigation do %>
22
+ # <nav>Main navigation</nav>
23
+ # <% end %>
24
+ # <% sidebar.with_footer { "User info" } %>
25
+ # <% end %>
26
+ class SidebarComponent < ApplicationComponent
27
+ # Width configurations
28
+ WIDTHS = {
29
+ sm: "w-16", # 64px - icon-only sidebar
30
+ md: "w-64", # 256px - standard sidebar
31
+ lg: "w-80" # 320px - wide sidebar
32
+ }.freeze
33
+
34
+ # Position configurations
35
+ POSITIONS = %i[left right].freeze
36
+
37
+ # Visual variant configurations
38
+ SIDEBAR_VARIANTS = {
39
+ light: {
40
+ bg: "bg-white",
41
+ border: "border-grayscale-200",
42
+ text: "text-grayscale-900"
43
+ },
44
+ dark: {
45
+ bg: "bg-grayscale-900",
46
+ border: "border-grayscale-700",
47
+ text: "text-white"
48
+ },
49
+ primary: {
50
+ bg: "bg-primary-800",
51
+ border: "border-primary-900",
52
+ text: "text-white"
53
+ }
54
+ }.freeze
55
+
56
+ # @!method with_header
57
+ # Slot for rendering the sidebar header section (logo, brand).
58
+ # @yieldreturn [String] the HTML content for the header
59
+ renders_one :header
60
+
61
+ # @!method with_navigation
62
+ # Slot for rendering the main navigation content.
63
+ # @yieldreturn [String] the HTML content for navigation
64
+ renders_one :navigation
65
+
66
+ # @!method with_footer
67
+ # Slot for rendering the sidebar footer section (user info, settings).
68
+ # @yieldreturn [String] the HTML content for the footer
69
+ renders_one :footer
70
+
71
+ # Initializes a new sidebar component.
72
+ #
73
+ # @param variant [Symbol] the visual variant (:light, :dark, :primary), defaults to :light
74
+ # @param position [Symbol] the sidebar position (:left, :right), defaults to :left
75
+ # @param width [Symbol] the width variant (:sm, :md, :lg), defaults to :md
76
+ # @param collapsible [Boolean] whether sidebar can be collapsed (icon-only mode), defaults to true
77
+ # @param container_classes [String, nil] additional CSS classes for the container
78
+ # @param header_classes [String, nil] additional CSS classes for the header section
79
+ # @param navigation_classes [String, nil] additional CSS classes for the navigation section
80
+ # @param footer_classes [String, nil] additional CSS classes for the footer section
81
+ # @param options [Hash] additional HTML attributes passed to the sidebar element
82
+ #
83
+ # @raise [ArgumentError] if variant is not one of the allowed values
84
+ # @raise [ArgumentError] if position is not one of the allowed values
85
+ # @raise [ArgumentError] if width is not one of the allowed values
86
+ def initialize(
87
+ variant: :light,
88
+ position: :left,
89
+ width: :md,
90
+ collapsible: true,
91
+ container_classes: nil,
92
+ header_classes: nil,
93
+ navigation_classes: nil,
94
+ footer_classes: nil,
95
+ **options
96
+ )
97
+ @variant = validate_variant(variant)
98
+ @position = validate_position(position)
99
+ @width = validate_width(width)
100
+ @collapsible = collapsible
101
+ @container_classes = container_classes
102
+ @header_classes = header_classes
103
+ @navigation_classes = navigation_classes
104
+ @footer_classes = footer_classes
105
+ @options = options
106
+ end
107
+
108
+ # Returns the sidebar position.
109
+ #
110
+ # @return [Symbol] the position (:left or :right)
111
+ attr_reader :position
112
+
113
+ private
114
+
115
+ # Returns the complete CSS classes for the sidebar container.
116
+ #
117
+ # @return [String] the merged CSS class string
118
+ # @api private
119
+ def component_classes
120
+ css_classes([
121
+ "flex",
122
+ "flex-col",
123
+ "h-full",
124
+ width_class,
125
+ variant_classes,
126
+ border_classes,
127
+ "shrink-0",
128
+ @container_classes
129
+ ].flatten.compact)
130
+ end
131
+
132
+ # Returns CSS class for the width.
133
+ #
134
+ # @return [String] the width class
135
+ # @api private
136
+ def width_class
137
+ WIDTHS[@width]
138
+ end
139
+
140
+ # Returns CSS classes for the variant.
141
+ #
142
+ # @return [Array<String>] array of CSS classes for the variant
143
+ # @api private
144
+ def variant_classes
145
+ config = SIDEBAR_VARIANTS[@variant]
146
+ [ config[:bg], config[:text] ]
147
+ end
148
+
149
+ # Returns CSS classes for the border based on position.
150
+ #
151
+ # @return [String] the border class
152
+ # @api private
153
+ def border_classes
154
+ border_color = SIDEBAR_VARIANTS[@variant][:border]
155
+ case @position
156
+ when :left
157
+ "border-r #{border_color}"
158
+ when :right
159
+ "border-l #{border_color}"
160
+ end
161
+ end
162
+
163
+ # Returns CSS classes for the header section.
164
+ #
165
+ # @return [String] CSS classes for header wrapper
166
+ # @api private
167
+ def header_wrapper_classes
168
+ css_classes([
169
+ "shrink-0",
170
+ "p-4",
171
+ "border-b",
172
+ SIDEBAR_VARIANTS[@variant][:border],
173
+ @header_classes
174
+ ].compact)
175
+ end
176
+
177
+ # Returns CSS classes for the navigation section.
178
+ #
179
+ # @return [String] CSS classes for navigation wrapper
180
+ # @api private
181
+ def navigation_wrapper_classes
182
+ css_classes([
183
+ "flex-1",
184
+ "overflow-y-auto",
185
+ "p-4",
186
+ @navigation_classes
187
+ ].compact)
188
+ end
189
+
190
+ # Returns CSS classes for the footer section.
191
+ #
192
+ # @return [String] CSS classes for footer wrapper
193
+ # @api private
194
+ def footer_wrapper_classes
195
+ css_classes([
196
+ "shrink-0",
197
+ "p-4",
198
+ "border-t",
199
+ SIDEBAR_VARIANTS[@variant][:border],
200
+ @footer_classes
201
+ ].compact)
202
+ end
203
+
204
+ # Returns HTML attributes for the sidebar element.
205
+ #
206
+ # @return [Hash] HTML attributes hash
207
+ # @api private
208
+ def html_attributes
209
+ @options.merge(data: data_attributes)
210
+ end
211
+
212
+ # Returns data attributes for the sidebar.
213
+ #
214
+ # @return [Hash] data attributes hash
215
+ # @api private
216
+ def data_attributes
217
+ attrs = {}
218
+ attrs[:position] = @position
219
+ attrs[:collapsible] = @collapsible
220
+ (@options[:data] || {}).merge(attrs)
221
+ end
222
+
223
+ # Validates the variant parameter.
224
+ #
225
+ # @param variant [Symbol] the variant to validate
226
+ # @return [Symbol] the validated variant
227
+ # @raise [ArgumentError] if variant is invalid
228
+ # @api private
229
+ def validate_variant(variant)
230
+ unless SIDEBAR_VARIANTS.key?(variant)
231
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{SIDEBAR_VARIANTS.keys.join(', ')}"
232
+ end
233
+ variant
234
+ end
235
+
236
+ # Validates the position parameter.
237
+ #
238
+ # @param position [Symbol] the position to validate
239
+ # @return [Symbol] the validated position
240
+ # @raise [ArgumentError] if position is invalid
241
+ # @api private
242
+ def validate_position(position)
243
+ unless POSITIONS.include?(position)
244
+ raise ArgumentError, "Invalid position: #{position}. Must be one of: #{POSITIONS.join(', ')}"
245
+ end
246
+ position
247
+ end
248
+
249
+ # Validates the width parameter.
250
+ #
251
+ # @param width [Symbol] the width to validate
252
+ # @return [Symbol] the validated width
253
+ # @raise [ArgumentError] if width is invalid
254
+ # @api private
255
+ def validate_width(width)
256
+ unless WIDTHS.key?(width)
257
+ raise ArgumentError, "Invalid width: #{width}. Must be one of: #{WIDTHS.keys.join(', ')}"
258
+ end
259
+ width
260
+ end
261
+ end
262
+ end
263
+ end