fluxbit_view_components 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) 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/component.rb +15 -8
  35. data/app/components/fluxbit/form/dropzone_component.rb +3 -3
  36. data/app/components/fluxbit/form/field_component.rb +4 -2
  37. data/app/components/fluxbit/form/help_text_component.rb +1 -1
  38. data/app/components/fluxbit/form/label_component.rb +10 -3
  39. data/app/components/fluxbit/form/password_component.rb +247 -0
  40. data/app/components/fluxbit/form/radio_group_button_component.rb +126 -0
  41. data/app/components/fluxbit/form/select_component.rb +108 -11
  42. data/app/components/fluxbit/form/text_field_component.rb +40 -23
  43. data/app/components/fluxbit/form/toggle_component.rb +2 -2
  44. data/app/components/fluxbit/form/upload_image_component.html.erb +3 -3
  45. data/app/components/fluxbit/form/upload_image_component.rb +12 -1
  46. data/app/components/fluxbit/gravatar_component.rb +7 -0
  47. data/app/components/fluxbit/icon_helpers.rb +167 -0
  48. data/app/components/fluxbit/link_component.rb +42 -0
  49. data/app/components/fluxbit/modal_component.rb +28 -31
  50. data/app/components/fluxbit/pagination_component.rb +206 -0
  51. data/app/components/fluxbit/popover_component.rb +14 -14
  52. data/app/components/fluxbit/progress_component.rb +196 -0
  53. data/app/components/fluxbit/skeleton_component.rb +237 -0
  54. data/app/components/fluxbit/speed_dial_action_component.html.erb +30 -0
  55. data/app/components/fluxbit/speed_dial_action_component.rb +59 -0
  56. data/app/components/fluxbit/speed_dial_component.html.erb +33 -0
  57. data/app/components/fluxbit/speed_dial_component.rb +73 -0
  58. data/app/components/fluxbit/spinner_component.rb +71 -0
  59. data/app/components/fluxbit/spinner_percent_component.rb +174 -0
  60. data/app/components/fluxbit/stepper_component.rb +223 -0
  61. data/app/components/fluxbit/tab_component.rb +44 -25
  62. data/app/components/fluxbit/table_component.rb +186 -0
  63. data/app/components/fluxbit/table_group_component.rb +28 -0
  64. data/app/components/fluxbit/theme_button_component.rb +64 -0
  65. data/app/components/fluxbit/timeline_component.rb +63 -0
  66. data/app/components/fluxbit/timeline_item_component.html.erb +64 -0
  67. data/app/components/fluxbit/timeline_item_component.rb +78 -0
  68. data/app/components/fluxbit/tooltip_component.rb +2 -2
  69. data/app/helpers/fluxbit/components_helper.rb +74 -4
  70. data/app/helpers/fluxbit/form_builder.rb +64 -15
  71. data/app/helpers/fluxbit/view_helper.rb +71 -0
  72. data/config/locales/en.yml +37 -4
  73. data/config/locales/pt-BR.yml +36 -0
  74. data/lib/fluxbit/config/accordion_component.rb +73 -0
  75. data/lib/fluxbit/config/avatar_component.rb +11 -11
  76. data/lib/fluxbit/config/badge_component.rb +14 -11
  77. data/lib/fluxbit/config/banner_component.rb +60 -0
  78. data/lib/fluxbit/config/bottom_navigation_component.rb +74 -0
  79. data/lib/fluxbit/config/breadcrumb_component.rb +24 -0
  80. data/lib/fluxbit/config/button_component.rb +6 -4
  81. data/lib/fluxbit/config/card_component.rb +23 -12
  82. data/lib/fluxbit/config/carousel_component.rb +33 -0
  83. data/lib/fluxbit/config/drawer_component.rb +48 -0
  84. data/lib/fluxbit/config/dropdown_component.rb +29 -0
  85. data/lib/fluxbit/config/form/check_box_component.rb +1 -1
  86. data/lib/fluxbit/config/form/dropzone_component.rb +1 -1
  87. data/lib/fluxbit/config/form/help_text_component.rb +1 -1
  88. data/lib/fluxbit/config/form/label_component.rb +3 -2
  89. data/lib/fluxbit/config/form/password_component.rb +19 -0
  90. data/lib/fluxbit/config/form/radio_group_button_component.rb +24 -0
  91. data/lib/fluxbit/config/form/text_field_component.rb +11 -11
  92. data/lib/fluxbit/config/form/toggle_component.rb +5 -5
  93. data/lib/fluxbit/config/link_component.rb +24 -0
  94. data/lib/fluxbit/config/modal_component.rb +1 -1
  95. data/lib/fluxbit/config/pagination_component.rb +31 -0
  96. data/lib/fluxbit/config/popover_component.rb +1 -1
  97. data/lib/fluxbit/config/progress_component.rb +63 -0
  98. data/lib/fluxbit/config/skeleton_component.rb +82 -0
  99. data/lib/fluxbit/config/speed_dial_component.rb +50 -0
  100. data/lib/fluxbit/config/spinner_component.rb +30 -0
  101. data/lib/fluxbit/config/spinner_percent_component.rb +61 -0
  102. data/lib/fluxbit/config/stepper_component.rb +299 -0
  103. data/lib/fluxbit/config/tab_component.rb +6 -0
  104. data/lib/fluxbit/config/table_component.rb +75 -0
  105. data/lib/fluxbit/config/theme_button_component.rb +19 -0
  106. data/lib/fluxbit/config/timeline_component.rb +77 -0
  107. data/lib/fluxbit/view_components/engine.rb +11 -3
  108. data/lib/fluxbit/view_components/version.rb +1 -1
  109. data/lib/fluxbit/view_components.rb +20 -0
  110. data/lib/generators/fluxbit/devise_views_generator.rb +116 -0
  111. data/lib/generators/fluxbit/pagy_generator.rb +39 -0
  112. data/lib/generators/fluxbit/scaffold_generator.rb +165 -0
  113. data/lib/generators/fluxbit/templates/_alert.html.erb.tt +1 -0
  114. data/lib/generators/fluxbit/templates/_flash.html.erb.tt +15 -0
  115. data/lib/generators/fluxbit/templates/_form.html.erb.tt +38 -0
  116. data/lib/generators/fluxbit/templates/_metadata.html.erb.tt +44 -0
  117. data/lib/generators/fluxbit/templates/controller.rb.tt +406 -0
  118. data/lib/generators/fluxbit/templates/create.turbo_stream.erb.tt +7 -0
  119. data/lib/generators/fluxbit/templates/destroy.turbo_stream.erb.tt +3 -0
  120. data/lib/generators/fluxbit/templates/destroy_all.turbo_stream.erb.tt +9 -0
  121. data/lib/generators/fluxbit/templates/devise_views/confirmations/new.html.erb +11 -0
  122. data/lib/generators/fluxbit/templates/devise_views/layouts/devise.html.erb +64 -0
  123. data/lib/generators/fluxbit/templates/devise_views/mailer/confirmation_instructions.html.erb +5 -0
  124. data/lib/generators/fluxbit/templates/devise_views/mailer/email_changed.html.erb +7 -0
  125. data/lib/generators/fluxbit/templates/devise_views/mailer/password_changed.html.erb +3 -0
  126. data/lib/generators/fluxbit/templates/devise_views/mailer/reset_password_instructions.html.erb +8 -0
  127. data/lib/generators/fluxbit/templates/devise_views/mailer/unlock_instructions.html.erb +7 -0
  128. data/lib/generators/fluxbit/templates/devise_views/passwords/edit.html.erb +29 -0
  129. data/lib/generators/fluxbit/templates/devise_views/passwords/new.html.erb +11 -0
  130. data/lib/generators/fluxbit/templates/devise_views/registrations/edit.html.erb +43 -0
  131. data/lib/generators/fluxbit/templates/devise_views/registrations/new.html.erb +34 -0
  132. data/lib/generators/fluxbit/templates/devise_views/sessions/new.html.erb +15 -0
  133. data/lib/generators/fluxbit/templates/devise_views/shared/_error_messages.html.erb +14 -0
  134. data/lib/generators/fluxbit/templates/devise_views/shared/_links.html.erb +25 -0
  135. data/lib/generators/fluxbit/templates/devise_views/unlocks/new.html.erb +11 -0
  136. data/lib/generators/fluxbit/templates/edit.html.erb.tt +47 -0
  137. data/lib/generators/fluxbit/templates/fluxbit_pagy.css +27 -0
  138. data/lib/generators/fluxbit/templates/i18n.en.yml.tt +121 -0
  139. data/lib/generators/fluxbit/templates/i18n.pt-BR.yml.tt +121 -0
  140. data/lib/generators/fluxbit/templates/index.html.erb.tt +254 -0
  141. data/lib/generators/fluxbit/templates/index.json.jbuilder.tt +33 -0
  142. data/lib/generators/fluxbit/templates/new.html.erb.tt +47 -0
  143. data/lib/generators/fluxbit/templates/partial.html.erb.tt +61 -0
  144. data/lib/generators/fluxbit/templates/policy.rb.tt +36 -0
  145. data/lib/generators/fluxbit/templates/send_alert_via_drawer.erb.tt +10 -0
  146. data/lib/generators/fluxbit/templates/show.html.erb.tt +44 -0
  147. data/lib/generators/fluxbit/templates/show.json.jbuilder.tt +6 -0
  148. data/lib/generators/fluxbit/templates/update.turbo_stream.erb.tt +10 -0
  149. data/lib/generators/fluxbit/templates/update_all.turbo_stream.erb.tt +20 -0
  150. data/lib/install/install.rb +58 -0
  151. metadata +107 -18
  152. data/app/helpers/fluxbit/classes_helper.rb +0 -9
