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
|
@@ -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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<% if current? %>
|
|
2
|
+
<span class="<%= current_classes %>" aria-current="page">
|
|
3
|
+
<%= icon_before if icon_before? %>
|
|
4
|
+
<%= @label %>
|
|
5
|
+
</span>
|
|
6
|
+
<% else %>
|
|
7
|
+
<a href="<%= @href %>" class="<%= link_classes %>">
|
|
8
|
+
<%= icon_before if icon_before? %>
|
|
9
|
+
<%= @label %>
|
|
10
|
+
</a>
|
|
11
|
+
<% end %>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Breadcrumb
|
|
5
|
+
# A breadcrumb item component that renders as a link or current page indicator.
|
|
6
|
+
#
|
|
7
|
+
# When an href is provided, the item renders as a link. When href is nil,
|
|
8
|
+
# it renders as a span with aria-current="page" to indicate the current page.
|
|
9
|
+
#
|
|
10
|
+
# @example Link item
|
|
11
|
+
# <%= render BetterUi::Breadcrumb::ItemComponent.new(label: "Home", href: "/") %>
|
|
12
|
+
#
|
|
13
|
+
# @example Current page item (no href)
|
|
14
|
+
# <%= render BetterUi::Breadcrumb::ItemComponent.new(label: "Settings") %>
|
|
15
|
+
#
|
|
16
|
+
# @example With icon
|
|
17
|
+
# <%= render BetterUi::Breadcrumb::ItemComponent.new(label: "Home", href: "/") do |item| %>
|
|
18
|
+
# <% item.with_icon_before do %>
|
|
19
|
+
# <svg>...</svg>
|
|
20
|
+
# <% end %>
|
|
21
|
+
# <% end %>
|
|
22
|
+
class ItemComponent < ApplicationComponent
|
|
23
|
+
# @!method with_icon_before
|
|
24
|
+
# Slot for rendering an icon before the label.
|
|
25
|
+
# @yieldreturn [String] the SVG or icon HTML content
|
|
26
|
+
renders_one :icon_before
|
|
27
|
+
|
|
28
|
+
# Initializes a new breadcrumb item component.
|
|
29
|
+
#
|
|
30
|
+
# @param label [String] the text to display (required)
|
|
31
|
+
# @param href [String, nil] the link URL (nil renders as current page)
|
|
32
|
+
# @param options [Hash] additional HTML attributes passed to the element
|
|
33
|
+
def initialize(label:, href: nil, **options)
|
|
34
|
+
@label = label
|
|
35
|
+
@href = href
|
|
36
|
+
@options = options
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether this item represents the current page.
|
|
40
|
+
#
|
|
41
|
+
# @return [Boolean] true if no href is provided
|
|
42
|
+
def current?
|
|
43
|
+
@href.nil?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Returns CSS classes for the current page indicator.
|
|
49
|
+
#
|
|
50
|
+
# @return [String] the merged CSS class string
|
|
51
|
+
# @api private
|
|
52
|
+
def current_classes
|
|
53
|
+
css_classes(
|
|
54
|
+
"text-grayscale-500",
|
|
55
|
+
"font-medium",
|
|
56
|
+
"inline-flex",
|
|
57
|
+
"items-center",
|
|
58
|
+
"gap-1"
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns CSS classes for the link element.
|
|
63
|
+
#
|
|
64
|
+
# @return [String] the merged CSS class string
|
|
65
|
+
# @api private
|
|
66
|
+
def link_classes
|
|
67
|
+
css_classes(
|
|
68
|
+
"text-grayscale-600",
|
|
69
|
+
"hover:text-grayscale-900",
|
|
70
|
+
"inline-flex",
|
|
71
|
+
"items-center",
|
|
72
|
+
"gap-1",
|
|
73
|
+
"transition-colors"
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -43,7 +43,7 @@ module BetterUi
|
|
|
43
43
|
# # Soft: Light colored background
|
|
44
44
|
# <%= render BetterUi::CardComponent.new(style: :soft) { "Soft card" } %>
|
|
45
45
|
#
|
|
46
|
-
# # Bordered:
|
|
46
|
+
# # Bordered: White background with variant-colored border (defaults to :light)
|
|
47
47
|
# <%= render BetterUi::CardComponent.new(style: :bordered) { "Bordered card" } %>
|
|
48
48
|
class CardComponent < ApplicationComponent
|
|
49
49
|
# Size configurations for padding, text, and border radius
|
|
@@ -78,7 +78,8 @@ module BetterUi
|
|
|
78
78
|
|
|
79
79
|
# Initializes a new card component.
|
|
80
80
|
#
|
|
81
|
-
# @param variant [Symbol] the color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark)
|
|
81
|
+
# @param variant [Symbol, nil] the color variant (:primary, :secondary, :accent, :success, :danger, :warning, :info, :light, :dark).
|
|
82
|
+
# Defaults to :primary for most styles, :light for :bordered style.
|
|
82
83
|
# @param style [Symbol] the visual style (:solid, :outline, :ghost, :soft, :bordered), defaults to :solid
|
|
83
84
|
# @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
|
|
84
85
|
# @param shadow [Boolean] whether to apply shadow, defaults to true
|
|
@@ -127,10 +128,10 @@ module BetterUi
|
|
|
127
128
|
# <% end %>
|
|
128
129
|
# <% end %>
|
|
129
130
|
def initialize(
|
|
130
|
-
variant:
|
|
131
|
+
variant: nil,
|
|
131
132
|
style: :solid,
|
|
132
133
|
size: :md,
|
|
133
|
-
shadow:
|
|
134
|
+
shadow: :sm,
|
|
134
135
|
header_padding: true,
|
|
135
136
|
body_padding: true,
|
|
136
137
|
footer_padding: true,
|
|
@@ -140,10 +141,11 @@ module BetterUi
|
|
|
140
141
|
footer_classes: nil,
|
|
141
142
|
**options
|
|
142
143
|
)
|
|
143
|
-
@variant = validate_variant(variant)
|
|
144
144
|
@style = validate_style(style)
|
|
145
|
+
resolved_variant = variant || default_variant_for_style
|
|
146
|
+
@variant = validate_variant(resolved_variant)
|
|
145
147
|
@size = validate_size(size)
|
|
146
|
-
@shadow = shadow
|
|
148
|
+
@shadow = normalize_shadow(shadow)
|
|
147
149
|
@header_padding = header_padding
|
|
148
150
|
@body_padding = body_padding
|
|
149
151
|
@footer_padding = footer_padding
|
|
@@ -299,13 +301,23 @@ module BetterUi
|
|
|
299
301
|
end
|
|
300
302
|
|
|
301
303
|
# Returns CSS classes for bordered style.
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
+
# White background with variant-colored border.
|
|
305
|
+
# Positioned between soft and outline in visual weight.
|
|
304
306
|
#
|
|
305
307
|
# @return [Array<String>] array of CSS classes
|
|
306
308
|
# @api private
|
|
307
309
|
def bordered_classes
|
|
308
|
-
|
|
310
|
+
case @variant
|
|
311
|
+
when :primary then [ "bg-white", "border", "border-primary-300", "text-grayscale-900" ]
|
|
312
|
+
when :secondary then [ "bg-white", "border", "border-secondary-300", "text-grayscale-900" ]
|
|
313
|
+
when :accent then [ "bg-white", "border", "border-accent-300", "text-grayscale-900" ]
|
|
314
|
+
when :success then [ "bg-white", "border", "border-success-300", "text-grayscale-900" ]
|
|
315
|
+
when :danger then [ "bg-white", "border", "border-danger-300", "text-grayscale-900" ]
|
|
316
|
+
when :warning then [ "bg-white", "border", "border-warning-300", "text-grayscale-900" ]
|
|
317
|
+
when :info then [ "bg-white", "border", "border-info-300", "text-grayscale-900" ]
|
|
318
|
+
when :light then [ "bg-white", "border", "border-grayscale-300", "text-grayscale-900" ]
|
|
319
|
+
when :dark then [ "bg-white", "border", "border-grayscale-700", "text-grayscale-900" ]
|
|
320
|
+
end
|
|
309
321
|
end
|
|
310
322
|
|
|
311
323
|
# Returns shadow CSS classes based on the shadow parameter.
|
|
@@ -313,7 +325,7 @@ module BetterUi
|
|
|
313
325
|
# @return [String, nil] shadow class or nil
|
|
314
326
|
# @api private
|
|
315
327
|
def shadow_classes
|
|
316
|
-
@shadow
|
|
328
|
+
SHADOWS[@shadow]
|
|
317
329
|
end
|
|
318
330
|
|
|
319
331
|
# Returns the size configuration hash for the current size.
|
|
@@ -406,7 +418,17 @@ module BetterUi
|
|
|
406
418
|
when :ghost
|
|
407
419
|
"border-transparent"
|
|
408
420
|
when :bordered
|
|
409
|
-
|
|
421
|
+
case @variant
|
|
422
|
+
when :primary then "border-primary-200"
|
|
423
|
+
when :secondary then "border-secondary-200"
|
|
424
|
+
when :accent then "border-accent-200"
|
|
425
|
+
when :success then "border-success-200"
|
|
426
|
+
when :danger then "border-danger-200"
|
|
427
|
+
when :warning then "border-warning-200"
|
|
428
|
+
when :info then "border-info-200"
|
|
429
|
+
when :light then "border-grayscale-200"
|
|
430
|
+
when :dark then "border-grayscale-600"
|
|
431
|
+
end
|
|
410
432
|
end
|
|
411
433
|
end
|
|
412
434
|
|
|
@@ -418,6 +440,18 @@ module BetterUi
|
|
|
418
440
|
@options
|
|
419
441
|
end
|
|
420
442
|
|
|
443
|
+
# Returns the default variant for the current style.
|
|
444
|
+
# Bordered style defaults to :light (neutral gray), all others default to :primary.
|
|
445
|
+
#
|
|
446
|
+
# @return [Symbol] the default variant
|
|
447
|
+
# @api private
|
|
448
|
+
def default_variant_for_style
|
|
449
|
+
case @style
|
|
450
|
+
when :bordered then :light
|
|
451
|
+
else :primary
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
421
455
|
# Validates the variant parameter.
|
|
422
456
|
#
|
|
423
457
|
# @param variant [Symbol] the variant to validate
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Concerns
|
|
5
|
+
# Shared styling methods for inline label components (Badge, Tag).
|
|
6
|
+
#
|
|
7
|
+
# Provides variant-aware color classes for solid, outline, soft, and ghost styles.
|
|
8
|
+
# Components including this concern must define their own STYLES constant and
|
|
9
|
+
# may override hook methods for style divergence.
|
|
10
|
+
#
|
|
11
|
+
# Hook methods:
|
|
12
|
+
# - outline_light_text_class: CSS class for light variant outline text
|
|
13
|
+
# (default: "text-grayscale-400")
|
|
14
|
+
module InlineLabelStyles
|
|
15
|
+
extend ActiveSupport::Concern
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def solid_classes
|
|
20
|
+
bg_classes = case @variant
|
|
21
|
+
when :primary
|
|
22
|
+
[ "bg-primary-600" ]
|
|
23
|
+
when :secondary
|
|
24
|
+
[ "bg-secondary-600" ]
|
|
25
|
+
when :accent
|
|
26
|
+
[ "bg-accent-600" ]
|
|
27
|
+
when :success
|
|
28
|
+
[ "bg-success-600" ]
|
|
29
|
+
when :danger
|
|
30
|
+
[ "bg-danger-600" ]
|
|
31
|
+
when :warning
|
|
32
|
+
[ "bg-warning-600" ]
|
|
33
|
+
when :info
|
|
34
|
+
[ "bg-info-600" ]
|
|
35
|
+
when :light
|
|
36
|
+
[ "bg-grayscale-100" ]
|
|
37
|
+
when :dark
|
|
38
|
+
[ "bg-grayscale-900" ]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
bg_classes + [ text_color_for_solid ]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def outline_classes
|
|
45
|
+
color_classes = case @variant
|
|
46
|
+
when :primary
|
|
47
|
+
[ "border-primary-600", "text-primary-600" ]
|
|
48
|
+
when :secondary
|
|
49
|
+
[ "border-secondary-600", "text-secondary-600" ]
|
|
50
|
+
when :accent
|
|
51
|
+
[ "border-accent-600", "text-accent-600" ]
|
|
52
|
+
when :success
|
|
53
|
+
[ "border-success-600", "text-success-600" ]
|
|
54
|
+
when :danger
|
|
55
|
+
[ "border-danger-600", "text-danger-600" ]
|
|
56
|
+
when :warning
|
|
57
|
+
[ "border-warning-600", "text-warning-600" ]
|
|
58
|
+
when :info
|
|
59
|
+
[ "border-info-600", "text-info-600" ]
|
|
60
|
+
when :light
|
|
61
|
+
[ "border-grayscale-400", outline_light_text_class ]
|
|
62
|
+
when :dark
|
|
63
|
+
[ "border-grayscale-700", "text-grayscale-700" ]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
[ "bg-transparent", "border" ] + color_classes
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def soft_classes
|
|
70
|
+
case @variant
|
|
71
|
+
when :primary
|
|
72
|
+
[ "bg-primary-100", "text-primary-700" ]
|
|
73
|
+
when :secondary
|
|
74
|
+
[ "bg-secondary-100", "text-secondary-700" ]
|
|
75
|
+
when :accent
|
|
76
|
+
[ "bg-accent-100", "text-accent-700" ]
|
|
77
|
+
when :success
|
|
78
|
+
[ "bg-success-100", "text-success-700" ]
|
|
79
|
+
when :danger
|
|
80
|
+
[ "bg-danger-100", "text-danger-700" ]
|
|
81
|
+
when :warning
|
|
82
|
+
[ "bg-warning-100", "text-warning-700" ]
|
|
83
|
+
when :info
|
|
84
|
+
[ "bg-info-100", "text-info-700" ]
|
|
85
|
+
when :light
|
|
86
|
+
[ "bg-grayscale-100", "text-grayscale-700" ]
|
|
87
|
+
when :dark
|
|
88
|
+
[ "bg-grayscale-800", "text-grayscale-100" ]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ghost_classes
|
|
93
|
+
color_classes = case @variant
|
|
94
|
+
when :primary
|
|
95
|
+
[ "text-primary-600" ]
|
|
96
|
+
when :secondary
|
|
97
|
+
[ "text-secondary-600" ]
|
|
98
|
+
when :accent
|
|
99
|
+
[ "text-accent-600" ]
|
|
100
|
+
when :success
|
|
101
|
+
[ "text-success-600" ]
|
|
102
|
+
when :danger
|
|
103
|
+
[ "text-danger-600" ]
|
|
104
|
+
when :warning
|
|
105
|
+
[ "text-warning-600" ]
|
|
106
|
+
when :info
|
|
107
|
+
[ "text-info-600" ]
|
|
108
|
+
when :light
|
|
109
|
+
[ "text-grayscale-700" ]
|
|
110
|
+
when :dark
|
|
111
|
+
[ "text-grayscale-700" ]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
[ "bg-transparent" ] + color_classes
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def text_color_for_solid
|
|
118
|
+
@variant == :light ? "text-grayscale-900" : "text-grayscale-50"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Hook method for outline light variant text class.
|
|
122
|
+
# Override in including component to customize.
|
|
123
|
+
#
|
|
124
|
+
# @return [String] CSS class for outline light text color
|
|
125
|
+
def outline_light_text_class
|
|
126
|
+
"text-grayscale-400"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def validate_variant(variant)
|
|
130
|
+
unless BetterUi::ApplicationComponent::VARIANTS.key?(variant)
|
|
131
|
+
raise ArgumentError, "Invalid variant: #{variant}. Must be one of: #{BetterUi::ApplicationComponent::VARIANTS.keys.join(", ")}"
|
|
132
|
+
end
|
|
133
|
+
variant
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
# A responsive container component for constraining content width.
|
|
5
|
+
#
|
|
6
|
+
# This component provides a simple wrapper div with configurable max-width,
|
|
7
|
+
# horizontal padding, and centering. It is ideal for wrapping page content
|
|
8
|
+
# to maintain readable line lengths and consistent layout across breakpoints.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic container (default: lg, padded, centered)
|
|
11
|
+
# <%= render BetterUi::ContainerComponent.new do %>
|
|
12
|
+
# Page content here
|
|
13
|
+
# <% end %>
|
|
14
|
+
#
|
|
15
|
+
# @example Extra-large container
|
|
16
|
+
# <%= render BetterUi::ContainerComponent.new(size: :xl) do %>
|
|
17
|
+
# Wide content area
|
|
18
|
+
# <% end %>
|
|
19
|
+
#
|
|
20
|
+
# @example Full-width container without padding
|
|
21
|
+
# <%= render BetterUi::ContainerComponent.new(size: :full, padding: false) do %>
|
|
22
|
+
# Edge-to-edge content
|
|
23
|
+
# <% end %>
|
|
24
|
+
#
|
|
25
|
+
# @example Non-centered container
|
|
26
|
+
# <%= render BetterUi::ContainerComponent.new(centered: false) do %>
|
|
27
|
+
# Left-aligned content
|
|
28
|
+
# <% end %>
|
|
29
|
+
class ContainerComponent < ApplicationComponent
|
|
30
|
+
# Available container sizes mapped to Tailwind max-width classes.
|
|
31
|
+
#
|
|
32
|
+
# @note Classes must be hardcoded strings for Tailwind JIT detection.
|
|
33
|
+
SIZES = {
|
|
34
|
+
sm: "max-w-screen-sm",
|
|
35
|
+
md: "max-w-screen-md",
|
|
36
|
+
lg: "max-w-screen-lg",
|
|
37
|
+
xl: "max-w-screen-xl",
|
|
38
|
+
full: "max-w-full"
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Initializes a new container component.
|
|
42
|
+
#
|
|
43
|
+
# @param size [Symbol] the max-width size (:sm, :md, :lg, :xl, :full), defaults to :lg
|
|
44
|
+
# @param padding [Boolean] whether to apply horizontal padding, defaults to true
|
|
45
|
+
# @param centered [Boolean] whether to center the container with mx-auto, defaults to true
|
|
46
|
+
# @param container_classes [String, nil] additional CSS classes for the container
|
|
47
|
+
# @param options [Hash] additional HTML attributes passed to the container div
|
|
48
|
+
#
|
|
49
|
+
# @raise [ArgumentError] if size is not one of the allowed values
|
|
50
|
+
#
|
|
51
|
+
# @example With all options
|
|
52
|
+
# <%= render BetterUi::ContainerComponent.new(
|
|
53
|
+
# size: :xl,
|
|
54
|
+
# padding: true,
|
|
55
|
+
# centered: true,
|
|
56
|
+
# container_classes: "my-8",
|
|
57
|
+
# id: "main-content",
|
|
58
|
+
# data: { controller: "page" }
|
|
59
|
+
# ) do %>
|
|
60
|
+
# Content here
|
|
61
|
+
# <% end %>
|
|
62
|
+
def initialize(
|
|
63
|
+
size: :lg,
|
|
64
|
+
padding: true,
|
|
65
|
+
centered: true,
|
|
66
|
+
container_classes: nil,
|
|
67
|
+
**options
|
|
68
|
+
)
|
|
69
|
+
@size = validate_size(size)
|
|
70
|
+
@padding = padding
|
|
71
|
+
@centered = centered
|
|
72
|
+
@container_classes = container_classes
|
|
73
|
+
@options = options
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
attr_reader :size, :padding, :centered, :container_classes, :options
|
|
79
|
+
|
|
80
|
+
# Returns the complete CSS classes for the container element.
|
|
81
|
+
#
|
|
82
|
+
# @return [String] the merged CSS class string
|
|
83
|
+
# @api private
|
|
84
|
+
def component_classes
|
|
85
|
+
css_classes([
|
|
86
|
+
"w-full",
|
|
87
|
+
size_classes,
|
|
88
|
+
padding_classes,
|
|
89
|
+
centered_classes,
|
|
90
|
+
@container_classes
|
|
91
|
+
].flatten.compact)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the max-width class for the current size.
|
|
95
|
+
#
|
|
96
|
+
# @return [String] Tailwind max-width class
|
|
97
|
+
# @api private
|
|
98
|
+
def size_classes
|
|
99
|
+
SIZES[@size]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns horizontal padding classes when padding is enabled.
|
|
103
|
+
#
|
|
104
|
+
# @return [String, nil] padding classes or nil
|
|
105
|
+
# @api private
|
|
106
|
+
def padding_classes
|
|
107
|
+
return nil unless @padding
|
|
108
|
+
|
|
109
|
+
"px-4 sm:px-6 lg:px-8"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns centering class when centered is enabled.
|
|
113
|
+
#
|
|
114
|
+
# @return [String, nil] centering class or nil
|
|
115
|
+
# @api private
|
|
116
|
+
def centered_classes
|
|
117
|
+
return nil unless @centered
|
|
118
|
+
|
|
119
|
+
"mx-auto"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns HTML attributes for the container element.
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash] HTML attributes hash
|
|
125
|
+
# @api private
|
|
126
|
+
def html_attributes
|
|
127
|
+
@options
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Validates the size parameter.
|
|
131
|
+
#
|
|
132
|
+
# @param size [Symbol] the size to validate
|
|
133
|
+
# @return [Symbol] the validated size
|
|
134
|
+
# @raise [ArgumentError] if size is invalid
|
|
135
|
+
# @api private
|
|
136
|
+
def validate_size(size)
|
|
137
|
+
unless SIZES.key?(size)
|
|
138
|
+
raise ArgumentError, "Invalid size: #{size}. Must be one of: #{SIZES.keys.join(', ')}"
|
|
139
|
+
end
|
|
140
|
+
size
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|