fluxbit_view_components 0.2.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.
Files changed (173) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/app/assets/javascripts/fluxbit_view_components/assigner_controller.js +49 -0
  4. data/app/assets/javascripts/fluxbit_view_components/auto_submit_controller.js +39 -0
  5. data/app/assets/javascripts/fluxbit_view_components/drawer_controller.js +135 -0
  6. data/app/assets/javascripts/fluxbit_view_components/index.js +56 -0
  7. data/app/assets/javascripts/fluxbit_view_components/method_link_controller.js +143 -0
  8. data/app/assets/javascripts/fluxbit_view_components/modal_controller.js +118 -0
  9. data/app/assets/javascripts/fluxbit_view_components/password_controller.js +170 -0
  10. data/app/assets/javascripts/fluxbit_view_components/progress_controller.js +374 -0
  11. data/app/assets/javascripts/fluxbit_view_components/row_click_controller.js +32 -0
  12. data/app/assets/javascripts/fluxbit_view_components/select_all_controller.js +122 -0
  13. data/app/assets/javascripts/fluxbit_view_components/spinner_percent_controller.js +174 -0
  14. data/app/assets/javascripts/fluxbit_view_components/theme_button_controller.js +90 -0
  15. data/app/assets/javascripts/fluxbit_view_components.js +1175 -0
  16. data/app/components/fluxbit/accordion_component.rb +125 -0
  17. data/app/components/fluxbit/alert_component.rb +8 -8
  18. data/app/components/fluxbit/avatar_component.rb +11 -12
  19. data/app/components/fluxbit/avatar_group_component.rb +1 -1
  20. data/app/components/fluxbit/badge_component.rb +8 -7
  21. data/app/components/fluxbit/banner_component.rb +139 -0
  22. data/app/components/fluxbit/bottom_navigation_component.rb +437 -0
  23. data/app/components/fluxbit/breadcrumb_component.rb +66 -0
  24. data/app/components/fluxbit/button_component.rb +39 -11
  25. data/app/components/fluxbit/button_group_component.rb +1 -1
  26. data/app/components/fluxbit/card_component.rb +26 -23
  27. data/app/components/fluxbit/carousel_component.rb +154 -0
  28. data/app/components/fluxbit/component.rb +24 -3
  29. data/app/components/fluxbit/drawer_component.html.erb +30 -0
  30. data/app/components/fluxbit/drawer_component.rb +125 -0
  31. data/app/components/fluxbit/dropdown_component.rb +41 -0
  32. data/app/components/fluxbit/dropdown_item_component.rb +68 -0
  33. data/app/components/fluxbit/flex_component.rb +1 -1
  34. data/app/components/fluxbit/form/check_box_component.rb +56 -0
  35. data/app/components/fluxbit/form/component.rb +27 -26
  36. data/app/components/fluxbit/form/dropzone_component.html.erb +39 -0
  37. data/app/components/fluxbit/form/dropzone_component.rb +39 -0
  38. data/app/components/fluxbit/form/field_component.rb +28 -0
  39. data/app/components/fluxbit/form/form_builder_component.rb +1 -1
  40. data/app/components/fluxbit/form/{helper_text_component.rb → help_text_component.rb} +9 -4
  41. data/app/components/fluxbit/form/label_component.rb +40 -30
  42. data/app/components/fluxbit/form/password_component.rb +247 -0
  43. data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
  44. data/app/components/fluxbit/form/range_component.rb +52 -0
  45. data/app/components/fluxbit/form/select_component.rb +185 -0
  46. data/app/components/fluxbit/form/text_field_component.rb +185 -0
  47. data/app/components/fluxbit/form/toggle_component.html.erb +23 -0
  48. data/app/components/fluxbit/form/toggle_component.rb +81 -0
  49. data/app/components/fluxbit/form/upload_image_component.html.erb +50 -0
  50. data/app/components/fluxbit/form/upload_image_component.rb +61 -0
  51. data/app/components/fluxbit/gravatar_component.rb +7 -0
  52. data/app/components/fluxbit/icon_helpers.rb +167 -0
  53. data/app/components/fluxbit/link_component.rb +42 -0
  54. data/app/components/fluxbit/modal_component.rb +28 -31
  55. data/app/components/fluxbit/pagination_component.rb +206 -0
  56. data/app/components/fluxbit/popover_component.rb +14 -14
  57. data/app/components/fluxbit/progress_component.rb +196 -0
  58. data/app/components/fluxbit/skeleton_component.rb +237 -0
  59. data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
  60. data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
  61. data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
  62. data/app/components/fluxbit/speed_dial_component.rb +73 -0
  63. data/app/components/fluxbit/spinner_component.rb +71 -0
  64. data/app/components/fluxbit/spinner_percent_component.rb +174 -0
  65. data/app/components/fluxbit/stepper_component.rb +223 -0
  66. data/app/components/fluxbit/tab_component.rb +44 -25
  67. data/app/components/fluxbit/table_component.rb +186 -0
  68. data/app/components/fluxbit/table_group_component.rb +28 -0
  69. data/app/components/fluxbit/theme_button_component.rb +64 -0
  70. data/app/components/fluxbit/timeline_component.rb +63 -0
  71. data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
  72. data/app/components/fluxbit/timeline_item_component.rb +78 -0
  73. data/app/components/fluxbit/tooltip_component.rb +2 -2
  74. data/app/helpers/fluxbit/components_helper.rb +93 -51
  75. data/app/helpers/fluxbit/form_builder.rb +136 -0
  76. data/app/helpers/fluxbit/view_helper.rb +71 -0
  77. data/config/locales/en.yml +37 -4
  78. data/config/locales/pt-BR.yml +36 -0
  79. data/lib/fluxbit/config/accordion_component.rb +73 -0
  80. data/lib/fluxbit/config/avatar_component.rb +11 -11
  81. data/lib/fluxbit/config/badge_component.rb +14 -11
  82. data/lib/fluxbit/config/banner_component.rb +60 -0
  83. data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
  84. data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
  85. data/lib/fluxbit/config/button_component.rb +6 -4
  86. data/lib/fluxbit/config/card_component.rb +23 -12
  87. data/lib/fluxbit/config/carousel_component.rb +33 -0
  88. data/lib/fluxbit/config/drawer_component.rb +48 -0
  89. data/lib/fluxbit/config/dropdown_component.rb +29 -0
  90. data/lib/fluxbit/config/form/check_box_component.rb +19 -0
  91. data/lib/fluxbit/config/form/dropzone_component.rb +20 -0
  92. data/lib/fluxbit/config/form/{helper_text_component.rb → help_text_component.rb} +2 -2
  93. data/lib/fluxbit/config/form/label_component.rb +31 -0
  94. data/lib/fluxbit/config/form/password_component.rb +19 -0
  95. data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
  96. data/lib/fluxbit/config/form/range_component.rb +15 -0
  97. data/lib/fluxbit/config/form/text_field_component.rb +76 -0
  98. data/lib/fluxbit/config/form/toggle_component.rb +79 -0
  99. data/lib/fluxbit/config/link_component.rb +24 -0
  100. data/lib/fluxbit/config/modal_component.rb +1 -1
  101. data/lib/fluxbit/config/pagination_component.rb +31 -0
  102. data/lib/fluxbit/config/popover_component.rb +1 -1
  103. data/lib/fluxbit/config/progress_component.rb +63 -0
  104. data/lib/fluxbit/config/skeleton_component.rb +82 -0
  105. data/lib/fluxbit/config/speed_dial_component.rb +50 -0
  106. data/lib/fluxbit/config/spinner_component.rb +30 -0
  107. data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
  108. data/lib/fluxbit/config/stepper_component.rb +299 -0
  109. data/lib/fluxbit/config/tab_component.rb +6 -0
  110. data/lib/fluxbit/config/table_component.rb +75 -0
  111. data/lib/fluxbit/config/theme_button_component.rb +19 -0
  112. data/lib/fluxbit/config/timeline_component.rb +77 -0
  113. data/lib/fluxbit/view_components/engine.rb +11 -3
  114. data/lib/fluxbit/view_components/version.rb +1 -1
  115. data/lib/fluxbit/view_components.rb +27 -1
  116. data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
  117. data/lib/generators/fluxbit/pagy_generator.rb +39 -0
  118. data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
  119. data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
  120. data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
  121. data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
  122. data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
  123. data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
  124. data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
  125. data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
  126. data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
  127. data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
  128. data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
  129. data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
  130. data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
  131. data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
  132. data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
  133. data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
  134. data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
  135. data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
  136. data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
  137. data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
  138. data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
  139. data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
  140. data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
  141. data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
  142. data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
  143. data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
  144. data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
  145. data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
  146. data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
  147. data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
  148. data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
  149. data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
  150. data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
  151. data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
  152. data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
  153. data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
  154. data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
  155. data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
  156. data/lib/install/install.rb +61 -3
  157. metadata +127 -35
  158. data/LICENSE.txt +0 -20
  159. data/app/components/fluxbit/form/checkbox_input_component.rb +0 -61
  160. data/app/components/fluxbit/form/datepicker_component.rb +0 -7
  161. data/app/components/fluxbit/form/radio_input_component.rb +0 -21
  162. data/app/components/fluxbit/form/range_input_component.rb +0 -51
  163. data/app/components/fluxbit/form/select_free_input_component.rb +0 -77
  164. data/app/components/fluxbit/form/select_input_component.rb +0 -21
  165. data/app/components/fluxbit/form/spacer_input_component.rb +0 -12
  166. data/app/components/fluxbit/form/text_input_component.rb +0 -225
  167. data/app/components/fluxbit/form/textarea_input_component.rb +0 -57
  168. data/app/components/fluxbit/form/toggle_input_component.rb +0 -166
  169. data/app/components/fluxbit/form/upload_image_input_component.html.erb +0 -48
  170. data/app/components/fluxbit/form/upload_image_input_component.rb +0 -61
  171. data/app/components/fluxbit/form/upload_input_component.html.erb +0 -12
  172. data/app/components/fluxbit/form/upload_input_component.rb +0 -47
  173. data/app/helpers/fluxbit/classes_helper.rb +0 -9