@@ -0,0 +1,206 @@
1
+ # The `Fluxbit::PaginationComponent` is a component for rendering customizable pagination controls.
2
+ # It extends `Fluxbit::Component` and provides options for configuring the pagination's
3
+ # appearance, behavior, and content areas. You can control the pagination's layout, item count,
4
+ # and other interactive elements. The pagination is divided into different sections (previous, next, etc.),
5
+ # each of which can be styled or customized through various properties.
6
+ class Fluxbit::PaginationComponent < Fluxbit::Component
7
+ include Fluxbit::Config::PaginationComponent
8
+
9
+ def initialize(pagy = nil, **props)
10
+ @pagy = pagy
11
+
12
+ @props = props
13
+ @count = @props.delete(:count) || 0
14
+ @last = @props.delete(:last) || 1
15
+ @next = @props.delete(:next)
16
+ @page = @props.delete(:page) || 1
17
+ @prev = @props.delete(:prev)
18
+ @size = @props.delete(:size) || :default
19
+ @ends = @props.delete(:ends) || true
20
+ @request_path = @props.delete(:request_path) || nil
21
+
22
+ if @pagy
23
+ @count = @pagy.count
24
+ @last = @pagy.last
25
+ @next = @pagy.next
26
+ @page = @pagy.page
27
+ @prev = @pagy.prev
28
+ @size = @pagy.vars[:size]
29
+ @ends = @pagy.vars[:ends]
30
+ @request_path = @pagy.vars[:request_path]
31
+ end
32
+
33
+ unless @size.is_a?(Integer) && @size >= 0
34
+ raise ArgumentError, "expected :size to be an Integer >= 0, got #{@size.inspect} (#{@size.class})"
35
+ end
36
+
37
+ @show_first_last = options @props.delete(:show_first_last), default: @@show_first_last
38
+ @show_prev_next = options @props.delete(:show_prev_next), default: @@show_prev_next
39
+ @show_pages = options @props.delete(:show_pages), default: @@show_pages
40
+ @show_icons = options @props.delete(:show_icons), default: @@show_icons
41
+ @show_texts = options @props.delete(:show_texts), default: @@show_texts
42
+ @sizing = options @props.delete(:sizing), default: @@sizing
43
+ @aria_label = @props.delete(:aria_label) || translate("aria_label.nav", count: @last)
44
+ @show_texts = true if !@show_icons && !@show_texts
45
+
46
+ add(class: [ styles[:root], styles[:sizes][@sizing][:root] ], to: @props)
47
+ @page_link_style = [ styles[:page_link], styles[:sizes][@sizing][:page_link] ].join(" ")
48
+ @current_style = [ styles[:current], styles[:sizes][@sizing][:page_link] ].join(" ")
49
+ @props[:aria] ||= {}
50
+ @props[:aria][:label] = @aria_label unless @props[:aria][:label]
51
+ end
52
+
53
+ def call
54
+ tag.nav(**@props) do
55
+ concat first_button if @show_first_last
56
+ concat prev_button if @show_prev_next
57
+
58
+ if @show_pages
59
+ series.each do |item|
60
+ case item
61
+ when Integer
62
+ concat(tag.a(item.to_s, href: url_for(item), role: "link", class: @page_link_style, aria: { label: item.to_s }))
63
+ when String
64
+ concat(tag.a(item.to_s, role: "link", class: @current_style, aria: { disabled: true, current: "page" }))
65
+ when :gap
66
+ concat(tag.a(ellipsis_horizontal, role: "link", class: @page_link_style, aria: { disabled: true }))
67
+ end
68
+ end
69
+ end
70
+
71
+ concat next_button if @show_prev_next
72
+ concat last_button if @show_first_last
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def translate(key, options = {})
79
+ I18n.t(key, **options.merge(scope: "fluxbit.pagination")) # , default: Fluxbit::DEFAULT_TRANSLATIONS["fluxbit.pagination.#{key}"]))
80
+ end
81
+
82
+ def first_button
83
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.first") } }
84
+ if @page != 1
85
+ props[:href] = url_for(1)
86
+ else
87
+ props[:aria][:disabled] = true
88
+ add class: styles[:disabled], to: props, first_element: true
89
+ end
90
+ add class: styles[:previous], to: props
91
+
92
+ tag.a(**props) do
93
+ concat(chevron_double_left) if @show_icons
94
+ concat(tag.span(
95
+ translate("first"),
96
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_prev] : styles[:only_text]) : styles[:only_icon]
97
+ ))
98
+ end
99
+ end
100
+
101
+ def last_button
102
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.last") } }
103
+ if @page != @last
104
+ props[:href] = url_for(@last)
105
+ else
106
+ props[:aria][:disabled] = true
107
+ add class: styles[:disabled], to: props, first_element: true
108
+ end
109
+ add class: styles[:next], to: props
110
+
111
+ tag.a(**props) do
112
+ concat(tag.span(
113
+ translate("last"),
114
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_next] : styles[:only_text]) : styles[:only_icon]
115
+ ))
116
+ concat(chevron_double_right) if @show_icons
117
+ end
118
+ end
119
+
120
+ def prev_button
121
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.prev") } }
122
+ if prev_page = @prev
123
+ props[:href] = url_for(prev_page)
124
+ else
125
+ props[:aria][:disabled] = true
126
+ add class: styles[:disabled], to: props, first_element: true
127
+ end
128
+ add(class: styles[:previous], to: props) unless @show_first_last
129
+
130
+ tag.a(**props) do
131
+ concat(chevron_left) if @show_icons
132
+ concat(tag.span(
133
+ translate("prev"),
134
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_prev] : styles[:only_text]) : styles[:only_icon]
135
+ )
136
+ )
137
+ end
138
+ end
139
+
140
+ def next_button
141
+ props = { role: "link", class: @page_link_style, aria: { label: translate("aria_label.next") } }
142
+ if next_page = @next
143
+ props[:href] = url_for(next_page)
144
+ else
145
+ props[:aria][:disabled] = true
146
+ add class: styles[:disabled], to: props, first_element: true
147
+ end
148
+ add(class: styles[:next], to: props) unless @show_first_last
149
+
150
+ tag.a(**props) do
151
+ concat(tag.span(
152
+ translate("next"),
153
+ class: @show_texts ? (@show_icons ? styles[:text_with_icon_next] : styles[:only_text]) : styles[:only_icon]
154
+ ))
155
+ concat(chevron_right) if @show_icons
156
+ end
157
+ end
158
+
159
+ def series
160
+ return @pagy.series(size: @size) if @pagy && @pagy.respond_to?(:series, true)
161
+ return [] if @size.zero?
162
+
163
+ [].tap do |series|
164
+ if @size >= @last
165
+ series.push(*1..@last)
166
+ else
167
+ left = ((@size - 1) / 2.0).floor # left half might be 1 page shorter for even size
168
+ start = if @page <= left # beginning pages
169
+ 1
170
+ elsif @page > (@last - @size + left) # end pages
171
+ @last - @size + 1
172
+ else # intermediate pages
173
+ @page - left
174
+ end
175
+ series.push(*start...start + @size)
176
+ # Set first and last pages plus gaps when needed, respecting the size
177
+ if @ends && @size >= 7
178
+ series[0] = 1
179
+ series[1] = :gap unless series[1] == 2
180
+ series[-2] = :gap unless series[-2] == @last - 1
181
+ series[-1] = @last
182
+ end
183
+ end
184
+ series[series.index(@page)] = @page.to_s
185
+ end
186
+ end
187
+
188
+ def url_for(page)
189
+ vars = @pagy&.vars || {}
190
+ # Use current request parameters as base
191
+ params = (respond_to?(:request) ? request.GET : controller.request.GET).dup
192
+ params.merge!(vars[:params].transform_keys(&:to_s)) if vars[:params].is_a?(Hash)
193
+ # Set page and possibly limit
194
+ params[vars[:page_param].to_s] = page
195
+ params[vars[:limit_param].to_s] = vars[:limit] if vars[:limit_extra]
196
+ # Apply params proc if given
197
+ params = vars[:params].call(params) if vars[:params].is_a?(Proc)
198
+
199
+ # Build query string
200
+ query_str = params.any? ? "?#{Rack::Utils.build_nested_query(params)}" : ""
201
+ # Base path (use stored request_path or current path)
202
+ base_path = @request_path || (respond_to?(:request) ? request.path : controller.request.path)
203
+ base_path = "#{request.base_url}#{base_path}" if vars[:absolute]
204
+ "#{base_path}#{query_str}#{vars[:fragment] || ''}"
205
+ end
206
+ end
@@ -16,7 +16,7 @@ class Fluxbit::PopoverComponent < Fluxbit::Component
16
16
  # @option props [Boolean] :has_arrow (true) Determines if an arrow should be displayed on the popover.
