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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -29
  3. data/app/components/better_ui/application_component.rb +34 -0
  4. data/app/components/better_ui/avatar_component/avatar_component.html.erb +15 -0
  5. data/app/components/better_ui/avatar_component.rb +227 -0
  6. data/app/components/better_ui/badge_component/badge_component.html.erb +16 -0
  7. data/app/components/better_ui/badge_component.rb +114 -0
  8. data/app/components/better_ui/breadcrumb/breadcrumb_component/breadcrumb_component.html.erb +12 -0
  9. data/app/components/better_ui/breadcrumb/breadcrumb_component.rb +148 -0
  10. data/app/components/better_ui/breadcrumb/item_component/item_component.html.erb +11 -0
  11. data/app/components/better_ui/breadcrumb/item_component.rb +78 -0
  12. data/app/components/better_ui/card_component.rb +45 -11
  13. data/app/components/better_ui/concerns/inline_label_styles.rb +137 -0
  14. data/app/components/better_ui/container_component/container_component.html.erb +3 -0
  15. data/app/components/better_ui/container_component.rb +143 -0
  16. data/app/components/better_ui/dialog/alert_component/alert_component.html.erb +61 -0
  17. data/app/components/better_ui/dialog/alert_component.rb +78 -0
  18. data/app/components/better_ui/dialog/confirm_component/confirm_component.html.erb +67 -0
  19. data/app/components/better_ui/dialog/confirm_component.rb +80 -0
  20. data/app/components/better_ui/dialog/dialog_component/dialog_component.html.erb +44 -0
  21. data/app/components/better_ui/dialog/dialog_component.rb +81 -0
  22. data/app/components/better_ui/divider_component/divider_component.html.erb +11 -0
  23. data/app/components/better_ui/divider_component.rb +344 -0
  24. data/app/components/better_ui/drawer/sidebar_component.rb +1 -0
  25. data/app/components/better_ui/dropdown/divider_component/divider_component.html.erb +1 -0
  26. data/app/components/better_ui/dropdown/divider_component.rb +20 -0
  27. data/app/components/better_ui/dropdown/dropdown_component/dropdown_component.html.erb +19 -0
  28. data/app/components/better_ui/dropdown/dropdown_component.rb +108 -0
  29. data/app/components/better_ui/dropdown/header_component/header_component.html.erb +3 -0
  30. data/app/components/better_ui/dropdown/header_component.rb +25 -0
  31. data/app/components/better_ui/dropdown/item_component/item_component.html.erb +7 -0
  32. data/app/components/better_ui/dropdown/item_component.rb +97 -0
  33. data/app/components/better_ui/fa_icon_component/fa_icon_component.html.erb +1 -0
  34. data/app/components/better_ui/fa_icon_component.rb +165 -0
  35. data/app/components/better_ui/forms/base_component.rb +3 -1
  36. data/app/components/better_ui/forms/select_component/select_component.html.erb +86 -0
  37. data/app/components/better_ui/forms/select_component.rb +347 -0
  38. data/app/components/better_ui/forms/text_input_component.rb +24 -2
  39. data/app/components/better_ui/heading_component/heading_component.html.erb +11 -0
  40. data/app/components/better_ui/heading_component.rb +259 -0
  41. data/app/components/better_ui/link_component/link_component.html.erb +5 -0
  42. data/app/components/better_ui/link_component.rb +169 -0
  43. data/app/components/better_ui/progress_component/progress_component.html.erb +15 -0
  44. data/app/components/better_ui/progress_component.rb +98 -0
  45. data/app/components/better_ui/spinner_component/spinner_component.html.erb +11 -0
  46. data/app/components/better_ui/spinner_component.rb +70 -0
  47. data/app/components/better_ui/table/cell_component/cell_component.html.erb +3 -0
  48. data/app/components/better_ui/table/cell_component.rb +84 -0
  49. data/app/components/better_ui/table/column_component.rb +75 -0
  50. data/app/components/better_ui/table/header_cell_component/header_cell_component.html.erb +18 -0
  51. data/app/components/better_ui/table/header_cell_component.rb +138 -0
  52. data/app/components/better_ui/table/header_component/header_component.html.erb +5 -0
  53. data/app/components/better_ui/table/header_component.rb +37 -0
  54. data/app/components/better_ui/table/row_component/row_component.html.erb +5 -0
  55. data/app/components/better_ui/table/row_component.rb +88 -0
  56. data/app/components/better_ui/table/table_component/table_component.html.erb +90 -0
  57. data/app/components/better_ui/table/table_component.rb +467 -0
  58. data/app/components/better_ui/tabs/container_component/container_component.html.erb +40 -0
  59. data/app/components/better_ui/tabs/container_component.rb +428 -0
  60. data/app/components/better_ui/tabs/panel_component/panel_component.html.erb +3 -0
  61. data/app/components/better_ui/tabs/panel_component.rb +105 -0
  62. data/app/components/better_ui/tabs/tab_component/tab_component.html.erb +9 -0
  63. data/app/components/better_ui/tabs/tab_component.rb +316 -0
  64. data/app/components/better_ui/tag_component/tag_component.html.erb +33 -0
  65. data/app/components/better_ui/tag_component.rb +114 -0
  66. data/app/components/better_ui/tooltip_component/tooltip_component.html.erb +11 -0
  67. data/app/components/better_ui/tooltip_component.rb +154 -0
  68. data/app/form_builders/better_ui/ui_form_builder.rb +90 -0
  69. data/app/helpers/better_ui/application_helper.rb +575 -0
  70. data/lib/better_ui/engine.rb +7 -0
  71. data/lib/better_ui/version.rb +1 -1
  72. metadata +63 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64c0ed9d31f8392770eaedb20fe0727444877a4fa8886b6241bf70c5d093b03e