@@ -6,32 +6,36 @@ class Fluxbit::Form::Component < Fluxbit::Component
6
6
  return @id ||= random_id if @props[:id].nil? && @form.nil?
7
7
  return @props[:id] unless @props[:id].nil?
8
8
 
9
- "#{@form.object_name}_#{@field}"
9
+ "#{@form.object_name}_#{@attribute}"
10
10
  end
11
11
 
12
- def define_helper_text(helper_text, object, field)
13
- return nil if helper_text.is_a? FalseClass
12
+ def define_help_text(help_text, object, attribute)
13
+ return nil if help_text.is_a? FalseClass
14
14
 
15
- if helper_text.nil? && !object.nil? && !field.nil?
16
- helper_text = I18n.t(
17
- field,
18
- scope: [ :activerecord, :helper_text, object.class.name.underscore.to_sym ],
15
+ if help_text.nil? && !object.nil? && !attribute.nil?
16
+ help_text = I18n.t(
17
+ attribute,
18
+ scope: [ object.class.name.pluralize.underscore.to_sym, :help_text ],
19
19
  default: nil
20
20
  )
21
21
  end
22
22
 
23
- (helper_text.is_a?(Array) ? helper_text : [ helper_text ]) + errors
23
+ (help_text.is_a?(Array) ? help_text : [ help_text ]) + errors
24
24
  end
25
25
 
26
- def define_helper_popover(helper_popover, object, field)
27
- return helper_popover if (helper_popover != false && !helper_popover.nil?) || object.nil?
26
+ def define_helper_popover(helper_popover, object, attribute)
27
+ return nil if helper_popover == false
28
+ return helper_popover if !helper_popover.nil? || object.nil?
28
29
 
29
- object_name = object.class.name.underscore.to_sym
30
- I18n.t(field, scope: [ :activerecord, :helper_popover, object_name ], default: nil)
30
+ I18n.t(attribute, scope: [ object.class.name.pluralize.underscore.to_sym, :helper_popover ], default: nil)
31
31
  end
32
32
 
33
- def label_value(label, object, field, id)
34
- return object.class.human_attribute_name(field) if label.nil? && !object.nil? && !field.nil?
33
+ def label_value(label, object, attribute, id)
34
+ if label.nil? && !object.nil? && !attribute.nil?
35
+ key = [ object.class.name.pluralize.underscore.to_sym, :fields, attribute ]
36
+ return I18n.exists?(key) ? I18n.t(attribute, scope: key[0..-2]) : object.class.human_attribute_name(attribute)
37
+ end
38
+ return attribute.to_s.humanize if label.nil? && object.nil?
35
39
  return id.to_s.humanize if label.nil? && !id.nil?
36
40
  return label unless label.nil?
37
41
 
@@ -46,27 +50,24 @@ class Fluxbit::Form::Component < Fluxbit::Component
46
50
  color: @color,
47
51
  helper_popover: @helper_popover,
48
52
  helper_popover_placement: @helper_popover_placement,
49
- class: @label_class
53
+ class: @label_class,
54
+ required: @required
50
55
  ).with_content(@label).render_in(view_context)