17
17
  # @option props [String] :image (nil) The URL of an image to be displayed in the popover.
18
18
  # @option props [Symbol] :image_position (:right) The position of the image relative to the content (:left or :right).
19
- # @option props [Hash] :image_props ({}) Additional HTML attributes for the image element.
19
+ # @option props [Hash] :image_html ({}) Additional HTML attributes for the image element.
20
20
  # @option props [Symbol, String] :size (2) The size of the popover (0 to 4).
21
21
  # @option props [String] :remove_class ('') Classes to be removed from the default popover class list.
22
22
  # @option props [Hash] **props Remaining options declared as HTML attributes, applied to the popover container.
@@ -27,31 +27,31 @@ class Fluxbit::PopoverComponent < Fluxbit::Component
27
27
  @has_arrow = options @props.delete(:has_arrow), default: @@has_arrow
28
28
  @image = @props.delete(:image)
29
29
  @image_position = options @props.delete(:image_position), default: @@image_position
30
- @image_props = options @props.delete(:image_props), default: @@image_props
30
+ @image_html = options @props.delete(:image_html), default: @@image_html
31
31
  @props["data-popover"] = "data-popover"
32
32
  @props["role"] = "tooltip"
33
33
 
34
34
  add(class: [ styles[:base], styles[:size][@props.delete(:size) || @@size] ], to: @props)