4
- data.tar.gz: bbf85b45c167057d13b403d3270eb0bf744cab5d1b2646ee3c6ed88c6cdfceca
3
+ metadata.gz: 2c218661dc93efb93042ba9a4a5277091a7dac3de216acc162d50c8d36077bc8
4
+ data.tar.gz: b06ea500b4d3c547249664dcee953910457263029549d77af2a3314cef45ef58
5
5
  SHA512:
6
- metadata.gz: 9fa81ccba17b2fc8bf7922ebb4e41a38328cd8439d851008dce1eb1a9b06e47b0481db570f2377ca4a69b65ae91814306584ec24f0d1514e2114763b34db4d89
7
- data.tar.gz: 92ac9e0d3d760f148f25c0b0fa11e2d57aa7d56d8cf367df637b2521deb8f7bdc11b59063e8fd523e11db0c3de4d8276e824ef1388fada0c71199c4fe05d5395
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
- <!-- In your views -->
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 (currently in beta)
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
- <%= render BetterUi::ButtonComponent.new(
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
- <%= render BetterUi::CardComponent.new(size: :lg, style: :bordered) do |c| %>
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
- <%= render BetterUi::ActionMessagesComponent.new(
121
- variant: "danger",
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
- <%= render BetterUi::Forms::TextInputComponent.new(
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
- <%= render BetterUi::Forms::NumberInputComponent.new(
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
- <%= render BetterUi::Forms::PasswordInputComponent.new(
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
- <%= render BetterUi::Forms::TextareaComponent.new(
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
- <%= render BetterUi::Forms::CheckboxComponent.new(
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
- <%= render BetterUi::Forms::CheckboxGroupComponent.new(
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
- <%= render BetterUi::Drawer::LayoutComponent.new do |layout| %>
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
- <%= render BetterUi::Drawer::NavGroupComponent.new(title: "Menu") do |group| %>
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.ui_text_input :name %>
241
- <%= f.ui_text_input :email, hint: "We'll never share your email" %>
242
- <%= f.ui_password_input :password %>
243
- <%= f.ui_textarea :bio, rows: 6 %>
244
- <%= f.ui_number_input :age, min: 0, max: 120 %>
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>