fluxbit_view_components 0.3.0 → 0.4.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 +10 -0
- data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
- data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
- data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
- data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
- data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
- data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
- data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
- data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
- data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
- data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
- data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
- data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
- data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
- data/app/components/fluxbit/accordion_component.rb +125 -0
- data/app/components/fluxbit/alert_component.rb +8 -8
- data/app/components/fluxbit/avatar_component.rb +11 -12
- data/app/components/fluxbit/avatar_group_component.rb +1 -1
- data/app/components/fluxbit/badge_component.rb +8 -7
- data/app/components/fluxbit/banner_component.rb +139 -0
- data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
- data/app/components/fluxbit/breadcrumb_component.rb +66 -0
- data/app/components/fluxbit/button_component.rb +39 -11
- data/app/components/fluxbit/button_group_component.rb +1 -1
- data/app/components/fluxbit/card_component.rb +26 -23
- data/app/components/fluxbit/carousel_component.rb +154 -0
- data/app/components/fluxbit/component.rb +24 -3
- data/app/components/fluxbit/drawer_component.html.erb +30 -0
- data/app/components/fluxbit/drawer_component.rb +125 -0
- data/app/components/fluxbit/dropdown_component.rb +41 -0
- data/app/components/fluxbit/dropdown_item_component.rb +68 -0
- data/app/components/fluxbit/flex_component.rb +1 -1
- data/app/components/fluxbit/form/component.rb +15 -8
- data/app/components/fluxbit/form/dropzone_component.rb +3 -3
- data/app/components/fluxbit/form/field_component.rb +4 -2
- data/app/components/fluxbit/form/help_text_component.rb +1 -1
- data/app/components/fluxbit/form/label_component.rb +10 -3
- data/app/components/fluxbit/form/password_component.rb +247 -0
- data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
- data/app/components/fluxbit/form/select_component.rb +108 -11
- data/app/components/fluxbit/form/text_field_component.rb +40 -23
- data/app/components/fluxbit/form/toggle_component.rb +2 -2
- data/app/components/fluxbit/form/upload_image_component.html.erb +3 -3
- data/app/components/fluxbit/form/upload_image_component.rb +12 -1
- data/app/components/fluxbit/gravatar_component.rb +7 -0
- data/app/components/fluxbit/icon_helpers.rb +167 -0
- data/app/components/fluxbit/link_component.rb +42 -0
- data/app/components/fluxbit/modal_component.rb +28 -31
- data/app/components/fluxbit/pagination_component.rb +206 -0
- data/app/components/fluxbit/popover_component.rb +14 -14
- data/app/components/fluxbit/progress_component.rb +196 -0
- data/app/components/fluxbit/skeleton_component.rb +237 -0
- data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
- data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
- data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
- data/app/components/fluxbit/speed_dial_component.rb +73 -0
- data/app/components/fluxbit/spinner_component.rb +71 -0
- data/app/components/fluxbit/spinner_percent_component.rb +174 -0
- data/app/components/fluxbit/stepper_component.rb +223 -0
- data/app/components/fluxbit/tab_component.rb +44 -25
- data/app/components/fluxbit/table_component.rb +186 -0
- data/app/components/fluxbit/table_group_component.rb +28 -0
- data/app/components/fluxbit/theme_button_component.rb +64 -0
- data/app/components/fluxbit/timeline_component.rb +63 -0
- data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
- data/app/components/fluxbit/timeline_item_component.rb +78 -0
- data/app/components/fluxbit/tooltip_component.rb +2 -2
- data/app/helpers/fluxbit/components_helper.rb +74 -4
- data/app/helpers/fluxbit/form_builder.rb +64 -15
- data/app/helpers/fluxbit/view_helper.rb +71 -0
- data/config/locales/en.yml +37 -4
- data/config/locales/pt-BR.yml +36 -0
- data/lib/fluxbit/config/accordion_component.rb +73 -0
- data/lib/fluxbit/config/avatar_component.rb +11 -11
- data/lib/fluxbit/config/badge_component.rb +14 -11
- data/lib/fluxbit/config/banner_component.rb +60 -0
- data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
- data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
- data/lib/fluxbit/config/button_component.rb +6 -4
- data/lib/fluxbit/config/card_component.rb +23 -12
- data/lib/fluxbit/config/carousel_component.rb +33 -0
- data/lib/fluxbit/config/drawer_component.rb +48 -0
- data/lib/fluxbit/config/dropdown_component.rb +29 -0
- data/lib/fluxbit/config/form/check_box_component.rb +1 -1
- data/lib/fluxbit/config/form/dropzone_component.rb +1 -1
- data/lib/fluxbit/config/form/help_text_component.rb +1 -1
- data/lib/fluxbit/config/form/label_component.rb +3 -2
- data/lib/fluxbit/config/form/password_component.rb +19 -0
- data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
- data/lib/fluxbit/config/form/text_field_component.rb +11 -11
- data/lib/fluxbit/config/form/toggle_component.rb +5 -5
- data/lib/fluxbit/config/link_component.rb +24 -0
- data/lib/fluxbit/config/modal_component.rb +1 -1
- data/lib/fluxbit/config/pagination_component.rb +31 -0
- data/lib/fluxbit/config/popover_component.rb +1 -1
- data/lib/fluxbit/config/progress_component.rb +63 -0
- data/lib/fluxbit/config/skeleton_component.rb +82 -0
- data/lib/fluxbit/config/speed_dial_component.rb +50 -0
- data/lib/fluxbit/config/spinner_component.rb +30 -0
- data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
- data/lib/fluxbit/config/stepper_component.rb +299 -0
- data/lib/fluxbit/config/tab_component.rb +6 -0
- data/lib/fluxbit/config/table_component.rb +75 -0
- data/lib/fluxbit/config/theme_button_component.rb +19 -0
- data/lib/fluxbit/config/timeline_component.rb +77 -0
- data/lib/fluxbit/view_components/engine.rb +11 -3
- data/lib/fluxbit/view_components/version.rb +1 -1
- data/lib/fluxbit/view_components.rb +20 -0
- data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
- data/lib/generators/fluxbit/pagy_generator.rb +39 -0
- data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
- data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
- data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
- data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
- data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
- data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
- data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
- data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
- data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
- data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
- data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
- data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
- data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
- data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
- data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
- data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
- data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
- data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
- data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
- data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
- data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
- data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
- data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
- data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
- data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
- data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
- data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
- data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
- data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
- data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
- data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
- data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
- data/lib/install/install.rb +58 -0
- metadata +107 -18
- data/app/helpers/fluxbit/classes_helper.rb +0 -9
|
@@ -9,6 +9,7 @@ class Fluxbit::Form::FieldComponent < Fluxbit::Form::Component
|
|
|
9
9
|
@name = @props.delete(:name) || (@attribute if @form.present?)
|
|
10
10
|
@value = @props.delete(:value)
|
|
11
11
|
@id = @props.delete(:id)
|
|
12
|
+
@required = @props.delete(:required)
|
|
12
13
|
|
|
13
14
|
@object = @form&.object
|
|
14
15
|
@help_text = define_help_text(props.delete(:help_text), @object, @attribute)
|
|
@@ -16,11 +17,12 @@ class Fluxbit::Form::FieldComponent < Fluxbit::Form::Component
|
|
|
16
17
|
@helper_popover_placement = props.delete(:helper_popover_placement) || "right"
|
|
17
18
|
@label = label_value(props.delete(:label), @object, @attribute, @id)
|
|
18
19
|
@wrapper_html = props.delete(:wrapper_html) || {}
|
|
20
|
+
@wrapper_html = { class: @wrapper_html } if @wrapper_html.is_a?(String)
|
|
19
21
|
define_wrapper_options
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def define_wrapper_options
|
|
23
|
-
add(to: @wrapper_html, class: "required") if @
|
|
24
|
-
add(to: @wrapper_html, class: @name) if @name.present?
|
|
25
|
+
add(to: @wrapper_html, class: "required") if @required.present?
|
|
26
|
+
add(to: @wrapper_html, class: @name.to_s) if @name.present?
|
|
25
27
|
end
|
|
26
28
|
end
|
|
@@ -20,7 +20,7 @@ class Fluxbit::Form::HelpTextComponent < Fluxbit::Form::Component
|
|
|
20
20
|
def initialize(color: nil, **props)
|
|
21
21
|
super
|
|
22
22
|
@props = props
|
|
23
|
-
color = @@color unless color.in?
|
|
23
|
+
color = @@color unless color.in? styles[:colors].keys
|
|
24
24
|
add class: style(color), to: @props, first_element: true
|
|
25
25
|
end
|
|
26
26
|
|
|
@@ -18,7 +18,7 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
|
|
|
18
18
|
# @param helper_popover [String] Popover content shown on icon hover
|
|
19
19
|
# @param helper_popover_placement [String] Placement of the popover (default: "right")
|
|
20
20
|
# @param sizing [Integer] Size index for label text (default: config default)
|
|
21
|
-
# @param color [Symbol] Label color (:default, :success, :
|
|
21
|
+
# @param color [Symbol] Label color (:default, :success, :danger, :info, :warning)
|
|
22
22
|
# @param class [String] Additional CSS classes for the label element
|
|
23
23
|
# @param ... any other HTML attribute supported by the <label> tag
|
|
24
24
|
def initialize(**props)
|
|
@@ -32,6 +32,7 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
|
|
|
32
32
|
@sizing = @props[:sizing].to_i || @@sizing
|
|
33
33
|
@sizing = (styles[:sizes].count - 1) if @sizing > (styles[:sizes].count - 1)
|
|
34
34
|
@color = options(@props.delete(:color), collection: styles[:colors], default: @@color)
|
|
35
|
+
@required = @props.delete(:required) || false
|
|
35
36
|
|
|
36
37
|
add class: styles[:colors][@color], to: @props, first_element: true
|
|
37
38
|
add class: styles[:base], to: @props, first_element: true
|
|
@@ -42,7 +43,7 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
|
|
|
42
43
|
return "" if @helper_popover.nil?
|
|
43
44
|
|
|
44
45
|
content_tag :span,
|
|
45
|
-
anyicon(
|
|
46
|
+
anyicon(@@helper_popover_icon, class: @@helper_popover_icon_class),
|
|
46
47
|
{
|
|
47
48
|
"data-popover-placement": @helper_popover_placement,
|
|
48
49
|
"data-popover-target": target,
|
|
@@ -59,10 +60,16 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
|
|
|
59
60
|
def call
|
|
60
61
|
safe_join(
|
|
61
62
|
[
|
|
62
|
-
content_tag(:label, safe_join([ content || @with_content, span_helper_popover ]), @props),
|
|
63
|
+
content_tag(:label, safe_join([ content || @with_content, span_helper_popover, required ]), @props),
|
|
63
64
|
help_text,
|
|
64
65
|
render_popover
|
|
65
66
|
]
|
|
66
67
|
)
|
|
67
68
|
end
|
|
69
|
+
|
|
70
|
+
def required
|
|
71
|
+
return "" unless @required
|
|
72
|
+
|
|
73
|
+
content_tag(:span, "*", class: styles[:required])
|
|
74
|
+
end
|
|
68
75
|
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The `Fluxbit::Form::PasswordComponent` is a password input component that extends `Fluxbit::Form::TextFieldComponent`.
|
|
4
|
+
# It provides a password field with a toggleable visibility icon (eye/eye-slash) and optional password strength indicators.
|
|
5
|
+
# The component can display validation checks for password requirements such as length, letters, capital letters, numbers, and special characters.
|
|
6
|
+
#
|
|
7
|
+
# @example Basic usage
|
|
8
|
+
# = render Fluxbit::Form::PasswordComponent.new(name: :password)
|
|
9
|
+
#
|
|
10
|
+
# @example With password strength checks
|
|
11
|
+
# = render Fluxbit::Form::PasswordComponent.new(name: :password, show_strength: true)
|
|
12
|
+
#
|
|
13
|
+
# @example Custom requirements
|
|
14
|
+
# = render Fluxbit::Form::PasswordComponent.new(
|
|
15
|
+
# name: :password,
|
|
16
|
+
# show_strength: true,
|
|
17
|
+
# min_length: 12,
|
|
18
|
+
# require_uppercase: true,
|
|
19
|
+
# require_lowercase: true,
|
|
20
|
+
# require_numbers: true,
|
|
21
|
+
# require_special: true
|
|
22
|
+
# )
|
|
23
|
+
#
|
|
24
|
+
# @see docs/03_Forms/Password.md For detailed documentation and examples.
|
|
25
|
+
class Fluxbit::Form::PasswordComponent < Fluxbit::Form::TextFieldComponent
|
|
26
|
+
# Initializes the password field component with the given properties.
|
|
27
|
+
#
|
|
28
|
+
# @param form [ActionView::Helpers::FormBuilder] The form builder (optional, for Rails forms)
|
|
29
|
+
# @param attribute [Symbol] The model attribute to be used in the form (required if using form builder)
|
|
30
|
+
# @param show_strength [Boolean] Whether to display password strength indicators (default: false)
|
|
31
|
+
# @param min_length [Integer] Minimum password length requirement (default: 8)
|
|
32
|
+
# @param require_uppercase [Boolean] Require at least one uppercase letter (default: true)
|
|
33
|
+
# @param require_lowercase [Boolean] Require at least one lowercase letter (default: true)
|
|
34
|
+
# @param require_numbers [Boolean] Require at least one number (default: true)
|
|
35
|
+
# @param require_special [Boolean] Require at least one special character (default: false)
|
|
36
|
+
# @param strength_labels [Hash] Custom labels for strength checks
|
|
37
|
+
# @param ... any other options supported by TextFieldComponent
|
|
38
|
+
def initialize(**props)
|
|
39
|
+
@show_strength = props.delete(:show_strength) || false
|
|
40
|
+
@min_length = props.delete(:min_length) || 8
|
|
41
|
+
|
|
42
|
+
# Get boolean values, defaulting to true for uppercase/lowercase/numbers, false for special
|
|
43
|
+
uppercase_val = props.delete(:require_uppercase)
|
|
44
|
+
@require_uppercase = uppercase_val.nil? ? true : uppercase_val
|
|
45
|
+
|
|
46
|
+
lowercase_val = props.delete(:require_lowercase)
|
|
47
|
+
@require_lowercase = lowercase_val.nil? ? true : lowercase_val
|
|
48
|
+
|
|
49
|
+
numbers_val = props.delete(:require_numbers)
|
|
50
|
+
@require_numbers = numbers_val.nil? ? true : numbers_val
|
|
51
|
+
|
|
52
|
+
@require_special = props.delete(:require_special) || false
|
|
53
|
+
@strength_labels = props.delete(:strength_labels) || default_strength_labels
|
|
54
|
+
|
|
55
|
+
# Force type to password and add eye icon for visibility toggle
|
|
56
|
+
props[:type] = :password
|
|
57
|
+
props[:right_icon] ||= :"heroicons_outline:eye"
|
|
58
|
+
|
|
59
|
+
# Add data attributes for Stimulus controller
|
|
60
|
+
props[:data] ||= {}
|
|
61
|
+
# props[:data][:controller] = add_controller(props[:data][:controller], "fx-password")
|
|
62
|
+
props[:data][:action] = add_action(props[:data][:action], "input->fx-password#validate")
|
|
63
|
+
|
|
64
|
+
# Store right_icon_html for later use in create_right_icon
|
|
65
|
+
@password_right_icon_html = props.delete(:right_icon_html) || {}
|
|
66
|
+
|
|
67
|
+
# Set up wrapper_html with controller before calling super
|
|
68
|
+
props[:wrapper_html] ||= {}
|
|
69
|
+
props[:wrapper_html][:data] ||= {}
|
|
70
|
+
props[:wrapper_html][:data][:controller] = "fx-password"
|
|
71
|
+
props[:wrapper_html][:data].merge!(
|
|
72
|
+
{
|
|
73
|
+
"fx-password-target": "inputWrapper",
|
|
74
|
+
"fx-password-min-length-value": @min_length,
|
|
75
|
+
"fx-password-require-uppercase-value": @require_uppercase,
|
|
76
|
+
"fx-password-require-lowercase-value": @require_lowercase,
|
|
77
|
+
"fx-password-require-numbers-value": @require_numbers,
|
|
78
|
+
"fx-password-require-special-value": @require_special
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
super(**props)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def call
|
|
87
|
+
content_tag :div, **@wrapper_html do
|
|
88
|
+
safe_join [
|
|
89
|
+
label,
|
|
90
|
+
icon_container_wrapper,
|
|
91
|
+
help_text,
|
|
92
|
+
(@show_strength ? strength_indicator : nil)
|
|
93
|
+
].compact
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def password_styles
|
|
100
|
+
@password_styles ||= {
|
|
101
|
+
strength_wrapper: "mt-2 space-y-2",
|
|
102
|
+
strength_bar_wrapper: "space-y-1",
|
|
103
|
+
strength_bar_label: "text-sm font-medium text-slate-700 dark:text-slate-300",
|
|
104
|
+
strength_bar_container: "w-full bg-slate-200 rounded-full h-2 dark:bg-slate-700",
|
|
105
|
+
strength_bar: "h-2 rounded-full transition-all duration-300 bg-slate-300 dark:bg-slate-600",
|
|
106
|
+
checks_list: "space-y-1",
|
|
107
|
+
check_item: "flex items-center gap-2 text-sm text-slate-600 dark:text-slate-400",
|
|
108
|
+
check_icon: "flex-shrink-0 text-red-500 dark:text-red-400",
|
|
109
|
+
check_label: ""
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def icon_container_wrapper
|
|
114
|
+
content_tag :div,
|
|
115
|
+
data: {
|
|
116
|
+
"fx-password-target": "inputWrapper",
|
|
117
|
+
"fx-password-min-length-value": @min_length,
|
|
118
|
+
"fx-password-require-uppercase-value": @require_uppercase,
|
|
119
|
+
"fx-password-require-lowercase-value": @require_lowercase,
|
|
120
|
+
"fx-password-require-numbers-value": @require_numbers,
|
|
121
|
+
"fx-password-require-special-value": @require_special
|
|
122
|
+
} do
|
|
123
|
+
icon_container
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def default_strength_labels
|
|
128
|
+
{
|
|
129
|
+
length: I18n.t("fluxbit.form.password.checks.length", default: "At least %{count} characters", count: @min_length),
|
|
130
|
+
uppercase: I18n.t("fluxbit.form.password.checks.uppercase", default: "Contains uppercase letter"),
|
|
131
|
+
lowercase: I18n.t("fluxbit.form.password.checks.lowercase", default: "Contains lowercase letter"),
|
|
132
|
+
numbers: I18n.t("fluxbit.form.password.checks.numbers", default: "Contains number"),
|
|
133
|
+
special: I18n.t("fluxbit.form.password.checks.special", default: "Contains special character"),
|
|
134
|
+
strength: I18n.t("fluxbit.form.password.strength", default: "Password strength")
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def strength_indicator
|
|
139
|
+
content_tag :div, class: password_styles[:strength_wrapper], data: { "fx-password-target": "strengthIndicator" } do
|
|
140
|
+
safe_join [
|
|
141
|
+
strength_bar,
|
|
142
|
+
strength_checks
|
|
143
|
+
]
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def strength_bar
|
|
148
|
+
content_tag :div, class: password_styles[:strength_bar_wrapper] do
|
|
149
|
+
safe_join([
|
|
150
|
+
content_tag(:div, class: password_styles[:strength_bar_label]) do
|
|
151
|
+
content_tag :span, @strength_labels[:strength]
|
|
152
|
+
end,
|
|
153
|
+
content_tag(:div, class: password_styles[:strength_bar_container]) do
|
|
154
|
+
content_tag :div,
|
|
155
|
+
"",
|
|
156
|
+
class: password_styles[:strength_bar],
|
|
157
|
+
data: { "fx-password-target": "strengthBar" }
|
|
158
|
+
end
|
|
159
|
+
])
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def strength_checks
|
|
164
|
+
content_tag :ul, class: password_styles[:checks_list] do
|
|
165
|
+
safe_join [
|
|
166
|
+
strength_check(:length, @strength_labels[:length]),
|
|
167
|
+
(@require_lowercase ? strength_check(:lowercase, @strength_labels[:lowercase]) : nil),
|
|
168
|
+
(@require_uppercase ? strength_check(:uppercase, @strength_labels[:uppercase]) : nil),
|
|
169
|
+
(@require_numbers ? strength_check(:numbers, @strength_labels[:numbers]) : nil),
|
|
170
|
+
(@require_special ? strength_check(:special, @strength_labels[:special]) : nil)
|
|
171
|
+
].compact
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def strength_check(type, label)
|
|
176
|
+
content_tag :li,
|
|
177
|
+
class: password_styles[:check_item],
|
|
178
|
+
data: { "fx-password-target": "check#{type.to_s.capitalize}" } do
|
|
179
|
+
safe_join([
|
|
180
|
+
# Icon container with both check and X icons
|
|
181
|
+
content_tag(:span, class: password_styles[:check_icon]) do
|
|
182
|
+
safe_join([
|
|
183
|
+
# X icon (visible by default - password requirement not met)
|
|
184
|
+
content_tag(:span,
|
|
185
|
+
data: { "fx-password-target": "check#{type.to_s.capitalize}Fail" }
|
|
186
|
+
) do
|
|
187
|
+
anyicon(:"heroicons_solid:x-circle", class: "size-4")
|
|
188
|
+
end,
|
|
189
|
+
# Check icon (hidden by default - password requirement met)
|
|
190
|
+
content_tag(:span,
|
|
191
|
+
class: "hidden",
|
|
192
|
+
data: { "fx-password-target": "check#{type.to_s.capitalize}Pass" }
|
|
193
|
+
) do
|
|
194
|
+
anyicon(:"heroicons_solid:check-circle", class: "size-4")
|
|
195
|
+
end
|
|
196
|
+
])
|
|
197
|
+
end,
|
|
198
|
+
content_tag(:span, label, class: password_styles[:check_label])
|
|
199
|
+
])
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def add_controller(existing, new_controller)
|
|
204
|
+
existing ? "#{existing} #{new_controller}" : new_controller
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def add_action(existing, new_action)
|
|
208
|
+
existing ? "#{existing} #{new_action}" : new_action
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def add_class(existing, new_class)
|
|
212
|
+
existing ? "#{existing} #{new_class}" : new_class
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Override to render both eye icons with toggle visibility
|
|
216
|
+
def create_right_icon
|
|
217
|
+
return "" if @right_icon.nil?
|
|
218
|
+
|
|
219
|
+
# Build wrapper attributes
|
|
220
|
+
wrapper_attrs = {
|
|
221
|
+
class: add_class(@password_right_icon_html[:class], "absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer"),
|
|
222
|
+
onclick: "", # Prevent pointer-events-none
|
|
223
|
+
data: {
|
|
224
|
+
action: "click->fx-password#toggleVisibility"
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
content_tag :div, **wrapper_attrs do
|
|
229
|
+
safe_join([
|
|
230
|
+
# Eye icon (visible by default)
|
|
231
|
+
content_tag(:div,
|
|
232
|
+
class: "#{styles[:additional_icons][:class][@color]}",
|
|
233
|
+
data: { "fx-password-target": "eyeIcon" }
|
|
234
|
+
) do
|
|
235
|
+
eye_icon
|
|
236
|
+
end,
|
|
237
|
+
# Eye-slash icon (hidden by default)
|
|
238
|
+
content_tag(:div,
|
|
239
|
+
class: "#{styles[:additional_icons][:class][@color]} hidden",
|
|
240
|
+
data: { "fx-password-target": "eyeSlashIcon" }
|
|
241
|
+
) do
|
|
242
|
+
eye_slash_icon
|
|
243
|
+
end
|
|
244
|
+
])
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
##
|
|
4
|
+
# The `Fluxbit::Form::RadioGroupButtonComponent` is a component for rendering radio buttons
|
|
5
|
+
# styled as a button group. It provides the visual appearance of grouped buttons while
|
|
6
|
+
# maintaining radio button behavior (only one option can be selected at a time).
|
|
7
|
+
#
|
|
8
|
+
# This component is useful for creating segmented controls, view toggles, or any interface
|
|
9
|
+
# where users need to select one option from a group with a button-like appearance.
|
|
10
|
+
class Fluxbit::Form::RadioGroupButtonComponent < Fluxbit::Form::FieldComponent
|
|
11
|
+
include Fluxbit::Config::Form::RadioGroupButtonComponent
|
|
12
|
+
|
|
13
|
+
renders_many :radio_options, lambda { |**props, &block|
|
|
14
|
+
@options_group << ComponentObj.new(props, view_context.capture(&block))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Initializes the radio group button component with the given properties.
|
|
19
|
+
#
|
|
20
|
+
# @param [Hash] props The properties to customize the radio group button.
|
|
21
|
+
# @option props [String] :name (nil) The name attribute for the radio button group (required for proper radio functionality).
|
|
22
|
+
# @option props [Symbol, String] :color (:default) The color style of the buttons.
|
|
23
|
+
# @option props [Symbol, String] :size (1) The size of the buttons (e.g., `0` to `4`).
|
|
24
|
+
# @option props [Boolean] :pill (false) Determines if the buttons have pill-shaped edges.
|
|
25
|
+
# @option props [Hash] **props Remaining options declared as HTML attributes, applied to the group container.
|
|
26
|
+
#
|
|
27
|
+
# @return [Fluxbit::Form::RadioGroupButtonComponent]
|
|
28
|
+
def initialize(**props)
|
|
29
|
+
super
|
|
30
|
+
@props = props
|
|
31
|
+
@name = @props.delete(:name) || "radio_group_#{fx_id}"
|
|
32
|
+
@color = options (@props.delete(:color) || "").to_sym, collection: button_styles[:colors].keys, default: @@color
|
|
33
|
+
@size = @props.delete(:size) || @@size
|
|
34
|
+
@pill = options @props.delete(:pill), default: false
|
|
35
|
+
@outline = @color.to_s.end_with?("_outline")
|
|
36
|
+
@options_group = []
|
|
37
|
+
|
|
38
|
+
add class: Fluxbit::Config::Form::RadioGroupButtonComponent.styles[:group], to: @props, first_element: true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def call
|
|
42
|
+
radio_options
|
|
43
|
+
tag.div(**@props) do
|
|
44
|
+
@options_group.each_with_index do |option, index|
|
|
45
|
+
concat render_radio_button(option, index)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def render_radio_button(option, index)
|
|
53
|
+
option_props = option.props || {}
|
|
54
|
+
option_content = option.content
|
|
55
|
+
option_value = option_props.delete(:value) || index
|
|
56
|
+
option_checked = option_props.delete(:checked) || false
|
|
57
|
+
option_disabled = option_props.delete(:disabled) || false
|
|
58
|
+
|
|
59
|
+
radio_id = "#{@name}_#{option_value}_#{fx_id}"
|
|
60
|
+
|
|
61
|
+
# Input element
|
|
62
|
+
input_html = {
|
|
63
|
+
type: "radio",
|
|
64
|
+
id: radio_id,
|
|
65
|
+
name: @name,
|
|
66
|
+
value: option_value,
|
|
67
|
+
class: "#{Fluxbit::Config::Form::RadioGroupButtonComponent.styles[:input]} peer"
|
|
68
|
+
}
|
|
69
|
+
input_html[:checked] = true if option_checked
|
|
70
|
+
input_html[:disabled] = true if option_disabled
|
|
71
|
+
|
|
72
|
+
# Label element (styled as button)
|
|
73
|
+
label_html = option_props.dup
|
|
74
|
+
label_html[:for] = radio_id
|
|
75
|
+
|
|
76
|
+
add class: Fluxbit::Config::Form::RadioGroupButtonComponent.styles[:label][:base], to: label_html, first_element: true
|
|
77
|
+
add class: button_color_classes, to: label_html, first_element: true
|
|
78
|
+
add class: button_size_classes, to: label_html, first_element: true
|
|
79
|
+
add class: button_pill_classes, to: label_html, first_element: true
|
|
80
|
+
add class: button_outline_classes, to: label_html, first_element: true
|
|
81
|
+
|
|
82
|
+
# Position classes
|
|
83
|
+
add class: Fluxbit::Config::Form::RadioGroupButtonComponent.styles[:label][:position][:start], to: label_html if index == 0
|
|
84
|
+
add class: Fluxbit::Config::Form::RadioGroupButtonComponent.styles[:label][:position][:end], to: label_html if index == @options_group.size - 1
|
|
85
|
+
add class: Fluxbit::Config::Form::RadioGroupButtonComponent.styles[:label][:position][:middle], to: label_html if index > 0 && index < @options_group.size - 1
|
|
86
|
+
|
|
87
|
+
# Selected state - use peer-checked for CSS-only interaction
|
|
88
|
+
# The peer modifier allows the label to change when the radio input is checked
|
|
89
|
+
add class: "peer-checked:brightness-90 dark:peer-checked:brightness-75", to: label_html
|
|
90
|
+
|
|
91
|
+
# Disabled state
|
|
92
|
+
if option_disabled
|
|
93
|
+
add class: Fluxbit::Config::ButtonComponent.styles[:disabled], to: label_html, first_element: true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
tag.div(class: "relative") do
|
|
97
|
+
concat tag.input(**input_html)
|
|
98
|
+
concat tag.label(option_content, **label_html)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def button_styles
|
|
103
|
+
Fluxbit::Config::ButtonComponent.styles
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def button_color_classes
|
|
107
|
+
@color.in?(button_styles[:colors].keys) ? button_styles[:colors][@color] : button_styles[:colors][:default]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def button_size_classes
|
|
111
|
+
button_styles[:size][@size.to_i]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def button_pill_classes
|
|
115
|
+
return "" if @outline
|
|
116
|
+
button_styles[:pill][@pill ? :on : :off]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def button_outline_classes
|
|
120
|
+
if @outline
|
|
121
|
+
"#{button_styles[:outline][:on]} #{button_styles[:outline][:pill][@pill ? :on : :off]}"
|
|
122
|
+
else
|
|
123
|
+
button_styles[:outline][:off]
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -12,14 +12,22 @@ class Fluxbit::Form::SelectComponent < Fluxbit::Form::TextFieldComponent
|
|
|
12
12
|
# Initializes the select component with the given properties.
|
|
13
13
|
#
|
|
14
14
|
# @param name [String] Name of the field (required unless using form builder)
|
|
15
|
-
# @param label [String] Label for the
|
|
15
|
+
# @param label [String, false] Label text for the select field (optional, auto-generated from attribute if using form builder)
|
|
16
|
+
# @param help_text [String, Array, false] Help text displayed below the field (optional, supports i18n)
|
|
17
|
+
# @param helper_popover [String, false] Content for an info popover next to the label (optional, supports i18n)
|
|
18
|
+
# @param helper_popover_placement [String] Placement of the popover: "top", "right", "bottom", "left" (default: "right")
|
|
16
19
|
# @param value [String] Value for the field (optional)
|
|
17
20
|
# @param grouped [Boolean] Enables grouped select options (default: false)
|
|
18
21
|
# @param time_zone [Boolean] Uses Rails time zone select options (default: false)
|
|
19
22
|
# @param select_options [Hash] Options for select tag (prompt, selected, disabled, etc)
|
|
20
23
|
# @param choices [Array] List of choices for options (alternative to options)
|
|
21
|
-
# @param options [Array, Hash] List or hash of options
|
|
22
|
-
# @param
|
|
24
|
+
# @param options [Array, Hash, String] List or hash of options, or pre-formatted HTML from options_for_select
|
|
25
|
+
# @param prompt [String, Boolean, false] Prompt text, true to use default, or false to disable (optional, defaults to I18n if object/attribute present)
|
|
26
|
+
# @param include_blank [String, Boolean] Include blank option (optional)
|
|
27
|
+
# @param selected [Object] Pre-selected value (optional)
|
|
28
|
+
# @param disabled [Array, Object] Disabled options (optional)
|
|
29
|
+
# @param color [Symbol] Color state: :default, :success, :danger, :warning, :info (optional)
|
|
30
|
+
# @param sizing [Integer] Field size (0 to 2, default: 1)
|
|
23
31
|
# @param class [String] Additional CSS classes for the select element
|
|
24
32
|
# @param ... any other HTML attribute supported by <select>
|
|
25
33
|
def initialize(**props)
|
|
@@ -29,18 +37,38 @@ class Fluxbit::Form::SelectComponent < Fluxbit::Form::TextFieldComponent
|
|
|
29
37
|
@select_options = @props.delete(:select_options) || {}
|
|
30
38
|
@choices = @props.delete(:choices) || nil
|
|
31
39
|
@options = @props.delete(:options) || {}
|
|
40
|
+
|
|
41
|
+
# Extract select-specific options
|
|
42
|
+
prompt_value = @select_options.delete(:prompt) || @props.delete(:prompt)
|
|
43
|
+
@include_blank = @select_options.delete(:include_blank) || @props.delete(:include_blank)
|
|
44
|
+
@selected = @select_options.delete(:selected) || @props.delete(:selected)
|
|
45
|
+
@disabled_options = @select_options.delete(:disabled) || @props.delete(:disabled)
|
|
46
|
+
|
|
32
47
|
@options = ::ActiveSupport::TimeZone.all if @time_zone
|
|
48
|
+
|
|
49
|
+
# Define prompt with I18n support
|
|
50
|
+
define_prompt(prompt_value)
|
|
33
51
|
end
|
|
34
52
|
|
|
35
53
|
def input
|
|
36
54
|
if @form.present? && @attribute.present?
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
@
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
# form.select(attribute, choices, options = {}, html_options = {})
|
|
56
|
+
# For form builder with time zones, use the specialized helper
|
|
57
|
+
if @time_zone
|
|
58
|
+
@form.time_zone_select(@attribute, nil, build_select_options_hash, @props)
|
|
59
|
+
else
|
|
60
|
+
# form.select can accept raw choices OR pre-formatted option tags
|
|
61
|
+
# We use pre-formatted tags for consistency with grouped selects
|
|
62
|
+
@form.select(
|
|
63
|
+
@attribute,
|
|
64
|
+
build_options_for_select,
|
|
65
|
+
build_select_options_hash,
|
|
66
|
+
@props
|
|
67
|
+
)
|
|
68
|
+
end
|
|
43
69
|
else
|
|
70
|
+
# select_tag(name, option_tags = nil, options = {})
|
|
71
|
+
# option_tags should be pre-formatted HTML
|
|
44
72
|
select_tag(
|
|
45
73
|
@name,
|
|
46
74
|
build_options_for_select,
|
|
@@ -49,13 +77,38 @@ class Fluxbit::Form::SelectComponent < Fluxbit::Form::TextFieldComponent
|
|
|
49
77
|
end
|
|
50
78
|
end
|
|
51
79
|
|
|
80
|
+
# Build options hash for form.select (prompt, include_blank, etc.)
|
|
81
|
+
# Note: Don't include selected/disabled if options are pre-formatted HTML
|
|
82
|
+
def build_select_options_hash
|
|
83
|
+
options_hash = @select_options.dup
|
|
84
|
+
options_hash[:prompt] = @prompt if @prompt
|
|
85
|
+
options_hash[:include_blank] = @include_blank if @include_blank
|
|
86
|
+
|
|
87
|
+
# Only add selected/disabled if we're building options from raw data
|
|
88
|
+
unless options_are_preformatted?
|
|
89
|
+
options_hash[:selected] = @selected if @selected
|
|
90
|
+
options_hash[:disabled] = @disabled_options if @disabled_options
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
options_hash
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Build pre-formatted option tags for select_tag and form.select
|
|
52
97
|
def build_options_for_select
|
|
53
|
-
|
|
98
|
+
# If options are already HTML (from options_for_select/grouped_options_for_select), use as-is
|
|
99
|
+
if options_are_preformatted?
|
|
100
|
+
html = @options.dup
|
|
101
|
+
# Add prompt if needed and not already in the HTML (only for select_tag)
|
|
102
|
+
html = add_prompt_to_html(html) if @prompt && !html.include?(@prompt.to_s) && !using_form_builder?
|
|
103
|
+
return html
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Otherwise, build the HTML from raw data
|
|
107
|
+
html = if @grouped
|
|
54
108
|
grouped_options_for_select(
|
|
55
109
|
@options,
|
|
56
110
|
@selected,
|
|
57
111
|
disabled: @disabled_options,
|
|
58
|
-
prompt: @prompt,
|
|
59
112
|
divider: @divider
|
|
60
113
|
)
|
|
61
114
|
elsif @time_zone
|
|
@@ -63,10 +116,54 @@ class Fluxbit::Form::SelectComponent < Fluxbit::Form::TextFieldComponent
|
|
|
63
116
|
else
|
|
64
117
|
options_for_select(@options, selected: @selected, disabled: @disabled_options)
|
|
65
118
|
end
|
|
119
|
+
|
|
120
|
+
# Add prompt option at the beginning if specified
|
|
121
|
+
# Only add it manually for select_tag (form.select handles it via options hash)
|
|
122
|
+
html = add_prompt_to_html(html) if @prompt && !using_form_builder?
|
|
123
|
+
html
|
|
66
124
|
end
|
|
67
125
|
|
|
68
126
|
private
|
|
69
127
|
|
|
128
|
+
# Check if we're using a form builder
|
|
129
|
+
def using_form_builder?
|
|
130
|
+
@form.present? && @attribute.present?
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def define_prompt(prompt)
|
|
134
|
+
# If prompt is explicitly false, don't set it
|
|
135
|
+
return if prompt.is_a?(FalseClass)
|
|
136
|
+
|
|
137
|
+
# If prompt is explicitly provided (string or true), use it
|
|
138
|
+
if prompt.present?
|
|
139
|
+
@prompt = prompt
|
|
140
|
+
return
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# If no prompt provided and we have an object and attribute, try I18n lookup
|
|
144
|
+
if prompt.nil? && @object.present? && @attribute.present?
|
|
145
|
+
i18n_prompt = I18n.t(
|
|
146
|
+
@attribute,
|
|
147
|
+
scope: [ @object.class.name.pluralize.underscore.to_sym, :prompts ],
|
|
148
|
+
default: nil
|
|
149
|
+
)
|
|
150
|
+
@prompt = i18n_prompt if i18n_prompt.present?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Check if options are already pre-formatted HTML
|
|
155
|
+
# Pre-formatted options are strings containing HTML tags
|
|
156
|
+
def options_are_preformatted?
|
|
157
|
+
@options.is_a?(String) && @options.include?("<option")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Add prompt option to the beginning of the HTML options
|
|
161
|
+
def add_prompt_to_html(html)
|
|
162
|
+
prompt_text = @prompt.is_a?(String) ? @prompt : "Please select"
|
|
163
|
+
prompt_option = "<option value=\"\">#{ERB::Util.html_escape(prompt_text)}</option>".html_safe
|
|
164
|
+
prompt_option + html
|
|
165
|
+
end
|
|
166
|
+
|
|
70
167
|
def grouped_selected_option
|
|
71
168
|
@options.each do |group|
|
|
72
169
|
group_to_traverse = @divider ? group[1] : group
|