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
@@ -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
@@ -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,185 @@
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, 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")
19
+ # @param value [String] Value for the field (optional)
20
+ # @param grouped [Boolean] Enables grouped select options (default: false)
21
+ # @param time_zone [Boolean] Uses Rails time zone select options (default: false)
22
+ # @param select_options [Hash] Options for select tag (prompt, selected, disabled, etc)
23
+ # @param choices [Array] List of choices for options (alternative to options)
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)
31
+ # @param class [String] Additional CSS classes for the select element
32
+ # @param ... any other HTML attribute supported by <select>
33
+ def initialize(**props)
34
+ super(**props)
35
+ @grouped = @props.delete(:grouped) || false
36
+ @time_zone = @props.delete(:time_zone) || false
37
+ @select_options = @props.delete(:select_options) || {}
38
+ @choices = @props.delete(:choices) || nil
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
+
47
+ @options = ::ActiveSupport::TimeZone.all if @time_zone
48
+
49
+ # Define prompt with I18n support
50
+ define_prompt(prompt_value)
51
+ end
52
+
53
+ def input
54
+ if @form.present? && @attribute.present?
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
69
+ else
70
+ # select_tag(name, option_tags = nil, options = {})
71
+ # option_tags should be pre-formatted HTML
72
+ select_tag(
73
+ @name,
74
+ build_options_for_select,
75
+ @props
76
+ )
77
+ end
78
+ end
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
97
+ def build_options_for_select
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
108
+ grouped_options_for_select(
109
+ @options,
110
+ @selected,
111
+ disabled: @disabled_options,
112
+ divider: @divider
113
+ )
114
+ elsif @time_zone
115
+ time_zone_options_for_select(@selected)
116
+ else
117
+ options_for_select(@options, selected: @selected, disabled: @disabled_options)
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
124
+ end
125
+
126
+ private
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
+
167
+ def grouped_selected_option
168
+ @options.each do |group|
169
+ group_to_traverse = @divider ? group[1] : group
170
+ if group_to_traverse.is_a?(String)
171
+ return group_to_traverse if group_to_traverse == @selected.to_s
172
+
173
+ next
174
+ end
175
+
176
+ group_to_traverse.each do |item|
177
+ if item.is_a?(Array) && item[1] == @selected.to_s
178
+ return item[0]
179
+ elsif item.is_a?(String) && item == @selected.to_s
180
+ return item
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,185 @@
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 placeholder [String] Placeholder text for the input field (optional, defaults to I18n if object/attribute present, pass false to disable)
28
+ # @param type [Symbol] Input type (`:text`, `:email`, etc)
29
+ # @param icon [Symbol] Left icon (optional)
30
+ # @param right_icon [Symbol] Right icon (optional)
31
+ # @param addon [String] Add-on text or icon before the input (optional)
32
+ # @param addon_html [Hash] Props for the Add-on (optional)
33
+ # @param icon_html [Hash] Props for the left icon (optional)
34
+ # @param right_icon_html [Hash] Props for the right icon (optional)
35
+ # @param div_html [Hash] Props for the whole div (optional)
36
+ # @param multiline [Boolean] Renders a textarea if true
37
+ # @param color [Symbol] Field color (`:default`, `:success`, `:danger`, etc)
38
+ # @param sizing [Integer] Input size
39
+ # @param shadow [Boolean] Adds drop shadow if true
40
+ # @param disabled [Boolean] Disables the input if true
41
+ # @param readonly [Boolean] Makes the input readonly if true
42
+ # @param ... any other HTML attribute supported by input/textarea
43
+ def initialize(**props)
44
+ super(**props)
45
+ @color = valid_color(@props.delete(:color))
46
+ @type = options(@props.delete(:type), collection: TYPE_OPTIONS, default: TYPE_DEFAULT)
47
+ @icon = @props.delete(:icon)
48
+ @multiline = options(@props.delete(:multiline), default: false)
49
+ @shadow = @props.delete(:shadow)
50
+ @addon = @props.delete(:addon)
51
+ @right_icon = @props.delete(:right_icon)
52
+ @addon_html = @props.delete(:addon_html) || {}
53
+ @div_html = @props.delete(:div_html) || {}
54
+ @icon_html = @props.delete(:icon_html) || {}
55
+ @right_icon_html = @props.delete(:right_icon_html) || {}
56
+ @sizing = sizing_with_addon @props.delete(:sizing)
57
+ @props[:type] = @type
58
+
59
+ define_placeholder(@props.delete(:placeholder))
60
+ declare_classes
61
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
62
+ end
63
+
64
+ def call
65
+ content_tag :div, **@wrapper_html do
66
+ safe_join [ label, icon_container, help_text ]
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ def define_placeholder(placeholder)
73
+ return if placeholder.is_a?(FalseClass)
74
+
75
+ if placeholder.nil? && @object.present? && @attribute.present?
76
+ placeholder = I18n.t(
77
+ @attribute,
78
+ scope: [ @object.class.name.pluralize.underscore.to_sym, :placeholders ],
79
+ default: nil
80
+ )
81
+ end
82
+
83
+ @props[:placeholder] = placeholder if placeholder.present?
84
+ end
85
+
86
+ def valid_color(color)
87
+ return color if styles[:bg].key?(color)
88
+ return :danger if errors.present?
89
+
90
+ @@color
91
+ end
92
+
93
+ def sizing_with_addon(sizing)
94
+ sizing.to_i < styles[:sizes].count ? sizing.to_i : @@sizing
95
+ end
96
+
97
+ def declare_classes
98
+ add to: @props,
99
+ first_element: true,
100
+ class: [
101
+ styles[:default],
102
+ (@props.key?(:readonly) || @props.key?(:disabled) ? styles[:text][@color] : nil),
103
+ styles[:ring][@color],
104
+ styles[:bg][@color],
105
+ styles[:placeholder][@color],
106
+ styles[:border][@color],
107
+ @addon ? styles[:sizing_md_addon] : styles[:sizes][@sizing],
108
+ (@shadow ? styles[:shadow] : nil),
109
+ (@right_icon ? styles[:right_icon] : nil),
110
+ (@icon ? styles[:icon] : nil)
111
+ ].compact.join(" ")
112
+ end
113
+
114
+ def icon(icon_v, tag: :div, props: nil)
115
+ return "" if icon_v.blank?
116
+
117
+ content_tag(
118
+ tag,
119
+ anyicon(
120
+ icon_v,
121
+ class: styles[:additional_icons][:class][@color]
122
+ ),
123
+ **props
124
+ )
125
+ end
126
+
127
+ def create_icon
128
+ add class: styles[:additional_icons][:icon], to: @icon_html
129
+ add(class: "pointer-events-none", to: @icon_html) unless events?(@icon_html)
130
+ icon(@icon, props: @icon_html)
131
+ end
132
+
133
+ def create_addon
134
+ add class: styles[:additional_icons][:addon][@color], to: @addon_html
135
+ icon(@addon, tag: :span, props: @addon_html)
136
+ end
137
+
138
+ def create_right_icon
139
+ add class: styles[:additional_icons][:right_icon], to: @right_icon_html
140
+ add(class: "pointer-events-none", to: @right_icon_html) unless events?(@right_icon_html)
141
+ icon(@right_icon, props: @right_icon_html)
142
+ end
143
+
144
+ def events?(props)
145
+ props.keys.intersection(
146
+ %i[onclick onsubmit onchange onkeydown onkeyup onkeypress href]
147
+ ).present?
148
+ end
149
+
150
+ def input
151
+ input_type = case @type
152
+ when :text
153
+ @multiline ? "text_area" : "text_field"
154
+ when :tel then "telephone_field"
155
+ when :currency then "text_field"
156
+ when :textarea, :text_area then "text_area"
157
+ else
158
+ "#{@type}_field"
159
+ end
160
+
161
+ if @form.present? && @attribute.present?
162
+ @props[:value] = @value if @value.present?
163
+ @form.public_send(input_type, @attribute, @props)
164
+ else
165
+ public_send("#{input_type}_tag", @name, @value, @props)
166
+ end
167
+ end
168
+
169
+ def icon_container_with_addon
170
+ add class: "flex", to: @div_html
171
+ content_tag :div, safe_join([ create_addon, create_right_icon, input ]), @div_html
172
+ end
173
+
174
+ def icon_container_without_addon
175
+ add class: "relative w-full", to: @div_html
176
+ content_tag :div, safe_join([ create_icon, create_right_icon, input ]), @div_html
177
+ end
178
+
179
+ def icon_container
180
+ return input if @icon.nil? && @right_icon.nil? && @addon.nil?
181
+ return icon_container_with_addon unless @addon.nil?
182
+
183
+ icon_container_without_addon
184
+ end
185
+ end
@@ -0,0 +1,23 @@
1
+ <%= content_tag :div, **@wrapper_html do %>
2
+ <label class="<%= label_class %>">
3
+ <%= if @invert_label
4
+ label
5
+ else
6
+ @other_label || (other_label if other_label?)
7
+ end %>
8
+
9
+ <% if @form.present? && @attribute.present? %>
10
+ <%= @form.check_box(@attribute, **@props) %>
11
+ <% else %>
12
+ <%= check_box_tag(@name, **@props) %>
13
+ <% end %>
14
+
15
+ <span class="<%= toggle_class %>"></span>
16
+ <%= if @invert_label
17
+ @other_label || (other_label if other_label?)
18
+ else
19
+ label
20
+ end %>
21
+ </label>
22
+ <%= help_text %>
23
+ <% end %>
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ # The `Fluxbit::Form::ToggleComponent` renders a styled switch/toggle (on/off) input.
5
+ # It supports custom label placement, color and sizing options, helper text, and is fully compatible
6
+ # with Rails form builders. Additional options allow you to invert label order, customize colors
7
+ # for checked/unchecked/button states, and provide an extra label via a slot.
8
+ #
9
+ # @example Basic usage
10
+ # = render Fluxbit::Form::ToggleComponent.new(name: :enabled, label: "Enabled?")
11
+ #
12
+ # @see docs/03_Forms/Toggle.md For detailed documentation and examples.
13
+ class Fluxbit::Form::ToggleComponent < Fluxbit::Form::FieldComponent
14
+ include Fluxbit::Config::Form::ToggleComponent
15
+
16
+ renders_one :other_label, "Fluxbit::Form::LabelComponent"
17
+
18
+ # Initializes the toggle component with the given properties.
19
+ #
20
+ # @param form [ActionView::Helpers::FormBuilder] The form builder (optional, for Rails forms)
21
+ # @param attribute [Symbol] The model attribute to be used in the form (required if using form builder)
22
+ # @param id [String] The id of the input element (optional)
23
+ # @param label [String] The label for the input field (optional)
24
+ # @param help_text [String] Additional help text for the input field (optional)
25
+ # @param helper_popover [String] Content for a popover helper (optional)
26
+ # @param helper_popover_placement [String] Placement of the popover (default: "right")
27
+ # @param name [String] Name of the field (required unless using form builder)
28
+ # @param other_label [String] Additional label, rendered via slot (optional)
29
+ # @param sizing [Integer] Size index for the toggle (default: config)
30
+ # @param color [Symbol] Checked toggle color (:default, :success, :danger, :info, :warning, etc)
31
+ # @param unchecked_color [Symbol] Unchecked toggle color (see config)
32
+ # @param button_color [Symbol] Color for the toggle button
33
+ # @param invert_label [Boolean] If true, inverts label/toggle order (default: config)
34
+ # @param disabled [Boolean] Disables the toggle if true
35
+ # @param class [String] Additional CSS classes for the input element
36
+ # @param ... any other HTML attribute supported by <input type="checkbox">
37
+ def initialize(**props)
38
+ super(**props)
39
+
40
+ @other_label = props.delete(:other_label)
41
+ @sizing = @props.delete(:sizing) || @@sizing
42
+ @sizing = (styles[:toggle][:sizes].count - 1) if @sizing > (styles[:toggle][:sizes].count - 1)
43
+ @color = valid_color(@props.delete(:color))
44
+ @unchecked_color = options(
45
+ @props.delete(:unchecked_color),
46
+ collection: styles[:toggle][:unchecked].keys,
47
+ default: @@unchecked_color
48
+ )
49
+ @button_color = options(@props.delete(:button_color), collection: styles[:toggle][:button].keys, default: @@button_color)
50
+ @invert_label = options(@props.delete(:invert_label), collection: [ true, false ], default: @@invert_label)
51
+
52
+ add to: @props, first_element: true, class: styles[:input]
53
+ end
54
+
55
+ def valid_color(color)
56
+ return color if styles[:toggle][:checked].key?(color)
57
+ return :danger if errors.present?
58
+
59
+ @@color
60
+ end
61
+
62
+ def label_class
63
+ styles[:label]
64
+ end
65
+
66
+ def input_class
67
+ styles[:input]
68
+ end
69
+
70
+ def toggle_class
71
+ [
72
+ (@invert_label || @other_label || other_label?) ? styles[:toggle][:invert_label] : nil,
73
+ styles[:toggle][:base],
74
+ styles[:toggle][:unchecked][@unchecked_color],
75
+ styles[:toggle][:checked][@color],
76
+ styles[:toggle][:button][@button_color],
77
+ styles[:toggle][:sizes][@sizing],
78
+ styles[:toggle][:active][(@props[:disabled] ? :off : :on)]
79
+ ].compact.join(" ")
80
+ end
81
+ end