better_ui 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +257 -212
- data/Rakefile +11 -2
- data/app/components/better_ui/action_messages_component/action_messages_component.html.erb +48 -0
- data/app/components/better_ui/action_messages_component.rb +544 -0
- data/app/components/better_ui/application_component.rb +66 -0
- data/app/components/better_ui/button_component/button_component.html.erb +31 -0
- data/app/components/better_ui/button_component.rb +307 -0
- data/app/components/better_ui/card_component/card_component.html.erb +17 -0
- data/app/components/better_ui/card_component.rb +460 -0
- data/app/components/better_ui/drawer/header_component/header_component.html.erb +24 -0
- data/app/components/better_ui/drawer/header_component.rb +238 -0
- data/app/components/better_ui/drawer/layout_component/layout_component.html.erb +44 -0
- data/app/components/better_ui/drawer/layout_component.rb +270 -0
- data/app/components/better_ui/drawer/nav_group_component/nav_group_component.html.erb +10 -0
- data/app/components/better_ui/drawer/nav_group_component.rb +155 -0
- data/app/components/better_ui/drawer/nav_item_component/nav_item_component.html.erb +13 -0
- data/app/components/better_ui/drawer/nav_item_component.rb +225 -0
- data/app/components/better_ui/drawer/sidebar_component/sidebar_component.html.erb +17 -0
- data/app/components/better_ui/drawer/sidebar_component.rb +263 -0
- data/app/components/better_ui/forms/base_component.rb +450 -0
- data/app/components/better_ui/forms/checkbox_component/checkbox_component.html.erb +28 -0
- data/app/components/better_ui/forms/checkbox_component.rb +419 -0
- data/app/components/better_ui/forms/checkbox_group_component/checkbox_group_component.html.erb +40 -0
- data/app/components/better_ui/forms/checkbox_group_component.rb +363 -0
- data/app/components/better_ui/forms/number_input_component/number_input_component.html.erb +40 -0
- data/app/components/better_ui/forms/number_input_component.rb +320 -0
- data/app/components/better_ui/forms/password_input_component/password_input_component.html.erb +71 -0
- data/app/components/better_ui/forms/password_input_component.rb +206 -0
- data/app/components/better_ui/forms/text_input_component/text_input_component.html.erb +40 -0
- data/app/components/better_ui/forms/text_input_component.rb +258 -0
- data/app/components/better_ui/forms/textarea_component/textarea_component.html.erb +40 -0
- data/app/components/better_ui/forms/textarea_component.rb +329 -0
- data/app/form_builders/better_ui/ui_form_builder.rb +467 -0
- data/app/helpers/better_ui/application_helper.rb +325 -58
- data/app/views/layouts/better_ui/application.html.erb +1 -1
- data/config/routes.rb +1 -0
- data/lib/better_ui/engine.rb +34 -5
- data/lib/better_ui/version.rb +1 -1
- data/lib/better_ui.rb +32 -5
- data/lib/generators/better_ui/install/USAGE +44 -0
- data/lib/generators/better_ui/install/install_generator.rb +87 -0
- data/lib/generators/better_ui/install/templates/better_ui_theme.css.tt +280 -0
- data/lib/tasks/better_ui_tasks.rake +39 -4
- metadata +52 -200
- data/app/components/better_ui/application/card/component.html.erb +0 -20
- data/app/components/better_ui/application/card/component.rb +0 -214
- data/app/components/better_ui/application/main/component.html.erb +0 -9
- data/app/components/better_ui/application/main/component.rb +0 -123
- data/app/components/better_ui/application/navbar/component.html.erb +0 -92
- data/app/components/better_ui/application/navbar/component.rb +0 -136
- data/app/components/better_ui/application/sidebar/component.html.erb +0 -249
- data/app/components/better_ui/application/sidebar/component.rb +0 -187
- data/app/components/better_ui/general/accordion/component.html.erb +0 -5
- data/app/components/better_ui/general/accordion/component.rb +0 -92
- data/app/components/better_ui/general/accordion/item_component.html.erb +0 -12
- data/app/components/better_ui/general/accordion/item_component.rb +0 -176
- data/app/components/better_ui/general/alert/component.html.erb +0 -32
- data/app/components/better_ui/general/alert/component.rb +0 -242
- data/app/components/better_ui/general/avatar/component.html.erb +0 -20
- data/app/components/better_ui/general/avatar/component.rb +0 -301
- data/app/components/better_ui/general/badge/component.html.erb +0 -23
- data/app/components/better_ui/general/badge/component.rb +0 -248
- data/app/components/better_ui/general/breadcrumb/component.html.erb +0 -15
- data/app/components/better_ui/general/breadcrumb/component.rb +0 -187
- data/app/components/better_ui/general/button/component.html.erb +0 -34
- data/app/components/better_ui/general/button/component.rb +0 -214
- data/app/components/better_ui/general/divider/component.html.erb +0 -10
- data/app/components/better_ui/general/divider/component.rb +0 -226
- data/app/components/better_ui/general/dropdown/component.html.erb +0 -28
- data/app/components/better_ui/general/dropdown/component.rb +0 -192
- data/app/components/better_ui/general/dropdown/divider_component.html.erb +0 -1
- data/app/components/better_ui/general/dropdown/divider_component.rb +0 -41
- data/app/components/better_ui/general/dropdown/item_component.html.erb +0 -6
- data/app/components/better_ui/general/dropdown/item_component.rb +0 -119
- data/app/components/better_ui/general/field/component.html.erb +0 -27
- data/app/components/better_ui/general/field/component.rb +0 -37
- data/app/components/better_ui/general/grid/cell_component.html.erb +0 -3
- data/app/components/better_ui/general/grid/cell_component.rb +0 -390
- data/app/components/better_ui/general/grid/component.html.erb +0 -3
- data/app/components/better_ui/general/grid/component.rb +0 -301
- data/app/components/better_ui/general/heading/component.html.erb +0 -22
- data/app/components/better_ui/general/heading/component.rb +0 -257
- data/app/components/better_ui/general/icon/component.html.erb +0 -7
- data/app/components/better_ui/general/icon/component.rb +0 -240
- data/app/components/better_ui/general/input/checkbox/component.html.erb +0 -5
- data/app/components/better_ui/general/input/checkbox/component.rb +0 -238
- data/app/components/better_ui/general/input/datetime/component.html.erb +0 -5
- data/app/components/better_ui/general/input/datetime/component.rb +0 -223
- data/app/components/better_ui/general/input/pin/component.html.erb +0 -1
- data/app/components/better_ui/general/input/pin/component.rb +0 -201
- data/app/components/better_ui/general/input/radio/component.html.erb +0 -5
- data/app/components/better_ui/general/input/radio/component.rb +0 -230
- data/app/components/better_ui/general/input/rating/component.html.erb +0 -4
- data/app/components/better_ui/general/input/rating/component.rb +0 -272
- data/app/components/better_ui/general/input/select/component.html.erb +0 -78
- data/app/components/better_ui/general/input/select/component.rb +0 -249
- data/app/components/better_ui/general/input/select/select_component.html.erb +0 -5
- data/app/components/better_ui/general/input/select/select_component.rb +0 -37
- data/app/components/better_ui/general/input/text/component.html.erb +0 -5
- data/app/components/better_ui/general/input/text/component.rb +0 -171
- data/app/components/better_ui/general/input/textarea/component.html.erb +0 -5
- data/app/components/better_ui/general/input/textarea/component.rb +0 -166
- data/app/components/better_ui/general/input/toggle/component.html.erb +0 -5
- data/app/components/better_ui/general/input/toggle/component.rb +0 -242
- data/app/components/better_ui/general/link/component.html.erb +0 -18
- data/app/components/better_ui/general/link/component.rb +0 -258
- data/app/components/better_ui/general/modal/component.html.erb +0 -5
- data/app/components/better_ui/general/modal/component.rb +0 -47
- data/app/components/better_ui/general/modal/modal_component.html.erb +0 -52
- data/app/components/better_ui/general/modal/modal_component.rb +0 -160
- data/app/components/better_ui/general/pagination/component.html.erb +0 -85
- data/app/components/better_ui/general/pagination/component.rb +0 -216
- data/app/components/better_ui/general/panel/component.html.erb +0 -28
- data/app/components/better_ui/general/panel/component.rb +0 -249
- data/app/components/better_ui/general/progress/component.html.erb +0 -11
- data/app/components/better_ui/general/progress/component.rb +0 -160
- data/app/components/better_ui/general/spinner/component.html.erb +0 -35
- data/app/components/better_ui/general/spinner/component.rb +0 -93
- data/app/components/better_ui/general/table/component.html.erb +0 -5
- data/app/components/better_ui/general/table/component.rb +0 -217
- data/app/components/better_ui/general/table/tbody_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tbody_component.rb +0 -30
- data/app/components/better_ui/general/table/td_component.html.erb +0 -3
- data/app/components/better_ui/general/table/td_component.rb +0 -44
- data/app/components/better_ui/general/table/tfoot_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tfoot_component.rb +0 -28
- data/app/components/better_ui/general/table/th_component.html.erb +0 -6
- data/app/components/better_ui/general/table/th_component.rb +0 -51
- data/app/components/better_ui/general/table/thead_component.html.erb +0 -3
- data/app/components/better_ui/general/table/thead_component.rb +0 -28
- data/app/components/better_ui/general/table/tr_component.html.erb +0 -3
- data/app/components/better_ui/general/table/tr_component.rb +0 -30
- data/app/components/better_ui/general/tabs/component.html.erb +0 -11
- data/app/components/better_ui/general/tabs/component.rb +0 -120
- data/app/components/better_ui/general/tabs/panel_component.html.erb +0 -3
- data/app/components/better_ui/general/tabs/panel_component.rb +0 -37
- data/app/components/better_ui/general/tabs/tab_component.html.erb +0 -13
- data/app/components/better_ui/general/tabs/tab_component.rb +0 -111
- data/app/components/better_ui/general/tag/component.html.erb +0 -3
- data/app/components/better_ui/general/tag/component.rb +0 -104
- data/app/components/better_ui/general/text/component.html.erb +0 -1
- data/app/components/better_ui/general/text/component.rb +0 -194
- data/app/components/better_ui/general/tooltip/component.html.erb +0 -7
- data/app/components/better_ui/general/tooltip/component.rb +0 -239
- data/app/helpers/better_ui/application/components/card/card_helper.rb +0 -96
- data/app/helpers/better_ui/application/components/card.rb +0 -11
- data/app/helpers/better_ui/application/components/main/main_helper.rb +0 -64
- data/app/helpers/better_ui/application/components/navbar/navbar_helper.rb +0 -77
- data/app/helpers/better_ui/application/components/sidebar/sidebar_helper.rb +0 -51
- data/app/helpers/better_ui/general/components/accordion/accordion_helper.rb +0 -73
- data/app/helpers/better_ui/general/components/alert/alert_helper.rb +0 -57
- data/app/helpers/better_ui/general/components/avatar/avatar_helper.rb +0 -29
- data/app/helpers/better_ui/general/components/badge/badge_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/breadcrumb/breadcrumb_helper.rb +0 -37
- data/app/helpers/better_ui/general/components/button/button_helper.rb +0 -65
- data/app/helpers/better_ui/general/components/container/container_helper.rb +0 -60
- data/app/helpers/better_ui/general/components/divider/divider_helper.rb +0 -63
- data/app/helpers/better_ui/general/components/dropdown/divider_helper.rb +0 -32
- data/app/helpers/better_ui/general/components/dropdown/dropdown_helper.rb +0 -88
- data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -68
- data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
- data/app/helpers/better_ui/general/components/grid/grid_helper.rb +0 -145
- data/app/helpers/better_ui/general/components/heading/heading_helper.rb +0 -72
- data/app/helpers/better_ui/general/components/icon/icon_helper.rb +0 -16
- data/app/helpers/better_ui/general/components/input/checkbox/checkbox_helper.rb +0 -81
- data/app/helpers/better_ui/general/components/input/datetime/datetime_helper.rb +0 -91
- data/app/helpers/better_ui/general/components/input/pin/pin_helper.rb +0 -76
- data/app/helpers/better_ui/general/components/input/radio/radio_helper.rb +0 -79
- data/app/helpers/better_ui/general/components/input/radio_group/radio_group_helper.rb +0 -124
- data/app/helpers/better_ui/general/components/input/rating/rating_helper.rb +0 -70
- data/app/helpers/better_ui/general/components/input/select/select_helper.rb +0 -86
- data/app/helpers/better_ui/general/components/input/text/text_helper.rb +0 -138
- data/app/helpers/better_ui/general/components/input/textarea/textarea_helper.rb +0 -73
- data/app/helpers/better_ui/general/components/input/toggle/toggle_helper.rb +0 -77
- data/app/helpers/better_ui/general/components/link/link_helper.rb +0 -89
- data/app/helpers/better_ui/general/components/modal/modal_helper.rb +0 -85
- data/app/helpers/better_ui/general/components/pagination/pagination_helper.rb +0 -82
- data/app/helpers/better_ui/general/components/panel/panel_helper.rb +0 -83
- data/app/helpers/better_ui/general/components/progress/progress_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/spinner/spinner_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/table_helper.rb +0 -53
- data/app/helpers/better_ui/general/components/table/tbody_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/td_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/tfoot_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/th_helper.rb +0 -19
- data/app/helpers/better_ui/general/components/table/thead_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/table/tr_helper.rb +0 -13
- data/app/helpers/better_ui/general/components/tabs/panel_helper.rb +0 -62
- data/app/helpers/better_ui/general/components/tabs/tab_helper.rb +0 -55
- data/app/helpers/better_ui/general/components/tabs/tabs_helper.rb +0 -95
- data/app/helpers/better_ui/general/components/tag/tag_helper.rb +0 -26
- data/app/helpers/better_ui/general/components/text/text_helper.rb +0 -83
- data/app/helpers/better_ui/general/components/tooltip/tooltip_helper.rb +0 -60
- data/app/jobs/better_ui/application_job.rb +0 -4
- data/app/mailers/better_ui/application_mailer.rb +0 -6
- data/config/initializers/lookbook.rb +0 -23
- data/lib/better_ui/railtie.rb +0 -20
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Forms
|
|
5
|
+
# A text input component with support for labels, hints, errors, and prefix/suffix icons.
|
|
6
|
+
#
|
|
7
|
+
# This component extends {BaseComponent} to provide a standard text input field with
|
|
8
|
+
# optional decorative or functional icons positioned before (prefix) or after (suffix)
|
|
9
|
+
# the input text. Perfect for search fields, email inputs, URL fields, and more.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic text input
|
|
12
|
+
# <%= render BetterUi::Forms::TextInputComponent.new(
|
|
13
|
+
# name: "user[email]",
|
|
14
|
+
# label: "Email Address",
|
|
15
|
+
# placeholder: "you@example.com"
|
|
16
|
+
# ) %>
|
|
17
|
+
#
|
|
18
|
+
# @example Text input with prefix icon (search)
|
|
19
|
+
# <%= render BetterUi::Forms::TextInputComponent.new(
|
|
20
|
+
# name: "search",
|
|
21
|
+
# label: "Search",
|
|
22
|
+
# placeholder: "Search..."
|
|
23
|
+
# ) do |component| %>
|
|
24
|
+
# <% component.with_prefix_icon do %>
|
|
25
|
+
# <svg class="h-5 w-5 text-gray-400">...</svg>
|
|
26
|
+
# <% end %>
|
|
27
|
+
# <% end %>
|
|
28
|
+
#
|
|
29
|
+
# @example Text input with suffix icon (verified checkmark)
|
|
30
|
+
# <%= render BetterUi::Forms::TextInputComponent.new(
|
|
31
|
+
# name: "user[email]",
|
|
32
|
+
# value: "user@example.com",
|
|
33
|
+
# label: "Verified Email"
|
|
34
|
+
# ) do |component| %>
|
|
35
|
+
# <% component.with_suffix_icon do %>
|
|
36
|
+
# <svg class="h-5 w-5 text-success-600">...</svg>
|
|
37
|
+
# <% end %>
|
|
38
|
+
# <% end %>
|
|
39
|
+
#
|
|
40
|
+
# @example Text input with both icons
|
|
41
|
+
# <%= render BetterUi::Forms::TextInputComponent.new(
|
|
42
|
+
# name: "website",
|
|
43
|
+
# label: "Website URL",
|
|
44
|
+
# placeholder: "example.com"
|
|
45
|
+
# ) do |component| %>
|
|
46
|
+
# <% component.with_prefix_icon do %>
|
|
47
|
+
# <span class="text-gray-500">https://</span>
|
|
48
|
+
# <% end %>
|
|
49
|
+
# <% component.with_suffix_icon do %>
|
|
50
|
+
# <svg class="h-5 w-5 text-gray-400">...</svg>
|
|
51
|
+
# <% end %>
|
|
52
|
+
# <% end %>
|
|
53
|
+
#
|
|
54
|
+
# @example With validation errors
|
|
55
|
+
# <%= render BetterUi::Forms::TextInputComponent.new(
|
|
56
|
+
# name: "user[email]",
|
|
57
|
+
# value: "invalid",
|
|
58
|
+
# label: "Email",
|
|
59
|
+
# errors: ["Email is invalid", "Email can't be blank"]
|
|
60
|
+
# ) %>
|
|
61
|
+
#
|
|
62
|
+
# @example Using with Rails form builder
|
|
63
|
+
# <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
|
|
64
|
+
# <%= f.ui_text_input :email do |component| %>
|
|
65
|
+
# <% component.with_prefix_icon do %>
|
|
66
|
+
# <svg>...</svg>
|
|
67
|
+
# <% end %>
|
|
68
|
+
# <% end %>
|
|
69
|
+
# <% end %>
|
|
70
|
+
#
|
|
71
|
+
# @see BaseComponent
|
|
72
|
+
# @see BetterUi::UiFormBuilder#ui_text_input
|
|
73
|
+
class TextInputComponent < BaseComponent
|
|
74
|
+
# @!method with_prefix_icon
|
|
75
|
+
# Slot for rendering an icon or content before (left of) the input text.
|
|
76
|
+
# The icon is positioned absolutely and input padding is adjusted automatically.
|
|
77
|
+
# @yieldreturn [String] the HTML content for the prefix icon
|
|
78
|
+
renders_one :prefix_icon
|
|
79
|
+
|
|
80
|
+
# @!method with_suffix_icon
|
|
81
|
+
# Slot for rendering an icon or content after (right of) the input text.
|
|
82
|
+
# The icon is positioned absolutely and input padding is adjusted automatically.
|
|
83
|
+
# @yieldreturn [String] the HTML content for the suffix icon
|
|
84
|
+
renders_one :suffix_icon
|
|
85
|
+
|
|
86
|
+
# Initializes a new text input component.
|
|
87
|
+
#
|
|
88
|
+
# All parameters are passed to {BaseComponent#initialize}. See the parent class
|
|
89
|
+
# for detailed parameter descriptions.
|
|
90
|
+
#
|
|
91
|
+
# @param (see BaseComponent#initialize)
|
|
92
|
+
# @option (see BaseComponent#initialize)
|
|
93
|
+
# @raise (see BaseComponent#initialize)
|
|
94
|
+
#
|
|
95
|
+
# @see BaseComponent#initialize
|
|
96
|
+
def initialize(
|
|
97
|
+
name:,
|
|
98
|
+
value: nil,
|
|
99
|
+
label: nil,
|
|
100
|
+
hint: nil,
|
|
101
|
+
placeholder: nil,
|
|
102
|
+
size: :md,
|
|
103
|
+
disabled: false,
|
|
104
|
+
readonly: false,
|
|
105
|
+
required: false,
|
|
106
|
+
errors: nil,
|
|
107
|
+
container_classes: nil,
|
|
108
|
+
label_classes: nil,
|
|
109
|
+
input_classes: nil,
|
|
110
|
+
hint_classes: nil,
|
|
111
|
+
error_classes: nil,
|
|
112
|
+
**options
|
|
113
|
+
)
|
|
114
|
+
super(
|
|
115
|
+
name: name,
|
|
116
|
+
value: value,
|
|
117
|
+
label: label,
|
|
118
|
+
hint: hint,
|
|
119
|
+
placeholder: placeholder,
|
|
120
|
+
size: size,
|
|
121
|
+
disabled: disabled,
|
|
122
|
+
readonly: readonly,
|
|
123
|
+
required: required,
|
|
124
|
+
errors: errors,
|
|
125
|
+
container_classes: container_classes,
|
|
126
|
+
label_classes: label_classes,
|
|
127
|
+
input_classes: input_classes,
|
|
128
|
+
hint_classes: hint_classes,
|
|
129
|
+
error_classes: error_classes,
|
|
130
|
+
**options
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# Returns the HTML input type attribute.
|
|
137
|
+
#
|
|
138
|
+
# @return [String] the input type ("text")
|
|
139
|
+
# @api private
|
|
140
|
+
def input_type
|
|
141
|
+
"text"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Checks if a prefix icon has been provided via the slot.
|
|
145
|
+
#
|
|
146
|
+
# @return [Boolean] true if prefix_icon slot is present, false otherwise
|
|
147
|
+
# @api private
|
|
148
|
+
def has_prefix_icon?
|
|
149
|
+
prefix_icon.present?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Checks if a suffix icon has been provided via the slot.
|
|
153
|
+
#
|
|
154
|
+
# @return [Boolean] true if suffix_icon slot is present, false otherwise
|
|
155
|
+
# @api private
|
|
156
|
+
def has_suffix_icon?
|
|
157
|
+
suffix_icon.present?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns the base CSS classes for icon wrapper elements.
|
|
161
|
+
#
|
|
162
|
+
# Icons are positioned absolutely within the input wrapper and include
|
|
163
|
+
# size-specific padding to ensure proper spacing.
|
|
164
|
+
#
|
|
165
|
+
# @return [String] the merged CSS class string for icon wrappers
|
|
166
|
+
# @api private
|
|
167
|
+
def icon_wrapper_classes
|
|
168
|
+
css_classes([
|
|
169
|
+
"absolute",
|
|
170
|
+
"inset-y-0",
|
|
171
|
+
"flex",
|
|
172
|
+
"items-center",
|
|
173
|
+
"pointer-events-none",
|
|
174
|
+
icon_size_padding
|
|
175
|
+
].flatten.compact)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns the CSS classes for the prefix icon wrapper.
|
|
179
|
+
#
|
|
180
|
+
# Extends icon_wrapper_classes with left positioning.
|
|
181
|
+
#
|
|
182
|
+
# @return [String] the merged CSS class string for the prefix icon wrapper
|
|
183
|
+
# @api private
|
|
184
|
+
def prefix_icon_classes
|
|
185
|
+
css_classes([
|
|
186
|
+
icon_wrapper_classes,
|
|
187
|
+
"left-0"
|
|
188
|
+
].flatten.compact)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Returns the CSS classes for the suffix icon wrapper.
|
|
192
|
+
#
|
|
193
|
+
# Extends icon_wrapper_classes with right positioning.
|
|
194
|
+
#
|
|
195
|
+
# @return [String] the merged CSS class string for the suffix icon wrapper
|
|
196
|
+
# @api private
|
|
197
|
+
def suffix_icon_classes
|
|
198
|
+
css_classes([
|
|
199
|
+
icon_wrapper_classes,
|
|
200
|
+
"right-0"
|
|
201
|
+
].flatten.compact)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Returns size-specific horizontal padding for icon wrappers.
|
|
205
|
+
#
|
|
206
|
+
# Ensures icons maintain proper spacing from the input borders across all sizes.
|
|
207
|
+
#
|
|
208
|
+
# @return [String] the padding class for the current component size
|
|
209
|
+
# @api private
|
|
210
|
+
def icon_size_padding
|
|
211
|
+
case @size
|
|
212
|
+
when :xs then "px-2"
|
|
213
|
+
when :sm then "px-3"
|
|
214
|
+
when :md then "px-4"
|
|
215
|
+
when :lg then "px-5"
|
|
216
|
+
when :xl then "px-6"
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns input element classes with icon-adjusted padding.
|
|
221
|
+
#
|
|
222
|
+
# When prefix or suffix icons are present, this method adds extra padding to the
|
|
223
|
+
# input element to prevent text from overlapping with the icons. Padding amount
|
|
224
|
+
# is proportional to the component size.
|
|
225
|
+
#
|
|
226
|
+
# @return [String] the CSS class string with icon padding adjustments
|
|
227
|
+
# @api private
|
|
228
|
+
def input_element_classes_with_icons
|
|
229
|
+
classes = input_element_classes
|
|
230
|
+
classes = "#{classes} pl-10" if has_prefix_icon? && @size == :md
|
|
231
|
+
classes = "#{classes} pr-10" if has_suffix_icon? && @size == :md
|
|
232
|
+
classes = "#{classes} pl-8" if has_prefix_icon? && @size == :sm
|
|
233
|
+
classes = "#{classes} pr-8" if has_suffix_icon? && @size == :sm
|
|
234
|
+
classes = "#{classes} pl-6" if has_prefix_icon? && @size == :xs
|
|
235
|
+
classes = "#{classes} pr-6" if has_suffix_icon? && @size == :xs
|
|
236
|
+
classes = "#{classes} pl-12" if has_prefix_icon? && @size == :lg
|
|
237
|
+
classes = "#{classes} pr-12" if has_suffix_icon? && @size == :lg
|
|
238
|
+
classes = "#{classes} pl-14" if has_prefix_icon? && @size == :xl
|
|
239
|
+
classes = "#{classes} pr-14" if has_suffix_icon? && @size == :xl
|
|
240
|
+
classes
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Returns the complete set of HTML attributes for the input element.
|
|
244
|
+
#
|
|
245
|
+
# Extends the parent implementation to add the input type and conditionally
|
|
246
|
+
# apply icon-adjusted classes when icons are present.
|
|
247
|
+
#
|
|
248
|
+
# @return [Hash] hash of HTML attributes for the input element
|
|
249
|
+
# @api private
|
|
250
|
+
def input_attributes
|
|
251
|
+
attrs = super
|
|
252
|
+
attrs[:type] = input_type
|
|
253
|
+
attrs[:class] = input_element_classes_with_icons if has_prefix_icon? || has_suffix_icon?
|
|
254
|
+
attrs
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<div class="<%= wrapper_classes %>">
|
|
2
|
+
<% if @label.present? %>
|
|
3
|
+
<label for="<%= @name %>" class="<%= label_element_classes %>">
|
|
4
|
+
<%= @label %>
|
|
5
|
+
<% if @required %>
|
|
6
|
+
<span class="text-danger-600">*</span>
|
|
7
|
+
<% end %>
|
|
8
|
+
</label>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<div class="<%= input_wrapper_classes %>">
|
|
12
|
+
<% if has_prefix_icon? %>
|
|
13
|
+
<div class="<%= prefix_icon_classes %>">
|
|
14
|
+
<%= prefix_icon %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<textarea <%= tag.attributes(input_attributes) %>><%= @value %></textarea>
|
|
19
|
+
|
|
20
|
+
<% if has_suffix_icon? %>
|
|
21
|
+
<div class="<%= suffix_icon_classes %>">
|
|
22
|
+
<%= suffix_icon %>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<% if @hint.present? %>
|
|
28
|
+
<div class="<%= hint_element_classes %>">
|
|
29
|
+
<%= @hint %>
|
|
30
|
+
</div>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
33
|
+
<% if has_errors? %>
|
|
34
|
+
<div class="<%= errors_element_classes %>">
|
|
35
|
+
<% @errors.each do |error| %>
|
|
36
|
+
<div><%= error %></div>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Forms
|
|
5
|
+
# A textarea component with support for labels, hints, errors, and prefix/suffix icons.
|
|
6
|
+
#
|
|
7
|
+
# This component extends {BaseComponent} to provide a standard textarea field with
|
|
8
|
+
# optional decorative or functional icons positioned before (prefix) or after (suffix)
|
|
9
|
+
# the textarea. Perfect for multi-line text inputs like comments, descriptions, and messages.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic textarea
|
|
12
|
+
# <%= render BetterUi::Forms::TextareaComponent.new(
|
|
13
|
+
# name: "post[content]",
|
|
14
|
+
# label: "Post Content",
|
|
15
|
+
# placeholder: "Write your post here..."
|
|
16
|
+
# ) %>
|
|
17
|
+
#
|
|
18
|
+
# @example Textarea with custom rows
|
|
19
|
+
# <%= render BetterUi::Forms::TextareaComponent.new(
|
|
20
|
+
# name: "comment[body]",
|
|
21
|
+
# label: "Comment",
|
|
22
|
+
# rows: 6,
|
|
23
|
+
# placeholder: "Enter your comment..."
|
|
24
|
+
# ) %>
|
|
25
|
+
#
|
|
26
|
+
# @example Textarea with prefix icon
|
|
27
|
+
# <%= render BetterUi::Forms::TextareaComponent.new(
|
|
28
|
+
# name: "description",
|
|
29
|
+
# label: "Description",
|
|
30
|
+
# placeholder: "Enter description..."
|
|
31
|
+
# ) do |component| %>
|
|
32
|
+
# <% component.with_prefix_icon do %>
|
|
33
|
+
# <svg class="h-5 w-5 text-gray-400">...</svg>
|
|
34
|
+
# <% end %>
|
|
35
|
+
# <% end %>
|
|
36
|
+
#
|
|
37
|
+
# @example Textarea with character limit
|
|
38
|
+
# <%= render BetterUi::Forms::TextareaComponent.new(
|
|
39
|
+
# name: "bio",
|
|
40
|
+
# label: "Bio",
|
|
41
|
+
# maxlength: 500,
|
|
42
|
+
# hint: "Maximum 500 characters"
|
|
43
|
+
# ) %>
|
|
44
|
+
#
|
|
45
|
+
# @example Textarea with disabled resize
|
|
46
|
+
# <%= render BetterUi::Forms::TextareaComponent.new(
|
|
47
|
+
# name: "notes",
|
|
48
|
+
# label: "Notes",
|
|
49
|
+
# resize: :none
|
|
50
|
+
# ) %>
|
|
51
|
+
#
|
|
52
|
+
# @example With validation errors
|
|
53
|
+
# <%= render BetterUi::Forms::TextareaComponent.new(
|
|
54
|
+
# name: "post[content]",
|
|
55
|
+
# value: "",
|
|
56
|
+
# label: "Content",
|
|
57
|
+
# errors: ["Content can't be blank", "Content is too short"]
|
|
58
|
+
# ) %>
|
|
59
|
+
#
|
|
60
|
+
# @example Using with Rails form builder
|
|
61
|
+
# <%= form_with model: @post, builder: BetterUi::UiFormBuilder do |f| %>
|
|
62
|
+
# <%= f.ui_textarea :content do |component| %>
|
|
63
|
+
# <% component.with_prefix_icon do %>
|
|
64
|
+
# <svg>...</svg>
|
|
65
|
+
# <% end %>
|
|
66
|
+
# <% end %>
|
|
67
|
+
# <% end %>
|
|
68
|
+
#
|
|
69
|
+
# @see BaseComponent
|
|
70
|
+
# @see BetterUi::UiFormBuilder#ui_textarea
|
|
71
|
+
class TextareaComponent < BaseComponent
|
|
72
|
+
# @!method with_prefix_icon
|
|
73
|
+
# Slot for rendering an icon or content before (left of) the textarea.
|
|
74
|
+
# The icon is positioned absolutely at the top left and textarea padding is adjusted automatically.
|
|
75
|
+
# @yieldreturn [String] the HTML content for the prefix icon
|
|
76
|
+
renders_one :prefix_icon
|
|
77
|
+
|
|
78
|
+
# @!method with_suffix_icon
|
|
79
|
+
# Slot for rendering an icon or content after (right of) the textarea.
|
|
80
|
+
# The icon is positioned absolutely at the top right and textarea padding is adjusted automatically.
|
|
81
|
+
# @yieldreturn [String] the HTML content for the suffix icon
|
|
82
|
+
renders_one :suffix_icon
|
|
83
|
+
|
|
84
|
+
# Initializes a new textarea component.
|
|
85
|
+
#
|
|
86
|
+
# All standard parameters are passed to {BaseComponent#initialize}. See the parent class
|
|
87
|
+
# for detailed parameter descriptions.
|
|
88
|
+
#
|
|
89
|
+
# @param name [String] the field name for form submission (required)
|
|
90
|
+
# @param value [String, nil] the current value of the textarea
|
|
91
|
+
# @param label [String, nil] the label text displayed above the textarea
|
|
92
|
+
# @param hint [String, nil] helper text displayed below the textarea
|
|
93
|
+
# @param placeholder [String, nil] placeholder text shown when textarea is empty
|
|
94
|
+
# @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
|
|
95
|
+
# @param disabled [Boolean] whether the textarea is disabled, defaults to false
|
|
96
|
+
# @param readonly [Boolean] whether the textarea is read-only, defaults to false
|
|
97
|
+
# @param required [Boolean] whether the field is required (shows asterisk), defaults to false
|
|
98
|
+
# @param errors [Array<String>, String, nil] validation error messages to display
|
|
99
|
+
# @param rows [Integer] number of visible text lines, defaults to 4
|
|
100
|
+
# @param cols [Integer, nil] width in characters (optional, usually controlled by CSS)
|
|
101
|
+
# @param maxlength [Integer, nil] maximum number of characters allowed
|
|
102
|
+
# @param resize [Symbol] CSS resize behavior (:none, :vertical, :horizontal, :both), defaults to :vertical
|
|
103
|
+
# @param container_classes [String, nil] additional CSS classes for the outer wrapper
|
|
104
|
+
# @param label_classes [String, nil] additional CSS classes for the label element
|
|
105
|
+
# @param input_classes [String, nil] additional CSS classes for the textarea element
|
|
106
|
+
# @param hint_classes [String, nil] additional CSS classes for the hint text
|
|
107
|
+
# @param error_classes [String, nil] additional CSS classes for error messages
|
|
108
|
+
# @param options [Hash] additional HTML attributes passed to the textarea element
|
|
109
|
+
#
|
|
110
|
+
# @raise [ArgumentError] if size is not one of the allowed values
|
|
111
|
+
#
|
|
112
|
+
# @see BaseComponent#initialize
|
|
113
|
+
def initialize(
|
|
114
|
+
name:,
|
|
115
|
+
value: nil,
|
|
116
|
+
label: nil,
|
|
117
|
+
hint: nil,
|
|
118
|
+
placeholder: nil,
|
|
119
|
+
size: :md,
|
|
120
|
+
disabled: false,
|
|
121
|
+
readonly: false,
|
|
122
|
+
required: false,
|
|
123
|
+
errors: nil,
|
|
124
|
+
rows: 4,
|
|
125
|
+
cols: nil,
|
|
126
|
+
maxlength: nil,
|
|
127
|
+
resize: :vertical,
|
|
128
|
+
container_classes: nil,
|
|
129
|
+
label_classes: nil,
|
|
130
|
+
input_classes: nil,
|
|
131
|
+
hint_classes: nil,
|
|
132
|
+
error_classes: nil,
|
|
133
|
+
**options
|
|
134
|
+
)
|
|
135
|
+
super(
|
|
136
|
+
name: name,
|
|
137
|
+
value: value,
|
|
138
|
+
label: label,
|
|
139
|
+
hint: hint,
|
|
140
|
+
placeholder: placeholder,
|
|
141
|
+
size: size,
|
|
142
|
+
disabled: disabled,
|
|
143
|
+
readonly: readonly,
|
|
144
|
+
required: required,
|
|
145
|
+
errors: errors,
|
|
146
|
+
container_classes: container_classes,
|
|
147
|
+
label_classes: label_classes,
|
|
148
|
+
input_classes: input_classes,
|
|
149
|
+
hint_classes: hint_classes,
|
|
150
|
+
error_classes: error_classes,
|
|
151
|
+
**options
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@rows = rows
|
|
155
|
+
@cols = cols
|
|
156
|
+
@maxlength = maxlength
|
|
157
|
+
@resize = resize
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
# Returns the HTML element type.
|
|
163
|
+
#
|
|
164
|
+
# @return [String] the element type ("textarea")
|
|
165
|
+
# @api private
|
|
166
|
+
def input_type
|
|
167
|
+
"textarea"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Checks if a prefix icon has been provided via the slot.
|
|
171
|
+
#
|
|
172
|
+
# @return [Boolean] true if prefix_icon slot is present, false otherwise
|
|
173
|
+
# @api private
|
|
174
|
+
def has_prefix_icon?
|
|
175
|
+
prefix_icon.present?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Checks if a suffix icon has been provided via the slot.
|
|
179
|
+
#
|
|
180
|
+
# @return [Boolean] true if suffix_icon slot is present, false otherwise
|
|
181
|
+
# @api private
|
|
182
|
+
def has_suffix_icon?
|
|
183
|
+
suffix_icon.present?
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns the base CSS classes for icon wrapper elements.
|
|
187
|
+
#
|
|
188
|
+
# Icons are positioned absolutely at the top of the textarea wrapper and include
|
|
189
|
+
# size-specific padding to ensure proper spacing.
|
|
190
|
+
#
|
|
191
|
+
# @return [String] the merged CSS class string for icon wrappers
|
|
192
|
+
# @api private
|
|
193
|
+
def icon_wrapper_classes
|
|
194
|
+
css_classes([
|
|
195
|
+
"absolute",
|
|
196
|
+
"top-0",
|
|
197
|
+
"flex",
|
|
198
|
+
"items-start",
|
|
199
|
+
"pointer-events-none",
|
|
200
|
+
icon_size_padding,
|
|
201
|
+
icon_vertical_padding
|
|
202
|
+
].flatten.compact)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Returns the CSS classes for the prefix icon wrapper.
|
|
206
|
+
#
|
|
207
|
+
# Extends icon_wrapper_classes with left positioning.
|
|
208
|
+
#
|
|
209
|
+
# @return [String] the merged CSS class string for the prefix icon wrapper
|
|
210
|
+
# @api private
|
|
211
|
+
def prefix_icon_classes
|
|
212
|
+
css_classes([
|
|
213
|
+
icon_wrapper_classes,
|
|
214
|
+
"left-0"
|
|
215
|
+
].flatten.compact)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns the CSS classes for the suffix icon wrapper.
|
|
219
|
+
#
|
|
220
|
+
# Extends icon_wrapper_classes with right positioning.
|
|
221
|
+
#
|
|
222
|
+
# @return [String] the merged CSS class string for the suffix icon wrapper
|
|
223
|
+
# @api private
|
|
224
|
+
def suffix_icon_classes
|
|
225
|
+
css_classes([
|
|
226
|
+
icon_wrapper_classes,
|
|
227
|
+
"right-0"
|
|
228
|
+
].flatten.compact)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Returns size-specific horizontal padding for icon wrappers.
|
|
232
|
+
#
|
|
233
|
+
# Ensures icons maintain proper spacing from the textarea borders across all sizes.
|
|
234
|
+
#
|
|
235
|
+
# @return [String] the padding class for the current component size
|
|
236
|
+
# @api private
|
|
237
|
+
def icon_size_padding
|
|
238
|
+
case @size
|
|
239
|
+
when :xs then "px-2"
|
|
240
|
+
when :sm then "px-3"
|
|
241
|
+
when :md then "px-4"
|
|
242
|
+
when :lg then "px-5"
|
|
243
|
+
when :xl then "px-6"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Returns size-specific vertical padding for icon wrappers.
|
|
248
|
+
#
|
|
249
|
+
# Aligns icons properly with the top of the textarea text.
|
|
250
|
+
#
|
|
251
|
+
# @return [String] the vertical padding class for the current component size
|
|
252
|
+
# @api private
|
|
253
|
+
def icon_vertical_padding
|
|
254
|
+
case @size
|
|
255
|
+
when :xs then "pt-1"
|
|
256
|
+
when :sm then "pt-1.5"
|
|
257
|
+
when :md then "pt-2"
|
|
258
|
+
when :lg then "pt-2.5"
|
|
259
|
+
when :xl then "pt-3"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Returns textarea element classes with icon-adjusted padding.
|
|
264
|
+
#
|
|
265
|
+
# When prefix or suffix icons are present, this method adds extra padding to the
|
|
266
|
+
# textarea element to prevent text from overlapping with the icons. Padding amount
|
|
267
|
+
# is proportional to the component size.
|
|
268
|
+
#
|
|
269
|
+
# @return [String] the CSS class string with icon padding adjustments
|
|
270
|
+
# @api private
|
|
271
|
+
def input_element_classes_with_icons
|
|
272
|
+
classes = input_element_classes
|
|
273
|
+
classes = "#{classes} pl-10" if has_prefix_icon? && @size == :md
|
|
274
|
+
classes = "#{classes} pr-10" if has_suffix_icon? && @size == :md
|
|
275
|
+
classes = "#{classes} pl-8" if has_prefix_icon? && @size == :sm
|
|
276
|
+
classes = "#{classes} pr-8" if has_suffix_icon? && @size == :sm
|
|
277
|
+
classes = "#{classes} pl-6" if has_prefix_icon? && @size == :xs
|
|
278
|
+
classes = "#{classes} pr-6" if has_suffix_icon? && @size == :xs
|
|
279
|
+
classes = "#{classes} pl-12" if has_prefix_icon? && @size == :lg
|
|
280
|
+
classes = "#{classes} pr-12" if has_suffix_icon? && @size == :lg
|
|
281
|
+
classes = "#{classes} pl-14" if has_prefix_icon? && @size == :xl
|
|
282
|
+
classes = "#{classes} pr-14" if has_suffix_icon? && @size == :xl
|
|
283
|
+
classes
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Returns CSS classes for resize behavior.
|
|
287
|
+
#
|
|
288
|
+
# Maps the resize parameter to Tailwind resize classes.
|
|
289
|
+
#
|
|
290
|
+
# @return [String] the resize class based on @resize value
|
|
291
|
+
# @api private
|
|
292
|
+
def resize_classes
|
|
293
|
+
case @resize
|
|
294
|
+
when :none then "resize-none"
|
|
295
|
+
when :vertical then "resize-y"
|
|
296
|
+
when :horizontal then "resize-x"
|
|
297
|
+
when :both then "resize"
|
|
298
|
+
else "resize-y" # default to vertical
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Returns the complete set of HTML attributes for the textarea element.
|
|
303
|
+
#
|
|
304
|
+
# Extends the parent implementation to add textarea-specific attributes (rows, cols, maxlength)
|
|
305
|
+
# and conditionally apply icon-adjusted classes and resize classes.
|
|
306
|
+
#
|
|
307
|
+
# @return [Hash] hash of HTML attributes for the textarea element
|
|
308
|
+
# @api private
|
|
309
|
+
def input_attributes
|
|
310
|
+
attrs = super
|
|
311
|
+
attrs.delete(:type) # textareas don't have a type attribute
|
|
312
|
+
attrs[:rows] = @rows
|
|
313
|
+
attrs[:cols] = @cols if @cols.present?
|
|
314
|
+
attrs[:maxlength] = @maxlength if @maxlength.present?
|
|
315
|
+
|
|
316
|
+
# Build classes: base classes (with icon adjustments if needed) + resize classes
|
|
317
|
+
if has_prefix_icon? || has_suffix_icon?
|
|
318
|
+
classes = "#{input_element_classes_with_icons} #{resize_classes}".strip
|
|
319
|
+
else
|
|
320
|
+
classes = attrs[:class] || ""
|
|
321
|
+
classes = "#{classes} #{resize_classes}".strip
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
attrs[:class] = classes
|
|
325
|
+
attrs
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|