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.
- checksums.yaml +4 -4
- data/README.md +257 -212
- data/Rakefile +11 -2
- data/app/components/better_ui/action_messages_component/action_messages_component.html.erb +48 -0
- data/app/components/better_ui/action_messages_component.rb +544 -0
- data/app/components/better_ui/application_component.rb +66 -0
- data/app/components/better_ui/button_component/button_component.html.erb +31 -0
- data/app/components/better_ui/button_component.rb +307 -0
- data/app/components/better_ui/card_component/card_component.html.erb +17 -0
- data/app/components/better_ui/card_component.rb +460 -0
- data/app/components/better_ui/drawer/header_component/header_component.html.erb +24 -0
- data/app/components/better_ui/drawer/header_component.rb +238 -0
- data/app/components/better_ui/drawer/layout_component/layout_component.html.erb +44 -0
- data/app/components/better_ui/drawer/layout_component.rb +270 -0
- data/app/components/better_ui/drawer/nav_group_component/nav_group_component.html.erb +10 -0
- data/app/components/better_ui/drawer/nav_group_component.rb +155 -0
- data/app/components/better_ui/drawer/nav_item_component/nav_item_component.html.erb +13 -0
- data/app/components/better_ui/drawer/nav_item_component.rb +225 -0
- data/app/components/better_ui/drawer/sidebar_component/sidebar_component.html.erb +17 -0
- data/app/components/better_ui/drawer/sidebar_component.rb +263 -0
- data/app/components/better_ui/forms/base_component.rb +450 -0
- data/app/components/better_ui/forms/checkbox_component/checkbox_component.html.erb +28 -0
- data/app/components/better_ui/forms/checkbox_component.rb +419 -0
- data/app/components/better_ui/forms/checkbox_group_component/checkbox_group_component.html.erb +40 -0
- data/app/components/better_ui/forms/checkbox_group_component.rb +363 -0
- data/app/components/better_ui/forms/number_input_component/number_input_component.html.erb +40 -0
- data/app/components/better_ui/forms/number_input_component.rb +320 -0
- data/app/components/better_ui/forms/password_input_component/password_input_component.html.erb +71 -0
- data/app/components/better_ui/forms/password_input_component.rb +206 -0
- data/app/components/better_ui/forms/text_input_component/text_input_component.html.erb +40 -0
- data/app/components/better_ui/forms/text_input_component.rb +258 -0
- data/app/components/better_ui/forms/textarea_component/textarea_component.html.erb +40 -0
- data/app/components/better_ui/forms/textarea_component.rb +329 -0
- data/app/form_builders/better_ui/ui_form_builder.rb +467 -0
- data/app/helpers/better_ui/application_helper.rb +325 -58
- data/app/views/layouts/better_ui/application.html.erb +1 -1
- data/config/routes.rb +1 -0
- data/lib/better_ui/engine.rb +34 -5
- data/lib/better_ui/version.rb +1 -1
- data/lib/better_ui.rb +32 -5
- data/lib/generators/better_ui/install/USAGE +44 -0
- data/lib/generators/better_ui/install/install_generator.rb +87 -0
- data/lib/generators/better_ui/install/templates/better_ui_theme.css.tt +280 -0
- data/lib/tasks/better_ui_tasks.rake +39 -4
- metadata +55 -203
- data/app/components/better_ui/application/card/component.html.erb +0 -20
- data/app/components/better_ui/application/card/component.rb +0 -214
- data/app/components/better_ui/application/main/component.html.erb +0 -9
- data/app/components/better_ui/application/main/component.rb +0 -123
- data/app/components/better_ui/application/navbar/component.html.erb +0 -92
- data/app/components/better_ui/application/navbar/component.rb +0 -136
- data/app/components/better_ui/application/sidebar/component.html.erb +0 -249
- data/app/components/better_ui/application/sidebar/component.rb +0 -187
- data/app/components/better_ui/general/accordion/component.html.erb +0 -5
- data/app/components/better_ui/general/accordion/component.rb +0 -92
- data/app/components/better_ui/general/accordion/item_component.html.erb +0 -12
- data/app/components/better_ui/general/accordion/item_component.rb +0 -176
- data/app/components/better_ui/general/alert/component.html.erb +0 -32
- data/app/components/better_ui/general/alert/component.rb +0 -242
- data/app/components/better_ui/general/avatar/component.html.erb +0 -20
- data/app/components/better_ui/general/avatar/component.rb +0 -301
- data/app/components/better_ui/general/badge/component.html.erb +0 -23
- data/app/components/better_ui/general/badge/component.rb +0 -248
- data/app/components/better_ui/general/breadcrumb/component.html.erb +0 -15
- data/app/components/better_ui/general/breadcrumb/component.rb +0 -187
- data/app/components/better_ui/general/button/component.html.erb +0 -34
- data/app/components/better_ui/general/button/component.rb +0 -214
- data/app/components/better_ui/general/divider/component.html.erb +0 -10
- data/app/components/better_ui/general/divider/component.rb +0 -226
- data/app/components/better_ui/general/dropdown/component.html.erb +0 -28
- data/app/components/better_ui/general/dropdown/component.rb +0 -192
- data/app/components/better_ui/general/dropdown/divider_component.html.erb +0 -1
- data/app/components/better_ui/general/dropdown/divider_component.rb +0 -41
- data/app/components/better_ui/general/dropdown/item_component.html.erb +0 -6
- data/app/components/better_ui/general/dropdown/item_component.rb +0 -119
- data/app/components/better_ui/general/field/component.html.erb +0 -27
- data/app/components/better_ui/general/field/component.rb +0 -37
- data/app/components/better_ui/general/grid/cell_component.html.erb +0 -3
- data/app/components/better_ui/general/grid/cell_component.rb +0 -390
- data/app/components/better_ui/general/grid/component.html.erb +0 -3
- data/app/components/better_ui/general/grid/component.rb +0 -301
- data/app/components/better_ui/general/heading/component.html.erb +0 -22
- data/app/components/better_ui/general/heading/component.rb +0 -257
- data/app/components/better_ui/general/icon/component.html.erb +0 -7
- data/app/components/better_ui/general/icon/component.rb +0 -240
- data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
- data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
- data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
- data/app/components/better_ui/general/input/datetime/component.rb +0 -223
- data/app/components/better_ui/general/input/pin/component.html.erb +0 -1
- data/app/components/better_ui/general/input/pin/component.rb +0 -201
- data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
- data/app/components/better_ui/general/input/radio/component.rb +0 -230
- data/app/components/better_ui/general/input/rating/component.html.erb +0 -4
- data/app/components/better_ui/general/input/rating/component.rb +0 -272
- data/app/components/better_ui/general/input/select/component.html.erb +0 -78
- data/app/components/better_ui/general/input/select/component.rb +0 -249
- data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
- data/app/components/better_ui/general/input/select/select_component.rb +0 -37
- data/app/components/better_ui/general/input/text/component.html.erb +0 -5
- data/app/components/better_ui/general/input/text/component.rb +0 -171
- data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
- data/app/components/better_ui/general/input/textarea/component.rb +0 -166
- data/app/components/better_ui/general/input/toggle/component.html.erb +0 -5
- data/app/components/better_ui/general/input/toggle/component.rb +0 -242
- data/app/components/better_ui/general/link/component.html.erb +0 -18
- data/app/components/better_ui/general/link/component.rb +0 -258
- data/app/components/better_ui/general/modal/component.html.erb +0 -5
- data/app/components/better_ui/general/modal/component.rb +0 -47
- data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
- data/app/components/better_ui/general/modal/modal_component.rb +0 -160
- data/app/components/better_ui/general/pagination/component.html.erb +0 -85
- data/app/components/better_ui/general/pagination/component.rb +0 -216
- data/app/components/better_ui/general/panel/component.html.erb +0 -28
- data/app/components/better_ui/general/panel/component.rb +0 -249
- data/app/components/better_ui/general/progress/component.html.erb +0 -11
- data/app/components/better_ui/general/progress/component.rb +0 -160
- data/app/components/better_ui/general/spinner/component.html.erb +0 -35
- data/app/components/better_ui/general/spinner/component.rb +0 -93
- data/app/components/better_ui/general/table/component.html.erb +0 -5
- data/app/components/better_ui/general/table/component.rb +0 -217
- data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tbody_component.rb +0 -30
- data/app/components/better_ui/general/table/td_component.html.erb +0 -3
- data/app/components/better_ui/general/table/td_component.rb +0 -44
- data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
- data/app/components/better_ui/general/table/th_component.html.erb +0 -6
- data/app/components/better_ui/general/table/th_component.rb +0 -51
- data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
- data/app/components/better_ui/general/table/thead_component.rb +0 -28
- data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tr_component.rb +0 -30
- data/app/components/better_ui/general/tabs/component.html.erb +0 -11
- data/app/components/better_ui/general/tabs/component.rb +0 -120
- data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
- data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
- data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
- data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
- data/app/components/better_ui/general/tag/component.html.erb +0 -3
- data/app/components/better_ui/general/tag/component.rb +0 -104
- data/app/components/better_ui/general/text/component.html.erb +0 -1
- data/app/components/better_ui/general/text/component.rb +0 -194
- data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
- data/app/components/better_ui/general/tooltip/component.rb +0 -239
- data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
- data/app/helpers/better_ui/application/components/card.rb +0 -11
- data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
- data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
- data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
- data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
- data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
- data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
- data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
- data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
- data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
- data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
- data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
- data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -88
- data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -68
- data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
- data/app/helpers/better_ui/general/components/grid/grid_helper.rb +0 -145
- data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
- data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
- data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
- data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
- data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +0 -76
- data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
- data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
- data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +0 -70
- data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -86
- data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
- data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
- data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +0 -77
- data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
- data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
- data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
- data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
- data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
- data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
- data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
- data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
- data/app/helpers/better_ui/general/components/text/text_helper.rb +0 -83
- data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
- data/app/jobs/better_ui/application_job.rb +0 -4
- data/app/mailers/better_ui/application_mailer.rb +0 -6
- data/config/initializers/lookbook.rb +0 -23
- 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>
|