fluxbit_view_components 0.2.0 → 0.3.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/components/fluxbit/form/check_box_component.rb +56 -0
  3. data/app/components/fluxbit/form/component.rb +18 -24
  4. data/app/components/fluxbit/form/dropzone_component.html.erb +39 -0
  5. data/app/components/fluxbit/form/dropzone_component.rb +39 -0
  6. data/app/components/fluxbit/form/field_component.rb +26 -0
  7. data/app/components/fluxbit/form/form_builder_component.rb +1 -1
  8. data/app/components/fluxbit/form/{helper_text_component.rb → help_text_component.rb} +8 -3
  9. data/app/components/fluxbit/form/label_component.rb +32 -29
  10. data/app/components/fluxbit/form/range_component.rb +52 -0
  11. data/app/components/fluxbit/form/select_component.rb +88 -0
  12. data/app/components/fluxbit/form/text_field_component.rb +168 -0
  13. data/app/components/fluxbit/form/toggle_component.html.erb +23 -0
  14. data/app/components/fluxbit/form/toggle_component.rb +81 -0
  15. data/app/components/fluxbit/form/upload_image_component.html.erb +50 -0
  16. data/app/components/fluxbit/form/upload_image_component.rb +50 -0
  17. data/app/helpers/fluxbit/components_helper.rb +23 -51
  18. data/app/helpers/fluxbit/form_builder.rb +87 -0
  19. data/lib/fluxbit/config/form/check_box_component.rb +19 -0
  20. data/lib/fluxbit/config/form/dropzone_component.rb +20 -0
  21. data/lib/fluxbit/config/form/{helper_text_component.rb → help_text_component.rb} +1 -1
  22. data/lib/fluxbit/config/form/label_component.rb +30 -0
  23. data/lib/fluxbit/config/form/range_component.rb +15 -0
  24. data/lib/fluxbit/config/form/text_field_component.rb +76 -0
  25. data/{app/components/fluxbit/form/toggle_input_component.rb → lib/fluxbit/config/form/toggle_component.rb} +28 -115
  26. data/lib/fluxbit/view_components/version.rb +1 -1
  27. data/lib/fluxbit/view_components.rb +7 -1
  28. data/lib/install/install.rb +3 -3
  29. metadata +21 -18
  30. data/LICENSE.txt +0 -20
  31. data/app/components/fluxbit/form/checkbox_input_component.rb +0 -61
  32. data/app/components/fluxbit/form/datepicker_component.rb +0 -7
  33. data/app/components/fluxbit/form/radio_input_component.rb +0 -21
  34. data/app/components/fluxbit/form/range_input_component.rb +0 -51
  35. data/app/components/fluxbit/form/select_free_input_component.rb +0 -77
  36. data/app/components/fluxbit/form/select_input_component.rb +0 -21
  37. data/app/components/fluxbit/form/spacer_input_component.rb +0 -12
  38. data/app/components/fluxbit/form/text_input_component.rb +0 -225
  39. data/app/components/fluxbit/form/textarea_input_component.rb +0 -57
  40. data/app/components/fluxbit/form/upload_image_input_component.html.erb +0 -48
  41. data/app/components/fluxbit/form/upload_image_input_component.rb +0 -61
  42. data/app/components/fluxbit/form/upload_input_component.html.erb +0 -12
  43. data/app/components/fluxbit/form/upload_input_component.rb +0 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbaf175424393a228d6fc82018fcd81b93dc388df861dd8afc1fb4ce0ab901b1
4
- data.tar.gz: '0728a9d9f386fcb06bf8e21898e39c0cf58a9b2f44763acfb6fe591d251f2600'
3
+ metadata.gz: 5f5ca4fde5d32556cdbc0f25088912fa549531dea43e9e46557806c742a61a3c
4
+ data.tar.gz: 2feca2ea7274688b6d1e850aee6dc6d5638718718584e977514f0abf96cedd8e
5
5
  SHA512:
