better_ui 0.6.0 → 0.7.1

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 (198) 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 -58
  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 -5
  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 +55 -203
  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 -249
  53. data/app/components/better_ui/application/sidebar/component.rb +0 -187
  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 -28
  71. data/app/components/better_ui/general/dropdown/component.rb +0 -192
  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/grid/cell_component.html.erb +0 -3
  79. data/app/components/better_ui/general/grid/cell_component.rb +0 -390
  80. data/app/components/better_ui/general/grid/component.html.erb +0 -3
  81. data/app/components/better_ui/general/grid/component.rb +0 -301
  82. data/app/components/better_ui/general/heading/component.html.erb +0 -22
  83. data/app/components/better_ui/general/heading/component.rb +0 -257
  84. data/app/components/better_ui/general/icon/component.html.erb +0 -7
  85. data/app/components/better_ui/general/icon/component.rb +0 -240
  86. data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
  87. data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
  88. data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
  89. data/app/components/better_ui/general/input/datetime/component.rb +0 -223
  90. data/app/components/better_ui/general/input/pin/component.html.erb +0 -1
  91. data/app/components/better_ui/general/input/pin/component.rb +0 -201
  92. data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
  93. data/app/components/better_ui/general/input/radio/component.rb +0 -230
  94. data/app/components/better_ui/general/input/rating/component.html.erb +0 -4
  95. data/app/components/better_ui/general/input/rating/component.rb +0 -272
  96. data/app/components/better_ui/general/input/select/component.html.erb +0 -78
  97. data/app/components/better_ui/general/input/select/component.rb +0 -249
  98. data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
  99. data/app/components/better_ui/general/input/select/select_component.rb +0 -37
  100. data/app/components/better_ui/general/input/text/component.html.erb +0 -5
  101. data/app/components/better_ui/general/input/text/component.rb +0 -171
  102. data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
  103. data/app/components/better_ui/general/input/textarea/component.rb +0 -166
  104. data/app/components/better_ui/general/input/toggle/component.html.erb +0 -5
  105. data/app/components/better_ui/general/input/toggle/component.rb +0 -242
  106. data/app/components/better_ui/general/link/component.html.erb +0 -18
  107. data/app/components/better_ui/general/link/component.rb +0 -258
  108. data/app/components/better_ui/general/modal/component.html.erb +0 -5
  109. data/app/components/better_ui/general/modal/component.rb +0 -47
  110. data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
  111. data/app/components/better_ui/general/modal/modal_component.rb +0 -160
  112. data/app/components/better_ui/general/pagination/component.html.erb +0 -85
  113. data/app/components/better_ui/general/pagination/component.rb +0 -216
  114. data/app/components/better_ui/general/panel/component.html.erb +0 -28
  115. data/app/components/better_ui/general/panel/component.rb +0 -249
  116. data/app/components/better_ui/general/progress/component.html.erb +0 -11
  117. data/app/components/better_ui/general/progress/component.rb +0 -160
  118. data/app/components/better_ui/general/spinner/component.html.erb +0 -35
  119. data/app/components/better_ui/general/spinner/component.rb +0 -93
  120. data/app/components/better_ui/general/table/component.html.erb +0 -5
  121. data/app/components/better_ui/general/table/component.rb +0 -217
  122. data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
  123. data/app/components/better_ui/general/table/tbody_component.rb +0 -30
  124. data/app/components/better_ui/general/table/td_component.html.erb +0 -3
  125. data/app/components/better_ui/general/table/td_component.rb +0 -44
  126. data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
  127. data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
  128. data/app/components/better_ui/general/table/th_component.html.erb +0 -6
  129. data/app/components/better_ui/general/table/th_component.rb +0 -51
  130. data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
  131. data/app/components/better_ui/general/table/thead_component.rb +0 -28
  132. data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
  133. data/app/components/better_ui/general/table/tr_component.rb +0 -30
  134. data/app/components/better_ui/general/tabs/component.html.erb +0 -11
  135. data/app/components/better_ui/general/tabs/component.rb +0 -120
  136. data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
  137. data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
  138. data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
  139. data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
  140. data/app/components/better_ui/general/tag/component.html.erb +0 -3
  141. data/app/components/better_ui/general/tag/component.rb +0 -104
  142. data/app/components/better_ui/general/text/component.html.erb +0 -1
  143. data/app/components/better_ui/general/text/component.rb +0 -194
  144. data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
  145. data/app/components/better_ui/general/tooltip/component.rb +0 -239
  146. data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
  147. data/app/helpers/better_ui/application/components/card.rb +0 -11
  148. data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
  149. data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
  150. data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
  151. data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
  152. data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
  153. data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
  154. data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
  155. data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
  156. data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
  157. data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
  158. data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
  159. data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
  160. data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -88
  161. data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -68
  162. data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
  163. data/app/helpers/better_ui/general/components/grid/grid_helper.rb +0 -145
  164. data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
  165. data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
  166. data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
  167. data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
  168. data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +0 -76
  169. data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
  170. data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
  171. data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +0 -70
  172. data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -86
  173. data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
  174. data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
  175. data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +0 -77
  176. data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
  177. data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
  178. data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
  179. data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
  180. data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
  181. data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
  182. data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
  183. data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
  184. data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
  185. data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
  186. data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
  187. data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
  188. data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
  189. data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
  190. data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
  191. data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
  192. data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
  193. data/app/helpers/better_ui/general/components/text/text_helper.rb +0 -83
  194. data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
  195. data/app/jobs/better_ui/application_job.rb +0 -4
  196. data/app/mailers/better_ui/application_mailer.rb +0 -6
  197. data/config/initializers/lookbook.rb +0 -23
  198. data/lib/better_ui/railtie.rb +0 -20
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Drawer
5
+ # A flexible header component for drawer layouts with support for logo, navigation, and actions.
6
+ #
7
+ # This component provides a responsive header with slots for logo, navigation, actions,
8
+ # and mobile menu button. It supports sticky positioning and multiple visual variants.
9
+ #
10
+ # @example Basic header
11
+ # <%= render BetterUi::Drawer::HeaderComponent.new do |header| %>
12
+ # <% header.with_logo { image_tag("logo.svg") } %>
13
+ # <% header.with_actions { "User Menu" } %>
14
+ # <% end %>
15
+ #
16
+ # @example Header with navigation
17
+ # <%= render BetterUi::Drawer::HeaderComponent.new(sticky: true, variant: :light) do |header| %>
18
+ # <% header.with_logo { "Brand" } %>
19
+ # <% header.with_navigation do %>
20
+ # <nav>Navigation links</nav>
21
+ # <% end %>
22
+ # <% header.with_mobile_menu_button do %>
23
+ # <button>Menu</button>
24
+ # <% end %>
25
+ # <% header.with_actions { "Actions" } %>
26
+ # <% end %>
27
+ class HeaderComponent < ApplicationComponent
28
+ # Height configurations
29
+ HEIGHTS = {
30
+ sm: "h-12",
31
+ md: "h-16",
32
+ lg: "h-20"
33
+ }.freeze
34
+
35
+ # Visual variant configurations
36
+ HEADER_VARIANTS = {
37
+ light: {
38
+ bg: "bg-white",
39
+ border: "border-b border-grayscale-200",
40
+ text: "text-grayscale-900"
41
+ },
42
+ dark: {
43
+ bg: "bg-grayscale-900",
44
+ border: "border-b border-grayscale-700",
45
+ text: "text-white"
46
+ },
47
+ transparent: {
48
+ bg: "bg-transparent",
49
+ border: "",
50
+ text: "text-grayscale-900"
51
+ },
52
+ primary: {
53
+ bg: "bg-primary-600",
54
+ border: "border-b border-primary-700",
55
+ text: "text-white"
56
+ }
57
+ }.freeze
58
+
59
+ # @!method with_logo
60
+ # Slot for rendering the logo/brand section (left side).
61
+ # @yieldreturn [String] the HTML content for the logo
62
+ renders_one :logo
63
+
64
+ # @!method with_navigation
65
+ # Slot for rendering the main navigation (center or after logo).
66
+ # Hidden on mobile by default.
67
+ # @yieldreturn [String] the HTML content for navigation
68
+ renders_one :navigation
69
+
70
+ # @!method with_actions
71
+ # Slot for rendering actions section (right side).
72
+ # Typically contains user menu, notifications, etc.
73
+ # @yieldreturn [String] the HTML content for actions
74
+ renders_one :actions
75
+
76
+ # @!method with_mobile_menu_button
77
+ # Slot for rendering the mobile menu toggle button.
78
+ # Only visible on mobile screens.
79
+ # @yieldreturn [String] the HTML content for the mobile menu button
80
+ renders_one :mobile_menu_button
81
+
82
+ # Initializes a new header component.
83
+ #
84
+ # @param variant [Symbol] the visual variant (:light, :dark, :transparent, :primary), defaults to :light
85
+ # @param sticky [Boolean] whether the header is sticky/fixed at top, defaults to true
86
+ # @param height [Symbol] the height variant (:sm, :md, :lg), defaults to :md
87
+ # @param container_classes [String, nil] additional CSS classes for the container
88
+ # @param options [Hash] additional HTML attributes passed to the header element
89
+ #
90
+ # @raise [ArgumentError] if variant is not one of the allowed values
91
+ # @raise [ArgumentError] if height is not one of the allowed values
92
+ def initialize(
93
+ variant: :light,
94
+ sticky: true,
95
+ height: :md,
96
+ container_classes: nil,
97
+ **options
98
+ )
99
+ @variant = validate_variant(variant)
100
+ @sticky = sticky
101
+ @height = validate_height(height)
102
+ @container_classes = container_classes
103
+ @options = options
104
+ end
105
+
106
+ private
107
+
108
+ # Returns the complete CSS classes for the header container.
109
+ #
110
+ # @return [String] the merged CSS class string
111
+ # @api private
112
+ def component_classes
113
+ css_classes([
114
+ "w-full",
115
+ height_class,
116
+ variant_classes,
117
+ sticky_classes,
118
+ "flex",
119
+ "items-center",
120
+ "justify-between",
121
+ "px-4",
122
+ "z-40",
123
+ @container_classes
124
+ ].flatten.compact)
125
+ end
126
+
127
+ # Returns CSS class for the height.
128
+ #
129
+ # @return [String] the height class
130
+ # @api private
131
+ def height_class
132
+ HEIGHTS[@height]
133
+ end
134
+
135
+ # Returns CSS classes for the variant.
136
+ #
137
+ # @return [Array<String>] array of CSS classes for the variant
138
+ # @api private
139
+ def variant_classes
140
+ config = HEADER_VARIANTS[@variant]
141
+ [ config[:bg], config[:border], config[:text] ]
142
+ end
143
+
144
+ # Returns CSS classes for sticky positioning.
145
+ #
146
+ # @return [String, nil] sticky class or nil
147
+ # @api private
148
+ def sticky_classes
149
+ @sticky ? "sticky top-0" : nil
150
+ end
151
+
152
+ # Returns CSS classes for the logo section.
153
+ #
154
+ # @return [String] CSS classes for logo wrapper
155
+ # @api private
156
+ def logo_classes
157
+ css_classes([
158
+ "flex",
159
+ "items-center",
160
+ "shrink-0"
161
+ ])
162
+ end
163
+
164
+ # Returns CSS classes for the navigation section.
165
+ #
166
+ # @return [String] CSS classes for navigation wrapper
167
+ # @api private
168
+ def navigation_classes
169
+ css_classes([
170
+ "hidden",
171
+ "lg:flex",
172
+ "flex-1",
173
+ "items-center",
174
+ "justify-center",
175
+ "px-4"
176
+ ])
177
+ end
178
+
179
+ # Returns CSS classes for the actions section.
180
+ #
181
+ # @return [String] CSS classes for actions wrapper
182
+ # @api private
183
+ def actions_classes
184
+ css_classes([
185
+ "flex",
186
+ "items-center",
187
+ "gap-2"
188
+ ])
189
+ end
190
+
191
+ # Returns CSS classes for the mobile menu button.
192
+ #
193
+ # @return [String] CSS classes for mobile menu button wrapper
194
+ # @api private
195
+ def mobile_menu_button_classes
196
+ css_classes([
197
+ "lg:hidden",
198
+ "flex",
199
+ "items-center"
200
+ ])
201
+ end
202
+
203
+ # Returns HTML attributes for the header element.
204
+ #
205
+ # @return [Hash] HTML attributes hash
206
+ # @api private
207
+ def html_attributes
208
+ @options
209
+ end
210
+
211
+ # Validates the variant parameter.
212
+ #
213
+ # @param variant [Symbol] the variant to validate
214
+ # @return [Symbol] the validated variant
215
+ # @raise [ArgumentError] if variant is invalid
216
+ # @api private
217
+ def validate_variant(variant)
218
+ unless HEADER_VARIANTS.key?(variant)
219
+ raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{HEADER_VARIANTS.keys.join(', ')}"
220
+ end
221
+ variant
222
+ end
223
+
224
+ # Validates the height parameter.
225
+ #
226
+ # @param height [Symbol] the height to validate
227
+ # @return [Symbol] the validated height
228
+ # @raise [ArgumentError] if height is invalid
229
+ # @api private
230
+ def validate_height(height)
231
+ unless HEIGHTS.key?(height)
232
+ raise ArgumentError, "Invalid height: #{height}. Must be one of: #{HEIGHTS.keys.join(', ')}"
233
+ end
234
+ height
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,44 @@
1
+ <div class="<%= component_classes %>" <%= tag.attributes(html_attributes) %>>
2
+ <%# Header section (always visible) %>
3
+ <% if header? %>
4
+ <%= header %>
5
+ <% end %>
6
+
7
+ <%# Body wrapper (sidebar + main) %>
8
+ <div class="<%= body_wrapper_classes %>">
9
+ <%# Overlay for mobile drawer %>
10
+ <div class="<%= overlay_classes %>"
11
+ data-<%= controller_name %>-target="overlay"
12
+ data-action="click-><%= controller_name %>#close">
13
+ </div>
14
+
15
+ <%# Desktop sidebar (always visible on desktop) %>
16
+ <% if sidebar? %>
17
+ <% if sidebar_position == :left %>
18
+ <div class="<%= sidebar_wrapper_classes %>">
19
+ <%= sidebar %>
20
+ </div>
21
+ <% end %>
22
+ <% end %>
23
+
24
+ <%# Mobile sidebar (drawer) %>
25
+ <% if sidebar? %>
26
+ <div class="<%= mobile_sidebar_wrapper_classes %>"
27
+ data-<%= controller_name %>-target="sidebar">
28
+ <%= sidebar %>
29
+ </div>
30
+ <% end %>
31
+
32
+ <%# Main content area %>
33
+ <main class="<%= main_wrapper_classes %>">
34
+ <%= main? ? main : content %>
35
+ </main>
36
+
37
+ <%# Desktop sidebar on right (if positioned right) %>
38
+ <% if sidebar? && sidebar_position == :right %>
39
+ <div class="<%= sidebar_wrapper_classes %>">
40
+ <%= sidebar %>
41
+ </div>
42
+ <% end %>
43
+ </div>
44
+ </div>
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterUi
4
+ module Drawer
5
+ # A responsive layout component that composes header and sidebar with mobile drawer support.
6
+ #
7
+ # This component provides a complete page layout with a sticky header, responsive sidebar,
8
+ # and main content area. On mobile, the sidebar becomes a slide-out drawer that can be
9
+ # toggled via a menu button.
10
+ #
11
+ # @example Basic layout
12
+ # <%= render BetterUi::Drawer::LayoutComponent.new do |layout| %>
13
+ # <% layout.with_header(sticky: true) do |header| %>
14
+ # <% header.with_logo { "Logo" } %>
15
+ # <% header.with_mobile_menu_button { "☰" } %>
16
+ # <% end %>
17
+ # <% layout.with_sidebar do |sidebar| %>
18
+ # <% sidebar.with_navigation { render "nav" } %>
19
+ # <% end %>
20
+ # <% layout.with_main do %>
21
+ # Main content here
22
+ # <% end %>
23
+ # <% end %>
24
+ #
25
+ # @example Right-positioned sidebar
26
+ # <%= render BetterUi::Drawer::LayoutComponent.new(sidebar_position: :right) do |layout| %>
27
+ # <% layout.with_sidebar(position: :right) do |sidebar| %>
28
+ # <% sidebar.with_navigation { "Nav" } %>
29
+ # <% end %>
30
+ # <% layout.with_main { "Content" } %>
31
+ # <% end %>
32
+ class LayoutComponent < ApplicationComponent
33
+ # Breakpoint configurations for desktop mode
34
+ BREAKPOINTS = {
35
+ md: "md:flex",
36
+ lg: "lg:flex",
37
+ xl: "xl:flex"
38
+ }.freeze
39
+
40
+ # Position configurations
41
+ POSITIONS = %i[left right].freeze
42
+
43
+ # @!method with_header
44
+ # Slot for rendering the HeaderComponent.
45
+ # @yieldparam [BetterUi::Drawer::HeaderComponent] header the header component instance
46
+ # @yieldreturn [String] the HTML content for the header
47
+ renders_one :header, HeaderComponent
48
+
49
+ # @!method with_sidebar
50
+ # Slot for rendering the SidebarComponent.
51
+ # @yieldparam [BetterUi::Drawer::SidebarComponent] sidebar the sidebar component instance
52
+ # @yieldreturn [String] the HTML content for the sidebar
53
+ renders_one :sidebar, SidebarComponent
54
+
55
+ # @!method with_main
56
+ # Slot for rendering the main content area.
57
+ # @yieldreturn [String] the HTML content for the main area
58
+ renders_one :main
59
+
60
+ # Initializes a new layout component.
61
+ #
62
+ # @param sidebar_position [Symbol] the sidebar position (:left, :right), defaults to :left
63
+ # @param sidebar_breakpoint [Symbol] the breakpoint for desktop sidebar (:md, :lg, :xl), defaults to :lg
64
+ # @param container_classes [String, nil] additional CSS classes for the outer container
65
+ # @param main_classes [String, nil] additional CSS classes for the main content area
66
+ # @param options [Hash] additional HTML attributes passed to the layout element
67
+ #
68
+ # @raise [ArgumentError] if sidebar_position is not one of the allowed values
69
+ # @raise [ArgumentError] if sidebar_breakpoint is not one of the allowed values
70
+ def initialize(
71
+ sidebar_position: :left,
72
+ sidebar_breakpoint: :lg,
73
+ container_classes: nil,
74
+ main_classes: nil,
75
+ **options
76
+ )
77
+ @sidebar_position = validate_position(sidebar_position)
78
+ @sidebar_breakpoint = validate_breakpoint(sidebar_breakpoint)
79
+ @container_classes = container_classes
80
+ @main_classes = main_classes
81
+ @options = options
82
+ end
83
+
84
+ # Returns the sidebar position.
85
+ #
86
+ # @return [Symbol] the position (:left or :right)
87
+ attr_reader :sidebar_position
88
+
89
+ # Returns the sidebar breakpoint.
90
+ #
91
+ # @return [Symbol] the breakpoint (:md, :lg, or :xl)
92
+ attr_reader :sidebar_breakpoint
93
+
94
+ private
95
+
96
+ # Returns the complete CSS classes for the outer layout container.
97
+ #
98
+ # @return [String] the merged CSS class string
99
+ # @api private
100
+ def component_classes
101
+ css_classes([
102
+ "h-screen",
103
+ "overflow-hidden",
104
+ "flex",
105
+ "flex-col",
106
+ @container_classes
107
+ ].compact)
108
+ end
109
+
110
+ # Returns CSS classes for the body wrapper (contains sidebar + main).
111
+ #
112
+ # @return [String] the merged CSS class string
113
+ # @api private
114
+ def body_wrapper_classes
115
+ css_classes([
116
+ "flex",
117
+ "flex-1",
118
+ "overflow-hidden",
119
+ "relative"
120
+ ])
121
+ end
122
+
123
+ # Returns CSS classes for the sidebar wrapper (desktop visibility).
124
+ #
125
+ # @return [String] the merged CSS class string
126
+ # @api private
127
+ def sidebar_wrapper_classes
128
+ css_classes([
129
+ "hidden", # Hidden on mobile by default
130
+ "h-full", # Full height to pass to sidebar
131
+ BREAKPOINTS[@sidebar_breakpoint] # Show on desktop breakpoint
132
+ ])
133
+ end
134
+
135
+ # Returns CSS classes for the mobile sidebar wrapper (overlay drawer).
136
+ #
137
+ # @return [String] the merged CSS class string
138
+ # @api private
139
+ def mobile_sidebar_wrapper_classes
140
+ css_classes([
141
+ "fixed",
142
+ "inset-y-0",
143
+ mobile_position_class,
144
+ "z-50",
145
+ "transform",
146
+ "transition-transform",
147
+ "duration-300",
148
+ "ease-in-out",
149
+ initial_transform_class,
150
+ desktop_hidden_class
151
+ ])
152
+ end
153
+
154
+ # Returns the position class for mobile sidebar.
155
+ #
156
+ # @return [String] the position class
157
+ # @api private
158
+ def mobile_position_class
159
+ @sidebar_position == :right ? "right-0" : "left-0"
160
+ end
161
+
162
+ # Returns the initial transform class for hidden state.
163
+ #
164
+ # @return [String] the transform class
165
+ # @api private
166
+ def initial_transform_class
167
+ @sidebar_position == :right ? "translate-x-full" : "-translate-x-full"
168
+ end
169
+
170
+ # Returns CSS class to hide on desktop.
171
+ #
172
+ # @return [String] the hidden class for desktop
173
+ # @api private
174
+ def desktop_hidden_class
175
+ case @sidebar_breakpoint
176
+ when :md then "md:hidden"
177
+ when :lg then "lg:hidden"
178
+ when :xl then "xl:hidden"
179
+ end
180
+ end
181
+
182
+ # Returns CSS classes for the overlay.
183
+ #
184
+ # @return [String] the merged CSS class string
185
+ # @api private
186
+ def overlay_classes
187
+ css_classes([
188
+ "fixed",
189
+ "inset-0",
190
+ "bg-black/50",
191
+ "z-40",
192
+ "hidden", # Initially hidden
193
+ "opacity-0",
194
+ "transition-opacity",
195
+ "duration-300",
196
+ desktop_hidden_class
197
+ ])
198
+ end
199
+
200
+ # Returns CSS classes for the main content area.
201
+ #
202
+ # @return [String] the merged CSS class string
203
+ # @api private
204
+ def main_wrapper_classes
205
+ css_classes([
206
+ "flex-1",
207
+ "overflow-auto",
208
+ @main_classes
209
+ ].compact)
210
+ end
211
+
212
+ # Returns the Stimulus controller identifier.
213
+ #
214
+ # @return [String] the controller name
215
+ # @api private
216
+ def controller_name
217
+ "better-ui--drawer--layout"
218
+ end
219
+
220
+ # Returns HTML attributes for the layout element.
221
+ #
222
+ # @return [Hash] HTML attributes hash
223
+ # @api private
224
+ def html_attributes
225
+ @options.merge(
226
+ data: data_attributes
227
+ )
228
+ end
229
+
230
+ # Returns data attributes for Stimulus controller.
231
+ #
232
+ # @return [Hash] data attributes hash
233
+ # @api private
234
+ def data_attributes
235
+ attrs = {
236
+ controller: controller_name,
237
+ "#{controller_name}-position-value": @sidebar_position,
238
+ "#{controller_name}-breakpoint-value": @sidebar_breakpoint
239
+ }
240
+ (@options[:data] || {}).merge(attrs)
241
+ end
242
+
243
+ # Validates the sidebar_position parameter.
244
+ #
245
+ # @param position [Symbol] the position to validate
246
+ # @return [Symbol] the validated position
247
+ # @raise [ArgumentError] if position is invalid
248
+ # @api private
249
+ def validate_position(position)
250
+ unless POSITIONS.include?(position)
251
+ raise ArgumentError, "Invalid sidebar_position: #{position}. Must be one of: #{POSITIONS.join(', ')}"
252
+ end
253
+ position
254
+ end
255
+
256
+ # Validates the sidebar_breakpoint parameter.
257
+ #
258
+ # @param breakpoint [Symbol] the breakpoint to validate
259
+ # @return [Symbol] the validated breakpoint
260
+ # @raise [ArgumentError] if breakpoint is invalid
261
+ # @api private
262
+ def validate_breakpoint(breakpoint)
263
+ unless BREAKPOINTS.key?(breakpoint)
264
+ raise ArgumentError, "Invalid sidebar_breakpoint: #{breakpoint}. Must be one of: #{BREAKPOINTS.keys.join(', ')}"
265
+ end
266
+ breakpoint
267
+ end
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,10 @@
1
+ <div class="<%= component_classes %>" <%= tag.attributes(html_attributes) %>>
2
+ <% if render_title? %>
3
+ <h3 class="<%= title_classes %>"><%= title %></h3>
4
+ <% end %>
5
+ <div class="<%= items_wrapper_classes %>">
6
+ <% items.each do |item| %>
7
+ <%= item %>
8
+ <% end %>
9
+ </div>
10
+ </div>