51
56
  end
52
57
 
53
58
  def errors
54
59
  return [] unless @object&.errors&.any?
55
60
 
56
- @object.errors.filter { |f| f.attribute == @field }.map(&:full_message)
61
+ @object.errors.filter { |f| f.attribute == @attribute }.map(&:full_message)
57
62
  end
58
63
 
59
- def helper_text
60
- return "" if @helper_text.blank?
61
-
62
- # safe_join(
63
- # @helper_text.compact.map do |text|
64
- # Fluxbit::HelperTextComponent.new(color: @color).with_content(text).render_in(view_context)
65
- # end
66
- # )
64
+ def help_text
65
+ return "" if @help_text.blank? || @help_text.compact.blank?
67
66
 
68
- @helper_text.compact.map do |text|
69
- concat Fluxbit::Form::HelperTextComponent.new(color: @color).with_content(text).render_in(view_context)
67
+ nodes = @help_text.compact.map do |text|
68
+ Fluxbit::Form::HelpTextComponent.new(color: @color).with_content(text).render_in(view_context)
70
69
  end
70
+
71
+ view_context.safe_join(nodes)
71
72
  end
72
73
  end
@@ -0,0 +1,39 @@
1
+ <%= content_tag :div, **@wrapper_html do %>
2
+ <div class="<%= self.styles[:base] %>">
3
+ <label for="<%= id %>" class="<%= self.styles[:label] %> <%= self.styles[:height][@height] %>">
4
+ <div class="<%= self.styles[:inner_div] %>">
5
+ <% if content? %>
6
+ <%= content %>
7
+ <% else %>
8
+ <%= create_icon %>
9
+
10
+ <% if @title != false %>
11
+ <p class="<%= self.styles[:title] %>">
12
+ <% if @title != true %>
13
+ <%= @title %>
14
+ <% else %>
15
+ <span class="font-semibold">Click to upload</span> or drag and drop
16
+ <% end %>
17
+ </p>
18
+ <% end %>
19
+
20
+ <% if @subtitle != false %>
21
+ <p class="<%= self.styles[:subtitle] %>">
22
+ <% if @subtitle != true %>
23
+ <%= @subtitle %>
24
+ <% else %>
25
+ SVG, PNG, JPG or GIF (MAX. 800x400px)
26
+ <% end %>
27
+ </p>
28
+ <% end %>
29
+ <% end %>
30
+ </div>
31
+ <% if @form.present? && @attribute.present? %>
32
+ <%= @form.file_field(@attribute, **@props) %>
33
+ <% else %>
34
+ <%= file_field_tag(@name, **@props) %>
35
+ <% end %>
36
+ </label>
37
+ </div>
38
+ <%= help_text %>
39
+ <% end %>
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::Form::DropzoneComponent` provides a drag-and-drop file input zone with support for labels,
4
+ # titles, subtitles, icons, validation states, and integration with Rails form builders.
5
+ # It renders a visually rich area that lets users drag files or click to select a file, and is fully customizable
6
+ # via its options and slot for custom content.
7
+ #
8
+ # @example Basic usage
9
+ # = render Fluxbit::Form::DropzoneComponent.new(name: :avatar)
10
+ #
11
+ # @see docs/03_Forms/Dropzone.md For detailed documentation and examples.
12
+ class Fluxbit::Form::DropzoneComponent < Fluxbit::Form::FieldComponent
13
+ include Fluxbit::Config::Form::DropzoneComponent
14
+
15
+ # Initializes the dropzone component with the given properties.
16
+ #
17
+ # @param name [String] Name of the field (required unless using form builder)
18
+ # @param label [String] Label for the input (optional)
19
+ # @param title [Boolean, String] Title text above the dropzone (true for default, false to hide, or custom string)
20
+ # @param subtitle [Boolean, String] Subtitle text below the title (true for default, false to hide, or custom string)
21
+ # @param icon [String, Symbol] Icon to display above the title (defaults to config)
22
+ # @param icon_html [Hash] Extra props for the icon element
23
+ # @param height [Integer] Height preset (0: auto, 1: h-32, 2: h-64, 3: h-96; default is 0)
24
+ # @param help_text [String] Helper or error text below the field
25
+ # @param ... any other HTML attribute supported by file_field_tag
26
+ def initialize(**props)
27
+ super(**props)
28
+ @title = options(@props.delete(:title), default: true)
29
+ @subtitle = options(@props.delete(:subtitle), default: true)
30
+ @icon = @props.delete(:icon) || @@icon
31
+ @icon_html = @props.delete(:icon_html) || { class: styles[:icon] }
32
+ @height = @props.delete(:height) || @@height
33
+ add to: @props, class: "hidden"
34
+ end
35
+
36
+ def create_icon
37
+ anyicon(@icon, class: styles[:icon])
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Fluxbit::Form::FieldComponent < Fluxbit::Form::Component
4
+ def initialize(**props)
5
+ super
6
+ @props = props
7
+ @form = @props.delete(:form)
8
+ @attribute = @props.delete(:attribute)
9
+ @name = @props.delete(:name) || (@attribute if @form.present?)
10
+ @value = @props.delete(:value)
11
+ @id = @props.delete(:id)
12
+ @required = @props.delete(:required)
13
+
14
+ @object = @form&.object
15
+ @help_text = define_help_text(props.delete(:help_text), @object, @attribute)
16
+ @helper_popover = define_helper_popover(props.delete(:helper_popover), @object, @attribute)
17
+ @helper_popover_placement = props.delete(:helper_popover_placement) || "right"
18
+ @label = label_value(props.delete(:label), @object, @attribute, @id)
19
+ @wrapper_html = props.delete(:wrapper_html) || {}
20
+ @wrapper_html = { class: @wrapper_html } if @wrapper_html.is_a?(String)
21
+ define_wrapper_options
22
+ end
23
+
24
+ def define_wrapper_options
25
+ add(to: @wrapper_html, class: "required") if @required.present?
26
+ add(to: @wrapper_html, class: @name.to_s) if @name.present?
27
+ end
28
+ end
@@ -89,7 +89,7 @@ class Fluxbit::Form::FormBuilderComponent < Fluxbit::Component
89
89
  end
90
90
 
91
91
  def element_type(type)
92
- return "TextInput" if type.nil? || type.to_s.in?(TEXT_TYPES)
92
+ return "TextField" if type.nil? || type.to_s.in?(TEXT_TYPES)
93
93
  return type.to_s.concat("_input").camelcase if type.to_s.in?(INPUT_TYPES)
94
94
 
95
95
  case type
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The `Fluxbit::HelperTextComponent` is a component for rendering customizable helper text elements.
3
+ # The `Fluxbit::HelpTextComponent` is a component for rendering customizable helper text elements.
4
4
  # It extends `Fluxbit::Component` and provides options for configuring the helper text's
5
5
  # appearance and behavior. You can control the helper text's color and other attributes.
6
6
  # The helper text can have various styles applied based on the provided properties.
7
- class Fluxbit::Form::HelperTextComponent < Fluxbit::Form::Component
8
- include Fluxbit::Config::Form::HelperTextComponent
7
+ #
8
+ # @example Basic usage
9
+ # = render Fluxbit::Form::HelpTextComponent.new { "Your password must be at least 8 characters." }
10
+ #
11
+ # @see docs/03_Forms/HelpText.md For detailed documentation and examples.
12
+ class Fluxbit::Form::HelpTextComponent < Fluxbit::Form::Component
13
+ include Fluxbit::Config::Form::HelpTextComponent
9
14
 
10
15
  # Initializes the helper text component with the given properties.
11
16
  #
@@ -15,7 +20,7 @@ class Fluxbit::Form::HelperTextComponent < Fluxbit::Form::Component
15
20
  def initialize(color: nil, **props)
16
21
  super
17
22
  @props = props
18
- color = @@color unless color.in? %i[info default success failure warning]
23
+ color = @@color unless color.in? styles[:colors].keys
19
24
  add class: style(color), to: @props, first_element: true
20
25
  end
21
26
 
@@ -1,36 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # The `Fluxbit::Form::LabelComponent` is a flexible and accessible label for form fields.
4
+ # It supports custom content, helper popovers, multiple color styles, sizing options, and
5
+ # displays associated help text when provided. It is fully compatible with Rails form builders.
6
+ #
7
+ # @example Basic usage
8
+ # = render Fluxbit::Form::LabelComponent.new(with_content: "Your Name")
9
+ #
10
+ # @see docs/03_Forms/Label.md For detailed documentation and examples.
3
11
  class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
4
- cattr_accessor :styles do
5
- {
6
- base: "flex font-medium",
7
- colors: {
8
- default: "text-gray-900 dark:text-white",
9
- success: "text-green-700 dark:text-green-500",
10
- failure: "text-red-700 dark:text-red-500",
11
- info: "text-cyan-500 dark:text-cyan-600",
12
- warning: "text-yellow-500 dark:text-yellow-600"
13
- },
14
- sizes: {
15
- sm: "text-sm",
16
- md: "text-md",
17
- lg: "text-lg"
18
- },
19
- helper_popover: "px-2 text-slate-400"
20
- }
21
- end
12
+ include Fluxbit::Config::Form::LabelComponent
22
13
 
23
- def initialize(color: :default, form: nil, with_content: nil, helper_text: nil,
24
- sizing: :sm, helper_popover: nil, helper_popover_placement: "right", **props)
14
+ # Initializes the label component with the given properties.
15
+ #
16
+ # @param with_content [String] The label text to display (alternative to block content)
17
+ # @param help_text [String, Array<String>] One or more help text messages to render below the label
18
+ # @param helper_popover [String] Popover content shown on icon hover
19
+ # @param helper_popover_placement [String] Placement of the popover (default: "right")
20
+ # @param sizing [Integer] Size index for label text (default: config default)
21
+ # @param color [Symbol] Label color (:default, :success, :danger, :info, :warning)
22
+ # @param class [String] Additional CSS classes for the label element
23
+ # @param ... any other HTML attribute supported by the <label> tag
24
+ def initialize(**props)
25
25
  super
26
26
  @props = props
27
- @sizing = sizing.in?(styles[:sizes].keys) ? sizing : :sm
28
- @with_content = with_content
29
- @helper_text = helper_text.is_a?(Array) ? helper_text : [ helper_text ]
30
- @helper_popover = helper_popover
31
- @helper_popover_placement = helper_popover_placement
32
- color = :default unless color.in? %i[info default success failure warning]
33
- add class: styles[:colors][color], to: @props, first_element: true
27
+ @with_content = @props.delete(:with_content)
28
+ @help_text = @props.delete(:help_text)
29
+ @help_text = [ @help_text ] if !@help_text.is_a?(Array)
30
+ @helper_popover = @props.delete(:helper_popover)
31
+ @helper_popover_placement = @props.delete(:helper_popover_placement) || @@helper_popover_placement
32
+ @sizing = @props[:sizing].to_i || @@sizing
33
+ @sizing = (styles[:sizes].count - 1) if @sizing > (styles[:sizes].count - 1)
34
+ @color = options(@props.delete(:color), collection: styles[:colors], default: @@color)
35
+ @required = @props.delete(:required) || false
36
+
37
+ add class: styles[:colors][@color], to: @props, first_element: true
34
38
  add class: styles[:base], to: @props, first_element: true
35
39
  add class: styles[:sizes][@sizing], to: @props, first_element: true
36
40
  end
@@ -39,7 +43,7 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
39
43
  return "" if @helper_popover.nil?
40
44
 
41
45
  content_tag :span,
42
- anyicon(icon: "heroicons_solid:question-mark-circle", class: "w-4 h-4"),
46
+ anyicon(@@helper_popover_icon, class: @@helper_popover_icon_class),
43
47
  {
44
48
  "data-popover-placement": @helper_popover_placement,
45
49
  "data-popover-target": target,
@@ -56,10 +60,16 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
56
60
  def call
57
61
  safe_join(
58
62
  [
59
- content_tag(:label, safe_join([ content || @with_content, span_helper_popover ]), @props),
60
- helper_text,
63
+ content_tag(:label, safe_join([ content || @with_content, span_helper_popover, required ]), @props),
64
+ help_text,
61
65
  render_popover
62
66
  ]
63
67
  )
64
68
  end
69
+
70
+ def required
71
+ return "" unless @required
72
+
73
+ content_tag(:span, "*", class: styles[:required])
74
+ end
65
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