better_ui 0.3.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 -51
- 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 -4
- 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 -185
- 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 -227
- data/app/components/better_ui/application/sidebar/component.rb +0 -130
- 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 -25
- data/app/components/better_ui/general/dropdown/component.rb +0 -170
- 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/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 -239
- 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/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/select/component.html.erb +0 -16
- data/app/components/better_ui/general/input/select/component.rb +0 -184
- 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/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/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/accordion.rb +0 -11
- 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 -79
- data/app/helpers/better_ui/general/components/dropdown/item_helper.rb +0 -62
- data/app/helpers/better_ui/general/components/field/field_helper.rb +0 -26
- 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/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/select/select_helper.rb +0 -70
- 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/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/modal.rb +0 -11
- 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/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,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Forms
|
|
5
|
+
# A number input component with support for min/max/step constraints, spinner controls,
|
|
6
|
+
# and prefix/suffix icons.
|
|
7
|
+
#
|
|
8
|
+
# This component extends {BaseComponent} to provide a number input field with optional
|
|
9
|
+
# decorative or functional icons, numeric constraints (min, max, step), and the ability
|
|
10
|
+
# to hide the browser's default up/down arrow spinner buttons.
|
|
11
|
+
#
|
|
12
|
+
# @example Basic number input
|
|
13
|
+
# <%= render BetterUi::Forms::NumberInputComponent.new(
|
|
14
|
+
# name: "user[age]",
|
|
15
|
+
# label: "Age",
|
|
16
|
+
# min: 0,
|
|
17
|
+
# max: 120
|
|
18
|
+
# ) %>
|
|
19
|
+
#
|
|
20
|
+
# @example Number input with step (decimal values)
|
|
21
|
+
# <%= render BetterUi::Forms::NumberInputComponent.new(
|
|
22
|
+
# name: "product[price]",
|
|
23
|
+
# label: "Price",
|
|
24
|
+
# min: 0,
|
|
25
|
+
# step: 0.01,
|
|
26
|
+
# placeholder: "0.00"
|
|
27
|
+
# ) %>
|
|
28
|
+
#
|
|
29
|
+
# @example Number input without spinner controls
|
|
30
|
+
# <%= render BetterUi::Forms::NumberInputComponent.new(
|
|
31
|
+
# name: "quantity",
|
|
32
|
+
# label: "Quantity",
|
|
33
|
+
# min: 1,
|
|
34
|
+
# show_spinner: false
|
|
35
|
+
# ) %>
|
|
36
|
+
#
|
|
37
|
+
# @example Number input with prefix icon (currency)
|
|
38
|
+
# <%= render BetterUi::Forms::NumberInputComponent.new(
|
|
39
|
+
# name: "amount",
|
|
40
|
+
# label: "Amount",
|
|
41
|
+
# min: 0,
|
|
42
|
+
# step: 0.01
|
|
43
|
+
# ) do |component| %>
|
|
44
|
+
# <% component.with_prefix_icon do %>
|
|
45
|
+
# <span class="text-gray-500">$</span>
|
|
46
|
+
# <% end %>
|
|
47
|
+
# <% end %>
|
|
48
|
+
#
|
|
49
|
+
# @example Number input with suffix icon (unit)
|
|
50
|
+
# <%= render BetterUi::Forms::NumberInputComponent.new(
|
|
51
|
+
# name: "weight",
|
|
52
|
+
# label: "Weight",
|
|
53
|
+
# min: 0
|
|
54
|
+
# ) do |component| %>
|
|
55
|
+
# <% component.with_suffix_icon do %>
|
|
56
|
+
# <span class="text-gray-500">kg</span>
|
|
57
|
+
# <% end %>
|
|
58
|
+
# <% end %>
|
|
59
|
+
#
|
|
60
|
+
# @example With validation errors
|
|
61
|
+
# <%= render BetterUi::Forms::NumberInputComponent.new(
|
|
62
|
+
# name: "user[age]",
|
|
63
|
+
# value: "150",
|
|
64
|
+
# label: "Age",
|
|
65
|
+
# errors: ["Age must be between 0 and 120"]
|
|
66
|
+
# ) %>
|
|
67
|
+
#
|
|
68
|
+
# @example Using with Rails form builder
|
|
69
|
+
# <%= form_with model: @product, builder: BetterUi::UiFormBuilder do |f| %>
|
|
70
|
+
# <%= f.ui_number_input :price, min: 0, step: 0.01 do |component| %>
|
|
71
|
+
# <% component.with_prefix_icon do %>
|
|
72
|
+
# <span>$</span>
|
|
73
|
+
# <% end %>
|
|
74
|
+
# <% end %>
|
|
75
|
+
# <% end %>
|
|
76
|
+
#
|
|
77
|
+
# @see BaseComponent
|
|
78
|
+
# @see TextInputComponent
|
|
79
|
+
# @see BetterUi::UiFormBuilder#ui_number_input
|
|
80
|
+
class NumberInputComponent < BaseComponent
|
|
81
|
+
# @!method with_prefix_icon
|
|
82
|
+
# Slot for rendering an icon or content before (left of) the input text.
|
|
83
|
+
# The icon is positioned absolutely and input padding is adjusted automatically.
|
|
84
|
+
# @yieldreturn [String] the HTML content for the prefix icon
|
|
85
|
+
renders_one :prefix_icon
|
|
86
|
+
|
|
87
|
+
# @!method with_suffix_icon
|
|
88
|
+
# Slot for rendering an icon or content after (right of) the input text.
|
|
89
|
+
# The icon is positioned absolutely and input padding is adjusted automatically.
|
|
90
|
+
# @yieldreturn [String] the HTML content for the suffix icon
|
|
91
|
+
renders_one :suffix_icon
|
|
92
|
+
|
|
93
|
+
# Initializes a new number input component.
|
|
94
|
+
#
|
|
95
|
+
# Accepts all parameters from {BaseComponent#initialize} plus additional
|
|
96
|
+
# number-specific parameters for constraints and spinner control.
|
|
97
|
+
#
|
|
98
|
+
# @param name [String] the name attribute for the input field (required for form submission)
|
|
99
|
+
# @param value [String, Numeric, nil] the current value of the input field
|
|
100
|
+
# @param label [String, nil] the label text to display above the input
|
|
101
|
+
# @param hint [String, nil] helpful hint text displayed below the input
|
|
102
|
+
# @param placeholder [String, nil] placeholder text shown when input is empty
|
|
103
|
+
# @param size [Symbol] the size variant (:xs, :sm, :md, :lg, :xl), defaults to :md
|
|
104
|
+
# @param disabled [Boolean] whether the input should be disabled (non-interactive), defaults to false
|
|
105
|
+
# @param readonly [Boolean] whether the input should be readonly (viewable but not editable), defaults to false
|
|
106
|
+
# @param required [Boolean] whether the field is required (shows asterisk indicator), defaults to false
|
|
107
|
+
# @param errors [Array<String>, String, nil] validation error messages to display below the input
|
|
108
|
+
# @param min [Numeric, nil] the minimum allowed value (HTML5 min attribute)
|
|
109
|
+
# @param max [Numeric, nil] the maximum allowed value (HTML5 max attribute)
|
|
110
|
+
# @param step [Numeric, nil] the step increment for value changes (e.g., 0.01 for currency, 1 for integers)
|
|
111
|
+
# @param show_spinner [Boolean] whether to display browser's up/down arrow buttons, defaults to true
|
|
112
|
+
# @param container_classes [String, Array<String>, nil] additional CSS classes for the outer wrapper
|
|
113
|
+
# @param label_classes [String, Array<String>, nil] additional CSS classes for the label element
|
|
114
|
+
# @param input_classes [String, Array<String>, nil] additional CSS classes for the input element
|
|
115
|
+
# @param hint_classes [String, Array<String>, nil] additional CSS classes for the hint text
|
|
116
|
+
# @param error_classes [String, Array<String>, nil] additional CSS classes for error messages
|
|
117
|
+
# @param options [Hash] additional HTML attributes to pass through to the input element
|
|
118
|
+
#
|
|
119
|
+
# @raise [ArgumentError] if size is not one of the valid SIZES
|
|
120
|
+
#
|
|
121
|
+
# @example Basic initialization with constraints
|
|
122
|
+
# BetterUi::Forms::NumberInputComponent.new(
|
|
123
|
+
# name: "product[quantity]",
|
|
124
|
+
# label: "Quantity",
|
|
125
|
+
# min: 1,
|
|
126
|
+
# max: 100,
|
|
127
|
+
# step: 1
|
|
128
|
+
# )
|
|
129
|
+
#
|
|
130
|
+
# @example Currency input with decimal precision
|
|
131
|
+
# BetterUi::Forms::NumberInputComponent.new(
|
|
132
|
+
# name: "product[price]",
|
|
133
|
+
# label: "Price",
|
|
134
|
+
# min: 0,
|
|
135
|
+
# step: 0.01,
|
|
136
|
+
# show_spinner: false
|
|
137
|
+
# )
|
|
138
|
+
#
|
|
139
|
+
# @see BaseComponent#initialize
|
|
140
|
+
def initialize(
|
|
141
|
+
name:,
|
|
142
|
+
value: nil,
|
|
143
|
+
label: nil,
|
|
144
|
+
hint: nil,
|
|
145
|
+
placeholder: nil,
|
|
146
|
+
size: :md,
|
|
147
|
+
disabled: false,
|
|
148
|
+
readonly: false,
|
|
149
|
+
required: false,
|
|
150
|
+
errors: nil,
|
|
151
|
+
min: nil,
|
|
152
|
+
max: nil,
|
|
153
|
+
step: nil,
|
|
154
|
+
show_spinner: true,
|
|
155
|
+
container_classes: nil,
|
|
156
|
+
label_classes: nil,
|
|
157
|
+
input_classes: nil,
|
|
158
|
+
hint_classes: nil,
|
|
159
|
+
error_classes: nil,
|
|
160
|
+
**options
|
|
161
|
+
)
|
|
162
|
+
@min = min
|
|
163
|
+
@max = max
|
|
164
|
+
@step = step
|
|
165
|
+
@show_spinner = show_spinner
|
|
166
|
+
|
|
167
|
+
super(
|
|
168
|
+
name: name,
|
|
169
|
+
value: value,
|
|
170
|
+
label: label,
|
|
171
|
+
hint: hint,
|
|
172
|
+
placeholder: placeholder,
|
|
173
|
+
size: size,
|
|
174
|
+
disabled: disabled,
|
|
175
|
+
readonly: readonly,
|
|
176
|
+
required: required,
|
|
177
|
+
errors: errors,
|
|
178
|
+
container_classes: container_classes,
|
|
179
|
+
label_classes: label_classes,
|
|
180
|
+
input_classes: input_classes,
|
|
181
|
+
hint_classes: hint_classes,
|
|
182
|
+
error_classes: error_classes,
|
|
183
|
+
**options
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# Returns the HTML input type attribute.
|
|
190
|
+
#
|
|
191
|
+
# @return [String] the input type ("number")
|
|
192
|
+
# @api private
|
|
193
|
+
def input_type
|
|
194
|
+
"number"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Checks if a prefix icon has been provided via the slot.
|
|
198
|
+
#
|
|
199
|
+
# @return [Boolean] true if prefix_icon slot is present, false otherwise
|
|
200
|
+
# @api private
|
|
201
|
+
def has_prefix_icon?
|
|
202
|
+
prefix_icon.present?
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Checks if a suffix icon has been provided via the slot.
|
|
206
|
+
#
|
|
207
|
+
# @return [Boolean] true if suffix_icon slot is present, false otherwise
|
|
208
|
+
# @api private
|
|
209
|
+
def has_suffix_icon?
|
|
210
|
+
suffix_icon.present?
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Returns the base CSS classes for icon wrapper elements.
|
|
214
|
+
#
|
|
215
|
+
# Icons are positioned absolutely within the input wrapper and include
|
|
216
|
+
# size-specific padding to ensure proper spacing.
|
|
217
|
+
#
|
|
218
|
+
# @return [String] the merged CSS class string for icon wrappers
|
|
219
|
+
# @api private
|
|
220
|
+
def icon_wrapper_classes
|
|
221
|
+
css_classes([
|
|
222
|
+
"absolute",
|
|
223
|
+
"inset-y-0",
|
|
224
|
+
"flex",
|
|
225
|
+
"items-center",
|
|
226
|
+
"pointer-events-none",
|
|
227
|
+
icon_size_padding
|
|
228
|
+
].flatten.compact)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Returns the CSS classes for the prefix icon wrapper.
|
|
232
|
+
#
|
|
233
|
+
# Extends icon_wrapper_classes with left positioning.
|
|
234
|
+
#
|
|
235
|
+
# @return [String] the merged CSS class string for the prefix icon wrapper
|
|
236
|
+
# @api private
|
|
237
|
+
def prefix_icon_classes
|
|
238
|
+
css_classes([
|
|
239
|
+
icon_wrapper_classes,
|
|
240
|
+
"left-0"
|
|
241
|
+
].flatten.compact)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Returns the CSS classes for the suffix icon wrapper.
|
|
245
|
+
#
|
|
246
|
+
# Extends icon_wrapper_classes with right positioning.
|
|
247
|
+
#
|
|
248
|
+
# @return [String] the merged CSS class string for the suffix icon wrapper
|
|
249
|
+
# @api private
|
|
250
|
+
def suffix_icon_classes
|
|
251
|
+
css_classes([
|
|
252
|
+
icon_wrapper_classes,
|
|
253
|
+
"right-0"
|
|
254
|
+
].flatten.compact)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Returns size-specific horizontal padding for icon wrappers.
|
|
258
|
+
#
|
|
259
|
+
# Ensures icons maintain proper spacing from the input borders across all sizes.
|
|
260
|
+
#
|
|
261
|
+
# @return [String] the padding class for the current component size
|
|
262
|
+
# @api private
|
|
263
|
+
def icon_size_padding
|
|
264
|
+
case @size
|
|
265
|
+
when :xs then "px-2"
|
|
266
|
+
when :sm then "px-3"
|
|
267
|
+
when :md then "px-4"
|
|
268
|
+
when :lg then "px-5"
|
|
269
|
+
when :xl then "px-6"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Returns input element classes with icon-adjusted padding and optional spinner hiding.
|
|
274
|
+
#
|
|
275
|
+
# When prefix or suffix icons are present, this method adds extra padding to the
|
|
276
|
+
# input element to prevent text from overlapping with the icons. When show_spinner
|
|
277
|
+
# is false, adds the 'hide-number-spinner' class to hide browser's up/down arrows.
|
|
278
|
+
# Padding amount is proportional to the component size.
|
|
279
|
+
#
|
|
280
|
+
# @return [String] the CSS class string with icon padding and spinner visibility adjustments
|
|
281
|
+
# @api private
|
|
282
|
+
def input_element_classes_with_icons
|
|
283
|
+
classes = input_element_classes
|
|
284
|
+
classes = "#{classes} pl-10" if has_prefix_icon? && @size == :md
|
|
285
|
+
classes = "#{classes} pr-10" if has_suffix_icon? && @size == :md
|
|
286
|
+
classes = "#{classes} pl-8" if has_prefix_icon? && @size == :sm
|
|
287
|
+
classes = "#{classes} pr-8" if has_suffix_icon? && @size == :sm
|
|
288
|
+
classes = "#{classes} pl-6" if has_prefix_icon? && @size == :xs
|
|
289
|
+
classes = "#{classes} pr-6" if has_suffix_icon? && @size == :xs
|
|
290
|
+
classes = "#{classes} pl-12" if has_prefix_icon? && @size == :lg
|
|
291
|
+
classes = "#{classes} pr-12" if has_suffix_icon? && @size == :lg
|
|
292
|
+
classes = "#{classes} pl-14" if has_prefix_icon? && @size == :xl
|
|
293
|
+
classes = "#{classes} pr-14" if has_suffix_icon? && @size == :xl
|
|
294
|
+
classes = "#{classes} hide-number-spinner" unless @show_spinner
|
|
295
|
+
classes
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Returns the complete set of HTML attributes for the number input element.
|
|
299
|
+
#
|
|
300
|
+
# Extends the parent implementation to add number-specific attributes:
|
|
301
|
+
# - type: "number"
|
|
302
|
+
# - min: minimum allowed value (if specified)
|
|
303
|
+
# - max: maximum allowed value (if specified)
|
|
304
|
+
# - step: increment step for value changes (if specified)
|
|
305
|
+
# - class: icon-adjusted classes when icons are present or spinner is hidden
|
|
306
|
+
#
|
|
307
|
+
# @return [Hash] hash of HTML attributes for the input element
|
|
308
|
+
# @api private
|
|
309
|
+
def input_attributes
|
|
310
|
+
attrs = super
|
|
311
|
+
attrs[:type] = input_type
|
|
312
|
+
attrs[:min] = @min if @min
|
|
313
|
+
attrs[:max] = @max if @max
|
|
314
|
+
attrs[:step] = @step if @step
|
|
315
|
+
attrs[:class] = input_element_classes_with_icons if has_prefix_icon? || has_suffix_icon? || !@show_spinner
|
|
316
|
+
attrs
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
data/app/components/better_ui/forms/password_input_component/password_input_component.html.erb
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
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 <%= tag.attributes(input_wrapper_attributes) %>>
|
|
12
|
+
<% if has_prefix_icon? %>
|
|
13
|
+
<div class="<%= prefix_icon_classes %>">
|
|
14
|
+
<%= prefix_icon %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<input <%= tag.attributes(input_attributes) %> />
|
|
19
|
+
|
|
20
|
+
<% if has_toggle_button? %>
|
|
21
|
+
<div class="<%= toggle_button_classes %>">
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
tabindex="-1"
|
|
25
|
+
class="text-gray-400 hover:text-gray-600 focus:outline-none focus:text-gray-600 transition-colors duration-150"
|
|
26
|
+
data-action="mousedown->better-ui--forms--password-input#toggle"
|
|
27
|
+
aria-label="Toggle password visibility"
|
|
28
|
+
>
|
|
29
|
+
<!-- Eye icon (shown when password is hidden) -->
|
|
30
|
+
<svg
|
|
31
|
+
class="<%= toggle_icon_size %>"
|
|
32
|
+
fill="none"
|
|
33
|
+
stroke="currentColor"
|
|
34
|
+
viewBox="0 0 24 24"
|
|
35
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
36
|
+
data-better-ui--forms--password-input-target="eyeIcon"
|
|
37
|
+
>
|
|
38
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
39
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
40
|
+
</svg>
|
|
41
|
+
|
|
42
|
+
<!-- Eye-slash icon (shown when password is visible) -->
|
|
43
|
+
<svg
|
|
44
|
+
class="<%= toggle_icon_size %> hidden"
|
|
45
|
+
fill="none"
|
|
46
|
+
stroke="currentColor"
|
|
47
|
+
viewBox="0 0 24 24"
|
|
48
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
49
|
+
data-better-ui--forms--password-input-target="eyeSlashIcon"
|
|
50
|
+
>
|
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
52
|
+
</svg>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<% if @hint.present? %>
|
|
59
|
+
<div class="<%= hint_element_classes %>">
|
|
60
|
+
<%= @hint %>
|
|
61
|
+
</div>
|
|
62
|
+
<% end %>
|
|
63
|
+
|
|
64
|
+
<% if has_errors? %>
|
|
65
|
+
<div class="<%= errors_element_classes %>">
|
|
66
|
+
<% @errors.each do |error| %>
|
|
67
|
+
<div><%= error %></div>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterUi
|
|
4
|
+
module Forms
|
|
5
|
+
# A password input component with visibility toggle functionality.
|
|
6
|
+
#
|
|
7
|
+
# This component extends {TextInputComponent} to provide a password input field with
|
|
8
|
+
# a toggle button that allows users to show or hide the password text. The toggle
|
|
9
|
+
# button displays an eye icon when the password is hidden and an eye-slash icon when
|
|
10
|
+
# the password is visible.
|
|
11
|
+
#
|
|
12
|
+
# The component inherits all features from TextInputComponent including labels, hints,
|
|
13
|
+
# errors, validation states, sizes, and the prefix_icon slot. The suffix position is
|
|
14
|
+
# reserved for the password visibility toggle button.
|
|
15
|
+
#
|
|
16
|
+
# @example Basic password input
|
|
17
|
+
# <%= render BetterUi::Forms::PasswordInputComponent.new(
|
|
18
|
+
# name: "user[password]",
|
|
19
|
+
# label: "Password",
|
|
20
|
+
# placeholder: "Enter your password"
|
|
21
|
+
# ) %>
|
|
22
|
+
#
|
|
23
|
+
# @example Password input with prefix icon (lock)
|
|
24
|
+
# <%= render BetterUi::Forms::PasswordInputComponent.new(
|
|
25
|
+
# name: "user[password]",
|
|
26
|
+
# label: "Password",
|
|
27
|
+
# required: true
|
|
28
|
+
# ) do |component| %>
|
|
29
|
+
# <% component.with_prefix_icon do %>
|
|
30
|
+
# <svg class="h-5 w-5 text-gray-400">...</svg>
|
|
31
|
+
# <% end %>
|
|
32
|
+
# <% end %>
|
|
33
|
+
#
|
|
34
|
+
# @example With validation errors
|
|
35
|
+
# <%= render BetterUi::Forms::PasswordInputComponent.new(
|
|
36
|
+
# name: "user[password]",
|
|
37
|
+
# label: "Password",
|
|
38
|
+
# errors: ["Password is too short", "Password must include a number"]
|
|
39
|
+
# ) %>
|
|
40
|
+
#
|
|
41
|
+
# @example With hint text
|
|
42
|
+
# <%= render BetterUi::Forms::PasswordInputComponent.new(
|
|
43
|
+
# name: "user[password]",
|
|
44
|
+
# label: "Password",
|
|
45
|
+
# hint: "Must be at least 8 characters with 1 number and 1 special character",
|
|
46
|
+
# required: true
|
|
47
|
+
# ) %>
|
|
48
|
+
#
|
|
49
|
+
# @example Using with Rails form builder
|
|
50
|
+
# <%= form_with model: @user, builder: BetterUi::UiFormBuilder do |f| %>
|
|
51
|
+
# <%= f.ui_password_input :password, hint: "Minimum 8 characters" %>
|
|
52
|
+
# <% end %>
|
|
53
|
+
#
|
|
54
|
+
# @note The suffix_icon slot from TextInputComponent is not available in PasswordInputComponent
|
|
55
|
+
# as the suffix position is occupied by the visibility toggle button.
|
|
56
|
+
#
|
|
57
|
+
# @see TextInputComponent
|
|
58
|
+
# @see BaseComponent
|
|
59
|
+
# @see BetterUi::UiFormBuilder#ui_password_input
|
|
60
|
+
class PasswordInputComponent < TextInputComponent
|
|
61
|
+
# Initializes a new password input component.
|
|
62
|
+
#
|
|
63
|
+
# All parameters are passed to {TextInputComponent#initialize}. See the parent class
|
|
64
|
+
# for detailed parameter descriptions.
|
|
65
|
+
#
|
|
66
|
+
# @param (see TextInputComponent#initialize)
|
|
67
|
+
# @option (see TextInputComponent#initialize)
|
|
68
|
+
# @raise (see TextInputComponent#initialize)
|
|
69
|
+
#
|
|
70
|
+
# @see TextInputComponent#initialize
|
|
71
|
+
def initialize(
|
|
72
|
+
name:,
|
|
73
|
+
value: nil,
|
|
74
|
+
label: nil,
|
|
75
|
+
hint: nil,
|
|
76
|
+
placeholder: nil,
|
|
77
|
+
size: :md,
|
|
78
|
+
disabled: false,
|
|
79
|
+
readonly: false,
|
|
80
|
+
required: false,
|
|
81
|
+
errors: nil,
|
|
82
|
+
container_classes: nil,
|
|
83
|
+
label_classes: nil,
|
|
84
|
+
input_classes: nil,
|
|
85
|
+
hint_classes: nil,
|
|
86
|
+
error_classes: nil,
|
|
87
|
+
**options
|
|
88
|
+
)
|
|
89
|
+
super(
|
|
90
|
+
name: name,
|
|
91
|
+
value: value,
|
|
92
|
+
label: label,
|
|
93
|
+
hint: hint,
|
|
94
|
+
placeholder: placeholder,
|
|
95
|
+
size: size,
|
|
96
|
+
disabled: disabled,
|
|
97
|
+
readonly: readonly,
|
|
98
|
+
required: required,
|
|
99
|
+
errors: errors,
|
|
100
|
+
container_classes: container_classes,
|
|
101
|
+
label_classes: label_classes,
|
|
102
|
+
input_classes: input_classes,
|
|
103
|
+
hint_classes: hint_classes,
|
|
104
|
+
error_classes: error_classes,
|
|
105
|
+
**options
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
# Returns the HTML input type attribute.
|
|
112
|
+
#
|
|
113
|
+
# For password inputs, this returns "password" by default. The Stimulus controller
|
|
114
|
+
# toggles this between "password" and "text" to show/hide the password.
|
|
115
|
+
#
|
|
116
|
+
# @return [String] the input type ("password")
|
|
117
|
+
# @api private
|
|
118
|
+
def input_type
|
|
119
|
+
"password"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Checks if the password visibility toggle button is present.
|
|
123
|
+
#
|
|
124
|
+
# For PasswordInputComponent, the toggle button is always present unless the
|
|
125
|
+
# input is disabled or readonly (as toggling would not be functional).
|
|
126
|
+
#
|
|
127
|
+
# @return [Boolean] true if toggle button should be rendered
|
|
128
|
+
# @api private
|
|
129
|
+
def has_toggle_button?
|
|
130
|
+
!@disabled && !@readonly
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns size-specific classes for the toggle button icon.
|
|
134
|
+
#
|
|
135
|
+
# Icon sizes are proportional to the input size to maintain visual balance.
|
|
136
|
+
#
|
|
137
|
+
# @return [String] the icon size class for the current component size
|
|
138
|
+
# @api private
|
|
139
|
+
def toggle_icon_size
|
|
140
|
+
case @size
|
|
141
|
+
when :xs then "w-4 h-4"
|
|
142
|
+
when :sm then "w-4 h-4"
|
|
143
|
+
when :md then "w-5 h-5"
|
|
144
|
+
when :lg then "w-5 h-5"
|
|
145
|
+
when :xl then "w-6 h-6"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Returns the CSS classes for the toggle button wrapper.
|
|
150
|
+
#
|
|
151
|
+
# Positions the toggle button absolutely within the input wrapper.
|
|
152
|
+
#
|
|
153
|
+
# @return [String] the merged CSS class string for the toggle button wrapper
|
|
154
|
+
# @api private
|
|
155
|
+
def toggle_button_classes
|
|
156
|
+
css_classes([
|
|
157
|
+
"absolute",
|
|
158
|
+
"inset-y-0",
|
|
159
|
+
"right-0",
|
|
160
|
+
"flex",
|
|
161
|
+
"items-center",
|
|
162
|
+
icon_size_padding
|
|
163
|
+
].flatten.compact)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Returns the complete set of HTML attributes for the input element.
|
|
167
|
+
#
|
|
168
|
+
# Extends the parent implementation to add Stimulus target attribute
|
|
169
|
+
# for the password visibility toggle functionality.
|
|
170
|
+
#
|
|
171
|
+
# @return [Hash] hash of HTML attributes for the input element
|
|
172
|
+
# @api private
|
|
173
|
+
def input_attributes
|
|
174
|
+
attrs = super
|
|
175
|
+
attrs[:data] ||= {}
|
|
176
|
+
attrs[:data][:"better-ui--forms--password-input-target"] = "input"
|
|
177
|
+
attrs
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Returns the HTML attributes for the input wrapper div.
|
|
181
|
+
#
|
|
182
|
+
# Adds Stimulus controller to the wrapper so all child targets are accessible.
|
|
183
|
+
#
|
|
184
|
+
# @return [Hash] hash of HTML attributes for the wrapper div
|
|
185
|
+
# @api private
|
|
186
|
+
def input_wrapper_attributes
|
|
187
|
+
{
|
|
188
|
+
class: input_wrapper_classes,
|
|
189
|
+
data: {
|
|
190
|
+
controller: "better-ui--forms--password-input"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Override parent method to always consider toggle button as suffix icon.
|
|
196
|
+
#
|
|
197
|
+
# This ensures input padding is adjusted for the toggle button.
|
|
198
|
+
#
|
|
199
|
+
# @return [Boolean] true if toggle button is present
|
|
200
|
+
# @api private
|
|
201
|
+
def has_suffix_icon?
|
|
202
|
+
has_toggle_button?
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
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
|
+
<input <%= tag.attributes(input_attributes) %> />
|
|
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>
|