6
- metadata.gz: 618572080999a095ed82ed77875fae33507d208c5c0abbca39bd410742502e651215f3f7c5cedab195baf0d2a486aaa5ce45fcc4910646ab7806f33454c6d98f
7
- data.tar.gz: e1e05c2e0445d8c9f92bae5f3580e3f08f235c65b0bab273538ca45acd3f07df2e2e35429934178883682c89ace072816010c79ae2ac435b6b9892894356a420
6
+ metadata.gz: 85ef792f0b9ead019bf89459531723043c7b771fe7b6c8f016ba03697d2e80d93139a081e0f4250d46545eb97b31f159fcaa9ba2bd5592c27053a0792c8063f5
7
+ data.tar.gz: da59d2153f6ea5bb116ee44ae2dd7fd8cd8631c9024ebe7972d5103eb58f1fc05012953386d4520aed4247970dc00fc810ede2afc08a0ecc8329db053b3d5cb6
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::Form::CheckBoxComponent` is a form input component for check boxes and radio buttons.
4
+ # It extends `Fluxbit::Form::FieldComponent` and provides a styled checkbox/radio with label, helper text,
5
+ # and support for different visual states and groupings. It automatically adds the correct styles for both
6
+ # checkbox and radio types and works with or without Rails form builders.
7
+ #
8
+ # @example Basic usage
9
+ # = render Fluxbit::Form::CheckBoxComponent.new(name: :accept_terms, label: "Accept the terms")
10
+ #
11
+ # @see docs/03_Forms/CheckBox.md For detailed documentation and examples.
12
+ class Fluxbit::Form::CheckBoxComponent < Fluxbit::Form::FieldComponent
13
+ include Fluxbit::Config::Form::CheckBoxComponent
14
+ TYPE_DEFAULT = :check_box
15
+ TYPE_OPTIONS = %i[check_box checkbox radio_button].freeze
16
+
17
+ # Initializes the check box component with the given properties.
18
+ #
19
+ # @param name [String] Name of the field (required unless using form builder)
20
+ # @param label [String] Label text next to the input (optional)
21
+ # @param value [String] Value for the field (optional)
22
+ # @param type [String, Symbol] Input type (`"check_box"`, `"checkbox"`, `"radio_button"`)
23
+ # @param help_text [String] Helper or error text below the field
24
+ # @param disabled [Boolean] Disables the input if true
25
+ # @param checked [Boolean] Marks the input as checked if true
26
+ # @param class [String] Additional CSS classes for the input element
27
+ # @param ... any other HTML attribute supported by check_box_tag/radio_button_tag
28
+ def initialize(**props)
29
+ super(**props)
30
+ @type = options(@props.delete(:type), collection: TYPE_OPTIONS, default: TYPE_DEFAULT)
31
+ add(class: styles[:checkbox], to: @props, first_element: true) if @props[:type] == "checkbox"
32
+ add(class: styles[:base], to: @props, first_element: true)
33
+ end
34
+
35
+ def input
36
+ if @form.present? && @attribute.present?
37
+ @form.public_send(@type, @attribute, @props)
38
+ else
39
+ public_send("#{@type}_tag", @name, @value, @props)
40
+ end
41
+ end
42
+
43
+ def call
44
+ if @help_text
45
+ content_tag :div, { class: "flex" } do
46
+ concat content_tag(:div, input, { class: styles[:input_div] })
47
+ concat content_tag(:div, safe_join([ label, help_text ]), { class: styles[:helper_div] })
48
+ end
49
+ else
50
+ content_tag :div, { class: styles[:no_helper_div] } do
51
+ concat input
52
+ concat label
53
+ end
54
+ end
55
+ end
56
+ end
@@ -6,32 +6,32 @@ 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: [ :activerecord, :help_text, object.class.name.underscore.to_sym ],
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)
26
+ def define_helper_popover(helper_popover, object, attribute)
27
27
  return helper_popover if (helper_popover != false && !helper_popover.nil?) || object.nil?
28
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: [ :activerecord, :helper_popover, object_name ], 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
+ return object.class.human_attribute_name(attribute) if label.nil? && !object.nil? && !attribute.nil?
35
35
  return id.to_s.humanize if label.nil? && !id.nil?
36
36
  return label unless label.nil?
37
37
 
@@ -53,20 +53,14 @@ class Fluxbit::Form::Component < Fluxbit::Component
53
53
  def errors
54
54
  return [] unless @object&.errors&.any?
55
55
 
56
- @object.errors.filter { |f| f.attribute == @field }.map(&:full_message)
56
+ @object.errors.filter { |f| f.attribute == @attribute }.map(&:full_message)
57
57
  end
58
58
 
59
- def helper_text
60
- return "" if @helper_text.blank?
59
+ def help_text
60
+ return "" if @help_text.blank? || @help_text.compact.blank?
61
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
- # )
67
-
68
- @helper_text.compact.map do |text|
69
- concat Fluxbit::Form::HelperTextComponent.new(color: @color).with_content(text).render_in(view_context)
70
- end
62
+ @help_text.compact.map do |text|
63
+ Fluxbit::Form::HelpTextComponent.new(color: @color).with_content(text).render_in(view_context)
64
+ end.join.html_safe
71
65
  end
72
66
  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_props [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_props = @props.delete(:icon_props) || { 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: @icon, class: styles[:icon])
38
+ end
39
+ end
@@ -0,0 +1,26 @@
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
+
13
+ @object = @form&.object
14
+ @help_text = define_help_text(props.delete(:help_text), @object, @attribute)
15
+ @helper_popover = define_helper_popover(props.delete(:helper_popover), @object, @attribute)
16
+ @helper_popover_placement = props.delete(:helper_popover_placement) || "right"
17
+ @label = label_value(props.delete(:label), @object, @attribute, @id)
18
+ @wrapper_html = props.delete(:wrapper_html) || {}
19
+ define_wrapper_options
20
+ end
21
+
22
+ def define_wrapper_options
23
+ add(to: @wrapper_html, class: "required") if @props[:required].present?
24
+ add(to: @wrapper_html, class: @name) if @name.present?
25
+ end
26
+ 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
  #
@@ -1,36 +1,39 @@
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, :failure, :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
+
36
+ add class: styles[:colors][@color], to: @props, first_element: true
34
37
  add class: styles[:base], to: @props, first_element: true
35
38
  add class: styles[:sizes][@sizing], to: @props, first_element: true
36
39
  end
@@ -39,7 +42,7 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
39
42
  return "" if @helper_popover.nil?
40
43
 
41
44
  content_tag :span,
42
- anyicon(icon: "heroicons_solid:question-mark-circle", class: "w-4 h-4"),
45
+ anyicon(icon: @@helper_popover_icon, class: @@helper_popover_icon_class),
43
46
  {
44
47
  "data-popover-placement": @helper_popover_placement,
45
48
  "data-popover-target": target,
@@ -57,7 +60,7 @@ class Fluxbit::Form::LabelComponent < Fluxbit::Form::Component
57
60
  safe_join(
58
61
  [
59
62
  content_tag(:label, safe_join([ content || @with_content, span_helper_popover ]), @props),
60
- helper_text,
63
+ help_text,
61
64
  render_popover
62
65
  ]
63
66
  )
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::Form::RangeComponent` renders a styled range slider for selecting numeric values within a range.
4
+ # It supports vertical and horizontal orientation, sizing options, helper text, labels, and full compatibility with Rails form builders.
5
+ # Custom classes and HTML attributes can be passed for further styling and control.
6
+ #
7
+ # @example Basic usage
8
+ # = render Fluxbit::Form::RangeComponent.new(name: :volume, label: "Volume")
9
+ #
10
+ # @see docs/03_Forms/Range.md For detailed documentation and examples.
11
+ class Fluxbit::Form::RangeComponent < Fluxbit::Form::FieldComponent
12
+ include Fluxbit::Config::Form::RangeComponent
13
+
14
+ # Initializes the range component with the given properties.
15
+ #
16
+ # @param name [String] Name of the field (required unless using form builder)
17
+ # @param label [String] Label for the input (optional)
18
+ # @param value [Numeric] Value for the range input (optional)
19
+ # @param min [Numeric] Minimum value for the range slider (optional)
20
+ # @param max [Numeric] Maximum value for the range slider (optional)
21
+ # @param step [Numeric] Step value for the slider (optional)
22
+ # @param vertical [Boolean] Renders the slider vertically if true (default: false)
23
+ # @param sizing [Integer] Size index for slider height/thickness (default: config default)
24
+ # @param help_text [String] Helper or error text below the field
25
+ # @param class [String] Additional CSS classes for the input element
26
+ # @param ... any other HTML attribute supported by the <input type="range"> tag
27
+ def initialize(**props)
28
+ super(**props)
29
+ @vertical = options(@props.delete(:vertical), collection: [ true, false ], default: @@vertical)
30
+ @sizing = @props[:sizing].to_i || @@sizing
31
+ @sizing = (styles[:sizes].count - 1) if @sizing > (styles[:sizes].count - 1)
32
+ @props[:type] = "range"
33
+ @props[:style] = @props[:style] || "" + ";transform: rotate(270deg);" if @vertical
34
+
35
+ add(class: styles[:sizes][@sizing], to: @props, first_element: true)
36
+ add(class: styles[:base], to: @props, first_element: true)
37
+ end
38
+
39
+ def range
40
+ if @form.nil?
41
+ text_field_tag @name, @value, @props
42
+ else
43
+ @form.text_field(@attribute, **@props)
44
+ end
45
+ end
46
+
47
+ def call
48
+ content_tag :div, **@wrapper_html do
49
+ safe_join [ label, range, help_text ]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::Form::SelectComponent` is a styled dropdown/select field for forms.
4
+ # It supports standard, grouped, and time zone options, integrates with Rails form builders,
5
+ # and provides flexible props for prompt, disabled/selected options, helper text, and more.
6
+ #
7
+ # @example Basic usage
8
+ # = render Fluxbit::Form::SelectComponent.new(name: :role, options: ["Admin", "User", "Guest"], label: "User Role")
9
+ #
10
+ # @see docs/03_Forms/Select.md For detailed documentation and examples.
11
+ class Fluxbit::Form::SelectComponent < Fluxbit::Form::TextFieldComponent
12
+ # Initializes the select component with the given properties.
13
+ #
14
+ # @param name [String] Name of the field (required unless using form builder)
15
+ # @param label [String] Label for the input (optional)
16
+ # @param value [String] Value for the field (optional)
17
+ # @param grouped [Boolean] Enables grouped select options (default: false)
18
+ # @param time_zone [Boolean] Uses Rails time zone select options (default: false)
19
+ # @param select_options [Hash] Options for select tag (prompt, selected, disabled, etc)
20
+ # @param choices [Array] List of choices for options (alternative to options)
21
+ # @param options [Array, Hash] List or hash of options (or groups if grouped)
22
+ # @param help_text [String] Helper or error text below the field
23
+ # @param class [String] Additional CSS classes for the select element
24
+ # @param ... any other HTML attribute supported by <select>
25
+ def initialize(**props)
26
+ super(**props)
27
+ @grouped = @props.delete(:grouped) || false
28
+ @time_zone = @props.delete(:time_zone) || false
29
+ @select_options = @props.delete(:select_options) || {}
30
+ @choices = @props.delete(:choices) || nil
31
+ @options = @props.delete(:options) || {}
32
+ @options = ::ActiveSupport::TimeZone.all if @time_zone
33
+ end
34
+
35
+ def input
36
+ if @form.present? && @attribute.present?
37
+ @form.select(
38
+ @attribute,
39
+ build_options_for_select,
40
+ @select_options,
41
+ @props
42
+ )
43
+ else
44
+ select_tag(
45
+ @name,
46
+ build_options_for_select,
47
+ @props
48
+ )
49
+ end
50
+ end
51
+
52
+ def build_options_for_select
53
+ if @grouped
54
+ grouped_options_for_select(
55
+ @options,
56
+ @selected,
57
+ disabled: @disabled_options,
58
+ prompt: @prompt,
59
+ divider: @divider
60
+ )
61
+ elsif @time_zone
62
+ time_zone_options_for_select(@selected)
63
+ else
64
+ options_for_select(@options, selected: @selected, disabled: @disabled_options)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def grouped_selected_option
71
+ @options.each do |group|
72
+ group_to_traverse = @divider ? group[1] : group
73
+ if group_to_traverse.is_a?(String)
74
+ return group_to_traverse if group_to_traverse == @selected.to_s
75
+
76
+ next
77
+ end
78
+
79
+ group_to_traverse.each do |item|
80
+ if item.is_a?(Array) && item[1] == @selected.to_s
81
+ return item[0]
82
+ elsif item.is_a?(String) && item == @selected.to_s
83
+ return item
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The `Fluxbit::Form::TextFieldComponent` is a form input component that extends `Fluxbit::Form::FieldComponent`.
4
+ # It provides a styled text input (or textarea) with support for various HTML input types, optional icons or add-on content,
5
+ # and color-coded validation states (e.g. default, success, error).
6
+ #
7
+ # @example Basic usage
8
+ # = render Fluxbit::Form::TextFieldComponent.new(name: :email)
9
+ #
10
+ # @see docs/03_Forms/TextField.md For detailed documentation and examples.
11
+ class Fluxbit::Form::TextFieldComponent < Fluxbit::Form::FieldComponent
12
+ TYPE_DEFAULT = :text
13
+ TYPE_OPTIONS = %i[text textarea text_area color number email password search tel url date datetime_local month time week currency]
14
+ include Fluxbit::Config::Form::TextFieldComponent
15
+
16
+ # Initializes the text field component with the given properties.
17
+ #
18
+ # @param form [ActionView::Helpers::FormBuilder] The form builder (optional, for Rails forms)
19
+ # @param attribute [Symbol] The model attribute to be used in the form (required if using form builder)
20
+ # @param id [String] The id of the input element (optional)
21
+ # @param label [String] The label for the input field (optional)
22
+ # @param help_text [String] Additional help text for the input field (optional)
23
+ # @param helper_popover [String] Content for a popover helper (optional)
24
+ # @param helper_popover_placement [String] Placement of the popover (default: "right")
25
+ # @param name [String] Name of the field (required, unless using form builder)
26
+ # @param value [String] Value for the field (optional)
27
+ # @param type [Symbol] Input type (`:text`, `:email`, etc)
28
+ # @param icon [Symbol] Left icon (optional)
29
+ # @param right_icon [Symbol] Right icon (optional)
30
+ # @param addon [String] Add-on text or icon before the input (optional)
31
+ # @param addon_props [Hash] Props for the Add-on (optional)
32
+ # @param icon_props [Hash] Props for the left icon (optional)
33
+ # @param right_icon_props [Hash] Props for the right icon (optional)
34
+ # @param div_props [Hash] Props for the whole div (optional)
35
+ # @param multiline [Boolean] Renders a textarea if true
36
+ # @param color [Symbol] Field color (`:default`, `:success`, `:failure`, etc)
37
+ # @param sizing [Integer] Input size
38
+ # @param shadow [Boolean] Adds drop shadow if true
39
+ # @param disabled [Boolean] Disables the input if true
40
+ # @param readonly [Boolean] Makes the input readonly if true
41
+ # @param ... any other HTML attribute supported by input/textarea
42
+ def initialize(**props)
43
+ super(**props)
44
+ @color = valid_color(@props.delete(:color))
45
+ @type = options(@props.delete(:type), collection: TYPE_OPTIONS, default: TYPE_DEFAULT)
46
+ @icon = @props.delete(:icon)
47
+ @multiline = options(@props.delete(:multiline), default: false)
48
+ @shadow = @props.delete(:shadow)
49
+ @addon = @props.delete(:addon)
50
+ @right_icon = @props.delete(:right_icon)
51
+ @addon_props = @props.delete(:addon_props) || {}
52
+ @div_props = @props.delete(:div_props) || {}
53
+ @icon_props = @props.delete(:icon_props) || {}
54
+ @right_icon_props = @props.delete(:right_icon_props) || {}
55
+ @sizing = sizing_with_addon @props.delete(:sizing)
56
+ @props[:type] = @type
57
+
58
+ declare_classes
59
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
60
+ end
61
+
62
+ def call
63
+ content_tag :div, **@wrapper_html do
64
+ safe_join [ label, icon_container, help_text ]
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def valid_color(color)
71
+ return color if styles[:bg].key?(color)
72
+ return :failure if errors.present?
73
+
74
+ @@color
75
+ end
76
+
77
+ def sizing_with_addon(sizing)
78
+ sizing.to_i < styles[:sizes].count ? sizing.to_i : @@sizing
79
+ end
80
+
81
+ def declare_classes
82
+ add to: @props,
83
+ first_element: true,
84
+ class: [
85
+ styles[:default],
86
+ (@props.key?(:readonly) || @props.key?(:disabled) ? styles[:text][@color] : nil),
87
+ styles[:ring][@color],
88
+ styles[:bg][@color],
89
+ styles[:placeholder][@color],
90
+ styles[:border][@color],
91
+ @addon ? styles[:sizing_md_addon] : styles[:sizes][@sizing],
92
+ (@shadow ? styles[:shadow] : nil),
93
+ (@right_icon ? styles[:right_icon] : nil),
94
+ (@icon ? styles[:icon] : nil)
95
+ ].compact.join(" ")
96
+ end
97
+
98
+ def icon(icon_v, tag: :div, props: nil)
99
+ return "" if icon_v.blank?
100
+
101
+ content_tag(
102
+ tag,
103
+ anyicon(
104
+ icon: icon_v,
105
+ class: styles[:additional_icons][:class][@color]
106
+ ),
107
+ **props
108
+ )
109
+ end
110
+
111
+ def create_icon
112
+ add class: styles[:additional_icons][:icon], to: @icon_props
113
+ add(class: "pointer-events-none", to: @icon_props) unless events?(@icon_props)
114
+ icon(@icon, props: @icon_props)
115
+ end
116
+
117
+ def create_addon
118
+ add class: styles[:additional_icons][:addon][@color], to: @addon_props
119
+ icon(@addon, tag: :span, props: @addon_props)
120
+ end
121
+
122
+ def create_right_icon
123
+ add class: styles[:additional_icons][:right_icon], to: @right_icon_props
124
+ add(class: "pointer-events-none", to: @right_icon_props) unless events?(@right_icon_props)
125
+ icon(@right_icon, props: @right_icon_props)
126
+ end
127
+
128
+ def events?(props)
129
+ props.keys.intersection(
130
+ %i[onclick onsubmit onchange onkeydown onkeyup onkeypress href]
131
+ ).present?
132
+ end
133
+
134
+ def input
135
+ input_type = case @type
136
+ when :text
137
+ @multiline ? "text_area" : "text_field"
138
+ when :tel then "telephone_field"
139
+ when :currency then "text_field"
140
+ when :textarea, :text_area then "text_area"
141
+ else
142
+ "#{@type}_field"
143
+ end
144
+
145
+ if @form.present? && @attribute.present?
146
+ @form.public_send(input_type, @attribute, @props)
147
+ else
148
+ public_send("#{input_type}_tag", @name, @value, @props)
149
+ end
150
+ end
151
+
152
+ def icon_container_with_addon
153
+ add class: "flex", to: @div_props
154
+ content_tag :div, safe_join([ create_addon, create_right_icon, input ]), @div_props
155
+ end
156
+
157
+ def icon_container_without_addon
158
+ add class: "relative w-full", to: @div_props
159
+ content_tag :div, safe_join([ create_icon, create_right_icon, input ]), @div_props
160
+ end
161
+
162
+ def icon_container
163
+ return input if @icon.nil? && @right_icon.nil? && @addon.nil?
164
+ return icon_container_with_addon unless @addon.nil?
165
+
166
+ icon_container_without_addon
167
+ end
168
+ end