35
- add(class: styles[:image_content][:image], to: @image_props)
36
- @image_props[:src] = @image
35
+ add(class: styles[:image_content][:image], to: @image_html)
36
+ @image_html[:src] = @image
37
37
 
38
38
  @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
39
- @image_props[:class] = remove_class(@props.delete(:remove_class) || "", @image_props[:class])
39
+ @image_html[:class] = remove_class(@props.delete(:remove_class) || "", @image_html[:class])
40
40
  end
41
41
 
42
42
  def call
43
- content_tag :div, @props do
43
+ tag.div(**@props) do
44
44
  concat div_title unless @title.blank?
45
- concat (content_tag(:div, class: styles[@image.blank? ? :content : :image_base]) do
45
+ concat (tag.div(class: styles[@image.blank? ? :content : :image_base]) do
46
46
  if @image.blank?
47
47
  content
48
48
  else
49
49
  if @image_position == :left
50
- concat content_tag(:img, nil, @image_props)
51
- concat content_tag(:div, content, class: styles[:image_content][:text])
50
+ concat tag.img(**@image_html)
51
+ concat tag.div(content, class: styles[:image_content][:text])
52
52
  else
53
- concat content_tag(:div, content, class: styles[:image_content][:text])
54
- concat content_tag(:img, nil, @image_props)
53
+ concat tag.div(content, class: styles[:image_content][:text])
54
+ concat tag.img(**@image_html)
55
55
  end
56
56
  end
57
57
  end)
@@ -60,12 +60,12 @@ class Fluxbit::PopoverComponent < Fluxbit::Component
60
60
  end
61
61
 
62
62
  def popper_arrow
63
- content_tag :div, "", "data-popper-arrow" => true
63
+ tag.div("data-popper-arrow" => true)
64
64
  end
65
65
 
66
66
  def div_title
67
- content_tag :div, class: styles[:title][:div] do
68
- content_tag :h3, @title, class: styles[:title][:h3]
67
+ tag.div(class: styles[:title][:div]) do
68
+ tag.h3(@title, class: styles[:title][:h3])
69
69
  end
70
70
  end
71
71
  end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The `Fluxbit::ProgressComponent` is a customizable progress bar component that extends `Fluxbit::Component`.
5
+ # It allows you to create progress indicators with various styles, sizes, colors, and label positioning options
6
+ # to display completion status or loading progress.
7
+ #
8
+ # @example Basic usage
9
+ # = fx_progress(progress: 45)
10
+ #
11
+ # @example With labels
12
+ # = fx_progress(progress: 75, text_label: "Loading", label_progress: true)
13
+ #
14
+ # @see docs/02_Components/Progress.md For detailed documentation.
15
+ class Fluxbit::ProgressComponent < Fluxbit::Component
16
+ include Fluxbit::Config::ProgressComponent
17
+
18
+ ##
19
+ # Initializes the progress component with the given properties.
20
+ #
21
+ # @param [Hash] **props The properties to customize the component.
22
+ # @option props [Integer] :progress (0) The progress percentage (0-100).
23
+ # @option props [Symbol, String] :color (:default) The color theme of the progress bar.
24
+ # @option props [Integer] :size (1) The size of the progress bar (0-3).
25
+ # @option props [String] :text_label (nil) Label text to display with the progress bar.
26
+ # @option props [Boolean] :label_progress (false) Whether to show the progress percentage.
27
+ # @option props [Boolean] :label_text (false) Whether to show the text label.
28
+ # @option props [Symbol] :progress_label_position (:inside) Position of progress label (:inside or :outside).
29
+ # @option props [Symbol] :text_label_position (:outside) Position of text label (:inside or :outside).
30
+ # @option props [Hash] :label_html ({}) HTML attributes for label elements. Supports :remove_class.
31
+ # @option props [Boolean] :stimulus (false) Whether to add Stimulus controller data attributes for JavaScript interactions.
32
+ # @option props [String] :remove_class ('') CSS classes to remove from the default class list.
33
+ # @option props [Hash] **props Remaining options as HTML attributes.
34
+ #
35
+ # @return [Fluxbit::ProgressComponent]
36
+ def initialize(**props)
37
+ super
38
+ @props = props
39
+
40
+ # Use options() function with config defaults
41
+ @progress = options(@props.delete(:progress), default: @@progress)
42
+ @color = options(@props.delete(:color), collection: styles[:bar][:colors].keys, default: @@color)
43
+ @size = options(@props.delete(:size), default: @@size)
44
+ @text_label = options(@props.delete(:text_label), default: @@text_label)
45
+ @label_progress = options(@props.delete(:label_progress), default: @@label_progress)
46
+ @label_text = options(@props.delete(:label_text), default: @@label_text)
47
+ @progress_label_position = options(@props.delete(:progress_label_position),
48
+ collection: [ :inside, :outside ],
49
+ default: @@progress_label_position)
50
+ @text_label_position = options(@props.delete(:text_label_position),
51
+ collection: [ :inside, :outside ],
52
+ default: @@text_label_position)
53
+ @label_html = options(@props.delete(:label_html), default: @@label_html)
54
+ @stimulus = options(@props.delete(:stimulus), default: @@stimulus)
55
+
56
+ # Sanitize progress value
57
+ @progress = [ @progress.to_i, 0 ].max
58
+ @progress = [ @progress, 100 ].min
59
+
60
+ # Apply styling
61
+ declare_classes
62
+
63
+ # Handle class removal
64
+ @props[:class] = remove_class(@props.delete(:remove_class) || "", @props[:class])
65
+ end
66
+
67
+ def call
68
+ content_tag(:div) do
69
+ safe_join([
70
+ render_outside_labels,
71
+ render_progress_container
72
+ ].compact)
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def declare_classes
79
+ add(class: styles[:base], to: @props, first_element: true)
80
+ add(class: styles[:sizes][@size], to: @props, first_element: true)
81
+
82
+ # Add Stimulus controller attributes if enabled
83
+ if @stimulus
84
+ @props["data-controller"] = [ @props["data-controller"], "fx-progress" ].compact.join(" ")
85
+ @props["data-fx-progress-progress-value"] = @progress
86
+ @props["data-fx-progress-animate-value"] = true
87
+ end
88
+ end
89
+
90
+ def render_outside_labels
91
+ return unless has_outside_labels?
92
+
93
+ content_tag(:div, class: styles[:labels][:outside][:base]) do
94
+ safe_join([
95
+ render_outside_text_label,
96
+ render_outside_progress_label
97
+ ].compact)
98
+ end
99
+ end
100
+
101
+ def render_outside_text_label
102
+ return unless @label_text && @text_label.present? && @text_label_position == :outside
103
+
104
+ label_props = build_label_props(styles[:labels][:outside][:text], is_progress_label: false)
105
+ content_tag(:span, @text_label, **label_props)
106
+ end
107
+
108
+ def render_outside_progress_label
109
+ return unless @label_progress && @progress_label_position == :outside
110
+
111
+ label_props = build_label_props(styles[:labels][:outside][:progress], is_progress_label: true)
112
+ content_tag(:span, "#{@progress}%", **label_props)
113
+ end
114
+
115
+ def render_progress_container
116
+ content_tag(:div, **@props) do
117
+ content_tag(:div, progress_bar_content, **progress_bar_props)
118
+ end
119
+ end
120
+
121
+ def progress_bar_props
122
+ bar_props = {
123
+ class: progress_bar_classes,
124
+ style: "width: #{@progress}%"
125
+ }
126
+
127
+ # Add Stimulus target if enabled
128
+ if @stimulus
129
+ bar_props["data-fx-progress-target"] = "bar"
130
+ end
131
+
132
+ bar_props
133
+ end
134
+
135
+ def progress_bar_classes
136
+ classes = [ styles[:bar][:base] ]
137
+ classes << styles[:bar][:colors][@color]
138
+ classes << styles[:bar][:text_sizes][@size] if has_inside_labels?
139
+ classes.join(" ")
140
+ end
141
+
142
+ def progress_bar_content
143
+ return "" unless has_inside_labels?
144
+
145
+ safe_join([
146
+ render_inside_text_label,
147
+ render_inside_progress_label
148
+ ].compact, " ")
149
+ end
150
+
151
+ def render_inside_text_label
152
+ return unless @label_text && @text_label.present? && @text_label_position == :inside
153
+
154
+ label_props = build_label_props(styles[:labels][:inside][:text], is_progress_label: false)
155
+ content_tag(:span, @text_label, **label_props)
156
+ end
157
+
158
+ def render_inside_progress_label
159
+ return unless @label_progress && @progress_label_position == :inside
160
+
161
+ label_props = build_label_props(styles[:labels][:inside][:progress], is_progress_label: true)
162
+ content_tag(:span, "#{@progress}%", **label_props)
163
+ end
164
+
165
+ def has_outside_labels?
166
+ (@label_text && @text_label.present? && @text_label_position == :outside) ||
167
+ (@label_progress && @progress_label_position == :outside)
168
+ end
169
+
170
+ def has_inside_labels?
171
+ (@label_text && @text_label.present? && @text_label_position == :inside) ||
172
+ (@label_progress && @progress_label_position == :inside)
173
+ end
174
+
175
+ def build_label_props(base_class, is_progress_label: false)
176
+ label_props = @label_html.dup
177
+
178
+ # Start with base class and merge with any custom classes
179
+ label_props[:class] = base_class
180
+ add(class: @label_html[:class], to: label_props) if @label_html[:class].present?
181
+
182
+ # Add Stimulus target if enabled and this is a progress label
183
+ if @stimulus && is_progress_label
184
+ label_props["data-fx-progress-target"] = "progressLabel"
185
+ elsif @stimulus && !is_progress_label
186
+ label_props["data-fx-progress-target"] = "textLabel"
187
+ end
188
+
189
+ # Handle remove_class for labels
190
+ if label_props[:remove_class].present?
191
+ label_props[:class] = remove_class(label_props.delete(:remove_class), label_props[:class])
192
+ end
193
+
194
+ label_props
195
+ end
196
+ end