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.
- 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 -51
- 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 -4
- 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 +52 -185
- 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 -227
- data/app/components/better_ui/application/sidebar/component.rb +0 -130
- 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 -25
- data/app/components/better_ui/general/dropdown/component.rb +0 -170
- 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/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 -239
- 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/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/select/component.html.erb +0 -16
- data/app/components/better_ui/general/input/select/component.rb +0 -184
- 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/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/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/accordion.rb +0 -11
- 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 -79
- data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -62
- data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
- 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/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/select/select_helper.rb +0 -70
- 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/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/modal.rb +0 -11
- 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/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,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
|