better_ui 0.7.2 → 0.9.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 +86 -29
- data/app/components/better_ui/application_component.rb +34 -0
- data/app/components/better_ui/avatar_component/avatar_component.html.erb +15 -0
- data/app/components/better_ui/avatar_component.rb +227 -0
- data/app/components/better_ui/badge_component/badge_component.html.erb +16 -0
- data/app/components/better_ui/badge_component.rb +114 -0
- data/app/components/better_ui/breadcrumb/breadcrumb_component/breadcrumb_component.html.erb +12 -0
- data/app/components/better_ui/breadcrumb/breadcrumb_component.rb +148 -0
- data/app/components/better_ui/breadcrumb/item_component/item_component.html.erb +11 -0
- data/app/components/better_ui/breadcrumb/item_component.rb +78 -0
- data/app/components/better_ui/card_component.rb +45 -11
- data/app/components/better_ui/concerns/inline_label_styles.rb +137 -0
- data/app/components/better_ui/container_component/container_component.html.erb +3 -0
- data/app/components/better_ui/container_component.rb +143 -0
- data/app/components/better_ui/dialog/alert_component/alert_component.html.erb +61 -0
- data/app/components/better_ui/dialog/alert_component.rb +78 -0
- data/app/components/better_ui/dialog/confirm_component/confirm_component.html.erb +67 -0
- data/app/components/better_ui/dialog/confirm_component.rb +80 -0
- data/app/components/better_ui/dialog/dialog_component/dialog_component.html.erb +44 -0
- data/app/components/better_ui/dialog/dialog_component.rb +81 -0
- data/app/components/better_ui/divider_component/divider_component.html.erb +11 -0
- data/app/components/better_ui/divider_component.rb +344 -0
- data/app/components/better_ui/drawer/sidebar_component.rb +1 -0
- data/app/components/better_ui/dropdown/divider_component/divider_component.html.erb +1 -0
- data/app/components/better_ui/dropdown/divider_component.rb +20 -0
- data/app/components/better_ui/dropdown/dropdown_component/dropdown_component.html.erb +19 -0
- data/app/components/better_ui/dropdown/dropdown_component.rb +108 -0
- data/app/components/better_ui/dropdown/header_component/header_component.html.erb +3 -0
- data/app/components/better_ui/dropdown/header_component.rb +25 -0
- data/app/components/better_ui/dropdown/item_component/item_component.html.erb +7 -0
- data/app/components/better_ui/dropdown/item_component.rb +97 -0
- data/app/components/better_ui/fa_icon_component/fa_icon_component.html.erb +1 -0
- data/app/components/better_ui/fa_icon_component.rb +165 -0
- data/app/components/better_ui/forms/base_component.rb +3 -1
- data/app/components/better_ui/forms/select_component/select_component.html.erb +86 -0
- data/app/components/better_ui/forms/select_component.rb +347 -0
- data/app/components/better_ui/forms/text_input_component.rb +24 -2
- data/app/components/better_ui/heading_component/heading_component.html.erb +11 -0
- data/app/components/better_ui/heading_component.rb +259 -0
- data/app/components/better_ui/link_component/link_component.html.erb +5 -0
- data/app/components/better_ui/link_component.rb +169 -0
- data/app/components/better_ui/progress_component/progress_component.html.erb +15 -0
- data/app/components/better_ui/progress_component.rb +98 -0
- data/app/components/better_ui/spinner_component/spinner_component.html.erb +11 -0
- data/app/components/better_ui/spinner_component.rb +70 -0
- data/app/components/better_ui/table/cell_component/cell_component.html.erb +3 -0
- data/app/components/better_ui/table/cell_component.rb +84 -0
- data/app/components/better_ui/table/column_component.rb +75 -0
- data/app/components/better_ui/table/header_cell_component/header_cell_component.html.erb +18 -0
- data/app/components/better_ui/table/header_cell_component.rb +138 -0
- data/app/components/better_ui/table/header_component/header_component.html.erb +5 -0
- data/app/components/better_ui/table/header_component.rb +37 -0
- data/app/components/better_ui/table/row_component/row_component.html.erb +5 -0
- data/app/components/better_ui/table/row_component.rb +88 -0
- data/app/components/better_ui/table/table_component/table_component.html.erb +90 -0
- data/app/components/better_ui/table/table_component.rb +467 -0
- data/app/components/better_ui/tabs/container_component/container_component.html.erb +40 -0
- data/app/components/better_ui/tabs/container_component.rb +428 -0
- data/app/components/better_ui/tabs/panel_component/panel_component.html.erb +3 -0
- data/app/components/better_ui/tabs/panel_component.rb +105 -0
- data/app/components/better_ui/tabs/tab_component/tab_component.html.erb +9 -0
- data/app/components/better_ui/tabs/tab_component.rb +316 -0
- data/app/components/better_ui/tag_component/tag_component.html.erb +33 -0
- data/app/components/better_ui/tag_component.rb +114 -0
- data/app/components/better_ui/tooltip_component/tooltip_component.html.erb +11 -0
- data/app/components/better_ui/tooltip_component.rb +154 -0
- data/app/form_builders/better_ui/ui_form_builder.rb +90 -0
- data/app/helpers/better_ui/application_helper.rb +575 -0
- data/lib/better_ui/engine.rb +7 -0
- data/lib/better_ui/version.rb +1 -1
- metadata +63 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c218661dc93efb93042ba9a4a5277091a7dac3de216acc162d50c8d36077bc8
|
|
4
|
+
data.tar.gz: b06ea500b4d3c547249664dcee953910457263029549d77af2a3314cef45ef58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 238ed43c059619f7f8ab17ffcc28aa6e6d461b1de72a7bfe57a9a188da44e4f810381ad2c0aac5118b79013855f985635f4bbc01355558640a7f3cb377695fd9
|
|
7
|
+
data.tar.gz: 5a8971012b178358d34198642446a423da9ccee69f6418f6d2cc3bc77529bb96a4185e5f281d14b0a6d83fd5195cc20255e61664c7fa4899ef2c06758b63b04e
|
data/README.md
CHANGED
|
@@ -44,13 +44,9 @@ registerControllers(application)
|
|
|
44
44
|
/* @import "@pandev-srl/better-ui/utilities"; */
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
**Start using components** - `bui_*` helpers are available automatically:
|
|
47
48
|
```erb
|
|
48
|
-
|
|
49
|
-
<%= render BetterUi::ButtonComponent.new(
|
|
50
|
-
label: "Get Started",
|
|
51
|
-
variant: "primary",
|
|
52
|
-
size: "lg"
|
|
53
|
-
) %>
|
|
49
|
+
<%= bui_button(label: "Get Started", variant: :primary, size: :lg) %>
|
|
54
50
|
```
|
|
55
51
|
|
|
56
52
|
## Features
|
|
@@ -59,6 +55,7 @@ registerControllers(application)
|
|
|
59
55
|
- **ViewComponent Architecture**: Encapsulated, testable, and reusable components
|
|
60
56
|
- **Tailwind CSS v4**: Leverages the latest Tailwind features with OKLCH color space
|
|
61
57
|
- **Fully Customizable**: 9 semantic color variants with complete theme control
|
|
58
|
+
- **View Helpers**: Concise `bui_*` helpers available automatically (no setup required)
|
|
62
59
|
- **Form Builder Integration**: Seamless integration with Rails forms via `UiFormBuilder`
|
|
63
60
|
- **Stimulus Controllers**: Interactive components with built-in JavaScript behaviors
|
|
64
61
|
- **Accessible by Default**: ARIA attributes and keyboard navigation support
|
|
@@ -72,7 +69,7 @@ For detailed installation and configuration instructions, see the [Installation
|
|
|
72
69
|
|
|
73
70
|
- Rails 8.1.1 or higher
|
|
74
71
|
- Node.js and npm (for Tailwind CSS)
|
|
75
|
-
- Tailwind CSS v4
|
|
72
|
+
- Tailwind CSS v4
|
|
76
73
|
|
|
77
74
|
## Component Overview
|
|
78
75
|
|
|
@@ -86,11 +83,7 @@ A versatile button component with multiple styles, sizes, and states.
|
|
|
86
83
|
- **Features**: Loading states, icons, disabled states
|
|
87
84
|
|
|
88
85
|
```erb
|
|
89
|
-
<%=
|
|
90
|
-
label: "Save Changes",
|
|
91
|
-
variant: "success",
|
|
92
|
-
style: "solid"
|
|
93
|
-
) do |c| %>
|
|
86
|
+
<%= bui_button(label: "Save Changes", variant: :success, style: :solid) do |c| %>
|
|
94
87
|
<% c.with_icon_before { "💾" } %>
|
|
95
88
|
<% end %>
|
|
96
89
|
```
|
|
@@ -103,7 +96,7 @@ A flexible container component with customizable padding and optional slots.
|
|
|
103
96
|
- **Slots**: header, body, footer
|
|
104
97
|
|
|
105
98
|
```erb
|
|
106
|
-
<%=
|
|
99
|
+
<%= bui_card(size: :lg, style: :bordered) do |c| %>
|
|
107
100
|
<% c.with_header { "Card Title" } %>
|
|
108
101
|
<% c.with_body { "Card content goes here" } %>
|
|
109
102
|
<% c.with_footer { "Footer content" } %>
|
|
@@ -117,8 +110,8 @@ Display notifications, alerts, and validation messages with style.
|
|
|
117
110
|
- **Features**: Dismissible, auto-dismiss, titles, icons
|
|
118
111
|
|
|
119
112
|
```erb
|
|
120
|
-
<%=
|
|
121
|
-
variant:
|
|
113
|
+
<%= bui_action_messages(
|
|
114
|
+
variant: :danger,
|
|
122
115
|
title: "Validation Errors",
|
|
123
116
|
messages: @model.errors.full_messages,
|
|
124
117
|
dismissible: true,
|
|
@@ -132,7 +125,7 @@ Display notifications, alerts, and validation messages with style.
|
|
|
132
125
|
Standard text input with error handling and icon support.
|
|
133
126
|
|
|
134
127
|
```erb
|
|
135
|
-
<%=
|
|
128
|
+
<%= bui_text_input(
|
|
136
129
|
name: "user[email]",
|
|
137
130
|
label: "Email Address",
|
|
138
131
|
hint: "We'll never share your email",
|
|
@@ -146,7 +139,7 @@ Standard text input with error handling and icon support.
|
|
|
146
139
|
Numeric input with min/max validation and optional spinners.
|
|
147
140
|
|
|
148
141
|
```erb
|
|
149
|
-
<%=
|
|
142
|
+
<%= bui_number_input(
|
|
150
143
|
name: "product[price]",
|
|
151
144
|
label: "Price",
|
|
152
145
|
min: 0,
|
|
@@ -161,7 +154,7 @@ Numeric input with min/max validation and optional spinners.
|
|
|
161
154
|
Password field with visibility toggle functionality.
|
|
162
155
|
|
|
163
156
|
```erb
|
|
164
|
-
<%=
|
|
157
|
+
<%= bui_password_input(
|
|
165
158
|
name: "user[password]",
|
|
166
159
|
label: "Password",
|
|
167
160
|
hint: "Minimum 8 characters"
|
|
@@ -172,7 +165,7 @@ Password field with visibility toggle functionality.
|
|
|
172
165
|
Multi-line text input with resizing options.
|
|
173
166
|
|
|
174
167
|
```erb
|
|
175
|
-
<%=
|
|
168
|
+
<%= bui_textarea(
|
|
176
169
|
name: "post[content]",
|
|
177
170
|
label: "Content",
|
|
178
171
|
rows: 6,
|
|
@@ -185,7 +178,7 @@ Multi-line text input with resizing options.
|
|
|
185
178
|
Single checkbox with color variants and label positioning.
|
|
186
179
|
|
|
187
180
|
```erb
|
|
188
|
-
<%=
|
|
181
|
+
<%= bui_checkbox(
|
|
189
182
|
name: "user[terms]",
|
|
190
183
|
label: "I agree to the terms and conditions",
|
|
191
184
|
variant: :primary
|
|
@@ -196,7 +189,7 @@ Single checkbox with color variants and label positioning.
|
|
|
196
189
|
Multiple checkboxes for selecting from a collection.
|
|
197
190
|
|
|
198
191
|
```erb
|
|
199
|
-
<%=
|
|
192
|
+
<%= bui_checkbox_group(
|
|
200
193
|
name: "user[roles]",
|
|
201
194
|
collection: [["Admin", "admin"], ["Editor", "editor"]],
|
|
202
195
|
legend: "User Roles",
|
|
@@ -215,13 +208,13 @@ BetterUi provides a complete drawer layout system for building responsive admin
|
|
|
215
208
|
- **NavGroupComponent**: Grouped navigation with titles
|
|
216
209
|
|
|
217
210
|
```erb
|
|
218
|
-
<%=
|
|
211
|
+
<%= bui_drawer_layout do |layout| %>
|
|
219
212
|
<% layout.with_header do |header| %>
|
|
220
213
|
<% header.with_logo { "MyApp" } %>
|
|
221
214
|
<% end %>
|
|
222
215
|
<% layout.with_sidebar do |sidebar| %>
|
|
223
216
|
<% sidebar.with_navigation do %>
|
|
224
|
-
<%=
|
|
217
|
+
<%= bui_drawer_nav_group(title: "Menu") do |group| %>
|
|
225
218
|
<% group.with_item(label: "Dashboard", href: "/", active: true) %>
|
|
226
219
|
<% group.with_item(label: "Settings", href: "/settings") %>
|
|
227
220
|
<% end %>
|
|
@@ -237,16 +230,80 @@ BetterUi includes a custom form builder for seamless Rails form integration:
|
|
|
237
230
|
|
|
238
231
|
```erb
|
|
239
232
|
<%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
|
|
240
|
-
<%= f.
|
|
241
|
-
<%= f.
|
|
242
|
-
<%= f.
|
|
243
|
-
<%= f.
|
|
244
|
-
<%= f.
|
|
233
|
+
<%= f.bui_text_input :name %>
|
|
234
|
+
<%= f.bui_text_input :email, hint: "We'll never share your email" %>
|
|
235
|
+
<%= f.bui_password_input :password %>
|
|
236
|
+
<%= f.bui_textarea :bio, rows: 6 %>
|
|
237
|
+
<%= f.bui_number_input :age, min: 0, max: 120 %>
|
|
238
|
+
<%= f.bui_select :role, [["Admin", "admin"], ["Editor", "editor"]] %>
|
|
245
239
|
<%= f.bui_checkbox :newsletter, label: "Subscribe to newsletter" %>
|
|
246
240
|
<%= f.bui_checkbox_group :roles, [["Admin", "admin"], ["Editor", "editor"]] %>
|
|
247
241
|
<% end %>
|
|
248
242
|
```
|
|
249
243
|
|
|
244
|
+
## Available View Helpers
|
|
245
|
+
|
|
246
|
+
### Core Components
|
|
247
|
+
|
|
248
|
+
| Helper | Component |
|
|
249
|
+
|--------|-----------|
|
|
250
|
+
| `bui_button` | ButtonComponent |
|
|
251
|
+
| `bui_link` | LinkComponent |
|
|
252
|
+
| `bui_card` | CardComponent |
|
|
253
|
+
| `bui_action_messages` | ActionMessagesComponent |
|
|
254
|
+
| `bui_avatar` | AvatarComponent |
|
|
255
|
+
| `bui_badge` | BadgeComponent |
|
|
256
|
+
| `bui_tag` | TagComponent |
|
|
257
|
+
| `bui_heading` | HeadingComponent |
|
|
258
|
+
| `bui_spinner` | SpinnerComponent |
|
|
259
|
+
| `bui_progress` | ProgressComponent |
|
|
260
|
+
| `bui_divider` | DividerComponent |
|
|
261
|
+
| `bui_tooltip` | TooltipComponent |
|
|
262
|
+
| `bui_container` | ContainerComponent |
|
|
263
|
+
| `bui_fa_icon` | FaIconComponent |
|
|
264
|
+
| `bui_breadcrumb` | Breadcrumb::BreadcrumbComponent |
|
|
265
|
+
|
|
266
|
+
### Form Components
|
|
267
|
+
|
|
268
|
+
| Helper | Component |
|
|
269
|
+
|--------|-----------|
|
|
270
|
+
| `bui_text_input` | Forms::TextInputComponent |
|
|
271
|
+
| `bui_email_input` | Forms::TextInputComponent (type: :email) |
|
|
272
|
+
| `bui_tel_input` | Forms::TextInputComponent (type: :tel) |
|
|
273
|
+
| `bui_date_input` | Forms::TextInputComponent (type: :date) |
|
|
274
|
+
| `bui_time_input` | Forms::TextInputComponent (type: :time) |
|
|
275
|
+
| `bui_number_input` | Forms::NumberInputComponent |
|
|
276
|
+
| `bui_password_input` | Forms::PasswordInputComponent |
|
|
277
|
+
| `bui_textarea` | Forms::TextareaComponent |
|
|
278
|
+
| `bui_checkbox` | Forms::CheckboxComponent |
|
|
279
|
+
| `bui_checkbox_group` | Forms::CheckboxGroupComponent |
|
|
280
|
+
| `bui_select` | Forms::SelectComponent |
|
|
281
|
+
|
|
282
|
+
### Layout & Navigation
|
|
283
|
+
|
|
284
|
+
| Helper | Component |
|
|
285
|
+
|--------|-----------|
|
|
286
|
+
| `bui_drawer_layout` | Drawer::LayoutComponent |
|
|
287
|
+
| `bui_drawer_sidebar` | Drawer::SidebarComponent |
|
|
288
|
+
| `bui_drawer_header` | Drawer::HeaderComponent |
|
|
289
|
+
| `bui_drawer_nav_item` | Drawer::NavItemComponent |
|
|
290
|
+
| `bui_drawer_nav_group` | Drawer::NavGroupComponent |
|
|
291
|
+
|
|
292
|
+
### Interactive Components
|
|
293
|
+
|
|
294
|
+
| Helper | Component |
|
|
295
|
+
|--------|-----------|
|
|
296
|
+
| `bui_dialog` | Dialog::DialogComponent |
|
|
297
|
+
| `bui_dialog_alert` | Dialog::AlertComponent |
|
|
298
|
+
| `bui_dialog_confirm` | Dialog::ConfirmComponent |
|
|
299
|
+
| `bui_dropdown` | Dropdown::DropdownComponent |
|
|
300
|
+
| `bui_tabs` | Tabs::ContainerComponent |
|
|
301
|
+
| `bui_tab` | Tabs::TabComponent |
|
|
302
|
+
| `bui_tab_panel` | Tabs::PanelComponent |
|
|
303
|
+
| `bui_table` | Table::TableComponent |
|
|
304
|
+
|
|
305
|
+
> **Note**: You can also use ViewComponent directly with `render BetterUi::*Component.new(...)` if you prefer the explicit rendering syntax.
|
|
306
|
+
|
|
250
307
|
## Documentation
|
|
251
308
|
|
|
252
309
|
- [**Installation Guide**](doc/INSTALLATION.md) - Detailed setup and configuration instructions
|
|
@@ -343,4 +400,4 @@ Powered by:
|
|
|
343
400
|
|
|
344
401
|
## Acknowledgments
|
|
345
402
|
|
|
346
|
-
Special thanks to the Ruby on Rails community and all contributors who help make BetterUi better.
|
|
403
|
+
Special thanks to the Ruby on Rails community and all contributors who help make BetterUi better.
|
|
@@ -49,8 +49,42 @@ module BetterUi
|
|
|
49
49
|
dark: 900 # Dark backgrounds and dark text
|
|
50
50
|
}.freeze
|
|
51
51
|
|
|
52
|
+
# Shadow size definitions mapping to Tailwind shadow classes.
|
|
53
|
+
# Used across all components for consistent elevation styling.
|
|
54
|
+
#
|
|
55
|
+
# @example Usage in component
|
|
56
|
+
# SHADOWS[@shadow] # => "shadow-sm"
|
|
57
|
+
SHADOWS = {
|
|
58
|
+
none: nil,
|
|
59
|
+
sm: "shadow-sm",
|
|
60
|
+
md: "shadow-md",
|
|
61
|
+
lg: "shadow-lg",
|
|
62
|
+
xl: "shadow-xl"
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
52
65
|
private
|
|
53
66
|
|
|
67
|
+
# Normalizes a shadow parameter value.
|
|
68
|
+
# Accepts Symbol sizes (:sm, :md, etc.), booleans for backward compatibility,
|
|
69
|
+
# or false/nil to disable shadows.
|
|
70
|
+
#
|
|
71
|
+
# @param value [Symbol, Boolean] the shadow value to normalize
|
|
72
|
+
# @param default [Symbol] the default shadow size (used when value is true)
|
|
73
|
+
# @return [Symbol] normalized shadow key
|
|
74
|
+
def normalize_shadow(value, default: :sm)
|
|
75
|
+
case value
|
|
76
|
+
when false, nil, :none then :none
|
|
77
|
+
when true then default
|
|
78
|
+
when Symbol
|
|
79
|
+
unless SHADOWS.key?(value)
|
|
80
|
+
raise ArgumentError, "Invalid shadow: #{value}. Must be one of: #{SHADOWS.keys.join(', ')}"
|
|
81
|
+
end
|
|
82
|
+
value
|
|
83
|
+
else
|
|
84
|
+
raise ArgumentError, "Invalid shadow: #{value}. Must be a Symbol or Boolean"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
54
88
|
# Helper to merge CSS classes intelligently using TailwindMerge
|
|
55
89
|
# Resolves conflicting Tailwind utility classes
|
|
56
90
|
#
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<div class="relative inline-flex <%= @container_classes %>">
|
|
2
|
+
<% if @src %>
|
|
3
|
+
<img src="<%= @src %>" alt="<%= alt_text %>" class="<%= component_classes %> object-cover" />
|
|
4
|
+
<% else %>
|
|
5
|
+
<div class="<%= component_classes %> flex items-center justify-center font-medium">
|
|
6
|
+
<%= initials %>
|
|
7
|
+
</div>
|
|
8
|
+
<% end %>
|
|
9
|
+
<% if @status %>
|
|
10
|
+
<span class="absolute bottom-0 right-0 block <%= status_classes %> rounded-full ring-2 ring-white"></span>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% if badge? %>
|
|
13
|
+
<span class="absolute -top-1 -right-1"><%= badge %></span>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
# An avatar component for displaying user profile images or initials.
|
|
5
|
+
#
|
|
6
|
+
# Supports image display, initials fallback from a name, multiple sizes,
|
|
7
|
+
# shapes, color variants, status indicators, and a badge slot.
|
|
8
|
+
#
|
|
9
|
+
# @example With image
|
|
10
|
+
# <%= render BetterUi::AvatarComponent.new(src: user.avatar_url, alt: user.name) %>
|
|
11
|
+
#
|
|
12
|
+
# @example With initials fallback
|
|
13
|
+
# <%= render BetterUi::AvatarComponent.new(name: "John Doe", variant: :primary) %>
|
|
14
|
+
#
|
|
15
|
+
# @example With status indicator
|
|
16
|
+
# <%= render BetterUi::AvatarComponent.new(
|
|
17
|
+
# src: user.avatar_url,
|
|
18
|
+
# name: user.name,
|
|
19
|
+
# status: :online,
|
|
20
|
+
# size: :lg
|
|
21
|
+
# ) %>
|
|
22
|
+
#
|
|
23
|
+
# @example With badge slot
|
|
24
|
+
# <%= render BetterUi::AvatarComponent.new(src: user.avatar_url) do |avatar| %>
|
|
25
|
+
# <% avatar.with_badge do %>
|
|
26
|
+
# <span class="bg-red-500 text-white text-xs rounded-full w-5 h-5 flex items-center justify-center">3</span>
|
|
27
|
+
# <% end %>
|
|
28
|
+
# <% end %>
|
|
29
|
+
class AvatarComponent < ApplicationComponent
|
|
30
|
+
# Size configurations mapping to Tailwind dimension and text classes
|
|
31
|
+
SIZES = {
|
|
32
|
+
xs: "w-6 h-6 text-xs",
|
|
33
|
+
sm: "w-8 h-8 text-sm",
|
|
34
|
+
md: "w-10 h-10 text-base",
|
|
35
|
+
lg: "w-14 h-14 text-lg",
|
|
36
|
+
xl: "w-20 h-20 text-xl"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
# Shape configurations mapping to Tailwind border-radius classes
|
|
40
|
+
SHAPES = {
|
|
41
|
+
circle: "rounded-full",
|
|
42
|
+
square: "rounded-none",
|
|
43
|
+
rounded: "rounded-lg"
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
# Status indicator colors
|
|
47
|
+
STATUSES = %i[online offline busy away].freeze
|
|
48
|
+
|
|
49
|
+
# Status dot size classes scaled by avatar size
|
|
50
|
+
STATUS_DOT_SIZES = {
|
|
51
|
+
xs: "w-2 h-2",
|
|
52
|
+
sm: "w-2 h-2",
|
|
53
|
+
md: "w-2.5 h-2.5",
|
|
54
|
+
lg: "w-3 h-3",
|
|
55
|
+
xl: "w-4 h-4"
|
|
56
|
+
}.freeze
|
|
57
|
+
|
|
58
|
+
# @!method with_badge
|
|
59
|
+
# Slot for rendering an optional badge overlay (e.g., notification count).
|
|
60
|
+
# Positioned at top-right of the avatar.
|
|
61
|
+
# @yieldreturn [String] the HTML content for the badge
|
|
62
|
+
renders_one :badge
|
|
63
|
+
|
|
64
|
+
# Initializes a new avatar component.
|
|
65
|
+
#
|
|
66
|
+
# @param src [String, nil] URL of the avatar image
|
|
67
|
+
# @param alt [String, nil] Alt text for the image (falls back to name)
|
|
68
|
+
# @param name [String, nil] Full name used to generate initials fallback
|
|
69
|
+
# @param variant [Symbol] Color variant for initials background
|
|
70
|
+
# @param size [Symbol] Avatar size (:xs, :sm, :md, :lg, :xl)
|
|
71
|
+
# @param shape [Symbol] Avatar shape (:circle, :square, :rounded)
|
|
72
|
+
# @param status [Symbol, nil] Status indicator (:online, :offline, :busy, :away)
|
|
73
|
+
# @param container_classes [String, nil] Additional CSS classes for the outer wrapper
|
|
74
|
+
# @param options [Hash] Additional HTML attributes
|
|
75
|
+
#
|
|
76
|
+
# @raise [ArgumentError] if variant, size, shape, or status is invalid
|
|
77
|
+
def initialize(
|
|
78
|
+
src: nil,
|
|
79
|
+
alt: nil,
|
|
80
|
+
name: nil,
|
|
81
|
+
variant: :primary,
|
|
82
|
+
size: :md,
|
|
83
|
+
shape: :circle,
|
|
84
|
+
status: nil,
|
|
85
|
+
container_classes: nil,
|
|
86
|
+
**options
|
|
87
|
+
)
|
|
88
|
+
@src = src
|
|
89
|
+
@alt = alt
|
|
90
|
+
@name = name
|
|
91
|
+
@variant = validate_variant(variant)
|
|
92
|
+
@size = validate_size(size)
|
|
93
|
+
@shape = validate_shape(shape)
|
|
94
|
+
@status = validate_status(status) if status
|
|
95
|
+
@container_classes = container_classes
|
|
96
|
+
@options = options
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
attr_reader :src, :alt, :name, :variant, :size, :shape, :status,
|
|
102
|
+
:container_classes, :options
|
|
103
|
+
|
|
104
|
+
# Returns the combined CSS classes for the avatar element (img or div).
|
|
105
|
+
#
|
|
106
|
+
# @return [String] merged CSS class string
|
|
107
|
+
# @api private
|
|
108
|
+
def component_classes
|
|
109
|
+
css_classes(
|
|
110
|
+
SIZES[@size],
|
|
111
|
+
SHAPES[@shape],
|
|
112
|
+
variant_classes,
|
|
113
|
+
@container_classes
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns variant-specific background and text color classes for initials mode.
|
|
118
|
+
# When an image is provided, no variant background is needed.
|
|
119
|
+
#
|
|
120
|
+
# @return [String, nil] variant CSS classes or nil
|
|
121
|
+
# @api private
|
|
122
|
+
def variant_classes
|
|
123
|
+
return nil if @src
|
|
124
|
+
|
|
125
|
+
case @variant
|
|
126
|
+
when :primary then "bg-primary-100 text-primary-700"
|
|
127
|
+
when :secondary then "bg-secondary-100 text-secondary-700"
|
|
128
|
+
when :accent then "bg-accent-100 text-accent-700"
|
|
129
|
+
when :success then "bg-success-100 text-success-700"
|
|
130
|
+
when :danger then "bg-danger-100 text-danger-700"
|
|
131
|
+
when :warning then "bg-warning-100 text-warning-700"
|
|
132
|
+
when :info then "bg-info-100 text-info-700"
|
|
133
|
+
when :light then "bg-grayscale-100 text-grayscale-700"
|
|
134
|
+
when :dark then "bg-grayscale-800 text-grayscale-100"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns CSS classes for the status indicator dot.
|
|
139
|
+
#
|
|
140
|
+
# @return [String] combined status color and size classes
|
|
141
|
+
# @api private
|
|
142
|
+
def status_classes
|
|
143
|
+
color = case @status
|
|
144
|
+
when :online then "bg-success-500"
|
|
145
|
+
when :offline then "bg-grayscale-400"
|
|
146
|
+
when :busy then "bg-danger-500"
|
|
147
|
+
when :away then "bg-warning-500"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
"#{color} #{STATUS_DOT_SIZES[@size]}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Extracts initials from the name.
|
|
154
|
+
# Takes the first letter of the first word and the first letter of the second word (if present).
|
|
155
|
+
#
|
|
156
|
+
# @return [String] uppercase initials (e.g., "JD" for "John Doe", "A" for "Alice")
|
|
157
|
+
# @api private
|
|
158
|
+
def initials
|
|
159
|
+
return "" unless @name
|
|
160
|
+
|
|
161
|
+
parts = @name.strip.split(/\s+/)
|
|
162
|
+
result = parts[0][0].to_s
|
|
163
|
+
result += parts[1][0].to_s if parts.length > 1
|
|
164
|
+
result.upcase
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Returns the alt text for the image, falling back to name.
|
|
168
|
+
#
|
|
169
|
+
# @return [String, nil] alt text
|
|
170
|
+
# @api private
|
|
171
|
+
def alt_text
|
|
172
|
+
@alt || @name
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Validates the variant parameter.
|
|
176
|
+
#
|
|
177
|
+
# @param variant [Symbol] the variant to validate
|
|
178
|
+
# @return [Symbol] the validated variant
|
|
179
|
+
# @raise [ArgumentError] if variant is invalid
|
|
180
|
+
# @api private
|
|
181
|
+
def validate_variant(variant)
|
|
182
|
+
unless BetterUi::ApplicationComponent::VARIANTS.key?(variant)
|
|
183
|
+
raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{BetterUi::ApplicationComponent::VARIANTS.keys.join(', ')}"
|
|
184
|
+
end
|
|
185
|
+
variant
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Validates the size parameter.
|
|
189
|
+
#
|
|
190
|
+
# @param size [Symbol] the size to validate
|
|
191
|
+
# @return [Symbol] the validated size
|
|
192
|
+
# @raise [ArgumentError] if size is invalid
|
|
193
|
+
# @api private
|
|
194
|
+
def validate_size(size)
|
|
195
|
+
unless SIZES.key?(size)
|
|
196
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.keys.join(', ')}"
|
|
197
|
+
end
|
|
198
|
+
size
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Validates the shape parameter.
|
|
202
|
+
#
|
|
203
|
+
# @param shape [Symbol] the shape to validate
|
|
204
|
+
# @return [Symbol] the validated shape
|
|
205
|
+
# @raise [ArgumentError] if shape is invalid
|
|
206
|
+
# @api private
|
|
207
|
+
def validate_shape(shape)
|
|
208
|
+
unless SHAPES.key?(shape)
|
|
209
|
+
raise ArgumentError, "Invalid shape: #{shape}. Must be one of: #{SHAPES.keys.join(', ')}"
|
|
210
|
+
end
|
|
211
|
+
shape
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Validates the status parameter.
|
|
215
|
+
#
|
|
216
|
+
# @param status [Symbol] the status to validate
|
|
217
|
+
# @return [Symbol] the validated status
|
|
218
|
+
# @raise [ArgumentError] if status is invalid
|
|
219
|
+
# @api private
|
|
220
|
+
def validate_status(status)
|
|
221
|
+
unless STATUSES.include?(status)
|
|
222
|
+
raise ArgumentError, "Invalid status: #{status}. Must be one of: #{STATUSES.join(', ')}"
|
|
223
|
+
end
|
|
224
|
+
status
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<span <%= tag.attributes(component_attributes) %> class="<%= component_classes %>">
|
|
2
|
+
<% if dot %>
|
|
3
|
+
<span class="w-2 h-2 rounded-full <%= dot_classes %>"></span>
|
|
4
|
+
<% else %>
|
|
5
|
+
<% if icon_before? %>
|
|
6
|
+
<span class="inline-flex items-center <%= SIZES[size][:icon] %>">
|
|
7
|
+
<%= icon_before %>
|
|
8
|
+
</span>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% if counter %>
|
|
11
|
+
<%= counter %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<%= content %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<% end %>
|
|
16
|
+
</span>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
class BadgeComponent < ApplicationComponent
|
|
5
|
+
include BetterUi::Concerns::InlineLabelStyles
|
|
6
|
+
|
|
7
|
+
renders_one :icon_before
|
|
8
|
+
|
|
9
|
+
SIZES = {
|
|
10
|
+
xs: { padding: "px-1.5 py-0.5", text: "text-xs", icon: "w-3 h-3", gap: "gap-1" },
|
|
11
|
+
sm: { padding: "px-2 py-0.5", text: "text-xs", icon: "w-3.5 h-3.5", gap: "gap-1" },
|
|
12
|
+
md: { padding: "px-2.5 py-1", text: "text-sm", icon: "w-4 h-4", gap: "gap-1.5" },
|
|
13
|
+
lg: { padding: "px-3 py-1.5", text: "text-base", icon: "w-5 h-5", gap: "gap-2" }
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
STYLES = %i[solid outline soft ghost].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(
|
|
19
|
+
variant: :primary,
|
|
20
|
+
style: :solid,
|
|
21
|
+
size: :md,
|
|
22
|
+
pill: true,
|
|
23
|
+
dot: false,
|
|
24
|
+
counter: nil,
|
|
25
|
+
container_classes: nil,
|
|
26
|
+
**options
|
|
27
|
+
)
|
|
28
|
+
@variant = validate_variant(variant)
|
|
29
|
+
@style = validate_style(style)
|
|
30
|
+
@size = validate_size(size)
|
|
31
|
+
@pill = pill
|
|
32
|
+
@dot = dot
|
|
33
|
+
@counter = counter
|
|
34
|
+
@container_classes = container_classes
|
|
35
|
+
@options = options
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :variant, :style, :size, :pill, :dot, :counter, :container_classes, :options
|
|
41
|
+
|
|
42
|
+
def component_classes
|
|
43
|
+
css_classes([
|
|
44
|
+
base_classes,
|
|
45
|
+
style_classes,
|
|
46
|
+
size_classes,
|
|
47
|
+
shape_classes,
|
|
48
|
+
@container_classes
|
|
49
|
+
].flatten.compact)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def component_attributes
|
|
53
|
+
@options
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def base_classes
|
|
57
|
+
[
|
|
58
|
+
"inline-flex items-center",
|
|
59
|
+
"font-medium",
|
|
60
|
+
"transition-colors duration-200"
|
|
61
|
+
]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def shape_classes
|
|
65
|
+
@pill ? "rounded-full" : "rounded-md"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def style_classes
|
|
69
|
+
case @style
|
|
70
|
+
when :solid then solid_classes
|
|
71
|
+
when :outline then outline_classes
|
|
72
|
+
when :soft then soft_classes
|
|
73
|
+
when :ghost then ghost_classes
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def size_classes
|
|
78
|
+
size_config = SIZES[@size]
|
|
79
|
+
[
|
|
80
|
+
size_config[:padding],
|
|
81
|
+
size_config[:text],
|
|
82
|
+
size_config[:gap]
|
|
83
|
+
]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def dot_classes
|
|
87
|
+
case @variant
|
|
88
|
+
when :primary then "bg-primary-600"
|
|
89
|
+
when :secondary then "bg-secondary-600"
|
|
90
|
+
when :accent then "bg-accent-600"
|
|
91
|
+
when :success then "bg-success-600"
|
|
92
|
+
when :danger then "bg-danger-600"
|
|
93
|
+
when :warning then "bg-warning-600"
|
|
94
|
+
when :info then "bg-info-600"
|
|
95
|
+
when :light then "bg-grayscale-400"
|
|
96
|
+
when :dark then "bg-grayscale-900"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_style(style)
|
|
101
|
+
unless STYLES.include?(style)
|
|
102
|
+
raise ArgumentError, "Invalid style: #{style}. Must be one of: #{STYLES.join(", ")}"
|
|
103
|
+
end
|
|
104
|
+
style
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_size(size)
|
|
108
|
+
unless SIZES.key?(size)
|
|
109
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.keys.join(", ")}"
|
|
110
|
+
end
|
|
111
|
+
size
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<nav aria-label="Breadcrumb" class="<%= @container_classes %>">
|
|
2
|
+
<ol class="<%= component_classes %>">
|
|
3
|
+
<% items.each_with_index do |item, index| %>
|
|
4
|
+
<li class="inline-flex items-center">
|
|
5
|
+
<% if index > 0 %>
|
|
6
|
+
<span class="<%= separator_classes %>" aria-hidden="true"><%= separator_content %></span>
|
|
7
|
+
<% end %>
|
|
8
|
+
<%= item %>
|
|
9
|
+
</li>
|
|
10
|
+
<% end %>
|
|
11
|
+
</ol>
|
|
12
|
+
</nav>
|