better_ui 0.8.0 → 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 +49 -6
- 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/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/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 +501 -0
- data/lib/better_ui/version.rb +1 -1
- metadata +57 -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
|
@@ -69,7 +69,7 @@ For detailed installation and configuration instructions, see the [Installation
|
|
|
69
69
|
|
|
70
70
|
- Rails 8.1.1 or higher
|
|
71
71
|
- Node.js and npm (for Tailwind CSS)
|
|
72
|
-
- Tailwind CSS v4
|
|
72
|
+
- Tailwind CSS v4
|
|
73
73
|
|
|
74
74
|
## Component Overview
|
|
75
75
|
|
|
@@ -230,11 +230,12 @@ BetterUi includes a custom form builder for seamless Rails form integration:
|
|
|
230
230
|
|
|
231
231
|
```erb
|
|
232
232
|
<%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
|
|
233
|
-
<%= f.
|
|
234
|
-
<%= f.
|
|
235
|
-
<%= f.
|
|
236
|
-
<%= f.
|
|
237
|
-
<%= 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"]] %>
|
|
238
239
|
<%= f.bui_checkbox :newsletter, label: "Subscribe to newsletter" %>
|
|
239
240
|
<%= f.bui_checkbox_group :roles, [["Admin", "admin"], ["Editor", "editor"]] %>
|
|
240
241
|
<% end %>
|
|
@@ -242,23 +243,65 @@ BetterUi includes a custom form builder for seamless Rails form integration:
|
|
|
242
243
|
|
|
243
244
|
## Available View Helpers
|
|
244
245
|
|
|
246
|
+
### Core Components
|
|
247
|
+
|
|
245
248
|
| Helper | Component |
|
|
246
249
|
|--------|-----------|
|
|
247
250
|
| `bui_button` | ButtonComponent |
|
|
251
|
+
| `bui_link` | LinkComponent |
|
|
248
252
|
| `bui_card` | CardComponent |
|
|
249
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
|
+
|--------|-----------|
|
|
250
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) |
|
|
251
275
|
| `bui_number_input` | Forms::NumberInputComponent |
|
|
252
276
|
| `bui_password_input` | Forms::PasswordInputComponent |
|
|
253
277
|
| `bui_textarea` | Forms::TextareaComponent |
|
|
254
278
|
| `bui_checkbox` | Forms::CheckboxComponent |
|
|
255
279
|
| `bui_checkbox_group` | Forms::CheckboxGroupComponent |
|
|
280
|
+
| `bui_select` | Forms::SelectComponent |
|
|
281
|
+
|
|
282
|
+
### Layout & Navigation
|
|
283
|
+
|
|
284
|
+
| Helper | Component |
|
|
285
|
+
|--------|-----------|
|
|
256
286
|
| `bui_drawer_layout` | Drawer::LayoutComponent |
|
|
257
287
|
| `bui_drawer_sidebar` | Drawer::SidebarComponent |
|
|
258
288
|
| `bui_drawer_header` | Drawer::HeaderComponent |
|
|
259
289
|
| `bui_drawer_nav_item` | Drawer::NavItemComponent |
|
|
260
290
|
| `bui_drawer_nav_group` | Drawer::NavGroupComponent |
|
|
261
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
|
+
|
|
262
305
|
> **Note**: You can also use ViewComponent directly with `render BetterUi::*Component.new(...)` if you prefer the explicit rendering syntax.
|
|
263
306
|
|
|
264
307
|
## Documentation
|
|
@@ -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>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Breadcrumb
|
|
5
|
+
# A breadcrumb navigation component that renders an ordered list of items
|
|
6
|
+
# with configurable separators and sizes.
|
|
7
|
+
#
|
|
8
|
+
# This is a compound component that uses ItemComponent for individual breadcrumb items.
|
|
9
|
+
# The last item (without href) is typically rendered as the current page indicator.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic breadcrumb
|
|
12
|
+
# <%= render BetterUi::Breadcrumb::BreadcrumbComponent.new do |breadcrumb| %>
|
|
13
|
+
# <% breadcrumb.with_item(label: "Home", href: "/") %>
|
|
14
|
+
# <% breadcrumb.with_item(label: "Products", href: "/products") %>
|
|
15
|
+
# <% breadcrumb.with_item(label: "Widget") %>
|
|
16
|
+
# <% end %>
|
|
17
|
+
#
|
|
18
|
+
# @example With chevron separator and large size
|
|
19
|
+
# <%= render BetterUi::Breadcrumb::BreadcrumbComponent.new(separator: :chevron, size: :lg) do |breadcrumb| %>
|
|
20
|
+
# <% breadcrumb.with_item(label: "Home", href: "/") %>
|
|
21
|
+
# <% breadcrumb.with_item(label: "Settings") %>
|
|
22
|
+
# <% end %>
|
|
23
|
+
class BreadcrumbComponent < ApplicationComponent
|
|
24
|
+
# @!method with_item
|
|
25
|
+
# Slot for rendering breadcrumb items.
|
|
26
|
+
# @param label [String] the item label text
|
|
27
|
+
# @param href [String, nil] the item link URL
|
|
28
|
+
# @yieldparam [BetterUi::Breadcrumb::ItemComponent] item the item component instance
|
|
29
|
+
# @yieldreturn [String] the HTML content for the item
|
|
30
|
+
renders_many :items, BetterUi::Breadcrumb::ItemComponent
|
|
31
|
+
|
|
32
|
+
# Allowed separator types
|
|
33
|
+
SEPARATORS = %i[slash chevron dot].freeze
|
|
34
|
+
|
|
35
|
+
# Allowed size options
|
|
36
|
+
SIZES = %i[sm md lg].freeze
|
|
37
|
+
|
|
38
|
+
# Initializes a new breadcrumb component.
|
|
39
|
+
#
|
|
40
|
+
# @param separator [Symbol] the separator type (:slash, :chevron, :dot), defaults to :slash
|
|
41
|
+
# @param size [Symbol] the text size (:sm, :md, :lg), defaults to :md
|
|
42
|
+
# @param container_classes [String, nil] additional CSS classes for the nav element
|
|
43
|
+
# @param options [Hash] additional HTML attributes passed to the nav element
|
|
44
|
+
#
|
|
45
|
+
# @raise [ArgumentError] if separator is not one of the allowed values
|
|
46
|
+
# @raise [ArgumentError] if size is not one of the allowed values
|
|
47
|
+
def initialize(
|
|
48
|
+
separator: :slash,
|
|
49
|
+
size: :md,
|
|
50
|
+
container_classes: nil,
|
|
51
|
+
**options
|
|
52
|
+
)
|
|
53
|
+
@separator = validate_separator(separator)
|
|
54
|
+
@size = validate_size(size)
|
|
55
|
+
@container_classes = container_classes
|
|
56
|
+
@options = options
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# Returns the complete CSS classes for the ordered list element.
|
|
62
|
+
#
|
|
63
|
+
# @return [String] the merged CSS class string
|
|
64
|
+
# @api private
|
|
65
|
+
def component_classes
|
|
66
|
+
css_classes(
|
|
67
|
+
"flex",
|
|
68
|
+
"items-center",
|
|
69
|
+
"flex-wrap",
|
|
70
|
+
"gap-1",
|
|
71
|
+
size_classes
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns CSS classes for separator elements.
|
|
76
|
+
#
|
|
77
|
+
# @return [String] the merged CSS class string
|
|
78
|
+
# @api private
|
|
79
|
+
def separator_classes
|
|
80
|
+
css_classes(
|
|
81
|
+
"mx-2",
|
|
82
|
+
"text-grayscale-400"
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the separator content (character or SVG) based on the separator type.
|
|
87
|
+
#
|
|
88
|
+
# @return [String] the separator HTML content
|
|
89
|
+
# @api private
|
|
90
|
+
def separator_content
|
|
91
|
+
case @separator
|
|
92
|
+
when :slash
|
|
93
|
+
"/"
|
|
94
|
+
when :chevron
|
|
95
|
+
chevron_svg
|
|
96
|
+
when :dot
|
|
97
|
+
"\u00B7"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns the size-specific CSS class.
|
|
102
|
+
#
|
|
103
|
+
# @return [String] the Tailwind text size class
|
|
104
|
+
# @api private
|
|
105
|
+
def size_classes
|
|
106
|
+
case @size
|
|
107
|
+
when :sm then "text-sm"
|
|
108
|
+
when :md then "text-base"
|
|
109
|
+
when :lg then "text-lg"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns an SVG chevron-right icon for the chevron separator.
|
|
114
|
+
#
|
|
115
|
+
# @return [String] the SVG HTML markup
|
|
116
|
+
# @api private
|
|
117
|
+
def chevron_svg
|
|
118
|
+
'<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" /></svg>'.html_safe
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Validates the separator parameter.
|
|
122
|
+
#
|
|
123
|
+
# @param separator [Symbol] the separator to validate
|
|
124
|
+
# @return [Symbol] the validated separator
|
|
125
|
+
# @raise [ArgumentError] if separator is invalid
|
|
126
|
+
# @api private
|
|
127
|
+
def validate_separator(separator)
|
|
128
|
+
unless SEPARATORS.include?(separator)
|
|
129
|
+
raise ArgumentError, "Invalid separator: #{separator}. Must be one of: #{SEPARATORS.join(', ')}"
|
|
130
|
+
end
|
|
131
|
+
separator
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Validates the size parameter.
|
|
135
|
+
#
|
|
136
|
+
# @param size [Symbol] the size to validate
|
|
137
|
+
# @return [Symbol] the validated size
|
|
138
|
+
# @raise [ArgumentError] if size is invalid
|
|
139
|
+
# @api private
|
|
140
|
+
def validate_size(size)
|
|
141
|
+
unless SIZES.include?(size)
|
|
142
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.join(', ')}"
|
|
143
|
+
end
|
|
144
|
+
size
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|